main
Elbert Alias 6 years ago
parent 4a96491c0f
commit eca77528d6

@ -0,0 +1,2 @@
**/lib/*
node_modules

@ -0,0 +1,6 @@
module.exports = {
"extends": "airbnb-base",
"rules": {
"no-param-reassign": 0
}
};

1545
npm-shrinkwrap.json generated

File diff suppressed because it is too large Load Diff

@ -6,9 +6,14 @@
},
"devDependencies": {
"chai": "^4.1.2",
"eslint": "^4.19.1",
"eslint-config-airbnb-base": "^13.0.0",
"eslint-plugin-import": "^2.13.0",
"mocha": "^5.2.0"
},
"scripts": {
"test": "mocha -R spec ./test"
"test": "mocha -R spec ./test",
"lint": "eslint src",
"lint:fix": "eslint src --fix"
}
}

@ -5,29 +5,29 @@
/** global: wappalyzer */
/** global: XMLHttpRequest */
(function() {
(function () {
wappalyzer.driver.document = document;
const container = document.getElementById('wappalyzer-container');
const url = wappalyzer.parseUrl(window.top.location.href);
const hasOwn = Object.prototype.hasOwnProperty;
const container = document.getElementById('wappalyzer-container');
const url = wappalyzer.parseUrl(window.top.location.href);
const hasOwn = Object.prototype.hasOwnProperty;
/**
* Log messages to console
*/
wappalyzer.driver.log = (message, source, type) => {
console.log('[wappalyzer ' + type + ']', '[' + source + ']', message);
console.log(`[wappalyzer ${type}]`, `[${source}]`, message);
};
function getPageContent() {
wappalyzer.log('func: getPageContent', 'driver');
var scripts = Array.prototype.slice
const scripts = Array.prototype.slice
.apply(document.scripts)
.filter(s => s.src)
.map(s => s.src);
var html = new window.XMLSerializer().serializeToString(document).split('\n');
let html = new window.XMLSerializer().serializeToString(document).split('\n');
html = html
.slice(0, 1000).concat(html.slice(html.length - 1000))
@ -35,47 +35,48 @@
.join('\n');
wappalyzer.analyze(url, {
html: html,
scripts: scripts
html,
scripts,
});
}
function getResponseHeaders() {
function getResponseHeaders() {
wappalyzer.log('func: getResponseHeaders', 'driver');
var xhr = new XMLHttpRequest();
const xhr = new XMLHttpRequest();
xhr.open('GET', url, true);
xhr.onreadystatechange = () => {
if ( xhr.readyState === 4 && xhr.status ) {
var headers = xhr.getAllResponseHeaders().split("\n");
if (xhr.readyState === 4 && xhr.status) {
const headers = xhr.getAllResponseHeaders().split('\n');
if ( headers.length > 0 && headers[0] != '' ) {
wappalyzer.log('responseHeaders: ' + xhr.getAllResponseHeaders(), 'driver');
if (headers.length > 0 && headers[0] != '') {
wappalyzer.log(`responseHeaders: ${xhr.getAllResponseHeaders()}`, 'driver');
var responseHeaders = {};
const responseHeaders = {};
headers.forEach(line => {
var name, value;
headers.forEach((line) => {
let name,
value;
if ( line ) {
name = line.substring(0, line.indexOf(': '));
if (line) {
name = line.substring(0, line.indexOf(': '));
value = line.substring(line.indexOf(': ') + 2, line.length - 1);
if ( !responseHeaders[name.toLowerCase()] ){
responseHeaders[name.toLowerCase()] = []
if (!responseHeaders[name.toLowerCase()]) {
responseHeaders[name.toLowerCase()] = [];
}
responseHeaders[name.toLowerCase()].push(value);
}
});
wappalyzer.analyze(url, {
headers: responseHeaders
headers: responseHeaders,
});
}
}
}
};
xhr.send();
}
@ -83,46 +84,45 @@
/**
* Display apps
*/
wappalyzer.driver.displayApps = detected => {
wappalyzer.driver.displayApps = (detected) => {
wappalyzer.log('func: diplayApps', 'driver');
var first = true;
var app;
var category;
var html;
let first = true;
let app;
let category;
let html;
html =
'<a id="wappalyzer-close" href="javascript: document.body.removeChild(document.getElementById(\'wappalyzer-container\')); void(0);">' +
'Close' +
'</a>' +
'<div id="wappalyzer-apps">';
html = '<a id="wappalyzer-close" href="javascript: document.body.removeChild(document.getElementById(\'wappalyzer-container\')); void(0);">'
+ 'Close'
+ '</a>'
+ '<div id="wappalyzer-apps">';
if ( detected != null && Object.keys(detected).length ) {
for ( app in detected ) {
if ( !hasOwn.call(detected, app) ) {
if (detected != null && Object.keys(detected).length) {
for (app in detected) {
if (!hasOwn.call(detected, app)) {
continue;
}
var version = detected[app].version,
let version = detected[app].version,
confidence = detected[app].confidence;
html +=
'<div class="wappalyzer-app' + ( first ? ' wappalyzer-first' : '' ) + '">' +
'<a target="_blank" class="wappalyzer-application" href="' + wappalyzer.config.websiteURL + 'applications/' + app.toLowerCase().replace(/ /g, '-').replace(/[^a-z0-9-]/g, '') + '">' +
'<strong>' +
'<img src="' + wappalyzer.config.websiteURL + 'images/icons/' + (wappalyzer.apps[app].icon || 'default.svg') + '" width="16" height="16"/> ' + app +
'</strong>' +
( version ? ' ' + version : '' ) + ( confidence < 100 ? ' (' + confidence + '% sure)' : '' ) +
'</a>';
for ( let i in wappalyzer.apps[app].cats ) {
if ( !hasOwn.call(wappalyzer.apps[app].cats, i) ) {
html
+= `<div class="wappalyzer-app${first ? ' wappalyzer-first' : ''}">`
+ `<a target="_blank" class="wappalyzer-application" href="${wappalyzer.config.websiteURL}applications/${app.toLowerCase().replace(/ /g, '-').replace(/[^a-z0-9-]/g, '')}">`
+ '<strong>'
+ `<img src="${wappalyzer.config.websiteURL}images/icons/${wappalyzer.apps[app].icon || 'default.svg'}" width="16" height="16"/> ${app
}</strong>${
version ? ` ${version}` : ''}${confidence < 100 ? ` (${confidence}% sure)` : ''
}</a>`;
for (const i in wappalyzer.apps[app].cats) {
if (!hasOwn.call(wappalyzer.apps[app].cats, i)) {
continue;
}
category = wappalyzer.categories[wappalyzer.apps[app].cats[i]].name;
html += '<a target="_blank" class="wappalyzer-category" href="' + wappalyzer.config.websiteURL + 'categories/' + slugify(category) + '">' + category + '</a>';
html += `<a target="_blank" class="wappalyzer-category" href="${wappalyzer.config.websiteURL}categories/${slugify(category)}">${category}</a>`;
}
html += '</div>';
@ -143,7 +143,7 @@
*/
function openTab(args) {
open(args.url);
}
};
function slugify(string) {
return string.toLowerCase().replace(/[^a-z0-9-]/g, '-').replace(/--+/g, '-').replace(/(?:^-|-$)/, '');
@ -151,4 +151,4 @@
getPageContent();
getResponseHeaders();
})();
}());

@ -1,27 +1,79 @@
'use strict';
const Wappalyzer = require('./wappalyzer');
const url = require('url');
const fs = require('fs');
const path = require('path');
const Browser = require('zombie');
const Wappalyzer = require('./wappalyzer');
const json = JSON.parse(fs.readFileSync(path.resolve(__dirname + '/apps.json')));
const json = JSON.parse(fs.readFileSync(path.resolve(`${__dirname}/apps.json`)));
const extensions = /^([^.]+$|\.(asp|aspx|cgi|htm|html|jsp|php)$)/;
const errorTypes = {
RESPONSE_NOT_OK: 'Response was not ok',
NO_RESPONSE: 'No response from server',
NO_HTML_DOCUMENT: 'No HTML document',
RESPONSE_NOT_OK: 'Response was not ok',
NO_RESPONSE: 'No response from server',
NO_HTML_DOCUMENT: 'No HTML document',
};
function sleep(ms) {
return ms ? new Promise(resolve => setTimeout(resolve, ms)) : Promise.resolve();
}
function getHeaders(browser) {
const headers = {};
const resource = browser.resources.length
? browser.resources.filter(_resource => _resource.response).shift() : null;
if (resource) {
// eslint-disable-next-line no-underscore-dangle
resource.response.headers._headers.forEach((header) => {
if (!headers[header[0]]) {
headers[header[0]] = [];
}
headers[header[0]].push(header[1]);
});
}
return headers;
}
function getScripts(browser) {
if (!browser.document || !browser.document.scripts) {
return [];
}
const scripts = Array.prototype.slice
.apply(browser.document.scripts)
.filter(script => script.src)
.map(script => script.src);
return scripts;
}
function getCookies(browser) {
const cookies = [];
if (browser.cookies) {
browser.cookies.forEach(cookie => cookies.push({
name: cookie.key,
value: cookie.value,
domain: cookie.domain,
path: cookie.path,
}));
}
return cookies;
}
class Driver {
constructor(pageUrl, options) {
this.options = Object.assign({}, {
password: '',
proxy: null,
username: '',
password: '',
proxy: null,
username: '',
chunkSize: 5,
debug: false,
delay: 500,
@ -58,46 +110,48 @@ class Driver {
this.wappalyzer.driver.log = (message, source, type) => this.log(message, source, type);
this.wappalyzer.driver.displayApps = (detected, meta, context) => this.displayApps(detected, meta, context);
process.on('uncaughtException', e => this.wappalyzer.log('Uncaught exception: ' + e.message, 'driver', 'error'));
process.on('uncaughtException', e => this.wappalyzer.log(`Uncaught exception: ${e.message}`, 'driver', 'error'));
}
analyze() {
this.time = {
start: new Date().getTime(),
last: new Date().getTime(),
}
};
return this.crawl(this.origPageUrl);
}
log(message, source, type) {
this.options.debug && console.log('[wappalyzer ' + type + ']', '[' + source + ']', message);
if (this.options.debug) {
console.log(`[wappalyzer ${type}]`, `[${source}]`, message);
}
}
displayApps(detected, meta) {
this.meta = meta;
Object.keys(detected).forEach(appName => {
Object.keys(detected).forEach((appName) => {
const app = detected[appName];
var categories = [];
const categories = [];
app.props.cats.forEach(id => {
var category = {};
app.props.cats.forEach((id) => {
const category = {};
category[id] = json.categories[id].name;
categories.push(category)
categories.push(category);
});
if ( !this.apps.some(detectedApp => detectedApp.name === app.name) ) {
if (!this.apps.some(detectedApp => detectedApp.name === app.name)) {
this.apps.push({
name: app.name,
confidence: app.confidenceTotal.toString(),
version: app.version || null,
icon: app.props.icon || 'default.svg',
website: app.props.website,
categories
categories,
});
}
});
@ -105,55 +159,65 @@ class Driver {
fetch(pageUrl, index, depth) {
// Return when the URL is a duplicate or maxUrls has been reached
if (this.analyzedPageUrls[pageUrl.href] || this.analyzedPageUrls.length >= this.options.maxUrls) {
if (
this.analyzedPageUrls[pageUrl.href]
|| this.analyzedPageUrls.length >= this.options.maxUrls
) {
return Promise.resolve();
}
this.analyzedPageUrls[pageUrl.href] = {
status: 0,
};
status: 0,
};
const timerScope = {
last: new Date().getTime()
last: new Date().getTime(),
};
this.timer('fetch; url: ' + pageUrl.href + '; depth: ' + depth + '; delay: ' + (this.options.delay * index) + 'ms', timerScope);
this.timer(`fetch; url: ${pageUrl.href}; depth: ${depth}; delay: ${this.options.delay * index}ms`, timerScope);
return new Promise((resolve, reject) => this.sleep(this.options.delay * index).then(() => this.visit(pageUrl, timerScope, resolve, reject)));
return new Promise((resolve, reject) => {
sleep(this.options.delay * index)
.then(() => this.visit(pageUrl, timerScope, resolve, reject));
});
}
visit(pageUrl, timerScope, resolve, reject) {
const browser = new Browser({
proxy: this.options.proxy,
proxy: this.options.proxy,
silent: true,
strictSSL: false,
userAgent: this.options.userAgent,
waitDuration: this.options.maxWait,
});
browser.on('authenticate', auth => {
auth.username = this.options.username;
auth.password = this.options.password;
});
browser.on('authenticate', (auth) => {
auth.username = this.options.username;
auth.password = this.options.password;
});
this.timer('browser.visit start; url: ' + pageUrl.href, timerScope);
this.timer(`browser.visit start; url: ${pageUrl.href}`, timerScope);
browser.visit(pageUrl.href, () => {
this.timer('browser.visit end; url: ' + pageUrl.href, timerScope);
this.timer(`browser.visit end; url: ${pageUrl.href}`, timerScope);
try {
if (!this.checkResponse(browser, pageUrl)) {
resolve();
try {
if (!this.checkResponse(browser, pageUrl)) {
return resolve();
}
} catch(error) {
return reject(error);
}
return;
}
} catch (error) {
reject(error);
return;
}
const headers = this.getHeaders(browser);
const headers = getHeaders(browser);
const html = this.getHtml(browser);
const scripts = this.getScripts(browser);
const scripts = getScripts(browser);
const js = this.getJs(browser);
const cookies = this.getCookies(browser);
const cookies = getCookies(browser);
this.wappalyzer.analyze(pageUrl, {
headers,
@ -165,14 +229,14 @@ class Driver {
.then(() => {
const links = Array.prototype.reduce.call(
browser.document.getElementsByTagName('a'), (results, link) => {
if ( link.protocol.match(/https?:/) && link.hostname === this.origPageUrl.hostname && extensions.test(link.pathname) ) {
if (link.protocol.match(/https?:/) && link.hostname === this.origPageUrl.hostname && extensions.test(link.pathname)) {
link.hash = '';
results.push(url.parse(link.href));
}
return results;
}, []
}, [],
);
return resolve(links);
@ -182,25 +246,26 @@ class Driver {
checkResponse(browser, pageUrl) {
// Validate response
const resource = browser.resources.length ? browser.resources.filter(resource => resource.response).shift() : null;
const resource = browser.resources.length
? browser.resources.filter(_resource => _resource.response).shift() : null;
if ( !resource ) {
if (!resource) {
throw new Error('NO_RESPONSE');
}
this.analyzedPageUrls[pageUrl.href].status = resource.response.status;
this.analyzedPageUrls[pageUrl.href].status = resource.response.status;
if ( resource.response.status !== 200 ) {
if (resource.response.status !== 200) {
throw new Error('RESPONSE_NOT_OK');
}
const headers = this.getHeaders(browser);
const headers = getHeaders(browser);
// Validate content type
const contentType = headers.hasOwnProperty('content-type') ? headers['content-type'].shift() : null;
const contentType = headers['content-type'] ? headers['content-type'].shift() : null;
if ( !contentType || !/\btext\/html\b/.test(contentType) ) {
this.wappalyzer.log('Skipping; url: ' + pageUrl.href + '; content type: ' + contentType, 'driver');
if (!contentType || !/\btext\/html\b/.test(contentType)) {
this.wappalyzer.log(`Skipping; url: ${pageUrl.href}; content type: ${contentType}`, 'driver');
delete this.analyzedPageUrls[pageUrl.href];
@ -208,80 +273,50 @@ class Driver {
}
// Validate document
if ( !browser.document || !browser.document.documentElement ) {
if (!browser.document || !browser.document.documentElement) {
throw new Error('NO_HTML_DOCUMENT');
}
return true;
}
getHeaders(browser) {
const headers = {};
const resource = browser.resources.length ? browser.resources.filter(resource => resource.response).shift() : null;
if ( resource ) {
resource.response.headers._headers.forEach(header => {
if ( !headers[header[0]] ){
headers[header[0]] = [];
}
headers[header[0]].push(header[1]);
});
}
return headers;
}
getHtml(browser) {
let html = '';
try {
html = browser.html()
.split('\n')
.slice(0, this.options.htmlMaxRows / 2).concat(html.slice(html.length - this.options.htmlMaxRows / 2))
.slice(0, this.options.htmlMaxRows / 2)
.concat(html.slice(html.length - this.options.htmlMaxRows / 2))
.map(line => line.substring(0, this.options.htmlMaxCols))
.join('\n');
} catch(error) {
} catch (error) {
this.wappalyzer.log(error.message, 'browser', 'error');
}
return html;
}
getScripts(browser) {
if ( !browser.document || !browser.document.scripts ) {
return [];
}
const scripts = Array.prototype.slice
.apply(browser.document.scripts)
.filter(script => script.src)
.map(script => script.src);
return scripts;
}
getJs(browser) {
const patterns = this.wappalyzer.jsPatterns;
const js = {};
Object.keys(patterns).forEach(appName => {
Object.keys(patterns).forEach((appName) => {
js[appName] = {};
Object.keys(patterns[appName]).forEach(chain => {
Object.keys(patterns[appName]).forEach((chain) => {
js[appName][chain] = {};
patterns[appName][chain].forEach((pattern, index) => {
const properties = chain.split('.');
let value = properties.reduce((parent, property) => {
return parent && parent.hasOwnProperty(property) ? parent[property] : null;
}, browser.window);
let value = properties
.reduce((parent, property) => (parent && parent[property]
? parent[property] : null), browser.window);
value = typeof value === 'string' || typeof value === 'number' ? value : !!value;
if ( value ) {
if (value) {
js[appName][chain][index] = value;
}
});
@ -291,81 +326,61 @@ class Driver {
return js;
}
getCookies(browser) {
const cookies = [];
if ( browser.cookies ) {
browser.cookies.forEach(cookie => cookies.push({
name: cookie.key,
value: cookie.value,
domain: cookie.domain,
path: cookie.path,
}));
}
return cookies;
}
crawl(pageUrl, index = 1, depth = 1) {
pageUrl.canonical = pageUrl.protocol + '//' + pageUrl.host + pageUrl.pathname;
pageUrl.canonical = `${pageUrl.protocol}//${pageUrl.host}${pageUrl.pathname}`;
return new Promise(resolve => {
return new Promise((resolve) => {
this.fetch(pageUrl, index, depth)
.catch(error => {
const type = error.message && errorTypes[error.message] ? error.message : 'UNKNOWN_ERROR';
const message = error.message && errorTypes[error.message] ? errorTypes[error.message] : 'Unknown error';
this.analyzedPageUrls[pageUrl.href].error = {
type,
message,
};
this.wappalyzer.log(`${message}; url: ${pageUrl.href}`, 'driver', 'error');
})
.then(links => {
if ( links && this.options.recursive && depth < this.options.maxDepth ) {
.catch((error) => {
const type = error.message && errorTypes[error.message] ? error.message : 'UNKNOWN_ERROR';
const message = error.message && errorTypes[error.message] ? errorTypes[error.message] : 'Unknown error';
this.analyzedPageUrls[pageUrl.href].error = {
type,
message,
};
this.wappalyzer.log(`${message}; url: ${pageUrl.href}`, 'driver', 'error');
})
.then((links) => {
if (links && this.options.recursive && depth < this.options.maxDepth) {
return this.chunk(links.slice(0, this.options.maxUrls), depth + 1);
} else {
return Promise.resolve();
}
return Promise.resolve();
})
.then(() => {
resolve({
urls: this.analyzedPageUrls,
applications: this.apps,
meta: this.meta
meta: this.meta,
});
});
});
}
chunk(links, depth, chunk = 0) {
if ( links.length === 0 ) {
if (links.length === 0) {
return Promise.resolve();
}
const chunked = links.splice(0, this.options.chunkSize);
return new Promise(resolve => {
return new Promise((resolve) => {
Promise.all(chunked.map((link, index) => this.crawl(link, index, depth)))
.then(() => this.chunk(links, depth, chunk + 1))
.then(() => resolve());
});
}
sleep(ms) {
return ms ? new Promise(resolve => setTimeout(resolve, ms)) : Promise.resolve();
}
timer(message, scope) {
const time = new Date().getTime();
const sinceStart = ( Math.round(( time - this.time.start ) / 10) / 100) + 's';
const sinceLast = ( Math.round(( time - scope.last ) / 10) / 100) + 's';
const sinceStart = `${Math.round((time - this.time.start) / 10) / 100}s`;
const sinceLast = `${Math.round((time - scope.last) / 10) / 100}s`;
this.wappalyzer.log('[timer] ' + message + '; lapsed: ' + sinceLast + ' / ' + sinceStart, 'driver');
this.wappalyzer.log(`[timer] ${message}; lapsed: ${sinceLast} / ${sinceStart}`, 'driver');
scope.last = time;
}
};
}
module.exports = Driver;

@ -1,5 +1,5 @@
#!/usr/bin/env node
'use strict';
const Wappalyzer = require('./driver');
@ -7,36 +7,39 @@ const args = process.argv.slice(2);
const url = args.shift() || '';
if ( !url ) {
if (!url) {
process.stderr.write('No URL specified\n');
process.exit(1);
}
let options = {};
const options = {};
let arg;
while ( arg = args.shift() ) {
let matches = /--([^=]+)=(.+)/.exec(arg);
do {
arg = args.shift();
if ( matches ) {
let key = matches[1].replace(/-\w/g, matches => matches[1].toUpperCase());
let value = matches[2];
const matches = /--([^=]+)=(.+)/.exec(arg);
if (matches) {
const key = matches[1].replace(/-\w/g, _matches => _matches[1].toUpperCase());
const value = matches[2];
options[key] = value;
}
}
} while (arg);
const wappalyzer = new Wappalyzer(url, options);
wappalyzer.analyze()
.then(json => {
process.stdout.write(JSON.stringify(json) + '\n')
.then((json) => {
process.stdout.write(`${JSON.stringify(json)}\n`);
process.exit(0);
})
.catch(error => {
process.stderr.write(error + '\n')
.catch((error) => {
process.stderr.write(`${error}\n`);
process.exit(1);
});

@ -7,7 +7,7 @@
<script src="../node_modules/webextension-polyfill/dist/browser-polyfill.js"></script>
<script src="../js/wappalyzer.js"></script>
<script src="../js/driver.js"></script>
<script src="../js/network.js"></script>
<script src="../js/lib/network.js"></script>
</head>
<body>
</body>

@ -7,7 +7,7 @@
<link rel="stylesheet" href="../css/popup.css">
<script src="../node_modules/webextension-polyfill/dist/browser-polyfill.js"></script>
<script src="../js/jsontodom.js"></script>
<script src="../js/lib/jsontodom.js"></script>
<script src="../js/popup.js"></script>
</head>
<body>

@ -1,12 +1,12 @@
/** global: browser */
/** global: XMLSerializer */
if ( typeof browser !== 'undefined' && typeof document.body !== 'undefined' ) {
if (typeof browser !== 'undefined' && typeof document.body !== 'undefined') {
try {
sendMessage('init', {});
// HTML
var html = new XMLSerializer().serializeToString(document).split('\n');
let html = new XMLSerializer().serializeToString(document).split('\n');
html = html
.slice(0, 1000).concat(html.slice(html.length - 1000))
@ -26,7 +26,7 @@ if ( typeof browser !== 'undefined' && typeof document.body !== 'undefined' ) {
const script = document.createElement('script');
script.onload = () => {
const onMessage = event => {
const onMessage = (event) => {
if (event.data.id !== 'js') {
return;
}
@ -40,11 +40,11 @@ if ( typeof browser !== 'undefined' && typeof document.body !== 'undefined' ) {
addEventListener('message', onMessage);
sendMessage('get_js_patterns', {}, response => {
sendMessage('get_js_patterns', {}, (response) => {
if (response) {
postMessage({
id: 'patterns',
patterns: response.patterns
patterns: response.patterns,
}, '*');
}
});
@ -62,8 +62,8 @@ function sendMessage(id, subject, callback) {
(chrome || browser).runtime.sendMessage({
id,
subject,
source: 'content.js'
}, callback || ( () => {} ));
source: 'content.js',
}, callback || (() => {}));
}
// https://stackoverflow.com/a/44774834

@ -2,17 +2,22 @@
* WebExtension driver
*/
/* eslint-env browser */
/* global browser, chrome, fetch, Wappalyzer */
/** global: browser */
/** global: chrome */
/** global: fetch */
/** global: Wappalyzer */
const wappalyzer = new Wappalyzer();
var tabCache = {};
var categoryOrder = [];
var options = {};
var robotsTxtQueue = {};
const tabCache = {};
let categoryOrder = [];
const options = {};
const robotsTxtQueue = {};
browser.tabs.onRemoved.addListener(tabId => {
browser.tabs.onRemoved.addListener((tabId) => {
tabCache[tabId] = null;
});
@ -21,19 +26,19 @@ browser.tabs.onRemoved.addListener(tabId => {
*/
function getOption(name, defaultValue = null) {
return new Promise((resolve, reject) => {
const callback = item => {
options[name] = item.hasOwnProperty(name) ? item[name] : defaultValue;
const callback = (item) => {
options[name] = item[name] ? item[name] : defaultValue;
resolve(options[name]);
};
browser.storage.local.get(name)
.then(callback)
.catch(error => {
wappalyzer.log(error, 'driver', 'error')
.catch((error) => {
wappalyzer.log(error, 'driver', 'error');
reject();
});
});
});
}
@ -56,7 +61,7 @@ function setOption(name, value) {
function openTab(args) {
browser.tabs.create({
url: args.url,
active: args.background === undefined || !args.background
active: args.background === undefined || !args.background,
});
}
@ -66,7 +71,7 @@ function openTab(args) {
function post(url, body) {
fetch(url, {
method: 'POST',
body: JSON.stringify(body)
body: JSON.stringify(body),
})
.then(response => wappalyzer.log(`POST ${url}: ${response.status}`, 'driver'))
.catch(error => wappalyzer.log(`POST ${url}: ${error}`, 'driver', 'error'));
@ -74,7 +79,7 @@ function post(url, body) {
fetch('../apps.json')
.then(response => response.json())
.then(json => {
.then((json) => {
wappalyzer.apps = json.apps;
wappalyzer.categories = json.categories;
@ -87,21 +92,21 @@ fetch('../apps.json')
.catch(error => wappalyzer.log(`GET apps.json: ${error}`, 'driver', 'error'));
// Version check
let version = browser.runtime.getManifest().version;
const { version } = browser.runtime.getManifest();
getOption('version')
.then(previousVersion => {
.then((previousVersion) => {
if (previousVersion === null) {
openTab({
url: wappalyzer.config.websiteURL + 'installed'
url: `${wappalyzer.config.websiteURL}installed`,
});
} else if (version !== previousVersion) {
getOption('upgradeMessage', true)
.then(upgradeMessage => {
.then((upgradeMessage) => {
if (upgradeMessage) {
openTab({
url: wappalyzer.config.websiteURL + 'upgraded?v' + version,
background: true
url: `${wappalyzer.config.websiteURL}upgraded?v${version}`,
background: true,
});
}
});
@ -113,32 +118,37 @@ getOption('version')
getOption('dynamicIcon', true);
getOption('pinnedCategory');
getOption('hostnameCache', {}).then(hostnameCache => wappalyzer.hostnameCache = hostnameCache);
getOption('hostnameCache', {})
.then((hostnameCache) => {
wappalyzer.hostnameCache = hostnameCache;
return hostnameCache;
});
// Run content script on all tabs
browser.tabs.query({ url: [ 'http://*/*', 'https://*/*' ] })
.then(tabs => {
tabs.forEach(tab => {
browser.tabs.query({ url: ['http://*/*', 'https://*/*'] })
.then((tabs) => {
tabs.forEach((tab) => {
browser.tabs.executeScript(tab.id, {
file: '../js/content.js'
file: '../js/content.js',
});
})
});
})
.catch(error => wappalyzer.log(error, 'driver', 'error'));
// Capture response headers
browser.webRequest.onCompleted.addListener(request => {
browser.webRequest.onCompleted.addListener((request) => {
const headers = {};
if (request.responseHeaders) {
const url = wappalyzer.parseUrl(request.url);
browser.tabs.query({ url: [ url.href ] })
.then(tabs => {
browser.tabs.query({ url: [url.href] })
.then((tabs) => {
const tab = tabs[0] || null;
if (tab) {
request.responseHeaders.forEach(header => {
request.responseHeaders.forEach((header) => {
const name = header.name.toLowerCase();
headers[name] = headers[name] || [];
@ -153,32 +163,32 @@ browser.webRequest.onCompleted.addListener(request => {
})
.catch(error => wappalyzer.log(error, 'driver', 'error'));
}
}, { urls: [ 'http://*/*', 'https://*/*' ], types: [ 'main_frame' ] }, [ 'responseHeaders' ]);
}, { urls: ['http://*/*', 'https://*/*'], types: ['main_frame'] }, ['responseHeaders']);
// Listen for messages
(chrome || browser).runtime.onMessage.addListener((message, sender, sendResponse) => {
if (typeof message.id != 'undefined') {
if (typeof message.id !== 'undefined') {
if (message.id !== 'log') {
wappalyzer.log('Message' + (message.source ? ' from ' + message.source : '') + ': ' + message.id, 'driver');
wappalyzer.log(`Message${message.source ? ` from ${message.source}` : ''}: ${message.id}`, 'driver');
}
let url = wappalyzer.parseUrl(sender.tab ? sender.tab.url : '');
const url = wappalyzer.parseUrl(sender.tab ? sender.tab.url : '');
let response;
switch ( message.id ) {
switch (message.id) {
case 'log':
wappalyzer.log(message.subject, message.source);
break;
case 'init':
browser.cookies.getAll({ domain: '.' + url.hostname })
browser.cookies.getAll({ domain: `.${url.hostname}` })
.then(cookies => wappalyzer.analyze(url, { cookies }, { tab: sender.tab }));
break;
case 'analyze':
wappalyzer.analyze(url, message.subject, { tab: sender.tab });
setOption('hostnameCache', wappalyzer.hostnameCache);
setOption('hostnameCache', wappalyzer.hostnameCache);
break;
case 'ad_log':
@ -200,7 +210,7 @@ browser.webRequest.onCompleted.addListener(request => {
break;
case 'get_js_patterns':
response = {
patterns: wappalyzer.jsPatterns
patterns: wappalyzer.jsPatterns,
};
break;
@ -226,14 +236,14 @@ wappalyzer.driver.log = (message, source, type) => {
* Display apps
*/
wappalyzer.driver.displayApps = (detected, meta, context) => {
let tab = context.tab;
const { tab } = context;
if (tab === undefined) {
return;
}
tabCache[tab.id] = tabCache[tab.id] || {
detected: []
detected: [],
};
tabCache[tab.id].detected = detected;
@ -241,11 +251,11 @@ wappalyzer.driver.displayApps = (detected, meta, context) => {
let found = false;
// Find the main application to display
[ options.pinnedCategory ].concat(categoryOrder).forEach(match => {
Object.keys(detected).forEach(appName => {
let app = detected[appName];
[options.pinnedCategory].concat(categoryOrder).forEach((match) => {
Object.keys(detected).forEach((appName) => {
const app = detected[appName];
app.props.cats.forEach(category => {
app.props.cats.forEach((category) => {
if (category === match && !found) {
let icon = app.props.icon || 'default.svg';
@ -254,15 +264,15 @@ wappalyzer.driver.displayApps = (detected, meta, context) => {
}
if (/\.svg$/i.test(icon)) {
icon = 'converted/' + icon.replace(/\.svg$/, '.png');
icon = `converted/${icon.replace(/\.svg$/, '.png')}`;
}
try {
browser.pageAction.setIcon({
tabId: tab.id,
path: '../images/icons/' + icon
path: `../images/icons/${icon}`,
});
} catch(e) {
} catch (e) {
// Firefox for Android does not support setIcon see https://bugzilla.mozilla.org/show_bug.cgi?id=1331746
}
@ -284,35 +294,39 @@ wappalyzer.driver.displayApps = (detected, meta, context) => {
* Fetch and cache robots.txt for host
*/
wappalyzer.driver.getRobotsTxt = (host, secure = false) => {
if (robotsTxtQueue.hasOwnProperty(host)) {
if (robotsTxtQueue[host]) {
return robotsTxtQueue[host];
}
robotsTxtQueue[host] = new Promise(resolve => {
robotsTxtQueue[host] = new Promise((resolve) => {
getOption('tracking', true)
.then(tracking => {
.then((tracking) => {
if (!tracking) {
return resolve([]);
resolve([]);
return;
}
getOption('robotsTxtCache')
.then(robotsTxtCache => {
.then((robotsTxtCache) => {
robotsTxtCache = robotsTxtCache || {};
if ( host in robotsTxtCache ) {
return resolve(robotsTxtCache[host]);
if (host in robotsTxtCache) {
resolve(robotsTxtCache[host]);
return;
}
const timeout = setTimeout(() => resolve([]), 3000);
fetch('http' + (secure ? 's' : '') + '://' + host + '/robots.txt', { redirect: 'follow' })
.then(response => {
fetch(`http${secure ? 's' : ''}://${host}/robots.txt`, { redirect: 'follow' })
.then((response) => {
clearTimeout(timeout);
return response.ok ? response.text() : '';
})
.then(robotsTxt => {
robotsTxtCache[host] = wappalyzer.parseRobotsTxt(robotsTxt);
.then((robotsTxt) => {
robotsTxtCache[host] = Wappalyzer.parseRobotsTxt(robotsTxt);
setOption('robotsTxtCache', robotsTxtCache);
@ -322,7 +336,7 @@ wappalyzer.driver.getRobotsTxt = (host, secure = false) => {
});
});
})
.finally(() => delete robotsTxtQueue[host]);
.finally(() => delete robotsTxtQueue[host]);
return robotsTxtQueue[host];
};
@ -332,7 +346,7 @@ wappalyzer.driver.getRobotsTxt = (host, secure = false) => {
*/
wappalyzer.driver.ping = (hostnameCache = {}, adCache = []) => {
getOption('tracking', true)
.then(tracking => {
.then((tracking) => {
if (tracking) {
if (Object.keys(hostnameCache).length) {
post('https://api.wappalyzer.com/ping/v1/', hostnameCache);

File diff suppressed because it is too large Load Diff

@ -1,12 +1,12 @@
(() => {
try {
const detectJs = chain => {
try {
const detectJs = (chain) => {
const properties = chain.split('.');
let value = properties.length ? window : null;
for (let i = 0; i < properties.length; i ++) {
let property = properties[i];
for (let i = 0; i < properties.length; i++) {
const property = properties[i];
if (value && value.hasOwnProperty(property)) {
value = value[property];
@ -20,7 +20,7 @@
return typeof value === 'string' || typeof value === 'number' ? value : !!value;
};
const onMessage = event => {
const onMessage = (event) => {
if (event.data.id !== 'patterns') {
return;
}
@ -31,15 +31,15 @@
const js = {};
for (let appName in patterns) {
for (const appName in patterns) {
if (patterns.hasOwnProperty(appName)) {
js[appName] = {};
for (let chain in patterns[appName]) {
for (const chain in patterns[appName]) {
if (patterns[appName].hasOwnProperty(chain)) {
js[appName][chain] = {};
for (let index in patterns[appName][chain]) {
for (const index in patterns[appName][chain]) {
const value = detectJs(chain);
if (value && patterns[appName][chain].hasOwnProperty(index)) {
@ -55,7 +55,7 @@
};
addEventListener('message', onMessage);
} catch(e) {
} catch (e) {
// Fail quietly
}
})();

@ -1,63 +0,0 @@
jsonToDOM.namespaces = {
html: "http://www.w3.org/1999/xhtml",
xul: "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
};
jsonToDOM.defaultNamespace = jsonToDOM.namespaces.html;
function jsonToDOM(jsonTemplate, doc, nodes) {
function namespace(name) {
var reElemNameParts = /^(?:(.*):)?(.*)$/.exec(name);
return { namespace: jsonToDOM.namespaces[reElemNameParts[1]], shortName: reElemNameParts[2] };
}
// Note that 'elemNameOrArray' is: either the full element name (eg. [html:]div) or an array of elements in JSON notation
function tag(elemNameOrArray, elemAttr) {
// Array of elements? Parse each one...
if (Array.isArray(elemNameOrArray)) {
var frag = doc.createDocumentFragment();
Array.prototype.forEach.call(arguments, function(thisElem) {
frag.appendChild(tag.apply(null, thisElem));
});
return frag;
}
// Single element? Parse element namespace prefix (if none exists, default to defaultNamespace), and create element
var elemNs = namespace(elemNameOrArray);
var elem = doc.createElementNS(elemNs.namespace || jsonToDOM.defaultNamespace, elemNs.shortName);
// Set element's attributes and/or callback functions (eg. onclick)
for (var key in elemAttr) {
var val = elemAttr[key];
if (nodes && key == "key") {
nodes[val] = elem;
continue;
}
var attrNs = namespace(key);
if (typeof val == "function") {
// Special case for function attributes; don't just add them as 'on...' attributes, but as events, using addEventListener
elem.addEventListener(key.replace(/^on/, ""), val, false);
}
else {
// Note that the default namespace for XML attributes is, and should be, blank (ie. they're not in any namespace)
elem.setAttributeNS(attrNs.namespace || "", attrNs.shortName, val);
}
}
// Create and append this element's children
var childElems = Array.prototype.slice.call(arguments, 2);
childElems.forEach(function(childElem) {
if (childElem != null) {
elem.appendChild(
childElem instanceof doc.defaultView.Node ? childElem :
Array.isArray(childElem) ? tag.apply(null, childElem) :
doc.createTextNode(childElem));
}
});
return elem;
}
return tag.apply(null, jsonTemplate);
}

File diff suppressed because it is too large Load Diff

@ -0,0 +1,63 @@
jsonToDOM.namespaces = {
html: 'http://www.w3.org/1999/xhtml',
xul: 'http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul',
};
jsonToDOM.defaultNamespace = jsonToDOM.namespaces.html;
function jsonToDOM(jsonTemplate, doc, nodes) {
function namespace(name) {
const reElemNameParts = /^(?:(.*):)?(.*)$/.exec(name);
return { namespace: jsonToDOM.namespaces[reElemNameParts[1]], shortName: reElemNameParts[2] };
}
// Note that 'elemNameOrArray' is: either the full element name (eg. [html:]div) or an array of elements in JSON notation
function tag(elemNameOrArray, elemAttr) {
// Array of elements? Parse each one...
if (Array.isArray(elemNameOrArray)) {
const frag = doc.createDocumentFragment();
Array.prototype.forEach.call(arguments, (thisElem) => {
frag.appendChild(tag(...thisElem));
});
return frag;
}
// Single element? Parse element namespace prefix (if none exists, default to defaultNamespace), and create element
const elemNs = namespace(elemNameOrArray);
const elem = doc.createElementNS(elemNs.namespace || jsonToDOM.defaultNamespace, elemNs.shortName);
// Set element's attributes and/or callback functions (eg. onclick)
for (const key in elemAttr) {
const val = elemAttr[key];
if (nodes && key == 'key') {
nodes[val] = elem;
continue;
}
const attrNs = namespace(key);
if (typeof val === 'function') {
// Special case for function attributes; don't just add them as 'on...' attributes, but as events, using addEventListener
elem.addEventListener(key.replace(/^on/, ''), val, false);
} else {
// Note that the default namespace for XML attributes is, and should be, blank (ie. they're not in any namespace)
elem.setAttributeNS(attrNs.namespace || '', attrNs.shortName, val);
}
}
// Create and append this element's children
const childElems = Array.prototype.slice.call(arguments, 2);
childElems.forEach((childElem) => {
if (childElem != null) {
elem.appendChild(
childElem instanceof doc.defaultView.Node ? childElem
: Array.isArray(childElem) ? tag(...childElem)
: doc.createTextNode(childElem),
);
}
});
return elem;
}
return tag(...jsonTemplate);
}

@ -0,0 +1,793 @@
(function () {
function isChrome() {
return (typeof chrome !== 'undefined'
&& window.navigator.userAgent.match(/Chrom(?:e|ium)\/([0-9\.]+)/));
}
let browserProxy;
if (isChrome()) {
browserProxy = chrome;
} else {
browserProxy = browser;
}
const MIN_FF_MAJOR_VERSION = 51;
const requiredBrowserApis = [
browserProxy.webNavigation,
browserProxy.tabs,
browserProxy.webRequest,
browserProxy.runtime,
];
let areListenersRegistered = false;
const secBefore = 2000;
const secAfter = 5000;
const secBetweenDupAssets = 10e3;
const minVidSize = 500e3;
const maxVidSize = 25e6;
const maxContentRange = 25e6;
const videoExtensions = [
'af', '3gp', 'asf', 'avchd', 'avi', 'cam', 'dsh', 'flv', 'm1v', 'm2v',
'fla', 'flr', 'sol', 'm4v', 'mkv', 'wrap', 'mng', 'mov', 'mpeg', 'mpg',
'mpe', 'mp4', 'mxf', 'nsv', 'ogg', 'rm', 'svi', 'smi', 'wmv', 'webm',
];
const extensionsReg = new RegExp(`\\.${videoExtensions.join('$|\\.')}$`);
const videoContentTypesPrefixes = ['binary/octet-stream', 'video/', 'flv-application/', 'media'];
const bannedContentTypes = ['video/mp2t', 'video/f4m', 'video/f4f'];
const bannedFiletypes = ['ts'];
const bannedFiletypesReg = new RegExp(`\\.${bannedFiletypes.join('$|\\.')}$`);
const whitelistReqTypes = ['object', 'xmlhttprequest', 'other'];
const topVideoAssetDomains = [
'2mdn.net',
'adap.tv',
'adnxs.com',
'adsrvr.org',
'btrll.com',
'celtra.com',
'flashtalking.com',
'flite.com',
'innovid.com',
'jivox.com',
'mixpo.com',
'nytimes.com',
'playwire.com',
'selectmedia.asia',
'serving-sys.com',
'solvemedia.com',
'spotible.com',
'teads.tv',
'tribalfusion.com',
'tubemogul.com',
'videologygroup.com',
'washingtonpost.com',
];
const robotsTxtAllows = wappalyzer.robotsTxtAllows.bind(wappalyzer);
if (!String.prototype.endsWith) {
String.prototype.endsWith = function (searchString, position) {
const subjectString = this.toString();
if (typeof position !== 'number' || !isFinite(position)
|| Math.floor(position) !== position || position > subjectString.length) {
position = subjectString.length;
}
position -= searchString.length;
const lastIndex = subjectString.indexOf(searchString, position);
return lastIndex !== -1 && lastIndex === position;
};
}
function getFrame(getFrameDetails, callback) {
if (typeof chrome !== 'undefined') {
chrome.webNavigation.getFrame(getFrameDetails, callback);
} else if (typeof browser !== 'undefined') {
const gettingFrame = browser.webNavigation.getFrame(getFrameDetails);
gettingFrame.then(callback);
}
}
function ifBrowserValid(callback, elseCallback) {
if (isChrome()) {
callback();
} else if (typeof browser !== 'undefined') {
try {
const gettingInfo = browser.runtime.getBrowserInfo();
gettingInfo.then((browserInfo) => {
const browserVersion = parseInt(browserInfo.version.split('.')[0]);
if (browserInfo.name === 'Firefox'
&& browserVersion >= MIN_FF_MAJOR_VERSION) {
callback();
} else {
elseCallback();
}
});
} catch (err) {
elseCallback();
}
} else {
elseCallback();
}
}
function ifTrackingEnabled(details, ifCallback, elseCallback) {
const fullIfCallback = function () {
allowedByRobotsTxt(details, ifCallback, elseCallback);
};
browser.storage.local.get('tracking').then((item) => {
if (item.hasOwnProperty('tracking')) {
if (item.tracking) {
fullIfCallback();
} else {
elseCallback();
}
} else {
fullIfCallback();
}
});
}
function allowedByRobotsTxt(details, ifCallback, elseCallback) {
if (details.url && !details.url.startsWith('chrome://')) {
robotsTxtAllows(details.url).then(ifCallback, elseCallback);
} else {
elseCallback();
}
}
function isPixelRequest(request) {
return (request.type === 'image' || request.responseStatus === 204)
&& request.size <= 1000;
}
function isVpaidOrVastRequest(request) {
const lowerCaseUrl = request.url.toLowerCase();
return lowerCaseUrl.indexOf('vpaid') !== -1 || lowerCaseUrl.indexOf('vast') !== -1;
}
function hasValidRequestType(request) {
return whitelistReqTypes.indexOf(request.type) >= 0;
}
function stripQueryParams(url) {
return url.split('?', 1)[0];
}
function parseHostnameFromUrl(url) {
const parser = document.createElement('a');
parser.href = url;
return parser.hostname;
}
function hasDomain(url, domain) {
return parseHostnameFromUrl(url).endsWith(domain);
}
function findHeader(headers, key) {
let header;
for (let i = 0; i < headers.length; i += 1) {
header = headers[i];
if (header.name.toLowerCase() === key) {
return header;
}
}
return null;
}
function validVideoType(vtype) {
const goodType = videoContentTypesPrefixes.some(prefix => vtype.indexOf(prefix) === 0);
return goodType;
}
function assetMsgKey(assetReq) {
const url = stripQueryParams(assetReq.url);
const key = `${assetReq.frameId}-${url}`;
return key;
}
const PageNetworkTrafficCollector = function (tabId) {
this.tabId = tabId;
this.displayAdFound = false;
this.requests = {};
this.msgsBeingSent = {};
this.assetsSeen = {};
this.allRedirects = {};
};
var globalPageContainer = {
collectors: {},
dyingCollectors: {},
cleanupCollector(tabId) {
if (tabId in this.collectors) {
delete globalPageContainer.collectors[tabId];
}
},
onNewNavigation(details) {
const tabId = details.tabId;
this.cleanupCollector(tabId);
ifTrackingEnabled(
details,
() => {
if (!areListenersRegistered) {
registerListeners();
}
this.collectors[tabId] = new PageNetworkTrafficCollector(tabId);
},
() => {
if (areListenersRegistered) {
unregisterListeners();
}
},
);
},
onNavigationCommitted(details) {
},
onNavigationCompleted(details) {
},
onTabClose(tabId, closeInfo) {
this.cleanupCollector(tabId);
delete this.collectors[tabId];
},
onDisplayAdFound(tabId) {
this.collectors[tabId].displayAdFound = true;
},
getRandId() {
return String(Math.floor(Math.random() * 1e9));
},
getCollector(tabId) {
if (this.collectors.hasOwnProperty(tabId)) {
return this.collectors[tabId];
}
return null;
},
forwardCall(details, collectorMemberFunction) {
const collector = this.getCollector(details.tabId);
if (collector !== null) {
collectorMemberFunction.apply(collector, [details]);
}
},
};
PageNetworkTrafficCollector.prototype.sendLogMessageToTabConsole = function () {
const logMessage = Array.from(arguments).join(' ');
const message = { message: logMessage, event: 'console-log-message' };
browserProxy.tabs.sendMessage(this.tabId, message);
};
PageNetworkTrafficCollector.prototype.sendToTab = function (assetReq, reqs, curPageUrl, adTrackingEvent) {
const msg = {};
msg.assets = [];
msg.requests = [];
msg.event_data = {};
msg.event = adTrackingEvent;
if (adTrackingEvent === 'new-video-ad') {
msg.requests = reqs;
msg.requests.sort((reqA, reqB) => reqA.requestTimestamp - reqB.requestTimestamp);
if (assetReq) {
msg.assets = [assetReq];
}
} else if (adTrackingEvent === 'new-invalid-video-ad') {
msg.requests = reqs.map(request => parseHostnameFromUrl(request.url));
msg.assets = [{
url: parseHostnameFromUrl(assetReq.url),
contentType: assetReq.contentType,
size: assetReq.size,
}];
}
msg.origUrl = curPageUrl;
msg.displayAdFound = this.displayAdFound;
browserProxy.tabs.sendMessage(this.tabId, msg);
};
PageNetworkTrafficCollector.prototype.getRedirKey = function (url, frameId) {
return `${url}:${frameId}`;
};
PageNetworkTrafficCollector.prototype.seenBefore = function (request) {
const oldTime = this.assetsSeen[assetMsgKey(request)];
if (oldTime && (request.requestTimestamp - oldTime < secBetweenDupAssets)) {
return true;
}
return false;
};
PageNetworkTrafficCollector.prototype.recordSeenAsset = function (request) {
this.assetsSeen[assetMsgKey(request)] = request.requestTimestamp;
};
PageNetworkTrafficCollector.prototype.onBeforeRequest = function (details) {
const req = {
url: details.url,
type: details.type,
httpMethod: details.method,
frameId: details.frameId,
parentFrameId: details.parentFrameId,
requestTimestamp: details.timeStamp,
};
this.requests[details.requestId] = req;
};
PageNetworkTrafficCollector.prototype.onSendHeaders = function (details) {
let request,
header;
request = this.requests[details.requestId];
header = request && findHeader(details.requestHeaders, 'x-requested-with');
if (header && header.value.toLowerCase().indexOf('flash') > -1) {
request.from_flash = true;
}
};
PageNetworkTrafficCollector.prototype.onHeadersReceived = function (details) {
const getFrameDetails = {
tabId: details.tabId,
processId: null,
frameId: details.frameId,
};
const pageNetworkTrafficController = this;
getFrame(getFrameDetails, (frameDetails) => {
if (frameDetails && frameDetails.url) {
pageNetworkTrafficController._onHeadersReceived(details, frameDetails);
}
});
};
PageNetworkTrafficCollector.prototype._onHeadersReceived = function (details, frameDetails) {
let contentSize,
contentRange;
const request = this.requests[details.requestId];
if (request) {
const redirParent = this.allRedirects[this.getRedirKey(details.url, details.frameId)];
let header = request && findHeader(details.responseHeaders, 'content-type');
const contentType = header && header.value.toLowerCase();
if (contentType) {
request.contentType = contentType;
}
header = request && findHeader(details.responseHeaders, 'content-length');
contentSize = header && header.value;
if (contentSize) {
request.size = request.size || 0;
request.size += parseInt(contentSize);
}
header = request && findHeader(details.responseHeaders, 'content-range');
contentRange = header && header.value;
if (contentRange) {
request.contentRange = parseInt(contentRange.split('/')[1]);
}
let frameUrl = null;
if (frameDetails && frameDetails.url) {
frameUrl = frameDetails.url;
}
if (!this.bannedRequest(request)
&& (this.isVideoReq(frameUrl, request) || (redirParent && redirParent.isVideo))) {
request.isVideo = true;
}
}
};
PageNetworkTrafficCollector.prototype.onBeforeRedirect = function (details) {
const request = this.requests[details.requestId];
if (request) {
if (request.redirects) {
request.redirects.push(details.redirectUrl);
} else {
request.redirects = [details.redirectUrl];
}
this.allRedirects[this.getRedirKey(details.redirectUrl, details.frameId)] = request;
}
};
PageNetworkTrafficCollector.prototype.isYoutubeMastheadRequest = function (url) {
const re = /video_masthead/;
return this.hasYoutubeDomain(url) && re.test(url);
};
PageNetworkTrafficCollector.prototype.isYoutubeVideoRequest = function (srcUrl, destUrl) {
if (!this.hasYoutubeDomain(srcUrl)) {
return false;
}
const re = /https?:\/\/r.*?\.googlevideo\.com\/videoplayback\?/;
return re.test(destUrl);
};
PageNetworkTrafficCollector.prototype.processResponse = function (requestDetails, frameDetails) {
let request;
if (requestDetails) {
request = this.requests[requestDetails.requestId];
if (request) {
request.responseStatus = requestDetails.statusCode;
request.responseTimestamp = requestDetails.timeStamp;
let frameUrl = null;
if (frameDetails && frameDetails.url) {
frameUrl = frameDetails.url;
}
let requestUrl = null;
if (request.url) {
requestUrl = request.url;
}
if (this.isYoutubeAdReq(frameUrl, requestUrl)) {
const destVideoId = this.parseYoutubeVideoIdFromUrl(requestUrl);
const srcVideoId = this.parseYoutubeVideoIdFromUrl(frameUrl);
if (srcVideoId && destVideoId) {
request.isYoutubeAd = true;
request.isVideo = true;
request.rawSrcUrl = frameUrl;
request.rawDestUrl = requestUrl;
request.url = `https://www.youtube.com/watch?v=${this.parseYoutubeVideoIdFromUrl(requestUrl)}`;
}
} else if (!this.bannedRequest(request)
&& (this.isVideo || this.isVideoReq(frameUrl, request))) {
request.isVideo = true;
}
if (request.isVideo) {
const msgKey = assetMsgKey(request);
this.msgsBeingSent[msgKey] = request;
if (!this.seenBefore(request)) {
this.sendMsgWhenQuiet(msgKey);
}
this.recordSeenAsset(request);
}
}
}
};
PageNetworkTrafficCollector.prototype.onResponseStarted = function (responseDetails) {
if (responseDetails.frameId < 0) {
responseDetails.frameId = 99999;
}
const getFrameDetails = {
tabId: responseDetails.tabId,
processId: null,
frameId: responseDetails.frameId,
};
const pageNetworkTrafficController = this;
getFrame(getFrameDetails, (frameDetails) => {
if (frameDetails && frameDetails.url) {
pageNetworkTrafficController.processResponse(responseDetails, frameDetails);
}
});
};
PageNetworkTrafficCollector.prototype.hasBannedFiletype = function (request) {
const url = stripQueryParams(request.url);
if (bannedFiletypesReg.exec(url)) {
return true;
}
return false;
};
PageNetworkTrafficCollector.prototype.checkContentHeaders = function (request) {
if (request.contentType && validVideoType(request.contentType)) {
return true;
}
return false;
};
PageNetworkTrafficCollector.prototype.checkUrlExtension = function (request) {
const url = stripQueryParams(request.url);
if (extensionsReg.exec(url)) {
return true;
}
return false;
};
PageNetworkTrafficCollector.prototype.isVideoReq = function (srcUrl, request) {
if (this.isYoutubeVideoRequest(srcUrl, request.url)) {
return false;
}
return this.checkUrlExtension(request) || this.checkContentHeaders(request);
};
PageNetworkTrafficCollector.prototype.hasYoutubeDomain = function (url) {
const hostname = parseHostnameFromUrl(url);
if (hostname === 'www.youtube.com') {
return true;
}
return false;
};
PageNetworkTrafficCollector.prototype.parseYoutubeVideoIdFromUrl = function (url) {
let re = /^https?:\/\/www\.youtube\.com\/get_video_info.*(?:\?|&)video_id=(.*?)(?:$|&)/;
let match = re.exec(url);
if (match && match.length > 1) {
return match[1];
}
re = /^https?:\/\/www\.youtube\.com\/embed\/(.*?)(?:$|\?)/;
match = re.exec(url);
if (match && match.length > 1) {
return match[1];
}
re = /^https?:\/\/www\.youtube\.com\/watch.*(\?|&)v=([^&]*)/;
match = re.exec(url);
if (match && match.length > 1) {
return match[1];
}
return null;
};
PageNetworkTrafficCollector.prototype.isYoutubeGetVideoInfoReq = function (url) {
const re = /^https?:\/\/www\.youtube\.com\/get_video_info\?/;
return re.test(url);
};
PageNetworkTrafficCollector.prototype.isYoutubeAdReq = function (srcUrl, destUrl) {
if (!this.hasYoutubeDomain(srcUrl)
|| !this.isYoutubeGetVideoInfoReq(destUrl)) {
return false;
}
if (this.parseYoutubeVideoIdFromUrl(srcUrl)
=== this.parseYoutubeVideoIdFromUrl(destUrl)
&& !this.isYoutubeMastheadRequest(destUrl)) {
return false;
}
return true;
};
PageNetworkTrafficCollector.prototype.bannedRequest = function (request) {
return this.bannedVideoType(request) || this.hasBannedFiletype(request) || this.bannedVideoSize(request);
};
PageNetworkTrafficCollector.prototype.bannedVideoType = function (request) {
let badType = false;
if (request.contentType) {
badType = bannedContentTypes.some(prefix => request.contentType.indexOf(prefix) >= 0);
}
return badType;
};
PageNetworkTrafficCollector.prototype.bannedVideoSize = function (request) {
if (request.size !== null) {
if (request.size < minVidSize || request.size > maxVidSize || request.contentRange > maxContentRange) {
return true;
}
}
return false;
};
PageNetworkTrafficCollector.prototype.grabTagReqs = function (tabRequests, assetRequest) {
let minTimestamp,
maxTimestamp;
minTimestamp = assetRequest.requestTimestamp - secBefore;
maxTimestamp = assetRequest.requestTimestamp + secAfter;
const filteredRequests = tabRequests.filter(request => (request.requestTimestamp > minTimestamp
&& request.requestTimestamp < maxTimestamp
&& request.frameId === assetRequest.frameId
&& request.url !== assetRequest.url
&& (hasValidRequestType(request)
|| isPixelRequest(request))));
return filteredRequests;
};
PageNetworkTrafficCollector.prototype.isValidVideoAd = function (assetRequest, tagRequests) {
const hasVpaidOrVastRequest = tagRequests.some(tagRequest => isVpaidOrVastRequest(tagRequest));
if (assetRequest.isYoutubeAd) {
return true;
}
if (hasVpaidOrVastRequest) {
return true;
}
const hasTopVideoAssetDomain = topVideoAssetDomains.some(assetDomain => hasDomain(assetRequest.url, assetDomain));
return hasTopVideoAssetDomain;
};
PageNetworkTrafficCollector.prototype.sendMsgWhenQuiet = function (msgKey) {
let _this = this,
origPageUrl,
msgAssetReq;
msgAssetReq = this.msgsBeingSent[msgKey];
browserProxy.tabs.get(this.tabId, (tab) => { origPageUrl = tab.url; });
setTimeout(() => {
const rawRequests = [];
if (globalPageContainer.collectors[_this.tabId] === _this) {
for (const reqId in _this.requests) {
rawRequests.push(_this.requests[reqId]);
}
const tagReqs = _this.grabTagReqs(rawRequests, msgAssetReq);
if (_this.isValidVideoAd(msgAssetReq, tagReqs)) {
_this.sendToTab(msgAssetReq, tagReqs, origPageUrl, 'new-video-ad');
} else {
_this.sendToTab(msgAssetReq, tagReqs, origPageUrl, 'new-invalid-video-ad');
}
} else {
}
delete _this.msgsBeingSent[msgKey];
}, secAfter + secBefore);
};
PageNetworkTrafficCollector.prototype.existingMessage = function (candidateRequest) {
const frameMsg = this.msgsBeingSent[candidateRequest.frameId];
if (frameMsg) {
return frameMsg;
}
return null;
};
function onBeforeRequestListener(details) {
globalPageContainer.forwardCall(details, PageNetworkTrafficCollector.prototype.onBeforeRequest);
}
function onSendHeadersListener(details) {
globalPageContainer.forwardCall(details, PageNetworkTrafficCollector.prototype.onSendHeaders);
}
function onHeadersReceivedListener(details) {
globalPageContainer.forwardCall(details, PageNetworkTrafficCollector.prototype.onHeadersReceived);
}
function onBeforeRedirectListener(details) {
globalPageContainer.forwardCall(details, PageNetworkTrafficCollector.prototype.onBeforeRedirect);
}
function onResponseStartedListener(details) {
globalPageContainer.forwardCall(details, PageNetworkTrafficCollector.prototype.onResponseStarted);
}
function onCommittedListener(details) {
if (details.frameId === 0) {
globalPageContainer.onNavigationCommitted(details);
}
}
function onCompletedListener(details) {
if (details.frameId === 0) {
globalPageContainer.onNavigationCompleted(details);
}
}
function onRemovedListener(tabId, closeInfo) {
globalPageContainer.onTabClose(tabId, closeInfo);
}
function onMessageListener(message, sender, sendResponse) {
if (message.event === 'new-ad' && message.data.event === 'ad') {
const tabId = sender.tab.id;
if (tabId) {
globalPageContainer.onDisplayAdFound(tabId);
}
}
}
function registerListeners() {
browserProxy.webRequest.onBeforeRequest.addListener(
onBeforeRequestListener,
{ urls: ['http://*/*', 'https://*/*'] },
[],
);
browserProxy.webRequest.onSendHeaders.addListener(
onSendHeadersListener,
{ urls: ['http://*/*', 'https://*/*'] },
['requestHeaders'],
);
browserProxy.webRequest.onHeadersReceived.addListener(
onHeadersReceivedListener,
{ urls: ['http://*/*', 'https://*/*'] },
['responseHeaders'],
);
browserProxy.webRequest.onBeforeRedirect.addListener(
onBeforeRedirectListener,
{ urls: ['http://*/*', 'https://*/*'] },
[],
);
browserProxy.webRequest.onResponseStarted.addListener(
onResponseStartedListener,
{ urls: ['http://*/*', 'https://*/*'] },
['responseHeaders'],
);
browserProxy.webNavigation.onCommitted.addListener(onCommittedListener);
browserProxy.webNavigation.onCompleted.addListener(onCompletedListener);
browserProxy.tabs.onRemoved.addListener(onRemovedListener);
browserProxy.runtime.onMessage.addListener(onMessageListener);
areListenersRegistered = true;
}
function unregisterListeners() {
browserProxy.webRequest.onBeforeRequest.removeListener(
onBeforeRequestListener,
);
browserProxy.webRequest.onSendHeaders.removeListener(
onSendHeadersListener,
);
browserProxy.webRequest.onHeadersReceived.removeListener(
onHeadersReceivedListener,
);
browserProxy.webRequest.onBeforeRedirect.removeListener(
onBeforeRedirectListener,
);
browserProxy.webRequest.onResponseStarted.removeListener(
onResponseStartedListener,
);
browserProxy.webNavigation.onCommitted.removeListener(onCommittedListener);
browserProxy.webNavigation.onCompleted.removeListener(onCompletedListener);
browserProxy.tabs.onRemoved.removeListener(onRemovedListener);
browserProxy.runtime.onMessage.removeListener(onMessageListener);
areListenersRegistered = false;
}
function areRequiredBrowserApisAvailable() {
return requiredBrowserApis.every(api => typeof api !== 'undefined');
}
if (areRequiredBrowserApisAvailable()) {
ifBrowserValid(
() => {
browserProxy.webNavigation.onBeforeNavigate.addListener(
(details) => {
if (details.frameId === 0) {
globalPageContainer.onNewNavigation(details);
}
},
{
url: [{ urlMatches: 'http://*/*' }, { urlMatches: 'https://*/*' }],
},
);
}, () => {
},
);
}
browserProxy.runtime.onMessage.addListener((request, sender, sendResponse) => {
if (request === 'is_browser_valid') {
ifBrowserValid(
sendResponse({ browser_valid: true }),
sendResponse({ browser_valid: false }),
);
}
});
browserProxy.runtime.onMessage.addListener((request, sender, sendResponse) => {
if (request === 'is_tracking_enabled') {
ifTrackingEnabled(
sender.tab,
() => {
try { sendResponse({ tracking_enabled: true }); } catch (err) {}
},
() => {
try { sendResponse({ tracking_enabled: false }); } catch (err) {}
},
);
}
return true;
});
}());

@ -1,824 +0,0 @@
'use strict';
(function() {
function isChrome() {
return (typeof chrome !== 'undefined' &&
window.navigator.userAgent.match(/Chrom(?:e|ium)\/([0-9\.]+)/));
}
var browserProxy;
if ( isChrome() ) {
browserProxy = chrome;
} else {
browserProxy = browser;
}
var MIN_FF_MAJOR_VERSION = 51;
var requiredBrowserApis = [
browserProxy.webNavigation,
browserProxy.tabs,
browserProxy.webRequest,
browserProxy.runtime
];
var areListenersRegistered = false;
var secBefore = 2000;
var secAfter = 5000;
var secBetweenDupAssets = 10e3;
var minVidSize = 500e3;
var maxVidSize = 25e6;
var maxContentRange = 25e6;
var videoExtensions = [
'af', '3gp', 'asf', 'avchd', 'avi', 'cam', 'dsh', 'flv', 'm1v', 'm2v',
'fla', 'flr', 'sol', 'm4v', 'mkv', 'wrap', 'mng', 'mov', 'mpeg', 'mpg',
'mpe', 'mp4', 'mxf', 'nsv', 'ogg', 'rm', 'svi', 'smi', 'wmv', 'webm'
];
var extensionsReg = new RegExp('\\.' + videoExtensions.join('$|\\.') + '$');
var videoContentTypesPrefixes = ['binary/octet-stream', 'video/', 'flv-application/', 'media'];
var bannedContentTypes = ['video/mp2t','video/f4m','video/f4f'];
var bannedFiletypes = ['ts'];
var bannedFiletypesReg = new RegExp('\\.' + bannedFiletypes.join('$|\\.') + '$');
var whitelistReqTypes = ['object', 'xmlhttprequest', 'other'];
var topVideoAssetDomains = [
'2mdn.net',
'adap.tv',
'adnxs.com',
'adsrvr.org',
'btrll.com',
'celtra.com',
'flashtalking.com',
'flite.com',
'innovid.com',
'jivox.com',
'mixpo.com',
'nytimes.com',
'playwire.com',
'selectmedia.asia',
'serving-sys.com',
'solvemedia.com',
'spotible.com',
'teads.tv',
'tribalfusion.com',
'tubemogul.com',
'videologygroup.com',
'washingtonpost.com'
];
var robotsTxtAllows = wappalyzer.robotsTxtAllows.bind(wappalyzer);
if ( !String.prototype.endsWith ) {
String.prototype.endsWith = function(searchString, position) {
var subjectString = this.toString();
if ( typeof position !== 'number' || !isFinite(position) ||
Math.floor(position) !== position || position > subjectString.length) {
position = subjectString.length;
}
position -= searchString.length;
var lastIndex = subjectString.indexOf(searchString, position);
return lastIndex !== -1 && lastIndex === position;
};
}
function getFrame(getFrameDetails, callback) {
if ( typeof chrome !== 'undefined' ) {
chrome.webNavigation.getFrame(getFrameDetails, callback);
} else if ( typeof browser !== 'undefined' ) {
var gettingFrame = browser.webNavigation.getFrame(getFrameDetails);
gettingFrame.then(callback);
}
}
function ifBrowserValid(callback, elseCallback) {
if ( isChrome() ) {
callback();
} else if ( typeof browser !== 'undefined' ) {
try {
var gettingInfo = browser.runtime.getBrowserInfo();
gettingInfo.then(function(browserInfo) {
var browserVersion = parseInt(browserInfo.version.split('.')[0]);
if ( browserInfo.name === 'Firefox' &&
browserVersion >= MIN_FF_MAJOR_VERSION) {
callback();
} else {
elseCallback();
}
});
} catch (err) {
elseCallback();
}
} else {
elseCallback();
}
}
function ifTrackingEnabled(details, ifCallback, elseCallback) {
var fullIfCallback = function() {
allowedByRobotsTxt(details, ifCallback, elseCallback);
};
browser.storage.local.get('tracking').then(function(item) {
if ( item.hasOwnProperty('tracking') ) {
if ( item.tracking ) {
fullIfCallback();
} else {
elseCallback();
}
} else {
fullIfCallback();
}
});
}
function allowedByRobotsTxt(details, ifCallback, elseCallback) {
if ( details.url && !details.url.startsWith('chrome://') ) {
robotsTxtAllows(details.url).then(ifCallback, elseCallback);
} else {
elseCallback();
}
}
function isPixelRequest(request) {
return (request.type === 'image' || request.responseStatus === 204) &&
request.size <= 1000;
}
function isVpaidOrVastRequest(request) {
var lowerCaseUrl = request.url.toLowerCase();
return lowerCaseUrl.indexOf('vpaid') !== -1 || lowerCaseUrl.indexOf('vast') !== -1;
}
function hasValidRequestType(request) {
return whitelistReqTypes.indexOf(request.type) >= 0;
}
function stripQueryParams(url) {
return url.split('?', 1)[0];
}
function parseHostnameFromUrl(url) {
var parser = document.createElement('a');
parser.href = url;
return parser.hostname;
}
function hasDomain(url, domain) {
return parseHostnameFromUrl(url).endsWith(domain);
}
function findHeader(headers, key) {
var header;
for ( var i = 0; i < headers.length; i += 1 ) {
header = headers[i];
if ( header.name.toLowerCase() === key ) {
return header;
}
}
return null;
}
function validVideoType(vtype) {
var goodType = videoContentTypesPrefixes.some(function(prefix) {
return vtype.indexOf(prefix) === 0;
});
return goodType;
}
function assetMsgKey(assetReq) {
var url = stripQueryParams(assetReq.url);
var key = assetReq.frameId + '-' + url;
return key;
}
var PageNetworkTrafficCollector = function(tabId) {
this.tabId = tabId;
this.displayAdFound = false;
this.requests = {};
this.msgsBeingSent = {};
this.assetsSeen = {};
this.allRedirects = {};
};
var globalPageContainer = {
collectors: {},
dyingCollectors: {},
cleanupCollector: function(tabId) {
if ( tabId in this.collectors ) {
delete globalPageContainer.collectors[tabId];
}
},
onNewNavigation: function(details) {
var tabId = details.tabId;
this.cleanupCollector(tabId);
ifTrackingEnabled(
details,
function() {
if ( !areListenersRegistered ) {
registerListeners();
}
this.collectors[tabId] = new PageNetworkTrafficCollector(tabId);
}.bind(this),
function() {
if ( areListenersRegistered ) {
unregisterListeners();
}
}
);
},
onNavigationCommitted: function(details) {
},
onNavigationCompleted: function(details) {
},
onTabClose: function(tabId, closeInfo) {
this.cleanupCollector(tabId);
delete this.collectors[tabId];
},
onDisplayAdFound: function(tabId) {
this.collectors[tabId].displayAdFound = true;
},
getRandId: function() {
return String(Math.floor(Math.random() * 1e9));
},
getCollector: function(tabId) {
if ( this.collectors.hasOwnProperty(tabId) ) {
return this.collectors[tabId];
}
return null;
},
forwardCall: function(details, collectorMemberFunction) {
var collector = this.getCollector(details.tabId);
if ( collector !== null ) {
collectorMemberFunction.apply(collector, [details]);
}
}
};
PageNetworkTrafficCollector.prototype.sendLogMessageToTabConsole = function() {
var logMessage = Array.from(arguments).join(' ');
var message = {message: logMessage, event: 'console-log-message'};
browserProxy.tabs.sendMessage(this.tabId, message);
};
PageNetworkTrafficCollector.prototype.sendToTab = function(assetReq, reqs, curPageUrl, adTrackingEvent) {
var msg = {};
msg.assets = [];
msg.requests = [];
msg.event_data = {};
msg.event = adTrackingEvent;
if ( adTrackingEvent === 'new-video-ad' ) {
msg.requests = reqs;
msg.requests.sort(function(reqA, reqB) {return reqA.requestTimestamp - reqB.requestTimestamp;});
if ( assetReq ) {
msg.assets = [assetReq];
}
} else if ( adTrackingEvent === 'new-invalid-video-ad' ) {
msg.requests = reqs.map(function(request) {
return parseHostnameFromUrl(request.url);
});
msg.assets = [{
url: parseHostnameFromUrl(assetReq.url),
contentType: assetReq.contentType,
size: assetReq.size
}];
}
msg.origUrl = curPageUrl;
msg.displayAdFound = this.displayAdFound;
browserProxy.tabs.sendMessage(this.tabId, msg);
};
PageNetworkTrafficCollector.prototype.getRedirKey = function(url, frameId) {
return url + ':' + frameId;
};
PageNetworkTrafficCollector.prototype.seenBefore = function(request) {
var oldTime = this.assetsSeen[assetMsgKey(request)];
if ( oldTime && (request.requestTimestamp-oldTime < secBetweenDupAssets)){
return true;
}
return false;
};
PageNetworkTrafficCollector.prototype.recordSeenAsset = function(request) {
this.assetsSeen[assetMsgKey(request)] = request.requestTimestamp;
};
PageNetworkTrafficCollector.prototype.onBeforeRequest = function(details) {
var req = {
url: details.url,
type: details.type,
httpMethod: details.method,
frameId: details.frameId,
parentFrameId: details.parentFrameId,
requestTimestamp: details.timeStamp,
};
this.requests[details.requestId] = req;
};
PageNetworkTrafficCollector.prototype.onSendHeaders = function(details) {
var request, header;
request = this.requests[details.requestId];
header = request && findHeader(details.requestHeaders, 'x-requested-with');
if ( header && header.value.toLowerCase().indexOf('flash') > -1 ) {
request.from_flash = true;
}
};
PageNetworkTrafficCollector.prototype.onHeadersReceived = function(details) {
var getFrameDetails = {
tabId: details.tabId,
processId: null,
frameId: details.frameId
};
var pageNetworkTrafficController = this;
getFrame(getFrameDetails, function(frameDetails) {
if ( frameDetails && frameDetails.url ) {
pageNetworkTrafficController._onHeadersReceived(details, frameDetails);
}
});
};
PageNetworkTrafficCollector.prototype._onHeadersReceived = function(details, frameDetails) {
var contentSize, contentRange;
var request = this.requests[details.requestId];
if ( request ) {
var redirParent = this.allRedirects[this.getRedirKey(details.url, details.frameId)];
var header = request && findHeader(details.responseHeaders, 'content-type');
var contentType = header && header.value.toLowerCase();
if ( contentType){
request.contentType = contentType;
}
header = request && findHeader(details.responseHeaders, 'content-length');
contentSize = header && header.value;
if ( contentSize ) {
request.size = request.size || 0;
request.size += parseInt(contentSize);
}
header = request && findHeader(details.responseHeaders, 'content-range');
contentRange = header && header.value;
if ( contentRange ) {
request.contentRange = parseInt(contentRange.split('/')[1]);
}
var frameUrl = null;
if ( frameDetails && frameDetails.url ) {
frameUrl = frameDetails.url;
}
if ( !this.bannedRequest(request) &&
(this.isVideoReq(frameUrl, request) || (redirParent && redirParent.isVideo))) {
request.isVideo = true;
}
}
};
PageNetworkTrafficCollector.prototype.onBeforeRedirect = function(details) {
var request = this.requests[details.requestId];
if ( request ) {
if ( request.redirects ) {
request.redirects.push(details.redirectUrl);
} else {
request.redirects = [details.redirectUrl];
}
this.allRedirects[this.getRedirKey(details.redirectUrl, details.frameId)] = request;
}
};
PageNetworkTrafficCollector.prototype.isYoutubeMastheadRequest = function(url) {
var re = /video_masthead/;
return this.hasYoutubeDomain(url) && re.test(url);
};
PageNetworkTrafficCollector.prototype.isYoutubeVideoRequest = function(srcUrl, destUrl) {
if ( !this.hasYoutubeDomain(srcUrl) ) {
return false;
}
var re = /https?:\/\/r.*?\.googlevideo\.com\/videoplayback\?/;
return re.test(destUrl);
};
PageNetworkTrafficCollector.prototype.processResponse = function(requestDetails, frameDetails) {
var request;
if ( requestDetails ) {
request = this.requests[requestDetails.requestId];
if ( request ) {
request.responseStatus = requestDetails.statusCode;
request.responseTimestamp = requestDetails.timeStamp;
var frameUrl = null;
if ( frameDetails && frameDetails.url ) {
frameUrl = frameDetails.url;
}
var requestUrl = null;
if ( request.url ) {
requestUrl = request.url;
}
if ( this.isYoutubeAdReq(frameUrl, requestUrl) ) {
var destVideoId = this.parseYoutubeVideoIdFromUrl(requestUrl);
var srcVideoId = this.parseYoutubeVideoIdFromUrl(frameUrl);
if ( srcVideoId && destVideoId ) {
request.isYoutubeAd = true;
request.isVideo = true;
request.rawSrcUrl = frameUrl;
request.rawDestUrl = requestUrl;
request.url = 'https://www.youtube.com/watch?v=' + this.parseYoutubeVideoIdFromUrl(requestUrl);
}
} else if ( !this.bannedRequest(request) &&
(this.isVideo || this.isVideoReq(frameUrl, request))) {
request.isVideo = true;
}
if ( request.isVideo ) {
var msgKey = assetMsgKey(request);
this.msgsBeingSent[msgKey] = request;
if ( !this.seenBefore(request) ) {
this.sendMsgWhenQuiet(msgKey);
}
this.recordSeenAsset(request);
}
}
}
};
PageNetworkTrafficCollector.prototype.onResponseStarted = function(responseDetails) {
if ( responseDetails.frameId < 0 ) {
responseDetails.frameId = 99999;
}
var getFrameDetails = {
tabId: responseDetails.tabId,
processId: null,
frameId: responseDetails.frameId
};
var pageNetworkTrafficController = this;
getFrame(getFrameDetails, function(frameDetails) {
if ( frameDetails && frameDetails.url ) {
pageNetworkTrafficController.processResponse(responseDetails, frameDetails);
}
});
};
PageNetworkTrafficCollector.prototype.hasBannedFiletype = function(request) {
var url = stripQueryParams(request.url);
if ( bannedFiletypesReg.exec(url) ) {
return true;
} else {
return false;
}
};
PageNetworkTrafficCollector.prototype.checkContentHeaders = function(request) {
if ( request.contentType && validVideoType(request.contentType) ) {
return true;
}
return false;
};
PageNetworkTrafficCollector.prototype.checkUrlExtension = function(request) {
var url = stripQueryParams(request.url);
if ( extensionsReg.exec(url) ) {
return true;
} else {
return false;
}
};
PageNetworkTrafficCollector.prototype.isVideoReq = function(srcUrl, request) {
if ( this.isYoutubeVideoRequest(srcUrl, request.url) ) {
return false;
}
return this.checkUrlExtension(request) || this.checkContentHeaders(request);
};
PageNetworkTrafficCollector.prototype.hasYoutubeDomain = function(url) {
var hostname = parseHostnameFromUrl(url) ;
if ( hostname === 'www.youtube.com' ) {
return true;
}
return false;
};
PageNetworkTrafficCollector.prototype.parseYoutubeVideoIdFromUrl = function(url) {
var re = /^https?:\/\/www\.youtube\.com\/get_video_info.*(?:\?|&)video_id=(.*?)(?:$|&)/;
var match = re.exec(url);
if ( match && match.length > 1 ) {
return match[1];
}
re = /^https?:\/\/www\.youtube\.com\/embed\/(.*?)(?:$|\?)/;
match = re.exec(url);
if ( match && match.length > 1 ) {
return match[1];
}
re = /^https?:\/\/www\.youtube\.com\/watch.*(\?|&)v=([^&]*)/;
match = re.exec(url);
if ( match && match.length > 1 ) {
return match[1];
}
return null;
};
PageNetworkTrafficCollector.prototype.isYoutubeGetVideoInfoReq = function(url) {
var re = /^https?:\/\/www\.youtube\.com\/get_video_info\?/;
return re.test(url);
};
PageNetworkTrafficCollector.prototype.isYoutubeAdReq = function(srcUrl, destUrl) {
if ( !this.hasYoutubeDomain(srcUrl) ||
!this.isYoutubeGetVideoInfoReq(destUrl)) {
return false;
}
if ( this.parseYoutubeVideoIdFromUrl(srcUrl) ===
this.parseYoutubeVideoIdFromUrl(destUrl) &&
!this.isYoutubeMastheadRequest(destUrl)) {
return false;
}
return true;
};
PageNetworkTrafficCollector.prototype.bannedRequest = function(request) {
return this.bannedVideoType(request) || this.hasBannedFiletype(request) || this.bannedVideoSize(request);
};
PageNetworkTrafficCollector.prototype.bannedVideoType = function(request) {
var badType = false;
if ( request.contentType ) {
badType = bannedContentTypes.some(function(prefix) {
return request.contentType.indexOf(prefix) >= 0;
});
}
return badType;
};
PageNetworkTrafficCollector.prototype.bannedVideoSize = function(request) {
if ( request.size !== null ) {
if ( request.size < minVidSize || request.size > maxVidSize || request.contentRange > maxContentRange ) {
return true;
}
}
return false;
};
PageNetworkTrafficCollector.prototype.grabTagReqs = function(tabRequests, assetRequest) {
var minTimestamp, maxTimestamp;
minTimestamp = assetRequest.requestTimestamp - secBefore;
maxTimestamp = assetRequest.requestTimestamp + secAfter;
var filteredRequests = tabRequests.filter(function(request) {
return (request.requestTimestamp > minTimestamp &&
request.requestTimestamp < maxTimestamp &&
request.frameId === assetRequest.frameId &&
request.url !== assetRequest.url &&
(hasValidRequestType(request) ||
isPixelRequest(request)));
});
return filteredRequests;
};
PageNetworkTrafficCollector.prototype.isValidVideoAd = function(assetRequest, tagRequests) {
var hasVpaidOrVastRequest = tagRequests.some(function(tagRequest) {
return isVpaidOrVastRequest(tagRequest);
});
if ( assetRequest.isYoutubeAd ) {
return true;
}
if ( hasVpaidOrVastRequest ) {
return true;
}
var hasTopVideoAssetDomain = topVideoAssetDomains.some(function(assetDomain) {
return hasDomain(assetRequest.url, assetDomain);
});
return hasTopVideoAssetDomain;
};
PageNetworkTrafficCollector.prototype.sendMsgWhenQuiet = function(msgKey) {
var _this = this,
origPageUrl, msgAssetReq;
msgAssetReq = this.msgsBeingSent[msgKey];
browserProxy.tabs.get(this.tabId, function(tab) {origPageUrl = tab.url;});
setTimeout(function() {
var rawRequests = [];
if ( globalPageContainer.collectors[_this.tabId] === _this ) {
for ( var reqId in _this.requests ) {
rawRequests.push(_this.requests[reqId]);
}
var tagReqs = _this.grabTagReqs(rawRequests, msgAssetReq);
if ( _this.isValidVideoAd(msgAssetReq, tagReqs) ) {
_this.sendToTab(msgAssetReq, tagReqs, origPageUrl, 'new-video-ad');
} else {
_this.sendToTab(msgAssetReq, tagReqs, origPageUrl, 'new-invalid-video-ad');
}
} else {
}
delete _this.msgsBeingSent[msgKey];
}, secAfter+secBefore);
};
PageNetworkTrafficCollector.prototype.existingMessage = function(candidateRequest) {
var frameMsg = this.msgsBeingSent[candidateRequest.frameId];
if ( frameMsg ) {
return frameMsg;
} else {
return null;
}
};
function onBeforeRequestListener(details) {
globalPageContainer.forwardCall(details, PageNetworkTrafficCollector.prototype.onBeforeRequest);
}
function onSendHeadersListener(details) {
globalPageContainer.forwardCall(details, PageNetworkTrafficCollector.prototype.onSendHeaders);
}
function onHeadersReceivedListener(details) {
globalPageContainer.forwardCall(details, PageNetworkTrafficCollector.prototype.onHeadersReceived);
}
function onBeforeRedirectListener(details) {
globalPageContainer.forwardCall(details, PageNetworkTrafficCollector.prototype.onBeforeRedirect);
}
function onResponseStartedListener(details) {
globalPageContainer.forwardCall(details, PageNetworkTrafficCollector.prototype.onResponseStarted);
}
function onCommittedListener(details) {
if ( details.frameId === 0 ) {
globalPageContainer.onNavigationCommitted(details);
}
}
function onCompletedListener(details) {
if ( details.frameId === 0 ) {
globalPageContainer.onNavigationCompleted(details);
}
}
function onRemovedListener(tabId, closeInfo) {
globalPageContainer.onTabClose(tabId, closeInfo);
}
function onMessageListener(message, sender, sendResponse) {
if ( message.event === 'new-ad' && message.data.event === 'ad' ) {
var tabId = sender.tab.id;
if ( tabId ) {
globalPageContainer.onDisplayAdFound(tabId);
}
}
}
function registerListeners() {
browserProxy.webRequest.onBeforeRequest.addListener(
onBeforeRequestListener,
{urls: ['http://*/*', 'https://*/*']},
[]
);
browserProxy.webRequest.onSendHeaders.addListener(
onSendHeadersListener,
{urls: ['http://*/*', 'https://*/*']},
['requestHeaders']
);
browserProxy.webRequest.onHeadersReceived.addListener(
onHeadersReceivedListener,
{urls: ['http://*/*', 'https://*/*']},
['responseHeaders']
);
browserProxy.webRequest.onBeforeRedirect.addListener(
onBeforeRedirectListener,
{urls: ['http://*/*', 'https://*/*']},
[]
);
browserProxy.webRequest.onResponseStarted.addListener(
onResponseStartedListener,
{urls: ['http://*/*', 'https://*/*']},
['responseHeaders']
);
browserProxy.webNavigation.onCommitted.addListener(onCommittedListener);
browserProxy.webNavigation.onCompleted.addListener(onCompletedListener);
browserProxy.tabs.onRemoved.addListener(onRemovedListener);
browserProxy.runtime.onMessage.addListener(onMessageListener);
areListenersRegistered = true;
}
function unregisterListeners() {
browserProxy.webRequest.onBeforeRequest.removeListener(
onBeforeRequestListener
);
browserProxy.webRequest.onSendHeaders.removeListener(
onSendHeadersListener
);
browserProxy.webRequest.onHeadersReceived.removeListener(
onHeadersReceivedListener
);
browserProxy.webRequest.onBeforeRedirect.removeListener(
onBeforeRedirectListener
);
browserProxy.webRequest.onResponseStarted.removeListener(
onResponseStartedListener
);
browserProxy.webNavigation.onCommitted.removeListener(onCommittedListener);
browserProxy.webNavigation.onCompleted.removeListener(onCompletedListener);
browserProxy.tabs.onRemoved.removeListener(onRemovedListener);
browserProxy.runtime.onMessage.removeListener(onMessageListener);
areListenersRegistered = false;
}
function areRequiredBrowserApisAvailable() {
return requiredBrowserApis.every(function(api) {
return typeof api !== 'undefined';
});
}
if ( areRequiredBrowserApisAvailable() ) {
ifBrowserValid(
function() {
browserProxy.webNavigation.onBeforeNavigate.addListener(
function(details) {
if ( details.frameId === 0 ) {
globalPageContainer.onNewNavigation(details);
}
},
{
url: [{urlMatches: 'http://*/*'}, {urlMatches: 'https://*/*'}]
}
);
}, function() {
}
);
}
browserProxy.runtime.onMessage.addListener(function(request, sender, sendResponse) {
if ( request === 'is_browser_valid' ) {
ifBrowserValid(
sendResponse({'browser_valid': true}),
sendResponse({'browser_valid': false})
);
}
});
browserProxy.runtime.onMessage.addListener(function(request, sender, sendResponse) {
if ( request === 'is_tracking_enabled' ) {
ifTrackingEnabled(
sender.tab,
function() {
try {sendResponse({'tracking_enabled': true});}
catch(err) {} },
function() {
try {sendResponse({'tracking_enabled': false});}
catch(err) {}}
);
}
return true;
});
})();

@ -5,23 +5,23 @@ const wappalyzer = new Wappalyzer();
function getOption(name, defaultValue, callback) {
browser.storage.local.get(name)
.then(item => {
.then((item) => {
callback(item.hasOwnProperty(name) ? item[name] : defaultValue);
});
}
function setOption(name, value) {
( chrome || browser ).runtime.sendMessage({
(chrome || browser).runtime.sendMessage({
id: 'set_option',
key: name,
value: value
value,
});
}
document.addEventListener('DOMContentLoaded', () => {
var nodes = document.querySelectorAll('[data-i18n]');
const nodes = document.querySelectorAll('[data-i18n]');
Array.prototype.forEach.call(nodes, node => {
Array.prototype.forEach.call(nodes, (node) => {
node.childNodes[0].nodeValue = browser.i18n.getMessage(node.dataset.i18n);
});
@ -37,7 +37,7 @@ document.addEventListener('DOMContentLoaded', () => {
open(wappalyzer.config.websiteURL);
});
getOption('upgradeMessage', true, value => {
getOption('upgradeMessage', true, (value) => {
const el = document.querySelector('#option-upgrade-message');
el.checked = value;
@ -47,7 +47,7 @@ document.addEventListener('DOMContentLoaded', () => {
});
});
getOption('dynamicIcon', true, value => {
getOption('dynamicIcon', true, (value) => {
const el = document.querySelector('#option-dynamic-icon');
el.checked = value;
@ -57,7 +57,7 @@ document.addEventListener('DOMContentLoaded', () => {
});
});
getOption('tracking', true, value => {
getOption('tracking', true, (value) => {
const el = document.querySelector('#option-tracking');
el.checked = value;

@ -1,14 +1,14 @@
/** global: chrome */
/** global: browser */
var pinnedCategory = null;
let pinnedCategory = null;
var func = tabs => {
( chrome || browser ).runtime.sendMessage({
const func = (tabs) => {
(chrome || browser).runtime.sendMessage({
id: 'get_apps',
tab: tabs[0],
source: 'popup.js'
}, response => {
source: 'popup.js',
}, (response) => {
pinnedCategory = response.pinnedCategory;
replaceDomWhenReady(appsToDomTemplate(response));
@ -20,7 +20,7 @@ browser.tabs.query({ active: true, currentWindow: true })
.catch(console.error);
function replaceDomWhenReady(dom) {
if ( /complete|interactive|loaded/.test(document.readyState) ) {
if (/complete|interactive|loaded/.test(document.readyState)) {
replaceDom(dom);
} else {
document.addEventListener('DOMContentLoaded', () => {
@ -30,32 +30,32 @@ function replaceDomWhenReady(dom) {
}
function replaceDom(domTemplate) {
var container = document.getElementsByClassName('container')[0];
const container = document.getElementsByClassName('container')[0];
while ( container.firstChild ) {
while (container.firstChild) {
container.removeChild(container.firstChild);
}
container.appendChild(jsonToDOM(domTemplate, document, {}));
var nodes = document.querySelectorAll('[data-i18n]');
const nodes = document.querySelectorAll('[data-i18n]');
Array.prototype.forEach.call(nodes, node => {
Array.prototype.forEach.call(nodes, (node) => {
node.childNodes[0].nodeValue = browser.i18n.getMessage(node.dataset.i18n);
});
Array.from(document.querySelectorAll('.detected__category-pin-wrapper')).forEach(pin => {
Array.from(document.querySelectorAll('.detected__category-pin-wrapper')).forEach((pin) => {
pin.addEventListener('click', () => {
const categoryId = parseInt(pin.dataset.categoryId, 10);
if ( categoryId === pinnedCategory ) {
if (categoryId === pinnedCategory) {
pin.className = 'detected__category-pin-wrapper';
pinnedCategory = null;
} else {
const active = document.querySelector('.detected__category-pin-wrapper--active');
if ( active ) {
if (active) {
active.className = 'detected__category-pin-wrapper';
}
@ -64,7 +64,7 @@ function replaceDom(domTemplate) {
pinnedCategory = categoryId;
}
( chrome || browser ).runtime.sendMessage({
(chrome || browser).runtime.sendMessage({
id: 'set_option',
key: 'pinnedCategory',
value: pinnedCategory,
@ -74,115 +74,115 @@ function replaceDom(domTemplate) {
}
function appsToDomTemplate(response) {
let template = [];
let template = [];
if ( response.tabCache && Object.keys(response.tabCache.detected).length > 0 ) {
if (response.tabCache && Object.keys(response.tabCache.detected).length > 0) {
const categories = {};
// Group apps by category
for ( let appName in response.tabCache.detected ) {
response.apps[appName].cats.forEach(cat => {
for (const appName in response.tabCache.detected) {
response.apps[appName].cats.forEach((cat) => {
categories[cat] = categories[cat] || { apps: [] };
categories[cat].apps[appName] = appName;
});
}
for ( let cat in categories ) {
for (const cat in categories) {
const apps = [];
for ( let appName in categories[cat].apps ) {
let confidence = response.tabCache.detected[appName].confidenceTotal;
let version = response.tabCache.detected[appName].version;
for (const appName in categories[cat].apps) {
const confidence = response.tabCache.detected[appName].confidenceTotal;
const version = response.tabCache.detected[appName].version;
apps.push(
[
'a', {
class: 'detected__app',
target: '_blank',
href: 'https://www.wappalyzer.com/technologies/' + slugify(appName)
href: `https://www.wappalyzer.com/technologies/${slugify(appName)}`,
}, [
'img', {
class: 'detected__app-icon',
src: '../images/icons/' + (response.apps[appName].icon || 'default.svg')
src: `../images/icons/${response.apps[appName].icon || 'default.svg'}`,
},
], [
'span', {
class: 'detected__app-name'
class: 'detected__app-name',
},
appName,
], version ? [
'span', {
class: 'detected__app-version'
class: 'detected__app-version',
},
version
version,
] : null, confidence < 100 ? [
'span', {
class: 'detected__app-confidence'
class: 'detected__app-confidence',
},
confidence + '% sure'
] : null
]
`${confidence}% sure`,
] : null,
],
);
}
template.push(
[
'div', {
class: 'detected__category'
class: 'detected__category',
}, [
'div', {
class: 'detected__category-name'
class: 'detected__category-name',
}, [
'a', {
class: 'detected__category-link',
target: '_blank',
href: 'https://www.wappalyzer.com/categories/' + slugify(response.categories[cat].name)
href: `https://www.wappalyzer.com/categories/${slugify(response.categories[cat].name)}`,
},
browser.i18n.getMessage('categoryName' + cat),
browser.i18n.getMessage(`categoryName${cat}`),
], [
'span', {
class: 'detected__category-pin-wrapper' + ( pinnedCategory == cat ? ' detected__category-pin-wrapper--active' : '' ),
class: `detected__category-pin-wrapper${pinnedCategory == cat ? ' detected__category-pin-wrapper--active' : ''}`,
'data-category-id': cat,
'title': browser.i18n.getMessage('categoryPin'),
title: browser.i18n.getMessage('categoryPin'),
}, [
'img', {
class: 'detected__category-pin detected__category-pin--active',
src: '../images/pin-active.svg'
src: '../images/pin-active.svg',
},
], [
'img', {
class: 'detected__category-pin detected__category-pin--inactive',
src: '../images/pin.svg'
}
]
]
src: '../images/pin.svg',
},
],
],
], [
'div', {
class: 'detected__apps'
class: 'detected__apps',
},
apps
]
]
apps,
],
],
);
}
template = [
'div', {
class: 'detected'
class: 'detected',
},
template
template,
];
} else {
template = [
'div', {
class: 'empty'
class: 'empty',
},
[
'span', {
class: 'empty__text'
class: 'empty__text',
},
browser.i18n.getMessage('noAppsDetected')
browser.i18n.getMessage('noAppsDetected'),
],
];
}

@ -51,7 +51,7 @@
"https://www.alphaecommerce.gr/*"
],
"js": [
"js/iframe.js"
"lib/js/iframe.js"
],
"run_at": "document_start",
"all_frames": true

@ -6,13 +6,116 @@
* License: GPLv3 http://www.gnu.org/licenses/gpl-3.0.txt
*/
'use strict';
const validation = {
hostname: /(www.)?((.+?)\.(([a-z]{2,3}\.)?[a-z]{2,6}))$/,
hostnameBlacklist: /((local|dev(elopment)?|stag(e|ing)?|test(ing)?|demo(shop)?|admin|google|cache)\.|\/admin|\.local)/
hostnameBlacklist: /((local|dev(elopment)?|stag(e|ing)?|test(ing)?|demo(shop)?|admin|google|cache)\.|\/admin|\.local)/,
};
/**
* Enclose string in array
*/
function asArray(value) {
return value instanceof Array ? value : [value];
}
/**
*
*/
function asyncForEach(iterable, iterator) {
return Promise.all((iterable || [])
.map(item => new Promise(resolve => setTimeout(() => resolve(iterator(item)), 1))));
}
/**
* Mark application as detected, set confidence and version
*/
function addDetected(app, pattern, type, value, key) {
app.detected = true;
// Set confidence level
app.confidence[`${type} ${key ? `${key} ` : ''}${pattern.regex}`] = pattern.confidence === undefined ? 100 : parseInt(pattern.confidence, 10);
// Detect version number
if (pattern.version) {
const versions = [];
const matches = pattern.regex.exec(value);
let { version } = pattern;
if (matches) {
matches.forEach((match, i) => {
// Parse ternary operator
const ternary = new RegExp(`\\\\${i}\\?([^:]+):(.*)$`).exec(version);
if (ternary && ternary.length === 3) {
version = version.replace(ternary[0], match ? ternary[1] : ternary[2]);
}
// Replace back references
version = version.trim().replace(new RegExp(`\\\\${i}`, 'g'), match || '');
});
if (version && versions.indexOf(version) === -1) {
versions.push(version);
}
if (versions.length) {
// Use the longest detected version number
app.version = versions.reduce((a, b) => (a.length > b.length ? a : b));
}
}
}
}
function resolveExcludes(apps, detected) {
const excludes = [];
// Exclude app in detected apps only
Object.keys(Object.assign({}, apps, detected)).forEach((appName) => {
const app = apps[appName];
if (app.props.excludes) {
asArray(app.props.excludes).forEach((excluded) => {
excludes.push(excluded);
});
}
});
// Remove excluded applications
Object.keys(apps).forEach((appName) => {
if (excludes.indexOf(appName) > -1) {
delete apps[appName];
}
});
}
class Application {
constructor(name, props, detected) {
this.confidence = {};
this.confidenceTotal = 0;
this.detected = Boolean(detected);
this.excludes = [];
this.name = name;
this.props = props;
this.version = '';
}
/**
* Calculate confidence total
*/
getConfidence() {
let total = 0;
Object.values(this.confidence).forEach((id) => {
total += this.confidence[id];
});
this.confidenceTotal = Math.min(total, 100);
return this.confidenceTotal;
}
}
class Wappalyzer {
constructor() {
this.apps = {};
@ -37,27 +140,29 @@ class Wappalyzer {
this.driver.log(message, source || '', type || 'debug');
}
asyncForEach(iterable, iterator) {
return Promise.all(( iterable || [] ).map(item => new Promise(resolve => setTimeout(() => resolve(iterator(item)), 1))));
}
analyze(url, data, context) {
const startTime = new Date();
const apps = {};
const promises = [];
const startTime = new Date();
const {
scripts,
cookies,
headers,
js,
} = data;
var apps = {};
let { html } = data;
if ( this.detected[url.canonical] === undefined ) {
if (this.detected[url.canonical] === undefined) {
this.detected[url.canonical] = {};
}
// Additional information
var language = null;
let language = null;
if ( data.html ) {
if ( typeof data.html !== 'string' ) {
data.html = '';
if (html) {
if (typeof html !== 'string') {
html = '';
}
const matches = data.html.match(/<html[^>]*[: ]lang="([a-z]{2}((-|_)[A-Z]{2})?)"/i);
@ -65,60 +170,62 @@ class Wappalyzer {
language = matches && matches.length ? matches[1] : null;
}
Object.keys(this.apps).forEach(appName => {
apps[appName] = this.detected[url.canonical] && this.detected[url.canonical][appName] ? this.detected[url.canonical][appName] : new Application(appName, this.apps[appName]);
Object.keys(this.apps).forEach((appName) => {
apps[appName] = this.detected[url.canonical] && this.detected[url.canonical][appName]
? this.detected[url.canonical][appName]
: new Application(appName, this.apps[appName]);
var app = apps[appName];
const app = apps[appName];
this.analyzeUrl(app, url);
if ( data.html ) {
promises.push(this.analyzeHtml(app, data.html));
promises.push(this.analyzeMeta(app, data.html));
if (html) {
promises.push(this.analyzeHtml(app, html));
promises.push(this.analyzeMeta(app, html));
}
if ( data.scripts ) {
promises.push(this.analyzeScripts(app, data.scripts));
if (scripts) {
promises.push(this.analyzeScripts(app, scripts));
}
if ( data.cookies ) {
promises.push(this.analyzeCookies(app, data.cookies));
if (cookies) {
promises.push(this.analyzeCookies(app, cookies));
}
if ( data.headers ) {
promises.push(this.analyzeHeaders(app, data.headers));
if (headers) {
promises.push(this.analyzeHeaders(app, headers));
}
});
if ( data.js ) {
Object.keys(data.js).forEach(appName => {
if (typeof data.js[appName] != 'function') {
promises.push(this.analyzeJs(apps[appName], data.js[appName]));
if (js) {
Object.keys(js).forEach((appName) => {
if (typeof js[appName] !== 'function') {
promises.push(this.analyzeJs(apps[appName], js[appName]));
}
});
}
return new Promise(resolve => {
return new Promise((resolve) => {
Promise.all(promises)
.then(() => {
Object.keys(apps).forEach(appName => {
let app = apps[appName];
Object.keys(apps).forEach((appName) => {
const app = apps[appName];
if (!app.detected || !app.getConfidence()) {
delete apps[app.name];
}
});
this.resolveExcludes(apps, this.detected[url]);
resolveExcludes(apps, this.detected[url]);
this.resolveImplies(apps, url.canonical);
this.cacheDetectedApps(apps, url.canonical);
this.trackDetectedApps(apps, url, language);
this.log('Processing ' + Object.keys(data).join(', ') + ' took ' + (( new Date() - startTime ) / 1000).toFixed(2) + 's (' + url.hostname + ')', 'core');
this.log(`Processing ${Object.keys(data).join(', ')} took ${((new Date() - startTime) / 1000).toFixed(2)}s (${url.hostname})`, 'core');
if ( Object.keys(apps).length ) {
this.log('Identified ' + Object.keys(apps).join(', ') + ' (' + url.hostname + ')', 'core');
if (Object.keys(apps).length) {
this.log(`Identified ${Object.keys(apps).join(', ')} (${url.hostname})`, 'core');
}
this.driver.displayApps(this.detected[url.canonical], { language }, context);
@ -140,32 +247,34 @@ class Wappalyzer {
*/
robotsTxtAllows(url) {
return new Promise((resolve, reject) => {
var parsed = this.parseUrl(url);
const parsed = this.parseUrl(url);
if ( parsed.protocol !== 'http:' && parsed.protocol !== 'https:' ) {
return reject();
if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
reject();
return;
}
this.driver.getRobotsTxt(parsed.host, parsed.protocol === 'https:')
.then(robotsTxt => {
if ( robotsTxt.some(disallowedPath => parsed.pathname.indexOf(disallowedPath) === 0) ) {
.then((robotsTxt) => {
if (robotsTxt.some(disallowedPath => parsed.pathname.indexOf(disallowedPath) === 0)) {
return reject();
}
return resolve();
}, () => resolve());
});
};
}
/**
* Parse a URL
*/
parseUrl(url) {
var a = this.driver.document.createElement('a');
const a = this.driver.document.createElement('a');
a.href = url;
a.canonical = a.protocol + '//' + a.host + a.pathname;
a.canonical = `${a.protocol}//${a.host}${a.pathname}`;
return a;
}
@ -173,22 +282,21 @@ class Wappalyzer {
/**
*
*/
parseRobotsTxt(robotsTxt) {
var userAgent;
var disallow = [];
static parseRobotsTxt(robotsTxt) {
const disallow = [];
robotsTxt.split('\n').forEach(line => {
var matches = /^User-agent:\s*(.+)$/i.exec(line);
let userAgent;
if ( matches ) {
robotsTxt.split('\n').forEach((line) => {
let matches = /^User-agent:\s*(.+)$/i.exec(line);
if (matches) {
userAgent = matches[1].toLowerCase();
} else {
if ( userAgent === '*' || userAgent === 'wappalyzer' ) {
matches = /^Disallow:\s*(.+)$/i.exec(line);
} else if (userAgent === '*' || userAgent === 'wappalyzer') {
matches = /^Disallow:\s*(.+)$/i.exec(line);
if ( matches ) {
disallow.push(matches[1]);
}
if (matches) {
disallow.push(matches[1]);
}
}
});
@ -200,66 +308,59 @@ class Wappalyzer {
*
*/
ping() {
if ( Object.keys(this.hostnameCache).length > 100 ) {
if (Object.keys(this.hostnameCache).length > 100) {
this.driver.ping(this.hostnameCache);
this.hostnameCache = {};
}
if ( this.adCache.length > 50 ) {
if (this.adCache.length > 50) {
this.driver.ping({}, this.adCache);
this.adCache = [];
}
}
/**
* Enclose string in array
*/
asArray(value) {
return value instanceof Array ? value : [ value ];
}
/**
* Parse apps.json patterns
*/
parsePatterns(patterns) {
if ( !patterns ) {
if (!patterns) {
return [];
}
var parsed = {};
let parsed = {};
// Convert string to object containing array containing string
if ( typeof patterns === 'string' || patterns instanceof Array ) {
if (typeof patterns === 'string' || patterns instanceof Array) {
patterns = {
main: this.asArray(patterns)
main: asArray(patterns),
};
}
Object.keys(patterns).forEach(key => {
Object.keys(patterns).forEach((key) => {
parsed[key] = [];
this.asArray(patterns[key]).forEach(pattern => {
var attrs = {};
asArray(patterns[key]).forEach((pattern) => {
const attrs = {};
pattern.split('\\;').forEach((attr, i) => {
if ( i ) {
if (i) {
// Key value pairs
attr = attr.split(':');
if ( attr.length > 1 ) {
if (attr.length > 1) {
attrs[attr.shift()] = attr.join(':');
}
} else {
attrs.string = attr;
try {
attrs.regex = new RegExp(attr.replace('/', '\/'), 'i'); // Escape slashes in regular expression
attrs.regex = new RegExp(attr.replace('/', '\\/'), 'i'); // Escape slashes in regular expression
} catch (e) {
attrs.regex = new RegExp();
this.log(e + ': ' + attr, 'error', 'core');
this.log(`${e}: ${attr}`, 'error', 'core');
}
}
});
@ -269,7 +370,7 @@ class Wappalyzer {
});
// Convert back to array if the original pattern list was an array (or string)
if ( 'main' in parsed ) {
if ('main' in parsed) {
parsed = parsed.main;
}
@ -280,65 +381,45 @@ class Wappalyzer {
* Parse JavaScript patterns
*/
parseJsPatterns() {
Object.keys(this.apps).forEach(appName => {
Object.keys(this.apps).forEach((appName) => {
if (this.apps[appName].js) {
this.jsPatterns[appName] = this.parsePatterns(this.apps[appName].js);
}
});
}
resolveExcludes(apps, detected) {
let excludes = [];
// Exclude app in detected apps only
Object.keys(Object.assign({}, apps, detected)).forEach(appName => {
let app = apps[appName];
if (app.props.excludes) {
this.asArray(app.props.excludes).forEach(excluded => {
excludes.push(excluded);
});
}
});
// Remove excluded applications
Object.keys(apps).forEach(appName => {
if (excludes.indexOf(appName) > -1) {
delete apps[appName];
}
});
}
resolveImplies(apps, url) {
var checkImplies = true;
let checkImplies = true;
// Implied applications
// Run several passes as implied apps may imply other apps
while ( checkImplies ) {
while (checkImplies) {
checkImplies = false;
Object.keys(apps).forEach(appName => {
var app = apps[appName];
Object.keys(apps).forEach((appName) => {
const app = apps[appName];
if ( app && app.props.implies ) {
this.asArray(app.props.implies).forEach(implied => {
implied = this.parsePatterns(implied)[0];
if (app && app.props.implies) {
asArray(app.props.implies).forEach((implied) => {
[implied] = this.parsePatterns(implied);
if ( !this.apps[implied.string] ) {
this.log('Implied application ' + implied.string + ' does not exist', 'core', 'warn');
if (!this.apps[implied.string]) {
this.log(`Implied application ${implied.string} does not exist`, 'core', 'warn');
return;
}
if ( !( implied.string in apps ) ) {
apps[implied.string] = this.detected[url] && this.detected[url][implied.string] ? this.detected[url][implied.string] : new Application(implied.string, this.apps[implied.string], true);
if (!(implied.string in apps)) {
apps[implied.string] = this.detected[url] && this.detected[url][implied.string]
? this.detected[url][implied.string]
: new Application(implied.string, this.apps[implied.string], true);
checkImplies = true;
}
// Apply app confidence to implied app
Object.keys(app.confidence).forEach(id => {
apps[implied.string].confidence[id + ' implied by ' + appName] = app.confidence[id] * ( implied.confidence === undefined ? 1 : implied.confidence / 100 );
Object.keys(app.confidence).forEach((id) => {
apps[implied.string].confidence[`${id} implied by ${appName}`] = app.confidence[id] * (implied.confidence === undefined ? 1 : implied.confidence / 100);
});
});
}
@ -350,14 +431,17 @@ class Wappalyzer {
* Cache detected applications
*/
cacheDetectedApps(apps, url) {
Object.keys(apps).forEach(appName => {
let app = apps[appName];
Object.keys(apps).forEach((appName) => {
const app = apps[appName];
// Per URL
this.detected[url][appName] = app;
Object.keys(app.confidence).forEach(id => this.detected[url][appName].confidence[id] = app.confidence[id]);
})
Object.keys(app.confidence)
.forEach((id) => {
this.detected[url][appName].confidence[id] = app.confidence[id];
});
});
if (this.driver.ping instanceof Function) {
this.ping();
@ -368,41 +452,44 @@ class Wappalyzer {
* Track detected applications
*/
trackDetectedApps(apps, url, language) {
if ( !( this.driver.ping instanceof Function ) ) {
if (!(this.driver.ping instanceof Function)) {
return;
}
const hostname = url.protocol + '//' + url.hostname;
const hostname = `${url.protocol}//${url.hostname}`;
Object.keys(apps).forEach(appName => {
Object.keys(apps).forEach((appName) => {
const app = apps[appName];
if ( this.detected[url.canonical][appName].getConfidence() >= 100 ) {
if ( validation.hostname.test(url.hostname) && !validation.hostnameBlacklist.test(url.hostname) ) {
if ( !( hostname in this.hostnameCache ) ) {
if (this.detected[url.canonical][appName].getConfidence() >= 100) {
if (
validation.hostname.test(url.hostname)
&& !validation.hostnameBlacklist.test(url.hostname)
) {
if (!(hostname in this.hostnameCache)) {
this.hostnameCache[hostname] = {
applications: {},
meta: {}
meta: {},
};
}
if ( !( appName in this.hostnameCache[hostname].applications ) ) {
if (!(appName in this.hostnameCache[hostname].applications)) {
this.hostnameCache[hostname].applications[appName] = {
hits: 0
hits: 0,
};
}
this.hostnameCache[hostname].applications[appName].hits ++;
this.hostnameCache[hostname].applications[appName].hits += 1;
if ( apps[appName].version ) {
if (apps[appName].version) {
this.hostnameCache[hostname].applications[appName].version = app.version;
}
}
}
});
if ( hostname in this.hostnameCache ) {
this.hostnameCache[hostname].meta['language'] = language;
if (hostname in this.hostnameCache) {
this.hostnameCache[hostname].meta.language = language;
}
this.ping();
@ -412,15 +499,15 @@ class Wappalyzer {
* Analyze URL
*/
analyzeUrl(app, url) {
var patterns = this.parsePatterns(app.props.url);
const patterns = this.parsePatterns(app.props.url);
if ( !patterns.length ) {
if (!patterns.length) {
return Promise.resolve();
}
return this.asyncForEach(patterns, pattern => {
if ( pattern.regex.test(url.canonical) ) {
this.addDetected(app, pattern, 'url', url.canonical);
return asyncForEach(patterns, (pattern) => {
if (pattern.regex.test(url.canonical)) {
addDetected(app, pattern, 'url', url.canonical);
}
});
}
@ -429,15 +516,15 @@ class Wappalyzer {
* Analyze HTML
*/
analyzeHtml(app, html) {
var patterns = this.parsePatterns(app.props.html);
const patterns = this.parsePatterns(app.props.html);
if ( !patterns.length ) {
if (!patterns.length) {
return Promise.resolve();
}
return this.asyncForEach(patterns, pattern => {
if ( pattern.regex.test(html) ) {
this.addDetected(app, pattern, 'html', html);
return asyncForEach(patterns, (pattern) => {
if (pattern.regex.test(html)) {
addDetected(app, pattern, 'html', html);
}
});
}
@ -446,16 +533,16 @@ class Wappalyzer {
* Analyze script tag
*/
analyzeScripts(app, scripts) {
var patterns = this.parsePatterns(app.props.script);
const patterns = this.parsePatterns(app.props.script);
if ( !patterns.length ) {
if (!patterns.length) {
return Promise.resolve();
}
return this.asyncForEach(patterns, pattern => {
scripts.forEach(uri => {
if ( pattern.regex.test(uri) ) {
this.addDetected(app, pattern, 'script', uri);
return asyncForEach(patterns, (pattern) => {
scripts.forEach((uri) => {
if (pattern.regex.test(uri)) {
addDetected(app, pattern, 'script', uri);
}
});
});
@ -471,20 +558,20 @@ class Wappalyzer {
let matches;
while ( patterns && ( matches = regex.exec(html) ) ) {
for ( let meta in patterns ) {
const r = new RegExp('(?:name|property)=["\']' + meta + '["\']', 'i');
while (patterns && (matches = regex.exec(html))) {
patterns.forEach((meta) => {
const r = new RegExp(`(?:name|property)=["']${meta}["']`, 'i');
if ( r.test(matches[0]) ) {
let content = matches[0].match(/content=("|')([^"']+)("|')/i);
if (r.test(matches[0])) {
const content = matches[0].match(/content=("|')([^"']+)("|')/i);
promises.push(this.asyncForEach(patterns[meta], pattern => {
if ( content && content.length === 4 && pattern.regex.test(content[2]) ) {
this.addDetected(app, pattern, 'meta', content[2], meta);
promises.push(asyncForEach(patterns[meta], (pattern) => {
if (content && content.length === 4 && pattern.regex.test(content[2])) {
addDetected(app, pattern, 'meta', content[2], meta);
}
}));
}
}
});
}
return promises ? Promise.all(promises) : Promise.resolve();
@ -497,15 +584,15 @@ class Wappalyzer {
const patterns = this.parsePatterns(app.props.headers);
const promises = [];
Object.keys(patterns).forEach(headerName => {
if (typeof patterns[headerName] != 'function') {
promises.push(this.asyncForEach(patterns[headerName], pattern => {
Object.keys(patterns).forEach((headerName) => {
if (typeof patterns[headerName] !== 'function') {
promises.push(asyncForEach(patterns[headerName], (pattern) => {
headerName = headerName.toLowerCase();
if ( headerName in headers ) {
headers[headerName].forEach(headerValue => {
if ( pattern.regex.test(headerValue) ) {
this.addDetected(app, pattern, 'headers', headerValue, headerName);
if (headerName in headers) {
headers[headerName].forEach((headerValue) => {
if (pattern.regex.test(headerValue)) {
addDetected(app, pattern, 'headers', headerValue, headerName);
}
});
}
@ -523,15 +610,15 @@ class Wappalyzer {
const patterns = this.parsePatterns(app.props.cookies);
const promises = [];
Object.keys(patterns).forEach(cookieName => {
if (typeof patterns[cookieName] != 'function') {
Object.keys(patterns).forEach((cookieName) => {
if (typeof patterns[cookieName] !== 'function') {
cookieName = cookieName.toLowerCase();
promises.push(this.asyncForEach(patterns[cookieName], pattern => {
const cookie = cookies.find(cookie => cookie.name.toLowerCase() === cookieName);
promises.push(asyncForEach(patterns[cookieName], (pattern) => {
const cookie = cookies.find(_cookie => _cookie.name.toLowerCase() === cookieName);
if ( cookie && pattern.regex.test(cookie.value) ) {
this.addDetected(app, pattern, 'cookies', cookie.value, cookieName);
if (cookie && pattern.regex.test(cookie.value)) {
addDetected(app, pattern, 'cookies', cookie.value, cookieName);
}
}));
}
@ -546,14 +633,14 @@ class Wappalyzer {
analyzeJs(app, results) {
const promises = [];
Object.keys(results).forEach(string => {
if (typeof results[string] != 'function') {
promises.push(this.asyncForEach(Object.keys(results[string]), index => {
Object.keys(results).forEach((string) => {
if (typeof results[string] !== 'function') {
promises.push(asyncForEach(Object.keys(results[string]), (index) => {
const pattern = this.jsPatterns[app.name][string][index];
const value = results[string][index];
if ( pattern && pattern.regex.test(value) ) {
this.addDetected(app, pattern, 'js', value);
if (pattern && pattern.regex.test(value)) {
addDetected(app, pattern, 'js', value);
}
}));
}
@ -561,76 +648,8 @@ class Wappalyzer {
return promises ? Promise.all(promises) : Promise.resolve();
}
/**
* Mark application as detected, set confidence and version
*/
addDetected(app, pattern, type, value, key) {
app.detected = true;
// Set confidence level
app.confidence[type + ' ' + ( key ? key + ' ' : '' ) + pattern.regex] = pattern.confidence === undefined ? 100 : parseInt(pattern.confidence);
// Detect version number
if ( pattern.version ) {
var versions = [];
var version = pattern.version;
var matches = pattern.regex.exec(value);
if ( matches ) {
matches.forEach((match, i) => {
// Parse ternary operator
var ternary = new RegExp('\\\\' + i + '\\?([^:]+):(.*)$').exec(version);
if ( ternary && ternary.length === 3 ) {
version = version.replace(ternary[0], match ? ternary[1] : ternary[2]);
}
// Replace back references
version = version.trim().replace(new RegExp('\\\\' + i, 'g'), match || '');
});
if ( version && versions.indexOf(version) === -1 ) {
versions.push(version);
}
if ( versions.length ) {
// Use the longest detected version number
app.version = versions.reduce((a, b) => a.length > b.length ? a : b);
}
}
}
}
}
/**
* Application class
*/
class Application {
constructor(name, props, detected) {
this.confidence = {};
this.confidenceTotal = 0;
this.detected = Boolean(detected);
this.excludes = [];
this.name = name;
this.props = props;
this.version = '';
}
/**
* Calculate confidence total
*/
getConfidence() {
var total = 0;
for ( let id in this.confidence ) {
total += this.confidence[id];
}
return this.confidenceTotal = Math.min(total, 100);
}
}
if ( typeof module === 'object' ) {
if (typeof module === 'object') {
module.exports = Wappalyzer;
}