You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
437 lines
9.8 KiB
437 lines
9.8 KiB
/**
|
|
* 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')
|
|
}
|
|
})()
|