t880216t-server 1.5.20
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/LICENSE +124 -0
- package/README.md +191 -0
- package/bin/nohost.js +178 -0
- package/bin/plugin.js +70 -0
- package/bin/util.js +93 -0
- package/index.js +89 -0
- package/lib/config.js +42 -0
- package/lib/index.js +205 -0
- package/lib/main/cgi/getSettings.js +11 -0
- package/lib/main/cgi/getVersion.js +12 -0
- package/lib/main/cgi/login.js +14 -0
- package/lib/main/cgi/restart.js +5 -0
- package/lib/main/cgi/setAdmin.js +6 -0
- package/lib/main/cgi/setDomain.js +13 -0
- package/lib/main/cgi/status.js +122 -0
- package/lib/main/index.js +94 -0
- package/lib/main/router.js +79 -0
- package/lib/main/storage.js +132 -0
- package/lib/main/util.js +138 -0
- package/lib/main/whistleMgr.js +56 -0
- package/lib/main/worker.js +52 -0
- package/lib/main/workerNum.js +11 -0
- package/lib/plugins/account/whistle.share/menu.html +171 -0
- package/lib/plugins/account/whistle.share/package.json +20 -0
- package/lib/plugins/account/whistle.storage/index.js +43 -0
- package/lib/plugins/account/whistle.storage/package.json +8 -0
- package/lib/plugins/account/whistle.storage/rules.txt +1 -0
- package/lib/plugins/whistle.nohost/_rules.txt +38 -0
- package/lib/plugins/whistle.nohost/index.js +3 -0
- package/lib/plugins/whistle.nohost/initial.js +9 -0
- package/lib/plugins/whistle.nohost/lib/accountMgr.js +750 -0
- package/lib/plugins/whistle.nohost/lib/envMgr.js +114 -0
- package/lib/plugins/whistle.nohost/lib/rulesServer.js +85 -0
- package/lib/plugins/whistle.nohost/lib/tunnelRulesServer.js +71 -0
- package/lib/plugins/whistle.nohost/lib/uiServer/cgi/account/changePassword.js +8 -0
- package/lib/plugins/whistle.nohost/lib/uiServer/cgi/admin/activeAccount.js +9 -0
- package/lib/plugins/whistle.nohost/lib/uiServer/cgi/admin/addAccount.js +8 -0
- package/lib/plugins/whistle.nohost/lib/uiServer/cgi/admin/allAccounts.js +9 -0
- package/lib/plugins/whistle.nohost/lib/uiServer/cgi/admin/changeNotice.js +8 -0
- package/lib/plugins/whistle.nohost/lib/uiServer/cgi/admin/changePassword.js +8 -0
- package/lib/plugins/whistle.nohost/lib/uiServer/cgi/admin/enableGuest.js +5 -0
- package/lib/plugins/whistle.nohost/lib/uiServer/cgi/admin/getAuthKey.js +3 -0
- package/lib/plugins/whistle.nohost/lib/uiServer/cgi/admin/getSettings.js +22 -0
- package/lib/plugins/whistle.nohost/lib/uiServer/cgi/admin/login.js +14 -0
- package/lib/plugins/whistle.nohost/lib/uiServer/cgi/admin/move.js +9 -0
- package/lib/plugins/whistle.nohost/lib/uiServer/cgi/admin/removeAccount.js +9 -0
- package/lib/plugins/whistle.nohost/lib/uiServer/cgi/admin/setAccountRules.js +5 -0
- package/lib/plugins/whistle.nohost/lib/uiServer/cgi/admin/setAuthKey.js +5 -0
- package/lib/plugins/whistle.nohost/lib/uiServer/cgi/admin/setDefaultRules.js +5 -0
- package/lib/plugins/whistle.nohost/lib/uiServer/cgi/admin/setEntryPatterns.js +6 -0
- package/lib/plugins/whistle.nohost/lib/uiServer/cgi/admin/setJsonData.js +5 -0
- package/lib/plugins/whistle.nohost/lib/uiServer/cgi/admin/setRulesTpl.js +5 -0
- package/lib/plugins/whistle.nohost/lib/uiServer/cgi/admin/setTestRules.js +5 -0
- package/lib/plugins/whistle.nohost/lib/uiServer/cgi/admin/specPattern.js +4 -0
- package/lib/plugins/whistle.nohost/lib/uiServer/cgi/allowlist.js +4 -0
- package/lib/plugins/whistle.nohost/lib/uiServer/cgi/entryRules.js +4 -0
- package/lib/plugins/whistle.nohost/lib/uiServer/cgi/follow.js +17 -0
- package/lib/plugins/whistle.nohost/lib/uiServer/cgi/getEnv.js +12 -0
- package/lib/plugins/whistle.nohost/lib/uiServer/cgi/list.js +43 -0
- package/lib/plugins/whistle.nohost/lib/uiServer/cgi/patterns.js +14 -0
- package/lib/plugins/whistle.nohost/lib/uiServer/cgi/pluginRules.js +61 -0
- package/lib/plugins/whistle.nohost/lib/uiServer/cgi/proxy.js +42 -0
- package/lib/plugins/whistle.nohost/lib/uiServer/cgi/proxyNetwork.js +12 -0
- package/lib/plugins/whistle.nohost/lib/uiServer/cgi/redirect.js +10 -0
- package/lib/plugins/whistle.nohost/lib/uiServer/cgi/selectEnv.js +30 -0
- package/lib/plugins/whistle.nohost/lib/uiServer/cgi/unfollow.js +4 -0
- package/lib/plugins/whistle.nohost/lib/uiServer/index.js +60 -0
- package/lib/plugins/whistle.nohost/lib/uiServer/network.js +38 -0
- package/lib/plugins/whistle.nohost/lib/uiServer/openAPI.js +94 -0
- package/lib/plugins/whistle.nohost/lib/uiServer/router.js +85 -0
- package/lib/plugins/whistle.nohost/lib/util.js +230 -0
- package/lib/plugins/whistle.nohost/lib/whistle.js +149 -0
- package/lib/plugins/whistle.nohost/lib/whistleMgr.js +94 -0
- package/lib/plugins/whistle.nohost/package.json +7 -0
- package/lib/plugins/whistle.nohost/rules.txt +3 -0
- package/lib/service/cgi/export.js +76 -0
- package/lib/service/cgi/import.js +33 -0
- package/lib/service/cgi/util.js +35 -0
- package/lib/service/index.js +37 -0
- package/lib/service/router.js +7 -0
- package/lib/service/server.js +25 -0
- package/lib/util/address.js +55 -0
- package/lib/util/getPort.js +19 -0
- package/lib/util/login.js +55 -0
- package/lib/util/parseDomain.js +17 -0
- package/lib/whistle.js +86 -0
- package/package.json +135 -0
- package/pnpm-workspace.yaml +6 -0
- package/public/admin.75d42731d4aa7f7a558d3abf2a375fcb.js +2 -0
- package/public/admin.75d42731d4aa7f7a558d3abf2a375fcb.js.LICENSE.txt +76 -0
- package/public/admin.html +11 -0
- package/public/button.js +2 -0
- package/public/button.js.LICENSE.txt +31 -0
- package/public/capture.7bab900f27c9bb1b0e33523e994554ae.js +2 -0
- package/public/capture.7bab900f27c9bb1b0e33523e994554ae.js.LICENSE.txt +83 -0
- package/public/capture.html +11 -0
- package/public/eed368d0656f03932671530c3132ed61.svg +1 -0
- package/public/favicon.ico +0 -0
- package/public/network.e4814ec8c966a8a789296679619ec573.js +2 -0
- package/public/network.e4814ec8c966a8a789296679619ec573.js.LICENSE.txt +76 -0
- package/public/network.html +11 -0
- package/public/select.cb638be6656d02a558f2690acd59901a.js +2 -0
- package/public/select.cb638be6656d02a558f2690acd59901a.js.LICENSE.txt +24 -0
- package/public/select.html +26 -0
package/index.js
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tencent is pleased to support the open source community by making nohost-环境配置与抓包调试平台 available.
|
|
3
|
+
* Copyright (C) 2020 THL A29 Limited, a Tencent company. All rights reserved. The below software in
|
|
4
|
+
* this distribution may have been modified by THL A29 Limited ("Tencent Modifications").
|
|
5
|
+
* All Tencent Modifications are Copyright (C) THL A29 Limited.
|
|
6
|
+
* nohost-环境配置与抓包调试平台 is licensed under the MIT License except for the third-party components listed below.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
// 避免第三方模块没处理好异常导致程序crash
|
|
10
|
+
require('whistle/lib/util/patch');
|
|
11
|
+
const fse = require('fs-extra');
|
|
12
|
+
const fs = require('fs');
|
|
13
|
+
const path = require('path');
|
|
14
|
+
const { getWhistlePath } = require('whistle/lib/config');
|
|
15
|
+
const pkg = require('./package.json');
|
|
16
|
+
const initConfig = require('./lib/config');
|
|
17
|
+
|
|
18
|
+
const PURE_URL_RE = /^(#?(?:https?:)?\/\/[\w.-]+[^?#]*)/;
|
|
19
|
+
|
|
20
|
+
// 设置存储路径
|
|
21
|
+
const WHISTLE_PATH = process.env.NOHOST_PATH || getWhistlePath();
|
|
22
|
+
process.env.WHISTLE_PATH = WHISTLE_PATH;
|
|
23
|
+
fse.ensureDirSync(process.env.WHISTLE_PATH); // eslint-disable-line
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
const getPureUrl = (url) => {
|
|
27
|
+
if (!url || !PURE_URL_RE.test(url)) {
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
return RegExp.$1.replace(/\/+$/, '');
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const getErrorStack = (err) => {
|
|
34
|
+
if (!err) {
|
|
35
|
+
return '';
|
|
36
|
+
}
|
|
37
|
+
let stack;
|
|
38
|
+
try {
|
|
39
|
+
stack = err.stack;
|
|
40
|
+
} catch (e) {}
|
|
41
|
+
stack = stack || err.message || err;
|
|
42
|
+
const result = [
|
|
43
|
+
`From: nohost@${pkg.version}`,
|
|
44
|
+
`Node: ${process.version}`,
|
|
45
|
+
`Date: ${new Date().toLocaleString()}`,
|
|
46
|
+
stack];
|
|
47
|
+
return result.join('\r\n');
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const handleUncaughtException = (err) => {
|
|
51
|
+
if (!err || err.code !== 'ERR_IPC_CHANNEL_CLOSED') {
|
|
52
|
+
const stack = getErrorStack(err);
|
|
53
|
+
fs.writeFileSync(path.join(WHISTLE_PATH, 'nohost.log'), `\r\n${stack}\r\n`, { flag: 'a' }); // eslint-disable-line
|
|
54
|
+
console.error(stack); // eslint-disable-line
|
|
55
|
+
}
|
|
56
|
+
process.exit(1);
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
process.on('unhandledRejection', handleUncaughtException);
|
|
60
|
+
process.on('uncaughtException', handleUncaughtException);
|
|
61
|
+
|
|
62
|
+
module.exports = (options, cb) => {
|
|
63
|
+
if (typeof options === 'function') {
|
|
64
|
+
cb = options;
|
|
65
|
+
options = {};
|
|
66
|
+
} else if (!options) {
|
|
67
|
+
options = {};
|
|
68
|
+
}
|
|
69
|
+
if (options.__maxHttpHeaderSize > 0) {
|
|
70
|
+
process.env.PFORK_MAX_HTTP_HEADER_SIZE = options.__maxHttpHeaderSize;
|
|
71
|
+
}
|
|
72
|
+
if (options.debugMode) {
|
|
73
|
+
const mode = typeof options.mode === 'string' ? options.mode.trim().split(/\s*[|,&]\s*/) : [];
|
|
74
|
+
if (mode.includes('prod') || mode.includes('production')) {
|
|
75
|
+
options.debugMode = false;
|
|
76
|
+
} else {
|
|
77
|
+
process.env.PFORK_MODE = 'bind';
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
const redirect = getPureUrl(options.redirect);
|
|
81
|
+
if (redirect && redirect[0] === '#') {
|
|
82
|
+
options.redirect = redirect.substring(1);
|
|
83
|
+
options.deprecated = true;
|
|
84
|
+
} else {
|
|
85
|
+
options.redirect = redirect;
|
|
86
|
+
}
|
|
87
|
+
initConfig(options);
|
|
88
|
+
require('./lib')(options, cb); // eslint-disable-line
|
|
89
|
+
};
|
package/lib/config.js
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
const { shasum } = require('./util/login');
|
|
2
|
+
|
|
3
|
+
const getPaths = (paths) => {
|
|
4
|
+
if (typeof paths === 'string') {
|
|
5
|
+
paths = paths.trim().split(/\s*[|,;]\s*/);
|
|
6
|
+
} else if (!Array.isArray(paths)) {
|
|
7
|
+
return;
|
|
8
|
+
}
|
|
9
|
+
paths = paths.filter((path) => {
|
|
10
|
+
return path && typeof path === 'string';
|
|
11
|
+
});
|
|
12
|
+
return paths.length ? paths : undefined;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
const getDefaultKey = () => {
|
|
16
|
+
return shasum(`${Math.random()}\n${Date.now()}\n${Math.random()}`);
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
module.exports = (options) => {
|
|
20
|
+
options.totalReqs = 0;
|
|
21
|
+
options.uiReqs = 0;
|
|
22
|
+
options.upgradeReqs = 0;
|
|
23
|
+
options.tunnelReqs = 0;
|
|
24
|
+
options.totalQps = 0;
|
|
25
|
+
options.uiQps = 0;
|
|
26
|
+
options.globalPluginPath = getPaths(options.globalPluginPath);
|
|
27
|
+
options.accountPluginPath = getPaths(options.accountPluginPath || options.workerPluginPath);
|
|
28
|
+
options.mainAuthKey = getDefaultKey();
|
|
29
|
+
let totalReqs = 0;
|
|
30
|
+
let uiReqs = 0;
|
|
31
|
+
let now = Date.now();
|
|
32
|
+
setInterval(() => {
|
|
33
|
+
const cur = Date.now();
|
|
34
|
+
const cost = cur - now || 1;
|
|
35
|
+
options.totalQps = ((options.totalReqs - totalReqs) * 1000 / cost).toFixed(2);
|
|
36
|
+
options.uiQps = ((options.uiReqs - uiReqs) * 1000 / cost).toFixed(2);
|
|
37
|
+
totalReqs = options.totalReqs;
|
|
38
|
+
uiReqs = options.uiReqs;
|
|
39
|
+
now = cur;
|
|
40
|
+
}, 1000);
|
|
41
|
+
module.exports = options;
|
|
42
|
+
};
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
const assert = require('assert');
|
|
2
|
+
const { createServer } = require('http');
|
|
3
|
+
const { onClose } = require('@nohost/router');
|
|
4
|
+
const handleUIRequest = require('./main');
|
|
5
|
+
const { passToWhistle } = require('./main/util');
|
|
6
|
+
const { isUIRequest, setAdmin, checkDomain } = require('./main/storage');
|
|
7
|
+
|
|
8
|
+
const NOHOST_ENV = 'x-whistle-nohost-env';
|
|
9
|
+
const NOHOST_RULE = 'x-whistle-nohost-rule';
|
|
10
|
+
const NOHOST_VALUE = 'x-whistle-nohost-value';
|
|
11
|
+
const WHISTLE_RULE = 'x-whistle-rule-value';
|
|
12
|
+
const WHISTLE_VALUE = 'x-whistle-key-value';
|
|
13
|
+
const WORKER_RE = /^\$(\d+)?$/;
|
|
14
|
+
const DEFAULT_PORT = 8080;
|
|
15
|
+
const LOCALHOST = '127.0.0.1';
|
|
16
|
+
const HOST_LIST_RE = /^(?:([\w.-]+):)?([1-9]\d{0,4}(?:[-~][1-9]\d{0,4})?(?:,[1-9]\d{0,4}(?:[-~][1-9]\d{0,4})?)*)$/;
|
|
17
|
+
const HTTP_RE = /^\w+\s+\S+\s+HTTP\/1.\d$/mi;
|
|
18
|
+
const SPEC_PATH_RE = /^(?:https?:\/\/[^/?]+)?(\/[_-]\/)/;
|
|
19
|
+
const resolveHost = (host) => {
|
|
20
|
+
if (/^(?:([\w.-]+):)?([1-9]\d{0,4})$/.test(host) || /^\[([\w.:]+)\]:([1-9]\d{0,4})$/.test(host)) {
|
|
21
|
+
return {
|
|
22
|
+
host: RegExp.$1,
|
|
23
|
+
port: parseInt(RegExp.$2, 10),
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
return {};
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const resolveHostList = (str) => {
|
|
30
|
+
if (!str || typeof str !== 'string') {
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
str = str.split(/\||;/);
|
|
34
|
+
let list;
|
|
35
|
+
str.forEach((host) => {
|
|
36
|
+
if (!HOST_LIST_RE.test(host)) {
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
host = RegExp.$1 || LOCALHOST;
|
|
40
|
+
RegExp.$2.split(',').forEach((port) => {
|
|
41
|
+
if (/^([1-9]\d{0,4})[-~]([1-9]\d{0,4})$/.test(port)) {
|
|
42
|
+
const p1 = parseInt(RegExp.$1, 10);
|
|
43
|
+
const p2 = parseInt(RegExp.$2, 10);
|
|
44
|
+
const max = Math.min(128, p2 - p1 + 1);
|
|
45
|
+
for (let i = 0; i < max; ++i) {
|
|
46
|
+
list = list || [];
|
|
47
|
+
list.push({ host, port: p1 + i });
|
|
48
|
+
}
|
|
49
|
+
} else {
|
|
50
|
+
list = list || [];
|
|
51
|
+
list.push({ host, port: parseInt(port, 10) });
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
return list;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const sendEstablished = (socket, err) => {
|
|
59
|
+
const msg = err ? 'Bad Gateway' : 'Connection Established';
|
|
60
|
+
const body = String((err && err.stack) || '');
|
|
61
|
+
socket.write([
|
|
62
|
+
`HTTP/1.1 ${err ? 502 : 200} ${msg}`,
|
|
63
|
+
`Content-Length: ${Buffer.byteLength(body)}`,
|
|
64
|
+
'Proxy-Agent: nohost/server',
|
|
65
|
+
'\r\n',
|
|
66
|
+
].join('\r\n'));
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
const CLIENT_ID_HEAD = 'x-whistle-client-id';
|
|
71
|
+
const CLIENT_PORT_HEAD = 'x-whistle-client-port';
|
|
72
|
+
const CLIENT_IP_HEAD = 'x-forwarded-for';
|
|
73
|
+
const PROXY_AUTH_HEAD = 'proxy-authorization';
|
|
74
|
+
const ID_HEAD = `x-whistle-.-${Math.random()}`;
|
|
75
|
+
|
|
76
|
+
const restoreClientInfo = function(req) {
|
|
77
|
+
const { headers, socket } = req;
|
|
78
|
+
let { clientInfo } = socket;
|
|
79
|
+
if (!clientInfo) {
|
|
80
|
+
clientInfo = headers[ID_HEAD];
|
|
81
|
+
if (clientInfo) {
|
|
82
|
+
delete headers[ID_HEAD];
|
|
83
|
+
try {
|
|
84
|
+
clientInfo = clientInfo && JSON.parse(decodeURIComponent(clientInfo));
|
|
85
|
+
req.socket.clientInfo = clientInfo;
|
|
86
|
+
} catch (e) {
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
if (clientInfo) {
|
|
92
|
+
const {
|
|
93
|
+
clientIp,
|
|
94
|
+
clientPort,
|
|
95
|
+
clientId,
|
|
96
|
+
auth,
|
|
97
|
+
} = clientInfo;
|
|
98
|
+
if (clientIp) {
|
|
99
|
+
headers[CLIENT_IP_HEAD] = clientIp;
|
|
100
|
+
}
|
|
101
|
+
if (clientPort) {
|
|
102
|
+
headers[CLIENT_PORT_HEAD] = clientPort;
|
|
103
|
+
}
|
|
104
|
+
if (clientId) {
|
|
105
|
+
headers[CLIENT_ID_HEAD] = clientId;
|
|
106
|
+
}
|
|
107
|
+
if (auth) {
|
|
108
|
+
headers[PROXY_AUTH_HEAD] = auth;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
const addClientInfo = (socket, chunk, statusLine) => {
|
|
114
|
+
chunk = chunk.slice(Buffer.byteLength(statusLine));
|
|
115
|
+
const { remoteAddress, remotePort, headers } = socket;
|
|
116
|
+
const auth = headers[PROXY_AUTH_HEAD];
|
|
117
|
+
const clientIp = headers[CLIENT_IP_HEAD] || remoteAddress;
|
|
118
|
+
const clientPort = headers[CLIENT_PORT_HEAD] || remotePort;
|
|
119
|
+
const clientId = headers[CLIENT_ID_HEAD];
|
|
120
|
+
const clientInfo = JSON.stringify({ auth, clientIp, clientPort, clientId });
|
|
121
|
+
statusLine += `\r\n${ID_HEAD}: ${encodeURIComponent(clientInfo)}`;
|
|
122
|
+
socket.emit('data', Buffer.concat([Buffer.from(statusLine), chunk]));
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
module.exports = (options, cb) => {
|
|
127
|
+
const { username, password } = options;
|
|
128
|
+
if (username || password) {
|
|
129
|
+
if (username !== '+') {
|
|
130
|
+
assert(username && typeof username === 'string', 'username is required.');
|
|
131
|
+
assert(/^[\w.-]{1,32}$/.test(username), 'username is incorrect (/^[\\w.-]{1,32}$/).');
|
|
132
|
+
assert(password && typeof password === 'string', 'password is required.');
|
|
133
|
+
setAdmin({ username, password });
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
const { host, port } = resolveHost(options.port);
|
|
137
|
+
options.host = host;
|
|
138
|
+
options.port = port || DEFAULT_PORT;
|
|
139
|
+
options.storage = resolveHostList(options.storage);
|
|
140
|
+
options.portStr = `${options.port}`;
|
|
141
|
+
const { nohostDomain } = options;
|
|
142
|
+
delete options.nohostDomain;
|
|
143
|
+
options.domain = checkDomain(nohostDomain) ? nohostDomain.toLowerCase() : '';
|
|
144
|
+
|
|
145
|
+
const handleRequest = (req, res) => {
|
|
146
|
+
const { headers } = req;
|
|
147
|
+
if (WORKER_RE.test(headers[NOHOST_ENV])) {
|
|
148
|
+
if (headers[NOHOST_RULE]) {
|
|
149
|
+
headers[WHISTLE_RULE] = headers[NOHOST_RULE];
|
|
150
|
+
}
|
|
151
|
+
if (headers[NOHOST_VALUE]) {
|
|
152
|
+
headers[WHISTLE_VALUE] = headers[NOHOST_VALUE];
|
|
153
|
+
}
|
|
154
|
+
passToWhistle(req, res, RegExp.$1);
|
|
155
|
+
} else {
|
|
156
|
+
passToWhistle(req, res);
|
|
157
|
+
}
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
const server = createServer((req, res) => {
|
|
161
|
+
onClose(res);
|
|
162
|
+
++options.totalReqs;
|
|
163
|
+
if (!SPEC_PATH_RE.test(req.url) && isUIRequest(req)) {
|
|
164
|
+
++options.uiReqs;
|
|
165
|
+
restoreClientInfo(req);
|
|
166
|
+
const { headers } = req;
|
|
167
|
+
delete headers.referer;
|
|
168
|
+
// 强制替换域名
|
|
169
|
+
headers['x-whistle-real-host'] = 'local.whistlejs.com';
|
|
170
|
+
headers.host = 'local.whistlejs.com';
|
|
171
|
+
handleUIRequest(req, res);
|
|
172
|
+
} else {
|
|
173
|
+
handleRequest(req, res);
|
|
174
|
+
}
|
|
175
|
+
});
|
|
176
|
+
server.timeout = 360000;
|
|
177
|
+
server.on('upgrade', (req, socket) => {
|
|
178
|
+
onClose(socket);
|
|
179
|
+
++options.upgradeReqs;
|
|
180
|
+
++options.totalReqs;
|
|
181
|
+
handleRequest(req, socket);
|
|
182
|
+
});
|
|
183
|
+
server.on('connect', async (req, socket) => {
|
|
184
|
+
onClose(socket);
|
|
185
|
+
++options.tunnelReqs;
|
|
186
|
+
++options.totalReqs;
|
|
187
|
+
if (isUIRequest(req, true)) {
|
|
188
|
+
sendEstablished(socket);
|
|
189
|
+
socket.headers = req.headers;
|
|
190
|
+
socket.once('data', (chunk) => {
|
|
191
|
+
if (HTTP_RE.test(`${chunk}`)) {
|
|
192
|
+
const statusLine = RegExp['$&'];
|
|
193
|
+
server.emit('connection', socket);
|
|
194
|
+
addClientInfo(socket, chunk, statusLine);
|
|
195
|
+
} else {
|
|
196
|
+
socket.destroy();
|
|
197
|
+
}
|
|
198
|
+
});
|
|
199
|
+
} else {
|
|
200
|
+
handleRequest(req, socket);
|
|
201
|
+
}
|
|
202
|
+
});
|
|
203
|
+
server.listen(options.port, host, cb);
|
|
204
|
+
return server;
|
|
205
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
const { checkLogin } = require('../../util/login');
|
|
2
|
+
|
|
3
|
+
module.exports = async (ctx, next) => {
|
|
4
|
+
const { username, password } = ctx.storage.getAdmin();
|
|
5
|
+
const accept = checkLogin(ctx, {
|
|
6
|
+
username,
|
|
7
|
+
password,
|
|
8
|
+
nameKey: 'nohost_admin_name',
|
|
9
|
+
authKey: 'nohost_admin_key',
|
|
10
|
+
});
|
|
11
|
+
if (accept) {
|
|
12
|
+
await next();
|
|
13
|
+
}
|
|
14
|
+
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
|
|
2
|
+
module.exports = async (ctx) => {
|
|
3
|
+
const { domain } = ctx.request.body;
|
|
4
|
+
const { checkDomain, getDomain } = ctx.storage;
|
|
5
|
+
if (checkDomain(domain)) {
|
|
6
|
+
const curDomain = getDomain();
|
|
7
|
+
ctx.storage.setDomain(domain);
|
|
8
|
+
if (curDomain !== domain) {
|
|
9
|
+
ctx.whistleMgr.restart();
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
ctx.body = { ec: 0 };
|
|
13
|
+
};
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
const WORKER_NUM = require('../workerNum');
|
|
2
|
+
const config = require('../../config');
|
|
3
|
+
|
|
4
|
+
const STATUS = { workerNum: WORKER_NUM };
|
|
5
|
+
let allWorkers = [];
|
|
6
|
+
let envWorkerMap = {};
|
|
7
|
+
let curWorkerInfo;
|
|
8
|
+
const MAX_CACHE_TIME = 1000 * 60 * 8;
|
|
9
|
+
const WORKER_INFO_RE = /^\d{1,3}(\.\d{1,3}){3}:\d{1,5}\/[1-9]\d*(,\d{1,3}(\.\d{1,3}){3}:\d{1,5}\/[1-9]\d*)*$/;
|
|
10
|
+
|
|
11
|
+
const parseWorker = (item) => {
|
|
12
|
+
item = item.split('/');
|
|
13
|
+
const index = parseInt(item[1], 10);
|
|
14
|
+
item = item[0].split(':');
|
|
15
|
+
return {
|
|
16
|
+
index,
|
|
17
|
+
envNum: 0,
|
|
18
|
+
host: item[0],
|
|
19
|
+
port: parseInt(item[1], 10),
|
|
20
|
+
};
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
setInterval(() => {
|
|
24
|
+
const now = Date.now();
|
|
25
|
+
let isChanged;
|
|
26
|
+
Object.keys(envWorkerMap).forEach((env) => {
|
|
27
|
+
const { updateTime, worker } = envWorkerMap[env];
|
|
28
|
+
if (now - updateTime > MAX_CACHE_TIME) {
|
|
29
|
+
delete envWorkerMap[env];
|
|
30
|
+
--worker.envNum;
|
|
31
|
+
isChanged = true;
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
if (isChanged) {
|
|
35
|
+
allWorkers.sort((w1, w2) => {
|
|
36
|
+
return w1.envNum > w2.envNum ? 1 : -1;
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
}, 30000);
|
|
40
|
+
|
|
41
|
+
const flattenWorkers = (workerList) => {
|
|
42
|
+
const result = [];
|
|
43
|
+
let len = workerList.length;
|
|
44
|
+
while (workerList.length) {
|
|
45
|
+
for (let i = len - 1; i >= 0; i--) {
|
|
46
|
+
const worker = workerList[i];
|
|
47
|
+
--worker.index;
|
|
48
|
+
if (!worker.index) {
|
|
49
|
+
workerList.splice(i, 1);
|
|
50
|
+
}
|
|
51
|
+
result.push(Object.assign({}, worker, STATUS));
|
|
52
|
+
}
|
|
53
|
+
len = workerList.length;
|
|
54
|
+
}
|
|
55
|
+
return result;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const update = (options) => {
|
|
59
|
+
if (!options || curWorkerInfo === options) {
|
|
60
|
+
return allWorkers;
|
|
61
|
+
}
|
|
62
|
+
let workerList;
|
|
63
|
+
try {
|
|
64
|
+
workerList = Buffer.from(options, 'base64').toString();
|
|
65
|
+
if (!WORKER_INFO_RE.test(workerList)) {
|
|
66
|
+
return allWorkers;
|
|
67
|
+
}
|
|
68
|
+
} catch (e) {
|
|
69
|
+
return allWorkers;
|
|
70
|
+
}
|
|
71
|
+
curWorkerInfo = options;
|
|
72
|
+
workerList = workerList.split(',').map(parseWorker);
|
|
73
|
+
allWorkers = flattenWorkers(workerList);
|
|
74
|
+
envWorkerMap = {};
|
|
75
|
+
return allWorkers;
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
const notEmptyStr = (str) => {
|
|
79
|
+
return str && typeof str === 'string';
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
const getWorker = ({ space, group, env }, servers) => {
|
|
83
|
+
if (!notEmptyStr(space) || !notEmptyStr(group) || !update(servers).length) {
|
|
84
|
+
return STATUS;
|
|
85
|
+
}
|
|
86
|
+
env = [space, group, env || ''].join('/');
|
|
87
|
+
const info = envWorkerMap[env];
|
|
88
|
+
if (info) {
|
|
89
|
+
info.updateTime = Date.now();
|
|
90
|
+
return info.worker;
|
|
91
|
+
}
|
|
92
|
+
const worker = allWorkers[0];
|
|
93
|
+
++worker.envNum;
|
|
94
|
+
envWorkerMap[env] = {
|
|
95
|
+
updateTime: Date.now(),
|
|
96
|
+
worker,
|
|
97
|
+
};
|
|
98
|
+
const index = allWorkers.indexOf(worker);
|
|
99
|
+
for (let i = allWorkers.length - 1; i > index; i--) {
|
|
100
|
+
const next = allWorkers[i];
|
|
101
|
+
if (!next || worker.envNum > next.envNum) {
|
|
102
|
+
allWorkers[i] = worker;
|
|
103
|
+
allWorkers[index] = next;
|
|
104
|
+
return worker;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return worker;
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
module.exports = (ctx) => {
|
|
112
|
+
const servers = ctx.get('x-nohost-servers');
|
|
113
|
+
const { query } = ctx.request;
|
|
114
|
+
const status = Object.assign({}, getWorker(query, servers));
|
|
115
|
+
status.totalReqs = config.totalReqs;
|
|
116
|
+
status.uiReqs = config.uiReqs;
|
|
117
|
+
status.upgradeReqs = config.upgradeReqs;
|
|
118
|
+
status.tunnelReqs = config.tunnelReqs;
|
|
119
|
+
status.totalQps = config.totalQps;
|
|
120
|
+
status.uiQps = config.uiQps;
|
|
121
|
+
ctx.body = status;
|
|
122
|
+
};
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
|
|
2
|
+
const Koa = require('koa');
|
|
3
|
+
const onerror = require('koa-onerror');
|
|
4
|
+
const serve = require('koa-static');
|
|
5
|
+
const { join } = require('path');
|
|
6
|
+
const { Z_SYNC_FLUSH } = require('zlib');
|
|
7
|
+
const router = require('koa-router')();
|
|
8
|
+
const compress = require('koa-compress');
|
|
9
|
+
const setupRouter = require('./router');
|
|
10
|
+
const whistleMgr = require('./whistleMgr');
|
|
11
|
+
const storage = require('./storage');
|
|
12
|
+
const { getRedirectUrl } = require('./util');
|
|
13
|
+
const config = require('../config');
|
|
14
|
+
|
|
15
|
+
const MAX_AGE = 1000 * 60 * 5;
|
|
16
|
+
const HEADLESS_RE = /^\/account\/[^/]+\/share\//;
|
|
17
|
+
const EXPORT_RE = /\/export_sessions$/;
|
|
18
|
+
const SPECIAL_PATH = '/nohost/';
|
|
19
|
+
const aliasPages = {
|
|
20
|
+
'/': '/select.html',
|
|
21
|
+
'/index.html': '/select.html',
|
|
22
|
+
'/data.html': '/capture.html',
|
|
23
|
+
'/share.html': '/network.html',
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const startApp = () => {
|
|
27
|
+
const app = new Koa();
|
|
28
|
+
app.proxy = true;
|
|
29
|
+
app.silent = true;
|
|
30
|
+
onerror(app);
|
|
31
|
+
app.use(async (ctx, next) => {
|
|
32
|
+
if (config.deprecated) {
|
|
33
|
+
return ctx.redirect(config.redirect);
|
|
34
|
+
}
|
|
35
|
+
let { path, req } = ctx;
|
|
36
|
+
if (!path.indexOf(SPECIAL_PATH)) {
|
|
37
|
+
req.url = req.url.replace(SPECIAL_PATH, '/');
|
|
38
|
+
path = ctx.path;
|
|
39
|
+
}
|
|
40
|
+
const newPath = aliasPages[path];
|
|
41
|
+
if (newPath) {
|
|
42
|
+
ctx.pageName = path;
|
|
43
|
+
req.url = newPath;
|
|
44
|
+
} else if (HEADLESS_RE.test(path)) {
|
|
45
|
+
req.url = req.url.replace(RegExp['$&'], '/');
|
|
46
|
+
path = ctx.path;
|
|
47
|
+
if (path === '/' || path === '/share.html') {
|
|
48
|
+
req.url = '/network.html';
|
|
49
|
+
ctx.pageName = '/share.html';
|
|
50
|
+
}
|
|
51
|
+
} else if (EXPORT_RE.test(path)) {
|
|
52
|
+
path = '/cgi-bin/sessions/export';
|
|
53
|
+
let query = req.url.indexOf('?');
|
|
54
|
+
query = query === -1 ? '' : req.url.substring(query);
|
|
55
|
+
req.url = path + query;
|
|
56
|
+
} else {
|
|
57
|
+
const index = path.indexOf('/nohost_share/');
|
|
58
|
+
if (index !== -1) {
|
|
59
|
+
const accountName = path.substring(0, index);
|
|
60
|
+
ctx.accountName = accountName.substring(accountName.lastIndexOf('/') + 1);
|
|
61
|
+
req.url = req.url.substring(req.url.indexOf('/nohost_share/') + 13);
|
|
62
|
+
path = ctx.path;
|
|
63
|
+
if (path === '/' || path === '/share.html') {
|
|
64
|
+
req.url = '/network.html';
|
|
65
|
+
ctx.pageName = '/share.html';
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
const redirectUrl = getRedirectUrl(ctx);
|
|
70
|
+
if (redirectUrl) {
|
|
71
|
+
return ctx.redirect(redirectUrl);
|
|
72
|
+
}
|
|
73
|
+
ctx.whistleMgr = whistleMgr;
|
|
74
|
+
ctx.storage = storage;
|
|
75
|
+
await next();
|
|
76
|
+
});
|
|
77
|
+
setupRouter(router);
|
|
78
|
+
app.use(router.routes());
|
|
79
|
+
app.use(router.allowedMethods());
|
|
80
|
+
app.use(compress({
|
|
81
|
+
threshold: 2048,
|
|
82
|
+
gzip: {
|
|
83
|
+
flush: Z_SYNC_FLUSH,
|
|
84
|
+
},
|
|
85
|
+
deflate: {
|
|
86
|
+
flush: Z_SYNC_FLUSH,
|
|
87
|
+
},
|
|
88
|
+
br: false, // disable brotli
|
|
89
|
+
}));
|
|
90
|
+
app.use(serve(join(__dirname, '../../public'), { maxage: MAX_AGE }));
|
|
91
|
+
return app.callback();
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
module.exports = startApp();
|