From ee0326c9f4af5f67196823982e141bb108b72417 Mon Sep 17 00:00:00 2001 From: Jonatan Nilsson Date: Sun, 10 Nov 2024 00:26:27 +0000 Subject: [PATCH] remove all the nonsense benchmark stuff --- flaska_buffer copy.mjs | 1230 --------------------------------------- flaska_buffer.mjs | 1230 --------------------------------------- flaska_old.mjs | 1128 ------------------------------------ flaska_old2.mjs | 1240 ---------------------------------------- router_v2.mjs | 242 -------- temp.mjs | 22 - test.mjs | 26 - test_fast.mjs | 20 - test_old.mjs | 20 - 9 files changed, 5158 deletions(-) delete mode 100644 flaska_buffer copy.mjs delete mode 100644 flaska_buffer.mjs delete mode 100644 flaska_old.mjs delete mode 100644 flaska_old2.mjs delete mode 100644 router_v2.mjs delete mode 100644 temp.mjs delete mode 100644 test.mjs delete mode 100644 test_fast.mjs delete mode 100644 test_old.mjs diff --git a/flaska_buffer copy.mjs b/flaska_buffer copy.mjs deleted file mode 100644 index 574b40f..0000000 --- a/flaska_buffer copy.mjs +++ /dev/null @@ -1,1230 +0,0 @@ -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 = "'use strict'" - output += '\nreturn function flaskaBufferStrClos(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_buffer.mjs b/flaska_buffer.mjs deleted file mode 100644 index 574b40f..0000000 --- a/flaska_buffer.mjs +++ /dev/null @@ -1,1230 +0,0 @@ -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 = "'use strict'" - output += '\nreturn function flaskaBufferStrClos(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 deleted file mode 100644 index db51c89..0000000 --- a/flaska_old.mjs +++ /dev/null @@ -1,1128 +0,0 @@ -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/flaska_old2.mjs b/flaska_old2.mjs deleted file mode 100644 index e769774..0000000 --- a/flaska_old2.mjs +++ /dev/null @@ -1,1240 +0,0 @@ -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' }); - let sealed = Object.seal({}) - - 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: sealed, - }) - } - - // 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, pathParamMapIndex) { - let pathIndex = paths.indexOf(branch.path) - if (pathIndex < 0) { - throw new RouterError(branch.path, null, 'InternalError: Specified path was not found in paths') - } - let mapIndex = pathParamMapIndex.size + 1 - pathParamMapIndex.set(mapIndex, pathIndex) - let output = '\n' + indentString + `return {` - output += '\n' + indentString + ` path: paths_${mapIndex},` - 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 + ` {},` - } - output += '\n' + indentString + `}` - return output - } - - __treeIntoCompiledCodeBranch(paths, branches, indent = 0, params = [], pathParamMapIndex) { - 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 (str.charCodeAt(${this.__getIndex(indent, 0, params)}) === ${branch.char.charCodeAt(0)}) { // ${branch.char}` - - if (branch.path) { - output += '\n' + indentation + ` if (strLength === ${this.__getIndex(indent, 1, params)}) {` - output += this.__treeIntoCompiledCodeReturnPath(indentation + ' ', paths, branch, params, pathParamMapIndex) - 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 ? '' : `, 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, pathParamMapIndex) - } else if (branch.path) { - output += '\n' + indentation + `if (strLength === ${this.__getIndex(indent, 0, params)}) {` - output += this.__treeIntoCompiledCodeReturnPath(indentation + ' ', paths, branch, params, pathParamMapIndex) - 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(), pathParamMapIndex) - } - if (addEndBracket) { - output += '\n' + indentation + '} ' - } - } - return output - } - - __treeIntoCompiledCodeClosure(paths, tree, staticList) { - let pathParamMapIndex = new Map() - let output = '' - let prefix = '"use strict"' - 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 let strLength = str.length' - output += '\n ' + this.__treeIntoCompiledCodeBranch(paths, tree, 1, [], pathParamMapIndex) - - } - output += '\n return null' - output += '\n}' - pathParamMapIndex.forEach(function (val, key) { - prefix += `let paths_${key} = paths[${val}]\n` - }) - output = prefix + 'return function flaskaFastRouter(str) {' + 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) - }) - this.server.closeAllConnections() - }) - } -} diff --git a/router_v2.mjs b/router_v2.mjs deleted file mode 100644 index b292de2..0000000 --- a/router_v2.mjs +++ /dev/null @@ -1,242 +0,0 @@ - -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 = [] -} - -function 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(buildChild(x, i + 1, consume)) - consume.splice(0, letter.children[letter.children.length - 1].count) - } - return letter -} - -function buildTree(splitPaths) { - let builder = [] - while (splitPaths.length) { - builder.push(buildChild(0, 0, splitPaths)) - splitPaths.splice(0, builder[builder.length - 1].count) - } - return builder -} - -const regParamPrefix = /^::?/ -const regCleanNonAschii = /(?![a-zA-Z_])./g -const regCleanRest = /_+/g - -function 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, - } -} - -const SlashCode = '/'.charCodeAt(0) - -function getIndex(offset, additions, params) { - return (offset + additions) - + (params.length - ? ' + ' + params.map(a => `offset${a[1]}`).join(' + ') - : '') -} - -function 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 -} - -const spaces = ' ' - -function 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[${getIndex(indent, 0, params)}] === ${branch.char.charCodeAt(0)}) { // ${branch.char}` - - if (branch.path) { - output += '\n' + indentation + ` if (buf.length === ${getIndex(indent, 1, params)}) {` - output += treeIntoCompiledCodeReturnPath(indentation + ' ', paths, branch, params) - output += '\n' + indentation + ` }` - } - } else { - addEndBracket = false - let paramVarName = (params.length + 1) + '_' + branch.paramVarName - output += `let s${paramVarName} = str.slice(${getIndex(indent, 0, params)}${branch.isFullParams ? '' : `, buf.indexOf(${SlashCode}, ${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 += treeIntoCompiledCodeReturnPath(indentation, paths, branch, params) - } else if (branch.path) { - output += '\n' + indentation + `if (buf.length === ${getIndex(indent, 0, params)}) {` - output += treeIntoCompiledCodeReturnPath(indentation + ' ', paths, branch, params) - output += '\n' + indentation + `}` - } - } - - if (branch.children.length) { - if (branch.path) { - output += ' else ' - } else { - output += '\n' + indentation + ' ' - } - output += treeIntoCompiledCodeBranch(paths, branch.children, indent + 1, params.slice()) - } - if (addEndBracket) { - output += '\n' + indentation + '} ' - } - } - return output -} - -function treeIntoCompiledCodeClosure(paths, tree, staticList) { - let output = 'return function RBufferStrSliceClosure(str) {' - output += '\n let checkStatic = staticList.get(str)' - output += '\n if(checkStatic) {' - output += '\n return checkStatic' - output += '\n }' - output += '\n var buf = Buffer.from(str)' - output += '\n ' + treeIntoCompiledCodeBranch(paths, tree, 1, []) - output += '\n return null' - output += '\n}' - return new Function('paths', 'staticList', output)(paths, staticList) -} - -export function compilePaths(paths) { - let splitPaths = splitAndSortPaths(paths) - let tree = buildTree(splitPaths.paramsPaths.slice()) - return treeIntoCompiledCodeClosure(paths, tree, splitPaths.staticPaths) -} diff --git a/temp.mjs b/temp.mjs deleted file mode 100644 index 844641a..0000000 --- a/temp.mjs +++ /dev/null @@ -1,22 +0,0 @@ -function test1() { - delete Object.prototype -} -var test2 = new Function(` - delete Object.prototype -`) -var test3 = new Function(` - 'use strict' - delete Object.prototype -`) - -var test4 = new Function(` - 'use strict' - return function() { - delete Object.prototype - } -`) - -try { test1() } catch (err) { console.log('test1', err.message) } -try { test2() } catch (err) { console.log('test2', err.message) } -try { test3() } catch (err) { console.log('test3', err.message) } -try { test4()() } catch (err) { console.log('test4', err.message) } diff --git a/test.mjs b/test.mjs deleted file mode 100644 index 8fc5ba2..0000000 --- a/test.mjs +++ /dev/null @@ -1,26 +0,0 @@ -import { Flaska } from './flaska.mjs' - -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) -}) - -flaska.listen(port, function() { - console.log('listening on port', port) -}) diff --git a/test_fast.mjs b/test_fast.mjs deleted file mode 100644 index 428afb6..0000000 --- a/test_fast.mjs +++ /dev/null @@ -1,20 +0,0 @@ -import { Flaska } from './flaska_fast.mjs' - -const port = 51028 -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) -}) diff --git a/test_old.mjs b/test_old.mjs deleted file mode 100644 index 0bfba85..0000000 --- a/test_old.mjs +++ /dev/null @@ -1,20 +0,0 @@ -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) -})