开源堡垒机Guacamole二次开发记录之二

这篇主要记录录屏和SFTP的实现。

录屏及视频播放

对于录屏及录屏的播放,因为我们的项目中需要把guacd和java后端分开两台服务器部署,而guacamole的录屏是通过guacd程序录制的。我的要求是在Java后端直接把录好的视频文件通过http前端播放,因此需要把录屏放在Java端的服务器上。 

首先稍微修改一下guacamole-common的源码,添加几个可重载的函数,分别是向前端下发ws消息,向guacd上传前端消息以及ws连接关闭的地方。

GuacamoleWebSocketTunnelEndpoint类的onMessage函数中,添加receiveData(message);

try {// Write received messagewriter.write(message.toCharArray());receiveData(message);}catch (GuacamoleConnectionClosedException e) {logger.debug("Connection to guacd closed.", e);}catch (GuacamoleException e) {logger.debug("WebSocket tunnel write failed.", e);}tunnel.releaseWriter();

onClose函数中添加closeConnect函数调用。

public void onClose(Session session, CloseReason closeReason) {try {if (tunnel != null)tunnel.close();closeConnect();}catch (GuacamoleException e) {logger.debug("Unable to close WebSocket tunnel.", e);}}

定义两个可重载的函数

protected void receiveData(String message) {//logger.info("GuacamoleWebSocketTunnelEndpoint-receiveData");}protected void closeConnect() {//logger.info("GuacamoleWebSocketTunnelEndpoint-receiveData");}

在Java工程的WebSocketTunnel类中重载函数

receiveData函数用于记录鼠标键盘事件

@Overrideprotected void receiveData(String message)  {//logger.info("WebSocketTunnel-receiveData : " + message);
//        try {
//            userConnectLogEntity.getBufferedWriter2().write(message);
//            userConnectLogEntity.getBufferedWriter2().newLine();
//            userConnectLogEntity.getBufferedWriter2().flush();
//        } catch (IOException e) {
//            throw new RuntimeException(e);
//        }}

sendInstruction函数,对将要发送给前端的报文进行拦截处理,重点是最后的几行,把报文记录在一个文件中。

Overrideprotected void sendInstruction(String instruction) throws IOException {if(instruction.startsWith("0.,36.")) {uuid = instruction.substring(6, instruction.length()-1);System.out.println("uuid: "+uuid);TunnelStream tunnelStream = new TunnelStream();tunnelStream.setWebSocketTunnel(this);tunnelStream.setEnd(false);tunnelStream.setBuffer(null);streamMap.tunnelStreamMap.put(uuid, tunnelStream);streamMap.tunnelStreamMap.get(uuid).setOk(false);}else if(instruction.contains("application/octet-stream")) {fileTranfer = true;GuacamoleParser parser = new GuacamoleParser();int parsed;int offset = 0;int length = instruction.toCharArray().length;while (true) {try {if (!((parsed = parser.append(instruction.toCharArray(), offset, length)) != 0))break;}catch (GuacamoleException e) {throw new RuntimeException(e);}offset += parsed;length -= parsed;}GuacamoleInstruction ins = parser.next();synchronized (bufferInstructions) {bufferInstructions.put(ins.getArgs().get(0), ins);}}else if(instruction.contains("17.SFTP: File opened")) {streamMap.tunnelStreamMap.get(uuid).setOk(true);}else if(instruction.contains("8.SFTP: OK")) {streamMap.tunnelStreamMap.get(uuid).setOk(true);}else {if(fileTranfer) {if(instruction.startsWith("4.blob")) {int num1 = instruction.indexOf(",");int num2 = instruction.indexOf(",", num1+1);int num3 = instruction.indexOf(".", num1+1);int id = Integer.parseInt(instruction.substring(num3+1, num2));int num4 = instruction.indexOf(".", num2+1);String str = instruction.substring(num4+1, instruction.length()-1);TunnelStream tunnelStream = streamMap.tunnelStreamMap.get(uuid);if(tunnelStream != null) {synchronized(streamMap) {streamMap.tunnelStreamMap.get(uuid).setBuffer(str);}instruction = instruction.substring(0, num2+1) + "0.;";}}else if(instruction.startsWith("3.end")) {System.out.println("3.end");fileTranfer = false;TunnelStream tunnelStream = streamMap.tunnelStreamMap.get(uuid);synchronized(streamMap) {streamMap.tunnelStreamMap.get(uuid).setEnd(true);}}}}super.sendInstruction(instruction);if(!instruction.startsWith("0.")) {userConnectLogEntity.getBufferedWriter().write(instruction);}}

closeConnect函数,用于ws连接断开时,记录日志,启动线程进行录屏文件的转换。sendInstruction函数中记录了下发的报文,通过调用guacenc程序把日志转换成m4v格式的视频文件。

@Overrideprotected void closeConnect() {try {streamMap.tunnelStreamMap.remove(uuid);userConnectLogEntity.getBufferedWriter().flush();userConnectLogEntity.getBufferedWriter().close();userConnectLogEntity.setEtime(new Date(System.currentTimeMillis()));userConnectLogEntity.setPeriod((int)(userConnectLogEntity.getEtime().getTime()-userConnectLogEntity.getStime().getTime()) / 1000);Thread thread = new MyThread(userConnectLogEntity, userConnectLogService);thread.start();} catch (IOException e) {throw new RuntimeException(e);}}

 视频转换线程

public class MyThread extends Thread {private UserConnectLogEntity userConnectLogEntity;private IUserConnectLogService userConnectLogService;public MyThread(UserConnectLogEntity userConnectLogEntity, IUserConnectLogService userConnectLogService) {this.userConnectLogEntity = userConnectLogEntity;this.userConnectLogService = userConnectLogService;}public void run() {try {String fileName = userConnectLogEntity.getVideo().substring(0, userConnectLogEntity.getVideo().length()-4);String str = "guacenc -s 1024x768 -r 300000 -f " + fileName;Process process = Runtime.getRuntime().exec(str);process.waitFor();logger.info("转换视频完成: " + fileName);}catch (Exception e) {logger.error(e.getMessage(), e);}String str = userConnectLogEntity.getVideo();int num1 = str.lastIndexOf(File.separator);int num2 = str.lastIndexOf(File.separator, num1-1);userConnectLogEntity.setVideo("/video"+str.substring(num2));userConnectLogService.updateById(userConnectLogEntity);}}

 把视频文件暴露给web端

@Configuration
public class WebAppConfig extends WebMvcConfigurerAdapter {@Value("${fileserver.videofolder}")private String videoFolder;@Overridepublic void addResourceHandlers(ResourceHandlerRegistry registry) {registry.addResourceHandler("/video/**").addResourceLocations("file:"+videoFolder);super.addResourceHandlers(registry);}
}

这样视频文件直接通过web链接就可以在浏览器中播放。

另外要说明一点的是,默认的guacenc程序转换出来的视频文件在浏览器中是无法播放的,视频的内部格式不对,需要修改一下guacamole-server的源码重新编译一下。

guacamole-server-1.5.1\src\guacenc\guacenc.c文件,121行左右,修改一下视频格式重新编译。

//if (guacenc_encode(path, out_path, "mpeg4", width, height, bitrate, force))
// 修改为
if (guacenc_encode(path, out_path, "libx264", width, height, bitrate, force))

SFTP实现

SFTP的实现较为复杂,需要对SFTP上传下载的流程及guacamole封装的协议有较好的了解,才能实现。

文件列表

文件列表相对简单些,通过查看guacamole的前端代码,基本可以了解其流程,自己再按照流程重新写一下前端就行。

实现Guacamole.Client的onfilesystem的响应

guac.onfilesystem = function(object, name) {filesystemObject = object;currentPath = name;listDirectory(currentPath);};

获取文件列表的函数 ,主要是调用filesystemObject.requestInputStream、sendAck

listDirectory(path) {filesystemObject.requestInputStream(path, function handleStream(stream, mimetype) {// Ignore stream if mimetype is wrongif (mimetype !== Guacamole.Object.STREAM_INDEX_MIMETYPE) {stream.sendAck('Unexpected mimetype', Guacamole.Status.Code.UNSUPPORTED);return;}currentPath = path;let exchangePath = path.replace(/^\//,'')folders = exchangePath.length ? exchangePath.split('/') : []paths = []folders.reduce((tmp, item, index) => {let path = tmp+"/"+itemlet obj = {path: path,folder: item}paths.push(obj)return path }, "")// Signal server that data is ready to be receivedstream.sendAck('Ready', Guacamole.Status.Code.SUCCESS);// Read stream as JSONlet reader = new Guacamole.JSONReader(stream);// Acknowledge received JSON blobsreader.onprogress = function onprogress() {stream.sendAck("Received", Guacamole.Status.Code.SUCCESS);};// Reset contents of directoryreader.onend = function jsonReady() {fileList = []// For each received stream namevar mimetypes = reader.getJSON();for (var name in mimetypes) {if (name.substring(0, path.length) !== path){continue;}var filename = name.substring(path.length);if(path.substring(path.length-1) != '/'){filename = name.substring(path.length+1);}let one = {}one.path = filenameif (mimetypes[name] === Guacamole.Object.STREAM_INDEX_MIMETYPE) {one.type="folder"}else {one.type="file"}one.fullpath = namefileList.push(one)}};});
},

上传下载

上传下载,首先得搞清楚整体得流程,

通过wireshark抓包,可以查看guacd与java后端的通信报文,

通过浏览器自带的调试工具,可以查看前端和Java后端之间的websocket通信报文,

通过上面两个工具的抓包分析,分析出上传下载的流程。

文件下载流程:

  1. 首先前端通过websocket发送3.get报文,java后端接受到后,直接发往guacd服务端;然后前端再通过http接口发送文件请求到Java端;
  2. guacd回复application/octet-stream报文给Java后端,Java后端直接向guacd端回复ack消息,不向前端转发;
  3. guacd端开始发送4.blob文件段,Java端接收到后,将4.blob报文的实际blob字段截取下来,通过WebSocket向前端回复截取后的报文,同时通过HTTP接口向前端发送实际的文件段;
  4. 前端websocket接受到blob消息后,回复ack,Java端转发给guacd,guacd再发下一段文件,循环这个过程直到文件发送完毕。
  5. 最后guacd端发送end报文,java端通过websocket转发给前端,整个下载过程结束。

文件上传流程:

  1. 前端发送put指令,Java端接收到后,直接转给guacd端
  2. guacd端回复File Opened消息通知文件已准备好,可以写入
  3. 前端通过Http post 发送MultipartFile给java端,java端接收到后转发给guacd端
  4. guacd端回复SFTP OK消息
  5. 前端发送下一段,循环发送直到文件发送完成,最后Java端发送end命令给guacd端
  6. guacd端回复OK消息,整个文件上传流程结束

下面是代码实现的大致流程:

前端下载代码,先通过filesystemObject.requestInputStream发送下载请求,再通过iframe挂一个http get请求开始下载文件,中间通过stream.onblob事件回复Ack消息,通过stream.onend事件结束下载流程

downloadfile(path){filesystemObject.requestInputStream(path, function downloadStreamReceived(stream, mimetype) {// Parse filename from stringvar filename = path.match(/(.*[\\/])?(.*)/)[2];var url = '/tunnels/' + uuid + '/sessions/' + stream.index + '/files/' + filename;// Create temporary hidden iframe to facilitate downloadvar iframe = document.createElement('iframe');iframe.style.display = 'none';// The iframe MUST be part of the DOM for the download to occurdocument.body.appendChild(iframe);iframe.onload = function downloadComplete() {document.body.removeChild(iframe);};// Acknowledge (and ignore) any received blobsstream.onblob = function acknowledgeData() {stream.sendAck('OK', Guacamole.Status.Code.SUCCESS);};// Automatically remove iframe from DOM a few seconds after the stream// ends, in the browser does NOT fire the "load" event for downloadsstream.onend = function downloadComplete() {window.setTimeout(function cleanupIframe() {if (iframe.parentElement) {document.body.removeChild(iframe);}}, 5000);};// Begin downloadiframe.src = url;});
}

前端上传文件代码,file类型input的change事件响应函数。通过filesystemObject.createOutputStream发送文件上传请求,通过XMLHttpRequest post 发送文件给Java端,

changFile(event){let file1 = event.target.files[0];var stream = filesystemObject.createOutputStream(file1.type, currentPath+'/'+file1.name);stream.onack = function beginUpload(status) {if (status.isError()) {return;}}var fd = new FormData();fd.append('file', file1);var url = '/tunnels/' + uuid + '/sessions/' + stream.index;const xhr = new XMLHttpRequest();xhr.open('POST', url);xhr.send(fd);xhr.onreadystatechange = function () {if (xhr.readyState === 4 && xhr.status === 200) {console.log('上传成功');updateDirectory(currentPath);}}
},

接下来是java端的http接口,

下载文件接口,主要是通过ServletOutputStream向前端写文件流。文件流实际是在websocket处理函数中接收的,这儿guacamole通过消息过滤等方式实现了,比较复杂。我这儿简单粗暴的用了全局的公共变量实现,每个websocket实例接受到文件段后,保存到一个公共缓冲区中,再置一个标志位,http controller这儿,循环判断标准位,取出文件段,向前端写文件流。

@GetMapping("/tunnels/{tnid}/sessions/{snid}/files/{filename}")
public void download(@PathVariable("tnid")String tnid, @PathVariable("snid")String snid, @PathVariable("filename")String filename, HttpServletResponse response) {try {System.out.println("download controller: "+tnid);response.setCharacterEncoding("UTF-8");response.addHeader("Content-Disposition", "attachment;filename=" + URLEncoder.encode(filename, "UTF-8"));ServletOutputStream os = response.getOutputStream();if(streamMap.tunnelStreamMap.get(tnid) != null) {streamMap.tunnelStreamMap.get(tnid).getWebSocketTunnel().startSendFile(snid);streamMap.tunnelStreamMap.get(tnid).setEnd(false);streamMap.tunnelStreamMap.get(tnid).setBuffer(null);long start = System.currentTimeMillis();while(!streamMap.tunnelStreamMap.get(tnid).isEnd()){synchronized(streamMap) {String str = streamMap.tunnelStreamMap.get(tnid).getBuffer();if (str != null) {streamMap.tunnelStreamMap.get(tnid).setBuffer(null);os.write(decoder.decode(str.getBytes()));}}}}os.close();}catch (Exception e) {throw new RuntimeException(e);}
}

上传文件接口。同样通过公共的Bean和websocket线程同步消息

@PostMapping("/tunnels/{tnid}/sessions/{snid}")
public void upload(@RequestParam("file") MultipartFile uploadFile, @PathVariable("tnid")String tnid, @PathVariable("snid")String snid) {try {InputStream inputStream = uploadFile.getInputStream();byte[] buffer = new byte[8192];int bytesRead = 0;while ((bytesRead = inputStream.read(buffer, 0, buffer.length)) != -1) {long start = System.currentTimeMillis();while(!streamMap.tunnelStreamMap.get(tnid).isOk()) {// 等待上传完成消息}streamMap.tunnelStreamMap.get(tnid).setOk(false);System.out.println(bytesRead);byte[] bb = null;if(bytesRead < 8192) {bb = new byte[bytesRead];System.arraycopy(buffer, 0, bb, 0, bytesRead);}else {bb = buffer;}if(streamMap.tunnelStreamMap.get(tnid) != null) {streamMap.tunnelStreamMap.get(tnid).getWebSocketTunnel().sendBlob(snid, bb);}}if(streamMap.tunnelStreamMap.get(tnid) != null) {streamMap.tunnelStreamMap.get(tnid).getWebSocketTunnel().sendEnd(snid);}inputStream.close();}catch (Exception e) {throw new RuntimeException(e);}
}

 websocket处理部分,注意和http controller的同步

@Override
protected void sendInstruction(String instruction) throws IOException {if(instruction.startsWith("0.,36.")) {uuid = instruction.substring(6, instruction.length()-1);System.out.println("uuid: "+uuid);TunnelStream tunnelStream = new TunnelStream();tunnelStream.setWebSocketTunnel(this);tunnelStream.setEnd(false);tunnelStream.setBuffer(null);streamMap.tunnelStreamMap.put(uuid, tunnelStream);streamMap.tunnelStreamMap.get(uuid).setOk(false);}else if(instruction.contains("application/octet-stream")) {fileTranfer = true;GuacamoleParser parser = new GuacamoleParser();int parsed;int offset = 0;int length = instruction.toCharArray().length;while (true) {try {if (!((parsed = parser.append(instruction.toCharArray(), offset, length)) != 0))break;}catch (GuacamoleException e) {throw new RuntimeException(e);}offset += parsed;length -= parsed;}GuacamoleInstruction ins = parser.next();synchronized (bufferInstructions) {bufferInstructions.put(ins.getArgs().get(0), ins);}}else if(instruction.contains("17.SFTP: File opened")) {streamMap.tunnelStreamMap.get(uuid).setOk(true);}else if(instruction.contains("8.SFTP: OK")) {streamMap.tunnelStreamMap.get(uuid).setOk(true);}else {if(fileTranfer) {if(instruction.startsWith("4.blob")) {int num1 = instruction.indexOf(",");int num2 = instruction.indexOf(",", num1+1);int num3 = instruction.indexOf(".", num1+1);int id = Integer.parseInt(instruction.substring(num3+1, num2));int num4 = instruction.indexOf(".", num2+1);String str = instruction.substring(num4+1, instruction.length()-1);TunnelStream tunnelStream = streamMap.tunnelStreamMap.get(uuid);if(tunnelStream != null) {synchronized(streamMap) {streamMap.tunnelStreamMap.get(uuid).setBuffer(str);}instruction = instruction.substring(0, num2+1) + "0.;";}}else if(instruction.startsWith("3.end")) {System.out.println("3.end");fileTranfer = false;//int num1 = instruction.indexOf(".", 3);//int id = Integer.parseInt(instruction.substring(num1+1, instruction.length()-1));TunnelStream tunnelStream = streamMap.tunnelStreamMap.get(uuid);synchronized(streamMap) {streamMap.tunnelStreamMap.get(uuid).setEnd(true);}}}}super.sendInstruction(instruction);if(!instruction.startsWith("0.")) {userConnectLogEntity.getBufferedWriter().write(instruction);}
}public void startSendFile(String sid) {acknowledgeStream(sid);
}@Override
protected void receiveData(String message)  {
}public void sendBlob(String sid, byte[] bytes) {GuacamoleWriter writer = guacamoleTunnel.acquireWriter();GuacamoleInstruction ins = new GuacamoleInstruction("blob", sid, BaseEncoding.base64().encode(bytes));try {writer.writeInstruction(ins);}catch (GuacamoleException e) {logger.debug("Unable to send \"{}\" for intercepted stream.", ins.getOpcode(), e);}guacamoleTunnel.releaseWriter();
}public void sendEnd(String sid) {GuacamoleWriter writer = guacamoleTunnel.acquireWriter();GuacamoleInstruction ins = new GuacamoleInstruction("end", sid);try {writer.writeInstruction(ins);}catch (GuacamoleException e) {logger.debug("Unable to send \"{}\" for intercepted stream.", ins.getOpcode(), e);}guacamoleTunnel.releaseWriter();
}protected void acknowledgeStream(String sid) {GuacamoleInstruction ins = null;synchronized (bufferInstructions) {ins = bufferInstructions.remove(sid);}if(ins != null) {GuacamoleWriter writer = guacamoleTunnel.acquireWriter();try {writer.writeInstruction(new GuacamoleInstruction("ack", ins.getArgs().get(0), "OK",Integer.toString(GuacamoleStatus.SUCCESS.getGuacamoleStatusCode())));}catch (GuacamoleException e) {throw new RuntimeException(e);}guacamoleTunnel.releaseWriter();}
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/19571.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

数据结构(王卓版)——线性表

数据的存储结构之线性表 1、线性表的定义和特点

Python 图书管理系统 GUI界面 (源码在最后)

部分效果图&#xff1a; 部分源码&#xff1a; def creatPage(self):self.addPage AddFrame(self.root)#录入界面self.deletePage DeleteFrame(self.root) #删除界面self.countPage CountFrame(self.root) #统计界面self.register_admirPage Register_admirFrame(self.root…

内卷浪潮中的必考证书——CISP,抓住机会迅速上岸

近年来&#xff0c;随着网络空间安全日趋复杂&#xff0c;网络攻击、病毒入侵、信息失窃、信息泄密事件日益严重&#xff0c;信息安全保障工作被迅速提到了战略高度&#xff0c;能够构建全面的安全体系从而保障组织信息资产安全的专业人才非常紧缺&#xff0c;注册专业资质证书…

【运维知识进阶篇】Zabbix5.0稳定版详解10(Zabbix自动注册+Ansible自动部署,实现一条命令监控任意主机)

当我们的Zabbix自动注册Ansible自动部署在一起时&#xff0c;会碰出什么样的火花&#xff0c;答案就是可以实现执行ansible的一条命令&#xff0c;监控任意一台或多台主机。 目录 一、配置好自动注册规则 二、编写Ansible playbook 三、运行Ansible&#xff0c;查看监控效果…

【 Python 全栈开发 - 人工智能篇 - 41 】线性回归算法

文章目录 一、简介1.1 什么是线性回归&#xff1f;1.2 线性回归在人工智能中的应用预测分析特征工程异常检测 1.3 Python 在人工智能中的角色数据处理和分析机器学习和深度学习自然语言处理 二、理解线性回归2.1 线性回归的基本原理2.2 线性回归模型的假设2.3 线性回归的评估指…

Codeforces Round 875 (Div. 1) A. Copil Copac Draws Trees

题意 Copil Copac 给定了一个由 n−1 条边组成的列表&#xff0c;该列表描述了一棵由 n 个顶点组成的树。他决定用下面的算法来绘制它&#xff1a; 步骤 0&#xff1a;绘制第一个顶点&#xff08;顶点1&#xff09;。转到步骤1。 步骤 1&#xff1a;对于输入中的每一条边&#…

字符串模式匹配算法(暴力破解、KMP、BM、Sunday)

目录 暴力破解 KMP 算法 构造 next 数组 KMP代码 BM 算法 Sunday 算法 参考资料 又通过leetcode复习了之前的知识:找出字符串中第一个匹配项的下标 暴力破解 你的面前有两段序列 S 和 T&#xff0c;你需要判断 T 是否可以匹配成为 S 的子串。 你可能会凭肉眼立即得出结…

【亲测】python 安装 pillow报错 如何处理

今天在新系统上安装pillow库&#xff0c;提示错误&#xff1a; WARNING: Retrying (Retry(total4, connectNone, readNone, redirectNone, statusNone)) after connection broken by SSLError(SSLEOFError(8, EOF occurred in violation of protocol (_ssl.c:997))): /simple/…

Spring MVC文件上传

Spring MVC文件上传 Spring MVC 框架的文件上传基于 commons-fileupload 组件&#xff0c;并在该组件上做了进一步的封装&#xff0c;简化了文件上传的代码实现&#xff0c;取消了不同上传组件上的编程差异。 1. MultipartResolver接口 在 Spring MVC 中实现文件上传十分容易…

Python爬虫学习笔记(一)————网页基础

目录 1.网页的组成 2.HTML &#xff08;1&#xff09;标签 &#xff08;2&#xff09;比较重要且常用的标签&#xff1a; ①列表标签 ②超链接标签 &#xff08;a标签&#xff09; ③img标签&#xff1a;用于渲染&#xff0c;图片资源的标签 ④div标签和span标签 &…

超级应用App的建设路径:业务功能小程序化

过往硅谷巨头对于「微信」这样的「超级应用」不屑一顾&#xff0c;如今Super App似乎已经成为巨头间的一个新共识&#xff0c;Meta、Snap、Uber等公司逐步将更多功能塞进现有App。 Facebook 做起了约会、招聘&#xff1b;Snap 则实打实学起了微信的「平台战略」&#xff0c;开始…

k8s中网络通讯简单介绍

1 前言 Kubernetes的网络模型假定了所有的pod都在一个可以直接连通的扁平的网络空间中&#xff0c;这在GCE&#xff08;Google Compute Engine&#xff09;里面是现成的网络模型&#xff0c;Kubernetes假设这定这个网络已经存在。但是在私有云里搭建Kubernetes集群&#xff0c;…