diff --git a/benchmark/regexp_cve.mjs b/benchmark/regexp_cve.mjs new file mode 100644 index 0000000..6ca50fb --- /dev/null +++ b/benchmark/regexp_cve.mjs @@ -0,0 +1,19 @@ +import { printTime } from './utils.mjs' + +var arg = ""; +for (var i = 0; i < 1000000; i++) { + arg += "\\"; +} +arg += "◎"; + +let s1 = process.hrtime.bigint() +let s2 = process.hrtime.bigint() + +arg = arg.replace(/(?=\\*?)"/g, '$1$1\\"'); +arg = arg.replace(/(?=\\*?)$/, '$1$1'); + +let s3 = process.hrtime.bigint() + +let time = s3 - s2 - (s2 - s1) + +printTime(time) diff --git a/benchmark/router_v2_compile.mjs b/benchmark/router_v2_compile.mjs index 1533eec..cf93c4c 100644 --- a/benchmark/router_v2_compile.mjs +++ b/benchmark/router_v2_compile.mjs @@ -1,13 +1,16 @@ -import { compilePaths } from "../router_v2.mjs" +import { FlaskaRouter } from "../flaska.mjs" import { printTime } from './utils.mjs' import * as consts from './const.js' let paths = consts.allManyRoutes.map(x => ({ path: x })) +let router = new FlaskaRouter() +router.paths = paths + let s1 = process.hrtime.bigint() let s2 = process.hrtime.bigint() -compilePaths(paths) +router.compile() let s3 = process.hrtime.bigint() diff --git a/benchmark/utils.mjs b/benchmark/utils.mjs index d26a56b..d4a448e 100644 --- a/benchmark/utils.mjs +++ b/benchmark/utils.mjs @@ -1,6 +1,6 @@ export function printTime (t) { let time = Number(t) - let units = ['n', 'μ', 'm', 'c', 's'] + let units = ['n', 'μ', 'ms', 's'] let unit = units[0] let unitPower = 1 for (let i = 0; i < units.length; i++) { @@ -9,7 +9,7 @@ export function printTime (t) { break } unitPower = power - unit = units[i] + unit = units[i + 1] } console.log(t, '=', Number((time / unitPower).toFixed(2)), unit) -} \ No newline at end of file +} diff --git a/flaska.d.ts b/flaska.d.ts new file mode 100644 index 0000000..bea95aa --- /dev/null +++ b/flaska.d.ts @@ -0,0 +1,147 @@ +import { IncomingMessage, ServerResponse, Server } from 'node:http' +import { Stats } from 'node:fs' + +type Maybe = T | null | undefined + +export const MimeTypeDb: Record> + +export type Logger = { + fatal: (...data: any[]) => void, + error: (...data: any[]) => void, + warn: (...data: any[]) => void, + info: (...data: any[]) => void, + debug: (...data: any[]) => void, + trace: (...data: any[]) => void, + log: (...data: any[]) => void, +} + +export type Method = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'OPTIONS' | 'PATCH' | 'HEAD' + +export type Context = { + log: Logger, + req: IncomingMessage, + res: ServerResponse>, + method: Method, + url: string, + search: string, + state: Record, + status: number, + query: URLSearchParams, + body: any, + type: string | null, + length: number | null, + headers: Record, + aborted?: boolean, +} + +export interface BodyHasHandler = { handleRequest: (ctx: Context) => any } +export type HandlerSync = (ctx: Context) => void +export type HandlerAsync = (ctx: Context) => Promise +export type Handler = HandlerSync | HandlerAsync + +export function QueryHandler(): Handler +export function JsonHandler(options: { sizeLimit?: number | null } = {}): Handler +export function CorsHandler(options: { + allowedMethod?: Maybe, + allowedOrigins?: Maybe, + allowedHeaders?: Maybe, + credentials?: Maybe, + exposeHeaders?: Maybe, + maxAge?: Maybe, +} = {}): Handler + +export type File = { name: string, path: string, filename: string } + +export class HttpError { + constructor(statusCode: number, message: string, body: any) + status: number + body: any +} + +export class FileResponse implements BodyHasHandler { + constructor(filepath: string, stat: Stats) + filepath: string + stat: Stats + handleRequst(ctx: Context): any +} + +export function FormidableHandler(formidable: any, options: { + parseFields?: Maybe, + uploadDir?: Maybe, + filename?: Maybe<(file: File) => string>, + maxFileSize?: Maybe, + maxFieldSize?: Maybe, + maxFields?: Maybe, + rename?: Maybe, +} = {}): Handler + +export class RouterError { + constructor(route1: any, route2: any, message: string) + routeA: any + routeB: any +} + +export type FlaskaRouterMatch = { + path: { + path: string, + handlers: Handler[], + }, + params: Record, +} + +export class FlaskaRouter { + constructor() + addRoute(path: string, handler: Handler): void + addRoute(path: string, middlewares: Handler | Handler[], handler: Handler): void + compile(): void + match(url: string): FlaskaRouterMatch | null +} + +export type FlaskaOptions = { + defaultHeaders?: Maybe>, + log?: Maybe, + nonce?: Maybe>, + nonceCacheLength: Maybe, + appendHeaders?: Maybe>, +} + +export class Flaska { + log: Logger + server: Server + routers: { + GET: FlaskaRouter, + POST: FlaskaRouter, + PUT: FlaskaRouter, + DELETE: FlaskaRouter, + OPTIONS: FlaskaRouter, + PATCH: FlaskaRouter, + HEAD: FlaskaRouter, + } + get: FlaskaRouter['addRoute'] + post: FlaskaRouter['addRoute'] + put: FlaskaRouter['addRoute'] + delete: FlaskaRouter['addRoute'] + options: FlaskaRouter['addRoute'] + patch: FlaskaRouter['addRoute'] + head: FlaskaRouter['addRoute'] + + constructor(options: FlaskaOptions) + devMode(): void + on404(handler: Handler): void + onerror(handler: Handler): void + onreqerror(handler: Handler): void + onreserror(handler: Handler): void + before(handler: HandlerSync): void + beforeAsync(handler: HandlerAsync): void + after(handler: HandlerSync): void + afterAsync(handler: HandlerAsync): void + listen(port: number): void + listen(port: number, ip: string): void + listen(port: number, cb: () => void): void + listen(port: number, ip:string, cb: () => void): void + listenAsync(port: number): Promise + listenAsync(port: number, ip: string): Promise + listenAsync(port: number, cb: () => void): Promise + listenAsync(port: number, ip:string, cb: () => void): Promise + closeAsync(): Promise +} diff --git a/flaska.mjs b/flaska.mjs index ce92c36..e949c6c 100644 --- a/flaska.mjs +++ b/flaska.mjs @@ -37,10 +37,6 @@ 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', @@ -935,7 +931,7 @@ ctx.state.nonce = nonce; this._afterAsync.push(handler) } - requestStart(req, res) { + __requestStart(req, res) { let url = req.url let search = '' let hasSearch = url.indexOf('?') @@ -965,14 +961,14 @@ ctx.state.nonce = nonce; ctx.aborted = true } this._onreqerror(err, ctx) - this.requestEnded(ctx) + this.__requestEnded(ctx) }) res.on('error', (err) => { this._onreserror(err, ctx) }) res.on('finish', () => { - this.requestEnded(ctx) + this.__requestEnded(ctx) }) try { @@ -980,59 +976,59 @@ ctx.state.nonce = nonce; if (this._beforeAsyncCompiled) { return this._beforeAsyncCompiled(ctx) .then(() => { - this.requestStartInternal(ctx) + this.__requestStartInternal(ctx) }).catch(err => { - this.requestEnd(err, ctx) + this.__requestEnd(err, ctx) }) } - this.requestStartInternal(ctx) + this.__requestStartInternal(ctx) } catch (err) { - this.requestEnd(err, ctx) + this.__requestEnd(err, ctx) } } - requestStartInternal(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) + this.__requestEnd(null, ctx) }, err => { - this.requestEnd(err, ctx) + this.__requestEnd(err, ctx) }) } - return this.requestEnd(null, ctx) + return this.__requestEnd(null, ctx) } ctx.params = route.params - let handlers = this.runHandlers(ctx, route.path.handlers, 0) + let handlers = this.__runHandlers(ctx, route.path.handlers, 0) if (handlers && handlers.then) { return handlers.then(() => { - this.requestEnd(null, ctx) + this.__requestEnd(null, ctx) }, err => { - this.requestEnd(err, ctx) + this.__requestEnd(err, ctx) }) } - this.requestEnd(null, ctx) + this.__requestEnd(null, ctx) } - runHandlers(ctx, middles, index) { + __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) + return this.__runHandlers(ctx, middles, i + 1) }) } } } - requestEnd(orgErr, ctx) { + __requestEnd(orgErr, ctx) { let err = orgErr let handleUsed = Boolean(ctx.body && ctx.body.handleRequest) if (handleUsed) { @@ -1120,7 +1116,7 @@ ctx.state.nonce = nonce; } } - requestEnded(ctx) { + __requestEnded(ctx) { if (ctx.finished) return ctx.finished = true @@ -1134,7 +1130,7 @@ ctx.state.nonce = nonce; } } - compile() { + __compile() { let types = ['before', 'after'] for (let i = 0; i < types.length; i++) { let type = types[i] @@ -1170,9 +1166,9 @@ ctx.state.nonce = nonce; this.routers.PATCH.compile() } - create() { - this.compile() - this.server = this.http.createServer(this.requestStart.bind(this)) + __create() { + this.__compile() + this.server = this.http.createServer(this.__requestStart.bind(this)) this.server.on('connection', function (socket) { // Set socket idle timeout in milliseconds @@ -1197,7 +1193,7 @@ ctx.state.nonce = nonce; throw new Error('Flaska.listen() called with non-number in port') } - this.create() + this.__create() this.server.listen(port, ip, cb) } @@ -1207,7 +1203,7 @@ ctx.state.nonce = nonce; return Promise.reject(new Error('Flaska.listen() called with non-number in port')) } - this.create() + this.__create() if (this.server.listenAsync && typeof(this.server.listenAsync) === 'function') { return this.server.listenAsync(port, ip) diff --git a/package.json b/package.json index 8aad104..5626430 100644 --- a/package.json +++ b/package.json @@ -1,12 +1,13 @@ { "name": "flaska", - "version": "1.4.0", + "version": "1.5.0", "description": "Flaska is a micro web-framework for node. It is designed to be fast, simple and lightweight, and is distributed as a single file module with no dependencies.", "main": "flaska.mjs", "scripts": { "test": "eltro -r dot", "test:watch": "eltro -r dot -w test" }, + "types": "flaska.d.ts", "watch": { "test": { "patterns": [ diff --git a/test/flaska.api.test.mjs b/test/flaska.api.test.mjs index 9485ee5..9c49fd7 100644 --- a/test/flaska.api.test.mjs +++ b/test/flaska.api.test.mjs @@ -668,7 +668,7 @@ t.describe('#compile()', function() { flaska.before(function(ctx) { ctx.c = 3 }) flaska.before(function(ctx) { ctx.d = 4 }) assert.notOk(flaska._beforeCompiled) - flaska.compile() + flaska.__compile() assert.ok(flaska._beforeCompiled) assert.notOk(flaska._beforeAsyncCompiled) assert.strictEqual(typeof(flaska._beforeCompiled), 'function') @@ -686,7 +686,7 @@ t.describe('#compile()', function() { flaska.beforeAsync(function(ctx) { return new Promise(function(res) { ctx.c = 3; res() }) }) flaska.beforeAsync(function(ctx) { ctx.d = 4 }) assert.notOk(flaska._beforeAsyncCompiled) - flaska.compile() + flaska.__compile() assert.ok(flaska._beforeAsyncCompiled) assert.strictEqual(typeof(flaska._beforeAsyncCompiled), 'function') let ctx = createCtx() @@ -704,7 +704,7 @@ t.describe('#compile()', function() { flaska.after(function(ctx) { ctx.c = 3 }) flaska.after(function(ctx) { ctx.d = 4 }) assert.notOk(flaska._afterCompiled) - flaska.compile() + flaska.__compile() assert.ok(flaska._afterCompiled) assert.notOk(flaska._afterAsyncCompiled) assert.strictEqual(typeof(flaska._afterCompiled), 'function') @@ -722,7 +722,7 @@ t.describe('#compile()', function() { flaska.afterAsync(function(ctx) { return new Promise(function(res) { ctx.c = 3; res() }) }) flaska.afterAsync(function(ctx) { ctx.d = 4 }) assert.notOk(flaska._afterAsyncCompiled) - flaska.compile() + flaska.__compile() assert.ok(flaska._afterAsyncCompiled) assert.strictEqual(typeof(flaska._afterAsyncCompiled), 'function') let ctx = createCtx() @@ -738,13 +738,13 @@ t.describe('#compile()', function() { t.describe('#runHandlers()', function() { t.test('should work with empty array', function() { let flaska = new Flaska({}, faker) - flaska.runHandlers({}, [], 0) + flaska.__runHandlers({}, [], 0) }) t.test('should work with correct index', function() { let checkIsTrue = false let flaska = new Flaska({}, faker) - flaska.runHandlers({}, [ + flaska.__runHandlers({}, [ function() { throw new Error('should not be thrown') }, function() { throw new Error('should not be thrown') }, function() { throw new Error('should not be thrown') }, @@ -757,7 +757,7 @@ t.describe('#runHandlers()', function() { const assertCtx = createCtx({ a: 1 }) let checkCounter = 0 let flaska = new Flaska({}, faker) - flaska.runHandlers(assertCtx, [ + flaska.__runHandlers(assertCtx, [ function(ctx) { assert.strictEqual(ctx, assertCtx); checkCounter++ }, function(ctx) { assert.strictEqual(ctx, assertCtx); checkCounter++ }, function(ctx) { assert.strictEqual(ctx, assertCtx); checkCounter++ }, @@ -771,7 +771,7 @@ t.describe('#runHandlers()', function() { const assertCtx = createCtx({ a: 1 }) let checkCounter = 0 let flaska = new Flaska({}, faker) - let result = flaska.runHandlers(assertCtx, [ + let result = flaska.__runHandlers(assertCtx, [ function(ctx) { assert.strictEqual(ctx, assertCtx); assert.strictEqual(checkCounter, 0); checkCounter++ }, function(ctx) { assert.strictEqual(ctx, assertCtx); assert.strictEqual(checkCounter, 1); checkCounter++ }, function(ctx) { return new Promise(function(res) { assert.strictEqual(ctx, assertCtx); assert.strictEqual(checkCounter, 2); checkCounter++; res() }) }, @@ -793,19 +793,19 @@ t.describe('#runHandlers()', function() { const assertError = { a: 1 } let checkCounter = 0 let flaska = new Flaska({}, faker) - let err = await assert.isRejected(flaska.runHandlers({}, [ + let err = await assert.isRejected(flaska.__runHandlers({}, [ function() { }, function() { return new Promise(function(res, rej) { rej(assertError) }) }, function() { throw new Error('should not be seen') }, ], 0)) assert.strictEqual(err, assertError) - err = await assert.isRejected(flaska.runHandlers({}, [ + err = await assert.isRejected(flaska.__runHandlers({}, [ function() { }, function() { return Promise.reject(assertError) }, function() { throw new Error('should not be seen') }, ], 0)) assert.strictEqual(err, assertError) - err = await assert.isRejected(flaska.runHandlers({}, [ + err = await assert.isRejected(flaska.__runHandlers({}, [ function() { }, function() { return Promise.resolve() }, function() { throw assertError }, @@ -828,7 +828,7 @@ t.describe('#listen()', function() { t.test('it should automatically call compile', function() { let assertCalled = false let flaska = new Flaska({}, faker) - flaska.compile = function() { assertCalled = true } + flaska.__compile = function() { assertCalled = true } flaska.listen(404) assert.strictEqual(assertCalled, true) }) @@ -847,8 +847,8 @@ t.describe('#listen()', function() { checkListenCb = cb }) let flaska = new Flaska({}, testFaker) - assert.ok(flaska.requestStart) - flaska.requestStart = function() { + assert.ok(flaska.__requestStart) + flaska.__requestStart = function() { checkInternalThis = this checkIsTrue = true } @@ -871,8 +871,8 @@ t.describe('#listen()', function() { checkListenCb = cb }) let flaska = new Flaska({}, testFaker) - assert.ok(flaska.requestStart) - flaska.requestStart = function() { + assert.ok(flaska.__requestStart) + flaska.__requestStart = function() { checkInternalThis = this checkIsTrue = true } @@ -891,14 +891,14 @@ t.describe('#listen()', function() { checkHandler = cb }) let flaska = new Flaska({}, testFaker) - assert.ok(flaska.requestStart) - flaska.requestStart = function() { + assert.ok(flaska.__requestStart) + flaska.__requestStart = function() { checkInternalThis = this checkIsTrue = true } flaska.listen(404) assert.strictEqual(typeof(checkHandler), 'function') - assert.notStrictEqual(checkHandler, flaska.requestStart) + assert.notStrictEqual(checkHandler, flaska.__requestStart) assert.notStrictEqual(checkIsTrue, true) assert.notStrictEqual(checkInternalThis, flaska) checkHandler() @@ -929,7 +929,7 @@ t.describe('#listenAsync()', function() { t.test('it should automatically call compile', async function() { let assertCalled = false let flaska = new Flaska({}, faker) - flaska.compile = function() { assertCalled = true } + flaska.__compile = function() { assertCalled = true } await flaska.listenAsync(404) assert.strictEqual(assertCalled, true) }) @@ -947,8 +947,8 @@ t.describe('#listenAsync()', function() { }) let flaska = new Flaska({}, testFaker) - assert.ok(flaska.requestStart) - flaska.requestStart = function() { + assert.ok(flaska.__requestStart) + flaska.__requestStart = function() { checkInternalThis = this checkIsTrue = true } @@ -968,8 +968,8 @@ t.describe('#listenAsync()', function() { cb() }) let flaska = new Flaska({}, testFaker) - assert.ok(flaska.requestStart) - flaska.requestStart = function() { + assert.ok(flaska.__requestStart) + flaska.__requestStart = function() { checkInternalThis = this checkIsTrue = true } @@ -992,8 +992,8 @@ t.describe('#listenAsync()', function() { } } }) - assert.ok(flaska.requestStart) - flaska.requestStart = function() { + assert.ok(flaska.__requestStart) + flaska.__requestStart = function() { checkInternalThis = this checkIsTrue = true } @@ -1016,8 +1016,8 @@ t.describe('#listenAsync()', function() { } } }) - assert.ok(flaska.requestStart) - flaska.requestStart = function() { + assert.ok(flaska.__requestStart) + flaska.__requestStart = function() { checkInternalThis = this checkIsTrue = true } @@ -1036,14 +1036,14 @@ t.describe('#listenAsync()', function() { checkHandler = cb }) let flaska = new Flaska({}, testFaker) - assert.ok(flaska.requestStart) - flaska.requestStart = function() { + assert.ok(flaska.__requestStart) + flaska.__requestStart = function() { checkInternalThis = this checkIsTrue = true } await flaska.listenAsync(404) assert.strictEqual(typeof(checkHandler), 'function') - assert.notStrictEqual(checkHandler, flaska.requestStart) + assert.notStrictEqual(checkHandler, flaska.__requestStart) assert.notStrictEqual(checkIsTrue, true) assert.notStrictEqual(checkInternalThis, flaska) checkHandler()