Skip to content

Commit

Permalink
fix: full support for Protocol Version 2 (#13)
Browse files Browse the repository at this point in the history
BREAKING CHANGE: I ran into issues with the `git-http-backend` npm module when trying to add support for Git Protocol Version 2 support, so I ended up ditching it and modifying my code to replace that functionality. This change doesn't break any isomorphic-git tests, but I'm releasing this as a major version bump just to be cautious.
  • Loading branch information
billiegoose authored Jun 29, 2020
1 parent 3200a5a commit 53e2cbe
Show file tree
Hide file tree
Showing 3 changed files with 103 additions and 81 deletions.
167 changes: 102 additions & 65 deletions middleware.js
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand All @@ -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)
Expand All @@ -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))
}
}
}

Expand Down
16 changes: 1 addition & 15 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 0 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down

0 comments on commit 53e2cbe

Please sign in to comment.