From 6a4f5fb8bc497c7a64aec6bd16aba70fe4a34373 Mon Sep 17 00:00:00 2001 From: Hanashi Date: Sun, 23 Aug 2026 02:16:31 -0400 Subject: [PATCH] change webview user agent --- .../utils/midjourney_browser_profile.rs | 25 +++++++++++-------- .../login_window/login_window_trait.rs | 9 +++++++ .../logins/midjourney_login_window.rs | 9 +++++++ .../windows/login_window/open_login_window.rs | 15 ++++++++--- .../browser_emulation/src/browser_profile.rs | 13 ++++++++++ 5 files changed, 58 insertions(+), 13 deletions(-) diff --git a/crates/artcraftx/src/services/midjourney/utils/midjourney_browser_profile.rs b/crates/artcraftx/src/services/midjourney/utils/midjourney_browser_profile.rs index ae70e444..812d5d9b 100644 --- a/crates/artcraftx/src/services/midjourney/utils/midjourney_browser_profile.rs +++ b/crates/artcraftx/src/services/midjourney/utils/midjourney_browser_profile.rs @@ -1,17 +1,22 @@ use browser_emulation::browser_profile::BrowserProfile; +/// The User-Agent used for BOTH the Midjourney login webview and every +/// first-party Midjourney HTTP/websocket call. +/// +/// It must be identical in both places: Cloudflare's `cf_clearance` cookie +/// (captured by the webview) is bound to the exact User-Agent, so the wreq +/// calls that replay those cookies have to present the same string. A +/// mainstream desktop Safari UA (rather than the default WKWebView UA) also +/// makes Google's embedded-webview risk detection less likely to force a +/// passkey step-up during sign-in. +pub const MIDJOURNEY_USER_AGENT: &str = + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.6 Safari/605.1.15"; + /// The browser profile used for ALL first-party Midjourney HTTP/websocket /// traffic (submit, imagine, index page, image downloads, websocket upgrade). /// -/// Cloudflare's `cf_clearance` cookie is bound to the User-Agent of the browser -/// that solved the challenge — here, the app's login webview, which on macOS is -/// a WKWebView (Safari engine). So we emulate Safari to keep the fingerprint -/// consistent with the captured cookies. -/// -/// NB: for `cf_clearance` to validate, the emulated UA must match the webview's -/// actual UA closely. If Cloudflare still challenges, the next step is to -/// capture the webview's exact `navigator.userAgent` at login and use it as a -/// `BrowserProfile::Custom` UA override. +/// Safari fingerprint + the exact [`MIDJOURNEY_USER_AGENT`], to stay consistent +/// with the macOS WKWebView login window that captured the cookies. pub fn midjourney_browser_profile() -> BrowserProfile { - BrowserProfile::Safari18 + BrowserProfile::safari_macos_with_user_agent(MIDJOURNEY_USER_AGENT) } diff --git a/crates/artcraftx/src/windows/login_window/login_window_trait.rs b/crates/artcraftx/src/windows/login_window/login_window_trait.rs index eede5367..90090c8a 100644 --- a/crates/artcraftx/src/windows/login_window/login_window_trait.rs +++ b/crates/artcraftx/src/windows/login_window/login_window_trait.rs @@ -52,6 +52,15 @@ pub trait LoginWindowSite: Send + Sync { self.login_website().credential_service() } + /// An optional User-Agent override for the login webview. Return `Some` when + /// the site's sign-in misbehaves under the default WKWebView UA (e.g. Google + /// forcing a passkey step-up), or when the captured cookies must later be + /// replayed by an HTTP client under a specific UA (Cloudflare `cf_clearance` + /// is UA-bound, so the two must match). Defaults to the platform default. + fn user_agent(&self) -> Option<&'static str> { + None + } + /// SSO / identity-provider hosts that mean "still logging in". fn auth_flow_hostnames(&self) -> &[&str] { DEFAULT_AUTH_FLOW_HOSTNAMES diff --git a/crates/artcraftx/src/windows/login_window/logins/midjourney_login_window.rs b/crates/artcraftx/src/windows/login_window/logins/midjourney_login_window.rs index 7e062294..c82a302e 100644 --- a/crates/artcraftx/src/windows/login_window/logins/midjourney_login_window.rs +++ b/crates/artcraftx/src/windows/login_window/logins/midjourney_login_window.rs @@ -1,4 +1,5 @@ use crate::credentials::login_website::LoginWebsite; +use crate::services::midjourney::utils::midjourney_browser_profile::MIDJOURNEY_USER_AGENT; use crate::windows::login_window::login_journey::LoginJourney; use crate::windows::login_window::login_window_trait::LoginWindowSite; use once_cell::sync::Lazy; @@ -31,6 +32,14 @@ impl LoginWindowSite for MidjourneyLoginWindow { DESTINATION_HOSTNAMES } + // Google's sign-in forces a passkey step-up under the default WKWebView UA + // (which the embedded browser can't satisfy). Present a mainstream desktop + // Safari UA — the SAME one the Midjourney HTTP/websocket calls use — so the + // captured cf_clearance cookie validates when those calls replay it. + fn user_agent(&self) -> Option<&'static str> { + Some(MIDJOURNEY_USER_AGENT) + } + // Midjourney's session rides in the AuthUserTokenV3 cookie pair (the same // names midjourney_client's cookie_store_has_auth_cookies checks); either // appearing in a cookie-cleared window is the definitive login signal. diff --git a/crates/artcraftx/src/windows/login_window/open_login_window.rs b/crates/artcraftx/src/windows/login_window/open_login_window.rs index ac938337..4df1734d 100644 --- a/crates/artcraftx/src/windows/login_window/open_login_window.rs +++ b/crates/artcraftx/src/windows/login_window/open_login_window.rs @@ -56,7 +56,7 @@ pub async fn open_login_window( }; let start_url = WebviewUrl::External(first_url); - let window = WebviewWindowBuilder::new(app, &window_name, start_url) + let mut builder = WebviewWindowBuilder::new(app, &window_name, start_url) .title(site.window_title()) .center() .resizable(true) @@ -64,8 +64,17 @@ pub async fn open_login_window( .closable(true) .min_inner_size(200.0, 800.0) .focused(true) - .devtools(true) - .build()?; + .devtools(true); + + // Some sites (e.g. Midjourney, whose Google sign-in otherwise forces a + // passkey step-up that the default WKWebView can't satisfy) need a + // mainstream desktop User-Agent. It must match the UA the site's HTTP client + // later uses, since Cloudflare's cf_clearance is UA-bound. + if let Some(user_agent) = site.user_agent() { + builder = builder.user_agent(user_agent); + } + + let window = builder.build()?; let webview = window.get_webview(&window_name) .ok_or_else(|| anyhow!("no webview found"))?; diff --git a/crates/lib/browser_emulation/src/browser_profile.rs b/crates/lib/browser_emulation/src/browser_profile.rs index 113ebdb9..92c4739c 100644 --- a/crates/lib/browser_emulation/src/browser_profile.rs +++ b/crates/lib/browser_emulation/src/browser_profile.rs @@ -34,6 +34,19 @@ pub struct CustomBrowserProfile { } impl BrowserProfile { + /// A macOS Safari profile with an explicit User-Agent override. + /// + /// Use this when the UA must EXACTLY match another client's — e.g. an + /// embedded webview that captured the cookies, since Cloudflare's + /// `cf_clearance` is bound to the exact User-Agent string. + pub fn safari_macos_with_user_agent(user_agent: impl Into) -> Self { + Self::Custom(CustomBrowserProfile { + emulation: Emulation::Safari18, + os: EmulationOS::MacOS, + maybe_user_agent: Some(user_agent.into()), + }) + } + /// Build a ready-to-use client carrying this profile's full fingerprint and /// identity headers. This is the usual entry point. pub fn build_client(&self) -> Result {