xshell 1.0.47 → 1.0.49
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/file.js +3 -3
- package/i18n/dict.json +6 -0
- package/i18n/rwdict.js +2 -2
- package/net.browser.js +1 -0
- package/net.d.ts +4 -2
- package/net.js +5 -1
- package/package.json +6 -5
- package/prototype.js +2 -1
- package/repl.js +2 -9
- package/server.d.ts +21 -9
- package/server.js +93 -55
- package/utils.browser.d.ts +3 -0
- package/utils.browser.js +8 -0
- package/utils.d.ts +3 -0
- package/utils.js +8 -0
package/file.js
CHANGED
|
@@ -207,7 +207,7 @@ export async function fcopy(fp_src, fp_dst, { print = true, overwrite = true, }
|
|
|
207
207
|
assert(fp_src.isdir === fp_dst.isdir, t('fp_src 和 fp_dst 必须同为文件路径或文件夹路径'));
|
|
208
208
|
assert(path.isAbsolute(fp_src) && path.isAbsolute(fp_dst), t('fp_src 和 fp_dst 必须为完整路径'));
|
|
209
209
|
if (print)
|
|
210
|
-
console.log(t('复制'), fp_src, '
|
|
210
|
+
console.log(t('复制'), fp_src, '->', fp_dst);
|
|
211
211
|
const { copy } = await import('fs-extra');
|
|
212
212
|
await copy(fp_src, fp_dst, { overwrite, errorOnExist: true });
|
|
213
213
|
}
|
|
@@ -222,7 +222,7 @@ export async function fmove(src, dst, { overwrite = false, print = true } = {})
|
|
|
222
222
|
if (!path.isAbsolute(src) || !path.isAbsolute(dst))
|
|
223
223
|
throw new Error(t('src 和 dst 必须为完整路径'));
|
|
224
224
|
if (print)
|
|
225
|
-
console.log(t('移动'), src, '
|
|
225
|
+
console.log(t('移动'), src, '->', dst);
|
|
226
226
|
const { move } = await import('fs-extra');
|
|
227
227
|
await move(src, dst, { overwrite });
|
|
228
228
|
}
|
|
@@ -241,7 +241,7 @@ export async function frename(fp, fp_, { fpd, print = true, overwrite = true } =
|
|
|
241
241
|
}
|
|
242
242
|
assert(path.isAbsolute(fp) && path.isAbsolute(fp_), t('fp 和 fp_ 必须是绝对路径'));
|
|
243
243
|
if (print)
|
|
244
|
-
console.log(t('重命名'), fp, '
|
|
244
|
+
console.log(t('重命名'), fp, '->', fp_);
|
|
245
245
|
if (!overwrite && fexists(fp_))
|
|
246
246
|
throw new Error(t('文件已存在:') + fp_);
|
|
247
247
|
await fsp.rename(fp, fp_);
|
package/i18n/dict.json
CHANGED
|
@@ -337,5 +337,11 @@
|
|
|
337
337
|
},
|
|
338
338
|
"fsend 必须传 absolute 选项或 root 文件夹": {
|
|
339
339
|
"en": "fsend must pass absolute option or root folder"
|
|
340
|
+
},
|
|
341
|
+
"message.data 数组中不能有 undefined 的项, 因为 json 序列化后会变为 null": {
|
|
342
|
+
"en": "There cannot be undefined items in the message.data array, because json will become null after serialization"
|
|
343
|
+
},
|
|
344
|
+
"xshell 启动成功,正在监听: http://localhost:8421": {
|
|
345
|
+
"en": "xshell started successfully and is listening: http://localhost:8421"
|
|
340
346
|
}
|
|
341
347
|
}
|
package/i18n/rwdict.js
CHANGED
|
@@ -60,14 +60,14 @@ export class RWDict extends Dict {
|
|
|
60
60
|
if (_translation !== translation)
|
|
61
61
|
if (!overwrite) {
|
|
62
62
|
console.error(`${`已存在 ${id} 词条:`.red} ${JSON.stringify(item)}`);
|
|
63
|
-
console.error(`${'M? '.yellow}${_translation.replace(/\n/g, '\\n')}
|
|
63
|
+
console.error(`${'M? '.yellow}${_translation.replace(/\n/g, '\\n')} -> ${translation.replace(/\n/g, '\\n')}`);
|
|
64
64
|
if (!dryrun)
|
|
65
65
|
console.error('如要更新翻译请设置 { overwrite: true },否则使用 i18n.t(\'text\', { context: \'xxx\' }) 标记文本以区分。\n');
|
|
66
66
|
return;
|
|
67
67
|
}
|
|
68
68
|
else {
|
|
69
69
|
if (print)
|
|
70
|
-
console.log(`${'M '.yellow}${_translation.replace(/\n/g, '\\n')}
|
|
70
|
+
console.log(`${'M '.yellow}${_translation.replace(/\n/g, '\\n')} -> ${translation.replace(/\n/g, '\\n')}`);
|
|
71
71
|
if (!dryrun)
|
|
72
72
|
item[language] = translation;
|
|
73
73
|
}
|
package/net.browser.js
CHANGED
|
@@ -380,6 +380,7 @@ export class Remote {
|
|
|
380
380
|
作为 websocket 连接接收方,必传 websocket 参数
|
|
381
381
|
发送或连接出错时自动清理 message.id 对应的 handler */
|
|
382
382
|
async send(message, websocket) {
|
|
383
|
+
assert(!message.data || message.data.every(arg => arg !== undefined), t('message.data 数组中不能有 undefined 的项, 因为 json 序列化后会变为 null'));
|
|
383
384
|
if (this.print)
|
|
384
385
|
console.log('remote.send:', message);
|
|
385
386
|
try {
|
package/net.d.ts
CHANGED
|
@@ -10,12 +10,15 @@ declare module 'tough-cookie' {
|
|
|
10
10
|
import './prototype.js';
|
|
11
11
|
import type { Encoding } from './file.js';
|
|
12
12
|
import { inspect, Lock } from './utils.js';
|
|
13
|
+
export type { WebSocket, Headers };
|
|
14
|
+
export declare const WebSocketConnecting = 0;
|
|
13
15
|
export declare const WebSocketOpen = 1;
|
|
16
|
+
export declare const WebSocketClosing = 2;
|
|
17
|
+
export declare const WebSocketClosed = 3;
|
|
14
18
|
export declare enum MyProxy {
|
|
15
19
|
socks5 = "http://localhost:10080",
|
|
16
20
|
whistle = "http://localhost:8899"
|
|
17
21
|
}
|
|
18
|
-
export type { Headers };
|
|
19
22
|
export declare const cookies: {
|
|
20
23
|
store: MemoryCookieStore;
|
|
21
24
|
jar: CookieJar;
|
|
@@ -102,7 +105,6 @@ export declare function rpc(func: string, args?: any[], { url, async: _async, ig
|
|
|
102
105
|
async?: boolean;
|
|
103
106
|
ignore?: boolean;
|
|
104
107
|
}): Promise<any>;
|
|
105
|
-
export type { WebSocket };
|
|
106
108
|
export declare class WebSocketConnectionError extends Error {
|
|
107
109
|
name: string;
|
|
108
110
|
websocket: WebSocket;
|
package/net.js
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import { t } from './i18n/instance.js';
|
|
2
2
|
import './prototype.js';
|
|
3
3
|
import { inspect, concat, assert, genid, delay, Lock } from './utils.js';
|
|
4
|
+
export const WebSocketConnecting = 0;
|
|
4
5
|
export const WebSocketOpen = 1;
|
|
6
|
+
export const WebSocketClosing = 2;
|
|
7
|
+
export const WebSocketClosed = 3;
|
|
5
8
|
export var MyProxy;
|
|
6
9
|
(function (MyProxy) {
|
|
7
10
|
MyProxy["socks5"] = "http://localhost:10080";
|
|
@@ -84,7 +87,7 @@ export async function request(url, { method, queries, headers: _headers, body, t
|
|
|
84
87
|
// --- headers, http/2 开始都用小写的 headers
|
|
85
88
|
let headers = new Headers({
|
|
86
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',
|
|
87
|
-
'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/115.0.0.0 Safari/537.36',
|
|
88
91
|
});
|
|
89
92
|
if (body !== undefined)
|
|
90
93
|
headers.set('content-type', type);
|
|
@@ -538,6 +541,7 @@ export class Remote {
|
|
|
538
541
|
作为 websocket 连接接收方,必传 websocket 参数
|
|
539
542
|
发送或连接出错时自动清理 message.id 对应的 handler */
|
|
540
543
|
async send(message, websocket) {
|
|
544
|
+
assert(!message.data || message.data.every(arg => arg !== undefined), 'message.data 数组中不能有 undefined 的项, 因为 json 序列化后会变为 null');
|
|
541
545
|
if (this.print)
|
|
542
546
|
console.log('remote.send:', message);
|
|
543
547
|
try {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "xshell",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.49",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./index.js",
|
|
6
6
|
"bin": {
|
|
@@ -67,19 +67,19 @@
|
|
|
67
67
|
"js-cookie": "^3.0.5",
|
|
68
68
|
"koa": "^2.14.2",
|
|
69
69
|
"koa-compress": "^5.1.1",
|
|
70
|
-
"koa-useragent": "^4.1.0",
|
|
71
70
|
"lodash": "^4.17.21",
|
|
72
71
|
"map-stream": "0.0.7",
|
|
73
72
|
"mime-types": "^2.1.35",
|
|
74
73
|
"ora": "^7.0.1",
|
|
75
74
|
"react": "^18.2.0",
|
|
76
|
-
"react-i18next": "^13.1.
|
|
75
|
+
"react-i18next": "^13.1.2",
|
|
77
76
|
"resolve-path": "^1.4.0",
|
|
78
77
|
"strip-ansi": "^7.1.0",
|
|
79
78
|
"through2": "^4.0.2",
|
|
80
79
|
"tough-cookie": "^4.1.3",
|
|
81
80
|
"tslib": "^2.6.1",
|
|
82
81
|
"typescript": "^5.1.6",
|
|
82
|
+
"ua-parser-js": "2.0.0-alpha.2",
|
|
83
83
|
"undici": "^5.23.0",
|
|
84
84
|
"vinyl": "^3.0.0",
|
|
85
85
|
"vinyl-fs": "^4.0.0",
|
|
@@ -97,15 +97,16 @@
|
|
|
97
97
|
"@types/koa-compress": "^4.0.3",
|
|
98
98
|
"@types/lodash": "^4.14.197",
|
|
99
99
|
"@types/mime-types": "^2.1.1",
|
|
100
|
-
"@types/node": "^20.4.
|
|
100
|
+
"@types/node": "^20.4.10",
|
|
101
101
|
"@types/react": "^18.2.20",
|
|
102
102
|
"@types/through2": "^2.0.38",
|
|
103
103
|
"@types/tough-cookie": "^4.0.2",
|
|
104
|
+
"@types/ua-parser-js": "^0.7.36",
|
|
104
105
|
"@types/vinyl-fs": "^3.0.2",
|
|
105
106
|
"@types/vscode": "^1.81.0",
|
|
106
107
|
"@typescript-eslint/eslint-plugin": "^6.3.0",
|
|
107
108
|
"@typescript-eslint/parser": "^6.3.0",
|
|
108
|
-
"eslint": "^8.
|
|
109
|
+
"eslint": "^8.47.0",
|
|
109
110
|
"eslint-plugin-react": "^7.33.1",
|
|
110
111
|
"eslint-plugin-xlint": "^1.0.6"
|
|
111
112
|
},
|
package/prototype.js
CHANGED
|
@@ -93,7 +93,8 @@ if (!globalThis.my_prototype_defined) {
|
|
|
93
93
|
cur_width += w;
|
|
94
94
|
if (cur_width > width) {
|
|
95
95
|
const i_fitted_next = i_fitted + 1;
|
|
96
|
-
|
|
96
|
+
// … 在 winterm 中对不齐,使用 ··· 代替
|
|
97
|
+
const t = s.slice(0, i_fitted_next) + ' '.repeat(width - 2 - fitted_width) + '··';
|
|
97
98
|
return color_bak ? color_bak + t + '\u001b[39m' : t;
|
|
98
99
|
}
|
|
99
100
|
}
|
package/repl.js
CHANGED
|
@@ -4,7 +4,7 @@ import { fileURLToPath } from 'url';
|
|
|
4
4
|
import { path } from './path.js';
|
|
5
5
|
import { t } from './i18n/instance.js';
|
|
6
6
|
import './prototype.js';
|
|
7
|
-
import { delay, set_inspect_options
|
|
7
|
+
import { delay, set_inspect_options } from './utils.js';
|
|
8
8
|
import { Remote } from './net.js';
|
|
9
9
|
set_inspect_options();
|
|
10
10
|
let server;
|
|
@@ -12,7 +12,6 @@ let server;
|
|
|
12
12
|
export const fpd_root = path.dirname(fileURLToPath(import.meta.url)) + '/';
|
|
13
13
|
export async function start_repl() {
|
|
14
14
|
// ------------ 加载库
|
|
15
|
-
console.log(t('xshell 开始启动').yellow);
|
|
16
15
|
// --- prevent from exiting
|
|
17
16
|
process.on('uncaughtException', error => {
|
|
18
17
|
console.error(error);
|
|
@@ -25,7 +24,6 @@ export async function start_repl() {
|
|
|
25
24
|
useColors: true,
|
|
26
25
|
terminal: true,
|
|
27
26
|
});
|
|
28
|
-
console.log(t('nodejs.repl 启动成功'));
|
|
29
27
|
process.title = 'xshell';
|
|
30
28
|
await Promise.all([
|
|
31
29
|
pollute_global(),
|
|
@@ -44,13 +42,9 @@ export async function start_repl() {
|
|
|
44
42
|
})
|
|
45
43
|
});
|
|
46
44
|
await server.start();
|
|
47
|
-
console.log(t('server 启动成功'));
|
|
48
45
|
})(),
|
|
49
46
|
]);
|
|
50
|
-
console.log(('
|
|
51
|
-
t('xshell 启动成功,用时 ') + delta2str(new Date().getTime() - globalThis.tstarted.getTime()) + '\n' +
|
|
52
|
-
t('正在监听: http://localhost:8421\n') +
|
|
53
|
-
'-'.repeat(30)).green);
|
|
47
|
+
console.log(t('xshell 启动成功,正在监听: http://localhost:8421').green);
|
|
54
48
|
}
|
|
55
49
|
export async function stop() {
|
|
56
50
|
console.log(t('xshell 正在退出').red);
|
|
@@ -88,7 +82,6 @@ export async function pollute_global() {
|
|
|
88
82
|
pollute_module_exports('./server.js'),
|
|
89
83
|
pollute_module_exports('./repl.js'),
|
|
90
84
|
]);
|
|
91
|
-
console.log(t('所有模块加载成功'));
|
|
92
85
|
}
|
|
93
86
|
export async function pollute_module_exports(fp_mod) {
|
|
94
87
|
Object.assign(globalThis, await import(fp_mod));
|
package/server.d.ts
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
/// <reference types="node" resolution-mode="require"/>
|
|
2
|
+
/// <reference types="ua-parser-js" />
|
|
2
3
|
/// <reference types="node" resolution-mode="require"/>
|
|
3
|
-
|
|
4
|
+
/// <reference types="node" resolution-mode="require"/>
|
|
5
|
+
import { type Server as HttpServer, type IncomingHttpHeaders } from 'http';
|
|
6
|
+
import type { IncomingHttpHeaders as IncomingHttp2Headers } from 'http2';
|
|
4
7
|
import type { WebSocketServer } from 'ws';
|
|
5
8
|
import type { default as Koa, Context, Next } from 'koa';
|
|
6
|
-
import type { UserAgentContext } from 'koa-useragent';
|
|
7
9
|
declare module 'koa' {
|
|
8
10
|
interface Request {
|
|
9
11
|
/** 经过 decodeURIComponent 后,在路径重写之前的路径 */
|
|
@@ -12,9 +14,6 @@ declare module 'koa' {
|
|
|
12
14
|
}
|
|
13
15
|
interface Context {
|
|
14
16
|
compress: boolean;
|
|
15
|
-
userAgent: UserAgentContext['userAgent'] & {
|
|
16
|
-
isWechat: boolean;
|
|
17
|
-
};
|
|
18
17
|
}
|
|
19
18
|
}
|
|
20
19
|
import { type Remote, type Headers } from './net.js';
|
|
@@ -29,6 +28,19 @@ declare module 'http' {
|
|
|
29
28
|
export declare class Server {
|
|
30
29
|
/** proxy 时需要丢弃的 resposne headers */
|
|
31
30
|
static drop_response_headers: Set<string>;
|
|
31
|
+
UAParser: {
|
|
32
|
+
(uastring?: string, extensions?: Record<string, unknown>): import("ua-parser-js").IResult;
|
|
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
|
+
};
|
|
32
44
|
js_exts: Set<string>;
|
|
33
45
|
app: Koa;
|
|
34
46
|
handler: ReturnType<Koa['callback']>;
|
|
@@ -45,15 +57,15 @@ export declare class Server {
|
|
|
45
57
|
start(): Promise<void>;
|
|
46
58
|
stop(): void;
|
|
47
59
|
entry(ctx: Context, next: Next): Promise<void>;
|
|
48
|
-
/**
|
|
49
|
-
|
|
50
|
-
process request.ip
|
|
51
|
-
*/
|
|
60
|
+
/** 解析 req.body to request.body
|
|
61
|
+
处理 request.ip */
|
|
52
62
|
parse(ctx: Context): Promise<void>;
|
|
53
63
|
_router(ctx: Context, next: Next): Promise<void>;
|
|
54
64
|
/** 被子类重写以自定义处理逻辑 */
|
|
55
65
|
router(ctx: Context): Promise<boolean>;
|
|
56
66
|
logger(ctx: Context): void;
|
|
67
|
+
process_ua(ctx: Context): string;
|
|
68
|
+
format_ua(headers: IncomingHttpHeaders | IncomingHttp2Headers): string;
|
|
57
69
|
proxy(ctx: Context, url: string | URL, headers_?: Record<string, string>): Promise<void>;
|
|
58
70
|
static filter_response_headers(headers: Headers): {};
|
|
59
71
|
try_send(ctx: Context, fp: string, { root, log_404, }: {
|
package/server.js
CHANGED
|
@@ -3,7 +3,6 @@ import zlib from 'zlib';
|
|
|
3
3
|
import { createReadStream } from 'fs';
|
|
4
4
|
import { Readable } from 'stream';
|
|
5
5
|
import util from 'util';
|
|
6
|
-
import querystring from 'querystring';
|
|
7
6
|
// --- my libs
|
|
8
7
|
import { t } from './i18n/instance.js';
|
|
9
8
|
import { request as _request } from './net.js';
|
|
@@ -20,6 +19,7 @@ export class Server {
|
|
|
20
19
|
'transfer-encoding',
|
|
21
20
|
'x-powered-by'
|
|
22
21
|
]);
|
|
22
|
+
UAParser = null;
|
|
23
23
|
js_exts = new Set(['.js', '.mjs', '.cjs']);
|
|
24
24
|
app;
|
|
25
25
|
handler;
|
|
@@ -39,7 +39,8 @@ export class Server {
|
|
|
39
39
|
const { default: Koa } = await import('koa');
|
|
40
40
|
const { default: KoaCors } = await import('@koa/cors');
|
|
41
41
|
const { default: KoaCompress } = await import('koa-compress');
|
|
42
|
-
const {
|
|
42
|
+
const { UAParser } = await import('ua-parser-js');
|
|
43
|
+
this.UAParser = UAParser;
|
|
43
44
|
const { WebSocketServer } = await import('ws');
|
|
44
45
|
// --- init koa app
|
|
45
46
|
let app = new Koa();
|
|
@@ -60,7 +61,6 @@ export class Server {
|
|
|
60
61
|
threshold: 512
|
|
61
62
|
}));
|
|
62
63
|
app.use(KoaCors({ credentials: true }));
|
|
63
|
-
app.use(KoaUserAgent);
|
|
64
64
|
app.use(this._router.bind(this));
|
|
65
65
|
this.app = app;
|
|
66
66
|
this.handler = this.app.callback();
|
|
@@ -79,9 +79,23 @@ export class Server {
|
|
|
79
79
|
});
|
|
80
80
|
this.server_http.on('upgrade', (request, socket, head) => {
|
|
81
81
|
// url 只有路径部分
|
|
82
|
-
const { url, headers: { host = ''
|
|
82
|
+
const { url, headers, headers: { host = '' }, } = request;
|
|
83
83
|
const ip = request.socket.remoteAddress.replace(/^::ffff:/, '');
|
|
84
|
-
console.log(
|
|
84
|
+
console.log(
|
|
85
|
+
// 时间
|
|
86
|
+
`${new Date().to_time_str()} ` +
|
|
87
|
+
// ip(位置)
|
|
88
|
+
(ip || '').limit(60) + ' ' +
|
|
89
|
+
// ua
|
|
90
|
+
this.format_ua(headers).limit(56) + ' ' +
|
|
91
|
+
// https/2.0
|
|
92
|
+
`${this.colors ? 'websocket'.limit(10).magenta : 'websocket'.limit(10)} ` +
|
|
93
|
+
// method
|
|
94
|
+
''.limit(6) + ' ' +
|
|
95
|
+
// host
|
|
96
|
+
`${host.limit(24)} ` +
|
|
97
|
+
// path
|
|
98
|
+
(this.colors ? url.yellow : url));
|
|
85
99
|
switch (url) {
|
|
86
100
|
case '/':
|
|
87
101
|
this.server_ws.handleUpgrade(request, socket, head, ws => {
|
|
@@ -117,10 +131,8 @@ export class Server {
|
|
|
117
131
|
response.type = 'text/plain';
|
|
118
132
|
}
|
|
119
133
|
}
|
|
120
|
-
/**
|
|
121
|
-
|
|
122
|
-
process request.ip
|
|
123
|
-
*/
|
|
134
|
+
/** 解析 req.body to request.body
|
|
135
|
+
处理 request.ip */
|
|
124
136
|
async parse(ctx) {
|
|
125
137
|
let { req, request } = ctx;
|
|
126
138
|
const buf = await stream_to_buffer(req);
|
|
@@ -131,14 +143,10 @@ export class Server {
|
|
|
131
143
|
// --- parse body
|
|
132
144
|
if (!req.body)
|
|
133
145
|
return;
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
else if (ctx.is('multipart/form-data'))
|
|
139
|
-
throw new Error('multipart/form-data is not supported');
|
|
140
|
-
else
|
|
141
|
-
request.body = req.body;
|
|
146
|
+
request.body = ctx.is('application/json') || ctx.is('text/plain') ?
|
|
147
|
+
JSON.parse(req.body.toString())
|
|
148
|
+
:
|
|
149
|
+
req.body;
|
|
142
150
|
}
|
|
143
151
|
async _router(ctx, next) {
|
|
144
152
|
let { request } = ctx;
|
|
@@ -167,49 +175,32 @@ export class Server {
|
|
|
167
175
|
const { request } = ctx;
|
|
168
176
|
const { query, body, path, _path, protocol, host, req: { httpVersion: http_version }, ip, } = request;
|
|
169
177
|
let { method } = request;
|
|
170
|
-
const ua = ctx.userAgent;
|
|
171
178
|
let s = '';
|
|
172
|
-
//
|
|
173
|
-
s += `${new Date().to_time_str()}
|
|
174
|
-
//
|
|
175
|
-
s += (ip || '').
|
|
176
|
-
//
|
|
177
|
-
s += (()
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
if (ua.isDesktop)
|
|
182
|
-
t += 'desktop';
|
|
183
|
-
if (ua.isBot)
|
|
184
|
-
t += `${t ? ' ' : ''}${colors ? 'robot'.blue : 'robot'}`;
|
|
185
|
-
if (ua.platform !== 'unknown' && !ua.os.startsWith('Windows'))
|
|
186
|
-
t += '/' + ua.platform.toLowerCase().replace('apple mac', 'mac');
|
|
187
|
-
if (ua.os !== 'unknown' && ua.platform !== 'Android')
|
|
188
|
-
t += '/' + ua.os.toLowerCase();
|
|
189
|
-
if (ua.browser !== 'unknown')
|
|
190
|
-
t += '/' + ua.browser.toLowerCase();
|
|
191
|
-
if (ua.isWechat)
|
|
192
|
-
t += '/weixin';
|
|
193
|
-
if (ua.version !== 'unknown')
|
|
194
|
-
t += '/' + ua.version.split('.').slice(0, 2).join('.');
|
|
195
|
-
return t;
|
|
196
|
-
})().pad(40) + ' ';
|
|
197
|
-
// --- https/2.0
|
|
198
|
-
s += `${`${protocol.pad(5)}/${http_version}`.pad(10)} `;
|
|
199
|
-
// --- method
|
|
179
|
+
// 时间
|
|
180
|
+
s += `${new Date().to_time_str()} `;
|
|
181
|
+
// ip(位置)
|
|
182
|
+
s += (ip || '').limit(60) + ' ';
|
|
183
|
+
// ua
|
|
184
|
+
s += this.process_ua(ctx).limit(56) + ' ';
|
|
185
|
+
// https/2.0
|
|
186
|
+
s += `${`${protocol.limit(5)} ${http_version}`.limit(10)} `;
|
|
187
|
+
// method
|
|
200
188
|
method = method.toLowerCase();
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
189
|
+
let t = method.limit(6) + ' ';
|
|
190
|
+
if (colors && method !== 'get')
|
|
191
|
+
method = t.yellow;
|
|
192
|
+
s += t;
|
|
193
|
+
// host
|
|
194
|
+
s += `${host.limit(24)} `;
|
|
195
|
+
// path
|
|
205
196
|
s += (() => {
|
|
206
197
|
if (path.toLowerCase() !== _path.toLowerCase())
|
|
207
|
-
return `${_path.blue}
|
|
198
|
+
return `${_path.blue} -> ${path}`;
|
|
208
199
|
if (!path.includes('.'))
|
|
209
200
|
return colors ? path.yellow : path;
|
|
210
201
|
return path;
|
|
211
202
|
})();
|
|
212
|
-
//
|
|
203
|
+
// query
|
|
213
204
|
if (Object.keys(query).length) {
|
|
214
205
|
let t = inspect(query, { compact: true })
|
|
215
206
|
.replace('[Object: null prototype] ', '');
|
|
@@ -218,12 +209,59 @@ export class Server {
|
|
|
218
209
|
s += (s + t).width > output_width ? '\n' : ' ';
|
|
219
210
|
s += t;
|
|
220
211
|
}
|
|
221
|
-
//
|
|
212
|
+
// body
|
|
222
213
|
if (body && Object.keys(body).length)
|
|
223
214
|
s += '\n' + inspect(body).replace('[Object: null prototype] ', '');
|
|
224
|
-
//
|
|
215
|
+
// 打印日志
|
|
225
216
|
console.log(s);
|
|
226
217
|
}
|
|
218
|
+
process_ua(ctx) {
|
|
219
|
+
const { headers, headers: { 'user-agent': ua_str } } = ctx.request;
|
|
220
|
+
if (!ua_str)
|
|
221
|
+
return '';
|
|
222
|
+
ctx.response.set('accept-ch', 'Sec-CH-UA, Sec-CH-UA-Mobile, Sec-CH-UA-Model, Sec-CH-UA-Platform, Sec-CH-UA-Platform-Version, RTT, Device-Memory');
|
|
223
|
+
return this.format_ua(headers);
|
|
224
|
+
}
|
|
225
|
+
format_ua(headers) {
|
|
226
|
+
const { rtt, 'device-memory': memory } = headers;
|
|
227
|
+
const { device, os, browser } = this.UAParser(headers).withClientHints();
|
|
228
|
+
const vendor = device.vendor?.toLowerCase();
|
|
229
|
+
const model = device.model?.toLowerCase();
|
|
230
|
+
const osname = os.name?.toLowerCase();
|
|
231
|
+
const browser_name = browser.name?.toLowerCase();
|
|
232
|
+
let s = '';
|
|
233
|
+
if (vendor && vendor !== 'apple')
|
|
234
|
+
s += vendor;
|
|
235
|
+
if (model && model !== 'k' && model !== 'macintosh') {
|
|
236
|
+
if (s)
|
|
237
|
+
s += ' ';
|
|
238
|
+
s += model.replace('22127rk46c', 'redmi k60 pro');
|
|
239
|
+
}
|
|
240
|
+
if (osname) {
|
|
241
|
+
if (s)
|
|
242
|
+
s += '/';
|
|
243
|
+
s += osname;
|
|
244
|
+
if (os.version)
|
|
245
|
+
s += ` ${os.version.split('.')[0].toLowerCase()}`;
|
|
246
|
+
}
|
|
247
|
+
if (browser_name) {
|
|
248
|
+
if (s)
|
|
249
|
+
s += '/';
|
|
250
|
+
s += browser_name.replace('mobile chrome', 'chrome');
|
|
251
|
+
if (browser.version) {
|
|
252
|
+
const [major, minor] = browser.version.toLowerCase().split('.');
|
|
253
|
+
s += ` ${major}`;
|
|
254
|
+
if (minor && minor !== '0')
|
|
255
|
+
s += `.${minor}`;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
if (memory) {
|
|
259
|
+
s += `/${memory}g`;
|
|
260
|
+
if (rtt && rtt !== '0')
|
|
261
|
+
s += ` ${rtt}ms`;
|
|
262
|
+
}
|
|
263
|
+
return s;
|
|
264
|
+
}
|
|
227
265
|
async proxy(ctx, url, headers_ = {}) {
|
|
228
266
|
const { request: { method, headers, query, body } } = ctx;
|
|
229
267
|
let { response } = ctx;
|
|
@@ -277,7 +315,7 @@ export class Server {
|
|
|
277
315
|
if (method !== 'HEAD' && method !== 'GET')
|
|
278
316
|
return false;
|
|
279
317
|
function _log_404() {
|
|
280
|
-
let s = `${' '.repeat(
|
|
318
|
+
let s = `${' '.repeat(11)} ${method.toLowerCase()} 404: ${path}`;
|
|
281
319
|
if (_path !== path)
|
|
282
320
|
s += ` ${_path.bracket()}`;
|
|
283
321
|
console.log(s.red);
|
package/utils.browser.d.ts
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
export declare const noop: () => void;
|
|
2
2
|
export declare function assert(assertion: any, message?: string): never | void;
|
|
3
|
+
/** 生成 0, 1, ..., n - 1 (不包括 n) 的数组,支持传入 generator 函数,通过 index 生成各个元素
|
|
4
|
+
@example seq(10, i => `item-${i}`) */
|
|
5
|
+
export declare function seq<T = number>(n: number, generator?: (index: number) => T): T[];
|
|
3
6
|
export declare function delay(milliseconds: number): Promise<void>;
|
|
4
7
|
export declare function timeout(milliseconds: number): Promise<void>;
|
|
5
8
|
/** https://stackoverflow.com/questions/63297164/how-to-only-accept-arraybuffer-as-parameter */
|
package/utils.browser.js
CHANGED
|
@@ -6,6 +6,14 @@ export function assert(assertion, message) {
|
|
|
6
6
|
throw Object.assign(new Error(`断言失败: ${message ? `${message}: ` : ''}${assertion}`), { assertion });
|
|
7
7
|
}
|
|
8
8
|
}
|
|
9
|
+
/** 生成 0, 1, ..., n - 1 (不包括 n) 的数组,支持传入 generator 函数,通过 index 生成各个元素
|
|
10
|
+
@example seq(10, i => `item-${i}`) */
|
|
11
|
+
export function seq(n, generator) {
|
|
12
|
+
let a = new Array(n);
|
|
13
|
+
for (let i = 0; i < n; i++)
|
|
14
|
+
a[i] = generator ? generator(i) : i;
|
|
15
|
+
return a;
|
|
16
|
+
}
|
|
9
17
|
export async function delay(milliseconds) {
|
|
10
18
|
return new Promise(resolve => {
|
|
11
19
|
setTimeout(resolve, milliseconds);
|
package/utils.d.ts
CHANGED
|
@@ -10,6 +10,9 @@ export declare const noop: () => void;
|
|
|
10
10
|
export declare const output_width = 230;
|
|
11
11
|
export declare function set_inspect_options(colors?: boolean): void;
|
|
12
12
|
export declare function assert(assertion: any, message?: string): never | void;
|
|
13
|
+
/** 生成 0, 1, ..., n - 1 (不包括 n) 的数组,支持传入 generator 函数,通过 index 生成各个元素
|
|
14
|
+
@example seq(10, i => `item-${i}`) */
|
|
15
|
+
export declare function seq<T = number>(n: number, generator?: (index: number) => T): T[];
|
|
13
16
|
export declare function dedent(templ: TemplateStringsArray | string, ...values: any[]): string;
|
|
14
17
|
/** 数组或 iterable 去重(可按 selector 去重)
|
|
15
18
|
- selector?: 可以是 key (string) 或 (obj: any) => any
|
package/utils.js
CHANGED
|
@@ -27,6 +27,14 @@ export function assert(assertion, message) {
|
|
|
27
27
|
throw Object.assign(new Error(`断言失败: ${message ? `${message}: ` : ''}${inspect(assertion, { colors: false, compact: true })}`), { assertion });
|
|
28
28
|
}
|
|
29
29
|
}
|
|
30
|
+
/** 生成 0, 1, ..., n - 1 (不包括 n) 的数组,支持传入 generator 函数,通过 index 生成各个元素
|
|
31
|
+
@example seq(10, i => `item-${i}`) */
|
|
32
|
+
export function seq(n, generator) {
|
|
33
|
+
let a = new Array(n);
|
|
34
|
+
for (let i = 0; i < n; i++)
|
|
35
|
+
a[i] = generator ? generator(i) : i;
|
|
36
|
+
return a;
|
|
37
|
+
}
|
|
30
38
|
export function dedent(templ, ...values) {
|
|
31
39
|
let strings = Array.from(typeof templ === 'string' ? [templ] : templ.raw);
|
|
32
40
|
// 1. remove trailing whitespace
|