use super::{SOCKET_FLAGS, SocketWriter, apply_timeouts, close_on_exec, receive, socket};
use core::{
fmt::Write as _,
net::{Ipv4Addr, SocketAddr, SocketAddrV4},
time::Duration,
};
use git_zero::{
Error, Result,
http::{Request, Response},
io::{SliceWriter, Write},
};
use rustix::{
fd::{AsFd, OwnedFd},
net::{self, sockopt},
};
mod chunked;
pub use chunked::ChunkedLimits;
mod deadline;
use deadline::{DeadlineReader, DeadlineWriter};
pub use deadline::{RequestDeadlines, ResponseDeadlines};
mod header;
use header::{Headers, parse_request, read_header, request_line};
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub struct RequestLimits {
pub max_wire_bytes: Option<usize>,
pub max_body_bytes: Option<usize>,
}
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub struct HeaderLimits {
pub max_bytes: Option<usize>,
pub max_fields: Option<usize>,
pub max_name_bytes: Option<usize>,
pub max_value_bytes: Option<usize>,
}
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub struct RequestPolicy {
pub request: RequestLimits,
pub headers: HeaderLimits,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct AcceptedSocketTimeouts {
pub read: Option<Duration>,
pub write: Option<Duration>,
}
impl Default for AcceptedSocketTimeouts {
fn default() -> Self {
Self {
read: Some(Duration::from_secs(10)),
write: Some(Duration::from_secs(10)),
}
}
}
pub fn listen(port: u16) -> Result<OwnedFd> {
listen_on(SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::LOCALHOST, port)))
}
pub fn listen_on(address: SocketAddr) -> Result<OwnedFd> {
let family = if address.is_ipv4() {
net::AddressFamily::INET
} else {
net::AddressFamily::INET6
};
let socket = socket(family)?;
#[cfg(not(target_os = "openbsd"))]
if address.is_ipv6() {
sockopt::set_ipv6_v6only(&socket, true).map_err(|_| Error::Backend)?;
}
sockopt::set_socket_reuseaddr(&socket, true).map_err(|_| Error::Backend)?;
net::bind(&socket, &address).map_err(|_| Error::Backend)?;
net::listen(&socket, 128).map_err(|_| Error::Backend)?;
Ok(socket)
}
pub fn local_port(socket: &impl AsFd) -> Result<u16> {
Ok(local_address(socket)?.port())
}
pub fn local_address(socket: &impl AsFd) -> Result<SocketAddr> {
let address = net::getsockname(socket).map_err(|_| Error::Backend)?;
SocketAddr::try_from(address).map_err(|_| Error::Backend)
}
pub fn accept(socket: &impl AsFd) -> Result<OwnedFd> {
accept_with_timeouts(socket, AcceptedSocketTimeouts::default())
}
pub fn accept_with_timeouts(
socket: &impl AsFd,
timeouts: AcceptedSocketTimeouts,
) -> Result<OwnedFd> {
if [timeouts.read, timeouts.write]
.into_iter()
.flatten()
.any(|value| value.is_zero())
{
return Err(Error::InvalidRequest);
}
accept_configured(socket, |client| {
apply_timeouts(client, timeouts.read, timeouts.write)
})
}
fn accept_configured(
socket: &impl AsFd,
configure: impl FnOnce(&OwnedFd) -> Result<()>,
) -> Result<OwnedFd> {
let client =
close_on_exec(net::accept_with(socket, SOCKET_FLAGS).map_err(|_| Error::Backend)?)?;
configure(&client)?;
Ok(client)
}
pub fn read_request<'a>(socket: &impl AsFd, storage: &'a mut [u8]) -> Result<Request<'a>> {
read_request_with_limits(socket, storage, RequestLimits::default())
}
pub fn read_request_with_limits<'a>(
socket: &impl AsFd,
storage: &'a mut [u8],
limits: RequestLimits,
) -> Result<Request<'a>> {
read_request_with_policy(
socket,
storage,
RequestPolicy {
request: limits,
headers: HeaderLimits::default(),
},
)
}
pub fn read_request_with_policy<'a>(
socket: &impl AsFd,
storage: &'a mut [u8],
policy: RequestPolicy,
) -> Result<Request<'a>> {
read_request_from(&mut |bytes| receive(socket, bytes), storage, policy)
}
pub fn read_request_with_deadlines<'a>(
socket: &impl AsFd,
storage: &'a mut [u8],
policy: RequestPolicy,
deadlines: RequestDeadlines,
) -> Result<Request<'a>> {
read_socket_request(socket, storage, policy, deadlines, None)
}
pub fn read_request_from<'a>(
receive: &mut impl FnMut(&mut [u8]) -> Result<usize>,
storage: &'a mut [u8],
policy: RequestPolicy,
) -> Result<Request<'a>> {
read_request_framed(receive, storage, policy, None)
}
pub fn read_request_from_chunked<'a>(
receive: &mut impl FnMut(&mut [u8]) -> Result<usize>,
storage: &'a mut [u8],
policy: RequestPolicy,
chunks: ChunkedLimits,
) -> Result<Request<'a>> {
read_request_framed(receive, storage, policy, Some(chunks))
}
pub fn read_request_chunked<'a>(
socket: &impl AsFd,
storage: &'a mut [u8],
policy: RequestPolicy,
deadlines: RequestDeadlines,
chunks: ChunkedLimits,
) -> Result<Request<'a>> {
read_socket_request(socket, storage, policy, deadlines, Some(chunks))
}
fn read_socket_request<'a>(
socket: &impl AsFd,
storage: &'a mut [u8],
policy: RequestPolicy,
deadlines: RequestDeadlines,
chunks: Option<ChunkedLimits>,
) -> Result<Request<'a>> {
if deadlines == RequestDeadlines::default() {
return read_request_framed(&mut |bytes| receive(socket, bytes), storage, policy, chunks);
}
let mut reader = DeadlineReader::new(socket, deadlines)?;
let result = read_request_framed(&mut |bytes| reader.receive(bytes), storage, policy, chunks);
reader.check()?;
result
}
fn read_request_framed<'a>(
receive: &mut impl FnMut(&mut [u8]) -> Result<usize>,
storage: &'a mut [u8],
policy: RequestPolicy,
chunks: Option<ChunkedLimits>,
) -> Result<Request<'a>> {
let limits = policy.request;
let (end, length) = read_header(
receive,
storage,
limits.max_wire_bytes,
policy.headers.max_bytes,
)?;
let (header, storage) = storage.split_at_mut(end);
let length = length - end;
let header = core::str::from_utf8(header).map_err(|_| Error::InvalidRequest)?;
let line = request_line(header)?;
let headers = if chunks.is_some() {
Headers::parse_framed(header, policy.headers, true)?
} else {
Headers::parse(header, policy.headers)?
};
let mut request = parse_request(line, &headers)?;
let identity = headers
.content_encoding
.is_none_or(|value| value.eq_ignore_ascii_case("identity"));
let (wire_length, body_length) = if headers.chunked {
if !line.ends_with(" HTTP/1.1") {
return Err(Error::InvalidRequest);
}
chunked::read(
receive,
storage,
length,
limits.max_wire_bytes.map(|limit| limit - end),
if identity {
limits.max_body_bytes
} else {
None
},
chunks.unwrap(),
)?
} else {
let total = headers
.content_length
.map(super::parse_length)
.transpose()?
.unwrap_or(0);
let total = read_fixed_body(
receive,
storage,
length,
total,
RequestLimits {
max_wire_bytes: limits.max_wire_bytes.map(|limit| limit - end),
max_body_bytes: if identity {
limits.max_body_bytes
} else {
None
},
},
)?;
(total, total)
};
let (wire, expanded) = storage.split_at_mut(wire_length);
request.body = decode_body(
&wire[..body_length],
expanded,
headers.content_encoding,
limits.max_body_bytes,
)?;
Ok(request)
}
fn read_fixed_body(
receive: &mut impl FnMut(&mut [u8]) -> Result<usize>,
storage: &mut [u8],
mut length: usize,
total: usize,
limits: RequestLimits,
) -> Result<usize> {
if limits.max_wire_bytes.is_some_and(|limit| total > limit)
|| limits.max_body_bytes.is_some_and(|limit| total > limit)
{
return Err(Error::LimitExceeded);
}
if total > storage.len() {
return Err(Error::BufferTooSmall);
}
while length < total {
let count = receive(&mut storage[length..total])?;
if count == 0 {
return Err(Error::InvalidRequest);
}
if count > total - length {
return Err(Error::Backend);
}
length += count;
}
Ok(total)
}
fn decode_body<'a>(
body: &'a [u8],
expanded: &'a mut [u8],
encoding: Option<&str>,
max_body_bytes: Option<usize>,
) -> Result<&'a [u8]> {
match encoding {
None => Ok(body),
Some(value) if value.eq_ignore_ascii_case("identity") => Ok(body),
Some(value) if value.eq_ignore_ascii_case("gzip") => {
use git_zero::zlib::{InflateLimits, WINDOW_SIZE, decode_gzip};
let length = decode_gzip(
body,
expanded,
&mut [0; WINDOW_SIZE],
InflateLimits {
max_output: max_body_bytes
.map_or(expanded.len(), |limit| limit.min(expanded.len()))
as u64,
max_ratio: 128,
ratio_slack: expanded.len() as u64,
},
)
.map_err(|error| {
if error == Error::BufferTooSmall
&& max_body_bytes.is_some_and(|limit| limit <= expanded.len())
{
Error::LimitExceeded
} else {
error
}
})?;
Ok(&expanded[..length])
}
_ => Err(Error::Unsupported),
}
}
pub fn respond(socket: &impl AsFd, response: Response, body: &[u8]) -> Result<()> {
respond_headers(socket, response, body.len() as u64)?;
SocketWriter(socket.as_fd()).write_all(body)
}
pub fn respond_stream(
socket: &impl AsFd,
repository: &impl git_zero::Repository,
encoder: &mut git_zero::http::stream::ResponseEncoder<'_>,
staging: &mut [u8],
) -> Result<()> {
respond_stream_using(
&mut SocketWriter(socket.as_fd()),
repository,
encoder,
staging,
)
}
pub fn respond_stream_with_deadlines(
socket: &impl AsFd,
repository: &impl git_zero::Repository,
encoder: &mut git_zero::http::stream::ResponseEncoder<'_>,
staging: &mut [u8],
deadlines: ResponseDeadlines,
) -> Result<()> {
if deadlines == ResponseDeadlines::default() {
return respond_stream(socket, repository, encoder, staging);
}
let mut writer = match DeadlineWriter::new(socket, deadlines) {
Ok(writer) => writer,
Err(error) => {
encoder.cancel();
return Err(error);
}
};
respond_stream_using(&mut writer, repository, encoder, staging)
}
fn respond_stream_using(
writer: &mut impl Write,
repository: &impl git_zero::Repository,
encoder: &mut git_zero::http::stream::ResponseEncoder<'_>,
staging: &mut [u8],
) -> Result<()> {
let result = respond_body(writer, encoder.response(), None, staging, |bytes| {
encoder.read(repository, bytes)
});
if result.is_err() {
encoder.cancel();
}
result
}
fn respond_body(
writer: &mut impl Write,
response: Response,
mut remaining: Option<u64>,
staging: &mut [u8],
mut read: impl FnMut(&mut [u8]) -> Result<usize>,
) -> Result<()> {
if staging.is_empty() {
return Err(Error::BufferTooSmall);
}
let mut storage = [0; 512];
let mut header = SliceWriter::new(&mut storage);
write!(
&mut header,
"HTTP/1.0 {} Response\r\nContent-Type: {}\r\n",
response.status, response.content_type
)
.map_err(|_| Error::BufferTooSmall)?;
if let Some(size) = remaining {
write!(&mut header, "Content-Length: {size}\r\n").map_err(|_| Error::BufferTooSmall)?;
}
header.write_all(b"Cache-Control: no-store\r\nConnection: close\r\n\r\n")?;
writer.write_all(header.as_bytes())?;
loop {
writer.write_all(&[])?; let count = read(staging)?;
if let Some(left) = &mut remaining {
*left = left.checked_sub(count as u64).ok_or(Error::Backend)?;
}
writer.write_all(&staging[..count])?; if count == 0 {
return if remaining.unwrap_or(0) == 0 {
Ok(())
} else {
Err(Error::Backend)
};
}
}
}
pub fn respond_published<R: git_zero::Repository>(
socket: &impl AsFd,
encoder: &mut git_zero::http::dumb::PublishedResponse<'_, R>,
staging: &mut [u8],
) -> Result<()> {
let result = respond_known(
socket,
encoder.response(),
encoder.size(),
staging,
|bytes| encoder.read(bytes),
);
if result.is_err() {
encoder.cancel();
}
result
}
pub fn respond_published_with_deadlines<R: git_zero::Repository>(
socket: &impl AsFd,
encoder: &mut git_zero::http::dumb::PublishedResponse<'_, R>,
staging: &mut [u8],
deadlines: ResponseDeadlines,
) -> Result<()> {
if deadlines == ResponseDeadlines::default() {
return respond_published(socket, encoder, staging);
}
let result = DeadlineWriter::new(socket, deadlines).and_then(|mut writer| {
respond_body(
&mut writer,
encoder.response(),
Some(encoder.size()),
staging,
|bytes| encoder.read(bytes),
)
});
if result.is_err() {
encoder.cancel();
}
result
}
#[cfg(feature = "storage")]
pub fn respond_file_published<R: git_zero::Repository>(
socket: &impl AsFd,
encoder: &mut crate::FilePublishedResponse<'_, R>,
staging: &mut [u8],
) -> Result<()> {
let result = respond_known(
socket,
encoder.response(),
encoder.size(),
staging,
|bytes| encoder.read(bytes),
);
if result.is_err() {
encoder.cancel();
}
result
}
fn respond_known(
socket: &impl AsFd,
response: Response,
size: u64,
staging: &mut [u8],
read: impl FnMut(&mut [u8]) -> Result<usize>,
) -> Result<()> {
respond_body(
&mut SocketWriter(socket.as_fd()),
response,
Some(size),
staging,
read,
)
}
pub fn respond_headers(socket: &impl AsFd, response: Response, body_length: u64) -> Result<()> {
let mut storage = [0; 512];
let mut header = SliceWriter::new(&mut storage);
write!(&mut header, "HTTP/1.0 {} Response\r\nContent-Type: {}\r\nContent-Length: {}\r\nCache-Control: no-store\r\nConnection: close\r\n\r\n", response.status, response.content_type, body_length).map_err(|_| Error::BufferTooSmall)?;
SocketWriter(socket.as_fd()).write_all(header.as_bytes())
}
#[cfg(test)]
mod tests {
extern crate std;
use super::*;
#[test]
fn explicit_ip_listeners_report_bound_addresses_and_isolate_ipv6() {
for value in ["127.0.0.1:0", "[::1]:0"] {
let address: SocketAddr = value.parse().unwrap();
let listener = listen_on(address).unwrap();
let bound = local_address(&listener).unwrap();
assert_eq!(bound.ip(), address.ip());
assert_ne!(bound.port(), 0);
assert_eq!(local_port(&listener).unwrap(), bound.port());
assert!(
rustix::io::fcntl_getfd(&listener)
.unwrap()
.contains(rustix::io::FdFlags::CLOEXEC)
);
let family = if address.is_ipv4() {
net::AddressFamily::INET
} else {
assert!(sockopt::ipv6_v6only(&listener).unwrap());
net::AddressFamily::INET6
};
let client = socket(family).unwrap();
net::connect(&client, &bound).unwrap();
let accepted = accept(&listener).unwrap();
assert!(
rustix::io::fcntl_getfd(&accepted)
.unwrap()
.contains(rustix::io::FdFlags::CLOEXEC)
);
for timeout in [sockopt::Timeout::Recv, sockopt::Timeout::Send] {
assert_eq!(
sockopt::socket_timeout(&accepted, timeout).unwrap(),
Some(core::time::Duration::from_secs(10))
);
}
}
}
#[test]
fn disconnected_stream_cancels_before_any_more_output() {
let (socket, peer) = net::socketpair(
net::AddressFamily::UNIX,
net::SocketType::STREAM,
SOCKET_FLAGS,
None,
)
.unwrap();
let repository = git_zero::EmptyRepository::new("refs/heads/main").unwrap();
let request = Request {
method: "GET",
path: "HEAD",
query: None,
content_type: None,
git_protocol: None,
body: &[],
};
let mut control = [0; 128];
let mut encoder =
git_zero::http::stream::ResponseEncoder::new(&repository, request, &mut control)
.unwrap();
drop(peer);
assert_eq!(
respond_stream(&socket, &repository, &mut encoder, &mut [0; 3]),
Err(Error::Backend)
);
assert_eq!(
encoder.read(&repository, &mut [0; 3]),
Err(Error::InvalidRequest)
);
}
#[test]
fn failed_accepted_configuration_closes_the_child() {
use std::io::Read;
let listener = listen(0).unwrap();
let mut peer = std::net::TcpStream::connect(local_address(&listener).unwrap()).unwrap();
peer.set_read_timeout(Some(Duration::from_secs(2))).unwrap();
assert!(matches!(
accept_configured(&listener, |_| Err(Error::Backend)),
Err(Error::Backend)
));
assert_eq!(peer.read(&mut [0; 1]).unwrap(), 0);
let _peer = std::net::TcpStream::connect(local_address(&listener).unwrap()).unwrap();
assert!(accept(&listener).is_ok());
}
}