@cvasingh/httpsurl 1.0.0 → 1.1.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/README.md +106 -46
- package/package.json +2 -1
- package/src/cli.js +164 -94
- package/src/tunnel-client.js +44 -31
package/README.md
CHANGED
|
@@ -1,98 +1,158 @@
|
|
|
1
|
-
# @httpsurl
|
|
1
|
+
# @cvasingh/httpsurl
|
|
2
2
|
|
|
3
|
-
CLI tool to expose local HTTP servers to the internet via httpsurl.in
|
|
3
|
+
CLI tool to expose local HTTP servers to the internet via secure HTTPS tunnels at `httpsurl.in`.
|
|
4
|
+
|
|
5
|
+
Published on npm as [`@cvasingh/httpsurl`](https://www.npmjs.com/package/@cvasingh/httpsurl).
|
|
4
6
|
|
|
5
7
|
## Installation
|
|
6
8
|
|
|
7
9
|
```bash
|
|
8
|
-
|
|
9
|
-
|
|
10
|
+
npm install -g @cvasingh/httpsurl
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Both `hsurl` and `httpsurl` commands are available after install.
|
|
14
|
+
|
|
15
|
+
## Command Reference
|
|
16
|
+
|
|
17
|
+
| Command | Aliases | Description |
|
|
18
|
+
|---------|---------|-------------|
|
|
19
|
+
| `hsurl -v` | `-V`, `--version` | Show version |
|
|
20
|
+
| `hsurl -h` | `--help` | Show help |
|
|
21
|
+
| `hsurl login` | `signin`, `auth` | Authenticate via browser |
|
|
22
|
+
| `hsurl login --token <t>` | | Set token directly |
|
|
23
|
+
| `hsurl logout` | `signout` | Remove saved credentials |
|
|
24
|
+
| `hsurl http <port>` | `live`, `start`, `tunnel`, `expose`, `serve` | Create a tunnel |
|
|
25
|
+
| `hsurl http <port> -s <url>` | | Custom server URL |
|
|
26
|
+
| `hsurl status` | `info` | Show authentication status |
|
|
27
|
+
| `hsurl whoami` | `me` | Print current user ID |
|
|
28
|
+
|
|
29
|
+
## Quick Start
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
# 1. Login
|
|
33
|
+
hsurl login
|
|
10
34
|
|
|
11
|
-
#
|
|
12
|
-
|
|
35
|
+
# 2. Start a local server
|
|
36
|
+
npx http-server -p 8080
|
|
37
|
+
|
|
38
|
+
# 3. Expose it
|
|
39
|
+
hsurl live 8080
|
|
13
40
|
```
|
|
14
41
|
|
|
42
|
+
Your server is now accessible at `https://<tunnelId>.httpsurl.in`.
|
|
43
|
+
|
|
15
44
|
## Usage
|
|
16
45
|
|
|
17
46
|
### Login
|
|
18
47
|
|
|
19
48
|
```bash
|
|
20
|
-
# Opens browser for Clerk authentication
|
|
21
|
-
|
|
49
|
+
# Opens browser for Clerk authentication + CLI token exchange
|
|
50
|
+
hsurl login
|
|
22
51
|
|
|
23
|
-
# Or provide token directly
|
|
24
|
-
|
|
52
|
+
# Or provide a CLI token directly
|
|
53
|
+
hsurl login --token <cli-token>
|
|
54
|
+
|
|
55
|
+
# Aliases
|
|
56
|
+
hsurl signin
|
|
57
|
+
hsurl auth
|
|
25
58
|
```
|
|
26
59
|
|
|
27
|
-
|
|
60
|
+
The login flow:
|
|
61
|
+
1. CLI starts a temporary localhost HTTP server
|
|
62
|
+
2. Opens browser to `https://httpsurl.in/cli-auth?callback=http://localhost:PORT`
|
|
63
|
+
3. You sign in via Clerk in the browser
|
|
64
|
+
4. The browser exchanges the Clerk JWT for a long-lived CLI token via `POST /api/cli-token`
|
|
65
|
+
5. The CLI token (`cli_<userId>_<hash>`) is redirected back to the CLI callback
|
|
66
|
+
6. Token is saved to `~/.httpsurl/credentials.json` (chmod 600)
|
|
28
67
|
|
|
29
68
|
### Create a Tunnel
|
|
30
69
|
|
|
31
70
|
```bash
|
|
32
71
|
# Expose local port 3000
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
72
|
+
hsurl http 3000
|
|
73
|
+
hsurl live 3000
|
|
74
|
+
hsurl start 3000
|
|
75
|
+
hsurl tunnel 3000
|
|
76
|
+
hsurl expose 3000
|
|
77
|
+
hsurl serve 3000
|
|
78
|
+
|
|
79
|
+
# Custom server URL (for local development)
|
|
80
|
+
hsurl http 8080 --server ws://localhost:3000/connect
|
|
40
81
|
```
|
|
41
82
|
|
|
42
83
|
Output:
|
|
43
84
|
|
|
44
85
|
```
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
86
|
+
i Connecting to wss://httpsurl.in/connect...
|
|
87
|
+
i Connected, registering tunnel...
|
|
88
|
+
v Tunnel registered!
|
|
48
89
|
|
|
49
90
|
httpsurl.in
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
91
|
+
-----------------------------
|
|
92
|
+
> Forwarding: https://abc123def456.httpsurl.in
|
|
93
|
+
-----------------------------
|
|
94
|
+
|
|
95
|
+
> GET / 200 12ms
|
|
96
|
+
> POST /api/data 201 45ms
|
|
97
|
+
> GET /style.css 200 3ms
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
### Account
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
# Check auth status
|
|
104
|
+
hsurl status
|
|
105
|
+
hsurl info
|
|
106
|
+
|
|
107
|
+
# Print user ID
|
|
108
|
+
hsurl whoami
|
|
109
|
+
hsurl me
|
|
53
110
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
111
|
+
# Logout
|
|
112
|
+
hsurl logout
|
|
113
|
+
hsurl signout
|
|
57
114
|
```
|
|
58
115
|
|
|
59
116
|
## Modules
|
|
60
117
|
|
|
61
|
-
### `cli.js`
|
|
62
|
-
|
|
63
|
-
- `
|
|
64
|
-
- `
|
|
118
|
+
### `cli.js` -- Commander CLI
|
|
119
|
+
Commands with aliases:
|
|
120
|
+
- `login` (`signin`, `auth`) -- Opens browser to Clerk sign-in, exchanges JWT for CLI token
|
|
121
|
+
- `http` (`live`, `start`, `tunnel`, `expose`, `serve`) -- Creates tunnel with `--server` option
|
|
122
|
+
- `status` (`info`) -- Shows authentication status and credentials path
|
|
123
|
+
- `whoami` (`me`) -- Prints current user ID
|
|
124
|
+
- `logout` (`signout`) -- Removes saved credentials
|
|
65
125
|
|
|
66
|
-
### `tunnel-client.js`
|
|
67
|
-
- Connects to server, sends `register` message with
|
|
126
|
+
### `tunnel-client.js` -- WebSocket Client
|
|
127
|
+
- Connects to server, sends `register` message with CLI token
|
|
68
128
|
- Receives `registered` with tunnel URL
|
|
69
129
|
- Handles `request` messages by proxying to local server via `local-proxy.js`
|
|
70
130
|
- Sends `response` or `response_error` back
|
|
71
131
|
- Auto-reconnects with exponential backoff on disconnect
|
|
72
|
-
- Preserves tunnel ID across reconnections
|
|
132
|
+
- Preserves tunnel ID across reconnections (within 60s grace period)
|
|
73
133
|
|
|
74
|
-
### `local-proxy.js`
|
|
134
|
+
### `local-proxy.js` -- HTTP Proxy
|
|
75
135
|
- Makes HTTP requests to `127.0.0.1:<port>`
|
|
76
136
|
- Strips `host` and `x-tunnel-host` headers
|
|
77
137
|
- Base64 encodes/decodes request and response bodies
|
|
78
138
|
- 30s timeout per request
|
|
79
139
|
|
|
80
|
-
### `reconnect.js`
|
|
140
|
+
### `reconnect.js` -- Exponential Backoff
|
|
81
141
|
- Initial delay: 1s
|
|
82
|
-
- Doubles each attempt: 1s
|
|
142
|
+
- Doubles each attempt: 1s -> 2s -> 4s -> 8s -> 16s -> 30s (max)
|
|
83
143
|
- Resets on successful reconnection
|
|
84
144
|
- Cancellable
|
|
85
145
|
|
|
86
|
-
### `logger.js`
|
|
146
|
+
### `logger.js` -- Terminal Output
|
|
87
147
|
Colored output using chalk:
|
|
88
|
-
- `info`
|
|
89
|
-
- `success`
|
|
90
|
-
- `warn`
|
|
91
|
-
- `error`
|
|
92
|
-
- `request`
|
|
93
|
-
- `banner`
|
|
94
|
-
|
|
95
|
-
### `protocol.js`
|
|
148
|
+
- `info` -- blue
|
|
149
|
+
- `success` -- green
|
|
150
|
+
- `warn` -- yellow
|
|
151
|
+
- `error` -- red
|
|
152
|
+
- `request` -- method-colored badges with status code and latency
|
|
153
|
+
- `banner` -- formatted tunnel URL display
|
|
154
|
+
|
|
155
|
+
### `protocol.js` -- Message Constants
|
|
96
156
|
Shared message type constants (duplicated from server for simplicity):
|
|
97
157
|
- `MSG_REGISTER`, `MSG_RESPONSE`, `MSG_RESPONSE_ERROR`
|
|
98
158
|
- `MSG_REGISTERED`, `MSG_REQUEST`, `MSG_ERROR`
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cvasingh/httpsurl",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "Expose local servers to the internet via httpsurl.in tunnels",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"tunnel",
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
},
|
|
19
19
|
"author": "cvasingh",
|
|
20
20
|
"bin": {
|
|
21
|
+
"hsurl": "bin/httpsurl.js",
|
|
21
22
|
"httpsurl": "bin/httpsurl.js"
|
|
22
23
|
},
|
|
23
24
|
"files": [
|
package/src/cli.js
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
|
-
const { Command } = require(
|
|
2
|
-
const { TunnelClient } = require(
|
|
3
|
-
const log = require(
|
|
4
|
-
const fs = require(
|
|
5
|
-
const path = require(
|
|
6
|
-
const os = require(
|
|
1
|
+
const { Command } = require("commander");
|
|
2
|
+
const { TunnelClient } = require("./tunnel-client");
|
|
3
|
+
const log = require("./logger");
|
|
4
|
+
const fs = require("fs");
|
|
5
|
+
const path = require("path");
|
|
6
|
+
const os = require("os");
|
|
7
7
|
|
|
8
|
-
const CREDENTIALS_DIR = path.join(os.homedir(),
|
|
9
|
-
const CREDENTIALS_FILE = path.join(CREDENTIALS_DIR,
|
|
8
|
+
const CREDENTIALS_DIR = path.join(os.homedir(), ".httpsurl");
|
|
9
|
+
const CREDENTIALS_FILE = path.join(CREDENTIALS_DIR, "credentials.json");
|
|
10
10
|
|
|
11
11
|
function loadToken() {
|
|
12
12
|
try {
|
|
13
|
-
const data = JSON.parse(fs.readFileSync(CREDENTIALS_FILE,
|
|
13
|
+
const data = JSON.parse(fs.readFileSync(CREDENTIALS_FILE, "utf8"));
|
|
14
14
|
return data.token;
|
|
15
15
|
} catch {
|
|
16
16
|
return null;
|
|
@@ -23,108 +23,178 @@ function saveToken(token) {
|
|
|
23
23
|
fs.chmodSync(CREDENTIALS_FILE, 0o600);
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
+
// Shared tunnel action
|
|
27
|
+
async function tunnelAction(port, options) {
|
|
28
|
+
if (!port || port < 1 || port > 65535) {
|
|
29
|
+
log.error("Invalid port number");
|
|
30
|
+
process.exit(1);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const token = loadToken();
|
|
34
|
+
if (!token) {
|
|
35
|
+
log.error("Not authenticated. Run: hsurl login");
|
|
36
|
+
process.exit(1);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const client = new TunnelClient({
|
|
40
|
+
serverUrl: options.server,
|
|
41
|
+
localPort: port,
|
|
42
|
+
token,
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
process.on("SIGINT", () => {
|
|
46
|
+
log.info("\nShutting down...");
|
|
47
|
+
client.close();
|
|
48
|
+
process.exit(0);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
try {
|
|
52
|
+
await client.connect();
|
|
53
|
+
} catch (err) {
|
|
54
|
+
log.error(`Failed to connect: ${err.message}`);
|
|
55
|
+
process.exit(1);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Shared login action
|
|
60
|
+
async function loginAction(options) {
|
|
61
|
+
if (options.token) {
|
|
62
|
+
saveToken(options.token);
|
|
63
|
+
log.success("Token saved!");
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const http = require("http");
|
|
68
|
+
|
|
69
|
+
const server = http.createServer((req, res) => {
|
|
70
|
+
const url = new URL(req.url, "http://localhost");
|
|
71
|
+
const token = url.searchParams.get("token");
|
|
72
|
+
|
|
73
|
+
if (token) {
|
|
74
|
+
saveToken(token);
|
|
75
|
+
res.writeHead(200, { "Content-Type": "text/html" });
|
|
76
|
+
res.end(`
|
|
77
|
+
<html>
|
|
78
|
+
<body style="font-family: system-ui; display: flex; justify-content: center; align-items: center; height: 100vh; margin: 0;">
|
|
79
|
+
<div style="text-align: center;">
|
|
80
|
+
<h1 style="font-weight: 300;">Authenticated</h1>
|
|
81
|
+
<p style="color: #666;">You can close this window and return to the terminal.</p>
|
|
82
|
+
</div>
|
|
83
|
+
</body>
|
|
84
|
+
</html>
|
|
85
|
+
`);
|
|
86
|
+
server.close();
|
|
87
|
+
log.success("Authenticated successfully!");
|
|
88
|
+
process.exit(0);
|
|
89
|
+
} else {
|
|
90
|
+
res.writeHead(400);
|
|
91
|
+
res.end("Missing token");
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
server.listen(0, async () => {
|
|
96
|
+
const port = server.address().port;
|
|
97
|
+
const callbackUrl = `http://localhost:${port}`;
|
|
98
|
+
const loginUrl = `https://httpsurl.in/cli-auth?callback=${encodeURIComponent(callbackUrl)}`;
|
|
99
|
+
|
|
100
|
+
log.info("Opening browser for authentication...");
|
|
101
|
+
log.info(`If browser doesn't open, visit: ${loginUrl}`);
|
|
102
|
+
|
|
103
|
+
try {
|
|
104
|
+
const open = require("open");
|
|
105
|
+
await open(loginUrl);
|
|
106
|
+
} catch {
|
|
107
|
+
log.warn("Could not open browser automatically");
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
|
|
26
112
|
function createCli() {
|
|
27
113
|
const program = new Command();
|
|
28
114
|
|
|
29
115
|
program
|
|
30
|
-
.name(
|
|
31
|
-
.description(
|
|
32
|
-
.version(
|
|
116
|
+
.name("hsurl")
|
|
117
|
+
.description("Expose local servers to the internet via httpsurl.in")
|
|
118
|
+
.version("1.1.0", "-v, -V, --version");
|
|
33
119
|
|
|
34
|
-
//
|
|
120
|
+
// ── login / signin / auth ──
|
|
121
|
+
program
|
|
122
|
+
.command("login")
|
|
123
|
+
.alias("signin")
|
|
124
|
+
.alias("auth")
|
|
125
|
+
.description("Authenticate with httpsurl.in")
|
|
126
|
+
.option("--token <token>", "Provide token directly")
|
|
127
|
+
.action(loginAction);
|
|
128
|
+
|
|
129
|
+
// ── logout / signout ──
|
|
35
130
|
program
|
|
36
|
-
.command(
|
|
37
|
-
.
|
|
38
|
-
.
|
|
39
|
-
.action(
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
log.success(
|
|
43
|
-
|
|
131
|
+
.command("logout")
|
|
132
|
+
.alias("signout")
|
|
133
|
+
.description("Remove saved credentials")
|
|
134
|
+
.action(() => {
|
|
135
|
+
try {
|
|
136
|
+
fs.unlinkSync(CREDENTIALS_FILE);
|
|
137
|
+
log.success("Logged out successfully");
|
|
138
|
+
} catch {
|
|
139
|
+
log.info("No credentials found");
|
|
44
140
|
}
|
|
45
|
-
|
|
46
|
-
// Open browser for Clerk auth
|
|
47
|
-
const http = require('http');
|
|
48
|
-
|
|
49
|
-
const server = http.createServer((req, res) => {
|
|
50
|
-
const url = new URL(req.url, 'http://localhost');
|
|
51
|
-
const token = url.searchParams.get('token');
|
|
52
|
-
|
|
53
|
-
if (token) {
|
|
54
|
-
saveToken(token);
|
|
55
|
-
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
56
|
-
res.end(`
|
|
57
|
-
<html>
|
|
58
|
-
<body style="font-family: system-ui; display: flex; justify-content: center; align-items: center; height: 100vh; margin: 0;">
|
|
59
|
-
<div style="text-align: center;">
|
|
60
|
-
<h1 style="font-weight: 300;">✓ Authenticated</h1>
|
|
61
|
-
<p style="color: #666;">You can close this window and return to the terminal.</p>
|
|
62
|
-
</div>
|
|
63
|
-
</body>
|
|
64
|
-
</html>
|
|
65
|
-
`);
|
|
66
|
-
server.close();
|
|
67
|
-
log.success('Authenticated successfully!');
|
|
68
|
-
process.exit(0);
|
|
69
|
-
} else {
|
|
70
|
-
res.writeHead(400);
|
|
71
|
-
res.end('Missing token');
|
|
72
|
-
}
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
server.listen(0, async () => {
|
|
76
|
-
const port = server.address().port;
|
|
77
|
-
const callbackUrl = `http://localhost:${port}`;
|
|
78
|
-
const loginUrl = `https://httpsurl.in/sign-in?redirect_url=${encodeURIComponent(callbackUrl)}`;
|
|
79
|
-
|
|
80
|
-
log.info(`Opening browser for authentication...`);
|
|
81
|
-
log.info(`If browser doesn't open, visit: ${loginUrl}`);
|
|
82
|
-
|
|
83
|
-
try {
|
|
84
|
-
const open = require('open');
|
|
85
|
-
await open(loginUrl);
|
|
86
|
-
} catch {
|
|
87
|
-
log.warn('Could not open browser automatically');
|
|
88
|
-
}
|
|
89
|
-
});
|
|
90
141
|
});
|
|
91
142
|
|
|
92
|
-
//
|
|
143
|
+
// ── http / live / start / tunnel / expose / serve ──
|
|
144
|
+
const tunnelOpts = [
|
|
145
|
+
"-s, --server <url>",
|
|
146
|
+
"Server WebSocket URL",
|
|
147
|
+
"wss://httpsurl.in/connect",
|
|
148
|
+
];
|
|
149
|
+
|
|
150
|
+
program
|
|
151
|
+
.command("http")
|
|
152
|
+
.alias("live")
|
|
153
|
+
.alias("start")
|
|
154
|
+
.alias("tunnel")
|
|
155
|
+
.alias("expose")
|
|
156
|
+
.alias("serve")
|
|
157
|
+
.description("Create a tunnel to a local HTTP server")
|
|
158
|
+
.argument("<port>", "Local port to expose", parseInt)
|
|
159
|
+
.option(...tunnelOpts)
|
|
160
|
+
.action(tunnelAction);
|
|
161
|
+
|
|
162
|
+
// ── status / info ──
|
|
93
163
|
program
|
|
94
|
-
.command(
|
|
95
|
-
.
|
|
96
|
-
.
|
|
97
|
-
.
|
|
98
|
-
|
|
99
|
-
if (!
|
|
100
|
-
log.error(
|
|
164
|
+
.command("status")
|
|
165
|
+
.alias("info")
|
|
166
|
+
.description("Show current authentication status")
|
|
167
|
+
.action(() => {
|
|
168
|
+
const token = loadToken();
|
|
169
|
+
if (!token) {
|
|
170
|
+
log.error("Not authenticated. Run: hsurl login");
|
|
101
171
|
process.exit(1);
|
|
102
172
|
}
|
|
173
|
+
if (token.startsWith("cli_")) {
|
|
174
|
+
const userId = token.split("_").slice(1, -1).join("_");
|
|
175
|
+
log.success(`Authenticated as ${userId}`);
|
|
176
|
+
} else {
|
|
177
|
+
log.success("Authenticated (token saved)");
|
|
178
|
+
}
|
|
179
|
+
log.info(`Credentials: ${CREDENTIALS_FILE}`);
|
|
180
|
+
});
|
|
103
181
|
|
|
182
|
+
// ── whoami / me ──
|
|
183
|
+
program
|
|
184
|
+
.command("whoami")
|
|
185
|
+
.alias("me")
|
|
186
|
+
.description("Show the current authenticated user")
|
|
187
|
+
.action(() => {
|
|
104
188
|
const token = loadToken();
|
|
105
189
|
if (!token) {
|
|
106
|
-
log.error(
|
|
190
|
+
log.error("Not authenticated. Run: hsurl login");
|
|
107
191
|
process.exit(1);
|
|
108
192
|
}
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
token
|
|
114
|
-
});
|
|
115
|
-
|
|
116
|
-
// Graceful shutdown
|
|
117
|
-
process.on('SIGINT', () => {
|
|
118
|
-
log.info('\nShutting down...');
|
|
119
|
-
client.close();
|
|
120
|
-
process.exit(0);
|
|
121
|
-
});
|
|
122
|
-
|
|
123
|
-
try {
|
|
124
|
-
await client.connect();
|
|
125
|
-
} catch (err) {
|
|
126
|
-
log.error(`Failed to connect: ${err.message}`);
|
|
127
|
-
process.exit(1);
|
|
193
|
+
if (token.startsWith("cli_")) {
|
|
194
|
+
const userId = token.split("_").slice(1, -1).join("_");
|
|
195
|
+
console.log(userId);
|
|
196
|
+
} else {
|
|
197
|
+
console.log("authenticated (legacy token)");
|
|
128
198
|
}
|
|
129
199
|
});
|
|
130
200
|
|
package/src/tunnel-client.js
CHANGED
|
@@ -1,8 +1,15 @@
|
|
|
1
|
-
const WebSocket = require(
|
|
2
|
-
const {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
1
|
+
const WebSocket = require("ws");
|
|
2
|
+
const {
|
|
3
|
+
MSG_REGISTER,
|
|
4
|
+
MSG_REGISTERED,
|
|
5
|
+
MSG_REQUEST,
|
|
6
|
+
MSG_RESPONSE,
|
|
7
|
+
MSG_RESPONSE_ERROR,
|
|
8
|
+
MSG_ERROR,
|
|
9
|
+
} = require("./protocol");
|
|
10
|
+
const { makeLocalRequest } = require("./local-proxy");
|
|
11
|
+
const { ReconnectStrategy } = require("./reconnect");
|
|
12
|
+
const log = require("./logger");
|
|
6
13
|
|
|
7
14
|
class TunnelClient {
|
|
8
15
|
constructor(options) {
|
|
@@ -26,31 +33,33 @@ class TunnelClient {
|
|
|
26
33
|
|
|
27
34
|
this.ws = new WebSocket(url);
|
|
28
35
|
|
|
29
|
-
this.ws.on(
|
|
30
|
-
log.info(
|
|
36
|
+
this.ws.on("open", () => {
|
|
37
|
+
log.info("Connected, registering tunnel...");
|
|
31
38
|
|
|
32
39
|
// Send register message
|
|
33
|
-
this.ws.send(
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
40
|
+
this.ws.send(
|
|
41
|
+
JSON.stringify({
|
|
42
|
+
type: MSG_REGISTER,
|
|
43
|
+
token: this.token,
|
|
44
|
+
port: this.localPort,
|
|
45
|
+
tunnelId: this.tunnelId, // null on first connect, set on reconnect
|
|
46
|
+
}),
|
|
47
|
+
);
|
|
39
48
|
});
|
|
40
49
|
|
|
41
|
-
this.ws.on(
|
|
50
|
+
this.ws.on("message", (data) => {
|
|
42
51
|
let msg;
|
|
43
52
|
try {
|
|
44
53
|
msg = JSON.parse(data.toString());
|
|
45
54
|
} catch {
|
|
46
|
-
log.error(
|
|
55
|
+
log.error("Invalid message from server");
|
|
47
56
|
return;
|
|
48
57
|
}
|
|
49
58
|
|
|
50
59
|
this._handleMessage(msg, resolve);
|
|
51
60
|
});
|
|
52
61
|
|
|
53
|
-
this.ws.on(
|
|
62
|
+
this.ws.on("close", (code, reason) => {
|
|
54
63
|
if (this._closing) return;
|
|
55
64
|
|
|
56
65
|
log.warn(`Disconnected (code: ${code}). Reconnecting...`);
|
|
@@ -58,7 +67,7 @@ class TunnelClient {
|
|
|
58
67
|
log.info(`Reconnecting in ${delay}ms...`);
|
|
59
68
|
});
|
|
60
69
|
|
|
61
|
-
this.ws.on(
|
|
70
|
+
this.ws.on("error", (err) => {
|
|
62
71
|
if (this._closing) return;
|
|
63
72
|
|
|
64
73
|
// Only reject on first connection attempt
|
|
@@ -78,7 +87,7 @@ class TunnelClient {
|
|
|
78
87
|
this._reconnect.reset();
|
|
79
88
|
|
|
80
89
|
log.success(`Tunnel registered!`);
|
|
81
|
-
log.banner(`
|
|
90
|
+
log.banner(`https://${msg.url}`);
|
|
82
91
|
|
|
83
92
|
if (resolveConnect) resolveConnect(this);
|
|
84
93
|
break;
|
|
@@ -109,22 +118,26 @@ class TunnelClient {
|
|
|
109
118
|
body,
|
|
110
119
|
});
|
|
111
120
|
|
|
112
|
-
this.ws.send(
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
121
|
+
this.ws.send(
|
|
122
|
+
JSON.stringify({
|
|
123
|
+
type: MSG_RESPONSE,
|
|
124
|
+
requestId,
|
|
125
|
+
statusCode: response.statusCode,
|
|
126
|
+
headers: response.headers,
|
|
127
|
+
body: response.body,
|
|
128
|
+
}),
|
|
129
|
+
);
|
|
119
130
|
|
|
120
131
|
const latency = Date.now() - start;
|
|
121
132
|
log.request(method, path, response.statusCode, latency);
|
|
122
133
|
} catch (err) {
|
|
123
|
-
this.ws.send(
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
134
|
+
this.ws.send(
|
|
135
|
+
JSON.stringify({
|
|
136
|
+
type: MSG_RESPONSE_ERROR,
|
|
137
|
+
requestId,
|
|
138
|
+
error: err.message,
|
|
139
|
+
}),
|
|
140
|
+
);
|
|
128
141
|
|
|
129
142
|
log.error(`${method} ${path} → Error: ${err.message}`);
|
|
130
143
|
}
|
|
@@ -134,7 +147,7 @@ class TunnelClient {
|
|
|
134
147
|
this._closing = true;
|
|
135
148
|
this._reconnect.destroy();
|
|
136
149
|
if (this.ws) {
|
|
137
|
-
this.ws.close(1000,
|
|
150
|
+
this.ws.close(1000, "Client closing");
|
|
138
151
|
}
|
|
139
152
|
}
|
|
140
153
|
|