Skip to content

Commit f96a8c4

Browse files
authored
Merge pull request #1060 from fabiovincenzi/clone-fix
fix(proxy): preserve original Git pack POST streams before validation
2 parents 75fb0e6 + 37d9464 commit f96a8c4

File tree

7 files changed

+186
-193
lines changed

7 files changed

+186
-193
lines changed

cypress/e2e/repo.cy.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ describe('Repo', () => {
1010

1111
describe('Code button for repo row', () => {
1212
it('Opens tooltip with correct content and can copy', () => {
13-
const cloneURL = 'http://localhost:8000/finos/test-repo.git';
13+
const cloneURL = 'http://localhost:8000/finos/git-proxy.git';
1414
const tooltipQuery = 'div[role="tooltip"]';
1515

1616
cy
@@ -19,8 +19,8 @@ describe('Repo', () => {
1919
.should('not.exist');
2020

2121
cy
22-
// find the entry for finos/test-repo
23-
.get('a[href="/dashboard/repo/test-repo"]')
22+
// find the entry for finos/git-proxy
23+
.get('a[href="/dashboard/repo/git-proxy"]')
2424
// take it's parent row
2525
.closest('tr')
2626
// find the nearby span containing Code we can click to open the tooltip

package-lock.json

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

package.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,6 @@
4646
"axios": "^1.6.0",
4747
"bcryptjs": "^3.0.2",
4848
"bit-mask": "^1.0.2",
49-
"body-parser": "^2.2.0",
5049
"classnames": "2.5.1",
5150
"concurrently": "^9.0.0",
5251
"connect-mongo": "^5.1.0",

src/proxy/index.ts

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import express, { Application } from 'express';
2-
import bodyParser from 'body-parser';
32
import http from 'http';
43
import https from 'https';
54
import fs from 'fs';
@@ -57,8 +56,6 @@ export const proxyPreparations = async () => {
5756
// just keep this async incase it needs async stuff in the future
5857
const createApp = async (): Promise<Application> => {
5958
const app = express();
60-
// Setup the proxy middleware
61-
app.use(bodyParser.raw(options));
6259
app.use('/', router);
6360
return app;
6461
};

src/proxy/routes/index.ts

Lines changed: 83 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
1-
import { Router } from 'express';
1+
import { Router, Request, Response, NextFunction } from 'express';
22
import proxy from 'express-http-proxy';
3+
import { PassThrough } from 'stream';
4+
import getRawBody from 'raw-body';
35
import { executeChain } from '../chain';
46
import { getProxyUrl } from '../../config';
57

@@ -48,93 +50,109 @@ const validGitRequest = (url: string, headers: any): boolean => {
4850
return false;
4951
}
5052
// https://www.git-scm.com/docs/http-protocol#_uploading_data
51-
return agent.startsWith('git/') && accept.startsWith('application/x-git-') ;
53+
return agent.startsWith('git/') && accept.startsWith('application/x-git-');
5254
}
5355
return false;
5456
};
5557

58+
const isPackPost = (req: Request) =>
59+
req.method === 'POST' &&
60+
// eslint-disable-next-line no-useless-escape
61+
/^\/[^\/]+\/[^\/]+\.git\/(?:git-upload-pack|git-receive-pack)$/.test(req.url);
62+
63+
const teeAndValidate = async (req: Request, res: Response, next: NextFunction) => {
64+
if (!isPackPost(req)) return next();
65+
66+
const proxyStream = new PassThrough();
67+
const pluginStream = new PassThrough();
68+
69+
req.pipe(proxyStream);
70+
req.pipe(pluginStream);
71+
72+
try {
73+
const buf = await getRawBody(pluginStream, { limit: '1gb' });
74+
(req as any).body = buf;
75+
const verdict = await executeChain(req, res);
76+
console.log('action processed');
77+
if (verdict.error || verdict.blocked) {
78+
let msg = '';
79+
80+
if (verdict.error) {
81+
msg = verdict.errorMessage!;
82+
console.error(msg);
83+
}
84+
if (verdict.blocked) {
85+
msg = verdict.blockedMessage!;
86+
}
87+
88+
res
89+
.set({
90+
'content-type': 'application/x-git-receive-pack-result',
91+
expires: 'Fri, 01 Jan 1980 00:00:00 GMT',
92+
pragma: 'no-cache',
93+
'cache-control': 'no-cache, max-age=0, must-revalidate',
94+
vary: 'Accept-Encoding',
95+
'x-frame-options': 'DENY',
96+
connection: 'close',
97+
})
98+
.status(200)
99+
.send(handleMessage(msg));
100+
return;
101+
}
102+
103+
(req as any).pipe = (dest: any, opts: any) => proxyStream.pipe(dest, opts);
104+
next();
105+
} catch (e) {
106+
console.error(e);
107+
proxyStream.destroy(e as Error);
108+
res.status(500).end('Proxy error');
109+
}
110+
};
111+
112+
router.use(teeAndValidate);
113+
56114
router.use(
57115
'/',
58116
proxy(getProxyUrl(), {
117+
parseReqBody: false,
59118
preserveHostHdr: false,
60-
filter: async function (req, res) {
61-
try {
62-
console.log('request url: ', req.url);
63-
console.log('host: ', req.headers.host);
64-
console.log('user-agent: ', req.headers['user-agent']);
65-
const gitPath = stripGitHubFromGitPath(req.url);
66-
if (gitPath === undefined || !validGitRequest(gitPath, req.headers)) {
67-
res.status(400).send('Invalid request received');
68-
return false;
69-
}
70-
71-
const action = await executeChain(req, res);
72-
console.log('action processed');
73-
74-
if (action.error || action.blocked) {
75-
res.set('content-type', 'application/x-git-receive-pack-result');
76-
res.set('expires', 'Fri, 01 Jan 1980 00:00:00 GMT');
77-
res.set('pragma', 'no-cache');
78-
res.set('cache-control', 'no-cache, max-age=0, must-revalidate');
79-
res.set('vary', 'Accept-Encoding');
80-
res.set('x-frame-options', 'DENY');
81-
res.set('connection', 'close');
82-
83-
let message = '';
84-
85-
if (action.error) {
86-
message = action.errorMessage!;
87-
console.error(message);
88-
}
89-
if (action.blocked) {
90-
message = action.blockedMessage!;
91-
}
92-
93-
const packetMessage = handleMessage(message);
94-
95-
console.log(req.headers);
96-
97-
res.status(200).send(packetMessage);
98-
99-
return false;
100-
}
101-
102-
return true;
103-
} catch (e) {
104-
console.error(e);
119+
120+
filter: async (req, res) => {
121+
console.log('request url: ', req.url);
122+
console.log('host: ', req.headers.host);
123+
console.log('user-agent: ', req.headers['user-agent']);
124+
const gitPath = stripGitHubFromGitPath(req.url);
125+
if (gitPath === undefined || !validGitRequest(gitPath, req.headers)) {
126+
res.status(400).send('Invalid request received');
105127
return false;
106128
}
129+
return true;
107130
},
131+
108132
proxyReqPathResolver: (req) => {
109133
const url = getProxyUrl() + req.originalUrl;
110134
console.log('Sending request to ' + url);
111135
return url;
112136
},
113-
proxyReqOptDecorator: function (proxyReqOpts) {
114-
return proxyReqOpts;
115-
},
116137

117-
proxyReqBodyDecorator: function (bodyContent, srcReq) {
118-
if (srcReq.method === 'GET') {
119-
return '';
120-
}
121-
return bodyContent;
122-
},
123-
124-
proxyErrorHandler: function (err, res, next) {
138+
proxyErrorHandler: (err, res, next) => {
125139
console.log(`ERROR=${err}`);
126140
next(err);
127141
},
128142
}),
129143
);
130144

131145
const handleMessage = (message: string): string => {
132-
const errorMessage = `\t${message}`;
133-
const len = 6 + new TextEncoder().encode(errorMessage).length;
134-
135-
const prefix = len.toString(16);
136-
const packetMessage = `${prefix.padStart(4, '0')}\x02${errorMessage}\n0000`;
137-
return packetMessage;
146+
const body = `\t${message}`;
147+
const len = (6 + Buffer.byteLength(body)).toString(16).padStart(4, '0');
148+
return `${len}\x02${body}\n0000`;
138149
};
139150

140-
export { router, handleMessage, validGitRequest, stripGitHubFromGitPath };
151+
export {
152+
router,
153+
handleMessage,
154+
validGitRequest,
155+
teeAndValidate,
156+
isPackPost,
157+
stripGitHubFromGitPath,
158+
};

0 commit comments

Comments
 (0)