如何解決Vue.js裏面noVNC的截圖問題以後篇——用web虛擬終端做爲替代功能

  使用node.js開發webSocket開發代理,能夠解決webSocket的cookies跨域問題。css

  這時有人會說,若是openstack的虛擬桌面流量太大,把代理衝內存溢出了,如何處理?vue

  實際上,不是什麼人都特別須要用WEB虛擬桌面操控虛擬機或物理機的,除非是windows系統,linux系統徹底能夠用流量更小的虛擬終端登錄。實際上進入linux虛擬桌面以後,好多操做不是還要用終端的嗎?java

  業務層面,推薦使用linux系虛擬桌面的用戶使用終端,甚至徹底不提供虛擬桌面,這纔是解決流量擁塞的方法。node

  然而想要在web上操縱linux終端,就須要經過 SSH 代理的方式調用並返回一個 shell 的虛擬終端(pty)的開源的 Web Terminal 項目。linux

  這裏爲了防止SSH代理與項目耦合,致使代碼難以查找,用node.js中間件或者Java的Springboot實現。git

  node.js的服務端實現(node.js對於websocket服務端的解決方法有二:原生websocket包和socket.io,後者能夠在瀏覽器不支持的狀況下轉換爲sockJS連接):github

var http = require('http'); var io = require('socket.io'); var utf8 = require('utf8'); var SSHClient = require('ssh2').Client; var server2 = http.createServer(function(request, response) { console.log((new Date()) + ' Server is reseiveing on port 4041'); response.writeHead(204); response.end(); }); server2.listen(4041, function() { console.log((new Date()) + ' Server is listening on port 4041'); }); io = io.listen(server2,{origins: '*:*'}); function createNewServer(machineConfig, socket) { var ssh = new SSHClient(); let {msgId, ip, username, password, port, rows, cols} = machineConfig; ssh.on('ready', function () { socket.emit(msgId, '\r\n***' + ip + ' SSH CONNECTION ESTABLISHED ***\r\n'); ssh.shell(function(err, stream) { if(rows != null && cols != null) stream.setWindow(rows, cols); if(err) { return socket.emit(msgId, '\r\n*** SSH SHELL ERROR: ' + err.message + ' ***\r\n'); } socket.on(msgId, function (data) { var mydata = data.data; if(mydata != null){ console.log(">>>" + data.data + "<<<"); stream.write(mydata); } var size = data.rows; if(size != null){ stream.setWindow(data.rows, data.cols); } }); stream.on('data', function (d) { try{ var mydata = utf8.decode(d.toString('binary')); mydata = mydata.replace(/ \r(?!\n)/g,'');
                    console.log("<<<" + mydata + ">>>"); socket.emit(msgId, mydata); }catch(err){ socket.emit(msgId, '\r\n*** SSH CONNECTION ERROR: ' + err.message + ' ***\r\n'); } }).on('close', function () { ssh.end(); }); }) }).on('close', function () { socket.emit(msgId, '\r\n*** SSH CONNECTION CLOSED ***\r\n'); ssh.end(); }).on('error', function (err) { console.log(err); socket.emit(msgId, '\r\n*** SSH CONNECTION ERROR: ' + err.message + ' ***\r\n'); ssh.end(); }).connect({ host: ip, port: port, username: username, password: password }); } io.on('connection', function(socket) { socket.on('createNewServer', function(machineConfig) {//新建一個ssh鏈接
        console.log("createNewServer"); createNewServer(machineConfig, socket); }) socket.on('disconnect', function(){ console.log('user disconnected'); }); })
node ssh代理

  Vue.js代碼這樣寫(需導入xterm):web

<template>
</template>
 
<script> import 'xterm/dist/xterm.css' import { Terminal } from 'xterm'; import * as fit from 'xterm/dist/addons/fit/fit'; import * as fullscreen from 'xterm/dist/addons/fullscreen/fullscreen' import openSocket from 'socket.io-client'; export default { name: 'sshweb', props:['ip','port'], data () { return { wsServer:null, localip:'', localport:'', env: "", podName: "", contaName: "", logtxt: "", term:[0,0], colsLen:9, rowsLen:19, colRemain:21, msgId:0, col:80, row:24, terminal: { pid: 1, name: 'terminal', cols: 80, rows: 24 }, } }, watch:{ port(val){ this.localport=port; }, ip(val){ this.localip=ip; } }, methods: { S4() { return (((1+Math.random())*0x10000)|0).toString(16).substring(1); }, guid() { return (this.S4()+this.S4()+"-"+this.S4()+"-"+this.S4()+"-"+this.S4()+"-"+this.S4()+this.S4()+this.S4()); }, createServer1(){ this.msgId = this.guid(); var msgId = this.msgId; var myserver = this.wsServer; var selfy = this; var ipport = this.$route.params.ipport.split(':'); var myport = ipport[0]; var myip = ipport[1]; myserver.emit("createNewServer", {msgId: msgId, ip: myport, username: "root", password: "xunfang", port: myip, rows: this.term[0].rows, cols: this.term[0].cols}); let term = this.term[0]; term.on("data", function(data) { myserver.emit(msgId, {'data':data}); }); myserver.on(msgId, function (data) { term.write(data); }); term.attachCustomKeyEventHandler(function(ev) { if (ev.keyCode == 86 && ev.ctrlKey) { myserver.emit(msgId, new TextEncoder().encode("\x00" + this.copy)); } }); myserver.on('connect_error', function(data){ console.log(data + ' - connect_error'); }); myserver.on('connect_timeout', function(data){ console.log(data + ' - connect_timeout'); }); myserver.on('error', function(data){ console.log(data + ' - error'); }); myserver.on('disconnect', function(data){ console.log(data + ' - disconnect'); }); myserver.on('reconnect', function(data){ console.log(data + ' - reconnect'); }); myserver.on('reconnect_attempt', function(data){ console.log(data + ' - reconnect_attempt'); }); myserver.on('reconnecting', function(data){ console.log(data + ' - reconnecting'); }); myserver.on('reconnect_error', function(data){ console.log(data + ' - reconnect_error'); }); myserver.on('reconnect_failed', function(data){ console.log(data + ' - reconnect_failed'); }); myserver.on('ping', function(data){ console.log(data + ' - ping'); }); myserver.on('pong', function(data){ console.log(data + ' - pong'); }); }, resize(row,col){ row = Math.floor(row/this.rowsLen);
 col = Math.floor((col-this.colRemain)/this.colsLen);
                if(row<24)row=24; if(col<80)col=80; if(this.row != row || this.col != col){ this.row=row; this.col=col; this.term[0].fit(); //this.term[0].resize(col,row);
                    this.wsServer.emit(this.msgId, {'rows':this.term[0].rows.toString(),'cols':this.term[0].cols.toString()}); //this.wsServer.emit(this.msgId, {'rows':row.toString(),'cols':col.toString()});
 } } }, mounted(){ this.wsServer = new openSocket('ws://127.0.0.1:4041'); var selfy = this; window.onload = function(){ for(var i = 0;i < 1;i++){ var idname = 'net0'; Terminal.applyAddon(fit); Terminal.applyAddon(fullscreen); var terminalContainer = document.getElementById('app'); //terminalContainer.style.height = (selfy.rowsLen * selfy.terminal.rows).toString() + 'px' ;
                    //terminalContainer.style.width = (selfy.colsLen * selfy.terminal.cols + selfy.colRemain).toString() + 'px' ;
 selfy.term[i] = new Terminal({ cursorBlink: true }); selfy.term[i].open(terminalContainer, true); if(window.innerWidth > 0 && window.innerHeight > 0) selfy.term[i].fit(); selfy.createServer1(); } } $(window).resize(() =>{ if(window.innerWidth > 0 && window.innerHeight > 0){ selfy.resize(window.innerHeight, window.innerWidth); } }); }, components: { } } </script>
 
<style scoped> #app{height:100%;width:100%;}
</style>
Vue.js的ssh客戶端

  打開服務器和Vue系統,登錄這個客戶端上SSH,大功告成!spring

  Java部分:shell

  這裏有個完整的解決方案,在我改正過的github倉庫裏。

  這裏說一下websocket鏈接代碼:

  vip.r0n9.ws.WebSshHandler:

package vip.r0n9.ws; import com.jcraft.jsch.JSchException; import org.springframework.stereotype.Component; import vip.r0n9.util.SshSession; import javax.websocket.*; import javax.websocket.server.PathParam; import javax.websocket.server.ServerEndpoint; import java.io.*; import java.util.HashMap; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; @ServerEndpoint(value = "/ssh/{id}", configurator = WebSocketConfigrator.class) @Component public class WebSshHandler { private static Map<String, SshSession> map = new ConcurrentHashMap<String, SshSession>(); @OnOpen public void onOpen(final Session session, @PathParam("id") String id) throws JSchException, IOException, EncodeException, InterruptedException { System.out.println("有新連接 " + session.getUserProperties().get("ClientIP") + " 加入!當前在線人數爲" + getOnlineCount()); Map<String,String> parammap = new HashMap<String,String>(); String[] param =  session.getQueryString().split("&"); for(String keyvalue:param){ String[] pair = keyvalue.split("="); if(pair.length==2){ parammap.put(pair[0], pair[1]); } } String hostname = parammap.get("hostname"); String password = parammap.get("password"); Integer port,cols,rows; try { port = Integer.valueOf(parammap.get("port")); }catch(Exception e) { port = 22; } String username = parammap.get("username"); try { rows = Integer.valueOf(parammap.get("rows")); }catch(Exception e) { rows = 24; } try { cols = Integer.valueOf(parammap.get("cols")); }catch(Exception e) { cols = 80; } SshSession sshSession; sshSession = new SshSession(hostname, port, username, password, session, rows, cols); map.put(session.getId(), sshSession); } @OnClose public void onClose(Session session) { SshSession sshsession = map.remove(session.getId()); sshsession.close(); } @OnMessage public void onMessage(String message, Session session) throws IOException, JSchException { map.get(session.getId()).getMessage(message); } @OnError public void onError(Session session, Throwable throwable) { throwable.printStackTrace(); try { session.getBasicRemote().sendText(throwable.getMessage()); } catch (IOException e) { e.printStackTrace(); } } public static synchronized int getOnlineCount() { return map.size(); } }
WebSshController.java

  這裏就是websocket服務端代碼,鏈接websocket首先要在這裏進行處理。

  vip.r0n9.util.SshSession:

package vip.r0n9.util; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.OutputStream; import java.io.PrintWriter; import java.nio.ByteBuffer; import java.util.Iterator; import javax.websocket.Session; import com.fasterxml.jackson.databind.JsonNode; import com.jcraft.jsch.Channel; import com.jcraft.jsch.ChannelShell; import com.jcraft.jsch.JSch; import com.jcraft.jsch.JSchException; import vip.r0n9.JsonUtil; import vip.r0n9.ws.WebSshHandler; public class SshSession { private Session websession;//從客戶端發起的websocket鏈接

    private StringBuilder dataToDst = new StringBuilder(); private JSch jsch = new JSch();//ssh客戶端

    private com.jcraft.jsch.Session jschSession;//ssh服務端返回的單個客戶端鏈接

    private ChannelShell channel; private InputStream inputStream; private BufferedReader stdout; private OutputStream outputStream; private PrintWriter printWriter; public SshSession() {} public SshSession(String hostname,int port,String username, String password, final Session session2, int rows, int cols) throws JSchException, IOException { this.websession = session2; jschSession = jsch.getSession(username, hostname, port); jschSession.setPassword(password); java.util.Properties config = new java.util.Properties(); config.put("StrictHostKeyChecking", "no"); jschSession.setConfig(config); jschSession.connect(); channel = (ChannelShell) jschSession.openChannel("shell"); channel.setPty(true); channel.setPtyType("xterm"); channel.setPtySize(cols, rows, cols*8, rows*16); inputStream = channel.getInputStream(); outputStream = channel.getOutputStream(); printWriter = new PrintWriter(outputStream,false); channel.connect(); outputStream.write("\r".getBytes()); outputStream.flush(); //這裏能夠用newFixedThreadPool線程池,能夠更方便管理線程
        Thread thread = new Thread() { @Override public void run() { try { byte[] byteset = new byte[3072]; int res = inputStream.read(byteset); if(res == -1)res = 0; while (session2 != null && session2.isOpen()) { // 這裏會阻塞,因此必須起線程來讀取channel返回內容
                        ByteBuffer byteBuffer = ByteBuffer.wrap(byteset, 0, res); synchronized (this) { if(res != 0) session2.getBasicRemote().sendBinary(byteBuffer); } res = inputStream.read(byteset); if(res == -1)res = 0; } } catch (IOException e) { e.printStackTrace(); } } }; thread.start(); try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } } public void close() { channel.disconnect(); jschSession.disconnect(); try { this.websession.close(); } catch (IOException e) { e.printStackTrace(); } try { this.inputStream.close(); } catch (IOException e) { e.printStackTrace(); } try { this.outputStream.close(); } catch (IOException e) { e.printStackTrace(); } } public void getMessage(String message) throws IOException, JSchException { Session mysession = this.websession; System.out.println("來自客戶端 " + mysession.getUserProperties().get("ClientIP") + " 的消息:" + message); JsonNode node = JsonUtil.strToJsonObject(message); if (node.has("resize")) { Iterator<JsonNode> myiter = node.get("resize").elements(); int col = myiter.next().asInt(); int row = myiter.next().asInt(); channel.setPtySize(col, row, col*8, row*16); return; } if (node.has("data")) { String str = node.get("data").asText(); outputStream.write(str.getBytes("utf-8")); outputStream.flush(); return; } } public StringBuilder getDataToDst() { return dataToDst; } public OutputStream getOutputStream() { return outputStream; } }
SshSession.java

  代理SSH客戶端的核心邏輯在此,這裏要注意不要用Reader和Writer,一些終端功能會沒法運行。

  下載項目,開啓Springboot,在瀏覽器上訪問http://localhost:10003/,會進入登陸頁面,目前不支持RSA祕鑰登陸,只支持帳號密碼登陸。

  客戶端也能夠用Vue.js實現:

<template>
</template>
 
<script> import 'xterm/dist/xterm.css' import { Terminal } from 'xterm'; import * as fit from 'xterm/dist/addons/fit/fit'; import * as fullscreen from 'xterm/dist/addons/fullscreen/fullscreen' import openSocket from 'socket.io-client'; export default { name: 'sshweb', props:['ip','port'], data () { return { wsServer:null, localip:'', localport:'', env: "", podName: "", contaName: "", logtxt: "", term:[0,0], colsLen:9, rowsLen:19, colRemain:21, msgId:0, col:80, row:24, terminal: { pid: 1, name: 'terminal', cols: 80, rows: 24 }, } }, watch:{ port(val){ this.localport=port; }, ip(val){ this.localip=ip; } }, methods: { S4() { return (((1+Math.random())*0x10000)|0).toString(16).substring(1); }, guid() { return (this.S4()+this.S4()+"-"+this.S4()+"-"+this.S4()+"-"+this.S4()+"-"+this.S4()+this.S4()+this.S4()); }, createServer1(){ this.msgId = this.guid(); var msgId = this.msgId; var selfy = this; var ipport = this.$route.params.ipport.split(':'); var myport = ipport[1]; var myip = ipport[0]; var wsurl = 'ws://127.0.0.1:10003/ssh/1?hostname=' + myip + '&port=' + myport + '&username=root&password=xunfang'; this.wsServer = new WebSocket(wsurl); var myserver = this.wsServer; let term = this.term[0]; term.on("data", function(data) { var you = data; if(you.length > 1)you = you[0]; console.log(you.charCodeAt()); myserver.send(JSON.stringify({'data': data})); }); myserver.onopen = function(evt) { console.log(evt); }; myserver.onmessage = function(msg) { var reader = new window.FileReader(); var isend = false; reader.onloadend = function(){ var decoder = new window.TextDecoder('utf-8'); console.log(decoder); var text = decoder.decode(reader.result); console.log(text); term.write(text); }; reader.readAsArrayBuffer(msg.data); }; term.attachCustomKeyEventHandler(function(ev) { if (ev.keyCode == 86 && ev.ctrlKey) { myserver.send(JSON.stringify({'data': new TextEncoder().encode("\x00" + this.copy)})); } }); }, resize(row,col){ row = Math.floor(row/this.rowsLen);
 col = Math.floor((col-this.colRemain)/this.colsLen);
                if(row<24)row=24; if(col<80)col=80; if(this.row != row || this.col != col){ this.row=row; this.col=col; this.term[0].fit(); myserver.send(JSON.stringify({'resize': [cols, rows]})); } } }, mounted(){ var selfy = this; window.onload = function(){ for(var i = 0;i < 1;i++){ var idname = 'net0'; Terminal.applyAddon(fit); Terminal.applyAddon(fullscreen); var terminalContainer = document.getElementById('app'); selfy.term[i] = new Terminal({ cursorBlink: true }); selfy.term[i].open(terminalContainer, true); if(window.innerWidth > 0 && window.innerHeight > 0) selfy.term[i].fit(); selfy.createServer1(); } } $(window).resize(() =>{ if(window.innerWidth > 0 && window.innerHeight > 0){ selfy.resize(window.innerHeight, window.innerWidth); } }); }, components: { } } </script>
 
<style scoped> #app{height:100%;width:100%;}
</style>
sshClient.vue

  兩個項目的SSH窗口都是全屏的,只要窗口不小於某個大小,窗口的字會隨着窗口縮放而調整位置。

  虛擬終端演示:

相關文章
相關標籤/搜索