233 lines
6.1 KiB
JavaScript
233 lines
6.1 KiB
JavaScript
'use strict';
|
|
|
|
const Hoek = require('@hapi/hoek');
|
|
const Boom = require('@hapi/boom');
|
|
|
|
|
|
const internals = {};
|
|
|
|
|
|
exports.selection = function (header, preferences, options) {
|
|
|
|
const selections = exports.selections(header, preferences, options);
|
|
return selections.length ? selections[0] : '';
|
|
};
|
|
|
|
|
|
exports.selections = function (header, preferences, options) {
|
|
|
|
Hoek.assert(!preferences || Array.isArray(preferences), 'Preferences must be an array');
|
|
|
|
return internals.parse(header || '', preferences, options);
|
|
};
|
|
|
|
|
|
// RFC 7231 Section 5.3.3 (https://tools.ietf.org/html/rfc7231#section-5.3.3)
|
|
//
|
|
// Accept-Charset = *( "," OWS ) ( ( charset / "*" ) [ weight ] ) *( OWS "," [ OWS ( ( charset / "*" ) [ weight ] ) ] )
|
|
// charset = token
|
|
//
|
|
// Accept-Charset: iso-8859-5, unicode-1-1;q=0.8
|
|
|
|
|
|
// RFC 7231 Section 5.3.4 (https://tools.ietf.org/html/rfc7231#section-5.3.4)
|
|
//
|
|
// Accept-Encoding = [ ( "," / ( codings [ weight ] ) ) *( OWS "," [ OWS ( codings [ weight ] ) ] ) ]
|
|
// codings = content-coding / "identity" / "*"
|
|
// content-coding = token
|
|
//
|
|
// Accept-Encoding: compress, gzip
|
|
// Accept-Encoding:
|
|
// Accept-Encoding: *
|
|
// Accept-Encoding: compress;q=0.5, gzip;q=1.0
|
|
// Accept-Encoding: gzip;q=1.0, identity; q=0.5, *;q=0
|
|
|
|
|
|
// RFC 7231 Section 5.3.5 (https://tools.ietf.org/html/rfc7231#section-5.3.5)
|
|
//
|
|
// Accept-Language = *( "," OWS ) ( language-range [ weight ] ) *( OWS "," [ OWS ( language-range [ weight ] ) ] )
|
|
// language-range = ( 1*8ALPHA *( "-" 1*8alphanum ) ) / "*" ; [RFC4647], Section 2.1
|
|
// alphanum = ALPHA / DIGIT
|
|
//
|
|
// Accept-Language: da, en-gb;q=0.8, en;q=0.7
|
|
|
|
|
|
// token = 1*tchar
|
|
// tchar = "!" / "#" / "$" / "%" / "&" / "'" / "*"
|
|
// / "+" / "-" / "." / "^" / "_" / "`" / "|" / "~"
|
|
// / DIGIT / ALPHA
|
|
// ; any VCHAR, except delimiters
|
|
// OWS = *( SP / HTAB )
|
|
|
|
|
|
// RFC 7231 Section 5.3.1 (https://tools.ietf.org/html/rfc7231#section-5.3.1)
|
|
//
|
|
// The weight is normalized to a real number in the range 0 through 1,
|
|
// where 0.001 is the least preferred and 1 is the most preferred; a
|
|
// value of 0 means "not acceptable". If no "q" parameter is present,
|
|
// the default weight is 1.
|
|
//
|
|
// weight = OWS ";" OWS "q=" qvalue
|
|
// qvalue = ( "0" [ "." 0*3DIGIT ] ) / ( "1" [ "." 0*3("0") ] )
|
|
|
|
|
|
internals.parse = function (raw, preferences, options) {
|
|
|
|
// Normalize header (remove spaces and tabs)
|
|
|
|
const header = raw.replace(/[ \t]/g, '');
|
|
|
|
// Normalize preferences
|
|
|
|
const lowers = new Map();
|
|
if (preferences) {
|
|
let pos = 0;
|
|
for (const preference of preferences) {
|
|
const lower = preference.toLowerCase();
|
|
lowers.set(lower, { orig: preference, pos: pos++ });
|
|
|
|
if (options.prefixMatch) {
|
|
const parts = lower.split('-');
|
|
while (parts.pop(), parts.length > 0) {
|
|
const joined = parts.join('-');
|
|
if (!lowers.has(joined)) {
|
|
lowers.set(joined, { orig: preference, pos: pos++ });
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Parse selections
|
|
|
|
const parts = header.split(',');
|
|
const selections = [];
|
|
const map = new Set();
|
|
|
|
for (let i = 0; i < parts.length; ++i) {
|
|
const part = parts[i];
|
|
if (!part) { // Ignore empty parts or leading commas
|
|
continue;
|
|
}
|
|
|
|
// Parse parameters
|
|
|
|
const params = part.split(';');
|
|
if (params.length > 2) {
|
|
throw Boom.badRequest(`Invalid ${options.type} header`);
|
|
}
|
|
|
|
let token = params[0].toLowerCase();
|
|
if (!token) {
|
|
throw Boom.badRequest(`Invalid ${options.type} header`);
|
|
}
|
|
|
|
if (options.equivalents?.has(token)) {
|
|
token = options.equivalents.get(token);
|
|
}
|
|
|
|
const selection = {
|
|
token,
|
|
pos: i,
|
|
q: 1
|
|
};
|
|
|
|
if (preferences &&
|
|
lowers.has(token)) {
|
|
|
|
selection.pref = lowers.get(token).pos;
|
|
}
|
|
|
|
map.add(selection.token);
|
|
|
|
// Parse q=value
|
|
|
|
if (params.length === 2) {
|
|
const q = params[1];
|
|
const [key, value] = q.split('=');
|
|
|
|
if (!value ||
|
|
key !== 'q' && key !== 'Q') {
|
|
|
|
throw Boom.badRequest(`Invalid ${options.type} header`);
|
|
}
|
|
|
|
const score = parseFloat(value);
|
|
if (score === 0) {
|
|
continue;
|
|
}
|
|
|
|
if (Number.isFinite(score) &&
|
|
score <= 1 &&
|
|
score >= 0.001) {
|
|
|
|
selection.q = score;
|
|
}
|
|
}
|
|
|
|
selections.push(selection); // Only add allowed selections (q !== 0)
|
|
}
|
|
|
|
// Sort selection based on q and then position in header
|
|
|
|
selections.sort(internals.sort);
|
|
|
|
// Extract tokens
|
|
|
|
const values = selections.map((selection) => selection.token);
|
|
|
|
if (options.default &&
|
|
!map.has(options.default)) {
|
|
|
|
values.push(options.default);
|
|
}
|
|
|
|
if (!preferences?.length) {
|
|
return values;
|
|
}
|
|
|
|
const preferred = [];
|
|
for (const selection of values) {
|
|
if (selection === '*') {
|
|
for (const [preference, value] of lowers) {
|
|
if (!map.has(preference)) {
|
|
preferred.push(value.orig);
|
|
}
|
|
}
|
|
}
|
|
else {
|
|
const lower = selection.toLowerCase();
|
|
if (lowers.has(lower)) {
|
|
preferred.push(lowers.get(lower).orig);
|
|
}
|
|
}
|
|
}
|
|
|
|
return preferred;
|
|
};
|
|
|
|
|
|
internals.sort = function (a, b) {
|
|
|
|
const aFirst = -1;
|
|
const bFirst = 1;
|
|
|
|
if (b.q !== a.q) {
|
|
return b.q - a.q;
|
|
}
|
|
|
|
if (b.pref !== a.pref) {
|
|
if (a.pref === undefined) {
|
|
return bFirst;
|
|
}
|
|
|
|
if (b.pref === undefined) {
|
|
return aFirst;
|
|
}
|
|
|
|
return a.pref - b.pref;
|
|
}
|
|
|
|
return a.pos - b.pos;
|
|
};
|