├── .gitignore ├── cors.js ├── daemon.js ├── demo.git ├── HEAD ├── config ├── description ├── index ├── info │ └── exclude ├── logs │ ├── HEAD │ └── refs │ │ └── heads │ │ └── master └── packed-refs ├── demo └── .hooks │ └── pre-receive.js ├── http-daemon.js ├── http-server.js ├── identify-request.js ├── logo.js ├── lookup.js ├── middleware.js ├── package-lock.json ├── package.json ├── parse-request.js ├── pre-receive-hook.js └── sandbox.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /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 -------------------------------------------------------------------------------- /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 | } -------------------------------------------------------------------------------- /demo.git/HEAD: -------------------------------------------------------------------------------- 1 | ref: refs/heads/master 2 | -------------------------------------------------------------------------------- /demo.git/config: -------------------------------------------------------------------------------- 1 | [core] 2 | repositoryformatversion = 0 3 | filemode = true 4 | bare = true 5 | ignorecase = true 6 | precomposeunicode = true 7 | -------------------------------------------------------------------------------- /demo.git/description: -------------------------------------------------------------------------------- 1 | Unnamed repository; edit this file 'description' to name the repository. 2 | -------------------------------------------------------------------------------- /demo.git/index: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/isomorphic-git/server/72e59cc4192da6091cf296162169892076b26e72/demo.git/index -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /demo.git/packed-refs: -------------------------------------------------------------------------------- 1 | # pack-refs with: peeled fully-peeled sorted 2 | 59eabecab1988d343a2cc8c054a56d5e6cb3bd89 refs/heads/master 3 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /logo.js: -------------------------------------------------------------------------------- 1 | var figlet = require('figlet'); 2 | 3 | module.exports = figlet.textSync('GitServer', { font: 'Cyberlarge' }) 4 | -------------------------------------------------------------------------------- /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 | } -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@isomorphic-git/server", 3 | "version": "0.0.0", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "@isomorphic-git/pgp-plugin": { 8 | "version": "0.0.7", 9 | "resolved": "https://registry.npmjs.org/@isomorphic-git/pgp-plugin/-/pgp-plugin-0.0.7.tgz", 10 | "integrity": "sha512-6adXezgOeXFn5CHYlxAtmq7OoDjLPWxyxL7FXtM6ROdLOT/WScU1asPXG0dhGmKJNR4u23WF5pIbNHJtqKLZ/Q==", 11 | "requires": { 12 | "@isomorphic-pgp/sign-and-verify": "^0.0.10", 13 | "@isomorphic-pgp/util": "^0.0.6" 14 | } 15 | }, 16 | "@isomorphic-pgp/parser": { 17 | "version": "0.0.3", 18 | "resolved": "https://registry.npmjs.org/@isomorphic-pgp/parser/-/parser-0.0.3.tgz", 19 | "integrity": "sha512-d1lu99aTI0VpHIQDeY+jNPbvT2TpkAaU0CC/udcWXwfyphH+qYXNo/4NgCO/084Zlbjoz23z8ZbrpMM9KrkwLw==", 20 | "requires": { 21 | "array-buffer-to-hex": "^1.0.0", 22 | "base64-js": "^1.3.0", 23 | "bn.js": "^4.11.8", 24 | "clz-buffer": "^1.0.0", 25 | "concat-buffers": "^1.0.0", 26 | "crc": "^3.8.0", 27 | "isomorphic-textencoder": "^1.0.1", 28 | "select-case": "^1.0.0" 29 | } 30 | }, 31 | "@isomorphic-pgp/sign-and-verify": { 32 | "version": "0.0.10", 33 | "resolved": "https://registry.npmjs.org/@isomorphic-pgp/sign-and-verify/-/sign-and-verify-0.0.10.tgz", 34 | "integrity": "sha512-YGta8FP2UXUcek1T5TY1taO1+TSIEu5Tk3+d5c/SmACIm3AJX4K1ncn4XuCXy+ECS8WyVj0sSzr6vxYEyZ6HiA==", 35 | "requires": { 36 | "@isomorphic-pgp/parser": "^0.0.3", 37 | "@isomorphic-pgp/util": "^0.0.6", 38 | "@wmhilton/crypto-hash": "^1.0.2", 39 | "array-buffer-to-hex": "^1.0.0", 40 | "isomorphic-textencoder": "^1.0.1", 41 | "jsbn": "^1.1.0", 42 | "sha.js": "^2.4.11" 43 | } 44 | }, 45 | "@isomorphic-pgp/util": { 46 | "version": "0.0.6", 47 | "resolved": "https://registry.npmjs.org/@isomorphic-pgp/util/-/util-0.0.6.tgz", 48 | "integrity": "sha512-6J60flPSPJpsuGiVZ29F5oJ8ma7v44ya9FwpjjEJUV4mTYuMujXFuKlAX1VNnNmi2DaerE8XRB5ocJi15Fe5Dg==", 49 | "requires": { 50 | "@isomorphic-pgp/parser": "^0.0.3", 51 | "array-buffer-to-hex": "^1.0.0", 52 | "concat-buffers": "^1.0.0", 53 | "sha.js": "^2.4.11" 54 | } 55 | }, 56 | "@wmhilton/crypto-hash": { 57 | "version": "1.0.2", 58 | "resolved": "https://registry.npmjs.org/@wmhilton/crypto-hash/-/crypto-hash-1.0.2.tgz", 59 | "integrity": "sha512-3lopwJLEfPXLA3f1Ovvtfq+uWQTLWs+rZN/KJkOniFhGhz/36ST1rpnouh1hkVQGw+qistFPpwwK17a1e0hBCg==" 60 | }, 61 | "ansi-styles": { 62 | "version": "3.2.1", 63 | "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", 64 | "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", 65 | "requires": { 66 | "color-convert": "^1.9.0" 67 | } 68 | }, 69 | "array-buffer-to-hex": { 70 | "version": "1.0.0", 71 | "resolved": "https://registry.npmjs.org/array-buffer-to-hex/-/array-buffer-to-hex-1.0.0.tgz", 72 | "integrity": "sha512-arycdkxgK1cj6s03GDb96tlCxOl1n3kg9M2OHseUc6Pqyqp+lgfceFPmG507eI5V+oxOSEnlOw/dFc7LXBXF4Q==" 73 | }, 74 | "base64-js": { 75 | "version": "1.3.1", 76 | "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.3.1.tgz", 77 | "integrity": "sha512-mLQ4i2QO1ytvGWFWmcngKO//JXAQueZvwEKtjgQFM4jIK0kU+ytMfplL8j+n5mspOfjHwoAg+9yhb7BwAHm36g==" 78 | }, 79 | "basic-auth": { 80 | "version": "2.0.1", 81 | "resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz", 82 | "integrity": "sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==", 83 | "requires": { 84 | "safe-buffer": "5.1.2" 85 | } 86 | }, 87 | "bn.js": { 88 | "version": "4.11.8", 89 | "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.8.tgz", 90 | "integrity": "sha512-ItfYfPLkWHUjckQCk8xC+LwxgK8NYcXywGigJgSwOP8Y2iyWT4f2vsZnoOXTTbo+o5yXmIUJ4gn5538SO5S3gA==" 91 | }, 92 | "buffer": { 93 | "version": "5.4.3", 94 | "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.4.3.tgz", 95 | "integrity": "sha512-zvj65TkFeIt3i6aj5bIvJDzjjQQGs4o/sNoezg1F1kYap9Nu2jcUdpwzRSJTHMMzG0H7bZkn4rNQpImhuxWX2A==", 96 | "requires": { 97 | "base64-js": "^1.0.2", 98 | "ieee754": "^1.1.4" 99 | } 100 | }, 101 | "buffer-equal-constant-time": { 102 | "version": "1.0.1", 103 | "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", 104 | "integrity": "sha1-+OcRMvf/5uAaXJaXpMbz5I1cyBk=" 105 | }, 106 | "chalk": { 107 | "version": "2.4.2", 108 | "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", 109 | "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", 110 | "requires": { 111 | "ansi-styles": "^3.2.1", 112 | "escape-string-regexp": "^1.0.5", 113 | "supports-color": "^5.3.0" 114 | } 115 | }, 116 | "clz-buffer": { 117 | "version": "1.0.0", 118 | "resolved": "https://registry.npmjs.org/clz-buffer/-/clz-buffer-1.0.0.tgz", 119 | "integrity": "sha512-ApZYoWy06SfeIaJG/oBecF9ZBOYkP6uz31FYJF8gtGGacqz+YgD5kZuo5XBkgqI3tmL0E/vxyZ5q0PYgXjtxWg==" 120 | }, 121 | "color-convert": { 122 | "version": "1.9.3", 123 | "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", 124 | "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", 125 | "requires": { 126 | "color-name": "1.1.3" 127 | } 128 | }, 129 | "color-name": { 130 | "version": "1.1.3", 131 | "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", 132 | "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=" 133 | }, 134 | "concat-buffers": { 135 | "version": "1.0.0", 136 | "resolved": "https://registry.npmjs.org/concat-buffers/-/concat-buffers-1.0.0.tgz", 137 | "integrity": "sha1-YQ2y9pSyv279znXbl/PD5ALJsb4=" 138 | }, 139 | "crc": { 140 | "version": "3.8.0", 141 | "resolved": "https://registry.npmjs.org/crc/-/crc-3.8.0.tgz", 142 | "integrity": "sha512-iX3mfgcTMIq3ZKLIsVFAbv7+Mc10kxabAGQb8HvjA1o3T1PIYprbakQ65d3I+2HGHt6nSKkM9PYjgoJO2KcFBQ==", 143 | "requires": { 144 | "buffer": "^5.1.0" 145 | } 146 | }, 147 | "daemonize-process": { 148 | "version": "1.0.9", 149 | "resolved": "https://registry.npmjs.org/daemonize-process/-/daemonize-process-1.0.9.tgz", 150 | "integrity": "sha512-YoB+AmcgHIBDVeyfVWSCV90FNk799zX8Uvn7RJTDCD8Y0EMNbSfIKLG961VgchJme2GHmqpXUuV8Rxe2j2L+bw==" 151 | }, 152 | "decompress-response": { 153 | "version": "4.2.1", 154 | "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-4.2.1.tgz", 155 | "integrity": "sha512-jOSne2qbyE+/r8G1VU+G/82LBs2Fs4LAsTiLSHOCOMZQl2OKZ6i8i4IyHemTe+/yIXOtTcRQMzPcgyhoFlqPkw==", 156 | "requires": { 157 | "mimic-response": "^2.0.0" 158 | } 159 | }, 160 | "escape-string-regexp": { 161 | "version": "1.0.5", 162 | "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", 163 | "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=" 164 | }, 165 | "fast-text-encoding": { 166 | "version": "1.0.0", 167 | "resolved": "https://registry.npmjs.org/fast-text-encoding/-/fast-text-encoding-1.0.0.tgz", 168 | "integrity": "sha512-R9bHCvweUxxwkDwhjav5vxpFvdPGlVngtqmx4pIZfSUhM/Q4NiIUHB456BAf+Q1Nwu3HEZYONtu+Rya+af4jiQ==" 169 | }, 170 | "figlet": { 171 | "version": "1.2.4", 172 | "resolved": "https://registry.npmjs.org/figlet/-/figlet-1.2.4.tgz", 173 | "integrity": "sha512-mv8YA9RruB4C5QawPaD29rEVx3N97ZTyNrE4DAfbhuo6tpcMdKnPVo8MlyT3RP5uPcg5M14bEJBq7kjFf4kAWg==" 174 | }, 175 | "has-flag": { 176 | "version": "3.0.0", 177 | "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", 178 | "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=" 179 | }, 180 | "ieee754": { 181 | "version": "1.1.13", 182 | "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.13.tgz", 183 | "integrity": "sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg==" 184 | }, 185 | "inherits": { 186 | "version": "2.0.4", 187 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", 188 | "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" 189 | }, 190 | "isomorphic-textencoder": { 191 | "version": "1.0.1", 192 | "resolved": "https://registry.npmjs.org/isomorphic-textencoder/-/isomorphic-textencoder-1.0.1.tgz", 193 | "integrity": "sha512-676hESgHullDdHDsj469hr+7t3i/neBKU9J7q1T4RHaWwLAsaQnywC0D1dIUId0YZ+JtVrShzuBk1soo0+GVcQ==", 194 | "requires": { 195 | "fast-text-encoding": "^1.0.0" 196 | } 197 | }, 198 | "jsbn": { 199 | "version": "1.1.0", 200 | "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-1.1.0.tgz", 201 | "integrity": "sha1-sBMHyym2GKHtJux56RH4A8TaAEA=" 202 | }, 203 | "micro-cors": { 204 | "version": "0.1.1", 205 | "resolved": "https://registry.npmjs.org/micro-cors/-/micro-cors-0.1.1.tgz", 206 | "integrity": "sha512-6WqIahA5sbQR1Gjexp1VuWGFDKbZZleJb/gy1khNGk18a6iN1FdTcr3Q8twaxkV5H94RjxIBjirYbWCehpMBFw==" 207 | }, 208 | "mimic-response": { 209 | "version": "2.0.0", 210 | "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-2.0.0.tgz", 211 | "integrity": "sha512-8ilDoEapqA4uQ3TwS0jakGONKXVJqpy+RpM+3b7pLdOjghCrEiGp9SRkFbUHAmZW9vdnrENWHjaweIoTIJExSQ==" 212 | }, 213 | "minimist": { 214 | "version": "1.2.0", 215 | "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", 216 | "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=" 217 | }, 218 | "minimisted": { 219 | "version": "2.0.0", 220 | "resolved": "https://registry.npmjs.org/minimisted/-/minimisted-2.0.0.tgz", 221 | "integrity": "sha512-oP88Dw3LK/pdrKyMdlbmg3W50969UNr4ctISzJfPl+YPYHTAOrS+dihXnsgRNKSRIzDsrnV3eE2CCVlZbpOKdQ==", 222 | "requires": { 223 | "minimist": "^1.2.0" 224 | } 225 | }, 226 | "once": { 227 | "version": "1.4.0", 228 | "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", 229 | "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", 230 | "requires": { 231 | "wrappy": "1" 232 | } 233 | }, 234 | "safe-buffer": { 235 | "version": "5.1.2", 236 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", 237 | "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" 238 | }, 239 | "select-case": { 240 | "version": "1.0.0", 241 | "resolved": "https://registry.npmjs.org/select-case/-/select-case-1.0.0.tgz", 242 | "integrity": "sha512-YN35jnDPQyvPk84kUGhJWBVKRhtNn51byIXbznpC6tZpTCUz330w7WX2tT5A6+3rw+NXIdya7ZQHqxKXi1m25g==" 243 | }, 244 | "sha.js": { 245 | "version": "2.4.11", 246 | "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz", 247 | "integrity": "sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==", 248 | "requires": { 249 | "inherits": "^2.0.1", 250 | "safe-buffer": "^5.0.1" 251 | } 252 | }, 253 | "simple-concat": { 254 | "version": "1.0.0", 255 | "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.0.tgz", 256 | "integrity": "sha1-c0TLuLbib7J9ZrL8hvn21Zl1IcY=" 257 | }, 258 | "simple-get": { 259 | "version": "3.1.0", 260 | "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-3.1.0.tgz", 261 | "integrity": "sha512-bCR6cP+aTdScaQCnQKbPKtJOKDp/hj9EDLJo3Nw4y1QksqaovlW/bnptB6/c1e+qmNIDHRK+oXFDdEqBT8WzUA==", 262 | "requires": { 263 | "decompress-response": "^4.2.0", 264 | "once": "^1.3.1", 265 | "simple-concat": "^1.0.0" 266 | } 267 | }, 268 | "supports-color": { 269 | "version": "5.5.0", 270 | "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", 271 | "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", 272 | "requires": { 273 | "has-flag": "^3.0.0" 274 | } 275 | }, 276 | "tree-kill": { 277 | "version": "1.2.1", 278 | "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.1.tgz", 279 | "integrity": "sha512-4hjqbObwlh2dLyW4tcz0Ymw0ggoaVDMveUB9w8kFSQScdRLo0gxO9J7WFcUBo+W3C1TLdFIEwNOWebgZZ0RH9Q==" 280 | }, 281 | "vm2": { 282 | "version": "3.8.4", 283 | "resolved": "https://registry.npmjs.org/vm2/-/vm2-3.8.4.tgz", 284 | "integrity": "sha512-5HThl+RBO/pwE9SF0kM4nLrpq5vXHBNk4BMX27xztvl0j1RsZ4/PMVJAu9rM9yfOtTo5KroL7XNX3031ExleSw==" 285 | }, 286 | "wrappy": { 287 | "version": "1.0.2", 288 | "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", 289 | "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" 290 | } 291 | } 292 | } 293 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | --------------------------------------------------------------------------------