Skip to content

Commit 4cc286e

Browse files
committed
feat: adds ssh operations
1 parent 242f998 commit 4cc286e

File tree

4 files changed

+107
-26
lines changed

4 files changed

+107
-26
lines changed

src/config/index.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,10 @@ const getProxyUrl = () => {
3737
return _proxyUrl;
3838
};
3939

40+
const getSSHProxyUrl = () => {
41+
return getProxyUrl().replace('https://', 'git@');
42+
};
43+
4044
// Gets a list of authorised repositories
4145
const getAuthorisedList = () => {
4246
if (_userSettings !== null && _userSettings.authorisedList) {
@@ -216,6 +220,7 @@ const getGitProtocol = () => {
216220

217221
exports.getAPIs = getAPIs;
218222
exports.getProxyUrl = getProxyUrl;
223+
exports.getSSHProxyUrl = getSSHProxyUrl;
219224
exports.getAuthorisedList = getAuthorisedList;
220225
exports.getDatabase = getDatabase;
221226
exports.logConfiguration = logConfiguration;

src/proxy/processors/pre-processor/parseAction.js

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,18 +6,21 @@ const exec = async (req) => {
66
const repoName = getRepoNameFromUrl(req.originalUrl);
77
const paths = req.originalUrl.split('/');
88

9-
let type = 'default';
9+
// Determine protocol based on request source
10+
const protocol = req.isSSH ? 'ssh' : 'https';
11+
let type = `${protocol}-default`;
1012

1113
if (paths[paths.length - 1].endsWith('git-upload-pack') && req.method == 'GET') {
12-
type = 'pull';
14+
type = `${protocol}-pull`;
1315
}
1416
if (
1517
paths[paths.length - 1] == 'git-receive-pack' &&
1618
req.method == 'POST' &&
1719
req.headers['content-type'] == 'application/x-git-receive-pack-request'
1820
) {
19-
type = 'push';
21+
type = `${protocol}-push`;
2022
}
23+
2124
return new actions.Action(id, type, req.method, timestamp, repoName);
2225
};
2326

src/proxy/processors/push-action/pullRemote.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ const exec = async (req, action) => {
2727

2828
if (gitProtocol === 'ssh') {
2929
// Convert HTTPS URL to SSH URL
30-
cloneUrl = action.url.replace('https://github.com/', 'git@github.com:');
30+
cloneUrl = action.url.replace('https://', 'git@');
3131
const cmd = `git clone ${cloneUrl}`;
3232
step.log(`Executing ${cmd}`);
3333

src/proxy/ssh/server.js

Lines changed: 95 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -9,44 +9,46 @@ class SSHServer {
99
{
1010
hostKeys: [require('fs').readFileSync(config.getSSHConfig().hostKey.privateKeyPath)],
1111
authMethods: ['publickey', 'password'],
12-
debug: (msg) => {
13-
console.debug('SSH Debug:', msg);
14-
},
12+
// debug: (msg) => {
13+
// console.debug('[SSH Debug]', msg);
14+
// },
1515
},
1616
this.handleClient.bind(this),
1717
);
1818
}
1919

2020
async handleClient(client) {
21-
console.log('Client connected', client);
21+
console.log('[SSH] Client connected', client);
2222
client.on('authentication', async (ctx) => {
23-
console.log(`Authentication attempt: ${ctx.method}`);
23+
console.log(`[SSH] Authentication attempt: ${ctx.method}`);
2424

2525
if (ctx.method === 'publickey') {
2626
try {
27-
console.log(`CTX KEY: ${JSON.stringify(ctx.key)}`);
27+
console.log(`[SSH] CTX KEY: ${JSON.stringify(ctx.key)}`);
2828
// Get the key type and key data
2929
const keyType = ctx.key.algo;
3030
const keyData = ctx.key.data;
3131

3232
// Format the key in the same way as stored in user's publicKeys (without comment)
3333
const keyString = `${keyType} ${keyData.toString('base64')}`;
3434

35-
console.log(`Attempting public key authentication with key: ${keyString}`);
35+
console.log(`[SSH] Attempting public key authentication with key: ${keyString}`);
3636

3737
// Find user by SSH key
3838
const user = await db.findUserBySSHKey(keyString);
3939
if (!user) {
40-
console.log('No user found with this SSH key');
40+
console.log('[SSH] No user found with this SSH key');
4141
ctx.reject();
4242
return;
4343
}
4444

45-
console.log(`Public key authentication successful for user ${user.username}`);
45+
console.log(`[SSH] Public key authentication successful for user ${user.username}`);
4646
client.username = user.username;
47+
// Store the user's private key for later use with GitHub
48+
client.userPrivateKey = ctx.key;
4749
ctx.accept();
4850
} catch (error) {
49-
console.error('Error during public key authentication:', error);
51+
console.error('[SSH] Error during public key authentication:', error);
5052
// Let the client try the next key
5153
ctx.reject();
5254
}
@@ -59,22 +61,22 @@ class SSHServer {
5961
const bcrypt = require('bcryptjs');
6062
const isValid = await bcrypt.compare(ctx.password, user.password);
6163
if (isValid) {
62-
console.log(`Password authentication successful for user ${ctx.username}`);
64+
console.log(`[SSH] Password authentication successful for user ${ctx.username}`);
6365
ctx.accept();
6466
} else {
65-
console.log(`Password authentication failed for user ${ctx.username}`);
67+
console.log(`[SSH] Password authentication failed for user ${ctx.username}`);
6668
ctx.reject();
6769
}
6870
} else {
69-
console.log(`User ${ctx.username} not found or no password set`);
71+
console.log(`[SSH] User ${ctx.username} not found or no password set`);
7072
ctx.reject();
7173
}
7274
} catch (error) {
73-
console.error('Error during password authentication:', error);
75+
console.error('[SSH] Error during password authentication:', error);
7476
ctx.reject();
7577
}
7678
} else {
77-
console.log('Password authentication attempted but public key was provided');
79+
console.log('[SSH] Password authentication attempted but public key was provided');
7880
ctx.reject();
7981
}
8082
} else {
@@ -84,12 +86,12 @@ class SSHServer {
8486
});
8587

8688
client.on('ready', () => {
87-
console.log(`Client ready: ${client.username}`);
89+
console.log(`[SSH] Client ready: ${client.username}`);
8890
client.on('session', this.handleSession.bind(this));
8991
});
9092

9193
client.on('error', (err) => {
92-
console.error('Client error:', err);
94+
console.error('[SSH] Client error:', err);
9395
});
9496
}
9597

@@ -100,7 +102,7 @@ class SSHServer {
100102
const command = info.command;
101103

102104
// Parse Git command
103-
console.log('Command', command);
105+
console.log('[SSH] Command', command);
104106
if (command.startsWith('git-')) {
105107
// Extract the repository path from the command
106108
// Remove quotes and 'git-' prefix, then trim any leading/trailing slashes
@@ -113,6 +115,7 @@ class SSHServer {
113115
const req = {
114116
method: command === 'git-upload-pack' ? 'GET' : 'POST',
115117
originalUrl: repoPath,
118+
isSSH: true,
116119
headers: {
117120
'user-agent': 'git/2.0.0',
118121
'content-type':
@@ -121,15 +124,85 @@ class SSHServer {
121124
};
122125

123126
try {
124-
console.log('Executing chain', req);
127+
console.log('[SSH] Executing chain', req);
125128
const action = await chain.executeChain(req);
126-
stream.write(action.getContent());
127-
stream.end();
129+
130+
console.log('[SSH] Action', action);
131+
132+
if (action.error || action.blocked) {
133+
// If there's an error or the action is blocked, send the error message
134+
console.log(
135+
'[SSH] Action error or blocked',
136+
action.errorMessage || action.blockedMessage,
137+
);
138+
stream.write(action.errorMessage || action.blockedMessage);
139+
stream.end();
140+
return;
141+
}
142+
143+
// Create SSH connection to GitHub
144+
const githubSsh = new ssh2.Client();
145+
146+
console.log('[SSH] Creating SSH connection to GitHub');
147+
githubSsh.on('ready', () => {
148+
console.log('[SSH] Connected to GitHub');
149+
150+
// Execute the Git command on GitHub
151+
githubSsh.exec(command, { env: { GIT_PROTOCOL: 'version=2' } }, (err, githubStream) => {
152+
if (err) {
153+
console.error('[SSH] Failed to execute command on GitHub:', err);
154+
stream.write(err.toString());
155+
stream.end();
156+
return;
157+
}
158+
159+
// Pipe data between client and GitHub
160+
stream.pipe(githubStream).pipe(stream);
161+
162+
githubStream.on('exit', (code) => {
163+
console.log(`[SSH] GitHub command exited with code ${code}`);
164+
githubSsh.end();
165+
});
166+
});
167+
});
168+
169+
githubSsh.on('error', (err) => {
170+
console.error('[SSH] GitHub SSH error:', err);
171+
stream.write(err.toString());
172+
stream.end();
173+
});
174+
175+
// Get the client's SSH key that was used for authentication
176+
// console.log('[SSH] Session:', session);
177+
const clientKey = session._channel._client.userPrivateKey;
178+
console.log('[SSH] Client key:', clientKey ? 'Available' : 'Not available');
179+
180+
if (clientKey) {
181+
console.log('[SSH] Using client key to connect to GitHub');
182+
// Use the client's private key to connect to GitHub
183+
githubSsh.connect({
184+
host: 'github.com',
185+
port: 22,
186+
username: 'git',
187+
privateKey: clientKey,
188+
});
189+
} else {
190+
console.log('[SSH] No client key available, using proxy key');
191+
// Fallback to proxy's SSH key if no client key is available
192+
githubSsh.connect({
193+
host: 'github.com',
194+
port: 22,
195+
username: 'git',
196+
privateKey: require('fs').readFileSync(config.getSSHConfig().hostKey.privateKeyPath),
197+
});
198+
}
128199
} catch (error) {
200+
console.error('[SSH] Error during SSH connection:', error);
129201
stream.write(error.toString());
130202
stream.end();
131203
}
132204
} else {
205+
console.log('[SSH] Unsupported command', command);
133206
stream.write('Unsupported command');
134207
stream.end();
135208
}
@@ -139,7 +212,7 @@ class SSHServer {
139212
start() {
140213
const port = config.getSSHConfig().port;
141214
this.server.listen(port, '0.0.0.0', () => {
142-
console.log(`SSH server listening on port ${port}`);
215+
console.log(`[SSH] Server listening on port ${port}`);
143216
});
144217
}
145218
}

0 commit comments

Comments
 (0)