diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d898b8fd..5eae40cd 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -143,7 +143,10 @@ jobs: if: startsWith(matrix.os, 'windows') run: choco install nasm shell: cmd - - run: cargo test - if: startsWith(matrix.os, 'windows') + - if: startsWith(matrix.os, 'windows') + # CI's Windows doesn't have require root certs + run: cargo test --workspace --exclude tokio-boring --exclude hyper-boring name: Run tests (Windows) - shell: cmd \ No newline at end of file + - if: "!startsWith(matrix.os, 'windows')" + run: cargo test + name: Run tests (not Windows) \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml index d48b3865..be4296d9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,5 +3,6 @@ members = [ "boring", "boring-sys", "systest", - "tokio-boring" + "tokio-boring", + "hyper-boring" ] diff --git a/README.md b/README.md index b3341534..75967820 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # boring -[![crates.io](https://img.shields.io/crates/v/openssl.svg)](https://crates.io/crates/boring) +[![crates.io](https://img.shields.io/crates/v/boring.svg)](https://crates.io/crates/boring) BoringSSL bindings for the Rust programming language and TLS adapters for [tokio](https://github.com/tokio-rs/tokio) and [hyper](https://github.com/hyperium/hyper) built on top of it. diff --git a/boring-sys/Cargo.toml b/boring-sys/Cargo.toml index 4f47c6c4..4568795b 100644 --- a/boring-sys/Cargo.toml +++ b/boring-sys/Cargo.toml @@ -9,6 +9,18 @@ description = "FFI bindings to BoringSSL" repository = "https://github.com/inikulin/boring" readme = "README.md" categories = ["cryptography", "external-ffi-bindings"] +include = [ + "/*.md", + "/*.toml", + "/LICENSE-MIT", + "/deps/boringssl/**/*.[chS]", + "/deps/boringssl/src/**/*.cc", + "/deps/boringssl/**/CMakeLists.txt", + "/deps/boringssl/**/sources.cmake", + "/deps/boringssl/LICENSE", + "/buid.rs", + "/src", +] [dependencies] libc = "0.2" diff --git a/hyper-boring/.gitignore b/hyper-boring/.gitignore new file mode 100644 index 00000000..582014fd --- /dev/null +++ b/hyper-boring/.gitignore @@ -0,0 +1,5 @@ +target/ +Cargo.lock +.vscode/ +.idea/ +*.iml diff --git a/hyper-boring/CHANGELOG.md b/hyper-boring/CHANGELOG.md new file mode 100644 index 00000000..bab285de --- /dev/null +++ b/hyper-boring/CHANGELOG.md @@ -0,0 +1,103 @@ +# Change Log + +## [Unreleased] + +## [v0.8.0] - 2019-12-10 + +### Changed + +* Upgraded to hyper 0.13 + +## [v0.8.0-alpha.4] - 2019-09-04 + +### Changed + +* Upgraded to hyper 0.13-alpha.4. + +## [v0.8.0-alpha.1] - 2019-09-04 + +### Changed + +* Upgraded to hyper 0.13-alpha.1. + +## [v0.7.1] - 2019-03-01 + +### Changed + +* TLSv1.3 sessions are now only used once, in accordance with the RFC's recommendation. + +## [v0.7.0] - 2018-11-13 + +### Changed + +* Upgraded `tokio-openssl` to 0.3. Handshake errors now return `openssl::ssl::HandshakeError` + rather than `openssl::ssl::Error`. + +## [v0.6.2] - 2018-11-07 + +### Added + +* Added ALPN support. If the `h2` protocol is selected during the handshake via ALPN, the connector + will automatically indicate that to Hyper via the `Connected::negotiated_h2` method. The + `HttpsConnector::new` method configures ALPN to support both the `h2` and `http/1.1` protocols. + Code using `HttpsConnector::with_connector` will need to configure ALPN manually. + +## [v0.6.1] - 2018-06-13 + +### Changed + +* `MaybeHttpsStream` now delegates `AsyncRead::read_buf` and `AsyncWrite::write_buf` to support + readv/writev over HTTP connections. + +## [v0.6.0] - 2018-06-04 + +### Changed + +* Upgraded to hyper 0.12. +* The callback closure now takes a `&Destination` rather than a `&URI` to match what Hyper provides + to connectors. + +## [v0.5.0] - 2018-02-18 + +### Changed + +* The `HttpsConnector::with_connector` function now takes an `SslConnectorBuilder` rather than an + `SslConnector` due to a change in the session caching implementation. This is requried to + properly support TLSv1.3. + +## [v0.4.1] - 2018-01-11 + +### Changed + +* Stopped enabling default features for `hyper`. + +## [v0.4.0] - 2018-01-11 + +### Removed + +* The `HttpsConnector::danger_disable_hostname_verification` method has been removed. Instead, use + a callback which configures the `ConnectConfiguration` directly. + +### Changed + +* Upgraded to openssl 0.10. +* The `HttpsConnector::ssl_callback` method has been renamed to `HttpsConnector::set_callback`, + and is passed a reference to the `ConnectConfiguration` rather than just the `SslRef`. + +## Older + +Look at the [release tags] for information about older releases. + +[Unreleased]: https://github.com/sfackler/hyper-openssl/compare/0.8.0...master +[v0.8.0]: https://github.com/sfackler/hyper-openssl/compare/0.8.0-alpha.4...0.8.0 +[v0.8.0-alpha.4]: https://github.com/sfackler/hyper-openssl/compare/0.8.0-alpha.1...0.8.0-alpha.4 +[v0.8.0-alpha.1]: https://github.com/sfackler/hyper-openssl/compare/0.7.1...0.8.0-alpha.1 +[v0.7.1]: https://github.com/sfackler/hyper-openssl/compare/0.7.0...0.7.1 +[v0.7.0]: https://github.com/sfackler/hyper-openssl/compare/0.6.2...0.7.0 +[v0.6.2]: https://github.com/sfackler/hyper-openssl/compare/0.6.1...0.6.2 +[v0.6.1]: https://github.com/sfackler/hyper-openssl/compare/0.6.0...0.6.1 +[v0.6.0]: https://github.com/sfackler/hyper-openssl/compare/0.5.0...0.6.0 +[v0.5.0]: https://github.com/sfackler/hyper-openssl/compare/0.4.1...0.5.0 +[v0.4.1]: https://github.com/sfackler/hyper-openssl/compare/0.4.0...0.4.1 +[v0.4.0]: https://github.com/sfackler/hyper-openssl/compare/0.3.1...0.4.0 +[release tags]: https://github.com/sfackler/hyper-openssl/releases diff --git a/hyper-boring/Cargo.toml b/hyper-boring/Cargo.toml new file mode 100644 index 00000000..7666f16c --- /dev/null +++ b/hyper-boring/Cargo.toml @@ -0,0 +1,33 @@ +[package] +name = "hyper-boring" +version = "1.0.0" +authors = ["Steven Fackler ", "Ivan Nikulin "] +edition = "2018" +description = "Hyper TLS support via BoringSSL" +license = "MIT/Apache-2.0" +repository = "https://github.com/sfackler/hyper-boring" +readme = "README.md" +exclude = ["test/*"] + +[features] +default = ["runtime"] + +runtime = ["hyper/runtime"] + +[dependencies] +antidote = "1.0.0" +bytes = "0.5" +http = "0.2" +hyper = { version = "0.13", default-features = false } +linked_hash_set = "0.1" +once_cell = "1.0" +boring = { version = "1.0.0", path = "../boring" } +boring-sys = { version = "1.0.0", path = "../boring-sys" } +tokio = "0.2" +tokio-boring = { version = "1.0.0", path = "../tokio-boring" } +tower-layer = "0.3" + +[dev-dependencies] +hyper = "0.13" +tokio = { version = "0.2", features = ["full"] } +futures = "0.3" diff --git a/hyper-boring/LICENSE-APACHE b/hyper-boring/LICENSE-APACHE new file mode 100644 index 00000000..16fe87b0 --- /dev/null +++ b/hyper-boring/LICENSE-APACHE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + +Copyright [yyyy] [name of copyright owner] + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/hyper-boring/LICENSE-MIT b/hyper-boring/LICENSE-MIT new file mode 100644 index 00000000..c1073587 --- /dev/null +++ b/hyper-boring/LICENSE-MIT @@ -0,0 +1,23 @@ +The MIT License (MIT) + +Copyright (c) 2015-2016 Steven Fackler +Copyright (c) 2020 Ivan Nikulin + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + diff --git a/hyper-boring/README.md b/hyper-boring/README.md new file mode 100644 index 00000000..10bf1994 --- /dev/null +++ b/hyper-boring/README.md @@ -0,0 +1,25 @@ +# hyper-boring + +[Documentation](https://docs.rs/hyper-boring) + +Hyper SSL support via BoringSSL. + +## License + +Licensed under either of + + * Apache License, Version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or http://www.apache.org/licenses/LICENSE-2.0) + * MIT license ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT) + +at your option. + +### Contribution + +Unless you explicitly state otherwise, any contribution intentionally +submitted for inclusion in the work by you, as defined in the Apache-2.0 +license, shall be dual licensed as above, without any additional terms or +conditions. + +## Accolades + +The project is based on a fork of [hyper-openssl](https://github.com/sfackler/hyper-openssl). \ No newline at end of file diff --git a/hyper-boring/src/cache.rs b/hyper-boring/src/cache.rs new file mode 100644 index 00000000..f750c0f2 --- /dev/null +++ b/hyper-boring/src/cache.rs @@ -0,0 +1,92 @@ +use boring::ssl::SslVersion; +use boring::ssl::{SslSession, SslSessionRef}; +use linked_hash_set::LinkedHashSet; +use std::borrow::Borrow; +use std::collections::hash_map::{Entry, HashMap}; +use std::hash::{Hash, Hasher}; + +#[derive(Hash, PartialEq, Eq, Clone)] +pub struct SessionKey { + pub host: String, + pub port: u16, +} + +#[derive(Clone)] +struct HashSession(SslSession); + +impl PartialEq for HashSession { + fn eq(&self, other: &HashSession) -> bool { + self.0.id() == other.0.id() + } +} + +impl Eq for HashSession {} + +impl Hash for HashSession { + fn hash(&self, state: &mut H) + where + H: Hasher, + { + self.0.id().hash(state); + } +} + +impl Borrow<[u8]> for HashSession { + fn borrow(&self) -> &[u8] { + self.0.id() + } +} + +pub struct SessionCache { + sessions: HashMap>, + reverse: HashMap, +} + +impl SessionCache { + pub fn new() -> SessionCache { + SessionCache { + sessions: HashMap::new(), + reverse: HashMap::new(), + } + } + + pub fn insert(&mut self, key: SessionKey, session: SslSession) { + let session = HashSession(session); + + self.sessions + .entry(key.clone()) + .or_insert_with(LinkedHashSet::new) + .insert(session.clone()); + self.reverse.insert(session, key); + } + + pub fn get(&mut self, key: &SessionKey) -> Option { + let session = { + let sessions = self.sessions.get_mut(key)?; + sessions.front().cloned()?.0 + }; + + // https://tools.ietf.org/html/rfc8446#appendix-C.4 + // OpenSSL will remove the session from its cache after the handshake completes anyway, but this ensures + // that concurrent handshakes don't end up with the same session. + if session.protocol_version() == SslVersion::TLS1_3 { + self.remove(&session); + } + + Some(session) + } + + pub fn remove(&mut self, session: &SslSessionRef) { + let key = match self.reverse.remove(session.id()) { + Some(key) => key, + None => return, + }; + + if let Entry::Occupied(mut sessions) = self.sessions.entry(key) { + sessions.get_mut().remove(session.id()); + if sessions.get().is_empty() { + sessions.remove(); + } + } + } +} diff --git a/hyper-boring/src/lib.rs b/hyper-boring/src/lib.rs new file mode 100644 index 00000000..f8310656 --- /dev/null +++ b/hyper-boring/src/lib.rs @@ -0,0 +1,350 @@ +//! Hyper SSL support via OpenSSL. +#![warn(missing_docs)] +#![doc(html_root_url = "https://docs.rs/hyper-boring")] + +use crate::cache::{SessionCache, SessionKey}; +use antidote::Mutex; +use boring::error::ErrorStack; +use boring::ex_data::Index; +use boring::ssl::{ + ConnectConfiguration, Ssl, SslConnector, SslConnectorBuilder, SslMethod, SslSessionCacheMode, +}; +use bytes::{Buf, BufMut}; +use http::uri::Scheme; +use hyper::client::connect::{Connected, Connection}; +#[cfg(feature = "runtime")] +use hyper::client::HttpConnector; +use hyper::service::Service; +use hyper::Uri; +use once_cell::sync::OnceCell; +use std::error::Error; +use std::fmt::Debug; +use std::future::Future; +use std::io; +use std::mem::MaybeUninit; +use std::pin::Pin; +use std::sync::Arc; +use std::task::{Context, Poll}; +use tokio::io::{AsyncRead, AsyncWrite}; +use tokio_boring::SslStream; +use tower_layer::Layer; + +mod cache; +#[cfg(test)] +mod test; + +fn key_index() -> Result, ErrorStack> { + static IDX: OnceCell> = OnceCell::new(); + IDX.get_or_try_init(Ssl::new_ex_index).map(|v| *v) +} + +#[derive(Clone)] +struct Inner { + ssl: SslConnector, + cache: Arc>, + #[allow(clippy::type_complexity)] + callback: Option< + Arc Result<(), ErrorStack> + Sync + Send>, + >, +} + +impl Inner { + fn setup_ssl(&self, uri: &Uri, host: &str) -> Result { + let mut conf = self.ssl.configure()?; + + if let Some(ref callback) = self.callback { + callback(&mut conf, uri)?; + } + + let key = SessionKey { + host: host.to_string(), + port: uri.port_u16().unwrap_or(443), + }; + + if let Some(session) = self.cache.lock().get(&key) { + unsafe { + conf.set_session(&session)?; + } + } + + let idx = key_index()?; + conf.set_ex_data(idx, key); + + Ok(conf) + } +} + +/// A layer which wraps services in an `HttpsConnector`. +pub struct HttpsLayer { + inner: Inner, +} + +impl HttpsLayer { + /// Creates a new `HttpsLayer` with default settings. + /// + /// ALPN is configured to support both HTTP/1 and HTTP/1.1. + pub fn new() -> Result { + let mut ssl = SslConnector::builder(SslMethod::tls())?; + // avoid unused_mut warnings when building against OpenSSL 1.0.1 + ssl = ssl; + + ssl.set_alpn_protos(b"\x02h2\x08http/1.1")?; + + Self::with_connector(ssl) + } + + /// Creates a new `HttpsLayer`. + /// + /// The session cache configuration of `ssl` will be overwritten. + pub fn with_connector(mut ssl: SslConnectorBuilder) -> Result { + let cache = Arc::new(Mutex::new(SessionCache::new())); + + ssl.set_session_cache_mode(SslSessionCacheMode::CLIENT); + + ssl.set_new_session_callback({ + let cache = cache.clone(); + move |ssl, session| { + if let Some(key) = key_index().ok().and_then(|idx| ssl.ex_data(idx)) { + cache.lock().insert(key.clone(), session); + } + } + }); + + ssl.set_remove_session_callback({ + let cache = cache.clone(); + move |_, session| cache.lock().remove(session) + }); + + Ok(HttpsLayer { + inner: Inner { + ssl: ssl.build(), + cache, + callback: None, + }, + }) + } + + /// Registers a callback which can customize the configuration of each connection. + pub fn set_callback(&mut self, callback: F) + where + F: Fn(&mut ConnectConfiguration, &Uri) -> Result<(), ErrorStack> + 'static + Sync + Send, + { + self.inner.callback = Some(Arc::new(callback)); + } +} + +impl Layer for HttpsLayer { + type Service = HttpsConnector; + + fn layer(&self, inner: S) -> HttpsConnector { + HttpsConnector { + http: inner, + inner: self.inner.clone(), + } + } +} + +/// A Connector using OpenSSL to support `http` and `https` schemes. +#[derive(Clone)] +pub struct HttpsConnector { + http: T, + inner: Inner, +} + +#[cfg(feature = "runtime")] +impl HttpsConnector { + /// Creates a a new `HttpsConnector` using default settings. + /// + /// 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() -> Result, ErrorStack> { + let mut http = HttpConnector::new(); + http.enforce_http(false); + + HttpsLayer::new().map(|l| l.layer(http)) + } +} + +impl HttpsConnector +where + S: Service + Send, + S::Error: Into>, + S::Future: Unpin + Send + 'static, + T: AsyncRead + AsyncWrite + Connection + Unpin + Debug + Sync + Send + 'static, +{ + /// Creates a new `HttpsConnector`. + /// + /// The session cache configuration of `ssl` will be overwritten. + pub fn with_connector( + http: S, + ssl: SslConnectorBuilder, + ) -> Result, ErrorStack> { + HttpsLayer::with_connector(ssl).map(|l| l.layer(http)) + } + + /// Registers a callback which can customize the configuration of each connection. + pub fn set_callback(&mut self, callback: F) + where + F: Fn(&mut ConnectConfiguration, &Uri) -> Result<(), ErrorStack> + 'static + Sync + Send, + { + self.inner.callback = Some(Arc::new(callback)); + } +} + +impl Service for HttpsConnector +where + S: Service + Send, + S::Error: Into>, + S::Future: Unpin + Send + 'static, + S::Response: AsyncRead + AsyncWrite + Connection + Unpin + Debug + Sync + Send + 'static, +{ + type Response = MaybeHttpsStream; + type Error = Box; + #[allow(clippy::type_complexity)] + type Future = Pin> + Send>>; + + fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll> { + self.http.poll_ready(cx).map_err(Into::into) + } + + fn call(&mut self, uri: Uri) -> Self::Future { + let tls_setup = if uri.scheme() == Some(&Scheme::HTTPS) { + Some((self.inner.clone(), uri.clone())) + } else { + None + }; + + let connect = self.http.call(uri); + + let f = async { + let conn = connect.await.map_err(Into::into)?; + + let (inner, uri) = match tls_setup { + Some((inner, uri)) => (inner, uri), + None => return Ok(MaybeHttpsStream::Http(conn)), + }; + + let host = uri.host().ok_or_else(|| "URI missing host")?; + + let config = inner.setup_ssl(&uri, host)?; + let stream = tokio_boring::connect(config, host, conn).await?; + + Ok(MaybeHttpsStream::Https(stream)) + }; + + Box::pin(f) + } +} + +/// A stream which may be wrapped with TLS. +pub enum MaybeHttpsStream { + /// A raw HTTP stream. + Http(T), + /// An SSL-wrapped HTTP stream. + Https(SslStream), +} + +impl AsyncRead for MaybeHttpsStream +where + T: AsyncRead + AsyncWrite + Unpin, +{ + unsafe fn prepare_uninitialized_buffer(&self, buf: &mut [MaybeUninit]) -> bool { + match &*self { + MaybeHttpsStream::Http(s) => s.prepare_uninitialized_buffer(buf), + MaybeHttpsStream::Https(s) => s.prepare_uninitialized_buffer(buf), + } + } + + fn poll_read( + mut self: Pin<&mut Self>, + ctx: &mut Context<'_>, + buf: &mut [u8], + ) -> Poll> { + match &mut *self { + MaybeHttpsStream::Http(s) => Pin::new(s).poll_read(ctx, buf), + MaybeHttpsStream::Https(s) => Pin::new(s).poll_read(ctx, buf), + } + } + + fn poll_read_buf( + mut self: Pin<&mut Self>, + ctx: &mut Context<'_>, + buf: &mut B, + ) -> Poll> + where + B: BufMut, + { + match &mut *self { + MaybeHttpsStream::Http(s) => Pin::new(s).poll_read_buf(ctx, buf), + MaybeHttpsStream::Https(s) => Pin::new(s).poll_read_buf(ctx, buf), + } + } +} + +impl AsyncWrite for MaybeHttpsStream +where + T: AsyncRead + AsyncWrite + Unpin, +{ + fn poll_write( + mut self: Pin<&mut Self>, + ctx: &mut Context<'_>, + buf: &[u8], + ) -> Poll> { + match &mut *self { + MaybeHttpsStream::Http(s) => Pin::new(s).poll_write(ctx, buf), + MaybeHttpsStream::Https(s) => Pin::new(s).poll_write(ctx, buf), + } + } + + fn poll_flush(mut self: Pin<&mut Self>, ctx: &mut Context<'_>) -> Poll> { + match &mut *self { + MaybeHttpsStream::Http(s) => Pin::new(s).poll_flush(ctx), + MaybeHttpsStream::Https(s) => Pin::new(s).poll_flush(ctx), + } + } + + fn poll_shutdown(mut self: Pin<&mut Self>, ctx: &mut Context<'_>) -> Poll> { + match &mut *self { + MaybeHttpsStream::Http(s) => Pin::new(s).poll_shutdown(ctx), + MaybeHttpsStream::Https(s) => Pin::new(s).poll_shutdown(ctx), + } + } + + fn poll_write_buf( + mut self: Pin<&mut Self>, + ctx: &mut Context<'_>, + buf: &mut B, + ) -> Poll> + where + B: Buf, + { + match &mut *self { + MaybeHttpsStream::Http(s) => Pin::new(s).poll_write_buf(ctx, buf), + MaybeHttpsStream::Https(s) => Pin::new(s).poll_write_buf(ctx, buf), + } + } +} + +impl Connection for MaybeHttpsStream +where + T: Connection, +{ + fn connected(&self) -> Connected { + match self { + MaybeHttpsStream::Http(s) => s.connected(), + MaybeHttpsStream::Https(s) => { + let mut connected = s.get_ref().connected(); + // Avoid unused_mut warnings on OpenSSL 1.0.1 + connected = connected; + + if s.ssl().selected_alpn_protocol() == Some(b"h2") { + connected = connected.negotiated_h2(); + } + + connected + } + } + } +} diff --git a/hyper-boring/src/test.rs b/hyper-boring/src/test.rs new file mode 100644 index 00000000..8d431cbe --- /dev/null +++ b/hyper-boring/src/test.rs @@ -0,0 +1,139 @@ +use super::*; +use boring::ssl::{SslAcceptor, SslFiletype, SslMethod}; +use futures::StreamExt; +use hyper::client::HttpConnector; +use hyper::server::conn::Http; +use hyper::{service, Response}; +use hyper::{Body, Client}; +use tokio::net::TcpListener; + +#[tokio::test] +#[cfg(feature = "runtime")] +async fn google() { + let ssl = HttpsConnector::new().unwrap(); + let client = Client::builder() + .pool_max_idle_per_host(0) + .build::<_, Body>(ssl); + + for _ in 0..3 { + let resp = client + .get("https://www.google.com".parse().unwrap()) + .await + .unwrap(); + assert!(resp.status().is_success(), "{}", resp.status()); + let mut body = resp.into_body(); + while body.next().await.transpose().unwrap().is_some() {} + } +} + +#[tokio::test] +async fn localhost() { + let mut listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); + let port = listener.local_addr().unwrap().port(); + + let server = async move { + let mut acceptor = SslAcceptor::mozilla_intermediate(SslMethod::tls()).unwrap(); + acceptor.set_session_id_context(b"test").unwrap(); + acceptor + .set_private_key_file("test/key.pem", SslFiletype::PEM) + .unwrap(); + acceptor + .set_certificate_chain_file("test/cert.pem") + .unwrap(); + let acceptor = acceptor.build(); + + for _ in 0..3 { + let stream = listener.accept().await.unwrap().0; + let stream = tokio_boring::accept(&acceptor, stream).await.unwrap(); + + let service = + service::service_fn(|_| async { Ok::<_, io::Error>(Response::new(Body::empty())) }); + + Http::new() + .http1_keep_alive(false) + .serve_connection(stream, service) + .await + .unwrap(); + } + }; + tokio::spawn(server); + + let mut connector = HttpConnector::new(); + connector.enforce_http(false); + let mut ssl = SslConnector::builder(SslMethod::tls()).unwrap(); + ssl.set_ca_file("test/cert.pem").unwrap(); + + use std::fs::File; + use std::io::Write; + + let file = File::create("../target/keyfile.log").unwrap(); + ssl.set_keylog_callback(move |_, line| { + let _ = writeln!(&file, "{}", line); + }); + + let ssl = HttpsConnector::with_connector(connector, ssl).unwrap(); + let client = Client::builder().build::<_, Body>(ssl); + + for _ in 0..3 { + let resp = client + .get(format!("https://localhost:{}", port).parse().unwrap()) + .await + .unwrap(); + assert!(resp.status().is_success(), "{}", resp.status()); + let mut body = resp.into_body(); + while body.next().await.transpose().unwrap().is_some() {} + } +} + +#[tokio::test] +async fn alpn_h2() { + use boring::ssl::{self, AlpnError}; + + let mut listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); + let port = listener.local_addr().unwrap().port(); + + let server = async move { + let mut acceptor = SslAcceptor::mozilla_modern(SslMethod::tls()).unwrap(); + acceptor + .set_certificate_chain_file("test/cert.pem") + .unwrap(); + acceptor + .set_private_key_file("test/key.pem", SslFiletype::PEM) + .unwrap(); + acceptor.set_alpn_select_callback(|_, client| { + ssl::select_next_proto(b"\x02h2", client).ok_or(AlpnError::NOACK) + }); + let acceptor = acceptor.build(); + + let stream = listener.accept().await.unwrap().0; + let stream = tokio_boring::accept(&acceptor, stream).await.unwrap(); + assert_eq!(stream.ssl().selected_alpn_protocol().unwrap(), b"h2"); + + let service = + service::service_fn(|_| async { Ok::<_, io::Error>(Response::new(Body::empty())) }); + + Http::new() + .http2_only(true) + .serve_connection(stream, service) + .await + .unwrap(); + }; + tokio::spawn(server); + + let mut connector = HttpConnector::new(); + 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 resp = client + .get(format!("https://localhost:{}", port).parse().unwrap()) + .await + .unwrap(); + assert!(resp.status().is_success(), "{}", resp.status()); + let mut body = resp.into_body(); + while body.next().await.transpose().unwrap().is_some() {} +} diff --git a/hyper-boring/test/cert.pem b/hyper-boring/test/cert.pem new file mode 100644 index 00000000..2d08323d --- /dev/null +++ b/hyper-boring/test/cert.pem @@ -0,0 +1,21 @@ +-----BEGIN CERTIFICATE----- +MIIDhTCCAm2gAwIBAgIJALClJS+cq+ykMA0GCSqGSIb3DQEBCwUAMFkxCzAJBgNV +BAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJbnRlcm5ldCBX +aWRnaXRzIFB0eSBMdGQxEjAQBgNVBAMMCWxvY2FsaG9zdDAeFw0xNjExMjgwNTU5 +MjNaFw0yNjExMjYwNTU5MjNaMFkxCzAJBgNVBAYTAkFVMRMwEQYDVQQIDApTb21l +LVN0YXRlMSEwHwYDVQQKDBhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQxEjAQBgNV +BAMMCWxvY2FsaG9zdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAL4k +A0mghV17MPi033tKh1IK4pM6zpzHFrgi2smU97O/kxvSbNnDoZHEdqq3AtRcjELg +HeKTd02jyAHxGoRTAVORIp0p4LCCvlR7EnHH78e3vIq3lkLe5uqyujnx2NJJIrIX +r5Y+Z3QuMUSAix6GreAb19KcTZG82igvh4dOQP/pmlqQsyrPpioLy50O2NuBqU5Q +xyevFRHsWfe3M7ayzJBVwMpDJxg3saOETXgzMzfKtrj2Pw0mfcHQMtsPv7z85ug0 +yyd9iXwwLYx2RqZ6epChsWuY2zj7Zfcis3DzbsrW8/J758KNkjZVWS9aJmDGsT3R +xRlVDnIeow/SWi5qtqECAwEAAaNQME4wHQYDVR0OBBYEFNU1F6I+C06y6rN1yjn0 +i/ARufw1MB8GA1UdIwQYMBaAFNU1F6I+C06y6rN1yjn0i/ARufw1MAwGA1UdEwQF +MAMBAf8wDQYJKoZIhvcNAQELBQADggEBACvFmTY+QSrc9EIAtuGk20L4OHrkOoRv +veMIu3PAGbrzjE0rRC1qeLqkqudlWCk+xE6nNe90tB0qyY8AOgj68K2OplrJIhqt +rxJ/Ohtbepwi53Q5npRoib6f9aL+FuT0hnVtVon2ngWRizSdH/CY7vCWuJjTtlon +3J8TGPA1cnj8FtEEfF3ISd0/XCE2oar875FOscf7S0eLnORbuunCVU/RaNn25h/r +9EhvoaPZ6cSZpt7UliMkSt6b07/A2SwU5C19BS1XoqGH02P9OV0pmuJn7N/fOGer +aVbDiPpb+UAUHFUSyu32iK6T2/6OuJS7MQ1cI2biB2SWgWNBTmhRF1s= +-----END CERTIFICATE----- diff --git a/hyper-boring/test/key.pem b/hyper-boring/test/key.pem new file mode 100644 index 00000000..58f1020a --- /dev/null +++ b/hyper-boring/test/key.pem @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC+JANJoIVdezD4 +tN97SodSCuKTOs6cxxa4ItrJlPezv5Mb0mzZw6GRxHaqtwLUXIxC4B3ik3dNo8gB +8RqEUwFTkSKdKeCwgr5UexJxx+/Ht7yKt5ZC3ubqsro58djSSSKyF6+WPmd0LjFE +gIsehq3gG9fSnE2RvNooL4eHTkD/6ZpakLMqz6YqC8udDtjbgalOUMcnrxUR7Fn3 +tzO2ssyQVcDKQycYN7GjhE14MzM3yra49j8NJn3B0DLbD7+8/OboNMsnfYl8MC2M +dkamenqQobFrmNs4+2X3IrNw827K1vPye+fCjZI2VVkvWiZgxrE90cUZVQ5yHqMP +0louarahAgMBAAECggEAd08brQCHjs/1O6orLS7n2IgyAhZ9fQzD6ckdJi5Oe8Cz +K1sPqFlEMbZoi9iIcv6bmH8O4ZSM4O/rWaSTcgKvq2M/qASWE8wGZ/ZN7Y16nQRi +z1xBcjZyCUUa668g0VrI5Z1NNWZ0/gbaLVTHduEli6GM/H/NgKxS67JfRXzJ9onl +d6vrK+xmeHyA7QSOieEDettaNCvm+HjU8mmOb4F1pCNZktDrch5rI8EzQlmFQuq4 +y50YLRZGSlK1QLjzMnT//oaP7mHjN/inzZTHBvTzhU2OjcjzEW7l4ry224Sdu/eH +lhEnNk2eq+mH/yESkn3sJcmH4uYIXh8Dyvcy/uVkPQKBgQDzSC89qxT4sls9n0sL +0DfVhq1D7kEXggD/4wNA714N24N/NWi5BYUDZVh9Kxqy9SuWlFYg1L1ZNZHB02aV +GJdEiFMFgRea2E5NHnhWop+qYPq5N9jD72MHmz/6swX9VGi1p5DqjzK2hWMgoih9 +4ky1zxMw+P+aDaQ6xwZF1nr+mwKBgQDIFKTvaJYjqQ/lzRMIPLA3sg6RQ+Mqwt/C +BZ9Oc3DGtuglV8F73i7ML2Ptg0GtVZo3NJgGzMerpNvEoc1pDCuZkzSYitcYysQQ +wsailMQFCv9jJ9g28lSGKlEPYhcLejH8ZRi8jH0fObHIvgr7komNvvPIDFnw/uR8 +WsgrloD1cwKBgAdlAkqVkKWehjdxSA6r3YaX+Vw/OatFQFKGy+qFXA5/xZdwQCaf +jFN2GSJ01PLrkM+a4qNM1BSKFEwX6N5PSQnEOwHH0rfaK0cczfuUJdY/7F8E24nZ +FOF+TouINX5lumkLFtSKVbhGhaTQSPrKjhpYmPS8HMjJ8Vv4ALDOvB5RAoGBAJAS +RX3bCpmdCESKOdUplh5UyaaSgsZs0qCsWb0s5R1B4cHaAgnGwF3pFgSWCjndNRHh +fkMPPAv9xv49IGMvD0ojtLDO8Pn6L9p91niFtOyIscNdkpRmRLTjTcFM+ZkbIVlE +Ft7WLtbIPZt2NQRXzVLTGEmJk040zKQ63n58flm/AoGBAKt97WLeHB9S/q0dpEGX +Qk+1BXRAH0/4wK9lNrSeaw+npFr8rNN9K3sIBC/XnOwhT+wbKBpOoBT3PNHbNxVr +EPPQ/pPmZ1TcHc7bszJnZon2S2PFJRDN4601X1/eFoTvakBnLlt1096paaolSmCG +nYED9qXuh2VzUU1GgcqPXgf/ +-----END PRIVATE KEY----- diff --git a/systest/Cargo.toml b/systest/Cargo.toml index 60718243..a2a8632a 100644 --- a/systest/Cargo.toml +++ b/systest/Cargo.toml @@ -2,6 +2,7 @@ name = "systest" version = "0.1.0" authors = ["Alex Crichton "] +publish = false [dependencies] libc = "0.2" diff --git a/tokio-boring/Cargo.toml b/tokio-boring/Cargo.toml index d3d1b146..ac3e2dc0 100644 --- a/tokio-boring/Cargo.toml +++ b/tokio-boring/Cargo.toml @@ -14,8 +14,8 @@ An implementation of SSL streams for Tokio backed by BoringSSL [dependencies] boring = { version = "1.0.0", path = "../boring" } boring-sys = { version = "1.0.0", path = "../boring-sys" } -tokio = "0.3" +tokio = "0.2" [dev-dependencies] futures = "0.3" -tokio = { version = "0.3", features = ["full"] } +tokio = { version = "0.2", features = ["full"] } diff --git a/tokio-boring/LICENSE-MIT b/tokio-boring/LICENSE-MIT index d5a3b639..5aefd5ec 100644 --- a/tokio-boring/LICENSE-MIT +++ b/tokio-boring/LICENSE-MIT @@ -1,4 +1,5 @@ Copyright (c) 2016 Tokio contributors +Copyright (c) 2020 Ivan Nikulin Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated diff --git a/tokio-boring/src/lib.rs b/tokio-boring/src/lib.rs index b7cb10d0..69f9c694 100644 --- a/tokio-boring/src/lib.rs +++ b/tokio-boring/src/lib.rs @@ -22,9 +22,10 @@ use std::error::Error; use std::fmt; use std::future::Future; use std::io::{self, Read, Write}; +use std::mem::MaybeUninit; use std::pin::Pin; use std::task::{Context, Poll}; -use tokio::io::{AsyncRead, AsyncWrite, ReadBuf}; +use tokio::io::{AsyncRead, AsyncWrite}; /// Asynchronously performs a client-side TLS handshake over the provided stream. pub async fn connect( @@ -98,13 +99,10 @@ where S: AsyncRead + Unpin, { fn read(&mut self, buf: &mut [u8]) -> io::Result { - self.with_context(|ctx, stream| { - let mut buf = ReadBuf::new(buf); - match stream.poll_read(ctx, &mut buf)? { - Poll::Ready(()) => Ok(buf.filled().len()), - Poll::Pending => Err(io::Error::from(io::ErrorKind::WouldBlock)), - } - }) + match self.with_context(|ctx, stream| stream.poll_read(ctx, buf)) { + Poll::Ready(r) => r, + Poll::Pending => Err(io::Error::from(io::ErrorKind::WouldBlock)), + } } } @@ -176,7 +174,7 @@ impl SslStream where S: AsyncRead + AsyncWrite + Unpin, { - /// Constructs an `SslStream` from a pointer to the underlying BoringSSL `SSL` struct. + /// Constructs an `SslStream` from a pointer to the underlying OpenSSL `SSL` struct. /// /// This is useful if the handshake has already been completed elsewhere. /// @@ -193,18 +191,19 @@ impl AsyncRead for SslStream where S: AsyncRead + AsyncWrite + Unpin, { + unsafe fn prepare_uninitialized_buffer(&self, _: &mut [MaybeUninit]) -> bool { + // Note that this does not forward to `S` because the buffer is + // unconditionally filled in by OpenSSL, not the actual object `S`. + // We're decrypting bytes from `S` into the buffer above! + false + } + fn poll_read( mut self: Pin<&mut Self>, ctx: &mut Context<'_>, - buf: &mut ReadBuf<'_>, - ) -> Poll> { - self.with_context(ctx, |s| match cvt(s.read(buf.initialize_unfilled()))? { - Poll::Ready(nread) => { - buf.advance(nread); - Poll::Ready(Ok(())) - } - Poll::Pending => Poll::Pending, - }) + buf: &mut [u8], + ) -> Poll> { + self.with_context(ctx, |s| cvt(s.read(buf))) } } diff --git a/tokio-boring/tests/google.rs b/tokio-boring/tests/google.rs index b3cd6525..25fca764 100644 --- a/tokio-boring/tests/google.rs +++ b/tokio-boring/tests/google.rs @@ -33,7 +33,7 @@ async fn google() { #[tokio::test] async fn server() { - let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); + let mut listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); let addr = listener.local_addr().unwrap(); let server = async move {