max / mnw-cli
6 files changed,
+369 insertions,
-12 deletions
| @@ -215,6 +215,12 @@ pub struct SshKeyInfo { | |||
| 215 | 215 | pub created_at: String, | |
| 216 | 216 | } | |
| 217 | 217 | ||
| 218 | + | /// Response from the git authorize endpoint. | |
| 219 | + | #[derive(Debug, Deserialize)] | |
| 220 | + | pub struct GitAuthResponse { | |
| 221 | + | pub repo_path: String, | |
| 222 | + | } | |
| 223 | + | ||
| 218 | 224 | /// Client for calling MNW internal API endpoints. | |
| 219 | 225 | #[derive(Clone)] | |
| 220 | 226 | pub struct MnwApiClient { | |
| @@ -937,6 +943,42 @@ impl MnwApiClient { | |||
| 937 | 943 | ||
| 938 | 944 | // ── SSH keys ── | |
| 939 | 945 | ||
| 946 | + | /// Authorize a git operation and get the on-disk repo path. | |
| 947 | + | pub async fn git_authorize( | |
| 948 | + | &self, | |
| 949 | + | user_id: &str, | |
| 950 | + | operation: &str, | |
| 951 | + | owner: &str, | |
| 952 | + | repo_name: &str, | |
| 953 | + | ) -> anyhow::Result<GitAuthResponse> { | |
| 954 | + | let url = format!("{}/api/internal/git/authorize", self.base_url); | |
| 955 | + | let resp = self | |
| 956 | + | .http | |
| 957 | + | .post(&url) | |
| 958 | + | .bearer_auth(&self.service_token) | |
| 959 | + | .json(&serde_json::json!({ | |
| 960 | + | "user_id": user_id, | |
| 961 | + | "operation": operation, | |
| 962 | + | "owner": owner, | |
| 963 | + | "repo_name": repo_name, | |
| 964 | + | })) | |
| 965 | + | .send() | |
| 966 | + | .await?; | |
| 967 | + | ||
| 968 | + | if !resp.status().is_success() { | |
| 969 | + | let status = resp.status(); | |
| 970 | + | let body = resp.text().await.unwrap_or_default(); | |
| 971 | + | // Parse JSON error if available, fall back to status text | |
| 972 | + | let msg = serde_json::from_str::<serde_json::Value>(&body) | |
| 973 | + | .ok() | |
| 974 | + | .and_then(|v| v.get("error").and_then(|e| e.as_str()).map(String::from)) | |
| 975 | + | .unwrap_or_else(|| format!("HTTP {status}")); | |
| 976 | + | anyhow::bail!("{msg}"); | |
| 977 | + | } | |
| 978 | + | ||
| 979 | + | Ok(resp.json().await?) | |
| 980 | + | } | |
| 981 | + | ||
| 940 | 982 | /// List registered SSH keys for a user. | |
| 941 | 983 | pub async fn list_ssh_keys(&self, user_id: &str) -> anyhow::Result<Vec<SshKeyInfo>> { | |
| 942 | 984 | let url = format!("{}/api/internal/creator/ssh-keys", self.base_url); |
| @@ -14,6 +14,8 @@ pub struct Config { | |||
| 14 | 14 | pub host_key_path: PathBuf, | |
| 15 | 15 | /// Base directory for staging uploaded files. | |
| 16 | 16 | pub staging_dir: PathBuf, | |
| 17 | + | /// System user that owns git repositories (for `sudo -u`). | |
| 18 | + | pub git_user: String, | |
| 17 | 19 | } | |
| 18 | 20 | ||
| 19 | 21 | impl Config { | |
| @@ -38,12 +40,16 @@ impl Config { | |||
| 38 | 40 | .unwrap_or_else(|_| "/var/lib/mnw-cli/staging".to_string()) | |
| 39 | 41 | .into(); | |
| 40 | 42 | ||
| 43 | + | let git_user = std::env::var("GIT_SUDO_USER") | |
| 44 | + | .unwrap_or_else(|_| "git".to_string()); | |
| 45 | + | ||
| 41 | 46 | Ok(Config { | |
| 42 | 47 | port, | |
| 43 | 48 | api_url, | |
| 44 | 49 | service_token, | |
| 45 | 50 | host_key_path, | |
| 46 | 51 | staging_dir, | |
| 52 | + | git_user, | |
| 47 | 53 | }) | |
| 48 | 54 | } | |
| 49 | 55 | } |
| @@ -65,7 +65,7 @@ async fn main() -> anyhow::Result<()> { | |||
| 65 | 65 | ||
| 66 | 66 | let staging_dir = Arc::new(config.staging_dir); | |
| 67 | 67 | let api_client = api::MnwApiClient::new(config.api_url, config.service_token); | |
| 68 | - | let mut server = ssh::MnwServer::new(api_client, staging_dir); | |
| 68 | + | let mut server = ssh::MnwServer::new(api_client, staging_dir, config.git_user); | |
| 69 | 69 | ||
| 70 | 70 | let addr = format!("0.0.0.0:{}", config.port); | |
| 71 | 71 | tracing::info!(%addr, "listening for SSH connections"); |
| @@ -0,0 +1,218 @@ | |||
| 1 | + | //! Git operation proxy: command parsing and subprocess management. | |
| 2 | + | //! | |
| 3 | + | //! When a git client connects via SSH (e.g., `git push git@ssh.makenot.work:max/repo.git`), | |
| 4 | + | //! the exec_request receives a command like `git-receive-pack 'max/repo.git'`. This module | |
| 5 | + | //! parses that command, and spawns the git subprocess with its stdin/stdout/stderr piped | |
| 6 | + | //! through the SSH channel. | |
| 7 | + | ||
| 8 | + | use russh::server::Handle; | |
| 9 | + | use russh::ChannelId; | |
| 10 | + | use tokio::io::AsyncReadExt; | |
| 11 | + | use tokio::process::{Child, Command}; | |
| 12 | + | ||
| 13 | + | /// Parse a git exec command into (operation, raw_path). | |
| 14 | + | /// | |
| 15 | + | /// Git clients send commands like: | |
| 16 | + | /// `git-upload-pack '/max/repo.git'` | |
| 17 | + | /// `git-receive-pack 'max/repo.git'` | |
| 18 | + | /// | |
| 19 | + | /// Returns `None` for non-git commands. | |
| 20 | + | pub fn parse_git_command(cmd: &str) -> Option<(&str, &str)> { | |
| 21 | + | let (operation, rest) = cmd.split_once(' ')?; | |
| 22 | + | ||
| 23 | + | match operation { | |
| 24 | + | "git-upload-pack" | "git-receive-pack" | "git-upload-archive" => {} | |
| 25 | + | _ => return None, | |
| 26 | + | } | |
| 27 | + | ||
| 28 | + | // Strip surrounding quotes (single or double) | |
| 29 | + | let path = rest.trim(); | |
| 30 | + | let path = path | |
| 31 | + | .strip_prefix('\'') | |
| 32 | + | .and_then(|s| s.strip_suffix('\'')) | |
| 33 | + | .or_else(|| path.strip_prefix('"').and_then(|s| s.strip_suffix('"'))) | |
| 34 | + | .unwrap_or(path); | |
| 35 | + | ||
| 36 | + | Some((operation, path)) | |
| 37 | + | } | |
| 38 | + | ||
| 39 | + | /// Parse a repo path like "max/repo.git" or "/max/repo" into (owner, repo_name). | |
| 40 | + | /// | |
| 41 | + | /// Strips leading `/` and trailing `.git`. | |
| 42 | + | pub fn parse_repo_path(path: &str) -> Option<(&str, &str)> { | |
| 43 | + | let path = path.strip_prefix('/').unwrap_or(path); | |
| 44 | + | let (owner, repo_name) = path.split_once('/')?; | |
| 45 | + | ||
| 46 | + | if owner.is_empty() || owner.contains("..") { | |
| 47 | + | return None; | |
| 48 | + | } | |
| 49 | + | ||
| 50 | + | // Strip trailing .git | |
| 51 | + | let repo_name = repo_name.strip_suffix(".git").unwrap_or(repo_name); | |
| 52 | + | ||
| 53 | + | if repo_name.is_empty() || repo_name.contains("..") || repo_name.contains('/') { | |
| 54 | + | return None; | |
| 55 | + | } | |
| 56 | + | ||
| 57 | + | Some((owner, repo_name)) | |
| 58 | + | } | |
| 59 | + | ||
| 60 | + | /// Spawn a git subprocess and wire its I/O through the SSH channel. | |
| 61 | + | /// | |
| 62 | + | /// Returns the child's stdin handle so the caller can forward SSH data() to it. | |
| 63 | + | /// Stdout/stderr forwarding and process cleanup run in background tasks. | |
| 64 | + | pub async fn spawn_git_process( | |
| 65 | + | git_user: &str, | |
| 66 | + | operation: &str, | |
| 67 | + | repo_path: &str, | |
| 68 | + | channel: ChannelId, | |
| 69 | + | handle: Handle, | |
| 70 | + | ) -> anyhow::Result<tokio::process::ChildStdin> { | |
| 71 | + | let mut child: Child = Command::new("sudo") | |
| 72 | + | .args(["-u", git_user, operation, repo_path]) | |
| 73 | + | .stdin(std::process::Stdio::piped()) | |
| 74 | + | .stdout(std::process::Stdio::piped()) | |
| 75 | + | .stderr(std::process::Stdio::piped()) | |
| 76 | + | .kill_on_drop(true) | |
| 77 | + | .spawn()?; | |
| 78 | + | ||
| 79 | + | let stdin = child | |
| 80 | + | .stdin | |
| 81 | + | .take() | |
| 82 | + | .ok_or_else(|| anyhow::anyhow!("failed to capture child stdin"))?; | |
| 83 | + | let stdout = child | |
| 84 | + | .stdout | |
| 85 | + | .take() | |
| 86 | + | .ok_or_else(|| anyhow::anyhow!("failed to capture child stdout"))?; | |
| 87 | + | let stderr = child | |
| 88 | + | .stderr | |
| 89 | + | .take() | |
| 90 | + | .ok_or_else(|| anyhow::anyhow!("failed to capture child stderr"))?; | |
| 91 | + | ||
| 92 | + | // Forward stdout → SSH channel data | |
| 93 | + | let stdout_handle = handle.clone(); | |
| 94 | + | let stdout_task = tokio::spawn(async move { | |
| 95 | + | let mut reader = stdout; | |
| 96 | + | let mut buf = [0u8; 32768]; | |
| 97 | + | loop { | |
| 98 | + | match reader.read(&mut buf).await { | |
| 99 | + | Ok(0) => break, | |
| 100 | + | Ok(n) => { | |
| 101 | + | let data = bytes::Bytes::copy_from_slice(&buf[..n]); | |
| 102 | + | if stdout_handle.data(channel, data).await.is_err() { | |
| 103 | + | break; | |
| 104 | + | } | |
| 105 | + | } | |
| 106 | + | Err(_) => break, | |
| 107 | + | } | |
| 108 | + | } | |
| 109 | + | }); | |
| 110 | + | ||
| 111 | + | // Forward stderr → SSH channel extended data (type 1 = stderr) | |
| 112 | + | let stderr_handle = handle.clone(); | |
| 113 | + | let stderr_task = tokio::spawn(async move { | |
| 114 | + | let mut reader = stderr; | |
| 115 | + | let mut buf = [0u8; 8192]; | |
| 116 | + | loop { | |
| 117 | + | match reader.read(&mut buf).await { | |
| 118 | + | Ok(0) => break, | |
| 119 | + | Ok(n) => { | |
| 120 | + | let data = bytes::Bytes::copy_from_slice(&buf[..n]); | |
| 121 | + | if stderr_handle.extended_data(channel, 1, data).await.is_err() { | |
| 122 | + | break; | |
| 123 | + | } | |
| 124 | + | } | |
| 125 | + | Err(_) => break, | |
| 126 | + | } | |
| 127 | + | } | |
| 128 | + | }); | |
| 129 | + | ||
| 130 | + | // Wait for subprocess to complete, then close the SSH channel | |
| 131 | + | tokio::spawn(async move { | |
| 132 | + | let _ = stdout_task.await; | |
| 133 | + | let _ = stderr_task.await; | |
| 134 | + | ||
| 135 | + | let exit_code = match child.wait().await { | |
| 136 | + | Ok(status) => status.code().unwrap_or(1) as u32, | |
| 137 | + | Err(_) => 1, | |
| 138 | + | }; | |
| 139 | + | ||
| 140 | + | let _ = handle.exit_status_request(channel, exit_code).await; | |
| 141 | + | let _ = handle.eof(channel).await; | |
| 142 | + | let _ = handle.close(channel).await; | |
| 143 | + | }); | |
| 144 | + | ||
| 145 | + | Ok(stdin) | |
| 146 | + | } | |
| 147 | + | ||
| 148 | + | #[cfg(test)] | |
| 149 | + | mod tests { | |
| 150 | + | use super::*; | |
| 151 | + | ||
| 152 | + | #[test] | |
| 153 | + | fn parse_git_upload_pack() { | |
| 154 | + | let (op, path) = parse_git_command("git-upload-pack '/max/repo.git'").unwrap(); | |
| 155 | + | assert_eq!(op, "git-upload-pack"); | |
| 156 | + | assert_eq!(path, "/max/repo.git"); | |
| 157 | + | } | |
| 158 | + | ||
| 159 | + | #[test] | |
| 160 | + | fn parse_git_receive_pack_no_quotes() { | |
| 161 | + | let (op, path) = parse_git_command("git-receive-pack max/repo.git").unwrap(); | |
| 162 | + | assert_eq!(op, "git-receive-pack"); | |
| 163 | + | assert_eq!(path, "max/repo.git"); | |
| 164 | + | } | |
| 165 | + | ||
| 166 | + | #[test] | |
| 167 | + | fn parse_git_upload_archive_double_quotes() { | |
| 168 | + | let (op, path) = parse_git_command("git-upload-archive \"/max/repo.git\"").unwrap(); | |
| 169 | + | assert_eq!(op, "git-upload-archive"); | |
| 170 | + | assert_eq!(path, "/max/repo.git"); | |
| 171 | + | } | |
| 172 | + | ||
| 173 | + | #[test] | |
| 174 | + | fn parse_non_git_command() { | |
| 175 | + | assert!(parse_git_command("ls -la").is_none()); | |
| 176 | + | assert!(parse_git_command("scp -t /tmp/file").is_none()); | |
| 177 | + | } | |
| 178 | + | ||
| 179 | + | #[test] | |
| 180 | + | fn parse_repo_path_basic() { | |
| 181 | + | let (owner, repo) = parse_repo_path("max/repo.git").unwrap(); | |
| 182 | + | assert_eq!(owner, "max"); | |
| 183 | + | assert_eq!(repo, "repo"); | |
| 184 | + | } | |
| 185 | + | ||
| 186 | + | #[test] | |
| 187 | + | fn parse_repo_path_with_leading_slash() { | |
| 188 | + | let (owner, repo) = parse_repo_path("/max/myproject.git").unwrap(); | |
| 189 | + | assert_eq!(owner, "max"); | |
| 190 | + | assert_eq!(repo, "myproject"); | |
| 191 | + | } | |
| 192 | + | ||
| 193 | + | #[test] | |
| 194 | + | fn parse_repo_path_no_git_suffix() { | |
| 195 | + | let (owner, repo) = parse_repo_path("max/repo").unwrap(); | |
| 196 | + | assert_eq!(owner, "max"); | |
| 197 | + | assert_eq!(repo, "repo"); | |
| 198 | + | } | |
| 199 | + | ||
| 200 | + | #[test] | |
| 201 | + | fn parse_repo_path_rejects_traversal() { | |
| 202 | + | assert!(parse_repo_path("../evil/repo.git").is_none()); | |
| 203 | + | assert!(parse_repo_path("max/../../etc.git").is_none()); | |
| 204 | + | } | |
| 205 | + | ||
| 206 | + | #[test] | |
| 207 | + | fn parse_repo_path_rejects_empty() { | |
| 208 | + | assert!(parse_repo_path("").is_none()); | |
| 209 | + | assert!(parse_repo_path("/").is_none()); | |
| 210 | + | assert!(parse_repo_path("max/").is_none()); | |
| 211 | + | assert!(parse_repo_path("max/.git").is_none()); | |
| 212 | + | } | |
| 213 | + | ||
| 214 | + | #[test] | |
| 215 | + | fn parse_repo_path_rejects_nested() { | |
| 216 | + | assert!(parse_repo_path("max/sub/repo.git").is_none()); | |
| 217 | + | } | |
| 218 | + | } |
| @@ -11,6 +11,7 @@ use russh::{Channel, ChannelId}; | |||
| 11 | 11 | use tokio::sync::Mutex; | |
| 12 | 12 | ||
| 13 | 13 | use crate::api::{MnwApiClient, UserInfo}; | |
| 14 | + | use crate::ssh::git; | |
| 14 | 15 | use crate::ssh::sftp::SftpSession; | |
| 15 | 16 | use crate::ssh::terminal::TerminalHandle; | |
| 16 | 17 | use crate::staging; | |
| @@ -21,6 +22,7 @@ pub struct MnwHandler { | |||
| 21 | 22 | api: MnwApiClient, | |
| 22 | 23 | peer_addr: Option<SocketAddr>, | |
| 23 | 24 | staging_dir: Arc<PathBuf>, | |
| 25 | + | git_user: Arc<str>, | |
| 24 | 26 | /// Populated after successful auth. | |
| 25 | 27 | user: Option<UserInfo>, | |
| 26 | 28 | /// Terminal dimensions (cols, rows). | |
| @@ -29,18 +31,27 @@ pub struct MnwHandler { | |||
| 29 | 31 | app: Option<tui::AppHandle>, | |
| 30 | 32 | /// Channels stored between open and shell/subsystem request. | |
| 31 | 33 | channels: Arc<Mutex<HashMap<ChannelId, Channel<Msg>>>>, | |
| 34 | + | /// Active git subprocess stdin handles, keyed by channel. | |
| 35 | + | git_processes: HashMap<ChannelId, tokio::process::ChildStdin>, | |
| 32 | 36 | } | |
| 33 | 37 | ||
| 34 | 38 | impl MnwHandler { | |
| 35 | - | pub fn new(api: MnwApiClient, peer_addr: Option<SocketAddr>, staging_dir: Arc<PathBuf>) -> Self { | |
| 39 | + | pub fn new( | |
| 40 | + | api: MnwApiClient, | |
| 41 | + | peer_addr: Option<SocketAddr>, | |
| 42 | + | staging_dir: Arc<PathBuf>, | |
| 43 | + | git_user: Arc<str>, | |
| 44 | + | ) -> Self { | |
| 36 | 45 | Self { | |
| 37 | 46 | api, | |
| 38 | 47 | peer_addr, | |
| 39 | 48 | staging_dir, | |
| 49 | + | git_user, | |
| 40 | 50 | user: None, | |
| 41 | 51 | term_size: (80, 24), | |
| 42 | 52 | app: None, | |
| 43 | 53 | channels: Arc::new(Mutex::new(HashMap::new())), | |
| 54 | + | git_processes: HashMap::new(), | |
| 44 | 55 | } | |
| 45 | 56 | } | |
| 46 | 57 | } | |
| @@ -220,6 +231,66 @@ impl russh::server::Handler for MnwHandler { | |||
| 220 | 231 | let command_line = String::from_utf8_lossy(data); | |
| 221 | 232 | let handle = session.handle(); | |
| 222 | 233 | ||
| 234 | + | let Some(ref user) = self.user else { | |
| 235 | + | let _ = handle.close(channel).await; | |
| 236 | + | return Ok(()); | |
| 237 | + | }; | |
| 238 | + | ||
| 239 | + | // Git operations: bidirectional streaming via subprocess proxy | |
| 240 | + | if let Some((operation, raw_path)) = git::parse_git_command(&command_line) { | |
| 241 | + | if let Some((owner, repo_name)) = git::parse_repo_path(raw_path) { | |
| 242 | + | tracing::info!( | |
| 243 | + | user = %user.username, | |
| 244 | + | %operation, | |
| 245 | + | %owner, | |
| 246 | + | %repo_name, | |
| 247 | + | "git operation" | |
| 248 | + | ); | |
| 249 | + | ||
| 250 | + | match self | |
| 251 | + | .api | |
| 252 | + | .git_authorize(&user.user_id, operation, owner, repo_name) | |
| 253 | + | .await | |
| 254 | + | { | |
| 255 | + | Ok(auth) => { | |
| 256 | + | match git::spawn_git_process( | |
| 257 | + | &self.git_user, | |
| 258 | + | operation, | |
| 259 | + | &auth.repo_path, | |
| 260 | + | channel, | |
| 261 | + | handle.clone(), | |
| 262 | + | ) | |
| 263 | + | .await | |
| 264 | + | { | |
| 265 | + | Ok(stdin) => { | |
| 266 | + | self.git_processes.insert(channel, stdin); | |
| 267 | + | session.channel_success(channel)?; | |
| 268 | + | } | |
| 269 | + | Err(e) => { | |
| 270 | + | tracing::error!(error = ?e, "failed to spawn git process"); | |
| 271 | + | let msg = bytes::Bytes::from(format!( | |
| 272 | + | "fatal: internal error\r\n" | |
| 273 | + | )); | |
| 274 | + | let _ = handle.data(channel, msg).await; | |
| 275 | + | let _ = handle.exit_status_request(channel, 1).await; | |
| 276 | + | let _ = handle.eof(channel).await; | |
| 277 | + | let _ = handle.close(channel).await; | |
| 278 | + | } | |
| 279 | + | } | |
| 280 | + | } | |
| 281 | + | Err(e) => { | |
| 282 | + | let msg = | |
| 283 | + | bytes::Bytes::from(format!("fatal: {e}\r\n")); | |
| 284 | + | let _ = handle.extended_data(channel, 1, msg).await; | |
| 285 | + | let _ = handle.exit_status_request(channel, 1).await; | |
| 286 | + | let _ = handle.eof(channel).await; | |
| 287 | + | let _ = handle.close(channel).await; | |
| 288 | + | } | |
| 289 | + | } | |
| 290 | + | return Ok(()); | |
| 291 | + | } | |
| 292 | + | } | |
| 293 | + | ||
| 223 | 294 | // Check if this looks like a legacy SCP transfer | |
| 224 | 295 | if command_line.starts_with("scp ") { | |
| 225 | 296 | let msg: &[u8] = | |
| @@ -233,11 +304,6 @@ impl russh::server::Handler for MnwHandler { | |||
| 233 | 304 | } | |
| 234 | 305 | ||
| 235 | 306 | // Execute the command | |
| 236 | - | let Some(ref user) = self.user else { | |
| 237 | - | let _ = handle.close(channel).await; | |
| 238 | - | return Ok(()); | |
| 239 | - | }; | |
| 240 | - | ||
| 241 | 307 | let user = user.clone(); | |
| 242 | 308 | let api = self.api.clone(); | |
| 243 | 309 | let cmd = command_line.to_string(); | |
| @@ -255,16 +321,30 @@ impl russh::server::Handler for MnwHandler { | |||
| 255 | 321 | ||
| 256 | 322 | async fn data( | |
| 257 | 323 | &mut self, | |
| 258 | - | _channel: ChannelId, | |
| 324 | + | channel: ChannelId, | |
| 259 | 325 | data: &[u8], | |
| 260 | 326 | _session: &mut Session, | |
| 261 | 327 | ) -> Result<(), Self::Error> { | |
| 262 | - | if let Some(ref app) = self.app { | |
| 328 | + | if let Some(stdin) = self.git_processes.get_mut(&channel) { | |
| 329 | + | use tokio::io::AsyncWriteExt; | |
| 330 | + | let _ = stdin.write_all(data).await; | |
| 331 | + | } else if let Some(ref app) = self.app { | |
| 263 | 332 | app.send_input(data).await; | |
| 264 | 333 | } | |
| 265 | 334 | Ok(()) | |
| 266 | 335 | } | |
| 267 | 336 | ||
| 337 | + | async fn channel_eof( | |
| 338 | + | &mut self, | |
| 339 | + | channel: ChannelId, | |
| 340 | + | _session: &mut Session, | |
| 341 | + | ) -> Result<(), Self::Error> { | |
| 342 | + | if let Some(stdin) = self.git_processes.remove(&channel) { | |
| 343 | + | drop(stdin); // Closes pipe → subprocess sees EOF | |
| 344 | + | } | |
| 345 | + | Ok(()) | |
| 346 | + | } | |
| 347 | + | ||
| 268 | 348 | async fn window_change_request( | |
| 269 | 349 | &mut self, | |
| 270 | 350 | _channel: ChannelId, |
| @@ -1,5 +1,6 @@ | |||
| 1 | 1 | //! SSH server implementation using russh. | |
| 2 | 2 | ||
| 3 | + | pub mod git; | |
| 3 | 4 | pub mod handler; | |
| 4 | 5 | pub mod sftp; | |
| 5 | 6 | pub mod terminal; | |
| @@ -14,11 +15,16 @@ use crate::api::MnwApiClient; | |||
| 14 | 15 | pub struct MnwServer { | |
| 15 | 16 | api: MnwApiClient, | |
| 16 | 17 | staging_dir: Arc<PathBuf>, | |
| 18 | + | git_user: Arc<str>, | |
| 17 | 19 | } | |
| 18 | 20 | ||
| 19 | 21 | impl MnwServer { | |
| 20 | - | pub fn new(api: MnwApiClient, staging_dir: Arc<PathBuf>) -> Self { | |
| 21 | - | Self { api, staging_dir } | |
| 22 | + | pub fn new(api: MnwApiClient, staging_dir: Arc<PathBuf>, git_user: String) -> Self { | |
| 23 | + | Self { | |
| 24 | + | api, | |
| 25 | + | staging_dir, | |
| 26 | + | git_user: Arc::from(git_user), | |
| 27 | + | } | |
| 22 | 28 | } | |
| 23 | 29 | } | |
| 24 | 30 | ||
| @@ -27,6 +33,11 @@ impl russh::server::Server for MnwServer { | |||
| 27 | 33 | ||
| 28 | 34 | fn new_client(&mut self, peer_addr: Option<SocketAddr>) -> Self::Handler { | |
| 29 | 35 | tracing::info!(?peer_addr, "new SSH connection"); | |
| 30 | - | handler::MnwHandler::new(self.api.clone(), peer_addr, Arc::clone(&self.staging_dir)) | |
| 36 | + | handler::MnwHandler::new( | |
| 37 | + | self.api.clone(), | |
| 38 | + | peer_addr, | |
| 39 | + | Arc::clone(&self.staging_dir), | |
| 40 | + | Arc::clone(&self.git_user), | |
| 41 | + | ) | |
| 31 | 42 | } | |
| 32 | 43 | } |