feat: add webshell
This commit is contained in:
61
packages/webshell/webshell-node/src/app.ts
Normal file
61
packages/webshell/webshell-node/src/app.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
import express, { Request, Response, NextFunction } from 'express';
|
||||
import path from 'path';
|
||||
import logger from 'morgan';
|
||||
import bodyParser from 'body-parser';
|
||||
import { renderFile } from 'ejs';
|
||||
|
||||
import indexRouter from './routes/index';
|
||||
|
||||
const app = express();
|
||||
|
||||
// view engine setup
|
||||
app.set('views', path.join(process.cwd(), 'public'));
|
||||
app.set('view engine', 'html');
|
||||
app.engine('html', renderFile);
|
||||
|
||||
app.use(logger('dev'));
|
||||
app.use(bodyParser.json()); // for parsing application/json
|
||||
app.use(bodyParser.urlencoded({ extended: true })); // for parsing application/x-www-form-urlencoded
|
||||
app.use(express.json());
|
||||
app.use(express.urlencoded({ extended: false }));
|
||||
app.use(express.static(path.join(process.cwd(), 'public')));
|
||||
|
||||
app.use((req: Request, res: Response, next: NextFunction) => {
|
||||
// @ts-ignore
|
||||
res.success = function (data: any) {
|
||||
res.json({
|
||||
code: 0,
|
||||
msg: '操作成功',
|
||||
data: data,
|
||||
});
|
||||
};
|
||||
|
||||
// @ts-ignore
|
||||
res.fail = function (message: string) {
|
||||
res.json({
|
||||
code: 1,
|
||||
msg: message,
|
||||
});
|
||||
};
|
||||
next();
|
||||
});
|
||||
|
||||
app.use('/api', indexRouter);
|
||||
|
||||
app.use('*', (req: Request, res: Response) => {
|
||||
res.render('index', { title: 'Express' });
|
||||
});
|
||||
|
||||
// error handler
|
||||
app.use((err: any, req: Request, res: Response, next: NextFunction) => {
|
||||
// set locals, only providing error in development
|
||||
res.locals.message = err.message;
|
||||
res.locals.error = req.app.get('env') === 'development' ? err : {};
|
||||
|
||||
// render the error page
|
||||
res.status(err.status || 500);
|
||||
// res.render('error');
|
||||
res.send(err);
|
||||
});
|
||||
|
||||
export default app;
|
||||
@@ -1,20 +0,0 @@
|
||||
import os from 'node:os';
|
||||
import pty, { IPty } from 'node-pty';
|
||||
|
||||
const shell: string = os.platform() === 'win32' ? 'powershell.exe' : 'bash';
|
||||
console.log(shell);
|
||||
const ptyProcess: IPty = pty.spawn(shell, [], {
|
||||
name: 'xterm-color',
|
||||
cols: 80,
|
||||
rows: 30,
|
||||
cwd: process.env.HOME || '',
|
||||
env: process.env as NodeJS.ProcessEnv,
|
||||
});
|
||||
|
||||
ptyProcess.onData((data: string) => {
|
||||
process.stdout.write(data);
|
||||
});
|
||||
|
||||
ptyProcess.write('ls\r');
|
||||
ptyProcess.resize(100, 40);
|
||||
ptyProcess.write('ls\r');
|
||||
21
packages/webshell/webshell-node/src/routes/index.ts
Normal file
21
packages/webshell/webshell-node/src/routes/index.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import express, { Request, Response } from 'express';
|
||||
import { exec } from 'child_process';
|
||||
import { promisify } from 'util';
|
||||
|
||||
const router = express.Router();
|
||||
const promiseExec = promisify(exec);
|
||||
|
||||
router.get('/cwd', async (req: Request, res: Response) => {
|
||||
const { pid } = req.query;
|
||||
try {
|
||||
const { stdout } = await promiseExec(`lsof -a -p ${pid} -d cwd -Fn | tail -1 | sed 's/.//'`);
|
||||
const cwd = stdout.trim();
|
||||
// @ts-ignore
|
||||
res.success(cwd);
|
||||
} catch (error) {
|
||||
// @ts-ignore
|
||||
res.fail('Failed to retrieve current working directory');
|
||||
}
|
||||
});
|
||||
|
||||
export default router;
|
||||
12
packages/webshell/webshell-node/src/socket/index.ts
Normal file
12
packages/webshell/webshell-node/src/socket/index.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { Server as HttpServer } from 'http';
|
||||
import { SocketServer } from './socket';
|
||||
import { terminal } from './terminal';
|
||||
|
||||
export const createSocketServer = (server: HttpServer) => {
|
||||
const socketServer = new SocketServer(server, {
|
||||
path: '/terminal',
|
||||
pingTimeout: 1000 * 60 * 60 * 24,
|
||||
});
|
||||
|
||||
socketServer.use('terminal', terminal);
|
||||
};
|
||||
23
packages/webshell/webshell-node/src/socket/socket.ts
Normal file
23
packages/webshell/webshell-node/src/socket/socket.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { Server as HttpServer } from 'http';
|
||||
import { ServerOptions, Server as SocketIoServer, Namespace, Socket } from 'socket.io';
|
||||
|
||||
export class SocketServer {
|
||||
private io: SocketIoServer;
|
||||
|
||||
constructor(server: HttpServer, options?: Partial<ServerOptions>) {
|
||||
this.io = new SocketIoServer(server, options);
|
||||
}
|
||||
|
||||
use(name: string | ((socket: Socket) => void), fn?: (socket: Socket) => void): boolean {
|
||||
if (!name) return false;
|
||||
if (typeof name === 'string') {
|
||||
if (!fn) return false;
|
||||
if (typeof fn !== 'function') throw new TypeError('middleware must be a function!');
|
||||
|
||||
this.io.of(name).on('connection', fn);
|
||||
} else if (typeof name === 'function') {
|
||||
this.io.on('connection', name);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
49
packages/webshell/webshell-node/src/socket/terminal.ts
Normal file
49
packages/webshell/webshell-node/src/socket/terminal.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import * as pty from 'node-pty';
|
||||
import * as os from 'os';
|
||||
import { homedir } from 'os';
|
||||
import { Socket } from 'socket.io';
|
||||
|
||||
const shell: string = os.platform() === 'win32' ? 'powershell.exe' : 'zsh';
|
||||
|
||||
interface Option {
|
||||
name: string;
|
||||
cols?: number;
|
||||
rows?: number;
|
||||
cwd?: string;
|
||||
}
|
||||
|
||||
let ptyContainers: { [key: string]: pty.IPty } = {};
|
||||
|
||||
export const terminal = (socket: Socket) => {
|
||||
socket.on('create', (option: Option) => {
|
||||
let ptyProcess = pty.spawn(shell, ['--login'], {
|
||||
name: 'xterm-color',
|
||||
cols: option.cols || 80,
|
||||
rows: option.rows || 24,
|
||||
cwd: option.cwd || homedir(),
|
||||
env: process.env,
|
||||
}) as any;
|
||||
console.log('create', option.name);
|
||||
ptyProcess.on('data', (data: string) => socket.emit(option.name + '-output', data));
|
||||
socket.on(option.name + '-input', (data: string) => ptyProcess.write(data));
|
||||
socket.on(option.name + '-resize', (size: [number, number]) => {
|
||||
ptyProcess.resize(size[0], size[1]);
|
||||
});
|
||||
socket.on(option.name + '-exit', () => {
|
||||
console.log('exit', option.name)
|
||||
ptyProcess.destroy();
|
||||
});
|
||||
socket.emit(option.name + '-pid', ptyProcess.pid);
|
||||
ptyContainers[option.name] = ptyProcess;
|
||||
});
|
||||
socket.on('remove', (name: string) => {
|
||||
socket.removeAllListeners(name + '-input');
|
||||
socket.removeAllListeners(name + '-resize');
|
||||
socket.removeAllListeners(name + '-exit');
|
||||
if (name && ptyContainers[name] && ptyContainers[name].pid) {
|
||||
const curentContainer = ptyContainers[name] as any;
|
||||
curentContainer.destroy();
|
||||
delete ptyContainers[name];
|
||||
}
|
||||
});
|
||||
};
|
||||
47
packages/webshell/webshell-node/src/www.ts
Normal file
47
packages/webshell/webshell-node/src/www.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
#!/usr/bin/env node
|
||||
process.env.NODE_ENV = 'production';
|
||||
import http from 'http';
|
||||
import args from 'args';
|
||||
import app from './app';
|
||||
import { createSocketServer } from './socket/index';
|
||||
|
||||
args.option('port', 'The port on which the app will be running', 3000);
|
||||
const flags = args.parse(process.argv);
|
||||
let port: number = flags.port;
|
||||
|
||||
app.set('port', port);
|
||||
|
||||
let server = http.createServer(app);
|
||||
|
||||
createSocketServer(server);
|
||||
|
||||
server.listen(port, () => {
|
||||
console.log(`Server is running on port ${port}`);
|
||||
});
|
||||
server.on('error', onError);
|
||||
|
||||
/**
|
||||
* Event listener for HTTP server "error" event.
|
||||
*/
|
||||
|
||||
function onError(error: NodeJS.ErrnoException): void {
|
||||
if (error.syscall !== 'listen') {
|
||||
throw error;
|
||||
}
|
||||
|
||||
const bind = typeof port === 'string' ? 'Pipe ' + port : 'Port ' + port;
|
||||
|
||||
// handle specific listen errors with friendly messages
|
||||
switch (error.code) {
|
||||
case 'EACCES':
|
||||
console.error(bind + ' requires elevated privileges');
|
||||
process.exit(1);
|
||||
break;
|
||||
case 'EADDRINUSE':
|
||||
console.error(bind + ' is already in use');
|
||||
process.exit(1);
|
||||
break;
|
||||
default:
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user