2022-03-17 15:09:22 +00:00
import os from 'os'
2022-03-24 09:29:54 +00:00
import crypto from 'crypto'
2022-03-17 15:09:22 +00:00
import path from 'path'
2021-10-09 00:12:56 +00:00
import http from 'http'
import stream from 'stream'
2022-03-17 15:09:22 +00:00
import fs from 'fs/promises'
2022-03-26 15:50:18 +00:00
import fsSync from 'fs'
2022-03-17 15:09:22 +00:00
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 ( ' G6osAJwFdhv1SrFZSJz4SQJuhCSzWMqRl / mTge / er7L + ds / rnr9Ub3M7IqCSiNJgWSapw9Lny + zVadeap7R13N82CgFnYjgIkgm2 / 2 VqmTYwWDc4sg67Pj7jIle5rE1SBdH2e7 + nNdMDotgDEIXBACphiDVYPyB5xgHkVQm7ciQjGR8p96lz4V2kJFSQKMmlc4gBmN / 0 zrdU7 / tjWD0Q2mmt9Tb8xGpRkQRvnbvDSWwSfx89zvTN0i05wS9K7Ob + lplWCNdk9vaWp07SuLGKPwQxo + RdJ / ItamTGttQLQt5dU6ZrQl1FDHxzE5GM + r7HEsC / lZKoKyG3kg9GLt9ojBgVecsLQ0F5Byb5 / J5WeMdqkg3h2jw14aRKFpufk1G2jwad7Nx5Tah / zZ0cnNrtbSeFwG5LNlSLL7fKBensC1w / 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
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 ( )
2021-10-09 00:12:56 +00:00
2020-03-11 12:34:34 +00:00
/ * *
* Router
* /
2021-07-05 17:54:00 +00:00
class Branch {
constructor ( ) {
this . children = new Map ( )
this . paramName = null
this . fullparamName = null
this . handler = null
this . middlewares = [ ]
}
2020-03-11 12:34:34 +00:00
}
2021-10-09 00:12:56 +00:00
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
}
}
2020-03-11 12:34:34 +00:00
const _ _paramMapName = '__param'
2021-07-05 17:54:00 +00:00
const _ _fullParamMapName = '__fullparam'
2021-10-09 00:12:56 +00:00
function assertIsHandler ( handler , name ) {
if ( typeof ( handler ) !== 'function' ) {
throw new Error ( ` ${ name } was called with a handler that was not a function ` )
}
}
2021-10-10 20:38:12 +00:00
export function QueryHandler ( ) {
return function ( ctx ) {
ctx . query = ( new URL ( ctx . req . url , 'http://localhost' ) ) . searchParams
}
}
2022-03-17 15:09:22 +00:00
export function JsonHandler ( org = { } ) {
let opts = {
sizeLimit : org . sizeLimit || ( 10 * 1024 )
}
return function ( ctx ) {
2022-03-16 10:41:25 +00:00
const buffers = [ ] ;
2022-03-17 15:09:22 +00:00
let size = 0
2022-03-16 10:41:25 +00:00
2022-03-17 15:09:22 +00:00
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 ,
} ) )
}
} )
}
}
2022-03-24 09:29:54 +00:00
export function CorsHandler ( opts = { } ) {
const options = {
allowedMethods : opts . allowedMethods || 'GET,HEAD,PUT,POST,DELETE,PATCH' ,
2022-03-24 15:50:31 +00:00
allowedOrigins : opts . allowedOrigins || [ ] ,
2022-03-24 09:29:54 +00:00
allowedHeaders : opts . allowedHeaders ,
2022-03-24 15:50:31 +00:00
credentials : opts . credentials || false ,
exposeHeaders : opts . exposeHeaders || '' ,
maxAge : opts . maxAge || '' ,
2022-03-24 09:29:54 +00:00
}
return function ( ctx ) {
2022-03-24 15:50:31 +00:00
// 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
2022-03-24 09:29:54 +00:00
let origin = ctx . req . headers [ 'origin' ]
2022-03-24 15:50:31 +00:00
if ( ! origin || ! 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
}
}
2022-03-24 09:29:54 +00:00
ctx . headers [ 'Access-Control-Allow-Origin' ] = origin
2022-03-24 15:50:31 +00:00
if ( options . credentials ) {
ctx . headers [ 'Access-Control-Allow-Credentials' ] = 'true'
2022-03-24 09:29:54 +00:00
}
}
}
2022-03-17 15:09:22 +00:00
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
2022-03-16 10:41:25 +00:00
2022-03-17 15:09:22 +00:00
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 )
2022-03-16 10:41:25 +00:00
2022-03-17 15:09:22 +00:00
} )
} )
2022-03-16 10:41:25 +00:00
}
}
export class HttpError extends Error {
constructor ( statusCode , message , body = null ) {
2022-03-25 13:26:13 +00:00
super ( message ) ;
2022-03-16 10:41:25 +00:00
2022-03-25 13:26:13 +00:00
Error . captureStackTrace ( this , HttpError ) ;
2022-03-16 10:41:25 +00:00
2022-03-25 13:26:13 +00:00
let proto = Object . getPrototypeOf ( this ) ;
proto . name = 'HttpError' ;
2022-03-16 10:41:25 +00:00
2022-03-25 13:26:13 +00:00
this . status = statusCode
this . body = body
}
}
2022-03-26 15:59:48 +00:00
const RangeRegexTester = /bytes=(\d+)-(\d+)?/
2022-03-25 13:26:13 +00:00
export class FileResponse {
constructor ( filepath , stat ) {
this . filepath = filepath
this . stat = stat
2022-03-16 10:41:25 +00:00
}
2022-03-26 15:50:18 +00:00
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' ] ) {
2022-03-26 15:59:48 +00:00
let match = RangeRegexTester . exec ( ctx . req . headers [ 'range' ] )
2022-03-26 15:50:18 +00:00
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
}
}
}
2022-03-26 15:59:48 +00:00
2022-03-26 15:50:18 +00:00
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
2022-03-27 00:30:56 +00:00
if ( ctx . method !== 'HEAD' ) {
let stream = useFs . createReadStream ( this . filepath , readOptions )
return stream
}
return null
2022-03-26 15:50:18 +00:00
}
2022-03-16 10:41:25 +00:00
}
2021-07-05 17:54:00 +00:00
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 ]
}
2021-10-09 00:12:56 +00:00
assertIsHandler ( handler , 'addRoute()' )
2021-07-05 17:54:00 +00:00
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 ++
}
}
}
2020-03-11 12:34:34 +00:00
2021-07-05 17:54:00 +00:00
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
2021-10-09 00:12:56 +00:00
}
else if ( output = branch . children . get ( _ _fullParamMapName ) ) {
params [ output . fullparamName ] = url . slice ( start )
return {
handler : output . handler ,
middlewares : output . middlewares ,
params : params ,
}
2021-07-05 17:54:00 +00:00
} else {
2021-10-09 00:12:56 +00:00
if ( output = this . root . children . get ( _ _fullParamMapName ) ) {
params = {
[ output . fullparamName ] : url . slice ( 1 )
}
return {
handler : output . handler ,
middlewares : output . middlewares ,
params : params ,
}
}
2021-07-05 17:54:00 +00:00
return null
}
i ++
end = start = i
char = url [ i ]
}
2021-10-09 00:12:56 +00:00
// 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 ) {
2021-07-05 17:54:00 +00:00
return {
handler : branch . handler ,
middlewares : branch . middlewares ,
params : params ,
}
}
if ( char === '/' ) {
end = start = i + 1
} else {
end ++
}
}
2021-10-09 00:12:56 +00:00
if ( output = this . root . children . get ( _ _fullParamMapName ) ) {
params = {
[ output . fullparamName ] : url . slice ( 1 )
}
return {
handler : output . handler ,
middlewares : output . middlewares ,
params : params ,
}
}
2021-07-05 17:54:00 +00:00
return null
}
}
2021-10-09 00:12:56 +00:00
/ * *
* Flaska
* /
export class Flaska {
2022-03-24 09:29:54 +00:00
constructor ( opts = { } , orgHttp = http , orgStream = stream ) {
2021-10-09 00:12:56 +00:00
this . _before = [ ]
this . _beforeCompiled = null
this . _beforeAsync = [ ]
this . _beforeAsyncCompiled = null
this . _after = [ ]
this . _afterCompiled = null
this . _afterAsync = [ ]
this . _afterAsyncCompiled = null
this . _on404 = function ( ctx ) {
2022-03-24 09:29:54 +00:00
if ( ctx . body == null && ctx . status !== 204 ) {
ctx . status = 404
ctx . body = {
status : 404 ,
message : statuses [ 404 ] ,
}
2021-10-09 00:12:56 +00:00
}
}
this . _backuperror = this . _onerror = function ( err , ctx ) {
ctx . log . error ( err )
2022-03-17 15:09:22 +00:00
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 ] ,
}
2021-10-09 00:12:56 +00:00
}
}
this . _onreqerror = function ( err , ctx ) {
2022-04-02 19:40:46 +00:00
if ( err . message !== 'aborted' ) {
2022-03-17 15:09:22 +00:00
ctx . log . error ( err )
2022-04-02 19:40:46 +00:00
ctx . res . statusCode = ctx . statusCode = 400
2022-03-17 15:09:22 +00:00
}
2021-10-09 00:12:56 +00:00
ctx . res . end ( )
}
this . _onreserror = function ( err , ctx ) {
ctx . log . error ( err )
}
2020-03-11 12:34:34 +00:00
2022-03-24 09:29:54 +00:00
let options = {
defaultHeaders : opts . defaultHeaders || {
'Server' : 'Flaska' ,
'X-Content-Type-Options' : 'nosniff' ,
2022-05-12 16:40:14 +00:00
'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' ` ,
2022-03-24 09:29:54 +00:00
'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
}
2021-07-05 17:54:00 +00:00
2022-06-16 09:58:11 +00:00
if ( opts . appendHeaders ) {
let appendKeys = Object . keys ( opts . appendHeaders )
for ( let key of appendKeys ) {
options . defaultHeaders [ key ] = opts . appendHeaders [ key ]
}
}
2022-03-24 09:29:54 +00:00
if ( ! options . defaultHeaders && options . nonce . length ) {
// throw error
2022-03-17 15:09:22 +00:00
}
2022-03-24 09:29:54 +00:00
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 = {'
2022-03-26 15:50:18 +00:00
constructFunction += ` 'Date': new Date().toUTCString(), `
2022-03-24 09:29:54 +00:00
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
2021-10-09 00:12:56 +00:00
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 ( ) ,
}
2022-03-21 07:34:01 +00:00
// HEAD and GET should be identical
this . routers [ 'HEAD' ] = this . routers [ 'GET' ]
2021-10-09 00:12:56 +00:00
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 )
2021-07-05 17:54:00 +00:00
}
2021-10-09 00:12:56 +00:00
_assertIsHandler ( handler , name ) {
if ( typeof ( handler ) !== 'function' ) {
throw new Error ( ` ${ name } was called with a handler that was not a function ` )
2021-07-05 17:54:00 +00:00
}
}
2020-03-11 12:34:34 +00:00
2021-10-09 00:12:56 +00:00
devMode ( ) {
this . _backuperror = this . _onerror = function ( err , ctx ) {
ctx . log . error ( err )
2022-03-17 15:09:22 +00:00
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 || '' ,
}
2020-03-11 12:34:34 +00:00
}
}
2021-10-09 00:12:56 +00:00
}
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 = {
2021-10-10 19:57:48 +00:00
log : this . log ,
2021-10-09 00:12:56 +00:00
req : req ,
res : res ,
method : req . method ,
url : url ,
search : search ,
state : { } ,
status : 200 ,
2021-10-10 20:38:12 +00:00
query : new Map ( ) ,
2021-10-09 00:12:56 +00:00
body : null ,
type : null ,
length : null ,
2020-03-11 12:34:34 +00:00
}
2021-10-09 00:12:56 +00:00
req . on ( 'error' , ( err ) => {
2022-03-17 15:09:22 +00:00
if ( err . message === 'aborted' ) {
ctx . aborted = true
}
2021-10-09 00:12:56 +00:00
this . _onreqerror ( err , ctx )
2021-10-11 03:16:40 +00:00
this . requestEnded ( ctx )
2021-10-09 00:12:56 +00:00
} )
res . on ( 'error' , ( err ) => {
this . _onreserror ( err , ctx )
} )
2021-10-11 02:06:22 +00:00
res . on ( 'finish' , ( ) => {
this . requestEnded ( ctx )
} )
2021-10-09 00:12:56 +00:00
try {
this . _beforeCompiled ( ctx )
if ( this . _beforeAsyncCompiled ) {
return this . _beforeAsyncCompiled ( ctx )
. then ( ( ) => {
this . requestStartInternal ( ctx )
} ) . catch ( err => {
this . requestEnd ( err , ctx )
} )
2021-07-05 17:54:00 +00:00
}
2021-10-09 00:12:56 +00:00
this . requestStartInternal ( ctx )
2020-03-11 12:34:34 +00:00
}
2021-10-09 00:12:56 +00:00
catch ( err ) {
this . requestEnd ( err , ctx )
2020-03-11 12:34:34 +00:00
}
}
2021-10-09 00:12:56 +00:00
requestStartInternal ( ctx ) {
let route = this . routers [ ctx . method ] . match ( ctx . url )
2022-01-05 14:06:06 +00:00
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 )
}
2021-10-09 00:12:56 +00:00
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 )
} )
2021-07-05 17:54:00 +00:00
}
2021-10-09 00:12:56 +00:00
this . requestEnd ( null , ctx )
2021-07-05 17:54:00 +00:00
}
2021-10-09 00:12:56 +00:00
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 )
} )
2020-03-11 12:34:34 +00:00
}
}
2021-10-09 00:12:56 +00:00
}
2022-03-26 15:50:18 +00:00
requestEnd ( orgErr , ctx ) {
let err = orgErr
2022-03-27 00:30:56 +00:00
let handleUsed = Boolean ( ctx . body && ctx . body . handleRequest )
if ( handleUsed ) {
2022-03-26 15:50:18 +00:00
try {
ctx . body = ctx . body . handleRequest ( ctx )
} catch ( newErr ) {
err = newErr
}
}
2021-10-09 00:12:56 +00:00
if ( err ) {
2022-01-05 14:06:06 +00:00
try {
this . _onerror ( err , ctx )
} catch ( err ) {
this . _backuperror ( err , ctx )
}
2021-10-09 00:12:56 +00:00
}
2022-03-17 15:09:22 +00:00
2021-10-10 21:32:15 +00:00
if ( ctx . res . writableEnded ) {
2021-10-09 00:12:56 +00:00
return
}
2022-03-27 00:30:56 +00:00
if ( ctx . body === null && ! handleUsed && ctx . status === 200 ) {
ctx . status = 204
}
2021-10-09 00:12:56 +00:00
if ( statuses . empty [ ctx . status ] ) {
2022-03-25 13:26:13 +00:00
ctx . res . writeHead ( ctx . status , ctx . headers )
2021-10-09 00:12:56 +00:00
return ctx . res . end ( )
}
let body = ctx . body
2022-03-27 00:30:56 +00:00
// Special handling for files
2021-10-09 00:12:56 +00:00
if ( body && typeof ( body . pipe ) === 'function' ) {
2022-03-17 15:09:22 +00:00
// 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 ]
}
}
2022-03-25 13:26:13 +00:00
ctx . headers [ 'Content-Type' ] = ctx . type || 'application/octet-stream'
ctx . res . writeHead ( ctx . status , ctx . headers )
2022-03-27 00:30:56 +00:00
if ( ctx . method !== 'HEAD' ) {
return this . stream . pipeline ( body , ctx . res , function ( ) { } )
} else {
try {
body . destroy ( )
} catch { }
return ctx . res . end ( )
}
2020-03-11 12:34:34 +00:00
}
2022-03-27 00:30:56 +00:00
let length = 0
if ( typeof ( body ) === 'object' && body ) {
2021-10-09 00:12:56 +00:00
body = JSON . stringify ( body )
length = Buffer . byteLength ( body )
2022-03-27 00:30:56 +00:00
ctx . type = 'application/json; charset=utf-8'
} else if ( body ) {
2021-10-09 00:12:56 +00:00
body = body . toString ( )
length = Buffer . byteLength ( body )
2022-03-27 00:30:56 +00:00
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
2020-03-11 12:34:34 +00:00
}
2022-03-25 13:26:13 +00:00
ctx . res . writeHead ( ctx . status , ctx . headers )
2022-03-27 00:30:56 +00:00
if ( body && ctx . method !== 'HEAD' ) {
ctx . res . end ( body )
} else {
ctx . res . end ( )
}
2020-03-11 12:34:34 +00:00
}
2021-10-09 00:12:56 +00:00
requestEnded ( ctx ) {
2021-10-11 02:06:22 +00:00
if ( ctx . finished ) return
ctx . finished = true
2022-03-17 15:09:22 +00:00
// Prevent accidental leaking
if ( ctx . body && ctx . body . pipe && ! ctx . body . closed ) {
ctx . body . destroy ( )
}
2021-10-09 00:12:56 +00:00
this . _afterCompiled ( ctx )
if ( this . _afterAsyncCompiled ) {
return this . _afterAsyncCompiled ( ctx ) . then ( )
}
}
2021-07-05 17:54:00 +00:00
2021-10-09 00:12:56 +00:00
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 ` ] )
}
}
}
2022-03-16 10:41:25 +00:00
listen ( port , orgIp , orgcb ) {
let ip = orgIp
let cb = orgcb
if ( ! cb && typeof ( orgIp ) === 'function' ) {
ip = '::'
cb = orgIp
}
2021-10-09 00:12:56 +00:00
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 ) )
2022-03-16 10:41:25 +00:00
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 ) )
2022-03-27 14:48:11 +00:00
if ( this . server . listenAsync && typeof ( this . server . listenAsync ) === 'function' ) {
return this . server . listenAsync ( port , ip )
}
2022-03-16 10:41:25 +00:00
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 )
} )
} )
2021-10-09 00:12:56 +00:00
}
2020-03-11 12:34:34 +00:00
}