Radish alpha
h
Radicle Heartwood Protocol & Stack
Radicle
Git (anonymous pull)
Log in to clone via SSH
node: Use `gix_packetline`
✗ CI failure Lorenz Leutgeb committed 2 months ago
commit 4dabfaa701ea17a51d44dbd597b240c1b1961be2
parent c06b00e330d82c8b8221cc8f8776c883208d159f
1 failed (1 total) View logs
5 files changed +99 -113
modified Cargo.lock
@@ -3036,6 +3036,7 @@ dependencies = [
 "crossbeam-channel",
 "cyphernet",
 "fastrand",
+
 "gix-packetline",
 "lexopt",
 "log",
 "mio 1.0.4",
modified Cargo.toml
@@ -30,6 +30,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.19.0", default-features = false }
+
gix-packetline = { version = "0.19.1", default-features = false }
human-panic = "2.0.6"
itertools = "0.14"
lexopt = "0.3.0"
modified 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"] }
modified crates/radicle-node/src/worker.rs
@@ -141,15 +141,52 @@ 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::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 Ok(header) = upload_pack::GitRequest::try_from(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) {
modified crates/radicle-node/src/worker/upload_pack.rs
@@ -25,7 +25,7 @@ pub fn upload_pack<R, W>(
    remote: NodeId,
    storage: &Storage,
    emitter: &Emitter<Event>,
-
    header: &pktline::GitRequest,
+
    header: &GitRequest,
    mut recv: R,
    send: W,
    timeout: Duration,
@@ -239,119 +239,65 @@ where
    }
}

-
pub(super) mod pktline {
-
    use std::io;
-
    use std::io::Read;
-
    use std::str;
-

-
    use radicle::prelude::RepoId;
-

-
    pub const HEADER_LEN: usize = 4;
-

-
    /// Read and parse the `GitRequest` data from the client side.
-
    pub fn git_request<R>(reader: &mut R) -> io::Result<GitRequest>
-
    where
-
        R: io::Read,
-
    {
-
        let mut reader = Reader::new(reader);
-
        let (header, _) = reader.read_request_pktline()?;
-
        Ok(header)
-
    }
-

-
    struct Reader<'a, R> {
-
        stream: &'a mut R,
-
    }
-

-
    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<u8>)> {
-
            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<usize> {
-
            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)
-
        }
-
    }
+
/// The Git request packet-line for a repository.
+
/// 
+
/// See <https://git-scm.com/docs/pack-protocol.html#_git_transport>.
+
///
+
/// 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<u16>)>,
+
    pub extra: Vec<(String, Option<String>)>,
+
}

-
    impl<R: io::Read> io::Read for Reader<'_, R> {
-
        fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
-
            self.stream.read(buf)
-
        }
-
    }
+
impl GitRequest {
+
    /// Parse a Git command from a packet-line.
+
    fn parse(input: &[u8]) -> Option<Self> {
+
        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::<u16>().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();

-
    /// 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<u16>)>,
-
        pub extra: Vec<(String, Option<String>)>,
+
        Some(Self {
+
            repo,
+
            path,
+
            host,
+
            extra,
+
        })
    }
+
}

-
    impl GitRequest {
-
        /// Parse a Git command from a packet-line.
-
        fn parse(input: &[u8]) -> Option<Self> {
-
            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::<u16>().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();
+
impl TryFrom<gix_packetline::PacketLineRef<'_>> for GitRequest {
+
    type Error = ();

-
            Some(Self {
-
                repo,
-
                path,
-
                host,
-
                extra,
-
            })
-
        }
+
    fn try_from(value: gix_packetline::PacketLineRef<'_>) -> Result<Self, Self::Error> {
+
        value.as_slice().and_then(Self::parse).ok_or(())
    }
}