/** * WebExtension driver */ /* eslint-env browser */ /* global browser, chrome, Wappalyzer */ /** global: browser */ /** global: chrome */ /** global: fetch */ /** global: Wappalyzer */ const wappalyzer = new Wappalyzer() const tabCache = {} const robotsTxtQueue = {} let categoryOrder = [] browser.tabs.onRemoved.addListener((tabId) => { tabCache[tabId] = null }) function userAgent() { const url = chrome.extension.getURL('/') if (url.match(/^moz-/)) { return 'firefox' } if (url.match(/^ms-browser-/)) { return 'edge' } return 'chrome' } /** * Get a value from localStorage */ function getOption(name, defaultValue = null) { return new Promise(async (resolve, reject) => { let value = defaultValue try { const option = await browser.storage.local.get(name) if (option[name] !== undefined) { value = option[name] } } catch (error) { wappalyzer.log(error.message, 'driver', 'error') return reject(error.message) } return resolve(value) }) } /** * Set a value in localStorage */ function setOption(name, value) { return new Promise(async (resolve, reject) => { try { await browser.storage.local.set({ [name]: value }) } catch (error) { wappalyzer.log(error.message, 'driver', 'error') return reject(error.message) } return resolve() }) } /** * Open a tab */ function openTab(args) { browser.tabs.create({ url: args.url, active: args.background === undefined || !args.background }) } /** * Make a POST request */ async function post(url, body) { try { const response = await fetch(url, { method: 'POST', body: JSON.stringify(body) }) wappalyzer.log(`POST ${url}: ${response.status}`, 'driver') } catch (error) { wappalyzer.log(`POST ${url}: ${error}`, 'driver', 'error') } } // Capture response headers browser.webRequest.onCompleted.addListener( async (request) => { const headers = {} if (request.responseHeaders) { const url = wappalyzer.parseUrl(request.url) let tab try { ;[tab] = await browser.tabs.query({ url: [url.href] }) } catch (error) { wappalyzer.log(error, 'driver', 'error') } if (tab) { request.responseHeaders.forEach((header) => { const name = header.name.toLowerCase() headers[name] = headers[name] || [] headers[name].push( (header.value || header.binaryValue || '').toString() ) }) if ( headers['content-type'] && /\/x?html/.test(headers['content-type'][0]) ) { wappalyzer.analyze(url, { headers }, { tab }) } } } }, { urls: ['http://*/*', 'https://*/*'], types: ['main_frame'] }, ['responseHeaders'] ) browser.runtime.onConnect.addListener((port) => { port.onMessage.addListener(async (message) => { if (message.id === undefined) { return } if (message.id !== 'log') { wappalyzer.log(`Message from ${port.name}: ${message.id}`, 'driver') } const pinnedCategory = await getOption('pinnedCategory') const url = wappalyzer.parseUrl(port.sender.tab ? port.sender.tab.url : '') const cookies = await browser.cookies.getAll({ domain: `.${url.hostname}` }) let response switch (message.id) { case 'log': wappalyzer.log(message.subject, message.source) break case 'init': wappalyzer.analyze(url, { cookies }, { tab: port.sender.tab }) break case 'analyze': if (message.subject.html) { browser.i18n .detectLanguage(message.subject.html) .then(({ languages }) => { const language = languages .filter(({ percentage }) => percentage >= 75) .map(({ language: lang }) => lang)[0] message.subject.language = language wappalyzer.analyze(url, message.subject, { tab: port.sender.tab }) }) } else { wappalyzer.analyze(url, message.subject, { tab: port.sender.tab }) } await setOption('hostnameCache', wappalyzer.hostnameCache) break case 'ad_log': wappalyzer.cacheDetectedAds(message.subject) break case 'get_apps': response = { tabCache: tabCache[message.tab.id], apps: wappalyzer.apps, categories: wappalyzer.categories, pinnedCategory, termsAccepted: userAgent() === 'chrome' || (await getOption('termsAccepted', false)) } break case 'set_option': await setOption(message.key, message.value) break case 'get_js_patterns': response = { patterns: wappalyzer.jsPatterns } break case 'update_theme_mode': // Sync theme mode to popup. response = { themeMode: await getOption('themeMode', false) } break default: // Do nothing } if (response) { port.postMessage({ id: message.id, response }) } }) }) wappalyzer.driver.document = document /** * Log messages to console */ wappalyzer.driver.log = (message, source, type) => { const log = ['warn', 'error'].includes(type) ? type : 'log' console[log](`[wappalyzer ${type}]`, `[${source}]`, message) // eslint-disable-line no-console } /** * Display apps */ wappalyzer.driver.displayApps = async (detected, meta, context) => { const { tab } = context if (tab === undefined) { return } tabCache[tab.id] = tabCache[tab.id] || { detected: [] } tabCache[tab.id].detected = detected const pinnedCategory = await getOption('pinnedCategory') const dynamicIcon = await getOption('dynamicIcon', true) let found = false // Find the main application to display ;[pinnedCategory].concat(categoryOrder).forEach((match) => { Object.keys(detected).forEach((appName) => { const app = detected[appName] app.props.cats.forEach((category) => { if (category === match && !found) { let icon = app.props.icon && dynamicIcon ? app.props.icon : 'default.svg' if (/\.svg$/i.test(icon)) { icon = `converted/${icon.replace(/\.svg$/, '.png')}` } try { browser.pageAction.setIcon({ tabId: tab.id, path: `../images/icons/${icon}` }) } catch (e) { // Firefox for Android does not support setIcon see https://bugzilla.mozilla.org/show_bug.cgi?id=1331746 } found = true } }) }) }) browser.pageAction.show(tab.id) } /** * Fetch and cache robots.txt for host */ wappalyzer.driver.getRobotsTxt = async (host, secure = false) => { if (robotsTxtQueue[host]) { return robotsTxtQueue[host] } const tracking = await getOption('tracking', true) const robotsTxtCache = await getOption('robotsTxtCache', {}) robotsTxtQueue[host] = new Promise(async (resolve) => { if (!tracking) { return resolve([]) } if (host in robotsTxtCache) { return resolve(robotsTxtCache[host]) } const timeout = setTimeout(() => resolve([]), 3000) let response try { response = await fetch(`http${secure ? 's' : ''}://${host}/robots.txt`, { redirect: 'follow', mode: 'no-cors' }) } catch (error) { wappalyzer.log(error, 'driver', 'error') return resolve([]) } clearTimeout(timeout) const robotsTxt = response.ok ? await response.text() : '' robotsTxtCache[host] = Wappalyzer.parseRobotsTxt(robotsTxt) await setOption('robotsTxtCache', robotsTxtCache) delete robotsTxtQueue[host] return resolve(robotsTxtCache[host]) }) return robotsTxtQueue[host] } /** * Anonymously track detected applications for research purposes */ wappalyzer.driver.ping = async (hostnameCache = {}, adCache = []) => { const tracking = await getOption('tracking', true) const termsAccepted = userAgent() === 'chrome' || (await getOption('termsAccepted', false)) if (tracking && termsAccepted) { if (Object.keys(hostnameCache).length) { post('https://api.wappalyzer.com/ping/v1/', hostnameCache) } if (adCache.length) { post('https://ad.wappalyzer.com/log/wp/', adCache) } await setOption('robotsTxtCache', {}) } } // Init ;(async () => { // Technologies try { const response = await fetch('../apps.json') const json = await response.json() wappalyzer.apps = json.apps wappalyzer.categories = json.categories } catch (error) { wappalyzer.log(`GET apps.json: ${error.message}`, 'driver', 'error') } wappalyzer.parseJsPatterns() categoryOrder = Object.keys(wappalyzer.categories) .map((categoryId) => parseInt(categoryId, 10)) .sort( (a, b) => wappalyzer.categories[a].priority - wappalyzer.categories[b].priority ) // Version check const { version } = browser.runtime.getManifest() const previousVersion = await getOption('version') const upgradeMessage = await getOption('upgradeMessage', true) if (previousVersion === null) { openTab({ url: `${wappalyzer.config.websiteURL}installed` }) } else if (version !== previousVersion && upgradeMessage) { openTab({ url: `${wappalyzer.config.websiteURL}upgraded?v${version}`, background: true }) } await setOption('version', version) // Hostname cache wappalyzer.hostnameCache = await getOption('hostnameCache', {}) // Run content script on all tabs try { const tabs = await browser.tabs.query({ url: ['http://*/*', 'https://*/*'] }) tabs.forEach(async (tab) => { try { await browser.tabs.executeScript(tab.id, { file: '../js/content.js' }) } catch (error) { // } }) } catch (error) { wappalyzer.log(error, 'driver', 'error') } })()