From 6bd7dd0c2ba24f77bef03c4dd9b65c2107a56a58 Mon Sep 17 00:00:00 2001 From: Steven Fackler Date: Thu, 1 Nov 2018 17:42:34 -0700 Subject: [PATCH] Handle ALPN The `new` constructor registers support for h2 and http/1.1, and consumers of `with_connector` need to set up supported protocols themselves. --- Cargo.toml | 2 +- build.rs | 3 ++ src/lib.rs | 24 +++++++++-- src/test.rs | 121 +++++++++++++++++++++++++++++++--------------------- 4 files changed, 97 insertions(+), 53 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 126d1114..765c8b24 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,7 +16,7 @@ runtime = ["hyper/runtime"] [dependencies] antidote = "1.0.0" bytes = "0.4" -hyper = { version = "0.12.1", default-features = false } +hyper = { version = "0.12.14", default-features = false } lazy_static = "1.0" linked_hash_set = "0.1" openssl = "0.10.7" diff --git a/build.rs b/build.rs index d3def6db..3c94a721 100644 --- a/build.rs +++ b/build.rs @@ -4,6 +4,9 @@ fn main() { if let Ok(version) = env::var("DEP_OPENSSL_VERSION_NUMBER") { let version = u64::from_str_radix(&version, 16).unwrap(); + if version >= 0x1_00_02_00_0 { + println!("cargo:rustc-cfg=ossl102"); + } if version >= 0x1_01_01_00_0 { println!("cargo:rustc-cfg=ossl111"); } diff --git a/src/lib.rs b/src/lib.rs index 8cb1e81f..d9a28798 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -91,13 +91,20 @@ pub struct HttpsConnector { impl HttpsConnector { /// Creates a a new `HttpsConnector` using default settings. /// - /// The Hyper `HttpConnector` is used to perform the TCP socket connection. + /// The Hyper `HttpConnector` is used to perform the TCP socket connection. ALPN is configured to support both + /// HTTP/2 and HTTP/1.1. /// /// Requires the `runtime` Cargo feature. pub fn new(threads: usize) -> Result, ErrorStack> { let mut http = HttpConnector::new(threads); http.enforce_http(false); - let ssl = SslConnector::builder(SslMethod::tls())?; + let mut ssl = SslConnector::builder(SslMethod::tls())?; + // avoid unused_mut warnings when building against OpenSSL 1.0.1 + ssl = ssl; + + #[cfg(ossl102)] + ssl.set_alpn_protos(b"\x02h2\x08http/1.1")?; + HttpsConnector::with_connector(http, ssl) } } @@ -227,10 +234,19 @@ where }, ConnectState::Handshake { mut handshake, - connected, + mut connected, } => match handshake.poll() { Ok(Async::Ready(stream)) => { - return Ok(Async::Ready((MaybeHttpsStream::Https(stream), connected))) + // avoid unused_mut warnings on OpenSSL 1.0.1 + connected = connected; + + #[cfg(ossl102)] + { + if let Some(b"h2") = stream.get_ref().ssl().selected_alpn_protocol() { + connected = connected.negotiated_h2(); + } + } + return Ok(Async::Ready((MaybeHttpsStream::Https(stream), connected))); } Ok(Async::NotReady) => { self.0 = ConnectState::Handshake { diff --git a/src/test.rs b/src/test.rs index 7ff4c3af..b5bf3c37 100644 --- a/src/test.rs +++ b/src/test.rs @@ -1,8 +1,7 @@ use futures::stream::Stream; use hyper::client::HttpConnector; use hyper::{Body, Client}; -use openssl::ssl::{SslContext, SslFiletype, SslMethod}; -use std::net::TcpListener; +use openssl::ssl::{SslAcceptor, SslFiletype, SslMethod}; use std::thread; use tokio::runtime::current_thread::Runtime; @@ -43,10 +42,10 @@ fn google() { #[test] fn localhost() { - let listener = TcpListener::bind("127.0.0.1:15410").unwrap(); + let listener = ::std::net::TcpListener::bind("127.0.0.1:0").unwrap(); let port = listener.local_addr().unwrap().port(); - let mut ctx = SslContext::builder(SslMethod::tls()).unwrap(); + let mut ctx = SslAcceptor::mozilla_modern(SslMethod::tls()).unwrap(); ctx.set_session_id_context(b"test").unwrap(); ctx.set_certificate_chain_file("test/cert.pem").unwrap(); ctx.set_private_key_file("test/key.pem", SslFiletype::PEM) @@ -54,35 +53,15 @@ fn localhost() { let ctx = ctx.build(); let thread = thread::spawn(move || { - let stream = listener.accept().unwrap().0; - let ssl = Ssl::new(&ctx).unwrap(); - let mut stream = ssl.accept(stream).unwrap(); - stream.read_exact(&mut [0]).unwrap(); - stream - .write_all(b"HTTP/1.1 204 No Content\r\nConnection: close\r\n\r\n") - .unwrap(); - stream.shutdown().unwrap(); - drop(stream); - - let stream = listener.accept().unwrap().0; - let ssl = Ssl::new(&ctx).unwrap(); - let mut stream = ssl.accept(stream).unwrap(); - stream.read_exact(&mut [0]).unwrap(); - stream - .write_all(b"HTTP/1.1 204 No Content\r\nConnection: close\r\n\r\n") - .unwrap(); - stream.shutdown().unwrap(); - drop(stream); - - let stream = listener.accept().unwrap().0; - let ssl = Ssl::new(&ctx).unwrap(); - let mut stream = ssl.accept(stream).unwrap(); - stream.read_exact(&mut [0]).unwrap(); - stream - .write_all(b"HTTP/1.1 204 No Content\r\nConnection: close\r\n\r\n") - .unwrap(); - stream.shutdown().unwrap(); - drop(stream); + for _ in 0..3 { + let stream = listener.accept().unwrap().0; + let mut stream = ctx.accept(stream).unwrap(); + stream.read_exact(&mut [0]).unwrap(); + stream + .write_all(b"HTTP/1.1 204 No Content\r\nConnection: close\r\n\r\n") + .unwrap(); + stream.shutdown().unwrap(); + } }); let mut connector = HttpConnector::new(1); @@ -106,21 +85,66 @@ fn localhost() { let mut runtime = Runtime::new().unwrap(); - let f = client - .get(format!("https://localhost:{}", port).parse().unwrap()) - .and_then(|resp| { - assert!(resp.status().is_success(), "{}", resp.status()); - resp.into_body().for_each(|_| Ok(())) - }); - runtime.block_on(f).unwrap(); + for _ in 0..3 { + let f = client + .get(format!("https://localhost:{}", port).parse().unwrap()) + .and_then(|resp| { + assert!(resp.status().is_success(), "{}", resp.status()); + resp.into_body().for_each(|_| Ok(())) + }); + runtime.block_on(f).unwrap(); + } - let f = client - .get(format!("https://localhost:{}", port).parse().unwrap()) - .and_then(|resp| { - assert!(resp.status().is_success(), "{}", resp.status()); - resp.into_body().for_each(|_| Ok(())) - }); - runtime.block_on(f).unwrap(); + thread.join().unwrap(); +} + +#[test] +#[cfg(ossl102)] +fn alpn_h2() { + use futures::future; + use hyper::server::conn::Http; + use hyper::service; + use hyper::Response; + use openssl::ssl::{self, AlpnError}; + use tokio::net::TcpListener; + use tokio_openssl::SslAcceptorExt; + + let mut listener = TcpListener::bind(&"127.0.0.1:0".parse().unwrap()).unwrap(); + let port = listener.local_addr().unwrap().port(); + + let mut ctx = SslAcceptor::mozilla_modern(SslMethod::tls()).unwrap(); + ctx.set_certificate_chain_file("test/cert.pem").unwrap(); + ctx.set_private_key_file("test/key.pem", SslFiletype::PEM) + .unwrap(); + ctx.set_alpn_select_callback(|_, client| { + ssl::select_next_proto(b"\x02h2", client).ok_or(AlpnError::NOACK) + }); + let ctx = ctx.build(); + + let server = future::poll_fn(move || listener.poll_accept()) + .map_err(|e| panic!("tcp accept error: {}", e)) + .and_then(move |(stream, _)| ctx.accept_async(stream)) + .map_err(|e| panic!("tls accept error: {}", e)) + .and_then(|stream| { + assert_eq!(stream.get_ref().ssl().selected_alpn_protocol().unwrap(), b"h2"); + Http::new().http2_only(true).serve_connection( + stream, + service::service_fn_ok(|_| Response::new(Body::empty())), + ) + }).map(|_| ()) + .map_err(|e| panic!("http error: {}", e)); + + let mut runtime = Runtime::new().unwrap(); + runtime.spawn(server); + + let mut connector = HttpConnector::new(1); + connector.enforce_http(false); + let mut ssl = SslConnector::builder(SslMethod::tls()).unwrap(); + ssl.set_ca_file("test/cert.pem").unwrap(); + ssl.set_alpn_protos(b"\x02h2\x08http/1.1").unwrap(); + + let ssl = HttpsConnector::with_connector(connector, ssl).unwrap(); + let client = Client::builder().build::<_, Body>(ssl); let f = client .get(format!("https://localhost:{}", port).parse().unwrap()) @@ -129,6 +153,7 @@ fn localhost() { resp.into_body().for_each(|_| Ok(())) }); runtime.block_on(f).unwrap(); + drop(client); - thread.join().unwrap(); + runtime.run().unwrap(); }