├── .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 | ![效果图](https://user-images.githubusercontent.com/11450939/40436253-28a90f28-5ee5-11e8-97a5-fd598e32e0df.gif) -------------------------------------------------------------------------------- /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 | ![账号页面](https://user-images.githubusercontent.com/4689952/69627459-9386c800-1085-11ea-8611-75dbb666c18d.png) 10 | 11 | 点击 **+添加账号** 可以添加使用账号: 12 | 13 | 14 | 新添加的账号可以设置独立的规则,地址为: 15 | http://imwebtest.test.com:8080/data.html?name={新建的账号} 16 | > 其中 imwebtest.test.com 表示Nohost运行的域名 17 | 18 | ![账号规则](https://user-images.githubusercontent.com/4689952/69627831-62f35e00-1086-11ea-8a37-95647c392242.png) 19 | 20 | 添加的账号会出现在**环境选择列表**,**环境选择列表**可能出现在多个地方,如果在**入口配置**( http://imwebtest.test.com:8080/admin.html#config/entrySettings )配置了出现**环境选择圆点**的页面,则访问这个页面经过本地Whistle代理的时候,会被注入**小圆点**,比如: 21 | 22 | ![环境选择](https://user-images.githubusercontent.com/4689952/69627856-6f77b680-1086-11ea-8f15-76cf117103c6.png) 23 | -------------------------------------------------------------------------------- /docs/zh/admin/certs.md: -------------------------------------------------------------------------------- 1 | # 证书 2 | 3 | Nohost作为代理抓包服务器,如果用户希望Nohost能抓包https请求,则需要用到“中间人攻击”原理。 4 | 5 | ![中间人](https://user-images.githubusercontent.com/4689952/69628380-a3eb7280-1086-11ea-9863-cbdc4ab0be85.png) 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 | ![上传证书](https://user-images.githubusercontent.com/4689952/69628531-b4035200-1086-11ea-9935-7feb0cfd2df7.png) 26 | 27 | *.crt和 *.key 文件要一一对应,每个文件内可包含多个域名,会自动识别文件内有哪些域名。 28 | -------------------------------------------------------------------------------- /docs/zh/admin/config.md: -------------------------------------------------------------------------------- 1 | # 配置 2 | ## 入口配置 3 | **您在入口配置里可以配置一些规则,来决定哪些请求需要通过Nohost转发。** 4 | 5 | 例如:可以设置一些页面不注入Whistle小圆点,一些请求不转发到Nohost服务器。 6 | 7 | 具体请求链路如下所示: 8 | 9 | ![image](https://user-images.githubusercontent.com/9802379/70115398-89268880-169a-11ea-8ad9-f0fab305b633.png) 10 | 11 | ![image](https://user-images.githubusercontent.com/11450939/80209935-77ec1180-8665-11ea-909c-f45e0c6db636.png) 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 | ![image](https://user-images.githubusercontent.com/9802379/70145884-07574f00-16dc-11ea-8746-60da5ebd0883.png) 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 | ![image](https://user-images.githubusercontent.com/9802379/70147394-1390db80-16df-11ea-8074-274bf278cc75.png) 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 | ![image](https://user-images.githubusercontent.com/29788452/104795699-5cc0c380-57eb-11eb-80fd-645ecaae8670.png) 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 | ![image](https://user-images.githubusercontent.com/11450939/92067071-07ed5500-edd6-11ea-84b0-22ce7fd0a471.png) 69 | 70 | ![image](https://user-images.githubusercontent.com/11450939/92067083-0b80dc00-edd6-11ea-9520-532ddf225706.png) 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 | ![架构图](https://user-images.githubusercontent.com/11450939/146495077-987ac90f-5f04-411f-97ea-a9919f048ab7.png) 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 ", 20 | "license": "MIT", 21 | "bugs": { 22 | "url": "https://github.com/Tencent/nohost/issues" 23 | }, 24 | "homepage": "https://github.com/Tencent/nohost#readme", 25 | "dependencies": { 26 | "@nohost/router": "^1.1.0", 27 | "whistle": "^2.9.10" 28 | }, 29 | "engines": { 30 | "node": ">= 14" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /packages/gateway/test/README.md: -------------------------------------------------------------------------------- 1 | # 说明 2 | 运行本示例,需要安装 [Whistle](https://github.com/avwo/whistle) 和 [Nohost](https://github.com/Tencent/nohost): 3 | 4 | ``` sh 5 | npm i -g whistle @nohost/server 6 | ``` 7 | 8 | 启动服务: 9 | ``` sh 10 | w2 start 11 | n2 start 12 | n2 run -p 7001 13 | n2 run -p 7002 14 | ``` 15 | -------------------------------------------------------------------------------- /packages/gateway/test/admin/dispatch.js: -------------------------------------------------------------------------------- 1 | const { createServer } = require('http'); 2 | const handleServer = require('./lib'); 3 | 4 | const PORT = 6001; 5 | const server = createServer(); 6 | handleServer(server); 7 | server.listen(PORT, () => { 8 | console.log(`Server is listening on ${PORT}.`); // eslint-disable-line 9 | }); 10 | -------------------------------------------------------------------------------- /packages/gateway/test/admin/lib/index.js: -------------------------------------------------------------------------------- 1 | const Koa = require('koa'); 2 | const bodyParser = require('koa-bodyparser'); 3 | const onerror = require('koa-onerror'); 4 | const serve = require('koa-static'); 5 | const path = require('path'); 6 | const router = require('koa-router')(); 7 | const setupRouter = require('./router'); 8 | 9 | const MAX_AGE = 1000 * 60 * 5; 10 | 11 | module.exports = (server) => { 12 | const app = new Koa(); 13 | app.proxy = true; 14 | app.silent = true; 15 | onerror(app); 16 | setupRouter(router); 17 | app.use(async (ctx, next) => { 18 | ctx.disableBodyParser = true; 19 | await next(); 20 | }); 21 | app.use(bodyParser()); 22 | app.use(router.routes()); 23 | app.use(router.allowedMethods()); 24 | app.use(serve(path.join(__dirname, '../public'), { maxage: MAX_AGE })); 25 | server.on('request', app.callback()); 26 | }; 27 | -------------------------------------------------------------------------------- /packages/gateway/test/admin/lib/router.js: -------------------------------------------------------------------------------- 1 | const Router = require('@nohost/router'); 2 | 3 | // 支持异步获取 4 | const getServers = async () => { 5 | return [ 6 | { 7 | host: '127.0.0.1', 8 | port: 7001, 9 | }, 10 | { 11 | host: '127.0.0.1', 12 | port: 7002, 13 | }, 14 | ]; 15 | }; 16 | const clusterRouter = new Router(getServers); 17 | 18 | const routeMap = { 19 | 1: { 20 | host: '127.0.0.1', 21 | port: 8899, 22 | }, 23 | 2: { 24 | host: '127.0.0.1', 25 | port: 8080, 26 | }, 27 | }; 28 | 29 | const envMap = { 30 | 1: { 31 | space: '测试空间1', 32 | group: '测试分组1', 33 | env: '测试环境1', 34 | }, 35 | 12: { 36 | space: '测试空间1', 37 | group: '测试分组2', 38 | env: '测试环境1', 39 | }, 40 | 123: { 41 | space: '测试空间1', 42 | group: '测试分组2', 43 | env: '测试环境3', 44 | }, 45 | 2: { 46 | space: '测试空间2', 47 | group: '测试分组2', 48 | env: '测试环境2', 49 | }, 50 | 3: { 51 | space: '测试空间3', 52 | group: '测试分组3', 53 | env: '测试环境3', 54 | }, 55 | 4: { 56 | space: '测试空间4', 57 | group: '测试分组4', 58 | env: '测试环境4', 59 | }, 60 | 5: { 61 | space: '测试空间5', 62 | group: '测试分组5', 63 | env: '测试环境5', 64 | }, 65 | }; 66 | // 支持异步获取 67 | const getRouteInfoById = async (id) => { 68 | return envMap[id]; 69 | }; 70 | 71 | const getRoute = async (id) => { 72 | return routeMap[id]; 73 | }; 74 | 75 | const handleResponse = (ctx, svrRes) => { 76 | ctx.status = svrRes.statusCode; 77 | ctx.set(svrRes.headers); 78 | ctx.body = svrRes; 79 | }; 80 | 81 | module.exports = (router) => { 82 | router.all('/network/cluster/:id/(.*)', async (ctx) => { 83 | const { params: { id }, req, res } = ctx; 84 | const info = await getRouteInfoById(id); 85 | if (!info) { 86 | return; 87 | } 88 | req.url = req.url.replace(`/network/cluster/${id}`, ''); 89 | const svrRes = await clusterRouter.proxyUI(req, res, info); 90 | handleResponse(ctx, svrRes); 91 | }); 92 | router.all('/network/proxy/:id/(.*)', async (ctx) => { 93 | const { params: { id }, req, res } = ctx; 94 | const route = await getRoute(id); 95 | if (!route) { 96 | return; 97 | } 98 | req.url = req.url.replace(`/network/proxy/${id}`, ''); 99 | const svrRes = await Router.proxyUI(req, res, route); 100 | handleResponse(ctx, svrRes); 101 | }); 102 | }; 103 | -------------------------------------------------------------------------------- /packages/gateway/test/gateway/dispatch.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const startGateway = require('../../index'); 3 | 4 | const ROOT = path.join(__dirname, 'lib'); 5 | 6 | startGateway({ 7 | sniCallback: path.join(ROOT, 'sniCallback'), 8 | handleRequest: path.join(ROOT, 'handleRequest'), 9 | handleConnect: path.join(ROOT, 'handleConnect'), 10 | handleUpgrade: path.join(ROOT, 'handleUpgrade'), 11 | port: 8081, 12 | }); 13 | -------------------------------------------------------------------------------- /packages/gateway/test/gateway/lib/auth.js: -------------------------------------------------------------------------------- 1 | 2 | module.exports = async (req) => { 3 | if (req.url.indexOf('forbidden') !== -1) { 4 | return { statusCode: 403 }; 5 | } 6 | }; 7 | -------------------------------------------------------------------------------- /packages/gateway/test/gateway/lib/handleConnect.js: -------------------------------------------------------------------------------- 1 | const auth = require('./auth'); 2 | 3 | module.exports = async (req/* , res */) => { 4 | const result = await auth(req); 5 | return result || { 6 | host: '127.0.0.1', 7 | port: 8080, 8 | }; 9 | }; 10 | -------------------------------------------------------------------------------- /packages/gateway/test/gateway/lib/handleRequest.js: -------------------------------------------------------------------------------- 1 | const auth = require('./auth'); 2 | 3 | module.exports = async (req/* , res */) => { 4 | const result = await auth(req); 5 | return result || { 6 | host: '127.0.0.1', 7 | port: 8899, 8 | resRulesUrl: 'http://127.0.0.1:7003', 9 | }; 10 | }; 11 | -------------------------------------------------------------------------------- /packages/gateway/test/gateway/lib/handleUpgrade.js: -------------------------------------------------------------------------------- 1 | const { Router } = require('../../../index'); 2 | const auth = require('./auth'); 3 | // 支持异步获取 4 | const getServers = async () => { 5 | return [ 6 | { 7 | host: '127.0.0.1', 8 | port: 7001, 9 | }, 10 | { 11 | host: '127.0.0.1', 12 | port: 7002, 13 | }, 14 | ]; 15 | }; 16 | const clusterRouter = new Router(getServers); 17 | const envMap = { 18 | 1: { 19 | space: '测试空间1', 20 | group: '测试分组1', 21 | env: '测试环境1', 22 | }, 23 | 12: { 24 | space: '测试空间1', 25 | group: '测试分组2', 26 | env: '测试环境1', 27 | }, 28 | 123: { 29 | space: '测试空间1', 30 | group: '测试分组2', 31 | env: '测试环境3', 32 | }, 33 | 2: { 34 | space: '测试空间2', 35 | group: '测试分组2', 36 | env: '测试环境2', 37 | }, 38 | 3: { 39 | space: '测试空间3', 40 | group: '测试分组3', 41 | env: '测试环境3', 42 | }, 43 | 4: { 44 | space: '测试空间4', 45 | group: '测试分组4', 46 | env: '测试环境4', 47 | }, 48 | 5: { 49 | space: '测试空间5', 50 | group: '测试分组5', 51 | env: '测试环境5', 52 | }, 53 | }; 54 | const envInfo = envMap[1]; 55 | 56 | module.exports = async (req/* , res */) => { 57 | const result = await auth(req); 58 | return result || { 59 | ...envInfo, 60 | router: clusterRouter, 61 | resRulesUrl: 'http://127.0.0.1:7003', 62 | }; 63 | }; 64 | -------------------------------------------------------------------------------- /packages/gateway/test/gateway/lib/sniCallback.js: -------------------------------------------------------------------------------- 1 | 2 | module.exports = (servername/* , req */) => { 3 | return servername.includes('ke.qq.com'); 4 | }; 5 | -------------------------------------------------------------------------------- /packages/gateway/test/plugins/resRules.js: -------------------------------------------------------------------------------- 1 | const { createServer } = require('http'); 2 | 3 | const server = createServer((req, res) => { 4 | res.on('error', () => {}); 5 | res.end(JSON.stringify({ rules: '* resHeaders://x-test1=123' })); 6 | }); 7 | 8 | server.listen(7003); 9 | -------------------------------------------------------------------------------- /packages/plugins/whistle.nohost-security/README.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tencent/nohost/d8994d9709eedd980ab6aeb359224896e67e09cd/packages/plugins/whistle.nohost-security/README.md -------------------------------------------------------------------------------- /packages/plugins/whistle.nohost-settings/README.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tencent/nohost/d8994d9709eedd980ab6aeb359224896e67e09cd/packages/plugins/whistle.nohost-settings/README.md -------------------------------------------------------------------------------- /packages/server/package.json: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tencent/nohost/d8994d9709eedd980ab6aeb359224896e67e09cd/packages/server/package.json -------------------------------------------------------------------------------- /packages/tools/app/README.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tencent/nohost/d8994d9709eedd980ab6aeb359224896e67e09cd/packages/tools/app/README.md -------------------------------------------------------------------------------- /packages/tools/pc/README.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tencent/nohost/d8994d9709eedd980ab6aeb359224896e67e09cd/packages/tools/pc/README.md -------------------------------------------------------------------------------- /packages/tools/router/.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 | -------------------------------------------------------------------------------- /packages/tools/router/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 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/tools/router/README.md: -------------------------------------------------------------------------------- 1 | # router 2 | > 以下需要使用最新版的 nohost:https://github.com/nohosts/nohost 3 | 4 | nohost 集群部署时,用来分发请求到各个节点的路由器。 5 | 6 | # 安装 7 | ``` sh 8 | npm i --save @nohost/router 9 | ``` 10 | 11 | # 使用 12 | 13 | ``` js 14 | const Router = require('@nohost/router'); 15 | 16 | const { 17 | SPACE_NAME, 18 | GROUP_NAME, 19 | ENV_NAME, 20 | NOHOST_RULE, 21 | NOHOST_VALUE, 22 | CLIENT_ID, 23 | CLIENT_ID_FILTER, 24 | writeHead, 25 | writeError, 26 | } = Router; 27 | // 初始化传人部署的 nohost 服务器列表 28 | const router = new Router([ 29 | { 30 | host: '10.11.12.13', 31 | port: 8080 32 | }, 33 | ... 34 | ]); 35 | 36 | // 更新服务器列表 37 | 38 | 39 | // 支持http、websocket、tunnel 40 | try { 41 | const svrRes = await router.proxy(req, res); 42 | writeHead(res, svrRes); 43 | svrRes.pipe(res); 44 | } catch (err) { 45 | writeError(res, err); 46 | } 47 | 48 | // 查看抓包请求 49 | router.proxyUI(req, res); 50 | ``` 51 | 52 | #### 更新服务器列表 53 | ``` js 54 | router.update([ 55 | { 56 | host: '10.11.12.13', 57 | port: 8080 58 | }, 59 | { 60 | host: '10.31.32.33', 61 | port: 8080 62 | }, 63 | ... 64 | ]); 65 | ``` 66 | > router 每 12s 会检测一遍所有服务,并剔除不可用的 67 | 68 | #### 转发正常请求 69 | ``` js 70 | const getOptions = (req) => { 71 | const { headers } = req; 72 | const spaceName = 'imweb'; 73 | let gruopName; 74 | let envName; 75 | if (headers.host === 'km.oa2.com') { 76 | gruopName = 'avenwu'; 77 | envName = '测试'; // 可选 78 | } else if (req.headers.host !== 'km.oa.com') { 79 | gruopName = 'avenwu2'; 80 | envName = '测试2'; // 可选 81 | } 82 | 83 | return { 84 | rules: 'file://{test.html} km.oa2.com www.test2.com', 85 | values: { 'test.html': 'hell world.' }, 86 | spaceName, 87 | gruopName, 88 | envName, 89 | callback: console.log, // 可选 90 | // clientId: 'test', // 如果从外网转发过来的带登录态请求,设置下 clientId 方便插件当前用户的请求抓包 91 | }; 92 | }; 93 | 94 | router.proxy(req, res, getOptions(req)); 95 | 96 | // 或自己处理响应 97 | // const svrRes = await router.proxy(req); 98 | 99 | ``` 100 | #### 查看抓包数据 101 | ``` js 102 | router.proxyUI(req, res, getOptions(req)); 103 | ``` 104 | 105 | ### 只转发到指定 Nohost 服务 106 | ``` js 107 | const Router = require('@nohost/router'); 108 | 109 | const router = new Router({ 110 | host: '10.x.x.x', 111 | port: 8080, 112 | }); 113 | 114 | router.proxy(req, res); // 同测试用例,无需设置 space / group,env 按需设置 115 | router.proxyUI(req, res); // 同测试用例,无需设置 space / group,env 按需设置 116 | ``` 117 | 118 | 具体实现参考:[测试用例](./test/README.md) 119 | 120 | -------------------------------------------------------------------------------- /packages/tools/router/lib/address.js: -------------------------------------------------------------------------------- 1 | const os = require('os'); 2 | 3 | const LOCALHOST = '127.0.0.1'; 4 | let addressList = []; 5 | let serverIp; 6 | 7 | (function updateSystyemInfo() { 8 | const interfaces = os.networkInterfaces(); 9 | addressList = []; 10 | Object.keys(interfaces).forEach((name) => { 11 | const list = interfaces[name]; 12 | if (Array.isArray(list)) { 13 | list.forEach((info) => { 14 | if (!info.internal && info.family === 'IPv4') { 15 | serverIp = info.address; 16 | } 17 | addressList.push(info.address.toLowerCase()); 18 | }); 19 | } 20 | }); 21 | setTimeout(updateSystyemInfo, 30000).unref(); 22 | }()); 23 | 24 | exports.getAddressList = () => addressList; 25 | 26 | const isLocalHost = (ip) => { 27 | if (!ip || typeof ip !== 'string') { 28 | return true; 29 | } 30 | return ip.length < 7 || ip === LOCALHOST; 31 | }; 32 | 33 | const isLocalAddress = (address) => { 34 | if (isLocalHost(address)) { 35 | return true; 36 | } 37 | if (address === '0:0:0:0:0:0:0:1') { 38 | return true; 39 | } 40 | address = address.toLowerCase(); 41 | if (address[0] === '[') { 42 | address = address.slice(1, -1); 43 | } 44 | return addressList.indexOf(address) !== -1; 45 | }; 46 | 47 | exports.isLocalAddress = isLocalAddress; 48 | 49 | exports.getServerIp = () => serverIp; 50 | -------------------------------------------------------------------------------- /packages/tools/router/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@nohost/router", 3 | "version": "1.1.0", 4 | "description": "Nohost cluster router", 5 | "main": "lib/", 6 | "scripts": { 7 | "lint": "eslint lib", 8 | "lintfix": "eslint --fix lib", 9 | "test": "" 10 | }, 11 | "registry": "https://registry.npmjs.org/@nohost/router", 12 | "repository": { 13 | "type": "git", 14 | "url": "git+https://github.com/Tencent/nohost.git" 15 | }, 16 | "keywords": [ 17 | "whistle", 18 | "nohost", 19 | "cluster" 20 | ], 21 | "author": "avwu ", 22 | "license": "MIT", 23 | "bugs": { 24 | "url": "https://github.com/Tencent/nohost/issues" 25 | }, 26 | "homepage": "https://github.com/Tencent/nohost#readme", 27 | "dependencies": { 28 | "hparser": "^0.4.0" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /packages/tools/router/test/README.md: -------------------------------------------------------------------------------- 1 | 1. 启动正常请求转发服务: [node proxy.js](./proxy.js) 2 | 2. 启动查看抓包转发服务:[node data.js](./data.js) 3 | > 也可以将上述两个服务合成一个,通过域名做区分 4 | 3. 在 [whistle](https://github.com/avwo/whistle) 上设置规则: 5 | ``` txt 6 | # 查看抓包数据 7 | www.test.com internal-proxy://127.0.0.1:6677 8 | 9 | # ke.qq.com 域下的所有请求转到 proxy server 10 | ke.qq.com internal-proxy://127.0.0.1:5566 11 | ``` 12 | 4. 如果只是将请求透传到某个 Nohost server,可以采用下面到方式: 13 | ``` js 14 | const Router = require('@nohost/router'); 15 | 16 | const { writeHead, writeError } = router; 17 | const router = new Router({ 18 | host: '10.x.x.x', 19 | port: 8080, 20 | }); 21 | 22 | // 同测试用例,无需设置 space / group,env 按需设置 23 | try { 24 | const svrRes = await router.proxy(req, res); 25 | writeHead(svrRes); 26 | svrRes.pipe(res); 27 | } catch (err) { 28 | writeError(err); 29 | } 30 | router.proxyUI(req, res); // 同测试用例,无需设置 space / group,env 按需设置 31 | ``` 32 | 33 | ![whistle规则](https://user-images.githubusercontent.com/11450939/85247237-ae84b380-b47f-11ea-92c7-601fb120ed54.png) 34 | 35 | ![正常请求](https://user-images.githubusercontent.com/11450939/85247348-06bbb580-b480-11ea-8640-6142f6b01e3e.png) 36 | 37 | ![抓包数据](https://user-images.githubusercontent.com/11450939/85247316-ee4b9b00-b47f-11ea-9973-dc5e3f6454c4.png) 38 | -------------------------------------------------------------------------------- /packages/tools/router/test/data.js: -------------------------------------------------------------------------------- 1 | const http = require('http'); 2 | const Koa = require('koa'); 3 | const koaRouter = require('koa-router')(); 4 | const Router = require('../lib'); 5 | 6 | const { 7 | writeHead, 8 | } = Router; 9 | const servers = [ 10 | { 11 | host: '127.0.0.1', 12 | port: 8080, 13 | }, 14 | ]; 15 | const ENV_MAP = { 16 | 1: { 17 | spaceName: 'imweb', 18 | groupName: 'avenwu', 19 | envName: '测试', 20 | }, 21 | 2: { 22 | spaceName: 'imweb', 23 | groupName: 'avenwu2', 24 | envName: '测试2', 25 | clientId: 'test', 26 | // callback: console.log, 27 | }, 28 | }; 29 | 30 | const router = new Router(servers); 31 | const server = http.createServer(); 32 | const app = new Koa(); 33 | 34 | koaRouter.all('/network/:id/(.*)', async (ctx) => { 35 | const network = ENV_MAP[ctx.params.id]; 36 | if (!network) { 37 | return; 38 | } 39 | const { req, res } = ctx; 40 | req.url = req.url.replace(`/network/${ctx.params.id}`, ''); 41 | const svrRes = await router.proxyUI(req, res, network); 42 | writeHead(res, svrRes); 43 | // ctx.status = svrRes.statusCode; 44 | // ctx.set(svrRes.headers); 45 | ctx.body = svrRes; 46 | }); 47 | 48 | app.use(koaRouter.routes()); 49 | app.use(koaRouter.allowedMethods()); 50 | 51 | server.on('request', app.callback()); 52 | server.listen(6677); 53 | -------------------------------------------------------------------------------- /packages/tools/router/test/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tencent/nohost/d8994d9709eedd980ab6aeb359224896e67e09cd/packages/tools/router/test/favicon.ico -------------------------------------------------------------------------------- /packages/tools/router/test/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 抓包数据 8 | 19 | 20 | 21 | 22 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /packages/tools/router/test/proxy.js: -------------------------------------------------------------------------------- 1 | const http = require('http'); 2 | const Router = require('../lib'); 3 | 4 | const { 5 | writeHead, 6 | writeError, 7 | // CLIENT_ID, 8 | } = Router; 9 | // router 会自动去重 10 | const servers = [ 11 | { 12 | host: '127.0.0.1', 13 | port: 8080, 14 | }, 15 | { 16 | host: '127.0.0.1', 17 | port: 8080, 18 | }, 19 | ]; 20 | 21 | const router = new Router(servers); 22 | 23 | const getOptions = (req) => { 24 | const { headers } = req; 25 | const spaceName = 'imweb'; 26 | let groupName; 27 | let envName; 28 | let clientId; 29 | if (headers.host === 'km.oa2.com') { 30 | groupName = 'avenwu'; 31 | envName = '测试'; // 可选 32 | } else if (req.headers.host !== 'km.oa.com') { 33 | groupName = 'avenwu2'; 34 | envName = '测试2'; // 可选 35 | if (req.headers.host === 'ke.qq.com') { 36 | clientId = 'test'; 37 | } 38 | } 39 | 40 | return { 41 | rules: 'file://{test.html} km.oa2.com www.test2.com', 42 | values: { 'test.html': 'hell world.' }, 43 | spaceName, 44 | groupName, 45 | envName, 46 | clientId, 47 | // callback: console.log, // 可选 48 | }; 49 | }; 50 | 51 | const server = http.createServer(async (req, res) => { 52 | try { 53 | const svrRes = await router.proxy(req, res, getOptions(req)); 54 | writeHead(res, svrRes); 55 | svrRes.pipe(res); 56 | } catch (err) { 57 | writeError(res, err); 58 | } 59 | }); 60 | 61 | const handleSocket = async (req, socket) => { 62 | router.proxy(req, socket, getOptions(req)); 63 | }; 64 | // TCP 请求 65 | server.on('connect', handleSocket); 66 | // WebSocket 请求 67 | server.on('upgrade', handleSocket); 68 | 69 | server.listen(5566); 70 | -------------------------------------------------------------------------------- /src/admin/accounts/components/AccountList.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { Row, Col, Button, Form, Input } from 'antd'; 3 | import PropTypes from 'prop-types'; 4 | 5 | const FormItem = Form.Item; 6 | 7 | 8 | class ModPasswordForm extends Component { 9 | formItemLayout = { 10 | labelCol: { span: 6 }, 11 | wrapperCol: { span: 14 }, 12 | }; 13 | 14 | constructor(props) { 15 | super(props); 16 | this.state = { 17 | loading: false, 18 | }; 19 | } 20 | 21 | handelSubmit = (e) => { 22 | e.preventDefault(); 23 | this.props.form.validateFields((err, values) => { 24 | if (!err) { 25 | this.setState({ loading: true }); 26 | this.props.handleSubmit(values.password); 27 | } 28 | }); 29 | }; 30 | 31 | checkPassword = (rule, value, callback) => { 32 | const { form } = this.props; 33 | if (value && value !== form.getFieldValue('password')) { 34 | callback('两次密码不一致!'); 35 | } else { 36 | callback(); 37 | } 38 | }; 39 | 40 | render() { 41 | const { getFieldDecorator } = this.props.form; 42 | return ( 43 |
44 | 45 | { getFieldDecorator('password', { 46 | rules: [{ required: true, message: '请输入密码' }], 47 | })()} 54 | 55 | 56 | { getFieldDecorator('confirm', { 57 | rules: [ 58 | { required: true, message: '请再次输入密码' }, 59 | { validator: this.checkPassword }, 60 | ], 61 | })()} 68 | 69 | 70 | 71 | 74 | 75 | 76 |
77 | ); 78 | } 79 | } 80 | 81 | ModPasswordForm.propTypes = { 82 | handleSubmit: PropTypes.func.isRequired, 83 | }; 84 | 85 | export default Form.create()(ModPasswordForm); 86 | -------------------------------------------------------------------------------- /src/admin/accounts/components/AddNoticeForm.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { Row, Col, Button, Form, Input } from 'antd'; 3 | import PropTypes from 'prop-types'; 4 | 5 | const FormItem = Form.Item; 6 | const MAXLENGTH = 32; 7 | 8 | class AddNoticeForm extends Component { 9 | constructor(props) { 10 | super(props); 11 | this.state = { 12 | loading: false, 13 | }; 14 | } 15 | 16 | handelSubmit = (e) => { 17 | e.preventDefault(); 18 | this.props.form.validateFields((err, values) => { 19 | if (!err) { 20 | this.setState({ loading: true }); 21 | 22 | this.props.handleSubmit(values.notice); 23 | } 24 | }); 25 | }; 26 | 27 | // 失去焦点,清除前后空格 28 | onBlur = ({ target }) => { 29 | const { id, value } = target; 30 | this.props.form.setFieldsValue({ [id]: value.trim() }); 31 | } 32 | 33 | render() { 34 | const { getFieldDecorator } = this.props.form; 35 | return ( 36 |
37 | 38 | { getFieldDecorator('notice', { 39 | initialValue: this.props.notice, 40 | })()} 48 | 49 | 50 | 51 | 54 | 55 | 56 |
57 | ); 58 | } 59 | } 60 | 61 | AddNoticeForm.propTypes = { 62 | handleSubmit: PropTypes.func.isRequired, 63 | notice: PropTypes.string, 64 | }; 65 | 66 | AddNoticeForm.defaultProps = { 67 | notice: '', 68 | }; 69 | 70 | export default Form.create()(AddNoticeForm); 71 | -------------------------------------------------------------------------------- /src/admin/accounts/components/AddUserForm.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { Row, Col, Button, Form, Input } from 'antd'; 3 | import PropTypes from 'prop-types'; 4 | 5 | const FormItem = Form.Item; 6 | 7 | 8 | class AddUserForm extends Component { 9 | formItemLayout = { 10 | labelCol: { span: 4 }, 11 | wrapperCol: { span: 19 }, 12 | }; 13 | 14 | constructor(props) { 15 | super(props); 16 | this.state = { 17 | loading: false, 18 | }; 19 | } 20 | 21 | handelSubmit = (e) => { 22 | e.preventDefault(); 23 | this.props.form.validateFields((err, values) => { 24 | if (!err) { 25 | this.setState({ loading: true }); 26 | this.props.handleSubmit(values); 27 | } 28 | }); 29 | }; 30 | 31 | checkUser = (rule, value, callback) => { 32 | this.props.users.forEach((user) => { 33 | if (user.name === value) { 34 | callback('已存在该账号名'); 35 | } 36 | }); 37 | callback(); 38 | }; 39 | 40 | checkPassword = (rule, value, callback) => { 41 | const { form } = this.props; 42 | if (value && value !== form.getFieldValue('password')) { 43 | callback('两次密码不一致!'); 44 | } else { 45 | callback(); 46 | } 47 | }; 48 | 49 | render() { 50 | const { getFieldDecorator } = this.props.form; 51 | return ( 52 |
53 | 54 | { getFieldDecorator('username', { 55 | rules: [ 56 | { required: true, message: '请输入账号名' }, 57 | { pattern: /^[\w.-]{1,24}$/, message: '只能包含大小写字母、数字、下划线、点或横线' }, 58 | { validator: this.checkUser }, 59 | ], 60 | })()} 66 | 67 | 68 | { getFieldDecorator('password', { 69 | rules: [ 70 | { required: true, message: '请输入密码' }, 71 | ], 72 | })()} 79 | 80 | 81 | { getFieldDecorator('confirm', { 82 | rules: [ 83 | { required: true, message: '请再次输入密码' }, 84 | { validator: this.checkPassword }, 85 | ], 86 | })()} 93 | 94 | 95 | 96 | 99 | 100 | 101 |
102 | ); 103 | } 104 | } 105 | 106 | AddUserForm.propTypes = { 107 | handleSubmit: PropTypes.func.isRequired, 108 | users: PropTypes.array.isRequired, 109 | }; 110 | 111 | export default Form.create()(AddUserForm); 112 | -------------------------------------------------------------------------------- /src/admin/accounts/index.css: -------------------------------------------------------------------------------- 1 | /* 旧样式 */ 2 | .p-accounts-action { 3 | white-space: nowrap; 4 | } 5 | .p-accounts-action a { 6 | margin-right: 25px; 7 | } 8 | .p-accounts-action a i { 9 | margin-right: 5px; 10 | } 11 | 12 | /* 新样式 */ 13 | table { 14 | table-layout: fixed; 15 | } 16 | #root .ant-tabs-bar { 17 | margin: 0; 18 | } 19 | .n-settings { 20 | margin: 0 auto; 21 | width: 1024px; 22 | padding-bottom: 60px; 23 | } 24 | 25 | .n-settings textarea { 26 | display: block; 27 | width: 100%; 28 | resize: none; 29 | border: none; 30 | height: 280px; 31 | padding: 5px; 32 | border-bottom-left-radius: 5px; 33 | border-bottom-right-radius: 5px; 34 | color: #fff; 35 | background-color: #002240; 36 | font-family: consolas, monospace !important; 37 | } 38 | 39 | .n-settings fieldset { 40 | margin-top: 50px; 41 | border: 1px solid #ccc; 42 | border-radius: 5px; 43 | } 44 | .n-settings fieldset:nth-child(1) { 45 | margin-top: 20px; 46 | } 47 | 48 | .n-settings legend { 49 | font-size: 16px; 50 | width: auto; 51 | margin: 0 0 0 10px; 52 | padding: 0 8px; 53 | line-height: 1; 54 | } 55 | 56 | .n-settings-bar { 57 | text-align: right; 58 | margin: 0; 59 | padding: 0 20px 6px; 60 | border-bottom: 1px solid #ccc; 61 | } 62 | .n-auth-key-input { 63 | border: none !important; 64 | color: #fff !important; 65 | background-color: #002240 !important; 66 | border-top-left-radius: 0 !important; 67 | border-top-right-radius: 0 !important; 68 | } 69 | 70 | .p-view-capture input { 71 | margin-right: 5px; 72 | } 73 | 74 | .nohost-user-notice-content { 75 | overflow: hidden; 76 | margin-left: 20px; 77 | white-space: nowrap; 78 | text-overflow: ellipsis; 79 | } 80 | -------------------------------------------------------------------------------- /src/admin/ajax.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 $ = require('jquery'); 10 | // 生成调用后台接口的方法 11 | function createCgi(url, settings) { 12 | const self = this; 13 | if (typeof url === 'string') { 14 | url = { url }; 15 | } 16 | settings = $.extend({ dataType: 'json' }, settings, url); 17 | url = url.url; 18 | const queue = []; 19 | let jqXhr; 20 | 21 | const cgiFn = function (data, callback, options) { 22 | const opts = { url: typeof url === 'function' ? url() : url }; 23 | if (typeof data === 'function') { 24 | options = callback; 25 | callback = data; 26 | data = null; 27 | } else { 28 | opts.data = data; 29 | } 30 | 31 | options = $.extend(true, {}, settings, options, opts); 32 | if (jqXhr) { 33 | // 处理请求类型 34 | const { mode } = options; 35 | if (mode === 'ignore') { 36 | return; 37 | } 38 | if (mode === 'cancel') { 39 | jqXhr.abort(); 40 | } else if (mode === 'chain') { 41 | return queue.push([data, callback, options]); 42 | } 43 | } 44 | 45 | const execCallback = function (result, xhr) { 46 | jqXhr = null; 47 | if (typeof callback === 'function') { 48 | callback.call(this, result, xhr); 49 | } 50 | const args = queue.shift(); 51 | if (args) { 52 | cgiFn.apply(self, args); 53 | } 54 | }; 55 | options.success = function (result, statusText, xhr) { 56 | execCallback.call(this, result, xhr); 57 | }; 58 | options.error = function (xhr) { 59 | execCallback.call(this, false, xhr); 60 | }; 61 | 62 | return (jqXhr = $.ajax(options)); 63 | }; 64 | 65 | return cgiFn; 66 | } 67 | 68 | export default function (obj, settings) { 69 | const cgi = {}; 70 | Object.keys(obj).forEach((name) => { 71 | cgi[name] = createCgi(obj[name], settings); 72 | }); 73 | return cgi; 74 | } 75 | -------------------------------------------------------------------------------- /src/admin/certs/index.css: -------------------------------------------------------------------------------- 1 | .p-certs a i { 2 | margin-right: 5px; 3 | } 4 | .upload-wrapper { 5 | position: relative; 6 | float: right; 7 | width: 110px; 8 | height: 32px; 9 | } 10 | #upload-input, .upload-btn { 11 | position: absolute; 12 | left: 0; 13 | top: 0; 14 | width: 100%; 15 | height: 100%; 16 | } 17 | #upload-input { 18 | opacity: 0; 19 | z-index: 50; 20 | } 21 | 22 | .p-clear-valid-certs { 23 | margin-right: 20px; 24 | } 25 | -------------------------------------------------------------------------------- /src/admin/config/index.css: -------------------------------------------------------------------------------- 1 | .p-config { 2 | padding-bottom: 30px; 3 | } 4 | 5 | .n-editor-dialog .ant-modal-body { 6 | padding: 20px; 7 | } 8 | 9 | .n-editor-dialog-footer { 10 | text-align: right; 11 | margin-top: 20px; 12 | } 13 | 14 | .n-editor-default-pattern { 15 | margin-top: 10px; 16 | font-size: 12px; 17 | } 18 | 19 | .n-editor-default-pattern em { 20 | font-style: normal; 21 | font-weight: 500; 22 | display: inline-block; 23 | background: #f0f0f0; 24 | padding: 3px 10px; 25 | border-radius: 8px; 26 | } 27 | -------------------------------------------------------------------------------- /src/admin/index.css: -------------------------------------------------------------------------------- 1 | .p-container { 2 | overflow: auto; 3 | min-width: 1200px; 4 | } 5 | 6 | .p-full-container { 7 | max-width: unset; 8 | width: 100%; 9 | } 10 | 11 | .p-left-menu { 12 | width: 250px; 13 | padding-top: 10px; 14 | } 15 | 16 | .p-left-menu .ant-menu-item-selected { 17 | background-color: transparent !important; 18 | } 19 | 20 | .p-mid { 21 | padding-right: 250px; 22 | overflow: auto; 23 | } 24 | 25 | .p-mid-con { 26 | min-width: 720px; 27 | max-width: 72%; 28 | margin: 0 auto; 29 | } 30 | 31 | .p-title { 32 | margin: 30px 0 15px; 33 | font-size: 14px; 34 | font-weight: normal; 35 | } 36 | .p-mid-ctn { 37 | background: #fff; 38 | margin-bottom: 30px; 39 | } 40 | 41 | .p-left-menu > ul { 42 | border: none; 43 | background: transparent; 44 | } 45 | 46 | .p-action-bar { 47 | padding: 22px 0 8px; 48 | text-align: right; 49 | width: 100%; 50 | z-index: 1; 51 | line-height: 2.6; 52 | position: relative; 53 | } 54 | 55 | .p-certs-bar { 56 | padding: 15px 20px !important; 57 | } 58 | 59 | .p-action-bar > strong { 60 | position: absolute; 61 | left: 0; 62 | } 63 | 64 | .p-action-bar > span { 65 | color: red; 66 | } 67 | 68 | .p-content { 69 | overflow: auto; 70 | } 71 | 72 | .p-textarea.ant-input { 73 | height: 420px; 74 | } 75 | -------------------------------------------------------------------------------- /src/admin/index.js: -------------------------------------------------------------------------------- 1 | import '../base.less'; 2 | import './index.css'; 3 | import React, { Component, Fragment } from 'react'; 4 | import ReactDOM from 'react-dom'; 5 | import { parse } from 'query-string'; 6 | import NavBar from '../components/navBar'; 7 | import Accounts from './accounts'; 8 | import Config from './config'; 9 | import Certs from './certs'; 10 | import Settings from './settings'; 11 | 12 | const { location } = window; 13 | const query = parse(location.search); 14 | const hideNavBar = query.hideNohostNavBar === 'true'; 15 | const TABS = [ 16 | 'accounts', 17 | 'config', 18 | 'certs', 19 | 'whistle', 20 | 'system', 21 | ]; 22 | 23 | const subMenu = { 24 | accounts: 'accounts', 25 | whistle: 'whistle', 26 | config: 'config/entrySettings', 27 | certs: 'certs', 28 | system: 'system/administrator', 29 | }; 30 | 31 | const getActive = (active) => { 32 | active = active.split('/')[0] || query.active || query.name; 33 | return TABS[TABS.indexOf(active)] || 'accounts'; 34 | }; 35 | 36 | class App extends Component { 37 | tabStatus = {} 38 | 39 | constructor(props) { 40 | super(props); 41 | const active = getActive(location.hash.substring(1)); 42 | this.tabStatus[active] = 1; 43 | this.state = { active }; 44 | } 45 | 46 | componentDidMount() { 47 | const changeTab = (name) => { 48 | if (TABS.indexOf(name) === -1) { 49 | name = 'accounts'; 50 | } 51 | this.onTabChange(name); 52 | }; 53 | try { 54 | const { onNohostPageReady } = window.parent; 55 | if (typeof onNohostPageReady === 'function') { 56 | onNohostPageReady({ changeTab }); 57 | } 58 | } catch (e) {} 59 | } 60 | 61 | onTabChange = (active) => { 62 | this.tabStatus[active] = 1; 63 | location.hash = subMenu[active]; 64 | this.setState({ 65 | active, 66 | }); 67 | } 68 | 69 | onConfigChange = (key) => { 70 | subMenu.config = `config/${key}`; 71 | } 72 | 73 | onSettingsChange = (key) => { 74 | subMenu.system = `system/${key}`; 75 | } 76 | 77 | render() { 78 | const { 79 | accounts, 80 | whistle, 81 | config, 82 | certs, 83 | system, 84 | } = this.tabStatus; 85 | const { 86 | active, 87 | } = this.state; 88 | const showWhistle = active === 'whistle'; 89 | 90 | return ( 91 | 92 | 93 |
94 | { accounts ? : null } 95 | { whistle ? ( 96 |