Skip to content

Commit e5322fd

Browse files
committed
feat(proxy): introduce teeAndValidate middleware for pack POSTs
1 parent b5901d0 commit e5322fd

File tree

1 file changed

+74
-66
lines changed

1 file changed

+74
-66
lines changed

src/proxy/routes/index.ts

Lines changed: 74 additions & 66 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

@@ -33,7 +35,7 @@ const stripGitHubFromGitPath = (url: string): string | undefined => {
3335
* @return {boolean} If true, this is a valid and expected git request. Otherwise, false.
3436
*/
3537
const validGitRequest = (url: string, headers: any): boolean => {
36-
const { 'user-agent': agent, accept } = headers;
38+
const { 'user-agent': agent = '', accept = '' } = headers;
3739
if (['/info/refs?service=git-upload-pack', '/info/refs?service=git-receive-pack'].includes(url)) {
3840
// https://www.git-scm.com/docs/http-protocol#_discovering_references
3941
// We can only filter based on User-Agent since the Accept header is not
@@ -47,89 +49,95 @@ const validGitRequest = (url: string, headers: any): boolean => {
4749
return false;
4850
};
4951

52+
const isPackPost = (req: Request) =>
53+
req.method === 'POST' && /\/.*\.git\/(git-upload-pack|git-receive-pack)$/.test(req.url);
54+
55+
const teeAndValidate = async (req: Request, res: Response, next: NextFunction) => {
56+
if (!isPackPost(req)) return next();
57+
58+
const proxyStream = new PassThrough();
59+
const pluginStream = new PassThrough();
60+
61+
req.pipe(proxyStream);
62+
req.pipe(pluginStream);
63+
64+
try {
65+
const buf = await getRawBody(pluginStream, { limit: '1gb' });
66+
(req as any).body = buf;
67+
const verdict = await executeChain(req, res);
68+
console.log('action processed');
69+
70+
if (verdict.error || verdict.blocked) {
71+
const msg = verdict.error ? verdict.errorMessage! : verdict.blockedMessage!;
72+
res
73+
.set({
74+
'content-type': 'application/x-git-receive-pack-result',
75+
'transfer-encoding': 'chunked',
76+
expires: 'Fri, 01 Jan 1980 00:00:00 GMT',
77+
pragma: 'no-cache',
78+
'cache-control': 'no-cache, max-age=0, must-revalidate',
79+
vary: 'Accept-Encoding',
80+
'x-frame-options': 'DENY',
81+
connection: 'close',
82+
})
83+
.status(200)
84+
.send(handleMessage(msg));
85+
return;
86+
}
87+
88+
(req as any).pipe = (dest: any, opts: any) => proxyStream.pipe(dest, opts);
89+
next();
90+
} catch (e) {
91+
console.error(e);
92+
proxyStream.destroy(e as Error);
93+
res.status(500).end('Proxy error');
94+
}
95+
};
96+
97+
router.use(teeAndValidate);
98+
5099
router.use(
51100
'/',
52101
proxy(getProxyUrl(), {
102+
parseReqBody: false,
53103
preserveHostHdr: false,
54-
filter: async function (req, res) {
55-
try {
56-
console.log('request url: ', req.url);
57-
console.log('host: ', req.headers.host);
58-
console.log('user-agent: ', req.headers['user-agent']);
59-
const gitPath = stripGitHubFromGitPath(req.url);
60-
if (gitPath === undefined || !validGitRequest(gitPath, req.headers)) {
61-
res.status(400).send('Invalid request received');
62-
return false;
63-
}
64-
65-
const action = await executeChain(req, res);
66-
console.log('action processed');
67-
68-
if (action.error || action.blocked) {
69-
res.set('content-type', 'application/x-git-receive-pack-result');
70-
res.set('transfer-encoding', 'chunked');
71-
res.set('expires', 'Fri, 01 Jan 1980 00:00:00 GMT');
72-
res.set('pragma', 'no-cache');
73-
res.set('cache-control', 'no-cache, max-age=0, must-revalidate');
74-
res.set('vary', 'Accept-Encoding');
75-
res.set('x-frame-options', 'DENY');
76-
res.set('connection', 'close');
77-
78-
let message = '';
79-
80-
if (action.error) {
81-
message = action.errorMessage!;
82-
console.error(message);
83-
}
84-
if (action.blocked) {
85-
message = action.blockedMessage!;
86-
}
87-
88-
const packetMessage = handleMessage(message);
89-
90-
console.log(req.headers);
91-
92-
res.status(200).send(packetMessage);
93-
94-
return false;
95-
}
96-
97-
return true;
98-
} catch (e) {
99-
console.error(e);
104+
105+
filter: async (req, res) => {
106+
console.log('request url: ', req.url);
107+
console.log('host: ', req.headers.host);
108+
console.log('user-agent: ', req.headers['user-agent']);
109+
const gitPath = stripGitHubFromGitPath(req.url);
110+
if (gitPath === undefined || !validGitRequest(gitPath, req.headers)) {
111+
res.status(400).send('Invalid request received');
100112
return false;
101113
}
114+
return true;
102115
},
116+
103117
proxyReqPathResolver: (req) => {
104118
const url = getProxyUrl() + req.originalUrl;
105119
console.log('Sending request to ' + url);
106120
return url;
107121
},
108-
proxyReqOptDecorator: function (proxyReqOpts) {
109-
return proxyReqOpts;
110-
},
111122

112-
proxyReqBodyDecorator: function (bodyContent, srcReq) {
113-
if (srcReq.method === 'GET') {
114-
return '';
115-
}
116-
return bodyContent;
117-
},
118-
119-
proxyErrorHandler: function (err, res, next) {
123+
proxyErrorHandler: (err, res, next) => {
120124
console.log(`ERROR=${err}`);
121125
next(err);
122126
},
123127
}),
124128
);
125129

126130
const handleMessage = (message: string): string => {
127-
const errorMessage = `\t${message}`;
128-
const len = 6 + new TextEncoder().encode(errorMessage).length;
129-
130-
const prefix = len.toString(16);
131-
const packetMessage = `${prefix.padStart(4, '0')}\x02${errorMessage}\n0000`;
132-
return packetMessage;
131+
const body = `\t${message}`;
132+
const len = (6 + Buffer.byteLength(body)).toString(16).padStart(4, '0');
133+
return `${len}\x02${body}\n0000`;
133134
};
134135

135-
export { router, handleMessage, validGitRequest, stripGitHubFromGitPath };
136+
export {
137+
router,
138+
handleMessage,
139+
validGitRequest,
140+
teeAndValidate,
141+
isPackPost,
142+
stripGitHubFromGitPath,
143+
};

0 commit comments

Comments
 (0)