use super::{SocketWriter, content_length, header_end, receive, socket, timeouts};
use core::{fmt::Write as _, net::SocketAddrV4};
use git_zero::{
Error, Result,
client::Transport,
http::Request,
io::{SliceWriter, Write},
};
use rustix::{fd::AsFd, net};
pub struct HttpTransport<'a> {
pub address: SocketAddrV4,
pub repository_path: &'a str,
pub wire_storage: &'a mut [u8],
}
impl Transport for HttpTransport<'_> {
fn request(&mut self, request: Request<'_>, response: &mut [u8]) -> Result<(u16, usize)> {
let socket = socket(net::AddressFamily::INET)?;
timeouts(&socket)?;
net::connect(&socket, &self.address).map_err(|_| Error::Backend)?;
self.send_request(&socket, request)?;
self.read_response(&socket, response)
}
}
impl HttpTransport<'_> {
fn send_request(&mut self, socket: &impl AsFd, request: Request<'_>) -> Result<()> {
let mut header = SliceWriter::new(self.wire_storage);
write!(
&mut header,
"{} {}/{}",
request.method,
self.repository_path.trim_end_matches('/'),
request.path.trim_start_matches('/')
)
.map_err(|_| Error::BufferTooSmall)?;
if let Some(query) = request.query {
write!(&mut header, "?{query}").map_err(|_| Error::BufferTooSmall)?;
}
write!(
&mut header,
" HTTP/1.0\r\nHost: {}\r\nContent-Length: {}\r\n",
self.address,
request.body.len()
)
.map_err(|_| Error::BufferTooSmall)?;
for (name, value) in [
("Content-Type", request.content_type),
("Git-Protocol", request.git_protocol),
] {
if let Some(value) = value {
if value.bytes().any(|b| matches!(b, 0 | b'\r' | b'\n')) {
return Err(Error::InvalidRequest);
}
write!(&mut header, "{name}: {value}\r\n").map_err(|_| Error::BufferTooSmall)?;
}
}
header.write_all(b"Connection: close\r\n\r\n")?;
let mut output = SocketWriter(socket.as_fd());
output.write_all(header.as_bytes())?;
output.write_all(request.body)?;
Ok(())
}
fn read_response(&mut self, socket: &impl AsFd, response: &mut [u8]) -> Result<(u16, usize)> {
let length = read_to_end(socket, self.wire_storage)?;
let end = header_end(&self.wire_storage[..length]).ok_or(Error::InvalidRequest)?;
let header =
core::str::from_utf8(&self.wire_storage[..end]).map_err(|_| Error::InvalidRequest)?;
let status = header
.lines()
.next()
.and_then(|line| line.split_ascii_whitespace().nth(1))
.ok_or(Error::InvalidRequest)?
.parse()
.map_err(|_| Error::InvalidRequest)?;
let body = &self.wire_storage[end..length];
if content_length(header)?.is_some_and(|expected| expected != body.len()) {
return Err(Error::InvalidRequest);
}
response
.get_mut(..body.len())
.ok_or(Error::BufferTooSmall)?
.copy_from_slice(body);
Ok((status, body.len()))
}
}
fn read_to_end(socket: &impl AsFd, storage: &mut [u8]) -> Result<usize> {
let mut length = 0;
loop {
if length == storage.len() {
let mut extra = [0; 1];
if receive(socket, &mut extra)? != 0 {
return Err(Error::BufferTooSmall);
}
break;
}
let count = receive(socket, &mut storage[length..])?;
if count == 0 {
break;
}
length += count;
}
Ok(length)
}