@lifelessrasel/devtunnel-cli 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 +48 -0
- package/bin/devtunnel.js +104 -0
- package/package.json +38 -0
package/README.md
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# Tunnel Client (CLI)
|
|
2
|
+
|
|
3
|
+
The `tunnel-client` is a Node.js CLI tool that exposes your local server to the internet.
|
|
4
|
+
|
|
5
|
+
## How it Works
|
|
6
|
+
|
|
7
|
+
1. **Connection**: The CLI connects to the **Tunnel Server** via a WebSocket (`wss://tunnel.mohammadrasel.com`).
|
|
8
|
+
2. **Registration**: It sends a `register` event to claim a subdomain (e.g., `myapp`).
|
|
9
|
+
3. **Listening**: It keeps the WebSocket open, waiting for requests.
|
|
10
|
+
4. **Forwarding**:
|
|
11
|
+
* When the Tunnel Server receives a public HTTP request (e.g., `https://myapp.tunnel.mohammadrasel.com`), it serializes the request and sends it to the CLI over the WebSocket.
|
|
12
|
+
* The CLI receives this message.
|
|
13
|
+
* The CLI uses `axios` to make the **actual HTTP request** to your local service (e.g., `http://localhost:3000`).
|
|
14
|
+
* The CLI captures the response (status, headers, body).
|
|
15
|
+
* The CLI sends the response back to the Tunnel Server over the WebSocket.
|
|
16
|
+
5. **Response**: The Tunnel Server deserializes the response and sends it to the original public user.
|
|
17
|
+
|
|
18
|
+
## Usage
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
# Start a tunnel forwarding to localhost:3000
|
|
22
|
+
dt start --port 3000 --subdomain myapp --server wss://tunnel.mohammadrasel.com
|
|
23
|
+
|
|
24
|
+
# Options
|
|
25
|
+
--port, -p Local port to expose (default: 3000)
|
|
26
|
+
--subdomain, -s Requested subdomain (default: random)
|
|
27
|
+
--server Tunnel server URL (default: ws://localhost:8000)
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Architecture Diagram
|
|
31
|
+
|
|
32
|
+
```mermaid
|
|
33
|
+
sequenceDiagram
|
|
34
|
+
participant PublicUser
|
|
35
|
+
participant TunnelServer
|
|
36
|
+
participant CLI
|
|
37
|
+
participant LocalApp
|
|
38
|
+
|
|
39
|
+
CLI->>TunnelServer: WebSocket Connect (Register 'myapp')
|
|
40
|
+
TunnelServer-->>CLI: Registered 'myapp.devtunnel.net'
|
|
41
|
+
|
|
42
|
+
PublicUser->>TunnelServer: HTTP GET https://myapp.devtunnel.net/api/users
|
|
43
|
+
TunnelServer->>CLI: WS Message (type: request, path: /api/users)
|
|
44
|
+
CLI->>LocalApp: HTTP GET http://localhost:3000/api/users
|
|
45
|
+
LocalApp-->>CLI: JSON Response { "users": [] }
|
|
46
|
+
CLI->>TunnelServer: WS Message (type: response, body: {...})
|
|
47
|
+
TunnelServer-->>PublicUser: HTTP 200 OK { "users": [] }
|
|
48
|
+
```
|
package/bin/devtunnel.js
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const { Command } = require('commander');
|
|
4
|
+
const WebSocket = require('ws');
|
|
5
|
+
const axios = require('axios');
|
|
6
|
+
const program = new Command();
|
|
7
|
+
|
|
8
|
+
program
|
|
9
|
+
.name('devtunnel')
|
|
10
|
+
.description('Expose your local server to the internet')
|
|
11
|
+
.version('1.0.0');
|
|
12
|
+
|
|
13
|
+
program.command('start')
|
|
14
|
+
.description('Start a tunnel')
|
|
15
|
+
.option('-p, --port <number>', 'Local port to expose', '3000')
|
|
16
|
+
.option('-s, --subdomain <string>', 'Requested subdomain')
|
|
17
|
+
.option('--server <string>', 'Tunnel server URL', 'ws://localhost:8000')
|
|
18
|
+
.action(async (options) => {
|
|
19
|
+
const localPort = options.port;
|
|
20
|
+
const serverUrl = options.server;
|
|
21
|
+
const requestedSubdomain = options.subdomain;
|
|
22
|
+
|
|
23
|
+
console.log(`Connecting to ${serverUrl} requesting subdomain: ${requestedSubdomain || '(random)'}...`);
|
|
24
|
+
|
|
25
|
+
const ws = new WebSocket(serverUrl);
|
|
26
|
+
|
|
27
|
+
ws.on('open', () => {
|
|
28
|
+
console.log('Connected to Tunnel Server.');
|
|
29
|
+
ws.send(JSON.stringify({
|
|
30
|
+
type: 'register',
|
|
31
|
+
subdomain: requestedSubdomain
|
|
32
|
+
}));
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
ws.on('message', async (data) => {
|
|
36
|
+
try {
|
|
37
|
+
const message = JSON.parse(data);
|
|
38
|
+
|
|
39
|
+
if (message.type === 'registered') {
|
|
40
|
+
console.log(`\nTunnel Connected! 🚀`);
|
|
41
|
+
console.log(`Public URL: ${message.url}`);
|
|
42
|
+
console.log(`Forwarding to: http://localhost:${localPort}\n`);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
else if (message.type === 'request') {
|
|
46
|
+
const { requestId, method, path, headers, body } = message;
|
|
47
|
+
|
|
48
|
+
// Forward to local app
|
|
49
|
+
try {
|
|
50
|
+
// Filter headers (host header usually causes issues)
|
|
51
|
+
const { host, ...forwardHeaders } = headers;
|
|
52
|
+
|
|
53
|
+
const response = await axios({
|
|
54
|
+
method: method,
|
|
55
|
+
url: `http://localhost:${localPort}${path}`,
|
|
56
|
+
headers: forwardHeaders,
|
|
57
|
+
data: body ? Buffer.from(body, 'base64') : undefined,
|
|
58
|
+
validateStatus: () => true, // Accept all status codes
|
|
59
|
+
responseType: 'arraybuffer' // Get raw response
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
// Send response back
|
|
63
|
+
ws.send(JSON.stringify({
|
|
64
|
+
type: 'response',
|
|
65
|
+
requestId,
|
|
66
|
+
status: response.status,
|
|
67
|
+
headers: response.headers,
|
|
68
|
+
body: response.data.toString('base64')
|
|
69
|
+
}));
|
|
70
|
+
|
|
71
|
+
console.log(`[${method}] ${path} -> ${response.status}`);
|
|
72
|
+
|
|
73
|
+
} catch (err) {
|
|
74
|
+
console.error(`Error forwarding request: ${err.message}`);
|
|
75
|
+
ws.send(JSON.stringify({
|
|
76
|
+
type: 'response',
|
|
77
|
+
requestId,
|
|
78
|
+
status: 502,
|
|
79
|
+
body: Buffer.from('Bad Gateway: Local server Error').toString('base64')
|
|
80
|
+
}));
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
else if (message.type === 'error') {
|
|
85
|
+
console.error(`Server Error: ${message.message}`);
|
|
86
|
+
process.exit(1);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
} catch (e) {
|
|
90
|
+
console.error('Error parsing message:', e);
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
ws.on('close', () => {
|
|
95
|
+
console.log('Disconnected from server. Retrying in 3s...');
|
|
96
|
+
setTimeout(() => process.exit(1), 3000); // Simple restart for now
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
ws.on('error', (err) => {
|
|
100
|
+
console.error('Connection error:', err.message);
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
program.parse();
|
package/package.json
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@lifelessrasel/devtunnel-cli",
|
|
3
|
+
"publishConfig": {
|
|
4
|
+
"access": "public"
|
|
5
|
+
},
|
|
6
|
+
"version": "1.0.0",
|
|
7
|
+
"description": "A specialized tunneling tool to expose local servers to the internet via DevTunnel.net",
|
|
8
|
+
"main": "bin/devtunnel.js",
|
|
9
|
+
"scripts": {
|
|
10
|
+
"test": "echo \"Error: no test specified\" && exit 1"
|
|
11
|
+
},
|
|
12
|
+
"keywords": [
|
|
13
|
+
"tunnel",
|
|
14
|
+
"localhost",
|
|
15
|
+
"proxy",
|
|
16
|
+
"devtunnel",
|
|
17
|
+
"cli"
|
|
18
|
+
],
|
|
19
|
+
"author": "Mohammad Rasel",
|
|
20
|
+
"license": "ISC",
|
|
21
|
+
"type": "commonjs",
|
|
22
|
+
"bin": {
|
|
23
|
+
"dt": "bin/devtunnel.js",
|
|
24
|
+
"devtunnel": "bin/devtunnel.js"
|
|
25
|
+
},
|
|
26
|
+
"dependencies": {
|
|
27
|
+
"axios": "^1.13.2",
|
|
28
|
+
"commander": "^14.0.2",
|
|
29
|
+
"ws": "^8.19.0"
|
|
30
|
+
},
|
|
31
|
+
"repository": {
|
|
32
|
+
"type": "git",
|
|
33
|
+
"url": "git+https://github.com/lifelessrasel/devtunnel.git"
|
|
34
|
+
},
|
|
35
|
+
"engines": {
|
|
36
|
+
"node": ">=16.0.0"
|
|
37
|
+
}
|
|
38
|
+
}
|