api-response-manager 2.6.0 ā 2.6.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/README.md +1 -1
- package/commands/serve.js +156 -104
- package/package.json +1 -1
package/README.md
CHANGED
package/commands/serve.js
CHANGED
|
@@ -443,125 +443,177 @@ async function connectFileTunnel(tunnelId, subdomain, localPort) {
|
|
|
443
443
|
const token = api.getToken();
|
|
444
444
|
const userId = config.get('userId');
|
|
445
445
|
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
ws.on('open', () => {
|
|
449
|
-
console.log(chalk.green('ā Connected to tunnel server'));
|
|
450
|
-
|
|
451
|
-
ws.send(JSON.stringify({
|
|
452
|
-
type: 'register',
|
|
453
|
-
tunnelId,
|
|
454
|
-
subdomain,
|
|
455
|
-
localPort,
|
|
456
|
-
protocol: 'http',
|
|
457
|
-
authToken: token,
|
|
458
|
-
userId
|
|
459
|
-
}));
|
|
460
|
-
|
|
461
|
-
// Heartbeat
|
|
462
|
-
setInterval(() => {
|
|
463
|
-
if (ws.readyState === WebSocket.OPEN) {
|
|
464
|
-
ws.send(JSON.stringify({ type: 'heartbeat', tunnelId, subdomain }));
|
|
465
|
-
}
|
|
466
|
-
}, 30000);
|
|
467
|
-
});
|
|
468
|
-
|
|
469
|
-
// Track total bytes transferred
|
|
446
|
+
// Track total bytes transferred (persists across reconnects)
|
|
470
447
|
let totalBytesTransferred = 0;
|
|
471
448
|
let limitReached = false;
|
|
449
|
+
let heartbeatInterval = null;
|
|
450
|
+
let reconnectAttempts = 0;
|
|
451
|
+
const maxReconnectAttempts = 50; // Allow many reconnects
|
|
452
|
+
let isShuttingDown = false;
|
|
472
453
|
|
|
473
|
-
|
|
474
|
-
|
|
454
|
+
function connect() {
|
|
455
|
+
if (isShuttingDown) return;
|
|
475
456
|
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
457
|
+
const ws = new WebSocket(tunnelServerUrl, {
|
|
458
|
+
// Keep connection alive with WebSocket-level ping/pong
|
|
459
|
+
perMessageDeflate: false
|
|
460
|
+
});
|
|
461
|
+
|
|
462
|
+
ws.on('open', () => {
|
|
463
|
+
reconnectAttempts = 0; // Reset on successful connection
|
|
464
|
+
console.log(chalk.green('ā Connected to tunnel server'));
|
|
482
465
|
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
return;
|
|
466
|
+
ws.send(JSON.stringify({
|
|
467
|
+
type: 'register',
|
|
468
|
+
tunnelId,
|
|
469
|
+
subdomain,
|
|
470
|
+
localPort,
|
|
471
|
+
protocol: 'http',
|
|
472
|
+
authToken: token,
|
|
473
|
+
userId,
|
|
474
|
+
persistent: true // File serving tunnels should not have idle timeout
|
|
475
|
+
}));
|
|
476
|
+
|
|
477
|
+
// Clear any existing heartbeat
|
|
478
|
+
if (heartbeatInterval) {
|
|
479
|
+
clearInterval(heartbeatInterval);
|
|
498
480
|
}
|
|
499
481
|
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
482
|
+
// Heartbeat every 10 seconds to keep connection alive during large transfers
|
|
483
|
+
heartbeatInterval = setInterval(() => {
|
|
484
|
+
if (ws.readyState === WebSocket.OPEN) {
|
|
485
|
+
ws.send(JSON.stringify({ type: 'heartbeat', tunnelId, subdomain }));
|
|
486
|
+
}
|
|
487
|
+
}, 10000);
|
|
488
|
+
});
|
|
489
|
+
|
|
490
|
+
// Respond to WebSocket-level ping from server
|
|
491
|
+
ws.on('ping', () => {
|
|
492
|
+
ws.pong();
|
|
493
|
+
});
|
|
494
|
+
|
|
495
|
+
ws.on('message', async (data) => {
|
|
496
|
+
const message = JSON.parse(data.toString());
|
|
497
|
+
|
|
498
|
+
if (message.type === 'registered') {
|
|
499
|
+
console.log(chalk.green.bold('\nš File Server is now publicly accessible!\n'));
|
|
500
|
+
console.log(chalk.white('Share this URL:'));
|
|
501
|
+
console.log(chalk.cyan.bold(` ${message.publicUrl}\n`));
|
|
502
|
+
} else if (message.type === 'request') {
|
|
503
|
+
const timestamp = new Date().toLocaleTimeString();
|
|
504
|
+
|
|
505
|
+
// Check if limit was reached
|
|
506
|
+
if (limitReached) {
|
|
507
|
+
console.log(chalk.red(`[${timestamp}]`), chalk.red('BLOCKED'), chalk.white(message.path), chalk.red('- Transfer limit reached'));
|
|
508
|
+
ws.send(JSON.stringify({
|
|
509
|
+
type: 'response',
|
|
510
|
+
requestId: message.requestId,
|
|
511
|
+
statusCode: 429,
|
|
512
|
+
headers: { 'content-type': 'application/json' },
|
|
513
|
+
body: Buffer.from(JSON.stringify({
|
|
514
|
+
error: 'Monthly file transfer limit reached',
|
|
515
|
+
message: 'Upgrade your plan at https://tunnelapi.in/pricing for more transfer quota.'
|
|
516
|
+
})).toString('base64'),
|
|
517
|
+
encoding: 'base64'
|
|
518
|
+
}));
|
|
519
|
+
return;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
console.log(chalk.gray(`[${timestamp}]`), chalk.blue(message.method), chalk.white(message.path));
|
|
504
523
|
|
|
505
|
-
const response = await axios({
|
|
506
|
-
method: message.method.toLowerCase(),
|
|
507
|
-
url: localUrl,
|
|
508
|
-
headers: { ...message.headers, host: `localhost:${localPort}` },
|
|
509
|
-
data: message.body,
|
|
510
|
-
validateStatus: () => true,
|
|
511
|
-
responseType: 'arraybuffer',
|
|
512
|
-
maxRedirects: 0,
|
|
513
|
-
timeout: 30000
|
|
514
|
-
});
|
|
515
|
-
|
|
516
|
-
const cleanHeaders = { ...response.headers };
|
|
517
|
-
delete cleanHeaders['transfer-encoding'];
|
|
518
|
-
delete cleanHeaders['connection'];
|
|
519
|
-
|
|
520
|
-
const responseData = Buffer.from(response.data);
|
|
521
|
-
const bodyBase64 = responseData.toString('base64');
|
|
522
|
-
const bytesTransferred = responseData.length;
|
|
523
|
-
|
|
524
|
-
// Track file transfer usage
|
|
525
524
|
try {
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
525
|
+
const localUrl = `http://localhost:${localPort}${message.path}`;
|
|
526
|
+
|
|
527
|
+
const response = await axios({
|
|
528
|
+
method: message.method.toLowerCase(),
|
|
529
|
+
url: localUrl,
|
|
530
|
+
headers: { ...message.headers, host: `localhost:${localPort}` },
|
|
531
|
+
data: message.body,
|
|
532
|
+
validateStatus: () => true,
|
|
533
|
+
responseType: 'arraybuffer',
|
|
534
|
+
maxRedirects: 0,
|
|
535
|
+
timeout: 0 // No timeout for large file transfers
|
|
536
|
+
});
|
|
537
|
+
|
|
538
|
+
const cleanHeaders = { ...response.headers };
|
|
539
|
+
delete cleanHeaders['transfer-encoding'];
|
|
540
|
+
delete cleanHeaders['connection'];
|
|
541
|
+
|
|
542
|
+
const responseData = Buffer.from(response.data);
|
|
543
|
+
const bodyBase64 = responseData.toString('base64');
|
|
544
|
+
const bytesTransferred = responseData.length;
|
|
545
|
+
|
|
546
|
+
// Track file transfer usage
|
|
547
|
+
try {
|
|
548
|
+
await api.trackFileTransfer(userId, bytesTransferred);
|
|
549
|
+
totalBytesTransferred += bytesTransferred;
|
|
550
|
+
} catch (trackError) {
|
|
551
|
+
if (trackError.response?.data?.limitReached) {
|
|
552
|
+
limitReached = true;
|
|
553
|
+
console.log(chalk.red.bold('\nā ļø Monthly file transfer limit reached!'));
|
|
554
|
+
console.log(chalk.yellow('š” Upgrade your plan at https://tunnelapi.in/pricing for more transfer quota.\n'));
|
|
555
|
+
}
|
|
533
556
|
}
|
|
557
|
+
|
|
558
|
+
ws.send(JSON.stringify({
|
|
559
|
+
type: 'response',
|
|
560
|
+
requestId: message.requestId,
|
|
561
|
+
statusCode: response.status,
|
|
562
|
+
headers: cleanHeaders,
|
|
563
|
+
body: bodyBase64,
|
|
564
|
+
encoding: 'base64'
|
|
565
|
+
}));
|
|
566
|
+
} catch (error) {
|
|
567
|
+
console.error(chalk.red(`Error: ${error.message}`));
|
|
568
|
+
ws.send(JSON.stringify({
|
|
569
|
+
type: 'response',
|
|
570
|
+
requestId: message.requestId,
|
|
571
|
+
statusCode: 502,
|
|
572
|
+
headers: { 'content-type': 'application/json' },
|
|
573
|
+
body: Buffer.from(JSON.stringify({ error: 'File server error' })).toString('base64'),
|
|
574
|
+
encoding: 'base64'
|
|
575
|
+
}));
|
|
534
576
|
}
|
|
577
|
+
}
|
|
578
|
+
});
|
|
535
579
|
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
} catch (error) {
|
|
545
|
-
console.error(chalk.red(`Error: ${error.message}`));
|
|
546
|
-
ws.send(JSON.stringify({
|
|
547
|
-
type: 'response',
|
|
548
|
-
requestId: message.requestId,
|
|
549
|
-
statusCode: 502,
|
|
550
|
-
headers: { 'content-type': 'application/json' },
|
|
551
|
-
body: Buffer.from(JSON.stringify({ error: 'File server error' })).toString('base64'),
|
|
552
|
-
encoding: 'base64'
|
|
553
|
-
}));
|
|
580
|
+
ws.on('error', (error) => {
|
|
581
|
+
console.error(chalk.red(`WebSocket error: ${error.message}`));
|
|
582
|
+
});
|
|
583
|
+
|
|
584
|
+
ws.on('close', (code, reason) => {
|
|
585
|
+
if (heartbeatInterval) {
|
|
586
|
+
clearInterval(heartbeatInterval);
|
|
587
|
+
heartbeatInterval = null;
|
|
554
588
|
}
|
|
555
|
-
}
|
|
556
|
-
});
|
|
557
589
|
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
590
|
+
if (isShuttingDown) {
|
|
591
|
+
console.log(chalk.yellow('Tunnel connection closed'));
|
|
592
|
+
return;
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
reconnectAttempts++;
|
|
596
|
+
if (reconnectAttempts <= maxReconnectAttempts) {
|
|
597
|
+
const delay = Math.min(1000 * Math.pow(2, reconnectAttempts - 1), 30000); // Exponential backoff, max 30s
|
|
598
|
+
console.log(chalk.yellow(`Connection lost. Reconnecting in ${delay / 1000}s... (attempt ${reconnectAttempts}/${maxReconnectAttempts})`));
|
|
599
|
+
setTimeout(connect, delay);
|
|
600
|
+
} else {
|
|
601
|
+
console.log(chalk.red('Max reconnection attempts reached. Please restart the serve command.'));
|
|
602
|
+
}
|
|
603
|
+
});
|
|
604
|
+
|
|
605
|
+
// Handle process shutdown
|
|
606
|
+
process.on('SIGINT', () => {
|
|
607
|
+
isShuttingDown = true;
|
|
608
|
+
if (heartbeatInterval) {
|
|
609
|
+
clearInterval(heartbeatInterval);
|
|
610
|
+
}
|
|
611
|
+
ws.close();
|
|
612
|
+
});
|
|
613
|
+
}
|
|
561
614
|
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
});
|
|
615
|
+
// Start initial connection
|
|
616
|
+
connect();
|
|
565
617
|
}
|
|
566
618
|
|
|
567
619
|
module.exports = {
|