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
149 changes: 149 additions & 0 deletions docs/CAPTCHA_FIX.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
# CAPTCHA Compatibility Fix

## Problem Statement

When attempting to load CAPTCHA verification systems (reCAPTCHA, hCaptcha, Cloudflare Turnstile, Yandex Cloud, etc.) through the Ultraviolet proxy, the following errors occurred:

```
Uncaught DataCloneError: Failed to execute 'postMessage' on 'Window':
A MessagePort could not be cloned because it was not transferred.
```

Additionally, preload resource warnings appeared:
```
A preload for '...' is found, but is not used because the request credentials mode does not match.
Consider taking a look at crossorigin attribute.
```

## Root Cause

The issue stems from how Ultraviolet intercepts `postMessage` calls. CAPTCHA systems extensively use `postMessage` with `MessagePort` objects for secure cross-origin communication between iframes. According to the [Structured Clone Algorithm](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Structured_clone_algorithm), `MessagePort` objects cannot be cloned - they must be explicitly transferred via the `transfer` parameter.

When UV intercepts `postMessage`, it doesn't properly handle the transfer of `MessagePort` objects, causing the `DataCloneError`.

## Solution

### 1. CAPTCHA Patch Script (`/public/captcha-patch.js`)

A standalone script that must be loaded **before** the UV handler. It patches `Window.prototype.postMessage` to:

- Recursively scan the message object for `MessagePort` instances
- Automatically add found ports to the `transfer` array
- Properly invoke the native `postMessage` with transfers

**Key Features:**
- Avoids circular references with `WeakSet` tracking
- Fallback to original implementation on error
- Marks itself to prevent UV from overriding the fix

### 2. Enhanced CAPTCHA Handler (`src/utils/captcha-handler.ts`)

Extended to provide comprehensive CAPTCHA support:

**`patchPostMessage()` Function:**
- Secondary patch layer for iframe contentWindow
- Handles edge cases UV might miss
- Ensures proper MessagePort transfer in all contexts

**Preload Resource Monitor:**
- Watches for `<link rel="preload">` elements for CAPTCHA resources
- Automatically adds `crossorigin="anonymous"` attribute
- Sets appropriate `as` attribute (script, style, font) based on file type

**Supported CAPTCHA Providers:**
- Google reCAPTCHA v2/v3
- hCaptcha
- Cloudflare Turnstile
- Yandex Cloud CAPTCHA
- Other providers using similar patterns

### 3. Service Worker Updates (`public/sw.js`)

Added CAPTCHA domain detection for proper request handling:
- Preserves credentials for CAPTCHA cookies
- Ensures proper headers for CAPTCHA requests
- Handles special routing for verification domains

### 4. Proxy Initialization (`src/utils/proxy.ts`)

Modified to load the CAPTCHA patch **before** UV scripts:
```javascript
createScript("/captcha-patch.js", false); // Load first
createScript("/vu/uv.bundle.js", true);
createScript("/vu/uv.config.js", true);
```

## Technical Details

### MessagePort Transfer

The fix implements the proper way to handle MessagePorts in `postMessage`:

```javascript
// ❌ WRONG - Causes DataCloneError
window.postMessage(messageWithPort, "*");

// ✅ CORRECT - Transfers the port
window.postMessage(messageWithPort, "*", [messagePort]);
```

Our patch automatically detects ports in the message and constructs the proper transfer array.

### Crossorigin Attribute

CAPTCHA resources often load from different origins (e.g., `gstatic.com` for reCAPTCHA). Preload hints must match the credential mode:

```html
<!-- Without crossorigin, credentials mode mismatch occurs -->
<link rel="preload" href="https://www.gstatic.com/recaptcha/..." as="script">

<!-- Fixed with crossorigin attribute -->
<link rel="preload" href="https://www.gstatic.com/recaptcha/..." as="script" crossorigin="anonymous">
```

## Testing

To test CAPTCHA functionality:

1. Build the project: `npm run build`
2. Start the server: `npm start`
3. Navigate through the proxy to a site with CAPTCHA:
- reCAPTCHA: https://www.google.com/recaptcha/api2/demo
- hCaptcha: https://www.hcaptcha.com/
- Cloudflare Turnstile: Sites with Cloudflare bot protection

The CAPTCHA should load and function properly without console errors.

## Browser Compatibility

The fix is compatible with all modern browsers that support:
- `Window.prototype.postMessage`
- `MessagePort` API
- `MutationObserver`
- `WeakSet` (for circular reference detection)

This includes:
- Chrome/Edge 60+
- Firefox 55+
- Safari 11+

## Security Considerations

- The patch does not modify the security model of `postMessage`
- MessagePorts are still transferred (not cloned), maintaining their single-owner semantics
- CAPTCHA verification still occurs server-side; this only fixes client-side communication
- No sensitive data is exposed or logged

## Future Improvements

Potential enhancements:
- Upstream fix to Ultraviolet to natively handle MessagePort transfers
- Performance optimization for large object trees
- Support for additional transferable objects (e.g., ArrayBuffer)

## References

- [MDN: Window.postMessage()](https://developer.mozilla.org/en-US/docs/Web/API/Window/postMessage)
- [MDN: MessagePort](https://developer.mozilla.org/en-US/docs/Web/API/MessagePort)
- [Structured Clone Algorithm](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Structured_clone_algorithm)
- [HTML Spec: MessagePort transfer](https://html.spec.whatwg.org/multipage/web-messaging.html#message-ports)
95 changes: 95 additions & 0 deletions public/captcha-patch.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
/**
* CAPTCHA Compatibility Patch
* This script must be loaded BEFORE UV handler to fix MessagePort handling in postMessage
*
* Fixes the error: "DataCloneError: Failed to execute 'postMessage' on 'Window':
* A MessagePort could not be cloned because it was not transferred."
*/

(function () {
"use strict";

// Only run once
if (window.__captchaPatchApplied) return;
window.__captchaPatchApplied = true;

// Store the original postMessage method
const originalWindowPostMessage = Window.prototype.postMessage;

/**
* Enhanced postMessage that properly handles MessagePort transfers
* This is critical for CAPTCHA systems (reCAPTCHA, hCaptcha, Cloudflare Turnstile, Yandex)
*/
Window.prototype.postMessage = function (message, targetOrigin, transfer) {
try {
// If transfer is already provided, use it directly
if (transfer !== undefined) {
return originalWindowPostMessage.call(this, message, targetOrigin, transfer);
}

// Extract MessagePorts from the message to transfer them properly
const ports = [];

if (message && typeof message === "object") {
// Recursively find MessagePorts in the message
const findPorts = (obj, visited) => {
if (!obj || typeof obj !== "object") return;

// Avoid circular references
visited = visited || new WeakSet();
if (visited.has(obj)) return;
visited.add(obj);

// Check if this is a MessagePort
if (obj instanceof MessagePort) {
ports.push(obj);
return;
}

// Check arrays
if (Array.isArray(obj)) {
for (let i = 0; i < obj.length; i++) {
findPorts(obj[i], visited);
}
return;
}

// Check object properties
for (const key in obj) {
try {
if (Object.prototype.hasOwnProperty.call(obj, key)) {
findPorts(obj[key], visited);
}
} catch (e) {
// Ignore errors accessing properties (e.g., cross-origin)
}
}
};

findPorts(message);
}

// If we found MessagePorts, transfer them
if (ports.length > 0) {
return originalWindowPostMessage.call(this, message, targetOrigin, ports);
}

// Otherwise, use the original call
return originalWindowPostMessage.call(this, message, targetOrigin);
} catch (error) {
// If our enhanced version fails, try the original
console.warn("[CAPTCHA Patch] Enhanced postMessage failed, using fallback:", error);
try {
return originalWindowPostMessage.call(this, message, targetOrigin, transfer);
} catch (fallbackError) {
console.error("[CAPTCHA Patch] Original postMessage also failed:", fallbackError);
throw fallbackError;
}
}
};

// Mark the patched method to prevent UV from breaking it
Window.prototype.postMessage.__captchaPatched = true;

console.log("[CAPTCHA Patch] MessagePort handling enabled for CAPTCHA compatibility");
})();
6 changes: 5 additions & 1 deletion public/sw.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,11 @@ 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",
"yandex.net/captcha",
"captcha-delivery.com"
];

// Helper function to check if URL is CAPTCHA-related
Expand Down
Loading