'use strict' const { test } = require('node:test') const HeaderParser = require('../deps/dicer/lib/HeaderParser') test('dicer-headerparser', async t => { const DCRLF = '\r\n\r\n' const MAXED_BUFFER = Buffer.allocUnsafe(128 * 1024) MAXED_BUFFER.fill(0x41) // 'A' const tests = [ { source: DCRLF, expected: {}, what: 'No header' }, { source: ['Foo: bar\r', '\n\r', '\nextra'], expected: { foo: ['bar'] }, what: 'Header terminator across chunks' }, { source: ['Foo: bar\r', '\n\r\n'], expected: { foo: ['bar'] }, what: 'Header terminator split between CR and LFCRLF leaves no trailing CR on the value' }, { source: ['Foo: bar\r', '\n\r\n'], cfg: { maxHeaderSize: 8 }, expected: { foo: ['bar'] }, what: 'Header terminator split after maxHeaderSize truncation keeps the value intact' }, { source: ['Foo: bar\r', '\n\r', '\nextra'], cfg: { maxHeaderSize: 0 }, expected: {}, what: 'Header terminator across chunks after max header size' }, { source: ['Content-Type:\t text/plain', 'Content-Length:0' ].join('\r\n') + DCRLF, expected: { 'content-type': [' text/plain'], 'content-length': ['0'] }, what: 'Value spacing' }, { source: ['Content-Type:\t text/plain', 'Content-Length:0' ].join('\r\n') + DCRLF, cfg: { maxHeaderPairs: 0 }, expected: {}, what: 'should enforce maxHeaderPairs of 0' }, { source: ['Content-Type:\t text/plain', 'Content-Length:0' ].join('\r\n') + DCRLF, cfg: { maxHeaderPairs: 1 }, expected: { 'content-type': [' text/plain'] }, what: 'should enforce maxHeaderPairs of 1' }, { source: ['Content-Type:\r\n text/plain', 'Foo:\r\n bar\r\n baz' ].join('\r\n') + DCRLF, expected: {}, cfg: { maxHeaderSize: 0 }, what: 'should enforce maxHeaderSize of 0' }, { source: ['Content-Type:\r\n text/plain', 'Foo:\r\n bar\r\n baz' ].join('\r\n') + DCRLF, expected: { 'content-type': [' text/plai'] }, cfg: { maxHeaderSize: 25 }, what: 'should enforce maxHeaderSize of 25' }, { source: ['Content-Type:\r\n text/plain', 'Foo:\r\n bar\r\n baz' ].join('\r\n') + DCRLF, expected: { 'content-type': [' text/plain'] }, cfg: { maxHeaderSize: 31 }, what: 'should enforce maxHeaderSize of 31 and ignore the second header' }, { source: ['Content-Type:\r\n text/plain', 'Foo:\r\n bar\r\n baz' ].join('\r\n') + DCRLF, expected: { 'content-type': [' text/plain'], foo: [''] }, cfg: { maxHeaderSize: 32 }, what: 'should enforce maxHeaderSize of 32 and only add key of second header' }, { source: ['Content-Type:\r\n text/plain', 'Foo:\r\n bar\r\n baz' ].join('\r\n') + DCRLF, expected: { 'content-type': [' text/plain'] }, cfg: { maxHeaderSize: 33 }, what: 'should reject a truncated header value ending with CR' }, { source: 'Foo: bar\rbare\r\n\r\n', expected: {}, what: 'Bare CR in header value' }, { source: 'Foo: bar\nbare\r\n\r\n', expected: {}, what: 'Bare LF in header value' }, { source: 'Foo\rbad: bar\r\n\r\n', expected: {}, what: 'Bare CR in header name' }, { source: 'Foo\nbad: bar\r\n\r\n', expected: {}, what: 'Bare LF in header name' }, { source: ['Content-Type:\r\n text/plain', ' : ' ].join('\r\n') + DCRLF, expected: { 'content-type': [' text/plain : '] }, what: 'should not break if invalid header pair (colon exists but empty key and value) is provided' }, { source: ['Content-Type:\r\n text/plain', 'FoobazFoobazFoobazFoobazFoobazFoobazFoobazFoobazFoobazFoobazFoobazFoobazFoobazFoobazFoobazFoobazFoobazFoobazFoobazFoobaz' ].join('\r\n') + DCRLF, expected: { 'content-type': [' text/plain'] }, what: 'should not break if invalid header pair (no distinctive colon) is provided' }, { source: ['Content-Type:\r\n text/plain', ':FoobazFoobazFoobazFoobazFoobazFoobazFoobazFoobazFoobazFoobazFoobazFoobazFoobazFoobazFoobazFoobazFoobazFoobazFoobazFoobaz' ].join('\r\n') + DCRLF, expected: { 'content-type': [' text/plain'] }, what: 'should not break if invalid header pair (no key) is provided' }, { source: ['Content-Type:\t text/plain', 'Content-Length:0' ].join('\r\n') + DCRLF, cfg: { maxHeaderPairs: 2 }, expected: { 'content-type': [' text/plain'], 'content-length': ['0'] }, what: 'should enforce maxHeaderPairs of 2' }, { source: ['Content-Type:\r\n text/plain', 'Foo:\r\n bar\r\n baz' ].join('\r\n') + DCRLF, expected: { 'content-type': [' text/plain'], foo: [' bar baz'] }, what: 'Folded values' }, { source: [ 'Foo: bar', 'Foo: baz' ].join('\r\n') + DCRLF, expected: { foo: ['bar', 'baz'] }, what: 'Folded values' }, { source: ['Content-Type:', 'Foo: ' ].join('\r\n') + DCRLF, expected: { 'content-type': [''], foo: [''] }, what: 'Empty values' }, { source: MAXED_BUFFER.toString('ascii') + DCRLF, expected: {}, what: 'Max header size (single chunk)' }, { source: ['ABCDEFGHIJ', MAXED_BUFFER.toString('ascii'), DCRLF], expected: {}, what: 'Max header size (multiple chunks #1)' }, { source: [MAXED_BUFFER.toString('ascii'), MAXED_BUFFER.toString('ascii'), DCRLF], expected: {}, what: 'Max header size (multiple chunk #2)' } ] t.plan(tests.length) for (const v of tests) { await t.test(v.what, t => { t.plan(4) const cfg = { ...v.cfg } const parser = Object.keys(cfg).length ? new HeaderParser(cfg) : new HeaderParser() let fired = false parser.on('header', function (header) { t.assert.ok(!fired, `${v.what}: Header event fired more than once`) fired = true t.assert.deepStrictEqual({ ...header }, v.expected, `${v.what}: Parsed result mismatch`) }) if (!Array.isArray(v.source)) { v.source = [v.source] } v.source.forEach(function (s) { parser.push(s) }) t.assert.ok(fired, `${v.what}: Did not receive header from parser`) t.assert.ok('passed') }) } }) test('dicer-headerparser protects prototype property names', t => { for (const name of ['__proto__', 'constructor']) { const parser = new HeaderParser() let header parser.on('header', value => { header = value }) parser.push(`${name}: injected\r\n\r\n`) t.assert.strictEqual(Object.getPrototypeOf(header), null) t.assert.deepStrictEqual(header[name], ['injected']) } })