From f48d63038d3b492703677ac8fbd4d7f09bad569e Mon Sep 17 00:00:00 2001 From: Jonatan Nilsson Date: Sat, 2 Nov 2024 23:40:54 +0000 Subject: [PATCH] buffer and charCodeAt impl --- benchmark/set_arr.mjs | 117 ++++ flaska.mjs | 528 +++++++++------- flaska_buffer.mjs | 1229 ++++++++++++++++++++++++++++++++++++++ flaska_old.mjs | 1128 ++++++++++++++++++++++++++++++++++ test.mjs | 12 +- test/flaska.api.test.mjs | 76 +-- test/flaska.in.test.mjs | 48 +- test/http.test.mjs | 50 +- test/router.test.mjs | 208 ++----- test_old.mjs | 20 + 10 files changed, 2966 insertions(+), 450 deletions(-) create mode 100644 benchmark/set_arr.mjs create mode 100644 flaska_buffer.mjs create mode 100644 flaska_old.mjs create mode 100644 test_old.mjs diff --git a/benchmark/set_arr.mjs b/benchmark/set_arr.mjs new file mode 100644 index 0000000..dbd109b --- /dev/null +++ b/benchmark/set_arr.mjs @@ -0,0 +1,117 @@ +import { summary, run, bench } from 'mitata'; + +// Warmup (de-optimize `bench()` calls) +bench('noop', () => { }); +bench('noop2', () => { }); + +function padStart(length) { + return ''.padStart(length * 2) +} + +const data = [ + '/', + '/api/articles', + '/api/articles/:id/file', + '/api/articles/:id', + '/api/articles/public', + '/api/articles/public/:id', + '/api/categories', + '/api/categories/:categoryId/products', + '/api/categories/:categoryId/properties', + '/api/categories/:categoryId/values/:props', + '/api/categories/:categoryId', + //'/api/categories/:categoryId/products/:productId', + '/api/categories/:categoryId/products/:productId', + '/api/customers', + '/api/customers/:id', + '/api/customers/kennitala/:kennitala', + '/api/customers/public/kennitala/:kennitala', + '/api/customers/search/:search', + '/api/file', + '/api/file/:id', + '/api/media', + '/api/media/:id', + '/api/orderitem', + '/api/orderitem/:id', + '/api/orders', + '/api/orders/:orderId', + '/api/orders/:orderId/sell', + '/api/pages', + '/api/pages/:pageId', + '/api/pages/:pageId/articles', + '/api/pages/:pageId/articles/public', + '/api/products', + '/api/products/:id', + '/api/products/:id/movement', + '/api/products/:id/sub_products/:productId', + //'/api/products/:id/sub_products/:productId', + '/api/products/code/:code', + '/api/products/property/:propertyId', + '/api/properties', + '/api/properties/:id', + '/api/sales', + '/api/sales/:id', + '/api/stockitem', + '/api/stockitem/:id', + '/api/stocks', + '/api/stocks/:id', + '/api/stocks/:id/commit', + '/api/test', + '/api/test/auth', + '/api/test/error', + '/api/workentries', + '/api/workentries/:id', + '/api/works', + '/api/works/:id', + '/api/works/:id/lock', + '/api/works/public', + '/api/staff', + '/api/staff/:id', + '/::rest', +] + +function arrIncludes(data) { + let out = new Array() + for (let item of data) { + if (out.includes(item)) { break } + out.push(item) + } + return out +} + +function setAdd(data) { + let s = new Set() + for (let item of data) { + let size = s.size + if (s.add(item).size === size) { break } + } + return s +} + +let func = [arrIncludes, setAdd]; +for (let fun of func) { + console.log(`--- warming up ${fun.name || 'mapl'} ---`) + for (var i = 0; i < 100; i++) { + fun(data) + } +} + +await new Promise(res => setTimeout(res, 3000)) + +summary(() => { + for (let i = 20; i >= 0; i--) { + const dataSet = data.slice(0, data.length - i) + + func.forEach(function(fun) { + bench(`${dataSet.length} items: ${fun.name}`, function() { + return fun(dataSet) + }) + }) + } + // console.log(tests, fun, tests.map(fun)) + /*bench(fun.name, function() { + return fun(data) + })*/ +}) + +run(); diff --git a/flaska.mjs b/flaska.mjs index da65cdf..07ab3f7 100644 --- a/flaska.mjs +++ b/flaska.mjs @@ -37,16 +37,6 @@ export const MimeTypeDb = getDb() * Router */ -class Branch { - constructor() { - this.children = new Map() - this.paramName = null - this.fullparamName = null - this.handler = null - this.middlewares = [] - } -} - export const ErrorCodes = { ERR_CONNECTION_ABORTED: 'ERR_CON_ABORTED' } @@ -114,9 +104,9 @@ export function JsonHandler(org = {}) { ctx.req.body = {} return } - + const data = Buffer.concat(buffers).toString(); - + try { ctx.req.body = JSON.parse(data) } catch (err) { @@ -145,7 +135,7 @@ export function CorsHandler(opts = {}) { // Always add vary header on origin. Prevent caches from // accidentally caching wrong preflight request ctx.headers['Vary'] = 'Origin' - + // Set status to 204 if OPTIONS. Just handy for flaska and // other checking. if (ctx.method === 'OPTIONS') { @@ -228,7 +218,7 @@ export function FormidableHandler(formidable, org = {}) { // For testing/stubbing purposes let rename = formidable.fsRename || fs.rename - + return function(ctx) { let form = formidable.IncomingForm() form.uploadDir = opts.uploadDir @@ -263,7 +253,7 @@ export function FormidableHandler(formidable, org = {}) { keys.map(key => { let filename let target - + try { filename = opts.filename(ctx.req.files[key]) || ctx.req.files[key].name target = path.join(opts.uploadDir, filename) @@ -386,7 +376,7 @@ export class FileResponse { } } } - + let ext = path.extname(this.filepath).slice(1) let found = MimeTypeDb[ext] if (found) { @@ -403,190 +393,312 @@ export class FileResponse { } } +/* + * --- Router --- +*/ + +class RouterError extends Error { + constructor(route1, route2, ...params) { + // Pass remaining arguments (including vendor specific ones) to parent constructor + super(...params); + + // Maintains proper stack trace for where our error was thrown (only available on V8) + if (Error.captureStackTrace) { + Error.captureStackTrace(this, RouterError); + } + + this.name = "RouterError"; + this.routeA = route1 + this.routeB = route2 + } +} + +function Child(split, x, i) { + this.path = null + this.isParams = split[x].isParams ? split[x].word : null + this.isFullParams = split[x].isFullParams ? split[x].word : null + this.paramVarName = split[x].paramVarName ?? null + this.char = !this.isParams && !this.isFullParams ? split[x].word[i] || '/' : null + this.count = 0 + this.children = [] +} + +const regParamPrefix = /^::?/ +const regCleanNonAschii = /(?![a-zA-Z_])./g +const regCleanRest = /_+/g +const regStarDoubleParam = /::[^:/]+/g +const regStarSingleParam = /:[^:/]+/g +const SlashCode = '/'.charCodeAt(0) +const spaces = ' ' + export class FlaskaRouter { constructor() { - this.root = new Branch() + this.paths = [] + this.registeredPaths = new Set() } - addRoute(route, orgMiddlewares, orgHandler) { - if (route[0] !== '/') - throw new Error(`route "${route}" must start with forward slash`) - - let middlewares = orgMiddlewares - let handler = orgHandler - if (!orgHandler) { - handler = orgMiddlewares - middlewares = [] - } - if (middlewares && typeof(middlewares) === 'function') { - middlewares = [middlewares] - } - assertIsHandler(handler, 'addRoute()') - - let start = 1 - let end = 1 - let name = '' - let param = '' - let isParam = false - let isFullParam = false - let branch = this.root - - if (route.indexOf(':') < 0 && false) { - let name = route - if (name.length > 1 && name[name.length - 1] === '/') { - name = name.slice(0, -1) + addRoute(path, middlewares, orgHandler) { + if (path[0] !== '/') + throw new RouterError(null, null, `addRoute("${path}") path must start with forward slash`) + + let cleaned = path + if (cleaned.indexOf('/:') >= 0) { + cleaned = cleaned.replace(regStarDoubleParam, '**').replace(regStarSingleParam, '*') + if (cleaned.indexOf(':') > 0) { + throw new RouterError(null, null, `addRoute("${path}") path has missing name or word between two forward slashes`) } - let child = new Branch() - branch.children.set(name, child) - child.handler = handler - child.middlewares = middlewares - } - - for (let i = 1; i <= route.length; i++) { - if ((i === route.length || route[i] === '/') && end > start) { - if (branch.fullparamName) { - throw new Error(`route "${route}" conflicts with a sub-branch that has a full param child`) - } - let child - name = route.substring(start, end) - if (isFullParam) { - param = name - name = __fullParamMapName - } else if (isParam) { - param = name - name = __paramMapName - } - if (branch.children.has(name)) { - child = branch.children.get(name) - } - else if (isParam && !isFullParam && branch.children.has(__fullParamMapName)) { - throw new Error(`route "${route}" conflicts with a sub-branch that has a full param child`) - } - else if (isFullParam && branch.children.has(__paramMapName)) { - throw new Error(`route "${route}" conflicts with a sub-branch that has a partial param child`) - } - else { - child = new Branch() - branch.children.set(name, child) - } - branch = child - end = i - start = i - if (isParam) { - if (branch.paramName && branch.paramName !== param) { - throw new Error(`route "${route}" conflicts with pre-existing param name of ${branch.paramName} instead of ${param}`) - } - if (isFullParam) { - branch.fullparamName = param - } else { - branch.paramName = param - } - isParam = false - } - } else if (route[i] === '/' && end === start) { - throw new Error(`route "${route}" has missing path name inbetween slashes`) - } - if (i === route.length) { - branch.handler = handler - branch.middlewares = middlewares - continue - } - if (route[i] === ':') { - if (isParam) { - isFullParam = true - } - isParam = true - end = start = i + 1 - } - else if (route[i] === '/') { - end = start = i + 1 - } - else { - end++ + if (cleaned.indexOf('**/') > 0) { + throw new RouterError(null, null, `addRoute("${path}") cannot add anything after a full param route`) } } + if (cleaned.indexOf('//') >= 0) { + throw new RouterError(null, null, `addRoute("${path}") path has missing name or word between two forward slashes`) + } + + let size = this.registeredPaths.size + if (this.registeredPaths.add(cleaned).size === size) { + throw new RouterError(null, null, `addRoute("${path}") found an existing route with same path of ${cleaned}`) + } + + let handlers = [] + if (Array.isArray(middlewares)) { + handlers.push(...middlewares) + if (typeof(orgHandler) !== 'function') { + throw new RouterError(orgHandler, null, `addRoute("${path}") was called with a handler that was not a function`) + } + } else { + handlers.push(middlewares) + } + if (orgHandler) { + handlers.push(orgHandler) + } + for (let handler of handlers) { + if (typeof(handler) !== 'function') { + throw new RouterError(handler, null, `addRoute("${path}") was called with a handler that was not a function`) + } + } + + this.paths.push({ + path, + handlers + }) } - match(orgUrl) { - let url = orgUrl - if (url.length > 1 && url[url.length - 1] === '/') { - url = url.slice(0, -1) + __buildChild(x, i, splitPaths) { + let splitPath = splitPaths[0] + let letter = new Child(splitPath.split, x, i) + + let consume = [] + if (splitPath.split.length === x + 1 + && (splitPath.split[x].isParams + || splitPath.split[x].isFullParams + || splitPath.split[x].word.length === i + 1)) { + letter.path = splitPath.entry + letter.count += 1 + } else { + consume = [splitPath] } - let branch = this.root - let start = 1 - let end = 1 - let output - let name - let char - let params = {} - if (output = branch.children.get(url)) { - return { - handler: output.handler, - middlewares: output.middlewares, - params: params, + + for (let y = 1; y < splitPaths.length; y++) { + let checkPath = splitPaths[y] + if (!checkPath.split[x] + || checkPath.split[x].isParams !== splitPath.split[x].isParams + || checkPath.split[x].isFullParams !== splitPath.split[x].isFullParams + || !checkPath.split[x].isParams + && !checkPath.split[x].isFullParams + && (checkPath.split[x].word[i] || '/') !== letter.char) break + consume.push(checkPath) + } + + letter.count += consume.length + if (splitPath.split[x].word.length === i || splitPath.split[x].isParams || splitPath.split[x].isFullParams) { + x++ + i = -1 + } + while (consume.length) { + letter.children.push(this.__buildChild(x, i + 1, consume)) + consume.splice(0, letter.children[letter.children.length - 1].count) + } + return letter + } + + __buildTree(splitPaths) { + let builder = [] + while (splitPaths.length) { + builder.push(this.__buildChild(0, 0, splitPaths)) + splitPaths.splice(0, builder[builder.length - 1].count) + } + return builder + } + + __splitAndSortPaths(paths, separateStatic = true) { + let staticPaths = new Map() + let paramsPaths = [] + let collator = new Intl.Collator('en', { sensitivity: 'accent' }); + + paths.forEach(function(entry) { + if (entry.path[0] !== '/') throw new RouterError(entry, null, 'Specified route was missing forward slash at start') + + // Collect static paths separately + if (entry.path.indexOf('/:') < 0 && separateStatic) { + return staticPaths.set(entry.path, { + path: entry, + params: {} + }) } - } - for (let i = 1; i <= url.length; i++) { - char = url[i] - if ((i === url.length || char === '/') && end > start) { - name = url.slice(start, end) - if (output = branch.children.get(name)) { - branch = output - } - else if (output = branch.children.get(__paramMapName)) { - branch = output - params[branch.paramName] = name - } - else if (output = branch.children.get(__fullParamMapName)) { - params[output.fullparamName] = url.slice(start) + + // Collect params path separately + paramsPaths.push({ + split: entry.path.slice(1).split(/\//g).map(function(word) { + let actualWord = word.replace(regParamPrefix, '') return { - handler: output.handler, - middlewares: output.middlewares, - params: params, + word: actualWord, + isParams: word[0] === ':' && word[1] !== ':', + isFullParams: word[0] === ':' && word[1] === ':', + paramVarName: word[0] === ':' + ? actualWord.replace(regCleanNonAschii, '_').replace(regCleanRest, '_') + : null } + }), + entry, + }) + }) + paramsPaths.sort(function(aGroup, bGroup) { + let length = Math.max(aGroup.split.length, bGroup.split.length) + for (let x = 0; x < length; x++) { + let a = aGroup.split[x] + let b = bGroup.split[x] + if (!a) return -1 + if (!b) return 1 + // Full params go last + if (a.isFullParams && b.isFullParams) throw new RouterError(aGroup.entry, bGroup.entry, 'Two full path routes found on same level') + if (a.isFullParams) return 1 + if (b.isFullParams) return -1 + // Params go second last + if (a.isParams && !b.isParams) return 1 + if (!a.isParams && b.isParams) return -1 + // otherwise sort alphabetically if not identical + if (a.word !== b.word) return collator.compare(a.word, b.word) + } + throw new RouterError(aGroup, bGroup, 'Two identical paths were found') + }) + + return { + staticPaths, + paramsPaths, + } + } + + + __getIndex(offset, additions, params) { + return (offset + additions) + + (params.length + ? ' + ' + params.map(a => `offset${a[1]}`).join(' + ') + : '') + } + + __treeIntoCompiledCodeReturnPath(indentString, paths, branch, params) { + let pathIndex = paths.indexOf(branch.path) + if (pathIndex < 0) { + throw new RouterError(branch.path, null, 'InternalError: Specified path was not found in paths') + } + let output = '\n' + indentString + `return {` + output += '\n' + indentString + ` path: paths[${pathIndex}],` + if (params.length) { + output += '\n' + indentString + ` params: {` + for (let param of params) { + output += '\n' + indentString + ` ${param[0]}: s${param[1]},` + } + output += '\n' + indentString + ` },` + } else { + output += '\n' + indentString + ` params: {},` + } + output += '\n' + indentString + `}` + return output + } + + __treeIntoCompiledCodeBranch(paths, branches, indent = 0, params = []) { + let output = '' + let indentation = spaces.slice(0, (indent - params.length) * 2) + let addEndBracket = true + + for (let i = 0; i < branches.length; i++) { + let branch = branches[i] + if (i > 0) { + if (!branch.isParams && !branch.isFullParams) { + output += ' else ' } else { - if (output = this.root.children.get(__fullParamMapName)) { - params = { - [output.fullparamName]: url.slice(1) - } - return { - handler: output.handler, - middlewares: output.middlewares, - params: params, - } - } - return null - } - i++ - end = start = i - char = url[i] - } - // Check branch.handler. This can happen if route /::path is added - // and request is '/' it will attempt to match root which will fail - if (i >= url.length && branch.handler) { - return { - handler: branch.handler, - middlewares: branch.middlewares, - params: params, + // output += '} //' + output += '\n' + indentation } } - if (char === '/') { - end = start = i + 1 + + if (!branch.isParams && !branch.isFullParams) { + output += `if (str.charCodeAt(${this.__getIndex(indent, 0, params)}) === ${branch.char.charCodeAt(0)}) { // ${branch.char}` + + if (branch.path) { + output += '\n' + indentation + ` if (str.length === ${this.__getIndex(indent, 1, params)}) {` + output += this.__treeIntoCompiledCodeReturnPath(indentation + ' ', paths, branch, params) + output += '\n' + indentation + ` }` + } } else { - end++ + addEndBracket = false + let paramVarName = (params.length + 1) + '_' + branch.paramVarName + output += `let s${paramVarName} = str.slice(${this.__getIndex(indent, 0, params)}${branch.isFullParams ? '' : `, str.indexOf('/', ${this.__getIndex(indent, 0, params)}) >>> 0`})` + output += '\n' + indentation + `let offset${paramVarName} = s${paramVarName}.length` + output += '\n' + indentation + params.push([branch.isParams || branch.isFullParams, paramVarName]) + + if (branch.isFullParams) { + output += this.__treeIntoCompiledCodeReturnPath(indentation, paths, branch, params) + } else if (branch.path) { + output += '\n' + indentation + `if (str.length === ${this.__getIndex(indent, 0, params)}) {` + output += this.__treeIntoCompiledCodeReturnPath(indentation + ' ', paths, branch, params) + output += '\n' + indentation + `}` + } + } + + if (branch.children.length) { + if (branch.path) { + output += ' else ' + } else { + output += '\n' + indentation + ' ' + } + output += this.__treeIntoCompiledCodeBranch(paths, branch.children, indent + 1, params.slice()) + } + if (addEndBracket) { + output += '\n' + indentation + '} ' } } - if (output = this.root.children.get(__fullParamMapName)) { - params = { - [output.fullparamName]: url.slice(1) - } - return { - handler: output.handler, - middlewares: output.middlewares, - params: params, - } + return output + } + + __treeIntoCompiledCodeClosure(paths, tree, staticList) { + let output = 'return function RBufferStrSliceClosure(str) {' + if (staticList.size > 0) { + output += '\n let checkStatic = staticList.get(str)' + output += '\n if(checkStatic) {' + output += '\n return checkStatic' + output += '\n }' } - return null + if (tree.length) { + output += '\n ' + this.__treeIntoCompiledCodeBranch(paths, tree, 1, []) + } + output += '\n return null' + output += '\n}' + //console.log(output) + return new Function('paths', 'staticList', output)(paths, staticList) + } + + compile() { + let splitPaths = this.__splitAndSortPaths(this.paths) + let tree = this.__buildTree(splitPaths.paramsPaths.slice()) + this.match = this.__treeIntoCompiledCodeClosure(this.paths, tree, splitPaths.staticPaths) + } + + match(url) { + this.compile() + return this.match(url) } } @@ -682,7 +794,7 @@ export class Flaska { 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--; @@ -692,7 +804,7 @@ ctx.state.nonce = nonce; constructFunction += 'ctx.headers = {' constructFunction += `'Date': new Date().toUTCString(),` - for (let key of headerKeys) { + 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++) { @@ -754,12 +866,6 @@ ctx.state.nonce = nonce; this.patch = this.routers.PATCH.addRoute.bind(this.routers.PATCH) } - _assertIsHandler(handler, name) { - if (typeof(handler) !== 'function') { - throw new Error(`${name} was called with a handler that was not a function`) - } - } - devMode() { this._backuperror = this._onerror = function(err, ctx) { ctx.log.error(err) @@ -795,7 +901,7 @@ ctx.state.nonce = nonce; assertIsHandler(handler, 'onreqerror()') this._onreqerror = handler } - + onreserror(handler) { assertIsHandler(handler, 'onreserror()') this._onreserror = handler @@ -810,7 +916,7 @@ ctx.state.nonce = nonce; assertIsHandler(handler, 'beforeAsync()') this._beforeAsync.push(handler) } - + after(handler) { assertIsHandler(handler, 'after()') this._after.push(handler) @@ -820,7 +926,7 @@ ctx.state.nonce = nonce; assertIsHandler(handler, 'afterAsync()') this._afterAsync.push(handler) } - + requestStart(req, res) { let url = req.url let search = '' @@ -894,37 +1000,25 @@ ctx.state.nonce = nonce; ctx.params = route.params - if (route.middlewares.length) { - let middle = this.handleMiddleware(ctx, route.middlewares, 0) + let handlers = this.runHandlers(ctx, route.path.handlers, 0) - if (middle && middle.then) { - return middle.then(() => { - return route.handler(ctx) - }) - .then(() => { - this.requestEnd(null, ctx) - }, err => { - this.requestEnd(err, ctx) - }) - } - } - let handler = route.handler(ctx) - if (handler && handler.then) { - return handler.then(() => { + if (handlers && handlers.then) { + return handlers.then(() => { this.requestEnd(null, ctx) }, err => { this.requestEnd(err, ctx) }) } + this.requestEnd(null, ctx) } - handleMiddleware(ctx, middles, index) { + runHandlers(ctx, middles, index) { for (let i = index; i < middles.length; i++) { let res = middles[i](ctx) if (res && res.then) { return res.then(() => { - return this.handleMiddleware(ctx, middles, i + 1) + return this.runHandlers(ctx, middles, i + 1) }) } } @@ -952,7 +1046,7 @@ ctx.state.nonce = nonce; return } - if (ctx.body === null && !handleUsed && ctx.status === 200) { + if (ctx.body == null && !handleUsed && ctx.status === 200) { ctx.status = 204 } @@ -989,7 +1083,7 @@ ctx.state.nonce = nonce; } let length = 0 - + if (body instanceof Buffer) { length = body.byteLength ctx.type = ctx.type || 'application/octet-stream' @@ -1060,6 +1154,12 @@ ctx.state.nonce = nonce; this[`_${type}AsyncCompiled`] = func.bind(this, ...this[`_${type}Async`]) } } + this.routers.GET.compile() + this.routers.POST.compile() + this.routers.PUT.compile() + this.routers.DELETE.compile() + this.routers.OPTIONS.compile() + this.routers.PATCH.compile() } create() { @@ -1069,7 +1169,7 @@ ctx.state.nonce = nonce; this.server.on('connection', function (socket) { // Set socket idle timeout in milliseconds socket.setTimeout(1000 * 60 * 5) // 5 minutes - + // Wait for timeout event (socket will emit it when idle timeout elapses) socket.on('timeout', function () { // Call destroy again @@ -1090,7 +1190,7 @@ ctx.state.nonce = nonce; } this.create() - + this.server.listen(port, ip, cb) } @@ -1121,7 +1221,7 @@ ctx.state.nonce = nonce; if (err) { return rej(err) } // Waiting 0.1 second for it to close down - setTimeout(function() { res() }, 100) + setTimeout(res, 100) }) }) } diff --git a/flaska_buffer.mjs b/flaska_buffer.mjs new file mode 100644 index 0000000..d422fbd --- /dev/null +++ b/flaska_buffer.mjs @@ -0,0 +1,1229 @@ +import os from 'os' +import crypto from 'crypto' +import path from 'path' +import http from 'http' +import stream from 'stream' +import fs from 'fs/promises' +import fsSync from 'fs' +import { URL } from 'url' +import { Buffer } from 'buffer' +import zlib from 'zlib' + +function getDb() { + // Take from @thi-ng/mime which is a reduced mime-db + let MimeTypeDbRaw = Buffer.from('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', 'base64') + let inbetween = JSON.parse(zlib.brotliDecompressSync(MimeTypeDbRaw).toString()) + + let res = {} + for (let groupID in inbetween) { + const group = inbetween[groupID]; + for (let type in group) { + const mime = groupID + "/" + type; + for (let e of group[type].split(",")) { + const isLowPri = e[0] === "*"; + const ext = isLowPri ? e.substr(1) : e; + let coll = res[ext]; + !coll && (coll = res[ext] = []); + isLowPri ? coll.push(mime) : coll.unshift(mime); + } + } + } + return res +} + +export const MimeTypeDb = getDb() + +/** + * Router + */ + +export const ErrorCodes = { + ERR_CONNECTION_ABORTED: 'ERR_CON_ABORTED' +} + +// Taken from https://github.com/nfp-projects/koa-lite/blob/master/lib/statuses.js +const statuses = { + 100: 'Continue', 101: 'Switching Protocols', 102: 'Processing', 103: 'Early Hints', + 200: 'OK', 201: 'Created', 202: 'Accepted', 203: 'Non-Authoritative Information', 204: 'No Content', 205: 'Reset Content', 206: 'Partial Content', 207: 'Multi-Status', 208: 'Already Reported', 226: 'IM Used', + 300: 'Multiple Choices', 301: 'Moved Permanently', 302: 'Found', 303: 'See Other', 304: 'Not Modified', 305: 'Use Proxy', 306: '(Unused)', 307: 'Temporary Redirect', 308: 'Permanent Redirect', + 400: 'Bad Request', 401: 'Unauthorized', 402: 'Payment Required', 403: 'Forbidden', 404: 'Not Found', 405: 'Method Not Allowed', 406: 'Not Acceptable', 407: 'Proxy Authentication Required', 408: 'Request Timeout', 409: 'Conflict', 410: 'Gone', 411: 'Length Required', 412: 'Precondition Failed', 413: 'Payload Too Large', 414: 'URI Too Long', 415: 'Unsupported Media Type', 416: 'Range Not Satisfiable', 417: 'Expectation Failed', 418: 'I\'m a teapot', 421: 'Misdirected Request', 422: 'Unprocessable Entity', 423: 'Locked', 424: 'Failed Dependency', 425: 'Too Early', 426: 'Upgrade Required', 428: 'Precondition Required', 429: 'Too Many Requests', 431: 'Request Header Fields Too Large', 451: 'Unavailable For Legal Reasons', + 500: 'Internal Server Error', 501: 'Not Implemented', 502: 'Bad Gateway', 503: 'Service Unavailable', 504: 'Gateway Timeout', 505: 'HTTP Version Not Supported', 506: 'Variant Also Negotiates', 507: 'Insufficient Storage', 508: 'Loop Detected', 509: 'Bandwidth Limit Exceeded', 510: 'Not Extended', 511: 'Network Authentication Required', + redirect: { + 300: true, + 301: true, + 302: true, + 303: true, + 305: true, + 307: true, + 308: true + }, + empty: { + 204: true, + 205: true, + 304: true + } +} +const __paramMapName = '__param' +const __fullParamMapName = '__fullparam' + +function assertIsHandler(handler, name) { + if (typeof(handler) !== 'function') { + throw new Error(`${name} was called with a handler that was not a function`) + } +} + +export function QueryHandler() { + return function(ctx) { + ctx.query = (new URL(ctx.req.url, 'http://localhost')).searchParams + } +} + +export function JsonHandler(org = {}) { + let opts = { + sizeLimit: org.sizeLimit || (10 * 1024) + } + return function(ctx) { + const buffers = []; + let size = 0 + + return new Promise(function(res, rej) { + ctx.req.on('data', chunk => { + size += chunk.length + if (size > opts.sizeLimit) { + return rej(new HttpError(413, `Body limit of ${opts.sizeLimit} bytes reached with ${size} bytes`)) + } + + buffers.push(chunk) + }) + ctx.req.on('end', () => { + res() + }) + }) + .then(function() { + if (!buffers.length) { + ctx.req.body = {} + return + } + + const data = Buffer.concat(buffers).toString(); + + try { + ctx.req.body = JSON.parse(data) + } catch (err) { + return Promise.reject(new HttpError(400, `Invalid JSON: ${err.message}`, { + status: 400, + message: `Invalid JSON: ${err.message}`, + request: data, + })) + } + }) + } +} + +export function CorsHandler(opts = {}) { + const options = { + allowedMethods: opts.allowedMethods || 'GET,HEAD,PUT,POST,DELETE,PATCH', + allowedOrigins: opts.allowedOrigins || [], + allowedHeaders: opts.allowedHeaders, + credentials: opts.credentials || false, + exposeHeaders: opts.exposeHeaders || '', + maxAge: opts.maxAge || '', + } + const allowAll = options.allowedOrigins.includes('*') + + return function(ctx) { + // Always add vary header on origin. Prevent caches from + // accidentally caching wrong preflight request + ctx.headers['Vary'] = 'Origin' + + // Set status to 204 if OPTIONS. Just handy for flaska and + // other checking. + if (ctx.method === 'OPTIONS') { + ctx.status = 204 + } + + // Check origin is specified. Nothing needs to be done if + // there is no origin or it doesn't match + let origin = ctx.req.headers['origin'] + if (!origin || (!allowAll && !options.allowedOrigins.includes(origin))) { + return + } + + // Set some extra headers if this is a pre-flight. Most of + // these are not needed during a normal request. + if (ctx.method === 'OPTIONS') { + if (!ctx.req.headers['access-control-request-method']) { + return + } + + if (options.maxAge) { + ctx.headers['Access-Control-Max-Age'] = options.maxAge + } + + let reqHeaders = options.allowedHeaders + || ctx.req.headers['access-control-request-headers'] + if (reqHeaders && options.allowedHeaders !== false) { + ctx.headers['Access-Control-Allow-Headers'] = reqHeaders + } + ctx.headers['Access-Control-Allow-Methods'] = options.allowedMethods + } else { + if (options.exposeHeaders) { + ctx.headers['Access-Control-Expose-Headers'] = options.exposeHeaders + } + } + + ctx.headers['Access-Control-Allow-Origin'] = origin + + if (options.credentials) { + ctx.headers['Access-Control-Allow-Credentials'] = 'true' + } + } +} + +export function FormidableHandler(formidable, org = {}) { + let lastDateString = '' + let incrementor = 1 + + let opts = { + rename: true, + parseFields: org.parseFields || false, + uploadDir: org.uploadDir || os.tmpdir(), + filename: org.filename || function(file) { + let prefix = new Date() + .toISOString() + .replace(/-/g, '') + .replace('T', '_') + .replace(/:/g, '') + .replace(/\..+/, '_') + + // Prevent accidental overwriting if two file uploads with + // same name get uploaded at exact same second. + if (prefix === lastDateString) { + prefix += incrementor.toString().padStart('2', '0') + '_' + incrementor++ + } else { + lastDateString = prefix + incrementor + } + + return prefix + file.name + }, + maxFileSize: org.maxFileSize || 8 * 1024 * 1024, + maxFieldsSize: org.maxFieldsSize || 10 * 1024, + maxFields: org.maxFields || 50, + } + if (org.rename != null) { + opts.rename = org.rename + } + + // For testing/stubbing purposes + let rename = formidable.fsRename || fs.rename + + return function(ctx) { + let form = formidable.IncomingForm() + form.uploadDir = opts.uploadDir + form.maxFileSize = opts.maxFileSize + form.maxFieldsSize = opts.maxFieldsSize + form.maxFields = opts.maxFields + + return new Promise(function(res, rej) { + form.parse(ctx.req, function(err, fields, files) { + if (err) return rej(new HttpError(400, err.message)) + + if (opts.parseFields) { + Object.keys(fields).forEach(function(key) { + try { + fields[key] = JSON.parse(fields[key]) + } catch { } + }) + } + + ctx.req.body = fields + ctx.req.files = files + ctx.req.file = null + + + if (!ctx.req.files) { + return res() + } + + let keys = Object.keys(files).filter(key => Boolean(ctx.req.files[key])) + + Promise.all( + keys.map(key => { + let filename + let target + + try { + filename = opts.filename(ctx.req.files[key]) || ctx.req.files[key].name + target = path.join(opts.uploadDir, filename) + } catch (err) { + return Promise.reject(err) + } + + return rename(ctx.req.files[key].path, target) + .then(function() { + if (!ctx.req.files[key].type || ctx.req.files[key].type === 'application/octet-stream') { + let found = MimeTypeDb[path.extname(filename).slice(1)] + ctx.req.files[key].type = found && found[0] || 'application/octet-stream' + } + ctx.req.files[key].path = target + ctx.req.files[key].filename = filename + }) + }) + ) + .then(() => { + if (keys.length === 1 && keys[0] === 'file') { + ctx.req.file = ctx.req.files.file + } + res() + }, rej) + }) + }) + } +} + +export class HttpError extends Error { + constructor(statusCode, message, body = null) { + super(message); + + Error.captureStackTrace(this, HttpError); + + let proto = Object.getPrototypeOf(this); + proto.name = 'HttpError'; + + this.status = statusCode + this.body = body + } +} + +const RangeRegexTester = /bytes=(\d+)-(\d+)?/ + +export class FileResponse { + constructor(filepath, stat) { + this.filepath = filepath + this.stat = stat + } + + handleRequest(ctx, useFs = fsSync) { + let etag = '"' + this.stat.ino + '-' + this.stat.size + '-' + this.stat.mtime.getTime() + '"' + let lastModified = this.stat.mtime.toUTCString() + let lastModifiedRounded = Date.parse(lastModified) + + if (ctx.req.headers['if-match'] && ctx.req.headers['if-match'] !== etag) { + throw new HttpError(412, `Request if-match pre-condition failed`) + } + if (ctx.req.headers['if-unmodified-since']) { + let check = Date.parse(ctx.req.headers['if-unmodified-since']) + if (!check || check < lastModifiedRounded) { + throw new HttpError(412, `Request if-unmodified-since pre-condition failed`) + } + } + + ctx.headers['Etag'] = etag + + if (ctx.req.headers['if-none-match']) { + let split = ctx.req.headers['if-none-match'].split(',') + for (let check of split) { + if (check.trim() === etag) { + ctx.status = 304 + return null + } + } + } else if (ctx.req.headers['if-modified-since']) { + let check = Date.parse(ctx.req.headers['if-modified-since']) + if (check >= lastModifiedRounded) { + ctx.status = 304 + return null + } + } + + let readOptions = {} + let size = this.stat.size + + if (ctx.req.headers['range']) { + let match = RangeRegexTester.exec(ctx.req.headers['range']) + + let ifRange = ctx.req.headers['if-range'] + if (ifRange) { + if (ifRange[0] === '"' && ifRange !== etag) { + match = null + } else if (ifRange[0] !== '"') { + let check = Date.parse(ifRange) + if (!check || check < lastModifiedRounded) { + match = null + } + } + } + + if (match) { + let start = Number(match[1]) + let end = size - 1 + if (match[2]) { + end = Math.min(Number(match[2]), size - 1) + } + + if (start >= size) { + throw new HttpError(416, `Out of range start ${start} outside of ${size} bounds`) + } + + if (start <= end) { + size = end - start + 1 + readOptions.start = start + readOptions.end = end + ctx.headers['Content-Range'] = start + '-' + end + '/' + this.stat.size + ctx.status = 206 + } + } + } + + let ext = path.extname(this.filepath).slice(1) + let found = MimeTypeDb[ext] + if (found) { + ctx.type = found[found.length - 1] + } + + ctx.headers['Last-Modified'] = lastModified + ctx.headers['Content-Length'] = size + if (ctx.method !== 'HEAD') { + let stream = useFs.createReadStream(this.filepath, readOptions) + return stream + } + return null + } +} + +/* + * --- Router --- +*/ + +class RouterError extends Error { + constructor(route1, route2, ...params) { + // Pass remaining arguments (including vendor specific ones) to parent constructor + super(...params); + + // Maintains proper stack trace for where our error was thrown (only available on V8) + if (Error.captureStackTrace) { + Error.captureStackTrace(this, RouterError); + } + + this.name = "RouterError"; + this.routeA = route1 + this.routeB = route2 + } +} + +function Child(split, x, i) { + this.path = null + this.isParams = split[x].isParams ? split[x].word : null + this.isFullParams = split[x].isFullParams ? split[x].word : null + this.paramVarName = split[x].paramVarName ?? null + this.char = !this.isParams && !this.isFullParams ? split[x].word[i] || '/' : null + this.count = 0 + this.children = [] +} + +const regParamPrefix = /^::?/ +const regCleanNonAschii = /(?![a-zA-Z_])./g +const regCleanRest = /_+/g +const regStarDoubleParam = /::[^:/]+/g +const regStarSingleParam = /:[^:/]+/g +const SlashCode = '/'.charCodeAt(0) +const spaces = ' ' + +export class FlaskaRouter { + constructor() { + this.paths = [] + this.registeredPaths = new Set() + } + + addRoute(path, middlewares, orgHandler) { + if (path[0] !== '/') + throw new RouterError(null, null, `addRoute("${path}") path must start with forward slash`) + + let cleaned = path + if (cleaned.indexOf('/:') >= 0) { + cleaned = cleaned.replace(regStarDoubleParam, '**').replace(regStarSingleParam, '*') + if (cleaned.indexOf(':') > 0) { + throw new RouterError(null, null, `addRoute("${path}") path has missing name or word between two forward slashes`) + } + if (cleaned.indexOf('**/') > 0) { + throw new RouterError(null, null, `addRoute("${path}") cannot add anything after a full param route`) + } + } + if (cleaned.indexOf('//') >= 0) { + throw new RouterError(null, null, `addRoute("${path}") path has missing name or word between two forward slashes`) + } + + let size = this.registeredPaths.size + if (this.registeredPaths.add(cleaned).size === size) { + throw new RouterError(null, null, `addRoute("${path}") found an existing route with same path of ${cleaned}`) + } + + let handlers = [] + if (Array.isArray(middlewares)) { + handlers.push(...middlewares) + if (typeof(orgHandler) !== 'function') { + throw new RouterError(orgHandler, null, `addRoute("${path}") was called with a handler that was not a function`) + } + } else { + handlers.push(middlewares) + } + if (orgHandler) { + handlers.push(orgHandler) + } + for (let handler of handlers) { + if (typeof(handler) !== 'function') { + throw new RouterError(handler, null, `addRoute("${path}") was called with a handler that was not a function`) + } + } + + this.paths.push({ + path, + handlers + }) + } + + __buildChild(x, i, splitPaths) { + let splitPath = splitPaths[0] + let letter = new Child(splitPath.split, x, i) + + let consume = [] + if (splitPath.split.length === x + 1 + && (splitPath.split[x].isParams + || splitPath.split[x].isFullParams + || splitPath.split[x].word.length === i + 1)) { + letter.path = splitPath.entry + letter.count += 1 + } else { + consume = [splitPath] + } + + for (let y = 1; y < splitPaths.length; y++) { + let checkPath = splitPaths[y] + if (!checkPath.split[x] + || checkPath.split[x].isParams !== splitPath.split[x].isParams + || checkPath.split[x].isFullParams !== splitPath.split[x].isFullParams + || !checkPath.split[x].isParams + && !checkPath.split[x].isFullParams + && (checkPath.split[x].word[i] || '/') !== letter.char) break + consume.push(checkPath) + } + + letter.count += consume.length + if (splitPath.split[x].word.length === i || splitPath.split[x].isParams || splitPath.split[x].isFullParams) { + x++ + i = -1 + } + while (consume.length) { + letter.children.push(this.__buildChild(x, i + 1, consume)) + consume.splice(0, letter.children[letter.children.length - 1].count) + } + return letter + } + + __buildTree(splitPaths) { + let builder = [] + while (splitPaths.length) { + builder.push(this.__buildChild(0, 0, splitPaths)) + splitPaths.splice(0, builder[builder.length - 1].count) + } + return builder + } + + __splitAndSortPaths(paths, separateStatic = true) { + let staticPaths = new Map() + let paramsPaths = [] + let collator = new Intl.Collator('en', { sensitivity: 'accent' }); + + paths.forEach(function(entry) { + if (entry.path[0] !== '/') throw new RouterError(entry, null, 'Specified route was missing forward slash at start') + + // Collect static paths separately + if (entry.path.indexOf('/:') < 0 && separateStatic) { + return staticPaths.set(entry.path, { + path: entry, + params: {} + }) + } + + // Collect params path separately + paramsPaths.push({ + split: entry.path.slice(1).split(/\//g).map(function(word) { + let actualWord = word.replace(regParamPrefix, '') + return { + word: actualWord, + isParams: word[0] === ':' && word[1] !== ':', + isFullParams: word[0] === ':' && word[1] === ':', + paramVarName: word[0] === ':' + ? actualWord.replace(regCleanNonAschii, '_').replace(regCleanRest, '_') + : null + } + }), + entry, + }) + }) + paramsPaths.sort(function(aGroup, bGroup) { + let length = Math.max(aGroup.split.length, bGroup.split.length) + for (let x = 0; x < length; x++) { + let a = aGroup.split[x] + let b = bGroup.split[x] + if (!a) return -1 + if (!b) return 1 + // Full params go last + if (a.isFullParams && b.isFullParams) throw new RouterError(aGroup.entry, bGroup.entry, 'Two full path routes found on same level') + if (a.isFullParams) return 1 + if (b.isFullParams) return -1 + // Params go second last + if (a.isParams && !b.isParams) return 1 + if (!a.isParams && b.isParams) return -1 + // otherwise sort alphabetically if not identical + if (a.word !== b.word) return collator.compare(a.word, b.word) + } + throw new RouterError(aGroup, bGroup, 'Two identical paths were found') + }) + + return { + staticPaths, + paramsPaths, + } + } + + + __getIndex(offset, additions, params) { + return (offset + additions) + + (params.length + ? ' + ' + params.map(a => `offset${a[1]}`).join(' + ') + : '') + } + + __treeIntoCompiledCodeReturnPath(indentString, paths, branch, params) { + let pathIndex = paths.indexOf(branch.path) + if (pathIndex < 0) { + throw new RouterError(branch.path, null, 'InternalError: Specified path was not found in paths') + } + let output = '\n' + indentString + `return {` + output += '\n' + indentString + ` path: paths[${pathIndex}],` + if (params.length) { + output += '\n' + indentString + ` params: {` + for (let param of params) { + output += '\n' + indentString + ` ${param[0]}: s${param[1]},` + } + output += '\n' + indentString + ` },` + } else { + output += '\n' + indentString + ` params: {},` + } + output += '\n' + indentString + `}` + return output + } + + __treeIntoCompiledCodeBranch(paths, branches, indent = 0, params = []) { + let output = '' + let indentation = spaces.slice(0, (indent - params.length) * 2) + let addEndBracket = true + + for (let i = 0; i < branches.length; i++) { + let branch = branches[i] + if (i > 0) { + if (!branch.isParams && !branch.isFullParams) { + output += ' else ' + } else { + // output += '} //' + output += '\n' + indentation + } + } + + if (!branch.isParams && !branch.isFullParams) { + output += `if (buf[${this.__getIndex(indent, 0, params)}] === ${branch.char.charCodeAt(0)}) { // ${branch.char}` + + if (branch.path) { + output += '\n' + indentation + ` if (buf.length === ${this.__getIndex(indent, 1, params)}) {` + output += this.__treeIntoCompiledCodeReturnPath(indentation + ' ', paths, branch, params) + output += '\n' + indentation + ` }` + } + } else { + addEndBracket = false + let paramVarName = (params.length + 1) + '_' + branch.paramVarName + output += `let s${paramVarName} = str.slice(${this.__getIndex(indent, 0, params)}${branch.isFullParams ? '' : `, buf.indexOf(${SlashCode}, ${this.__getIndex(indent, 0, params)}) >>> 0`})` + output += '\n' + indentation + `let offset${paramVarName} = s${paramVarName}.length` + output += '\n' + indentation + params.push([branch.isParams || branch.isFullParams, paramVarName]) + + if (branch.isFullParams) { + output += this.__treeIntoCompiledCodeReturnPath(indentation, paths, branch, params) + } else if (branch.path) { + output += '\n' + indentation + `if (buf.length === ${this.__getIndex(indent, 0, params)}) {` + output += this.__treeIntoCompiledCodeReturnPath(indentation + ' ', paths, branch, params) + output += '\n' + indentation + `}` + } + } + + if (branch.children.length) { + if (branch.path) { + output += ' else ' + } else { + output += '\n' + indentation + ' ' + } + output += this.__treeIntoCompiledCodeBranch(paths, branch.children, indent + 1, params.slice()) + } + if (addEndBracket) { + output += '\n' + indentation + '} ' + } + } + return output + } + + __treeIntoCompiledCodeClosure(paths, tree, staticList) { + let output = 'return function RBufferStrSliceClosure(str) {' + if (staticList.size > 0) { + output += '\n let checkStatic = staticList.get(str)' + output += '\n if(checkStatic) {' + output += '\n return checkStatic' + output += '\n }' + } + if (tree.length) { + output += '\n var buf = Buffer.from(str)' + output += '\n ' + this.__treeIntoCompiledCodeBranch(paths, tree, 1, []) + } + output += '\n return null' + output += '\n}' + // console.log(output) + return new Function('paths', 'staticList', output)(paths, staticList) + } + + compile() { + let splitPaths = this.__splitAndSortPaths(this.paths) + let tree = this.__buildTree(splitPaths.paramsPaths.slice()) + this.match = this.__treeIntoCompiledCodeClosure(this.paths, tree, splitPaths.staticPaths) + } + + match(url) { + this.compile() + return this.match(url) + } +} + +/** + * Flaska + */ +export class Flaska { + constructor(opts = {}, orgHttp = http, orgStream = stream) { + this._before = [] + this._beforeCompiled = null + this._beforeAsync = [] + this._beforeAsyncCompiled = null + this._after = [] + this._afterCompiled = null + this._afterAsync = [] + this._afterAsyncCompiled = null + this._on404 = function(ctx) { + if (ctx.body == null && ctx.status !== 204) { + ctx.status = 404 + ctx.body = { + status: 404, + message: statuses[404], + } + } + } + this._backuperror = this._onerror = function(err, ctx) { + ctx.log.error(err) + if (err instanceof HttpError) { + ctx.status = err.status + ctx.body = err.body || { + status: err.status, + message: statuses[err.status] || statuses[500], + } + } else { + ctx.status = 500 + ctx.body = { + status: 500, + message: statuses[500], + } + } + } + this._onreqerror = function(err, ctx) { + if (err.message !== 'aborted') { + ctx.log.error(err) + ctx.res.statusCode = ctx.statusCode = 400 + } + ctx.res.end() + } + this._onreserror = function(err, ctx) { + ctx.log.error(err) + } + + 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:; font-src 'self' data:; 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 (opts.appendHeaders) { + let appendKeys = Object.keys(opts.appendHeaders) + for (let key of appendKeys) { + options.defaultHeaders[key] = opts.appendHeaders[key] + } + } + + 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 = {' + constructFunction += `'Date': new Date().toUTCString(),` + 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 + this.routers = { + 'GET': new FlaskaRouter(), + 'POST': new FlaskaRouter(), + 'PUT': new FlaskaRouter(), + 'DELETE': new FlaskaRouter(), + 'OPTIONS': new FlaskaRouter(), + 'PATCH': new FlaskaRouter(), + } + // HEAD and GET should be identical + this.routers['HEAD'] = this.routers['GET'] + + this.get = this.routers.GET.addRoute.bind(this.routers.GET) + this.post = this.routers.POST.addRoute.bind(this.routers.POST) + this.put = this.routers.PUT.addRoute.bind(this.routers.PUT) + this.delete = this.routers.DELETE.addRoute.bind(this.routers.DELETE) + this.options = this.routers.OPTIONS.addRoute.bind(this.routers.OPTIONS) + this.patch = this.routers.PATCH.addRoute.bind(this.routers.PATCH) + } + + devMode() { + this._backuperror = this._onerror = function(err, ctx) { + ctx.log.error(err) + if (err instanceof HttpError) { + ctx.status = err.status + ctx.body = err.body || { + status: err.status, + message: `${statuses[err.status] || statuses[500]}: ${err.message}`, + stack: err.stack || '', + } + } else { + ctx.status = 500 + ctx.body = { + status: 500, + message: `${statuses[500]}: ${err.message}`, + stack: err.stack || '', + } + } + } + } + + on404(handler) { + assertIsHandler(handler, 'on404()') + this._on404 = handler + } + + onerror(handler) { + assertIsHandler(handler, 'onerror()') + this._onerror = handler + } + + onreqerror(handler) { + assertIsHandler(handler, 'onreqerror()') + this._onreqerror = handler + } + + onreserror(handler) { + assertIsHandler(handler, 'onreserror()') + this._onreserror = handler + } + + before(handler) { + assertIsHandler(handler, 'before()') + this._before.push(handler) + } + + beforeAsync(handler) { + assertIsHandler(handler, 'beforeAsync()') + this._beforeAsync.push(handler) + } + + after(handler) { + assertIsHandler(handler, 'after()') + this._after.push(handler) + } + + afterAsync(handler) { + assertIsHandler(handler, 'afterAsync()') + this._afterAsync.push(handler) + } + + requestStart(req, res) { + let url = req.url + let search = '' + let hasSearch = url.indexOf('?') + + if (hasSearch > 0) { + search = url.slice(hasSearch) + url = url.slice(0, hasSearch) + } + + let ctx = { + log: this.log, + req: req, + res: res, + method: req.method, + url: url, + search: search, + state: {}, + status: 200, + query: new Map(), + body: null, + type: null, + length: null, + } + + req.on('error', (err) => { + if (err.message === 'aborted') { + ctx.aborted = true + } + this._onreqerror(err, ctx) + this.requestEnded(ctx) + }) + res.on('error', (err) => { + this._onreserror(err, ctx) + }) + + res.on('finish', () => { + this.requestEnded(ctx) + }) + + try { + this._beforeCompiled(ctx) + if (this._beforeAsyncCompiled) { + return this._beforeAsyncCompiled(ctx) + .then(() => { + this.requestStartInternal(ctx) + }).catch(err => { + this.requestEnd(err, ctx) + }) + } + this.requestStartInternal(ctx) + } + catch (err) { + this.requestEnd(err, ctx) + } + } + + requestStartInternal(ctx) { + let route = this.routers[ctx.method].match(ctx.url) + if (!route) { + let middle = this._on404(ctx) + if (middle && middle.then) { + return middle.then(() => { + this.requestEnd(null, ctx) + }, err => { + this.requestEnd(err, ctx) + }) + } + return this.requestEnd(null, ctx) + } + + ctx.params = route.params + + let handlers = this.runHandlers(ctx, route.path.handlers, 0) + + if (handlers && handlers.then) { + return handlers.then(() => { + this.requestEnd(null, ctx) + }, err => { + this.requestEnd(err, ctx) + }) + } + + this.requestEnd(null, ctx) + } + + runHandlers(ctx, middles, index) { + for (let i = index; i < middles.length; i++) { + let res = middles[i](ctx) + if (res && res.then) { + return res.then(() => { + return this.runHandlers(ctx, middles, i + 1) + }) + } + } + } + + requestEnd(orgErr, ctx) { + let err = orgErr + let handleUsed = Boolean(ctx.body && ctx.body.handleRequest) + if (handleUsed) { + try { + ctx.body = ctx.body.handleRequest(ctx) + } catch (newErr) { + err = newErr + } + } + if (err) { + try { + this._onerror(err, ctx) + } catch (err) { + this._backuperror(err, ctx) + } + } + + if (ctx.res.writableEnded) { + return + } + + if (ctx.body == null && !handleUsed && ctx.status === 200) { + ctx.status = 204 + } + + if (statuses.empty[ctx.status]) { + ctx.res.writeHead(ctx.status, ctx.headers) + return ctx.res.end() + } + + let body = ctx.body + + // Special handling for files + if (body && typeof(body.pipe) === 'function') { + // Be smart when handling file handles, auto detect mime-type + // based off of the extension of the file. + if (!ctx.type && body.path) { + let ext = path.extname(body.path).slice(1) + if (ext && MimeTypeDb[ext]) { + let found = MimeTypeDb[ext] + ctx.type = found[found.length - 1] + } + } + ctx.headers['Content-Type'] = ctx.type || 'application/octet-stream' + + ctx.res.writeHead(ctx.status, ctx.headers) + + if (ctx.method !== 'HEAD') { + return this.stream.pipeline(body, ctx.res, function() { }) + } else { + try { + body.destroy() + } catch { } + return ctx.res.end() + } + } + + let length = 0 + + if (body instanceof Buffer) { + length = body.byteLength + ctx.type = ctx.type || 'application/octet-stream' + } else if (typeof(body) === 'object' && body) { + body = JSON.stringify(body) + length = Buffer.byteLength(body) + ctx.type = 'application/json; charset=utf-8' + } else if (body) { + body = body.toString() + length = Buffer.byteLength(body) + ctx.type = ctx.type || 'text/plain; charset=utf-8' + } + + if (ctx.type) { + ctx.headers['Content-Type'] = ctx.type + } + if (!ctx.headers['Content-Length']) { + ctx.headers['Content-Length'] = length + } + + ctx.res.writeHead(ctx.status, ctx.headers) + if (body && ctx.method !== 'HEAD') { + ctx.res.end(body) + } else { + ctx.res.end() + } + } + + requestEnded(ctx) { + if (ctx.finished) return + ctx.finished = true + + // Prevent accidental leaking + if (ctx.body && ctx.body.pipe && !ctx.body.closed) { + ctx.body.destroy() + } + this._afterCompiled(ctx) + if (this._afterAsyncCompiled) { + return this._afterAsyncCompiled(ctx).then() + } + } + + compile() { + let types = ['before', 'after'] + for (let i = 0; i < types.length; i++) { + let type = types[i] + let args = '' + let body = '' + for (let i = 0; i < this['_' + type].length; i++) { + args += `a${i}, ` + body += `a${i}(ctx);` + } + args += 'ctx' + + let func = new Function(args, body) + this[`_${type}Compiled`] = func.bind(this, ...this['_' + type]) + + if (this[`_${type}Async`].length) { + args = '' + body = 'return Promise.all([' + for (let i = 0; i < this[`_${type}Async`].length; i++) { + args += `a${i}, ` + body += `a${i}(ctx),` + } + args += 'ctx' + body += '])' + func = new Function(args, body) + this[`_${type}AsyncCompiled`] = func.bind(this, ...this[`_${type}Async`]) + } + } + this.routers.GET.compile() + this.routers.POST.compile() + this.routers.PUT.compile() + this.routers.DELETE.compile() + this.routers.OPTIONS.compile() + this.routers.PATCH.compile() + } + + create() { + this.compile() + this.server = this.http.createServer(this.requestStart.bind(this)) + + this.server.on('connection', function (socket) { + // Set socket idle timeout in milliseconds + socket.setTimeout(1000 * 60 * 5) // 5 minutes + + // Wait for timeout event (socket will emit it when idle timeout elapses) + socket.on('timeout', function () { + // Call destroy again + socket.destroy(); + }) + }) + } + + listen(port, orgIp, orgcb) { + let ip = orgIp + let cb = orgcb + if (!cb && typeof(orgIp) === 'function') { + ip = '::' + cb = orgIp + } + if (typeof(port) !== 'number') { + throw new Error('Flaska.listen() called with non-number in port') + } + + this.create() + + this.server.listen(port, ip, cb) + } + + listenAsync(port, ip = '::') { + if (typeof(port) !== 'number') { + return Promise.reject(new Error('Flaska.listen() called with non-number in port')) + } + + this.create() + + if (this.server.listenAsync && typeof(this.server.listenAsync) === 'function') { + return this.server.listenAsync(port, ip) + } + + return new Promise((res, rej) => { + this.server.listen(port, ip, function(err) { + if (err) return rej(err) + return res() + }) + }) + } + + closeAsync() { + if (!this.server) return Promise.resolve() + + return new Promise((res, rej) => { + this.server.close(function(err) { + if (err) { return rej(err) } + + // Waiting 0.1 second for it to close down + setTimeout(res, 100) + }) + }) + } +} diff --git a/flaska_old.mjs b/flaska_old.mjs new file mode 100644 index 0000000..db51c89 --- /dev/null +++ b/flaska_old.mjs @@ -0,0 +1,1128 @@ +import os from 'os' +import crypto from 'crypto' +import path from 'path' +import http from 'http' +import stream from 'stream' +import fs from 'fs/promises' +import fsSync from 'fs' +import { URL } from 'url' +import { Buffer } from 'buffer' +import zlib from 'zlib' + +function getDb() { + // Take from @thi-ng/mime which is a reduced mime-db + let MimeTypeDbRaw = Buffer.from('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', 'base64') + let inbetween = JSON.parse(zlib.brotliDecompressSync(MimeTypeDbRaw).toString()) + + let res = {} + for (let groupID in inbetween) { + const group = inbetween[groupID]; + for (let type in group) { + const mime = groupID + "/" + type; + for (let e of group[type].split(",")) { + const isLowPri = e[0] === "*"; + const ext = isLowPri ? e.substr(1) : e; + let coll = res[ext]; + !coll && (coll = res[ext] = []); + isLowPri ? coll.push(mime) : coll.unshift(mime); + } + } + } + return res +} + +export const MimeTypeDb = getDb() + +/** + * Router + */ + +class Branch { + constructor() { + this.children = new Map() + this.paramName = null + this.fullparamName = null + this.handler = null + this.middlewares = [] + } +} + +export const ErrorCodes = { + ERR_CONNECTION_ABORTED: 'ERR_CON_ABORTED' +} + +// Taken from https://github.com/nfp-projects/koa-lite/blob/master/lib/statuses.js +const statuses = { + 100: 'Continue', 101: 'Switching Protocols', 102: 'Processing', 103: 'Early Hints', + 200: 'OK', 201: 'Created', 202: 'Accepted', 203: 'Non-Authoritative Information', 204: 'No Content', 205: 'Reset Content', 206: 'Partial Content', 207: 'Multi-Status', 208: 'Already Reported', 226: 'IM Used', + 300: 'Multiple Choices', 301: 'Moved Permanently', 302: 'Found', 303: 'See Other', 304: 'Not Modified', 305: 'Use Proxy', 306: '(Unused)', 307: 'Temporary Redirect', 308: 'Permanent Redirect', + 400: 'Bad Request', 401: 'Unauthorized', 402: 'Payment Required', 403: 'Forbidden', 404: 'Not Found', 405: 'Method Not Allowed', 406: 'Not Acceptable', 407: 'Proxy Authentication Required', 408: 'Request Timeout', 409: 'Conflict', 410: 'Gone', 411: 'Length Required', 412: 'Precondition Failed', 413: 'Payload Too Large', 414: 'URI Too Long', 415: 'Unsupported Media Type', 416: 'Range Not Satisfiable', 417: 'Expectation Failed', 418: 'I\'m a teapot', 421: 'Misdirected Request', 422: 'Unprocessable Entity', 423: 'Locked', 424: 'Failed Dependency', 425: 'Too Early', 426: 'Upgrade Required', 428: 'Precondition Required', 429: 'Too Many Requests', 431: 'Request Header Fields Too Large', 451: 'Unavailable For Legal Reasons', + 500: 'Internal Server Error', 501: 'Not Implemented', 502: 'Bad Gateway', 503: 'Service Unavailable', 504: 'Gateway Timeout', 505: 'HTTP Version Not Supported', 506: 'Variant Also Negotiates', 507: 'Insufficient Storage', 508: 'Loop Detected', 509: 'Bandwidth Limit Exceeded', 510: 'Not Extended', 511: 'Network Authentication Required', + redirect: { + 300: true, + 301: true, + 302: true, + 303: true, + 305: true, + 307: true, + 308: true + }, + empty: { + 204: true, + 205: true, + 304: true + } +} +const __paramMapName = '__param' +const __fullParamMapName = '__fullparam' + +function assertIsHandler(handler, name) { + if (typeof(handler) !== 'function') { + throw new Error(`${name} was called with a handler that was not a function`) + } +} + +export function QueryHandler() { + return function(ctx) { + ctx.query = (new URL(ctx.req.url, 'http://localhost')).searchParams + } +} + +export function JsonHandler(org = {}) { + let opts = { + sizeLimit: org.sizeLimit || (10 * 1024) + } + return function(ctx) { + const buffers = []; + let size = 0 + + return new Promise(function(res, rej) { + ctx.req.on('data', chunk => { + size += chunk.length + if (size > opts.sizeLimit) { + return rej(new HttpError(413, `Body limit of ${opts.sizeLimit} bytes reached with ${size} bytes`)) + } + + buffers.push(chunk) + }) + ctx.req.on('end', () => { + res() + }) + }) + .then(function() { + if (!buffers.length) { + ctx.req.body = {} + return + } + + const data = Buffer.concat(buffers).toString(); + + try { + ctx.req.body = JSON.parse(data) + } catch (err) { + return Promise.reject(new HttpError(400, `Invalid JSON: ${err.message}`, { + status: 400, + message: `Invalid JSON: ${err.message}`, + request: data, + })) + } + }) + } +} + +export function CorsHandler(opts = {}) { + const options = { + allowedMethods: opts.allowedMethods || 'GET,HEAD,PUT,POST,DELETE,PATCH', + allowedOrigins: opts.allowedOrigins || [], + allowedHeaders: opts.allowedHeaders, + credentials: opts.credentials || false, + exposeHeaders: opts.exposeHeaders || '', + maxAge: opts.maxAge || '', + } + const allowAll = options.allowedOrigins.includes('*') + + return function(ctx) { + // Always add vary header on origin. Prevent caches from + // accidentally caching wrong preflight request + ctx.headers['Vary'] = 'Origin' + + // Set status to 204 if OPTIONS. Just handy for flaska and + // other checking. + if (ctx.method === 'OPTIONS') { + ctx.status = 204 + } + + // Check origin is specified. Nothing needs to be done if + // there is no origin or it doesn't match + let origin = ctx.req.headers['origin'] + if (!origin || (!allowAll && !options.allowedOrigins.includes(origin))) { + return + } + + // Set some extra headers if this is a pre-flight. Most of + // these are not needed during a normal request. + if (ctx.method === 'OPTIONS') { + if (!ctx.req.headers['access-control-request-method']) { + return + } + + if (options.maxAge) { + ctx.headers['Access-Control-Max-Age'] = options.maxAge + } + + let reqHeaders = options.allowedHeaders + || ctx.req.headers['access-control-request-headers'] + if (reqHeaders && options.allowedHeaders !== false) { + ctx.headers['Access-Control-Allow-Headers'] = reqHeaders + } + ctx.headers['Access-Control-Allow-Methods'] = options.allowedMethods + } else { + if (options.exposeHeaders) { + ctx.headers['Access-Control-Expose-Headers'] = options.exposeHeaders + } + } + + ctx.headers['Access-Control-Allow-Origin'] = origin + + if (options.credentials) { + ctx.headers['Access-Control-Allow-Credentials'] = 'true' + } + } +} + +export function FormidableHandler(formidable, org = {}) { + let lastDateString = '' + let incrementor = 1 + + let opts = { + rename: true, + parseFields: org.parseFields || false, + uploadDir: org.uploadDir || os.tmpdir(), + filename: org.filename || function(file) { + let prefix = new Date() + .toISOString() + .replace(/-/g, '') + .replace('T', '_') + .replace(/:/g, '') + .replace(/\..+/, '_') + + // Prevent accidental overwriting if two file uploads with + // same name get uploaded at exact same second. + if (prefix === lastDateString) { + prefix += incrementor.toString().padStart('2', '0') + '_' + incrementor++ + } else { + lastDateString = prefix + incrementor + } + + return prefix + file.name + }, + maxFileSize: org.maxFileSize || 8 * 1024 * 1024, + maxFieldsSize: org.maxFieldsSize || 10 * 1024, + maxFields: org.maxFields || 50, + } + if (org.rename != null) { + opts.rename = org.rename + } + + // For testing/stubbing purposes + let rename = formidable.fsRename || fs.rename + + return function(ctx) { + let form = formidable.IncomingForm() + form.uploadDir = opts.uploadDir + form.maxFileSize = opts.maxFileSize + form.maxFieldsSize = opts.maxFieldsSize + form.maxFields = opts.maxFields + + return new Promise(function(res, rej) { + form.parse(ctx.req, function(err, fields, files) { + if (err) return rej(new HttpError(400, err.message)) + + if (opts.parseFields) { + Object.keys(fields).forEach(function(key) { + try { + fields[key] = JSON.parse(fields[key]) + } catch { } + }) + } + + ctx.req.body = fields + ctx.req.files = files + ctx.req.file = null + + + if (!ctx.req.files) { + return res() + } + + let keys = Object.keys(files).filter(key => Boolean(ctx.req.files[key])) + + Promise.all( + keys.map(key => { + let filename + let target + + try { + filename = opts.filename(ctx.req.files[key]) || ctx.req.files[key].name + target = path.join(opts.uploadDir, filename) + } catch (err) { + return Promise.reject(err) + } + + return rename(ctx.req.files[key].path, target) + .then(function() { + if (!ctx.req.files[key].type || ctx.req.files[key].type === 'application/octet-stream') { + let found = MimeTypeDb[path.extname(filename).slice(1)] + ctx.req.files[key].type = found && found[0] || 'application/octet-stream' + } + ctx.req.files[key].path = target + ctx.req.files[key].filename = filename + }) + }) + ) + .then(() => { + if (keys.length === 1 && keys[0] === 'file') { + ctx.req.file = ctx.req.files.file + } + res() + }, rej) + }) + }) + } +} + +export class HttpError extends Error { + constructor(statusCode, message, body = null) { + super(message); + + Error.captureStackTrace(this, HttpError); + + let proto = Object.getPrototypeOf(this); + proto.name = 'HttpError'; + + this.status = statusCode + this.body = body + } +} + +const RangeRegexTester = /bytes=(\d+)-(\d+)?/ + +export class FileResponse { + constructor(filepath, stat) { + this.filepath = filepath + this.stat = stat + } + + handleRequest(ctx, useFs = fsSync) { + let etag = '"' + this.stat.ino + '-' + this.stat.size + '-' + this.stat.mtime.getTime() + '"' + let lastModified = this.stat.mtime.toUTCString() + let lastModifiedRounded = Date.parse(lastModified) + + if (ctx.req.headers['if-match'] && ctx.req.headers['if-match'] !== etag) { + throw new HttpError(412, `Request if-match pre-condition failed`) + } + if (ctx.req.headers['if-unmodified-since']) { + let check = Date.parse(ctx.req.headers['if-unmodified-since']) + if (!check || check < lastModifiedRounded) { + throw new HttpError(412, `Request if-unmodified-since pre-condition failed`) + } + } + + ctx.headers['Etag'] = etag + + if (ctx.req.headers['if-none-match']) { + let split = ctx.req.headers['if-none-match'].split(',') + for (let check of split) { + if (check.trim() === etag) { + ctx.status = 304 + return null + } + } + } else if (ctx.req.headers['if-modified-since']) { + let check = Date.parse(ctx.req.headers['if-modified-since']) + if (check >= lastModifiedRounded) { + ctx.status = 304 + return null + } + } + + let readOptions = {} + let size = this.stat.size + + if (ctx.req.headers['range']) { + let match = RangeRegexTester.exec(ctx.req.headers['range']) + + let ifRange = ctx.req.headers['if-range'] + if (ifRange) { + if (ifRange[0] === '"' && ifRange !== etag) { + match = null + } else if (ifRange[0] !== '"') { + let check = Date.parse(ifRange) + if (!check || check < lastModifiedRounded) { + match = null + } + } + } + + if (match) { + let start = Number(match[1]) + let end = size - 1 + if (match[2]) { + end = Math.min(Number(match[2]), size - 1) + } + + if (start >= size) { + throw new HttpError(416, `Out of range start ${start} outside of ${size} bounds`) + } + + if (start <= end) { + size = end - start + 1 + readOptions.start = start + readOptions.end = end + ctx.headers['Content-Range'] = start + '-' + end + '/' + this.stat.size + ctx.status = 206 + } + } + } + + let ext = path.extname(this.filepath).slice(1) + let found = MimeTypeDb[ext] + if (found) { + ctx.type = found[found.length - 1] + } + + ctx.headers['Last-Modified'] = lastModified + ctx.headers['Content-Length'] = size + if (ctx.method !== 'HEAD') { + let stream = useFs.createReadStream(this.filepath, readOptions) + return stream + } + return null + } +} + +export class FlaskaRouter { + constructor() { + this.root = new Branch() + } + + addRoute(route, orgMiddlewares, orgHandler) { + if (route[0] !== '/') + throw new Error(`route "${route}" must start with forward slash`) + + let middlewares = orgMiddlewares + let handler = orgHandler + if (!orgHandler) { + handler = orgMiddlewares + middlewares = [] + } + if (middlewares && typeof(middlewares) === 'function') { + middlewares = [middlewares] + } + assertIsHandler(handler, 'addRoute()') + + let start = 1 + let end = 1 + let name = '' + let param = '' + let isParam = false + let isFullParam = false + let branch = this.root + + if (route.indexOf(':') < 0) { + let name = route + if (name.length > 1 && name[name.length - 1] === '/') { + name = name.slice(0, -1) + } + let child = new Branch() + branch.children.set(name, child) + child.handler = handler + child.middlewares = middlewares + } + + for (let i = 1; i <= route.length; i++) { + if ((i === route.length || route[i] === '/') && end > start) { + if (branch.fullparamName) { + throw new Error(`route "${route}" conflicts with a sub-branch that has a full param child`) + } + let child + name = route.substring(start, end) + if (isFullParam) { + param = name + name = __fullParamMapName + } else if (isParam) { + param = name + name = __paramMapName + } + if (branch.children.has(name)) { + child = branch.children.get(name) + } + else if (isParam && !isFullParam && branch.children.has(__fullParamMapName)) { + throw new Error(`route "${route}" conflicts with a sub-branch that has a full param child`) + } + else if (isFullParam && branch.children.has(__paramMapName)) { + throw new Error(`route "${route}" conflicts with a sub-branch that has a partial param child`) + } + else { + child = new Branch() + branch.children.set(name, child) + } + branch = child + end = i + start = i + if (isParam) { + if (branch.paramName && branch.paramName !== param) { + throw new Error(`route "${route}" conflicts with pre-existing param name of ${branch.paramName} instead of ${param}`) + } + if (isFullParam) { + branch.fullparamName = param + } else { + branch.paramName = param + } + isParam = false + } + } else if (route[i] === '/' && end === start) { + throw new Error(`route "${route}" has missing path name inbetween slashes`) + } + if (i === route.length) { + branch.handler = handler + branch.middlewares = middlewares + continue + } + if (route[i] === ':') { + if (isParam) { + isFullParam = true + } + isParam = true + end = start = i + 1 + } + else if (route[i] === '/') { + end = start = i + 1 + } + else { + end++ + } + } + } + + match(orgUrl) { + let url = orgUrl + if (url.length > 1 && url[url.length - 1] === '/') { + url = url.slice(0, -1) + } + let branch = this.root + let start = 1 + let end = 1 + let output + let name + let char + let params = {} + if (output = branch.children.get(url)) { + return { + handler: output.handler, + middlewares: output.middlewares, + params: params, + } + } + for (let i = 1; i <= url.length; i++) { + char = url[i] + if ((i === url.length || char === '/') && end > start) { + name = url.slice(start, end) + if (output = branch.children.get(name)) { + branch = output + } + else if (output = branch.children.get(__paramMapName)) { + branch = output + params[branch.paramName] = name + } + else if (output = branch.children.get(__fullParamMapName)) { + params[output.fullparamName] = url.slice(start) + return { + handler: output.handler, + middlewares: output.middlewares, + params: params, + } + } else { + if (output = this.root.children.get(__fullParamMapName)) { + params = { + [output.fullparamName]: url.slice(1) + } + return { + handler: output.handler, + middlewares: output.middlewares, + params: params, + } + } + return null + } + i++ + end = start = i + char = url[i] + } + // Check branch.handler. This can happen if route /::path is added + // and request is '/' it will attempt to match root which will fail + if (i >= url.length && branch.handler) { + return { + handler: branch.handler, + middlewares: branch.middlewares, + params: params, + } + } + if (char === '/') { + end = start = i + 1 + } else { + end++ + } + } + if (output = this.root.children.get(__fullParamMapName)) { + params = { + [output.fullparamName]: url.slice(1) + } + return { + handler: output.handler, + middlewares: output.middlewares, + params: params, + } + } + return null + } +} + +/** + * Flaska + */ +export class Flaska { + constructor(opts = {}, orgHttp = http, orgStream = stream) { + this._before = [] + this._beforeCompiled = null + this._beforeAsync = [] + this._beforeAsyncCompiled = null + this._after = [] + this._afterCompiled = null + this._afterAsync = [] + this._afterAsyncCompiled = null + this._on404 = function(ctx) { + if (ctx.body == null && ctx.status !== 204) { + ctx.status = 404 + ctx.body = { + status: 404, + message: statuses[404], + } + } + } + this._backuperror = this._onerror = function(err, ctx) { + ctx.log.error(err) + if (err instanceof HttpError) { + ctx.status = err.status + ctx.body = err.body || { + status: err.status, + message: statuses[err.status] || statuses[500], + } + } else { + ctx.status = 500 + ctx.body = { + status: 500, + message: statuses[500], + } + } + } + this._onreqerror = function(err, ctx) { + if (err.message !== 'aborted') { + ctx.log.error(err) + ctx.res.statusCode = ctx.statusCode = 400 + } + ctx.res.end() + } + this._onreserror = function(err, ctx) { + ctx.log.error(err) + } + + 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:; font-src 'self' data:; 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 (opts.appendHeaders) { + let appendKeys = Object.keys(opts.appendHeaders) + for (let key of appendKeys) { + options.defaultHeaders[key] = opts.appendHeaders[key] + } + } + + 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 = {' + constructFunction += `'Date': new Date().toUTCString(),` + 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 + this.routers = { + 'GET': new FlaskaRouter(), + 'POST': new FlaskaRouter(), + 'PUT': new FlaskaRouter(), + 'DELETE': new FlaskaRouter(), + 'OPTIONS': new FlaskaRouter(), + 'PATCH': new FlaskaRouter(), + } + // HEAD and GET should be identical + this.routers['HEAD'] = this.routers['GET'] + + this.get = this.routers.GET.addRoute.bind(this.routers.GET) + this.post = this.routers.POST.addRoute.bind(this.routers.POST) + this.put = this.routers.PUT.addRoute.bind(this.routers.PUT) + this.delete = this.routers.DELETE.addRoute.bind(this.routers.DELETE) + this.options = this.routers.OPTIONS.addRoute.bind(this.routers.OPTIONS) + this.patch = this.routers.PATCH.addRoute.bind(this.routers.PATCH) + } + + _assertIsHandler(handler, name) { + if (typeof(handler) !== 'function') { + throw new Error(`${name} was called with a handler that was not a function`) + } + } + + devMode() { + this._backuperror = this._onerror = function(err, ctx) { + ctx.log.error(err) + if (err instanceof HttpError) { + ctx.status = err.status + ctx.body = err.body || { + status: err.status, + message: `${statuses[err.status] || statuses[500]}: ${err.message}`, + stack: err.stack || '', + } + } else { + ctx.status = 500 + ctx.body = { + status: 500, + message: `${statuses[500]}: ${err.message}`, + stack: err.stack || '', + } + } + } + } + + on404(handler) { + assertIsHandler(handler, 'on404()') + this._on404 = handler + } + + onerror(handler) { + assertIsHandler(handler, 'onerror()') + this._onerror = handler + } + + onreqerror(handler) { + assertIsHandler(handler, 'onreqerror()') + this._onreqerror = handler + } + + onreserror(handler) { + assertIsHandler(handler, 'onreserror()') + this._onreserror = handler + } + + before(handler) { + assertIsHandler(handler, 'before()') + this._before.push(handler) + } + + beforeAsync(handler) { + assertIsHandler(handler, 'beforeAsync()') + this._beforeAsync.push(handler) + } + + after(handler) { + assertIsHandler(handler, 'after()') + this._after.push(handler) + } + + afterAsync(handler) { + assertIsHandler(handler, 'afterAsync()') + this._afterAsync.push(handler) + } + + requestStart(req, res) { + let url = req.url + let search = '' + let hasSearch = url.indexOf('?') + + if (hasSearch > 0) { + search = url.slice(hasSearch) + url = url.slice(0, hasSearch) + } + + let ctx = { + log: this.log, + req: req, + res: res, + method: req.method, + url: url, + search: search, + state: {}, + status: 200, + query: new Map(), + body: null, + type: null, + length: null, + } + + req.on('error', (err) => { + if (err.message === 'aborted') { + ctx.aborted = true + } + this._onreqerror(err, ctx) + this.requestEnded(ctx) + }) + res.on('error', (err) => { + this._onreserror(err, ctx) + }) + + res.on('finish', () => { + this.requestEnded(ctx) + }) + + try { + this._beforeCompiled(ctx) + if (this._beforeAsyncCompiled) { + return this._beforeAsyncCompiled(ctx) + .then(() => { + this.requestStartInternal(ctx) + }).catch(err => { + this.requestEnd(err, ctx) + }) + } + this.requestStartInternal(ctx) + } + catch (err) { + this.requestEnd(err, ctx) + } + } + + requestStartInternal(ctx) { + let route = this.routers[ctx.method].match(ctx.url) + if (!route) { + let middle = this._on404(ctx) + if (middle && middle.then) { + return middle.then(() => { + this.requestEnd(null, ctx) + }, err => { + this.requestEnd(err, ctx) + }) + } + return this.requestEnd(null, ctx) + } + + ctx.params = route.params + + if (route.middlewares.length) { + let middle = this.handleMiddleware(ctx, route.middlewares, 0) + + if (middle && middle.then) { + return middle.then(() => { + return route.handler(ctx) + }) + .then(() => { + this.requestEnd(null, ctx) + }, err => { + this.requestEnd(err, ctx) + }) + } + } + let handler = route.handler(ctx) + if (handler && handler.then) { + return handler.then(() => { + this.requestEnd(null, ctx) + }, err => { + this.requestEnd(err, ctx) + }) + } + this.requestEnd(null, ctx) + } + + handleMiddleware(ctx, middles, index) { + for (let i = index; i < middles.length; i++) { + let res = middles[i](ctx) + if (res && res.then) { + return res.then(() => { + return this.handleMiddleware(ctx, middles, i + 1) + }) + } + } + } + + requestEnd(orgErr, ctx) { + let err = orgErr + let handleUsed = Boolean(ctx.body && ctx.body.handleRequest) + if (handleUsed) { + try { + ctx.body = ctx.body.handleRequest(ctx) + } catch (newErr) { + err = newErr + } + } + if (err) { + try { + this._onerror(err, ctx) + } catch (err) { + this._backuperror(err, ctx) + } + } + + if (ctx.res.writableEnded) { + return + } + + if (ctx.body === null && !handleUsed && ctx.status === 200) { + ctx.status = 204 + } + + if (statuses.empty[ctx.status]) { + ctx.res.writeHead(ctx.status, ctx.headers) + return ctx.res.end() + } + + let body = ctx.body + + // Special handling for files + if (body && typeof(body.pipe) === 'function') { + // Be smart when handling file handles, auto detect mime-type + // based off of the extension of the file. + if (!ctx.type && body.path) { + let ext = path.extname(body.path).slice(1) + if (ext && MimeTypeDb[ext]) { + let found = MimeTypeDb[ext] + ctx.type = found[found.length - 1] + } + } + ctx.headers['Content-Type'] = ctx.type || 'application/octet-stream' + + ctx.res.writeHead(ctx.status, ctx.headers) + + if (ctx.method !== 'HEAD') { + return this.stream.pipeline(body, ctx.res, function() { }) + } else { + try { + body.destroy() + } catch { } + return ctx.res.end() + } + } + + let length = 0 + + if (body instanceof Buffer) { + length = body.byteLength + ctx.type = ctx.type || 'application/octet-stream' + } else if (typeof(body) === 'object' && body) { + body = JSON.stringify(body) + length = Buffer.byteLength(body) + ctx.type = 'application/json; charset=utf-8' + } else if (body) { + body = body.toString() + length = Buffer.byteLength(body) + ctx.type = ctx.type || 'text/plain; charset=utf-8' + } + + if (ctx.type) { + ctx.headers['Content-Type'] = ctx.type + } + if (!ctx.headers['Content-Length']) { + ctx.headers['Content-Length'] = length + } + + ctx.res.writeHead(ctx.status, ctx.headers) + if (body && ctx.method !== 'HEAD') { + ctx.res.end(body) + } else { + ctx.res.end() + } + } + + requestEnded(ctx) { + if (ctx.finished) return + ctx.finished = true + + // Prevent accidental leaking + if (ctx.body && ctx.body.pipe && !ctx.body.closed) { + ctx.body.destroy() + } + this._afterCompiled(ctx) + if (this._afterAsyncCompiled) { + return this._afterAsyncCompiled(ctx).then() + } + } + + compile() { + let types = ['before', 'after'] + for (let i = 0; i < types.length; i++) { + let type = types[i] + let args = '' + let body = '' + for (let i = 0; i < this['_' + type].length; i++) { + args += `a${i}, ` + body += `a${i}(ctx);` + } + args += 'ctx' + + let func = new Function(args, body) + this[`_${type}Compiled`] = func.bind(this, ...this['_' + type]) + + if (this[`_${type}Async`].length) { + args = '' + body = 'return Promise.all([' + for (let i = 0; i < this[`_${type}Async`].length; i++) { + args += `a${i}, ` + body += `a${i}(ctx),` + } + args += 'ctx' + body += '])' + func = new Function(args, body) + this[`_${type}AsyncCompiled`] = func.bind(this, ...this[`_${type}Async`]) + } + } + } + + create() { + this.compile() + this.server = this.http.createServer(this.requestStart.bind(this)) + + this.server.on('connection', function (socket) { + // Set socket idle timeout in milliseconds + socket.setTimeout(1000 * 60 * 5) // 5 minutes + + // Wait for timeout event (socket will emit it when idle timeout elapses) + socket.on('timeout', function () { + // Call destroy again + socket.destroy(); + }) + }) + } + + listen(port, orgIp, orgcb) { + let ip = orgIp + let cb = orgcb + if (!cb && typeof(orgIp) === 'function') { + ip = '::' + cb = orgIp + } + if (typeof(port) !== 'number') { + throw new Error('Flaska.listen() called with non-number in port') + } + + this.create() + + this.server.listen(port, ip, cb) + } + + listenAsync(port, ip = '::') { + if (typeof(port) !== 'number') { + return Promise.reject(new Error('Flaska.listen() called with non-number in port')) + } + + this.create() + + if (this.server.listenAsync && typeof(this.server.listenAsync) === 'function') { + return this.server.listenAsync(port, ip) + } + + return new Promise((res, rej) => { + this.server.listen(port, ip, function(err) { + if (err) return rej(err) + return res() + }) + }) + } + + closeAsync() { + if (!this.server) return Promise.resolve() + + return new Promise((res, rej) => { + this.server.close(function(err) { + if (err) { return rej(err) } + + // Waiting 0.1 second for it to close down + setTimeout(function() { res() }, 100) + }) + }) + } +} diff --git a/test.mjs b/test.mjs index 80727e3..8fc5ba2 100644 --- a/test.mjs +++ b/test.mjs @@ -1,12 +1,22 @@ import { Flaska } from './flaska.mjs' -const port = 51024 +const port = 51026 const flaska = new Flaska({}, ) +flaska.devMode() + flaska.get('/', function(ctx) { ctx.body = { status: true } }) +flaska.get('/:item/asdf/herp/:derp/bla', function(ctx) { + ctx.body = { item: ctx.params.item } +}) + +flaska.get('/a', function(ctx) { + ctx.body = { status: true } +}) + flaska.get('/error', function(ctx) { process.exit(1) }) diff --git a/test/flaska.api.test.mjs b/test/flaska.api.test.mjs index f0af7b9..6253b4d 100644 --- a/test/flaska.api.test.mjs +++ b/test/flaska.api.test.mjs @@ -5,11 +5,11 @@ import { createCtx, fakeHttp } from './helper.mjs' const faker = fakeHttp() t.describe('#constructor', function() { - t.test('should be able to override the http', 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) @@ -25,7 +25,7 @@ t.describe('#constructor', 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) @@ -38,7 +38,7 @@ t.describe('#constructor', function() { 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) @@ -78,7 +78,7 @@ t.describe('#constructor', function() { let ctx = {} flaska._before[0](ctx) - + let keys = Object.keys(defaultHeaders) assert.strictEqual(Object.keys(ctx.headers).length, keys.length + 1) @@ -103,7 +103,7 @@ t.describe('#constructor', function() { let ctx = {} flaska._before[0](ctx) - + assert.deepEqual( Object.keys(ctx.headers).sort(), ['Server', 'Herp', 'X-Content-Type-Options','Content-Security-Policy','Cross-Origin-Opener-Policy','Cross-Origin-Resource-Policy','Cross-Origin-Embedder-Policy','Date'].sort() @@ -140,7 +140,7 @@ t.describe('#_nonce', function() { set.add(entry) }) assert.strictEqual(set.size, flaska._nonces.length) - + let ctx = createCtx() assert.notOk(ctx.state.nonce) @@ -148,12 +148,12 @@ t.describe('#_nonce', function() { 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.strictEqual(ctx.headers['Server'], 'Flaska') assert.strictEqual(ctx.headers['X-Content-Type-Options'], 'nosniff') assert.strictEqual(ctx.headers['Content-Security-Policy'], `default-src 'self'; style-src 'self' 'unsafe-inline' 'nonce-${ctx.state.nonce}'; img-src * data: blob:; font-src 'self' data:; object-src 'none'; frame-ancestors 'none'; script-src 'nonce-${ctx.state.nonce}'`) @@ -162,7 +162,7 @@ t.describe('#_nonce', function() { assert.strictEqual(ctx.headers['Cross-Origin-Embedder-Policy'], 'require-corp') assert.ok(new Date(ctx.headers['Date']).getDate()) }) - + t.test('should always return nonce values even if it runs out in cache', function() { let flaska = new Flaska({ nonce: ['script-src'], @@ -178,9 +178,9 @@ t.describe('#_nonce', function() { } assert.notOk(flaska._nonces[flaska._noncesIndex]) - + flaska._before[0](ctx) - + assert.notOk(flaska._nonces[flaska._noncesIndex]) assert.ok(ctx.state.nonce) @@ -206,13 +206,13 @@ t.describe('#_nonce', function() { 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]) @@ -227,7 +227,7 @@ t.describe('#_nonce', function() { 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]) @@ -249,12 +249,12 @@ t.describe('#_nonce', function() { 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) @@ -271,19 +271,19 @@ t.describe('#_nonce', function() { 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]) @@ -299,7 +299,7 @@ t.describe('#log', function() { assert.strictEqual(typeof(flaska.log.info), 'function') assert.strictEqual(typeof(flaska.log.warn), 'function') }) - + t.test('allow overwriting in options', function() { const assertFunction = function() { return 1 } let flaska = new Flaska({ log: { @@ -323,7 +323,7 @@ specialHandlers.forEach(function(type) { let flaska = new Flaska({}, faker) assert.strictEqual(typeof(flaska[type]), 'function') }) - + t.test('validate handler', function() { let flaska = new Flaska({}, faker) assert.throws(function() { flaska[type]() }, /[Ff]unction/) @@ -424,7 +424,7 @@ t.describe('_onerror', function() { const assertStatus = 431 const assertBody = { a: 1 } const assertError = new HttpError(assertStatus, 'should not be seen', assertBody) - + let ctx = createCtx() let flaska = new Flaska({}, faker) flaska._onerror(assertError, ctx) @@ -437,7 +437,7 @@ t.describe('_onerror', function() { t.test('default valid handling of HttpError with no body', function() { const assertStatus = 413 const assertError = new HttpError(assertStatus, 'should not be seen') - + let ctx = createCtx() let flaska = new Flaska({}, faker) flaska._onerror(assertError, ctx) @@ -453,7 +453,7 @@ t.describe('_onerror', function() { t.test('default valid handling of HttpError with missing status message', function() { const assertStatus = 432 const assertError = new HttpError(assertStatus, 'should not be seen') - + let ctx = createCtx() let flaska = new Flaska({}, faker) flaska._onerror(assertError, ctx) @@ -531,7 +531,7 @@ t.describe('#devMode()', function() { const assertStatus = 431 const assertBody = { a: 1 } const assertError = new HttpError(assertStatus, 'should not be seen', assertBody) - + let ctx = createCtx() let flaska = new Flaska({}, faker) flaska.devMode() @@ -546,7 +546,7 @@ t.describe('#devMode()', function() { const assertStatus = 413 const assertErrorMessage = 'A day' const assertError = new HttpError(assertStatus, assertErrorMessage) - + let ctx = createCtx() let flaska = new Flaska({}, faker) flaska.devMode() @@ -563,7 +563,7 @@ t.describe('#devMode()', function() { const assertStatus = 432 const assertErrorMessage = 'Jet Coaster Ride' const assertError = new HttpError(assertStatus, assertErrorMessage) - + let ctx = createCtx() let flaska = new Flaska({}, faker) flaska.devMode() @@ -735,16 +735,16 @@ t.describe('#compile()', function() { }) }) -t.describe('#handleMiddleware()', function() { +t.describe('#runHandlers()', function() { t.test('should work with empty array', function() { let flaska = new Flaska({}, faker) - flaska.handleMiddleware({}, [], 0) + flaska.runHandlers({}, [], 0) }) t.test('should work with correct index', function() { let checkIsTrue = false let flaska = new Flaska({}, faker) - flaska.handleMiddleware({}, [ + flaska.runHandlers({}, [ function() { throw new Error('should not be thrown') }, function() { throw new Error('should not be thrown') }, function() { throw new Error('should not be thrown') }, @@ -757,7 +757,7 @@ t.describe('#handleMiddleware()', function() { const assertCtx = createCtx({ a: 1 }) let checkCounter = 0 let flaska = new Flaska({}, faker) - flaska.handleMiddleware(assertCtx, [ + flaska.runHandlers(assertCtx, [ function(ctx) { assert.strictEqual(ctx, assertCtx); checkCounter++ }, function(ctx) { assert.strictEqual(ctx, assertCtx); checkCounter++ }, function(ctx) { assert.strictEqual(ctx, assertCtx); checkCounter++ }, @@ -771,7 +771,7 @@ t.describe('#handleMiddleware()', function() { const assertCtx = createCtx({ a: 1 }) let checkCounter = 0 let flaska = new Flaska({}, faker) - let result = flaska.handleMiddleware(assertCtx, [ + let result = flaska.runHandlers(assertCtx, [ function(ctx) { assert.strictEqual(ctx, assertCtx); assert.strictEqual(checkCounter, 0); checkCounter++ }, function(ctx) { assert.strictEqual(ctx, assertCtx); assert.strictEqual(checkCounter, 1); checkCounter++ }, function(ctx) { return new Promise(function(res) { assert.strictEqual(ctx, assertCtx); assert.strictEqual(checkCounter, 2); checkCounter++; res() }) }, @@ -793,19 +793,19 @@ t.describe('#handleMiddleware()', function() { const assertError = { a: 1 } let checkCounter = 0 let flaska = new Flaska({}, faker) - let err = await assert.isRejected(flaska.handleMiddleware({}, [ + let err = await assert.isRejected(flaska.runHandlers({}, [ function() { }, function() { return new Promise(function(res, rej) { rej(assertError) }) }, function() { throw new Error('should not be seen') }, ], 0)) assert.strictEqual(err, assertError) - err = await assert.isRejected(flaska.handleMiddleware({}, [ + err = await assert.isRejected(flaska.runHandlers({}, [ function() { }, function() { return Promise.reject(assertError) }, function() { throw new Error('should not be seen') }, ], 0)) assert.strictEqual(err, assertError) - err = await assert.isRejected(flaska.handleMiddleware({}, [ + err = await assert.isRejected(flaska.runHandlers({}, [ function() { }, function() { return Promise.resolve() }, function() { throw assertError }, @@ -977,7 +977,7 @@ t.describe('#listenAsync()', function() { assert.strictEqual(checkPort, assertPort) assert.strictEqual(checkIp, '::') }) - + t.test('call http and listenAsync correctly if supported', async function() { const assertPort = 4632 const assertIp = 'asdf' @@ -1002,7 +1002,7 @@ t.describe('#listenAsync()', function() { assert.strictEqual(stubListenAsync.firstCall[0], assertPort) assert.strictEqual(stubListenAsync.firstCall[1], assertIp) }) - + t.test('call http and listenAsync correctly if supported and ip is null', async function() { const assertPort = 325897235 const assertReturns = { a: 1 } diff --git a/test/flaska.in.test.mjs b/test/flaska.in.test.mjs index 76d47f7..6cbc66b 100644 --- a/test/flaska.in.test.mjs +++ b/test/flaska.in.test.mjs @@ -30,7 +30,7 @@ t.describe('#requestStart()', function() { assert.strictEqual(onResError.callCount, 1) assert.strictEqual(onResError.firstCall[0], assertErrorTwo) assert.strictEqual(onResError.firstCall[1], ctx) - + assert.strictEqual(assertRes.on.secondCall[0], 'finish') assert.strictEqual(typeof(assertRes.on.secondCall[1]), 'function') assert.strictEqual(onEnded.callCount, 0) @@ -220,7 +220,7 @@ t.describe('#requestStart()', function() { }), createRes()) }) - t.test('calls handleMiddleware correctly', function(cb) { + t.test('calls handlers correctly', function(cb) { const assertError = new Error('test') const assertMiddles = [1, 2] const assertParams = { a: 1, b: 2 } @@ -231,12 +231,11 @@ t.describe('#requestStart()', function() { flaska.routers.GET.match = function() { return { - handler: function() {}, - middlewares: assertMiddles, + path: { handlers: assertMiddles, }, params: assertParams, } } - flaska.handleMiddleware = function(ctx, middles, index) { + flaska.runHandlers = function(ctx, middles, index) { assert.strictEqual(index, 0) assert.strictEqual(middles, assertMiddles) checkMiddleCtx = ctx @@ -265,7 +264,7 @@ t.describe('#requestStart()', function() { checkHandlerCtx = ctx throw assertError } - + let flaska = new Flaska({}, faker) flaska.get('/:id', handler) flaska.compile() @@ -289,7 +288,7 @@ t.describe('#requestStart()', function() { let handler = function() { throw new Error('should not be called') } - + let flaska = new Flaska({}, faker) flaska.on404(on404Error) flaska.get('/test', function() { throw new Error('should not be called') }) @@ -307,7 +306,7 @@ t.describe('#requestStart()', function() { method: 'GET', }), createRes()) }) - + t.test('calls 404 if route handler is not found and supports promise', function(cb) { let checkCtx = null const assertError = new Error('should be seen') @@ -315,7 +314,7 @@ t.describe('#requestStart()', function() { checkCtx = ctx return Promise.resolve().then(function() { return Promise.reject(assertError) }) } - + let flaska = new Flaska({}, faker) flaska.on404(on404Error) flaska.get('/test', function() { throw new Error('should not be called') }) @@ -336,7 +335,7 @@ t.describe('#requestStart()', function() { t.test(`should handle unexpected errors in on404 correctly`, function(cb) { const assertError = new Error('should be seen') let checkCtx = null - + let flaska = new Flaska({}, faker) flaska.on404(function(ctx) { checkCtx = ctx @@ -360,7 +359,7 @@ t.describe('#requestStart()', function() { t.test(`should handle unexpected errors in middleware correctly`, function(cb) { const assertError = new Error('should be seen') let checkCtx = null - + let flaska = new Flaska({}, faker) let middles = [function(ctx) { checkCtx = ctx @@ -387,13 +386,10 @@ t.describe('#requestStart()', function() { assert.strictEqual(ctx.params.path, 'test/something/here') ctx.body = assertBody } - + let flaska = new Flaska({}, faker) flaska.get('/::path', handler) flaska.compile() - flaska.handleMiddleware = function() { - throw new Error('should not be called') - } flaska.requestEnd = cb.finish(function(err, ctx) { assert.notOk(err) @@ -407,7 +403,7 @@ t.describe('#requestStart()', function() { }), createRes()) }) - t.test('calls handleMiddleware correctly if is promise', function(cb) { + t.test('calls runHandlers correctly if is promise', function(cb) { const assertError = new Error('test') const assertMiddles = [1] @@ -416,11 +412,11 @@ t.describe('#requestStart()', function() { flaska.routers.GET.match = function() { return { - handler: function() {}, + path: { handlers: function() {}, }, middlewares: assertMiddles, } } - flaska.handleMiddleware = function() { + flaska.runHandlers = function() { return Promise.resolve().then(function() { return Promise.reject(assertError) }) } @@ -444,7 +440,7 @@ t.describe('#requestStart()', function() { checkHandlerCtx = ctx throw assertError } - + let flaska = new Flaska({}, faker) flaska.get('/:id', [function() { return Promise.resolve() }], handler) flaska.compile() @@ -470,7 +466,7 @@ t.describe('#requestStart()', function() { checkHandlerCtx = ctx return Promise.resolve().then(function() { return Promise.reject(assertError) }) } - + let flaska = new Flaska({}, faker) flaska.get('/:id', [function() { return Promise.resolve() }], handler) flaska.compile() @@ -504,7 +500,7 @@ t.describe('#requestStart()', function() { ctx.body = assertBody }) } - + let flaska = new Flaska({}, faker) flaska.get('/::path', [middle], handler) flaska.compile() @@ -521,7 +517,7 @@ t.describe('#requestStart()', function() { method: 'GET', }), createRes()) }) - + t.test('calls route handler correctly if promise', function(cb) { const assertError = new Error('test') let checkHandlerCtx = null @@ -532,7 +528,7 @@ t.describe('#requestStart()', function() { return Promise.reject(assertError) }) } - + let flaska = new Flaska({}, faker) flaska.get('/:id', handler) flaska.compile() @@ -558,15 +554,11 @@ t.describe('#requestStart()', function() { ctx.body = assertBody }) } - + let flaska = new Flaska({}, faker) flaska.get('/::path', [], handler) flaska.compile() - flaska.handleMiddleware = function() { - throw new Error('should not be called') - } - flaska.requestEnd = cb.finish(function(err, ctx) { assert.notOk(err) assert.ok(ctx) diff --git a/test/http.test.mjs b/test/http.test.mjs index ec59e1c..8bed562 100644 --- a/test/http.test.mjs +++ b/test/http.test.mjs @@ -7,7 +7,7 @@ import { setTimeout } from 'timers/promises' import { Flaska, FormidableHandler, FileResponse } from '../flaska.mjs' import Client from './client.mjs' -const port = 51024 +const port = 51025 const log = { fatal: stub(), error: stub(), @@ -111,7 +111,7 @@ t.describe('/file', function() { assert.ok(target.closed) assert.ok(file.closed) - + let [statSource, statTarget] = await Promise.all([ fs.stat('./test/test.png'), fs.stat('./test_tmp.png'), @@ -136,11 +136,11 @@ t.describe('/file', function() { assert.notOk(file.closed) req.destroy() - + while (!file.closed) { await setTimeout(10) } - + assert.strictEqual(log.error.callCount, 0) assert.strictEqual(log.info.callCount, 0) @@ -195,7 +195,7 @@ t.describe('/filehandle', function() { assert.strictEqual(res.data, 'llo ') assert.strictEqual(res.headers['content-length'], '4') - + res = await client.customRequest('GET', '/filehandle', null, { getRaw: true, agent: agent, headers: { 'Range': 'bytes=0-0' @@ -311,15 +311,15 @@ t.describe('HEAD', function() { t.describe('/file', function() { t.test('server return HEAD for pipes', async function() { log.error.reset() - + let res = await client.customRequest('HEAD', '/file', null, { getRaw: true, agent: agent }) - + while (!file.closed) { await setTimeout(10) } assert.ok(file.closed) - + let statSource = await fs.stat('./test/test.png') assert.strictEqual(res.data, '') @@ -328,37 +328,37 @@ t.describe('HEAD', function() { }) t.test('server should autoclose body file handles on errors', async function() { reset() - + file = null - + let req = await client.customRequest('HEAD', '/file/leak', null, { returnRequest: true }) - + req.end() - + while (!file) { await setTimeout(10) } - + assert.ok(file) assert.notOk(file.closed) - + req.destroy() - + while (!file.closed) { await setTimeout(10) } - + assert.strictEqual(log.error.callCount, 0) assert.strictEqual(log.info.callCount, 0) - + assert.ok(file.closed) }) }) - + t.describe('/filehandle', function() { t.test('server should send correctly', async function() { log.error.reset() - + let res = await client.customRequest('HEAD', '/filehandle', null, { getRaw: true, agent: agent }) assert.strictEqual(res.status, 200) assert.strictEqual(res.headers['content-length'], '11') @@ -369,7 +369,7 @@ t.describe('HEAD', function() { let etag = res.headers['etag'] assert.ok(d.getTime()) assert.strictEqual(res.data, '') - + res = await client.customRequest('HEAD', '/filehandle', null, { getRaw: true, agent: agent, headers: { 'If-Modified-Since': d.toUTCString() @@ -378,7 +378,7 @@ t.describe('HEAD', function() { assert.strictEqual(res.status, 304) assert.strictEqual(res.data, '') assert.strictEqual(res.headers['etag'], etag) - + res = await client.customRequest('HEAD', '/filehandle', null, { getRaw: true, agent: agent, headers: { 'If-None-Match': etag @@ -387,7 +387,7 @@ t.describe('HEAD', function() { assert.strictEqual(res.status, 304) assert.strictEqual(res.data, '') assert.strictEqual(res.headers['etag'], etag) - + res = await client.customRequest('HEAD', '/filehandle', null, { getRaw: true, agent: agent, headers: { 'Range': 'bytes=2-5' @@ -396,8 +396,8 @@ t.describe('HEAD', function() { assert.strictEqual(res.status, 206) assert.strictEqual(res.data, '') assert.strictEqual(res.headers['content-length'], '4') - - + + res = await client.customRequest('HEAD', '/filehandle', null, { getRaw: true, agent: agent, headers: { 'Range': 'bytes=0-0' @@ -407,7 +407,7 @@ t.describe('HEAD', function() { assert.strictEqual(res.data, '') assert.strictEqual(res.headers['content-length'], '1') }) - + t.after(function() { agent.destroy() }) diff --git a/test/router.test.mjs b/test/router.test.mjs index a4ea4d0..83597d4 100644 --- a/test/router.test.mjs +++ b/test/router.test.mjs @@ -9,7 +9,7 @@ t.describe('#addRoute()', function() { assert.throws(function() { router.addRoute(':test') }, /forward slash/) assert.throws(function() { router.addRoute('test/test2') }, /forward slash/) }) - + t.test('fail if missing handler', function() { let router = new FlaskaRouter() assert.throws(function() { router.addRoute('/') }, /handler/) @@ -45,8 +45,21 @@ t.describe('#addRoute()', function() { router.addRoute('/:test/bla', function() {}) router.addRoute('/bla/bla', function() {}) router.addRoute('/bla/bla/bla', function() {}) - assert.throws(function() { router.addRoute('/:asdf/', function() {}) }, /param/) - assert.throws(function() { router.addRoute('/:test/asdf/:foobar', function() {}) }, /param/) + assert.throws(function() { router.addRoute('/:asdf', function() {}) }, /existing/) + assert.throws(function() { router.addRoute('/:test/asdf/:foobar', function() {}) }, /existing/) + }) + + t.test('fail if adding anything after a fullparam', function() { + let router = new FlaskaRouter() + assert.throws(function() { router.addRoute('/::bla/:bla', function() {}) }, /after/) + assert.throws(function() { router.addRoute('/:test/::bla/test', function() {}) }, /after/) + assert.throws(function() { router.addRoute('/::test/bla', function() {}) }, /after/) + }) + + t.test('should work with param and full param side by side', function() { + let router = new FlaskaRouter() + router.addRoute('/:bla', function() {}) + router.addRoute('/::bla', function() {}) }) t.test('fail if adding multiple fullparam', function() { @@ -56,10 +69,8 @@ t.describe('#addRoute()', function() { router.addRoute('/:test/bla', function() {}) router.addRoute('/:test/::bla', function() {}) router.addRoute('/bla/bla/bla', function() {}) - assert.throws(function() { router.addRoute('/:test/asdf/::bla/fail', function() {}) }, /full.+param/) - assert.throws(function() { router.addRoute('/:test/::bla/test', function() {}) }, /full.+param/) - assert.throws(function() { router.addRoute('/:test/:bla', function() {}) }, /full.+param/) - assert.throws(function() { router.addRoute('/::test', function() {}) }, /partial.+param/) + assert.throws(function() { router.addRoute('/:test/asdf/::bla', function() {}) }, /existing/) + assert.throws(function() { router.addRoute('/:test/::bla', function() {}) }, /existing/) }) t.test('add route correctly', function() { @@ -67,39 +78,7 @@ t.describe('#addRoute()', function() { let router = new FlaskaRouter() router.addRoute('/a/b/c', assertHandler) let result = router.match('/a/b/c') - assert.strictEqual(result.handler, assertHandler) - }) - - t.test('add param route correctly', function() { - let assertHandler = function() { return 1 } - let router = new FlaskaRouter() - router.addRoute('/a/:b/c', assertHandler) - assert.ok(router.root.children.get('a')) - assert.ok(router.root.children.get('a').children.get('__param')) - assert.strictEqual(router.root.children.get('a').children.get('__param').paramName, 'b') - assert.ok(router.root.children.get('a').children.get('__param').children.get('c')) - assert.strictEqual(router.root.children.get('a').children.get('__param').children.get('c').handler, assertHandler) - }) - - t.test('add full param route correctly', function() { - let assertHandler = function() { return 1 } - let router = new FlaskaRouter() - router.addRoute('/a/::b', assertHandler) - assert.ok(router.root.children.get('a')) - assert.ok(router.root.children.get('a').children.get('__fullparam')) - assert.strictEqual(router.root.children.get('a').children.get('__fullparam').fullparamName, 'b') - assert.strictEqual(router.root.children.get('a').children.get('__fullparam').handler, assertHandler) - }) - - t.test('add param route correctly', function() { - let assertHandler = function() { return 1 } - let router = new FlaskaRouter() - router.addRoute('/a/:b/c', assertHandler) - assert.ok(router.root.children.get('a')) - assert.ok(router.root.children.get('a').children.get('__param')) - assert.strictEqual(router.root.children.get('a').children.get('__param').paramName, 'b') - assert.ok(router.root.children.get('a').children.get('__param').children.get('c')) - assert.strictEqual(router.root.children.get('a').children.get('__param').children.get('c').handler, assertHandler) + assert.strictEqual(result.path.handlers[0], assertHandler) }) t.test('support single middlewares correctly', function() { @@ -107,10 +86,10 @@ t.describe('#addRoute()', function() { let assertMiddleware = function() { return 1 } let router = new FlaskaRouter() router.addRoute('/a', assertMiddleware, assertHandler) - assert.ok(router.root.children.get('a')) - assert.strictEqual(router.root.children.get('a').handler, assertHandler) - assert.strictEqual(router.root.children.get('a').middlewares.length, 1) - assert.strictEqual(router.root.children.get('a').middlewares[0], assertMiddleware) + let result = router.match('/a') + assert.strictEqual(result.path.handlers.length, 2) + assert.strictEqual(result.path.handlers[0], assertMiddleware) + assert.strictEqual(result.path.handlers[1], assertHandler) }) t.test('support multi middlewares correctly', function() { @@ -119,15 +98,15 @@ t.describe('#addRoute()', function() { let router = new FlaskaRouter() router.addRoute('/a', [assertMiddleware], assertHandler) router.addRoute('/b', [assertMiddleware, assertMiddleware], assertHandler) - assert.ok(router.root.children.get('a')) - assert.strictEqual(router.root.children.get('a').handler, assertHandler) - assert.strictEqual(router.root.children.get('a').middlewares.length, 1) - assert.strictEqual(router.root.children.get('a').middlewares[0], assertMiddleware) - assert.ok(router.root.children.get('b')) - assert.strictEqual(router.root.children.get('b').handler, assertHandler) - assert.strictEqual(router.root.children.get('b').middlewares.length, 2) - assert.strictEqual(router.root.children.get('b').middlewares[0], assertMiddleware) - assert.strictEqual(router.root.children.get('b').middlewares[1], assertMiddleware) + let resultA = router.match('/a') + assert.strictEqual(resultA.path.handlers.length, 2) + assert.strictEqual(resultA.path.handlers[0], assertMiddleware) + assert.strictEqual(resultA.path.handlers[1], assertHandler) + let resultB = router.match('/b') + assert.strictEqual(resultB.path.handlers.length, 3) + assert.strictEqual(resultB.path.handlers[0], assertMiddleware) + assert.strictEqual(resultB.path.handlers[1], assertMiddleware) + assert.strictEqual(resultB.path.handlers[2], assertHandler) }) }) @@ -136,94 +115,47 @@ t.describe('#match()', function() { let assertMatched = false let router = new FlaskaRouter() router.addRoute('/test', function() { assertMatched = true }) - let result = router.match('/test') - assert.ok(result.handler) - assert.ok(result.middlewares) - assert.strictEqual(result.middlewares.length, 0) - result.handler() - assert.strictEqual(assertMatched, true) - - // Test with extra slash at the end - assertMatched = false - result = router.match('/test/') - assert.ok(result.handler) - assert.ok(result.middlewares) - assert.strictEqual(result.middlewares.length, 0) - result.handler() + let result = router.match('/test').path + assert.strictEqual(result.handlers.length, 1) + result.handlers[0]() assert.strictEqual(assertMatched, true) }) - - t.test('return middlewares', function() { + t.test('return middlewares in handlers', function() { let assertMatched = false let assertMiddleware = function() { assertMatched = true } let router = new FlaskaRouter() router.addRoute('/test', assertMiddleware, function() { }) - let result = router.match('/test') - assert.ok(result.handler) - assert.ok(result.middlewares) - assert.strictEqual(result.middlewares.length, 1) - result.middlewares[0]() - assert.strictEqual(assertMatched, true) - - // Test with extra slash at the end - assertMatched = false - result = router.match('/test/') - assert.ok(result.handler) - assert.ok(result.middlewares) - assert.strictEqual(result.middlewares.length, 1) - result.middlewares[0]() + let result = router.match('/test').path + assert.strictEqual(result.handlers.length, 2) + result.handlers[0]() assert.strictEqual(assertMatched, true) }) - + t.test('match variable paths', function() { const assertParameter = 'bla' let assertMatched = false let router = new FlaskaRouter() router.addRoute('/test/:id', function() { assertMatched = true }) let result = router.match('/test/' + assertParameter) - assert.ok(result.handler) - assert.ok(result.middlewares) - assert.strictEqual(result.middlewares.length, 0) - result.handler() - assert.strictEqual(assertMatched, true) - assert.strictEqual(result.params.id, assertParameter) - - // Test with extra slash at the end - assertMatched = false - result = router.match('/test/' + assertParameter + '/') - assert.ok(result.handler) - assert.ok(result.middlewares) - assert.strictEqual(result.middlewares.length, 0) - result.handler() + assert.strictEqual(result.path.handlers.length, 1) + result.path.handlers[0]() assert.strictEqual(assertMatched, true) assert.strictEqual(result.params.id, assertParameter) }) - + t.test('match full path variable paths', function() { const assertParameter = 'bla/bla/bla' let assertMatched = false let router = new FlaskaRouter() router.addRoute('/test/::id', function() { assertMatched = true }) let result = router.match('/test/' + assertParameter) - assert.ok(result.handler) - assert.ok(result.middlewares) - assert.strictEqual(result.middlewares.length, 0) - result.handler() - assert.strictEqual(assertMatched, true) - assert.strictEqual(result.params.id, assertParameter) - - // Test with extra slash at the end - assertMatched = false - result = router.match('/test/' + assertParameter + '/') - assert.ok(result.handler) - assert.ok(result.middlewares) - assert.strictEqual(result.middlewares.length, 0) - result.handler() + assert.strictEqual(result.path.handlers.length, 1) + result.path.handlers[0]() assert.strictEqual(assertMatched, true) assert.strictEqual(result.params.id, assertParameter) }) - + t.test('match full path root path properly', function() { const assertParamFunc = function() { } const assertFullFunc = function() { } @@ -231,15 +163,13 @@ t.describe('#match()', function() { router.addRoute('/test/:bla', assertParamFunc) router.addRoute('/::id', assertFullFunc) let result = router.match('/test/123') - assert.strictEqual(result.handler, assertParamFunc) - assert.ok(result.middlewares) - assert.strictEqual(result.middlewares.length, 0) + assert.strictEqual(result.path.handlers.length, 1) + assert.strictEqual(result.path.handlers[0], assertParamFunc) assert.strictEqual(result.params.bla, '123') result = router.match('/test/123/asdf') - assert.strictEqual(result.handler, assertFullFunc) - assert.ok(result.middlewares) - assert.strictEqual(result.middlewares.length, 0) + assert.strictEqual(result.path.handlers.length, 1) + assert.strictEqual(result.path.handlers[0], assertFullFunc) assert.strictEqual(result.params.id, 'test/123/asdf') assert.notOk(result.params.bla) }) @@ -250,19 +180,8 @@ t.describe('#match()', function() { router.addRoute('/test/:id', function() { assertMatched = false }) router.addRoute('/test/:id/test1', function() { }) let result = router.match('/test/asdf/test1') - assert.ok(result.handler) - assert.ok(result.middlewares) - assert.strictEqual(result.middlewares.length, 0) - result.handler() - assert.strictEqual(assertMatched, true) - assert.strictEqual(result.params.id, 'asdf') - - // Test with extra slash at the end - result = router.match('/test/asdf/test1/') - assert.ok(result.handler) - assert.ok(result.middlewares) - assert.strictEqual(result.middlewares.length, 0) - result.handler() + assert.strictEqual(result.path.handlers.length, 1) + result.path.handlers[0]() assert.strictEqual(assertMatched, true) assert.strictEqual(result.params.id, 'asdf') }) @@ -277,24 +196,25 @@ t.describe('#match()', function() { router.addRoute('/foo/::path', assertFunction) router.addRoute('/::path', assertFailFunction) - assert.strictEqual(router.match('/test/123').handler, assertFunction) - assert.strictEqual(router.match('/test/asdfasdg').handler, assertFunction) - assert.strictEqual(router.match('/test/test/sdafsda').handler, assertFunction) - assert.strictEqual(router.match('/test/test/sdafsda/gdfshe4/43y34/wtaw').handler, assertFunction) - assert.strictEqual(router.match('/foo/123').handler, assertFunction) - assert.strictEqual(router.match('/foo/bar/baz/test').handler, assertFunction) + assert.strictEqual(router.match('/test/123').path.handlers[0], assertFunction) + assert.strictEqual(router.match('/test/asdfasdg').path.handlers[0], assertFunction) + assert.strictEqual(router.match('/test/test/sdafsda').path.handlers[0], assertFunction) + assert.strictEqual(router.match('/test/test/sdafsda/gdfshe4/43y34/wtaw').path.handlers[0], assertFunction) + assert.strictEqual(router.match('/foo/123').path.handlers[0], assertFunction) + assert.strictEqual(router.match('/foo/bar/baz/test').path.handlers[0], assertFunction) assert.ok(router.match('/test/123/yweherher/reher/h34h34/')) - assert.strictEqual(router.match('/test/123/yweherher/reher/h34h34/').handler, assertFailFunction) + assert.strictEqual(router.match('/test/123/yweherher/reher/h34h34/').path.handlers[0], assertFailFunction) assert.ok(router.match('/test/foo/bar')) - assert.strictEqual(router.match('/test/foo/bar').handler, assertFailFunction) + assert.strictEqual(router.match('/test/foo/bar').path.handlers[0], assertFailFunction) assert.ok(router.match('/')) - assert.strictEqual(router.match('/').handler, assertFailFunction) + assert.strictEqual(router.match('/').path.handlers[0], assertFailFunction) assert.ok(router.match('/something/else/goes/here')) - assert.strictEqual(router.match('/something/else/goes/here').handler, assertFailFunction) + assert.strictEqual(router.match('/something/else/goes/here').path.handlers[0], assertFailFunction) router.addRoute('/', assertRootFunction) + router.compile() assert.ok(router.match('/')) - assert.strictEqual(router.match('/').handler, assertRootFunction) + assert.strictEqual(router.match('/').path.handlers[0], assertRootFunction) }) t.test('return null when no match is found', function() { diff --git a/test_old.mjs b/test_old.mjs new file mode 100644 index 0000000..0bfba85 --- /dev/null +++ b/test_old.mjs @@ -0,0 +1,20 @@ +import { Flaska } from './flaska_buffer.mjs' + +const port = 51027 +const flaska = new Flaska({}, ) + +flaska.get('/', function(ctx) { + ctx.body = { status: true } +}) + +flaska.get('/:item/asdf/herp/:derp/bla', function(ctx) { + ctx.body = { item: ctx.params.item } +}) + +flaska.get('/error', function(ctx) { + process.exit(1) +}) + +flaska.listen(port, function() { + console.log('listening on port', port) +})