UNPKG

@tunframework/tun

Version:

tun framework for node with typescript

501 lines (500 loc) 17.1 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.TunRequest = void 0; const HttpMethod_js_1 = require("./constants/http/HttpMethod.js"); const url_1 = require("url"); const mime_js_1 = require("./constants/mime.js"); const symbol_js_1 = require("./constants/http/symbol.js"); const defaultTunRequestOptions = { // proxy: false, subdomainOffset: 2 }; class TunRequest { [symbol_js_1.RAW_REQUEST]; #options; #method; #url; /** * formdata fields * @see [bodyparser](https://github.com/tunframework/tun-bodyparser) */ fields = {}; _fields = {}; /** * formdata files * @see [bodyparser](https://github.com/tunframework/tun-bodyparser) */ files = {}; _files = {}; /** * request body * @see [bodyparser](https://github.com/tunframework/tun-bodyparser) */ body = {}; /** * @example "/product/abc" matches "/product/:id", ctx.req.slugs.id === "abc" * @see [rest-router](https://github.com/tunframework/tun-rest-router) */ slugs = {}; constructor(req, options) { options = this.#options = Object.assign({}, defaultTunRequestOptions, options); this[symbol_js_1.RAW_REQUEST] = req; this.#method = (req.method || ''); if (options.proxy) { this.#url = new url_1.URL(`${req.headers['x-forwarded-proto']}://${req.headers['x-forwarded-host']}${req.url}`); } else { // @ts-ignore const protocol = req['protocol'] || 'http'; this.#url = new url_1.URL(`${protocol}://${req.headers['host']}${req.url}`); } } get header() { return this[symbol_js_1.RAW_REQUEST].headers; } set header(val) { this[symbol_js_1.RAW_REQUEST].headers = val; } get headers() { return this[symbol_js_1.RAW_REQUEST].headers; } set headers(val) { this[symbol_js_1.RAW_REQUEST].headers = val; } /** * * http methods * * refers: https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods * * Get/Set request method, useful for implementing middleware such as methodOverride(). * */ get method() { return this.#method; } set method(val) { this.#method = val; } /** * Return request Content-Length as a number when preqent, or undefined. */ get length() { return this.headers['content-length'] ? parseInt(this.headers['content-length']) : undefined; } get url() { return this.#url.href; } set url(val) { this.#url.href = val; } get originalUrl() { return this[symbol_js_1.RAW_REQUEST].url; } /** * Get origin of URL, include protocol and host. * @example http://example.com */ get origin() { return this.headers['origin']; } /** * Get full request URL, include protocol, host and url. * @example http://example.com/foo/bar?q=1 */ get href() { return this.#url.href; } get path() { return this.#url.pathname; } set path(val) { const [pathname, search] = val.split('?'); this.#url.pathname = pathname; search && (this.#url.search = search); } get querystring() { if (!this.#url.search) return ''; return this.#url.search.startsWith('?') ? this.#url.search.substring(1) : this.#url.search; } set querystring(val) { this.#url.search = val; } get search() { return this.#url.search; } set search(val) { this.#url.search = val; } /** * Get host (hostname:port) when present. * Supports X-Forwarded-Host when app.proxy is true, * otherwise Host is used. */ get host() { if (this.#options.proxy) { let host = (this.headers['x-forwarded-host'] || '') .toString() .split(/,\s*/)[0]; if (host) { return host; } } return this.headers['host']; } /** * Get hostname when present. * Supports X-Forwarded-Host when app.proxy is true, * otherwise Host is used. * * //// ignore: If host is IPv6, delegates parsing to WHATWG URL API, Note This may impact performance. */ get hostname() { if (this.#options.proxy) { let host = (this.headers['x-forwarded-host'] || '') .toString() .split(/,\s*/)[0]; if (host) { return host.split(':')[0]; } } // @ts-ignore return this.headers['host'].split(':')[0]; } /** * Get WHATWG parsed URL object. * * refers: https://nodebeginner.org/blog/post/nodejs-tutorial-whatwg-url-parser/ * * url模块现在(node 8.0+)提供了一个额外的实现,它实现了标准化的WHATWG URL API,使得Node.js的url-parsing代码与Web浏览器解析URL的方式相同 * * 原有的 'url.parse()' Nodejs 被认为 */ get URL() { return this.#url; } // Get request Content-Type void of parameters such as "charset". get type() { return (this.headers['content-type'] || '').toString().split(/;\s*/)[0]; } /** * Get request charset when present, or undefined: * @example "utf-8" */ get charset() { const prefix = 'charset='; const part = (this.headers['content-type'] || '') .split(';') .find((item) => item.trim().startsWith(prefix)); return part && part.trim().substring(prefix.length); } get query() { return this.#url.searchParams; } set query(val) { this.#url.search = val.toString(); } // /** // * Check if a request cache is "fresh", // * aka the contents have not changed. // * This method is for cache negotiation between If-None-Match / ETag, // * and If-Modified-Since and Last-Modified. // * It should be referenced after setting one or more of these response headers. // * // * refers: https://www.zhihu.com/question/57840128 // */ // get fresh() { // if (this.headers['cache-control'] === 'no-cache') { // return false; // } // if (this.headers['if-none-match']) { // } // if (this.headers['if-modified-since'] && this.headers['last-modified']<='') { // } // return false; // } // get stale() { // return !this.fresh // } get protocol() { return ((this.#options.proxy && this.headers['x-forwarded-proto']) || this.#url.protocol.substring(0, this.#url.protocol.length - 1)); } get secure() { return this.protocol === 'https'; } /** * Request remote address. Supports X-Forwarded-For when app.proxy is true. */ get ip() { let ip; if (this.#options.proxy) { ip = this.headers['x-forwarded-for']; if (ip && !/unknown/i.test(ip.toString())) { return ip.toString().split(/,\s*/)[0]; } } ip = this.headers['x-real-ip']; if (ip && !/unknown/i.test(ip.toString())) { return ip.toString().split(/,\s*/)[0]; } return this[symbol_js_1.RAW_REQUEST].socket.remoteAddress; } /** * When X-Forwarded-For is present and app.proxy is enabled an array of these ips is returned, * ordered from upstream to downstream. * When disabled an empty array is returned. * * refers: https://support.stackpath.com/hc/en-us/articles/360021658292-Getting-Real-Client-IPs-with-X-Forwarded-For */ get ips() { let ips; if (this.#options.proxy) { ips = this.headers['x-forwarded-for']; if (ips && !/unknown/i.test(ips.toString())) { return ips.toString().split(/,\s*/); } } ips = this.headers['x-real-ip']; if (ips && !/unknown/i.test(ips.toString())) { return ips.toString().split(/,\s*/); } return [this[symbol_js_1.RAW_REQUEST].connection.remoteAddress]; } /** * 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 app.subdomainOffset. * * For example, if the domain is "tobi.ferrets.example.com": If app.subdomainOffset is not set, * ctx.subdomains is ["ferrets", "tobi"]. * If app.subdomainOffset is 3, ctx.subdomains is ["tobi"]. */ get subdomains() { const parts = this.hostname.split('.'); return parts .slice(0, Math.max(0, parts.length - this.#options.subdomainOffset)) .reverse(); } /** * Check if the incoming request contains the "Content-Type" header field, * and it contains any of the give mime types. * If there is no request body, null is returned. * If there is no content type, or the match fails false is returned. * Otherwise, it returns the matching content-type. */ is(...types) { if (Array.isArray(types[0])) { types = [...types[0]]; } const type = this.type; 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; } // begin Content Negotiation /** * Check if the given type(s) is acceptable, * returning the best match when true, otherwise false. * The type value may be one or more mime type string such as "application/json", * the extension name such as "json", * or an array ["json", "html", "text/plain"]. * * refers: https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Headers/Accept */ accepts(...types) { if (Array.isArray(types[0])) { types = [...types[0]]; } let acceptList = (this.headers['accept'] || '').toString().split(/,\s*/); // no accept header, return the first give type if (acceptList.length === 0 || acceptList[0] === '') { return types[0]; } const scoreList = types.map((type) => { let score = 0; const isSubType = type.indexOf('/') > -1; acceptList.find((item) => { const [accept, Q = '1'] = item.split(';q='); const q = parseFloat(Q); if (isSubType) { if (accept === type || accept === `${type.substring(0, type.lastIndexOf('/'))}/*`) { score += q; return true; } } else { // given file extension like png,gif,html,js if (accept.endsWith('/*')) { const mimeList = Object.keys(mime_js_1.mimeExtMap).filter((item) => item.startsWith(accept.substring(0, accept.lastIndexOf('/')))); const mimeExt = mimeList.find((item) => mime_js_1.mimeExtMap[item] && mime_js_1.mimeExtMap[item].indexOf(type) > -1); if (mimeExt && typeof mimeExt === 'string') { score += q; return true; } } else if (mime_js_1.mimeExtMap[accept] && mime_js_1.mimeExtMap[accept].split(' ').indexOf(type) > -1) { score += q; return true; } } }); return score; }); const maxScore = Math.max(...scoreList); if (maxScore > 0) { return types[scoreList.indexOf(maxScore)]; } return false; } /** * Check if encodings are acceptable, * returning the best match when true, otherwise false. * Note that you should include identity as one of the encodings! * * // Accept-Encoding: gzip * ctx.acceptsEncodings('gzip', 'deflate', 'identity'); * // "gzip" * * ctx.acceptsEncodings(['gzip', 'deflate', 'identity']); * // "gzip" * * Note that the identity encoding (which means no encoding) * could be unacceptable if the client explicitly sends identity;q=0. * Although this is an edge case, * you should still handle the case where this method returns false. */ acceptsEncodings(...encodings) { const { acceptHeaderParts, maxScoreAccept } = getAcceptableByQ(this.headers['accept-encoding'], encodings); if (encodings.length === 0) { const tmp = acceptHeaderParts.map((item) => item.split(';q=')[0]); if (tmp.indexOf('identity') === -1) { tmp.push('identity'); } return tmp; } return maxScoreAccept; } /** * Check if charsets are acceptable, * returning the best match when true, otherwise false. */ acceptsCharsets(...charsets) { const { // acceptHeaderParts, maxScoreAccept } = getAcceptableByQ(this.headers['accept-charset'], charsets); if (charsets.length === 0) { return false; } return maxScoreAccept; } /** * Check if langs are acceptable, * returning the best match when true, * otherwise false. * * When no arguments are given all accepted languages are returned as an array * * refers: https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Headers/Accept-Language * @returns return wheather (given) languages acceptable, or all accepted languages (when no arguments are given) */ acceptsLanguages(...langs) { const { acceptHeaderParts, maxScoreAccept } = getAcceptableByQ(this.headers['accept-language'], langs); if (langs.length === 0) { return acceptHeaderParts.map((item) => item.split(/;q=\s*/)[0]); } return maxScoreAccept; } // end Content Negotiation /** * Check if the request is idempotent. * * refers: https://www.cnblogs.com/weidagang2046/archive/2011/06/04/idempotence.html */ idempotent() { switch (HttpMethod_js_1.HttpMethod[this.method]) { case HttpMethod_js_1.HttpMethod.GET: case HttpMethod_js_1.HttpMethod.HEAD: return true; default: return false; } } get socket() { return this[symbol_js_1.RAW_REQUEST].socket; } /** * Return request header. * * field lower-kebad-case-field-name */ get(field) { return this.headers[field]; } } // TunRequest end exports.TunRequest = TunRequest; function getAcceptableByQ(acceptHeader, accepts) { // flatten const flattenAccepts = []; accepts.forEach((item) => { if (Array.isArray(item)) { flattenAccepts.push(...item); } else { flattenAccepts.push(item); } }); const acceptHeaderParts = (acceptHeader || '').toString().split(/,\s*/); const scoreList = flattenAccepts.map((encoding) => { let score = 0; acceptHeaderParts.find((item) => { const [accept, Q = '1'] = item.split(';q='); let q = parseFloat(Q); if (accept === encoding) { score += q; return true; } }); return score; }); const maxScore = Math.max(...scoreList); let maxScoreAccept = false; if (maxScore > 0) { maxScoreAccept = flattenAccepts[scoreList.indexOf(maxScore)].split(';q=')[0]; } return { acceptHeaderParts, scoreList, maxScore, maxScoreAccept }; }