// Flags: --expose-internals 'use strict'; // Tests the internal utility functions that are used to prepare headers // to pass to the internal binding layer and to build a header object. const common = require('../common'); if (!common.hasCrypto) common.skip('missing crypto'); const assert = require('assert'); const { mapToHeaders, toHeaderObject } = require('internal/http2/util'); const { internalBinding } = require('internal/test/binding'); const { HTTP2_HEADER_STATUS, HTTP2_HEADER_METHOD, HTTP2_HEADER_AUTHORITY, HTTP2_HEADER_SCHEME, HTTP2_HEADER_PATH, HTTP2_HEADER_ACCESS_CONTROL_ALLOW_CREDENTIALS, HTTP2_HEADER_ACCESS_CONTROL_MAX_AGE, HTTP2_HEADER_ACCESS_CONTROL_REQUEST_METHOD, HTTP2_HEADER_AGE, HTTP2_HEADER_AUTHORIZATION, HTTP2_HEADER_CONTENT_ENCODING, HTTP2_HEADER_CONTENT_LANGUAGE, HTTP2_HEADER_CONTENT_LENGTH, HTTP2_HEADER_CONTENT_LOCATION, HTTP2_HEADER_CONTENT_MD5, HTTP2_HEADER_CONTENT_RANGE, HTTP2_HEADER_CONTENT_TYPE, HTTP2_HEADER_DATE, HTTP2_HEADER_DNT, HTTP2_HEADER_ETAG, HTTP2_HEADER_EXPIRES, HTTP2_HEADER_FROM, HTTP2_HEADER_IF_MATCH, HTTP2_HEADER_IF_MODIFIED_SINCE, HTTP2_HEADER_IF_NONE_MATCH, HTTP2_HEADER_IF_RANGE, HTTP2_HEADER_IF_UNMODIFIED_SINCE, HTTP2_HEADER_LAST_MODIFIED, HTTP2_HEADER_LOCATION, HTTP2_HEADER_MAX_FORWARDS, HTTP2_HEADER_PROXY_AUTHORIZATION, HTTP2_HEADER_RANGE, HTTP2_HEADER_REFERER, HTTP2_HEADER_RETRY_AFTER, HTTP2_HEADER_TK, HTTP2_HEADER_UPGRADE_INSECURE_REQUESTS, HTTP2_HEADER_USER_AGENT, HTTP2_HEADER_X_CONTENT_TYPE_OPTIONS, HTTP2_HEADER_ACCEPT_CHARSET, HTTP2_HEADER_ACCEPT_ENCODING, HTTP2_HEADER_ACCEPT_LANGUAGE, HTTP2_HEADER_ACCEPT_RANGES, HTTP2_HEADER_ACCEPT, HTTP2_HEADER_ACCESS_CONTROL_ALLOW_HEADERS, HTTP2_HEADER_ACCESS_CONTROL_ALLOW_METHODS, HTTP2_HEADER_ACCESS_CONTROL_ALLOW_ORIGIN, HTTP2_HEADER_ACCESS_CONTROL_EXPOSE_HEADERS, HTTP2_HEADER_ACCESS_CONTROL_REQUEST_HEADERS, HTTP2_HEADER_ALLOW, HTTP2_HEADER_CACHE_CONTROL, HTTP2_HEADER_CONTENT_DISPOSITION, HTTP2_HEADER_COOKIE, HTTP2_HEADER_EXPECT, HTTP2_HEADER_FORWARDED, HTTP2_HEADER_LINK, HTTP2_HEADER_PREFER, HTTP2_HEADER_PROXY_AUTHENTICATE, HTTP2_HEADER_REFRESH, HTTP2_HEADER_SERVER, HTTP2_HEADER_SET_COOKIE, HTTP2_HEADER_STRICT_TRANSPORT_SECURITY, HTTP2_HEADER_TRAILER, HTTP2_HEADER_VARY, HTTP2_HEADER_VIA, HTTP2_HEADER_WARNING, HTTP2_HEADER_WWW_AUTHENTICATE, HTTP2_HEADER_X_FRAME_OPTIONS, HTTP2_HEADER_CONNECTION, HTTP2_HEADER_UPGRADE, HTTP2_HEADER_HTTP2_SETTINGS, HTTP2_HEADER_TE, HTTP2_HEADER_TRANSFER_ENCODING, HTTP2_HEADER_HOST, HTTP2_HEADER_KEEP_ALIVE, HTTP2_HEADER_PROXY_CONNECTION } = internalBinding('http2').constants; { const headers = { 'abc': 1, ':status': 200, ':path': 'abc', 'xyz': [1, '2', { toString() { return '3'; } }, 4], 'foo': [], 'BAR': [1] }; assert.deepStrictEqual( mapToHeaders(headers), [ [ ':path', 'abc', ':status', '200', 'abc', '1', 'xyz', '1', 'xyz', '2', 'xyz', '3', 'xyz', '4', 'bar', '1', '' ].join('\0'), 8 ] ); } { const headers = { 'abc': 1, ':path': 'abc', ':status': [200], ':authority': [], 'xyz': [1, 2, 3, 4] }; assert.deepStrictEqual( mapToHeaders(headers), [ [ ':status', '200', ':path', 'abc', 'abc', '1', 'xyz', '1', 'xyz', '2', 'xyz', '3', 'xyz', '4', '' ].join('\0'), 7 ] ); } { const headers = { 'abc': 1, ':path': 'abc', 'xyz': [1, 2, 3, 4], '': 1, ':status': 200, [Symbol('test')]: 1 // Symbol keys are ignored }; assert.deepStrictEqual( mapToHeaders(headers), [ [ ':status', '200', ':path', 'abc', 'abc', '1', 'xyz', '1', 'xyz', '2', 'xyz', '3', 'xyz', '4', '' ].join('\0'), 7 ] ); } { // Only own properties are used const base = { 'abc': 1 }; const headers = Object.create(base); headers[':path'] = 'abc'; headers.xyz = [1, 2, 3, 4]; headers.foo = []; headers[':status'] = 200; assert.deepStrictEqual( mapToHeaders(headers), [ [ ':status', '200', ':path', 'abc', 'xyz', '1', 'xyz', '2', 'xyz', '3', 'xyz', '4', '' ].join('\0'), 6 ] ); } { // Arrays containing a single set-cookie value are handled correctly // (https://github.com/nodejs/node/issues/16452) const headers = { 'set-cookie': ['foo=bar'] }; assert.deepStrictEqual( mapToHeaders(headers), [ [ 'set-cookie', 'foo=bar', '' ].join('\0'), 1 ] ); } { // pseudo-headers are only allowed a single value const headers = { ':status': 200, ':statuS': 204, }; common.expectsError(() => mapToHeaders(headers), { code: 'ERR_HTTP2_HEADER_SINGLE_VALUE', type: TypeError, message: 'Header field ":status" must only have a single value' }); } // The following are not allowed to have multiple values [ HTTP2_HEADER_STATUS, HTTP2_HEADER_METHOD, HTTP2_HEADER_AUTHORITY, HTTP2_HEADER_SCHEME, HTTP2_HEADER_PATH, HTTP2_HEADER_ACCESS_CONTROL_ALLOW_CREDENTIALS, HTTP2_HEADER_ACCESS_CONTROL_MAX_AGE, HTTP2_HEADER_ACCESS_CONTROL_REQUEST_METHOD, HTTP2_HEADER_AGE, HTTP2_HEADER_AUTHORIZATION, HTTP2_HEADER_CONTENT_ENCODING, HTTP2_HEADER_CONTENT_LANGUAGE, HTTP2_HEADER_CONTENT_LENGTH, HTTP2_HEADER_CONTENT_LOCATION, HTTP2_HEADER_CONTENT_MD5, HTTP2_HEADER_CONTENT_RANGE, HTTP2_HEADER_CONTENT_TYPE, HTTP2_HEADER_DATE, HTTP2_HEADER_DNT, HTTP2_HEADER_ETAG, HTTP2_HEADER_EXPIRES, HTTP2_HEADER_FROM, HTTP2_HEADER_IF_MATCH, HTTP2_HEADER_IF_MODIFIED_SINCE, HTTP2_HEADER_IF_NONE_MATCH, HTTP2_HEADER_IF_RANGE, HTTP2_HEADER_IF_UNMODIFIED_SINCE, HTTP2_HEADER_LAST_MODIFIED, HTTP2_HEADER_LOCATION, HTTP2_HEADER_MAX_FORWARDS, HTTP2_HEADER_PROXY_AUTHORIZATION, HTTP2_HEADER_RANGE, HTTP2_HEADER_REFERER, HTTP2_HEADER_RETRY_AFTER, HTTP2_HEADER_TK, HTTP2_HEADER_UPGRADE_INSECURE_REQUESTS, HTTP2_HEADER_USER_AGENT, HTTP2_HEADER_X_CONTENT_TYPE_OPTIONS ].forEach((name) => { const msg = `Header field "${name}" must only have a single value`; common.expectsError(() => mapToHeaders({ [name]: [1, 2, 3] }), { code: 'ERR_HTTP2_HEADER_SINGLE_VALUE', message: msg }); }); [ HTTP2_HEADER_ACCEPT_CHARSET, HTTP2_HEADER_ACCEPT_ENCODING, HTTP2_HEADER_ACCEPT_LANGUAGE, HTTP2_HEADER_ACCEPT_RANGES, HTTP2_HEADER_ACCEPT, HTTP2_HEADER_ACCESS_CONTROL_ALLOW_HEADERS, HTTP2_HEADER_ACCESS_CONTROL_ALLOW_METHODS, HTTP2_HEADER_ACCESS_CONTROL_ALLOW_ORIGIN, HTTP2_HEADER_ACCESS_CONTROL_EXPOSE_HEADERS, HTTP2_HEADER_ACCESS_CONTROL_REQUEST_HEADERS, HTTP2_HEADER_ALLOW, HTTP2_HEADER_CACHE_CONTROL, HTTP2_HEADER_CONTENT_DISPOSITION, HTTP2_HEADER_COOKIE, HTTP2_HEADER_EXPECT, HTTP2_HEADER_FORWARDED, HTTP2_HEADER_LINK, HTTP2_HEADER_PREFER, HTTP2_HEADER_PROXY_AUTHENTICATE, HTTP2_HEADER_REFRESH, HTTP2_HEADER_SERVER, HTTP2_HEADER_SET_COOKIE, HTTP2_HEADER_STRICT_TRANSPORT_SECURITY, HTTP2_HEADER_TRAILER, HTTP2_HEADER_VARY, HTTP2_HEADER_VIA, HTTP2_HEADER_WARNING, HTTP2_HEADER_WWW_AUTHENTICATE, HTTP2_HEADER_X_FRAME_OPTIONS ].forEach((name) => { assert(!(mapToHeaders({ [name]: [1, 2, 3] }) instanceof Error), name); }); [ HTTP2_HEADER_CONNECTION, HTTP2_HEADER_UPGRADE, HTTP2_HEADER_HTTP2_SETTINGS, HTTP2_HEADER_TE, HTTP2_HEADER_TRANSFER_ENCODING, HTTP2_HEADER_HOST, HTTP2_HEADER_PROXY_CONNECTION, HTTP2_HEADER_KEEP_ALIVE, 'Connection', 'Upgrade', 'HTTP2-Settings', 'TE', 'Transfer-Encoding', 'Proxy-Connection', 'Keep-Alive' ].forEach((name) => { common.expectsError(() => mapToHeaders({ [name]: 'abc' }), { code: 'ERR_HTTP2_INVALID_CONNECTION_HEADERS', name: 'TypeError [ERR_HTTP2_INVALID_CONNECTION_HEADERS]', message: 'HTTP/1 Connection specific headers are forbidden: ' + `"${name.toLowerCase()}"` }); }); common.expectsError(() => mapToHeaders({ [HTTP2_HEADER_TE]: ['abc'] }), { code: 'ERR_HTTP2_INVALID_CONNECTION_HEADERS', name: 'TypeError [ERR_HTTP2_INVALID_CONNECTION_HEADERS]', message: 'HTTP/1 Connection specific headers are forbidden: ' + `"${HTTP2_HEADER_TE}"` }); common.expectsError( () => mapToHeaders({ [HTTP2_HEADER_TE]: ['abc', 'trailers'] }), { code: 'ERR_HTTP2_INVALID_CONNECTION_HEADERS', name: 'TypeError [ERR_HTTP2_INVALID_CONNECTION_HEADERS]', message: 'HTTP/1 Connection specific headers are forbidden: ' + `"${HTTP2_HEADER_TE}"` }); // These should not throw mapToHeaders({ te: 'trailers' }); mapToHeaders({ te: ['trailers'] }); { const rawHeaders = [ ':status', '200', 'cookie', 'foo', 'set-cookie', 'sc1', 'age', '10', 'x-multi', 'first' ]; const headers = toHeaderObject(rawHeaders); assert.strictEqual(headers[':status'], 200); assert.strictEqual(headers.cookie, 'foo'); assert.deepStrictEqual(headers['set-cookie'], ['sc1']); assert.strictEqual(headers.age, '10'); assert.strictEqual(headers['x-multi'], 'first'); } { const rawHeaders = [ ':status', '200', ':status', '400', 'cookie', 'foo', 'cookie', 'bar', 'set-cookie', 'sc1', 'set-cookie', 'sc2', 'age', '10', 'age', '20', 'x-multi', 'first', 'x-multi', 'second' ]; const headers = toHeaderObject(rawHeaders); assert.strictEqual(headers[':status'], 200); assert.strictEqual(headers.cookie, 'foo; bar'); assert.deepStrictEqual(headers['set-cookie'], ['sc1', 'sc2']); assert.strictEqual(headers.age, '10'); assert.strictEqual(headers['x-multi'], 'first, second'); }