Skip to main content

base_http_protocol/
lib.rs

1// Released under the MIT License.
2// Copyright, 2026, by Samuel Williams.
3
4#![doc = include_str!("../readme.md")]
5#![forbid(unsafe_code)]
6
7use base_endpoint::stream::{ReadableStream, WritableStream};
8use base_http1::{RequestReader, RequestWriter, ResponseReader, ResponseWriter};
9use std::fmt;
10
11/// Enabled HTTP implementations and their establishment policy.
12#[derive(Clone, Copy, Debug, Default)]
13pub struct HttpProtocols {
14    /// Reject an encrypted peer that did not negotiate an application protocol.
15    /// Unknown negotiated protocols are rejected regardless of this setting.
16    pub require_alpn: bool,
17}
18impl HttpProtocols {
19    /// Opaque application-protocol names supplied to the security provider.
20    pub fn alpn_protocols(&self) -> Vec<Vec<u8>> {
21        vec![b"http/1.1".to_vec()]
22    }
23    /// The initial explicit policy for a plain TCP stream.
24    pub fn plaintext(&self) -> HttpProtocol {
25        HttpProtocol::Http1
26    }
27    /// Select the engine after an authenticated transport reports negotiation.
28    pub fn negotiated(&self, selected: Option<&[u8]>) -> Result<HttpProtocol, NegotiationError> {
29        match selected {
30            Some(b"http/1.1") => Ok(HttpProtocol::Http1),
31            None if !self.require_alpn => Ok(HttpProtocol::Http1),
32            None => Err(NegotiationError::Missing),
33            Some(name) => Err(NegotiationError::Unsupported(name.to_vec())),
34        }
35    }
36}
37
38/// An enabled wire engine selected before exchange admission.
39#[derive(Clone, Copy, Debug, PartialEq, Eq)]
40pub enum HttpProtocol {
41    /// HTTP/1 framing over a reliable plaintext byte stream.
42    Http1,
43}
44impl HttpProtocol {
45    /// Construct matching client codec directions over an established stream.
46    pub fn client<R: ReadableStream, W: WritableStream>(
47        self,
48        reader: R,
49        writer: W,
50        options: base_http1::Options,
51    ) -> Result<(ResponseReader<R>, RequestWriter<W>), base_http1::Error> {
52        match self {
53            Self::Http1 => Ok((
54                ResponseReader::new(reader, options)?,
55                RequestWriter::new(writer),
56            )),
57        }
58    }
59    /// Construct matching server codec directions over an established stream.
60    pub fn server<R: ReadableStream, W: WritableStream>(
61        self,
62        reader: R,
63        writer: W,
64        options: base_http1::Options,
65    ) -> Result<(RequestReader<R>, ResponseWriter<W>), base_http1::Error> {
66        match self {
67            Self::Http1 => Ok((
68                RequestReader::new(reader, options)?,
69                ResponseWriter::new(writer),
70            )),
71        }
72    }
73}
74
75/// Negotiated evidence did not select an enabled HTTP engine.
76#[derive(Clone, Debug, PartialEq, Eq)]
77pub enum NegotiationError {
78    /// The caller required a negotiated identifier but none was supplied.
79    Missing,
80    /// The peer selected an unsupported identifier. Retain its opaque bytes.
81    Unsupported(Vec<u8>),
82}
83impl fmt::Display for NegotiationError {
84    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
85        match self {
86            Self::Missing => formatter.write_str("HTTP application protocol was not negotiated"),
87            Self::Unsupported(name) => {
88                let name = name.escape_ascii();
89                write!(
90                    formatter,
91                    "unsupported HTTP application protocol: \"{name}\""
92                )
93            }
94        }
95    }
96}
97impl std::error::Error for NegotiationError {}
98
99#[cfg(test)]
100mod tests {
101    use super::*;
102
103    #[test]
104    fn unsupported_identifiers_never_fall_back_to_http1() {
105        for required in [false, true] {
106            let protocols = HttpProtocols {
107                require_alpn: required,
108            };
109            for name in [b"h2".as_slice(), b"h3", b"http/1.1\0", b"\xff"] {
110                assert_eq!(
111                    protocols.negotiated(Some(name)),
112                    Err(NegotiationError::Unsupported(name.to_vec()))
113                );
114            }
115            assert_eq!(
116                protocols.negotiated(Some(b"http/1.1")),
117                Ok(HttpProtocol::Http1)
118            );
119            assert_eq!(protocols.plaintext(), HttpProtocol::Http1);
120        }
121    }
122
123    #[test]
124    fn http1_is_advertised_and_absent_negotiation_follows_policy() {
125        let optional = HttpProtocols::default();
126        assert_eq!(optional.alpn_protocols(), vec![b"http/1.1".to_vec()]);
127        assert_eq!(optional.negotiated(None), Ok(HttpProtocol::Http1));
128        let required = HttpProtocols { require_alpn: true };
129        assert_eq!(required.negotiated(None), Err(NegotiationError::Missing));
130    }
131
132    #[test]
133    fn negotiation_errors_describe_the_selection() {
134        assert_eq!(
135            NegotiationError::Missing.to_string(),
136            "HTTP application protocol was not negotiated"
137        );
138        assert_eq!(
139            NegotiationError::Unsupported(b"h2".to_vec()).to_string(),
140            "unsupported HTTP application protocol: \"h2\""
141        );
142        assert_eq!(
143            NegotiationError::Unsupported(b"\xff\"".to_vec()).to_string(),
144            r#"unsupported HTTP application protocol: "\xff\"""#
145        );
146    }
147}