diff --git a/Cargo.toml b/Cargo.toml index 3ff1b04..e22a239 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -26,6 +26,9 @@ default = ["runtime-smol"] vendored = ["native-tls/vendored"] +# Expose ALPN configuration on server-side `TlsAcceptor` (no-op on Apple platforms). +alpn-accept = ["native-tls/alpn-accept"] + # Runtime runtime-smol = ["futures-util"] runtime-tokio = ["tokio"] diff --git a/README.md b/README.md index 12ce36e..e7dc32d 100644 --- a/README.md +++ b/README.md @@ -57,6 +57,14 @@ $ cargo add async-native-tls * `runtime-tokio`: Use the `tokio` runtime. This is mutually exclusive with `runtime-smol`. + * `alpn-accept`: Enable server-side ALPN configuration via + `TlsAcceptor::new_with_alpn`. Note that the underlying `native-tls` crate + does not implement server-side ALPN on Apple platforms (Secure Transport + exposes no public API for it), so the configured protocols are silently + ignored there. Client-side ALPN (`TlsConnector::request_alpns` and + `TlsStream::negotiated_alpn`) is always available and not gated by this + feature. + ## Example #### smol diff --git a/src/acceptor.rs b/src/acceptor.rs index 7c99cd9..1317497 100644 --- a/src/acceptor.rs +++ b/src/acceptor.rs @@ -65,6 +65,41 @@ impl TlsAcceptor { Ok(TlsAcceptor(native_tls::TlsAcceptor::new(identity)?)) } + /// Create a new TlsAcceptor that advertises the given ALPN protocols to clients. + /// + /// `protocols` is a server-preference-ordered list of ALPN identifiers (for + /// example `&["h2", "http/1.1"]`). + /// + /// # Platform support + /// + /// On Apple platforms, the underlying `native-tls` crate is built on + /// Secure Transport, which does not expose a server-side ALPN selection + /// API. The supplied protocols are silently ignored there — the handshake + /// proceeds without advertising an ALPN list and + /// [`TlsStream::negotiated_alpn`] will return `Ok(None)`. See + /// . + #[cfg(feature = "alpn-accept")] + #[cfg_attr(docsrs, doc(cfg(feature = "alpn-accept")))] + pub async fn new_with_alpn( + mut file: R, + password: S, + protocols: &[P], + ) -> Result + where + R: AsyncRead + Unpin, + S: AsRef, + P: AsRef, + { + let mut identity = vec![]; + file.read_to_end(&mut identity).await?; + + let identity = native_tls::Identity::from_pkcs12(&identity, password.as_ref())?; + let acceptor = native_tls::TlsAcceptor::builder(identity) + .accept_alpn(protocols) + .build()?; + Ok(TlsAcceptor(acceptor)) + } + /// Accepts a new client connection with the provided stream. /// /// This function will internally call `TlsAcceptor::accept` to connect @@ -135,4 +170,43 @@ mod tests { assert_eq!(res, b"hello"); }) } + + #[cfg(all(feature = "alpn-accept", not(target_vendor = "apple")))] + #[test] + fn test_acceptor_alpn() { + smol::block_on(async { + let key = File::open("tests/identity.pfx").await.unwrap(); + let acceptor = TlsAcceptor::new_with_alpn(key, "hello", &["h2", "http/1.1"]) + .await + .unwrap(); + let listener = TcpListener::bind("127.0.0.1:8443").await.unwrap(); + smol::spawn(async move { + let mut incoming = listener.incoming(); + while let Some(stream) = incoming.next().await { + let acceptor = acceptor.clone(); + let stream = stream.unwrap(); + smol::spawn(async move { + let mut stream = acceptor.accept(stream).await.unwrap(); + stream.write_all(b"hello").await.unwrap(); + }) + .detach(); + } + }) + .detach(); + + let stream = TcpStream::connect("127.0.0.1:8443").await.unwrap(); + let connector = TlsConnector::new() + .danger_accept_invalid_certs(true) + .request_alpns(&["h2"]); + + let mut stream = connector.connect("127.0.0.1", stream).await.unwrap(); + assert_eq!( + stream.negotiated_alpn().unwrap().as_deref(), + Some(&b"h2"[..]) + ); + let mut res = Vec::new(); + stream.read_to_end(&mut res).await.unwrap(); + assert_eq!(res, b"hello"); + }) + } } diff --git a/src/tls_stream.rs b/src/tls_stream.rs index 03e59a9..d01d6d7 100644 --- a/src/tls_stream.rs +++ b/src/tls_stream.rs @@ -72,6 +72,20 @@ impl TlsStream { { self.0.tls_server_end_point() } + + /// Returns the protocol negotiated via ALPN, if any. + /// + /// Returns `Ok(None)` if no ALPN protocol was negotiated, including when + /// neither peer advertised a list. On Apple platforms this also returns + /// `Ok(None)` for server-side acceptors configured via + /// [`TlsAcceptor::new_with_alpn`](crate::TlsAcceptor::new_with_alpn), + /// since Secure Transport does not implement server-side ALPN. + pub fn negotiated_alpn(&self) -> crate::Result>> + where + S: AsyncRead + AsyncWrite + Unpin, + { + self.0.negotiated_alpn() + } } #[cfg(feature = "runtime-smol")]