├── .babelrc
├── .editorconfig
├── .eslintignore
├── .eslintrc
├── .gitignore
├── .npmignore
├── .travis.yml
├── CHANGELOG.md
├── LICENSE
├── README.md
├── bin
├── nohost.js
├── plugin.js
└── util.js
├── commitlint.config.js
├── docs
├── .vuepress
│ ├── config.js
│ └── public
│ │ ├── favicon.ico
│ │ └── logo.png
├── README.md
└── zh
│ ├── admin
│ ├── README.md
│ ├── accounts.md
│ ├── certs.md
│ ├── config.md
│ ├── system.md
│ └── whistle.md
│ ├── advance
│ └── README.md
│ ├── api.md
│ ├── developer
│ ├── README.md
│ ├── capture.md
│ ├── customMenu.md
│ ├── plugin.md
│ └── usage.md
│ ├── quickstart.md
│ └── users
│ ├── README.md
│ ├── others.md
│ ├── pd.md
│ └── tester.md
├── index.js
├── lib
├── config.js
├── index.js
├── main
│ ├── cgi
│ │ ├── getSettings.js
│ │ ├── getVersion.js
│ │ ├── login.js
│ │ ├── restart.js
│ │ ├── setAdmin.js
│ │ ├── setDomain.js
│ │ └── status.js
│ ├── index.js
│ ├── router.js
│ ├── storage.js
│ ├── util.js
│ ├── whistleMgr.js
│ ├── worker.js
│ └── workerNum.js
├── plugins
│ ├── account
│ │ ├── whistle.share
│ │ │ ├── menu.html
│ │ │ └── package.json
│ │ └── whistle.storage
│ │ │ ├── index.js
│ │ │ ├── package.json
│ │ │ └── rules.txt
│ └── whistle.nohost
│ │ ├── _rules.txt
│ │ ├── index.js
│ │ ├── initial.js
│ │ ├── lib
│ │ ├── accountMgr.js
│ │ ├── envMgr.js
│ │ ├── rulesServer.js
│ │ ├── tunnelRulesServer.js
│ │ ├── uiServer
│ │ │ ├── cgi
│ │ │ │ ├── account
│ │ │ │ │ └── changePassword.js
│ │ │ │ ├── admin
│ │ │ │ │ ├── activeAccount.js
│ │ │ │ │ ├── addAccount.js
│ │ │ │ │ ├── allAccounts.js
│ │ │ │ │ ├── changeNotice.js
│ │ │ │ │ ├── changePassword.js
│ │ │ │ │ ├── enableGuest.js
│ │ │ │ │ ├── getAuthKey.js
│ │ │ │ │ ├── getSettings.js
│ │ │ │ │ ├── login.js
│ │ │ │ │ ├── move.js
│ │ │ │ │ ├── removeAccount.js
│ │ │ │ │ ├── setAccountRules.js
│ │ │ │ │ ├── setAuthKey.js
│ │ │ │ │ ├── setDefaultRules.js
│ │ │ │ │ ├── setEntryPatterns.js
│ │ │ │ │ ├── setJsonData.js
│ │ │ │ │ ├── setRulesTpl.js
│ │ │ │ │ ├── setTestRules.js
│ │ │ │ │ └── specPattern.js
│ │ │ │ ├── allowlist.js
│ │ │ │ ├── entryRules.js
│ │ │ │ ├── follow.js
│ │ │ │ ├── getEnv.js
│ │ │ │ ├── list.js
│ │ │ │ ├── patterns.js
│ │ │ │ ├── pluginRules.js
│ │ │ │ ├── proxy.js
│ │ │ │ ├── proxyNetwork.js
│ │ │ │ ├── redirect.js
│ │ │ │ ├── selectEnv.js
│ │ │ │ └── unfollow.js
│ │ │ ├── index.js
│ │ │ ├── network.js
│ │ │ ├── openAPI.js
│ │ │ └── router.js
│ │ ├── util.js
│ │ ├── whistle.js
│ │ └── whistleMgr.js
│ │ ├── package.json
│ │ └── rules.txt
├── service
│ ├── cgi
│ │ ├── export.js
│ │ ├── import.js
│ │ └── util.js
│ ├── index.js
│ ├── router.js
│ └── server.js
├── util
│ ├── address.js
│ ├── getPort.js
│ ├── login.js
│ └── parseDomain.js
└── whistle.js
├── package-lock.json
├── package.json
├── packages
├── admin
│ └── package.json
├── gateway
│ ├── .npmignore
│ ├── LICENSE
│ ├── README.md
│ ├── index.js
│ ├── lib
│ │ ├── util.js
│ │ ├── whistle.gateway
│ │ │ ├── index.js
│ │ │ ├── initial.js
│ │ │ ├── package.json
│ │ │ └── rules.txt
│ │ └── whistle.snicallback
│ │ │ ├── index.js
│ │ │ ├── initial.js
│ │ │ ├── package.json
│ │ │ └── rules.txt
│ ├── package-lock.json
│ ├── package.json
│ └── test
│ │ ├── README.md
│ │ ├── admin
│ │ ├── dispatch.js
│ │ └── lib
│ │ │ ├── index.js
│ │ │ └── router.js
│ │ ├── gateway
│ │ ├── dispatch.js
│ │ └── lib
│ │ │ ├── auth.js
│ │ │ ├── handleConnect.js
│ │ │ ├── handleRequest.js
│ │ │ ├── handleUpgrade.js
│ │ │ └── sniCallback.js
│ │ └── plugins
│ │ └── resRules.js
├── plugins
│ ├── whistle.nohost-security
│ │ └── README.md
│ └── whistle.nohost-settings
│ │ └── README.md
├── server
│ └── package.json
└── tools
│ ├── app
│ └── README.md
│ ├── pc
│ └── README.md
│ └── router
│ ├── .npmignore
│ ├── LICENSE
│ ├── README.md
│ ├── lib
│ ├── address.js
│ ├── connect.js
│ ├── index.js
│ └── util.js
│ ├── package-lock.json
│ ├── package.json
│ └── test
│ ├── README.md
│ ├── data.js
│ ├── favicon.ico
│ ├── index.html
│ └── proxy.js
├── src
├── admin
│ ├── accounts
│ │ ├── components
│ │ │ ├── AccountList.js
│ │ │ ├── AddNoticeForm.js
│ │ │ ├── AddUserForm.js
│ │ │ └── UserTable.js
│ │ ├── index.css
│ │ └── index.js
│ ├── ajax.js
│ ├── certs
│ │ ├── index.css
│ │ └── index.js
│ ├── cgi.js
│ ├── config
│ │ ├── index.css
│ │ └── index.js
│ ├── index.css
│ ├── index.js
│ ├── network.js
│ ├── settings
│ │ ├── component
│ │ │ ├── administrator.js
│ │ │ ├── authKeySetting.js
│ │ │ └── domain.js
│ │ ├── index.css
│ │ └── index.js
│ └── util.js
├── assets
│ └── favicon.ico
├── base.less
├── button
│ ├── const.js
│ ├── envHistory.js
│ ├── getEnvData.js
│ ├── helper.js
│ ├── index.css
│ ├── index.js
│ └── tpl.js
├── capture
│ ├── index.css
│ ├── index.js
│ └── utils.js
├── components
│ ├── history
│ │ ├── helper.js
│ │ ├── index.css
│ │ └── index.js
│ ├── navBar
│ │ ├── index.css
│ │ └── index.js
│ ├── panel
│ │ └── index.js
│ ├── qrCode
│ │ └── index.js
│ ├── tab
│ │ ├── index.jsx
│ │ ├── lib
│ │ │ ├── tabpane.jsx
│ │ │ └── tabs.jsx
│ │ └── style
│ │ │ └── index.css
│ ├── textareaPanel
│ │ └── index.js
│ └── whistleEditor
│ │ ├── index.css
│ │ └── index.js
├── config
│ ├── webpack.common.js
│ ├── webpack.dev.js
│ └── webpack.prod.js
├── iconfonts
│ ├── iconfont.eot
│ ├── iconfont.svg
│ ├── iconfont.ttf
│ └── iconfont.woff
├── img
│ └── logo.png
├── pages
│ ├── select.html
│ └── template.html
└── select
│ ├── index.css
│ └── index.js
└── test
├── .nohost
├── .backup
│ └── properties
└── properties
├── app
└── assets
│ └── custom_certs
│ ├── file1.crt
│ └── file1.key
├── index.test.js
├── jest
├── babel.config.js
└── jest.config.js
└── lib
├── config.test.js
├── index.test.js
├── main
├── cgi
│ ├── getSetting.test.js
│ ├── getVersion.test.js
│ ├── login.test.js
│ ├── restart.test.js
│ ├── setAdmin.test.js
│ └── setDomain.test.js
├── storage.test.js
├── util.test.js
└── whistleMgr.test.js
├── service
└── index.test.js
└── util
├── address.test.js
├── getPort.test.js
└── login.test.js
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": [
3 | "es2015",
4 | "react",
5 | "env",
6 | "stage-0"
7 | ],
8 | "plugins": [
9 | ["import", { "libraryName": "antd" }],
10 | ["transform-class-properties", { "spec": true }],
11 | "transform-object-assign"
12 | ]
13 | }
14 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | # http://editorconfig.org
2 | root = true
3 |
4 | [*]
5 | charset = utf-8
6 | end_of_line = lf
7 | indent_size = 2
8 | indent_style = space
9 | insert_final_newline = true
10 | max_line_length = 80
11 | trim_trailing_whitespace = true
12 |
13 | [*.md]
14 | max_line_length = 0
15 | trim_trailing_whitespace = false
16 |
17 | [COMMIT_EDITMSG]
18 | max_line_length = 0
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | node_modules
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "eslint-config-imweb"
3 | }
4 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 |
6 | # Runtime data
7 | pids
8 | *.pid
9 | *.seed
10 |
11 | # Directory for instrumented libs generated by jscoverage/JSCover
12 | lib-cov
13 |
14 | # Coverage directory used by tools like istanbul
15 | coverage
16 |
17 | # nyc test coverage
18 | .nyc_output
19 |
20 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
21 | .grunt
22 |
23 | # node-waf configuration
24 | .lock-wscript
25 |
26 | # Compiled binary addons (http://nodejs.org/api/addons.html)
27 | build/Release
28 |
29 | # Dependency directories
30 | jspm_packages
31 |
32 | # Optional npm cache directory
33 | .npm
34 |
35 | # Optional REPL history
36 | .node_repl_history
37 | .idea
38 | node_modules
39 | .DS_Store
40 | .idea
41 | public
42 | .history
43 |
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 |
6 | # Runtime data
7 | pids
8 | *.pid
9 | *.seed
10 |
11 | # Directory for instrumented libs generated by jscoverage/JSCover
12 | lib-cov
13 |
14 | # Coverage directory used by tools like istanbul
15 | coverage
16 |
17 | # nyc test coverage
18 | .nyc_output
19 |
20 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
21 | .grunt
22 |
23 | # node-waf configuration
24 | .lock-wscript
25 |
26 | # Compiled binary addons (http://nodejs.org/api/addons.html)
27 | build/Release
28 |
29 | # Dependency directories
30 | node_modules
31 | jspm_packages
32 |
33 | # Optional npm cache directory
34 | .npm
35 |
36 | # Optional REPL history
37 | .node_repl_history
38 |
39 | # test
40 | test
41 |
42 | # source
43 | /src
44 | /docs
45 | /deploy.sh
46 | /.*
47 | /commitlint.config.js
48 | /CHANGELOG.md
49 | /packages
50 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 | node_js:
3 | - lts/*
4 | install:
5 | - npm i
6 | script:
7 | - npm run docs:build
8 | deploy:
9 | provider: pages
10 | skip-cleanup: true
11 | local_dir: docs/.vuepress/dist
12 | github-token: $GITHUB_TOKEN
13 | keep-history: true
14 | on:
15 | branch: master
16 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # v1.5.0
2 | 1. feat: 支持自定义专属环境
3 | 2. feat: 管理后台的账号默认环境、测试环境、专属测试环境支持通过 `@url` 引入远程规则
4 |
5 | # v1.4.0
6 | 1. feat: 调整全局插件安装目录(可能会影响到之前通过 `n2 i -g whistle.xxx` 安装的插件,如果遇到此问题手动再安装下即可)
7 |
8 | # 1.3.9
9 | 1. feat: 优化 nohost 插件安装目录
10 |
11 | # v1.3.4
12 | 1. refactor: 请求域名优先使用 `x-whistle-real-host`
13 |
14 | # v1.3.3
15 | 1. refactor: 调整日志默认输出文件为 `~/.WhistleAppData/nohost.log`
16 |
17 | # v1.3.2
18 | 1. refactor: update whistle
19 | # v1.3.1
20 | 1. refactor: update whistle.script
21 |
22 | # v1.3.0
23 | 1. fix: https://github.com/Tencent/nohost/pull/105
24 |
25 | # v1.2.5
26 | 1. feat: 支持设置多个插件路径
27 |
28 | # v1.2.5
29 | 1. feat: 支持通过命令参数 `--dnsServer` 自定义 dns 服务,参见:https://github.com/Tencent/nohost/issues/100
30 | 2. feat: 支持通过命令行 `--globalPluginPath` 和 `--accountPluginPath` 分别设置全局插件和账号插件,参见:https://github.com/Tencent/nohost/issues/101
31 |
32 | # v1.2.4
33 | # v1.2.3
34 | 1. refactor: 减少本地 Whistle 配置的干扰
35 |
36 | # v1.2.2
37 | 1. refactor: 使用 `@nohost/router` 代替 `@nohost/connect`,更新 Whistle 依赖
38 | 2. style: 添加上次证书成功提示
39 |
40 | # v1.2.1
41 | 1. refactor: 上传证书无需重启服务
42 |
43 | # v1.1.3
44 | 1. refactor: 调整启动参数存储目录
45 |
46 | # v1.0.1
47 | 1. fix: `--max-http-header-size` 不生效问题
48 |
49 | # v1.0.0
50 | 1. feat: 完整支持隧道代理请求,包括 UI 请求
51 | 2. fix: uninstall 插件无法清理干净的问题
52 |
53 | # v0.9
54 | 1. fix: 设置域名后还是无法访问界面的问题
55 | 2. refactor: 优化内部实现适应复杂网络
56 |
57 | # v0.8
58 | 1. perf: 解决一些内存无法及时释放问题
59 | 2. feat: 支持将用户页面重定向到指定 URL
60 |
61 | # v0.7
62 | 1. refactor: 优化查看本机请求,解决
63 | 2. style: 小圆点支持记录5个最近选择到环境
64 | # v0.6
65 | 1. style: 调整界面样式
66 | 2. refactor: 调整 GC 参数
67 |
68 | # v0.5
69 | 1. feat: 添加 headless 模式
70 | 2. fix: 内存泄露问题
71 |
72 | # v0.4
73 | 1. feat: 支持通过命令行 `-n name -w pwd` 设置管理员的用户名和密码
74 | 2. fix: 修复了一些bug
75 |
76 | # v0.3
77 | 1. feat: 支持 docker 的宿主机的IP访问界面
78 | 2. feat: 配置代理后,支持默认域名 `admin.nohost.pro` 访问界面
79 | 3. feat: 支持通过 `n2 install whistle.xxx` 及 `n2 uninstall whistle.xxx` 安装和卸载插件
80 |
81 | # v0.2
82 | 1. 正式稳定版本
83 |
84 | # v0.1
85 | 1. 可用版本,请使用最新版本
86 |
87 | # v0.0.x
88 | 1. 非正式版本,请使用最新版本
--------------------------------------------------------------------------------
/bin/plugin.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const os = require('os');
3 | const fs = require('fs');
4 | const fse = require('fs-extra');
5 |
6 | const PLUGINS_DIR = path.resolve(os.homedir(), 'whistle-plugins/whistle.nohost');
7 | const WHISTLE_PLUGIN_RE = /^(@[\w-]+\/)?whistle\.[a-z\d_-]+$/;
8 | const ACCOUNT_RE = /^[\w.-]{1,24}$/;
9 |
10 | const getAccount = (argv) => {
11 | let account;
12 | for (let i = 0, len = argv.length; i < len; i++) {
13 | const arg = argv[i];
14 | if (['-a', '-w', '--account'].indexOf(arg) !== -1) {
15 | account = argv[i + 1];
16 | argv.splice(i, 2);
17 | break;
18 | }
19 | if (/^--account=/.test(arg)) {
20 | account = arg.substring(10);
21 | argv.splice(i, 1);
22 | break;
23 | }
24 | }
25 | return account && ACCOUNT_RE.test(account) ? account : null;
26 | };
27 |
28 | const parseArgv = (argv) => {
29 | argv = argv.slice();
30 | const account = getAccount(argv);
31 | const args = [];
32 | const plugins = argv.filter((name) => {
33 | if (WHISTLE_PLUGIN_RE.test(name)) {
34 | return true;
35 | }
36 | args.push(name);
37 | return false;
38 | });
39 | return {
40 | account,
41 | plugins,
42 | args,
43 | };
44 | };
45 |
46 | exports.parseArgv = parseArgv;
47 |
48 | const removeDir = (dir) => {
49 | if (fs.existsSync(dir)) { // eslint-disable-line
50 | fse.removeSync(dir); // eslint-disable-line
51 | }
52 | };
53 |
54 | exports.uninstall = (argv) => {
55 | const { account, plugins } = parseArgv(argv);
56 | if (!plugins.length) {
57 | return;
58 | }
59 | if (account) {
60 | plugins.forEach((name) => {
61 | removeDir(path.join(PLUGINS_DIR, account, 'node_modules', name));
62 | removeDir(path.join(PLUGINS_DIR, account, 'lib/node_modules', name));
63 | });
64 | } else {
65 | plugins.forEach((name) => {
66 | removeDir(path.join(PLUGINS_DIR, 'node_modules', name));
67 | removeDir(path.join(PLUGINS_DIR, 'lib/node_modules', name));
68 | });
69 | }
70 | };
71 |
--------------------------------------------------------------------------------
/bin/util.js:
--------------------------------------------------------------------------------
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 | const os = require('os');
10 | const colors = require('colors/safe');
11 | const pkg = require('../package.json');
12 |
13 | const isWin = process.platform === 'win32';
14 |
15 | const formatOptions = (options) => {
16 | if (!options || !/^(?:([\w.-]+):)?([1-9]\d{0,4})$/.test(options.port)) {
17 | return options;
18 | }
19 | options.host = options.host || RegExp.$1;
20 | options.port = parseInt(RegExp.$2, 10);
21 | return options;
22 | };
23 |
24 | exports.formatOptions = formatOptions;
25 | /* eslint-disable no-console */
26 | function getIpList() {
27 | const ipList = [];
28 | const ifaces = os.networkInterfaces();
29 | Object.keys(ifaces).forEach((ifname) => {
30 | ifaces[ifname].forEach((iface) => {
31 | if (iface.family === 'IPv4' || iface.family === 4) {
32 | ipList.push(iface.address);
33 | }
34 | });
35 | });
36 | const index = ipList.indexOf('127.0.0.1');
37 | if (index !== -1) {
38 | ipList.splice(index, 1);
39 | }
40 | ipList.unshift('127.0.0.1');
41 | return ipList;
42 | }
43 |
44 | function error(msg) {
45 | console.log(colors.red(msg));
46 | }
47 |
48 | function warn(msg) {
49 | console.log(colors.yellow(msg));
50 | }
51 |
52 | function info(msg) {
53 | console.log(colors.green(msg));
54 | }
55 | exports.error = error;
56 | exports.warn = warn;
57 | exports.info = info;
58 |
59 | function showKillError() {
60 | error('[!] Cannot kill nohost owned by root');
61 | info(`[i] Try to run command ${isWin ? 'as an administrator' : 'with `sudo`'}`);
62 | }
63 |
64 | exports.showKillError = showKillError;
65 |
66 | function showUsage(isRunning, options, restart) {
67 | if (isRunning) {
68 | if (restart) {
69 | showKillError();
70 | } else {
71 | warn(`[!] nohost@${pkg.version} is running`);
72 | }
73 | } else {
74 | info(`[i] nohost@${pkg.version}${restart ? ' restarted' : ' started'}`);
75 | }
76 | const { host, port } = formatOptions(options);
77 | const curPort = port ? options.port : pkg.port;
78 | const list = host ? [host] : getIpList();
79 | info(`[i] use your device to visit the following URL list, gets the ${colors.bold('IP')} of the URL you can access:`);
80 | info(list.map((ip) => {
81 | return ` http://${colors.bold(ip)}${curPort ? `:${curPort}` : ''}/`;
82 | }).join('\n'));
83 |
84 | warn(' Note: If all the above URLs are unable to access, check the firewall settings');
85 | warn(` For help see ${colors.bold('https://nohost.pro/')}`);
86 |
87 | if (parseInt(process.version.slice(1), 10) < 6) {
88 | // eslint-disable-next-line
89 | warn(colors.bold('\nWarning: The current Node version is too low, access https://nodejs.org to install the latest version, or may not be able to Capture HTTPS CONNECTs\n'));
90 | }
91 | }
92 |
93 | exports.showUsage = showUsage;
94 |
--------------------------------------------------------------------------------
/commitlint.config.js:
--------------------------------------------------------------------------------
1 | module.exports = { extends: ['@commitlint/config-conventional'] };
2 |
--------------------------------------------------------------------------------
/docs/.vuepress/config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | title: 'Nohost',
3 | description: 'Multi-user & multi-env web debugging proxy based on whistle',
4 | base: '/nohost/',
5 | port: 8081,
6 | head: [
7 | ['link', { rel: 'shortcut icon', href: '/favicon.ico' }]
8 | ],
9 | themeConfig: {
10 | logo: '/logo.png',
11 | repo: 'https://github.com/Tencent/nohost.git',
12 | repoLabel: 'GitHub',
13 | displayAllHeaders: true,
14 | sidebarDepth: 1,
15 | sidebar: [
16 | {
17 | title: '概述',
18 | path: '/',
19 | },
20 | {
21 | title: '快速上手',
22 | path: '/zh/quickstart',
23 | },
24 | {
25 | title: '管理员',
26 | path: '/zh/admin/',
27 | collapsable: false,
28 | children: [
29 | {
30 | title: '账号',
31 | path: '/zh/admin/accounts',
32 | },
33 | {
34 | title: '证书',
35 | path: '/zh/admin/certs',
36 | },
37 | {
38 | title: '配置',
39 | path: '/zh/admin/config',
40 | },
41 | {
42 | title: 'Whistle',
43 | path: '/zh/admin/whistle',
44 | },
45 | {
46 | title: '系统',
47 | path: '/zh/admin/system',
48 | },
49 | ],
50 | },
51 | {
52 | title: '普通开发',
53 | path: '/zh/developer/',
54 | collapsable: false,
55 | children: [
56 | {
57 | title: '如何使用',
58 | path: '/zh/developer/usage',
59 | },
60 | {
61 | title: '抓包调试',
62 | path: '/zh/developer/capture',
63 | },
64 | {
65 | title: '安装插件',
66 | path: '/zh/developer/plugin',
67 | },
68 | {
69 | title: '自定义菜单',
70 | path: '/zh/developer/customMenu.md',
71 | },
72 | ],
73 | },
74 | {
75 | title: '普通用户',
76 | path: '/zh/users/',
77 | collapsable: false,
78 | children: [
79 | {
80 | title: '产品经理',
81 | path: '/zh/users/pd',
82 | },
83 | {
84 | title: '测试同事',
85 | path: '/zh/users/tester',
86 | },
87 | {
88 | title: '外网用户',
89 | path: '/zh/users/others',
90 | },
91 | ],
92 | },
93 | {
94 | title: 'API',
95 | path: '/zh/api',
96 | collapsable: false,
97 | },
98 | {
99 | title: '高级应用',
100 | path: '/zh/advance/',
101 | collapsable: false,
102 | },
103 | ],
104 | },
105 | }
106 |
--------------------------------------------------------------------------------
/docs/.vuepress/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Tencent/nohost/d8994d9709eedd980ab6aeb359224896e67e09cd/docs/.vuepress/public/favicon.ico
--------------------------------------------------------------------------------
/docs/.vuepress/public/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Tencent/nohost/d8994d9709eedd980ab6aeb359224896e67e09cd/docs/.vuepress/public/logo.png
--------------------------------------------------------------------------------
/docs/README.md:
--------------------------------------------------------------------------------
1 | # 概述
2 | Nohost 是基于 [Whistle](https://github.com/avwo/whistle) 实现的多用户多环境配置及抓包调试系统,不仅具备 Whistle 的所有功能,并在 Whistle 基础上扩展了一些功能,且支持多人多环境同时使用,主要用于部署在公共服务器上供整个部门(公司)的同事共同使用,具有以下功能:
3 | 1. 环境共享:前端无需配后台环境,后台无需配前端环境,其他人无需配任何环境
4 | 2. 抓包调试:远程实时抓包调试,支持各种 Whistle 规则,以及通过链接分享抓包数据
5 | 3. 历史记录:可以把环境配置及抓包数据沉淀下来,供后续随时切换查看
6 | 4. 插件扩展:可以通过插件扩展实现诸如 [inspect](https://github.com/whistle-plugins/whistle.inspect),[vase](https://github.com/whistle-plugins/whistle.vase),[autosave](https://github.com/whistle-plugins/whistle.autosave) 等功能
7 | 5. 对外接口:提供对外接口,可供发布系统、CI等工具操作,实现自动化增删查改环境配置
8 |
9 | 
--------------------------------------------------------------------------------
/docs/zh/admin/README.md:
--------------------------------------------------------------------------------
1 | # 管理员
--------------------------------------------------------------------------------
/docs/zh/admin/accounts.md:
--------------------------------------------------------------------------------
1 | # 账号
2 |
3 | Nohost 是基于 Whistle 实现的多用户环境配置及抓包调试服务。小圆点里面出现的账号,需要到Nohost管理平台配置。
4 | 地址为http://imwebtest.test.com:8080/admin.html#accounts
5 | > 其中 imwebtest.test.com 表示Nohost运行的域名
6 |
7 | 页面如下图所示:
8 |
9 | 
10 |
11 | 点击 **+添加账号** 可以添加使用账号:
12 |
13 |
14 | 新添加的账号可以设置独立的规则,地址为:
15 | http://imwebtest.test.com:8080/data.html?name={新建的账号}
16 | > 其中 imwebtest.test.com 表示Nohost运行的域名
17 |
18 | 
19 |
20 | 添加的账号会出现在**环境选择列表**,**环境选择列表**可能出现在多个地方,如果在**入口配置**( http://imwebtest.test.com:8080/admin.html#config/entrySettings )配置了出现**环境选择圆点**的页面,则访问这个页面经过本地Whistle代理的时候,会被注入**小圆点**,比如:
21 |
22 | 
23 |
--------------------------------------------------------------------------------
/docs/zh/admin/certs.md:
--------------------------------------------------------------------------------
1 | # 证书
2 |
3 | Nohost作为代理抓包服务器,如果用户希望Nohost能抓包https请求,则需要用到“中间人攻击”原理。
4 |
5 | 
6 |
7 | 中间人截取客户端发送给服务器的请求,然后伪装成客户端与服务器进行通信;将服务器返回给客户端的内容发送给客户端,伪装成服务器与客户端进行通信。 通过这样的手段,便可以获取客户端和服务器之间通信的所有内容。 使用中间人攻击手段,必须要让客户端信任中间人的证书。
8 |
9 |
10 | 让客户端信任中间人的证书有2种方案:
11 | 1. 客户端安装Whistle的根证书
12 | 2. 客户端不需要安装Whistle的根证书,Nohost服务器安装业务域名真实的证书,这样Nohost服务器可以充当“真实”服务器
13 |
14 | > 这里的**中间人** 就是Nohost代理。
15 |
16 | 如果希望https的页面能注入**环境选择小圆点**,也是有必要实施上面的2种方案,否则Nohost代理是没法拦截页面然后在页面注入脚本的。用户体验上,第二种方案Nohost代理上传证书,可以省去让用户安装Whistle根证书的麻烦。
17 |
18 |
19 | **上传证书**
20 |
21 | Nohost管理页面提供了上传业务域名证书功能, 地址为
22 | http://imwebtest.test.com:8080/admin.html#certs
23 | > 其中 imwebtest.test.com 表示Nohost运行的域名
24 |
25 | 
26 |
27 | *.crt和 *.key 文件要一一对应,每个文件内可包含多个域名,会自动识别文件内有哪些域名。
28 |
--------------------------------------------------------------------------------
/docs/zh/admin/config.md:
--------------------------------------------------------------------------------
1 | # 配置
2 | ## 入口配置
3 | **您在入口配置里可以配置一些规则,来决定哪些请求需要通过Nohost转发。**
4 |
5 | 例如:可以设置一些页面不注入Whistle小圆点,一些请求不转发到Nohost服务器。
6 |
7 | 具体请求链路如下所示:
8 |
9 | 
10 |
11 | 
12 | 入口配置的规则有三种(#xxx表示注释):
13 | ```
14 | pattern #转发到Nohost,如果是html页面则注入小圆点
15 | -pattern #转发到Nohost,不注入小圆点
16 | --pattern #不转发到Nohost,且不注入小圆点
17 | x)-pattern #x为整数(正负数零都可以),表示手动设置优先级,默认为0
18 | ```
19 | [pattern规则](https://wproxy.org/whistle/pattern.html):匹配顺序是从上到下,每个请求只会匹配其中一个,证书里面到域名优先级默认最低,可以通过 1) 设置优先级。
20 |
21 | 如:
22 | ```
23 | ke.qq.com
24 | -*.url.cn
25 | --localhost.**
26 | -1)**.qq.com
27 |
28 | 表示:
29 |
30 | 所有 ke.qq.com 的请求都转发到Nohost,且所有 html 都注入小圆点
31 | 所有 xxx.url.cn 的请求都转发到Nohost,但不注入小圆点
32 | 所有 localhost.xxx.yyy... 的请求都不转发到Nohost,且不注入小圆点
33 | 所有 qq.com 的子代域名请求都转发到Nohost,但不注入小圆点,并优先级设为 -1 ,确保证书里面的 qq.com 子域名可以正常注入小圆点
34 | ```
35 |
36 | > 如果想通过代码决定是否将请求转到 Nohost 账号,可以请求头里面注入 `x-whistle-nohost-policy: none` 或 `x-whistle-nohost-policy: 1`,后者如果是html页面会注入小圆点
37 |
38 | ## 账号规则
39 | 每个接入Nohost的团队都可以设置账号规则,其主要作用于你团队里的成员,规则分为默认规则和专属规则。
40 |
41 | ### 账号默认规则
42 | **账号的默认规则可以对所有用户生效,就是account进程里面定义的规则。**
43 |
44 | 默认规则相当于每个用户本地Whistle的default规则。
45 |
46 | 注意:它的优先级是低于Nohost插件里规则。例如:Nohost插件里面如果设置某个cgi的返回码是500,账号规则里是无法改变的。下图所示的就是Nohost插件的规则:
47 |
48 | 
49 |
50 | ### 环境默认规则
51 | 对所有非 `Default` 的用户自定义环境生效。
52 |
53 | ### 专属环境默认规则
54 | 账号的专属规则只有配置一些特殊规则时 才会生效。
55 |
56 | 例如:配置 cgi-a b,这种key value的形式
57 |
58 | ## 规则模板
59 | 规则模板将Whistle的rules里面的信息通过模板(@形式)传递给“规则模板”使用,类似于类似es6的模板字符串。
60 |
61 | 例如:本地/服务器Whistle的rules里面@name,就会传递name到规则模板,然后${name}就可以拿到name了
62 | 
63 |
64 | ## 模板配置
65 | 模板配置是一个JSON对象的格式,主要有以下三种用途:
66 | - 作为“规则模板”的数据来源,例如:
67 | ```
68 | # 模板配置为以下内容
69 | {
70 | "user1": 1,
71 | "user2": 2
72 | }
73 | # 然后在模板里可以${xxx}就可以拿到xxx对应的属性值了
74 | ${user1}
75 | ```
76 | - 账号规则里面可以通过$name,同样可以获取到name值
77 | - 替换功能,例如:
78 | 例如ip1: ip2,表示用ip2全局替换ip1。
79 | ```
80 | # 模板配置为以下内容时,10.1.2.3会被替换成10.4.5.6
81 | {
82 | "10.1.2.3": "10.4.5.6"
83 | }
84 | ```
85 |
--------------------------------------------------------------------------------
/docs/zh/admin/system.md:
--------------------------------------------------------------------------------
1 | # 系统
2 | ## 管理员
3 | 1. 管理员的初始用户名、密码主要是用来登录此管理后台页面,具体如下所示:
4 | - 账号:admin
5 | - 密码:123456
6 |
7 |
8 | 2. 您也可以在此页面重新设置用户名和密码,请妥善保管好您的密码。
9 |
10 | **注意:如果您忘记了用户名密码,可以执行如下脚本来重置密码为初始值
11 | ```n2 restart --reset```**
12 |
13 |
14 | ## 设置域名
15 | 您可以在这里设置Nohost服务器ip所对应的域名。
16 |
17 | 这样以后就可以直接通过域名来访问Nohost相关页面(如管理员页面,选择环境/抓包页面等)。
18 |
19 | 例如:可以设置 10.222.2.200 的域名为 imwebtest.test.com,这样就可以直接访问[http://imwebtest.test.com:8080/](http://imwebtest.test.com:8080/)去选择环境了。
20 |
21 | **注意:设置的域名 DNS 一定要指向该IP,否则可能出现不可用状态,`imwebtest.test.com` 只是示例域名不要直接使用。**
22 |
23 |
24 | ## Auth Key
25 | 因为第三方应用要调Nohost的接口,所以在请求头里会设置x-whistle-auth-key 为此值。
26 |
27 | 所以,这个Auth Key主要是用来进行用户鉴权。
28 |
29 | 例如:您可以设置为nohost@imweb
30 |
31 | ## 重启服务
32 | 如果您觉得最近的服务处理有一些慢,或者Nohost服务占用机器cpu内存较高,可以点击一下这个“重启”按钮来重启Nohost服务~
33 |
34 | *后续会支持设置定时重启。*
35 |
--------------------------------------------------------------------------------
/docs/zh/admin/whistle.md:
--------------------------------------------------------------------------------
1 | # Whistle
2 | Nohost 主进程里面运行了一个全局 Whistle 实例,所有请求都会经过该 Whistle,可以在里面对所有请求进行操作,如:给某个 cdn 域名统一设置 cors,或拦截某些上报请求 `xxx.report.com statusCode://204` 等。
3 |
4 | 该 Whistle 也可以用来管理全局等插件,包括系统里面用到等 Nohost 插件。
5 |
6 |
7 |
--------------------------------------------------------------------------------
/docs/zh/advance/README.md:
--------------------------------------------------------------------------------
1 | # 高级应用
--------------------------------------------------------------------------------
/docs/zh/api.md:
--------------------------------------------------------------------------------
1 | # API
2 | Nohost 也对外提供了接口,方便第三方平台(如:发布部署系统)对接 Nohost,可以对 Nohost 账号对环境进行增删查改。
3 |
4 | > 以下假设当前 Nohost 部署对域名和端口为 `imwebtest.test.com:8080`
5 |
6 | 首先,需要在管理员页面设置 `Auth Key` :
7 | 
8 |
9 | 假设设置的 `Auth Key` 为:`test@imweb`。
10 |
11 | ### 获取所有账号及环境列表
12 | 1. url: `http://imwebtest.test.com:8080/cgi-bin/list`
13 | 2. method: `GET`
14 | 3. 无需任何鉴权信息
15 |
16 | ### 获取当前选中的环境信息
17 | 1. url: `http://imwebtest.test.com:8080/cgi-bin/get-env`
18 | 2. method: `GET`
19 | 3. 无需任何鉴权信息
20 |
21 | ### 获取指定账号的环境列表
22 | 1. url: `http://imwebtest.test.com:8080/open-api/list`
23 | 2. method: `GET`
24 | 3. 鉴权参数,设置以下请求头:
25 | - `x-nohost-auth-key`: `test@imweb` (以实际 AuthKey 为准)
26 | - `x-nohost-account-name`: `test` (填入要添加环境的账号,如果环境名称包含非 ascii 字符,记得先 `encodeURIComponent(envName)`)
27 |
28 | ### 添加环境
29 | 1. url: `http://imwebtest.test.com:8080/open-api/add-env`
30 | > 添加到顶部:`http://imwebtest.test.com:8080/open-api/add-top-env`
31 | 2. method: `post`
32 | 3. 鉴权参数,设置以下请求头:
33 | - `x-nohost-auth-key`: `test@imweb` (以实际 AuthKey 为准)
34 | - `x-nohost-account-name`: `test` (填入要添加环境的账号,如果环境名称包含非 ascii 字符,记得先 `encodeURIComponent(envName)`)
35 | 4. 参数:
36 | - `name`: 环境名称,最好不要加空格
37 | - `value`: 环境内容
38 |
39 | ### 修改环境
40 | 1. url: `http://imwebtest.test.com:8080/open-api/modify-env`
41 | 2. method: `POST`
42 | 3. 鉴权参数,设置以下请求头:
43 | - `x-nohost-auth-key`: `test@imweb` (以实际 AuthKey 为准)
44 | - `x-nohost-account-name`: `test` (填入要添加环境的账号,如果环境名称包含非 ascii 字符,记得先 `encodeURIComponent(envName)`)
45 | 4. 参数:
46 | - `name`: 环境名称,最好不要加空格
47 | - `value`: 环境内容
48 |
49 | ### 修改环境名称
50 | 1. url: `http://imwebtest.test.com:8080/open-api/rename-env`
51 | 2. method: `POST`
52 | 3. 鉴权参数,设置以下请求头:
53 | - `x-nohost-auth-key`: `test@imweb` (以实际 AuthKey 为准)
54 | - `x-nohost-account-name`: `test` (填入要添加环境的账号,如果环境名称包含非 ascii 字符,记得先 `encodeURIComponent(envName)`)
55 | 4. 参数:
56 | - `name`: 当前环境名称
57 | - `newName`: 新的环境名称
58 |
59 | ### 删除环境
60 | 1. url: `http://imwebtest.test.com:8080/open-api/remove-env`
61 | 2. method: `POST`
62 | 3. 鉴权参数,设置以下请求头:
63 | - `x-nohost-auth-key`: `test@imweb` (以实际 AuthKey 为准)
64 | - `x-nohost-account-name`: `test` (填入要添加环境的账号,如果环境名称包含非 ascii 字符,记得先 `encodeURIComponent(envName)`)
65 | 4. 参数:
66 | - `name`: 环境名称
67 |
68 |
--------------------------------------------------------------------------------
/docs/zh/developer/README.md:
--------------------------------------------------------------------------------
1 | # 普通开发
--------------------------------------------------------------------------------
/docs/zh/developer/capture.md:
--------------------------------------------------------------------------------
1 | # 抓包调试
--------------------------------------------------------------------------------
/docs/zh/developer/customMenu.md:
--------------------------------------------------------------------------------
1 | # 如何生成自定义菜单
2 | 自定义菜单通过识别 `window.nohostContextMenuExtensions` 的值来进行生成自定义菜单
3 | `nohostContextMenuExtensions` 值类型如下:
4 | ``` ts
5 | interface MenuItem {
6 | name: string;
7 | title: string;
8 | autoHide?: boolean; // default true
9 | onClick: (e: MouseEvent & { hide: () => void }) => void; // 调用hide可以关闭菜单
10 | }
11 | type nohostContextMenuExtensions = MenuItem[];
12 | ```
13 | ***默认点击菜单会自动关闭菜单的,如果要手动关闭菜单,需要设置item.autoHide为false,并手动调用e.hide方法进行关闭***
14 |
15 | 可以通过 Whistle 插件或者 rule 的形式,注入 `window.nohostContextMenuExtensions`。
16 | > 建议不要直接对该属性赋值,不然可能存在覆盖其他插件的自定义菜单的可能。
17 |
18 | ***例子***
19 | 通过Whistle rule直接注入
20 | ```` ts
21 | * jsAppend://{test.js}
22 |
23 | ``` test.js
24 | ;(function() {
25 | // Xxx 表示插件的名称,如果插件 whsitle.test, 则为 window.__nohostTestPluginExtMenusDidMount__
26 | if (window.__nohostXxxPluginExtMenusDidMount__) {
27 | return;
28 | }
29 | window.__nohostXxxPluginExtMenusDidMount__ = true;
30 | if (Object.prototype.toString.call(window.nohostContextMenuExtensions) === '[object Array]') {
31 | window.nohostContextMenuExtensions.push(
32 | { name: '测试不关闭', title: '测试', autoHide: false, onClick: (e) => { console.log('测试'); } },
33 | { name: '测试关闭', title: '测试', onClick: (e) => { console.log('测试'); } },
34 | );
35 | } else {
36 | window.nohostContextMenuExtensions = [
37 | { name: '测试不关闭', title: '测试', autoHide: false, onClick: (e) => { console.log('测试'); } },
38 | { name: '测试关闭', title: '测试', onClick: (e) => { console.log('测试'); } }
39 | ];
40 | }
41 | })();
42 | ```
43 | ````
44 | 注意要在 `__nohostXxxPluginExtMenusDidMount__` 变量标记是否已经注册过,不然可能会反复注册,导致菜单重复出现
45 |
46 | 除了直接内联js,也支持外链的形式注入:
47 |
48 | 1. 通过cdn上的js注入
49 | ```` ts
50 | ``` ext-menu.html
51 |
52 | ```
53 |
54 | ke.qq.com htmlAppend://{whistle.xxx/ext-menu.html}
55 | ````
56 | 2. 通过插件内部的js注入
57 | ```` ts
58 | ke.qq.com htmlAppend://{whistle.xxx/ext-menu.html}
59 |
60 | ``` ext-menu.html
61 |
62 | ```
63 | ````
64 | `/...whistle-path.5b6af7b9884e1165.../` 是Whistle内部路径,要修改的是后面/whistle.xxx/path/to/menus.js的部分
65 |
66 | 效果:
67 |
68 | 
69 |
70 | 
71 |
72 |
--------------------------------------------------------------------------------
/docs/zh/developer/plugin.md:
--------------------------------------------------------------------------------
1 | # 安装插件
2 | Nohost 兼容所有 Whistle 插件(如何开发 Whistle 插件直接参考 [GitHub 文档](https://github.com/avwo/whistle)),并支持把插件安装在全局的 Whistle,或只对所有账号生效,也可以只对某个账号生效。
3 |
4 | ## 安装全局插件
5 | 通过 `npm` 或其它 `xnpm` 全局安装即可:
6 | ``` sh
7 | npm i -g whistle.xxxx
8 | ```
9 | > 或 `npm i -g @org/whistle.xxx`
10 |
11 | 安装后可以通过设置对 Nohost 域名及端口访问:`http://imwebtest.test.com:8080/whistle.xxx/`
12 |
13 | ## 安装所有账号的插件(要求 `v0.3.5` 及以上版本)
14 | 安装只针对所有账号生效对局部插件:
15 | ``` sh
16 | n2 i whistle.xxx @tnpm/whistle.yyy
17 | ```
18 | > 如果使用 tnpm 或 cnpm 等第三方命令,可以用 `n2 ti whistle.xxx @tnpm/whistle.yyy` 或 `n2 ci whistle.xxx @tnpm/whistle.yyy`
19 | 安装后可以通过 `http://imwebtest.test.com:8080/account/account-name/whistle.xxx/`,`account-name` 表示某个账号的名称
20 |
21 | ## 安装指定账号的插件(要求 `v0.3.5` 及以上版本)
22 | 只对某个账号生效对插件:
23 | ``` sh
24 | n2 i whistle.xxx @tnpm/whistle.yyy -a account-name
25 | ```
26 |
--------------------------------------------------------------------------------
/docs/zh/developer/usage.md:
--------------------------------------------------------------------------------
1 | # 如何使用
--------------------------------------------------------------------------------
/docs/zh/quickstart.md:
--------------------------------------------------------------------------------
1 | ## 准备
2 | 安装 Nohost 之前,建议先做好以下工作:
3 |
4 | 1. 准备一台服务器,假设IP为:10.1.2.3(以你自己的服务器为准,建议4核8G以上的配置)
5 | 2. 准备一个域名(以下假设为:imwebtest.test.com),并把 DNS 指向上述服务器(10.1.2.3)
6 | 3. 收集涉及域名的证书对,只支持 `xxx.key` 和 `xxx.crt`(非必须,但建议用正式的证书,否则要么 Nohost 里面无法查看 HTTPS 的内容,要么每个访问 Nohost 的客户端都要安装一遍根证书)
7 |
8 | > 申请域名的好处是可以直接用域名访问管理及账号页面,手机也可以通过域名设置代理访问 Nohost,方便记忆及输入
9 |
10 | ## 安装
11 | 首先,需要安装Node(建议使用最新的LTS版本):[Node](https://nodejs.org/en/)
12 |
13 | Node安装成功后,通过npm安装 `Nohost`:
14 | ``` sh
15 | npm i -g @nohost/server --registry=https://r.npm.taobao.org
16 | ```
17 | 安装完成后执行启动命令:
18 | ``` sh
19 | n2 start
20 | ```
21 | > Nohost 的默认端口为 8080,如果需要自定义端口,可以通过 `n2 restart -p 80` 设置。
22 | > 如果命令行提示没有对应命令,检查下系统环境变量 `PATH` 配置,看看 Nohost 安装后生成的命令所在目录是否已添加到 `PATH`。
23 |
24 | 重启 `Nohost`:
25 | ``` sh
26 | n2 restart
27 | ```
28 |
29 | 停止 `Nohost`:
30 | ``` sh
31 | n2 stop
32 | ```
33 |
34 | 重置管理员账号:
35 | ``` sh
36 | n2 restart --reset
37 | ```
38 |
39 | ## 配置
40 |
41 | 安装启动成功后,打开管理员页面 `http://10.1.2.3:8080/admin.html#system/administrator`,输入默认用户名(`admin`)和密码(`123456`),打开系统配置后:
42 | > 其中 `10.1.2.3` 表示Nohost运行的服务器IP,具体根据实际 ServerIP 替换
43 | 1. 修改管理员的默认账号名和密码(**不建议使用默认账号及密码,如果忘记管理员账号名或密码,可以通过 `n2 restart --reset` 重置**)
44 | 2. 设置Nohost的域名(将申请的域名填上,如果需要设置多个域名,可以通过逗号 `,` 分隔)
45 | 3. 上传涉及的 key 和证书(证书只支持 `.crt` 格式)
46 |
47 | ## 访问
48 | Nohost 本身就是一个代理,可以直接配置浏览器或系统代理访问,也可以通过 Nginx反向代理访问,为方便大家使用,针对不同的人群可以使用不同的方案(以下用 `imwebtest.test.com` 表示 Nohost 的域名,具体域名需要自己申请及设置)。
49 |
50 | #### 前端开发
51 | 前端开发建议使用最新版的 [Whistle](https://github.com/avwo/whistle),可以通过以下两种方式访问 Nohost:
52 |
53 | 1. 直接在 Whistle 上配置远程规则
54 | ``` txt
55 | @http://imwebtest.test.com:8080/whistle.nohost/cgi-bin/plugin-rules
56 | ```
57 | > 上述配置表示 Whistle 从 `http://imwebtest.test.com:8080/whistle.nohost/cgi-bin/plugin-rules` 获取 Nohost 的生成的入口规则,并且如果 Nohost 规则有变会自动更新规则,这些规则是由 Nohost 上传证书的域名及界面 `配置/入口配置` 配置的规则自动生成(具体参见后面的**配置**),这些规则可以自动过滤掉无关请求,只会把相关的请求转到Nohost。
58 |
59 | 当然这种直接手动配置在 Whistle 上还不是最好的方式,更建议的方式是把这些规则集成到插件里面,这样开发者只需安装插件即可。
60 | 2. **【强烈推荐】** 集成 Whistle 插件,具体参考:[https://github.com/nohosts/whistle.nohost-imweb/blob/master/dev.md](https://github.com/nohosts/whistle.nohost-imweb/blob/master/dev.md)
61 |
62 | #### 后台开发
63 | 后台开发推荐使用 Chrome 的 [SwitchyOmega](https://chrome.google.com/webstore/detail/proxy-switchyomega/padekgcemlokbadohgkifijomclgjgif) 配置代理规则 (如上述代理配置 `imwebtest.test.com` + `8080`),如果不想所有请求都转到 Nohost,可以配置 SwitchyOmega 的自动切换或者用PAC脚本代替,也可以参考 `nohost-client` 打包一个客户端:[https://github.com/nohosts/client](https://github.com/nohosts/client)。手机端可以直接配代理,或者通过 VPN App 设置代理,如 iPhone 可以用 `detour`。
64 |
65 | #### 其他人员
66 | 非开发人员尽量使用客户端、APP、或通过外网转发的方式,减少他们的接入成本,如何打包客户端参考:[https://github.com/nohosts/client](https://github.com/nohosts/client);手机等同后台开发的配置方式。
67 |
68 | #### 外网访问
69 | 一般 Nohost 是部署在公司内网,外网是不可以直接访问,需要通过接入层(如:Nginx)转发。
70 |
--------------------------------------------------------------------------------
/docs/zh/users/README.md:
--------------------------------------------------------------------------------
1 | # 普通用户
--------------------------------------------------------------------------------
/docs/zh/users/others.md:
--------------------------------------------------------------------------------
1 | # 外网用户
--------------------------------------------------------------------------------
/docs/zh/users/pd.md:
--------------------------------------------------------------------------------
1 | # 产品经理
2 |
--------------------------------------------------------------------------------
/docs/zh/users/tester.md:
--------------------------------------------------------------------------------
1 | # 测试人员
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
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 | };
90 |
--------------------------------------------------------------------------------
/lib/config.js:
--------------------------------------------------------------------------------
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 | };
43 |
--------------------------------------------------------------------------------
/lib/main/cgi/getSettings.js:
--------------------------------------------------------------------------------
1 |
2 | module.exports = async (ctx) => {
3 | const { storage } = ctx;
4 | ctx.body = {
5 | ec: 0,
6 | admin: {
7 | username: storage.getAdmin().username,
8 | },
9 | domain: storage.getDomain(),
10 | };
11 | };
12 |
--------------------------------------------------------------------------------
/lib/main/cgi/getVersion.js:
--------------------------------------------------------------------------------
1 | const pkg = require('../../../package.json');
2 |
3 | const { pid } = process;
4 |
5 | module.exports = (ctx) => {
6 | ctx.body = {
7 | ec: 0,
8 | pid,
9 | version: pkg.version,
10 | latestVersion: ctx.storage.latestVersion,
11 | };
12 | };
13 |
--------------------------------------------------------------------------------
/lib/main/cgi/login.js:
--------------------------------------------------------------------------------
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 | };
15 |
--------------------------------------------------------------------------------
/lib/main/cgi/restart.js:
--------------------------------------------------------------------------------
1 |
2 | module.exports = (ctx) => {
3 | ctx.whistleMgr.restart();
4 | ctx.body = { ec: 0 };
5 | };
6 |
--------------------------------------------------------------------------------
/lib/main/cgi/setAdmin.js:
--------------------------------------------------------------------------------
1 | module.exports = async (ctx) => {
2 | ctx.body = { ec: 0 };
3 | if (ctx.storage.setAdmin(ctx.request.body)) {
4 | ctx.whistleMgr.restart();
5 | }
6 | };
7 |
--------------------------------------------------------------------------------
/lib/main/cgi/setDomain.js:
--------------------------------------------------------------------------------
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 | };
14 |
--------------------------------------------------------------------------------
/lib/main/index.js:
--------------------------------------------------------------------------------
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();
95 |
--------------------------------------------------------------------------------
/lib/main/router.js:
--------------------------------------------------------------------------------
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 | };
80 |
--------------------------------------------------------------------------------
/lib/main/whistleMgr.js:
--------------------------------------------------------------------------------
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 | };
57 |
--------------------------------------------------------------------------------
/lib/main/worker.js:
--------------------------------------------------------------------------------
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 | };
53 |
--------------------------------------------------------------------------------
/lib/main/workerNum.js:
--------------------------------------------------------------------------------
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 | }
12 |
--------------------------------------------------------------------------------
/lib/plugins/account/whistle.share/package.json:
--------------------------------------------------------------------------------
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 | }
21 |
--------------------------------------------------------------------------------
/lib/plugins/account/whistle.storage/index.js:
--------------------------------------------------------------------------------
1 |
2 | const { Pool } = require('socketx');
3 | const { encode } = require('simpleproto');
4 | const crc32 = require('crc32');
5 |
6 | const UID_RE = /^[\w.-]{1,64}$/;
7 |
8 | exports.uiServer = (server, { data }) => {
9 | const { storageServer } = data || '';
10 | const len = storageServer && storageServer.length;
11 | const rules = len ? '* whistle.storage:// includeFilter://reqH:x-whistle-nohost-storage-uid=/^[\\w.-]{1,64}$/' : '';
12 | server.on('request', (_, res) => {
13 | res.writeHead(200, { 'content-type': 'text/plain; charset=utf8' });
14 | res.end(rules);
15 | });
16 | };
17 |
18 | exports.resStatsServer = (server, { data }) => {
19 | const { storageServer } = data || '';
20 | const len = storageServer && storageServer.length;
21 | if (!len) {
22 | return;
23 | }
24 | const pool = new Pool();
25 | storageServer.forEach((opt) => {
26 | opt.idleTimeout = 30000;
27 | });
28 | server.on('request', (req) => {
29 | req.getSession(async (s) => {
30 | if (s) {
31 | const uid = s.req.headers['x-whistle-nohost-storage-uid'];
32 | if (!uid || !UID_RE.test(uid)) {
33 | return;
34 | }
35 | const index = len > 1 ? (parseInt(crc32(uid), 16) % len) : 0;
36 | try {
37 | const socket = await pool.connect(storageServer[index]);
38 | socket.write(encode(s));
39 | } catch (e) {}
40 | }
41 | });
42 | });
43 | };
44 |
--------------------------------------------------------------------------------
/lib/plugins/account/whistle.storage/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "whistle.storage",
3 | "version": "1.0.0",
4 | "whistleConfig": {
5 | "hideLongProtocol": true,
6 | "hideShortProtocol": true
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/lib/plugins/account/whistle.storage/rules.txt:
--------------------------------------------------------------------------------
1 | @whistle.storage
--------------------------------------------------------------------------------
/lib/plugins/whistle.nohost/_rules.txt:
--------------------------------------------------------------------------------
1 |
2 | ``` whistle.nohost/inject.html
3 |
4 |
32 | ```
33 |
34 | ``` whistle.nohost/none.html
35 |
38 | ```
39 |
--------------------------------------------------------------------------------
/lib/plugins/whistle.nohost/index.js:
--------------------------------------------------------------------------------
1 | exports.uiServer = require('./lib/uiServer');
2 | exports.rulesServer = require('./lib/rulesServer');
3 | exports.tunnelRulesServer = require('./lib/tunnelRulesServer');
4 |
--------------------------------------------------------------------------------
/lib/plugins/whistle.nohost/initial.js:
--------------------------------------------------------------------------------
1 | const initAccountMgr = require('./lib/accountMgr');
2 | const initEnvMgr = require('./lib/envMgr');
3 | const { initPlugin } = require('./lib/util');
4 |
5 | module.exports = (options) => {
6 | initPlugin(options);
7 | const accountMgr = initAccountMgr(options);
8 | initEnvMgr(accountMgr);
9 | };
10 |
--------------------------------------------------------------------------------
/lib/plugins/whistle.nohost/lib/envMgr.js:
--------------------------------------------------------------------------------
1 | const LRU = require('lru-cache');
2 | const {
3 | COOKIE_NAME,
4 | WHISTLE_ENV_HEADER,
5 | decodeURIComponentSafe,
6 | getClientId,
7 | isFromComposer,
8 | } = require('./util');
9 |
10 | const followers = new LRU({ max: 10000, maxAge: 1000 * 60 * 30 });
11 | const cache = new LRU({ max: 10000 });
12 | let accountMgr;
13 |
14 | class EnvMgr {
15 | checkEnvName({ envList }, envName) {
16 | if (!envName) {
17 | return '';
18 | }
19 | return envList.some(({ name }) => name === envName) ? envName : '';
20 | }
21 |
22 | setFollower(followId, ctx) {
23 | const clientId = getClientId(ctx);
24 | if (clientId !== followId) {
25 | followers.set(followId, clientId);
26 | }
27 | }
28 |
29 | unfollow(ctx) {
30 | followers.del(getClientId(ctx));
31 | }
32 |
33 | getFollower(ctx) {
34 | return followers.get(getClientId(ctx));
35 | }
36 |
37 | setEnv(ip, name, envName) {
38 | const account = accountMgr.getAccount(name);
39 | if (!account) {
40 | cache.set(ip, '');
41 | return;
42 | }
43 | envName = this.checkEnvName(account, envName);
44 | const env = { name, envName };
45 | cache.set(ip, env);
46 | return env;
47 | }
48 |
49 | getEnvFromCookie(ctx, withAccount) {
50 | let env = decodeURIComponentSafe(ctx.cookies.get(COOKIE_NAME));
51 | if (!env) {
52 | return '';
53 | }
54 | const index = env.indexOf('/');
55 | let name = env;
56 | let envName;
57 | if (index !== -1) {
58 | name = env.substring(0, index);
59 | envName = env.substring(index + 1);
60 | }
61 | const account = accountMgr.getAccount(name);
62 | if (!account) {
63 | return '';
64 | }
65 | envName = this.checkEnvName(account, envName);
66 | env = { name, envName };
67 | return withAccount ? { account, env } : { name, envName };
68 | }
69 |
70 | getEnv(ctx) {
71 | let env = cache.get(getClientId(ctx));
72 | if (env != null) {
73 | if (env) {
74 | const { name, envName } = env;
75 | const account = accountMgr.getAccount(name);
76 | if (!account || !this.checkEnvName(account, envName)) {
77 | env.envName = '';
78 | }
79 | }
80 | return env;
81 | }
82 | env = this.getEnvFromCookie(ctx);
83 | cache.set(getClientId(ctx), env);
84 | return env;
85 | }
86 |
87 | getEnvOnly(ctx) {
88 | return cache.get(getClientId(ctx));
89 | }
90 |
91 | getEnvByHeader(ctx) {
92 | let name = decodeURIComponentSafe(ctx.get(WHISTLE_ENV_HEADER));
93 | if (!name) {
94 | return (isFromComposer(ctx) && this.getEnvFromCookie(ctx, true)) || '';
95 | }
96 | const index = name.indexOf('/');
97 | let envName = '';
98 | if (index !== -1) {
99 | envName = name.substring(index + 1);
100 | name = name.substring(0, index);
101 | }
102 | const account = accountMgr.getAccount(name);
103 | if (!account) {
104 | return '';
105 | }
106 | envName = this.checkEnvName(account, envName);
107 | return { account, env: { name, envName } };
108 | }
109 | }
110 |
111 | module.exports = (mgr) => {
112 | accountMgr = mgr;
113 | module.exports = new EnvMgr();
114 | };
115 |
--------------------------------------------------------------------------------
/lib/plugins/whistle.nohost/lib/rulesServer.js:
--------------------------------------------------------------------------------
1 | const Koa = require('koa');
2 | const accountMgr = require('./accountMgr');
3 | const envMgr = require('./envMgr');
4 | const whistleMgr = require('./whistleMgr');
5 | const {
6 | COOKIE_NAME,
7 | ENV_MAX_AGE,
8 | WHISTLE_ENV_HEADER,
9 | WHISTLE_RULE_VALUE,
10 | getRuleValue,
11 | getDomain,
12 | } = require('./util');
13 |
14 | const getCookie = (value, maxAge, hostname) => {
15 | return {
16 | whistleEnvCookie: {
17 | [COOKIE_NAME]: {
18 | value,
19 | maxAge,
20 | domain: getDomain(hostname),
21 | path: '/',
22 | },
23 | },
24 | };
25 | };
26 |
27 | module.exports = (server) => {
28 | const app = new Koa();
29 | app.proxy = true;
30 | app.silent = true;
31 | app.use(async (ctx) => {
32 | let { account, env } = envMgr.getEnvByHeader(ctx);
33 | if (!account && !ctx.get(WHISTLE_ENV_HEADER)) {
34 | env = envMgr.getEnv(ctx);
35 | account = env && accountMgr.getAccount(env.name);
36 | }
37 | const hostname = (ctx.get('host') || '').split(':')[0];
38 | const noInject = getRuleValue(ctx) === 'none' || ctx.get('x-whistle-nohost-hide');
39 | const injectRule = `htmlPrepend://{whistle.nohost/${noInject ? 'none' : 'inject'}.html} enable://safeHtml`;
40 | if (!account) {
41 | ctx.body = {
42 | rules: `* resCookies://{whistleEnvCookie} ${injectRule}`,
43 | values: getCookie('', -ENV_MAX_AGE, hostname),
44 | };
45 | return;
46 | }
47 | const name = account && account.name;
48 | let envHeader = '';
49 | let envKey = `${name}/`;
50 | const { envName } = env;
51 | const reqHeaders = {};
52 |
53 | if (envName) {
54 | envKey += envName;
55 | let { rules, headers } = accountMgr.getRules(account.name, envName);
56 | if (headers) {
57 | headers = JSON.stringify(headers);
58 | rules += `\n* reqHeaders://(${headers})`;
59 | }
60 | if (rules) {
61 | reqHeaders[WHISTLE_RULE_VALUE] = encodeURIComponent(rules);
62 | }
63 | }
64 | reqHeaders[WHISTLE_ENV_HEADER] = encodeURIComponent(envKey);
65 | const {
66 | port,
67 | remoteAddrHead,
68 | remotePortHead,
69 | } = await whistleMgr.fork(account);
70 | const {
71 | remoteAddress,
72 | remotePort,
73 | } = ctx.req.originalReq;
74 | reqHeaders[remoteAddrHead] = remoteAddress;
75 | reqHeaders[remotePortHead] = remotePort;
76 | envHeader = `reqHeaders://(${JSON.stringify(reqHeaders)})`;
77 | const proxyUrl = `internal-proxy://127.0.0.1:${port}`;
78 | const cookie = getCookie(envKey, ENV_MAX_AGE, hostname);
79 | ctx.body = {
80 | rules: `* ${proxyUrl} ${envHeader} ${injectRule} ${cookie ? 'resCookies://{whistleEnvCookie}' : ''}`,
81 | values: cookie,
82 | };
83 | });
84 | server.on('request', app.callback());
85 | };
86 |
--------------------------------------------------------------------------------
/lib/plugins/whistle.nohost/lib/tunnelRulesServer.js:
--------------------------------------------------------------------------------
1 | const Koa = require('koa');
2 | const accountMgr = require('./accountMgr');
3 | const envMgr = require('./envMgr');
4 | const whistleMgr = require('./whistleMgr');
5 | const {
6 | WHISTLE_ENV_HEADER,
7 | WHISTLE_RULE_VALUE,
8 | } = require('./util');
9 |
10 | const SEP_RE = /\s+/;
11 | const CRLF_RE = /\s*[\r\n]+\s*/;
12 | const CAPTURE_RE = /^(?:[^\r\n]*\s)?enable:\/\/(?:capture|https|intercept)(?:\s|$)/m;
13 |
14 | module.exports = (server) => {
15 | const app = new Koa();
16 | app.proxy = true;
17 | app.silent = true;
18 | app.use(async (ctx) => {
19 | let { account, env } = envMgr.getEnvByHeader(ctx);
20 | if (!account) {
21 | env = envMgr.getEnvOnly(ctx);
22 | account = env && accountMgr.getAccount(env.name);
23 | }
24 | let proxyUrl = '';
25 | if (account) {
26 | const headers = {};
27 | let envValue = `${account.name}/`;
28 | if (env) {
29 | const { envName } = env;
30 | if (envName) {
31 | envValue = `${envValue}${envName}`;
32 | const { rules } = accountMgr.getRules(account.name, envName);
33 | if (rules) {
34 | // 设置了拦截https请求,则所有该环境的请求都开启
35 | if (CAPTURE_RE.test(rules)) {
36 | const capRules = [];
37 | rules.trim().split(CRLF_RE).forEach((line) => {
38 | if (CAPTURE_RE.test(line)) {
39 | line.trim().split(SEP_RE).forEach(p => {
40 | if (p) {
41 | capRules.push(`enable://capture ${p}`);
42 | }
43 | });
44 | }
45 | });
46 | ctx.body = capRules.join('\n');
47 | return;
48 | }
49 | headers[WHISTLE_RULE_VALUE] = encodeURIComponent(rules);
50 | }
51 | }
52 | }
53 | headers[WHISTLE_ENV_HEADER] = encodeURIComponent(envValue);
54 | const {
55 | port,
56 | remoteAddrHead,
57 | remotePortHead,
58 | } = await whistleMgr.fork(account);
59 | const {
60 | remoteAddress,
61 | remotePort,
62 | } = ctx.req.originalReq;
63 | headers[remoteAddrHead] = remoteAddress;
64 | headers[remotePortHead] = remotePort;
65 | const envHeader = `reqHeaders://(${JSON.stringify(headers)})`;
66 | proxyUrl = `internal-proxy://127.0.0.1:${port}`;
67 | ctx.body = `* ${proxyUrl} ${envHeader}`;
68 | }
69 | });
70 | server.on('request', app.callback());
71 | };
72 |
--------------------------------------------------------------------------------
/lib/plugins/whistle.nohost/lib/uiServer/cgi/account/changePassword.js:
--------------------------------------------------------------------------------
1 | module.exports = (ctx) => {
2 | const { password } = ctx.request.body;
3 | if (ctx.accountMgr.changePassword(ctx.user.name, password)) {
4 | ctx.success();
5 | } else {
6 | ctx.error();
7 | }
8 | };
9 |
--------------------------------------------------------------------------------
/lib/plugins/whistle.nohost/lib/uiServer/cgi/admin/activeAccount.js:
--------------------------------------------------------------------------------
1 |
2 | module.exports = (ctx) => {
3 | const { name, active } = ctx.request.body;
4 | if (ctx.accountMgr.activeAccount(name, active !== 'false')) {
5 | ctx.success();
6 | } else {
7 | ctx.error();
8 | }
9 | };
10 |
--------------------------------------------------------------------------------
/lib/plugins/whistle.nohost/lib/uiServer/cgi/admin/addAccount.js:
--------------------------------------------------------------------------------
1 |
2 | module.exports = (ctx) => {
3 | if (ctx.accountMgr.addAccount(ctx.request.body)) {
4 | ctx.success();
5 | } else {
6 | ctx.error();
7 | }
8 | };
9 |
--------------------------------------------------------------------------------
/lib/plugins/whistle.nohost/lib/uiServer/cgi/admin/allAccounts.js:
--------------------------------------------------------------------------------
1 |
2 | module.exports = (ctx) => {
3 | const { accountMgr } = ctx;
4 | ctx.body = {
5 | ec: 0,
6 | enableGuest: accountMgr.isEnableGuest(),
7 | list: accountMgr.getAllAccounts(),
8 | };
9 | };
10 |
--------------------------------------------------------------------------------
/lib/plugins/whistle.nohost/lib/uiServer/cgi/admin/changeNotice.js:
--------------------------------------------------------------------------------
1 | module.exports = (ctx) => {
2 | const { name, notice } = ctx.request.body;
3 | if (ctx.accountMgr.changeNotice(name, notice)) {
4 | ctx.success();
5 | } else {
6 | ctx.error();
7 | }
8 | };
9 |
--------------------------------------------------------------------------------
/lib/plugins/whistle.nohost/lib/uiServer/cgi/admin/changePassword.js:
--------------------------------------------------------------------------------
1 | module.exports = (ctx) => {
2 | const { name, password } = ctx.request.body;
3 | if (ctx.accountMgr.changePassword(name, password)) {
4 | ctx.success();
5 | } else {
6 | ctx.error();
7 | }
8 | };
9 |
--------------------------------------------------------------------------------
/lib/plugins/whistle.nohost/lib/uiServer/cgi/admin/enableGuest.js:
--------------------------------------------------------------------------------
1 | module.exports = (ctx) => {
2 | const { enableGuest } = ctx.request.body;
3 | ctx.accountMgr.enableGuest(!!enableGuest);
4 | ctx.success();
5 | };
6 |
--------------------------------------------------------------------------------
/lib/plugins/whistle.nohost/lib/uiServer/cgi/admin/getAuthKey.js:
--------------------------------------------------------------------------------
1 | module.exports = (ctx) => {
2 | ctx.body = { ec: 0, authKey: ctx.accountMgr.getAuthKey() };
3 | };
4 |
--------------------------------------------------------------------------------
/lib/plugins/whistle.nohost/lib/uiServer/cgi/admin/getSettings.js:
--------------------------------------------------------------------------------
1 | module.exports = (ctx) => {
2 | const {
3 | jsonDataStr,
4 | rulesTpl,
5 | defaultRules,
6 | accountRules,
7 | testRules,
8 | entryPatterns,
9 | specPattern,
10 | } = ctx.accountMgr;
11 | ctx.body = {
12 | ec: 0,
13 | jsonData: jsonDataStr,
14 | authKey: ctx.accountMgr.getAuthKey(),
15 | rulesTpl,
16 | defaultRules,
17 | accountRules,
18 | testRules,
19 | entryPatterns,
20 | specPattern,
21 | };
22 | };
23 |
--------------------------------------------------------------------------------
/lib/plugins/whistle.nohost/lib/uiServer/cgi/admin/login.js:
--------------------------------------------------------------------------------
1 | const { checkLogin } = require('../../../../../../util/login');
2 |
3 | module.exports = async (ctx, next) => {
4 | const { username, password } = ctx.admin;
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 | };
15 |
--------------------------------------------------------------------------------
/lib/plugins/whistle.nohost/lib/uiServer/cgi/admin/move.js:
--------------------------------------------------------------------------------
1 |
2 | module.exports = (ctx) => {
3 | const { fromName, toName } = ctx.request.body;
4 | if (ctx.accountMgr.moveAccount(fromName, toName)) {
5 | ctx.success();
6 | } else {
7 | ctx.error();
8 | }
9 | };
10 |
--------------------------------------------------------------------------------
/lib/plugins/whistle.nohost/lib/uiServer/cgi/admin/removeAccount.js:
--------------------------------------------------------------------------------
1 | module.exports = (ctx) => {
2 | const { name } = ctx.request.body;
3 | if (ctx.accountMgr.removeAccount(name)) {
4 | ctx.whistleMgr.kill(name);
5 | ctx.success();
6 | } else {
7 | ctx.error();
8 | }
9 | };
10 |
--------------------------------------------------------------------------------
/lib/plugins/whistle.nohost/lib/uiServer/cgi/admin/setAccountRules.js:
--------------------------------------------------------------------------------
1 | module.exports = (ctx) => {
2 | const { rules } = ctx.request.body;
3 | ctx.accountMgr.setAccountRules(rules);
4 | ctx.body = { ec: 0 };
5 | };
6 |
--------------------------------------------------------------------------------
/lib/plugins/whistle.nohost/lib/uiServer/cgi/admin/setAuthKey.js:
--------------------------------------------------------------------------------
1 | module.exports = (ctx) => {
2 | const { authKey } = ctx.request.body;
3 | ctx.accountMgr.setAuthKey(authKey);
4 | ctx.body = { ec: 0 };
5 | };
6 |
--------------------------------------------------------------------------------
/lib/plugins/whistle.nohost/lib/uiServer/cgi/admin/setDefaultRules.js:
--------------------------------------------------------------------------------
1 | module.exports = (ctx) => {
2 | const { defaultRules } = ctx.request.body;
3 | ctx.accountMgr.setDefaultRules(defaultRules);
4 | ctx.body = { ec: 0 };
5 | };
6 |
--------------------------------------------------------------------------------
/lib/plugins/whistle.nohost/lib/uiServer/cgi/admin/setEntryPatterns.js:
--------------------------------------------------------------------------------
1 | module.exports = (ctx) => {
2 | const { entryPatterns } = ctx.request.body;
3 | ctx.accountMgr.setEntryPatterns(entryPatterns);
4 | ctx.body = { ec: 0 };
5 | ctx.updateRules();
6 | };
7 |
--------------------------------------------------------------------------------
/lib/plugins/whistle.nohost/lib/uiServer/cgi/admin/setJsonData.js:
--------------------------------------------------------------------------------
1 | module.exports = (ctx) => {
2 | const { jsonData } = ctx.request.body;
3 | ctx.accountMgr.setJsonData(jsonData);
4 | ctx.body = { ec: 0 };
5 | };
6 |
--------------------------------------------------------------------------------
/lib/plugins/whistle.nohost/lib/uiServer/cgi/admin/setRulesTpl.js:
--------------------------------------------------------------------------------
1 | module.exports = (ctx) => {
2 | const { rulesTpl } = ctx.request.body;
3 | ctx.accountMgr.setRulesTpl(rulesTpl);
4 | ctx.body = { ec: 0 };
5 | };
6 |
--------------------------------------------------------------------------------
/lib/plugins/whistle.nohost/lib/uiServer/cgi/admin/setTestRules.js:
--------------------------------------------------------------------------------
1 | module.exports = (ctx) => {
2 | const { testRules } = ctx.request.body;
3 | ctx.accountMgr.setTestRules(testRules);
4 | ctx.body = { ec: 0 };
5 | };
6 |
--------------------------------------------------------------------------------
/lib/plugins/whistle.nohost/lib/uiServer/cgi/admin/specPattern.js:
--------------------------------------------------------------------------------
1 | module.exports = (ctx) => {
2 | ctx.accountMgr.setSpecPattern(ctx.request.body.specPattern);
3 | ctx.body = { ec: 0 };
4 | };
5 |
--------------------------------------------------------------------------------
/lib/plugins/whistle.nohost/lib/uiServer/cgi/allowlist.js:
--------------------------------------------------------------------------------
1 |
2 | module.exports = (ctx) => {
3 | ctx.body = ctx.accountMgr.allowlist;
4 | };
5 |
--------------------------------------------------------------------------------
/lib/plugins/whistle.nohost/lib/uiServer/cgi/entryRules.js:
--------------------------------------------------------------------------------
1 |
2 | module.exports = (ctx) => {
3 | ctx.body = ctx.accountMgr.entryRules;
4 | };
5 |
--------------------------------------------------------------------------------
/lib/plugins/whistle.nohost/lib/uiServer/cgi/follow.js:
--------------------------------------------------------------------------------
1 |
2 | const { getClientId, isClientId } = require('../../util');
3 |
4 | module.exports = (ctx) => {
5 | const { followId } = ctx.request.query;
6 | if (isClientId(followId)) {
7 | ctx.envMgr.setFollower(followId, ctx);
8 | ctx.type = 'html';
9 | ctx.body = '
设置成功,点击调整到选择环境页面
'; 10 | return; 11 | } 12 | ctx.body = { 13 | ec: 0, 14 | clientId: getClientId(ctx), 15 | followerIp: ctx.envMgr.getFollower(ctx), 16 | }; 17 | }; 18 | -------------------------------------------------------------------------------- /lib/plugins/whistle.nohost/lib/uiServer/cgi/getEnv.js: -------------------------------------------------------------------------------- 1 | 2 | const { getClientId } = require('../../util'); 3 | 4 | module.exports = (ctx) => { 5 | const curEnv = ctx.envMgr.getEnv(ctx) || null; 6 | ctx.body = { 7 | ec: 0, 8 | curEnv, 9 | clientIp: ctx.ip, 10 | clientId: getClientId(ctx), 11 | }; 12 | }; 13 | -------------------------------------------------------------------------------- /lib/plugins/whistle.nohost/lib/uiServer/cgi/list.js: -------------------------------------------------------------------------------- 1 | const Limiter = require('async-limiter'); 2 | const { gzip } = require('zlib'); 3 | const LRU = require('lru-cache'); 4 | 5 | const limiter = new Limiter({ concurrency: 10 }); 6 | const cache = new LRU({ max: 2 }); 7 | 8 | const compress = (body) => { 9 | let promise = cache.get(body); 10 | if (!promise) { 11 | promise = new Promise((resolve, reject) => { 12 | limiter.push((done) => { 13 | gzip(body, (err, buf) => { 14 | done(); 15 | if (err) { 16 | delete cache[body]; 17 | reject(err); 18 | } else { 19 | resolve(buf); 20 | } 21 | }); 22 | }); 23 | }); 24 | cache.set(body, promise); 25 | } 26 | return promise; 27 | }; 28 | 29 | module.exports = async (ctx) => { 30 | const curEnv = ctx.envMgr.getEnv(ctx); 31 | const list = ctx.accountMgr.getAccountList(ctx.request.query.parsed); 32 | let body = JSON.stringify({ 33 | admin: ctx.admin, 34 | ec: 0, 35 | baseUrl: ctx.baseUrl, 36 | curEnv, 37 | list, 38 | }); 39 | body = await compress(body); 40 | ctx.set('content-encoding', 'gzip'); 41 | ctx.set('content-type', 'application/json'); 42 | ctx.body = body; 43 | }; 44 | -------------------------------------------------------------------------------- /lib/plugins/whistle.nohost/lib/uiServer/cgi/patterns.js: -------------------------------------------------------------------------------- 1 | 2 | module.exports = (ctx) => { 3 | const { 4 | patterns, 5 | preRules, 6 | postRules, 7 | } = ctx.accountMgr; 8 | ctx.body = { 9 | ec: 0, 10 | patterns, 11 | preRules, 12 | postRules, 13 | }; 14 | }; 15 | -------------------------------------------------------------------------------- /lib/plugins/whistle.nohost/lib/uiServer/cgi/pluginRules.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | 4 | const resolveValue = (text) => { 5 | return text.substring(text.indexOf('```')); 6 | }; 7 | const INJECT_HTML = resolveValue(fs.readFileSync(path.join(__dirname, '../../../_rules.txt'), { encoding: 'utf8' })); // eslint-disable-line 8 | const INJECT_RULE = 'htmlPrepend://{whistle.nohost/inject.html}'; 9 | const WHISTLE_HOST = 'local.whistlejs.com'; 10 | let curBaseUrl; 11 | let curPatterns; 12 | let curRules; 13 | 14 | module.exports = (ctx) => { 15 | const { 16 | patterns, 17 | getBaseUrl, 18 | getDomainList, 19 | preRules, 20 | postRules, 21 | } = ctx.accountMgr; 22 | const curRuntimeId = ctx.get('x-whistle-runtime-id'); 23 | const isSelf = curRuntimeId === ctx.runtimeId; 24 | const baseUrl = !isSelf && getBaseUrl(); 25 | const domainList = getDomainList().concat([WHISTLE_HOST]); 26 | if (baseUrl) { 27 | if (curBaseUrl !== baseUrl || curPatterns !== patterns) { 28 | curBaseUrl = baseUrl; 29 | curPatterns = patterns; 30 | let ignorePatterns = []; 31 | const injectRule = [`internal-proxy://${baseUrl} ${INJECT_RULE} enable://clientId|multiClient|safeHtml`]; 32 | const normalRule = [`internal-proxy://${baseUrl} enable://clientId|multiClient`]; 33 | curRules = []; 34 | patterns.forEach((item) => { 35 | if (item.ignore) { 36 | ignorePatterns.push(`excludeFilter://${item.pattern}`); 37 | } else if (item.button) { 38 | injectRule.push(item.pattern); 39 | } else { 40 | normalRule.push(item.pattern); 41 | } 42 | }); 43 | ignorePatterns = ignorePatterns.join(' '); 44 | if (normalRule.length > 1) { 45 | normalRule.push(ignorePatterns); 46 | curRules.push(normalRule.join(' ')); 47 | } 48 | if (injectRule.length > 1) { 49 | injectRule.push(ignorePatterns); 50 | curRules.push(injectRule.join(' ')); 51 | } 52 | curRules.unshift(`internal-proxy://${baseUrl} ignore://rule|html enable://hide|proxyHost|clientId|multiClient ${domainList.join(' ')}`); 53 | curRules.push(`*/.whistle-path.5b6af7b9884e1165/ ignore://-proxy|-host reqHeaders://whistleInternalHost=${WHISTLE_HOST} enable://proxyTunnel`); 54 | curRules.push('\n', INJECT_HTML); 55 | curRules = preRules.concat(curRules).concat(postRules).join('\n'); 56 | } 57 | ctx.body = curRules; 58 | } else { 59 | ctx.body = ''; 60 | } 61 | }; 62 | -------------------------------------------------------------------------------- /lib/plugins/whistle.nohost/lib/uiServer/cgi/proxy.js: -------------------------------------------------------------------------------- 1 | const LRU = require('lru-cache'); 2 | const { transformWhistle, changeFilter } = require('../../util'); 3 | const whistleMgr = require('../../whistleMgr'); 4 | 5 | const tempCache = new LRU({ maxAge: 5000 }); 6 | 7 | module.exports = async (ctx, next) => { 8 | let { params: { name }, account } = ctx; 9 | if (!account) { 10 | account = ctx.accountMgr.getAccount(name); 11 | if (!account) { 12 | await next(); 13 | return; 14 | } 15 | } else { 16 | name = account.name; 17 | } 18 | const { req } = ctx; 19 | req.url = req.url.substring(`/account/${name}`.length); 20 | changeFilter(req, ctx.envMgr.getFollower(ctx)); 21 | req.headers.host = 'local.wproxy.org'; 22 | req.headers['x-forwarded-for'] = ctx.ip || '127.0.0.1'; 23 | const { 24 | port, 25 | remoteAddrHead, 26 | remotePortHead, 27 | } = await whistleMgr.fork(account); 28 | const remoteAddr = ctx.get('x-whistle-remote-address'); 29 | if (remoteAddr) { 30 | req.headers[remoteAddrHead] = remoteAddr; 31 | req.headers[remotePortHead] = ctx.get('x-whistle-remote-port'); 32 | } 33 | try { 34 | await transformWhistle(ctx, port); 35 | } catch (err) { 36 | if (err.code === 'ECONNREFUSED' && !tempCache.get(name)) { 37 | tempCache.set(name, 1); 38 | whistleMgr.kill(name); 39 | } 40 | throw err; 41 | } 42 | }; 43 | -------------------------------------------------------------------------------- /lib/plugins/whistle.nohost/lib/uiServer/cgi/proxyNetwork.js: -------------------------------------------------------------------------------- 1 | const { transformWhistle, changeFilter } = require('../../util'); 2 | const initNetwork = require('../network'); 3 | 4 | module.exports = async (ctx) => { 5 | const { req } = ctx; 6 | req.url = req.url.replace('/network', ''); 7 | req.headers.host = 'local.wproxy.org'; 8 | req.headers['x-forwarded-for'] = ctx.ip || '127.0.0.1'; 9 | const port = await initNetwork(); 10 | changeFilter(req); 11 | await transformWhistle(ctx, port); 12 | }; 13 | -------------------------------------------------------------------------------- /lib/plugins/whistle.nohost/lib/uiServer/cgi/redirect.js: -------------------------------------------------------------------------------- 1 | const { getClientId } = require('../../util'); 2 | 3 | module.exports = (ctx) => { 4 | const { url, name, env } = ctx.request.query; 5 | const account = ctx.accountMgr.getAccount(name); 6 | if (account) { 7 | ctx.envMgr.setEnv(getClientId(ctx), name, env); 8 | } 9 | ctx.redirect(url || './'); 10 | }; 11 | -------------------------------------------------------------------------------- /lib/plugins/whistle.nohost/lib/uiServer/cgi/selectEnv.js: -------------------------------------------------------------------------------- 1 | const { COOKIE_NAME, ENV_MAX_AGE, decodeURIComponentSafe, getClientId } = require('../../util'); 2 | 3 | module.exports = (ctx) => { 4 | const { name, envId, redirect } = ctx.request.query; 5 | const envName = decodeURIComponentSafe(envId); 6 | const env = ctx.envMgr.setEnv(getClientId(ctx), name, envName); 7 | let value = ''; 8 | if (env) { 9 | value = encodeURIComponent(`${name}/${env.envName}`); 10 | } 11 | ctx.cookies.set(COOKIE_NAME, value, { 12 | path: '/', 13 | expires: new Date(Date.now() + (ENV_MAX_AGE * 1000)), 14 | }); 15 | if (redirect && typeof redirect === 'string') { 16 | ctx.redirect(redirect); 17 | } else { 18 | ctx.body = { ec: 0 }; 19 | } 20 | }; 21 | -------------------------------------------------------------------------------- /lib/plugins/whistle.nohost/lib/uiServer/cgi/unfollow.js: -------------------------------------------------------------------------------- 1 | module.exports = (ctx) => { 2 | ctx.envMgr.unfollow(ctx); 3 | ctx.body = { ec: 0 }; 4 | }; 5 | -------------------------------------------------------------------------------- /lib/plugins/whistle.nohost/lib/uiServer/index.js: -------------------------------------------------------------------------------- 1 | const Koa = require('koa'); 2 | const onerror = require('koa-onerror'); 3 | const serve = require('koa-static'); 4 | const path = require('path'); 5 | const router = require('koa-router')(); 6 | const setupRouter = require('./router'); 7 | const accountMgr = require('../accountMgr'); 8 | const envMgr = require('../envMgr'); 9 | const whistleMgr = require('../whistleMgr'); 10 | 11 | const MAX_AGE = 1000 * 60 * 5; 12 | const error = function (em, ec) { 13 | em = em || '请求失败,请刷新页面重试'; 14 | ec = ec || 2; 15 | this.body = { ec, em }; 16 | }; 17 | 18 | module.exports = (server, options) => { 19 | const { username, password } = options.data; 20 | const { runtimeId } = options.config; 21 | const app = new Koa(); 22 | app.proxy = true; 23 | app.silent = true; 24 | if (process.env.PFORK_MODE === 'bind') { 25 | onerror(app); 26 | } 27 | app.use(async (ctx, next) => { 28 | const baseUrl = `http://${accountMgr.getBaseUrl()}/`; 29 | ctx.baseUrl = baseUrl; 30 | ctx.success = function () { 31 | this.body = { ec: 0, baseUrl }; 32 | }; 33 | ctx.error = error; 34 | ctx.accountMgr = accountMgr; 35 | ctx.envMgr = envMgr; 36 | ctx.config = options.config; 37 | ctx.updateRules = options.updateRules; 38 | ctx.whistleMgr = whistleMgr; 39 | ctx.admin = { username, password }; 40 | ctx.runtimeId = runtimeId; 41 | ctx.set('x-nohost-base-url', baseUrl); 42 | const { path: pathname } = ctx; 43 | if (pathname === '/' || pathname === '/index.html') { 44 | ctx.req.url = '/select.html'; 45 | } else if (pathname === '/js/nohost.js') { 46 | ctx.req.url = '/button.js'; 47 | } 48 | const origin = ctx.get('origin'); 49 | if (origin) { 50 | ctx.set('Access-Control-Allow-Origin', origin); 51 | ctx.set('Access-Control-Allow-Credentials', true); 52 | } 53 | await next(); 54 | }); 55 | setupRouter(router); 56 | app.use(router.routes()); 57 | app.use(router.allowedMethods()); 58 | app.use(serve(path.join(__dirname, '../../../../../public/'), { maxage: MAX_AGE })); 59 | server.on('request', app.callback()); 60 | }; 61 | -------------------------------------------------------------------------------- /lib/plugins/whistle.nohost/lib/uiServer/network.js: -------------------------------------------------------------------------------- 1 | 2 | const startWhistle = require('whistle'); 3 | const path = require('path'); 4 | const { getNohostPluginsPath, getPort, PLUGINS_DIR } = require('../util'); 5 | 6 | let result; 7 | const projectPluginsPath = [ 8 | path.join(__dirname, '../../../account'), 9 | ]; 10 | const customPluginsPath = [ 11 | path.join(getNohostPluginsPath(), 'worker_plugins'), 12 | ]; 13 | const pluginsPath = [ 14 | path.join(PLUGINS_DIR, 'whistle.nohost/lib/node_modules'), 15 | path.join(PLUGINS_DIR, 'whistle.nohost/node_modules'), 16 | ]; 17 | 18 | module.exports = () => { 19 | if (!result) { 20 | result = new Promise((resolve) => { 21 | getPort((port) => { 22 | const proxy = startWhistle({ 23 | globalData: 'Network', 24 | cmdName: 'n2', 25 | port, 26 | host: '127.0.0.1', 27 | encrypted: true, 28 | storage: 'nohost_network_mode', 29 | mode: 'plugins|disableUpdateTips|hideLeftMenu', 30 | projectPluginsPath, 31 | customPluginsPath, 32 | pluginsPath, 33 | }, () => resolve(port, proxy)); 34 | }); 35 | }); 36 | } 37 | return result; 38 | }; 39 | -------------------------------------------------------------------------------- /lib/plugins/whistle.nohost/lib/uiServer/openAPI.js: -------------------------------------------------------------------------------- 1 | const proxy = require('./cgi/proxy'); 2 | const accountMgr = require('../accountMgr'); 3 | const { AUTH_KEY } = require('../util'); 4 | 5 | const CGI_MAP = { 6 | 'add-env': 'rules/add', 7 | 'add-top-env': 'rules/project', 8 | 'add-top-rules': 'rules/project', 9 | 'add-rules': 'rules/add', 10 | 'modify-env': 'rules/add', 11 | 'modify-rules': 'rules/add', 12 | 'remove-env': 'rules/remove', 13 | 'remove-rules': 'rules/remove', 14 | 'rename-env': 'rules/rename', 15 | 'rename-rules': 'rules/rename', 16 | list: 'rules/list', 17 | }; 18 | 19 | const getRules = (options) => { 20 | const { account, name, env, envName } = options; 21 | let { rules = '', headers } = accountMgr.getRules(account || name, env || envName); 22 | if (headers) { 23 | headers = JSON.stringify(headers); 24 | if (headers.length > 2) { 25 | rules += `\n* reqHeaders://(${headers})`; 26 | } 27 | } 28 | return rules; 29 | }; 30 | 31 | module.exports = async (ctx, next) => { 32 | let { cgiName } = ctx.params; 33 | const authKey = ctx.accountMgr.getAuthKey(); 34 | if (cgiName === 'rules') { 35 | const { query } = ctx.request; 36 | if (!authKey || authKey !== query.authKey) { 37 | ctx.status = 403; 38 | return; 39 | } 40 | ctx.body = getRules(query); 41 | return; 42 | } 43 | cgiName = CGI_MAP[cgiName]; 44 | const name = ctx.get('x-nohost-account-name'); 45 | const account = cgiName && ctx.accountMgr.getAccount(name); 46 | if (!account) { 47 | return; 48 | } 49 | const curAuthKey = authKey && ctx.get('x-nohost-auth-key'); 50 | if (!curAuthKey || (authKey !== curAuthKey && curAuthKey !== ctx.accountMgr.defaultAuthKey)) { 51 | ctx.status = 403; 52 | return; 53 | } 54 | const { req } = ctx; 55 | req.headers['x-whistle-auth-key'] = AUTH_KEY; 56 | let { url } = req; 57 | const index = url.indexOf('?'); 58 | url = index === -1 ? '' : url.substring(index); 59 | req.url = `/account/${name}/cgi-bin/${cgiName}${url}`; 60 | ctx.account = account; 61 | await proxy(ctx, next); 62 | }; 63 | -------------------------------------------------------------------------------- /lib/plugins/whistle.nohost/lib/whistleMgr.js: -------------------------------------------------------------------------------- 1 | const p = require('pfork'); 2 | const path = require('path'); 3 | const accountMgr = require('./accountMgr'); 4 | const { 5 | CONFIG_DATA_TYPE, 6 | AUTH_KEY, 7 | pluginConfig, 8 | } = require('./util'); 9 | 10 | const { realPort, realHost } = pluginConfig; 11 | const GUEST_AUTH = { guestName: '-' }; 12 | const WHISTLE_WORKER = path.join(__dirname, 'whistle.js'); 13 | const DELAY = 6000; 14 | const UPDATE_INTERVAL = 5000; 15 | const cache = {}; 16 | 17 | exports.fork = (account) => { 18 | if (!account) { 19 | return; 20 | } 21 | const { name, password } = account; 22 | if (cache[name]) { 23 | return cache[name]; 24 | } 25 | const getAccountData = () => { 26 | const data = { 27 | type: CONFIG_DATA_TYPE, 28 | username: account.name, 29 | password: account.password, 30 | guest: accountMgr.isEnableGuest() ? GUEST_AUTH : null, 31 | defaultRules: accountMgr.accountRules, 32 | }; 33 | return data; 34 | }; 35 | cache[name] = new Promise((resolve, reject) => { 36 | const guestName = accountMgr.isEnableGuest() ? '-' : undefined; 37 | p.fork({ 38 | authKey: AUTH_KEY, 39 | password, 40 | guestName, 41 | defaultRules: accountMgr.accountRules, 42 | storage: `whistle.nohost/${name}`, 43 | script: WHISTLE_WORKER, 44 | value: name, 45 | realPort, 46 | realHost, 47 | storageServer: accountMgr.storageServer, 48 | dnsServer: accountMgr.dnsServer, 49 | accountPluginPath: accountMgr.accountPluginPath, 50 | }, (err, result, child) => { 51 | if (err) { 52 | delete cache[name]; 53 | return reject(err); 54 | } 55 | resolve(result); 56 | accountMgr.addEnvList(name, result.envList); 57 | let timer; 58 | const updateAuth = () => { 59 | account = account && accountMgr.getAccount(account.name); 60 | if (!account) { 61 | return; 62 | } 63 | child.sendData(getAccountData()); 64 | timer = setTimeout(updateAuth, UPDATE_INTERVAL); 65 | }; 66 | timer = setTimeout(updateAuth, UPDATE_INTERVAL); 67 | child.on('exit', () => { 68 | clearTimeout(timer); 69 | delete cache[name]; 70 | }); 71 | child.on('data', (data) => { 72 | const type = data && data.type; 73 | if (type !== CONFIG_DATA_TYPE) { 74 | return; 75 | } 76 | account = account && accountMgr.getAccount(account.name); 77 | if (!account) { 78 | return; 79 | } 80 | const accountData = getAccountData(); 81 | accountData.index = data.index; 82 | child.sendData(accountData); 83 | accountMgr.addEnvList(name, data.envList); 84 | }); 85 | }); 86 | }); 87 | return cache[name]; 88 | }; 89 | exports.kill = (name) => { 90 | p.kill({ 91 | script: WHISTLE_WORKER, 92 | value: name, 93 | }, DELAY); 94 | }; 95 | -------------------------------------------------------------------------------- /lib/plugins/whistle.nohost/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "whistle.nohost", 3 | "version": "1.0.0", 4 | "whistleConfig": { 5 | "priority": 100 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /lib/plugins/whistle.nohost/rules.txt: -------------------------------------------------------------------------------- 1 | * whistle.nohost://none includeFilter://reqH:x-whistle-nohost-env=/^\$\d+$/ 2 | * whistle.nohost://`${reqH.x-whistle-nohost-policy}` includeFilter://reqH:x-whistle-nohost-policy 3 | @whistle.nohost/cgi-bin/rules -------------------------------------------------------------------------------- /lib/service/cgi/export.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | const fse = require('fs-extra'); 4 | const Limiter = require('async-limiter'); 5 | const { gzip } = require('zlib'); 6 | const { getSessionsDir, getDate, getAccessCode } = require('./util'); 7 | 8 | const promises = {}; 9 | const limiter = new Limiter({ concurrency: 10 }); 10 | const ROUTE_RE = /^[\w.:/=+-]{1,100}$/; 11 | 12 | const writeFile = (filepath, data) => { 13 | return new Promise((resolve, reject) => { 14 | fs.writeFile(filepath, data, { 15 | flag: 'wx', 16 | }, (err) => { 17 | if (err) { 18 | reject(err); 19 | } else { 20 | resolve(); 21 | } 22 | }); 23 | }); 24 | }; 25 | 26 | const createDir = (dir) => { 27 | let p = promises[dir]; 28 | if (!p) { 29 | p = fse.ensureDir(dir); 30 | promises[dir] = p; 31 | } 32 | return p; 33 | }; 34 | 35 | const compressSessions = (sessions) => { 36 | return new Promise((resolve, reject) => { 37 | limiter.push((done) => { 38 | gzip(sessions, (err, buf) => { 39 | done(); 40 | if (err) { 41 | reject(err); 42 | } else { 43 | resolve(buf); 44 | } 45 | }); 46 | }); 47 | }); 48 | }; 49 | 50 | module.exports = async (ctx) => { 51 | let { query: { username, route }, body: { name, sessions, code } } = ctx.request; 52 | if (!name || !sessions || typeof sessions !== 'string') { 53 | return; 54 | } 55 | code = getAccessCode(code); 56 | const date = getDate(); 57 | const dir = getSessionsDir(username, date, code); 58 | if (!dir) { 59 | return; 60 | } 61 | await createDir(dir); 62 | try { 63 | sessions = await compressSessions(sessions); 64 | name = encodeURIComponent(name); 65 | if (code) { 66 | name = `${name}[${code}]`; 67 | } 68 | await writeFile(path.join(dir, `${name}[gz]`), sessions); 69 | ctx.body = { ec: 0, date, username, route: route && ROUTE_RE.test(route) ? route : undefined }; 70 | } catch (e) { 71 | ctx.body = { ec: e.code === 'EEXIST' ? 1 : 2 }; 72 | } 73 | if (ctx.get('origin')) { 74 | ctx.set('Access-Control-Allow-Origin', '*'); 75 | } 76 | }; 77 | -------------------------------------------------------------------------------- /lib/service/cgi/import.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | const { getSessionsDir, getAccessCode } = require('./util'); 4 | 5 | const isGzipFile = (gzFilePath) => { 6 | return new Promise((resolve) => { 7 | fs.stat(gzFilePath, (err, stat) => { 8 | resolve(!err && stat.isFile()); 9 | }); 10 | }); 11 | }; 12 | 13 | module.exports = async (ctx) => { 14 | let { username, name, date, code } = ctx.request.query; 15 | if (!name || typeof name !== 'string') { 16 | return; 17 | } 18 | code = getAccessCode(code); 19 | const dir = getSessionsDir(username, date, code); 20 | if (!dir) { 21 | return; 22 | } 23 | name = encodeURIComponent(name); 24 | if (code) { 25 | name = `${name}[${code}]`; 26 | } 27 | const gzFilePath = path.join(dir, `${name}[gz]`); 28 | const isGFile = await isGzipFile(gzFilePath); 29 | if (isGFile) { 30 | ctx.set('content-encoding', 'gzip'); 31 | } 32 | ctx.body = fs.createReadStream(isGFile ? gzFilePath : path.join(dir, name)); 33 | }; 34 | -------------------------------------------------------------------------------- /lib/service/cgi/util.js: -------------------------------------------------------------------------------- 1 | const os = require('os'); 2 | const path = require('path'); 3 | 4 | const USERNAME_RE = /^[a-z\d.]{1,64}$/; 5 | const DATE_RE = /^\d{8}$/; 6 | const ACCESS_CODE_RE = /^[a-z\d]{4}$/i; 7 | const leftPad = num => (num > 9 ? num : `0${num}`); 8 | let nohostPath; 9 | 10 | const getSessionsPath = () => { 11 | nohostPath = nohostPath || process.env.NOHOST_PATH || path.join(os.homedir(), '.NohostAppData'); 12 | return nohostPath; 13 | }; 14 | 15 | const getDate = () => { 16 | const now = new Date(); 17 | return `${now.getFullYear()}${leftPad(now.getMonth() + 1)}${leftPad(now.getDate())}`; 18 | }; 19 | 20 | exports.getDate = getDate; 21 | 22 | const checkUsername = (username) => { 23 | return username == null || username === '' || (typeof username === 'string' && USERNAME_RE.test(username)); 24 | }; 25 | 26 | exports.getSessionsDir = (username, date, encrypted) => { 27 | if (!DATE_RE.test(date) || !checkUsername(username)) { 28 | return; 29 | } 30 | const dir = getSessionsPath(); 31 | date = path.join(date, encrypted ? 'encrypted' : 'sessions'); 32 | return username ? path.join(dir, username, date) : path.join(dir, date); 33 | }; 34 | 35 | exports.getAccessCode = code => (ACCESS_CODE_RE.test(code) ? code.toLowerCase() : ''); 36 | -------------------------------------------------------------------------------- /lib/service/index.js: -------------------------------------------------------------------------------- 1 | const { fork } = require('pfork'); 2 | const script = require('path').join(__dirname, 'server.js'); 3 | const getPort = require('../util/getPort'); 4 | 5 | const workers = []; 6 | const getCreator = () => { 7 | let promise; 8 | return () => { 9 | if (!promise) { 10 | promise = new Promise((resolve, reject) => { 11 | getPort((port) => { 12 | fork({ value: `${port}`, script }, (err, _, child) => { 13 | if (err) { 14 | promise = null; 15 | reject(err); 16 | } else { 17 | child.once('close', () => { 18 | promise = null; 19 | }); 20 | resolve(port); 21 | } 22 | }); 23 | }); 24 | }); 25 | } 26 | return promise; 27 | }; 28 | }; 29 | 30 | module.exports = (index) => { 31 | let createServer = workers[index]; 32 | if (!createServer) { 33 | createServer = getCreator(); 34 | workers[index] = getCreator(); 35 | } 36 | return createServer(); 37 | }; 38 | -------------------------------------------------------------------------------- /lib/service/router.js: -------------------------------------------------------------------------------- 1 | const exportSessions = require('./cgi/export'); 2 | const importSessions = require('./cgi/import'); 3 | 4 | module.exports = (router) => { 5 | router.post('/cgi-bin/sessions/export', exportSessions); 6 | router.get('/cgi-bin/sessions/import', importSessions); 7 | }; 8 | -------------------------------------------------------------------------------- /lib/service/server.js: -------------------------------------------------------------------------------- 1 | 2 | const { createServer } = require('http'); 3 | const Koa = require('koa'); 4 | const onerror = require('koa-onerror'); 5 | const router = require('koa-router')(); 6 | const bodyParser = require('koa-bodyparser'); 7 | const setupRouter = require('./router'); 8 | 9 | 10 | module.exports = ({ value: port }, callback) => { 11 | const server = createServer(); 12 | const app = new Koa(); 13 | app.proxy = true; 14 | app.silent = true; 15 | if (process.env.PFORK_MODE === 'bind') { 16 | onerror(app); 17 | } 18 | setupRouter(router); 19 | app.use(bodyParser({ formLimit: '8mb' })); 20 | app.use(router.routes()); 21 | app.use(router.allowedMethods()); 22 | server.on('request', app.callback()); 23 | server.on('error', callback); 24 | server.listen(port, '127.0.0.1', callback); 25 | }; 26 | -------------------------------------------------------------------------------- /lib/util/address.js: -------------------------------------------------------------------------------- 1 | const os = require('os'); 2 | const net = require('net'); 3 | 4 | const LOCALHOST = '127.0.0.1'; 5 | let addressList = []; 6 | let serverIp; 7 | 8 | (function updateSystyemInfo() { 9 | const interfaces = os.networkInterfaces(); 10 | addressList = []; 11 | Object.keys(interfaces).forEach((name) => { 12 | const list = interfaces[name]; 13 | if (Array.isArray(list)) { 14 | list.forEach((info) => { 15 | if (!info.internal && (info.family === 'IPv4' || info.family === 4)) { 16 | serverIp = info.address; 17 | } 18 | addressList.push(info.address.toLowerCase()); 19 | }); 20 | // 支持多网卡时,以环境变量指定 serverIp 21 | const envServerIp = process.env.NOHOST_SERVER_IP; 22 | if (net.isIP(envServerIp) && addressList.includes(envServerIp)) { 23 | serverIp = envServerIp; 24 | } 25 | } 26 | }); 27 | setTimeout(updateSystyemInfo, 30000); 28 | }()); 29 | 30 | exports.getAddressList = () => addressList; 31 | 32 | const isLocalHost = (ip) => { 33 | if (!ip || typeof ip !== 'string') { 34 | return true; 35 | } 36 | return ip.length < 7 || ip === LOCALHOST; 37 | }; 38 | 39 | const isLocalAddress = (address) => { 40 | if (isLocalHost(address)) { 41 | return true; 42 | } 43 | if (address === '0:0:0:0:0:0:0:1') { 44 | return true; 45 | } 46 | address = address.toLowerCase(); 47 | if (address[0] === '[') { 48 | address = address.slice(1, -1); 49 | } 50 | return addressList.indexOf(address) !== -1; 51 | }; 52 | 53 | exports.isLocalAddress = isLocalAddress; 54 | 55 | exports.getServerIp = () => serverIp; 56 | -------------------------------------------------------------------------------- /lib/util/getPort.js: -------------------------------------------------------------------------------- 1 | const http = require('http'); 2 | 3 | let curPort = 30013; 4 | 5 | const getPort = (callback) => { 6 | const server = http.createServer(); 7 | server.on('error', () => { 8 | if (++curPort % 5 === 0) { 9 | ++curPort; 10 | } 11 | getPort(callback); 12 | }); 13 | server.listen(curPort, '127.0.0.1', () => { 14 | server.removeAllListeners(); 15 | server.close(() => callback(curPort)); 16 | }); 17 | }; 18 | 19 | module.exports = getPort; 20 | -------------------------------------------------------------------------------- /lib/util/login.js: -------------------------------------------------------------------------------- 1 | const crypto = require('crypto'); 2 | const getAuth = require('basic-auth'); 3 | 4 | const ENV_MAX_AGE = 60 * 60 * 24 * 3; 5 | 6 | const shasum = (str) => { 7 | if (typeof str !== 'string') { 8 | str = ''; 9 | } 10 | const result = crypto.createHash('sha1'); 11 | result.update(str); 12 | return result.digest('hex'); 13 | }; 14 | exports.shasum = shasum; 15 | 16 | const getLoginKey = (ctx, username, password) => { 17 | const ip = ctx.ip || '127.0.0.1'; 18 | return shasum(`${username || ''}\n${password || ''}\n${ip}`); 19 | }; 20 | 21 | exports.checkLogin = (ctx, authConf) => { 22 | const { 23 | username, 24 | password, 25 | nameKey, 26 | authKey, 27 | } = authConf; 28 | 29 | if (!username || !password) { 30 | return true; 31 | } 32 | const curName = ctx.cookies.get(nameKey); 33 | const lkey = ctx.cookies.get(authKey); 34 | const correctKey = getLoginKey(ctx, username, password); 35 | if (curName === username && correctKey === lkey) { 36 | return true; 37 | } 38 | 39 | const { name, pass } = getAuth(ctx.req) || {}; 40 | if (name === username && shasum(pass) === password) { 41 | const options = { 42 | expires: new Date(Date.now() + (ENV_MAX_AGE * 1000)), 43 | path: '/', 44 | }; 45 | ctx.cookies.set(nameKey, username, options); 46 | ctx.cookies.set(authKey, correctKey, options); 47 | return true; 48 | } 49 | 50 | ctx.status = 401; 51 | ctx.set('WWW-Authenticate', ' Basic realm=User Login'); 52 | ctx.set('Content-Type', 'text/html; charset=utf8'); 53 | ctx.body = 'Access denied, please try again.'; 54 | return false; 55 | }; 56 | -------------------------------------------------------------------------------- /lib/util/parseDomain.js: -------------------------------------------------------------------------------- 1 | 2 | const isBaseUrl = d => /^[\w-]+(?:\.[\w-]+){1,}$/.test(d); 3 | const SEP_RE = /\s*[,\s]\s*/; 4 | let domainList = []; 5 | let curDomain; 6 | 7 | const parseDomain = (domainStr) => { 8 | if (domainStr !== curDomain) { 9 | curDomain = domainStr; 10 | domainList = curDomain.trim().split(SEP_RE).filter(isBaseUrl); 11 | } 12 | return domainList; 13 | }; 14 | 15 | module.exports = (str) => { 16 | return !str || typeof str !== 'string' ? [] : parseDomain(str); 17 | }; 18 | -------------------------------------------------------------------------------- /lib/whistle.js: -------------------------------------------------------------------------------- 1 | 2 | const path = require('path'); 3 | const startWhistle = require('whistle'); 4 | const { homedir } = require('os'); 5 | const { getWhistlePath } = require('whistle'); 6 | const { getServerIp } = require('./util/address'); 7 | const parseDomain = require('./util/parseDomain'); 8 | const getPort = require('./util/getPort'); 9 | 10 | const getShadowRules = (port, domain) => { 11 | domain = parseDomain(domain); 12 | domain.unshift(`//${getServerIp()}:${port}`); 13 | return domain.map((d) => { 14 | return `${d} http://127.0.0.1:${port} enable://capture`; 15 | }).join('\n'); 16 | }; 17 | 18 | module.exports = (options, callback) => { 19 | getPort((port) => { 20 | const { 21 | domain, 22 | username, 23 | password, 24 | realPort, 25 | realHost, 26 | authKey, 27 | mainAuthKey, 28 | debugMode, 29 | storageServer, 30 | dnsServer, 31 | globalPluginPath, 32 | accountPluginPath, 33 | } = options; 34 | const projectPluginPath = [path.join(__dirname, 'plugins')]; 35 | const notUninstallPluginPath = [path.join(__dirname, '../node_modules')]; 36 | const addon = globalPluginPath ? projectPluginPath.concat(globalPluginPath) : projectPluginPath; 37 | let { baseDir } = options; 38 | if (/^~\//.test(baseDir)) { 39 | baseDir = path.join(homedir(), baseDir.substring(2)); 40 | } else if (baseDir && /^[\w.-]+$/.test(baseDir)) { 41 | baseDir = path.join(homedir(), '.nohost', baseDir); 42 | } 43 | if (baseDir) { 44 | process.env.NOHOST_BADE_DIR = baseDir; 45 | } 46 | const mode = 'proxyServer|master|x-forwarded-proto'; 47 | const proxy = startWhistle({ 48 | port, 49 | baseDir, 50 | projectPluginPath, 51 | notUninstallPluginPath, 52 | customPluginPath: path.join(getWhistlePath(), 'nohost_plugins/main_plugins'), 53 | addon, 54 | encrypted: true, 55 | username, 56 | password, 57 | realPort, 58 | realHost, 59 | cmdName: 'n2 -g', 60 | host: '127.0.0.1', 61 | shadowRules: getShadowRules(realPort, domain), 62 | dnsServer, 63 | authKey: mainAuthKey, 64 | pluginsDataMap: { 65 | nohost: { 66 | username, 67 | password, 68 | domain, 69 | realPort, 70 | realHost, 71 | authKey, 72 | storageServer, 73 | dnsServer, 74 | accountPluginPath, 75 | }, 76 | }, 77 | mode: debugMode ? mode : `${mode}|strict|rules|disableUpdateTips|proxifier|notAllowedDisablePlugins`, 78 | }, () => { 79 | const { 80 | REMOTE_ADDR_HEAD: remoteAddrHead, 81 | REMOTE_PORT_HEAD: remotePortHead, 82 | } = proxy.config; 83 | callback(null, { remoteAddrHead, remotePortHead, port }); 84 | }); 85 | }); 86 | }; 87 | -------------------------------------------------------------------------------- /packages/admin/package.json: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tencent/nohost/d8994d9709eedd980ab6aeb359224896e67e09cd/packages/admin/package.json -------------------------------------------------------------------------------- /packages/gateway/.npmignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # nyc test coverage 18 | .nyc_output 19 | 20 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 21 | .grunt 22 | 23 | # node-waf configuration 24 | .lock-wscript 25 | 26 | # Compiled binary addons (http://nodejs.org/api/addons.html) 27 | build/Release 28 | 29 | # Dependency directories 30 | node_modules 31 | jspm_packages 32 | 33 | # Optional npm cache directory 34 | .npm 35 | 36 | # Optional REPL history 37 | .node_repl_history 38 | 39 | # test 40 | test 41 | jest 42 | 43 | # Hidden file 44 | /.* 45 | /jest.* 46 | 47 | -------------------------------------------------------------------------------- /packages/gateway/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 nohosts 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /packages/gateway/README.md: -------------------------------------------------------------------------------- 1 | # gateway 2 | Nohost 网关是一个用于集群部署的请求接入层,可以接收以下 5 种请求: 3 | 1. HTTP 代理请求 4 | 2. HTTPS 代理请求 5 | 3. Socks v5 代理请求 6 | 4. HTTP & HTTPS 直接请求 7 | 5. Nginx 网关反向代理请求等 8 | 9 | 并支持: 10 | 1. 异步加载证书将 HTTPS 请求转成普通 HTTP 请求 11 | 2. 设置 Whistle 规则 12 | 3. 将请求转到指定代理 13 | 14 | 支持以下几种代理服务: 15 | 1. 普通代理,如 Whistle、Fiddler、Charles 等 16 | 2. Nohost 单实例(单机部署) 17 | 3. Nohost Cluster(多机部署) 18 | 19 | 也可以直接将请求转到后台 Server 等。 20 | 21 |  22 | 23 | # 用法 24 | 在项目中引入 npm 包: 25 | ``` sh 26 | npm i --save @nohost/gateway 27 | ``` 28 | 29 | 自定义启动文件 `dispatch.js`: 30 | ``` js 31 | const startGateway = require('@nohost/gateway'); 32 | 33 | startGateway({ 34 | sniCallback, //【可选】加载证书方法的路径 35 | port, //【可选】代理端口,默认 8080 端口,也可以通过 `"host:port"` 指定网卡 36 | workers, //【可选】启动的 Worker 数,默认为系统 CPU 核数 37 | socksPort, //【可选】设置 Socks v5 代理端口,设置后会自动启一个 Socks v5 服务,默认关闭,也可以通过 `"host:port"` 指定网卡 38 | httpsPort, //【可选】设置 HTTPS Server 端口,设置后会自动启一个 HTTPS 服务,默认关闭,也可以通过 `"host:port"` 指定网卡(用户无需通过代理请求) 39 | httpPort, //【可选】设置 HTTP Server 端口,设置后会自动启一个 HTTP 服务,默认关闭,也可以通过 `"host:port"` 指定网卡(用户无需通过代理请求) 40 | mode, //【可选】同 https://github.com/avwo/whistle/blob/master/lib/config.js#L720 41 | handleRequest(req, res), //【可选】处理 HTTP 请求 42 | handleUpgrade(req, socket), //【可选】处理 WebSocket 请求 43 | handleConnect(req, socket), //【可选】处理隧道代理(Tunnel)请求 44 | }); 45 | ``` 46 | 47 | 以上参数也可以通过命令行和环境变量 `process.env` 设置,优先级 `命令行 > 环境变量 > 代码参数`: 48 | 49 | ### 命令行 50 | 例子(修改启动端口): 51 | ``` sh 52 | node dispatch --port 8888 53 | ``` 54 | 55 | 支持以下参数: 56 | 1. `--sniCallback [sniCallback]`:同 `sniCallback` 参数 57 | 2. `--port [port]`:同 `port` 参数 58 | 3. `--workers [workers]`:同 `workers` 参数 59 | 4. `--socksPort [socksPort]`:同 `socksPort` 参数 60 | 5. `--httpsPort [httpsPort]`:同 `httpsPort` 参数 61 | 6. `--httpPort [httpPort]`:同 `httpPort` 参数 62 | 7. `--mode [mode]`:同 `mode` 参数 63 | 8. `--debug`:**启用调试模式(调试模式下只会启一个 Worker)可以查看抓包,默认关闭** 64 | > 命令行参数多了 `--debug` 选项,其它类型参数不支持 65 | 66 | ### 环境变量 67 | 支持以下环境变量: 68 | 1. `process.env.NOHOST_SNI_CALLBACK`:同 `sniCallback` 参数 69 | 2. `process.env.NOHOST_PORT`:同 `port` 参数 70 | 3. `process.env.NOHOST_WORKERS`:同 `workers` 参数 71 | 4. `process.env.NOHOST_SOCKS_PORT`:同 `socksPort` 参数 72 | 5. `process.env.NOHOST_HTTPS_PORT`:同 `httpsPort` 参数 73 | 6. `process.env.NOHOST_HTTP_PORT`:同 `httpPort` 参数 74 | 7. `process.env.NOHOST_MODE`:同 `mode` 参数 75 | 76 | 77 | # 例子 78 | 1. 代理到 Whistle 79 | 2. 代理到 Nohost 80 | 3. 代理到 Nohost 集群 81 | 82 | ### 代理到 Whistle 83 | 1. 部署 Whistle 84 | - 安装 Whistle:`npm i -g whistle` 85 | - 启动 Whistle:`w2 start`(默认端口 `8899`) 86 | - 如果用 Docker,可以通过 `w2 run -M prod` 启动 87 | 2. 转发普通请求:[test/whistle/gateway/dispatch.js](test/whistle/gateway/dispatch.js) 88 | 3. 查看抓包请求:[test/whistle/admin/dispatch.js](test/whistle/admin/dispatch.js) 89 | 90 | ### 代理到 Nohost 91 | 1. 部署 Nohost 92 | - 安装 Nohost`npm i -g nohost` 93 | - 启动 Nohost`n2 start`(默认端口 `8080`) 94 | - 如果用 Docker,可以通过 `n2 run -M prod` 启动 95 | 2. 转发普通请求:[test/nohost/gateway/dispatch.js](test/whistle/gateway/dispatch.js) 96 | 3. 查看抓包请求:[test/nohost/admin/dispatch.js](test/whistle/admin/dispatch.js) 97 | 98 | ### 代理到 Nohost 集群 99 | 1. 部署 Nohost 集群 100 | - 安装 Nohost`npm i -g nohost` 101 | - 启动 Nohost`n2 start`(默认端口 `8080`,需要在多个机器上起多个实例) 102 | - 如果用 Docker,可以通过 `n2 run -M prod` 启动 103 | 2. 转发普通请求:[test/cluster/gateway/dispatch.js](test/whistle/gateway/dispatch.js) 104 | 3. 查看抓包请求:[test/cluster/admin/dispatch.js](test/whistle/admin/dispatch.js) 105 | 106 | # License 107 | [MIT](./LICENSE) 108 | -------------------------------------------------------------------------------- /packages/gateway/index.js: -------------------------------------------------------------------------------- 1 | const startWhistle = require('whistle'); 2 | const Router = require('@nohost/router'); 3 | const { parseArgv } = require('./lib/util'); 4 | 5 | module.exports = (options) => { 6 | return new Promise((resolve) => { 7 | startWhistle(parseArgv(options), function(proxy) { 8 | resolve(proxy, this); 9 | }); 10 | }); 11 | }; 12 | 13 | module.exports.Router = Router; 14 | -------------------------------------------------------------------------------- /packages/gateway/lib/whistle.gateway/index.js: -------------------------------------------------------------------------------- 1 | const Router = require('@nohost/router'); 2 | const { notEmptyStr } = require('../util'); 3 | 4 | const getBody = (body) => { 5 | if (body == null) { 6 | return ''; 7 | } 8 | return Buffer.isBuffer(body) ? body : `${body}`; 9 | }; 10 | 11 | const handleResponse = async (res, svrRes, type) => { 12 | if (type === 'ws' || type === 'tunnel') { 13 | return; 14 | } 15 | let done; 16 | res.writeHead(svrRes.statusCode, svrRes.headers); 17 | svrRes.on('error', (err) => { 18 | if (!done) { 19 | done = true; 20 | res.emit('error', err); 21 | } 22 | }); 23 | svrRes.pipe(res); 24 | }; 25 | 26 | module.exports = async (server, { handlers }) => { 27 | const handleRequest = async (req, res, type) => { 28 | const handle = handlers[type] || handlers.http; 29 | const { 30 | headers, 31 | remoteAddress, 32 | remotePort, 33 | fullUrl, 34 | } = req.originalReq; 35 | req.headers = headers; 36 | // 让 router 转发后保留 https 协议 37 | if (req.isHttps) { 38 | req.url = fullUrl[0] === 'w' ? fullUrl.replace('ws', 'http') : fullUrl; 39 | } 40 | const result = handle && (await handle(req, res)); 41 | if (!result) { 42 | return req.passThrough(); 43 | } 44 | const { 45 | statusCode, 46 | headers: resHeaders, 47 | body, 48 | router, 49 | host, 50 | port, 51 | isUIRequest, 52 | } = result; 53 | let svrRes; 54 | if (statusCode > 0) { 55 | res.writeHead(statusCode, resHeaders); 56 | if (typeof result.pipe === 'function') { 57 | svrRes = result; 58 | } else if (typeof body?.pipe === 'function') { 59 | svrRes = body; 60 | } 61 | if (svrRes) { 62 | svrRes.on('error', err => res.emit('error', err)); 63 | return svrRes.pipe(res); 64 | } 65 | return res.end(getBody(body)); 66 | } 67 | if (notEmptyStr(host) && port > 0) { 68 | req.isUIRequest = isUIRequest; 69 | svrRes = await Router.proxy(req, res, result); 70 | await handleResponse(res, svrRes, type); 71 | return; 72 | } 73 | if (typeof router?.proxy !== 'function') { 74 | return req.passThrough(); 75 | } 76 | headers['x-whistle-remote-address'] = remoteAddress; 77 | headers['x-whistle-remote-port'] = remotePort; 78 | svrRes = await router.proxy(req, res, result); 79 | await handleResponse(res, svrRes, type); 80 | }; 81 | server.on('request', handleRequest); 82 | server.on('upgrade', (req, socket) => { 83 | handleRequest(req, socket, 'ws'); 84 | }); 85 | server.on('connect', (req, socket) => { 86 | handleRequest(req, socket, 'tunnel'); 87 | }); 88 | }; 89 | -------------------------------------------------------------------------------- /packages/gateway/lib/whistle.gateway/initial.js: -------------------------------------------------------------------------------- 1 | const loadModule = require; 2 | 3 | const getHandler = (fn) => { 4 | fn = fn && loadModule(fn); 5 | return typeof fn === 'function' ? fn : undefined; 6 | }; 7 | 8 | module.exports = async (options) => { 9 | const { 10 | handleUpgrade, 11 | handleConnect, 12 | handleRequest, 13 | } = options.config.globalData || {}; 14 | options.handlers = { 15 | http: getHandler(handleRequest), 16 | ws: getHandler(handleUpgrade), 17 | tunnel: getHandler(handleConnect), 18 | }; 19 | }; 20 | -------------------------------------------------------------------------------- /packages/gateway/lib/whistle.gateway/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "whistle.gateway", 3 | "version": "1.0.0", 4 | "description": "获取证书的插件" 5 | } 6 | -------------------------------------------------------------------------------- /packages/gateway/lib/whistle.gateway/rules.txt: -------------------------------------------------------------------------------- 1 | * gateway:// 2 | -------------------------------------------------------------------------------- /packages/gateway/lib/whistle.snicallback/index.js: -------------------------------------------------------------------------------- 1 | exports.sniCallback = (req, { sniCallback }) => { 2 | if (sniCallback) { 3 | return sniCallback(req.originalReq.servername, req); 4 | } 5 | return false; 6 | }; 7 | -------------------------------------------------------------------------------- /packages/gateway/lib/whistle.snicallback/initial.js: -------------------------------------------------------------------------------- 1 | const loadModule = require; 2 | 3 | module.exports = (options) => { 4 | let { sniCallback } = options.config.globalData || {}; 5 | if (sniCallback && typeof sniCallback === 'string') { 6 | sniCallback = loadModule(sniCallback); 7 | if (typeof sniCallback === 'function') { 8 | options.sniCallback = sniCallback; 9 | } 10 | } 11 | }; 12 | -------------------------------------------------------------------------------- /packages/gateway/lib/whistle.snicallback/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "whistle.snicallback", 3 | "version": "1.0.0", 4 | "description": "获取证书的插件" 5 | } 6 | -------------------------------------------------------------------------------- /packages/gateway/lib/whistle.snicallback/rules.txt: -------------------------------------------------------------------------------- 1 | 2 | * sniCallback://snicallback enable://capture 3 | -------------------------------------------------------------------------------- /packages/gateway/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@nohost/gateway", 3 | "version": "1.0.0", 4 | "description": "Nohost cluster gateway", 5 | "scripts": { 6 | "lint": "eslint *.js lib test", 7 | "lintfix": "eslint --fix *.js lib test" 8 | }, 9 | "registry": "https://registry.npmjs.org/@nohost/gateway", 10 | "repository": { 11 | "type": "git", 12 | "url": "git+https://github.com/Tencent/nohost.git" 13 | }, 14 | "keywords": [ 15 | "whistle", 16 | "nohost", 17 | "cluster" 18 | ], 19 | "author": "avwu