Skip to main content

kora/
lib.rs

1use std::fmt;
2use std::time::Duration;
3
4use base64::engine::general_purpose::{STANDARD, URL_SAFE_NO_PAD};
5use base64::Engine;
6use chrono::{DateTime, Utc};
7use ed25519_dalek::{Signature, Verifier, VerifyingKey};
8use serde::de::DeserializeOwned;
9use serde::Deserialize;
10use serde_json::{json, Map, Value};
11
12#[derive(Debug)]
13pub enum Error {
14    Http(String),
15    Status(u16, String),
16    Signature,
17    Offline(String),
18    Json(String),
19}
20
21impl fmt::Display for Error {
22    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
23        match self {
24            Error::Http(message) => write!(formatter, "Request to Kora failed: {message}"),
25            Error::Status(status, message) => write!(formatter, "{message} ({status})"),
26            Error::Signature => write!(formatter, "Response signature verification failed"),
27            Error::Offline(message) => write!(formatter, "{message}"),
28            Error::Json(message) => write!(formatter, "Kora sent an unexpected answer: {message}"),
29        }
30    }
31}
32
33impl std::error::Error for Error {}
34
35#[derive(Debug, Deserialize)]
36pub struct Validation {
37    pub valid: bool,
38    pub code: String,
39    pub message: String,
40    pub license: Option<Value>,
41    pub timestamp: i64,
42}
43
44#[derive(Debug, Deserialize)]
45pub struct Deactivation {
46    pub success: bool,
47    pub code: String,
48    pub message: String,
49    pub removed: i64,
50    pub timestamp: i64,
51}
52
53pub struct Client {
54    base: String,
55    api_key: Option<String>,
56    product: Option<String>,
57    public_key: Option<String>,
58    agent: ureq::Agent,
59}
60
61pub struct Reply {
62    pub status: u16,
63    pub text: String,
64    pub signature: Option<String>,
65}
66
67impl Client {
68    pub fn new(url: &str) -> Self {
69        let trimmed = url.trim_end_matches('/');
70        Client {
71            base: match trimmed.strip_suffix("/api/v1") {
72                Some(base) => base.to_string(),
73                None => trimmed.to_string(),
74            },
75            api_key: None,
76            product: None,
77            public_key: None,
78            agent: ureq::AgentBuilder::new().timeout(Duration::from_secs(15)).build(),
79        }
80    }
81
82    pub fn api_key(mut self, key: impl Into<String>) -> Self {
83        self.api_key = Some(key.into());
84        self
85    }
86
87    pub fn product(mut self, product: impl Into<String>) -> Self {
88        self.product = Some(product.into());
89        self
90    }
91
92    pub fn public_key(mut self, key: impl Into<String>) -> Self {
93        self.public_key = Some(key.into());
94        self
95    }
96
97    pub fn licenses(&self) -> Resource<'_> {
98        Resource { client: self, name: "licenses", single: "license" }
99    }
100
101    pub fn products(&self) -> Resource<'_> {
102        Resource { client: self, name: "products", single: "product" }
103    }
104
105    pub fn customers(&self) -> Resource<'_> {
106        Resource { client: self, name: "customers", single: "customer" }
107    }
108
109    pub fn send(&self, method: &str, path: &str, query: &[(&str, &str)], body: Option<Value>) -> Result<Reply, Error> {
110        let mut request = self.agent.request(method, &format!("{}/api/v1{}", self.base, path)).set("Accept", "application/json");
111        if let Some(key) = &self.api_key {
112            request = request.set("Authorization", &format!("Bearer {key}"));
113        }
114        for (name, value) in query {
115            request = request.query(name, value);
116        }
117        let result = match body {
118            Some(value) => request.send_json(value),
119            None => request.call(),
120        };
121        let response = match result {
122            Ok(response) => response,
123            Err(ureq::Error::Status(_, response)) => response,
124            Err(error) => return Err(Error::Http(error.to_string())),
125        };
126        let status = response.status();
127        let signature = response.header("x-kora-signature").map(str::to_string);
128        let text = response.into_string().map_err(|error| Error::Http(error.to_string()))?;
129        Ok(Reply { status, text, signature })
130    }
131
132    fn fail(reply: &Reply) -> Error {
133        let message = serde_json::from_str::<Value>(&reply.text).ok().and_then(|value| value.get("error").and_then(Value::as_str).map(str::to_string));
134        match message {
135            Some(text) => Error::Status(reply.status, text),
136            None => Error::Status(reply.status, format!("Kora answered with status {}", reply.status)),
137        }
138    }
139
140    pub fn call(&self, method: &str, path: &str, query: &[(&str, &str)], body: Option<Value>) -> Result<Value, Error> {
141        let reply = self.send(method, path, query, body)?;
142        if reply.status >= 400 {
143            return Err(Self::fail(&reply));
144        }
145        if reply.text.is_empty() {
146            return Ok(Value::Null);
147        }
148        serde_json::from_str(&reply.text).map_err(|error| Error::Json(error.to_string()))
149    }
150
151    fn key(&self) -> Result<VerifyingKey, Error> {
152        let pem = match &self.public_key {
153            Some(key) => key,
154            None => return Err(Error::Offline("A public key is required".to_string())),
155        };
156        let body: String = pem.lines().filter(|line| !line.starts_with("-----")).map(str::trim).collect();
157        let der = STANDARD.decode(body).map_err(|_| Error::Signature)?;
158        if der.len() < 32 {
159            return Err(Error::Signature);
160        }
161        let raw: [u8; 32] = der[der.len() - 32..].try_into().map_err(|_| Error::Signature)?;
162        VerifyingKey::from_bytes(&raw).map_err(|_| Error::Signature)
163    }
164
165    fn verify(&self, message: &[u8], signature: &[u8]) -> Result<(), Error> {
166        let signature = Signature::from_slice(signature).map_err(|_| Error::Signature)?;
167        self.key()?.verify(message, &signature).map_err(|_| Error::Signature)
168    }
169
170    fn check<T: DeserializeOwned>(&self, path: &str, body: Value) -> Result<T, Error> {
171        let reply = self.send("POST", path, &[], Some(body))?;
172        if reply.status == 429 || reply.status >= 500 {
173            return Err(Self::fail(&reply));
174        }
175        if self.public_key.is_some() {
176            let signature = reply.signature.as_deref().ok_or(Error::Signature)?;
177            self.verify(reply.text.as_bytes(), &STANDARD.decode(signature).map_err(|_| Error::Signature)?)?;
178        }
179        serde_json::from_str(&reply.text).map_err(|error| Error::Json(error.to_string()))
180    }
181
182    fn body(&self, key: &str, fields: &[(&str, &str)], product: bool) -> Value {
183        let mut body: Map<String, Value> = fields.iter().map(|(name, value)| (name.to_string(), json!(value))).collect();
184        body.insert("license_key".to_string(), json!(key));
185        if let (true, Some(name), false) = (product, &self.product, body.contains_key("product_name")) {
186            body.insert("product_name".to_string(), json!(name));
187        }
188        Value::Object(body)
189    }
190
191    pub fn validate(&self, key: &str, fields: &[(&str, &str)]) -> Result<Validation, Error> {
192        self.check("/licenses/validate", self.body(key, fields, true))
193    }
194
195    pub fn activate(&self, key: &str, fields: &[(&str, &str)]) -> Result<Validation, Error> {
196        self.check("/licenses/activate", self.body(key, fields, true))
197    }
198
199    pub fn deactivate(&self, key: &str, fields: &[(&str, &str)]) -> Result<Deactivation, Error> {
200        self.check("/licenses/deactivate", self.body(key, fields, false))
201    }
202
203    pub fn file(&self, id: &str) -> Result<String, Error> {
204        let reply = self.send("GET", &format!("/licenses/{id}/file"), &[], None)?;
205        if reply.status >= 400 {
206            return Err(Self::fail(&reply));
207        }
208        Ok(reply.text.trim().to_string())
209    }
210
211    fn date(license: &Value, field: &str) -> Result<Option<DateTime<Utc>>, Error> {
212        match license.get(field).and_then(Value::as_str) {
213            Some(text) => DateTime::parse_from_rfc3339(text).map(|date| Some(date.with_timezone(&Utc))).map_err(|error| Error::Offline(error.to_string())),
214            None => Ok(None),
215        }
216    }
217
218    pub fn offline(&self, file: &str) -> Result<Value, Error> {
219        let parts: Vec<&str> = file.trim().split('.').collect();
220        if parts.len() != 2 {
221            return Err(Error::Offline("Offline license file is malformed".to_string()));
222        }
223        let payload = URL_SAFE_NO_PAD.decode(parts[0]).map_err(|_| Error::Offline("Offline license file is malformed".to_string()))?;
224        let signature = URL_SAFE_NO_PAD.decode(parts[1]).map_err(|_| Error::Offline("Offline license file is malformed".to_string()))?;
225        self.verify(&payload, &signature)?;
226        let license: Value = serde_json::from_slice(&payload).map_err(|error| Error::Json(error.to_string()))?;
227        let now = Utc::now();
228        if matches!(Self::date(&license, "refresh_after")?, Some(date) if date < now) {
229            return Err(Error::Offline("Offline license needs a refresh".to_string()));
230        }
231        if matches!(Self::date(&license, "expires_at")?, Some(date) if date < now) {
232            return Err(Error::Offline("Offline license expired".to_string()));
233        }
234        Ok(license)
235    }
236
237    pub fn merge(&self, customer: &str, duplicate: &str) -> Result<Value, Error> {
238        let data = self.call("POST", &format!("/customers/{customer}/merge"), &[], Some(json!({ "customer_id": duplicate })))?;
239        Ok(data["customer"].clone())
240    }
241}
242
243pub struct Resource<'a> {
244    client: &'a Client,
245    name: &'static str,
246    single: &'static str,
247}
248
249impl Resource<'_> {
250    pub fn list(&self, query: &[(&str, &str)]) -> Result<(i64, Vec<Value>), Error> {
251        let data = self.client.call("GET", &format!("/{}", self.name), query, None)?;
252        let rows = data[self.name].as_array().cloned().ok_or_else(|| Error::Json(format!("missing {}", self.name)))?;
253        let total = data["total"].as_i64().ok_or_else(|| Error::Json("missing total".to_string()))?;
254        Ok((total, rows))
255    }
256
257    pub fn get(&self, id: &str) -> Result<Value, Error> {
258        self.client.call("GET", &format!("/{}/{}", self.name, id), &[], None)
259    }
260
261    pub fn create(&self, fields: Value) -> Result<Value, Error> {
262        Ok(self.client.call("POST", &format!("/{}", self.name), &[], Some(fields))?[self.single].clone())
263    }
264
265    pub fn update(&self, id: &str, fields: Value) -> Result<Value, Error> {
266        Ok(self.client.call("PATCH", &format!("/{}/{}", self.name, id), &[], Some(fields))?[self.single].clone())
267    }
268
269    pub fn delete(&self, id: &str, force: bool) -> Result<(), Error> {
270        let query: &[(&str, &str)] = if force { &[("force", "true")] } else { &[] };
271        self.client.call("DELETE", &format!("/{}/{}", self.name, id), query, None).map(|_| ())
272    }
273}