Merge remote-tracking branch 'upstream/master'
This commit is contained in:
commit
b0fe60dcf6
|
|
@ -322,7 +322,8 @@ jobs:
|
|||
working-directory: ${{ runner.temp }}/llvm/bin
|
||||
run: ln -s clang clang++-12
|
||||
- name: Install ${{ matrix.target }} toolchain
|
||||
run: brew tap messense/macos-cross-toolchains && brew install --overwrite python@3.11 && brew install ${{ matrix.target }}
|
||||
# TODO(rmehra): find a better way to overwrite the python3 version without specifying version
|
||||
run: brew tap messense/macos-cross-toolchains && brew install --overwrite python@3.12 && brew install ${{ matrix.target }}
|
||||
- name: Set BORING_BSSL_FIPS_COMPILER_EXTERNAL_TOOLCHAIN
|
||||
run: echo "BORING_BSSL_FIPS_COMPILER_EXTERNAL_TOOLCHAIN=$(brew --prefix ${{ matrix.target }})/toolchain" >> $GITHUB_ENV
|
||||
shell: bash
|
||||
|
|
@ -360,3 +361,5 @@ jobs:
|
|||
name: Run `rpk,underscore-wildcards` tests
|
||||
- run: cargo test --features pq-experimental,rpk,underscore-wildcards
|
||||
name: Run `pq-experimental,rpk,underscore-wildcards` tests
|
||||
- run: cargo test -p hyper-boring --features hyper1
|
||||
name: Run hyper 1.0 tests for hyper-boring
|
||||
|
|
|
|||
21
Cargo.toml
21
Cargo.toml
|
|
@ -8,7 +8,7 @@ members = [
|
|||
resolver = "2"
|
||||
|
||||
[workspace.package]
|
||||
version = "4.9.1"
|
||||
version = "4.10.2"
|
||||
repository = "https://github.com/cloudflare/boring"
|
||||
edition = "2021"
|
||||
|
||||
|
|
@ -19,11 +19,12 @@ tag-prefix = ""
|
|||
publish = false
|
||||
|
||||
[workspace.dependencies]
|
||||
boring-sys = { version = "4.9.1", path = "./boring-sys", package = "rboring-sys" }
|
||||
boring = { version = "4.9.1", path = "./boring", package = "rboring"}
|
||||
tokio-boring = { version = "4.9.1", path = "./tokio-boring", package = "tokio-rboring"}
|
||||
boring-sys = { version = "4.10.2", path = "./rboring-sys" }
|
||||
boring = { version = "4.10.2", path = "./rboring" }
|
||||
tokio-boring = { version = "4.10.2", path = "./rtokio-boring" }
|
||||
|
||||
bindgen = { version = "0.68.1", default-features = false, features = ["runtime"] }
|
||||
bindgen = { version = "0.70.1", default-features = false, features = ["runtime"] }
|
||||
bytes = "1"
|
||||
cmake = "0.1.18"
|
||||
fs_extra = "1.3.0"
|
||||
fslock = "0.2"
|
||||
|
|
@ -36,9 +37,15 @@ futures = "0.3"
|
|||
tokio = "1"
|
||||
anyhow = "1"
|
||||
antidote = "1.0.0"
|
||||
http = "0.2"
|
||||
hyper = { package = "rhyper", version = "0.14", default-features = false }
|
||||
http = "1"
|
||||
http-body-util = "0.1.2"
|
||||
http_old = { package = "http", version = "0.2" }
|
||||
hyper = "1"
|
||||
hyper-util = "0.1.6"
|
||||
hyper_old = { package = "rhyper", version = "0.14", default-features = false }
|
||||
linked_hash_set = "0.1"
|
||||
once_cell = "1.0"
|
||||
openssl-macros = "0.1.1"
|
||||
tower = "0.4"
|
||||
tower-layer = "0.3"
|
||||
tower-service = "0.3"
|
||||
|
|
|
|||
|
|
@ -1,3 +1,25 @@
|
|||
4.10.2
|
||||
- 2024-09-18 boring-pq.patch Fix by not updating crypto_test_data.cc
|
||||
|
||||
4.10.1
|
||||
- 2024-09-18 Don't support X25519MLKEM768 by default (yet)
|
||||
|
||||
4.10.0
|
||||
- 2024-09-18 Implement optional Hyper 1 support in hyper-boring (#246)
|
||||
- 2024-09-17 Add post-quantum key agreement X25519MLKEM768
|
||||
- 2024-09-10 Revert "PQ: fix timing sidechannels and add IPDWing"
|
||||
- 2024-09-17 Update bindgen to 0.70.1
|
||||
- 2024-09-17 Expose SSL(_CTX)_set1_curves_list (#270)
|
||||
- 2024-09-11 Expose SSL_CTX_set_info_callback (#266)
|
||||
- 2024-09-03 Use ForeignType::into_ptr wherever applicable
|
||||
- 2024-08-19 Expose RSAPSS public key Id type
|
||||
- 2024-08-15 Fix macos FIPS crossbuild
|
||||
- 2024-08-15 Add tests for X509Ref::subject_key_id, X509Ref::authority_key_id, and X509NameRef::print_ex
|
||||
- 2024-08-14 Expose X509NameRef::print_ex
|
||||
- 2024-08-13 Introduce `corresponds` macro from openssl-macros
|
||||
- 2024-08-14 Introduce ForeignTypeExt and ForeignTypeRefExt
|
||||
- 2024-08-09 Expose mTLS related APIs
|
||||
- 2024-08-14 chore(boring-sys): Fix git apply patch on Windows (#261)
|
||||
|
||||
4.9.1
|
||||
- 2024-08-04 Properly handle `Option<i32>` in `SslRef::set_curves`
|
||||
|
|
|
|||
|
|
@ -81,3 +81,6 @@ bindgen = { workspace = true }
|
|||
cmake = { workspace = true }
|
||||
fs_extra = { workspace = true }
|
||||
fslock = { workspace = true }
|
||||
|
||||
[lints.rust]
|
||||
unexpected_cfgs = { level = "allow", check-cfg = ['cfg(const_fn)'] }
|
||||
|
|
|
|||
|
|
@ -504,6 +504,14 @@ fn ensure_patches_applied(config: &Config) -> io::Result<()> {
|
|||
|
||||
fn apply_patch(config: &Config, patch_name: &str) -> io::Result<()> {
|
||||
let src_path = get_boringssl_source_path(config);
|
||||
#[cfg(not(windows))]
|
||||
let cmd_path = config
|
||||
.manifest_dir
|
||||
.join("patches")
|
||||
.join(patch_name)
|
||||
.canonicalize()?;
|
||||
|
||||
#[cfg(windows)]
|
||||
let cmd_path = config.manifest_dir.join("patches").join(patch_name);
|
||||
|
||||
let mut args = vec!["apply", "-v", "--whitespace=fix"];
|
||||
|
|
@ -683,6 +691,7 @@ fn main() {
|
|||
});
|
||||
|
||||
let mut builder = bindgen::Builder::default()
|
||||
.rust_target(bindgen::RustTarget::Stable_1_68) // bindgen MSRV is 1.70, so this is enough
|
||||
.derive_copy(true)
|
||||
.derive_debug(true)
|
||||
.derive_default(true)
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -69,6 +69,7 @@ kx-client-nist-required = ["kx-safe-default"]
|
|||
bitflags = { workspace = true }
|
||||
foreign-types = { workspace = true }
|
||||
once_cell = { workspace = true }
|
||||
openssl-macros = { workspace = true }
|
||||
libc = { workspace = true }
|
||||
boring-sys = { workspace = true }
|
||||
|
||||
|
|
|
|||
|
|
@ -280,8 +280,7 @@ impl Dsa<Public> {
|
|||
let dsa = Dsa::from_ptr(cvt_p(ffi::DSA_new())?);
|
||||
cvt(DSA_set0_pqg(dsa.0, p.as_ptr(), q.as_ptr(), g.as_ptr()))?;
|
||||
mem::forget((p, q, g));
|
||||
cvt(DSA_set0_key(dsa.0, pub_key.as_ptr(), ptr::null_mut()))?;
|
||||
mem::forget(pub_key);
|
||||
cvt(DSA_set0_key(dsa.0, pub_key.into_ptr(), ptr::null_mut()))?;
|
||||
Ok(dsa)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -74,19 +74,17 @@
|
|||
//! support by turning on `post-quantum` compilation feature.
|
||||
//!
|
||||
//! Upstream BoringSSL support the post-quantum hybrid key agreement `X25519Kyber768Draft00`. Most
|
||||
//! users should stick to that one. Enabling this feature, adds a few other post-quantum key
|
||||
//! users should stick to that one for now. Enabling this feature, adds a few other post-quantum key
|
||||
//! agreements:
|
||||
//!
|
||||
//! - `X25519MLKEM768` is the successor of `X25519Kyber768Draft00`. We expect servers to switch
|
||||
//! before the end of 2024.
|
||||
//! - `X25519Kyber768Draft00Old` is the same as `X25519Kyber768Draft00`, but under its old codepoint.
|
||||
//! - `X25519Kyber512Draft00`. Similar to `X25519Kyber768Draft00`, but uses level 1 parameter set for
|
||||
//! Kyber. Not recommended. It's useful to test whether the shorter ClientHello upsets fewer middle
|
||||
//! boxes.
|
||||
//! - `P256Kyber768Draft00`. Similar again to `X25519Kyber768Draft00`, but uses P256 as classical
|
||||
//! part. It uses a non-standard codepoint. Not recommended.
|
||||
//! - `IPDWing`. A preliminary version of
|
||||
//! [X-Wing](https://datatracker.ietf.org/doc/draft-connolly-cfrg-xwing-kem/02/).
|
||||
//! Similar to `X25519Kyber768Draft00Old`, but uses a newer (but not yet final) version of Kyber
|
||||
//! called ML-KEM-ipd. Not recommended.
|
||||
//!
|
||||
//! Presently all these key agreements are deployed by Cloudflare, but we do not guarantee continued
|
||||
//! support for them.
|
||||
|
|
|
|||
|
|
@ -72,6 +72,7 @@ pub struct Id(c_int);
|
|||
|
||||
impl Id {
|
||||
pub const RSA: Id = Id(ffi::EVP_PKEY_RSA);
|
||||
pub const RSAPSS: Id = Id(ffi::EVP_PKEY_RSA_PSS);
|
||||
pub const DSA: Id = Id(ffi::EVP_PKEY_DSA);
|
||||
pub const DH: Id = Id(ffi::EVP_PKEY_DH);
|
||||
pub const EC: Id = Id(ffi::EVP_PKEY_EC);
|
||||
|
|
@ -303,6 +304,7 @@ impl<T> fmt::Debug for PKey<T> {
|
|||
fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
|
||||
let alg = match self.id() {
|
||||
Id::RSA => "RSA",
|
||||
Id::RSAPSS => "RSAPSS",
|
||||
Id::DSA => "DSA",
|
||||
Id::DH => "DH",
|
||||
Id::EC => "EC",
|
||||
|
|
|
|||
|
|
@ -2,8 +2,9 @@
|
|||
|
||||
use super::{
|
||||
AlpnError, ClientHello, GetSessionPendingError, PrivateKeyMethod, PrivateKeyMethodError,
|
||||
SelectCertError, SniError, Ssl, SslAlert, SslContext, SslContextRef, SslRef, SslSession,
|
||||
SslSessionRef, SslSignatureAlgorithm, SslVerifyError, SESSION_CTX_INDEX,
|
||||
SelectCertError, SniError, Ssl, SslAlert, SslContext, SslContextRef, SslInfoCallbackAlert,
|
||||
SslInfoCallbackMode, SslInfoCallbackValue, SslRef, SslSession, SslSessionRef,
|
||||
SslSignatureAlgorithm, SslVerifyError, SESSION_CTX_INDEX,
|
||||
};
|
||||
use crate::error::ErrorStack;
|
||||
use crate::ffi;
|
||||
|
|
@ -521,3 +522,32 @@ where
|
|||
Err(err) => err.0,
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) unsafe extern "C" fn raw_info_callback<F>(
|
||||
ssl: *const ffi::SSL,
|
||||
mode: c_int,
|
||||
value: c_int,
|
||||
) where
|
||||
F: Fn(&SslRef, SslInfoCallbackMode, SslInfoCallbackValue) + Send + Sync + 'static,
|
||||
{
|
||||
// Due to FFI signature requirements we have to pass a *const SSL into this function, but
|
||||
// foreign-types requires a *mut SSL to get the Rust SslRef
|
||||
let mut_ref = ssl as *mut ffi::SSL;
|
||||
|
||||
// SAFETY: boring provides valid inputs.
|
||||
let ssl = unsafe { SslRef::from_ptr(mut_ref) };
|
||||
let ssl_context = ssl.ssl_context();
|
||||
|
||||
let callback = ssl_context
|
||||
.ex_data(SslContext::cached_ex_index::<F>())
|
||||
.expect("BUG: info callback missing");
|
||||
|
||||
let value = match mode {
|
||||
ffi::SSL_CB_READ_ALERT | ffi::SSL_CB_WRITE_ALERT => {
|
||||
SslInfoCallbackValue::Alert(SslInfoCallbackAlert(value))
|
||||
}
|
||||
_ => SslInfoCallbackValue::Unit,
|
||||
};
|
||||
|
||||
callback(ssl, SslInfoCallbackMode(mode), value);
|
||||
}
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -1052,3 +1052,17 @@ fn drop_ex_data_in_ssl() {
|
|||
assert_eq!(ssl.replace_ex_data(index, "camembert"), Some("comté"));
|
||||
assert_eq!(ssl.replace_ex_data(index, "raclette"), Some("camembert"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_info_callback() {
|
||||
static CALLED_BACK: AtomicBool = AtomicBool::new(false);
|
||||
|
||||
let server = Server::builder().build();
|
||||
let mut client = server.client();
|
||||
client.ctx().set_info_callback(move |_, _, _| {
|
||||
CALLED_BACK.store(true, Ordering::Relaxed);
|
||||
});
|
||||
|
||||
client.connect();
|
||||
assert!(CALLED_BACK.load(Ordering::Relaxed));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
use crate::error::ErrorStack;
|
||||
use foreign_types::{ForeignType, ForeignTypeRef};
|
||||
use libc::{c_char, c_int, c_void};
|
||||
use std::any::Any;
|
||||
use std::panic::{self, AssertUnwindSafe};
|
||||
use std::slice;
|
||||
|
||||
use crate::error::ErrorStack;
|
||||
|
||||
/// Wraps a user-supplied callback and a slot for panics thrown inside the callback (while FFI
|
||||
/// frames are on the stack).
|
||||
///
|
||||
|
|
@ -65,3 +65,30 @@ where
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub trait ForeignTypeExt: ForeignType {
|
||||
unsafe fn from_ptr_opt(ptr: *mut Self::CType) -> Option<Self> {
|
||||
if ptr.is_null() {
|
||||
None
|
||||
} else {
|
||||
Some(Self::from_ptr(ptr))
|
||||
}
|
||||
}
|
||||
}
|
||||
impl<FT: ForeignType> ForeignTypeExt for FT {}
|
||||
|
||||
pub trait ForeignTypeRefExt: ForeignTypeRef {
|
||||
unsafe fn from_const_ptr<'a>(ptr: *const Self::CType) -> &'a Self {
|
||||
Self::from_ptr(ptr as *mut Self::CType)
|
||||
}
|
||||
|
||||
unsafe fn from_const_ptr_opt<'a>(ptr: *const Self::CType) -> Option<&'a Self> {
|
||||
if ptr.is_null() {
|
||||
None
|
||||
} else {
|
||||
Some(Self::from_const_ptr(ptr as *mut Self::CType))
|
||||
}
|
||||
}
|
||||
}
|
||||
impl<FT: ForeignTypeRef> ForeignTypeRefExt for FT {}
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@
|
|||
|
||||
use foreign_types::{ForeignType, ForeignTypeRef};
|
||||
use libc::{c_int, c_long, c_void};
|
||||
use openssl_macros::corresponds;
|
||||
use std::convert::TryInto;
|
||||
use std::error::Error;
|
||||
use std::ffi::{CStr, CString};
|
||||
|
|
@ -25,7 +26,7 @@ use crate::asn1::{
|
|||
Asn1BitStringRef, Asn1IntegerRef, Asn1Object, Asn1ObjectRef, Asn1StringRef, Asn1TimeRef,
|
||||
Asn1Type,
|
||||
};
|
||||
use crate::bio::MemBioSlice;
|
||||
use crate::bio::{MemBio, MemBioSlice};
|
||||
use crate::conf::ConfRef;
|
||||
use crate::error::ErrorStack;
|
||||
use crate::ex_data::Index;
|
||||
|
|
@ -36,6 +37,7 @@ use crate::pkey::{HasPrivate, HasPublic, PKey, PKeyRef, Public};
|
|||
use crate::ssl::SslRef;
|
||||
use crate::stack::{Stack, StackRef, Stackable};
|
||||
use crate::string::OpensslString;
|
||||
use crate::util::ForeignTypeRefExt;
|
||||
use crate::x509::verify::X509VerifyParamRef;
|
||||
use crate::{cvt, cvt_n, cvt_p};
|
||||
|
||||
|
|
@ -57,15 +59,13 @@ foreign_type_and_impl_send_sync! {
|
|||
impl X509StoreContext {
|
||||
/// Returns the index which can be used to obtain a reference to the `Ssl` associated with a
|
||||
/// context.
|
||||
#[corresponds(SSL_get_ex_data_X509_STORE_CTX_idx)]
|
||||
pub fn ssl_idx() -> Result<Index<X509StoreContext, SslRef>, ErrorStack> {
|
||||
unsafe { cvt_n(ffi::SSL_get_ex_data_X509_STORE_CTX_idx()).map(|idx| Index::from_raw(idx)) }
|
||||
}
|
||||
|
||||
/// Creates a new `X509StoreContext` instance.
|
||||
///
|
||||
/// This corresponds to [`X509_STORE_CTX_new`].
|
||||
///
|
||||
/// [`X509_STORE_CTX_new`]: https://www.openssl.org/docs/man1.1.0/crypto/X509_STORE_CTX_new.html
|
||||
#[corresponds(X509_STORE_CTX_new)]
|
||||
pub fn new() -> Result<X509StoreContext, ErrorStack> {
|
||||
unsafe {
|
||||
ffi::init();
|
||||
|
|
@ -76,10 +76,7 @@ impl X509StoreContext {
|
|||
|
||||
impl X509StoreContextRef {
|
||||
/// Returns application data pertaining to an `X509` store context.
|
||||
///
|
||||
/// This corresponds to [`X509_STORE_CTX_get_ex_data`].
|
||||
///
|
||||
/// [`X509_STORE_CTX_get_ex_data`]: https://www.openssl.org/docs/man1.0.2/crypto/X509_STORE_CTX_get_ex_data.html
|
||||
#[corresponds(X509_STORE_CTX_get_ex_data)]
|
||||
pub fn ex_data<T>(&self, index: Index<X509StoreContext, T>) -> Option<&T> {
|
||||
unsafe {
|
||||
let data = ffi::X509_STORE_CTX_get_ex_data(self.as_ptr(), index.as_raw());
|
||||
|
|
@ -92,10 +89,7 @@ impl X509StoreContextRef {
|
|||
}
|
||||
|
||||
/// Returns the verify result of the context.
|
||||
///
|
||||
/// This corresponds to [`X509_STORE_CTX_get_error`].
|
||||
///
|
||||
/// [`X509_STORE_CTX_get_error`]: https://www.openssl.org/docs/man1.1.0/crypto/X509_STORE_CTX_get_error.html
|
||||
#[corresponds(X509_STORE_CTX_get_error)]
|
||||
pub fn verify_result(&self) -> X509VerifyResult {
|
||||
unsafe { X509VerifyError::from_raw(ffi::X509_STORE_CTX_get_error(self.as_ptr())) }
|
||||
}
|
||||
|
|
@ -149,10 +143,7 @@ impl X509StoreContextRef {
|
|||
}
|
||||
|
||||
/// Returns a mutable reference to the X509 verification configuration.
|
||||
///
|
||||
/// This corresponds to [`X509_STORE_CTX_get0_param`].
|
||||
///
|
||||
/// [`SSL_get0_param`]: https://www.openssl.org/docs/manmaster/man3/X509_STORE_CTX_get0_param.html
|
||||
#[corresponds(X509_STORE_CTX_get0_param)]
|
||||
pub fn verify_param_mut(&mut self) -> &mut X509VerifyParamRef {
|
||||
unsafe { X509VerifyParamRef::from_ptr_mut(ffi::X509_STORE_CTX_get0_param(self.as_ptr())) }
|
||||
}
|
||||
|
|
@ -163,19 +154,13 @@ impl X509StoreContextRef {
|
|||
/// validation error if the certificate was not valid.
|
||||
///
|
||||
/// This will only work inside of a call to `init`.
|
||||
///
|
||||
/// This corresponds to [`X509_verify_cert`].
|
||||
///
|
||||
/// [`X509_verify_cert`]: https://www.openssl.org/docs/man1.0.2/crypto/X509_verify_cert.html
|
||||
#[corresponds(X509_verify_cert)]
|
||||
pub fn verify_cert(&mut self) -> Result<bool, ErrorStack> {
|
||||
unsafe { cvt_n(ffi::X509_verify_cert(self.as_ptr())).map(|n| n != 0) }
|
||||
}
|
||||
|
||||
/// Set the verify result of the context.
|
||||
///
|
||||
/// This corresponds to [`X509_STORE_CTX_set_error`].
|
||||
///
|
||||
/// [`X509_STORE_CTX_set_error`]: https://www.openssl.org/docs/man1.1.0/crypto/X509_STORE_CTX_set_error.html
|
||||
#[corresponds(X509_STORE_CTX_set_error)]
|
||||
pub fn set_error(&mut self, result: X509VerifyResult) {
|
||||
unsafe {
|
||||
ffi::X509_STORE_CTX_set_error(
|
||||
|
|
@ -190,10 +175,7 @@ impl X509StoreContextRef {
|
|||
|
||||
/// Returns a reference to the certificate which caused the error or None if
|
||||
/// no certificate is relevant to the error.
|
||||
///
|
||||
/// This corresponds to [`X509_STORE_CTX_get_current_cert`].
|
||||
///
|
||||
/// [`X509_STORE_CTX_get_current_cert`]: https://www.openssl.org/docs/man1.1.0/crypto/X509_STORE_CTX_get_current_cert.html
|
||||
#[corresponds(X509_STORE_CTX_get_current_cert)]
|
||||
pub fn current_cert(&self) -> Option<&X509Ref> {
|
||||
unsafe {
|
||||
let ptr = ffi::X509_STORE_CTX_get_current_cert(self.as_ptr());
|
||||
|
|
@ -209,19 +191,13 @@ impl X509StoreContextRef {
|
|||
/// chain where the error occurred. If it is zero it occurred in the end
|
||||
/// entity certificate, one if it is the certificate which signed the end
|
||||
/// entity certificate and so on.
|
||||
///
|
||||
/// This corresponds to [`X509_STORE_CTX_get_error_depth`].
|
||||
///
|
||||
/// [`X509_STORE_CTX_get_error_depth`]: https://www.openssl.org/docs/man1.1.0/crypto/X509_STORE_CTX_get_error_depth.html
|
||||
#[corresponds(X509_STORE_CTX_get_error_depth)]
|
||||
pub fn error_depth(&self) -> u32 {
|
||||
unsafe { ffi::X509_STORE_CTX_get_error_depth(self.as_ptr()) as u32 }
|
||||
}
|
||||
|
||||
/// Returns a reference to a complete valid `X509` certificate chain.
|
||||
///
|
||||
/// This corresponds to [`X509_STORE_CTX_get0_chain`].
|
||||
///
|
||||
/// [`X509_STORE_CTX_get0_chain`]: https://www.openssl.org/docs/man1.1.0/crypto/X509_STORE_CTX_get0_chain.html
|
||||
#[corresponds(X509_STORE_CTX_get0_chain)]
|
||||
pub fn chain(&self) -> Option<&StackRef<X509>> {
|
||||
unsafe {
|
||||
let chain = X509_STORE_CTX_get0_chain(self.as_ptr());
|
||||
|
|
@ -240,6 +216,7 @@ pub struct X509Builder(X509);
|
|||
|
||||
impl X509Builder {
|
||||
/// Creates a new builder.
|
||||
#[corresponds(X509_new)]
|
||||
pub fn new() -> Result<X509Builder, ErrorStack> {
|
||||
unsafe {
|
||||
ffi::init();
|
||||
|
|
@ -248,11 +225,13 @@ impl X509Builder {
|
|||
}
|
||||
|
||||
/// Sets the notAfter constraint on the certificate.
|
||||
#[corresponds(X509_set1_notAfter)]
|
||||
pub fn set_not_after(&mut self, not_after: &Asn1TimeRef) -> Result<(), ErrorStack> {
|
||||
unsafe { cvt(X509_set1_notAfter(self.0.as_ptr(), not_after.as_ptr())).map(|_| ()) }
|
||||
}
|
||||
|
||||
/// Sets the notBefore constraint on the certificate.
|
||||
#[corresponds(X509_set1_notBefore)]
|
||||
pub fn set_not_before(&mut self, not_before: &Asn1TimeRef) -> Result<(), ErrorStack> {
|
||||
unsafe { cvt(X509_set1_notBefore(self.0.as_ptr(), not_before.as_ptr())).map(|_| ()) }
|
||||
}
|
||||
|
|
@ -261,11 +240,13 @@ impl X509Builder {
|
|||
///
|
||||
/// Note that the version is zero-indexed; that is, a certificate corresponding to version 3 of
|
||||
/// the X.509 standard should pass `2` to this method.
|
||||
#[corresponds(X509_set_version)]
|
||||
pub fn set_version(&mut self, version: i32) -> Result<(), ErrorStack> {
|
||||
unsafe { cvt(ffi::X509_set_version(self.0.as_ptr(), version.into())).map(|_| ()) }
|
||||
}
|
||||
|
||||
/// Sets the serial number of the certificate.
|
||||
#[corresponds(X509_set_serialNumber)]
|
||||
pub fn set_serial_number(&mut self, serial_number: &Asn1IntegerRef) -> Result<(), ErrorStack> {
|
||||
unsafe {
|
||||
cvt(ffi::X509_set_serialNumber(
|
||||
|
|
@ -277,6 +258,7 @@ impl X509Builder {
|
|||
}
|
||||
|
||||
/// Sets the issuer name of the certificate.
|
||||
#[corresponds(X509_set_issuer_name)]
|
||||
pub fn set_issuer_name(&mut self, issuer_name: &X509NameRef) -> Result<(), ErrorStack> {
|
||||
unsafe {
|
||||
cvt(ffi::X509_set_issuer_name(
|
||||
|
|
@ -305,6 +287,7 @@ impl X509Builder {
|
|||
/// let mut x509 = boring::x509::X509::builder().unwrap();
|
||||
/// x509.set_subject_name(&x509_name).unwrap();
|
||||
/// ```
|
||||
#[corresponds(X509_set_subject_name)]
|
||||
pub fn set_subject_name(&mut self, subject_name: &X509NameRef) -> Result<(), ErrorStack> {
|
||||
unsafe {
|
||||
cvt(ffi::X509_set_subject_name(
|
||||
|
|
@ -316,6 +299,7 @@ impl X509Builder {
|
|||
}
|
||||
|
||||
/// Sets the public key associated with the certificate.
|
||||
#[corresponds(X509_set_pubkey)]
|
||||
pub fn set_pubkey<T>(&mut self, key: &PKeyRef<T>) -> Result<(), ErrorStack>
|
||||
where
|
||||
T: HasPublic,
|
||||
|
|
@ -326,6 +310,7 @@ impl X509Builder {
|
|||
/// Returns a context object which is needed to create certain X509 extension values.
|
||||
///
|
||||
/// Set `issuer` to `None` if the certificate will be self-signed.
|
||||
#[corresponds(X509V3_set_ctx)]
|
||||
pub fn x509v3_context<'a>(
|
||||
&'a self,
|
||||
issuer: Option<&'a X509Ref>,
|
||||
|
|
@ -365,10 +350,7 @@ impl X509Builder {
|
|||
}
|
||||
|
||||
/// Adds an X509 extension value to the certificate.
|
||||
///
|
||||
/// This corresponds to [`X509_add_ext`].
|
||||
///
|
||||
/// [`X509_add_ext`]: https://www.openssl.org/docs/man1.1.0/man3/X509_get_ext.html
|
||||
#[corresponds(X509_add_ext)]
|
||||
pub fn append_extension2(&mut self, extension: &X509ExtensionRef) -> Result<(), ErrorStack> {
|
||||
unsafe {
|
||||
cvt(ffi::X509_add_ext(self.0.as_ptr(), extension.as_ptr(), -1))?;
|
||||
|
|
@ -377,6 +359,7 @@ impl X509Builder {
|
|||
}
|
||||
|
||||
/// Signs the certificate with a private key.
|
||||
#[corresponds(X509_sign)]
|
||||
pub fn sign<T>(&mut self, key: &PKeyRef<T>, hash: MessageDigest) -> Result<(), ErrorStack>
|
||||
where
|
||||
T: HasPrivate,
|
||||
|
|
@ -400,43 +383,22 @@ foreign_type_and_impl_send_sync! {
|
|||
|
||||
impl X509Ref {
|
||||
/// Returns this certificate's subject name.
|
||||
///
|
||||
/// This corresponds to [`X509_get_subject_name`].
|
||||
///
|
||||
/// [`X509_get_subject_name`]: https://www.openssl.org/docs/man1.1.0/crypto/X509_get_subject_name.html
|
||||
#[corresponds(X509_get_subject_name)]
|
||||
pub fn subject_name(&self) -> &X509NameRef {
|
||||
unsafe {
|
||||
let name = ffi::X509_get_subject_name(self.as_ptr());
|
||||
assert!(!name.is_null());
|
||||
X509NameRef::from_ptr(name)
|
||||
X509NameRef::from_const_ptr_opt(name).expect("issuer name must not be null")
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the hash of the certificates subject
|
||||
///
|
||||
/// This corresponds to `X509_subject_name_hash`.
|
||||
#[corresponds(X509_subject_name_hash)]
|
||||
pub fn subject_name_hash(&self) -> u32 {
|
||||
unsafe { ffi::X509_subject_name_hash(self.as_ptr()) as u32 }
|
||||
}
|
||||
|
||||
/// Returns this certificate's issuer name.
|
||||
///
|
||||
/// This corresponds to [`X509_get_issuer_name`].
|
||||
///
|
||||
/// [`X509_get_issuer_name`]: https://www.openssl.org/docs/man1.1.0/crypto/X509_get_subject_name.html
|
||||
pub fn issuer_name(&self) -> &X509NameRef {
|
||||
unsafe {
|
||||
let name = ffi::X509_get_issuer_name(self.as_ptr());
|
||||
assert!(!name.is_null());
|
||||
X509NameRef::from_ptr(name)
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns this certificate's subject alternative name entries, if they exist.
|
||||
///
|
||||
/// This corresponds to [`X509_get_ext_d2i`] called with `NID_subject_alt_name`.
|
||||
///
|
||||
/// [`X509_get_ext_d2i`]: https://www.openssl.org/docs/man1.1.0/crypto/X509_get_ext_d2i.html
|
||||
#[corresponds(X509_get_ext_d2i)]
|
||||
pub fn subject_alt_names(&self) -> Option<Stack<GeneralName>> {
|
||||
unsafe {
|
||||
let stack = ffi::X509_get_ext_d2i(
|
||||
|
|
@ -453,11 +415,17 @@ impl X509Ref {
|
|||
}
|
||||
}
|
||||
|
||||
/// Returns this certificate's issuer name.
|
||||
#[corresponds(X509_get_issuer_name)]
|
||||
pub fn issuer_name(&self) -> &X509NameRef {
|
||||
unsafe {
|
||||
let name = ffi::X509_get_issuer_name(self.as_ptr());
|
||||
X509NameRef::from_const_ptr_opt(name).expect("issuer name must not be null")
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns this certificate's issuer alternative name entries, if they exist.
|
||||
///
|
||||
/// This corresponds to [`X509_get_ext_d2i`] called with `NID_issuer_alt_name`.
|
||||
///
|
||||
/// [`X509_get_ext_d2i`]: https://www.openssl.org/docs/man1.1.0/crypto/X509_get_ext_d2i.html
|
||||
#[corresponds(X509_get_ext_d2i)]
|
||||
pub fn issuer_alt_names(&self) -> Option<Stack<GeneralName>> {
|
||||
unsafe {
|
||||
let stack = ffi::X509_get_ext_d2i(
|
||||
|
|
@ -474,6 +442,24 @@ impl X509Ref {
|
|||
}
|
||||
}
|
||||
|
||||
/// Returns this certificate's subject key id, if it exists.
|
||||
#[corresponds(X509_get0_subject_key_id)]
|
||||
pub fn subject_key_id(&self) -> Option<&Asn1StringRef> {
|
||||
unsafe {
|
||||
let data = ffi::X509_get0_subject_key_id(self.as_ptr());
|
||||
Asn1StringRef::from_const_ptr_opt(data)
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns this certificate's authority key id, if it exists.
|
||||
#[corresponds(X509_get0_authority_key_id)]
|
||||
pub fn authority_key_id(&self) -> Option<&Asn1StringRef> {
|
||||
unsafe {
|
||||
let data = ffi::X509_get0_authority_key_id(self.as_ptr());
|
||||
Asn1StringRef::from_const_ptr_opt(data)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn public_key(&self) -> Result<PKey<Public>, ErrorStack> {
|
||||
unsafe {
|
||||
let pkey = cvt_p(ffi::X509_get_pubkey(self.as_ptr()))?;
|
||||
|
|
@ -1058,6 +1044,20 @@ impl X509NameRef {
|
|||
}
|
||||
}
|
||||
|
||||
/// Returns an owned String representing the X509 name configurable via incoming flags.
|
||||
///
|
||||
/// This function will return `None` if the underlying string contains invalid utf-8.
|
||||
#[corresponds(X509_NAME_print_ex)]
|
||||
pub fn print_ex(&self, flags: i32) -> Option<String> {
|
||||
unsafe {
|
||||
let bio = MemBio::new().ok()?;
|
||||
ffi::X509_NAME_print_ex(bio.as_ptr(), self.as_ptr(), 0, flags as _);
|
||||
let buf = bio.get_buf().to_vec();
|
||||
let res = String::from_utf8(buf);
|
||||
res.ok()
|
||||
}
|
||||
}
|
||||
|
||||
to_der! {
|
||||
/// Serializes the certificate into a DER-encoded X509 name structure.
|
||||
///
|
||||
|
|
@ -1584,9 +1584,7 @@ impl GeneralName {
|
|||
ffi::init();
|
||||
let gn = cvt_p(ffi::GENERAL_NAME_new())?;
|
||||
(*gn).type_ = ffi::GEN_RID;
|
||||
(*gn).d.registeredID = oid.as_ptr();
|
||||
|
||||
mem::forget(oid);
|
||||
(*gn).d.registeredID = oid.into_ptr();
|
||||
|
||||
Ok(GeneralName::from_ptr(gn))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -179,6 +179,49 @@ fn test_subject_alt_name_iter() {
|
|||
assert!(subject_alt_names_iter.next().is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_subject_key_id() {
|
||||
// nid_test_cert_pem has SKI, but no AKI
|
||||
let cert = include_bytes!("../../../test/nid_test_cert.pem");
|
||||
let cert = X509::from_pem(cert).unwrap();
|
||||
|
||||
let ski = cert.subject_key_id().expect("unable to extract SKI");
|
||||
assert_eq!(
|
||||
ski.as_slice(),
|
||||
[
|
||||
80, 107, 158, 237, 95, 61, 235, 100, 212, 115, 249, 244, 219, 163, 124, 55, 141, 2, 76,
|
||||
5
|
||||
]
|
||||
);
|
||||
|
||||
let aki = cert.authority_key_id();
|
||||
assert!(aki.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_x509_name_print_ex() {
|
||||
let cert = include_bytes!("../../../test/cert.pem");
|
||||
let cert = X509::from_pem(cert).unwrap();
|
||||
|
||||
let name_no_flags = cert
|
||||
.subject_name()
|
||||
.print_ex(0)
|
||||
.expect("failed to print cert subject name");
|
||||
assert_eq!(
|
||||
name_no_flags,
|
||||
"C=AU, ST=Some-State, O=Internet Widgits Pty Ltd, CN=foobar.com"
|
||||
);
|
||||
|
||||
let name_rfc2253 = cert
|
||||
.subject_name()
|
||||
.print_ex(ffi::XN_FLAG_RFC2253)
|
||||
.expect("failed to print cert subject name");
|
||||
assert_eq!(
|
||||
name_rfc2253,
|
||||
"CN=foobar.com,O=Internet Widgits Pty Ltd,ST=Some-State,C=AU"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn x509_builder() {
|
||||
let pkey = pkey();
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ rustdoc-args = ["--cfg", "docsrs"]
|
|||
[features]
|
||||
default = ["runtime"]
|
||||
|
||||
runtime = ["hyper/runtime"]
|
||||
runtime = ["hyper_old/runtime"]
|
||||
|
||||
# Use a FIPS-validated version of boringssl.
|
||||
fips = ["tokio-boring/fips"]
|
||||
|
|
@ -28,19 +28,30 @@ fips-link-precompiled = ["tokio-boring/fips-link-precompiled"]
|
|||
# Enables experimental post-quantum crypto (https://blog.cloudflare.com/post-quantum-for-all/)
|
||||
pq-experimental = ["tokio-boring/pq-experimental"]
|
||||
|
||||
# Enable Hyper 1 support
|
||||
hyper1 = ["dep:http", "dep:hyper", "dep:hyper-util", "dep:tower-service"]
|
||||
|
||||
[dependencies]
|
||||
antidote = { workspace = true }
|
||||
http = { workspace = true }
|
||||
hyper = { workspace = true, features = ["client"] }
|
||||
http = { workspace = true, optional = true }
|
||||
http_old = { workspace = true }
|
||||
hyper = { workspace = true, optional = true }
|
||||
hyper-util = { workspace = true, optional = true, features = ["client", "client-legacy"] }
|
||||
hyper_old = { workspace = true, features = ["client"] }
|
||||
linked_hash_set = { workspace = true }
|
||||
once_cell = { workspace = true }
|
||||
boring = { workspace = true }
|
||||
tokio = { workspace = true }
|
||||
tokio-boring = { workspace = true }
|
||||
tower-layer = { workspace = true }
|
||||
tower-service = { workspace = true, optional = true }
|
||||
|
||||
[dev-dependencies]
|
||||
hyper = { workspace = true, features = [ "full" ] }
|
||||
bytes = { workspace = true }
|
||||
http-body-util = { workspace = true }
|
||||
hyper-util = { workspace = true, features = ["http1", "http2", "service", "tokio"] }
|
||||
hyper = { workspace = true, features = ["server"] }
|
||||
hyper_old = { workspace = true, features = [ "full" ] }
|
||||
tokio = { workspace = true, features = [ "full" ] }
|
||||
tower = { workspace = true, features = ["util"] }
|
||||
futures = { workspace = true }
|
||||
|
|
|
|||
|
|
@ -2,91 +2,27 @@
|
|||
#![warn(missing_docs)]
|
||||
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
|
||||
|
||||
use crate::cache::{SessionCache, SessionKey};
|
||||
use antidote::Mutex;
|
||||
use crate::cache::SessionKey;
|
||||
use boring::error::ErrorStack;
|
||||
use boring::ex_data::Index;
|
||||
use boring::ssl::{
|
||||
ConnectConfiguration, Ssl, SslConnector, SslConnectorBuilder, SslMethod, SslRef,
|
||||
SslSessionCacheMode,
|
||||
};
|
||||
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 boring::ssl::Ssl;
|
||||
use once_cell::sync::OnceCell;
|
||||
use std::fmt::Debug;
|
||||
use std::future::Future;
|
||||
use std::io;
|
||||
use std::net;
|
||||
use std::pin::Pin;
|
||||
use std::sync::Arc;
|
||||
use std::task::{Context, Poll};
|
||||
use std::{error::Error, fmt};
|
||||
use tokio::io::{AsyncRead, AsyncWrite, ReadBuf};
|
||||
use std::fmt;
|
||||
use tokio_boring::SslStream;
|
||||
use tower_layer::Layer;
|
||||
|
||||
mod cache;
|
||||
#[cfg(test)]
|
||||
mod test;
|
||||
mod v0;
|
||||
/// Hyper 1 support.
|
||||
#[cfg(feature = "hyper1")]
|
||||
pub mod v1;
|
||||
|
||||
pub use self::v0::*;
|
||||
|
||||
fn key_index() -> Result<Index<Ssl, SessionKey>, ErrorStack> {
|
||||
static IDX: OnceCell<Index<Ssl, SessionKey>> = OnceCell::new();
|
||||
IDX.get_or_try_init(Ssl::new_ex_index).copied()
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct Inner {
|
||||
ssl: SslConnector,
|
||||
cache: Arc<Mutex<SessionCache>>,
|
||||
callback: Option<Callback>,
|
||||
ssl_callback: Option<SslCallback>,
|
||||
}
|
||||
|
||||
type Callback =
|
||||
Arc<dyn Fn(&mut ConnectConfiguration, &Uri) -> Result<(), ErrorStack> + Sync + Send>;
|
||||
type SslCallback = Arc<dyn Fn(&mut SslRef, &Uri) -> Result<(), ErrorStack> + Sync + Send>;
|
||||
|
||||
impl Inner {
|
||||
fn setup_ssl(&self, uri: &Uri, host: &str) -> Result<Ssl, ErrorStack> {
|
||||
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);
|
||||
|
||||
let mut ssl = conf.into_ssl(host)?;
|
||||
|
||||
if let Some(ref ssl_callback) = self.ssl_callback {
|
||||
ssl_callback(&mut ssl, uri)?;
|
||||
}
|
||||
|
||||
Ok(ssl)
|
||||
}
|
||||
}
|
||||
|
||||
/// A layer which wraps services in an `HttpsConnector`.
|
||||
pub struct HttpsLayer {
|
||||
inner: Inner,
|
||||
}
|
||||
|
||||
/// Settings for [`HttpsLayer`]
|
||||
pub struct HttpsLayerSettings {
|
||||
session_cache_capacity: usize,
|
||||
|
|
@ -123,214 +59,6 @@ impl HttpsLayerSettingsBuilder {
|
|||
}
|
||||
}
|
||||
|
||||
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<HttpsLayer, ErrorStack> {
|
||||
let mut ssl = SslConnector::builder(SslMethod::tls())?;
|
||||
|
||||
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(ssl: SslConnectorBuilder) -> Result<HttpsLayer, ErrorStack> {
|
||||
Self::with_connector_and_settings(ssl, Default::default())
|
||||
}
|
||||
|
||||
/// Creates a new `HttpsLayer` with settings
|
||||
pub fn with_connector_and_settings(
|
||||
mut ssl: SslConnectorBuilder,
|
||||
settings: HttpsLayerSettings,
|
||||
) -> Result<HttpsLayer, ErrorStack> {
|
||||
let cache = Arc::new(Mutex::new(SessionCache::with_capacity(
|
||||
settings.session_cache_capacity,
|
||||
)));
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Ok(HttpsLayer {
|
||||
inner: Inner {
|
||||
ssl: ssl.build(),
|
||||
cache,
|
||||
callback: None,
|
||||
ssl_callback: None,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
/// Registers a callback which can customize the configuration of each connection.
|
||||
///
|
||||
/// Unsuitable to change verify hostflags (with `config.param_mut().set_hostflags(…)`),
|
||||
/// as they are reset after the callback is executed. Use [`Self::set_ssl_callback`]
|
||||
/// instead.
|
||||
pub fn set_callback<F>(&mut self, callback: F)
|
||||
where
|
||||
F: Fn(&mut ConnectConfiguration, &Uri) -> Result<(), ErrorStack> + 'static + Sync + Send,
|
||||
{
|
||||
self.inner.callback = Some(Arc::new(callback));
|
||||
}
|
||||
|
||||
/// Registers a callback which can customize the `Ssl` of each connection.
|
||||
pub fn set_ssl_callback<F>(&mut self, callback: F)
|
||||
where
|
||||
F: Fn(&mut SslRef, &Uri) -> Result<(), ErrorStack> + 'static + Sync + Send,
|
||||
{
|
||||
self.inner.ssl_callback = Some(Arc::new(callback));
|
||||
}
|
||||
}
|
||||
|
||||
impl<S> Layer<S> for HttpsLayer {
|
||||
type Service = HttpsConnector<S>;
|
||||
|
||||
fn layer(&self, inner: S) -> HttpsConnector<S> {
|
||||
HttpsConnector {
|
||||
http: inner,
|
||||
inner: self.inner.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A Connector using OpenSSL to support `http` and `https` schemes.
|
||||
#[derive(Clone)]
|
||||
pub struct HttpsConnector<T> {
|
||||
http: T,
|
||||
inner: Inner,
|
||||
}
|
||||
|
||||
#[cfg(feature = "runtime")]
|
||||
impl HttpsConnector<HttpConnector> {
|
||||
/// 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<HttpsConnector<HttpConnector>, ErrorStack> {
|
||||
let mut http = HttpConnector::new();
|
||||
http.enforce_http(false);
|
||||
|
||||
HttpsLayer::new().map(|l| l.layer(http))
|
||||
}
|
||||
}
|
||||
|
||||
impl<S, T> HttpsConnector<S>
|
||||
where
|
||||
S: Service<Uri, Response = T> + Send,
|
||||
S::Error: Into<Box<dyn Error + Send + Sync>>,
|
||||
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<HttpsConnector<S>, ErrorStack> {
|
||||
HttpsLayer::with_connector(ssl).map(|l| l.layer(http))
|
||||
}
|
||||
|
||||
/// Registers a callback which can customize the configuration of each connection.
|
||||
///
|
||||
/// Unsuitable to change verify hostflags (with `config.param_mut().set_hostflags(…)`),
|
||||
/// as they are reset after the callback is executed. Use [`Self::set_ssl_callback`]
|
||||
/// instead.
|
||||
pub fn set_callback<F>(&mut self, callback: F)
|
||||
where
|
||||
F: Fn(&mut ConnectConfiguration, &Uri) -> Result<(), ErrorStack> + 'static + Sync + Send,
|
||||
{
|
||||
self.inner.callback = Some(Arc::new(callback));
|
||||
}
|
||||
|
||||
/// Registers a callback which can customize the `Ssl` of each connection.
|
||||
pub fn set_ssl_callback<F>(&mut self, callback: F)
|
||||
where
|
||||
F: Fn(&mut SslRef, &Uri) -> Result<(), ErrorStack> + 'static + Sync + Send,
|
||||
{
|
||||
self.inner.ssl_callback = Some(Arc::new(callback));
|
||||
}
|
||||
}
|
||||
|
||||
impl<S> Service<Uri> for HttpsConnector<S>
|
||||
where
|
||||
S: Service<Uri> + Send,
|
||||
S::Error: Into<Box<dyn Error + Send + Sync>>,
|
||||
S::Future: Unpin + Send + 'static,
|
||||
S::Response: AsyncRead + AsyncWrite + Connection + Unpin + Debug + Sync + Send + 'static,
|
||||
{
|
||||
type Response = MaybeHttpsStream<S::Response>;
|
||||
type Error = Box<dyn Error + Sync + Send>;
|
||||
#[allow(clippy::type_complexity)]
|
||||
type Future = Pin<Box<dyn Future<Output = Result<Self::Response, Self::Error>> + Send>>;
|
||||
|
||||
fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
|
||||
self.http.poll_ready(cx).map_err(Into::into)
|
||||
}
|
||||
|
||||
fn call(&mut self, uri: Uri) -> Self::Future {
|
||||
let is_tls_scheme = uri
|
||||
.scheme()
|
||||
.map(|s| s == &Scheme::HTTPS || s.as_str() == "wss")
|
||||
.unwrap_or(false);
|
||||
|
||||
let tls_setup = if is_tls_scheme {
|
||||
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 mut host = uri.host().ok_or("URI missing host")?;
|
||||
|
||||
// If `host` is an IPv6 address, we must strip away the square brackets that surround
|
||||
// it (otherwise, boring will fail to parse the host as an IP address, eventually
|
||||
// causing the handshake to fail due a hostname verification error).
|
||||
if !host.is_empty() {
|
||||
let last = host.len() - 1;
|
||||
let mut chars = host.chars();
|
||||
|
||||
if let (Some('['), Some(']')) = (chars.next(), chars.last()) {
|
||||
if host[1..last].parse::<net::Ipv6Addr>().is_ok() {
|
||||
host = &host[1..last];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let ssl = inner.setup_ssl(&uri, host)?;
|
||||
let stream = tokio_boring::SslStreamBuilder::new(ssl, conn)
|
||||
.connect()
|
||||
.await?;
|
||||
|
||||
Ok(MaybeHttpsStream::Https(stream))
|
||||
};
|
||||
|
||||
Box::pin(f)
|
||||
}
|
||||
}
|
||||
|
||||
/// A stream which may be wrapped with TLS.
|
||||
pub enum MaybeHttpsStream<T> {
|
||||
/// A raw HTTP stream.
|
||||
|
|
@ -339,72 +67,6 @@ pub enum MaybeHttpsStream<T> {
|
|||
Https(SslStream<T>),
|
||||
}
|
||||
|
||||
impl<T> AsyncRead for MaybeHttpsStream<T>
|
||||
where
|
||||
T: AsyncRead + AsyncWrite + Unpin,
|
||||
{
|
||||
fn poll_read(
|
||||
mut self: Pin<&mut Self>,
|
||||
ctx: &mut Context<'_>,
|
||||
buf: &mut ReadBuf,
|
||||
) -> Poll<io::Result<()>> {
|
||||
match &mut *self {
|
||||
MaybeHttpsStream::Http(s) => Pin::new(s).poll_read(ctx, buf),
|
||||
MaybeHttpsStream::Https(s) => Pin::new(s).poll_read(ctx, buf),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> AsyncWrite for MaybeHttpsStream<T>
|
||||
where
|
||||
T: AsyncRead + AsyncWrite + Unpin,
|
||||
{
|
||||
fn poll_write(
|
||||
mut self: Pin<&mut Self>,
|
||||
ctx: &mut Context<'_>,
|
||||
buf: &[u8],
|
||||
) -> Poll<io::Result<usize>> {
|
||||
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<io::Result<()>> {
|
||||
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<io::Result<()>> {
|
||||
match &mut *self {
|
||||
MaybeHttpsStream::Http(s) => Pin::new(s).poll_shutdown(ctx),
|
||||
MaybeHttpsStream::Https(s) => Pin::new(s).poll_shutdown(ctx),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Connection for MaybeHttpsStream<T>
|
||||
where
|
||||
T: Connection,
|
||||
{
|
||||
fn connected(&self) -> Connected {
|
||||
match self {
|
||||
MaybeHttpsStream::Http(s) => s.connected(),
|
||||
MaybeHttpsStream::Https(s) => {
|
||||
let mut connected = s.get_ref().connected();
|
||||
|
||||
if s.ssl().selected_alpn_protocol() == Some(b"h2") {
|
||||
connected = connected.negotiated_h2();
|
||||
}
|
||||
|
||||
connected
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> fmt::Debug for MaybeHttpsStream<T> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
match *self {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,345 @@
|
|||
use crate::cache::{SessionCache, SessionKey};
|
||||
use crate::{key_index, HttpsLayerSettings, MaybeHttpsStream};
|
||||
use antidote::Mutex;
|
||||
use boring::error::ErrorStack;
|
||||
use boring::ssl::{
|
||||
ConnectConfiguration, Ssl, SslConnector, SslConnectorBuilder, SslMethod, SslRef,
|
||||
SslSessionCacheMode,
|
||||
};
|
||||
use http_old::uri::Scheme;
|
||||
use hyper_old::client::connect::{Connected, Connection};
|
||||
use hyper_old::client::HttpConnector;
|
||||
use hyper_old::service::Service;
|
||||
use hyper_old::Uri;
|
||||
use std::error::Error;
|
||||
use std::future::Future;
|
||||
use std::net;
|
||||
use std::pin::Pin;
|
||||
use std::sync::Arc;
|
||||
use std::task::{Context, Poll};
|
||||
use std::{fmt, io};
|
||||
use tokio::io::{AsyncRead, AsyncWrite, ReadBuf};
|
||||
use tower_layer::Layer;
|
||||
|
||||
/// A Connector using OpenSSL to support `http` and `https` schemes.
|
||||
#[derive(Clone)]
|
||||
pub struct HttpsConnector<T> {
|
||||
http: T,
|
||||
inner: Inner,
|
||||
}
|
||||
|
||||
#[cfg(feature = "runtime")]
|
||||
impl HttpsConnector<HttpConnector> {
|
||||
/// 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<HttpsConnector<HttpConnector>, ErrorStack> {
|
||||
let mut http = HttpConnector::new();
|
||||
http.enforce_http(false);
|
||||
|
||||
HttpsLayer::new().map(|l| l.layer(http))
|
||||
}
|
||||
}
|
||||
|
||||
impl<S, T> HttpsConnector<S>
|
||||
where
|
||||
S: Service<Uri, Response = T> + Send,
|
||||
S::Error: Into<Box<dyn Error + Send + Sync>>,
|
||||
S::Future: Unpin + Send + 'static,
|
||||
T: AsyncRead + AsyncWrite + Connection + Unpin + fmt::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<HttpsConnector<S>, ErrorStack> {
|
||||
HttpsLayer::with_connector(ssl).map(|l| l.layer(http))
|
||||
}
|
||||
|
||||
/// Registers a callback which can customize the configuration of each connection.
|
||||
///
|
||||
/// Unsuitable to change verify hostflags (with `config.param_mut().set_hostflags(…)`),
|
||||
/// as they are reset after the callback is executed. Use [`Self::set_ssl_callback`]
|
||||
/// instead.
|
||||
pub fn set_callback<F>(&mut self, callback: F)
|
||||
where
|
||||
F: Fn(&mut ConnectConfiguration, &Uri) -> Result<(), ErrorStack> + 'static + Sync + Send,
|
||||
{
|
||||
self.inner.callback = Some(Arc::new(callback));
|
||||
}
|
||||
|
||||
/// Registers a callback which can customize the `Ssl` of each connection.
|
||||
pub fn set_ssl_callback<F>(&mut self, callback: F)
|
||||
where
|
||||
F: Fn(&mut SslRef, &Uri) -> Result<(), ErrorStack> + 'static + Sync + Send,
|
||||
{
|
||||
self.inner.ssl_callback = Some(Arc::new(callback));
|
||||
}
|
||||
}
|
||||
|
||||
/// A layer which wraps services in an `HttpsConnector`.
|
||||
pub struct HttpsLayer {
|
||||
inner: Inner,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct Inner {
|
||||
ssl: SslConnector,
|
||||
cache: Arc<Mutex<SessionCache>>,
|
||||
callback: Option<Callback>,
|
||||
ssl_callback: Option<SslCallback>,
|
||||
}
|
||||
|
||||
type Callback =
|
||||
Arc<dyn Fn(&mut ConnectConfiguration, &Uri) -> Result<(), ErrorStack> + Sync + Send>;
|
||||
type SslCallback = Arc<dyn Fn(&mut SslRef, &Uri) -> Result<(), ErrorStack> + Sync + Send>;
|
||||
|
||||
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<HttpsLayer, ErrorStack> {
|
||||
let mut ssl = SslConnector::builder(SslMethod::tls())?;
|
||||
|
||||
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(ssl: SslConnectorBuilder) -> Result<HttpsLayer, ErrorStack> {
|
||||
Self::with_connector_and_settings(ssl, Default::default())
|
||||
}
|
||||
|
||||
/// Creates a new `HttpsLayer` with settings
|
||||
pub fn with_connector_and_settings(
|
||||
mut ssl: SslConnectorBuilder,
|
||||
settings: HttpsLayerSettings,
|
||||
) -> Result<HttpsLayer, ErrorStack> {
|
||||
let cache = Arc::new(Mutex::new(SessionCache::with_capacity(
|
||||
settings.session_cache_capacity,
|
||||
)));
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Ok(HttpsLayer {
|
||||
inner: Inner {
|
||||
ssl: ssl.build(),
|
||||
cache,
|
||||
callback: None,
|
||||
ssl_callback: None,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
/// Registers a callback which can customize the configuration of each connection.
|
||||
///
|
||||
/// Unsuitable to change verify hostflags (with `config.param_mut().set_hostflags(…)`),
|
||||
/// as they are reset after the callback is executed. Use [`Self::set_ssl_callback`]
|
||||
/// instead.
|
||||
pub fn set_callback<F>(&mut self, callback: F)
|
||||
where
|
||||
F: Fn(&mut ConnectConfiguration, &Uri) -> Result<(), ErrorStack> + 'static + Sync + Send,
|
||||
{
|
||||
self.inner.callback = Some(Arc::new(callback));
|
||||
}
|
||||
|
||||
/// Registers a callback which can customize the `Ssl` of each connection.
|
||||
pub fn set_ssl_callback<F>(&mut self, callback: F)
|
||||
where
|
||||
F: Fn(&mut SslRef, &Uri) -> Result<(), ErrorStack> + 'static + Sync + Send,
|
||||
{
|
||||
self.inner.ssl_callback = Some(Arc::new(callback));
|
||||
}
|
||||
}
|
||||
|
||||
impl<S> Layer<S> for HttpsLayer {
|
||||
type Service = HttpsConnector<S>;
|
||||
|
||||
fn layer(&self, inner: S) -> HttpsConnector<S> {
|
||||
HttpsConnector {
|
||||
http: inner,
|
||||
inner: self.inner.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Inner {
|
||||
fn setup_ssl(&self, uri: &Uri, host: &str) -> Result<Ssl, ErrorStack> {
|
||||
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);
|
||||
|
||||
let mut ssl = conf.into_ssl(host)?;
|
||||
|
||||
if let Some(ref ssl_callback) = self.ssl_callback {
|
||||
ssl_callback(&mut ssl, uri)?;
|
||||
}
|
||||
|
||||
Ok(ssl)
|
||||
}
|
||||
}
|
||||
|
||||
impl<S> Service<Uri> for HttpsConnector<S>
|
||||
where
|
||||
S: Service<Uri> + Send,
|
||||
S::Error: Into<Box<dyn Error + Send + Sync>>,
|
||||
S::Future: Unpin + Send + 'static,
|
||||
S::Response: AsyncRead + AsyncWrite + Connection + Unpin + fmt::Debug + Sync + Send + 'static,
|
||||
{
|
||||
type Response = MaybeHttpsStream<S::Response>;
|
||||
type Error = Box<dyn Error + Sync + Send>;
|
||||
#[allow(clippy::type_complexity)]
|
||||
type Future = Pin<Box<dyn Future<Output = Result<Self::Response, Self::Error>> + Send>>;
|
||||
|
||||
fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
|
||||
self.http.poll_ready(cx).map_err(Into::into)
|
||||
}
|
||||
|
||||
fn call(&mut self, uri: Uri) -> Self::Future {
|
||||
let is_tls_scheme = uri
|
||||
.scheme()
|
||||
.map(|s| s == &Scheme::HTTPS || s.as_str() == "wss")
|
||||
.unwrap_or(false);
|
||||
|
||||
let tls_setup = if is_tls_scheme {
|
||||
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 mut host = uri.host().ok_or("URI missing host")?;
|
||||
|
||||
// If `host` is an IPv6 address, we must strip away the square brackets that surround
|
||||
// it (otherwise, boring will fail to parse the host as an IP address, eventually
|
||||
// causing the handshake to fail due a hostname verification error).
|
||||
if !host.is_empty() {
|
||||
let last = host.len() - 1;
|
||||
let mut chars = host.chars();
|
||||
|
||||
if let (Some('['), Some(']')) = (chars.next(), chars.last()) {
|
||||
if host[1..last].parse::<net::Ipv6Addr>().is_ok() {
|
||||
host = &host[1..last];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let ssl = inner.setup_ssl(&uri, host)?;
|
||||
let stream = tokio_boring::SslStreamBuilder::new(ssl, conn)
|
||||
.connect()
|
||||
.await?;
|
||||
|
||||
Ok(MaybeHttpsStream::Https(stream))
|
||||
};
|
||||
|
||||
Box::pin(f)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Connection for MaybeHttpsStream<T>
|
||||
where
|
||||
T: Connection,
|
||||
{
|
||||
fn connected(&self) -> Connected {
|
||||
match self {
|
||||
MaybeHttpsStream::Http(s) => s.connected(),
|
||||
MaybeHttpsStream::Https(s) => {
|
||||
let mut connected = s.get_ref().connected();
|
||||
|
||||
if s.ssl().selected_alpn_protocol() == Some(b"h2") {
|
||||
connected = connected.negotiated_h2();
|
||||
}
|
||||
|
||||
connected
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> AsyncRead for MaybeHttpsStream<T>
|
||||
where
|
||||
T: AsyncRead + AsyncWrite + Unpin,
|
||||
{
|
||||
fn poll_read(
|
||||
mut self: Pin<&mut Self>,
|
||||
ctx: &mut Context<'_>,
|
||||
buf: &mut ReadBuf<'_>,
|
||||
) -> Poll<io::Result<()>> {
|
||||
match &mut *self {
|
||||
MaybeHttpsStream::Http(s) => Pin::new(s).poll_read(ctx, buf),
|
||||
MaybeHttpsStream::Https(s) => Pin::new(s).poll_read(ctx, buf),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> AsyncWrite for MaybeHttpsStream<T>
|
||||
where
|
||||
T: AsyncRead + AsyncWrite + Unpin,
|
||||
{
|
||||
fn poll_write(
|
||||
mut self: Pin<&mut Self>,
|
||||
ctx: &mut Context<'_>,
|
||||
buf: &[u8],
|
||||
) -> Poll<io::Result<usize>> {
|
||||
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<io::Result<()>> {
|
||||
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<io::Result<()>> {
|
||||
match &mut *self {
|
||||
MaybeHttpsStream::Http(s) => Pin::new(s).poll_shutdown(ctx),
|
||||
MaybeHttpsStream::Https(s) => Pin::new(s).poll_shutdown(ctx),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,350 @@
|
|||
use crate::cache::{SessionCache, SessionKey};
|
||||
use crate::{key_index, HttpsLayerSettings, MaybeHttpsStream};
|
||||
use antidote::Mutex;
|
||||
use boring::error::ErrorStack;
|
||||
use boring::ssl::{
|
||||
ConnectConfiguration, Ssl, SslConnector, SslConnectorBuilder, SslMethod, SslRef,
|
||||
SslSessionCacheMode,
|
||||
};
|
||||
use http::uri::Scheme;
|
||||
use http::Uri;
|
||||
use hyper::rt::{Read, ReadBufCursor, Write};
|
||||
use hyper_util::client::legacy::connect::{Connected, Connection, HttpConnector};
|
||||
use hyper_util::rt::TokioIo;
|
||||
use std::error::Error;
|
||||
use std::fmt;
|
||||
use std::future::Future;
|
||||
use std::pin::Pin;
|
||||
use std::sync::Arc;
|
||||
use std::task::{Context, Poll};
|
||||
use std::{io, net};
|
||||
use tokio::io::{AsyncRead, AsyncWrite};
|
||||
use tower_layer::Layer;
|
||||
use tower_service::Service;
|
||||
|
||||
/// A Connector using BoringSSL to support `http` and `https` schemes.
|
||||
#[derive(Clone)]
|
||||
pub struct HttpsConnector<T> {
|
||||
http: T,
|
||||
inner: Inner,
|
||||
}
|
||||
|
||||
#[cfg(feature = "runtime")]
|
||||
impl HttpsConnector<HttpConnector> {
|
||||
/// 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<HttpsConnector<HttpConnector>, ErrorStack> {
|
||||
let mut http = HttpConnector::new();
|
||||
http.enforce_http(false);
|
||||
|
||||
HttpsLayer::new().map(|l| l.layer(http))
|
||||
}
|
||||
}
|
||||
|
||||
impl<S, T> HttpsConnector<S>
|
||||
where
|
||||
S: Service<Uri, Response = TokioIo<T>> + Send,
|
||||
S::Error: Into<Box<dyn Error + Send + Sync>>,
|
||||
S::Future: Unpin + Send + 'static,
|
||||
T: AsyncRead + AsyncWrite + Connection + Unpin + fmt::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<HttpsConnector<S>, ErrorStack> {
|
||||
HttpsLayer::with_connector(ssl).map(|l| l.layer(http))
|
||||
}
|
||||
|
||||
/// Registers a callback which can customize the configuration of each connection.
|
||||
///
|
||||
/// Unsuitable to change verify hostflags (with `config.param_mut().set_hostflags(…)`),
|
||||
/// as they are reset after the callback is executed. Use [`Self::set_ssl_callback`]
|
||||
/// instead.
|
||||
pub fn set_callback<F>(&mut self, callback: F)
|
||||
where
|
||||
F: Fn(&mut ConnectConfiguration, &Uri) -> Result<(), ErrorStack> + 'static + Sync + Send,
|
||||
{
|
||||
self.inner.callback = Some(Arc::new(callback));
|
||||
}
|
||||
|
||||
/// Registers a callback which can customize the `Ssl` of each connection.
|
||||
pub fn set_ssl_callback<F>(&mut self, callback: F)
|
||||
where
|
||||
F: Fn(&mut SslRef, &Uri) -> Result<(), ErrorStack> + 'static + Sync + Send,
|
||||
{
|
||||
self.inner.ssl_callback = Some(Arc::new(callback));
|
||||
}
|
||||
}
|
||||
|
||||
/// A layer which wraps services in an `HttpsConnector`.
|
||||
pub struct HttpsLayer {
|
||||
inner: Inner,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct Inner {
|
||||
ssl: SslConnector,
|
||||
cache: Arc<Mutex<SessionCache>>,
|
||||
callback: Option<Callback>,
|
||||
ssl_callback: Option<SslCallback>,
|
||||
}
|
||||
|
||||
type Callback =
|
||||
Arc<dyn Fn(&mut ConnectConfiguration, &Uri) -> Result<(), ErrorStack> + Sync + Send>;
|
||||
type SslCallback = Arc<dyn Fn(&mut SslRef, &Uri) -> Result<(), ErrorStack> + Sync + Send>;
|
||||
|
||||
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<HttpsLayer, ErrorStack> {
|
||||
let mut ssl = SslConnector::builder(SslMethod::tls())?;
|
||||
|
||||
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(ssl: SslConnectorBuilder) -> Result<HttpsLayer, ErrorStack> {
|
||||
Self::with_connector_and_settings(ssl, Default::default())
|
||||
}
|
||||
|
||||
/// Creates a new `HttpsLayer` with settings
|
||||
pub fn with_connector_and_settings(
|
||||
mut ssl: SslConnectorBuilder,
|
||||
settings: HttpsLayerSettings,
|
||||
) -> Result<HttpsLayer, ErrorStack> {
|
||||
let cache = Arc::new(Mutex::new(SessionCache::with_capacity(
|
||||
settings.session_cache_capacity,
|
||||
)));
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Ok(HttpsLayer {
|
||||
inner: Inner {
|
||||
ssl: ssl.build(),
|
||||
cache,
|
||||
callback: None,
|
||||
ssl_callback: None,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
/// Registers a callback which can customize the configuration of each connection.
|
||||
///
|
||||
/// Unsuitable to change verify hostflags (with `config.param_mut().set_hostflags(…)`),
|
||||
/// as they are reset after the callback is executed. Use [`Self::set_ssl_callback`]
|
||||
/// instead.
|
||||
pub fn set_callback<F>(&mut self, callback: F)
|
||||
where
|
||||
F: Fn(&mut ConnectConfiguration, &Uri) -> Result<(), ErrorStack> + 'static + Sync + Send,
|
||||
{
|
||||
self.inner.callback = Some(Arc::new(callback));
|
||||
}
|
||||
|
||||
/// Registers a callback which can customize the `Ssl` of each connection.
|
||||
pub fn set_ssl_callback<F>(&mut self, callback: F)
|
||||
where
|
||||
F: Fn(&mut SslRef, &Uri) -> Result<(), ErrorStack> + 'static + Sync + Send,
|
||||
{
|
||||
self.inner.ssl_callback = Some(Arc::new(callback));
|
||||
}
|
||||
}
|
||||
|
||||
impl<S> Layer<S> for HttpsLayer {
|
||||
type Service = HttpsConnector<S>;
|
||||
|
||||
fn layer(&self, inner: S) -> HttpsConnector<S> {
|
||||
HttpsConnector {
|
||||
http: inner,
|
||||
inner: self.inner.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Inner {
|
||||
fn setup_ssl(&self, uri: &Uri, host: &str) -> Result<Ssl, ErrorStack> {
|
||||
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);
|
||||
|
||||
let mut ssl = conf.into_ssl(host)?;
|
||||
|
||||
if let Some(ref ssl_callback) = self.ssl_callback {
|
||||
ssl_callback(&mut ssl, uri)?;
|
||||
}
|
||||
|
||||
Ok(ssl)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, S> Service<Uri> for HttpsConnector<S>
|
||||
where
|
||||
S: Service<Uri, Response = TokioIo<T>> + Send,
|
||||
S::Error: Into<Box<dyn Error + Send + Sync>>,
|
||||
S::Future: Unpin + Send + 'static,
|
||||
T: AsyncRead + AsyncWrite + Connection + Unpin + fmt::Debug + Sync + Send + 'static,
|
||||
{
|
||||
type Response = MaybeHttpsStream<T>;
|
||||
type Error = Box<dyn Error + Sync + Send>;
|
||||
#[allow(clippy::type_complexity)]
|
||||
type Future = Pin<Box<dyn Future<Output = Result<Self::Response, Self::Error>> + Send>>;
|
||||
|
||||
fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
|
||||
self.http.poll_ready(cx).map_err(Into::into)
|
||||
}
|
||||
|
||||
fn call(&mut self, uri: Uri) -> Self::Future {
|
||||
let is_tls_scheme = uri
|
||||
.scheme()
|
||||
.map(|s| s == &Scheme::HTTPS || s.as_str() == "wss")
|
||||
.unwrap_or(false);
|
||||
|
||||
let tls_setup = if is_tls_scheme {
|
||||
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)?.into_inner();
|
||||
|
||||
let (inner, uri) = match tls_setup {
|
||||
Some((inner, uri)) => (inner, uri),
|
||||
None => return Ok(MaybeHttpsStream::Http(conn)),
|
||||
};
|
||||
|
||||
let mut host = uri.host().ok_or("URI missing host")?;
|
||||
|
||||
// If `host` is an IPv6 address, we must strip away the square brackets that surround
|
||||
// it (otherwise, boring will fail to parse the host as an IP address, eventually
|
||||
// causing the handshake to fail due a hostname verification error).
|
||||
if !host.is_empty() {
|
||||
let last = host.len() - 1;
|
||||
let mut chars = host.chars();
|
||||
|
||||
if let (Some('['), Some(']')) = (chars.next(), chars.last()) {
|
||||
if host[1..last].parse::<net::Ipv6Addr>().is_ok() {
|
||||
host = &host[1..last];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let ssl = inner.setup_ssl(&uri, host)?;
|
||||
let stream = tokio_boring::SslStreamBuilder::new(ssl, conn)
|
||||
.connect()
|
||||
.await?;
|
||||
|
||||
Ok(MaybeHttpsStream::Https(stream))
|
||||
};
|
||||
|
||||
Box::pin(f)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Connection for MaybeHttpsStream<T>
|
||||
where
|
||||
T: Connection,
|
||||
{
|
||||
fn connected(&self) -> Connected {
|
||||
match self {
|
||||
MaybeHttpsStream::Http(s) => s.connected(),
|
||||
MaybeHttpsStream::Https(s) => {
|
||||
let mut connected = s.get_ref().connected();
|
||||
|
||||
if s.ssl().selected_alpn_protocol() == Some(b"h2") {
|
||||
connected = connected.negotiated_h2();
|
||||
}
|
||||
|
||||
connected
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Read for MaybeHttpsStream<T>
|
||||
where
|
||||
T: AsyncRead + AsyncWrite + Unpin,
|
||||
{
|
||||
fn poll_read(
|
||||
mut self: Pin<&mut Self>,
|
||||
cx: &mut Context<'_>,
|
||||
buf: ReadBufCursor<'_>,
|
||||
) -> Poll<Result<(), std::io::Error>> {
|
||||
match &mut *self {
|
||||
MaybeHttpsStream::Http(inner) => Pin::new(&mut TokioIo::new(inner)).poll_read(cx, buf),
|
||||
MaybeHttpsStream::Https(inner) => Pin::new(&mut TokioIo::new(inner)).poll_read(cx, buf),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Write for MaybeHttpsStream<T>
|
||||
where
|
||||
T: AsyncRead + AsyncWrite + Unpin,
|
||||
{
|
||||
fn poll_write(
|
||||
mut self: Pin<&mut Self>,
|
||||
ctx: &mut Context<'_>,
|
||||
buf: &[u8],
|
||||
) -> Poll<io::Result<usize>> {
|
||||
match &mut *self {
|
||||
MaybeHttpsStream::Http(inner) => {
|
||||
Pin::new(&mut TokioIo::new(inner)).poll_write(ctx, buf)
|
||||
}
|
||||
MaybeHttpsStream::Https(inner) => {
|
||||
Pin::new(&mut TokioIo::new(inner)).poll_write(ctx, buf)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn poll_flush(mut self: Pin<&mut Self>, ctx: &mut Context<'_>) -> Poll<io::Result<()>> {
|
||||
match &mut *self {
|
||||
MaybeHttpsStream::Http(inner) => Pin::new(&mut TokioIo::new(inner)).poll_flush(ctx),
|
||||
MaybeHttpsStream::Https(inner) => Pin::new(&mut TokioIo::new(inner)).poll_flush(ctx),
|
||||
}
|
||||
}
|
||||
|
||||
fn poll_shutdown(mut self: Pin<&mut Self>, ctx: &mut Context<'_>) -> Poll<io::Result<()>> {
|
||||
match &mut *self {
|
||||
MaybeHttpsStream::Http(inner) => Pin::new(&mut TokioIo::new(inner)).poll_shutdown(ctx),
|
||||
MaybeHttpsStream::Https(inner) => Pin::new(&mut TokioIo::new(inner)).poll_shutdown(ctx),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,12 +1,12 @@
|
|||
use super::*;
|
||||
use boring::ssl::{SslAcceptor, SslFiletype, SslMethod};
|
||||
use boring::ssl::{SslAcceptor, SslConnector, SslFiletype, SslMethod};
|
||||
use futures::StreamExt;
|
||||
use hyper::client::HttpConnector;
|
||||
use hyper::server::conn::Http;
|
||||
use hyper::{service, Response};
|
||||
use hyper::{Body, Client};
|
||||
use hyper_boring::HttpsConnector;
|
||||
use hyper_old::client::HttpConnector;
|
||||
use hyper_old::server::conn::Http;
|
||||
use hyper_old::{service, Response};
|
||||
use hyper_old::{Body, Client};
|
||||
use std::convert::Infallible;
|
||||
use std::iter;
|
||||
use std::{io, iter};
|
||||
use tokio::net::TcpListener;
|
||||
|
||||
#[tokio::test]
|
||||
|
|
@ -37,10 +37,10 @@ async fn localhost() {
|
|||
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)
|
||||
.set_private_key_file("tests/test/key.pem", SslFiletype::PEM)
|
||||
.unwrap();
|
||||
acceptor
|
||||
.set_certificate_chain_file("test/cert.pem")
|
||||
.set_certificate_chain_file("tests/test/cert.pem")
|
||||
.unwrap();
|
||||
let acceptor = acceptor.build();
|
||||
|
||||
|
|
@ -69,7 +69,7 @@ async fn localhost() {
|
|||
|
||||
let mut ssl = SslConnector::builder(SslMethod::tls()).unwrap();
|
||||
|
||||
ssl.set_ca_file("test/root-ca.pem").unwrap();
|
||||
ssl.set_ca_file("tests/test/root-ca.pem").unwrap();
|
||||
|
||||
use std::fs::File;
|
||||
use std::io::Write;
|
||||
|
|
@ -104,10 +104,10 @@ async fn alpn_h2() {
|
|||
let server = async move {
|
||||
let mut acceptor = SslAcceptor::mozilla_modern(SslMethod::tls()).unwrap();
|
||||
acceptor
|
||||
.set_certificate_chain_file("test/cert.pem")
|
||||
.set_certificate_chain_file("tests/test/cert.pem")
|
||||
.unwrap();
|
||||
acceptor
|
||||
.set_private_key_file("test/key.pem", SslFiletype::PEM)
|
||||
.set_private_key_file("tests/test/key.pem", SslFiletype::PEM)
|
||||
.unwrap();
|
||||
acceptor.set_alpn_select_callback(|_, client| {
|
||||
ssl::select_next_proto(b"\x02h2", client).ok_or(AlpnError::NOACK)
|
||||
|
|
@ -138,7 +138,7 @@ async fn alpn_h2() {
|
|||
|
||||
let mut ssl = SslConnector::builder(SslMethod::tls()).unwrap();
|
||||
|
||||
ssl.set_ca_file("test/root-ca.pem").unwrap();
|
||||
ssl.set_ca_file("tests/test/root-ca.pem").unwrap();
|
||||
|
||||
let mut ssl = HttpsConnector::with_connector(connector, ssl).unwrap();
|
||||
|
||||
|
|
@ -0,0 +1,160 @@
|
|||
#![cfg(feature = "hyper1")]
|
||||
|
||||
use boring::ssl::{SslAcceptor, SslConnector, SslFiletype, SslMethod};
|
||||
use bytes::Bytes;
|
||||
use futures::StreamExt;
|
||||
use http_body_util::{BodyStream, Empty};
|
||||
use hyper::{service, Response};
|
||||
use hyper_boring::v1::HttpsConnector;
|
||||
use hyper_util::client::legacy::connect::HttpConnector;
|
||||
use hyper_util::client::legacy::Client;
|
||||
use hyper_util::rt::{TokioExecutor, TokioIo};
|
||||
use std::convert::Infallible;
|
||||
use std::{io, iter};
|
||||
use tokio::net::TcpListener;
|
||||
|
||||
#[tokio::test]
|
||||
async fn google() {
|
||||
let ssl = HttpsConnector::new().unwrap();
|
||||
let client = Client::builder(TokioExecutor::new())
|
||||
.pool_max_idle_per_host(0)
|
||||
.build::<_, Empty<Bytes>>(ssl);
|
||||
|
||||
for _ in 0..3 {
|
||||
let resp = client
|
||||
.get("https://www.google.com".parse().unwrap())
|
||||
.await
|
||||
.expect("connection should succeed");
|
||||
let mut body = BodyStream::new(resp.into_body());
|
||||
while body.next().await.transpose().unwrap().is_some() {}
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn localhost() {
|
||||
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
|
||||
let addr = listener.local_addr().unwrap();
|
||||
let port = addr.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("tests/test/key.pem", SslFiletype::PEM)
|
||||
.unwrap();
|
||||
acceptor
|
||||
.set_certificate_chain_file("tests/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(<Empty<Bytes>>::new()))
|
||||
});
|
||||
|
||||
hyper::server::conn::http1::Builder::new()
|
||||
.keep_alive(false)
|
||||
.serve_connection(TokioIo::new(stream), service)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
};
|
||||
tokio::spawn(server);
|
||||
|
||||
let resolver =
|
||||
tower::service_fn(move |_name| async move { Ok::<_, Infallible>(iter::once(addr)) });
|
||||
|
||||
let mut connector = HttpConnector::new_with_resolver(resolver);
|
||||
|
||||
connector.enforce_http(false);
|
||||
|
||||
let mut ssl = SslConnector::builder(SslMethod::tls()).unwrap();
|
||||
|
||||
ssl.set_ca_file("tests/test/root-ca.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(TokioExecutor::new()).build::<_, Empty<Bytes>>(ssl);
|
||||
|
||||
for _ in 0..3 {
|
||||
let resp = client
|
||||
.get(format!("https://foobar.com:{}", port).parse().unwrap())
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(resp.status().is_success(), "{}", resp.status());
|
||||
let mut body = BodyStream::new(resp.into_body());
|
||||
while body.next().await.transpose().unwrap().is_some() {}
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn alpn_h2() {
|
||||
use boring::ssl::{self, AlpnError};
|
||||
|
||||
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
|
||||
let addr = listener.local_addr().unwrap();
|
||||
let port = addr.port();
|
||||
|
||||
let server = async move {
|
||||
let mut acceptor = SslAcceptor::mozilla_modern(SslMethod::tls()).unwrap();
|
||||
acceptor
|
||||
.set_certificate_chain_file("tests/test/cert.pem")
|
||||
.unwrap();
|
||||
acceptor
|
||||
.set_private_key_file("tests/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(<Empty<Bytes>>::new()))
|
||||
});
|
||||
|
||||
hyper::server::conn::http2::Builder::new(TokioExecutor::new())
|
||||
.serve_connection(TokioIo::new(stream), service)
|
||||
.await
|
||||
.unwrap();
|
||||
};
|
||||
tokio::spawn(server);
|
||||
|
||||
let resolver =
|
||||
tower::service_fn(move |_name| async move { Ok::<_, Infallible>(iter::once(addr)) });
|
||||
|
||||
let mut connector = HttpConnector::new_with_resolver(resolver);
|
||||
|
||||
connector.enforce_http(false);
|
||||
|
||||
let mut ssl = SslConnector::builder(SslMethod::tls()).unwrap();
|
||||
|
||||
ssl.set_ca_file("tests/test/root-ca.pem").unwrap();
|
||||
|
||||
let mut ssl = HttpsConnector::with_connector(connector, ssl).unwrap();
|
||||
|
||||
ssl.set_ssl_callback(|ssl, _| ssl.set_alpn_protos(b"\x02h2\x08http/1.1"));
|
||||
|
||||
let client = Client::builder(TokioExecutor::new()).build::<_, Empty<Bytes>>(ssl);
|
||||
|
||||
let resp = client
|
||||
.get(format!("https://foobar.com:{}", port).parse().unwrap())
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(resp.status().is_success(), "{}", resp.status());
|
||||
let mut body = BodyStream::new(resp.into_body());
|
||||
while body.next().await.transpose().unwrap().is_some() {}
|
||||
}
|
||||
Loading…
Reference in New Issue