use std::io::{Error as IOError, Read, Write}; use std::net::TcpStream; use std::str::FromStr; use httplz::NeedsMoreData; use httplzx::ToEvents; pub struct Body { buf: httplz::Buf, u8>, chunk_end: usize, read_amt: usize, stream: TcpStream, conn: httplz::Connection, } impl Body { fn find_next_chunk_sz(&mut self) -> Result<(), IOError> { self.buf.pop_front(self.chunk_end); self.chunk_end = 0; self.read_amt = 0; while self.conn.is_recving() { dbg!(self.conn, String::from_utf8_lossy(self.buf.filled())); let res = self.conn.poll_recv(self.buf.filled()); if res.needs_more_data() { self.buf.read_from(&mut self.stream)?; continue; } let (amt, ev) = res.map_err(|e| IOError::other(format!("{e:?}")))?; match ev { httplz::Event::BodyChunk(b) => { self.chunk_end = b.len(); return Ok(()); }, _ => self.buf.pop_front(amt), }; } Ok(()) } } impl Read for Body { fn read(&mut self, mut buf: &mut [u8]) -> std::io::Result { if self.chunk_end == self.read_amt { self.find_next_chunk_sz()?; } let chunk = &self.buf.filled()[self.read_amt..self.chunk_end]; let amt = buf.write(chunk)?; self.read_amt += amt; Ok(amt) } } impl Body { pub fn collect_bytes(self) -> Result, IOError> { self.bytes().collect() } } pub type Response = http::Response; #[derive(Debug)] pub enum Error { IOError(IOError), HttplzError(httplz::Error), InvalidUri, InvalidStatusCode, InvalidHeaderValue, } impl From for Error { fn from(value: IOError) -> Self { Self::IOError(value) } } impl From for Error { fn from(value: httplz::Error) -> Self { Self::HttplzError(value) } } pub fn fetch(path: &str) -> Result { let uri = http::Uri::from_str(path).map_err(|_| Error::InvalidUri)?; let scheme = uri.scheme().cloned().unwrap_or(http::uri::Scheme::HTTPS); let host = uri.host().ok_or(Error::InvalidUri)?; let mut stream = TcpStream::connect(format!( "{}:{}", host, uri.port_u16().unwrap_or_else(|| match scheme.as_str() { "http" => 80, _ => 443, }) ))?; let mut conn = httplz::Connection::new(httplz::Role::Client); let (mut parts, _) = http::Request::new(()).into_parts(); parts .headers .insert("Host", http::HeaderValue::from_str(host).unwrap()); parts.uri = uri; let mut events = parts.to_events(); events.extend_from_slice(&[httplz::Event::HeadersDone, httplz::Event::SendDone]); dbg!("a"); let mut buf = httplz::Buf::new(vec![0; 4096].into_boxed_slice()); for event in events { conn.handle_send(&event, &mut buf)?; } stream.write_all(buf.filled())?; assert!(conn.is_recving()); buf.clear(); let (mut parts, _) = http::Response::new(()).into_parts(); while conn.is_recving() { let data = buf.filled(); let res = conn.poll_recv(data); if res.needs_more_data() { buf.read_from(&mut stream)?; continue; } let (amt, event) = res?; let mut brk = false; match event { httplz::Event::StatusLine(sl) => { parts.status = http::StatusCode::from_u16(sl.status_code) .map_err(|_| Error::InvalidStatusCode)?; parts.version = http::Version::HTTP_11; }, httplz::Event::Header(h) => { parts.headers.insert( http::HeaderName::from_str(h.name).unwrap(), http::HeaderValue::from_bytes(h.value) .map_err(|_| Error::InvalidHeaderValue)?, ); }, httplz::Event::HeadersDone => brk = true, _ => (), }; dbg!(amt, brk); buf.pop_front(amt); if brk { break; } } Ok(http::Response::from_parts( parts, Body { buf, stream, conn, chunk_end: 0, read_amt: 0, }, )) } #[cfg(test)] mod test_fetch { use crate::fetch; #[test] fn test_fetch_1() { let resp = fetch("http://httpbin.org/get") .unwrap() .map(|b| b.collect_bytes().unwrap()); let body = resp.into_body(); let body = String::from_utf8_lossy(&body); assert_eq!( body, include_str!("../testing/snapshots/httpbin_get_empty.txt") ) } }