@freedomofpress/webcat 0.0.0-stage → 3.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Readme.md +45 -0
- package/data/block.json +101 -0
- package/data/list.json +80 -0
- package/dist/hooks/content.js +1 -0
- package/dist/lib/browser/chrome.d.ts +86 -0
- package/dist/lib/browser/chrome.js +138 -0
- package/dist/lib/browser/kvstore.d.ts +70 -0
- package/dist/lib/browser/kvstore.js +241 -0
- package/dist/lib/browser/permissions.d.ts +113 -0
- package/dist/lib/browser/permissions.js +182 -0
- package/dist/lib/browser/requests.d.ts +145 -0
- package/dist/lib/browser/requests.js +292 -0
- package/dist/lib/browser/scripting.d.ts +19 -0
- package/dist/lib/browser/scripting.js +92 -0
- package/dist/lib/browser/sync.d.ts +68 -0
- package/dist/lib/browser/sync.js +114 -0
- package/dist/lib/browser/utils.d.ts +5 -0
- package/dist/lib/browser/utils.js +11 -0
- package/dist/lib/config.d.ts +45 -0
- package/dist/lib/config.js +45 -0
- package/dist/lib/validator_set.d.ts +4 -0
- package/dist/lib/validator_set.js +20 -0
- package/dist/lib/webcat/cache.d.ts +200 -0
- package/dist/lib/webcat/cache.js +308 -0
- package/dist/lib/webcat/canonicalize.d.ts +1 -0
- package/dist/lib/webcat/canonicalize.js +58 -0
- package/dist/lib/webcat/db.d.ts +30 -0
- package/dist/lib/webcat/db.js +131 -0
- package/dist/lib/webcat/encoding.d.ts +8 -0
- package/dist/lib/webcat/encoding.js +57 -0
- package/dist/lib/webcat/handler.d.ts +26 -0
- package/dist/lib/webcat/handler.js +419 -0
- package/dist/lib/webcat/hookbuilder.d.ts +47 -0
- package/dist/lib/webcat/hookbuilder.js +170 -0
- package/dist/lib/webcat/interfaces/bundle.d.ts +46 -0
- package/dist/lib/webcat/interfaces/bundle.js +8 -0
- package/dist/lib/webcat/interfaces/database.d.ts +68 -0
- package/dist/lib/webcat/interfaces/database.js +1 -0
- package/dist/lib/webcat/interfaces/errors.d.ts +451 -0
- package/dist/lib/webcat/interfaces/errors.js +109 -0
- package/dist/lib/webcat/interfaces/log.d.ts +14 -0
- package/dist/lib/webcat/interfaces/log.js +10 -0
- package/dist/lib/webcat/interfaces/originstate.d.ts +40 -0
- package/dist/lib/webcat/interfaces/originstate.js +1 -0
- package/dist/lib/webcat/interfaces/requeststate.d.ts +10 -0
- package/dist/lib/webcat/interfaces/requeststate.js +1 -0
- package/dist/lib/webcat/logger.d.ts +64 -0
- package/dist/lib/webcat/logger.js +112 -0
- package/dist/lib/webcat/originstate.d.ts +45 -0
- package/dist/lib/webcat/originstate.js +302 -0
- package/dist/lib/webcat/parsers.d.ts +21 -0
- package/dist/lib/webcat/parsers.js +36 -0
- package/dist/lib/webcat/request.d.ts +11 -0
- package/dist/lib/webcat/request.js +78 -0
- package/dist/lib/webcat/resources.d.ts +3 -0
- package/dist/lib/webcat/resources.js +41 -0
- package/dist/lib/webcat/response.d.ts +39 -0
- package/dist/lib/webcat/response.js +394 -0
- package/dist/lib/webcat/ui.d.ts +70 -0
- package/dist/lib/webcat/ui.js +282 -0
- package/dist/lib/webcat/updater.d.ts +115 -0
- package/dist/lib/webcat/updater.js +296 -0
- package/dist/lib/webcat/utils.d.ts +8 -0
- package/dist/lib/webcat/utils.js +74 -0
- package/dist/lib/webcat/validators.d.ts +16 -0
- package/dist/lib/webcat/validators.js +457 -0
- package/dist/lib/webcat.d.ts +11 -0
- package/dist/lib/webcat.js +130 -0
- package/icons/dark/webcat-error.SVG +1 -0
- package/icons/dark/webcat-ok.SVG +1 -0
- package/icons/dark/webcat.SVG +1 -0
- package/icons/light/webcat-error.SVG +1 -0
- package/icons/light/webcat-ok.SVG +1 -0
- package/icons/light/webcat.SVG +1 -0
- package/package.json +72 -4
- package/pages/error.css +9 -0
- package/pages/error.html +70 -0
- package/pages/error.js +51 -0
- package/pages/popup.css +16 -0
- package/pages/popup.html +14 -0
- package/pages/popup.js +48 -0
- package/README.md +0 -3
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
const ALL_TYPES = [
|
|
2
|
+
"main_frame",
|
|
3
|
+
"sub_frame",
|
|
4
|
+
"stylesheet",
|
|
5
|
+
"script",
|
|
6
|
+
"image",
|
|
7
|
+
"font",
|
|
8
|
+
"object",
|
|
9
|
+
"xmlhttprequest",
|
|
10
|
+
"ping",
|
|
11
|
+
"csp_report",
|
|
12
|
+
"media",
|
|
13
|
+
"websocket",
|
|
14
|
+
"other",
|
|
15
|
+
"xslt",
|
|
16
|
+
"beacon",
|
|
17
|
+
"imageset",
|
|
18
|
+
"speculative",
|
|
19
|
+
"web_manifest"
|
|
20
|
+
];
|
|
21
|
+
const FRAME_TYPES = [
|
|
22
|
+
"main_frame",
|
|
23
|
+
"sub_frame"
|
|
24
|
+
];
|
|
25
|
+
const NON_FRAME_TYPES = ALL_TYPES.filter((t) => !FRAME_TYPES.includes(t));
|
|
26
|
+
const PASS_THROUGH_TYPES = /* @__PURE__ */ new Set([
|
|
27
|
+
"image",
|
|
28
|
+
"imageset",
|
|
29
|
+
"media",
|
|
30
|
+
"font",
|
|
31
|
+
"websocket",
|
|
32
|
+
"xmlhttprequest",
|
|
33
|
+
"ping",
|
|
34
|
+
"speculative",
|
|
35
|
+
"other"
|
|
36
|
+
]);
|
|
37
|
+
export {
|
|
38
|
+
FRAME_TYPES,
|
|
39
|
+
NON_FRAME_TYPES,
|
|
40
|
+
PASS_THROUGH_TYPES
|
|
41
|
+
};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { BeforeRequestDetails, HeadersReceivedDetails } from "../browser/requests";
|
|
2
|
+
import { HookBuilder } from "./hookbuilder";
|
|
3
|
+
import { Database } from "./interfaces/database";
|
|
4
|
+
import { WebcatError } from "./interfaces/errors";
|
|
5
|
+
import { Stateful } from "./interfaces/requeststate";
|
|
6
|
+
import { WebcatUI } from "./ui";
|
|
7
|
+
/**
|
|
8
|
+
* Validates responses from WEBCAT-enrolled origins.
|
|
9
|
+
*/
|
|
10
|
+
export declare class ResponseValidator {
|
|
11
|
+
#private;
|
|
12
|
+
constructor(db: Database, ui: WebcatUI, hooks: HookBuilder);
|
|
13
|
+
/**
|
|
14
|
+
* Validates response headers.
|
|
15
|
+
*
|
|
16
|
+
* @param details The request details object to validate.
|
|
17
|
+
* @returns A Promise that resolves to undefined on successful validation, or
|
|
18
|
+
* to a {@link WebcatError} on validation failure.
|
|
19
|
+
*/
|
|
20
|
+
validateHeaders(details: Stateful<HeadersReceivedDetails>): Promise<WebcatError | undefined>;
|
|
21
|
+
/** @internal */
|
|
22
|
+
extractAndValidateHeaders(details: Stateful<HeadersReceivedDetails>): Map<string, string> | WebcatError;
|
|
23
|
+
/**
|
|
24
|
+
* Validates response content.
|
|
25
|
+
*
|
|
26
|
+
* @param details The request details object to validate.
|
|
27
|
+
*/
|
|
28
|
+
validateContent(details: Stateful<BeforeRequestDetails>): Promise<void>;
|
|
29
|
+
/**
|
|
30
|
+
* Marks the start of response content by injecting a secret token. Must be
|
|
31
|
+
* called *later* than {@link validateContent}, which will remove the marker.
|
|
32
|
+
* The counter-intuitive ordering is inherited from
|
|
33
|
+
* {@link browser.webRequest.StreamFilter | StreamFilter} behavior.
|
|
34
|
+
*
|
|
35
|
+
* @param details
|
|
36
|
+
* @returns
|
|
37
|
+
*/
|
|
38
|
+
markContent(details: HeadersReceivedDetails): void;
|
|
39
|
+
}
|
|
@@ -0,0 +1,394 @@
|
|
|
1
|
+
var __typeError = (msg) => {
|
|
2
|
+
throw TypeError(msg);
|
|
3
|
+
};
|
|
4
|
+
var __accessCheck = (obj, member, msg) => member.has(obj) || __typeError("Cannot " + msg);
|
|
5
|
+
var __privateGet = (obj, member, getter) => (__accessCheck(obj, member, "read from private field"), getter ? getter.call(obj) : member.get(obj));
|
|
6
|
+
var __privateAdd = (obj, member, value) => member.has(obj) ? __typeError("Cannot add the same private member more than once") : member instanceof WeakSet ? member.add(obj) : member.set(obj, value);
|
|
7
|
+
var __privateSet = (obj, member, value, setter) => (__accessCheck(obj, member, "write to private field"), setter ? setter.call(obj, value) : member.set(obj, value), value);
|
|
8
|
+
var _marker, _db, _ui, _hooks;
|
|
9
|
+
import permissions from "../browser/permissions.js";
|
|
10
|
+
import { CacheKey } from "./cache.js";
|
|
11
|
+
import { stringToUint8Array, Uint8ArrayToBase64Url, Uint8ArrayToString, base64UrlToUint8Array } from "./encoding.js";
|
|
12
|
+
import { WebcatError, WebcatErrorCode } from "./interfaces/errors.js";
|
|
13
|
+
import { logger } from "./logger.js";
|
|
14
|
+
import { PASS_THROUGH_TYPES } from "./resources.js";
|
|
15
|
+
import { isNewerSemver, clearBrowserCaches, isSameOriginURL, arraysEqual, SHA256 } from "./utils.js";
|
|
16
|
+
import "../browser/sync.js";
|
|
17
|
+
var __create = Object.create;
|
|
18
|
+
var __defProp = Object.defineProperty;
|
|
19
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
20
|
+
var __knownSymbol = (name, symbol) => (symbol = Symbol[name]) ? symbol : Symbol.for("Symbol." + name);
|
|
21
|
+
var __typeError2 = (msg) => {
|
|
22
|
+
throw TypeError(msg);
|
|
23
|
+
};
|
|
24
|
+
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
|
|
25
|
+
var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
|
|
26
|
+
var __decoratorStart = (base) => [, , , __create(null)];
|
|
27
|
+
var __decoratorStrings = ["class", "method", "getter", "setter", "accessor", "field", "value", "get", "set"];
|
|
28
|
+
var __expectFn = (fn) => fn !== void 0 && typeof fn !== "function" ? __typeError2("Function expected") : fn;
|
|
29
|
+
var __decoratorContext = (kind, name, done, metadata, fns) => ({ kind: __decoratorStrings[kind], name, metadata, addInitializer: (fn) => done._ ? __typeError2("Already initialized") : fns.push(__expectFn(fn || null)) });
|
|
30
|
+
var __decoratorMetadata = (array, target) => __defNormalProp(target, __knownSymbol("metadata"), array[3]);
|
|
31
|
+
var __runInitializers = (array, flags, self, value) => {
|
|
32
|
+
for (var i = 0, fns = array[flags >> 1], n = fns && fns.length; i < n; i++) fns[i].call(self);
|
|
33
|
+
return value;
|
|
34
|
+
};
|
|
35
|
+
var __decorateElement = (array, flags, name, decorators, target, extra) => {
|
|
36
|
+
var it, done, ctx, k = flags & 7, p = false;
|
|
37
|
+
var j = 0;
|
|
38
|
+
var extraInitializers = array[j] || (array[j] = []);
|
|
39
|
+
var desc = k && (target = target.prototype, k < 5 && (k > 3 || !p) && __getOwnPropDesc(target, name));
|
|
40
|
+
__name(target, name);
|
|
41
|
+
for (var i = decorators.length - 1; i >= 0; i--) {
|
|
42
|
+
ctx = __decoratorContext(k, name, done = {}, array[3], extraInitializers);
|
|
43
|
+
it = (0, decorators[i])(target, ctx), done._ = 1;
|
|
44
|
+
__expectFn(it) && (target = it);
|
|
45
|
+
}
|
|
46
|
+
return __decoratorMetadata(array, target), desc && __defProp(target, name, desc), p ? k ^ 4 ? extra : desc : target;
|
|
47
|
+
};
|
|
48
|
+
var _ResponseValidator_decorators, _init;
|
|
49
|
+
const WORKER_FETCH_DESTINATIONS = [
|
|
50
|
+
"worker",
|
|
51
|
+
"serviceworker",
|
|
52
|
+
"sharedworker",
|
|
53
|
+
"audioworklet",
|
|
54
|
+
"paintworklet"
|
|
55
|
+
];
|
|
56
|
+
function assertVerifiedManifest(originState) {
|
|
57
|
+
if (!originState.isManifestVerified()) {
|
|
58
|
+
throw new Error("origin is not populated when it was expected");
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
function assertHeadersAvailable(details) {
|
|
62
|
+
if (!("responseHeaders" in details)) {
|
|
63
|
+
throw new Error("response headers not available when expected");
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
_ResponseValidator_decorators = [permissions.require("webRequest"), permissions.require("webRequestBlocking"), permissions.require("webRequestFilterResponse"), permissions.require("webRequestFilterResponse.serviceWorkerScript")];
|
|
67
|
+
class ResponseValidator {
|
|
68
|
+
constructor(db, ui, hooks) {
|
|
69
|
+
// #marker is ephemeral, not persisted anywhere, but that's ok:
|
|
70
|
+
// ResponseValidator attaches a StreamFilter that prevents the
|
|
71
|
+
// background service worker from terminating while a response
|
|
72
|
+
// is being processed
|
|
73
|
+
__privateAdd(this, _marker, stringToUint8Array(
|
|
74
|
+
`__WEBCAT_END__{${Uint8ArrayToBase64Url(crypto.getRandomValues(new Uint8Array(32)))}}
|
|
75
|
+
`
|
|
76
|
+
));
|
|
77
|
+
__privateAdd(this, _db);
|
|
78
|
+
__privateAdd(this, _ui);
|
|
79
|
+
__privateAdd(this, _hooks);
|
|
80
|
+
__privateSet(this, _db, db);
|
|
81
|
+
__privateSet(this, _ui, ui);
|
|
82
|
+
__privateSet(this, _hooks, hooks);
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Validates response headers.
|
|
86
|
+
*
|
|
87
|
+
* @param details The request details object to validate.
|
|
88
|
+
* @returns A Promise that resolves to undefined on successful validation, or
|
|
89
|
+
* to a {@link WebcatError} on validation failure.
|
|
90
|
+
*/
|
|
91
|
+
async validateHeaders(details) {
|
|
92
|
+
if (!details.state.pendingOrigin) {
|
|
93
|
+
throw new Error("missing pendingOrigin in request state");
|
|
94
|
+
}
|
|
95
|
+
logger.info(`Validating response headers, url: ${details.url}`, details);
|
|
96
|
+
const result = this.extractAndValidateHeaders(details);
|
|
97
|
+
if (result instanceof WebcatError) {
|
|
98
|
+
return result;
|
|
99
|
+
}
|
|
100
|
+
const normalizedHeaders = result;
|
|
101
|
+
const csp = normalizedHeaders.get("content-security-policy");
|
|
102
|
+
const version = normalizedHeaders.get("x-webcat-version");
|
|
103
|
+
const delegation = normalizedHeaders.get("x-webcat-delegation");
|
|
104
|
+
const enrollment_header = normalizedHeaders.get("x-webcat-enrollment");
|
|
105
|
+
if (details.state.pendingOrigin.isRequestSent()) {
|
|
106
|
+
let enrollment;
|
|
107
|
+
if (enrollment_header) {
|
|
108
|
+
try {
|
|
109
|
+
enrollment = JSON.parse(
|
|
110
|
+
Uint8ArrayToString(base64UrlToUint8Array(enrollment_header))
|
|
111
|
+
);
|
|
112
|
+
} catch {
|
|
113
|
+
return new WebcatError(WebcatErrorCode.Headers.ENROLLMENT_MALFORMED);
|
|
114
|
+
}
|
|
115
|
+
await details.state.pendingOrigin.verifyEnrollment(
|
|
116
|
+
enrollment,
|
|
117
|
+
delegation
|
|
118
|
+
);
|
|
119
|
+
} else {
|
|
120
|
+
await details.state.pendingOrigin.verifyEnrollment(
|
|
121
|
+
void 0,
|
|
122
|
+
delegation
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
if (details.state.pendingOrigin.isFailed()) {
|
|
126
|
+
return details.state.pendingOrigin.error;
|
|
127
|
+
}
|
|
128
|
+
logger.debug("Header parsing complete", details);
|
|
129
|
+
await details.state.pendingOrigin.verifyManifest();
|
|
130
|
+
if (details.state.pendingOrigin.isFailed()) {
|
|
131
|
+
return details.state.pendingOrigin.error;
|
|
132
|
+
}
|
|
133
|
+
if (!details.state.pendingOrigin.isManifestVerified()) {
|
|
134
|
+
throw new Error(
|
|
135
|
+
`Error with the origin state: expected origin to be in state verified_manifest`
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
logger.info(`Metadata for ${details.url} loaded`, details);
|
|
139
|
+
}
|
|
140
|
+
if (!details.state.pendingOrigin.isManifestVerified()) {
|
|
141
|
+
throw new Error(
|
|
142
|
+
"Validating headers, but no valid manifest for the origin has been found."
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
if (version && isNewerSemver(version, details.state.pendingOrigin.manifest.version)) {
|
|
146
|
+
logger.info(
|
|
147
|
+
`Detected new version ${version}, current_version ${details.state.pendingOrigin.manifest.version}`,
|
|
148
|
+
details
|
|
149
|
+
);
|
|
150
|
+
await __privateGet(this, _db).origins.delete(
|
|
151
|
+
CacheKey(details.state.fqdn, details.state.cachePartition)
|
|
152
|
+
);
|
|
153
|
+
details.state.pendingOrigin.stale = true;
|
|
154
|
+
await clearBrowserCaches([details.state.fqdn]);
|
|
155
|
+
browser.tabs.reload(details.tabId);
|
|
156
|
+
}
|
|
157
|
+
const pathname = new URL(details.url).pathname;
|
|
158
|
+
if (csp) {
|
|
159
|
+
if (!details.state.pendingOrigin.verifyCSP(csp, pathname)) {
|
|
160
|
+
return new WebcatError(WebcatErrorCode.CSP.MISMATCH, [
|
|
161
|
+
String(pathname)
|
|
162
|
+
]);
|
|
163
|
+
}
|
|
164
|
+
logger.info(`CSP validated for path ${pathname}`, details);
|
|
165
|
+
} else if (details.fromCache === true || details.statusCode === 304) {
|
|
166
|
+
logger.debug(
|
|
167
|
+
`Skipping CSP check for cached/304 response on path ${pathname}`,
|
|
168
|
+
details
|
|
169
|
+
);
|
|
170
|
+
} else {
|
|
171
|
+
return new WebcatError(WebcatErrorCode.Headers.MISSING_CRITICAL, [
|
|
172
|
+
"content-security-policy"
|
|
173
|
+
]);
|
|
174
|
+
}
|
|
175
|
+
if (details.type === "main_frame") {
|
|
176
|
+
__privateGet(this, _ui).showOKIcon(
|
|
177
|
+
details.tabId,
|
|
178
|
+
details.state.pendingOrigin.delegation
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
/** @internal */
|
|
183
|
+
extractAndValidateHeaders(details) {
|
|
184
|
+
if (!details.responseHeaders) {
|
|
185
|
+
return new WebcatError(WebcatErrorCode.Headers.MISSING);
|
|
186
|
+
}
|
|
187
|
+
const criticalHeaders = /* @__PURE__ */ new Set(["content-security-policy"]);
|
|
188
|
+
const forbiddenHeaders = /* @__PURE__ */ new Set([
|
|
189
|
+
// See https://github.com/freedomofpress/webcat/issues/23
|
|
190
|
+
// Furthermore, as reported by TBD there's the risk of TBD
|
|
191
|
+
//"location",
|
|
192
|
+
// See https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Refresh
|
|
193
|
+
// It's just another way to achieve redirects
|
|
194
|
+
"refresh",
|
|
195
|
+
// See https://github.com/freedomofpress/webcat/issues/24
|
|
196
|
+
"link"
|
|
197
|
+
]);
|
|
198
|
+
const seenCriticalHeaders = /* @__PURE__ */ new Set();
|
|
199
|
+
const normalizedHeaders = /* @__PURE__ */ new Map();
|
|
200
|
+
for (const header of details.responseHeaders) {
|
|
201
|
+
if (header.name && header.value) {
|
|
202
|
+
const lowerName = header.name.toLowerCase();
|
|
203
|
+
const value = header.value;
|
|
204
|
+
if (lowerName === "location") {
|
|
205
|
+
if (!details.state.isFrame) {
|
|
206
|
+
return new WebcatError(
|
|
207
|
+
WebcatErrorCode.Headers.LOCATION_SUBRESOURCE,
|
|
208
|
+
[String(value)]
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
if (!isSameOriginURL(value, details.url)) {
|
|
212
|
+
return new WebcatError(WebcatErrorCode.Headers.LOCATION_EXTERNAL, [
|
|
213
|
+
String(value)
|
|
214
|
+
]);
|
|
215
|
+
}
|
|
216
|
+
} else if (lowerName === "origin-agent-cluster" && value.trim() !== "?1") {
|
|
217
|
+
return new WebcatError(WebcatErrorCode.Headers.FORBIDDEN, [
|
|
218
|
+
`${lowerName}: ${value}`
|
|
219
|
+
]);
|
|
220
|
+
} else if (forbiddenHeaders.has(lowerName)) {
|
|
221
|
+
return new WebcatError(WebcatErrorCode.Headers.FORBIDDEN, [
|
|
222
|
+
String(lowerName)
|
|
223
|
+
]);
|
|
224
|
+
}
|
|
225
|
+
if (criticalHeaders.has(lowerName)) {
|
|
226
|
+
if (seenCriticalHeaders.has(lowerName)) {
|
|
227
|
+
return new WebcatError(WebcatErrorCode.Headers.DUPLICATE, [
|
|
228
|
+
String(lowerName)
|
|
229
|
+
]);
|
|
230
|
+
}
|
|
231
|
+
seenCriticalHeaders.add(lowerName);
|
|
232
|
+
}
|
|
233
|
+
normalizedHeaders.set(lowerName, header.value);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
if (details.fromCache !== true && details.statusCode !== 304) {
|
|
237
|
+
for (const criticalHeader of criticalHeaders) {
|
|
238
|
+
if (!normalizedHeaders.has(criticalHeader)) {
|
|
239
|
+
return new WebcatError(WebcatErrorCode.Headers.MISSING_CRITICAL, [
|
|
240
|
+
String(criticalHeader)
|
|
241
|
+
]);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
return normalizedHeaders;
|
|
246
|
+
}
|
|
247
|
+
/**
|
|
248
|
+
* Validates response content.
|
|
249
|
+
*
|
|
250
|
+
* @param details The request details object to validate.
|
|
251
|
+
*/
|
|
252
|
+
async validateContent(details) {
|
|
253
|
+
function deny(filter2) {
|
|
254
|
+
filter2.write(new Uint8Array([68, 69, 78, 73, 69, 68]));
|
|
255
|
+
}
|
|
256
|
+
const pathname = new URL(details.url).pathname;
|
|
257
|
+
const originState = details.state.pendingOrigin;
|
|
258
|
+
if (!originState) {
|
|
259
|
+
throw new Error("missing pendingOrigin in request state");
|
|
260
|
+
}
|
|
261
|
+
let manifest;
|
|
262
|
+
const filter = browser.webRequest.filterResponseData(details.requestId);
|
|
263
|
+
const source = [];
|
|
264
|
+
filter.onstart = () => {
|
|
265
|
+
assertVerifiedManifest(originState);
|
|
266
|
+
assertHeadersAvailable(details);
|
|
267
|
+
manifest = originState.manifest;
|
|
268
|
+
if (!manifest.files[pathname] && !(pathname.endsWith("/") && manifest.files[pathname + manifest.default_index]) && PASS_THROUGH_TYPES.has(details.type)) {
|
|
269
|
+
filter.disconnect();
|
|
270
|
+
}
|
|
271
|
+
if (details.type === "script" && details.requestHeaders) {
|
|
272
|
+
for (const header of details.requestHeaders) {
|
|
273
|
+
if (header.name.toLowerCase() === "sec-fetch-dest" && header.value !== void 0) {
|
|
274
|
+
if (WORKER_FETCH_DESTINATIONS.includes(header.value)) {
|
|
275
|
+
const hooks = __privateGet(this, _hooks).getPageHooks(
|
|
276
|
+
manifest.wasm,
|
|
277
|
+
details.state.cachePartition.firstParty,
|
|
278
|
+
// Hooks are only injected to workers, and CSP restrictions only allow
|
|
279
|
+
// same-origin workers, so the request's web origin is the URL's origin;
|
|
280
|
+
// determine whether the URL is same-origin with the first party
|
|
281
|
+
details.state.cachePartition.firstParty === new URL(details.url).origin
|
|
282
|
+
);
|
|
283
|
+
source.push(hooks.then((h) => stringToUint8Array(h).buffer));
|
|
284
|
+
}
|
|
285
|
+
break;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
};
|
|
290
|
+
let writeQueue = Promise.resolve();
|
|
291
|
+
let markerSeen = false;
|
|
292
|
+
filter.ondata = (event) => {
|
|
293
|
+
if (arraysEqual(__privateGet(this, _marker), new Uint8Array(event.data))) {
|
|
294
|
+
source.forEach((hook) => {
|
|
295
|
+
writeQueue = writeQueue.then(async () => filter.write(await hook));
|
|
296
|
+
});
|
|
297
|
+
source.length = 0;
|
|
298
|
+
markerSeen = true;
|
|
299
|
+
} else {
|
|
300
|
+
source.push(Promise.resolve(event.data));
|
|
301
|
+
}
|
|
302
|
+
};
|
|
303
|
+
filter.onstop = async () => {
|
|
304
|
+
if (!markerSeen && !PASS_THROUGH_TYPES.has(details.type)) {
|
|
305
|
+
filter.close();
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
try {
|
|
309
|
+
if (details.tabId !== -1) {
|
|
310
|
+
await details.completed;
|
|
311
|
+
}
|
|
312
|
+
} catch {
|
|
313
|
+
logger.warn(`Request canceled, url: ${details.url}`, details);
|
|
314
|
+
filter.close();
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
const blob = await new Blob(await Promise.all(source)).arrayBuffer();
|
|
318
|
+
let manifest_hash;
|
|
319
|
+
if (manifest.files[pathname]) {
|
|
320
|
+
manifest_hash = manifest.files[pathname];
|
|
321
|
+
} else if (pathname.endsWith("/")) {
|
|
322
|
+
manifest_hash = manifest.files[pathname + manifest.default_index];
|
|
323
|
+
}
|
|
324
|
+
if (!manifest_hash && details.type === "main_frame") {
|
|
325
|
+
manifest_hash = manifest.files[manifest.default_fallback];
|
|
326
|
+
}
|
|
327
|
+
if (!manifest_hash) {
|
|
328
|
+
deny(filter);
|
|
329
|
+
filter.close();
|
|
330
|
+
__privateGet(this, _ui).showErrorPage(
|
|
331
|
+
details,
|
|
332
|
+
new WebcatError(WebcatErrorCode.File.MISSING, [pathname]),
|
|
333
|
+
true
|
|
334
|
+
);
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
const content_hash = await SHA256(blob);
|
|
338
|
+
if (!arraysEqual(
|
|
339
|
+
base64UrlToUint8Array(manifest_hash),
|
|
340
|
+
new Uint8Array(content_hash)
|
|
341
|
+
) && blob.byteLength !== 0) {
|
|
342
|
+
deny(filter);
|
|
343
|
+
filter.close();
|
|
344
|
+
__privateGet(this, _ui).showErrorPage(
|
|
345
|
+
details,
|
|
346
|
+
new WebcatError(WebcatErrorCode.File.MISMATCH, [
|
|
347
|
+
pathname,
|
|
348
|
+
String(manifest_hash),
|
|
349
|
+
String(Uint8ArrayToBase64Url(new Uint8Array(content_hash)))
|
|
350
|
+
]),
|
|
351
|
+
true
|
|
352
|
+
);
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
355
|
+
logger.info(`${pathname} verified.`, details);
|
|
356
|
+
await writeQueue;
|
|
357
|
+
filter.write(blob);
|
|
358
|
+
filter.close();
|
|
359
|
+
if (details.type === "main_frame") {
|
|
360
|
+
__privateGet(this, _ui).showOKIcon(details.tabId, originState.delegation);
|
|
361
|
+
}
|
|
362
|
+
};
|
|
363
|
+
}
|
|
364
|
+
/**
|
|
365
|
+
* Marks the start of response content by injecting a secret token. Must be
|
|
366
|
+
* called *later* than {@link validateContent}, which will remove the marker.
|
|
367
|
+
* The counter-intuitive ordering is inherited from
|
|
368
|
+
* {@link browser.webRequest.StreamFilter | StreamFilter} behavior.
|
|
369
|
+
*
|
|
370
|
+
* @param details
|
|
371
|
+
* @returns
|
|
372
|
+
*/
|
|
373
|
+
markContent(details) {
|
|
374
|
+
if (PASS_THROUGH_TYPES.has(details.type)) return;
|
|
375
|
+
const endMarkerInjector = browser.webRequest.filterResponseData(
|
|
376
|
+
details.requestId
|
|
377
|
+
);
|
|
378
|
+
endMarkerInjector.onstart = () => {
|
|
379
|
+
endMarkerInjector.write(__privateGet(this, _marker));
|
|
380
|
+
};
|
|
381
|
+
endMarkerInjector.ondata = (event) => endMarkerInjector.write(event.data);
|
|
382
|
+
endMarkerInjector.onstop = () => endMarkerInjector.close();
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
_marker = new WeakMap();
|
|
386
|
+
_db = new WeakMap();
|
|
387
|
+
_ui = new WeakMap();
|
|
388
|
+
_hooks = new WeakMap();
|
|
389
|
+
_init = __decoratorStart();
|
|
390
|
+
ResponseValidator = __decorateElement(_init, 0, "ResponseValidator", _ResponseValidator_decorators, ResponseValidator);
|
|
391
|
+
__runInitializers(_init, 1, ResponseValidator);
|
|
392
|
+
export {
|
|
393
|
+
ResponseValidator
|
|
394
|
+
};
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { BrowserChromeController, BrowserChromeControllerConfig, IconOptions } from "../browser/chrome";
|
|
2
|
+
import { NamespacedKVStore } from "../browser/kvstore";
|
|
3
|
+
import { RequestDetails } from "../browser/requests";
|
|
4
|
+
import { Database } from "./interfaces/database";
|
|
5
|
+
import { WebcatError } from "./interfaces/errors";
|
|
6
|
+
import { Stateful } from "./interfaces/requeststate";
|
|
7
|
+
/**
|
|
8
|
+
* Manages the UI for WEBCAT.
|
|
9
|
+
*/
|
|
10
|
+
export declare class WebcatUI extends BrowserChromeController {
|
|
11
|
+
#private;
|
|
12
|
+
/**
|
|
13
|
+
* @param db The database to load enrollments from.
|
|
14
|
+
* @param store The store to use for persistence.
|
|
15
|
+
* @param config
|
|
16
|
+
*/
|
|
17
|
+
constructor(db: Database, store: NamespacedKVStore, config: BrowserChromeControllerConfig);
|
|
18
|
+
/**
|
|
19
|
+
* Displays the default WEBCAT icon in the browser's URL bar. If the UI has
|
|
20
|
+
* warnings, shows the warning icon instead.
|
|
21
|
+
*
|
|
22
|
+
* @param tabId The ID of the tab to display the icon in.
|
|
23
|
+
* @param options Icon options.
|
|
24
|
+
*/
|
|
25
|
+
showIcon(tabId: number, options?: IconOptions): Promise<void>;
|
|
26
|
+
/**
|
|
27
|
+
* Displays the warning icon in the browser's URL bar. Clicking on the icon
|
|
28
|
+
* opens the popup with full warning info.
|
|
29
|
+
*
|
|
30
|
+
* @param tabId The ID of the tab to display the icon in.
|
|
31
|
+
*/
|
|
32
|
+
showWarningIcon(tabId: number): Promise<void>;
|
|
33
|
+
/**
|
|
34
|
+
* Displays the OK icon in the browser's URL bar. If the UI has warnings,
|
|
35
|
+
* shows the warning icon instead.
|
|
36
|
+
*
|
|
37
|
+
* @param tabId The ID of the tab to display the icon in.
|
|
38
|
+
* @param delegation Optional delegation information to include in the title
|
|
39
|
+
* text.
|
|
40
|
+
*/
|
|
41
|
+
showOKIcon(tabId: number, delegation?: string): Promise<void>;
|
|
42
|
+
/**
|
|
43
|
+
* Displays the error icon in the browser's URL bar.
|
|
44
|
+
*
|
|
45
|
+
* @param tabId The ID of the tab to display the icon in.
|
|
46
|
+
*/
|
|
47
|
+
showErrorIcon(tabId: number): Promise<void>;
|
|
48
|
+
/**
|
|
49
|
+
* Loads the error page in the tab(s) indicated by details.
|
|
50
|
+
*
|
|
51
|
+
* @param details Details for the request that triggered the error.
|
|
52
|
+
* @param error The error information.
|
|
53
|
+
* @param replace true to replace the current history entry, false to add a
|
|
54
|
+
* new one.
|
|
55
|
+
*/
|
|
56
|
+
showErrorPage(details: Stateful<RequestDetails>, error: WebcatError, replace?: boolean): Promise<void>;
|
|
57
|
+
/**
|
|
58
|
+
* Sets the message for the specified warning.
|
|
59
|
+
*
|
|
60
|
+
* @param id The ID of the warning.
|
|
61
|
+
* @param args The arguments of the warning. Must be serializable as JSON.
|
|
62
|
+
*/
|
|
63
|
+
setWarning(id: string, ...args: unknown[]): Promise<void>;
|
|
64
|
+
/**
|
|
65
|
+
* Clears a specific warning from the UI.
|
|
66
|
+
*
|
|
67
|
+
* @param id The ID of the warning to clear.
|
|
68
|
+
*/
|
|
69
|
+
clearWarning(id: string): Promise<void>;
|
|
70
|
+
}
|