2024-11-02 23:40:54 +00:00
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 ( ' 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 ( )
/ * *
* 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 ) {
2024-11-03 09:16:11 +00:00
let output = "'use strict'"
output += '\nreturn function flaskaBufferStrClos(str) {'
2024-11-02 23:40:54 +00:00
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 )
} )
} )
}
}