diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index e4902a57..7f987166 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -10,6 +10,7 @@ jobs: - default - ssl-openssl - ssl-rustls + - ssl-native-tls steps: - uses: actions/checkout@v2 - name: Install stable toolchain @@ -38,11 +39,12 @@ jobs: rust: - stable - nightly - - 1.56 + - 1.57 features: - default - ssl-openssl - ssl-rustls + - ssl-native-tls steps: - uses: actions/checkout@v2 - name: Install toolchain diff --git a/Cargo.toml b/Cargo.toml index 76d30183..eb838704 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,12 +9,14 @@ keywords = ["http", "server", "web"] license = "MIT OR Apache-2.0" repository = "https://github.com/tiny-http/tiny-http" edition = "2018" +rust-version = "1.57" [features] default = ["log"] ssl = ["ssl-openssl"] ssl-openssl = ["openssl", "zeroize"] ssl-rustls = ["rustls", "rustls-pemfile", "zeroize"] +ssl-native-tls = ["native-tls", "zeroize"] [dependencies] ascii = "1.0" @@ -26,6 +28,7 @@ openssl = { version = "0.10", optional = true } rustls = { version = "0.20", optional = true } rustls-pemfile = { version = "0.2.1", optional = true } zeroize = { version = "1", optional = true } +native-tls = { version = "0.2", optional = true } [dev-dependencies] rustc-serialize = "0.3" diff --git a/README.md b/README.md index 0d437937..8e122e1b 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ What does **tiny-http** handle? - Accepting and managing connections to the clients - Parsing requests - Requests pipelining - - HTTPS (using either OpenSSL or Rustls) + - HTTPS (using either OpenSSL, Rustls or native-tls) - Transfer-Encoding and Content-Encoding - Turning user input (eg. POST input) into a contiguous UTF-8 string (**not implemented yet**) - Ranges (**not implemented yet**) diff --git a/examples/ssl.rs b/examples/ssl.rs index eef2c052..0379d10a 100644 --- a/examples/ssl.rs +++ b/examples/ssl.rs @@ -1,11 +1,19 @@ extern crate tiny_http; -#[cfg(not(any(feature = "ssl-openssl", feature = "ssl-rustls")))] +#[cfg(not(any( + feature = "ssl-openssl", + feature = "ssl-rustls", + feature = "ssl-native-tls" +)))] fn main() { println!("This example requires one of the supported `ssl-*` features to be enabled"); } -#[cfg(any(feature = "ssl-openssl", feature = "ssl-rustls"))] +#[cfg(any( + feature = "ssl-openssl", + feature = "ssl-rustls", + feature = "ssl-native-tls" +))] fn main() { use tiny_http::{Response, Server}; diff --git a/src/lib.rs b/src/lib.rs index a04116da..f223dd23 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -91,7 +91,11 @@ #![deny(rust_2018_idioms)] #![allow(clippy::match_like_matches_macro)] -#[cfg(any(feature = "ssl-openssl", feature = "ssl-rustls"))] +#[cfg(any( + feature = "ssl-openssl", + feature = "ssl-rustls", + feature = "ssl-native-tls" +))] use zeroize::Zeroizing; use std::error::Error; @@ -203,7 +207,11 @@ impl Server { } /// Shortcut for an HTTPS server on a specific address. - #[cfg(any(feature = "ssl-openssl", feature = "ssl-rustls"))] + #[cfg(any( + feature = "ssl-openssl", + feature = "ssl-rustls", + feature = "ssl-native-tls" + ))] #[inline] pub fn https( addr: A, @@ -256,22 +264,42 @@ impl Server { }; // building the SSL capabilities - #[cfg(all(feature = "ssl-openssl", feature = "ssl-rustls"))] + #[cfg(any( + all(feature = "ssl-openssl", feature = "ssl-rustls"), + all(feature = "ssl-openssl", feature = "ssl-native-tls"), + all(feature = "ssl-native-tls", feature = "ssl-rustls"), + ))] compile_error!( - "Features 'ssl-openssl' and 'ssl-rustls' must not be enabled at the same time" + "Only one feature from 'ssl-openssl', 'ssl-rustls', 'ssl-native-tls' can be enabled at the same time" ); - #[cfg(not(any(feature = "ssl-openssl", feature = "ssl-rustls")))] + #[cfg(not(any( + feature = "ssl-openssl", + feature = "ssl-rustls", + feature = "ssl-native-tls" + )))] type SslContext = (); - #[cfg(any(feature = "ssl-openssl", feature = "ssl-rustls"))] + #[cfg(any( + feature = "ssl-openssl", + feature = "ssl-rustls", + feature = "ssl-native-tls" + ))] type SslContext = crate::ssl::SslContextImpl; let ssl: Option = { match ssl_config { - #[cfg(any(feature = "ssl-openssl", feature = "ssl-rustls"))] + #[cfg(any( + feature = "ssl-openssl", + feature = "ssl-rustls", + feature = "ssl-native-tls" + ))] Some(config) => Some(SslContext::from_pem( config.certificate, Zeroizing::new(config.private_key), )?), - #[cfg(not(any(feature = "ssl-openssl", feature = "ssl-rustls")))] + #[cfg(not(any( + feature = "ssl-openssl", + feature = "ssl-rustls", + feature = "ssl-native-tls" + )))] Some(_) => return Err( "Building a server with SSL requires enabling the `ssl` feature in tiny-http" .into(), @@ -297,7 +325,11 @@ impl Server { use util::RefinedTcpStream; let (read_closable, write_closable) = match ssl { None => RefinedTcpStream::new(sock), - #[cfg(any(feature = "ssl-openssl", feature = "ssl-rustls"))] + #[cfg(any( + feature = "ssl-openssl", + feature = "ssl-rustls", + feature = "ssl-native-tls" + ))] Some(ref ssl) => { // trying to apply SSL over the connection // if an error occurs, we just close the socket and resume listening @@ -308,7 +340,11 @@ impl Server { RefinedTcpStream::new(sock) } - #[cfg(not(any(feature = "ssl-openssl", feature = "ssl-rustls")))] + #[cfg(not(any( + feature = "ssl-openssl", + feature = "ssl-rustls", + feature = "ssl-native-tls" + )))] Some(ref _ssl) => unreachable!(), }; diff --git a/src/ssl.rs b/src/ssl.rs index d4a8f1b9..b77b923b 100644 --- a/src/ssl.rs +++ b/src/ssl.rs @@ -18,3 +18,10 @@ pub(crate) mod rustls; pub(crate) use self::rustls::RustlsContext as SslContextImpl; #[cfg(feature = "ssl-rustls")] pub(crate) use self::rustls::RustlsStream as SslStream; + +#[cfg(feature = "ssl-native-tls")] +pub(crate) mod native_tls; +#[cfg(feature = "ssl-native-tls")] +pub(crate) use self::native_tls::NativeTlsContext as SslContextImpl; +#[cfg(feature = "ssl-native-tls")] +pub(crate) use self::native_tls::NativeTlsStream as SslStream; diff --git a/src/ssl/native_tls.rs b/src/ssl/native_tls.rs new file mode 100644 index 00000000..84f02ea2 --- /dev/null +++ b/src/ssl/native_tls.rs @@ -0,0 +1,84 @@ +use crate::connection::Connection; +use crate::util::refined_tcp_stream::Stream as RefinedStream; +use std::error::Error; +use std::io::{Read, Write}; +use std::net::{Shutdown, SocketAddr}; +use std::sync::{Arc, Mutex}; +use zeroize::Zeroizing; + +/// A wrapper around a `native_tls` stream. +/// +/// Uses an internal Mutex to permit disparate reader & writer threads to access the stream independently. +#[derive(Clone)] +pub(crate) struct NativeTlsStream(Arc>>); + +// These struct methods form the implict contract for swappable TLS implementations +impl NativeTlsStream { + pub(crate) fn peer_addr(&mut self) -> std::io::Result> { + self.0 + .lock() + .expect("Failed to lock SSL stream mutex") + .get_mut() + .peer_addr() + } + + pub(crate) fn shutdown(&mut self, how: Shutdown) -> std::io::Result<()> { + self.0 + .lock() + .expect("Failed to lock SSL stream mutex") + .get_mut() + .shutdown(how) + } +} + +impl Read for NativeTlsStream { + fn read(&mut self, buf: &mut [u8]) -> std::io::Result { + self.0 + .lock() + .expect("Failed to lock SSL stream mutex") + .read(buf) + } +} + +impl Write for NativeTlsStream { + fn write(&mut self, buf: &[u8]) -> std::io::Result { + self.0 + .lock() + .expect("Failed to lock SSL stream mutex") + .write(buf) + } + + fn flush(&mut self) -> std::io::Result<()> { + self.0 + .lock() + .expect("Failed to lock SSL stream mutex") + .flush() + } +} + +pub(crate) struct NativeTlsContext(native_tls::TlsAcceptor); + +impl NativeTlsContext { + pub fn from_pem( + certificates: Vec, + private_key: Zeroizing>, + ) -> Result> { + let identity = native_tls::Identity::from_pkcs8(&certificates, &private_key)?; + let acceptor = native_tls::TlsAcceptor::new(identity)?; + Ok(Self(acceptor)) + } + + pub fn accept( + &self, + stream: Connection, + ) -> Result> { + let stream = self.0.accept(stream)?; + Ok(NativeTlsStream(Arc::new(Mutex::new(stream)))) + } +} + +impl From for RefinedStream { + fn from(stream: NativeTlsStream) -> Self { + RefinedStream::Https(stream) + } +} diff --git a/src/util/refined_tcp_stream.rs b/src/util/refined_tcp_stream.rs index 875fbe2f..88cbe1e4 100644 --- a/src/util/refined_tcp_stream.rs +++ b/src/util/refined_tcp_stream.rs @@ -3,12 +3,20 @@ use std::io::{Read, Write}; use std::net::{Shutdown, SocketAddr}; use crate::connection::Connection; -#[cfg(any(feature = "ssl-openssl", feature = "ssl-rustls"))] +#[cfg(any( + feature = "ssl-openssl", + feature = "ssl-rustls", + feature = "ssl-native-tls" +))] use crate::ssl::SslStream; pub(crate) enum Stream { Http(Connection), - #[cfg(any(feature = "ssl-openssl", feature = "ssl-rustls"))] + #[cfg(any( + feature = "ssl-openssl", + feature = "ssl-rustls", + feature = "ssl-native-tls" + ))] Https(SslStream), } @@ -16,7 +24,11 @@ impl Clone for Stream { fn clone(&self) -> Self { match self { Stream::Http(tcp_stream) => Stream::Http(tcp_stream.try_clone().unwrap()), - #[cfg(any(feature = "ssl-openssl", feature = "ssl-rustls"))] + #[cfg(any( + feature = "ssl-openssl", + feature = "ssl-rustls", + feature = "ssl-native-tls" + ))] Stream::Https(ssl_stream) => Stream::Https(ssl_stream.clone()), } } @@ -32,7 +44,11 @@ impl Stream { fn secure(&self) -> bool { match self { Stream::Http(_) => false, - #[cfg(any(feature = "ssl-openssl", feature = "ssl-rustls"))] + #[cfg(any( + feature = "ssl-openssl", + feature = "ssl-rustls", + feature = "ssl-native-tls" + ))] Stream::Https(_) => true, } } @@ -40,7 +56,11 @@ impl Stream { fn peer_addr(&mut self) -> IoResult> { match self { Stream::Http(tcp_stream) => tcp_stream.peer_addr(), - #[cfg(any(feature = "ssl-openssl", feature = "ssl-rustls"))] + #[cfg(any( + feature = "ssl-openssl", + feature = "ssl-rustls", + feature = "ssl-native-tls" + ))] Stream::Https(ssl_stream) => ssl_stream.peer_addr(), } } @@ -48,7 +68,11 @@ impl Stream { fn shutdown(&mut self, how: Shutdown) -> IoResult<()> { match self { Stream::Http(tcp_stream) => tcp_stream.shutdown(how), - #[cfg(any(feature = "ssl-openssl", feature = "ssl-rustls"))] + #[cfg(any( + feature = "ssl-openssl", + feature = "ssl-rustls", + feature = "ssl-native-tls" + ))] Stream::Https(ssl_stream) => ssl_stream.shutdown(how), } } @@ -58,7 +82,11 @@ impl Read for Stream { fn read(&mut self, buf: &mut [u8]) -> IoResult { match self { Stream::Http(tcp_stream) => tcp_stream.read(buf), - #[cfg(any(feature = "ssl-openssl", feature = "ssl-rustls"))] + #[cfg(any( + feature = "ssl-openssl", + feature = "ssl-rustls", + feature = "ssl-native-tls" + ))] Stream::Https(ssl_stream) => ssl_stream.read(buf), } } @@ -68,7 +96,11 @@ impl Write for Stream { fn write(&mut self, buf: &[u8]) -> IoResult { match self { Stream::Http(tcp_stream) => tcp_stream.write(buf), - #[cfg(any(feature = "ssl-openssl", feature = "ssl-rustls"))] + #[cfg(any( + feature = "ssl-openssl", + feature = "ssl-rustls", + feature = "ssl-native-tls" + ))] Stream::Https(ssl_stream) => ssl_stream.write(buf), } } @@ -76,7 +108,11 @@ impl Write for Stream { fn flush(&mut self) -> IoResult<()> { match self { Stream::Http(tcp_stream) => tcp_stream.flush(), - #[cfg(any(feature = "ssl-openssl", feature = "ssl-rustls"))] + #[cfg(any( + feature = "ssl-openssl", + feature = "ssl-rustls", + feature = "ssl-native-tls" + ))] Stream::Https(ssl_stream) => ssl_stream.flush(), } }