1pub mod error;
2pub mod output;
3mod tests;
4use chrono::{prelude::*, Duration};
5use error::Error;
6
7use std::process::Stdio;
8use tokio::io::AsyncWriteExt;
9use tokio::process::Command;
10
11use crate::sealed::{FirstCmd, SecondCmd};
12
13pub type Result<T> = std::result::Result<T, Error>;
14
15#[derive(Clone)]
18pub struct OpCLI {
19 expiration_time: DateTime<Utc>,
20 session: String,
21}
22
23impl OpCLI {
24 #[inline]
25 pub async fn new_with_pass(username: &str, password: &str) -> Result<Self> {
26 let mut child = Command::new("op")
27 .arg("signin")
28 .arg(username)
29 .arg("--raw")
30 .stdin(Stdio::piped())
31 .stderr(Stdio::piped())
32 .stdout(Stdio::piped())
33 .spawn()?;
34 let stdin = child.stdin.as_mut().unwrap();
35 stdin.write_all(password.as_bytes()).await?;
36 let output = child.wait_with_output().await?;
37 handle_op_signin_error(String::from_utf8_lossy(&output.stderr).to_string()).await?;
38 let expiration_time = Utc::now() + Duration::minutes(29);
39 Ok(Self {
40 expiration_time,
41 session: String::from_utf8_lossy(&output.stdout).to_string(),
42 })
43 }
44
45 pub fn get(&self) -> GetCmd {
46 GetCmd {
47 cmd: "get".to_string(),
48 session: self.session.to_string(),
49 }
50 }
51
52 pub fn create(&self) -> CreateCmd {
53 CreateCmd {
54 cmd: "create".to_string(),
55 session: self.session.to_string(),
56 }
57 }
58
59 #[inline]
60 pub fn list(&self) -> ListCmd {
61 ListCmd {
62 cmd: "list".to_string(),
63 session: self.session.to_string(),
64 }
65 }
66
67 #[inline]
68 pub fn delete(&self) -> DeleteCmd {
69 DeleteCmd {
70 cmd: "delete".to_string(),
71 session: self.session.to_string(),
72 }
73 }
74}
75
76#[derive(Debug, Clone)]
77pub struct GetCmd {
78 cmd: String,
79 session: String,
80}
81
82impl sealed::FirstCmd for GetCmd {
83 #[doc(hidden)]
84 fn cmd(&self) -> &str {
85 &self.cmd
86 }
87 #[doc(hidden)]
88 fn session(&self) -> &str {
89 &self.session
90 }
91}
92
93macro_rules! its_first_cmd {
96 ($first_cmd:ident) => {
97 #[derive(Debug, Clone)]
98 pub struct $first_cmd {
99 cmd: String,
100 session: String,
101 }
102
103 impl sealed::FirstCmd for $first_cmd {
104 #[doc(hidden)]
105 fn cmd(&self) -> &str {
106 &self.cmd
107 }
108 #[doc(hidden)]
109 fn session(&self) -> &str {
110 &self.session
111 }
112 }
113 };
114}
115
116its_first_cmd!(CreateCmd);
117its_first_cmd!(ListCmd);
118its_first_cmd!(DeleteCmd);
119
120impl GetCmd {
123 pub fn account(&self) -> AccountCmd {
124 let flags: Vec<String> = Vec::new();
125 AccountCmd {
126 first: self.clone(),
127 cmd: "account".to_string(),
128 flags,
129 }
130 }
131
132 pub fn item_lite(&self, item: &str) -> ItemLiteCmd {
134 let flags: Vec<String> = vec![
135 item.to_string(),
136 "--fields".to_string(),
137 "website,username,password".to_string(),
138 ];
139 ItemLiteCmd {
140 first: self.clone(),
141 cmd: "item".to_string(),
142 flags,
143 }
144 }
145
146 pub fn item(&self, item: &str) -> GetItemCmd {
147 let flags: Vec<String> = vec![item.to_string()];
148 GetItemCmd {
149 first: self.clone(),
150 cmd: "item".to_string(),
151 flags,
152 }
153 }
154
155 pub fn document(&self, doc: &str) -> GetDocumentCmd {
156 let flags: Vec<String> = vec![doc.to_string()];
157 GetDocumentCmd {
158 first: self.clone(),
159 cmd: "document".to_string(),
160 flags,
161 }
162 }
163
164 pub fn totp(&self, item_name: &str) -> GetTotpCmd {
165 let flags: Vec<String> = vec![item_name.to_string()];
166
167 GetTotpCmd {
168 first: self.clone(),
169 cmd: "totp".to_string(),
170 flags,
171 }
172 }
173
174 pub fn user(&self, uuid: &str) -> GetUserCmd {
175 let flags: Vec<String> = vec![uuid.to_string()];
176
177 GetUserCmd {
178 first: self.clone(),
179 cmd: "user".to_string(),
180 flags,
181 }
182 }
183}
184impl CreateCmd {
185 pub fn document(&self, path: &str) -> CreateDocumentCmd {
186 let flags: Vec<String> = vec![path.to_string()];
187 CreateDocumentCmd {
188 first: self.clone(),
189 cmd: "document".to_string(),
190 flags,
191 }
192 }
193}
194
195impl ListCmd {
196 pub fn documents(&self) -> ListDocumentsCmd {
197 let flags: Vec<String> = Vec::new();
198 ListDocumentsCmd {
199 first: self.clone(),
200 cmd: "documents".to_string(),
201 flags,
202 }
203 }
204
205 pub fn items(&self) -> ListItemsCmd {
206 let flags: Vec<String> = Vec::new();
207 ListItemsCmd {
208 first: self.clone(),
209 cmd: "items".to_string(),
210 flags,
211 }
212 }
213
214 pub fn users(&self) -> ListUsersCmd {
215 let flags: Vec<String> = Vec::new();
216 ListUsersCmd {
217 first: self.clone(),
218 cmd: "users".to_string(),
219 flags,
220 }
221 }
222}
223
224impl DeleteCmd {
225 pub fn item(&self) -> DeleteItemCmd {
226 let flags: Vec<String> = Vec::new();
227 DeleteItemCmd {
228 first: self.clone(),
229 cmd: "item".to_string(),
230 flags,
231 }
232 }
233
234 pub fn document(&self, doc: &str) -> DeleteDocumentCmd {
235 let flags: Vec<String> = vec![doc.to_string()];
236 DeleteDocumentCmd {
237 first: self.clone(),
238 cmd: "document".to_string(),
239 flags,
240 }
241 }
242}
243
244#[async_trait::async_trait]
245pub trait SecondCmdExt: SecondCmd {
246 fn add_flag(&mut self, flags: &[&str]) -> &Self {
247 for flag in flags {
248 if !self.flags().contains(&flag.to_string()) {
249 self.flags().push(flag.to_string())
250 }
251 }
252 self
253 }
254
255 async fn run(&self) -> Result<Self::Output> {
256 let mut args: Vec<String> = vec![
257 self.first().cmd().to_string(),
258 self.cmd().to_string(),
259 "--session".to_string(),
260 self.first().session().trim().to_string(),
261 ];
262 if !self.flags().is_empty() {
263 self.flags()
264 .into_iter()
265 .for_each(|flag| args.push(flag.to_string()))
266 }
267 let out_str: &str = &exec_command(args).await?;
268 if out_str.len() == 0 {
269 return Ok(serde_json::from_str("{\"field\":\"ok\"}")?);
270 }
271 Ok(serde_json::from_str(out_str)?)
272 }
273}
274
275impl<T: SecondCmd> SecondCmdExt for T {}
276
277#[derive(Debug)]
278pub struct AccountCmd {
279 first: GetCmd,
280 cmd: String,
281 flags: Vec<String>,
282}
283
284#[async_trait::async_trait]
285impl SecondCmd for AccountCmd {
286 type Output = output::Account;
287 type First = GetCmd;
288
289 #[doc(hidden)]
290 fn first(&self) -> &GetCmd {
291 &self.first
292 }
293
294 #[doc(hidden)]
295 fn cmd(&self) -> &str {
296 &self.cmd
297 }
298
299 #[doc(hidden)]
300 fn flags(&self) -> Vec<String> {
301 self.flags.clone()
302 }
303}
304
305macro_rules! its_second_cmd {
308 ($first_cmd:ident,$second_cmd:ident,$output:ident) => {
309 #[derive(Debug)]
310 pub struct $second_cmd {
311 first: $first_cmd,
312 cmd: String,
313 flags: Vec<String>,
314 }
315
316 #[async_trait::async_trait]
317 impl SecondCmd for $second_cmd {
318 type Output = output::$output;
319 type First = $first_cmd;
320 #[doc(hidden)]
321 fn first(&self) -> &$first_cmd {
322 &self.first
323 }
324 #[doc(hidden)]
325 fn cmd(&self) -> &str {
326 &self.cmd
327 }
328 #[doc(hidden)]
329 fn flags(&self) -> Vec<String> {
330 self.flags.clone()
331 }
332 }
333 };
334}
335
336its_second_cmd!(GetCmd, ItemLiteCmd, ItemLite);
337its_second_cmd!(GetCmd, GetDocumentCmd, Value);
338its_second_cmd!(GetCmd, GetTotpCmd, Value);
339its_second_cmd!(GetCmd, GetItemCmd, GetItem);
340its_second_cmd!(GetCmd, GetUserCmd, GetUser);
341its_second_cmd!(CreateCmd, CreateDocumentCmd, CreateDocument);
342its_second_cmd!(ListCmd, ListDocumentsCmd, ListDocuments);
343its_second_cmd!(ListCmd, ListItemsCmd, ListItems);
344its_second_cmd!(ListCmd, ListUsersCmd, ListUsers);
345its_second_cmd!(DeleteCmd, DeleteItemCmd, DeleteItem);
346its_second_cmd!(DeleteCmd, DeleteDocumentCmd, DeleteDocument);
347
348#[inline]
349async fn exec_command(args: Vec<String>) -> Result<String> {
350 let child = Command::new("op")
351 .args(args)
352 .stdout(Stdio::piped())
353 .stderr(Stdio::piped())
354 .spawn()?;
355 let output = child.wait_with_output().await?;
356 handle_op_exec_error(String::from_utf8_lossy(&output.stderr).to_string()).await?;
357 Ok(String::from_utf8_lossy(&output.stdout).to_string())
358}
359
360#[inline]
361async fn handle_op_signin_error(std_err: String) -> std::result::Result<(), Error> {
362 match std_err.trim() {
363 err if err.contains("401") => Err(Error::OPSignInError("Wrong password".to_string())),
364 err if err.contains("Account not found") => Err(Error::OPSignInError(
365 "Account does not exist,may be you should firstly setup 1password-cli.".to_string(),
366 )),
367 _ => Ok(()),
368 }
369}
370
371#[inline]
372async fn handle_op_exec_error(std_err: String) -> std::result::Result<(), Error> {
373 match std_err.trim() {
374 err if err.contains("doesn't seem to be an item") => {
375 Err(Error::ItemQueryError("Item not founded".to_string()))
376 }
377 err if err.contains("Invalid session token") => {
378 Err(Error::ItemQueryError("In valid session token".to_string()))
379 }
380 err if err.contains("More than one item matches") => Err(Error::ItemQueryError(
381 "More than one item matches,Please specify one by uuid".to_string(),
382 )),
383 _ => Ok(()),
384 }
385}
386
387macro_rules! impl_casting_method {
391 ($($ObjName:ident),* $(,)?) => {
392 $(
393 impl $ObjName {
394
395 pub async fn run(&self) -> Result<<Self as SecondCmd>::Output> {
396 <Self as SecondCmdExt>::run(self).await
397 }
398
399 pub fn add_flag(&mut self, flags: &[&str]) -> &Self {
400 <Self as SecondCmdExt>::add_flag(self, flags)
401 }
402 }
403 )*
404 };
405}
406
407impl_casting_method!(
408 ItemLiteCmd,
409 GetDocumentCmd,
410 GetTotpCmd,
411 GetItemCmd,
412 CreateDocumentCmd,
413 ListDocumentsCmd,
414 ListItemsCmd,
415 AccountCmd
416);
417
418mod sealed {
419 use serde::de::DeserializeOwned;
420
421 pub trait FirstCmd {
422 #[doc(hidden)]
423 fn cmd(&self) -> &str;
424 #[doc(hidden)]
425 fn session(&self) -> &str;
426 }
427
428 #[async_trait::async_trait]
429 pub trait SecondCmd {
430 type Output: DeserializeOwned;
431 type First: FirstCmd + Clone;
432
433 #[doc(hidden)]
434 fn first(&self) -> &Self::First;
435 #[doc(hidden)]
436 fn cmd(&self) -> &str;
437 #[doc(hidden)]
438 fn flags(&self) -> Vec<String>;
439 }
440}