From 1cbb928581e0e8d2d295d56c82dc3ced9cab7316 Mon Sep 17 00:00:00 2001 From: Matheus Sampaio Queiroga Date: Wed, 25 Oct 2023 00:19:12 +0000 Subject: [PATCH] reewrite package to reduce depencies Signed-off-by: GitHub --- README.md | 22 +- export/index.cjs | 10 - export/index.d.ts | 11 - export/index.mjs | 9 - package.json | 48 +- src/application.ts | 377 ++++++--------- src/handler.ts | 478 +++++++++++++++++++ src/layer.ts | 87 ---- src/middles/bodyParse.ts | 2 +- src/middles/staticFile.ts | 2 +- src/ranger.ts | 169 +++++++ src/request.ts | 426 ----------------- src/response.ts | 946 -------------------------------------- src/util.ts | 32 ++ src/utils.ts | 277 ----------- testLocal/.gitignore | 1 - testLocal/index.cjs | 36 -- testLocal/index.html | 115 ----- tsconfig.json | 10 +- 19 files changed, 840 insertions(+), 2218 deletions(-) delete mode 100644 export/index.cjs delete mode 100644 export/index.d.ts delete mode 100644 export/index.mjs create mode 100644 src/handler.ts delete mode 100644 src/layer.ts create mode 100644 src/ranger.ts delete mode 100644 src/request.ts delete mode 100644 src/response.ts create mode 100644 src/util.ts delete mode 100644 src/utils.ts delete mode 100644 testLocal/.gitignore delete mode 100644 testLocal/index.cjs delete mode 100644 testLocal/index.html diff --git a/README.md b/README.md index 50d66e5..a1c26a2 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,6 @@ A fork of [express](https://github.com/expressjs/express) with patches and impro ```js import neste from "neste"; - const app = neste(); app.get("/", (req, res) => res.send("hello world")); @@ -15,26 +14,11 @@ app.listen(3000, () => { }); ``` -```js -const neste = require("neste"), app = neste(); +## 3.x notice -app.get("/", (req, res) => res.send("hello world")); -app.get("/json", (req, res) => res.json({message: "hello world"})); - -app.listen(3000, () => { - console.log("Listen on %s", 3000); -}); -``` - -## Installation - -in a simple way: - -```sh -npm install --save neste -``` +version 3.0.0 is removing support for CommonJS, keeping only the ESM module. if you app/module is Commonjs module migrate to ESM or keep on ExpressJS. -### Express middleware's +## Express middleware's > **Important** > as a fork of express will be compatible some, probably some others have stopped in the future. diff --git a/export/index.cjs b/export/index.cjs deleted file mode 100644 index bc0dccb..0000000 --- a/export/index.cjs +++ /dev/null @@ -1,10 +0,0 @@ -const app = require("../src/application"); -const { parseBody } = require("../src/middles/bodyParse"); -const { staticFile } = require("../src/middles/staticFile"); - -function neste() { return new app.Neste(); } -function router() { return new app.Router(); } -module.exports = neste; -module.exports.router = router; -module.exports.parseBody = parseBody; -module.exports.staticFile = staticFile; \ No newline at end of file diff --git a/export/index.d.ts b/export/index.d.ts deleted file mode 100644 index df05099..0000000 --- a/export/index.d.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { Neste, Router } from "../src/application.js"; -import { parseBody } from "../src/middles/bodyParse.js"; -import { staticFile } from "../src/middles/staticFile.js"; - -declare module "neste" { - function neste(): Neste; - export function router(): Router; - export { parseBody, staticFile }; - export default neste; - export = neste; -} \ No newline at end of file diff --git a/export/index.mjs b/export/index.mjs deleted file mode 100644 index 0dbb243..0000000 --- a/export/index.mjs +++ /dev/null @@ -1,9 +0,0 @@ -import main from "../src/application.js"; -import __body from "../src/middles/bodyParse.js"; -import __static_files from "../src/middles/staticFile.js"; -const { Neste, Router } = main, { LocalFile, parseBody } = __body, { staticFile } = __static_files; - -export default function neste() { return new Neste(); } -function router() { return new Router() }; - -export { LocalFile, neste, parseBody, router, staticFile }; \ No newline at end of file diff --git a/package.json b/package.json index 67dd43d..3ec43c5 100644 --- a/package.json +++ b/package.json @@ -1,20 +1,11 @@ { "name": "neste", "description": "a express fork", - "version": "2.0.6", + "version": "3.0.0", "author": "Matheus Sampaio Queiroga ", "license": "MIT", - "type": "commonjs", - "main": "./export/index.cjs", - "types": "./export/index.d.ts", - "default": "./export/index.mjs", - "exports": { - ".": { - "require": "./export/index.cjs", - "default": "./export/index.mjs", - "types": "./export/index.d.ts" - } - }, + "type": "module", + "main": "./src/index.js", "repository": { "type": "git", "url": "git+https://github.com/Sirherobrine23/neste.git" @@ -42,38 +33,17 @@ "postpack": "tsc --build --clean" }, "devDependencies": { - "@types/accepts": "^1.3.5", - "@types/busboy": "^1.5.0", - "@types/content-disposition": "^0.5.5", - "@types/content-type": "^1.1.5", - "@types/cookie": "^0.5.1", - "@types/encodeurl": "^1.0.0", - "@types/escape-html": "^1.0.2", - "@types/finalhandler": "^1.2.0", - "@types/node": "^20.4.9", - "@types/proxy-addr": "^2.0.0", - "@types/qs": "^6.9.7", - "@types/range-parser": "^1.2.4", - "@types/send": "^0.17.1", - "@types/type-is": "^1.6.3", - "@types/vary": "^1.1.0", - "@types/ws": "^8.5.5", + "@types/busboy": "^1.5.2", + "@types/cookie": "^0.5.3", + "@types/node": "^20.8.8", + "@types/ws": "^8.5.8", "ts-node": "^10.9.1", - "typescript": "^5.1.6" + "typescript": "^5.2.2" }, "dependencies": { - "accepts": "^1.3.8", "busboy": "^1.6.0", - "content-disposition": "^0.5.4", - "content-type": "^1.0.5", "cookie": "^0.5.0", - "finalhandler": "^1.2.0", "path-to-regexp": "^6.2.1", - "proxy-addr": "^2.0.7", - "qs": "^6.11.2", - "send": "^0.18.0", - "type-is": "^1.6.18", - "vary": "^1.1.2", - "ws": "^8.13.0" + "ws": "^8.14.2" } } diff --git a/src/application.ts b/src/application.ts index 2245be3..32e4b61 100644 --- a/src/application.ts +++ b/src/application.ts @@ -1,30 +1,14 @@ -import cookie from "cookie"; -import { EventEmitter } from "events"; -import finalhandler from "finalhandler"; -import { IncomingMessage, Server, ServerResponse, createServer } from "http"; -import { isIPv6 } from "net"; -import { resolve as pathResolve } from "path/posix"; +import { IncomingMessage, Server, ServerResponse } from "http"; +import { AddressInfo, ListenOptions } from "net"; import { parse } from "url"; -import nodeUtil from "util"; -import { ErrorRequestHandler, Handler, Layer, RequestHandler, WsRequestHandler } from "./layer"; -import { Request, req as __req } from "./request"; -import { Response, res as __res } from "./response"; -import { compileQueryParser, compileTrust, methods, mixin, setPrototypeOf } from "./utils"; -import wss, { WebSocket } from "ws"; -export type { Handler }; - -const trustProxyDefaultSymbol = '@@symbol:trust_proxy_default'; - -let __RouterHandler = __RouterHandler__.toString(); __RouterHandler = __RouterHandler.slice(__RouterHandler.indexOf("{")+1, -1).trim(); -function __RouterHandler__(this: Router, req: IncomingMessage, res: ServerResponse, next: (err?: any) => void): void { - if (typeof this.handler === "function") return this.handler.apply(this, arguments); - else if (typeof arguments.callee["handler"] === "function") return arguments.callee["handler"].apply(arguments.callee, arguments); - throw new Error("Cannot get Router class"); -} +import util from "util"; +import { WebSocket, WebSocketServer } from "ws"; +import { ErrorRequestHandler, Handlers, Layer, NextFunction, RequestHandler, WsRequestHandler, assignRequest, assignResponse, assignWsResponse } from "./handler.js"; +import { Methods, methods } from "./util.js"; export interface Router { - (req: IncomingMessage, socket: WebSocket, next: (err?: any) => void): void; - (req: IncomingMessage, res: ServerResponse, next: (err?: any) => void): void; + (req: IncomingMessage, res: ServerResponse, next?: (err?: any) => void): void; + (req: IncomingMessage, socket: WebSocket, next?: (err?: any) => void): void; ws(path: string|RegExp, ...fn: WsRequestHandler[]): this; get(path: string|RegExp, ...fn: RequestHandler[]): this; put(path: string|RegExp, ...fn: RequestHandler[]): this; @@ -37,247 +21,174 @@ export interface Router { } export class Router extends Function { - cache = {}; - settings: Record = {}; - locals: Record = Object.create(null); - mountpath = "/"; - - request: Request; - response: Response; - - constructor() { - super(__RouterHandler); - mixin(this, EventEmitter.prototype, false); - const env = process.env.NODE_ENV || "development"; - this.set("env", env); - this.set("query parser", "extended"); - this.set("subdomain offset", 2); - this.set("trust proxy", false); - this.set("json spaces", 2); - this.set("path resolve", true); - - // trust proxy inherit back-compat - Object.defineProperty(this.settings, trustProxyDefaultSymbol, { - configurable: true, - value: true - }); - - // default locals - this.locals.settings = this.settings; - - // default configuration - this.set("jsonp callback name", "callback"); - - // expose the prototype that will get set on requests and responses - this.request = Object.create(__req, { - app: { configurable: true, enumerable: true, writable: true, value: this } - }); - this.response = Object.create(__res, { - app: { configurable: true, enumerable: true, writable: true, value: this } - }); + constructor(opts?: any) { + super("if (typeof this.handler === 'function') { this.handler.apply(this, arguments); } else if (typeof arguments.callee === 'function') { arguments.callee.apply(arguments.callee, arguments); } else { throw new Error('Cannot get Router class'); }"); } - stacks: Layer[] = []; - - /** - * - * @param req - Request socket from http, https server - * @param res - Response socket from http, https server - * @param next - Handler request - */ - handler(req: IncomingMessage, res: ServerResponse|WebSocket, done?: (err?: any) => void): void { - if (!(this instanceof Router)) throw new Error("Cannot access class"); - else if (this.stacks.length === 0) return done(); - const method = req.method = res instanceof WebSocket ? "ws" : typeof req.method === "string" ? req.method.toLowerCase() : req.method; - req["res"] = res; - // @ts-ignore - res["req"] = req; - req["next"] = next; - let { pathname, query } = parse(req.url); - req["path"] ||= (!!(this.set("path resolve")) ? pathResolve(pathname) : pathname); - if (!req["fullPath"]) { - req["fullPath"] = req["path"]; - Object.defineProperty(req, "fullPath", { - configurable: false, - enumerable: false, - writable: false, - value: req["path"] - }); - } - const parseQuery = new URLSearchParams(query); - const CookiesStorage = new Map(); - if (typeof req.headers.cookie === "string") { - const parsed = cookie.parse(req.headers.cookie); - Object.keys(parsed).forEach(k => CookiesStorage.set(k, parsed[k])); - } - setPrototypeOf(req, Object.create(this.request, { - query: { configurable: true, enumerable: true, writable: true, value: Array.from(parseQuery.keys()).reduce>((acc, key) => { acc[key] = parseQuery.get(key); return acc; }, {}) }, - ipPort: { configurable: false, enumerable: false, writable: false, value: req.socket.remoteAddress ? (isIPv6(req.socket.remoteAddress) ? `[${req.socket.remoteAddress}]:${req.socket.remotePort}` : `${req.socket.remoteAddress}:${req.socket.remotePort}`) : undefined }, - method: { configurable: false, enumerable: false, writable: false, value: method }, - Cookies: { configurable: true, enumerable: true, writable: true, value: CookiesStorage }, - })); - - if (!(res instanceof WebSocket)) { - if (done === undefined) done = finalhandler(req, res, { env: this.set("env"), onerror: (err) => { if (this.set("env") !== "test") console.error(err.stack || err.toString()); } }); - setPrototypeOf(res, Object.create(this.response, {})); - res["locals"] = res["locals"] || Object.create(null); - } else { - if (done === undefined) done = () => res.close(404); - req.on("close", () => {}); + layers: Layer[] = []; + wsRooms: Map = new Map(); + + handler(req: IncomingMessage, res: WebSocket|ServerResponse, next?: NextFunction) { + if (typeof next !== "function") next = (err) => { + if (err && !(err === "router" || err === "route")) console.error(err); + if (res instanceof WebSocket) { + res.send("Close connection!"); + res.close(); + } else { + if (err) { + res.statusCode = 500; + res.end(err?.stack||err?.message||String(err)) + } else { + res.statusCode = 404; + res.end("No path\n"); + } + } } - let stackX = 0; - const { stacks } = this; - const saveParms = Object.freeze(req["params"] || {}); - const originalPath = req["path"]; + if (!req["path"]) req["path"] = (parse(req.url)).pathname; + const { layers } = this, method = (res instanceof WebSocket ? "ws" : (String(req.method||"").toLowerCase())), saveParms = Object.freeze(req["params"] || {}), originalPath = req["path"];; + let layersIndex = 0; - next(); - function next(err?: any) { + nextHandler().catch(next); + async function nextHandler(err?: any) { req["path"] = originalPath; - if (err && err === "route") return done(); - else if (err && err === "router") return done(err); - const layer = stacks.at(stackX++); - if (!layer) return done(err); - else if (layer.method && layer.method !== method) return next(err); + req["params"] = Object.assign({}, saveParms); + if (err && err === "route") return next(); + else if (err && err === "router") return next(err); + const layer = layers.at(layersIndex++); + if (!layer) return next(err); + else if (layer.method && layer.method !== method) return nextHandler(err); const layerMatch = layer.match(req["path"]); - if (!layerMatch) return next(err); - if (layerMatch.path.length < req["path"].length) req["path"] = req["path"].slice(layerMatch.path.length); - req["params"] = Object.assign({}, saveParms, layerMatch.params); - - const fn = layer.handle; - if (err && fn.length !== 4) next(err); - else if (err) Promise.resolve().then(() => (fn as ErrorRequestHandler)(err, req as any, res as any, next)).catch(next); - else Promise.resolve().then(() => (fn as RequestHandler)(req as any, res as any, next)).catch(next); + if (!layerMatch) return nextHandler(err); + if (err && layer.handler.length !== 4) return nextHandler(err); + try { + if (err) { + if (res instanceof WebSocket) return nextHandler(err); + const fn = layer.handler as ErrorRequestHandler; + await fn(err, assignRequest(req, method, Object.assign({}, saveParms, layerMatch.params)), assignResponse(res), nextHandler); + } else { + if (res instanceof WebSocket) { + const fn = layer.handler as WsRequestHandler; + await fn(assignRequest(req, method, Object.assign({}, saveParms, layerMatch.params)), assignWsResponse(res, this), nextHandler); + } else { + const fn = layer.handler as RequestHandler; + await fn(assignRequest(req, method, Object.assign({}, saveParms, layerMatch.params)), assignResponse(res), nextHandler); + } + } + } catch (err) { + nextHandler(err); + } } } - /** - * Middleare extension - * - * @example - * ```js - * app.use(neste.Router().get(("/" ({res}) => res.json({ ok: true })))); - * ``` - */ - use(...fn: RequestHandler[]): this; - /** - * Middleare extension - * - * @example - * ```js - * app.use("/foo", neste.Router().get(("/bar" ({res}) => res.json({ ok: true })))); - * ``` - */ - use(path: string|RegExp, ...fn: RequestHandler[]): this; - use() { - const Args = Array.from(arguments); - let path: any = "/", offset = 0; - if (typeof Args[0] === "string" || Args[0] instanceof RegExp) { - path = Args[0]; - offset = 1; - } - for (; offset < Args.length;) { - const fn = Args[offset++]; - if (typeof fn !== "function") throw new Error(nodeUtil.format("Invalid middleare, require function, recived %s", typeof fn)); - const layerFN = new Layer(path, fn, { - strict: false, - end: false, - }); - this.stacks.push(layerFN); + use(...fn: Handlers[]): this; + use(path: string|RegExp, ...fn: Handlers[]): this; + use() { + let p: [string|RegExp, Handlers[]]; + if (!(arguments[0] instanceof RegExp || typeof arguments[0] === "string" && arguments[0].trim())) p = ["/", Array.from(arguments)]; + else p = [arguments[0], Array.from(arguments).slice(1)]; + for (const fn of p[1]) { + if (typeof fn !== "function") throw new Error(util.format("Invalid middleare, require function, recived %s", typeof fn)); + this.layers.push(new Layer(p[0], fn, { strict: false, end: false })); } return this; - }; - - useError(...fn: ErrorRequestHandler[]): this; - useError() { - return this.use.apply(this, arguments); } - all(path: string|RegExp, ...fn: RequestHandler[]): this ; - all() { - if (!(arguments[0] instanceof RegExp || (typeof arguments[0] === "string" && arguments[0].length > 0))) throw new Error("Require path"); - this.use.apply(this, arguments); + __method(method: Methods, path: string|RegExp, ...handlers: RequestHandler[]) { + if (!(path instanceof RegExp || typeof path === "string" && path.trim())) throw new Error("Set path"); + for (const fn of handlers) { + const layerHand = new Layer(path, fn); + layerHand.method = method; + this.layers.push(layerHand); + } return this; } +}; - set(setting: string): any; - set(setting: string, val: any): this; - set(setting: string, val?: any) { - if (arguments.length === 1) { - let settings = this.settings - while (settings && settings !== Object.prototype) { - if (Object.hasOwnProperty.call(settings, setting)) return settings[setting]; - settings = Object.getPrototypeOf(settings); - } - - return undefined; - } - - // set value - this.settings[setting] = val; - - // trigger matched settings - switch (setting) { - case 'query parser': - this.set('query parser fn', compileQueryParser(val)); - break; - case 'trust proxy': - this.set('trust proxy fn', compileTrust(val)); - // trust proxy inherit back-compat - Object.defineProperty(this.settings, trustProxyDefaultSymbol, { - configurable: true, - value: false - }); - break; - } +methods.forEach(method => Router.prototype[method] = function(this: Router) { return this.__method.apply(this, ([method] as any[]).concat(Array.from(arguments))) } as any) +export class Neste extends Router { + httpServer: Server; + listen(port?: number, hostname?: string, backlog?: number, listeningListener?: () => void): this; + listen(port?: number, hostname?: string, listeningListener?: () => void): this; + listen(port?: number, backlog?: number, listeningListener?: () => void): this; + listen(port?: number, listeningListener?: () => void): this; + listen(path: string, backlog?: number, listeningListener?: () => void): this; + listen(path: string, listeningListener?: () => void): this; + listen(options: ListenOptions, listeningListener?: () => void): this; + listen(handle: any, backlog?: number, listeningListener?: () => void): this; + listen(handle: any, listeningListener?: () => void): this; + listen(): this { + (this.httpServer||(() => { + this.httpServer = new Server(this); + const wsServer = new WebSocketServer({ noServer: true }); + this.httpServer.on("upgrade", (req, sock, head) => wsServer.handleUpgrade(req, sock, head, (client) => this.handler(req, client))); + return this.httpServer; + })()).listen.apply(this.httpServer, arguments); return this; - }; - - enabled(setting: string) { - return Boolean(this.set(setting)); } - enable(setting: string) { - return this.set(setting, true); + getConnections(cb: (error: Error, count: number) => void): void { + (this.httpServer||(() => { + this.httpServer = new Server(this); + const wsServer = new WebSocketServer({ noServer: true }); + this.httpServer.on("upgrade", (req, sock, head) => wsServer.handleUpgrade(req, sock, head, (client) => this.handler(req, client))); + return this.httpServer; + })()).getConnections(cb); } - disabled(setting: string) { - return !(this.set(setting)); + address(): string | AddressInfo { + return (this.httpServer||(() => { + this.httpServer = new Server(this); + const wsServer = new WebSocketServer({ noServer: true }); + this.httpServer.on("upgrade", (req, sock, head) => wsServer.handleUpgrade(req, sock, head, (client) => this.handler(req, client))); + return this.httpServer; + })()).address() } - disable(setting: string) { - return this.set(setting, false); + closeAllConnections(): void { + (this.httpServer||(() => { + this.httpServer = new Server(this); + const wsServer = new WebSocketServer({ noServer: true }); + this.httpServer.on("upgrade", (req, sock, head) => wsServer.handleUpgrade(req, sock, head, (client) => this.handler(req, client))); + return this.httpServer; + })()).closeAllConnections() } - path() { - return this["parent"] ? this["parent"].path() + this.mountpath : ""; + closeIdleConnections(): void { + (this.httpServer||(() => { + this.httpServer = new Server(this); + const wsServer = new WebSocketServer({ noServer: true }); + this.httpServer.on("upgrade", (req, sock, head) => wsServer.handleUpgrade(req, sock, head, (client) => this.handler(req, client))); + return this.httpServer; + })()).closeIdleConnections(); } -}; -methods.forEach(method => { - Router.prototype[method] = function() { - const [ path, ...fn ] = Array.from(arguments); - if (!(path instanceof RegExp || (typeof path === "string" && path.length > 0))) throw new Error("Require path"); - for (const _fn of fn) { - const layerFN = new Layer(path, _fn, {}); - layerFN.method = method; - this.stacks.push(layerFN); - } + close(callback?: (err?: Error) => void) { + (this.httpServer||(() => { + this.httpServer = new Server(this); + const wsServer = new WebSocketServer({ noServer: true }); + this.httpServer.on("upgrade", (req, sock, head) => wsServer.handleUpgrade(req, sock, head, (client) => this.handler(req, client))); + return this.httpServer; + })()).close(callback); + return this; } -}); -export interface Neste { listen: Server["listen"] }; -export class Neste extends Router { + setTimeout(msecs?: number, callback?: () => void): this; + setTimeout(callback: () => void): this; + setTimeout(): this { + (this.httpServer||(() => { + this.httpServer = new Server(this); + const wsServer = new WebSocketServer({ noServer: true }); + this.httpServer.on("upgrade", (req, sock, head) => wsServer.handleUpgrade(req, sock, head, (client) => this.handler(req, client))); + return this.httpServer; + })()).setTimeout.apply(this.httpServer, arguments); + return this; + } } -Neste.prototype.listen = function (this: Neste) { - const self = this; - const wsServer = new wss.Server({ noServer: true }); - const server = createServer(); - server.on("request", this.handler.bind(this)); - server.on("upgrade", (req, sock, head) => wsServer.handleUpgrade(req, sock, head, (client) => self.handler(req, client))); - return server.listen.apply(server, arguments); -} \ No newline at end of file +const app = new Neste(); +app.listen(3000, () => console.log("Http 3000")) +app.get("/", ({headers, Cookies, hostname}, res) => res.json({ req: { headers, Cookies: Cookies.toJSON(), hostname } })) \ No newline at end of file diff --git a/src/handler.ts b/src/handler.ts new file mode 100644 index 0000000..ccb974b --- /dev/null +++ b/src/handler.ts @@ -0,0 +1,478 @@ +import { MatchFunction, ParseOptions, RegexpToFunctionOptions, TokensToRegexpOptions, match as regexMatch } from "path-to-regexp"; +import { IncomingMessage, ServerResponse } from "http"; +import { WebSocket } from "ws"; +import cookie from "cookie"; +import { parse } from "url"; +import { isIP } from "net"; +import { defineProperties, mixin } from "./util.js"; +import stream from "stream"; +import * as ranger from "./ranger.js"; +import { Router } from "./application.js"; + +export class CookieManeger extends Map { + constructor(public initialCookies: string) { + super(); + if (!initialCookies) return; + const parsed = cookie.parse(initialCookies); + Object.keys(parsed).forEach(k => this.set(k, parsed[k])); + } + + // @ts-ignore + set(key: string, value: string, opts?: cookie.CookieSerializeOptions): this { + super.set(key, [value, opts || {}]); + return this; + } + + toString() { + const parsed = cookie.parse(this.initialCookies); + return Array.from(this.entries()).filter(([key, [value]]) => parsed[key] && parsed[key] !== value).map(([key, [value, opt]]) => cookie.serialize(key, value, opt)).join("; "); + } + + get sizeDiff() { + const parsed = cookie.parse(this.initialCookies); + return Array.from(this.entries()).filter(([key, [value]]) => parsed[key] && parsed[key] !== value).length; + } + + toJSON() { + return Array.from(this.entries()).map(([key, [value, opt]]) => ({ key, value, options: opt })); + } +} + +export interface Request extends IncomingMessage { + protocol: "https"|"http"; + secure: boolean; + path: string; + reqPath: string; + ip?: string; + hostname?: string; + subdomains?: string[]; + + Cookies: CookieManeger; + query: Record; + params: Record; + body?: any; +} + +export class Request { + /** + * Return request header. + * + * The `Referrer` header field is special-cased, + * both `Referrer` and `Referer` are interchangeable. + * + * Examples: + * + * req.get('Content-Type'); + * // => "text/plain" + * + * req.get('content-type'); + * // => "text/plain" + * + * req.get('Something'); + * // => undefined + * + */ + get(name: string): string|string[]|undefined { + if (!name || typeof name !== "string") throw new TypeError("name must be a string to req.get"); + return this.headers[name.toLowerCase()] || this.headers[name]; + } + + /** + * Check if the incoming request contains the "Content-Type" + * header field, and it contains the given mime `type`. + * + * Examples: + * + * // With Content-Type: text/html; charset=utf-8 + * req.is('html'); + * req.is('text/html'); + * req.is('text/*'); + * // => true + * + * // When Content-Type is application/json + * req.is('json'); + * req.is('application/json'); + * req.is('application/*'); + * // => true + * + * req.is('html'); + * // => false + * + */ + is(str: (string[])|string): boolean { + if (typeof str === "string") return String(this.headers["content-type"]||"").includes(str); + for (let st of str) if (String(this.get("content-type")||"").includes(st)) return true; + return false; + } + + /** + * Parse Range header field, capping to the given `size`. + * + * Unspecified ranges such as "0-" require knowledge of your resource length. In + * the case of a byte range this is of course the total number of bytes. If the + * Range header field is not given `undefined` is returned, `-1` when unsatisfiable, + * and `-2` when syntactically invalid. + * + * When ranges are returned, the array has a "type" property which is the type of + * range that is required (most commonly, "bytes"). Each array element is an object + * with a "start" and "end" property for the portion of the range. + * + * The "combine" option can be set to `true` and overlapping & adjacent ranges + * will be combined into a single range. + * + * NOTE: remember that ranges are inclusive, so for example "Range: users=0-3" + * should respond with 4 users when available, not 3. + */ + range(size: number, options?: ranger.Options): ranger.Result|ranger.Ranges { + const range = this.get("Range"); + if (!range || typeof range !== "string") return undefined; + return ranger.rangeParser(size, range, options); + } +} + +export const codes = { + "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", + "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" +} + +export interface Response extends Omit { + req: Request; +} +export class Response { + set(key: string, value: string|string[]|number) { + this.setHeader(key, value); + return this; + } + + get(key: string): string | string[] | number { + return this.getHeader(key); + } + + has(key: string) { + return !!(this.hasHeader(key)||this.hasHeader(key.toLocaleLowerCase())); + } + + status(statusCode: number) { + if (!(statusCode > 0)) throw new TypeError("Set valid code status"); + this.statusCode = statusCode; + return this; + } + + /** + * Send given HTTP status code. + * + * Sets the response status to `statusCode` and the body of the + * response to the standard description from node's http.STATUS_CODES + * or the statusCode number if no description. + * + * Examples: + * + * res.sendStatus(200); + * + * @param statusCode + * @public + */ + sendStatus(statusCode: number) { + if (!(statusCode > 0)) throw new TypeError("Set valid code status"); + this.statusCode = statusCode; + return this.send(codes[statusCode]||String(statusCode)); + } + + /** + * Send JSON response. + * + * Examples: + * + * res.json(null); + * res.json({ user: 'tj' }); + */ + json(obj: any) { + if (!(this.has("Content-Type"))) this.set("Content-Type", "application/json") + return this.send(JSON.stringify(obj, null, 2)); + } + + /** + * Set Link header field with the given `links`. + * + * Examples: + * + * res.links({ + * next: 'http://api.example.com/users?page=2', + * last: 'http://api.example.com/users?page=5' + * }); + * + * @param links + * @public + */ + links(links: Record) { + let link = this.get("Link") || ""; + if (link) link += ", "; + return this.set("Link", link + Object.keys(links).map((rel) => "<" + links[rel] + ">; rel=\"' + rel + '\"").join(", ")); + } + + /** + * Send a response. + * + * Examples: + * + * res.send(Buffer.from('wahoo')); + * res.send({ some: 'json' }); + * res.send('

some html

'); + */ + send(body: any) { + if (body === undefined || body === null) throw new TypeError("Require body"); + let encoding: BufferEncoding = "utf8"; + const bodyType = typeof body; + if (bodyType === "string") { + if (!(this.has("Content-Type"))) this.set("Content-Type", "text/plain"); + } else if (bodyType === "boolean" || bodyType === "number" || bodyType === "object") { + if (Buffer.isBuffer(body)) { + encoding = "binary"; + if (!(this.has("Content-Type"))) this.set("Content-Type", "application/octet-stream"); + } else { + return this.json(body); + } + } + + // strip irrelevant headers + if (204 === this.statusCode || 304 === this.statusCode) { + this.removeHeader("Content-Type"); + this.removeHeader("Content-Length"); + this.removeHeader("Transfer-Encoding"); + body = ""; + encoding = "utf8"; + } + + // alter headers for 205 + if (this.statusCode === 205) { + this.set("Content-Length", "0") + this.removeHeader("Transfer-Encoding") + body = "" + encoding = "utf8"; + } + + if (this.req.Cookies.sizeDiff > 0) this.set("Cookie", this.req.Cookies.toString()); + + // skip body for HEAD + if (this.req.method === "HEAD") this.end(); + else if (body instanceof stream.Readable) body.pipe(this); + else this.end(body, encoding); + + return this; + } + + /** + * Redirect to the given `url` with optional response `status` + * defaulting to 302. + * + * The resulting `url` is determined by `res.location()`, so + * it will play nicely with mounted apps, relative paths, + * `"back"` etc. + * + * Examples: + * + * res.redirect('/foo/bar'); + * res.redirect('http://example.com'); + * res.redirect(new URL('/test', 'http://example.com')); + * res.status(301).redirect('http://example.com'); + * res.redirect('../login'); // /blog/post/1 -> /blog/login + */ + redirect(url: string|URL): void { + if (url instanceof URL) url = url.toString(); + this.status(302).set("Location", url); + this.send(`

Redirecting to ${url}

`); + } +}; + +export interface WsResponse extends WebSocket {} +export class WsResponse {}; + +export interface NextFunction { + /** + * Call for any error's + */ + (err?: any): void; + /** + * "Break-out" of a router by calling {next('router')}; + */ + (deferToNext: "router"): void; + /** + * "Break-out" of a route by calling {next("route")}; + */ + (deferToNext: "route"): void; +} + +export type WsRequestHandler = (req: Request, res: WsResponse, next: NextFunction) => void; +export type RequestHandler = (req: Request, res: Response, next: NextFunction) => void; +export type ErrorRequestHandler = (err: any, req: Request, res: Response, next: NextFunction) => void; +export type Handlers = WsRequestHandler|RequestHandler|ErrorRequestHandler; + +export class Layer { + method?: string; + handler: Handlers; + matchFunc: MatchFunction; + match(path: string): undefined|{ path: string, params: Record } { + const value = this.matchFunc(path); + if (!value) return undefined; + return { + path: value.path, + params: value.params as any, + }; + } + + constructor(path: string|RegExp, fn: Handlers, options?: Omit) { + if (!(typeof fn === "function")) throw new Error("Register function"); + if (!(options)) options = {}; + if (path === "*") path = "(.*)"; + this.handler = fn; + this.matchFunc = regexMatch(path, {...options, decode: decodeURIComponent }); + } +} + +export function assignRequest(req: IncomingMessage, method: string, params: Record): Request { + const parseQuery = new URLSearchParams(parse(req.url).query); + mixin(req, Request.prototype, false); + defineProperties(req, { + method: { configurable: false, enumerable: false, writable: false, value: method }, + Cookies: { configurable: false, enumerable: false, writable: false, value: new CookieManeger((req.headers||{}).cookie||"") }, + query: { configurable: true, enumerable: true, writable: true, value: Object.assign(Array.from(parseQuery.keys()).reduce>((acc, key) => { acc[key] = parseQuery.get(key); return acc; }, {}), req["query"]) }, + params: { configurable: false, enumerable: false, writable: false, value: params }, + protocol: { + configurable: true, + enumerable: true, + get() { + const proto = (this.socket || this.connection)["encrypted"] ? "https" : "http"; + // Note: X-Forwarded-Proto is normally only ever a single value, but this is to be safe. + const header = this.get('X-Forwarded-Proto') || proto; + const index = header.indexOf(',') + return index !== -1 ? header.substring(0, index).trim() : header.trim() + } + }, + secure: { + configurable: true, + enumerable: true, + get() { + return this.protocol === "https" || this.protocol === "wss"; + } + }, + hostname: { + configurable: true, + enumerable: true, + get() { + let host: string = this.get("X-Forwarded-Host") || this.get("Host"); + if (host.indexOf(",") !== -1) { + // Note: X-Forwarded-Host is normally only ever a single value, but this is to be safe. + host = host.substring(0, host.indexOf(",")).trim(); + } + if (!host) return undefined; + + // IPv6 literal support + const offset = host[0] === "[" ? host.indexOf("]") + 1 : 0; + const index = host.indexOf(":", offset); + + return index !== -1 ? host.substring(0, index) : host; + } + }, + subdomains: { + configurable: true, + enumerable: true, + get() { + const hostname: string = this.hostname; + if (!hostname) return []; + let offset = 1; + const subdomains = !isIP(hostname) ? hostname.split(".").reverse() : [hostname]; + if (isIP(hostname)) offset = 0; + return subdomains.slice(offset); + } + }, + ip: { + configurable: true, + enumerable: true, + get() { + return req.socket.remoteAddress; + } + }, + reqPath: { + configurable: true, + enumerable: true, + get() { + return parse(this.url).pathname; + } + }, + }); + return req as any; +} + +export function assignResponse(res: ServerResponse): Response { + mixin(res, Response.prototype, false); + defineProperties(res, {}); + return res as any; +} + +export function assignWsResponse(res: WebSocket, router: Router): WsResponse { + mixin(res, WsResponse.prototype, false); + router.wsRooms + defineProperties(res, {}); + return res as any; +} \ No newline at end of file diff --git a/src/layer.ts b/src/layer.ts deleted file mode 100644 index 956b74e..0000000 --- a/src/layer.ts +++ /dev/null @@ -1,87 +0,0 @@ -import { MatchFunction, ParseOptions, RegexpToFunctionOptions, TokensToRegexpOptions, match as regexMatch } from "path-to-regexp"; -import { WebSocket } from "ws"; -import { Request } from "./request"; -import { Response } from "./response"; - - -export interface NextFunction { - /** - * Call for any error's - */ - (err?: any): void; - /** - * "Break-out" of a router by calling {next('router')}; - */ - (deferToNext: 'router'): void; - /** - * "Break-out" of a route by calling {next('route')}; - */ - (deferToNext: 'route'): void; -} -export type ErrorRequestHandler = (error: any, req: Request, res: Response, next: NextFunction) => void; -export type WsRequestHandler = (req: Request, socket: WebSocket, next: NextFunction) => void; -export type RequestHandler = (req: Request, res: Response, next: NextFunction) => void; -export type Handler = RequestHandler|ErrorRequestHandler|WsRequestHandler; - -export class Layer { - handle: Handler; - regexp: MatchFunction; - fast_star?: boolean; - fast_slash?: boolean; - - constructor(path: string|RegExp, fn: Handler, options?: Omit) { - if (!(typeof fn === "function")) throw new Error("Register function"); - if (!(options)) options = {}; - if (path === "*") path = "(.*)"; - this.handle = fn; - this.regexp = regexMatch(path, {...options, decode: decodeURIComponent }); - this.fast_star = path === "(.*)"; - this.fast_slash = path === "/" && options.end === false; - } - - /** - * Request Method - */ - method?: string; - - match(path: string): undefined|{ path: string, params: Record } { - const decode_param = (val) => { - if (typeof val !== 'string' || val.length === 0) return val; - try { - return decodeURIComponent(val); - } catch (err) { - if (err instanceof URIError) { - err.message = 'Failed to decode param \'' + val + '\''; - err["status"] = err["statusCode"] = 400; - } - throw err; - } - } - - // fast path non-ending match for / (any path matches) - if (this.fast_slash) return { path: "", params: {} }; - - // fast path for * (everything matche d in a param) - if (this.fast_star) return { path, params: {"0": decode_param(path)} }; - const value = this.regexp(path); - if (!value) return undefined; - return { - path: value.path, - params: value.params as any, - }; - } - - /** @deprecated */ - async handle_request(req: Request, res: Response, next: NextFunction) { - const fn = this.handle; - if (fn.length > 3) return next(); - Promise.resolve().then(() => fn.call(fn, req, res, next)).catch(next); - } - - /** @deprecated */ - async handle_error(err: any, req: Request, res: Response, next: NextFunction) { - const fn = this.handle; - if (fn.length !== 4) return next(err); - Promise.resolve().then(() => fn.call(fn, err, req, res, next)).catch(next); - } -} \ No newline at end of file diff --git a/src/middles/bodyParse.ts b/src/middles/bodyParse.ts index 6cc3435..948b53f 100644 --- a/src/middles/bodyParse.ts +++ b/src/middles/bodyParse.ts @@ -5,7 +5,7 @@ import path from "path"; import stream from "stream"; import { finished } from "stream/promises"; import { promisify } from "util"; -import { RequestHandler } from "../layer"; +import { RequestHandler } from "../handler.js"; export type FileStorage = { filePath?: string; diff --git a/src/middles/staticFile.ts b/src/middles/staticFile.ts index 0a8cc97..4592d0d 100644 --- a/src/middles/staticFile.ts +++ b/src/middles/staticFile.ts @@ -1,7 +1,7 @@ import fs from "fs"; import path from "path"; import { pipeline } from "stream"; -import { RequestHandler } from "../layer"; +import { RequestHandler } from "../handler.js"; export function staticFile(folderPath: string): RequestHandler { folderPath = path.resolve(process.cwd(), folderPath); diff --git a/src/ranger.ts b/src/ranger.ts new file mode 100644 index 0000000..08a2040 --- /dev/null +++ b/src/ranger.ts @@ -0,0 +1,169 @@ +/*! + * range-parser + * Copyright(c) 2012-2014 TJ Holowaychuk + * Copyright(c) 2015-2016 Douglas Christopher Wilson + * MIT Licensed + */ + +export interface Ranges extends Array { + type: string; +} + +export interface Range { + start: number; + end: number; +} + +export interface Options { + /** + * The "combine" option can be set to `true` and overlapping & adjacent ranges + * will be combined into a single range. + */ + combine?: boolean | undefined; +} + +export type ResultUnsatisfiable = -1; +export type ResultInvalid = -2; +export type Result = ResultUnsatisfiable | ResultInvalid; + + +/** + * Parse "Range" header `str` relative to the given file `size`. + * + */ + +export function rangeParser(size: number, str: string, options?: Options): Result|Ranges { + if (typeof str !== 'string') { + throw new TypeError('argument str must be a string') + } + + var index = str.indexOf('=') + if (index === -1) { + return -2 + } + + // split the range string + var arr = str.slice(index + 1).split(',') + var ranges: Ranges = [] as any; + + // add ranges type + ranges.type = str.slice(0, index) + + // parse all ranges + for (var i = 0; i < arr.length; i++) { + var range = arr[i].split('-') + var start = parseInt(range[0], 10) + var end = parseInt(range[1], 10) + + // -nnn + if (isNaN(start)) { + start = size - end + end = size - 1 + // nnn- + } else if (isNaN(end)) { + end = size - 1 + } + + // limit last-byte-pos to current length + if (end > size - 1) { + end = size - 1 + } + + // invalid or unsatisifiable + if (isNaN(start) || isNaN(end) || start > end || start < 0) { + continue + } + + // add range + ranges.push({ + start: start, + end: end + }) + } + + if (ranges.length < 1) { + // unsatisifiable + return -1 + } + + return options && options.combine + ? combineRanges(ranges) + : ranges +} + +/** + * Combine overlapping & adjacent ranges. + * @private + */ + +function combineRanges (ranges) { + var ordered = ranges.map(mapWithIndex).sort(sortByRangeStart) + + for (var j = 0, i = 1; i < ordered.length; i++) { + var range = ordered[i] + var current = ordered[j] + + if (range.start > current.end + 1) { + // next range + ordered[++j] = range + } else if (range.end > current.end) { + // extend range + current.end = range.end + current.index = Math.min(current.index, range.index) + } + } + + // trim ordered array + ordered.length = j + 1 + + // generate combined range + var combined = ordered.sort(sortByRangeIndex).map(mapWithoutIndex) + + // copy ranges type + combined.type = ranges.type + + return combined +} + +/** + * Map function to add index value to ranges. + * @private + */ + +function mapWithIndex (range, index) { + return { + start: range.start, + end: range.end, + index: index + } +} + +/** + * Map function to remove index value from ranges. + * @private + */ + +function mapWithoutIndex (range) { + return { + start: range.start, + end: range.end + } +} + +/** + * Sort function to sort ranges by index. + * @private + */ + +function sortByRangeIndex (a, b) { + return a.index - b.index +} + +/** + * Sort function to sort ranges by start position. + * @private + */ + +function sortByRangeStart (a, b) { + return a.start - b.start +} \ No newline at end of file diff --git a/src/request.ts b/src/request.ts deleted file mode 100644 index 2b94400..0000000 --- a/src/request.ts +++ /dev/null @@ -1,426 +0,0 @@ -import accepts from "accepts"; -import http from "http"; -import { isIP } from "net"; -import proxyaddr from "proxy-addr"; -import parseRange from "range-parser"; -import typeis from "type-is"; -import { parse } from "url"; -import { Response } from "./response"; -import { defineGetter } from "./utils"; - -export const req: Request = Object.create(http.IncomingMessage.prototype); -export interface Request extends http.IncomingMessage { - res: Response; - next(err?: any): void; - protocol: "https"|"http"; - secure: boolean; - path: string; - fullPath: string; - stale: boolean; - xhr: boolean; - hostname?: string; - subdomains?: string[]; - ipPort?: string; - ip?: string; - ips?: string[]; - - query: Record; - Cookies: Map; - params: Record; - body?: any; - - /** - * Return request header. - * - * The `Referrer` header field is special-cased, - * both `Referrer` and `Referer` are interchangeable. - * - * Examples: - * - * req.get('Content-Type'); - * // => "text/plain" - * - * req.get('content-type'); - * // => "text/plain" - * - * req.get('Something'); - * // => undefined - * - */ - get(name: string): string|undefined; - /** - * Return request header. - * - * The `Referrer` header field is special-cased, - * both `Referrer` and `Referer` are interchangeable. - * - * Examples: - * - * req.header('Content-Type'); - * // => "text/plain" - * - * req.header('content-type'); - * // => "text/plain" - * - * req.header('Something'); - * // => undefined - * - */ - header(name: string): string|undefined; - - /** - * To do: update docs. - * - * Check if the given `type(s)` is acceptable, returning - * the best match when true, otherwise `undefined`, in which - * case you should respond with 406 "Not Acceptable". - * - * The `type` value may be a single MIME type string - * such as "application/json", an extension name - * such as "json", a comma-delimited list such as "json, html, text/plain", - * an argument list such as `"json", "html", "text/plain"`, - * or an array `["json", "html", "text/plain"]`. When a list - * or array is given, the _best_ match, if any is returned. - * - * Examples: - * - * // Accept: text/html - * req.accepts('html'); - * // => "html" - * - * // Accept: text/*, application/json - * req.accepts('html'); - * // => "html" - * req.accepts('text/html'); - * // => "text/html" - * req.accepts('json, text'); - * // => "json" - * req.accepts('application/json'); - * // => "application/json" - * - * // Accept: text/*, application/json - * req.accepts('image/png'); - * req.accepts('png'); - * // => undefined - * - * // Accept: text/*;q=.5, application/json - * req.accepts(['html', 'json']); - * req.accepts('html', 'json'); - * req.accepts('html, json'); - * // => "json" - */ - accepts(...args: ([string, ...string[]])[]): string|boolean|any[]; - - /** - * Check if the given `encoding`s are accepted. - */ - acceptsEncodings(...encoding: string[]): string|any[]; - - /** - * Check if the given `charset`s are acceptable, - * otherwise you should respond with 406 "Not Acceptable". - */ - acceptsCharsets(...charset: string[]): string|any[]; - - /** - * Check if the given `lang`s are acceptable, - * otherwise you should respond with 406 "Not Acceptable". - */ - acceptsLanguages(...langs: string[]): string|any[]; - - /** - * Parse Range header field, capping to the given `size`. - * - * Unspecified ranges such as "0-" require knowledge of your resource length. In - * the case of a byte range this is of course the total number of bytes. If the - * Range header field is not given `undefined` is returned, `-1` when unsatisfiable, - * and `-2` when syntactically invalid. - * - * When ranges are returned, the array has a "type" property which is the type of - * range that is required (most commonly, "bytes"). Each array element is an object - * with a "start" and "end" property for the portion of the range. - * - * The "combine" option can be set to `true` and overlapping & adjacent ranges - * will be combined into a single range. - * - * NOTE: remember that ranges are inclusive, so for example "Range: users=0-3" - * should respond with 4 users when available, not 3. - */ - range(size: number, options?: parseRange.Options): parseRange.Result | parseRange.Ranges; - - /** - * Return the value of param `name` when present or `defaultValue`. - * - * - Checks route placeholders, ex: _/user/:id_ - * - Checks body params, ex: id=12, {"id":12} - * - Checks query string params, ex: ?id=12 - * - * To utilize request bodies, `req.body` - * should be an object. This can be done by using - * the `bodyParser()` middleware. - */ - - param(name: string, ...args: unknown[]): string; - - /** - * Check if the incoming request contains the "Content-Type" - * header field, and it contains the given mime `type`. - * - * Examples: - * - * // With Content-Type: text/html; charset=utf-8 - * req.is('html'); - * req.is('text/html'); - * req.is('text/*'); - * // => true - * - * // When Content-Type is application/json - * req.is('json'); - * req.is('application/json'); - * req.is('application/*'); - * // => true - * - * req.is('html'); - * // => false - * - */ - is(args: (string[])|string): boolean|string; -} - -req.get = req.header = function header(name) { - if (!name) { - throw new TypeError('name argument is required to req.get'); - } - - if (typeof name !== 'string') { - throw new TypeError('name must be a string to req.get'); - } - - const lc = name.toLowerCase(); - - switch (lc) { - case 'referer': - case 'referrer': - return this.headers.referrer - || this.headers.referer; - default: - return this.headers[lc]; - } -}; - - - -req.accepts = function(){ - const accept = accepts(this); - return accept.types.apply(accept, arguments); -}; - -req.acceptsEncodings = function(){ - const accept = accepts(this); - return accept.encodings.apply(accept, arguments); -}; - -req.acceptsCharsets = function(){ - const accept = accepts(this); - return accept.charsets.apply(accept, arguments); -}; - -req.acceptsLanguages = function(){ - const accept = accepts(this); - return accept.languages.apply(accept, arguments); -}; - -req.range = function range(size, options) { - const range = this.get("Range"); - if (!range) return undefined; - return parseRange(size, range, options); -}; - -req.param = function param(name, defaultValue) { - // const params = this.params || {}; - const body = this.body || {}; - const query = this.query || {}; - - // const args = arguments.length === 1 ? 'name' : 'name, default'; - if (null != body[name]) return body[name]; - if (null != query[name]) return query[name]; - - return defaultValue; -}; - -req.is = function is(types) { - let arr = types; - // support flattened arguments - if (!Array.isArray(types)) { - arr = new Array(arguments.length); - for (let i = 0; i < arr.length; i++) { - arr[i] = arguments[i]; - } - } - - return typeis(this, arr as any[]); -}; - -/** - * Return the protocol string "http" or "https" - * when requested with TLS. When the "trust proxy" - * setting trusts the socket address, the - * "X-Forwarded-Proto" header field will be trusted - * and used if present. - * - * If you're running behind a reverse proxy that - * supplies https for you this may be enabled. - * - * @return {String} - * @public - */ - -defineGetter(req, "protocol", function protocol() { - const proto = (this.socket || this.connection)["encrypted"] ? "https" : "http"; - const trust = this["app"].set('trust proxy fn'); - - if (!trust((this.socket || this.connection).remoteAddress, 0)) { - return proto; - } - - // Note: X-Forwarded-Proto is normally only ever a - // single value, but this is to be safe. - const header = this.get('X-Forwarded-Proto') || proto - const index = header.indexOf(',') - - return index !== -1 - ? header.substring(0, index).trim() - : header.trim() -}); - -/** - * Short-hand for: - * - * req.protocol === 'https' - * - * @return {Boolean} - * @public - */ - -defineGetter(req, 'secure', function secure() { - return this.protocol === 'https'; -}); - -/** - * Return the remote address from the trusted proxy. - * - * The is the remote address on the socket unless - * "trust proxy" is set. - * - * @return {String} - * @public - */ - -defineGetter(req, 'ip', function ip() { - const trust = this["app"].set('trust proxy fn'); - return proxyaddr(this, trust); -}); - -/** - * When "trust proxy" is set, trusted proxy addresses + client. - * - * For example if the value were "client, proxy1, proxy2" - * you would receive the array `["client", "proxy1", "proxy2"]` - * where "proxy2" is the furthest down-stream and "proxy1" and - * "proxy2" were trusted. - * - * @return {Array} - * @public - */ - -defineGetter(req, 'ips', function ips() { - const trust = this["app"].set('trust proxy fn'); - const addrs = proxyaddr.all(this, trust); - - // reverse the order (to farthest -> closest) - // and remove socket address - addrs.reverse().pop() - - return addrs -}); - -/** - * Return subdomains as an array. - * - * Subdomains are the dot-separated parts of the host before the main domain of - * the app. By default, the domain of the app is assumed to be the last two - * parts of the host. This can be changed by setting "subdomain offset". - * - * For example, if the domain is "tobi.ferrets.example.com": - * If "subdomain offset" is not set, req.subdomains is `["ferrets", "tobi"]`. - * If "subdomain offset" is 3, req.subdomains is `["tobi"]`. - * - * @return {Array} - * @public - */ - -defineGetter(req, 'subdomains', function subdomains() { - const hostname = this.hostname; - - if (!hostname) return []; - - const offset = this["app"].set('subdomain offset'); - const subdomains = !isIP(hostname) - ? hostname.split('.').reverse() - : [hostname]; - - return subdomains.slice(offset); -}); - -/** - * Short-hand for `url.parse(req.url).pathname`. - * - * @return {String} - * @public - */ - -defineGetter(req, 'path', function path() { - return parse(this.url).pathname; -}); - -/** - * Parse the "Host" header field to a hostname. - * - * When the "trust proxy" setting trusts the socket - * address, the "X-Forwarded-Host" header field will - * be trusted. - * - * @return {String} - * @public - */ - -defineGetter(req, 'hostname', function hostname(){ - let trust = this["app"].set('trust proxy fn'), host = this.get('X-Forwarded-Host'); - - if (!host || !trust(this.connection.remoteAddress, 0)) host = this.get('Host'); - else if (host.indexOf(',') !== -1) { - // Note: X-Forwarded-Host is normally only ever a - // single value, but this is to be safe. - host = host.substring(0, host.indexOf(',')).trimRight() - } - - if (!host) return undefined; - - // IPv6 literal support - const offset = host[0] === '[' ? host.indexOf(']') + 1 : 0; - const index = host.indexOf(':', offset); - - return index !== -1 ? host.substring(0, index) : host; -}); - -/** - * Check if the request was an _XMLHttpRequest_. - * - * @return {Boolean} - * @public - */ - -defineGetter(req, "xhr", function xhr() { - const val = String(this.get("X-Requested-With") || ""); - return val.toLowerCase() === "xmlhttprequest"; -}); \ No newline at end of file diff --git a/src/response.ts b/src/response.ts deleted file mode 100644 index 48013f5..0000000 --- a/src/response.ts +++ /dev/null @@ -1,946 +0,0 @@ -import contentDisposition from "content-disposition"; -import cookie from "cookie"; -import encodeUrl from "encodeurl"; -import escapeHtml from "escape-html"; -import http from "http"; -import createError from "http-errors"; -import { extname, resolve } from "path"; -import send, { mime } from "send"; -import statuses from "statuses"; -import { finished } from "stream"; -import vary from "vary"; -import { Request } from "./request"; -import { isAbsolute, normalizeType, normalizeTypes, setCharset } from "./utils"; - -export const res: Response = Object.create(http.ServerResponse.prototype); -export interface Response extends http.ServerResponse { - req: Request; - - /** set Status code */ - status(code: number): this; - - /** - * Redirect to the given `url` with optional response `status` - * defaulting to 302. - * - * The resulting `url` is determined by `res.location()`, so - * it will play nicely with mounted apps, relative paths, - * `"back"` etc. - * - * Examples: - * - * res.redirect('/foo/bar'); - * res.redirect('http://example.com'); - * res.redirect(new URL('/test', 'http://example.com')); - * res.status(301).redirect('http://example.com'); - * res.redirect('../login'); // /blog/post/1 -> /blog/login - */ - redirect(url: string|URL): void; - - /** - * Send a response. - * - * Examples: - * - * res.send(Buffer.from('wahoo')); - * res.send({ some: 'json' }); - * res.send('

some html

'); - */ - send(body: any): this; - - /** - * Send JSON response. - * - * Examples: - * - * res.json(null); - * res.json({ user: 'tj' }); - */ - json(body: any): this; - - /** - * Send JSON response with JSONP callback support. - * - * Examples: - * - * res.jsonp(null); - * res.jsonp({ user: 'tj' }); - */ - jsonp(body: any): this; - - /** - * Send given HTTP status code. - * - * Sets the response status to `statusCode` and the body of the - * response to the standard description from node's http.STATUS_CODES - * or the statusCode number if no description. - * - * Examples: - * - * res.sendStatus(200); - * - * @param {number} statusCode - * @public - */ - sendStatus(statusCode: number): this; - - /** - * Transfer the file at the given `path`. - * - * Automatically sets the _Content-Type_ response header field. - * The callback `callback(err)` is invoked when the transfer is complete - * or when an error occurs. Be sure to check `res.headersSent` - * if you wish to attempt responding, as the header and some data - * may have already been transferred. - * - * Options: - * - * - `maxAge` defaulting to 0 (can be string converted by `ms`) - * - `root` root directory for relative filenames - * - `headers` object of headers to serve with file - * - `dotfiles` serve dotfiles, defaulting to false; can be `"allow"` to send them - * - * Other options are passed along to `send`. - * - * Examples: - * - * The following example illustrates how `res.sendFile()` may - * be used as an alternative for the `static()` middleware for - * dynamic situations. The code backing `res.sendFile()` is actually - * the same code, so HTTP cache support etc is identical. - * - * app.set('/user/:uid/photos/:file', function(req, res){ - * const uid = req.params.uid - * , file = req.params.file; - * - * req.user.mayViewFilesFrom(uid, function(yes){ - * if (yes) { - * res.sendFile('/uploads/' + uid + '/' + file); - * } else { - * res.send(403, 'Sorry! you cant see that.'); - * } - * }); - * }); - * - * @public - */ - sendFile(path: string, options?: any, callback?: (err?: any) => void): void; - - /** - * Transfer the file at the given `path` as an attachment. - * - * Optionally providing an alternate attachment `filename`, - * and optional callback `callback(err)`. The callback is invoked - * when the data transfer is complete, or when an error has - * occurred. Be sure to check `res.headersSent` if you plan to respond. - * - * Optionally providing an `options` object to use with `res.sendFile()`. - * This function will set the `Content-Disposition` header, overriding - * any `Content-Disposition` header passed as header options in order - * to set the attachment and filename. - * - * This method uses `res.sendFile()`. - * - * @public - */ - download(path: string, filename?: string, options?: any, callback?: (err?: any) => void): void; - - /** - * Set _Content-Type_ response header with `type` through `mime.lookup()` - * when it does not contain "/", or set the Content-Type to `type` otherwise. - * - * Examples: - * - * res.type('.html'); - * res.type('html'); - * res.type('json'); - * res.type('application/json'); - * res.type('png'); - */ - contentType(type: string): this; - - /** - * Set _Content-Type_ response header with `type` through `mime.lookup()` - * when it does not contain "/", or set the Content-Type to `type` otherwise. - * - * Examples: - * - * res.type('.html'); - * res.type('html'); - * res.type('json'); - * res.type('application/json'); - * res.type('png'); - */ - type(type: string): this; - - /** - * Respond to the Acceptable formats using an `obj` - * of mime-type callbacks. - * - * This method uses `req.accepted`, an array of - * acceptable types ordered by their quality values. - * When "Accept" is not present the _first_ callback - * is invoked, otherwise the first match is used. When - * no match is performed the server responds with - * 406 "Not Acceptable". - * - * Content-Type is set for you, however if you choose - * you may alter this within the callback using `res.type()` - * or `res.set('Content-Type', ...)`. - * - * res.format({ - * 'text/plain': function(){ - * res.send('hey'); - * }, - * - * 'text/html': function(){ - * res.send('

hey

'); - * }, - * - * 'application/json': function () { - * res.send({ message: 'hey' }); - * } - * }); - * - * In addition to canonicalized MIME types you may - * also use extnames mapped to these types: - * - * res.format({ - * text: function(){ - * res.send('hey'); - * }, - * - * html: function(){ - * res.send('

hey

'); - * }, - * - * json: function(){ - * res.send({ message: 'hey' }); - * } - * }); - * - * By default Express passes an `Error` - * with a `.status` of 406 to `next(err)` - * if a match is not made. If you provide - * a `.default` callback it will be invoked - * instead. - * - * @param {Object} obj - * @return {ServerResponse} for chaining - * @public - */ - format(arg0: Record): this; - - /** - * Set _Content-Disposition_ header to _attachment_ with optional `filename`. - */ - attachment(filename: string): this; - - /** - * Get value for header `field`. - */ - get(name: string): string; - - /** - * Set header `field` to `val`, or pass - * an object of header fields. - * - * Examples: - * - * res.set('Foo', ['bar', 'baz']); - * res.set('Accept', 'application/json'); - * res.set({ Accept: 'text/plain', 'X-API-Key': 'tobi' }); - * - * Aliased as `res.header()`. - * - * @param {String|Object} field - */ - set(field: string, value: string|string[]): this; - - /** - * Set header `field` to `val`, or pass - * an object of header fields. - * - * Examples: - * - * res.set('Foo', ['bar', 'baz']); - * res.set('Accept', 'application/json'); - * res.set({ Accept: 'text/plain', 'X-API-Key': 'tobi' }); - * - * Aliased as `res.header()`. - */ - set(headers: Record): this; - - /** - * Set header `field` to `val`, or pass - * an object of header fields. - * - * Examples: - * - * res.set('Foo', ['bar', 'baz']); - * res.set('Accept', 'application/json'); - * res.set({ Accept: 'text/plain', 'X-API-Key': 'tobi' }); - * - * Aliased as `res.header()`. - * - * @param {String|Object} field - */ - header(field: string, value: string|string[]): this; - - /** - * Set header `field` to `val`, or pass - * an object of header fields. - * - * Examples: - * - * res.set('Foo', ['bar', 'baz']); - * res.set('Accept', 'application/json'); - * res.set({ Accept: 'text/plain', 'X-API-Key': 'tobi' }); - * - * Aliased as `res.header()`. - */ - header(headers: Record): this; - - /** - * Append additional header `field` with value `val`. - * - * Example: - * - * res.append('Link', ['', '']); - * res.append('Set-Cookie', 'foo=bar; Path=/; HttpOnly'); - * res.append('Warning', '199 Miscellaneous warning'); - * - * @param {String} field - * @param {String|Array} val - * @return {ServerResponse} for chaining - * @public - */ - append(field: string, val: string|string[]): this; - - /** - * Set the location header to `url`. - * - * The given `url` can also be "back", which redirects - * to the _Referrer_ or _Referer_ headers or "/". - * - * Examples: - * - * res.location('/foo/bar').; - * res.location('http://example.com'); - * res.location('../login'); - */ - location(url: string|URL): this; - - /** - * Add `field` to Vary. If already present in the Vary set, then - * this call is simply ignored. - */ - vary(field: string|string[]): this; - - /** - * Set Link header field with the given `links`. - * - * Examples: - * - * res.links({ - * next: 'http://api.example.com/users?page=2', - * last: 'http://api.example.com/users?page=5' - * }); - * - * @param {Object} links - * @return {ServerResponse} - * @public - */ - links(links: Record): this; - - setCookie(name: string, value?: string, options?: cookie.CookieSerializeOptions): this; -} - -const charsetRegExp = /;\s*charset\s*=/; - -res.setCookie = function setCookie(this: Response, name, value, opt) { - const cookieMap = new Map(); - const cookieHead = this.getHeader("Set-Cookie") || []; - const RfcKeys = [ "Expires", "Max-Age", "Domain", "Path", "Secure", "HttpOnly", "SameSite" ]; - if (typeof cookieHead === "string") { - const ck = cookie.parse(cookieHead); - const dd = Object.keys(ck).reduce((acc, k) => { - if (!!(RfcKeys.find(c => c.toLowerCase() === k.toLowerCase()))) acc.settings[k] = ck[k]; - else acc.values[k] = ck[k]; - return acc; - }, { - values: {} as Record, - settings: {} as cookie.CookieSerializeOptions - }); - Object.keys(dd.values).forEach(k => cookieMap.set(k, [ dd.values[k], dd.settings ])); - } else if (Array.isArray(cookieHead)) { - cookieHead.forEach(cookieHead => { - const ck = cookie.parse(cookieHead); - const dd = Object.keys(ck).reduce((acc, k) => { - if (!!(RfcKeys.find(c => c.toLowerCase() === k.toLowerCase()))) acc.settings[k] = ck[k]; - else acc.values[k] = ck[k]; - return acc; - }, { - values: {} as Record, - settings: {} as cookie.CookieSerializeOptions - }); - Object.keys(dd.values).forEach(k => cookieMap.set(k, [ dd.values[k], dd.settings ])); - }); - } - cookieMap.set(name, [ value||"", opt ]); - this.setHeader("Set-Cookie", Array.from(cookieMap.keys()).map(k => { - const [ value, options ] = cookieMap.get(k); - if (!value) return cookie.serialize(k, value, { expires: new Date(0), maxAge: 0 }); - return cookie.serialize(k, value, options); - })) - return this; -} - -res.status = function status(code) { - if ((typeof code === "string" || Math.floor(code) !== code) && code > 99 && code < 1000) throw new Error("res.status(" + JSON.stringify(code) + "): use res.status(" + Math.floor(code) + ") instead"); - this.statusCode = code; - return this; -}; - -res.links = function(links){ - let link = this.get("Link") || ""; - if (link) link += ", "; - return this.set("Link", link + Object.keys(links).map((rel) => "<" + links[rel] + ">; rel=\"' + rel + '\"").join(", ")); -}; - -res.send = function send(body) { - let chunk = body; - let encoding; - let req = this.req; - let type; - - // settings - const app = this.app; - - // allow status / body - if (arguments.length === 2) { - // res.send(body, status) backwards compat - if (typeof arguments[0] !== 'number' && typeof arguments[1] === 'number') { - throw new Error('res.send(body, status): Use res.status(status).send(body) instead'); - this.statusCode = arguments[1]; - } else { - throw new Error('res.send(status, body): Use res.status(status).send(body) instead'); - this.statusCode = arguments[0]; - chunk = arguments[1]; - } - } - - // disambiguate res.send(status) and res.send(status, num) - if (typeof chunk === 'number' && arguments.length === 1) { - // res.send(status) will set status message as text string - if (!this.get('Content-Type')) { - this.type('txt'); - } - - throw new Error('res.send(status): Use res.sendStatus(status) instead'); - this.statusCode = chunk; - chunk = statuses.message[chunk] - } - - switch (typeof chunk) { - // string defaulting to html - case 'string': - if (!this.get('Content-Type')) { - this.type('html'); - } - break; - case 'boolean': - case 'number': - case 'object': - if (chunk === null) { - chunk = ''; - } else if (Buffer.isBuffer(chunk)) { - if (!this.get('Content-Type')) { - this.type('bin'); - } - } else { - return this.json(chunk); - } - break; - } - - // write strings in utf-8 - if (typeof chunk === 'string') { - encoding = 'utf8'; - type = this.get('Content-Type'); - - // reflect this in content-type - if (typeof type === 'string') { - this.set('Content-Type', setCharset(type, 'utf-8')); - } - } - - // determine if ETag should be generated - const etagFn = app.set('etag fn') - const generateETag = !this.get('ETag') && typeof etagFn === 'function' - - // populate Content-Length - let len - if (chunk !== undefined) { - if (Buffer.isBuffer(chunk)) { - // get length of Buffer - len = chunk.length - } else if (!generateETag && chunk.length < 1000) { - // just calculate length when no ETag + small chunk - len = Buffer.byteLength(chunk, encoding) - } else { - // convert chunk to Buffer and calculate - chunk = Buffer.from(chunk, encoding) - encoding = undefined; - len = chunk.length - } - - this.set('Content-Length', len); - } - - // freshness - if (req.fresh) this.statusCode = 304; - - // strip irrelevant headers - if (204 === this.statusCode || 304 === this.statusCode) { - this.removeHeader('Content-Type'); - this.removeHeader('Content-Length'); - this.removeHeader('Transfer-Encoding'); - chunk = ''; - } - - // alter headers for 205 - if (this.statusCode === 205) { - this.set('Content-Length', '0') - this.removeHeader('Transfer-Encoding') - chunk = '' - } - - if (req.method === 'HEAD') { - // skip body for HEAD - this.end(); - } else { - // respond - this.end(chunk, encoding); - } - - return this; -}; - -res.json = function json(obj) { - let val = obj; - - // allow status / body - if (arguments.length === 2) { - if (typeof arguments[1] === "number") throw new Error("res.json(obj, status): Use res.status(status).json(obj) instead"); - else throw new Error("res.json(status, obj): Use res.status(status).json(obj) instead"); - } - - // settings - const app = this.app; - const escape = app.set("json escape") - const replacer = app.set("json replacer"); - const spaces = app.set("json spaces"); - const body = stringify(val, replacer, spaces, escape); - - // content-type - if (!(this.get("Content-Type"))) this.set("Content-Type", "application/json"); - - return this.send(body); -}; - -res.jsonp = function jsonp(obj) { - let val = obj; - - // allow status / body - if (arguments.length === 2) { - if (typeof arguments[1] === "number") throw new Error("res.jsonp(obj, status): Use res.status(status).jsonp(obj) instead"); - else throw new Error("res.jsonp(status, obj): Use res.status(status).jsonp(obj) instead"); - } - - // settings - const app = this.app; - const escape = app.set("json escape") - const replacer = app.set("json replacer"); - const spaces = app.set("json spaces"); - let body = stringify(val, replacer, spaces, escape) - let callback = this.req.query[app.set("jsonp callback name")]; - - // content-type - if (!(this.get("Content-Type"))) { - this.set("X-Content-Type-Options", "nosniff"); - this.set("Content-Type", "application/json"); - } - - // fixup callback - if (Array.isArray(callback)) callback = callback[0]; - - // jsonp - if (typeof callback === "string" && callback.length !== 0) { - this.set("X-Content-Type-Options", "nosniff"); - this.set("Content-Type", "text/javascript"); - - // restrict callback charset - callback = callback.replace(/[^\[\]\w$.]/g, ""); - - if (body === undefined) { - // empty argument - body = "" - } else if (typeof body === "string") { - // replace chars not allowed in JavaScript that are in JSON - body = body.replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029"); - } - - // the /**/ is a specific security mitigation for "Rosetta Flash JSONP abuse" - // the typeof check is just to reduce client error noise - body = "/**/ typeof " + callback + " === \'function\" && " + callback + "(' + body + ');"; - } - - return this.send(body); -}; - -res.sendStatus = function sendStatus(statusCode) { - const body = statuses.message[statusCode] || String(statusCode) - this.statusCode = statusCode; - this.type("txt"); - return this.send(body); -}; - -res.sendFile = function sendFile(path, options, callback) { - let done = callback; - let req = this.req; - let res = this; - let next = req.next; - let opts = options || {}; - - if (!path) throw new TypeError("path argument is required to res.sendFile"); - - if (typeof path !== "string") throw new TypeError("path must be a string to res.sendFile") - - // support function as second arg - if (typeof options === "function") { - done = options; - opts = {}; - } - - if (!opts.root && !isAbsolute(path)) throw new TypeError("path must be absolute or specify root to res.sendFile"); - - // create file stream - const pathname = encodeURI(path); - const file = send(req, pathname, opts); - - // transfer - sendfile(res, file, opts, function (err) { - if (done) return done(err); - if (err && err.code === "EISDIR") return next(); - - // next() all but write errors - if (err && err.code !== "ECONNABORTED" && err.syscall !== "write") next(err); - }); -}; - -res.download = function download(path, filename, options, callback) { - let done = callback; - let name = filename; - let opts = options || null - - // support function as second or third arg - if (typeof filename === "function") { - done = filename; - name = null; - opts = null - } else if (typeof options === "function") { - done = options - opts = null - } - - // support optional filename, where options may be in it's place - if (typeof filename === "object" && (typeof options === "function" || options === undefined)) { - name = null - opts = filename - } - - // set Content-Disposition when file is sent - const headers = { - "Content-Disposition": contentDisposition(name || path) - }; - - // merge user-provided headers - if (opts && opts.headers) { - const keys = Object.keys(opts.headers) - for (let i = 0; i < keys.length; i++) { - const key = keys[i] - if (key.toLowerCase() !== 'content-disposition') { - headers[key] = opts.headers[key] - } - } - } - - // merge user-provided options - opts = Object.create(opts) - opts.headers = headers - - // Resolve the full path for sendFile - const fullPath = !opts.root - ? resolve(path) - : path - - // send file - return this.sendFile(fullPath, opts, done) -}; - -res.contentType = res.type = function contentType(type) { - const ct = type.indexOf('/') === -1 - ? mime.lookup(type) - : type; - - return this.set('Content-Type', ct); -}; - -res.format = function(obj){ - const req = this.req; - const next = req.next; - - const keys = Object.keys(obj).filter(function (v) { return v !== 'default' }) - const key = keys.length > 0 ? req.accepts(keys) : false; - this.vary("Accept"); - - if (key) { - this.set('Content-Type', normalizeType(key).value); - obj[key](req, this, next); - } else if (obj.default) { - obj.default(req, this, next) - } else { - next(createError(406, { - types: normalizeTypes(keys).map(function (o) { return o.value }) - })); - } - - return this; -}; - -res.attachment = function attachment(filename) { - if (filename) { - this.type(extname(filename)); - } - - this.set('Content-Disposition', contentDisposition(filename)); - - return this; -}; - -res.append = function append(field, val) { - let prev = this.get(field), value = val; - - if (prev) { - // concat the new and prev vals - value = Array.isArray(prev) ? prev.concat(val) - : Array.isArray(val) ? [prev].concat(val) - : [prev, val] - } - - return this.set(field, value); -}; - -res.set = res.header = function header(field: string|Record, val?: string|string[]) { - if (!(typeof field === "string" && (typeof val === "string" || Array.isArray(val)))) Object.keys(field).forEach(key => this.set(key, field[key])); - else { - let value = Array.isArray(val) ? val.map(String) : String(val); - - // add charset to content-type - if (field.toLowerCase() === "content-type") { - if (Array.isArray(value)) throw new TypeError("Content-Type cannot be set to an Array"); - if (!charsetRegExp.test(value)) { - const charset = mime.charsets.lookup(value.split(";")[0], ""); - if (charset) value += "; charset=" + charset.toLowerCase(); - } - } - - this.setHeader(field, value); - } - return this; -}; - -res.get = function(field){ - return this.getHeader(field); -}; - -res.location = function location(url) { - let loc = url; - - // "back" is an alias for the referrer - if (url === 'back') loc = this.req.get('Referrer') || '/'; - if (loc instanceof URL) loc = loc.toString(); - - // set location - return this.set('Location', encodeUrl(loc)); -}; - -res.redirect = function redirect(this: Response, url) { - let address = url, body, status = 302; - - // allow status / url - if (arguments.length === 2) { - if (typeof arguments[0] === 'number') { - status = arguments[0]; - address = arguments[1]; - } else throw new Error('res.redirect(url, status): Use res.redirect(status, url) instead'); - } - - // Set location header - if (address instanceof URL) address = address.toString(); - address = this.location(address).get('Location'); - - // Support text/{plain,html} by default - this.format({ - text: function(){ - body = statuses.message[status] + '. Redirecting to ' + address - }, - html: function(){ - const u = escapeHtml(String(address)); - body = "

" + statuses.message[status] + ". Redirecting to " + u + "

" - }, - default: function(){ - body = ""; - } - }); - - // Respond - this.statusCode = status; - this.set("Content-Length", String(Buffer.byteLength(body))); - - if (this.req.method.toLowerCase() === "head") this.end(); - else this.end(body); -}; - -res.vary = function(field){ - // checks for back-compat - if (!field || (Array.isArray(field) && !field.length)) throw new Error("res.vary(): Provide a field name"); - vary(this, field); - return this; -}; - -// pipe the send file stream -function sendfile(res, file, options, callback) { - let done = false, streaming; - - // request aborted - function onaborted() { - if (done) return; - done = true; - - const err: any = new Error('Request aborted'); - err.code = 'ECONNABORTED'; - callback(err); - } - - // directory - function ondirectory() { - if (done) return; - done = true; - - const err: any = new Error('EISDIR, read'); - err.code = 'EISDIR'; - callback(err); - } - - // errors - function onerror(err) { - if (done) return; - done = true; - callback(err); - } - - // ended - function onend() { - if (done) return; - done = true; - callback(); - } - - // file - function onfile() { - streaming = false; - } - - // finished - function onfinish(err) { - if (err && err.code === 'ECONNRESET') return onaborted(); - if (err) return onerror(err); - if (done) return; - - setImmediate(function () { - if (streaming !== false && !done) { - onaborted(); - return; - } - - if (done) return; - done = true; - callback(); - }); - } - - // streaming - function onstream() { - streaming = true; - } - - file.on('directory', ondirectory); - file.on('end', onend); - file.on('error', onerror); - file.on('file', onfile); - file.on('stream', onstream); - finished(res, onfinish); - - if (options.headers) { - // set headers on successful transfer - file.on('headers', function headers(res) { - const obj = options.headers; - const keys = Object.keys(obj); - - for (let i = 0; i < keys.length; i++) { - const k = keys[i]; - res.setHeader(k, obj[k]); - } - }); - } - - // pipe - file.pipe(res); -} - -/** - * Stringify JSON, like JSON.stringify, but v8 optimized, with the - * ability to escape characters that can trigger HTML sniffing. - * - * @param {*} value - * @param {function} replacer - * @param {number} spaces - * @param {boolean} escape - * @returns {string} - * @private - */ - -function stringify(value, replacer, spaces, escape) { - // v8 checks arguments.length for optimizing simple call - // https://bugs.chromium.org/p/v8/issues/detail?id=4730 - let json = replacer || spaces ? JSON.stringify(value, replacer, spaces) : JSON.stringify(value); - if (escape && typeof json === "string") { - json = json.replace(/[<>&]/g, function (c) { - switch (c.charCodeAt(0)) { - case 0x3c: - return "\\u003c" - case 0x3e: - return "\\u003e" - case 0x26: - return "\\u0026" - /* istanbul ignore next: unreachable default */ - default: - return c - } - }); - } - - return json -} \ No newline at end of file diff --git a/src/util.ts b/src/util.ts new file mode 100644 index 0000000..36edb81 --- /dev/null +++ b/src/util.ts @@ -0,0 +1,32 @@ +const __methods = [ "ws", "get", "post", "put", "delete", "head", "connect", "options", "trace" ] as const; +export type Methods = typeof __methods[number]; +export const methods: Methods[] = Object.freeze(__methods) as any; + +/** + * Merge the property descriptors of `src` into `dest` + * + * @param dest Object to add descriptors to + * @param src Object to clone descriptors from + * @param {boolean} [redefine=true] Redefine `dest` properties with `src` properties + */ +export function mixin(dest: T, src: C, redefine?: boolean): T & C { + if (!dest) throw new TypeError('argument dest is required'); + if (!src) throw new TypeError('argument src is required'); + if (redefine === undefined) redefine = true; // Default to true + + Object.getOwnPropertyNames(src).forEach(function forEachOwnPropertyName(name) { + // Skip desriptor + if (!redefine && Object.hasOwnProperty.call(dest, name)) return; + + // Copy descriptor + var descriptor = Object.getOwnPropertyDescriptor(src, name); + Object.defineProperty(dest, name, descriptor); + }); + + return dest as any; +} + +export function defineProperties(obj: any, config: Record>) { + for (let key in config) Object.defineProperty(obj, key, config[key]); + return obj; +} \ No newline at end of file diff --git a/src/utils.ts b/src/utils.ts deleted file mode 100644 index bf9f41b..0000000 --- a/src/utils.ts +++ /dev/null @@ -1,277 +0,0 @@ -import contentDisposition from "content-disposition"; -import proxyaddr from "proxy-addr"; -import contentType from "content-type"; -import qs from "qs"; -import { mime } from "send"; -export { contentDisposition }; - -const __methods = [ "ws", "get", "post", "put", "delete", "head", "connect", "options", "trace" ] as const; -export type Methods = typeof __methods[number]; -export const methods: Methods[] = Object.freeze(__methods) as any; - -function setProtoOf(obj: any, proto: any) { - obj.__proto__ = proto; - return obj; -} - -function mixinProperties(obj, proto) { - for (var prop in proto) { if (!Object.prototype.hasOwnProperty.call(obj, prop)) { obj[prop] = proto[prop]; } } - return obj; -} - -export const setPrototypeOf = Object.setPrototypeOf || ({ __proto__: [] } instanceof Array ? setProtoOf : mixinProperties); -export function merge(a, b){ - if (a && b) { - for (var key in b) { - a[key] = b[key]; - } - } - return a; -} - -/*! - * merge-descriptors - * Copyright(c) 2014 Jonathan Ong - * Copyright(c) 2015 Douglas Christopher Wilson - * MIT Licensed - */ -var hasOwnProperty = Object.prototype.hasOwnProperty - -/** - * Merge the property descriptors of `src` into `dest` - * - * @param dest Object to add descriptors to - * @param src Object to clone descriptors from - * @param {boolean} [redefine=true] Redefine `dest` properties with `src` properties - */ -export function mixin(dest: T, src: C, redefine?: boolean): T & C { - if (!dest) throw new TypeError('argument dest is required'); - if (!src) throw new TypeError('argument src is required'); - if (redefine === undefined) redefine = true; // Default to true - - Object.getOwnPropertyNames(src).forEach(function forEachOwnPropertyName(name) { - // Skip desriptor - if (!redefine && hasOwnProperty.call(dest, name)) return; - - // Copy descriptor - var descriptor = Object.getOwnPropertyDescriptor(src, name); - Object.defineProperty(dest, name, descriptor); - }); - - return dest as any; -} - -/** - * Check if `path` looks absolute. - * - * @param {String} path - * @return {Boolean} - * @api private - */ - -export function isAbsolute(path){ - if ('/' === path[0]) return true; - if (':' === path[1] && ('\\' === path[2] || '/' === path[2])) return true; // Windows device path - if ('\\\\' === path.substring(0, 2)) return true; // Microsoft Azure absolute path - return false; -}; - -type PickValue = T extends ReadonlyArray ? { [K in Extract]: PickValue; }[number] : T; -type FlatArray> = Array>; -export function flatten(args: T): FlatArray { - return Array.from(args).flat(Infinity); -}; - -/** - * Helper function for creating a getter on an object. - * - * @param {Object} obj - * @param {String} name - * @param {Function} getter - * @private - */ -export function defineGetter, C extends string>(obj: T, name: C, getter: (this: T, ...args: Parameters) => void) { - Object.defineProperty(obj, name, { - configurable: true, - enumerable: true, - get: getter - }); -} - -/** - * Normalize the given `type`, for example "html" becomes "text/html". - * - * @param {String} type - * @return {Object} - * @api private - */ - -export function normalizeType(type){ - return ~type.indexOf('/') - ? acceptParams(type) - : { value: mime.lookup(type), params: {} }; -}; - -/** - * Normalize `types`, for example "html" becomes "text/html". - * - * @param {Array} types - * @return {Array} - * @api private - */ - -export function normalizeTypes(types){ - const ret = []; - - for (let i = 0; i < types.length; ++i) ret.push(normalizeType(types[i])); - - return ret; -}; - -/** - * Parse accept params `str` returning an - * object with `.value`, `.quality` and `.params`. - * - * @param {String} str - * @return {Object} - * @api private - */ - -function acceptParams(str) { - const parts = str.split(/ *; */); - const ret = { value: parts[0], quality: 1, params: {} } - - for (let i = 1; i < parts.length; ++i) { - const pms = parts[i].split(/ *= */); - if ('q' === pms[0]) { - ret.quality = parseFloat(pms[1]); - } else { - ret.params[pms[0]] = pms[1]; - } - } - - return ret; -} - -/** - * Compile "query parser" value to function. - * - * @return {Function} - * @api private - */ - -export function compileQueryParser(val: string|boolean|Function) { - let fn: Function; - if (typeof val === 'function') return val; - switch (val) { - case true: - case 'simple': - fn = function(qs, sep, eq) { - sep = sep || '&'; - eq = eq || '='; - var obj = {}; - if (typeof qs !== 'string' || qs.length === 0) return obj; - qs.split(sep).forEach(function(kvp) { - var x = kvp.split(eq); - var k = decodeURIComponent(x[0]); - var v = decodeURIComponent(x.slice(1).join(eq)); - - if (!(k in obj)) obj[k] = v; - else if (!Array.isArray(obj[k])) obj[k] = [obj[k], v]; - else obj[k].push(v); - }); - - return obj; - }; - break; - case false: - fn = newObject; - break; - case 'extended': - fn = parseExtendedQueryString; - break; - default: - throw new TypeError('unknown value for query parser function: ' + val); - } - - return fn; -} - -/** - * Compile "proxy trust" value to function. - * - * @param {Boolean|String|Number|Array|Function} val - * @return {Function} - * @api private - */ - -export function compileTrust(val) { - if (typeof val === 'function') return val; - - if (val === true) { - // Support plain true/false - return function(){ return true }; - } - - if (typeof val === 'number') { - // Support trusting hop count - return function(a, i){ return i < val }; - } - - if (typeof val === 'string') { - // Support comma-separated values - val = val.split(',') - .map(function (v) { return v.trim() }) - } - - return proxyaddr.compile(val || []); -} - -/** - * Set the charset in a given Content-Type string. - * - * @param {String} type - * @param {String} charset - * @return {String} - * @api private - */ - -export function setCharset(type, charset) { - if (!type || !charset) { - return type; - } - - // parse type - const parsed = contentType.parse(type); - - // set charset - parsed.parameters.charset = charset; - - // format type - return contentType.format(parsed); -}; - -/** - * Parse an extended query string with qs. - * - * @param {String} str - * @return {Object} - * @private - */ - -function parseExtendedQueryString(str) { - return qs.parse(str, { - allowPrototypes: true - }); -} - -/** - * Return new empty object. - * - * @return {Object} - * @api private - */ - -function newObject() { - return {}; -} \ No newline at end of file diff --git a/testLocal/.gitignore b/testLocal/.gitignore deleted file mode 100644 index 009aa5a..0000000 --- a/testLocal/.gitignore +++ /dev/null @@ -1 +0,0 @@ -localUpload*/ \ No newline at end of file diff --git a/testLocal/index.cjs b/testLocal/index.cjs deleted file mode 100644 index 07c4b3e..0000000 --- a/testLocal/index.cjs +++ /dev/null @@ -1,36 +0,0 @@ -const { randomBytes } = require("crypto"); -const neste = require("../export/index.cjs"); -const path = require("path"); -const app = neste(); -app.listen(3000, () => console.log("Listen on %s", 3000)); -// app.use(({res}) => res.status(500).json({ error: ok })); -app.use(neste.staticFile(__dirname), neste.parseBody({ formData: { tmpFolder: path.join(__dirname, "localUpload") } })); -app.post("/", async (req, res) => { - console.log(req.body); - res.json({ - ok: true, - body: req.body, - headers: req.headers, - }); -}); - -app.get("/:fist", ({ params, path, fullPath }, res) => res.json({ path, fullPath, params })); -app.use("/:personName", neste.router().get("/:union/:Specie", ({ params, path, fullPath }, res) => { - // if ((["sonic", "shadown", "shadow"]).includes(params.personName.toLowerCase()) && params.Specie.toLowerCase() === "hedgehog") return res.json({ message: "You are Sonic fan" }); - res.json({ path, fullPath, params }); -})); - -app.ws("/", (req, res) => { - res.onmessage = e => { - if (e.data.length <= 0) return; - console.log("From WS: %O", e.data.toString()); - res.send(JSON.stringify({ - at: new Date(), - msg: e.data.toString(), - })); - res.send(randomBytes(8)); - }; -}); - -app.use(({res}) => res.status(404).json({ error: "Path not exists" })); -app.useError((err, req, res, next) => res.status(500).json({ error: err.message || String(err) })); \ No newline at end of file diff --git a/testLocal/index.html b/testLocal/index.html deleted file mode 100644 index 3312df1..0000000 --- a/testLocal/index.html +++ /dev/null @@ -1,115 +0,0 @@ - - - - - - Send file test - - -
-

WebSocket

-
- - - Status: Disconnected -
-
- - -
-
-
-
-
- -
-
-
-

application/x-www-form-urlencoded

-
- -
- -
- -
-
-
-
-

multipart/form-data

-
-
-
-
- - -
- Result: -
-

-      
-    
-
- - \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json index 111e553..713c647 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,7 +1,7 @@ { "compilerOptions": { "esModuleInterop": true, - "module": "CommonJS", + "module": "NodeNext", "moduleResolution": "NodeNext", "target": "ESNext", "forceConsistentCasingInFileNames": true, @@ -19,12 +19,8 @@ ] }, "exclude": [ - "export/**", - "**/*.cjs", - "**/*.mjs", - "**/*.test.ts", - "**/testLocal/**", - "**/node_modules/**" + "**/node_modules/**", + "**/*.test.*" ], "ts-node": { "files": true,