Skip to main content

kellnr_settings/
proxy.rs

1use provcfg::{ClapArgs, Configurable};
2use serde::{Deserialize, Serialize};
3use url::Url;
4
5fn default_proxy_url() -> Url {
6    Url::parse("https://static.crates.io/crates/").unwrap()
7}
8
9fn default_index_url() -> Url {
10    Url::parse("https://index.crates.io/").unwrap()
11}
12
13fn default_api_url() -> Url {
14    Url::parse("https://crates.io/api/v1/crates/").unwrap()
15}
16
17fn default_user_agent() -> String {
18    kellnr_common::cratesio_downloader::DEFAULT_USER_AGENT.to_string()
19}
20
21#[derive(Debug, Deserialize, Serialize, Eq, PartialEq, Clone, Configurable, ClapArgs)]
22#[serde(default)]
23#[configurable(clap_prefix = "proxy")]
24pub struct Proxy {
25    /// Enable crates.io proxy
26    pub enabled: bool,
27
28    /// Number of proxy threads
29    pub num_threads: usize,
30
31    /// Download crates on index update
32    pub download_on_update: bool,
33
34    /// Crates.io download URL
35    pub url: Url,
36
37    /// Crates.io index URL
38    pub index: Url,
39
40    /// Crates.io API URL
41    pub api: Url,
42
43    /// Connect timeout in seconds for upstream requests
44    #[arg(long = "proxy-connect-timeout")]
45    pub connect_timeout_seconds: u64,
46
47    /// Request timeout in seconds for upstream downloads
48    #[arg(long = "proxy-request-timeout")]
49    pub request_timeout_seconds: u64,
50
51    /// User-agent sent with requests to crates.io. crates.io's data-access
52    /// policy asks for a user-agent that identifies the application and a way
53    /// to get in contact, e.g. "kellnr.io/kellnr (contact@example.com)".
54    #[arg(long = "proxy-user-agent")]
55    pub user_agent: String,
56}
57
58impl Default for Proxy {
59    fn default() -> Self {
60        Self {
61            enabled: false,
62            num_threads: 10,
63            download_on_update: false,
64            url: default_proxy_url(),
65            index: default_index_url(),
66            api: default_api_url(),
67            connect_timeout_seconds: 5,
68            request_timeout_seconds: 30,
69            user_agent: default_user_agent(),
70        }
71    }
72}
73
74impl Proxy {
75    /// The upstream sparse-index URL that internal cargo invocations (e.g. the
76    /// docs builder) should resolve crates.io dependencies from, if it differs
77    /// from the built-in crates.io default.
78    ///
79    /// Returns `None` when the proxy is disabled or the default index is in
80    /// use. In both cases cargo's own default (`index.crates.io`) is correct
81    /// and source replacement would be redundant. When `Some`, callers should
82    /// point cargo's `crates-io` source at this URL so dependency resolution
83    /// honors `proxy.index` instead of hitting crates.io directly. See #1185.
84    pub fn cratesio_index_override(&self) -> Option<&Url> {
85        if self.enabled && self.index != default_index_url() {
86            Some(&self.index)
87        } else {
88            None
89        }
90    }
91}
92
93#[cfg(test)]
94mod tests {
95    use super::*;
96
97    #[test]
98    fn default_api_url_is_cratesio() {
99        let proxy = Proxy::default();
100        assert_eq!(
101            proxy.api,
102            Url::parse("https://crates.io/api/v1/crates/").unwrap()
103        );
104    }
105
106    #[test]
107    fn deserialize_custom_api_url_from_toml() {
108        let toml = r#"
109            api = "https://rsproxy.cn/api/v1/crates/"
110        "#;
111        let proxy: Proxy = toml::from_str(toml).unwrap();
112        assert_eq!(
113            proxy.api,
114            Url::parse("https://rsproxy.cn/api/v1/crates/").unwrap()
115        );
116    }
117
118    #[test]
119    fn missing_api_url_uses_default() {
120        let toml = r#"
121            enabled = true
122            url = "https://rsproxy.cn/api/v1/crates/"
123            index = "https://rsproxy.cn/index/"
124        "#;
125        let proxy: Proxy = toml::from_str(toml).unwrap();
126        assert_eq!(
127            proxy.api,
128            Url::parse("https://crates.io/api/v1/crates/").unwrap()
129        );
130    }
131
132    #[test]
133    fn api_url_join_produces_correct_crate_url() {
134        let proxy: Proxy = toml::from_str(r#"api = "https://rsproxy.cn/api/v1/crates/""#).unwrap();
135        let url = proxy.api.join("serde").unwrap();
136        assert_eq!(url.as_str(), "https://rsproxy.cn/api/v1/crates/serde");
137    }
138
139    #[test]
140    fn default_url_join_produces_correct_crate_url() {
141        let proxy = Proxy::default();
142        let url = proxy.api.join("tokio").unwrap();
143        assert_eq!(url.as_str(), "https://crates.io/api/v1/crates/tokio");
144    }
145
146    #[test]
147    fn default_user_agent_is_kellnr() {
148        let proxy = Proxy::default();
149        assert_eq!(proxy.user_agent, "kellnr.io/kellnr");
150    }
151
152    #[test]
153    fn missing_user_agent_uses_default() {
154        let proxy: Proxy = toml::from_str("enabled = true").unwrap();
155        assert_eq!(proxy.user_agent, "kellnr.io/kellnr");
156    }
157
158    #[test]
159    fn deserialize_custom_user_agent_from_toml() {
160        let toml = r#"user_agent = "kellnr.io/kellnr (admin@example.com)""#;
161        let proxy: Proxy = toml::from_str(toml).unwrap();
162        assert_eq!(proxy.user_agent, "kellnr.io/kellnr (admin@example.com)");
163    }
164
165    #[test]
166    fn index_override_none_when_disabled() {
167        let proxy: Proxy = toml::from_str(r#"index = "https://rsproxy.cn/index/""#).unwrap();
168        assert!(!proxy.enabled);
169        assert_eq!(proxy.cratesio_index_override(), None);
170    }
171
172    #[test]
173    fn index_override_none_when_default_index() {
174        let proxy: Proxy = toml::from_str("enabled = true").unwrap();
175        assert_eq!(proxy.cratesio_index_override(), None);
176    }
177
178    #[test]
179    fn index_override_some_when_enabled_and_custom() {
180        let toml = r#"
181            enabled = true
182            index = "https://rsproxy.cn/index/"
183        "#;
184        let proxy: Proxy = toml::from_str(toml).unwrap();
185        assert_eq!(
186            proxy.cratesio_index_override().map(Url::as_str),
187            Some("https://rsproxy.cn/index/")
188        );
189    }
190}