Skip to main content

max / mnw-cli

4.3 KB · 131 lines History Blame Raw
1 //! MNW CLI — SSH-based TUI for the Makenot.work creator platform.
2 //!
3 //! Runs a russh SSH server that authenticates creators by their registered
4 //! SSH public keys (via the MNW internal API) and presents a ratatui TUI
5 //! for managing projects, items, uploads, and analytics.
6
7 mod api;
8 mod commands;
9 mod config;
10 mod format;
11 mod ssh;
12 mod staging;
13 mod tui;
14
15 use std::sync::Arc;
16
17 use russh::keys::{self, Algorithm, PrivateKey, ssh_key};
18 use russh::server::Server as _;
19 use russh::MethodKind;
20 use tokio::signal;
21 use tracing_subscriber::EnvFilter;
22
23 #[tokio::main]
24 async fn main() -> anyhow::Result<()> {
25 tracing_subscriber::fmt()
26 .with_env_filter(EnvFilter::from_default_env().add_directive("mnw_cli=info".parse()?))
27 .init();
28
29 let config = config::Config::from_env()?;
30
31 // Ensure staging base directory exists
32 std::fs::create_dir_all(&config.staging_dir)?;
33 tracing::info!(staging_dir = %config.staging_dir.display(), "staging directory ready");
34
35 // Spawn hourly cleanup task for stale staging files (24h TTL)
36 let cleanup_dir = config.staging_dir.clone();
37 tokio::spawn(async move {
38 let ttl = std::time::Duration::from_secs(24 * 60 * 60);
39 let mut interval = tokio::time::interval(std::time::Duration::from_secs(60 * 60));
40 loop {
41 interval.tick().await;
42 staging::cleanup_stale(&cleanup_dir, ttl).await;
43 }
44 });
45
46 // Load or generate host key
47 let host_key = load_or_generate_host_key(&config.host_key_path)?;
48
49 tracing::info!(
50 port = config.port,
51 api_url = %config.api_url,
52 host_key = %config.host_key_path.display(),
53 "starting MNW CLI SSH server"
54 );
55
56 let mut methods = russh::MethodSet::empty();
57 methods.push(MethodKind::PublicKey);
58
59 let ssh_config = russh::server::Config {
60 methods,
61 keys: vec![host_key],
62 auth_rejection_time: std::time::Duration::from_secs(1),
63 auth_rejection_time_initial: Some(std::time::Duration::from_millis(0)),
64 ..Default::default()
65 };
66
67 let staging_dir = Arc::new(config.staging_dir);
68 let api_client = api::MnwApiClient::new(config.api_url, config.service_token);
69 let mut server = ssh::MnwServer::new(api_client, staging_dir, config.git_user);
70
71 let addr = format!("0.0.0.0:{}", config.port);
72 tracing::info!(%addr, "listening for SSH connections");
73
74 // Run SSH server with graceful shutdown on SIGTERM/SIGINT
75 tokio::select! {
76 result = server.run_on_address(Arc::new(ssh_config), addr) => {
77 result?;
78 }
79 _ = shutdown_signal() => {
80 tracing::info!("shutdown signal received, stopping");
81 }
82 }
83
84 tracing::info!("MNW CLI server stopped");
85 Ok(())
86 }
87
88 async fn shutdown_signal() {
89 let ctrl_c = signal::ctrl_c();
90 #[cfg(unix)]
91 let mut sigterm = signal::unix::signal(signal::unix::SignalKind::terminate())
92 .expect("failed to register SIGTERM handler");
93 #[cfg(unix)]
94 tokio::select! {
95 _ = ctrl_c => {}
96 _ = sigterm.recv() => {}
97 }
98 #[cfg(not(unix))]
99 ctrl_c.await.ok();
100 }
101
102 /// Load an ed25519 host key from disk, or generate and save one if it doesn't exist.
103 fn load_or_generate_host_key(path: &std::path::Path) -> anyhow::Result<PrivateKey> {
104 if path.exists() {
105 tracing::info!(path = %path.display(), "loading host key");
106 let key = keys::load_secret_key(path, None)?;
107 Ok(key)
108 } else {
109 tracing::info!(path = %path.display(), "generating new ed25519 host key");
110 // Use the rand_core OsRng re-exported through russh's ssh_key dependency
111 // to avoid version conflicts with the standalone rand crate.
112 let key = ssh_key::private::PrivateKey::random(
113 &mut keys::signature::rand_core::OsRng,
114 Algorithm::Ed25519,
115 )?;
116 // Save to disk in OpenSSH format
117 let pem = key.to_openssh(ssh_key::LineEnding::LF)?;
118 if let Some(parent) = path.parent() {
119 std::fs::create_dir_all(parent)?;
120 }
121 std::fs::write(path, pem.as_bytes())?;
122 #[cfg(unix)]
123 {
124 use std::os::unix::fs::PermissionsExt;
125 std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o600))?;
126 }
127 tracing::info!(path = %path.display(), "host key saved");
128 Ok(key)
129 }
130 }
131