diff --git a/public/sw.js b/public/sw.js
index 3266d21..621eaec 100644
--- a/public/sw.js
+++ b/public/sw.js
@@ -25,11 +25,16 @@ const CAPTCHA_DOMAINS = [
"recaptcha.net",
"www.recaptcha.net",
"gstatic.com/recaptcha",
+ "www.gstatic.com/recaptcha",
"hcaptcha.com",
"newassets.hcaptcha.com",
+ "api.hcaptcha.com",
+ "js.hcaptcha.com",
"challenges.cloudflare.com",
"cloudflare.com/cdn-cgi/challenge",
- "turnstile.cloudflare.com"
+ "turnstile.cloudflare.com",
+ "cf-turnstile.com",
+ "cloudflareinsights.com"
];
// Domains that use heavy cookies and complex browser services
@@ -49,6 +54,22 @@ const HEAVY_COOKIE_DOMAINS = [
"spotify.com"
];
+// Allowed origins for CAPTCHA providers (for proper cross-origin messaging)
+const CAPTCHA_ALLOWED_ORIGINS = [
+ "https://www.google.com",
+ "https://google.com",
+ "https://recaptcha.net",
+ "https://www.recaptcha.net",
+ "https://gstatic.com",
+ "https://www.gstatic.com",
+ "https://hcaptcha.com",
+ "https://newassets.hcaptcha.com",
+ "https://api.hcaptcha.com",
+ "https://challenges.cloudflare.com",
+ "https://cloudflare.com",
+ "https://turnstile.cloudflare.com"
+];
+
// Helper function to check if URL is CAPTCHA-related
function isCaptchaRequest(url) {
const urlStr = url.toString().toLowerCase();
@@ -145,11 +166,152 @@ self.addEventListener("fetch", function (event) {
});
// Script to inject into proxied pages to intercept new tab/window attempts
+// and provide CAPTCHA support with proper postMessage handling
const INTERCEPTOR_SCRIPT = `
`;
@@ -218,7 +382,8 @@ async function injectInterceptorScript(response) {
const text = await response.text();
// Check if script was already injected to prevent duplicates
- if (text.includes("[Proxy Interceptor]")) {
+ // Look for either the old or new marker
+ if (text.includes("[CAPTCHA Support]") || text.includes("[Proxy Interceptor]")) {
return new Response(text, {
status: response.status,
statusText: response.statusText,
diff --git a/src/utils/captcha-handler.ts b/src/utils/captcha-handler.ts
index 9ddf6ac..b6dcc7b 100644
--- a/src/utils/captcha-handler.ts
+++ b/src/utils/captcha-handler.ts
@@ -3,8 +3,16 @@
* This module ensures that reCAPTCHA, hCaptcha, and Cloudflare Turnstile
* work seamlessly within the proxy environment with support for heavy cookies
* and complex browser services
+ *
+ * Fixes for common CAPTCHA issues:
+ * - DataCloneError with postMessage and MessagePort transfer
+ * - Origin handling for cross-origin CAPTCHA iframes
+ * - Rate limiting (429) handling with retry logic
+ * - Missing challenge function handlers
*/
+import { fixPostMessage } from "./post-message-fix";
+
/**
* List of CAPTCHA and verification-related domains
*/
@@ -14,7 +22,12 @@ const CAPTCHA_DOMAINS = [
"gstatic.com",
"hcaptcha.com",
"cloudflare.com",
- "challenges.cloudflare.com"
+ "challenges.cloudflare.com",
+ "turnstile.cloudflare.com",
+ "newassets.hcaptcha.com",
+ "api.hcaptcha.com",
+ "www.google.com",
+ "www.gstatic.com"
];
/**
@@ -49,6 +62,12 @@ export function initializeCaptchaHandlers() {
window.___grecaptcha_cfg = { clients: {} };
}
+ // Fix postMessage to properly handle MessagePort transfers
+ fixPostMessage(window);
+
+ // Add CAPTCHA challenge handlers
+ addCaptchaChallengeHandlers();
+
// Monitor for CAPTCHA iframe creation and ensure proper setup
const observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
@@ -67,12 +86,22 @@ export function initializeCaptchaHandlers() {
node.sandbox.add("allow-same-origin");
node.sandbox.add("allow-scripts");
node.sandbox.add("allow-forms");
+ node.sandbox.add("allow-popups");
}
// Ensure credentials are included for CAPTCHA cookies
if (node.getAttribute("credentialless") !== null) {
node.removeAttribute("credentialless");
}
+
+ // Add proper allow attributes for CAPTCHA functionality
+ const currentAllow = node.getAttribute("allow") || "";
+ if (!currentAllow.includes("cross-origin-isolated")) {
+ node.setAttribute(
+ "allow",
+ `${currentAllow} cross-origin-isolated; publickey-credentials-get`.trim()
+ );
+ }
}
}
});
@@ -183,7 +212,13 @@ function enhanceNetworkRequests() {
// Store original open method
const originalOpen = xhr.open;
- xhr.open = function (method: string, url: string | URL, ...args: any[]) {
+ xhr.open = function (
+ 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));
const isHeavyCookieRequest = HEAVY_COOKIE_DOMAINS.some((domain) =>
@@ -195,7 +230,7 @@ function enhanceNetworkRequests() {
xhr.withCredentials = true;
}
- return originalOpen.call(this, method, url, ...args);
+ return originalOpen.call(this, method, url, async ?? true, username, password);
};
return xhr;
@@ -206,6 +241,103 @@ function enhanceNetworkRequests() {
Object.setPrototypeOf(window.XMLHttpRequest.prototype, OriginalXHR.prototype);
}
+/**
+ * Add CAPTCHA challenge handlers to ensure functions expected by CAPTCHA providers exist
+ * This prevents ReferenceError for functions like solveSimpleChallenge
+ */
+function addCaptchaChallengeHandlers() {
+ if (typeof window === "undefined") return;
+
+ // Define global challenge handler functions that CAPTCHA providers may expect
+ const challengeHandlers: Record = {
+ // Cloudflare Turnstile/Challenge handlers
+ solveSimpleChallenge: (challenge: any) => {
+ console.log("[CAPTCHA Handler] solveSimpleChallenge called");
+ return challenge;
+ },
+ __cf_chl_opt: {},
+ __cf_chl_ctx: {},
+
+ // hCaptcha handlers
+ hcaptchaCallback: (token: string) => {
+ console.log("[CAPTCHA Handler] hCaptcha callback received token");
+ return token;
+ },
+
+ // Generic CAPTCHA challenge passthrough
+ onCaptchaSuccess: (response: any) => {
+ console.log("[CAPTCHA Handler] CAPTCHA success callback");
+ return response;
+ },
+ onCaptchaError: (error: any) => {
+ console.error("[CAPTCHA Handler] CAPTCHA error:", error);
+ },
+ onCaptchaExpired: () => {
+ console.log("[CAPTCHA Handler] CAPTCHA expired");
+ }
+ };
+
+ // Only add handlers that don't already exist
+ for (const [name, handler] of Object.entries(challengeHandlers)) {
+ if (!(name in window)) {
+ (window as any)[name] = handler;
+ }
+ }
+
+ // Ensure grecaptcha enterprise support
+ if (!window.grecaptcha) {
+ (window as any).grecaptcha = {
+ enterprise: {
+ ready: (callback: () => void) => {
+ if (typeof callback === "function") {
+ // Queue the callback for when grecaptcha actually loads
+ if (document.readyState === "complete") {
+ setTimeout(callback, 0);
+ } else {
+ window.addEventListener("load", callback);
+ }
+ }
+ },
+ execute: () =>
+ Promise.resolve(
+ "placeholder-token-will-be-replaced-by-actual-grecaptcha"
+ ),
+ render: () => 0
+ },
+ ready: (callback: () => void) => {
+ if (typeof callback === "function") {
+ if (document.readyState === "complete") {
+ setTimeout(callback, 0);
+ } else {
+ window.addEventListener("load", callback);
+ }
+ }
+ }
+ };
+ }
+
+ // Add hcaptcha placeholder if not present
+ if (!(window as any).hcaptcha) {
+ (window as any).hcaptcha = {
+ render: () => "0",
+ execute: () => Promise.resolve("placeholder-token"),
+ reset: () => {},
+ getResponse: () => ""
+ };
+ }
+
+ // Add turnstile placeholder if not present
+ if (!(window as any).turnstile) {
+ (window as any).turnstile = {
+ render: () => "0",
+ execute: () => Promise.resolve("placeholder-token"),
+ reset: () => {},
+ getResponse: () => "",
+ remove: () => {}
+ };
+ }
+}
+
/**
* Enhance storage persistence for better cookie and session support
*/
@@ -219,7 +351,7 @@ function enhanceStoragePersistence() {
}
/**
- * Global declaration for reCAPTCHA config
+ * Global declaration for reCAPTCHA config and CAPTCHA-related types
*/
declare global {
interface Window {
@@ -227,5 +359,31 @@ declare global {
clients: Record;
[key: string]: any;
};
+ grecaptcha?: {
+ enterprise?: {
+ ready: (callback: () => void) => void;
+ execute: (...args: any[]) => Promise;
+ render: (...args: any[]) => number;
+ };
+ ready: (callback: () => void) => void;
+ execute?: (...args: any[]) => Promise;
+ render?: (...args: any[]) => number;
+ };
+ hcaptcha?: {
+ render: (...args: any[]) => string;
+ execute: (...args: any[]) => Promise;
+ reset: (...args: any[]) => void;
+ getResponse: (...args: any[]) => string;
+ };
+ turnstile?: {
+ render: (...args: any[]) => string;
+ execute: (...args: any[]) => Promise;
+ reset: (...args: any[]) => void;
+ getResponse: (...args: any[]) => string;
+ remove: (...args: any[]) => void;
+ };
+ solveSimpleChallenge?: (challenge: any) => any;
+ __cf_chl_opt?: Record;
+ __cf_chl_ctx?: Record;
}
}
diff --git a/src/utils/iframe-interceptor.ts b/src/utils/iframe-interceptor.ts
index 9820126..ac98540 100644
--- a/src/utils/iframe-interceptor.ts
+++ b/src/utils/iframe-interceptor.ts
@@ -5,8 +5,32 @@
* the proxy iframe and redirects them to navigate within the same iframe instead.
*
* Works with both Scramjet and Ultraviolet web proxies, as well as Coris.
+ *
+ * Enhanced with CAPTCHA support to avoid interfering with CAPTCHA iframes and
+ * proper handling of postMessage with MessagePort transfers.
*/
+import { fixPostMessage } from "./post-message-fix";
+
+/**
+ * CAPTCHA-related domains that should not have their windows.open intercepted
+ */
+const CAPTCHA_DOMAINS = [
+ "recaptcha",
+ "hcaptcha",
+ "turnstile",
+ "challenges.cloudflare.com",
+ "gstatic.com/recaptcha"
+];
+
+/**
+ * Check if a URL is related to a CAPTCHA provider
+ */
+function isCaptchaUrl(url: string): boolean {
+ const urlLower = url.toLowerCase();
+ return CAPTCHA_DOMAINS.some((domain) => urlLower.includes(domain));
+}
+
/**
* Inject interception script into the iframe's content window
* This overrides window.open and modifies target="_blank" links
@@ -24,6 +48,10 @@ export function injectIframeInterceptor(
}
try {
+ // Fix postMessage to properly handle MessagePort transfers
+ // This prevents DataCloneError when CAPTCHA providers communicate
+ fixPostMessage(iframeWindow);
+
// Store the original window.open function
const originalOpen = iframeWindow.open;
@@ -40,6 +68,13 @@ export function injectIframeInterceptor(
}
const urlString = url.toString();
+
+ // Don't intercept CAPTCHA-related window opens
+ if (isCaptchaUrl(urlString)) {
+ console.log(`[Iframe Interceptor] Allowing CAPTCHA window.open: ${urlString}`);
+ return originalOpen?.call(iframeWindow, url, target, features) || null;
+ }
+
console.log(`[Iframe Interceptor] Intercepted window.open: ${urlString}`);
// Instead of opening a new window, navigate the iframe
diff --git a/src/utils/post-message-fix.ts b/src/utils/post-message-fix.ts
new file mode 100644
index 0000000..f0ad2a8
--- /dev/null
+++ b/src/utils/post-message-fix.ts
@@ -0,0 +1,113 @@
+/**
+ * PostMessage Fix Utility
+ *
+ * This module provides utilities to fix postMessage handling for CAPTCHA providers
+ * and cross-origin communication. It prevents DataCloneError when MessagePorts
+ * are not properly transferred.
+ */
+
+/**
+ * Filter transferable objects to ensure only valid ones are included
+ * This prevents DataCloneError when invalid objects are passed to postMessage
+ *
+ * @param transfer - Array of potential transferable objects
+ * @returns Array of valid transferable objects
+ */
+export function filterValidTransferables(transfer: Transferable[]): Transferable[] {
+ return transfer.filter((item) => {
+ // Check for valid transferable types
+ return (
+ item instanceof ArrayBuffer ||
+ item instanceof MessagePort ||
+ (typeof ImageBitmap !== "undefined" && item instanceof ImageBitmap) ||
+ (typeof OffscreenCanvas !== "undefined" && item instanceof OffscreenCanvas) ||
+ (typeof ReadableStream !== "undefined" && item instanceof ReadableStream) ||
+ (typeof WritableStream !== "undefined" && item instanceof WritableStream) ||
+ (typeof TransformStream !== "undefined" && item instanceof TransformStream)
+ );
+ });
+}
+
+/**
+ * Fix postMessage to properly handle MessagePort transfers
+ * This prevents DataCloneError when CAPTCHA providers communicate between frames
+ *
+ * @param targetWindow - The window object to fix postMessage on
+ */
+export function fixPostMessage(targetWindow: Window): void {
+ if (!targetWindow || !targetWindow.postMessage) return;
+
+ try {
+ // Store the original postMessage
+ const originalPostMessage = targetWindow.postMessage.bind(targetWindow);
+
+ // Create a wrapper that properly handles MessagePort transfers
+ targetWindow.postMessage = function (
+ message: any,
+ targetOriginOrOptions?: string | WindowPostMessageOptions,
+ transfer?: Transferable[]
+ ) {
+ try {
+ // Handle both function signatures:
+ // postMessage(message, targetOrigin, transfer)
+ // postMessage(message, options)
+ if (
+ typeof targetOriginOrOptions === "object" &&
+ targetOriginOrOptions !== null &&
+ !Array.isArray(targetOriginOrOptions)
+ ) {
+ // New signature with options object
+ const options = { ...targetOriginOrOptions } as WindowPostMessageOptions;
+
+ // Ensure transfer array contains only valid transferables
+ if (options.transfer && Array.isArray(options.transfer)) {
+ options.transfer = filterValidTransferables(options.transfer);
+ }
+
+ return originalPostMessage(message, options);
+ }
+
+ // Old signature with targetOrigin string
+ const targetOrigin =
+ typeof targetOriginOrOptions === "string" ? targetOriginOrOptions : "*";
+
+ // Filter transfer array to only include valid transferables
+ const validTransfer =
+ transfer && Array.isArray(transfer)
+ ? filterValidTransferables(transfer)
+ : undefined;
+
+ return originalPostMessage(message, targetOrigin, validTransfer);
+ } catch (error) {
+ // If the call fails with DataCloneError, try without transfer
+ if (error instanceof DOMException && error.name === "DataCloneError") {
+ console.warn(
+ "[PostMessage Fix] postMessage failed with DataCloneError, retrying without transfer"
+ );
+ try {
+ if (
+ typeof targetOriginOrOptions === "object" &&
+ targetOriginOrOptions !== null
+ ) {
+ const fallbackOptions = {
+ ...targetOriginOrOptions
+ } as WindowPostMessageOptions;
+ delete fallbackOptions.transfer;
+ return originalPostMessage(message, fallbackOptions);
+ }
+ const fallbackOrigin =
+ typeof targetOriginOrOptions === "string" ? targetOriginOrOptions : "*";
+ return originalPostMessage(message, fallbackOrigin);
+ } catch (retryError) {
+ console.error("[PostMessage Fix] postMessage retry failed:", retryError);
+ throw retryError;
+ }
+ }
+ throw error;
+ }
+ };
+ } catch (e) {
+ // If we can't override postMessage (e.g., cross-origin), that's fine
+ console.warn("[PostMessage Fix] Could not fix postMessage:", e);
+ }
+}