├── .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 | 
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 | 
20 | 
21 | 
--------------------------------------------------------------------------------
/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 |
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 | 查看图标add-line
github-line
question-mark
Font Class Demo · Symbol Demo · Unicode Demo
--------------------------------------------------------------------------------
/src/assets/icon/fonts/symbol.html:
--------------------------------------------------------------------------------
1 | 查看图标icon-add-line
icon-github-line
icon-question-mark
Font Class Demo · Symbol Demo · Unicode Demo
--------------------------------------------------------------------------------
/src/assets/icon/fonts/unicode.html:
--------------------------------------------------------------------------------
1 | 查看图标-
add-line
 -
github-line
 -
question-mark

Font Class Demo · Symbol Demo · Unicode Demo
--------------------------------------------------------------------------------
/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 |
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 |
--------------------------------------------------------------------------------