diff options
Diffstat (limited to 'src/scripts/utils.js')
-rw-r--r-- | src/scripts/utils.js | 324 |
1 files changed, 324 insertions, 0 deletions
diff --git a/src/scripts/utils.js b/src/scripts/utils.js new file mode 100644 index 0000000..e456d23 --- /dev/null +++ b/src/scripts/utils.js @@ -0,0 +1,324 @@ +'use strict'; + +// ----------------- Constants ----------------------------- +const FOXYPROXY_BASIC = false; + +// Bit-wise flags so we can add/remove these independently. We may add more later so PROTOCOL_ALL is future-proof. +const PROTOCOL_ALL = 1; // in case other protocols besides http and https are supported later +const PROTOCOL_HTTP = 2; +const PROTOCOL_HTTPS = 4; + + +// import | pac +const PROXY_TYPE_HTTP = 1; +const PROXY_TYPE_HTTPS = 2; +const PROXY_TYPE_SOCKS5 = 3; +const PROXY_TYPE_SOCKS4 = 4; +const PROXY_TYPE_NONE = 5; // DIRECT +const PROXY_TYPE_PAC = 6; +const PROXY_TYPE_WPAD = 7; +const PROXY_TYPE_SYSTEM = 8; +const PROXY_TYPE_PASS = 9; + + +const PATTERN_TYPE_WILDCARD = 1; +const PATTERN_TYPE_REGEXP = 2; + +// Storage keys that are not proxy settings +const NON_PROXY_KEYS = ['mode', 'logging', 'sync', 'browserVersion', 'foxyProxyVersion', 'foxyProxyEdition', 'nextIndex']; + +// bg | import | proxy | utils +const PATTERN_ALL_WHITE = { + title: 'all URLs', + active: true, + pattern: '*', + type: 1, // PATTERN_TYPE_WILDCARD, + protocols: 1 // PROTOCOL_ALL +}; + +const DEFAULT_COLOR = '#66cc66'; // default proxy color + +// patterns | proxy +// the local-internal blacklist, always used as a set +const blacklistSet = [ + { + title: "local hostnames (usually no dots in the name). Pattern exists because 'Do not use this proxy for localhost and intranet/private IP addresses' is checked.", + pattern: "^(?:[^:@/]+(?::[^@/]+)?@)?(?:localhost|127\\.\\d+\\.\\d+\\.\\d+)(?::\\d+)?(?:/.*)?$", + }, + { + title: "local subnets (IANA reserved address space). Pattern exists because 'Do not use this proxy for localhost and intranet/private IP addresses' is checked.", + pattern: "^(?:[^:@/]+(?::[^@/]+)?@)?(?:192\\.168\\.\\d+\\.\\d+|10\\.\\d+\\.\\d+\\.\\d+|172\\.(?:1[6789]|2[0-9]|3[01])\\.\\d+\\.\\d+)(?::\\d+)?(?:/.*)?$", + }, + { + title: "localhost - matches the local host optionally prefixed by a user:password authentication string and optionally suffixed by a port number. The entire local subnet (127.0.0.0/8) matches. Pattern exists because 'Do not use this proxy for localhost and intranet/private IP addresses' is checked.", + pattern: "^(?:[^:@/]+(?::[^@/]+)?@)?[\\w-]+(?::\\d+)?(?:/.*)?$" + } +].map (item => { + item.active = true; + item.type = 2; // PATTERN_TYPE_REGEXP, + item.protocols = 1; // PROTOCOL_ALL + return item; +}); + +// ----------------- Utils --------------------------------- +class Utils { + + static notify(message, title = 'FoxyProxy') { + // the id is not used anywhere and can be omitted, it is only useful if you want to manually close the notification early + chrome.notifications.create('foxyproxy', { + type: 'basic', + iconUrl: '/images/icon.svg', + title, + message + }); + } + + // options | popup + static isUnsupportedType(type) { + //return type === PROXY_TYPE_PAC || type === PROXY_TYPE_WPAD || type === PROXY_TYPE_SYSTEM || type === PROXY_TYPE_PASS; + return [PROXY_TYPE_PAC, PROXY_TYPE_WPAD, PROXY_TYPE_SYSTEM, PROXY_TYPE_PASS].includes(type); + } + + // bg | pattern-tester | validate-pattern + static wildcardToRegExp(pat) { + + let start = 0, end = pat.length, matchOptionalSubdomains = false; + + if (pat[0] === '.') { pat = '*' + pat; } + + if (pat.startsWith('**')) { + // Strip asterisks from front and back + while (pat[start] === '*' && start < end) start++; + while (pat[end - 1] === '*' && start < end) end--; + // If there's only an asterisk left, match everything + if (end - start == 1 && pat[start] == '*') return ''; + } + else if (pat.startsWith('*.')) { matchOptionalSubdomains = true; } + + let regExpStr = pat.substring(start, end+1) + // $& replaces with the string found, but with that string escaped + .replace(/[$.+()^{}\]\[|]/g, '\\$&') + .replace(/\*/g, '.*') + .replace(/\?/g, '.'); + + if (matchOptionalSubdomains) { + // Non-capturing group that matches: + // any group of non-whitespace characters following by an optional . repeated zero or more times + regExpStr = '(?:\\S+\\.)*' + regExpStr.substring(4); + } + + // Leading or ending double-asterisks mean exact starting and ending positions + if (start === 0) { regExpStr = '^' + regExpStr; } + if (end === pat.length) { regExpStr += '$'; } + return regExpStr; + } + + // Prep the patternObject for matching: convert wildcards to regexp, + // store the originalPattern which the user entered so we can display if needed, etc. + // Return null if patternObject is inactive or there is an error. + static processPatternObject(patternObject) { + if (patternObject.active) { + // Store the original pattern so if this pattern matches something, + // we can display whatever the user entered ("original") in the log. + patternObject.originalPattern = patternObject.pattern; + if (patternObject.type === PATTERN_TYPE_WILDCARD) { + patternObject.pattern = Utils.wildcardToRegExp(patternObject.pattern); + } + try { + // Convert to real RegExp, not just a string. Validate. If invalid, notify user. + patternObject.pattern = new RegExp(patternObject.pattern, 'i'); + return patternObject; + } + catch(e) { + console.error(`Error creating regexp for pattern: ${patternObject.pattern}`, e); + Utils.notify(`Error creating regular expression for pattern ${regExpStr}`); + } + } + return null; + } + + // import | pattern + static importFile(file, mimeTypeArr, maxSizeBytes, jsonOrXml, callback) { + + if (!file) { + alert('There was an error'); + return; + } + + // Check MIME type // Ch65 no filetype for JSON + if (!mimeTypeArr.includes(file.type)) { + alert('Unsupported file format'); + return; + } + + if (file.size > maxSizeBytes) { + alert('Filesize is too large'); + return; + } + + const reader = new FileReader(); + reader.onloadend = () => { + if (reader.error) { + alert('Error reading file.'); + return; + } + + let settings; + try { + if (jsonOrXml === 'json') { settings = JSON.parse(reader.result); } + else if (jsonOrXml === 'xml') { + settings = new DOMParser().parseFromString(reader.result, 'text/xml'); + if (settings.documentElement.nodeName === 'parsererror') { throw new Error(); } + } + } + catch(e) { + console.log(e); + alert("Error parsing file. Please remove sensitive data from the file, and then email it to support@getfoxyproxy.org so we can fix bugs in our parser."); + return; + } + if (settings && confirm('This will overwite existing proxy settings. Are you sure?')) { callback(settings); } + else { callback(); } + + }; + reader.onerror = () => { alert('Error reading file'); }; + reader.readAsText(file); + } + + // import | options + static exportFile() { + + chrome.storage.local.get(null, result => { + browser.runtime.getBrowserInfo().then((bi) => { + !result.sync ? Utils.saveAs(result, bi.version) : chrome.storage.sync.get(null, result => { + Utils.saveAs(result, bi.version, true); + }); + }); + }); + } + // exportFile helper + static saveAs(data, browserVersion, sync) { + + const settings = data; //Utils.prepareForSettings(data); + // Browser version and extension version. These are used for debugging. + settings.browserVersion = browserVersion; + settings.foxyProxyVersion = chrome.runtime.getManifest().version; + settings.foxyProxyEdition = FOXYPROXY_BASIC ? 'basic' : 'standard'; + settings.sync = sync; + const blob = new Blob([JSON.stringify(settings, null, 2)], {type : 'text/plain;charset=utf-8'}); + const filename = chrome.i18n.getMessage('extensionName') + '_' + new Date().toISOString().substring(0, 10) + '.json'; + chrome.downloads.download({ + url: URL.createObjectURL(blob), + filename, + saveAs: true, + conflictAction: 'uniquify' + }); + } + + static updateIcon(iconPath, color, title, titleIsKey, badgeText, badgeTextIsKey) { + chrome.browserAction.setIcon({path: iconPath}); + if (color) { + chrome.browserAction.setBadgeBackgroundColor({color: color}); + } + else { + // TODO: confirm this is OK to do + chrome.browserAction.setBadgeBackgroundColor({color: null}); + } + if (title) { + chrome.browserAction.setTitle({title: 'FoxyProxy: ' + (titleIsKey ? chrome.i18n.getMessage(title) : title)}); + } + else { + chrome.browserAction.setTitle({title: ''}); + } + if (badgeText) { + chrome.browserAction.setBadgeText({text: badgeTextIsKey ? chrome.i18n.getMessage(badgeText) : badgeText}); + } + else { + chrome.browserAction.setBadgeText({text: ''}); + } + } + + static getProxyTitle(proxySetting) { + if (proxySetting.title) { + return proxySetting.title; + } + else if (proxySetting.type === PROXY_TYPE_NONE) { + return 'Direct (no proxy)'; + } + else { + return `${proxySetting.address}:${proxySetting.port}`; + } + } + +/* + // utils only used for export, will be removed as DB format export is adapted + static prepareForSettings(settings = {}) { + + //if (settings && !settings.mode) { }// 5.0 settings + + let lastResortFound = false; + const prefKeys = Object.keys(settings); + + const def = { + id: LASTRESORT, + active: true, + title: 'Default', + notes: 'These are the settings that are used when no patterns match a URL.', + color: '#0055E5', + type: PROXY_TYPE_NONE, + whitePatterns: [PATTERN_ALL_WHITE], + blackPatterns: [] + }; + + // base format + const ret = { + mode: 'disabled', + proxySettings: [], + logging: { + size: 500, + active: true + } + }; + + if (!prefKeys.length) { // settings is {} + ret.proxySettings = [def]; + return ret; + } + + prefKeys.forEach(key => { + + switch (key) { + + case 'mode': + case 'logging': + ret[key] = settings[key]; + break; + + case 'sync': break; // do nothing + + default: + const temp = settings[key]; + temp.id = key; // Copy the id into the object + temp.id === LASTRESORT && (lastResortFound = true); + ret.proxySettings.push(temp); + } + }); + + ret.proxySettings.sort((a, b) => a.index - b.index); + ret.proxySettings.forEach(item => delete item.index); // Re-calculated when/if this object is written to disk again (user may move proxySetting up/down) + + !lastResortFound && ret.proxySettings.push(def); // add default lastresort + + return ret; + } +*/ + + static getUniqueId() { + // We don't need cryptographically secure UUIDs, just something unique + return Math.random().toString(36).substring(7) + new Date().getTime(); + } + + static stripBadChars(str) { + return str ? str.replace(/[&<>"']+/g, '') : null; + } + +} |