1use super::config;
2use super::totp;
3use std::env;
4use std::fmt;
5use std::fs::{DirBuilder, File};
6use std::io::prelude::*;
7use std::path::Path;
8
9const SAVE_DIR_NAME: &str = "mfa-cli";
11const HIDDEN_SAVE_DIR_NAME: &str = ".mfa-cli";
12const CONFIG_FILE_NAME: &str = "profile";
14
15#[derive(Debug)]
17pub struct Profile {
18 name: String,
19}
20
21impl fmt::Display for Profile {
22 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
23 write!(f, "{}", self.name)
24 }
25}
26
27impl Profile {
28 pub fn new(name: String) -> Self {
29 Self { name }
30 }
31}
32
33#[derive(Debug, Default)]
34pub struct Mfa {
35 config: config::Config,
36 dump_file: DumpFile,
37}
38
39impl Mfa {
40 pub fn new() -> Result<Self, String> {
41 let mut this = Self {
42 config: Default::default(),
43 dump_file: Default::default(),
44 };
45
46 match this.setup() {
47 Ok(_) => Ok(this),
48 Err(err) => Err(err),
49 }
50 }
51
52 pub fn register_profile(&mut self, account_name: &str, secret: &str) -> Result<(), String> {
54 match self.config.new_profile(account_name, secret) {
55 Ok(_) => Ok(()),
56 Err(err) => Err(err.to_string()),
57 }
58 }
59
60 pub fn list_profiles(&self) -> Vec<Profile> {
62 self.config
63 .get_profiles()
64 .iter()
65 .map(|profile| Profile::new(profile.get_name().to_string()))
66 .collect()
67 }
68
69 pub fn remove_profile(&mut self, profile_name: &str) -> Result<(), String> {
70 self.config.remove_profile(profile_name)
71 }
72
73 pub fn get_secret_by_name(&self, profile_name: &str) -> Option<Vec<u8>> {
75 self.config.get_secret_by_name(profile_name)
76 }
77
78 pub fn get_code_by_name(&self, profile_name: &str) -> Result<String, String> {
80 match self.get_secret_by_name(profile_name) {
81 Some(secret) => totp::totp(secret.as_ref()),
82 None => Err(format!(
83 "can't get the secret that profile: {}",
84 profile_name
85 )),
86 }
87 }
88
89 pub fn dump(&self) -> Result<(), String> {
91 let config_data = match self.config.serialize() {
92 Ok(data) => data,
93 Err(err) => return Err(err),
94 };
95
96 let mut file = match File::create(self.dump_file.path()) {
97 Ok(file) => file,
98 Err(err) => return Err(err.to_string()),
99 };
100 match file.write_all(config_data.as_bytes()) {
101 Ok(()) => Ok(()),
102 Err(err) => Err(err.to_string()),
103 }
104 }
105
106 pub fn restore(&mut self) -> Result<(), String> {
108 let mut file = match File::open(self.dump_file.path()) {
109 Ok(file) => file,
110 Err(err) => return Err(err.to_string()),
111 };
112 let mut contents = String::new();
113 if let Err(err) = file.read_to_string(&mut contents) {
114 return Err(err.to_string());
115 };
116
117 self.config.deserialize(&contents)
118 }
119
120 fn setup(&mut self) -> Result<(), String> {
125 if !self.dump_file.dir_exists() {
127 if let Err(err) = DirBuilder::new()
128 .recursive(true)
129 .create(self.dump_file.dir_path())
130 {
131 return Err(err.to_string());
132 }
133 }
134
135 if !self.dump_file.exists() {
137 return Ok(());
138 }
139
140 if self.dump_file.check() {
141 return self.restore();
142 }
143
144 Ok(())
145 }
146}
147
148#[derive(Debug)]
149struct DumpFile {
150 dir: Box<Path>,
151 file_name: &'static str,
152}
153
154impl Default for DumpFile {
155 fn default() -> Self {
156 let path = fetch_dump_path().to_path_buf();
157
158 Self {
159 dir: path.into_boxed_path(),
160 file_name: CONFIG_FILE_NAME,
161 }
162 }
163}
164
165impl DumpFile {
166 fn exists(&self) -> bool {
168 self.path().exists()
169 }
170
171 fn dir_exists(&self) -> bool {
172 self.dir_path().exists()
173 }
174
175 fn check(&self) -> bool {
180 if !self.exists() {
181 return false;
182 }
183
184 let meta = match self.path().metadata() {
185 Ok(meta) => meta,
186 Err(_) => return false,
187 };
188
189 meta.is_file() && 0 < meta.len()
190 }
191
192 fn path(&self) -> Box<Path> {
193 let mut path = self.dir.to_path_buf();
194 path.push(self.file_name);
195 path.into_boxed_path()
196 }
197
198 fn dir_path(&self) -> &Path {
199 &self.dir
200 }
201}
202
203fn fetch_dump_path() -> Box<Path> {
205 if let Some(path) = env_my_home() {
206 let mut path = Path::new(&path).to_path_buf();
207 path.push(SAVE_DIR_NAME);
208 return path.into_boxed_path();
209 }
210
211 if let Some(path) = env_xdg_config_home() {
212 let mut path = Path::new(&path).to_path_buf();
213 path.push(SAVE_DIR_NAME);
214 return path.into_boxed_path();
215 }
216
217 if let Some(path) = env_home() {
218 let mut path = Path::new(&path).to_path_buf();
219 path.push(HIDDEN_SAVE_DIR_NAME);
220 return path.into_boxed_path();
221 }
222
223 if let Ok(mut path) = env::current_dir() {
224 path.push(HIDDEN_SAVE_DIR_NAME);
225 return path.into_boxed_path();
226 }
227
228 panic!("can't find save directory");
229}
230
231fn env_my_home() -> Option<String> {
232 match env::var("MFA_CLI_CONFIG_HOME") {
233 Ok(path) if Path::new(&path).exists() => Some(path),
234 Ok(path) if !Path::new(&path).exists() => {
235 DirBuilder::new().recursive(true).create(&path).unwrap();
236 Some(path)
237 }
238 _ => None,
239 }
240}
241
242fn env_xdg_config_home() -> Option<String> {
243 match env::var("XDG_CONFIG_HOME") {
244 Ok(path) if Path::new(&path).exists() => Some(path),
245 _ => None,
246 }
247}
248
249fn env_home() -> Option<String> {
250 match env::var("HOME") {
251 Ok(path) if Path::new(&path).exists() => Some(path),
252 _ => None,
253 }
254}
255
256#[cfg(test)]
257mod tests {
258 use super::*;
259 use tempfile;
260
261 #[test]
262 fn dump_file_path() {
263 let dump_file = DumpFile {
264 dir: Path::new("/path/to").to_path_buf().into_boxed_path(),
265 file_name: "file",
266 };
267
268 let path = dump_file.path();
269
270 assert_eq!(*path.as_ref(), *Path::new("/path/to/file"));
271 }
272
273 #[test]
274 #[ignore]
275 fn setup_config_dir_for_for_xdg_config_home() {
276 let config_home = tempfile::tempdir().unwrap();
277 env::set_var("MFA_CLI_CONFIG_HOME", config_home.path().to_str().unwrap());
278
279 let _ = Mfa::new();
280 assert!(config_home.path().join("mfa-cli").is_dir());
281 }
282
283 #[test]
284 #[ignore]
285 fn setup_config_dir_for_current_dir() {
286 let pwd = env::current_dir().unwrap();
287 let config_home = tempfile::tempdir().unwrap();
288
289 env::remove_var("MFA_CLI_CONFIG_HOME");
290 env::remove_var("XDG_CONFIG_HOME");
291 env::remove_var("HOME");
292 let _ = env::set_current_dir(config_home.path());
293
294 let _ = Mfa::new();
295 assert!(config_home.path().join(".mfa-cli").is_dir());
296
297 let _ = env::set_current_dir(pwd);
298 }
299
300 #[test]
301 fn fetch_dump_path_from_env_my_home_when_that_exists() {
302 let current_dir = env::current_dir().unwrap();
303 let expected = current_dir.join("tests/tmp/mfa-cli");
304 env::set_var("MFA_CLI_CONFIG_HOME", current_dir.join("tests/tmp"));
305
306 assert_eq!(*fetch_dump_path(), *expected);
307 }
308
309 #[test]
310 fn fetch_dump_path_from_env_my_home_when_that_does_not_exist() {
311 let current_dir = env::current_dir().unwrap();
312
313 let expected = current_dir.join("tests/tmp/does_not_exist/mfa-cli");
314 let config_home_path = current_dir.join("tests/tmp/does_not_exist");
315 env::set_var("MFA_CLI_CONFIG_HOME", config_home_path.clone());
316
317 assert_eq!(*fetch_dump_path(), *expected);
318 std::fs::remove_dir(config_home_path).unwrap();
319 }
320
321 #[test]
324 #[ignore]
325 fn fetch_dump_path_from_env_xdg_config_home() {
326 env::remove_var("MFA_CLI_CONFIG_HOME");
327 env::set_var("XDG_CONFIG_HOME", "./tests/tmp");
328 assert_eq!(*fetch_dump_path(), *Path::new("./tests/tmp/mfa-cli"));
329 }
330
331 #[test]
332 fn fetch_dump_path_from_env_home() {
333 env::remove_var("MFA_CLI_CONFIG_HOME");
334 env::remove_var("XDG_CONFIG_HOME");
335
336 env::set_var("HOME", "./tests/tmp");
337 assert_eq!(*fetch_dump_path(), *Path::new("./tests/tmp/.mfa-cli"));
338 }
339
340 #[test]
343 #[ignore]
344 fn fetch_dump_path_from_current_dir() {
345 env::remove_var("MFA_CLI_CONFIG_HOME");
346 env::remove_var("XDG_CONFIG_HOME");
347 env::remove_var("HOME");
348
349 let expected = env::current_dir().unwrap().join(".mfa-cli");
350 assert_eq!(*fetch_dump_path(), expected);
351 }
352
353 #[test]
354 fn setup_when_there_is_empty_config_file() {
355 env::set_var("MFA_CLI_CONFIG_HOME", "tests/tmp/");
356 std::fs::create_dir_all("tests/tmp/mfa-cli/").unwrap();
357 File::create("tests/tmp/mfa-cli/profile").unwrap();
358
359 assert!(Mfa::new().is_ok());
360
361 std::fs::remove_file("tests/tmp/mfa-cli/profile").unwrap()
362 }
363
364 #[test]
365 fn test_remove_profile() {
366 let mut mfa: Mfa = Default::default();
367 mfa.config.new_profile("test", "hoge").unwrap();
368
369 mfa.remove_profile("test").unwrap();
370 assert!(mfa.get_secret_by_name("test").is_none());
371 }
372
373 #[test]
374 fn test_list_profiles() {
375 let mut mfa: Mfa = Default::default();
376 mfa.config.new_profile("test1", "hoge").unwrap();
377 mfa.config.new_profile("test2", "hoge").unwrap();
378
379 let profiles = mfa.list_profiles();
380 assert_eq!(profiles.get(0).unwrap().name, "test1");
381 assert_eq!(profiles.get(1).unwrap().name, "test2");
382 assert!(profiles.get(2).is_none());
383 }
384}