Add support for alternate TLs implementations.

This commit is contained in:
Jacob Hoffman-Andrews
2021-10-04 22:47:00 -07:00
committed by Martin Algesten
parent 1c1dfaa691
commit 56276c3742
17 changed files with 527 additions and 233 deletions

View File

@@ -39,7 +39,7 @@ jobs:
with: with:
command: doc command: doc
# Keep in sync with Cargo.toml's [package.metadata.docs.rs] # Keep in sync with Cargo.toml's [package.metadata.docs.rs]
args: --no-default-features --no-deps --features "tls json charset cookies socks-proxy" args: --no-default-features --no-deps --features "tls native-tls json charset cookies socks-proxy"
build_and_test: build_and_test:
name: Test name: Test
runs-on: ubuntu-latest runs-on: ubuntu-latest
@@ -47,7 +47,8 @@ jobs:
matrix: matrix:
tls: tls:
- "" - ""
- tls - "tls"
- "native-tls"
feature: feature:
- "" - ""
- json - json
@@ -69,4 +70,4 @@ jobs:
uses: actions-rs/cargo@v1 uses: actions-rs/cargo@v1
with: with:
command: test command: test
args: --no-default-features --features "${{ matrix.tls }} ${{ matrix.feature }}" args: --no-default-features --features "${{ matrix.tls }}" "${{ matrix.feature }}"

View File

@@ -12,14 +12,14 @@ edition = "2018"
[package.metadata.docs.rs] [package.metadata.docs.rs]
# Keep in sync with .github/workflows/test.yml # Keep in sync with .github/workflows/test.yml
features = [ "tls", "json", "charset", "cookies", "socks-proxy" ] features = ["tls", "native-tls", "json", "charset", "cookies", "socks-proxy"]
[features] [features]
default = ["tls"] default = ["tls"]
tls = ["webpki", "webpki-roots", "rustls"]
native-certs = ["rustls-native-certs"]
json = ["serde", "serde_json"] json = ["serde", "serde_json"]
charset = ["encoding_rs"] charset = ["encoding_rs"]
tls = ["rustls", "webpki", "webpki-roots"]
native-certs = ["rustls-native-certs"]
cookies = ["cookie", "cookie_store"] cookies = ["cookie", "cookie_store"]
socks-proxy = ["socks"] socks-proxy = ["socks"]
@@ -30,15 +30,16 @@ cookie = { version = "0.15", default-features = false, optional = true}
once_cell = "1" once_cell = "1"
url = "2" url = "2"
socks = { version = "0.3", optional = true } socks = { version = "0.3", optional = true }
rustls = { version = "0.20.1", optional = true }
webpki = { version = "0.22", optional = true }
webpki-roots = { version = "0.22", optional = true }
rustls-native-certs = { version = "0.6", optional = true }
serde = { version = "1", optional = true } serde = { version = "1", optional = true }
serde_json = { version = "1", optional = true } serde_json = { version = "1", optional = true }
encoding_rs = { version = "0.8", optional = true } encoding_rs = { version = "0.8", optional = true }
cookie_store = { version = "0.15", optional = true, default-features = false, features = ["preserve_order"] } cookie_store = { version = "0.15", optional = true, default-features = false, features = ["preserve_order"] }
log = "0.4" log = "0.4"
webpki = { version = "0.22", optional = true }
webpki-roots = { version = "0.22", optional = true }
rustls = { version = "0.20", optional = true }
rustls-native-certs = { version = "0.6", optional = true }
native-tls = { version = "0.2", optional = true }
[dev-dependencies] [dev-dependencies]
serde = { version = "1", features = ["derive"] } serde = { version = "1", features = ["derive"] }
@@ -51,4 +52,4 @@ name = "smoke-test"
[[example]] [[example]]
name = "cureq" name = "cureq"
required-features = ["charset", "cookies", "socks-proxy"] required-features = ["charset", "cookies", "socks-proxy", "native-tls"]

View File

@@ -12,7 +12,7 @@ HTTPS, and charset decoding.
Ureq is in pure Rust for safety and ease of understanding. It avoids using Ureq is in pure Rust for safety and ease of understanding. It avoids using
`unsafe` directly. It [uses blocking I/O][blocking] instead of async I/O, because that keeps `unsafe` directly. It [uses blocking I/O][blocking] instead of async I/O, because that keeps
the API simple and and keeps dependencies to a minimum. For TLS, ureq uses the API simple and and keeps dependencies to a minimum. For TLS, ureq uses
[rustls]. [rustls or native-tls](#tls).
Version 2.0.0 was released recently and changed some APIs. See the [changelog] for details. Version 2.0.0 was released recently and changed some APIs. See the [changelog] for details.
@@ -101,12 +101,18 @@ You can control them when including ureq as a dependency.
`ureq = { version = "*", features = ["json", "charset"] }` `ureq = { version = "*", features = ["json", "charset"] }`
* `tls` enables https. This is enabled by default. * `tls` enables https. This is enabled by default.
* `native-certs` makes the default TLS implementation use the OS' trust store (see TLS doc below).
* `cookies` enables cookies. * `cookies` enables cookies.
* `json` enables [Response::into_json()] and [Request::send_json()] via serde_json. * `json` enables [Response::into_json()] and [Request::send_json()] via serde_json.
* `charset` enables interpreting the charset part of the Content-Type header * `charset` enables interpreting the charset part of the Content-Type header
(e.g. `Content-Type: text/plain; charset=iso-8859-1`). Without this, the (e.g. `Content-Type: text/plain; charset=iso-8859-1`). Without this, the
library defaults to Rust's built in `utf-8`. library defaults to Rust's built in `utf-8`.
* `socks-proxy` enables proxy config using the `socks4://`, `socks4a://`, `socks5://` and `socks://` (equal to `socks5://`) prefix. * `socks-proxy` enables proxy config using the `socks4://`, `socks4a://`, `socks5://` and `socks://` (equal to `socks5://`) prefix.
* `native-tls` enables an adapter so you can pass a `native_tls::TlsConnector` instance
to `AgentBuilder::tls_connector`. Due to the risk of diamond dependencies accidentally switching on an unwanted
TLS implementation, `native-tls` is never picked up as a default or used by the crate level
convenience calls (`ureq::get` etc) it must be configured on the agent. The `native-certs` feature
does nothing for `native-tls`.
## Plain requests ## Plain requests
@@ -211,6 +217,40 @@ fn proxy_example_2() -> std::result::Result<(), ureq::Error> {
} }
``` ```
## HTTPS / TLS / SSL
On platforms that support rustls, ureq uses rustls. On other platforms, native-tls can
be manually configured using [`AgentBuilder::tls_connector`].
You might want to use native-tls if you need to interoperate with servers that
only support less-secure TLS configurations (rustls doesn't support TLS 1.0 and 1.1, for
instance). You might also want to use it if you need to validate certificates for IP addresses,
which are not currently supported in rustls.
Here's an example of constructing an Agent that uses native-tls. It requires the
"native-tls" feature to be enabled.
```rust
use std::sync::Arc;
use ureq::Agent;
let agent = ureq::AgentBuilder::new()
.tls_connector(Arc::new(native_tls::TlsConnector::new().unwrap()))
.build();
```
### Trusted Roots
When you use rustls (`tls` feature), ureq defaults to trusting
[webpki-roots](https://docs.rs/webpki-roots/), a
copy of the Mozilla Root program that is bundled into your program (and so won't update if your
program isn't updated). You can alternately configure
[rustls-native-certs](https://docs.rs/rustls-native-certs/) which extracts the roots from your
OS' trust store. That means it will update when your OS is updated, and also that it will
include locally installed roots.
When you use `native-tls`, ureq will use your OS' certificate verifier and root store.
## Blocking I/O for simplicity ## Blocking I/O for simplicity
Ureq uses blocking I/O rather than Rust's newer [asynchronous (async) I/O][async]. Async I/O Ureq uses blocking I/O rather than Rust's newer [asynchronous (async) I/O][async]. Async I/O

View File

@@ -3,13 +3,14 @@ use std::fmt;
use std::io; use std::io;
use std::thread; use std::thread;
use std::time::Duration; use std::time::Duration;
use std::time::SystemTime;
use std::{env, sync::Arc}; use std::{env, sync::Arc};
use rustls::{ use rustls::client::ServerCertVerified;
Certificate, ClientConfig, RootCertStore, ServerCertVerified, ServerCertVerifier, TLSError, use rustls::client::ServerCertVerifier;
}; use rustls::ServerName;
use rustls::{Certificate, ClientConfig};
use ureq; use ureq;
use webpki::DNSNameRef;
#[derive(Debug)] #[derive(Debug)]
struct StringError(String); struct StringError(String);
@@ -100,11 +101,13 @@ struct AcceptAll {}
impl ServerCertVerifier for AcceptAll { impl ServerCertVerifier for AcceptAll {
fn verify_server_cert( fn verify_server_cert(
&self, &self,
_roots: &RootCertStore, _end_entity: &Certificate,
_presented_certs: &[Certificate], _intermediates: &[Certificate],
_dns_name: DNSNameRef<'_>, _server_name: &ServerName,
_scts: &mut dyn Iterator<Item = &[u8]>,
_ocsp_response: &[u8], _ocsp_response: &[u8],
) -> Result<ServerCertVerified, TLSError> { _now: SystemTime,
) -> Result<ServerCertVerified, rustls::Error> {
Ok(ServerCertVerified::assertion()) Ok(ServerCertVerified::assertion())
} }
} }
@@ -132,6 +135,7 @@ fn main2() -> Result<(), Error> {
-k Ignore certificate errors -k Ignore certificate errors
-m <time> Max time for the entire request -m <time> Max time for the entire request
-ct <time> Connection timeout -ct <time> Connection timeout
--native-tls Use native-tls
Fetch url and copy it to stdout. Fetch url and copy it to stdout.
"##, "##,
@@ -160,12 +164,15 @@ Fetch url and copy it to stdout.
wait = Duration::from_secs(wait_seconds); wait = Duration::from_secs(wait_seconds);
} }
"-k" => { "-k" => {
let mut client_config = ClientConfig::new(); let client_config = ClientConfig::builder()
client_config .with_safe_defaults()
.dangerous() .with_custom_certificate_verifier(Arc::new(AcceptAll {}))
.set_certificate_verifier(Arc::new(AcceptAll {})); .with_no_client_auth();
builder = builder.tls_config(Arc::new(client_config)); builder = builder.tls_config(Arc::new(client_config));
} }
"--native-tls" => {
builder = builder.tls_connector(Arc::new(native_tls::TlsConnector::new().unwrap()));
}
"-m" => { "-m" => {
let t: f32 = args let t: f32 = args
.next() .next()

View File

@@ -1,3 +1,4 @@
use std::fmt;
use std::sync::Arc; use std::sync::Arc;
use url::Url; use url::Url;
@@ -6,6 +7,7 @@ use crate::pool::ConnectionPool;
use crate::proxy::Proxy; use crate::proxy::Proxy;
use crate::request::Request; use crate::request::Request;
use crate::resolve::{ArcResolver, StdResolver}; use crate::resolve::{ArcResolver, StdResolver};
use crate::stream::TlsConnector;
use std::time::Duration; use std::time::Duration;
#[cfg(feature = "cookies")] #[cfg(feature = "cookies")]
@@ -28,7 +30,7 @@ pub struct AgentBuilder {
} }
/// Config as built by AgentBuilder and then static for the lifetime of the Agent. /// Config as built by AgentBuilder and then static for the lifetime of the Agent.
#[derive(Debug, Clone)] #[derive(Clone)]
pub(crate) struct AgentConfig { pub(crate) struct AgentConfig {
pub proxy: Option<Proxy>, pub proxy: Option<Proxy>,
pub timeout_connect: Option<Duration>, pub timeout_connect: Option<Duration>,
@@ -37,8 +39,13 @@ pub(crate) struct AgentConfig {
pub timeout: Option<Duration>, pub timeout: Option<Duration>,
pub redirects: u32, pub redirects: u32,
pub user_agent: String, pub user_agent: String,
#[cfg(feature = "tls")] pub tls_config: Arc<dyn TlsConnector>,
pub tls_config: Option<TLSClientConfig>, }
impl fmt::Debug for AgentConfig {
fn fmt(&self, _f: &mut fmt::Formatter<'_>) -> fmt::Result {
todo!()
}
} }
/// Agents keep state between requests. /// Agents keep state between requests.
@@ -215,8 +222,7 @@ impl AgentBuilder {
timeout: None, timeout: None,
redirects: 5, redirects: 5,
user_agent: format!("ureq/{}", env!("CARGO_PKG_VERSION")), user_agent: format!("ureq/{}", env!("CARGO_PKG_VERSION")),
#[cfg(feature = "tls")] tls_config: crate::default_tls_config(),
tls_config: None,
}, },
max_idle_connections: DEFAULT_MAX_IDLE_CONNECTIONS, max_idle_connections: DEFAULT_MAX_IDLE_CONNECTIONS,
max_idle_connections_per_host: DEFAULT_MAX_IDLE_CONNECTIONS_PER_HOST, max_idle_connections_per_host: DEFAULT_MAX_IDLE_CONNECTIONS_PER_HOST,
@@ -472,9 +478,10 @@ impl AgentBuilder {
self self
} }
/// Set the TLS client config to use for the connection. See [`ClientConfig`](https://docs.rs/rustls/latest/rustls/struct.ClientConfig.html). /// Configure TLS options for rustls to use when making HTTPS connections from this Agent.
///
/// This overrides any previous call to tls_config or tls_connector.
/// ///
/// Example:
/// ``` /// ```
/// # fn main() -> Result<(), ureq::Error> { /// # fn main() -> Result<(), ureq::Error> {
/// # ureq::is_test(true); /// # ureq::is_test(true);
@@ -497,10 +504,34 @@ impl AgentBuilder {
/// .build(); /// .build();
/// # Ok(()) /// # Ok(())
/// # } /// # }
/// ```
#[cfg(feature = "tls")] #[cfg(feature = "tls")]
pub fn tls_config(mut self, tls_config: Arc<rustls::ClientConfig>) -> Self { pub fn tls_config(mut self, tls_config: Arc<rustls::ClientConfig>) -> Self {
self.config.tls_config = Some(TLSClientConfig(tls_config)); self.config.tls_config = Arc::new(tls_config);
self
}
/// Configure TLS options for a backend other than rustls. The parameter can be a
/// any type which implements the [HttpsConnector] trait. If you enable the native-tls
/// feature, we provide `impl HttpsConnector for native_tls::TlsConnector` so you can pass
/// [`Arc<native_tls::TlsConnector>`](https://docs.rs/native-tls/0.2.7/native_tls/struct.TlsConnector.html).
///
/// This overrides any previous call to tls_config or tls_connector.
///
/// ```
/// # fn main() -> Result<(), ureq::Error> {
/// # ureq::is_test(true);
/// use std::sync::Arc;
/// # #[cfg(feature = "native-tls")]
/// let tls_connector = Arc::new(native_tls::TlsConnector::new().unwrap());
/// # #[cfg(feature = "native-tls")]
/// let agent = ureq::builder()
/// .tls_connector(tls_connector.clone())
/// .build();
/// # Ok(())
/// # }
/// ```
pub fn tls_connector<T: TlsConnector + 'static>(mut self, tls_config: Arc<T>) -> Self {
self.config.tls_config = tls_config;
self self
} }
@@ -537,17 +568,6 @@ impl AgentBuilder {
} }
} }
#[cfg(feature = "tls")]
#[derive(Clone)]
pub(crate) struct TLSClientConfig(pub(crate) Arc<rustls::ClientConfig>);
#[cfg(feature = "tls")]
impl std::fmt::Debug for TLSClientConfig {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("TLSClientConfig").finish()
}
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;

View File

@@ -64,7 +64,7 @@ impl fmt::Display for HeaderLine {
#[derive(Clone, PartialEq)] #[derive(Clone, PartialEq)]
/// Wrapper type for a header field. /// Wrapper type for a header field.
/// https://tools.ietf.org/html/rfc7230#section-3.2 /// <https://tools.ietf.org/html/rfc7230#section-3.2>
pub struct Header { pub struct Header {
// Line contains the unmodified bytes of single header field. // Line contains the unmodified bytes of single header field.
// It does not contain the final CRLF. // It does not contain the final CRLF.

View File

@@ -16,8 +16,8 @@
//! //!
//! Ureq is in pure Rust for safety and ease of understanding. It avoids using //! Ureq is in pure Rust for safety and ease of understanding. It avoids using
//! `unsafe` directly. It [uses blocking I/O][blocking] instead of async I/O, because that keeps //! `unsafe` directly. It [uses blocking I/O][blocking] instead of async I/O, because that keeps
//! the API simple and keeps dependencies to a minimum. For TLS, ureq uses //! the API simple and and keeps dependencies to a minimum. For TLS, ureq uses
//! [rustls]. //! [rustls or native-tls](#tls).
//! //!
//! Version 2.0.0 was released recently and changed some APIs. See the [changelog] for details. //! Version 2.0.0 was released recently and changed some APIs. See the [changelog] for details.
//! //!
@@ -121,12 +121,18 @@
//! `ureq = { version = "*", features = ["json", "charset"] }` //! `ureq = { version = "*", features = ["json", "charset"] }`
//! //!
//! * `tls` enables https. This is enabled by default. //! * `tls` enables https. This is enabled by default.
//! * `native-certs` makes the default TLS implementation use the OS' trust store (see TLS doc below).
//! * `cookies` enables cookies. //! * `cookies` enables cookies.
//! * `json` enables [Response::into_json()] and [Request::send_json()] via serde_json. //! * `json` enables [Response::into_json()] and [Request::send_json()] via serde_json.
//! * `charset` enables interpreting the charset part of the Content-Type header //! * `charset` enables interpreting the charset part of the Content-Type header
//! (e.g. `Content-Type: text/plain; charset=iso-8859-1`). Without this, the //! (e.g. `Content-Type: text/plain; charset=iso-8859-1`). Without this, the
//! library defaults to Rust's built in `utf-8`. //! library defaults to Rust's built in `utf-8`.
//! * `socks-proxy` enables proxy config using the `socks4://`, `socks4a://`, `socks5://` and `socks://` (equal to `socks5://`) prefix. //! * `socks-proxy` enables proxy config using the `socks4://`, `socks4a://`, `socks5://` and `socks://` (equal to `socks5://`) prefix.
//! * `native-tls` enables an adapter so you can pass a `native_tls::TlsConnector` instance
//! to `AgentBuilder::tls_connector`. Due to the risk of diamond dependencies accidentally switching on an unwanted
//! TLS implementation, `native-tls` is never picked up as a default or used by the crate level
//! convenience calls (`ureq::get` etc) it must be configured on the agent. The `native-certs` feature
//! does nothing for `native-tls`.
//! //!
//! # Plain requests //! # Plain requests
//! //!
@@ -234,6 +240,46 @@
//! # fn main() {} //! # fn main() {}
//! ``` //! ```
//! //!
//! # HTTPS / TLS / SSL
//!
//! On platforms that support rustls, ureq uses rustls. On other platforms, native-tls can
//! be manually configured using [`AgentBuilder::tls_connector`].
//!
//! You might want to use native-tls if you need to interoperate with servers that
//! only support less-secure TLS configurations (rustls doesn't support TLS 1.0 and 1.1, for
//! instance). You might also want to use it if you need to validate certificates for IP addresses,
//! which are not currently supported in rustls.
//!
//! Here's an example of constructing an Agent that uses native-tls. It requires the
//! "native-tls" feature to be enabled.
//!
//! ```no_run
//! # #[cfg(feature = "native-tls")]
//! # fn build() -> std::result::Result<(), ureq::Error> {
//! # ureq::is_test(true);
//! use std::sync::Arc;
//! use ureq::Agent;
//!
//! let agent = ureq::AgentBuilder::new()
//! .tls_connector(Arc::new(native_tls::TlsConnector::new().unwrap()))
//! .build();
//! # Ok(())
//! # }
//! # fn main() {}
//! ```
//!
//! ## Trusted Roots
//!
//! When you use rustls (`tls` feature), ureq defaults to trusting
//! [webpki-roots](https://docs.rs/webpki-roots/), a
//! copy of the Mozilla Root program that is bundled into your program (and so won't update if your
//! program isn't updated). You can alternately configure
//! [rustls-native-certs](https://docs.rs/rustls-native-certs/) which extracts the roots from your
//! OS' trust store. That means it will update when your OS is updated, and also that it will
//! include locally installed roots.
//!
//! When you use `native-tls`, ureq will use your OS' certificate verifier and root store.
//!
//! # Blocking I/O for simplicity //! # Blocking I/O for simplicity
//! //!
//! Ureq uses blocking I/O rather than Rust's newer [asynchronous (async) I/O][async]. Async I/O //! Ureq uses blocking I/O rather than Rust's newer [asynchronous (async) I/O][async]. Async I/O
@@ -287,6 +333,46 @@ mod response;
mod stream; mod stream;
mod unit; mod unit;
// rustls is our default tls engine. If the feature is on, it will be
// used for the shortcut calls the top of the crate (`ureq::get` etc).
#[cfg(feature = "tls")]
mod rtls;
// native-tls is a feature that must be configured via the AgentBuilder.
// it is never picked up as a default (and never used by `ureq::get` etc).
#[cfg(feature = "native-tls")]
mod ntls;
// If we have rustls compiled, that is the default.
#[cfg(feature = "tls")]
pub(crate) fn default_tls_config() -> std::sync::Arc<dyn TlsConnector> {
rtls::default_tls_config()
}
// Without rustls compiled, we just fail on https when using the shortcut
// calls at the top of the crate (`ureq::get` etc).
#[cfg(not(feature = "tls"))]
pub(crate) fn default_tls_config() -> std::sync::Arc<dyn TlsConnector> {
use crate::stream::HttpsStream;
use std::net::TcpStream;
use std::sync::Arc;
struct NoTlsConfig;
impl TlsConnector for NoTlsConfig {
fn connect(
&self,
_dns_name: &str,
_tcp_stream: TcpStream,
) -> Result<Box<dyn HttpsStream>, crate::error::Error> {
Err(ErrorKind::UnknownScheme
.msg("cannot make HTTPS request because no TLS backend is configured"))
}
}
Arc::new(NoTlsConfig)
}
#[cfg(feature = "cookies")] #[cfg(feature = "cookies")]
mod cookies; mod cookies;
@@ -307,6 +393,7 @@ pub use crate::proxy::Proxy;
pub use crate::request::{Request, RequestUrl}; pub use crate::request::{Request, RequestUrl};
pub use crate::resolve::Resolver; pub use crate::resolve::Resolver;
pub use crate::response::Response; pub use crate::response::Response;
pub use crate::stream::TlsConnector;
// re-export // re-export
#[cfg(feature = "cookies")] #[cfg(feature = "cookies")]
@@ -439,7 +526,7 @@ mod tests {
#[test] #[test]
#[cfg(feature = "tls")] #[cfg(feature = "tls")]
fn connect_https_google() { fn connect_https_google_rustls() {
let agent = Agent::new(); let agent = Agent::new();
let resp = agent.get("https://www.google.com/").call().unwrap(); let resp = agent.get("https://www.google.com/").call().unwrap();
@@ -451,7 +538,22 @@ mod tests {
} }
#[test] #[test]
#[cfg(feature = "tls")] #[cfg(feature = "native-tls")]
fn connect_https_google_native_tls() {
use std::sync::Arc;
let tls_config = native_tls::TlsConnector::new().unwrap();
let agent = builder().tls_connector(Arc::new(tls_config)).build();
let resp = agent.get("https://www.google.com/").call().unwrap();
assert_eq!(
"text/html; charset=ISO-8859-1",
resp.header("content-type").unwrap()
);
assert_eq!("text/html", resp.content_type());
}
#[test]
fn connect_https_invalid_name() { fn connect_https_invalid_name() {
let result = get("https://example.com{REQUEST_URI}/").call(); let result = get("https://example.com{REQUEST_URI}/").call();
let e = ErrorKind::Dns; let e = ErrorKind::Dns;

30
src/ntls.rs Normal file
View File

@@ -0,0 +1,30 @@
use crate::error::Error;
use crate::error::ErrorKind;
use crate::stream::{HttpsStream, TlsConnector};
use std::net::TcpStream;
use std::sync::Arc;
#[allow(dead_code)]
pub(crate) fn default_tls_config() -> std::sync::Arc<dyn TlsConnector> {
Arc::new(native_tls::TlsConnector::new().unwrap())
}
impl TlsConnector for native_tls::TlsConnector {
fn connect(
&self,
dns_name: &str,
tcp_stream: TcpStream,
) -> Result<Box<dyn HttpsStream>, Error> {
let stream = native_tls::TlsConnector::connect(self, dns_name, tcp_stream)
.map_err(|e| ErrorKind::Dns.new().src(e))?;
Ok(Box::new(stream))
}
}
#[cfg(feature = "native-tls")]
impl HttpsStream for native_tls::TlsStream<TcpStream> {
fn socket(&self) -> Option<&TcpStream> {
Some(self.get_ref())
}
}

View File

@@ -491,8 +491,8 @@ impl Response {
} }
#[cfg(test)] #[cfg(test)]
pub fn to_write_vec(self) -> Vec<u8> { pub fn as_write_vec(&self) -> &[u8] {
self.stream.to_write_vec() self.stream.as_write_vec()
} }
#[cfg(test)] #[cfg(test)]

121
src/rtls.rs Normal file
View File

@@ -0,0 +1,121 @@
use std::convert::TryFrom;
use std::io::{self, Read, Write};
use std::net::TcpStream;
use std::sync::Arc;
use once_cell::sync::Lazy;
use crate::ErrorKind;
use crate::{
stream::{HttpsStream, TlsConnector},
Error,
};
#[allow(deprecated)]
fn is_close_notify(e: &std::io::Error) -> bool {
if e.kind() != io::ErrorKind::ConnectionAborted {
return false;
}
if let Some(msg) = e.get_ref() {
// :(
return msg.description().contains("CloseNotify");
}
false
}
struct RustlsStream(rustls::StreamOwned<rustls::ClientConnection, TcpStream>);
impl HttpsStream for RustlsStream {
fn socket(&self) -> Option<&TcpStream> {
Some(self.0.get_ref())
}
}
// TODO: After upgrading to rustls 0.20 or higher, we can remove these Read
// and Write impls, leaving only `impl TlsStream for rustls::StreamOwned...`.
// Currently we need to implement Read in order to treat close_notify specially.
// The next release of rustls will handle close_notify in a more intuitive way.
impl Read for RustlsStream {
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
match self.0.read(buf) {
Ok(size) => Ok(size),
Err(ref e) if is_close_notify(e) => Ok(0),
Err(e) => Err(e),
}
}
}
impl Write for RustlsStream {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
self.0.write(buf)
}
fn flush(&mut self) -> io::Result<()> {
self.0.flush()
}
}
#[cfg(feature = "native-certs")]
fn root_certs() -> rustls::RootCertStore {
let mut root_store = rustls::RootCertStore::empty();
let certs = rustls_native_certs::load_native_certs().expect("Could not load platform certs");
for cert in certs {
// Repackage the certificate DER bytes.
let rustls_cert = rustls::Certificate(cert.0);
root_store
.add(&rustls_cert)
.expect("Failed to add native certificate too root store");
}
root_store
}
#[cfg(not(feature = "native-certs"))]
fn root_certs() -> rustls::RootCertStore {
let mut root_store = rustls::RootCertStore::empty();
root_store.add_server_trust_anchors(webpki_roots::TLS_SERVER_ROOTS.0.iter().map(|ta| {
rustls::OwnedTrustAnchor::from_subject_spki_name_constraints(
ta.subject,
ta.spki,
ta.name_constraints,
)
}));
root_store
}
impl TlsConnector for Arc<rustls::ClientConfig> {
fn connect(
&self,
dns_name: &str,
mut tcp_stream: TcpStream,
) -> Result<Box<dyn HttpsStream>, Error> {
let sni =
rustls::ServerName::try_from(dns_name).map_err(|e| ErrorKind::Dns.new().src(e))?;
let mut sess = rustls::ClientConnection::new(self.clone(), sni)
.map_err(|e| ErrorKind::Io.new().src(e))?;
sess.complete_io(&mut tcp_stream)
.map_err(|err| ErrorKind::ConnectionFailed.new().src(err))?;
let stream = rustls::StreamOwned::new(sess, tcp_stream);
Ok(Box::new(RustlsStream(stream)))
}
}
pub fn default_tls_config() -> Arc<dyn TlsConnector> {
static TLS_CONF: Lazy<Arc<dyn TlsConnector>> = Lazy::new(|| {
let config = rustls::ClientConfig::builder()
.with_safe_defaults()
.with_root_certificates(root_certs())
.with_no_client_auth();
Arc::new(Arc::new(config))
});
TLS_CONF.clone()
}

View File

@@ -8,10 +8,6 @@ use std::{fmt, io::Cursor};
use chunked_transfer::Decoder as ChunkDecoder; use chunked_transfer::Decoder as ChunkDecoder;
#[cfg(feature = "tls")]
use rustls::ClientConnection;
#[cfg(feature = "tls")]
use rustls::StreamOwned;
#[cfg(feature = "socks-proxy")] #[cfg(feature = "socks-proxy")]
use socks::{TargetAddr, ToTargetAddr}; use socks::{TargetAddr, ToTargetAddr};
@@ -21,16 +17,83 @@ use crate::{error::Error, proxy::Proto};
use crate::error::ErrorKind; use crate::error::ErrorKind;
use crate::unit::Unit; use crate::unit::Unit;
pub(crate) struct Stream { pub trait HttpsStream: Read + Write + Send + Sync + 'static {
inner: BufReader<Inner>, fn socket(&self) -> Option<&TcpStream>;
} }
#[allow(clippy::large_enum_variant)] pub trait TlsConnector: Send + Sync {
enum Inner { fn connect(
Http(TcpStream), &self,
#[cfg(feature = "tls")] dns_name: &str,
Https(rustls::StreamOwned<rustls::ClientConnection, TcpStream>), tcp_stream: TcpStream,
Test(Box<dyn Read + Send + Sync>, Vec<u8>), ) -> Result<Box<dyn HttpsStream>, crate::error::Error>;
}
pub(crate) struct Stream {
inner: BufReader<Box<dyn Inner + Send + Sync + 'static>>,
}
trait Inner: Read + Write {
fn is_poolable(&self) -> bool;
fn socket(&self) -> Option<&TcpStream>;
fn as_write_vec(&self) -> &[u8] {
panic!("as_write_vec on non Test stream");
}
}
impl<T: HttpsStream + ?Sized> HttpsStream for Box<T> {
fn socket(&self) -> Option<&TcpStream> {
HttpsStream::socket(self.as_ref())
}
}
impl<T: HttpsStream> Inner for T {
fn is_poolable(&self) -> bool {
true
}
fn socket(&self) -> Option<&TcpStream> {
HttpsStream::socket(self)
}
}
impl Inner for TcpStream {
fn is_poolable(&self) -> bool {
true
}
fn socket(&self) -> Option<&TcpStream> {
Some(self)
}
}
struct TestStream(Box<dyn Read + Send + Sync>, Vec<u8>);
impl Inner for TestStream {
fn is_poolable(&self) -> bool {
false
}
fn socket(&self) -> Option<&TcpStream> {
None
}
fn as_write_vec(&self) -> &[u8] {
&self.1
}
}
impl Read for TestStream {
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
self.0.read(buf)
}
}
impl Write for TestStream {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
self.1.write(buf)
}
fn flush(&mut self) -> io::Result<()> {
Ok(())
}
} }
// DeadlineStream wraps a stream such that read() will return an error // DeadlineStream wraps a stream such that read() will return an error
@@ -112,16 +175,20 @@ pub(crate) fn io_err_timeout(error: String) -> io::Error {
impl fmt::Debug for Stream { impl fmt::Debug for Stream {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self.inner.get_ref() { match self.inner.get_ref().socket() {
Inner::Http(tcpstream) => write!(f, "{:?}", tcpstream), Some(s) => write!(f, "{:?}", s),
#[cfg(feature = "tls")] None => write!(f, "Stream(Test)"),
Inner::Https(tlsstream) => write!(f, "{:?}", tlsstream.get_ref()),
Inner::Test(_, _) => write!(f, "Stream(Test)"),
} }
} }
} }
impl Stream { impl Stream {
fn new(t: impl Inner + Send + Sync + 'static) -> Stream {
Stream::logged_create(Stream {
inner: BufReader::new(Box::new(t)),
})
}
fn logged_create(stream: Stream) -> Stream { fn logged_create(stream: Stream) -> Stream {
debug!("created stream: {:?}", stream); debug!("created stream: {:?}", stream);
stream stream
@@ -129,20 +196,13 @@ impl Stream {
pub(crate) fn from_vec(v: Vec<u8>) -> Stream { pub(crate) fn from_vec(v: Vec<u8>) -> Stream {
Stream::logged_create(Stream { Stream::logged_create(Stream {
inner: BufReader::new(Inner::Test(Box::new(Cursor::new(v)), vec![])), inner: BufReader::new(Box::new(TestStream(Box::new(Cursor::new(v)), vec![]))),
}) })
} }
fn from_tcp_stream(t: TcpStream) -> Stream { fn from_tcp_stream(t: TcpStream) -> Stream {
Stream::logged_create(Stream { Stream::logged_create(Stream {
inner: BufReader::new(Inner::Http(t)), inner: BufReader::new(Box::new(t)),
})
}
#[cfg(feature = "tls")]
fn from_tls_stream(t: StreamOwned<ClientConnection, TcpStream>) -> Stream {
Stream::logged_create(Stream {
inner: BufReader::new(Inner::Https(t)),
}) })
} }
@@ -186,12 +246,7 @@ impl Stream {
} }
} }
pub fn is_poolable(&self) -> bool { pub fn is_poolable(&self) -> bool {
match self.inner.get_ref() { self.inner.get_ref().is_poolable()
Inner::Http(_) => true,
#[cfg(feature = "tls")]
Inner::Https(_) => true,
_ => false,
}
} }
pub(crate) fn reset(&mut self) -> io::Result<()> { pub(crate) fn reset(&mut self) -> io::Result<()> {
@@ -206,12 +261,7 @@ impl Stream {
} }
pub(crate) fn socket(&self) -> Option<&TcpStream> { pub(crate) fn socket(&self) -> Option<&TcpStream> {
match self.inner.get_ref() { self.inner.get_ref().socket()
Inner::Http(b) => Some(b),
#[cfg(feature = "tls")]
Inner::Https(b) => Some(b.get_ref()),
_ => None,
}
} }
pub(crate) fn set_read_timeout(&self, timeout: Option<Duration>) -> io::Result<()> { pub(crate) fn set_read_timeout(&self, timeout: Option<Duration>) -> io::Result<()> {
@@ -223,11 +273,8 @@ impl Stream {
} }
#[cfg(test)] #[cfg(test)]
pub fn to_write_vec(&self) -> Vec<u8> { pub fn as_write_vec(&self) -> &[u8] {
match self.inner.get_ref() { self.inner.get_ref().as_write_vec()
Inner::Test(_, writer) => writer.clone(),
_ => panic!("to_write_vec on non Test stream"),
}
} }
} }
@@ -237,17 +284,6 @@ impl Read for Stream {
} }
} }
impl Read for Inner {
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
match self {
Inner::Http(sock) => sock.read(buf),
#[cfg(feature = "tls")]
Inner::Https(stream) => read_https(stream, buf),
Inner::Test(reader, _) => reader.read(buf),
}
}
}
impl BufRead for Stream { impl BufRead for Stream {
fn fill_buf(&mut self) -> io::Result<&[u8]> { fn fill_buf(&mut self) -> io::Result<&[u8]> {
self.inner.fill_buf() self.inner.fill_buf()
@@ -268,50 +304,12 @@ where
} }
} }
#[cfg(feature = "tls")]
fn read_https(
stream: &mut StreamOwned<ClientConnection, TcpStream>,
buf: &mut [u8],
) -> io::Result<usize> {
match stream.read(buf) {
Ok(size) => Ok(size),
Err(ref e) if is_close_notify(e) => Ok(0),
Err(e) => Err(e),
}
}
#[allow(deprecated)]
#[cfg(feature = "tls")]
fn is_close_notify(e: &std::io::Error) -> bool {
if e.kind() != io::ErrorKind::ConnectionAborted {
return false;
}
if let Some(msg) = e.get_ref() {
// :(
return msg.description().contains("CloseNotify");
}
false
}
impl Write for Stream { impl Write for Stream {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> { fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
match self.inner.get_mut() { self.inner.get_mut().write(buf)
Inner::Http(sock) => sock.write(buf),
#[cfg(feature = "tls")]
Inner::Https(stream) => stream.write(buf),
Inner::Test(_, writer) => writer.write(buf),
}
} }
fn flush(&mut self) -> io::Result<()> { fn flush(&mut self) -> io::Result<()> {
match self.inner.get_mut() { self.inner.get_mut().flush()
Inner::Http(sock) => sock.flush(),
#[cfg(feature = "tls")]
Inner::Https(stream) => stream.flush(),
Inner::Test(_, writer) => writer.flush(),
}
} }
} }
@@ -328,55 +326,14 @@ pub(crate) fn connect_http(unit: &Unit, hostname: &str) -> Result<Stream, Error>
connect_host(unit, hostname, port).map(Stream::from_tcp_stream) connect_host(unit, hostname, port).map(Stream::from_tcp_stream)
} }
#[cfg(feature = "tls")]
pub(crate) fn connect_https(unit: &Unit, hostname: &str) -> Result<Stream, Error> { pub(crate) fn connect_https(unit: &Unit, hostname: &str) -> Result<Stream, Error> {
use once_cell::sync::Lazy;
use std::{convert::TryFrom, sync::Arc};
static TLS_CONF: Lazy<Arc<rustls::ClientConfig>> = Lazy::new(|| {
let mut root_store = rustls::RootCertStore::empty();
#[cfg(not(feature = "native-certs"))]
root_store.add_server_trust_anchors(webpki_roots::TLS_SERVER_ROOTS.0.iter().map(|ta| {
rustls::OwnedTrustAnchor::from_subject_spki_name_constraints(
ta.subject,
ta.spki,
ta.name_constraints,
)
}));
#[cfg(feature = "native-certs")]
for cert in rustls_native_certs::load_native_certs().expect("Could not load platform certs")
{
root_store.add(&rustls::Certificate(cert.0)).unwrap();
}
let config = rustls::ClientConfig::builder()
.with_safe_defaults()
.with_root_certificates(root_store)
.with_no_client_auth();
Arc::new(config)
});
let port = unit.url.port().unwrap_or(443); let port = unit.url.port().unwrap_or(443);
let tls_conf: Arc<rustls::ClientConfig> = unit let sock = connect_host(unit, hostname, port)?;
.agent
.config
.tls_config
.as_ref()
.map(|c| c.0.clone())
.unwrap_or_else(|| TLS_CONF.clone());
let mut sock = connect_host(unit, hostname, port)?;
let mut sess = rustls::ClientConnection::new(
tls_conf,
rustls::ServerName::try_from(hostname).map_err(|e| ErrorKind::Dns.new().src(e))?,
)
.map_err(|e| ErrorKind::Io.new().src(e))?;
sess.complete_io(&mut sock) let tls_conf = &unit.agent.config.tls_config;
.map_err(|err| ErrorKind::ConnectionFailed.new().src(err))?; let https_stream = tls_conf.connect(hostname, sock)?;
let stream = rustls::StreamOwned::new(sess, sock); Ok(Stream::new(https_stream))
Ok(Stream::from_tls_stream(stream))
} }
pub(crate) fn connect_host(unit: &Unit, hostname: &str, port: u16) -> Result<TcpStream, Error> { pub(crate) fn connect_host(unit: &Unit, hostname: &str, port: u16) -> Result<TcpStream, Error> {
@@ -666,10 +623,3 @@ pub(crate) fn connect_test(unit: &Unit) -> Result<Stream, Error> {
pub(crate) fn connect_test(unit: &Unit) -> Result<Stream, Error> { pub(crate) fn connect_test(unit: &Unit) -> Result<Stream, Error> {
Err(ErrorKind::UnknownScheme.msg(&format!("unknown scheme '{}'", unit.url.scheme()))) Err(ErrorKind::UnknownScheme.msg(&format!("unknown scheme '{}'", unit.url.scheme())))
} }
#[cfg(not(feature = "tls"))]
pub(crate) fn connect_https(unit: &Unit, _hostname: &str) -> Result<Stream, Error> {
Err(ErrorKind::UnknownScheme
.msg("URL has 'https:' scheme but ureq was build without HTTP support")
.url(unit.url.clone()))
}

View File

@@ -10,7 +10,7 @@ fn content_length_on_str() {
let resp = post("test://host/content_length_on_str") let resp = post("test://host/content_length_on_str")
.send_string("Hello World!!!") .send_string("Hello World!!!")
.unwrap(); .unwrap();
let vec = resp.to_write_vec(); let vec = resp.as_write_vec();
let s = String::from_utf8_lossy(&vec); let s = String::from_utf8_lossy(&vec);
assert!(s.contains("\r\nContent-Length: 14\r\n")); assert!(s.contains("\r\nContent-Length: 14\r\n"));
} }
@@ -24,7 +24,7 @@ fn user_set_content_length_on_str() {
.set("Content-Length", "12345") .set("Content-Length", "12345")
.send_string("Hello World!!!") .send_string("Hello World!!!")
.unwrap(); .unwrap();
let vec = resp.to_write_vec(); let vec = resp.as_write_vec();
let s = String::from_utf8_lossy(&vec); let s = String::from_utf8_lossy(&vec);
assert!(s.contains("\r\nContent-Length: 12345\r\n")); assert!(s.contains("\r\nContent-Length: 12345\r\n"));
} }
@@ -43,7 +43,7 @@ fn content_length_on_json() {
let resp = post("test://host/content_length_on_json") let resp = post("test://host/content_length_on_json")
.send_json(SerdeValue::Object(json)) .send_json(SerdeValue::Object(json))
.unwrap(); .unwrap();
let vec = resp.to_write_vec(); let vec = resp.as_write_vec();
let s = String::from_utf8_lossy(&vec); let s = String::from_utf8_lossy(&vec);
assert!(s.contains("\r\nContent-Length: 20\r\n")); assert!(s.contains("\r\nContent-Length: 20\r\n"));
} }
@@ -57,7 +57,7 @@ fn content_length_and_chunked() {
.set("Transfer-Encoding", "chunked") .set("Transfer-Encoding", "chunked")
.send_string("Hello World!!!") .send_string("Hello World!!!")
.unwrap(); .unwrap();
let vec = resp.to_write_vec(); let vec = resp.as_write_vec();
let s = String::from_utf8_lossy(&vec); let s = String::from_utf8_lossy(&vec);
assert!(s.contains("Transfer-Encoding: chunked\r\n")); assert!(s.contains("Transfer-Encoding: chunked\r\n"));
assert!(!s.contains("\r\nContent-Length:\r\n")); assert!(!s.contains("\r\nContent-Length:\r\n"));
@@ -73,7 +73,7 @@ fn str_with_encoding() {
.set("Content-Type", "text/plain; charset=iso-8859-1") .set("Content-Type", "text/plain; charset=iso-8859-1")
.send_string("Hällo Wörld!!!") .send_string("Hällo Wörld!!!")
.unwrap(); .unwrap();
let vec = resp.to_write_vec(); let vec = resp.as_write_vec();
assert_eq!( assert_eq!(
&vec[vec.len() - 14..], &vec[vec.len() - 14..],
//H ä l l o _ W ö r l d ! ! ! //H ä l l o _ W ö r l d ! ! !
@@ -95,7 +95,7 @@ fn content_type_on_json() {
let resp = post("test://host/content_type_on_json") let resp = post("test://host/content_type_on_json")
.send_json(SerdeValue::Object(json)) .send_json(SerdeValue::Object(json))
.unwrap(); .unwrap();
let vec = resp.to_write_vec(); let vec = resp.as_write_vec();
let s = String::from_utf8_lossy(&vec); let s = String::from_utf8_lossy(&vec);
assert!(s.contains("\r\nContent-Type: application/json\r\n")); assert!(s.contains("\r\nContent-Type: application/json\r\n"));
} }
@@ -115,7 +115,7 @@ fn content_type_not_overriden_on_json() {
.set("content-type", "text/plain") .set("content-type", "text/plain")
.send_json(SerdeValue::Object(json)) .send_json(SerdeValue::Object(json))
.unwrap(); .unwrap();
let vec = resp.to_write_vec(); let vec = resp.as_write_vec();
let s = String::from_utf8_lossy(&vec); let s = String::from_utf8_lossy(&vec);
assert!(s.contains("\r\ncontent-type: text/plain\r\n")); assert!(s.contains("\r\ncontent-type: text/plain\r\n"));
} }

View File

@@ -8,7 +8,7 @@ fn no_query_string() {
test::make_response(200, "OK", vec![], vec![]) test::make_response(200, "OK", vec![], vec![])
}); });
let resp = get("test://host/no_query_string").call().unwrap(); let resp = get("test://host/no_query_string").call().unwrap();
let vec = resp.to_write_vec(); let vec = resp.as_write_vec();
let s = String::from_utf8_lossy(&vec); let s = String::from_utf8_lossy(&vec);
assert!(s.contains("GET /no_query_string HTTP/1.1")) assert!(s.contains("GET /no_query_string HTTP/1.1"))
} }
@@ -23,7 +23,7 @@ fn escaped_query_string() {
.query("baz", "yo lo") .query("baz", "yo lo")
.call() .call()
.unwrap(); .unwrap();
let vec = resp.to_write_vec(); let vec = resp.as_write_vec();
let s = String::from_utf8_lossy(&vec); let s = String::from_utf8_lossy(&vec);
assert!( assert!(
s.contains("GET /escaped_query_string?foo=bar&baz=yo+lo HTTP/1.1"), s.contains("GET /escaped_query_string?foo=bar&baz=yo+lo HTTP/1.1"),
@@ -38,7 +38,7 @@ fn query_in_path() {
test::make_response(200, "OK", vec![], vec![]) test::make_response(200, "OK", vec![], vec![])
}); });
let resp = get("test://host/query_in_path?foo=bar").call().unwrap(); let resp = get("test://host/query_in_path?foo=bar").call().unwrap();
let vec = resp.to_write_vec(); let vec = resp.as_write_vec();
let s = String::from_utf8_lossy(&vec); let s = String::from_utf8_lossy(&vec);
assert!(s.contains("GET /query_in_path?foo=bar HTTP/1.1")) assert!(s.contains("GET /query_in_path?foo=bar HTTP/1.1"))
} }
@@ -52,7 +52,7 @@ fn query_in_path_and_req() {
.query("baz", "1 2 3") .query("baz", "1 2 3")
.call() .call()
.unwrap(); .unwrap();
let vec = resp.to_write_vec(); let vec = resp.as_write_vec();
let s = String::from_utf8_lossy(&vec); let s = String::from_utf8_lossy(&vec);
assert!(s.contains("GET /query_in_path_and_req?foo=bar&baz=1+2+3 HTTP/1.1")) assert!(s.contains("GET /query_in_path_and_req?foo=bar&baz=1+2+3 HTTP/1.1"))
} }

View File

@@ -1,12 +1,11 @@
#[cfg(feature = "tls")]
use std::io::Read;
#[cfg(feature = "tls")]
use super::super::*;
#[test] #[test]
#[cfg(feature = "tls")] #[cfg(feature = "tls")]
fn read_range() { fn read_range_rustls() {
use std::io::Read;
use super::super::*;
// rustls is used via crate level convenience calls
let resp = get("https://ureq.s3.eu-central-1.amazonaws.com/sherlock.txt") let resp = get("https://ureq.s3.eu-central-1.amazonaws.com/sherlock.txt")
.set("Range", "bytes=1000-1999") .set("Range", "bytes=1000-1999")
.call() .call()
@@ -21,3 +20,30 @@ fn read_range() {
[83, 99, 111, 116, 116, 34, 10, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32] [83, 99, 111, 116, 116, 34, 10, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32]
) )
} }
#[test]
#[cfg(feature = "native-tls")]
fn read_range_native_tls() {
use std::io::Read;
use std::sync::Arc;
use super::super::*;
let tls_config = native_tls::TlsConnector::new().unwrap();
let agent = builder().tls_connector(Arc::new(tls_config)).build();
let resp = agent
.get("https://ureq.s3.eu-central-1.amazonaws.com/sherlock.txt")
.set("Range", "bytes=1000-1999")
.call()
.unwrap();
assert_eq!(resp.status(), 206);
let mut reader = resp.into_reader();
let mut buf = vec![];
let len = reader.read_to_end(&mut buf).unwrap();
assert_eq!(len, 1000);
assert_eq!(
&buf[0..20],
[83, 99, 111, 116, 116, 34, 10, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32]
)
}

View File

@@ -120,7 +120,7 @@ fn escape_path() {
test::make_response(200, "OK", vec![], vec![]) test::make_response(200, "OK", vec![], vec![])
}); });
let resp = get("test://host/escape_path here").call().unwrap(); let resp = get("test://host/escape_path here").call().unwrap();
let vec = resp.to_write_vec(); let vec = resp.as_write_vec();
let s = String::from_utf8_lossy(&vec); let s = String::from_utf8_lossy(&vec);
assert!(s.contains("GET /escape_path%20here HTTP/1.1")) assert!(s.contains("GET /escape_path%20here HTTP/1.1"))
} }
@@ -198,7 +198,7 @@ pub fn host_no_port() {
test::make_response(200, "OK", vec![], vec![]) test::make_response(200, "OK", vec![], vec![])
}); });
let resp = get("test://myhost/host_no_port").call().unwrap(); let resp = get("test://myhost/host_no_port").call().unwrap();
let vec = resp.to_write_vec(); let vec = resp.as_write_vec();
let s = String::from_utf8_lossy(&vec); let s = String::from_utf8_lossy(&vec);
assert!(s.contains("\r\nHost: myhost\r\n")); assert!(s.contains("\r\nHost: myhost\r\n"));
} }
@@ -209,7 +209,7 @@ pub fn host_with_port() {
test::make_response(200, "OK", vec![], vec![]) test::make_response(200, "OK", vec![], vec![])
}); });
let resp = get("test://myhost:234/host_with_port").call().unwrap(); let resp = get("test://myhost:234/host_with_port").call().unwrap();
let vec = resp.to_write_vec(); let vec = resp.as_write_vec();
let s = String::from_utf8_lossy(&vec); let s = String::from_utf8_lossy(&vec);
assert!(s.contains("\r\nHost: myhost:234\r\n")); assert!(s.contains("\r\nHost: myhost:234\r\n"));
} }

View File

@@ -4,11 +4,9 @@ set -eu
export RUST_BACKTRACE=1 export RUST_BACKTRACE=1
export RUSTFLAGS="-D dead_code -D unused-variables -D unused" export RUSTFLAGS="-D dead_code -D unused-variables -D unused"
for tls in "" tls ; do for feature in "" tls json charset cookies socks-proxy "tls native-certs" native-tls; do
for feature in "" json charset cookies socks-proxy native-certs ; do if ! cargo test --no-default-features --features "${feature}" ; then
if ! cargo test --no-default-features --features "${tls} ${feature}" ; then echo Command failed: cargo test --no-default-features --features \"${feature}\"
echo Command failed: cargo test \"${what}\" --no-default-features --features \"${tls} ${feature}\"
exit 1 exit 1
fi fi
done done
done

View File

@@ -1,5 +1,4 @@
#[cfg(feature = "tls")] #[cfg(all(feature = "json", any(feature = "tls", feature = "tls-native")))]
#[cfg(feature = "json")]
#[test] #[test]
fn agent_set_header() { fn agent_set_header() {
use serde::Deserialize; use serde::Deserialize;
@@ -23,9 +22,11 @@ fn agent_set_header() {
assert_eq!("value", json.headers.get("Header").unwrap()); assert_eq!("value", json.headers.get("Header").unwrap());
} }
#[test]
#[cfg(any(feature = "tls", feature = "tls-native"))]
// From here https://badssl.com/download/ // From here https://badssl.com/download/
// Decrypt key with: openssl rsa -in ./badssl.com-client.pem // Decrypt key with: openssl rsa -in ./badssl.com-client.pem
#[cfg(feature = "tls")] fn tls_client_certificate() {
const BADSSL_CLIENT_CERT_PEM: &str = r#"Bag Attributes const BADSSL_CLIENT_CERT_PEM: &str = r#"Bag Attributes
localKeyID: 41 C3 6C 33 C7 E3 36 DD EA 4A 1F C0 B7 23 B8 E6 9C DC D8 0F localKeyID: 41 C3 6C 33 C7 E3 36 DD EA 4A 1F C0 B7 23 B8 E6 9C DC D8 0F
subject=C = US, ST = California, L = San Francisco, O = BadSSL, CN = BadSSL Client Certificate subject=C = US, ST = California, L = San Francisco, O = BadSSL, CN = BadSSL Client Certificate
@@ -91,9 +92,6 @@ m0Wqhhi8/24Sy934t5Txgkfoltg8ahkx934WjP6WWRnSAu+cf+vW
-----END RSA PRIVATE KEY----- -----END RSA PRIVATE KEY-----
"#; "#;
#[cfg(feature = "tls")]
#[test]
fn tls_client_certificate() {
use ureq::OrAnyStatus; use ureq::OrAnyStatus;
let certs = rustls_pemfile::certs(&mut BADSSL_CLIENT_CERT_PEM.as_bytes()) let certs = rustls_pemfile::certs(&mut BADSSL_CLIENT_CERT_PEM.as_bytes())