f2ad9b8
use bytes::Bytes;
use russh::ChannelId;
use russh::server::Session;
use crate::handler::GitSshHandler;
impl GitSshHandler {
/// Append incoming client data to the per-channel buffer. All buffered
/// bytes are flushed to the git subprocess in a single chunk on EOF.
pub async fn data_impl(&mut self, channel: ChannelId, data: &[u8]) -> Result<(), russh::Error> {
let state = self.channels.entry(channel).or_default();
state.buffer.extend_from_slice(data);
Ok(())
}
pub async fn channel_close_impl(&mut self, channel: ChannelId) {
self.channels.remove(&channel);
}
/// On EOF, dispatch the buffered bytes to the git subprocess
/// (asynchronously) and send the result back over the channel.
pub async fn channel_eof_impl(
&mut self,
channel: ChannelId,
session: &mut Session,
) -> Result<(), russh::Error> {
let Some(state) = self.channels.get_mut(&channel) else {
let _ = session.close(channel);
let _ = session.flush();
return Ok(());
};
let op = state.git_op.take();
let buffer = std::mem::take(&mut state.buffer);
let push_coordinator = self.service.push_coordinator();
let handle = session.handle();
tokio::spawn(async move {
let is_write = op.as_ref().is_some_and(|o| o.is_write());
let push_key = op
.as_ref()
.map(|o| format!("git_op:{}", o.bare().display()))
.unwrap_or_default();
let run = async { run_git(op.as_ref(), &buffer, &handle, channel).await };
let result = if is_write {
push_coordinator
.run(&push_key, async {
run.await
.map_err(|e| gitsmart::SmartError::Internal(e.to_string()))
})
.await
.map_err(crate::error::SshError::from)
} else {
run.await
};
let code = match &result {
Ok(_) => 0,
Err(_) => 1,
};
let _ = handle.exit_status_request(channel, code).await;
tokio::time::sleep(std::time::Duration::from_millis(20)).await;
let _ = handle.eof(channel).await;
let _ = handle.close(channel).await;
});
Ok(())
}
pub async fn shell_request_impl(
&mut self,
channel: ChannelId,
session: &mut Session,
) -> Result<(), russh::Error> {
let msg = Bytes::from_static(
b"Welcome to GitSsh. This is a git-only server; shell access is disabled.\r\n",
);
let _ = session.data(channel, msg);
let _ = session.exit_status_request(channel, 0);
let _ = session.eof(channel);
let _ = session.close(channel);
let _ = session.flush();
Ok(())
}
}
/// Run the git subprocess once: write the buffered input, read stdout, and
/// forward it back to the client.
async fn run_git(
op: Option<&crate::handler::git_op::GitOperation>,
input: &[u8],
handle: &russh::server::Handle,
channel: russh::ChannelId,
) -> Result<(), crate::error::SshError> {
let Some(op) = op else {
return Ok(());
};
let mut cmd = tokio::process::Command::new("git");
cmd.current_dir(op.bare())
.arg(op.service_arg())
.arg("--stateless-rpc")
.env("GIT_DIR", op.bare())
.env("GIT_TERMINAL_PROMPT", "0")
.stdin(std::process::Stdio::piped())
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped());
let mut child = cmd.spawn().map_err(crate::error::SshError::Io)?;
use tokio::io::AsyncWriteExt;
if let Some(mut stdin) = child.stdin.take() {
stdin
.write_all(input)
.await
.map_err(crate::error::SshError::Io)?;
let _ = stdin.shutdown().await;
}
let output = child
.wait_with_output()
.await
.map_err(crate::error::SshError::Io)?;
if !output.stdout.is_empty() {
let _ = handle.data(channel, Bytes::from(output.stdout)).await;
}
if !output.stderr.is_empty() && !output.status.success() {
let _ = handle
.extended_data(channel, 1, Bytes::from(output.stderr))
.await;
}
if output.status.success() {
Ok(())
} else {
Err(crate::error::SshError::Internal(format!(
"git {} failed: exit {:?}",
op.service_arg(),
output.status.code()
)))
}
}