From 5b9d9aeca89f084876b77f08416f38b3ab9743a0 Mon Sep 17 00:00:00 2001 From: KevinMidboe Date: Sun, 3 Jan 2021 18:16:01 +0100 Subject: [PATCH] Setup logger, configloader, middleware & endpoints --- api/config/configuration.js | 45 +++++++++++++++++ api/config/environmentVariables.js | 15 ++++++ api/config/field.js | 49 +++++++++++++++++++ api/config/filters.js | 34 +++++++++++++ api/logger.js | 57 ++++++++++++++++++++++ api/webserver/middleware/addIdToRequest.js | 23 +++++++++ api/webserver/middleware/setupCORS.js | 6 +++ api/webserver/middleware/setupHeaders.js | 37 ++++++++++++++ api/webserver/server.js | 37 ++++++++++++++ 9 files changed, 303 insertions(+) create mode 100644 api/config/configuration.js create mode 100644 api/config/environmentVariables.js create mode 100644 api/config/field.js create mode 100644 api/config/filters.js create mode 100644 api/logger.js create mode 100644 api/webserver/middleware/addIdToRequest.js create mode 100644 api/webserver/middleware/setupCORS.js create mode 100644 api/webserver/middleware/setupHeaders.js create mode 100644 api/webserver/server.js diff --git a/api/config/configuration.js b/api/config/configuration.js new file mode 100644 index 0000000..6965fc4 --- /dev/null +++ b/api/config/configuration.js @@ -0,0 +1,45 @@ +const path = require('path'); +const Field = require('./field.js'); + +let instance = null; + +class Config { + constructor() { + this.location = Config.determineLocation(); + this.fields = require(`${this.location}`); + } + + static getInstance() { + if (instance == null) { + instance = new Config(); + } + return instance; + } + + static determineLocation() { + if (process.env.NODE_ENV === "production") + return path.join(__dirname, "../../config/env/production.json"); + return path.join(__dirname, "../../config/env/development.json"); + } + + get(section, option) { + if (this.fields[section] === undefined || this.fields[section][option] === undefined) { + throw new Error(`Field "${section} => ${option}" does not exist.`); + } + + const field = new Field(this.fields[section][option]); + + if (field.value === '') { + const envField = process.env[[section.toUpperCase(), option.toUpperCase()].join('_')]; + if (envField !== undefined && envField.length !== 0) { return envField; } + } + + if (field.value === undefined) { + throw new Error(`${section} => ${option} is empty.`); + } + + return field.value; + } +} + +module.exports = Config; diff --git a/api/config/environmentVariables.js b/api/config/environmentVariables.js new file mode 100644 index 0000000..b7fc3f2 --- /dev/null +++ b/api/config/environmentVariables.js @@ -0,0 +1,15 @@ +class EnvironmentVariables { + constructor(variables) { + this.variables = variables || process.env; + } + + get(variable) { + return this.variables[variable]; + } + + has(variable) { + return this.get(variable) !== undefined; + } +} + +module.exports = EnvironmentVariables; diff --git a/api/config/field.js b/api/config/field.js new file mode 100644 index 0000000..42eb8cd --- /dev/null +++ b/api/config/field.js @@ -0,0 +1,49 @@ +const Filters = require('./filters.js'); +const EnvironmentVariables = require('./environmentVariables.js'); + +class Field { + constructor(rawValue, environmentVariables) { + this.rawValue = rawValue; + this.filters = new Filters(rawValue); + this.valueWithoutFilters = this.filters.removeFiltersFromValue(); + this.environmentVariables = new EnvironmentVariables(environmentVariables); + } + + get value() { + if (this.filters.isEmpty()) { + return this.valueWithoutFilters; + } + + if (this.filters.has('base64') && !this.filters.has('env')) { + return Field.base64Decode(this.valueWithoutFilters); + } + + if (this.environmentVariables.has(this.valueWithoutFilters) && + this.environmentVariables.get(this.valueWithoutFilters) === '') { + return undefined; + } + + if (!this.filters.has('base64') && this.filters.has('env')) { + if (this.environmentVariables.has(this.valueWithoutFilters)) { + return this.environmentVariables.get(this.valueWithoutFilters); + } + return undefined; + } + + if (this.filters.has('env') && this.filters.has('base64')) { + if (this.environmentVariables.has(this.valueWithoutFilters)) { + const encodedEnvironmentVariable = this.environmentVariables.get(this.valueWithoutFilters); + return Field.base64Decode(encodedEnvironmentVariable); + } + return undefined; + } + + return this.valueWithoutFilters; + } + + static base64Decode(string) { + return new Buffer(string, 'base64').toString('utf-8'); + } +} + +module.exports = Field; diff --git a/api/config/filters.js b/api/config/filters.js new file mode 100644 index 0000000..b4ec359 --- /dev/null +++ b/api/config/filters.js @@ -0,0 +1,34 @@ +class Filters { + constructor(value) { + this.value = value; + this.delimiter = '|'; + } + + get filters() { + return this.value.split(this.delimiter).slice(0, -1); + } + + isEmpty() { + return !this.hasValidType() || this.value.length === 0; + } + + has(filter) { + return this.filters.includes(filter); + } + + hasValidType() { + return (typeof this.value === 'string'); + } + + removeFiltersFromValue() { + if (this.hasValidType() === false) { + return this.value; + } + + let filtersCombined = this.filters.join(this.delimiter); + filtersCombined += this.filters.length >= 1 ? this.delimiter : ''; + return this.value.replace(filtersCombined, ''); + } +} + +module.exports = Filters; diff --git a/api/logger.js b/api/logger.js new file mode 100644 index 0000000..36651c2 --- /dev/null +++ b/api/logger.js @@ -0,0 +1,57 @@ +const winston = require('winston'); +const httpContext = require("express-http-context"); + +const logLevel = 'trace'; + +const customLevels = { + levels: { + fatal: 0, + error: 1, + warning: 2, + info: 3, + debug: 4, + trace: 5 + }, + colors: { + trace: 'blue', + debug: 'white', + info: 'green', + warning: 'yellow', + error: 'red', + fatal: 'red' + } +}; + +const appendSessionId = winston.format(info => { + info.sessionId = httpContext.get("sessionId"); + return info +}); + + +const logger = winston.createLogger({ + level: logLevel, + levels: customLevels.levels, + transports: [ + new winston.transports.File({ + filename: `${__base}/logs/all-logs.log`, + format: winston.format.combine( + appendSessionId(), + winston.format.json() + ) + }) + ] +}); + +winston.addColors(customLevels.colors); + +if (process.env.NODE_ENV !== 'production') { + + logger.add(new winston.transports.Console({ + format: winston.format.combine( + winston.format.colorize(), + winston.format.simple() + ) + })); +}; + +module.exports = logger; \ No newline at end of file diff --git a/api/webserver/middleware/addIdToRequest.js b/api/webserver/middleware/addIdToRequest.js new file mode 100644 index 0000000..87d18a8 --- /dev/null +++ b/api/webserver/middleware/addIdToRequest.js @@ -0,0 +1,23 @@ +const crypto = require("crypto"); +const httpContext = require("express-http-context"); + +const addIdToRequest = (req, res, next) => { + try { + crypto.randomBytes(16, (err, buf) => { + if (err) { + // log err + id = null; + } + id = buf.toString("hex"); + + httpContext.set("sessionId", id); + next(); + }); + } catch (err) { + // log err + httpContext.set("sessionId", null); + next(); + } +}; + +module.exports = addIdToRequest; \ No newline at end of file diff --git a/api/webserver/middleware/setupCORS.js b/api/webserver/middleware/setupCORS.js new file mode 100644 index 0000000..76ef27f --- /dev/null +++ b/api/webserver/middleware/setupCORS.js @@ -0,0 +1,6 @@ +const openCORS = (req, res, next) => { + res.set("Access-Control-Allow-Origin", "*") + return next(); +}; + +module.exports = openCORS; \ No newline at end of file diff --git a/api/webserver/middleware/setupHeaders.js b/api/webserver/middleware/setupHeaders.js new file mode 100644 index 0000000..cd0abfa --- /dev/null +++ b/api/webserver/middleware/setupHeaders.js @@ -0,0 +1,37 @@ +const camelToKebabCase = str => str.replace(/[A-Z]/g, letter => `-${letter.toLowerCase()}`); + +const mapFeaturePolicyToString = (features) => { + return Object.entries(features).map(([key, value]) => { + key = camelToKebabCase(key) + value = value == "*" ? value : `'${ value }'` + return `${key} ${value}` + }).join("; ") +} + +const setupHeaders = (req, res, next) => { + res.set("Access-Control-Allow-Headers", "Content-Type") + + // Security + res.set("X-Content-Type-Options", "nosniff"); + res.set("X-XSS-Protection", "1; mode=block"); + res.set("X-Frame-Options", "SAMEORIGIN"); + res.set("X-DNS-Prefetch-Control", "off"); + res.set("X-Download-Options", "noopen"); + res.set("Strict-Transport-Security", "max-age=15552000; includeSubDomains") + + // Feature policy + const features = { + fullscreen: "*", + payment: "none", + microphone: "none", + camera: "self", + speaker: "*", + syncXhr: "self" + } + const featureString = mapFeaturePolicyToString(features); + res.set("Feature-Policy", featureString) + + return next(); +} + +module.exports = setupHeaders; \ No newline at end of file diff --git a/api/webserver/server.js b/api/webserver/server.js new file mode 100644 index 0000000..b26104c --- /dev/null +++ b/api/webserver/server.js @@ -0,0 +1,37 @@ +const express = require("express"); +const app = express(); +const path = require("path"); +global.__base = path.join(__dirname, ".."); +global.__middleware = path.join(__dirname, "middleware"); +global.__controllers = path.join(__dirname, "controllers"); + +// logging +const logger = require(`${__base}/logger`); + +// middleware +const httpContext = require("express-http-context"); +const setupCORS = require(`${__middleware}/setupCORS`); +const setupHeaders = require(`${__middleware}/setupHeaders`); +const addIdToRequest = require(`${__middleware}/addIdToRequest`); +app.use(httpContext.middleware); +app.use(setupCORS); +app.use(setupHeaders); +app.use(addIdToRequest); + +// parse application/json +app.use(express.json()); + +const router = express.Router(); +// const TokenController = require(`${__controllers}/tokenController`); +const PostController = require(`${__controllers}/postController`); + +router.get("/api/post/:id/render", PostController.renderPost); +router.get("/api/post/:id", PostController.getPost); +router.put("/api/post/:id", PostController.updatePost); +// router.post("/api/payment/callback/v2/payments/:id", PaymentController.updatePayment); + +app.use(router); + +logger.info("Server started, listening at :30010"); + +app.listen(30010);