UNPKG

@tunframework/tun

Version:

tun framework for node with typescript

286 lines (285 loc) 9.5 kB
import { basename, extname } from 'path'; import { HttpStatus, HttpStatusMessage } from './constants/http/HttpStatus.js'; import { RAW_REQUEST, RAW_RESPONSE } from './constants/http/symbol.js'; import { mimeExtMap } from './constants/mime.js'; export class TunResponse { [RAW_REQUEST]; [RAW_RESPONSE]; #body = null; // status: HttpStatus = HttpStatus.NOT_FOUND; // // message = // get message() { // return httpStatusMessage[this.status]; // } cookies = []; constructor(req, res) { this[RAW_REQUEST] = req; this[RAW_RESPONSE] = res; this.status = HttpStatus.NOT_FOUND; this.message = HttpStatusMessage[this.status]; } get header() { return this[RAW_RESPONSE].getHeaders(); } get headers() { return this[RAW_RESPONSE].getHeaders(); } get socket() { return this[RAW_RESPONSE].socket || this[RAW_RESPONSE].connection; } /* Get response status. By default, response.status is set to 404 unlike node's res.statusCode which defaults to 200. */ get status() { return this[RAW_RESPONSE].statusCode; } set status(val) { if (Object.prototype.hasOwnProperty.call(HttpStatusMessage, val)) { this[RAW_RESPONSE].statusCode = val; this[RAW_RESPONSE].statusMessage = HttpStatusMessage[val] || ''; } else { throw new Error(`error statusCode ${val}, expect one of below listed code: \n${JSON.stringify(HttpStatusMessage, null, ' ')}`); } } /** * Get response status message. * By default, it is associated with response.status. */ get message() { return this[RAW_RESPONSE].statusMessage; } set message(val) { this[RAW_RESPONSE].statusMessage = val; } set length(val) { this[RAW_RESPONSE].setHeader('content-length', val); } /** * Return response Content-Length as a number when present, * or deduce from ctx.body when possible, or undefined. */ get length() { return parseInt((this[RAW_RESPONSE].getHeader('content-length') || '0').toString()); } get body() { return this.#body; } set body(val) { this.#body = val; } /** * Get a response header field value with case-insensitive field. * * e.g. const etag = ctx.response.get('ETag'); */ get(field) { return this[RAW_RESPONSE].getHeader(field); } /** * Set response header field to value: * * e.g. `ctx.request.set('Cache-Control', 'no-cache');` * ``` * ctx.request.set({ * 'Etag': '1234', * 'Last-Modified': date * }) * ``` */ set(field, value) { if (typeof field === 'string') { this[RAW_RESPONSE].setHeader(field, value); } else if (typeof field === 'object') { Object.keys(field).forEach((item) => { this[RAW_RESPONSE].setHeader(item, field[item]); }); } } /** * Append additional header field with value val. */ append(field, value) { const oldHeader = (this[RAW_RESPONSE].getHeader(field) || '').toString(); if (oldHeader) { const val = (value || '').toString(); if (val) { const t = oldHeader.split(/,\s*/); t.push(val); this[RAW_RESPONSE].setHeader(field, t.join(',')); } } else { this[RAW_RESPONSE].setHeader(field, value); } } /** * Remove header field. */ remove(field) { this[RAW_RESPONSE].removeHeader(field); } /** * Get request Content-Type void of parameters such as "charset". */ get type() { let rawType = (this.headers['content-type'] || '').toString(); let mineType = rawType.split(/;\s*/)[0]; return { mineType, suffixType: mimeExtMap[mineType], rawType }; } set type(val) { // this[RAW_RESPONSE].setDefaultEncoding if (val) { let _val = val.rawType || val.mineType || val.suffixType || ''; if (_val.indexOf('/') > -1) { // mime this.set('content-type', _val); } else { _val = _val.toLowerCase(); if (_val.startsWith('.')) { _val = _val.substring(1); } const mimeKey = Object.keys(mimeExtMap).find((item) => _val === item || _val === mimeExtMap[item]); if (!mimeKey) { console.warn(`app-response unknown type ${_val}`); this.set('content-type', _val); } else { this.set('content-type', mimeKey); } } } } /** * Very similar to ctx.request.is(). * Check whether the response type is one of the supplied types. * This is particularly useful for creating middleware that manipulate responses. */ is(...types) { if (Array.isArray(types[0])) { types = [...types[0]]; } const type = this.type.mineType || (this.type.suffixType && mimeExtMap[this.type.suffixType]) || this.type.rawType; if (!type) { return false; } for (let i = 0; i < types.length; i++) { const item = types[i]; if (!item) continue; if (item.endsWith('/*')) { const re = new RegExp(`${item.substring(0, item.lastIndexOf('/*'))}/.+`); if (re.test(type)) { return type; } } else if (item.indexOf('/') > -1) { if (type === item) { return type; } } else { const re = new RegExp(item); if (re.test(type)) { return type.substring(type.indexOf('/') + 1); } } } return false; } /** * Perform a [302] redirect to url. * * The string "back" is special-cased to provide Referrer support, * when Referrer is not present alt or "/" is used. */ redirect(url, alt) { if (this.status !== 301) { this.status = 302; } if (url === 'back') { // this.headers['location'] = alt || this[RAW_REQUEST].headers['referer'] || '/'; this[RAW_RESPONSE].setHeader('location', alt || this[RAW_REQUEST].headers['referer'] || '/'); return; } // this.header['location'] = url; this[RAW_RESPONSE].setHeader('location', url); } /** * 设置 响应的 附件下载 Content-Disposition 头,并设置相应 Content-Type 头 * * refers: * https://github.com/jshttp/content-disposition * https://github.com/jshttp/content-disposition/blob/master/index.js */ attachment(filename) { if (!filename) { return; } if (typeof filename !== 'string') { throw new TypeError('filename must be a string'); } const name = encodeURIComponent(basename(filename)); this[RAW_RESPONSE].setHeader('Content-Disposition', `attachment;filename=${name}`); let ext = extname(name); if (ext) { ext = ext.substring(1).toLowerCase(); this[RAW_RESPONSE].setHeader('Content-Type', Object.keys(mimeExtMap).find((item) => mimeExtMap[item].split(' ').indexOf(ext) > -1) || 'application/oct-stream'); } else { this[RAW_RESPONSE].setHeader('Content-Type', 'application/oct-stream'); } } get headerSent() { return this[RAW_RESPONSE].headersSent; } /** * Return '' if header not exists * * Set the Last-Modified header as an appropriate UTC string. * You can either set it as a Date or date string. */ get lastModified() { const t = this[RAW_RESPONSE].getHeader('Last-Modified'); return new Date(String(t)) || ''; } set lastModified(date) { this[RAW_RESPONSE].setHeader('Last-Modified', (typeof date === 'string' ? new Date(date) : date).toUTCString()); } /** * Set the ETag of a response including the wrapped "s. * Note that there is no corresponding response.etag getter. * * e.g. `crypto.createHash('md5').update(ctx.body).digest('hex')` */ get etag() { return this[RAW_RESPONSE].getHeader('ETag'); } set etag(val) { this[RAW_RESPONSE].setHeader('ETag', val); } /** * 对向CDN缓存服务等 响应 说明区分某个字段的值做两份缓存(gzip|identity)? * * refers: https://imququ.com/post/vary-header-in-http.html */ vary(field) { this[RAW_RESPONSE].setHeader('vary', field); } /** * Flush any set headers, and begin the body. */ flushHeaders() { this[RAW_RESPONSE].flushHeaders(); } }