From ed45048920a20758ea13c6034268aca51879ca65 Mon Sep 17 00:00:00 2001 From: Evert Prants Date: Tue, 5 Feb 2019 18:27:36 +0200 Subject: [PATCH] initial commit --- .gitignore | 3 + applications/distrib-ipfs/.gitignore | 1 + applications/distrib-ipfs/index.js | 48 +++ .../distrib-ipfs/migrations/001-initial.sql | 10 + applications/dyndns/index.js | 282 ++++++++++++++++++ applications/highlight-termbin/index.js | 66 ++++ config-loader.js | 12 + package.json | 17 ++ runner.js | 3 + server.js | 60 ++++ 10 files changed, 502 insertions(+) create mode 100644 .gitignore create mode 100644 applications/distrib-ipfs/.gitignore create mode 100644 applications/distrib-ipfs/index.js create mode 100644 applications/distrib-ipfs/migrations/001-initial.sql create mode 100644 applications/dyndns/index.js create mode 100644 applications/highlight-termbin/index.js create mode 100644 config-loader.js create mode 100644 package.json create mode 100644 runner.js create mode 100644 server.js diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c85f9cf --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +/*/**/config.json +/node_modules/ +/package-lock.json diff --git a/applications/distrib-ipfs/.gitignore b/applications/distrib-ipfs/.gitignore new file mode 100644 index 0000000..98e6ef6 --- /dev/null +++ b/applications/distrib-ipfs/.gitignore @@ -0,0 +1 @@ +*.db diff --git a/applications/distrib-ipfs/index.js b/applications/distrib-ipfs/index.js new file mode 100644 index 0000000..41c04dc --- /dev/null +++ b/applications/distrib-ipfs/index.js @@ -0,0 +1,48 @@ +const express = require('express') +const path = require('path') +const sqlite = require('sqlite') +const crypto = require('crypto') + +const router = express.Router() +const cfgLoader = require(path.join('..', '..', 'config-loader'))(path.join(__dirname, 'config.json'), { + database: 'shortened.db', + gateway: 'https://distributed.icynet.eu', + tokens: {} +}) + +async function init () { + let config = await cfgLoader + + const dbPromise = Promise.resolve() + .then(() => sqlite.open(path.join(__dirname, config.database), { Promise, cache: true })) + .then(db => db.migrate({ migrationsPath: path.join(__dirname, 'migrations') })) + + router.get('/:hash', async (req, res, next) => { + let db = await dbPromise + let translation = await db.get('SELECT * FROM Translation WHERE translation = ?', req.params.hash) + if (!translation) return res.status(404).end() + + res.header('Cache-Control', 'max-age=' + 7 * 24 * 60 * 60 * 1000) + res.redirect(config.gateway + '/ipfs/' + translation.file_hash) + }) + + router.post('/publish', async (req, res, next) => { + let token = req.header('token') || req.body.token + if (!token || !config.tokens[token]) return res.status(402).send('Forbidden') + + let baseurl = config.tokens[token] + let hash = req.query.hash || req.body.hash + let filename = req.query.filename || req.body.filename + if (!hash) return res.status(400).send('Invalid request: missing IPFS hash') + if (!filename) filename = crypto.randomBytes(8).toString('hex') + + let db = await dbPromise + await db.run('INSERT INTO Translation (translation,file_hash,timeat) VALUES (?,?,?)', filename, hash, new Date()) + + res.send(baseurl + filename) + }) + + return router +} + +module.exports = init diff --git a/applications/distrib-ipfs/migrations/001-initial.sql b/applications/distrib-ipfs/migrations/001-initial.sql new file mode 100644 index 0000000..d749d15 --- /dev/null +++ b/applications/distrib-ipfs/migrations/001-initial.sql @@ -0,0 +1,10 @@ +-- Up + +CREATE TABLE Translation ( + translation TEXT, + file_hash TEXT, + timeat TEXT +); + +-- Down +DROP TABLE Translation; diff --git a/applications/dyndns/index.js b/applications/dyndns/index.js new file mode 100644 index 0000000..ff7b26a --- /dev/null +++ b/applications/dyndns/index.js @@ -0,0 +1,282 @@ +const express = require('express') +const util = require('util') +const exec = util.promisify(require('child_process').exec) +const path = require('path') +const fs = require('fs') +const fsp = fs.promises + +const strs = [ + '%sdmn IN A %ip', + '%sdmn IN AAAA %ip' +] + +const router = express.Router() +const cfgLoader = require(path.join('..', '..', 'config-loader'))(path.join(__dirname, 'config.json'), { + named: '/var/named/', + tokens: {} +}) + +let memcache = {} + +function fmtip (i, sd, ip) { + return strs[i].replace('%sdmn', sd).replace('%ip', ip) +} + +async function updateSerial (zonefile, contents) { + let returnContents = contents != null + if (!contents) { + contents = await fsp.readFile(zonefile, {encoding: 'utf8'}) + } + + // Split the file into lines + let lines = contents.split('\n') + + // Find 'serial' + let prv = false + let found = false + for (let i in lines) { + if (found) break + + let line = lines[i] + if (line.indexOf('SOA') !== -1) { + prv = true + continue + } + + if (prv && line.toLowerCase().indexOf('serial') !== -1) { + // Ladies and gentlemen, we got 'em + found = true + line = line.replace(/\d+/, Math.floor(Date.now() / 1000)) + lines[i] = line + break + } + + if (prv) { + if (line.indexOf(',') === -1 && line.indexOf('(') === -1 + && line.indexOf(')') === -1 && line.indexOf('IN') === -1) { + found = true + line = line.replace(/\d+/, Math.floor(Date.now() / 1000)) + lines[i] = line + break + } else { + // Just give up, not in this script's readability scope.. + break + } + } + + prv = false + } + + contents = lines.join('\n') + if (!found) return contents + if (returnContents) return contents + await fsp.writeFile(zonefile, contents) +} + +async function updateZone (cfg, v4, v6) { + let zone = cfg.domain + '.zone' + let zfile = path.join(cfg.root, zone) + await fsp.access(zfile, fs.constants.F_OK | fs.constants.W_OK) + + if (!memcache[cfg.token]) { + memcache[cfg.token] = {} + } else if (memcache[cfg.token]['v4'] === v4 && memcache[cfg.token]['v6'] === v6) { + // Don't update when it's identical + return false + } + + memcache[cfg.token].v6 = v6 + memcache[cfg.token].v4 = v4 + + // If subdomain exists, use that file instead and update serial on primary + if (cfg.subdomain && cfg.subdomain !== '@') { + let zone2 = cfg.subdomain + '.' + zone + let zfile2 = path.join(cfg.root, zone2) + await fsp.access(zfile2, fs.constants.F_OK | fs.constants.W_OK) + + let file = '; GENERATED BY DYNDNS' + + if (v4) { + file += '\n' + fmtip(0, cfg.subdomain, v4) + } + + if (v6) { + file += '\n' + fmtip(1, cfg.subdomain, v6) + } + + await fsp.writeFile(zfile2, file) + await updateSerial(zfile) + await exec('rndc reload ' + cfg.domain) + return true + } + + // Subdomain not set, just update '@' target in main zone file + let zoneFile = await fsp.readFile(zfile, {encoding: 'utf8'}) + let atlines = [] + let lines = zoneFile.split('\n') + for (let i in lines) { + let line = lines[i] + if (line.indexOf('@') === 0 && line.indexOf('SOA') === -1 && line.indexOf('A') !== -1) { + atlines.push(i) + } + } + + if (atlines.length === 0) throw new Error('I dont know what to do with this zone file.') + let set6 = false + let set4 = false + for (let j in atlines) { + let line = lines[atlines[j]] + if (line.indexOf('AAAA') !== -1) { + if (set6 || v6 === null) { + lines.splice(atlines[j], 1) + continue + } + if (v6) { + lines[atlines[j]] = fmtip(1, '@', v6) + set6 = true + } else if (v4 && atlines.length !== 1) { + lines[atlines[j]] = fmtip(0, '@', v4) + set4 = true + } + if (v4 && atlines.length === 1) { + lines.splice(atlines[j], 0, fmtip(0, '@', v4)) + set4 = true + } + } else { + if (set4 || v4 === null) { + lines.splice(atlines[j], 1) + continue + } + if (v4) { + lines[atlines[j]] = fmtip(0, '@', v4) + set4 = true + } else if (v6 && atlines.length !== 1) { + lines[atlines[j]] = fmtip(1, '@', v6) + set6 = true + } + if (v6 && atlines.length === 1) { + lines.splice(atlines[j], 0, fmtip(1, '@', v6)) + set6 = true + } + } + } + + zoneFile = await updateSerial(null, lines.join('\n')) + if (!zoneFile) throw new Error('I dont know what to do with this zone file.') + await fsp.writeFile(zfile, zoneFile) + await exec('rndc reload ' + cfg.domain) +} + +function validv4 (ipaddress) { + if (/^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/.test(ipaddress)) { + return true + } + return false +} + +function validv6 (value) { + // See https://blogs.msdn.microsoft.com/oldnewthing/20060522-08/?p=31113 and + // https://4sysops.com/archives/ipv6-tutorial-part-4-ipv6-address-syntax/ + const components = value.split(':') + if (components.length < 2 || components.length > 8) { + return false + } + + if (components[0] !== '' || components[1] !== '') { + // Address does not begin with a zero compression ("::") + if (!components[0].match(/^[\da-f]{1,4}/i)) { + // Component must contain 1-4 hex characters + return false + } + } + + let numberOfZeroCompressions = 0 + for (let i = 1; i < components.length; ++i) { + if (components[i] === '') { + // We're inside a zero compression ("::") + ++numberOfZeroCompressions + if (numberOfZeroCompressions > 1) { + // Zero compression can only occur once in an address + return false + } + continue + } + if (!components[i].match(/^[\da-f]{1,4}/i)) { + // Component must contain 1-4 hex characters + return false + } + } + + return true +} + +async function init () { + let config = await cfgLoader + await fsp.access(config.named, fs.constants.F_OK) + + router.post('/', async (req, res, next) => { + let token = req.header('token') || req.body.token + + // Check token + if (!token || !config.tokens[token]) return res.status(402).send('Forbidden') + + let v4 = null + let qv4 = req.query.ipv4 || req.body.ipv4 + + let v6 = null + let qv6 = req.query.ipv6 || req.body.ipv6 + + // Lets begin our trials + // Determine Address from request headers + if (req.header('http_x_forwarded_for')) { + v4 = req.header('http_x_forwarded_for') + } else if (req.header('remote_addr')) { + v4 = req.header('remote_addr') + } else { + v4 = req.connection.remoteAddress + } + + if (!validv4(v4)) { + v6 = v4 + v4 = null + } + + // IPv4 + if (qv4 && validv4(qv4)) { + v4 = qv4 + } + + if (qv4 === 'ignore') { + v4 = null + } + + // IPv6 + if (qv6 && validv6(qv6)) { + v6 = qv6 + } + + if (qv6 === 'ignore') { + v6 = null + } + + if (v4 === null && v6 === null) { + res.send('Nothing to do') + } + + try { + await updateZone(Object.assign({ + token: token, + root: config.named, + }, config.tokens[token]), v4, v6) + } catch (e) { + console.error(e.stack) + return res.status(500).send('Internal Server Error') + } + + res.status(204).end() + }) + + return router +} + +module.exports = init diff --git a/applications/highlight-termbin/index.js b/applications/highlight-termbin/index.js new file mode 100644 index 0000000..16f8ae7 --- /dev/null +++ b/applications/highlight-termbin/index.js @@ -0,0 +1,66 @@ +const express = require('express') +const path = require('path') +const fs = require('fs') +const fsp = fs.promises + +const router = express.Router() +const cfgLoader = require(path.join('..', '..', 'config-loader'))(path.join(__dirname, 'config.json'), { + root: path.join(process.cwd(), '..', 'fiche', 'code'), + style: 'tomorrow-night' +}) + +async function init () { + let config = await cfgLoader + let root = path.resolve(config.root) + + await fsp.access(root, fs.constants.F_OK) + + router.get('/:name', async (req, res, next) => { + let name = req.params.name + if (name.length != 4) { + try { + let text = await fsp.readFile(path.join(root, 'index.html')) + res.send(text) + } catch (e) { + res.status(403) + } + return res.end() + } + + let fichePath = path.join(root, name, 'index.txt') + let reqRaw = req.query.raw != null + + let text + try { + text = await fsp.readFile(fichePath, {encoding: 'utf8'}) + } catch (e) { + return res.status(404).end() + } + + if (reqRaw) { + return res.set('Content-Type', 'text/plain').send(text) + } + + let etext = text.replace(/&/g,'&').replace(//g,'>') + let payload = + '' + + '' + + '' + + '' + name + '' + + '' + + '' + + '' + + '' + + '' + + '
' + etext + '
' + + '' + + '' + + '' + + res.set('Content-Type', 'application/xhtml+xml').send(payload) + }) + + return router +} + +module.exports = init diff --git a/config-loader.js b/config-loader.js new file mode 100644 index 0000000..4283731 --- /dev/null +++ b/config-loader.js @@ -0,0 +1,12 @@ +const fs = require('fs').promises + +module.exports = async function (file, defaults) { + let f + try { + f = await fs.readFile(file) + f = JSON.parse(f) + } catch (e) { + return defaults + } + return Object.assign({}, defaults, f) +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..ad86bb6 --- /dev/null +++ b/package.json @@ -0,0 +1,17 @@ +{ + "name": "serve-lunasqu.ee", + "version": "1.0.0", + "description": "lunasqu.ee - unite segregated services", + "main": "runner.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1", + "start": "node runner.js" + }, + "private": true, + "license": "Unlicense", + "dependencies": { + "body-parser": "^1.18.3", + "express": "^4.16.4", + "sqlite": "^3.0.1" + } +} diff --git a/runner.js b/runner.js new file mode 100644 index 0000000..23c70f4 --- /dev/null +++ b/runner.js @@ -0,0 +1,3 @@ +const path = require('path') + +require(path.join(__dirname, 'server'))().catch((e) => console.error(e)) diff --git a/server.js b/server.js new file mode 100644 index 0000000..bf1a757 --- /dev/null +++ b/server.js @@ -0,0 +1,60 @@ +const express = require('express') +const bodyParser = require('body-parser') +const path = require('path') +const fs = require('fs') +const fsPromises = fs.promises + +const app = express() + +let apps = process.env.APPS ? JSON.parse(process.env.APPS) : ['highlight-termbin', 'dyndns', 'distrib-ipfs'] +let sock = process.env.SOCKET || '/tmp/serve-lunasqu.ee.t.sock' + +app.enable('trust proxy', 1) +app.disable('x-powered-by') + +app.use(bodyParser.urlencoded({ extended: false })) +app.use(bodyParser.json()) + +function requireNoCache (file) { + let fullPath = path.resolve(file) + + let data = require(fullPath) + + if (require.cache && require.cache[fullPath]) { + delete require.cache[fullPath] + } + + return data +} + +async function enableApp (name) { + let papp = path.join(__dirname, 'applications', name) + await fsPromises.access(papp, fs.constants.F_OK) + + let runner = requireNoCache(papp) + if (!runner) throw new Error('No such application.') + let router = await runner() + + app.use('/' + name, router) +} + +async function init () { + for (let i in apps) { + let papp = apps[i] + try { + await enableApp(papp) + console.log(' =>', papp, 'was enabled successfully.') + } catch (e) { + console.error('Failed to start application', papp) + console.error(e.stack) + } + } + + try { await fsPromises.unlink(sock) } catch (e) {} + + app.listen(sock, function () { + console.log('Started server on', sock) + }) +} + +module.exports = init