@@ -5,6 +5,36 @@ import * as http from "http";
55import ResponseChannel from "./tunnel.js" ;
66import * 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+
838const 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 } ) ;
0 commit comments