├── .cz-config.js ├── .gitignore ├── .npmrc ├── .vscode └── settings.json ├── README.md ├── index.html ├── package.json ├── packages ├── localtunnel │ ├── .gitignore │ ├── .travis.yml │ ├── CHANGELOG.md │ ├── LICENSE │ ├── README.md │ ├── bin │ │ └── lt.js │ ├── lib │ │ ├── HeaderHostTransformer.js │ │ ├── Tunnel.js │ │ └── TunnelCluster.js │ ├── localtunnel.js │ ├── localtunnel.spec.js │ ├── package.json │ └── yarn.lock └── preload │ ├── .gitignore │ ├── README.md │ ├── package.json │ ├── src │ └── index.ts │ ├── tsconfig.json │ └── yarn.lock ├── pnpm-lock.yaml ├── pnpm-workspace.yaml ├── public ├── logo.png └── plugin.json ├── src ├── assets │ ├── css │ │ └── vs.css │ ├── icon │ │ ├── add-line.svg │ │ ├── fonts │ │ │ ├── icon.css │ │ │ ├── icon.eot │ │ │ ├── icon.less │ │ │ ├── icon.module.less │ │ │ ├── icon.svg │ │ │ ├── icon.symbol.svg │ │ │ ├── icon.ttf │ │ │ ├── icon.woff │ │ │ ├── icon.woff2 │ │ │ ├── index.html │ │ │ ├── symbol.html │ │ │ └── unicode.html │ │ ├── github-line.svg │ │ └── question-mark.svg │ └── image │ │ └── undraw_void_3ggu.png ├── components │ ├── alert │ │ ├── index.module.scss │ │ └── index.tsx │ ├── help │ │ ├── index.module.scss │ │ └── index.tsx │ ├── icon │ │ ├── index.module.scss │ │ └── index.tsx │ ├── nat-edit │ │ ├── index.module.scss │ │ └── index.tsx │ └── nat-log │ │ ├── index.module.scss │ │ └── index.tsx ├── index.tsx ├── model │ └── nat.ts ├── pages │ └── manager │ │ ├── index.module.scss │ │ └── index.tsx ├── react-app-env.d.ts └── store │ └── server.ts ├── tsconfig.json ├── tsconfig.path.json └── vite.config.ts /.cz-config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | types: [ 3 | { 4 | value: 'feat', 5 | name: '✨ 新功能', 6 | }, 7 | { 8 | value: 'fix', 9 | name: '🐛 bug修复', 10 | }, 11 | { 12 | value: 'refactor', 13 | name: '🎨 样式调整', 14 | }, 15 | { 16 | value: 'perf', 17 | name: '👌 性能优化', 18 | }, 19 | { 20 | value: 'build & ci', 21 | name: '📦 构建与CI修改', 22 | }, 23 | { 24 | value: 'doc', 25 | name: '📖 文档更新', 26 | }, 27 | { 28 | value: 'chore', 29 | name: '🙈 其他修改', 30 | }, 31 | ], 32 | 33 | scopes: [], 34 | 35 | messages: { 36 | type: '提交类型:', 37 | subject: '简短说明:', 38 | confirmCommit: '确认提交?', 39 | }, 40 | 41 | allowCustomScopes: true, 42 | allowBreakingChanges: ['feat', 'fix'], 43 | skipQuestions: ['scope', 'body', 'breaking', 'footer'], 44 | } 45 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # compiled output 2 | /dist 3 | /node_modules 4 | /build 5 | /.cache 6 | /public/index.js 7 | /.parcel-cache 8 | 9 | # Logs 10 | logs 11 | *.log 12 | npm-debug.log* 13 | yarn-debug.log* 14 | yarn-error.log* 15 | lerna-debug.log* 16 | 17 | # OS 18 | .DS_Store 19 | 20 | # Tests 21 | /coverage 22 | /.nyc_output 23 | 24 | # IDEs and editors 25 | /.idea 26 | .project 27 | .classpath 28 | .c9/ 29 | *.launch 30 | .settings/ 31 | *.sublime-workspace 32 | 33 | # IDE - VSCode 34 | .vscode/* 35 | !.vscode/settings.json 36 | !.vscode/tasks.json 37 | !.vscode/launch.json 38 | !.vscode/extensions.json -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | public-hoist-pattern[]=querystring -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "cssvar.extensions": ["src/assets/css/vs.css"] 3 | } 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## 内网穿透 NAT 2 | 3 | 基于 localtunnel 封装的 utools 内网穿透插件 4 | 5 | 欢迎 start ✨ 6 | 7 | ![](https://cdn.jsdelivr.net/gh/lblblong/image-bed@main/1634030014550QQ%E6%88%AA%E5%9B%BE20211012170819.png) 8 | 9 | ## 安装 10 | 11 | 首先下载最新一个 [release](https://github.com/lblblong/nat-utools/releases) 中的 `upx` 插件文件 12 | 13 | 然后呼出 `utools` 输入框,将刚刚下载的 `upx` 插件拖入输入框安装即可 14 | 15 | 现在就可以使用啦,在 `utools` 输入框输入关键词即可打开内网穿透面板:nw,内网穿透 16 | 17 | ## 部分界面 18 | 19 | ![](https://cdn.jsdelivr.net/gh/lblblong/image-bed@main/1634030014550QQ%E6%88%AA%E5%9B%BE20211012170819.png) 20 | ![](https://cdn.jsdelivr.net/gh/lblblong/image-bed@main/1634030036322QQ%E6%88%AA%E5%9B%BE20211012171258.png) 21 | ![](https://cdn.jsdelivr.net/gh/lblblong/image-bed@main/1634030047907QQ%E6%88%AA%E5%9B%BE20211012170837.png) -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 内网穿透 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nat-utools", 3 | "version": "2.0.1", 4 | "description": "", 5 | "homepage": ".", 6 | "author": "lblblong", 7 | "license": "ISC", 8 | "scripts": { 9 | "serve": "vite build -w", 10 | "serve:preload": "cd ./packages/preload && npm run serve", 11 | "build": "npm run build:preload && npm run build:html", 12 | "build:html": "vite build", 13 | "build:preload": "cd ./packages/preload && npm run build", 14 | "commit": "git-cz" 15 | }, 16 | "dependencies": { 17 | "classnames": "^2.3.1", 18 | "dayjs": "^1.10.7", 19 | "is-wsl": "^2.2.0", 20 | "lbl-popups": "^1.1.1", 21 | "mobx": "^6.3.3", 22 | "mobx-react": "^7.2.0", 23 | "react": "^17.0.2", 24 | "react-dom": "^17.0.2" 25 | }, 26 | "devDependencies": { 27 | "@types/localtunnel": "^2.0.1", 28 | "@types/node": "^12.0.0", 29 | "@types/react": "^17.0.0", 30 | "@types/react-dom": "^17.0.0", 31 | "@vitejs/plugin-react": "^1.0.7", 32 | "commitizen": "^4.2.4", 33 | "csstype": "^3.0.9", 34 | "cz-customizable": "^6.3.0", 35 | "react-scripts": "4.0.3", 36 | "sass": "^1.42.1", 37 | "typescript": "^4.1.2", 38 | "utility-types": "^3.10.0", 39 | "utools-helper": "^1.4.6", 40 | "vite": "^2.6.14" 41 | }, 42 | "config": { 43 | "commitizen": { 44 | "path": "node_modules/cz-customizable" 45 | } 46 | }, 47 | "prettier": { 48 | "semi": false, 49 | "singleQuote": true, 50 | "tabWidth": 2, 51 | "printWidth": 120, 52 | "trailingComma": "all", 53 | "jsxSingleQuote": true 54 | }, 55 | "browserslist": { 56 | "production": [ 57 | ">0.2%", 58 | "not dead", 59 | "not op_mini all" 60 | ], 61 | "development": [ 62 | "last 1 chrome version", 63 | "last 1 firefox version", 64 | "last 1 safari version" 65 | ] 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /packages/localtunnel/.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | -------------------------------------------------------------------------------- /packages/localtunnel/.travis.yml: -------------------------------------------------------------------------------- 1 | dist: xenial 2 | language: node_js 3 | node_js: 4 | - "8" 5 | - "10" 6 | - "12" 7 | -------------------------------------------------------------------------------- /packages/localtunnel/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 2.0.2 (2021-09-18) 2 | 3 | - Upgrade dependencies 4 | 5 | # 2.0.1 (2021-01-09) 6 | 7 | - Upgrade dependencies 8 | 9 | # 2.0.0 (2019-09-16) 10 | 11 | - Add support for tunneling a local HTTPS server 12 | - Add support for localtunnel server with IP-based tunnel URLs 13 | - Node.js client API is now Promise-based, with backwards compatibility to callback 14 | - Major refactor of entire codebase using modern ES syntax (requires Node.js v8.3.0 or above) 15 | 16 | # 1.9.2 (2019-06-01) 17 | 18 | - Update debug to 4.1.1 19 | - Update axios to 0.19.0 20 | 21 | # 1.9.1 (2018-09-08) 22 | 23 | - Update debug to 2.6.9 24 | 25 | # 1.9.0 (2018-04-03) 26 | 27 | - Add _request_ event to Tunnel emitter 28 | - Update yargs to support config via environment variables 29 | - Add basic request logging when --print-requests argument is used 30 | 31 | # 1.8.3 (2017-06-11) 32 | 33 | - update request dependency 34 | - update debug dependency 35 | - update openurl dependency 36 | 37 | # 1.8.2 (2016-11-17) 38 | 39 | - fix host header transform 40 | - update request dependency 41 | 42 | # 1.8.1 (2016-01-20) 43 | 44 | - fix bug w/ HostHeaderTransformer and binary data 45 | 46 | # 1.8.0 (2015-11-04) 47 | 48 | - pass socket errors up to top level 49 | 50 | # 1.7.0 (2015-07-22) 51 | 52 | - add short arg options 53 | 54 | # 1.6.0 (2015-05-15) 55 | 56 | - keep sockets alive after connecting 57 | - add --open param to CLI 58 | 59 | # 1.5.0 (2014-10-25) 60 | 61 | - capture all errors on remote socket and restart the tunnel 62 | 63 | # 1.4.0 (2014-08-31) 64 | 65 | - don't emit errors for ETIMEDOUT 66 | 67 | # 1.2.0 / 2014-04-28 68 | 69 | - return `client` from `localtunnel` API instantiation 70 | 71 | # 1.1.0 / 2014-02-24 72 | 73 | - add a host header transform to change the 'Host' header in requests 74 | 75 | # 1.0.0 / 2014-02-14 76 | 77 | - default to localltunnel.me for host 78 | - remove exported `connect` method (just export one function that does the same thing) 79 | - change localtunnel signature to (port, opt, fn) 80 | 81 | # 0.2.2 / 2014-01-09 82 | -------------------------------------------------------------------------------- /packages/localtunnel/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Roman Shtylman 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/localtunnel/README.md: -------------------------------------------------------------------------------- 1 | # localtunnel 2 | 3 | localtunnel exposes your localhost to the world for easy testing and sharing! No need to mess with DNS or deploy just to have others test out your changes. 4 | 5 | Great for working with browser testing tools like browserling or external api callback services like twilio which require a public url for callbacks. 6 | 7 | ## Quickstart 8 | 9 | ``` 10 | npx localtunnel --port 8000 11 | ``` 12 | 13 | ## Installation 14 | 15 | ### Globally 16 | 17 | ``` 18 | npm install -g localtunnel 19 | ``` 20 | 21 | ### As a dependency in your project 22 | 23 | ``` 24 | yarn add localtunnel 25 | ``` 26 | 27 | ## CLI usage 28 | 29 | When localtunnel is installed globally, just use the `lt` command to start the tunnel. 30 | 31 | ``` 32 | lt --port 8000 33 | ``` 34 | 35 | Thats it! It will connect to the tunnel server, setup the tunnel, and tell you what url to use for your testing. This url will remain active for the duration of your session; so feel free to share it with others for happy fun time! 36 | 37 | You can restart your local server all you want, `lt` is smart enough to detect this and reconnect once it is back. 38 | 39 | ### Arguments 40 | 41 | Below are some common arguments. See `lt --help` for additional arguments 42 | 43 | - `--subdomain` request a named subdomain on the localtunnel server (default is random characters) 44 | - `--local-host` proxy to a hostname other than localhost 45 | 46 | You may also specify arguments via env variables. E.x. 47 | 48 | ``` 49 | PORT=3000 lt 50 | ``` 51 | 52 | ## API 53 | 54 | The localtunnel client is also usable through an API (for test integration, automation, etc) 55 | 56 | ### localtunnel(port [,options][,callback]) 57 | 58 | Creates a new localtunnel to the specified local `port`. Will return a Promise that resolves once you have been assigned a public localtunnel url. `options` can be used to request a specific `subdomain`. A `callback` function can be passed, in which case it won't return a Promise. This exists for backwards compatibility with the old Node-style callback API. You may also pass a single options object with `port` as a property. 59 | 60 | ```js 61 | const localtunnel = require('localtunnel'); 62 | 63 | (async () => { 64 | const tunnel = await localtunnel({ port: 3000 }); 65 | 66 | // the assigned public url for your tunnel 67 | // i.e. https://abcdefgjhij.localtunnel.me 68 | tunnel.url; 69 | 70 | tunnel.on('close', () => { 71 | // tunnels are closed 72 | }); 73 | })(); 74 | ``` 75 | 76 | #### options 77 | 78 | - `port` (number) [required] The local port number to expose through localtunnel. 79 | - `subdomain` (string) Request a specific subdomain on the proxy server. **Note** You may not actually receive this name depending on availability. 80 | - `host` (string) URL for the upstream proxy server. Defaults to `https://localtunnel.me`. 81 | - `local_host` (string) Proxy to this hostname instead of `localhost`. This will also cause the `Host` header to be re-written to this value in proxied requests. 82 | - `local_https` (boolean) Enable tunneling to local HTTPS server. 83 | - `local_cert` (string) Path to certificate PEM file for local HTTPS server. 84 | - `local_key` (string) Path to certificate key file for local HTTPS server. 85 | - `local_ca` (string) Path to certificate authority file for self-signed certificates. 86 | - `allow_invalid_cert` (boolean) Disable certificate checks for your local HTTPS server (ignore cert/key/ca options). 87 | 88 | Refer to [tls.createSecureContext](https://nodejs.org/api/tls.html#tls_tls_createsecurecontext_options) for details on the certificate options. 89 | 90 | ### Tunnel 91 | 92 | The `tunnel` instance returned to your callback emits the following events 93 | 94 | | event | args | description | 95 | | ------- | ---- | ------------------------------------------------------------------------------------ | 96 | | request | info | fires when a request is processed by the tunnel, contains _method_ and _path_ fields | 97 | | error | err | fires when an error happens on the tunnel | 98 | | close | | fires when the tunnel has closed | 99 | 100 | The `tunnel` instance has the following methods 101 | 102 | | method | args | description | 103 | | ------ | ---- | ---------------- | 104 | | close | | close the tunnel | 105 | 106 | ## other clients 107 | 108 | Clients in other languages 109 | 110 | _go_ [gotunnelme](https://github.com/NoahShen/gotunnelme) 111 | 112 | _go_ [go-localtunnel](https://github.com/localtunnel/go-localtunnel) 113 | 114 | _C#/.NET_ [localtunnel-client](https://github.com/angelobreuer/localtunnel-client) 115 | 116 | ## server 117 | 118 | See [localtunnel/server](//github.com/localtunnel/server) for details on the server that powers localtunnel. 119 | 120 | ## License 121 | 122 | MIT 123 | -------------------------------------------------------------------------------- /packages/localtunnel/bin/lt.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | /* eslint-disable no-console */ 3 | 4 | const openurl = require('openurl'); 5 | const yargs = require('yargs'); 6 | 7 | const localtunnel = require('../localtunnel'); 8 | const { version } = require('../package'); 9 | 10 | const { argv } = yargs 11 | .usage('Usage: lt --port [num] ') 12 | .env(true) 13 | .option('p', { 14 | alias: 'port', 15 | describe: 'Internal HTTP server port', 16 | }) 17 | .option('h', { 18 | alias: 'host', 19 | describe: 'Upstream server providing forwarding', 20 | default: 'https://localtunnel.me', 21 | }) 22 | .option('s', { 23 | alias: 'subdomain', 24 | describe: 'Request this subdomain', 25 | }) 26 | .option('l', { 27 | alias: 'local-host', 28 | describe: 'Tunnel traffic to this host instead of localhost, override Host header to this host', 29 | }) 30 | .option('local-https', { 31 | describe: 'Tunnel traffic to a local HTTPS server', 32 | }) 33 | .option('local-cert', { 34 | describe: 'Path to certificate PEM file for local HTTPS server', 35 | }) 36 | .option('local-key', { 37 | describe: 'Path to certificate key file for local HTTPS server', 38 | }) 39 | .option('local-ca', { 40 | describe: 'Path to certificate authority file for self-signed certificates', 41 | }) 42 | .option('allow-invalid-cert', { 43 | describe: 'Disable certificate checks for your local HTTPS server (ignore cert/key/ca options)', 44 | }) 45 | .options('o', { 46 | alias: 'open', 47 | describe: 'Opens the tunnel URL in your browser', 48 | }) 49 | .option('print-requests', { 50 | describe: 'Print basic request info', 51 | }) 52 | .require('port') 53 | .boolean('local-https') 54 | .boolean('allow-invalid-cert') 55 | .boolean('print-requests') 56 | .help('help', 'Show this help and exit') 57 | .version(version); 58 | 59 | if (typeof argv.port !== 'number') { 60 | yargs.showHelp(); 61 | console.error('\nInvalid argument: `port` must be a number'); 62 | process.exit(1); 63 | } 64 | 65 | (async () => { 66 | const tunnel = await localtunnel({ 67 | port: argv.port, 68 | host: argv.host, 69 | subdomain: argv.subdomain, 70 | local_host: argv.localHost, 71 | local_https: argv.localHttps, 72 | local_cert: argv.localCert, 73 | local_key: argv.localKey, 74 | local_ca: argv.localCa, 75 | allow_invalid_cert: argv.allowInvalidCert, 76 | }).catch(err => { 77 | throw err; 78 | }); 79 | 80 | tunnel.on('error', err => { 81 | throw err; 82 | }); 83 | 84 | console.log('your url is: %s', tunnel.url); 85 | 86 | /** 87 | * `cachedUrl` is set when using a proxy server that support resource caching. 88 | * This URL generally remains available after the tunnel itself has closed. 89 | * @see https://github.com/localtunnel/localtunnel/pull/319#discussion_r319846289 90 | */ 91 | if (tunnel.cachedUrl) { 92 | console.log('your cachedUrl is: %s', tunnel.cachedUrl); 93 | } 94 | 95 | if (argv.open) { 96 | openurl.open(tunnel.url); 97 | } 98 | 99 | if (argv['print-requests']) { 100 | tunnel.on('request', info => { 101 | console.log(new Date().toString(), info.method, info.path); 102 | }); 103 | } 104 | })(); 105 | -------------------------------------------------------------------------------- /packages/localtunnel/lib/HeaderHostTransformer.js: -------------------------------------------------------------------------------- 1 | const { Transform } = require('stream'); 2 | 3 | class HeaderHostTransformer extends Transform { 4 | constructor(opts = {}) { 5 | super(opts); 6 | this.host = opts.host || 'localhost'; 7 | this.replaced = false; 8 | } 9 | 10 | _transform(data, encoding, callback) { 11 | callback( 12 | null, 13 | this.replaced // after replacing the first instance of the Host header we just become a regular passthrough 14 | ? data 15 | : data.toString().replace(/(\r\n[Hh]ost: )\S+/, (match, $1) => { 16 | this.replaced = true; 17 | return $1 + this.host; 18 | }) 19 | ); 20 | } 21 | } 22 | 23 | module.exports = HeaderHostTransformer; 24 | -------------------------------------------------------------------------------- /packages/localtunnel/lib/Tunnel.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable consistent-return, no-underscore-dangle */ 2 | 3 | const { parse } = require('url'); 4 | const { EventEmitter } = require('events'); 5 | const axios = require('axios'); 6 | const debug = require('debug')('localtunnel:client'); 7 | 8 | axios.interceptors.request.use(function (config) { 9 | // 解决 localtrunnel 返回 401 问题 10 | config.headers['Bypass-Tunnel-Reminder'] = 'true' 11 | return config 12 | }) 13 | 14 | axios.interceptors.response.use( 15 | function (response) { 16 | return response 17 | }, 18 | function (err) { 19 | if (err.response == null) { 20 | return Promise.reject(Error('网络连接异常')) 21 | } else if (err.response.status === 500) { 22 | return Promise.reject(Error('服务器异常')) 23 | } 24 | 25 | return err.response 26 | }, 27 | ) 28 | 29 | const TunnelCluster = require('./TunnelCluster'); 30 | 31 | module.exports = class Tunnel extends EventEmitter { 32 | constructor(opts = {}) { 33 | super(opts); 34 | this.opts = opts; 35 | this.closed = false; 36 | if (!this.opts.host) { 37 | this.opts.host = 'https://localtunnel.me'; 38 | } 39 | } 40 | 41 | _getInfo(body) { 42 | /* eslint-disable camelcase */ 43 | const { id, ip, port, url, cached_url, max_conn_count } = body; 44 | const { host, port: local_port, local_host } = this.opts; 45 | const { local_https, local_cert, local_key, local_ca, allow_invalid_cert } = this.opts; 46 | return { 47 | name: id, 48 | url, 49 | cached_url, 50 | max_conn: max_conn_count || 1, 51 | remote_host: parse(host).hostname, 52 | remote_ip: ip, 53 | remote_port: port, 54 | local_port, 55 | local_host, 56 | local_https, 57 | local_cert, 58 | local_key, 59 | local_ca, 60 | allow_invalid_cert, 61 | }; 62 | /* eslint-enable camelcase */ 63 | } 64 | 65 | // initialize connection 66 | // callback with connection info 67 | _init(cb) { 68 | const opt = this.opts; 69 | const getInfo = this._getInfo.bind(this); 70 | 71 | const params = { 72 | responseType: 'json', 73 | }; 74 | 75 | const baseUri = `${opt.host}/`; 76 | // no subdomain at first, maybe use requested domain 77 | const assignedDomain = opt.subdomain; 78 | // where to quest 79 | const uri = baseUri + (assignedDomain || '?new'); 80 | 81 | (function getUrl() { 82 | axios 83 | .get(uri, params) 84 | .then(res => { 85 | const body = res.data; 86 | debug('got tunnel information', res.data); 87 | if (res.status !== 200) { 88 | const err = new Error( 89 | (body && body.message) || 'localtunnel server returned an error, please try again' 90 | ); 91 | return cb(err); 92 | } 93 | cb(null, getInfo(body)); 94 | }) 95 | .catch(err => { 96 | debug(`tunnel server offline: ${err.message}, retry 1s`); 97 | return setTimeout(getUrl, 1000); 98 | }); 99 | })(); 100 | } 101 | 102 | _establish(info) { 103 | // increase max event listeners so that localtunnel consumers don't get 104 | // warning messages as soon as they setup even one listener. See #71 105 | this.setMaxListeners(info.max_conn + (EventEmitter.defaultMaxListeners || 10)); 106 | 107 | this.tunnelCluster = new TunnelCluster(info); 108 | 109 | // only emit the url the first time 110 | this.tunnelCluster.once('open', () => { 111 | this.emit('url', info.url); 112 | }); 113 | 114 | // re-emit socket error 115 | this.tunnelCluster.on('error', err => { 116 | debug('got socket error', err.message); 117 | this.emit('error', err); 118 | }); 119 | 120 | let tunnelCount = 0; 121 | 122 | // track open count 123 | this.tunnelCluster.on('open', tunnel => { 124 | tunnelCount++; 125 | debug('tunnel open [total: %d]', tunnelCount); 126 | 127 | const closeHandler = () => { 128 | tunnel.destroy(); 129 | }; 130 | 131 | if (this.closed) { 132 | return closeHandler(); 133 | } 134 | 135 | this.once('close', closeHandler); 136 | tunnel.once('close', () => { 137 | this.removeListener('close', closeHandler); 138 | }); 139 | }); 140 | 141 | // when a tunnel dies, open a new one 142 | this.tunnelCluster.on('dead', () => { 143 | tunnelCount--; 144 | debug('tunnel dead [total: %d]', tunnelCount); 145 | if (this.closed) { 146 | return; 147 | } 148 | this.tunnelCluster.open(); 149 | }); 150 | 151 | this.tunnelCluster.on('request', req => { 152 | this.emit('request', req); 153 | }); 154 | 155 | // establish as many tunnels as allowed 156 | for (let count = 0; count < info.max_conn; ++count) { 157 | this.tunnelCluster.open(); 158 | } 159 | } 160 | 161 | open(cb) { 162 | this._init((err, info) => { 163 | if (err) { 164 | return cb(err); 165 | } 166 | 167 | this.clientId = info.name; 168 | this.url = info.url; 169 | 170 | // `cached_url` is only returned by proxy servers that support resource caching. 171 | if (info.cached_url) { 172 | this.cachedUrl = info.cached_url; 173 | } 174 | 175 | this._establish(info); 176 | cb(); 177 | }); 178 | } 179 | 180 | close() { 181 | this.closed = true; 182 | this.emit('close'); 183 | } 184 | }; 185 | -------------------------------------------------------------------------------- /packages/localtunnel/lib/TunnelCluster.js: -------------------------------------------------------------------------------- 1 | const { EventEmitter } = require('events'); 2 | const debug = require('debug')('localtunnel:client'); 3 | const fs = require('fs'); 4 | const net = require('net'); 5 | const tls = require('tls'); 6 | 7 | const HeaderHostTransformer = require('./HeaderHostTransformer'); 8 | 9 | // manages groups of tunnels 10 | module.exports = class TunnelCluster extends EventEmitter { 11 | constructor(opts = {}) { 12 | super(opts); 13 | this.opts = opts; 14 | } 15 | 16 | open() { 17 | const opt = this.opts; 18 | 19 | // Prefer IP if returned by the server 20 | const remoteHostOrIp = opt.remote_ip || opt.remote_host; 21 | const remotePort = opt.remote_port; 22 | const localHost = opt.local_host || 'localhost'; 23 | const localPort = opt.local_port; 24 | const localProtocol = opt.local_https ? 'https' : 'http'; 25 | const allowInvalidCert = opt.allow_invalid_cert; 26 | 27 | debug( 28 | 'establishing tunnel %s://%s:%s <> %s:%s', 29 | localProtocol, 30 | localHost, 31 | localPort, 32 | remoteHostOrIp, 33 | remotePort 34 | ); 35 | 36 | // connection to localtunnel server 37 | const remote = net.connect({ 38 | host: remoteHostOrIp, 39 | port: remotePort, 40 | }); 41 | 42 | remote.setKeepAlive(true); 43 | 44 | remote.on('error', err => { 45 | debug('got remote connection error', err.message); 46 | 47 | // emit connection refused errors immediately, because they 48 | // indicate that the tunnel can't be established. 49 | if (err.code === 'ECONNREFUSED') { 50 | this.emit( 51 | 'error', 52 | new Error( 53 | `connection refused: ${remoteHostOrIp}:${remotePort} (check your firewall settings)` 54 | ) 55 | ); 56 | } 57 | 58 | remote.end(); 59 | }); 60 | 61 | const connLocal = () => { 62 | if (remote.destroyed) { 63 | debug('remote destroyed'); 64 | this.emit('dead'); 65 | return; 66 | } 67 | 68 | debug('connecting locally to %s://%s:%d', localProtocol, localHost, localPort); 69 | remote.pause(); 70 | 71 | if (allowInvalidCert) { 72 | debug('allowing invalid certificates'); 73 | } 74 | 75 | const getLocalCertOpts = () => 76 | allowInvalidCert 77 | ? { rejectUnauthorized: false } 78 | : { 79 | cert: fs.readFileSync(opt.local_cert), 80 | key: fs.readFileSync(opt.local_key), 81 | ca: opt.local_ca ? [fs.readFileSync(opt.local_ca)] : undefined, 82 | }; 83 | 84 | // connection to local http server 85 | const local = opt.local_https 86 | ? tls.connect({ host: localHost, port: localPort, ...getLocalCertOpts() }) 87 | : net.connect({ host: localHost, port: localPort }); 88 | 89 | const remoteClose = () => { 90 | debug('remote close'); 91 | this.emit('dead'); 92 | local.end(); 93 | }; 94 | 95 | remote.once('close', remoteClose); 96 | 97 | // TODO some languages have single threaded servers which makes opening up 98 | // multiple local connections impossible. We need a smarter way to scale 99 | // and adjust for such instances to avoid beating on the door of the server 100 | local.once('error', err => { 101 | debug('local error %s', err.message); 102 | local.end(); 103 | 104 | remote.removeListener('close', remoteClose); 105 | 106 | if (err.code !== 'ECONNREFUSED') { 107 | return remote.end(); 108 | } 109 | 110 | // retrying connection to local server 111 | setTimeout(connLocal, 1000); 112 | }); 113 | 114 | local.once('connect', () => { 115 | debug('connected locally'); 116 | remote.resume(); 117 | 118 | let stream = remote; 119 | 120 | // if user requested specific local host 121 | // then we use host header transform to replace the host header 122 | if (opt.local_host) { 123 | debug('transform Host header to %s', opt.local_host); 124 | stream = remote.pipe(new HeaderHostTransformer({ host: opt.local_host })); 125 | } 126 | 127 | stream.pipe(local).pipe(remote); 128 | 129 | // when local closes, also get a new remote 130 | local.once('close', hadError => { 131 | debug('local connection closed [%s]', hadError); 132 | }); 133 | }); 134 | }; 135 | 136 | remote.on('data', data => { 137 | const match = data.toString().match(/^(\w+) (\S+)/); 138 | if (match) { 139 | this.emit('request', { 140 | method: match[1], 141 | path: match[2], 142 | }); 143 | } 144 | }); 145 | 146 | // tunnel is considered open when remote connects 147 | remote.once('connect', () => { 148 | this.emit('open', remote); 149 | connLocal(); 150 | }); 151 | } 152 | }; 153 | -------------------------------------------------------------------------------- /packages/localtunnel/localtunnel.js: -------------------------------------------------------------------------------- 1 | const Tunnel = require('./lib/Tunnel'); 2 | 3 | module.exports = function localtunnel(arg1, arg2, arg3) { 4 | const options = typeof arg1 === 'object' ? arg1 : { ...arg2, port: arg1 }; 5 | const callback = typeof arg1 === 'object' ? arg2 : arg3; 6 | const client = new Tunnel(options); 7 | if (callback) { 8 | client.open(err => (err ? callback(err) : callback(null, client))); 9 | return client; 10 | } 11 | return new Promise((resolve, reject) => 12 | client.open(err => (err ? reject(err) : resolve(client))) 13 | ); 14 | }; 15 | -------------------------------------------------------------------------------- /packages/localtunnel/localtunnel.spec.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | 3 | const crypto = require('crypto'); 4 | const http = require('http'); 5 | const https = require('https'); 6 | const url = require('url'); 7 | const assert = require('assert'); 8 | 9 | const localtunnel = require('./localtunnel'); 10 | 11 | let fakePort; 12 | 13 | before(done => { 14 | const server = http.createServer(); 15 | server.on('request', (req, res) => { 16 | res.write(req.headers.host); 17 | res.end(); 18 | }); 19 | server.listen(() => { 20 | const { port } = server.address(); 21 | fakePort = port; 22 | done(); 23 | }); 24 | }); 25 | 26 | it('query localtunnel server w/ ident', async done => { 27 | const tunnel = await localtunnel({ port: fakePort }); 28 | assert.ok(new RegExp('^https://.*localtunnel.me$').test(tunnel.url)); 29 | 30 | const parsed = url.parse(tunnel.url); 31 | const opt = { 32 | host: parsed.host, 33 | port: 443, 34 | headers: { host: parsed.hostname }, 35 | path: '/', 36 | }; 37 | 38 | const req = https.request(opt, res => { 39 | res.setEncoding('utf8'); 40 | let body = ''; 41 | 42 | res.on('data', chunk => { 43 | body += chunk; 44 | }); 45 | 46 | res.on('end', () => { 47 | assert(/.*[.]localtunnel[.]me/.test(body), body); 48 | tunnel.close(); 49 | done(); 50 | }); 51 | }); 52 | 53 | req.end(); 54 | }); 55 | 56 | it('request specific domain', async () => { 57 | const subdomain = Math.random() 58 | .toString(36) 59 | .substr(2); 60 | const tunnel = await localtunnel({ port: fakePort, subdomain }); 61 | assert.ok(new RegExp(`^https://${subdomain}.localtunnel.me$`).test(tunnel.url)); 62 | tunnel.close(); 63 | }); 64 | 65 | describe('--local-host localhost', () => { 66 | it('override Host header with local-host', async done => { 67 | const tunnel = await localtunnel({ port: fakePort, local_host: 'localhost' }); 68 | assert.ok(new RegExp('^https://.*localtunnel.me$').test(tunnel.url)); 69 | 70 | const parsed = url.parse(tunnel.url); 71 | const opt = { 72 | host: parsed.host, 73 | port: 443, 74 | headers: { host: parsed.hostname }, 75 | path: '/', 76 | }; 77 | 78 | const req = https.request(opt, res => { 79 | res.setEncoding('utf8'); 80 | let body = ''; 81 | 82 | res.on('data', chunk => { 83 | body += chunk; 84 | }); 85 | 86 | res.on('end', () => { 87 | assert.strictEqual(body, 'localhost'); 88 | tunnel.close(); 89 | done(); 90 | }); 91 | }); 92 | 93 | req.end(); 94 | }); 95 | }); 96 | 97 | describe('--local-host 127.0.0.1', () => { 98 | it('override Host header with local-host', async done => { 99 | const tunnel = await localtunnel({ port: fakePort, local_host: '127.0.0.1' }); 100 | assert.ok(new RegExp('^https://.*localtunnel.me$').test(tunnel.url)); 101 | 102 | const parsed = url.parse(tunnel.url); 103 | const opt = { 104 | host: parsed.host, 105 | port: 443, 106 | headers: { 107 | host: parsed.hostname, 108 | }, 109 | path: '/', 110 | }; 111 | 112 | const req = https.request(opt, res => { 113 | res.setEncoding('utf8'); 114 | let body = ''; 115 | 116 | res.on('data', chunk => { 117 | body += chunk; 118 | }); 119 | 120 | res.on('end', () => { 121 | assert.strictEqual(body, '127.0.0.1'); 122 | tunnel.close(); 123 | done(); 124 | }); 125 | }); 126 | 127 | req.end(); 128 | }); 129 | 130 | it('send chunked request', async done => { 131 | const tunnel = await localtunnel({ port: fakePort, local_host: '127.0.0.1' }); 132 | assert.ok(new RegExp('^https://.*localtunnel.me$').test(tunnel.url)); 133 | 134 | const parsed = url.parse(tunnel.url); 135 | const opt = { 136 | host: parsed.host, 137 | port: 443, 138 | headers: { 139 | host: parsed.hostname, 140 | 'Transfer-Encoding': 'chunked', 141 | }, 142 | path: '/', 143 | }; 144 | 145 | const req = https.request(opt, res => { 146 | res.setEncoding('utf8'); 147 | let body = ''; 148 | 149 | res.on('data', chunk => { 150 | body += chunk; 151 | }); 152 | 153 | res.on('end', () => { 154 | assert.strictEqual(body, '127.0.0.1'); 155 | tunnel.close(); 156 | done(); 157 | }); 158 | }); 159 | 160 | req.end(crypto.randomBytes(1024 * 8).toString('base64')); 161 | }); 162 | }); 163 | -------------------------------------------------------------------------------- /packages/localtunnel/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "localtunnel", 3 | "description": "Expose localhost to the world", 4 | "version": "2.0.2", 5 | "license": "MIT", 6 | "repository": { 7 | "type": "git", 8 | "url": "git://github.com/localtunnel/localtunnel.git" 9 | }, 10 | "author": "Roman Shtylman ", 11 | "contributors": [ 12 | "Roman Shtylman ", 13 | "Gert Hengeveld ", 14 | "Tom Coleman " 15 | ], 16 | "main": "./localtunnel.js", 17 | "bin": { 18 | "lt": "bin/lt.js" 19 | }, 20 | "scripts": { 21 | "test": "mocha --reporter list --timeout 60000 -- *.spec.js" 22 | }, 23 | "dependencies": { 24 | "axios": "0.21.4", 25 | "debug": "4.3.2", 26 | "openurl": "1.1.1", 27 | "yargs": "17.1.1" 28 | }, 29 | "devDependencies": { 30 | "mocha": "~9.1.1" 31 | }, 32 | "engines": { 33 | "node": ">=8.3.0" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /packages/localtunnel/yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | "@ungap/promise-all-settled@1.1.2": 6 | version "1.1.2" 7 | resolved "https://registry.yarnpkg.com/@ungap/promise-all-settled/-/promise-all-settled-1.1.2.tgz#aa58042711d6e3275dd37dc597e5d31e8c290a44" 8 | integrity sha512-sL/cEvJWAnClXw0wHk85/2L0G6Sj8UB0Ctc1TEMbKSsmpRosqhwj9gWgFRZSrBr2f9tiXISwNhCPmlfqUqyb9Q== 9 | 10 | ansi-colors@4.1.1: 11 | version "4.1.1" 12 | resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-4.1.1.tgz#cbb9ae256bf750af1eab344f229aa27fe94ba348" 13 | integrity sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA== 14 | 15 | ansi-regex@^3.0.0: 16 | version "3.0.0" 17 | resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-3.0.0.tgz#ed0317c322064f79466c02966bddb605ab37d998" 18 | integrity sha1-7QMXwyIGT3lGbAKWa922Bas32Zg= 19 | 20 | ansi-regex@^5.0.0: 21 | version "5.0.0" 22 | resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.0.tgz#388539f55179bf39339c81af30a654d69f87cb75" 23 | integrity sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg== 24 | 25 | ansi-styles@^4.0.0, ansi-styles@^4.1.0: 26 | version "4.3.0" 27 | resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937" 28 | integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg== 29 | dependencies: 30 | color-convert "^2.0.1" 31 | 32 | anymatch@~3.1.2: 33 | version "3.1.2" 34 | resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.2.tgz#c0557c096af32f106198f4f4e2a383537e378716" 35 | integrity sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg== 36 | dependencies: 37 | normalize-path "^3.0.0" 38 | picomatch "^2.0.4" 39 | 40 | argparse@^2.0.1: 41 | version "2.0.1" 42 | resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38" 43 | integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q== 44 | 45 | axios@0.21.4: 46 | version "0.21.4" 47 | resolved "https://registry.yarnpkg.com/axios/-/axios-0.21.4.tgz#c67b90dc0568e5c1cf2b0b858c43ba28e2eda575" 48 | integrity sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg== 49 | dependencies: 50 | follow-redirects "^1.14.0" 51 | 52 | balanced-match@^1.0.0: 53 | version "1.0.0" 54 | resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767" 55 | integrity sha1-ibTRmasr7kneFk6gK4nORi1xt2c= 56 | 57 | binary-extensions@^2.0.0: 58 | version "2.1.0" 59 | resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.1.0.tgz#30fa40c9e7fe07dbc895678cd287024dea241dd9" 60 | integrity sha512-1Yj8h9Q+QDF5FzhMs/c9+6UntbD5MkRfRwac8DoEm9ZfUBZ7tZ55YcGVAzEe4bXsdQHEk+s9S5wsOKVdZrw0tQ== 61 | 62 | brace-expansion@^1.1.7: 63 | version "1.1.11" 64 | resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" 65 | integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA== 66 | dependencies: 67 | balanced-match "^1.0.0" 68 | concat-map "0.0.1" 69 | 70 | braces@~3.0.2: 71 | version "3.0.2" 72 | resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107" 73 | integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A== 74 | dependencies: 75 | fill-range "^7.0.1" 76 | 77 | browser-stdout@1.3.1: 78 | version "1.3.1" 79 | resolved "https://registry.yarnpkg.com/browser-stdout/-/browser-stdout-1.3.1.tgz#baa559ee14ced73452229bad7326467c61fabd60" 80 | integrity sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw== 81 | 82 | camelcase@^6.0.0: 83 | version "6.2.0" 84 | resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-6.2.0.tgz#924af881c9d525ac9d87f40d964e5cea982a1809" 85 | integrity sha512-c7wVvbw3f37nuobQNtgsgG9POC9qMbNuMQmTCqZv23b6MIz0fcYpBiOlv9gEN/hdLdnZTDQhg6e9Dq5M1vKvfg== 86 | 87 | chalk@^4.1.0: 88 | version "4.1.2" 89 | resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01" 90 | integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== 91 | dependencies: 92 | ansi-styles "^4.1.0" 93 | supports-color "^7.1.0" 94 | 95 | chokidar@3.5.2: 96 | version "3.5.2" 97 | resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.2.tgz#dba3976fcadb016f66fd365021d91600d01c1e75" 98 | integrity sha512-ekGhOnNVPgT77r4K/U3GDhu+FQ2S8TnK/s2KbIGXi0SZWuwkZ2QNyfWdZW+TVfn84DpEP7rLeCt2UI6bJ8GwbQ== 99 | dependencies: 100 | anymatch "~3.1.2" 101 | braces "~3.0.2" 102 | glob-parent "~5.1.2" 103 | is-binary-path "~2.1.0" 104 | is-glob "~4.0.1" 105 | normalize-path "~3.0.0" 106 | readdirp "~3.6.0" 107 | optionalDependencies: 108 | fsevents "~2.3.2" 109 | 110 | cliui@^7.0.2: 111 | version "7.0.4" 112 | resolved "https://registry.yarnpkg.com/cliui/-/cliui-7.0.4.tgz#a0265ee655476fc807aea9df3df8df7783808b4f" 113 | integrity sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ== 114 | dependencies: 115 | string-width "^4.2.0" 116 | strip-ansi "^6.0.0" 117 | wrap-ansi "^7.0.0" 118 | 119 | color-convert@^2.0.1: 120 | version "2.0.1" 121 | resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3" 122 | integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ== 123 | dependencies: 124 | color-name "~1.1.4" 125 | 126 | color-name@~1.1.4: 127 | version "1.1.4" 128 | resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" 129 | integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== 130 | 131 | concat-map@0.0.1: 132 | version "0.0.1" 133 | resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" 134 | integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s= 135 | 136 | debug@4.3.1: 137 | version "4.3.1" 138 | resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.1.tgz#f0d229c505e0c6d8c49ac553d1b13dc183f6b2ee" 139 | integrity sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ== 140 | dependencies: 141 | ms "2.1.2" 142 | 143 | debug@4.3.2: 144 | version "4.3.2" 145 | resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.2.tgz#f0a49c18ac8779e31d4a0c6029dfb76873c7428b" 146 | integrity sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw== 147 | dependencies: 148 | ms "2.1.2" 149 | 150 | decamelize@^4.0.0: 151 | version "4.0.0" 152 | resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-4.0.0.tgz#aa472d7bf660eb15f3494efd531cab7f2a709837" 153 | integrity sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ== 154 | 155 | diff@5.0.0: 156 | version "5.0.0" 157 | resolved "https://registry.yarnpkg.com/diff/-/diff-5.0.0.tgz#7ed6ad76d859d030787ec35855f5b1daf31d852b" 158 | integrity sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w== 159 | 160 | emoji-regex@^8.0.0: 161 | version "8.0.0" 162 | resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" 163 | integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== 164 | 165 | escalade@^3.1.1: 166 | version "3.1.1" 167 | resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40" 168 | integrity sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw== 169 | 170 | escape-string-regexp@4.0.0: 171 | version "4.0.0" 172 | resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34" 173 | integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA== 174 | 175 | fill-range@^7.0.1: 176 | version "7.0.1" 177 | resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40" 178 | integrity sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ== 179 | dependencies: 180 | to-regex-range "^5.0.1" 181 | 182 | find-up@5.0.0: 183 | version "5.0.0" 184 | resolved "https://registry.yarnpkg.com/find-up/-/find-up-5.0.0.tgz#4c92819ecb7083561e4f4a240a86be5198f536fc" 185 | integrity sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng== 186 | dependencies: 187 | locate-path "^6.0.0" 188 | path-exists "^4.0.0" 189 | 190 | flat@^5.0.2: 191 | version "5.0.2" 192 | resolved "https://registry.yarnpkg.com/flat/-/flat-5.0.2.tgz#8ca6fe332069ffa9d324c327198c598259ceb241" 193 | integrity sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ== 194 | 195 | follow-redirects@^1.14.0: 196 | version "1.14.3" 197 | resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.14.3.tgz#6ada78118d8d24caee595595accdc0ac6abd022e" 198 | integrity sha512-3MkHxknWMUtb23apkgz/83fDoe+y+qr0TdgacGIA7bew+QLBo3vdgEN2xEsuXNivpFy4CyDhBBZnNZOtalmenw== 199 | 200 | fs.realpath@^1.0.0: 201 | version "1.0.0" 202 | resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" 203 | integrity sha1-FQStJSMVjKpA20onh8sBQRmU6k8= 204 | 205 | fsevents@~2.3.2: 206 | version "2.3.2" 207 | resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a" 208 | integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA== 209 | 210 | get-caller-file@^2.0.5: 211 | version "2.0.5" 212 | resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" 213 | integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== 214 | 215 | glob-parent@~5.1.2: 216 | version "5.1.2" 217 | resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4" 218 | integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow== 219 | dependencies: 220 | is-glob "^4.0.1" 221 | 222 | glob@7.1.7: 223 | version "7.1.7" 224 | resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.7.tgz#3b193e9233f01d42d0b3f78294bbeeb418f94a90" 225 | integrity sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ== 226 | dependencies: 227 | fs.realpath "^1.0.0" 228 | inflight "^1.0.4" 229 | inherits "2" 230 | minimatch "^3.0.4" 231 | once "^1.3.0" 232 | path-is-absolute "^1.0.0" 233 | 234 | growl@1.10.5: 235 | version "1.10.5" 236 | resolved "https://registry.yarnpkg.com/growl/-/growl-1.10.5.tgz#f2735dc2283674fa67478b10181059355c369e5e" 237 | integrity sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA== 238 | 239 | has-flag@^4.0.0: 240 | version "4.0.0" 241 | resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" 242 | integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== 243 | 244 | he@1.2.0: 245 | version "1.2.0" 246 | resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f" 247 | integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw== 248 | 249 | inflight@^1.0.4: 250 | version "1.0.6" 251 | resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" 252 | integrity sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk= 253 | dependencies: 254 | once "^1.3.0" 255 | wrappy "1" 256 | 257 | inherits@2: 258 | version "2.0.4" 259 | resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" 260 | integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== 261 | 262 | is-binary-path@~2.1.0: 263 | version "2.1.0" 264 | resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09" 265 | integrity sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw== 266 | dependencies: 267 | binary-extensions "^2.0.0" 268 | 269 | is-extglob@^2.1.1: 270 | version "2.1.1" 271 | resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" 272 | integrity sha1-qIwCU1eR8C7TfHahueqXc8gz+MI= 273 | 274 | is-fullwidth-code-point@^2.0.0: 275 | version "2.0.0" 276 | resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz#a3b30a5c4f199183167aaab93beefae3ddfb654f" 277 | integrity sha1-o7MKXE8ZkYMWeqq5O+764937ZU8= 278 | 279 | is-fullwidth-code-point@^3.0.0: 280 | version "3.0.0" 281 | resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d" 282 | integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg== 283 | 284 | is-glob@^4.0.1, is-glob@~4.0.1: 285 | version "4.0.1" 286 | resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.1.tgz#7567dbe9f2f5e2467bc77ab83c4a29482407a5dc" 287 | integrity sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg== 288 | dependencies: 289 | is-extglob "^2.1.1" 290 | 291 | is-number@^7.0.0: 292 | version "7.0.0" 293 | resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" 294 | integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== 295 | 296 | is-plain-obj@^2.1.0: 297 | version "2.1.0" 298 | resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-2.1.0.tgz#45e42e37fccf1f40da8e5f76ee21515840c09287" 299 | integrity sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA== 300 | 301 | is-unicode-supported@^0.1.0: 302 | version "0.1.0" 303 | resolved "https://registry.yarnpkg.com/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz#3f26c76a809593b52bfa2ecb5710ed2779b522a7" 304 | integrity sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw== 305 | 306 | isexe@^2.0.0: 307 | version "2.0.0" 308 | resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" 309 | integrity sha1-6PvzdNxVb/iUehDcsFctYz8s+hA= 310 | 311 | js-yaml@4.1.0: 312 | version "4.1.0" 313 | resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.0.tgz#c1fb65f8f5017901cdd2c951864ba18458a10602" 314 | integrity sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA== 315 | dependencies: 316 | argparse "^2.0.1" 317 | 318 | locate-path@^6.0.0: 319 | version "6.0.0" 320 | resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-6.0.0.tgz#55321eb309febbc59c4801d931a72452a681d286" 321 | integrity sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw== 322 | dependencies: 323 | p-locate "^5.0.0" 324 | 325 | log-symbols@4.1.0: 326 | version "4.1.0" 327 | resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-4.1.0.tgz#3fbdbb95b4683ac9fc785111e792e558d4abd503" 328 | integrity sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg== 329 | dependencies: 330 | chalk "^4.1.0" 331 | is-unicode-supported "^0.1.0" 332 | 333 | minimatch@3.0.4, minimatch@^3.0.4: 334 | version "3.0.4" 335 | resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" 336 | integrity sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA== 337 | dependencies: 338 | brace-expansion "^1.1.7" 339 | 340 | mocha@~9.1.1: 341 | version "9.1.1" 342 | resolved "https://registry.yarnpkg.com/mocha/-/mocha-9.1.1.tgz#33df2eb9c6262434630510c5f4283b36efda9b61" 343 | integrity sha512-0wE74YMgOkCgBUj8VyIDwmLUjTsS13WV1Pg7l0SHea2qzZzlq7MDnfbPsHKcELBRk3+izEVkRofjmClpycudCA== 344 | dependencies: 345 | "@ungap/promise-all-settled" "1.1.2" 346 | ansi-colors "4.1.1" 347 | browser-stdout "1.3.1" 348 | chokidar "3.5.2" 349 | debug "4.3.1" 350 | diff "5.0.0" 351 | escape-string-regexp "4.0.0" 352 | find-up "5.0.0" 353 | glob "7.1.7" 354 | growl "1.10.5" 355 | he "1.2.0" 356 | js-yaml "4.1.0" 357 | log-symbols "4.1.0" 358 | minimatch "3.0.4" 359 | ms "2.1.3" 360 | nanoid "3.1.23" 361 | serialize-javascript "6.0.0" 362 | strip-json-comments "3.1.1" 363 | supports-color "8.1.1" 364 | which "2.0.2" 365 | wide-align "1.1.3" 366 | workerpool "6.1.5" 367 | yargs "16.2.0" 368 | yargs-parser "20.2.4" 369 | yargs-unparser "2.0.0" 370 | 371 | ms@2.1.2: 372 | version "2.1.2" 373 | resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" 374 | integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== 375 | 376 | ms@2.1.3: 377 | version "2.1.3" 378 | resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" 379 | integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== 380 | 381 | nanoid@3.1.23: 382 | version "3.1.23" 383 | resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.1.23.tgz#f744086ce7c2bc47ee0a8472574d5c78e4183a81" 384 | integrity sha512-FiB0kzdP0FFVGDKlRLEQ1BgDzU87dy5NnzjeW9YZNt+/c3+q82EQDUwniSAUxp/F0gFNI1ZhKU1FqYsMuqZVnw== 385 | 386 | normalize-path@^3.0.0, normalize-path@~3.0.0: 387 | version "3.0.0" 388 | resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" 389 | integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== 390 | 391 | once@^1.3.0: 392 | version "1.4.0" 393 | resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" 394 | integrity sha1-WDsap3WWHUsROsF9nFC6753Xa9E= 395 | dependencies: 396 | wrappy "1" 397 | 398 | openurl@1.1.1: 399 | version "1.1.1" 400 | resolved "https://registry.yarnpkg.com/openurl/-/openurl-1.1.1.tgz#3875b4b0ef7a52c156f0db41d4609dbb0f94b387" 401 | integrity sha1-OHW0sO96UsFW8NtB1GCduw+Us4c= 402 | 403 | p-limit@^3.0.2: 404 | version "3.1.0" 405 | resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-3.1.0.tgz#e1daccbe78d0d1388ca18c64fea38e3e57e3706b" 406 | integrity sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ== 407 | dependencies: 408 | yocto-queue "^0.1.0" 409 | 410 | p-locate@^5.0.0: 411 | version "5.0.0" 412 | resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-5.0.0.tgz#83c8315c6785005e3bd021839411c9e110e6d834" 413 | integrity sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw== 414 | dependencies: 415 | p-limit "^3.0.2" 416 | 417 | path-exists@^4.0.0: 418 | version "4.0.0" 419 | resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3" 420 | integrity sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w== 421 | 422 | path-is-absolute@^1.0.0: 423 | version "1.0.1" 424 | resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" 425 | integrity sha1-F0uSaHNVNP+8es5r9TpanhtcX18= 426 | 427 | picomatch@^2.0.4, picomatch@^2.2.1: 428 | version "2.2.2" 429 | resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.2.2.tgz#21f333e9b6b8eaff02468f5146ea406d345f4dad" 430 | integrity sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg== 431 | 432 | randombytes@^2.1.0: 433 | version "2.1.0" 434 | resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a" 435 | integrity sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ== 436 | dependencies: 437 | safe-buffer "^5.1.0" 438 | 439 | readdirp@~3.6.0: 440 | version "3.6.0" 441 | resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.6.0.tgz#74a370bd857116e245b29cc97340cd431a02a6c7" 442 | integrity sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA== 443 | dependencies: 444 | picomatch "^2.2.1" 445 | 446 | require-directory@^2.1.1: 447 | version "2.1.1" 448 | resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" 449 | integrity sha1-jGStX9MNqxyXbiNE/+f3kqam30I= 450 | 451 | safe-buffer@^5.1.0: 452 | version "5.2.1" 453 | resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" 454 | integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== 455 | 456 | serialize-javascript@6.0.0: 457 | version "6.0.0" 458 | resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-6.0.0.tgz#efae5d88f45d7924141da8b5c3a7a7e663fefeb8" 459 | integrity sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag== 460 | dependencies: 461 | randombytes "^2.1.0" 462 | 463 | "string-width@^1.0.2 || 2": 464 | version "2.1.1" 465 | resolved "https://registry.yarnpkg.com/string-width/-/string-width-2.1.1.tgz#ab93f27a8dc13d28cac815c462143a6d9012ae9e" 466 | integrity sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw== 467 | dependencies: 468 | is-fullwidth-code-point "^2.0.0" 469 | strip-ansi "^4.0.0" 470 | 471 | string-width@^4.1.0, string-width@^4.2.0: 472 | version "4.2.0" 473 | resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.0.tgz#952182c46cc7b2c313d1596e623992bd163b72b5" 474 | integrity sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg== 475 | dependencies: 476 | emoji-regex "^8.0.0" 477 | is-fullwidth-code-point "^3.0.0" 478 | strip-ansi "^6.0.0" 479 | 480 | strip-ansi@^4.0.0: 481 | version "4.0.0" 482 | resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-4.0.0.tgz#a8479022eb1ac368a871389b635262c505ee368f" 483 | integrity sha1-qEeQIusaw2iocTibY1JixQXuNo8= 484 | dependencies: 485 | ansi-regex "^3.0.0" 486 | 487 | strip-ansi@^6.0.0: 488 | version "6.0.0" 489 | resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.0.tgz#0b1571dd7669ccd4f3e06e14ef1eed26225ae532" 490 | integrity sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w== 491 | dependencies: 492 | ansi-regex "^5.0.0" 493 | 494 | strip-json-comments@3.1.1: 495 | version "3.1.1" 496 | resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006" 497 | integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig== 498 | 499 | supports-color@8.1.1: 500 | version "8.1.1" 501 | resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-8.1.1.tgz#cd6fc17e28500cff56c1b86c0a7fd4a54a73005c" 502 | integrity sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q== 503 | dependencies: 504 | has-flag "^4.0.0" 505 | 506 | supports-color@^7.1.0: 507 | version "7.2.0" 508 | resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da" 509 | integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw== 510 | dependencies: 511 | has-flag "^4.0.0" 512 | 513 | to-regex-range@^5.0.1: 514 | version "5.0.1" 515 | resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4" 516 | integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ== 517 | dependencies: 518 | is-number "^7.0.0" 519 | 520 | which@2.0.2: 521 | version "2.0.2" 522 | resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1" 523 | integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA== 524 | dependencies: 525 | isexe "^2.0.0" 526 | 527 | wide-align@1.1.3: 528 | version "1.1.3" 529 | resolved "https://registry.yarnpkg.com/wide-align/-/wide-align-1.1.3.tgz#ae074e6bdc0c14a431e804e624549c633b000457" 530 | integrity sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA== 531 | dependencies: 532 | string-width "^1.0.2 || 2" 533 | 534 | workerpool@6.1.5: 535 | version "6.1.5" 536 | resolved "https://registry.yarnpkg.com/workerpool/-/workerpool-6.1.5.tgz#0f7cf076b6215fd7e1da903ff6f22ddd1886b581" 537 | integrity sha512-XdKkCK0Zqc6w3iTxLckiuJ81tiD/o5rBE/m+nXpRCB+/Sq4DqkfXZ/x0jW02DG1tGsfUGXbTJyZDP+eu67haSw== 538 | 539 | wrap-ansi@^7.0.0: 540 | version "7.0.0" 541 | resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" 542 | integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== 543 | dependencies: 544 | ansi-styles "^4.0.0" 545 | string-width "^4.1.0" 546 | strip-ansi "^6.0.0" 547 | 548 | wrappy@1: 549 | version "1.0.2" 550 | resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" 551 | integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8= 552 | 553 | y18n@^5.0.5: 554 | version "5.0.5" 555 | resolved "https://registry.yarnpkg.com/y18n/-/y18n-5.0.5.tgz#8769ec08d03b1ea2df2500acef561743bbb9ab18" 556 | integrity sha512-hsRUr4FFrvhhRH12wOdfs38Gy7k2FFzB9qgN9v3aLykRq0dRcdcpz5C9FxdS2NuhOrI/628b/KSTJ3rwHysYSg== 557 | 558 | yargs-parser@20.2.4, yargs-parser@^20.2.2: 559 | version "20.2.4" 560 | resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.4.tgz#b42890f14566796f85ae8e3a25290d205f154a54" 561 | integrity sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA== 562 | 563 | yargs-unparser@2.0.0: 564 | version "2.0.0" 565 | resolved "https://registry.yarnpkg.com/yargs-unparser/-/yargs-unparser-2.0.0.tgz#f131f9226911ae5d9ad38c432fe809366c2325eb" 566 | integrity sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA== 567 | dependencies: 568 | camelcase "^6.0.0" 569 | decamelize "^4.0.0" 570 | flat "^5.0.2" 571 | is-plain-obj "^2.1.0" 572 | 573 | yargs@16.2.0: 574 | version "16.2.0" 575 | resolved "https://registry.yarnpkg.com/yargs/-/yargs-16.2.0.tgz#1c82bf0f6b6a66eafce7ef30e376f49a12477f66" 576 | integrity sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw== 577 | dependencies: 578 | cliui "^7.0.2" 579 | escalade "^3.1.1" 580 | get-caller-file "^2.0.5" 581 | require-directory "^2.1.1" 582 | string-width "^4.2.0" 583 | y18n "^5.0.5" 584 | yargs-parser "^20.2.2" 585 | 586 | yargs@17.1.1: 587 | version "17.1.1" 588 | resolved "https://registry.yarnpkg.com/yargs/-/yargs-17.1.1.tgz#c2a8091564bdb196f7c0a67c1d12e5b85b8067ba" 589 | integrity sha512-c2k48R0PwKIqKhPMWjeiF6y2xY/gPMUlro0sgxqXpbOIohWiLNXWslsootttv7E1e73QPAMQSg5FeySbVcpsPQ== 590 | dependencies: 591 | cliui "^7.0.2" 592 | escalade "^3.1.1" 593 | get-caller-file "^2.0.5" 594 | require-directory "^2.1.1" 595 | string-width "^4.2.0" 596 | y18n "^5.0.5" 597 | yargs-parser "^20.2.2" 598 | 599 | yocto-queue@^0.1.0: 600 | version "0.1.0" 601 | resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" 602 | integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q== 603 | -------------------------------------------------------------------------------- /packages/preload/.gitignore: -------------------------------------------------------------------------------- 1 | # compiled output 2 | /dist 3 | /node_modules 4 | /.cache 5 | # Logs 6 | logs 7 | *.log 8 | npm-debug.log* 9 | yarn-debug.log* 10 | yarn-error.log* 11 | lerna-debug.log* 12 | /.parcel-cache 13 | 14 | # OS 15 | .DS_Store 16 | 17 | # Tests 18 | /coverage 19 | /.nyc_output 20 | 21 | # IDEs and editors 22 | /.idea 23 | .project 24 | .classpath 25 | .c9/ 26 | *.launch 27 | .settings/ 28 | *.sublime-workspace 29 | 30 | # IDE - VSCode 31 | .vscode/* 32 | !.vscode/settings.json 33 | !.vscode/tasks.json 34 | !.vscode/launch.json 35 | !.vscode/extensions.json -------------------------------------------------------------------------------- /packages/preload/README.md: -------------------------------------------------------------------------------- 1 | #### preload 2 | 3 | ../utools/index.js 文件 -------------------------------------------------------------------------------- /packages/preload/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "preload", 3 | "version": "0.0.1", 4 | "private": true, 5 | "homepage": ".", 6 | "app": "../../public/index.js", 7 | "targets": { 8 | "app": { 9 | "source": "src/index.ts", 10 | "context": "node", 11 | "sourceMap": false, 12 | "includeNodeModules": true 13 | } 14 | }, 15 | "scripts": { 16 | "build": "rm -rf ../public/index.js && parcel build" 17 | }, 18 | "dependencies": { 19 | "localtunnel": "^2.0.2", 20 | "tslib": "^2.1.0" 21 | }, 22 | "devDependencies": { 23 | "@types/localtunnel": "^2.0.1", 24 | "parcel": "^2.0.0-rc.0" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /packages/preload/src/index.ts: -------------------------------------------------------------------------------- 1 | import localtunnel from 'localtunnel' 2 | 3 | declare global { 4 | interface Window { 5 | localtunnel: typeof localtunnel 6 | } 7 | } 8 | 9 | window.localtunnel = localtunnel 10 | -------------------------------------------------------------------------------- /packages/preload/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES5", 4 | "module": "commonjs", 5 | "strict": false, 6 | "noImplicitAny": false, 7 | "experimentalDecorators": true, 8 | "emitDecoratorMetadata": true, 9 | "charset": "utf8", 10 | "allowJs": false, 11 | "pretty": true, 12 | "noEmitOnError": false, 13 | "noUnusedLocals": false, 14 | "noUnusedParameters": true, 15 | "allowUnreachableCode": false, 16 | "allowUnusedLabels": false, 17 | "strictPropertyInitialization": false, 18 | "noFallthroughCasesInSwitch": true, 19 | "skipLibCheck": true, 20 | "skipDefaultLibCheck": true, 21 | "inlineSourceMap": true, 22 | "importHelpers": true, 23 | "outDir": "dist", 24 | "sourceMap": false, 25 | "esModuleInterop": true, 26 | "types": [ 27 | "utools-helper/@types/utools", 28 | "utools-helper/@types/electron", 29 | "@types/node" 30 | ] 31 | }, 32 | "include": ["src"] 33 | } 34 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - 'packages/**' -------------------------------------------------------------------------------- /public/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lblblong/nat-utools/e449e92f5a326178a1f5d0339aff7692a379d4f2/public/logo.png -------------------------------------------------------------------------------- /public/plugin.json: -------------------------------------------------------------------------------- 1 | { 2 | "pluginName": "内网穿透NAT", 3 | "description": "内网穿透", 4 | "author": "lblblong", 5 | "homepage": "https://github.com/lblblong/nat-utools", 6 | "main": "./index.html", 7 | "preload": "./index.js", 8 | "version": "2.0.0", 9 | "logo": "./logo.png", 10 | "platform": ["win32", "darwin", "linux"], 11 | "features": [ 12 | { 13 | "code": "startLiveServer", 14 | "explain": "内网穿透管理面板", 15 | "cmds": ["内网穿透"] 16 | } 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /src/assets/css/vs.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --color-primary: #5d9cec; 3 | --color-third: #f2237b; 4 | } 5 | -------------------------------------------------------------------------------- /src/assets/icon/add-line.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/icon/fonts/icon.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: "icon"; 3 | src: url('icon.eot?t=1627271109350'); /* IE9*/ 4 | src: url('icon.eot?t=1627271109350#iefix') format('embedded-opentype'), /* IE6-IE8 */ 5 | url("icon.woff2?t=1627271109350") format("woff2"), 6 | url("icon.woff?t=1627271109350") format("woff"), 7 | url('icon.ttf?t=1627271109350') format('truetype'), /* chrome, firefox, opera, Safari, Android, iOS 4.2+*/ 8 | url('icon.svg?t=1627271109350#icon') format('svg'); /* iOS 4.1- */ 9 | } 10 | 11 | [class^="icon-"], [class*=" icon-"] { 12 | font-family: 'icon' !important; 13 | font-size:16px; 14 | font-style:normal; 15 | -webkit-font-smoothing: antialiased; 16 | -moz-osx-font-smoothing: grayscale; 17 | } 18 | 19 | 20 | .icon-add-line:before { content: "\ea01"; } 21 | .icon-github-line:before { content: "\ea02"; } 22 | .icon-question-mark:before { content: "\ea03"; } 23 | -------------------------------------------------------------------------------- /src/assets/icon/fonts/icon.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lblblong/nat-utools/e449e92f5a326178a1f5d0339aff7692a379d4f2/src/assets/icon/fonts/icon.eot -------------------------------------------------------------------------------- /src/assets/icon/fonts/icon.less: -------------------------------------------------------------------------------- 1 | @font-face {font-family: "icon"; 2 | src: url('icon.eot?t=1627271109350'); /* IE9*/ 3 | src: url('icon.eot?t=1627271109350#iefix') format('embedded-opentype'), /* IE6-IE8 */ 4 | url("icon.woff2?t=1627271109350") format("woff2"), 5 | url("icon.woff?t=1627271109350") format("woff"), 6 | url('icon.ttf?t=1627271109350') format('truetype'), /* chrome, firefox, opera, Safari, Android, iOS 4.2+*/ 7 | url('icon.svg?t=1627271109350#icon') format('svg'); /* iOS 4.1- */ 8 | } 9 | 10 | [class^="icon-"], [class*=" icon-"] { 11 | font-family: 'icon' !important; 12 | font-size:16px; 13 | font-style:normal; 14 | -webkit-font-smoothing: antialiased; 15 | -moz-osx-font-smoothing: grayscale; 16 | } 17 | 18 | .icon-add-line:before { content: "\ea01"; } 19 | .icon-github-line:before { content: "\ea02"; } 20 | .icon-question-mark:before { content: "\ea03"; } 21 | -------------------------------------------------------------------------------- /src/assets/icon/fonts/icon.module.less: -------------------------------------------------------------------------------- 1 | @font-face {font-family: "icon"; 2 | src: url('icon.eot?t=1627271109350'); /* IE9*/ 3 | src: url('icon.eot?t=1627271109350#iefix') format('embedded-opentype'), /* IE6-IE8 */ 4 | url("icon.woff2?t=1627271109350") format("woff2"), 5 | url("icon.woff?t=1627271109350") format("woff"), 6 | url('icon.ttf?t=1627271109350') format('truetype'), /* chrome, firefox, opera, Safari, Android, iOS 4.2+*/ 7 | url('icon.svg?t=1627271109350#icon') format('svg'); /* iOS 4.1- */ 8 | } 9 | 10 | [class^="icon-"], [class*=" icon-"] { 11 | font-family: 'icon' !important; 12 | font-size:16px; 13 | font-style:normal; 14 | -webkit-font-smoothing: antialiased; 15 | -moz-osx-font-smoothing: grayscale; 16 | } 17 | 18 | :global { 19 | .icon-add-line:before { content: "\ea01"; } 20 | .icon-github-line:before { content: "\ea02"; } 21 | .icon-question-mark:before { content: "\ea03"; } 22 | 23 | } -------------------------------------------------------------------------------- /src/assets/icon/fonts/icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 9 | 10 | 13 | 16 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /src/assets/icon/fonts/icon.symbol.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/icon/fonts/icon.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lblblong/nat-utools/e449e92f5a326178a1f5d0339aff7692a379d4f2/src/assets/icon/fonts/icon.ttf -------------------------------------------------------------------------------- /src/assets/icon/fonts/icon.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lblblong/nat-utools/e449e92f5a326178a1f5d0339aff7692a379d4f2/src/assets/icon/fonts/icon.woff -------------------------------------------------------------------------------- /src/assets/icon/fonts/icon.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lblblong/nat-utools/e449e92f5a326178a1f5d0339aff7692a379d4f2/src/assets/icon/fonts/icon.woff2 -------------------------------------------------------------------------------- /src/assets/icon/fonts/index.html: -------------------------------------------------------------------------------- 1 | 查看图标

查看图标1

File icons in the file tree.

Font Class Demo · Symbol Demo · Unicode Demo

  • add-line

  • github-line

  • question-mark

-------------------------------------------------------------------------------- /src/assets/icon/fonts/symbol.html: -------------------------------------------------------------------------------- 1 | 查看图标

查看图标1

File icons in the file tree.

Font Class Demo · Symbol Demo · Unicode Demo

  • icon-add-line

  • icon-github-line

  • icon-question-mark

-------------------------------------------------------------------------------- /src/assets/icon/fonts/unicode.html: -------------------------------------------------------------------------------- 1 | 查看图标

查看图标1

File icons in the file tree.

Font Class Demo · Symbol Demo · Unicode Demo

  • add-line

    &#59905;
  • github-line

    &#59906;
  • question-mark

    &#59907;
-------------------------------------------------------------------------------- /src/assets/icon/github-line.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/icon/question-mark.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/image/undraw_void_3ggu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lblblong/nat-utools/e449e92f5a326178a1f5d0339aff7692a379d4f2/src/assets/image/undraw_void_3ggu.png -------------------------------------------------------------------------------- /src/components/alert/index.module.scss: -------------------------------------------------------------------------------- 1 | .index { 2 | padding: 18px; 3 | border-radius: 12px; 4 | background-color: #fff; 5 | width: 50vw; 6 | max-width: 400px; 7 | display: flex; 8 | flex-direction: column; 9 | line-height: 1; 10 | 11 | > * + * { 12 | margin-top: 18px; 13 | font-size: 18px; 14 | } 15 | 16 | .title { 17 | font-size: 28px; 18 | color: #333; 19 | } 20 | 21 | .content { 22 | color: #666; 23 | padding: 16px 0; 24 | line-height: 1.7; 25 | } 26 | 27 | .btn { 28 | padding: 16px; 29 | background-color: var(--color-primary); 30 | display: flex; 31 | justify-content: center; 32 | align-items: center; 33 | color: #fff; 34 | border-radius: 12px; 35 | cursor: pointer; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/components/alert/index.tsx: -------------------------------------------------------------------------------- 1 | import { Popups, useController } from 'lbl-popups' 2 | import { Observer } from 'mobx-react' 3 | import { FC } from 'react' 4 | import styles from './index.module.scss' 5 | 6 | interface Props { 7 | title: React.ReactNode 8 | content: React.ReactNode 9 | } 10 | 11 | const Alert: FC = (props) => { 12 | const ctl = useController() 13 | return ( 14 | 15 | {() => ( 16 |
17 |
{props.title}
18 | 19 |
{props.content}
20 | 21 |
{ 24 | ctl.close() 25 | }} 26 | > 27 | 确定 28 |
29 |
30 | )} 31 |
32 | ) 33 | } 34 | 35 | export function openAlert(props: Props) { 36 | return Popups.open({ 37 | el: Alert, 38 | position: 'center', 39 | props, 40 | }) 41 | } 42 | -------------------------------------------------------------------------------- /src/components/help/index.module.scss: -------------------------------------------------------------------------------- 1 | .index { 2 | padding: 18px; 3 | border-radius: 12px; 4 | background-color: #fff; 5 | width: 50vw; 6 | max-width: 400px; 7 | display: flex; 8 | flex-direction: column; 9 | line-height: 1; 10 | 11 | .link { 12 | color: var(--color-primary); 13 | cursor: pointer; 14 | } 15 | 16 | > * + * { 17 | margin-top: 18px; 18 | font-size: 18px; 19 | } 20 | 21 | .title { 22 | font-size: 28px; 23 | color: #333; 24 | } 25 | 26 | .content { 27 | color: #666; 28 | padding: 16px 0; 29 | line-height: 1.7; 30 | max-height: 50vh; 31 | overflow-y: auto; 32 | overflow-x: hidden; 33 | } 34 | 35 | .btn { 36 | padding: 16px; 37 | background-color: var(--color-primary); 38 | display: flex; 39 | justify-content: center; 40 | align-items: center; 41 | color: #fff; 42 | border-radius: 12px; 43 | cursor: pointer; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/components/help/index.tsx: -------------------------------------------------------------------------------- 1 | import { Popups } from 'lbl-popups' 2 | import { Observer } from 'mobx-react' 3 | import { FC } from 'react' 4 | import styles from './index.module.scss' 5 | 6 | const Help: FC = () => { 7 | return ( 8 | 9 | {() => ( 10 |
11 |
关于内网穿透NAT
12 | 13 |
14 |

15 | 该 utools 扩展是基于 localtunnel 的封装,目前已在 GitHub 开源: 16 | utools.shellOpenExternal('https://github.com/lblblong/nat-utools')} 19 | > 20 | https://github.com/lblblong/nat-utools 21 | 22 |

23 |

使用说明:

24 |

25 | 点击右下角 + 26 | 号按钮,输入你本地服务监听的端口号,如果需要自定义子域名则输入自定义的子域名,然后点击确定,即可获得可被外网访问的地址 27 |

28 |

参数说明:

29 |

30 |

    31 |
  • 端口号:需要通过内网穿透公开的本地端口号
  • 32 |
  • 子域名:自定义的子域名,过于火热的域名通常会被占用
  • 33 |
  • 自定义HOST:当你需要公开的服务不在localhost上时,你会需要它
  • 34 |
35 |

36 |

状态说明:

37 |

38 |

    39 |
  • 灰色:未启动
  • 40 |
  • 灰蓝闪动:连接中
  • 41 |
  • 蓝色:已启动
  • 42 |
43 |

44 |
45 |
46 | )} 47 |
48 | ) 49 | } 50 | 51 | export function openHelp() { 52 | return Popups.open({ 53 | el: Help, 54 | position: 'center', 55 | }) 56 | } 57 | -------------------------------------------------------------------------------- /src/components/icon/index.module.scss: -------------------------------------------------------------------------------- 1 | .index { 2 | font-size: inherit; 3 | line-height: inherit; 4 | } 5 | -------------------------------------------------------------------------------- /src/components/icon/index.tsx: -------------------------------------------------------------------------------- 1 | import { CSSProperties, FC } from 'react' 2 | import styles from './index.module.scss' 3 | import classNames from 'classnames' 4 | 5 | interface Props { 6 | className?: string 7 | value?: string 8 | style?: CSSProperties 9 | } 10 | 11 | export const Icon: FC = (props) => { 12 | return 13 | } 14 | 15 | Icon.defaultProps = { 16 | value: 'star', 17 | } 18 | -------------------------------------------------------------------------------- /src/components/nat-edit/index.module.scss: -------------------------------------------------------------------------------- 1 | .index { 2 | padding: 18px; 3 | border-radius: 12px; 4 | background-color: #fff; 5 | width: 50vw; 6 | max-width: 400px; 7 | display: flex; 8 | flex-direction: column; 9 | line-height: 1; 10 | 11 | > * + * { 12 | margin-top: 18px; 13 | font-size: 18px; 14 | } 15 | 16 | .title { 17 | font-size: 28px; 18 | } 19 | 20 | input { 21 | padding: 16px; 22 | border: none; 23 | border-bottom: 1px solid #efefef; 24 | outline: none; 25 | font-weight: bold; 26 | } 27 | 28 | .btn { 29 | padding: 16px; 30 | background-color: var(--color-primary); 31 | display: flex; 32 | justify-content: center; 33 | align-items: center; 34 | color: #fff; 35 | border-radius: 12px; 36 | cursor: pointer; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/components/nat-edit/index.tsx: -------------------------------------------------------------------------------- 1 | import { Popups, useController } from 'lbl-popups' 2 | import { Observer, useLocalStore } from 'mobx-react' 3 | import { FC } from 'react' 4 | import { Nat } from 'src/model/nat' 5 | import styles from './index.module.scss' 6 | 7 | type Data = Pick 8 | 9 | interface Props { 10 | defaultValue?: Data 11 | } 12 | 13 | const NatEdit: FC = (props) => { 14 | const ctl = useController() 15 | const localStore = useLocalStore(() => { 16 | return { 17 | port: props.defaultValue?.port || '', 18 | subdomian: props.defaultValue?.subdomain || '', 19 | local_host: props.defaultValue?.local_host || '', 20 | bindOnChange: (key: string) => { 21 | return (e: any) => { 22 | localStore[key] = e.target.value 23 | } 24 | }, 25 | } 26 | }) 27 | 28 | return ( 29 | 30 | {() => ( 31 |
32 |
创建内网穿透
33 | 39 | 44 | 49 |
{ 52 | ctl.close({ 53 | port: localStore.port, 54 | subdomain: localStore.subdomian, 55 | local_host: localStore.local_host, 56 | }) 57 | }} 58 | > 59 | 确定 60 |
61 |
62 | )} 63 |
64 | ) 65 | } 66 | 67 | export function openNatEdit(props?: Props): Promise { 68 | return Popups.open({ 69 | el: NatEdit, 70 | position: 'center', 71 | props, 72 | }) 73 | } 74 | -------------------------------------------------------------------------------- /src/components/nat-log/index.module.scss: -------------------------------------------------------------------------------- 1 | .index { 2 | width: 70vw; 3 | max-width: 800px; 4 | height: 100vh; 5 | background-color: #fff; 6 | overflow-y: auto; 7 | color: #333; 8 | .content { 9 | padding: 20px; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/components/nat-log/index.tsx: -------------------------------------------------------------------------------- 1 | import { Popups } from 'lbl-popups' 2 | import { Observer } from 'mobx-react' 3 | import { FC, useRef } from 'react' 4 | import { StoreServer } from 'src/store/server' 5 | import styles from './index.module.scss' 6 | 7 | interface Props { 8 | id: number 9 | } 10 | 11 | const NatLog: FC = (props) => { 12 | const nat = StoreServer.getNatById(props.id) 13 | const indexRef = useRef(null) 14 | const contentRef = useRef(null) 15 | 16 | const resetScroll = () => { 17 | setTimeout(() => { 18 | if (indexRef.current && contentRef.current) indexRef.current.scrollTop = contentRef.current.offsetHeight 19 | }) 20 | } 21 | 22 | return ( 23 | 24 | {() => { 25 | resetScroll() 26 | return ( 27 |
28 |
29 | {nat.logs.map((l) => { 30 | return
{l}
31 | })} 32 |
33 |
34 | ) 35 | }} 36 |
37 | ) 38 | } 39 | 40 | export function openNatLog(props: Props) { 41 | return Popups.open({ 42 | el: NatLog, 43 | props, 44 | position: 'right', 45 | }) 46 | } 47 | -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | import { Popups } from 'lbl-popups' 2 | import { configure } from 'mobx' 3 | import React from 'react' 4 | import ReactDOM from 'react-dom' 5 | import './assets/css/vs.css' 6 | import { IndexPage } from './pages/manager' 7 | configure({ 8 | enforceActions: 'never', 9 | }) 10 | 11 | ReactDOM.render( 12 | <> 13 | 14 | 15 | , 16 | document.getElementById('root'), 17 | ) 18 | -------------------------------------------------------------------------------- /src/model/nat.ts: -------------------------------------------------------------------------------- 1 | import { EventEmitter } from 'events' 2 | import { makeAutoObservable } from 'mobx' 3 | import { openAlert } from 'src/components/alert' 4 | 5 | export enum NatState { 6 | on, 7 | off, 8 | loading, 9 | } 10 | 11 | export class Nat { 12 | constructor(it: Partial = {}) { 13 | makeAutoObservable(this) 14 | 15 | if (!it.port) throw Error('请传入端口号') 16 | 17 | this.id = it.id || Date.now() 18 | this.subdomain = it.subdomain || undefined 19 | this.local_host = it.local_host || undefined 20 | this.port = it.port 21 | } 22 | 23 | id: number 24 | subdomain?: string 25 | local_host?: string 26 | port = 6600 27 | state = NatState.off 28 | tunnel?: EventEmitter & { url: string; close: () => void } 29 | url?: string 30 | logs: string[] = [] 31 | 32 | async start() { 33 | try { 34 | if (this.state === NatState.loading) return 35 | this.state = NatState.loading 36 | this.log('启动中') 37 | this.tunnel = await window.localtunnel({ 38 | port: this.port, 39 | subdomain: this.subdomain, 40 | local_host: this.local_host, 41 | }) 42 | if (this.state !== NatState.loading) throw Error('主动关闭') 43 | this.url = this.tunnel.url 44 | this.state = NatState.on 45 | this.log(`启动成功:${this.url}`) 46 | 47 | const onRequest = (req: any) => { 48 | this.log(`${req.method} ${req.path}`) 49 | } 50 | 51 | const onError = (err: Error) => { 52 | console.log(this.id, err) 53 | this.log(err.message) 54 | } 55 | 56 | this.tunnel?.once('close', () => { 57 | this.tunnel?.off('error', onError) 58 | this.tunnel?.off('request', onRequest) 59 | this.reset() 60 | this.log('已关闭') 61 | }) 62 | this.tunnel?.on('error', onError) 63 | this.tunnel?.on('request', onRequest) 64 | } catch (err) { 65 | this.state = NatState.off 66 | this.log('启动失败:' + err.message) 67 | openAlert({ 68 | title: '启动失败', 69 | content: err.message, 70 | }) 71 | throw err 72 | } 73 | } 74 | 75 | async stop() { 76 | if (this.state === NatState.off) return 77 | 78 | if (this.tunnel) { 79 | return new Promise((ok) => { 80 | this.tunnel?.once('close', () => { 81 | ok() 82 | }) 83 | this.tunnel?.close() 84 | }) 85 | } else { 86 | this.reset() 87 | } 88 | } 89 | 90 | reset() { 91 | this.state = NatState.off 92 | this.tunnel = undefined 93 | this.url = '' 94 | } 95 | 96 | log(log: string) { 97 | this.logs.push(log) 98 | if (this.logs.length >= 1000) { 99 | this.logs = this.logs.slice(100) 100 | } 101 | } 102 | 103 | toJSON() { 104 | return { 105 | id: this.id, 106 | subdomain: this.subdomain, 107 | local_host: this.local_host, 108 | port: this.port, 109 | } 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /src/pages/manager/index.module.scss: -------------------------------------------------------------------------------- 1 | .index { 2 | position: relative; 3 | .actions { 4 | position: fixed; 5 | bottom: 24px; 6 | right: 24px; 7 | display: flex; 8 | align-items: center; 9 | .action + .action { 10 | margin-left: 16px; 11 | } 12 | 13 | .action { 14 | width: 40px; 15 | height: 40px; 16 | border-radius: 50%; 17 | background-color: var(--color-primary); 18 | color: #fff; 19 | display: flex; 20 | justify-content: center; 21 | align-items: center; 22 | font-size: 18px; 23 | cursor: pointer; 24 | box-shadow: 0px 1px 2px -2px rgba($color: #000000, $alpha: 0.16); 25 | transition: all 0.3s; 26 | &:hover { 27 | // background-color: rgba($color: var(--color-primary), $alpha: 0.9); 28 | box-shadow: 0px 5px 12px 4px rgba($color: #000000, $alpha: 0.09); 29 | } 30 | } 31 | } 32 | 33 | .list { 34 | padding-bottom: 100px; 35 | .item + .item { 36 | border-top: 1px solid #efefef; 37 | } 38 | .item { 39 | line-height: 1.3; 40 | padding: 24px 16px; 41 | color: #999; 42 | &:hover { 43 | background: rgba($color: #5d9cec, $alpha: 0.06); 44 | } 45 | $state-width: calc(14px + 16px); 46 | .state { 47 | width: 14px; 48 | height: 14px; 49 | background-color: #dfdfdf; 50 | border-radius: 50%; 51 | } 52 | 53 | .top { 54 | display: flex; 55 | align-items: center; 56 | > * { 57 | display: flex; 58 | align-items: center; 59 | } 60 | .lbox { 61 | width: $state-width; 62 | } 63 | .cbox { 64 | flex: 1; 65 | .name { 66 | font-weight: bold; 67 | } 68 | } 69 | 70 | .rbox { 71 | margin: -8px; 72 | button { 73 | border: none; 74 | background: none; 75 | padding: 0; 76 | color: var(--color-primary); 77 | margin: 8px; 78 | cursor: pointer; 79 | &:hover { 80 | text-decoration: underline; 81 | } 82 | } 83 | .del { 84 | color: var(--color-third); 85 | } 86 | button:disabled { 87 | color: #999; 88 | } 89 | } 90 | } 91 | .bottom { 92 | padding-left: $state-width; 93 | padding-top: 16px; 94 | .url { 95 | cursor: pointer; 96 | overflow: hidden; 97 | text-overflow: ellipsis; 98 | white-space: nowrap; 99 | &:hover { 100 | color: var(--color-primary); 101 | } 102 | } 103 | } 104 | } 105 | 106 | .loading { 107 | .state { 108 | animation-name: blink; 109 | animation-duration: 0.5s; 110 | animation-iteration-count: infinite; 111 | transition: all 0.5s; 112 | } 113 | } 114 | 115 | .active { 116 | color: #333; 117 | .state { 118 | background-color: var(--color-primary); 119 | } 120 | } 121 | } 122 | } 123 | 124 | @keyframes blink { 125 | from { 126 | background-color: #dfdfdf; 127 | } 128 | to { 129 | background-color: var(--color-primary); 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /src/pages/manager/index.tsx: -------------------------------------------------------------------------------- 1 | import classNames from 'classnames' 2 | import { Observer } from 'mobx-react' 3 | import React from 'react' 4 | import { openAlert } from 'src/components/alert' 5 | import { Icon } from '../../components/icon' 6 | import { NatState } from '../../model/nat' 7 | import { StoreServer } from '../../store/server' 8 | import styles from './index.module.scss' 9 | 10 | export const IndexPage = () => { 11 | return ( 12 | 13 | {() => ( 14 |
15 |
16 |
utools.shellOpenExternal('https://github.com/lblblong/nat-utools')} 19 | > 20 | 21 |
22 |
StoreServer.openHelp()}> 23 | 24 |
25 |
StoreServer.add()}> 26 | 27 |
28 |
29 |
30 | {StoreServer.natList.map((it) => { 31 | return ( 32 |
39 |
40 |
41 | 42 |
43 | 44 |
45 | 46 | {it.local_host || 'localhost'}:{it.port} 47 | {it.subdomain ? ` • ${it.subdomain}` : ''} 48 | 49 |
50 | 51 |
52 | 55 | 62 | 65 | 75 | {/* {it.state === NatState.off && ( 76 | 79 | )} */} 80 | 88 |
89 |
90 |
91 | { 94 | if (it.url) { 95 | utools.shellOpenExternal(it.url) 96 | } else { 97 | openAlert({ 98 | title: '提示', 99 | content: '请先启动服务', 100 | }) 101 | } 102 | }} 103 | > 104 | {it.url || `-`} 105 | 106 |
107 |
108 | ) 109 | })} 110 |
111 |
112 | )} 113 |
114 | ) 115 | } 116 | -------------------------------------------------------------------------------- /src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /src/store/server.ts: -------------------------------------------------------------------------------- 1 | import localtunnel from 'localtunnel' 2 | import { makeAutoObservable } from 'mobx' 3 | import { openAlert } from 'src/components/alert' 4 | import { openHelp } from 'src/components/help' 5 | import { openNatEdit } from 'src/components/nat-edit' 6 | import { openNatLog } from 'src/components/nat-log' 7 | import { onPluginEnterCBParams } from 'utools-helper/@types/utools' 8 | import { Nat, NatState } from '../model/nat' 9 | 10 | declare global { 11 | interface Window { 12 | localtunnel: typeof localtunnel 13 | } 14 | } 15 | 16 | function getStorageID() { 17 | return utools.getLocalId() + '_servers' 18 | } 19 | 20 | class Store { 21 | constructor() { 22 | makeAutoObservable(this) 23 | utools.onPluginEnter(this.onPluginEnter) 24 | utools.onPluginReady(() => { 25 | this.loadDb() 26 | if (this.natList.length === 0) { 27 | openHelp() 28 | } 29 | }) 30 | } 31 | 32 | onPluginEnter = (params: onPluginEnterCBParams) => {} 33 | 34 | natList: Nat[] = [] 35 | 36 | async add() { 37 | const data = await openNatEdit() 38 | const nat = new Nat(data) 39 | this.natList.push(nat) 40 | this.flushDb() 41 | this.start(nat.id) 42 | } 43 | 44 | getNatById(id: number) { 45 | return this.natList.find((it) => it.id === id)! 46 | } 47 | 48 | async start(id: number) { 49 | const nat = this.getNatById(id) 50 | await nat.start() 51 | if (nat.subdomain && nat.url!.indexOf('//' + nat.subdomain) === -1) { 52 | openAlert({ 53 | title: '提示', 54 | content: '子域名被占用,已为您分配其他子域名', 55 | }) 56 | } 57 | } 58 | 59 | async stop(id: number) { 60 | const nat = this.getNatById(id) 61 | await nat.stop() 62 | } 63 | 64 | async del(id: number) { 65 | await this.stop(id) 66 | this.natList = this.natList.filter((it) => it.id !== id) 67 | this.flushDb() 68 | } 69 | 70 | async edit(id: number) { 71 | const nat = this.getNatById(id) 72 | const originState = nat.state 73 | if (originState === NatState.loading) return 74 | if (originState === NatState.on) { 75 | await this.stop(id) 76 | } 77 | const data = await openNatEdit({ 78 | defaultValue: nat.toJSON(), 79 | }) 80 | nat.port = data.port 81 | nat.subdomain = data.subdomain 82 | nat.local_host = data.local_host 83 | this.flushDb() 84 | if (originState === NatState.on) { 85 | this.start(id) 86 | } 87 | } 88 | 89 | async showLog(id: number) { 90 | openNatLog({ id }) 91 | } 92 | 93 | async openHelp() { 94 | openHelp() 95 | } 96 | 97 | toggle = async (id: number) => { 98 | const nat = this.getNatById(id) 99 | if (nat.state === NatState.on || nat.state === NatState.loading) { 100 | await this.stop(nat.id) 101 | } else { 102 | await this.start(nat.id) 103 | } 104 | } 105 | 106 | loadDb = () => { 107 | try { 108 | const it = utools.db.get< 109 | { 110 | id: number 111 | subdomain?: string 112 | port: number 113 | }[] 114 | >(getStorageID()) 115 | if (!it || !it.data) return 116 | this.natList = it.data.map((d) => new Nat(d)) 117 | } catch (err) { 118 | alert(err) 119 | } 120 | } 121 | 122 | flushDb() { 123 | let it = utools.db.get< 124 | { 125 | id: number 126 | subdomain?: string 127 | port: number 128 | }[] 129 | >(getStorageID()) 130 | 131 | const data = this.natList.map((d) => d.toJSON()) 132 | 133 | if (!it) { 134 | it = { 135 | _id: getStorageID(), 136 | data: data, 137 | } 138 | } else { 139 | it.data = data 140 | } 141 | 142 | const { ok, error } = utools.db.put(it) 143 | if (!ok) { 144 | throw Error(error) 145 | } 146 | } 147 | } 148 | 149 | export const StoreServer = new Store() 150 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.path.json", 3 | "compilerOptions": { 4 | "target": "es5", 5 | "lib": [ 6 | "dom", 7 | "dom.iterable", 8 | "esnext" 9 | ], 10 | "allowJs": true, 11 | "skipLibCheck": true, 12 | "esModuleInterop": true, 13 | "allowSyntheticDefaultImports": true, 14 | "strict": false, 15 | "forceConsistentCasingInFileNames": true, 16 | "noFallthroughCasesInSwitch": true, 17 | "module": "esnext", 18 | "moduleResolution": "node", 19 | "resolveJsonModule": true, 20 | "isolatedModules": true, 21 | "noEmit": true, 22 | "jsx": "react-jsx", 23 | "preserveSymlinks": true, 24 | "types": [ 25 | "utools-helper/@types/utools", 26 | "utools-helper/@types/electron", 27 | "@types/node" 28 | ] 29 | }, 30 | "include": [ 31 | "src", 32 | "typing/global.d.ts" 33 | ] 34 | } 35 | -------------------------------------------------------------------------------- /tsconfig.path.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": ".", 4 | "paths": { 5 | "src/*": ["./src/*"] 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import react from '@vitejs/plugin-react' 2 | import path from 'path' 3 | import { defineConfig } from 'vite' 4 | import isWsl from 'is-wsl' 5 | 6 | // https://vitejs.dev/config/ 7 | export default defineConfig({ 8 | build: { 9 | sourcemap: false, 10 | emptyOutDir: true, 11 | // wsl 下输出到 c 盘目录 12 | outDir: isWsl ? '/mnt/c/code/utools/nat-utools' : 'dist', 13 | }, 14 | base: './', 15 | plugins: [react()], 16 | resolve: { 17 | alias: [ 18 | { 19 | find: 'src/', 20 | replacement: path.resolve(__dirname, 'src') + '/', 21 | }, 22 | { 23 | find: /^~/, 24 | replacement: '', 25 | }, 26 | ], 27 | }, 28 | }) 29 | --------------------------------------------------------------------------------