Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
119 changes: 88 additions & 31 deletions src/request.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ const accepts = require("accepts");
const typeis = require("type-is");
const parseRange = require("range-parser");
const proxyaddr = require("proxy-addr");
const { isIP } = require("node:net");
const fresh = require("fresh");
const { Readable } = require("stream");

Expand Down Expand Up @@ -153,19 +154,28 @@ module.exports = class Request extends Readable {

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()
const isTrusted = !!(trust && trust(this.connection.remoteAddress, 0));
const rawHeader = (isTrusted && this.headers['x-forwarded-host']) || this.headers['host'];
let host = Array.isArray(rawHeader) ? rawHeader[0] : rawHeader;

if (typeof host !== 'string' || !host) return;
host = host.trim();

if (isTrusted) {
const commaIndex = host.indexOf(',');
if (commaIndex !== -1) {
// Note: X-Forwarded-Host is normally only ever a
// single value, but this is to be safe.
host = host.substring(0, commaIndex).trimEnd();
}
}

return val ? val.split(':')[0] : undefined;

if (!host) return;

const offset = host[0] === '[' ? host.indexOf(']') + 1 : 0;
const portIndex = host.indexOf(':', offset);

return portIndex !== -1 ? host.substring(0, portIndex) : host;
}

get host() {
Expand All @@ -174,11 +184,7 @@ module.exports = class Request extends Readable {
}

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;
return this.#host;
}

get httpVersion() {
Expand Down Expand Up @@ -246,18 +252,28 @@ module.exports = class Request extends Readable {
return this.protocol === 'https';
}

#cachedSubdomains = null;

get subdomains() {
let host = this.hostname;
let subdomains = host.split('.');
const so = this.app.get('subdomain offset');
if(so === 0) {
return subdomains.reverse();
if(this.#cachedSubdomains !== null) {
return this.#cachedSubdomains;
}

const hostname = this.hostname;
if(!hostname || isIP(hostname)) {
return this.#cachedSubdomains = [];
}
return subdomains.slice(0, -so).reverse();

const offset = this.app.get('subdomain offset');
const parts = hostname.split('.');
const subdomains = parts.reverse().slice(offset);

return this.#cachedSubdomains = subdomains;
}

get xhr() {
return this.headers['x-requested-with'] === 'XMLHttpRequest';
const val = this.headers?.['x-requested-with'];
return typeof val === 'string' && val.toLowerCase() === 'xmlhttprequest';
}

get parsedIp() {
Expand Down Expand Up @@ -327,6 +343,12 @@ module.exports = class Request extends Readable {
}

get(field) {
if(!field) {
throw new TypeError('name argument is required to req.get');
}
if(typeof field !== 'string') {
throw new TypeError('name must be a string to req.get');
}
field = field.toLowerCase();
if(field === 'referrer' || field === 'referer') {
const res = this.headers['referrer'];
Expand Down Expand Up @@ -355,19 +377,54 @@ module.exports = class Request extends Readable {
return accepts(this).languages(...languages);
}

is(type) {
return typeis(this, type);
acceptsEncoding(...args) {
deprecated('req.acceptsEncoding', 'req.acceptsEncodings');
return this.acceptsEncodings(...args);
}

acceptsCharset(...args) {
deprecated('req.acceptsCharset', 'req.acceptsCharsets');
return this.acceptsCharsets(...args);
}

acceptsLanguage(...args) {
deprecated('req.acceptsLanguage', 'req.acceptsLanguages');
return this.acceptsLanguages(...args);
}

is(types) {
if(Array.isArray(types)) {
return typeis(this, types);
}

if(arguments.length === 1) {
return typeis(this, [types]);
}

return typeis(this, [...arguments]);
}

param(name, defaultValue) {
deprecated('req.param(name)', 'req.params, req.body, or req.query');
if(this.params[name]) {
return this.params[name];

if(name == null) return defaultValue;

if(this.params && Object.prototype.hasOwnProperty.call(this.params, name)) {
const value = this.params[name];
if(value != null) return value;
}
if(this.body && this.body[name]) {
return this.body[name];

if(this.body && Object.prototype.hasOwnProperty.call(this.body, name)) {
const value = this.body[name];
if(value != null) return value;
}

if(this.query && Object.prototype.hasOwnProperty.call(this.query, name)) {
const value = this.query[name];
if(value != null) return value;
}
return this.query[name] ?? defaultValue;

return defaultValue;
}

range(size, options) {
Expand Down
20 changes: 17 additions & 3 deletions tests/tests/req/req-get.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,13 @@ async function sendRequest(method, url, arrayHeaders) {
client.connect(parseInt(port), host, () => {
let request = `${method} ${path} HTTP/1.1\r\n`;
request += `Host: ${host}:${port}\r\n`;

for (const [key, value] of arrayHeaders) {
request += `${key}: ${value}\r\n`;
}

request += '\r\n';

client.write(request);

setTimeout(() => {
Expand All @@ -42,6 +42,16 @@ app.get("/test", (req, res) => {
res.send("test");
});

app.get("/validate", (req, res) => {
const errors = [];

try { req.get(); } catch(e) { errors.push(e.message); }

try { req.get(42); } catch(e) { errors.push(e.message); }

res.json(errors);
});

app.listen(13333, async () => {
console.log('Server is running on port 13333');

Expand Down Expand Up @@ -74,5 +84,9 @@ app.listen(13333, async () => {
headers.push(['content-type', 'application/json']);
res = await sendRequest('GET', 'http://localhost:13333/test', headers);

// test parameter validation
res = await fetch('http://localhost:13333/validate');
console.log('validate:', await res.text());

process.exit(0);
})
78 changes: 63 additions & 15 deletions tests/tests/req/req-hostname.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,18 +10,16 @@ async function sendRequest(method, url, customHost) {
const [host, port] = url.split('://')[1].split('/')[0].split(':');
const path = '/' + url.split('/').slice(3).join('/');

client.on('data', () => client.end());
client.on('end', resolve);

client.connect(parseInt(port), host, () => {
let request = `${method} ${path} HTTP/1.1\r\n`;
request += `Host: ${customHost}\r\n`;

for (const [key, value] of customHost) {
request += `${key}: ${value}\r\n`;
}
request += '\r\n';

client.write(request);

setTimeout(() => {
client.destroy();
resolve();
}, 100);
});
});
}
Expand All @@ -32,14 +30,64 @@ app.get("/test", (req, res) => {
res.send("test");
});

// trust proxy app
const trustedApp = express();
trustedApp.set('trust proxy', true);

trustedApp.get("/test", (req, res) => {
console.log(req.hostname);
res.send("ok");
});

app.listen(13333, async () => {
console.log('Server is running on port 13333');
trustedApp.listen(13334, async () => {
console.log('Server is running on port 13333');

let res;

res = await fetch('http://localhost:13333/test');
console.log(await res.text());

let res;
res = await fetch('http://localhost:13333/test');
console.log(await res.text());
// host with port
res = await sendRequest('GET', 'http://localhost:13333/test', [
['Host', 'example.com:8080']
]);

res = await sendRequest('GET', 'http://localhost:13333/test', 'test:13333');
// IPv6 literal with port
res = await sendRequest('GET', 'http://localhost:13333/test', [
['Host', '[::1]:13333']
]);

process.exit(0);
})
// IPv6 literal without port
res = await sendRequest('GET', 'http://localhost:13333/test', [
['Host', '[::1]']
]);

// --- trust proxy tests ---

// trusted: X-Forwarded-Host present
res = await sendRequest('GET', 'http://localhost:13334/test', [
['Host', 'real.host.com'],
['X-Forwarded-Host', 'forwarded.example.com']
]);

// trusted: X-Forwarded-Host with comma (multiple values)
res = await sendRequest('GET', 'http://localhost:13334/test', [
['Host', 'real.host.com'],
['X-Forwarded-Host', 'first.example.com, second.example.com']
]);

// trusted: X-Forwarded-Host with port
res = await sendRequest('GET', 'http://localhost:13334/test', [
['Host', 'real.host.com'],
['X-Forwarded-Host', 'forwarded.example.com:8443']
]);

// trusted: no X-Forwarded-Host → should fallback to Host header
res = await sendRequest('GET', 'http://localhost:13334/test', [
['Host', 'fallback.host.com']
]);

process.exit(0);
});
});
14 changes: 14 additions & 0 deletions tests/tests/req/req-is.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,12 @@ app.all('/test', (req, res) => {
res.send('test');
});

app.all('/multi', (req, res) => {
console.log('multi-args:', req.is('html', 'json'));
console.log('array:', req.is(['html', 'json']));
res.send('ok');
});

app.listen(13333, async () => {
console.log('Server is running on port 13333');

Expand All @@ -40,5 +46,13 @@ app.listen(13333, async () => {
},
body: 'Hello, World!'
}).then(res => res.text());

// test multi-argument
await fetch('http://localhost:13333/multi', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ test: 1 })
}).then(res => res.text());

process.exit(0);
});
24 changes: 20 additions & 4 deletions tests/tests/req/req-param.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,19 +25,28 @@ app.get('/test/:test', (req, res) => {
res.send('test');
});

app.post('/falsy', (req, res) => {
const results = [
req.param('zero'),
req.param('empty'),
req.param('missing', 'fallback'),
];
res.json(results);
});

app.listen(13333, async () => {
console.log('Server is running on port 13333');

await fetch('http://localhost:13333/delete', {
method: 'DELETE',
body: JSON.stringify({test: 'aaa'}),
method: 'DELETE',
body: JSON.stringify({test: 'aaa'}),
headers: {
"Content-Type": "application/json",
}
}).then(res => res.text());
await fetch('http://localhost:13333/delete', {
method: 'DELETE',
body: JSON.stringify({}),
method: 'DELETE',
body: JSON.stringify({}),
headers: {
"Content-Type": "application/json",
}
Expand All @@ -49,5 +58,12 @@ app.listen(13333, async () => {
await fetch('http://localhost:13333/test/test').then(res => res.text());
await fetch('http://localhost:13333/test/test/test').then(res => res.text());

const falsy = await fetch('http://localhost:13333/falsy', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ zero: 0, empty: '' }),
}).then(res => res.text());
console.log('falsy:', falsy);

process.exit(0);
});
Loading