flaska/flaska_buffer copy.mjs

1230 lines
42 KiB
JavaScript

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