var _ = require('../util').lodash, Property = require('../collection/property').Property, Url = require('../collection/url').Url, STRING = 'string', UNDEFINED = 'undefined', MATCH_ALL = '*', PREFIX_DELIMITER = '^', PROTOCOL_DELIMITER = '+', POSTFIX_DELIMITER = '$', MATCH_ALL_URLS = '', ALLOWED_PROTOCOLS = ['http', 'https', 'file', 'ftp'], ALLOWED_PROTOCOLS_REGEX = ALLOWED_PROTOCOLS.join('|'), // @todo initialize this and ALLOWED_PROTOCOLS via UrlMatchPattern options DEFAULT_PROTOCOL_PORT = { ftp: '21', http: '80', https: '443' }, regexes = { escapeMatcher: /[.+^${}()|[\]\\]/g, escapeMatchReplacement: '\\$&', questionmarkMatcher: /\?/g, questionmarkReplacment: '.', starMatcher: '*', starReplacement: '.*', // @todo match valid HOST name // @note PATH is required(can be empty '/' or '/*') i.e, {PROTOCOL}://{HOST}/ patternSplit: '^((' + ALLOWED_PROTOCOLS_REGEX + '|\\*)(\\+(' + ALLOWED_PROTOCOLS_REGEX + '))*)://(\\*|\\*\\.[^*/:]+|[^*/:]+)(:\\*|:\\d+)?(/.*)$' }, UrlMatchPattern; /** * @typedef UrlMatchPattern.definition * @property {String} pattern The url match pattern string */ _.inherit(( /** * UrlMatchPattern allows to create rules to define Urls to match for. * It is based on Google's Match Pattern - https://developer.chrome.com/extensions/match_patterns * * @constructor * @extends {Property} * @param {UrlMatchPattern.definition} options - * * @example An example UrlMatchPattern * var matchPattern = new UrlMatchPattern('https://*.google.com/*'); */ UrlMatchPattern = function UrlMatchPattern (options) { // called as new UrlMatchPattern('https+http://*.example.com/*') if (_.isString(options)) { options = { pattern: options }; } // this constructor is intended to inherit and as such the super constructor is required to be executed UrlMatchPattern.super_.apply(this, arguments); // Assign defaults before proceeding _.assign(this, /** @lends UrlMatchPattern */ { /** * The url match pattern string * * @type {String} */ pattern: MATCH_ALL_URLS }); this.update(options); }), Property); _.assign(UrlMatchPattern.prototype, /** @lends UrlMatchPattern.prototype */ { /** * Assigns the given properties to the UrlMatchPattern. * * @param {{ pattern: (string) }} options - */ update (options) { _.has(options, 'pattern') && (_.isString(options.pattern) && !_.isEmpty(options.pattern)) && (this.pattern = options.pattern); // create a match pattern and store it on cache this._matchPatternObject = this.createMatchPattern(); }, /** * Used to generate the match regex object from the match string we have. * * @private * @returns {*} Match regex object */ createMatchPattern () { var matchPattern = this.pattern, // Check the match pattern of sanity and split it into protocol, host and path match = matchPattern.match(regexes.patternSplit); if (!match) { // This ensures it is a invalid match pattern return; } return { protocols: _.uniq(match[1].split(PROTOCOL_DELIMITER)), host: match[5], port: match[6] && match[6].substr(1), // remove leading `:` path: this.globPatternToRegexp(match[7]) }; }, /** * Converts a given glob pattern into a regular expression. * * @private * @param {String} pattern Glob pattern string * @returns {RegExp=} */ globPatternToRegexp (pattern) { // Escape everything except ? and *. pattern = pattern.replace(regexes.escapeMatcher, regexes.escapeMatchReplacement); pattern = pattern.replace(regexes.questionmarkMatcher, regexes.questionmarkReplacment); pattern = pattern.replace(regexes.starMatcher, regexes.starReplacement); // eslint-disable-next-line security/detect-non-literal-regexp return new RegExp(PREFIX_DELIMITER + pattern + POSTFIX_DELIMITER); }, /** * Tests if the given protocol string, is allowed by the pattern. * * @param {String=} protocol The protocol to be checked if the pattern allows. * @returns {Boolean=} */ testProtocol (protocol) { var matchRegexObject = this._matchPatternObject; return _.includes(ALLOWED_PROTOCOLS, protocol) && (_.includes(matchRegexObject.protocols, MATCH_ALL) || _.includes(matchRegexObject.protocols, protocol)); }, /** * Returns the protocols supported * * @returns {Array.} */ getProtocols () { return _.get(this, '_matchPatternObject.protocols') || []; }, /** * Tests if the given host string, is allowed by the pattern. * * @param {String=} host The host to be checked if the pattern allows. * @returns {Boolean=} */ testHost (host) { /* * For Host match, we are considering the port with the host, hence we are using getRemote() instead of getHost() * We need to address three cases for the host urlStr * 1. * It matches all the host + protocol, hence we are not having any parsing logic for it. * 2. *.foo.bar.com Here the prefix could be anything but it should end with foo.bar.com * 3. foo.bar.com This is the absolute matching needs to done. */ var matchRegexObject = this._matchPatternObject; return ( this.matchAnyHost(matchRegexObject) || this.matchAbsoluteHostPattern(matchRegexObject, host) || this.matchSuffixHostPattern(matchRegexObject, host) ); }, /** * Checks whether the matchRegexObject has the MATCH_ALL host. * * @private * @param {Object=} matchRegexObject The regex object generated by the createMatchPattern function. * @returns {Boolean} */ matchAnyHost (matchRegexObject) { return matchRegexObject.host === MATCH_ALL; }, /** * Check for the (*.foo.bar.com) kind of matches with the remote provided. * * @private * @param {Object=} matchRegexObject The regex object generated by the createMatchPattern function. * @param {String=} remote The remote url (host+port) of the url for which the hostpattern needs to checked * @returns {Boolean} */ matchSuffixHostPattern (matchRegexObject, remote) { var hostSuffix = matchRegexObject.host.substr(2); return matchRegexObject.host[0] === MATCH_ALL && (remote === hostSuffix || remote.endsWith('.' + hostSuffix)); }, /** * Check for the absolute host match. * * @private * @param {Object=} matchRegexObject The regex object generated by the createMatchPattern function. * @param {String=} remote The remote url, host+port of the url for which the hostpattern needs to checked * @returns {Boolean} */ matchAbsoluteHostPattern (matchRegexObject, remote) { return matchRegexObject.host === remote; }, /** * Tests if the current pattern allows the given port. * * @param {String} port The port to be checked if the pattern allows. * @param {String} protocol Protocol to refer default port. * @returns {Boolean} */ testPort (port, protocol) { var portRegex = this._matchPatternObject.port, // default port for given protocol defaultPort = protocol && DEFAULT_PROTOCOL_PORT[protocol]; // return true if both given port and match pattern are absent if (typeof port === UNDEFINED && typeof portRegex === UNDEFINED) { return true; } // convert integer port to string (port && typeof port !== STRING) && (port = String(port)); // assign default port or portRegex !port && (port = defaultPort); !portRegex && (portRegex = defaultPort); // matches * or specific port return ( portRegex === MATCH_ALL || portRegex === port ); }, /** * Tests if the current pattern allows the given path. * * @param {String=} path The path to be checked if the pattern allows. * @returns {Boolean=} */ testPath (path) { var matchRegexObject = this._matchPatternObject; return !_.isEmpty(path.match(matchRegexObject.path)); }, /** * Tests the url string with the match pattern provided. * Follows the https://developer.chrome.com/extensions/match_patterns pattern for pattern validation and matching * * @param {String=} urlStr The url string for which the proxy match needs to be done. * @returns {Boolean=} */ test (urlStr) { /* * This function executes the code in the following sequence for early return avoiding the costly regex matches. * To avoid most of the memory consuming code. * 1. It check whether the match string is in that case, it return immediately without any further * processing. * 2. Checks whether the matchPattern follows the rules, https://developer.chrome.com/extensions/match_patterns, * If not then, don't process it. * 3. Check for the protocol, as it is a normal array check. * 4. Checks the host, as it doesn't involve regex match and has only string comparisons. * 5. Finally, checks for the path, which actually involves the Regex matching, the slow process. */ // If the matchPattern is then there is no need for any validations. if (this.pattern === MATCH_ALL_URLS) { return true; } // Empty _matchPatternObject represents the match is INVALID match if (_.isEmpty(this._matchPatternObject)) { return false; } const url = new Url(urlStr); return (this.testProtocol(url.protocol) && this.testHost(url.getHost()) && this.testPort(url.port, url.protocol) && this.testPath(url.getPath())); }, /** * Returns a string representation of the match pattern * * @returns {String} pattern */ toString () { return String(this.pattern); }, /** * Returns the JSON representation. * * @returns {{ pattern: (String) }} */ toJSON () { var pattern; pattern = this.toString(); return { pattern }; } }); _.assign(UrlMatchPattern, /** @lends UrlMatchPattern */ { /** * Defines the name of this property for internal use * * @private * @readOnly * @type {String} */ _postman_propertyName: 'UrlMatchPattern', /** * Multiple protocols in the match pattern should be separated by this string * * @readOnly * @type {String} */ PROTOCOL_DELIMITER: PROTOCOL_DELIMITER, /** * String representation for matching all urls - * * @readOnly * @type {String} */ MATCH_ALL_URLS: MATCH_ALL_URLS }); module.exports = { UrlMatchPattern };