diff --git a/lib/www/server/queues/asaqc/dummy_server/api/index.js b/lib/www/server/queues/asaqc/dummy_server/api/index.js new file mode 100644 index 0000000..74bc747 --- /dev/null +++ b/lib/www/server/queues/asaqc/dummy_server/api/index.js @@ -0,0 +1,156 @@ +/** + * A minimalist mock-up of the ASAQC API, used + * for testing. + * + * Use the following environment variables to + * configure its behaviour: + * + * HTTP_PORT Port to listen to. Defaults to 3077. + * + * HTTPS_PEM Combined public and private parts of + * a TLS certificate to use. If provided, an HTTPS + * server will be started. Alternatively, the user + * may provide separate files (see below). + * + * HTTPS_CERT Public certificate to use in HTTPS + * mode. + * + * HTTPS_KEY Private key to use along `HTTPS_CERT` + * in HTTPS mode. Note that both must be provided. + * + * HTTPS_CA Public certificate of a certificate + * authority or the public part of a client certificate. + * If provided, the client needs to authenticate with + * a certificate signed by this authority, or with this + * certifcate itself, if self-signed. + * + * See also ../index.js for other environment + * variable options. + * + */ +const http = require('http'); +const https = require('https'); +const fs = require('fs'); +const express = require('express'); + +const mw = require('./middleware'); + +const app = express(); +const verbose = process.env.NODE_ENV != 'test'; + +app.map = function(a, route){ + route = route || ''; + for (var key in a) { + switch (typeof a[key]) { + // { '/path': { ... }} + case 'object': + if (!Array.isArray(a[key])) { + app.map(a[key], route + key); + break; + } // else drop through + // get: function(){ ... } + case 'function': + if (verbose) console.log('%s %s', key, route); + app[key](route, a[key]); + break; + } + } +}; + +app.use(express.json({type: "application/json", strict: false, limit: '10mb'})); +app.use(express.urlencoded({ type: "application/x-www-form-urlencoded", extended: true })); +app.use(express.text({type: "text/*", limit: '10mb'})); +app.use((req, res, next) => { + res.set("Access-Control-Allow-Origin", "*"); + res.set("Access-Control-Allow-Methods", "OPTIONS, GET, POST, PUT, PATCH, DELETE"); + res.set("Access-Control-Allow-Headers", "Content-Type"); + next(); +}); + +app.map({ + '/vt/v1/api/upload-file-encoded': { + post: [ mw.post ] + } +}); + +// Generic error handler. Stops stack dumps +// being sent to clients. +app.use(function (err, req, res, next) { + const title = `HTTP backend error at ${req.method} ${req.originalUrl}`; + const description = err.message; + const message = err.message; + const alert = {title, message, description, error: err}; + + console.log("Error:", err); + + res.set("Content-Type", "application/json"); + if (err instanceof Error && err.name != "UnauthorizedError") { + console.error(err.stack); + res.set("Content-Type", "text/plain"); + res.status(500).send('General internal error'); + } else if (typeof err === 'string') { + res.status(500).send({message: err}); + } else { + res.status(err.status || 500).send({message: err.message || (err.inner && err.inner.message) || "Internal error"}); + } +}); + +app.disable('x-powered-by'); +app.enable('trust proxy'); +console.log('trust proxy is ' + (app.get('trust proxy')? 'on' : 'off')); + +const addr = "127.0.0.1"; + +if (!module.parent) { + var port = process.env.HTTP_PORT || 3000; + var server = http.createServer(app).listen(port, addr); + + console.log('API started on port ' + port); +} else { + app.start = function (port = 3000, path, locals={}) { + + app.locals = {...app.locals, ...locals}; + + var root = app; + if (path) { + root = express(); + ['x-powered-by', 'trust proxy'].forEach(k => root.set(k, app.get(k))); + root.use(path, app); + } + + if (process.env.HTTPS_PEM || (process.env.HTTPS_CERT && process.env.HTTPS_KEY)) { + + const options = { + cert: fs.readFileSync(process.env.HTTPS_CERT || process.env.HTTPS_PEM), + key: fs.readFileSync(process.env.HTTPS_KEY || process.env.HTTPS_PEM) + }; + + if (process.env.HTTPS_CA) { + // Enable client certificate authentication + options.requestCert = true; + options.ca = fs.readFileSync(process.env.HTTPS_CA); + options.rejectUnauthorized = true; + console.log("TLS client authentication requested"); + } + + const server = https.createServer(options, root).listen(port, addr); + + if (server) { + console.log(`TLS API started on port ${port}, prefix: ${path || "/"}`); + } + + return server; + } else { + + const server = http.createServer(root).listen(port, addr); + + if (server) { + console.log(`API started on port ${port}, prefix: ${path || "/"}`); + } + + return server; + } + + } + module.exports = app; +} diff --git a/lib/www/server/queues/asaqc/dummy_server/api/middleware/index.js b/lib/www/server/queues/asaqc/dummy_server/api/middleware/index.js new file mode 100644 index 0000000..fa93531 --- /dev/null +++ b/lib/www/server/queues/asaqc/dummy_server/api/middleware/index.js @@ -0,0 +1,3 @@ +module.exports = { + post: require('./post') +}; diff --git a/lib/www/server/queues/asaqc/dummy_server/api/middleware/post.js b/lib/www/server/queues/asaqc/dummy_server/api/middleware/post.js new file mode 100644 index 0000000..cd97190 --- /dev/null +++ b/lib/www/server/queues/asaqc/dummy_server/api/middleware/post.js @@ -0,0 +1,68 @@ + +const fs = require('fs'); +const path = require('path'); +const uuid = require('uuid/v4'); + +/** + * Suggest new names for files. + * + * Takes a file name and adds a numeric suffix to it if + * there isn't one, or increments it otherwise. + */ +function rename(filename) { + const ext = path.extname(filename); + const base = path.basename(filename, ext); + const bare = base.match(/^(.*?)(-\d+)?$/)[1]; // Strips out any -\d+ suffix + const suffix = Number((base.match(/-(\d+)$/) || [])[1]) || 0; // Because NaN + return `${bare}-${suffix+1}${ext}`; +} + +/** + * Save `data` as `filename` in `dirname`. + */ +async function saveFile (dirname, filename, data) { + const fname = path.resolve(dirname, filename); + if (fs.existsSync(fname)) { + return await saveFile(dirname, rename(filename), data); + } else { + await fs.promises.writeFile(fname, data); + return filename; + } +} + +/** + * This method imitates the ASAQC endpoint + * /vt/v1/api/upload-file-encoded + * + * If OUTPUT_DIR is defined, it tries to save the + * received data as files in that directory, else + * it discards the data but still produces a + * plausible response. + */ +module.exports = async function (req, res, next) { + + try { + const payload = req.body; + const response = {id: uuid()}; + + if (payload.fileName && payload.encodedData) { + const data = Buffer.from(payload.encodedData, 'base64'); + + console.log(`Received ${payload.fileName}, ${data.length} bytes`); + + if (req.app.locals.OUTPUT_DIR) { + response.fileName = await saveFile(req.app.locals.OUTPUT_DIR, payload.fileName, data); + } else { + console.log("No disk output"); + } + + res.send(response); + } else { + res.status(400).send({message: "Bad syntax"}); + } + next(); + } catch (err) { + next(err); + } + +}; diff --git a/lib/www/server/queues/asaqc/dummy_server/index.js b/lib/www/server/queues/asaqc/dummy_server/index.js new file mode 100755 index 0000000..ede7309 --- /dev/null +++ b/lib/www/server/queues/asaqc/dummy_server/index.js @@ -0,0 +1,29 @@ +#!/usr/bin/node + +/** + * A minimalist mock-up of the ASAQC server, used + * for testing. + * + * Use the following environment variables to + * configure its behaviour: + * + * ASAQC_DUMMY_OUTPUT_DIR Path to a directory in which + * to store the received data. If not provided, the + * data will be discarded but the API will still return + * a success code. Alternatively, a path may be given + * in the command line. + * + * See also api/index.js for other environment + * variable options. + * + * Example command line: + * + * HTTPS_CA="./certs/client.pem" HTTPS_PEM="./certs/dougal.pem" ./index.js ./store + * + */ + +const api = require('./api'); + +const OUTPUT_DIR = process.argv[2] || process.env.ASAQC_DUMMY_OUTPUT_DIR; + +const server = api.start(process.env.HTTP_PORT || 3077, process.env.HTTP_PATH, {OUTPUT_DIR});