From 277dc4c35d00309ea839f0bb0afc0e79302cb00b Mon Sep 17 00:00:00 2001 From: William Hilton Date: Mon, 29 Jun 2020 14:23:18 -0400 Subject: [PATCH] fix: full support for Protocol Version 2 --- middleware.js | 167 ++++++++++++++++++++++++++++------------------ package-lock.json | 16 +---- package.json | 1 - 3 files changed, 103 insertions(+), 81 deletions(-) diff --git a/middleware.js b/middleware.js index 0c23151..5ec912a 100644 --- a/middleware.js +++ b/middleware.js @@ -6,13 +6,35 @@ var url = require('url') var auth = require('basic-auth') var chalk = require('chalk') var fixturez = require('fixturez') -var backend = require('git-http-backend') var htpasswd = require('htpasswd-js') function pad (str) { return (str + ' ').slice(0, 7) } + +function matchInfo (req) { + var u = url.parse(req.url) + if (req.method === 'GET' && u.pathname.endsWith('/info/refs')) { + return true + } else { + return false + } +} + +function matchService (req) { + var u = url.parse(req.url, true) + if (req.method === 'GET' && u.pathname.endsWith('/info/refs')) { + return u.query.service + } + if (req.method === 'POST' && req.headers['content-type'] === 'application/x-git-upload-pack-request') { + return 'git-upload-pack' + } + if (req.method === 'POST' && req.headers['content-type'] === 'application/x-git-receive-pack-request') { + return 'git-receive-pack' + } +} + function factory (config) { if (!config.root) throw new Error('Missing required "gitHttpServer.root" config option') if (!config.route) throw new Error('Missing required "gitHttpServer.route" config option') @@ -23,17 +45,19 @@ function factory (config) { function getGitDir (req) { var u = url.parse(req.url) if (u.pathname.startsWith(config.route)) { - if (req.method === 'GET' && u.pathname.endsWith('/info/refs')) { + const info = matchInfo(req) + if (info) { let gitdir = u.pathname.replace(config.route, '').replace(/\/info\/refs$/, '').replace(/^\//, '') let fixtureName = path.posix.basename(gitdir) return f.find(fixtureName) } - if (req.method === 'POST' && req.headers['content-type'] === 'application/x-git-upload-pack-request') { + const service = matchService(req) + if (service === 'git-upload-pack') { let gitdir = u.pathname.replace(config.route, '').replace(/\/git-upload-pack$/, '').replace(/^\//, '') let fixtureName = path.posix.basename(gitdir) return f.find(fixtureName) } - if (req.method === 'POST' && req.headers['content-type'] === 'application/x-git-receive-pack-request') { + if (service === 'git-receive-pack') { let gitdir = u.pathname.replace(config.route, '').replace(/\/git-receive-pack$/, '').replace(/^\//, '') let fixtureName = path.posix.basename(gitdir) return f.copy(fixtureName) @@ -43,78 +67,91 @@ function factory (config) { } return async function middleware (req, res, next) { - // handle pre-flight OPTIONS - if (req.method === 'OPTIONS') { - res.statusCode = 204 - res.end('') - console.log(chalk.green('[git-http-server] 204 ' + pad(req.method) + ' ' + req.url)) - return - } - if (!next) next = () => void(0) - try { - var gitdir = getGitDir(req) - } catch (err) { - res.statusCode = 404 - res.end(err.message + '\n') - console.log(chalk.red('[git-http-server] 404 ' + pad(req.method) + ' ' + req.url)) - return - } - if (gitdir == null) return next() - - // Check for a .htaccess file - let data = null try { - data = fs.readFileSync(path.join(gitdir, '.htpasswd'), 'utf8') - } catch (err) { - // no .htaccess file, proceed without authentication - } - if (data) { - // The previous line would have failed if there wasn't an .htaccess file, so - // we must treat this as protected. - let cred = auth.parse(req.headers['authorization']) - if (cred === undefined) { - res.statusCode = 401 - // The default reason phrase used in Node is "Unauthorized", but - // we will use "Authorization Required" to match what Github uses. - res.statusMessage = 'Authorization Required' - res.setHeader('WWW-Authenticate', 'Basic') - res.end('Unauthorized' + '\n') - console.log(chalk.green('[git-http-server] 401 ' + pad(req.method) + ' ' + req.url)) + // handle pre-flight OPTIONS + if (req.method === 'OPTIONS') { + res.statusCode = 204 + res.end('') + console.log(chalk.green('[git-http-server] 204 ' + pad(req.method) + ' ' + req.url)) return } - let valid = await htpasswd.authenticate({ - username: cred.name, - password: cred.pass, - data - }) - if (!valid) { - res.statusCode = 401 - // The default reason phrase used in Node is "Unauthorized", but - // we will use "Authorization Required" to match what Github uses. - res.statusMessage = 'Authorization Required' - res.setHeader('WWW-Authenticate', 'Basic') - res.end('Bad credentials' + '\n') - console.log(chalk.green('[git-http-server] 401 ' + pad(req.method) + ' ' + req.url)) + if (!next) next = () => void(0) + try { + var gitdir = getGitDir(req) + } catch (err) { + res.statusCode = 404 + res.end(err.message + '\n') + console.log(chalk.red('[git-http-server] 404 ' + pad(req.method) + ' ' + req.url)) return } - } + if (gitdir == null) return next() - req.pipe(backend(req.url, function (err, service) { - if (err) { - res.statusCode = 500 - res.end(err + '\n') - console.log(chalk.red('[git-http-server] 500 ' + pad(req.method) + ' ' + req.url)) - return + // Check for a .htaccess file + let data = null + try { + data = fs.readFileSync(path.join(gitdir, '.htpasswd'), 'utf8') + } catch (err) { + // no .htaccess file, proceed without authentication + } + if (data) { + // The previous line would have failed if there wasn't an .htaccess file, so + // we must treat this as protected. + let cred = auth.parse(req.headers['authorization']) + if (cred === undefined) { + res.statusCode = 401 + // The default reason phrase used in Node is "Unauthorized", but + // we will use "Authorization Required" to match what Github uses. + res.statusMessage = 'Authorization Required' + res.setHeader('WWW-Authenticate', 'Basic') + res.end('Unauthorized' + '\n') + console.log(chalk.green('[git-http-server] 401 ' + pad(req.method) + ' ' + req.url)) + return + } + let valid = await htpasswd.authenticate({ + username: cred.name, + password: cred.pass, + data + }) + if (!valid) { + res.statusCode = 401 + // The default reason phrase used in Node is "Unauthorized", but + // we will use "Authorization Required" to match what Github uses. + res.statusMessage = 'Authorization Required' + res.setHeader('WWW-Authenticate', 'Basic') + res.end('Bad credentials' + '\n') + console.log(chalk.green('[git-http-server] 401 ' + pad(req.method) + ' ' + req.url)) + return + } } + const info = matchInfo(req) + const service = matchService(req) const env = req.headers['git-protocol'] ? { GIT_PROTOCOL: req.headers['git-protocol'] } : {} - res.setHeader('content-type', service.type) + const args = ['--stateless-rpc' ]; + if (info) args.push('--advertise-refs') + args.push(gitdir) + + if (info) { + res.setHeader('content-type', `application/x-${service}-advertisement`) + function pack (s) { + var n = (4 + s.length).toString(16); + return Array(4 - n.length + 1).join('0') + n + s; + } + res.write(pack('# service=' + service + '\n') + '0000'); + } else { + res.setHeader('content-type', `application/x-${service}-result`) + } + + const ps = spawn(service, args, { env }) + req.pipe(ps.stdin) + ps.stdout.pipe(res) console.log(chalk.green('[git-http-server] 200 ' + pad(req.method) + ' ' + req.url)) - // console.log('[git-http-server] ' + service.cmd + ' ' + service.args.concat(gitdir).join(' ')) - var ps = spawn(service.cmd, service.args.concat(gitdir), { env }) - ps.stdout.pipe(service.createStream()).pipe(ps.stdin) - })).pipe(res) + } catch (err) { + res.statusCode = 500 + res.end(err + '\n') + console.log(chalk.red('[git-http-server] 500 ' + pad(req.method) + ' ' + req.url)) + } } } diff --git a/package-lock.json b/package-lock.json index 1413057..6154565 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "git-http-mock-server", - "version": "0.0.0", + "version": "0.0.0-development", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -2593,15 +2593,6 @@ "integrity": "sha1-3BXKHGcjh8p2vTesCjlbogQqLCg=", "dev": true }, - "git-http-backend": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/git-http-backend/-/git-http-backend-1.0.2.tgz", - "integrity": "sha1-3AHkKEIJNTBkRTpw+S+J3gg06xA=", - "requires": { - "git-side-band-message": "~0.0.3", - "inherits": "~2.0.1" - } - }, "git-log-parser": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/git-log-parser/-/git-log-parser-1.2.0.tgz", @@ -2637,11 +2628,6 @@ } } }, - "git-side-band-message": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/git-side-band-message/-/git-side-band-message-0.0.3.tgz", - "integrity": "sha1-uKU0jC3L8ZSf0pXFBgFOJsPyakY=" - }, "glob": { "version": "7.1.6", "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", diff --git a/package.json b/package.json index a3cf616..28495d8 100644 --- a/package.json +++ b/package.json @@ -34,7 +34,6 @@ "chalk": "^2.4.1", "daemonize-process": "^1.0.9", "fixturez": "^1.1.0", - "git-http-backend": "^1.0.2", "htpasswd-js": "^1.0.2", "micro-cors": "^0.1.1", "minimisted": "^2.0.0",