Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion public/sw.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,10 @@ const CAPTCHA_DOMAINS = [
"newassets.hcaptcha.com",
"challenges.cloudflare.com",
"cloudflare.com/cdn-cgi/challenge",
"turnstile.cloudflare.com"
"turnstile.cloudflare.com",
"yandex.com/captcha",
"yandex.ru/captcha",
"smartcaptcha.yandexcloud.net"
];

// Helper function to check if URL is CAPTCHA-related
Expand Down
238 changes: 232 additions & 6 deletions src/utils/captcha-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,10 @@ const CAPTCHA_DOMAINS = [
"gstatic.com",
"hcaptcha.com",
"cloudflare.com",
"challenges.cloudflare.com"
"challenges.cloudflare.com",
"yandex.com",
"yandex.ru",
"smartcaptcha.yandexcloud.net"
];

/**
Expand All @@ -23,6 +26,12 @@ const CAPTCHA_DOMAINS = [
export function initializeCaptchaHandlers() {
if (typeof window === "undefined") return;

// Fix MessagePort cloning errors for CAPTCHA iframes
fixMessagePortCloning();

// Add missing Cloudflare challenge solver functions
addCloudflareChallengeHandlers();

// Ensure global CAPTCHA callbacks are accessible
if (!window.___grecaptcha_cfg) {
window.___grecaptcha_cfg = { clients: {} };
Expand Down Expand Up @@ -71,6 +80,143 @@ export function initializeCaptchaHandlers() {
enhanceNetworkRequests();
}

/**
* Fix MessagePort cloning errors that occur in CAPTCHA iframes
* This prevents "DataCloneError: Failed to execute 'postMessage' on 'Window'" errors
*/
function fixMessagePortCloning() {
// Maximum recursion depth when searching for MessagePort objects
const MAX_RECURSION_DEPTH = 10;

const originalPostMessage = window.postMessage.bind(window);

// Helper to check if an object is a MessagePort
const isMessagePort = (obj: any): boolean => {
if (!obj) return false;
// Try instanceof first (most reliable)
if (obj instanceof MessagePort) return true;
// Fallback: check for MessagePort-like interface
if (
typeof obj === "object" &&
typeof obj.postMessage === "function" &&
typeof obj.start === "function" &&
typeof obj.close === "function"
) {
// Additional check for constructor name as a hint (not definitive)
return obj.constructor?.name === "MessagePort" || obj.toString() === "[object MessagePort]";
}
return false;
};

// Override postMessage to properly handle MessagePort transfers
// Note: Using unknown for message type as it can be any cloneable data
(window as any).postMessage = function (message: unknown, ...args: any[]) {
try {
// Handle both old (targetOrigin, transfer) and new (options) signatures
const targetOrigin = typeof args[0] === "string" ? args[0] : "*";
let transfer = args[1];

// Handle new WindowPostMessageOptions signature
if (typeof args[0] === "object" && args[0] !== null && "targetOrigin" in args[0]) {
const options = args[0] as WindowPostMessageOptions;
transfer = options.transfer;
return originalPostMessage(message, options);
}

// If transfer array contains MessagePort objects, ensure they are properly transferred
if (transfer && Array.isArray(transfer)) {
const hasMessagePort = transfer.some((item: any) => isMessagePort(item));

if (hasMessagePort) {
// Use the transfer parameter explicitly
return originalPostMessage(message, targetOrigin, transfer);
}
}

// For other cases, check if message contains MessagePort and auto-detect transfer
if (message && typeof message === "object") {
const ports: MessagePort[] = [];
const collectPorts = (obj: any, depth: number = 0) => {
// Limit recursion depth to prevent infinite loops
if (depth > MAX_RECURSION_DEPTH) return;

if (isMessagePort(obj)) {
ports.push(obj);
} else if (obj && typeof obj === "object") {
for (const key in obj) {
if (Object.prototype.hasOwnProperty.call(obj, key)) {
collectPorts(obj[key], depth + 1);
}
}
}
};
collectPorts(message);

if (ports.length > 0) {
// Auto-transfer detected MessagePorts
return originalPostMessage(message, targetOrigin, ports);
}
}

// Standard call
if (transfer !== undefined) {
return originalPostMessage(message, targetOrigin, transfer);
} else {
return originalPostMessage(message, targetOrigin);
}
} catch (error) {
// Fallback: try without transfer parameter
console.warn("postMessage transfer failed, attempting without transfer:", error);
try {
const targetOrigin = typeof args[0] === "string" ? args[0] : "*";
return originalPostMessage(message, targetOrigin);
} catch (fallbackError) {
console.error("postMessage completely failed:", fallbackError);
throw fallbackError;
}
}
};
}

/**
* Add missing Cloudflare challenge solver functions
* This fixes "ReferenceError: solveSimpleChallenge is not defined" errors
*
* Note: These are stub implementations. Cloudflare's actual challenge solving
* is handled by their own scripts loaded in the page. These functions just need
* to exist to prevent reference errors when Cloudflare scripts try to call them.
*/
function addCloudflareChallengeHandlers() {
// Define solveSimpleChallenge for Cloudflare Turnstile/Challenge pages
// This is a stub - actual challenge solving is done by Cloudflare's own scripts
if (typeof (window as any).solveSimpleChallenge === "undefined") {
(window as any).solveSimpleChallenge = function () {
console.log("Simple challenge solver called - handled by Cloudflare scripts");
// Empty implementation - Cloudflare's scripts handle the actual solving
};
}

// Add support for managed challenge callback
// This receives the challenge token from Cloudflare after successful verification
if (typeof (window as any).managedChallengeCallback === "undefined") {
(window as any).managedChallengeCallback = function (token: string) {
const tokenPreview = token && token.length > 20 ? token.substring(0, 20) + "..." : token;
console.log("Managed challenge callback received token:", tokenPreview);
// The token is automatically used by Cloudflare's scripts
// This callback is just for logging/debugging purposes
};
}

// Add support for interactive challenge
// Called when user interaction is required (e.g., clicking a checkbox)
if (typeof (window as any).interactiveChallenge === "undefined") {
(window as any).interactiveChallenge = function () {
console.log("Interactive challenge initiated - waiting for user interaction");
// Cloudflare's scripts handle the actual UI and interaction
};
}
}

/**
* Enhance cookie handling to ensure CAPTCHA tokens are properly stored
*/
Expand Down Expand Up @@ -128,9 +274,25 @@ function enhanceNetworkRequests() {
if (!init.headers.has("Accept")) {
init.headers.set("Accept", "*/*");
}

// Set mode to cors for CAPTCHA requests to avoid CORS issues
if (!init.mode || init.mode === "navigate") {
init.mode = "cors";
}
}

return originalFetch.call(this, input, init);
return originalFetch.call(this, input, init).catch((error) => {
// Enhanced error handling for CAPTCHA requests
if (isCaptchaRequest) {
console.warn("CAPTCHA fetch error:", url, error);
// Try again without custom init for preload compatibility
if (init && (init.credentials || init.mode)) {
console.log("Retrying CAPTCHA request with default settings");
return originalFetch.call(this, input, { credentials: "include" });
}
}
throw error;
});
};

// Store original XMLHttpRequest
Expand All @@ -141,8 +303,17 @@ function enhanceNetworkRequests() {
const xhr = new OriginalXHR();

// Store original open method
const originalOpen = xhr.open;
xhr.open = function (method: string, url: string | URL, ...args: any[]) {
const originalOpen = xhr.open.bind(xhr);

// Override open method to add CAPTCHA support
xhr.open = function (
this: XMLHttpRequest,
method: string,
url: string | URL,
async?: boolean,
username?: string | null,
password?: string | null
) {
const urlStr = url.toString();
const isCaptchaRequest = CAPTCHA_DOMAINS.some((domain) => urlStr.includes(domain));

Expand All @@ -151,15 +322,70 @@ function enhanceNetworkRequests() {
xhr.withCredentials = true;
}

return originalOpen.call(this, method, url, ...args);
};
// Call original open with appropriate arguments
// Note: Using 'as any' here because XMLHttpRequest.open has overloaded signatures
// that TypeScript cannot properly infer when calling dynamically with variable arguments.
// This is safe because we're calling the same native method with its original signatures.
const openFn = originalOpen as any;
if (username !== undefined && password !== undefined) {
return openFn(method, url, async ?? true, username, password);
} else if (username !== undefined) {
return openFn(method, url, async ?? true, username);
} else if (async !== undefined) {
return openFn(method, url, async);
} else {
return openFn(method, url);
}
} as typeof xhr.open;

return xhr;
} as any;

// Copy static properties
Object.setPrototypeOf(window.XMLHttpRequest, OriginalXHR);
Object.setPrototypeOf(window.XMLHttpRequest.prototype, OriginalXHR.prototype);

// Fix preload resource loading for CAPTCHA scripts
enhancePreloadHandling();
}

/**
* Enhance preload handling to fix credential mode mismatches
*/
function enhancePreloadHandling() {
// Monitor for link elements being added to the page
const observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
mutation.addedNodes.forEach((node) => {
if (node instanceof HTMLLinkElement && node.rel === "preload") {
const href = node.href || "";
// Check if this is a CAPTCHA-related resource
if (CAPTCHA_DOMAINS.some((domain) => href.includes(domain))) {
// Ensure crossorigin attribute is set for proper credential handling
if (!node.hasAttribute("crossorigin")) {
node.setAttribute("crossorigin", "use-credentials");
}
}
}
// Also handle script tags that might be preloaded
if (node instanceof HTMLScriptElement) {
const src = node.src || "";
if (CAPTCHA_DOMAINS.some((domain) => src.includes(domain))) {
// Ensure crossorigin attribute is set
if (!node.hasAttribute("crossorigin")) {
node.setAttribute("crossorigin", "use-credentials");
}
}
}
});
});
});

// Start observing
observer.observe(document.documentElement, {
childList: true,
subtree: true
});
}

/**
Expand Down
2 changes: 1 addition & 1 deletion src/utils/proxy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -182,7 +182,7 @@ class SW {
}
});
if ("serviceWorker" in navigator) {
await this.#scramjetController.init();
await this.#scramjetController!.init();
navigator.serviceWorker.ready.then(async (reg) => {
console.log("SW ready to go!");
this.#serviceWorker = reg;
Expand Down