mirror of
https://github.com/nodejs/node.git
synced 2026-10-28 10:15:07 +08:00
Add non-throwing counterparts of http.validateHeaderName() and
http.validateHeaderValue() that return a boolean instead of throwing.
Rejecting an invalid header with the existing validators costs a few
microseconds, because an error object and its stack trace are created,
compared to ~20ns for the boolean check. Userland HTTP implementations
such as undici (fetch Headers, request options) therefore keep private
copies of the token and field-value tables from _http_common. These new
functions let them reuse the core implementation.
isValidHeaderValue() accepts an optional `httpValidation` option
('strict' or 'relaxed') that has the same meaning as the option of the
same name on http.createServer() and http.request().
Signed-off-by: James M Snell <jasnell@gmail.com>
PR-URL: https://github.com/nodejs/node/pull/66334
Reviewed-By: Filip Skokan <panva.ip@gmail.com>
Reviewed-By: Tim Perry <pimterry@gmail.com>
Reviewed-By: Marco Ippolito <marcoippolito54@gmail.com>
169 lines
4.1 KiB
JavaScript
169 lines
4.1 KiB
JavaScript
'use strict';
|
|
require('../common');
|
|
const assert = require('assert');
|
|
const {
|
|
isValidHeaderName,
|
|
isValidHeaderValue,
|
|
validateHeaderName,
|
|
validateHeaderValue,
|
|
} = require('http');
|
|
|
|
function succeeds(fn) {
|
|
try {
|
|
fn();
|
|
return true;
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// isValidHeaderName
|
|
{
|
|
const valid = [
|
|
'a',
|
|
'user-agent',
|
|
'USER-AGENT',
|
|
'User-Agent',
|
|
'x-forwarded-for',
|
|
'x-request-id-with-a-long-name',
|
|
"!#$%&'*+-.^_`|~",
|
|
'0123456789',
|
|
];
|
|
const invalid = [
|
|
'',
|
|
' ',
|
|
'bad header',
|
|
'bad:header',
|
|
'x-forwarded-fםr',
|
|
'איקס-פורוורד-פור',
|
|
'x\r\ny',
|
|
'x\0',
|
|
'(comment)',
|
|
'"quoted"',
|
|
'a,b',
|
|
'long-invalid-header-name\u00e9',
|
|
];
|
|
const nonStrings = [
|
|
undefined, null, 0, 1, true, false, {}, [], ['a'],
|
|
Symbol('a'), () => {}, 1n, Buffer.from('a'),
|
|
];
|
|
|
|
for (const name of valid) {
|
|
assert.strictEqual(isValidHeaderName(name), true, name);
|
|
}
|
|
for (const name of [...invalid, ...nonStrings]) {
|
|
assert.strictEqual(isValidHeaderName(name), false, String(name?.toString?.()));
|
|
}
|
|
|
|
// Must agree with validateHeaderName() for every input.
|
|
for (const name of [...valid, ...invalid, ...nonStrings]) {
|
|
assert.strictEqual(
|
|
isValidHeaderName(name),
|
|
succeeds(() => validateHeaderName(name)),
|
|
);
|
|
}
|
|
|
|
// Every single-character name agrees with validateHeaderName(), for both
|
|
// the short (lookup table) and long (regexp) code paths.
|
|
for (let c = 0; c <= 0x10ff; c++) {
|
|
const ch = String.fromCharCode(c);
|
|
for (const name of [ch, `${ch}xxxxxxxxxxxx`]) {
|
|
assert.strictEqual(
|
|
isValidHeaderName(name),
|
|
succeeds(() => validateHeaderName(name)),
|
|
`char code ${c}`,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
// isValidHeaderValue
|
|
{
|
|
const valid = [
|
|
'',
|
|
'text/html',
|
|
'a b\tc',
|
|
'\u00e9\u00ff',
|
|
'\x80',
|
|
1,
|
|
0,
|
|
null,
|
|
true,
|
|
['a', 'b'],
|
|
];
|
|
const invalid = [
|
|
undefined,
|
|
'a\r\nb',
|
|
'a\nb',
|
|
'a\rb',
|
|
'a\0b',
|
|
'a\x01b',
|
|
'a\x7fb',
|
|
'לא תקין',
|
|
'\u0100',
|
|
['a', 'b\n'],
|
|
Symbol('a'),
|
|
];
|
|
|
|
for (const value of valid) {
|
|
assert.strictEqual(isValidHeaderValue(value), true, String(value));
|
|
}
|
|
for (const value of invalid) {
|
|
assert.strictEqual(isValidHeaderValue(value), false, String(value));
|
|
}
|
|
|
|
// Must agree with validateHeaderValue() for every input.
|
|
for (const value of [...valid, ...invalid]) {
|
|
assert.strictEqual(
|
|
isValidHeaderValue(value),
|
|
succeeds(() => validateHeaderValue('x-test', value)),
|
|
);
|
|
}
|
|
|
|
for (let c = 0; c <= 0x10ff; c++) {
|
|
const value = `a${String.fromCharCode(c)}b`;
|
|
assert.strictEqual(
|
|
isValidHeaderValue(value),
|
|
succeeds(() => validateHeaderValue('x-test', value)),
|
|
`char code ${c}`,
|
|
);
|
|
// Explicit 'strict' is the same as the default.
|
|
assert.strictEqual(
|
|
isValidHeaderValue(value, { httpValidation: 'strict' }),
|
|
isValidHeaderValue(value),
|
|
`char code ${c}`,
|
|
);
|
|
}
|
|
|
|
// 'relaxed' follows the Fetch spec: only NUL, CR, LF and code points above
|
|
// U+00FF are rejected.
|
|
const relaxed = { httpValidation: 'relaxed' };
|
|
for (let c = 0; c <= 0x10ff; c++) {
|
|
const expected = !(c === 0x00 || c === 0x0a || c === 0x0d || c > 0xff);
|
|
assert.strictEqual(
|
|
isValidHeaderValue(`a${String.fromCharCode(c)}b`, relaxed),
|
|
expected,
|
|
`char code ${c}`,
|
|
);
|
|
}
|
|
assert.strictEqual(isValidHeaderValue(undefined, relaxed), false);
|
|
assert.strictEqual(isValidHeaderValue('a\x01b', relaxed), true);
|
|
assert.strictEqual(isValidHeaderValue('a\x7fb', relaxed), true);
|
|
|
|
// An empty options object uses the default.
|
|
assert.strictEqual(isValidHeaderValue('a\x01b', {}), false);
|
|
assert.strictEqual(isValidHeaderValue('a\x01b', { httpValidation: undefined }), false);
|
|
|
|
// Invalid options throw.
|
|
for (const options of [null, 1, 'relaxed', true]) {
|
|
assert.throws(() => isValidHeaderValue('a', options), {
|
|
code: 'ERR_INVALID_ARG_TYPE',
|
|
});
|
|
}
|
|
for (const httpValidation of ['insecure', 'RELAXED', '', 1, null]) {
|
|
assert.throws(() => isValidHeaderValue('a', { httpValidation }), {
|
|
code: 'ERR_INVALID_ARG_VALUE',
|
|
});
|
|
}
|
|
}
|