Implement SSH receive-pack pushes
This commit is contained in:
@@ -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());
|
||||
|
||||
Reference in New Issue
Block a user