Skip to content
This repository was archived by the owner on Sep 21, 2022. It is now read-only.

Commit f11e23f

Browse files
committed
working 3 checks
1 parent 2c39566 commit f11e23f

File tree

11 files changed

+333
-140
lines changed

11 files changed

+333
-140
lines changed

src/checks/manifest.ts

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import { ManifestDetectionResult, ServiceWorkerDetectionResult, SiteData } from "../interfaces/validation";
2+
import { getTabId } from "../utils/chromium";
3+
4+
let manifestInfoPromise: Promise<ManifestDetectionResult>;
5+
const messageType = 'manifestMessage';
6+
7+
export function getManifestInfo() : Promise<ManifestDetectionResult> {
8+
if (manifestInfoPromise) {
9+
return manifestInfoPromise;
10+
}
11+
12+
manifestInfoPromise = new Promise(async (resolve, reject) => {
13+
const listener = (message: any, sender: any, sendResponse: any) => {
14+
if (message.type && message.type === messageType) {
15+
// we finally have all the info we need for the service worker
16+
resolve(message.manifestInfo);
17+
chrome.runtime.onMessage.removeListener(listener);
18+
sendResponse(true);
19+
}
20+
}
21+
22+
chrome.runtime.onMessage.addListener(listener)
23+
24+
const tabId = await getTabId();
25+
if (tabId) {
26+
await chrome.scripting.executeScript({
27+
target: {tabId},
28+
func: runContentScriptIsolated,
29+
args: [messageType]
30+
});
31+
} else {
32+
reject("No active tab found");
33+
}
34+
});
35+
36+
return manifestInfoPromise;
37+
}
38+
39+
// running code isolated so we can use messaging back to extension
40+
// should be run first to setup window message listener for messages from other content script
41+
async function runContentScriptIsolated(messageType: string) {
42+
43+
const manifestInfo : ManifestDetectionResult = {
44+
hasManifest: false
45+
}
46+
47+
const link = document.querySelector('link[rel="manifest"]') as HTMLLinkElement;
48+
if (link) {
49+
const manifestUri = link.href;
50+
51+
const manifestResponse = await fetch(manifestUri, { credentials: "include" })
52+
const manifest = await manifestResponse.json();
53+
54+
manifestInfo.hasManifest = true;
55+
manifestInfo.manifestUri = manifestUri;
56+
manifestInfo.manifest = manifest;
57+
}
58+
59+
chrome.runtime.sendMessage({type: messageType, manifestInfo});
60+
return true;
61+
}

src/checks/security.ts

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import { api } from "../endpoints";
2+
import { TestResult } from "../interfaces/manifest";
3+
import { SecurityDataResults } from "../interfaces/validation";
4+
5+
const default_results = [
6+
{
7+
result: false,
8+
infoString: "Uses HTTPS",
9+
category: "required",
10+
},
11+
{
12+
result: false,
13+
infoString: "Has a valid SSL certificate",
14+
category: "required",
15+
},
16+
{
17+
result: false,
18+
infoString: "No mixed content on page",
19+
category: "required",
20+
},
21+
];
22+
23+
export async function testSecurity(url: string): Promise<Array<TestResult>> {
24+
// We've witnessed this call take a very long time. We're going to time-box it to 20s.
25+
const twentySecondTimeout = new Promise<void>((resolve) =>
26+
setTimeout(() => resolve(), 20000)
27+
);
28+
29+
const encodedUrl = encodeURIComponent(url);
30+
const securityUrl = `${api}/Security?site=${encodedUrl}`;
31+
const fetchResult = fetch(securityUrl);
32+
33+
const fetchResultOrTimeout: void | Response = await Promise.race([
34+
twentySecondTimeout,
35+
fetchResult,
36+
]);
37+
38+
if (!fetchResultOrTimeout) {
39+
console.warn("Security check timed out after 20 seconds.");
40+
return default_results;
41+
}
42+
43+
if (!fetchResultOrTimeout.ok) {
44+
console.warn(
45+
"Security check failed with non-successful status code",
46+
fetchResultOrTimeout.status,
47+
fetchResultOrTimeout.statusText
48+
);
49+
return default_results;
50+
}
51+
52+
// we have made it through, yay!
53+
const results: { data: SecurityDataResults } =
54+
await fetchResultOrTimeout.json();
55+
console.info("Security detection completed successfully", results);
56+
57+
const organizedResults = [
58+
{
59+
result: results.data.isHTTPS,
60+
infoString: "Uses HTTPS",
61+
category: "required",
62+
},
63+
{
64+
result: results.data.valid,
65+
infoString: "Has a valid SSL certificate",
66+
category: "required",
67+
},
68+
{
69+
result: results.data.validProtocol,
70+
infoString: "No mixed content on page",
71+
category: "required",
72+
},
73+
];
74+
75+
return organizedResults;
76+
}

src/checks/sw.ts

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
import { ManifestDetectionResult, ServiceWorkerDetectionResult, SiteData } from "../interfaces/validation";
2+
import { getTabId } from "../utils/chromium";
3+
4+
let getServiceWorkerInfoPromise: Promise<SiteData>;
5+
const messageType = 'serviceWorkerMessage';
6+
7+
export function getSwInfo() : Promise<SiteData> {
8+
if (getServiceWorkerInfoPromise) {
9+
return getServiceWorkerInfoPromise;
10+
}
11+
12+
getServiceWorkerInfoPromise = new Promise(async (resolve, reject) => {
13+
const listener = (message: any, sender: any, sendResponse: any) => {
14+
if (message.type && message.type === messageType) {
15+
// we finally have all the info we need for the service worker
16+
resolve(message.serviceWorkerRegistration);
17+
chrome.runtime.onMessage.removeListener(listener);
18+
sendResponse(true);
19+
}
20+
}
21+
22+
chrome.runtime.onMessage.addListener(listener)
23+
24+
const tabId = await getTabId();
25+
if (tabId) {
26+
await chrome.scripting.executeScript({
27+
target: {tabId},
28+
func: runContentScriptIsolated,
29+
args: [messageType]
30+
});
31+
32+
await chrome.scripting.executeScript({
33+
target: {tabId},
34+
func: runContentScriptOnPage,
35+
world: "MAIN",
36+
args: [messageType]
37+
});
38+
} else {
39+
reject("No active tab found");
40+
}
41+
});
42+
43+
return getServiceWorkerInfoPromise;
44+
}
45+
46+
47+
// running code on page allowing us to get the service worker registration
48+
async function runContentScriptOnPage(messageType: string) {
49+
50+
const timeoutPromise = new Promise((resolve, reject) => {
51+
setTimeout(resolve, 10000);
52+
})
53+
54+
55+
const result = await Promise.any([timeoutPromise, navigator.serviceWorker.ready]);
56+
57+
const swInfo : ServiceWorkerDetectionResult = {
58+
hasSW: false
59+
}
60+
61+
if (result) {
62+
const registration = result as ServiceWorkerRegistration;
63+
swInfo.hasSW = true;
64+
swInfo.url = registration.active?.scriptURL;
65+
swInfo.scope = registration.scope;
66+
swInfo.isHtmlInCache = false;
67+
68+
async function fetchSwCode(uri: string) {
69+
const response = await fetch(uri, { credentials: "include" });
70+
const text = await response.text();
71+
return text;
72+
}
73+
74+
async function isHtmlInCache() {
75+
const cacheNames = await caches.keys();
76+
for (let cacheName of cacheNames) {
77+
const cache = await caches.open(cacheName);
78+
const cacheKeys = await cache.keys();
79+
for (let key of cacheKeys) {
80+
const cachedObject = await cache.match(key);
81+
if (cachedObject && cachedObject.headers) {
82+
const contentType = cachedObject.headers.get('Content-Type');
83+
if (contentType && contentType.startsWith('text/html')) {
84+
return true;
85+
}
86+
}
87+
}
88+
}
89+
return false;
90+
}
91+
92+
function hasSwEventSubscription(eventName: string, eventMethodName: string, testString: string) {
93+
const addEventListenerRegEx = `(\\n\\s*|.)addEventListener\\(['|\\"]${eventName}['|\\"]`;
94+
const methodnameRegEx = `(\\n\\s*|.)${eventName}\\s*=`;
95+
96+
return (new RegExp(`(${addEventListenerRegEx}|${methodnameRegEx})`)).test(testString);
97+
}
98+
99+
const results = await Promise.all([fetchSwCode(registration.active?.scriptURL!), isHtmlInCache()]);
100+
swInfo.rawSW = results[0];
101+
swInfo.isHtmlInCache = results[1];
102+
103+
const pushSubscription = await registration.pushManager.getSubscription();
104+
swInfo.hasPushRegistration = pushSubscription !== null || hasSwEventSubscription("push", "onpush", swInfo.rawSW);
105+
106+
try {
107+
const periodicSyncTags : [] = await (registration as any).periodicSync.getTags();
108+
swInfo.hasPeriodicBackgroundSync = periodicSyncTags.length > 0 || hasSwEventSubscription("periodicsync", "onperiodicsync", swInfo.rawSW);
109+
} catch {
110+
swInfo.hasPeriodicBackgroundSync = hasSwEventSubscription("periodicsync", "onperiodicsync", swInfo.rawSW);
111+
}
112+
113+
try {
114+
const syncTags : [] = await (registration as any).sync.getTags();
115+
swInfo.hasBackgroundSync = syncTags.length > 0 || hasSwEventSubscription("sync", "onsync", swInfo.rawSW);
116+
} catch {
117+
swInfo.hasBackgroundSync = hasSwEventSubscription("sync", "onsync", swInfo.rawSW);
118+
}
119+
120+
}
121+
122+
window.postMessage({type: messageType, serviceWorkerRegistration: swInfo}, "*");
123+
}
124+
125+
// running code isolated so we can use messaging back to extension
126+
// should be run first to setup window message listener for messages from other content script
127+
function runContentScriptIsolated(messageType: string) {
128+
const callback = (event: MessageEvent) => {
129+
console.log('got message there', event.data);
130+
if (event.source !== window) {
131+
return;
132+
}
133+
134+
if (event.data.type && event.data.type === messageType) {
135+
window.removeEventListener("message", callback);
136+
console.log('here', event.data.serviceWorkerRegistration)
137+
chrome.runtime.sendMessage<ServiceWorkerDetectionResult>(event.data);
138+
}
139+
};
140+
141+
window.addEventListener("message", callback)
142+
return true;
143+
}

src/components/manifest-designer.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import { LitElement, html } from "lit";
22
import {customElement, state} from 'lit/decorators.js';
3-
import { getSiteInfo } from "../extensionHelpers";
43
import { Manifest } from "../interfaces/manifest";
54

65
import {
@@ -13,6 +12,7 @@ import {
1312
} from "@fluentui/web-components";
1413

1514
import "@shoelace-style/shoelace/dist/components/color-picker/color-picker";
15+
import { getManifestInfo } from "../checks/manifest";
1616

1717
provideFluentDesignSystem().register(
1818
fluentButton(),
@@ -122,9 +122,9 @@ export class ManifestDesigner extends LitElement {
122122
private manifest!: Manifest;
123123

124124
async firstUpdated() {
125-
const siteInfo = await getSiteInfo();
126-
if (siteInfo && siteInfo.manifest && siteInfo.manifest.manifest) {
127-
this.manifest = siteInfo.manifest.manifest;
125+
const manifestInfo = await getManifestInfo();
126+
if (manifestInfo && manifestInfo.manifest) {
127+
this.manifest = manifestInfo.manifest;
128128
}
129129

130130
console.log('manifest', this.manifest);

src/components/package-windows.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import { LitElement, html, css } from "lit";
22
import { customElement, state } from "lit/decorators.js";
33
import { windowsEndpoint } from "../endpoints";
4-
import { getSiteInfo } from "../extensionHelpers";
54
import { WindowsOptions } from "../interfaces/windowsOptions";
65

76
import {
@@ -11,6 +10,7 @@ import {
1110
fluentTooltip,
1211
fluentAnchor,
1312
} from "@fluentui/web-components";
13+
import { getManifestInfo } from "../checks/manifest";
1414

1515
provideFluentDesignSystem().register(
1616
fluentTextField(),
@@ -60,11 +60,11 @@ export class PackageWindows extends LitElement {
6060
];
6161

6262
public async firstUpdated() {
63-
const siteInfo = await getSiteInfo();
63+
const manifestInfo = await getManifestInfo();
6464

65-
if (siteInfo && siteInfo.manifest) {
66-
this.currentManiUrl = siteInfo.manifest.manifestUri;
67-
const manifest = siteInfo.manifest.manifest;
65+
if (manifestInfo) {
66+
this.currentManiUrl = manifestInfo.manifestUri;
67+
const manifest = manifestInfo.manifest;
6868

6969
// get current url
7070
let url = await chrome.tabs.query({ active: true, currentWindow: true });

src/components/scanner.ts

Lines changed: 24 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,11 @@ import "@lottiefiles/lottie-player";
1111
import "@shoelace-style/shoelace/dist/components/progress-ring/progress-ring";
1212
import "@shoelace-style/shoelace/dist/components/badge/badge";
1313

14-
import { getSiteInfo } from "../extensionHelpers";
1514
import { runManifestChecks } from "../utils/manifest";
1615
import { TestResult } from "../interfaces/manifest";
16+
import { testSecurity } from "../checks/security";
17+
import { getSwInfo } from "../checks/sw";
18+
import { getManifestInfo } from "../checks/manifest";
1719

1820
provideFluentDesignSystem().register(
1921
fluentButton(),
@@ -73,18 +75,29 @@ export class PWAScanner extends LitElement {
7375
this.currentUrl = url[0].url || "";
7476
}
7577

76-
const siteInfo = await getSiteInfo();
78+
console.log("fetching site info");
79+
// const siteInfo = await getSiteInfo();
80+
// console.log(siteInfo);
7781

78-
this.testResults = await runManifestChecks({
79-
manifestUrl: siteInfo.manifest.manifestUri,
80-
initialManifest: siteInfo.manifest.manifest,
81-
siteUrl: this.currentUrl,
82-
isGenerated: false,
83-
isEdited: false,
84-
manifest: siteInfo.manifest.manifest,
85-
});
82+
// this.testResults = await runManifestChecks({
83+
// manifestUrl: siteInfo.manifest.manifestUri!,
84+
// initialManifest: siteInfo.manifest.manifest!,
85+
// siteUrl: this.currentUrl,
86+
// isGenerated: false,
87+
// isEdited: false,
88+
// manifest: siteInfo.manifest.manifest!,
89+
// });
90+
91+
// if (siteInfo.sw.hasSW) {
92+
// console.log('service worker tests', analyzeSW(siteInfo.sw.rawSW!))
93+
// }
94+
95+
// console.log('manifest tests', this.testResults);
96+
97+
console.log("manifest", await getManifestInfo());
98+
console.log("sw", await getSwInfo());
99+
console.log('security', await testSecurity(this.currentUrl));
86100

87-
console.log(this.testResults);
88101
}
89102

90103
render() {

0 commit comments

Comments
 (0)