优化批量执行指令和windows接入发送组合键

This commit is contained in:
dushixiang 2021-01-23 22:41:44 +08:00
parent ba982dacd0
commit 038f59d155
10 changed files with 290 additions and 91 deletions

View File

@ -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,13 +161,50 @@ 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)
switch msg.Type {
case Resize:
var winSize WindowSize
err = json.Unmarshal([]byte(msg.Content), &winSize)
if err != nil {
logrus.Debugf("Tunnel write: %v", err)
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
}
@ -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 {

View File

@ -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 != "-" {

20
web/package-lock.json generated
View File

@ -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",

View File

@ -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",

View File

@ -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 {
<Menu.Item key="2" icon={<FileZipOutlined/>} onClick={this.showFileSystem}>
文件管理
</Menu.Item>
<Menu.Item key="3" icon={<ExpandOutlined />} onClick={this.fullScreen}>
{this.state.fullScreenBtnText}
</Menu.Item>
<SubMenu title="发送快捷键" icon={<DesktopOutlined/>}>
<Menu.Item
onClick={() => this.sendCombinationKey(['65507', '65513', '65535'])}>Ctrl+Alt+Delete</Menu.Item>
<Menu.Item
onClick={() => this.sendCombinationKey(['65507', '65513', '65288'])}>Ctrl+Alt+Backspace</Menu.Item>
<Menu.Item
onClick={() => this.sendCombinationKey(['65515', '114'])}>Windows+R</Menu.Item>
<Menu.Item
onClick={() => this.sendCombinationKey(['65515'])}>Windows</Menu.Item>
</SubMenu>
</Menu>
);
@ -892,22 +920,6 @@ class Access extends Component {
</Affix>
</Draggable>
{/*{*/}
{/* this.state.protocol === 'ssh' || this.state.protocol === 'rdp' ?*/}
{/* <Affix style={{position: 'absolute', top: 50, right: 50}}>*/}
{/* <Button*/}
{/* shape="circle"*/}
{/* icon={<FolderOpenOutlined/>}*/}
{/* onClick={() => {*/}
{/* this.showFileSystem();*/}
{/* }}*/}
{/* >*/}
{/* </Button>*/}
{/* </Affix>*/}
{/* : null*/}
{/*}*/}
<Drawer
title={title}
placement="right"

View File

@ -1,16 +1,16 @@
import React, {Component} from 'react';
import "xterm/css/xterm.css"
import {Terminal} from "xterm";
import {AttachAddon} from 'xterm-addon-attach';
import qs from "qs";
import {wsServer} from "../../common/constants";
import "./Console.css"
import {getToken} from "../../utils/utils";
import {FitAddon} from 'xterm-addon-fit'
function getGeometry(width, height) {
const cols = Math.floor(width / 9);
const rows = Math.floor(height / 17);
const rows = Math.floor(height / 17) - 1;
return [cols, rows];
}
@ -18,9 +18,11 @@ class Console extends Component {
state = {
containerOverflow: 'hidden',
containerWidth: 0,
containerHeight: 0,
term: null,
width: 0,
height: 0,
term: undefined,
webSocket: undefined,
fitAddon: undefined
};
componentDidMount() {
@ -30,9 +32,6 @@ class Console extends Component {
let width = this.props.width;
let height = this.props.height;
// let width = Math.floor(window.innerWidth * scale);
// let height = Math.floor(window.innerHeight * scale);
let params = {
'width': width,
'height': height,
@ -41,66 +40,127 @@ class Console extends Component {
let paramStr = qs.stringify(params);
let [cols, rows] = getGeometry(width, height);
const ua = navigator.userAgent.toLowerCase();
let lineHeight = 1;
if (ua.includes('windows')) {
lineHeight = 1.1;
}
let term = new Terminal({
cols: cols,
rows: rows,
// screenKeys: true,
// fontFamily: 'menlo',
fontFamily: 'monaco, Consolas, "Lucida Console", monospace',
fontSize: 14,
lineHeight: lineHeight,
theme: {
background: '#1b1b1b'
},
rightClickSelectsWord: true,
});
// let fitAddon = new FitAddon();
// term.loadAddon(fitAddon);
let fitAddon = new FitAddon();
term.loadAddon(fitAddon);
term.open(this.refs.terminal);
// fitAddon.fit();
term.writeln('正在努力连接服务器中...');
term.writeln('Trying to connect to the server ...');
term.onResize(e => {
});
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);
term.loadAddon(new AttachAddon(webSocket));
this.props.appendWebsocket(webSocket);
this.props.appendWebsocket({'id': assetId, 'ws': webSocket});
webSocket.onopen = (e => {
term.clear();
term.focus();
if (command !== '') {
webSocket.send(command + String.fromCharCode(13));
}
this.onWindowResize();
});
webSocket.onerror = (e) => {
term.writeln("Failed to connect to server.");
}
webSocket.onclose = (e) => {
term.writeln("Connection is closed.");
}
let executedCommand = false
webSocket.onmessage = (e) => {
let msg = JSON.parse(e.data);
switch (msg['type']) {
case 'data':
term.write(msg['content']);
break;
case 'closed':
term.writeln(`\x1B[1;3;31m${msg['content']}\x1B[0m `)
webSocket.close();
break;
}
if (!executedCommand) {
if (command !== '') {
let webSocket = this.state.webSocket;
if (webSocket !== undefined && webSocket.readyState === WebSocket.OPEN) {
webSocket.send(JSON.stringify({type: 'data', content: command + String.fromCharCode(13)}));
}
}
executedCommand = true;
}
}
this.setState({
term: term,
containerWidth: width,
containerHeight: height
fitAddon: fitAddon,
webSocket: webSocket,
width: width,
height: height
});
// window.addEventListener('resize', this.onWindowResize);
window.addEventListener('resize', this.onWindowResize);
}
componentWillUnmount() {
let webSocket = this.state.webSocket;
if (webSocket) {
webSocket.close()
}
}
onWindowResize = (e) => {
let term = this.state.term;
if (term) {
const [cols, rows] = getGeometry(this.state.containerWidth, this.state.containerHeight);
term.resize(cols, rows);
let fitAddon = this.state.fitAddon;
let webSocket = this.state.webSocket;
if (term && fitAddon && webSocket) {
let height = term.cols;
let width = term.rows;
try {
fitAddon.fit();
} catch (e) {
console.log(e);
}
term.focus();
if(webSocket.readyState === WebSocket.OPEN){
webSocket.send(JSON.stringify({type: 'resize', content: JSON.stringify({height, width})}));
}
}
};
render() {
return (
<div>
<div ref='terminal' style={{
<div ref='terminal' id='terminal' style={{
overflow: this.state.containerOverflow,
width: this.state.containerWidth,
height: this.state.containerHeight,
backgroundColor: 'black'
width: this.state.width,
height: this.state.height,
backgroundColor: '#1b1b1b'
}}/>
</div>
);

View File

@ -249,6 +249,8 @@ class Asset extends Component {
} else {
asset['tags'] = asset['tags'].split(',');
}
}else {
asset['tags'] = [];
}
this.setState({

View File

@ -1,8 +1,10 @@
import React, {Component} from 'react';
import {List, Card, Input, PageHeader} from "antd";
import {Card, Input, List, PageHeader, Popconfirm} from "antd";
import Console from "../access/Console";
import {itemRender} from "../../utils/utils";
import Logout from "../user/Logout";
import './Command.css'
const {Search} = Input;
const routes = [
{
@ -25,7 +27,8 @@ class BatchCommand extends Component {
state = {
webSockets: [],
assets: []
assets: [],
active: undefined,
}
componentDidMount() {
@ -66,7 +69,13 @@ class BatchCommand extends Component {
<div className="page-search">
<Search ref={this.commandRef} placeholder="请输入指令" onSearch={value => {
for (let i = 0; i < this.state.webSockets.length; i++) {
this.state.webSockets[i].send(value + String.fromCharCode(13))
let ws = this.state.webSockets[i]['ws'];
if (ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify({
type: 'data',
content: value + String.fromCharCode(13)
}));
}
}
this.commandRef.current.setValue('');
}} enterButton='执行'/>
@ -78,10 +87,23 @@ class BatchCommand extends Component {
dataSource={this.state.assets}
renderItem={item => (
<List.Item>
<Card title={item.name}>
<Card title={item.name}
className={this.state.active === item['id'] ? 'command-active' : ''}
onClick={() => {
if (this.state.active === item['id']) {
this.setState({
active: undefined
})
} else {
this.setState({
active: item['id']
})
}
}}
>
<Console assetId={item.id} command={this.state.command}
width={(window.innerWidth - 350) / 2}
height={400}
height={420}
appendWebsocket={this.appendWebsocket}/>
</Card>
</List.Item>

View File

@ -0,0 +1,4 @@
.command-active {
box-shadow: 0 0 0 2px #1890FF;
outline: 2px solid #1890FF;
}

View File

@ -78,7 +78,7 @@ export const cloneObj = (obj, ignoreFields) => {
export function download(url) {
let aElement = document.createElement('a');
aElement.setAttribute('download', '');
aElement.setAttribute('target', '_blank');
// aElement.setAttribute('target', '_blank');
aElement.setAttribute('href', url);
aElement.click();
}
@ -177,3 +177,36 @@ export function difference(a, b) {
let bSet = new Set(b)
return Array.from(new Set(a.concat(b).filter(v => !aSet.has(v) || !bSet.has(v))))
}
export function requestFullScreen(element) {
// 判断各种浏览器,找到正确的方法
const requestMethod = element.requestFullScreen || //W3C
element.webkitRequestFullScreen || //FireFox
element.mozRequestFullScreen || //Chrome等
element.msRequestFullScreen; //IE11
if (requestMethod) {
requestMethod.call(element);
} else if (typeof window.ActiveXObject !== "undefined") { //for Internet Explorer
const wScript = new window.ActiveXObject("WScript.Shell");
if (wScript !== null) {
wScript.SendKeys("{F11}");
}
}
}
//退出全屏 判断浏览器种类
export function exitFull() {
// 判断各种浏览器,找到正确的方法
const exitMethod = document.exitFullscreen || //W3C
document.mozCancelFullScreen || //FireFox
document.webkitExitFullscreen || //Chrome等
document.webkitExitFullscreen; //IE11
if (exitMethod) {
exitMethod.call(document);
} else if (typeof window.ActiveXObject !== "undefined") { //for Internet Explorer
const wScript = new window.ActiveXObject("WScript.Shell");
if (wScript !== null) {
wScript.SendKeys("{F11}");
}
}
}