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    /// me-central-1
81    MeCentral1,
82    /// sa-east-1
83    SaEast1,
84    /// Digital Ocean nyc3
85    DoNyc3,
86    /// Digital Ocean ams3
87    DoAms3,
88    /// Digital Ocean sgp1
89    DoSgp1,
90    /// Digital Ocean fra1
91    DoFra1,
92    /// Yandex Object Storage
93    OvhGra,
94    /// gra
95    OvhRbx,
96    /// rbx
97    OvhSbg,
98    /// sbg
99    OvhDe,
100    /// de
101    OvhUk,
102    /// uk
103    OvhWaw,
104    /// waw
105    OvhBhs,
106    /// bhs
107    OvhCaEastTor,
108    /// ca-east-tor
109    OvhSgp,
110    /// sgp
111    Yandex,
112    /// Wasabi us-east-1
113    WaUsEast1,
114    /// Wasabi us-east-2
115    WaUsEast2,
116    /// Wasabi us-central-1
117    WaUsCentral1,
118    /// Wasabi us-west-1
119    WaUsWest1,
120    /// Wasabi ca-central-1
121    WaCaCentral1,
122    /// Wasabi eu-central-1
123    WaEuCentral1,
124    /// Wasabi eu-central-2
125    WaEuCentral2,
126    /// Wasabi eu-west-1
127    WaEuWest1,
128    /// Wasabi eu-west-2
129    WaEuWest2,
130    /// Wasabi ap-northeast-1
131    WaApNortheast1,
132    /// Wasabi ap-northeast-2
133    WaApNortheast2,
134    /// Wasabi ap-southeast-1
135    WaApSoutheast1,
136    /// Wasabi ap-southeast-2
137    WaApSoutheast2,
138    /// Cloudflare R2 (global)
139    R2 { account_id: String },
140    /// Cloudflare R2 EU jurisdiction
141    R2Eu { account_id: String },
142    /// Custom region
143    Custom { region: String, endpoint: String },
144}
145
146impl fmt::Display for Region {
147    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
148        use self::Region::*;
149        match *self {
150            UsEast1 => write!(f, "us-east-1"),
151            UsEast2 => write!(f, "us-east-2"),
152            UsWest1 => write!(f, "us-west-1"),
153            UsWest2 => write!(f, "us-west-2"),
154            AfSouth1 => write!(f, "af-south-1"),
155            CaCentral1 => write!(f, "ca-central-1"),
156            ApEast1 => write!(f, "ap-east-1"),
157            ApSouth1 => write!(f, "ap-south-1"),
158            ApNortheast1 => write!(f, "ap-northeast-1"),
159            ApNortheast2 => write!(f, "ap-northeast-2"),
160            ApNortheast3 => write!(f, "ap-northeast-3"),
161            ApSoutheast1 => write!(f, "ap-southeast-1"),
162            ApSoutheast2 => write!(f, "ap-southeast-2"),
163            CnNorth1 => write!(f, "cn-north-1"),
164            CnNorthwest1 => write!(f, "cn-northwest-1"),
165            EuNorth1 => write!(f, "eu-north-1"),
166            EuCentral1 => write!(f, "eu-central-1"),
167            EuCentral2 => write!(f, "eu-central-2"),
168            EuWest1 => write!(f, "eu-west-1"),
169            EuWest2 => write!(f, "eu-west-2"),
170            EuWest3 => write!(f, "eu-west-3"),
171            SaEast1 => write!(f, "sa-east-1"),
172            IlCentral1 => write!(f, "il-central-1"),
173            MeCentral1 => write!(f, "me-central-1"),
174            MeSouth1 => write!(f, "me-south-1"),
175            DoNyc3 => write!(f, "nyc3"),
176            DoAms3 => write!(f, "ams3"),
177            DoSgp1 => write!(f, "sgp1"),
178            DoFra1 => write!(f, "fra1"),
179            Yandex => write!(f, "ru-central1"),
180            WaUsEast1 => write!(f, "us-east-1"),
181            WaUsEast2 => write!(f, "us-east-2"),
182            WaUsCentral1 => write!(f, "us-central-1"),
183            WaUsWest1 => write!(f, "us-west-1"),
184            WaCaCentral1 => write!(f, "ca-central-1"),
185            WaEuCentral1 => write!(f, "eu-central-1"),
186            WaEuCentral2 => write!(f, "eu-central-2"),
187            WaEuWest1 => write!(f, "eu-west-1"),
188            WaEuWest2 => write!(f, "eu-west-2"),
189            WaApNortheast1 => write!(f, "ap-northeast-1"),
190            WaApNortheast2 => write!(f, "ap-northeast-2"),
191            WaApSoutheast1 => write!(f, "ap-southeast-1"),
192            WaApSoutheast2 => write!(f, "ap-southeast-2"),
193            OvhGra => write!(f, "gra"),
194            OvhRbx => write!(f, "rbx"),
195            OvhSbg => write!(f, "sbg"),
196            OvhDe => write!(f, "de"),
197            OvhUk => write!(f, "uk"),
198            OvhWaw => write!(f, "waw"),
199            OvhBhs => write!(f, "bhs"),
200            OvhCaEastTor => write!(f, "ca-east-tor"),
201            OvhSgp => write!(f, "sgp"),
202            R2 { .. } => write!(f, "auto"),
203            R2Eu { .. } => write!(f, "auto"),
204            Custom { ref region, .. } => write!(f, "{}", region),
205        }
206    }
207}
208
209impl FromStr for Region {
210    type Err = std::str::Utf8Error;
211
212    fn from_str(s: &str) -> Result<Self, Self::Err> {
213        use self::Region::*;
214        match s {
215            "us-east-1" => Ok(UsEast1),
216            "us-east-2" => Ok(UsEast2),
217            "us-west-1" => Ok(UsWest1),
218            "us-west-2" => Ok(UsWest2),
219            "ca-central-1" => Ok(CaCentral1),
220            "af-south-1" => Ok(AfSouth1),
221            "ap-east-1" => Ok(ApEast1),
222            "ap-south-1" => Ok(ApSouth1),
223            "ap-northeast-1" => Ok(ApNortheast1),
224            "ap-northeast-2" => Ok(ApNortheast2),
225            "ap-northeast-3" => Ok(ApNortheast3),
226            "ap-southeast-1" => Ok(ApSoutheast1),
227            "ap-southeast-2" => Ok(ApSoutheast2),
228            "cn-north-1" => Ok(CnNorth1),
229            "cn-northwest-1" => Ok(CnNorthwest1),
230            "eu-north-1" => Ok(EuNorth1),
231            "eu-central-1" => Ok(EuCentral1),
232            "eu-central-2" => Ok(EuCentral2),
233            "eu-west-1" => Ok(EuWest1),
234            "eu-west-2" => Ok(EuWest2),
235            "eu-west-3" => Ok(EuWest3),
236            "sa-east-1" => Ok(SaEast1),
237            "il-central-1" => Ok(IlCentral1),
238            "me-central-1" => Ok(MeCentral1),
239            "me-south-1" => Ok(MeSouth1),
240            "nyc3" => Ok(DoNyc3),
241            "ams3" => Ok(DoAms3),
242            "sgp1" => Ok(DoSgp1),
243            "fra1" => Ok(DoFra1),
244            "yandex" => Ok(Yandex),
245            "ru-central1" => Ok(Yandex),
246            "wa-us-east-1" => Ok(WaUsEast1),
247            "wa-us-east-2" => Ok(WaUsEast2),
248            "wa-us-central-1" => Ok(WaUsCentral1),
249            "wa-us-west-1" => Ok(WaUsWest1),
250            "wa-ca-central-1" => Ok(WaCaCentral1),
251            "wa-eu-central-1" => Ok(WaEuCentral1),
252            "wa-eu-central-2" => Ok(WaEuCentral2),
253            "wa-eu-west-1" => Ok(WaEuWest1),
254            "wa-eu-west-2" => Ok(WaEuWest2),
255            "wa-ap-northeast-1" => Ok(WaApNortheast1),
256            "wa-ap-northeast-2" => Ok(WaApNortheast2),
257            "wa-ap-southeast-1" => Ok(WaApSoutheast1),
258            "wa-ap-southeast-2" => Ok(WaApSoutheast2),
259            x => Ok(Custom {
260                region: x.to_string(),
261                endpoint: x.to_string(),
262            }),
263        }
264    }
265}
266
267impl Region {
268    pub fn endpoint(&self) -> String {
269        use self::Region::*;
270        match *self {
271            // Surprisingly, us-east-1 does not have a
272            // s3-us-east-1.amazonaws.com DNS record
273            UsEast1 => String::from("s3.amazonaws.com"),
274            UsEast2 => String::from("s3-us-east-2.amazonaws.com"),
275            UsWest1 => String::from("s3-us-west-1.amazonaws.com"),
276            UsWest2 => String::from("s3-us-west-2.amazonaws.com"),
277            CaCentral1 => String::from("s3-ca-central-1.amazonaws.com"),
278            AfSouth1 => String::from("s3-af-south-1.amazonaws.com"),
279            ApEast1 => String::from("s3-ap-east-1.amazonaws.com"),
280            ApSouth1 => String::from("s3-ap-south-1.amazonaws.com"),
281            ApNortheast1 => String::from("s3-ap-northeast-1.amazonaws.com"),
282            ApNortheast2 => String::from("s3-ap-northeast-2.amazonaws.com"),
283            ApNortheast3 => String::from("s3-ap-northeast-3.amazonaws.com"),
284            ApSoutheast1 => String::from("s3-ap-southeast-1.amazonaws.com"),
285            ApSoutheast2 => String::from("s3-ap-southeast-2.amazonaws.com"),
286            CnNorth1 => String::from("s3.cn-north-1.amazonaws.com.cn"),
287            CnNorthwest1 => String::from("s3.cn-northwest-1.amazonaws.com.cn"),
288            EuNorth1 => String::from("s3-eu-north-1.amazonaws.com"),
289            EuCentral1 => String::from("s3.eu-central-1.amazonaws.com"),
290            EuCentral2 => String::from("s3.eu-central-2.amazonaws.com"),
291            EuWest1 => String::from("s3-eu-west-1.amazonaws.com"),
292            EuWest2 => String::from("s3-eu-west-2.amazonaws.com"),
293            EuWest3 => String::from("s3-eu-west-3.amazonaws.com"),
294            SaEast1 => String::from("s3-sa-east-1.amazonaws.com"),
295            IlCentral1 => String::from("s3.il-central-1.amazonaws.com"),
296            MeCentral1 => String::from("s3.me-central-1.amazonaws.com"),
297            MeSouth1 => String::from("s3-me-south-1.amazonaws.com"),
298            DoNyc3 => String::from("nyc3.digitaloceanspaces.com"),
299            DoAms3 => String::from("ams3.digitaloceanspaces.com"),
300            DoSgp1 => String::from("sgp1.digitaloceanspaces.com"),
301            DoFra1 => String::from("fra1.digitaloceanspaces.com"),
302            Yandex => String::from("storage.yandexcloud.net"),
303            WaUsEast1 => String::from("s3.us-east-1.wasabisys.com"),
304            WaUsEast2 => String::from("s3.us-east-2.wasabisys.com"),
305            WaUsCentral1 => String::from("s3.us-central-1.wasabisys.com"),
306            WaUsWest1 => String::from("s3.us-west-1.wasabisys.com"),
307            WaCaCentral1 => String::from("s3.ca-central-1.wasabisys.com"),
308            WaEuCentral1 => String::from("s3.eu-central-1.wasabisys.com"),
309            WaEuCentral2 => String::from("s3.eu-central-2.wasabisys.com"),
310            WaEuWest1 => String::from("s3.eu-west-1.wasabisys.com"),
311            WaEuWest2 => String::from("s3.eu-west-2.wasabisys.com"),
312            WaApNortheast1 => String::from("s3.ap-northeast-1.wasabisys.com"),
313            WaApNortheast2 => String::from("s3.ap-northeast-2.wasabisys.com"),
314            WaApSoutheast1 => String::from("s3.ap-southeast-1.wasabisys.com"),
315            WaApSoutheast2 => String::from("s3.ap-southeast-2.wasabisys.com"),
316            OvhGra => String::from("s3.gra.io.cloud.ovh.net"),
317            OvhRbx => String::from("s3.rbx.io.cloud.ovh.net"),
318            OvhSbg => String::from("s3.sbg.io.cloud.ovh.net"),
319            OvhDe => String::from("s3.de.io.cloud.ovh.net"),
320            OvhUk => String::from("s3.uk.io.cloud.ovh.net"),
321            OvhWaw => String::from("s3.waw.io.cloud.ovh.net"),
322            OvhBhs => String::from("s3.bhs.io.cloud.ovh.net"),
323            OvhCaEastTor => String::from("s3.ca-east-tor.io.cloud.ovh.net"),
324            OvhSgp => String::from("s3.sgp.io.cloud.ovh.net"),
325            R2 { ref account_id } => format!("{}.r2.cloudflarestorage.com", account_id),
326            R2Eu { ref account_id } => format!("{}.eu.r2.cloudflarestorage.com", account_id),
327            Custom { ref endpoint, .. } => endpoint.to_string(),
328        }
329    }
330
331    pub fn scheme(&self) -> String {
332        match *self {
333            Region::Custom { ref endpoint, .. } => match endpoint.find("://") {
334                Some(pos) => endpoint[..pos].to_string(),
335                None => "https".to_string(),
336            },
337            _ => "https".to_string(),
338        }
339    }
340
341    pub fn host(&self) -> String {
342        match *self {
343            Region::Custom { ref endpoint, .. } => {
344                let host = match endpoint.find("://") {
345                    Some(pos) => endpoint[pos + 3..].to_string(),
346                    None => endpoint.to_string(),
347                };
348                // Remove trailing slashes to avoid signature mismatches
349                // AWS CLI and other SDKs handle this similarly
350                host.trim_end_matches('/').to_string()
351            }
352            _ => self.endpoint(),
353        }
354    }
355
356    pub fn from_env(region_env: &str, endpoint_env: Option<&str>) -> Result<Region, RegionError> {
357        if let Some(endpoint_env) = endpoint_env {
358            Ok(Region::Custom {
359                region: env::var(region_env)?,
360                endpoint: env::var(endpoint_env)?,
361            })
362        } else {
363            Ok(Region::from_str(&env::var(region_env)?)?)
364        }
365    }
366
367    /// Attempts to create a Region from AWS_REGION and AWS_ENDPOINT environment variables
368    pub fn from_default_env() -> Result<Region, RegionError> {
369        if let Ok(endpoint) = env::var("AWS_ENDPOINT") {
370            Ok(Region::Custom {
371                region: env::var("AWS_REGION")?,
372                endpoint,
373            })
374        } else {
375            Ok(Region::from_str(&env::var("AWS_REGION")?)?)
376        }
377    }
378}
379
380#[test]
381fn yandex_object_storage() {
382    let yandex = Region::Custom {
383        endpoint: "storage.yandexcloud.net".to_string(),
384        region: "ru-central1".to_string(),
385    };
386
387    let yandex_region = "ru-central1".parse::<Region>().unwrap();
388
389    assert_eq!(yandex.endpoint(), yandex_region.endpoint());
390
391    assert_eq!(yandex.to_string(), yandex_region.to_string());
392}
393
394#[test]
395fn test_region_eu_central_2() {
396    let region = "eu-central-2".parse::<Region>().unwrap();
397    assert_eq!(region.endpoint(), "s3.eu-central-2.amazonaws.com");
398}
399
400#[test]
401fn test_region_me_central_1() {
402    let region = "me-central-1".parse::<Region>().unwrap();
403    assert_eq!(region.endpoint(), "s3.me-central-1.amazonaws.com");
404    assert_eq!(region.to_string(), "me-central-1");
405}
406
407#[test]
408fn test_custom_endpoint_trailing_slash() {
409    // Test that trailing slashes are removed from custom endpoints
410    let region_with_slash = Region::Custom {
411        region: "eu-central-1".to_owned(),
412        endpoint: "https://s3.gra.io.cloud.ovh.net/".to_owned(),
413    };
414    assert_eq!(region_with_slash.host(), "s3.gra.io.cloud.ovh.net");
415
416    // Test without trailing slash
417    let region_without_slash = Region::Custom {
418        region: "eu-central-1".to_owned(),
419        endpoint: "https://s3.gra.io.cloud.ovh.net".to_owned(),
420    };
421    assert_eq!(region_without_slash.host(), "s3.gra.io.cloud.ovh.net");
422
423    // Test multiple trailing slashes
424    let region_multiple_slashes = Region::Custom {
425        region: "eu-central-1".to_owned(),
426        endpoint: "https://s3.example.com///".to_owned(),
427    };
428    assert_eq!(region_multiple_slashes.host(), "s3.example.com");
429
430    // Test with port and trailing slash
431    let region_with_port = Region::Custom {
432        region: "eu-central-1".to_owned(),
433        endpoint: "http://localhost:9000/".to_owned(),
434    };
435    assert_eq!(region_with_port.host(), "localhost:9000");
436}