From 5cbaf3a1b86a271cca1a771b181809523fb73846 Mon Sep 17 00:00:00 2001 From: Nikolay Kim Date: Tue, 30 Jan 2018 11:17:17 -0800 Subject: [PATCH] add client ssl support --- examples/tls/Cargo.toml | 2 +- examples/websocket/Cargo.toml | 1 + src/client/connect.rs | 142 ++++++++++++++++++++++++++++++++-- src/client/mod.rs | 1 + src/lib.rs | 14 +--- src/ws/client.rs | 17 +++- 6 files changed, 157 insertions(+), 20 deletions(-) diff --git a/examples/tls/Cargo.toml b/examples/tls/Cargo.toml index 4d227c7c..baba01f9 100644 --- a/examples/tls/Cargo.toml +++ b/examples/tls/Cargo.toml @@ -12,4 +12,4 @@ path = "src/main.rs" env_logger = "0.5" actix = "^0.4.2" actix-web = { path = "../../", features=["alpn"] } -openssl = { version="0.10", features = ["v110"] } +openssl = { version="0.10" } diff --git a/examples/websocket/Cargo.toml b/examples/websocket/Cargo.toml index fd7b683d..4b5de2cd 100644 --- a/examples/websocket/Cargo.toml +++ b/examples/websocket/Cargo.toml @@ -2,6 +2,7 @@ name = "websocket" version = "0.1.0" authors = ["Nikolay Kim "] +workspace = "../.." [[bin]] name = "server" diff --git a/src/client/connect.rs b/src/client/connect.rs index 1e2432b0..6d071813 100644 --- a/src/client/connect.rs +++ b/src/client/connect.rs @@ -6,19 +6,33 @@ use std::time::Duration; use actix::{fut, Actor, ActorFuture, Arbiter, ArbiterService, Context, Handler, Response, ResponseType, Supervised}; +use actix::fut::WrapFuture; use actix::actors::{Connector, ConnectorError, Connect as ResolveConnect}; -use http::Uri; +use http::{Uri, HttpTryFrom, Error as HttpError}; use futures::{Async, Future, Poll}; use tokio_core::reactor::Timeout; use tokio_core::net::{TcpStream, TcpStreamNew}; use tokio_io::{AsyncRead, AsyncWrite}; +#[cfg(feature="alpn")] +use openssl::ssl::{SslMethod, SslConnector, SslVerifyMode, Error as OpensslError}; +#[cfg(feature="alpn")] +use tokio_openssl::SslConnectorExt; + +use HAS_OPENSSL; use server::IoStream; + #[derive(Debug)] pub struct Connect(pub Uri); +impl Connect { + pub fn new(uri: U) -> Result where Uri: HttpTryFrom { + Ok(Connect(Uri::try_from(uri).map_err(|e| e.into())?)) + } +} + impl ResponseType for Connect { type Item = Connection; type Error = ClientConnectorError; @@ -34,6 +48,11 @@ pub enum ClientConnectorError { #[fail(display="SSL is not supported")] SslIsNotSupported, + /// SSL error + #[cfg(feature="alpn")] + #[fail(display="{}", _0)] + SslError(OpensslError), + /// Connection error #[fail(display = "{}", _0)] Connector(ConnectorError), @@ -57,8 +76,9 @@ impl From for ClientConnectorError { } } -#[derive(Debug, Default)] pub struct ClientConnector { + #[cfg(feature="alpn")] + connector: SslConnector, } impl Actor for ClientConnector { @@ -69,16 +89,86 @@ impl Supervised for ClientConnector {} impl ArbiterService for ClientConnector {} +impl Default for ClientConnector { + fn default() -> ClientConnector { + #[cfg(feature="alpn")] + { + let mut builder = SslConnector::builder(SslMethod::tls()).unwrap(); + builder.set_verify(SslVerifyMode::NONE); + ClientConnector { + connector: builder.build() + } + } + + #[cfg(not(feature="alpn"))] + ClientConnector {} + } +} + +impl ClientConnector { + + #[cfg(feature="alpn")] + /// Create `ClientConnector` actor with custom `SslConnector` instance. + /// + /// By default `ClientConnector` uses very simple ssl configuration. + /// With `with_connector` method it is possible to use custom `SslConnector` + /// object. + /// + /// ```rust + /// # #![cfg(feature="alpn")] + /// # extern crate actix; + /// # extern crate actix_web; + /// # extern crate futures; + /// # use futures::Future; + /// # use std::io::Write; + /// extern crate openssl; + /// use actix::prelude::*; + /// use actix_web::client::{Connect, ClientConnector}; + /// + /// use openssl::ssl::{SslMethod, SslConnector}; + /// + /// fn main() { + /// let sys = System::new("test"); + /// + /// // Start `ClientConnector` with custom `SslConnector` + /// let ssl_conn = SslConnector::builder(SslMethod::tls()).unwrap().build(); + /// let conn: Address<_> = ClientConnector::with_connector(ssl_conn).start(); + /// + /// Arbiter::handle().spawn({ + /// conn.call_fut( + /// Connect::new("https://www.rust-lang.org").unwrap()) // <- connecto to host + /// .map_err(|_| ()) + /// .and_then(|res| { + /// if let Ok(mut stream) = res { + /// stream.write_all(b"GET / HTTP/1.0\r\n\r\n").unwrap(); + /// } + /// # Arbiter::system().send(actix::msgs::SystemExit(0)); + /// Ok(()) + /// }) + /// }); + /// + /// sys.run(); + /// } + /// ``` + pub fn with_connector(connector: SslConnector) -> ClientConnector { + ClientConnector { + connector: connector + } + } +} + impl Handler for ClientConnector { type Result = Response; fn handle(&mut self, msg: Connect, _: &mut Self::Context) -> Self::Result { let uri = &msg.0; + // host name is required if uri.host().is_none() { return Self::reply(Err(ClientConnectorError::InvalidUrl)) } + // supported protocols let proto = match uri.scheme_part() { Some(scheme) => match Protocol::from(scheme.as_str()) { Some(proto) => proto, @@ -87,20 +177,51 @@ impl Handler for ClientConnector { None => return Self::reply(Err(ClientConnectorError::InvalidUrl)), }; + // check ssl availability + if proto.is_secure() && !HAS_OPENSSL { //&& !HAS_TLS { + return Self::reply(Err(ClientConnectorError::SslIsNotSupported)) + } + + let host = uri.host().unwrap().to_owned(); let port = uri.port().unwrap_or_else(|| proto.port()); Self::async_reply( Connector::from_registry() - .call(self, ResolveConnect::host_and_port(uri.host().unwrap(), port)) + .call(self, ResolveConnect::host_and_port(&host, port)) .map_err(|_, _, _| ClientConnectorError::Disconnected) - .and_then(|res, _, _| match res { - Ok(stream) => fut::ok(Connection{stream: Box::new(stream)}), - Err(err) => fut::err(err.into()) + .and_then(move |res, _act, _| { + #[cfg(feature="alpn")] + match res { + Err(err) => fut::Either::B(fut::err(err.into())), + Ok(stream) => { + if proto.is_secure() { + fut::Either::A( + _act.connector.connect_async(&host, stream) + .map_err(|e| ClientConnectorError::SslError(e)) + .map(|stream| Connection{stream: Box::new(stream)}) + .into_actor(_act)) + } else { + fut::Either::B(fut::ok(Connection{stream: Box::new(stream)})) + } + } + } + + #[cfg(not(feature="alpn"))] + match res { + Err(err) => fut::err(err.into()), + Ok(stream) => { + if proto.is_secure() { + fut::err(ClientConnectorError::SslIsNotSupported) + } else { + fut::ok(Connection{stream: Box::new(stream)}) + } + } + } })) } } -#[derive(PartialEq, Hash, Debug)] +#[derive(PartialEq, Hash, Debug, Clone, Copy)] enum Protocol { Http, Https, @@ -119,6 +240,13 @@ impl Protocol { } } + fn is_secure(&self) -> bool { + match *self { + Protocol::Https | Protocol::Wss => true, + _ => false, + } + } + fn port(&self) -> u16 { match *self { Protocol::Http | Protocol::Ws => 80, diff --git a/src/client/mod.rs b/src/client/mod.rs index 3bd96f64..ba5a88f9 100644 --- a/src/client/mod.rs +++ b/src/client/mod.rs @@ -8,3 +8,4 @@ pub(crate) use self::writer::HttpClientWriter; pub use self::request::{ClientRequest, ClientRequestBuilder}; pub use self::response::ClientResponse; pub use self::parser::{HttpResponseParser, HttpResponseParserError}; +pub use self::connect::{Connect, Connection, ClientConnector, ClientConnectorError}; diff --git a/src/lib.rs b/src/lib.rs index 8194793f..c42bef71 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -111,9 +111,7 @@ mod resource; mod handler; mod pipeline; -#[doc(hidden)] pub mod client; - pub mod fs; pub mod ws; pub mod error; @@ -143,19 +141,15 @@ pub use http::{Method, StatusCode, Version}; #[cfg(feature="tls")] pub use native_tls::Pkcs12; -#[doc(hidden)] -#[cfg(feature="openssl")] -pub use openssl::pkcs12::Pkcs12; - #[cfg(feature="openssl")] pub(crate) const HAS_OPENSSL: bool = true; #[cfg(not(feature="openssl"))] pub(crate) const HAS_OPENSSL: bool = false; -#[cfg(feature="tls")] -pub(crate) const HAS_TLS: bool = true; -#[cfg(not(feature="tls"))] -pub(crate) const HAS_TLS: bool = false; +// #[cfg(feature="tls")] +// pub(crate) const HAS_TLS: bool = true; +// #[cfg(not(feature="tls"))] +// pub(crate) const HAS_TLS: bool = false; pub mod headers { diff --git a/src/ws/client.rs b/src/ws/client.rs index 83c912f4..58517fe1 100644 --- a/src/ws/client.rs +++ b/src/ws/client.rs @@ -99,21 +99,31 @@ pub struct WsClient { http_err: Option, origin: Option, protocols: Option, + conn: Address, } impl WsClient { + /// Create new websocket connection pub fn new>(uri: S) -> WsClient { + WsClient::with_connector(uri, ClientConnector::from_registry()) + } + + /// Create new websocket connection with custom `ClientConnector` + pub fn with_connector>(uri: S, conn: Address) -> WsClient { let mut cl = WsClient { request: ClientRequest::build(), err: None, http_err: None, origin: None, - protocols: None }; + protocols: None, + conn: conn, + }; cl.request.uri(uri.as_ref()); cl } + /// Set supported websocket protocols pub fn protocols(&mut self, protos: U) -> &mut Self where U: IntoIterator + 'static, V: AsRef @@ -125,6 +135,7 @@ impl WsClient { self } + /// Set cookie for handshake request pub fn cookie<'c>(&mut self, cookie: Cookie<'c>) -> &mut Self { self.request.cookie(cookie); self @@ -141,6 +152,7 @@ impl WsClient { self } + /// Set request header pub fn header(&mut self, key: K, value: V) -> &mut Self where HeaderName: HttpTryFrom, HeaderValue: HttpTryFrom { @@ -148,6 +160,7 @@ impl WsClient { self } + /// Connect to websocket server and do ws handshake pub fn connect(&mut self) -> Result, WsClientError> { if let Some(e) = self.err.take() { return Err(e) @@ -183,7 +196,7 @@ impl WsClient { // get connection and start handshake Ok(Box::new( - ClientConnector::from_registry().call_fut(Connect(request.uri().clone())) + self.conn.call_fut(Connect(request.uri().clone())) .map_err(|_| WsClientError::Disconnected) .and_then(|res| match res { Ok(stream) => Either::A(WsHandshake::new(stream, request)),