1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
|
{
'use strict';
let listenersMap = new Map();
let backlog = new Set();
let ns = {
debug: true, // DEV_ONLY
get embeddingDocument() {
delete this.embeddingDocument;
return this.embeddingDocument = CSP.isEmbedType(document.contentType);
},
on(eventName, listener) {
let listeners = listenersMap.get(eventName);
if (!listeners) listenersMap.set(eventName, listeners = new Set());
listeners.add(listener);
if (backlog.has(eventName)) this.fire(eventName, listener);
},
detach(eventName, listener) {
let listeners = listenersMap.get(eventName);
if (listeners) listeners.delete(listener);
},
fire(eventName, listener = null) {
if (listener) {
listener({type:eventName, source: this});
return;
}
let listeners = listenersMap.get(eventName);
if (listeners) {
for (let l of listeners) {
this.fire(eventName, l);
}
}
backlog.add(eventName);
},
fetchPolicy() {
let url = document.URL;
debug(`Fetching policy from document %s, readyState %s`,
url, document.readyState
, document.documentElement.outerHTML, // DEV_ONLY
document.domain, document.baseURI, window.isSecureContext // DEV_ONLY
);
if (!/^(?:file|ftp|https?):/i.test(url)) {
if (/^(javascript|about):/.test(url)) {
url = document.readyState === "loading"
? document.baseURI
: `${window.isSecureContext ? "https" : "http"}://${document.domain}`;
debug("Fetching policy for actual URL %s (was %s)", url, document.URL);
}
(async () => {
let policy;
try {
policy = await Messages.send("fetchChildPolicy", {url, contextUrl: url});
} catch (e) {
console.error("Error while fetching policy", e);
}
if (policy === undefined) {
log("Policy was undefined, retrying in 1/2 sec...");
setTimeout(() => this.fetchPolicy(), 500);
return;
}
this.setup(policy);
})();
return;
}
let originalState = document.readyState;
let blockedScripts = [];
let localPolicyKey, localPolicy;
if (UA.isMozilla && /^(?:ftp|file):/.test(url)) {
localPolicyKey = `ns.policy.${url}|${browser.runtime.getURL("")}`;
let localPolicy = sessionStorage.getItem(localPolicyKey);
sessionStorage.removeItem(localPolicyKey);
if (localPolicy) {
debug("Falling back to localPolicy", localPolicy);
try {
this.setup(JSON.parse(localPolicy));
return;
} catch(e) {
error(e, "Could not setup local policy", localPolicy);
}
}
addEventListener("beforescriptexecute", e => {
// safety net for synchronous loads on Firefox
if (!this.canScript) {
e.preventDefault();
let script = e.target;
blockedScripts.push(script)
log("Some script managed to be inserted in the DOM while fetching policy, blocking it.\n", script);
}
}, true);
}
let policy = null;
let setup = policy => {
debug("Fetched %o, readyState %s", policy, document.readyState); // DEV_ONLY
this.setup(policy);
if (this.canScript && blockedScripts.length && originalState === "loading") {
log("Running suspended scripts which are permitted by %s policy.", url);
// something went wrong, e.g. with session restore.
if (url.startsWith("file:") && !localPolicy) {
stop();
sessionStorage.setItem(localPolicyKey, JSON.stringify(policy));
location.reload(false);
return;
}
for (let s of blockedScripts) {
// reinsert the script:
// just s.cloneNode(true) doesn't work, the script wouldn't run,
// let's clone it the hard way...
try {
s.replaceWith(document.createRange().createContextualFragment(s.outerHTML));
} catch (e) {
error(e);
}
}
}
}
for (;;) {
try {
policy = browser.runtime.sendSyncMessage(
{id: "fetchPolicy", url, contextUrl: url}, setup);
break;
} catch (e) {
if (!Messages.isMissingEndpoint(e)) {
error(e);
break;
}
error("Background page not ready yet, retrying to fetch policy...")
}
}
},
setup(policy) {
debug("%s, %s, %o", document.URL, document.readyState, policy);
if (!policy) {
policy = {permissions: {capabilities: []}, localFallback: true};
}
this.policy = policy;
if (!policy.permissions || policy.unrestricted) {
this.allows = () => true;
this.capabilities = Object.assign(
new Set(["script"]), { has() { return true; } });
} else {
let perms = policy.permissions;
this.capabilities = new Set(perms.capabilities);
new DocumentCSP(document).apply(this.capabilities, this.embeddingDocument);
}
this.canScript = this.allows("script");
this.fire("capabilities");
},
policy: null,
allows(cap) {
return this.capabilities && this.capabilities.has(cap);
},
getWindowName() {
return window.name;
}
};
if (this.ns) {
this.ns.merge(ns);
} else {
this.ns = ns;
}
}
|