| | |
| | | const TARGET = "BV1HA3o6oEJJ"; |
| | | const CANONICAL_URL = "https://www.bilibili.com/video/BV1HA3o6oEJJ"; |
| | | const TARGET_PATH = "/video/BV1HA3o6oEJJ"; |
| | | const HOST = "com.project_info.bili_auth_ingress"; |
| | | const SCHEMA = 2; |
| | | const EXTENSION_BUILD = "project-info-bili-auth-ingress/1.0.0+20260805.v002"; |
| | | const HOST_BUILD = "project-info-bili-auth-native-host/1.0.0+20260805.v002"; |
| | | const SIDEPANEL_URL = chrome.runtime.getURL("sidepanel.html"); |
| | | const EXPECTED_DURATION_MS = 3133950; |
| | | const DURATION_TOLERANCE_MS = 3134; |
| | | const HOST_NAME = "com.project_info.bili_auth_ingress"; |
| | | const SCHEMA = 3; |
| | | const EXTENSION_BUILD = "project-info-bili-auth-ingress/1.2.25+20260829.generic.v027"; |
| | | const OWNED_TAB_STORAGE_KEY = "bili_auth_job_owned_tab_v1"; |
| | | const OWNED_TAB_SCHEMA = 1; |
| | | const BVID_RE = /^BV1[1-9A-HJ-NP-Za-km-z]{9}$/; |
| | | const CREATOR_RE = /^[1-9][0-9]{0,19}$/; |
| | | const HEX32_RE = /^[0-9a-f]{32}$/; |
| | | const HEX64_RE = /^[0-9a-f]{64}$/; |
| | | const UPPER_HEX64_RE = /^[0-9A-F]{64}$/; |
| | | const MESSAGE_ID_RE = /^msg_[0-9]{17}_[0-9a-f]{8}$/; |
| | | const HANDOFF_ID_RE = /^HANDOFF-[A-Z0-9-]{8,240}$/; |
| | | const AUDIT_ID_RE = /^DEV-AUDIT-[A-Z0-9-]{8,240}$/; |
| | | const ERROR_CODE_RE = /^E_[A-Z0-9_]{1,48}$/; |
| | | const SAFE_PHASES = new Set([ |
| | | "IDLE", "READY", "CHECKING", "DOWNLOADING", "MERGING", "VALIDATING", |
| | | "PUBLISHING", "MEDIA_COMPLETE", "POSTPROCESS_PENDING", "POSTPROCESS_FAILED", |
| | | "COMPLETE", "FAILED", "CANCELED", "RELOAD_REQUIRED" |
| | | ]); |
| | | const RESPONSE_KEYS = [ |
| | | "error_code", "formal_filename", "host_build", "job", "lease_id", "maintenance", |
| | | "mapping_filename", "phase", "prepare_id", "progress", "schema", "type" |
| | | ]; |
| | | const JOB_KEYS = ["bvid", "canonical_url", "creator_uid", "expected_duration_ms", "job_id", "published_at", "title"]; |
| | | const LINEAGE_KEYS = [ |
| | | "authorization_handoff_id", "authorization_message_id", "authorization_sha256", |
| | | "predecessor_job_id", "predecessor_terminal_error_code", "repair_audit_bytes", |
| | | "repair_audit_id", "repair_audit_sha256", "repair_review_result_message_id", "retry_generation" |
| | | ]; |
| | | const PREPARELESS_TERMINAL_DIAGNOSTIC_CONTRACT = new Map([ |
| | | ["E_DRM", ["PAGE_REJECTED", new Set(["PAGE_DRM_REJECTED"])]], |
| | | ["E_DURATION", ["PAGE_REJECTED", new Set(["PAGE_DURATION_REJECTED"])]], |
| | | ["E_MULTI_PART", ["PAGE_REJECTED", new Set(["PAGE_MULTIPART_REJECTED"])]], |
| | | ["E_OWNER", ["PAGE_REJECTED", new Set(["PAGE_IDENTITY_REJECTED", "PAGE_OWNER_REJECTED"])]], |
| | | ["E_PAGE_ACCESS_CONTROL", ["PAGE_REJECTED", new Set(["PAGE_ACCESS_REJECTED"])]], |
| | | ["E_PAGE_DUPLICATE_TAB", ["TAB_IDENTITY", new Set(["TAB_IDENTITY_DRIFT"])]], |
| | | ["E_PAGE_DIMENSIONS_UNAVAILABLE", ["DIMENSIONS_PENDING", new Set(["VIDEO_DIMENSIONS_PENDING"])]], |
| | | ["E_PAGE_IDENTITY_UNAVAILABLE", ["IDENTITY_PENDING", new Set([ |
| | | "PAGE_DOM_IDENTITY_PENDING", "PAGE_NAVIGATION_PENDING", "PAGE_SCRIPTING_PENDING", |
| | | "PAGE_TAB_ACTIVATION_PENDING", "PAGE_URL_PENDING", "PAGE_VISIBILITY_PENDING", |
| | | "PAGE_WINDOW_FOCUS_PENDING", "PAGE_WINDOW_MINIMIZED_PENDING", "PAGE_WINDOW_STATE_PENDING" |
| | | ])]], |
| | | ["E_PAGE_METADATA_UNAVAILABLE", ["METADATA_NOT_READY", new Set([ |
| | | "INITIAL_STATE_ASSIGNMENT_ABSENT", "INITIAL_STATE_JSON_INVALID", |
| | | "INITIAL_STATE_VIDEO_DATA_ABSENT" |
| | | ])]], |
| | | ["E_PAGE_OWNER_ANCHOR_UNAVAILABLE", ["OWNER_PENDING", new Set([ |
| | | "OWNER_ANCHOR_ABSENT", "OWNER_ANCHOR_AMBIGUOUS", "OWNER_ANCHOR_MISMATCH" |
| | | ])]], |
| | | ["E_PAGE_PROOF", ["PAGE_REJECTED", new Set([ |
| | | "PAGE_BVID_REJECTED", "PAGE_IDENTITY_REJECTED", "PAGE_ORIGIN_REJECTED", "PAGE_URL_REJECTED" |
| | | ])]], |
| | | ["E_PAGE_SCRIPT_TIMEOUT", ["SCRIPT_TIMEOUT", new Set(["SCRIPT_INVOCATION_FAILED"])]], |
| | | ["E_PAGE_STABILITY_TIMEOUT", ["READY_UNSTABLE", new Set(["STABLE_SNAPSHOTS_PENDING"])]], |
| | | ["E_PAGE_TAB_DRIFT", ["TAB_IDENTITY", new Set(["TAB_IDENTITY_DRIFT"])]], |
| | | ["E_PAGE_VIDEO_ABSENT", ["VIDEO_ABSENT", new Set(["VIDEO_ELEMENT_ABSENT"])]], |
| | | ]); |
| | | const PREPARELESS_REJECT_CODES = new Set(PREPARELESS_TERMINAL_DIAGNOSTIC_CONTRACT.keys()); |
| | | const PAGE_PROOF_DEADLINE_MS = 60000; |
| | | const PAGE_EXTENSION_ACTIVATION_GRACE_MS = 5000; |
| | | const PAGE_PROOF_INVOCATION_TIMEOUT_MS = 5000; |
| | | const PAGE_PROOF_POLL_MS = 500; |
| | | const PAGE_PROOF_STABLE_SNAPSHOTS = 3; |
| | | const PAGE_PROOF_KEYS = [ |
| | | "bvid", "canonical_url", "creator_uid", "eme_present", "job_id", "observed_at_unix_ms", |
| | | "metadata_source", "observed_duration_ms", "ready_state", "task_nonce", "video_height", "video_width" |
| | | ]; |
| | | const PAGE_PENDING_STATES = new Set([ |
| | | "DIMENSIONS_PENDING", "IDENTITY_PENDING", "METADATA_NOT_READY", "OWNER_PENDING", "VIDEO_ABSENT" |
| | | ]); |
| | | const PAGE_DIAGNOSTIC_STATES = new Set([ |
| | | ...PAGE_PENDING_STATES, "PAGE_REJECTED", "READY_UNSTABLE", "SCRIPT_TIMEOUT", "TAB_IDENTITY" |
| | | ]); |
| | | const PAGE_DIAGNOSTIC_REASONS = new Set([ |
| | | "DEADLINE_EXHAUSTED", "METADATA_FIELDS_PENDING", "OWNER_ANCHOR_ABSENT", |
| | | "OWNER_ANCHOR_AMBIGUOUS", "OWNER_ANCHOR_MISMATCH", |
| | | "INITIAL_STATE_ASSIGNMENT_ABSENT", "INITIAL_STATE_JSON_INVALID", |
| | | "INITIAL_STATE_VIDEO_DATA_ABSENT", |
| | | "PAGE_ACCESS_REJECTED", "PAGE_BVID_REJECTED", "PAGE_DOM_IDENTITY_PENDING", |
| | | "PAGE_DRM_REJECTED", "PAGE_DURATION_REJECTED", "PAGE_IDENTITY_REJECTED", |
| | | "PAGE_MULTIPART_REJECTED", "PAGE_NAVIGATION_PENDING", "PAGE_ORIGIN_REJECTED", |
| | | "PAGE_OWNER_REJECTED", "PAGE_SCRIPTING_PENDING", "PAGE_TAB_ACTIVATION_PENDING", |
| | | "PAGE_URL_PENDING", "PAGE_URL_REJECTED", "PAGE_VISIBILITY_PENDING", |
| | | "PAGE_WINDOW_FOCUS_PENDING", "PAGE_WINDOW_MINIMIZED_PENDING", "PAGE_WINDOW_STATE_PENDING", |
| | | "SCRIPT_INVOCATION_FAILED", "STABLE_SNAPSHOTS_PENDING", |
| | | "TAB_IDENTITY_DRIFT", "VIDEO_DIMENSIONS_PENDING", "VIDEO_ELEMENT_ABSENT" |
| | | ]); |
| | | const PAGE_METADATA_UNAVAILABLE_REASONS = new Set([ |
| | | "INITIAL_STATE_ASSIGNMENT_ABSENT", "INITIAL_STATE_JSON_INVALID", |
| | | "INITIAL_STATE_VIDEO_DATA_ABSENT" |
| | | ]); |
| | | const PAGE_PENDING_TERMINAL_CONTRACT = new Map([ |
| | | ["E_PAGE_DIMENSIONS_UNAVAILABLE", ["DIMENSIONS_PENDING", new Set(["VIDEO_DIMENSIONS_PENDING"])]], |
| | | ["E_PAGE_IDENTITY_UNAVAILABLE", ["IDENTITY_PENDING", new Set([ |
| | | "PAGE_DOM_IDENTITY_PENDING", "PAGE_NAVIGATION_PENDING", "PAGE_SCRIPTING_PENDING", |
| | | "PAGE_TAB_ACTIVATION_PENDING", "PAGE_URL_PENDING", "PAGE_VISIBILITY_PENDING", |
| | | "PAGE_WINDOW_FOCUS_PENDING", "PAGE_WINDOW_MINIMIZED_PENDING", "PAGE_WINDOW_STATE_PENDING" |
| | | ])]], |
| | | ["E_PAGE_METADATA_NOT_READY", ["METADATA_NOT_READY", PAGE_METADATA_UNAVAILABLE_REASONS]], |
| | | ["E_PAGE_OWNER_ANCHOR_UNAVAILABLE", ["OWNER_PENDING", new Set([ |
| | | "OWNER_ANCHOR_ABSENT", "OWNER_ANCHOR_AMBIGUOUS", "OWNER_ANCHOR_MISMATCH" |
| | | ])]], |
| | | ["E_PAGE_VIDEO_ABSENT", ["VIDEO_ABSENT", new Set(["VIDEO_ELEMENT_ABSENT"])]], |
| | | ]); |
| | | const MAX_COOKIE_COUNT = 128; |
| | | const COOKIE_KEYS = [ |
| | | "domain", "expiration_unix", "host_only", "http_only", "name", "partition_key", |
| | | "path", "same_site", "secure", "session", "store_id", "value" |
| | | ]; |
| | | const COOKIE_SAME_SITE = new Set(["no_restriction", "lax", "strict", "unspecified"]); |
| | | const COOKIE_CONTROL_RE = /\p{C}/u; |
| | | const STORE_ID_RE = /^[0-9]{1,8}$/; |
| | | const AUTH_COOKIE_NAME = "SESSDATA"; |
| | | const REQUIRED_AUTH_COOKIE_NAMES = new Set([AUTH_COOKIE_NAME]); |
| | | const TRANSFER_COOKIE_NAMES = new Set([AUTH_COOKIE_NAME]); |
| | | const COOKIE_ACCESS_REASONS = new Set([ |
| | | "COOKIE_API_ERROR", "COOKIE_API_SHAPE", "COOKIE_AUTH_SET_EMPTY", |
| | | "COOKIE_EQUAL_PRECEDENCE_CONFLICT", |
| | | "COOKIE_RELEVANCE_AMBIGUOUS", "COOKIE_SELECTED_INVALID", |
| | | "COOKIE_SELECTION_EXCEPTION", "COOKIE_TRANSFER_INVALID", |
| | | "COOKIE_TRANSFER_OVERFLOW" |
| | | ]); |
| | | let drainLease = false; |
| | | let lastDrainErrorCode = null; |
| | | let lastCookieAccessReason = null; |
| | | let lastTabDriftReason = null; |
| | | let lastPageReadinessDiagnostic = null; |
| | | let lastExtensionDiagnostic = null; |
| | | let lastBrowserLifecycleDiagnostic = null; |
| | | |
| | | let currentProof = null; |
| | | let currentTabId = null; |
| | | let currentTaskNonce = null; |
| | | let nativePort = null; |
| | | let nativeReady = false; |
| | | let nativeRevision = 0; |
| | | let lastNativeType = null; |
| | | let preparedTaskNonce = null; |
| | | let preparedLeaseId = null; |
| | | let activeStartLease = null; |
| | | const prepareResponses = new Map(); |
| | | let safeState = { |
| | | phase: "IDLE", |
| | | progress: 0, |
| | | error_code: null, |
| | | formal_filename: null, |
| | | mapping_filename: null |
| | | }; |
| | | const BROWSER_LIFECYCLE_DIAGNOSTIC_CONTRACT = new Map([ |
| | | ["OWNED_TAB_RECORDED", "BROWSER_OWNERSHIP"], |
| | | ["OWNED_TAB_ADOPTED", "BROWSER_OWNERSHIP"], |
| | | ["OWNED_TAB_RECORD_INVALID", "BROWSER_OWNERSHIP"], |
| | | ["OWNED_TAB_STORAGE_FAILED", "BROWSER_OWNERSHIP"], |
| | | ["OWNED_TAB_CLOSED", "BROWSER_CLEANUP"], |
| | | ["OWNED_TAB_ALREADY_CLOSED", "BROWSER_CLEANUP"], |
| | | ["OWNED_TAB_IDENTITY_DRIFT", "BROWSER_CLEANUP"], |
| | | ["OWNED_TAB_CLOSE_FAILED", "BROWSER_CLEANUP"], |
| | | ["OWNED_TAB_AUTOPLAY_STOPPED", "BROWSER_CLEANUP"] |
| | | ]); |
| | | |
| | | chrome.runtime.onInstalled.addListener(() => { |
| | | chrome.sidePanel.setPanelBehavior({openPanelOnActionClick: true}); |
| | | }); |
| | | const EXTENSION_DIAGNOSTIC_CONTRACT = new Map([ |
| | | ["DOCUMENT_FOCUS_FALSE", "SCRIPTING"], |
| | | ["DOCUMENT_FOCUS_TRUE", "SCRIPTING"], |
| | | ["DOCUMENT_FOCUS_UNAVAILABLE", "SCRIPTING"], |
| | | ["NATIVE_CONNECT_FAILED", "NATIVE_MESSAGING"], |
| | | ["NATIVE_DISCONNECT_FAILED", "NATIVE_MESSAGING"], |
| | | ["SCRIPTING_EXECUTE_FAILED", "SCRIPTING"], |
| | | ["SERVICE_WORKER_ERROR", "SERVICE_WORKER"], |
| | | ["SERVICE_WORKER_UNHANDLED_REJECTION", "SERVICE_WORKER"], |
| | | ["TABS_CREATE_FAILED", "TABS"], |
| | | ["TABS_GET_FAILED", "TABS"], |
| | | ["TABS_QUERY_FAILED", "TABS"], |
| | | ["TABS_UPDATE_FAILED", "TABS"], |
| | | ["WINDOWS_GET_FAILED", "WINDOWS"], |
| | | ["WINDOWS_UPDATE_FAILED", "WINDOWS"], |
| | | ["WINDOW_FOCUS_FALSE", "WINDOWS"], |
| | | ["WINDOW_FOCUS_TRUE", "WINDOWS"], |
| | | ["DOCUMENT_VISIBILITY_HIDDEN", "SCRIPTING"], |
| | | ["DOCUMENT_VISIBILITY_VISIBLE", "SCRIPTING"], |
| | | ["OS_FOREGROUND_LAUNCH_FAILED", "NATIVE_FOREGROUND"], |
| | | ["OS_FOREGROUND_LOCKED", "NATIVE_FOREGROUND"], |
| | | ["OS_FOREGROUND_PLATFORM_UNSUPPORTED", "NATIVE_FOREGROUND"], |
| | | ["OS_FOREGROUND_READY", "NATIVE_FOREGROUND"], |
| | | ["OS_FOREGROUND_REPLAY", "NATIVE_FOREGROUND"], |
| | | ["OS_FOREGROUND_WINDOW_ABSENT", "NATIVE_FOREGROUND"], |
| | | ["OS_FOREGROUND_WINDOW_AMBIGUOUS", "NATIVE_FOREGROUND"] |
| | | ]); |
| | | const DOCUMENT_FOCUS_DIAGNOSTICS = new Set([ |
| | | "DOCUMENT_FOCUS_FALSE", "DOCUMENT_FOCUS_TRUE", "DOCUMENT_FOCUS_UNAVAILABLE" |
| | | ]); |
| | | const DOCUMENT_VISIBILITY_DIAGNOSTICS = new Set([ |
| | | "DOCUMENT_VISIBILITY_HIDDEN", "DOCUMENT_VISIBILITY_VISIBLE" |
| | | ]); |
| | | const FOREGROUND_ERROR_DIAGNOSTICS = new Map([ |
| | | ["E_FOREGROUND_LAUNCH_FAILED", "OS_FOREGROUND_LAUNCH_FAILED"], |
| | | ["E_FOREGROUND_LOCKED", "OS_FOREGROUND_LOCKED"], |
| | | ["E_FOREGROUND_PLATFORM_UNSUPPORTED", "OS_FOREGROUND_PLATFORM_UNSUPPORTED"], |
| | | ["E_FOREGROUND_REPLAY", "OS_FOREGROUND_REPLAY"], |
| | | ["E_FOREGROUND_WINDOW_ABSENT", "OS_FOREGROUND_WINDOW_ABSENT"], |
| | | ["E_FOREGROUND_WINDOW_AMBIGUOUS", "OS_FOREGROUND_WINDOW_AMBIGUOUS"] |
| | | ]); |
| | | const FOREGROUND_ELIGIBLE_CODES = new Set([ |
| | | "E_PAGE_DIMENSIONS_UNAVAILABLE", "E_PAGE_IDENTITY_UNAVAILABLE", |
| | | "E_PAGE_METADATA_UNAVAILABLE", "E_PAGE_OWNER_ANCHOR_UNAVAILABLE", |
| | | "E_PAGE_SCRIPT_TIMEOUT", "E_PAGE_STABILITY_TIMEOUT", "E_PAGE_VIDEO_ABSENT" |
| | | ]); |
| | | |
| | | function randomNonce() { |
| | | const bytes = new Uint8Array(16); |
| | | crypto.getRandomValues(bytes); |
| | | return Array.from(bytes, value => value.toString(16).padStart(2, "0")).join(""); |
| | | function recordExtensionDiagnostic(reason) { |
| | | const phase = EXTENSION_DIAGNOSTIC_CONTRACT.get(reason); |
| | | if (typeof phase !== "string") return; |
| | | lastExtensionDiagnostic = Object.freeze({phase, reason}); |
| | | } |
| | | |
| | | function fixedError(code) { |
| | | function recordBrowserLifecycleDiagnostic(reason) { |
| | | const phase = BROWSER_LIFECYCLE_DIAGNOSTIC_CONTRACT.get(reason); |
| | | if (typeof phase !== "string") return; |
| | | lastBrowserLifecycleDiagnostic = Object.freeze({phase, reason}); |
| | | } |
| | | |
| | | function exactOwnedTabRecord(value) { |
| | | const keys = [ |
| | | "schema", "job_id", "lease_id", "bvid", "tab_id", "window_id", "cookie_store_id" |
| | | ]; |
| | | if (!isPlainObject(value) || !exactKeys(value, keys) || value.schema !== OWNED_TAB_SCHEMA || |
| | | !HEX64_RE.test(value.job_id || "") || !HEX32_RE.test(value.lease_id || "") || |
| | | !BVID_RE.test(value.bvid || "") || !Number.isInteger(value.tab_id) || value.tab_id < 0 || |
| | | !Number.isInteger(value.window_id) || value.window_id < 0 || |
| | | !STORE_ID_RE.test(value.cookie_store_id || "")) return null; |
| | | return Object.freeze({...value}); |
| | | } |
| | | |
| | | async function storageSessionCall(methodName, args) { |
| | | if (!chrome.storage?.session) throw new Error("E_OWNED_TAB_STORAGE"); |
| | | return chromeApiCall( |
| | | chrome.storage.session, methodName, args, |
| | | "E_OWNED_TAB_STORAGE", "OWNED_TAB_STORAGE_FAILED" |
| | | ); |
| | | } |
| | | |
| | | async function loadOwnedTabRecord() { |
| | | try { |
| | | const value = await storageSessionCall("get", [OWNED_TAB_STORAGE_KEY]); |
| | | const raw = isPlainObject(value) ? value[OWNED_TAB_STORAGE_KEY] : null; |
| | | if (raw === undefined || raw === null) return null; |
| | | const record = exactOwnedTabRecord(raw); |
| | | if (record !== null) return record; |
| | | recordBrowserLifecycleDiagnostic("OWNED_TAB_RECORD_INVALID"); |
| | | await storageSessionCall("remove", [OWNED_TAB_STORAGE_KEY]); |
| | | } catch { |
| | | recordBrowserLifecycleDiagnostic("OWNED_TAB_STORAGE_FAILED"); |
| | | } |
| | | return null; |
| | | } |
| | | |
| | | async function persistOwnedTabRecord(job, leaseId, binding) { |
| | | const record = exactOwnedTabRecord({ |
| | | schema: OWNED_TAB_SCHEMA, job_id: job.job_id, lease_id: leaseId, bvid: job.bvid, |
| | | tab_id: binding.tabId, window_id: binding.windowId, cookie_store_id: binding.cookieStoreId |
| | | }); |
| | | if (record === null || binding.managed !== true) throw new Error("E_OWNED_TAB_STORAGE"); |
| | | try { |
| | | await storageSessionCall("set", [{[OWNED_TAB_STORAGE_KEY]: record}]); |
| | | } catch { |
| | | recordBrowserLifecycleDiagnostic("OWNED_TAB_STORAGE_FAILED"); |
| | | throw new Error("E_OWNED_TAB_STORAGE"); |
| | | } |
| | | recordBrowserLifecycleDiagnostic("OWNED_TAB_RECORDED"); |
| | | return record; |
| | | } |
| | | |
| | | async function clearOwnedTabRecord() { |
| | | try { |
| | | await storageSessionCall("remove", [OWNED_TAB_STORAGE_KEY]); |
| | | return true; |
| | | } catch { |
| | | recordBrowserLifecycleDiagnostic("OWNED_TAB_STORAGE_FAILED"); |
| | | return false; |
| | | } |
| | | } |
| | | |
| | | async function cleanupOwnedTab(recordValue = null) { |
| | | const record = exactOwnedTabRecord(recordValue) || await loadOwnedTabRecord(); |
| | | if (record === null) return true; |
| | | let tab; |
| | | try { |
| | | tab = await chromeApiCall( |
| | | chrome.tabs, "get", [record.tab_id], "E_OWNED_TAB_CLOSE", "TABS_GET_FAILED" |
| | | ); |
| | | } catch { |
| | | await clearOwnedTabRecord(); |
| | | recordBrowserLifecycleDiagnostic("OWNED_TAB_ALREADY_CLOSED"); |
| | | return true; |
| | | } |
| | | if (!isPlainObject(tab) || tab.id !== record.tab_id || tab.windowId !== record.window_id || |
| | | String(tab.cookieStoreId ?? "0") !== record.cookie_store_id || |
| | | (!locationMatches(tab.url, record.bvid) && !locationMatches(tab.pendingUrl, record.bvid) && |
| | | !isManagedIdentityPendingUrl(tab.url, record.bvid))) { |
| | | await clearOwnedTabRecord(); |
| | | recordBrowserLifecycleDiagnostic("OWNED_TAB_IDENTITY_DRIFT"); |
| | | return false; |
| | | } |
| | | // Retire the durable ownership capability before the non-atomic remove. A |
| | | // failed remove must never leave a record that a restarted service worker |
| | | // could apply to a same-id/same-BVID user replacement. |
| | | if (!await clearOwnedTabRecord()) { |
| | | recordBrowserLifecycleDiagnostic("OWNED_TAB_CLOSE_FAILED"); |
| | | return false; |
| | | } |
| | | try { |
| | | await chromeApiCall( |
| | | chrome.tabs, "remove", [record.tab_id], "E_OWNED_TAB_CLOSE", "TABS_REMOVE_FAILED" |
| | | ); |
| | | recordBrowserLifecycleDiagnostic("OWNED_TAB_CLOSED"); |
| | | return true; |
| | | } catch { |
| | | // tabs.remove is not an atomic compare-and-close operation. Once it fails, |
| | | // the numeric tab id may already name a replacement user tab, including a |
| | | // replacement on the same BVID. No subsequent tabs.update/remove mutation |
| | | // can prove object ownership. The durable capability was already retired, |
| | | // so surface only a fixed, sanitized lifecycle result and never retry it. |
| | | recordBrowserLifecycleDiagnostic("OWNED_TAB_CLOSE_FAILED"); |
| | | return false; |
| | | } |
| | | } |
| | | |
| | | function extensionApiError(code, reason) { |
| | | recordExtensionDiagnostic(reason); |
| | | return new Error(code); |
| | | } |
| | | |
| | | function chromeApiCall(receiver, methodName, args, code, reason) { |
| | | return new Promise((resolve, reject) => { |
| | | let settled = false; |
| | | const settle = (error, value) => { |
| | | if (settled) return; |
| | | settled = true; |
| | | if (error) reject(extensionApiError(code, reason)); else resolve(value); |
| | | }; |
| | | const callback = value => { |
| | | let failed = false; |
| | | try { failed = chrome.runtime.lastError != null; } catch { failed = true; } |
| | | settle(failed, value); |
| | | }; |
| | | let returned; |
| | | try { |
| | | returned = receiver[methodName](...args, callback); |
| | | } catch { |
| | | settle(true); |
| | | return; |
| | | } |
| | | if (returned && typeof returned.then === "function") { |
| | | returned.then(value => settle(false, value), () => settle(true)); |
| | | } |
| | | }); |
| | | } |
| | | |
| | | if (typeof globalThis.addEventListener === "function") { |
| | | globalThis.addEventListener("error", () => recordExtensionDiagnostic("SERVICE_WORKER_ERROR")); |
| | | globalThis.addEventListener("unhandledrejection", () => |
| | | recordExtensionDiagnostic("SERVICE_WORKER_UNHANDLED_REJECTION")); |
| | | } |
| | | |
| | | function isPlainObject(value) { |
| | | if (value === null || typeof value !== "object" || Array.isArray(value)) return false; |
| | | const prototype = Object.getPrototypeOf(value); |
| | | return prototype === Object.prototype || prototype === null; |
| | | } |
| | | |
| | | function exactKeys(value, keys) { |
| | | return isPlainObject(value) && Object.keys(value).sort().join("\0") === [...keys].sort().join("\0"); |
| | | } |
| | | |
| | | function safeErrorCode(error, fallback = "E_EXTENSION") { |
| | | const candidate = typeof error?.message === "string" ? error.message : ""; |
| | | return ERROR_CODE_RE.test(candidate) ? candidate : fallback; |
| | | } |
| | | |
| | | function canonicalUrl(bvid) { |
| | | return `https://www.bilibili.com/video/${bvid}`; |
| | | } |
| | | |
| | | function locationMatches(rawUrl, bvid) { |
| | | if (!BVID_RE.test(bvid) || typeof rawUrl !== "string") return false; |
| | | let parsed; |
| | | try { parsed = new URL(rawUrl); } catch { return false; } |
| | | const path = `/video/${bvid}`; |
| | | return parsed.origin === "https://www.bilibili.com" && |
| | | (parsed.pathname === path || parsed.pathname === `${path}/`) && |
| | | (parsed.search === "" || /^\?vd_source=[0-9a-f]{32}$/.test(parsed.search)) && |
| | | parsed.hash === ""; |
| | | } |
| | | |
| | | function isManagedIdentityPendingUrl(rawUrl, bvid) { |
| | | if (rawUrl === "about:blank") return true; |
| | | if (typeof rawUrl !== "string" || !BVID_RE.test(bvid)) return false; |
| | | let parsed; |
| | | try { parsed = new URL(rawUrl); } catch { return true; } |
| | | if (parsed.origin !== "https://www.bilibili.com") return false; |
| | | const videoMatch = /^\/video\/(BV1[1-9A-HJ-NP-Za-km-z]{9})\/?$/.exec(parsed.pathname); |
| | | return videoMatch === null || videoMatch[1] === bvid; |
| | | } |
| | | |
| | | function durationToleranceMs(expected) { |
| | | return Math.max(1500, Math.min(10000, Math.ceil(expected / 1000))); |
| | | } |
| | | |
| | | async function sha256Hex(text) { |
| | | const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(text)); |
| | | return Array.from(new Uint8Array(digest), value => value.toString(16).padStart(2, "0")).join(""); |
| | | } |
| | | |
| | | async function validateJob(job) { |
| | | const successor = isPlainObject(job) && Object.prototype.hasOwnProperty.call(job, "lineage"); |
| | | if (!exactKeys(job, successor ? [...JOB_KEYS, "lineage"] : JOB_KEYS) || !HEX64_RE.test(job.job_id) || !BVID_RE.test(job.bvid) || |
| | | !CREATOR_RE.test(job.creator_uid) || job.canonical_url !== canonicalUrl(job.bvid) || |
| | | !Number.isSafeInteger(job.expected_duration_ms) || job.expected_duration_ms < 1000 || |
| | | job.expected_duration_ms > 86400000 || typeof job.title !== "string" || !job.title.trim() || |
| | | new TextEncoder().encode(job.title).length > 600 || typeof job.published_at !== "string" || |
| | | job.published_at.length > 64 || !Number.isFinite(Date.parse(job.published_at))) return false; |
| | | let stable; |
| | | if (successor) { |
| | | const lineage = job.lineage; |
| | | if (!exactKeys(lineage, LINEAGE_KEYS) || !HEX64_RE.test(lineage.predecessor_job_id) || |
| | | !Number.isSafeInteger(lineage.retry_generation) || lineage.retry_generation < 1 || |
| | | lineage.retry_generation > 1000000 || !ERROR_CODE_RE.test(lineage.predecessor_terminal_error_code) || |
| | | !MESSAGE_ID_RE.test(lineage.authorization_message_id) || |
| | | !HANDOFF_ID_RE.test(lineage.authorization_handoff_id) || |
| | | !UPPER_HEX64_RE.test(lineage.authorization_sha256) || |
| | | !MESSAGE_ID_RE.test(lineage.repair_review_result_message_id) || |
| | | !AUDIT_ID_RE.test(lineage.repair_audit_id) || |
| | | !Number.isSafeInteger(lineage.repair_audit_bytes) || lineage.repair_audit_bytes < 1 || |
| | | !UPPER_HEX64_RE.test(lineage.repair_audit_sha256)) return false; |
| | | stable = await sha256Hex([ |
| | | "bili-auth-job-v2", job.creator_uid, job.bvid, lineage.predecessor_job_id, |
| | | String(lineage.retry_generation), lineage.predecessor_terminal_error_code, |
| | | lineage.authorization_message_id, lineage.authorization_handoff_id, |
| | | lineage.authorization_sha256, lineage.repair_review_result_message_id, |
| | | lineage.repair_audit_id, String(lineage.repair_audit_bytes), lineage.repair_audit_sha256 |
| | | ].join("\0")); |
| | | } else { |
| | | stable = await sha256Hex(`bili-auth-job-v1\0${job.creator_uid}\0${job.bvid}`); |
| | | } |
| | | return stable === job.job_id; |
| | | } |
| | | |
| | | function validateMaintenance(value) { |
| | | return exactKeys(value, ["reload_required", "reload_token", "required_extension_build", "retry_after_unix_ms"]) && |
| | | typeof value.required_extension_build === "string" && value.required_extension_build.length <= 128 && |
| | | typeof value.reload_required === "boolean" && |
| | | (value.reload_token === null || HEX32_RE.test(value.reload_token)) && |
| | | Number.isSafeInteger(value.retry_after_unix_ms) && value.retry_after_unix_ms >= 0; |
| | | } |
| | | |
| | | async function validateResponse(value, expectedType) { |
| | | if (!exactKeys(value, RESPONSE_KEYS) || value.schema !== SCHEMA || value.type !== expectedType || |
| | | typeof value.host_build !== "string" || !SAFE_PHASES.has(value.phase) || |
| | | !Number.isInteger(value.progress) || value.progress < 0 || value.progress > 100 || |
| | | (value.error_code !== null && !/^E_[A-Z0-9_]{1,48}$/.test(value.error_code)) || |
| | | !validateMaintenance(value.maintenance)) return false; |
| | | if (value.job !== null && !(await validateJob(value.job))) return false; |
| | | if (value.lease_id !== null && !HEX32_RE.test(value.lease_id)) return false; |
| | | for (const name of ["formal_filename", "mapping_filename", "prepare_id"]) { |
| | | if (value[name] !== null && typeof value[name] !== "string") return false; |
| | | } |
| | | return true; |
| | | } |
| | | |
| | | function nativeSession() { |
| | | let port; |
| | | try { |
| | | port = chrome.runtime.connectNative(HOST_NAME); |
| | | } catch { |
| | | throw extensionApiError("E_HOST_CONNECT", "NATIVE_CONNECT_FAILED"); |
| | | } |
| | | if (!isPlainObject(port) && (port === null || typeof port !== "object")) { |
| | | throw extensionApiError("E_HOST_CONNECT", "NATIVE_CONNECT_FAILED"); |
| | | } |
| | | const messages = []; |
| | | const waiters = []; |
| | | let disconnected = false; |
| | | let closeRequested = false; |
| | | let lastDeterministicError = null; |
| | | const rejectWaiters = code => { |
| | | while (waiters.length) { |
| | | const waiter = waiters.shift(); |
| | | clearTimeout(waiter.timer); |
| | | waiter.reject(new Error(code)); |
| | | } |
| | | }; |
| | | port.onMessage.addListener(message => { |
| | | if (isPlainObject(message) && ERROR_CODE_RE.test(message.error_code || "")) { |
| | | lastDeterministicError = message.error_code; |
| | | } |
| | | const waiter = waiters.shift(); |
| | | if (waiter) { |
| | | clearTimeout(waiter.timer); |
| | | waiter.resolve(message); |
| | | } else messages.push(message); |
| | | }); |
| | | port.onDisconnect.addListener(() => { |
| | | if (disconnected) return; |
| | | disconnected = true; |
| | | let runtimeFailure = false; |
| | | try { runtimeFailure = chrome.runtime.lastError != null; } catch { runtimeFailure = true; } |
| | | if (runtimeFailure) recordExtensionDiagnostic("NATIVE_DISCONNECT_FAILED"); |
| | | rejectWaiters(lastDeterministicError || "E_HOST_DISCONNECT"); |
| | | }); |
| | | const next = timeoutMs => new Promise((resolve, reject) => { |
| | | if (messages.length) { resolve(messages.shift()); return; } |
| | | if (disconnected) { reject(new Error(lastDeterministicError || "E_HOST_DISCONNECT")); return; } |
| | | const waiter = {resolve, reject, timer: null}; |
| | | waiters.push(waiter); |
| | | waiter.timer = setTimeout(() => { |
| | | const index = waiters.indexOf(waiter); |
| | | if (index >= 0) { |
| | | waiters.splice(index, 1); |
| | | reject(new Error("E_HOST_TIMEOUT")); |
| | | } |
| | | }, timeoutMs); |
| | | }); |
| | | return { |
| | | async send(message, timeoutMs = 20000) { |
| | | if (disconnected) throw new Error(lastDeterministicError || "E_HOST_DISCONNECT"); |
| | | const response = next(timeoutMs); |
| | | try { |
| | | port.postMessage(message); |
| | | } catch (error) { |
| | | rejectWaiters(safeErrorCode(error, "E_HOST_SEND")); |
| | | } |
| | | return response; |
| | | }, |
| | | disconnect() { |
| | | if (closeRequested) return; |
| | | closeRequested = true; |
| | | if (!disconnected) { |
| | | try { port.disconnect(); } catch {} |
| | | } |
| | | } |
| | | }; |
| | | } |
| | | |
| | | function randomHex(bytes) { |
| | | const data = new Uint8Array(bytes); |
| | | crypto.getRandomValues(data); |
| | | return Array.from(data, value => value.toString(16).padStart(2, "0")).join(""); |
| | | } |
| | | |
| | | function isPageReadinessDiagnostic(value) { |
| | | return exactKeys(value, ["attempts", "elapsed_ms", "reason", "state"]) && |
| | | Number.isInteger(value.attempts) && value.attempts >= 0 && value.attempts <= 10000 && |
| | | Number.isInteger(value.elapsed_ms) && value.elapsed_ms >= 0 && value.elapsed_ms <= 7200000 && |
| | | PAGE_DIAGNOSTIC_STATES.has(value.state) && PAGE_DIAGNOSTIC_REASONS.has(value.reason); |
| | | } |
| | | |
| | | function isCanonicalPreparelessTerminal(code, diagnostic) { |
| | | if (!isPageReadinessDiagnostic(diagnostic)) return false; |
| | | const contract = PREPARELESS_TERMINAL_DIAGNOSTIC_CONTRACT.get(code); |
| | | return Array.isArray(contract) && contract.length === 2 && diagnostic.state === contract[0] && |
| | | contract[1] instanceof Set && contract[1].has(diagnostic.reason); |
| | | } |
| | | |
| | | function pageReadinessError(code, attempts, startedAtUnixMs, state, reason) { |
| | | const diagnostic = { |
| | | attempts, |
| | | elapsed_ms: Math.max(0, Math.min(7200000, Date.now() - startedAtUnixMs)), |
| | | state, |
| | | reason |
| | | }; |
| | | if (!isCanonicalPreparelessTerminal(code, diagnostic)) return new Error("E_REJECT"); |
| | | lastPageReadinessDiagnostic = diagnostic; |
| | | const error = new Error(code); |
| | | error.code = code; |
| | | error.page_readiness_diagnostic = diagnostic; |
| | | return error; |
| | | } |
| | | |
| | | async function activeTargetTab() { |
| | | const tabs = await chrome.tabs.query({active: true, currentWindow: true}); |
| | | if (tabs.length !== 1 || !Number.isInteger(tabs[0].id)) { |
| | | throw fixedError("E_PAGE_PROOF"); |
| | | } |
| | | const parsed = new URL(tabs[0].url || "about:blank"); |
| | | if (parsed.protocol !== "https:" || parsed.hostname !== "www.bilibili.com" || parsed.pathname !== TARGET_PATH) { |
| | | throw fixedError("E_PAGE_PROOF"); |
| | | } |
| | | return tabs[0]; |
| | | function fallbackPageReadinessDiagnostic(code) { |
| | | const contract = PREPARELESS_TERMINAL_DIAGNOSTIC_CONTRACT.get(code); |
| | | if (!Array.isArray(contract) || contract.length !== 2 || !(contract[1] instanceof Set) || |
| | | contract[1].size !== 1) return null; |
| | | const value = {attempts: 0, elapsed_ms: 0, state: contract[0], reason: [...contract[1]][0]}; |
| | | lastPageReadinessDiagnostic = value; |
| | | return value; |
| | | } |
| | | |
| | | async function observePage(tab, nonce) { |
| | | const results = await chrome.scripting.executeScript({ |
| | | target: {tabId: tab.id}, |
| | | world: "ISOLATED", |
| | | args: [TARGET, CANONICAL_URL, nonce], |
| | | func: (target, canonicalUrl, taskNonce) => { |
| | | const videos = Array.from(document.querySelectorAll("video")).filter(video => |
| | | video.readyState >= 1 && video.videoWidth > 0 && video.videoHeight > 0 |
| | | ); |
| | | if (location.hostname !== "www.bilibili.com" || location.pathname !== `/video/${target}` || videos.length !== 1) { |
| | | return null; |
| | | } |
| | | const video = videos[0]; |
| | | return { |
| | | target, |
| | | canonical_url: canonicalUrl, |
| | | task_nonce: taskNonce, |
| | | observed_at_unix_ms: Date.now(), |
| | | observed_duration_ms: Math.round(video.duration * 1000), |
| | | video_width: video.videoWidth, |
| | | video_height: video.videoHeight, |
| | | ready_state: video.readyState, |
| | | eme_present: video.mediaKeys !== null |
| | | }; |
| | | } |
| | | function tabDriftError(reason) { |
| | | lastTabDriftReason = reason; |
| | | const error = new Error("E_PAGE_TAB_DRIFT"); |
| | | error.reason_code = reason; |
| | | return error; |
| | | } |
| | | |
| | | function tabIdentityReason(tab, binding) { |
| | | if (!isPlainObject(tab) || !Number.isInteger(tab.id)) return "TAB_ID_INVALID"; |
| | | if (tab.id !== binding.tabId) return "TAB_ID_CHANGED"; |
| | | if (!Number.isInteger(tab.windowId) || tab.windowId !== binding.windowId) return "TAB_WINDOW_CHANGED"; |
| | | if (String(tab.cookieStoreId ?? "0") !== binding.cookieStoreId) return "TAB_STORE_CHANGED"; |
| | | return null; |
| | | } |
| | | |
| | | function bindTab(tab, bvid, managed) { |
| | | if (!isPlainObject(tab) || !Number.isInteger(tab.id) || !Number.isInteger(tab.windowId)) { |
| | | throw tabDriftError("TAB_IDENTITY_INVALID"); |
| | | } |
| | | const exactUrl = locationMatches(tab.url, bvid); |
| | | const exactPendingUrl = managed === true && locationMatches(tab.pendingUrl, bvid); |
| | | if (!exactUrl && !exactPendingUrl) throw tabDriftError("TAB_URL_UNBOUND"); |
| | | return Object.freeze({ |
| | | tabId: tab.id, |
| | | windowId: tab.windowId, |
| | | cookieStoreId: String(tab.cookieStoreId ?? "0"), |
| | | managed: managed === true |
| | | }); |
| | | if (results.length !== 1 || !results[0].result) { |
| | | throw fixedError("E_PAGE_PROOF"); |
| | | } |
| | | const proof = results[0].result; |
| | | const now = Date.now(); |
| | | const exactKeys = [ |
| | | "canonical_url", "eme_present", "observed_at_unix_ms", "observed_duration_ms", |
| | | "ready_state", "target", "task_nonce", "video_height", "video_width" |
| | | ]; |
| | | if (Object.keys(proof).sort().join("|") !== exactKeys.sort().join("|") || |
| | | proof.target !== TARGET || proof.canonical_url !== CANONICAL_URL || |
| | | proof.task_nonce !== nonce || proof.eme_present !== false || |
| | | !Number.isSafeInteger(proof.observed_at_unix_ms) || |
| | | proof.observed_at_unix_ms < now - 60000 || proof.observed_at_unix_ms > now + 5000 || |
| | | !Number.isSafeInteger(proof.observed_duration_ms) || |
| | | Math.abs(proof.observed_duration_ms - EXPECTED_DURATION_MS) > DURATION_TOLERANCE_MS || |
| | | !Number.isInteger(proof.video_width) || proof.video_width < 1 || proof.video_width > 7680 || |
| | | !Number.isInteger(proof.video_height) || proof.video_height < 1 || proof.video_height > 4320 || |
| | | !Number.isInteger(proof.ready_state) || proof.ready_state < 1 || proof.ready_state > 4) { |
| | | throw fixedError(proof.eme_present ? "E_DRM" : "E_PAGE_PROOF"); |
| | | } |
| | | return proof; |
| | | } |
| | | |
| | | async function validateCurrentPage({leaseId = null} = {}) { |
| | | if (activeStartLease !== null && activeStartLease !== leaseId) { |
| | | throw fixedError("E_BUSY"); |
| | | async function revalidateBoundTab(binding, bvid) { |
| | | let current; |
| | | try { |
| | | current = await chromeApiCall( |
| | | chrome.tabs, "get", [binding.tabId], "E_PAGE_TAB_DRIFT", "TABS_GET_FAILED" |
| | | ); |
| | | } catch { |
| | | throw tabDriftError("TAB_REMOVED_OR_UNAVAILABLE"); |
| | | } |
| | | const tab = await activeTargetTab(); |
| | | const nonce = randomNonce(); |
| | | const proof = await observePage(tab, nonce); |
| | | currentProof = proof; |
| | | currentTabId = tab.id; |
| | | if (nativePort) { |
| | | const previousPort = nativePort; |
| | | nativePort = null; |
| | | nativeReady = false; |
| | | preparedTaskNonce = null; |
| | | preparedLeaseId = null; |
| | | prepareResponses.clear(); |
| | | previousPort.disconnect(); |
| | | } |
| | | await awaitNativeReady(); |
| | | safeState = {...safeState, phase: "READY", error_code: null}; |
| | | return safeState; |
| | | const identityReason = tabIdentityReason(current, binding); |
| | | if (identityReason !== null) throw tabDriftError(identityReason); |
| | | if (!locationMatches(current.url, bvid)) throw tabDriftError("TAB_URL_CHANGED"); |
| | | return current; |
| | | } |
| | | |
| | | function connectNative() { |
| | | if (nativePort) { |
| | | return; |
| | | async function activateBoundTab(binding, tab, bvid, startedAtUnixMs) { |
| | | const identityReason = tabIdentityReason(tab, binding); |
| | | if (identityReason !== null) throw tabDriftError(identityReason); |
| | | let focusedWindow; |
| | | try { |
| | | focusedWindow = await chromeApiCall( |
| | | chrome.windows, "update", [binding.windowId, {state: "normal", focused: true}], |
| | | "E_PAGE_IDENTITY_UNAVAILABLE", "WINDOWS_UPDATE_FAILED" |
| | | ); |
| | | } catch { |
| | | throw pageReadinessError( |
| | | "E_PAGE_IDENTITY_UNAVAILABLE", 0, startedAtUnixMs, |
| | | "IDENTITY_PENDING", "PAGE_WINDOW_FOCUS_PENDING" |
| | | ); |
| | | } |
| | | nativeReady = false; |
| | | preparedTaskNonce = null; |
| | | preparedLeaseId = null; |
| | | prepareResponses.clear(); |
| | | const port = chrome.runtime.connectNative(HOST); |
| | | nativePort = port; |
| | | port.onMessage.addListener(message => { |
| | | const safeKeys = new Set([ |
| | | "schema", "type", "host_build", "target", "phase", "progress", "error_code", |
| | | "formal_filename", "mapping_filename", "prepare_id" |
| | | ]); |
| | | const basename = value => value === null || (typeof value === "string" && value.length > 0 && !/[\\/]/.test(value)); |
| | | const prepareIdValid = message?.prepare_id === null || |
| | | (typeof message?.prepare_id === "string" && /^[0-9a-f]{32}$/.test(message.prepare_id)); |
| | | if (!message || Object.keys(message).some(key => !safeKeys.has(key)) || |
| | | message.schema !== SCHEMA || message.host_build !== HOST_BUILD || message.target !== TARGET || |
| | | !["IDLE", "READY", "CHECKING", "DOWNLOADING", "MERGING", "VALIDATING", "PUBLISHING", "COMPLETE", "FAILED", "CANCELED"].includes(message.phase) || |
| | | !Number.isInteger(message.progress) || message.progress < 0 || message.progress > 100 || |
| | | !basename(message.formal_filename) || !basename(message.mapping_filename) || !prepareIdValid || |
| | | (message.type === "prepare" ? message.prepare_id === null : message.prepare_id !== null)) { |
| | | safeState = {...safeState, phase: "FAILED", error_code: "E_PROTOCOL"}; |
| | | return; |
| | | } |
| | | nativeReady = nativeReady || (message.type === "hello" && message.phase === "READY"); |
| | | lastNativeType = message.type; |
| | | if (message.type === "prepare") { |
| | | prepareResponses.set(message.prepare_id, { |
| | | phase: message.phase, |
| | | error_code: message.error_code |
| | | }); |
| | | } |
| | | safeState = { |
| | | phase: message.phase, |
| | | progress: message.progress, |
| | | error_code: message.error_code, |
| | | formal_filename: message.formal_filename, |
| | | mapping_filename: message.mapping_filename |
| | | if (!isPlainObject(focusedWindow) || focusedWindow.id !== binding.windowId) { |
| | | throw tabDriftError("TAB_WINDOW_CHANGED"); |
| | | } |
| | | let updated; |
| | | try { |
| | | updated = await chromeApiCall( |
| | | chrome.tabs, "update", [binding.tabId, {active: true}], |
| | | "E_PAGE_IDENTITY_UNAVAILABLE", "TABS_UPDATE_FAILED" |
| | | ); |
| | | } catch { |
| | | throw pageReadinessError( |
| | | "E_PAGE_IDENTITY_UNAVAILABLE", 0, startedAtUnixMs, |
| | | "IDENTITY_PENDING", "PAGE_TAB_ACTIVATION_PENDING" |
| | | ); |
| | | } |
| | | const updatedIdentityReason = tabIdentityReason(updated, binding); |
| | | if (updatedIdentityReason !== null) throw tabDriftError(updatedIdentityReason); |
| | | if (!locationMatches(updated.url, bvid) && |
| | | !(binding.managed && (locationMatches(updated.pendingUrl, bvid) || |
| | | isManagedIdentityPendingUrl(updated.url, bvid)))) { |
| | | throw tabDriftError("TAB_MANAGED_URL_DRIFT"); |
| | | } |
| | | return updated; |
| | | } |
| | | |
| | | async function waitForComplete(binding, tab, bvid, startedAtUnixMs, deadlineUnixMs) { |
| | | const identityReason = tabIdentityReason(tab, binding); |
| | | if (identityReason !== null) throw tabDriftError(identityReason); |
| | | if (tab.status === "complete" && locationMatches(tab.url, bvid)) { |
| | | return revalidateBoundTab(binding, bvid); |
| | | } |
| | | if (!locationMatches(tab.url, bvid) && |
| | | !(binding.managed && (locationMatches(tab.pendingUrl, bvid) || |
| | | isManagedIdentityPendingUrl(tab.url, bvid)))) { |
| | | throw tabDriftError("TAB_URL_UNBOUND"); |
| | | } |
| | | return new Promise((resolve, reject) => { |
| | | let done = false; |
| | | let timer = null; |
| | | const finish = (error, value) => { |
| | | if (done) return; |
| | | done = true; |
| | | chrome.tabs.onUpdated.removeListener(onUpdated); |
| | | chrome.tabs.onRemoved.removeListener(onRemoved); |
| | | if (timer !== null) clearTimeout(timer); |
| | | if (error) reject(error); else resolve(value); |
| | | }; |
| | | nativeRevision += 1; |
| | | }); |
| | | port.onDisconnect.addListener(() => { |
| | | if (nativePort === port) { |
| | | nativePort = null; |
| | | nativeReady = false; |
| | | preparedTaskNonce = null; |
| | | preparedLeaseId = null; |
| | | prepareResponses.clear(); |
| | | if (!["COMPLETE", "FAILED", "CANCELED"].includes(safeState.phase)) { |
| | | safeState = {...safeState, phase: "FAILED", error_code: "E_NATIVE_DISCONNECT"}; |
| | | const onUpdated = (tabId, change, current) => { |
| | | if (tabId !== binding.tabId) return; |
| | | const currentIdentityReason = tabIdentityReason(current, binding); |
| | | if (currentIdentityReason !== null) { |
| | | finish(tabDriftError(currentIdentityReason)); |
| | | return; |
| | | } |
| | | if (typeof change?.url === "string" && !locationMatches(change.url, bvid) && |
| | | !(binding.managed && isManagedIdentityPendingUrl(change.url, bvid))) { |
| | | finish(tabDriftError("TAB_MANAGED_URL_DRIFT")); |
| | | return; |
| | | } |
| | | if (locationMatches(current.url, bvid) && current.status === "complete") { |
| | | finish(null, current); |
| | | return; |
| | | } |
| | | if (!locationMatches(current.url, bvid) && |
| | | !(binding.managed && (locationMatches(current.pendingUrl, bvid) || |
| | | isManagedIdentityPendingUrl(current.url, bvid)))) { |
| | | finish(tabDriftError("TAB_URL_CHANGED")); |
| | | } |
| | | }; |
| | | const onRemoved = tabId => { |
| | | if (tabId === binding.tabId) finish(tabDriftError("TAB_REMOVED_OR_UNAVAILABLE")); |
| | | }; |
| | | chrome.tabs.onUpdated.addListener(onUpdated); |
| | | chrome.tabs.onRemoved.addListener(onRemoved); |
| | | timer = setTimeout(() => finish(pageReadinessError( |
| | | "E_PAGE_IDENTITY_UNAVAILABLE", 0, startedAtUnixMs, |
| | | "IDENTITY_PENDING", "PAGE_URL_PENDING" |
| | | )), Math.max(1, deadlineUnixMs - Date.now())); |
| | | }); |
| | | } |
| | | |
| | | async function waitForActiveNonMinimizedBoundTab(binding, bvid, startedAtUnixMs, deadlineUnixMs) { |
| | | let pendingReason = "PAGE_WINDOW_STATE_PENDING"; |
| | | while (Date.now() < deadlineUnixMs) { |
| | | const current = await revalidateBoundTab(binding, bvid); |
| | | let currentWindow; |
| | | try { |
| | | currentWindow = await chromeApiCall( |
| | | chrome.windows, "get", [binding.windowId], |
| | | "E_PAGE_IDENTITY_UNAVAILABLE", "WINDOWS_GET_FAILED" |
| | | ); |
| | | } catch { |
| | | currentWindow = null; |
| | | } |
| | | }); |
| | | port.postMessage({ |
| | | schema: SCHEMA, |
| | | type: "hello", |
| | | extension_build: EXTENSION_BUILD, |
| | | target: TARGET |
| | | }); |
| | | } |
| | | |
| | | async function freshNativeStatus() { |
| | | const prior = nativeRevision; |
| | | nativePort.postMessage({schema: SCHEMA, type: "status", target: TARGET}); |
| | | const deadline = Date.now() + 5000; |
| | | while (nativeRevision === prior && Date.now() < deadline) { |
| | | await new Promise(resolve => setTimeout(resolve, 25)); |
| | | } |
| | | if (nativeRevision === prior) { |
| | | throw fixedError("E_NATIVE_CONNECT"); |
| | | } |
| | | return safeState; |
| | | } |
| | | |
| | | async function awaitNativeReady() { |
| | | connectNative(); |
| | | const deadline = Date.now() + 5000; |
| | | while (!nativeReady && Date.now() < deadline) { |
| | | if (safeState.error_code) { |
| | | throw fixedError(safeState.error_code); |
| | | if (!isPlainObject(currentWindow) || currentWindow.id !== binding.windowId) { |
| | | pendingReason = "PAGE_WINDOW_STATE_PENDING"; |
| | | } else if (typeof currentWindow.state !== "string") { |
| | | pendingReason = "PAGE_WINDOW_STATE_PENDING"; |
| | | } else if (currentWindow.state === "minimized") { |
| | | pendingReason = "PAGE_WINDOW_MINIMIZED_PENDING"; |
| | | } else if (!["normal", "maximized", "fullscreen", "locked-fullscreen"].includes(currentWindow.state)) { |
| | | pendingReason = "PAGE_WINDOW_STATE_PENDING"; |
| | | } else if (current.active !== true) { |
| | | pendingReason = "PAGE_TAB_ACTIVATION_PENDING"; |
| | | } else { |
| | | recordExtensionDiagnostic(currentWindow.focused === true ? |
| | | "WINDOW_FOCUS_TRUE" : "WINDOW_FOCUS_FALSE"); |
| | | return current; |
| | | } |
| | | await new Promise(resolve => setTimeout(resolve, 25)); |
| | | const remainingMs = deadlineUnixMs - Date.now(); |
| | | if (remainingMs <= 0) break; |
| | | await new Promise(resolve => setTimeout(resolve, Math.min(PAGE_PROOF_POLL_MS, remainingMs))); |
| | | } |
| | | if (!nativeReady) { |
| | | throw fixedError("E_NATIVE_CONNECT"); |
| | | throw pageReadinessError( |
| | | "E_PAGE_IDENTITY_UNAVAILABLE", 0, startedAtUnixMs, |
| | | "IDENTITY_PENDING", pendingReason |
| | | ); |
| | | } |
| | | |
| | | function foregroundFallbackEligible(error) { |
| | | return FOREGROUND_ELIGIBLE_CODES.has(error?.message) && |
| | | isPageReadinessDiagnostic(error?.page_readiness_diagnostic); |
| | | } |
| | | |
| | | function exactWindowBounds(value) { |
| | | if (!isPlainObject(value)) return null; |
| | | const bounds = {}; |
| | | for (const key of ["left", "top", "width", "height"]) { |
| | | if (!Number.isSafeInteger(value[key])) return null; |
| | | bounds[key] = value[key]; |
| | | } |
| | | if (bounds.width < 1 || bounds.height < 1) return null; |
| | | return bounds; |
| | | } |
| | | |
| | | async function foregroundAndRebind(session, job, leaseId, selected, flow, sourceError) { |
| | | if (flow.foregroundUsed) throw sourceError; |
| | | flow.foregroundUsed = true; |
| | | let windowValue; |
| | | try { |
| | | windowValue = await chromeApiCall( |
| | | chrome.windows, "get", [selected.binding.windowId], |
| | | "E_PAGE_IDENTITY_UNAVAILABLE", "WINDOWS_GET_FAILED" |
| | | ); |
| | | } catch { |
| | | throw sourceError; |
| | | } |
| | | const windowBounds = exactWindowBounds(windowValue); |
| | | if (windowBounds === null) throw sourceError; |
| | | const response = await session.send({ |
| | | schema: SCHEMA, type: "foreground", job_id: job.job_id, lease_id: leaseId, |
| | | tab_id: selected.binding.tabId, window_id: selected.binding.windowId, |
| | | window_bounds: windowBounds |
| | | }, 20000); |
| | | if (!(await validateResponse(response, "foreground")) || |
| | | response.job?.job_id !== job.job_id || response.lease_id !== leaseId) { |
| | | throw sourceError; |
| | | } |
| | | if (response.phase !== "READY" || response.error_code !== null) { |
| | | const diagnosticReason = FOREGROUND_ERROR_DIAGNOSTICS.get(response.error_code); |
| | | if (typeof diagnosticReason === "string") recordExtensionDiagnostic(diagnosticReason); |
| | | throw sourceError; |
| | | } |
| | | recordExtensionDiagnostic("OS_FOREGROUND_READY"); |
| | | let pendingError = sourceError; |
| | | while (Date.now() < flow.deadlineUnixMs) { |
| | | let tabs; |
| | | try { |
| | | tabs = await chromeApiCall( |
| | | chrome.tabs, "query", [{url: ["https://www.bilibili.com/video/*"], windowType: "normal"}], |
| | | "E_PAGE_IDENTITY_UNAVAILABLE", "TABS_QUERY_FAILED" |
| | | ); |
| | | } catch { |
| | | tabs = null; |
| | | } |
| | | if (Array.isArray(tabs)) { |
| | | const exactCandidates = tabs.filter(item => |
| | | item?.incognito !== true && locationMatches(item?.url, job.bvid)); |
| | | if (exactCandidates.length > 1) throw new Error("E_PAGE_DUPLICATE_TAB"); |
| | | if (exactCandidates.length === 1) { |
| | | const preservesOwnership = selected.binding.managed === true && |
| | | exactCandidates[0].id === selected.binding.tabId && |
| | | exactCandidates[0].windowId === selected.binding.windowId; |
| | | const rebound = { |
| | | binding: bindTab(exactCandidates[0], job.bvid, preservesOwnership), tab: exactCandidates[0] |
| | | }; |
| | | const active = await waitForActiveNonMinimizedBoundTab( |
| | | rebound.binding, job.bvid, flow.startedAtUnixMs, flow.deadlineUnixMs |
| | | ); |
| | | return {binding: rebound.binding, tab: active, activeNonMinimized: active}; |
| | | } |
| | | pendingError = pageReadinessError( |
| | | "E_PAGE_IDENTITY_UNAVAILABLE", 0, flow.startedAtUnixMs, |
| | | "IDENTITY_PENDING", "PAGE_URL_PENDING" |
| | | ); |
| | | } |
| | | const remainingMs = flow.deadlineUnixMs - Date.now(); |
| | | if (remainingMs <= 0) break; |
| | | await new Promise(resolve => setTimeout(resolve, Math.min(PAGE_PROOF_POLL_MS, remainingMs))); |
| | | } |
| | | throw pendingError; |
| | | } |
| | | |
| | | async function targetTab(session, job, leaseId, flow) { |
| | | const startedAtUnixMs = Date.now(); |
| | | let tabs; |
| | | try { |
| | | tabs = await chromeApiCall( |
| | | chrome.tabs, "query", [{ |
| | | url: ["https://www.bilibili.com/video/*"], windowType: "normal" |
| | | }], "E_PAGE_IDENTITY_UNAVAILABLE", "TABS_QUERY_FAILED" |
| | | ); |
| | | } catch { |
| | | throw pageReadinessError( |
| | | "E_PAGE_IDENTITY_UNAVAILABLE", 0, startedAtUnixMs, |
| | | "IDENTITY_PENDING", "PAGE_DOM_IDENTITY_PENDING" |
| | | ); |
| | | } |
| | | if (!Array.isArray(tabs)) throw new Error("E_PAGE_PROOF"); |
| | | const storedOwnership = await loadOwnedTabRecord(); |
| | | const exactCandidates = []; |
| | | for (const candidate of tabs.filter(item => item?.incognito !== true && locationMatches(item?.url, job.bvid))) { |
| | | const adopted = storedOwnership !== null && storedOwnership.job_id === job.job_id && |
| | | storedOwnership.lease_id === leaseId && storedOwnership.bvid === job.bvid && |
| | | storedOwnership.tab_id === candidate.id && storedOwnership.window_id === candidate.windowId && |
| | | storedOwnership.cookie_store_id === String(candidate.cookieStoreId ?? "0"); |
| | | const provisional = bindTab(candidate, job.bvid, adopted); |
| | | const current = await revalidateBoundTab(provisional, job.bvid); |
| | | exactCandidates.push({binding: bindTab(current, job.bvid, adopted), tab: current}); |
| | | if (adopted) recordBrowserLifecycleDiagnostic("OWNED_TAB_ADOPTED"); |
| | | } |
| | | if (exactCandidates.length > 1) throw new Error("E_PAGE_DUPLICATE_TAB"); |
| | | let selected; |
| | | if (exactCandidates.length === 1) { |
| | | selected = exactCandidates[0]; |
| | | } else { |
| | | let created; |
| | | try { |
| | | created = await chromeApiCall( |
| | | chrome.tabs, "create", [{url: canonicalUrl(job.bvid), active: false}], |
| | | "E_PAGE_IDENTITY_UNAVAILABLE", "TABS_CREATE_FAILED" |
| | | ); |
| | | } catch { |
| | | throw pageReadinessError( |
| | | "E_PAGE_IDENTITY_UNAVAILABLE", 0, startedAtUnixMs, |
| | | "IDENTITY_PENDING", "PAGE_NAVIGATION_PENDING" |
| | | ); |
| | | } |
| | | selected = {binding: bindTab(created, job.bvid, true), tab: created}; |
| | | await persistOwnedTabRecord(job, leaseId, selected.binding); |
| | | } |
| | | try { |
| | | const activated = await activateBoundTab( |
| | | selected.binding, selected.tab, job.bvid, startedAtUnixMs |
| | | ); |
| | | const activationDeadline = Math.min( |
| | | flow.deadlineUnixMs, Date.now() + PAGE_EXTENSION_ACTIVATION_GRACE_MS |
| | | ); |
| | | const ready = await waitForComplete( |
| | | selected.binding, activated, job.bvid, startedAtUnixMs, activationDeadline |
| | | ); |
| | | const activeNonMinimized = await waitForActiveNonMinimizedBoundTab( |
| | | selected.binding, job.bvid, startedAtUnixMs, activationDeadline |
| | | ); |
| | | return {binding: selected.binding, tab: ready, activeNonMinimized}; |
| | | } catch (error) { |
| | | if (!foregroundFallbackEligible(error)) throw error; |
| | | return foregroundAndRebind(session, job, leaseId, selected, flow, error); |
| | | } |
| | | } |
| | | |
| | | async function freshNativePrepare(proof, leaseId) { |
| | | await awaitNativeReady(); |
| | | if (activeStartLease !== leaseId) { |
| | | throw fixedError("E_BUSY"); |
| | | async function acquirePageProof(session, job, leaseId, selected, flow) { |
| | | const remainingMs = flow.deadlineUnixMs - Date.now(); |
| | | if (remainingMs <= 0) { |
| | | throw pageReadinessError( |
| | | "E_PAGE_IDENTITY_UNAVAILABLE", 0, flow.startedAtUnixMs, |
| | | "IDENTITY_PENDING", "PAGE_URL_PENDING" |
| | | ); |
| | | } |
| | | preparedTaskNonce = null; |
| | | preparedLeaseId = null; |
| | | const prepareId = randomNonce(); |
| | | prepareResponses.delete(prepareId); |
| | | nativePort.postMessage({ |
| | | schema: SCHEMA, |
| | | type: "prepare", |
| | | extension_build: EXTENSION_BUILD, |
| | | target: TARGET, |
| | | prepare_id: prepareId, |
| | | page_proof: proof |
| | | }); |
| | | const deadline = Date.now() + 120000; |
| | | while (!prepareResponses.has(prepareId) && Date.now() < deadline) { |
| | | await new Promise(resolve => setTimeout(resolve, 25)); |
| | | const initialBudgetMs = flow.foregroundUsed ? remainingMs : |
| | | Math.min(PAGE_EXTENSION_ACTIVATION_GRACE_MS, remainingMs); |
| | | try { |
| | | return await observe(job, selected.binding, initialBudgetMs); |
| | | } catch (error) { |
| | | if (flow.foregroundUsed || !foregroundFallbackEligible(error)) throw error; |
| | | const rebound = await foregroundAndRebind(session, job, leaseId, selected, flow, error); |
| | | const finalBudgetMs = flow.deadlineUnixMs - Date.now(); |
| | | if (finalBudgetMs <= 0) throw error; |
| | | return observe(job, rebound.binding, finalBudgetMs); |
| | | } |
| | | const response = prepareResponses.get(prepareId); |
| | | prepareResponses.delete(prepareId); |
| | | if (!response || response.phase !== "READY" || response.error_code) { |
| | | throw fixedError(response?.error_code || "E_PREPARE"); |
| | | } |
| | | preparedTaskNonce = proof.task_nonce; |
| | | preparedLeaseId = leaseId; |
| | | return prepareId; |
| | | } |
| | | |
| | | async function currentCookieStore(tabId) { |
| | | const stores = await chrome.cookies.getAllCookieStores(); |
| | | const matches = stores.filter(store => Array.isArray(store.tabIds) && store.tabIds.includes(tabId)); |
| | | if (matches.length !== 1 || !/^[0-9]{1,8}$/.test(matches[0].id)) { |
| | | throw fixedError("E_SECRET_INPUT"); |
| | | function injectedObservation(job, taskNonce) { |
| | | const path = `/video/${job.bvid}`; |
| | | if (location.href === "about:blank" || |
| | | (typeof document.readyState === "string" && document.readyState !== "complete")) { |
| | | return {status: "PENDING", terminal_error_code: "E_PAGE_IDENTITY_UNAVAILABLE", |
| | | state: "IDENTITY_PENDING", reason: "PAGE_URL_PENDING"}; |
| | | } |
| | | return matches[0].id; |
| | | } |
| | | |
| | | function projectCookie(cookie, storeId) { |
| | | if (cookie.partitionKey !== undefined) { |
| | | throw fixedError("E_SECRET_INPUT"); |
| | | const documentVisibilityDiagnostic = document.visibilityState === "visible" ? |
| | | "DOCUMENT_VISIBILITY_VISIBLE" : "DOCUMENT_VISIBILITY_HIDDEN"; |
| | | const documentFocusDiagnostic = typeof document.hasFocus !== "function" ? |
| | | "DOCUMENT_FOCUS_UNAVAILABLE" : |
| | | (document.hasFocus() === true ? "DOCUMENT_FOCUS_TRUE" : "DOCUMENT_FOCUS_FALSE"); |
| | | if (location.origin !== "https://www.bilibili.com") { |
| | | return {status: "REJECT", error_code: "E_PAGE_PROOF", |
| | | state: "PAGE_REJECTED", reason: "PAGE_ORIGIN_REJECTED"}; |
| | | } |
| | | if (!cookie.name || !cookie.value) { |
| | | return null; |
| | | const observedBvid = /^\/video\/(BV1[1-9A-HJ-NP-Za-km-z]{9})\/?$/.exec(location.pathname)?.[1] ?? null; |
| | | if (observedBvid !== null && observedBvid !== job.bvid) { |
| | | return {status: "REJECT", error_code: "E_PAGE_PROOF", |
| | | state: "PAGE_REJECTED", reason: "PAGE_BVID_REJECTED"}; |
| | | } |
| | | const session = Boolean(cookie.session); |
| | | const expiration = session ? null : Math.floor(cookie.expirationDate); |
| | | if (!session && (!Number.isSafeInteger(expiration) || expiration <= 0)) { |
| | | throw fixedError("E_SECRET_INPUT"); |
| | | if ((location.pathname !== path && location.pathname !== `${path}/`) || |
| | | (location.search !== "" && !/^\?vd_source=[0-9a-f]{32}$/.test(location.search)) || |
| | | location.hash !== "") { |
| | | return {status: "REJECT", error_code: "E_PAGE_PROOF", |
| | | state: "PAGE_REJECTED", reason: "PAGE_URL_REJECTED"}; |
| | | } |
| | | if (document.querySelectorAll( |
| | | ".error-container,.error-body,[data-bili-access-state='denied']" |
| | | ).length > 0) { |
| | | return {status: "REJECT", error_code: "E_PAGE_ACCESS_CONTROL", |
| | | state: "PAGE_REJECTED", reason: "PAGE_ACCESS_REJECTED"}; |
| | | } |
| | | const videos = Array.from(document.querySelectorAll("video")); |
| | | if (videos.length === 0) return {status: "PENDING", terminal_error_code: "E_PAGE_VIDEO_ABSENT", |
| | | state: "VIDEO_ABSENT", reason: "VIDEO_ELEMENT_ABSENT"}; |
| | | if (videos.length > 1) return {status: "REJECT", error_code: "E_MULTI_PART", |
| | | state: "PAGE_REJECTED", reason: "PAGE_MULTIPART_REJECTED"}; |
| | | const video = videos[0]; |
| | | if (video.mediaKeys !== null) return {status: "REJECT", error_code: "E_DRM", |
| | | state: "PAGE_REJECTED", reason: "PAGE_DRM_REJECTED"}; |
| | | let initialVideo = null; |
| | | let initialStateReason = "INITIAL_STATE_ASSIGNMENT_ABSENT"; |
| | | for (const script of Array.from(document.querySelectorAll("script"))) { |
| | | const raw = script?.textContent; |
| | | if (typeof raw !== "string" || raw.length < 30 || raw.length > 2_000_000) continue; |
| | | const trimmed = raw.trim(); |
| | | const assignment = /^window\.__INITIAL_STATE__\s*=\s*/.exec(trimmed); |
| | | if (assignment === null) continue; |
| | | initialStateReason = "INITIAL_STATE_JSON_INVALID"; |
| | | let encoded = trimmed.slice(assignment[0].length); |
| | | const runtimeMarker = encoded.lastIndexOf(";(function(){"); |
| | | if (runtimeMarker >= 0) encoded = encoded.slice(0, runtimeMarker); |
| | | else if (encoded.endsWith(";")) encoded = encoded.slice(0, -1); |
| | | try { |
| | | const parsed = JSON.parse(encoded); |
| | | const candidate = parsed?.videoData; |
| | | initialStateReason = "INITIAL_STATE_VIDEO_DATA_ABSENT"; |
| | | if (candidate && typeof candidate === "object" && !Array.isArray(candidate)) { |
| | | if (initialVideo !== null) return {status: "REJECT", error_code: "E_PAGE_PROOF", |
| | | state: "PAGE_REJECTED", reason: "PAGE_URL_REJECTED"}; |
| | | initialVideo = candidate; |
| | | initialStateReason = "METADATA_FIELDS_PENDING"; |
| | | } |
| | | } catch {} |
| | | } |
| | | const ownerAnchorIdentities = new Set(); |
| | | const ownerProofRoots = document.querySelectorAll( |
| | | "#v_upinfo,.up-info-container,.up-panel-container,.video-owner" |
| | | ); |
| | | for (const root of ownerProofRoots) { |
| | | if (root === null || typeof root.querySelectorAll !== "function") continue; |
| | | for (const anchor of root.querySelectorAll("a[href]")) { |
| | | try { |
| | | const href = new URL(anchor.href, location.href); |
| | | if (href.origin !== "https://space.bilibili.com" || |
| | | !/^\/[1-9][0-9]{0,19}\/?$/.test(href.pathname)) { |
| | | continue; |
| | | } |
| | | const ownerUid = href.pathname.replace(/^\//, "").replace(/\/$/, ""); |
| | | ownerAnchorIdentities.add(`https://space.bilibili.com/${ownerUid}`); |
| | | } catch {} |
| | | } |
| | | } |
| | | const canonicalOwnerUrl = `https://space.bilibili.com/${job.creator_uid}`; |
| | | const ownerAnchorExact = ownerAnchorIdentities.size === 1 && |
| | | ownerAnchorIdentities.has(canonicalOwnerUrl); |
| | | const ownerAnchorReason = ownerAnchorExact ? null : |
| | | (ownerAnchorIdentities.size === 0 ? "OWNER_ANCHOR_ABSENT" : |
| | | (ownerAnchorIdentities.size === 1 ? |
| | | "OWNER_ANCHOR_MISMATCH" : "OWNER_ANCHOR_AMBIGUOUS")); |
| | | let initialMetadata = null; |
| | | if (initialVideo !== null) { |
| | | if (initialVideo.bvid !== job.bvid) return {status: "REJECT", error_code: "E_PAGE_PROOF", |
| | | state: "PAGE_REJECTED", reason: "PAGE_BVID_REJECTED"}; |
| | | if (!Array.isArray(initialVideo.pages) || initialVideo.pages.length === 0) { |
| | | return {status: "REJECT", error_code: "E_PAGE_PROOF", |
| | | state: "PAGE_REJECTED", reason: "PAGE_URL_REJECTED"}; |
| | | } |
| | | if (initialVideo.pages.length > 1) { |
| | | return {status: "REJECT", error_code: "E_MULTI_PART", |
| | | state: "PAGE_REJECTED", reason: "PAGE_MULTIPART_REJECTED"}; |
| | | } |
| | | const initialPage = initialVideo.pages[0]; |
| | | const initialDimension = initialVideo.dimension; |
| | | const pageDimension = initialPage?.dimension; |
| | | const positiveSafeInteger = value => typeof value === "number" && |
| | | Number.isSafeInteger(value) && value > 0; |
| | | const positiveFiniteNumber = value => typeof value === "number" && |
| | | Number.isFinite(value) && value > 0; |
| | | if (!initialPage || typeof initialPage !== "object" || Array.isArray(initialPage) || |
| | | !Object.prototype.hasOwnProperty.call(initialPage, "cid") || |
| | | !Object.prototype.hasOwnProperty.call(initialPage, "page") || |
| | | !Object.prototype.hasOwnProperty.call(initialPage, "duration") || |
| | | !Object.prototype.hasOwnProperty.call(initialPage, "dimension") || |
| | | !initialDimension || typeof initialDimension !== "object" || Array.isArray(initialDimension) || |
| | | !pageDimension || typeof pageDimension !== "object" || Array.isArray(pageDimension) || |
| | | !positiveSafeInteger(initialVideo.cid) || !positiveSafeInteger(initialPage.cid) || |
| | | initialPage.cid !== initialVideo.cid || !positiveSafeInteger(initialPage.page) || |
| | | initialPage.page !== 1 || !positiveFiniteNumber(initialVideo.duration) || |
| | | !positiveFiniteNumber(initialPage.duration) || initialPage.duration !== initialVideo.duration || |
| | | !positiveSafeInteger(initialDimension.width) || |
| | | !positiveSafeInteger(initialDimension.height) || |
| | | !positiveSafeInteger(pageDimension.width) || |
| | | !positiveSafeInteger(pageDimension.height) || |
| | | pageDimension.width !== initialDimension.width || |
| | | pageDimension.height !== initialDimension.height) { |
| | | return {status: "REJECT", error_code: "E_PAGE_PROOF", |
| | | state: "PAGE_REJECTED", reason: "PAGE_URL_REJECTED"}; |
| | | } |
| | | const initialOwner = String(initialVideo.owner?.mid ?? ""); |
| | | if (initialOwner !== "" && initialOwner !== job.creator_uid) { |
| | | return {status: "REJECT", error_code: "E_OWNER", |
| | | state: "PAGE_REJECTED", reason: "PAGE_OWNER_REJECTED"}; |
| | | } |
| | | initialMetadata = { |
| | | duration: initialVideo.duration, |
| | | width: initialDimension.width, |
| | | height: initialDimension.height, |
| | | owner: initialOwner |
| | | }; |
| | | } |
| | | let metadataSource = "HTML_MEDIA_ELEMENT"; |
| | | let durationSeconds = video.duration; |
| | | let videoWidth = video.videoWidth; |
| | | let videoHeight = video.videoHeight; |
| | | let readyState = video.readyState; |
| | | if (initialMetadata?.owner === "" && !ownerAnchorExact) { |
| | | return {status: "PENDING", terminal_error_code: "E_PAGE_OWNER_ANCHOR_UNAVAILABLE", |
| | | state: "OWNER_PENDING", reason: ownerAnchorReason}; |
| | | } |
| | | if (!Number.isFinite(durationSeconds) || durationSeconds <= 0 || readyState < 1 || |
| | | videoWidth <= 0 || videoHeight <= 0) { |
| | | if (initialMetadata !== null && |
| | | (initialMetadata.owner === job.creator_uid || (initialMetadata.owner === "" && ownerAnchorExact))) { |
| | | metadataSource = "BILIBILI_INITIAL_STATE"; |
| | | durationSeconds = initialMetadata.duration; |
| | | videoWidth = initialMetadata.width; |
| | | videoHeight = initialMetadata.height; |
| | | readyState = 1; |
| | | } else if (!Number.isFinite(durationSeconds) || durationSeconds <= 0 || readyState < 1) { |
| | | if (!PAGE_METADATA_UNAVAILABLE_REASONS.has(initialStateReason)) { |
| | | return {status: "REJECT", error_code: "E_PAGE_PROOF", |
| | | state: "PAGE_REJECTED", reason: "PAGE_URL_REJECTED"}; |
| | | } |
| | | return {status: "PENDING", terminal_error_code: "E_PAGE_METADATA_NOT_READY", |
| | | state: "METADATA_NOT_READY", reason: initialStateReason}; |
| | | } else { |
| | | return {status: "PENDING", terminal_error_code: "E_PAGE_DIMENSIONS_UNAVAILABLE", |
| | | state: "DIMENSIONS_PENDING", reason: "VIDEO_DIMENSIONS_PENDING"}; |
| | | } |
| | | } |
| | | if (!ownerAnchorExact && String(initialVideo?.owner?.mid ?? "") !== job.creator_uid) { |
| | | return {status: "PENDING", terminal_error_code: "E_PAGE_OWNER_ANCHOR_UNAVAILABLE", |
| | | state: "OWNER_PENDING", reason: ownerAnchorReason}; |
| | | } |
| | | const durationMs = Math.round(durationSeconds * 1000); |
| | | const tolerance = Math.max(1500, Math.min(10000, Math.ceil(job.expected_duration_ms / 1000))); |
| | | if (Math.abs(durationMs - job.expected_duration_ms) > tolerance) { |
| | | return {status: "REJECT", error_code: "E_DURATION", |
| | | state: "PAGE_REJECTED", reason: "PAGE_DURATION_REJECTED"}; |
| | | } |
| | | return { |
| | | name: String(cookie.name), |
| | | value: String(cookie.value), |
| | | domain: String(cookie.domain), |
| | | host_only: Boolean(cookie.hostOnly), |
| | | path: String(cookie.path), |
| | | secure: Boolean(cookie.secure), |
| | | http_only: Boolean(cookie.httpOnly), |
| | | same_site: String(cookie.sameSite || "unspecified"), |
| | | session, |
| | | expiration_unix: expiration, |
| | | store_id: storeId, |
| | | partition_key: null |
| | | status: "READY", |
| | | document_focus_diagnostic: documentFocusDiagnostic, |
| | | document_visibility_diagnostic: documentVisibilityDiagnostic, |
| | | proof: { |
| | | job_id: job.job_id, |
| | | bvid: job.bvid, |
| | | creator_uid: job.creator_uid, |
| | | canonical_url: job.canonical_url, |
| | | task_nonce: taskNonce, |
| | | observed_at_unix_ms: Date.now(), |
| | | metadata_source: metadataSource, |
| | | observed_duration_ms: durationMs, |
| | | video_width: videoWidth, |
| | | video_height: videoHeight, |
| | | ready_state: readyState, |
| | | eme_present: video.mediaKeys !== null |
| | | } |
| | | }; |
| | | } |
| | | |
| | | async function startExactTask({retry = false} = {}) { |
| | | if (activeStartLease !== null) { |
| | | throw fixedError("E_BUSY"); |
| | | } |
| | | const leaseId = randomNonce(); |
| | | activeStartLease = leaseId; |
| | | try { |
| | | if (retry) { |
| | | await validateCurrentPage({leaseId}); |
| | | async function observe(job, binding, deadlineMs = PAGE_PROOF_DEADLINE_MS) { |
| | | const taskNonce = randomHex(16); |
| | | const startedAtUnixMs = Date.now(); |
| | | const deadlineUnixMs = startedAtUnixMs + deadlineMs; |
| | | let pendingTerminalCode = "E_PAGE_PROOF"; |
| | | let pendingState = "PAGE_REJECTED"; |
| | | let pendingReason = "PAGE_IDENTITY_REJECTED"; |
| | | let attempts = 0; |
| | | let stableSnapshots = 0; |
| | | let stableSignature = null; |
| | | let stableRejectSnapshots = 0; |
| | | let stableRejectSignature = null; |
| | | const deadlineCode = () => pendingTerminalCode === "E_PAGE_METADATA_NOT_READY" ? |
| | | "E_PAGE_METADATA_UNAVAILABLE" : pendingTerminalCode; |
| | | while (true) { |
| | | await revalidateBoundTab(binding, job.bvid); |
| | | const remainingBeforeInvocationMs = deadlineUnixMs - Date.now(); |
| | | if (remainingBeforeInvocationMs <= 0) { |
| | | throw pageReadinessError( |
| | | deadlineCode(), attempts, startedAtUnixMs, pendingState, pendingReason |
| | | ); |
| | | } |
| | | const tab = await activeTargetTab(); |
| | | if (!currentProof || currentTabId !== tab.id || Date.now() - currentProof.observed_at_unix_ms > 60000) { |
| | | throw fixedError("E_PAGE_PROOF"); |
| | | } |
| | | const proof = currentProof; |
| | | const prepareId = await freshNativePrepare(proof, leaseId); |
| | | if (activeStartLease !== leaseId || preparedLeaseId !== leaseId || preparedTaskNonce !== proof.task_nonce) { |
| | | throw fixedError("E_PREPARE"); |
| | | } |
| | | const storeId = await currentCookieStore(tab.id); |
| | | const rawCookies = await chrome.cookies.getAll({url: "https://www.bilibili.com/", storeId}); |
| | | let records = []; |
| | | const invocationBudgetMs = Math.min(PAGE_PROOF_INVOCATION_TIMEOUT_MS, remainingBeforeInvocationMs); |
| | | let deadlineTimer = null; |
| | | let results; |
| | | attempts += 1; |
| | | try { |
| | | records = rawCookies.map(cookie => projectCookie(cookie, storeId)).filter(Boolean); |
| | | if (records.length < 1 || records.length > 128) { |
| | | throw fixedError("E_SECRET_INPUT"); |
| | | results = await Promise.race([ |
| | | chromeApiCall(chrome.scripting, "executeScript", [{ |
| | | target: {tabId: binding.tabId, frameIds: [0]}, world: "ISOLATED", func: injectedObservation, |
| | | args: [job, taskNonce] |
| | | }], "E_PAGE_IDENTITY_UNAVAILABLE", "SCRIPTING_EXECUTE_FAILED"), |
| | | new Promise((resolve, reject) => { |
| | | deadlineTimer = setTimeout(() => reject(new Error("E_PAGE_SCRIPT_TIMEOUT")), invocationBudgetMs); |
| | | }) |
| | | ]); |
| | | } catch (error) { |
| | | if (error?.message === "E_PAGE_SCRIPT_TIMEOUT") { |
| | | pendingTerminalCode = "E_PAGE_SCRIPT_TIMEOUT"; |
| | | pendingState = "SCRIPT_TIMEOUT"; |
| | | pendingReason = "SCRIPT_INVOCATION_FAILED"; |
| | | } else { |
| | | pendingTerminalCode = "E_PAGE_IDENTITY_UNAVAILABLE"; |
| | | pendingState = "IDENTITY_PENDING"; |
| | | pendingReason = "PAGE_SCRIPTING_PENDING"; |
| | | } |
| | | nativePort.postMessage({ |
| | | schema: SCHEMA, |
| | | type: "start", |
| | | extension_build: EXTENSION_BUILD, |
| | | target: TARGET, |
| | | canonical_url: CANONICAL_URL, |
| | | cookie_store_id: storeId, |
| | | prepare_id: prepareId, |
| | | page_proof: proof, |
| | | cookies: records |
| | | }); |
| | | currentTaskNonce = proof.task_nonce; |
| | | preparedTaskNonce = null; |
| | | preparedLeaseId = null; |
| | | safeState = {...safeState, phase: "CHECKING", progress: 0, error_code: null}; |
| | | currentProof = null; |
| | | currentTabId = null; |
| | | return safeState; |
| | | stableSnapshots = 0; |
| | | stableSignature = null; |
| | | stableRejectSnapshots = 0; |
| | | stableRejectSignature = null; |
| | | continue; |
| | | } finally { |
| | | for (const record of records) { |
| | | if (record) { |
| | | record.name = ""; |
| | | record.value = ""; |
| | | } |
| | | if (deadlineTimer !== null) clearTimeout(deadlineTimer); |
| | | } |
| | | if (Date.now() >= deadlineUnixMs) { |
| | | throw pageReadinessError( |
| | | deadlineCode(), attempts, startedAtUnixMs, pendingState, pendingReason |
| | | ); |
| | | } |
| | | if (!Array.isArray(results) || results.length !== 1 || !isPlainObject(results[0]) || |
| | | !isPlainObject(results[0].result)) { |
| | | pendingTerminalCode = "E_PAGE_IDENTITY_UNAVAILABLE"; |
| | | pendingState = "IDENTITY_PENDING"; |
| | | pendingReason = "PAGE_DOM_IDENTITY_PENDING"; |
| | | stableSnapshots = 0; |
| | | stableSignature = null; |
| | | stableRejectSnapshots = 0; |
| | | stableRejectSignature = null; |
| | | continue; |
| | | } |
| | | const result = results[0].result; |
| | | if (result.status === "READY" && |
| | | exactKeys(result, ["document_focus_diagnostic", "document_visibility_diagnostic", "proof", "status"]) && |
| | | DOCUMENT_FOCUS_DIAGNOSTICS.has(result.document_focus_diagnostic) && |
| | | DOCUMENT_VISIBILITY_DIAGNOSTICS.has(result.document_visibility_diagnostic) && |
| | | exactKeys(result.proof, PAGE_PROOF_KEYS)) { |
| | | if (result.document_focus_diagnostic !== "DOCUMENT_FOCUS_TRUE") { |
| | | recordExtensionDiagnostic(result.document_focus_diagnostic); |
| | | } |
| | | records.fill(null); |
| | | rawCookies.fill(null); |
| | | if (result.document_visibility_diagnostic !== "DOCUMENT_VISIBILITY_VISIBLE") { |
| | | recordExtensionDiagnostic(result.document_visibility_diagnostic); |
| | | } |
| | | const signature = JSON.stringify(PAGE_PROOF_KEYS.filter( |
| | | key => key !== "observed_at_unix_ms" && key !== "task_nonce" |
| | | ).map(key => result.proof[key])); |
| | | stableSnapshots = signature === stableSignature ? stableSnapshots + 1 : 1; |
| | | stableSignature = signature; |
| | | stableRejectSnapshots = 0; |
| | | stableRejectSignature = null; |
| | | if (stableSnapshots >= PAGE_PROOF_STABLE_SNAPSHOTS) return result.proof; |
| | | pendingTerminalCode = "E_PAGE_STABILITY_TIMEOUT"; |
| | | pendingState = "READY_UNSTABLE"; |
| | | pendingReason = "STABLE_SNAPSHOTS_PENDING"; |
| | | } else if (result.status === "REJECT" && |
| | | exactKeys(result, ["error_code", "reason", "state", "status"]) && |
| | | PREPARELESS_REJECT_CODES.has(result.error_code) && |
| | | isCanonicalPreparelessTerminal(result.error_code, { |
| | | attempts, elapsed_ms: Math.max(0, Math.min(7200000, Date.now() - startedAtUnixMs)), |
| | | state: result.state, reason: result.reason |
| | | })) { |
| | | const stableIdentityReject = new Set([ |
| | | "E_OWNER", "E_PAGE_ACCESS_CONTROL", "E_PAGE_PROOF" |
| | | ]).has(result.error_code); |
| | | if (!stableIdentityReject) { |
| | | throw pageReadinessError( |
| | | result.error_code, attempts, startedAtUnixMs, result.state, result.reason |
| | | ); |
| | | } |
| | | pendingTerminalCode = result.error_code; |
| | | pendingState = result.state; |
| | | pendingReason = result.reason; |
| | | stableSnapshots = 0; |
| | | stableSignature = null; |
| | | const rejectSignature = `${result.error_code}\0${result.state}\0${result.reason}`; |
| | | stableRejectSnapshots = rejectSignature === stableRejectSignature ? |
| | | stableRejectSnapshots + 1 : 1; |
| | | stableRejectSignature = rejectSignature; |
| | | if (stableRejectSnapshots >= PAGE_PROOF_STABLE_SNAPSHOTS) { |
| | | throw pageReadinessError( |
| | | result.error_code, attempts, startedAtUnixMs, result.state, result.reason |
| | | ); |
| | | } |
| | | } else if (result.status !== "PENDING" || |
| | | !exactKeys(result, ["reason", "state", "status", "terminal_error_code"]) || |
| | | !PAGE_PENDING_TERMINAL_CONTRACT.has(result.terminal_error_code)) { |
| | | throw new Error("E_PAGE_PROOF"); |
| | | } else { |
| | | const [expectedState, allowedReasons] = PAGE_PENDING_TERMINAL_CONTRACT.get(result.terminal_error_code); |
| | | if (!PAGE_PENDING_STATES.has(result.state) || result.state !== expectedState || |
| | | !PAGE_DIAGNOSTIC_REASONS.has(result.reason) || !allowedReasons.has(result.reason)) { |
| | | throw new Error("E_PAGE_PROOF"); |
| | | } |
| | | pendingTerminalCode = result.terminal_error_code; |
| | | pendingState = result.state; |
| | | pendingReason = result.reason; |
| | | stableSnapshots = 0; |
| | | stableSignature = null; |
| | | stableRejectSnapshots = 0; |
| | | stableRejectSignature = null; |
| | | } |
| | | } finally { |
| | | if (activeStartLease === leaseId) { |
| | | activeStartLease = null; |
| | | const remainingMs = deadlineUnixMs - Date.now(); |
| | | if (remainingMs <= 0) { |
| | | throw pageReadinessError( |
| | | deadlineCode(), attempts, startedAtUnixMs, pendingState, pendingReason |
| | | ); |
| | | } |
| | | await new Promise(resolve => setTimeout(resolve, Math.min(PAGE_PROOF_POLL_MS, remainingMs))); |
| | | } |
| | | } |
| | | |
| | | async function requestStatus() { |
| | | await awaitNativeReady(); |
| | | return freshNativeStatus(); |
| | | } |
| | | |
| | | async function cancelTask() { |
| | | if (!currentTaskNonce) { |
| | | throw fixedError("E_CANCEL"); |
| | | } |
| | | await awaitNativeReady(); |
| | | nativePort.postMessage({schema: SCHEMA, type: "cancel", target: TARGET, task_nonce: currentTaskNonce}); |
| | | return safeState; |
| | | } |
| | | |
| | | chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { |
| | | if (sender.id !== chrome.runtime.id || sender.url !== SIDEPANEL_URL || !message || typeof message.action !== "string") { |
| | | return false; |
| | | } |
| | | const actions = { |
| | | validate: () => validateCurrentPage(), |
| | | start: () => startExactTask(), |
| | | retry: () => startExactTask({retry: true}), |
| | | status: () => requestStatus(), |
| | | cancel: () => cancelTask() |
| | | function normalizeCookie(cookie, storeId) { |
| | | return { |
| | | name: cookie.name, |
| | | value: cookie.value, |
| | | domain: cookie.domain, |
| | | host_only: cookie.hostOnly, |
| | | path: cookie.path, |
| | | secure: cookie.secure, |
| | | http_only: cookie.httpOnly, |
| | | same_site: cookie.sameSite, |
| | | session: cookie.session, |
| | | expiration_unix: cookie.session === true ? null : Math.floor(cookie.expirationDate), |
| | | store_id: cookie.storeId, |
| | | partition_key: cookie.partitionKey === undefined ? null : cookie.partitionKey |
| | | }; |
| | | const action = actions[message.action]; |
| | | if (!action) { |
| | | sendResponse({ok: false, error_code: "E_PROTOCOL"}); |
| | | } |
| | | |
| | | function cookiePathMatches(path, bvid) { |
| | | const wanted = `/video/${bvid}`; |
| | | return path === "/" || (wanted.startsWith(path) && |
| | | (path.endsWith("/") || path.length === wanted.length || wanted[path.length] === "/")); |
| | | } |
| | | |
| | | function safeCookieString(value, minimum, maximumUtf8) { |
| | | if (typeof value !== "string" || COOKIE_CONTROL_RE.test(value)) return false; |
| | | const bytes = new TextEncoder().encode(value).length; |
| | | return bytes >= minimum && bytes <= maximumUtf8; |
| | | } |
| | | |
| | | function validBrowserCookie(cookie, storeId, proof, bvid, partitioned) { |
| | | return exactKeys(cookie, COOKIE_KEYS) && |
| | | safeCookieString(cookie.name, 1, 256) && safeCookieString(cookie.value, 1, 4096) && |
| | | typeof cookie.host_only === "boolean" && |
| | | cookie.domain === (cookie.host_only ? "www.bilibili.com" : ".bilibili.com") && |
| | | safeCookieString(cookie.path, 1, 1024) && |
| | | cookie.path.startsWith("/") && cookiePathMatches(cookie.path, bvid) && |
| | | typeof cookie.secure === "boolean" && typeof cookie.http_only === "boolean" && |
| | | typeof cookie.session === "boolean" && COOKIE_SAME_SITE.has(cookie.same_site) && |
| | | STORE_ID_RE.test(cookie.store_id) && cookie.store_id === storeId && |
| | | (partitioned ? validPartitionKeyDescriptor(cookie.partition_key) : cookie.partition_key === null) && |
| | | (cookie.session |
| | | ? cookie.expiration_unix === null |
| | | : Number.isSafeInteger(cookie.expiration_unix) && cookie.expiration_unix >= 1 && |
| | | cookie.expiration_unix > Math.floor(proof.observed_at_unix_ms / 1000)); |
| | | } |
| | | |
| | | function validCookie(cookie, storeId, proof, bvid) { |
| | | return validBrowserCookie(cookie, storeId, proof, bvid, false); |
| | | } |
| | | |
| | | function validCookieSet(cookies, storeId, proof, bvid) { |
| | | return Array.isArray(cookies) && cookies.length >= 1 && cookies.length <= MAX_COOKIE_COUNT && |
| | | cookies.every(cookie => validCookie(cookie, storeId, proof, bvid)); |
| | | } |
| | | |
| | | function validPartitionKeyDescriptor(value) { |
| | | if (value === null || typeof value !== "object" || Array.isArray(value)) return false; |
| | | const keys = Object.keys(value).sort().join("\0"); |
| | | if (!["", "hasCrossSiteAncestor", "topLevelSite", "hasCrossSiteAncestor\0topLevelSite"].includes(keys)) { |
| | | return false; |
| | | } |
| | | action().then( |
| | | state => sendResponse({ok: true, state}), |
| | | error => sendResponse({ok: false, error_code: error.code || "E_EXTENSION"}) |
| | | if (Object.prototype.hasOwnProperty.call(value, "hasCrossSiteAncestor") && |
| | | typeof value.hasCrossSiteAncestor !== "boolean") return false; |
| | | if (!Object.prototype.hasOwnProperty.call(value, "topLevelSite")) return true; |
| | | if (!safeCookieString(value.topLevelSite, 1, 2048)) return false; |
| | | try { |
| | | const site = new URL(value.topLevelSite); |
| | | return (site.protocol === "https:" || site.protocol === "http:") && |
| | | site.origin === value.topLevelSite && site.username === "" && site.password === "" && |
| | | site.pathname === "/" && site.search === "" && site.hash === ""; |
| | | } catch { |
| | | return false; |
| | | } |
| | | } |
| | | |
| | | function validRawSessionExpiration(cookie, proof) { |
| | | if (typeof cookie.session !== "boolean") return false; |
| | | if (cookie.session) return cookie.expirationDate === undefined; |
| | | if (typeof cookie.expirationDate !== "number" || !Number.isFinite(cookie.expirationDate)) return false; |
| | | const expirationUnix = Math.floor(cookie.expirationDate); |
| | | return Number.isSafeInteger(expirationUnix) && expirationUnix >= 1 && |
| | | expirationUnix > Math.floor(proof.observed_at_unix_ms / 1000); |
| | | } |
| | | |
| | | function selectedCookieRelevance(cookie, storeId, bvid, proof) { |
| | | if (cookie === null || typeof cookie !== "object" || Array.isArray(cookie) || |
| | | !safeCookieString(cookie.name, 1, 256)) return "AMBIGUOUS"; |
| | | if (!TRANSFER_COOKIE_NAMES.has(cookie.name)) return "IRRELEVANT_NAME"; |
| | | if (cookie.partitionKey !== undefined) { |
| | | if (!validRawSessionExpiration(cookie, proof)) return "AMBIGUOUS"; |
| | | let normalized = null; |
| | | try { |
| | | normalized = normalizeCookie(cookie, storeId); |
| | | return validBrowserCookie(normalized, storeId, proof, bvid, true) |
| | | ? "IRRELEVANT_PARTITIONED" : "AMBIGUOUS"; |
| | | } catch { |
| | | return "AMBIGUOUS"; |
| | | } finally { |
| | | if (normalized !== null) { |
| | | normalized.name = ""; |
| | | normalized.value = ""; |
| | | } |
| | | } |
| | | } |
| | | return "SELECTED"; |
| | | } |
| | | |
| | | function selectTransferCookies(rawCookies, storeId, proof, bvid) { |
| | | const selected = []; |
| | | const requiredNames = new Set(); |
| | | if (!Array.isArray(rawCookies)) return {ok: false, reason: "COOKIE_API_SHAPE", cookies: selected}; |
| | | if (rawCookies.length > MAX_COOKIE_COUNT) { |
| | | return {ok: false, reason: "COOKIE_TRANSFER_OVERFLOW", cookies: selected}; |
| | | } |
| | | for (const rawCookie of rawCookies) { |
| | | const relevance = selectedCookieRelevance(rawCookie, storeId, bvid, proof); |
| | | if (relevance === "IRRELEVANT_NAME" || relevance === "IRRELEVANT_PARTITIONED") continue; |
| | | if (relevance !== "SELECTED") { |
| | | return {ok: false, reason: "COOKIE_RELEVANCE_AMBIGUOUS", cookies: selected}; |
| | | } |
| | | const cookie = normalizeCookie(rawCookie, storeId); |
| | | selected.push(cookie); |
| | | if (!validCookie(cookie, storeId, proof, bvid)) { |
| | | return {ok: false, reason: "COOKIE_SELECTED_INVALID", cookies: selected}; |
| | | } |
| | | if (REQUIRED_AUTH_COOKIE_NAMES.has(cookie.name)) requiredNames.add(cookie.name); |
| | | } |
| | | if (selected.length === 0) return {ok: false, reason: "COOKIE_AUTH_SET_EMPTY", cookies: selected}; |
| | | for (const requiredName of REQUIRED_AUTH_COOKIE_NAMES) { |
| | | if (!requiredNames.has(requiredName)) { |
| | | return {ok: false, reason: "COOKIE_AUTH_SET_EMPTY", cookies: selected}; |
| | | } |
| | | } |
| | | const pathSpecificity = cookie => new TextEncoder().encode(cookie.path).length; |
| | | const bestSpecificity = Math.max(...selected.map(pathSpecificity)); |
| | | const highestPrecedence = selected.filter(cookie => pathSpecificity(cookie) === bestSpecificity); |
| | | const firstValue = highestPrecedence[0].value; |
| | | if (highestPrecedence.some(cookie => cookie.value !== firstValue)) { |
| | | return {ok: false, reason: "COOKIE_EQUAL_PRECEDENCE_CONFLICT", cookies: selected}; |
| | | } |
| | | highestPrecedence.sort((left, right) => |
| | | Number(right.host_only) - Number(left.host_only) || |
| | | left.domain.localeCompare(right.domain) || left.path.localeCompare(right.path) || |
| | | left.same_site.localeCompare(right.same_site) || Number(right.secure) - Number(left.secure) || |
| | | Number(right.http_only) - Number(left.http_only) || Number(right.session) - Number(left.session) |
| | | ); |
| | | return true; |
| | | const chosen = highestPrecedence[0]; |
| | | for (const cookie of selected) { |
| | | if (cookie !== chosen) { |
| | | cookie.name = ""; |
| | | cookie.value = ""; |
| | | } |
| | | } |
| | | const transfer = [chosen]; |
| | | selected.length = 0; |
| | | const valid = validCookieSet(transfer, storeId, proof, bvid); |
| | | return {ok: valid, reason: valid ? "COOKIE_TRANSFER_READY" : "COOKIE_TRANSFER_INVALID", cookies: transfer}; |
| | | } |
| | | |
| | | function zeroizeCookies(rawCookies, cookies) { |
| | | for (const cookie of Array.isArray(cookies) ? cookies : []) { |
| | | cookie.name = ""; |
| | | cookie.value = ""; |
| | | } |
| | | for (const cookie of Array.isArray(rawCookies) ? rawCookies : []) { |
| | | if (cookie !== null && typeof cookie === "object") { |
| | | try { cookie.name = ""; } catch {} |
| | | try { cookie.value = ""; } catch {} |
| | | } |
| | | } |
| | | if (Array.isArray(cookies)) cookies.length = 0; |
| | | if (Array.isArray(rawCookies)) rawCookies.length = 0; |
| | | } |
| | | |
| | | async function abortPreparedForCookieAccess(session, job, leaseId, prepareId, rawCookies, cookies, reason) { |
| | | lastCookieAccessReason = COOKIE_ACCESS_REASONS.has(reason) ? reason : "COOKIE_SELECTION_EXCEPTION"; |
| | | zeroizeCookies(rawCookies, cookies); |
| | | const aborted = await session.send({ |
| | | schema: SCHEMA, type: "abort_prepare", job_id: job.job_id, lease_id: leaseId, |
| | | prepare_id: prepareId, error_code: "E_COOKIE_ACCESS", error_reason: lastCookieAccessReason |
| | | }); |
| | | if (!(await validateResponse(aborted, "abort_prepare")) || aborted.phase !== "FAILED" || |
| | | aborted.error_code !== "E_COOKIE_ACCESS" || aborted.prepare_id !== prepareId || |
| | | aborted.job?.job_id !== job.job_id || aborted.lease_id !== leaseId) { |
| | | throw new Error("E_PREPARE"); |
| | | } |
| | | return aborted; |
| | | } |
| | | |
| | | async function runJobCore(session, job, leaseId, lifecycle) { |
| | | lastTabDriftReason = null; |
| | | lastCookieAccessReason = null; |
| | | lastPageReadinessDiagnostic = null; |
| | | let selectedTab; |
| | | let proof; |
| | | const flowStartedAtUnixMs = Date.now(); |
| | | const foregroundFlow = { |
| | | startedAtUnixMs: flowStartedAtUnixMs, |
| | | deadlineUnixMs: flowStartedAtUnixMs + PAGE_PROOF_DEADLINE_MS, |
| | | foregroundUsed: false |
| | | }; |
| | | try { |
| | | selectedTab = await targetTab(session, job, leaseId, foregroundFlow); |
| | | lifecycle.binding = selectedTab.binding; |
| | | proof = await acquirePageProof(session, job, leaseId, selectedTab, foregroundFlow); |
| | | await revalidateBoundTab(selectedTab.binding, job.bvid); |
| | | } catch (error) { |
| | | const code = typeof error?.message === "string" && PREPARELESS_REJECT_CODES.has(error.message) |
| | | ? error.message : null; |
| | | if (code === null) throw error; |
| | | const candidate = isPageReadinessDiagnostic(error?.page_readiness_diagnostic) |
| | | ? error.page_readiness_diagnostic |
| | | : isPageReadinessDiagnostic(lastPageReadinessDiagnostic) |
| | | ? lastPageReadinessDiagnostic |
| | | : fallbackPageReadinessDiagnostic(code); |
| | | const diagnostic = isCanonicalPreparelessTerminal(code, candidate) |
| | | ? candidate : fallbackPageReadinessDiagnostic(code); |
| | | if (!isCanonicalPreparelessTerminal(code, diagnostic)) throw new Error("E_REJECT"); |
| | | const rejected = await session.send({ |
| | | schema: SCHEMA, type: "reject", job_id: job.job_id, lease_id: leaseId, |
| | | error_code: code, diagnostic |
| | | }); |
| | | if (!(await validateResponse(rejected, "reject")) || rejected.phase !== "FAILED" || |
| | | rejected.error_code !== code || rejected.job?.job_id !== job.job_id || rejected.lease_id !== leaseId) { |
| | | throw new Error("E_REJECT"); |
| | | } |
| | | return rejected; |
| | | } |
| | | const prepareId = randomHex(16); |
| | | const prepared = await session.send({ |
| | | schema: SCHEMA, type: "prepare", extension_build: EXTENSION_BUILD, |
| | | lease_id: leaseId, prepare_id: prepareId, job, page_proof: proof |
| | | }, 150000); |
| | | const preparedValid = await validateResponse(prepared, "prepare"); |
| | | if (preparedValid && prepared.phase === "COMPLETE" && prepared.prepare_id === prepareId && |
| | | prepared.job?.job_id === job.job_id && prepared.lease_id === leaseId && |
| | | prepared.progress === 100 && prepared.error_code === null && |
| | | prepared.formal_filename === `${job.bvid}.mkv` && |
| | | prepared.mapping_filename === `${job.bvid}.download.json`) { |
| | | return prepared; |
| | | } |
| | | if (!preparedValid || prepared.phase !== "READY" || prepared.prepare_id !== prepareId) { |
| | | const code = preparedValid && prepared.phase === "FAILED" && ERROR_CODE_RE.test(prepared.error_code || "") |
| | | ? prepared.error_code : "E_PREPARE"; |
| | | throw new Error(code); |
| | | } |
| | | const storeId = selectedTab.binding.cookieStoreId; |
| | | let rawCookies = []; |
| | | let cookies = []; |
| | | try { |
| | | rawCookies = await chrome.cookies.getAll({ |
| | | url: canonicalUrl(job.bvid), |
| | | name: AUTH_COOKIE_NAME, |
| | | storeId |
| | | }); |
| | | } catch { |
| | | return abortPreparedForCookieAccess(session, job, leaseId, prepareId, rawCookies, cookies, "COOKIE_API_ERROR"); |
| | | } |
| | | let selection; |
| | | try { |
| | | selection = selectTransferCookies(rawCookies, storeId, proof, job.bvid); |
| | | } catch { |
| | | return abortPreparedForCookieAccess( |
| | | session, job, leaseId, prepareId, rawCookies, cookies, "COOKIE_SELECTION_EXCEPTION" |
| | | ); |
| | | } |
| | | cookies = selection.cookies; |
| | | if (!selection.ok) { |
| | | return abortPreparedForCookieAccess( |
| | | session, job, leaseId, prepareId, rawCookies, cookies, selection.reason |
| | | ); |
| | | } |
| | | let started; |
| | | try { |
| | | started = await session.send({ |
| | | schema: SCHEMA, type: "start", extension_build: EXTENSION_BUILD, |
| | | lease_id: leaseId, prepare_id: prepareId, job, cookie_store_id: storeId, |
| | | page_proof: proof, cookies |
| | | }, 30000); |
| | | } finally { |
| | | zeroizeCookies(rawCookies, cookies); |
| | | } |
| | | if (!(await validateResponse(started, "status")) || started.job?.job_id !== job.job_id) { |
| | | throw new Error("E_START"); |
| | | } |
| | | let state = started; |
| | | while (!["COMPLETE", "FAILED", "POSTPROCESS_FAILED", "CANCELED"].includes(state.phase)) { |
| | | await new Promise(resolve => setTimeout(resolve, 1000)); |
| | | state = await session.send({schema: SCHEMA, type: "status", job_id: job.job_id, lease_id: leaseId}); |
| | | if (!(await validateResponse(state, "status")) || state.job?.job_id !== job.job_id) throw new Error("E_STATUS"); |
| | | } |
| | | return state; |
| | | } |
| | | |
| | | async function runJob(session, job, leaseId) { |
| | | const lifecycle = {binding: null}; |
| | | try { |
| | | return await runJobCore(session, job, leaseId, lifecycle); |
| | | } finally { |
| | | // The durable session record is the authority. This also closes a tab when |
| | | // a restart/rebind path lost its in-memory binding after Host start. |
| | | await cleanupOwnedTab(); |
| | | } |
| | | } |
| | | |
| | | async function drainQueue() { |
| | | if (drainLease) return; |
| | | drainLease = true; |
| | | lastDrainErrorCode = null; |
| | | let session; |
| | | try { |
| | | session = nativeSession(); |
| | | const hello = await session.send({schema: SCHEMA, type: "hello", extension_build: EXTENSION_BUILD}); |
| | | const helloValid = await validateResponse(hello, "hello"); |
| | | if (!helloValid) throw new Error("E_HELLO"); |
| | | if (hello.phase === "FAILED") throw new Error(hello.error_code || "E_HOST"); |
| | | if (hello.maintenance.reload_required) { |
| | | const token = hello.maintenance.reload_token; |
| | | if (!HEX32_RE.test(token || "")) return; |
| | | const response = await session.send({schema: SCHEMA, type: "reload_begin", extension_build: EXTENSION_BUILD, reload_token: token}); |
| | | if (!(await validateResponse(response, "reload_begin")) || response.phase !== "RELOAD_REQUIRED") return; |
| | | session.disconnect(); |
| | | session = null; |
| | | chrome.runtime.reload(); |
| | | return; |
| | | } |
| | | const polled = await session.send({schema: SCHEMA, type: "poll"}); |
| | | const pollValid = await validateResponse(polled, "poll"); |
| | | if (!pollValid) throw new Error("E_POLL"); |
| | | const owned = await loadOwnedTabRecord(); |
| | | const continuesOwnedJob = polled.phase === "READY" && polled.job && polled.lease_id && |
| | | owned !== null && owned.job_id === polled.job.job_id && owned.lease_id === polled.lease_id; |
| | | if (owned !== null && !continuesOwnedJob) await cleanupOwnedTab(owned); |
| | | if (["FAILED", "POSTPROCESS_FAILED"].includes(polled.phase)) { |
| | | throw new Error(polled.error_code || "E_HOST"); |
| | | } |
| | | if (polled.phase === "COMPLETE") { |
| | | if (!polled.job || !polled.lease_id || polled.progress !== 100 || polled.error_code !== null || |
| | | polled.formal_filename !== `${polled.job.bvid}.mkv` || |
| | | polled.mapping_filename !== `${polled.job.bvid}.download.json`) { |
| | | throw new Error("E_POLL"); |
| | | } |
| | | return; |
| | | } |
| | | if (polled.phase !== "READY" || !polled.job || !polled.lease_id) return; |
| | | await runJob(session, polled.job, polled.lease_id); |
| | | } catch (error) { |
| | | // Memory-only safe code: never persist secrets, URLs, protocol payloads, or Host diagnostics here. |
| | | lastDrainErrorCode = safeErrorCode(error); |
| | | } finally { |
| | | if (session) session.disconnect(); |
| | | drainLease = false; |
| | | } |
| | | } |
| | | |
| | | function schedule() { |
| | | chrome.alarms.create("bili-auth-queue", {periodInMinutes: 1}); |
| | | void drainQueue(); |
| | | } |
| | | |
| | | chrome.runtime.onInstalled.addListener(schedule); |
| | | chrome.runtime.onStartup.addListener(schedule); |
| | | chrome.alarms.onAlarm.addListener(alarm => { |
| | | if (alarm?.name === "bili-auth-queue") void drainQueue(); |
| | | }); |