diff --git a/src/headers.js b/src/headers.js index 3a701a7..ab5c37d 100644 --- a/src/headers.js +++ b/src/headers.js @@ -4,43 +4,33 @@ const getBrowser = require('./browser') const HEADERS_ORDER = require('./headers-order.json') -const getHeader = (key, headers) => { - let result +const NO_ORDER = { keys: [], set: new Set() } - for (const [header, value] of Object.entries(headers)) { - if (header.toLowerCase() === key) { - result = value - break - } - } +const ORDERS = Object.fromEntries( + Object.entries(HEADERS_ORDER).map(([browser, keys]) => [browser, { keys, set: new Set(keys) }]) +) - return result +const getHeader = (key, headers) => { + const headerKeys = Object.keys(headers) + for (let i = 0; i < headerKeys.length; i++) { + if (headerKeys[i].toLowerCase() === key) return headers[headerKeys[i]] + } } const sortHeaders = headers => { const userAgent = getHeader('user-agent', headers) - const browser = getBrowser(userAgent) - const order = HEADERS_ORDER[browser] || [] + const { keys, set } = ORDERS[getBrowser(userAgent)] || NO_ORDER const orderedHeaders = {} - // Pre-calculate order as a Set for O(1) lookup - const orderSet = new Set(order) - const headerKeys = Object.keys(headers) - const headersLength = headerKeys.length - const orderLength = order.length - let i = 0 - let key - - // Add ordered headers - for (; i < orderLength; i++) { - key = order[i] + for (let i = 0; i < keys.length; i++) { + const key = keys[i] if (key in headers) orderedHeaders[key] = headers[key] } - // Add remaining headers - for (i = 0; i < headersLength; i++) { - key = headerKeys[i] - if (!orderSet.has(key)) orderedHeaders[key] = headers[key] + const headerKeys = Object.keys(headers) + for (let i = 0; i < headerKeys.length; i++) { + const key = headerKeys[i] + if (!set.has(key)) orderedHeaders[key] = headers[key] } return orderedHeaders diff --git a/test/headers.js b/test/headers.js index 33fe34c..65a2cfa 100644 --- a/test/headers.js +++ b/test/headers.js @@ -6,13 +6,32 @@ const { sortHeaders, getHeader } = require('..') const { UA } = require('./helpers') +const assertHeaders = (t, actual, expected) => { + t.deepEqual(Object.keys(actual), Object.keys(expected)) + t.deepEqual(actual, expected) +} + test('.getHeader', t => { t.is(getHeader('user-agent', { 'user-agent': 'googlebot' }), 'googlebot') t.is(getHeader('user-agent', { 'User-Agent': 'googlebot' }), 'googlebot') + t.is( + getHeader('user-agent', { + accept: 'a', + 'User-Agent': 'first', + 'user-agent': 'second' + }), + 'first' + ) + t.is(getHeader('user-agent', { accept: 'a', host: 'h' }), undefined) + t.is(getHeader('User-Agent', { 'user-agent': 'googlebot' }), undefined) + t.is(getHeader('user-agent', { accept: 'a', 'user-agent': '' }), '') + t.throws(() => getHeader('user-agent', null), { instanceOf: TypeError }) + t.throws(() => getHeader('user-agent', undefined), { instanceOf: TypeError }) }) test('.sortHeaders', t => { - t.deepEqual( + assertHeaders( + t, sortHeaders({ 'x-random': 'foo', 'user-agent': UA.CHROME, @@ -23,13 +42,14 @@ test('.sortHeaders', t => { { Host: 'foo', 'user-agent': UA.CHROME, - referer: 'bar', accept: 'foo', + referer: 'bar', 'x-random': 'foo' } ) - t.deepEqual( + assertHeaders( + t, sortHeaders({ 'x-random': 'foo', 'user-agent': 'safari', @@ -37,10 +57,117 @@ test('.sortHeaders', t => { accept: 'foo' }), { - accept: 'foo', referer: 'bar', + accept: 'foo', 'user-agent': 'safari', 'x-random': 'foo' } ) }) + +test('sortHeaders orders firefox headers', t => { + assertHeaders( + t, + sortHeaders({ + 'x-random': 'foo', + 'User-Agent': UA.FIREFOX, + accept: 'foo', + Host: 'foo' + }), + { + Host: 'foo', + 'User-Agent': UA.FIREFOX, + accept: 'foo', + 'x-random': 'foo' + } + ) +}) + +test('sortHeaders keeps insertion order when no browser matches', t => { + const headers = { 'z-last': '1', accept: 'a', Host: 'h' } + const sorted = sortHeaders(headers) + assertHeaders(t, sorted, { 'z-last': '1', accept: 'a', Host: 'h' }) + t.not(sorted, headers) + + assertHeaders(t, sortHeaders({ 'user-agent': '', accept: 'a', host: 'h' }), { + 'user-agent': '', + accept: 'a', + host: 'h' + }) + assertHeaders(t, sortHeaders({}), {}) +}) + +test('sortHeaders ignores an inherited user-agent', t => { + const headers = Object.create({ 'user-agent': UA.CHROME, accept: 'inherited' }) + headers['x-own'] = 'yes' + + assertHeaders(t, sortHeaders(headers), { 'x-own': 'yes' }) +}) + +test('sortHeaders copies inherited ordered headers and skips inherited extras', t => { + const headers = Object.create({ accept: 'inherited', 'x-inherited': 'no' }) + headers['user-agent'] = UA.CHROME + headers['x-own'] = 'yes' + + assertHeaders(t, sortHeaders(headers), { + 'user-agent': UA.CHROME, + accept: 'inherited', + 'x-own': 'yes' + }) +}) + +test('sortHeaders keeps a casing that is not in the browser order', t => { + assertHeaders( + t, + sortHeaders({ + host: 'lower', + 'user-agent': UA.CHROME, + Host: 'upper' + }), + { + Host: 'upper', + 'user-agent': UA.CHROME, + host: 'lower' + } + ) +}) + +test('sortHeaders reads an ordered header once', t => { + let reads = 0 + const headers = { 'user-agent': UA.CHROME } + Object.defineProperty(headers, 'accept', { + enumerable: true, + get () { + reads += 1 + return 'foo' + } + }) + headers['x-random'] = 'z' + + assertHeaders(t, sortHeaders(headers), { + 'user-agent': UA.CHROME, + accept: 'foo', + 'x-random': 'z' + }) + t.is(reads, 1) +}) + +test('sortHeaders orders a null-prototype map', t => { + const headers = Object.create(null) + headers.Host = 'h' + headers['x-random'] = 'z' + headers['user-agent'] = UA.CHROME + + const sorted = sortHeaders(headers) + assertHeaders(t, sorted, { + Host: 'h', + 'user-agent': UA.CHROME, + 'x-random': 'z' + }) + t.not(sorted, headers) +}) + +test('sortHeaders throws on null and undefined', t => { + t.throws(() => sortHeaders(null), { instanceOf: TypeError }) + t.throws(() => sortHeaders(undefined), { instanceOf: TypeError }) +}) diff --git a/test/hook.js b/test/hook.js index 92ff940..ac64d7f 100644 --- a/test/hook.js +++ b/test/hook.js @@ -5,6 +5,7 @@ const userAgents = require('top-user-agents') const test = require('ava').default const decorate = require('../hook') +const { UA } = require('./helpers') const randomUserAgent = uniqueRandomArray(userAgents) @@ -29,3 +30,14 @@ test('does nothing if user agent is not present', t => { t.deepEqual(Object.keys(options.https), []) }) + +test('reads a mixed-case user-agent that is not the first header', t => { + const options = { + https: {}, + headers: { accept: 'text/html', 'User-Agent': UA.CHROME } + } + + decorate(options) + + t.is(options.https.minVersion, 'TLSv1') +})