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
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
const bodyParser = require('koa-bodyparser');
|
|
2
|
+
const { passToWhistle, passToService } = require('./util');
|
|
3
|
+
const login = require('./cgi/login');
|
|
4
|
+
const restart = require('./cgi/restart');
|
|
5
|
+
const getSettings = require('./cgi/getSettings');
|
|
6
|
+
const setAdmin = require('./cgi/setAdmin');
|
|
7
|
+
const setDomain = require('./cgi/setDomain');
|
|
8
|
+
const getVersion = require('./cgi/getVersion');
|
|
9
|
+
const status = require('./cgi/status');
|
|
10
|
+
const config = require('../config');
|
|
11
|
+
|
|
12
|
+
const PUB_KEY_RE = /^[\w.-]+\.pub\.[a-z\d]+$/i;
|
|
13
|
+
|
|
14
|
+
const passDirect = async (ctx) => {
|
|
15
|
+
await passToWhistle(ctx);
|
|
16
|
+
};
|
|
17
|
+
const getPassHandler = (cgiPath) => {
|
|
18
|
+
return async (ctx) => {
|
|
19
|
+
const { req } = ctx;
|
|
20
|
+
req.url = req.url.replace(cgiPath, `/plugin.nohost${cgiPath}`);
|
|
21
|
+
await passToWhistle(ctx);
|
|
22
|
+
};
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const handleValues = (ctx) => {
|
|
26
|
+
if (PUB_KEY_RE.test(ctx.query.name)) {
|
|
27
|
+
ctx.headers['x-whistle-auth-key'] = config.mainAuthKey;
|
|
28
|
+
}
|
|
29
|
+
return passDirect(ctx);
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
module.exports = (router) => {
|
|
33
|
+
router.get('/cgi-bin/get-custom-certs-info', passDirect);
|
|
34
|
+
router.post('/cgi-bin/certs/remove', passDirect);
|
|
35
|
+
router.post('/cgi-bin/certs/upload', passDirect);
|
|
36
|
+
router.get('/cgi-bin/rootca', passDirect);
|
|
37
|
+
router.get('/rules', passDirect);
|
|
38
|
+
router.get('/values', handleValues);
|
|
39
|
+
router.all('/cgi-bin/sessions/export', passToService);
|
|
40
|
+
router.all('/cgi-bin/sessions/import', passToService);
|
|
41
|
+
router.get('/status', status);
|
|
42
|
+
router.all('/cgi-bin/(.*)', getPassHandler('/cgi-bin/'));
|
|
43
|
+
router.all('/network/(.*)', getPassHandler('/network/'));
|
|
44
|
+
router.all(/^\/account\/\$(\d+)\//, async (ctx) => {
|
|
45
|
+
const index = ctx.params[0];
|
|
46
|
+
ctx.url = ctx.url.replace(`/account/$${index}/`, '/');
|
|
47
|
+
await passToWhistle(ctx, null, index);
|
|
48
|
+
});
|
|
49
|
+
router.all('/account/(.*)', getPassHandler('/account/'));
|
|
50
|
+
router.all('/user/:name', (ctx) => {
|
|
51
|
+
const name = ctx.params.name.replace(/\..*$/, '');
|
|
52
|
+
ctx.redirect(`${ctx.path.slice(-1) === '/' ? '../' : ''}../account/${name}/`);
|
|
53
|
+
});
|
|
54
|
+
router.all('/open-api/(.*)', getPassHandler('/open-api/'));
|
|
55
|
+
router.all('/follow', getPassHandler('/follow'));
|
|
56
|
+
router.all('/unfollow', getPassHandler('/unfollow'));
|
|
57
|
+
router.all('/redirect', getPassHandler('/redirect'));
|
|
58
|
+
|
|
59
|
+
router.all('/p/:name/(.*)', async (ctx) => {
|
|
60
|
+
let { name } = ctx.params;
|
|
61
|
+
const segPath = `/p/${name}`;
|
|
62
|
+
name = name.indexOf('.') === -1 ? `/plugin.${name}` : name;
|
|
63
|
+
ctx.req.url = ctx.req.url.replace(segPath, name);
|
|
64
|
+
await passToWhistle(ctx);
|
|
65
|
+
});
|
|
66
|
+
router.all('/p/:name', (ctx) => {
|
|
67
|
+
ctx.redirect(ctx.req.url.replace(/(\?|$)/, '/$1'));
|
|
68
|
+
});
|
|
69
|
+
router.all('/whistle/(.*)', passDirect);
|
|
70
|
+
router.all('/whistle.(.*)', passDirect);
|
|
71
|
+
router.all('/plugin.(.*)', passDirect);
|
|
72
|
+
router.get('/get-version', getVersion);
|
|
73
|
+
router.all('/admin.html', login);
|
|
74
|
+
router.all('/main/cgi-bin/(.*)', login, bodyParser({ formLimit: '1mb' }));
|
|
75
|
+
router.post('/main/cgi-bin/restart', restart);
|
|
76
|
+
router.get('/main/cgi-bin/get-settings', getSettings);
|
|
77
|
+
router.post('/main/cgi-bin/set-admin', setAdmin);
|
|
78
|
+
router.post('/main/cgi-bin/set-domain', setDomain);
|
|
79
|
+
};
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
const Storage = require('whistle/lib/rules/storage');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const net = require('net');
|
|
4
|
+
const { parse } = require('url');
|
|
5
|
+
const https = require('https');
|
|
6
|
+
const { isLocalAddress } = require('../util/address');
|
|
7
|
+
const { shasum } = require('../util/login');
|
|
8
|
+
const config = require('../config');
|
|
9
|
+
const parseDomain = require('../util/parseDomain');
|
|
10
|
+
const { registry } = require('../../package.json');
|
|
11
|
+
|
|
12
|
+
const registryOpts = parse(registry);
|
|
13
|
+
const storage = new Storage(path.join(process.env.WHISTLE_PATH, '.nohost'));
|
|
14
|
+
const HOST_RE = /^https?:\/\/([^/]+)/;
|
|
15
|
+
const TUNNEL_PATH_RE = /^([^/]+)$/;
|
|
16
|
+
const INNER_PATH_RE = /^(?:\w+:\/\/[^/]+)?\/\.nohost-inner-path\.\//;
|
|
17
|
+
const MAX_DOMAIN_LEN = 128;
|
|
18
|
+
const DEFAULT_PASSWORD = shasum('123456');
|
|
19
|
+
const ILLEGAL_DOMAIN_RE = /[^\w.,\s-]/;
|
|
20
|
+
const noop = () => {};
|
|
21
|
+
|
|
22
|
+
const checkDomain = (domain) => {
|
|
23
|
+
return typeof domain === 'string' && !ILLEGAL_DOMAIN_RE.test(domain) && domain.length < MAX_DOMAIN_LEN;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
exports.checkDomain = checkDomain;
|
|
27
|
+
|
|
28
|
+
registryOpts.rejectUnauthorized = false;
|
|
29
|
+
const updateVersion = () => {
|
|
30
|
+
const client = https.get(registryOpts, (res) => {
|
|
31
|
+
res.on('error', noop);
|
|
32
|
+
if (res.statusCode !== 200) {
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
let body;
|
|
36
|
+
res.on('data', (chunk) => {
|
|
37
|
+
body = body ? Buffer.concat([body, chunk]) : chunk;
|
|
38
|
+
});
|
|
39
|
+
res.on('end', () => {
|
|
40
|
+
body = body && `${body}`;
|
|
41
|
+
try {
|
|
42
|
+
const ver = body && JSON.parse(body)['dist-tags'].latest;
|
|
43
|
+
exports.latestVersion = ver;
|
|
44
|
+
storage.setProperty('latestVersion', ver);
|
|
45
|
+
} catch (e) {}
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
client.on('error', noop);
|
|
49
|
+
client.end();
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
exports.latestVersion = storage.getProperty('latestVersion');
|
|
53
|
+
updateVersion();
|
|
54
|
+
setInterval(updateVersion, 1000 * 60 * 30);
|
|
55
|
+
|
|
56
|
+
const initData = () => {
|
|
57
|
+
const domain = storage.getProperty('domain');
|
|
58
|
+
if (!checkDomain(domain)) {
|
|
59
|
+
storage.setProperty('domain', '');
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
initData();
|
|
64
|
+
|
|
65
|
+
const getString = (str) => {
|
|
66
|
+
return typeof str === 'string' ? str : '';
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
const getReqDomain = (req, isConnect) => {
|
|
70
|
+
let host = req.headers['x-whistle-real-host'] || req.headers.host;
|
|
71
|
+
if (isConnect ? TUNNEL_PATH_RE.test(req.url) : (!host && HOST_RE.test(req.url))) {
|
|
72
|
+
host = RegExp.$1;
|
|
73
|
+
}
|
|
74
|
+
return host && typeof host === 'string' ? host.split(':', 2) : '';
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const getDomain = () => {
|
|
78
|
+
return storage.getProperty('domain');
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
const isUIDomain = (domain) => {
|
|
82
|
+
return domain === 'admin.nohost.pro' || parseDomain(`${getDomain()},${config.domain}`).includes(domain);
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
exports.isUIRequest = (req, isConnect) => {
|
|
86
|
+
const domain = getReqDomain(req, isConnect);
|
|
87
|
+
if (!domain) {
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
if (req.headers['x-whistle-nohost-ui'] || isUIDomain(domain[0])) {
|
|
91
|
+
return true;
|
|
92
|
+
}
|
|
93
|
+
if (INNER_PATH_RE.test(req.url)) {
|
|
94
|
+
req.url = req.url.replace('/.nohost-inner-path./', '/');
|
|
95
|
+
return true;
|
|
96
|
+
}
|
|
97
|
+
if (!net.isIP(domain[0]) || config.portStr !== (domain[1] || '80')) {
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
if (isLocalAddress(domain[0])) {
|
|
101
|
+
return true;
|
|
102
|
+
}
|
|
103
|
+
req.headers['x-whistle-nohost-ui'] = 1;
|
|
104
|
+
return false;
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
const getAdmin = () => {
|
|
108
|
+
const admin = storage.getProperty('admin') || '';
|
|
109
|
+
const username = getString(admin.username) || 'admin';
|
|
110
|
+
const password = getString(admin.password) || DEFAULT_PASSWORD;
|
|
111
|
+
return { username, password };
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
exports.setAdmin = (admin) => {
|
|
115
|
+
if (admin) {
|
|
116
|
+
const { username, password } = admin;
|
|
117
|
+
if (getString(username) && /^[\w.-]{1,32}$/.test(username) && getString(password)) {
|
|
118
|
+
const oldAdmin = getAdmin();
|
|
119
|
+
admin = { username, password: shasum(password) };
|
|
120
|
+
storage.setProperty('admin', admin);
|
|
121
|
+
return admin.username !== oldAdmin.username || admin.password !== oldAdmin.password;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
exports.getAdmin = getAdmin;
|
|
127
|
+
|
|
128
|
+
exports.setDomain = (str) => {
|
|
129
|
+
storage.setProperty('domain', str);
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
exports.getDomain = getDomain;
|
package/lib/main/util.js
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
const { proxy, getRawHeaders } = require('@nohost/router');
|
|
2
|
+
const { isIP } = require('net');
|
|
3
|
+
const cpuNum = require('os').cpus().length;
|
|
4
|
+
const {
|
|
5
|
+
getRemoteAddr,
|
|
6
|
+
getRemotePort,
|
|
7
|
+
} = require('whistle/lib/util');
|
|
8
|
+
const { fork } = require('./whistleMgr');
|
|
9
|
+
const startService = require('../service');
|
|
10
|
+
const { fork: forkWorker } = require('./worker');
|
|
11
|
+
const config = require('../config');
|
|
12
|
+
|
|
13
|
+
const LOCALHOST = '127.0.0.1';
|
|
14
|
+
const FROM_RE = /[?&]from=[\w.-]+(?:&|$)/;
|
|
15
|
+
const ERROR_HEADERS = { 'x-server': 'nohost/router' };
|
|
16
|
+
|
|
17
|
+
const sendError = (res, err) => {
|
|
18
|
+
if (res.writeHead) {
|
|
19
|
+
res.writeHead(500, ERROR_HEADERS);
|
|
20
|
+
res.end(err.stack);
|
|
21
|
+
} else {
|
|
22
|
+
res.destroy();
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
const proxyCtx = async (ctx, options) => {
|
|
26
|
+
const svrRes = await proxy(ctx.req, ctx.res, options);
|
|
27
|
+
ctx.status = svrRes.statusCode;
|
|
28
|
+
ctx.set(svrRes.headers);
|
|
29
|
+
ctx.body = svrRes;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
exports.passToWhistle = async (req, res, index) => {
|
|
33
|
+
const ctx = !res && req;
|
|
34
|
+
if (ctx) {
|
|
35
|
+
res = ctx.res;
|
|
36
|
+
req = ctx.req;
|
|
37
|
+
}
|
|
38
|
+
const options = { host: LOCALHOST };
|
|
39
|
+
let remoteAddrHead;
|
|
40
|
+
let remotePortHead;
|
|
41
|
+
const isSingleton = index == null;
|
|
42
|
+
try {
|
|
43
|
+
const result = isSingleton ? await fork() : await forkWorker(index);
|
|
44
|
+
options.port = result.port;
|
|
45
|
+
remoteAddrHead = result.remoteAddrHead;
|
|
46
|
+
remotePortHead = result.remotePortHead;
|
|
47
|
+
} catch (err) {
|
|
48
|
+
if (ctx) {
|
|
49
|
+
throw err;
|
|
50
|
+
}
|
|
51
|
+
return sendError(res, err);
|
|
52
|
+
}
|
|
53
|
+
if (req._hasError) {
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
const { headers } = req;
|
|
57
|
+
const remoteAddress = !isSingleton && headers['x-whistle-remote-address'];
|
|
58
|
+
const remotePort = !isSingleton && headers['x-whistle-remote-port'];
|
|
59
|
+
if (remotePort > 0 && remotePort < 65536 && isIP(remoteAddress)) {
|
|
60
|
+
headers[remoteAddrHead] = remoteAddress;
|
|
61
|
+
headers[remotePortHead] = remotePort;
|
|
62
|
+
delete headers['x-whistle-remote-address'];
|
|
63
|
+
delete headers['x-whistle-remote-port'];
|
|
64
|
+
} else {
|
|
65
|
+
headers[remoteAddrHead] = getRemoteAddr(req);
|
|
66
|
+
headers[remotePortHead] = getRemotePort(req);
|
|
67
|
+
}
|
|
68
|
+
if (ctx) {
|
|
69
|
+
return proxyCtx(ctx, options);
|
|
70
|
+
}
|
|
71
|
+
if (req.upgrade) {
|
|
72
|
+
return proxy(req, res, options);
|
|
73
|
+
}
|
|
74
|
+
try {
|
|
75
|
+
const svrRes = await proxy(req, res, options);
|
|
76
|
+
res.writeHead(svrRes.statusCode, getRawHeaders(svrRes));
|
|
77
|
+
svrRes.pipe(res);
|
|
78
|
+
} catch (err) {
|
|
79
|
+
sendError(res, err);
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
const MAX_REQUESTS = 120;
|
|
84
|
+
const conns = [0];
|
|
85
|
+
|
|
86
|
+
const getServerIndex = () => {
|
|
87
|
+
const len = conns.length;
|
|
88
|
+
let min = conns[0];
|
|
89
|
+
if (min < MAX_REQUESTS) {
|
|
90
|
+
++conns[0];
|
|
91
|
+
return 0;
|
|
92
|
+
}
|
|
93
|
+
let index = 0;
|
|
94
|
+
for (let i = 1; i < len; i++) {
|
|
95
|
+
if (min > conns[i]) {
|
|
96
|
+
min = conns[i];
|
|
97
|
+
index = i;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
if (min < MAX_REQUESTS || len >= cpuNum) {
|
|
101
|
+
++conns[index];
|
|
102
|
+
return index;
|
|
103
|
+
}
|
|
104
|
+
conns[len] = 1;
|
|
105
|
+
return len;
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
exports.passToService = async (ctx) => {
|
|
109
|
+
const index = getServerIndex();
|
|
110
|
+
const port = await startService(index);
|
|
111
|
+
await proxyCtx(ctx, { host: LOCALHOST, port });
|
|
112
|
+
--conns[index];
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
const REDIRECT_PAGES = {
|
|
116
|
+
'/': '/select.html',
|
|
117
|
+
'/share.html': '/share.html',
|
|
118
|
+
'/select.html': '/select.html',
|
|
119
|
+
'/index.html': '/select.html',
|
|
120
|
+
'/capture.html': '/',
|
|
121
|
+
'/data.html': '/',
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
exports.getRedirectUrl = (ctx) => {
|
|
125
|
+
let { redirect } = config;
|
|
126
|
+
const { pageName, originalUrl: url, accountName } = ctx;
|
|
127
|
+
const page = REDIRECT_PAGES[pageName];
|
|
128
|
+
if (!redirect || !page || FROM_RE.test(url)) {
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
redirect = `${redirect}${page}`;
|
|
132
|
+
const index = url.indexOf('?');
|
|
133
|
+
let query = index === -1 ? '' : url.substring(index + 1);
|
|
134
|
+
if (accountName) {
|
|
135
|
+
query = `${query ? `${query}&` : ''}account=${encodeURIComponent(accountName)}`;
|
|
136
|
+
}
|
|
137
|
+
return query ? `${redirect}?${query}` : redirect;
|
|
138
|
+
};
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
const path = require('path');
|
|
2
|
+
const { fork } = require('pfork');
|
|
3
|
+
const { getAdmin, getDomain } = require('./storage');
|
|
4
|
+
const config = require('../config');
|
|
5
|
+
|
|
6
|
+
const script = path.join(__dirname, '../whistle.js');
|
|
7
|
+
const DELAY_TIME = 1000 * 6;
|
|
8
|
+
let server;
|
|
9
|
+
|
|
10
|
+
exports.fork = () => {
|
|
11
|
+
if (!server) {
|
|
12
|
+
const admin = getAdmin();
|
|
13
|
+
server = new Promise((resolve, reject) => fork({
|
|
14
|
+
script,
|
|
15
|
+
baseDir: config.baseDir,
|
|
16
|
+
username: admin.username,
|
|
17
|
+
password: admin.password,
|
|
18
|
+
debugMode: config.debugMode,
|
|
19
|
+
domain: `${getDomain()},${config.domain}`,
|
|
20
|
+
realPort: config.port,
|
|
21
|
+
realHost: config.host || '',
|
|
22
|
+
authKey: config.authKey,
|
|
23
|
+
mainAuthKey: config.mainAuthKey,
|
|
24
|
+
storageServer: config.storage,
|
|
25
|
+
dnsServer: config.dnsServer,
|
|
26
|
+
globalPluginPath: config.globalPluginPath,
|
|
27
|
+
accountPluginPath: config.accountPluginPath,
|
|
28
|
+
}, (err, options, child) => {
|
|
29
|
+
if (err) {
|
|
30
|
+
server = null;
|
|
31
|
+
reject(err);
|
|
32
|
+
} else {
|
|
33
|
+
server.whistleProcess = child;
|
|
34
|
+
child.once('close', () => {
|
|
35
|
+
server = null;
|
|
36
|
+
});
|
|
37
|
+
resolve(options);
|
|
38
|
+
}
|
|
39
|
+
}));
|
|
40
|
+
}
|
|
41
|
+
return server;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
exports.restart = () => {
|
|
45
|
+
if (server) {
|
|
46
|
+
if (server.whistleProcess) {
|
|
47
|
+
server.whistleProcess.kill(DELAY_TIME);
|
|
48
|
+
} else {
|
|
49
|
+
const svr = server;
|
|
50
|
+
server.then(() => {
|
|
51
|
+
svr.whistleProcess.kill(DELAY_TIME);
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
server = null;
|
|
55
|
+
}
|
|
56
|
+
};
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
const p = require('pfork');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const config = require('../config');
|
|
4
|
+
const workerNum = require('./workerNum');
|
|
5
|
+
|
|
6
|
+
const WHISTLE_WORKER = path.join(__dirname, '../plugins/whistle.nohost/lib/whistle.js');
|
|
7
|
+
const DELAY = 6000;
|
|
8
|
+
const cache = {};
|
|
9
|
+
|
|
10
|
+
const getName = (index) => {
|
|
11
|
+
if (index > 0) {
|
|
12
|
+
index %= workerNum;
|
|
13
|
+
}
|
|
14
|
+
return `$${index}`;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
exports.fork = (index) => {
|
|
18
|
+
if (index > 0) {
|
|
19
|
+
index %= workerNum;
|
|
20
|
+
}
|
|
21
|
+
const name = getName(index);
|
|
22
|
+
cache[name] = cache[name] || new Promise((resolve, reject) => {
|
|
23
|
+
p.fork({
|
|
24
|
+
worker: true,
|
|
25
|
+
pluginsPath: config.pluginsPath,
|
|
26
|
+
value: name,
|
|
27
|
+
password: `${Math.random()}`,
|
|
28
|
+
guestName: '-',
|
|
29
|
+
storage: `whistle.nohost/${name}`,
|
|
30
|
+
script: WHISTLE_WORKER,
|
|
31
|
+
storageServer: config.storage,
|
|
32
|
+
dnsServer: config.dnsServer,
|
|
33
|
+
accountPluginPath: config.accountPluginPath,
|
|
34
|
+
}, (err, result, child) => {
|
|
35
|
+
if (err) {
|
|
36
|
+
delete cache[name];
|
|
37
|
+
return reject(err);
|
|
38
|
+
}
|
|
39
|
+
child.on('exit', () => {
|
|
40
|
+
delete cache[name];
|
|
41
|
+
});
|
|
42
|
+
resolve(result);
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
return cache[name];
|
|
46
|
+
};
|
|
47
|
+
exports.kill = (index) => {
|
|
48
|
+
p.kill({
|
|
49
|
+
script: WHISTLE_WORKER,
|
|
50
|
+
value: getName(index),
|
|
51
|
+
}, DELAY);
|
|
52
|
+
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
const os = require('os');
|
|
2
|
+
|
|
3
|
+
const WORKER_NUM_FROM_ENV = parseInt(process.env.NOHOST_WORKER_NUM, 10);
|
|
4
|
+
const RESERVE_MEM = 1024 * 1024 * 1204 * 2;
|
|
5
|
+
if (WORKER_NUM_FROM_ENV > 0) {
|
|
6
|
+
module.exports = WORKER_NUM_FROM_ENV;
|
|
7
|
+
} else {
|
|
8
|
+
const WORKER_NUM_FROM_MEM = Math.floor(Math.max(os.totalmem() - RESERVE_MEM, 0) / (1024 * 1024 * 500)) || 1;
|
|
9
|
+
const WORKER_NUM_FROM_CPU = os.cpus().length * 4;
|
|
10
|
+
module.exports = Math.min(WORKER_NUM_FROM_MEM, WORKER_NUM_FROM_CPU);
|
|
11
|
+
}
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
<script>
|
|
2
|
+
var w2 = window.whistleBridge;
|
|
3
|
+
var showModal = w2.showModal;
|
|
4
|
+
var baseUrl = window.parent.location.href
|
|
5
|
+
.replace(/[?#].*$/, "")
|
|
6
|
+
.replace(/\/nohost_share\/.*$/, "")
|
|
7
|
+
.replace(/\/$/, "");
|
|
8
|
+
var codeList = [];
|
|
9
|
+
|
|
10
|
+
for (var c1 = 0; c1 < 10; c1++) {
|
|
11
|
+
codeList.push(c1 + "");
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
for (var c2 = 97; c2 <= 122; ++c2) {
|
|
15
|
+
codeList.push(String.fromCharCode(c2));
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
var codeLen = codeList.length;
|
|
19
|
+
|
|
20
|
+
function getExportList() {
|
|
21
|
+
var list = localStorage.exportInfoList;
|
|
22
|
+
try {
|
|
23
|
+
list = JSON.parse(list);
|
|
24
|
+
if (Array.isArray(list)) {
|
|
25
|
+
return list.filter(function(item) {
|
|
26
|
+
return item && item.name && item.date;
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
} catch (e) {}
|
|
30
|
+
return [];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function saveExportList(name, date, username, code, env, route) {
|
|
34
|
+
var list = getExportList();
|
|
35
|
+
list.push({ name, date, username, code, env, route });
|
|
36
|
+
if (list.length > 100) {
|
|
37
|
+
list = list.slice(-30);
|
|
38
|
+
}
|
|
39
|
+
localStorage.exportInfoList = JSON.stringify(list);
|
|
40
|
+
return list;
|
|
41
|
+
}
|
|
42
|
+
// 如果有构建,可以用模板代替
|
|
43
|
+
function createTable(list) {
|
|
44
|
+
var highlight = list;
|
|
45
|
+
list = list || getExportList();
|
|
46
|
+
var len = (Date.now() + "").length;
|
|
47
|
+
list = list
|
|
48
|
+
.reverse()
|
|
49
|
+
.map(function(item, i) {
|
|
50
|
+
var username = item.username;
|
|
51
|
+
var code = item.code;
|
|
52
|
+
var route = item.route || "";
|
|
53
|
+
if (route) {
|
|
54
|
+
route = "&route=" + encodeURIComponent(route);
|
|
55
|
+
}
|
|
56
|
+
username = username ? "&username=" + encodeURIComponent(username) : "";
|
|
57
|
+
var url =
|
|
58
|
+
baseUrl +
|
|
59
|
+
"/nohost_share/" +
|
|
60
|
+
"?name=" +
|
|
61
|
+
encodeURIComponent(item.name) +
|
|
62
|
+
"&date=" +
|
|
63
|
+
encodeURIComponent(item.date) +
|
|
64
|
+
username +
|
|
65
|
+
(code ? "&encrypted=1" : "") +
|
|
66
|
+
route;
|
|
67
|
+
var date = new Date(
|
|
68
|
+
parseInt(item.name.substring(0, len), 10)
|
|
69
|
+
).toLocaleString();
|
|
70
|
+
return (
|
|
71
|
+
"<tr" +
|
|
72
|
+
(!i && highlight ? ' style="font-weight: bold;"' : "") +
|
|
73
|
+
"><th>" +
|
|
74
|
+
(i + 1) +
|
|
75
|
+
"</th><td>" +
|
|
76
|
+
date +
|
|
77
|
+
'</td><td style="overflow: hidden"><a style="font-size: 13px;" href="' +
|
|
78
|
+
url +
|
|
79
|
+
'" target="_blank">' +
|
|
80
|
+
url +
|
|
81
|
+
"</a>" +
|
|
82
|
+
(code ? "<div>提取码:" + code + "</div>" : "") +
|
|
83
|
+
'</td><td><a href="javascript:;" class="w-copy-text-with-tips" data-clipboard-text="' +
|
|
84
|
+
url +
|
|
85
|
+
'">复制链接</a>' +
|
|
86
|
+
(code
|
|
87
|
+
? '<br/><a href="javascript:;" class="w-copy-text-with-tips" data-clipboard-text="' +
|
|
88
|
+
code +
|
|
89
|
+
'">复制提取码</a>'
|
|
90
|
+
: "") +
|
|
91
|
+
"</td>"
|
|
92
|
+
);
|
|
93
|
+
})
|
|
94
|
+
.join("");
|
|
95
|
+
if (!list) {
|
|
96
|
+
list = '<tr><td colspan="4" style="text-align: center;">Empty</td></tr>';
|
|
97
|
+
}
|
|
98
|
+
var head =
|
|
99
|
+
'<thead><th style="width: 60px">#</th><th style="width: 180px">Date</th><th>URL</th><th style="width: 120px">Operation</th></thead>';
|
|
100
|
+
return (
|
|
101
|
+
'<table class="table">' + head + "<tbody><tr>" + list + "</tbody></table>"
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function showFeeds(list) {
|
|
106
|
+
w2.showModal({
|
|
107
|
+
width: 1200,
|
|
108
|
+
title: "抓包分享记录",
|
|
109
|
+
body: createTable(list)
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function getEnvName(item) {
|
|
114
|
+
var headers = item && item.req && item.req.headers;
|
|
115
|
+
return headers && headers["x-whistle-nohost-env"];
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function exportSessions(options, code) {
|
|
119
|
+
var selectedList = options.selectedList;
|
|
120
|
+
if (!selectedList.length) {
|
|
121
|
+
selectedList = [options.activeItem];
|
|
122
|
+
}
|
|
123
|
+
var name = Date.now() + "" + Math.floor(Math.random() * 10000);
|
|
124
|
+
w2.request(
|
|
125
|
+
{
|
|
126
|
+
url: baseUrl + "/export_sessions?route=!required!",
|
|
127
|
+
crossDomain: true,
|
|
128
|
+
type: "post",
|
|
129
|
+
data: {
|
|
130
|
+
code: code || "",
|
|
131
|
+
name: name,
|
|
132
|
+
sessions: JSON.stringify(selectedList)
|
|
133
|
+
}
|
|
134
|
+
},
|
|
135
|
+
function(data) {
|
|
136
|
+
if (!data) {
|
|
137
|
+
return w2.toast.error("分享失败,请稍后再试!");
|
|
138
|
+
}
|
|
139
|
+
var env = getEnvName(selectedList[0]);
|
|
140
|
+
showFeeds(
|
|
141
|
+
saveExportList(name, data.date, data.username, code, env, data.route)
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
w2.addNetworkListener(function(options) {
|
|
147
|
+
if (options.name === "【分享】生成链接") {
|
|
148
|
+
w2.showModal({
|
|
149
|
+
width: 320,
|
|
150
|
+
body:
|
|
151
|
+
'<p style="font-size: 18px; text-align: center;">是否设置提取码?</p><span style="color: #666;">设置提取码后,用户只有输入提取码才能查看。</span><button type="button" class="close" data-dismiss="modal" style="position: absolute; top: 3px; right: 5px;"><span aria-hidden="true">×</span></button>',
|
|
152
|
+
footer:
|
|
153
|
+
'<button type="button" onclick="exportWithCode()" class="btn btn-primary" data-dismiss="modal"><span class="glyphicon glyphicon-lock" style="margin-right: 3px;"></span>设置提取码</button><button type="button" onclick="exportWithoutCode()" class="btn btn-default" data-dismiss="modal">不设置</button>',
|
|
154
|
+
methods: {
|
|
155
|
+
exportWithoutCode: function() {
|
|
156
|
+
exportSessions(options);
|
|
157
|
+
},
|
|
158
|
+
exportWithCode: function() {
|
|
159
|
+
var code = [];
|
|
160
|
+
for (var i = 0; i < 4; i++) {
|
|
161
|
+
code[i] = codeList[Math.floor(Math.random() * 100000) % codeLen];
|
|
162
|
+
}
|
|
163
|
+
exportSessions(options, code.join(""));
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
});
|
|
167
|
+
} else {
|
|
168
|
+
showFeeds();
|
|
169
|
+
}
|
|
170
|
+
});
|
|
171
|
+
</script>
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "whistle.share",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"whistleConfig": {
|
|
5
|
+
"priority": 100,
|
|
6
|
+
"hideLongProtocol": true,
|
|
7
|
+
"hideShortProtocol": true,
|
|
8
|
+
"networkMenus": [
|
|
9
|
+
{
|
|
10
|
+
"name": "【分享】生成链接",
|
|
11
|
+
"page": "/menu.html",
|
|
12
|
+
"required": true
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
"name": "【分享】查看记录",
|
|
16
|
+
"page": "/menu.html"
|
|
17
|
+
}
|
|
18
|
+
]
|
|
19
|
+
}
|
|
20
|
+
}
|