| 1 |
|
| 2 |
|
| 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 |
|
| 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 |
|
| 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 |
|
| 73 |
|
| 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 |
|
| 84 |
pub fn get_bytes(&self, path: &str) -> Result<Vec<u8>> { |
| 85 |
match self.req("GET", path).call() { |
| 86 |
Ok(r) => { |
| 87 |
let mut out = Vec::new(); |
| 88 |
std::io::Read::read_to_end(&mut r.into_reader(), &mut out)?; |
| 89 |
Ok(out) |
| 90 |
} |
| 91 |
Err(e) => Self::finish(Err(e), &format!("GET {path}")).map(String::into_bytes), |
| 92 |
} |
| 93 |
} |
| 94 |
|
| 95 |
|
| 96 |
pub fn get_root_bytes(&self, path: &str) -> Result<Vec<u8>> { |
| 97 |
let hub = self.base.trim_end_matches("/api/v1"); |
| 98 |
let r = self.agent.get(&format!("{hub}{path}")).call().with_context(|| format!("GET {path}"))?; |
| 99 |
let mut out = Vec::new(); |
| 100 |
std::io::Read::read_to_end(&mut r.into_reader(), &mut out)?; |
| 101 |
Ok(out) |
| 102 |
} |
| 103 |
|
| 104 |
|
| 105 |
pub fn upload(&self, path: &str, bytes: &[u8]) -> Result<String> { |
| 106 |
Self::finish( |
| 107 |
self.req("POST", path).set("Content-Type", "application/octet-stream").timeout(Duration::from_secs(1800)).send_bytes(bytes), |
| 108 |
&format!("POST {path}"), |
| 109 |
) |
| 110 |
} |
| 111 |
} |