Unverified Commit 5b238ecc authored by Copilot's avatar Copilot Committed by GitHub
Browse files

Add HTTPS example with TLS support (#409)



* Initial plan

* feat: add HTTPS example with TLS support

Co-authored-by: default avatarNugine <30099658+Nugine@users.noreply.github.com>

* docs: fix HTTPS example usage command

Co-authored-by: default avatarNugine <30099658+Nugine@users.noreply.github.com>

* refactor: load TLS certificates from files instead of generating at runtime

Co-authored-by: default avatarNugine <30099658+Nugine@users.noreply.github.com>

* refactor: apply review feedback - use &Path and optimize PEM reading

Co-authored-by: default avatarNugine <30099658+Nugine@users.noreply.github.com>

* Apply suggestions from code review

Co-authored-by: default avatarCopilot <175728472+Copilot@users.noreply.github.com>

* fix

---------

Co-authored-by: default avatarcopilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: default avatarNugine <30099658+Nugine@users.noreply.github.com>
Co-authored-by: default avatarNugine <nugine@foxmail.com>
Co-authored-by: default avatarCopilot <175728472+Copilot@users.noreply.github.com>
parent 0248faa2
Loading
Loading
Loading
Loading
+21 −6
Original line number Diff line number Diff line
@@ -396,7 +396,7 @@ dependencies = [
 "rustls-native-certs 0.8.1",
 "rustls-pki-types",
 "tokio",
 "tokio-rustls 0.26.2",
 "tokio-rustls 0.26.4",
 "tower",
 "tracing",
]
@@ -1642,7 +1642,7 @@ dependencies = [
 "rustls-native-certs 0.8.1",
 "rustls-pki-types",
 "tokio",
 "tokio-rustls 0.26.2",
 "tokio-rustls 0.26.4",
 "tower-service",
 "webpki-roots",
]
@@ -2641,7 +2641,7 @@ dependencies = [
 "serde_urlencoded",
 "sync_wrapper",
 "tokio",
 "tokio-rustls 0.26.2",
 "tokio-rustls 0.26.4",
 "tokio-util",
 "tower",
 "tower-http",
@@ -2748,6 +2748,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c0ebcbd2f03de0fc1122ad9bb24b127a5a6cd51d72604a3f3c50ac459762b6cc"
dependencies = [
 "aws-lc-rs",
 "log",
 "once_cell",
 "ring",
 "rustls-pki-types",
@@ -2763,7 +2764,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a9aace74cb666635c918e9c12bc0d348266037aa8eb599b5cba565709a8dff00"
dependencies = [
 "openssl-probe",
 "rustls-pemfile",
 "rustls-pemfile 1.0.4",
 "schannel",
 "security-framework 2.11.1",
]
@@ -2789,6 +2790,15 @@ dependencies = [
 "base64 0.21.7",
]

[[package]]
name = "rustls-pemfile"
version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50"
dependencies = [
 "rustls-pki-types",
]

[[package]]
name = "rustls-pki-types"
version = "1.12.0"
@@ -2846,6 +2856,7 @@ dependencies = [
 "bytestring",
 "cfg-if",
 "chrono",
 "clap",
 "const-str",
 "crc-fast",
 "futures",
@@ -2856,6 +2867,7 @@ dependencies = [
 "http-body-util",
 "httparse",
 "hyper 1.8.1",
 "hyper-util",
 "itoa",
 "md-5 0.11.0-rc.3",
 "memchr",
@@ -2865,6 +2877,7 @@ dependencies = [
 "openssl",
 "pin-project-lite",
 "quick-xml 0.37.5",
 "rustls-pemfile 2.2.0",
 "serde",
 "serde_json",
 "serde_urlencoded",
@@ -2877,9 +2890,11 @@ dependencies = [
 "thiserror",
 "time",
 "tokio",
 "tokio-rustls 0.26.4",
 "tokio-util",
 "tower",
 "tracing",
 "tracing-subscriber",
 "transform-stream",
 "urlencoding",
 "zeroize",
@@ -3523,9 +3538,9 @@ dependencies = [

[[package]]
name = "tokio-rustls"
version = "0.26.2"
version = "0.26.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e727b36a1a0e8b74c376ac2211e40c2c8af09fb4013c60d910495810f008e9b"
checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61"
dependencies = [
 "rustls 0.23.31",
 "tokio",
+5 −0
Original line number Diff line number Diff line
@@ -70,6 +70,11 @@ cfg-if = "1.0.4"

[dev-dependencies]
axum = "0.8.7"
clap = { version = "4.5.53", features = ["derive"] }
hyper-util = { version = "0.1.18", features = ["server-auto", "server-graceful", "http1", "http2", "tokio"] }
rustls-pemfile = "2.2.0"
serde_json = "1.0.145"
tokio = { version = "1.48.0", features = ["full"] }
tokio-rustls = "0.26.4"
tokio-util = { version = "0.7.17", features = ["io"] }
tracing-subscriber = { version = "0.3.22", features = ["env-filter"] }
+221 −0
Original line number Diff line number Diff line
//! HTTPS server example for s3s
//!
//! This example demonstrates how to run an S3 service over HTTPS using TLS.
//! It uses tokio-rustls for TLS support and loads certificates from PEM files.
//!
//! # Generating Test Certificates
//!
//! You can generate self-signed certificates for testing using OpenSSL:
//!
//! ```bash
//! openssl req -x509 -newkey rsa:2048 -nodes -keyout key.pem -out cert.pem \
//!     -days 365 -subj "/CN=localhost"
//! ```
//!
//! # Usage
//!
//! ```bash
//! # Generate test certificates first (see above)
//! cargo run --example https -- --cert cert.pem --key key.pem
//! ```
//!
//! Then you can access the server at <https://localhost:8014>. You'll need to accept
//! the self-signed certificate warning in your browser or S3 client.
//!
//! For production use, use certificates from a trusted certificate authority.

use s3s::dto::{GetObjectInput, GetObjectOutput};
use s3s::service::S3ServiceBuilder;
use s3s::{S3, S3Request, S3Response, S3Result};

use std::fs::File;
use std::io::{self, BufReader};
use std::path::{Path, PathBuf};
use std::sync::Arc;

use tokio::net::TcpListener;
use tokio_rustls::TlsAcceptor;
use tokio_rustls::rustls;
use tokio_rustls::rustls::ServerConfig;
use tokio_rustls::rustls::pki_types::{CertificateDer, PrivateKeyDer};

use hyper_util::rt::{TokioExecutor, TokioIo};
use hyper_util::server::conn::auto::Builder as ConnBuilder;

use clap::Parser;

/// A minimal S3 implementation for demonstration purposes
#[derive(Debug, Clone)]
struct DummyS3;

#[async_trait::async_trait]
impl S3 for DummyS3 {
    async fn get_object(&self, _req: S3Request<GetObjectInput>) -> S3Result<S3Response<GetObjectOutput>> {
        Err(s3s::s3_error!(NotImplemented, "GetObject is not implemented"))
    }
}

/// Load certificates from a PEM file
fn load_certs(path: &Path) -> io::Result<Vec<CertificateDer<'static>>> {
    let file = File::open(path)?;
    let mut reader = BufReader::new(file);
    let certs = rustls_pemfile::certs(&mut reader).collect::<Result<Vec<_>, _>>()?;
    if certs.is_empty() {
        return Err(io::Error::new(io::ErrorKind::InvalidInput, "No valid certificates found in file"));
    }
    Ok(certs)
}

/// Load private key from a PEM file
fn load_private_key(path: &Path) -> io::Result<PrivateKeyDer<'static>> {
    use rustls_pemfile::Item;

    let file = File::open(path)?;
    let mut reader = BufReader::new(file);

    loop {
        match rustls_pemfile::read_one(&mut reader)? {
            Some(Item::Pkcs8Key(key)) => {
                return Ok(PrivateKeyDer::Pkcs8(key));
            }
            Some(Item::Pkcs1Key(key)) => {
                return Ok(PrivateKeyDer::Pkcs1(key));
            }
            Some(Item::Sec1Key(key)) => {
                return Ok(PrivateKeyDer::Sec1(key));
            }
            Some(_) => {}
            None => break,
        }
    }

    Err(io::Error::new(io::ErrorKind::InvalidInput, "No valid private key found in file"))
}

/// Create TLS server configuration from certificate and key files
fn create_tls_config(cert_path: &Path, key_path: &Path) -> io::Result<ServerConfig> {
    let certs = load_certs(cert_path)?;
    let key = load_private_key(key_path)?;

    let mut config = ServerConfig::builder()
        .with_no_client_auth()
        .with_single_cert(certs, key)
        .map_err(io::Error::other)?;

    // Use default protocol versions (TLS 1.2 and 1.3)
    config.alpn_protocols = vec![b"h2".to_vec(), b"http/1.1".to_vec()];

    Ok(config)
}

#[derive(clap::Parser)]
#[command(name = "https-example")]
#[command(about = "HTTPS server example for s3s", long_about = None)]
struct Args {
    /// Path to the TLS certificate file (PEM format)
    #[arg(long)]
    cert: PathBuf,

    /// Path to the TLS private key file (PEM format)
    #[arg(long)]
    key: PathBuf,

    /// Host to listen on
    #[arg(long, default_value = "127.0.0.1")]
    host: String,

    /// Port to listen on
    #[arg(long, default_value = "8014")]
    port: u16,
}

#[tokio::main]
async fn main() -> io::Result<()> {
    // Install the default crypto provider (required for rustls)
    let _ = rustls::crypto::aws_lc_rs::default_provider().install_default();

    // Parse command line arguments
    let args = Args::parse();

    // Setup tracing
    tracing_subscriber::fmt()
        .with_env_filter(tracing_subscriber::EnvFilter::from_default_env().add_directive(tracing::Level::INFO.into()))
        .init();

    // Create a simple S3 service
    let s3_service = {
        let builder = S3ServiceBuilder::new(DummyS3);
        builder.build()
    };

    // Create TLS configuration from certificate files
    let tls_config = create_tls_config(&args.cert, &args.key)?;
    let tls_acceptor = TlsAcceptor::from(Arc::new(tls_config));

    // Bind to address
    let addr = format!("{}:{}", args.host, args.port);
    let listener = TcpListener::bind(&addr).await?;

    tracing::info!("HTTPS server listening on https://{}", addr);
    tracing::info!("Using certificate from: {:?}", args.cert);
    tracing::info!("Using private key from: {:?}", args.key);
    tracing::info!("Press Ctrl+C to stop");

    let http_server = ConnBuilder::new(TokioExecutor::new());
    let graceful = hyper_util::server::graceful::GracefulShutdown::new();

    let mut ctrl_c = std::pin::pin!(tokio::signal::ctrl_c());

    loop {
        let (stream, remote_addr) = tokio::select! {
            res = listener.accept() => {
                match res {
                    Ok(conn) => conn,
                    Err(err) => {
                        tracing::error!("error accepting connection: {err}");
                        continue;
                    }
                }
            }
            _ = ctrl_c.as_mut() => {
                tracing::info!("Received Ctrl+C, shutting down...");
                break;
            }
        };

        tracing::debug!("Accepted connection from {}", remote_addr);

        // Perform TLS handshake
        let tls_stream = match tls_acceptor.accept(stream).await {
            Ok(s) => s,
            Err(e) => {
                tracing::error!("TLS handshake failed from {}: {}", remote_addr, e);
                continue;
            }
        };

        tracing::debug!("TLS handshake completed for {}", remote_addr);

        // Serve the connection
        let conn = http_server.serve_connection(TokioIo::new(tls_stream), s3_service.clone());
        let conn = graceful.watch(conn.into_owned());

        tokio::spawn(async move {
            if let Err(e) = conn.await {
                tracing::error!("Error serving connection: {}", e);
            }
        });
    }

    // Graceful shutdown
    tokio::select! {
        () = graceful.shutdown() => {
            tracing::info!("Gracefully shut down!");
        },
        () = tokio::time::sleep(std::time::Duration::from_secs(10)) => {
            tracing::info!("Waited 10 seconds for graceful shutdown, aborting...");
        }
    }

    Ok(())
}