flaska/flaska.mjs
Jonatan Nilsson 28b4f6ea33
All checks were successful
continuous-integration/appveyor/branch AppVeyor build succeeded
Flaska: Testing and Version 1.0.0 released
Added JsonHandler
Added FormidableHandler
Fixed a bunch of request handling
2022-03-17 15:09:22 +00:00

814 lines
27 KiB
JavaScript

import os from 'os'
import path from 'path'
import http from 'http'
import stream from 'stream'
import fs from 'fs/promises'
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 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(err)
if (opts.parseFields) {
Object.keys(fields).forEach(function(key) {
try {
fields[key] = JSON.parse(fields[key])
} catch { }
})
}
ctx.req.body = fields
ctx.req.file = files?.file || null
if (!ctx.req.file) {
return res()
}
let filename
let target
try {
filename = opts.filename(ctx.req.file) || ctx.req.file.name
target = path.join(opts.uploadDir, filename)
} catch (err) {
return rej(err)
}
rename(ctx.req.file.path, target)
.then(function() {
ctx.req.file.path = target
ctx.req.file.filename = filename
})
.then(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
}
}
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) {
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.info(err)
} else {
ctx.log.error(err)
}
ctx.res.statusCode = ctx.statusCode = 400
ctx.res.end()
}
this._onreserror = function(err, ctx) {
ctx.log.error(err)
}
let options = opts || {}
this.log = options.log || {
fatal: console.error.bind(console),
error: console.error.bind(console),
warn: console.log.bind(console),
info: console.log.bind(console),
debug: console.debug.bind(console),
trace: console.debug.bind(console),
log: console.log.bind(console),
}
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(),
}
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)
})
req.on('close', () => {
ctx.closed = true
this.requestEnded(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(err, ctx) {
if (err) {
try {
this._onerror(err, ctx)
} catch (err) {
this._backuperror(err, ctx)
}
}
if (ctx.res.writableEnded) {
return
}
ctx.res.statusCode = ctx.status
if (statuses.empty[ctx.status]) {
return ctx.res.end()
}
let body = ctx.body
let length = 0
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.res.setHeader('Content-Type', ctx.type || 'application/octet-stream')
return this.stream.pipeline(body, ctx.res, function() { })
}
if (typeof(body) === 'object') {
body = JSON.stringify(body)
length = Buffer.byteLength(body)
ctx.res.setHeader('Content-Type', 'application/json; charset=utf-8')
} else {
body = body.toString()
length = Buffer.byteLength(body)
ctx.res.setHeader('Content-Type', ctx.type || 'text/plain; charset=utf-8')
}
ctx.res.setHeader('Content-Length', length)
ctx.res.end(body)
}
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`])
}
}
}
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.compile()
this.server = this.http.createServer(this.requestStart.bind(this))
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.compile()
this.server = this.http.createServer(this.requestStart.bind(this))
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)
})
})
}
}