Skip to content

Commit 2bfe8fa

Browse files
author
=
committed
fix(client): parse Windows per-scheme proxy settings
1 parent 911d0f2 commit 2bfe8fa

1 file changed

Lines changed: 153 additions & 4 deletions

File tree

src/client/proxy/matcher.rs

Lines changed: 153 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -662,6 +662,66 @@ mod mac {
662662
#[cfg(feature = "client-proxy-system")]
663663
#[cfg(windows)]
664664
mod win {
665+
666+
struct WindowsProxy {
667+
http: Option<String>,
668+
https: Option<String>,
669+
all: Option<String>,
670+
}
671+
672+
const HTTP_SCHEME: &str = "http";
673+
const HTTPS_SCHEME: &str = "https";
674+
const SOCKS_SCHEME: &str = "socks";
675+
676+
const SOCKS_WINDOWS_MAP_TO: &str = "socks5";
677+
678+
impl WindowsProxy {
679+
fn uri_from_schema_and_authority(scheme: &str, authority: &str) -> String {
680+
let safe_authority = authority
681+
.split_once("://")
682+
.map(|(_scheme, authority)| authority)
683+
.unwrap_or(authority);
684+
format!("{scheme}://{safe_authority}")
685+
}
686+
687+
fn from_proxy_server_setting(proxy_server_setting: &str) -> Self {
688+
let mut http = None;
689+
let mut https = None;
690+
let mut all = None;
691+
for proxy in proxy_server_setting
692+
.split(';')
693+
.map(str::trim)
694+
.filter(|p| !p.is_empty())
695+
{
696+
let schema_authority = proxy
697+
.split_once('=')
698+
.map(|(a, b)| (Some(a), Some(b)))
699+
.unwrap_or((None, None));
700+
match schema_authority {
701+
(Some(HTTP_SCHEME), Some(authority)) => http = Some(authority.to_owned()),
702+
(Some(HTTPS_SCHEME), Some(authority)) => https = Some(authority.to_owned()),
703+
(Some(SOCKS_SCHEME), Some(authority)) => {
704+
all = Some(Self::uri_from_schema_and_authority(
705+
SOCKS_WINDOWS_MAP_TO,
706+
authority,
707+
))
708+
}
709+
// What to do if a non-schemed proxy is there? Set all, or http and https
710+
// Lets set all so http and https can override later on Matcher's build
711+
(None, None) => all = Some(proxy.into()),
712+
_ => {}
713+
}
714+
}
715+
Self { http, https, all }
716+
}
717+
}
718+
719+
impl From<String> for WindowsProxy {
720+
fn from(value: String) -> Self {
721+
Self::from_proxy_server_setting(value.as_str())
722+
}
723+
}
724+
665725
pub(super) fn with_system(builder: &mut super::Builder) {
666726
let settings = if let Ok(settings) = windows_registry::CURRENT_USER
667727
.open("Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings")
@@ -676,11 +736,21 @@ mod win {
676736
}
677737

678738
if let Ok(val) = settings.get_string("ProxyServer") {
679-
if builder.http.is_empty() {
680-
builder.http = val.clone();
739+
let windows_proxy: WindowsProxy = val.into();
740+
if let Some(val) = windows_proxy.http {
741+
if builder.http.is_empty() {
742+
builder.http = val;
743+
}
681744
}
682-
if builder.https.is_empty() {
683-
builder.https = val;
745+
if let Some(val) = windows_proxy.https {
746+
if builder.https.is_empty() {
747+
builder.https = val;
748+
}
749+
}
750+
if let Some(val) = windows_proxy.all {
751+
if builder.all.is_empty() {
752+
builder.all = val;
753+
}
684754
}
685755
}
686756

@@ -695,6 +765,85 @@ mod win {
695765
}
696766
}
697767
}
768+
769+
#[cfg(test)]
770+
mod tests {
771+
use super::WindowsProxy;
772+
773+
#[test]
774+
fn per_scheme_values_go_to_their_own_slots() {
775+
let proxy = WindowsProxy::from_proxy_server_setting("http=127.0.0.1:8080;https=127.0.0.1:8443");
776+
assert_eq!(proxy.http.as_deref(), Some("127.0.0.1:8080"));
777+
assert_eq!(proxy.https.as_deref(), Some("127.0.0.1:8443"));
778+
assert_eq!(proxy.all, None);
779+
}
780+
781+
#[test]
782+
fn socks_is_normalized_to_socks5_and_stored_as_all() {
783+
let proxy = WindowsProxy::from_proxy_server_setting("socks=127.0.0.1:1080");
784+
assert_eq!(proxy.http, None);
785+
assert_eq!(proxy.https, None);
786+
assert_eq!(proxy.all.as_deref(), Some("socks5://127.0.0.1:1080"));
787+
}
788+
789+
#[test]
790+
fn socks_with_existing_scheme_is_rewritten_to_socks5() {
791+
let proxy = WindowsProxy::from_proxy_server_setting("socks=socks://127.0.0.1:1080");
792+
assert_eq!(proxy.all.as_deref(), Some("socks5://127.0.0.1:1080"));
793+
}
794+
795+
#[test]
796+
fn all_schemes_together() {
797+
let proxy = WindowsProxy::from_proxy_server_setting(
798+
"http=10.0.0.1:8080;https=10.0.0.1:8443;socks=10.0.0.1:1080",
799+
);
800+
assert_eq!(proxy.http.as_deref(), Some("10.0.0.1:8080"));
801+
assert_eq!(proxy.https.as_deref(), Some("10.0.0.1:8443"));
802+
assert_eq!(proxy.all.as_deref(), Some("socks5://10.0.0.1:1080"));
803+
}
804+
805+
#[test]
806+
fn single_proxy_without_scheme_goes_to_all() {
807+
let proxy = WindowsProxy::from_proxy_server_setting("127.0.0.1:8080");
808+
assert_eq!(proxy.http, None);
809+
assert_eq!(proxy.https, None);
810+
assert_eq!(proxy.all.as_deref(), Some("127.0.0.1:8080"));
811+
}
812+
813+
#[test]
814+
fn unknown_schemes_are_ignored() {
815+
let proxy = WindowsProxy::from_proxy_server_setting("ftp=127.0.0.1:8021");
816+
assert_eq!(proxy.http, None);
817+
assert_eq!(proxy.https, None);
818+
assert_eq!(proxy.all, None);
819+
}
820+
821+
#[test]
822+
fn whitespace_and_empty_segments_are_ignored() {
823+
let proxy = WindowsProxy::from_proxy_server_setting(" http=127.0.0.1:8080 ; ; ");
824+
assert_eq!(proxy.http.as_deref(), Some("127.0.0.1:8080"));
825+
assert_eq!(proxy.https, None);
826+
assert_eq!(proxy.all, None);
827+
}
828+
829+
#[test]
830+
fn trailing_separator_does_not_clobber_socks() {
831+
let proxy = WindowsProxy::from_proxy_server_setting("socks=127.0.0.1:1080;");
832+
assert_eq!(proxy.all.as_deref(), Some("socks5://127.0.0.1:1080"));
833+
}
834+
835+
#[test]
836+
fn uri_from_schema_and_authority_strips_existing_scheme() {
837+
assert_eq!(
838+
WindowsProxy::uri_from_schema_and_authority("socks5", "127.0.0.1:1080"),
839+
"socks5://127.0.0.1:1080"
840+
);
841+
assert_eq!(
842+
WindowsProxy::uri_from_schema_and_authority("socks5", "socks://127.0.0.1:1080"),
843+
"socks5://127.0.0.1:1080"
844+
);
845+
}
846+
}
698847
}
699848

700849
#[cfg(test)]

0 commit comments

Comments
 (0)