Implement SSH receive-pack pushes

This commit is contained in:
2026-08-25 20:56:06 +02:00
parent 051e14235f
commit 76e707f645
5 changed files with 1370 additions and 67 deletions

View File

@@ -47,6 +47,12 @@ pub(crate) struct SshGitCommand {
pub(crate) completion: SshCommandCompletion,
}
pub(crate) struct SshRawGitCommand {
pub(crate) stdout: SshStdout,
pub(crate) stdin: SshStdin,
pub(crate) completion: SshCommandCompletion,
}
pub(crate) type SshCommandCompletion = tokio::sync::oneshot::Receiver<Result<(), GitError>>;
pub(crate) struct SshStdout {
@@ -195,10 +201,50 @@ impl SshSession {
remote: &GitRemote,
control: &GitOperationControl,
) -> Result<SshGitCommand, GitError> {
let RemoteEndpoint::Ssh(endpoint) = remote.endpoint() else {
return Err(GitError::ForbiddenRemoteUrl);
};
let command = upload_pack_command(endpoint.path())?;
let endpoint = remote
.endpoint()
.as_ssh()
.ok_or(GitError::ForbiddenRemoteUrl)?;
let SshRawGitCommand {
stdout,
stdin,
completion,
} = self.open_git_command(remote, GitService::UploadPack, control)?;
let transport = gix::protocol::transport::client::git::blocking_io::Connection::new(
stdout,
stdin,
gix::protocol::transport::Protocol::V1,
endpoint.path().as_str().as_bytes().to_vec(),
None::<(String, Option<u16>)>,
gix::protocol::transport::client::git::ConnectMode::Process,
false,
)
.custom_url(Some(remote.url().into()));
Ok(SshGitCommand {
transport,
completion,
})
}
pub(crate) fn open_receive_pack(
&self,
remote: &GitRemote,
control: &GitOperationControl,
) -> Result<SshRawGitCommand, GitError> {
self.open_git_command(remote, GitService::ReceivePack, control)
}
fn open_git_command(
&self,
remote: &GitRemote,
service: GitService,
control: &GitOperationControl,
) -> Result<SshRawGitCommand, GitError> {
let endpoint = remote
.endpoint()
.as_ssh()
.ok_or(GitError::ForbiddenRemoteUrl)?;
let command = git_service_command(service, endpoint.path())?;
let channel = self.runtime.block_on(async {
controlled(
tokio::time::timeout(
@@ -222,22 +268,13 @@ impl SshSession {
let result = pump_command(channel, input_rx, output_tx, &operation).await;
let _ = completion_tx.send(result);
});
let transport = gix::protocol::transport::client::git::blocking_io::Connection::new(
SshStdout {
Ok(SshRawGitCommand {
stdout: SshStdout {
receiver: output_rx,
current: Vec::new(),
offset: 0,
},
SshStdin { sender: input_tx },
gix::protocol::transport::Protocol::V1,
endpoint.path().as_str().as_bytes().to_vec(),
None::<(String, Option<u16>)>,
gix::protocol::transport::client::git::ConnectMode::Process,
false,
)
.custom_url(Some(remote.url().into()));
Ok(SshGitCommand {
transport,
stdin: SshStdin { sender: input_tx },
completion: completion_rx,
})
}
@@ -359,9 +396,19 @@ async fn pump_command(
}
}
fn upload_pack_command(path: &SshRepositoryPath) -> Result<Vec<u8>, GitError> {
#[derive(Clone, Copy)]
enum GitService {
UploadPack,
ReceivePack,
}
fn git_service_command(service: GitService, path: &SshRepositoryPath) -> Result<Vec<u8>, GitError> {
let service = match service {
GitService::UploadPack => "git-upload-pack",
GitService::ReceivePack => "git-receive-pack",
};
let path = shell_quote(path.as_str());
let command = format!("git-upload-pack {path}");
let command = format!("{service} {path}");
if command.len() > 64 * 1024 {
return Err(GitError::ForbiddenRemoteUrl);
}
@@ -949,7 +996,9 @@ mod tests {
repository::SecretBytes,
};
use super::{SshSession, persist_confirmed_host, ssh_host_key, upload_pack_command};
use super::{
GitService, SshSession, git_service_command, persist_confirmed_host, ssh_host_key,
};
struct Passphrase(Option<&'static [u8]>);
@@ -1321,13 +1370,18 @@ mod tests {
),
] {
let endpoint = RemoteEndpoint::parse(url).expect("valid SSH endpoint");
let command =
upload_pack_command(endpoint.as_ssh().expect("SSH").path()).expect("safe command");
let command = std::str::from_utf8(&command).expect("UTF-8 command");
assert_eq!(
shlex::split(command).expect("shell command"),
["git-upload-pack", expected]
);
for (service, expected_service) in [
(GitService::UploadPack, "git-upload-pack"),
(GitService::ReceivePack, "git-receive-pack"),
] {
let command = git_service_command(service, endpoint.as_ssh().expect("SSH").path())
.expect("safe command");
let command = std::str::from_utf8(&command).expect("UTF-8 command");
assert_eq!(
shlex::split(command).expect("shell command"),
[expected_service, expected]
);
}
}
assert!(RemoteEndpoint::parse("git@example.test:-upload-pack").is_err());
assert!(RemoteEndpoint::parse("git@example.test:repo\nsecond").is_err());