change webview user agent

This commit is contained in:
Hanashi 2026-08-23 02:16:31 -04:00
parent 39efa4545e
commit 6a4f5fb8bc
5 changed files with 58 additions and 13 deletions

View File

@ -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)
}

View File

@ -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

View File

@ -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.

View File

@ -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"))?;

View File

@ -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<String>) -> 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<Client, wreq::Error> {