diff --git a/pkg/api/ssh.go b/pkg/api/ssh.go index 579a531..f4ccf27 100644 --- a/pkg/api/ssh.go +++ b/pkg/api/ssh.go @@ -2,6 +2,7 @@ package api import ( "bytes" + "encoding/json" "fmt" "github.com/gorilla/websocket" "github.com/labstack/echo/v4" @@ -46,6 +47,22 @@ func (w *NextWriter) Read() ([]byte, int, error) { return buf, read, err } +const ( + Data = "data" + Resize = "resize" + Closed = "closed" +) + +type Message struct { + Type string `json:"type"` + Content string `json:"content"` +} + +type WindowSize struct { + Height int `json:"height"` + Width int `json:"width"` +} + func SSHEndpoint(c echo.Context) error { ws, err := UpGrader.Upgrade(c.Response().Writer, c.Request(), nil) if err != nil { @@ -60,12 +77,22 @@ func SSHEndpoint(c echo.Context) error { sshClient, err := CreateSshClient(assetId) if err != nil { logrus.Errorf("创建SSH客户端失败:%v", err.Error()) + msg := Message{ + Type: Closed, + Content: err.Error(), + } + err := WriteMessage(ws, msg) return err } session, err := sshClient.NewSession() if err != nil { logrus.Errorf("创建SSH会话失败:%v", err.Error()) + msg := Message{ + Type: Closed, + Content: err.Error(), + } + err := WriteMessage(ws, msg) return err } defer session.Close() @@ -93,15 +120,39 @@ func SSHEndpoint(c echo.Context) error { return err } - go func() { + msg := Message{ + Type: Data, + Content: "Connect to server successfully.", + } + _ = WriteMessage(ws, msg) + var mut sync.Mutex + var active = true + + go func() { for true { + mut.Lock() + if !active { + logrus.Debugf("会话: %v -> %v 关闭", sshClient.LocalAddr().String(), sshClient.RemoteAddr().String()) + break + } + mut.Unlock() + p, n, err := b.Read() if err != nil { continue } if n > 0 { - WriteByteMessage(ws, p) + msg := Message{ + Type: Data, + Content: string(p), + } + message, err := json.Marshal(msg) + if err != nil { + logrus.Warnf("生成Json失败 %v", err) + continue + } + WriteByteMessage(ws, message) } time.Sleep(time.Duration(100) * time.Millisecond) } @@ -110,16 +161,53 @@ func SSHEndpoint(c echo.Context) error { for true { _, message, err := ws.ReadMessage() if err != nil { + // web socket会话关闭后主动关闭ssh会话 + _ = session.Close() + mut.Lock() + active = false + mut.Unlock() + break + } + + var msg Message + err = json.Unmarshal(message, &msg) + if err != nil { + logrus.Warnf("解析Json失败: %v, 原始字符串:%v", err, string(message)) continue } - _, err = stdinPipe.Write(message) - if err != nil { - logrus.Debugf("Tunnel write: %v", err) + + switch msg.Type { + case Resize: + var winSize WindowSize + err = json.Unmarshal([]byte(msg.Content), &winSize) + if err != nil { + logrus.Warnf("解析SSH会话窗口大小失败: %v", err) + continue + } + if err := session.WindowChange(winSize.Height, winSize.Height); err != nil { + logrus.Warnf("更改SSH会话窗口大小失败: %v", err) + continue + } + case Data: + _, err = stdinPipe.Write([]byte(msg.Content)) + if err != nil { + logrus.Debugf("SSH会话写入失败: %v", err) + } } + } return err } +func WriteMessage(ws *websocket.Conn, msg Message) error { + message, err := json.Marshal(msg) + if err != nil { + logrus.Warnf("生成Json失败 %v", err) + } + WriteByteMessage(ws, message) + return err +} + func CreateSshClient(assetId string) (*ssh.Client, error) { asset, err := model.FindAssetById(assetId) if err != nil { @@ -195,10 +283,6 @@ func CreateSshClient(assetId string) (*ssh.Client, error) { return sshClient, nil } -func WriteMessage(ws *websocket.Conn, message string) { - WriteByteMessage(ws, []byte(message)) -} - func WriteByteMessage(ws *websocket.Conn, p []byte) { err := ws.WriteMessage(websocket.TextMessage, p) if err != nil { diff --git a/pkg/api/tunnel.go b/pkg/api/tunnel.go index ca60e4d..2b3dece 100644 --- a/pkg/api/tunnel.go +++ b/pkg/api/tunnel.go @@ -89,6 +89,7 @@ func TunEndpoint(c echo.Context) error { configuration.SetParameter(guacd.DisableBitmapCaching, propertyMap[guacd.DisableBitmapCaching]) configuration.SetParameter(guacd.DisableOffscreenCaching, propertyMap[guacd.DisableOffscreenCaching]) configuration.SetParameter(guacd.DisableGlyphCaching, propertyMap[guacd.DisableGlyphCaching]) + configuration.SetParameter("server-layout", "en-us-qwerty") break case "ssh": if len(session.PrivateKey) > 0 && session.PrivateKey != "-" { diff --git a/web/package-lock.json b/web/package-lock.json index 318ce80..51a0911 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -2537,14 +2537,6 @@ "resolved": "https://registry.npmjs.org/@types/html-minifier-terser/-/html-minifier-terser-5.1.1.tgz", "integrity": "sha512-giAlZwstKbmvMk1OO7WXSj4OZ0keXAcl2TQq4LWHiiPH2ByaH7WeUzng+Qej8UPxxv+8lRTuouo0iaNDBuzIBA==" }, - "@types/http-proxy": { - "version": "1.17.4", - "resolved": "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.4.tgz", - "integrity": "sha512-IrSHl2u6AWXduUaDLqYpt45tLVCtYv7o4Z0s1KghBCDgIIS9oW5K1H8mZG/A2CfeLdEa7rTd1ACOiHBc1EMT2Q==", - "requires": { - "@types/node": "*" - } - }, "@types/istanbul-lib-coverage": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.3.tgz", @@ -7972,18 +7964,6 @@ "requires-port": "^1.0.0" } }, - "http-proxy-middleware": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-1.0.6.tgz", - "integrity": "sha512-NyL6ZB6cVni7pl+/IT2W0ni5ME00xR0sN27AQZZrpKn1b+qRh+mLbBxIq9Cq1oGfmTc7BUq4HB77mxwCaxAYNg==", - "requires": { - "@types/http-proxy": "^1.17.4", - "http-proxy": "^1.18.1", - "is-glob": "^4.0.1", - "lodash": "^4.17.20", - "micromatch": "^4.0.2" - } - }, "http-signature": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", diff --git a/web/package.json b/web/package.json index 34a10b5..bdf8fe7 100644 --- a/web/package.json +++ b/web/package.json @@ -17,7 +17,8 @@ "react-scripts": "^4.0.0", "typescript": "^3.9.7", "xterm": "^4.9.0", - "xterm-addon-web-links": "^0.4.0" + "xterm-addon-web-links": "^0.4.0", + "xterm-addon-fit": "^0.4.0" }, "scripts": { "start": "react-scripts start", diff --git a/web/src/components/access/Access.js b/web/src/components/access/Access.js index 8dc2328..0fba26c 100644 --- a/web/src/components/access/Access.js +++ b/web/src/components/access/Access.js @@ -28,7 +28,7 @@ import { CloudUploadOutlined, CopyOutlined, DeleteOutlined, - DesktopOutlined, + DesktopOutlined, ExpandOutlined, FileZipOutlined, FolderAddOutlined, LoadingOutlined, @@ -36,7 +36,7 @@ import { UploadOutlined } from '@ant-design/icons'; import Upload from "antd/es/upload"; -import {download, getToken} from "../../utils/utils"; +import {download, exitFull, getToken, requestFullScreen} from "../../utils/utils"; import './Access.css' import Draggable from 'react-draggable'; @@ -87,7 +87,9 @@ class Access extends Component { confirmLoading: false, uploadVisible: false, uploadLoading: false, - startTime: new Date() + startTime: new Date(), + fullScreen: false, + fullScreenBtnText: '进入全屏' }; async componentDidMount() { @@ -345,6 +347,7 @@ class Access extends Component { return true; } + console.log('--------------------') console.log(keysym) this.state.client.sendKeyEvent(1, keysym); if (keysym === 65288) { @@ -370,6 +373,24 @@ class Access extends Component { }); }; + fullScreen = () => { + let fs = this.state.fullScreen; + if(fs){ + exitFull(); + this.setState({ + fullScreen: false, + fullScreenBtnText: '进入全屏' + }) + }else { + requestFullScreen(document.documentElement); + this.setState({ + fullScreen: true, + fullScreenBtnText: '退出全屏' + }) + } + + } + showClipboard = () => { this.setState({ clipboardVisible: true @@ -823,11 +844,18 @@ class Access extends Component {