diff --git a/crates/ide-core/src/proxy_settings.rs b/crates/ide-core/src/proxy_settings.rs index 564c2df90..ebae548d7 100644 --- a/crates/ide-core/src/proxy_settings.rs +++ b/crates/ide-core/src/proxy_settings.rs @@ -207,6 +207,58 @@ impl ProxySettings { env.push((OsString::from("no_proxy"), Some(OsString::from(no_proxy)))); env } + + /// System properties that make a JVM follow this configuration. A JVM reads none of + /// [`Self::child_env`]: a Java tool (jetty, Maven's resolver, a language server's own + /// HTTP client) goes direct without these, and a repository its proxy answers in + /// milliseconds can then hang it instead. + pub fn jvm_args(&self) -> Vec { + match self.mode { + // macOS and Windows JVMs read the system proxy with this one. + ProxyMode::Auto => vec!["-Djava.net.useSystemProxies=true".into()], + ProxyMode::None => Vec::new(), + ProxyMode::Manual if self.manual() => self.manual_jvm_args(), + ProxyMode::Manual => Vec::new(), + } + } + + fn manual_jvm_args(&self) -> Vec { + let (host, port) = (self.host.trim(), self.port); + match self.kind { + ProxyKind::Http => { + // HTTPS hosts read `https.proxyHost`, plain ones `http.proxyHost`. + let mut args = vec![ + format!("-Dhttp.proxyHost={host}"), + format!("-Dhttp.proxyPort={port}"), + format!("-Dhttps.proxyHost={host}"), + format!("-Dhttps.proxyPort={port}"), + ]; + let hosts = self.non_proxy_hosts(); + if !hosts.is_empty() { + args.push(format!("-Dhttp.nonProxyHosts={}", hosts.join("|"))); + } + args + } + // SOCKS proxies every TCP connection, so there is one property pair. + ProxyKind::Socks => vec![ + format!("-DsocksProxyHost={host}"), + format!("-DsocksProxyPort={port}"), + "-Djava.net.useSystemProxies=false".into(), + ], + } + } + + /// [`Self::no_proxy`] in the JVM's `http.nonProxyHosts` format: `|` between host + /// globs. CIDR ranges and IPv6 literals have no equivalent there and are dropped. + fn non_proxy_hosts(&self) -> Vec { + let mut hosts: Vec = LOOPBACK.iter().filter(|h| !h.contains(':')).map(|h| (*h).to_owned()).collect(); + for entry in self.no_proxy_entries() { + if !entry.contains(['/', ':', '[', ']']) && !hosts.contains(&entry) { + hosts.push(entry); + } + } + hosts + } } /// Case-insensitive host glob (`pattern` is lowercase): `*` matches any run of characters. @@ -292,6 +344,34 @@ mod tests { assert!(p.child_env().is_empty()); } + #[test] + fn jvm_args_carry_the_proxy_as_system_properties() { + // A JVM reads none of `child_env`: a Java language server needs these to not go + // direct while curl, git and Node follow the environment. + assert_eq!(ProxySettings::default().jvm_args(), ["-Djava.net.useSystemProxies=true"]); + assert!(ProxySettings { mode: ProxyMode::None, ..manual("proxy", 3128) }.jvm_args().is_empty()); + assert_eq!( + manual("proxy.corp", 3128).jvm_args(), + [ + "-Dhttp.proxyHost=proxy.corp", + "-Dhttp.proxyPort=3128", + "-Dhttps.proxyHost=proxy.corp", + "-Dhttps.proxyPort=3128", + "-Dhttp.nonProxyHosts=localhost|127.0.0.1", + ] + ); + let socks = ProxySettings { kind: ProxyKind::Socks, ..manual("10.0.0.1", 1080) }; + assert_eq!(socks.jvm_args()[..2], ["-DsocksProxyHost=10.0.0.1", "-DsocksProxyPort=1080"]); + // `http.nonProxyHosts` takes host globs: CIDR ranges and IPv6 literals drop out. + let p = ProxySettings { + no_proxy: "*.corp.com, 172.0.0.0/8, [::1], ::1, localhost".into(), + ..manual("proxy.corp", 3128) + }; + assert_eq!(p.jvm_args().last().unwrap(), "-Dhttp.nonProxyHosts=localhost|127.0.0.1|*.corp.com"); + // A manual proxy without a host connects directly, and so does its JVM. + assert!(manual(" ", 80).jvm_args().is_empty()); + } + #[test] fn manual_http_and_socks_urls() { let p = manual("proxy.corp", 3128); diff --git a/crates/ide-sdk/src/java.rs b/crates/ide-sdk/src/java.rs index 9d104583a..21b217dc0 100644 --- a/crates/ide-sdk/src/java.rs +++ b/crates/ide-sdk/src/java.rs @@ -313,6 +313,19 @@ fn text_key(text: &str) -> String { format!("{h:016x}") } +fn add_jdtls_proxy_args(args: &mut Vec, proxy: &ide_core::proxy_settings::ProxySettings) { + for arg in proxy.jvm_args() { + let key = arg.split('=').next().unwrap_or(arg.as_str()); + if !args.iter().enumerate().any(|(i, a)| { + let value = + a.strip_prefix("--jvm-arg=").or_else(|| (i > 0 && args[i - 1] == "--jvm-arg").then_some(a.as_str())); + value.is_some_and(|a| a.split('=').next() == Some(key)) + }) { + args.push(format!("--jvm-arg={arg}")); + } + } +} + const METADATA_PROPERTY: &str = "java.import.generatesMetadataFilesAtProjectRoot"; /// jdtls: its Eclipse workspace (`-data`) lives under the IDE's data @@ -367,6 +380,10 @@ pub fn configure_jdtls( cfg.args.push(format!("--jvm-arg=-javaagent:{}", jar.to_string_lossy())); } crate::jdtls::add_default_vm_args(&mut cfg.args); + // jdtls resolves Maven dependencies through its own HTTP client, and a JVM reads none + // of the `HTTP_PROXY` / `HTTPS_PROXY` it inherits: pass the same proxy as system + // properties. One the user set in their own JVM arguments wins. + add_jdtls_proxy_args(&mut cfg.args, &ide_core::proxy_settings::proxy()); let managed = language_server_runtimes_dir(data_dir); let managed = dunce::canonicalize(&managed).unwrap_or(managed); let project_jdks: Vec<_> = project_jdk @@ -705,6 +722,35 @@ mod tests { assert!(other.args.is_empty() && other.initialization_options.is_none()); } + #[test] + fn jdtls_proxy_properties_respect_exact_user_overrides() { + use ide_core::proxy_settings::{ProxyMode, ProxySettings}; + let proxy = + ProxySettings { mode: ProxyMode::Manual, host: "ide.proxy".into(), port: 3128, ..Default::default() }; + for (user_arg, overridden) in [ + ("--jvm-arg=-Dhttp.proxyHost=user.proxy", true), + ("--jvm-arg=-Dhttp.proxyHost", true), + ("--jvm-arg=-Dhttp.proxyHostSuffix=unrelated", false), + ("--jvm-arg=-Dother=-Dhttp.proxyHost", false), + ("/workspace/-Dhttp.proxyHost", false), + ] { + let mut forms = vec![vec![user_arg.to_owned()]]; + if let Some(value) = user_arg.strip_prefix("--jvm-arg=") { + forms.push(vec!["--jvm-arg".into(), value.into()]); + } + for mut args in forms { + let user_args = args.clone(); + add_jdtls_proxy_args(&mut args, &proxy); + assert_eq!(args[..user_args.len()], user_args); + assert_eq!(args.contains(&"--jvm-arg=-Dhttp.proxyHost=ide.proxy".into()), !overridden, "{user_arg}"); + assert!(args.contains(&"--jvm-arg=-Dhttps.proxyHost=ide.proxy".into())); + let before = args.clone(); + add_jdtls_proxy_args(&mut args, &proxy); + assert_eq!(args, before, "idempotent: {user_arg}"); + } + } + } + #[test] fn jdtls_lombok_agent() { let jdtls = || -> LanguageServerConfig { toml::from_str("name = \"jdtls\"\ncommand = \"jdtls\"").unwrap() };