@cvasingh/httpsurl 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/README.md +108 -0
- package/bin/httpsurl.js +6 -0
- package/package.json +45 -0
- package/src/cli.js +134 -0
- package/src/local-proxy.js +52 -0
- package/src/logger.js +45 -0
- package/src/protocol.js +11 -0
- package/src/reconnect.js +33 -0
- package/src/tunnel-client.js +151 -0
package/README.md
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
# @httpsurl/client
|
|
2
|
+
|
|
3
|
+
CLI tool to expose local HTTP servers to the internet via httpsurl.in tunnels.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
# From the monorepo root
|
|
9
|
+
npm install
|
|
10
|
+
|
|
11
|
+
# Or install globally (after publishing)
|
|
12
|
+
npm install -g @httpsurl/client
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Usage
|
|
16
|
+
|
|
17
|
+
### Login
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
# Opens browser for Clerk authentication
|
|
21
|
+
httpsurl login
|
|
22
|
+
|
|
23
|
+
# Or provide token directly
|
|
24
|
+
httpsurl login --token <clerk-session-token>
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Token is stored in `~/.httpsurl/credentials.json` (chmod 600).
|
|
28
|
+
|
|
29
|
+
### Create a Tunnel
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
# Expose local port 3000
|
|
33
|
+
httpsurl http 3000
|
|
34
|
+
|
|
35
|
+
# Custom server URL
|
|
36
|
+
httpsurl http 8080 --server wss://httpsurl.in/connect
|
|
37
|
+
|
|
38
|
+
# Local development
|
|
39
|
+
httpsurl http 8080 --server ws://localhost:3000/connect
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Output:
|
|
43
|
+
|
|
44
|
+
```
|
|
45
|
+
ℹ Connecting to wss://httpsurl.in/connect...
|
|
46
|
+
ℹ Connected, registering tunnel...
|
|
47
|
+
✔ Tunnel registered!
|
|
48
|
+
|
|
49
|
+
httpsurl.in
|
|
50
|
+
─────────────────────────────
|
|
51
|
+
▸ Forwarding: http://abc123def.httpsurl.in
|
|
52
|
+
─────────────────────────────
|
|
53
|
+
|
|
54
|
+
→ GET / 200 12ms
|
|
55
|
+
→ POST /api/data 201 45ms
|
|
56
|
+
→ GET /style.css 200 3ms
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Modules
|
|
60
|
+
|
|
61
|
+
### `cli.js` — Commander CLI
|
|
62
|
+
Two commands:
|
|
63
|
+
- `httpsurl login` — Opens browser to Clerk sign-in, receives token via localhost callback server
|
|
64
|
+
- `httpsurl http <port>` — Creates tunnel with `--server` option (default: `wss://httpsurl.in/connect`)
|
|
65
|
+
|
|
66
|
+
### `tunnel-client.js` — WebSocket Client
|
|
67
|
+
- Connects to server, sends `register` message with Clerk token
|
|
68
|
+
- Receives `registered` with tunnel URL
|
|
69
|
+
- Handles `request` messages by proxying to local server via `local-proxy.js`
|
|
70
|
+
- Sends `response` or `response_error` back
|
|
71
|
+
- Auto-reconnects with exponential backoff on disconnect
|
|
72
|
+
- Preserves tunnel ID across reconnections
|
|
73
|
+
|
|
74
|
+
### `local-proxy.js` — HTTP Proxy
|
|
75
|
+
- Makes HTTP requests to `127.0.0.1:<port>`
|
|
76
|
+
- Strips `host` and `x-tunnel-host` headers
|
|
77
|
+
- Base64 encodes/decodes request and response bodies
|
|
78
|
+
- 30s timeout per request
|
|
79
|
+
|
|
80
|
+
### `reconnect.js` — Exponential Backoff
|
|
81
|
+
- Initial delay: 1s
|
|
82
|
+
- Doubles each attempt: 1s → 2s → 4s → 8s → 16s → 30s (max)
|
|
83
|
+
- Resets on successful reconnection
|
|
84
|
+
- Cancellable
|
|
85
|
+
|
|
86
|
+
### `logger.js` — Terminal Output
|
|
87
|
+
Colored output using chalk:
|
|
88
|
+
- `info` — blue
|
|
89
|
+
- `success` — green
|
|
90
|
+
- `warn` — yellow
|
|
91
|
+
- `error` — red
|
|
92
|
+
- `request` — method-colored badges with status code and latency
|
|
93
|
+
- `banner` — formatted tunnel URL display
|
|
94
|
+
|
|
95
|
+
### `protocol.js` — Message Constants
|
|
96
|
+
Shared message type constants (duplicated from server for simplicity):
|
|
97
|
+
- `MSG_REGISTER`, `MSG_RESPONSE`, `MSG_RESPONSE_ERROR`
|
|
98
|
+
- `MSG_REGISTERED`, `MSG_REQUEST`, `MSG_ERROR`
|
|
99
|
+
|
|
100
|
+
## Dependencies
|
|
101
|
+
|
|
102
|
+
| Package | Purpose |
|
|
103
|
+
|---------|---------|
|
|
104
|
+
| ws | WebSocket client |
|
|
105
|
+
| commander | CLI argument parsing |
|
|
106
|
+
| chalk (v4, CJS) | Terminal colors |
|
|
107
|
+
| nanoid (v3, CJS) | Request ID generation |
|
|
108
|
+
| open (v8) | Open browser for login |
|
package/bin/httpsurl.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@cvasingh/httpsurl",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Expose local servers to the internet via httpsurl.in tunnels",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"tunnel",
|
|
7
|
+
"localhost",
|
|
8
|
+
"ngrok",
|
|
9
|
+
"proxy",
|
|
10
|
+
"cli",
|
|
11
|
+
"httpsurl"
|
|
12
|
+
],
|
|
13
|
+
"license": "MIT",
|
|
14
|
+
"repository": {
|
|
15
|
+
"type": "git",
|
|
16
|
+
"url": "git+https://github.com/cvasingh/httpsurl.git",
|
|
17
|
+
"directory": "packages/client"
|
|
18
|
+
},
|
|
19
|
+
"author": "cvasingh",
|
|
20
|
+
"bin": {
|
|
21
|
+
"httpsurl": "bin/httpsurl.js"
|
|
22
|
+
},
|
|
23
|
+
"files": [
|
|
24
|
+
"bin/",
|
|
25
|
+
"src/",
|
|
26
|
+
"README.md"
|
|
27
|
+
],
|
|
28
|
+
"scripts": {
|
|
29
|
+
"test": "vitest run",
|
|
30
|
+
"test:watch": "vitest"
|
|
31
|
+
},
|
|
32
|
+
"dependencies": {
|
|
33
|
+
"chalk": "^4.1.2",
|
|
34
|
+
"commander": "^12.0.0",
|
|
35
|
+
"nanoid": "^3.3.7",
|
|
36
|
+
"open": "^8.4.2",
|
|
37
|
+
"ws": "^8.16.0"
|
|
38
|
+
},
|
|
39
|
+
"devDependencies": {
|
|
40
|
+
"vitest": "^1.6.0"
|
|
41
|
+
},
|
|
42
|
+
"engines": {
|
|
43
|
+
"node": ">=18"
|
|
44
|
+
}
|
|
45
|
+
}
|
package/src/cli.js
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
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
|
+
|
|
8
|
+
const CREDENTIALS_DIR = path.join(os.homedir(), '.httpsurl');
|
|
9
|
+
const CREDENTIALS_FILE = path.join(CREDENTIALS_DIR, 'credentials.json');
|
|
10
|
+
|
|
11
|
+
function loadToken() {
|
|
12
|
+
try {
|
|
13
|
+
const data = JSON.parse(fs.readFileSync(CREDENTIALS_FILE, 'utf8'));
|
|
14
|
+
return data.token;
|
|
15
|
+
} catch {
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function saveToken(token) {
|
|
21
|
+
fs.mkdirSync(CREDENTIALS_DIR, { recursive: true });
|
|
22
|
+
fs.writeFileSync(CREDENTIALS_FILE, JSON.stringify({ token }, null, 2));
|
|
23
|
+
fs.chmodSync(CREDENTIALS_FILE, 0o600);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function createCli() {
|
|
27
|
+
const program = new Command();
|
|
28
|
+
|
|
29
|
+
program
|
|
30
|
+
.name('httpsurl')
|
|
31
|
+
.description('Expose local servers to the internet via httpsurl.in')
|
|
32
|
+
.version('1.0.0');
|
|
33
|
+
|
|
34
|
+
// Login command
|
|
35
|
+
program
|
|
36
|
+
.command('login')
|
|
37
|
+
.description('Authenticate with httpsurl.in')
|
|
38
|
+
.option('--token <token>', 'Provide token directly')
|
|
39
|
+
.action(async (options) => {
|
|
40
|
+
if (options.token) {
|
|
41
|
+
saveToken(options.token);
|
|
42
|
+
log.success('Token saved!');
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
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
|
+
});
|
|
91
|
+
|
|
92
|
+
// HTTP tunnel command
|
|
93
|
+
program
|
|
94
|
+
.command('http')
|
|
95
|
+
.description('Create a tunnel to a local HTTP server')
|
|
96
|
+
.argument('<port>', 'Local port to expose', parseInt)
|
|
97
|
+
.option('-s, --server <url>', 'Server WebSocket URL', 'wss://httpsurl.in/connect')
|
|
98
|
+
.action(async (port, options) => {
|
|
99
|
+
if (!port || port < 1 || port > 65535) {
|
|
100
|
+
log.error('Invalid port number');
|
|
101
|
+
process.exit(1);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const token = loadToken();
|
|
105
|
+
if (!token) {
|
|
106
|
+
log.error('Not authenticated. Run: httpsurl login');
|
|
107
|
+
process.exit(1);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const client = new TunnelClient({
|
|
111
|
+
serverUrl: options.server,
|
|
112
|
+
localPort: port,
|
|
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);
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
return program;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
module.exports = { createCli };
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
const http = require('http');
|
|
2
|
+
|
|
3
|
+
function makeLocalRequest(port, requestData) {
|
|
4
|
+
return new Promise((resolve, reject) => {
|
|
5
|
+
const { method, path, headers, body } = requestData;
|
|
6
|
+
|
|
7
|
+
// Remove headers that shouldn't be forwarded to local server
|
|
8
|
+
const localHeaders = { ...headers };
|
|
9
|
+
delete localHeaders.host;
|
|
10
|
+
delete localHeaders['x-tunnel-host'];
|
|
11
|
+
|
|
12
|
+
const options = {
|
|
13
|
+
hostname: '127.0.0.1',
|
|
14
|
+
port,
|
|
15
|
+
path,
|
|
16
|
+
method,
|
|
17
|
+
headers: localHeaders,
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const req = http.request(options, (res) => {
|
|
21
|
+
const chunks = [];
|
|
22
|
+
|
|
23
|
+
res.on('data', (chunk) => chunks.push(chunk));
|
|
24
|
+
|
|
25
|
+
res.on('end', () => {
|
|
26
|
+
const responseBody = Buffer.concat(chunks);
|
|
27
|
+
resolve({
|
|
28
|
+
statusCode: res.statusCode,
|
|
29
|
+
headers: res.headers,
|
|
30
|
+
body: responseBody.length > 0 ? responseBody.toString('base64') : null,
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
req.on('error', (err) => {
|
|
36
|
+
reject(new Error(`Local server error: ${err.message}`));
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
// Set timeout
|
|
40
|
+
req.setTimeout(30000, () => {
|
|
41
|
+
req.destroy(new Error('Local server timeout'));
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
if (body) {
|
|
45
|
+
req.write(Buffer.from(body, 'base64'));
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
req.end();
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
module.exports = { makeLocalRequest };
|
package/src/logger.js
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
const chalk = require('chalk');
|
|
2
|
+
|
|
3
|
+
const log = {
|
|
4
|
+
info(msg, ...args) {
|
|
5
|
+
console.log(chalk.blue('ℹ'), msg, ...args);
|
|
6
|
+
},
|
|
7
|
+
success(msg, ...args) {
|
|
8
|
+
console.log(chalk.green('✔'), msg, ...args);
|
|
9
|
+
},
|
|
10
|
+
warn(msg, ...args) {
|
|
11
|
+
console.log(chalk.yellow('⚠'), msg, ...args);
|
|
12
|
+
},
|
|
13
|
+
error(msg, ...args) {
|
|
14
|
+
console.error(chalk.red('✖'), msg, ...args);
|
|
15
|
+
},
|
|
16
|
+
request(method, path, statusCode, latency) {
|
|
17
|
+
const methodColor = {
|
|
18
|
+
GET: chalk.cyan,
|
|
19
|
+
POST: chalk.green,
|
|
20
|
+
PUT: chalk.yellow,
|
|
21
|
+
DELETE: chalk.red,
|
|
22
|
+
PATCH: chalk.magenta,
|
|
23
|
+
};
|
|
24
|
+
const colorFn = methodColor[method] || chalk.white;
|
|
25
|
+
const statusColor = statusCode < 400 ? chalk.green : statusCode < 500 ? chalk.yellow : chalk.red;
|
|
26
|
+
|
|
27
|
+
console.log(
|
|
28
|
+
chalk.gray('→'),
|
|
29
|
+
colorFn(method.padEnd(7)),
|
|
30
|
+
chalk.white(path),
|
|
31
|
+
statusColor(statusCode),
|
|
32
|
+
chalk.gray(`${latency}ms`)
|
|
33
|
+
);
|
|
34
|
+
},
|
|
35
|
+
banner(url) {
|
|
36
|
+
console.log('');
|
|
37
|
+
console.log(chalk.bold(' httpsurl.in'));
|
|
38
|
+
console.log(chalk.gray(' ─────────────────────────────'));
|
|
39
|
+
console.log(` ${chalk.green('▸')} Forwarding: ${chalk.cyan.underline(url)}`);
|
|
40
|
+
console.log(chalk.gray(' ─────────────────────────────'));
|
|
41
|
+
console.log('');
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
module.exports = log;
|
package/src/protocol.js
ADDED
package/src/reconnect.js
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
class ReconnectStrategy {
|
|
2
|
+
constructor(options = {}) {
|
|
3
|
+
this.initialDelay = options.initialDelay || 1000;
|
|
4
|
+
this.maxDelay = options.maxDelay || 30000;
|
|
5
|
+
this.currentDelay = this.initialDelay;
|
|
6
|
+
this._timer = null;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
schedule(fn) {
|
|
10
|
+
const delay = this.currentDelay;
|
|
11
|
+
this._timer = setTimeout(fn, delay);
|
|
12
|
+
// Exponential backoff
|
|
13
|
+
this.currentDelay = Math.min(this.currentDelay * 2, this.maxDelay);
|
|
14
|
+
return delay;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
reset() {
|
|
18
|
+
this.currentDelay = this.initialDelay;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
cancel() {
|
|
22
|
+
if (this._timer) {
|
|
23
|
+
clearTimeout(this._timer);
|
|
24
|
+
this._timer = null;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
destroy() {
|
|
29
|
+
this.cancel();
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
module.exports = { ReconnectStrategy };
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
const WebSocket = require('ws');
|
|
2
|
+
const { MSG_REGISTER, MSG_REGISTERED, MSG_REQUEST, MSG_RESPONSE, MSG_RESPONSE_ERROR, MSG_ERROR } = require('./protocol');
|
|
3
|
+
const { makeLocalRequest } = require('./local-proxy');
|
|
4
|
+
const { ReconnectStrategy } = require('./reconnect');
|
|
5
|
+
const log = require('./logger');
|
|
6
|
+
|
|
7
|
+
class TunnelClient {
|
|
8
|
+
constructor(options) {
|
|
9
|
+
this.serverUrl = options.serverUrl;
|
|
10
|
+
this.localPort = options.localPort;
|
|
11
|
+
this.token = options.token;
|
|
12
|
+
this.tunnelId = null;
|
|
13
|
+
this.tunnelUrl = null;
|
|
14
|
+
this.ws = null;
|
|
15
|
+
this._reconnect = new ReconnectStrategy();
|
|
16
|
+
this._closing = false;
|
|
17
|
+
this._requestCount = 0;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
connect() {
|
|
21
|
+
return new Promise((resolve, reject) => {
|
|
22
|
+
this._closing = false;
|
|
23
|
+
const url = this.serverUrl;
|
|
24
|
+
|
|
25
|
+
log.info(`Connecting to ${url}...`);
|
|
26
|
+
|
|
27
|
+
this.ws = new WebSocket(url);
|
|
28
|
+
|
|
29
|
+
this.ws.on('open', () => {
|
|
30
|
+
log.info('Connected, registering tunnel...');
|
|
31
|
+
|
|
32
|
+
// Send register message
|
|
33
|
+
this.ws.send(JSON.stringify({
|
|
34
|
+
type: MSG_REGISTER,
|
|
35
|
+
token: this.token,
|
|
36
|
+
port: this.localPort,
|
|
37
|
+
tunnelId: this.tunnelId, // null on first connect, set on reconnect
|
|
38
|
+
}));
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
this.ws.on('message', (data) => {
|
|
42
|
+
let msg;
|
|
43
|
+
try {
|
|
44
|
+
msg = JSON.parse(data.toString());
|
|
45
|
+
} catch {
|
|
46
|
+
log.error('Invalid message from server');
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
this._handleMessage(msg, resolve);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
this.ws.on('close', (code, reason) => {
|
|
54
|
+
if (this._closing) return;
|
|
55
|
+
|
|
56
|
+
log.warn(`Disconnected (code: ${code}). Reconnecting...`);
|
|
57
|
+
const delay = this._reconnect.schedule(() => this.connect());
|
|
58
|
+
log.info(`Reconnecting in ${delay}ms...`);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
this.ws.on('error', (err) => {
|
|
62
|
+
if (this._closing) return;
|
|
63
|
+
|
|
64
|
+
// Only reject on first connection attempt
|
|
65
|
+
if (!this.tunnelId) {
|
|
66
|
+
reject(err);
|
|
67
|
+
}
|
|
68
|
+
log.error(`WebSocket error: ${err.message}`);
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
_handleMessage(msg, resolveConnect) {
|
|
74
|
+
switch (msg.type) {
|
|
75
|
+
case MSG_REGISTERED: {
|
|
76
|
+
this.tunnelId = msg.tunnelId;
|
|
77
|
+
this.tunnelUrl = msg.url;
|
|
78
|
+
this._reconnect.reset();
|
|
79
|
+
|
|
80
|
+
log.success(`Tunnel registered!`);
|
|
81
|
+
log.banner(`http://${msg.url}`);
|
|
82
|
+
|
|
83
|
+
if (resolveConnect) resolveConnect(this);
|
|
84
|
+
break;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
case MSG_REQUEST: {
|
|
88
|
+
this._handleRequest(msg);
|
|
89
|
+
break;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
case MSG_ERROR: {
|
|
93
|
+
log.error(`Server error: ${msg.error}`);
|
|
94
|
+
break;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async _handleRequest(msg) {
|
|
100
|
+
const { requestId, method, path, headers, body } = msg;
|
|
101
|
+
const start = Date.now();
|
|
102
|
+
this._requestCount++;
|
|
103
|
+
|
|
104
|
+
try {
|
|
105
|
+
const response = await makeLocalRequest(this.localPort, {
|
|
106
|
+
method,
|
|
107
|
+
path,
|
|
108
|
+
headers,
|
|
109
|
+
body,
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
this.ws.send(JSON.stringify({
|
|
113
|
+
type: MSG_RESPONSE,
|
|
114
|
+
requestId,
|
|
115
|
+
statusCode: response.statusCode,
|
|
116
|
+
headers: response.headers,
|
|
117
|
+
body: response.body,
|
|
118
|
+
}));
|
|
119
|
+
|
|
120
|
+
const latency = Date.now() - start;
|
|
121
|
+
log.request(method, path, response.statusCode, latency);
|
|
122
|
+
} catch (err) {
|
|
123
|
+
this.ws.send(JSON.stringify({
|
|
124
|
+
type: MSG_RESPONSE_ERROR,
|
|
125
|
+
requestId,
|
|
126
|
+
error: err.message,
|
|
127
|
+
}));
|
|
128
|
+
|
|
129
|
+
log.error(`${method} ${path} → Error: ${err.message}`);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
close() {
|
|
134
|
+
this._closing = true;
|
|
135
|
+
this._reconnect.destroy();
|
|
136
|
+
if (this.ws) {
|
|
137
|
+
this.ws.close(1000, 'Client closing');
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
get stats() {
|
|
142
|
+
return {
|
|
143
|
+
tunnelId: this.tunnelId,
|
|
144
|
+
url: this.tunnelUrl,
|
|
145
|
+
requestCount: this._requestCount,
|
|
146
|
+
connected: this.ws?.readyState === WebSocket.OPEN,
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
module.exports = { TunnelClient };
|