From 4f3bd92e48405352be119f36b9e4c75825d22cca Mon Sep 17 00:00:00 2001 From: rozwader Date: Wed, 19 Aug 2026 14:56:23 +0200 Subject: [PATCH 1/2] feat: added ability to decline TOS; added blockades for hosts while TOS is declined --- Cargo.lock | 1 + packages/oneclient_app/src/hooks/actions.rs | 31 +++- packages/oneclient_app/src/main.rs | 2 + .../src/view/app/settings/launcher.rs | 24 +++- .../src/view/onboarding/terms.rs | 136 ++++++++++++++++-- packages/oneclient_app/src/view/startup.rs | 4 +- packages/oneclient_common/Cargo.toml | 2 + packages/oneclient_common/src/consent.rs | 98 +++++++++++++ packages/oneclient_common/src/lib.rs | 1 + packages/oneclient_core/src/reporting.rs | 5 + .../oneclient_core/src/settings/launcher.rs | 2 + packages/oneclient_core/src/state.rs | 2 + packages/oneclient_net/src/error.rs | 6 + packages/oneclient_net/src/service.rs | 11 +- packages/oneclient_net/src/status.rs | 8 ++ packages/oneclient_polyplus/src/lib.rs | 33 ++++- 16 files changed, 343 insertions(+), 23 deletions(-) create mode 100644 packages/oneclient_common/src/consent.rs diff --git a/Cargo.lock b/Cargo.lock index 176b72b8..ee5566e9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4608,6 +4608,7 @@ dependencies = [ "serde", "strum", "thiserror 2.0.18", + "url", ] [[package]] diff --git a/packages/oneclient_app/src/hooks/actions.rs b/packages/oneclient_app/src/hooks/actions.rs index aee585fc..6c61ec2e 100644 --- a/packages/oneclient_app/src/hooks/actions.rs +++ b/packages/oneclient_app/src/hooks/actions.rs @@ -233,9 +233,38 @@ impl Actions { } pub fn accept_tos(&self, terms_version: u32, privacy_version: u32) { - if let Some(updated) = self.mutate_settings(|settings| { + let mut was_declined = false; + + let Some(updated) = self.mutate_settings(|settings| { + was_declined = settings.declined_tos; settings.accepted_tos_version = terms_version; settings.accepted_privacy_version = privacy_version; + settings.declined_tos = false; + }) else { + return; + }; + + self.persist(updated); + + if was_declined { + self.notify("Restart to finish") + .body( + "Thanks. OneClient reconnects to Polyfrost services the next time you start \ + it.", + ) + .icon(IconType::RefreshCw01) + .send(); + } + } + + pub fn decline_tos(&self) { + oneclient_common::consent::decline(); + + if let Some(updated) = self.mutate_settings(|settings| { + settings.declined_tos = true; + settings.accepted_tos_version = 0; + settings.accepted_privacy_version = 0; + settings.seen_onboarding = true; }) { self.persist(updated); } diff --git a/packages/oneclient_app/src/main.rs b/packages/oneclient_app/src/main.rs index fffbaf56..a6c7f83d 100644 --- a/packages/oneclient_app/src/main.rs +++ b/packages/oneclient_app/src/main.rs @@ -74,6 +74,8 @@ fn main() { let settings = rt.block_on(oneclient_core::settings::store::load_settings(None)); + oneclient_common::consent::init(settings.declined_tos); + if settings.log_debug { oneclient_core::logger::init_debug() } else { diff --git a/packages/oneclient_app/src/view/app/settings/launcher.rs b/packages/oneclient_app/src/view/app/settings/launcher.rs index c5420fb7..b5440e17 100644 --- a/packages/oneclient_app/src/view/app/settings/launcher.rs +++ b/packages/oneclient_app/src/view/app/settings/launcher.rs @@ -1,9 +1,11 @@ use freya::prelude::*; +use freya::router::RouterContext; use super::settings_page; -use crate::components::{IconType, link_button, toggle}; +use crate::components::{Button, IconType, link_button, toggle}; use crate::hooks::{use_dispatch, use_launcher, use_settings_snapshot}; use crate::platform; +use crate::routes::Route; use crate::view::app::settings::{section_header, settings_row}; #[derive(PartialEq)] @@ -45,6 +47,20 @@ impl Component for SettingsLauncher { let folder = data_dir.clone(); let open_folder = link_button().on_press(move |_| platform::open_url(&folder)); + // The only way back for someone who declined during onboarding + let consent_summary = if settings.declined_tos { + "Declined. Polyfrost services stay off until you accept and restart OneClient." + } else { + "Accepted. Review them again at any time." + }; + let review_terms = Button::new() + .secondary() + .small() + .on_press(|_| { + let _ = RouterContext::get().replace(Route::OnboardingTerms {}); + }) + .text("Review"); + settings_page() .child(section_header("GENERAL")) .child(settings_row( @@ -59,6 +75,12 @@ impl Component for SettingsLauncher { "Send anonymous crash and error reports to help fix bugs. Applies on restart.", toggle(crash_reporting), )) + .child(settings_row( + IconType::File02, + "Terms & Privacy", + consent_summary, + review_terms, + )) .child(section_header("FOLDERS AND FILES")) .child(settings_row( IconType::Folder, diff --git a/packages/oneclient_app/src/view/onboarding/terms.rs b/packages/oneclient_app/src/view/onboarding/terms.rs index b4eb2ecf..4b245b4a 100644 --- a/packages/oneclient_app/src/view/onboarding/terms.rs +++ b/packages/oneclient_app/src/view/onboarding/terms.rs @@ -35,6 +35,7 @@ impl Component for OnboardingTerms { let accepted = use_state(|| false); let tab = use_state(|| LegalTab::Terms); + let confirming_decline = use_state(|| false); let returning = settings.seen_onboarding; let next = if returning { @@ -74,6 +75,8 @@ impl Component for OnboardingTerms { fallback_body(error.as_deref(), query) }; + let deciding = *confirming_decline.read(); + let content = rect() .vertical() .width(Size::fill()) @@ -85,17 +88,40 @@ impl Component for OnboardingTerms { .maybe_child(tabs) .child(body) .child(link_row(terms_url, privacy_url)) - .child(accept_row(accepted)) + .child(if deciding { + decline_warning() + } else { + accept_row(accepted) + }) .into_element(); - onboarding_page( - onboarding_illustration(IconType::File02), - content, - terms_nav(back, *accepted.read() && !loading, move || { - dispatch.accept_tos(terms_version, privacy_version); - let _ = RouterContext::get().replace(next.clone()); - }), - ) + let nav = if deciding { + decline_nav( + move || { + let mut confirming = confirming_decline; + confirming.set(false); + }, + move || { + dispatch.decline_tos(); + let _ = RouterContext::get().replace(Route::Home {}); + }, + ) + } else { + terms_nav( + back, + *accepted.read() && !loading, + move || { + let mut confirming = confirming_decline; + confirming.set(true); + }, + move || { + dispatch.accept_tos(terms_version, privacy_version); + let _ = RouterContext::get().replace(next.clone()); + }, + ) + }; + + onboarding_page(onboarding_illustration(IconType::File02), content, nav) } } @@ -234,7 +260,7 @@ fn external_link_button(text: &'static str, url: String) -> impl IntoElement { .child(Icon::new(IconType::LinkExternal01).size(14.)) } -fn accept_row(accepted: State) -> impl IntoElement { +fn accept_row(accepted: State) -> Element { rect() .horizontal() .width(Size::fill()) @@ -259,7 +285,7 @@ fn accept_row(accepted: State) -> impl IntoElement { ) .child( label() - .text("Required to use OneClient.") + .text("Required for Poly+, downloads and updates.") .font_size(11.) .color(colors::fg_secondary()), ), @@ -268,11 +294,62 @@ fn accept_row(accepted: State) -> impl IntoElement { .into_element() } +fn decline_warning() -> Element { + rect() + .vertical() + .width(Size::fill()) + .spacing(8.) + .padding(Gaps::new_symmetric(14., 16.)) + .corner_radius(CornerRadius::new_all(12.)) + .background(colors::page_elevated()) + .border(border_all_color(1., colors::danger())) + .child( + rect() + .horizontal() + .cross_align(Alignment::Center) + .spacing(8.) + .child( + Icon::new(IconType::AlertTriangle) + .size(18.) + .color(colors::code_warn()), + ) + .child( + label() + .text("Continue without accepting?") + .font_size(14.) + .font_weight(FontWeight::MEDIUM) + .color(colors::fg_primary()), + ), + ) + .child( + label() + .text( + "OneClient stops contacting Polyfrost entirely: no Poly+, no crash reports, \ + and no version, mod or bundle downloads. Instances you have already \ + installed keep working, and signing in to Minecraft still works.", + ) + .font_size(12.) + .color(colors::fg_secondary()), + ) + .child( + label() + .text( + "You can accept later under Settings > Launcher. That takes effect after a \ + restart.", + ) + .font_size(11.) + .color(colors::fg_secondary().with_a(180)), + ) + .into_element() +} + fn terms_nav( back: Option, next_enabled: bool, + on_decline: impl FnMut() + 'static, on_next: impl FnMut() + 'static, -) -> impl IntoElement { +) -> Element { + let mut on_decline = on_decline; let mut on_next = on_next; rect() .horizontal() @@ -291,6 +368,13 @@ fn terms_nav( .text("Back") .into_element() })) + .child( + Button::new() + .secondary() + .width(Size::px(128.)) + .on_press(move |_| on_decline()) + .text("Decline"), + ) .child( Button::new() .primary() @@ -302,3 +386,31 @@ fn terms_nav( ) .into_element() } + +fn decline_nav(on_cancel: impl FnMut() + 'static, on_confirm: impl FnMut() + 'static) -> Element { + let mut on_cancel = on_cancel; + let mut on_confirm = on_confirm; + + rect() + .horizontal() + .width(Size::fill()) + .main_align(Alignment::End) + .cross_align(Alignment::Center) + .spacing(12.) + .padding(Gaps::new(0., 40., 32., 40.)) + .child( + Button::new() + .secondary() + .width(Size::px(128.)) + .on_press(move |_| on_cancel()) + .text("Go back"), + ) + .child( + Button::new() + .danger() + .width(Size::px(220.)) + .on_press(move |_| on_confirm()) + .text("Decline and continue"), + ) + .into_element() +} diff --git a/packages/oneclient_app/src/view/startup.rs b/packages/oneclient_app/src/view/startup.rs index a7e1edb8..1055d9eb 100644 --- a/packages/oneclient_app/src/view/startup.rs +++ b/packages/oneclient_app/src/view/startup.rs @@ -96,7 +96,9 @@ impl Component for Startup { let stale = settings.settings.accepted_tos_version < required_terms || settings.settings.accepted_privacy_version < required_privacy; - let destination = if !settings.settings.seen_onboarding { + let destination = if settings.settings.declined_tos { + Route::Home {} + } else if !settings.settings.seen_onboarding { Route::OnboardingWelcome {} } else if stale { Route::OnboardingTerms {} diff --git a/packages/oneclient_common/Cargo.toml b/packages/oneclient_common/Cargo.toml index 7ba3aa7d..cf1ad39b 100644 --- a/packages/oneclient_common/Cargo.toml +++ b/packages/oneclient_common/Cargo.toml @@ -21,3 +21,5 @@ directories.workspace = true serde.workspace = true strum.workspace = true thiserror.workspace = true + +url.workspace = true diff --git a/packages/oneclient_common/src/consent.rs b/packages/oneclient_common/src/consent.rs new file mode 100644 index 00000000..525af503 --- /dev/null +++ b/packages/oneclient_common/src/consent.rs @@ -0,0 +1,98 @@ +use std::sync::atomic::{AtomicBool, Ordering}; + +/// Hosts that are out of bounds for someone who didn't accept TOS +const GATED_DOMAINS: &[&str] = &["polyfrost.org", "sentry.io"]; + +static DECLINED: AtomicBool = AtomicBool::new(false); + +pub fn init(declined: bool) { + DECLINED.store(declined, Ordering::SeqCst); +} + +pub fn decline() { + DECLINED.store(true, Ordering::SeqCst); +} + +#[must_use] +pub fn declined() -> bool { + DECLINED.load(Ordering::Relaxed) +} + +#[must_use] +pub fn blocks_url(url: &str) -> bool { + declined() && is_gated_url(url) +} + +#[must_use] +pub fn blocks_host(host: &str) -> bool { + declined() && is_gated_host(host) +} + +#[must_use] +pub fn is_gated_url(url: &str) -> bool { + url::Url::parse(url) + .ok() + .and_then(|parsed| parsed.host_str().map(is_gated_host)) + .unwrap_or(true) +} + +#[must_use] +pub fn is_gated_host(host: &str) -> bool { + let host = host.trim_end_matches('.').to_ascii_lowercase(); + + GATED_DOMAINS + .iter() + .any(|domain| host == *domain || host.ends_with(&format!(".{domain}"))) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn gates_polyfrost_services_and_the_crash_reporter() { + for host in [ + "polyfrost.org", + "plus.polyfrost.org", + "meta.polyfrost.org", + "data-v2.polyfrost.org", + "status.polyfrost.org", + "PLUS.POLYFROST.ORG", + "plus.polyfrost.org.", + "o4511714343124992.ingest.us.sentry.io", + ] { + assert!(is_gated_host(host), "expected {host} to be gated"); + } + } + + #[test] + fn leaves_everything_else_alone() { + for host in [ + "api.modrinth.com", + "api.curseforge.com", + "sessionserver.mojang.com", + "api.minecraftservices.com", + "www.gstatic.com", + "github.com", + "notpolyfrost.org", + "polyfrost.org.attacker.com", + "evil-sentry.io.attacker.com", + ] { + assert!(!is_gated_host(host), "unexpected gate on {host}"); + } + } + + #[test] + fn gates_urls_by_their_host() { + assert!(is_gated_url("https://plus.polyfrost.org/account/login")); + assert!(is_gated_url("wss://plus.polyfrost.org/websocket")); + assert!(!is_gated_url("https://api.modrinth.com/v2/search")); + } + + #[test] + fn unparseable_urls_fail_closed() { + assert!(is_gated_url("not a url")); + assert!(is_gated_url("")); + assert!(is_gated_url("file:///etc/hosts")); + } +} diff --git a/packages/oneclient_common/src/lib.rs b/packages/oneclient_common/src/lib.rs index bd0372b7..79545fe7 100644 --- a/packages/oneclient_common/src/lib.rs +++ b/packages/oneclient_common/src/lib.rs @@ -1,6 +1,7 @@ //! Leaf crate must never depend on the database network stack or any //! launcher subsystem or the dependency graph cycles back into a monolith +pub mod consent; pub mod constants; pub mod domain; pub mod os_ext; diff --git a/packages/oneclient_core/src/reporting.rs b/packages/oneclient_core/src/reporting.rs index 96d36982..01a15bf6 100644 --- a/packages/oneclient_core/src/reporting.rs +++ b/packages/oneclient_core/src/reporting.rs @@ -42,6 +42,11 @@ fn drop_opted_out_breadcrumbs(breadcrumb: Breadcrumb) -> Option { } pub fn init(enabled: bool) -> Option { + if oneclient_common::consent::declined() { + tracing::debug!("crash reporting disabled: terms and privacy policy declined"); + return None; + } + if !enabled { tracing::debug!("crash reporting disabled by settings"); return None; diff --git a/packages/oneclient_core/src/settings/launcher.rs b/packages/oneclient_core/src/settings/launcher.rs index 702ad598..1586782a 100644 --- a/packages/oneclient_core/src/settings/launcher.rs +++ b/packages/oneclient_core/src/settings/launcher.rs @@ -37,6 +37,7 @@ pub struct LauncherSettings { pub seen_onboarding: bool, pub accepted_tos_version: u32, pub accepted_privacy_version: u32, + pub declined_tos: bool, pub seen_versions: Vec, pub seen_changelog_version: Option, pub modrinth_api_key: Option, @@ -72,6 +73,7 @@ impl Default for LauncherSettings { seen_onboarding: false, accepted_tos_version: 0, accepted_privacy_version: 0, + declined_tos: false, seen_versions: Vec::new(), seen_changelog_version: None, modrinth_api_key: None, diff --git a/packages/oneclient_core/src/state.rs b/packages/oneclient_core/src/state.rs index 7bda2d29..8fb1df79 100644 --- a/packages/oneclient_core/src/state.rs +++ b/packages/oneclient_core/src/state.rs @@ -73,6 +73,8 @@ impl LauncherState { }; let settings = store::load_settings(Some(&services.events)).await; + + oneclient_common::consent::init(settings.declined_tos); services .requester .set_config(crate::settings::net_config(&settings)); diff --git a/packages/oneclient_net/src/error.rs b/packages/oneclient_net/src/error.rs index 048d33c3..93938f32 100644 --- a/packages/oneclient_net/src/error.rs +++ b/packages/oneclient_net/src/error.rs @@ -63,6 +63,12 @@ pub enum RequestError { actual: u64, }, + #[error( + "OneClient did not contact {url} because the Terms of Service and Privacy Policy were \ + declined. Accept them in Settings to turn Polyfrost services back on." + )] + ConsentRequired { url: String }, + #[error("Invalid URL: {0}")] UrlParseError(#[from] url::ParseError), diff --git a/packages/oneclient_net/src/service.rs b/packages/oneclient_net/src/service.rs index 13036ce2..facd2a8e 100644 --- a/packages/oneclient_net/src/service.rs +++ b/packages/oneclient_net/src/service.rs @@ -13,9 +13,6 @@ use crate::response::{ResponseExt, ResponseOptions}; const MAX_THROTTLE_RETRIES: u32 = 6; -/// Backstop against runaway fan-out not the download throttle -/// Must stay above the sum of per-phase caller concurrency or it becomes the -/// bottleneck const MAX_INFLIGHT_REQUESTS: usize = 64; fn retry_after(response: &Response) -> Option { @@ -145,6 +142,14 @@ impl RequestClient { let mut retries = 0; let mut throttle_retries = 0u32; + if let Some(host) = request.request.url().host_str() + && oneclient_common::consent::blocks_host(host) + { + let url = request.request.url().to_string(); + tracing::debug!(%url, "request withheld: terms and privacy policy declined"); + return Err(RequestError::ConsentRequired { url }); + } + apply_curseforge_auth(&mut request.request, &self.config.load().curseforge_api_key)?; let cloned_backup = request.request.try_clone(); diff --git a/packages/oneclient_net/src/status.rs b/packages/oneclient_net/src/status.rs index 199943ac..13f7ce66 100644 --- a/packages/oneclient_net/src/status.rs +++ b/packages/oneclient_net/src/status.rs @@ -135,6 +135,14 @@ pub async fn check_service_status(requester: &RequestClient) -> ServiceStatus { let mc_auth_up = reachable(client, MC_AUTH_URL).await; + if oneclient_common::consent::blocks_url(POLYFROST_STATUS_URL) { + return ServiceStatus { + online: true, + mc_auth_up, + polyfrost_up: true, + }; + } + let polyfrost_up = match client .get(POLYFROST_STATUS_URL) .timeout(PROBE_TIMEOUT) diff --git a/packages/oneclient_polyplus/src/lib.rs b/packages/oneclient_polyplus/src/lib.rs index 743a7aae..2388d2a8 100644 --- a/packages/oneclient_polyplus/src/lib.rs +++ b/packages/oneclient_polyplus/src/lib.rs @@ -35,6 +35,9 @@ pub enum PlusError { body: String, }, + #[error("Poly+ is off because the Terms of Service and Privacy Policy were declined")] + ConsentRequired, + #[error(transparent)] Request(#[from] reqwest::Error), @@ -67,6 +70,11 @@ pub fn start(auth: Arc) { return; } + if plus_blocked() { + tracing::info!("[plus] playtime reporting stays off: terms and privacy policy declined"); + return; + } + tokio::spawn(async move { let client = match build_client() { Ok(client) => client, @@ -79,6 +87,15 @@ pub fn start(auth: Arc) { let mut delay = RECONNECT_DELAY; loop { + // Consent can only be withdrawn while the launcher is up, never + // restored, so ending the task is final rather than a pause + if plus_blocked() { + tracing::info!( + "[plus] ending playtime reporting: terms and privacy policy declined" + ); + return; + } + delay = match session(&auth, &client).await { Outcome::Connected | Outcome::NoAccount => RECONNECT_DELAY, Outcome::Failed => (delay * 2).min(MAX_RECONNECT_DELAY), @@ -89,6 +106,10 @@ pub fn start(auth: Arc) { }); } +fn plus_blocked() -> bool { + oneclient_common::consent::blocks_url(base_url()) +} + fn build_client() -> Result { #[cfg_attr(not(target_os = "windows"), allow(unused_mut))] let mut builder = reqwest::Client::builder() @@ -173,8 +194,8 @@ async fn run_session( tracing::info!("[plus] playtime websocket connected for {}", account.id); let account_id = account.id; - pump(&mut websocket, PING_INTERVAL, account_id, || { - account_changed(auth, account_id) + pump(&mut websocket, PING_INTERVAL, account_id, || async move { + plus_blocked() || account_changed(auth, account_id).await }) .await } @@ -203,9 +224,7 @@ where match event { Event::Ping => { if should_stop().await { - tracing::info!( - "[plus] default account changed, ending playtime session for {account_id}" - ); + tracing::info!("[plus] ending playtime session for {account_id}"); break; } @@ -227,6 +246,10 @@ async fn login( client: &reqwest::Client, account: &MinecraftAccount, ) -> Result { + if plus_blocked() { + return Err(PlusError::ConsentRequired); + } + let server_id = generate_server_id(); let join = client From 074ef1ce037f9d8f9b896e5e184e053c7fc7e28f Mon Sep 17 00:00:00 2001 From: rozwader Date: Thu, 20 Aug 2026 11:43:46 +0200 Subject: [PATCH 2/2] fix: allowed datastorage-v2, meta.polyfrost.org and status.polyfrost.org --- .../src/view/app/settings/launcher.rs | 2 +- .../src/view/onboarding/terms.rs | 8 ++-- packages/oneclient_common/src/consent.rs | 45 +++++++++++++++---- packages/oneclient_net/src/service.rs | 4 +- packages/oneclient_net/src/status.rs | 8 ---- 5 files changed, 43 insertions(+), 24 deletions(-) diff --git a/packages/oneclient_app/src/view/app/settings/launcher.rs b/packages/oneclient_app/src/view/app/settings/launcher.rs index b5440e17..5eec8d3d 100644 --- a/packages/oneclient_app/src/view/app/settings/launcher.rs +++ b/packages/oneclient_app/src/view/app/settings/launcher.rs @@ -49,7 +49,7 @@ impl Component for SettingsLauncher { // The only way back for someone who declined during onboarding let consent_summary = if settings.declined_tos { - "Declined. Polyfrost services stay off until you accept and restart OneClient." + "Declined. Poly+ and crash reporting stay off until you accept and restart OneClient." } else { "Accepted. Review them again at any time." }; diff --git a/packages/oneclient_app/src/view/onboarding/terms.rs b/packages/oneclient_app/src/view/onboarding/terms.rs index 4b245b4a..3b6934a8 100644 --- a/packages/oneclient_app/src/view/onboarding/terms.rs +++ b/packages/oneclient_app/src/view/onboarding/terms.rs @@ -285,7 +285,7 @@ fn accept_row(accepted: State) -> Element { ) .child( label() - .text("Required for Poly+, downloads and updates.") + .text("Required for Poly+ and crash reporting.") .font_size(11.) .color(colors::fg_secondary()), ), @@ -324,9 +324,9 @@ fn decline_warning() -> Element { .child( label() .text( - "OneClient stops contacting Polyfrost entirely: no Poly+, no crash reports, \ - and no version, mod or bundle downloads. Instances you have already \ - installed keep working, and signing in to Minecraft still works.", + "Poly+ turns off and crash reports stop being sent. Your instances keep \ + working and keep updating: mod bundles and Minecraft versions still \ + download, and signing in to Minecraft is unaffected.", ) .font_size(12.) .color(colors::fg_secondary()), diff --git a/packages/oneclient_common/src/consent.rs b/packages/oneclient_common/src/consent.rs index 525af503..4e3ac14e 100644 --- a/packages/oneclient_common/src/consent.rs +++ b/packages/oneclient_common/src/consent.rs @@ -3,6 +3,13 @@ use std::sync::atomic::{AtomicBool, Ordering}; /// Hosts that are out of bounds for someone who didn't accept TOS const GATED_DOMAINS: &[&str] = &["polyfrost.org", "sentry.io"]; +/// Hosts that are allowed even if someone didn't accept TOS +const ALLOWED_HOSTS: &[&str] = &[ + "data-v2.polyfrost.org", + "meta.polyfrost.org", + "status.polyfrost.org", +]; + static DECLINED: AtomicBool = AtomicBool::new(false); pub fn init(declined: bool) { @@ -23,11 +30,6 @@ pub fn blocks_url(url: &str) -> bool { declined() && is_gated_url(url) } -#[must_use] -pub fn blocks_host(host: &str) -> bool { - declined() && is_gated_host(host) -} - #[must_use] pub fn is_gated_url(url: &str) -> bool { url::Url::parse(url) @@ -40,6 +42,10 @@ pub fn is_gated_url(url: &str) -> bool { pub fn is_gated_host(host: &str) -> bool { let host = host.trim_end_matches('.').to_ascii_lowercase(); + if ALLOWED_HOSTS.contains(&host.as_str()) { + return false; + } + GATED_DOMAINS .iter() .any(|domain| host == *domain || host.ends_with(&format!(".{domain}"))) @@ -54,9 +60,6 @@ mod tests { for host in [ "polyfrost.org", "plus.polyfrost.org", - "meta.polyfrost.org", - "data-v2.polyfrost.org", - "status.polyfrost.org", "PLUS.POLYFROST.ORG", "plus.polyfrost.org.", "o4511714343124992.ingest.us.sentry.io", @@ -82,6 +85,32 @@ mod tests { } } + #[test] + fn lets_the_exempt_hosts_through() { + for url in [ + "https://data-v2.polyfrost.org/oneclient/bundles/metadata.json", + "https://data-v2.polyfrost.org/oneclient/versions/art/Tricky_Trials.jpg", + "https://data-v2.polyfrost.org/oneclient/tos.json", + "https://meta.polyfrost.org/fabric/v2/manifest.json", + "https://status.polyfrost.org/index.json", + "https://META.POLYFROST.ORG./minecraft/v1/manifest.json", + ] { + assert!(!is_gated_url(url), "unexpected gate on {url}"); + } + } + + #[test] + fn the_exemption_is_the_exact_host_and_nothing_near_it() { + for host in [ + "plus.polyfrost.org", + "evil.data-v2.polyfrost.org", + "meta.polyfrost.org.evil.polyfrost.org", + "status.polyfrost.org.evil.polyfrost.org", + ] { + assert!(is_gated_host(host), "expected {host} to stay gated"); + } + } + #[test] fn gates_urls_by_their_host() { assert!(is_gated_url("https://plus.polyfrost.org/account/login")); diff --git a/packages/oneclient_net/src/service.rs b/packages/oneclient_net/src/service.rs index facd2a8e..b645c685 100644 --- a/packages/oneclient_net/src/service.rs +++ b/packages/oneclient_net/src/service.rs @@ -142,9 +142,7 @@ impl RequestClient { let mut retries = 0; let mut throttle_retries = 0u32; - if let Some(host) = request.request.url().host_str() - && oneclient_common::consent::blocks_host(host) - { + if oneclient_common::consent::blocks_url(request.request.url().as_str()) { let url = request.request.url().to_string(); tracing::debug!(%url, "request withheld: terms and privacy policy declined"); return Err(RequestError::ConsentRequired { url }); diff --git a/packages/oneclient_net/src/status.rs b/packages/oneclient_net/src/status.rs index 13f7ce66..199943ac 100644 --- a/packages/oneclient_net/src/status.rs +++ b/packages/oneclient_net/src/status.rs @@ -135,14 +135,6 @@ pub async fn check_service_status(requester: &RequestClient) -> ServiceStatus { let mc_auth_up = reachable(client, MC_AUTH_URL).await; - if oneclient_common::consent::blocks_url(POLYFROST_STATUS_URL) { - return ServiceStatus { - online: true, - mc_auth_up, - polyfrost_up: true, - }; - } - let polyfrost_up = match client .get(POLYFROST_STATUS_URL) .timeout(PROBE_TIMEOUT)