Skip to content

fix: full support for Protocol Version 2 #13

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jun 29, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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