1mod ops;
6
7use anyhow::bail;
8use bytes::{BufMut, BytesMut};
9use ordinary_auth::token::{extract_hmac_no_check, get_exp};
10use serde_json::Value;
11use std::env::home_dir;
12use std::fs::{DirEntry, read_to_string};
13use std::path::Path;
14use std::path::PathBuf;
15use std::time::{Duration, SystemTime};
16use tracing::instrument;
17
18use ed25519_dalek::{SigningKey, ed25519::signature::SignerMut};
19use reqwest::Client;
20use sha2::{Digest, Sha256};
21use totp_rs::TOTP;
22use uuid::Uuid;
23
24pub struct OrdinaryApiClient<'a> {
26 pub(crate) addr: &'a str,
27 pub(crate) account: &'a str,
28 pub(crate) api_domain: Option<&'a str>,
29 pub(crate) client: Client,
30}
31
32fn compress_zstd(val: &[u8]) -> std::io::Result<Vec<u8>> {
33 zstd::stream::encode_all(std::io::Cursor::new(val), 17)
34}
35
36fn strip_http(addr: &str) -> &str {
37 if let Some(stripped) = addr.strip_prefix("https://") {
38 return stripped;
39 }
40 if let Some(stripped) = addr.strip_prefix("http://") {
41 return stripped;
42 }
43
44 addr
45}
46
47fn get_client_dir(domain: &str, account: &str) -> PathBuf {
48 home_dir()
49 .expect("failed to get home dir")
50 .join(".ordinary")
51 .join("clients")
52 .join(domain)
53 .join(account)
54}
55
56pub struct AccountMeta {
57 pub id: String,
58 pub host: String,
59 pub domain: String,
60 pub name: String,
61 pub project: String,
62 pub permissions: Vec<u8>,
63 pub refresh_exp: u64,
64}
65
66impl<'a> OrdinaryApiClient<'a> {
67 pub fn new(
68 addr: &'a str,
69 account: &'a str,
70 api_domain: Option<&'a str>,
71 danger_accept_invalid_certs: bool,
72 user_agent: &str,
73 ) -> anyhow::Result<OrdinaryApiClient<'a>> {
74 tracing::debug!("initializing Ordinary API client");
75
76 let mut client_builder = Client::builder().use_rustls_tls().zstd(true);
77 client_builder = client_builder.user_agent(user_agent);
78
79 if danger_accept_invalid_certs {
80 client_builder = client_builder.danger_accept_invalid_certs(true);
81 }
82
83 let client = client_builder.build()?;
84
85 Ok(OrdinaryApiClient {
86 addr,
87 account,
88 api_domain,
89 client,
90 })
91 }
92
93 pub(crate) fn get_password_hash_and_domain(&self, password: &str) -> (Vec<u8>, &'a str) {
94 let (mut input, domain) = match self.api_domain {
95 Some(domain) => (domain.as_bytes().to_vec(), domain),
96 None => (
97 strip_http(self.addr).as_bytes().to_vec(),
98 strip_http(self.addr),
99 ),
100 };
101
102 input.extend_from_slice(self.account.as_bytes());
103 input.extend_from_slice(password.as_bytes());
104
105 let mut hasher = Sha256::new();
106 hasher.update(&input);
107 let password = hasher.finalize().to_vec();
108
109 (password, domain)
110 }
111
112 #[allow(clippy::missing_panics_doc)]
113 pub fn list_accounts() -> anyhow::Result<Vec<AccountMeta>> {
114 let clients = home_dir()
115 .expect("failed to get home dir")
116 .join(".ordinary")
117 .join("clients");
118
119 let mut out = vec![];
120
121 for entry in std::fs::read_dir(&clients)? {
122 let path = entry?.path();
123
124 if path.is_dir()
125 && let Some(domain) = &path.strip_prefix(&clients)?.to_str()
126 {
127 for entry in std::fs::read_dir(&path)? {
128 let path = entry?.path();
129
130 if path.is_dir()
131 && let Some(account) = path.strip_prefix(clients.join(domain))?.to_str()
132 {
133 let host = read_to_string(clients.join(domain).join(account).join("host"))?;
134 out.push(Self::get_account(&host, domain, account)?);
135 }
136 }
137 }
138 }
139
140 Ok(out)
141 }
142
143 pub fn get_account(host: &str, domain: &str, account: &str) -> anyhow::Result<AccountMeta> {
144 tracing::debug!("getting account");
145
146 let path = get_client_dir(domain, account);
147 tracing::debug!(path = %path.display());
148
149 let refresh_token = std::fs::read(path.join("refresh_token"))?;
150 let access_token = std::fs::read(path.join("access_token"))?;
151
152 tracing::debug!("extracting claims");
153 let claims = extract_hmac_no_check(&access_token)?;
154
155 let claims_vec = match flexbuffers::Reader::get_root(
156 &claims[..claims.len().checked_sub(8 + 64).unwrap_or(claims.len())],
157 ) {
158 Ok(v) => v.as_vector(),
159 Err(_) => flexbuffers::Reader::get_root(claims)?.as_vector(),
160 };
161
162 let system_claims = claims_vec.idx(0).as_vector();
163 let token_uuid_bytes: [u8; 16] = system_claims.idx(0).as_blob().0.try_into()?;
164
165 let token_uuid_str = Uuid::from_bytes(token_uuid_bytes).to_string();
166
167 let project = claims_vec.idx(1).as_str();
168 let permissions = claims_vec
169 .idx(2)
170 .as_vector()
171 .iter()
172 .map(|r| r.as_u8())
173 .collect::<Vec<u8>>();
174
175 Ok(AccountMeta {
176 id: token_uuid_str,
177 host: (*host).to_owned(),
178 domain: (*domain).to_owned(),
179 name: (*account).to_owned(),
180 project: project.to_owned(),
181 permissions,
182 refresh_exp: get_exp(&refresh_token)?,
183 })
184 }
185
186 #[instrument(skip(self, password, invite_code), err)]
188 pub async fn register(
189 &self,
190 password: &str,
191 invite_code: &str,
192 ) -> anyhow::Result<(TOTP, String)> {
193 ops::account::register(self, password, invite_code).await
194 }
195
196 #[instrument(skip(self, password, mfa_code), err)]
198 pub async fn login(&self, password: &str, mfa_code: &str) -> anyhow::Result<()> {
199 ops::account::login(self, password, mfa_code).await
200 }
201
202 pub async fn get_access(&self, duration_s: Option<u32>) -> anyhow::Result<Vec<u8>> {
204 ops::account::get_access(self, duration_s).await
205 }
206
207 #[instrument(skip(self, old_password, mfa_code, new_password), err)]
209 pub async fn reset_password(
210 &self,
211 old_password: &str,
212 mfa_code: &str,
213 new_password: &str,
214 ) -> anyhow::Result<()> {
215 ops::account::reset_password(self, old_password, mfa_code, new_password).await
216 }
217
218 #[instrument(skip(self, new_password, recovery_code), err)]
220 pub async fn forgot_password(
221 &self,
222 new_password: &str,
223 recovery_code: &str,
224 ) -> anyhow::Result<()> {
225 ops::account::forgot_password(self, new_password, recovery_code).await
227 }
228
229 #[instrument(skip(self, password, mfa_code), err)]
231 pub async fn mfa_totp_reset(&self, password: &str, mfa_code: &str) -> anyhow::Result<TOTP> {
232 ops::account::mfa_totp_reset(self, password, mfa_code).await
233 }
234
235 #[instrument(skip(self, password, recovery_code), err)]
237 pub async fn mfa_totp_lost(&self, password: &str, recovery_code: &str) -> anyhow::Result<TOTP> {
238 ops::account::mfa_totp_lost(self, password, recovery_code).await
239 }
240
241 #[instrument(skip(self, password, mfa_code), err)]
243 pub async fn recovery_codes_reset(
244 &self,
245 password: &str,
246 mfa_code: &str,
247 ) -> anyhow::Result<String> {
248 ops::account::recovery_codes_reset(self, password, mfa_code).await
249 }
250
251 #[instrument(skip(self), err)]
253 pub async fn invite_api_account(
254 &self,
255 app_domain: &str,
256 account_name: &str,
257 permissions: Vec<u8>,
258 ) -> anyhow::Result<String> {
259 ops::account::invite_account(self, app_domain, account_name, permissions).await
260 }
261
262 #[instrument(skip(self), err)]
264 pub async fn api_accounts_list(&self, proj_path: Option<&str>) -> anyhow::Result<String> {
265 ops::account::accounts_list(self, proj_path).await
266 }
267
268 #[instrument(skip(self), err)]
270 pub async fn deploy(&self, proj_path: &str) -> anyhow::Result<u16> {
271 ops::app::deploy(self, proj_path).await
272 }
273
274 #[instrument(skip(self), err)]
276 pub async fn patch(&self, proj_path: &str) -> anyhow::Result<u16> {
277 ops::app::patch(self, proj_path).await
278 }
279
280 #[instrument(skip(self), err)]
282 pub async fn kill(&self, proj_path: &str) -> anyhow::Result<()> {
283 ops::app::kill(self, proj_path).await
284 }
285
286 #[instrument(skip(self), err)]
288 pub async fn restart(&self, proj_path: &str) -> anyhow::Result<u16> {
289 ops::app::restart(self, proj_path).await
290 }
291
292 #[instrument(skip(self), err)]
294 pub async fn app_logs(
295 &self,
296 proj_path: &str,
297 query: &str,
298 format: &str,
299 limit: &Option<usize>,
300 ) -> anyhow::Result<Value> {
301 ops::app::logs(self, proj_path, query, format, limit).await
302 }
303
304 #[instrument(skip(self), err)]
306 pub async fn app_accounts_list(&self, proj_path: &str) -> anyhow::Result<String> {
307 ops::app::accounts_list(self, proj_path).await
308 }
309
310 #[instrument(skip(self), err)]
312 pub async fn items_list(&self, proj_path: &str, model_name: &str) -> anyhow::Result<String> {
313 ops::models::items_list(self, proj_path, model_name).await
314 }
315
316 #[instrument(skip(self), err)]
318 pub async fn write(&self, proj_path: &str, asset_path: &str) -> anyhow::Result<()> {
319 ops::assets::write(self, proj_path, asset_path).await
320 }
321
322 #[instrument(skip(self), err)]
324 pub async fn write_all(&self, proj_path: &str) -> anyhow::Result<()> {
325 ops::assets::write_all(self, proj_path).await
326 }
327
328 #[instrument(skip(self), err)]
330 pub async fn upload(&self, proj_path: &str, template_name: &str) -> anyhow::Result<()> {
331 ops::templates::upload(self, proj_path, template_name).await
332 }
333
334 #[instrument(skip(self), err)]
336 pub async fn upload_all(&self, proj_path: &str) -> anyhow::Result<()> {
337 ops::templates::upload_all(self, proj_path).await
338 }
339
340 #[instrument(skip(self), err)]
342 pub async fn install(&self, proj_path: &str, action_name: &str) -> anyhow::Result<()> {
343 ops::actions::install(self, proj_path, action_name).await
344 }
345
346 #[instrument(skip(self), err)]
348 pub async fn install_all(&self, proj_path: &str) -> anyhow::Result<()> {
349 ops::actions::install_all(self, proj_path).await
350 }
351
352 #[instrument(skip(self), err)]
354 pub async fn update(&self, proj_path: &str) -> anyhow::Result<()> {
355 ops::content::update(self, proj_path).await
356 }
357
358 #[instrument(skip(self), err)]
360 pub async fn store(&self, proj_path: &str, name: &str, secret: &[u8]) -> anyhow::Result<()> {
361 ops::secrets::store(self, proj_path, name, secret).await
362 }
363
364 fn sign_token(
365 client_dir: &Path,
366 token: &mut BytesMut,
367 signing_key: Option<SigningKey>,
368 duration_s: Option<u32>,
369 ) -> anyhow::Result<()> {
370 let client_exp = match SystemTime::now()
371 .checked_add(Duration::from_secs(u64::from(duration_s.unwrap_or(3))))
372 {
373 Some(v) => v,
374 None => bail!("failed to add"),
375 }
376 .duration_since(SystemTime::UNIX_EPOCH)?
377 .as_secs();
378
379 token.put_u64(client_exp);
380
381 let mut signing_key = if let Some(sk) = signing_key {
382 sk
383 } else {
384 let signing_key_bytes: [u8; 32] =
385 match std::fs::read(client_dir.join("signing_key"))?.try_into() {
386 Ok(v) => v,
387 Err(_) => bail!("failed to convert"),
388 };
389
390 let signing_key: SigningKey = SigningKey::from_bytes(&signing_key_bytes);
391 signing_key
392 };
393
394 let signature = signing_key.sign(&token[..]);
395
396 token.put(&signature.to_bytes()[..]);
397
398 Ok(())
399 }
400}
401
402pub fn traverse(dir: &Path, cb: &dyn Fn(&DirEntry)) -> std::io::Result<()> {
403 if dir.is_dir() {
404 for entry in std::fs::read_dir(dir)? {
405 let entry = entry?;
406 let path = entry.path();
407 if path.is_dir() {
408 traverse(&path, cb)?;
409 } else {
410 cb(&entry);
411 }
412 }
413 }
414 Ok(())
415}