xshell 1.0.61 → 1.0.63
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/i18n/dict.json +12 -0
- package/package.json +1 -1
- package/repl.js +8 -10
- package/server.d.ts +37 -21
- package/server.js +155 -23
package/i18n/dict.json
CHANGED
|
@@ -349,5 +349,17 @@
|
|
|
349
349
|
},
|
|
350
350
|
"filter 选项只适用于 fp_src 为文件夹": {
|
|
351
351
|
"en": "filter option only applies to fp_src for folders"
|
|
352
|
+
},
|
|
353
|
+
"已订阅 stdio": {
|
|
354
|
+
"en": "subscribed to stdio"
|
|
355
|
+
},
|
|
356
|
+
"由于 websocket 连接关闭,stdio 订阅被关闭": {
|
|
357
|
+
"en": "stdio subscription was closed due to websocket connection closed"
|
|
358
|
+
},
|
|
359
|
+
"已取消订阅 stdio": {
|
|
360
|
+
"en": "stdio unsubscribed"
|
|
361
|
+
},
|
|
362
|
+
"{{name}} 启动成功,正在监听 {{ports}} 端口": {
|
|
363
|
+
"en": "{{name}} started successfully and is listening on {{ports}} port"
|
|
352
364
|
}
|
|
353
365
|
}
|
package/package.json
CHANGED
package/repl.js
CHANGED
|
@@ -5,7 +5,6 @@ import { path } from './path.js';
|
|
|
5
5
|
import { t } from './i18n/instance.js';
|
|
6
6
|
import './prototype.js';
|
|
7
7
|
import { delay, set_inspect_options } from './utils.js';
|
|
8
|
-
import { Remote } from './net.js';
|
|
9
8
|
set_inspect_options();
|
|
10
9
|
let server;
|
|
11
10
|
/** 谨慎使用,webpack 打包后可能会变成 /d:/1/mod/node_modules/xshell/ 这样的编译期路径 */
|
|
@@ -31,15 +30,14 @@ export async function start_repl() {
|
|
|
31
30
|
// --- http server
|
|
32
31
|
let { Server } = await import('./server.js');
|
|
33
32
|
server = new Server({
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
})
|
|
33
|
+
name: 'repl',
|
|
34
|
+
http_port: 8421,
|
|
35
|
+
funcs: {
|
|
36
|
+
echo({ data: [data] }) {
|
|
37
|
+
console.log('echo:', data);
|
|
38
|
+
return [data];
|
|
39
|
+
},
|
|
40
|
+
}
|
|
43
41
|
});
|
|
44
42
|
await server.start();
|
|
45
43
|
})(),
|
package/server.d.ts
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
/// <reference types="node" resolution-mode="require"/>
|
|
2
|
-
/// <reference types="ua-parser-js" />
|
|
3
2
|
/// <reference types="node" resolution-mode="require"/>
|
|
4
3
|
/// <reference types="node" resolution-mode="require"/>
|
|
5
4
|
import { type Server as HttpServer, type IncomingHttpHeaders } from 'http';
|
|
6
|
-
import type
|
|
5
|
+
import { type Http2SecureServer, type IncomingHttpHeaders as IncomingHttp2Headers } from 'http2';
|
|
7
6
|
import type { WebSocketServer } from 'ws';
|
|
8
7
|
import type { default as Koa, Context, Next } from 'koa';
|
|
9
8
|
declare module 'koa' {
|
|
@@ -16,7 +15,8 @@ declare module 'koa' {
|
|
|
16
15
|
compress: boolean;
|
|
17
16
|
}
|
|
18
17
|
}
|
|
19
|
-
import
|
|
18
|
+
import type { UAParser } from 'ua-parser-js';
|
|
19
|
+
import { Remote, type Headers, type RequestOptions } from './net.js';
|
|
20
20
|
declare module 'http' {
|
|
21
21
|
interface IncomingMessage {
|
|
22
22
|
body?: Buffer;
|
|
@@ -28,34 +28,50 @@ declare module 'http' {
|
|
|
28
28
|
export declare class Server {
|
|
29
29
|
/** proxy 时需要丢弃的 resposne headers */
|
|
30
30
|
static drop_response_headers: Set<string>;
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
(extensions?: Record<string, unknown>): import("ua-parser-js").IResult;
|
|
34
|
-
new (uastring?: string, extensions?: Record<string, unknown>): import("ua-parser-js").UAParserInstance;
|
|
35
|
-
new (extensions?: Record<string, unknown>): import("ua-parser-js").UAParserInstance;
|
|
36
|
-
VERSION: string;
|
|
37
|
-
BROWSER: import("ua-parser-js").BROWSER;
|
|
38
|
-
CPU: import("ua-parser-js").CPU;
|
|
39
|
-
DEVICE: import("ua-parser-js").DEVICE;
|
|
40
|
-
ENGINE: import("ua-parser-js").ENGINE;
|
|
41
|
-
OS: import("ua-parser-js").OS;
|
|
42
|
-
UAParser: any;
|
|
43
|
-
};
|
|
31
|
+
name: string;
|
|
32
|
+
UAParser: typeof UAParser;
|
|
44
33
|
js_exts: Set<string>;
|
|
45
34
|
app: Koa;
|
|
46
35
|
handler: ReturnType<Koa['callback']>;
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
36
|
+
http_port: number;
|
|
37
|
+
http2_port: number;
|
|
38
|
+
/** http2 server 证书文件夹路径,设置后才会启用 http2 server */
|
|
39
|
+
fpd_certs?: string;
|
|
40
|
+
default_hostnames?: string[];
|
|
41
|
+
http_server: HttpServer;
|
|
42
|
+
http2_server?: Http2SecureServer;
|
|
43
|
+
websocket_server?: WebSocketServer;
|
|
44
|
+
/** 设置后会启用 websocket rpc */
|
|
50
45
|
remote?: Remote;
|
|
51
46
|
colors: boolean;
|
|
52
|
-
|
|
53
|
-
|
|
47
|
+
/** 启用后增加 stdio 订阅相关的 remote.funcs */
|
|
48
|
+
stdio_subscribable?: boolean;
|
|
49
|
+
stdio_subscribers: (((chunk: Uint8Array | string) => Promise<void>) & {
|
|
50
|
+
id: number;
|
|
51
|
+
})[];
|
|
52
|
+
/** 原始 process.stdout.write 函数 bind 后的备份 */
|
|
53
|
+
stdout_write: Function;
|
|
54
|
+
stderr_write: Function;
|
|
55
|
+
encoder: TextEncoder;
|
|
56
|
+
constructor({ name, http_port, http2_port, fpd_certs, default_hostnames, remote, funcs, stdio_subscribable, }: {
|
|
57
|
+
name: string;
|
|
58
|
+
http_port?: number;
|
|
59
|
+
http2_port?: number;
|
|
60
|
+
fpd_certs?: string;
|
|
61
|
+
default_hostnames?: string[];
|
|
54
62
|
remote?: Remote;
|
|
63
|
+
funcs?: Remote['funcs'];
|
|
64
|
+
stdio_subscribable?: boolean;
|
|
55
65
|
});
|
|
56
66
|
/** start http server and listen */
|
|
57
67
|
start(): Promise<void>;
|
|
58
68
|
stop(): void;
|
|
69
|
+
/** 可被子类重写定义错误处理逻辑 */
|
|
70
|
+
on_error(error: Error & {
|
|
71
|
+
code?: string;
|
|
72
|
+
}, ctx?: Context): void;
|
|
73
|
+
/** 可被子类重写添加额外的中间件,在 start 时被调用 */
|
|
74
|
+
init_app(app: Koa): Promise<void>;
|
|
59
75
|
entry(ctx: Context, next: Next): Promise<void>;
|
|
60
76
|
/** 解析 req.body to request.body
|
|
61
77
|
处理 request.ip */
|
package/server.js
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
import { createServer as http_create_server } from 'http';
|
|
2
|
+
import { createSecureServer as http2_create_server } from 'http2';
|
|
3
|
+
import { createSecureContext } from 'tls';
|
|
2
4
|
import zlib from 'zlib';
|
|
3
5
|
import { createReadStream } from 'fs';
|
|
4
6
|
import { Readable } from 'stream';
|
|
5
7
|
import util from 'util';
|
|
6
8
|
// --- my libs
|
|
7
9
|
import { t } from './i18n/instance.js';
|
|
8
|
-
import { request as _request } from './net.js';
|
|
10
|
+
import { request as _request, Remote } from './net.js';
|
|
9
11
|
import { stream_to_buffer, inspect, output_width, assert } from './utils.js';
|
|
10
|
-
import { fstat } from './file.js';
|
|
12
|
+
import { flist, fread, fstat } from './file.js';
|
|
11
13
|
// ------------ my server
|
|
12
14
|
export class Server {
|
|
13
15
|
/** proxy 时需要丢弃的 resposne headers */
|
|
@@ -19,20 +21,47 @@ export class Server {
|
|
|
19
21
|
'transfer-encoding',
|
|
20
22
|
'x-powered-by'
|
|
21
23
|
]);
|
|
22
|
-
|
|
24
|
+
name;
|
|
25
|
+
UAParser;
|
|
23
26
|
js_exts = new Set(['.js', '.mjs', '.cjs']);
|
|
24
27
|
app;
|
|
25
28
|
handler;
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
+
http_port = 80;
|
|
30
|
+
http2_port = 443;
|
|
31
|
+
/** http2 server 证书文件夹路径,设置后才会启用 http2 server */
|
|
32
|
+
fpd_certs;
|
|
33
|
+
default_hostnames;
|
|
34
|
+
http_server;
|
|
35
|
+
http2_server;
|
|
36
|
+
websocket_server;
|
|
37
|
+
/** 设置后会启用 websocket rpc */
|
|
29
38
|
remote;
|
|
30
39
|
colors = util.inspect.defaultOptions.colors;
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
40
|
+
/** 启用后增加 stdio 订阅相关的 remote.funcs */
|
|
41
|
+
stdio_subscribable;
|
|
42
|
+
stdio_subscribers;
|
|
43
|
+
/** 原始 process.stdout.write 函数 bind 后的备份 */
|
|
44
|
+
stdout_write;
|
|
45
|
+
stderr_write;
|
|
46
|
+
encoder = new TextEncoder();
|
|
47
|
+
constructor({ name, http_port, http2_port, fpd_certs, default_hostnames, remote, funcs, stdio_subscribable, }) {
|
|
48
|
+
this.name = name;
|
|
49
|
+
if (http_port !== undefined)
|
|
50
|
+
this.http_port = http_port;
|
|
51
|
+
if (http2_port !== undefined)
|
|
52
|
+
this.http2_port = http2_port;
|
|
53
|
+
if (fpd_certs)
|
|
54
|
+
this.fpd_certs = fpd_certs;
|
|
55
|
+
if (default_hostnames)
|
|
56
|
+
this.default_hostnames = default_hostnames;
|
|
34
57
|
if (remote)
|
|
35
58
|
this.remote = remote;
|
|
59
|
+
else if (funcs)
|
|
60
|
+
this.remote = new Remote({ funcs });
|
|
61
|
+
if (stdio_subscribable !== undefined) {
|
|
62
|
+
assert(remote || funcs);
|
|
63
|
+
this.stdio_subscribable = stdio_subscribable;
|
|
64
|
+
}
|
|
36
65
|
}
|
|
37
66
|
/** start http server and listen */
|
|
38
67
|
async start() {
|
|
@@ -44,10 +73,7 @@ export class Server {
|
|
|
44
73
|
const { WebSocketServer } = await import('ws');
|
|
45
74
|
// --- init koa app
|
|
46
75
|
let app = new Koa();
|
|
47
|
-
app.on('error', (
|
|
48
|
-
console.error(error);
|
|
49
|
-
console.log(ctx);
|
|
50
|
-
});
|
|
76
|
+
app.on('error', this.on_error.bind(this));
|
|
51
77
|
app.use(this.entry.bind(this));
|
|
52
78
|
app.use(KoaCompress({
|
|
53
79
|
br: {
|
|
@@ -61,24 +87,48 @@ export class Server {
|
|
|
61
87
|
threshold: 512
|
|
62
88
|
}));
|
|
63
89
|
app.use(KoaCors({ credentials: true }));
|
|
90
|
+
await this.init_app(app);
|
|
64
91
|
app.use(this._router.bind(this));
|
|
65
92
|
this.app = app;
|
|
66
93
|
this.handler = this.app.callback();
|
|
67
|
-
this.
|
|
94
|
+
this.http_server = http_create_server(this.handler);
|
|
95
|
+
const { fpd_certs } = this;
|
|
96
|
+
if (fpd_certs) {
|
|
97
|
+
let lazy_secure_ctxs = Object.fromEntries(await Promise.all(
|
|
98
|
+
// fpd_certs 文件夹下面的每个 .key 对应一个证书及域名
|
|
99
|
+
(await flist(fpd_certs, { print: false, filter: /\.key$/ }))
|
|
100
|
+
.map(async (fname) => {
|
|
101
|
+
const domain = fname.slice(0, -'.key'.length);
|
|
102
|
+
const [key, cert] = await Promise.all([
|
|
103
|
+
fname,
|
|
104
|
+
`${domain}.crt`,
|
|
105
|
+
].map(async (fname) => fread(`${fpd_certs}${fname}`, { print: false })));
|
|
106
|
+
return [domain, { key, cert }];
|
|
107
|
+
})));
|
|
108
|
+
const default_ctx = lazy_secure_ctxs[this.default_hostnames.find(hostname => hostname in lazy_secure_ctxs)];
|
|
109
|
+
assert(default_ctx);
|
|
110
|
+
this.http2_server = http2_create_server({
|
|
111
|
+
SNICallback(servername, callback) {
|
|
112
|
+
let lazy_ctx = lazy_secure_ctxs[servername] || default_ctx;
|
|
113
|
+
callback(null, lazy_ctx.ctx ??= createSecureContext(lazy_ctx));
|
|
114
|
+
},
|
|
115
|
+
allowHTTP1: true,
|
|
116
|
+
}, this.handler);
|
|
117
|
+
}
|
|
68
118
|
// websocket rpc
|
|
69
119
|
if (this.remote) {
|
|
70
|
-
this.
|
|
120
|
+
this.websocket_server = new WebSocketServer({
|
|
71
121
|
noServer: true,
|
|
72
122
|
skipUTF8Validation: true,
|
|
73
123
|
perMessageDeflate: true,
|
|
74
124
|
maxPayload: 2 ** 30 // 1 GB
|
|
75
125
|
});
|
|
76
|
-
this.
|
|
126
|
+
this.websocket_server.on('connection', (ws, request) => {
|
|
77
127
|
ws.addEventListener('message', ({ data }) => {
|
|
78
128
|
this.remote.handle(new Uint8Array(data), ws);
|
|
79
129
|
});
|
|
80
130
|
});
|
|
81
|
-
this.
|
|
131
|
+
this.http_server.on('upgrade', (request, socket, head) => {
|
|
82
132
|
// url 只有路径部分
|
|
83
133
|
const { url, headers, headers: { host = '' }, } = request;
|
|
84
134
|
const ip = request.socket.remoteAddress.replace(/^::ffff:/, '');
|
|
@@ -99,9 +149,9 @@ export class Server {
|
|
|
99
149
|
(this.colors ? url.yellow : url));
|
|
100
150
|
switch (url) {
|
|
101
151
|
case '/':
|
|
102
|
-
this.
|
|
152
|
+
this.websocket_server.handleUpgrade(request, socket, head, ws => {
|
|
103
153
|
ws.binaryType = 'arraybuffer';
|
|
104
|
-
this.
|
|
154
|
+
this.websocket_server.emit('connection', ws, request);
|
|
105
155
|
});
|
|
106
156
|
return;
|
|
107
157
|
default:
|
|
@@ -109,13 +159,95 @@ export class Server {
|
|
|
109
159
|
socket.destroy();
|
|
110
160
|
}
|
|
111
161
|
});
|
|
162
|
+
// 将输出到 stdout, stderr 的内容 copy 一份通过 websocket 发到 web shell
|
|
163
|
+
if (this.stdio_subscribable) {
|
|
164
|
+
this.remote.funcs = {
|
|
165
|
+
...this.remote.funcs,
|
|
166
|
+
subscribe_stdio: ({ id }, websocket) => {
|
|
167
|
+
console.log(t('已订阅 stdio'));
|
|
168
|
+
const subscriber = async (chunk) => {
|
|
169
|
+
// send 时有可能 websocket 连接断开,抛异常,为防止循环调用 (console.error -> stdio subscriber -> throw error -> console.error)
|
|
170
|
+
// 这里只能忽略错误
|
|
171
|
+
try {
|
|
172
|
+
await this.remote.send({ id, data: [typeof chunk === 'string' ? this.encoder.encode(chunk) : chunk] }, websocket);
|
|
173
|
+
}
|
|
174
|
+
catch { }
|
|
175
|
+
};
|
|
176
|
+
// 让后续可以通过 unsubscribe_stdio 取消订阅
|
|
177
|
+
subscriber.id = id;
|
|
178
|
+
this.stdio_subscribers.push(subscriber);
|
|
179
|
+
websocket.addEventListener('close', () => {
|
|
180
|
+
const length = this.stdio_subscribers.length;
|
|
181
|
+
const stdio_subscribers_ = this.stdio_subscribers.filter(s => s !== subscriber);
|
|
182
|
+
if (stdio_subscribers_.length !== length) {
|
|
183
|
+
console.log(t('由于 websocket 连接关闭,stdio 订阅被关闭'));
|
|
184
|
+
this.stdio_subscribers = stdio_subscribers_;
|
|
185
|
+
}
|
|
186
|
+
}, { once: true });
|
|
187
|
+
},
|
|
188
|
+
unsubscribe_stdio: ({ data: [id] }, websocket) => {
|
|
189
|
+
this.stdio_subscribers = this.stdio_subscribers.filter(s => s.id !== id);
|
|
190
|
+
console.log(t('已取消订阅 stdio'));
|
|
191
|
+
return [];
|
|
192
|
+
},
|
|
193
|
+
};
|
|
194
|
+
this.stdout_write = process.stdout.write.bind(process.stdout);
|
|
195
|
+
this.stderr_write = process.stderr.write.bind(process.stderr);
|
|
196
|
+
process.stdout.write = (...args) => {
|
|
197
|
+
if (this.stdio_subscribers.length)
|
|
198
|
+
(async () => {
|
|
199
|
+
try {
|
|
200
|
+
await Promise.all(this.stdio_subscribers.map(async (subscriber) => subscriber(args[0])));
|
|
201
|
+
}
|
|
202
|
+
catch {
|
|
203
|
+
this.stdout_write('stdio_subscriber error\n');
|
|
204
|
+
}
|
|
205
|
+
})();
|
|
206
|
+
return this.stdout_write(...args);
|
|
207
|
+
};
|
|
208
|
+
process.stderr.write = (...args) => {
|
|
209
|
+
if (this.stdio_subscribers.length)
|
|
210
|
+
(async () => {
|
|
211
|
+
try {
|
|
212
|
+
await Promise.all(this.stdio_subscribers.map(async (subscriber) => subscriber(args[0])));
|
|
213
|
+
}
|
|
214
|
+
catch {
|
|
215
|
+
this.stderr_write('stderr_subscriber error\n');
|
|
216
|
+
}
|
|
217
|
+
})();
|
|
218
|
+
return this.stderr_write(...args);
|
|
219
|
+
};
|
|
220
|
+
}
|
|
112
221
|
}
|
|
113
|
-
await
|
|
114
|
-
|
|
115
|
-
|
|
222
|
+
await Promise.all([
|
|
223
|
+
new Promise(resolve => {
|
|
224
|
+
this.http_server.listen(this.http_port, resolve);
|
|
225
|
+
}),
|
|
226
|
+
fpd_certs && new Promise(resolve => {
|
|
227
|
+
this.http2_server.listen(this.http2_port, resolve);
|
|
228
|
+
}),
|
|
229
|
+
]);
|
|
230
|
+
console.log(t('{{name}} 启动成功,正在监听 {{ports}} 端口', {
|
|
231
|
+
name: this.name,
|
|
232
|
+
ports: `${this.http_port}${fpd_certs ? `, ${this.http2_port}` : ''}`
|
|
233
|
+
}));
|
|
116
234
|
}
|
|
117
235
|
stop() {
|
|
118
|
-
this.
|
|
236
|
+
this.http_server.close();
|
|
237
|
+
if (this.http2_server)
|
|
238
|
+
this.http2_server.close();
|
|
239
|
+
}
|
|
240
|
+
/** 可被子类重写定义错误处理逻辑 */
|
|
241
|
+
on_error(error, ctx) {
|
|
242
|
+
if (error.code === 'EPIPE' || error.code === 'ECONNRESET')
|
|
243
|
+
console.log(`${error.code}:`, ctx?.request?.url);
|
|
244
|
+
else {
|
|
245
|
+
console.error(error);
|
|
246
|
+
console.log(ctx);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
/** 可被子类重写添加额外的中间件,在 start 时被调用 */
|
|
250
|
+
async init_app(app) {
|
|
119
251
|
}
|
|
120
252
|
async entry(ctx, next) {
|
|
121
253
|
let { response } = ctx;
|