Oregami
Repositories/oxedyne/fe2o3

oxedyne/fe2o3/fe2o3_shield/src/app/tui.rs

12.3 KiB, 75 runs

created by r1870400018:4076, which is this file's identity for as long as the history lasts, whatever it is later renamed to

download · who wrote it · its history

1#![forbid(unsafe_code)]
2use crate::{
3 app::{
4 cfg::AppConfig,
5 constant,
6 repl::AppShellContext,
7 syntax as app_syntax,
8 },
9 srv::{
10 context::new_db,
11 },
12};
13
14use oxedyne_fe2o3_core::{
15 prelude::*,
16 log::{
17 bot::FileConfig,
18 },
19};
20use oxedyne_fe2o3_crypto::keystore::{
21 DEFAULT_WALLET_KDF_NAME,
22 Wallet,
23};
24use oxedyne_fe2o3_jdat::{
25 prelude::*,
26 cfg::Config,
27 file::{
28 JdatFile,
29 JdatMapFile,
30 },
31 string::{
32 dec::DecoderConfig,
33 enc::EncoderConfig,
34 },
35};
36use oxedyne_fe2o3_tui::lib_tui::{
37 repl::{
38 Evaluation,
39 ShellConfig,
40 },
41 input::UserInput,
42};
43use oxedyne_fe2o3_namex::InNamex;
44
45use std::{
46 collections::BTreeMap,
47 io::Write,
48 path::{
49 Path,
50 PathBuf,
51 },
52};
53
54use secrecy::{
55 ExposeSecret,
56};
57
58
59#[derive(Clone)]
60pub enum State {
61 NotStarted,
62 Running,
63 NotResponsive,
64}
65
66impl Default for State { fn default() -> Self { Self::NotStarted } }
67
68#[derive(Clone, Default)]
69pub struct AppStatus {
70 pub first: bool,
71 pub log: State,
72 pub db: State,
73 pub web: State,
74}
75
76pub fn run() -> Outcome<()> {
77
78 let mut app_status = AppStatus::default();
79 let cwd = res!(std::env::current_dir());
80 let cwd_str = res!(cwd.to_str().ok_or(err!(
81 "Converting the current working directory path '{:?}' to a string.", cwd;
82 Conversion, String)));
83 let err_str = fmt!("Failed to obtain the directory name from the current working path '{:?}'.",
84 cwd);
85 let this_dir = res!(
86 res!(cwd.file_name().ok_or(err!("{}", &err_str; Conversion, String)))
87 .to_str().ok_or(err!("{}", &err_str; Conversion, String))
88 );
89
90 // ┌───────────────────────────────────────────────────────────────────────────────────────────┐
91 // │ LOGIN STEP │
92 // │ The app executable, configuration and wallet files must be co-located but can exist │
93 // │ separately from the application root directory which contains all other data including │
94 // │ the database and logs. We first try and load the configuration, then the wallet. │
95 // └───────────────────────────────────────────────────────────────────────────────────────────┘
96
97 // ┌───────────────────────┐
98 // │ Load the config file. │
99 // │ It contains the app │
100 // │ root directory, │
101 // │ among other things. │
102 // └───────────────────────┘
103 let cfg_path = Path::new("./").join(constant::CONFIG_NAME);
104
105 if !cfg_path.is_file() {
106 app_status.first = true;
107 let mut cfg = res!(AppConfig::new());
108
109 println!("Welcome to the Hematite Shield Server, this appears to be a new app.");
110 println!("You'll now be asked to enter a human name and a description...");
111 for (field, prompt) in [
112 (&mut cfg.app_human_name, "App human name"),
113 (&mut cfg.app_description, "App description"),
114 ] {
115 print!("{}: ", prompt);
116 res!(std::io::stdout().flush());
117 let mut input = String::new();
118 res!(std::io::stdin().read_line(&mut input));
119 *field = input.trim().to_string();
120 }
121 cfg.app_name = this_dir.to_string();
122
123 res!(cfg.save(&cfg_path, " ", false));
124 println!(
125 "There is no {} file, a default has been created at {:?}.",
126 constant::CONFIG_NAME, cfg_path,
127 );
128 }
129 let mut cfg = res!(AppConfig::load(cfg_path));
130 res!(cfg.check_and_fix());
131 println!("Welcome to {}.", cfg.app_human_name);
132
133 // ┌───────────────────────┐
134 // │ Start logging. │
135 // └───────────────────────┘
136 let mut log_cfg = log_get_config!();
137 log_cfg.console = None;
138 log_cfg.level = match LogLevel::from_str(&cfg.app_log_level) {
139 Ok(level) => level,
140 _ => res!(LogLevel::from_str(constant::DEFAULT_LOG_LEVEL)),
141 };
142 log_cfg.file = Some(FileConfig::new(
143 PathBuf::from(cfg.app_root.clone()),
144 cfg.app_name.clone(),
145 constant::LOG_FILE_EXTENSION.to_string(),
146 0,
147 None, // No multiple log file archiving, just use same file.
148 ));
149 log_set_config!(log_cfg);
150 println!("Shell now logging at {:?}", log_get_file_path!());
151 info!(async_log::stream(), "┌───────────────────────┐");
152 info!(async_log::stream(), "│ New shell session. │");
153 info!(async_log::stream(), "└───────────────────────┘");
154
155 // ┌───────────────────────────────────────────────────────────────────────┐
156 // │ WALLET UNLOCK │
157 // │ │
158 // │ See `fe2o3_crypto::keystore::Wallet` for the data model. Any admin │
159 // │ password unwraps their entry recovers the shared master key, which │
160 // │ is then used as the Ozone database encryption key. Password is read │
161 // │ from stdin (echo off) with an optional `SHIELD_ADMIN_PASS` env var │
162 // │ override for non-interactive test flows. │
163 // └───────────────────────────────────────────────────────────────────────┘
164 let wallet_path = Path::new("./").join(constant::WALLET_NAME);
165 let (wallet, db_default_enc_key) = if wallet_path.is_file() {
166 let wallet = res!(Wallet::load(
167 wallet_path,
168 Some(DecoderConfig::<(), ()>::default()),
169 ));
170 let pass = if let Ok(s) = std::env::var("SHIELD_ADMIN_PASS") {
171 secrecy::Secret::new(s)
172 } else {
173 res!(UserInput::ask_for_secret(None))
174 };
175 let unlocked = match wallet.unlock(pass.expose_secret().as_bytes()) {
176 Ok(u) => u,
177 Err(e) => {
178 println!("Wallet unlock failed: {}.", e);
179 return Ok(());
180 }
181 };
182 info!(async_log::stream(),
183 "Wallet unlocked by admin '{}'.", unlocked.admin_name);
184 let db_default_enc_key = unlocked.master_key.expose_secret().clone();
185 (wallet, db_default_enc_key)
186 } else {
187 // ┌───────────────────────┐
188 // │ Wallet not found. │
189 // └───────────────────────┘
190 println!(
191 "There is no {} file.\nYou can replace it with a backup and restart, or create a new one.",
192 constant::WALLET_NAME,
193 );
194 println!("What would you like to do?");
195 println!(" 1. Exit, replace with a backup file, and restart.");
196 println!(" 2. Create a new {} file.", constant::WALLET_NAME);
197 print!("Please choose: ");
198 res!(std::io::stdout().flush());
199 let mut choice = String::new();
200 res!(std::io::stdin().read_line(&mut choice));
201 match choice.trim() {
202 "1" => {
203 println!("Ok, good luck!");
204 return Ok(());
205 },
206 "2" => {
207 // ┌───────────────────────┐
208 // │ Create the wallet │
209 // │ from scratch with one │
210 // │ operator admin entry. │
211 // └───────────────────────┘
212 println!("Ok, let's create the first wallet admin and passphrase.");
213 print!("Admin name (default 'operator'): ");
214 res!(std::io::stdout().flush());
215 let mut name_in = String::new();
216 res!(std::io::stdin().read_line(&mut name_in));
217 let admin_name = match name_in.trim() {
218 "" => "operator".to_string(),
219 s => s.to_string(),
220 };
221 let pass = res!(UserInput::create_pass(constant::MAX_CREATE_PASS_ATTEMPTS));
222 let pass_bytes = pass.expose_secret().as_bytes();
223
224 let mut metadata = BTreeMap::new();
225 metadata.insert(dat!("app_name"), dat!(cfg.app_name.clone()));
226 metadata.insert(dat!("app_root"), dat!(cfg.app_root.clone()));
227 metadata.insert(dat!("this_dir"), dat!(cwd_str));
228
229 let (wallet, unlocked) = res!(Wallet::create_with_first_admin(
230 metadata,
231 admin_name,
232 pass_bytes,
233 DEFAULT_WALLET_KDF_NAME,
234 ));
235 res!(wallet.save_secret(
236 &wallet_path,
237 " ",
238 Some(EncoderConfig::<(), ()>::default()),
239 ));
240 println!("Thank you, {:?} created.", wallet_path);
241 let db_default_enc_key = unlocked.master_key.expose_secret().clone();
242 (wallet, db_default_enc_key)
243 },
244 _ => return Err(err!(
245 "Invalid response, goodbye!";
246 Invalid, Input)),
247 }
248 };
249
250 // ┌───────────────────────────────────────────────────────────────────────────────────────────┐
251 // │ EXECUTION STEP │
252 // │ Functions can be executed directly from the command line, or within a shell. If no │
253 // │ commands are supplied, the user is presented with the shell. The command line is │
254 // │ technically part of the shell. │
255 // └───────────────────────────────────────────────────────────────────────────────────────────┘
256
257 let app_root = Path::new(&cfg.app_root);
258 let db_root = app_root.join(constant::DB_DIR);
259
260 let invocation_cmds: Vec<String> = std::env::args().skip(1).collect();
261
262 let syntax = res!(app_syntax::new_shell(
263 &cfg.app_human_name,
264 &constant::VERSION,
265 &fmt!("{} app: {}", cfg.app_human_name, cfg.app_description),
266 ));
267
268 let mut context = AppShellContext {
269 stat: app_status,
270 app_cfg: cfg.clone(),
271 syntax,
272 ws: BTreeMap::new(),
273 db: res!(new_db(&db_root, &db_default_enc_key)),
274 wallet,
275 };
276
277 let mut shell_cfg = ShellConfig::default();
278
279 if invocation_cmds.len() > 0 {
280 match context.execute(invocation_cmds, &shell_cfg) {
281 Ok(evals) => for eval in evals {
282 match eval {
283 Evaluation::Output(s) => println!("{}", s),
284 Evaluation::Exit => {
285 println!("Exiting {} now.", cfg.app_human_name);
286 }
287 _ => (),
288 }
289 }
290 Err(e) => {
291 //println!("{} error: {}", cfg.app_human_name, e);
292 return Err(e);
293 }
294 }
295 } else {
296 shell_cfg.greeting_msg =
297 fmt!("Welcome, type \"help\" for a help menu.");
298 res!(context.start_shell(&shell_cfg, None));
299 }
300
301 Ok(())
302}