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}