提交 v1.3.0-beta2

This commit is contained in:
dushixiang
2022-10-25 21:18:48 +08:00
parent effe708bf3
commit 59d1a0bcd9
3 changed files with 705 additions and 0 deletions

109
server/service/migrate.go Normal file
View File

@ -0,0 +1,109 @@
package service
import (
"context"
"errors"
"strings"
"next-terminal/server/branding"
"next-terminal/server/common"
"next-terminal/server/env"
"next-terminal/server/model"
"next-terminal/server/repository"
"next-terminal/server/utils"
"gorm.io/gorm"
)
type resourceSharer struct {
ID string `gorm:"primary_key,type:varchar(36)" json:"id"`
ResourceId string `gorm:"index,type:varchar(36)" json:"resourceId"`
ResourceType string `gorm:"index,type:varchar(36)" json:"resourceType"`
StrategyId string `gorm:"index,type:varchar(36)" json:"strategyId"`
UserId string `gorm:"index,type:varchar(36)" json:"userId"`
UserGroupId string `gorm:"index,type:varchar(36)" json:"userGroupId"`
}
var MigrateService = &migrateService{}
type migrateService struct {
baseService
}
func (s *migrateService) Migrate() error {
var needMigrate = false
var localVersion = ""
property, err := repository.PropertyRepository.FindByName(context.Background(), "version")
if err != nil {
if !errors.Is(err, gorm.ErrRecordNotFound) {
return err
}
// 未获取到版本数据
needMigrate = true
} else {
localVersion = property.Value
// 数据库版本小于当前版本
needMigrate = strings.Compare(localVersion, branding.Version) < 0
}
if !needMigrate {
return nil
}
if err := s.migrateFormV127to130(localVersion); err != nil {
return err
}
return PropertyService.Update(map[string]interface{}{"version": branding.Version})
}
func (s *migrateService) migrateFormV127to130(localVersion string) (err error) {
if !strings.Contains(localVersion, "beta") && strings.Compare(localVersion, "v1.3.0") > 0 {
return nil
}
err = env.GetDB().Exec(`update strategies set create_dir = 0 where create_dir = ''`).Error
if err != nil {
return err
}
ctx := context.Background()
var results []resourceSharer
err = env.GetDB().Raw(`select * from resource_sharers where resource_type = 'asset'`).Find(&results).Error
if err != nil {
// 数据库不存在
return nil
}
// 证明存在旧数据库,执行迁移
var items []model.Authorised
for _, result := range results {
assetId := result.ResourceId
strategyId := result.StrategyId
userId := result.UserId
userGroupId := result.UserGroupId
id := utils.Sign([]string{assetId, userId, userGroupId})
if err := repository.AuthorisedRepository.DeleteById(ctx, id); err != nil {
return err
}
authorised := model.Authorised{
ID: id,
AssetId: assetId,
CommandFilterId: "",
StrategyId: strategyId,
UserId: userId,
UserGroupId: userGroupId,
Created: common.NowJsonTime(),
}
items = append(items, authorised)
}
err = repository.AuthorisedRepository.CreateInBatches(ctx, items)
if err != nil {
return err
}
// 删除旧数据库
return env.GetDB().Exec(`drop table resource_sharers`).Error
}

View File

@ -0,0 +1,548 @@
import React, {useEffect, useState} from 'react';
import {useSearchParams} from "react-router-dom";
import sessionApi from "../../api/session";
import strings from "../../utils/strings";
import Guacamole from "guacamole-common-js";
import {wsServer} from "../../common/env";
import {exitFull, getToken, requestFullScreen} from "../../utils/utils";
import qs from "qs";
import {Affix, Button, Drawer, Dropdown, Menu, message, Modal} from "antd";
import {
CopyOutlined,
ExclamationCircleOutlined,
ExpandOutlined,
FolderOutlined,
WindowsOutlined
} from "@ant-design/icons";
import {Base64} from "js-base64";
import Draggable from "react-draggable";
import FileSystem from "../devops/FileSystem";
import GuacdClipboard from "./GuacdClipboard";
import {debounce} from "../../utils/fun";
let fixedSize = false;
const STATE_IDLE = 0;
const STATE_CONNECTING = 1;
const STATE_WAITING = 2;
const STATE_CONNECTED = 3;
const STATE_DISCONNECTING = 4;
const STATE_DISCONNECTED = 5;
const Guacd = () => {
let [searchParams] = useSearchParams();
let assetId = searchParams.get('assetId');
let assetName = searchParams.get('assetName');
let protocol = searchParams.get('protocol');
let width = searchParams.get('width');
let height = searchParams.get('height');
if (width && height) {
fixedSize = true;
} else {
width = window.innerWidth;
height = window.innerHeight;
}
const [box, setBox] = useState({width: width, height: height});
let [guacd, setGuacd] = useState({});
let [session, setSession] = useState({});
let [clipboardText, setClipboardText] = useState('');
let [fullScreened, setFullScreened] = useState(false);
let [clientState, setClientState] = useState(STATE_IDLE);
let [clipboardVisible, setClipboardVisible] = useState(false);
let [fileSystemVisible, setFileSystemVisible] = useState(false);
useEffect(() => {
document.title = assetName;
const renderDisplay = (sessionId, protocol, width, height) => {
let tunnel = new Guacamole.WebSocketTunnel(`${wsServer}/sessions/${sessionId}/tunnel`);
let client = new Guacamole.Client(tunnel);
// 处理从虚拟机收到的剪贴板内容
client.onclipboard = handleClipboardReceived;
// 处理客户端的状态变化事件
client.onstatechange = (state) => {
onClientStateChange(state, sessionId);
};
client.onerror = onError;
tunnel.onerror = 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 === '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);
client.connect(paramStr);
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();
const keyboard = new Guacamole.Keyboard(sink.getElement());
keyboard.onkeydown = (keysym) => {
client.sendKeyEvent(1, keysym);
if (keysym === 65288) {
return false;
}
};
keyboard.onkeyup = (keysym) => {
client.sendKeyEvent(0, keysym);
};
setGuacd({
client,
scale,
sink,
});
}
const x = async () => {
let session = await sessionApi.create(assetId, 'guacd');
if (!strings.hasText(session['id'])) {
return;
}
setSession(session);
renderDisplay(session['id'], protocol, width, height);
}
x();
}, [assetId, assetName]);
useEffect(() => {
let resize = debounce(() => {
onWindowResize();
});
window.addEventListener('resize', resize);
window.addEventListener('beforeunload', handleUnload);
window.addEventListener('focus', handleWindowFocus);
return () => {
window.removeEventListener('resize', resize);
window.removeEventListener('beforeunload', handleUnload);
window.removeEventListener('focus', handleWindowFocus);
};
}, [guacd])
const onWindowResize = () => {
console.log(guacd, fixedSize);
if (guacd.client && !fixedSize) {
const display = guacd.client.getDisplay();
let scale = guacd.scale;
display.scale(scale);
let width = window.innerWidth;
let height = window.innerHeight;
guacd.client.sendSize(width / scale, height / scale);
setBox({width, height})
}
}
const handleUnload = (e) => {
const message = "要离开网站吗?";
(e || window.event).returnValue = message; //Gecko + IE
return message;
}
const focus = () => {
if (guacd.sink) {
guacd.sink.focus();
}
}
const handleWindowFocus = (e) => {
if (navigator.clipboard) {
try {
navigator.clipboard.readText().then((text) => {
sendClipboard({
'data': text,
'type': 'text/plain'
});
})
} catch (e) {
// console.error(e);
}
}
};
const handleClipboardReceived = (stream, mimetype) => {
if (session['copy'] === '0') {
message.warn('禁止复制');
return
}
if (/^text\//.exec(mimetype)) {
let reader = new Guacamole.StringReader(stream);
let data = '';
reader.ontext = function textReceived(text) {
data += text;
};
reader.onend = async () => {
setClipboardText(data);
if (navigator.clipboard) {
await navigator.clipboard.writeText(data);
}
message.info('您选择的内容已复制到您的粘贴板中,在右侧的输入框中可同时查看到。');
};
} else {
let reader = new Guacamole.BlobReader(stream, mimetype);
reader.onend = () => {
console.log(stream, mimetype, reader)
setClipboardText(reader.getBlob());
}
}
};
const sendClipboard = (data) => {
if (clientState !== STATE_CONNECTED) {
return;
}
if (!guacd.client) {
return;
}
if (session['paste'] === '0') {
message.warn('禁止粘贴');
return
}
const stream = guacd.client.createClipboardStream(data.type);
if (typeof data.data === 'string') {
let writer = new Guacamole.StringWriter(stream);
writer.sendText(data.data);
writer.sendEnd();
} else {
let writer = new Guacamole.BlobWriter(stream);
writer.oncomplete = function clipboardSent() {
writer.sendEnd();
};
writer.sendBlob(data.data);
}
if (data.data && data.data.length > 0) {
message.info('您输入的内容已复制到远程服务器上');
}
}
const onClientStateChange = (state, sessionId) => {
setClientState(state);
const key = 'message';
switch (state) {
case STATE_IDLE:
message.destroy(key);
message.loading({content: '正在初始化中...', duration: 0, key: key});
break;
case STATE_CONNECTING:
message.destroy(key);
message.loading({content: '正在努力连接中...', duration: 0, key: key});
break;
case STATE_WAITING:
message.destroy(key);
message.loading({content: '正在等待服务器响应...', duration: 0, key: key});
break;
case STATE_CONNECTED:
Modal.destroyAll();
message.destroy(key);
message.success({content: '连接成功', duration: 3, key: key});
// 向后台发送请求,更新会话的状态
sessionApi.connect(sessionId);
break;
case STATE_DISCONNECTING:
break;
case STATE_DISCONNECTED:
message.info({content: '连接已关闭', duration: 3, key: key});
break;
default:
break;
}
};
const sendCombinationKey = (keys) => {
if (!guacd.client) {
return;
}
for (let i = 0; i < keys.length; i++) {
guacd.client.sendKeyEvent(1, keys[i]);
}
for (let j = 0; j < keys.length; j++) {
guacd.client.sendKeyEvent(0, keys[j]);
}
}
const showMessage = (msg) => {
message.destroy();
Modal.confirm({
title: '提示',
icon: <ExclamationCircleOutlined/>,
content: msg,
centered: true,
okText: '重新连接',
cancelText: '关闭页面',
onOk() {
window.location.reload();
},
onCancel() {
window.close();
},
});
}
const onError = (status) => {
console.log('通道异常。', status);
switch (status.code) {
case 256:
showMessage('未支持的访问');
break;
case 512:
showMessage('远程服务异常,请检查目标设备能否正常访问。');
break;
case 513:
showMessage('服务器忙碌');
break;
case 514:
showMessage('服务器连接超时');
break;
case 515:
showMessage('远程服务异常');
break;
case 516:
showMessage('资源未找到');
break;
case 517:
showMessage('资源冲突');
break;
case 518:
showMessage('资源已关闭');
break;
case 519:
showMessage('远程服务未找到');
break;
case 520:
showMessage('远程服务不可用');
break;
case 521:
showMessage('会话冲突');
break;
case 522:
showMessage('会话连接超时');
break;
case 523:
showMessage('会话已关闭');
break;
case 768:
showMessage('网络不可达');
break;
case 769:
showMessage('服务器密码验证失败');
break;
case 771:
showMessage('客户端被禁止');
break;
case 776:
showMessage('客户端连接超时');
break;
case 781:
showMessage('客户端异常');
break;
case 783:
showMessage('错误的请求类型');
break;
case 800:
showMessage('会话不存在');
break;
case 801:
showMessage('创建隧道失败请检查Guacd服务是否正常。');
break;
case 802:
showMessage('管理员强制关闭了此会话');
break;
default:
if (status.message) {
// guacd 无法处理中文字符所以进行了base64编码。
showMessage(Base64.decode(status.message));
} else {
showMessage('未知错误。');
}
}
};
const fullScreen = () => {
if (fullScreened) {
exitFull();
setFullScreened(false);
} else {
requestFullScreen(document.documentElement);
setFullScreened(true);
}
focus();
}
const hotKeyMenu = (
<Menu>
<Menu.Item key={'ctrl+alt+delete'}
onClick={() => sendCombinationKey(['65507', '65513', '65535'])}>Ctrl+Alt+Delete</Menu.Item>
<Menu.Item key={'ctrl+alt+backspace'}
onClick={() => sendCombinationKey(['65507', '65513', '65288'])}>Ctrl+Alt+Backspace</Menu.Item>
<Menu.Item key={'windows+d'}
onClick={() => sendCombinationKey(['65515', '100'])}>Windows+D</Menu.Item>
<Menu.Item key={'windows+e'}
onClick={() => sendCombinationKey(['65515', '101'])}>Windows+E</Menu.Item>
<Menu.Item key={'windows+r'}
onClick={() => sendCombinationKey(['65515', '114'])}>Windows+R</Menu.Item>
<Menu.Item key={'windows+x'}
onClick={() => sendCombinationKey(['65515', '120'])}>Windows+X</Menu.Item>
<Menu.Item key={'windows'}
onClick={() => sendCombinationKey(['65515'])}>Windows</Menu.Item>
</Menu>
);
return (
<div>
<div className="container" style={{
overflow: 'hidden',
width: box.width,
height: box.height,
margin: '0 auto'
}}>
<div id="display"/>
</div>
<Draggable>
<Affix style={{position: 'absolute', top: 50, right: 50}}>
<Button icon={<ExpandOutlined/>} disabled={clientState !== STATE_CONNECTED}
onClick={() => {
fullScreen();
}}/>
</Affix>
</Draggable>
{
session['copy'] === '1' || session['paste'] === '1' ?
<Draggable>
<Affix style={{position: 'absolute', top: 50, right: 100}}>
<Button icon={<CopyOutlined/>} disabled={clientState !== STATE_CONNECTED}
onClick={() => {
setClipboardVisible(true);
}}/>
</Affix>
</Draggable> : undefined
}
{
protocol === 'vnc' &&
<Draggable>
<Affix style={{position: 'absolute', top: 100, right: 100}}>
<Dropdown overlay={hotKeyMenu} trigger={['click']} placement="bottomLeft">
<Button icon={<WindowsOutlined/>}
disabled={clientState !== STATE_CONNECTED}/>
</Dropdown>
</Affix>
</Draggable>
}
{
(protocol === 'rdp' && session['fileSystem'] === '1') &&
<Draggable>
<Affix style={{position: 'absolute', top: 100, right: 50}}>
<Button icon={<FolderOutlined/>}
disabled={clientState !== STATE_CONNECTED} onClick={() => {
setFileSystemVisible(true);
}}/>
</Affix>
</Draggable>
}
{
protocol === 'rdp' &&
<Draggable>
<Affix style={{position: 'absolute', top: 100, right: 100}}>
<Dropdown overlay={hotKeyMenu} trigger={['click']} placement="bottomLeft">
<Button icon={<WindowsOutlined/>}
disabled={clientState !== STATE_CONNECTED}/>
</Dropdown>
</Affix>
</Draggable>
}
<Drawer
title={'文件管理'}
placement="right"
width={window.innerWidth * 0.8}
closable={true}
onClose={() => {
focus();
setFileSystemVisible(false);
}}
visible={fileSystemVisible}
>
<FileSystem
storageId={session['id']}
storageType={'sessions'}
upload={session['upload'] === '1'}
download={session['download'] === '1'}
delete={session['delete'] === '1'}
rename={session['rename'] === '1'}
edit={session['edit'] === '1'}
minHeight={window.innerHeight - 103}/>
</Drawer>
<GuacdClipboard
visible={clipboardVisible}
clipboardText={clipboardText}
handleOk={(text) => {
sendClipboard({
'data': text,
'type': 'text/plain'
});
setClipboardText(text);
setClipboardVisible(false);
}}
handleCancel={() => {
setClipboardVisible(false);
}}
/>
</div>
);
};
export default Guacd;

View File

@ -0,0 +1,48 @@
import React, {useEffect, useState} from 'react';
import {Form, Input, Modal} from "antd";
const GuacdClipboard = ({visible, clipboardText, handleOk, handleCancel}) => {
const [form] = Form.useForm();
let [confirmLoading, setConfirmLoading] = useState(false);
useEffect(() => {
form.setFieldsValue({
'clipboard': clipboardText
})
}, [visible]);
return (
<div>
<Modal
title="剪贴板"
maskClosable={false}
visible={visible}
onOk={() => {
form.validateFields()
.then(values => {
setConfirmLoading(true);
try {
handleOk(values['clipboard']);
} finally {
setConfirmLoading(false);
}
})
.catch(info => {
});
}}
confirmLoading={confirmLoading}
onCancel={handleCancel}
>
<Form form={form}>
<Form.Item name='clipboard'>
<Input.TextArea rows={10}/>
</Form.Item>
</Form>
</Modal>
</div>
);
};
export default GuacdClipboard;