This commit is contained in:
Younes Aassila 2023-05-12 00:46:07 +02:00
parent 4b5bd5fa2c
commit fc67664985
18 changed files with 90 additions and 143 deletions

View File

@ -1,15 +1,13 @@
import browser from "webextension-polyfill";
import isChrome from "../common/ts/isChrome";
import log from "../common/ts/log";
import onBeforeHlsRequest from "./handlers/onBeforeHlsRequest";
import onBeforeVodRequest from "./handlers/onBeforeVodRequest";
import onBeforeRequest from "./handlers/onBeforeRequest";
import onHeadersReceived from "./handlers/onHeadersReceived";
import onProxyRequest from "./handlers/onProxyRequest";
import onStartupStoreCleanup from "./handlers/onStartupStoreCleanup";
import onStartupUpdateCheck from "./handlers/onStartupUpdateCheck";
import updateProxySettings from "./updateProxySettings";
log("🚀 Background script running.");
console.info("🚀 Background script running.");
// Cleanup the session-related data in the store on startup.
browser.runtime.onStartup.addListener(onStartupStoreCleanup);
@ -23,15 +21,14 @@ if (!isChrome) {
urls: ["https://*.ttvnw.net/*"], // Filtered to video-weaver requests in the handler.
});
// TODO: Map channel names to HLS playlists.
browser.webRequest.onBeforeRequest.addListener(onBeforeHlsRequest, {
urls: ["https://usher.ttvnw.net/api/channel/hls/*"],
});
// TODO: Excluded from proxying.
browser.webRequest.onBeforeRequest.addListener(onBeforeVodRequest, {
urls: ["https://usher.ttvnw.net/vod/*"],
});
// Map channel names to video-weaver URLs.
browser.webRequest.onBeforeRequest.addListener(
onBeforeRequest,
{
urls: ["https://usher.ttvnw.net/api/channel/hls/*"],
},
["blocking"]
);
// Monitor video-weaver responses.
browser.webRequest.onHeadersReceived.addListener(onHeadersReceived, {

View File

@ -1,9 +0,0 @@
import { WebRequest } from "webextension-polyfill";
export default function onBeforeHlsRequest(
details: WebRequest.OnBeforeRequestDetailsType
): void {
// TODO: Get channel name from URL.
// TODO: Filter response data to HLS playlists.
// TODO: Map channel name to HLS playlists.
}

View File

@ -0,0 +1,34 @@
import { WebRequest } from "webextension-polyfill";
import filterResponseDataWrapper from "../../common/ts/filterResponseDataWrapper";
import {
twitchApiChannelNameRegex,
videoWeaverUrlRegex,
} from "../../common/ts/regexes";
import store from "../../store";
export default function onBeforeRequest(
details: WebRequest.OnBeforeRequestDetailsType
): void | WebRequest.BlockingResponseOrPromise {
const match = twitchApiChannelNameRegex.exec(details.url);
if (!match) return;
const channelName = match[1]?.toLowerCase();
if (!channelName) return;
filterResponseDataWrapper(details, text => {
const videoWeaverUrls = text.match(videoWeaverUrlRegex);
if (!videoWeaverUrls) return text;
console.log(
`📺 Found ${videoWeaverUrls.length} video-weaver URLs for ${channelName}.`
);
const existingVideoWeaverUrls =
store.state.videoWeaverUrlsByChannel[channelName] ?? [];
const newVideoWeaverUrls = videoWeaverUrls.filter(
url => !existingVideoWeaverUrls.includes(url)
);
store.state.videoWeaverUrlsByChannel[channelName] = [
...existingVideoWeaverUrls,
...newVideoWeaverUrls,
];
return text;
});
}

View File

@ -1,8 +0,0 @@
import { WebRequest } from "webextension-polyfill";
export default function onBeforeVodRequest(
details: WebRequest.OnBeforeRequestDetailsType
): void {
// TODO: Filter response data to HLS playlists.
// TODO: Exclude HLS playlists from proxying.
}

View File

@ -7,16 +7,18 @@ export default function onHeadersReceived(
details: WebRequest.OnHeadersReceivedDetailsType & {
proxyInfo?: ProxyInfo;
}
): void {
): void | WebRequest.BlockingResponseOrPromise {
// Filter to video-weaver responses.
const host = getHostFromUrl(details.url);
if (!host || !videoWeaverHostRegex.test(host)) return;
const proxyInfo = details.proxyInfo; // Firefox only.
if (!proxyInfo || proxyInfo.type === "direct")
return console.log(`Failed to proxy ${details.url}`);
return console.log(`Did not proxy ${details.url}`);
console.log(
`✅ Proxied ${details.url} through ${proxyInfo.host}:${proxyInfo.port} (${proxyInfo.type})`
);
// TODO: Stream status.
}

View File

@ -1,4 +1,5 @@
import { Proxy } from "webextension-polyfill";
import findChannelFromVideoWeaverUrl from "../../common/ts/findChannelFromVideoWeaverUrl";
import getHostFromUrl from "../../common/ts/getHostFromUrl";
import { videoWeaverHostRegex } from "../../common/ts/regexes";
import store from "../../store";
@ -11,6 +12,19 @@ export default function onProxyRequest(
const host = getHostFromUrl(details.url);
if (!host || !videoWeaverHostRegex.test(host)) return { type: "direct" };
// Check if the channel is whitelisted.
const channelName = findChannelFromVideoWeaverUrl(details.url);
const isWhitelisted = (channelName: string) => {
const whitelistedChannelsLower = store.state.whitelistedChannels.map(
channel => channel.toLowerCase()
);
return whitelistedChannelsLower.includes(channelName.toLowerCase());
};
if (channelName != null && isWhitelisted(channelName)) {
console.log(`✋ Channel ${channelName} is whitelisted.`);
return { type: "direct" };
}
const proxies = store.state.servers;
const proxyInfoArray: ProxyInfo[] = proxies.map(host => {
const [hostname, port] = host.split(":");
@ -20,11 +34,11 @@ export default function onProxyRequest(
port: Number(port) ?? 3128,
} as ProxyInfo;
});
console.log(
`🔄 Proxying ${details.url} through one of: [${proxies.toString()}]`
`⌛ Proxying ${details.url} (${channelName ?? "unknown"}) through one of: ${
proxies.toString() || "<empty>"
}`
);
if (proxyInfoArray.length === 0) return { type: "direct" };
return proxyInfoArray;
}

View File

@ -23,7 +23,9 @@ export default function updateProxySettings() {
};
chrome.proxy.settings.set({ value: config, scope: "regular" }, function () {
console.log(
`Proxying video-weaver requests through one of: [${store.state.servers.toString()}]`
`⚙️ Proxying video-weaver requests through one of: ${
store.state.servers.toString() || "<empty>"
}`
);
});
}

View File

@ -0,0 +1,9 @@
import store from "../../store";
export default function findChannelFromVideoWeaverUrl(videoWeaverUrl: string) {
const channelName = Object.keys(store.state.videoWeaverUrlsByChannel).find(
channelName =>
store.state.videoWeaverUrlsByChannel[channelName].includes(videoWeaverUrl)
);
return channelName ?? null;
}

View File

@ -1,3 +0,0 @@
export default function log(...args: any[]) {
console.log("[TTV LOL PRO]", ...args);
}

View File

@ -1,5 +1,6 @@
export const TWITCH_URL_REGEX =
/^https?:\/\/(?:(?:www|m)\.)?twitch\.tv\/(?:videos\/)?([A-Z0-9][A-Z0-9_]*)/i;
export const TWITCH_API_URL_REGEX = /\/(hls|vod)\/(.+)\.m3u8(?:\?(.*))?$/i;
export const TTV_LOL_API_URL_REGEX = /\/(?:playlist|vod)\/(.+)\.m3u8/i;
export const twitchApiChannelNameRegex = /\/hls\/(.+)\.m3u8/i;
export const twitchWatchPageUrlRegex =
/^https?:\/\/(?:(?:www|m)\.)?twitch\.tv\/(?:videos\/)?(\w+)/i;
export const videoWeaverHostRegex = /^video-weaver\.\w+\.hls\.ttvnw\.net$/i;
export const videoWeaverUrlRegex =
/^https?:\/\/video-weaver\.\w+\.hls\.ttvnw\.net\/v1\/playlist\/.+\.m3u8$/gim;

View File

@ -1,15 +1,14 @@
import log from "../common/ts/log";
import { TWITCH_URL_REGEX } from "../common/ts/regexes";
import { twitchWatchPageUrlRegex } from "../common/ts/regexes";
import store from "../store";
log("🚀 Content script running.");
console.info("[TTV LOL PRO] 🚀 Content script running.");
if (store.readyState === "complete") clearErrors();
else store.addEventListener("load", clearErrors);
// Clear errors for stream on page load/reload.
function clearErrors() {
const match = TWITCH_URL_REGEX.exec(location.href);
const match = twitchWatchPageUrlRegex.exec(location.href);
if (!match) return;
const [, streamId] = match;
if (!streamId) return;

View File

@ -38,6 +38,7 @@
"proxy",
"storage",
"webRequest",
"webRequestBlocking",
"https://*.ttvnw.net/*",
"https://www.twitch.tv/*",
"https://m.twitch.tv/*",

View File

@ -36,10 +36,6 @@ const whitelistedChannelsListElement = $(
$;
// Proxies
const serversListElement = $("#servers-list") as HTMLOListElement;
// Privacy
const disableVodRedirectCheckboxElement = $(
"#disable-vod-redirect-checkbox"
) as HTMLInputElement;
// Ignored channel subscriptions
const ignoredChannelSubscriptionsListElement = $(
"#ignored-channel-subscriptions-list"
@ -84,24 +80,6 @@ function main() {
const checkbox = e.target as HTMLInputElement;
store.state.checkForUpdates = checkbox.checked;
});
// Disable VOD proxying
disableVodRedirectCheckboxElement.checked = store.state.disableVodRedirect;
disableVodRedirectCheckboxElement.addEventListener("change", e => {
const checkbox = e.target as HTMLInputElement;
if (checkbox.checked) {
store.state.disableVodRedirect = checkbox.checked;
} else {
// Ask for confirmation before enabling VOD proxying.
const consent = confirm(
"Are you sure?\n\nYour Twitch token (containing sensitive information) will be sent to TTV LOL's API server when watching VODs."
);
if (consent) {
store.state.disableVodRedirect = checkbox.checked;
} else {
checkbox.checked = true;
}
}
});
// Server list
listInit(serversListElement, "servers", store.state.servers, {
getPromptPlaceholder: insertMode => {
@ -295,9 +273,7 @@ exportButtonElement.addEventListener("click", () => {
"ttv-lol-pro_backup.json",
JSON.stringify({
checkForUpdates: store.state.checkForUpdates,
disableVodRedirect: store.state.disableVodRedirect,
ignoredChannelSubscriptions: store.state.ignoredChannelSubscriptions,
resetPlayerOnMidroll: store.state.resetPlayerOnMidroll,
servers: store.state.servers,
whitelistedChannels: store.state.whitelistedChannels,
}),

View File

@ -64,29 +64,6 @@
</small>
</section>
<section id="privacy-section" class="section" hidden>
<h2>Privacy</h2>
<ul class="options-list">
<li>
<input
type="checkbox"
name="disable-vod-redirect-checkbox"
id="disable-vod-redirect-checkbox"
/>
<label for="disable-vod-redirect-checkbox">
Disable VOD proxying
</label>
<br />
<small>
TTV LOL's API requires your Twitch token (containing sensitive
information) to remove ads from VODs. To protect your privacy, and
since most ad blockers (like uBlock Origin) already remove ads
from VODs, this option is enabled by default.
</small>
</li>
</ul>
</section>
<section
id="ignored-channel-subscriptions-section"
class="section"

View File

@ -1,6 +1,6 @@
import browser from "webextension-polyfill";
import $ from "../common/ts/$";
import { TWITCH_URL_REGEX } from "../common/ts/regexes";
import { twitchWatchPageUrlRegex } from "../common/ts/regexes";
import store from "../store";
import type { StreamStatus } from "../types";
@ -28,7 +28,7 @@ async function main() {
const activeTab = tabs[0];
if (!activeTab || !activeTab.url) return;
const match = TWITCH_URL_REGEX.exec(activeTab.url);
const match = twitchWatchPageUrlRegex.exec(activeTab.url);
if (!match) return;
const [, streamId] = match;
if (!streamId) return;

View File

@ -3,12 +3,12 @@ import type { State } from "./types";
export default function getDefaultState() {
return {
checkForUpdates: false, // No need to check for updates on startup for CRX and XPI installs. The default value is set in the store initializer.
disableVodRedirect: true, // Most ad-blockers already remove ads from VODs (VOD proxying requires a Twitch token).
ignoredChannelSubscriptions: [], // Some channels might show ads even if you're subscribed to them.
isUpdateAvailable: false,
lastUpdateCheck: 0,
servers: [],
streamStatuses: {},
videoWeaverUrlsByChannel: {},
whitelistedChannels: [],
} as State;
}

View File

@ -6,12 +6,12 @@ export type StorageAreaName = "local" | "managed" | "sync";
export interface State {
checkForUpdates: boolean;
disableVodRedirect: boolean;
ignoredChannelSubscriptions: string[];
isUpdateAvailable: boolean;
lastUpdateCheck: number;
servers: string[];
streamStatuses: Record<string, StreamStatus>;
videoWeaverUrlsByChannel: Record<string, string[]>;
whitelistedChannels: string[];
}

View File

@ -15,51 +15,6 @@ export interface StreamStatusError {
status: number;
}
export const enum PlaylistType {
Playlist = "playlist",
VOD = "vod",
}
export interface Token {
adblock?: boolean;
authorization: {
forbidden: boolean;
reason: string;
};
blackout_enabled?: boolean;
channel?: string;
channel_id?: number;
chansub: {
restricted_bitrates?: string[];
view_until: number;
};
ci_gb?: boolean;
geoblock_reason?: string;
device_id?: string;
expires: number;
extended_history_allowed?: boolean;
game?: string;
hide_ads?: boolean;
https_required: boolean;
mature?: boolean;
partner?: boolean;
platform?: string;
player_type?: string;
private?: {
allowed_to_view: boolean;
};
privileged: boolean;
role?: string;
server_ads?: boolean;
show_ads?: boolean;
subscriber?: boolean;
turbo?: boolean;
user_id?: number;
user_ip?: string;
version: number;
vod_id?: number;
}
// From https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/proxy/ProxyInfo
export type ProxyInfo = {
type: "direct" | "http" | "https" | "socks" | "socks4";