Last active
May 10, 2022 01:49
-
-
Save mikedilger/589f616a2ca607ad1daed278042c1bb8 to your computer and use it in GitHub Desktop.
Using hyper and rustls
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
[package] | |
name = "example" | |
version = "0.1.0" | |
edition = "2021" | |
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html | |
[dependencies] | |
anyhow = { version = "1.0", features = [ "backtrace" ] } | |
futures = "0.3" | |
hyper = { version = "0.14", features = [ "http1", "http2", "server", "stream", "runtime" ] } | |
rustls = { version = "0.20", features = [ "tls12", "quic" ] } | |
rustls-pemfile = "0.2" | |
tokio = { version = "1", features = [ "rt-multi-thread", "macros" ] } | |
tokio-rustls = "0.23" |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#[macro_use] extern crate anyhow; | |
pub use anyhow::{Error, Result}; | |
use std::net::SocketAddr; | |
use rustls::{ServerConnection, SupportedCipherSuite, ProtocolVersion, ServerConfig, | |
Certificate, PrivateKey}; | |
use hyper::{Request, Response, Body, StatusCode}; | |
use hyper::service::Service; | |
use std::fs; | |
use std::convert::Infallible; | |
use std::future::Future; | |
use std::pin::Pin; | |
use std::sync::Arc; | |
use std::task::{Context, Poll}; | |
#[derive(Debug, Clone)] | |
pub struct Config { | |
pub cert_path: String, | |
pub key_path: String, | |
pub socket_addr: SocketAddr, | |
} | |
#[derive(Debug, Clone)] | |
pub struct State { | |
pub config: Config, // read-only | |
// other shared Send/Sync objects go here | |
} | |
fn main() -> Result<()> | |
{ | |
let config = Config { | |
cert_path: "./cert.pem".to_owned(), | |
key_path: "./key.pem".to_owned(), | |
socket_addr: "127.0.0.1:3000".parse::<SocketAddr>()?, | |
}; | |
// Start global state object (simplified) | |
let state = State { | |
config: config.clone() | |
}; | |
// Read our certificates | |
let cert_pem = fs::read(&*config.cert_path)?; | |
let key_pem = fs::read(&*config.key_path)?; | |
// drop privilege here, if you are reading letsencrypt certificates as root. | |
// note that you'll want to set CAP_NET_BIND_SERVICE if you need to bind the | |
// webserver to low ports. | |
let tls_config = { | |
let certs: Vec<Certificate> = rustls_pemfile::certs(&mut &*cert_pem) | |
.map(|mut certs| certs.drain(..).map(Certificate).collect())?; | |
if certs.len() < 1 { | |
return Err(anyhow!("No certificates found.")); | |
} | |
let mut keys: Vec<PrivateKey> = rustls_pemfile::pkcs8_private_keys(&mut &*key_pem) | |
.map(|mut keys| keys.drain(..).map(PrivateKey).collect())?; | |
if keys.len() < 1 { | |
return Err(anyhow!("No private keys found.")); | |
} | |
let mut tls_config = ServerConfig::builder() | |
.with_safe_defaults() | |
.with_no_client_auth() | |
.with_single_cert(certs, keys.remove(0))?; | |
tls_config.alpn_protocols = vec![b"h2".to_vec(), b"http/1.1".to_vec()]; | |
tls_config | |
}; | |
// Go asynchronous | |
tokio_main(state, tls_config) | |
} | |
#[tokio::main] | |
async fn tokio_main(state: State, tls_config: ServerConfig) -> Result<()> | |
{ | |
let server = main_server(state.clone(), tls_config); | |
server.await?; | |
Ok(()) | |
} | |
async fn main_server(state: State, tls_config: ServerConfig) | |
-> Result<()> | |
{ | |
let listener = tokio::net::TcpListener::bind(&state.config.socket_addr).await?; | |
let local_addr = listener.local_addr()?; | |
let http = hyper::server::conn::Http::new(); | |
let acceptor = tokio_rustls::TlsAcceptor::from(Arc::new(tls_config)); | |
loop { | |
let (conn, remote_addr) = listener.accept().await?; | |
let acceptor = acceptor.clone(); | |
let http = http.clone(); | |
let cloned_state = state.clone(); | |
let fut = async move { | |
match acceptor.accept(conn).await { | |
Ok(stream) => { | |
let (_io, tls_connection) = stream.get_ref(); | |
let handler = PerConnHandler { | |
state: cloned_state, | |
local_addr: local_addr, | |
remote_addr: remote_addr, | |
tls_info: Some(TlsInfo::from_tls_connection(tls_connection)), | |
}; | |
if let Err(e) = http.serve_connection(stream, handler).await { | |
eprintln!("HYPER: {}", e); | |
} | |
}, | |
Err(e) => eprintln!("TLS: {}", e) | |
} | |
}; | |
tokio::spawn(fut); | |
} | |
} | |
#[derive(Clone)] | |
pub struct PerConnHandler { | |
pub state: State, | |
pub local_addr: SocketAddr, | |
pub remote_addr: SocketAddr, | |
pub tls_info: Option<TlsInfo>, | |
} | |
impl Service<Request<Body>> for PerConnHandler | |
{ | |
type Response = Response<Body>; | |
type Error = Infallible; | |
type Future = Pin<Box<dyn Future<Output = std::result::Result<Self::Response, Self::Error>> + Send>>; | |
fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll<std::result::Result<(), Self::Error>> | |
{ | |
Poll::Ready(Ok(())) | |
} | |
fn call(&mut self, req: Request<Body>) -> Self::Future | |
{ | |
let data = PerRequestData { | |
state: self.state.clone(), | |
local_addr: self.local_addr, | |
remote_addr: self.remote_addr, | |
tls_info: self.tls_info.clone(), | |
request: req, | |
response: Response::new("".into()), | |
}; | |
Box::pin(handle(data)) | |
} | |
} | |
pub struct PerRequestData { | |
pub state: State, | |
pub local_addr: SocketAddr, | |
pub remote_addr: SocketAddr, | |
pub tls_info: Option<TlsInfo>, | |
pub request: Request<Body>, | |
pub response: Response<Body>, | |
} | |
pub async fn handle(mut data: PerRequestData) | |
-> std::result::Result<Response<Body>, Infallible> | |
{ | |
// Handle requests here | |
// elided: drop requests if 'accept' is not acceptable | |
// elided: handle OPTIONS requests | |
// elided: read session cookie and establish session | |
// elided: route to page | |
*data.response.status_mut() = StatusCode::OK; | |
*data.response.body_mut() = format!( | |
"<!DOCTYPE html><html><body>Remote Addr: {}<br>Cipher suite: {:?}</body></html>", | |
data.remote_addr, | |
data.tls_info.as_ref().unwrap().ciphersuite | |
).into(); | |
// elided: write access log | |
return Ok(data.response); | |
} | |
#[derive(Clone)] | |
pub struct TlsInfo { | |
pub sni_hostname: Option<String>, | |
pub alpn_protocol: Option<String>, | |
pub ciphersuite: Option<SupportedCipherSuite>, | |
pub version: Option<ProtocolVersion>, | |
} | |
impl TlsInfo { | |
pub fn from_tls_connection(conn: &ServerConnection) -> TlsInfo | |
{ | |
TlsInfo { | |
sni_hostname: conn.sni_hostname().map(|s| s.to_owned()), | |
alpn_protocol: conn.alpn_protocol().map(|s| String::from_utf8_lossy(s).into_owned()), | |
ciphersuite: conn.negotiated_cipher_suite(), | |
version: conn.protocol_version(), | |
} | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Create self-signed certificates on linux with: | |
$ openssl req -x509 -newkey rsa:4096 -keyout key.pem -out cert.pem -sha256 -days 365 -nodes |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment