diff --git a/CHANGELOG.md b/CHANGELOG.md index aa5c95c..f1d7f18 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `redirect_temporary_lossy` for `Response` and `ResponseHeader` - `bad_request_lossy` for `Response` and `ResponseHeader` - support for a lot more mime-types in `guess_mime_from_path`, backed by the `mime_guess` crate +- customizable TLS cert & key paths by [@Alch-Emi](https://github.com/Alch-Emi) +- `server_dir` default feature for serve_dir utils [@Alch-Emi](https://github.com/Alch-Emi) +### Improved +- build time and size by [@Alch-Emi](https://github.com/Alch-Emi) ## [0.3.0] - 2020-11-14 ### Added diff --git a/Cargo.toml b/Cargo.toml index dd12927..9ad991d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,20 +8,25 @@ description = "Gemini server implementation" repository = "https://github.com/panicbit/northstar" documentation = "https://docs.rs/northstar" +[features] +default = ["serve_dir"] +serve_dir = ["mime_guess", "tokio/fs"] + [dependencies] anyhow = "1.0.33" rustls = { version = "0.18.1", features = ["dangerous_configuration"] } tokio-rustls = "0.20.0" -tokio = { version = "0.3.1", features = ["full"] } +tokio = { version = "0.3.1", features = ["io-util","net","time", "rt"] } mime = "0.3.16" uriparse = "0.6.3" percent-encoding = "2.1.0" -futures = "0.3.7" -itertools = "0.9.0" +futures-core = "0.3.7" log = "0.4.11" webpki = "0.21.0" lazy_static = "1.4.0" -mime_guess = "2.0.3" +mime_guess = { version = "2.0.3", optional = true } [dev-dependencies] env_logger = "0.8.1" +futures-util = "0.3.7" +tokio = { version = "0.3.1", features = ["macros", "rt-multi-thread", "sync"] } diff --git a/examples/certificates.rs b/examples/certificates.rs index f0f98b3..541fbe5 100644 --- a/examples/certificates.rs +++ b/examples/certificates.rs @@ -1,5 +1,6 @@ use anyhow::*; -use futures::{future::BoxFuture, FutureExt}; +use futures_core::future::BoxFuture; +use futures_util::FutureExt; use log::LevelFilter; use tokio::sync::RwLock; use northstar::{Certificate, GEMINI_MIME, GEMINI_PORT, Request, Response, Server}; diff --git a/examples/document.rs b/examples/document.rs index 5986896..8ff6bbb 100644 --- a/examples/document.rs +++ b/examples/document.rs @@ -1,5 +1,6 @@ use anyhow::*; -use futures::{future::BoxFuture, FutureExt}; +use futures_core::future::BoxFuture; +use futures_util::FutureExt; use log::LevelFilter; use northstar::{Server, Request, Response, GEMINI_PORT, Document}; use northstar::document::HeadingLevel::*; diff --git a/examples/serve_dir.rs b/examples/serve_dir.rs index 47145e1..fd26ac4 100644 --- a/examples/serve_dir.rs +++ b/examples/serve_dir.rs @@ -1,5 +1,6 @@ use anyhow::*; -use futures::{future::BoxFuture, FutureExt}; +use futures_core::future::BoxFuture; +use futures_util::FutureExt; use log::LevelFilter; use northstar::{Server, Request, Response, GEMINI_PORT}; diff --git a/src/lib.rs b/src/lib.rs index bb4ced9..b8ed975 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -5,9 +5,10 @@ use std::{ convert::TryFrom, io::BufReader, sync::Arc, + path::PathBuf, time::Duration, }; -use futures::{future::BoxFuture, FutureExt}; +use futures_core::future::BoxFuture; use tokio::{ prelude::*, io::{self, BufStream}, @@ -32,7 +33,7 @@ pub const REQUEST_URI_MAX_LEN: usize = 1024; pub const GEMINI_PORT: u16 = 1965; type Handler = Arc HandlerResponse + Send + Sync>; -type HandlerResponse = BoxFuture<'static, Result>; +pub (crate) type HandlerResponse = BoxFuture<'static, Result>; #[derive(Clone)] pub struct Server { @@ -94,7 +95,7 @@ impl Server { let handler = (self.handler)(request); let handler = AssertUnwindSafe(handler); - let response = handler.catch_unwind().await + let response = util::HandlerCatchUnwind::new(handler).await .unwrap_or_else(|_| Response::server_error("")) .or_else(|err| { error!("Handler failed: {:?}", err); @@ -202,6 +203,8 @@ impl Server { pub struct Builder { addr: A, + cert_path: PathBuf, + key_path: PathBuf, timeout: Duration, complex_body_timeout_override: Option, } @@ -212,9 +215,51 @@ impl Builder { addr, timeout: Duration::from_secs(1), complex_body_timeout_override: Some(Duration::from_secs(30)), + cert_path: PathBuf::from("cert/cert.pem"), + key_path: PathBuf::from("cert/key.pem"), } } + /// Sets the directory that northstar should look for TLS certs and keys into + /// + /// Northstar will look for files called `cert.pem` and `key.pem` in the provided + /// directory. + /// + /// This does not need to be set if both [`set_cert()`](Self::set_cert()) and + /// [`set_key()`](Self::set_key()) have been called. + /// + /// If not set, the default is `cert/` + pub fn set_tls_dir(self, dir: impl Into) -> Self { + let dir = dir.into(); + self.set_cert(dir.join("cert.pem")) + .set_key(dir.join("key.pem")) + } + + /// Set the path to the TLS certificate northstar will use + /// + /// This defaults to `cert/cert.pem`. + /// + /// This does not need to be called it [`set_tls_dir()`](Self::set_tls_dir()) has been + /// called. + pub fn set_cert(mut self, cert_path: impl Into) -> Self { + self.cert_path = cert_path.into(); + self + } + + /// Set the path to the ertificate key northstar will use + /// + /// This defaults to `cert/key.pem`. + /// + /// This does not need to be called it [`set_tls_dir()`](Self::set_tls_dir()) has been + /// called. + /// + /// This should of course correspond to the key set in + /// [`set_cert()`](Self::set_cert()) + pub fn set_key(mut self, key_path: impl Into) -> Self { + self.key_path = key_path.into(); + self + } + /// Set the timeout on incoming requests /// /// Note that this timeout is applied twice, once for the delivery of the request, and @@ -278,7 +323,7 @@ impl Builder { where F: Fn(Request) -> HandlerResponse + Send + Sync + 'static, { - let config = tls_config() + let config = tls_config(&self.cert_path, &self.key_path) .context("Failed to create TLS config")?; let listener = TcpListener::bind(self.addr).await @@ -345,12 +390,12 @@ async fn send_response_body(body: Body, stream: &mut (impl AsyncWrite + Unpin)) Ok(()) } -fn tls_config() -> Result> { +fn tls_config(cert_path: &PathBuf, key_path: &PathBuf) -> Result> { let mut config = ServerConfig::new(AllowAnonOrSelfsignedClient::new()); - let cert_chain = load_cert_chain() + let cert_chain = load_cert_chain(cert_path) .context("Failed to load TLS certificate")?; - let key = load_key() + let key = load_key(key_path) .context("Failed to load TLS key")?; config.set_single_cert(cert_chain, key) .context("Failed to use loaded TLS certificate")?; @@ -358,24 +403,22 @@ fn tls_config() -> Result> { Ok(config.into()) } -fn load_cert_chain() -> Result> { - let cert_path = "cert/cert.pem"; +fn load_cert_chain(cert_path: &PathBuf) -> Result> { let certs = std::fs::File::open(cert_path) - .with_context(|| format!("Failed to open `{}`", cert_path))?; + .with_context(|| format!("Failed to open `{:?}`", cert_path))?; let mut certs = BufReader::new(certs); let certs = rustls::internal::pemfile::certs(&mut certs) - .map_err(|_| anyhow!("failed to load certs `{}`", cert_path))?; + .map_err(|_| anyhow!("failed to load certs `{:?}`", cert_path))?; Ok(certs) } -fn load_key() -> Result { - let key_path = "cert/key.pem"; +fn load_key(key_path: &PathBuf) -> Result { let keys = std::fs::File::open(key_path) - .with_context(|| format!("Failed to open `{}`", key_path))?; + .with_context(|| format!("Failed to open `{:?}`", key_path))?; let mut keys = BufReader::new(keys); let mut keys = rustls::internal::pemfile::pkcs8_private_keys(&mut keys) - .map_err(|_| anyhow!("failed to load key `{}`", key_path))?; + .map_err(|_| anyhow!("failed to load key `{:?}`", key_path))?; ensure!(!keys.is_empty(), "no key found"); diff --git a/src/types/body.rs b/src/types/body.rs index dfeb8ca..d1356cc 100644 --- a/src/types/body.rs +++ b/src/types/body.rs @@ -1,4 +1,6 @@ -use tokio::{io::AsyncRead, fs::File}; +use tokio::io::AsyncRead; +#[cfg(feature="serve_dir")] +use tokio::fs::File; use crate::types::Document; @@ -37,6 +39,7 @@ impl<'a> From<&'a str> for Body { } } +#[cfg(feature="serve_dir")] impl From for Body { fn from(file: File) -> Self { Self::Reader(Box::new(file)) diff --git a/src/types/document.rs b/src/types/document.rs index f6fc5aa..e322357 100644 --- a/src/types/document.rs +++ b/src/types/document.rs @@ -39,7 +39,6 @@ use std::convert::TryInto; use std::fmt; -use itertools::Itertools; use crate::types::URIReference; use crate::util::Cowy; @@ -550,5 +549,6 @@ fn strip_newlines(text: impl Cowy) -> String { text.as_ref() .lines() .filter(|part| !part.is_empty()) + .collect::>() .join(" ") } diff --git a/src/util.rs b/src/util.rs index 1ba11af..c48b8bf 100644 --- a/src/util.rs +++ b/src/util.rs @@ -1,13 +1,21 @@ -use std::path::Path; +#[cfg(feature="serve_dir")] +use std::path::{Path, PathBuf}; +#[cfg(feature="serve_dir")] use mime::Mime; use anyhow::*; +#[cfg(feature="serve_dir")] use tokio::{ fs::{self, File}, io, }; -use crate::types::{Response, Document, document::HeadingLevel::*}; -use itertools::Itertools; +#[cfg(feature="serve_dir")] +use crate::types::{Document, document::HeadingLevel::*}; +use crate::types::Response; +use std::panic::{catch_unwind, AssertUnwindSafe}; +use std::task::Poll; +use futures_core::future::Future; +#[cfg(feature="serve_dir")] pub async fn serve_file>(path: P, mime: &Mime) -> Result { let path = path.as_ref(); @@ -22,6 +30,7 @@ pub async fn serve_file>(path: P, mime: &Mime) -> Result, P: AsRef>(dir: D, virtual_path: &[P]) -> Result { debug!("Dir: {}", dir.as_ref().display()); let dir = dir.as_ref().canonicalize() @@ -47,6 +56,7 @@ pub async fn serve_dir, P: AsRef>(dir: D, virtual_path: &[P serve_dir_listing(path, virtual_path).await } +#[cfg(feature="serve_dir")] async fn serve_dir_listing, B: AsRef>(path: P, virtual_path: &[B]) -> Result { let mut dir = match fs::read_dir(path).await { Ok(dir) => dir, @@ -56,10 +66,10 @@ async fn serve_dir_listing, B: AsRef>(path: P, virtual_path } }; - let breadcrumbs = virtual_path.iter().map(|segment| segment.as_ref().display()).join("/"); + let breadcrumbs: PathBuf = virtual_path.iter().collect(); let mut document = Document::new(); - document.add_heading(H1, format!("Index of /{}", breadcrumbs)); + document.add_heading(H1, format!("Index of /{}", breadcrumbs.display())); document.add_blank_line(); if virtual_path.get(0).map(<_>::as_ref) != Some(Path::new("")) { @@ -85,6 +95,7 @@ async fn serve_dir_listing, B: AsRef>(path: P, virtual_path Ok(Response::document(document)) } +#[cfg(feature="serve_dir")] pub fn guess_mime_from_path>(path: P) -> Mime { let path = path.as_ref(); let extension = path.extension().and_then(|s| s.to_str()); @@ -115,3 +126,32 @@ where C: AsRef + Into, T: ToOwned + ?Sized, {} + +/// A utility for catching unwinds on Futures. +/// +/// This is adapted from the futures-rs CatchUnwind, in an effort to reduce the large +/// amount of dependencies tied into the feature that provides this simple struct. +#[must_use = "futures do nothing unless polled"] +pub (crate) struct HandlerCatchUnwind { + future: AssertUnwindSafe, +} + +impl HandlerCatchUnwind { + pub(super) fn new(future: AssertUnwindSafe) -> Self { + Self { future } + } +} + +impl Future for HandlerCatchUnwind { + type Output = Result, Box>; + + fn poll( + mut self: std::pin::Pin<&mut Self>, + cx: &mut std::task::Context + ) -> Poll { + match catch_unwind(AssertUnwindSafe(|| self.future.as_mut().poll(cx))) { + Ok(res) => res.map(Ok), + Err(e) => Poll::Ready(Err(e)) + } + } +}