wraith/src-tauri/src/commands/docker_commands.rs
Vantz Stockwell 17973fc3dc fix: SEC-1/SEC-2 shell escape utility + MCP bearer token auth
- New shell_escape() utility for safe command interpolation
- Applied across all MCP tools, docker, scanner, network commands
- MCP server generates random bearer token at startup
- Token written to mcp-token file with 0600 permissions
- All MCP HTTP requests require Authorization header
- Bridge binary reads token and sends on every request

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 16:40:13 -04:00

118 lines
5.0 KiB
Rust

//! Tauri commands for Docker management via SSH exec channels.
use tauri::State;
use serde::Serialize;
use crate::AppState;
use crate::utils::shell_escape;
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct DockerContainer {
pub id: String,
pub name: String,
pub image: String,
pub status: String,
pub ports: String,
pub created: String,
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct DockerImage {
pub id: String,
pub repository: String,
pub tag: String,
pub size: String,
pub created: String,
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct DockerVolume {
pub name: String,
pub driver: String,
pub mountpoint: String,
}
#[tauri::command]
pub async fn docker_list_containers(session_id: String, all: Option<bool>, state: State<'_, AppState>) -> Result<Vec<DockerContainer>, String> {
let session = state.ssh.get_session(&session_id).ok_or("Session not found")?;
let flag = if all.unwrap_or(true) { "-a" } else { "" };
let output = exec(&session.handle, &format!("docker ps {} --format '{{{{.ID}}}}|{{{{.Names}}}}|{{{{.Image}}}}|{{{{.Status}}}}|{{{{.Ports}}}}|{{{{.CreatedAt}}}}' 2>&1", flag)).await?;
Ok(output.lines().filter(|l| !l.is_empty() && !l.starts_with("CONTAINER")).map(|line| {
let p: Vec<&str> = line.splitn(6, '|').collect();
DockerContainer {
id: p.first().unwrap_or(&"").to_string(),
name: p.get(1).unwrap_or(&"").to_string(),
image: p.get(2).unwrap_or(&"").to_string(),
status: p.get(3).unwrap_or(&"").to_string(),
ports: p.get(4).unwrap_or(&"").to_string(),
created: p.get(5).unwrap_or(&"").to_string(),
}
}).collect())
}
#[tauri::command]
pub async fn docker_list_images(session_id: String, state: State<'_, AppState>) -> Result<Vec<DockerImage>, String> {
let session = state.ssh.get_session(&session_id).ok_or("Session not found")?;
let output = exec(&session.handle, "docker images --format '{{.ID}}|{{.Repository}}|{{.Tag}}|{{.Size}}|{{.CreatedAt}}' 2>&1").await?;
Ok(output.lines().filter(|l| !l.is_empty()).map(|line| {
let p: Vec<&str> = line.splitn(5, '|').collect();
DockerImage {
id: p.first().unwrap_or(&"").to_string(),
repository: p.get(1).unwrap_or(&"").to_string(),
tag: p.get(2).unwrap_or(&"").to_string(),
size: p.get(3).unwrap_or(&"").to_string(),
created: p.get(4).unwrap_or(&"").to_string(),
}
}).collect())
}
#[tauri::command]
pub async fn docker_list_volumes(session_id: String, state: State<'_, AppState>) -> Result<Vec<DockerVolume>, String> {
let session = state.ssh.get_session(&session_id).ok_or("Session not found")?;
let output = exec(&session.handle, "docker volume ls --format '{{.Name}}|{{.Driver}}|{{.Mountpoint}}' 2>&1").await?;
Ok(output.lines().filter(|l| !l.is_empty()).map(|line| {
let p: Vec<&str> = line.splitn(3, '|').collect();
DockerVolume {
name: p.first().unwrap_or(&"").to_string(),
driver: p.get(1).unwrap_or(&"").to_string(),
mountpoint: p.get(2).unwrap_or(&"").to_string(),
}
}).collect())
}
#[tauri::command]
pub async fn docker_action(session_id: String, action: String, target: String, state: State<'_, AppState>) -> Result<String, String> {
let session = state.ssh.get_session(&session_id).ok_or("Session not found")?;
let t = shell_escape(&target);
let cmd = match action.as_str() {
"start" => format!("docker start {} 2>&1", t),
"stop" => format!("docker stop {} 2>&1", t),
"restart" => format!("docker restart {} 2>&1", t),
"remove" => format!("docker rm -f {} 2>&1", t),
"logs" => format!("docker logs --tail 100 {} 2>&1", t),
"remove-image" => format!("docker rmi {} 2>&1", t),
"remove-volume" => format!("docker volume rm {} 2>&1", t),
"builder-prune" => "docker builder prune -f 2>&1".to_string(),
"system-prune" => "docker system prune -f 2>&1".to_string(),
"system-prune-all" => "docker system prune -a -f 2>&1".to_string(),
_ => return Err(format!("Unknown docker action: {}", action)),
};
exec(&session.handle, &cmd).await
}
async fn exec(handle: &std::sync::Arc<tokio::sync::Mutex<russh::client::Handle<crate::ssh::session::SshClient>>>, cmd: &str) -> Result<String, String> {
let mut channel = { let h = handle.lock().await; h.channel_open_session().await.map_err(|e| format!("Exec failed: {}", e))? };
channel.exec(true, cmd).await.map_err(|e| format!("Exec failed: {}", e))?;
let mut output = String::new();
loop {
match channel.wait().await {
Some(russh::ChannelMsg::Data { ref data }) => { if let Ok(t) = std::str::from_utf8(data.as_ref()) { output.push_str(t); } }
Some(russh::ChannelMsg::Eof) | Some(russh::ChannelMsg::Close) | None => break,
_ => {}
}
}
Ok(output)
}