Skip to content

Commit a327ab7

Browse files
committed
added session timeout
1 parent fe28b8b commit a327ab7

4 files changed

Lines changed: 277 additions & 16 deletions

File tree

.env.example

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
# ProxyHub Configuration
2+
3+
# Server Configuration
4+
CONNECTION_TIMEOUT_MINUTES=30 # Server-side connection timeout in minutes (0 to disable)
5+
SOCKET_URL=https://connect.proxyhub.cloud
6+
SOCKET_PATH=/socket.io
7+
BASE_DOMAIN=proxyhub.cloud
8+
PROTOCOL=https
9+
10+
# Pricing Plan Examples:
11+
# Free tier: CONNECTION_TIMEOUT_MINUTES=5 # 5 minute sessions
12+
# Basic tier: CONNECTION_TIMEOUT_MINUTES=30 # 30 minute sessions
13+
# Pro tier: CONNECTION_TIMEOUT_MINUTES=120 # 2 hour sessions
14+
# Premium tier: CONNECTION_TIMEOUT_MINUTES=0 # Unlimited sessions

packages/client/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,8 @@
1010
"scripts": {
1111
"build": "tsc",
1212
"start": "node dist/index.js",
13-
"dev": "nodemon --exec \"tsx src/index.ts\" -- -p 3000 ",
14-
"dev:custom": "nodemon --exec \"tsx src/index.ts\" --",
13+
"dev": "tsx src/index.ts",
14+
"dev:watch": "nodemon --exec \"tsx src/index.ts\" --",
1515
"test": "tsx src/index.ts -p 8080 -d --keep-history",
1616
"example-server": "node example-server.js",
1717
"prepublish": "npm run build"

packages/client/src/lib/socket.ts

Lines changed: 214 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,36 @@ import * as http from "http";
55
import ResponseChannel from "./tunnel.js";
66
import * as crypto from "crypto";
77

8+
// Global variable to store timeout interval
9+
let timeoutInterval: NodeJS.Timeout | null = null;
10+
11+
const formatTimeRemaining = (remainingMs: number): string => {
12+
if (remainingMs <= 0) return '0s';
13+
14+
const totalSeconds = Math.floor(remainingMs / 1000);
15+
const hours = Math.floor(totalSeconds / 3600);
16+
const minutes = Math.floor((totalSeconds % 3600) / 60);
17+
const seconds = totalSeconds % 60;
18+
19+
if (hours > 0) {
20+
return `${hours}h ${minutes}m`;
21+
} else if (minutes > 0) {
22+
return `${minutes}m ${seconds}s`;
23+
} else {
24+
return `${seconds}s`;
25+
}
26+
};
27+
28+
const formatExpirationTime = (sessionStartTime: number, durationMs: number): string => {
29+
const expirationTime = new Date(sessionStartTime + durationMs);
30+
return expirationTime.toLocaleTimeString('en-US', {
31+
hour12: true,
32+
hour: 'numeric',
33+
minute: '2-digit',
34+
second: '2-digit'
35+
});
36+
};
37+
838
const displayTunnelInfo = (data: any) => {
939
const line = '─'.repeat(80);
1040
console.log(chalk.cyan(line));
@@ -25,11 +55,131 @@ const displayTunnelInfo = (data: any) => {
2555
console.log(formatLine('Session Status', data.status || 'online', chalk.green));
2656
console.log(formatLine('Version', data.version || '1.0.0', chalk.white));
2757
console.log(formatLine('Forwarding', `${data.tunnelUrl || 'N/A'} -> http://localhost:${data.port || 'N/A'}`, chalk.cyan));
58+
59+
// Display timeout information if available
60+
if (data.timeout) {
61+
const timeout = data.timeout;
62+
if (timeout.enabled) {
63+
const timeoutDisplay = `${timeout.minutes} minutes (enabled)`;
64+
console.log(formatLine('Session Timeout', timeoutDisplay, chalk.yellow));
65+
66+
// Show actual expiration time
67+
const expirationTime = formatExpirationTime(timeout.sessionStartTime, timeout.durationMs);
68+
const expirationDisplay = `🕒 ${expirationTime}`;
69+
console.log(formatLine('Session Expires', expirationDisplay, chalk.cyan));
70+
71+
// Start top-of-terminal countdown
72+
startTopCountdown(timeout);
73+
} else {
74+
console.log(formatLine('Session Timeout', 'Unlimited session', chalk.green));
75+
}
76+
}
77+
2878
console.log();
2979
console.log(chalk.cyan(line));
3080
console.log();
3181
};
3282

83+
let lastWarningTime = 0;
84+
85+
const startTopCountdown = (timeoutConfig: any) => {
86+
// Clear any existing interval
87+
if (timeoutInterval) {
88+
clearInterval(timeoutInterval);
89+
}
90+
91+
// Store original cursor position (if possible)
92+
const saveCursor = () => process.stdout.write('\u001b[s');
93+
const restoreCursor = () => process.stdout.write('\u001b[u');
94+
const moveToTop = () => process.stdout.write('\u001b[1;1H');
95+
const clearLine = () => process.stdout.write('\u001b[2K');
96+
97+
// Try to use top-of-terminal display, fallback to warnings
98+
let useTopDisplay = true;
99+
100+
timeoutInterval = setInterval(() => {
101+
const elapsed = Date.now() - timeoutConfig.sessionStartTime;
102+
const remaining = timeoutConfig.durationMs - elapsed;
103+
104+
if (remaining <= 0) {
105+
// Time expired, clear interval
106+
if (timeoutInterval) {
107+
clearInterval(timeoutInterval);
108+
timeoutInterval = null;
109+
}
110+
111+
if (useTopDisplay) {
112+
// Clear top line
113+
moveToTop();
114+
clearLine();
115+
restoreCursor();
116+
}
117+
118+
// Show final expiration message
119+
console.log(chalk.red.bold('\n🔴 Session expired - Connection will be terminated'));
120+
return;
121+
}
122+
123+
const remainingMs = remaining;
124+
const shouldShowWarning =
125+
remainingMs <= 10 * 60 * 1000 && // Less than 10 minutes
126+
(Date.now() - lastWarningTime) > 60000; // At least 1 minute since last warning
127+
128+
if (useTopDisplay) {
129+
try {
130+
// Try top-of-terminal display
131+
saveCursor();
132+
moveToTop();
133+
clearLine();
134+
135+
const expirationTime = formatExpirationTime(timeoutConfig.sessionStartTime, timeoutConfig.durationMs);
136+
const remainingDisplay = formatTimeRemaining(remaining);
137+
let color = chalk.cyan;
138+
let icon = '🕒';
139+
140+
if (remaining < 2 * 60 * 1000) { // Less than 2 minutes
141+
color = chalk.red;
142+
icon = '🔴';
143+
} else if (remaining < 5 * 60 * 1000) { // Less than 5 minutes
144+
color = chalk.red;
145+
icon = '⚠️';
146+
} else if (remaining < 10 * 60 * 1000) { // Less than 10 minutes
147+
color = chalk.yellow;
148+
icon = '⚠️';
149+
}
150+
151+
process.stdout.write(color(`${icon} Session expires at: ${expirationTime} (${remainingDisplay} left)`));
152+
restoreCursor();
153+
} catch (error) {
154+
// Fallback to warning system if top display fails
155+
useTopDisplay = false;
156+
}
157+
}
158+
159+
// Warning system (either as fallback or when time is critical)
160+
if (!useTopDisplay && shouldShowWarning) {
161+
showTimeoutWarning(remaining, timeoutConfig);
162+
lastWarningTime = Date.now();
163+
}
164+
165+
}, useTopDisplay ? 30000 : 60000); // 30s for top display, 60s for warnings
166+
};
167+
168+
const showTimeoutWarning = (remainingMs: number, timeoutConfig: any) => {
169+
const remaining = formatTimeRemaining(remainingMs);
170+
const expirationTime = formatExpirationTime(timeoutConfig.sessionStartTime, timeoutConfig.durationMs);
171+
172+
if (remainingMs <= 60 * 1000) { // 1 minute
173+
console.log(chalk.red.bold(`\n🔴 Session expires at ${expirationTime} (${remaining} left) - Connection will be terminated soon!`));
174+
} else if (remainingMs <= 2 * 60 * 1000) { // 2 minutes
175+
console.log(chalk.red.bold(`\n🔴 Session expires at ${expirationTime} (${remaining} left)`));
176+
} else if (remainingMs <= 5 * 60 * 1000) { // 5 minutes
177+
console.log(chalk.red(`\n⚠️ Session expires at ${expirationTime} (${remaining} left)`));
178+
} else if (remainingMs <= 10 * 60 * 1000) { // 10 minutes
179+
console.log(chalk.yellow(`\n⚠️ Session expires at ${expirationTime} (${remaining} left)`));
180+
}
181+
};
182+
33183
/**
34184
* Generate a stable tunnel ID that persists across reconnections
35185
* This ID is based on the user's machine and port to ensure consistency
@@ -76,7 +226,7 @@ const socketHandler = (option: ClientInitializationOptions) => {
76226
reconnection: true,
77227
reconnectionAttempts: 3,
78228
reconnectionDelay: 500,
79-
timeout: 10000,
229+
timeout: 10000, // 10 second connection timeout
80230
autoConnect: true,
81231
forceNew: true,
82232
protocols: ["websocket"],
@@ -154,6 +304,25 @@ const socketHandler = (option: ClientInitializationOptions) => {
154304

155305
// Handle disconnection
156306
socket.on("disconnect", (reason) => {
307+
// Clear timeout interval and top display on disconnect
308+
if (timeoutInterval) {
309+
clearInterval(timeoutInterval);
310+
timeoutInterval = null;
311+
}
312+
313+
// Clear top line if it was being used
314+
try {
315+
const moveToTop = () => process.stdout.write('\u001b[1;1H');
316+
const clearLine = () => process.stdout.write('\u001b[2K');
317+
const restoreCursor = () => process.stdout.write('\u001b[u');
318+
319+
moveToTop();
320+
clearLine();
321+
restoreCursor();
322+
} catch (error) {
323+
// Ignore cleanup errors
324+
}
325+
157326
console.log(chalk.red.bold("🔌 Disconnected from ProxyHub server"), reason);
158327
if (option.debug) {
159328
printDebug("Disconnect reason", reason);
@@ -202,7 +371,7 @@ const socketHandler = (option: ClientInitializationOptions) => {
202371
keepAliveMsecs: 1000,
203372
maxSockets: 100,
204373
}),
205-
timeout: 5000,
374+
timeout: 5000, // 5 second timeout for local requests
206375
});
207376

208377
// Prepare request body
@@ -211,6 +380,15 @@ const socketHandler = (option: ClientInitializationOptions) => {
211380
? JSON.stringify(request.body)
212381
: request.body;
213382

383+
// Handle request timeout
384+
proxyRequest.on("timeout", () => {
385+
printError(`Request timeout after 5 seconds`);
386+
proxyRequest.destroy();
387+
if (option.debug) {
388+
printDebug("Request timeout", { timeout: 5, path: request.path });
389+
}
390+
});
391+
214392
// Handle request errors
215393
proxyRequest.once("error", (error: Error) => {
216394
printError(`Request failed: ${error.message}`);
@@ -274,12 +452,46 @@ const socketHandler = (option: ClientInitializationOptions) => {
274452
// Handle graceful shutdown
275453
process.on('SIGINT', () => {
276454
console.log(chalk.yellow.bold('\n🛑 Shutting down ProxyHub client...'));
455+
456+
// Clear timeout interval and top display
457+
if (timeoutInterval) {
458+
clearInterval(timeoutInterval);
459+
timeoutInterval = null;
460+
}
461+
462+
// Clear top line if it was being used
463+
try {
464+
const moveToTop = () => process.stdout.write('\u001b[1;1H');
465+
const clearLine = () => process.stdout.write('\u001b[2K');
466+
moveToTop();
467+
clearLine();
468+
} catch (error) {
469+
// Ignore cleanup errors
470+
}
471+
277472
socket.disconnect();
278473
process.exit(0);
279474
});
280475

281476
process.on('SIGTERM', () => {
282477
console.log(chalk.yellow.bold('\n🛑 Shutting down ProxyHub client...'));
478+
479+
// Clear timeout interval and top display
480+
if (timeoutInterval) {
481+
clearInterval(timeoutInterval);
482+
timeoutInterval = null;
483+
}
484+
485+
// Clear top line if it was being used
486+
try {
487+
const moveToTop = () => process.stdout.write('\u001b[1;1H');
488+
const clearLine = () => process.stdout.write('\u001b[2K');
489+
moveToTop();
490+
clearLine();
491+
} catch (error) {
492+
// Ignore cleanup errors
493+
}
494+
283495
socket.disconnect();
284496
process.exit(0);
285497
});

packages/server/src/lib/socket.ts

Lines changed: 47 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -106,23 +106,47 @@ class SocketHandler {
106106
}
107107

108108
private setConnectionTimeout(socket: any) {
109-
const timeoutDuration = 30 * 60 * 1000; // 30 minutes in milliseconds
109+
// Clear any existing timeout for this socket
110+
this.clearConnectionTimeout(socket.id);
111+
112+
// Get timeout duration from environment variable (in minutes), default to 30 minutes
113+
const timeoutMinutes = parseInt(process.env.CONNECTION_TIMEOUT_MINUTES || '30', 10);
114+
115+
// If timeout is 0 or negative, disable timeout
116+
if (timeoutMinutes <= 0) {
117+
console.log(`Timeout disabled for socket ${socket.id} (CONNECTION_TIMEOUT_MINUTES=${timeoutMinutes})`);
118+
return;
119+
}
120+
121+
const timeoutDuration = timeoutMinutes * 60 * 1000; // Convert minutes to milliseconds
110122

111123
const timeout = setTimeout(() => {
112-
console.log(`Connection timeout reached for socket ${socket.id}, disconnecting...`);
113-
socket.emit('connection-timeout', {
114-
message: 'Connection has been active for 30 minutes and will be disconnected.',
115-
timeoutMinutes: 30
116-
});
124+
console.log(`Connection timeout reached for socket ${socket.id} after ${timeoutMinutes} minutes, disconnecting...`);
117125

118-
// Give client 5 seconds to handle the timeout message, then disconnect
119-
setTimeout(() => {
120-
socket.disconnect(true);
121-
}, 5000);
126+
// Check if socket is still connected before emitting
127+
if (socket.connected) {
128+
socket.emit('connection-timeout', {
129+
message: `Connection has been active for ${timeoutMinutes} minutes and will be disconnected.`,
130+
timeoutMinutes: timeoutMinutes
131+
});
132+
133+
// Give client 5 seconds to handle the timeout message, then disconnect
134+
setTimeout(() => {
135+
if (socket.connected) {
136+
console.log(`Forcefully disconnecting socket ${socket.id} after timeout`);
137+
socket.disconnect(true);
138+
}
139+
}, 5000);
140+
} else {
141+
console.log(`Socket ${socket.id} already disconnected, skipping timeout disconnect`);
142+
}
143+
144+
// Clean up the timeout reference
145+
this.connectionTimeouts.delete(socket.id);
122146
}, timeoutDuration);
123147

124148
this.connectionTimeouts.set(socket.id, timeout);
125-
console.log(`Set 30-minute timeout for socket ${socket.id}`);
149+
console.log(`Set ${timeoutMinutes}-minute timeout for socket ${socket.id} (timeout in ${timeoutDuration}ms)`);
126150
}
127151

128152
private clearConnectionTimeout(socketId: string) {
@@ -205,9 +229,20 @@ class SocketHandler {
205229
const tunnelInfo = this.getTunnelInfo(data.stableTunnelId);
206230
console.log('Generated tunnel info:', tunnelInfo);
207231

232+
// Get timeout configuration for client display
233+
const timeoutMinutes = parseInt(process.env.CONNECTION_TIMEOUT_MINUTES || '30', 10);
234+
const timeoutEnabled = timeoutMinutes > 0;
235+
const sessionStartTime = Date.now();
236+
208237
socket.emit('on-connect-tunnel', {
209238
id: data.stableTunnelId,
210-
...tunnelInfo
239+
...tunnelInfo,
240+
timeout: {
241+
minutes: timeoutMinutes,
242+
enabled: timeoutEnabled,
243+
sessionStartTime: sessionStartTime,
244+
durationMs: timeoutEnabled ? timeoutMinutes * 60 * 1000 : 0
245+
}
211246
});
212247
});
213248

0 commit comments

Comments
 (0)