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',
|
||||
proxy: {
|
||||
'/api': {
|
||||
target: 'http://127.0.0.1:5678/',
|
||||
target: 'http://127.0.0.1:5600',
|
||||
ws: true,
|
||||
changeOrigin: true,
|
||||
},
|
||||
},
|
||||
|
|
|
@ -4,6 +4,7 @@ import cookie from './cookie';
|
|||
import config from './config';
|
||||
import log from './log';
|
||||
import cron from './cron';
|
||||
import terminal from './terminal';
|
||||
|
||||
export default () => {
|
||||
const app = Router();
|
||||
|
@ -12,6 +13,7 @@ export default () => {
|
|||
config(app);
|
||||
log(app);
|
||||
cron(app);
|
||||
terminal(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 jwt from 'express-jwt';
|
||||
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 }) => {
|
||||
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.use(cors());
|
||||
|
||||
app.use(bodyParser.json({ limit: '50mb' }));
|
||||
app.use(bodyParser.urlencoded({ limit: '50mb', extended: true }));
|
||||
app.use(
|
||||
jwt({ secret: config.secret as string, algorithms: ['HS384'] }).unless({
|
||||
path: ['/api/login'],
|
||||
auth(getTokenFromReq).unless({
|
||||
path: excludePath,
|
||||
}),
|
||||
);
|
||||
app.use((req, res, next) => {
|
||||
if (req.url && req.url.includes('/api/login')) {
|
||||
if (req.url && excludePath.includes(req.path)) {
|
||||
return next();
|
||||
}
|
||||
const data = fs.readFileSync(config.authConfigFile, 'utf8');
|
||||
const authHeader = req.headers.authorization;
|
||||
const authHeader = getTokenFromReq(req);
|
||||
if (data) {
|
||||
const { token } = JSON.parse(data);
|
||||
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",
|
||||
"express": "^4.17.1",
|
||||
"express-jwt": "^6.0.0",
|
||||
"express-ws": "^4.0.0",
|
||||
"got": "^11.8.2",
|
||||
"jsonwebtoken": "^8.5.1",
|
||||
"nedb": "^1.8.0",
|
||||
"node-fetch": "^2.6.1",
|
||||
"node-pty": "^0.10.1",
|
||||
"node-schedule": "^2.0.0",
|
||||
"p-queue": "6.6.2",
|
||||
"reflect-metadata": "^0.1.13",
|
||||
"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": {
|
||||
"@ant-design/pro-layout": "^6.5.0",
|
||||
"@ant-design/icons": "^4.6.2",
|
||||
"@ant-design/pro-layout": "^6.5.0",
|
||||
"@types/cors": "^2.8.10",
|
||||
"@types/express": "^4.17.8",
|
||||
"@types/express-jwt": "^6.0.1",
|
||||
|
@ -54,6 +60,7 @@
|
|||
"@types/qrcode.react": "^1.0.1",
|
||||
"@types/react": "^17.0.0",
|
||||
"@types/react-dom": "^17.0.0",
|
||||
"@types/ws": "^7.4.4",
|
||||
"@umijs/plugin-antd": "^0.9.1",
|
||||
"@umijs/test": "^3.3.9",
|
||||
"codemirror": "^5.59.4",
|
||||
|
|
|
@ -7,7 +7,9 @@ import {
|
|||
FolderOutlined,
|
||||
RadiusSettingOutlined,
|
||||
ControlOutlined,
|
||||
DesktopOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import React from 'react';
|
||||
|
||||
export default {
|
||||
route: {
|
||||
|
@ -30,6 +32,12 @@ export default {
|
|||
icon: <RadiusSettingOutlined />,
|
||||
component: '@/pages/cookie/index',
|
||||
},
|
||||
{
|
||||
path: '/terminal',
|
||||
name: '终端管理',
|
||||
icon: <DesktopOutlined />,
|
||||
component: '@/pages/terminal/index',
|
||||
},
|
||||
{
|
||||
path: '/config',
|
||||
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 errorHandler = function (error: any) {
|
||||
if (error.response) {
|
||||
const message = error.data
|
||||
const msg = error.data
|
||||
? error.data.message || error.data
|
||||
: error.response.statusText;
|
||||
if (error.response.status !== 401 && error.response.status !== 502) {
|
||||
message.error(message);
|
||||
message.error(msg);
|
||||
} else {
|
||||
console.log(error.response);
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue
Block a user