├── .gitignore ├── examples ├── tls_config │ ├── local.cert │ ├── local.key │ ├── local.pfx │ ├── local2.key │ ├── local2.pfx │ ├── local2.cert │ ├── localcert.pem │ └── mod.rs ├── axum.rs ├── http-stream.rs ├── http.rs ├── echo-threads.rs ├── echo.rs ├── http-change-certificate.rs └── test_examples.py ├── tools └── update-version.awk ├── docs └── RELEASE.md ├── .github ├── workflows │ ├── publish.yml │ ├── release.yml │ └── ci.yml └── dependabot.yml ├── tests ├── helper │ ├── asserts.rs │ ├── mod.rs │ └── mocks.rs ├── basic.rs └── long_text.txt ├── README.md ├── src ├── net.rs ├── spawning_handshake.rs ├── axum.rs ├── accept.rs └── lib.rs ├── Cargo.toml ├── cliff.toml ├── CHANGELOG.md └── LICENSE /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | **/*.rs.bk 3 | Cargo.lock 4 | .idea/ 5 | __pycache__/ 6 | -------------------------------------------------------------------------------- /examples/tls_config/local.cert: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tmccombs/tls-listener/HEAD/examples/tls_config/local.cert -------------------------------------------------------------------------------- /examples/tls_config/local.key: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tmccombs/tls-listener/HEAD/examples/tls_config/local.key -------------------------------------------------------------------------------- /examples/tls_config/local.pfx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tmccombs/tls-listener/HEAD/examples/tls_config/local.pfx -------------------------------------------------------------------------------- /examples/tls_config/local2.key: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tmccombs/tls-listener/HEAD/examples/tls_config/local2.key -------------------------------------------------------------------------------- /examples/tls_config/local2.pfx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tmccombs/tls-listener/HEAD/examples/tls_config/local2.pfx -------------------------------------------------------------------------------- /examples/tls_config/local2.cert: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tmccombs/tls-listener/HEAD/examples/tls_config/local2.cert -------------------------------------------------------------------------------- /tools/update-version.awk: -------------------------------------------------------------------------------- 1 | $0 == "[package]" { inpkg = 1; } 2 | inpkg && /^version +=/ { 3 | sub(/"[^"]*"/, "\"" ENVIRON["VERSION"] "\"") 4 | inpkg = 0 5 | } 6 | { print } 7 | -------------------------------------------------------------------------------- /docs/RELEASE.md: -------------------------------------------------------------------------------- 1 | Steps to create a new release: 2 | 3 | 1. Update version in Cargo.toml 4 | 2. Run `gi-cliff -p CHANGELOG.md -u -t ` to update the changelog 5 | 3. Merge changes 6 | 4. Publish a new release for the target tag. 7 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish 2 | on: 3 | release: 4 | types: [published] 5 | 6 | jobs: 7 | publish: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v6 11 | - name: Install toolchain 12 | run: | 13 | rustup toolchain install stable --profile minimal 14 | rustup default stable 15 | - name: Publish 16 | run: cargo publish 17 | env: 18 | CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} 19 | 20 | -------------------------------------------------------------------------------- /tests/helper/asserts.rs: -------------------------------------------------------------------------------- 1 | macro_rules! assert_err { 2 | ($ex:expr, $m:pat) => { 3 | match $ex { 4 | Err($m) => (), 5 | Err(e) => panic!("Unexpected error: {:?}", e), 6 | Ok(_) => panic!("Expected error, but got Ok"), 7 | } 8 | }; 9 | } 10 | 11 | macro_rules! assert_ascii_eq { 12 | ($one:expr, $two:expr_2021) => { 13 | assert_eq!( 14 | ::std::str::from_utf8(&*$one).unwrap(), 15 | ::std::str::from_utf8(&*$two).unwrap() 16 | ) 17 | }; 18 | } 19 | 20 | pub(crate) use assert_ascii_eq; 21 | pub(crate) use assert_err; 22 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "cargo" 9 | directory: "/" 10 | schedule: 11 | interval: "monthly" 12 | cooldown: 13 | default-days: 7 14 | groups: 15 | bump: 16 | applies-to: "version-updates" 17 | security: 18 | applies-to: "security-updates" 19 | - package-ecosystem: "github-actions" 20 | directory: "/" 21 | schedule: 22 | interval: "monthly" 23 | cooldown: 24 | default-days: 7 25 | -------------------------------------------------------------------------------- /tests/helper/mod.rs: -------------------------------------------------------------------------------- 1 | use futures_util::StreamExt; 2 | use tls_listener::TlsListener; 3 | use tokio::io::{copy, split}; 4 | use tokio::sync::oneshot; 5 | use tokio::task::JoinHandle; 6 | 7 | mod asserts; 8 | pub(crate) use asserts::*; 9 | 10 | mod mocks; 11 | pub use mocks::*; 12 | 13 | pub fn setup() -> (MockConnect, TlsListener) { 14 | let (connect, accept) = accepting(); 15 | (connect, TlsListener::new(MockTls, accept)) 16 | } 17 | 18 | pub fn setup_echo(end: oneshot::Receiver<()>) -> (MockConnect, JoinHandle<()>) { 19 | let (connector, listener) = setup(); 20 | 21 | let handle = tokio::spawn( 22 | listener 23 | .take_until(end) 24 | .for_each_concurrent(None, |s| async { 25 | let (mut reader, mut writer) = split(s.expect("Unexpected error").0); 26 | copy(&mut reader, &mut writer) 27 | .await 28 | .expect("Failed to copy"); 29 | }), 30 | ); 31 | (connector, handle) 32 | } 33 | -------------------------------------------------------------------------------- /examples/axum.rs: -------------------------------------------------------------------------------- 1 | use axum::{Router, routing::get}; 2 | use std::net::SocketAddr; 3 | use tls_listener::TlsListener; 4 | use tokio::net::TcpListener; 5 | 6 | mod tls_config; 7 | use tls_config::tls_acceptor; 8 | 9 | /// An example of running an axum server with `TlsListener`. 10 | /// 11 | /// One can also bypass `axum::serve` and use the `Router` with Hyper's `serve_connection` API 12 | /// directly. The main advantages of using `axum::serve` are that 13 | /// - graceful shutdown is made easy with axum's `.with_graceful_shutdown` API, and 14 | /// - the Hyper server is configured by axum itself, allowing options specific to axum to be set 15 | /// (for example, axum currently enables the `CONNECT` protocol in order to support HTTP/2 16 | /// websockets). 17 | #[tokio::main(flavor = "current_thread")] 18 | async fn main() { 19 | let app = Router::new().route("/", get(|| async { "Hello, World!" })); 20 | 21 | let local_addr = "0.0.0.0:3000".parse::().unwrap(); 22 | let tcp_listener = TcpListener::bind(local_addr).await.unwrap(); 23 | let listener = TlsListener::new(tls_acceptor(), tcp_listener); 24 | 25 | axum::serve(listener, app).await.unwrap(); 26 | } 27 | -------------------------------------------------------------------------------- /examples/tls_config/localcert.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIDJDCCAgygAwIBAgIUEsB08OS9bW2jDA5ReGkLyX03CeQwDQYJKoZIhvcNAQEL 3 | BQAwFTETMBEGA1UEAwwKdGVzdC5sb2NhbDAgFw0yMjAzMTMwNzUzNDVaGA8yMTIy 4 | MDIxNzA3NTM0NVowFTETMBEGA1UEAwwKdGVzdC5sb2NhbDCCASIwDQYJKoZIhvcN 5 | AQEBBQADggEPADCCAQoCggEBAJ2FCbhUzRHDTSRyZPdR6naoSnesKzjgDXJx2qA+ 6 | BMmofWyWswusN/skgFOkfXonJOjbRgbOYuq8Y0rpZ+P9gchnm068iX7mgwBg8VJ/ 7 | 2uuE4V2unEqoKjHwKK0cAXWprv86TYpzRa18bbU7qLAR8UyuD/ub0lOw8TEN+bp3 8 | g+TGysmCm+Ip21j+dyi59dt0mzVPOLFHz5kaYzk2b551jmLvURVl6sCarBFre4LZ 9 | uGqHWDOeTTljegVMfhiyNWIM3QZJsImKSUMqf4QKFAJJrPNDpfG+yCwJxzsT2fMM 10 | 1Wd2Q43OxdRGEbxMcoGEaZjSg+5kvSBBOm1KNxzQowOxLxcCAwEAAaNqMGgwHQYD 11 | VR0OBBYEFHlvZRxUJLKYqFj0NxOokpGYCHPnMB8GA1UdIwQYMBaAFHlvZRxUJLKY 12 | qFj0NxOokpGYCHPnMA8GA1UdEwEB/wQFMAMBAf8wFQYDVR0RBA4wDIIKdGVzdC5s 13 | b2NhbDANBgkqhkiG9w0BAQsFAAOCAQEAdw+6TJvSne/kOdFWZYHbsGNhRM5afJ69 14 | wLC6s50zEfkl0Mi/EQgsPVPEWwAWByHwBf6i5twEIYGmmjW0N+6MKxb1SSJyiZhC 15 | w/Hp7jG0OPeJOKT+ALfS4FYVOn0lDOVDttt/DXpVk3AwDx0xXevOj+wHb8xbwDDX 16 | yXp7Oe4mn0jBZJTDVbGCzIzdyybHwtdAkB0BMPJ1+gtTrMs3Ef9Oshz1c566+RR8 17 | juUZ8bEB1zuiYtvw2f9L2qQjZwObN8Y58ZPdt4Q90wYE+zjwC/m5yHYSdlpeqccC 18 | U7Bxur/YjHk36W39Hu8Ly+kiJwCIvtk4WuEGhyWcPYKng9G1jl36EQ== 19 | -----END CERTIFICATE----- 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # tls-listener 2 | 3 | [![Apache 2 License](https://img.shields.io/badge/License-Apache--2.0-brightgreen)](https://www.apache.org/licenses/LICENSE-2.0) 4 | [![Crate version](https://img.shields.io/crates/v/tls-listener)](https://crates.io/crates/tls-listener) 5 | [![Docs](https://docs.rs/tls-listener/badge.svg)](https://docs.rs/tls-listener) 6 | [![Build status](https://github.com/tmccombs/tls-listener/workflows/CI/badge.svg)](https://github.com/tmccombs/tls-listener/actions?query=workflow%3ACI) 7 | 8 | This library is intended to automatically initiate a TLS connection 9 | as for each new connection in a source of new streams (such as a listening 10 | TCP or unix domain socket). 11 | 12 | It can be used to easily create a `Stream` of TLS connections from a listening socket. 13 | 14 | See examples for examples of usage. 15 | 16 | You must enable either one of the `rustls` (more details below), `native-tls`, or `openssl` 17 | features depending on which implementation you would like to use. 18 | 19 | When enabling the `rustls` feature, the `rustls` crate will be added as a dependency along 20 | with it's default [cryptography provider](https://docs.rs/rustls/latest/rustls/#cryptography-providers). 21 | To avoid this behaviour and use other cryptography providers, the `rustls-core` feature can be used instead. 22 | Additional feature flags for other [rustls built-in cryptography providers](https://docs.rs/rustls/latest/rustls/#built-in-providers) are also available: 23 | `rustls-aws-lc` (default), `rustls-fips` and `rustls-ring` -------------------------------------------------------------------------------- /examples/http-stream.rs: -------------------------------------------------------------------------------- 1 | use futures_util::stream::StreamExt; 2 | use hyper::server::conn::http1; 3 | use hyper::service::service_fn; 4 | use hyper::{Request, Response}; 5 | use hyper_util::rt::tokio::TokioIo; 6 | use std::convert::Infallible; 7 | use std::future::ready; 8 | use tokio::net::TcpListener; 9 | 10 | use tls_listener::TlsListener; 11 | 12 | mod tls_config; 13 | use tls_config::tls_acceptor; 14 | 15 | async fn hello(_: Request) -> Result, Infallible> { 16 | Ok(Response::new("Hello, World!".into())) 17 | } 18 | 19 | #[tokio::main(flavor = "current_thread")] 20 | async fn main() -> Result<(), Box> { 21 | let addr: std::net::SocketAddr = ([127, 0, 0, 1], 3000).into(); 22 | 23 | // This uses a filter to handle errors with connecting 24 | TlsListener::new(tls_acceptor(), TcpListener::bind(addr).await?) 25 | .connections() 26 | .filter_map(|conn| { 27 | ready(match conn { 28 | Err(err) => { 29 | eprintln!("Error: {:?}", err); 30 | None 31 | } 32 | Ok(c) => Some(TokioIo::new(c)), 33 | }) 34 | }) 35 | .for_each_concurrent(None, |conn| async { 36 | if let Err(err) = http1::Builder::new() 37 | .serve_connection(conn, service_fn(hello)) 38 | .await 39 | { 40 | eprintln!("Error serving connection: {:?}", err); 41 | } 42 | }) 43 | .await; 44 | 45 | Ok(()) 46 | } 47 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | on: 3 | workflow_dispatch: 4 | inputs: 5 | version: 6 | type: string 7 | required: true 8 | 9 | 10 | env: 11 | VERSION: "${{ inputs.version }}" 12 | 13 | jobs: 14 | release: 15 | runs-on: ubuntu-latest 16 | permissions: 17 | contents: write 18 | steps: 19 | - name: checkout 20 | uses: actions/checkout@v6 21 | with: 22 | persist-credentials: true 23 | fetch-depth: 0 24 | - name: Git setup 25 | run: | 26 | git config user.name "github-actions[bot]" 27 | git config user.email " 41898282+github-actions[bot]@users.noreply.github.com" 28 | - name: Update changelog 29 | uses: orhun/git-cliff-action@d77b37db2e3f7398432d34b72a12aa3e2ba87e51 # v4.6.0 30 | with: 31 | args: "--unreleased" 32 | env: 33 | OUTPUT: release.md 34 | GIT_CLIFF_PREPEND: CHANGELOG.md 35 | GIT_CLIFF_TAG: "v${{ inputs.version }}" 36 | GITHUB_REPO: ${{ github.repository }} 37 | - name: Update Cargo.toml 38 | run: | 39 | awk -i inplace -f tools/update-version.awk Cargo.toml 40 | - name: Commit and push changes 41 | run: | 42 | set +e 43 | git add CHANGELOG.md Cargo.toml 44 | git commit -m "Update version to ${VERSION}" 45 | git push "https://github.com/${GITHUB_REPOSITORY}.git" main 46 | - name: Create release 47 | run: | 48 | gh release create --notes-file release.md "v${VERSION}" 49 | env: 50 | GH_TOKEN: ${{ github.token }} 51 | 52 | 53 | -------------------------------------------------------------------------------- /examples/http.rs: -------------------------------------------------------------------------------- 1 | use hyper::server::conn::http1; 2 | use hyper::service::service_fn; 3 | use hyper::{Request, Response}; 4 | use hyper_util::rt::tokio::TokioIo; 5 | use std::convert::Infallible; 6 | use tls_listener::TlsListener; 7 | use tokio::net::TcpListener; 8 | 9 | mod tls_config; 10 | use tls_config::tls_acceptor; 11 | 12 | async fn hello(_: Request) -> Result, Infallible> { 13 | Ok(Response::new("Hello, World!".into())) 14 | } 15 | 16 | #[tokio::main(flavor = "current_thread")] 17 | async fn main() -> Result<(), Box> { 18 | let addr: std::net::SocketAddr = ([127, 0, 0, 1], 3000).into(); 19 | 20 | let mut listener = TlsListener::new(tls_acceptor(), TcpListener::bind(addr).await?); 21 | 22 | // We start a loop to continuously accept incoming connections 23 | loop { 24 | match listener.accept().await { 25 | Ok((stream, _)) => { 26 | let io = TokioIo::new(stream); 27 | 28 | tokio::task::spawn(async move { 29 | if let Err(err) = http1::Builder::new() 30 | .serve_connection(io, service_fn(hello)) 31 | .await 32 | { 33 | println!("Error serving connection: {:?}", err); 34 | } 35 | }); 36 | } 37 | Err(err) => { 38 | if let Some(remote_addr) = err.peer_addr() { 39 | eprint!("[client {remote_addr}] "); 40 | } 41 | 42 | eprintln!("Error accepting connection: {}", err); 43 | } 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /examples/echo-threads.rs: -------------------------------------------------------------------------------- 1 | use futures_util::StreamExt; 2 | use std::net::SocketAddr; 3 | use tls_listener::{SpawningHandshakes, TlsListener}; 4 | use tokio::io::{copy, split}; 5 | use tokio::net::{TcpListener, TcpStream}; 6 | use tokio::signal::ctrl_c; 7 | #[cfg(all(feature = "native-tls", not(feature = "rustls-core")))] 8 | use tokio_native_tls::TlsStream; 9 | #[cfg(feature = "rustls-core")] 10 | use tokio_rustls::server::TlsStream; 11 | 12 | mod tls_config; 13 | use tls_config::tls_acceptor; 14 | 15 | #[inline] 16 | async fn handle_stream(stream: TlsStream, _remote_addr: SocketAddr) { 17 | let (mut reader, mut writer) = split(stream); 18 | match copy(&mut reader, &mut writer).await { 19 | Ok(cnt) => eprintln!("Processed {} bytes", cnt), 20 | Err(err) => eprintln!("Error: {}", err), 21 | }; 22 | } 23 | 24 | /// For example try opening and closing a connection with: 25 | /// `echo "Q" | openssl s_client -connect host:port` 26 | #[tokio::main(flavor = "multi_thread", worker_threads = 4)] 27 | async fn main() -> Result<(), Box> { 28 | let addr: SocketAddr = ([127, 0, 0, 1], 3000).into(); 29 | 30 | let listener = TcpListener::bind(&addr).await?; 31 | 32 | TlsListener::new(SpawningHandshakes(tls_acceptor()), listener) 33 | .take_until(ctrl_c()) 34 | .for_each_concurrent(None, |s| async { 35 | match s { 36 | Ok((stream, remote_addr)) => { 37 | handle_stream(stream, remote_addr).await; 38 | } 39 | Err(e) => { 40 | eprintln!("Error: {:?}", e); 41 | } 42 | } 43 | }) 44 | .await; 45 | Ok(()) 46 | } 47 | -------------------------------------------------------------------------------- /src/net.rs: -------------------------------------------------------------------------------- 1 | use super::{AsyncAccept, AsyncListener}; 2 | use std::io; 3 | use std::pin::Pin; 4 | use std::task::{Context, Poll}; 5 | use tokio::net::{TcpListener, TcpStream}; 6 | #[cfg(unix)] 7 | use tokio::net::{UnixListener, UnixStream}; 8 | 9 | #[cfg_attr(docsrs, doc(cfg(feature = "tokio-net")))] 10 | impl AsyncAccept for TcpListener { 11 | type Connection = TcpStream; 12 | type Error = io::Error; 13 | type Address = std::net::SocketAddr; 14 | 15 | fn poll_accept( 16 | self: Pin<&mut Self>, 17 | cx: &mut Context<'_>, 18 | ) -> Poll> { 19 | match (*self).poll_accept(cx) { 20 | Poll::Ready(Ok(conn)) => Poll::Ready(Ok(conn)), 21 | Poll::Ready(Err(e)) => Poll::Ready(Err(e)), 22 | Poll::Pending => Poll::Pending, 23 | } 24 | } 25 | } 26 | 27 | #[cfg_attr(docsrs, doc(cfg(feature = "tokio-net")))] 28 | impl AsyncListener for TcpListener { 29 | #[inline] 30 | fn local_addr(&self) -> Result { 31 | TcpListener::local_addr(self) 32 | } 33 | } 34 | 35 | #[cfg(unix)] 36 | #[cfg_attr(docsrs, doc(cfg(feature = "tokio-net")))] 37 | impl AsyncAccept for UnixListener { 38 | type Connection = UnixStream; 39 | type Error = io::Error; 40 | type Address = tokio::net::unix::SocketAddr; 41 | 42 | fn poll_accept( 43 | self: Pin<&mut Self>, 44 | cx: &mut Context<'_>, 45 | ) -> Poll> { 46 | match (*self).poll_accept(cx) { 47 | Poll::Ready(Ok(conn)) => Poll::Ready(Ok(conn)), 48 | Poll::Ready(Err(e)) => Poll::Ready(Err(e)), 49 | Poll::Pending => Poll::Pending, 50 | } 51 | } 52 | } 53 | 54 | #[cfg(unix)] 55 | #[cfg_attr(docsrs, doc(cfg(feature = "tokio-net")))] 56 | impl AsyncListener for UnixListener { 57 | #[inline] 58 | fn local_addr(&self) -> Result { 59 | UnixListener::local_addr(self) 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /examples/echo.rs: -------------------------------------------------------------------------------- 1 | use futures_util::StreamExt; 2 | use std::net::SocketAddr; 3 | use tls_listener::TlsListener; 4 | use tokio::io::{copy, split}; 5 | use tokio::net::{TcpListener, TcpStream}; 6 | use tokio::signal::ctrl_c; 7 | 8 | #[cfg(all( 9 | feature = "native-tls", 10 | not(any(feature = "rustls-core", feature = "openssl")) 11 | ))] 12 | use tokio_native_tls::TlsStream; 13 | #[cfg(all( 14 | feature = "openssl", 15 | not(any(feature = "rustls-core", feature = "native-tls")) 16 | ))] 17 | use tokio_openssl::SslStream as TlsStream; 18 | #[cfg(feature = "rustls-core")] 19 | use tokio_rustls::server::TlsStream; 20 | 21 | mod tls_config; 22 | use tls_config::tls_acceptor; 23 | 24 | #[inline] 25 | async fn handle_stream(stream: TlsStream, _remote_addr: SocketAddr) { 26 | let (mut reader, mut writer) = split(stream); 27 | match copy(&mut reader, &mut writer).await { 28 | Ok(cnt) => eprintln!("Processed {} bytes", cnt), 29 | Err(err) => eprintln!("Error during copy: {}", err), 30 | }; 31 | } 32 | 33 | /// For example try opening and closing a connection with: 34 | /// `echo "Q" | openssl s_client -connect localhost:3000` 35 | #[tokio::main(flavor = "current_thread")] 36 | async fn main() -> Result<(), Box> { 37 | let addr: SocketAddr = ([127, 0, 0, 1], 3000).into(); 38 | 39 | let listener = TcpListener::bind(&addr).await?; 40 | 41 | TlsListener::new(tls_acceptor(), listener) 42 | .take_until(ctrl_c()) 43 | .for_each_concurrent(None, |s| async { 44 | match s { 45 | Ok((stream, remote_addr)) => { 46 | handle_stream(stream, remote_addr).await; 47 | } 48 | Err(e) => { 49 | if let Some(remote_addr) = e.peer_addr() { 50 | eprint!("[client {remote_addr}] "); 51 | } 52 | 53 | eprintln!("Error accepting connection: {:?}", e); 54 | } 55 | } 56 | }) 57 | .await; 58 | Ok(()) 59 | } 60 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | push: 4 | branches: 5 | - main 6 | paths-ignore: 7 | - '*.md' 8 | pull_request: 9 | 10 | jobs: 11 | build-full: 12 | runs-on: ${{ matrix.os }} 13 | strategy: 14 | matrix: 15 | os: [ubuntu-latest, windows-latest] 16 | rust: [stable, beta, nightly] 17 | continue-on-error: ${{ matrix.rust == 'nightly' }} 18 | steps: 19 | - uses: actions/checkout@v6 20 | - name: Install toolchain 21 | run: | 22 | rustup toolchain install ${{ matrix.rust }} --profile minimal --component clippy,rustfmt 23 | rustup default ${{ matrix.rust }} 24 | - name: Install OpenSSL on Windows 25 | if: runner.os == 'Windows' 26 | run: | 27 | vcpkg integrate install 28 | vcpkg.exe install openssl:x64-windows-static-md 29 | - name: Build 30 | run: cargo build --verbose --examples --all-features 31 | - name: Test 32 | run: cargo test --verbose --all-features 33 | - name: Lint 34 | run: cargo clippy --examples --all-features 35 | - name: Format check 36 | run: cargo fmt -- --check 37 | check-features: 38 | runs-on: ubuntu-latest 39 | strategy: 40 | matrix: 41 | features: 42 | - "" 43 | - tokio-net 44 | - rustls 45 | - native-tls 46 | - openssl 47 | - rt 48 | - rustls,native-tls,openssl 49 | - tokio-net,rt,rustls 50 | - tokio-net,native-tls 51 | steps: 52 | - uses: actions/checkout@v6 53 | - run: | 54 | rustup toolchain install stable --profile minimal 55 | rustup default stable 56 | - name: Check 57 | run: cargo check --verbose --no-default-features --features "${{ matrix.features }}" 58 | test-examples: 59 | runs-on: ubuntu-latest 60 | steps: 61 | - uses: actions/checkout@v6 62 | - run: | 63 | rustup toolchain install stable --profile minimal 64 | rustup default stable 65 | - name: Test examples 66 | run: python -m unittest examples.test_examples 67 | -------------------------------------------------------------------------------- /src/spawning_handshake.rs: -------------------------------------------------------------------------------- 1 | use super::AsyncTls; 2 | use std::future::Future; 3 | use std::pin::Pin; 4 | use std::task::{Context, Poll}; 5 | use tokio::io::{AsyncRead, AsyncWrite}; 6 | use tokio::task::JoinHandle; 7 | 8 | /// Convert an [`AsyncTls`] into one that will spawn a new task for each new connection. 9 | /// 10 | /// This will wrap each call to [`accept`](AsyncTls::accept) with a call to [`tokio::spawn`]. This 11 | /// is especially useful when using a multi-threaded runtime, so that the TLS handshakes 12 | /// are distributed between multiple threads. 13 | #[cfg_attr(docsrs, doc(cfg(feature = "rt")))] 14 | #[derive(Clone, Debug)] 15 | pub struct SpawningHandshakes(pub T); 16 | 17 | impl AsyncTls for SpawningHandshakes 18 | where 19 | T: AsyncTls, 20 | C: AsyncRead + AsyncWrite, 21 | T::AcceptFuture: Send + 'static, 22 | T::Stream: Send + 'static, 23 | T::Error: Send + 'static, 24 | { 25 | type Stream = T::Stream; 26 | type Error = T::Error; 27 | type AcceptFuture = HandshakeJoin; 28 | 29 | fn accept(&self, stream: C) -> Self::AcceptFuture { 30 | HandshakeJoin(tokio::spawn(self.0.accept(stream))) 31 | } 32 | } 33 | 34 | /// Future type returned by [`SpawningHandshakeTls::accept`]; 35 | #[cfg_attr(docsrs, doc(cfg(feature = "rt")))] 36 | pub struct HandshakeJoin(JoinHandle>); 37 | 38 | impl Future for HandshakeJoin { 39 | type Output = Result; 40 | fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { 41 | match Pin::new(&mut self.as_mut().0).poll(cx) { 42 | Poll::Ready(Ok(v)) => Poll::Ready(v), 43 | Poll::Pending => Poll::Pending, 44 | Poll::Ready(Err(e)) => { 45 | if e.is_panic() { 46 | std::panic::resume_unwind(e.into_panic()); 47 | } else { 48 | unreachable!("Tls handshake was aborted: {:?}", e); 49 | } 50 | } 51 | } 52 | } 53 | } 54 | 55 | impl Drop for HandshakeJoin { 56 | fn drop(&mut self) { 57 | self.0.abort(); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/axum.rs: -------------------------------------------------------------------------------- 1 | use super::{AsyncAccept, AsyncListener, AsyncTls, Error, TlsListener}; 2 | use std::io; 3 | use std::marker::Unpin; 4 | 5 | use axum::serve::Listener; 6 | use tokio::io::{AsyncRead, AsyncWrite}; 7 | use tracing::error; 8 | 9 | #[cfg_attr(docsrs, doc(cfg(feature = "axum")))] 10 | impl Listener for TlsListener 11 | where 12 | Self: Unpin + Send + 'static, 13 | A: AsyncAccept + AsyncListener, 14 | A::Address: Send, 15 | T: AsyncTls, 16 | T::Error: Send, 17 | T::Stream: AsyncRead + AsyncWrite + Unpin + Send + 'static, 18 | { 19 | type Io = T::Stream; 20 | type Addr = A::Address; 21 | 22 | async fn accept(&mut self) -> (Self::Io, Self::Addr) { 23 | loop { 24 | match TlsListener::accept(self).await { 25 | Ok(conn) => break conn, 26 | // Is there something better we could do here? 27 | // log with tracing library? 28 | Err(Error::ListenerError(e)) => handle_accept_error(e).await, 29 | Err(e) => error!("TLS accept error: {}", e), 30 | } 31 | } 32 | } 33 | 34 | fn local_addr(&self) -> tokio::io::Result { 35 | self.listener().local_addr() 36 | } 37 | } 38 | 39 | // This mirrors https://github.com/tokio-rs/axum/blob/8954d7922a7e81de0cff50078c47381299776897/axum/src/serve/listener.rs#L245 40 | // which in turn references https://github.com/hyperium/hyper/blob/v0.14.27/src/server/tcp.rs#L186 41 | // 42 | // > A possible scenario is that the process has hit the max open files 43 | // > allowed, and so trying to accept a new connection will fail with 44 | // > `EMFILE`. In some cases, it's preferable to just wait for some time, if 45 | // > the application will likely close some files (or connections), and try 46 | // > to accept the connection again. If this option is `true`, the error 47 | // > will be logged at the `error` level, since it is still a big deal, 48 | // > and then the listener will sleep for 1 second. 49 | // 50 | // TODO: have a way to customize error handling 51 | async fn handle_accept_error(e: io::Error) { 52 | if is_connection_error(&e) { 53 | return; 54 | } 55 | error!("accept error: {e}"); 56 | tokio::time::sleep(std::time::Duration::from_secs(1)).await; 57 | } 58 | 59 | fn is_connection_error(e: &io::Error) -> bool { 60 | use std::io::ErrorKind::*; 61 | matches!( 62 | e.kind(), 63 | ConnectionRefused | ConnectionAborted | ConnectionReset 64 | ) 65 | } 66 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "tls-listener" 3 | description = "wrap incoming Stream of connections in TLS" 4 | version = "v0.12.0" 5 | authors = ["Thayne McCombs "] 6 | repository = "https://github.com/tmccombs/tls-listener" 7 | edition = "2024" 8 | license = "Apache-2.0" 9 | rust-version = "1.85" 10 | 11 | [features] 12 | default = ["tokio-net"] 13 | rustls-core = ["tokio-rustls"] 14 | rustls-aws-lc = ["rustls-core", "tokio-rustls/aws-lc-rs"] 15 | rustls-fips = ["rustls-aws-lc", "tokio-rustls/fips"] 16 | rustls-ring = ["rustls-core", "tokio-rustls/ring"] 17 | rustls = ["rustls-aws-lc", "tokio-rustls/default"] 18 | native-tls = ["tokio-native-tls"] 19 | openssl = ["tokio-openssl", "openssl_impl"] 20 | rt = ["tokio/rt"] 21 | axum = ["dep:axum", "tracing"] 22 | 23 | tokio-net = ["tokio/net"] 24 | 25 | [dependencies] 26 | futures-util = "0.3.8" 27 | pin-project-lite = "0.2.13" 28 | thiserror = "2.0.3" 29 | tokio = { version = "1.0", features = ["time"] } 30 | tokio-native-tls = { version = "0.3.0", optional = true } 31 | tokio-rustls = { version = "0.26.1", default-features = false, optional = true } 32 | tokio-openssl = { version = "0.6.3", optional = true } 33 | openssl_impl = { package = "openssl", version = "0.10.32", optional = true } 34 | tracing = { version = "0.1.41", optional = true } 35 | 36 | [dependencies.axum] 37 | version = "0.8.1" 38 | optional = true 39 | default-features = false 40 | # http2 would work instead of http1, but we need one of them in order to get 41 | # the serve mod. 42 | features = ["tokio", "http1"] 43 | 44 | 45 | [dev-dependencies] 46 | axum = "0.8.1" 47 | hyper = { version = "1.0", features = ["http1", "server"] } 48 | hyper-util = { version = "0.1.1", features = ["tokio"] } 49 | tokio = { version = "1.0", features = [ 50 | "rt", 51 | "macros", 52 | "net", 53 | "io-util", 54 | "signal", 55 | ] } 56 | 57 | [[example]] 58 | name = "http" 59 | path = "examples/http.rs" 60 | 61 | [[example]] 62 | name = "http-stream" 63 | path = "examples/http-stream.rs" 64 | 65 | [[example]] 66 | name = "echo" 67 | path = "examples/echo.rs" 68 | required-features = ["tokio-net"] 69 | 70 | [[example]] 71 | name = "echo-threads" 72 | path = "examples/echo-threads.rs" 73 | required-features = ["tokio-net", "rt", "tokio/rt-multi-thread"] 74 | 75 | [[example]] 76 | name = "http-change-certificate" 77 | path = "examples/http-change-certificate.rs" 78 | 79 | [[example]] 80 | name = "axum" 81 | path = "examples/axum.rs" 82 | required-features = ["axum"] 83 | 84 | [package.metadata.docs.rs] 85 | features = [ 86 | "rustls-core", 87 | "native-tls", 88 | "openssl", 89 | "rt" 90 | ] 91 | rustdoc-args = ["--cfg", "docsrs"] 92 | -------------------------------------------------------------------------------- /examples/http-change-certificate.rs: -------------------------------------------------------------------------------- 1 | use hyper::server::conn::http1; 2 | use hyper::service::service_fn; 3 | use hyper::{Request, Response, body::Body}; 4 | use hyper_util::rt::tokio::TokioIo; 5 | use std::convert::Infallible; 6 | use std::num::NonZeroUsize; 7 | use std::sync::Arc; 8 | use std::sync::atomic::{AtomicU64, Ordering}; 9 | use tokio::net::TcpListener; 10 | 11 | mod tls_config; 12 | use tls_config::{Acceptor, tls_acceptor, tls_acceptor2}; 13 | use tokio::sync::mpsc; 14 | 15 | /// To view the current certificate try: 16 | /// `echo "Q" |openssl s_client -showcerts -connect 127.0.0.1:3000 | grep subject=CN` 17 | /// 18 | /// To change the certificate make a HTTP request: 19 | /// `curl https://127.0.0.1:3000 -k` 20 | #[tokio::main(flavor = "current_thread")] 21 | async fn main() { 22 | let addr: std::net::SocketAddr = ([127, 0, 0, 1], 3000).into(); 23 | let counter = Arc::new(AtomicU64::new(0)); 24 | 25 | let mut listener = tls_listener::builder(tls_acceptor()) 26 | .accept_batch_size(NonZeroUsize::new(10).unwrap()) 27 | .listen(TcpListener::bind(addr).await.expect("Failed to bind port")); 28 | 29 | let (tx, mut rx) = mpsc::channel::(1); 30 | 31 | let http = http1::Builder::new(); 32 | loop { 33 | tokio::select! { 34 | conn = listener.accept() => { 35 | match conn { 36 | Ok((conn, remote_addr)) => { 37 | let http = http.clone(); 38 | let tx = tx.clone(); 39 | let counter = counter.clone(); 40 | tokio::spawn(async move { 41 | let svc = service_fn(move |request| handle_request(tx.clone(), counter.clone(), request)); 42 | if let Err(err) = http.serve_connection(TokioIo::new(conn), svc).await { 43 | eprintln!("Application error (client address: {remote_addr}): {err}"); 44 | } 45 | }); 46 | }, 47 | Err(e) => { 48 | if let Some(remote_addr) = e.peer_addr() { 49 | eprint!("[client {remote_addr}] "); 50 | } 51 | 52 | eprintln!("Bad connection: {}", e); 53 | } 54 | } 55 | }, 56 | message = rx.recv() => { 57 | // Certificate is loaded on another task; we don't want to block the listener loop 58 | let acceptor = message.expect("Channel should not be closed"); 59 | println!("Rotating certificate..."); 60 | listener.replace_acceptor(acceptor); 61 | } 62 | } 63 | } 64 | } 65 | 66 | async fn handle_request( 67 | change_certificate: mpsc::Sender, 68 | counter: Arc, 69 | _request: Request, 70 | ) -> Result, Infallible> { 71 | let counter = counter.fetch_add(1, Ordering::Relaxed) + 1; 72 | let new_cert = if counter % 2 == 0 { 73 | tls_acceptor() 74 | } else { 75 | tls_acceptor2() 76 | }; 77 | change_certificate.send(new_cert).await.ok(); 78 | Ok(Response::new("Changing certificate...".into())) 79 | } 80 | -------------------------------------------------------------------------------- /src/accept.rs: -------------------------------------------------------------------------------- 1 | use pin_project_lite::pin_project; 2 | use std::fmt::Debug; 3 | use std::future::Future; 4 | use std::pin::Pin; 5 | use std::task::{Context, Poll}; 6 | 7 | use tokio::io::{AsyncRead, AsyncWrite}; 8 | 9 | /// Asynchronously accept connections. 10 | pub trait AsyncAccept { 11 | /// The type of the connection that is accepted. 12 | type Connection: AsyncRead + AsyncWrite; 13 | /// The type of the remote address, such as [`std::net::SocketAddr`]. 14 | /// 15 | /// If no remote address can be determined (such as for mock connections), 16 | /// `()` or a similar dummy type can be used. 17 | type Address: Debug; 18 | /// The type of error that may be returned. 19 | type Error: std::error::Error; 20 | 21 | /// Poll to accept the next connection. 22 | /// 23 | /// On success return the new connection, and the address of the peer. 24 | #[allow(clippy::type_complexity)] 25 | fn poll_accept( 26 | self: Pin<&mut Self>, 27 | cx: &mut Context<'_>, 28 | ) -> Poll>; 29 | } 30 | 31 | pin_project! { 32 | struct AcceptGenerator 33 | { 34 | accept: A, 35 | #[pin] 36 | current: F, 37 | } 38 | } 39 | 40 | impl AsyncAccept for AcceptGenerator 41 | where 42 | A: FnMut() -> F, 43 | Conn: AsyncRead + AsyncWrite, 44 | E: std::error::Error, 45 | Addr: Debug, 46 | F: Future>, 47 | { 48 | type Connection = Conn; 49 | type Address = Addr; 50 | type Error = E; 51 | 52 | fn poll_accept( 53 | self: Pin<&mut Self>, 54 | cx: &mut Context<'_>, 55 | ) -> Poll> { 56 | let mut this = self.project(); 57 | 58 | let result = this.current.as_mut().poll(cx); 59 | if result.is_ready() { 60 | // Prime the future for the next poll 61 | let next = (this.accept)(); 62 | this.current.set(next); 63 | } 64 | result 65 | } 66 | } 67 | 68 | /// Create a new `AsyncAccept` from a generator function 69 | /// 70 | /// This allows you to create an `AsyncAccept` implementation from any 71 | /// function that can generate futures of connections. 72 | /// 73 | /// `accept_fn` will be called immediately, to create the initial future, then again 74 | /// whenever polling the `Future` returned by the previous call returns a ready status. 75 | /// 76 | /// This function is experimental, and may be changed or removed in future versions 77 | pub fn accept_generator( 78 | mut accept_fn: Acc, 79 | ) -> impl AsyncAccept + Send 80 | where 81 | Acc: (FnMut() -> F) + Send, 82 | Conn: AsyncRead + AsyncWrite + 'static, 83 | Addr: Debug + 'static, 84 | F: Future> + Send, 85 | E: std::error::Error + 'static, 86 | { 87 | let first_future = (accept_fn)(); 88 | AcceptGenerator { 89 | accept: accept_fn, 90 | current: first_future, 91 | } 92 | } 93 | 94 | ///An AsyncListener that can also report its local address 95 | pub trait AsyncListener: AsyncAccept { 96 | /// The local address of the listener, if available. 97 | fn local_addr(&self) -> Result; 98 | } 99 | -------------------------------------------------------------------------------- /cliff.toml: -------------------------------------------------------------------------------- 1 | # git-cliff ~ default configuration file 2 | # https://git-cliff.org/docs/configuration 3 | # 4 | # Lines starting with "#" are comments. 5 | # Configuration options are organized into tables and keys. 6 | # See documentation for more information on available options. 7 | 8 | [changelog] 9 | # changelog header 10 | header = """ 11 | # Changelog\n 12 | All notable changes to this project will be documented in this file.\n 13 | """ 14 | # template for the changelog body 15 | # https://keats.github.io/tera/docs/#introduction 16 | body = """ 17 | {% if version %}\ 18 | ## [{{ version | trim_start_matches(pat="v") }}] - {{ timestamp | date(format="%Y-%m-%d") }} 19 | {% else %}\ 20 | ## [unreleased] 21 | {% endif %}\ 22 | {% for group, commits in commits | group_by(attribute="group") %} 23 | ### {{ group | upper_first }} 24 | {% for commit in commits %} 25 | - {% if commit.breaking %}[**breaking**] {% endif %}{{ commit.message | upper_first }}\ 26 | {% for footer in commit.footers | filter(attribute="breaking", value=true) %} 27 | {% raw %} {% endraw %}* BREAKING CHANGE: {{ footer.value }}\ 28 | {% endfor %} 29 | {% endfor %} 30 | {% endfor %}\n 31 | """ 32 | # remove the leading and trailing whitespace from the template 33 | trim = true 34 | # changelog footer 35 | footer = """ 36 | 37 | """ 38 | # postprocessors 39 | postprocessors = [ 40 | # { pattern = '', replace = "https://github.com/orhun/git-cliff" }, # replace repository URL 41 | ] 42 | [git] 43 | # parse the commits based on https://www.conventionalcommits.org 44 | conventional_commits = true 45 | # filter out the commits that are not conventional 46 | filter_unconventional = true 47 | # process each line of a commit as an individual commit 48 | split_commits = false 49 | # regex for preprocessing the commit messages 50 | commit_preprocessors = [ 51 | # { pattern = '\((\w+\s)?#([0-9]+)\)', replace = "([#${2}](/issues/${2}))"}, # replace issue numbers 52 | ] 53 | # regex for parsing and grouping commits 54 | commit_parsers = [ 55 | { message = "^feat", group = "Features" }, 56 | { message = "^fix", group = "Bug Fixes" }, 57 | { message = "^doc", group = "Documentation" }, 58 | { message = "^perf", group = "Performance" }, 59 | { message = "^refactor", group = "Refactor" }, 60 | { message = "^style", group = "Styling" }, 61 | { message = "^test", group = "Testing" }, 62 | { message = "^chore\\(release\\): prepare for", skip = true }, 63 | { message = "^chore\\(deps\\)", skip = true }, 64 | { message = "^chore\\(pr\\)", skip = true }, 65 | { message = "^chore\\(pull\\)", skip = true }, 66 | { message = "^chore|ci", group = "Miscellaneous Tasks" }, 67 | { body = ".*security", group = "Security" }, 68 | { message = "^revert", group = "Revert" }, 69 | ] 70 | # protect breaking changes from being skipped due to matching a skipping commit_parser 71 | protect_breaking_commits = false 72 | # filter out the commits that are not matched by commit parsers 73 | filter_commits = false 74 | # glob pattern for matching git tags 75 | tag_pattern = "v[0-9]*" 76 | # regex for skipping tags 77 | skip_tags = "v0.1.0-beta.1" 78 | # regex for ignoring tags 79 | ignore_tags = "" 80 | # sort the tags topologically 81 | topo_order = false 82 | # sort the commits inside sections by oldest/newest order 83 | sort_commits = "oldest" 84 | # limit the number of commits included in the changelog. 85 | # limit_commits = 42 86 | -------------------------------------------------------------------------------- /examples/tls_config/mod.rs: -------------------------------------------------------------------------------- 1 | #[cfg(feature = "rustls-core")] 2 | mod config { 3 | use std::sync::Arc; 4 | use tokio_rustls::rustls::{ 5 | ServerConfig, 6 | pki_types::{CertificateDer, PrivateKeyDer}, 7 | }; 8 | 9 | const CERT: &[u8] = include_bytes!("local.cert"); 10 | const PKEY: &[u8] = include_bytes!("local.key"); 11 | #[allow(dead_code)] 12 | const CERT2: &[u8] = include_bytes!("local2.cert"); 13 | #[allow(dead_code)] 14 | const PKEY2: &[u8] = include_bytes!("local2.key"); 15 | 16 | pub type Acceptor = tokio_rustls::TlsAcceptor; 17 | 18 | #[allow(dead_code)] 19 | pub type Stream = tokio_rustls::server::TlsStream; 20 | 21 | fn tls_acceptor_impl(key_der: &[u8], cert_der: &[u8]) -> Acceptor { 22 | let key = PrivateKeyDer::Pkcs1(key_der.to_owned().into()); 23 | let cert = CertificateDer::from(cert_der).into_owned(); 24 | Arc::new( 25 | ServerConfig::builder() 26 | .with_no_client_auth() 27 | .with_single_cert(vec![cert], key) 28 | .unwrap(), 29 | ) 30 | .into() 31 | } 32 | 33 | pub fn tls_acceptor() -> Acceptor { 34 | tls_acceptor_impl(PKEY, CERT) 35 | } 36 | 37 | #[allow(dead_code)] 38 | pub fn tls_acceptor2() -> Acceptor { 39 | tls_acceptor_impl(PKEY2, CERT2) 40 | } 41 | } 42 | 43 | #[cfg(all( 44 | feature = "native-tls", 45 | not(any(feature = "rustls-core", feature = "openssl")) 46 | ))] 47 | mod config { 48 | use tokio_native_tls::native_tls::{Identity, TlsAcceptor}; 49 | 50 | const PFX: &[u8] = include_bytes!("local.pfx"); 51 | const PFX2: &[u8] = include_bytes!("local2.pfx"); 52 | 53 | pub type Acceptor = tokio_native_tls::TlsAcceptor; 54 | 55 | #[allow(dead_code)] 56 | pub type Stream = tokio_native_tls::TlsStream; 57 | 58 | fn tls_acceptor_impl(pfx: &[u8]) -> Acceptor { 59 | let identity = Identity::from_pkcs12(pfx, "").unwrap(); 60 | TlsAcceptor::builder(identity).build().unwrap().into() 61 | } 62 | 63 | pub fn tls_acceptor() -> Acceptor { 64 | tls_acceptor_impl(PFX) 65 | } 66 | 67 | pub fn tls_acceptor2() -> Acceptor { 68 | tls_acceptor_impl(PFX2) 69 | } 70 | } 71 | 72 | #[cfg(all( 73 | feature = "openssl", 74 | not(any(feature = "rustls-core", feature = "native-tls")) 75 | ))] 76 | mod config { 77 | use openssl_impl::ssl::{SslContext, SslFiletype, SslMethod}; 78 | use std::path::Path; 79 | 80 | pub type Acceptor = openssl_impl::ssl::SslContext; 81 | 82 | #[allow(dead_code)] 83 | pub type Stream = tokio_openssl::SslStream; 84 | 85 | fn tls_acceptor_impl>(cert_file: P, key_file: P) -> Acceptor { 86 | let mut builder = SslContext::builder(SslMethod::tls_server()).unwrap(); 87 | builder 88 | .set_certificate_file(cert_file, SslFiletype::ASN1) 89 | .unwrap(); 90 | builder 91 | .set_private_key_file(key_file, SslFiletype::ASN1) 92 | .unwrap(); 93 | builder.build() 94 | } 95 | 96 | pub fn tls_acceptor() -> Acceptor { 97 | tls_acceptor_impl( 98 | "./examples/tls_config/local.cert", 99 | "./examples/tls_config/local.key", 100 | ) 101 | } 102 | 103 | pub fn tls_acceptor2() -> Acceptor { 104 | tls_acceptor_impl( 105 | "./examples/tls_config/local2.cert", 106 | "./examples/tls_config/local2.key", 107 | ) 108 | } 109 | } 110 | 111 | pub use config::*; 112 | -------------------------------------------------------------------------------- /tests/basic.rs: -------------------------------------------------------------------------------- 1 | use futures_util::future::{self, Ready}; 2 | use std::io::{Error, ErrorKind, Result}; 3 | 4 | mod helper; 5 | 6 | use helper::*; 7 | use helper::{assert_ascii_eq, assert_err}; 8 | use tokio::io::{AsyncWriteExt, DuplexStream}; 9 | use tokio::spawn; 10 | use tokio::sync::oneshot; 11 | 12 | use futures_util::StreamExt; 13 | 14 | use tls_listener::Error::*; 15 | use tls_listener::{AsyncTls, TlsListener}; 16 | 17 | #[tokio::test] 18 | async fn accept_connections() { 19 | let (connecter, listener) = setup(); 20 | 21 | spawn(listener.for_each_concurrent(None, |s| async { 22 | s.expect("unexpected error") 23 | .0 24 | .write_all(b"HELLO, WORLD!") 25 | .await 26 | .unwrap(); 27 | })); 28 | 29 | assert_ascii_eq!( 30 | connecter.send_data(b"hello, bob.").await.unwrap(), 31 | b"hello, world!" 32 | ); 33 | assert_ascii_eq!( 34 | connecter.send_data(b"hello, orange.").await.unwrap(), 35 | b"hello, world!" 36 | ); 37 | assert_ascii_eq!( 38 | connecter.send_data(b"hello, banana.").await.unwrap(), 39 | b"hello, world!" 40 | ); 41 | } 42 | 43 | #[tokio::test] 44 | async fn stream_error() { 45 | let (connecter, mut listener) = setup(); 46 | 47 | connecter 48 | .send_error(Error::new(ErrorKind::ConnectionReset, "test")) 49 | .await; 50 | assert_err!(listener.accept().await, ListenerError(_)); 51 | } 52 | 53 | #[tokio::test] 54 | async fn tls_error() { 55 | #[derive(Clone)] 56 | struct ErrTls; 57 | impl AsyncTls for ErrTls { 58 | type Stream = DuplexStream; 59 | type Error = Error; 60 | type AcceptFuture = Ready>; 61 | 62 | fn accept(&self, _: DuplexStream) -> Self::AcceptFuture { 63 | future::ready(Err(Error::new(ErrorKind::ConnectionReset, "test"))) 64 | } 65 | } 66 | let (connect, accept) = accepting(); 67 | spawn(async move { connect.send_data(b"foo").await }); 68 | let mut listener = TlsListener::new(ErrTls, accept); 69 | 70 | assert_err!( 71 | listener.accept().await, 72 | TlsAcceptError { 73 | peer_addr: MockAddress(42), 74 | .. 75 | } 76 | ); 77 | } 78 | 79 | static LONG_TEXT: &[u8] = include_bytes!("long_text.txt"); 80 | 81 | #[tokio::test] 82 | async fn echo() { 83 | let (ender, ended) = oneshot::channel(); 84 | let (connector, listener) = setup_echo(ended); 85 | 86 | async fn check_message(c: &MockConnect, msg: &[u8]) { 87 | let resp = c.send_data(msg).await; 88 | assert_ascii_eq!(resp.unwrap(), msg.to_ascii_lowercase()); 89 | } 90 | 91 | let c = &connector; 92 | 93 | tokio::join!( 94 | check_message(c, b"test"), 95 | check_message(c, b"blue CheEse"), 96 | check_message( 97 | c, 98 | b"This is some text, that is a little longer than the other ones." 99 | ), 100 | check_message(c, LONG_TEXT), 101 | check_message(c, LONG_TEXT), 102 | ); 103 | ender.send(()).unwrap(); 104 | 105 | if let Err(e) = listener.await { 106 | std::panic::resume_unwind(e.into_panic()); 107 | } 108 | } 109 | 110 | #[tokio::test] 111 | async fn addr() { 112 | let (connector, mut listener) = setup(); 113 | 114 | spawn(async move { 115 | connector.send_data(b"hi").await.unwrap(); 116 | connector.send_data(b"boo").await.unwrap(); 117 | connector.send_data(b"test").await.unwrap(); 118 | }); 119 | 120 | for i in 42..44 { 121 | assert_eq!(listener.accept().await.unwrap().1, MockAddress(i)); 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /tests/helper/mocks.rs: -------------------------------------------------------------------------------- 1 | use futures_util::future::{self, Ready}; 2 | use futures_util::ready; 3 | use std::io; 4 | use std::pin::Pin; 5 | use std::sync::atomic::{AtomicU32, Ordering}; 6 | use std::task::{Context, Poll}; 7 | use tokio::io::{ 8 | AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt, DuplexStream, ReadBuf, duplex, split, 9 | }; 10 | use tokio::sync::mpsc; 11 | 12 | use tls_listener::{AsyncAccept, AsyncTls}; 13 | 14 | type ConnResult = io::Result<(DuplexStream, MockAddress)>; 15 | 16 | pub struct MockAccept { 17 | chan: mpsc::Receiver, 18 | } 19 | 20 | pub struct MockConnect { 21 | chan: mpsc::Sender, 22 | counter: AtomicU32, 23 | } 24 | 25 | #[derive(Clone, Copy, Debug, PartialEq)] 26 | pub struct MockAddress(pub u32); 27 | 28 | pub fn accepting() -> (MockConnect, MockAccept) { 29 | let (tx, rx) = mpsc::channel(32); 30 | ( 31 | MockConnect { 32 | chan: tx, 33 | counter: AtomicU32::new(42), 34 | }, 35 | MockAccept { chan: rx }, 36 | ) 37 | } 38 | 39 | impl MockConnect { 40 | pub async fn connect(&self) -> DuplexStream { 41 | let (tx, rx) = duplex(1024); 42 | let count = self.counter.fetch_add(1, Ordering::Relaxed); 43 | self.chan.send(Ok((rx, MockAddress(count)))).await.unwrap(); 44 | tx 45 | } 46 | 47 | pub async fn send_error(&self, err: io::Error) { 48 | self.chan.send(Err(err)).await.unwrap(); 49 | } 50 | 51 | pub async fn send_data(&self, data: &[u8]) -> io::Result> { 52 | let stream = self.connect().await; 53 | let (mut read, mut write) = split(stream); 54 | let mut buf = Vec::new(); 55 | 56 | tokio::try_join!( 57 | async move { 58 | write.write_all(data).await?; 59 | write.shutdown().await?; 60 | Ok(()) 61 | }, 62 | read.read_to_end(&mut buf), 63 | )?; 64 | Ok(buf) 65 | } 66 | } 67 | 68 | impl AsyncAccept for MockAccept { 69 | type Connection = DuplexStream; 70 | type Error = io::Error; 71 | type Address = MockAddress; 72 | 73 | fn poll_accept(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { 74 | Pin::into_inner(self).chan.poll_recv(cx).map(|c| c.unwrap()) 75 | } 76 | } 77 | 78 | #[derive(Clone)] 79 | pub struct MockTls; 80 | 81 | impl AsyncTls for MockTls { 82 | type Stream = MockTlsStream; 83 | type Error = io::Error; 84 | type AcceptFuture = Ready>; 85 | 86 | fn accept(&self, conn: DuplexStream) -> Self::AcceptFuture { 87 | future::ready(Ok(MockTlsStream(conn))) 88 | } 89 | } 90 | 91 | #[derive(Debug)] 92 | pub struct MockTlsStream(DuplexStream); 93 | 94 | impl MockTlsStream { 95 | fn inner(self: Pin<&mut Self>) -> Pin<&mut DuplexStream> { 96 | Pin::new(&mut Pin::into_inner(self).0) 97 | } 98 | } 99 | 100 | impl AsyncRead for MockTlsStream { 101 | fn poll_read( 102 | self: Pin<&mut Self>, 103 | cx: &mut Context<'_>, 104 | buf: &mut ReadBuf<'_>, 105 | ) -> Poll> { 106 | ready!(self.inner().poll_read(cx, buf))?; 107 | buf.filled_mut().make_ascii_uppercase(); 108 | Poll::Ready(Ok(())) 109 | } 110 | } 111 | 112 | impl AsyncWrite for MockTlsStream { 113 | fn poll_write( 114 | self: Pin<&mut Self>, 115 | cx: &mut Context<'_>, 116 | buf: &[u8], 117 | ) -> Poll> { 118 | let data = buf.to_ascii_lowercase(); 119 | self.inner().poll_write(cx, &data) 120 | } 121 | 122 | fn poll_flush(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { 123 | self.inner().poll_flush(cx) 124 | } 125 | 126 | fn poll_shutdown(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { 127 | self.inner().poll_shutdown(cx) 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /examples/test_examples.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 3 | This script is intended to test the examples in the `examples` folder. 4 | """ 5 | 6 | import signal 7 | import socket 8 | import ssl 9 | import subprocess 10 | import time 11 | import unittest 12 | from contextlib import contextmanager 13 | from http.client import HTTPConnection 14 | from os import path 15 | from urllib.request import HTTPSHandler, build_opener 16 | 17 | HOST = "localhost" 18 | PORT = 3000 19 | DOMAIN = "test.local" 20 | EXAMPLE_DIR = path.dirname(__file__) 21 | CA_FILE = path.join(EXAMPLE_DIR, "tls_config", "localcert.pem") 22 | 23 | context = ssl.create_default_context(cafile=CA_FILE) 24 | context.options = ssl.PROTOCOL_TLS_CLIENT 25 | 26 | 27 | class ExampleHttpsConnection(HTTPConnection): 28 | def connect(self): 29 | super().connect() 30 | self.sock = context.wrap_socket(self.sock, server_hostname=DOMAIN) 31 | 32 | 33 | class ExampleHttpsHandler(HTTPSHandler): 34 | def __init__(self, debuglevel=0): 35 | super().__init__(debuglevel, context=context) 36 | 37 | def https_open(self, req): 38 | return self.do_open(ExampleHttpsConnection, req) 39 | 40 | 41 | opener = build_opener(ExampleHttpsHandler) 42 | 43 | 44 | @contextmanager 45 | def tls_conn(): 46 | with socket.create_connection((HOST, PORT)) as sock: 47 | with context.wrap_socket(sock, server_hostname=DOMAIN) as tls: 48 | yield tls 49 | tls.shutdown(socket.SHUT_RDWR) 50 | 51 | 52 | def build_examples(): 53 | proc = subprocess.run( 54 | [ 55 | "cargo", 56 | "build", 57 | "--examples", 58 | "--features", 59 | "rustls,rt,tokio/rt-multi-thread,axum", 60 | ] 61 | ) 62 | proc.check_returncode() 63 | 64 | 65 | @contextmanager 66 | def run_example(name): 67 | proc = subprocess.Popen( 68 | path.join(EXAMPLE_DIR, "..", "target", "debug", "examples", name), 69 | ) 70 | try: 71 | time.sleep(0.1) # wait for process to start up 72 | yield proc 73 | finally: 74 | proc.terminate() 75 | proc.wait() # avoid warning about process still running 76 | 77 | 78 | class TestExamples(unittest.TestCase): 79 | @classmethod 80 | def setUpClass(cls): 81 | build_examples() 82 | 83 | def run_echo_test(self, conn): 84 | r = conn.makefile("r") 85 | w = conn.makefile("w") 86 | 87 | w.write("hello\n") 88 | w.write("world\n") 89 | w.write("it's great\n") 90 | w.flush() 91 | for line in ["hello\n", "world\n", "it's great\n"]: 92 | self.assertEqual(r.readline(), line) 93 | w.write("goodbye\n") 94 | w.flush() 95 | self.assertEqual(r.readline(), "goodbye\n") 96 | 97 | def echo_test(self): 98 | with tls_conn() as tls: 99 | self.run_echo_test(tls) 100 | 101 | def http_test(self): 102 | with opener.open(f"https://{HOST}:{PORT}") as resp: 103 | self.assertEqual(resp.status, 200) 104 | self.assertEqual(resp.read(), b"Hello, World!") 105 | 106 | def bad_connection_test(self, proc): 107 | with socket.create_connection((HOST, PORT)) as s: 108 | s.send(b"bad data") 109 | self.assertIsNone(proc.poll(), "Bad data shouldn't kill process") 110 | 111 | def test_echo(self): 112 | with run_example("echo") as r: 113 | f"pid={r.pid}" 114 | self.echo_test() 115 | with tls_conn() as t: 116 | r.send_signal(signal.SIGINT) 117 | time.sleep(0.1) 118 | self.assertIsNone( 119 | r.poll(), "Process should wait for connections to end on ctrl-c" 120 | ) 121 | self.run_echo_test(t) 122 | r.wait(0.5) # process should finish shortly after connection finishes 123 | 124 | def test_echo_threads(self): 125 | with run_example("echo-threads"): 126 | self.echo_test() 127 | 128 | def test_http_stream(self): 129 | with run_example("http-stream") as r: 130 | self.http_test() 131 | self.bad_connection_test(r) 132 | 133 | def test_http_plain(self): 134 | with run_example("http"): 135 | self.http_test() 136 | 137 | def test_axum(self): 138 | with run_example("axum"): 139 | self.http_test() 140 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | ## [0.12.0] - 2025-12-13 6 | 7 | ### Features 8 | 9 | - Add cooldown to dependabot 10 | 11 | 12 | ### Miscellaneous Tasks 13 | 14 | - Update to 2024 edition 15 | 16 | - Run CI for windows-latest 17 | 18 | - Add cfg cond for unix on UnixListener 19 | 20 | - Bump actions/checkout from 5 to 6 21 | 22 | 23 | ### Build 24 | 25 | - Rename release as publish 26 | 27 | - Add action to do a release 28 | 29 | - Fix awk script 30 | 31 | 32 | ## [0.11.2] - 2025-11-22 33 | 34 | ### Fixed 35 | 36 | - Add cfg cond for unix on UnixListener (#64) 37 | 38 | 39 | ## [0.11.1] - 2025-11-20 40 | 41 | ### Features 42 | 43 | - Add listener() method 44 | 45 | - Accept_generator fn 46 | 47 | - Add AsyncListener trait 48 | 49 | - Axum support 50 | 51 | 52 | ## [0.10.3] 53 | 54 | ### Changes 55 | 56 | - Make docs.rs builds more reliable 57 | - Added Github action to automate publishing to crates.io 58 | 59 | ## [0.10.2] 60 | 61 | ### Changes 62 | 63 | - Add rustls-core feature flag, to allow depending on rustls without requiring the aws-lc backend. 64 | For backwards compatibility, the "rustls" feature continues to depend on aws-lc, but in a 65 | future version this will likely change to only require the core rustls crate with no features by default. 66 | 67 | ## [0.10.1] 68 | 69 | ### Changes 70 | 71 | - Allow using tokio-rustls version 0.26 72 | 73 | ## [0.10.0] - 2024-03-15 74 | 75 | ### Security Advisory 76 | 77 | Versions prior to this using the default configuration are vulnerable to a Slowloris attack. 78 | 79 | This version mitigates the vulnerability. 80 | 81 | Previous versions can mitigate the vulnerability by increasing the value passed to `Builder::max_handshakes` to a large 82 | number (such as `usize::MAX`). Decreasing the `handshake_timeout` can also help, although it is still strongly recommended 83 | to increase the `max_handshakes` more than the current default. 84 | 85 | ### Changes 86 | 87 | - [**breaking**] Change `poll_accept` not to have a limit on the number of pending handshakes in the queue, 88 | so that connections that are not making progress towards completing the handshake will not block other 89 | connections from being accepted. This replaces `Builder::max_handshakes` with `Builder::accept_batch_size`. 90 | 91 | ## [0.9.1] - 2023-12-23 92 | 93 | ### Miscellaneous Tasks 94 | 95 | - Update tokio-rustls 96 | 97 | 98 | ## [0.9.0] - 2023-12-05 99 | 100 | ### Features 101 | 102 | - [**breaking**] Remove until & remove option from accept 103 | * BREAKING CHANGE: remove `until` from AsyncAccept trait. Use 104 | `StreamExt.take_until` on the TlsListener instead. 105 | * BREAKING CHANGE: `accept` fn on AsyncAccept trait no longer returns an 106 | Option 107 | * BREAKING CHANGE: `accept` fn on TlsListener no longer returns an Option 108 | 109 | 110 | ### Upgrade 111 | 112 | - [**breaking**] Update to hyper 1.0 113 | * BREAKING CHANGE: Removed hyper-h1 and hyper-h2 features 114 | 115 | 116 | ## [0.8.0] - 2023-10-19 117 | 118 | This is a backwards incompatible release. The main change is that accepting a new connection now returns a tuple of the new connection, and the peer 119 | address. The `AsyncAccept` trait was also changed similarly. The `Error` enum was also changed to provide more details about the error. And if 120 | the handshake times out, it now returns an error instead of silently waiting for the next connection. 121 | 122 | ### Features 123 | 124 | - [**breaking**] Add a new error type for handshake timeouts 125 | * BREAKING CHANGE: Adds a new variant to the Error Enum 126 | * BREAKING CHANGE: The Error enum is now non_exhaustive 127 | * BREAKING CHANGE: Now returns an error if a handshake times out 128 | 129 | - [**breaking**] Yield remote address upon accepting a connection, and include it in errors. 130 | * BREAKING CHANGE: The enum variant `Error::ListenerError` is now struct-like instead of tuple-like, and is `non_exhaustive` like the enum itself. 131 | * BREAKING CHANGE: `Error` now has three type parameters, not two. 132 | * BREAKING CHANGE: `TlsListener::accept` and `::next` yields a tuple of (connection, remote address), not just the connection. 133 | * BREAKING CHANGE: `AsyncAccept` now has an associated type `Address`, which `poll_accept` must now return along with the accepted connection. 134 | 135 | - [**breaking**] More changes for including peer address in response 136 | * BREAKING CHANGE: AsyncAccept::Error must implement std::error::Error 137 | * BREAKING CHANGE: TlsAcceptError is now a struct form variant. 138 | 139 | ## 0.7.0 - 2023-03-31 140 | 141 | ### Changed 142 | - Increase tokio-rustls version to 0.24.0 143 | 144 | ## 0.6.0 - 2022-12-30 145 | 146 | ### Added 147 | - Added additional tests and examples 148 | - Re-export tls engine crates as public modules. 149 | 150 | ### Changed 151 | - Increased default handshake timeout to 10 seconds (technically a breaking change) 152 | 153 | ## 0.5.1 - 2022-03-21 154 | 155 | ### Added 156 | 157 | - Support for [`openssl`](https://github.com/sfackler/rust-openssl) 158 | 159 | ### Fixed 160 | 161 | - Fixed compilation on non-unix environments, where tokio-net doesn't include unix sockets 162 | - `SpawningHandshakes` will abort the tasks for pending connections when the linked futures are dropped. This should allow timeouts to cause the connectionto be closed. 163 | 164 | ## 0.5.0 - 2022-03-20 165 | 166 | ### Added 167 | 168 | - Added [`AsyncAccept::until`] method, that creates a new `AsyncAccept` that will stop accepting connections after another future finishes. 169 | - Added `hyper` submodule to add additional support for hyper. Specifically, a newtype for the hyper `Accept` trait for `AsyncAccept`. 170 | - Added `SpawningHandshakes` struct behind the `rt` feature flag. This allows you to perform multiple handshakes in parallel with a multi-threaded runtime. 171 | 172 | ### Changed 173 | - **Backwards incompatible**: `AsyncAccept::poll_accept` now returns, `Poll>>` instead of `Poll>`. This allows the incoming stream of connections to stop, for example, if a graceful shutdown has been initiated. `impl`s provided by this crate have been updated, but custom implementations of `AsyncAccept`, or direct usage of the trait may break. 174 | - Removed unnecessary type bounds (see #14). Potentially a breaking change, although I'd be suprised if any real code was affected. 175 | 176 | 177 | ## 0.4.3 - 2022-03-20 178 | 179 | - Added `TlsListener::replace_accept_pin()` function to allow replacing the listener certificate at runtime, when the listener is pinned. 180 | 181 | ## 0.4.2 - 2022-03-09 182 | 183 | ### Added 184 | 185 | - Added `TlsListener::replace_acceptor()` function to allow replacing the listener certificate at runtime. 186 | 187 | ## 0.4.1 - 2022-03-09 188 | 189 | ### Changed 190 | 191 | - The implementation of `AsyncTls` for `tokio_native_tls::TlsAcceptor` now requires the connection type to implement `Send`. This in turn allows `TlsListener` to be `Send` when using the `native-tls` feature. Technically, this is a breaking change. However, in practice it is unlikely to break existing code and makes using `TlsListener` much easier to use when `native-tls` is enabled. 192 | 193 | ## 0.4.0 - 2022-02-22 194 | 195 | NOTE: This release contains several breaking changes. 196 | 197 | ### Added 198 | 199 | - Support for [`native-tls`](https://github.com/sfackler/rust-native-tls). 200 | 201 | ### Changed 202 | 203 | - The TLS backend is now configurable. Both rustls and native-tls are supported. Other backends can also be used by implementing the `AsyncTls` trait. 204 | - You must now supply either the `rustls` or `native-tls` features to get support for a tls backend. 205 | - Unfortunately, the machinery for this required adding an additional type parameter to `TlsListener`. 206 | - The `TlsListener` stream now returns a `tls_listener::Error` instead of `std::io::Error` type. 207 | - Signatures of `TcpListener::new()` and `builder()` have changed to now take an argument of the TLS type rather than a `rustls::ServerConfig`, 208 | to update existing calls, replace `builder(config)` with `builder(Arc::new(config).into())`. 209 | 210 | ### Fixed 211 | 212 | - Crate will now compile when linked against a target that doesn't explicitly enable the `tokio/time` and `hyper/tcp` 213 | features. 214 | -------------------------------------------------------------------------------- /tests/long_text.txt: -------------------------------------------------------------------------------- 1 | 2 | “It wouldn’t do for you, Jerry. Jerry, you honest tradesman, it wouldn’t 3 | suit _your_ line of business! Recalled--! Bust me if I don’t think he’d 4 | been a drinking!” 5 | 6 | His message perplexed his mind to that degree that he was fain, several 7 | times, to take off his hat to scratch his head. Except on the crown, 8 | which was raggedly bald, he had stiff, black hair, standing jaggedly all 9 | over it, and growing down hill almost to his broad, blunt nose. It was 10 | so like Smith’s work, so much more like the top of a strongly spiked 11 | wall than a head of hair, that the best of players at leap-frog might 12 | have declined him, as the most dangerous man in the world to go over. 13 | 14 | While he trotted back with the message he was to deliver to the night 15 | watchman in his box at the door of Tellson’s Bank, by Temple Bar, who 16 | was to deliver it to greater authorities within, the shadows of the 17 | night took such shapes to him as arose out of the message, and took such 18 | shapes to the mare as arose out of _her_ private topics of uneasiness. 19 | They seemed to be numerous, for she shied at every shadow on the road. 20 | 21 | What time, the mail-coach lumbered, jolted, rattled, and bumped upon 22 | its tedious way, with its three fellow-inscrutables inside. To whom, 23 | likewise, the shadows of the night revealed themselves, in the forms 24 | their dozing eyes and wandering thoughts suggested. 25 | 26 | Tellson’s Bank had a run upon it in the mail. As the bank 27 | passenger--with an arm drawn through the leathern strap, which did what 28 | lay in it to keep him from pounding against the next passenger, 29 | and driving him into his corner, whenever the coach got a special 30 | jolt--nodded in his place, with half-shut eyes, the little 31 | coach-windows, and the coach-lamp dimly gleaming through them, and the 32 | bulky bundle of opposite passenger, became the bank, and did a great 33 | stroke of business. The rattle of the harness was the chink of money, 34 | and more drafts were honoured in five minutes than even Tellson’s, with 35 | all its foreign and home connection, ever paid in thrice the time. Then 36 | the strong-rooms underground, at Tellson’s, with such of their valuable 37 | stores and secrets as were known to the passenger (and it was not a 38 | little that he knew about them), opened before him, and he went in among 39 | them with the great keys and the feebly-burning candle, and found them 40 | safe, and strong, and sound, and still, just as he had last seen them. 41 | 42 | But, though the bank was almost always with him, and though the coach 43 | (in a confused way, like the presence of pain under an opiate) was 44 | always with him, there was another current of impression that never 45 | ceased to run, all through the night. He was on his way to dig some one 46 | out of a grave. 47 | 48 | Now, which of the multitude of faces that showed themselves before him 49 | was the true face of the buried person, the shadows of the night did 50 | not indicate; but they were all the faces of a man of five-and-forty by 51 | years, and they differed principally in the passions they expressed, 52 | and in the ghastliness of their worn and wasted state. Pride, contempt, 53 | defiance, stubbornness, submission, lamentation, succeeded one another; 54 | so did varieties of sunken cheek, cadaverous colour, emaciated hands 55 | and figures. But the face was in the main one face, and every head was 56 | prematurely white. A hundred times the dozing passenger inquired of this 57 | spectre: 58 | 59 | “Buried how long?” 60 | 61 | The answer was always the same: “Almost eighteen years.” 62 | 63 | “You had abandoned all hope of being dug out?” 64 | 65 | “Long ago.” 66 | 67 | “You know that you are recalled to life?” 68 | 69 | “They tell me so.” 70 | 71 | “I hope you care to live?” 72 | 73 | “I can’t say.” 74 | 75 | “Shall I show her to you? Will you come and see her?” 76 | 77 | The answers to this question were various and contradictory. Sometimes 78 | the broken reply was, “Wait! It would kill me if I saw her too soon.” 79 | Sometimes, it was given in a tender rain of tears, and then it was, 80 | “Take me to her.” Sometimes it was staring and bewildered, and then it 81 | was, “I don’t know her. I don’t understand.” 82 | 83 | After such imaginary discourse, the passenger in his fancy would dig, 84 | and dig, dig--now with a spade, now with a great key, now with his 85 | hands--to dig this wretched creature out. Got out at last, with earth 86 | hanging about his face and hair, he would suddenly fan away to dust. The 87 | passenger would then start to himself, and lower the window, to get the 88 | reality of mist and rain on his cheek. 89 | 90 | Yet even when his eyes were opened on the mist and rain, on the moving 91 | patch of light from the lamps, and the hedge at the roadside retreating 92 | by jerks, the night shadows outside the coach would fall into the train 93 | of the night shadows within. The real Banking-house by Temple Bar, the 94 | real business of the past day, the real strong rooms, the real express 95 | sent after him, and the real message returned, would all be there. Out 96 | of the midst of them, the ghostly face would rise, and he would accost 97 | it again. 98 | 99 | “Buried how long?” 100 | 101 | “Almost eighteen years.” 102 | 103 | “I hope you care to live?” 104 | 105 | “I can’t say.” 106 | 107 | Dig--dig--dig--until an impatient movement from one of the two 108 | passengers would admonish him to pull up the window, draw his arm 109 | securely through the leathern strap, and speculate upon the two 110 | slumbering forms, until his mind lost its hold of them, and they again 111 | slid away into the bank and the grave. 112 | 113 | “Buried how long?” 114 | 115 | “Almost eighteen years.” 116 | 117 | “You had abandoned all hope of being dug out?” 118 | 119 | “Long ago.” 120 | 121 | The words were still in his hearing as just spoken--distinctly in 122 | his hearing as ever spoken words had been in his life--when the weary 123 | passenger started to the consciousness of daylight, and found that the 124 | shadows of the night were gone. 125 | 126 | He lowered the window, and looked out at the rising sun. There was a 127 | ridge of ploughed land, with a plough upon it where it had been left 128 | last night when the horses were unyoked; beyond, a quiet coppice-wood, 129 | in which many leaves of burning red and golden yellow still remained 130 | upon the trees. Though the earth was cold and wet, the sky was clear, 131 | and the sun rose bright, placid, and beautiful. 132 | 133 | “Eighteen years!” said the passenger, looking at the sun. “Gracious 134 | Creator of day! To be buried alive for eighteen years!” 135 | 136 | 137 | 138 | 139 | CHAPTER IV. 140 | The Preparation 141 | 142 | 143 | When the mail got successfully to Dover, in the course of the forenoon, 144 | the head drawer at the Royal George Hotel opened the coach-door as his 145 | custom was. He did it with some flourish of ceremony, for a mail journey 146 | from London in winter was an achievement to congratulate an adventurous 147 | traveller upon. 148 | 149 | By that time, there was only one adventurous traveller left be 150 | congratulated: for the two others had been set down at their respective 151 | roadside destinations. The mildewy inside of the coach, with its damp 152 | and dirty straw, its disagreeable smell, and its obscurity, was rather 153 | like a larger dog-kennel. Mr. Lorry, the passenger, shaking himself out 154 | of it in chains of straw, a tangle of shaggy wrapper, flapping hat, and 155 | muddy legs, was rather like a larger sort of dog. 156 | 157 | “There will be a packet to Calais, tomorrow, drawer?” 158 | 159 | “Yes, sir, if the weather holds and the wind sets tolerable fair. The 160 | tide will serve pretty nicely at about two in the afternoon, sir. Bed, 161 | sir?” 162 | 163 | “I shall not go to bed till night; but I want a bedroom, and a barber.” 164 | 165 | “And then breakfast, sir? Yes, sir. That way, sir, if you please. 166 | Show Concord! Gentleman’s valise and hot water to Concord. Pull off 167 | gentleman’s boots in Concord. (You will find a fine sea-coal fire, sir.) 168 | Fetch barber to Concord. Stir about there, now, for Concord!” 169 | 170 | The Concord bed-chamber being always assigned to a passenger by the 171 | mail, and passengers by the mail being always heavily wrapped up from 172 | head to foot, the room had the odd interest for the establishment of the 173 | Royal George, that although but one kind of man was seen to go into it, 174 | all kinds and varieties of men came out of it. Consequently, another 175 | drawer, and two porters, and several maids and the landlady, were all 176 | loitering by accident at various points of the road between the Concord 177 | and the coffee-room, when a gentleman of sixty, formally dressed in a 178 | brown suit of clothes, pretty well worn, but very well kept, with large 179 | square cuffs and large flaps to the pockets, passed along on his way to 180 | his breakfast. 181 | 182 | The coffee-room had no other occupant, that forenoon, than the gentleman 183 | in brown. His breakfast-table was drawn before the fire, and as he sat, 184 | with its light shining on him, waiting for the meal, he sat so still, 185 | that he might have been sitting for his portrait. 186 | 187 | Very orderly and methodical he looked, with a hand on each knee, and a 188 | loud watch ticking a sonorous sermon under his flapped waist-coat, 189 | as though it pitted its gravity and longevity against the levity and 190 | evanescence of the brisk fire. He had a good leg, and was a little vain 191 | of it, for his brown stockings fitted sleek and close, and were of a 192 | fine texture; his shoes and buckles, too, though plain, were trim. He 193 | wore an odd little sleek crisp flaxen wig, setting very close to his 194 | head: which wig, it is to be presumed, was made of hair, but which 195 | looked far more as though it were spun from filaments of silk or glass. 196 | His linen, though not of a fineness in accordance with his stockings, 197 | was as white as the tops of the waves that broke upon the neighbouring 198 | beach, or the specks of sail that glinted in the sunlight far at sea. A 199 | face habitually suppressed and quieted, was still lighted up under the 200 | quaint wig by a pair of moist bright eyes that it must have cost 201 | their owner, in years gone by, some pains to drill to the composed and 202 | reserved expression of Tellson’s Bank. He had a healthy colour in his 203 | cheeks, and his face, though lined, bore few traces of anxiety. 204 | But, perhaps the confidential bachelor clerks in Tellson’s Bank were 205 | principally occupied with the cares of other people; and perhaps 206 | second-hand cares, like second-hand clothes, come easily off and on. 207 | 208 | Completing his resemblance to a man who was sitting for his portrait, 209 | Mr. Lorry dropped off to sleep. The arrival of his breakfast roused him, 210 | and he said to the drawer, as he moved his chair to it: 211 | 212 | “I wish accommodation prepared for a young lady who may com -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | #![deny(missing_docs)] 2 | #![cfg_attr(docsrs, feature(doc_cfg))] 3 | 4 | //! Async TLS listener 5 | //! 6 | //! This library is intended to automatically initiate a TLS connection 7 | //! for each new connection in a source of new streams (such as a listening 8 | //! TCP or unix domain socket). 9 | //! 10 | //! # Features: 11 | //! - `tokio-net`: Implementations for tokio socket types (default) 12 | //! - `rt`: Features that depend on the tokio runtime, such as [`SpawningHandshakes`] 13 | //! - `rustls-core`: Support the tokio-rustls backend for tls 14 | //! - `rustls-aws-lc`: Include the aws-lc provider for rustls 15 | //! - `rustls-ring`: Include the ring provider for rustls 16 | //! - `rustls-fips`: Include enabling the "fips" feature for rustls 17 | //! - `native-tls`: support the tokio-native-tls backend for tls 18 | 19 | use futures_util::stream::{FuturesUnordered, Stream, StreamExt, TryStreamExt}; 20 | use pin_project_lite::pin_project; 21 | #[cfg(feature = "rt")] 22 | pub use spawning_handshake::SpawningHandshakes; 23 | use std::fmt::Debug; 24 | use std::future::{Future, poll_fn}; 25 | use std::num::NonZeroUsize; 26 | use std::pin::Pin; 27 | use std::task::{Context, Poll, ready}; 28 | use std::time::Duration; 29 | use thiserror::Error; 30 | use tokio::io::{AsyncRead, AsyncWrite}; 31 | use tokio::time::{Timeout, timeout}; 32 | #[cfg(feature = "native-tls")] 33 | pub use tokio_native_tls as native_tls; 34 | #[cfg(feature = "openssl")] 35 | pub use tokio_openssl as openssl; 36 | #[cfg(feature = "rustls-core")] 37 | pub use tokio_rustls as rustls; 38 | 39 | mod accept; 40 | #[cfg(feature = "tokio-net")] 41 | mod net; 42 | #[cfg(feature = "rt")] 43 | mod spawning_handshake; 44 | 45 | pub use accept::*; 46 | 47 | #[cfg(feature = "axum")] 48 | mod axum; 49 | 50 | /// Default number of connections to accept in a batch before trying to 51 | pub const DEFAULT_ACCEPT_BATCH_SIZE: NonZeroUsize = NonZeroUsize::new(64).unwrap(); 52 | /// Default timeout for the TLS handshake. 53 | pub const DEFAULT_HANDSHAKE_TIMEOUT: Duration = Duration::from_secs(10); 54 | 55 | /// Trait for TLS implementation. 56 | /// 57 | /// Implementations are provided by the rustls and native-tls features. 58 | pub trait AsyncTls: Clone { 59 | /// The type of the TLS stream created from the underlying stream. 60 | type Stream; 61 | /// Error type for completing the TLS handshake 62 | type Error: std::error::Error; 63 | /// Type of the Future for the TLS stream that is accepted. 64 | type AcceptFuture: Future>; 65 | 66 | /// Accept a TLS connection on an underlying stream 67 | fn accept(&self, stream: C) -> Self::AcceptFuture; 68 | } 69 | 70 | pin_project! { 71 | /// 72 | /// Wraps a `Stream` of connections (such as a TCP listener) so that each connection is itself 73 | /// encrypted using TLS. 74 | /// 75 | /// It is similar to: 76 | /// 77 | /// ```ignore 78 | /// tcpListener.and_then(|s| tlsAcceptor.accept(s)) 79 | /// ``` 80 | /// 81 | /// except that it has the ability to accept multiple transport-level connections 82 | /// simultaneously while the TLS handshake is pending for other connections. 83 | /// 84 | /// By default, if a client fails the TLS handshake, that is treated as an error, and the 85 | /// `TlsListener` will return an `Err`. If the error is not handled, then an invalid handshake can 86 | /// cause the server to stop accepting connections. 87 | /// See [`http-stream.rs`][2] or [`http-low-level`][3] examples, for examples of how to avoid this. 88 | /// 89 | /// Note that if the maximum number of pending connections is greater than 1, the resulting 90 | /// [`T::Stream`][4] connections may come in a different order than the connections produced by the 91 | /// underlying listener. 92 | /// 93 | /// [2]: https://github.com/tmccombs/tls-listener/blob/main/examples/http-stream.rs 94 | /// [3]: https://github.com/tmccombs/tls-listener/blob/main/examples/http-low-level.rs 95 | /// [4]: AsyncTls::Stream 96 | /// 97 | pub struct TlsListener> { 98 | #[pin] 99 | listener: A, 100 | tls: T, 101 | waiting: FuturesUnordered>, 102 | accept_batch_size: NonZeroUsize, 103 | timeout: Duration, 104 | } 105 | } 106 | 107 | /// Builder for `TlsListener`. 108 | #[derive(Clone)] 109 | pub struct Builder { 110 | tls: T, 111 | accept_batch_size: NonZeroUsize, 112 | handshake_timeout: Duration, 113 | } 114 | 115 | /// Wraps errors from either the listener or the TLS Acceptor 116 | #[derive(Debug, Error)] 117 | #[non_exhaustive] 118 | pub enum Error { 119 | /// An error that arose from the listener ([AsyncAccept::Error]) 120 | #[error("{0}")] 121 | ListenerError(#[source] LE), 122 | /// An error that occurred during the TLS accept handshake 123 | #[error("{error}")] 124 | #[non_exhaustive] 125 | TlsAcceptError { 126 | /// The original error that occurred 127 | #[source] 128 | error: TE, 129 | 130 | /// Address of the other side of the connection 131 | peer_addr: Addr, 132 | }, 133 | /// The TLS handshake timed out 134 | #[error("Timeout during TLS handshake")] 135 | #[non_exhaustive] 136 | HandshakeTimeout { 137 | /// Address of the other side of the connection 138 | peer_addr: Addr, 139 | }, 140 | } 141 | 142 | impl TlsListener 143 | where 144 | T: AsyncTls, 145 | { 146 | /// Create a `TlsListener` with default options. 147 | pub fn new(tls: T, listener: A) -> Self { 148 | builder(tls).listen(listener) 149 | } 150 | } 151 | 152 | /// Convenience type alias to get the proper error type from the type of the [`AsyncAccept`] and 153 | /// [`AsyncTls`] used. 154 | type TlsListenerError = Error< 155 | ::Error, 156 | ::Connection>>::Error, 157 | ::Address, 158 | >; 159 | 160 | impl TlsListener 161 | where 162 | A: AsyncAccept, 163 | T: AsyncTls, 164 | { 165 | /// Poll accepting a connection. 166 | /// 167 | /// This will return ready once the TLS handshake has completed on an incoming 168 | /// connection and return the connection and the source address. 169 | pub fn poll_accept(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<::Item> { 170 | let mut this = self.project(); 171 | 172 | loop { 173 | let mut empty_listener = false; 174 | for _ in 0..this.accept_batch_size.get() { 175 | match this.listener.as_mut().poll_accept(cx) { 176 | Poll::Pending => { 177 | empty_listener = true; 178 | break; 179 | } 180 | Poll::Ready(Ok((conn, addr))) => { 181 | this.waiting.push(Waiting { 182 | inner: timeout(*this.timeout, this.tls.accept(conn)), 183 | peer_addr: Some(addr), 184 | }); 185 | } 186 | Poll::Ready(Err(e)) => { 187 | return Poll::Ready(Err(Error::ListenerError(e))); 188 | } 189 | } 190 | } 191 | 192 | match this.waiting.poll_next_unpin(cx) { 193 | Poll::Ready(Some(result)) => return Poll::Ready(result), 194 | // If we don't have anything waiting yet, 195 | // then we are still pending, 196 | Poll::Ready(None) | Poll::Pending => { 197 | if empty_listener { 198 | return Poll::Pending; 199 | } 200 | } 201 | } 202 | } 203 | } 204 | 205 | /// Accept the next connection 206 | /// 207 | /// This is similar to `self.next()`, but doesn't return an `Option` because 208 | /// there isn't an end condition on accepting connections, 209 | /// and has a more domain-appropriate name. 210 | /// 211 | /// The future returned is "cancellation safe". 212 | pub fn accept(&mut self) -> impl Future::Item> + '_ 213 | where 214 | Self: Unpin, 215 | { 216 | let mut pinned = Pin::new(self); 217 | poll_fn(move |cx| pinned.as_mut().poll_accept(cx)) 218 | } 219 | 220 | /// Replaces the Tls Acceptor configuration, which will be used for new connections. 221 | /// 222 | /// This can be used to change the certificate used at runtime. 223 | pub fn replace_acceptor(&mut self, acceptor: T) { 224 | self.tls = acceptor; 225 | } 226 | 227 | /// Replaces the Tls Acceptor configuration from a pinned reference to `Self`. 228 | /// 229 | /// This is useful if your listener is `!Unpin`. 230 | /// 231 | /// This can be used to change the certificate used at runtime. 232 | pub fn replace_acceptor_pin(self: Pin<&mut Self>, acceptor: T) { 233 | *self.project().tls = acceptor; 234 | } 235 | 236 | /// Convert into a Stream of connections. 237 | /// 238 | /// This drops the address of the connection, but provides a more convenient API 239 | /// if the address isn't needed. 240 | /// 241 | /// The address will still be included in errors. 242 | pub fn connections(self) -> impl Stream>> { 243 | self.map_ok(|(conn, _addr)| conn) 244 | } 245 | 246 | /// Get a reference to the underlying connection listener 247 | /// 248 | /// Can be useful to get metadata about the listener, such as the 249 | /// local address. 250 | pub fn listener(&self) -> &A { 251 | &self.listener 252 | } 253 | 254 | /// Get the local address of the underlying listener 255 | pub fn local_addr(&self) -> Result 256 | where 257 | A: AsyncListener, 258 | { 259 | self.listener.local_addr() 260 | } 261 | } 262 | 263 | impl Stream for TlsListener 264 | where 265 | A: AsyncAccept, 266 | T: AsyncTls, 267 | { 268 | type Item = Result<(T::Stream, A::Address), TlsListenerError>; 269 | 270 | fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { 271 | self.poll_accept(cx).map(Some) 272 | } 273 | } 274 | 275 | #[cfg(feature = "rustls-core")] 276 | #[cfg_attr(docsrs, doc(cfg(feature = "rustls-core")))] 277 | impl AsyncTls for tokio_rustls::TlsAcceptor { 278 | type Stream = tokio_rustls::server::TlsStream; 279 | type Error = std::io::Error; 280 | type AcceptFuture = tokio_rustls::Accept; 281 | 282 | fn accept(&self, conn: C) -> Self::AcceptFuture { 283 | tokio_rustls::TlsAcceptor::accept(self, conn) 284 | } 285 | } 286 | 287 | #[cfg(feature = "native-tls")] 288 | #[cfg_attr(docsrs, doc(cfg(feature = "native-tls")))] 289 | impl AsyncTls for tokio_native_tls::TlsAcceptor 290 | where 291 | C: AsyncRead + AsyncWrite + Unpin + Send + 'static, 292 | { 293 | type Stream = tokio_native_tls::TlsStream; 294 | type Error = tokio_native_tls::native_tls::Error; 295 | type AcceptFuture = Pin> + Send>>; 296 | 297 | fn accept(&self, conn: C) -> Self::AcceptFuture { 298 | let tls = self.clone(); 299 | Box::pin(async move { tokio_native_tls::TlsAcceptor::accept(&tls, conn).await }) 300 | } 301 | } 302 | 303 | #[cfg(feature = "openssl")] 304 | #[cfg_attr(docsrs, doc(cfg(feature = "openssl")))] 305 | impl AsyncTls for openssl_impl::ssl::SslContext 306 | where 307 | C: AsyncRead + AsyncWrite + Unpin + Send + 'static, 308 | { 309 | type Stream = tokio_openssl::SslStream; 310 | type Error = openssl_impl::ssl::Error; 311 | type AcceptFuture = Pin> + Send>>; 312 | 313 | fn accept(&self, conn: C) -> Self::AcceptFuture { 314 | let ssl = match openssl_impl::ssl::Ssl::new(self) { 315 | Ok(s) => s, 316 | Err(e) => { 317 | return Box::pin(futures_util::future::err(e.into())); 318 | } 319 | }; 320 | let mut stream = match tokio_openssl::SslStream::new(ssl, conn) { 321 | Ok(s) => s, 322 | Err(e) => { 323 | return Box::pin(futures_util::future::err(e.into())); 324 | } 325 | }; 326 | Box::pin(async move { 327 | Pin::new(&mut stream).accept().await?; 328 | Ok(stream) 329 | }) 330 | } 331 | } 332 | 333 | impl Builder { 334 | /// Set the size of batches of incoming connections to accept at once 335 | /// 336 | /// When polling for a new connection, the `TlsListener` will first check 337 | /// for incomming connections on the listener that need to start a TLS handshake. 338 | /// This specifies the maximum number of connections it will accept before seeing if any 339 | /// TLS connections are ready. 340 | /// 341 | /// Having a limit for this ensures that ready TLS conections aren't starved if there are a 342 | /// large number of incoming connections. 343 | /// 344 | /// Defaults to `DEFAULT_ACCEPT_BATCH_SIZE`. 345 | pub fn accept_batch_size(&mut self, size: NonZeroUsize) -> &mut Self { 346 | self.accept_batch_size = size; 347 | self 348 | } 349 | 350 | /// Set the timeout for handshakes. 351 | /// 352 | /// If a timeout takes longer than `timeout`, then the handshake will be 353 | /// aborted and the underlying connection will be dropped. 354 | /// 355 | /// The default is fairly conservative, to avoid dropping connections. It is 356 | /// recommended that you adjust this to meet the specific needs of your use case 357 | /// in production deployments. 358 | /// 359 | /// Defaults to `DEFAULT_HANDSHAKE_TIMEOUT`. 360 | pub fn handshake_timeout(&mut self, timeout: Duration) -> &mut Self { 361 | self.handshake_timeout = timeout; 362 | self 363 | } 364 | 365 | /// Create a `TlsListener` from the builder 366 | /// 367 | /// Actually build the `TlsListener`. The `listener` argument should be 368 | /// an implementation of the `AsyncAccept` trait that accepts new connections 369 | /// that the `TlsListener` will encrypt using TLS. 370 | pub fn listen(&self, listener: A) -> TlsListener 371 | where 372 | T: AsyncTls, 373 | { 374 | TlsListener { 375 | listener, 376 | tls: self.tls.clone(), 377 | waiting: FuturesUnordered::new(), 378 | accept_batch_size: self.accept_batch_size, 379 | timeout: self.handshake_timeout, 380 | } 381 | } 382 | } 383 | 384 | impl Error { 385 | /// Get the peer address from the connection that caused the error, if applicable. 386 | /// 387 | /// This will only return Some for errors that occur after an initial connection 388 | /// is established, such as TlsAcceptError and HandshakeTimeout. And only if 389 | /// the [`AsyncAccept`] implementation implements [`peer_addr`](AsyncAccept::peer_addr) 390 | pub fn peer_addr(&self) -> Option<&A> { 391 | match self { 392 | Error::TlsAcceptError { peer_addr, .. } | Self::HandshakeTimeout { peer_addr, .. } => { 393 | Some(peer_addr) 394 | } 395 | _ => None, 396 | } 397 | } 398 | } 399 | 400 | /// Create a new Builder for a TlsListener 401 | /// 402 | /// `server_config` will be used to configure the TLS sessions. 403 | pub fn builder(tls: T) -> Builder { 404 | Builder { 405 | tls, 406 | accept_batch_size: DEFAULT_ACCEPT_BATCH_SIZE, 407 | handshake_timeout: DEFAULT_HANDSHAKE_TIMEOUT, 408 | } 409 | } 410 | 411 | pin_project! { 412 | struct Waiting 413 | where 414 | A: AsyncAccept, 415 | T: AsyncTls 416 | { 417 | #[pin] 418 | inner: Timeout, 419 | peer_addr: Option, 420 | } 421 | } 422 | 423 | impl Future for Waiting 424 | where 425 | A: AsyncAccept, 426 | T: AsyncTls, 427 | { 428 | type Output = Result<(T::Stream, A::Address), TlsListenerError>; 429 | 430 | fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { 431 | let mut this = self.project(); 432 | let res = ready!(this.inner.as_mut().poll(cx)); 433 | let addr = this 434 | .peer_addr 435 | .take() 436 | .expect("this future has already been polled to completion"); 437 | match res { 438 | // We succesfully got a connection 439 | Ok(Ok(conn)) => Poll::Ready(Ok((conn, addr))), 440 | // The handshake failed 441 | Ok(Err(e)) => Poll::Ready(Err(Error::TlsAcceptError { 442 | error: e, 443 | peer_addr: addr, 444 | })), 445 | // The handshake timed out 446 | Err(_) => Poll::Ready(Err(Error::HandshakeTimeout { peer_addr: addr })), 447 | } 448 | } 449 | } 450 | --------------------------------------------------------------------------------