1use std::borrow::Cow;
2use std::sync::LazyLock;
3
4use anyhow::{Context, Result};
5use reqsign::aws::DefaultSigner as AwsDefaultSigner;
6use reqsign::azure::DefaultSigner as AzureDefaultSigner;
7use reqsign::google::DefaultSigner as GcsDefaultSigner;
8use tracing::debug;
9use url::ParseError;
10
11use uv_preview::{Preview, PreviewFeature};
12use uv_redacted::SafeUrl;
13use uv_static::EnvVars;
14use uv_warnings::warn_user_once;
15
16use crate::Credentials;
17use crate::credentials::Token;
18use crate::index::is_path_prefix;
19use crate::realm::{Realm, RealmRef};
20
21static HUGGING_FACE_REALM: LazyLock<Realm> = LazyLock::new(|| {
23 let url = SafeUrl::parse("https://huggingface.co").expect("Failed to parse Hugging Face URL");
24 Realm::from(&url)
25});
26
27static HUGGING_FACE_TOKEN: LazyLock<Option<Vec<u8>>> = LazyLock::new(|| {
29 let hf_token = std::env::var(EnvVars::HF_TOKEN)
31 .ok()
32 .map(String::into_bytes)
33 .filter(|token| !token.is_empty())?;
34
35 if std::env::var_os(EnvVars::UV_NO_HF_TOKEN).is_some() {
36 debug!("Ignoring Hugging Face token from environment due to `UV_NO_HF_TOKEN`");
37 return None;
38 }
39
40 debug!("Found Hugging Face token in environment");
41 Some(hf_token)
42});
43
44#[derive(Debug, Clone, PartialEq, Eq)]
46pub(crate) struct HuggingFaceProvider;
47
48impl HuggingFaceProvider {
49 pub(crate) fn credentials_for(url: &SafeUrl) -> Option<Credentials> {
51 if RealmRef::from(url) == *HUGGING_FACE_REALM {
52 if let Some(token) = HUGGING_FACE_TOKEN.as_ref() {
53 return Some(Credentials::Bearer {
54 token: Token::new(token.clone()),
55 });
56 }
57 }
58 None
59 }
60}
61
62static S3_ENDPOINT_URL: LazyLock<Result<Option<SafeUrl>, ParseError>> =
64 LazyLock::new(|| endpoint_url(EnvVars::UV_S3_ENDPOINT_URL));
65
66#[derive(Debug, Clone, PartialEq, Eq)]
68pub(crate) struct S3EndpointProvider;
69
70impl S3EndpointProvider {
71 pub(crate) fn is_s3_endpoint(url: &SafeUrl, preview: Preview) -> Result<bool> {
73 if let Some(s3_endpoint_url) = S3_ENDPOINT_URL
74 .as_ref()
75 .map_err(|error| *error)
76 .with_context(|| format!("Invalid `{}`", EnvVars::UV_S3_ENDPOINT_URL))?
77 {
78 if !preview.is_enabled(PreviewFeature::S3Endpoint) {
79 warn_user_once!(
80 "The `s3-endpoint` option is experimental and may change without warning. Pass `--preview-features {}` to disable this warning.",
81 PreviewFeature::S3Endpoint
82 );
83 }
84
85 if is_endpoint_url(url, s3_endpoint_url) {
88 return Ok(true);
89 }
90 }
91 Ok(false)
92 }
93
94 pub(crate) fn create_signer() -> AwsDefaultSigner {
99 let region = std::env::var(EnvVars::AWS_REGION)
102 .map(Cow::Owned)
103 .unwrap_or_else(|_| {
104 std::env::var(EnvVars::AWS_DEFAULT_REGION)
105 .map(Cow::Owned)
106 .unwrap_or_else(|_| Cow::Borrowed("us-east-1"))
107 });
108 reqsign::aws::default_signer("s3", ®ion)
109 }
110}
111
112static GCS_ENDPOINT_URL: LazyLock<Result<Option<SafeUrl>, ParseError>> =
114 LazyLock::new(|| endpoint_url(EnvVars::UV_GCS_ENDPOINT_URL));
115
116#[derive(Debug, Clone, PartialEq, Eq)]
118pub(crate) struct GcsEndpointProvider;
119
120impl GcsEndpointProvider {
121 pub(crate) fn is_gcs_endpoint(url: &SafeUrl, preview: Preview) -> Result<bool> {
123 if let Some(gcs_endpoint_url) = GCS_ENDPOINT_URL
124 .as_ref()
125 .map_err(|error| *error)
126 .with_context(|| format!("Invalid `{}`", EnvVars::UV_GCS_ENDPOINT_URL))?
127 {
128 if !preview.is_enabled(PreviewFeature::GcsEndpoint) {
129 warn_user_once!(
130 "The `gcs-endpoint` option is experimental and may change without warning. Pass `--preview-features {}` to disable this warning.",
131 PreviewFeature::GcsEndpoint
132 );
133 }
134
135 if is_endpoint_url(url, gcs_endpoint_url) {
138 return Ok(true);
139 }
140 }
141 Ok(false)
142 }
143
144 pub(crate) fn create_signer() -> GcsDefaultSigner {
149 reqsign::google::default_signer("storage.googleapis.com")
150 }
151}
152
153static AZURE_ENDPOINT_URL: LazyLock<Result<Option<SafeUrl>, ParseError>> =
155 LazyLock::new(|| endpoint_url(EnvVars::UV_AZURE_ENDPOINT_URL));
156
157#[derive(Debug, Clone, PartialEq, Eq)]
159pub struct AzureEndpointProvider;
160
161impl AzureEndpointProvider {
162 pub fn is_azure_endpoint(url: &SafeUrl, preview: Preview) -> Result<bool> {
164 if let Some(azure_endpoint_url) = AZURE_ENDPOINT_URL
165 .as_ref()
166 .map_err(|error| *error)
167 .with_context(|| format!("Invalid `{}`", EnvVars::UV_AZURE_ENDPOINT_URL))?
168 {
169 if !preview.is_enabled(PreviewFeature::AzureEndpoint) {
170 warn_user_once!(
171 "The `azure-endpoint` option is experimental and may change without warning. Pass `--preview-features {}` to disable this warning.",
172 PreviewFeature::AzureEndpoint
173 );
174 }
175
176 if is_endpoint_url(url, azure_endpoint_url) {
179 return Ok(true);
180 }
181 }
182 Ok(false)
183 }
184
185 pub(crate) fn create_signer() -> AzureDefaultSigner {
190 reqsign::azure::default_signer()
191 }
192}
193
194fn endpoint_url(env_var: &str) -> Result<Option<SafeUrl>, ParseError> {
196 let Some(endpoint_url) = std::env::var(env_var).ok() else {
197 return Ok(None);
198 };
199 SafeUrl::parse_relaxed(&endpoint_url).map(Some)
200}
201
202fn is_endpoint_url(url: &SafeUrl, endpoint_url: &SafeUrl) -> bool {
207 let endpoint_realm = RealmRef::from(endpoint_url);
208 let realm = RealmRef::from(url);
209 if realm != endpoint_realm && !realm.is_subdomain_of(endpoint_realm) {
210 return false;
211 }
212
213 is_path_prefix(endpoint_url.path(), url.path())
214}
215
216#[cfg(test)]
217mod tests {
218 use super::*;
219
220 #[test]
221 fn test_endpoint_url_matches_path_prefix() {
222 let endpoint_url = SafeUrl::parse("https://example.com/private").unwrap();
223
224 for url in [
225 "https://example.com/private",
226 "https://example.com/private/",
227 "https://example.com/private/packages/anyio.whl",
228 ] {
229 assert!(
230 is_endpoint_url(&SafeUrl::parse(url).unwrap(), &endpoint_url),
231 "Failed to match endpoint URL prefix: {url}"
232 );
233 }
234 }
235
236 #[test]
237 fn test_endpoint_url_rejects_partial_path_segments() {
238 let endpoint_url = SafeUrl::parse("https://example.com/private").unwrap();
239
240 for url in [
241 "https://example.com/public",
242 "https://example.com/private-bucket",
243 "https://example.com/privatebucket",
244 ] {
245 assert!(
246 !is_endpoint_url(&SafeUrl::parse(url).unwrap(), &endpoint_url),
247 "Should not match URL outside endpoint path: {url}"
248 );
249 }
250 }
251
252 #[test]
253 fn test_endpoint_url_matches_subdomain_with_path_prefix() {
254 let endpoint_url = SafeUrl::parse("https://example.com/private").unwrap();
255
256 assert!(is_endpoint_url(
257 &SafeUrl::parse("https://bucket.example.com/private/package.whl").unwrap(),
258 &endpoint_url
259 ));
260 assert!(!is_endpoint_url(
261 &SafeUrl::parse("https://bucket.example.com/public/package.whl").unwrap(),
262 &endpoint_url
263 ));
264 }
265
266 #[test]
267 fn test_endpoint_url_root_path_matches_all_paths() {
268 let endpoint_url = SafeUrl::parse("https://example.com").unwrap();
269
270 for url in [
271 "https://example.com/package.whl",
272 "https://bucket.example.com/package.whl",
273 ] {
274 assert!(
275 is_endpoint_url(&SafeUrl::parse(url).unwrap(), &endpoint_url),
276 "Failed to match URL under endpoint root: {url}"
277 );
278 }
279 }
280}