diff --git a/pkg/api/ssh.go b/pkg/api/ssh.go index 98d7503..5da2c64 100644 --- a/pkg/api/ssh.go +++ b/pkg/api/ssh.go @@ -50,9 +50,10 @@ func (w *NextWriter) Read() ([]byte, int, error) { } const ( - Data = "data" - Resize = "resize" - Closed = "closed" + Connected = "connected" + Data = "data" + Resize = "resize" + Closed = "closed" ) type Message struct { @@ -61,8 +62,8 @@ type Message struct { } type WindowSize struct { - Height int `json:"height"` - Width int `json:"width"` + Cols int `json:"cols"` + Rows int `json:"rows"` } func SSHEndpoint(c echo.Context) error { @@ -136,8 +137,8 @@ func SSHEndpoint(c echo.Context) error { } msg := Message{ - Type: Data, - Content: "Connect to server successfully.", + Type: Connected, + Content: "Connect to server successfully.\r\n", } _ = WriteMessage(ws, msg) @@ -199,7 +200,7 @@ func SSHEndpoint(c echo.Context) error { logrus.Warnf("解析SSH会话窗口大小失败: %v", err) continue } - if err := session.WindowChange(winSize.Height, winSize.Height); err != nil { + if err := session.WindowChange(winSize.Rows, winSize.Cols); err != nil { logrus.Warnf("更改SSH会话窗口大小失败: %v", err) continue } diff --git a/web/src/App.js b/web/src/App.js index 275c78f..bac5f77 100644 --- a/web/src/App.js +++ b/web/src/App.js @@ -40,6 +40,7 @@ import {isEmpty, NT_PACKAGE} from "./utils/utils"; import {isAdmin} from "./service/permission"; import UserGroup from "./components/user/UserGroup"; import LoginLog from "./components/session/LoginLog"; +import AccessSSH from "./components/access/AccessSSH"; const {Footer, Sider} = Layout; @@ -66,7 +67,7 @@ class App extends Component { componentDidMount() { let hash = window.location.hash; let current = hash.replace('#/', ''); - if(isEmpty(current)){ + if (isEmpty(current)) { current = 'dashboard'; } this.setCurrent(current); @@ -113,6 +114,7 @@ class App extends Component { + diff --git a/web/src/components/access/AccessSSH.js b/web/src/components/access/AccessSSH.js new file mode 100644 index 0000000..f6e7ae8 --- /dev/null +++ b/web/src/components/access/AccessSSH.js @@ -0,0 +1,179 @@ +import React, {Component} from 'react'; +import "xterm/css/xterm.css" +import {Terminal} from "xterm"; +import qs from "qs"; +import {wsServer} from "../../common/constants"; +import "./Console.css" +import {getToken} from "../../utils/utils"; +import {FitAddon} from 'xterm-addon-fit'; + +class AccessSSH extends Component { + + state = { + width: window.innerWidth, + height: window.innerHeight, + term: undefined, + webSocket: undefined, + fitAddon: undefined + }; + + componentDidMount() { + + let urlParams = new URLSearchParams(this.props.location.search); + let assetId = urlParams.get('assetId'); + + let params = { + 'width': this.state.width, + 'height': this.state.height, + 'assetId': assetId + }; + + let paramStr = qs.stringify(params); + + let term = new Terminal({ + fontFamily: 'monaco, Consolas, "Lucida Console", monospace', + fontSize: 14, + theme: { + background: '#1b1b1b', + lineHeight: 17 + }, + rightClickSelectsWord: true, + }); + + term.open(this.refs.terminal); + const fitAddon = new FitAddon(); + term.loadAddon(fitAddon); + fitAddon.fit(); + term.focus(); + + term.writeln('Trying to connect to the server ...'); + + term.onSelectionChange(async () => { + let selection = term.getSelection(); + this.setState({ + selection: selection + }) + if (navigator.clipboard) { + await navigator.clipboard.writeText(selection); + } + }); + + term.attachCustomKeyEventHandler((e) => { + if (e.ctrlKey && e.key === 'c' && this.state.selection) { + return false; + } + return !(e.ctrlKey && e.key === 'v'); + }); + + term.onData(data => { + let webSocket = this.state.webSocket; + if (webSocket !== undefined) { + webSocket.send(JSON.stringify({type: 'data', content: data})); + } + }); + + let token = getToken(); + + let webSocket = new WebSocket(wsServer + '/ssh?X-Auth-Token=' + token + '&' + paramStr); + + let pingInterval; + webSocket.onopen = (e => { + pingInterval = setInterval(() => { + webSocket.send(JSON.stringify({type: 'ping'})) + }, 5000); + + let terminalSize = { + cols: term.cols, + rows: term.rows + } + webSocket.send(JSON.stringify({type: 'resize', content: JSON.stringify(terminalSize)})); + }); + + webSocket.onerror = (e) => { + term.writeln("Failed to connect to server."); + } + webSocket.onclose = (e) => { + term.writeln("Connection is closed."); + if (pingInterval) { + clearInterval(pingInterval); + } + } + + webSocket.onmessage = (e) => { + let msg = JSON.parse(e.data); + switch (msg['type']) { + case 'connected': + console.log(msg['content']) + this.onWindowResize(); + break; + case 'data': + term.write(msg['content']); + break; + case 'closed': + term.writeln(`\x1B[1;3;31m${msg['content']}\x1B[0m `) + webSocket.close(); + break; + default: + break; + } + } + + this.setState({ + term: term, + webSocket: webSocket, + fitAddon: fitAddon + }); + + window.addEventListener('resize', this.onWindowResize); + } + + componentWillUnmount() { + let webSocket = this.state.webSocket; + if (webSocket) { + webSocket.close() + } + } + + terminalSize() { + return { + cols: Math.floor(this.state.width / 7.5), + rows: Math.floor(window.innerHeight / 17), + } + } + + onWindowResize = (e) => { + let term = this.state.term; + let fitAddon = this.state.fitAddon; + let webSocket = this.state.webSocket; + + this.setState({ + width: window.innerWidth, + height: window.innerHeight, + }, () => { + if (webSocket && webSocket.readyState === WebSocket.OPEN) { + fitAddon.fit(); + term.focus(); + let terminalSize = { + cols: term.cols, + rows: term.rows + } + webSocket.send(JSON.stringify({type: 'resize', content: JSON.stringify(terminalSize)})); + } + }); + }; + + render() { + return ( +
+
+
+ ); + } +} + +export default AccessSSH; diff --git a/web/src/components/asset/Asset.js b/web/src/components/asset/Asset.js index be35b7b..e7862dd 100644 --- a/web/src/components/asset/Asset.js +++ b/web/src/components/asset/Asset.js @@ -319,7 +319,11 @@ class Asset extends Component { if (result.code === 1) { if (result.data === true) { message.success({content: '检测完成,您访问的资产在线,即将打开窗口进行访问。', key: id, duration: 3}); - window.open(`#/access?assetsId=${id}&protocol=${protocol}`); + if(protocol === 'ssh'){ + window.open(`#/access-ssh?assetId=${id}`); + }else { + window.open(`#/access?assetsId=${id}&protocol=${protocol}`); + } } else { message.warn('您访问的资产未在线,请确认网络状态。', 10); }