xshell 1.0.60 → 1.0.62
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/Terminal.d.ts +15 -0
- package/Terminal.js +81 -0
- package/file.d.ts +4 -2
- package/file.js +18 -9
- package/i18n/dict.json +15 -0
- package/i18n/scanner/index.js +10 -10
- package/net.js +1 -1
- package/package.json +25 -20
- package/path.d.ts +2 -2
- package/process.d.ts +1 -0
- package/prototype.browser.js +1 -1
- package/repl.js +8 -10
- package/server.d.ts +39 -21
- package/server.js +196 -35
- package/utils.browser.d.ts +3 -0
- package/utils.browser.js +17 -0
- package/utils.d.ts +3 -0
- package/utils.js +17 -0
package/Terminal.d.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import 'xterm/css/xterm.css';
|
|
2
|
+
import { Terminal as XTermTerminal } from 'xterm';
|
|
3
|
+
import { Model } from 'react-object-model';
|
|
4
|
+
import type { Remote } from './net.browser.js';
|
|
5
|
+
export declare function Terminal({ font }: {
|
|
6
|
+
font: string;
|
|
7
|
+
}): import("react/jsx-runtime").JSX.Element;
|
|
8
|
+
declare class TerminalModel extends Model<TerminalModel> {
|
|
9
|
+
term: XTermTerminal;
|
|
10
|
+
stdio_id: number;
|
|
11
|
+
subscribe_stdio(remote: Remote): Promise<void>;
|
|
12
|
+
unsubscribe_stdio(remote: Remote): Promise<void>;
|
|
13
|
+
}
|
|
14
|
+
export declare let terminal: TerminalModel;
|
|
15
|
+
export {};
|
package/Terminal.js
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import 'xterm/css/xterm.css';
|
|
3
|
+
import { useEffect, useRef } from 'react';
|
|
4
|
+
import { Terminal as XTermTerminal } from 'xterm';
|
|
5
|
+
import { FitAddon } from 'xterm-addon-fit';
|
|
6
|
+
import { WebglAddon } from 'xterm-addon-webgl';
|
|
7
|
+
import { Model } from 'react-object-model';
|
|
8
|
+
import { assert, genid } from './utils.browser.js';
|
|
9
|
+
export function Terminal({ font }) {
|
|
10
|
+
let rterminal = useRef();
|
|
11
|
+
useEffect(() => {
|
|
12
|
+
(async () => {
|
|
13
|
+
await document.fonts.ready;
|
|
14
|
+
let term = new XTermTerminal({
|
|
15
|
+
fontFamily: font,
|
|
16
|
+
fontSize: 16,
|
|
17
|
+
cursorStyle: 'bar',
|
|
18
|
+
disableStdin: true,
|
|
19
|
+
convertEol: true,
|
|
20
|
+
allowProposedApi: true,
|
|
21
|
+
theme: {
|
|
22
|
+
...myscheme,
|
|
23
|
+
magenta: myscheme.purple,
|
|
24
|
+
brightMagenta: myscheme.brightPurple,
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
const fit_addon = new FitAddon();
|
|
28
|
+
term.loadAddon(fit_addon);
|
|
29
|
+
term.open(rterminal.current);
|
|
30
|
+
term.loadAddon(new WebglAddon());
|
|
31
|
+
fit_addon.fit();
|
|
32
|
+
terminal.set({ term });
|
|
33
|
+
})();
|
|
34
|
+
}, []);
|
|
35
|
+
return _jsx("div", { className: 'term', ref: rterminal });
|
|
36
|
+
}
|
|
37
|
+
class TerminalModel extends Model {
|
|
38
|
+
term;
|
|
39
|
+
stdio_id = 0;
|
|
40
|
+
async subscribe_stdio(remote) {
|
|
41
|
+
assert(!this.stdio_id);
|
|
42
|
+
const id = this.stdio_id = genid();
|
|
43
|
+
remote.handlers.set(id, ({ data: [chunk] }) => {
|
|
44
|
+
this.term.write(chunk);
|
|
45
|
+
});
|
|
46
|
+
await remote.send({ id, func: 'subscribe_stdio' });
|
|
47
|
+
}
|
|
48
|
+
async unsubscribe_stdio(remote) {
|
|
49
|
+
let { stdio_id } = this;
|
|
50
|
+
assert(stdio_id);
|
|
51
|
+
await remote.call('unsubscribe_stdio', [stdio_id]);
|
|
52
|
+
remote.handlers.delete(stdio_id);
|
|
53
|
+
this.stdio_id = 0;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
export let terminal = new TerminalModel();
|
|
57
|
+
/** winterm.json */
|
|
58
|
+
const myscheme = {
|
|
59
|
+
background: '#FFFFFF',
|
|
60
|
+
black: '#000000',
|
|
61
|
+
blue: '#0070C0',
|
|
62
|
+
brightBlack: '#444444',
|
|
63
|
+
brightBlue: '#0000FF',
|
|
64
|
+
brightCyan: '#008080',
|
|
65
|
+
brightGreen: '#718C00',
|
|
66
|
+
brightPurple: '#8959A8',
|
|
67
|
+
brightRed: '#DF0000',
|
|
68
|
+
brightWhite: '#AAAAAA',
|
|
69
|
+
brightYellow: '#FF81FF',
|
|
70
|
+
cursorColor: '#000000',
|
|
71
|
+
cyan: '#821717',
|
|
72
|
+
foreground: '#000000',
|
|
73
|
+
green: '#008000',
|
|
74
|
+
name: 'myscheme',
|
|
75
|
+
purple: '#800080',
|
|
76
|
+
red: '#C82829',
|
|
77
|
+
selectionBackground: '#ADD6FF',
|
|
78
|
+
white: '#888888',
|
|
79
|
+
yellow: '#EC7600'
|
|
80
|
+
};
|
|
81
|
+
//# sourceMappingURL=Terminal.js.map
|
package/file.d.ts
CHANGED
|
@@ -116,12 +116,14 @@ export declare function fdelete(fp: string, { print }?: {
|
|
|
116
116
|
- options?:
|
|
117
117
|
- print?: `true`
|
|
118
118
|
- overwrite?: `true`
|
|
119
|
-
|
|
119
|
+
- filter?: 当 fp_src 为文件夹时选择性复制里面的部分内容,和 flist 的 filter 选项一样,但只支持文件夹中第一层的文件
|
|
120
|
+
|
|
120
121
|
@example
|
|
121
122
|
fcopy('D:/temp/camera/', 'D:/camera/') */
|
|
122
|
-
export declare function fcopy(fp_src: string, fp_dst: string, { print, overwrite, }?: {
|
|
123
|
+
export declare function fcopy(fp_src: string, fp_dst: string, { print, overwrite, filter, }?: {
|
|
123
124
|
print?: boolean;
|
|
124
125
|
overwrite?: boolean;
|
|
126
|
+
filter?: FListOptions['filter'];
|
|
125
127
|
}): Promise<void>;
|
|
126
128
|
/** 移动文件或文件夹
|
|
127
129
|
相同分区 / 文件系统下使用 rename, 否则 fallback 到复制后删除源文件
|
package/file.js
CHANGED
|
@@ -226,22 +226,31 @@ export async function fdelete(fp, { print = true } = {}) {
|
|
|
226
226
|
- options?:
|
|
227
227
|
- print?: `true`
|
|
228
228
|
- overwrite?: `true`
|
|
229
|
-
|
|
229
|
+
- filter?: 当 fp_src 为文件夹时选择性复制里面的部分内容,和 flist 的 filter 选项一样,但只支持文件夹中第一层的文件
|
|
230
|
+
|
|
230
231
|
@example
|
|
231
232
|
fcopy('D:/temp/camera/', 'D:/camera/') */
|
|
232
|
-
export async function fcopy(fp_src, fp_dst, { print = true, overwrite = true, } = {}) {
|
|
233
|
+
export async function fcopy(fp_src, fp_dst, { print = true, overwrite = true, filter, } = {}) {
|
|
233
234
|
const { isdir } = fp_src;
|
|
234
235
|
assert(isdir === fp_dst.isdir, t('fp_src 和 fp_dst 必须同为文件路径或文件夹路径'));
|
|
235
236
|
assert(path.isAbsolute(fp_src) && path.isAbsolute(fp_dst), t('fp_src 和 fp_dst 必须为完整路径'));
|
|
237
|
+
if (!isdir && filter)
|
|
238
|
+
throw new Error(t('filter 选项只适用于 fp_src 为文件夹'));
|
|
236
239
|
if (print)
|
|
237
240
|
console.log(t('复制'), fp_src, '->', fp_dst);
|
|
238
241
|
if (isdir)
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
242
|
+
if (filter) {
|
|
243
|
+
await fmkdir(fp_dst, { print });
|
|
244
|
+
await Promise.all((await flist(fp_src, { filter, print: false }))
|
|
245
|
+
.map(async (fname) => fcopy(fp_src + fname, fp_dst + fname, { print, overwrite })));
|
|
246
|
+
}
|
|
247
|
+
else
|
|
248
|
+
await fsp.cp(fp_src, fp_dst, {
|
|
249
|
+
recursive: true,
|
|
250
|
+
force: overwrite,
|
|
251
|
+
errorOnExist: !overwrite,
|
|
252
|
+
mode: overwrite ? 0 : fs.constants.COPYFILE_EXCL,
|
|
253
|
+
});
|
|
245
254
|
else
|
|
246
255
|
try {
|
|
247
256
|
await fsp.copyFile(fp_src, fp_dst, overwrite ? 0 : fs.constants.COPYFILE_EXCL);
|
|
@@ -306,7 +315,7 @@ export async function frename(fp, fp_, { fpd, print = true, overwrite = true } =
|
|
|
306
315
|
assert(path.isAbsolute(fp) && path.isAbsolute(fp_), t('fp 和 fp_ 必须是绝对路径'));
|
|
307
316
|
if (print)
|
|
308
317
|
console.log(t('重命名'), fp, '->', fp_);
|
|
309
|
-
if (!overwrite && fexists(fp_))
|
|
318
|
+
if (!overwrite && fexists(fp_, { print: false }))
|
|
310
319
|
throw new Error(`${t('已存在')} ${fp}`);
|
|
311
320
|
await fsp.rename(fp, fp_);
|
|
312
321
|
}
|
package/i18n/dict.json
CHANGED
|
@@ -346,5 +346,20 @@
|
|
|
346
346
|
},
|
|
347
347
|
"flstat: 参数 fp: '{{fp}}' 必须是绝对路径": {
|
|
348
348
|
"en": "flstat: parameter fp: '{{fp}}' must be an absolute path"
|
|
349
|
+
},
|
|
350
|
+
"filter 选项只适用于 fp_src 为文件夹": {
|
|
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"
|
|
349
364
|
}
|
|
350
365
|
}
|
package/i18n/scanner/index.js
CHANGED
|
@@ -18,7 +18,7 @@ const DEFAULT_CONFIG = {
|
|
|
18
18
|
debug: false,
|
|
19
19
|
input: [
|
|
20
20
|
// 'src/**/*.{js,jsx,ts,tsx}',
|
|
21
|
-
'!i18n/**',
|
|
21
|
+
'!i18n/**', // Use ! to filter out files or directories
|
|
22
22
|
'!node_modules/**',
|
|
23
23
|
'!**/*.d.ts',
|
|
24
24
|
],
|
|
@@ -35,7 +35,7 @@ const DEFAULT_CONFIG = {
|
|
|
35
35
|
extensions: [], // 避免在 transform 中执行原生的 parseFuncFromString
|
|
36
36
|
},
|
|
37
37
|
trans: {
|
|
38
|
-
extensions: [],
|
|
38
|
+
extensions: [], // 避免在 transform 中执行原生的 parseTransFromString
|
|
39
39
|
fallbackKey: true,
|
|
40
40
|
babylon: {
|
|
41
41
|
sourceType: 'module',
|
|
@@ -74,22 +74,22 @@ const DEFAULT_CONFIG = {
|
|
|
74
74
|
}
|
|
75
75
|
},
|
|
76
76
|
// 禁用 : 和 . 作为 seperator
|
|
77
|
-
keySeparator: false,
|
|
78
|
-
nsSeparator: false,
|
|
77
|
+
keySeparator: false, // char to separate keys
|
|
78
|
+
nsSeparator: false, // char to split namespace from key
|
|
79
79
|
// Context Form
|
|
80
|
-
context: true,
|
|
81
|
-
contextFallback: true,
|
|
82
|
-
contextSeparator: '_',
|
|
80
|
+
context: true, // whether to add context form key
|
|
81
|
+
contextFallback: true, // whether to add a fallback key as well as the context form key
|
|
82
|
+
contextSeparator: '_', // char to split context from key
|
|
83
83
|
// Plural
|
|
84
84
|
// whether to add plural form key
|
|
85
85
|
plural(language, ns, key, options /** Config */) {
|
|
86
86
|
return language === 'en';
|
|
87
87
|
},
|
|
88
|
-
pluralFallback: true,
|
|
89
|
-
pluralSeparator: '_',
|
|
88
|
+
pluralFallback: true, // whether to add a fallback key as well as the plural form key
|
|
89
|
+
pluralSeparator: '_', // char to split plural from key
|
|
90
90
|
// interpolation options
|
|
91
91
|
interpolation: {
|
|
92
|
-
prefix: '{{',
|
|
92
|
+
prefix: '{{', // prefix for interpolation
|
|
93
93
|
suffix: '}}' // suffix for interpolation
|
|
94
94
|
}
|
|
95
95
|
};
|
package/net.js
CHANGED
|
@@ -87,7 +87,7 @@ export async function request(url, { method, queries, headers: _headers, body, t
|
|
|
87
87
|
// --- headers, http/2 开始都用小写的 headers
|
|
88
88
|
let headers = new Headers({
|
|
89
89
|
'accept-language': 'zh-CN,zh;q=0.9,en-US;q=0.8,en;q=0.7,ja-JP;q=0.6,ja;q=0.5',
|
|
90
|
-
'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/
|
|
90
|
+
'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36',
|
|
91
91
|
'sec-ch-ua-platform': '"Windows"',
|
|
92
92
|
'sec-ch-ua-platform-version': '"15.0.0"',
|
|
93
93
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "xshell",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.62",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./index.js",
|
|
6
6
|
"bin": {
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
"interactive programming"
|
|
17
17
|
],
|
|
18
18
|
"engines": {
|
|
19
|
-
"node": ">=
|
|
19
|
+
"node": ">=21.3.0",
|
|
20
20
|
"vscode": ">=1.81.0"
|
|
21
21
|
},
|
|
22
22
|
"author": "ShenHongFei <shen.hongfei@outlook.com> (https://github.com/ShenHongFei)",
|
|
@@ -45,11 +45,11 @@
|
|
|
45
45
|
]
|
|
46
46
|
},
|
|
47
47
|
"dependencies": {
|
|
48
|
-
"@babel/core": "^7.23.
|
|
49
|
-
"@babel/parser": "^7.23.
|
|
50
|
-
"@babel/traverse": "^7.23.
|
|
48
|
+
"@babel/core": "^7.23.5",
|
|
49
|
+
"@babel/parser": "^7.23.5",
|
|
50
|
+
"@babel/traverse": "^7.23.5",
|
|
51
51
|
"@koa/cors": "^4.0.0",
|
|
52
|
-
"@types/ws": "^8.5.
|
|
52
|
+
"@types/ws": "^8.5.10",
|
|
53
53
|
"byte-size": "^8.1.1",
|
|
54
54
|
"chalk": "^5.3.0",
|
|
55
55
|
"chardet": "^2.0.0",
|
|
@@ -61,7 +61,7 @@
|
|
|
61
61
|
"fetch-cookie": "^2.1.0",
|
|
62
62
|
"gulp-sort": "^2.0.0",
|
|
63
63
|
"hash-string": "^1.0.0",
|
|
64
|
-
"i18next": "^23.
|
|
64
|
+
"i18next": "^23.7.7",
|
|
65
65
|
"i18next-scanner": "^4.4.0",
|
|
66
66
|
"js-cookie": "^3.0.5",
|
|
67
67
|
"koa": "^2.14.2",
|
|
@@ -71,40 +71,45 @@
|
|
|
71
71
|
"mime-types": "^2.1.35",
|
|
72
72
|
"ora": "^7.0.1",
|
|
73
73
|
"react": "^18.2.0",
|
|
74
|
-
"react-i18next": "^13.
|
|
74
|
+
"react-i18next": "^13.5.0",
|
|
75
|
+
"react-object-model": "^1.2.0",
|
|
75
76
|
"resolve-path": "^1.4.0",
|
|
76
77
|
"strip-ansi": "^7.1.0",
|
|
77
78
|
"through2": "^4.0.2",
|
|
78
79
|
"tough-cookie": "^4.1.3",
|
|
79
80
|
"tslib": "^2.6.2",
|
|
80
|
-
"typescript": "^5.
|
|
81
|
+
"typescript": "^5.3.2",
|
|
81
82
|
"ua-parser-js": "2.0.0-alpha.2",
|
|
82
|
-
"undici": "^5.
|
|
83
|
+
"undici": "^5.28.2",
|
|
83
84
|
"vinyl": "^3.0.0",
|
|
84
85
|
"vinyl-fs": "^4.0.0",
|
|
85
|
-
"ws": "^8.14.2"
|
|
86
|
+
"ws": "^8.14.2",
|
|
87
|
+
"xterm": "^5.3.0",
|
|
88
|
+
"xterm-addon-fit": "^0.8.0",
|
|
89
|
+
"xterm-addon-web-links": "^0.9.0",
|
|
90
|
+
"xterm-addon-webgl": "^0.16.0"
|
|
86
91
|
},
|
|
87
92
|
"devDependencies": {
|
|
88
|
-
"@babel/types": "^7.23.
|
|
93
|
+
"@babel/types": "^7.23.5",
|
|
89
94
|
"@types/babel__traverse": "^7.20.4",
|
|
90
95
|
"@types/byte-size": "^8.1.2",
|
|
91
96
|
"@types/chardet": "^0.8.3",
|
|
92
97
|
"@types/gulp-sort": "2.0.4",
|
|
93
98
|
"@types/js-cookie": "^3.0.6",
|
|
94
|
-
"@types/koa": "^2.13.
|
|
99
|
+
"@types/koa": "^2.13.12",
|
|
95
100
|
"@types/koa-compress": "^4.0.6",
|
|
96
|
-
"@types/lodash": "^4.14.
|
|
101
|
+
"@types/lodash": "^4.14.202",
|
|
97
102
|
"@types/mime-types": "^2.1.4",
|
|
98
|
-
"@types/node": "^20.
|
|
99
|
-
"@types/react": "^18.2.
|
|
103
|
+
"@types/node": "^20.10.3",
|
|
104
|
+
"@types/react": "^18.2.41",
|
|
100
105
|
"@types/through2": "^2.0.41",
|
|
101
106
|
"@types/tough-cookie": "^4.0.5",
|
|
102
107
|
"@types/ua-parser-js": "^0.7.39",
|
|
103
108
|
"@types/vinyl-fs": "^3.0.5",
|
|
104
|
-
"@types/vscode": "^1.84.
|
|
105
|
-
"@typescript-eslint/eslint-plugin": "^6.
|
|
106
|
-
"@typescript-eslint/parser": "^6.
|
|
107
|
-
"eslint": "^8.
|
|
109
|
+
"@types/vscode": "^1.84.2",
|
|
110
|
+
"@typescript-eslint/eslint-plugin": "^6.13.1",
|
|
111
|
+
"@typescript-eslint/parser": "^6.13.1",
|
|
112
|
+
"eslint": "^8.55.0",
|
|
108
113
|
"eslint-plugin-react": "^7.33.2",
|
|
109
114
|
"eslint-plugin-xlint": "^1.0.10"
|
|
110
115
|
},
|
package/path.d.ts
CHANGED
|
@@ -54,7 +54,7 @@ export declare function extname(path: string): string;
|
|
|
54
54
|
/** `/` */
|
|
55
55
|
export declare const sep = "/";
|
|
56
56
|
/** The platform-specific file delimiter. ';' or ':'. */
|
|
57
|
-
export declare const delimiter: "
|
|
57
|
+
export declare const delimiter: ":" | ";";
|
|
58
58
|
/** Returns an object from a path string - the opposite of format().
|
|
59
59
|
@param path path to evaluate.
|
|
60
60
|
@throws {TypeError} if `path` is not a string. */
|
|
@@ -81,7 +81,7 @@ export declare let path: {
|
|
|
81
81
|
basename: typeof basename;
|
|
82
82
|
extname: typeof extname;
|
|
83
83
|
sep: string;
|
|
84
|
-
delimiter: "
|
|
84
|
+
delimiter: ":" | ";";
|
|
85
85
|
parse: typeof parse;
|
|
86
86
|
format: typeof format;
|
|
87
87
|
toNamespacedPath: typeof toNamespacedPath;
|
package/process.d.ts
CHANGED
package/prototype.browser.js
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 */
|
|
@@ -63,6 +79,8 @@ export declare class Server {
|
|
|
63
79
|
_router(ctx: Context, next: Next): Promise<void>;
|
|
64
80
|
/** 被子类重写以自定义处理逻辑 */
|
|
65
81
|
router(ctx: Context): Promise<boolean>;
|
|
82
|
+
/** 重定向 ctx 到 url (可以是完整 url 也可以是路径),设置 status code 为 code */
|
|
83
|
+
redirect(ctx: Context, url: string, code?: 301 | 302): void;
|
|
66
84
|
logger(ctx: Context): void;
|
|
67
85
|
process_ua(ctx: Context): string;
|
|
68
86
|
format_ua(headers: IncomingHttpHeaders | IncomingHttp2Headers): string;
|
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,23 +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 = this.default_hostnames[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
|
-
perMessageDeflate: true
|
|
123
|
+
perMessageDeflate: true,
|
|
124
|
+
maxPayload: 2 ** 30 // 1 GB
|
|
74
125
|
});
|
|
75
|
-
this.
|
|
126
|
+
this.websocket_server.on('connection', (ws, request) => {
|
|
76
127
|
ws.addEventListener('message', ({ data }) => {
|
|
77
128
|
this.remote.handle(new Uint8Array(data), ws);
|
|
78
129
|
});
|
|
79
130
|
});
|
|
80
|
-
this.
|
|
131
|
+
this.http_server.on('upgrade', (request, socket, head) => {
|
|
81
132
|
// url 只有路径部分
|
|
82
133
|
const { url, headers, headers: { host = '' }, } = request;
|
|
83
134
|
const ip = request.socket.remoteAddress.replace(/^::ffff:/, '');
|
|
@@ -98,9 +149,9 @@ export class Server {
|
|
|
98
149
|
(this.colors ? url.yellow : url));
|
|
99
150
|
switch (url) {
|
|
100
151
|
case '/':
|
|
101
|
-
this.
|
|
152
|
+
this.websocket_server.handleUpgrade(request, socket, head, ws => {
|
|
102
153
|
ws.binaryType = 'arraybuffer';
|
|
103
|
-
this.
|
|
154
|
+
this.websocket_server.emit('connection', ws, request);
|
|
104
155
|
});
|
|
105
156
|
return;
|
|
106
157
|
default:
|
|
@@ -108,13 +159,95 @@ export class Server {
|
|
|
108
159
|
socket.destroy();
|
|
109
160
|
}
|
|
110
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
|
+
}
|
|
111
221
|
}
|
|
112
|
-
await
|
|
113
|
-
|
|
114
|
-
|
|
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
|
+
}));
|
|
115
234
|
}
|
|
116
235
|
stop() {
|
|
117
|
-
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) {
|
|
118
251
|
}
|
|
119
252
|
async entry(ctx, next) {
|
|
120
253
|
let { response } = ctx;
|
|
@@ -170,10 +303,18 @@ export class Server {
|
|
|
170
303
|
async router(ctx) {
|
|
171
304
|
return false;
|
|
172
305
|
}
|
|
306
|
+
/** 重定向 ctx 到 url (可以是完整 url 也可以是路径),设置 status code 为 code */
|
|
307
|
+
redirect(ctx, url, code = 301) {
|
|
308
|
+
const { request: { _path } } = ctx;
|
|
309
|
+
let { response } = ctx;
|
|
310
|
+
response.redirect(url);
|
|
311
|
+
response.status = code;
|
|
312
|
+
console.log(`${code} 重定向 ${_path} -> ${url}`.yellow);
|
|
313
|
+
}
|
|
173
314
|
logger(ctx) {
|
|
174
315
|
const { colors } = this;
|
|
175
316
|
const { request } = ctx;
|
|
176
|
-
const { query, body, path, _path, protocol, host, req: { httpVersion: http_version }, ip, } = request;
|
|
317
|
+
const { query, body, path, _path, protocol, host, req: { httpVersion: http_version }, ip, headers, } = request;
|
|
177
318
|
let { method } = request;
|
|
178
319
|
let s = '';
|
|
179
320
|
// 时间
|
|
@@ -200,6 +341,16 @@ export class Server {
|
|
|
200
341
|
return colors ? path.yellow : path;
|
|
201
342
|
return path;
|
|
202
343
|
})();
|
|
344
|
+
// range
|
|
345
|
+
let range = headers.range;
|
|
346
|
+
if (headers.range) {
|
|
347
|
+
let [, start, end] = /(\d*)-(\d*)/.exec(range) || [];
|
|
348
|
+
if (start)
|
|
349
|
+
start = Number(start).to_fsize_str();
|
|
350
|
+
if (end)
|
|
351
|
+
end = Number(end).to_fsize_str();
|
|
352
|
+
s += ` (${start} - ${end || ''})`;
|
|
353
|
+
}
|
|
203
354
|
// query
|
|
204
355
|
if (Object.keys(query).length) {
|
|
205
356
|
let t = inspect(query, { compact: true })
|
|
@@ -354,7 +505,7 @@ export class Server {
|
|
|
354
505
|
- download?: `undefined` 在 response.headers 中加上 content-disposition: attachment 指示浏览器下载文件 */
|
|
355
506
|
async fsend(ctx, fp, { root, absolute, download, } = {}) {
|
|
356
507
|
assert(absolute || root?.isdir, t('fsend 必须传 absolute 选项或 root 文件夹'));
|
|
357
|
-
const { request } = ctx;
|
|
508
|
+
const { request, request: { method } } = ctx;
|
|
358
509
|
let { response } = ctx;
|
|
359
510
|
if (!absolute) {
|
|
360
511
|
if (fp.startsWith(root))
|
|
@@ -384,34 +535,44 @@ export class Server {
|
|
|
384
535
|
error.message = `fs.stat 出错: ${error.message}`;
|
|
385
536
|
throw error;
|
|
386
537
|
}
|
|
538
|
+
// 上面的 fstat 异步操作之后有可能 socket 已经关闭,那么这里什么都不做,直接结束吧
|
|
539
|
+
// 如果不这么做,后续 createReadStream 打开的流会赋给 response.body,但是 response.res 不再触发 finish, close 事件,
|
|
540
|
+
// 打开的流永远不会关闭,最终导致资源泄露,文件句柄一直打开
|
|
541
|
+
if (!response.socket) {
|
|
542
|
+
response.res.end();
|
|
543
|
+
return fp;
|
|
544
|
+
}
|
|
387
545
|
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept-Ranges
|
|
388
546
|
// advertise server support of partial requests
|
|
389
547
|
response.set('accept-ranges', 'bytes');
|
|
548
|
+
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control#up-to-date_contents_always
|
|
390
549
|
if (!response.get('cache-control'))
|
|
391
|
-
response.set('cache-control', '
|
|
550
|
+
response.set('cache-control', 'no-cache');
|
|
551
|
+
const last_modified_str = stats.mtime.toUTCString();
|
|
392
552
|
if (!response.get('last-modified'))
|
|
393
|
-
response.set('last-modified',
|
|
553
|
+
response.set('last-modified', last_modified_str);
|
|
394
554
|
if (download)
|
|
395
555
|
response.set('content-disposition', `attachment; filename="${encodeURIComponent(fp.fname)}"`);
|
|
396
556
|
if (!response.get('content-type')) {
|
|
397
557
|
const { contentType: get_content_type } = await import('mime-types');
|
|
398
|
-
const fext = fp
|
|
399
|
-
response.set('content-type', (this.js_exts.has(fext) ? 'text/javascript; chatset=utf-8' : get_content_type(
|
|
558
|
+
const { fext } = fp;
|
|
559
|
+
response.set('content-type', (this.js_exts.has(fext) ? 'text/javascript; chatset=utf-8' : get_content_type(fext))
|
|
400
560
|
|| 'application/octet-stream');
|
|
401
561
|
}
|
|
402
|
-
|
|
562
|
+
const modified_since_str = request.get('if-modified-since');
|
|
563
|
+
if ((method === 'GET' || method === 'HEAD') && modified_since_str === last_modified_str) {
|
|
403
564
|
response.status = 304;
|
|
404
565
|
// 以上会自动设置 response.body = null
|
|
405
566
|
return fp;
|
|
406
567
|
}
|
|
407
|
-
|
|
568
|
+
const range_header = request.headers.range;
|
|
569
|
+
if (range_header)
|
|
408
570
|
try {
|
|
409
|
-
const range_header = request.headers.range;
|
|
410
571
|
const range_value = /=(.*)$/.exec(range_header)[1];
|
|
411
572
|
const range = /^[\w]*?(\d*)-(\d*)$/.exec(range_value);
|
|
412
573
|
assert(stats.size <= Number.MAX_SAFE_INTEGER);
|
|
413
|
-
let start = range[1] ?
|
|
414
|
-
let end = range[2] ?
|
|
574
|
+
let start = range[1] ? Number(range[1]) : undefined;
|
|
575
|
+
let end = range[2] ? Number(range[2]) : Number(stats.size) - 1;
|
|
415
576
|
if (start === undefined) {
|
|
416
577
|
start = Number(stats.size) - end;
|
|
417
578
|
end = Number(stats.size) - 1;
|
package/utils.browser.d.ts
CHANGED
|
@@ -52,7 +52,10 @@ export declare class Lock<TResource = void> {
|
|
|
52
52
|
export declare function pause(milliseconds?: number): Promise<void>;
|
|
53
53
|
/** 字符串字典序比较 */
|
|
54
54
|
export declare function strcmp(l: string, r: string): 0 | 1 | -1;
|
|
55
|
+
/** 比较 1.10.02 这种版本号 */
|
|
56
|
+
export declare function vercmp(l: string, r: string): number;
|
|
55
57
|
export declare function get<TReturn = any>(obj: any, keypath: string): TReturn;
|
|
58
|
+
export declare function global_get<TReturn = any>(keypath: string): TReturn;
|
|
56
59
|
export declare function invoke<TReturn = any>(obj: any, funcpath: string, args: any[]): TReturn;
|
|
57
60
|
/** 拼接 TypedArrays 生成一个完整的 Uint8Array */
|
|
58
61
|
export declare function concat(arrays: ArrayBufferView[]): Uint8Array;
|
package/utils.browser.js
CHANGED
|
@@ -130,12 +130,29 @@ export function strcmp(l, r) {
|
|
|
130
130
|
return -1;
|
|
131
131
|
return 1;
|
|
132
132
|
}
|
|
133
|
+
/** 比较 1.10.02 这种版本号 */
|
|
134
|
+
export function vercmp(l, r) {
|
|
135
|
+
const lparts = l.split('.').map(x => Number(x));
|
|
136
|
+
const rparts = r.split('.').map(x => Number(x));
|
|
137
|
+
assert(lparts.length === rparts.length, '传入 vercmp 的两个版本号应该格式一致');
|
|
138
|
+
for (let i = 0; i < lparts.length; i++) {
|
|
139
|
+
const l = lparts[i];
|
|
140
|
+
const r = rparts[i];
|
|
141
|
+
assert(!isNaN(l) && !isNaN(r), '传入 vercmp 的版本非法');
|
|
142
|
+
if (l !== r)
|
|
143
|
+
return l - r;
|
|
144
|
+
}
|
|
145
|
+
return 0;
|
|
146
|
+
}
|
|
133
147
|
export function get(obj, keypath) {
|
|
134
148
|
let obj_ = obj;
|
|
135
149
|
for (const key of keypath.split('.'))
|
|
136
150
|
obj_ = obj_[key];
|
|
137
151
|
return obj_;
|
|
138
152
|
}
|
|
153
|
+
export function global_get(keypath) {
|
|
154
|
+
return get(globalThis, keypath);
|
|
155
|
+
}
|
|
139
156
|
export function invoke(obj, funcpath, args) {
|
|
140
157
|
const paths = funcpath.split('.');
|
|
141
158
|
let obj_ = obj;
|
package/utils.d.ts
CHANGED
|
@@ -23,7 +23,10 @@ export declare function unique<T>(iterable: T[] | Iterable<T>, selector?: string
|
|
|
23
23
|
export declare function sort_keys<T>(obj: T): T;
|
|
24
24
|
/** 字符串字典序比较 */
|
|
25
25
|
export declare function strcmp(l: string, r: string): 0 | 1 | -1;
|
|
26
|
+
/** 比较 1.10.02 这种版本号 */
|
|
27
|
+
export declare function vercmp(l: string, r: string): number;
|
|
26
28
|
export declare function get<TReturn = any>(obj: any, keypath: string): TReturn;
|
|
29
|
+
export declare function global_get<TReturn = any>(keypath: string): TReturn;
|
|
27
30
|
export declare function invoke<TReturn = any>(obj: any, funcpath: string, args: any[]): TReturn;
|
|
28
31
|
/** 拼接 TypedArrays 生成一个完整的 Uint8Array */
|
|
29
32
|
export declare function concat(arrays: ArrayBufferView[]): Uint8Array;
|
package/utils.js
CHANGED
|
@@ -90,12 +90,29 @@ export function strcmp(l, r) {
|
|
|
90
90
|
return -1;
|
|
91
91
|
return 1;
|
|
92
92
|
}
|
|
93
|
+
/** 比较 1.10.02 这种版本号 */
|
|
94
|
+
export function vercmp(l, r) {
|
|
95
|
+
const lparts = l.split('.').map(x => Number(x));
|
|
96
|
+
const rparts = r.split('.').map(x => Number(x));
|
|
97
|
+
assert(lparts.length === rparts.length, '传入 vercmp 的两个版本号应该格式一致');
|
|
98
|
+
for (let i = 0; i < lparts.length; i++) {
|
|
99
|
+
const l = lparts[i];
|
|
100
|
+
const r = rparts[i];
|
|
101
|
+
assert(!isNaN(l) && !isNaN(r), '传入 vercmp 的版本非法');
|
|
102
|
+
if (l !== r)
|
|
103
|
+
return l - r;
|
|
104
|
+
}
|
|
105
|
+
return 0;
|
|
106
|
+
}
|
|
93
107
|
export function get(obj, keypath) {
|
|
94
108
|
let obj_ = obj;
|
|
95
109
|
for (const key of keypath.split('.'))
|
|
96
110
|
obj_ = obj_[key];
|
|
97
111
|
return obj_;
|
|
98
112
|
}
|
|
113
|
+
export function global_get(keypath) {
|
|
114
|
+
return get(globalThis, keypath);
|
|
115
|
+
}
|
|
99
116
|
export function invoke(obj, funcpath, args) {
|
|
100
117
|
const paths = funcpath.split('.');
|
|
101
118
|
let obj_ = obj;
|