Skip to main content

git_zero_rustix/
network.rs

1//! Shared blocking IP sockets and HTTP/1.0 framing primitives.
2use core::time::Duration;
3use git_zero::{Error, Result, io::Write};
4use rustix::{
5    fd::{AsFd, BorrowedFd, OwnedFd},
6    net::{
7        self, AddressFamily, RecvFlags, SendFlags, SocketFlags, SocketType,
8        sockopt::{self, Timeout},
9    },
10};
11
12#[cfg(feature = "client")]
13mod client;
14#[cfg(feature = "client")]
15pub use client::HttpTransport;
16#[cfg(feature = "server")]
17mod server;
18#[cfg(feature = "server")]
19pub use server::{
20    AcceptedSocketTimeouts, ChunkedLimits, HeaderLimits, RequestDeadlines, RequestLimits,
21    RequestPolicy, ResponseDeadlines, accept, accept_with_timeouts, listen, listen_on,
22    local_address, local_port, read_request, read_request_chunked, read_request_from,
23    read_request_from_chunked, read_request_with_deadlines, read_request_with_limits,
24    read_request_with_policy, respond, respond_headers, respond_published,
25    respond_published_with_deadlines, respond_stream, respond_stream_with_deadlines,
26};
27
28#[cfg(all(feature = "server", feature = "storage"))]
29pub use server::respond_file_published;
30
31#[cfg(not(target_vendor = "apple"))]
32const SOCKET_FLAGS: SocketFlags = SocketFlags::CLOEXEC;
33#[cfg(target_vendor = "apple")]
34const SOCKET_FLAGS: SocketFlags = SocketFlags::empty();
35
36fn close_on_exec(socket: OwnedFd) -> Result<OwnedFd> {
37    // Darwin has no SOCK_CLOEXEC/accept4 flags; set the descriptor flag after
38    // creation. Other platforms set it atomically through SOCKET_FLAGS.
39    #[cfg(target_vendor = "apple")]
40    rustix::io::fcntl_setfd(&socket, rustix::io::FdFlags::CLOEXEC).map_err(|_| Error::Backend)?;
41    Ok(socket)
42}
43
44fn socket(family: AddressFamily) -> Result<OwnedFd> {
45    close_on_exec(
46        net::socket_with(family, SocketType::STREAM, SOCKET_FLAGS, None)
47            .map_err(|_| Error::Backend)?,
48    )
49}
50
51#[cfg(feature = "client")]
52fn timeouts(socket: &impl AsFd) -> Result<()> {
53    apply_timeouts(
54        socket,
55        Some(Duration::from_secs(10)),
56        Some(Duration::from_secs(10)),
57    )
58}
59
60fn apply_timeouts(
61    socket: &impl AsFd,
62    read: Option<Duration>,
63    write: Option<Duration>,
64) -> Result<()> {
65    for (timeout, value) in [(Timeout::Recv, read), (Timeout::Send, write)] {
66        sockopt::set_socket_timeout(socket, timeout, value).map_err(|_| Error::Backend)?;
67    }
68    Ok(())
69}
70
71pub struct SocketWriter<'a>(pub BorrowedFd<'a>);
72
73impl Write for SocketWriter<'_> {
74    fn write_all(&mut self, mut bytes: &[u8]) -> Result<()> {
75        #[cfg(target_vendor = "apple")]
76        let flags = {
77            // Cover borrowed host sockets as well as sockets opened here.
78            sockopt::set_socket_nosigpipe(self.0, true).map_err(|_| Error::Backend)?;
79            SendFlags::empty()
80        };
81        #[cfg(not(target_vendor = "apple"))]
82        let flags = SendFlags::NOSIGNAL;
83        while !bytes.is_empty() {
84            match net::send(self.0, bytes, flags) {
85                Ok(0) => return Err(Error::Backend),
86                Ok(length) => bytes = &bytes[length..],
87                Err(rustix::io::Errno::INTR) => continue,
88                Err(_) => return Err(Error::Backend),
89            }
90        }
91        Ok(())
92    }
93}
94
95fn receive(socket: &impl AsFd, storage: &mut [u8]) -> Result<usize> {
96    loop {
97        match net::recv(socket, &mut *storage, RecvFlags::empty()) {
98            Ok((length, _)) => return Ok(length),
99            Err(rustix::io::Errno::INTR) => continue,
100            Err(_) => return Err(Error::Backend),
101        }
102    }
103}
104
105fn header_end(bytes: &[u8]) -> Option<usize> {
106    bytes
107        .windows(4)
108        .position(|part| part == b"\r\n\r\n")
109        .map(|index| index + 4)
110}
111
112#[cfg(feature = "client")]
113fn content_length(header: &str) -> Result<Option<usize>> {
114    let mut result = None;
115    for line in header.split("\r\n").skip(1) {
116        if line.is_empty() {
117            continue;
118        }
119        let (name, value) = line.split_once(':').ok_or(Error::InvalidRequest)?;
120        if name.eq_ignore_ascii_case("transfer-encoding") {
121            return Err(Error::Unsupported);
122        }
123        if name.eq_ignore_ascii_case("content-length") {
124            if result.is_some() {
125                return Err(Error::InvalidRequest);
126            }
127            result = Some(parse_length(value.trim())?);
128        }
129    }
130    Ok(result)
131}
132
133fn parse_length(value: &str) -> Result<usize> {
134    if value.is_empty() || !value.bytes().all(|byte| byte.is_ascii_digit()) {
135        return Err(Error::InvalidRequest);
136    }
137    value.parse().map_err(|_| Error::InvalidRequest)
138}