ultimate-express
Version:
The Ultimate Express. Fastest http server with full Express compatibility, based on uWebSockets.
429 lines (384 loc) • 13.7 kB
JavaScript
/*
Copyright 2024 dimden.dev
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
const { patternToRegex, deprecated, NullObject } = require("./utils.js");
const accepts = require("accepts");
const typeis = require("type-is");
const parseRange = require("range-parser");
const proxyaddr = require("proxy-addr");
const fresh = require("fresh");
const { Readable } = require("stream");
const discardedDuplicates = [
"age", "authorization", "content-length", "content-type", "etag", "expires",
"from", "host", "if-modified-since", "if-unmodified-since", "last-modified",
"location", "max-forwards", "proxy-authorization", "referer", "retry-after",
"server", "user-agent"
];
let key = 0;
module.exports = class Request extends Readable {
#cachedQuery = null;
#cachedHeaders = null;
#cachedDistinctHeaders = null;
#rawHeadersEntries = [];
#cachedParsedIp = null;
#needsData = false;
#doneReadingData = false;
#bufferedData = null;
constructor(req, res, app) {
super();
this._res = res;
this._req = req;
this.readable = true;
this._req.forEach((key, value) => {
this.#rawHeadersEntries.push([key, value]);
});
this.routeCount = 0;
this.key = key++;
if(key > 100000) {
key = 0;
}
this.app = app;
this.urlQuery = req.getQuery() ?? '';
if(this.urlQuery) {
this.urlQuery = '?' + this.urlQuery;
}
this.originalUrl = req.getUrl() + this.urlQuery;
this.url = this.originalUrl;
const iq = this.url.indexOf('?');
this.path = iq !== -1 ? this.url.substring(0, iq) : this.url;
this.endsWithSlash = this.path[this.path.length - 1] === '/';
this._opPath = this.path;
this._originalPath = this.path;
if(this.endsWithSlash && this.path !== '/' && !this.app.get('strict routing')) {
this._opPath = this._opPath.slice(0, -1);
}
this.method = req.getCaseSensitiveMethod().toUpperCase();
this._isOptions = this.method === 'OPTIONS';
this._isHead = this.method === 'HEAD';
this.params = {};
this._matchedMethods = new Set();
this._gotParams = new Set();
this._stack = [];
this._paramStack = [];
this.receivedData = false;
// reading ip is very slow in UWS, so its better to not do it unless truly needed
if(this.app.needsIpAfterResponse || this.key < 100) {
// if app needs ip after response, read it now because after response its not accessible
// also read it for first 100 requests to not error
this.rawIp = this._res.getRemoteAddress();
}
const additionalMethods = this.app.get('body methods');
// skip reading body for non-POST requests
// this makes it +10k req/sec faster
if(
this.method === 'POST' ||
this.method === 'PUT' ||
this.method === 'PATCH' ||
(additionalMethods && additionalMethods.includes(this.method))
) {
this.#bufferedData = Buffer.allocUnsafe(0);
this._res.onData((ab, isLast) => {
// make stream actually readable
this.receivedData = true;
if(isLast) {
this.#doneReadingData = true;
}
// instead of pushing data immediately, buffer it
// because writable streams cant handle the amount of data uWS gives (usually 512kb+)
const chunk = Buffer.from(ab);
this.#bufferedData = Buffer.concat([this.#bufferedData, chunk]);
if(this.#needsData) {
this.#needsData = false;
this._read();
}
});
} else {
this.receivedData = true;
}
}
_read() {
if(!this.receivedData || !this.#bufferedData) {
this.#needsData = true;
return;
}
if(this.#bufferedData.length > 0) {
// push 128kb chunks
const chunk = this.#bufferedData.subarray(0, 1024 * 128);
this.#bufferedData = this.#bufferedData.subarray(1024 * 128);
this.push(chunk);
} else if(this.#doneReadingData) {
this.push(null);
} else {
this.#needsData = true;
}
}
get baseUrl() {
let match = this._originalPath.match(patternToRegex(this._stack.join(""), true));
return match ? match[0] : '';
}
set baseUrl(x) {
return this._originalPath = x;
}
get #host() {
const trust = this.app.get('trust proxy fn');
if(!trust) {
return this.get('host');
}
let val = this.headers['x-forwarded-host'];
if (!val || !trust(this.connection.remoteAddress, 0)) {
val = this.headers['host'];
} else if (val.indexOf(',') !== -1) {
// Note: X-Forwarded-Host is normally only ever a
// single value, but this is to be safe.
val = val.substring(0, val.indexOf(',')).trimRight()
}
return val ? val.split(':')[0] : undefined;
}
get host() {
deprecated('req.host', 'req.hostname');
return this.hostname;
}
get hostname() {
const host = this.#host;
if(!host) return this.headers['host'].split(':')[0];
const offset = host[0] === '[' ? host.indexOf(']') + 1 : 0;
const index = host.indexOf(':', offset);
return index !== -1 ? host.slice(0, index) : host;
}
get httpVersion() {
return '1.1';
}
get httpVersionMajor() {
return 1;
}
get httpVersionMinor() {
return 1;
}
get ip() {
const trust = this.app.get('trust proxy fn');
if(!trust) {
return this.parsedIp;
}
return proxyaddr(this, trust);
}
get ips() {
const trust = this.app.get('trust proxy fn');
if(!trust) {
return [];
}
const addrs = proxyaddr.all(this, trust);
addrs.reverse().pop();
return addrs;
}
get protocol() {
const proto = this.app.ssl ? 'https' : 'http';
const trust = this.app.get('trust proxy fn');
if(!trust) {
return proto;
}
if(!trust(this.connection.remoteAddress, 0)) {
return proto;
}
const header = this.headers['x-forwarded-proto'] || proto;
const index = header.indexOf(',');
return index !== -1 ? header.slice(0, index).trim() : header.trim();
}
set query(query) {
return this.#cachedQuery = query;
}
get query() {
if(this.#cachedQuery) {
return this.#cachedQuery;
}
const qp = this.app.get('query parser fn');
if(qp) {
this.#cachedQuery = {...qp(this.urlQuery.slice(1))};
} else {
this.#cachedQuery = {...new NullObject()};
}
return this.#cachedQuery;
}
get secure() {
return this.protocol === 'https';
}
get subdomains() {
let host = this.hostname;
let subdomains = host.split('.');
const so = this.app.get('subdomain offset');
if(so === 0) {
return subdomains.reverse();
}
return subdomains.slice(0, -so).reverse();
}
get xhr() {
return this.headers['x-requested-with'] === 'XMLHttpRequest';
}
get parsedIp() {
if(this.#cachedParsedIp !== null) {
return this.#cachedParsedIp;
}
const finished = this.res.finished;
if(finished) {
// mark app as one that needs ip after response
this.app.needsIpAfterResponse = true;
}
if(!this.rawIp) {
if(finished) {
// fallback once
return '127.0.0.1';
}
this.rawIp = this._res.getRemoteAddress();
}
let ip = '';
if(this.rawIp.byteLength === 4) {
// ipv4
ip = new Uint8Array(this.rawIp).join('.');
} else if(this.rawIp.byteLength === 16) {
// ipv6
const dv = new DataView(this.rawIp);
for(let i = 0; i < 8; i++) {
ip += dv.getUint16(i * 2).toString(16).padStart(4, '0');
if(i < 7) {
ip += ':';
}
}
} else {
ip = undefined; // unix sockets dont have ip
}
this.#cachedParsedIp = ip;
return ip;
}
get connection() {
return {
remoteAddress: this.parsedIp,
localPort: this.app.port,
encrypted: this.app.ssl,
end: (body) => this.res.end(body)
};
}
get socket() {
return this.connection;
}
get fresh() {
if(this.method !== 'HEAD' && this.method !== 'GET') {
return false;
}
if((this.res.statusCode >= 200 && this.res.statusCode < 300) || this.res.statusCode === 304) {
return fresh(this.headers, {
'etag': this.res.headers['etag'],
'last-modified': this.res.headers['last-modified'],
});
}
return false;
}
get stale() {
return !this.fresh;
}
get(field) {
field = field.toLowerCase();
if(field === 'referrer' || field === 'referer') {
const res = this.headers['referrer'];
if(!res) {
return this.headers['referer'];
}
return res;
}
return this.headers[field];
}
header = this.get
accepts(...types) {
return accepts(this).types(...types);
}
acceptsCharsets(...charsets) {
return accepts(this).charsets(...charsets);
}
acceptsEncodings(...encodings) {
return accepts(this).encodings(...encodings);
}
acceptsLanguages(...languages) {
return accepts(this).languages(...languages);
}
is(type) {
return typeis(this, type);
}
param(name, defaultValue) {
deprecated('req.param(name)', 'req.params, req.body, or req.query');
if(this.params[name]) {
return this.params[name];
}
if(this.body && this.body[name]) {
return this.body[name];
}
return this.query[name] ?? defaultValue;
}
range(size, options) {
const range = this.headers['range'];
if(!range) return;
return parseRange(size, range, options);
}
set headers(headers) {
this.#cachedHeaders = headers;
}
get headers() {
// https://nodejs.org/api/http.html#messageheaders
if(this.#cachedHeaders) {
return this.#cachedHeaders;
}
this.#cachedHeaders = {...new NullObject()}; // seems to be faster
for (let index = 0, len = this.#rawHeadersEntries.length; index < len; index++) {
let [key, value] = this.#rawHeadersEntries[index];
key = key.toLowerCase();
if(this.#cachedHeaders[key]) {
if(discardedDuplicates.includes(key)) {
continue;
}
if(key === 'cookie') {
this.#cachedHeaders[key] += '; ' + value;
} else if(key === 'set-cookie') {
this.#cachedHeaders[key].push(value);
} else {
this.#cachedHeaders[key] += ', ' + value;
}
continue;
}
if(key === 'set-cookie') {
this.#cachedHeaders[key] = [value];
} else {
this.#cachedHeaders[key] = value;
}
}
return this.#cachedHeaders;
}
get headersDistinct() {
if(this.#cachedDistinctHeaders) {
return this.#cachedDistinctHeaders;
}
this.#cachedDistinctHeaders = {...new NullObject()};
this.#rawHeadersEntries.forEach((val) => {
const [key, value] = val;
if(!this.#cachedDistinctHeaders[key]) {
this.#cachedDistinctHeaders[key] = [value];
return;
}
this.#cachedDistinctHeaders[key].push(value);
});
return this.#cachedDistinctHeaders;
}
get rawHeaders() {
const res = [];
for (let index = 0, len = this.#rawHeadersEntries.length; index < len; index++) {
const val = this.#rawHeadersEntries[index];
res.push(val[0], val[1]);
}
return res;
}
}