@@ -30,20 +30,20 @@ interface ClientWithUser extends ssh2.Connection {
3030
3131export class SSHServer {
3232 private server : ssh2 . Server ;
33+ private keepaliveTimers : Map < ssh2 . Connection , NodeJS . Timeout > = new Map ( ) ;
3334
3435 constructor ( ) {
3536 const sshConfig = getSSHConfig ( ) ;
37+ // TODO: Server config could go to config file
3638 this . server = new ssh2 . Server (
3739 {
3840 hostKeys : [ fs . readFileSync ( sshConfig . hostKey . privateKeyPath ) ] ,
39- // Increase connection timeout and keepalive settings
40- keepaliveInterval : 5000 , // More frequent keepalive
41- keepaliveCountMax : 10 , // Allow more keepalive attempts
41+ authMethods : [ 'publickey' , 'password' ] as any ,
42+ keepaliveInterval : 20000 , // 20 seconds is recommended for SSH connections
43+ keepaliveCountMax : 5 , // Recommended for SSH connections is 3-5 attempts
4244 readyTimeout : 30000 , // Longer ready timeout
4345 debug : ( msg : string ) => {
44- if ( process . env . SSH_DEBUG === 'true' ) {
45- console . debug ( '[SSH Debug]' , msg ) ;
46- }
46+ console . debug ( '[SSH Debug]' , msg ) ;
4747 } ,
4848 } as any , // Cast to any to avoid strict type checking for now
4949 ( client : ssh2 . Connection , info : any ) => {
@@ -72,20 +72,31 @@ export class SSHServer {
7272 client . on ( 'error' , ( err : Error ) => {
7373 console . error ( `[SSH] Client error from ${ clientIp } :` , err ) ;
7474 clearTimeout ( connectionTimeout ) ;
75- // Close connection on error for security
76- client . end ( ) ;
75+ // Don't end the connection on error, let it try to recover
7776 } ) ;
7877
7978 // Handle client end
8079 client . on ( 'end' , ( ) => {
8180 console . log ( `[SSH] Client disconnected from ${ clientIp } ` ) ;
8281 clearTimeout ( connectionTimeout ) ;
82+ // Clean up keepalive timer
83+ const keepaliveTimer = this . keepaliveTimers . get ( client ) ;
84+ if ( keepaliveTimer ) {
85+ clearInterval ( keepaliveTimer ) ;
86+ this . keepaliveTimers . delete ( client ) ;
87+ }
8388 } ) ;
8489
8590 // Handle client close
8691 client . on ( 'close' , ( ) => {
8792 console . log ( `[SSH] Client connection closed from ${ clientIp } ` ) ;
8893 clearTimeout ( connectionTimeout ) ;
94+ // Clean up keepalive timer
95+ const keepaliveTimer = this . keepaliveTimers . get ( client ) ;
96+ if ( keepaliveTimer ) {
97+ clearInterval ( keepaliveTimer ) ;
98+ this . keepaliveTimers . delete ( client ) ;
99+ }
89100 } ) ;
90101
91102 // Handle keepalive requests
@@ -96,7 +107,7 @@ export class SSHServer {
96107 // Always accept keepalive requests to prevent connection drops
97108 accept ( ) ;
98109 } else {
99- console . log ( '[SSH] Rejecting global request:' , info . type ) ;
110+ console . log ( '[SSH] Rejecting unknown global request:' , info . type ) ;
100111 reject ( ) ;
101112 }
102113 } ) ;
@@ -185,28 +196,37 @@ export class SSHServer {
185196 }
186197 } ) ;
187198
188- // Set up keepalive functionality
199+ // Set up keepalive timer
189200 const startKeepalive = ( ) : void => {
190- const keepaliveInterval = setInterval ( ( ) => {
191- try {
192- // Use a type assertion to access ping method
193- ( client as any ) . ping ( ) ;
194- console . log ( '[SSH] Sent keepalive ping to client' ) ;
195- } catch ( err ) {
196- console . error ( '[SSH] Failed to send keepalive ping:' , err ) ;
197- clearInterval ( keepaliveInterval ) ;
201+ // Clean up any existing timer
202+ const existingTimer = this . keepaliveTimers . get ( client ) ;
203+ if ( existingTimer ) {
204+ clearInterval ( existingTimer ) ;
205+ }
206+
207+ const keepaliveTimer = setInterval ( ( ) => {
208+ if ( ( client as any ) . connected !== false ) {
209+ console . log ( `[SSH] Sending keepalive to ${ clientIp } ` ) ;
210+ try {
211+ ( client as any ) . ping ( ) ;
212+ } catch ( error ) {
213+ console . error ( `[SSH] Error sending keepalive to ${ clientIp } :` , error ) ;
214+ // Don't clear the timer on error, let it try again
215+ }
216+ } else {
217+ console . log ( `[SSH] Client ${ clientIp } disconnected, clearing keepalive` ) ;
218+ clearInterval ( keepaliveTimer ) ;
219+ this . keepaliveTimers . delete ( client ) ;
198220 }
199- } , 30000 ) ; // Send ping every 30 seconds
221+ } , 15000 ) ; // 15 seconds between keepalives (recommended for SSH connections is 15- 30 seconds)
200222
201- client . on ( 'close' , ( ) => {
202- clearInterval ( keepaliveInterval ) ;
203- } ) ;
223+ this . keepaliveTimers . set ( client , keepaliveTimer ) ;
204224 } ;
205225
206226 // Handle ready state
207227 client . on ( 'ready' , ( ) => {
208228 console . log (
209- `[SSH] Client ready from ${ clientIp } , user: ${ clientWithUser . authenticatedUser ?. username || 'unknown' } ` ,
229+ `[SSH] Client ready from ${ clientIp } , user: ${ clientWithUser . authenticatedUser ?. username || 'unknown' } , starting keepalive ` ,
210230 ) ;
211231 clearTimeout ( connectionTimeout ) ;
212232 startKeepalive ( ) ;
@@ -374,16 +394,22 @@ export class SSHServer {
374394 const remoteUrl = new URL ( proxyUrl ) ;
375395 const sshConfig = getSSHConfig ( ) ;
376396
397+ // TODO: Connection options could go to config
377398 // Set up connection options
378- const connectionOptions = {
399+ const connectionOptions : any = {
379400 host : remoteUrl . hostname ,
380401 port : 22 ,
381402 username : 'git' ,
382403 tryKeyboard : false ,
383404 readyTimeout : 30000 ,
384- keepaliveInterval : 5000 ,
385- keepaliveCountMax : 10 ,
405+ keepaliveInterval : 15000 , // 15 seconds between keepalives (recommended for SSH connections is 15-30 seconds)
406+ keepaliveCountMax : 5 , // Recommended for SSH connections is 3-5 attempts
407+ windowSize : 1024 * 1024 , // 1MB window size
408+ packetSize : 32768 , // 32KB packet size
386409 privateKey : fs . readFileSync ( sshConfig . hostKey . privateKeyPath ) ,
410+ debug : ( msg : string ) => {
411+ console . debug ( '[GitHub SSH Debug]' , msg ) ;
412+ } ,
387413 algorithms : {
388414 kex : [
389415 'ecdh-sha2-nistp256' as any ,
@@ -404,6 +430,51 @@ export class SSHServer {
404430 } ,
405431 } ;
406432
433+ // Get the client's SSH key that was used for authentication
434+ const clientKey = client . userPrivateKey ;
435+ console . log ( '[SSH] Client key:' , clientKey ? 'Available' : 'Not available' ) ;
436+
437+ // Handle client key if available (though we only have public key data)
438+ if ( clientKey ) {
439+ console . log ( '[SSH] Using client key info:' , JSON . stringify ( clientKey ) ) ;
440+ // Check if the key is in the correct format
441+ if ( typeof clientKey === 'object' && clientKey . keyType && clientKey . keyData ) {
442+ // We need to use the private key, not the public key data
443+ // Since we only have the public key from authentication, we'll use the proxy key
444+ console . log ( '[SSH] Only have public key data, using proxy key instead' ) ;
445+ } else if ( Buffer . isBuffer ( clientKey ) ) {
446+ // The key is a buffer, use it directly
447+ connectionOptions . privateKey = clientKey ;
448+ console . log ( '[SSH] Using client key buffer directly' ) ;
449+ } else {
450+ // Try to convert the key to a buffer if it's a string
451+ try {
452+ connectionOptions . privateKey = Buffer . from ( clientKey ) ;
453+ console . log ( '[SSH] Converted client key to buffer' ) ;
454+ } catch ( error ) {
455+ console . error ( '[SSH] Failed to convert client key to buffer:' , error ) ;
456+ // Fall back to the proxy key (already set)
457+ console . log ( '[SSH] Falling back to proxy key' ) ;
458+ }
459+ }
460+ } else {
461+ console . log ( '[SSH] No client key available, using proxy key' ) ;
462+ }
463+
464+ // Log the key type for debugging
465+ if ( connectionOptions . privateKey ) {
466+ if (
467+ typeof connectionOptions . privateKey === 'object' &&
468+ ( connectionOptions . privateKey as any ) . algo
469+ ) {
470+ console . log ( `[SSH] Key algo: ${ ( connectionOptions . privateKey as any ) . algo } ` ) ;
471+ } else if ( Buffer . isBuffer ( connectionOptions . privateKey ) ) {
472+ console . log ( `[SSH] Key is a buffer of length: ${ connectionOptions . privateKey . length } ` ) ;
473+ } else {
474+ console . log ( `[SSH] Key is of type: ${ typeof connectionOptions . privateKey } ` ) ;
475+ }
476+ }
477+
407478 const remoteGitSsh = new ssh2 . Client ( ) ;
408479
409480 // Handle connection success
@@ -426,6 +497,29 @@ export class SSHServer {
426497 `[SSH] Command executed on remote for user ${ userName } , setting up data piping` ,
427498 ) ;
428499
500+ // Handle stream errors
501+ remoteStream . on ( 'error' , ( err : Error ) => {
502+ console . error ( `[SSH] Remote stream error for user ${ userName } :` , err ) ;
503+ // Don't immediately end the stream on error, try to recover
504+ if (
505+ err . message . includes ( 'early EOF' ) ||
506+ err . message . includes ( 'unexpected disconnect' )
507+ ) {
508+ console . log (
509+ `[SSH] Detected early EOF or unexpected disconnect for user ${ userName } , attempting to recover` ,
510+ ) ;
511+ // Try to keep the connection alive
512+ if ( ( remoteGitSsh as any ) . connected ) {
513+ console . log ( `[SSH] Connection still active for user ${ userName } , continuing` ) ;
514+ // Don't end the stream, let it try to recover
515+ return ;
516+ }
517+ }
518+ // If we can't recover, then end the stream
519+ stream . stderr . write ( `Stream error: ${ err . message } \n` ) ;
520+ stream . end ( ) ;
521+ } ) ;
522+
429523 // Pipe data between client and remote
430524 stream . on ( 'data' , ( data : any ) => {
431525 remoteStream . write ( data ) ;
0 commit comments