Skip to main content

git_zero_rustix/network/
client.rs

1//! Blocking IPv4 HTTP/1.0 client transport.
2use super::{SocketWriter, content_length, header_end, receive, socket, timeouts};
3use core::{fmt::Write as _, net::SocketAddrV4};
4use git_zero::{
5    Error, Result,
6    client::Transport,
7    http::Request,
8    io::{SliceWriter, Write},
9};
10use rustix::{fd::AsFd, net};
11
12/// Numeric IPv4 addresses only. TLS, redirects, and chunked transfer decoding
13/// belong in richer host transports. All exchanges use fixed caller storage.
14pub struct HttpTransport<'a> {
15    pub address: SocketAddrV4,
16    pub repository_path: &'a str,
17    pub wire_storage: &'a mut [u8],
18}
19
20impl Transport for HttpTransport<'_> {
21    fn request(&mut self, request: Request<'_>, response: &mut [u8]) -> Result<(u16, usize)> {
22        let socket = socket(net::AddressFamily::INET)?;
23        timeouts(&socket)?;
24        net::connect(&socket, &self.address).map_err(|_| Error::Backend)?;
25        self.send_request(&socket, request)?;
26        self.read_response(&socket, response)
27    }
28}
29
30impl HttpTransport<'_> {
31    fn send_request(&mut self, socket: &impl AsFd, request: Request<'_>) -> Result<()> {
32        let mut header = SliceWriter::new(self.wire_storage);
33        write!(
34            &mut header,
35            "{} {}/{}",
36            request.method,
37            self.repository_path.trim_end_matches('/'),
38            request.path.trim_start_matches('/')
39        )
40        .map_err(|_| Error::BufferTooSmall)?;
41        if let Some(query) = request.query {
42            write!(&mut header, "?{query}").map_err(|_| Error::BufferTooSmall)?;
43        }
44        write!(
45            &mut header,
46            " HTTP/1.0\r\nHost: {}\r\nContent-Length: {}\r\n",
47            self.address,
48            request.body.len()
49        )
50        .map_err(|_| Error::BufferTooSmall)?;
51        for (name, value) in [
52            ("Content-Type", request.content_type),
53            ("Git-Protocol", request.git_protocol),
54        ] {
55            if let Some(value) = value {
56                if value.bytes().any(|b| matches!(b, 0 | b'\r' | b'\n')) {
57                    return Err(Error::InvalidRequest);
58                }
59                write!(&mut header, "{name}: {value}\r\n").map_err(|_| Error::BufferTooSmall)?;
60            }
61        }
62        header.write_all(b"Connection: close\r\n\r\n")?;
63        let mut output = SocketWriter(socket.as_fd());
64        output.write_all(header.as_bytes())?;
65        output.write_all(request.body)?;
66        Ok(())
67    }
68
69    fn read_response(&mut self, socket: &impl AsFd, response: &mut [u8]) -> Result<(u16, usize)> {
70        let length = read_to_end(socket, self.wire_storage)?;
71        let end = header_end(&self.wire_storage[..length]).ok_or(Error::InvalidRequest)?;
72        let header =
73            core::str::from_utf8(&self.wire_storage[..end]).map_err(|_| Error::InvalidRequest)?;
74        let status = header
75            .lines()
76            .next()
77            .and_then(|line| line.split_ascii_whitespace().nth(1))
78            .ok_or(Error::InvalidRequest)?
79            .parse()
80            .map_err(|_| Error::InvalidRequest)?;
81        let body = &self.wire_storage[end..length];
82        if content_length(header)?.is_some_and(|expected| expected != body.len()) {
83            return Err(Error::InvalidRequest);
84        }
85        response
86            .get_mut(..body.len())
87            .ok_or(Error::BufferTooSmall)?
88            .copy_from_slice(body);
89        Ok((status, body.len()))
90    }
91}
92
93// Preserve EOF framing while rejecting responses beyond the caller's wire budget.
94fn read_to_end(socket: &impl AsFd, storage: &mut [u8]) -> Result<usize> {
95    let mut length = 0;
96    loop {
97        if length == storage.len() {
98            let mut extra = [0; 1];
99            if receive(socket, &mut extra)? != 0 {
100                return Err(Error::BufferTooSmall);
101            }
102            break;
103        }
104        let count = receive(socket, &mut storage[length..])?;
105        if count == 0 {
106            break;
107        }
108        length += count;
109    }
110    Ok(length)
111}