@@ -9,13 +9,23 @@ interface SSHUser {
99 username : string ;
1010 password ?: string | null ;
1111 publicKeys ?: string [ ] ;
12+ email ?: string ;
13+ gitAccount ?: string ;
14+ }
15+
16+ interface AuthenticatedUser {
17+ username : string ;
18+ email ?: string ;
19+ gitAccount ?: string ;
1220}
1321
1422interface ClientWithUser extends ssh2 . Connection {
1523 userPrivateKey ?: {
1624 keyType : string ;
1725 keyData : Buffer ;
1826 } ;
27+ authenticatedUser ?: AuthenticatedUser ;
28+ clientIp ?: string ;
1929}
2030
2131export class SSHServer {
@@ -31,31 +41,51 @@ export class SSHServer {
3141 keepaliveCountMax : 10 , // Allow more keepalive attempts
3242 readyTimeout : 30000 , // Longer ready timeout
3343 debug : ( msg : string ) => {
34- console . debug ( '[SSH Debug]' , msg ) ;
44+ if ( process . env . SSH_DEBUG === 'true' ) {
45+ console . debug ( '[SSH Debug]' , msg ) ;
46+ }
3547 } ,
3648 } as any , // Cast to any to avoid strict type checking for now
37- this . handleClient . bind ( this ) ,
49+ ( client : ssh2 . Connection , info : any ) => {
50+ // Pass client connection info to the handler
51+ this . handleClient ( client , { ip : info ?. ip , family : info ?. family } ) ;
52+ } ,
3853 ) ;
3954 }
4055
41- async handleClient ( client : ssh2 . Connection ) : Promise < void > {
42- console . log ( '[SSH] Client connected' ) ;
56+ async handleClient (
57+ client : ssh2 . Connection ,
58+ clientInfo ?: { ip ?: string ; family ?: string } ,
59+ ) : Promise < void > {
60+ const clientIp = clientInfo ?. ip || 'unknown' ;
61+ console . log ( `[SSH] Client connected from ${ clientIp } ` ) ;
4362 const clientWithUser = client as ClientWithUser ;
63+ clientWithUser . clientIp = clientIp ;
64+
65+ // Set up connection timeout (10 minutes)
66+ const connectionTimeout = setTimeout ( ( ) => {
67+ console . log ( `[SSH] Connection timeout for ${ clientIp } - closing` ) ;
68+ client . end ( ) ;
69+ } , 600000 ) ; // 10 minute timeout
4470
4571 // Set up client error handling
4672 client . on ( 'error' , ( err : Error ) => {
47- console . error ( '[SSH] Client error:' , err ) ;
48- // Don't end the connection on error, let it try to recover
73+ console . error ( `[SSH] Client error from ${ clientIp } :` , err ) ;
74+ clearTimeout ( connectionTimeout ) ;
75+ // Close connection on error for security
76+ client . end ( ) ;
4977 } ) ;
5078
5179 // Handle client end
5280 client . on ( 'end' , ( ) => {
53- console . log ( '[SSH] Client disconnected' ) ;
81+ console . log ( `[SSH] Client disconnected from ${ clientIp } ` ) ;
82+ clearTimeout ( connectionTimeout ) ;
5483 } ) ;
5584
5685 // Handle client close
5786 client . on ( 'close' , ( ) => {
58- console . log ( '[SSH] Client connection closed' ) ;
87+ console . log ( `[SSH] Client connection closed from ${ clientIp } ` ) ;
88+ clearTimeout ( connectionTimeout ) ;
5989 } ) ;
6090
6191 // Handle keepalive requests
@@ -73,7 +103,12 @@ export class SSHServer {
73103
74104 // Handle authentication
75105 client . on ( 'authentication' , ( ctx : ssh2 . AuthContext ) => {
76- console . log ( '[SSH] Authentication attempt:' , ctx . method , 'for user:' , ctx . username ) ;
106+ console . log (
107+ `[SSH] Authentication attempt from ${ clientIp } :` ,
108+ ctx . method ,
109+ 'for user:' ,
110+ ctx . username ,
111+ ) ;
77112
78113 if ( ctx . method === 'publickey' ) {
79114 // Handle public key authentication
@@ -83,12 +118,19 @@ export class SSHServer {
83118 . findUserBySSHKey ( keyString )
84119 . then ( ( user : any ) => {
85120 if ( user ) {
86- console . log ( `[SSH] Public key authentication successful for user: ${ user . username } ` ) ;
87- // Store the public key info for later use
121+ console . log (
122+ `[SSH] Public key authentication successful for user: ${ user . username } from ${ clientIp } ` ,
123+ ) ;
124+ // Store the public key info and user context for later use
88125 clientWithUser . userPrivateKey = {
89126 keyType : ctx . key . algo ,
90127 keyData : ctx . key . data ,
91128 } ;
129+ clientWithUser . authenticatedUser = {
130+ username : user . username ,
131+ email : user . email ,
132+ gitAccount : user . gitAccount ,
133+ } ;
92134 ctx . accept ( ) ;
93135 } else {
94136 console . log ( '[SSH] Public key authentication failed - key not found' ) ;
@@ -113,8 +155,14 @@ export class SSHServer {
113155 ctx . reject ( ) ;
114156 } else if ( result ) {
115157 console . log (
116- `[SSH] Password authentication successful for user: ${ user . username } ` ,
158+ `[SSH] Password authentication successful for user: ${ user . username } from ${ clientIp } ` ,
117159 ) ;
160+ // Store user context for later use
161+ clientWithUser . authenticatedUser = {
162+ username : user . username ,
163+ email : user . email ,
164+ gitAccount : user . gitAccount ,
165+ } ;
118166 ctx . accept ( ) ;
119167 } else {
120168 console . log ( '[SSH] Password authentication failed - invalid password' ) ;
@@ -157,7 +205,10 @@ export class SSHServer {
157205
158206 // Handle ready state
159207 client . on ( 'ready' , ( ) => {
160- console . log ( '[SSH] Client ready, starting keepalive' ) ;
208+ console . log (
209+ `[SSH] Client ready from ${ clientIp } , user: ${ clientWithUser . authenticatedUser ?. username || 'unknown' } ` ,
210+ ) ;
211+ clearTimeout ( connectionTimeout ) ;
161212 startKeepalive ( ) ;
162213 } ) ;
163214
@@ -184,20 +235,31 @@ export class SSHServer {
184235 stream : ssh2 . ServerChannel ,
185236 client : ClientWithUser ,
186237 ) : Promise < void > {
187- console . log ( '[SSH] Handling command:' , command ) ;
238+ const userName = client . authenticatedUser ?. username || 'unknown' ;
239+ const clientIp = client . clientIp || 'unknown' ;
240+ console . log ( `[SSH] Handling command from ${ userName } @${ clientIp } : ${ command } ` ) ;
241+
242+ // Validate user is authenticated
243+ if ( ! client . authenticatedUser ) {
244+ console . error ( `[SSH] Unauthenticated command attempt from ${ clientIp } ` ) ;
245+ stream . stderr . write ( 'Authentication required\n' ) ;
246+ stream . exit ( 1 ) ;
247+ stream . end ( ) ;
248+ return ;
249+ }
188250
189251 try {
190252 // Check if it's a Git command
191- if ( command . startsWith ( 'git-' ) ) {
253+ if ( command . startsWith ( 'git-upload-pack' ) || command . startsWith ( 'git-receive-pack ') ) {
192254 await this . handleGitCommand ( command , stream , client ) ;
193255 } else {
194- console . log ( ' [SSH] Unsupported command:' , command ) ;
256+ console . log ( ` [SSH] Unsupported command from ${ userName } @ ${ clientIp } : ${ command } ` ) ;
195257 stream . stderr . write ( `Unsupported command: ${ command } \n` ) ;
196258 stream . exit ( 1 ) ;
197259 stream . end ( ) ;
198260 }
199261 } catch ( error ) {
200- console . error ( ' [SSH] Error handling command:' , error ) ;
262+ console . error ( ` [SSH] Error handling command from ${ userName } @ ${ clientIp } :` , error ) ;
201263 stream . stderr . write ( `Error: ${ error } \n` ) ;
202264 stream . exit ( 1 ) ;
203265 stream . end ( ) ;
@@ -217,30 +279,61 @@ export class SSHServer {
217279 }
218280
219281 const repoPath = repoMatch [ 1 ] ;
220- console . log ( '[SSH] Git command for repository:' , repoPath ) ;
282+ const isReceivePack = command . includes ( 'git-receive-pack' ) ;
283+ const gitPath = isReceivePack ? 'git-receive-pack' : 'git-upload-pack' ;
221284
222- // Create a simulated HTTP request for the proxy chain
285+ console . log (
286+ `[SSH] Git command for repository: ${ repoPath } from user: ${ client . authenticatedUser ?. username || 'unknown' } ` ,
287+ ) ;
288+
289+ // Create a properly formatted HTTP request for the proxy chain
290+ // Match the format expected by the HTTPS flow
223291 const req = {
224- url : repoPath ,
225- method : command . startsWith ( 'git-upload-pack' ) ? 'GET' : 'POST' ,
292+ originalUrl : `/${ repoPath } /${ gitPath } ` ,
293+ url : `/${ repoPath } /${ gitPath } ` ,
294+ method : isReceivePack ? 'POST' : 'GET' ,
226295 headers : {
227296 'user-agent' : 'git/ssh-proxy' ,
228- 'content-type' : command . startsWith ( 'git-receive-pack' )
297+ 'content-type' : isReceivePack
229298 ? 'application/x-git-receive-pack-request'
230299 : 'application/x-git-upload-pack-request' ,
300+ host : 'ssh-proxy' ,
231301 } ,
232302 body : null ,
233- user : client . userPrivateKey ? { username : 'ssh-user' } : null ,
303+ user : client . authenticatedUser || null ,
304+ isSSH : true ,
305+ } ;
306+
307+ // Create a mock response object for the chain
308+ const res = {
309+ headers : { } ,
310+ statusCode : 200 ,
311+ set : function ( headers : any ) {
312+ Object . assign ( this . headers , headers ) ;
313+ return this ;
314+ } ,
315+ status : function ( code : number ) {
316+ this . statusCode = code ;
317+ return this ;
318+ } ,
319+ send : function ( data : any ) {
320+ return this ;
321+ } ,
234322 } ;
235323
236324 // Execute the proxy chain
237325 try {
238- const result = await chain . executeChain ( req , { } as any ) ;
326+ const result = await chain . executeChain ( req , res ) ;
239327 if ( result . error || result . blocked ) {
240- throw new Error ( result . message || 'Request blocked by proxy chain' ) ;
328+ const message =
329+ result . errorMessage || result . blockedMessage || 'Request blocked by proxy chain' ;
330+ throw new Error ( message ) ;
241331 }
242332 } catch ( chainError ) {
243- console . error ( '[SSH] Chain execution failed:' , chainError ) ;
333+ console . error (
334+ `[SSH] Chain execution failed for user ${ client . authenticatedUser ?. username } :` ,
335+ chainError ,
336+ ) ;
244337 stream . stderr . write ( `Access denied: ${ chainError } \n` ) ;
245338 stream . exit ( 1 ) ;
246339 stream . end ( ) ;
@@ -263,12 +356,18 @@ export class SSHServer {
263356 client : ClientWithUser ,
264357 ) : Promise < void > {
265358 return new Promise ( ( resolve , reject ) => {
266- console . log ( '[SSH] Creating SSH connection to remote' ) ;
359+ const userName = client . authenticatedUser ?. username || 'unknown' ;
360+ console . log ( `[SSH] Creating SSH connection to remote for user: ${ userName } ` ) ;
267361
268362 // Get remote host from config
269363 const proxyUrl = getProxyUrl ( ) ;
270364 if ( ! proxyUrl ) {
271- reject ( new Error ( 'No proxy URL configured' ) ) ;
365+ const error = new Error ( 'No proxy URL configured' ) ;
366+ console . error ( `[SSH] ${ error . message } ` ) ;
367+ stream . stderr . write ( `Configuration error: ${ error . message } \n` ) ;
368+ stream . exit ( 1 ) ;
369+ stream . end ( ) ;
370+ reject ( error ) ;
272371 return ;
273372 }
274373
@@ -309,12 +408,12 @@ export class SSHServer {
309408
310409 // Handle connection success
311410 remoteGitSsh . on ( 'ready' , ( ) => {
312- console . log ( ' [SSH] Connected to remote Git server' ) ;
411+ console . log ( ` [SSH] Connected to remote Git server for user: ${ userName } ` ) ;
313412
314413 // Execute the Git command on the remote server
315414 remoteGitSsh . exec ( command , ( err : Error | undefined , remoteStream : ssh2 . ClientChannel ) => {
316415 if ( err ) {
317- console . error ( ' [SSH] Error executing command on remote:' , err ) ;
416+ console . error ( ` [SSH] Error executing command on remote for user ${ userName } :` , err ) ;
318417 stream . stderr . write ( `Remote execution error: ${ err . message } \n` ) ;
319418 stream . exit ( 1 ) ;
320419 stream . end ( ) ;
@@ -323,51 +422,66 @@ export class SSHServer {
323422 return ;
324423 }
325424
326- console . log ( '[SSH] Command executed on remote, setting up data piping' ) ;
425+ console . log (
426+ `[SSH] Command executed on remote for user ${ userName } , setting up data piping` ,
427+ ) ;
327428
328429 // Pipe data between client and remote
329- stream . on ( 'data' , ( data : Buffer ) => {
430+ stream . on ( 'data' , ( data : any ) => {
330431 remoteStream . write ( data ) ;
331432 } ) ;
332433
333- remoteStream . on ( 'data' , ( data : Buffer ) => {
434+ remoteStream . on ( 'data' , ( data : any ) => {
334435 stream . write ( data ) ;
335436 } ) ;
336437
337438 // Handle stream events
338439 remoteStream . on ( 'close' , ( ) => {
339- console . log ( ' [SSH] Remote stream closed' ) ;
440+ console . log ( ` [SSH] Remote stream closed for user: ${ userName } ` ) ;
340441 stream . end ( ) ;
341442 resolve ( ) ;
342443 } ) ;
343444
344445 remoteStream . on ( 'exit' , ( code : number , signal ?: string ) => {
345- console . log ( '[SSH] Remote command exited with code:' , code , 'signal:' , signal ) ;
446+ console . log (
447+ `[SSH] Remote command exited for user ${ userName } with code: ${ code } , signal: ${ signal || 'none' } ` ,
448+ ) ;
346449 stream . exit ( code || 0 ) ;
347450 resolve ( ) ;
348451 } ) ;
349452
350453 stream . on ( 'close' , ( ) => {
351- console . log ( ' [SSH] Client stream closed' ) ;
454+ console . log ( ` [SSH] Client stream closed for user: ${ userName } ` ) ;
352455 remoteStream . end ( ) ;
353456 } ) ;
354457
355458 stream . on ( 'end' , ( ) => {
356- console . log ( ' [SSH] Client stream ended' ) ;
459+ console . log ( ` [SSH] Client stream ended for user: ${ userName } ` ) ;
357460 setTimeout ( ( ) => {
358461 remoteGitSsh . end ( ) ;
359462 } , 1000 ) ;
360463 } ) ;
464+
465+ // Handle errors on streams
466+ remoteStream . on ( 'error' , ( err : Error ) => {
467+ console . error ( `[SSH] Remote stream error for user ${ userName } :` , err ) ;
468+ stream . stderr . write ( `Stream error: ${ err . message } \n` ) ;
469+ } ) ;
470+
471+ stream . on ( 'error' , ( err : Error ) => {
472+ console . error ( `[SSH] Client stream error for user ${ userName } :` , err ) ;
473+ remoteStream . destroy ( ) ;
474+ } ) ;
361475 } ) ;
362476 } ) ;
363477
364- // Handle connection errors with retry logic
478+ // Handle connection errors
365479 remoteGitSsh . on ( 'error' , ( err : Error ) => {
366- console . error ( ' [SSH] Remote connection error:' , err ) ;
480+ console . error ( ` [SSH] Remote connection error for user ${ userName } :` , err ) ;
367481
368482 if ( err . message . includes ( 'All configured authentication methods failed' ) ) {
369483 console . log (
370- ' [SSH] Authentication failed with default key, this is expected for some servers' ,
484+ ` [SSH] Authentication failed with default key for user ${ userName } , this may be expected for some servers` ,
371485 ) ;
372486 }
373487
@@ -379,10 +493,25 @@ export class SSHServer {
379493
380494 // Handle connection close
381495 remoteGitSsh . on ( 'close' , ( ) => {
382- console . log ( '[SSH] Remote connection closed' ) ;
496+ console . log ( `[SSH] Remote connection closed for user: ${ userName } ` ) ;
497+ } ) ;
498+
499+ // Set a timeout for the connection attempt
500+ const connectTimeout = setTimeout ( ( ) => {
501+ console . error ( `[SSH] Connection timeout to remote for user ${ userName } ` ) ;
502+ remoteGitSsh . end ( ) ;
503+ stream . stderr . write ( 'Connection timeout to remote server\n' ) ;
504+ stream . exit ( 1 ) ;
505+ stream . end ( ) ;
506+ reject ( new Error ( 'Connection timeout' ) ) ;
507+ } , 30000 ) ;
508+
509+ remoteGitSsh . on ( 'ready' , ( ) => {
510+ clearTimeout ( connectTimeout ) ;
383511 } ) ;
384512
385513 // Connect to remote
514+ console . log ( `[SSH] Connecting to ${ remoteUrl . hostname } for user ${ userName } ` ) ;
386515 remoteGitSsh . connect ( connectionOptions ) ;
387516 } ) ;
388517 }
0 commit comments