1use std::{path::PathBuf, process::Stdio};
16
17use chrono::{Duration, Utc};
18use jsonwebtoken::{EncodingKey, Header};
19#[cfg(feature = "cli")]
20use microsandbox_utils::term;
21use microsandbox_utils::{
22 env, DEFAULT_MSBSERVER_EXE_PATH, MSBSERVER_EXE_ENV_VAR, NAMESPACES_SUBDIR, SERVER_KEY_FILE,
23 SERVER_PID_FILE,
24};
25use rand::{distr::Alphanumeric, Rng};
26use serde::{Deserialize, Serialize};
27use tokio::{fs, process::Command};
28
29use crate::{MicrosandboxServerError, MicrosandboxServerResult};
30
31pub const API_KEY_PREFIX: &str = "msb_";
37
38const SERVER_KEY_LENGTH: usize = 32;
40
41#[cfg(feature = "cli")]
42const START_SERVER_MSG: &str = "Start sandbox server";
43
44#[cfg(feature = "cli")]
45const STOP_SERVER_MSG: &str = "Stop sandbox server";
46
47#[cfg(feature = "cli")]
48const KEYGEN_MSG: &str = "Generate new API key";
49
50#[derive(Debug, Serialize, Deserialize)]
56pub struct Claims {
57 pub exp: u64,
59
60 pub iat: u64,
62
63 pub namespace: String,
65}
66
67pub async fn start(
73 key: Option<String>,
74 port: Option<u16>,
75 namespace_dir: Option<PathBuf>,
76 dev_mode: bool,
77 detach: bool,
78 reset_key: bool,
79) -> MicrosandboxServerResult<()> {
80 let microsandbox_home_path = env::get_microsandbox_home_path();
82 fs::create_dir_all(µsandbox_home_path).await?;
83
84 let namespace_path = microsandbox_home_path.join(NAMESPACES_SUBDIR);
86 fs::create_dir_all(&namespace_path).await?;
87
88 #[cfg(feature = "cli")]
89 let start_server_sp = term::create_spinner(START_SERVER_MSG.to_string(), None, None);
90
91 let pid_file_path = microsandbox_home_path.join(SERVER_PID_FILE);
93 if pid_file_path.exists() {
94 let pid_str = fs::read_to_string(&pid_file_path).await?;
96 if let Ok(pid) = pid_str.trim().parse::<i32>() {
97 let process_running = unsafe { libc::kill(pid, 0) == 0 };
99
100 if process_running {
101 #[cfg(feature = "cli")]
102 term::finish_with_error(&start_server_sp);
103
104 #[cfg(feature = "cli")]
105 println!(
106 "A sandbox server is already running (PID: {}) - Use {} to stop it",
107 pid,
108 console::style("msb server stop").yellow()
109 );
110
111 tracing::info!(
112 "A sandbox server is already running (PID: {}). Use 'msb server stop' to stop it",
113 pid
114 );
115
116 return Ok(());
117 } else {
118 tracing::warn!("found stale PID file for process {}. Cleaning up.", pid);
120 clean(&pid_file_path).await?;
121 }
122 } else {
123 tracing::warn!("found invalid PID in server.pid file. Cleaning up.");
125 clean(&pid_file_path).await?;
126 }
127 }
128
129 let msbserver_path = microsandbox_utils::path::resolve_env_path(
131 MSBSERVER_EXE_ENV_VAR,
132 &*DEFAULT_MSBSERVER_EXE_PATH,
133 )
134 .map_err(|e| {
135 #[cfg(feature = "cli")]
136 term::finish_with_error(&start_server_sp);
137 e
138 })?;
139
140 let mut command = Command::new(msbserver_path);
141
142 if dev_mode {
143 command.arg("--dev");
144 }
145
146 if let Some(port) = port {
147 command.arg("--port").arg(port.to_string());
148 }
149
150 if let Some(namespace_dir) = namespace_dir {
151 command.arg("--path").arg(namespace_dir);
152 }
153
154 if !dev_mode {
156 let key_file_path = microsandbox_home_path.join(SERVER_KEY_FILE);
158
159 let key_provided = key.is_some();
161
162 let server_key = if let Some(key) = key {
163 command.arg("--key").arg(&key);
165 key
166 } else if key_file_path.exists() && !reset_key {
167 let existing_key = fs::read_to_string(&key_file_path).await.map_err(|e| {
169 #[cfg(feature = "cli")]
170 term::finish_with_error(&start_server_sp);
171
172 MicrosandboxServerError::StartError(format!(
173 "failed to read existing key file {}: {}",
174 key_file_path.display(),
175 e
176 ))
177 })?;
178 command.arg("--key").arg(&existing_key);
179 existing_key
180 } else {
181 let generated_key = generate_random_key();
183 command.arg("--key").arg(&generated_key);
184 generated_key
185 };
186
187 if !key_file_path.exists() || key_provided || reset_key {
189 fs::write(&key_file_path, &server_key).await.map_err(|e| {
190 #[cfg(feature = "cli")]
191 term::finish_with_error(&start_server_sp);
192
193 MicrosandboxServerError::StartError(format!(
194 "failed to write key file {}: {}",
195 key_file_path.display(),
196 e
197 ))
198 })?;
199
200 tracing::info!("created server key file at {}", key_file_path.display());
201 }
202 }
203
204 if detach {
205 unsafe {
206 command.pre_exec(|| {
207 libc::setsid();
208 Ok(())
209 });
210 }
211
212 command.stdout(Stdio::null());
215 command.stderr(Stdio::null());
216 command.stdin(Stdio::null());
217 }
218
219 if let Ok(rust_log) = std::env::var("RUST_LOG") {
221 tracing::debug!("using existing RUST_LOG: {:?}", rust_log);
222 command.env("RUST_LOG", rust_log);
223 }
224
225 let mut child = command.spawn().map_err(|e| {
226 #[cfg(feature = "cli")]
227 term::finish_with_error(&start_server_sp);
228
229 MicrosandboxServerError::StartError(format!("failed to spawn server process: {}", e))
230 })?;
231
232 let pid = child.id().unwrap_or(0);
233 tracing::info!("started sandbox server process with PID: {}", pid);
234
235 let pid_file_path = microsandbox_home_path.join(SERVER_PID_FILE);
237
238 fs::create_dir_all(µsandbox_home_path).await?;
240
241 fs::write(&pid_file_path, pid.to_string())
243 .await
244 .map_err(|e| {
245 #[cfg(feature = "cli")]
246 term::finish_with_error(&start_server_sp);
247
248 MicrosandboxServerError::StartError(format!(
249 "failed to write PID file {}: {}",
250 pid_file_path.display(),
251 e
252 ))
253 })?;
254
255 #[cfg(feature = "cli")]
256 start_server_sp.finish();
257
258 if detach {
259 return Ok(());
260 }
261
262 let mut sigterm = tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate())
264 .map_err(|e| {
265 #[cfg(feature = "cli")]
266 term::finish_with_error(&start_server_sp);
267
268 MicrosandboxServerError::StartError(format!("failed to set up signal handlers: {}", e))
269 })?;
270
271 let mut sigint = tokio::signal::unix::signal(tokio::signal::unix::SignalKind::interrupt())
272 .map_err(|e| {
273 #[cfg(feature = "cli")]
274 term::finish_with_error(&start_server_sp);
275
276 MicrosandboxServerError::StartError(format!("failed to set up signal handlers: {}", e))
277 })?;
278
279 tokio::select! {
281 status = child.wait() => {
282 if !status.as_ref().map_or(false, |s| s.success()) {
283 tracing::error!(
284 "child process — sandbox server — exited with status: {:?}",
285 status
286 );
287
288 clean(&pid_file_path).await?;
290
291 #[cfg(feature = "cli")]
292 term::finish_with_error(&start_server_sp);
293
294 return Err(MicrosandboxServerError::StartError(format!(
295 "child process — sandbox server — failed with exit status: {:?}",
296 status
297 )));
298 }
299
300 clean(&pid_file_path).await?;
302 }
303 _ = sigterm.recv() => {
304 tracing::info!("received SIGTERM signal");
305
306 if let Err(e) = child.kill().await {
308 tracing::error!("failed to send SIGTERM to child process: {}", e);
309 }
310
311 if let Err(e) = child.wait().await {
313 tracing::error!("error waiting for child after SIGTERM: {}", e);
314 }
315
316 clean(&pid_file_path).await?;
318
319 tracing::info!("server terminated by SIGTERM signal");
321 }
322 _ = sigint.recv() => {
323 tracing::info!("received SIGINT signal");
324
325 if let Err(e) = child.kill().await {
327 tracing::error!("failed to send SIGTERM to child process: {}", e);
328 }
329
330 if let Err(e) = child.wait().await {
332 tracing::error!("error waiting for child after SIGINT: {}", e);
333 }
334
335 clean(&pid_file_path).await?;
337
338 tracing::info!("server terminated by SIGINT signal");
340 }
341 }
342
343 Ok(())
344}
345
346pub async fn stop() -> MicrosandboxServerResult<()> {
348 let microsandbox_home_path = env::get_microsandbox_home_path();
349 let pid_file_path = microsandbox_home_path.join(SERVER_PID_FILE);
350
351 #[cfg(feature = "cli")]
352 let stop_server_sp = term::create_spinner(STOP_SERVER_MSG.to_string(), None, None);
353
354 if !pid_file_path.exists() {
356 #[cfg(feature = "cli")]
357 term::finish_with_error(&stop_server_sp);
358
359 return Err(MicrosandboxServerError::StopError(
360 "server is not running (PID file not found)".to_string(),
361 ));
362 }
363
364 let pid_str = fs::read_to_string(&pid_file_path).await?;
366 let pid = pid_str.trim().parse::<i32>().map_err(|_| {
367 MicrosandboxServerError::StopError("invalid PID found in server.pid file".to_string())
368 })?;
369
370 unsafe {
372 if libc::kill(pid, libc::SIGTERM) != 0 {
373 if std::io::Error::last_os_error().raw_os_error().unwrap() == libc::ESRCH {
375 clean(&pid_file_path).await?;
377
378 #[cfg(feature = "cli")]
379 term::finish_with_error(&stop_server_sp);
380
381 return Err(MicrosandboxServerError::StopError(
382 "server process not found (stale PID file removed)".to_string(),
383 ));
384 }
385
386 #[cfg(feature = "cli")]
387 term::finish_with_error(&stop_server_sp);
388
389 return Err(MicrosandboxServerError::StopError(format!(
390 "failed to stop server process (PID: {})",
391 pid
392 )));
393 }
394 }
395
396 clean(&pid_file_path).await?;
398
399 #[cfg(feature = "cli")]
400 stop_server_sp.finish();
401
402 tracing::info!("stopped sandbox server process (PID: {})", pid);
403
404 Ok(())
405}
406
407pub async fn keygen(
409 expire: Option<Duration>,
410 namespace: String,
411) -> MicrosandboxServerResult<String> {
412 let microsandbox_home_path = env::get_microsandbox_home_path();
413 let key_file_path = microsandbox_home_path.join(SERVER_KEY_FILE);
414
415 #[cfg(feature = "cli")]
416 let keygen_sp = term::create_spinner(KEYGEN_MSG.to_string(), None, None);
417
418 if !key_file_path.exists() {
420 #[cfg(feature = "cli")]
421 term::finish_with_error(&keygen_sp);
422
423 return Err(MicrosandboxServerError::KeyGenError(
424 "Server key file not found. Make sure the server is running in secure mode."
425 .to_string(),
426 ));
427 }
428
429 let server_key = fs::read_to_string(&key_file_path).await.map_err(|e| {
431 #[cfg(feature = "cli")]
432 term::finish_with_error(&keygen_sp);
433
434 MicrosandboxServerError::KeyGenError(format!(
435 "Failed to read server key file {}: {}",
436 key_file_path.display(),
437 e
438 ))
439 })?;
440
441 let expire = expire.unwrap_or(Duration::hours(24));
443
444 let now = Utc::now();
446 let expiry = now + expire;
447
448 let claims = Claims {
449 exp: expiry.timestamp() as u64,
450 iat: now.timestamp() as u64,
451 namespace,
452 };
453
454 let jwt_token = jsonwebtoken::encode(
456 &Header::default(),
457 &claims,
458 &EncodingKey::from_secret(server_key.as_bytes()),
459 )
460 .map_err(|e| {
461 #[cfg(feature = "cli")]
462 term::finish_with_error(&keygen_sp);
463
464 MicrosandboxServerError::KeyGenError(format!("Failed to generate token: {}", e))
465 })?;
466
467 let custom_token = convert_jwt_to_api_key(&jwt_token)?;
469
470 let token_str = custom_token.clone();
472 let expiry_str = expiry.to_rfc3339();
473
474 #[cfg(feature = "cli")]
475 keygen_sp.finish();
476
477 tracing::info!(
478 "Generated API token with namespace {} and expiry {}",
479 claims.namespace,
480 expiry_str
481 );
482
483 #[cfg(feature = "cli")]
484 {
485 println!("Token: {}", console::style(&token_str).cyan());
486 println!("Token expires: {}", console::style(&expiry_str).cyan());
487 println!("Namespace: {}", console::style(&claims.namespace).cyan());
488 }
489
490 Ok(token_str)
491}
492
493pub async fn clean(pid_file_path: &PathBuf) -> MicrosandboxServerResult<()> {
495 if pid_file_path.exists() {
497 fs::remove_file(pid_file_path).await?;
498 tracing::info!("removed server PID file at {}", pid_file_path.display());
499 }
500
501 Ok(())
502}
503
504fn generate_random_key() -> String {
510 rand::rng()
511 .sample_iter(&Alphanumeric)
512 .take(SERVER_KEY_LENGTH)
513 .map(char::from)
514 .collect()
515}
516
517pub fn convert_jwt_to_api_key(jwt_token: &str) -> MicrosandboxServerResult<String> {
521 let parts: Vec<&str> = jwt_token.split('.').collect();
522 if parts.len() != 3 {
523 return Err(MicrosandboxServerError::KeyGenError(
524 "Invalid JWT token format".to_string(),
525 ));
526 }
527
528 Ok(format!("{}{}.{}", API_KEY_PREFIX, parts[1], parts[2]))
530}