Files
portmaster/desktop/angular/projects/portmaster-chrome-extension/src/background/tab-tracker.ts

127 lines
3.9 KiB
TypeScript

import { deepClone } from "@safing/portmaster-api";
export interface Request {
/** The ID assigned by the browser */
id: string;
/** The domain this request was for */
domain: string;
/** The timestamp in milliseconds since epoch at which the request was initiated */
time: number;
/** Whether or not this request errored with net::ERR_ADDRESS_UNREACHABLE */
isUnreachable: boolean;
}
/**
* TabTracker tracks requests to domains made by a single browser tab.
*/
export class TabTracker {
/** A list of requests observed for this tab order by time they have been initiated */
private requests: Request[] = [];
/** A lookup map for requests to specific domains */
private byDomain = new Map<string, Request[]>();
/** A lookup map for requests by the chrome request ID */
private byRequestId = new Map<string, Request>;
constructor(public readonly tabId: number) { }
/** Returns an array of all requests observed in this tab. */
allRequests(): Request[] {
return deepClone(this.requests)
}
/** Returns a list of requests that have been observed for domain */
forDomain(domain: string): Request[] {
if (!domain.endsWith(".")) {
domain += "."
}
return this.byDomain.get(domain) || [];
}
/** Call to add the details of a web-request to this tab-tracker */
trackRequest(details: chrome.webRequest.WebRequestDetails) {
// If this is the wrong tab ID ignore the request details
if (details.tabId !== this.tabId) {
console.error(`TabTracker.trackRequest: called with wrong tab ID. Expected ${this.tabId} but got ${details.tabId}`)
return;
}
// if the type of the request is for the main_frame the user switched to a new website.
// In that case, we can wipe out all currently stored requests as the user will likely not
// care anymore.
if (details.type === "main_frame") {
this.clearState();
}
// get the domain of the request normalized to contain the trailing dot.
let domain = new URL(details.url).host;
if (!domain.endsWith(".")) {
domain += "."
}
const req: Request = {
id: details.requestId,
domain: domain,
time: details.timeStamp,
isUnreachable: false, // we don't actually know that yet
}
this.requests.push(req);
this.byRequestId.set(req.id, req)
// Add the request to the by-domain lookup map
let byDomainRequests = this.byDomain.get(req.domain);
if (!byDomainRequests) {
byDomainRequests = [];
this.byDomain.set(req.domain, byDomainRequests)
}
byDomainRequests.push(req)
console.log(`DEBUG: observed request ${req.id} to ${req.domain}`)
}
/** Call to notify the tab-tracker of a request error */
trackError(errorDetails: chrome.webRequest.WebResponseErrorDetails) {
// we only care about net::ERR_ADDRESS_UNREACHABLE here because that's how the
// Portmaster blocks the request.
// TODO(ppacher): docs say we must not rely on that value so we should figure out a better
// way to detect if the error is caused by the Portmaster.
if (errorDetails.error !== "net::ERR_ADDRESS_UNREACHABLE") {
return;
}
// the the previsouly observed request by the request ID.
const req = this.byRequestId.get(errorDetails.requestId)
if (!req) {
console.error("TabTracker.trackError: request has not been observed before")
return
}
// make sure the error details actually happend for the observed tab.
if (errorDetails.tabId !== this.tabId) {
console.error(`TabTracker.trackRequest: called with wrong tab ID. Expected ${this.tabId} but got ${errorDetails.tabId}`)
return;
}
// mark the request as unreachable.
req.isUnreachable = true;
console.log(`DEBUG: marked request ${req.id} to ${req.domain} as unreachable`)
}
/** Clears the current state of the tab tracker */
private clearState() {
this.requests = [];
this.byDomain = new Map();
this.byRequestId = new Map();
}
}