From 32b31eba1a080e8b671a08aacbd8dde78f934d01 Mon Sep 17 00:00:00 2001 From: dushixiang <798148596@qq.com> Date: Tue, 2 Feb 2021 23:06:50 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96=E6=8E=A5=E5=85=A5=E5=8A=9F?= =?UTF-8?q?=E8=83=BD&=E9=87=8D=E6=9E=84=E6=8E=A5=E5=85=A5=E6=96=87?= =?UTF-8?q?=E4=BB=B6=E6=93=8D=E4=BD=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pkg/api/session.go | 81 ++- pkg/api/tunnel.go | 13 +- pkg/global/store.go | 9 +- pkg/model/session.go | 9 +- web/src/App.js | 4 +- web/src/components/access/Access.js | 553 ++++-------------- .../access/{AccessSSH.css => AccessNaive.css} | 0 .../access/{AccessSSH.js => AccessNaive.js} | 9 +- web/src/components/access/FileSystem.js | 474 +++++++++++++++ web/src/components/access/Monitor.js | 75 +-- web/src/components/asset/Asset.js | 21 +- web/src/components/session/OfflineSession.js | 6 +- web/src/utils/utils.js | 12 + 13 files changed, 697 insertions(+), 569 deletions(-) rename web/src/components/access/{AccessSSH.css => AccessNaive.css} (100%) rename web/src/components/access/{AccessSSH.js => AccessNaive.js} (97%) create mode 100644 web/src/components/access/FileSystem.js diff --git a/pkg/api/session.go b/pkg/api/session.go index 32aa5bc..c713466 100644 --- a/pkg/api/session.go +++ b/pkg/api/session.go @@ -11,6 +11,7 @@ import ( "io/ioutil" "net/http" "next-terminal/pkg/global" + "next-terminal/pkg/guacd" "next-terminal/pkg/model" "next-terminal/pkg/utils" "os" @@ -18,7 +19,6 @@ import ( "strconv" "strings" "sync" - "time" ) func SessionPagingEndpoint(c echo.Context) error { @@ -40,10 +40,10 @@ func SessionPagingEndpoint(c echo.Context) error { if status == model.Disconnected && len(items[i].Recording) > 0 { var recording string - if items[i].Protocol == "rdp" || items[i].Protocol == "vnc" { - recording = items[i].Recording + "/recording" - } else { + if items[i].Mode == model.Naive { recording = items[i].Recording + } else { + recording = items[i].Recording + "/recording" } if utils.FileExists(recording) { @@ -100,7 +100,7 @@ func SessionDisconnectEndpoint(c echo.Context) error { split := strings.Split(sessionIds, ",") for i := range split { - CloseSessionById(split[i], ForcedDisconnect, "强制断开") + CloseSessionById(split[i], ForcedDisconnect, "forced disconnect") } return Success(c, nil) } @@ -154,15 +154,11 @@ func CloseWebSocket(ws *websocket.Conn, c int, t string) { if ws == nil { return } - ws.SetCloseHandler(func(code int, text string) error { - var message []byte - if code != websocket.CloseNoStatusReceived { - message = websocket.FormatCloseMessage(c, t) - } - _ = ws.WriteControl(websocket.CloseMessage, message, time.Now().Add(time.Second)) - return nil - }) - defer ws.Close() + err := guacd.NewInstruction("error", "", strconv.Itoa(c)) + _ = ws.WriteMessage(websocket.TextMessage, []byte(err.String())) + disconnect := guacd.NewInstruction("disconnect") + _ = ws.WriteMessage(websocket.TextMessage, []byte(disconnect.String())) + //defer ws.Close() } func SessionResizeEndpoint(c echo.Context) error { @@ -186,6 +182,14 @@ func SessionResizeEndpoint(c echo.Context) error { func SessionCreateEndpoint(c echo.Context) error { assetId := c.QueryParam("assetId") + mode := c.QueryParam("mode") + + if mode == model.Naive { + mode = model.Naive + } else { + mode = model.Guacd + } + user, _ := GetCurrentAccount(c) if model.TypeUser == user.Type { @@ -218,6 +222,7 @@ func SessionCreateEndpoint(c echo.Context) error { Status: model.NoConnect, Creator: user.ID, ClientIP: c.RealIP(), + Mode: mode, } if asset.AccountType == "credential" { @@ -351,11 +356,13 @@ func SessionDownloadEndpoint(c echo.Context) error { } type File struct { - Name string `json:"name"` - Path string `json:"path"` - IsDir bool `json:"isDir"` - Mode string `json:"mode"` - IsLink bool `json:"isLink"` + Name string `json:"name"` + Path string `json:"path"` + IsDir bool `json:"isDir"` + Mode string `json:"mode"` + IsLink bool `json:"isLink"` + ModTime utils.JsonTime `json:"modTime"` + Size int64 `json:"size"` } func SessionLsEndpoint(c echo.Context) error { @@ -387,12 +394,20 @@ func SessionLsEndpoint(c echo.Context) error { var files = make([]File, 0) for i := range fileInfos { + + // 忽略因此文件 + if strings.HasPrefix(fileInfos[i].Name(), ".") { + continue + } + file := File{ - Name: fileInfos[i].Name(), - Path: path.Join(remoteDir, fileInfos[i].Name()), - IsDir: fileInfos[i].IsDir(), - Mode: fileInfos[i].Mode().String(), - IsLink: fileInfos[i].Mode()&os.ModeSymlink == os.ModeSymlink, + Name: fileInfos[i].Name(), + Path: path.Join(remoteDir, fileInfos[i].Name()), + IsDir: fileInfos[i].IsDir(), + Mode: fileInfos[i].Mode().String(), + IsLink: fileInfos[i].Mode()&os.ModeSymlink == os.ModeSymlink, + ModTime: utils.NewJsonTime(fileInfos[i].ModTime()), + Size: fileInfos[i].Size(), } files = append(files, file) @@ -412,11 +427,13 @@ func SessionLsEndpoint(c echo.Context) error { var files = make([]File, 0) for i := range fileInfos { file := File{ - Name: fileInfos[i].Name(), - Path: path.Join(remoteDir, fileInfos[i].Name()), - IsDir: fileInfos[i].IsDir(), - Mode: fileInfos[i].Mode().String(), - IsLink: fileInfos[i].Mode()&os.ModeSymlink == os.ModeSymlink, + Name: fileInfos[i].Name(), + Path: path.Join(remoteDir, fileInfos[i].Name()), + IsDir: fileInfos[i].IsDir(), + Mode: fileInfos[i].Mode().String(), + IsLink: fileInfos[i].Mode()&os.ModeSymlink == os.ModeSymlink, + ModTime: utils.NewJsonTime(fileInfos[i].ModTime()), + Size: fileInfos[i].Size(), } files = append(files, file) @@ -539,10 +556,10 @@ func SessionRecordingEndpoint(c echo.Context) error { } var recording string - if session.Protocol == "rdp" || session.Protocol == "vnc" { - recording = session.Recording + "/recording" - } else { + if session.Mode == model.Naive { recording = session.Recording + } else { + recording = session.Recording + "/recording" } logrus.Debugf("读取录屏文件:%v,是否存在: %v, 是否为文件: %v", recording, utils.FileExists(recording), utils.IsFile(recording)) diff --git a/pkg/api/tunnel.go b/pkg/api/tunnel.go index 9b4b19d..75b047e 100644 --- a/pkg/api/tunnel.go +++ b/pkg/api/tunnel.go @@ -13,12 +13,11 @@ import ( ) const ( - TunnelClosed int = -1 - Normal int = 0 - NotFoundSession int = 2000 - NewTunnelError int = 2001 - NewSftpClientError int = 2002 - ForcedDisconnect int = 2003 + TunnelClosed int = -1 + Normal int = 0 + NotFoundSession int = 800 + NewTunnelError int = 801 + ForcedDisconnect int = 802 ) func TunEndpoint(c echo.Context) error { @@ -145,7 +144,7 @@ func TunEndpoint(c echo.Context) error { } tun := global.Tun{ - Protocol: configuration.Protocol, + Protocol: session.Protocol, Tunnel: tunnel, WebSocket: ws, } diff --git a/pkg/global/store.go b/pkg/global/store.go index 1617249..9e0cf99 100644 --- a/pkg/global/store.go +++ b/pkg/global/store.go @@ -20,8 +20,13 @@ func (r *Tun) Close() { if r.Protocol == "rdp" || r.Protocol == "vnc" { _ = r.Tunnel.Close() } else { - _ = r.SshClient.Close() - _ = r.SftpClient.Close() + if r.SshClient != nil { + _ = r.SshClient.Close() + } + + if r.SftpClient != nil { + _ = r.SftpClient.Close() + } } } diff --git a/pkg/model/session.go b/pkg/model/session.go index 2f4a228..c2fec30 100644 --- a/pkg/model/session.go +++ b/pkg/model/session.go @@ -13,6 +13,11 @@ const ( Disconnected = "disconnected" ) +const ( + Guacd = "guacd" + Naive = "naive" +) + type Session struct { ID string `gorm:"primary_key" json:"id"` Protocol string `json:"protocol"` @@ -34,6 +39,7 @@ type Session struct { Message string `json:"message"` ConnectedTime utils.JsonTime `json:"connectedTime"` DisconnectedTime utils.JsonTime `json:"disconnectedTime"` + Mode string `json:"mode"` } func (r *Session) TableName() string { @@ -60,6 +66,7 @@ type SessionVo struct { CreatorName string `json:"creatorName"` Code int `json:"code"` Message string `json:"message"` + Mode string `json:"mode"` } func FindPageSession(pageIndex, pageSize int, status, userId, clientIp, assetId, protocol string) (results []SessionVo, total int64, err error) { @@ -69,7 +76,7 @@ func FindPageSession(pageIndex, pageSize int, status, userId, clientIp, assetId, params = append(params, status) - itemSql := "SELECT s.id, s.protocol,s.recording, s.connection_id, s.asset_id, s.creator, s.client_ip, s.width, s.height, s.ip, s.port, s.username, s.status, s.connected_time, s.disconnected_time,s.code, s.message, a.name AS asset_name, u.nickname AS creator_name FROM sessions s LEFT JOIN assets a ON s.asset_id = a.id LEFT JOIN users u ON s.creator = u.id WHERE s.STATUS = ? " + itemSql := "SELECT s.id,s.mode, s.protocol,s.recording, s.connection_id, s.asset_id, s.creator, s.client_ip, s.width, s.height, s.ip, s.port, s.username, s.status, s.connected_time, s.disconnected_time,s.code, s.message, a.name AS asset_name, u.nickname AS creator_name FROM sessions s LEFT JOIN assets a ON s.asset_id = a.id LEFT JOIN users u ON s.creator = u.id WHERE s.STATUS = ? " countSql := "select count(*) from sessions as s where s.status = ? " if len(userId) > 0 { diff --git a/web/src/App.js b/web/src/App.js index bac5f77..aa63f55 100644 --- a/web/src/App.js +++ b/web/src/App.js @@ -40,7 +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"; +import AccessNaive from "./components/access/AccessNaive"; const {Footer, Sider} = Layout; @@ -114,7 +114,7 @@ class App extends Component { - + diff --git a/web/src/components/access/Access.js b/web/src/components/access/Access.js index c34e7a8..a649715 100644 --- a/web/src/components/access/Access.js +++ b/web/src/components/access/Access.js @@ -1,50 +1,28 @@ import React, {Component} from 'react'; import Guacamole from 'guacamole-common-js'; -import { - Affix, - Alert, - Button, - Card, - Col, - Drawer, - Dropdown, - Form, - Input, - Menu, - message, - Modal, - Row, - Space, - Spin, - Tooltip, - Tree -} from 'antd' +import {Affix, Button, Col, Drawer, Dropdown, Form, Input, Menu, message, Modal, Row, Space, Tooltip} from 'antd' import qs from "qs"; import request from "../../common/request"; -import {server, wsServer} from "../../common/constants"; +import {wsServer} from "../../common/constants"; import { AppstoreTwoTone, CloudDownloadOutlined, CloudUploadOutlined, - CopyOutlined, + CopyTwoTone, DeleteOutlined, DesktopOutlined, ExclamationCircleOutlined, ExpandOutlined, - FileZipOutlined, FolderAddOutlined, LoadingOutlined, - ReloadOutlined, - UploadOutlined + ReloadOutlined } from '@ant-design/icons'; -import Upload from "antd/es/upload"; -import {download, exitFull, getToken, isEmpty, requestFullScreen} from "../../utils/utils"; +import {exitFull, getToken, isEmpty, requestFullScreen} from "../../utils/utils"; import './Access.css' import Draggable from 'react-draggable'; +import FileSystem from "./FileSystem"; const {TextArea} = Input; -const {DirectoryTree} = Tree; -const {SubMenu} = Menu; const STATE_IDLE = 0; const STATE_CONNECTING = 1; @@ -53,16 +31,10 @@ const STATE_CONNECTED = 3; const STATE_DISCONNECTING = 4; const STATE_DISCONNECTED = 5; -const antIcon = ; - -const formItemLayout = { - labelCol: {span: 6}, - wrapperCol: {span: 14}, -}; - class Access extends Component { - formRef = React.createRef() + formRef = React.createRef(); + clipboardFormRef = React.createRef(); state = { sessionId: '', @@ -73,19 +45,12 @@ class Access extends Component { containerOverflow: 'hidden', containerWidth: 0, containerHeight: 0, - fileSystemVisible: false, - fileSystem: { - loading: false, - object: null, - currentDirectory: '/', - files: [], - }, uploadAction: '', uploadHeaders: {}, keyboard: {}, protocol: '', treeData: [], - selectNode: {}, + confirmVisible: false, confirmLoading: false, uploadVisible: false, @@ -97,10 +62,11 @@ class Access extends Component { async componentDidMount() { - let params = new URLSearchParams(this.props.location.search); - let assetsId = params.get('assetsId'); - let protocol = params.get('protocol'); - let sessionId = await this.createSession(assetsId); + let urlParams = new URLSearchParams(this.props.location.search); + let assetId = urlParams.get('assetId'); + document.title = urlParams.get('assetName'); + let protocol = urlParams.get('protocol'); + let sessionId = await this.createSession(assetId); if (isEmpty(sessionId)) { return; } @@ -150,11 +116,11 @@ class Access extends Component { }) if (this.state.protocol === 'ssh') { if (data.data && data.data.length > 0) { - // message.success('您输入的内容已复制到远程服务器上,使用右键将自动粘贴。'); + message.info('您输入的内容已复制到远程服务器上,使用右键将自动粘贴。'); } } else { if (data.data && data.data.length > 0) { - // message.success('您输入的内容已复制到远程服务器上'); + message.info('您输入的内容已复制到远程服务器上'); } } @@ -162,7 +128,7 @@ class Access extends Component { onTunnelStateChange = (state) => { if (state === Guacamole.Tunnel.State.CLOSED) { - this.showMessage('连接已关闭'); + console.log('web socket 已关闭'); } }; @@ -277,8 +243,14 @@ class Access extends Component { case 783: this.showMessage('错误的请求类型'); break; - case 797: - this.showMessage('客户端连接数量过多'); + case 800: + this.showMessage('会话不存在'); + break; + case 801: + this.showMessage('创建隧道失败'); + break; + case 802: + this.showMessage('管理员强制断开了此会话'); break; default: this.showMessage('未知错误。'); @@ -320,7 +292,7 @@ class Access extends Component { // Set clipboard contents once stream is finished reader.onend = async () => { - // message.success('您选择的内容已复制到您的粘贴板中,在右侧的输入框中可同时查看到。'); + message.info('您选择的内容已复制到您的粘贴板中,在右侧的输入框中可同时查看到。'); this.setState({ clipboardText: data }); @@ -343,17 +315,6 @@ class Access extends Component { } }; - uploadChange = (info) => { - if (info.file.status !== 'uploading') { - - } - if (info.file.status === 'done') { - message.success(`${info.file.name} 文件上传成功。`, 3); - } else if (info.file.status === 'error') { - message.error(`${info.file.name} 文件上传失败。`, 10); - } - } - onKeyDown = (keysym) => { if (true === this.state.clipboardVisible || true === this.state.confirmVisible) { return true; @@ -368,20 +329,6 @@ class Access extends Component { this.state.client.sendKeyEvent(0, keysym); }; - showFileSystem = () => { - this.setState({ - fileSystemVisible: true, - }); - - this.loadDirData('/'); - }; - - hideFileSystem = () => { - this.setState({ - fileSystemVisible: false, - }); - }; - fullScreen = () => { let fs = this.state.fullScreen; if (fs) { @@ -400,43 +347,12 @@ class Access extends Component { } - showClipboard = () => { - this.setState({ - clipboardVisible: true - }, () => { - let element = document.getElementById('clipboard'); - if (element) { - element.value = this.state.clipboardText; - } - }); - }; - - hideClipboard = () => { - this.setState({ - clipboardVisible: false - }); - }; - - updateClipboardFormTextarea = () => { - let clipboardText = document.getElementById('clipboard').value; - - this.setState({ - clipboardText: clipboardText - }); - - this.sendClipboard({ - 'data': clipboardText, - 'type': 'text/plain' - }); - }; - async createSession(assetsId) { - let result = await request.post(`/sessions?assetId=${assetsId}`); + let result = await request.post(`/sessions?assetId=${assetsId}&mode=guacd`); if (result['code'] !== 1) { this.showMessage(result['message']); return null; } - document.title = result['data']['name']; return result['data']['id']; } @@ -516,23 +432,6 @@ class Access extends Component { keyboard.onkeydown = this.onKeyDown; keyboard.onkeyup = this.onKeyUp; - let stateChecker = setInterval(async () => { - let result = await request.get(`/sessions/${sessionId}/status`); - if (result['code'] !== 1) { - clearInterval(stateChecker); - } else { - let session = result['data']; - if (session['status'] === 'connected') { - clearInterval(stateChecker); - return - } - - if (session['status'] === 'disconnected') { - this.showMessage(session['message']); - clearInterval(stateChecker); - } - } - }, 1000) this.setState({ client: client, containerWidth: width, @@ -620,185 +519,6 @@ class Access extends Component { } }; - onSelect = (keys, event) => { - this.setState({ - selectNode: { - key: keys[0], - isLeaf: event.node.isLeaf - } - }) - }; - - handleOk = async (values) => { - let params = { - 'dir': this.state.selectNode.key + '/' + values['dir'] - } - let paramStr = qs.stringify(params); - - this.setState({ - confirmLoading: true - }) - let result = await request.post(`/sessions/${this.state.sessionId}/mkdir?${paramStr}`); - if (result.code === 1) { - message.success('创建成功'); - let parentPath = this.state.selectNode.key; - let items = await this.getTreeNodes(parentPath); - this.setState({ - treeData: this.updateTreeData(this.state.treeData, parentPath, items), - selectNode: {} - }); - } else { - message.error(result.message); - } - - this.setState({ - confirmLoading: false, - confirmVisible: false - }) - } - - handleConfirmCancel = () => { - this.setState({ - confirmVisible: false - }) - } - - handleUploadCancel = () => { - this.setState({ - uploadVisible: false - }) - } - - mkdir = () => { - if (!this.state.selectNode.key || this.state.selectNode.isLeaf) { - message.warning('请选择一个目录'); - return; - } - this.setState({ - confirmVisible: true - }) - } - - upload = () => { - if (!this.state.selectNode.key || this.state.selectNode.isLeaf) { - message.warning('请选择一个目录进行上传'); - return; - } - this.setState({ - uploadVisible: true - }) - } - - download = () => { - if (!this.state.selectNode.key || !this.state.selectNode.isLeaf) { - message.warning('当前只支持下载文件'); - return; - } - download(`${server}/sessions/${this.state.sessionId}/download?file=${this.state.selectNode.key}`); - } - - rmdir = async () => { - if (!this.state.selectNode.key) { - message.warning('请选择一个文件或目录'); - return; - } - let result; - if (this.state.selectNode.isLeaf) { - result = await request.delete(`/sessions/${this.state.sessionId}/rm?file=${this.state.selectNode.key}`); - } else { - result = await request.delete(`/sessions/${this.state.sessionId}/rmdir?dir=${this.state.selectNode.key}`); - } - if (result.code !== 1) { - message.error(result.message); - } else { - message.success('删除成功'); - let path = this.state.selectNode.key; - let parentPath = path.substring(0, path.lastIndexOf('/')); - let items = await this.getTreeNodes(parentPath); - this.setState({ - treeData: this.updateTreeData(this.state.treeData, parentPath, items), - selectNode: {} - }); - } - } - - refresh = async () => { - if (!this.state.selectNode.key || this.state.selectNode.isLeaf) { - await this.loadDirData('/'); - } else { - let key = this.state.selectNode.key; - let items = await this.getTreeNodes(key); - this.setState({ - treeData: this.updateTreeData(this.state.treeData, key, items), - }); - } - message.success('刷新目录成功'); - } - - onRightClick = ({event, node}) => { - - }; - - loadDirData = async (key) => { - let items = await this.getTreeNodes(key); - this.setState({ - treeData: items, - }); - } - - getTreeNodes = async (key) => { - const url = server + '/sessions/' + this.state.sessionId + '/ls?dir=' + key; - - let result = await request.get(url); - - if (result.code !== 1) { - message.error(result['message']); - message.error(result['message']); - return []; - } - - let data = result.data; - - data = data.sort(((a, b) => a.name.localeCompare(b.name))); - - return data.map(item => { - return { - title: item['name'], - key: item['path'], - isLeaf: !item['isDir'] && !item['isLink'], - } - }); - } - - onLoadData = ({key, children}) => { - - return new Promise(async (resolve) => { - if (children) { - resolve(); - return; - } - - let items = await this.getTreeNodes(key); - this.setState({ - treeData: this.updateTreeData(this.state.treeData, key, items), - }); - - resolve(); - }); - } - - updateTreeData = (list, key, children) => { - return list.map((node) => { - if (node.key === key) { - return {...node, children}; - } else if (node.children) { - return {...node, children: this.updateTreeData(node.children, key, children)}; - } - - return node; - }); - } - sendCombinationKey = (keys) => { for (let i = 0; i < keys.length; i++) { this.state.client.sendKeyEvent(1, keys[i]); @@ -810,67 +530,22 @@ class Access extends Component { render() { - const title = ( - - - 远程文件管理 -   -   - -