import React, {Component} from 'react'; import Guacamole from 'guacamole-common-js'; import {Affix, Button, Drawer, Dropdown, Form, Input, Menu, message, Modal, Tooltip} from 'antd' import qs from "qs"; import request from "../../common/request"; import {wsServer} from "../../common/env"; import { CodeOutlined, CopyOutlined, ExclamationCircleOutlined, ExpandOutlined, FolderOutlined, LineChartOutlined, WindowsOutlined } from '@ant-design/icons'; import {exitFull, getToken, isEmpty, requestFullScreen, setToken} from "../../utils/utils"; import './Access.css' import Draggable from 'react-draggable'; import FileSystem from "../devops/FileSystem"; import Stats from "./Stats"; import {Base64} from "js-base64"; const {TextArea} = Input; const STATE_IDLE = 0; const STATE_CONNECTING = 1; const STATE_WAITING = 2; const STATE_CONNECTED = 3; const STATE_DISCONNECTING = 4; const STATE_DISCONNECTED = 5; class Access extends Component { clipboardFormRef = React.createRef(); statsRef = undefined; error = false; state = { session: {}, sessionId: '', client: undefined, scale: 1, clientState: STATE_IDLE, clipboardVisible: false, clipboardText: '', containerOverflow: 'hidden', containerWidth: 1024, containerHeight: 768, uploadAction: '', uploadHeaders: {}, keyboard: {}, protocol: '', confirmLoading: false, uploadVisible: false, uploadLoading: false, startTime: new Date(), fullScreen: false, fullScreenBtnText: '进入全屏', sink: undefined, commands: [], showFileSystem: false, external: false, fixedSize: false, }; async componentDidMount() { let href = window.location.href; let search = href.split('?')[1]; let urlParams = new URLSearchParams(search); let assetId = urlParams.get('assetId'); document.title = urlParams.get('assetName'); if (!assetId) { this.showMessage('获取资产ID失败'); return; } let protocol = urlParams.get('protocol'); let width = urlParams.get('width'); let height = urlParams.get('height'); let fixedSize = false; if (width && height) { fixedSize = true } else { width = window.innerWidth; height = window.innerHeight; } let shareSessionId = urlParams.get('shareSessionId'); let external = false; if (shareSessionId && shareSessionId !== '') { setToken(shareSessionId); external = true; let shareSession = await this.getShareSession(shareSessionId); if (!shareSession) { return } assetId = shareSession['assetId']; } let session = await this.createSession(assetId); if (!session) { return; } let sessionId = session['id']; if (isEmpty(sessionId)) { return; } this.setState({ session: session, sessionId: sessionId, protocol: protocol, showFileSystem: session['fileSystem'] === '1', external: external, fixedSize: fixedSize, containerWidth: width, containerHeight: height, }); this.renderDisplay(sessionId, protocol, width, height); window.addEventListener('resize', this.onWindowResize); window.addEventListener('beforeunload', this.handleUnload); window.onfocus = this.onWindowFocus; } componentWillUnmount() { if (this.state.client) { this.state.client.disconnect(); } window.removeEventListener('beforeunload', this.handleUnload); } sendClipboard(data) { if (this.state.session['paste'] === '0') { // message.warn('禁止粘贴'); return } let writer; // Create stream with proper mimetype const stream = this.state.client.createClipboardStream(data.type); // Send data as a string if it is stored as a string if (typeof data.data === 'string') { writer = new Guacamole.StringWriter(stream); writer.sendText(data.data); writer.sendEnd(); } else { // Write File/Blob asynchronously writer = new Guacamole.BlobWriter(stream); writer.oncomplete = function clipboardSent() { writer.sendEnd(); }; // Begin sending data writer.sendBlob(data.data); } this.setState({ clipboardText: data.data }) if (this.state.protocol === 'ssh') { if (data.data && data.data.length > 0) { // message.info('您输入的内容已复制到远程服务器上,使用右键将自动粘贴。'); } } else { if (data.data && data.data.length > 0) { // message.info('您输入的内容已复制到远程服务器上'); } } } onTunnelStateChange = (state) => { console.log(state) if (state === Guacamole.Tunnel.State.CLOSED) { console.log('web socket 已关闭'); } }; updateSessionStatus = async (sessionId) => { let result = await request.post(`/sessions/${sessionId}/connect`); if (result.code !== 1) { message.error(result.message); } } getCommands = async () => { let result = await request.get('/commands'); if (result.code !== 1) { message.error(result.message); return; } this.setState({ commands: result['data'] }) } onClientStateChange = (state) => { this.setState({ clientState: state }); switch (state) { case STATE_IDLE: message.destroy(); message.loading('正在初始化中...', 0); break; case STATE_CONNECTING: message.destroy(); message.loading('正在努力连接中...', 0); break; case STATE_WAITING: message.destroy(); message.loading('正在等待服务器响应...', 0); break; case STATE_CONNECTED: this.onWindowResize(null); Modal.destroyAll(); message.destroy(); message.success('连接成功'); // 向后台发送请求,更新会话的状态 this.updateSessionStatus(this.state.sessionId).then(_ => { }) if (this.state.protocol === 'ssh' && !this.state.external) { // 加载指令 this.getCommands(); } break; case STATE_DISCONNECTING: break; case STATE_DISCONNECTED: if (!this.error) { this.showMessage('连接已关闭'); } break; default: break; } }; onError = (status) => { this.error = true; console.log('通道异常。', status); switch (status.code) { case 256: this.showMessage('未支持的访问'); break; case 512: this.showMessage('远程服务异常,请检查目标设备能否正常访问。'); break; case 513: this.showMessage('服务器忙碌'); break; case 514: this.showMessage('服务器连接超时'); break; case 515: this.showMessage('远程服务异常'); break; case 516: this.showMessage('资源未找到'); break; case 517: this.showMessage('资源冲突'); break; case 518: this.showMessage('资源已关闭'); break; case 519: this.showMessage('远程服务未找到'); break; case 520: this.showMessage('远程服务不可用'); break; case 521: this.showMessage('会话冲突'); break; case 522: this.showMessage('会话连接超时'); break; case 523: this.showMessage('会话已关闭'); break; case 768: this.showMessage('网络不可达'); break; case 769: this.showMessage('服务器密码验证失败'); break; case 771: this.showMessage('客户端被禁止'); break; case 776: this.showMessage('客户端连接超时'); break; case 781: this.showMessage('客户端异常'); break; case 783: this.showMessage('错误的请求类型'); break; case 800: this.showMessage('会话不存在'); break; case 801: this.showMessage('创建隧道失败,请检查Guacd服务是否正常。'); break; case 802: this.showMessage('管理员强制关闭了此会话'); break; default: if (status.message) { // guacd 无法处理中文字符,所以进行了base64编码。 this.showMessage(Base64.decode(status.message)); } else { this.showMessage('未知错误。'); } } }; showMessage(msg) { message.destroy(); Modal.confirm({ title: '提示', icon: , content: msg, centered: true, okText: '重新连接', cancelText: '关闭页面', onOk() { window.location.reload(); }, onCancel() { window.close(); }, }); } clientClipboardReceived = (stream, mimetype) => { if (this.state.session['copy'] === '0') { // message.warn('禁止复制'); return } let reader; // If the received data is text, read it as a simple string if (/^text\//.exec(mimetype)) { reader = new Guacamole.StringReader(stream); let data = ''; reader.ontext = function textReceived(text) { data += text; }; // Set clipboard contents once stream is finished reader.onend = async () => { // message.info('您选择的内容已复制到您的粘贴板中,在右侧的输入框中可同时查看到。'); this.setState({ clipboardText: data }, () => { // 选中粘贴板文本框中的内容 // 调用API粘贴进 }); if (navigator.clipboard) { await navigator.clipboard.writeText(data); } }; } // Otherwise read the clipboard data as a Blob else { reader = new Guacamole.BlobReader(stream, mimetype); reader.onend = () => { this.setState({ clipboardText: reader.getBlob() }) } } }; onKeyDown = (keysym) => { this.state.client.sendKeyEvent(1, keysym); if (keysym === 65288) { return false; } }; onKeyUp = (keysym) => { this.state.client.sendKeyEvent(0, keysym); }; fullScreen = () => { let fs = this.state.fullScreen; if (fs) { exitFull(); this.setState({ fullScreen: false, fullScreenBtnText: '进入全屏' }) } else { requestFullScreen(document.documentElement); this.setState({ fullScreen: true, fullScreenBtnText: '退出全屏' }) } this.focus(); } async createSession(assetsId) { let result = await request.post(`/sessions?assetId=${assetsId}&mode=guacd`); if (result['code'] !== 1) { this.showMessage(result['message']); return undefined; } return result['data']; } async getShareSession(shareSessionId) { let result = await request.get(`/share-sessions/${shareSessionId}`); if (result['code'] !== 1) { this.showMessage(result['message']); return undefined; } return result['data']; } async renderDisplay(sessionId, protocol, width, height) { let tunnel = new Guacamole.WebSocketTunnel(`${wsServer}/sessions/${sessionId}/tunnel`); tunnel.onstatechange = this.onTunnelStateChange; // Get new client instance let client = new Guacamole.Client(tunnel); // 设置虚拟机剪贴板内容 client.sendClipboard = this.sendClipboard; // 处理从虚拟机收到的剪贴板内容 client.onclipboard = this.clientClipboardReceived; // 处理客户端的状态变化事件 client.onstatechange = this.onClientStateChange; client.onerror = this.onError; tunnel.onerror = this.onError; // Get display div from document const display = document.getElementById("display"); // Add client to display div const element = client.getDisplay().getElement(); display.appendChild(element); let scale = 1; let dpi = 96; if (protocol === 'ssh' || protocol === 'telnet') { dpi = dpi * 2; scale = 0.5; } let token = getToken(); let params = { 'width': width, 'height': height, 'dpi': dpi, 'X-Auth-Token': token }; let paramStr = qs.stringify(params); // Connect client.connect(paramStr); // Disconnect on close window.onunload = function () { client.disconnect(); }; // Mouse const mouse = new Guacamole.Mouse(element); mouse.onmousedown = mouse.onmouseup = function (mouseState) { client.sendMouseState(mouseState); }; mouse.onmousemove = function (mouseState) { mouseState.x = mouseState.x / scale; mouseState.y = mouseState.y / scale; client.sendMouseState(mouseState); }; const sink = new Guacamole.InputSink(); display.appendChild(sink.getElement()); sink.focus(); // Keyboard const keyboard = new Guacamole.Keyboard(sink.getElement()); keyboard.onkeydown = this.onKeyDown; keyboard.onkeyup = this.onKeyUp; this.setState({ client: client, scale: scale, keyboard: keyboard, sink: sink }); } onWindowResize = (e) => { if (this.state.client && !this.state.fixedSize) { const display = this.state.client.getDisplay(); let scale = this.state.scale; display.scale(scale); let width = window.innerWidth; let height = window.innerHeight; this.state.client.sendSize(width / scale, height / scale); this.setState({ containerWidth: width, containerHeight: height, }) this.resize(this.state.sessionId, width, height).then(_ => { }); } }; handleUnload(e) { var message = "要离开网站吗?"; (e || window.event).returnValue = message; //Gecko + IE return message; } resize = async (sessionId, width, height) => { let result = await request.post(`/sessions/${sessionId}/resize?width=${width}&height=${height}`); if (result.code !== 1) { message.error(result.message); } } onWindowFocus = (e) => { if (navigator.clipboard && this.state.clientState === STATE_CONNECTED) { try { navigator.clipboard.readText().then((text) => { this.sendClipboard({ 'data': text, 'type': 'text/plain' }); }) } catch (e) { // console.error(e); } } }; focus = () => { if (this.state.sink) { this.state.sink.focus(); } } sendCombinationKey = (keys) => { for (let i = 0; i < keys.length; i++) { this.state.client.sendKeyEvent(1, keys[i]); } for (let j = 0; j < keys.length; j++) { this.state.client.sendKeyEvent(0, keys[j]); } } writeCommand = (command) => { let client = this.state.client; let outputStream = client.createPipeStream('text/plain', 'STDIN'); // Wrap output stream in writer let writer = new Guacamole.StringWriter(outputStream); writer.sendText(command); writer.sendEnd(); this.focus(); } onRef = (statsRef) => { this.statsRef = statsRef; } render() { const hotKeyMenu = ( this.sendCombinationKey(['65507', '65513', '65535'])}>Ctrl+Alt+Delete this.sendCombinationKey(['65507', '65513', '65288'])}>Ctrl+Alt+Backspace this.sendCombinationKey(['65515', '100'])}>Windows+D this.sendCombinationKey(['65515', '101'])}>Windows+E this.sendCombinationKey(['65515', '114'])}>Windows+R this.sendCombinationKey(['65515', '120'])}>Windows+X this.sendCombinationKey(['65515'])}>Windows ); const cmdMenuItems = this.state.commands.map(item => { return { this.writeCommand(item['content']) }} key={'i-' + item['id']}>{item['name']} ; }); const cmdMenu = ( {cmdMenuItems} ); return (