├── .github ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md └── workflows │ └── ci.yaml ├── .gitignore ├── Cargo.toml ├── README.md ├── examples └── hello_tls.rs └── src ├── custom_tls_acceptor.rs ├── lib.rs ├── tcp_connection.rs ├── tls_listener.rs ├── tls_listener_builder.rs ├── tls_listener_config.rs └── tls_stream_wrapper.rs /.github/CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | 2 | # Contributor Covenant Code of Conduct 3 | 4 | ## Our Pledge 5 | 6 | We as members, contributors, and leaders pledge to make participation in our 7 | community a harassment-free experience for everyone, regardless of age, body 8 | size, visible or invisible disability, ethnicity, sex characteristics, gender 9 | identity and expression, level of experience, education, socio-economic status, 10 | nationality, personal appearance, race, religion, or sexual identity 11 | and orientation. 12 | 13 | We pledge to act and interact in ways that contribute to an open, welcoming, 14 | diverse, inclusive, and healthy community. 15 | 16 | ## Our Standards 17 | 18 | Examples of behavior that contributes to a positive environment for our 19 | community include: 20 | 21 | * Demonstrating empathy and kindness toward other people 22 | * Being respectful of differing opinions, viewpoints, and experiences 23 | * Giving and gracefully accepting constructive feedback 24 | * Accepting responsibility and apologizing to those affected by our mistakes, 25 | and learning from the experience 26 | * Focusing on what is best not just for us as individuals, but for the 27 | overall community 28 | 29 | Examples of unacceptable behavior include: 30 | 31 | * The use of sexualized language or imagery, and sexual attention or 32 | advances of any kind 33 | * Trolling, insulting or derogatory comments, and personal or political attacks 34 | * Public or private harassment 35 | * Publishing others' private information, such as a physical or email 36 | address, without their explicit permission 37 | * Other conduct which could reasonably be considered inappropriate in a 38 | professional setting 39 | 40 | ## Enforcement Responsibilities 41 | 42 | Community leaders are responsible for clarifying and enforcing our standards of 43 | acceptable behavior and will take appropriate and fair corrective action in 44 | response to any behavior that they deem inappropriate, threatening, offensive, 45 | or harmful. 46 | 47 | Community leaders have the right and responsibility to remove, edit, or reject 48 | comments, commits, code, wiki edits, issues, and other contributions that are 49 | not aligned to this Code of Conduct, and will communicate reasons for moderation 50 | decisions when appropriate. 51 | 52 | ## Scope 53 | 54 | This Code of Conduct applies within all community spaces, and also applies when 55 | an individual is officially representing the community in public spaces. 56 | Examples of representing our community include using an official e-mail address, 57 | posting via an official social media account, or acting as an appointed 58 | representative at an online or offline event. 59 | 60 | ## Enforcement 61 | 62 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 63 | reported to the community leaders responsible for enforcement at 64 | [INSERT CONTACT METHOD]. 65 | All complaints will be reviewed and investigated promptly and fairly. 66 | 67 | All community leaders are obligated to respect the privacy and security of the 68 | reporter of any incident. 69 | 70 | ## Enforcement Guidelines 71 | 72 | Community leaders will follow these Community Impact Guidelines in determining 73 | the consequences for any action they deem in violation of this Code of Conduct: 74 | 75 | ### 1. Correction 76 | 77 | **Community Impact**: Use of inappropriate language or other behavior deemed 78 | unprofessional or unwelcome in the community. 79 | 80 | **Consequence**: A private, written warning from community leaders, providing 81 | clarity around the nature of the violation and an explanation of why the 82 | behavior was inappropriate. A public apology may be requested. 83 | 84 | ### 2. Warning 85 | 86 | **Community Impact**: A violation through a single incident or series 87 | of actions. 88 | 89 | **Consequence**: A warning with consequences for continued behavior. No 90 | interaction with the people involved, including unsolicited interaction with 91 | those enforcing the Code of Conduct, for a specified period of time. This 92 | includes avoiding interactions in community spaces as well as external channels 93 | like social media. Violating these terms may lead to a temporary or 94 | permanent ban. 95 | 96 | ### 3. Temporary Ban 97 | 98 | **Community Impact**: A serious violation of community standards, including 99 | sustained inappropriate behavior. 100 | 101 | **Consequence**: A temporary ban from any sort of interaction or public 102 | communication with the community for a specified period of time. No public or 103 | private interaction with the people involved, including unsolicited interaction 104 | with those enforcing the Code of Conduct, is allowed during this period. 105 | Violating these terms may lead to a permanent ban. 106 | 107 | ### 4. Permanent Ban 108 | 109 | **Community Impact**: Demonstrating a pattern of violation of community 110 | standards, including sustained inappropriate behavior, harassment of an 111 | individual, or aggression toward or disparagement of classes of individuals. 112 | 113 | **Consequence**: A permanent ban from any sort of public interaction within 114 | the community. 115 | 116 | ## Attribution 117 | 118 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 119 | version 2.0, available at 120 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 121 | 122 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 123 | enforcement ladder](https://github.com/mozilla/diversity). 124 | 125 | [homepage]: https://www.contributor-covenant.org 126 | 127 | For answers to common questions about this code of conduct, see the FAQ at 128 | https://www.contributor-covenant.org/faq. Translations are available at 129 | https://www.contributor-covenant.org/translations. 130 | -------------------------------------------------------------------------------- /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | Contributions include code, documentation, answering user questions, running the 3 | project's infrastructure, and advocating for all types of users. 4 | 5 | The project welcomes all contributions from anyone willing to work in good faith 6 | with other contributors and the community. No contribution is too small and all 7 | contributions are valued. 8 | 9 | This guide explains the process for contributing to the project's GitHub 10 | Repository. 11 | 12 | - [Code of Conduct](#code-of-conduct) 13 | - [Bad Actors](#bad-actors) 14 | 15 | ## Code of Conduct 16 | The project has a [Code of Conduct](./CODE_OF_CONDUCT.md) that *all* 17 | contributors are expected to follow. This code describes the *minimum* behavior 18 | expectations for all contributors. 19 | 20 | As a contributor, how you choose to act and interact towards your 21 | fellow contributors, as well as to the community, will reflect back not only 22 | on yourself but on the project as a whole. The Code of Conduct is designed and 23 | intended, above all else, to help establish a culture within the project that 24 | allows anyone and everyone who wants to contribute to feel safe doing so. 25 | 26 | Should any individual act in any way that is considered in violation of the 27 | [Code of Conduct](./CODE_OF_CONDUCT.md), corrective actions will be taken. It is 28 | possible, however, for any individual to *act* in such a manner that is not in 29 | violation of the strict letter of the Code of Conduct guidelines while still 30 | going completely against the spirit of what that Code is intended to accomplish. 31 | 32 | Open, diverse, and inclusive communities live and die on the basis of trust. 33 | Contributors can disagree with one another so long as they trust that those 34 | disagreements are in good faith and everyone is working towards a common 35 | goal. 36 | 37 | ## Bad Actors 38 | All contributors to tacitly agree to abide by both the letter and 39 | spirit of the [Code of Conduct](./CODE_OF_CONDUCT.md). Failure, or 40 | unwillingness, to do so will result in contributions being respectfully 41 | declined. 42 | 43 | A *bad actor* is someone who repeatedly violates the *spirit* of the Code of 44 | Conduct through consistent failure to self-regulate the way in which they 45 | interact with other contributors in the project. In doing so, bad actors 46 | alienate other contributors, discourage collaboration, and generally reflect 47 | poorly on the project as a whole. 48 | 49 | Being a bad actor may be intentional or unintentional. Typically, unintentional 50 | bad behavior can be easily corrected by being quick to apologize and correct 51 | course *even if you are not entirely convinced you need to*. Giving other 52 | contributors the benefit of the doubt and having a sincere willingness to admit 53 | that you *might* be wrong is critical for any successful open collaboration. 54 | 55 | Don't be a bad actor. 56 | -------------------------------------------------------------------------------- /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: 7 | - main 8 | 9 | env: 10 | RUSTFLAGS: -Dwarnings 11 | 12 | jobs: 13 | build_and_test: 14 | name: Build and test 15 | runs-on: ${{ matrix.os }} 16 | strategy: 17 | matrix: 18 | os: [ubuntu-latest, windows-latest, macOS-latest] 19 | rust: [stable, nightly] 20 | 21 | steps: 22 | - uses: actions/checkout@main 23 | 24 | - name: Install ${{ matrix.rust }} 25 | uses: actions-rs/toolchain@v1 26 | with: 27 | toolchain: ${{ matrix.rust }} 28 | override: true 29 | 30 | - name: check 31 | uses: actions-rs/cargo@v1 32 | with: 33 | command: check 34 | args: --all --bins --examples 35 | 36 | - name: check avoid-dev-deps 37 | uses: actions-rs/cargo@v1 38 | if: matrix.rust == 'nightly' 39 | with: 40 | command: check 41 | args: --all -Z avoid-dev-deps 42 | 43 | - name: tests 44 | uses: actions-rs/cargo@v1 45 | with: 46 | command: test 47 | args: --all 48 | 49 | check_fmt_and_docs: 50 | name: Checking fmt, clippy, and docs 51 | runs-on: ubuntu-latest 52 | steps: 53 | - uses: actions/checkout@main 54 | 55 | - uses: actions-rs/toolchain@v1 56 | with: 57 | toolchain: stable 58 | override: true 59 | 60 | - name: setup 61 | run: | 62 | rustup component add clippy rustfmt 63 | rustc --version 64 | 65 | - name: clippy 66 | run: cargo clippy -- -D warnings 67 | 68 | - name: fmt 69 | run: cargo fmt --all -- --check 70 | 71 | - name: Docs 72 | run: cargo doc --no-deps 73 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | **.pem 3 | Cargo.lock 4 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "tide-rustls" 3 | version = "0.3.0" 4 | authors = ["Jacob Rothstein "] 5 | edition = "2018" 6 | description = "tide tls listener based on async-rustls and rustls" 7 | readme = "README.md" 8 | repository = "https://github.com/jbr/tide-rustls" 9 | documentation = "https://docs.rs/tide-rustls" 10 | license = "MIT OR Apache-2.0" 11 | keywords = ["tide", "https", "tls"] 12 | categories = ["web-programming::http-server", "web-programming"] 13 | 14 | [dependencies] 15 | async-std = "1.9.0" 16 | tide = { version = "0.16.0", default-features = false } 17 | async-rustls = "0.2.0" 18 | rustls = "0.19.0" 19 | async-h1 = "2.3.2" 20 | async-dup = "1.2.2" 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Tide rustls listener 2 | 3 | ## tls listener for [tide](https://github.com/http-rs/tide) based on [async-rustls](https://github.com/smol-rs/async-rustls) 4 | 5 | * [CI ![CI][ci-badge]][ci] 6 | * [API Docs][docs] [![docs.rs docs][docs-badge]][docs] 7 | * [Releases][releases] [![crates.io version][version-badge]][lib-rs] 8 | 9 | [ci]: https://github.com/http-rs/tide-rustls/actions?query=workflow%3ACI 10 | [ci-badge]: https://github.com/http-rs/tide-rustls/workflows/CI/badge.svg 11 | [releases]: https://github.com/http-rs/tide-rustls/releases 12 | [docs]: https://docs.rs/tide-rustls 13 | [lib-rs]: https://lib.rs/tide-rustls 14 | [docs-badge]: https://img.shields.io/badge/docs-latest-blue.svg?style=flat-square 15 | [version-badge]: https://img.shields.io/crates/v/tide-rustls.svg?style=flat-square 16 | 17 | ## Installation 18 | ```sh 19 | $ cargo add tide-rustls 20 | ``` 21 | 22 | ## Using with tide 23 | ```rust 24 | #[async_std::main] 25 | async fn main() -> tide::Result<()> { 26 | let mut app = tide::new(); 27 | app.at("/").get(|_| async { Ok("Hello TLS") }); 28 | app.listen( 29 | TlsListener::build() 30 | .addrs("localhost:4433") 31 | .cert(std::env::var("TIDE_CERT_PATH").unwrap()) 32 | .key(std::env::var("TIDE_KEY_PATH").unwrap()), 33 | ) 34 | .await?; 35 | Ok(()) 36 | } 37 | ``` 38 | 39 | ## Safety 40 | This crate uses ``#![deny(unsafe_code)]`` to ensure everything is implemented in 41 | 100% Safe Rust. 42 | 43 | ## License 44 | 45 | 46 | Licensed under either of Apache License, Version 47 | 2.0 or MIT license at your option. 48 | 49 | 50 |
51 | 52 | 53 | Unless you explicitly state otherwise, any contribution intentionally submitted 54 | for inclusion in this crate by you, as defined in the Apache-2.0 license, shall 55 | be dual licensed as above, without any additional terms or conditions. 56 | 57 | -------------------------------------------------------------------------------- /examples/hello_tls.rs: -------------------------------------------------------------------------------- 1 | use std::env; 2 | use tide::prelude::*; 3 | use tide_rustls::TlsListener; 4 | 5 | async fn endpoint(req: tide::Request<()>) -> tide::Result> { 6 | Ok(json!({ 7 | "localAddr": req.local_addr().unwrap_or("[unknown]"), 8 | "method": req.method().to_string(), 9 | "url": req.url().to_string() 10 | })) 11 | } 12 | 13 | fn main() -> std::io::Result<()> { 14 | async_std::task::block_on(async { 15 | let mut app = tide::new(); 16 | app.at("*").all(endpoint); 17 | app.at("/").all(endpoint); 18 | 19 | if let (Ok(cert), Ok(key)) = (env::var("TIDE_CERT"), env::var("TIDE_KEY")) { 20 | app.listen( 21 | TlsListener::build() 22 | .addrs("localhost:4433") 23 | .cert(cert) 24 | .key(key), 25 | ) 26 | .await?; 27 | } else { 28 | eprintln!( 29 | " 30 | To run this app locally: 31 | * install https://github.com/FiloSottile/mkcert 32 | * $ mkcert -key-file key.pem -cert-file cert.pem localhost 127.0.0.1 ::1 33 | * $ env TIDE_CERT=cert.pem TIDE_KEY=key.pem cargo run --example hello_tls 34 | * $ curl -v https://localhost:4433/secure 35 | " 36 | ); 37 | } 38 | 39 | Ok(()) 40 | }) 41 | } 42 | -------------------------------------------------------------------------------- /src/custom_tls_acceptor.rs: -------------------------------------------------------------------------------- 1 | use async_rustls::server::TlsStream; 2 | use async_std::net::TcpStream; 3 | 4 | /// The CustomTlsAcceptor trait provides a custom implementation of accepting 5 | /// TLS connections from a [`TcpStream`]. tide-rustls will call the 6 | /// [`CustomTlsAcceptor::accept`] function for each new [`TcpStream`] it 7 | /// accepts, to obtain a [`TlsStream`]). 8 | /// 9 | /// Implementing this trait gives you control over the TLS negotiation process, 10 | /// and allows you to process some TLS connections internally without passing 11 | /// them through to tide, such as for multiplexing or custom ALPN negotiation. 12 | #[tide::utils::async_trait] 13 | pub trait CustomTlsAcceptor: Send + Sync { 14 | /// Accept a [`TlsStream`] from a [`TcpStream`]. 15 | /// 16 | /// If TLS negotiation succeeds, but does not result in a stream that tide 17 | /// should process HTTP connections from, return `Ok(None)`. 18 | async fn accept(&self, stream: TcpStream) -> std::io::Result>>; 19 | } 20 | 21 | /// Crate-private adapter to make `async_rustls::TlsAcceptor` implement 22 | /// `CustomTlsAcceptor`, without creating a conflict between the two `accept` 23 | /// methods. 24 | pub(crate) struct StandardTlsAcceptor(pub(crate) async_rustls::TlsAcceptor); 25 | 26 | #[tide::utils::async_trait] 27 | impl CustomTlsAcceptor for StandardTlsAcceptor { 28 | async fn accept(&self, stream: TcpStream) -> std::io::Result>> { 29 | self.0.accept(stream).await.map(Some) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | //! tide tls listener built on async-rustls and rustls 2 | //! 3 | //! 4 | //! # Example 5 | //! ```rust 6 | //! # use tide_rustls::TlsListener; 7 | //! # fn main() -> tide::Result<()> { async_std::task::block_on(async { 8 | //! let mut app = tide::new(); 9 | //! app.at("/").get(|_| async { Ok("Hello tls") }); 10 | //! # if false { 11 | //! app.listen( 12 | //! TlsListener::build() 13 | //! .addrs("localhost:4433") 14 | //! .cert(std::env::var("TIDE_CERT_PATH").unwrap()) 15 | //! .key(std::env::var("TIDE_KEY_PATH").unwrap()), 16 | //! ) 17 | //! .await?; 18 | //! # } Ok(()) }) } 19 | //! ``` 20 | #![forbid(unsafe_code, future_incompatible)] 21 | #![deny( 22 | missing_debug_implementations, 23 | nonstandard_style, 24 | missing_docs, 25 | unreachable_pub, 26 | missing_copy_implementations, 27 | unused_qualifications 28 | )] 29 | 30 | mod custom_tls_acceptor; 31 | mod tcp_connection; 32 | mod tls_listener; 33 | mod tls_listener_builder; 34 | mod tls_listener_config; 35 | mod tls_stream_wrapper; 36 | 37 | pub(crate) use tcp_connection::TcpConnection; 38 | pub(crate) use tls_listener_config::TlsListenerConfig; 39 | pub(crate) use tls_stream_wrapper::TlsStreamWrapper; 40 | 41 | pub use custom_tls_acceptor::CustomTlsAcceptor; 42 | pub use tls_listener::TlsListener; 43 | pub use tls_listener_builder::TlsListenerBuilder; 44 | 45 | pub use async_rustls; 46 | pub use rustls; 47 | -------------------------------------------------------------------------------- /src/tcp_connection.rs: -------------------------------------------------------------------------------- 1 | use async_std::net::{SocketAddr, TcpListener}; 2 | use std::fmt::{self, Debug, Display, Formatter}; 3 | 4 | #[derive(Debug)] 5 | pub(crate) enum TcpConnection { 6 | Addrs(Vec), 7 | Connected(TcpListener), 8 | } 9 | 10 | impl Display for TcpConnection { 11 | fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { 12 | match self { 13 | Self::Addrs(addrs) => write!( 14 | f, 15 | "{}", 16 | addrs 17 | .iter() 18 | .map(|a| format!("https://{}", a)) 19 | .collect::>() 20 | .join(", ") 21 | ), 22 | 23 | Self::Connected(tcp) => write!( 24 | f, 25 | "https://{}", 26 | tcp.local_addr() 27 | .ok() 28 | .map(|a| a.to_string()) 29 | .as_deref() 30 | .unwrap_or("[unknown]") 31 | ), 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/tls_listener.rs: -------------------------------------------------------------------------------- 1 | use crate::custom_tls_acceptor::StandardTlsAcceptor; 2 | use crate::{ 3 | CustomTlsAcceptor, TcpConnection, TlsListenerBuilder, TlsListenerConfig, TlsStreamWrapper, 4 | }; 5 | 6 | use tide::listener::ListenInfo; 7 | use tide::listener::{Listener, ToListener}; 8 | use tide::Server; 9 | 10 | use async_std::net::{TcpListener, TcpStream}; 11 | use async_std::prelude::*; 12 | use async_std::{io, task}; 13 | 14 | use async_rustls::TlsAcceptor; 15 | use rustls::internal::pemfile::{certs, pkcs8_private_keys, rsa_private_keys}; 16 | use rustls::{Certificate, NoClientAuth, PrivateKey, ServerConfig}; 17 | 18 | use std::fmt::{self, Debug, Display, Formatter}; 19 | use std::fs::File; 20 | use std::io::{BufReader, Seek, SeekFrom}; 21 | use std::path::Path; 22 | use std::sync::Arc; 23 | use std::time::Duration; 24 | 25 | /// The primary type for this crate 26 | pub struct TlsListener { 27 | connection: TcpConnection, 28 | config: TlsListenerConfig, 29 | server: Option>, 30 | tcp_nodelay: Option, 31 | tcp_ttl: Option, 32 | } 33 | 34 | impl Debug for TlsListener { 35 | fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { 36 | f.debug_struct("TlsListener") 37 | .field(&"connection", &self.connection) 38 | .field(&"config", &self.config) 39 | .field( 40 | &"server", 41 | if self.server.is_some() { 42 | &"Some(Server)" 43 | } else { 44 | &"None" 45 | }, 46 | ) 47 | .field("tcp_ttl", &self.tcp_ttl) 48 | .field("tcp_nodelay", &self.tcp_nodelay) 49 | .finish() 50 | } 51 | } 52 | 53 | impl TlsListener { 54 | pub(crate) fn new( 55 | connection: TcpConnection, 56 | config: TlsListenerConfig, 57 | tcp_nodelay: Option, 58 | tcp_ttl: Option, 59 | ) -> Self { 60 | Self { 61 | connection, 62 | config, 63 | server: None, 64 | tcp_nodelay, 65 | tcp_ttl, 66 | } 67 | } 68 | /// The primary entrypoint to create a TlsListener. See 69 | /// [TlsListenerBuilder](crate::TlsListenerBuilder) for more 70 | /// configuration options. 71 | /// 72 | /// # Example 73 | /// 74 | /// ```rust 75 | /// # use tide_rustls::TlsListener; 76 | /// let listener = TlsListener::<()>::build() 77 | /// .addrs("localhost:4433") 78 | /// .cert("./tls/localhost-4433.cert") 79 | /// .key("./tls/localhost-4433.key") 80 | /// .finish(); 81 | /// ``` 82 | pub fn build() -> TlsListenerBuilder { 83 | TlsListenerBuilder::new() 84 | } 85 | 86 | async fn configure(&mut self) -> io::Result<()> { 87 | self.config = match std::mem::take(&mut self.config) { 88 | TlsListenerConfig::Paths { cert, key } => { 89 | let certs = load_certs(&cert)?; 90 | let mut keys = load_keys(&key)?; 91 | let mut config = ServerConfig::new(NoClientAuth::new()); 92 | config 93 | .set_single_cert(certs, keys.remove(0)) 94 | .map_err(|err| io::Error::new(io::ErrorKind::InvalidInput, err))?; 95 | 96 | TlsListenerConfig::Acceptor(Arc::new(StandardTlsAcceptor(TlsAcceptor::from( 97 | Arc::new(config), 98 | )))) 99 | } 100 | 101 | TlsListenerConfig::ServerConfig(config) => TlsListenerConfig::Acceptor(Arc::new( 102 | StandardTlsAcceptor(TlsAcceptor::from(Arc::new(config))), 103 | )), 104 | 105 | other @ TlsListenerConfig::Acceptor(_) => other, 106 | 107 | TlsListenerConfig::Unconfigured => { 108 | return Err(io::Error::new( 109 | io::ErrorKind::Other, 110 | "could not configure tlslistener", 111 | )); 112 | } 113 | }; 114 | 115 | Ok(()) 116 | } 117 | 118 | fn acceptor(&self) -> Option<&Arc> { 119 | match self.config { 120 | TlsListenerConfig::Acceptor(ref a) => Some(a), 121 | _ => None, 122 | } 123 | } 124 | 125 | fn tcp(&self) -> Option<&TcpListener> { 126 | match self.connection { 127 | TcpConnection::Connected(ref t) => Some(t), 128 | _ => None, 129 | } 130 | } 131 | 132 | async fn connect(&mut self) -> io::Result<()> { 133 | if let TcpConnection::Addrs(addrs) = &self.connection { 134 | let tcp = TcpListener::bind(&addrs[..]).await?; 135 | self.connection = TcpConnection::Connected(tcp); 136 | } 137 | Ok(()) 138 | } 139 | } 140 | 141 | fn handle_tls( 142 | app: Server, 143 | stream: TcpStream, 144 | acceptor: Arc, 145 | ) { 146 | task::spawn(async move { 147 | let local_addr = stream.local_addr().ok(); 148 | let peer_addr = stream.peer_addr().ok(); 149 | 150 | match acceptor.accept(stream).await { 151 | Ok(None) => {} 152 | 153 | Ok(Some(tls_stream)) => { 154 | let stream = TlsStreamWrapper::new(tls_stream); 155 | let fut = async_h1::accept(stream, |mut req| async { 156 | if req.url_mut().set_scheme("https").is_err() { 157 | tide::log::error!("unable to set https scheme on url", { url: req.url().to_string() }); 158 | } 159 | 160 | req.set_local_addr(local_addr); 161 | req.set_peer_addr(peer_addr); 162 | app.respond(req).await 163 | }); 164 | 165 | if let Err(error) = fut.await { 166 | tide::log::error!("async-h1 error", { error: error.to_string() }); 167 | } 168 | } 169 | 170 | Err(tls_error) => { 171 | tide::log::error!("tls error", { error: tls_error.to_string() }); 172 | } 173 | } 174 | }); 175 | } 176 | 177 | impl ToListener for TlsListener { 178 | type Listener = Self; 179 | fn to_listener(self) -> io::Result { 180 | Ok(self) 181 | } 182 | } 183 | 184 | impl ToListener for TlsListenerBuilder { 185 | type Listener = TlsListener; 186 | fn to_listener(self) -> io::Result { 187 | self.finish() 188 | } 189 | } 190 | 191 | #[tide::utils::async_trait] 192 | impl Listener for TlsListener { 193 | async fn bind(&mut self, server: Server) -> io::Result<()> { 194 | self.configure().await?; 195 | self.connect().await?; 196 | self.server = Some(server); 197 | Ok(()) 198 | } 199 | 200 | async fn accept(&mut self) -> io::Result<()> { 201 | let listener = self.tcp().unwrap(); 202 | let mut incoming = listener.incoming(); 203 | let acceptor = self.acceptor().unwrap(); 204 | let server = self.server.as_ref().unwrap(); 205 | 206 | while let Some(stream) = incoming.next().await { 207 | match stream { 208 | Err(ref e) if is_transient_error(e) => continue, 209 | 210 | Err(error) => { 211 | let delay = Duration::from_millis(500); 212 | tide::log::error!("Error: {}. Pausing for {:?}.", error, delay); 213 | task::sleep(delay).await; 214 | continue; 215 | } 216 | 217 | Ok(stream) => { 218 | if let Some(nodelay) = self.tcp_nodelay { 219 | stream.set_nodelay(nodelay)?; 220 | } 221 | 222 | if let Some(ttl) = self.tcp_ttl { 223 | stream.set_ttl(ttl)?; 224 | } 225 | 226 | handle_tls(server.clone(), stream, acceptor.clone()) 227 | } 228 | }; 229 | } 230 | Ok(()) 231 | } 232 | 233 | fn info(&self) -> Vec { 234 | vec![ListenInfo::new( 235 | self.connection.to_string(), 236 | String::from("tcp"), 237 | true, 238 | )] 239 | } 240 | } 241 | 242 | fn is_transient_error(e: &io::Error) -> bool { 243 | use io::ErrorKind::*; 244 | matches!( 245 | e.kind(), 246 | ConnectionRefused | ConnectionAborted | ConnectionReset 247 | ) 248 | } 249 | 250 | impl Display for TlsListener { 251 | fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { 252 | write!(f, "{}", self.connection) 253 | } 254 | } 255 | 256 | fn load_certs(path: &Path) -> io::Result> { 257 | certs(&mut BufReader::new(File::open(path)?)) 258 | .map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "invalid cert")) 259 | } 260 | 261 | fn load_keys(path: &Path) -> io::Result> { 262 | let mut bufreader = BufReader::new(File::open(path)?); 263 | if let Ok(pkcs8) = pkcs8_private_keys(&mut bufreader) { 264 | if !pkcs8.is_empty() { 265 | return Ok(pkcs8); 266 | } 267 | } 268 | 269 | bufreader.seek(SeekFrom::Start(0))?; 270 | 271 | if let Ok(rsa) = rsa_private_keys(&mut bufreader) { 272 | if !rsa.is_empty() { 273 | return Ok(rsa); 274 | } 275 | } 276 | 277 | Err(io::Error::new(io::ErrorKind::InvalidInput, "invalid key")) 278 | } 279 | -------------------------------------------------------------------------------- /src/tls_listener_builder.rs: -------------------------------------------------------------------------------- 1 | use async_std::io; 2 | use async_std::net::TcpListener; 3 | 4 | use rustls::ServerConfig; 5 | 6 | use super::{CustomTlsAcceptor, TcpConnection, TlsListener, TlsListenerConfig}; 7 | 8 | use std::marker::PhantomData; 9 | use std::net::{SocketAddr, ToSocketAddrs}; 10 | use std::path::{Path, PathBuf}; 11 | use std::sync::Arc; 12 | 13 | /// # A builder for TlsListeners 14 | /// 15 | /// This is created with a call to 16 | /// [`TlsListener::build`](crate::TlsListener::build). This also can 17 | /// be passed directly to [`tide::Server::listen`], skipping the 18 | /// [`TlsListenerBuilder::finish`] call. 19 | /// 20 | /// # Examples 21 | /// 22 | /// ```rust 23 | /// # use tide_rustls::TlsListener; 24 | /// let listener = TlsListener::<()>::build() 25 | /// .addrs("localhost:4433") 26 | /// .cert("./tls/localhost-4433.cert") 27 | /// .key("./tls/localhost-4433.key") 28 | /// .finish(); 29 | /// ``` 30 | /// 31 | /// ```rust 32 | /// # use tide_rustls::TlsListener; 33 | /// let listener = TlsListener::<()>::build() 34 | /// .tcp(std::net::TcpListener::bind("localhost:4433").unwrap()) 35 | /// .config(rustls::ServerConfig::new(rustls::NoClientAuth::new())) 36 | /// .finish(); 37 | /// ``` 38 | /// 39 | /// ```rust 40 | /// # use tide_rustls::TlsListener; 41 | /// let listener = TlsListener::<()>::build() 42 | /// .addrs("localhost:4433") 43 | /// .cert("./tls/localhost-4433.cert") 44 | /// .key("./tls/localhost-4433.key") 45 | /// .tcp_ttl(60) 46 | /// .tcp_nodelay(true) 47 | /// .finish(); 48 | /// ``` 49 | 50 | pub struct TlsListenerBuilder { 51 | key: Option, 52 | cert: Option, 53 | config: Option, 54 | tls_acceptor: Option>, 55 | tcp: Option, 56 | addrs: Option>, 57 | tcp_nodelay: Option, 58 | tcp_ttl: Option, 59 | _state: PhantomData, 60 | } 61 | 62 | impl Default for TlsListenerBuilder { 63 | fn default() -> Self { 64 | Self { 65 | key: None, 66 | cert: None, 67 | config: None, 68 | tls_acceptor: None, 69 | tcp: None, 70 | addrs: None, 71 | tcp_nodelay: None, 72 | tcp_ttl: None, 73 | _state: PhantomData, 74 | } 75 | } 76 | } 77 | 78 | impl std::fmt::Debug for TlsListenerBuilder { 79 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 80 | f.debug_struct("TlsListenerBuilder") 81 | .field("key", &self.key) 82 | .field("cert", &self.cert) 83 | .field( 84 | "config", 85 | &if self.config.is_some() { 86 | "Some(ServerConfig { .. })" 87 | } else { 88 | "None" 89 | }, 90 | ) 91 | .field( 92 | "tls_acceptor", 93 | &if self.tls_acceptor.is_some() { 94 | "Some(_)" 95 | } else { 96 | "None" 97 | }, 98 | ) 99 | .field("tcp", &self.tcp) 100 | .field("addrs", &self.addrs) 101 | .field("tcp_nodelay", &self.tcp_nodelay) 102 | .field("tcp_ttl", &self.tcp_ttl) 103 | .finish() 104 | } 105 | } 106 | 107 | impl TlsListenerBuilder { 108 | pub(crate) fn new() -> Self { 109 | Self::default() 110 | } 111 | 112 | /// Provide a path to a key file, in either pkcs8 or rsa 113 | /// formats. This is mutually exclusive with providing a server 114 | /// config with [`TlsListenerBuilder::config`], but must be used 115 | /// in conjunction with [`TlsListenerBuilder::cert`] 116 | pub fn key(mut self, path: impl AsRef) -> Self { 117 | self.key = Some(path.as_ref().into()); 118 | self 119 | } 120 | 121 | /// Provide a path to a cert file. This is mutually exclusive with 122 | /// providing a server config with [`TlsListenerBuilder::config`], 123 | /// but must be used in conjunction with 124 | /// [`TlsListenerBuilder::key`] 125 | pub fn cert(mut self, path: impl AsRef) -> Self { 126 | self.cert = Some(path.as_ref().into()); 127 | self 128 | } 129 | 130 | /// Provide a prebuilt 131 | /// [`rustls::ServerConfig`](::rustls::ServerConfig) with any 132 | /// options. This is mutually exclusive with both 133 | /// [`TlsListenerBuilder::key`] and [`TlsListenerBuilder::cert`], 134 | /// but provides the opportunity for more configuration choices. 135 | pub fn config(mut self, config: ServerConfig) -> Self { 136 | self.config = Some(config); 137 | self 138 | } 139 | 140 | /// Provides a custom acceptor for TLS connections. This is mutually 141 | /// exclusive with any of [`TlsListenerBuilder::key`], 142 | /// [`TlsListenerBuilder::cert`], and [`TlsListenerBuilder::config`], but 143 | /// gives total control over accepting TLS connections, including 144 | /// multiplexing other streams or ALPN negotiations on the same TLS 145 | /// connection that tide should ignore. 146 | pub fn tls_acceptor(mut self, acceptor: Arc) -> Self { 147 | self.tls_acceptor = Some(acceptor); 148 | self 149 | } 150 | 151 | /// Provides a bound tcp listener (either async-std or std) to 152 | /// build this tls listener on. This is mutually exclusive with 153 | /// [`TlsListenerBuilder::addrs`], but one of them is mandatory. 154 | pub fn tcp(mut self, tcp: impl Into) -> Self { 155 | self.tcp = Some(tcp.into()); 156 | self 157 | } 158 | 159 | /// Provides a [`std::net::ToSocketAddrs`] specification for this 160 | /// tls listener. This is mutually exclusive with 161 | /// [`TlsListenerBuilder::tcp`] but one of them is mandatory. 162 | pub fn addrs(mut self, addrs: impl ToSocketAddrs) -> Self { 163 | if let Ok(socket_addrs) = addrs.to_socket_addrs() { 164 | self.addrs = Some(socket_addrs.collect()); 165 | } 166 | self 167 | } 168 | 169 | /// Provides a TCP_NODELAY option for this tls listener. 170 | pub fn tcp_nodelay(mut self, nodelay: bool) -> Self { 171 | self.tcp_nodelay = Some(nodelay); 172 | self 173 | } 174 | 175 | /// Provides a TTL option for this tls listener, in seconds. 176 | pub fn tcp_ttl(mut self, ttl: u32) -> Self { 177 | self.tcp_ttl = Some(ttl); 178 | self 179 | } 180 | 181 | /// finishes building a TlsListener from this TlsListenerBuilder. 182 | /// 183 | /// # Errors 184 | /// 185 | /// this will return an error unless all of the following conditions are met: 186 | /// * either of these is provided, but not both 187 | /// * [`TlsListenerBuilder::tcp`] 188 | /// * [`TlsListenerBuilder::addrs`] 189 | /// * exactly one of these is provided 190 | /// * both [`TlsListenerBuilder::cert`] AND [`TlsListenerBuilder::key`] 191 | /// * [`TlsListenerBuilder::config`] 192 | /// * [`TlsListenerBuilder::tls_acceptor`] 193 | pub fn finish(self) -> io::Result> { 194 | let Self { 195 | key, 196 | cert, 197 | config, 198 | tls_acceptor, 199 | tcp, 200 | addrs, 201 | tcp_nodelay, 202 | tcp_ttl, 203 | .. 204 | } = self; 205 | 206 | let config = match (key, cert, config, tls_acceptor) { 207 | (Some(key), Some(cert), None, None) => TlsListenerConfig::Paths { key, cert }, 208 | (None, None, Some(config), None) => TlsListenerConfig::ServerConfig(config), 209 | (None, None, None, Some(tls_acceptor)) => TlsListenerConfig::Acceptor(tls_acceptor), 210 | _ => { 211 | return Err(io::Error::new( 212 | io::ErrorKind::InvalidInput, 213 | "need exactly one of cert + key, ServerConfig, or TLS acceptor", 214 | )) 215 | } 216 | }; 217 | 218 | let connection = match (tcp, addrs) { 219 | (Some(tcp), None) => TcpConnection::Connected(tcp), 220 | (None, Some(addrs)) => TcpConnection::Addrs(addrs), 221 | _ => { 222 | return Err(io::Error::new( 223 | io::ErrorKind::InvalidInput, 224 | "either tcp or addrs are required", 225 | )) 226 | } 227 | }; 228 | 229 | Ok(TlsListener::new(connection, config, tcp_nodelay, tcp_ttl)) 230 | } 231 | } 232 | -------------------------------------------------------------------------------- /src/tls_listener_config.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::{self, Debug, Formatter}; 2 | 3 | use rustls::ServerConfig; 4 | 5 | use super::CustomTlsAcceptor; 6 | 7 | use std::path::PathBuf; 8 | use std::sync::Arc; 9 | 10 | impl Default for TlsListenerConfig { 11 | fn default() -> Self { 12 | Self::Unconfigured 13 | } 14 | } 15 | pub(crate) enum TlsListenerConfig { 16 | Unconfigured, 17 | Acceptor(Arc), 18 | ServerConfig(ServerConfig), 19 | Paths { cert: PathBuf, key: PathBuf }, 20 | } 21 | 22 | impl Debug for TlsListenerConfig { 23 | fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { 24 | match self { 25 | Self::Unconfigured => write!(f, "TlsListenerConfig::Unconfigured"), 26 | Self::Acceptor(_) => write!(f, "TlsListenerConfig::Acceptor(..)"), 27 | Self::ServerConfig(_) => write!(f, "TlsListenerConfig::ServerConfig(..)"), 28 | Self::Paths { cert, key } => f 29 | .debug_struct("TlsListenerConfig::Paths") 30 | .field("cert", cert) 31 | .field("key", key) 32 | .finish(), 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/tls_stream_wrapper.rs: -------------------------------------------------------------------------------- 1 | use async_dup::{Arc, Mutex}; 2 | use async_rustls::server::TlsStream; 3 | use async_std::io::{Read, Result, Write}; 4 | use async_std::net::TcpStream; 5 | use std::pin::Pin; 6 | use std::task::{Context, Poll}; 7 | 8 | #[derive(Clone)] 9 | pub(crate) struct TlsStreamWrapper(Arc>>); 10 | 11 | impl TlsStreamWrapper { 12 | pub(crate) fn new(stream: TlsStream) -> Self { 13 | Self(Arc::new(Mutex::new(stream))) 14 | } 15 | } 16 | 17 | impl Read for TlsStreamWrapper { 18 | fn poll_read( 19 | self: Pin<&mut Self>, 20 | cx: &mut Context<'_>, 21 | buf: &mut [u8], 22 | ) -> Poll> { 23 | Pin::new(&mut &*self.0).poll_read(cx, buf) 24 | } 25 | } 26 | 27 | impl Write for TlsStreamWrapper { 28 | fn poll_write(self: Pin<&mut Self>, cx: &mut Context<'_>, buf: &[u8]) -> Poll> { 29 | Pin::new(&mut &*self.0).poll_write(cx, buf) 30 | } 31 | 32 | fn poll_flush(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { 33 | Pin::new(&mut &*self.0).poll_flush(cx) 34 | } 35 | 36 | fn poll_close(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { 37 | Pin::new(&mut &*self.0).poll_close(cx) 38 | } 39 | } 40 | --------------------------------------------------------------------------------