mirror of
https://github.com/whyour/qinglong.git
synced 2025-07-27 14:46:06 +08:00
Merge branch 'develop'
This commit is contained in:
commit
1c7c20dc76
|
@ -11,7 +11,8 @@ export default defineConfig({
|
||||||
favicon: 'https://qinglong.whyour.cn/g5.ico',
|
favicon: 'https://qinglong.whyour.cn/g5.ico',
|
||||||
proxy: {
|
proxy: {
|
||||||
'/api': {
|
'/api': {
|
||||||
target: 'http://127.0.0.1:5678/',
|
target: 'http://127.0.0.1:5600',
|
||||||
|
ws: true,
|
||||||
changeOrigin: true,
|
changeOrigin: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
@ -4,6 +4,7 @@ import cookie from './cookie';
|
||||||
import config from './config';
|
import config from './config';
|
||||||
import log from './log';
|
import log from './log';
|
||||||
import cron from './cron';
|
import cron from './cron';
|
||||||
|
import terminal from './terminal';
|
||||||
|
|
||||||
export default () => {
|
export default () => {
|
||||||
const app = Router();
|
const app = Router();
|
||||||
|
@ -12,6 +13,7 @@ export default () => {
|
||||||
config(app);
|
config(app);
|
||||||
log(app);
|
log(app);
|
||||||
cron(app);
|
cron(app);
|
||||||
|
terminal(app);
|
||||||
|
|
||||||
return app;
|
return app;
|
||||||
};
|
};
|
||||||
|
|
113
back/api/terminal.ts
Normal file
113
back/api/terminal.ts
Normal file
|
@ -0,0 +1,113 @@
|
||||||
|
import { Router } from 'express';
|
||||||
|
import * as pty from 'node-pty';
|
||||||
|
import os from 'os';
|
||||||
|
// Whether to use binary transport.
|
||||||
|
const USE_BINARY = os.platform() !== 'win32';
|
||||||
|
const route = Router();
|
||||||
|
|
||||||
|
export default (app: Router) => {
|
||||||
|
const terminals = {};
|
||||||
|
const logs = {};
|
||||||
|
app.use('/', route);
|
||||||
|
route.post('/terminals', (req, res) => {
|
||||||
|
const env = Object.assign({}, process.env);
|
||||||
|
env['COLORTERM'] = 'truecolor';
|
||||||
|
var cols = parseInt(req.query.cols),
|
||||||
|
rows = parseInt(req.query.rows),
|
||||||
|
term = pty.spawn(process.platform === 'win32' ? 'cmd.exe' : 'bash', [], {
|
||||||
|
name: 'xterm-256color',
|
||||||
|
cols: cols || 80,
|
||||||
|
rows: rows || 24,
|
||||||
|
cwd: process.platform === 'win32' ? undefined : env.PWD,
|
||||||
|
env: env,
|
||||||
|
encoding: USE_BINARY ? null : 'utf8',
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('Created terminal with PID: ' + term.pid);
|
||||||
|
terminals[term.pid] = term;
|
||||||
|
logs[term.pid] = '';
|
||||||
|
term.on('data', function (data) {
|
||||||
|
logs[term.pid] += data;
|
||||||
|
});
|
||||||
|
res.send(term.pid.toString());
|
||||||
|
res.end();
|
||||||
|
});
|
||||||
|
|
||||||
|
route.post('/terminals/:pid/size', (req, res) => {
|
||||||
|
var pid = parseInt(req.params.pid),
|
||||||
|
cols = parseInt(req.query.cols),
|
||||||
|
rows = parseInt(req.query.rows),
|
||||||
|
term = terminals[pid];
|
||||||
|
|
||||||
|
term.resize(cols, rows);
|
||||||
|
console.log(
|
||||||
|
'Resized terminal ' +
|
||||||
|
pid +
|
||||||
|
' to ' +
|
||||||
|
cols +
|
||||||
|
' cols and ' +
|
||||||
|
rows +
|
||||||
|
' rows.',
|
||||||
|
);
|
||||||
|
res.end();
|
||||||
|
});
|
||||||
|
|
||||||
|
route.ws('/terminals/:pid', function (ws, req) {
|
||||||
|
var term = terminals[parseInt(req.params.pid)];
|
||||||
|
console.log('Connected to terminal ' + term.pid);
|
||||||
|
ws.send(logs[term.pid]);
|
||||||
|
|
||||||
|
// string message buffering
|
||||||
|
function buffer(socket, timeout) {
|
||||||
|
let s = '';
|
||||||
|
let sender = null;
|
||||||
|
return (data) => {
|
||||||
|
s += data;
|
||||||
|
if (!sender) {
|
||||||
|
sender = setTimeout(() => {
|
||||||
|
socket.send(s);
|
||||||
|
s = '';
|
||||||
|
sender = null;
|
||||||
|
}, timeout);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
// binary message buffering
|
||||||
|
function bufferUtf8(socket, timeout) {
|
||||||
|
let buffer = [];
|
||||||
|
let sender = null;
|
||||||
|
let length = 0;
|
||||||
|
return (data) => {
|
||||||
|
buffer.push(data);
|
||||||
|
length += data.length;
|
||||||
|
if (!sender) {
|
||||||
|
sender = setTimeout(() => {
|
||||||
|
socket.send(Buffer.concat(buffer, length));
|
||||||
|
buffer = [];
|
||||||
|
sender = null;
|
||||||
|
length = 0;
|
||||||
|
}, timeout);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const send = USE_BINARY ? bufferUtf8(ws, 5) : buffer(ws, 5);
|
||||||
|
|
||||||
|
term.on('data', function (data) {
|
||||||
|
try {
|
||||||
|
send(data);
|
||||||
|
} catch (ex) {
|
||||||
|
// The WebSocket is not open, ignore
|
||||||
|
}
|
||||||
|
});
|
||||||
|
ws.on('message', function (msg) {
|
||||||
|
term.write(msg);
|
||||||
|
});
|
||||||
|
ws.on('close', function () {
|
||||||
|
term.kill();
|
||||||
|
console.log('Closed terminal ' + term.pid);
|
||||||
|
// Clean things up
|
||||||
|
delete terminals[term.pid];
|
||||||
|
delete logs[term.pid];
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
|
@ -5,24 +5,77 @@ import routes from '../api';
|
||||||
import config from '../config';
|
import config from '../config';
|
||||||
import jwt from 'express-jwt';
|
import jwt from 'express-jwt';
|
||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
|
import http from 'http';
|
||||||
|
import expressWs from 'express-ws';
|
||||||
|
import Logger from './logger';
|
||||||
|
|
||||||
|
const excludePath = ['/api/login'];
|
||||||
|
|
||||||
|
const auth = (getToken?: jwt.Options['getToken']) =>
|
||||||
|
jwt({
|
||||||
|
secret: config.secret as string,
|
||||||
|
algorithms: ['HS384'],
|
||||||
|
getToken,
|
||||||
|
});
|
||||||
|
|
||||||
|
const getTokenFromReq = (req) => {
|
||||||
|
if (
|
||||||
|
req.headers.authorization &&
|
||||||
|
req.headers.authorization.split(' ')[0] === 'Bearer'
|
||||||
|
) {
|
||||||
|
return req.headers.authorization.split(' ')[1];
|
||||||
|
} else if (req.query && req.query.token) {
|
||||||
|
return req.query.token;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
export default ({ app }: { app: Application }) => {
|
export default ({ app }: { app: Application }) => {
|
||||||
|
const server = http.createServer(app);
|
||||||
|
|
||||||
|
app.listen = function serverListen(...args) {
|
||||||
|
return server.listen(...args);
|
||||||
|
};
|
||||||
|
|
||||||
|
const wsInstance = expressWs(app, server);
|
||||||
|
|
||||||
|
// server.on('upgrade', function upgrade(request, socket, head) {
|
||||||
|
// const wss = wsInstance.getWss();
|
||||||
|
// auth((req) => {
|
||||||
|
// const searchParams = new URLSearchParams(
|
||||||
|
// req.url.slice(req.url.indexOf('?')),
|
||||||
|
// );
|
||||||
|
// return searchParams.get('token');
|
||||||
|
// })(request, {} as any, (err) => {
|
||||||
|
// Logger.error(err);
|
||||||
|
// if (err) {
|
||||||
|
// socket.write('HTTP/1.1 401 Unauthorized\r\n\r\n');
|
||||||
|
// socket.destroy();
|
||||||
|
// return;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// wss.handleUpgrade(request, socket, head, function done(ws) {
|
||||||
|
// wss.emit('connection', ws, request);
|
||||||
|
// });
|
||||||
|
// });
|
||||||
|
// });
|
||||||
|
|
||||||
app.enable('trust proxy');
|
app.enable('trust proxy');
|
||||||
app.use(cors());
|
app.use(cors());
|
||||||
|
|
||||||
app.use(bodyParser.json({ limit: '50mb' }));
|
app.use(bodyParser.json({ limit: '50mb' }));
|
||||||
app.use(bodyParser.urlencoded({ limit: '50mb', extended: true }));
|
app.use(bodyParser.urlencoded({ limit: '50mb', extended: true }));
|
||||||
app.use(
|
app.use(
|
||||||
jwt({ secret: config.secret as string, algorithms: ['HS384'] }).unless({
|
auth(getTokenFromReq).unless({
|
||||||
path: ['/api/login'],
|
path: excludePath,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
app.use((req, res, next) => {
|
app.use((req, res, next) => {
|
||||||
if (req.url && req.url.includes('/api/login')) {
|
if (req.url && excludePath.includes(req.path)) {
|
||||||
return next();
|
return next();
|
||||||
}
|
}
|
||||||
const data = fs.readFileSync(config.authConfigFile, 'utf8');
|
const data = fs.readFileSync(config.authConfigFile, 'utf8');
|
||||||
const authHeader = req.headers.authorization;
|
const authHeader = getTokenFromReq(req);
|
||||||
if (data) {
|
if (data) {
|
||||||
const { token } = JSON.parse(data);
|
const { token } = JSON.parse(data);
|
||||||
if (token && authHeader.includes(token)) {
|
if (token && authHeader.includes(token)) {
|
||||||
|
|
13
back/test/websocket.js
Normal file
13
back/test/websocket.js
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
const WebSocket = require('ws');
|
||||||
|
|
||||||
|
const ws = new WebSocket(
|
||||||
|
'ws://localhost:5600/api/terminal/123?token=eyJhbGciOiJIUzM4NCIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImFkbWluIiwicGFzc3dvcmQiOiJMV0QtRkR5OGVZS19pMG55cGVSYjYiLCJpYXQiOjE2MjI3MjYwMDksImV4cCI6MTYyMzMzMDgwOX0.skpXpLQ9Rzbwsj17NFSC3BVoLEqf9ttvLh3JR6irKcY40mLbw--pCDL5QlmEjOem',
|
||||||
|
);
|
||||||
|
|
||||||
|
ws.on('open', function open() {
|
||||||
|
ws.send('something');
|
||||||
|
});
|
||||||
|
|
||||||
|
ws.on('message', function incoming(data) {
|
||||||
|
console.log(data);
|
||||||
|
});
|
11
package.json
11
package.json
|
@ -31,19 +31,25 @@
|
||||||
"dotenv": "^8.2.0",
|
"dotenv": "^8.2.0",
|
||||||
"express": "^4.17.1",
|
"express": "^4.17.1",
|
||||||
"express-jwt": "^6.0.0",
|
"express-jwt": "^6.0.0",
|
||||||
|
"express-ws": "^4.0.0",
|
||||||
"got": "^11.8.2",
|
"got": "^11.8.2",
|
||||||
"jsonwebtoken": "^8.5.1",
|
"jsonwebtoken": "^8.5.1",
|
||||||
"nedb": "^1.8.0",
|
"nedb": "^1.8.0",
|
||||||
"node-fetch": "^2.6.1",
|
"node-fetch": "^2.6.1",
|
||||||
|
"node-pty": "^0.10.1",
|
||||||
"node-schedule": "^2.0.0",
|
"node-schedule": "^2.0.0",
|
||||||
"p-queue": "6.6.2",
|
"p-queue": "6.6.2",
|
||||||
"reflect-metadata": "^0.1.13",
|
"reflect-metadata": "^0.1.13",
|
||||||
"typedi": "^0.8.0",
|
"typedi": "^0.8.0",
|
||||||
"winston": "^3.3.3"
|
"winston": "^3.3.3",
|
||||||
|
"ws": "^7.4.6",
|
||||||
|
"xterm": "^4.12.0",
|
||||||
|
"xterm-addon-attach": "^0.6.0",
|
||||||
|
"xterm-addon-fit": "^0.5.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@ant-design/pro-layout": "^6.5.0",
|
|
||||||
"@ant-design/icons": "^4.6.2",
|
"@ant-design/icons": "^4.6.2",
|
||||||
|
"@ant-design/pro-layout": "^6.5.0",
|
||||||
"@types/cors": "^2.8.10",
|
"@types/cors": "^2.8.10",
|
||||||
"@types/express": "^4.17.8",
|
"@types/express": "^4.17.8",
|
||||||
"@types/express-jwt": "^6.0.1",
|
"@types/express-jwt": "^6.0.1",
|
||||||
|
@ -54,6 +60,7 @@
|
||||||
"@types/qrcode.react": "^1.0.1",
|
"@types/qrcode.react": "^1.0.1",
|
||||||
"@types/react": "^17.0.0",
|
"@types/react": "^17.0.0",
|
||||||
"@types/react-dom": "^17.0.0",
|
"@types/react-dom": "^17.0.0",
|
||||||
|
"@types/ws": "^7.4.4",
|
||||||
"@umijs/plugin-antd": "^0.9.1",
|
"@umijs/plugin-antd": "^0.9.1",
|
||||||
"@umijs/test": "^3.3.9",
|
"@umijs/test": "^3.3.9",
|
||||||
"codemirror": "^5.59.4",
|
"codemirror": "^5.59.4",
|
||||||
|
|
|
@ -7,7 +7,9 @@ import {
|
||||||
FolderOutlined,
|
FolderOutlined,
|
||||||
RadiusSettingOutlined,
|
RadiusSettingOutlined,
|
||||||
ControlOutlined,
|
ControlOutlined,
|
||||||
|
DesktopOutlined,
|
||||||
} from '@ant-design/icons';
|
} from '@ant-design/icons';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
route: {
|
route: {
|
||||||
|
@ -30,6 +32,12 @@ export default {
|
||||||
icon: <RadiusSettingOutlined />,
|
icon: <RadiusSettingOutlined />,
|
||||||
component: '@/pages/cookie/index',
|
component: '@/pages/cookie/index',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: '/terminal',
|
||||||
|
name: '终端管理',
|
||||||
|
icon: <DesktopOutlined />,
|
||||||
|
component: '@/pages/terminal/index',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: '/config',
|
path: '/config',
|
||||||
name: '配置文件',
|
name: '配置文件',
|
||||||
|
|
43
src/pages/terminal/index.module.less
Normal file
43
src/pages/terminal/index.module.less
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
@import '~@/styles/variable.less';
|
||||||
|
|
||||||
|
.left-tree {
|
||||||
|
&-container {
|
||||||
|
overflow: hidden;
|
||||||
|
position: relative;
|
||||||
|
// padding: 16px 0;
|
||||||
|
background-color: #fff;
|
||||||
|
height: calc(100vh - 128px);
|
||||||
|
height: calc(100vh - var(--vh-offset, 0px) - 128px);
|
||||||
|
width: @tree-width;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
&-scroller {
|
||||||
|
flex: 1;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
&-search {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
// position: absolute;
|
||||||
|
// top: 0;
|
||||||
|
// left: 0;
|
||||||
|
// padding-bottom: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-container {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global {
|
||||||
|
.log-wrapper {
|
||||||
|
.ant-pro-grid-content.wide .ant-pro-page-container-children-content {
|
||||||
|
padding: 0;
|
||||||
|
background-color: #f8f8f8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.CodeMirror {
|
||||||
|
width: calc(100% - 32px - @tree-width);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
84
src/pages/terminal/index.tsx
Normal file
84
src/pages/terminal/index.tsx
Normal file
|
@ -0,0 +1,84 @@
|
||||||
|
import {
|
||||||
|
useState,
|
||||||
|
useEffect,
|
||||||
|
useCallback,
|
||||||
|
Key,
|
||||||
|
useRef,
|
||||||
|
CSSProperties,
|
||||||
|
} from 'react';
|
||||||
|
import config from '@/utils/config';
|
||||||
|
import { PageContainer } from '@ant-design/pro-layout';
|
||||||
|
import { request } from '@/utils/http';
|
||||||
|
import { Terminal } from 'xterm';
|
||||||
|
import 'xterm/css/xterm.css';
|
||||||
|
import { AttachAddon } from 'xterm-addon-attach';
|
||||||
|
import { FitAddon } from 'xterm-addon-fit';
|
||||||
|
import { getToken } from '@/utils/auth';
|
||||||
|
|
||||||
|
const TerminalApp = () => {
|
||||||
|
const [width, setWdith] = useState('100%');
|
||||||
|
const [marginLeft, setMarginLeft] = useState(0);
|
||||||
|
const [marginTop, setMarginTop] = useState(-72);
|
||||||
|
const [style, setStyle] = useState<CSSProperties | undefined>(undefined);
|
||||||
|
|
||||||
|
const ref = useRef<HTMLElement>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const term = new Terminal();
|
||||||
|
const fitAddon = new FitAddon();
|
||||||
|
term.loadAddon(fitAddon);
|
||||||
|
const cols = 80;
|
||||||
|
const rows = 24;
|
||||||
|
fitAddon.fit();
|
||||||
|
term.open(ref.current);
|
||||||
|
term.focus();
|
||||||
|
request
|
||||||
|
.post(`${config.apiPrefix}terminals?cols=${cols}&rows=${rows}`)
|
||||||
|
.then((pid) => {
|
||||||
|
const ws = new WebSocket(
|
||||||
|
`ws://${location.host}${
|
||||||
|
config.apiPrefix
|
||||||
|
}terminals/${pid}?token=${getToken()}`,
|
||||||
|
);
|
||||||
|
ws.onopen = () => {
|
||||||
|
const attachAddon = new AttachAddon(ws);
|
||||||
|
term.loadAddon(attachAddon);
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (document.body.clientWidth < 768) {
|
||||||
|
setWdith('auto');
|
||||||
|
setMarginLeft(0);
|
||||||
|
setMarginTop(0);
|
||||||
|
} else {
|
||||||
|
setWdith('100%');
|
||||||
|
setMarginLeft(0);
|
||||||
|
setMarginTop(-72);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PageContainer
|
||||||
|
className="ql-container-wrapper terminal-wrapper"
|
||||||
|
title={'终端'}
|
||||||
|
header={{
|
||||||
|
style: {
|
||||||
|
padding: '4px 16px 4px 15px',
|
||||||
|
position: 'sticky',
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
zIndex: 20,
|
||||||
|
marginTop,
|
||||||
|
width,
|
||||||
|
marginLeft,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div ref={ref} style={style}></div>
|
||||||
|
</PageContainer>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default TerminalApp;
|
6
src/utils/auth.ts
Normal file
6
src/utils/auth.ts
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
import config from './config';
|
||||||
|
|
||||||
|
export function getToken() {
|
||||||
|
const token = localStorage.getItem(config.authKey);
|
||||||
|
return token;
|
||||||
|
}
|
|
@ -9,11 +9,11 @@ message.config({
|
||||||
const time = Date.now();
|
const time = Date.now();
|
||||||
const errorHandler = function (error: any) {
|
const errorHandler = function (error: any) {
|
||||||
if (error.response) {
|
if (error.response) {
|
||||||
const message = error.data
|
const msg = error.data
|
||||||
? error.data.message || error.data
|
? error.data.message || error.data
|
||||||
: error.response.statusText;
|
: error.response.statusText;
|
||||||
if (error.response.status !== 401 && error.response.status !== 502) {
|
if (error.response.status !== 401 && error.response.status !== 502) {
|
||||||
message.error(message);
|
message.error(msg);
|
||||||
} else {
|
} else {
|
||||||
console.log(error.response);
|
console.log(error.response);
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue
Block a user