mygensite 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
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.
package/README.en.md ADDED
@@ -0,0 +1,196 @@
1
+ # mygensite
2
+
3
+ Expose your localhost to the world via [mygen.site](https://mygen.site) with access control.
4
+
5
+ A fork of [localtunnel](https://github.com/localtunnel/localtunnel) with extended features: password protection, IP whitelisting, TTL, owner management, and admin tokens.
6
+
7
+ ## Quickstart
8
+
9
+ ```
10
+ npx mygensite --port 8000
11
+ ```
12
+
13
+ ## Installation
14
+
15
+ ### Globally
16
+
17
+ ```
18
+ npm install -g mygensite
19
+ ```
20
+
21
+ ### As a dependency in your project
22
+
23
+ ```
24
+ npm install mygensite
25
+ ```
26
+
27
+ ## CLI usage
28
+
29
+ ```
30
+ mygensite --port 8000
31
+ ```
32
+
33
+ It will connect to mygen.site, set up the tunnel, and tell you what URL to use. The `lt` command also works for backward compatibility.
34
+
35
+ ### Arguments
36
+
37
+ Below are some common arguments. See `mygensite --help` for all options.
38
+
39
+ - `--port` (required) local port to expose
40
+ - `--subdomain` request a named subdomain (default is random)
41
+ - `--host` upstream server URL (default: `https://mygen.site`)
42
+ - `--local-host` proxy to a hostname other than localhost
43
+ - `--access` access control mode: `public`, `password`, `ip_only`, `both` (default: `both`)
44
+ - `--password` password for access control (auto-generated if omitted)
45
+ - `--owner-email` owner email for dashboard management
46
+ - `--ttl` tunnel TTL in seconds, 60-86400 (default: 3600)
47
+
48
+ ```
49
+ mygensite --port 3000 --subdomain my-app --access password --password secret --ttl 7200
50
+ ```
51
+
52
+ Output includes URL, password, and admin_token for runtime management.
53
+
54
+ You may also specify arguments via env variables:
55
+
56
+ ```
57
+ PORT=3000 mygensite
58
+ ```
59
+
60
+ ## API
61
+
62
+ ### mygensite(options)
63
+
64
+ Creates a new tunnel to the specified local `port`. Returns a Promise that resolves once you have been assigned a public URL.
65
+
66
+ ```js
67
+ const mygensite = require('mygensite');
68
+
69
+ (async () => {
70
+ const tunnel = await mygensite({
71
+ port: 3000,
72
+ subdomain: 'my-app',
73
+ access: 'password',
74
+ password: 'secret',
75
+ owner_email: 'alice@company.com',
76
+ ttl: 3600,
77
+ });
78
+
79
+ console.log(tunnel.url); // https://my-app.mygen.site
80
+ console.log(tunnel.password); // "secret"
81
+ console.log(tunnel.admin_token); // "tok_xxx"
82
+ console.log(tunnel.access); // { mode: "password", ... }
83
+ console.log(tunnel.expires_at); // "2025-06-01T13:00:00Z"
84
+
85
+ tunnel.on('close', () => {
86
+ // tunnel closed
87
+ });
88
+ })();
89
+ ```
90
+
91
+ #### Options
92
+
93
+ ##### localtunnel compatible
94
+
95
+ - `port` (number) [required] The local port number to expose.
96
+ - `subdomain` (string) Request a specific subdomain on the proxy server.
97
+ - `host` (string) URL for the upstream proxy server. Defaults to `https://mygen.site`.
98
+ - `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.
99
+ - `local_https` (boolean) Enable tunneling to local HTTPS server.
100
+ - `local_cert` (string) Path to certificate PEM file for local HTTPS server.
101
+ - `local_key` (string) Path to certificate key file for local HTTPS server.
102
+ - `local_ca` (string) Path to certificate authority file for self-signed certificates.
103
+ - `allow_invalid_cert` (boolean) Disable certificate checks for your local HTTPS server.
104
+
105
+ ##### mygensite extensions
106
+
107
+ - `access` (string) Access control mode: `public`, `password`, `ip_only`, `both`. Default: `both`.
108
+ - `password` (string) Password for access control. Auto-generated if omitted.
109
+ - `allowed_ips` (string[]) IP whitelist for `ip_only` or `both` mode. Supports CIDR notation.
110
+ - `owner_email` (string) Owner email for dashboard management.
111
+ - `ttl` (number) Tunnel TTL in seconds (60-86400). Default: 3600.
112
+
113
+ ### Tunnel instance
114
+
115
+ #### Properties
116
+
117
+ | property | description |
118
+ | --- | --- |
119
+ | `url` | The public URL for the tunnel |
120
+ | `password` | The password (if access control is set) |
121
+ | `admin_token` | Token for runtime management via API |
122
+ | `access` | Access control settings object |
123
+ | `expires_at` | ISO timestamp when the tunnel expires |
124
+
125
+ #### Events
126
+
127
+ | event | args | description |
128
+ | --- | --- | --- |
129
+ | request | info | fires when a request is processed, contains `method` and `path` |
130
+ | error | err | fires when an error happens on the tunnel |
131
+ | close | | fires when the tunnel has closed |
132
+
133
+ #### Methods
134
+
135
+ | method | args | description |
136
+ | --- | --- | --- |
137
+ | `close()` | | Close the tunnel |
138
+ | `updateAccess(access)` | `{ mode, password, allowed_ips }` | Update access control at runtime. Returns a Promise. |
139
+ | `extendTTL(ttl)` | seconds (number) | Extend the tunnel TTL. Returns a Promise. |
140
+
141
+ ### Runtime management
142
+
143
+ ```js
144
+ // Switch to public access
145
+ await tunnel.updateAccess({ mode: 'public' });
146
+
147
+ // Add password protection
148
+ await tunnel.updateAccess({ mode: 'password', password: 'newpass' });
149
+
150
+ // Restrict by IP
151
+ await tunnel.updateAccess({ mode: 'ip_only', allowed_ips: ['1.2.3.0/24'] });
152
+
153
+ // Extend TTL by 1 hour
154
+ await tunnel.extendTTL(3600);
155
+ ```
156
+
157
+ ## Error Codes
158
+
159
+ ### Tunnel creation errors
160
+
161
+ | status | error | description | fix |
162
+ | --- | --- | --- | --- |
163
+ | 400 | `invalid_slug` | Slug must be 3-63 chars, lowercase alphanumeric and hyphens | Use a valid slug format, e.g. `my-app-1` |
164
+ | 400 | `reserved_slug` | This slug is reserved and cannot be used | Choose a different slug. Reserved: www, api, dashboard, admin, etc. |
165
+ | 400 | `invalid_ttl` | TTL must be between 60 and 86400 seconds | Use a value between 60 (1 min) and 86400 (24 hours) |
166
+ | 400 | `invalid_access` | Access mode must be: public, password, ip_only, both | Use one of the four valid modes |
167
+ | 409 | `slug_in_use` | This slug is already in use | Use a different slug, or omit `subdomain` for a random one |
168
+ | 503 | — | Server is temporarily unavailable | Retry after a few seconds |
169
+
170
+ ### Runtime management errors (updateAccess, extendTTL)
171
+
172
+ | status | error | description | fix |
173
+ | --- | --- | --- | --- |
174
+ | 401 | `unauthorized` | Invalid or missing admin_token | Use the `admin_token` returned from tunnel creation |
175
+ | 404 | `not_found` | Service not found | Check that the slug is correct and the tunnel is still active |
176
+ | 400 | `invalid_access` | Invalid access mode | Use one of: public, password, ip_only, both |
177
+ | 400 | `invalid_ttl` | TTL out of range | Use a value between 60 and 86400 |
178
+
179
+ ### Gateway errors (when accessing the tunnel URL)
180
+
181
+ | status | description | fix |
182
+ | --- | --- | --- |
183
+ | 404 | Service not found | Check that the slug exists and has not been deleted |
184
+ | 410 | Service has expired | Call `extendTTL()` or create a new tunnel |
185
+ | 403 | IP not allowed | Add your IP to `allowed_ips`, or switch access mode to `public` |
186
+ | 401 | Incorrect password | Retry with the correct password |
187
+ | 502 | Service is offline (tunnel disconnected) | Restart the tunnel client |
188
+ | 504 | Service timed out | Check that your local server is running and responsive |
189
+
190
+ ## Compatibility
191
+
192
+ mygensite is fully compatible with any localtunnel server. Extension options are sent as query parameters and silently ignored by servers that don't support them.
193
+
194
+ ## License
195
+
196
+ MIT
package/README.ko.md ADDED
@@ -0,0 +1,196 @@
1
+ # mygensite
2
+
3
+ [mygen.site](https://mygen.site)를 통해 로컬 서버를 접근 제어와 함께 외부에 노출합니다.
4
+
5
+ [localtunnel](https://github.com/localtunnel/localtunnel) fork에 비밀번호 보호, IP 화이트리스트, TTL, 소유자 관리, admin token 기능을 추가했습니다.
6
+
7
+ ## 빠른 시작
8
+
9
+ ```
10
+ npx mygensite --port 8000
11
+ ```
12
+
13
+ ## 설치
14
+
15
+ ### 전역 설치
16
+
17
+ ```
18
+ npm install -g mygensite
19
+ ```
20
+
21
+ ### 프로젝트 의존성으로 설치
22
+
23
+ ```
24
+ npm install mygensite
25
+ ```
26
+
27
+ ## CLI 사용법
28
+
29
+ ```
30
+ mygensite --port 8000
31
+ ```
32
+
33
+ mygen.site에 연결하여 터널을 생성하고, 사용할 URL을 알려줍니다. 하위 호환을 위해 `lt` 명령어도 사용 가능합니다.
34
+
35
+ ### 인자
36
+
37
+ 주요 인자 목록입니다. 전체 옵션은 `mygensite --help`를 참고하세요.
38
+
39
+ - `--port` (필수) 노출할 로컬 포트
40
+ - `--subdomain` 원하는 서브도메인 지정 (기본: 랜덤)
41
+ - `--host` 업스트림 서버 URL (기본: `https://mygen.site`)
42
+ - `--local-host` localhost 대신 프록시할 호스트명
43
+ - `--access` 접근 제어 모드: `public`, `password`, `ip_only`, `both` (기본: `both`)
44
+ - `--password` 접근 제어 비밀번호 (미지정 시 자동 생성)
45
+ - `--owner-email` 대시보드 관리용 소유자 이메일
46
+ - `--ttl` 터널 유효 시간(초), 60-86400 (기본: 3600)
47
+
48
+ ```
49
+ mygensite --port 3000 --subdomain my-app --access password --password secret --ttl 7200
50
+ ```
51
+
52
+ 출력에 URL, 비밀번호, admin_token이 포함됩니다.
53
+
54
+ 환경변수로도 인자를 지정할 수 있습니다:
55
+
56
+ ```
57
+ PORT=3000 mygensite
58
+ ```
59
+
60
+ ## API
61
+
62
+ ### mygensite(options)
63
+
64
+ 지정한 로컬 `port`로 터널을 생성합니다. 공개 URL이 할당되면 resolve되는 Promise를 반환합니다.
65
+
66
+ ```js
67
+ const mygensite = require('mygensite');
68
+
69
+ (async () => {
70
+ const tunnel = await mygensite({
71
+ port: 3000,
72
+ subdomain: 'my-app',
73
+ access: 'password',
74
+ password: 'secret',
75
+ owner_email: 'alice@company.com',
76
+ ttl: 3600,
77
+ });
78
+
79
+ console.log(tunnel.url); // https://my-app.mygen.site
80
+ console.log(tunnel.password); // "secret"
81
+ console.log(tunnel.admin_token); // "tok_xxx"
82
+ console.log(tunnel.access); // { mode: "password", ... }
83
+ console.log(tunnel.expires_at); // "2025-06-01T13:00:00Z"
84
+
85
+ tunnel.on('close', () => {
86
+ // 터널 종료됨
87
+ });
88
+ })();
89
+ ```
90
+
91
+ #### 옵션
92
+
93
+ ##### localtunnel 호환
94
+
95
+ - `port` (number) [필수] 노출할 로컬 포트 번호.
96
+ - `subdomain` (string) 프록시 서버에 요청할 서브도메인.
97
+ - `host` (string) 업스트림 프록시 서버 URL. 기본값: `https://mygen.site`.
98
+ - `local_host` (string) `localhost` 대신 프록시할 호스트명. 프록시 요청의 `Host` 헤더도 이 값으로 변경됩니다.
99
+ - `local_https` (boolean) 로컬 HTTPS 서버로 터널링.
100
+ - `local_cert` (string) 로컬 HTTPS 서버의 인증서 PEM 파일 경로.
101
+ - `local_key` (string) 로컬 HTTPS 서버의 인증서 키 파일 경로.
102
+ - `local_ca` (string) 자체 서명 인증서용 CA 파일 경로.
103
+ - `allow_invalid_cert` (boolean) 로컬 HTTPS 서버의 인증서 검증 비활성화.
104
+
105
+ ##### mygensite 확장
106
+
107
+ - `access` (string) 접근 제어 모드: `public`, `password`, `ip_only`, `both`. 기본값: `both`.
108
+ - `password` (string) 접근 제어 비밀번호. 미지정 시 자동 생성.
109
+ - `allowed_ips` (string[]) `ip_only` 또는 `both` 모드에서 허용할 IP 목록. CIDR 표기 지원.
110
+ - `owner_email` (string) 대시보드 관리용 소유자 이메일.
111
+ - `ttl` (number) 터널 유효 시간(초), 60-86400. 기본값: 3600.
112
+
113
+ ### Tunnel 인스턴스
114
+
115
+ #### 속성
116
+
117
+ | 속성 | 설명 |
118
+ | --- | --- |
119
+ | `url` | 터널의 공개 URL |
120
+ | `password` | 비밀번호 (접근 제어 설정 시) |
121
+ | `admin_token` | 런타임 관리용 API 토큰 |
122
+ | `access` | 접근 제어 설정 객체 |
123
+ | `expires_at` | 터널 만료 시각 (ISO 형식) |
124
+
125
+ #### 이벤트
126
+
127
+ | 이벤트 | 인자 | 설명 |
128
+ | --- | --- | --- |
129
+ | request | info | 요청 처리 시 발생, `method`와 `path` 포함 |
130
+ | error | err | 터널 에러 발생 시 |
131
+ | close | | 터널 종료 시 |
132
+
133
+ #### 메서드
134
+
135
+ | 메서드 | 인자 | 설명 |
136
+ | --- | --- | --- |
137
+ | `close()` | | 터널 종료 |
138
+ | `updateAccess(access)` | `{ mode, password, allowed_ips }` | 런타임에 접근 제어 변경. Promise 반환. |
139
+ | `extendTTL(ttl)` | 초 (number) | 터널 TTL 연장. Promise 반환. |
140
+
141
+ ### 런타임 관리
142
+
143
+ ```js
144
+ // 공개로 전환
145
+ await tunnel.updateAccess({ mode: 'public' });
146
+
147
+ // 비밀번호 보호 추가
148
+ await tunnel.updateAccess({ mode: 'password', password: 'newpass' });
149
+
150
+ // IP 제한
151
+ await tunnel.updateAccess({ mode: 'ip_only', allowed_ips: ['1.2.3.0/24'] });
152
+
153
+ // TTL 1시간 연장
154
+ await tunnel.extendTTL(3600);
155
+ ```
156
+
157
+ ## 에러 코드
158
+
159
+ ### 터널 생성 에러
160
+
161
+ | 상태 | 에러 | 설명 | 해결 |
162
+ | --- | --- | --- | --- |
163
+ | 400 | `invalid_slug` | slug는 3-63자, 소문자 영숫자와 하이픈만 가능 | 올바른 형식 사용, 예: `my-app-1` |
164
+ | 400 | `reserved_slug` | 예약된 slug로 사용 불가 | 다른 slug 사용. 예약어: www, api, dashboard, admin 등 |
165
+ | 400 | `invalid_ttl` | TTL은 60-86400초 범위여야 함 | 60(1분) ~ 86400(24시간) 사이 값 사용 |
166
+ | 400 | `invalid_access` | 접근 모드는 public, password, ip_only, both 중 하나 | 4가지 모드 중 하나를 지정 |
167
+ | 409 | `slug_in_use` | 이미 사용 중인 slug | 다른 slug 사용, 또는 `subdomain` 생략하여 랜덤 할당 |
168
+ | 503 | — | 서버 일시 장애 | 몇 초 후 재시도 |
169
+
170
+ ### 런타임 관리 에러 (updateAccess, extendTTL)
171
+
172
+ | 상태 | 에러 | 설명 | 해결 |
173
+ | --- | --- | --- | --- |
174
+ | 401 | `unauthorized` | admin_token 없음 또는 불일치 | 터널 생성 시 반환된 `admin_token` 사용 |
175
+ | 404 | `not_found` | 서비스 없음 | slug가 맞는지, 터널이 아직 활성 상태인지 확인 |
176
+ | 400 | `invalid_access` | 잘못된 접근 모드 | public, password, ip_only, both 중 하나 사용 |
177
+ | 400 | `invalid_ttl` | TTL 범위 초과 | 60 ~ 86400 사이 값 사용 |
178
+
179
+ ### Gateway 에러 (터널 URL 접속 시)
180
+
181
+ | 상태 | 설명 | 해결 |
182
+ | --- | --- | --- |
183
+ | 404 | 서비스 없음 | slug가 존재하고 삭제되지 않았는지 확인 |
184
+ | 410 | 서비스 만료 | `extendTTL()`로 연장하거나 새 터널 생성 |
185
+ | 403 | IP 접근 거부 | `allowed_ips`에 IP 추가, 또는 `public` 모드로 변경 |
186
+ | 401 | 비밀번호 틀림 | 올바른 비밀번호로 재시도 |
187
+ | 502 | 서비스 오프라인 (터널 연결 끊김) | 터널 클라이언트 재시작 |
188
+ | 504 | 서비스 응답 시간 초과 | 로컬 서버가 실행 중이고 응답 가능한지 확인 |
189
+
190
+ ## 호환성
191
+
192
+ mygensite는 모든 localtunnel 서버와 완전 호환됩니다. 확장 옵션은 쿼리 파라미터로 전송되며, 지원하지 않는 서버에서는 무시됩니다.
193
+
194
+ ## 라이선스
195
+
196
+ MIT
package/README.md ADDED
@@ -0,0 +1,77 @@
1
+ # mygensite
2
+
3
+ Expose your localhost to the world via [mygen.site](https://mygen.site) with access control.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ npm install mygensite
9
+ ```
10
+
11
+ ## Tunnel (expose local server)
12
+
13
+ ```js
14
+ const mygensite = require('mygensite');
15
+
16
+ const tunnel = await mygensite({
17
+ port: 3000, // required: local port
18
+ subdomain: 'my-app', // optional: default random
19
+ host: 'https://mygen.site', // optional: default mygen.site
20
+ access: 'password', // optional: public | password | ip_only | both (default: both)
21
+ password: 'secret', // optional: auto-generated if omitted
22
+ allowed_ips: ['1.2.3.0/24'], // optional: for ip_only or both
23
+ owner_email: 'alice@company.com', // optional: dashboard management
24
+ ttl: 3600, // optional: seconds, 60-86400 (default: 3600)
25
+ });
26
+
27
+ // Result
28
+ tunnel.url // "https://my-app.mygen.site"
29
+ tunnel.password // "secret"
30
+ tunnel.admin_token // "tok_xxx"
31
+ tunnel.expires_at // "2025-06-01T13:00:00Z"
32
+
33
+ // Runtime management
34
+ await tunnel.updateAccess({ mode: 'public' });
35
+ await tunnel.extendTTL(3600);
36
+
37
+ // Cleanup
38
+ tunnel.close();
39
+ ```
40
+
41
+ ## Access Modes
42
+
43
+ | mode | behavior |
44
+ |------|----------|
45
+ | `public` | anyone can access |
46
+ | `password` | password required |
47
+ | `ip_only` | allowed_ips only |
48
+ | `both` | allowed_ips + password (default) |
49
+
50
+ ## Error Codes
51
+
52
+ | status | error | when | fix |
53
+ |--------|-------|------|-----|
54
+ | 400 | `invalid_slug` | slug format invalid | use 3-63 chars, lowercase alphanum + hyphen (e.g. `my-app-1`) |
55
+ | 400 | `reserved_slug` | slug is reserved | choose different slug. reserved: www, api, dashboard, admin, etc. |
56
+ | 400 | `invalid_ttl` | TTL out of range | use 60-86400 (seconds) |
57
+ | 400 | `invalid_access` | bad access mode | use: public, password, ip_only, both |
58
+ | 401 | `unauthorized` | wrong admin_token | use the `admin_token` from tunnel creation response |
59
+ | 404 | `not_found` | service not found | verify slug is correct and tunnel is active |
60
+ | 409 | `slug_in_use` | slug already taken | use different slug, or omit `subdomain` for random |
61
+ | 410 | `expired` | TTL expired | call `extendTTL()` or create new tunnel |
62
+ | 502 | — | tunnel offline | restart tunnel client |
63
+
64
+ ## CLI
65
+
66
+ ```bash
67
+ mygensite --port 3000 --subdomain my-app --access password --password secret --ttl 7200
68
+ ```
69
+
70
+ ## Documentation
71
+
72
+ - [English (detailed)](./README.en.md) — full API reference, all options, events, methods
73
+ - [한국어](./README.ko.md) — 한국어 상세 문서
74
+
75
+ ## License
76
+
77
+ MIT
package/bin/lt.js ADDED
@@ -0,0 +1,129 @@
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: mygensite --port [num] <options>')
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://mygen.site',
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
+ .option('access', {
53
+ describe: 'Access control mode: public, password, ip_only, both',
54
+ })
55
+ .option('password', {
56
+ describe: 'Password for access control',
57
+ })
58
+ .option('owner-email', {
59
+ describe: 'Owner email for dashboard management',
60
+ })
61
+ .option('ttl', {
62
+ describe: 'Tunnel TTL in seconds (60-86400)',
63
+ type: 'number',
64
+ })
65
+ .require('port')
66
+ .boolean('local-https')
67
+ .boolean('allow-invalid-cert')
68
+ .boolean('print-requests')
69
+ .help('help', 'Show this help and exit')
70
+ .version(version);
71
+
72
+ if (typeof argv.port !== 'number') {
73
+ yargs.showHelp();
74
+ console.error('\nInvalid argument: `port` must be a number');
75
+ process.exit(1);
76
+ }
77
+
78
+ (async () => {
79
+ const tunnel = await localtunnel({
80
+ port: argv.port,
81
+ host: argv.host,
82
+ subdomain: argv.subdomain,
83
+ local_host: argv.localHost,
84
+ local_https: argv.localHttps,
85
+ local_cert: argv.localCert,
86
+ local_key: argv.localKey,
87
+ local_ca: argv.localCa,
88
+ allow_invalid_cert: argv.allowInvalidCert,
89
+ access: argv.access,
90
+ password: argv.password,
91
+ owner_email: argv.ownerEmail,
92
+ ttl: argv.ttl,
93
+ }).catch(err => {
94
+ throw err;
95
+ });
96
+
97
+ tunnel.on('error', err => {
98
+ throw err;
99
+ });
100
+
101
+ console.log('your url is: %s', tunnel.url);
102
+
103
+ if (tunnel.password) {
104
+ console.log('your password is: %s', tunnel.password);
105
+ }
106
+
107
+ if (tunnel.admin_token) {
108
+ console.log('your admin_token is: %s', tunnel.admin_token);
109
+ }
110
+
111
+ /**
112
+ * `cachedUrl` is set when using a proxy server that support resource caching.
113
+ * This URL generally remains available after the tunnel itself has closed.
114
+ * @see https://github.com/localtunnel/localtunnel/pull/319#discussion_r319846289
115
+ */
116
+ if (tunnel.cachedUrl) {
117
+ console.log('your cachedUrl is: %s', tunnel.cachedUrl);
118
+ }
119
+
120
+ if (argv.open) {
121
+ openurl.open(tunnel.url);
122
+ }
123
+
124
+ if (argv['print-requests']) {
125
+ tunnel.on('request', info => {
126
+ console.log(new Date().toString(), info.method, info.path);
127
+ });
128
+ }
129
+ })();
@@ -0,0 +1,23 @@
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;
package/lib/Tunnel.js ADDED
@@ -0,0 +1,214 @@
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
+ const TunnelCluster = require('./TunnelCluster');
9
+
10
+ module.exports = class Tunnel extends EventEmitter {
11
+ constructor(opts = {}) {
12
+ super(opts);
13
+ this.opts = opts;
14
+ this.closed = false;
15
+ if (!this.opts.host) {
16
+ this.opts.host = 'https://mygen.site';
17
+ }
18
+ }
19
+
20
+ _getInfo(body) {
21
+ /* eslint-disable camelcase */
22
+ const { id, ip, port, url, cached_url, max_conn_count } = body;
23
+ const { host, port: local_port, local_host } = this.opts;
24
+ const { local_https, local_cert, local_key, local_ca, allow_invalid_cert } = this.opts;
25
+
26
+ // Parse extended fields from mygensite server
27
+ this.password = body.password || null;
28
+ this.admin_token = body.admin_token || null;
29
+ this.access = body.access || null;
30
+ this.expires_at = body.expires_at || null;
31
+
32
+ return {
33
+ name: id,
34
+ url,
35
+ cached_url,
36
+ max_conn: max_conn_count || 1,
37
+ remote_host: parse(host).hostname,
38
+ remote_ip: ip,
39
+ remote_port: port,
40
+ local_port,
41
+ local_host,
42
+ local_https,
43
+ local_cert,
44
+ local_key,
45
+ local_ca,
46
+ allow_invalid_cert,
47
+ };
48
+ /* eslint-enable camelcase */
49
+ }
50
+
51
+ // initialize connection
52
+ // callback with connection info
53
+ _init(cb) {
54
+ const opt = this.opts;
55
+ const getInfo = this._getInfo.bind(this);
56
+
57
+ const params = {
58
+ responseType: 'json',
59
+ };
60
+
61
+ // Build extended query params for mygensite server
62
+ const queryParams = {};
63
+ if (opt.access) queryParams.access = opt.access;
64
+ if (opt.password) queryParams.password = opt.password;
65
+ if (opt.allowed_ips) {
66
+ queryParams.allowed_ips = Array.isArray(opt.allowed_ips)
67
+ ? opt.allowed_ips.join(',')
68
+ : opt.allowed_ips;
69
+ }
70
+ if (opt.owner_email) queryParams.owner_email = opt.owner_email;
71
+ if (opt.ttl) queryParams.ttl = String(opt.ttl);
72
+
73
+ if (Object.keys(queryParams).length > 0) {
74
+ params.params = queryParams;
75
+ }
76
+
77
+ const baseUri = `${opt.host}/`;
78
+ // no subdomain at first, maybe use requested domain
79
+ const assignedDomain = opt.subdomain;
80
+ // where to quest
81
+ const uri = baseUri + (assignedDomain || '?new');
82
+
83
+ (function getUrl() {
84
+ axios
85
+ .get(uri, params)
86
+ .then(res => {
87
+ const body = res.data;
88
+ debug('got tunnel information', res.data);
89
+ if (res.status !== 200) {
90
+ const err = new Error(
91
+ (body && body.message) || 'localtunnel server returned an error, please try again'
92
+ );
93
+ return cb(err);
94
+ }
95
+ cb(null, getInfo(body));
96
+ })
97
+ .catch(err => {
98
+ debug(`tunnel server offline: ${err.message}, retry 1s`);
99
+ return setTimeout(getUrl, 1000);
100
+ });
101
+ })();
102
+ }
103
+
104
+ _establish(info) {
105
+ // increase max event listeners so that localtunnel consumers don't get
106
+ // warning messages as soon as they setup even one listener. See #71
107
+ this.setMaxListeners(info.max_conn + (EventEmitter.defaultMaxListeners || 10));
108
+
109
+ this.tunnelCluster = new TunnelCluster(info);
110
+
111
+ // only emit the url the first time
112
+ this.tunnelCluster.once('open', () => {
113
+ this.emit('url', info.url);
114
+ });
115
+
116
+ // re-emit socket error
117
+ this.tunnelCluster.on('error', err => {
118
+ debug('got socket error', err.message);
119
+ this.emit('error', err);
120
+ });
121
+
122
+ let tunnelCount = 0;
123
+
124
+ // track open count
125
+ this.tunnelCluster.on('open', tunnel => {
126
+ tunnelCount++;
127
+ debug('tunnel open [total: %d]', tunnelCount);
128
+
129
+ const closeHandler = () => {
130
+ tunnel.destroy();
131
+ };
132
+
133
+ if (this.closed) {
134
+ return closeHandler();
135
+ }
136
+
137
+ this.once('close', closeHandler);
138
+ tunnel.once('close', () => {
139
+ this.removeListener('close', closeHandler);
140
+ });
141
+ });
142
+
143
+ // when a tunnel dies, open a new one
144
+ this.tunnelCluster.on('dead', () => {
145
+ tunnelCount--;
146
+ debug('tunnel dead [total: %d]', tunnelCount);
147
+ if (this.closed) {
148
+ return;
149
+ }
150
+ this.tunnelCluster.open();
151
+ });
152
+
153
+ this.tunnelCluster.on('request', req => {
154
+ this.emit('request', req);
155
+ });
156
+
157
+ // establish as many tunnels as allowed
158
+ for (let count = 0; count < info.max_conn; ++count) {
159
+ this.tunnelCluster.open();
160
+ }
161
+ }
162
+
163
+ open(cb) {
164
+ this._init((err, info) => {
165
+ if (err) {
166
+ return cb(err);
167
+ }
168
+
169
+ this.clientId = info.name;
170
+ this.url = info.url;
171
+
172
+ // `cached_url` is only returned by proxy servers that support resource caching.
173
+ if (info.cached_url) {
174
+ this.cachedUrl = info.cached_url;
175
+ }
176
+
177
+ this._establish(info);
178
+ cb();
179
+ });
180
+ }
181
+
182
+ close() {
183
+ this.closed = true;
184
+ this.emit('close');
185
+ }
186
+
187
+ async updateAccess(access) {
188
+ const res = await axios.patch(
189
+ `${this.opts.host}/api/services/${this.clientId}`,
190
+ { access },
191
+ {
192
+ headers: {
193
+ Authorization: `Bearer ${this.admin_token}`,
194
+ 'Content-Type': 'application/json',
195
+ },
196
+ }
197
+ );
198
+ return res.data;
199
+ }
200
+
201
+ async extendTTL(ttl) {
202
+ const res = await axios.patch(
203
+ `${this.opts.host}/api/services/${this.clientId}`,
204
+ { ttl },
205
+ {
206
+ headers: {
207
+ Authorization: `Bearer ${this.admin_token}`,
208
+ 'Content-Type': 'application/json',
209
+ },
210
+ }
211
+ );
212
+ return res.data;
213
+ }
214
+ };
@@ -0,0 +1,153 @@
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
+ && err.code !== 'ECONNRESET') {
108
+ return remote.end();
109
+ }
110
+
111
+ // retrying connection to local server
112
+ setTimeout(connLocal, 1000);
113
+ });
114
+
115
+ local.once('connect', () => {
116
+ debug('connected locally');
117
+ remote.resume();
118
+
119
+ let stream = remote;
120
+
121
+ // if user requested specific local host
122
+ // then we use host header transform to replace the host header
123
+ if (opt.local_host) {
124
+ debug('transform Host header to %s', opt.local_host);
125
+ stream = remote.pipe(new HeaderHostTransformer({ host: opt.local_host }));
126
+ }
127
+
128
+ stream.pipe(local).pipe(remote);
129
+
130
+ // when local closes, also get a new remote
131
+ local.once('close', hadError => {
132
+ debug('local connection closed [%s]', hadError);
133
+ });
134
+ });
135
+ };
136
+
137
+ remote.on('data', data => {
138
+ const match = data.toString().match(/^(\w+) (\S+)/);
139
+ if (match) {
140
+ this.emit('request', {
141
+ method: match[1],
142
+ path: match[2],
143
+ });
144
+ }
145
+ });
146
+
147
+ // tunnel is considered open when remote connects
148
+ remote.once('connect', () => {
149
+ this.emit('open', remote);
150
+ connLocal();
151
+ });
152
+ }
153
+ };
package/localtunnel.js ADDED
@@ -0,0 +1,14 @@
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
+ };
package/package.json ADDED
@@ -0,0 +1,43 @@
1
+ {
2
+ "name": "mygensite",
3
+ "description": "Expose your localhost to mygen.site with access control",
4
+ "version": "1.0.0",
5
+ "license": "MIT",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git://github.com/localtunnel/localtunnel.git"
9
+ },
10
+ "author": "Roman Shtylman <shtylman@gmail.com>",
11
+ "contributors": [
12
+ "Roman Shtylman <shtylman@gmail.com>",
13
+ "Gert Hengeveld <gert@hichroma.com>",
14
+ "Tom Coleman <tom@hichroma.com>"
15
+ ],
16
+ "files": [
17
+ "bin/",
18
+ "lib/",
19
+ "localtunnel.js",
20
+ "LICENSE",
21
+ "README.md"
22
+ ],
23
+ "main": "./localtunnel.js",
24
+ "bin": {
25
+ "mygensite": "bin/lt.js",
26
+ "lt": "bin/lt.js"
27
+ },
28
+ "scripts": {
29
+ "test": "mocha --reporter list --timeout 60000 -- *.spec.js"
30
+ },
31
+ "dependencies": {
32
+ "axios": "0.21.4",
33
+ "debug": "4.3.2",
34
+ "openurl": "1.1.1",
35
+ "yargs": "17.1.1"
36
+ },
37
+ "devDependencies": {
38
+ "mocha": "~9.1.1"
39
+ },
40
+ "engines": {
41
+ "node": ">=8.3.0"
42
+ }
43
+ }