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