Skip to main content

max / makenotwork

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