这篇主要记录录屏和SFTP的实现。
录屏及视频播放
对于录屏及录屏的播放,因为我们的项目中需要把guacd和java后端分开两台服务器部署,而guacamole的录屏是通过guacd程序录制的。我的要求是在Java后端直接把录好的视频文件通过http前端播放,因此需要把录屏放在Java端的服务器上。
首先稍微修改一下guacamole-common的源码,添加几个可重载的函数,分别是向前端下发ws消息,向guacd上传前端消息以及ws连接关闭的地方。
GuacamoleWebSocketTunnelEndpoint类的onMessage函数中,添加receiveData(message);
try {
// Write received message
writer.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函数用于记录鼠标键盘事件
@Override
protected 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函数,对将要发送给前端的报文进行拦截处理,重点是最后的几行,把报文记录在一个文件中。
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;
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格式的视频文件。
@Override
protected 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;
@Override
public 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 wrong
if (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+"/"+item
let obj = {
path: path,
folder: item
}
paths.push(obj)
return path }, "")
// Signal server that data is ready to be received
stream.sendAck('Ready', Guacamole.Status.Code.SUCCESS);
// Read stream as JSON
let reader = new Guacamole.JSONReader(stream);
// Acknowledge received JSON blobs
reader.onprogress = function onprogress() {
stream.sendAck("Received", Guacamole.Status.Code.SUCCESS);
};
// Reset contents of directory
reader.onend = function jsonReady() {
fileList = []
// For each received stream name
var 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 = filename
if (mimetypes[name] === Guacamole.Object.STREAM_INDEX_MIMETYPE) {
one.type="folder"
}
else {
one.type="file"
}
one.fullpath = name
fileList.push(one)
}
};
});
},
上传下载
上传下载,首先得搞清楚整体得流程,
通过wireshark抓包,可以查看guacd与java后端的通信报文,
通过浏览器自带的调试工具,可以查看前端和Java后端之间的websocket通信报文,
通过上面两个工具的抓包分析,分析出上传下载的流程。
文件下载流程:
- 首先前端通过websocket发送3.get报文,java后端接受到后,直接发往guacd服务端;然后前端再通过http接口发送文件请求到Java端;
- guacd回复application/octet-stream报文给Java后端,Java后端直接向guacd端回复ack消息,不向前端转发;
- guacd端开始发送4.blob文件段,Java端接收到后,将4.blob报文的实际blob字段截取下来,通过WebSocket向前端回复截取后的报文,同时通过HTTP接口向前端发送实际的文件段;
- 前端websocket接受到blob消息后,回复ack,Java端转发给guacd,guacd再发下一段文件,循环这个过程直到文件发送完毕。
- 最后guacd端发送end报文,java端通过websocket转发给前端,整个下载过程结束。
文件上传流程:
- 前端发送put指令,Java端接收到后,直接转给guacd端
- guacd端回复File Opened消息通知文件已准备好,可以写入
- 前端通过Http post 发送MultipartFile给java端,java端接收到后转发给guacd端
- guacd端回复SFTP OK消息
- 前端发送下一段,循环发送直到文件发送完成,最后Java端发送end命令给guacd端
- guacd端回复OK消息,整个文件上传流程结束
下面是代码实现的大致流程:
前端下载代码,先通过filesystemObject.requestInputStream发送下载请求,再通过iframe挂一个http get请求开始下载文件,中间通过stream.onblob事件回复Ack消息,通过stream.onend事件结束下载流程
downloadfile(path){
filesystemObject.requestInputStream(path, function downloadStreamReceived(stream, mimetype) {
// Parse filename from string
var filename = path.match(/(.*[\\/])?(.*)/)[2];
var url = '/tunnels/' + uuid + '/sessions/' + stream.index + '/files/' + filename;
// Create temporary hidden iframe to facilitate download
var iframe = document.createElement('iframe');
iframe.style.display = 'none';
// The iframe MUST be part of the DOM for the download to occur
document.body.appendChild(iframe);
iframe.onload = function downloadComplete() {
document.body.removeChild(iframe);
};
// Acknowledge (and ignore) any received blobs
stream.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 downloads
stream.onend = function downloadComplete() {
window.setTimeout(function cleanupIframe() {
if (iframe.parentElement) {
document.body.removeChild(iframe);
}
}, 5000);
};
// Begin download
iframe.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线程同步消息文章来源:https://www.toymoban.com/news/detail-552227.html
@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的同步文章来源地址https://www.toymoban.com/news/detail-552227.html
@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二次开发记录之二的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!