git_zero_rustix/
network.rs1use 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 #[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 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}