diff --git a/README.md b/README.md index 0c0b6fb43..7694a4e59 100644 --- a/README.md +++ b/README.md @@ -95,7 +95,8 @@ Patterns (regular expressions) are kept in [`src/technologies.json`](https://git "generator": "(?:Example|Another Example)" }, "script": "example-([0-9.]+)\\.js\\;confidence:50\\;version:\\1", - "url": ".+\\.example\\.com", + "url": "example\\.com", + "xhr": "example\\.com", "oss": true, "saas": true, "pricing": ["medium", "freemium", "recurring"], @@ -354,6 +355,12 @@ Plus any of: Full URL of the page. "^https?//.+\\.wordpress\\.com" + + xhr + String + Hostnames of XHR requests. + "cdn\\.netlify\\.com" + meta Object diff --git a/src/drivers/npm/driver.js b/src/drivers/npm/driver.js index c94c2c626..9404e727d 100644 --- a/src/drivers/npm/driver.js +++ b/src/drivers/npm/driver.js @@ -51,6 +51,8 @@ const { technologies, categories } = JSON.parse( setTechnologies(technologies) setCategories(categories) +const xhrDebounce = [] + function sleep(ms) { return new Promise((resolve) => setTimeout(resolve, ms)) } @@ -328,6 +330,26 @@ class Site { page.on('request', async (request) => { try { + if (request.resourceType() === 'xhr') { + let hostname + + try { + ;({ hostname } = new URL(request.url())) + } catch (error) { + return + } + + if (!xhrDebounce.includes(hostname)) { + xhrDebounce.push(hostname) + + setTimeout(() => { + xhrDebounce.splice(xhrDebounce.indexOf(hostname), 1) + + this.onDetect(analyze({ xhr: hostname })) + }, 1000) + } + } + if ( (responseReceived && request.isNavigationRequest()) || request.frame() !== page.mainFrame() || diff --git a/src/drivers/webextension/images/icons/commercelayer.svg b/src/drivers/webextension/images/icons/commercelayer.svg new file mode 100644 index 000000000..85fb6ccc5 --- /dev/null +++ b/src/drivers/webextension/images/icons/commercelayer.svg @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/src/drivers/webextension/js/driver.js b/src/drivers/webextension/js/driver.js index ba01cb039..8074f8912 100644 --- a/src/drivers/webextension/js/driver.js +++ b/src/drivers/webextension/js/driver.js @@ -16,6 +16,8 @@ const expiry = 1000 * 60 * 60 * 24 const hostnameIgnoreList = /((local|dev(elop(ment)?)?|stag(e|ing)?|preprod|preview|test(ing)?|demo(shop)?|admin|cache)[.-]|localhost|wappalyzer|google|facebook|twitter|reddit|yahoo|wikipedia|amazon|youtube|\/admin|\.local|\.test|\.dev|^[0-9.]$)/ +const xhrDebounce = [] + const Driver = { lastPing: Date.now(), @@ -66,6 +68,11 @@ const Driver = { ['responseHeaders'] ) + chrome.webRequest.onCompleted.addListener(Driver.onXhrRequestComplete, { + urls: ['http://*/*', 'https://*/*'], + types: ['xmlhttprequest'], + }) + chrome.tabs.onRemoved.addListener((id) => (Driver.cache.tabs[id] = null)) // Enable messaging between scripts @@ -80,10 +87,10 @@ const Driver = { 'https://www.wappalyzer.com/installed/?utm_source=installed&utm_medium=extension&utm_campaign=wappalyzer' ) } else if (version !== previous && upgradeMessage) { - // open( - // `https://www.wappalyzer.com/upgraded/?utm_source=upgraded&utm_medium=extension&utm_campaign=wappalyzer`, - // false - // ) + open( + `https://www.wappalyzer.com/upgraded/?utm_source=upgraded&utm_medium=extension&utm_campaign=wappalyzer`, + false + ) } await setOption('version', version) @@ -256,11 +263,11 @@ const Driver = { * @param {Object} request */ async onWebRequestComplete(request) { - if (await Driver.isDisabledDomain(request.url)) { - return - } - if (request.responseHeaders) { + if (await Driver.isDisabledDomain(request.url)) { + return + } + const headers = {} try { @@ -281,7 +288,7 @@ const Driver = { ) }) - await Driver.onDetect(request.url, analyze({ headers })) + Driver.onDetect(request.url, analyze({ headers })).catch(Driver.error) } } catch (error) { Driver.error(error) @@ -289,6 +296,36 @@ const Driver = { } }, + /** + * Analyse XHR request hostnames + * @param {Object} request + */ + async onXhrRequestComplete(request) { + if (await Driver.isDisabledDomain(request.url)) { + return + } + + let hostname + + try { + ;({ hostname } = new URL(request.url)) + } catch (error) { + return + } + + if (!xhrDebounce.includes(hostname)) { + xhrDebounce.push(hostname) + + setTimeout(() => { + xhrDebounce.splice(xhrDebounce.indexOf(hostname), 1) + + Driver.onDetect(request.originUrl, analyze({ xhr: hostname })).catch( + Driver.error + ) + }, 1000) + } + }, + /** * Process return values from content.js * @param {String} url diff --git a/src/technologies.json b/src/technologies.json index 55120703d..ebd7cf438 100644 --- a/src/technologies.json +++ b/src/technologies.json @@ -4216,6 +4216,10 @@ "description": "Contentful is an API-first content management platform to create, manage and publish content on any digital channel.", "html": "<[^>]+(?:https?:)?//(?:assets|downloads|images|videos)\\.(?:ct?fassets\\.net|contentful\\.com)", "icon": "Contentful.svg", + "headers": { + "x-contentful-request-id": "" + }, + "xhr": "cdn\\.contentful\\.com", "pricing": [ "mid", "freemium", @@ -15483,6 +15487,19 @@ }, "website": "https://shopfa.com" }, + "commercelayer": { + "cats": [ + 6 + ], + "pricing": [ + "medium", + "recurring" + ], + "saas": true, + "xhr": "\\.commercelayer\\.io", + "icon": "commercelayer.svg", + "website": "https://commercelayer.io" + }, "Shopify": { "cats": [ 6 @@ -21178,4 +21195,4 @@ "website": "https://www.xt-commerce.com" } } -} \ No newline at end of file +} diff --git a/src/wappalyzer.js b/src/wappalyzer.js index ac55b1c1e..c4da0fea4 100644 --- a/src/wappalyzer.js +++ b/src/wappalyzer.js @@ -189,6 +189,7 @@ const Wappalyzer = { */ analyze({ url, + xhr, html, css, robots, @@ -210,6 +211,7 @@ const Wappalyzer = { Wappalyzer.technologies.map((technology) => flatten([ oo(technology, 'url', url), + oo(technology, 'xhr', xhr), oo(technology, 'html', html), oo(technology, 'css', css), oo(technology, 'robots', robots), @@ -240,6 +242,7 @@ const Wappalyzer = { const { cats, url, + xhr, dom, html, css, @@ -263,6 +266,7 @@ const Wappalyzer = { categories: cats || [], slug: Wappalyzer.slugify(name), url: transform(url), + xhr: transform(xhr), headers: transform(headers), dns: transform(dns), cookies: transform(cookies),