From 308027db7c3c74161bbecf33504f0ae73747a84c Mon Sep 17 00:00:00 2001 From: Dan Bond Date: Wed, 6 Apr 2022 14:22:31 -0700 Subject: [PATCH] Add method for creating a HTTPS server (#2) * create https server Signed-off-by: Dan Bond * add TLS tests Signed-off-by: Dan Bond --- Cargo.toml | 4 +- Makefile | 6 +++ README.md | 23 ++++++++-- src/lib.rs | 10 ++--- src/server.rs | 85 ++++++++++++++++++++++--------------- tests/certs/certificate.pem | 9 ++++ tests/certs/private_key.pem | 3 ++ tests/server.rs | 43 ++++++++++++++++++- 8 files changed, 137 insertions(+), 46 deletions(-) create mode 100644 tests/certs/certificate.pem create mode 100644 tests/certs/private_key.pem diff --git a/Cargo.toml b/Cargo.toml index 564b222..3dcdede 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "metrics_server" -version = "0.3.0" +version = "0.4.0" authors = ["Dan Bond "] edition = "2021" rust-version = "1.58" @@ -15,7 +15,7 @@ categories = ["web-programming::http-server"] include = ["src/**/*", "tests", "examples", "LICENSE", "README.md"] [dependencies] -tiny_http = "0.11" +tiny_http = { version = "0.11", features = ["ssl-rustls"] } [dev-dependencies] prometheus-client = "0.15" diff --git a/Makefile b/Makefile index 19331c3..e257b61 100644 --- a/Makefile +++ b/Makefile @@ -21,3 +21,9 @@ publish: # https://doc.rust-lang.org/cargo/reference/publishing.html cargo package --list cargo publish ${PUBLISH_FLAGS} + +.PHONY: gen-certs +gen-certs: + mkdir -p ./tests/certs + openssl genpkey -algorithm Ed25519 -out ./tests/certs/private_key.pem + openssl req -x509 -key ./tests/certs/private_key.pem -out ./tests/certs/certificate.pem -sha256 -nodes -subj '/CN=localhost' diff --git a/README.md b/README.md index 6e3c4a6..7628d06 100644 --- a/README.md +++ b/README.md @@ -16,14 +16,14 @@ This crate provides a thread safe, minimalstic HTTP server used to buffer metric Include the lib in your `Cargo.toml` dependencies: ```toml [dependencies] -metrics_server = "0.3" +metrics_server = "0.4" ``` -In your application: +### HTTP ```rust use metrics_server::MetricsServer; -// Create a new server and start listening for requests in the background. +// Create a new HTTP server and start listening for requests in the background. let server = MetricsServer::new("localhost:8001"); // Publish you application metrics. @@ -31,4 +31,21 @@ let bytes = server.update(Vec::from([1, 2, 3, 4])); assert_eq!(4, bytes); ``` +### HTTPS +Note: there is currently no option to skip TLS cert verification. +```rust +use metrics_server::MetricsServer; + +// Load TLS config. +let cert = include_bytes!("/path/to/cert.pem").to_vec(); +let key = include_bytes!("/path/to/key.pem").to_vec(); + +// Create a new HTTPS server and start listening for requests in the background. +let server = MetricsServer::https("localhost:8443", cert, key); + +// Publish you application metrics. +let bytes = server.update(Vec::from([1, 2, 3, 4])); +assert_eq!(4, bytes); +``` + For more comprehensive usage, see the included [examples](./examples). diff --git a/src/lib.rs b/src/lib.rs index 86bb5eb..d56959b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,18 +1,18 @@ #![warn(missing_docs, rustdoc::missing_doc_code_examples)] -//! A hassle-free, single-responsibility HTTP server used to easily expose metrics in an application. +//! A hassle-free, single-responsibility HTTP/S server used to easily expose metrics in an application. //! -//! This crate provides a thread safe, minimalstic HTTP server used to buffer metrics and serve -//! them via a standard /metrics endpoint. It's aim is to remove the boilerplate needed to +//! This crate provides a thread safe, minimalstic HTTP/S server used to buffer metrics and serve +//! them via a standard `/metrics` endpoint. It's aim is to remove the boilerplate needed to //! create such simple mechanisms. It is currently somewhat oppinionated and naive in order to //! maintain little complexity. //! //! # Examples //! -//! ``` +//! ```rust //! use metrics_server::MetricsServer; //! -//! // Create a new server and start listening for requests in the background. +//! // Create a new HTTP server and start listening for requests in the background. //! let server = MetricsServer::new("localhost:8001"); //! //! // Publish your application metrics. diff --git a/src/server.rs b/src/server.rs index 02598d8..571a1b5 100644 --- a/src/server.rs +++ b/src/server.rs @@ -1,10 +1,11 @@ +use std::net::ToSocketAddrs; use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::{Arc, Mutex}; use std::thread; use tiny_http::{Method, Response, Server}; -/// A thread-safe datastore for serving metrics via a HTTP server. +/// A thread-safe datastore for serving metrics via a HTTP/S server. pub struct MetricsServer { shared: Arc, thread: Option>, @@ -17,27 +18,64 @@ struct MetricsServerShared { } impl MetricsServer { - /// Creates a new empty `MetricsServer`. + /// Creates an empty `MetricsServer` and starts a HTTP server on a new thread at the given address. /// - /// Starts a simple HTTP server on a new thread at the given address and expose the stored metrics. - /// This server is intended to only be queried synchronously as it blocks upon receiving - /// each request. + /// This server will only respond synchronously as it blocks until receiving new requests. /// - /// # Examples + /// # Panics + /// + /// Panics if given an invalid address. + pub fn new(addr: A) -> Self + where + A: ToSocketAddrs, + { + let config = tiny_http::ServerConfig { addr, ssl: None }; + + MetricsServer::serve(config) + } + + /// Creates an empty `MetricsServer` and starts a HTTPS server on a new thread at the given address. /// - /// ``` - /// use metrics_server::MetricsServer; + /// This server will only respond synchronously as it blocks until receiving new requests. /// - /// let server = MetricsServer::new("localhost:8001"); - /// ``` + /// Note: there is currently no option to skip TLS cert verification. /// /// # Panics /// - /// Panics if given an invalid address. - pub fn new(addr: &str) -> Self { + /// Panics if given an invalid address or incorrect TLS credentials. + pub fn https(addr: A, certificate: Vec, private_key: Vec) -> Self + where + A: ToSocketAddrs, + { + let config = tiny_http::ServerConfig { + addr, + ssl: Some(tiny_http::SslConfig { + certificate, + private_key, + }), + }; + + MetricsServer::serve(config) + } + + /// Safely updates the data in a `MetricsServer` and returns the number of bytes written. + /// + /// This method is protected by a mutex making it safe + /// to call concurrently from multiple threads. + pub fn update(&self, data: Vec) -> usize { + let mut buf = self.shared.data.lock().unwrap(); + *buf = data; + buf.as_slice().len() + } + + fn serve(config: tiny_http::ServerConfig) -> Self + where + A: ToSocketAddrs, + { + // Create an Arc of the shared data. let shared = Arc::new(MetricsServerShared { data: Mutex::new(Vec::new()), - server: Server::http(addr).unwrap(), + server: Server::new(config).unwrap(), stop: AtomicBool::new(false), }); @@ -81,27 +119,6 @@ impl MetricsServer { MetricsServer { shared, thread } } - - /// Safely updates the data in a `MetricsServer` and returns the number of - /// bytes written. - /// - /// This method is protected by a mutex making it safe - /// to call concurrently from multiple threads. - /// - /// # Examples - /// - /// ``` - /// use metrics_server::MetricsServer; - /// - /// let server = MetricsServer::new("localhost:8001"); - /// let bytes = server.update(Vec::from([1, 2, 3, 4])); - /// assert_eq!(4, bytes); - /// ``` - pub fn update(&self, data: Vec) -> usize { - let mut buf = self.shared.data.lock().unwrap(); - *buf = data; - buf.as_slice().len() - } } impl Drop for MetricsServer { diff --git a/tests/certs/certificate.pem b/tests/certs/certificate.pem new file mode 100644 index 0000000..bada470 --- /dev/null +++ b/tests/certs/certificate.pem @@ -0,0 +1,9 @@ +-----BEGIN CERTIFICATE----- +MIIBPDCB76ADAgECAhRxHim70w5qhJ/oUT/22jAulbCNATAFBgMrZXAwFDESMBAG +A1UEAwwJbG9jYWxob3N0MB4XDTIyMDQwNjE2MTc1MVoXDTIyMDUwNjE2MTc1MVow +FDESMBAGA1UEAwwJbG9jYWxob3N0MCowBQYDK2VwAyEAsqEDZLWNs1fXdMoAlHH/ +QMfSvRGM6fDE9MFnLwQgjLWjUzBRMB0GA1UdDgQWBBTH+IEpa29un9FHOfpAdqke +ahkXHTAfBgNVHSMEGDAWgBTH+IEpa29un9FHOfpAdqkeahkXHTAPBgNVHRMBAf8E +BTADAQH/MAUGAytlcANBAPn7SJqNHhWKuEpM2ekQZChi8DzOWigCejvufGpRSbUB +dm+RnjZ88KvlEXlqUqknvBTY9lbL3v/9ylsZiSKogAQ= +-----END CERTIFICATE----- diff --git a/tests/certs/private_key.pem b/tests/certs/private_key.pem new file mode 100644 index 0000000..0a221d8 --- /dev/null +++ b/tests/certs/private_key.pem @@ -0,0 +1,3 @@ +-----BEGIN PRIVATE KEY----- +MC4CAQAwBQYDK2VwBCIEIEmQJozhKPDztKioPj+Q8NhItwelrY2LBRJYoOIl4p2c +-----END PRIVATE KEY----- diff --git a/tests/server.rs b/tests/server.rs index d8d589b..6abfb23 100644 --- a/tests/server.rs +++ b/tests/server.rs @@ -2,12 +2,12 @@ use metrics_server::MetricsServer; #[test] #[should_panic] -fn test_server_invalid_address() { +fn test_http_server_invalid_address() { let _ = MetricsServer::new("invalid:99999999"); } #[test] -fn test_server_serve() { +fn test_http_server_serve() { let server = MetricsServer::new("localhost:8001"); for i in 0..3 { @@ -26,3 +26,42 @@ fn test_server_serve() { assert_eq!(v, buf); } } + +#[test] +#[should_panic] +fn test_https_server_invalid_address() { + // Load TLS config. + let cert = include_bytes!("./certs/certificate.pem").to_vec(); + let key = include_bytes!("./certs/private_key.pem").to_vec(); + + let _ = MetricsServer::https("invalid:99999999", cert, key); +} + +#[test] +#[should_panic] +fn test_https_server_invalid_cert() { + // Load TLS config. + let cert = Vec::new(); + let key = include_bytes!("./certs/private_key.pem").to_vec(); + + let _ = MetricsServer::https("localhost:8441", cert, key); +} + +#[test] +#[should_panic] +fn test_https_server_invalid_key() { + // Load TLS config. + let cert = include_bytes!("./certs/certificate.pem").to_vec(); + let key = Vec::new(); + + let _ = MetricsServer::https("localhost:8442", cert, key); +} + +#[test] +fn test_https_server_serve() { + // Load TLS config. + let cert = include_bytes!("./certs/certificate.pem").to_vec(); + let key = include_bytes!("./certs/private_key.pem").to_vec(); + + let _ = MetricsServer::https("localhost:8443", cert, key); +}