当前位置:   article > 正文

开源堡垒机Guacamole二次开发记录之二_基于guacamole堡垒机 开源

基于guacamole堡垒机 开源

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

录屏及视频播放

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

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

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

  1. try {
  2. // Write received message
  3. writer.write(message.toCharArray());
  4. receiveData(message);
  5. }
  6. catch (GuacamoleConnectionClosedException e) {
  7. logger.debug("Connection to guacd closed.", e);
  8. }
  9. catch (GuacamoleException e) {
  10. logger.debug("WebSocket tunnel write failed.", e);
  11. }
  12. tunnel.releaseWriter();

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

  1. public void onClose(Session session, CloseReason closeReason) {
  2. try {
  3. if (tunnel != null)
  4. tunnel.close();
  5. closeConnect();
  6. }
  7. catch (GuacamoleException e) {
  8. logger.debug("Unable to close WebSocket tunnel.", e);
  9. }
  10. }

定义两个可重载的函数

  1. protected void receiveData(String message) {
  2. //logger.info("GuacamoleWebSocketTunnelEndpoint-receiveData");
  3. }
  4. protected void closeConnect() {
  5. //logger.info("GuacamoleWebSocketTunnelEndpoint-receiveData");
  6. }

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

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

  1. @Override
  2. protected void receiveData(String message) {
  3. //logger.info("WebSocketTunnel-receiveData : " + message);
  4. // try {
  5. // userConnectLogEntity.getBufferedWriter2().write(message);
  6. // userConnectLogEntity.getBufferedWriter2().newLine();
  7. // userConnectLogEntity.getBufferedWriter2().flush();
  8. // } catch (IOException e) {
  9. // throw new RuntimeException(e);
  10. // }
  11. }

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

  1. Override
  2. protected void sendInstruction(String instruction) throws IOException {
  3. if(instruction.startsWith("0.,36.")) {
  4. uuid = instruction.substring(6, instruction.length()-1);
  5. System.out.println("uuid: "+uuid);
  6. TunnelStream tunnelStream = new TunnelStream();
  7. tunnelStream.setWebSocketTunnel(this);
  8. tunnelStream.setEnd(false);
  9. tunnelStream.setBuffer(null);
  10. streamMap.tunnelStreamMap.put(uuid, tunnelStream);
  11. streamMap.tunnelStreamMap.get(uuid).setOk(false);
  12. }
  13. else if(instruction.contains("application/octet-stream")) {
  14. fileTranfer = true;
  15. GuacamoleParser parser = new GuacamoleParser();
  16. int parsed;
  17. int offset = 0;
  18. int length = instruction.toCharArray().length;
  19. while (true) {
  20. try {
  21. if (!((parsed = parser.append(instruction.toCharArray(), offset, length)) != 0))
  22. break;
  23. }
  24. catch (GuacamoleException e) {
  25. throw new RuntimeException(e);
  26. }
  27. offset += parsed;
  28. length -= parsed;
  29. }
  30. GuacamoleInstruction ins = parser.next();
  31. synchronized (bufferInstructions) {
  32. bufferInstructions.put(ins.getArgs().get(0), ins);
  33. }
  34. }
  35. else if(instruction.contains("17.SFTP: File opened")) {
  36. streamMap.tunnelStreamMap.get(uuid).setOk(true);
  37. }
  38. else if(instruction.contains("8.SFTP: OK")) {
  39. streamMap.tunnelStreamMap.get(uuid).setOk(true);
  40. }
  41. else {
  42. if(fileTranfer) {
  43. if(instruction.startsWith("4.blob")) {
  44. int num1 = instruction.indexOf(",");
  45. int num2 = instruction.indexOf(",", num1+1);
  46. int num3 = instruction.indexOf(".", num1+1);
  47. int id = Integer.parseInt(instruction.substring(num3+1, num2));
  48. int num4 = instruction.indexOf(".", num2+1);
  49. String str = instruction.substring(num4+1, instruction.length()-1);
  50. TunnelStream tunnelStream = streamMap.tunnelStreamMap.get(uuid);
  51. if(tunnelStream != null) {
  52. synchronized(streamMap) {
  53. streamMap.tunnelStreamMap.get(uuid).setBuffer(str);
  54. }
  55. instruction = instruction.substring(0, num2+1) + "0.;";
  56. }
  57. }
  58. else if(instruction.startsWith("3.end")) {
  59. System.out.println("3.end");
  60. fileTranfer = false;
  61. TunnelStream tunnelStream = streamMap.tunnelStreamMap.get(uuid);
  62. synchronized(streamMap) {
  63. streamMap.tunnelStreamMap.get(uuid).setEnd(true);
  64. }
  65. }
  66. }
  67. }
  68. super.sendInstruction(instruction);
  69. if(!instruction.startsWith("0.")) {
  70. userConnectLogEntity.getBufferedWriter().write(instruction);
  71. }
  72. }

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

  1. @Override
  2. protected void closeConnect() {
  3. try {
  4. streamMap.tunnelStreamMap.remove(uuid);
  5. userConnectLogEntity.getBufferedWriter().flush();
  6. userConnectLogEntity.getBufferedWriter().close();
  7. userConnectLogEntity.setEtime(new Date(System.currentTimeMillis()));
  8. userConnectLogEntity.setPeriod((int)(userConnectLogEntity.getEtime().getTime()-userConnectLogEntity.getStime().getTime()) / 1000);
  9. Thread thread = new MyThread(userConnectLogEntity, userConnectLogService);
  10. thread.start();
  11. } catch (IOException e) {
  12. throw new RuntimeException(e);
  13. }
  14. }

 视频转换线程

  1. public class MyThread extends Thread {
  2. private UserConnectLogEntity userConnectLogEntity;
  3. private IUserConnectLogService userConnectLogService;
  4. public MyThread(UserConnectLogEntity userConnectLogEntity, IUserConnectLogService userConnectLogService) {
  5. this.userConnectLogEntity = userConnectLogEntity;
  6. this.userConnectLogService = userConnectLogService;
  7. }
  8. public void run() {
  9. try {
  10. String fileName = userConnectLogEntity.getVideo().substring(0, userConnectLogEntity.getVideo().length()-4);
  11. String str = "guacenc -s 1024x768 -r 300000 -f " + fileName;
  12. Process process = Runtime.getRuntime().exec(str);
  13. process.waitFor();
  14. logger.info("转换视频完成: " + fileName);
  15. }
  16. catch (Exception e) {
  17. logger.error(e.getMessage(), e);
  18. }
  19. String str = userConnectLogEntity.getVideo();
  20. int num1 = str.lastIndexOf(File.separator);
  21. int num2 = str.lastIndexOf(File.separator, num1-1);
  22. userConnectLogEntity.setVideo("/video"+str.substring(num2));
  23. userConnectLogService.updateById(userConnectLogEntity);
  24. }
  25. }

 把视频文件暴露给web端

  1. @Configuration
  2. public class WebAppConfig extends WebMvcConfigurerAdapter {
  3. @Value("${fileserver.videofolder}")
  4. private String videoFolder;
  5. @Override
  6. public void addResourceHandlers(ResourceHandlerRegistry registry) {
  7. registry.addResourceHandler("/video/**").addResourceLocations("file:"+videoFolder);
  8. super.addResourceHandlers(registry);
  9. }
  10. }

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

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

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

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

SFTP实现

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

文件列表

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

实现Guacamole.Client的onfilesystem的响应

  1. guac.onfilesystem = function(object, name) {
  2. filesystemObject = object;
  3. currentPath = name;
  4. listDirectory(currentPath);
  5. };

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

  1. listDirectory(path) {
  2. filesystemObject.requestInputStream(path, function handleStream(stream, mimetype) {
  3. // Ignore stream if mimetype is wrong
  4. if (mimetype !== Guacamole.Object.STREAM_INDEX_MIMETYPE) {
  5. stream.sendAck('Unexpected mimetype', Guacamole.Status.Code.UNSUPPORTED);
  6. return;
  7. }
  8. currentPath = path;
  9. let exchangePath = path.replace(/^\//,'')
  10. folders = exchangePath.length ? exchangePath.split('/') : []
  11. paths = []
  12. folders.reduce((tmp, item, index) => {
  13. let path = tmp+"/"+item
  14. let obj = {
  15. path: path,
  16. folder: item
  17. }
  18. paths.push(obj)
  19. return path }, "")
  20. // Signal server that data is ready to be received
  21. stream.sendAck('Ready', Guacamole.Status.Code.SUCCESS);
  22. // Read stream as JSON
  23. let reader = new Guacamole.JSONReader(stream);
  24. // Acknowledge received JSON blobs
  25. reader.onprogress = function onprogress() {
  26. stream.sendAck("Received", Guacamole.Status.Code.SUCCESS);
  27. };
  28. // Reset contents of directory
  29. reader.onend = function jsonReady() {
  30. fileList = []
  31. // For each received stream name
  32. var mimetypes = reader.getJSON();
  33. for (var name in mimetypes) {
  34. if (name.substring(0, path.length) !== path){
  35. continue;
  36. }
  37. var filename = name.substring(path.length);
  38. if(path.substring(path.length-1) != '/'){
  39. filename = name.substring(path.length+1);
  40. }
  41. let one = {}
  42. one.path = filename
  43. if (mimetypes[name] === Guacamole.Object.STREAM_INDEX_MIMETYPE) {
  44. one.type="folder"
  45. }
  46. else {
  47. one.type="file"
  48. }
  49. one.fullpath = name
  50. fileList.push(one)
  51. }
  52. };
  53. });
  54. },

上传下载

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

通过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事件结束下载流程

  1. downloadfile(path){
  2. filesystemObject.requestInputStream(path, function downloadStreamReceived(stream, mimetype) {
  3. // Parse filename from string
  4. var filename = path.match(/(.*[\\/])?(.*)/)[2];
  5. var url = '/tunnels/' + uuid + '/sessions/' + stream.index + '/files/' + filename;
  6. // Create temporary hidden iframe to facilitate download
  7. var iframe = document.createElement('iframe');
  8. iframe.style.display = 'none';
  9. // The iframe MUST be part of the DOM for the download to occur
  10. document.body.appendChild(iframe);
  11. iframe.onload = function downloadComplete() {
  12. document.body.removeChild(iframe);
  13. };
  14. // Acknowledge (and ignore) any received blobs
  15. stream.onblob = function acknowledgeData() {
  16. stream.sendAck('OK', Guacamole.Status.Code.SUCCESS);
  17. };
  18. // Automatically remove iframe from DOM a few seconds after the stream
  19. // ends, in the browser does NOT fire the "load" event for downloads
  20. stream.onend = function downloadComplete() {
  21. window.setTimeout(function cleanupIframe() {
  22. if (iframe.parentElement) {
  23. document.body.removeChild(iframe);
  24. }
  25. }, 5000);
  26. };
  27. // Begin download
  28. iframe.src = url;
  29. });
  30. }

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

  1. changFile(event){
  2. let file1 = event.target.files[0];
  3. var stream = filesystemObject.createOutputStream(file1.type, currentPath+'/'+file1.name);
  4. stream.onack = function beginUpload(status) {
  5. if (status.isError()) {
  6. return;
  7. }
  8. }
  9. var fd = new FormData();
  10. fd.append('file', file1);
  11. var url = '/tunnels/' + uuid + '/sessions/' + stream.index;
  12. const xhr = new XMLHttpRequest();
  13. xhr.open('POST', url);
  14. xhr.send(fd);
  15. xhr.onreadystatechange = function () {
  16. if (xhr.readyState === 4 && xhr.status === 200) {
  17. console.log('上传成功');
  18. updateDirectory(currentPath);
  19. }
  20. }
  21. },

接下来是java端的http接口,

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

  1. @GetMapping("/tunnels/{tnid}/sessions/{snid}/files/{filename}")
  2. public void download(@PathVariable("tnid")String tnid, @PathVariable("snid")String snid, @PathVariable("filename")String filename, HttpServletResponse response) {
  3. try {
  4. System.out.println("download controller: "+tnid);
  5. response.setCharacterEncoding("UTF-8");
  6. response.addHeader("Content-Disposition", "attachment;filename=" + URLEncoder.encode(filename, "UTF-8"));
  7. ServletOutputStream os = response.getOutputStream();
  8. if(streamMap.tunnelStreamMap.get(tnid) != null) {
  9. streamMap.tunnelStreamMap.get(tnid).getWebSocketTunnel().startSendFile(snid);
  10. streamMap.tunnelStreamMap.get(tnid).setEnd(false);
  11. streamMap.tunnelStreamMap.get(tnid).setBuffer(null);
  12. long start = System.currentTimeMillis();
  13. while(!streamMap.tunnelStreamMap.get(tnid).isEnd()){
  14. synchronized(streamMap) {
  15. String str = streamMap.tunnelStreamMap.get(tnid).getBuffer();
  16. if (str != null) {
  17. streamMap.tunnelStreamMap.get(tnid).setBuffer(null);
  18. os.write(decoder.decode(str.getBytes()));
  19. }
  20. }
  21. }
  22. }
  23. os.close();
  24. }
  25. catch (Exception e) {
  26. throw new RuntimeException(e);
  27. }
  28. }

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

  1. @PostMapping("/tunnels/{tnid}/sessions/{snid}")
  2. public void upload(@RequestParam("file") MultipartFile uploadFile, @PathVariable("tnid")String tnid, @PathVariable("snid")String snid) {
  3. try {
  4. InputStream inputStream = uploadFile.getInputStream();
  5. byte[] buffer = new byte[8192];
  6. int bytesRead = 0;
  7. while ((bytesRead = inputStream.read(buffer, 0, buffer.length)) != -1) {
  8. long start = System.currentTimeMillis();
  9. while(!streamMap.tunnelStreamMap.get(tnid).isOk()) {
  10. // 等待上传完成消息
  11. }
  12. streamMap.tunnelStreamMap.get(tnid).setOk(false);
  13. System.out.println(bytesRead);
  14. byte[] bb = null;
  15. if(bytesRead < 8192) {
  16. bb = new byte[bytesRead];
  17. System.arraycopy(buffer, 0, bb, 0, bytesRead);
  18. }
  19. else {
  20. bb = buffer;
  21. }
  22. if(streamMap.tunnelStreamMap.get(tnid) != null) {
  23. streamMap.tunnelStreamMap.get(tnid).getWebSocketTunnel().sendBlob(snid, bb);
  24. }
  25. }
  26. if(streamMap.tunnelStreamMap.get(tnid) != null) {
  27. streamMap.tunnelStreamMap.get(tnid).getWebSocketTunnel().sendEnd(snid);
  28. }
  29. inputStream.close();
  30. }
  31. catch (Exception e) {
  32. throw new RuntimeException(e);
  33. }
  34. }

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

  1. @Override
  2. protected void sendInstruction(String instruction) throws IOException {
  3. if(instruction.startsWith("0.,36.")) {
  4. uuid = instruction.substring(6, instruction.length()-1);
  5. System.out.println("uuid: "+uuid);
  6. TunnelStream tunnelStream = new TunnelStream();
  7. tunnelStream.setWebSocketTunnel(this);
  8. tunnelStream.setEnd(false);
  9. tunnelStream.setBuffer(null);
  10. streamMap.tunnelStreamMap.put(uuid, tunnelStream);
  11. streamMap.tunnelStreamMap.get(uuid).setOk(false);
  12. }
  13. else if(instruction.contains("application/octet-stream")) {
  14. fileTranfer = true;
  15. GuacamoleParser parser = new GuacamoleParser();
  16. int parsed;
  17. int offset = 0;
  18. int length = instruction.toCharArray().length;
  19. while (true) {
  20. try {
  21. if (!((parsed = parser.append(instruction.toCharArray(), offset, length)) != 0))
  22. break;
  23. }
  24. catch (GuacamoleException e) {
  25. throw new RuntimeException(e);
  26. }
  27. offset += parsed;
  28. length -= parsed;
  29. }
  30. GuacamoleInstruction ins = parser.next();
  31. synchronized (bufferInstructions) {
  32. bufferInstructions.put(ins.getArgs().get(0), ins);
  33. }
  34. }
  35. else if(instruction.contains("17.SFTP: File opened")) {
  36. streamMap.tunnelStreamMap.get(uuid).setOk(true);
  37. }
  38. else if(instruction.contains("8.SFTP: OK")) {
  39. streamMap.tunnelStreamMap.get(uuid).setOk(true);
  40. }
  41. else {
  42. if(fileTranfer) {
  43. if(instruction.startsWith("4.blob")) {
  44. int num1 = instruction.indexOf(",");
  45. int num2 = instruction.indexOf(",", num1+1);
  46. int num3 = instruction.indexOf(".", num1+1);
  47. int id = Integer.parseInt(instruction.substring(num3+1, num2));
  48. int num4 = instruction.indexOf(".", num2+1);
  49. String str = instruction.substring(num4+1, instruction.length()-1);
  50. TunnelStream tunnelStream = streamMap.tunnelStreamMap.get(uuid);
  51. if(tunnelStream != null) {
  52. synchronized(streamMap) {
  53. streamMap.tunnelStreamMap.get(uuid).setBuffer(str);
  54. }
  55. instruction = instruction.substring(0, num2+1) + "0.;";
  56. }
  57. }
  58. else if(instruction.startsWith("3.end")) {
  59. System.out.println("3.end");
  60. fileTranfer = false;
  61. //int num1 = instruction.indexOf(".", 3);
  62. //int id = Integer.parseInt(instruction.substring(num1+1, instruction.length()-1));
  63. TunnelStream tunnelStream = streamMap.tunnelStreamMap.get(uuid);
  64. synchronized(streamMap) {
  65. streamMap.tunnelStreamMap.get(uuid).setEnd(true);
  66. }
  67. }
  68. }
  69. }
  70. super.sendInstruction(instruction);
  71. if(!instruction.startsWith("0.")) {
  72. userConnectLogEntity.getBufferedWriter().write(instruction);
  73. }
  74. }
  75. public void startSendFile(String sid) {
  76. acknowledgeStream(sid);
  77. }
  78. @Override
  79. protected void receiveData(String message) {
  80. }
  81. public void sendBlob(String sid, byte[] bytes) {
  82. GuacamoleWriter writer = guacamoleTunnel.acquireWriter();
  83. GuacamoleInstruction ins = new GuacamoleInstruction("blob", sid, BaseEncoding.base64().encode(bytes));
  84. try {
  85. writer.writeInstruction(ins);
  86. }
  87. catch (GuacamoleException e) {
  88. logger.debug("Unable to send \"{}\" for intercepted stream.", ins.getOpcode(), e);
  89. }
  90. guacamoleTunnel.releaseWriter();
  91. }
  92. public void sendEnd(String sid) {
  93. GuacamoleWriter writer = guacamoleTunnel.acquireWriter();
  94. GuacamoleInstruction ins = new GuacamoleInstruction("end", sid);
  95. try {
  96. writer.writeInstruction(ins);
  97. }
  98. catch (GuacamoleException e) {
  99. logger.debug("Unable to send \"{}\" for intercepted stream.", ins.getOpcode(), e);
  100. }
  101. guacamoleTunnel.releaseWriter();
  102. }
  103. protected void acknowledgeStream(String sid) {
  104. GuacamoleInstruction ins = null;
  105. synchronized (bufferInstructions) {
  106. ins = bufferInstructions.remove(sid);
  107. }
  108. if(ins != null) {
  109. GuacamoleWriter writer = guacamoleTunnel.acquireWriter();
  110. try {
  111. writer.writeInstruction(
  112. new GuacamoleInstruction("ack", ins.getArgs().get(0), "OK",
  113. Integer.toString(GuacamoleStatus.SUCCESS.getGuacamoleStatusCode())));
  114. }
  115. catch (GuacamoleException e) {
  116. throw new RuntimeException(e);
  117. }
  118. guacamoleTunnel.releaseWriter();
  119. }
  120. }
声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/2023面试高手/article/detail/550738
推荐阅读
相关标签
  

闽ICP备14008679号