diff options
| author | bryan newbold <bnewbold@robocracy.org> | 2023-02-19 19:38:43 -0800 | 
|---|---|---|
| committer | bryan newbold <bnewbold@robocracy.org> | 2023-02-19 19:38:43 -0800 | 
| commit | 991bef89ba6b4adc8e973459e5c3a1deef810622 (patch) | |
| tree | 02faee432fe48c934afa10401428dddb9128db36 | |
| parent | 7d479d0871e6b4e2edaeff433e37f5e7c3b614fc (diff) | |
| download | adenosine-991bef89ba6b4adc8e973459e5c3a1deef810622.tar.gz adenosine-991bef89ba6b4adc8e973459e5c3a1deef810622.zip  | |
cli: crude login-via-user-pass flow
| -rw-r--r-- | adenosine-cli/src/bin/adenosine.rs | 213 | ||||
| -rw-r--r-- | adenosine/src/xrpc.rs | 65 | 
2 files changed, 196 insertions, 82 deletions
diff --git a/adenosine-cli/src/bin/adenosine.rs b/adenosine-cli/src/bin/adenosine.rs index 86b43eb..f3429b6 100644 --- a/adenosine-cli/src/bin/adenosine.rs +++ b/adenosine-cli/src/bin/adenosine.rs @@ -38,6 +38,24 @@ struct Opt {      )]      auth_token: Option<String>, +    /// Authentication handle (username), for commands that need it +    #[structopt( +        global = true, +        long = "--auth-handle", +        env = "ATP_AUTH_HANDLE", +        hide_env_values = true +    )] +    auth_handle: Option<String>, + +    /// Authentication password, for commands that need it +    #[structopt( +        global = true, +        long = "--auth-password", +        env = "ATP_AUTH_PASSWORD", +        hide_env_values = true +    )] +    auth_password: Option<String>, +      /// Log more messages. Pass multiple times for ever more verbosity      ///      /// By default, it'll only report errors. Passing `-v` one time also prints @@ -280,8 +298,28 @@ fn print_result_json(result: Option<Value>) -> Result<()> {      Ok(())  } +/// Helper for endpoints that require authentication. +/// +/// If an author token already exists, use it to refresh the session. If no auth token is provided, +/// tries using handle/password to login, resulting in a session auth token. +/// +/// Returns DID passed from session token if auth was successful, otherwise an Error. +fn require_auth_did(opt: &Opt, xrpc_client: &mut XrpcClient) -> Result<Did> { +    if opt.auth_token.is_some() { +        // TODO: currently clobbers session +        //xrpc_client.auth_refresh()?; +    } else if let (Some(handle), Some(passwd)) = (&opt.auth_handle, &opt.auth_password) { +        xrpc_client.auth_login(handle, passwd)?; +    } else { +        return Err(anyhow!( +            "command requires auth, but have neither token orhandle/password" +        )); +    } +    xrpc_client.auth_did() +} +  fn run(opt: Opt) -> Result<()> { -    let xrpc_client = XrpcClient::new(opt.atp_host.clone(), opt.auth_token.clone())?; +    let mut xrpc_client = XrpcClient::new(opt.atp_host.clone(), opt.auth_token.clone())?;      let mut params: HashMap<String, String> = HashMap::new();      let jwt_did: Option<String> = if let Some(ref token) = opt.auth_token {          Some(parse_did_from_jwt(token)?) @@ -366,9 +404,12 @@ fn run(opt: Opt) -> Result<()> {              }              None          } -        Command::Create { collection, fields } => { -            let did = jwt_did.ok_or(anyhow!("need auth token"))?; -            let val = value_from_fields(fields); +        Command::Create { +            ref collection, +            ref fields, +        } => { +            let did = require_auth_did(&opt, &mut xrpc_client)?; +            let val = value_from_fields(fields.clone());              xrpc_client.post(                  &Nsid::from_str("com.atproto.repo.createRecord")?,                  None, @@ -380,10 +421,17 @@ fn run(opt: Opt) -> Result<()> {                  })),              )?          } -        Command::Update { uri, fields } => { +        Command::Update { +            ref uri, +            ref fields, +        } => { +            require_auth_did(&opt, &mut xrpc_client)?;              let did = uri.repository.to_string(); -            let collection = uri.collection.ok_or(anyhow!("collection required"))?; -            let rkey = uri.record.ok_or(anyhow!("record key required"))?; +            let collection = uri +                .collection +                .clone() +                .ok_or(anyhow!("collection required"))?; +            let rkey = uri.record.clone().ok_or(anyhow!("record key required"))?;              params.insert("did".to_string(), did.clone());              params.insert("collection".to_string(), collection.clone());              params.insert("rkey".to_string(), rkey.clone()); @@ -391,7 +439,7 @@ fn run(opt: Opt) -> Result<()> {              let mut record = xrpc_client                  .get(&Nsid::from_str("com.atproto.repo.getRecord")?, Some(params))?                  .unwrap_or(json!({})); -            update_value_from_fields(fields, &mut record); +            update_value_from_fields(fields.clone(), &mut record);              xrpc_client.post(                  &Nsid::from_str("com.atproto.repo.putRecord")?,                  None, @@ -403,10 +451,14 @@ fn run(opt: Opt) -> Result<()> {                  })),              )?          } -        Command::Delete { uri } => { +        Command::Delete { ref uri } => { +            require_auth_did(&opt, &mut xrpc_client)?;              let did = uri.repository.to_string(); -            let collection = uri.collection.ok_or(anyhow!("collection required"))?; -            let rkey = uri.record.ok_or(anyhow!("record key required"))?; +            let collection = uri +                .collection +                .clone() +                .ok_or(anyhow!("collection required"))?; +            let rkey = uri.record.clone().ok_or(anyhow!("record key required"))?;              xrpc_client.post(                  &Nsid::from_str("com.atproto.repo.deleteRecord")?,                  None, @@ -418,15 +470,18 @@ fn run(opt: Opt) -> Result<()> {              )?          }          Command::Xrpc { -            method, -            nsid, -            fields, +            ref method, +            ref nsid, +            ref fields,          } => {              update_params_from_fields(&fields, &mut params); -            let body = value_from_fields(fields); +            let body = value_from_fields(fields.clone());              match method {                  XrpcMethod::Get => xrpc_client.get(&nsid, Some(params))?, -                XrpcMethod::Post => xrpc_client.post(&nsid, Some(params), Some(body))?, +                XrpcMethod::Post => { +                    require_auth_did(&opt, &mut xrpc_client)?; +                    xrpc_client.post(&nsid, Some(params), Some(body))? +                }              }          }          Command::Account { @@ -538,66 +593,84 @@ fn run(opt: Opt) -> Result<()> {          }          Command::Bsky {              cmd: BskyCommand::Timeline, -        } => xrpc_client.get(&Nsid::from_str("app.bsky.feed.getTimeline")?, None)?, +        } => { +            require_auth_did(&opt, &mut xrpc_client)?; +            xrpc_client.get(&Nsid::from_str("app.bsky.feed.getTimeline")?, None)? +        }          Command::Bsky {              cmd: BskyCommand::Notifications, -        } => xrpc_client.get(&Nsid::from_str("app.bsky.notifications.get")?, None)?, +        } => { +            require_auth_did(&opt, &mut xrpc_client)?; +            xrpc_client.get(&Nsid::from_str("app.bsky.notifications.get")?, None)? +        }          Command::Bsky { -            cmd: BskyCommand::Post { text }, -        } => xrpc_client.post( -            &Nsid::from_str("com.atproto.repo.createRecord")?, -            None, -            Some(json!({ -                "did": jwt_did.ok_or(anyhow!("need auth token"))?, -                "collection": "app.bsky.feed.post", -                "record": { -                    "text": text, -                    "createdAt": created_at_now(), -                }, -            })), -        )?, +            cmd: BskyCommand::Post { ref text }, +        } => { +            let did = require_auth_did(&opt, &mut xrpc_client)?; +            xrpc_client.post( +                &Nsid::from_str("com.atproto.repo.createRecord")?, +                None, +                Some(json!({ +                    "did": did, +                    "collection": "app.bsky.feed.post", +                    "record": { +                        "text": text, +                        "createdAt": created_at_now(), +                    }, +                })), +            )? +        }          Command::Bsky { -            cmd: BskyCommand::Repost { uri }, -        } => xrpc_client.post( -            &Nsid::from_str("com.atproto.repo.createRecord")?, -            None, -            Some(json!({ -                "did": jwt_did.ok_or(anyhow!("need auth token"))?, -                "collection": "app.bsky.feed.repost", -                "record": { -                    "subject": uri.to_string(), -                    "createdAt": created_at_now(), -                } -            })), -        )?, +            cmd: BskyCommand::Repost { ref uri }, +        } => { +            require_auth_did(&opt, &mut xrpc_client)?; +            xrpc_client.post( +                &Nsid::from_str("com.atproto.repo.createRecord")?, +                None, +                Some(json!({ +                    "did": jwt_did.ok_or(anyhow!("need auth token"))?, +                    "collection": "app.bsky.feed.repost", +                    "record": { +                        "subject": uri.to_string(), +                        "createdAt": created_at_now(), +                    } +                })), +            )? +        }          Command::Bsky { -            cmd: BskyCommand::Like { uri }, -        } => xrpc_client.post( -            &Nsid::from_str("com.atproto.repo.createRecord")?, -            None, -            Some(json!({ -                "did": jwt_did.ok_or(anyhow!("need auth token"))?, -                "collection": "app.bsky.feed.like", -                "record": { -                    "subject": { "uri": uri.to_string(), "cid": "TODO" }, -                    "createdAt": created_at_now(), -                }, -            })), -        )?, +            cmd: BskyCommand::Like { ref uri }, +        } => { +            require_auth_did(&opt, &mut xrpc_client)?; +            xrpc_client.post( +                &Nsid::from_str("com.atproto.repo.createRecord")?, +                None, +                Some(json!({ +                    "did": jwt_did.ok_or(anyhow!("need auth token"))?, +                    "collection": "app.bsky.feed.like", +                    "record": { +                        "subject": { "uri": uri.to_string(), "cid": "TODO" }, +                        "createdAt": created_at_now(), +                    }, +                })), +            )? +        }          Command::Bsky { -            cmd: BskyCommand::Follow { uri }, -        } => xrpc_client.post( -            &Nsid::from_str("com.atproto.repo.createRecord")?, -            None, -            Some(json!({ -                "did": jwt_did.ok_or(anyhow!("need auth token"))?, -                "collection": "app.bsky.graph.follow", -                "record": { -                    "subject": { "did": uri.to_string() }, -                    "createdAt": created_at_now(), -                } -            })), -        )?, +            cmd: BskyCommand::Follow { ref uri }, +        } => { +            require_auth_did(&opt, &mut xrpc_client)?; +            xrpc_client.post( +                &Nsid::from_str("com.atproto.repo.createRecord")?, +                None, +                Some(json!({ +                    "did": jwt_did.ok_or(anyhow!("need auth token"))?, +                    "collection": "app.bsky.graph.follow", +                    "record": { +                        "subject": { "did": uri.to_string() }, +                        "createdAt": created_at_now(), +                    } +                })), +            )? +        }          Command::Bsky {              cmd: BskyCommand::Profile { name },          } => { diff --git a/adenosine/src/xrpc.rs b/adenosine/src/xrpc.rs index 97caa4d..d958d5e 100644 --- a/adenosine/src/xrpc.rs +++ b/adenosine/src/xrpc.rs @@ -1,11 +1,12 @@ -use crate::identifiers::Nsid; +use crate::identifiers::{Did, Nsid}; +use crate::auth::parse_did_from_jwt;  use anyhow::anyhow;  pub use anyhow::Result;  use reqwest::header; -use serde_json::Value;  use std::collections::HashMap;  use std::str::FromStr;  use std::time::Duration; +use serde_json::{json, Value};  static APP_USER_AGENT: &str = concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION"),); @@ -30,26 +31,64 @@ impl FromStr for XrpcMethod {  pub struct XrpcClient {      http_client: reqwest::blocking::Client,      host: String, +    auth_token: Option<String>, +    refresh_token: Option<String>,  }  impl XrpcClient {      pub fn new(host: String, auth_token: Option<String>) -> Result<Self> { -        let mut headers = header::HeaderMap::new(); -        if let Some(token) = &auth_token { -            let mut auth_value = header::HeaderValue::from_str(&format!("Bearer {token}"))?; -            auth_value.set_sensitive(true); -            headers.insert(header::AUTHORIZATION, auth_value); -        };          let http_client = reqwest::blocking::Client::builder() -            .default_headers(headers)              .user_agent(APP_USER_AGENT)              .timeout(Duration::from_secs(30))              //.danger_accept_invalid_certs(true)              .build()              .expect("ERROR :: Could not build reqwest client"); -        Ok(XrpcClient { http_client, host }) +        Ok(XrpcClient { http_client, host, auth_token: auth_token.clone(), refresh_token: auth_token }) +    } + +    fn auth_headers(&self) -> reqwest::header::HeaderMap { +        let mut headers = header::HeaderMap::new(); +        if let Some(token) = &self.auth_token { +            let mut auth_value = header::HeaderValue::from_str(&format!("Bearer {token}")).expect("header formatting"); +            auth_value.set_sensitive(true); +            headers.insert(header::AUTHORIZATION, auth_value); +        }; +        headers +    } + +    /// Creates a new session, and updates current client auth tokens with the result +    pub fn auth_login(&mut self, handle: &str, password: &str) -> Result <()> { +        let resp = self.post( +            &Nsid::from_str("com.atproto.session.create")?, +            None, +            Some(json!({ +                "handle": handle, +                "password": password, +            })))?; +        let resp = resp.ok_or(anyhow!("missing session auth info"))?; +        self.auth_token = resp["accessJwt"].as_str().map(|s| s.to_string()); +        self.refresh_token = resp["refreshJwt"].as_str().map(|s| s.to_string()); +        Ok(()) +    } + +    /// Uses refresh token to update auth token +    pub fn auth_refresh(&mut self) -> Result<()> { +        self.auth_token = self.refresh_token.clone(); +        let resp = self.post(&Nsid::from_str("com.atproto.session.refresh")?, None, None)?; +        let resp = resp.ok_or(anyhow!("missing session auth info"))?; +        self.auth_token = resp["accessJwt"].as_str().map(|s| s.to_string()); +        self.refresh_token = resp["refreshJwt"].as_str().map(|s| s.to_string()); +        Ok(()) +    } + +    pub fn auth_did(&self) -> Result<Did> { +        if let Some(token) = &self.auth_token { +            return Did::from_str(&parse_did_from_jwt(&token)?) +        } else { +            Err(anyhow!("no auth token configured")) +        }      }      pub fn get( @@ -62,6 +101,7 @@ impl XrpcClient {          let res = self              .http_client              .get(format!("{}/xrpc/{nsid}", self.host)) +            .headers(self.auth_headers())              .query(¶ms)              .send()?;          // TODO: refactor this error handling stuff into single method @@ -92,6 +132,7 @@ impl XrpcClient {          let res = self              .http_client              .get(format!("{}/xrpc/{}", self.host, nsid)) +            .headers(self.auth_headers())              .query(¶ms)              .send()?;          if res.status() == 400 { @@ -127,6 +168,7 @@ impl XrpcClient {          let mut req = self              .http_client              .post(format!("{}/xrpc/{}", self.host, nsid)) +            .headers(self.auth_headers())              .query(¶ms);          req = if let Some(b) = body {              req.json(&b) @@ -167,6 +209,7 @@ impl XrpcClient {          let res = self              .http_client              .post(format!("{}/xrpc/{}", self.host, nsid)) +            .headers(self.auth_headers())              .query(¶ms)              .header(reqwest::header::CONTENT_TYPE, "application/cbor")              .body(buf) @@ -181,6 +224,4 @@ impl XrpcClient {          let res = res.error_for_status()?;          Ok(res.json()?)      } - -    //  reqwest::blocking::Body  }  | 
