webhookdrop-tunnel 1.0.1 → 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 +113 -72
- 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) => {
|
|
@@ -161,29 +157,51 @@ async function forwardRequest(data) {
|
|
|
161
157
|
});
|
|
162
158
|
}
|
|
163
159
|
|
|
164
|
-
//
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
160
|
+
// Connection pool
|
|
161
|
+
const connections = new Map();
|
|
162
|
+
let connectedCount = 0;
|
|
163
|
+
let totalRequests = 0;
|
|
164
|
+
let isShuttingDown = false;
|
|
169
165
|
|
|
170
|
-
|
|
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}`;
|
|
171
169
|
|
|
170
|
+
const ws = new WebSocket(wsUrl.replace('https://', 'wss://').replace('http://', 'ws://'));
|
|
172
171
|
let pingInterval;
|
|
172
|
+
let isConnected = false;
|
|
173
173
|
|
|
174
174
|
ws.on('open', () => {
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
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
|
+
}
|
|
180
196
|
|
|
181
|
-
// Send ping every
|
|
197
|
+
// Send ping every 10 seconds to keep connection alive
|
|
182
198
|
pingInterval = setInterval(() => {
|
|
183
199
|
if (ws.readyState === WebSocket.OPEN) {
|
|
184
200
|
ws.send(JSON.stringify({ type: 'ping' }));
|
|
185
201
|
}
|
|
186
|
-
},
|
|
202
|
+
}, PING_INTERVAL);
|
|
203
|
+
|
|
204
|
+
connections.set(connectionId, { ws, pingInterval, isConnected: true });
|
|
187
205
|
});
|
|
188
206
|
|
|
189
207
|
ws.on('message', async (data) => {
|
|
@@ -191,82 +209,105 @@ function connect() {
|
|
|
191
209
|
const message = JSON.parse(data.toString());
|
|
192
210
|
|
|
193
211
|
if (message.type === 'connected') {
|
|
194
|
-
// Initial connection confirmation
|
|
212
|
+
// Initial connection confirmation
|
|
195
213
|
return;
|
|
196
214
|
}
|
|
197
215
|
|
|
198
216
|
if (message.type === 'pong') {
|
|
199
|
-
//
|
|
217
|
+
// Heartbeat response - connection healthy
|
|
200
218
|
return;
|
|
201
219
|
}
|
|
202
220
|
|
|
203
221
|
if (message.type === 'webhook_request' || message.type === 'http_request') {
|
|
204
222
|
const requestId = message.request_id;
|
|
205
|
-
const requestData = message.data ||
|
|
206
|
-
const method = requestData.method || 'POST';
|
|
207
|
-
const path = requestData.path || '/';
|
|
223
|
+
const requestData = message.data || {};
|
|
208
224
|
|
|
209
|
-
|
|
225
|
+
totalRequests++;
|
|
226
|
+
console.log(`${timestamp()} ${chalk.blue('→')} ${requestData.method || 'POST'} ${requestData.path || '/'}`);
|
|
210
227
|
|
|
211
228
|
// Forward to local server
|
|
212
229
|
const response = await forwardRequest(requestData);
|
|
213
230
|
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
console.log(`${timestamp()} ${chalk.red('←')} ${response.status_code} ${response.error}`);
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
// Send response back to server
|
|
221
|
-
ws.send(JSON.stringify({
|
|
222
|
-
type: 'webhook_response',
|
|
231
|
+
// Send response back
|
|
232
|
+
const responseMessage = {
|
|
233
|
+
type: message.type === 'webhook_request' ? 'webhook_response' : 'http_response',
|
|
223
234
|
request_id: requestId,
|
|
224
235
|
response: response
|
|
225
|
-
}
|
|
226
|
-
}
|
|
236
|
+
};
|
|
227
237
|
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
238
|
+
if (ws.readyState === WebSocket.OPEN) {
|
|
239
|
+
ws.send(JSON.stringify(responseMessage));
|
|
240
|
+
}
|
|
231
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
|
+
}
|
|
232
245
|
} catch (e) {
|
|
233
|
-
console.error(`${timestamp()} ${chalk.red('Error
|
|
246
|
+
console.error(`${timestamp()} ${chalk.red('Error processing message:')} ${e.message}`);
|
|
234
247
|
}
|
|
235
248
|
});
|
|
236
249
|
|
|
237
|
-
ws.on('close', (
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
250
|
+
ws.on('close', () => {
|
|
251
|
+
if (isConnected) {
|
|
252
|
+
connectedCount--;
|
|
253
|
+
}
|
|
254
|
+
isConnected = false;
|
|
241
255
|
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
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
|
+
}
|
|
245
270
|
});
|
|
246
271
|
|
|
247
272
|
ws.on('error', (error) => {
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
if (error.message.includes('401')) {
|
|
251
|
-
console.error(`${timestamp()} ${chalk.red('❌ Authentication failed. Please check your token.')}`);
|
|
252
|
-
process.exit(1);
|
|
253
|
-
} else if (error.message.includes('404')) {
|
|
254
|
-
console.error(`${timestamp()} ${chalk.red('❌ Endpoint not found. Please check your endpoint ID.')}`);
|
|
255
|
-
process.exit(1);
|
|
256
|
-
} else {
|
|
273
|
+
// Suppress error logging for normal reconnection cases
|
|
274
|
+
if (!isShuttingDown && error.code !== 'ECONNREFUSED') {
|
|
257
275
|
console.error(`${timestamp()} ${chalk.red('Connection error:')} ${error.message}`);
|
|
258
|
-
console.log(`${timestamp()} Reconnecting in 5 seconds...`);
|
|
259
|
-
setTimeout(connect, 5000);
|
|
260
276
|
}
|
|
261
277
|
});
|
|
278
|
+
|
|
279
|
+
return ws;
|
|
262
280
|
}
|
|
263
281
|
|
|
264
|
-
//
|
|
265
|
-
|
|
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
|
+
|
|
266
296
|
console.log('');
|
|
267
|
-
console.log(`${timestamp()}
|
|
268
|
-
|
|
269
|
-
|
|
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
|
+
}
|
|
270
311
|
|
|
271
|
-
|
|
272
|
-
|
|
312
|
+
process.on('SIGINT', shutdown);
|
|
313
|
+
process.on('SIGTERM', shutdown);
|