UNPKG

@montevideo-tech/cmcd-validator

Version:
1,278 lines (1,206 loc) 48.6 kB
function _iterableToArrayLimit(arr, i) { var _i = null == arr ? null : "undefined" != typeof Symbol && arr[Symbol.iterator] || arr["@@iterator"]; if (null != _i) { var _s, _e, _x, _r, _arr = [], _n = !0, _d = !1; try { if (_x = (_i = _i.call(arr)).next, 0 === i) { if (Object(_i) !== _i) return; _n = !1; } else for (; !(_n = (_s = _x.call(_i)).done) && (_arr.push(_s.value), _arr.length !== i); _n = !0); } catch (err) { _d = !0, _e = err; } finally { try { if (!_n && null != _i.return && (_r = _i.return(), Object(_r) !== _r)) return; } finally { if (_d) throw _e; } } return _arr; } } function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); enumerableOnly && (symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; })), keys.push.apply(keys, symbols); } return keys; } function _objectSpread2(target) { for (var i = 1; i < arguments.length; i++) { var source = null != arguments[i] ? arguments[i] : {}; i % 2 ? ownKeys(Object(source), !0).forEach(function (key) { _defineProperty(target, key, source[key]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)) : ownKeys(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } return target; } function _defineProperty(obj, key, value) { key = _toPropertyKey(key); if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } function _slicedToArray(arr, i) { return _arrayWithHoles(arr) || _iterableToArrayLimit(arr, i) || _unsupportedIterableToArray(arr, i) || _nonIterableRest(); } function _arrayWithHoles(arr) { if (Array.isArray(arr)) return arr; } function _unsupportedIterableToArray(o, minLen) { if (!o) return; if (typeof o === "string") return _arrayLikeToArray(o, minLen); var n = Object.prototype.toString.call(o).slice(8, -1); if (n === "Object" && o.constructor) n = o.constructor.name; if (n === "Map" || n === "Set") return Array.from(o); if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _arrayLikeToArray(o, minLen); } function _arrayLikeToArray(arr, len) { if (len == null || len > arr.length) len = arr.length; for (var i = 0, arr2 = new Array(len); i < len; i++) arr2[i] = arr[i]; return arr2; } function _nonIterableRest() { throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } function _toPrimitive(input, hint) { if (typeof input !== "object" || input === null) return input; var prim = input[Symbol.toPrimitive]; if (prim !== undefined) { var res = prim.call(input, hint || "default"); if (typeof res !== "object") return res; throw new TypeError("@@toPrimitive must return a primitive value."); } return (hint === "string" ? String : Number)(input); } function _toPropertyKey(arg) { var key = _toPrimitive(arg, "string"); return typeof key === "symbol" ? key : String(key); } // Unique ID creation requires a high quality random # generator. In the browser we therefore // require the crypto API and do not support built-in fallback to lower quality random number // generators (like Math.random()). let getRandomValues; const rnds8 = new Uint8Array(16); function rng() { // lazy load so that environments that need to polyfill have a chance to do so if (!getRandomValues) { // getRandomValues needs to be invoked in a context where "this" is a Crypto implementation. getRandomValues = typeof crypto !== 'undefined' && crypto.getRandomValues && crypto.getRandomValues.bind(crypto); if (!getRandomValues) { throw new Error('crypto.getRandomValues() not supported. See https://github.com/uuidjs/uuid#getrandomvalues-not-supported'); } } return getRandomValues(rnds8); } /** * Convert array of 16 byte values to UUID string format of the form: * XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX */ const byteToHex = []; for (let i = 0; i < 256; ++i) { byteToHex.push((i + 0x100).toString(16).slice(1)); } function unsafeStringify(arr, offset = 0) { // Note: Be careful editing this code! It's been tuned for performance // and works in ways you may not expect. See https://github.com/uuidjs/uuid/pull/434 return (byteToHex[arr[offset + 0]] + byteToHex[arr[offset + 1]] + byteToHex[arr[offset + 2]] + byteToHex[arr[offset + 3]] + '-' + byteToHex[arr[offset + 4]] + byteToHex[arr[offset + 5]] + '-' + byteToHex[arr[offset + 6]] + byteToHex[arr[offset + 7]] + '-' + byteToHex[arr[offset + 8]] + byteToHex[arr[offset + 9]] + '-' + byteToHex[arr[offset + 10]] + byteToHex[arr[offset + 11]] + byteToHex[arr[offset + 12]] + byteToHex[arr[offset + 13]] + byteToHex[arr[offset + 14]] + byteToHex[arr[offset + 15]]).toLowerCase(); } const randomUUID = typeof crypto !== 'undefined' && crypto.randomUUID && crypto.randomUUID.bind(crypto); var native = { randomUUID }; function v4(options, buf, offset) { if (native.randomUUID && !buf && !options) { return native.randomUUID(); } options = options || {}; const rnds = options.random || (options.rng || rng)(); // Per 4.4, set bits for version and `clock_seq_hi_and_reserved` rnds[6] = rnds[6] & 0x0f | 0x40; rnds[8] = rnds[8] & 0x3f | 0x80; // Copy bytes to buffer, if provided if (buf) { offset = offset || 0; for (let i = 0; i < 16; ++i) { buf[offset + i] = rnds[i]; } return buf; } return unsafeStringify(rnds); } var commonjsGlobal = typeof globalThis !== 'undefined' ? globalThis : typeof window !== 'undefined' ? window : typeof global !== 'undefined' ? global : typeof self !== 'undefined' ? self : {}; function getDefaultExportFromCjs (x) { return x && x.__esModule && Object.prototype.hasOwnProperty.call(x, 'default') ? x['default'] : x; } var logger = {exports: {}}; /*! * js-logger - http://github.com/jonnyreeves/js-logger * Jonny Reeves, http://jonnyreeves.co.uk/ * js-logger may be freely distributed under the MIT license. */ logger.exports; (function (module) { (function (global) { // Top level module for the global, static logger instance. var Logger = {}; // For those that are at home that are keeping score. Logger.VERSION = "1.6.1"; // Function which handles all incoming log messages. var logHandler; // Map of ContextualLogger instances by name; used by Logger.get() to return the same named instance. var contextualLoggersByNameMap = {}; // Polyfill for ES5's Function.bind. var bind = function bind(scope, func) { return function () { return func.apply(scope, arguments); }; }; // Super exciting object merger-matron 9000 adding another 100 bytes to your download. var merge = function merge() { var args = arguments, target = args[0], key, i; for (i = 1; i < args.length; i++) { for (key in args[i]) { if (!(key in target) && args[i].hasOwnProperty(key)) { target[key] = args[i][key]; } } } return target; }; // Helper to define a logging level object; helps with optimisation. var defineLogLevel = function defineLogLevel(value, name) { return { value: value, name: name }; }; // Predefined logging levels. Logger.TRACE = defineLogLevel(1, 'TRACE'); Logger.DEBUG = defineLogLevel(2, 'DEBUG'); Logger.INFO = defineLogLevel(3, 'INFO'); Logger.TIME = defineLogLevel(4, 'TIME'); Logger.WARN = defineLogLevel(5, 'WARN'); Logger.ERROR = defineLogLevel(8, 'ERROR'); Logger.OFF = defineLogLevel(99, 'OFF'); // Inner class which performs the bulk of the work; ContextualLogger instances can be configured independently // of each other. var ContextualLogger = function ContextualLogger(defaultContext) { this.context = defaultContext; this.setLevel(defaultContext.filterLevel); this.log = this.info; // Convenience alias. }; ContextualLogger.prototype = { // Changes the current logging level for the logging instance. setLevel: function setLevel(newLevel) { // Ensure the supplied Level object looks valid. if (newLevel && "value" in newLevel) { this.context.filterLevel = newLevel; } }, // Gets the current logging level for the logging instance getLevel: function getLevel() { return this.context.filterLevel; }, // Is the logger configured to output messages at the supplied level? enabledFor: function enabledFor(lvl) { var filterLevel = this.context.filterLevel; return lvl.value >= filterLevel.value; }, trace: function trace() { this.invoke(Logger.TRACE, arguments); }, debug: function debug() { this.invoke(Logger.DEBUG, arguments); }, info: function info() { this.invoke(Logger.INFO, arguments); }, warn: function warn() { this.invoke(Logger.WARN, arguments); }, error: function error() { this.invoke(Logger.ERROR, arguments); }, time: function time(label) { if (typeof label === 'string' && label.length > 0) { this.invoke(Logger.TIME, [label, 'start']); } }, timeEnd: function timeEnd(label) { if (typeof label === 'string' && label.length > 0) { this.invoke(Logger.TIME, [label, 'end']); } }, // Invokes the logger callback if it's not being filtered. invoke: function invoke(level, msgArgs) { if (logHandler && this.enabledFor(level)) { logHandler(msgArgs, merge({ level: level }, this.context)); } } }; // Protected instance which all calls to the to level `Logger` module will be routed through. var globalLogger = new ContextualLogger({ filterLevel: Logger.OFF }); // Configure the global Logger instance. (function () { // Shortcut for optimisers. var L = Logger; L.enabledFor = bind(globalLogger, globalLogger.enabledFor); L.trace = bind(globalLogger, globalLogger.trace); L.debug = bind(globalLogger, globalLogger.debug); L.time = bind(globalLogger, globalLogger.time); L.timeEnd = bind(globalLogger, globalLogger.timeEnd); L.info = bind(globalLogger, globalLogger.info); L.warn = bind(globalLogger, globalLogger.warn); L.error = bind(globalLogger, globalLogger.error); // Don't forget the convenience alias! L.log = L.info; })(); // Set the global logging handler. The supplied function should expect two arguments, the first being an arguments // object with the supplied log messages and the second being a context object which contains a hash of stateful // parameters which the logging function can consume. Logger.setHandler = function (func) { logHandler = func; }; // Sets the global logging filter level which applies to *all* previously registered, and future Logger instances. // (note that named loggers (retrieved via `Logger.get`) can be configured independently if required). Logger.setLevel = function (level) { // Set the globalLogger's level. globalLogger.setLevel(level); // Apply this level to all registered contextual loggers. for (var key in contextualLoggersByNameMap) { if (contextualLoggersByNameMap.hasOwnProperty(key)) { contextualLoggersByNameMap[key].setLevel(level); } } }; // Gets the global logging filter level Logger.getLevel = function () { return globalLogger.getLevel(); }; // Retrieve a ContextualLogger instance. Note that named loggers automatically inherit the global logger's level, // default context and log handler. Logger.get = function (name) { // All logger instances are cached so they can be configured ahead of use. return contextualLoggersByNameMap[name] || (contextualLoggersByNameMap[name] = new ContextualLogger(merge({ name: name }, globalLogger.context))); }; // CreateDefaultHandler returns a handler function which can be passed to `Logger.setHandler()` which will // write to the window's console object (if present); the optional options object can be used to customise the // formatter used to format each log message. Logger.createDefaultHandler = function (options) { options = options || {}; options.formatter = options.formatter || function defaultMessageFormatter(messages, context) { // Prepend the logger's name to the log message for easy identification. if (context.name) { messages.unshift("[" + context.name + "]"); } }; // Map of timestamps by timer labels used to track `#time` and `#timeEnd()` invocations in environments // that don't offer a native console method. var timerStartTimeByLabelMap = {}; // Support for IE8+ (and other, slightly more sane environments) var invokeConsoleMethod = function invokeConsoleMethod(hdlr, messages) { Function.prototype.apply.call(hdlr, console, messages); }; // Check for the presence of a logger. if (typeof console === "undefined") { return function () {/* no console */}; } return function (messages, context) { // Convert arguments object to Array. messages = Array.prototype.slice.call(messages); var hdlr = console.log; var timerLabel; if (context.level === Logger.TIME) { timerLabel = (context.name ? '[' + context.name + '] ' : '') + messages[0]; if (messages[1] === 'start') { if (console.time) { console.time(timerLabel); } else { timerStartTimeByLabelMap[timerLabel] = new Date().getTime(); } } else { if (console.timeEnd) { console.timeEnd(timerLabel); } else { invokeConsoleMethod(hdlr, [timerLabel + ': ' + (new Date().getTime() - timerStartTimeByLabelMap[timerLabel]) + 'ms']); } } } else { // Delegate through to custom warn/error loggers if present on the console. if (context.level === Logger.WARN && console.warn) { hdlr = console.warn; } else if (context.level === Logger.ERROR && console.error) { hdlr = console.error; } else if (context.level === Logger.INFO && console.info) { hdlr = console.info; } else if (context.level === Logger.DEBUG && console.debug) { hdlr = console.debug; } else if (context.level === Logger.TRACE && console.trace) { hdlr = console.trace; } options.formatter(messages, context); invokeConsoleMethod(hdlr, messages); } }; }; // Configure and example a Default implementation which writes to the `window.console` (if present). The // `options` hash can be used to configure the default logLevel and provide a custom message formatter. Logger.useDefaults = function (options) { Logger.setLevel(options && options.defaultLevel || Logger.DEBUG); Logger.setHandler(Logger.createDefaultHandler(options)); }; // Createa an alias to useDefaults to avoid reaking a react-hooks rule. Logger.setDefaults = Logger.useDefaults; // Export to popular environments boilerplate. if (module.exports) { module.exports = Logger; } else { Logger._prevLogger = global.Logger; Logger.noConflict = function () { global.Logger = Logger._prevLogger; return Logger; }; global.Logger = Logger; } })(commonjsGlobal); })(logger); var loggerExports = logger.exports; var jsLogger = /*@__PURE__*/getDefaultExportFromCjs(loggerExports); const cmcdTypes = { number: 'number', boolean: 'boolean', token: 'token', string: 'string' }; const keyTypes = { br: cmcdTypes.number, bl: cmcdTypes.number, bs: cmcdTypes.boolean, cid: cmcdTypes.string, d: cmcdTypes.number, dl: cmcdTypes.number, mtp: cmcdTypes.number, nor: cmcdTypes.string, nrr: cmcdTypes.string, ot: cmcdTypes.token, pr: cmcdTypes.number, rtp: cmcdTypes.number, sf: cmcdTypes.token, sid: cmcdTypes.string, st: cmcdTypes.token, su: cmcdTypes.boolean, tb: cmcdTypes.number, v: cmcdTypes.number }; const errorTypes = { noCMCDRequest: 'no-CMCD-request', unknownKey: 'unknown-key', invalidValue: 'invalid-value', wrongTypeValue: 'wrong-type-value', incorrectFormat: 'incorrect-format', parameterEncoding: 'parameter-encoding', missingKey: 'missing-key', invalidHeader: 'invalid-header', invalidJson: 'invalid-json', duplicateKey: 'duplicate-key', duplicatedHeader: 'duplicated-header', noHeader: 'no-header', emptyHeader: 'empty-header', unnecessaryKey: 'unnecessary-key', wrongCustomType: 'wrong-custom-type', invalidCustomKey: 'invalid-custom-key', unknownSpecificKey: 'unknown-specific-key', wrongCustomHeader: 'wrong-custom-header', queryMalformed: 'query-malformed', noAmpersandBetweenRequests: 'no-ampersand-between-requests:' }; const errorDescription = { noCMCDRequest: 'No CMCD request found', unknownKey: 'Key is not part of reserved keys', invalidValue: 'Value does not meet requirements', wrongTypeValue: 'Value type is incorrect', incorrectFormat: 'Format is incorrect', parameterEncoding: 'Parameter is not encoded', missingKey: 'Key is missing', invalidHeader: 'Header is not valid', invalidJson: 'Json format is not valid', duplicateKey: 'Key/Keys are not unique', duplicatedHeader: 'Header is duplicated', noHeader: 'There is no CMCD headers.', emptyHeader: 'There is a header with no content', unnecessaryKey: 'This key must not be sent with the current value', wrongCustomType: 'The type does not mach with CMCD types', invalidCustomKey: 'Custom key names MUST carry a hyphenated prefix', unknownSpecificKey: 'The key does not mach with CMCD keys', wrongCustomHeader: 'The header does not mach with CMCD headers', queryMalformed: 'The query is malformed', noAmpersandBetweenRequests: 'Ampersand required between two or more requests' }; const cmcdHeader = { 'CMCD-Object': ['br', 'd', 'ot', 'tb'], 'CMCD-Request': ['bl', 'dl', 'mtp', 'nor', 'nrr', 'su'], 'CMCD-Session': ['cid', 'pr', 'sf', 'sid', 'st', 'v'], 'CMCD-Status': ['bs', 'rtp'] }; const warningTypes = { noAlphabeticalOrder: 'no-alphabetical-order', valuePr: 'pr-value', valueV: 'v-value', noSidReceived: 'no-sid-received', blWithWrongOtValue: 'bl-with-wrong-ot-value', noReverseDnsCustomKey: 'customkey-without-reversedns', specificKeysNotSent: 'specific-keys-not-sent' }; const warningDescription = { noAlphabeticalOrder: 'Keys are not arranged alphabetically', valuePr: 'Should only be sent if not equal to 1.00', valueV: 'Client should omit this field if the version is 1', noSidReceived: 'No sid received from CMCD message', blWithWrongOtValue: 'bl key should only be sent with an object type of a, v or av', noReverseDnsCustomKey: 'Customkey without Reverse DNS, could be: com.example-customkeyname', specificKeysNotSent: 'A specific/custom key was set but not sent' }; const getKeyByValue = (object, value) => Object.keys(object).find(key => object[key] === value); const createError = (type, requestID, key, value, description) => { jsLogger.useDefaults({ defaultLevel: jsLogger.TRACE }); if (!Object.values(errorTypes).includes(type)) { return -1; } const error = getKeyByValue(errorTypes, type); if (description === undefined) { jsLogger.info(`${requestID}: Error in '${key}': ${errorDescription[error]}`); return { type, key, value, description: errorDescription[error] }; } jsLogger.info(`${requestID}: Error in '${key}': ${description}`); return { type, key, value, description }; }; const isStringTokenCorrect = (key, value, errors, extendedkeyTypes, requestID) => { if ((extendedkeyTypes[key] === cmcdTypes.string || extendedkeyTypes[key] === cmcdTypes.token) && (typeof value === cmcdTypes.boolean || typeof value === cmcdTypes.number)) { const description = `The value for the key ${key} must be a ${extendedkeyTypes[key]}.`; errors.push(createError(errorTypes.wrongTypeValue, requestID, key, value, description)); return false; } return true; }; const isBooleanCorrectJson = (key, value, errors, extendedkeyTypes, requestID) => { if (extendedkeyTypes[key] === cmcdTypes.boolean && typeof value !== cmcdTypes.boolean) { const description = `The value for the key ${key} must be a boolean.`; errors.push(createError(errorTypes.wrongTypeValue, requestID, key, value, description)); return false; } return true; }; const isNumberCorrectJson = (key, value, errors, extendedkeyTypes, requestID) => { if (extendedkeyTypes[key] === cmcdTypes.number && typeof value !== cmcdTypes.number) { const description = `The value for the key ${key} must be a number`; errors.push(createError(errorTypes.wrongTypeValue, requestID, key, value, description)); return false; } return true; }; const jsonIsValid = (jsonString, errors, requestID, config, extendedKeyTypes) => { let valid = true; const keyvalue = jsonString.split(','); const keys = []; // Check if there's double key keyvalue.forEach(keyVal => { var _config$specificKey; const key = keyVal.split(':')[0].replace(/{|"/g, ''); if (config !== null && config !== void 0 && config.specificKey && !((_config$specificKey = config.specificKey) !== null && _config$specificKey !== void 0 && _config$specificKey.includes(key))) { return; } if (keys.includes(key)) { const description = `The key ${key} is repeated.`; errors.push(createError(errorTypes.duplicateKey, requestID, key, undefined, description)); valid = false; } keys.push(key); }); try { const obj = JSON.parse(jsonString); Object.keys(obj).forEach(key => { var _config$specificKey2; if (config !== null && config !== void 0 && config.specificKey && !((_config$specificKey2 = config.specificKey) !== null && _config$specificKey2 !== void 0 && _config$specificKey2.includes(key))) { return; } if (!isBooleanCorrectJson(key, obj[key], errors, extendedKeyTypes, requestID) || !isNumberCorrectJson(key, obj[key], errors, extendedKeyTypes, requestID) || !isStringTokenCorrect(key, obj[key], errors, extendedKeyTypes, requestID)) { valid = false; } }); } catch (error) { errors.push(createError(errorTypes.invalidJson, requestID)); valid = false; } return valid; }; const checkQuotes = value => value.indexOf('"') === 0 && value.lastIndexOf('"') === value.length - 1; const isKeyInCorrectHeader = (header, key, errors, extendedcmcdHeader, requestID) => { if (!extendedcmcdHeader[header].includes(key)) { const description = `The key ${key} does not go under the header ${header}`; errors.push(createError(errorTypes.incorrectFormat, requestID, key, description)); return false; } return true; }; const isStringCorrect = (key, value, errors, extendedkeyTypes, requestID) => { if (extendedkeyTypes[key] === cmcdTypes.string && !checkQuotes(value) || extendedkeyTypes[key] === cmcdTypes.token && checkQuotes(value)) { const description = `Incorrect format for key: ${key}`; errors.push(createError(errorTypes.incorrectFormat, requestID, key, value, description)); return false; } return true; }; const isBooleanCorrect = (key, value, errors, extendedkeyTypes, requestID) => { if (extendedkeyTypes[key] === cmcdTypes.boolean && value === 'true') { const description = 'If the value is TRUE, the = and the value must be omitted'; errors.push(createError(errorTypes.incorrectFormat, requestID, key, value, description)); return false; } if ((typeof value === cmcdTypes.number || typeof value === cmcdTypes.string && value !== 'false') && extendedkeyTypes[key] === cmcdTypes.boolean) { const description = `The value for the key ${key} must be a boolean.`; errors.push(createError(errorTypes.wrongTypeValue, requestID, key, value, description)); return false; } return true; }; const isNumberCorrect = (key, value, errors, extendedkeyTypes, requestID) => { if (extendedkeyTypes[key] === cmcdTypes.number && Number.isNaN(parseInt(value, 10))) { const description = `The value for the key ${key} must be a number`; errors.push(createError(errorTypes.incorrectFormat, requestID, key, value, description)); return false; } return true; }; const isSeparetedCorrectly = (keyVal, errors, extendedkeyTypes, requestID) => { if (keyVal.split('=').length > 2 || keyVal.split('=').length === 1 && extendedkeyTypes[keyVal] !== cmcdTypes.boolean) { const description = 'key-value pair not separated by =.'; errors.push(createError(errorTypes.incorrectFormat, requestID, keyVal, undefined, description)); return false; } return true; }; const isKeyRepeated = (key, keys, errors, requestID) => { if (keys.includes(key)) { const description = `The key ${key} is repeated.`; errors.push(createError(errorTypes.duplicateKey, requestID, key, undefined, description)); return true; } return false; }; const isHeaderRepeated = (header, headers, errors, requestID) => { if (headers.includes(header)) { const description = `This header ${header} is repeated.`; errors.push(createError(errorTypes.duplicatedHeader, requestID, header, description)); return true; } return false; }; const isHeader = (headers, errors, requestID) => { if (headers.length === 0) { const description = 'No headers detected!'; errors.push(createError(errorTypes.noCMCDRequest, requestID, description)); return false; } return true; }; const isEmptyHeader = (keyVal, header, errors, requestID) => { if (keyVal === '') { const description = `Empty header detected! Header: ${header}`; errors.push(createError(errorTypes.emptyHeader, requestID, header, description)); return true; } return false; }; const includesCMCDRequest = (queryString, errors, requestID) => { if (!queryString.includes('CMCD=')) { errors.push(createError(errorTypes.noCMCDRequest, requestID)); return false; } return true; }; const isURLMalformed = (queryString, errors, requestID) => { let valid = false; try { // Check if the URL is encoded if (decodeURI(queryString) === queryString) { errors.push(createError(errorTypes.parameterEncoding, requestID)); valid = true; } } catch (err) { errors.push(createError(errorTypes.queryMalformed, requestID)); valid = true; } return valid; }; const areRequestsSeparated = (requests, errors, requestID) => { if (requests[0].length > 0 && requests[0][requests[0].length - 1] !== '&') { errors.push(createError(errorTypes.noAmpersandBetweenRequests, requestID)); return false; } return true; }; const multipleCMCDReq = (requests, errors, requestID) => { if (requests.length > 1) { errors.push(createError(errorTypes.incorrectFormat, requestID)); return true; } return false; }; const queryValidator = (queryString, error, requestID, warnings, config, extendedKeyTypes) => { // Check if there is a CMCD request in the queryString and Catch if the URL is malformed if (!includesCMCDRequest(queryString, error, requestID) || isURLMalformed(queryString, error, requestID)) { return false; } const query = queryString.split('?').pop(); const requests = decodeURIComponent(query).split('CMCD='); // Check if there is another query before CMCD query and is missing a '&' separating them if (!areRequestsSeparated(requests, error, requestID)) { return false; } // Check if there is more than one CMCD request requests.shift(); if (multipleCMCDReq(requests, error, requestID)) { return false; } const values = decodeURIComponent(query).split('CMCD=')[1].split('&')[0].split(','); const keys = []; let valid = true; // Check: key/value is separated by = values.forEach(val => { if (isSeparetedCorrectly(val, error, extendedKeyTypes, requestID)) { var _config$specificKey; const _val$split = val.split('='), _val$split2 = _slicedToArray(_val$split, 2), key = _val$split2[0], value = _val$split2[1]; if (config !== null && config !== void 0 && config.specificKey && !((_config$specificKey = config.specificKey) !== null && _config$specificKey !== void 0 && _config$specificKey.includes(key))) { return; } if (isKeyRepeated(key, keys, error, requestID)) { valid = false; } // Check only the keys in the configuration // Check: string require "" if (!isStringCorrect(key, value, error, extendedKeyTypes, requestID) || !isBooleanCorrect(key, value, error, extendedKeyTypes, requestID) || !isNumberCorrect(key, value, error, extendedKeyTypes, requestID)) { valid = false; } keys.push(key); } else { valid = false; } }); return valid; }; const createWarning = (type, requestID, key, value) => { jsLogger.useDefaults({ defaultLevel: jsLogger.TRACE }); if (!Object.values(warningTypes).includes(type)) { return -1; } const warning = getKeyByValue(warningTypes, type); jsLogger.info(`${requestID}: Warning in '${key}': ${warningDescription[warning]}`); return { type, key, value, description: warningDescription[warning] }; }; const keySortedAlphabetically = (cmcdKeys, warnings, requestID) => { // cmcdKeys must be an array if (JSON.stringify(cmcdKeys) !== JSON.stringify(cmcdKeys.sort())) { warnings.push(createWarning(warningTypes.noAlphabeticalOrder, requestID)); } }; const headerValidator = (headerString, errors, requestID, warnings, config, extendedcmcdHeader, extendedKeyTypes, warningFlag = true) => { const headers = headerString.split('\n'); const cmcdHeaders = []; const keys = []; let headerKeys = []; let valid = true; // We comment the following eslint error because we don't want to // return any value in the foreach // eslint-disable-next-line consistent-return headers.forEach(element => { const _element$split = element.split(': '), _element$split2 = _slicedToArray(_element$split, 2), header = _element$split2[0], keysArray = _element$split2[1]; if (!(header in extendedcmcdHeader)) { return false; } if (isHeaderRepeated(header, cmcdHeaders, errors, requestID) || isEmptyHeader(keysArray, header, errors, requestID)) { valid = false; return false; } keysArray.split(',').forEach(keyVal => { if (isSeparetedCorrectly(keyVal, errors, extendedKeyTypes, requestID)) { var _config$specificKey; const _keyVal$split = keyVal.split('='), _keyVal$split2 = _slicedToArray(_keyVal$split, 2), key = _keyVal$split2[0], value = _keyVal$split2[1]; if (config !== null && config !== void 0 && config.specificKey && !((_config$specificKey = config.specificKey) !== null && _config$specificKey !== void 0 && _config$specificKey.includes(key))) { return; } if (isKeyRepeated(key, keys, errors, requestID) || !isKeyInCorrectHeader(header, key, errors, extendedcmcdHeader, requestID) || !isStringCorrect(key, value, errors, extendedKeyTypes, requestID) || !isBooleanCorrect(key, value, errors, extendedKeyTypes, requestID) || !isNumberCorrect(key, value, errors, extendedKeyTypes, requestID)) { valid = false; } keys.push(key); headerKeys.push(key); } else { valid = false; } }); if (warningFlag === true) { keySortedAlphabetically(headerKeys, warnings, requestID); headerKeys = []; } cmcdHeaders.push(header); }); if (!isHeader(cmcdHeaders, errors, requestID)) { return false; } return valid; }; const checkMaxLength = (errors, key, value, requestID) => { if (value.length > 64) { const description = `Invalid value for key ${key}. Maximum length is 64 characters.`; errors.push(createError(errorTypes.invalidValue, requestID, key, value, description)); } }; const isEncoded = (errors, key, value, requestID) => { if (decodeURIComponent(value) === value && encodeURIComponent(value) !== value) { const description = `The key: ${key} must be URLencoded.`; errors.push(createError(errorTypes.parameterEncoding, requestID, key, value, description)); return false; } return true; }; const isRelativePath = (errors, key, value, requestID) => { if (/^(http:\/\/|https:\/\/)/.test(decodeURIComponent(value))) { const description = `The key: ${key} must be a relative path.`; errors.push(createError(errorTypes.incorrectFormat, requestID, key, value, description)); return false; } return true; }; const checkValidNrrFormat = (errors, key, value, requestID) => { if (!/^(\d*-\d*)$/.test(value) && !/^\d*-$/.test(value) && !/^-\d*$/.test(value)) { const description = 'Invalid Nrr format'; errors.push(createError(errorTypes.incorrectFormat, requestID, key, value, description)); } }; const checkValidValue = (errors, key, value, array, requestID) => { if (!array.includes(value)) { const description = `${key} value does not meet the necessary requirements. Must be one of the following values: ${array}.`; errors.push(createError(errorTypes.invalidValue, requestID, key, value, description)); } }; const checkRoundToNearest = (errors, key, value, num, unit, requestID) => { if (value % num !== 0) { const description = `${key} value is not rounded to the nearest ${num}${unit}.`; errors.push(createError(errorTypes.invalidValue, requestID, key, value, description)); } }; const checkIgnoredParameter = (errors, key, value, exep, requestID) => { if (value === exep) { const description = `The ${key} key should not be sent if the value is ${exep}`; errors.push(createError(errorTypes.unnecessaryKey, requestID, key, value, description)); } }; const isReserved = (errors, requestID, key, value, extendedKeyTypes) => { if (!(key in extendedKeyTypes)) { const description = `The key ${key} is not reserved.`; errors.push(createError(errorTypes.unknownKey, requestID, key, value, description)); } }; const isPositive = (errors, key, value, requestID) => { if (keyTypes[key] === cmcdTypes.number && value < 0) { const description = `The ${key} value must be greater than 0.`; errors.push(createError(errorTypes.invalidValue, requestID, key, value, description)); } }; const checkBlKey = (cmcdJson, warnings, key, value, requestID) => { if (!('ot' in cmcdJson) || !['a', 'v', 'av'].includes(cmcdJson.ot)) { warnings.push(createWarning(warningTypes.blWithWrongOtValue, requestID, key, value)); } }; const checkCorrectType = (errors, key, value, requestID) => { if (typeof value !== keyTypes[key] && keyTypes[key] === cmcdTypes.token && typeof value !== cmcdTypes.string) { const description = `${key} type is incorrect.`; errors.push(createError(errorTypes.wrongTypeValue, requestID, key, value, description)); } }; const checkOtValidValue = (errors, key, value, requestID) => { checkValidValue(errors, key, value, ['m', 'a', 'v', 'av', 'i', 'c', 'tt', 'k', 'o'], requestID); }; const checkSfValidValue = (errors, key, value, requestID) => { checkValidValue(errors, key, value, ['d', 'h', 's', 'o'], requestID); }; const checkStValidValue = (errors, key, value, requestID) => { checkValidValue(errors, key, value, ['v', 'l'], requestID); }; const checkPrValue = (cmcdJson, warnings, key, value, requestID) => { if (key === 'pr' && value === 1) { warnings.push(createWarning(warningTypes.valuePr, requestID, key, value)); } }; const checkVValue = (cmcdJson, warnings, key, value, requestID) => { if (key === 'v' && cmcdJson.v === 1) { warnings.push(createWarning(warningTypes.valueV, requestID, key, value)); } }; const checkSidIsPresent = (cmcdJson, warnings, requestID) => { if (!('sid' in cmcdJson)) { warnings.push(createWarning(warningTypes.noSidReceived, requestID)); } }; // keyValValidator takes as a parameter cmcdJson, which is a javascript object. // The function iterates through it validating every key value pair. // eslint-disable-next-line max-len const keyValValidator = (cmcdJson, errors, requestID, warnings, config, extendedKeyTypes, warningFlag) => { if (warningFlag === true) { checkSidIsPresent(cmcdJson, warnings, requestID); } // Check if all the specifickey have been received in the request const keysReceived = Object.keys(cmcdJson); if (config !== null && config !== void 0 && config.specificKey && warningFlag) { config.specificKey.forEach(key => { if (!keysReceived.includes(key)) { warnings.push(createWarning(warningTypes.specificKeysNotSent, requestID, key)); } }); } if (config !== null && config !== void 0 && config.customKey && warningFlag) { config.customKey.forEach(cKey => { if (!keysReceived.includes(cKey.key)) { warnings.push(createWarning(warningTypes.specificKeysNotSent, requestID, cKey.key)); } }); } Object.keys(cmcdJson).forEach(key => { var _config$specificKey; // Check if we received a configuration if (config !== null && config !== void 0 && config.specificKey && !((_config$specificKey = config.specificKey) !== null && _config$specificKey !== void 0 && _config$specificKey.includes(key))) { return; } const keyValue = cmcdJson[key]; isReserved(errors, requestID, key, keyValue, extendedKeyTypes); checkCorrectType(errors, key, keyValue, requestID); isPositive(errors, key, keyValue, requestID); switch (key) { case 'bl': checkRoundToNearest(errors, key, keyValue, 100, 'ms', requestID); if (warningFlag === true) { checkBlKey(cmcdJson, warnings, key, keyValue, requestID); } break; case 'cid': checkMaxLength(errors, key, keyValue, requestID); break; case 'dl': checkRoundToNearest(errors, key, keyValue, 100, 'ms', requestID); break; case 'mtp': checkIgnoredParameter(errors, key, keyValue, 0, requestID); checkRoundToNearest(errors, key, keyValue, 100, 'kbps', requestID); break; case 'nor': if (isEncoded(errors, key, keyValue, requestID)) { isRelativePath(errors, key, keyValue, requestID); } break; case 'nrr': checkValidNrrFormat(errors, key, keyValue, requestID); break; case 'ot': checkOtValidValue(errors, key, keyValue, requestID); break; case 'rtp': checkRoundToNearest(errors, key, keyValue, 100, 'kbps', requestID); break; case 'sf': checkSfValidValue(errors, key, keyValue, requestID); break; case 'sid': checkMaxLength(errors, key, keyValue, requestID); break; case 'st': checkStValidValue(errors, key, keyValue, requestID); break; case 'su': checkIgnoredParameter(errors, key, keyValue, false, requestID); break; case 'bs': checkIgnoredParameter(errors, key, keyValue, false, requestID); break; case 'pr': if (warningFlag) { checkPrValue(cmcdJson, warnings, key, keyValue, requestID); } break; case 'v': if (warningFlag) { checkVValue(cmcdJson, warnings, key, keyValue, requestID); } break; } }); }; const parseQueryToJson = (queryString, extendedKeyTypes) => { const values = decodeURIComponent(queryString).split('CMCD=')[1].split('&')[0].split(','); const obj = {}; values.forEach(value => { if (!value.includes('=')) { obj[value] = true; } else { // eslint-disable-next-line prefer-const let _value$split = value.split('='), _value$split2 = _slicedToArray(_value$split, 2), key = _value$split2[0], val = _value$split2[1]; val = Number.isNaN(Number(val)) ? val.replace(/"/g, '') : Number(val); if (extendedKeyTypes[key] === 'boolean' && val === 'false') val = false; obj[key] = val; } }); return obj; }; const parseHeaderToJSON = (headerString, extendedcmcdHeader, extendedKeyTypes) => { const pairs = headerString.split('\n'); const result = {}; pairs.forEach(pair => { // eslint-disable-next-line prefer-const let _pair$split = pair.split(':'), _pair$split2 = _slicedToArray(_pair$split, 2), key = _pair$split2[0], value = _pair$split2[1]; if (!extendedcmcdHeader[key]) { return; } value = value.replace(/ /g, ''); const subPairs = value.split(','); subPairs.forEach(subPair => { if (!subPair.includes('=')) { result[subPair] = true; } else { // eslint-disable-next-line prefer-const let _subPair$split = subPair.split('='), _subPair$split2 = _slicedToArray(_subPair$split, 2), subKey = _subPair$split2[0], subValue = _subPair$split2[1]; subValue = Number.isNaN(Number(subValue)) ? subValue.replace(/"/g, '') : Number(subValue); if (extendedKeyTypes[subKey] === 'boolean' && subValue === 'false') subValue = false; result[subKey] = subValue; } }); }); return result; }; const setConfig = (config, errors, requestID, warnings, warningFlag = true) => { if (!config) { return [true, keyTypes, cmcdHeader]; } const customKey = config.customKey, specificKey = config.specificKey; let errorsCheck = false; const extendedKeyTypes = _objectSpread2({}, keyTypes); const extendedcmcdHeader = _objectSpread2({}, cmcdHeader); if (customKey) { const types = Object.values(cmcdTypes); customKey.forEach(customObj => { if (!/^[a-zA-Z0-9.]+-[a-zA-Z0-9]+$/.test(customObj.key)) { errors.push(createError(errorTypes.invalidCustomKey, requestID, customObj.key)); errorsCheck = true; } if (!types.includes(customObj.type)) { errors.push(createError(errorTypes.wrongCustomType, requestID, customObj.key, customObj.type)); errorsCheck = true; } if (!/^([a-zA-Z0-9]+\.[a-zA-Z0-9]+)+$/.test(customObj.key.split('-')[0]) && warningFlag === true) { warnings.push(createWarning(warningTypes.noReverseDnsCustomKey, requestID)); } if (customObj.headerType && !(customObj.headerType in cmcdHeader)) { errors.push(createError(errorTypes.invalidHeader, requestID, customObj.key, customObj.type)); errorsCheck = true; } if (!errorsCheck) { extendedKeyTypes[customObj.key] = customObj.type; if (customObj.headerType) { extendedcmcdHeader[customObj.headerType].push(customObj.key); } } }); } if (specificKey) { const cmcdKeyTypes = Object.keys(extendedKeyTypes); specificKey.forEach(key => { if (!cmcdKeyTypes.includes(key)) { errors.push(createError(errorTypes.unknownSpecificKey, requestID, key)); } }); } if (errors.length === 0) { return [true, extendedKeyTypes, extendedcmcdHeader]; } return [false, extendedKeyTypes, extendedcmcdHeader]; }; const createOutput = (errors, warnings, rawData, parsedData) => { const response = { valid: errors.length === 0, errors, warnings, parsedData, rawData }; return response; }; const CMCDQueryValidator = (query, config, warningFlag = true) => { const errors = []; const rawData = query; const warnings = []; const requestID = v4(); jsLogger.useDefaults({ defaultLevel: jsLogger.TRACE }); jsLogger.info(`${requestID}: Started CMCD Query Validation.`); const _setConfig = setConfig(config, errors, requestID, warnings, warningFlag), _setConfig2 = _slicedToArray(_setConfig, 2), validConfig = _setConfig2[0], extendedKeyTypes = _setConfig2[1]; // check config jsLogger.info(`${requestID}: Check Configuration.`); if (!validConfig) { jsLogger.info(`${requestID}: Configuration not valid.`); return createOutput(errors, warnings, rawData); } // Check query jsLogger.info(`${requestID}: Validating query format.`); const valid = queryValidator(query, errors, requestID, warnings, config, extendedKeyTypes); if (!valid) { jsLogger.info(`${requestID}: Query not valid.`); return createOutput(errors, warnings, rawData); } jsLogger.info(`${requestID}: Query is valid.`); // Parsed to json jsLogger.info(`${requestID}: Parsing query.`); const parsedData = parseQueryToJson(query, extendedKeyTypes); if (warningFlag === true) { keySortedAlphabetically(Object.keys(parsedData), warnings); } // Check key value jsLogger.info(`${requestID}: Validating query keys.`); keyValValidator(parsedData, errors, requestID, warnings, config, extendedKeyTypes, warningFlag); return createOutput(errors, warnings, rawData, parsedData); }; const CMCDJsonValidator = (jsonString, config, warningFlag = true) => { const errors = []; const rawData = jsonString; const warnings = []; const requestID = v4(); jsLogger.useDefaults({ defaultLevel: jsLogger.TRACE }); jsLogger.info(`${requestID}: Started CMCD Json Validation.`); const _setConfig = setConfig(config, errors, requestID, warnings, warningFlag), _setConfig2 = _slicedToArray(_setConfig, 2), validConfig = _setConfig2[0], extendedKeyTypes = _setConfig2[1]; // check config if (!validConfig) { jsLogger.info(`${requestID}: Configuration is not valid.`); return createOutput(errors, warnings, rawData); } jsLogger.info(`${requestID}: Configuration es valid.`); // Check json jsLogger.info(`${requestID}: Validating Json format.`); const valid = jsonIsValid(jsonString, errors, requestID, config, extendedKeyTypes); if (!valid) { jsLogger.info(`${requestID}: Json not valid.`); return createOutput(errors, warnings, rawData); } jsLogger.info(`${requestID}: Json is valid.`); const jsonObj = JSON.parse(jsonString); if (warningFlag === true) { keySortedAlphabetically(Object.keys(jsonObj), warnings, requestID); } // Check key value jsLogger.info(`${requestID}: Validating Json keys.`); keyValValidator(jsonObj, errors, requestID, warnings, config, extendedKeyTypes, warningFlag); return createOutput(errors, warnings, rawData, jsonObj); }; const CMCDHeaderValidator = (header, config, warningFlag = true) => { const errors = []; const rawData = header; const warnings = []; const requestID = v4(); jsLogger.useDefaults({ defaultLevel: jsLogger.TRACE }); jsLogger.info(`${requestID}: Started CMCD Header Validation.`); const _setConfig = setConfig(config, errors, requestID, warnings, warningFlag), _setConfig2 = _slicedToArray(_setConfig, 3), validConfig = _setConfig2[0], extendedKeyTypes = _setConfig2[1], extendedcmcdHeader = _setConfig2[2]; // check config if (!validConfig) { return createOutput(errors, warnings, rawData); } // Check header jsLogger.info(`${requestID}: Validating header format.`); const valid = headerValidator(header, errors, requestID, warnings, config, extendedcmcdHeader, extendedKeyTypes, warningFlag); if (!valid) { jsLogger.info(`${requestID}: Header not valid.`); return createOutput(errors, warnings, rawData); } jsLogger.info(`${requestID}: Header is valid.`); // Parsed to json jsLogger.info(`${requestID}: Parsing header.`); const parsedData = parseHeaderToJSON(header, extendedcmcdHeader, extendedKeyTypes); // Check key value keyValValidator(parsedData, errors, requestID, warnings, config, extendedKeyTypes, warningFlag); return createOutput(errors, warnings, rawData, parsedData); }; export { CMCDHeaderValidator, CMCDJsonValidator, CMCDQueryValidator };