@tunframework/tun
Version:
tun framework for node with typescript
286 lines (285 loc) • 9.5 kB
JavaScript
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();
}
}