use core::time::Duration;
use git_zero::{Error, Result, io::Write};
use rustix::{
fd::{AsFd, BorrowedFd, OwnedFd},
net::{
self, AddressFamily, RecvFlags, SendFlags, SocketFlags, SocketType,
sockopt::{self, Timeout},
},
};
#[cfg(feature = "client")]
mod client;
#[cfg(feature = "client")]
pub use client::HttpTransport;
#[cfg(feature = "server")]
mod server;
#[cfg(feature = "server")]
pub use server::{
AcceptedSocketTimeouts, ChunkedLimits, HeaderLimits, RequestDeadlines, RequestLimits,
RequestPolicy, ResponseDeadlines, accept, accept_with_timeouts, listen, listen_on,
local_address, local_port, read_request, read_request_chunked, read_request_from,
read_request_from_chunked, read_request_with_deadlines, read_request_with_limits,
read_request_with_policy, respond, respond_headers, respond_published,
respond_published_with_deadlines, respond_stream, respond_stream_with_deadlines,
};
#[cfg(all(feature = "server", feature = "storage"))]
pub use server::respond_file_published;
#[cfg(not(target_vendor = "apple"))]
const SOCKET_FLAGS: SocketFlags = SocketFlags::CLOEXEC;
#[cfg(target_vendor = "apple")]
const SOCKET_FLAGS: SocketFlags = SocketFlags::empty();
fn close_on_exec(socket: OwnedFd) -> Result<OwnedFd> {
#[cfg(target_vendor = "apple")]
rustix::io::fcntl_setfd(&socket, rustix::io::FdFlags::CLOEXEC).map_err(|_| Error::Backend)?;
Ok(socket)
}
fn socket(family: AddressFamily) -> Result<OwnedFd> {
close_on_exec(
net::socket_with(family, SocketType::STREAM, SOCKET_FLAGS, None)
.map_err(|_| Error::Backend)?,
)
}
#[cfg(feature = "client")]
fn timeouts(socket: &impl AsFd) -> Result<()> {
apply_timeouts(
socket,
Some(Duration::from_secs(10)),
Some(Duration::from_secs(10)),
)
}
fn apply_timeouts(
socket: &impl AsFd,
read: Option<Duration>,
write: Option<Duration>,
) -> Result<()> {
for (timeout, value) in [(Timeout::Recv, read), (Timeout::Send, write)] {
sockopt::set_socket_timeout(socket, timeout, value).map_err(|_| Error::Backend)?;
}
Ok(())
}
pub struct SocketWriter<'a>(pub BorrowedFd<'a>);
impl Write for SocketWriter<'_> {
fn write_all(&mut self, mut bytes: &[u8]) -> Result<()> {
#[cfg(target_vendor = "apple")]
let flags = {
sockopt::set_socket_nosigpipe(self.0, true).map_err(|_| Error::Backend)?;
SendFlags::empty()
};
#[cfg(not(target_vendor = "apple"))]
let flags = SendFlags::NOSIGNAL;
while !bytes.is_empty() {
match net::send(self.0, bytes, flags) {
Ok(0) => return Err(Error::Backend),
Ok(length) => bytes = &bytes[length..],
Err(rustix::io::Errno::INTR) => continue,
Err(_) => return Err(Error::Backend),
}
}
Ok(())
}
}
fn receive(socket: &impl AsFd, storage: &mut [u8]) -> Result<usize> {
loop {
match net::recv(socket, &mut *storage, RecvFlags::empty()) {
Ok((length, _)) => return Ok(length),
Err(rustix::io::Errno::INTR) => continue,
Err(_) => return Err(Error::Backend),
}
}
}
fn header_end(bytes: &[u8]) -> Option<usize> {
bytes
.windows(4)
.position(|part| part == b"\r\n\r\n")
.map(|index| index + 4)
}
#[cfg(feature = "client")]
fn content_length(header: &str) -> Result<Option<usize>> {
let mut result = None;
for line in header.split("\r\n").skip(1) {
if line.is_empty() {
continue;
}
let (name, value) = line.split_once(':').ok_or(Error::InvalidRequest)?;
if name.eq_ignore_ascii_case("transfer-encoding") {
return Err(Error::Unsupported);
}
if name.eq_ignore_ascii_case("content-length") {
if result.is_some() {
return Err(Error::InvalidRequest);
}
result = Some(parse_length(value.trim())?);
}
}
Ok(result)
}
fn parse_length(value: &str) -> Result<usize> {
if value.is_empty() || !value.bytes().all(|byte| byte.is_ascii_digit()) {
return Err(Error::InvalidRequest);
}
value.parse().map_err(|_| Error::InvalidRequest)
}