'use strict'; const express = require('express'); const next = require('next'); const fs = require('fs'); const path = require('path'); const sqlite3 = require('sqlite3').verbose(); const nodeGit = require('nodegit'); const segFaultHandler = require('segfault-handler'); const repositoryDirectory = './repositories/'; const dbFile = './gitcub.db'; const dev = process.env.NODE_ENV !== 'production'; const port = 3000; const app = next({ dev }); const handle = app.getRequestHandler(); segFaultHandler.registerHandler('crash.log'); const directoryExists = (path) => fs.existsSync(path) ? fs.statSync(path).isDirectory() : false; const repositoryExists = (name, rows) => ( directoryExists(repositoryDirectory + name + '.git') && rows.some((x) => x.name === name) ); const getContentType = (filename) => { const index = filename.indexOf('.'); if (index > -1) { switch (filename.slice(index + 1)) { case 'pdf': return 'application/pdf'; case 'gif': return 'image/gif'; case 'jpeg': case 'jpg': return 'image/jpeg'; case 'png': return 'image/png'; case 'tiff': case 'tif': return 'image/tiff'; default: return 'application/octet-stream'; } } else return 'application/octet-stream'; }; const sendBinary = (res, filename, content) => { const contentType = getContentType(filename); res.set('content-type', contentType); res.set('X-Content-Type-Options', 'nosniff'); res.send(content); }; const sendPlainText = (res, text) => { res.set('content-type', 'text/plain'); res.set('X-Content-Type-Options', 'nosniff'); res.send(text); }; const sendAPIResult = (res, resultType, resultPath, resultContent = '') => { const result = { resultType: resultType, resultPath: resultPath, resultContent: resultContent, }; res.json(result); }; const sendNotFound = (res, isAPIRequest) => { if (isAPIRequest) { sendAPIResult(res, 'notFound', []); } else { res.status(404).end(); } }; const sendError = (res, isAPIRequest, message) => { if (isAPIRequest) { sendAPIResult(res, 'error', [], message); } else { sendPlainText(res, message); } }; const resolveDirectoryPath = (res, pathArray, tree, isAPIRequest) => { const recursion = (depth, tree) => { if (pathArray.length > depth) { const entries = tree.entries(); const isMatchingTree = (x) => x.name() === pathArray[depth] && x.isTree(); const isMatchingBlob = (x) => x.name() === pathArray[depth] && x.isBlob(); if (entries.some(isMatchingTree)) { entries.find(isMatchingTree).getTree().then((tree) => { const newDepth = depth + 1; recursion(newDepth, tree); }); } else if (pathArray.length === depth + 1) { if (entries.some(isMatchingBlob)) { if (pathArray[1] === 'blob' || pathArray[1] === 'raw') { entries.find(isMatchingBlob).getBlob().then((blob) => { if (blob.isBinary()) { if (isAPIRequest) { sendAPIResult(res, 'binaryBlob', pathArray); } else { const buffer = blob.content(); sendBinary(res, pathArray[depth], buffer); } } else { const text = blob.toString(); if (isAPIRequest) { sendAPIResult(res, 'textBlob', pathArray, text); } else { sendPlainText(res, text); } } }); } else { sendNotFound(res, isAPIRequest); } } else { sendNotFound(res, isAPIRequest); } } else { sendNotFound(res, isAPIRequest); } } else { if (pathArray[1] === 'tree') { const entryNamesSorted = { trees: tree.entries().filter((entry) => entry.isTree()).map((entry) => entry.name()), blobs: tree.entries().filter((entry) => entry.isBlob()).map((entry) => entry.name()), }; sendAPIResult(res, 'nonRootDirectory', pathArray, entryNamesSorted); } else { sendNotFound(res, isAPIRequest); } } }; recursion(3, tree); }; const showRoot = (res, pathArray, tree) => { const entries = tree.entries(); const entryNamesSorted = { trees: entries.filter((entry) => entry.isTree()).map((entry) => entry.name()), blobs: entries.filter((entry) => entry.isBlob()).map((entry) => entry.name()), }; if (entryNamesSorted.blobs.some((x) => x.toLowerCase() === 'readme.md')) { entries.find((x) => x.name().toLowerCase() === 'readme.md' && x.isBlob()).getBlob().then((blob) => { if (!blob.isBinary()) { const rootContents = { entryNamesSorted: entryNamesSorted, readmeExists: true, readmeBlob: blob.toString(), }; sendAPIResult(res, 'rootDirectory', pathArray, rootContents); } else { const rootContents = { entryNamesSorted: entryNamesSorted, readmeExists: false, readmeBlob: '', }; sendAPIResult(res, 'rootDirectory', pathArray, rootContents); } }); } else { const rootContents = { entryNamesSorted: entryNamesSorted, readmeExists: false, readmeBlob: '', }; sendAPIResult(res, 'rootDirectory', pathArray, rootContents); } }; const tryShowCommits = (res, pathArray, repo, isAPIRequest) => { if (pathArray.length === 2) { repo.getMasterCommit() .then((commit) => { const commitEmitter = commit.history(); commitEmitter.on('end', (commits) => { const commitObjs = commits.map((item) => { const obj = { id: item.id().tostrS(), message: item.message(), }; return obj; }); sendAPIResult(res, 'commits', pathArray, commitObjs); }); commitEmitter.start(); }); } else { sendNotFound(res, isAPIRequest); } }; const tryParseCommit = (res, pathArray, repo, isAPIRequest) => { if (pathArray.length > 2) { repo.getMasterCommit() .then((commit) => { if (pathArray[2] === 'master') { commit.getTree() .then((tree) => { resolveDirectoryPath(res, pathArray, tree, isAPIRequest); }); } else { const commitEmitter = commit.history(); commitEmitter.on('end', (commits) => { if (commits.some((x) => x.id().tostrS() === pathArray[2])) { repo.getCommit(pathArray[2]) .then((commit) => commit.getTree()) .then((tree) => { resolveDirectoryPath(res, pathArray, tree, isAPIRequest); }); } else { sendNotFound(res, isAPIRequest); } }); commitEmitter.start(); } }); } else { sendNotFound(res, isAPIRequest); } }; const handleRepositoryPath = (res, pathArray, repo, isAPIRequest) => { if (pathArray.length > 1) { switch (pathArray[1]) { case 'commits': tryShowCommits(res, pathArray, repo, isAPIRequest); break; case 'tree': case 'blob': case 'raw': tryParseCommit(res, pathArray, repo, isAPIRequest); break; default: sendNotFound(res, isAPIRequest); } } else { repo.getMasterCommit() .then((commit) => commit.getTree()) .then((tree) => { showRoot(res, pathArray, tree); }); } }; const handleTotalPath = (res, pathArray, rows, isAPIRequest) => { if (pathArray.length > 0) { if (repositoryExists(pathArray[0], rows)) { const pathToRepo = repositoryDirectory + pathArray[0] + '.git'; nodeGit.Repository.openBare(pathToRepo) .then((repo) => { handleRepositoryPath(res, pathArray, repo, isAPIRequest); }); } else { sendNotFound(res, isAPIRequest); } } else { sendAPIResult(res, 'home', pathArray, rows); } }; const handleBackendRequest = (req, res, isAPIRequest) => { if (req.originalUrl.indexOf('\0') === -1) { const db = new sqlite3.Database(dbFile, sqlite3.OPEN_READWRITE); db.all('select name from repositories', (err, rows) => { db.close(); const pathNormalized = path.normalize(req.path); const pathArray = pathNormalized.split('/').filter((x) => x.length > 0); // pathArray at this point must have at least 1 element due to routing rules const pathArrayclipped = isAPIRequest ? pathArray.slice(1) : pathArray; handleTotalPath(res, pathArrayclipped, rows, isAPIRequest); }); } else { sendError(res, isAPIRequest, 'Null byte found in url. Nice try :)'); } }; app.prepare() .then(() => { const server = express(); server.get('/api/*', (req, res) => { handleBackendRequest(req, res, true); }); server.get(/^\/[^/]*\/raw(\/|$)/, (req, res) => { handleBackendRequest(req, res, false); }); server.get('*', (req, res) => { return handle(req, res); }); server.listen(port, () => { console.log(`App listening on port ${port}`); }); server.on('uncaughtException', (err) => { console.log(err); }); }) .catch((ex) => { console.error(ex.stack); process.exit(1); });