开源堡垒机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通信報文,
通過上面兩個工具的抓包分析,分析出上傳下載的流程。
文件下載流程:
文件上傳流程:
下面是代碼實現的大致流程:
前端下載代碼,先通過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();} }總結
以上是生活随笔為你收集整理的开源堡垒机Guacamole二次开发记录之二的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: bundle打包自动转换tiff格式的处
- 下一篇: RT1176 LPUART的坑