Skip to content

Instantly share code, notes, and snippets.

@mikedilger
Last active May 10, 2022 01:49
Show Gist options
  • Star 5 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save mikedilger/589f616a2ca607ad1daed278042c1bb8 to your computer and use it in GitHub Desktop.
Save mikedilger/589f616a2ca607ad1daed278042c1bb8 to your computer and use it in GitHub Desktop.
Using hyper and rustls
[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"
#[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(),
}
}
}
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