Skip to content

Commit 0b38aee

Browse files
committed
feat: update SSH server to enhance client handling and logging
- Add email and gitAccount fields to SSHUser and AuthenticatedUser interfaces - Improve client connection handling by logging client IP and user details - Refactor handleClient method to accept client connection info - Enhance error handling and logging for better debugging - Update tests to reflect changes in client handling and authentication
1 parent 2bcb475 commit 0b38aee

File tree

2 files changed

+205
-49
lines changed

2 files changed

+205
-49
lines changed

src/proxy/ssh/server.ts

Lines changed: 170 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -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

1422
interface ClientWithUser extends ssh2.Connection {
1523
userPrivateKey?: {
1624
keyType: string;
1725
keyData: Buffer;
1826
};
27+
authenticatedUser?: AuthenticatedUser;
28+
clientIp?: string;
1929
}
2030

2131
export 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

Comments
 (0)