1use std::collections::VecDeque;
27use std::net::SocketAddr;
28use std::sync::Arc;
29
30use rustls::ClientConfig;
31
32use crate::RxMeta;
33use crate::clock::monotonic_ns;
34use crate::endpoint::sealed::{Deliver, DialIo, Io};
35use crate::endpoint::{ConnId, ConnMut, Handler};
36use crate::error::{Error, Result};
37use crate::net::{Target, TlsDial};
38use crate::stream::TlsStream;
39use crate::tls::MAX_RECORD;
40
41pub const MAX_REQUEST: usize = 1 << 14;
44const MAX_HEADERS: usize = 64;
45pub const MAX_IN_FLIGHT: usize = 1024;
47
48const RESERVED: [&str; 8] = [
50 "content-length",
51 "transfer-encoding",
52 "host",
53 "connection",
54 "te",
55 "upgrade",
56 "trailer",
57 "expect",
58];
59
60pub(crate) const fn is_tchar(b: u8) -> bool {
62 b.is_ascii_alphanumeric()
63 || matches!(
64 b,
65 b'!' | b'#'
66 | b'$'
67 | b'%'
68 | b'&'
69 | b'\''
70 | b'*'
71 | b'+'
72 | b'-'
73 | b'.'
74 | b'^'
75 | b'_'
76 | b'`'
77 | b'|'
78 | b'~'
79 )
80}
81
82fn validate_header(name: &str, value: &str) -> Result<()> {
83 if name.is_empty() || !name.bytes().all(is_tchar) {
84 return Err(Error::Http(format!("invalid header name {name:?}")));
85 }
86 if RESERVED.iter().any(|r| name.eq_ignore_ascii_case(r)) {
87 return Err(Error::Http(format!("header {name} is set by the client")));
88 }
89 if value.bytes().any(|b| b == b'\r' || b == b'\n' || b == 0) {
91 return Err(Error::Http(format!("invalid value for header {name}")));
92 }
93 Ok(())
94}
95
96pub(crate) fn validate_request(method: &str, path: &str, headers: &[(&str, &str)]) -> Result<()> {
97 if method.is_empty() || !method.bytes().all(is_tchar) {
98 return Err(Error::Http(format!("invalid method {method:?}")));
99 }
100 if path.is_empty() || path.bytes().any(|b| b <= b' ' || b == 0x7F) {
101 return Err(Error::Http("invalid request target".into()));
102 }
103 for (k, v) in headers {
104 validate_header(k, v)?;
105 }
106 Ok(())
107}
108
109fn closes(version: Option<u8>, headers: &[httparse::Header<'_>]) -> bool {
112 let token = |t: &[u8]| {
113 headers
114 .iter()
115 .filter(|h| h.name.eq_ignore_ascii_case("connection"))
116 .flat_map(|h| h.value.split(|&b| b == b','))
117 .any(|v| v.trim_ascii().eq_ignore_ascii_case(t))
118 };
119 token(b"close") || (version == Some(0) && !token(b"keep-alive"))
120}
121
122#[derive(Debug)]
125pub struct Response<'a> {
126 pub status: u16,
128 pub headers: &'a [httparse::Header<'a>],
130 pub body: &'a [u8],
132}
133
134impl Response<'_> {
135 #[must_use]
137 pub fn header(&self, name: &str) -> Option<&[u8]> {
138 self.headers
139 .iter()
140 .find(|h| h.name.eq_ignore_ascii_case(name))
141 .map(|h| h.value)
142 }
143}
144
145pub trait HttpHandler: Handler {
147 fn on_response(&mut self, conn: ConnId, request: u64, response: &Response<'_>, meta: &RxMeta);
149}
150
151#[derive(Debug, Clone, Copy, PartialEq, Eq)]
153pub struct HttpOptions {
154 pub request_timeout_ms: u32,
156 pub max_in_flight: usize,
158 pub buf_size: usize,
160}
161
162impl Default for HttpOptions {
163 fn default() -> Self {
164 Self {
165 request_timeout_ms: 5_000,
166 max_in_flight: 1,
167 buf_size: 1 << 20,
168 }
169 }
170}
171
172#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
174pub struct HttpStats {
175 pub requests: u64,
177 pub responses: u64,
179 pub bytes_in: u64,
181 pub records: u64,
183 pub span_copy_bytes: u64,
185 pub compact_bytes: u64,
187 pub chunk_copy_bytes: u64,
189}
190
191struct InFlight {
192 id: u64,
193 deadline_ns: i64,
194 head: bool,
195 end: u64,
198}
199
200pub struct HttpConn {
203 stream: TlsStream,
204 fixed: Vec<u8>,
206 in_flight: VecDeque<InFlight>,
207 next_id: u64,
208 opts: HttpOptions,
209 body: Vec<u8>,
210 need: usize,
212 requests: u64,
213 responses: u64,
214 chunk_copy_bytes: u64,
215 handed: u64,
217}
218
219impl HttpConn {
220 pub fn request(
229 &mut self,
230 method: &str,
231 path: &str,
232 headers: &[(&str, &str)],
233 body: &[u8],
234 ) -> Result<u64> {
235 self.queue(method, path, headers, body, |_| {})
236 }
237
238 fn queue(
241 &mut self,
242 method: &str,
243 path: &str,
244 headers: &[(&str, &str)],
245 body: &[u8],
246 query: impl FnOnce(&mut Vec<u8>),
247 ) -> Result<u64> {
248 if self.in_flight.len() >= self.opts.max_in_flight {
249 return Err(Error::Busy("max_in_flight requests outstanding"));
250 }
251 validate_request(method, path, headers)?;
252 let with_length = !body.is_empty() || matches!(method, "POST" | "PUT" | "PATCH");
253 let fixed = &self.fixed;
254 self.stream.seal_build(|out| {
255 let start = out.len();
256 out.extend_from_slice(method.as_bytes());
257 out.push(b' ');
258 out.extend_from_slice(path.as_bytes());
259 query(out);
260 out.extend_from_slice(b" HTTP/1.1\r\n");
261 out.extend_from_slice(fixed);
262 for (k, v) in headers {
263 out.extend_from_slice(k.as_bytes());
264 out.extend_from_slice(b": ");
265 out.extend_from_slice(v.as_bytes());
266 out.extend_from_slice(b"\r\n");
267 }
268 if with_length {
269 out.extend_from_slice(b"Content-Length: ");
270 push_decimal(out, body.len());
271 out.extend_from_slice(b"\r\n");
272 }
273 out.extend_from_slice(b"\r\n");
274 out.extend_from_slice(body);
275 if out.len() - start > MAX_REQUEST {
276 return Err(Error::Http("request larger than one TLS record".into()));
277 }
278 Ok(())
279 })?;
280 let id = self.next_id;
281 self.next_id += 1;
282 let queued = self.stream.tx_queue().len() as u64;
283 self.in_flight.push_back(InFlight {
284 id,
285 deadline_ns: monotonic_ns() + i64::from(self.opts.request_timeout_ms) * 1_000_000,
286 head: method == "HEAD",
287 end: self.handed + queued,
288 });
289 self.requests += 1;
290 Ok(id)
291 }
292
293 pub fn signed_request(
302 &mut self,
303 method: &str,
304 path: &str,
305 params: &str,
306 headers: &[(&str, &str)],
307 signer: &crate::auth::Signer,
308 ) -> Result<u64> {
309 let now_ms = u64::try_from(crate::clock::realtime_ns() / 1_000_000).unwrap_or(0);
310 self.signed_request_at(method, path, params, headers, signer, now_ms)
311 }
312
313 pub fn signed_request_at(
318 &mut self,
319 method: &str,
320 path: &str,
321 params: &str,
322 headers: &[(&str, &str)],
323 signer: &crate::auth::Signer,
324 timestamp_ms: u64,
325 ) -> Result<u64> {
326 crate::auth::validate_params(params)?;
327 if path.contains('?') {
328 return Err(Error::Http("put query parameters in `params`".into()));
329 }
330 self.queue(method, path, headers, &[], |out| {
331 out.push(b'?');
332 crate::auth::append_signed_params(out, params, timestamp_ms, signer);
333 })
334 }
335
336 #[must_use]
338 pub fn in_flight(&self) -> usize {
339 self.in_flight.len()
340 }
341
342 #[must_use]
344 pub const fn stats(&self) -> HttpStats {
345 let s = self.stream.stats();
346 HttpStats {
347 requests: self.requests,
348 responses: self.responses,
349 bytes_in: s.bytes_in,
350 records: s.records,
351 span_copy_bytes: s.span_copy_bytes,
352 compact_bytes: s.compact_bytes,
353 chunk_copy_bytes: self.chunk_copy_bytes,
354 }
355 }
356
357 #[must_use]
360 pub fn raw_fd(&self) -> std::os::fd::RawFd {
361 self.stream.fd()
362 }
363
364 #[must_use]
368 pub fn route(&self) -> &crate::route::RouteCheck {
369 self.stream.route()
370 }
371
372 #[inline]
373 fn deliver<H: HttpHandler>(
374 &mut self,
375 id: ConnId,
376 n: usize,
377 meta: &RxMeta,
378 handler: &mut H,
379 ) -> Result<usize> {
380 self.stream.received(n);
381 let mut delivered = 0;
382 if n == 0 {
383 delivered += self.parse(id, meta, handler)?;
384 }
385 while self.stream.open_next()? {
386 delivered += self.parse(id, meta, handler)?;
387 }
388 self.stream.compact();
389 Ok(delivered)
390 }
391
392 fn parse<H: HttpHandler>(
394 &mut self,
395 id: ConnId,
396 meta: &RxMeta,
397 handler: &mut H,
398 ) -> Result<usize> {
399 let mut delivered = 0;
400 loop {
401 let window = self.stream.window();
402 if window.is_empty() || window.len() < self.need {
403 return Ok(delivered);
404 }
405 self.need = 0;
406 let Some(req) = self.in_flight.front() else {
407 return Err(Error::Http("response without a request".into()));
408 };
409 let mut hdrs = [httparse::EMPTY_HEADER; MAX_HEADERS];
410 let mut resp = httparse::Response::new(&mut hdrs);
411 let hl = match resp.parse(window).map_err(|e| Error::Http(e.to_string()))? {
412 httparse::Status::Partial => return Ok(delivered),
413 httparse::Status::Complete(n) => n,
414 };
415 let status = resp.code.unwrap_or(0);
416 if (100..200).contains(&status) {
417 self.stream.consume(hl);
419 continue;
420 }
421 let framing = framing(resp.headers, status, req.head)?;
422 let close = closes(resp.version, resp.headers);
423 let limit = self.opts.buf_size - 2 * MAX_RECORD;
424 let (body, total) = match framing {
425 Framing::Length(len) => {
426 let total = hl
427 .checked_add(len)
428 .filter(|&t| t <= limit)
429 .ok_or_else(|| Error::Http("response larger than the buffer".into()))?;
430 if window.len() < total {
431 self.need = total;
433 return Ok(delivered);
434 }
435 (&window[hl..total], total)
436 }
437 Framing::Chunked => {
438 let Some(end) = dechunk(&window[hl..], &mut self.body, limit)? else {
439 if window.len() > limit {
440 return Err(Error::Http("response larger than the buffer".into()));
441 }
442 return Ok(delivered);
443 };
444 self.chunk_copy_bytes += self.body.len() as u64;
445 (self.body.as_slice(), hl + end)
446 }
447 };
448 let request = req.id;
449 handler.on_response(
450 id,
451 request,
452 &Response {
453 status,
454 headers: resp.headers,
455 body,
456 },
457 meta,
458 );
459 self.in_flight.pop_front();
460 self.stream.consume(total);
461 self.responses += 1;
462 delivered += 1;
463 if close {
464 return Err(Error::Closed);
465 }
466 }
467 }
468}
469
470impl ConnMut<'_, HttpConn> {
472 pub fn request(
477 &mut self,
478 method: &str,
479 path: &str,
480 headers: &[(&str, &str)],
481 body: &[u8],
482 ) -> Result<u64> {
483 self.inner().request(method, path, headers, body)
484 }
485
486 pub fn signed_request(
491 &mut self,
492 method: &str,
493 path: &str,
494 params: &str,
495 headers: &[(&str, &str)],
496 signer: &crate::auth::Signer,
497 ) -> Result<u64> {
498 self.inner()
499 .signed_request(method, path, params, headers, signer)
500 }
501
502 pub fn signed_request_at(
507 &mut self,
508 method: &str,
509 path: &str,
510 params: &str,
511 headers: &[(&str, &str)],
512 signer: &crate::auth::Signer,
513 timestamp_ms: u64,
514 ) -> Result<u64> {
515 self.inner()
516 .signed_request_at(method, path, params, headers, signer, timestamp_ms)
517 }
518}
519
520enum Framing {
521 Length(usize),
522 Chunked,
523}
524
525fn framing(headers: &[httparse::Header<'_>], status: u16, head: bool) -> Result<Framing> {
526 if head || status == 204 || status == 304 {
527 return Ok(Framing::Length(0));
528 }
529 let mut length = None;
530 for h in headers {
531 if h.name.eq_ignore_ascii_case("transfer-encoding") {
532 if h.value.eq_ignore_ascii_case(b"chunked") {
533 return Ok(Framing::Chunked);
534 }
535 return Err(Error::Http("unsupported transfer encoding".into()));
536 }
537 if h.name.eq_ignore_ascii_case("content-length") {
538 let v = std::str::from_utf8(h.value)
539 .ok()
540 .and_then(|s| s.trim().parse::<usize>().ok())
541 .ok_or_else(|| Error::Http("bad Content-Length".into()))?;
542 if length.is_some_and(|l| l != v) {
543 return Err(Error::Http("conflicting Content-Length".into()));
544 }
545 length = Some(v);
546 }
547 }
548 length
549 .map(Framing::Length)
550 .ok_or_else(|| Error::Http("response without Content-Length".into()))
551}
552
553fn dechunk(data: &[u8], out: &mut Vec<u8>, limit: usize) -> Result<Option<usize>> {
556 out.clear();
557 let mut pos = 0;
558 loop {
559 let (hdr, size) = match httparse::parse_chunk_size(&data[pos..])
560 .map_err(|_| Error::Http("bad chunk size".into()))?
561 {
562 httparse::Status::Partial => return Ok(None),
563 httparse::Status::Complete(v) => v,
564 };
565 let size = usize::try_from(size)
566 .ok()
567 .filter(|&s| s <= limit)
568 .ok_or_else(|| Error::Http("chunk too large".into()))?;
569 pos += hdr;
570 if size == 0 {
571 let rest = &data[pos..];
573 let Some(end) = rest.windows(2).position(|w| w == b"\r\n") else {
574 return Ok(None);
575 };
576 if end == 0 {
577 return Ok(Some(pos + 2));
578 }
579 let Some(blank) = rest.windows(4).position(|w| w == b"\r\n\r\n") else {
581 return Ok(None);
582 };
583 return Ok(Some(pos + blank + 4));
584 }
585 let end = pos
586 .checked_add(size)
587 .and_then(|e| e.checked_add(2))
588 .ok_or_else(|| Error::Http("chunk too large".into()))?;
589 if data.len() < end {
590 return Ok(None);
591 }
592 out.extend_from_slice(&data[pos..end - 2]);
593 if &data[end - 2..end] != b"\r\n" {
594 return Err(Error::Http("chunk without CRLF".into()));
595 }
596 pos = end;
597 }
598}
599
600fn push_decimal(out: &mut Vec<u8>, mut v: usize) {
601 let mut digits = [0u8; 20];
602 let mut i = digits.len();
603 loop {
604 i -= 1;
605 #[allow(clippy::cast_possible_truncation)]
606 {
607 digits[i] = b'0' + (v % 10) as u8;
608 }
609 v /= 10;
610 if v == 0 {
611 break;
612 }
613 }
614 out.extend_from_slice(&digits[i..]);
615}
616
617pub struct HttpDial {
620 target: Target,
621 opts: HttpOptions,
622 tls: TlsDial,
623}
624
625impl HttpDial {
626 pub fn new(target: Target, cfg: Arc<ClientConfig>, opts: HttpOptions) -> Result<Self> {
631 if opts.buf_size < 8 * MAX_RECORD {
632 return Err(Error::Config(
633 "receive buffer must hold at least 8 records".into(),
634 ));
635 }
636 if opts.max_in_flight == 0 {
637 return Err(Error::Config("max_in_flight must be at least 1".into()));
638 }
639 if opts.max_in_flight > MAX_IN_FLIGHT {
640 return Err(Error::Config(format!(
641 "max_in_flight above {MAX_IN_FLIGHT}"
642 )));
643 }
644 for (k, v) in &target.headers {
645 validate_header(k, v)?;
646 }
647 let sni = target.host.clone();
648 Ok(Self {
649 target,
650 opts,
651 tls: TlsDial::new(cfg, sni)?,
652 })
653 }
654
655 fn fixed_headers(&self) -> Vec<u8> {
656 let mut f = Vec::new();
657 f.extend_from_slice(b"Host: ");
658 f.extend_from_slice(self.target.host_header().as_bytes());
659 f.extend_from_slice(b"\r\n");
660 for (k, v) in &self.target.headers {
661 f.extend_from_slice(k.as_bytes());
662 f.extend_from_slice(b": ");
663 f.extend_from_slice(v.as_bytes());
664 f.extend_from_slice(b"\r\n");
665 }
666 f
667 }
668}
669
670impl DialIo for HttpDial {
671 type Conn = HttpConn;
672
673 fn host(&self) -> &str {
674 &self.target.host
675 }
676
677 fn port(&self) -> u16 {
678 self.target.port
679 }
680
681 fn addr(&self) -> Option<SocketAddr> {
682 self.target.addr
683 }
684
685 fn start(&mut self, addr: SocketAddr) -> Result<()> {
686 self.tls.start(addr)
687 }
688
689 fn step(&mut self) -> Result<Option<HttpConn>> {
690 let Some((sock, tls, rest)) = self.tls.step()? else {
691 return Ok(None);
692 };
693 let mut buf = vec![1u8; self.opts.buf_size].into_boxed_slice();
696 if rest.len() > buf.len() {
697 return Err(Error::Record("handshake leftover larger than the buffer"));
698 }
699 buf[..rest.len()].copy_from_slice(&rest);
700 let mut stream = TlsStream::new(sock, buf, 0, rest.len(), tls, Vec::with_capacity(1 << 16));
701 if let Some(check) = self.tls.take_route() {
702 stream.set_route(crate::endpoint::sealed::RouteSlot::Ready(check));
703 }
704 Ok(Some(HttpConn {
705 stream,
706 fixed: self.fixed_headers(),
707 in_flight: VecDeque::with_capacity(self.opts.max_in_flight),
708 next_id: 0,
709 opts: self.opts,
710 body: Vec::with_capacity(1 << 16),
711 need: 0,
712 requests: 0,
713 responses: 0,
714 chunk_copy_bytes: 0,
715 handed: 0,
716 }))
717 }
718
719 fn gate_route(&mut self) {
720 self.tls.gate_route();
721 }
722
723 fn awaiting_route(&self) -> Option<std::os::fd::RawFd> {
724 self.tls.awaiting_route()
725 }
726
727 fn route_checked(&mut self, check: crate::route::RouteCheck) {
728 self.tls.route_checked(check);
729 }
730}
731
732impl HttpDial {
733 pub fn connect_blocking(mut self) -> Result<HttpConn> {
740 let addr = match self.target.addr {
741 Some(a) => a,
742 None => std::net::ToSocketAddrs::to_socket_addrs(&(
743 self.target.host.as_str(),
744 self.target.port,
745 ))?
746 .find(SocketAddr::is_ipv4)
747 .ok_or_else(|| Error::Dns(format!("{}: no IPv4 address", self.target.host)))?,
748 };
749 self.start(addr)?;
750 let start = std::time::Instant::now();
751 loop {
752 if let Some(c) = self.step()? {
753 return Ok(c);
754 }
755 if start.elapsed() > std::time::Duration::from_secs(30) {
756 return Err(Error::Timeout("http connect"));
757 }
758 std::thread::yield_now();
759 }
760 }
761}
762
763impl Io for HttpConn {
764 const DEADLINES: bool = true;
765
766 #[inline]
767 fn fd(&self) -> std::os::fd::RawFd {
768 self.stream.fd()
769 }
770 #[inline]
771 fn rx_space(&mut self) -> &mut [u8] {
772 self.stream.rx_space()
773 }
774 #[inline]
775 fn tx_queue(&mut self) -> &mut Vec<u8> {
776 self.stream.tx_queue()
777 }
778 fn has_buffered_input(&self) -> bool {
779 self.stream.has_buffered_input()
780 }
781 fn set_route(&mut self, route: crate::endpoint::sealed::RouteSlot) {
782 self.stream.set_route(route);
783 }
784 fn route_state(&self) -> &crate::endpoint::sealed::RouteSlot {
785 self.stream.route_state()
786 }
787 fn expire(&mut self, now_ns: i64) -> Result<()> {
788 match self.in_flight.front() {
789 Some(r) if r.deadline_ns <= now_ns => Err(Error::Timeout("http request")),
790 _ => Ok(()),
791 }
792 }
793 #[inline]
794 fn on_handed(&mut self, n: usize) {
795 self.handed += n as u64;
796 }
797 #[cold]
800 fn close_reason(&mut self, error: Error) -> Error {
801 if self.in_flight.is_empty() {
802 return error;
803 }
804 let first_unsent = self
805 .in_flight
806 .iter()
807 .find(|r| r.end > self.handed)
808 .map(|r| r.id);
809 Error::Unanswered(Box::new(crate::error::Unanswered {
810 cause: error,
811 first_unsent,
812 }))
813 }
814}
815
816impl<H: HttpHandler> Deliver<H> for HttpConn {
817 #[inline]
818 fn on_received(
819 &mut self,
820 id: ConnId,
821 n: usize,
822 meta: &RxMeta,
823 handler: &mut H,
824 ) -> Result<usize> {
825 self.deliver(id, n, meta, handler)
826 }
827}