@@ -3,6 +3,7 @@ use std::{
3
3
path:: { Path , PathBuf } ,
4
4
process:: { Command , Stdio } ,
5
5
str:: FromStr ,
6
+ sync:: LazyLock ,
6
7
} ;
7
8
8
9
use clap:: { ArgAction , Parser } ;
@@ -15,6 +16,7 @@ use rattler_conda_types::{
15
16
} ;
16
17
use rattler_networking:: s3_middleware;
17
18
use rattler_repodata_gateway:: { Gateway , GatewayBuilder , SourceConfig } ;
19
+ use reqwest:: { NoProxy , Proxy } ;
18
20
use serde:: { de:: IntoDeserializer , Deserialize , Serialize } ;
19
21
use url:: Url ;
20
22
@@ -55,6 +57,25 @@ pub fn get_default_author() -> Option<(String, String)> {
55
57
Some ( ( name?, email. unwrap_or_else ( || "" . into ( ) ) ) )
56
58
}
57
59
60
+ // detect proxy env vars like curl: https://curl.se/docs/manpage.html
61
+ static ENV_HTTP_PROXY : LazyLock < Option < String > > = LazyLock :: new ( || {
62
+ [ "http_proxy" , "all_proxy" , "ALL_PROXY" ]
63
+ . iter ( )
64
+ . find_map ( |& k| std:: env:: var ( k) . ok ( ) . filter ( |v| !v. is_empty ( ) ) )
65
+ } ) ;
66
+ static ENV_HTTPS_PROXY : LazyLock < Option < String > > = LazyLock :: new ( || {
67
+ [ "https_proxy" , "HTTPS_PROXY" , "all_proxy" , "ALL_PROXY" ]
68
+ . iter ( )
69
+ . find_map ( |& k| std:: env:: var ( k) . ok ( ) . filter ( |v| !v. is_empty ( ) ) )
70
+ } ) ;
71
+ static ENV_NO_PROXY : LazyLock < Option < String > > = LazyLock :: new ( || {
72
+ [ "no_proxy" , "NO_PROXY" ]
73
+ . iter ( )
74
+ . find_map ( |& k| std:: env:: var ( k) . ok ( ) . filter ( |v| !v. is_empty ( ) ) )
75
+ } ) ;
76
+ static USE_PROXY_FROM_ENV : LazyLock < bool > =
77
+ LazyLock :: new ( || ( * ENV_HTTPS_PROXY ) . is_some ( ) || ( * ENV_HTTP_PROXY ) . is_some ( ) ) ;
78
+
58
79
/// Get pixi home directory, default to `$HOME/.pixi`
59
80
///
60
81
/// It may be overridden by the `PIXI_HOME` environment variable.
@@ -651,6 +672,11 @@ pub struct Config {
651
672
#[ serde( skip_serializing_if = "ConcurrencyConfig::is_default" ) ]
652
673
pub concurrency : ConcurrencyConfig ,
653
674
675
+ /// Https/Http proxy configuration for pixi
676
+ #[ serde( default ) ]
677
+ #[ serde( skip_serializing_if = "ProxyConfig::is_default" ) ]
678
+ pub proxy_config : ProxyConfig ,
679
+
654
680
//////////////////////
655
681
// Deprecated fields //
656
682
//////////////////////
@@ -681,6 +707,7 @@ impl Default for Config {
681
707
shell : ShellConfig :: default ( ) ,
682
708
experimental : ExperimentalConfig :: default ( ) ,
683
709
concurrency : ConcurrencyConfig :: default ( ) ,
710
+ proxy_config : ProxyConfig :: default ( ) ,
684
711
685
712
// Deprecated fields
686
713
change_ps1 : None ,
@@ -783,6 +810,40 @@ impl ShellConfig {
783
810
}
784
811
}
785
812
813
+ #[ derive( Clone , Debug , Deserialize , Serialize , Default , PartialEq , Eq ) ]
814
+ #[ serde( rename_all = "kebab-case" ) ]
815
+ pub struct ProxyConfig {
816
+ /// https proxy.
817
+ #[ serde( default ) ]
818
+ #[ serde( skip_serializing_if = "Option::is_none" ) ]
819
+ pub https : Option < Url > ,
820
+ /// http proxy.
821
+ #[ serde( default ) ]
822
+ #[ serde( skip_serializing_if = "Option::is_none" ) ]
823
+ pub http : Option < Url > ,
824
+ /// A list of no proxy pattern
825
+ #[ serde( default ) ]
826
+ #[ serde( skip_serializing_if = "Vec::is_empty" ) ]
827
+ pub non_proxy_hosts : Vec < String > ,
828
+ }
829
+
830
+ impl ProxyConfig {
831
+ pub fn is_default ( & self ) -> bool {
832
+ self . https . is_none ( ) && self . https . is_none ( ) && self . non_proxy_hosts . is_empty ( )
833
+ }
834
+ pub fn merge ( & self , other : Self ) -> Self {
835
+ Self {
836
+ https : other. https . as_ref ( ) . or ( self . https . as_ref ( ) ) . cloned ( ) ,
837
+ http : other. http . as_ref ( ) . or ( self . http . as_ref ( ) ) . cloned ( ) ,
838
+ non_proxy_hosts : if other. is_default ( ) {
839
+ self . non_proxy_hosts . clone ( )
840
+ } else {
841
+ other. non_proxy_hosts . clone ( )
842
+ } ,
843
+ }
844
+ }
845
+ }
846
+
786
847
#[ derive( thiserror:: Error , Debug ) ]
787
848
pub enum ConfigError {
788
849
#[ error( "no file was found at {0}" ) ]
@@ -909,6 +970,23 @@ impl Config {
909
970
. validate ( )
910
971
. map_err ( |e| ConfigError :: ValidationError ( e, path. to_path_buf ( ) ) ) ?;
911
972
973
+ // check proxy config
974
+ if config. proxy_config . https . is_none ( ) && config. proxy_config . http . is_none ( ) {
975
+ if !config. proxy_config . non_proxy_hosts . is_empty ( ) {
976
+ tracing:: warn!( "proxy-config.non-proxy-hosts is not empty but will be ignored, as no https or http config is set." )
977
+ }
978
+ } else if * USE_PROXY_FROM_ENV {
979
+ let config_no_proxy = Some ( config. proxy_config . non_proxy_hosts . iter ( ) . join ( "," ) )
980
+ . filter ( |v| !v. is_empty ( ) ) ;
981
+ if ( * ENV_HTTPS_PROXY ) . as_deref ( ) != config. proxy_config . https . as_ref ( ) . map ( Url :: as_str)
982
+ || ( * ENV_HTTP_PROXY ) . as_deref ( )
983
+ != config. proxy_config . http . as_ref ( ) . map ( Url :: as_str)
984
+ || * ENV_NO_PROXY != config_no_proxy
985
+ {
986
+ tracing:: info!( "proxy configs are overridden by proxy environment vars." )
987
+ }
988
+ }
989
+
912
990
Ok ( config)
913
991
}
914
992
@@ -1047,6 +1125,10 @@ impl Config {
1047
1125
"s3-options.<bucket>.region" ,
1048
1126
"s3-options.<bucket>.force-path-style" ,
1049
1127
"experimental.use-environment-activation-cache" ,
1128
+ "proxy-config" ,
1129
+ "proxy-config.https" ,
1130
+ "proxy-config.http" ,
1131
+ "proxy-config.non-proxy-hosts" ,
1050
1132
]
1051
1133
}
1052
1134
@@ -1087,6 +1169,8 @@ impl Config {
1087
1169
// Make other take precedence over self to allow for setting the value through the CLI
1088
1170
concurrency : self . concurrency . merge ( other. concurrency ) ,
1089
1171
1172
+ proxy_config : self . proxy_config . merge ( other. proxy_config ) ,
1173
+
1090
1174
// Deprecated fields that we can ignore as we handle them inside `shell.` field
1091
1175
change_ps1 : None ,
1092
1176
force_activate : None ,
@@ -1162,6 +1246,43 @@ impl Config {
1162
1246
self . concurrency . downloads
1163
1247
}
1164
1248
1249
+ pub fn get_proxies ( & self ) -> reqwest:: Result < Vec < Proxy > > {
1250
+ if ( self . proxy_config . https . is_none ( ) && self . proxy_config . http . is_none ( ) )
1251
+ || * USE_PROXY_FROM_ENV
1252
+ {
1253
+ return Ok ( vec ! [ ] ) ;
1254
+ }
1255
+
1256
+ let config_no_proxy =
1257
+ Some ( self . proxy_config . non_proxy_hosts . iter ( ) . join ( "," ) ) . filter ( |v| !v. is_empty ( ) ) ;
1258
+
1259
+ let mut result: Vec < Proxy > = Vec :: new ( ) ;
1260
+ let config_no_proxy: Option < NoProxy > =
1261
+ config_no_proxy. as_deref ( ) . and_then ( NoProxy :: from_string) ;
1262
+
1263
+ if self . proxy_config . https == self . proxy_config . http {
1264
+ result. push (
1265
+ Proxy :: all (
1266
+ self . proxy_config
1267
+ . https
1268
+ . as_ref ( )
1269
+ . expect ( "must be some" )
1270
+ . as_str ( ) ,
1271
+ ) ?
1272
+ . no_proxy ( config_no_proxy) ,
1273
+ ) ;
1274
+ } else {
1275
+ if let Some ( url) = & self . proxy_config . http {
1276
+ result. push ( Proxy :: http ( url. as_str ( ) ) ?. no_proxy ( config_no_proxy. clone ( ) ) ) ;
1277
+ }
1278
+ if let Some ( url) = & self . proxy_config . https {
1279
+ result. push ( Proxy :: https ( url. as_str ( ) ) ?. no_proxy ( config_no_proxy) ) ;
1280
+ }
1281
+ }
1282
+
1283
+ Ok ( result)
1284
+ }
1285
+
1165
1286
/// Modify this config with the given key and value
1166
1287
///
1167
1288
/// # Note
@@ -1426,6 +1547,42 @@ impl Config {
1426
1547
_ => return Err ( err) ,
1427
1548
}
1428
1549
}
1550
+ key if key. starts_with ( "proxy-config" ) => {
1551
+ if key == "proxy-config" {
1552
+ if let Some ( value) = value {
1553
+ self . proxy_config = serde_json:: de:: from_str ( & value) . into_diagnostic ( ) ?;
1554
+ } else {
1555
+ self . proxy_config = ProxyConfig :: default ( ) ;
1556
+ }
1557
+ return Ok ( ( ) ) ;
1558
+ } else if !key. starts_with ( "proxy-config." ) {
1559
+ return Err ( err) ;
1560
+ }
1561
+
1562
+ let subkey = key. strip_prefix ( "proxy-config." ) . unwrap ( ) ;
1563
+ match subkey {
1564
+ "https" => {
1565
+ self . proxy_config . https = value
1566
+ . map ( |v| Url :: parse ( & v) )
1567
+ . transpose ( )
1568
+ . into_diagnostic ( ) ?;
1569
+ }
1570
+ "http" => {
1571
+ self . proxy_config . http = value
1572
+ . map ( |v| Url :: parse ( & v) )
1573
+ . transpose ( )
1574
+ . into_diagnostic ( ) ?;
1575
+ }
1576
+ "non-proxy-hosts" => {
1577
+ self . proxy_config . non_proxy_hosts = value
1578
+ . map ( |v| serde_json:: de:: from_str ( & v) )
1579
+ . transpose ( )
1580
+ . into_diagnostic ( ) ?
1581
+ . unwrap_or_default ( ) ;
1582
+ }
1583
+ _ => return Err ( err) ,
1584
+ }
1585
+ }
1429
1586
_ => return Err ( err) ,
1430
1587
}
1431
1588
@@ -1728,6 +1885,7 @@ UNUSED = "unused"
1728
1885
RepodataChannelConfig :: default ( ) ,
1729
1886
) ] ) ,
1730
1887
} ,
1888
+ proxy_config : ProxyConfig :: default ( ) ,
1731
1889
// Deprecated keys
1732
1890
change_ps1 : None ,
1733
1891
force_activate : None ,
@@ -2148,4 +2306,25 @@ UNUSED = "unused"
2148
2306
assert_eq ! ( anaconda_config. disable_zstd, Some ( false ) ) ;
2149
2307
assert_eq ! ( anaconda_config. disable_sharded, None ) ;
2150
2308
}
2309
+
2310
+ #[ test]
2311
+ fn test_proxy_config_parse ( ) {
2312
+ let toml = r#"
2313
+ [proxy-config]
2314
+ https = "http://proxy-for-https"
2315
+ http = "http://proxy-for-http"
2316
+ non-proxy-hosts = [ "a.com" ]
2317
+ "# ;
2318
+ let ( config, _) = Config :: from_toml ( toml, None ) . unwrap ( ) ;
2319
+ assert_eq ! (
2320
+ config. proxy_config. https,
2321
+ Some ( Url :: parse( "http://proxy-for-https" ) . unwrap( ) )
2322
+ ) ;
2323
+ assert_eq ! (
2324
+ config. proxy_config. http,
2325
+ Some ( Url :: parse( "http://proxy-for-http" ) . unwrap( ) )
2326
+ ) ;
2327
+ assert_eq ! ( config. proxy_config. non_proxy_hosts. len( ) , 1 ) ;
2328
+ assert_eq ! ( config. proxy_config. non_proxy_hosts[ 0 ] , "a.com" ) ;
2329
+ }
2151
2330
}
0 commit comments