1use std::collections::HashMap;
2use std::env;
3#[cfg(not(target_arch = "wasm32"))]
4use std::fs;
5
6#[cfg(not(target_arch = "wasm32"))]
7use anyhow::anyhow;
8#[cfg(not(target_arch = "wasm32"))]
9use anyhow::Result;
10#[cfg(not(target_arch = "wasm32"))]
11use ini::Ini;
12#[cfg(not(target_arch = "wasm32"))]
13use log::debug;
14
15use super::constants::*;
16#[cfg(not(target_arch = "wasm32"))]
17use crate::dirs::expand_homedir;
18
19#[derive(Clone)]
21#[cfg_attr(test, derive(Debug))]
22pub struct Config {
23 pub config_file: String,
28 pub shared_credentials_file: String,
33 pub profile: String,
39
40 pub region: Option<String>,
46 pub sts_regional_endpoints: String,
52 pub access_key_id: Option<String>,
58 pub secret_access_key: Option<String>,
64 pub session_token: Option<String>,
70 pub role_arn: Option<String>,
76 pub role_session_name: String,
82 pub duration_seconds: Option<usize>,
88 pub external_id: Option<String>,
93 pub tags: Option<Vec<(String, String)>>,
97 pub web_identity_token_file: Option<String>,
103 pub ec2_metadata_disabled: bool,
108 pub endpoint_url: Option<String>,
113}
114
115impl Default for Config {
116 fn default() -> Self {
117 Self {
118 config_file: "~/.aws/config".to_string(),
119 shared_credentials_file: "~/.aws/credentials".to_string(),
120 profile: "default".to_string(),
121 region: None,
122 sts_regional_endpoints: "legacy".to_string(),
123 access_key_id: None,
124 secret_access_key: None,
125 session_token: None,
126 role_arn: None,
127 role_session_name: "reqsign".to_string(),
128 duration_seconds: Some(3600),
129 external_id: None,
130 tags: None,
131 web_identity_token_file: None,
132 ec2_metadata_disabled: false,
133 endpoint_url: None,
134 }
135 }
136}
137
138impl Config {
139 pub fn from_env(mut self) -> Self {
141 let envs = env::vars().collect::<HashMap<_, _>>();
142
143 if let Some(v) = envs.get(AWS_CONFIG_FILE) {
144 self.config_file = v.to_string();
145 }
146 if let Some(v) = envs.get(AWS_SHARED_CREDENTIALS_FILE) {
147 self.shared_credentials_file = v.to_string();
148 }
149 if let Some(v) = envs.get(AWS_PROFILE) {
150 self.profile = v.to_string();
151 }
152 if let Some(v) = envs.get(AWS_REGION) {
153 self.region = Some(v.to_string())
154 }
155 if let Some(v) = envs.get(AWS_STS_REGIONAL_ENDPOINTS) {
156 self.sts_regional_endpoints = v.to_string();
157 }
158 if let Some(v) = envs.get(AWS_ACCESS_KEY_ID) {
159 self.access_key_id = Some(v.to_string())
160 }
161 if let Some(v) = envs.get(AWS_SECRET_ACCESS_KEY) {
162 self.secret_access_key = Some(v.to_string())
163 }
164 if let Some(v) = envs.get(AWS_SESSION_TOKEN) {
165 self.session_token = Some(v.to_string())
166 }
167 if let Some(v) = envs.get(AWS_ROLE_ARN) {
168 self.role_arn = Some(v.to_string())
169 }
170 if let Some(v) = envs.get(AWS_ROLE_SESSION_NAME) {
171 self.role_session_name = v.to_string();
172 }
173 if let Some(v) = envs.get(AWS_WEB_IDENTITY_TOKEN_FILE) {
174 self.web_identity_token_file = Some(v.to_string());
175 }
176 if let Some(v) = envs.get(AWS_EC2_METADATA_DISABLED) {
177 self.ec2_metadata_disabled = v == "true";
178 }
179 if let Some(v) = envs.get(AWS_ENDPOINT_URL) {
180 self.endpoint_url = Some(v.to_string());
181 }
182 self
183 }
184
185 #[cfg(not(target_arch = "wasm32"))]
190 pub fn from_profile(mut self) -> Self {
191 if let Ok(profile) = env::var(AWS_PROFILE) {
193 self.profile = profile;
194 }
195
196 if let Ok(config_file) = env::var(AWS_CONFIG_FILE) {
200 self.config_file = config_file;
201 }
202
203 if let Ok(shared_credentials_file) = env::var(AWS_SHARED_CREDENTIALS_FILE) {
204 self.shared_credentials_file = shared_credentials_file;
205 }
206
207 let _ = self.load_via_profile_config_file().map_err(|err| {
209 debug!("load_via_profile_config_file failed: {err:?}");
210 });
211
212 let _ = self
213 .load_via_profile_shared_credentials_file()
214 .map_err(|err| debug!("load_via_profile_shared_credentials_file failed: {err:?}"));
215
216 self
217 }
218
219 #[cfg(not(target_arch = "wasm32"))]
225 fn load_via_profile_shared_credentials_file(&mut self) -> Result<()> {
226 let path = expand_homedir(&self.shared_credentials_file)
227 .ok_or_else(|| anyhow!("expand homedir failed"))?;
228
229 let _ = fs::metadata(&path)?;
230
231 let conf = Ini::load_from_file(path)?;
232
233 let props = conf
234 .section(Some(&self.profile))
235 .ok_or_else(|| anyhow!("section {} is not found", self.profile))?;
236
237 if let Some(v) = props.get("aws_access_key_id") {
238 self.access_key_id = Some(v.to_string())
239 }
240 if let Some(v) = props.get("aws_secret_access_key") {
241 self.secret_access_key = Some(v.to_string())
242 }
243 if let Some(v) = props.get("aws_session_token") {
244 self.session_token = Some(v.to_string())
245 }
246
247 Ok(())
248 }
249
250 #[cfg(not(target_arch = "wasm32"))]
251 fn load_via_profile_config_file(&mut self) -> Result<()> {
252 let path =
253 expand_homedir(&self.config_file).ok_or_else(|| anyhow!("expand homedir failed"))?;
254
255 let _ = fs::metadata(&path)?;
256
257 let conf = Ini::load_from_file(path)?;
258
259 let section = match self.profile.as_str() {
260 "default" => "default".to_string(),
261 x => format!("profile {x}"),
262 };
263 let props = conf
264 .section(Some(section))
265 .ok_or_else(|| anyhow!("section {} is not found", self.profile))?;
266
267 if let Some(v) = props.get("region") {
268 self.region = Some(v.to_string())
269 }
270 if let Some(v) = props.get("sts_regional_endpoints") {
271 self.sts_regional_endpoints = v.to_string();
272 }
273 if let Some(v) = props.get("aws_access_key_id") {
274 self.access_key_id = Some(v.to_string())
275 }
276 if let Some(v) = props.get("aws_secret_access_key") {
277 self.secret_access_key = Some(v.to_string())
278 }
279 if let Some(v) = props.get("aws_session_token") {
280 self.session_token = Some(v.to_string())
281 }
282 if let Some(v) = props.get("role_arn") {
283 self.role_arn = Some(v.to_string())
284 }
285 if let Some(v) = props.get("role_session_name") {
286 self.role_session_name = v.to_string()
287 }
288 if let Some(v) = props.get("duration_seconds") {
289 self.duration_seconds = Some(v.to_string().parse::<usize>().unwrap())
290 }
291 if let Some(v) = props.get("web_identity_token_file") {
292 self.web_identity_token_file = Some(v.to_string())
293 }
294 if let Some(v) = props.get("endpoint_url") {
295 self.endpoint_url = Some(v.to_string())
296 }
297
298 Ok(())
299 }
300}
301
302#[cfg(test)]
303mod tests {
304 use super::*;
305 use pretty_assertions::assert_eq;
306 use std::fs::File;
307 use std::io::Write;
308 use tempfile::tempdir;
309
310 #[test]
311 #[cfg(not(target_arch = "wasm32"))]
312 fn test_config_from_profile_shared_credentials() -> Result<()> {
313 let _ = env_logger::builder().is_test(true).try_init();
314
315 let tmp_dir = tempdir()?;
317 let file_path = tmp_dir.path().join("credentials");
318 let mut tmp_file = File::create(&file_path)?;
319 writeln!(tmp_file, "[default]")?;
320 writeln!(tmp_file, "aws_access_key_id = DEFAULTACCESSKEYID")?;
321 writeln!(tmp_file, "aws_secret_access_key = DEFAULTSECRETACCESSKEY")?;
322 writeln!(tmp_file, "aws_session_token = DEFAULTSESSIONTOKEN")?;
323 writeln!(tmp_file)?;
324 writeln!(tmp_file, "[profile1]")?;
325 writeln!(tmp_file, "aws_access_key_id = PROFILE1ACCESSKEYID")?;
326 writeln!(tmp_file, "aws_secret_access_key = PROFILE1SECRETACCESSKEY")?;
327 writeln!(tmp_file, "aws_session_token = PROFILE1SESSIONTOKEN")?;
328
329 temp_env::with_vars(
330 [
331 (AWS_PROFILE, Some("profile1".to_owned())),
332 (AWS_CONFIG_FILE, None::<String>),
333 (
334 AWS_SHARED_CREDENTIALS_FILE,
335 Some(file_path.to_str().unwrap().to_owned()),
336 ),
337 ],
338 || {
339 let config = Config::default().from_profile();
340
341 assert_eq!(config.profile, "profile1".to_owned());
342 assert_eq!(config.access_key_id, Some("PROFILE1ACCESSKEYID".to_owned()));
343 assert_eq!(
344 config.secret_access_key,
345 Some("PROFILE1SECRETACCESSKEY".to_owned())
346 );
347 assert_eq!(
348 config.session_token,
349 Some("PROFILE1SESSIONTOKEN".to_owned())
350 );
351 },
352 );
353
354 Ok(())
355 }
356
357 #[test]
358 #[cfg(not(target_arch = "wasm32"))]
359 fn test_config_from_profile_config() -> Result<()> {
360 let _ = env_logger::builder().is_test(true).try_init();
361
362 let tmp_dir = tempdir()?;
364 let file_path = tmp_dir.path().join("config");
365 let mut tmp_file = File::create(&file_path)?;
366 writeln!(tmp_file, "[default]")?;
367 writeln!(tmp_file, "aws_access_key_id = DEFAULTACCESSKEYID")?;
368 writeln!(tmp_file, "aws_secret_access_key = DEFAULTSECRETACCESSKEY")?;
369 writeln!(tmp_file, "aws_session_token = DEFAULTSESSIONTOKEN")?;
370 writeln!(tmp_file)?;
371 writeln!(tmp_file, "[profile profile1]")?;
372 writeln!(tmp_file, "aws_access_key_id = PROFILE1ACCESSKEYID")?;
373 writeln!(tmp_file, "aws_secret_access_key = PROFILE1SECRETACCESSKEY")?;
374 writeln!(tmp_file, "aws_session_token = PROFILE1SESSIONTOKEN")?;
375 writeln!(tmp_file, "endpoint_url = http://localhost:8080")?;
376
377 temp_env::with_vars(
378 [
379 (AWS_PROFILE, Some("profile1".to_owned())),
380 (
381 AWS_CONFIG_FILE,
382 Some(file_path.to_str().unwrap().to_owned()),
383 ),
384 (AWS_SHARED_CREDENTIALS_FILE, None::<String>),
385 ],
386 || {
387 let config = Config::default().from_profile();
388
389 assert_eq!(config.profile, "profile1".to_owned());
390 assert_eq!(config.access_key_id, Some("PROFILE1ACCESSKEYID".to_owned()));
391 assert_eq!(
392 config.secret_access_key,
393 Some("PROFILE1SECRETACCESSKEY".to_owned())
394 );
395 assert_eq!(
396 config.session_token,
397 Some("PROFILE1SESSIONTOKEN".to_owned())
398 );
399 assert_eq!(
400 config.endpoint_url,
401 Some("http://localhost:8080".to_owned())
402 );
403 },
404 );
405
406 Ok(())
407 }
408}