Skip to content

Commit 8fcd8f0

Browse files
committed
feat: adds ssh implementation
1 parent 38041ff commit 8fcd8f0

File tree

25 files changed

+825
-54
lines changed

25 files changed

+825
-54
lines changed

SSH.md

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
## SSH Configuration
2+
3+
1. Generate SSH host key:
4+
5+
```bash
6+
mkdir -p .ssh
7+
ssh-keygen -t rsa -f ./.ssh/host_key
8+
```
9+
10+
2. Add authorized keys:
11+
12+
```bash
13+
# Add each user's public key to .ssh/authorized_keys
14+
echo "ssh-rsa AAAA..." >> ./.ssh/authorized_keys
15+
```
16+
17+
3. Configure Git to use SSH:
18+
19+
```bash
20+
git remote set-url origin ssh://localhost:2222/username/repo.git
21+
```
22+
23+
````
24+
25+
7. **Add Tests**:
26+
Create `test/testSSH.test.js`:
27+
28+
```javascript
29+
const chai = require('chai');
30+
const net = require('net');
31+
const ssh2 = require('ssh2');
32+
33+
describe('SSH Server', () => {
34+
it('should accept valid SSH connections', async () => {
35+
// Test implementation
36+
});
37+
38+
it('should handle git commands over SSH', async () => {
39+
// Test implementation
40+
});
41+
});
42+
````
43+
44+
8. **Security Considerations**:
45+
46+
- Use strong host keys (RSA 4096 or Ed25519)
47+
- Implement rate limiting for SSH connections
48+
- Add logging for SSH authentication attempts
49+
- Consider implementing SSH key rotation
50+
- Add IP whitelisting if needed
51+
52+
Would you like me to help implement any specific part of this SSH support?

config.schema.json

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,11 @@
66
"type": "object",
77
"properties": {
88
"proxyUrl": { "type": "string" },
9+
"gitProtocol": {
10+
"type": "string",
11+
"enum": ["https", "ssh"],
12+
"description": "The protocol to use for Git operations. Can be either 'https' or 'ssh'."
13+
},
914
"cookieSecret": { "type": "string" },
1015
"sessionMaxAgeHours": { "type": "number" },
1116
"api": {
@@ -78,6 +83,14 @@
7883
"type": "object"
7984
}
8085
}
86+
},
87+
"ssh": {
88+
"enabled": true,
89+
"port": 2222,
90+
"hostKey": {
91+
"privateKeyPath": "./.ssh/host_key",
92+
"publicKeyPath": "./.ssh/host_key.pub"
93+
}
8194
}
8295
},
8396
"definitions": {

git-proxy-cookie

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
["connect.sid=s%3Ap8ObJ0cfSkTlRbuDxpEXYydtWsuJ4RHU.cU4n8tX1FDPUp5uqswp6Jwimt2rFynediybG2NSn%2BQ8; Path=/; Expires=Tue, 18 Mar 2025 00:19:15 GMT; HttpOnly"]

package-lock.json

Lines changed: 47 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,8 @@
7272
"react-router-dom": "6.28.2",
7373
"simple-git": "^3.25.0",
7474
"uuid": "^11.0.0",
75-
"yargs": "^17.7.2"
75+
"yargs": "^17.7.2",
76+
"ssh2": "^1.16.0"
7677
},
7778
"devDependencies": {
7879
"@babel/core": "^7.23.2",

packages/git-proxy-cli/index.js

Lines changed: 110 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -19,30 +19,35 @@ axios.defaults.timeout = 30000;
1919
* @param {string} password The password to use for the login
2020
*/
2121
async function login(username, password) {
22+
console.log('Login', { username, password, baseUrl });
2223
try {
23-
let response = await axios.post(
24-
`${baseUrl}/api/auth/login`,
25-
{
26-
username,
27-
password,
28-
},
29-
{
30-
headers: { 'Content-Type': 'application/json' },
31-
withCredentials: true,
32-
},
33-
);
34-
const cookies = response.headers['set-cookie'];
24+
axios
25+
.post(
26+
`${baseUrl}/api/auth/login`,
27+
{
28+
username,
29+
password,
30+
},
31+
{
32+
headers: { 'Content-Type': 'application/json' },
33+
withCredentials: true,
34+
},
35+
)
36+
.then(async (response) => {
37+
const cookies = response.headers['set-cookie'];
3538

36-
response = await axios.get(`${baseUrl}/api/auth/profile`, {
37-
headers: { Cookie: cookies },
38-
withCredentials: true,
39-
});
39+
response = await axios.get(`${baseUrl}/api/auth/profile`, {
40+
headers: { Cookie: cookies },
41+
withCredentials: true,
42+
});
4043

41-
fs.writeFileSync(GIT_PROXY_COOKIE_FILE, JSON.stringify(cookies), 'utf8');
44+
fs.writeFileSync(GIT_PROXY_COOKIE_FILE, JSON.stringify(cookies), 'utf8');
4245

43-
const user = `"${response.data.username}" <${response.data.email}>`;
44-
const isAdmin = response.data.admin ? ' (admin)' : '';
45-
console.log(`Login ${user}${isAdmin}: OK`);
46+
const user = `"${response.data.username}" <${response.data.email}>`;
47+
const isAdmin = response.data.admin ? ' (admin)' : '';
48+
console.log(`Login ${user}${isAdmin}: OK`);
49+
})
50+
.catch((loginError) => console.error);
4651
} catch (error) {
4752
if (error.response) {
4853
console.error(`Error: Login '${username}': '${error.response.status}'`);
@@ -306,6 +311,60 @@ async function logout() {
306311
console.log('Logout: OK');
307312
}
308313

314+
/**
315+
* Add SSH key for a user
316+
* @param {string} username The username to add the key for
317+
* @param {string} keyPath Path to the public key file
318+
*/
319+
async function addSSHKey(username, keyPath) {
320+
console.log('Add SSH key', { username, keyPath });
321+
if (!fs.existsSync(GIT_PROXY_COOKIE_FILE)) {
322+
console.error('Error: SSH key: Authentication required');
323+
process.exitCode = 1;
324+
return;
325+
}
326+
327+
try {
328+
const cookies = JSON.parse(fs.readFileSync(GIT_PROXY_COOKIE_FILE, 'utf8'));
329+
const publicKey = fs.readFileSync(keyPath, 'utf8').trim();
330+
331+
console.log('Adding SSH key', { username, publicKey });
332+
await axios.post(
333+
`${baseUrl}/api/v1/user/${username}/ssh-keys`,
334+
{ publicKey },
335+
{
336+
headers: {
337+
Cookie: cookies,
338+
'Content-Type': 'application/json',
339+
},
340+
withCredentials: true,
341+
},
342+
);
343+
344+
console.log(`SSH key added successfully for user ${username}`);
345+
} catch (error) {
346+
let errorMessage = `Error: SSH key: '${error.message}'`;
347+
process.exitCode = 2;
348+
349+
if (error.response) {
350+
switch (error.response.status) {
351+
case 401:
352+
errorMessage = 'Error: SSH key: Authentication required';
353+
process.exitCode = 3;
354+
break;
355+
case 404:
356+
errorMessage = `Error: SSH key: User '${username}' not found`;
357+
process.exitCode = 4;
358+
break;
359+
}
360+
} else if (error.code === 'ENOENT') {
361+
errorMessage = `Error: SSH key: Could not find key file at ${keyPath}`;
362+
process.exitCode = 5;
363+
}
364+
console.error(errorMessage);
365+
}
366+
}
367+
309368
// Parsing command line arguments
310369
yargs(hideBin(process.argv))
311370
.command({
@@ -436,6 +495,37 @@ yargs(hideBin(process.argv))
436495
rejectGitPush(argv.id);
437496
},
438497
})
498+
.command({
499+
command: 'ssh-key',
500+
describe: 'Manage SSH keys',
501+
builder: {
502+
action: {
503+
describe: 'Action to perform (add/remove)',
504+
demandOption: true,
505+
type: 'string',
506+
choices: ['add', 'remove'],
507+
},
508+
username: {
509+
describe: 'Username to manage keys for',
510+
demandOption: true,
511+
type: 'string',
512+
},
513+
keyPath: {
514+
describe: 'Path to the public key file',
515+
demandOption: true,
516+
type: 'string',
517+
},
518+
},
519+
handler(argv) {
520+
if (argv.action === 'add') {
521+
addSSHKey(argv.username, argv.keyPath);
522+
} else if (argv.action === 'remove') {
523+
// TODO: Implement remove SSH key
524+
console.error('Error: SSH key: Remove action not implemented yet');
525+
process.exitCode = 1;
526+
}
527+
},
528+
})
439529
.demandCommand(1, 'You need at least one command before moving on')
440530
.strict()
441531
.help().argv;

proxy.config.json

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,18 @@
11
{
22
"proxyUrl": "https://github.com",
3+
"gitProtocol": "https",
34
"cookieSecret": "cookie secret",
45
"sessionMaxAgeHours": 12,
56
"tempPassword": {
67
"sendEmail": false,
78
"emailConfig": {}
89
},
910
"authorisedList": [
11+
{
12+
"project": "unlayer",
13+
"name": "react-email-editor",
14+
"url": "https://github.com/unlayer/react-email-editor.git"
15+
},
1016
{
1117
"project": "finos",
1218
"name": "git-proxy",
@@ -96,6 +102,14 @@
96102
"privateOrganizations": [],
97103
"urlShortener": "",
98104
"contactEmail": "",
99-
"csrfProtection": true,
100-
"plugins": []
105+
"csrfProtection": false,
106+
"plugins": [],
107+
"ssh": {
108+
"enabled": true,
109+
"port": 2222,
110+
"hostKey": {
111+
"privateKeyPath": "/Users/dcoric/.ssh/bb_rsa",
112+
"publicKeyPath": "/Users/dcoric/.ssh/bb_rsa.pub"
113+
}
114+
}
101115
}

0 commit comments

Comments
 (0)