fix(core): attempt to fix requesting issues
This commit is contained in:
parent
3b436f0d87
commit
7187e841b6
8 changed files with 220 additions and 20 deletions
30
Cargo.lock
generated
30
Cargo.lock
generated
|
|
@ -3045,9 +3045,11 @@ dependencies = [
|
||||||
"percent-encoding",
|
"percent-encoding",
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
"socket2 0.6.4",
|
"socket2 0.6.4",
|
||||||
|
"system-configuration",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tower-service",
|
"tower-service",
|
||||||
"tracing",
|
"tracing",
|
||||||
|
"windows-registry",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -3062,7 +3064,7 @@ dependencies = [
|
||||||
"js-sys",
|
"js-sys",
|
||||||
"log",
|
"log",
|
||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
"windows-core 0.56.0",
|
"windows-core 0.62.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -4809,6 +4811,7 @@ dependencies = [
|
||||||
"tracing",
|
"tracing",
|
||||||
"url",
|
"url",
|
||||||
"uuid 1.23.1",
|
"uuid 1.23.1",
|
||||||
|
"webpki-root-certs",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -6815,6 +6818,27 @@ dependencies = [
|
||||||
"windows 0.62.2",
|
"windows 0.62.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "system-configuration"
|
||||||
|
version = "0.7.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a13f3d0daba03132c0aa9767f98351b3488edc2c100cda2d2ec2b04f3d8d3c8b"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags 2.12.1",
|
||||||
|
"core-foundation 0.9.4",
|
||||||
|
"system-configuration-sys",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "system-configuration-sys"
|
||||||
|
version = "0.6.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4"
|
||||||
|
dependencies = [
|
||||||
|
"core-foundation-sys",
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tagptr"
|
name = "tagptr"
|
||||||
version = "0.2.0"
|
version = "0.2.0"
|
||||||
|
|
@ -7864,9 +7888,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "webpki-root-certs"
|
name = "webpki-root-certs"
|
||||||
version = "1.0.7"
|
version = "1.0.9"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f31141ce3fc3e300ae89b78c0dd67f9708061d1d2eda54b8209346fd6be9a92c"
|
checksum = "b96554aa2acc8ccdb7e1c9a58a7a68dd5d13bccc69cd124cb09406db612a1c9b"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"rustls-pki-types",
|
"rustls-pki-types",
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -85,8 +85,15 @@ reqwest = {version = "=0.13.3", default-features = false, features = [
|
||||||
"brotli",
|
"brotli",
|
||||||
"form",
|
"form",
|
||||||
"hickory-dns",
|
"hickory-dns",
|
||||||
|
# Without this reqwest sees only the HTTP_PROXY/HTTPS_PROXY env vars, and
|
||||||
|
# ignores the proxy configured in Windows Internet Options or macOS network
|
||||||
|
# preferences — which is how school and workplace networks are set up. Those
|
||||||
|
# users could reach every service in a browser but not from here.
|
||||||
|
"system-proxy",
|
||||||
]}
|
]}
|
||||||
reqwest-websocket = {version = "=0.6.0"}
|
reqwest-websocket = {version = "=0.6.0"}
|
||||||
|
# Fallback trust roots for Windows. See `oneclient_net::service` for why.
|
||||||
|
webpki-root-certs = {version = "=1.0.9"}
|
||||||
tokio = {version = "=1.52.3", features = ["full"]}
|
tokio = {version = "=1.52.3", features = ["full"]}
|
||||||
tokio-stream = {version = "=0.1.18", features = ["full"]}
|
tokio-stream = {version = "=0.1.18", features = ["full"]}
|
||||||
tokio-util = {version = "=0.7.18", features = ["full"]}
|
tokio-util = {version = "=0.7.18", features = ["full"]}
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,9 @@ pub fn diagnose_auth_error(err: &MinecraftAuthError) -> Option<AuthErrorGuidance
|
||||||
|
|
||||||
MinecraftAuthError::DeserializeError { step, .. } => guidance_for_step(*step),
|
MinecraftAuthError::DeserializeError { step, .. } => guidance_for_step(*step),
|
||||||
|
|
||||||
MinecraftAuthError::RequestError { .. } => Some(network_guidance()),
|
MinecraftAuthError::RequestError { source, .. } => {
|
||||||
|
Some(network_guidance(classify_network_failure(source)))
|
||||||
|
}
|
||||||
|
|
||||||
MinecraftAuthError::BrowserAuthorizationExpired
|
MinecraftAuthError::BrowserAuthorizationExpired
|
||||||
| MinecraftAuthError::BrowserLoginNotFound => Some(AuthErrorGuidance::new(
|
| MinecraftAuthError::BrowserLoginNotFound => Some(AuthErrorGuidance::new(
|
||||||
|
|
@ -271,7 +273,22 @@ pub fn preview_samples() -> Vec<AuthErrorSample> {
|
||||||
out.push(AuthErrorSample {
|
out.push(AuthErrorSample {
|
||||||
label: "Network unreachable",
|
label: "Network unreachable",
|
||||||
message: "error sending request for url (https://user.auth.xboxlive.com/user/authenticate): connection closed".to_string(),
|
message: "error sending request for url (https://user.auth.xboxlive.com/user/authenticate): connection closed".to_string(),
|
||||||
guidance: Some(network_guidance()),
|
guidance: Some(network_guidance(NetworkFailure::Generic)),
|
||||||
|
});
|
||||||
|
out.push(AuthErrorSample {
|
||||||
|
label: "TLS certificate rejected",
|
||||||
|
message: "error sending request for url (https://login.microsoftonline.com/consumers/oauth2/v2.0/devicecode): invalid peer certificate: UnknownIssuer".to_string(),
|
||||||
|
guidance: Some(network_guidance(NetworkFailure::Certificate)),
|
||||||
|
});
|
||||||
|
out.push(AuthErrorSample {
|
||||||
|
label: "TLS handshake failed",
|
||||||
|
message: "error sending request for url (https://login.microsoftonline.com/consumers/oauth2/v2.0/devicecode): received fatal alert: HandshakeFailure".to_string(),
|
||||||
|
guidance: Some(network_guidance(NetworkFailure::Handshake)),
|
||||||
|
});
|
||||||
|
out.push(AuthErrorSample {
|
||||||
|
label: "DNS lookup failed",
|
||||||
|
message: "error sending request for url (https://login.microsoftonline.com/consumers/oauth2/v2.0/devicecode): dns error: failed to lookup address information".to_string(),
|
||||||
|
guidance: Some(network_guidance(NetworkFailure::Dns)),
|
||||||
});
|
});
|
||||||
out
|
out
|
||||||
}
|
}
|
||||||
|
|
@ -285,8 +302,94 @@ fn xerr(code: u64) -> MinecraftAuthError {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn network_guidance() -> AuthErrorGuidance {
|
/// Which layer of the connection failed.
|
||||||
AuthErrorGuidance::new(
|
///
|
||||||
|
/// reqwest reports every one of these as a connect error, so the text of the
|
||||||
|
/// source chain is the only thing that separates "your antivirus is
|
||||||
|
/// intercepting TLS" from "your DNS is broken" — and they need different
|
||||||
|
/// instructions. A wrong guess here costs the user a wasted support round, so
|
||||||
|
/// anything unrecognised falls back to the generic advice.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
enum NetworkFailure {
|
||||||
|
Certificate,
|
||||||
|
Handshake,
|
||||||
|
Dns,
|
||||||
|
Generic,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn classify_network_failure(err: &reqwest::Error) -> NetworkFailure {
|
||||||
|
let chain = oneclient_net::error_chain(err).to_ascii_lowercase();
|
||||||
|
|
||||||
|
// Certificate before handshake: a rejected certificate also aborts the
|
||||||
|
// handshake, and the certificate wording is the more specific of the two.
|
||||||
|
const CERTIFICATE: &[&str] = &[
|
||||||
|
"invalid peer certificate",
|
||||||
|
"unknownissuer",
|
||||||
|
"certificate",
|
||||||
|
"certexpired",
|
||||||
|
"notvalidforname",
|
||||||
|
];
|
||||||
|
const HANDSHAKE: &[&str] = &[
|
||||||
|
"handshake",
|
||||||
|
"received fatal alert",
|
||||||
|
"peer misbehaved",
|
||||||
|
"no cipher suites in common",
|
||||||
|
"protocol version",
|
||||||
|
"unsupported protocol",
|
||||||
|
];
|
||||||
|
const DNS: &[&str] = &[
|
||||||
|
"dns error",
|
||||||
|
"failed to lookup address",
|
||||||
|
"no record found",
|
||||||
|
"nodename nor servname",
|
||||||
|
"name or service not known",
|
||||||
|
"no such host",
|
||||||
|
];
|
||||||
|
|
||||||
|
let matches = |markers: &[&str]| markers.iter().any(|marker| chain.contains(marker));
|
||||||
|
|
||||||
|
if matches(CERTIFICATE) {
|
||||||
|
NetworkFailure::Certificate
|
||||||
|
} else if matches(HANDSHAKE) {
|
||||||
|
NetworkFailure::Handshake
|
||||||
|
} else if matches(DNS) {
|
||||||
|
NetworkFailure::Dns
|
||||||
|
} else {
|
||||||
|
NetworkFailure::Generic
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn network_guidance(failure: NetworkFailure) -> AuthErrorGuidance {
|
||||||
|
match failure {
|
||||||
|
NetworkFailure::Certificate => AuthErrorGuidance::new(
|
||||||
|
"OneClient reached the Microsoft sign-in service, but could not verify its security certificate. Something on this machine or network is intercepting the encrypted connection — usually antivirus HTTPS scanning, a school or workplace filter, or a VPN.",
|
||||||
|
&[
|
||||||
|
"Turn off HTTPS/SSL scanning in your antivirus (often called \"encrypted connection scanning\", \"web shield\", or \"SSL interception\")",
|
||||||
|
"Temporarily disable your VPN or proxy and try signing in again",
|
||||||
|
"If you are on a school or workplace network, try a home network or phone hotspot to confirm",
|
||||||
|
"Check that your system date, time, and time zone are correct",
|
||||||
|
],
|
||||||
|
),
|
||||||
|
NetworkFailure::Handshake => AuthErrorGuidance::new(
|
||||||
|
"OneClient reached the Microsoft sign-in service, but the two could not agree on a secure connection. This normally means a proxy, filter, or antivirus in between is using outdated encryption settings.",
|
||||||
|
&[
|
||||||
|
"Turn off HTTPS/SSL scanning in your antivirus and try signing in again",
|
||||||
|
"Temporarily disable your VPN or proxy software",
|
||||||
|
"If you are on a school or workplace network, try a home network or phone hotspot to confirm",
|
||||||
|
"Make sure OneClient is up to date",
|
||||||
|
],
|
||||||
|
),
|
||||||
|
NetworkFailure::Dns => AuthErrorGuidance::new(
|
||||||
|
"OneClient could not look up the address of the Microsoft sign-in service. This is a DNS problem on this machine or network, not a problem with your account.",
|
||||||
|
&[
|
||||||
|
"Check that your internet connection is working",
|
||||||
|
"Temporarily disable your VPN or proxy and try signing in again",
|
||||||
|
"Check your hosts file for entries blocking Microsoft or Minecraft domains",
|
||||||
|
"Try a different DNS server, such as Cloudflare (1.1.1.1) or Google (8.8.8.8)",
|
||||||
|
"Restart OneClient and try signing in again",
|
||||||
|
],
|
||||||
|
),
|
||||||
|
NetworkFailure::Generic => AuthErrorGuidance::new(
|
||||||
"OneClient could not connect to a Microsoft, Xbox, or Minecraft service needed for sign-in. This is usually a local network, DNS, proxy, firewall, hosts file, VPN, or antivirus issue.",
|
"OneClient could not connect to a Microsoft, Xbox, or Minecraft service needed for sign-in. This is usually a local network, DNS, proxy, firewall, hosts file, VPN, or antivirus issue.",
|
||||||
&[
|
&[
|
||||||
"Restart OneClient and try signing in again",
|
"Restart OneClient and try signing in again",
|
||||||
|
|
@ -295,5 +398,6 @@ fn network_guidance() -> AuthErrorGuidance {
|
||||||
"Try a different network, or temporarily disable VPN/proxy software if you use one",
|
"Try a different network, or temporarily disable VPN/proxy software if you use one",
|
||||||
"If routing or DNS is the issue, a service like Cloudflare WARP can sometimes help",
|
"If routing or DNS is the issue, a service like Cloudflare WARP can sometimes help",
|
||||||
],
|
],
|
||||||
)
|
),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -33,7 +33,10 @@ pub enum MinecraftAuthError {
|
||||||
source: serde_json::Error,
|
source: serde_json::Error,
|
||||||
status_code: reqwest::StatusCode,
|
status_code: reqwest::StatusCode,
|
||||||
},
|
},
|
||||||
#[error("failed to request using HTTP during MSA step {step:?}: {source}")]
|
// `source` is rendered through `error_chain` rather than with `{source}`:
|
||||||
|
// reqwest's `Display` names only the URL, and the cause we need to tell
|
||||||
|
// apart (certificate, handshake, DNS, refused) is under it.
|
||||||
|
#[error("failed to request using HTTP during MSA step {step:?}: {}", oneclient_net::error_chain(source))]
|
||||||
RequestError {
|
RequestError {
|
||||||
step: MinecraftAuthStep,
|
step: MinecraftAuthStep,
|
||||||
#[source]
|
#[source]
|
||||||
|
|
|
||||||
|
|
@ -34,3 +34,6 @@ thiserror.workspace = true
|
||||||
tracing.workspace = true
|
tracing.workspace = true
|
||||||
url.workspace = true
|
url.workspace = true
|
||||||
uuid.workspace = true
|
uuid.workspace = true
|
||||||
|
|
||||||
|
[target.'cfg(windows)'.dependencies]
|
||||||
|
webpki-root-certs.workspace = true
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,31 @@
|
||||||
|
/// Renders an error together with every `source()` beneath it.
|
||||||
|
///
|
||||||
|
/// `reqwest::Error`'s own `Display` stops at "error sending request for url
|
||||||
|
/// (...)". The fact that actually names the failure — a rustls handshake
|
||||||
|
/// alert, an untrusted certificate, a DNS lookup failure, `ECONNREFUSED` —
|
||||||
|
/// lives further down the chain, so without this every transport failure in a
|
||||||
|
/// user report reads identically and is undiagnosable.
|
||||||
|
#[must_use]
|
||||||
|
pub fn error_chain(err: &dyn std::error::Error) -> String {
|
||||||
|
let mut rendered = err.to_string();
|
||||||
|
let mut cursor = err.source();
|
||||||
|
|
||||||
|
while let Some(cause) = cursor {
|
||||||
|
let text = cause.to_string();
|
||||||
|
// hyper and reqwest repeat the same sentence across several layers.
|
||||||
|
if !rendered.ends_with(&text) {
|
||||||
|
rendered.push_str(": ");
|
||||||
|
rendered.push_str(&text);
|
||||||
|
}
|
||||||
|
cursor = cause.source();
|
||||||
|
}
|
||||||
|
|
||||||
|
rendered
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, thiserror::Error)]
|
#[derive(Debug, thiserror::Error)]
|
||||||
pub enum RequestError {
|
pub enum RequestError {
|
||||||
#[error(transparent)]
|
#[error("{}", error_chain(.0))]
|
||||||
ReqwestError(#[from] reqwest::Error),
|
ReqwestError(#[from] reqwest::Error),
|
||||||
|
|
||||||
#[error("IO Error: {0}")]
|
#[error("IO Error: {0}")]
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,7 @@ pub mod status;
|
||||||
pub use cache::{EtagPolicy, Fetched, commit_etag, fetch_cached};
|
pub use cache::{EtagPolicy, Fetched, commit_etag, fetch_cached};
|
||||||
pub use config::NetConfig;
|
pub use config::NetConfig;
|
||||||
pub use download::{download_verified, fetch_verified, matches_on_disk};
|
pub use download::{download_verified, fetch_verified, matches_on_disk};
|
||||||
pub use error::RequestError;
|
pub use error::{RequestError, error_chain};
|
||||||
pub use request::*;
|
pub use request::*;
|
||||||
pub use response::*;
|
pub use response::*;
|
||||||
pub use service::RequestClient;
|
pub use service::RequestClient;
|
||||||
|
|
|
||||||
|
|
@ -81,6 +81,36 @@ fn apply_curseforge_auth(
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Adds the bundled Mozilla roots as a fallback behind the Windows trust store.
|
||||||
|
///
|
||||||
|
/// This does not replace the system store. `rustls-platform-verifier` builds
|
||||||
|
/// the chain against Windows first and only retries with these roots when that
|
||||||
|
/// came back untrusted or partial, so an intercepting proxy or an
|
||||||
|
/// enterprise-installed CA still works exactly as before.
|
||||||
|
///
|
||||||
|
/// It matters because Windows populates its root store lazily from Windows
|
||||||
|
/// Update. On a machine where root auto-update is disabled by policy — routine
|
||||||
|
/// on managed school and workplace images — or on a trimmed-down install, a
|
||||||
|
/// root that the rest of the world has can simply be absent. Every other
|
||||||
|
/// client those users try carries its own roots (Chrome since 105, Firefox via
|
||||||
|
/// NSS, Java-based launchers via `cacerts`), so sign-in works everywhere
|
||||||
|
/// except here. macOS and Linux do not have this failure mode, and adding
|
||||||
|
/// roots there would be additive rather than a fallback, so this is Windows
|
||||||
|
/// only.
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
fn add_fallback_roots(mut builder: ClientBuilder) -> ClientBuilder {
|
||||||
|
for der in webpki_root_certs::TLS_SERVER_ROOT_CERTS {
|
||||||
|
match reqwest::Certificate::from_der(der) {
|
||||||
|
Ok(root) => builder = builder.add_root_certificate(root),
|
||||||
|
// A malformed entry would be a bug in the bundle, not something the
|
||||||
|
// user can act on. Dropping one root beats failing to build a client.
|
||||||
|
Err(err) => tracing::warn!("skipping malformed fallback root: {err}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
builder
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct RequestClient {
|
pub struct RequestClient {
|
||||||
client: reqwest::Client,
|
client: reqwest::Client,
|
||||||
|
|
@ -109,7 +139,6 @@ impl RequestClient {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn new(config: NetConfig) -> Result<Self, RequestError> {
|
pub fn new(config: NetConfig) -> Result<Self, RequestError> {
|
||||||
#[cfg_attr(target_os = "windows", allow(unused_mut))]
|
|
||||||
let mut builder = ClientBuilder::new()
|
let mut builder = ClientBuilder::new()
|
||||||
.connect_timeout(std::time::Duration::from_secs(10))
|
.connect_timeout(std::time::Duration::from_secs(10))
|
||||||
.read_timeout(std::time::Duration::from_secs(30))
|
.read_timeout(std::time::Duration::from_secs(30))
|
||||||
|
|
@ -126,6 +155,11 @@ impl RequestClient {
|
||||||
builder = builder.hickory_dns(true);
|
builder = builder.hickory_dns(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
{
|
||||||
|
builder = add_fallback_roots(builder);
|
||||||
|
}
|
||||||
|
|
||||||
let client = builder.build()?;
|
let client = builder.build()?;
|
||||||
|
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue