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 +21 -0
- package/README.en.md +196 -0
- package/README.ko.md +196 -0
- package/README.md +77 -0
- package/bin/lt.js +129 -0
- package/lib/HeaderHostTransformer.js +23 -0
- package/lib/Tunnel.js +214 -0
- package/lib/TunnelCluster.js +153 -0
- package/localtunnel.js +14 -0
- package/package.json +43 -0
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
|
+
}
|