├── .gitignore ├── demo.git ├── HEAD ├── index ├── description ├── packed-refs ├── config ├── info │ └── exclude └── logs │ ├── HEAD │ └── refs │ └── heads │ └── master ├── logo.js ├── http-daemon.js ├── http-server.js ├── parse-request.js ├── package.json ├── cors.js ├── identify-request.js ├── daemon.js ├── lookup.js ├── sandbox.js ├── pre-receive-hook.js ├── demo └── .hooks │ └── pre-receive.js └── middleware.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /demo.git/HEAD: -------------------------------------------------------------------------------- 1 | ref: refs/heads/master 2 | -------------------------------------------------------------------------------- /demo.git/index: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/isomorphic-git/server/HEAD/demo.git/index -------------------------------------------------------------------------------- /demo.git/description: -------------------------------------------------------------------------------- 1 | Unnamed repository; edit this file 'description' to name the repository. 2 | -------------------------------------------------------------------------------- /logo.js: -------------------------------------------------------------------------------- 1 | var figlet = require('figlet'); 2 | 3 | module.exports = figlet.textSync('GitServer', { font: 'Cyberlarge' }) 4 | -------------------------------------------------------------------------------- /demo.git/packed-refs: -------------------------------------------------------------------------------- 1 | # pack-refs with: peeled fully-peeled sorted 2 | 59eabecab1988d343a2cc8c054a56d5e6cb3bd89 refs/heads/master 3 | -------------------------------------------------------------------------------- /demo.git/config: -------------------------------------------------------------------------------- 1 | [core] 2 | repositoryformatversion = 0 3 | filemode = true 4 | bare = true 5 | ignorecase = true 6 | precomposeunicode = true 7 | -------------------------------------------------------------------------------- /http-daemon.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | const cmdName = 'git-server' 3 | const target = require.resolve('./http-server.js') 4 | require('./daemon.js')(cmdName, target) 5 | -------------------------------------------------------------------------------- /demo.git/info/exclude: -------------------------------------------------------------------------------- 1 | # git ls-files --others --exclude-from=.git/info/exclude 2 | # Lines that start with '#' are comments. 3 | # For a project mostly in C, the following would be a good set of 4 | # exclude patterns (uncomment them if you want to use them): 5 | # *.[oa] 6 | # *~ 7 | .DS_Store 8 | -------------------------------------------------------------------------------- /http-server.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | var http = require('http') 3 | var factory = require('./middleware') 4 | var cors = require('./cors') 5 | 6 | var config = {} 7 | 8 | var server = http.createServer(cors(factory(config))) 9 | server.listen(process.env.GIT_HTTP_MOCK_SERVER_PORT || 8174) 10 | 11 | console.log(require('./logo.js')) 12 | -------------------------------------------------------------------------------- /parse-request.js: -------------------------------------------------------------------------------- 1 | function infoRefs (req, u) { 2 | const gitdir = u.pathname.replace(/\/info\/refs$/, '').replace(/^\//, '') 3 | return { gitdir, service: u.query.service } 4 | } 5 | 6 | function pull (req, u) { 7 | const gitdir = u.pathname.replace(/\/git-upload-pack$/, '').replace(/^\//, '') 8 | return { gitdir, service: 'git-receive-pack' } 9 | } 10 | 11 | function push (req, u) { 12 | const gitdir = u.pathname.replace(/\/git-receive-pack$/, '').replace(/^\//, '') 13 | return { gitdir, service: 'git-receive-pack' } 14 | } 15 | 16 | module.exports = { 17 | infoRefs, 18 | pull, 19 | push 20 | } 21 | -------------------------------------------------------------------------------- /demo.git/logs/HEAD: -------------------------------------------------------------------------------- 1 | 0000000000000000000000000000000000000000 71c666705d861b556d73f2badddaca0cfcf5e930 William Hilton 1574741587 -0500 commit (initial): Initial commit 2 | 0000000000000000000000000000000000000000 b1f9a9a2689e95d8aaf9d2ed5513fe78f4c53901 William Hilton 1575084779 -0500 commit (initial): Initial commit 3 | b1f9a9a2689e95d8aaf9d2ed5513fe78f4c53901 d76ea2f1501e44ac5c29a091e17903b6a33fc57f William Hilton 1575685262 -0500 commit: add pre-receive hook 4 | d76ea2f1501e44ac5c29a091e17903b6a33fc57f ee27a42768a70831127fca08ccc9df7b13f0f81e William Hilton 1575686304 -0500 commit (amend): add pre-receive hook 5 | ee27a42768a70831127fca08ccc9df7b13f0f81e 59eabecab1988d343a2cc8c054a56d5e6cb3bd89 William Hilton 1575686347 -0500 commit (amend): add pre-receive hook 6 | -------------------------------------------------------------------------------- /demo.git/logs/refs/heads/master: -------------------------------------------------------------------------------- 1 | 0000000000000000000000000000000000000000 71c666705d861b556d73f2badddaca0cfcf5e930 William Hilton 1574741587 -0500 commit (initial): Initial commit 2 | 0000000000000000000000000000000000000000 b1f9a9a2689e95d8aaf9d2ed5513fe78f4c53901 William Hilton 1575084779 -0500 commit (initial): Initial commit 3 | b1f9a9a2689e95d8aaf9d2ed5513fe78f4c53901 d76ea2f1501e44ac5c29a091e17903b6a33fc57f William Hilton 1575685262 -0500 commit: add pre-receive hook 4 | d76ea2f1501e44ac5c29a091e17903b6a33fc57f ee27a42768a70831127fca08ccc9df7b13f0f81e William Hilton 1575686304 -0500 commit (amend): add pre-receive hook 5 | ee27a42768a70831127fca08ccc9df7b13f0f81e 59eabecab1988d343a2cc8c054a56d5e6cb3bd89 William Hilton 1575686347 -0500 commit (amend): add pre-receive hook 6 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@isomorphic-git/server", 3 | "version": "0.0.0", 4 | "description": "git smart HTTP server built using isomorphic-git", 5 | "main": "middleware.js", 6 | "bin": { 7 | "git-server": "http-daemon.js" 8 | }, 9 | "scripts": { 10 | "test": "echo \"No tests\"" 11 | }, 12 | "keywords": [], 13 | "author": "William Hilton ", 14 | "license": "MIT", 15 | "dependencies": { 16 | "@isomorphic-git/pgp-plugin": "0.0.7", 17 | "basic-auth": "^2.0.0", 18 | "buffer-equal-constant-time": "^1.0.1", 19 | "chalk": "^2.4.1", 20 | "daemonize-process": "^1.0.9", 21 | "figlet": "^1.2.4", 22 | "micro-cors": "^0.1.1", 23 | "minimisted": "^2.0.0", 24 | "simple-get": "^3.1.0", 25 | "tree-kill": "^1.2.0", 26 | "vm2": "^3.8.4" 27 | }, 28 | "publishConfig": { 29 | "access": "public" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /cors.js: -------------------------------------------------------------------------------- 1 | const origin = process.env.GIT_HTTP_MOCK_SERVER_ALLOW_ORIGIN 2 | const allowHeaders = [ 3 | 'accept-encoding', 4 | 'accept-language', 5 | 'accept', 6 | 'access-control-allow-origin', 7 | 'authorization', 8 | 'cache-control', 9 | 'connection', 10 | 'content-length', 11 | 'content-type', 12 | 'dnt', 13 | 'pragma', 14 | 'range', 15 | 'referer', 16 | 'user-agent', 17 | 'x-http-method-override', 18 | 'x-requested-with', 19 | ] 20 | const exposeHeaders = [ 21 | 'accept-ranges', 22 | 'age', 23 | 'cache-control', 24 | 'content-length', 25 | 'content-language', 26 | 'content-type', 27 | 'date', 28 | 'etag', 29 | 'expires', 30 | 'last-modified', 31 | 'pragma', 32 | 'server', 33 | 'transfer-encoding', 34 | 'vary', 35 | 'x-github-request-id', 36 | ] 37 | const allowMethods = [ 38 | 'POST', 39 | 'GET', 40 | 'OPTIONS' 41 | ] 42 | const cors = require('micro-cors')({ 43 | allowHeaders, 44 | exposeHeaders, 45 | allowMethods, 46 | allowCredentials: false, 47 | origin 48 | }) 49 | 50 | module.exports = cors -------------------------------------------------------------------------------- /identify-request.js: -------------------------------------------------------------------------------- 1 | function preflightInfoRefs (req, u) { 2 | return req.method === 'OPTIONS' && u.pathname.endsWith('/info/refs') && (u.query.service === 'git-upload-pack' || u.query.service === 'git-receive-pack') 3 | } 4 | 5 | function infoRefs (req, u) { 6 | return req.method === 'GET' && u.pathname.endsWith('/info/refs') && (u.query.service === 'git-upload-pack' || u.query.service === 'git-receive-pack') 7 | } 8 | 9 | function preflightPull (req, u) { 10 | return req.method === 'OPTIONS' && req.headers['access-control-request-headers'].includes('content-type') && u.pathname.endsWith('git-upload-pack') 11 | } 12 | 13 | function pull (req, u) { 14 | return req.method === 'POST' && req.headers['content-type'] === 'application/x-git-upload-pack-request' && u.pathname.endsWith('git-upload-pack') 15 | } 16 | 17 | function preflightPush (req, u) { 18 | return req.method === 'OPTIONS' && req.headers['access-control-request-headers'].includes('content-type') && u.pathname.endsWith('git-receive-pack') 19 | } 20 | 21 | function push (req, u) { 22 | return req.method === 'POST' && req.headers['content-type'] === 'application/x-git-receive-pack-request' && u.pathname.endsWith('git-receive-pack') 23 | } 24 | 25 | module.exports = { 26 | preflightInfoRefs, 27 | infoRefs, 28 | preflightPull, 29 | pull, 30 | preflightPush, 31 | push 32 | } 33 | -------------------------------------------------------------------------------- /daemon.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | const fs = require('fs') 3 | const path = require('path') 4 | const {spawn} = require('child_process') 5 | const kill = require('tree-kill') 6 | const minimisted = require('minimisted') 7 | 8 | module.exports = function (cmdName, target) { 9 | const args = [ 10 | target 11 | ] 12 | 13 | async function main({_: [cmd]}) { 14 | switch (cmd) { 15 | case 'start': { 16 | require('daemonize-process')() 17 | let server = spawn( 18 | 'node', args, 19 | { 20 | stdio: 'inherit', 21 | windowsHide: true, 22 | } 23 | ) 24 | fs.writeFileSync( 25 | path.join(process.cwd(), `${cmdName}.pid`), 26 | String(process.pid), 27 | 'utf8' 28 | ) 29 | process.on('exit', server.kill) 30 | return 31 | } 32 | case 'stop': { 33 | let pid 34 | try { 35 | pid = fs.readFileSync( 36 | path.join(process.cwd(), `${cmdName}.pid`), 37 | 'utf8' 38 | ); 39 | } catch (err) { 40 | console.log(`No ${cmdName}.pid file`) 41 | return 42 | } 43 | pid = parseInt(pid) 44 | console.log('killing', pid) 45 | kill(pid, (err) => { 46 | if (err) { 47 | console.log(err) 48 | } else { 49 | fs.unlinkSync(path.join(process.cwd(), `${cmdName}.pid`)) 50 | } 51 | }) 52 | return 53 | } 54 | default: { 55 | require(target) 56 | } 57 | } 58 | } 59 | 60 | minimisted(main) 61 | } -------------------------------------------------------------------------------- /lookup.js: -------------------------------------------------------------------------------- 1 | // TODO: replace with a LRU cache 2 | const cache = {} 3 | const get = require('simple-get') 4 | 5 | module.exports = (username) => 6 | fetch() 7 | .then(res => res.json()) 8 | .then(json => { 9 | return json.map(data => data.raw_key) 10 | }) 11 | 12 | async function usernames2keys(usernames) { 13 | const all = await Promise.all( 14 | usernames.map(username => new Promise((resolve, reject) => { 15 | get.concat({ 16 | url: `https://api.github.com/users/${username}/gpg_keys`, 17 | json: true, 18 | headers: { 19 | 'user-agent': 'GitHub PGP KeyFinder' 20 | } 21 | }, (err, res, data) => { 22 | if (err) return reject(err) 23 | return resolve(data.map(i => i.raw_key)) 24 | }) 25 | })) 26 | ) 27 | return all.reduce((a, b) => a.concat(b)).filter(Boolean) 28 | } 29 | 30 | async function email2usernames(email) { 31 | return new Promise((resolve, reject) => { 32 | get.concat({ 33 | url: `https://api.github.com/search/users?q=${email}+in:email`, 34 | json: true, 35 | headers: { 36 | 'user-agent': 'GitHub PGP KeyFinder' 37 | } 38 | }, (err, res, data) => { 39 | if (err) return reject(err) 40 | if (data.total_count === 0) { 41 | return reject(new Error(`Could not find the GitHub user publicly associated with the email address "${email}"`)) 42 | } else if (data.total_count > 0) { 43 | return resolve(data.items.map(i => i.login)) 44 | } else { 45 | return reject('Unexpected value for data.total_count returned by GitHub API') 46 | } 47 | }) 48 | }) 49 | } 50 | 51 | async function lookup(email) { 52 | if (cache[email]) return cache[email] 53 | const usernames = await email2usernames(email) 54 | const keys = await usernames2keys(usernames) 55 | cache[email] = keys 56 | return cache[email] 57 | } 58 | 59 | function demote(email, key) { 60 | const i = cache[email].indexOf(key) 61 | cache[email].push(cache[email].splice(i, 1)[0]) 62 | } 63 | 64 | module.exports.lookup = lookup 65 | module.exports.demote = demote 66 | 67 | if (!module.parent) { 68 | lookup('wmhilton@gmail.com').then(console.log).then(() => lookup('wmhilton@gmail.com')).then(console.log) 69 | } -------------------------------------------------------------------------------- /sandbox.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const EventEmitter = require('events').EventEmitter 3 | 4 | const { VM, VMScript } = require('vm2'); 5 | const git = require('isomorphic-git') 6 | const { pgp } = require('@isomorphic-git/pgp-plugin') 7 | 8 | const { lookup, demote } = require('./lookup.js') 9 | 10 | const curry = ({ core, dir, gitdir }) => fn => argObject => fn({ ...argObject, core, dir, gitdir }) 11 | 12 | const sandbox = ({ name, core, dir, gitdir, res, oids, updates, script }) => { 13 | let ee = new EventEmitter() 14 | plugincore = git.cores.create(core) 15 | plugincore.set('emitter', ee) 16 | plugincore.set('fs', fs) 17 | plugincore.set('pgp', pgp) 18 | 19 | const $ = curry({ core, dir, gitdir }) 20 | const $git = { 21 | E: { ...git.E }, 22 | eventEmitter: ee, 23 | expandOid: $(git.expandOid), 24 | expandRef: $(git.expandRef), 25 | findMergeBase: $(git.findMergeBase), 26 | getRemoteInfo: $(git.getRemoteInfo), 27 | hashBlob: $(git.hashBlob), 28 | isDescendent: $(git.isDescendent), 29 | listBranches: $(git.listBranches), 30 | listFiles: $(git.listFiles), 31 | listRemotes: $(git.listRemotes), 32 | listTags: $(git.listTags), 33 | log: $(git.log), 34 | readObject: $(git.readObject), 35 | resolveRef: $(git.resolveRef), 36 | serveReceivePack: $(git.serveReceivePack), 37 | verify: $(git.verify), 38 | walkBeta2: $(git.walkBeta2), 39 | } 40 | const $res = { 41 | write: res.write.bind(res) 42 | } 43 | 44 | return new Promise((resolve, reject) => { 45 | const $console = { 46 | log: async (...args) => { 47 | res.write(await git.serveReceivePack({ type: 'print', message: args.join() })) 48 | }, 49 | error: (...args) => { 50 | reject(new Error(args.join())) 51 | } 52 | } 53 | const vm = new VM({ 54 | timeout: 10000, 55 | eval: false, 56 | wasm: false, 57 | sandbox: { 58 | updates, 59 | oids, 60 | git: $git, 61 | pgp: { lookup, demote }, 62 | done: (err) => err ? reject(err) : resolve(), 63 | console: $console, 64 | } 65 | }); 66 | try { 67 | script = new VMScript(script, name).compile(); 68 | } catch (err) { 69 | reject(err); 70 | } 71 | try { 72 | vm.run(script); 73 | } catch (e) { 74 | reject(e); 75 | } 76 | }) 77 | 78 | } 79 | 80 | module.exports = { 81 | sandbox 82 | } 83 | // console.log(runVM({ core: 'default', dir: '', gitdir: '', script: `String(Object.keys(git))` })) 84 | -------------------------------------------------------------------------------- /pre-receive-hook.js: -------------------------------------------------------------------------------- 1 | (async () => { 2 | console.log('Wish me luck!') 3 | 4 | function abbr (oid) { 5 | return oid.slice(0, 7) 6 | } 7 | 8 | // Verify objects (ideally we'd do this _before_ moving it into the repo... but I think we'd need a custom 'fs' implementation with overlays) 9 | console.log('\nVerifying objects...\n') 10 | let i = 0 11 | 12 | for (const oid of oids) { 13 | i++ 14 | console.log(`\rVerifying object ${i}/${oids.length}`) 15 | const { type, object } = await git.readObject({ oid }) 16 | if (type === 'commit' || type === 'tag') { 17 | const email = type === 'commit' ? object.author.email : object.tagger.email 18 | console.log(`\nVerifying ${type} ${abbr(oid)} by ${email}: `) 19 | let keys 20 | try { 21 | keys = await pgp.lookup(email) 22 | } catch (e) { 23 | console.fatal(`no keys found 👎\n`) 24 | return 25 | } 26 | if (keys.length === 0) { 27 | console.log(`no keys found 👎\n`) 28 | console.fatal(`\nSignature verification failed for ${type} ${abbr(oid)}. No PGP keys could be found for ${email}.\n`) 29 | return 30 | } 31 | let ok = false 32 | for (const key of keys) { 33 | let result 34 | try { 35 | result = await git.verify({ ref: oid, publicKeys: key }) 36 | } catch (e) { 37 | if (e.code && e.code === git.E.NoSignatureError) { 38 | console.log(`no signature 👎\n`) 39 | console.fatal(e.message + ` 40 | 41 | This server's policy is to only accept GPG-signed commits. 42 | Learn how you can create a GPG key and configure git to sign commits here: 43 | https://help.github.com/en/github/authenticating-to-github/managing-commit-signature-verification 44 | `) 45 | return 46 | } else { 47 | console.fatal(e.message) 48 | return 49 | } 50 | } 51 | if (result === false) { 52 | pgp.demote(email, key) 53 | } else { 54 | console.log(`signed with ${result[0]} 👍\n`) 55 | ok = true 56 | break 57 | } 58 | } 59 | if (!ok) { 60 | console.log(`no keys matched 👎\n`) 61 | console.fatal(`\nSignature verification failed for ${type} ${abbr(oid)}. It was not signed with a key publicly associated with the email address "${email}". 62 | 63 | Learn how you can associate your GPG key with your email account using GitHub here: 64 | https://help.github.com/en/github/authenticating-to-github/adding-a-new-gpg-key-to-your-github-account 65 | `) 66 | return 67 | } 68 | } 69 | } 70 | 71 | console.log(`\nVerification complete`) 72 | done() 73 | })() 74 | -------------------------------------------------------------------------------- /demo/.hooks/pre-receive.js: -------------------------------------------------------------------------------- 1 | (async () => { 2 | function abbr (oid) { 3 | return oid.slice(0, 7) 4 | } 5 | 6 | // Verify objects (ideally we'd do this _before_ moving it into the repo... but I think we'd need a custom 'fs' implementation with overlays) 7 | console.log('\nVerifying objects...\n') 8 | let i = 0 9 | 10 | for (const oid of oids) { 11 | i++ 12 | console.log(`\rVerifying object ${i}/${oids.length}`) 13 | const { type, object } = await git.readObject({ oid }) 14 | if (type === 'commit' || type === 'tag') { 15 | const email = type === 'commit' ? object.author.email : object.tagger.email 16 | console.log(`\nVerifying ${type} ${abbr(oid)} by ${email}: `) 17 | let keys 18 | try { 19 | keys = await pgp.lookup(email) 20 | } catch (e) { 21 | console.log(`no keys found 👎\n`) 22 | console.error(`\nSignature verification failed for ${type} ${abbr(oid)}. Key lookup for ${email} threw an error.\n`) 23 | return 24 | } 25 | if (keys.length === 0) { 26 | console.log(`no keys found 👎\n`) 27 | console.error(`\nSignature verification failed for ${type} ${abbr(oid)}. No PGP keys could be found for ${email}.\n`) 28 | return 29 | } 30 | let ok = false 31 | for (const key of keys) { 32 | let result 33 | try { 34 | result = await git.verify({ ref: oid, publicKeys: key }) 35 | } catch (e) { 36 | if (e.code && e.code === git.E.NoSignatureError) { 37 | console.log(`no signature 👎\n`) 38 | console.error(e.message + ` 39 | 40 | This server's policy is to only accept GPG-signed commits. 41 | Learn how you can create a GPG key and configure git to sign commits here: 42 | https://help.github.com/en/github/authenticating-to-github/managing-commit-signature-verification 43 | `) 44 | return 45 | } else { 46 | console.error(e.message + '\n' + e.stack) 47 | return 48 | } 49 | } 50 | if (result === false) { 51 | pgp.demote(email, key) 52 | } else { 53 | console.log(`signed with ${result[0]} 👍\n`) 54 | ok = true 55 | break 56 | } 57 | } 58 | if (!ok) { 59 | console.log(`no keys matched 👎\n`) 60 | console.error(`\nSignature verification failed for ${type} ${abbr(oid)}. It was not signed with a key publicly associated with the email address "${email}". 61 | 62 | Learn how you can associate your GPG key with your email account using GitHub here: 63 | https://help.github.com/en/github/authenticating-to-github/adding-a-new-gpg-key-to-your-github-account 64 | `) 65 | return 66 | } 67 | } 68 | } 69 | 70 | console.log(`\nVerification complete`) 71 | done() 72 | })() 73 | -------------------------------------------------------------------------------- /middleware.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const fp = require('fs').promises 3 | const path = require('path') 4 | const url = require('url') 5 | const EventEmitter = require('events').EventEmitter 6 | const { E, indexPack, plugins, readObject, resolveRef, serveInfoRefs, serveReceivePack, parseReceivePackRequest } = require('isomorphic-git') 7 | const { pgp } = require('@isomorphic-git/pgp-plugin') 8 | 9 | let ee = new EventEmitter() 10 | plugins.set('emitter', ee) 11 | plugins.set('fs', fs) 12 | plugins.set('pgp', pgp) 13 | 14 | const chalk = require('chalk') 15 | const is = require('./identify-request.js') 16 | const parse = require('./parse-request.js') 17 | const { lookup, demote } = require('./lookup.js') 18 | const { sandbox } = require('./sandbox.js') 19 | 20 | function pad (str) { 21 | return (str + ' ').slice(0, 7) 22 | } 23 | 24 | function abbr (oid) { 25 | return oid.slice(0, 7) 26 | } 27 | 28 | const sleep = ms => new Promise(cb => setTimeout(cb, ms)) 29 | 30 | const tick = () => new Promise(cb => process.nextTick(cb)) 31 | 32 | function logIncoming(req, res) { 33 | const color = res.statusCode > 399 ? chalk.red : chalk.green 34 | console.log(` ${pad(req.method)} ${req.url}`) 35 | return false 36 | } 37 | 38 | function logOutgoing(req, res) { 39 | const color = res.statusCode > 399 ? chalk.red : chalk.green 40 | console.log(color(`${res.statusCode} ${pad(req.method)} ${req.url}`)) 41 | return false 42 | } 43 | 44 | function factory (config) { 45 | fp.mkdir(path.join(__dirname, 'quarantine'), { recursive: true }) 46 | return async function middleware (req, res, next) { 47 | const u = url.parse(req.url, true) 48 | if (!next) next = () => void(0) 49 | 50 | logIncoming(req, res) 51 | 52 | if (is.preflightInfoRefs(req, u)) { 53 | res.statusCode = 204 54 | res.end('') 55 | } else if (is.preflightPull(req, u)) { 56 | res.statusCode = 204 57 | res.end('') 58 | } else if (is.preflightPush(req, u)) { 59 | res.statusCode = 204 60 | res.end('') 61 | } else if (is.infoRefs(req, u)) { 62 | let { gitdir, service } = parse.infoRefs(req, u) 63 | gitdir = path.join(__dirname, gitdir) 64 | const { headers, response } = await serveInfoRefs({ fs, gitdir, service }) 65 | for (const header in headers) { 66 | res.setHeader(header, headers[header]) 67 | } 68 | res.statusCode = 200 69 | for (const buffer of response) { 70 | res.write(buffer) 71 | } 72 | res.end() 73 | } else if (is.pull(req, u)) { 74 | const { gitdir } = parse.pull(req, u) 75 | res.statusCode = 500 76 | res.end('Unsupported operation\n') 77 | } else if (is.push(req, u)) { 78 | let { gitdir, service } = parse.push(req, u) 79 | try { 80 | let { capabilities, updates, packfile } = await parseReceivePackRequest(req) 81 | 82 | // Save packfile to quarantine folder 83 | const dir = await fp.mkdtemp(path.join(__dirname, 'quarantine', gitdir + '-')) 84 | let filepath = 'pack-.pack' 85 | const stream = fs.createWriteStream(path.join(dir, filepath)) 86 | let last20 87 | for await (const buffer of packfile) { 88 | if (buffer) { 89 | last20 = buffer.slice(-20) 90 | stream.write(buffer) 91 | } 92 | } 93 | stream.end() 94 | if (last20 && last20.length === 20) { 95 | last20 = last20.toString('hex') 96 | const oldfilepath = filepath 97 | filepath = `pack-${last20}.pack` 98 | await fp.rename(path.join(dir, oldfilepath), path.join(dir, filepath)) 99 | } 100 | const core = gitdir + '-' + String(Math.random()).slice(2, 8) 101 | gitdir = path.join(__dirname, gitdir) 102 | 103 | // send HTTP response headers 104 | const { headers } = await serveReceivePack({ type: 'service', service }) 105 | res.writeHead(200, headers) 106 | 107 | // index packfile 108 | res.write(await serveReceivePack({ type: 'print', message: 'Indexing packfile...' })) 109 | await tick() 110 | let currentPhase = null 111 | const listener = async ({ phase, loaded, total, lengthComputable }) => { 112 | let np = phase !== currentPhase ? '\n' : '\r' 113 | currentPhase = phase 114 | res.write(await serveReceivePack({ type: 'print', message: `${np}${phase} ${loaded}/${total}` })) 115 | } 116 | let oids 117 | try { 118 | ee.on(`${last20}:progress`, listener) 119 | oids = await indexPack({ fs, gitdir, dir, filepath, emitterPrefix: `${last20}:` }) 120 | await tick() 121 | res.write(await serveReceivePack({ type: 'print', message: '\nIndexing completed' })) 122 | res.write(await serveReceivePack({ type: 'unpack', unpack: 'ok' })) 123 | } catch (e) { 124 | res.write(await serveReceivePack({ type: 'print', message: '\nOh dear!' })) 125 | res.write(await serveReceivePack({ type: 'unpack', unpack: e.message })) 126 | 127 | for (const update of updates) { 128 | res.write(await serveReceivePack({ type: 'ng', ref: update.fullRef, message: 'Could not index pack' })) 129 | } 130 | throw e 131 | } finally { 132 | ee.removeListener(`${last20}:progress`, listener) 133 | } 134 | await tick() 135 | 136 | // Move packfile and index into repo 137 | await fp.mkdir(path.join(gitdir, 'objects', 'pack'), { recursive: true }) 138 | await fp.rename(path.join(dir, filepath), path.join(gitdir, 'objects', 'pack', filepath)) 139 | await fp.rename(path.join(dir, filepath.replace(/\.pack$/, '.idx')), path.join(gitdir, 'objects', 'pack', filepath.replace(/\.pack$/, '.idx'))) 140 | await fp.rmdir(path.join(dir)) 141 | 142 | // Run pre-receive-hook 143 | res.write(await serveReceivePack({ type: 'print', message: '\nRunning pre-receive hook\n' })) 144 | await tick() 145 | let script 146 | try { 147 | const oid = await resolveRef({ gitdir, ref: 'HEAD' }) 148 | const { object } = await readObject({ gitdir, oid, filepath: '.hooks/pre-receive.js', encoding: 'utf8' }) 149 | script = object 150 | } catch (e) { 151 | console.log(e) 152 | script = fs.readFileSync('./pre-receive-hook.js', 'utf8') 153 | } 154 | await sandbox({ name: 'pre-receive.js', core, dir, gitdir, res, oids, updates, script }) 155 | 156 | // refs 157 | for (const update of updates) { 158 | res.write(await serveReceivePack({ type: 'ok', ref: update.fullRef })) 159 | } 160 | 161 | // gratuitous banner 162 | res.write(await serveReceivePack({ type: 'print', message: '\n' + require('./logo.js') })) 163 | } catch (e) { 164 | if (e.message === 'Client is done') { 165 | res.statusCode = 200 166 | } else { 167 | res.write(await serveReceivePack({ type: 'error', message: `${e.message}\n${e.stack}` })) 168 | } 169 | } finally { 170 | // fin 171 | res.write(await serveReceivePack({ type: 'fin' })) 172 | res.end('') 173 | } 174 | } 175 | 176 | logOutgoing(req, res) 177 | } 178 | } 179 | 180 | module.exports = factory 181 | --------------------------------------------------------------------------------