awsregion/
region.rs

1#[cfg(feature = "serde")]
2use serde::{Deserialize, Serialize};
3use std::str::{self, FromStr};
4use std::{env, fmt};
5
6use crate::error::RegionError;
7
8/// AWS S3 [region identifier](https://docs.aws.amazon.com/general/latest/gr/rande.html#s3_region),
9/// passing in custom values is also possible, in that case it is up to you to pass a valid endpoint,
10/// otherwise boom will happen :)
11///
12/// Serde support available with the `serde` feature
13///
14/// # Example
15/// ```
16/// use std::str::FromStr;
17/// use awsregion::Region;
18///
19/// // Parse from a string
20/// let region: Region = "us-east-1".parse().unwrap();
21///
22/// // Choose region directly
23/// let region = Region::EuWest2;
24///
25/// // Custom region requires valid region name and endpoint
26/// let region_name = "nl-ams".to_string();
27/// let endpoint = "https://s3.nl-ams.scw.cloud".to_string();
28/// let region = Region::Custom { region: region_name, endpoint };
29///
30/// ```
31#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
32#[derive(Clone, Debug, Eq, PartialEq)]
33pub enum Region {
34    /// us-east-1
35    UsEast1,
36    /// us-east-2
37    UsEast2,
38    /// us-west-1
39    UsWest1,
40    /// us-west-2
41    UsWest2,
42    /// ca-central-1
43    CaCentral1,
44    /// af-south-1
45    AfSouth1,
46    /// ap-east-1
47    ApEast1,
48    /// ap-south-1
49    ApSouth1,
50    /// ap-northeast-1
51    ApNortheast1,
52    /// ap-northeast-2
53    ApNortheast2,
54    /// ap-northeast-3
55    ApNortheast3,
56    /// ap-southeast-1
57    ApSoutheast1,
58    /// ap-southeast-2
59    ApSoutheast2,
60    /// cn-north-1
61    CnNorth1,
62    /// cn-northwest-1
63    CnNorthwest1,
64    /// eu-north-1
65    EuNorth1,
66    /// eu-central-1
67    EuCentral1,
68    /// eu-central-2
69    EuCentral2,
70    /// eu-west-1
71    EuWest1,
72    /// eu-west-2
73    EuWest2,
74    /// eu-west-3
75    EuWest3,
76    /// il-central-1
77    IlCentral1,
78    /// me-south-1
79    MeSouth1,
80    /// sa-east-1
81    SaEast1,
82    /// Digital Ocean nyc3
83    DoNyc3,
84    /// Digital Ocean ams3
85    DoAms3,
86    /// Digital Ocean sgp1
87    DoSgp1,
88    /// Digital Ocean fra1
89    DoFra1,
90    /// Yandex Object Storage
91    OvhGra,
92    /// gra
93    OvhRbx,
94    /// rbx
95    OvhSbg,
96    /// sbg
97    OvhDe,
98    /// de
99    OvhUk,
100    /// uk
101    OvhWaw,
102    /// waw
103    OvhBhs,
104    /// bhs
105    OvhCaEastTor,
106    /// ca-east-tor
107    OvhSgp,
108    /// sgp
109    Yandex,
110    /// Wasabi us-east-1
111    WaUsEast1,
112    /// Wasabi us-east-2
113    WaUsEast2,
114    /// Wasabi us-central-1
115    WaUsCentral1,
116    /// Wasabi us-west-1
117    WaUsWest1,
118    /// Wasabi ca-central-1
119    WaCaCentral1,
120    /// Wasabi eu-central-1
121    WaEuCentral1,
122    /// Wasabi eu-central-2
123    WaEuCentral2,
124    /// Wasabi eu-west-1
125    WaEuWest1,
126    /// Wasabi eu-west-2
127    WaEuWest2,
128    /// Wasabi ap-northeast-1
129    WaApNortheast1,
130    /// Wasabi ap-northeast-2
131    WaApNortheast2,
132    /// Wasabi ap-southeast-1
133    WaApSoutheast1,
134    /// Wasabi ap-southeast-2
135    WaApSoutheast2,
136    /// Cloudflare R2 (global)
137    R2 { account_id: String },
138    /// Cloudflare R2 EU jurisdiction
139    R2Eu { account_id: String },
140    /// Custom region
141    Custom { region: String, endpoint: String },
142}
143
144impl fmt::Display for Region {
145    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
146        use self::Region::*;
147        match *self {
148            UsEast1 => write!(f, "us-east-1"),
149            UsEast2 => write!(f, "us-east-2"),
150            UsWest1 => write!(f, "us-west-1"),
151            UsWest2 => write!(f, "us-west-2"),
152            AfSouth1 => write!(f, "af-south-1"),
153            CaCentral1 => write!(f, "ca-central-1"),
154            ApEast1 => write!(f, "ap-east-1"),
155            ApSouth1 => write!(f, "ap-south-1"),
156            ApNortheast1 => write!(f, "ap-northeast-1"),
157            ApNortheast2 => write!(f, "ap-northeast-2"),
158            ApNortheast3 => write!(f, "ap-northeast-3"),
159            ApSoutheast1 => write!(f, "ap-southeast-1"),
160            ApSoutheast2 => write!(f, "ap-southeast-2"),
161            CnNorth1 => write!(f, "cn-north-1"),
162            CnNorthwest1 => write!(f, "cn-northwest-1"),
163            EuNorth1 => write!(f, "eu-north-1"),
164            EuCentral1 => write!(f, "eu-central-1"),
165            EuCentral2 => write!(f, "eu-central-2"),
166            EuWest1 => write!(f, "eu-west-1"),
167            EuWest2 => write!(f, "eu-west-2"),
168            EuWest3 => write!(f, "eu-west-3"),
169            SaEast1 => write!(f, "sa-east-1"),
170            IlCentral1 => write!(f, "il-central-1"),
171            MeSouth1 => write!(f, "me-south-1"),
172            DoNyc3 => write!(f, "nyc3"),
173            DoAms3 => write!(f, "ams3"),
174            DoSgp1 => write!(f, "sgp1"),
175            DoFra1 => write!(f, "fra1"),
176            Yandex => write!(f, "ru-central1"),
177            WaUsEast1 => write!(f, "us-east-1"),
178            WaUsEast2 => write!(f, "us-east-2"),
179            WaUsCentral1 => write!(f, "us-central-1"),
180            WaUsWest1 => write!(f, "us-west-1"),
181            WaCaCentral1 => write!(f, "ca-central-1"),
182            WaEuCentral1 => write!(f, "eu-central-1"),
183            WaEuCentral2 => write!(f, "eu-central-2"),
184            WaEuWest1 => write!(f, "eu-west-1"),
185            WaEuWest2 => write!(f, "eu-west-2"),
186            WaApNortheast1 => write!(f, "ap-northeast-1"),
187            WaApNortheast2 => write!(f, "ap-northeast-2"),
188            WaApSoutheast1 => write!(f, "ap-southeast-1"),
189            WaApSoutheast2 => write!(f, "ap-southeast-2"),
190            OvhGra => write!(f, "gra"),
191            OvhRbx => write!(f, "rbx"),
192            OvhSbg => write!(f, "sbg"),
193            OvhDe => write!(f, "de"),
194            OvhUk => write!(f, "uk"),
195            OvhWaw => write!(f, "waw"),
196            OvhBhs => write!(f, "bhs"),
197            OvhCaEastTor => write!(f, "ca-east-tor"),
198            OvhSgp => write!(f, "sgp"),
199            R2 { .. } => write!(f, "auto"),
200            R2Eu { .. } => write!(f, "auto"),
201            Custom { ref region, .. } => write!(f, "{}", region),
202        }
203    }
204}
205
206impl FromStr for Region {
207    type Err = std::str::Utf8Error;
208
209    fn from_str(s: &str) -> Result<Self, Self::Err> {
210        use self::Region::*;
211        match s {
212            "us-east-1" => Ok(UsEast1),
213            "us-east-2" => Ok(UsEast2),
214            "us-west-1" => Ok(UsWest1),
215            "us-west-2" => Ok(UsWest2),
216            "ca-central-1" => Ok(CaCentral1),
217            "af-south-1" => Ok(AfSouth1),
218            "ap-east-1" => Ok(ApEast1),
219            "ap-south-1" => Ok(ApSouth1),
220            "ap-northeast-1" => Ok(ApNortheast1),
221            "ap-northeast-2" => Ok(ApNortheast2),
222            "ap-northeast-3" => Ok(ApNortheast3),
223            "ap-southeast-1" => Ok(ApSoutheast1),
224            "ap-southeast-2" => Ok(ApSoutheast2),
225            "cn-north-1" => Ok(CnNorth1),
226            "cn-northwest-1" => Ok(CnNorthwest1),
227            "eu-north-1" => Ok(EuNorth1),
228            "eu-central-1" => Ok(EuCentral1),
229            "eu-central-2" => Ok(EuCentral2),
230            "eu-west-1" => Ok(EuWest1),
231            "eu-west-2" => Ok(EuWest2),
232            "eu-west-3" => Ok(EuWest3),
233            "sa-east-1" => Ok(SaEast1),
234            "il-central-1" => Ok(IlCentral1),
235            "me-south-1" => Ok(MeSouth1),
236            "nyc3" => Ok(DoNyc3),
237            "ams3" => Ok(DoAms3),
238            "sgp1" => Ok(DoSgp1),
239            "fra1" => Ok(DoFra1),
240            "yandex" => Ok(Yandex),
241            "ru-central1" => Ok(Yandex),
242            "wa-us-east-1" => Ok(WaUsEast1),
243            "wa-us-east-2" => Ok(WaUsEast2),
244            "wa-us-central-1" => Ok(WaUsCentral1),
245            "wa-us-west-1" => Ok(WaUsWest1),
246            "wa-ca-central-1" => Ok(WaCaCentral1),
247            "wa-eu-central-1" => Ok(WaEuCentral1),
248            "wa-eu-central-2" => Ok(WaEuCentral2),
249            "wa-eu-west-1" => Ok(WaEuWest1),
250            "wa-eu-west-2" => Ok(WaEuWest2),
251            "wa-ap-northeast-1" => Ok(WaApNortheast1),
252            "wa-ap-northeast-2" => Ok(WaApNortheast2),
253            "wa-ap-southeast-1" => Ok(WaApSoutheast1),
254            "wa-ap-southeast-2" => Ok(WaApSoutheast2),
255            x => Ok(Custom {
256                region: x.to_string(),
257                endpoint: x.to_string(),
258            }),
259        }
260    }
261}
262
263impl Region {
264    pub fn endpoint(&self) -> String {
265        use self::Region::*;
266        match *self {
267            // Surprisingly, us-east-1 does not have a
268            // s3-us-east-1.amazonaws.com DNS record
269            UsEast1 => String::from("s3.amazonaws.com"),
270            UsEast2 => String::from("s3-us-east-2.amazonaws.com"),
271            UsWest1 => String::from("s3-us-west-1.amazonaws.com"),
272            UsWest2 => String::from("s3-us-west-2.amazonaws.com"),
273            CaCentral1 => String::from("s3-ca-central-1.amazonaws.com"),
274            AfSouth1 => String::from("s3-af-south-1.amazonaws.com"),
275            ApEast1 => String::from("s3-ap-east-1.amazonaws.com"),
276            ApSouth1 => String::from("s3-ap-south-1.amazonaws.com"),
277            ApNortheast1 => String::from("s3-ap-northeast-1.amazonaws.com"),
278            ApNortheast2 => String::from("s3-ap-northeast-2.amazonaws.com"),
279            ApNortheast3 => String::from("s3-ap-northeast-3.amazonaws.com"),
280            ApSoutheast1 => String::from("s3-ap-southeast-1.amazonaws.com"),
281            ApSoutheast2 => String::from("s3-ap-southeast-2.amazonaws.com"),
282            CnNorth1 => String::from("s3.cn-north-1.amazonaws.com.cn"),
283            CnNorthwest1 => String::from("s3.cn-northwest-1.amazonaws.com.cn"),
284            EuNorth1 => String::from("s3-eu-north-1.amazonaws.com"),
285            EuCentral1 => String::from("s3.eu-central-1.amazonaws.com"),
286            EuCentral2 => String::from("s3.eu-central-2.amazonaws.com"),
287            EuWest1 => String::from("s3-eu-west-1.amazonaws.com"),
288            EuWest2 => String::from("s3-eu-west-2.amazonaws.com"),
289            EuWest3 => String::from("s3-eu-west-3.amazonaws.com"),
290            SaEast1 => String::from("s3-sa-east-1.amazonaws.com"),
291            IlCentral1 => String::from("s3.il-central-1.amazonaws.com"),
292            MeSouth1 => String::from("s3-me-south-1.amazonaws.com"),
293            DoNyc3 => String::from("nyc3.digitaloceanspaces.com"),
294            DoAms3 => String::from("ams3.digitaloceanspaces.com"),
295            DoSgp1 => String::from("sgp1.digitaloceanspaces.com"),
296            DoFra1 => String::from("fra1.digitaloceanspaces.com"),
297            Yandex => String::from("storage.yandexcloud.net"),
298            WaUsEast1 => String::from("s3.us-east-1.wasabisys.com"),
299            WaUsEast2 => String::from("s3.us-east-2.wasabisys.com"),
300            WaUsCentral1 => String::from("s3.us-central-1.wasabisys.com"),
301            WaUsWest1 => String::from("s3.us-west-1.wasabisys.com"),
302            WaCaCentral1 => String::from("s3.ca-central-1.wasabisys.com"),
303            WaEuCentral1 => String::from("s3.eu-central-1.wasabisys.com"),
304            WaEuCentral2 => String::from("s3.eu-central-2.wasabisys.com"),
305            WaEuWest1 => String::from("s3.eu-west-1.wasabisys.com"),
306            WaEuWest2 => String::from("s3.eu-west-2.wasabisys.com"),
307            WaApNortheast1 => String::from("s3.ap-northeast-1.wasabisys.com"),
308            WaApNortheast2 => String::from("s3.ap-northeast-2.wasabisys.com"),
309            WaApSoutheast1 => String::from("s3.ap-southeast-1.wasabisys.com"),
310            WaApSoutheast2 => String::from("s3.ap-southeast-2.wasabisys.com"),
311            OvhGra => String::from("s3.gra.io.cloud.ovh.net"),
312            OvhRbx => String::from("s3.rbx.io.cloud.ovh.net"),
313            OvhSbg => String::from("s3.sbg.io.cloud.ovh.net"),
314            OvhDe => String::from("s3.de.io.cloud.ovh.net"),
315            OvhUk => String::from("s3.uk.io.cloud.ovh.net"),
316            OvhWaw => String::from("s3.waw.io.cloud.ovh.net"),
317            OvhBhs => String::from("s3.bhs.io.cloud.ovh.net"),
318            OvhCaEastTor => String::from("s3.ca-east-tor.io.cloud.ovh.net"),
319            OvhSgp => String::from("s3.sgp.io.cloud.ovh.net"),
320            R2 { ref account_id } => format!("{}.r2.cloudflarestorage.com", account_id),
321            R2Eu { ref account_id } => format!("{}.eu.r2.cloudflarestorage.com", account_id),
322            Custom { ref endpoint, .. } => endpoint.to_string(),
323        }
324    }
325
326    pub fn scheme(&self) -> String {
327        match *self {
328            Region::Custom { ref endpoint, .. } => match endpoint.find("://") {
329                Some(pos) => endpoint[..pos].to_string(),
330                None => "https".to_string(),
331            },
332            _ => "https".to_string(),
333        }
334    }
335
336    pub fn host(&self) -> String {
337        match *self {
338            Region::Custom { ref endpoint, .. } => {
339                let host = match endpoint.find("://") {
340                    Some(pos) => endpoint[pos + 3..].to_string(),
341                    None => endpoint.to_string(),
342                };
343                // Remove trailing slashes to avoid signature mismatches
344                // AWS CLI and other SDKs handle this similarly
345                host.trim_end_matches('/').to_string()
346            }
347            _ => self.endpoint(),
348        }
349    }
350
351    pub fn from_env(region_env: &str, endpoint_env: Option<&str>) -> Result<Region, RegionError> {
352        if let Some(endpoint_env) = endpoint_env {
353            Ok(Region::Custom {
354                region: env::var(region_env)?,
355                endpoint: env::var(endpoint_env)?,
356            })
357        } else {
358            Ok(Region::from_str(&env::var(region_env)?)?)
359        }
360    }
361
362    /// Attempts to create a Region from AWS_REGION and AWS_ENDPOINT environment variables
363    pub fn from_default_env() -> Result<Region, RegionError> {
364        if let Ok(endpoint) = env::var("AWS_ENDPOINT") {
365            Ok(Region::Custom {
366                region: env::var("AWS_REGION")?,
367                endpoint,
368            })
369        } else {
370            Ok(Region::from_str(&env::var("AWS_REGION")?)?)
371        }
372    }
373}
374
375#[test]
376fn yandex_object_storage() {
377    let yandex = Region::Custom {
378        endpoint: "storage.yandexcloud.net".to_string(),
379        region: "ru-central1".to_string(),
380    };
381
382    let yandex_region = "ru-central1".parse::<Region>().unwrap();
383
384    assert_eq!(yandex.endpoint(), yandex_region.endpoint());
385
386    assert_eq!(yandex.to_string(), yandex_region.to_string());
387}
388
389#[test]
390fn test_region_eu_central_2() {
391    let region = "eu-central-2".parse::<Region>().unwrap();
392    assert_eq!(region.endpoint(), "s3.eu-central-2.amazonaws.com");
393}
394
395#[test]
396fn test_custom_endpoint_trailing_slash() {
397    // Test that trailing slashes are removed from custom endpoints
398    let region_with_slash = Region::Custom {
399        region: "eu-central-1".to_owned(),
400        endpoint: "https://s3.gra.io.cloud.ovh.net/".to_owned(),
401    };
402    assert_eq!(region_with_slash.host(), "s3.gra.io.cloud.ovh.net");
403
404    // Test without trailing slash
405    let region_without_slash = Region::Custom {
406        region: "eu-central-1".to_owned(),
407        endpoint: "https://s3.gra.io.cloud.ovh.net".to_owned(),
408    };
409    assert_eq!(region_without_slash.host(), "s3.gra.io.cloud.ovh.net");
410
411    // Test multiple trailing slashes
412    let region_multiple_slashes = Region::Custom {
413        region: "eu-central-1".to_owned(),
414        endpoint: "https://s3.example.com///".to_owned(),
415    };
416    assert_eq!(region_multiple_slashes.host(), "s3.example.com");
417
418    // Test with port and trailing slash
419    let region_with_port = Region::Custom {
420        region: "eu-central-1".to_owned(),
421        endpoint: "http://localhost:9000/".to_owned(),
422    };
423    assert_eq!(region_with_port.host(), "localhost:9000");
424}