RustのHyper + RustlsでHTTPSサーバーを立てるシンプルな例
やりたいこと
追記: 以下のリンクがそれなりにメンテナンスされている。permalinkなので必要に応じてmainブランチに切り替えて最新を確認する。
実際の例
code:bash
mkdir ssl_certs && cd ssl_certs && openssl req -x509 -newkey rsa:4096 -keyout server.key -out server.crt -days 365 -sha256 -nodes --subj '/CN=localhost/' && cd -
上記のコマンド生成されるのは、以下の2つのファイル。
ssl_certs/server.key
ssl_certs/server.crt
以下がHTTPSサーバーのhello world。
cargo runで立てられる。
curl -k https://localhost:3000/で動作確認できる。
code:rs
use futures_util::stream::{Stream, StreamExt, TryStreamExt};
use hyper::service::{make_service_fn, service_fn};
use hyper::{Body, Request, Response, Server};
use std::convert::Infallible;
use std::io;
use std::net::SocketAddr;
use tokio::net::{TcpListener, TcpStream};
use tokio_rustls::server::TlsStream;
use tokio_rustls::TlsAcceptor;
async fn hello_world(_req: Request<Body>) -> Result<Response<Body>, Infallible> {
Ok(Response::new("Hello, World".into()))
}
fn error(err: String) -> std::io::Error {
std::io::Error::new(std::io::ErrorKind::Other, err)
}
async fn main() -> io::Result<()> {
// We'll bind to 127.0.0.1:3000
// Build TLS configuration.
let tls_cfg = {
// Load public certificate.
let mut cert_reader = io::BufReader::new(std::fs::File::open("./ssl_certs/server.crt")?);
let certs = rustls::internal::pemfile::certs(&mut cert_reader).unwrap();
// Load private key.
let mut key_reader = io::BufReader::new(std::fs::File::open("./ssl_certs/server.key")?);
// Load and return a single private key.
let mut keys = rustls::internal::pemfile::pkcs8_private_keys(&mut key_reader).unwrap();
// Do not use client certificate authentication.
let mut cfg = rustls::ServerConfig::new(rustls::NoClientAuth::new());
// Select a certificate to use.
cfg.set_single_cert(certs, keys.remove(0)).unwrap();
// Configure ALPN to accept HTTP/2, HTTP/1.1 in that order.
std::sync::Arc::new(cfg)
};
// Create a TCP listener via tokio.
let mut tcp = TcpListener::bind(&addr).await?;
let tls_acceptor = &TlsAcceptor::from(tls_cfg);
// Prepare a long-running future stream to accept and serve clients.
let incoming_tls_stream = tcp
.incoming()
.map_err(|e| error(format!("Incoming failed: {:?}", e)))
.filter_map(move |s| async move {
let client = match s {
Ok(x) => x,
Err(e) => {
eprintln!("Failed to accept client: {}", e);
return None;
}
};
match tls_acceptor.accept(client).await {
Ok(x) => Some(Ok(x)),
Err(e) => {
eprintln!("Client connection error: {}", e);
None
}
}
});
// A Service is needed for every connection, so this
// creates one from our hello_world function.
let make_svc = make_service_fn(|_conn| async {
// service_fn converts our function into a Service
Ok::<_, Infallible>(service_fn(hello_world))
});
let server = Server::builder(HyperAcceptor {
acceptor: Box::pin(incoming_tls_stream),
})
.serve(make_svc);
// Run this server for... forever!
if let Err(e) = server.await {
eprintln!("server error: {}", e);
}
Ok(())
}
struct HyperAcceptor<S> {
acceptor: core::pin::Pin<Box<S>>,
}
impl<S> hyper::server::accept::Accept for HyperAcceptor<S>
where
S: Stream<Item = Result<TlsStream<TcpStream>, io::Error>>,
{
type Conn = TlsStream<TcpStream>;
type Error = io::Error;
fn poll_accept(
mut self: core::pin::Pin<&mut Self>,
cx: &mut core::task::Context,
) -> core::task::Poll<Option<Result<Self::Conn, Self::Error>>> {
self.acceptor.as_mut().poll_next(cx)
}
}
code:Cargo.toml
hyper = "0.13.0"
rustls = "0.18"
hyper-rustls = "0.21.0"
futures-util = "0.3.1"
tokio-rustls = "0.14.0"
以下にhyper-rustlsでのサーバー例がありこれを参考にしている。
hyper-rustlsと切り離された状態での例が欲しくて作った。
変更した点は、
証明書などの読み取りを簡素化したり、
Hyperのhello worldベースのHTTPSサーバーにしたり
accept時にエラーを起こしたときにサーバーがダウンしないようにしたり
HyperAcceptorでdynが使われていたところを静的ディスパッチできるように
したなどがある。
あと少し地味だがpemfile::pkcs8_private_keys()を使って秘密鍵をロードするように変更している。この方がopensslコマンドで生成したもの素直に読み取れ相性が良かった。 hyper-rustlsでの例では例えば信頼されていない証明書だと「received fatal alert: CertificateUnknown」エラーが起こり、サーバー自体もダウンするようにできている。以下のように「Voluntary server halt(自発的なサーバーの停止)」と書かれているのが該当コード。
code:rs
...
.and_then(move |s| {
tls_acceptor.accept(s).map_err(|e| {
println!("! Voluntary server halt due to client-connection error..."); // Errors could be handled here, instead of server aborting.
// Ok(None)
error(format!("TLS Error: {:?}", e))
})
上記のところを以下のfilter_map()に書き換えて、accept時にエラーが起こってもサーバー自体がダウンしないようにしている。
code:rs
...
.filter_map(move |s| async move {
let client = match s {
Ok(x) => x,
Err(e) => {
eprintln!("Failed to accept client: {}", e);
return None;
}
};
match tls_acceptor.accept(client).await {
Ok(x) => Some(Ok(x)),
Err(e) => {
eprintln!("Client connection error: {}", e);
None
}
}
});
&TlsAcceptor::from(tls_cfg)のように参照にするのが個人的なハマりどころだった。
以下に完全な状態のリポジトリがある。
以下がHyper公式のhello worldの例をHTTPS化するために加えた差分。 Chromeでの動作確認
https://gyazo.com/62c18aa4581c511b58569caa86f85a9a