feat(webext): relay via avatar peek/poke + experimental YouTube PO-token capture (ref #401, #402)

- Cookie relay now pokes the avatar credential broker
  (/api/v1/avatar/cred/poke/youtube, JSON) instead of the PeerTube-specific
  endpoint — one broker for all services (#402).
- content/yt-hook.js (page world) + yt-capture.js (content script): hook
  YouTube's fetch/XHR to capture the `pot` PO token + visitor_data, stash in
  extension storage; the relay sends them alongside cookies. EXPERIMENTAL —
  YouTube obfuscates/rotates these, best-effort (P4).
- manifest: content_scripts on youtube.com + web_accessible yt-hook.js.
This commit is contained in:
CyberMind-FR 2026-05-28 12:03:35 +02:00
parent b9c9b918cc
commit 1dd540e86a
5 changed files with 98 additions and 6 deletions

View File

@ -0,0 +1,22 @@
// SecuBox Companion — YouTube PO-token capture (content script, isolated world)
// Injects a page-world hook that watches YouTube's own streaming requests for
// the `pot` (PO token) param + visitor_data, and stashes them in extension
// storage so the popup can relay them to the avatar broker (#402 / #401 P4).
// EXPERIMENTAL: YouTube obfuscates + rotates these; best-effort only.
const api = globalThis.browser ?? globalThis.chrome;
try {
const s = document.createElement("script");
s.src = api.runtime.getURL("content/yt-hook.js");
s.async = false;
(document.head || document.documentElement).appendChild(s);
s.remove();
} catch (e) { /* page CSP may block; nothing we can do */ }
window.addEventListener("message", (e) => {
if (e.source !== window || !e.data || e.data.__secubox !== "yt-pot") return;
const upd = {};
if (e.data.po_token) { upd.yt_po_token = e.data.po_token; upd.yt_po_token_at = Date.now(); }
if (e.data.visitor_data) upd.yt_visitor_data = e.data.visitor_data;
if (Object.keys(upd).length) api.storage.local.set(upd);
});

View File

@ -0,0 +1,47 @@
// SecuBox Companion — YouTube PO-token hook (runs in the PAGE/main world).
// Wraps fetch + XHR to spot YouTube's streaming requests, which carry the
// `pot` (PO token) query param. Relays it (+ visitor_data from ytcfg) to the
// content script via window.postMessage. EXPERIMENTAL + YouTube-fragile.
(function () {
"use strict";
let lastSent = "";
function visitorData() {
try {
if (window.ytcfg && typeof window.ytcfg.get === "function") {
return window.ytcfg.get("VISITOR_DATA") || "";
}
if (window.ytcfg && window.ytcfg.data_) return window.ytcfg.data_.VISITOR_DATA || "";
} catch (e) { /* ignore */ }
return "";
}
function grab(url) {
if (!url) return;
try {
const u = new URL(url, location.href);
if (!/(googlevideo\.com|youtube\.com)/.test(u.hostname)) return;
const pot = u.searchParams.get("pot");
if (pot && pot.length > 20 && pot !== lastSent) {
lastSent = pot;
window.postMessage(
{ __secubox: "yt-pot", po_token: pot, visitor_data: visitorData() }, "*");
}
} catch (e) { /* ignore */ }
}
try {
const origFetch = window.fetch;
if (origFetch) {
window.fetch = function (input) {
try { grab(typeof input === "string" ? input : (input && input.url)); } catch (e) {}
return origFetch.apply(this, arguments);
};
}
const origOpen = XMLHttpRequest.prototype.open;
XMLHttpRequest.prototype.open = function (method, url) {
try { grab(url); } catch (e) {}
return origOpen.apply(this, arguments);
};
} catch (e) { /* ignore */ }
})();

View File

@ -22,6 +22,20 @@
"background": {
"scripts": ["background.js"]
},
"content_scripts": [
{
"matches": ["https://www.youtube.com/*"],
"js": ["content/yt-capture.js"],
"run_at": "document_start",
"all_frames": true
}
],
"web_accessible_resources": [
{
"resources": ["content/yt-hook.js"],
"matches": ["https://www.youtube.com/*"]
}
],
"options_ui": {
"page": "options/options.html",
"open_in_tab": true

View File

@ -4,7 +4,7 @@ const api = globalThis.browser ?? globalThis.chrome;
const DEFAULTS = {
hubBase: "",
modules: ["hub", "crowdsec", "waf", "wireguard", "peertube", "photoprism", "nextcloud"],
cookieEndpoint: "/api/v1/peertube/import/cookies",
cookieEndpoint: "/api/v1/avatar/cred/poke/youtube",
};
async function load() {

View File

@ -5,8 +5,9 @@ const DEFAULTS = {
hubBase: "", // e.g. "https://admin.gk2.secubox.in"
// Modules to probe (name shown + URL slug). Operator can edit in options.
modules: ["hub", "crowdsec", "waf", "wireguard", "peertube", "photoprism", "nextcloud"],
// PeerTube cookie-intake endpoint (backend lands with #401 P1.5 / #388).
cookieEndpoint: "/api/v1/peertube/import/cookies",
// Avatar credential-broker poke endpoint (#402): one place to share browser
// auth with any backend service. youtube → PeerTube.
cookieEndpoint: "/api/v1/avatar/cred/poke/youtube",
// SecuBox login endpoint (secubox-core JWT; any module's /auth/login works).
loginEndpoint: "/api/v1/peertube/auth/login",
};
@ -101,11 +102,19 @@ async function relayCookies(cfg) {
status.textContent = "No youtube.com cookies — log into YouTube in this browser first.";
return;
}
const body = toNetscape(cookies);
status.textContent = `Sending ${cookies.length} cookies…`;
const netscape = toNetscape(cookies);
// Optional PO token captured from a youtube.com tab (P4, experimental).
const { yt_po_token = "", yt_visitor_data = "" } = await api.storage.local.get(
{ yt_po_token: "", yt_visitor_data: "" });
status.textContent = `Poking ${cookies.length} cookies` + (yt_po_token ? " + PO token" : "") + "…";
const r = await fetch(`${cfg.hubBase}${cfg.cookieEndpoint}`, {
method: "POST", credentials: "include",
headers: { "Content-Type": "text/plain", ...(await authHeaders()) }, body,
headers: { "Content-Type": "application/json", ...(await authHeaders()) },
body: JSON.stringify({
cookies: netscape,
po_token: yt_po_token || undefined,
visitor_data: yt_visitor_data || undefined,
}),
});
if (r.status === 401 || r.status === 403) {
status.textContent = "SecuBox login required:";