diff --git a/Cargo.lock b/Cargo.lock index c822c4e5..5c1142ed 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3133,6 +3133,7 @@ dependencies = [ "crossbeam-channel", "cyphernet", "fastrand", + "gix-packetline", "lexopt", "log", "mio 1.0.4", diff --git a/Cargo.toml b/Cargo.toml index e106df8a..0ccc59f1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -31,6 +31,7 @@ dunce = "1.0.5" fastrand = { version = "2.0.0", default-features = false } git2 = { version = "0.19.0", default-features = false, features = ["vendored-libgit2"] } gix-hash = { version = "0.22.1", default-features = false, features = ["sha1"] } +gix-packetline = { version = "0.21.1", default-features = false } human-panic = "2.0.6" itertools = "0.14" lexopt = "0.3.0" diff --git a/crates/radicle-node/Cargo.toml b/crates/radicle-node/Cargo.toml index dd55ec2e..d6afff16 100644 --- a/crates/radicle-node/Cargo.toml +++ b/crates/radicle-node/Cargo.toml @@ -23,6 +23,7 @@ colored = { workspace = true } crossbeam-channel = { workspace = true } cyphernet = { workspace = true, features = ["tor", "dns", "ed25519", "p2p-ed25519", "noise-framework", "noise_sha2"] } fastrand = { workspace = true } +gix-packetline = { workspace = true, features = ["blocking-io"] } lexopt = { workspace = true } log = { workspace = true, features = ["kv", "std"] } mio = { version = "1", features = ["net", "os-poll"] } diff --git a/crates/radicle-node/src/worker.rs b/crates/radicle-node/src/worker.rs index 0449b526..52eaa955 100644 --- a/crates/radicle-node/src/worker.rs +++ b/crates/radicle-node/src/worker.rs @@ -141,15 +141,51 @@ impl Worker { let timeout = channels.timeout(); let (mut stream_r, stream_w) = channels.split(); - let header = match upload_pack::pktline::git_request(&mut stream_r) { - Ok(header) => header, - Err(e) => { + + let mut iter = gix_packetline::blocking_io::StreamingPeekableIter::new( + &mut stream_r, + &[gix_packetline::PacketLineRef::Flush], + false, /* packet tracing */ + ); + + let header = match iter.read_line() { + None => { + return FetchResult::Responder { + rid: None, + result: Err(UploadError::PacketLine(std::io::Error::new( + std::io::ErrorKind::UnexpectedEof, + "unexpected end of stream while reading upload-pack header", + ))), + } + } + Some(Err(e)) => { return FetchResult::Responder { rid: None, result: Err(UploadError::PacketLine(e)), } } + Some(Ok(Err(e))) => { + return FetchResult::Responder { + rid: None, + result: Err(UploadError::PacketLine(std::io::Error::new( + std::io::ErrorKind::InvalidData, + format!("invalid upload-pack header: {e}"), + ))), + } + } + Some(Ok(Ok(header))) => header, }; + + let Some(header) = upload_pack::GitRequest::from_packetline(header) else { + return FetchResult::Responder { + rid: None, + result: Err(UploadError::PacketLine(std::io::Error::new( + std::io::ErrorKind::InvalidData, + "failed to parse upload-pack header", + ))), + }; + }; + log::debug!(target: "worker", "Spawning upload-pack process for {} on stream {stream}..", header.repo); if let Err(e) = self.is_authorized(remote, header.repo) { diff --git a/crates/radicle-node/src/worker/upload_pack.rs b/crates/radicle-node/src/worker/upload_pack.rs index d36b30ff..beea8495 100644 --- a/crates/radicle-node/src/worker/upload_pack.rs +++ b/crates/radicle-node/src/worker/upload_pack.rs @@ -25,7 +25,7 @@ pub fn upload_pack( remote: NodeId, storage: &Storage, emitter: &Emitter, - header: &pktline::GitRequest, + header: &GitRequest, mut recv: R, send: W, timeout: Duration, @@ -239,119 +239,63 @@ where } } -pub(super) mod pktline { - use std::io; - use std::io::Read; - use std::str; +/// The Git request packet-line for a repository. +/// +/// See . +/// +/// Example: `0032git-upload-pack /rad:z3gqcJUoA1n9HaHKufZs5FCSGazv5.git\0host=myserver.com\0` +#[derive(Debug)] +pub struct GitRequest { + pub repo: RepoId, + #[allow(dead_code)] + pub path: String, + #[allow(dead_code)] + pub host: Option<(String, Option)>, + pub extra: Vec<(String, Option)>, +} - use radicle::prelude::RepoId; - - pub const HEADER_LEN: usize = 4; - - /// Read and parse the `GitRequest` data from the client side. - pub fn git_request(reader: &mut R) -> io::Result - where - R: io::Read, - { - let mut reader = Reader::new(reader); - let (header, _) = reader.read_request_pktline()?; - Ok(header) +impl GitRequest { + pub(super) fn from_packetline( + packet_line: gix_packetline::PacketLineRef<'_>, + ) -> Option { + packet_line.as_slice().and_then(Self::parse) } - struct Reader<'a, R> { - stream: &'a mut R, - } + /// Parse a Git command from a packet-line. + fn parse(input: &[u8]) -> Option { + let input = std::str::from_utf8(input).ok()?; + let mut parts = input + .strip_prefix("git-upload-pack ")? + .split_terminator('\0'); - impl<'a, R: io::Read> Reader<'a, R> { - /// Create a new packet-line reader. - pub fn new(stream: &'a mut R) -> Self { - Self { stream } - } - - /// Parse a Git request packet-line. - /// - /// Example: `0032git-upload-pack /project.git\0host=myserver.com\0` - /// - fn read_request_pktline(&mut self) -> io::Result<(GitRequest, Vec)> { - let mut pktline = [0u8; 1024]; - let length = self.read_pktline(&mut pktline)?; - let Some(cmd) = GitRequest::parse(&pktline[4..length]) else { - return Err(io::ErrorKind::InvalidInput.into()); - }; - Ok((cmd, Vec::from(&pktline[..length]))) - } - - /// Parse a Git packet-line. - fn read_pktline(&mut self, buf: &mut [u8]) -> io::Result { - self.read_exact(&mut buf[..HEADER_LEN])?; - - let length = str::from_utf8(&buf[..HEADER_LEN]) - .map_err(|e| io::Error::new(io::ErrorKind::InvalidInput, e.to_string()))?; - let length = usize::from_str_radix(length, 16) - .map_err(|e| io::Error::new(io::ErrorKind::InvalidInput, e.to_string()))?; - - self.read_exact(&mut buf[HEADER_LEN..length])?; - - Ok(length) - } - } - - impl io::Read for Reader<'_, R> { - fn read(&mut self, buf: &mut [u8]) -> io::Result { - self.stream.read(buf) - } - } - - /// The Git request packet-line for a Heartwood repository. - /// - /// Example: `0032git-upload-pack /rad:z3gqcJUoA1n9HaHKufZs5FCSGazv5.git\0host=myserver.com\0` - #[derive(Debug)] - pub struct GitRequest { - pub repo: RepoId, - #[allow(dead_code)] - pub path: String, - #[allow(dead_code)] - pub host: Option<(String, Option)>, - pub extra: Vec<(String, Option)>, - } - - impl GitRequest { - /// Parse a Git command from a packet-line. - fn parse(input: &[u8]) -> Option { - let input = str::from_utf8(input).ok()?; - let mut parts = input - .strip_prefix("git-upload-pack ")? - .split_terminator('\0'); - - let path = parts.next()?.to_owned(); - let repo = path.strip_prefix('/')?.parse().ok()?; - let host = match parts.next() { - None | Some("") => None, - Some(host) => { - let host = host.strip_prefix("host=")?; - match host.split_once(':') { - None => Some((host.to_owned(), None)), - Some((host, port)) => { - let port = port.parse::().ok()?; - Some((host.to_owned(), Some(port))) - } + let path = parts.next()?.to_owned(); + let repo = path.strip_prefix('/')?.parse().ok()?; + let host = match parts.next() { + None | Some("") => None, + Some(host) => { + let host = host.strip_prefix("host=")?; + match host.split_once(':') { + None => Some((host.to_owned(), None)), + Some((host, port)) => { + let port = port.parse::().ok()?; + Some((host.to_owned(), Some(port))) } } - }; - let extra = parts - .skip_while(|part| part.is_empty()) - .map(|part| match part.split_once('=') { - None => (part.to_owned(), None), - Some((k, v)) => (k.to_owned(), Some(v.to_owned())), - }) - .collect(); - - Some(Self { - repo, - path, - host, - extra, + } + }; + let extra = parts + .skip_while(|part| part.is_empty()) + .map(|part| match part.split_once('=') { + None => (part.to_owned(), None), + Some((k, v)) => (k.to_owned(), Some(v.to_owned())), }) - } + .collect(); + + Some(Self { + repo, + path, + host, + extra, + }) } }