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