Add ASAQC API mock-up.

To be used for testing and debugging. See
index.js for instructions.
This commit is contained in:
D. Berge
2021-10-04 01:55:38 +02:00
parent dfbccf3bc6
commit 0f447fc27d
4 changed files with 256 additions and 0 deletions

View File

@@ -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;
}

View File

@@ -0,0 +1,3 @@
module.exports = {
post: require('./post')
};

View File

@@ -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);
}
};

View File

@@ -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});