Skip to content

Commit e45e5b0

Browse files
authored
Merge pull request #1346 from CaptainMirage/fix/download-res-reli
Tested locally before merge: - `cargo test --all-targets --features ui` against the merge result on current `origin/main`: 248 passed. - `JAVA_HOME="/Applications/Android Studio.app/Contents/jbr/Contents/Home" ANDROID_HOME="/Users/dev/Library/Android/sdk" ./gradlew :app:assembleDebug`: BUILD SUCCESSFUL, including Rust JNI builds for the shipped Android ABIs. Merging with a merge commit to preserve the PR's internal commits as requested. --- Answered via LLM, Supervised @therealaleph
2 parents 40b5386 + eac5e22 commit e45e5b0

8 files changed

Lines changed: 537 additions & 42 deletions

File tree

src/android_jni.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,7 @@ fn install_logging_once() {
132132
.with_target(false)
133133
.with_ansi(false)
134134
.with_writer(LogcatWriter)
135+
.with_timer(crate::logging::CompactUtcTime)
135136
.try_init();
136137

137138
let _ = rustls::crypto::ring::default_provider().install_default();

src/bin/ui.rs

Lines changed: 166 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -294,6 +294,7 @@ struct FormState {
294294
auto_blacklist_window_secs: u64,
295295
auto_blacklist_cooldown_secs: u64,
296296
request_timeout_secs: u64,
297+
stream_timeout_secs: u64,
297298
/// Optional second-hop exit node for CF-anti-bot bypass (chatgpt.com /
298299
/// claude.ai / grok.com / x.com). Config-only — no UI editor yet.
299300
/// See `assets/exit_node/` for the generic exit-node handler.
@@ -391,6 +392,7 @@ fn load_form() -> (FormState, Option<String>) {
391392
auto_blacklist_window_secs: c.auto_blacklist_window_secs,
392393
auto_blacklist_cooldown_secs: c.auto_blacklist_cooldown_secs,
393394
request_timeout_secs: c.request_timeout_secs,
395+
stream_timeout_secs: c.stream_timeout_secs,
394396
exit_node: c.exit_node.clone(),
395397
}
396398
} else {
@@ -433,6 +435,7 @@ fn load_form() -> (FormState, Option<String>) {
433435
auto_blacklist_window_secs: 30,
434436
auto_blacklist_cooldown_secs: 120,
435437
request_timeout_secs: 30,
438+
stream_timeout_secs: 300,
436439
exit_node: mhrv_rs::config::ExitNodeConfig::default(),
437440
}
438441
};
@@ -618,6 +621,7 @@ impl FormState {
618621
auto_blacklist_window_secs: self.auto_blacklist_window_secs,
619622
auto_blacklist_cooldown_secs: self.auto_blacklist_cooldown_secs,
620623
request_timeout_secs: self.request_timeout_secs,
624+
stream_timeout_secs: self.stream_timeout_secs,
621625
// Exit-node config (CF-anti-bot bypass for chatgpt.com / claude.ai
622626
// / grok.com / x.com). Round-trip through FormState — config-only
623627
// editing for now, UI editor planned for v1.9.x desktop UI batch.
@@ -637,6 +641,166 @@ fn save_config(cfg: &Config) -> Result<PathBuf, String> {
637641
Ok(path)
638642
}
639643

644+
#[derive(serde::Serialize)]
645+
struct ConfigWire<'a> {
646+
mode: &'a str,
647+
google_ip: &'a str,
648+
front_domain: &'a str,
649+
#[serde(skip_serializing_if = "Option::is_none")]
650+
script_id: Option<ScriptIdWire<'a>>,
651+
auth_key: &'a str,
652+
listen_host: &'a str,
653+
listen_port: u16,
654+
#[serde(skip_serializing_if = "Option::is_none")]
655+
socks5_port: Option<u16>,
656+
log_level: &'a str,
657+
verify_ssl: bool,
658+
#[serde(skip_serializing_if = "std::collections::HashMap::is_empty")]
659+
hosts: &'a std::collections::HashMap<String, String>,
660+
#[serde(skip_serializing_if = "Option::is_none")]
661+
upstream_socks5: Option<&'a str>,
662+
#[serde(skip_serializing_if = "is_zero_u8")]
663+
parallel_relay: u8,
664+
#[serde(skip_serializing_if = "Option::is_none")]
665+
sni_hosts: Option<Vec<&'a str>>,
666+
#[serde(skip_serializing_if = "is_false")]
667+
normalize_x_graphql: bool,
668+
#[serde(skip_serializing_if = "is_false")]
669+
youtube_via_relay: bool,
670+
#[serde(skip_serializing_if = "Vec::is_empty")]
671+
passthrough_hosts: &'a Vec<String>,
672+
// IP-scan knobs. These used to be missing from the wire struct, so
673+
// every Save-config silently dropped them — the user would toggle
674+
// "fetch from API" on, save, reopen, and find it off again. Add
675+
// them here and keep them in sync if Config ever grows more.
676+
#[serde(skip_serializing_if = "is_false")]
677+
fetch_ips_from_api: bool,
678+
max_ips_to_scan: usize,
679+
scan_batch_size: usize,
680+
google_ip_validation: bool,
681+
/// Default false (= bypass DoH). Only emitted when explicitly true
682+
/// so unchanged configs stay clean.
683+
#[serde(skip_serializing_if = "is_false")]
684+
tunnel_doh: bool,
685+
#[serde(skip_serializing_if = "Vec::is_empty")]
686+
bypass_doh_hosts: &'a Vec<String>,
687+
/// PR #763: default true (= browser DoH rejected, system DNS used).
688+
/// Skip when matching default to keep unchanged configs clean —
689+
/// emit only when the user has explicitly disabled the block.
690+
#[serde(skip_serializing_if = "is_true")]
691+
block_doh: bool,
692+
/// Default false. Emit only when the user enables STUN/TURN blocking.
693+
#[serde(skip_serializing_if = "is_false")]
694+
block_stun: bool,
695+
#[serde(skip_serializing_if = "Vec::is_empty")]
696+
fronting_groups: &'a Vec<FrontingGroup>,
697+
/// Auto-blacklist tuning + batch timeout (#391, #444, #430). Skip
698+
/// serialization when matching the historical defaults so unchanged
699+
/// configs stay clean — only emitted when the user has explicitly
700+
/// tuned them.
701+
#[serde(skip_serializing_if = "is_default_strikes")]
702+
auto_blacklist_strikes: u32,
703+
#[serde(skip_serializing_if = "is_default_window_secs")]
704+
auto_blacklist_window_secs: u64,
705+
#[serde(skip_serializing_if = "is_default_cooldown_secs")]
706+
auto_blacklist_cooldown_secs: u64,
707+
#[serde(skip_serializing_if = "is_default_timeout_secs")]
708+
request_timeout_secs: u64,
709+
#[serde(skip_serializing_if = "is_default_stream_timeout_secs")]
710+
stream_timeout_secs: u64,
711+
/// HTTP/2 multiplexing kill switch. Default false (h2 active); only
712+
/// emitted on save when the user has explicitly disabled h2, so
713+
/// unchanged configs stay clean.
714+
#[serde(skip_serializing_if = "is_false")]
715+
force_http1: bool,
716+
/// Exit-node config (CF-anti-bot bypass for chatgpt.com / claude.ai /
717+
/// grok.com / x.com via exit-node second-hop relay). Skip when fully
718+
/// default (disabled with no URL/PSK/hosts) so configs without
719+
/// exit-node setup stay clean. Round-tripped through FormState so
720+
/// Save preserves user-edited values.
721+
#[serde(skip_serializing_if = "is_default_exit_node")]
722+
exit_node: &'a mhrv_rs::config::ExitNodeConfig,
723+
}
724+
725+
fn is_default_strikes(v: &u32) -> bool { *v == 3 }
726+
fn is_default_window_secs(v: &u64) -> bool { *v == 30 }
727+
fn is_default_cooldown_secs(v: &u64) -> bool { *v == 120 }
728+
fn is_default_timeout_secs(v: &u64) -> bool { *v == 30 }
729+
fn is_default_stream_timeout_secs(v: &u64) -> bool { *v == 300 }
730+
fn is_default_exit_node(en: &&mhrv_rs::config::ExitNodeConfig) -> bool {
731+
!en.enabled
732+
&& en.relay_url.is_empty()
733+
&& en.psk.is_empty()
734+
&& en.hosts.is_empty()
735+
&& (en.mode.is_empty() || en.mode == "selective")
736+
}
737+
738+
fn is_false(b: &bool) -> bool {
739+
!*b
740+
}
741+
742+
fn is_true(b: &bool) -> bool {
743+
*b
744+
}
745+
746+
fn is_zero_u8(v: &u8) -> bool {
747+
*v == 0
748+
}
749+
750+
#[derive(serde::Serialize)]
751+
#[serde(untagged)]
752+
enum ScriptIdWire<'a> {
753+
One(&'a str),
754+
Many(Vec<&'a str>),
755+
}
756+
757+
impl<'a> From<&'a Config> for ConfigWire<'a> {
758+
fn from(c: &'a Config) -> Self {
759+
let script_id = c.script_id.as_ref().map(|s| match s {
760+
ScriptId::One(v) => ScriptIdWire::One(v.as_str()),
761+
ScriptId::Many(v) => ScriptIdWire::Many(v.iter().map(String::as_str).collect()),
762+
});
763+
ConfigWire {
764+
mode: c.mode.as_str(),
765+
google_ip: c.google_ip.as_str(),
766+
front_domain: c.front_domain.as_str(),
767+
script_id,
768+
auth_key: c.auth_key.as_str(),
769+
listen_host: c.listen_host.as_str(),
770+
listen_port: c.listen_port,
771+
socks5_port: c.socks5_port,
772+
log_level: c.log_level.as_str(),
773+
verify_ssl: c.verify_ssl,
774+
hosts: &c.hosts,
775+
upstream_socks5: c.upstream_socks5.as_deref(),
776+
parallel_relay: c.parallel_relay,
777+
sni_hosts: c
778+
.sni_hosts
779+
.as_ref()
780+
.map(|v| v.iter().map(String::as_str).collect()),
781+
normalize_x_graphql: c.normalize_x_graphql,
782+
youtube_via_relay: c.youtube_via_relay,
783+
passthrough_hosts: &c.passthrough_hosts,
784+
fetch_ips_from_api: c.fetch_ips_from_api,
785+
max_ips_to_scan: c.max_ips_to_scan,
786+
scan_batch_size: c.scan_batch_size,
787+
google_ip_validation: c.google_ip_validation,
788+
tunnel_doh: c.tunnel_doh,
789+
bypass_doh_hosts: &c.bypass_doh_hosts,
790+
block_doh: c.block_doh,
791+
block_stun: c.block_stun,
792+
fronting_groups: &c.fronting_groups,
793+
auto_blacklist_strikes: c.auto_blacklist_strikes,
794+
auto_blacklist_window_secs: c.auto_blacklist_window_secs,
795+
auto_blacklist_cooldown_secs: c.auto_blacklist_cooldown_secs,
796+
request_timeout_secs: c.request_timeout_secs,
797+
stream_timeout_secs: c.stream_timeout_secs,
798+
force_http1: c.force_http1,
799+
exit_node: &c.exit_node,
800+
}
801+
}
802+
}
803+
640804
/// Accent color — same blue used throughout the UI for primary actions.
641805
const ACCENT: egui::Color32 = egui::Color32::from_rgb(70, 120, 180);
642806
const ACCENT_HOVER: egui::Color32 = egui::Color32::from_rgb(90, 145, 205);
@@ -2480,7 +2644,8 @@ fn install_ui_tracing(shared: Arc<Shared>, config_level: &str) {
24802644
let fmt_layer = tracing_subscriber::fmt::layer()
24812645
.with_target(false)
24822646
.with_ansi(false)
2483-
.with_writer(writer);
2647+
.with_writer(writer)
2648+
.with_timer(mhrv_rs::logging::CompactUtcTime);
24842649

24852650
let _ = tracing_subscriber::registry()
24862651
.with(filter_layer)

src/config.rs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -376,9 +376,22 @@ pub struct Config {
376376
/// retry sooner when a deployment hangs. Floor `5`, ceiling `300`
377377
/// (anything beyond exceeds Apps Script's hard 6-min cap with
378378
/// no benefit).
379+
///
380+
/// This applies to connection establishment and response header
381+
/// arrival only. Body streaming is governed by `stream_timeout_secs`.
379382
#[serde(default = "default_request_timeout_secs")]
380383
pub request_timeout_secs: u64,
381384

385+
/// Per-chunk body streaming idle timeout (seconds). Default `300`.
386+
/// Applies to each individual body chunk read after headers arrive —
387+
/// a chunk that goes silent for longer than this is considered a
388+
/// stalled connection and the request is aborted. Distinct from
389+
/// `request_timeout_secs` so large responses through Apps Script
390+
/// (where each 256 KB range chunk can take 30-90s) are not killed
391+
/// mid-transfer. Floor `10`, ceiling `3600`.
392+
#[serde(default = "default_stream_timeout_secs")]
393+
pub stream_timeout_secs: u64,
394+
382395
/// Optional second-hop exit node, for sites that block traffic
383396
/// from Google datacenter IPs (Apps Script's outbound IP space).
384397
/// Most visibly: Cloudflare-fronted services that flag the GCP IP
@@ -531,6 +544,10 @@ fn default_auto_blacklist_cooldown_secs() -> u64 { 120 }
531544
/// hard-coded `BATCH_TIMEOUT` and Apps Script's typical response cliff.
532545
fn default_request_timeout_secs() -> u64 { 30 }
533546

547+
/// Default for `stream_timeout_secs`: 300s per-chunk idle timeout for
548+
/// body streaming, separate from the header/connect timeout.
549+
fn default_stream_timeout_secs() -> u64 { 300 }
550+
534551
fn default_google_ip() -> String {
535552
"216.239.38.120".into()
536553
}
@@ -766,6 +783,8 @@ pub struct TomlRelay {
766783
pub auto_blacklist_cooldown_secs: u64,
767784
#[serde(default = "default_request_timeout_secs")]
768785
pub request_timeout_secs: u64,
786+
#[serde(default = "default_stream_timeout_secs")]
787+
pub stream_timeout_secs: u64,
769788
}
770789

771790
/// [network] section of config.toml.
@@ -919,6 +938,7 @@ impl From<TomlConfig> for Config {
919938
auto_blacklist_window_secs: t.relay.auto_blacklist_window_secs,
920939
auto_blacklist_cooldown_secs: t.relay.auto_blacklist_cooldown_secs,
921940
request_timeout_secs: t.relay.request_timeout_secs,
941+
stream_timeout_secs: t.relay.stream_timeout_secs,
922942
exit_node: t.exit_node,
923943
}
924944
}
@@ -946,6 +966,7 @@ impl From<&Config> for TomlConfig {
946966
auto_blacklist_window_secs: c.auto_blacklist_window_secs,
947967
auto_blacklist_cooldown_secs: c.auto_blacklist_cooldown_secs,
948968
request_timeout_secs: c.request_timeout_secs,
969+
stream_timeout_secs: c.stream_timeout_secs,
949970
},
950971
network: TomlNetwork {
951972
google_ip: c.google_ip.clone(),

0 commit comments

Comments
 (0)