From 7b682e8e9576fad3dd2a0bbaf408a91ddcc7d110 Mon Sep 17 00:00:00 2001 From: Jonatan Nilsson Date: Thu, 24 Mar 2022 09:29:54 +0000 Subject: [PATCH] Constructor: Support for defaultHeaders was added with default secure options. CSP: Added smart CSP support with nonce support as well. Can generate unique nonce values for each request. CorsHandler: Started development of basic cors handler. --- benchmark/index.bat | 3 +- benchmark/index.js | 192 +++++++++- benchmark/package-lock.json | 725 +++++++++++++++++++++++++++++++++++- flaska.mjs | 129 ++++++- test/flaska.api.test.mjs | 297 +++++++++++++-- test/flaska.in.test.mjs | 43 +++ test/helper.mjs | 2 + test/middlewares.test.mjs | 94 ++++- 8 files changed, 1430 insertions(+), 55 deletions(-) diff --git a/benchmark/index.bat b/benchmark/index.bat index 6a90fad..b61fc90 100644 --- a/benchmark/index.bat +++ b/benchmark/index.bat @@ -1,2 +1 @@ -start /B /WAIT /REALTIME node index.js -pause \ No newline at end of file +start /B /WAIT /REALTIME node index.js \ No newline at end of file diff --git a/benchmark/index.js b/benchmark/index.js index 2b43e76..b895214 100644 --- a/benchmark/index.js +++ b/benchmark/index.js @@ -1,3 +1,4 @@ +import crypto from 'crypto' import assert from 'assert' import Benchmark from 'benchmarkjs-pretty' import { koaRouter1, koaRouter2 } from './router_koa.js' @@ -176,7 +177,89 @@ function TestObjectAssign() { ctx.router2 = flaskaRouter2 } + function registerHeader(ctx) { + ctx.headers['Server'] = 'nginx/1.16.1' + ctx.headers['Date'] = 'Mon, 21 Mar 2022 07:26:01 GMT' + ctx.headers['Content-Type'] = 'application/json; charset=utf-8' + ctx.headers['Content-Length'] = '1646' + ctx.headers['Connection'] = 'keep-alive' + ctx.headers['vary'] = 'Origin' + ctx.headers['Link'] = '; rel="current"; title="Page 1"' + ctx.headers['pagination_total'] = '7' + ctx.headers['X-Frame-Options'] = 'DENY' + ctx.headers['X-Content-Type-Options'] = 'nosniff' + } + + function registerHeaderAlt(ctx) { + ctx.headers = { + 'Server': 'nginx/1.16.1', + 'Date': 'Mon, 21 Mar 2022 07:26:01 GMT', + 'Content-Type': 'application/json; charset=utf-8', + 'Content-Length': '1646', + 'Connection': 'keep-alive', + 'vary': 'Origin', + 'Link': '; rel="current"; title="Page 1"', + 'pagination_total': '7', + 'X-Frame-Options': 'DENY', + 'X-Content-Type-Options': 'nosniff', + } + } + + let baseHeaders = { + 'Server': 'nginx/1.16.1', + 'Date': 'Mon, 21 Mar 2022 07:26:01 GMT', + 'Content-Type': 'application/json; charset=utf-8', + 'Content-Length': '1646', + 'Connection': 'keep-alive', + 'vary': 'Origin', + 'Link': '; rel="current"; title="Page 1"', + 'pagination_total': '7', + 'X-Frame-Options': 'DENY', + 'X-Content-Type-Options': 'nosniff', + } + let keys = Object.keys(baseHeaders) + + return new Benchmark.default('test different method to initialize objects)') + .add('[HEADERS] Object.assign()', function() { + let ctx = { + headers: {} + } + Object.assign(ctx.headers, baseHeaders) + // assert.notStrictEqual(ctx.headers, baseHeaders) + }) + .add('[HEADERS] ecmascript spread', function() { + let ctx = { + headers: { + ...baseHeaders + } + } + // assert.notStrictEqual(ctx.headers, baseHeaders) + }) + .add('[HEADERS] Basic clone lol', function() { + let ctx = { + headers: {} + } + for (let key of keys) { + ctx.headers[key] = baseHeaders[key] + } + // assert.notStrictEqual(ctx.headers, baseHeaders) + }) + .add('[HEADERS] Use register function', function() { + let ctx = { + headers: {} + } + registerHeader(ctx) + // assert.notStrictEqual(ctx.headers, baseHeaders) + }) + .add('[HEADERS] Use register function ALT', function() { + let ctx = { + headers: {} + } + registerHeaderAlt(ctx) + // assert.notStrictEqual(ctx.headers, baseHeaders) + }) + /* .add('Object.assign()', function() { let ctx = { req: req, @@ -196,7 +279,7 @@ function TestObjectAssign() { register3(ctx) ctx.log.info() }) - /*.add('Object.create() all props', function() { + .add('Object.create() all props', function() { let ctx = Object.create({}, propMakerAlt) ctx.log.info() }) @@ -212,7 +295,100 @@ function TestObjectAssign() { let ctx = { } Object.defineProperties(ctx, propMakerAlt) ctx.log.info() - })*/ + }) + */ + .run() + .then(function() {}, function(e) { + console.error('error:', e) + process.exit(1) + }) +} + +function TestGenerateRandomString() { + + return new Benchmark.default('test different method to generate random string)') + .add('crypto.randomBytes(16)', function() { + for (let i = 0; i < 25; i++) { + crypto.randomBytes(16).toString('base64') + } + }) + .add('crypto.randomBytes(32)', function() { + for (let i = 0; i < 25; i++) { + crypto.randomBytes(32).toString('base64') + } + }) + .add('random (22 characters long)', function() { + for (let i = 0; i < 25; i++) { + let out = Math.random().toString(36).substring(2, 24) + + Math.random().toString(36).substring(2, 24) + } + }) + .add('random (44 characters long)', function() { + for (let i = 0; i < 25; i++) { + let out = Math.random().toString(36).substring(2, 24) + + Math.random().toString(36).substring(2, 24) + + Math.random().toString(36).substring(2, 24) + + Math.random().toString(36).substring(2, 24) + } + }) + .run() + .then(function() {}, function(e) { + console.error('error:', e) + process.exit(1) + }) +} + +function TestArrayReduce() { + return new Benchmark.default('test different method to reduce array)') + .add('currIndex', function() { + const arr1 = new Array(100) + + for (let i = 0; i < arr1.length; i++) { + arr1[i] = 'a' + } + + let currIndex = arr1.length - 1 + + let out = '' + while (currIndex >= 0) { + out += arr1[currIndex] + currIndex-- + } + }) + .add('crypto.randomBytes(32)', function() { + const arr1 = new Array(100) + + for (let i = 0; i < arr1.length; i++) { + arr1[i] = 'a' + } + + let out = '' + while (arr1.length > 0) { + out += arr1.splice(0, 1)[0] + } + }) + .run() + .then(function() {}, function(e) { + console.error('error:', e) + process.exit(1) + }) +} + +function TestStringCombination() { + let val1 = 'some' + let val2 = 'text' + let val3 = 'gose' + let val4 = 'here' + + return new Benchmark.default('test different method to combine string)') + .add('ES6 with variable', function() { + let out = `Hello my friend ${val1} this goes to ${val2} all my homies ${val3} over at my ${val4} house` + return out + }) + .add('String concatenation', function() { + let out = 'Hello my friend ' + val1 + ' this goes to ' + val2 + ' all my homies ' + val3 + ' over at my ' + val4 + ' house' + return out + }) .run() .then(function() {}, function(e) { console.error('error:', e) @@ -345,8 +521,8 @@ function TestLargeParamLargeUrlRoute() { }) } +/* TestSmallStaticRoute() -// TestObjectAssign() // TestPromiseCreators() .then(function() { return TestSmallParamRoute() @@ -362,4 +538,12 @@ TestSmallStaticRoute() }) .then(function() { process.exit(0) - }) + })*/ + +// TestObjectAssign() +// TestGenerateRandomString() +// TestArrayReduce() +TestStringCombination() +.then(function() { + process.exit(0) +}) \ No newline at end of file diff --git a/benchmark/package-lock.json b/benchmark/package-lock.json index 61b522f..6f70a73 100644 --- a/benchmark/package-lock.json +++ b/benchmark/package-lock.json @@ -1,8 +1,731 @@ { "name": "benchmark", "version": "1.0.0", - "lockfileVersion": 1, + "lockfileVersion": 2, "requires": true, + "packages": { + "": { + "name": "benchmark", + "version": "1.0.0", + "license": "WTFPL", + "dependencies": { + "benchmarkjs-pretty": "^2.0.0", + "express": "^4.17.1", + "koa-router": "^8.0.8" + } + }, + "node_modules/@types/benchmark": { + "version": "1.0.31", + "resolved": "https://registry.npmjs.org/@types/benchmark/-/benchmark-1.0.31.tgz", + "integrity": "sha512-F6fVNOkGEkSdo/19yWYOwVKGvzbTeWkR/XQYBKtGBQ9oGRjBN9f/L4aJI4sDcVPJO58Y1CJZN8va9V2BhrZapA==" + }, + "node_modules/@types/chalk": { + "version": "0.4.31", + "resolved": "https://registry.npmjs.org/@types/chalk/-/chalk-0.4.31.tgz", + "integrity": "sha1-ox10JBprHtu5c8822XooloNKUfk=" + }, + "node_modules/accepts": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.7.tgz", + "integrity": "sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA==", + "dependencies": { + "mime-types": "~2.1.24", + "negotiator": "0.6.2" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=" + }, + "node_modules/benchmark": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/benchmark/-/benchmark-2.1.4.tgz", + "integrity": "sha1-CfPeMckWQl1JjMLuVloOvzwqVik=", + "dependencies": { + "lodash": "^4.17.4", + "platform": "^1.3.3" + } + }, + "node_modules/benchmarkjs-pretty": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/benchmarkjs-pretty/-/benchmarkjs-pretty-2.0.0.tgz", + "integrity": "sha512-t5a+ztdAuim1HPEbQwBELN9ugqe5WCSbjwPh79olqS+zgw44Bi/3qPz472LNPIfXlTernAo+meL8KULaXuWAeQ==", + "dependencies": { + "@types/benchmark": "^1.0.30", + "@types/chalk": "^0.4.31", + "benchmark": "^2.1.4", + "chalk": "^2.0.1" + } + }, + "node_modules/body-parser": { + "version": "1.19.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.19.0.tgz", + "integrity": "sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw==", + "dependencies": { + "bytes": "3.1.0", + "content-type": "~1.0.4", + "debug": "2.6.9", + "depd": "~1.1.2", + "http-errors": "1.7.2", + "iconv-lite": "0.4.24", + "on-finished": "~2.3.0", + "qs": "6.7.0", + "raw-body": "2.4.0", + "type-is": "~1.6.17" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/body-parser/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/body-parser/node_modules/http-errors": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.2.tgz", + "integrity": "sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg==", + "dependencies": { + "depd": "~1.1.2", + "inherits": "2.0.3", + "setprototypeof": "1.1.1", + "statuses": ">= 1.5.0 < 2", + "toidentifier": "1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/body-parser/node_modules/inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" + }, + "node_modules/body-parser/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + }, + "node_modules/bytes": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz", + "integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=" + }, + "node_modules/content-disposition": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.3.tgz", + "integrity": "sha512-ExO0774ikEObIAEV9kDo50o+79VCUdEB6n6lzKgGwupcVeRlhrj3qGAfwq8G6uBJjkqLrhT0qEYFcWng8z1z0g==", + "dependencies": { + "safe-buffer": "5.1.2" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", + "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.0.tgz", + "integrity": "sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=" + }, + "node_modules/debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "deprecated": "Debug versions >=3.2.0 <3.2.7 || >=4 <4.3.1 have a low-severity ReDos regression when used in a Node.js environment. It is recommended you upgrade to 3.2.7 or 4.3.1. (https://github.com/visionmedia/debug/issues/797)", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/depd": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/destroy": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz", + "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=" + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=" + }, + "node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=" + }, + "node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express": { + "version": "4.17.1", + "resolved": "https://registry.npmjs.org/express/-/express-4.17.1.tgz", + "integrity": "sha512-mHJ9O79RqluphRrcw2X/GTh3k9tVv8YcoyY4Kkh4WDMUYKRZUq0h1o0w2rrrxBqM7VoeUVqgb27xlEMXTnYt4g==", + "dependencies": { + "accepts": "~1.3.7", + "array-flatten": "1.1.1", + "body-parser": "1.19.0", + "content-disposition": "0.5.3", + "content-type": "~1.0.4", + "cookie": "0.4.0", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "~1.1.2", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "~1.1.2", + "fresh": "0.5.2", + "merge-descriptors": "1.0.1", + "methods": "~1.1.2", + "on-finished": "~2.3.0", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.7", + "proxy-addr": "~2.0.5", + "qs": "6.7.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.1.2", + "send": "0.17.1", + "serve-static": "1.14.1", + "setprototypeof": "1.1.1", + "statuses": "~1.5.0", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/express/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/express/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + }, + "node_modules/finalhandler": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz", + "integrity": "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "on-finished": "~2.3.0", + "parseurl": "~1.3.3", + "statuses": "~1.5.0", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/finalhandler/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/finalhandler/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + }, + "node_modules/forwarded": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz", + "integrity": "sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ=", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "engines": { + "node": ">=4" + } + }, + "node_modules/http-errors": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.3.tgz", + "integrity": "sha512-ZTTX0MWrsQ2ZAhA1cejAwDLycFsd7I7nVtnkT3Ol0aqodaKW+0CTZDQ1uBv5whptCnc8e8HeRRJxRs0kmm/Qfw==", + "dependencies": { + "depd": "~1.1.2", + "inherits": "2.0.4", + "setprototypeof": "1.1.1", + "statuses": ">= 1.5.0 < 2", + "toidentifier": "1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=" + }, + "node_modules/koa-compose": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/koa-compose/-/koa-compose-4.1.0.tgz", + "integrity": "sha512-8ODW8TrDuMYvXRwra/Kh7/rJo9BtOfPc6qO8eAfC80CnCvSjSl0bkRM24X6/XBBEyj0v1nRUQ1LyOy3dbqOWXw==" + }, + "node_modules/koa-router": { + "version": "8.0.8", + "resolved": "https://registry.npmjs.org/koa-router/-/koa-router-8.0.8.tgz", + "integrity": "sha512-2rNF2cgu/EWi/NV8GlBE5+H/QBoaof83X6Z0dULmalkbt7W610/lyP2EOLVqVrUUFfjsVWL/Ju5TVBcGJDY9XQ==", + "dependencies": { + "debug": "^4.1.1", + "http-errors": "^1.7.3", + "koa-compose": "^4.1.0", + "methods": "^1.1.2", + "path-to-regexp": "1.x", + "urijs": "^1.19.2" + }, + "engines": { + "node": ">= 8.0.0" + } + }, + "node_modules/koa-router/node_modules/path-to-regexp": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.8.0.tgz", + "integrity": "sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA==", + "dependencies": { + "isarray": "0.0.1" + } + }, + "node_modules/lodash": { + "version": "4.17.15", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz", + "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==" + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", + "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=" + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.43.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.43.0.tgz", + "integrity": "sha512-+5dsGEEovYbT8UY9yD7eE4XTc4UwJ1jBYlgaQQF38ENsKR3wj/8q8RFZrF9WIZpB2V1ArTVFUva8sAul1NzRzQ==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.26", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.26.tgz", + "integrity": "sha512-01paPWYgLrkqAyrlDorC1uDwl2p3qZT7yl806vW7DvDoxwXi46jsjFbg+WdwotBIk6/MbEhO/dh5aZ5sNj/dWQ==", + "dependencies": { + "mime-db": "1.43.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "node_modules/negotiator": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz", + "integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/on-finished": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", + "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-to-regexp": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", + "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=" + }, + "node_modules/platform": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/platform/-/platform-1.3.5.tgz", + "integrity": "sha512-TuvHS8AOIZNAlE77WUDiR4rySV/VMptyMfcfeoMgs4P8apaZM3JrnbzBiixKUv+XR6i+BXrQh8WAnjaSPFO65Q==" + }, + "node_modules/proxy-addr": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.6.tgz", + "integrity": "sha512-dh/frvCBVmSsDYzw6n926jv974gddhkFPfiN8hPOi30Wax25QZyZEGveluCgliBnqmuM+UJmBErbAUFIoDbjOw==", + "dependencies": { + "forwarded": "~0.1.2", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/qs": { + "version": "6.7.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz", + "integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.4.0.tgz", + "integrity": "sha512-4Oz8DUIwdvoa5qMJelxipzi/iJIi40O5cGV1wNYp5hvZP8ZN0T+jiNkL0QepXs+EsQ9XJ8ipEDoiH70ySUJP3Q==", + "dependencies": { + "bytes": "3.1.0", + "http-errors": "1.7.2", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/raw-body/node_modules/http-errors": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.2.tgz", + "integrity": "sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg==", + "dependencies": { + "depd": "~1.1.2", + "inherits": "2.0.3", + "setprototypeof": "1.1.1", + "statuses": ">= 1.5.0 < 2", + "toidentifier": "1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body/node_modules/inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" + }, + "node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "node_modules/send": { + "version": "0.17.1", + "resolved": "https://registry.npmjs.org/send/-/send-0.17.1.tgz", + "integrity": "sha512-BsVKsiGcQMFwT8UxypobUKyv7irCNRHk1T0G680vk88yf6LBByGcZJOTJCrTP2xVN6yI+XjPJcNuE3V4fT9sAg==", + "dependencies": { + "debug": "2.6.9", + "depd": "~1.1.2", + "destroy": "~1.0.4", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "~1.7.2", + "mime": "1.6.0", + "ms": "2.1.1", + "on-finished": "~2.3.0", + "range-parser": "~1.2.1", + "statuses": "~1.5.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/send/node_modules/debug/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + }, + "node_modules/send/node_modules/ms": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", + "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==" + }, + "node_modules/serve-static": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.14.1.tgz", + "integrity": "sha512-JMrvUwE54emCYWlTI+hGrGv5I8dEwmco/00EvkzIIsR7MqrHonbD9pO2MOfFnpFntl7ecpZs+3mW+XbQZu9QCg==", + "dependencies": { + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.17.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/setprototypeof": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz", + "integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==" + }, + "node_modules/statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/toidentifier": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz", + "integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/urijs": { + "version": "1.19.2", + "resolved": "https://registry.npmjs.org/urijs/-/urijs-1.19.2.tgz", + "integrity": "sha512-s/UIq9ap4JPZ7H1EB5ULo/aOUbWqfDi7FKzMC2Nz+0Si8GiT1rIEaprt8hy3Vy2Ex2aJPpOQv4P4DuOZ+K1c6w==" + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=", + "engines": { + "node": ">= 0.8" + } + } + }, "dependencies": { "@types/benchmark": { "version": "1.0.31", diff --git a/flaska.mjs b/flaska.mjs index 29b2fe5..a18e228 100644 --- a/flaska.mjs +++ b/flaska.mjs @@ -1,4 +1,5 @@ import os from 'os' +import crypto from 'crypto' import path from 'path' import http from 'http' import stream from 'stream' @@ -128,6 +129,28 @@ export function JsonHandler(org = {}) { } } +export function CorsHandler(opts = {}) { + const options = { + allowedMethods: opts.allowedMethods || 'GET,HEAD,PUT,POST,DELETE,PATCH', + allowedHeaders: opts.allowedHeaders, + openerPolicy: 'same-origin', + resourcePolicy: 'same-origin', + embedderPolicy: 'require-corp', + } + + return function(ctx) { + let origin = ctx.req.headers['origin'] + let reqHeaders = options.allowedHeaders || ctx.req.headers['access-control-request-headers'] + + ctx.headers['Access-Control-Allow-Origin'] = origin + ctx.headers['Access-Control-Allow-Methods'] = options.allowedMethods + if (reqHeaders && options.allowedHeaders !== false) { + ctx.headers['Access-Control-Allow-Headers'] = reqHeaders + } + ctx.status = 204 + } +} + export function FormidableHandler(formidable, org = {}) { let lastDateString = '' let incrementor = 1 @@ -420,7 +443,7 @@ export class FlaskaRouter { * Flaska */ export class Flaska { - constructor(opts, orgHttp = http, orgStream = stream) { + constructor(opts = {}, orgHttp = http, orgStream = stream) { this._before = [] this._beforeCompiled = null this._beforeAsync = [] @@ -430,10 +453,12 @@ export class Flaska { this._afterAsync = [] this._afterAsyncCompiled = null this._on404 = function(ctx) { - ctx.status = 404 - ctx.body = { - status: 404, - message: statuses[404], + if (ctx.body == null && ctx.status !== 204) { + ctx.status = 404 + ctx.body = { + status: 404, + message: statuses[404], + } } } this._backuperror = this._onerror = function(err, ctx) { @@ -465,17 +490,91 @@ export class Flaska { ctx.log.error(err) } - let options = opts || {} - - this.log = options.log || { - fatal: console.error.bind(console), - error: console.error.bind(console), - warn: console.log.bind(console), - info: console.log.bind(console), - debug: console.debug.bind(console), - trace: console.debug.bind(console), - log: console.log.bind(console), + let options = { + defaultHeaders: opts.defaultHeaders || { + 'Server': 'Flaska', + 'X-Content-Type-Options': 'nosniff', + 'Content-Security-Policy': `default-src 'self'; style-src 'self' 'unsafe-inline'; img-src * data: blob:; object-src 'none'; frame-ancestors 'none'`, + 'Cross-Origin-Opener-Policy': 'same-origin', + 'Cross-Origin-Resource-Policy': 'same-origin', + 'Cross-Origin-Embedder-Policy': 'require-corp', + }, + log: opts.log || { + fatal: console.error.bind(console), + error: console.error.bind(console), + warn: console.log.bind(console), + info: console.log.bind(console), + debug: console.debug.bind(console), + trace: console.debug.bind(console), + log: console.log.bind(console), + }, + nonce: opts.nonce || [], + nonceCacheLength: opts.nonceCacheLength || 25 } + + if (!options.defaultHeaders && options.nonce.length) { + // throw error + } + + let headerKeys = Object.keys(options.defaultHeaders) + let constructFunction = '' + + if (options.nonce.length) { + this._nonces = new Array(options.nonceCacheLength) + this._noncesIndex = this._nonces.length - 1 + + for (let i = 0; i < this._nonces.length; i++) { + this._nonces[i] = crypto.randomBytes(16).toString('base64') + } + + constructFunction += ` +let nonce = this._nonces[this._noncesIndex] || crypto.randomBytes(16).toString('base64'); +this._noncesIndex--; +ctx.state.nonce = nonce; +` + } + + constructFunction += 'ctx.headers = {' + for (let key of headerKeys) { + if (key === 'Content-Security-Policy' && options.nonce.length) { + let groups = options.defaultHeaders[key].split(';') + for (let ni = 0; ni < options.nonce.length; ni++) { + let found = false + for (let x = 0; x < groups.length; x++) { + if (groups[x].trim().startsWith(options.nonce[ni])) { + groups[x] = groups[x].trimEnd() + ` 'nonce-$'` + found = true + break + } + } + if (!found) { + groups.push(` ${options.nonce[ni]} 'nonce-$'`) + } + } + groups = groups.join(';').replace(/\'/g, "\\'").split('$') + constructFunction += `'${key}': '${groups.join(`' + nonce + '`)}',` + } else { + constructFunction += `'${key}': '${options.defaultHeaders[key].replace(/\'/g, "\\'")}',` + } + } + constructFunction += '};' + + // console.log(constructFunction) + + if (options.nonce.length) { + this.before(new Function('crypto', 'ctx', constructFunction).bind(this, crypto)) + this.after(new Function('crypto', 'ctx', ` + this._noncesIndex = Math.max(this._noncesIndex, -1); + if (this._noncesIndex < this._nonces.length - 1) { + this._noncesIndex++; + this._nonces[this._noncesIndex] = crypto.randomBytes(16).toString('base64'); + } + `).bind(this, crypto)) + } else { + this.before(new Function('ctx', constructFunction).bind(this)) + } + + this.log = options.log this.http = orgHttp this.stream = orgStream this.server = null diff --git a/test/flaska.api.test.mjs b/test/flaska.api.test.mjs index 9823da9..a777478 100644 --- a/test/flaska.api.test.mjs +++ b/test/flaska.api.test.mjs @@ -4,34 +4,250 @@ import { createCtx, fakeHttp } from './helper.mjs' const faker = fakeHttp() -t.test('should be able to override the http', function() { - let flaska = new Flaska({}, faker) - assert.strictEqual(flaska.http, faker) +t.describe('#constructor', function() { + t.test('should be able to override the http', function() { + let flaska = new Flaska({}, faker) + assert.strictEqual(flaska.http, faker) + }) + + t.test('it should have all the common verbs', function() { + let flaska = new Flaska({}, faker) + assert.ok(flaska.get) + assert.strictEqual(typeof(flaska.get), 'function') + assert.ok(flaska.post) + assert.strictEqual(typeof(flaska.post), 'function') + assert.ok(flaska.put) + assert.strictEqual(typeof(flaska.put), 'function') + assert.ok(flaska.delete) + assert.strictEqual(typeof(flaska.delete), 'function') + assert.ok(flaska.options) + assert.strictEqual(typeof(flaska.options), 'function') + assert.ok(flaska.patch) + assert.strictEqual(typeof(flaska.patch), 'function') + }) + + t.test('the verbs GET and HEAD should be identical', function() { + let flaska = new Flaska({}, faker) + assert.ok(flaska.get) + assert.strictEqual(typeof(flaska.get), 'function') + assert.notOk(flaska.head) + assert.ok(flaska.routers['HEAD']) + assert.strictEqual(flaska.routers['GET'], flaska.routers['HEAD']) + }) + + t.test('should have before default header generator', function() { + let flaska = new Flaska({}, faker) + assert.strictEqual(flaska._before.length, 1) + + let ctx = {} + + flaska._before[0](ctx) + + assert.deepStrictEqual(ctx.headers, { + 'Server': 'Flaska', + 'X-Content-Type-Options': 'nosniff', + 'Content-Security-Policy': `default-src 'self'; style-src 'self' 'unsafe-inline'; img-src * data: blob:; object-src 'none'; frame-ancestors 'none'`, + 'Cross-Origin-Opener-Policy': 'same-origin', + 'Cross-Origin-Resource-Policy': 'same-origin', + 'Cross-Origin-Embedder-Policy': 'require-corp', + }) + assert.strictEqual(flaska._after.length, 0) + }) + + t.test('should have before ready setting headers on context if defaultHeaders is specified', function() { + const defaultHeaders = { + 'Server': 'nginx/1.16.1', + 'Date': 'Mon, 21 Mar\' 2022 07:26:01 GMT', + 'Content-Type': 'applicat"ion/json; charset=utf-8', + 'Content-Length': '1646', + 'Connection': 'keep-alive', + 'vary': 'Origin', + 'Link': 'Link goes here', + 'X-Frame-Options': 'DENY', + 'X-Content-Type-Options': 'nosniff', + } + let flaska = new Flaska({ + defaultHeaders: defaultHeaders, + }, faker) + assert.strictEqual(flaska._before.length, 1) + + let ctx = {} + + flaska._before[0](ctx) + + assert.notStrictEqual(ctx.headers, defaultHeaders) + assert.deepStrictEqual(ctx.headers, defaultHeaders) + assert.strictEqual(flaska._after.length, 0) + }) }) -t.test('it should have all the common verbs', function() { - let flaska = new Flaska({}, faker) - assert.ok(flaska.get) - assert.strictEqual(typeof(flaska.get), 'function') - assert.ok(flaska.post) - assert.strictEqual(typeof(flaska.post), 'function') - assert.ok(flaska.put) - assert.strictEqual(typeof(flaska.put), 'function') - assert.ok(flaska.delete) - assert.strictEqual(typeof(flaska.delete), 'function') - assert.ok(flaska.options) - assert.strictEqual(typeof(flaska.options), 'function') - assert.ok(flaska.patch) - assert.strictEqual(typeof(flaska.patch), 'function') -}) +t.describe('#_nonce', function() { + t.test('should support nonce parameter with cached pre-filled entries', function() { + let flaska = new Flaska({ + nonce: ['script-src', 'style-src'], + }, faker) -t.test('the verbs GET and HEAD should be identical', function() { - let flaska = new Flaska({}, faker) - assert.ok(flaska.get) - assert.strictEqual(typeof(flaska.get), 'function') - assert.notOk(flaska.head) - assert.ok(flaska.routers['HEAD']) - assert.strictEqual(flaska.routers['GET'], flaska.routers['HEAD']) + assert.ok(flaska._nonces) + assert.strictEqual(flaska._noncesIndex + 1, flaska._nonces.length) + assert.strictEqual(flaska._nonces.length, 25) + assert.ok(flaska._nonces[flaska._noncesIndex]) + + //Check they're all unique + let set = new Set() + flaska._nonces.forEach(function(entry) { + set.add(entry) + }) + assert.strictEqual(set.size, flaska._nonces.length) + + let ctx = createCtx() + + assert.notOk(ctx.state.nonce) + + let oldIndex = flaska._noncesIndex + + flaska._before[0](ctx) + + assert.ok(ctx.state.nonce) + assert.strictEqual(flaska._noncesIndex, oldIndex - 1) + assert.strictEqual(flaska._nonces[oldIndex], ctx.state.nonce) + + assert.deepStrictEqual(ctx.headers, { + 'Server': 'Flaska', + 'X-Content-Type-Options': 'nosniff', + 'Content-Security-Policy': `default-src 'self'; style-src 'self' 'unsafe-inline' 'nonce-${ctx.state.nonce}'; img-src * data: blob:; object-src 'none'; frame-ancestors 'none'; script-src 'nonce-${ctx.state.nonce}'`, + 'Cross-Origin-Opener-Policy': 'same-origin', + 'Cross-Origin-Resource-Policy': 'same-origin', + 'Cross-Origin-Embedder-Policy': 'require-corp', + }) + }) + + t.test('should always return nonce values even if it runs out in cache', function() { + let flaska = new Flaska({ + nonce: ['script-src'], + nonceCacheLength: 5, + }, faker) + let ctx = createCtx() + + for (let i = 0; i < 5; i++) { + let nextNonce = flaska._nonces[flaska._noncesIndex] + flaska._before[0](ctx) + assert.strictEqual(ctx.state.nonce, nextNonce) + assert.strictEqual(ctx.headers['Content-Security-Policy'], `default-src 'self'; style-src 'self' 'unsafe-inline'; img-src * data: blob:; object-src 'none'; frame-ancestors 'none'; script-src 'nonce-${ctx.state.nonce}'`) + } + + assert.notOk(flaska._nonces[flaska._noncesIndex]) + + flaska._before[0](ctx) + + assert.notOk(flaska._nonces[flaska._noncesIndex]) + assert.ok(ctx.state.nonce) + + for (let i = 0; i < flaska._nonces.length; i++) { + assert.notStrictEqual(ctx.state.nonce, flaska._nonces[i]) + } + + assert.strictEqual(ctx.headers['Content-Security-Policy'], `default-src 'self'; style-src 'self' 'unsafe-inline'; img-src * data: blob:; object-src 'none'; frame-ancestors 'none'; script-src 'nonce-${ctx.state.nonce}'`) + }) + + t.test('should have after that regenerates lost hashes', function() { + let flaska = new Flaska({ + nonce: ['script-src'], + nonceCacheLength: 5, + }, faker) + let ctx = createCtx() + + assert.strictEqual(flaska._after.length, 1) + + //Check they're all unique + let set = new Set() + flaska._nonces.forEach(function(entry) { + set.add(entry) + }) + assert.strictEqual(set.size, 5) + + flaska._before[0](ctx) + flaska._before[0](ctx) + flaska._before[0](ctx) + + assert.strictEqual(flaska._noncesIndex, 1) + + flaska._after[0](ctx) + assert.strictEqual(flaska._noncesIndex, 2) + set.add(flaska._nonces[flaska._noncesIndex]) + assert.strictEqual(set.size, 6) + + flaska._after[0](ctx) + assert.strictEqual(flaska._noncesIndex, 3) + set.add(flaska._nonces[flaska._noncesIndex]) + assert.strictEqual(set.size, 7) + + flaska._after[0](ctx) + assert.strictEqual(flaska._noncesIndex, 4) + set.add(flaska._nonces[flaska._noncesIndex]) + assert.strictEqual(set.size, 8) + + flaska._after[0](ctx) + assert.strictEqual(flaska._noncesIndex, 4) + set.add(flaska._nonces[flaska._noncesIndex]) + assert.strictEqual(set.size, 8) + }) + + t.test('after should not generate keys outside range', function() { + let flaska = new Flaska({ + nonce: ['script-src'], + nonceCacheLength: 2, + }, faker) + let ctx = createCtx() + + assert.strictEqual(flaska._after.length, 1) + + //Check they're all unique + let set = new Set() + flaska._nonces.forEach(function(entry) { + set.add(entry) + }) + assert.strictEqual(set.size, 2) + + flaska._before[0](ctx) + flaska._before[0](ctx) + + assert.strictEqual(flaska._noncesIndex, -1) + + flaska._before[0](ctx) + assert.strictEqual(flaska._noncesIndex, -2) + set.add(ctx.state.nonce) + assert.strictEqual(set.size, 3) + + flaska._before[0](ctx) + assert.strictEqual(flaska._noncesIndex, -3) + set.add(ctx.state.nonce) + assert.strictEqual(set.size, 4) + + flaska._before[0](ctx) + assert.strictEqual(flaska._noncesIndex, -4) + set.add(ctx.state.nonce) + assert.strictEqual(set.size, 5) + + assert.strictEqual(Object.keys(flaska._nonces).length, 2) + + flaska._after[0](ctx) + assert.strictEqual(flaska._noncesIndex, 0) + set.add(flaska._nonces[flaska._noncesIndex]) + assert.strictEqual(set.size, 6) + assert.strictEqual(Object.keys(flaska._nonces).length, 2) + + flaska._after[0](ctx) + assert.strictEqual(flaska._noncesIndex, 1) + set.add(flaska._nonces[flaska._noncesIndex]) + assert.strictEqual(set.size, 7) + assert.strictEqual(Object.keys(flaska._nonces).length, 2) + + flaska._after[0](ctx) + assert.strictEqual(flaska._noncesIndex, 1) + set.add(flaska._nonces[flaska._noncesIndex]) + assert.strictEqual(set.size, 7) + assert.strictEqual(Object.keys(flaska._nonces).length, 2) + }) }) t.describe('#log', function() { @@ -108,11 +324,6 @@ specialHandlers.forEach(function(type) { }) t.describe('_on404', function() { - t.test('a valid function', function() { - let flaska = new Flaska({}, faker) - assert.strictEqual(typeof(flaska._on404), 'function') - }) - t.test('default valid handling of context', function() { let ctx = createCtx() let flaska = new Flaska({}, faker) @@ -123,6 +334,28 @@ t.describe('_on404', function() { message: 'Not Found', }) }) + + t.test('should do nothing if body is not null', function() { + const assertBody = { a: 1 } + let ctx = createCtx() + let flaska = new Flaska({}, faker) + + ctx.body = assertBody + flaska._on404(ctx) + assert.strictEqual(ctx.status, 200) + assert.strictEqual(ctx.body, assertBody) + }) + + t.test('should do nothing if body is null but status is 204', function() { + let ctx = createCtx() + let flaska = new Flaska({}, faker) + + ctx.status = 204 + ctx.body = null + flaska._on404(ctx) + assert.strictEqual(ctx.status, 204) + assert.strictEqual(ctx.body, null) + }) }) t.describe('_onerror', function() { @@ -317,8 +550,8 @@ t.describe('#before()', function() { let flaska = new Flaska({}, faker) assert.ok(flaska._before) flaska.before(assertFunction) - assert.strictEqual(flaska._before.length, 1) - assert.strictEqual(flaska._before[0], assertFunction) + assert.strictEqual(flaska._before.length, 2) + assert.strictEqual(flaska._before[1], assertFunction) }) }) diff --git a/test/flaska.in.test.mjs b/test/flaska.in.test.mjs index 64a849f..e5916b5 100644 --- a/test/flaska.in.test.mjs +++ b/test/flaska.in.test.mjs @@ -153,6 +153,14 @@ t.describe('#requestStart()', function() { assert.ok(ctx.query.get) assert.ok(ctx.query.set) assert.ok(ctx.query.delete) + assert.deepStrictEqual(ctx.headers, { + 'Server': 'Flaska', + 'X-Content-Type-Options': 'nosniff', + 'Content-Security-Policy': `default-src 'self'; style-src 'self' 'unsafe-inline'; img-src * data: blob:; object-src 'none'; frame-ancestors 'none'`, + 'Cross-Origin-Opener-Policy': 'same-origin', + 'Cross-Origin-Resource-Policy': 'same-origin', + 'Cross-Origin-Embedder-Policy': 'require-corp', + }) cb() } catch (err) { cb(err) } } @@ -163,6 +171,41 @@ t.describe('#requestStart()', function() { }), createRes()) }) + t.test('correctly adds default headers', function(cb) { + const assertError = new Error('test') + const defaultHeaders = { + 'Test': 'Asdf', + 'Herp': 'Derp', + } + let flaska = new Flaska({ + defaultHeaders: defaultHeaders, + }, faker) + flaska.compile() + + flaska.routers.test = { + match: function(path) { + throw assertError + } + } + + flaska.requestEnd = function(err, ctx) { + if (err && err !== assertError) return cb(err) + + try { + assert.ok(err) + assert.strictEqual(err, assertError) + assert.notStrictEqual(ctx.headers, defaultHeaders) + assert.deepStrictEqual(ctx.headers, defaultHeaders) + cb() + } catch (err) { cb(err) } + } + + flaska.requestStart(createReq({ + url: '/', + method: 'test', + }), createRes()) + }) + t.test('calls correct router with correct url and context if beforeAsync', function(cb) { const assertError = new Error('test') const assertMethod = 'test' diff --git a/test/helper.mjs b/test/helper.mjs index 34135e0..5695dcf 100644 --- a/test/helper.mjs +++ b/test/helper.mjs @@ -24,6 +24,7 @@ export function fakeHttp(inj1, inj2) { export function createReq(def) { return defaults(def, { + headers: {}, on: spy(), }) } @@ -53,6 +54,7 @@ export function createCtx(def, endHandler) { body: null, type: null, length: null, + headers: {}, log: { error: spy(), info: spy(), diff --git a/test/middlewares.test.mjs b/test/middlewares.test.mjs index 342bac6..6bbc022 100644 --- a/test/middlewares.test.mjs +++ b/test/middlewares.test.mjs @@ -2,7 +2,7 @@ import os from 'os' import path from 'path' import { Buffer } from 'buffer' import { Eltro as t, assert, stub} from 'eltro' -import { QueryHandler, JsonHandler, FormidableHandler, HttpError } from '../flaska.mjs' +import { QueryHandler, JsonHandler, FormidableHandler, HttpError, CorsHandler } from '../flaska.mjs' import { createCtx } from './helper.mjs' import { finished } from 'stream' import { setTimeout } from 'timers/promises' @@ -29,6 +29,98 @@ t.describe('#QueryHandler()', function() { }) }) + +t.describe('#CorsHandler()', function() { + let corsHandler + let ctx + + t.beforeEach(function() { + ctx = createCtx() + }) + + t.test('should return a handler', function() { + corsHandler = CorsHandler() + assert.strictEqual(typeof(corsHandler), 'function') + }) + + t.test('should set status and headers', function() { + const assertOrigin = 'http://my.site.here' + const assertRequestHeaders = 'asdf,foobar' + + corsHandler = CorsHandler({ + allowedOrigins: [assertOrigin], + }) + ctx.method = 'OPTIONS' + ctx.req.headers['origin'] = assertOrigin + ctx.req.headers['access-control-request-method'] = 'GET' + ctx.req.headers['access-control-request-headers'] = assertRequestHeaders + + assert.notOk(ctx.headers['Access-Control-Allow-Origin']) + assert.notOk(ctx.headers['Access-Control-Allow-Methods']) + assert.notOk(ctx.headers['Access-Control-Allow-Headers']) + + corsHandler(ctx) + + assert.strictEqual(ctx.headers['Access-Control-Allow-Origin'], assertOrigin) + assert.strictEqual(ctx.headers['Access-Control-Allow-Methods'], 'GET,HEAD,PUT,POST,DELETE,PATCH') + assert.strictEqual(ctx.headers['Access-Control-Allow-Headers'], assertRequestHeaders) + assert.strictEqual(ctx.status, 204) + }) + + t.test('should support custom allowed methods and headers', function() { + const assertOrigin = 'http://my.site.here' + const assertAllowedMethods = 'GET,HEAD' + const assertAllowedHeaders = 'test1,test2' + + corsHandler = CorsHandler({ + allowedOrigins: [assertOrigin], + allowedMethods: assertAllowedMethods, + allowedHeaders: assertAllowedHeaders, + }) + ctx.method = 'OPTIONS' + ctx.req.headers['origin'] = assertOrigin + ctx.req.headers['access-control-request-method'] = 'GET' + ctx.req.headers['access-control-request-headers'] = 'asdfasdfasdfsfad' + + assert.notOk(ctx.headers['Access-Control-Allow-Origin']) + assert.notOk(ctx.headers['Access-Control-Allow-Methods']) + assert.notOk(ctx.headers['Access-Control-Allow-Headers']) + + corsHandler(ctx) + + assert.strictEqual(ctx.headers['Access-Control-Allow-Origin'], assertOrigin) + assert.strictEqual(ctx.headers['Access-Control-Allow-Methods'], assertAllowedMethods) + assert.strictEqual(ctx.headers['Access-Control-Allow-Headers'], assertAllowedHeaders) + assert.strictEqual(ctx.status, 204) + }) + + t.test('should not set any allowed headers if allowedHeaders is explicitly false', function() { + const assertOrigin = 'http://my.site.here' + const assertAllowedMethods = 'GET,HEAD' + + corsHandler = CorsHandler({ + allowedOrigins: [assertOrigin], + allowedMethods: assertAllowedMethods, + allowedHeaders: false, + }) + ctx.method = 'OPTIONS' + ctx.req.headers['origin'] = assertOrigin + ctx.req.headers['access-control-request-method'] = 'GET' + ctx.req.headers['access-control-request-headers'] = 'asdfasdfasdfsfad' + + assert.notOk(ctx.headers['Access-Control-Allow-Origin']) + assert.notOk(ctx.headers['Access-Control-Allow-Methods']) + assert.notOk(ctx.headers['Access-Control-Allow-Headers']) + + corsHandler(ctx) + + assert.strictEqual(ctx.headers['Access-Control-Allow-Origin'], assertOrigin) + assert.strictEqual(ctx.headers['Access-Control-Allow-Methods'], assertAllowedMethods) + assert.strictEqual(ctx.headers['Access-Control-Allow-Headers'], undefined) + assert.strictEqual(ctx.status, 204) + }) +}) + t.describe('#JsonHandler()', function() { let jsonHandler = JsonHandler() let ctx