OXYGENHUB
# 16ee5d19d220 oxygenhub / crates / oh / src / client.rs
4.2 KiB · Rust · last changed Agent grants: agents act for their owner with no key (keyless step 5) 18 days ago source blame history raw
1
//! The hub's JSON API over HTTPS, with the checkout's CA when the hub's
2
//! certificate is self-signed.
3
4
use crate::target::Target;
5
use anyhow::{bail, Context, Result};
6
use serde::de::DeserializeOwned;
7
use serde::Serialize;
8
use std::time::Duration;
9
10
pub struct Client {
11
    agent: ureq::Agent,
12
    base: String,
13
    token: Option<String>,
14
}
15
16
impl Client {
17
    pub fn new(t: &Target) -> Result<Client> {
18
        let tls = oxy_core::protocol::tls_client_config(t.ca.as_deref()).map_err(|e| anyhow::anyhow!("{e}"))?;
19
        let agent = ureq::AgentBuilder::new().tls_config(tls).timeout(Duration::from_secs(120)).build();
20
        Ok(Client { agent, base: format!("{}/api/v1", t.hub), token: t.token.clone() })
21
    }
22
23
    fn req(&self, method: &str, path: &str) -> ureq::Request {
24
        let r = self.agent.request(method, &format!("{}{path}", self.base)).set("Accept", "application/json");
25
        match &self.token {
26
            Some(t) => r.set("Authorization", &format!("Bearer {t}")),
27
            None => r,
28
        }
29
    }
30
31
    fn finish(res: Result<ureq::Response, ureq::Error>, what: &str) -> Result<String> {
32
        match res {
33
            Ok(r) => Ok(r.into_string()?),
34
            Err(ureq::Error::Status(code, r)) => {
35
                let body = r.into_string().unwrap_or_default();
36
                // Problem documents (RFC 7807) carry the reason in `detail`.
37
                let detail = serde_json::from_str::<serde_json::Value>(&body)
38
                    .ok()
39
                    .and_then(|v| v["detail"].as_str().map(String::from))
40
                    .unwrap_or(body);
41
                let hint = match code {
42
                    401 => " (sign in: `oh auth login`, or push with a token first)",
43
                    403 => " (your token lacks the scope or the role for this)",
44
                    _ => "",
45
                };
46
                bail!("{what}: HTTP {code}: {}{hint}", detail.trim())
47
            }
48
            Err(e) => Err(e).context(what.to_string()),
49
        }
50
    }
51
52
    /// GET, as raw JSON text.
53
    pub fn get_text(&self, path: &str) -> Result<String> {
54
        Self::finish(self.req("GET", path).call(), &format!("GET {path}"))
55
    }
56
57
    pub fn get<T: DeserializeOwned>(&self, path: &str) -> Result<T> {
58
        let text = self.get_text(path)?;
59
        serde_json::from_str(&text).with_context(|| format!("GET {path}: unexpected answer"))
60
    }
61
62
    pub fn send_text(&self, method: &str, path: &str, body: &impl Serialize) -> Result<String> {
63
        let json = serde_json::to_string(body)?;
64
        Self::finish(self.req(method, path).set("Content-Type", "application/json").send_string(&json), &format!("{method} {path}"))
65
    }
66
67
    pub fn send<T: DeserializeOwned>(&self, method: &str, path: &str, body: &impl Serialize) -> Result<T> {
68
        let text = self.send_text(method, path, body)?;
69
        serde_json::from_str(&text).with_context(|| format!("{method} {path}: unexpected answer"))
70
    }
71
72
    /// POST JSON, returning the status and body whatever the status (the
73
    /// device sign-in's answers while it waits are errors by design).
74
    pub fn post_raw(&self, path: &str, body: &impl Serialize) -> Result<(u16, String)> {
75
        let json = serde_json::to_string(body)?;
76
        match self.req("POST", path).set("Content-Type", "application/json").send_string(&json) {
77
            Ok(r) => Ok((r.status(), r.into_string()?)),
78
            Err(ureq::Error::Status(code, r)) => Ok((code, r.into_string().unwrap_or_default())),
79
            Err(e) => Err(e).context(format!("POST {path}")),
80
        }
81
    }
82
83
    /// GET a path at the hub's root, outside the API (`/.well-known/…`).
84
    pub fn get_root_bytes(&self, path: &str) -> Result<Vec<u8>> {
85
        let hub = self.base.trim_end_matches("/api/v1");
86
        let r = self.agent.get(&format!("{hub}{path}")).call().with_context(|| format!("GET {path}"))?;
87
        let mut out = Vec::new();
88
        std::io::Read::read_to_end(&mut r.into_reader(), &mut out)?;
89
        Ok(out)
90
    }
91
92
    /// POST raw bytes (a release asset).
93
    pub fn upload(&self, path: &str, bytes: &[u8]) -> Result<String> {
94
        Self::finish(
95
            self.req("POST", path).set("Content-Type", "application/octet-stream").timeout(Duration::from_secs(1800)).send_bytes(bytes),
96
            &format!("POST {path}"),
97
        )
98
    }
99
}