Skip to content

Commit

Permalink
⚡️ Optimize requests
Browse files Browse the repository at this point in the history
  • Loading branch information
younesaassila committed May 28, 2023
1 parent 70f913d commit 0cf2245
Show file tree
Hide file tree
Showing 15 changed files with 681 additions and 594 deletions.
882 changes: 441 additions & 441 deletions package-lock.json

Large diffs are not rendered by default.

6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "ttv-lol-pro",
"version": "2.0.0-beta.6",
"version": "2.0.0-beta.7",
"description": "TTV LOL PRO removes livestream ads from Twitch",
"@parcel/bundler-default": {
"minBundles": 10000000,
Expand Down Expand Up @@ -41,13 +41,13 @@
"xhook": "^1.6.0"
},
"devDependencies": {
"@parcel/config-webextension": "^2.9.0",
"@parcel/config-webextension": "^2.9.1",
"@types/chrome": "^0.0.237",
"@types/semver-compare": "^1.0.1",
"@types/webextension-polyfill": "^0.10.0",
"@types/xhook": "^1.5.0",
"cpy-cli": "^4.2.0",
"parcel": "^2.9.0",
"parcel": "^2.9.1",
"postcss": "^8.4.23",
"prettier": "^2.8.8",
"prettier-plugin-css-order": "^1.3.0",
Expand Down
18 changes: 9 additions & 9 deletions src/background/background.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import browser from "webextension-polyfill";
import isChromium from "../common/ts/isChromium";
import updateProxySettings from "../common/ts/updateProxySettings";
import store from "../store";
// import onBeforeSendHeaders from "./handlers/onBeforeSendHeaders";
import onBeforeSendHeaders from "./handlers/onBeforeSendHeaders";
import onBeforeUsherRequest from "./handlers/onBeforeUsherRequest";
import onBeforeVideoWeaverRequest from "./handlers/onBeforeVideoWeaverRequest";
import onHeadersReceived from "./handlers/onHeadersReceived";
Expand Down Expand Up @@ -38,14 +38,14 @@ if (isChromium) {
},
["requestHeaders"]
);
// // Remove the Client-Id flag from requests.
// browser.webRequest.onBeforeSendHeaders.addListener(
// onBeforeSendHeaders,
// {
// urls: ["https://*.ttvnw.net/*", "https://*.twitch.tv/*"],
// },
// ["blocking", "requestHeaders"]
// );
// Remove the Accept flag from requests.
browser.webRequest.onBeforeSendHeaders.addListener(
onBeforeSendHeaders,
{
urls: ["https://*.ttvnw.net/*", "https://*.twitch.tv/*"],
},
["blocking", "requestHeaders"]
);
// Check for ads in video-weaver responses.
browser.webRequest.onBeforeRequest.addListener(
onBeforeVideoWeaverRequest,
Expand Down
10 changes: 5 additions & 5 deletions src/background/handlers/onBeforeSendHeaders.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,19 @@
import { WebRequest } from "webextension-polyfill";
import clientIdFlag from "../../common/ts/clientIdFlag";
import acceptFlag from "../../common/ts/acceptFlag";
import isFlaggedRequest from "../../common/ts/isFlaggedRequest";

export default function onBeforeSendHeaders(
details: WebRequest.OnBeforeSendHeadersDetailsType
): WebRequest.BlockingResponse | Promise<WebRequest.BlockingResponse> {
if (isFlaggedRequest(details.requestHeaders)) {
console.log("🔄 Found flagged request, removing Client-ID header...");
console.log("🔎 Found flagged request, removing flag...");
return {
requestHeaders: details.requestHeaders!.reduce((acc, curr) => {
if (curr.name.toLowerCase() === "client-id") {
if (curr.value === clientIdFlag) return acc; // Remove header.
if (curr.name.toLowerCase() === "accept") {
if (curr.value === acceptFlag) return acc; // Remove header.
acc.push({
name: curr.name,
value: curr.value?.replace(clientIdFlag, ""),
value: curr.value?.replace(acceptFlag, ""),
});
return acc;
}
Expand Down
9 changes: 3 additions & 6 deletions src/background/handlers/onProxyRequest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,8 @@ import { Proxy } from "webextension-polyfill";
import findChannelFromVideoWeaverUrl from "../../common/ts/findChannelFromVideoWeaverUrl";
import getHostFromUrl from "../../common/ts/getHostFromUrl";
import isChannelWhitelisted from "../../common/ts/isChannelWhitelisted";
import {
passportHostRegex,
usherHostRegex,
videoWeaverHostRegex,
} from "../../common/ts/regexes";
import isFlaggedRequest from "../../common/ts/isFlaggedRequest";
import { passportHostRegex, usherHostRegex } from "../../common/ts/regexes";
import store from "../../store";
import type { ProxyInfo } from "../../types";

Expand Down Expand Up @@ -52,7 +49,7 @@ export default async function onProxyRequest(
}

// Video-weaver requests.
if (videoWeaverHostRegex.test(host)) {
if (isFlaggedRequest(details.requestHeaders)) {
const proxies = store.state.videoWeaverProxies;
const proxyInfoArray = getProxyInfoArrayFromHosts(proxies);
// Don't proxy whitelisted channels.
Expand Down
6 changes: 3 additions & 3 deletions src/common/ts/clientIdFlag.ts → src/common/ts/acceptFlag.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// PROXYING SPECIFIC REQUESTS WORKS BY SETTING A FLAG IN THE CLIENT-ID HEADER.
// PROXYING SPECIFIC REQUESTS WORKS BY SETTING A FLAG IN THE ACCEPT HEADER.

// This flag is then caught by the `onProxyRequest` listener, which proxies
// the request, then by the `onBeforeSendHeaders` listener,
Expand All @@ -8,6 +8,6 @@
// `onBeforeSendHeaders` listener, it still caused the CORS preflight request
// to fail.

const clientIdFlag = "TTV-LOL-PRO";
const acceptFlag = "TTV-LOL-PRO";

export default clientIdFlag;
export default acceptFlag;
6 changes: 3 additions & 3 deletions src/common/ts/isFlaggedRequest.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import { WebRequest } from "webextension-polyfill";
import clientIdFlag from "./clientIdFlag";
import acceptFlag from "./acceptFlag";

export default function isFlaggedRequest(
headers: WebRequest.HttpHeaders | undefined
): boolean {
if (!headers) return false;
return headers.some(
header =>
header.name.toLowerCase() === "client-id" &&
header.value?.includes(clientIdFlag)
header.name.toLowerCase() === "accept" &&
header.value?.includes(acceptFlag)
);
}
13 changes: 4 additions & 9 deletions src/content/content.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,16 @@
import pageScript from "url:../page/page.ts";
import store from "../store";
import workerScript from "url:../page/worker.ts";

console.info("[TTV LOL PRO] 🚀 Content script running.");

if (store.readyState === "complete") onStoreReady();
else store.addEventListener("load", onStoreReady);

function onStoreReady() {
if (store.state.proxyUsherRequests) {
injectScript(pageScript);
}
}
injectScript(pageScript);

function injectScript(src: string) {
// From https://stackoverflow.com/a/9517879
const script = document.createElement("script");
script.src = src;
// TODO: Find a better way to pass data to the script, preferably hidden from page.
script.dataset.params = JSON.stringify({ workerScriptURL: workerScript });
script.onload = () => script.remove();
// Note: Despite what the TS types say, `document.head` can be `null`.
(document.head || document.documentElement).append(script);
Expand Down
2 changes: 1 addition & 1 deletion src/manifest.chromium.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"manifest_version": 3,
"name": "TTV LOL PRO",
"description": "TTV LOL PRO removes livestream ads from Twitch.",
"version": "2.0.0.6",
"version": "2.0.0.7",
"background": {
"service_worker": "background/background.ts",
"type": "module"
Expand Down
9 changes: 8 additions & 1 deletion src/manifest.firefox.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"manifest_version": 2,
"name": "TTV LOL PRO",
"description": "TTV LOL PRO removes livestream ads from Twitch.",
"version": "2.0.0.6",
"version": "2.0.0.7",
"background": {
"scripts": ["background/background.ts"],
"persistent": false
Expand All @@ -19,6 +19,13 @@
"id": "ttv-lol-pro@ttv-lol-pro"
}
},
"content_scripts": [
{
"matches": ["https://*.twitch.tv/*"],
"js": ["content/content.ts"],
"run_at": "document_start"
}
],
"icons": {
"128": "images/brand/icon.png"
},
Expand Down
5 changes: 5 additions & 0 deletions src/options/page.html
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,11 @@ <h2>Passport</h2>
Browse Twitch as a TTV LOL PRO citizen. This option enables
advanced proxying to prevent ads.
</small>
<br />
<small>
This option is not an on/off switch. TTV LOL PRO will still
proxy some requests even if this option is disabled.
</small>
</li>
<li id="proxy-twitch-webpage-li">
<input
Expand Down
171 changes: 171 additions & 0 deletions src/page/fetch.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
import acceptFlag from "../common/ts/acceptFlag";
import getHostFromUrl from "../common/ts/getHostFromUrl";
import { videoWeaverHostRegex } from "../common/ts/regexes";

const NATIVE_FETCH = self.fetch;

const knownVideoWeaverUrls = new Set<string>();
const flaggedVideoWeaverUrls = new Map<string, number>(); // URL -> No. of times flagged.

/**
* Converts a HeadersInit to a map.
* @param headers
* @returns
*/
function getHeadersMap(
headers: Headers | HeadersInit | undefined
): Map<string, string> {
if (!headers) return new Map();
if (headers instanceof Headers) {
return new Map(headers.entries());
}
if (Array.isArray(headers)) {
return new Map(headers);
}
return new Map(Object.entries(headers));
}

/**
* Converts a BodyInit to a string.
* @param body
* @returns
*/
async function getRequestBodyText(
body: BodyInit | null | undefined
): Promise<string | null> {
if (!body) return null;
if (body instanceof Blob) {
return body.text();
}
if (body instanceof ArrayBuffer) {
return new TextDecoder().decode(body);
}
if (body instanceof FormData) {
const entries = [...body.entries()];
return entries.map(e => `${e[0]}=${e[1]}`).join("&");
}
return body.toString();
}

function findHeaderFromMap(
headersMap: Map<string, string>,
name: string
): string | undefined {
return [...headersMap.keys()].find(
header => header.toLowerCase() === name.toLowerCase()
);
}

function getHeaderFromMap(
headersMap: Map<string, string>,
name: string
): string | null {
const header = findHeaderFromMap(headersMap, name);
return header != null ? headersMap.get(header)! : null;
}

function setHeaderToMap(
headersMap: Map<string, string>,
name: string,
value: string
) {
const header = findHeaderFromMap(headersMap, name);
headersMap.set(header ?? name, value);
}

function removeHeaderFromMap(headersMap: Map<string, string>, name: string) {
const header = findHeaderFromMap(headersMap, name);
if (header != null) {
headersMap.delete(header);
}
}

function flagRequest(headersMap: Map<string, string>) {
const accept = getHeaderFromMap(headersMap, "Accept");
setHeaderToMap(headersMap, "Accept", `${accept || ""}${acceptFlag}`);
}

function cancelRequest(): never {
throw new Error();
}

export async function fetch(
input: RequestInfo | URL,
init?: RequestInit
): Promise<Response> {
console.debug("[TTV LOL PRO] 🥅 Caught fetch request.");
const url = input instanceof Request ? input.url : input.toString();
const host = getHostFromUrl(url);
const headersMap = getHeadersMap(init?.headers);
// const requestBody = await getRequestBodyText(init?.body);

// Video Weaver requests.
if (host != null && videoWeaverHostRegex.test(host)) {
const isNewUrl = !knownVideoWeaverUrls.has(url);
const isFlaggedUrl = flaggedVideoWeaverUrls.has(url);
if (isNewUrl || isFlaggedUrl) {
console.debug("[TTV LOL PRO] 🥅 Caught new or flagged Video Weaver URL.");
flagRequest(headersMap);
if (isNewUrl) knownVideoWeaverUrls.add(url);
flaggedVideoWeaverUrls.set(
url,
(flaggedVideoWeaverUrls.get(url) ?? 0) + 1
);
}
}

const response = await NATIVE_FETCH(input, {
...init,
headers: Object.fromEntries(headersMap),
});
const clonedResponse = response.clone();

// Video Weaver responses.
if (host != null && videoWeaverHostRegex.test(host)) {
const responseBody = await clonedResponse.text();

if (responseBody.includes("stitched")) {
console.debug(
"[TTV LOL PRO] 🥅 Caught Video Weaver response containing ad."
);
if (!flaggedVideoWeaverUrls.has(url)) {
// Let's proxy the next request for this URL, 2 attempts left.
flaggedVideoWeaverUrls.set(url, 0);
cancelRequest();
}
// 0: First attempt, not proxied, cancelled.
// 1: Second attempt, proxied, cancelled?
// 2: Third attempt, proxied, last attempt by Twitch.
// If the third attempt contains an ad, we have to let it through.
const isCancellable = flaggedVideoWeaverUrls.get(url) < 2;
if (isCancellable) {
cancelRequest();
} else {
console.log(
"[TTV LOL PRO] ❌ Could not cancel Video Weaver response containing ad. All attempts used."
);
flaggedVideoWeaverUrls.set(url, 0); // Reset attempts.
}
} else if (responseBody.includes("twitch-maf-ad")) {
console.debug(
"[TTV LOL PRO] 🥅 Caught Video Weaver response containing twitch-maf-ad."
);
const newReponseBody = responseBody
.split("\n")
.filter(line => {
return !line.includes("twitch-maf-ad");
})
.join("\n");
return new Response(newReponseBody, {
status: response.status,
statusText: response.statusText,
headers: response.headers,
});
} else {
// No ad, remove from flagged list.
flaggedVideoWeaverUrls.delete(url);
}
}

return response;
}
Loading

0 comments on commit 0cf2245

Please sign in to comment.