import browser from "@lumeweb/webextension-polyfill"; import BaseProvider from "./contentProviders/baseProvider.js"; import { BlockingResponse, OnBeforeNavigateDetailsType, OnBeforeRequestDetailsType, OnBeforeSendHeadersDetailsType, OnCompletedDetailsType, OnErrorOccurredDetailsType, OnHeadersReceivedDetailsType, OnRequestDetailsType, } from "./types"; import { getTld, isDomain, isIp, normalizeDomain } from "./util.js"; import tldEnum from "@lumeweb/tld-enum"; import { scanRecords } from "./dns.js"; import { blake2b, bufToHex } from "libskynet"; import { getAuthStatus } from "./main/vars.js"; import { DNSResult } from "@lumeweb/libresolver"; import "./contentFilters/index.js"; export default class WebEngine { private contentProviders: BaseProvider[] = []; private requests: Map = new Map(); private requestData: Map = new Map(); private navigations: Map> = new Map(); private domainContentProvider: Map = new Map(); constructor() { browser.webRequest.onHeadersReceived.addListener( this.headerHandler.bind(this), { urls: [""] }, ["blocking", "responseHeaders"] ); browser.proxy.onRequest.addListener(this.proxyHandler.bind(this), { urls: [""], }); browser.webRequest.onBeforeRequest.addListener( this.requestHandler.bind(this), { urls: [""] }, ["blocking"] ); browser.webRequest.onBeforeSendHeaders.addListener( this.reqHeaderHandler.bind(this), { urls: [""] }, ["requestHeaders", "blocking"] ); browser.webRequest.onCompleted.addListener( this.onCompletedHandler.bind(this), { urls: [""], } ); browser.webRequest.onErrorOccurred.addListener( this.onErrorHandler.bind(this), { urls: [""], } ); browser.webNavigation.onBeforeNavigate.addListener( this.handleNavigationRequest.bind(this) ); } private async headerHandler( details: OnHeadersReceivedDetailsType ): Promise { return this.processHandler(details, "handleHeaders", { responseHeaders: details.responseHeaders, }); } private async proxyHandler(details: OnRequestDetailsType): Promise { let handle = null; for (const provider of this.contentProviders) { if (await provider.shouldHandleRequest(details)) { handle = provider; break; } } if (!handle) { return {}; } this.requests.set(details.requestId, handle); this.domainContentProvider.set(new URL(details.url).hostname, handle); return this.processHandler(details, "handleProxy"); } private async requestHandler( details: OnBeforeRequestDetailsType ): Promise { const navId = this.getNavigationId(details); let navRedirect: boolean | string = false; if (this.navigations.has(navId)) { try { await this.navigations.get(navId); } catch (e: any) { navRedirect = e; } } if (navRedirect && navRedirect !== details.url) { return { redirectUrl: navRedirect as string }; } const domainPatterns = { "eth.link": "eth", "eth.limo": "eth", "hns.is": "", "hns.to": "", } as { [pattern: string]: string }; for (const pattern of Object.keys(domainPatterns)) { if (details.url.includes(pattern)) { return { redirectUrl: details.url.replace(pattern, domainPatterns[pattern]), }; } } const provider = this.getRequestProvider(details.requestId); if (provider) { let urlObj = new URL(details.url); if (urlObj.protocol == "https") { urlObj.protocol = "http"; return { redirectUrl: urlObj.toString() }; } } return this.processHandler(details, "handleRequest"); } private async reqHeaderHandler( details: OnBeforeSendHeadersDetailsType ): Promise { return this.processHandler(details, "handleReqHeaders"); } private async onCompletedHandler( details: OnCompletedDetailsType ): Promise { if (this.requests.has(details.requestId)) { this.requests.delete(details.requestId); } if (this.requestData.has(details.requestId)) { this.requests.delete(details.requestId); } } private async onErrorHandler( details: OnErrorOccurredDetailsType ): Promise { if (this.requests.has(details.requestId)) { this.requests.delete(details.requestId); } if (this.requestData.has(details.requestId)) { this.requests.delete(details.requestId); } } public registerContentProvider(provider: BaseProvider) { if (this.contentProviders.includes(provider)) { return; } this.contentProviders.push(provider); } public getRequestData(requestId: string, key: string) { if (!this.requestData.has(requestId)) { return undefined; } const store: any = this.requestData.get(requestId); return store[key]; } public setRequestData(requestId: string, key: string, value: any) { let store: any = {}; if (this.requestData.has(requestId)) { store = this.requestData.get(requestId); } store[key] = value; this.requestData.set(requestId, store); } private async processHandler( details: any, method: string, def = {} ): Promise { const provider = this.getRequestProvider(details.requestId); if (!provider) { return def; } const response = await provider[method](details); if (response !== false) { return response as BlockingResponse; } return def; } private async handleNavigationRequest(details: OnBeforeNavigateDetailsType) { if (!details.url) { return; } const originalUrl = new URL(details.url); const hostname = normalizeDomain(originalUrl.hostname); if (!isDomain(hostname) || isIp(hostname)) { return; } if (["chrome:"].includes(originalUrl.protocol)) { return; } if (!["google.com", "www.google.com"].includes(hostname)) { return; } if ( !( originalUrl.searchParams.has("client") && originalUrl.searchParams.get("client")?.includes("firefox") ) ) { return; } let queriedUrl = originalUrl.searchParams.get("q") as string; if (!queriedUrl.includes("://")) { queriedUrl = `http://${queriedUrl}`; } let queriedHost = queriedUrl; try { let queriedUrlUbj = new URL(queriedUrl); queriedHost = queriedUrlUbj.hostname; } catch {} if (tldEnum.list.includes(getTld(queriedHost))) { return false; } if (isIp(queriedHost)) { return; } if (/[\s_]/.test(queriedHost)) { return; } let dnsResult: boolean | DNSResult = false; let resolveRequest: any, rejectRequest: any; let promise = new Promise((resolve, reject) => { resolveRequest = resolve; rejectRequest = reject; }); this.navigations.set(this.getNavigationId(details), promise); if ("kernel.lume" === queriedHost) { if (!queriedUrl.includes("://")) { queriedUrl = `http://${queriedUrl}`; } rejectRequest(queriedUrl); return; } if (getAuthStatus().loginComplete !== true) { resolveRequest(); return; } try { dnsResult = await scanRecords(queriedHost); } catch (e) { resolveRequest(); return; } if (!dnsResult) { resolveRequest(); return; } if (!queriedUrl.includes("://")) { queriedUrl = `http://${queriedUrl}`; } rejectRequest(queriedUrl); } private getNavigationId(details: any) { return `${details.tabId}_${bufToHex( blake2b(new TextEncoder().encode(details.url)) )}`; } public getDomainContentProvider(domain: string): BaseProvider | null { return this.domainContentProvider.get(domain) ?? null; } private getRequestProvider( requestId: string ): { [p: string]: Function } | null { const provider = this.requests.get(requestId) as unknown as { [index: string]: Function; }; return provider ?? null; } }