Oregami
Repositories/oxedyne/fe2o3

oxedyne/fe2o3/fe2o3_steel/src/srv/ws/handler.rs

63.9 KiB, 143 runs

created by r1870400018:997, 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

1use crate::srv::{
2 constant,
3 context::{
4 ServerContext,
5 },
6 dev::refresh::DevRefreshManager,
7 ws::term::TerminalManager,
8};
9
10use oxedyne_fe2o3_core::{
11 prelude::*,
12};
13use oxedyne_fe2o3_hash::{
14 kdf::KeyDerivationScheme,
15};
16use oxedyne_fe2o3_iop_crypto::enc::Encrypter;
17use oxedyne_fe2o3_iop_db::api::Database;
18use oxedyne_fe2o3_iop_hash::{
19 api::Hasher,
20 kdf::KeyDeriver,
21};
22use oxedyne_fe2o3_jdat::{
23 prelude::*,
24 id::NumIdDat,
25};
26use oxedyne_fe2o3_net::{
27 http::{
28 handler::WebHandler,
29 msg::HttpMessage,
30 },
31 //smtp::handler::EmailHandler,
32 ws::{
33 WebSocket,
34 core::WebSocketMessage,
35 handler::WebSocketHandler,
36 },
37};
38use oxedyne_fe2o3_syntax::{
39 SyntaxRef,
40 msg::{
41 Msg,
42 MsgCmd,
43 },
44 val::Val,
45};
46
47
48use std::{
49 sync::{
50 Arc,
51 RwLock,
52 //Mutex,
53 },
54};
55
56use tokio::{
57 self,
58 io::{
59 AsyncRead,
60 AsyncWrite,
61 },
62 sync::broadcast,
63};
64
65
66impl<
67 const UIDL: usize,
68 UID: NumIdDat<UIDL> + 'static,
69 ENC: Encrypter + 'static,
70 KH: Hasher + 'static,
71 DB: Database<UIDL, UID, ENC, KH> + 'static,
72 //EH: EmailHandler,
73 WH: WebHandler + 'static,
74 WSH: WebSocketHandler + 'static,
75>
76 //ServerContext<UIDL, UID, ENC, KH, DB, EH, WH, WSH>
77 ServerContext<UIDL, UID, ENC, KH, DB, WH, WSH>
78{
79 pub async fn handle_websocket<
80 'a,
81 S: AsyncRead + AsyncWrite + Unpin,
82 >(
83 self,
84 stream: &'a mut S,
85 ws_handler: WSH,
86 ws_syntax: SyntaxRef,
87 vhost_db: Option<(Arc<RwLock<DB>>, UID)>,
88 request: HttpMessage,
89 id: &String,
90 )
91 -> Outcome<()>
92 {
93 let mut ws = WebSocket::new_server(
94 stream,
95 ws_handler.clone(),
96 constant::WEBSOCKET_CHUNK_SIZE,
97 constant::WEBSOCKET_CHUNKING_THRESHOLD,
98 );
99 match ws.connect_as_server(request).await {
100 Ok(()) => (),
101 Err(e) => return Err(err!(e,
102 "{}: WebSocket handshake failed.", id;
103 IO, Network, Wire)),
104 };
105
106 ws.listen(
107 vhost_db,
108 ws_syntax,
109 Some(self.cfg.ws_ping_interval_secs),
110 self.cfg.server_max_errors_allowed,
111 id,
112 ).await
113 }
114}
115
116#[derive(Clone, Debug)]
117pub struct AppWebSocketHandler {
118 dev_manager: Option<Arc<DevRefreshManager>>,
119 sid: Option<String>,
120 term_manager: Option<Arc<TerminalManager>>,
121 // Whether this connection carries an authenticated operator (dashboard)
122 // session. The `term_*` management commands spawn, list, rename and kill
123 // terminal sessions, which is an operator capability, so they are gated on
124 // this. It defaults to `false` and is set only when the router has resolved
125 // an operator principal for the request: absent that, the commands refuse.
126 operator_authed: bool,
127}
128
129impl AppWebSocketHandler {
130
131 pub fn new(dev_manager: Option<Arc<DevRefreshManager>>) -> Self {
132 Self {
133 dev_manager,
134 sid: None,
135 term_manager: None,
136 operator_authed: false,
137 }
138 }
139
140 pub fn with_term_manager(mut self, tm: Arc<TerminalManager>) -> Self {
141 self.term_manager = Some(tm);
142 self
143 }
144
145
146 fn scoped_sess_key(&self, user_key: &str) -> Option<Dat> {
147 self.sid.as_ref().map(|sid| {
148 Dat::Str(fmt!("sess:{}:{}", sid, user_key))
149 })
150 }
151
152 fn sess_meta_key(&self) -> Option<Dat> {
153 self.sid.as_ref().map(|sid| {
154 Dat::Str(fmt!("sess_meta:{}", sid))
155 })
156 }
157
158 fn user_key(username: &str) -> Dat {
159 Dat::Str(fmt!("user:{}", username))
160 }
161
162 fn unix_secs_now() -> u64 {
163 std::time::SystemTime::now()
164 .duration_since(std::time::UNIX_EPOCH)
165 .map(|d| d.as_secs())
166 .unwrap_or(0)
167 }
168
169 const AUTH_KDF_NAME: &'static str = "Argon2id_v0x13";
170
171 fn response_text(
172 syntax: SyntaxRef,
173 cmd: &str,
174 vals: Vec<Dat>,
175 )
176 -> Outcome<Option<WebSocketMessage>>
177 {
178 let mut response = res!(MsgCmd::new(syntax, cmd));
179 for val in vals {
180 response = res!(response.add_cmd_val(val));
181 }
182 trace!("Sending websocket message '{}'", response.to_string());
183 return Ok(Some(WebSocketMessage::Text(response.to_string())));
184 }
185
186 fn check_syntax(
187 syntax: SyntaxRef,
188 msgcmd: &MsgCmd,
189 )
190 -> Outcome<()>
191 {
192 match syntax.get_cmd(&*msgcmd.name) {
193 Some(cmd) => {
194 let cmdcfg = cmd.config();
195 if !Val::count_fits(&cmdcfg.vals, msgcmd.vals.len()) {
196 return Err(err!(
197 "The syntax '{}' command '{}' expects {} value(s), found {}.",
198 syntax.config().name,
199 msgcmd.name,
200 cmdcfg.vals.len(),
201 msgcmd.vals.len();
202 Input, Network, Mismatch));
203 }
204 for (i, got) in msgcmd.vals.iter().enumerate() {
205 let kind = match Val::slot(&cmdcfg.vals, i) {
206 Some(val) => &val.kind,
207 None => continue,
208 };
209 if *kind != Kind::Unknown && *kind != got.kind() {
210 return Err(err!(
211 "The syntax '{}' command '{}' expects value {} to be a '{:?}, found {:?}.",
212 syntax.config().name,
213 msgcmd.name,
214 i,
215 kind,
216 got.kind();
217 Input, Network, Mismatch));
218 }
219 }
220 }
221 None => {
222 return Err(err!(
223 "No command '{}' found in syntax '{}'.",
224 msgcmd.name,
225 syntax.config().name;
226 Input, Network, Unknown));
227 }
228 }
229 Ok(())
230 }
231}
232
233/// `Syntax` accomodates multiple commands per message, we limit this to one here.
234impl WebSocketHandler for AppWebSocketHandler {
235
236 fn attach_sid(mut self, sid: Option<String>) -> Self {
237 self.sid = sid;
238 self
239 }
240
241 fn with_operator_authed(mut self, authed: bool) -> Self {
242 self.operator_authed = authed;
243 self
244 }
245
246 fn handle_text<
247 const UIDL: usize,
248 UID: NumIdDat<UIDL> + 'static,
249 ENC: Encrypter,
250 KH: Hasher,
251 DB: Database<UIDL, UID, ENC, KH>,
252 >(
253 &mut self,
254 txt: String,
255 db: Option<(Arc<RwLock<DB>>, UID)>,
256 syntax: SyntaxRef,
257 id: &String,
258 )
259 -> Outcome<Option<WebSocketMessage>>
260 {
261 // Redacted, because this line is every message a client sends and some of
262 // them carry a passphrase. See `redact`.
263 debug!("{}: AppWebSocketHandler received text message: '{}'", id, redact(&txt));
264
265 let msgrx = Msg::new(syntax.clone());
266 let msgrx = match msgrx.from_str(&txt, None) {
267 Err(err) => {
268 error!(err.clone());
269 return Self::response_text(syntax, "error", vec![dat!(err.to_string())]);
270 }
271 Ok(msgrx) => msgrx,
272 };
273
274 if msgrx.cmds.len() != 1 {
275 let err = err!(
276 "Expected one command from syntax '{}', found {}.",
277 syntax.config().name, msgrx.cmds.len();
278 Invalid, Network, Input);
279 error!(err.clone());
280 return Self::response_text(syntax, "error", vec![dat!(err.to_string())]);
281 }
282
283 if let Some((cmd_name, mut cmdrx)) = msgrx.cmds.into_iter().next() {
284 if let Err(err) = Self::check_syntax(syntax.clone(), &cmdrx) {
285 error!(err.clone());
286 return Self::response_text(syntax, "error", vec![dat!(err.to_string())]);
287 }
288 match cmd_name.as_str() {
289 // ┌───────────────────────┐
290 // │ DEVELOPMENT │
291 // └───────────────────────┘
292 "dev_ping" => {
293 trace!("Received dev_ping");
294 if let Some(_manager) = &self.dev_manager {
295 return Self::response_text(syntax, "info", vec![dat!("pong")]);
296 } else {
297 return Self::response_text(syntax, "error",
298 vec![dat!("Dev mode not enabled.")]);
299 }
300 }
301 "dev_connect" => {
302 trace!("Received dev_connect");
303 if self.dev_manager.is_some() {
304 return Self::response_text(syntax, "info", vec![dat!("connected")]);
305 } else {
306 trace!("Not in dev mode");
307 return Self::response_text(syntax, "error",
308 vec![dat!("Dev mode not enabled.")]);
309 }
310 }
311 // ┌───────────────────────┐
312 // │ GENERAL IO │
313 // └───────────────────────┘
314 "echo" => return Ok(Some(WebSocketMessage::Text(txt))),
315 // ┌───────────────────────┐
316 // │ DATABASE IO │
317 // └───────────────────────┘
318 "insert" => {
319 trace!("Received insert");
320 // Raw, unscoped write of any key in the vhost store. Its
321 // scoped siblings -- `sess_put` (session namespace) and
322 // `user_put` (the authenticated user's namespace) -- carry
323 // every legitimate client write, each confined to a prefix
324 // the caller owns. This one is confined to nothing, so it is
325 // an operator capability: absent an operator session it
326 // refuses, the same gate the `term_*` commands and the
327 // `/term/` bridge stand behind. Otherwise a stranger could
328 // overwrite `publish/admins`, a user's credential record, or
329 // any session's binding.
330 if !self.operator_authed {
331 return Self::response_text(syntax, "error",
332 vec![dat!("insert: an authenticated operator session \
333 is required; use sess_put or user_put for \
334 scoped writes.")]);
335 }
336 if let Some((ref db, uid)) = db {
337 let db = match db.write() {
338 Err(_err) => {
339 let err = err!(
340 "While trying to access database.";
341 Lock, Poisoned, Write);
342 error!(err.clone());
343 return Self::response_text(syntax,
344 "error", vec![dat!(err.to_string())]);
345 }
346 Ok(v) => v,
347 };
348 {
349 let k = std::mem::take(&mut cmdrx.vals[0]);
350 let v = std::mem::take(&mut cmdrx.vals[1]);
351 let success = fmt!("Inserted value for key {} into database.", k);
352 match db.insert(
353 k,
354 v,
355 uid,
356 None,
357 ) {
358 Err(err) => {
359 error!(err.clone());
360 return Self::response_text(syntax,
361 "error", vec![dat!(err.to_string())]);
362 }
363 Ok((exists, num_chunks)) => {
364 let exists_txt = if exists {
365 "exists"
366 } else {
367 "did not exist"
368 };
369 let txt = fmt!(
370 "{} The key {}, {} chunks were used.",
371 success, exists_txt, num_chunks,
372 );
373 return Self::response_text(syntax, "info", vec![dat!(txt)]);
374 }
375 }
376 }
377 }
378 let err = err!(
379 "Database not accessible for 'insert' command.";
380 Invalid, Network, Input);
381 error!(err.clone());
382 return Self::response_text(syntax, "error", vec![dat!(err.to_string())]);
383 }
384 "get_data" => {
385 // Raw, unscoped read of any key in the vhost store, the read
386 // counterpart of `insert` and gated the same way. `sess_get`
387 // and `user_get` read only the caller's own namespace; this
388 // reads anything, so absent an operator session it refuses,
389 // rather than hand a stranger another user's credential
390 // record or any session's binding.
391 if !self.operator_authed {
392 return Self::response_text(syntax, "error",
393 vec![dat!("get_data: an authenticated operator session \
394 is required; use sess_get or user_get for \
395 scoped reads.")]);
396 }
397 if let Some((ref db, _uid)) = db {
398 let db = match db.read() {
399 Err(_err) => {
400 let err = err!(
401 "While trying to access database.";
402 Lock, Poisoned, Read);
403 error!(err.clone());
404 return Self::response_text(syntax,
405 "error", vec![dat!(err.to_string())]);
406 }
407 Ok(v) => v,
408 };
409 match db.get(
410 &cmdrx.vals[0],
411 None,
412 ) {
413 Err(err) => {
414 error!(err.clone());
415 return Self::response_text(syntax,
416 "error", vec![dat!(err.to_string())]);
417 }
418 Ok(Some((data, _meta))) => {
419 return Self::response_text(syntax, "data", vec![dat!(data)]);
420 }
421 Ok(None) => {
422 return Self::response_text(syntax, "data", vec![Dat::Empty]);
423 }
424 }
425 }
426 let err = err!(
427 "Database not accessible for 'get_data' command.";
428 Invalid, Network, Input);
429 error!(err.clone());
430 return Self::response_text(syntax, "error", vec![dat!(err.to_string())]);
431 }
432 // ┌───────────────────────┐
433 // │ SESSION IO │
434 // └───────────────────────┘
435 "sess_get" => {
436 trace!("{}: sess_get", id);
437 // The key the client sent is a user-facing string; the
438 // server prefixes it with `sess:<sid>:` so clients can
439 // never read outside their own session namespace.
440 let user_key = match &cmdrx.vals[0] {
441 Dat::Str(s) => s.clone(),
442 other => {
443 let err = err!(
444 "sess_get: key must be a string, got {:?}.",
445 other.kind();
446 Invalid, Network, Input);
447 error!(err.clone());
448 return Self::response_text(syntax, "error",
449 vec![dat!(err.to_string())]);
450 }
451 };
452 let scoped = match self.scoped_sess_key(&user_key) {
453 Some(k) => k,
454 None => {
455 let err = err!(
456 "sess_get: no session cookie attached to \
457 this connection.";
458 Invalid, Network, Input);
459 return Self::response_text(syntax, "error",
460 vec![dat!(err.to_string())]);
461 }
462 };
463 if let Some((ref db, _uid)) = db {
464 let db = match db.read() {
465 Err(_err) => {
466 let err = err!(
467 "While trying to access database.";
468 Lock, Poisoned, Read);
469 error!(err.clone());
470 return Self::response_text(syntax,
471 "error", vec![dat!(err.to_string())]);
472 }
473 Ok(v) => v,
474 };
475 match db.get(&scoped, None) {
476 Err(err) => {
477 error!(err.clone());
478 return Self::response_text(syntax,
479 "error", vec![dat!(err.to_string())]);
480 }
481 Ok(Some((data, _meta))) => {
482 return Self::response_text(syntax, "data",
483 vec![dat!(data)]);
484 }
485 Ok(None) => {
486 return Self::response_text(syntax, "data",
487 vec![Dat::Empty]);
488 }
489 }
490 }
491 let err = err!(
492 "Database not accessible for 'sess_get' command.";
493 Invalid, Network, Input);
494 error!(err.clone());
495 return Self::response_text(syntax, "error",
496 vec![dat!(err.to_string())]);
497 }
498 "sess_put" => {
499 trace!("{}: sess_put", id);
500 let user_key = match std::mem::take(&mut cmdrx.vals[0]) {
501 Dat::Str(s) => s,
502 other => {
503 let err = err!(
504 "sess_put: key must be a string, got {:?}.",
505 other.kind();
506 Invalid, Network, Input);
507 return Self::response_text(syntax, "error",
508 vec![dat!(err.to_string())]);
509 }
510 };
511 let value = std::mem::take(&mut cmdrx.vals[1]);
512 let scoped = match self.scoped_sess_key(&user_key) {
513 Some(k) => k,
514 None => {
515 let err = err!(
516 "sess_put: no session cookie attached to \
517 this connection.";
518 Invalid, Network, Input);
519 return Self::response_text(syntax, "error",
520 vec![dat!(err.to_string())]);
521 }
522 };
523 if let Some((ref db, uid)) = db {
524 let db = match db.write() {
525 Err(_err) => {
526 let err = err!(
527 "While trying to access database.";
528 Lock, Poisoned, Write);
529 error!(err.clone());
530 return Self::response_text(syntax,
531 "error", vec![dat!(err.to_string())]);
532 }
533 Ok(v) => v,
534 };
535 match db.insert(scoped, value, uid, None) {
536 Err(err) => {
537 error!(err.clone());
538 return Self::response_text(syntax, "error",
539 vec![dat!(err.to_string())]);
540 }
541 Ok((exists, num_chunks)) => {
542 let msg = fmt!(
543 "sess_put ok, key {} exist, {} chunks.",
544 if exists { "did" } else { "did not" },
545 num_chunks,
546 );
547 return Self::response_text(syntax, "info",
548 vec![dat!(msg)]);
549 }
550 }
551 }
552 let err = err!(
553 "Database not accessible for 'sess_put' command.";
554 Invalid, Network, Input);
555 error!(err.clone());
556 return Self::response_text(syntax, "error",
557 vec![dat!(err.to_string())]);
558 }
559 // ┌───────────────────────┐
560 // │ AUTH │
561 // └───────────────────────┘
562 "register" => {
563 trace!("{}: register", id);
564 let username = match std::mem::take(&mut cmdrx.vals[0]) {
565 Dat::Str(s) => s,
566 other => {
567 return Self::response_text(syntax, "error",
568 vec![dat!(fmt!("register: username must be Str, got {:?}.",
569 other.kind()))]);
570 }
571 };
572 let passphrase = match std::mem::take(&mut cmdrx.vals[1]) {
573 Dat::Str(s) => s,
574 other => {
575 return Self::response_text(syntax, "error",
576 vec![dat!(fmt!("register: passphrase must be Str, got {:?}.",
577 other.kind()))]);
578 }
579 };
580 if username.is_empty() {
581 let err = err!(
582 "register: username must not be empty.";
583 Invalid, Network, Input);
584 return Self::response_text(syntax, "error",
585 vec![dat!(err.to_string())]);
586 }
587 let user_key = Self::user_key(&username);
588 // Guard against overwriting an existing user.
589 if let Some((ref db, _uid)) = db {
590 let db_r = match db.read() {
591 Err(_) => return Self::response_text(syntax, "error",
592 vec![dat!("register: database read lock poisoned.")]),
593 Ok(v) => v,
594 };
595 match db_r.get(&user_key, None) {
596 Ok(Some(_)) => {
597 return Self::response_text(syntax, "error",
598 vec![dat!(fmt!(
599 "register: user '{}' already exists.",
600 username))]);
601 }
602 Ok(None) => (),
603 Err(err) => {
604 return Self::response_text(syntax, "error",
605 vec![dat!(err.to_string())]);
606 }
607 }
608 } else {
609 return Self::response_text(syntax, "error",
610 vec![dat!("register: no database available.")]);
611 }
612 // Derive the Argon2id hash of the passphrase.
613 let mut kdf = match KeyDerivationScheme::from_str(Self::AUTH_KDF_NAME) {
614 Ok(k) => k,
615 Err(err) => {
616 return Self::response_text(syntax, "error",
617 vec![dat!(err.to_string())]);
618 }
619 };
620 if let Err(err) = kdf.derive(passphrase.as_bytes()) {
621 return Self::response_text(syntax, "error",
622 vec![dat!(err.to_string())]);
623 }
624 let kdf_hash = match kdf.encode_to_string() {
625 Ok(s) => s,
626 Err(err) => {
627 return Self::response_text(syntax, "error",
628 vec![dat!(err.to_string())]);
629 }
630 };
631 // Build the user record.
632 let mut rec = DaticleMap::new();
633 rec.insert(dat!("kdf_name"), dat!(fmt!("{}", kdf)));
634 rec.insert(dat!("kdf_hash"), dat!(kdf_hash));
635 rec.insert(dat!("created_at"), Dat::U64(Self::unix_secs_now()));
636 let record = Dat::Map(rec);
637 // Write it.
638 if let Some((ref db, uid)) = db {
639 let db_w = match db.write() {
640 Err(_) => return Self::response_text(syntax, "error",
641 vec![dat!("register: database write lock poisoned.")]),
642 Ok(v) => v,
643 };
644 match db_w.insert(user_key, record, uid, None) {
645 Ok(_) => {
646 return Self::response_text(syntax, "info",
647 vec![dat!(fmt!(
648 "register: user '{}' created.", username))]);
649 }
650 Err(err) => {
651 return Self::response_text(syntax, "error",
652 vec![dat!(err.to_string())]);
653 }
654 }
655 }
656 return Self::response_text(syntax, "error",
657 vec![dat!("register: no database available.")]);
658 }
659 "login" => {
660 trace!("{}: login", id);
661 let sid = match self.sid.clone() {
662 Some(s) => s,
663 None => {
664 return Self::response_text(syntax, "error",
665 vec![dat!("login: no session cookie attached.")]);
666 }
667 };
668 let username = match std::mem::take(&mut cmdrx.vals[0]) {
669 Dat::Str(s) => s,
670 _ => {
671 return Self::response_text(syntax, "error",
672 vec![dat!("login: username must be a string.")]);
673 }
674 };
675 let passphrase = match std::mem::take(&mut cmdrx.vals[1]) {
676 Dat::Str(s) => s,
677 _ => {
678 return Self::response_text(syntax, "error",
679 vec![dat!("login: passphrase must be a string.")]);
680 }
681 };
682 let user_key = Self::user_key(&username);
683 let rec = if let Some((ref db, _)) = db {
684 let db_r = match db.read() {
685 Err(_) => return Self::response_text(syntax, "error",
686 vec![dat!("login: database read lock poisoned.")]),
687 Ok(v) => v,
688 };
689 match db_r.get(&user_key, None) {
690 Ok(Some((data, _meta))) => data,
691 Ok(None) => {
692 return Self::response_text(syntax, "error",
693 vec![dat!("login: invalid credentials.")]);
694 }
695 Err(err) => {
696 return Self::response_text(syntax, "error",
697 vec![dat!(err.to_string())]);
698 }
699 }
700 } else {
701 return Self::response_text(syntax, "error",
702 vec![dat!("login: no database available.")]);
703 };
704 // Extract kdf_name and kdf_hash from the stored record.
705 let (kdf_name, kdf_hash) = match &rec {
706 Dat::Map(m) => {
707 let name = match m.get(&dat!("kdf_name")) {
708 Some(Dat::Str(s)) => s.clone(),
709 _ => return Self::response_text(syntax, "error",
710 vec![dat!("login: malformed user record (kdf_name).")]),
711 };
712 let hash = match m.get(&dat!("kdf_hash")) {
713 Some(Dat::Str(s)) => s.clone(),
714 _ => return Self::response_text(syntax, "error",
715 vec![dat!("login: malformed user record (kdf_hash).")]),
716 };
717 (name, hash)
718 }
719 _ => return Self::response_text(syntax, "error",
720 vec![dat!("login: malformed user record (not a Map).")]),
721 };
722 // Rebuild the KDF and verify the passphrase.
723 let mut kdf = match KeyDerivationScheme::from_str(&kdf_name) {
724 Ok(k) => k,
725 Err(err) => return Self::response_text(syntax, "error",
726 vec![dat!(err.to_string())]),
727 };
728 if let Err(err) = kdf.decode_from_string(&kdf_hash) {
729 return Self::response_text(syntax, "error",
730 vec![dat!(err.to_string())]);
731 }
732 let ok = match kdf.verify(passphrase.as_bytes()) {
733 Ok(b) => b,
734 Err(err) => return Self::response_text(syntax, "error",
735 vec![dat!(err.to_string())]),
736 };
737 if !ok {
738 return Self::response_text(syntax, "error",
739 vec![dat!("login: invalid credentials.")]);
740 }
741 // Bind the session to the user.
742 let meta_key = Dat::Str(fmt!("sess_meta:{}", sid));
743 let mut meta = DaticleMap::new();
744 meta.insert(dat!("user"), dat!(username.clone()));
745 meta.insert(dat!("authenticated_at"),
746 Dat::U64(Self::unix_secs_now()));
747 let meta_rec = Dat::Map(meta);
748 if let Some((ref db, uid)) = db {
749 let db_w = match db.write() {
750 Err(_) => return Self::response_text(syntax, "error",
751 vec![dat!("login: database write lock poisoned.")]),
752 Ok(v) => v,
753 };
754 match db_w.insert(meta_key, meta_rec, uid, None) {
755 Ok(_) => {
756 return Self::response_text(syntax, "info",
757 vec![dat!(fmt!(
758 "login: authenticated as '{}'.", username))]);
759 }
760 Err(err) => {
761 return Self::response_text(syntax, "error",
762 vec![dat!(err.to_string())]);
763 }
764 }
765 }
766 return Self::response_text(syntax, "error",
767 vec![dat!("login: no database available.")]);
768 }
769 "logout" => {
770 trace!("{}: logout", id);
771 let meta_key = match self.sess_meta_key() {
772 Some(k) => k,
773 None => {
774 return Self::response_text(syntax, "error",
775 vec![dat!("logout: no session cookie attached.")]);
776 }
777 };
778 // Overwrite the session metadata with an empty map,
779 // marking the session as unauthenticated. Ozone does
780 // not currently expose a delete primitive on this
781 // surface; an empty record is treated as "not bound".
782 let empty_rec = Dat::Map(DaticleMap::new());
783 if let Some((ref db, uid)) = db {
784 let db_w = match db.write() {
785 Err(_) => return Self::response_text(syntax, "error",
786 vec![dat!("logout: database write lock poisoned.")]),
787 Ok(v) => v,
788 };
789 match db_w.insert(meta_key, empty_rec, uid, None) {
790 Ok(_) => {
791 return Self::response_text(syntax, "info",
792 vec![dat!("logout: session unbound.")]);
793 }
794 Err(err) => {
795 return Self::response_text(syntax, "error",
796 vec![dat!(err.to_string())]);
797 }
798 }
799 }
800 return Self::response_text(syntax, "error",
801 vec![dat!("logout: no database available.")]);
802 }
803 // ┌───────────────────────┐
804 // │ USER IO │
805 // └───────────────────────┘
806 "user_get" => {
807 trace!("{}: user_get", id);
808 let user_sub_key = match &cmdrx.vals[0] {
809 Dat::Str(s) => s.clone(),
810 other => {
811 return Self::response_text(syntax, "error",
812 vec![dat!(fmt!(
813 "user_get: key must be a string, got {:?}.",
814 other.kind()))]);
815 }
816 };
817 // Look up the session's authenticated user.
818 let meta_key = match self.sess_meta_key() {
819 Some(k) => k,
820 None => {
821 return Self::response_text(syntax, "error",
822 vec![dat!("user_get: no session cookie attached.")]);
823 }
824 };
825 let username = if let Some((ref db, _)) = db {
826 let db_r = match db.read() {
827 Err(_) => return Self::response_text(syntax, "error",
828 vec![dat!("user_get: database read lock poisoned.")]),
829 Ok(v) => v,
830 };
831 match db_r.get(&meta_key, None) {
832 Ok(Some((Dat::Map(m), _))) => match m.get(&dat!("user")) {
833 Some(Dat::Str(s)) if !s.is_empty() => s.clone(),
834 _ => return Self::response_text(syntax, "error",
835 vec![dat!("user_get: session is not authenticated.")]),
836 },
837 Ok(_) => return Self::response_text(syntax, "error",
838 vec![dat!("user_get: session is not authenticated.")]),
839 Err(err) => return Self::response_text(syntax, "error",
840 vec![dat!(err.to_string())]),
841 }
842 } else {
843 return Self::response_text(syntax, "error",
844 vec![dat!("user_get: no database available.")]);
845 };
846 // Read the user-scoped key.
847 let scoped = Dat::Str(fmt!("user:{}:{}", username, user_sub_key));
848 if let Some((ref db, _)) = db {
849 let db_r = match db.read() {
850 Err(_) => return Self::response_text(syntax, "error",
851 vec![dat!("user_get: database read lock poisoned.")]),
852 Ok(v) => v,
853 };
854 match db_r.get(&scoped, None) {
855 Ok(Some((data, _))) => {
856 return Self::response_text(syntax, "data",
857 vec![dat!(data)]);
858 }
859 Ok(None) => {
860 return Self::response_text(syntax, "data",
861 vec![Dat::Empty]);
862 }
863 Err(err) => {
864 return Self::response_text(syntax, "error",
865 vec![dat!(err.to_string())]);
866 }
867 }
868 }
869 return Self::response_text(syntax, "error",
870 vec![dat!("user_get: no database available.")]);
871 }
872 "user_put" => {
873 trace!("{}: user_put", id);
874 let user_sub_key = match std::mem::take(&mut cmdrx.vals[0]) {
875 Dat::Str(s) => s,
876 other => {
877 return Self::response_text(syntax, "error",
878 vec![dat!(fmt!(
879 "user_put: key must be a string, got {:?}.",
880 other.kind()))]);
881 }
882 };
883 let value = std::mem::take(&mut cmdrx.vals[1]);
884 // Same authentication lookup as user_get.
885 let meta_key = match self.sess_meta_key() {
886 Some(k) => k,
887 None => {
888 return Self::response_text(syntax, "error",
889 vec![dat!("user_put: no session cookie attached.")]);
890 }
891 };
892 let username = if let Some((ref db, _)) = db {
893 let db_r = match db.read() {
894 Err(_) => return Self::response_text(syntax, "error",
895 vec![dat!("user_put: database read lock poisoned.")]),
896 Ok(v) => v,
897 };
898 match db_r.get(&meta_key, None) {
899 Ok(Some((Dat::Map(m), _))) => match m.get(&dat!("user")) {
900 Some(Dat::Str(s)) if !s.is_empty() => s.clone(),
901 _ => return Self::response_text(syntax, "error",
902 vec![dat!("user_put: session is not authenticated.")]),
903 },
904 Ok(_) => return Self::response_text(syntax, "error",
905 vec![dat!("user_put: session is not authenticated.")]),
906 Err(err) => return Self::response_text(syntax, "error",
907 vec![dat!(err.to_string())]),
908 }
909 } else {
910 return Self::response_text(syntax, "error",
911 vec![dat!("user_put: no database available.")]);
912 };
913 let scoped = Dat::Str(fmt!("user:{}:{}", username, user_sub_key));
914 if let Some((ref db, uid)) = db {
915 let db_w = match db.write() {
916 Err(_) => return Self::response_text(syntax, "error",
917 vec![dat!("user_put: database write lock poisoned.")]),
918 Ok(v) => v,
919 };
920 match db_w.insert(scoped, value, uid, None) {
921 Ok((exists, num_chunks)) => {
922 let msg = fmt!(
923 "user_put ok, key {} exist, {} chunks.",
924 if exists { "did" } else { "did not" },
925 num_chunks,
926 );
927 return Self::response_text(syntax, "info",
928 vec![dat!(msg)]);
929 }
930 Err(err) => {
931 return Self::response_text(syntax, "error",
932 vec![dat!(err.to_string())]);
933 }
934 }
935 }
936 return Self::response_text(syntax, "error",
937 vec![dat!("user_put: no database available.")]);
938 }
939 "whoami" => {
940 trace!("{}: whoami", id);
941 // With no session cookie at all, report unauthenticated
942 // and stop. This is the expected state for a fresh
943 // connection that has not yet been through the anonymous
944 // session-issuance path.
945 let meta_key = match self.sess_meta_key() {
946 Some(k) => k,
947 None => {
948 return Self::response_text(syntax, "data",
949 vec![res!(whoami_dat(None))]);
950 }
951 };
952 if let Some((ref db, _)) = db {
953 let db_r = match db.read() {
954 Err(_) => return Self::response_text(syntax, "error",
955 vec![dat!("whoami: database read lock poisoned.")]),
956 Ok(v) => v,
957 };
958 match db_r.get(&meta_key, None) {
959 Ok(Some((data, _))) => {
960 let user_opt = match &data {
961 Dat::Map(m) => match m.get(&dat!("user")) {
962 Some(Dat::Str(s)) if !s.is_empty() => {
963 Some(s.clone())
964 }
965 _ => None,
966 },
967 _ => None,
968 };
969 return Self::response_text(syntax, "data",
970 vec![res!(whoami_dat(user_opt))]);
971 }
972 Ok(None) => {
973 return Self::response_text(syntax, "data",
974 vec![res!(whoami_dat(None))]);
975 }
976 Err(err) => {
977 return Self::response_text(syntax, "error",
978 vec![dat!(err.to_string())]);
979 }
980 }
981 }
982 return Self::response_text(syntax, "error",
983 vec![dat!("whoami: no database available.")]);
984 }
985 // ┌───────────────────────┐
986 // │ TERMINAL │
987 // └───────────────────────┘
988 "term_new" => {
989 trace!("{}: term_new", id);
990 if !self.operator_authed {
991 return Self::response_text(syntax, "error",
992 vec![dat!("term_new: an authenticated operator session \
993 is required.")]);
994 }
995 let tm = match &self.term_manager {
996 Some(t) => t.clone(),
997 None => return Self::response_text(syntax, "error",
998 vec![dat!("term_new: terminal features not enabled.")]),
999 };
1000 match tm.new_session() {
1001 Ok(name) => {
1002 let mut m = DaticleMap::new();
1003 m.insert(dat!("name"), dat!(name));
1004 return Self::response_text(syntax, "data",
1005 vec![Dat::Map(m)]);
1006 }
1007 Err(e) => return Self::response_text(syntax, "error",
1008 vec![dat!(e.to_string())]),
1009 }
1010 }
1011 "term_list" => {
1012 trace!("{}: term_list", id);
1013 if !self.operator_authed {
1014 return Self::response_text(syntax, "error",
1015 vec![dat!("term_list: an authenticated operator session \
1016 is required.")]);
1017 }
1018 let tm = match &self.term_manager {
1019 Some(t) => t.clone(),
1020 None => return Self::response_text(syntax, "error",
1021 vec![dat!("term_list: terminal features not enabled.")]),
1022 };
1023 match tm.list_sessions_dat() {
1024 Ok(dat) => return Self::response_text(syntax, "data",
1025 vec![dat]),
1026 Err(e) => return Self::response_text(syntax, "error",
1027 vec![dat!(e.to_string())]),
1028 }
1029 }
1030 "term_close" => {
1031 trace!("{}: term_close", id);
1032 if !self.operator_authed {
1033 return Self::response_text(syntax, "error",
1034 vec![dat!("term_close: an authenticated operator session \
1035 is required.")]);
1036 }
1037 let tm = match &self.term_manager {
1038 Some(t) => t.clone(),
1039 None => return Self::response_text(syntax, "error",
1040 vec![dat!("term_close: terminal features not enabled.")]),
1041 };
1042 let name = match std::mem::take(&mut cmdrx.vals[0]) {
1043 Dat::Str(s) => s,
1044 _ => return Self::response_text(syntax, "error",
1045 vec![dat!("term_close: session name must be a string.")]),
1046 };
1047 match tm.close_session(&name) {
1048 Ok(()) => return Self::response_text(syntax, "info",
1049 vec![dat!(fmt!("term_close: session '{}' closed.", name))]),
1050 Err(e) => return Self::response_text(syntax, "error",
1051 vec![dat!(e.to_string())]),
1052 }
1053 }
1054 "term_set_name" => {
1055 trace!("{}: term_set_name", id);
1056 if !self.operator_authed {
1057 return Self::response_text(syntax, "error",
1058 vec![dat!("term_set_name: an authenticated operator session \
1059 is required.")]);
1060 }
1061 let tm = match &self.term_manager {
1062 Some(t) => t.clone(),
1063 None => return Self::response_text(syntax, "error",
1064 vec![dat!("term_set_name: terminal features not enabled.")]),
1065 };
1066 let old = match std::mem::take(&mut cmdrx.vals[0]) {
1067 Dat::Str(s) => s,
1068 _ => return Self::response_text(syntax, "error",
1069 vec![dat!("term_set_name: old name must be a string.")]),
1070 };
1071 let new = match std::mem::take(&mut cmdrx.vals[1]) {
1072 Dat::Str(s) => s,
1073 _ => return Self::response_text(syntax, "error",
1074 vec![dat!("term_set_name: new name must be a string.")]),
1075 };
1076 match tm.set_session_name(&old, &new) {
1077 Ok(()) => return Self::response_text(syntax, "info",
1078 vec![dat!(fmt!("term_set_name: '{}' -> '{}'.", old, new))]),
1079 Err(e) => return Self::response_text(syntax, "error",
1080 vec![dat!(e.to_string())]),
1081 }
1082 }
1083 // ┌───────────────────────┐
1084 // │ AUTH — change_pass │
1085 // └───────────────────────┘
1086 "change_pass" => {
1087 trace!("{}: change_pass", id);
1088 let sid = match self.sid.clone() {
1089 Some(s) => s,
1090 None => return Self::response_text(syntax, "error",
1091 vec![dat!("change_pass: no session cookie attached.")]),
1092 };
1093 let old_pass = match std::mem::take(&mut cmdrx.vals[0]) {
1094 Dat::Str(s) => s,
1095 _ => return Self::response_text(syntax, "error",
1096 vec![dat!("change_pass: old passphrase must be a string.")]),
1097 };
1098 let new_pass = match std::mem::take(&mut cmdrx.vals[1]) {
1099 Dat::Str(s) => s,
1100 _ => return Self::response_text(syntax, "error",
1101 vec![dat!("change_pass: new passphrase must be a string.")]),
1102 };
1103 // Look up the session's bound user.
1104 let meta_key = Dat::Str(fmt!("sess_meta:{}", sid));
1105 let (db, uid) = match &db {
1106 Some(pair) => pair,
1107 None => return Self::response_text(syntax, "error",
1108 vec![dat!("change_pass: no database available.")]),
1109 };
1110 let username = {
1111 let db_r = match db.read() {
1112 Err(_) => return Self::response_text(syntax, "error",
1113 vec![dat!("change_pass: database read lock poisoned.")]),
1114 Ok(v) => v,
1115 };
1116 match db_r.get(&meta_key, None) {
1117 Ok(Some((data, _))) => match &data {
1118 Dat::Map(m) => match m.get(&dat!("user")) {
1119 Some(Dat::Str(s)) if !s.is_empty() => s.clone(),
1120 _ => return Self::response_text(syntax, "error",
1121 vec![dat!("change_pass: session not authenticated.")]),
1122 },
1123 _ => return Self::response_text(syntax, "error",
1124 vec![dat!("change_pass: session not authenticated.")]),
1125 },
1126 _ => return Self::response_text(syntax, "error",
1127 vec![dat!("change_pass: session not authenticated.")]),
1128 }
1129 };
1130 // Fetch and verify old passphrase.
1131 let user_key = Self::user_key(&username);
1132 let (kdf_name, kdf_hash) = {
1133 let db_r = match db.read() {
1134 Err(_) => return Self::response_text(syntax, "error",
1135 vec![dat!("change_pass: database read lock poisoned.")]),
1136 Ok(v) => v,
1137 };
1138 match db_r.get(&user_key, None) {
1139 Ok(Some((data, _))) => match &data {
1140 Dat::Map(m) => {
1141 let name = match m.get(&dat!("kdf_name")) {
1142 Some(Dat::Str(s)) => s.clone(),
1143 _ => return Self::response_text(syntax, "error",
1144 vec![dat!("change_pass: malformed user record (kdf_name).")]),
1145 };
1146 let hash = match m.get(&dat!("kdf_hash")) {
1147 Some(Dat::Str(s)) => s.clone(),
1148 _ => return Self::response_text(syntax, "error",
1149 vec![dat!("change_pass: malformed user record (kdf_hash).")]),
1150 };
1151 (name, hash)
1152 }
1153 _ => return Self::response_text(syntax, "error",
1154 vec![dat!("change_pass: malformed user record (not a Map).")]),
1155 },
1156 _ => return Self::response_text(syntax, "error",
1157 vec![dat!("change_pass: user record not found.")]),
1158 }
1159 };
1160 let mut kdf = match KeyDerivationScheme::from_str(&kdf_name) {
1161 Ok(k) => k,
1162 Err(e) => return Self::response_text(syntax, "error",
1163 vec![dat!(e.to_string())]),
1164 };
1165 if let Err(e) = kdf.decode_from_string(&kdf_hash) {
1166 return Self::response_text(syntax, "error",
1167 vec![dat!(e.to_string())]);
1168 }
1169 let ok = match kdf.verify(old_pass.as_bytes()) {
1170 Ok(b) => b,
1171 Err(e) => return Self::response_text(syntax, "error",
1172 vec![dat!(e.to_string())]),
1173 };
1174 if !ok {
1175 return Self::response_text(syntax, "error",
1176 vec![dat!("change_pass: old passphrase incorrect.")]);
1177 }
1178 // Derive new hash.
1179 let mut new_kdf = match KeyDerivationScheme::from_str(Self::AUTH_KDF_NAME) {
1180 Ok(k) => k,
1181 Err(e) => return Self::response_text(syntax, "error",
1182 vec![dat!(e.to_string())]),
1183 };
1184 if let Err(e) = new_kdf.derive(new_pass.as_bytes()) {
1185 return Self::response_text(syntax, "error",
1186 vec![dat!(e.to_string())]);
1187 }
1188 let new_hash = match new_kdf.encode_to_string() {
1189 Ok(s) => s,
1190 Err(e) => return Self::response_text(syntax, "error",
1191 vec![dat!(e.to_string())]),
1192 };
1193 // Build updated record and write.
1194 let mut rec = DaticleMap::new();
1195 rec.insert(dat!("kdf_name"), dat!(fmt!("{}", new_kdf)));
1196 rec.insert(dat!("kdf_hash"), dat!(new_hash));
1197 // Preserve created_at from old record.
1198 {
1199 let db_r = match db.read() {
1200 Err(_) => return Self::response_text(syntax, "error",
1201 vec![dat!("change_pass: database read lock poisoned.")]),
1202 Ok(v) => v,
1203 };
1204 if let Ok(Some((old_data, _))) = db_r.get(&user_key, None) {
1205 if let Dat::Map(m) = &old_data {
1206 if let Some(v) = m.get(&dat!("created_at")) {
1207 rec.insert(dat!("created_at"), v.clone());
1208 }
1209 }
1210 }
1211 }
1212 let record = Dat::Map(rec);
1213 let db_w = match db.write() {
1214 Err(_) => return Self::response_text(syntax, "error",
1215 vec![dat!("change_pass: database write lock poisoned.")]),
1216 Ok(v) => v,
1217 };
1218 match db_w.insert(user_key, record, *uid, None) {
1219 Ok(_) => return Self::response_text(syntax, "info",
1220 vec![dat!("change_pass: passphrase updated.")]),
1221 Err(e) => return Self::response_text(syntax, "error",
1222 vec![dat!(e.to_string())]),
1223 }
1224 }
1225 _ => {}
1226 }
1227 }
1228 unreachable!()
1229 }
1230
1231 fn handle_binary<
1232 const UIDL: usize,
1233 UID: NumIdDat<UIDL> + 'static,
1234 ENC: Encrypter,
1235 KH: Hasher,
1236 DB: Database<UIDL, UID, ENC, KH>,
1237 >(
1238 &mut self,
1239 byts: Vec<u8>,
1240 _db: Option<(Arc<RwLock<DB>>, UID)>,
1241 _syntax: SyntaxRef,
1242 id: &String,
1243 )
1244 -> Outcome<Option<WebSocketMessage>>
1245 {
1246 debug!("{}: AppWebSocketHandler received binary message of length {}: {:02x?}",
1247 id, byts.len(), byts);
1248 let response = WebSocketMessage::Binary(byts); // Echo.
1249 Ok(Some(response))
1250 }
1251
1252 fn dev_receiver(&self, id: &String) -> Outcome<Option<broadcast::Receiver<()>>> {
1253 if let Some(manager) = &self.dev_manager {
1254 debug!("{}: New client subscribed to dev refresh notifications.", id);
1255 Ok(Some(manager.get_receiver()))
1256 } else {
1257 debug!("{}: No dev receiver available to accept client refresh messages.", id);
1258 Ok(None)
1259 }
1260 }
1261}
1262
1263fn whoami_dat(user_opt: Option<String>) -> Outcome<Dat> {
1264 let mut m = DaticleMap::new();
1265 m.insert(dat!("authenticated"), Dat::Bool(user_opt.is_some()));
1266 if let Some(u) = user_opt {
1267 m.insert(dat!("user"), dat!(u));
1268 }
1269 Ok(dat!(res!(Dat::Map(m).json())))
1270}
1271
1272#[cfg(test)]
1273mod whoami_tests {
1274 use super::*;
1275
1276 use oxedyne_fe2o3_jdat::{
1277 string::dec::DecoderConfig,
1278 usr::{
1279 UsrKind,
1280 UsrKindCode,
1281 UsrKindId,
1282 },
1283 };
1284
1285 use std::collections::BTreeMap;
1286
1287 #[test]
1288 fn test_a_signed_in_session_is_json_00() -> Outcome<()> {
1289 let user = "eb00b174d02fc1ad";
1290 let dat = res!(whoami_dat(Some(user.to_string())));
1291 let txt = match &dat {
1292 Dat::Str(s) => s.clone(),
1293 other => return Err(err!(
1294 "Expected a string holding JSON, got {:?}.", other;
1295 Test, Invalid, Mismatch)),
1296 };
1297 // The kind wrapper is what the client cannot read: `(true)` is jdat, `true` is JSON.
1298 assert!(!txt.contains("(true)"), "The bool kept its jdat kind: {}", txt);
1299 assert!(txt.contains("\"authenticated\""), "No authenticated member: {}", txt);
1300 assert!(txt.contains("true"), "Not reported as authenticated: {}", txt);
1301 assert!(txt.contains(user), "The user did not survive: {}", txt);
1302 Ok(())
1303 }
1304
1305 #[test]
1306 fn test_a_signed_out_session_names_nobody_01() -> Outcome<()> {
1307 let dat = res!(whoami_dat(None));
1308 let txt = match &dat {
1309 Dat::Str(s) => s.clone(),
1310 other => return Err(err!(
1311 "Expected a string holding JSON, got {:?}.", other;
1312 Test, Invalid, Mismatch)),
1313 };
1314 assert!(!txt.contains("(false)"), "The bool kept its jdat kind: {}", txt);
1315 assert!(txt.contains("false"), "Not reported as unauthenticated: {}", txt);
1316 assert!(!txt.contains("\"user\""), "Named a user with nobody signed in: {}", txt);
1317 Ok(())
1318 }
1319
1320 #[test]
1321 fn test_the_answer_parses_as_json_02() -> Outcome<()> {
1322 for user_opt in [Some("abc123".to_string()), None] {
1323 let signed_in = user_opt.is_some();
1324 let dat = res!(whoami_dat(user_opt));
1325 let txt = match &dat {
1326 Dat::Str(s) => s.clone(),
1327 other => return Err(err!(
1328 "Expected a string holding JSON, got {:?}.", other;
1329 Test, Invalid, Mismatch)),
1330 };
1331 let cfg: DecoderConfig<
1332 BTreeMap<UsrKindCode, UsrKind>,
1333 BTreeMap<String, UsrKindId>,
1334 > = DecoderConfig::json(None);
1335 let back = res!(Dat::decode_string_with_config(txt.clone(), &cfg));
1336 let m = match back {
1337 Dat::Map(m) => m,
1338 other => return Err(err!(
1339 "Expected a JSON object, got {:?} from {}.", other, txt;
1340 Test, Invalid, Mismatch)),
1341 };
1342 match m.get(&dat!("authenticated")) {
1343 Some(Dat::Bool(b)) => assert_eq!(*b, signed_in,
1344 "Wrong authenticated value in {}.", txt),
1345 other => return Err(err!(
1346 "Expected a bool for authenticated, got {:?} from {}.", other, txt;
1347 Test, Invalid, Mismatch)),
1348 }
1349 }
1350 Ok(())
1351 }
1352}
1353
1354fn redact(txt: &str) -> String {
1355 let name = match txt.split_whitespace().next() {
1356 Some(n) => n,
1357 None => return txt.to_string(),
1358 };
1359 match name {
1360 "register" | "login" => fmt!("{} <redacted>", name),
1361 _ => txt.to_string(),
1362 }
1363}
1364
1365#[cfg(test)]
1366mod redact_tests {
1367 use super::*;
1368
1369 #[test]
1370 fn test_a_passphrase_is_not_logged_00() -> Outcome<()> {
1371 let secret = "correct horse battery staple";
1372 for cmd in ["login", "register"] {
1373 let line = fmt!("{} \"abc123\" \"{}\"", cmd, secret);
1374 let out = redact(&line);
1375 assert!(!out.contains(secret), "{} leaked the passphrase: {}", cmd, out);
1376 assert!(!out.contains("abc123"), "{} leaked the username: {}", cmd, out);
1377 assert!(out.starts_with(cmd), "{} lost its name: {}", cmd, out);
1378 }
1379 Ok(())
1380 }
1381
1382 #[test]
1383 fn test_the_rest_is_logged_whole_01() -> Outcome<()> {
1384 for line in [
1385 "sess_get \"cart\"",
1386 "user_put \"cart\" \"{}\"",
1387 "whoami",
1388 "logout",
1389 ] {
1390 assert_eq!(redact(line), line);
1391 }
1392 Ok(())
1393 }
1394
1395 #[test]
1396 fn test_odd_input_is_survived_02() -> Outcome<()> {
1397 assert_eq!(redact(""), "");
1398 assert_eq!(redact(" "), " ");
1399 assert_eq!(redact("login"), "login <redacted>");
1400 Ok(())
1401 }
1402}