UNPKG

rexuws

Version:

An express-like framework built on top of uWebsocket.js aims at simple codebase and high performance

469 lines (468 loc) 19.4 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } }); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); __setModuleDefault(result, mod); return result; }; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); const statuses_1 = __importDefault(require("statuses")); const fs_1 = __importStar(require("fs")); const mime_types_1 = __importStar(require("mime-types")); const cookie_1 = require("cookie"); const content_disposition_1 = __importDefault(require("content-disposition")); const path_1 = require("path"); const utils_1 = require("./utils"); const symbol_1 = require("./utils/symbol"); const CONTENT_TYPE = { JSON: 'application/json; charset=utf-8', PLAIN: 'text/plain; charset=utf-8', HTML: 'text/html; charset=utf-8', OCTET: 'application/octet-stream', }; class Response { constructor(res, opts, logger) { this._headers = []; this.locals = {}; this.maxReadFileSize = 102400; // 100KB this._cookies = []; this.originalRes = res; this[symbol_1.WRITE_HEADER] = this.originalRes.writeHeader.bind(res); this[symbol_1.WRITE_STATUS] = this.originalRes.writeStatus.bind(res); this[symbol_1.ON_WRITABLE] = this.originalRes.onWritable.bind(res); this[symbol_1.GET_PROXIED_ADDR] = this.originalRes.getProxiedRemoteAddressAsText.bind(res); this[symbol_1.GET_REMOTE_ADDR] = this.originalRes.getRemoteAddressAsText.bind(res); this[symbol_1.GET_WRITE_OFFSET] = this.originalRes.getWriteOffset.bind(res); this[symbol_1.ON_ABORTED] = this.originalRes.onAborted.bind(res); this[symbol_1.ON_WRITABLE] = this.originalRes.onWritable.bind(res); this[symbol_1.CORK] = this.originalRes.cork.bind(res); this[symbol_1.END] = this.originalRes.end.bind(res); this[symbol_1.TRY_END] = this.originalRes.tryEnd.bind(res); // Set epress setHeader method this.set = this.setHeader; this.header = this.setHeader; this.getHeader = this.get; // Set express type method this.type = this.setType; this.contentType = this.setType; this.debug = logger; this.debug = logger || { ...utils_1.colorConsole, deprecate() { return utils_1.colorConsole.warn.bind(utils_1.colorConsole, '[DEPRECATED]'); }, }; if (opts) { const { hasAsync = false, maxReadFileSize = 102400 } = opts; this[symbol_1.HAS_ASYNC] = hasAsync; // if (this[HAS_ASYNC]) { // res.onAborted(() => { // res.originalRes.aborted = true; // }); // } if (isNaN(maxReadFileSize)) this.maxReadFileSize = -1; } this.attachAbortHandler(); } // GETTER get preStatus() { return this._statusCode ? +this._statusCode : undefined; } get preHeader() { return this._headers; } // *************************************************************** // PRIVATE METHOD // *************************************************************** attachAbortHandler(calledByMethod = false) { if (calledByMethod) { if (this[symbol_1.HAS_ASYNC]) return; this[symbol_1.ON_ABORTED](() => { this.originalRes.aborted = true; if (this[symbol_1.READ_STREAM]) { if (this.originalRes.id === -1) { // console.log( // "ERROR! onAbortedOrFinishedResponse called twice for the same res!" // ); } else { // console.log("Stream was closed, openStreams: " + --openStreams); // console.timeEnd(res.id); this[symbol_1.READ_STREAM].destroy(); } /* Mark this response already accounted for */ this.originalRes.id = -1; } }); return; } if (this[symbol_1.HAS_ASYNC]) { this[symbol_1.ON_ABORTED](() => { this.originalRes.aborted = true; if (this[symbol_1.READ_STREAM]) { if (this.originalRes.id === -1) { // console.log( // "ERROR! onAbortedOrFinishedResponse called twice for the same res!" // ); } else { // console.log("Stream was closed, openStreams: " + --openStreams); // console.timeEnd(res.id); this[symbol_1.READ_STREAM].destroy(); } /* Mark this response already accounted for */ this.originalRes.id = -1; } }); } } setHeaderAndStatusByNativeMethod() { if (this._statusCode) this[symbol_1.WRITE_STATUS](this._statusCode); for (let i = 0; i < this._headers.length; i++) { this[symbol_1.WRITE_HEADER](this._headers[i].name, this._headers[i].value); } } setHeader(field, val) { if (typeof field === 'string') { const lowerCaseField = field.toLowerCase(); if (lowerCaseField === 'content-type' && Array.isArray(val)) { throw new TypeError('Content-Type cannot be set to an Array'); } if (typeof val === 'string') { this._headers.push({ name: lowerCaseField, value: val, }); return this; } if (Array.isArray(val)) { val.forEach((v) => { this._headers.push({ name: lowerCaseField, value: v, }); }); return this; } return this; } Object.entries(field).forEach(([key, value]) => { const lowerCaseKey = key.toLowerCase(); if (typeof value === 'string') { this._headers.push({ name: lowerCaseKey, value }); return; } if (Array.isArray(value)) value.forEach((v) => { this._headers.push({ name: lowerCaseKey, value: v, }); }); }); return this; } setType(type) { const ct = type.indexOf('/') === -1 ? mime_types_1.default.lookup(type) : type; return this.set('Content-Type', ct); } get(field) { return this._headers.find((h) => h.name === field.toLowerCase())?.value; } getHeaders() { return this._headers; } status(code) { this._statusCode = `${code}`; return this; } sendStatus(code) { const body = `${statuses_1.default(code)}` || `${this._statusCode}`; this._statusCode = `${code}`; this.type('txt'); this.send(body); } send(body) { const type = this.get('Content-Type'); if (typeof body === 'string') { if (!type) { this.set('Content-Type', CONTENT_TYPE.HTML); } return this.end(body); } // Check if body is Buffer if (Buffer.isBuffer(body)) { if (type) this.type(CONTENT_TYPE.OCTET); return this.end(body); } return this.json(body); } json(body) { if (!this.get('Content-Type')) this.type(CONTENT_TYPE.JSON); // this._headers.push({ // name: 'content-type', // value: 'application/json;charset=utf-8', // }); this.end(body && JSON.stringify(body)); } location(url) { if (url === 'back') { const loc = this[symbol_1.FROM_REQ].get('Referrer'); if (!loc) return this.set('Location', '/'); return this.set('Location', encodeURI(loc)); } return this.set('Location', encodeURI(url)); } redirect(urlOrStatus, url) { let statusCode = 302; if (arguments.length === 2) { if (typeof urlOrStatus === 'number') statusCode = urlOrStatus; this.status(statusCode); this.location(url); const body = `<p>${statuses_1.default[statusCode]}. Redirecting to <a href=${this.get('location')}>${this.get('location')}</a></p>`; this.end(body); return; } this.status(statusCode); this.location(urlOrStatus); const body = `<p>${statuses_1.default[statusCode]}. Redirecting to <a href=${this.get('location')}>${this.get('location')}</a></p>`; this.set('Content-Length', `${Buffer.byteLength(body)}`); this.end(body); } // TODO handle when there is a range in req header sendFile(path, options, cb) { // Serve buffer data to client const ct = this.get('Content-Disposition'); if (Buffer.isBuffer(path)) { if (typeof options !== 'object' || !options.mime) { this.debug.trace('Missing Content-Type when serving file directly by buffer'); this.status(404).end(); return; } if (!ct) { this.set('Content-Type', options.mime); this.set('Last-Modified', options.lastModified); this.set('Cache-Control', options.maxAge ? `public, max-age=${options.maxAge}` : 'no-cache, no-store, must-revalidate'); } this.end(path); return; } // Read file from path if (typeof path === 'string') { const fileName = path_1.basename(path); let totalSize = -1; let mimeType = ''; let isDir = false; let lastModified = ''; try { if (typeof options === 'object') { if (options.mime) { mimeType = options.mime; } else { mimeType = mime_types_1.contentType(fileName) || 'application/octet-stream'; } if (options.fileSize) { totalSize = options.fileSize; } if (options.lastModified) { lastModified = options.lastModified; } else { const stat = fs_1.default.statSync(path); totalSize = stat.size; isDir = stat.isDirectory(); lastModified = stat.mtime.toUTCString(); } } else { mimeType = mime_types_1.contentType(fileName) || 'application/octet-stream'; const stat = fs_1.default.statSync(path); totalSize = stat.size; isDir = stat.isDirectory(); lastModified = stat.mtime.toUTCString(); } if (isDir) { this.status(404).end(); return; } if (totalSize !== -1 && totalSize <= this.maxReadFileSize) { const fileBuffer = fs_1.default.readFileSync(path); if (!ct) { this.set('Content-Type', mimeType); this.set('Last-Modified', lastModified); this.set('Cache-Control', options && typeof options === 'object' && options.maxAge ? `public, max-age=${options.maxAge}` : 'no-cache, no-store, must-revalidate'); } this.end(fileBuffer); return; } const stream = fs_1.createReadStream(path); this[symbol_1.READ_STREAM] = stream; this.attachAbortHandler(true); if (!ct) this[symbol_1.WRITE_HEADER]('Content-Type', mimeType); else this[symbol_1.WRITE_HEADER]('Content-Disposition', ct); /** * This code was taken from uWebSockets.js examples * * @see https://github.com/uNetworking/uWebSockets.js/blob/master/examples/VideoStreamer.js */ stream .on('data', (chunk) => { /* We only take standard V8 units of data */ const ab = utils_1.toArrayBuffer(chunk); /* Store where we are, globally, in our response */ const lastOffset = this[symbol_1.GET_WRITE_OFFSET](); /* Streaming a chunk returns whether that chunk was sent, and if that chunk was last */ const [ok, done] = this[symbol_1.TRY_END](ab, totalSize); /* Did we successfully send last chunk? */ if (done) { if (this.originalRes.id === -1) { this.debug.error('ERROR! onAbortedOrFinishedResponse called twice for the same res!'); } else { stream.destroy(); } /* Mark this response already accounted for */ this.originalRes.id = -1; } else if (!ok) { /* If we could not send this chunk, pause */ stream.pause(); /* Save unsent chunk for when we can send it */ this.originalRes.ab = ab; this.originalRes.ab.abOffset = lastOffset; /* Register async handlers for drainage */ this[symbol_1.ON_WRITABLE]((offset) => { /* Here the timeout is off, we can spend as much time before calling tryEnd we want to */ /* On failure the timeout will start */ const [ok, done] = this[symbol_1.TRY_END](this.originalRes.ab.slice(offset - this.originalRes.abOffset), totalSize); if (done) { if (this.originalRes.id === -1) { this.debug.error('ERROR! onAbortedOrFinishedResponse called twice for the same res!'); } else { stream.destroy(); } /* Mark this response already accounted for */ this.originalRes.id = -1; } else if (ok) { /* We sent a chunk and it was not the last one, so let's resume reading. * Timeout is still disabled, so we can spend any amount of time waiting * for more chunks to send. */ stream.resume(); } /* We always have to return true/false in onWritable. * If you did not send anything, return true for success. */ return ok; }); } }) .on('error', (err) => { this.debug.trace(err); this.status(404); this[symbol_1.END](); }); return; } catch (err) { this.debug.trace(err); this.status(404); this.end(); return; } } this.debug.trace('Invalid agurments', arguments); this.status(404); this.end(); } cookie(name, val, options) { const opts = typeof options === 'object' ? options : {}; const str = typeof val === 'object' ? `j:${JSON.stringify(val)}` : `${val}`; if (opts.maxAge) { opts.expires = new Date(Date.now() + opts.maxAge); opts.maxAge /= 1000; } if (opts.path == null) { opts.path = '/'; } this.set('set-cookie', cookie_1.serialize(name, str, opts)); return this; } download(path, fileName, options = {}) { this.setHeader('Content-Disposition', content_disposition_1.default(path || fileName)).sendFile(path); } render(view, options, callback) { let cb = callback; let opts = options; if (typeof options === 'function') { cb = options; opts = {}; } if (!cb) { // eslint-disable-next-line consistent-return cb = (err, html) => { if (err) { this.debug.trace(err); if (err instanceof ReferenceError) { this.status(404); return this.send(utils_1.toHtml(`${err.stack ?.toString() .replace(/</g, '&lt;') .replace(/>/g, '&gt;')}`)); } return this.status(500).json(err); } this.set('Content-Type', CONTENT_TYPE.HTML).end(html); }; } const { render } = this[symbol_1.FROM_APP]; if (!render) { this.status(500).end('Missing view render method'); return; } render(view, opts, cb); } end(body = '', hasAsync) { if (hasAsync || this[symbol_1.HAS_ASYNC]) { if (!this.originalRes.aborted) this[symbol_1.CORK](() => { this.setHeaderAndStatusByNativeMethod(); this[symbol_1.END](body); }); return; } this.setHeaderAndStatusByNativeMethod(); this[symbol_1.END](body); } } exports.default = Response;