//! The hub's JSON API over HTTPS, with the checkout's CA when the hub's //! certificate is self-signed. use crate::target::Target; use anyhow::{bail, Context, Result}; use serde::de::DeserializeOwned; use serde::Serialize; use std::time::Duration; pub struct Client { agent: ureq::Agent, base: String, token: Option, } impl Client { pub fn new(t: &Target) -> Result { let tls = oxy_core::protocol::tls_client_config(t.ca.as_deref()).map_err(|e| anyhow::anyhow!("{e}"))?; let agent = ureq::AgentBuilder::new().tls_config(tls).timeout(Duration::from_secs(120)).build(); Ok(Client { agent, base: format!("{}/api/v1", t.hub), token: t.token.clone() }) } fn req(&self, method: &str, path: &str) -> ureq::Request { let r = self.agent.request(method, &format!("{}{path}", self.base)).set("Accept", "application/json"); match &self.token { Some(t) => r.set("Authorization", &format!("Bearer {t}")), None => r, } } fn finish(res: Result, what: &str) -> Result { match res { Ok(r) => Ok(r.into_string()?), Err(ureq::Error::Status(code, r)) => { let body = r.into_string().unwrap_or_default(); // Problem documents (RFC 7807) carry the reason in `detail`. let detail = serde_json::from_str::(&body) .ok() .and_then(|v| v["detail"].as_str().map(String::from)) .unwrap_or(body); let hint = match code { 401 => " (sign in: `oh auth login`, or push with a token first)", 403 => " (your token lacks the scope or the role for this)", _ => "", }; bail!("{what}: HTTP {code}: {}{hint}", detail.trim()) } Err(e) => Err(e).context(what.to_string()), } } /// GET, as raw JSON text. pub fn get_text(&self, path: &str) -> Result { Self::finish(self.req("GET", path).call(), &format!("GET {path}")) } pub fn get(&self, path: &str) -> Result { let text = self.get_text(path)?; serde_json::from_str(&text).with_context(|| format!("GET {path}: unexpected answer")) } pub fn send_text(&self, method: &str, path: &str, body: &impl Serialize) -> Result { let json = serde_json::to_string(body)?; Self::finish(self.req(method, path).set("Content-Type", "application/json").send_string(&json), &format!("{method} {path}")) } pub fn send(&self, method: &str, path: &str, body: &impl Serialize) -> Result { let text = self.send_text(method, path, body)?; serde_json::from_str(&text).with_context(|| format!("{method} {path}: unexpected answer")) } /// POST JSON, returning the status and body whatever the status (the /// device sign-in's answers while it waits are errors by design). pub fn post_raw(&self, path: &str, body: &impl Serialize) -> Result<(u16, String)> { let json = serde_json::to_string(body)?; match self.req("POST", path).set("Content-Type", "application/json").send_string(&json) { Ok(r) => Ok((r.status(), r.into_string()?)), Err(ureq::Error::Status(code, r)) => Ok((code, r.into_string().unwrap_or_default())), Err(e) => Err(e).context(format!("POST {path}")), } } /// GET a path at the hub's root, outside the API (`/.well-known/…`). pub fn get_root_bytes(&self, path: &str) -> Result> { let hub = self.base.trim_end_matches("/api/v1"); let r = self.agent.get(&format!("{hub}{path}")).call().with_context(|| format!("GET {path}"))?; let mut out = Vec::new(); std::io::Read::read_to_end(&mut r.into_reader(), &mut out)?; Ok(out) } /// POST raw bytes (a release asset). pub fn upload(&self, path: &str, bytes: &[u8]) -> Result { Self::finish( self.req("POST", path).set("Content-Type", "application/octet-stream").timeout(Duration::from_secs(1800)).send_bytes(bytes), &format!("POST {path}"), ) } }