1mod ops;
6
7use anyhow::bail;
8use bytes::{BufMut, BytesMut};
9use fs_err::DirEntry;
10use ordinary_auth::token::{extract_hmac_no_check, get_exp};
11use std::env::home_dir;
12use std::path::Path;
13use std::path::PathBuf;
14use std::time::{Duration, SystemTime};
15use tracing::instrument;
16
17use crate::{ApiInfo, PortsResponse};
18use ed25519_dalek::{SigningKey, ed25519::signature::SignerMut};
19use ordinary_monitor::LogFileMetadata;
20use reqwest::Client;
21use serde::{Deserialize, Serialize};
22use sha2::{Digest, Sha256};
23use totp_rs::TOTP;
24use uuid::Uuid;
25
26pub struct OrdinaryApiClient<'a> {
28 pub(crate) addr: &'a str,
29 pub(crate) account: &'a str,
30 pub(crate) api_domain: Option<&'a str>,
31 pub(crate) client: Client,
32 pub(crate) correlation_id: Option<Uuid>,
33}
34
35fn compress_zstd(val: &[u8]) -> std::io::Result<Vec<u8>> {
36 zstd::stream::encode_all(std::io::Cursor::new(val), 17)
37}
38
39fn strip_http(addr: &str) -> &str {
40 if let Some(stripped) = addr.strip_prefix("https://") {
41 return stripped;
42 }
43 if let Some(stripped) = addr.strip_prefix("http://") {
44 return stripped;
45 }
46
47 addr
48}
49
50fn get_client_dir(domain: &str, account: &str) -> PathBuf {
51 home_dir()
52 .expect("failed to get home dir")
53 .join(".ordinary")
54 .join("clients")
55 .join(domain)
56 .join(account)
57}
58
59#[derive(Clone, Debug, Serialize, Deserialize)]
60pub struct AccountMeta {
61 pub id: String,
62 pub host: String,
63 pub domain: String,
64 pub name: String,
65 pub project: String,
66 pub permissions: Vec<u8>,
67 pub refresh_exp: u64,
68}
69
70impl<'a> OrdinaryApiClient<'a> {
71 pub fn new(
72 addr: &'a str,
73 account: &'a str,
74 api_domain: Option<&'a str>,
75 danger_accept_invalid_certs: bool,
76 user_agent: &str,
77 correlation_id: bool,
78 ) -> anyhow::Result<OrdinaryApiClient<'a>> {
79 tracing::debug!("initializing Ordinary API client");
80
81 let mut client_builder = Client::builder()
82 .use_rustls_tls()
83 .zstd(true)
84 .user_agent(user_agent);
85
86 if danger_accept_invalid_certs {
87 client_builder = client_builder.danger_accept_invalid_certs(true);
88 }
89
90 let client = client_builder.build()?;
91
92 Ok(OrdinaryApiClient {
93 addr,
94 account,
95 api_domain,
96 client,
97 correlation_id: correlation_id.then(Uuid::new_v4),
98 })
99 }
100
101 pub(crate) fn get_password_hash_and_domain(&self, password: &str) -> (Vec<u8>, &'a str) {
102 let (mut input, domain) = match self.api_domain {
103 Some(domain) => (domain.as_bytes().to_vec(), domain),
104 None => (
105 strip_http(self.addr).as_bytes().to_vec(),
106 strip_http(self.addr),
107 ),
108 };
109
110 input.extend_from_slice(self.account.as_bytes());
111 input.extend_from_slice(password.as_bytes());
112
113 let mut hasher = Sha256::new();
114 hasher.update(&input);
115 let password = hasher.finalize().to_vec();
116
117 (password, domain)
118 }
119
120 #[allow(clippy::missing_panics_doc)]
121 pub fn get_host(domain: &str, account: &str) -> anyhow::Result<String> {
122 let clients = home_dir()
123 .expect("failed to get home dir")
124 .join(".ordinary")
125 .join("clients");
126
127 let host = fs_err::read_to_string(clients.join(domain).join(account).join("host"))?;
128
129 Ok(host)
130 }
131
132 #[allow(clippy::missing_panics_doc)]
133 pub fn list_accounts() -> anyhow::Result<Vec<AccountMeta>> {
134 let clients = home_dir()
135 .expect("failed to get home dir")
136 .join(".ordinary")
137 .join("clients");
138
139 let mut out = vec![];
140
141 for entry in fs_err::read_dir(&clients)? {
142 let path = entry?.path();
143
144 if path.is_dir()
145 && let Some(domain) = &path.strip_prefix(&clients)?.to_str()
146 {
147 for entry in fs_err::read_dir(&path)? {
148 let path = entry?.path();
149
150 if path.is_dir()
151 && let Some(account) = path.strip_prefix(clients.join(domain))?.to_str()
152 {
153 let host = fs_err::read_to_string(
154 clients.join(domain).join(account).join("host"),
155 )?;
156 out.push(Self::get_account(&host, domain, account)?);
157 }
158 }
159 }
160 }
161
162 Ok(out)
163 }
164
165 pub fn get_account(host: &str, domain: &str, account: &str) -> anyhow::Result<AccountMeta> {
166 tracing::debug!("getting account");
167
168 let path = get_client_dir(domain, account);
169 tracing::debug!(path = %path.display());
170
171 let refresh_token = fs_err::read(path.join("refresh_token"))?;
172 let access_token = fs_err::read(path.join("access_token"))?;
173
174 tracing::debug!("extracting claims");
175 let claims = extract_hmac_no_check(&access_token)?;
176
177 let claims_vec = match flexbuffers::Reader::get_root(
178 &claims[..claims.len().checked_sub(8 + 64).unwrap_or(claims.len())],
179 ) {
180 Ok(v) => v.as_vector(),
181 Err(_) => flexbuffers::Reader::get_root(claims)?.as_vector(),
182 };
183
184 let system_claims = claims_vec.idx(0).as_vector();
185 let token_uuid_bytes: [u8; 16] = system_claims.idx(0).as_blob().0.try_into()?;
186
187 let token_uuid_str = Uuid::from_bytes(token_uuid_bytes).to_string();
188
189 let project = claims_vec.idx(1).as_str();
190 let permissions = claims_vec
191 .idx(2)
192 .as_vector()
193 .iter()
194 .map(|r| r.as_u8())
195 .collect::<Vec<u8>>();
196
197 Ok(AccountMeta {
198 id: token_uuid_str,
199 host: (*host).to_owned(),
200 domain: (*domain).to_owned(),
201 name: (*account).to_owned(),
202 project: project.to_owned(),
203 permissions,
204 refresh_exp: get_exp(&refresh_token)?,
205 })
206 }
207
208 #[instrument(skip_all, err)]
210 pub async fn register(
211 &self,
212 password: &str,
213 invite_code: &str,
214 ) -> anyhow::Result<(TOTP, String)> {
215 ops::account::register(self, password, invite_code).await
216 }
217
218 #[instrument(skip_all, err)]
220 pub async fn login(&self, password: &str, mfa_code: &str) -> anyhow::Result<()> {
221 ops::account::login(self, password, mfa_code).await
222 }
223
224 pub async fn get_access(
226 &self,
227 duration_s: Option<u32>,
228 correlation_id: Option<String>,
229 ) -> anyhow::Result<Vec<u8>> {
230 ops::account::get_access(self, duration_s, correlation_id).await
231 }
232
233 #[instrument(skip_all, err)]
235 pub async fn reset_password(
236 &self,
237 old_password: &str,
238 mfa_code: &str,
239 new_password: &str,
240 ) -> anyhow::Result<()> {
241 ops::account::reset_password(self, old_password, mfa_code, new_password).await
242 }
243
244 #[instrument(skip_all, err)]
246 pub async fn forgot_password(
247 &self,
248 new_password: &str,
249 recovery_code: &str,
250 ) -> anyhow::Result<()> {
251 ops::account::forgot_password(self, new_password, recovery_code).await
253 }
254
255 #[instrument(skip_all, err)]
257 pub async fn mfa_totp_reset(&self, password: &str, mfa_code: &str) -> anyhow::Result<TOTP> {
258 ops::account::mfa_totp_reset(self, password, mfa_code).await
259 }
260
261 #[instrument(skip_all, err)]
263 pub async fn mfa_totp_lost(&self, password: &str, recovery_code: &str) -> anyhow::Result<TOTP> {
264 ops::account::mfa_totp_lost(self, password, recovery_code).await
265 }
266
267 #[instrument(skip_all, err)]
269 pub async fn recovery_codes_reset(
270 &self,
271 password: &str,
272 mfa_code: &str,
273 ) -> anyhow::Result<String> {
274 ops::account::recovery_codes_reset(self, password, mfa_code).await
275 }
276
277 #[instrument(skip_all, err)]
279 pub async fn delete_account(&self, password: &str, mfa_code: &str) -> anyhow::Result<()> {
280 ops::account::delete(self, password, mfa_code).await
281 }
282
283 #[instrument(skip(self), err)]
285 pub async fn invite_api_account(
286 &self,
287 app_domain: &str,
288 account_name: &str,
289 permissions: Vec<u8>,
290 ) -> anyhow::Result<String> {
291 ops::account::invite_account(self, app_domain, account_name, permissions).await
292 }
293
294 #[instrument(skip(self), err)]
296 pub async fn api_accounts_list(&self, proj_path: Option<&str>) -> anyhow::Result<String> {
297 ops::account::accounts_list(self, proj_path).await
298 }
299
300 #[instrument(skip(self), err)]
302 pub async fn deploy(
303 &self,
304 proj_path: &str,
305 tls_staging: bool,
306 ) -> anyhow::Result<PortsResponse> {
307 ops::app::deploy(self, proj_path, tls_staging).await
308 }
309
310 #[instrument(skip(self), err)]
312 pub async fn kill(&self, proj_path: &str) -> anyhow::Result<()> {
313 ops::app::kill(self, proj_path).await
314 }
315
316 #[instrument(skip(self), err)]
318 pub async fn restart(
319 &self,
320 proj_path: &str,
321 tls_staging: bool,
322 ) -> anyhow::Result<PortsResponse> {
323 ops::app::restart(self, proj_path, tls_staging).await
324 }
325
326 #[instrument(skip(self), err)]
328 pub async fn erase(&self, proj_path: &str) -> anyhow::Result<()> {
329 ops::app::erase(self, proj_path).await
330 }
331
332 #[instrument(skip_all, err)]
334 pub async fn root_get_metrics(&self, service: String) -> anyhow::Result<String> {
335 ops::root::metrics(self, None, service).await
336 }
337
338 #[instrument(skip_all, err)]
340 pub async fn root_get_info(&self) -> anyhow::Result<ApiInfo> {
341 ops::root::info(self, None).await
342 }
343
344 #[instrument(skip_all, err)]
346 pub async fn root_lock_account(&self, account: &str) -> anyhow::Result<()> {
347 ops::root::set_lock(self, None, account, true).await
348 }
349
350 #[instrument(skip_all, err)]
352 pub async fn root_unlock_account(&self, account: &str) -> anyhow::Result<()> {
353 ops::root::set_lock(self, None, account, false).await
354 }
355
356 #[instrument(skip_all, err)]
358 pub fn root_logs_local_metadata(&self) -> anyhow::Result<Vec<LogFileMetadata>> {
359 ops::logs::logs_local_metadata("root", false)
360 }
361
362 #[instrument(skip_all, err)]
364 pub async fn root_logs_remote_metadata(&self) -> anyhow::Result<Vec<LogFileMetadata>> {
365 ops::logs::logs_remote_metadata(self, "root", None, false).await
366 }
367
368 #[instrument(skip_all, err)]
370 pub async fn root_logs_sync(
371 &self,
372 force: Option<bool>,
373 file: Option<&str>,
374 ) -> anyhow::Result<()> {
375 ops::logs::logs_sync(self, "root", force, file, false).await
376 }
377
378 #[instrument(skip_all, err)]
380 pub fn root_logs_search(
381 &self,
382 query: &str,
383 format: &str,
384 limit: &Option<usize>,
385 ) -> anyhow::Result<String> {
386 ops::logs::logs_search("root", query, format, limit)
387 }
388
389 #[instrument(skip_all, err)]
391 pub fn root_logs_file_list(&self) -> anyhow::Result<()> {
392 ops::logs::logs_file_list("root")
393 }
394
395 #[instrument(skip_all, err)]
397 pub fn root_logs_file_read(&self, name: &str) -> anyhow::Result<()> {
398 ops::logs::logs_file_read("root", name)
399 }
400
401 #[instrument(skip_all, err)]
403 pub fn root_logs_file_read_all(&self) -> anyhow::Result<()> {
404 ops::logs::logs_file_read_all("root")
405 }
406
407 #[instrument(skip_all, err)]
409 pub fn app_logs_local_metadata(&self, proj_path: &str) -> anyhow::Result<Vec<LogFileMetadata>> {
410 ops::logs::logs_local_metadata(proj_path, true)
411 }
412
413 #[instrument(skip_all, err)]
415 pub async fn app_logs_remote_metadata(
416 &self,
417 proj_path: &str,
418 ) -> anyhow::Result<Vec<LogFileMetadata>> {
419 ops::logs::logs_remote_metadata(self, proj_path, None, true).await
420 }
421
422 #[instrument(skip_all, err)]
424 pub async fn app_logs_sync(
425 &self,
426 proj_path: &str,
427 force: Option<bool>,
428 file: Option<&str>,
429 ) -> anyhow::Result<()> {
430 ops::logs::logs_sync(self, proj_path, force, file, true).await
431 }
432
433 #[instrument(skip_all, err)]
435 pub fn app_logs_search(
436 &self,
437 proj_path: &str,
438 query: &str,
439 format: &str,
440 limit: &Option<usize>,
441 ) -> anyhow::Result<String> {
442 ops::logs::logs_search(proj_path, query, format, limit)
443 }
444
445 #[instrument(skip_all, err)]
447 pub fn app_logs_file_list(&self, project_path: &str) -> anyhow::Result<()> {
448 ops::logs::logs_file_list(project_path)
449 }
450
451 #[instrument(skip_all, err)]
453 pub fn app_logs_file_read(&self, project_path: &str, name: &str) -> anyhow::Result<()> {
454 ops::logs::logs_file_read(project_path, name)
455 }
456
457 #[instrument(skip_all, err)]
459 pub fn app_logs_file_read_all(&self, project_path: &str) -> anyhow::Result<()> {
460 ops::logs::logs_file_read_all(project_path)
461 }
462
463 #[instrument(skip(self), err)]
465 pub async fn app_accounts_list(&self, proj_path: &str) -> anyhow::Result<String> {
466 ops::app::accounts_list(self, proj_path).await
467 }
468
469 #[instrument(skip(self), err)]
471 pub async fn items_list(&self, proj_path: &str, model_name: &str) -> anyhow::Result<String> {
472 ops::models::items_list(self, proj_path, model_name).await
473 }
474
475 #[instrument(skip(self), err)]
477 pub async fn write(&self, proj_path: &str, asset_path: &str) -> anyhow::Result<()> {
478 ops::assets::write(self, proj_path, asset_path).await
479 }
480
481 #[instrument(skip(self), err)]
483 pub async fn write_all(&self, proj_path: &str) -> anyhow::Result<()> {
484 ops::assets::write_all(self, proj_path).await
485 }
486
487 #[instrument(skip(self), err)]
489 pub async fn upload(&self, proj_path: &str, template_name: &str) -> anyhow::Result<()> {
490 ops::templates::upload(self, proj_path, template_name).await
491 }
492
493 #[instrument(skip(self), err)]
495 pub async fn upload_all(&self, proj_path: &str) -> anyhow::Result<()> {
496 ops::templates::upload_all(self, proj_path).await
497 }
498
499 #[instrument(skip(self), err)]
501 pub async fn install(&self, proj_path: &str, action_name: &str) -> anyhow::Result<()> {
502 ops::actions::install(self, proj_path, action_name).await
503 }
504
505 #[instrument(skip(self), err)]
507 pub async fn install_all(&self, proj_path: &str) -> anyhow::Result<()> {
508 ops::actions::install_all(self, proj_path).await
509 }
510
511 #[instrument(skip(self), err)]
513 pub async fn update(&self, proj_path: &str) -> anyhow::Result<()> {
514 ops::content::update(self, proj_path).await
515 }
516
517 #[instrument(skip(self, secret), err)]
519 pub async fn store(&self, proj_path: &str, name: &str, secret: &[u8]) -> anyhow::Result<()> {
520 ops::secrets::store(self, proj_path, name, secret).await
521 }
522
523 fn sign_token(
524 client_dir: &Path,
525 token: &mut BytesMut,
526 signing_key: Option<SigningKey>,
527 duration_s: Option<u32>,
528 ) -> anyhow::Result<()> {
529 let client_exp = match SystemTime::now()
530 .checked_add(Duration::from_secs(u64::from(duration_s.unwrap_or(3))))
531 {
532 Some(v) => v,
533 None => bail!("failed to add"),
534 }
535 .duration_since(SystemTime::UNIX_EPOCH)?
536 .as_secs();
537
538 token.put_u64(client_exp);
539
540 let mut signing_key = if let Some(sk) = signing_key {
541 sk
542 } else {
543 let signing_key_bytes: [u8; 32] =
544 match fs_err::read(client_dir.join("signing_key"))?.try_into() {
545 Ok(v) => v,
546 Err(_) => bail!("failed to convert"),
547 };
548
549 let signing_key: SigningKey = SigningKey::from_bytes(&signing_key_bytes);
550 signing_key
551 };
552
553 let signature = signing_key.sign(&token[..]);
554
555 token.put(&signature.to_bytes()[..]);
556
557 Ok(())
558 }
559}
560
561pub fn traverse(dir: &Path, cb: &dyn Fn(&DirEntry)) -> std::io::Result<()> {
562 if dir.is_dir() {
563 for entry in fs_err::read_dir(dir)? {
564 let entry = entry?;
565 let path = entry.path();
566 if path.is_dir() {
567 traverse(&path, cb)?;
568 } else {
569 cb(&entry);
570 }
571 }
572 }
573 Ok(())
574}