xshell 1.1.1 → 1.1.2
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/package.json +5 -5
- package/process.d.ts +1 -0
- package/process.js +2 -0
- package/server.d.ts +14 -7
- package/server.js +43 -28
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "xshell",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.2",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./index.js",
|
|
6
6
|
"bin": {
|
|
@@ -57,9 +57,9 @@
|
|
|
57
57
|
"@svgr/webpack": "^8.1.0",
|
|
58
58
|
"@types/sass-loader": "^8.0.9",
|
|
59
59
|
"@types/ws": "^8.5.13",
|
|
60
|
-
"@typescript-eslint/eslint-plugin": "^8.
|
|
61
|
-
"@typescript-eslint/parser": "^8.
|
|
62
|
-
"@typescript-eslint/utils": "^8.
|
|
60
|
+
"@typescript-eslint/eslint-plugin": "^8.18.0",
|
|
61
|
+
"@typescript-eslint/parser": "^8.18.0",
|
|
62
|
+
"@typescript-eslint/utils": "^8.18.0",
|
|
63
63
|
"@xterm/addon-fit": "^0.10.0",
|
|
64
64
|
"@xterm/addon-web-links": "^0.11.0",
|
|
65
65
|
"@xterm/addon-webgl": "^0.18.0",
|
|
@@ -91,7 +91,7 @@
|
|
|
91
91
|
"mime-types": "^2.1.35",
|
|
92
92
|
"ora": "^8.1.1",
|
|
93
93
|
"react": "^19.0.0",
|
|
94
|
-
"react-i18next": "^15.1.
|
|
94
|
+
"react-i18next": "^15.1.4",
|
|
95
95
|
"react-object-model": "^1.2.20",
|
|
96
96
|
"resolve-path": "^1.4.0",
|
|
97
97
|
"sass": "^1.82.0",
|
package/process.d.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { type ChildProcess } from 'child_process';
|
|
|
2
2
|
import './prototype.ts';
|
|
3
3
|
import type { Encoding } from './file.ts';
|
|
4
4
|
import type { MyProxy } from './net.ts';
|
|
5
|
+
export declare const sea: boolean;
|
|
5
6
|
export declare const exe_nodejs: string;
|
|
6
7
|
export declare const platform: NodeJS.Platform;
|
|
7
8
|
export declare const username: string;
|
package/process.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { spawn } from 'child_process';
|
|
2
2
|
import os from 'os';
|
|
3
|
+
import node_sea from 'node:sea';
|
|
3
4
|
import "./prototype.js";
|
|
4
5
|
import { inspect, DecoderStream, filter_values, check } from "./utils.js";
|
|
6
|
+
export const sea = node_sea.isSea();
|
|
5
7
|
export const exe_nodejs = process.execPath.fp;
|
|
6
8
|
export const platform = os.platform();
|
|
7
9
|
export const username = os.userInfo().username;
|
package/server.d.ts
CHANGED
|
@@ -27,6 +27,8 @@ export declare class Server {
|
|
|
27
27
|
/** proxy 时需要丢弃的 resposne headers */
|
|
28
28
|
static drop_response_headers: Set<string>;
|
|
29
29
|
name: string;
|
|
30
|
+
/** sea 下最后修改时间,用于 http 资源缓存 */
|
|
31
|
+
last_modified_str?: string;
|
|
30
32
|
UAParser: typeof UAParser;
|
|
31
33
|
js_exts: Set<string>;
|
|
32
34
|
empty_body_statuses: Set<number>;
|
|
@@ -115,27 +117,32 @@ export declare class Server {
|
|
|
115
117
|
if (path.startsWith(prefix_docs)) {
|
|
116
118
|
await this.try_send(
|
|
117
119
|
ctx,
|
|
118
|
-
`T:/t/docs${prefix_docs}`,
|
|
119
120
|
(path.endsWith('/') ? `${path}index.html` : path).strip_start(prefix_docs),
|
|
120
|
-
|
|
121
|
+
{ fpd_root: `T:/t/docs${prefix_docs}` },
|
|
121
122
|
)
|
|
122
123
|
return
|
|
123
124
|
} */
|
|
124
|
-
try_send(ctx: Context,
|
|
125
|
+
try_send(ctx: Context, fp: string, { fpd_root, assets_root, sea: _sea, log_404, }?: {
|
|
126
|
+
fpd_root?: string;
|
|
127
|
+
assets_root?: string;
|
|
128
|
+
sea?: boolean;
|
|
129
|
+
log_404?: boolean;
|
|
130
|
+
}): Promise<boolean>;
|
|
125
131
|
/** 将 body 设置为 fp 所指文件的 ReadStream
|
|
126
132
|
检查 fp 是否合法,设置对应的 content-type,支持缓存,支持分块下载
|
|
127
133
|
- ctx: Koa.Context
|
|
128
|
-
- fp: 文件路径,有
|
|
134
|
+
- fp: 文件路径,有 fpd_root 时为相对路径或以 fpd_root 开头的绝对路径,无 fpd_root 时必须是绝对路径
|
|
129
135
|
- options?:
|
|
130
|
-
-
|
|
136
|
+
- fpd_root?: 只能访问 fpd_root 下面的文件
|
|
131
137
|
- absolute?: `false` 使用绝对路径指定发送的文件
|
|
132
138
|
- download?: `undefined` 在 response.headers 中加上 content-disposition: attachment 指示浏览器下载文件
|
|
133
139
|
- assets_root?: 替换模板 xxx.template.html 中 {root} 占位符 */
|
|
134
|
-
fsend(ctx: Context, fp: string, {
|
|
135
|
-
|
|
140
|
+
fsend(ctx: Context, fp: string, { fpd_root, absolute, download, assets_root, sea: _sea }?: {
|
|
141
|
+
fpd_root?: string;
|
|
136
142
|
absolute?: boolean;
|
|
137
143
|
download?: boolean;
|
|
138
144
|
assets_root?: string;
|
|
145
|
+
sea?: boolean;
|
|
139
146
|
}): Promise<string>;
|
|
140
147
|
/** - range: 取值为逗号分割的多个可用端口或端口区间 (不能含有空格,包含区间右值),比如:`8321,8322,8300-8310,11000-11999
|
|
141
148
|
- reverse?: `false` 在 range 内从后往前尝试 */
|
package/server.js
CHANGED
|
@@ -5,11 +5,13 @@ import zlib from 'zlib';
|
|
|
5
5
|
import fs from 'fs';
|
|
6
6
|
import { buffer as stream_to_buffer } from 'stream/consumers';
|
|
7
7
|
import util from 'util';
|
|
8
|
+
import node_sea from 'node:sea';
|
|
8
9
|
// --- my libs
|
|
9
10
|
import { t } from "./i18n/instance.js";
|
|
10
11
|
import { request as _request, Remote } from "./net.js";
|
|
11
|
-
import { inspect, output_width, check, range_to_numbers, encode, filter_keys, filter_values, consume_stream,
|
|
12
|
+
import { inspect, output_width, check, range_to_numbers, encode, filter_keys, filter_values, consume_stream, decode } from "./utils.js";
|
|
12
13
|
import { flist, fread, fstat } from "./file.js";
|
|
14
|
+
import { exe_nodejs, sea } from "./process.js";
|
|
13
15
|
// ------------ my server
|
|
14
16
|
export class Server {
|
|
15
17
|
/** proxy 时需要丢弃的 resposne headers */
|
|
@@ -22,6 +24,8 @@ export class Server {
|
|
|
22
24
|
'x-powered-by'
|
|
23
25
|
]);
|
|
24
26
|
name;
|
|
27
|
+
/** sea 下最后修改时间,用于 http 资源缓存 */
|
|
28
|
+
last_modified_str;
|
|
25
29
|
UAParser;
|
|
26
30
|
js_exts = new Set(['.js', '.mjs', '.cjs']);
|
|
27
31
|
empty_body_statuses = new Set([304, 204, 205]);
|
|
@@ -82,6 +86,9 @@ export class Server {
|
|
|
82
86
|
const { default: Koa } = await import('koa');
|
|
83
87
|
const { default: KoaCors } = await import('@koa/cors');
|
|
84
88
|
const { default: KoaCompress } = await import('koa-compress');
|
|
89
|
+
if (sea)
|
|
90
|
+
this.last_modified_str = (await fstat(exe_nodejs))
|
|
91
|
+
.mtime.toUTCString();
|
|
85
92
|
const { UAParser } = await import('ua-parser-js');
|
|
86
93
|
this.UAParser = UAParser;
|
|
87
94
|
// --- init koa app
|
|
@@ -534,20 +541,19 @@ export class Server {
|
|
|
534
541
|
if (path.startsWith(prefix_docs)) {
|
|
535
542
|
await this.try_send(
|
|
536
543
|
ctx,
|
|
537
|
-
`T:/t/docs${prefix_docs}`,
|
|
538
544
|
(path.endsWith('/') ? `${path}index.html` : path).strip_start(prefix_docs),
|
|
539
|
-
|
|
545
|
+
{ fpd_root: `T:/t/docs${prefix_docs}` },
|
|
540
546
|
)
|
|
541
547
|
return
|
|
542
548
|
} */
|
|
543
|
-
async try_send(ctx, fpd_root,
|
|
549
|
+
async try_send(ctx, fp, { fpd_root, assets_root, sea: _sea = sea, log_404 = true, } = {}) {
|
|
544
550
|
const { request: { _path, path, method }, response, } = ctx;
|
|
545
551
|
if (response.body !== undefined || response.status !== 404)
|
|
546
552
|
return true;
|
|
547
553
|
if (method !== 'HEAD' && method !== 'GET')
|
|
548
554
|
return false;
|
|
549
555
|
try {
|
|
550
|
-
await this.fsend(ctx, fp, {
|
|
556
|
+
await this.fsend(ctx, fp, { fpd_root, assets_root, sea: _sea });
|
|
551
557
|
return true;
|
|
552
558
|
}
|
|
553
559
|
catch (error) {
|
|
@@ -565,34 +571,38 @@ export class Server {
|
|
|
565
571
|
/** 将 body 设置为 fp 所指文件的 ReadStream
|
|
566
572
|
检查 fp 是否合法,设置对应的 content-type,支持缓存,支持分块下载
|
|
567
573
|
- ctx: Koa.Context
|
|
568
|
-
- fp: 文件路径,有
|
|
574
|
+
- fp: 文件路径,有 fpd_root 时为相对路径或以 fpd_root 开头的绝对路径,无 fpd_root 时必须是绝对路径
|
|
569
575
|
- options?:
|
|
570
|
-
-
|
|
576
|
+
- fpd_root?: 只能访问 fpd_root 下面的文件
|
|
571
577
|
- absolute?: `false` 使用绝对路径指定发送的文件
|
|
572
578
|
- download?: `undefined` 在 response.headers 中加上 content-disposition: attachment 指示浏览器下载文件
|
|
573
579
|
- assets_root?: 替换模板 xxx.template.html 中 {root} 占位符 */
|
|
574
|
-
async fsend(ctx, fp, {
|
|
575
|
-
check(absolute ||
|
|
580
|
+
async fsend(ctx, fp, { fpd_root, absolute, download, assets_root, sea: _sea = sea } = {}) {
|
|
581
|
+
check(absolute || _sea || fpd_root?.isdir, t('fsend 必须传 absolute 选项, sea 选项, 或 fpd_root 文件夹'));
|
|
576
582
|
const { request, request: { method } } = ctx;
|
|
577
583
|
let { response } = ctx;
|
|
578
|
-
if (!absolute) {
|
|
579
|
-
fp = fp.strip_if_start(
|
|
584
|
+
if (!absolute && !_sea) {
|
|
585
|
+
fp = fp.strip_if_start(fpd_root).strip_if_start('/');
|
|
580
586
|
const { default: resolve_safely } = await import('resolve-path');
|
|
581
587
|
try {
|
|
582
|
-
fp = resolve_safely(
|
|
588
|
+
fp = resolve_safely(fpd_root, fp).fp;
|
|
583
589
|
}
|
|
584
590
|
catch (error) {
|
|
585
591
|
error.message += `, fp: ${fp}`;
|
|
586
592
|
throw error;
|
|
587
593
|
}
|
|
588
594
|
}
|
|
589
|
-
//
|
|
595
|
+
// stats, asset
|
|
590
596
|
let stats;
|
|
597
|
+
let asset;
|
|
591
598
|
try {
|
|
592
|
-
|
|
599
|
+
if (_sea)
|
|
600
|
+
asset = new Uint8Array(node_sea.getRawAsset(fp));
|
|
601
|
+
else
|
|
602
|
+
stats = await fstat(fp);
|
|
593
603
|
}
|
|
594
604
|
catch (error) {
|
|
595
|
-
if (['ENOENT', 'ENAMETOOLONG', 'ENOTDIR'].includes(error.code)) {
|
|
605
|
+
if (_sea || ['ENOENT', 'ENAMETOOLONG', 'ENOTDIR'].includes(error.code)) {
|
|
596
606
|
error.status = 404;
|
|
597
607
|
throw error;
|
|
598
608
|
}
|
|
@@ -600,6 +610,7 @@ export class Server {
|
|
|
600
610
|
error.message = `fs.stat 出错: ${error.message}`;
|
|
601
611
|
throw error;
|
|
602
612
|
}
|
|
613
|
+
const size = _sea ? asset.length : Number(stats.size);
|
|
603
614
|
// 上面的 fstat 异步操作之后有可能 socket 已经关闭,那么这里什么都不做,直接结束吧
|
|
604
615
|
// 如果不这么做,后续 createReadStream 打开的流会赋给 response.body,但是 response.res 不再触发 finish, close 事件,
|
|
605
616
|
// 打开的流永远不会关闭,最终导致资源泄露,文件句柄一直打开
|
|
@@ -613,7 +624,9 @@ export class Server {
|
|
|
613
624
|
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control#up-to-date_contents_always
|
|
614
625
|
if (!response.get('cache-control'))
|
|
615
626
|
response.set('cache-control', 'no-cache');
|
|
616
|
-
const last_modified_str =
|
|
627
|
+
const last_modified_str = _sea
|
|
628
|
+
? this.last_modified_str
|
|
629
|
+
: stats.mtime.toUTCString();
|
|
617
630
|
if (!response.get('last-modified'))
|
|
618
631
|
response.set('last-modified', last_modified_str);
|
|
619
632
|
if (download)
|
|
@@ -632,8 +645,9 @@ export class Server {
|
|
|
632
645
|
}
|
|
633
646
|
if (assets_root) {
|
|
634
647
|
check(fp.endsWith('.html'), '传入 assets_root 参数时 fp 应该为 .html 文件');
|
|
635
|
-
response.body = (
|
|
636
|
-
|
|
648
|
+
response.body = (_sea
|
|
649
|
+
? decode(asset)
|
|
650
|
+
: (await fread(fp, { print: false }))).replaceAll('{root}', assets_root);
|
|
637
651
|
return fp;
|
|
638
652
|
}
|
|
639
653
|
const range_header = request.headers.range;
|
|
@@ -641,28 +655,29 @@ export class Server {
|
|
|
641
655
|
try {
|
|
642
656
|
const range_value = /=(.*)$/.exec(range_header)[1];
|
|
643
657
|
const range = /^[\w]*?(\d*)-(\d*)$/.exec(range_value);
|
|
644
|
-
assert(stats.size <= Number.MAX_SAFE_INTEGER);
|
|
645
658
|
let start = range[1] ? Number(range[1]) : undefined;
|
|
646
|
-
let end = range[2] ? Number(range[2]) :
|
|
659
|
+
let end = range[2] ? Number(range[2]) : size - 1;
|
|
647
660
|
if (start === undefined) {
|
|
648
|
-
start =
|
|
649
|
-
end =
|
|
661
|
+
start = size - end;
|
|
662
|
+
end = size - 1;
|
|
650
663
|
}
|
|
651
664
|
const chunksize = (end - start + 1);
|
|
652
665
|
response.status = 206;
|
|
653
666
|
response.set('content-length', String(chunksize));
|
|
654
|
-
response.set('content-range', `bytes ${start}-${end}/${
|
|
655
|
-
response.body =
|
|
667
|
+
response.set('content-range', `bytes ${start}-${end}/${size}`);
|
|
668
|
+
response.body = _sea
|
|
669
|
+
? asset.subarray(start, end)
|
|
670
|
+
: fs.createReadStream(fp, { start, end });
|
|
656
671
|
}
|
|
657
672
|
catch {
|
|
658
673
|
response.status = 416;
|
|
659
|
-
response.set('content-length', String(
|
|
660
|
-
response.set('content-range', `bytes */${
|
|
674
|
+
response.set('content-length', String(size));
|
|
675
|
+
response.set('content-range', `bytes */${size}`);
|
|
661
676
|
response.body = fs.createReadStream(fp);
|
|
662
677
|
}
|
|
663
678
|
else {
|
|
664
|
-
response.set('content-length', String(
|
|
665
|
-
response.body = fs.createReadStream(fp);
|
|
679
|
+
response.set('content-length', String(size));
|
|
680
|
+
response.body = _sea ? asset : fs.createReadStream(fp);
|
|
666
681
|
}
|
|
667
682
|
return fp;
|
|
668
683
|
}
|