Skip to content

Commit 3922f23

Browse files
committed
Refactored and improved syncFetchPolicy fallback for file: and ftp: special cases.
1 parent 058bf1b commit 3922f23

File tree

7 files changed

+142
-150
lines changed

7 files changed

+142
-150
lines changed

build.sh

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,11 @@ if ! [ $(date -r "$LIB/tld.js" +'%Y%m%d') -ge $(date +'%Y%m%d') -a "$1" != "tld
6969
git add src/lib/tld.js TLD && git commit -m'Updated TLDs.'
7070
fi
7171

72-
./html5_events/html5_events.pl
72+
if ./html5_events/html5_events.pl; then
73+
# update full event list as an array in src/content/syncFetchPolicy.js
74+
EVENTS=$(egrep '^on[a-z]+$' html5_events/html5_events_archive.txt | sed "s/^on//;s/.*/'&'/;H;1h;"'$!d;x;s/\n/, /g');
75+
perl -pi -e 's/(\blet eventTypes\s*=\s*)\[.*?\]/$1['"$EVENTS"']/' src/content/syncFetchPolicy.js
76+
fi
7377

7478
rm -rf "$BUILD" "$XPI"
7579
cp -pR "$SRC" "$BUILD"

html5_events/html5_events.pl

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -103,12 +103,12 @@ sub patch
103103
if ($must_replace) {
104104
rename $dst, $src;
105105
print "Patched.\n";
106+
return 0;
106107
}
107-
else
108-
{
109-
unlink $dst;
110-
print "Nothing to do.\n";
111-
}
108+
109+
unlink $dst;
110+
print "Nothing to do.\n";
111+
return 1;
112112
}
113113

114-
patch($SOURCE_FILE);
114+
exit(patch($SOURCE_FILE));

src/content/DocumentCSP.js

Lines changed: 0 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,6 @@ class DocumentCSP {
66
this.root = document.documentElement;
77
}
88

9-
removeEventAttributes() {
10-
console.debug("Removing event attributes"); // DEV_ONLY
11-
let {root} = this;
12-
this.rootAttrs = [...root.attributes].filter(a => a.name.toLowerCase().startsWith("on"));
13-
for (let a of this.rootAttrs) root.removeAttributeNode(a);
14-
}
15-
169
apply(capabilities, embedding = CSP.isEmbedType(this.document.contentType)) {
1710
let {document} = this;
1811
if (!capabilities.has("script")) {
@@ -40,8 +33,6 @@ class DocumentCSP {
4033
meta.setAttribute("http-equiv", header.name);
4134
meta.setAttribute("content", header.value);
4235
let root = document.documentElement;
43-
let rootAttrs = [...root.attributes].filter(a => a.name.toLowerCase().startsWith("on"));
44-
for (let a of rootAttrs) root.removeAttributeNode(a);
4536

4637
let {head} = document;
4738
let parent = head ||
@@ -60,13 +51,4 @@ class DocumentCSP {
6051
}
6152
return true;
6253
}
63-
64-
restoreEventAttributes() {
65-
if (!this.rootAttrs) return;
66-
console.debug("Restoring event attributes"); // DEV_ONLY
67-
let {root, rootAttrs} = this;
68-
for (let a of rootAttrs) {
69-
root.setAttributeNodeNS(a);
70-
}
71-
}
7254
}

src/content/staticNS.js

Lines changed: 5 additions & 121 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
'use strict';
33
let listenersMap = new Map();
44
let backlog = new Set();
5-
let documentCSP = new DocumentCSP(document);
65

76
let ns = {
87
debug: true, // DEV_ONLY
@@ -42,8 +41,9 @@
4241
//, document.domain, document.baseURI, window.isSecureContext // DEV_ONLY
4342
);
4443

45-
let requireDocumentCSP = /^(?:ftp|file):/.test(url);
46-
if (!requireDocumentCSP) {
44+
if (this.syncFetchPolicy) { // ftp: or file: - no CSP headers yet
45+
this.syncFetchPolicy();
46+
} else {
4747
// CSP headers have been already provided by webRequest, we are not in a hurry...
4848
if (/^(javascript|about):/.test(url)) {
4949
url = document.readyState === "loading"
@@ -68,118 +68,6 @@
6868
asyncFetch();
6969
return;
7070
}
71-
72-
// Here we've got no CSP header yet (file: or ftp: URL), we need one
73-
// injected in the DOM as soon as possible.
74-
debug("No CSP yet for non-HTTP document load: fetching policy synchronously...");
75-
documentCSP.removeEventAttributes();
76-
77-
let earlyScripts = [];
78-
let dequeueEarlyScripts = (last = false) => {
79-
if (!(ns.canScript && earlyScripts)) return;
80-
if (earlyScripts.length === 0) {
81-
earlyScripts = null;
82-
return;
83-
}
84-
for (let s; s = earlyScripts.shift(); ) {
85-
debug("Restoring", s);
86-
s.firstChild._replaced = true;
87-
s._original.replaceWith(s);
88-
}
89-
}
90-
91-
let syncFetch = callback => {
92-
browser.runtime.sendSyncMessage(
93-
{id: "fetchPolicy", url, contextUrl: url},
94-
callback);
95-
};
96-
97-
if (UA.isMozilla && document.readyState !== "complete") {
98-
// Mozilla has already parsed the <head> element, we must take extra steps...
99-
100-
debug("Early parsing: preemptively suppressing events and script execution.");
101-
102-
{
103-
let eventTypes = [];
104-
for (let p in document.documentElement) if (p.startsWith("on")) eventTypes.push(p.substring(2));
105-
let eventSuppressor = e => {
106-
try {
107-
debug("Event suppressor called for ", e.type, e.target, earlyScripts, e.target._earlyScript); // DEV_ONLY
108-
if (!earlyScripts || document.readyState === "complete") {
109-
debug("Stopping event suppression");
110-
for (let et of eventTypes) document.removeEventListener(et, eventSuppressor, true);
111-
return;
112-
}
113-
114-
if (!ns.canScript || e.target._earlyScript) {
115-
e.stopImmediatePropagation();
116-
debug(`Suppressing ${e.type} on `, e.target); // DEV_ONLY
117-
}
118-
} catch (e) {
119-
error(e);
120-
}
121-
}
122-
debug("Starting event suppression");
123-
for (let et of eventTypes) document.addEventListener(et, eventSuppressor, true);
124-
125-
ns.on("capabilities", () => {
126-
if (!ns.canScript) {
127-
try {
128-
for (node of document.querySelectorAll("*")) {
129-
let evAttrs = [...node.attributes].filter(a => a.name.toLowerCase().startsWith("on"));
130-
for (let a of evAttrs) {
131-
debug("Reparsing event attribute after CSP", a, node);
132-
node.removeAttributeNode(a);
133-
node.setAttributeNodeNS(a);
134-
}
135-
}
136-
} catch (e) {
137-
error(e);
138-
}
139-
}
140-
141-
});
142-
}
143-
144-
addEventListener("beforescriptexecute", e => {
145-
debug(e.type, e.target);
146-
if (earlyScripts) {
147-
let s = e.target;
148-
if (s._replaced) {
149-
debug("Replaced script found");
150-
dequeueEarlyScripts(true);
151-
return;
152-
}
153-
let replacement = document.createRange().createContextualFragment(s.outerHTML);
154-
replacement._original = s;
155-
s._earlyScript = true;
156-
earlyScripts.push(replacement);
157-
e.preventDefault();
158-
dequeueEarlyScripts(true);
159-
debug("Blocked early script");
160-
}
161-
}, true);
162-
}
163-
164-
let setup = policy => {
165-
debug("Fetched %o, readyState %s", policy, document.readyState); // DEV_ONLY
166-
this.setup(policy);
167-
documentCSP.restoreEventAttributes();
168-
}
169-
170-
for (let attempts = 3; attempts-- > 0;) {
171-
try {
172-
syncFetch(setup);
173-
break;
174-
} catch (e) {
175-
if (!Messages.isMissingEndpoint(e) || document.readyState === "complete") {
176-
error(e);
177-
break;
178-
}
179-
error("Background page not ready yet, retrying to fetch policy...")
180-
}
181-
}
182-
dequeueEarlyScripts();
18371
},
18472

18573
setup(policy) {
@@ -196,7 +84,7 @@
19684
} else {
19785
let perms = policy.permissions;
19886
this.capabilities = new Set(perms.capabilities);
199-
documentCSP.apply(this.capabilities, this.embeddingDocument);
87+
new DocumentCSP(document).apply(this.capabilities, this.embeddingDocument);
20088
}
20189
this.canScript = this.allows("script");
20290
this.fire("capabilities");
@@ -209,9 +97,5 @@
20997
},
21098
};
21199

212-
if (this.ns) {
213-
this.ns.merge(ns);
214-
} else {
215-
this.ns = ns;
216-
}
100+
this.ns = this.ns ? Object.assign(this.ns, ns) : ns;
217101
}

src/content/syncFetchPolicy.js

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
"use strict";
2+
3+
(this.ns || (this.ns = {})).syncFetchPolicy = function() {
4+
5+
let url = document.URL;
6+
7+
// Here we've got no CSP header yet (file: or ftp: URL), we need one
8+
// injected in the DOM as soon as possible.
9+
debug("No CSP yet for non-HTTP document load: fetching policy synchronously...");
10+
11+
let earlyScripts = [];
12+
let dequeueEarlyScripts = (last = false) => {
13+
if (!(ns.canScript && earlyScripts)) return;
14+
if (earlyScripts.length === 0) {
15+
earlyScripts = null;
16+
return;
17+
}
18+
for (let s; earlyScripts && (s = earlyScripts.shift()); ) {
19+
debug("Restoring", s);
20+
s.firstChild._replaced = true;
21+
s._original.replaceWith(s);
22+
}
23+
}
24+
25+
let syncFetch = callback => {
26+
browser.runtime.sendSyncMessage(
27+
{id: "fetchPolicy", url, contextUrl: url},
28+
callback);
29+
};
30+
31+
if (UA.isMozilla && document.readyState !== "complete") {
32+
// Mozilla has already parsed the <head> element, we must take extra steps...
33+
34+
debug("Early parsing: preemptively suppressing events and script execution.");
35+
36+
{
37+
// List updated by build.sh from https://hg.mozilla.org/mozilla-central/raw-file/tip/xpcom/ds/StaticAtoms.py
38+
// whenever html5_events/html5_events.pl retrieves something new.
39+
let eventTypes = ['abort', 'mozaccesskeynotfound', 'activate', 'afterprint', 'afterscriptexecute', 'animationcancel', 'animationend', 'animationiteration', 'animationstart', 'audioprocess', 'auxclick', 'beforecopy', 'beforecut', 'beforeinput', 'beforepaste', 'beforeprint', 'beforescriptexecute', 'beforeunload', 'blocked', 'blur', 'bounce', 'boundschange', 'broadcast', 'bufferedamountlow', 'cached', 'cancel', 'change', 'chargingchange', 'chargingtimechange', 'checking', 'click', 'close', 'command', 'commandupdate', 'complete', 'compositionend', 'compositionstart', 'compositionupdate', 'connect', 'connectionavailable', 'contextmenu', 'copy', 'cut', 'dblclick', 'dischargingtimechange', 'downloading', 'data', 'drag', 'dragdrop', 'dragend', 'dragenter', 'dragexit', 'dragleave', 'dragover', 'dragstart', 'drain', 'drop', 'error', 'finish', 'focus', 'focusin', 'focusout', 'fullscreenchange', 'fullscreenerror', 'get', 'hashchange', 'input', 'inputsourceschange', 'install', 'invalid', 'keydown', 'keypress', 'keyup', 'languagechange', 'levelchange', 'load', 'loading', 'loadingdone', 'loadingerror', 'popstate', 'merchantvalidation', 'message', 'messageerror', 'midimessage', 'mousedown', 'mouseenter', 'mouseleave', 'mouselongtap', 'mousemove', 'mouseout', 'mouseover', 'mouseup', 'mozfullscreenchange', 'mozfullscreenerror', 'mozkeydownonplugin', 'mozkeyuponplugin', 'mozpointerlockchange', 'mozpointerlockerror', 'mute', 'notificationclick', 'notificationclose', 'noupdate', 'obsolete', 'online', 'offline', 'open', 'orientationchange', 'overflow', 'pagehide', 'pageshow', 'paste', 'payerdetailchange', 'paymentmethodchange', 'pointerlockchange', 'pointerlockerror', 'popuphidden', 'popuphiding', 'popuppositioned', 'popupshowing', 'popupshown', 'processorerror', 'push', 'pushsubscriptionchange', 'readystatechange', 'rejectionhandled', 'remove', 'requestprogress', 'resourcetimingbufferfull', 'responseprogress', 'reset', 'resize', 'scroll', 'select', 'selectionchange', 'selectend', 'selectstart', 'set', 'shippingaddresschange', 'shippingoptionchange', 'show', 'squeeze', 'squeezeend', 'squeezestart', 'statechange', 'storage', 'submit', 'success', 'typechange', 'terminate', 'text', 'toggle', 'tonechange', 'touchstart', 'touchend', 'touchmove', 'touchcancel', 'transitioncancel', 'transitionend', 'transitionrun', 'transitionstart', 'underflow', 'unhandledrejection', 'unload', 'unmute', 'updatefound', 'updateready', 'upgradeneeded', 'versionchange', 'visibilitychange', 'voiceschanged', 'vrdisplayactivate', 'vrdisplayconnect', 'vrdisplaydeactivate', 'vrdisplaydisconnect', 'vrdisplaypresentchange', 'webkitanimationend', 'webkitanimationiteration', 'webkitanimationstart', 'webkittransitionend', 'wheel', 'zoom', 'begin', 'end', 'repeat', 'pointerdown', 'pointermove', 'pointerup', 'pointercancel', 'pointerover', 'pointerout', 'pointerenter', 'pointerleave', 'gotpointercapture', 'lostpointercapture', 'devicemotion', 'deviceorientation', 'absolutedeviceorientation', 'deviceproximity', 'mozorientationchange', 'userproximity', 'devicelight', 'devicechange', 'mozvisualresize', 'mozvisualscroll', 'mozshowdropdown', 'scrollend', 'loadend', 'loadstart', 'progress', 'suspend', 'emptied', 'stalled', 'play', 'pause', 'loadedmetadata', 'loadeddata', 'waiting', 'playing', 'canplay', 'canplaythrough', 'seeking', 'seeked', 'timeout', 'timeupdate', 'ended', 'formdata', 'ratechange', 'durationchange', 'volumechange', 'addtrack', 'controllerchange', 'cuechange', 'enter', 'exit', 'encrypted', 'waitingforkey', 'keystatuseschange', 'removetrack', 'dataavailable', 'warning', 'start', 'stop', 'photo', 'gamepadbuttondown', 'gamepadbuttonup', 'gamepadaxismove', 'gamepadconnected', 'gamepaddisconnected', 'fetch', 'audiostart', 'audioend', 'soundstart', 'soundend', 'speechstart', 'speechend', 'result', 'nomatch', 'resume', 'mark', 'boundary', 'activated', 'deactivated', 'metadatachange', 'playbackstatechange', 'positionstatechange', 'supportedkeyschange', 'sourceopen', 'sourceended', 'sourceclosed', 'updatestart', 'update', 'updateend', 'addsourcebuffer', 'removesourcebuffer', 'appinstalled', 'activestatechanged', 'adapteradded', 'adapterremoved', 'alerting', 'antennaavailablechange', 'attributechanged', 'attributereadreq', 'attributewritereq', 'beforeevicted', 'busy', 'callschanged', 'cardstatechange', 'cfstatechange', 'characteristicchanged', 'clirmodechange', 'connected', 'connecting', 'connectionstatechanged', 'currentchannelchanged', 'currentsourcechanged', 'datachange', 'dataerror', 'deleted', 'deliveryerror', 'deliverysuccess', 'devicefound', 'devicepaired', 'deviceunpaired', 'dialing', 'disabled', 'disconnect', 'disconnected', 'disconnecting', 'displaypasskeyreq', 'draggesture', 'eitbroadcasted', 'emergencycbmodechange', 'enabled', 'enterpincodereq', 'evicted', 'failed', 'frequencychange', 'groupchange', 'headphoneschange', 'held', 'hfpstatuschanged', 'hidstatuschanged', 'holding', 'iccchange', 'iccdetected', 'iccinfochange', 'iccundetected', 'incoming', 'mapfolderlistingreq', 'mapgetmessagereq', 'mapmessageslistingreq', 'mapmessageupdatereq', 'mapsendmessagereq', 'mapsetmessagestatusreq', 'mousewheel', 'mozbrowserafterkeydown', 'mozbrowserafterkeyup', 'mozbrowserbeforekeydown', 'mozbrowserbeforekeyup', 'mozinterruptbegin', 'mozinterruptend', 'moznetworkdownload', 'moznetworkupload', 'moztimechange', 'newrdsgroup', 'obexpasswordreq', 'otastatuschange', 'overflowchanged', 'paint', 'pairingaborted', 'pairingconfirmationreq', 'pairingconsentreq', 'pendingchange', 'pichange', 'pschange', 'ptychange', 'pullphonebookreq', 'pullvcardentryreq', 'pullvcardlistingreq', 'radiostatechange', 'rdsdisabled', 'rdsenabled', 'readerror', 'readsuccess', 'ready', 'received', 'reloadpage', 'remoteheld', 'remoteresumed', 'requestmediaplaystatus', 'resuming', 'retrieving', 'rtchange', 'scanningstatechanged', 'scostatuschanged', 'sending', 'sent', 'speakerforcedchange', 'statuschanged', 'stkcommand', 'stksessionend', 'storageareachanged', 'ussdreceived', 'voicechange', 'websocket'];
40+
let eventSuppressor = e => {
41+
try {
42+
debug("Event suppressor called for ", e.type, e.target, earlyScripts, e.target._earlyScript); // DEV_ONLY
43+
if (!earlyScripts || document.readyState === "complete") {
44+
debug("Stopping event suppression");
45+
for (let et of eventTypes) document.removeEventListener(et, eventSuppressor, true);
46+
return;
47+
}
48+
49+
if (!ns.canScript || e.target._earlyScript) {
50+
e.stopPropagation();
51+
debug(`Suppressing ${e.type} on `, e.target); // DEV_ONLY
52+
}
53+
} catch (e) {
54+
error(e);
55+
}
56+
}
57+
debug("Starting event suppression");
58+
for (let et of eventTypes) document.addEventListener(et, eventSuppressor, true);
59+
60+
ns.on("capabilities", () => {
61+
if (!ns.canScript) {
62+
try {
63+
for (let node of document.querySelectorAll("*")) {
64+
let evAttrs = [...node.attributes].filter(a => a.name.toLowerCase().startsWith("on"));
65+
for (let a of evAttrs) {
66+
debug("Reparsing event attribute after CSP", a, node);
67+
node.removeAttributeNode(a);
68+
node.setAttributeNodeNS(a);
69+
}
70+
}
71+
} catch (e) {
72+
error(e);
73+
}
74+
}
75+
});
76+
}
77+
78+
addEventListener("beforescriptexecute", e => {
79+
debug(e.type, e.target);
80+
if (earlyScripts) {
81+
let s = e.target;
82+
if (s._replaced) {
83+
debug("Replaced script found");
84+
dequeueEarlyScripts(true);
85+
return;
86+
}
87+
let replacement = document.createRange().createContextualFragment(s.outerHTML);
88+
replacement._original = s;
89+
s._earlyScript = true;
90+
earlyScripts.push(replacement);
91+
e.preventDefault();
92+
dequeueEarlyScripts(true);
93+
debug("Blocked early script");
94+
}
95+
}, true);
96+
}
97+
98+
let setup = policy => {
99+
debug("Fetched %o, readyState %s", policy, document.readyState); // DEV_ONLY
100+
ns.setup(policy);
101+
}
102+
103+
for (let attempts = 3; attempts-- > 0;) {
104+
try {
105+
syncFetch(setup);
106+
break;
107+
} catch (e) {
108+
if (!Messages.isMissingEndpoint(e) || document.readyState === "complete") {
109+
error(e);
110+
break;
111+
}
112+
error("Background page not ready yet, retrying to fetch policy...")
113+
}
114+
}
115+
dequeueEarlyScripts();
116+
}

src/lib/SyncMessage.js

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -244,8 +244,7 @@
244244
console.debug("sendSyncMessage suspending on ", records)
245245
suspend();
246246
});
247-
domSuspender.observe(document.documentElement, {childList: true});
248-
247+
domSuspender.observe(document, {childList: true, subtree: true});
249248
let finalize = () => {
250249
console.debug("sendSyncMessage finalizing");
251250
domSuspender.disconnect();

src/manifest.json

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,15 @@
7272
"css": [
7373
"/content/content.css"
7474
]
75-
},
75+
},
76+
{
77+
"run_at": "document_start",
78+
"matches": ["file://*/*", "ftp://*/*"],
79+
"js": [
80+
"lib/SyncMessage.js",
81+
"content/syncFetchPolicy.js"
82+
]
83+
},
7684
{
7785
"run_at": "document_start",
7886
"matches": ["<all_urls>"],
@@ -83,7 +91,6 @@
8391
"lib/browser-polyfill.js",
8492
"lib/log.js",
8593
"lib/uuid.js",
86-
"lib/SyncMessage.js",
8794
"lib/sha256.js",
8895
"lib/Messages.js",
8996
"lib/CSP.js",

0 commit comments

Comments
 (0)