UNPKG

grafserv

Version:

A highly optimized server for GraphQL, powered by Grafast

468 lines (466 loc) 18.2 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.makeAcceptMatcher = makeAcceptMatcher; const tslib_1 = require("tslib"); const lru_1 = tslib_1.__importDefault(require("@graphile/lru")); /** * Returns a function that returns the (first, if multiple equal matches) type * from mediaTypes that best matches the accept query specified by the given * `acceptHeader`. If no Accept header is present then the first mediaType will * be returned. If no match is possible, `null` will be returned. */ function makeAcceptMatcher(mediaTypes) { const typeDigests = mediaTypes.map((t) => { // NOTE: this parsing is super lazy and isn't 100% reliable; e.g. it could // be broken by `foo/bar;baz="\\";frog"`. We're only handling values passed // by our own code though, and we ain't passing this kind of nonsense. const [spec, ...params] = t.split(";"); const parameters = Object.create(null); for (const param of params) { const [key, val] = param.split("="); parameters[key] = val; } const [type, subtype] = spec.split("/"); return { type, subtype, parameters, q: 1, originalType: t, noParams: Object.keys(parameters).length === 0, }; }); const lru = new lru_1.default({ maxLength: 50 }); return function preferredAccept(acceptHeader) { if (acceptHeader === undefined) { return mediaTypes[0]; } const existing = lru.get(acceptHeader); if (existing !== undefined) { return existing; } else { const specs = parseAccepts(acceptHeader); // Find the first spec that matches each, then pick the one with the // highest q. let bestQ = 0; let bestMediaType = null; for (const digest of typeDigests) { const highestPrecedenceSpecMatch = specs.find((spec) => { return ((spec.type === "*" || (spec.type === digest.type && (spec.subtype === "*" || spec.subtype === digest.subtype))) && (spec.noParams || (!digest.noParams && matchesParameters(spec.parameters, digest.parameters)))); }); if (highestPrecedenceSpecMatch) { if (bestMediaType === null || highestPrecedenceSpecMatch.q > bestQ) { bestQ = highestPrecedenceSpecMatch.q; bestMediaType = digest.originalType; } } } lru.set(acceptHeader, bestMediaType); return bestMediaType; } }; } function matchesParameters(required, given) { for (const key in required) { if (given[key] !== required[key]) { return false; } } return true; } const SPACE = " ".charCodeAt(0); const HORIZONTAL_TAB = "\t".charCodeAt(0); const ASTERISK = "*".charCodeAt(0); const SLASH = "/".charCodeAt(0); const COMMA = ",".charCodeAt(0); const SEMICOLON = ";".charCodeAt(0); const EQUALS = "=".charCodeAt(0); const DOUBLE_QUOTE = '"'.charCodeAt(0); const BACKSLASH = "\\".charCodeAt(0); const DEL = 0x7f; /* * Whitespace: * 9 (tab) * 10 (line feed) * 11 (vertical tab) * 12 (form feed) * 13 (carriage return) * 32 (space) */ const WHITESPACE_START = 9; const WHITESPACE_END = 13; /** We're more forgiving in whitespace in most cases */ function isWhitespace(charCode) { return (charCode === SPACE || (charCode >= WHITESPACE_START && charCode <= WHITESPACE_END)); } /** is Optional White Space */ function isOWS(charCode) { return charCode === SPACE || charCode === HORIZONTAL_TAB; } /* "!" / "#" / "$" / "%" / "&" / "'" / "*" / "+" / "-" / "." / "^" / "_" / "`" / "|" / "~" / DIGIT / ALPHA 33|35-39|42|43|45-57|65-90|94-122|124|126 >=33 && <= 126 && !34|40|41|44|58-64|91-93|123|125 */ // Matches ordered from most likely to least likely for content types. function isToken(charCode) { return ( // ^_`a-z (charCode >= 94 && charCode <= 122) || // symbols and numbers (charCode >= 35 && charCode <= 57 && charCode !== 40 && charCode !== 41 && charCode !== 44) || // A-Z (charCode >= 65 && charCode <= 90) || // ! charCode === 33 || // | charCode === 124 || // ~ charCode === 126); } var State; (function (State) { State[State["EXPECT_TYPE"] = 0] = "EXPECT_TYPE"; State[State["CONTINUE_TYPE"] = 1] = "CONTINUE_TYPE"; State[State["EXPECT_SUBTYPE"] = 2] = "EXPECT_SUBTYPE"; State[State["CONTINUE_SUBTYPE"] = 3] = "CONTINUE_SUBTYPE"; State[State["EXPECT_COMMA_OR_SEMICOLON"] = 4] = "EXPECT_COMMA_OR_SEMICOLON"; State[State["EXPECT_PARAMETER_NAME"] = 5] = "EXPECT_PARAMETER_NAME"; State[State["CONTINUE_PARAMETER_NAME"] = 6] = "CONTINUE_PARAMETER_NAME"; State[State["EXPECT_PARAMETER_VALUE"] = 7] = "EXPECT_PARAMETER_VALUE"; State[State["CONTINUE_PARAMETER_VALUE"] = 8] = "CONTINUE_PARAMETER_VALUE"; State[State["CONTINUE_QUOTED_PARAMETER_VALUE"] = 9] = "CONTINUE_QUOTED_PARAMETER_VALUE"; })(State || (State = {})); // PERF: we could increase the speed of this significantly by checking the // type/subtype against the supported types/subtypes, and if a match is not // found then skip `i` right up to the next `,` without adding the entry to // `accepts` /** * Parser based on https://httpwg.org/specs/rfc9110.html#rfc.section.12.5.1 * * @remarks * * Why must you always write your own parsers, Benjie? */ function parseAccepts(acceptHeader) { const accepts = []; let state = State.EXPECT_TYPE; let currentAccept = null; let currentParameterName = ""; let currentParameterValue = ""; function next() { if (currentAccept.parameters.q) { const q = parseFloat(currentAccept.parameters.q); if (Number.isNaN(q) || q < 0 || q > 1) { throw new Error("q out of range"); } delete currentAccept.parameters.q; currentAccept.q = q; } accepts.push(currentAccept); currentAccept = null; state = State.EXPECT_TYPE; } for (let i = 0, l = acceptHeader.length; i < l; i++) { const charCode = acceptHeader.charCodeAt(i); switch (state) { case State.EXPECT_TYPE: { if ( /*@__INLINE__*/isWhitespace(charCode)) { continue; } else if (charCode === ASTERISK) { // `*/*` currentAccept = { type: "*", subtype: "*", q: 1, parameters: Object.create(null), noParams: true, }; const nextCharCode = acceptHeader.charCodeAt(++i); if (nextCharCode !== SLASH) { throw new Error("Expected '/' after '*'"); } const nextNextCharCode = acceptHeader.charCodeAt(++i); if (nextNextCharCode !== ASTERISK) { throw new Error("Expected '*' after '*/'"); } state = State.EXPECT_COMMA_OR_SEMICOLON; } else if ( /*@__INLINE__*/isToken(charCode)) { currentAccept = { type: acceptHeader[i], subtype: "", q: 1, parameters: Object.create(null), noParams: true, }; state = State.CONTINUE_TYPE; } else { throw new Error(`Unexpected character '${acceptHeader[i]}'`); } break; } case State.CONTINUE_TYPE: { if (charCode === SLASH) { state = State.EXPECT_SUBTYPE; } else if ( /*@__INLINE__*/isToken(charCode)) { currentAccept.type += acceptHeader[i]; } else { throw new Error(`Unexpected character '${acceptHeader[i]}'`); } break; } case State.EXPECT_SUBTYPE: { if (charCode === ASTERISK) { currentAccept.subtype = "*"; state = State.EXPECT_COMMA_OR_SEMICOLON; } else if ( /*@__INLINE__*/isToken(charCode)) { currentAccept.subtype = acceptHeader[i]; state = State.CONTINUE_SUBTYPE; } else { throw new Error(`Unexpected character '${acceptHeader[i]}'`); } break; } case State.CONTINUE_SUBTYPE: { if (charCode === SEMICOLON) { // Parameters state = State.EXPECT_PARAMETER_NAME; } else if (charCode === COMMA) { /*@__INLINE__*/ next(); } else if ( /*@__INLINE__*/isToken(charCode)) { currentAccept.subtype += acceptHeader[i]; } else if ( /*@__INLINE__*/isWhitespace(charCode)) { state = State.EXPECT_COMMA_OR_SEMICOLON; } else { throw new Error(`Unexpected character '${acceptHeader[i]}'`); } break; } case State.EXPECT_COMMA_OR_SEMICOLON: { if ( /*@__INLINE__*/isWhitespace(charCode)) { continue; } else if (charCode === SEMICOLON) { state = State.EXPECT_PARAMETER_NAME; } else if (charCode === COMMA) { /*@__INLINE__*/ next(); } else { throw new Error(`Unexpected character '${acceptHeader[i]}'`); } break; } case State.EXPECT_PARAMETER_NAME: { if (charCode === SEMICOLON) { continue; } else if (charCode === COMMA) { /*@__INLINE__*/ next(); continue; } else if ( /*@__INLINE__*/isOWS(charCode)) { continue; } else if ( /*@__INLINE__*/isToken(charCode)) { currentParameterName = acceptHeader[i]; currentParameterValue = ""; state = State.CONTINUE_PARAMETER_NAME; } else { throw new Error(`Unexpected character '${acceptHeader[i]}'`); } break; } case State.CONTINUE_PARAMETER_NAME: { if (charCode === EQUALS) { state = State.EXPECT_PARAMETER_VALUE; /* if (currentAccept?.parameters[currentParameterName]) { throw new Error("Overriding parameter!"); } */ // "q" is not a valid parameter name; it's just used for weighting. if (currentParameterName !== "q") { currentAccept.noParams = false; } } else if ( /*@__INLINE__*/isToken(charCode)) { currentParameterName += acceptHeader[i]; } else { throw new Error(`Unexpected character '${acceptHeader[i]}'`); } break; } case State.EXPECT_PARAMETER_VALUE: { if (charCode === DOUBLE_QUOTE) { state = State.CONTINUE_QUOTED_PARAMETER_VALUE; } else if ( /*@__INLINE__*/isToken(charCode)) { state = State.CONTINUE_PARAMETER_VALUE; currentParameterValue += acceptHeader[i]; } else { throw new Error(`Unexpected character '${acceptHeader[i]}'`); } break; } case State.CONTINUE_QUOTED_PARAMETER_VALUE: { if (charCode === DOUBLE_QUOTE) { currentAccept.parameters[currentParameterName] = currentParameterValue; state = State.EXPECT_COMMA_OR_SEMICOLON; } else if (charCode === BACKSLASH) { if (++i === l) { throw new Error(`Unexpected terminating backslash`); } // From the spec: // // > A sender SHOULD NOT generate a quoted-pair in a quoted-string // > except where necessary to quote DQUOTE and backslash octets // > occurring within that string. A sender SHOULD NOT generate a // > quoted-pair in a comment except where necessary to quote // > parentheses ["(" and ")"] and backslash octets occurring within // > that comment. // // i.e. this isn't for `\n` and `\t` and similar, those would just // come out as "n" and "t" in the output. This is specifically for // escaping quote marks, parenthesis, backslashes. // Respect `quoted-pair = "\" ( HTAB / SP / VCHAR / obs-text )` if (charCode === HORIZONTAL_TAB || (charCode >= 0x20 && charCode <= 0xff && charCode !== DEL)) { currentParameterValue += acceptHeader[i]; } else { throw new Error(`Unexpected escaped character with code '${charCode}' at position ${i}`); } } else { // Respect `qdtext = HTAB / SP / %x21 / %x23-5B / %x5D-7E / obs-text` // 0x09 0x20-0xff !0x22=`"` !0x5c=`\` !0x7f=DEL if (charCode === HORIZONTAL_TAB || (charCode >= 0x20 && charCode <= 0xff && /* charCode !== DOUBLE_QUOTE && */ /* charCode !== BACKSLASH && */ charCode !== DEL)) { currentParameterValue += acceptHeader[i]; } else { throw new Error(`Unexpected character with code '${charCode}' at position ${i}.`); } } break; } case State.CONTINUE_PARAMETER_VALUE: { if (charCode === SEMICOLON) { currentAccept.parameters[currentParameterName] = currentParameterValue; // Parameters state = State.EXPECT_PARAMETER_NAME; } else if (charCode === COMMA) { currentAccept.parameters[currentParameterName] = currentParameterValue; /*@__INLINE__*/ next(); } else if ( /*@__INLINE__*/isToken(charCode)) { currentParameterValue += acceptHeader[i]; } else { throw new Error(`Unexpected character '${acceptHeader[i]}'`); } break; } default: { const never = state; throw new Error(`Unhandled state '${never}'`); } } } // Now finish parsing switch (state) { case State.EXPECT_TYPE: case State.CONTINUE_SUBTYPE: case State.EXPECT_COMMA_OR_SEMICOLON: { /*@__INLINE__*/ next(); break; } case State.CONTINUE_PARAMETER_VALUE: { currentAccept.parameters[currentParameterName] = currentParameterValue; /*@__INLINE__*/ next(); break; } case State.CONTINUE_TYPE: { throw new Error("Invalid 'accept' header, expected slash"); } case State.EXPECT_SUBTYPE: { throw new Error("Invalid 'accept' header, expected subtype"); } case State.EXPECT_PARAMETER_NAME: { throw new Error("Invalid 'accept' header, expected parameter name"); } case State.CONTINUE_PARAMETER_NAME: { throw new Error("Invalid 'accept' header, expected parameter value"); } case State.EXPECT_PARAMETER_VALUE: { throw new Error("Invalid 'accept' header, expected parameter value"); } case State.CONTINUE_QUOTED_PARAMETER_VALUE: { throw new Error("Invalid 'accept' header, expected closing quote"); } default: { const never = state; throw new Error(`Unhandled terminal state '${never}'`); } } // Sort `accepts` by precedence. Precedence is how accurate the match is: // a/b;c=d // a/b // a/* // */* const score = (accept) => { let val = 0; if (accept.type !== "*") { val += 1_000; } if (accept.subtype !== "*") { val += 1_000_000; } val += Object.keys(accept.parameters).length; return val; }; accepts.sort((a, z) => { const scoreA = score(a); const scoreZ = score(z); return scoreZ - scoreA; }); return accepts; } //# sourceMappingURL=accept.js.map