webhookdrop-tunnel 1.0.0 → 1.0.3
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/bin/tunnel.js +123 -76
- package/package.json +1 -1
package/bin/tunnel.js
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
* WebhookDrop Tunnel Client
|
|
5
5
|
*
|
|
6
6
|
* Forwards webhooks from WebhookDrop to your local development server.
|
|
7
|
+
* Uses connection pooling for high-concurrency SPA tunneling.
|
|
7
8
|
*
|
|
8
9
|
* Usage:
|
|
9
10
|
* npx webhookdrop-tunnel --token YOUR_TOKEN --local http://localhost:3000
|
|
@@ -28,12 +29,15 @@ try {
|
|
|
28
29
|
blue: (s) => s,
|
|
29
30
|
gray: (s) => s,
|
|
30
31
|
bold: (s) => s,
|
|
31
|
-
cyan: (s) => s
|
|
32
|
+
cyan: (s) => s,
|
|
33
|
+
magenta: (s) => s
|
|
32
34
|
};
|
|
33
35
|
}
|
|
34
36
|
|
|
35
|
-
const VERSION = '1.0.
|
|
37
|
+
const VERSION = '1.0.3';
|
|
36
38
|
const DEFAULT_SERVER = 'wss://webhookdrop.app';
|
|
39
|
+
const POOL_SIZE = 20; // Number of WebSocket connections
|
|
40
|
+
const PING_INTERVAL = 10000; // 10 seconds
|
|
37
41
|
|
|
38
42
|
// Parse command line arguments
|
|
39
43
|
program
|
|
@@ -44,9 +48,11 @@ program
|
|
|
44
48
|
.requiredOption('-l, --local <url>', 'Local server URL to forward webhooks to (e.g., http://localhost:3000)')
|
|
45
49
|
.option('-e, --endpoint <id>', 'Endpoint ID (usually embedded in token)')
|
|
46
50
|
.option('-s, --server <url>', 'WebhookDrop server URL', DEFAULT_SERVER)
|
|
51
|
+
.option('-p, --pool <size>', 'Connection pool size', String(POOL_SIZE))
|
|
47
52
|
.parse(process.argv);
|
|
48
53
|
|
|
49
54
|
const options = program.opts();
|
|
55
|
+
const poolSize = parseInt(options.pool) || POOL_SIZE;
|
|
50
56
|
|
|
51
57
|
// Validate local URL
|
|
52
58
|
let localUrl;
|
|
@@ -75,21 +81,11 @@ if (!endpointId) {
|
|
|
75
81
|
process.exit(1);
|
|
76
82
|
}
|
|
77
83
|
|
|
78
|
-
//
|
|
79
|
-
const wsUrl = `${options.server}/ws/tunnel/${endpointId}?token=${encodeURIComponent(options.token)}&local_url=${encodeURIComponent(options.local)}`;
|
|
80
|
-
|
|
81
|
-
// Format timestamp
|
|
84
|
+
// Timestamp helper
|
|
82
85
|
function timestamp() {
|
|
83
86
|
return chalk.gray(new Date().toLocaleTimeString());
|
|
84
87
|
}
|
|
85
88
|
|
|
86
|
-
// Print banner
|
|
87
|
-
console.log('');
|
|
88
|
-
console.log(chalk.cyan('╔══════════════════════════════════════════╗'));
|
|
89
|
-
console.log(chalk.cyan('║') + chalk.bold(' WebhookDrop Tunnel Client ') + chalk.cyan('║'));
|
|
90
|
-
console.log(chalk.cyan('╚══════════════════════════════════════════╝'));
|
|
91
|
-
console.log('');
|
|
92
|
-
|
|
93
89
|
// Forward HTTP request to local server
|
|
94
90
|
async function forwardRequest(data) {
|
|
95
91
|
return new Promise((resolve) => {
|
|
@@ -113,20 +109,26 @@ async function forwardRequest(data) {
|
|
|
113
109
|
method: method,
|
|
114
110
|
headers: {
|
|
115
111
|
...headers,
|
|
116
|
-
'host': targetUrl.host
|
|
112
|
+
'host': targetUrl.host,
|
|
113
|
+
// Request uncompressed content to avoid encoding issues
|
|
114
|
+
'accept-encoding': 'identity'
|
|
117
115
|
},
|
|
118
116
|
timeout: 30000
|
|
119
117
|
};
|
|
120
118
|
|
|
121
119
|
const req = httpModule.request(requestOptions, (res) => {
|
|
122
|
-
|
|
123
|
-
|
|
120
|
+
// Collect response as Buffer chunks to handle binary data correctly
|
|
121
|
+
const chunks = [];
|
|
122
|
+
res.on('data', (chunk) => chunks.push(chunk));
|
|
124
123
|
res.on('end', () => {
|
|
124
|
+
const buffer = Buffer.concat(chunks);
|
|
125
125
|
resolve({
|
|
126
126
|
success: true,
|
|
127
127
|
status_code: res.statusCode,
|
|
128
128
|
headers: res.headers,
|
|
129
|
-
|
|
129
|
+
// Send as base64 to preserve binary data
|
|
130
|
+
body: buffer.toString('base64'),
|
|
131
|
+
body_encoding: 'base64'
|
|
130
132
|
});
|
|
131
133
|
});
|
|
132
134
|
});
|
|
@@ -155,29 +157,51 @@ async function forwardRequest(data) {
|
|
|
155
157
|
});
|
|
156
158
|
}
|
|
157
159
|
|
|
158
|
-
//
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
160
|
+
// Connection pool
|
|
161
|
+
const connections = new Map();
|
|
162
|
+
let connectedCount = 0;
|
|
163
|
+
let totalRequests = 0;
|
|
164
|
+
let isShuttingDown = false;
|
|
163
165
|
|
|
164
|
-
|
|
166
|
+
// Create a single WebSocket connection
|
|
167
|
+
function createConnection(connectionId) {
|
|
168
|
+
const wsUrl = `${options.server}/ws/tunnel/${endpointId}?token=${encodeURIComponent(options.token)}&local_url=${encodeURIComponent(options.local)}&connection_id=${connectionId}`;
|
|
165
169
|
|
|
170
|
+
const ws = new WebSocket(wsUrl.replace('https://', 'wss://').replace('http://', 'ws://'));
|
|
166
171
|
let pingInterval;
|
|
172
|
+
let isConnected = false;
|
|
167
173
|
|
|
168
174
|
ws.on('open', () => {
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
175
|
+
isConnected = true;
|
|
176
|
+
connectedCount++;
|
|
177
|
+
|
|
178
|
+
if (connectedCount === 1) {
|
|
179
|
+
// First connection - show banner
|
|
180
|
+
console.log('');
|
|
181
|
+
console.log(chalk.cyan('╔══════════════════════════════════════════════════╗'));
|
|
182
|
+
console.log(chalk.cyan('║') + chalk.bold(' WebhookDrop Tunnel Client (Pool Mode) ') + chalk.cyan('║'));
|
|
183
|
+
console.log(chalk.cyan('╚══════════════════════════════════════════════════╝'));
|
|
184
|
+
console.log('');
|
|
185
|
+
console.log(`${timestamp()} Forwarding to: ${chalk.cyan(options.local)}`);
|
|
186
|
+
console.log('');
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (connectedCount === poolSize) {
|
|
190
|
+
console.log(`${timestamp()} ${chalk.green('✓')} ${chalk.bold(`All ${poolSize} connections established!`)}`);
|
|
191
|
+
console.log(`${timestamp()} Endpoint: ${chalk.cyan(endpointId.substring(0, 8))}...`);
|
|
192
|
+
console.log('');
|
|
193
|
+
console.log(`${timestamp()} ${chalk.yellow('Waiting for requests...')} (Press Ctrl+C to stop)`);
|
|
194
|
+
console.log('');
|
|
195
|
+
}
|
|
174
196
|
|
|
175
|
-
// Send ping every
|
|
197
|
+
// Send ping every 10 seconds to keep connection alive
|
|
176
198
|
pingInterval = setInterval(() => {
|
|
177
199
|
if (ws.readyState === WebSocket.OPEN) {
|
|
178
200
|
ws.send(JSON.stringify({ type: 'ping' }));
|
|
179
201
|
}
|
|
180
|
-
},
|
|
202
|
+
}, PING_INTERVAL);
|
|
203
|
+
|
|
204
|
+
connections.set(connectionId, { ws, pingInterval, isConnected: true });
|
|
181
205
|
});
|
|
182
206
|
|
|
183
207
|
ws.on('message', async (data) => {
|
|
@@ -185,82 +209,105 @@ function connect() {
|
|
|
185
209
|
const message = JSON.parse(data.toString());
|
|
186
210
|
|
|
187
211
|
if (message.type === 'connected') {
|
|
188
|
-
// Initial connection confirmation
|
|
212
|
+
// Initial connection confirmation
|
|
189
213
|
return;
|
|
190
214
|
}
|
|
191
215
|
|
|
192
216
|
if (message.type === 'pong') {
|
|
193
|
-
//
|
|
217
|
+
// Heartbeat response - connection healthy
|
|
194
218
|
return;
|
|
195
219
|
}
|
|
196
220
|
|
|
197
221
|
if (message.type === 'webhook_request' || message.type === 'http_request') {
|
|
198
222
|
const requestId = message.request_id;
|
|
199
|
-
const requestData = message.data ||
|
|
200
|
-
const method = requestData.method || 'POST';
|
|
201
|
-
const path = requestData.path || '/';
|
|
223
|
+
const requestData = message.data || {};
|
|
202
224
|
|
|
203
|
-
|
|
225
|
+
totalRequests++;
|
|
226
|
+
console.log(`${timestamp()} ${chalk.blue('→')} ${requestData.method || 'POST'} ${requestData.path || '/'}`);
|
|
204
227
|
|
|
205
228
|
// Forward to local server
|
|
206
229
|
const response = await forwardRequest(requestData);
|
|
207
230
|
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
console.log(`${timestamp()} ${chalk.red('←')} ${response.status_code} ${response.error}`);
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
// Send response back to server
|
|
215
|
-
ws.send(JSON.stringify({
|
|
216
|
-
type: 'webhook_response',
|
|
231
|
+
// Send response back
|
|
232
|
+
const responseMessage = {
|
|
233
|
+
type: message.type === 'webhook_request' ? 'webhook_response' : 'http_response',
|
|
217
234
|
request_id: requestId,
|
|
218
235
|
response: response
|
|
219
|
-
}
|
|
220
|
-
}
|
|
236
|
+
};
|
|
221
237
|
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
238
|
+
if (ws.readyState === WebSocket.OPEN) {
|
|
239
|
+
ws.send(JSON.stringify(responseMessage));
|
|
240
|
+
}
|
|
225
241
|
|
|
242
|
+
const statusColor = response.success && response.status_code < 400 ? chalk.green : chalk.red;
|
|
243
|
+
console.log(`${timestamp()} ${statusColor('←')} ${response.status_code || 502} ${response.success ? 'OK' : response.error || 'Error'}`);
|
|
244
|
+
}
|
|
226
245
|
} catch (e) {
|
|
227
|
-
console.error(`${timestamp()} ${chalk.red('Error
|
|
246
|
+
console.error(`${timestamp()} ${chalk.red('Error processing message:')} ${e.message}`);
|
|
228
247
|
}
|
|
229
248
|
});
|
|
230
249
|
|
|
231
|
-
ws.on('close', (
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
250
|
+
ws.on('close', () => {
|
|
251
|
+
if (isConnected) {
|
|
252
|
+
connectedCount--;
|
|
253
|
+
}
|
|
254
|
+
isConnected = false;
|
|
235
255
|
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
256
|
+
if (pingInterval) {
|
|
257
|
+
clearInterval(pingInterval);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
connections.delete(connectionId);
|
|
261
|
+
|
|
262
|
+
if (!isShuttingDown) {
|
|
263
|
+
// Reconnect after delay
|
|
264
|
+
setTimeout(() => {
|
|
265
|
+
if (!isShuttingDown) {
|
|
266
|
+
createConnection(connectionId);
|
|
267
|
+
}
|
|
268
|
+
}, 2000);
|
|
269
|
+
}
|
|
239
270
|
});
|
|
240
271
|
|
|
241
272
|
ws.on('error', (error) => {
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
if (error.message.includes('401')) {
|
|
245
|
-
console.error(`${timestamp()} ${chalk.red('❌ Authentication failed. Please check your token.')}`);
|
|
246
|
-
process.exit(1);
|
|
247
|
-
} else if (error.message.includes('404')) {
|
|
248
|
-
console.error(`${timestamp()} ${chalk.red('❌ Endpoint not found. Please check your endpoint ID.')}`);
|
|
249
|
-
process.exit(1);
|
|
250
|
-
} else {
|
|
273
|
+
// Suppress error logging for normal reconnection cases
|
|
274
|
+
if (!isShuttingDown && error.code !== 'ECONNREFUSED') {
|
|
251
275
|
console.error(`${timestamp()} ${chalk.red('Connection error:')} ${error.message}`);
|
|
252
|
-
console.log(`${timestamp()} Reconnecting in 5 seconds...`);
|
|
253
|
-
setTimeout(connect, 5000);
|
|
254
276
|
}
|
|
255
277
|
});
|
|
278
|
+
|
|
279
|
+
return ws;
|
|
256
280
|
}
|
|
257
281
|
|
|
258
|
-
//
|
|
259
|
-
|
|
282
|
+
// Start connection pool
|
|
283
|
+
console.log('');
|
|
284
|
+
console.log(`${timestamp()} Starting connection pool (${poolSize} connections)...`);
|
|
285
|
+
|
|
286
|
+
for (let i = 0; i < poolSize; i++) {
|
|
287
|
+
const connectionId = `conn-${i.toString().padStart(2, '0')}`;
|
|
288
|
+
setTimeout(() => createConnection(connectionId), i * 100); // Stagger connections
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// Handle shutdown
|
|
292
|
+
function shutdown() {
|
|
293
|
+
if (isShuttingDown) return;
|
|
294
|
+
isShuttingDown = true;
|
|
295
|
+
|
|
260
296
|
console.log('');
|
|
261
|
-
console.log(`${timestamp()}
|
|
262
|
-
|
|
263
|
-
|
|
297
|
+
console.log(`${timestamp()} Shutting down tunnel...`);
|
|
298
|
+
console.log(`${timestamp()} Total requests handled: ${totalRequests}`);
|
|
299
|
+
|
|
300
|
+
for (const [id, conn] of connections) {
|
|
301
|
+
if (conn.pingInterval) {
|
|
302
|
+
clearInterval(conn.pingInterval);
|
|
303
|
+
}
|
|
304
|
+
if (conn.ws && conn.ws.readyState === WebSocket.OPEN) {
|
|
305
|
+
conn.ws.close();
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
setTimeout(() => process.exit(0), 1000);
|
|
310
|
+
}
|
|
264
311
|
|
|
265
|
-
|
|
266
|
-
|
|
312
|
+
process.on('SIGINT', shutdown);
|
|
313
|
+
process.on('SIGTERM', shutdown);
|