Merge remote-tracking branch 'upstream/master' into user-management
This commit is contained in:
commit
4996ae99d2
11
CHANGELOG.md
11
CHANGELOG.md
|
|
@ -5,6 +5,17 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
### Added
|
||||||
|
- `document` API for creating Gemini documents
|
||||||
|
- preliminary timeout API, incl a special case for complex MIMEs by [@Alch-Emi](https://github.com/Alch-Emi)
|
||||||
|
- `Response::success_with_body` by [@Alch-Emi](https://github.com/Alch-Emi)
|
||||||
|
- `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
|
## [0.3.0] - 2020-11-14
|
||||||
### Added
|
### Added
|
||||||
|
|
|
||||||
16
Cargo.toml
16
Cargo.toml
|
|
@ -10,29 +10,33 @@ documentation = "https://docs.rs/northstar"
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
user_management = ["sled", "bincode", "serde/derive", "bcrypt", "crc32fast"]
|
user_management = ["sled", "bincode", "serde/derive", "bcrypt", "crc32fast"]
|
||||||
|
default = ["serve_dir"]
|
||||||
|
serve_dir = ["mime_guess", "tokio/fs"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
anyhow = "1.0.33"
|
anyhow = "1.0.33"
|
||||||
rustls = { version = "0.18.1", features = ["dangerous_configuration"] }
|
rustls = { version = "0.18.1", features = ["dangerous_configuration"] }
|
||||||
tokio-rustls = "0.20.0"
|
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"
|
mime = "0.3.16"
|
||||||
uriparse = "0.6.3"
|
uriparse = "0.6.3"
|
||||||
percent-encoding = "2.1.0"
|
percent-encoding = "2.1.0"
|
||||||
futures = "0.3.7"
|
futures-core = "0.3.7"
|
||||||
itertools = "0.9.0"
|
|
||||||
log = "0.4.11"
|
log = "0.4.11"
|
||||||
webpki = "0.21.0"
|
webpki = "0.21.0"
|
||||||
lazy_static = "1.4.0"
|
lazy_static = "1.4.0"
|
||||||
|
mime_guess = { version = "2.0.3", optional = true }
|
||||||
sled = { version = "0.34.6", optional = true }
|
sled = { version = "0.34.6", optional = true }
|
||||||
bincode = { version = "1.3.1", optional = true }
|
bincode = { version = "1.3.1", optional = true }
|
||||||
serde = { version = "1.0", optional = true }
|
serde = { version = "1.0", optional = true }
|
||||||
bcrypt = { version = "0.9", optional = true }
|
bcrypt = { version = "0.9", optional = true }
|
||||||
crc32fast = { version = "1.2.1", optional = true }
|
crc32fast = { version = "1.2.1", optional = true }
|
||||||
|
|
||||||
[dev-dependencies]
|
|
||||||
env_logger = "0.8.1"
|
|
||||||
|
|
||||||
[[example]]
|
[[example]]
|
||||||
name = "user_management"
|
name = "user_management"
|
||||||
required-features = ["user_management"]
|
required-features = ["user_management"]
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
env_logger = "0.8.1"
|
||||||
|
futures-util = "0.3.7"
|
||||||
|
tokio = { version = "0.3.1", features = ["macros", "rt-multi-thread", "sync"] }
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
use anyhow::*;
|
use anyhow::*;
|
||||||
use futures::{future::BoxFuture, FutureExt};
|
use futures_core::future::BoxFuture;
|
||||||
|
use futures_util::FutureExt;
|
||||||
use log::LevelFilter;
|
use log::LevelFilter;
|
||||||
use tokio::sync::RwLock;
|
use tokio::sync::RwLock;
|
||||||
use northstar::{Certificate, GEMINI_MIME, GEMINI_PORT, Request, Response, Server};
|
use northstar::{Certificate, GEMINI_MIME, GEMINI_PORT, Request, Response, Server};
|
||||||
|
|
@ -37,8 +38,10 @@ fn handle_request(users: Arc<RwLock<HashMap<CertBytes, String>>>, request: Reque
|
||||||
if let Some(user) = users_read.get(cert_bytes) {
|
if let Some(user) = users_read.get(cert_bytes) {
|
||||||
// The user has already registered
|
// The user has already registered
|
||||||
Ok(
|
Ok(
|
||||||
Response::success(&GEMINI_MIME)
|
Response::success_with_body(
|
||||||
.with_body(format!("Welcome {}!", user))
|
&GEMINI_MIME,
|
||||||
|
format!("Welcome {}!", user)
|
||||||
|
)
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
// The user still needs to register
|
// The user still needs to register
|
||||||
|
|
@ -49,11 +52,13 @@ fn handle_request(users: Arc<RwLock<HashMap<CertBytes, String>>>, request: Reque
|
||||||
let mut users_write = users.write().await;
|
let mut users_write = users.write().await;
|
||||||
users_write.insert(cert_bytes.clone(), username.to_owned());
|
users_write.insert(cert_bytes.clone(), username.to_owned());
|
||||||
Ok(
|
Ok(
|
||||||
Response::success(&GEMINI_MIME)
|
Response::success_with_body(
|
||||||
.with_body(format!(
|
&GEMINI_MIME,
|
||||||
|
format!(
|
||||||
"Your account has been created {}! Welcome!",
|
"Your account has been created {}! Welcome!",
|
||||||
username
|
username
|
||||||
))
|
)
|
||||||
|
)
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
// The user didn't provide input, and should be prompted
|
// The user didn't provide input, and should be prompted
|
||||||
|
|
|
||||||
51
examples/document.rs
Normal file
51
examples/document.rs
Normal file
|
|
@ -0,0 +1,51 @@
|
||||||
|
use anyhow::*;
|
||||||
|
use futures_core::future::BoxFuture;
|
||||||
|
use futures_util::FutureExt;
|
||||||
|
use log::LevelFilter;
|
||||||
|
use northstar::{Server, Request, Response, GEMINI_PORT, Document};
|
||||||
|
use northstar::document::HeadingLevel::*;
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() -> Result<()> {
|
||||||
|
env_logger::builder()
|
||||||
|
.filter_module("northstar", LevelFilter::Debug)
|
||||||
|
.init();
|
||||||
|
|
||||||
|
Server::bind(("localhost", GEMINI_PORT))
|
||||||
|
.serve(handle_request)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_request(_request: Request) -> BoxFuture<'static, Result<Response>> {
|
||||||
|
async move {
|
||||||
|
let mut document = Document::new();
|
||||||
|
|
||||||
|
document
|
||||||
|
.add_preformatted(include_str!("northstar_logo.txt"))
|
||||||
|
.add_blank_line()
|
||||||
|
.add_link("https://docs.rs/northstar", "Documentation")
|
||||||
|
.add_link("https://github.com/panicbit/northstar", "GitHub")
|
||||||
|
.add_blank_line()
|
||||||
|
.add_heading(H1, "Usage")
|
||||||
|
.add_blank_line()
|
||||||
|
.add_text("Add the latest version of northstar to your `Cargo.toml`.")
|
||||||
|
.add_blank_line()
|
||||||
|
.add_heading(H2, "Manually")
|
||||||
|
.add_blank_line()
|
||||||
|
.add_preformatted_with_alt("toml", r#"northstar = "0.3.0" # check crates.io for the latest version"#)
|
||||||
|
.add_blank_line()
|
||||||
|
.add_heading(H2, "Automatically")
|
||||||
|
.add_blank_line()
|
||||||
|
.add_preformatted_with_alt("sh", "cargo add northstar")
|
||||||
|
.add_blank_line()
|
||||||
|
.add_heading(H1, "Generating a key & certificate")
|
||||||
|
.add_blank_line()
|
||||||
|
.add_preformatted_with_alt("sh", concat!(
|
||||||
|
"mkdir cert && cd cert\n",
|
||||||
|
"openssl req -x509 -nodes -newkey rsa:4096 -keyout key.pem -out cert.pem -days 365",
|
||||||
|
));
|
||||||
|
|
||||||
|
Ok(Response::document(document))
|
||||||
|
}
|
||||||
|
.boxed()
|
||||||
|
}
|
||||||
5
examples/northstar_logo.txt
Normal file
5
examples/northstar_logo.txt
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
__ __ __
|
||||||
|
____ ____ _____/ /_/ /_ _____/ /_____ ______
|
||||||
|
/ __ \/ __ \/ ___/ __/ __ \/ ___/ __/ __ `/ ___/
|
||||||
|
/ / / / /_/ / / / /_/ / / (__ ) /_/ /_/ / /
|
||||||
|
/_/ /_/\____/_/ \__/_/ /_/____/\__/\__,_/_/
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
use anyhow::*;
|
use anyhow::*;
|
||||||
use futures::{future::BoxFuture, FutureExt};
|
use futures_core::future::BoxFuture;
|
||||||
|
use futures_util::FutureExt;
|
||||||
use log::LevelFilter;
|
use log::LevelFilter;
|
||||||
use northstar::{Server, Request, Response, GEMINI_PORT};
|
use northstar::{Server, Request, Response, GEMINI_PORT};
|
||||||
|
|
||||||
|
|
|
||||||
252
src/lib.rs
252
src/lib.rs
|
|
@ -1,19 +1,27 @@
|
||||||
#[macro_use] extern crate log;
|
#[macro_use] extern crate log;
|
||||||
|
|
||||||
use std::{panic::AssertUnwindSafe, convert::TryFrom, io::BufReader, sync::Arc, path::PathBuf};
|
use std::{
|
||||||
use futures::{future::BoxFuture, FutureExt};
|
panic::AssertUnwindSafe,
|
||||||
|
convert::TryFrom,
|
||||||
|
io::BufReader,
|
||||||
|
sync::Arc,
|
||||||
|
path::PathBuf,
|
||||||
|
time::Duration,
|
||||||
|
};
|
||||||
|
use futures_core::future::BoxFuture;
|
||||||
use tokio::{
|
use tokio::{
|
||||||
prelude::*,
|
prelude::*,
|
||||||
io::{self, BufStream},
|
io::{self, BufStream},
|
||||||
net::{TcpStream, ToSocketAddrs},
|
net::{TcpStream, ToSocketAddrs},
|
||||||
|
time::timeout,
|
||||||
};
|
};
|
||||||
use tokio::net::TcpListener;
|
use tokio::net::TcpListener;
|
||||||
use rustls::ClientCertVerifier;
|
use rustls::ClientCertVerifier;
|
||||||
use tokio_rustls::{rustls, TlsAcceptor};
|
use tokio_rustls::{rustls, TlsAcceptor};
|
||||||
use rustls::*;
|
use rustls::*;
|
||||||
use anyhow::*;
|
use anyhow::*;
|
||||||
use uri::URIReference;
|
|
||||||
use lazy_static::lazy_static;
|
use lazy_static::lazy_static;
|
||||||
|
use crate::util::opt_timeout;
|
||||||
|
|
||||||
pub mod types;
|
pub mod types;
|
||||||
pub mod util;
|
pub mod util;
|
||||||
|
|
@ -31,13 +39,15 @@ pub const REQUEST_URI_MAX_LEN: usize = 1024;
|
||||||
pub const GEMINI_PORT: u16 = 1965;
|
pub const GEMINI_PORT: u16 = 1965;
|
||||||
|
|
||||||
type Handler = Arc<dyn Fn(Request) -> HandlerResponse + Send + Sync>;
|
type Handler = Arc<dyn Fn(Request) -> HandlerResponse + Send + Sync>;
|
||||||
type HandlerResponse = BoxFuture<'static, Result<Response>>;
|
pub (crate) type HandlerResponse = BoxFuture<'static, Result<Response>>;
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct Server {
|
pub struct Server {
|
||||||
tls_acceptor: TlsAcceptor,
|
tls_acceptor: TlsAcceptor,
|
||||||
listener: Arc<TcpListener>,
|
listener: Arc<TcpListener>,
|
||||||
handler: Handler,
|
handler: Handler,
|
||||||
|
timeout: Duration,
|
||||||
|
complex_timeout: Option<Duration>,
|
||||||
#[cfg(feature="user_management")]
|
#[cfg(feature="user_management")]
|
||||||
manager: UserManager,
|
manager: UserManager,
|
||||||
}
|
}
|
||||||
|
|
@ -62,16 +72,25 @@ impl Server {
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn serve_client(self, stream: TcpStream) -> Result<()> {
|
async fn serve_client(self, stream: TcpStream) -> Result<()> {
|
||||||
let stream = self.tls_acceptor.accept(stream).await
|
let fut_accept_request = async {
|
||||||
.context("Failed to establish TLS session")?;
|
let stream = self.tls_acceptor.accept(stream).await
|
||||||
let mut stream = BufStream::new(stream);
|
.context("Failed to establish TLS session")?;
|
||||||
|
let mut stream = BufStream::new(stream);
|
||||||
|
|
||||||
#[cfg(feature="user_management")]
|
#[cfg(feature="user_management")]
|
||||||
let mut request = self.receive_request(&mut stream).await
|
let request = self.receive_request(&mut stream).await
|
||||||
.context("Failed to receive request")?;
|
.context("Failed to receive request")?;
|
||||||
#[cfg(not(feature="user_management"))]
|
#[cfg(not(feature="user_management"))]
|
||||||
let mut request = Self::receive_request(&mut stream).await
|
let request = Self::receive_request(&mut stream).await
|
||||||
.context("Failed to receive request")?;
|
.context("Failed to receive request")?;
|
||||||
|
|
||||||
|
Result::<_, anyhow::Error>::Ok((request, stream))
|
||||||
|
};
|
||||||
|
|
||||||
|
// Use a timeout for interacting with the client
|
||||||
|
let fut_accept_request = timeout(self.timeout, fut_accept_request);
|
||||||
|
let (mut request, mut stream) = fut_accept_request.await
|
||||||
|
.context("Client timed out while waiting for response")??;
|
||||||
|
|
||||||
debug!("Client requested: {}", request.uri());
|
debug!("Client requested: {}", request.uri());
|
||||||
|
|
||||||
|
|
@ -88,7 +107,7 @@ impl Server {
|
||||||
let handler = (self.handler)(request);
|
let handler = (self.handler)(request);
|
||||||
let handler = AssertUnwindSafe(handler);
|
let handler = AssertUnwindSafe(handler);
|
||||||
|
|
||||||
let response = handler.catch_unwind().await
|
let response = util::HandlerCatchUnwind::new(handler).await
|
||||||
.unwrap_or_else(|_| Response::server_error(""))
|
.unwrap_or_else(|_| Response::server_error(""))
|
||||||
.or_else(|err| {
|
.or_else(|err| {
|
||||||
error!("Handler failed: {:?}", err);
|
error!("Handler failed: {:?}", err);
|
||||||
|
|
@ -96,11 +115,54 @@ impl Server {
|
||||||
})
|
})
|
||||||
.context("Request handler failed")?;
|
.context("Request handler failed")?;
|
||||||
|
|
||||||
send_response(response, &mut stream).await
|
self.send_response(response, &mut stream).await
|
||||||
.context("Failed to send response")?;
|
.context("Failed to send response")?;
|
||||||
|
|
||||||
stream.flush().await
|
Ok(())
|
||||||
.context("Failed to flush response data")?;
|
}
|
||||||
|
|
||||||
|
async fn send_response(&self, mut response: Response, stream: &mut (impl AsyncWrite + Unpin)) -> Result<()> {
|
||||||
|
let maybe_body = response.take_body();
|
||||||
|
let header = response.header();
|
||||||
|
|
||||||
|
let use_complex_timeout =
|
||||||
|
header.status.is_success() &&
|
||||||
|
maybe_body.is_some() &&
|
||||||
|
header.meta.as_str() != "text/plain" &&
|
||||||
|
header.meta.as_str() != "text/gemini" &&
|
||||||
|
self.complex_timeout.is_some();
|
||||||
|
|
||||||
|
let send_general_timeout;
|
||||||
|
let send_header_timeout;
|
||||||
|
let send_body_timeout;
|
||||||
|
|
||||||
|
if use_complex_timeout {
|
||||||
|
send_general_timeout = None;
|
||||||
|
send_header_timeout = Some(self.timeout);
|
||||||
|
send_body_timeout = self.complex_timeout;
|
||||||
|
} else {
|
||||||
|
send_general_timeout = Some(self.timeout);
|
||||||
|
send_header_timeout = None;
|
||||||
|
send_body_timeout = None;
|
||||||
|
}
|
||||||
|
|
||||||
|
opt_timeout(send_general_timeout, async {
|
||||||
|
// Send the header
|
||||||
|
opt_timeout(send_header_timeout, send_response_header(response.header(), stream))
|
||||||
|
.await
|
||||||
|
.context("Timed out while sending response header")?
|
||||||
|
.context("Failed to write response header")?;
|
||||||
|
|
||||||
|
// Send the body
|
||||||
|
opt_timeout(send_body_timeout, maybe_send_response_body(maybe_body, stream))
|
||||||
|
.await
|
||||||
|
.context("Timed out while sending response body")?
|
||||||
|
.context("Failed to write response body")?;
|
||||||
|
|
||||||
|
Ok::<_,Error>(())
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.context("Timed out while sending response data")??;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
@ -143,6 +205,10 @@ impl Server {
|
||||||
|
|
||||||
pub struct Builder<A> {
|
pub struct Builder<A> {
|
||||||
addr: A,
|
addr: A,
|
||||||
|
cert_path: PathBuf,
|
||||||
|
key_path: PathBuf,
|
||||||
|
timeout: Duration,
|
||||||
|
complex_body_timeout_override: Option<Duration>,
|
||||||
#[cfg(feature="user_management")]
|
#[cfg(feature="user_management")]
|
||||||
data_dir: PathBuf,
|
data_dir: PathBuf,
|
||||||
}
|
}
|
||||||
|
|
@ -151,6 +217,10 @@ impl<A: ToSocketAddrs> Builder<A> {
|
||||||
fn bind(addr: A) -> Self {
|
fn bind(addr: A) -> Self {
|
||||||
Self {
|
Self {
|
||||||
addr,
|
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"),
|
||||||
#[cfg(feature="user_management")]
|
#[cfg(feature="user_management")]
|
||||||
data_dir: "data".into(),
|
data_dir: "data".into(),
|
||||||
}
|
}
|
||||||
|
|
@ -165,11 +235,110 @@ impl<A: ToSocketAddrs> Builder<A> {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 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<PathBuf>) -> 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<PathBuf>) -> 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<PathBuf>) -> 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
|
||||||
|
/// once for sending the client's response. This means that for a 1 second timeout,
|
||||||
|
/// the client will have 1 second to complete the TLS handshake and deliver a request
|
||||||
|
/// header, then your API will have as much time as it needs to handle the request,
|
||||||
|
/// before the client has another second to receive the response.
|
||||||
|
///
|
||||||
|
/// If you would like a timeout for your code itself, please use
|
||||||
|
/// [`tokio::time::Timeout`] to implement it internally.
|
||||||
|
///
|
||||||
|
/// **The default timeout is 1 second.** As somewhat of a workaround for
|
||||||
|
/// shortcomings of the specification, this timeout, and any timeout set using this
|
||||||
|
/// method, is overridden in special cases, specifically for MIME types outside of
|
||||||
|
/// `text/plain` and `text/gemini`, to be 30 seconds. If you would like to change or
|
||||||
|
/// prevent this, please see
|
||||||
|
/// [`override_complex_body_timeout`](Self::override_complex_body_timeout()).
|
||||||
|
pub fn set_timeout(mut self, timeout: Duration) -> Self {
|
||||||
|
self.timeout = timeout;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Override the timeout for complex body types
|
||||||
|
///
|
||||||
|
/// Many clients choose to handle body types which cannot be displayed by prompting
|
||||||
|
/// the user if they would like to download or open the request body. However, since
|
||||||
|
/// this prompt occurs in the middle of receiving a request, often the connection
|
||||||
|
/// times out before the end user is able to respond to the prompt.
|
||||||
|
///
|
||||||
|
/// As a workaround, it is possible to set an override on the request timeout in
|
||||||
|
/// specific conditions:
|
||||||
|
///
|
||||||
|
/// 1. **Only override the timeout for receiving the body of the request.** This will
|
||||||
|
/// not override the timeout on sending the request header, nor on receiving the
|
||||||
|
/// response header.
|
||||||
|
/// 2. **Only override the timeout for successful responses.** The only bodies which
|
||||||
|
/// have bodies are successful ones. In all other cases, there's no body to
|
||||||
|
/// timeout for
|
||||||
|
/// 3. **Only override the timeout for complex body types.** Almost all clients are
|
||||||
|
/// able to display `text/plain` and `text/gemini` responses, and will not prompt
|
||||||
|
/// the user for these response types. This means that there is no reason to
|
||||||
|
/// expect a client to have a human-length response time for these MIME types.
|
||||||
|
/// Because of this, responses of this type will not be overridden.
|
||||||
|
///
|
||||||
|
/// This method is used to override the timeout for responses meeting these specific
|
||||||
|
/// criteria. All other stages of the connection will use the timeout specified in
|
||||||
|
/// [`set_timeout()`](Self::set_timeout()).
|
||||||
|
///
|
||||||
|
/// If this is set to [`None`], then the client will have the default amount of time
|
||||||
|
/// to both receive the header and the body. If this is set to [`Some`], the client
|
||||||
|
/// will have the default amount of time to recieve the header, and an *additional*
|
||||||
|
/// alotment of time to recieve the body.
|
||||||
|
///
|
||||||
|
/// The default timeout for this is 30 seconds.
|
||||||
|
pub fn override_complex_body_timeout(mut self, timeout: Option<Duration>) -> Self {
|
||||||
|
self.complex_body_timeout_override = timeout;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn serve<F>(self, handler: F) -> Result<()>
|
pub async fn serve<F>(self, handler: F) -> Result<()>
|
||||||
where
|
where
|
||||||
F: Fn(Request) -> HandlerResponse + Send + Sync + 'static,
|
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")?;
|
.context("Failed to create TLS config")?;
|
||||||
|
|
||||||
let listener = TcpListener::bind(self.addr).await
|
let listener = TcpListener::bind(self.addr).await
|
||||||
|
|
@ -179,6 +348,8 @@ impl<A: ToSocketAddrs> Builder<A> {
|
||||||
tls_acceptor: TlsAcceptor::from(config),
|
tls_acceptor: TlsAcceptor::from(config),
|
||||||
listener: Arc::new(listener),
|
listener: Arc::new(listener),
|
||||||
handler: Arc::new(handler),
|
handler: Arc::new(handler),
|
||||||
|
timeout: self.timeout,
|
||||||
|
complex_timeout: self.complex_body_timeout_override,
|
||||||
#[cfg(feature="user_management")]
|
#[cfg(feature="user_management")]
|
||||||
manager: UserManager::new(self.data_dir)?,
|
manager: UserManager::new(self.data_dir)?,
|
||||||
};
|
};
|
||||||
|
|
@ -187,18 +358,6 @@ impl<A: ToSocketAddrs> Builder<A> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn send_response(mut response: Response, stream: &mut (impl AsyncWrite + Unpin)) -> Result<()> {
|
|
||||||
send_response_header(response.header(), stream).await
|
|
||||||
.context("Failed to send response header")?;
|
|
||||||
|
|
||||||
if let Some(body) = response.take_body() {
|
|
||||||
send_response_body(body, stream).await
|
|
||||||
.context("Failed to send response body")?;
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn send_response_header(header: &ResponseHeader, stream: &mut (impl AsyncWrite + Unpin)) -> Result<()> {
|
async fn send_response_header(header: &ResponseHeader, stream: &mut (impl AsyncWrite + Unpin)) -> Result<()> {
|
||||||
let header = format!(
|
let header = format!(
|
||||||
"{status} {meta}\r\n",
|
"{status} {meta}\r\n",
|
||||||
|
|
@ -207,6 +366,15 @@ async fn send_response_header(header: &ResponseHeader, stream: &mut (impl AsyncW
|
||||||
);
|
);
|
||||||
|
|
||||||
stream.write_all(header.as_bytes()).await?;
|
stream.write_all(header.as_bytes()).await?;
|
||||||
|
stream.flush().await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn maybe_send_response_body(maybe_body: Option<Body>, stream: &mut (impl AsyncWrite + Unpin)) -> Result<()> {
|
||||||
|
if let Some(body) = maybe_body {
|
||||||
|
send_response_body(body, stream).await?;
|
||||||
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
@ -217,15 +385,17 @@ async fn send_response_body(body: Body, stream: &mut (impl AsyncWrite + Unpin))
|
||||||
Body::Reader(mut reader) => { io::copy(&mut reader, stream).await?; },
|
Body::Reader(mut reader) => { io::copy(&mut reader, stream).await?; },
|
||||||
}
|
}
|
||||||
|
|
||||||
|
stream.flush().await?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn tls_config() -> Result<Arc<ServerConfig>> {
|
fn tls_config(cert_path: &PathBuf, key_path: &PathBuf) -> Result<Arc<ServerConfig>> {
|
||||||
let mut config = ServerConfig::new(AllowAnonOrSelfsignedClient::new());
|
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")?;
|
.context("Failed to load TLS certificate")?;
|
||||||
let key = load_key()
|
let key = load_key(key_path)
|
||||||
.context("Failed to load TLS key")?;
|
.context("Failed to load TLS key")?;
|
||||||
config.set_single_cert(cert_chain, key)
|
config.set_single_cert(cert_chain, key)
|
||||||
.context("Failed to use loaded TLS certificate")?;
|
.context("Failed to use loaded TLS certificate")?;
|
||||||
|
|
@ -233,24 +403,22 @@ fn tls_config() -> Result<Arc<ServerConfig>> {
|
||||||
Ok(config.into())
|
Ok(config.into())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn load_cert_chain() -> Result<Vec<Certificate>> {
|
fn load_cert_chain(cert_path: &PathBuf) -> Result<Vec<Certificate>> {
|
||||||
let cert_path = "cert/cert.pem";
|
|
||||||
let certs = std::fs::File::open(cert_path)
|
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 mut certs = BufReader::new(certs);
|
||||||
let certs = rustls::internal::pemfile::certs(&mut 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)
|
Ok(certs)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn load_key() -> Result<PrivateKey> {
|
fn load_key(key_path: &PathBuf) -> Result<PrivateKey> {
|
||||||
let key_path = "cert/key.pem";
|
|
||||||
let keys = std::fs::File::open(key_path)
|
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 = BufReader::new(keys);
|
||||||
let mut keys = rustls::internal::pemfile::pkcs8_private_keys(&mut 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");
|
ensure!(!keys.is_empty(), "no key found");
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
pub use ::mime::Mime;
|
pub use ::mime::Mime;
|
||||||
pub use rustls::Certificate;
|
pub use rustls::Certificate;
|
||||||
|
pub use uriparse::URIReference;
|
||||||
|
|
||||||
mod meta;
|
mod meta;
|
||||||
pub use self::meta::Meta;
|
pub use self::meta::Meta;
|
||||||
|
|
@ -18,3 +19,6 @@ pub use response::Response;
|
||||||
|
|
||||||
mod body;
|
mod body;
|
||||||
pub use body::Body;
|
pub use body::Body;
|
||||||
|
|
||||||
|
pub mod document;
|
||||||
|
pub use document::Document;
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,20 @@
|
||||||
use tokio::{io::AsyncRead, fs::File};
|
use tokio::io::AsyncRead;
|
||||||
|
#[cfg(feature="serve_dir")]
|
||||||
|
use tokio::fs::File;
|
||||||
|
|
||||||
|
use crate::types::Document;
|
||||||
|
|
||||||
pub enum Body {
|
pub enum Body {
|
||||||
Bytes(Vec<u8>),
|
Bytes(Vec<u8>),
|
||||||
Reader(Box<dyn AsyncRead + Send + Sync + Unpin>),
|
Reader(Box<dyn AsyncRead + Send + Sync + Unpin>),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl From<Document> for Body {
|
||||||
|
fn from(document: Document) -> Self {
|
||||||
|
Body::from(document.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl From<Vec<u8>> for Body {
|
impl From<Vec<u8>> for Body {
|
||||||
fn from(bytes: Vec<u8>) -> Self {
|
fn from(bytes: Vec<u8>) -> Self {
|
||||||
Self::Bytes(bytes)
|
Self::Bytes(bytes)
|
||||||
|
|
@ -29,6 +39,7 @@ impl<'a> From<&'a str> for Body {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature="serve_dir")]
|
||||||
impl From<File> for Body {
|
impl From<File> for Body {
|
||||||
fn from(file: File) -> Self {
|
fn from(file: File) -> Self {
|
||||||
Self::Reader(Box::new(file))
|
Self::Reader(Box::new(file))
|
||||||
|
|
|
||||||
554
src/types/document.rs
Normal file
554
src/types/document.rs
Normal file
|
|
@ -0,0 +1,554 @@
|
||||||
|
//! Provides types for creating Gemini Documents.
|
||||||
|
//!
|
||||||
|
//! The module is centered around the `Document` type,
|
||||||
|
//! which provides all the necessary methods for programatically
|
||||||
|
//! creation of Gemini documents.
|
||||||
|
//!
|
||||||
|
//! # Examples
|
||||||
|
//!
|
||||||
|
//! ```
|
||||||
|
//! use northstar::document::HeadingLevel::*;
|
||||||
|
//!
|
||||||
|
//! let mut document = northstar::Document::new();
|
||||||
|
//!
|
||||||
|
//! document.add_heading(H1, "Heading 1");
|
||||||
|
//! document.add_heading(H2, "Heading 2");
|
||||||
|
//! document.add_heading(H3, "Heading 3");
|
||||||
|
//! document.add_blank_line();
|
||||||
|
//! document.add_text("text");
|
||||||
|
//! document.add_link("gemini://gemini.circumlunar.space", "Project Gemini");
|
||||||
|
//! document.add_unordered_list_item("list item");
|
||||||
|
//! document.add_quote("quote");
|
||||||
|
//! document.add_preformatted("preformatted");
|
||||||
|
//!
|
||||||
|
//! assert_eq!(document.to_string(), "\
|
||||||
|
//! ## Heading 1\n\
|
||||||
|
//! ### Heading 2\n\
|
||||||
|
//! #### Heading 3\n\
|
||||||
|
//! \n\
|
||||||
|
//! text\n\
|
||||||
|
//! => gemini://gemini.circumlunar.space/ Project Gemini\n\
|
||||||
|
//! * list item\n\
|
||||||
|
//! > quote\n\
|
||||||
|
//! ```\n\
|
||||||
|
//! preformatted\n\
|
||||||
|
//! ```\n\
|
||||||
|
//! ");
|
||||||
|
//! ```
|
||||||
|
#![warn(missing_docs)]
|
||||||
|
use std::convert::TryInto;
|
||||||
|
use std::fmt;
|
||||||
|
|
||||||
|
use crate::types::URIReference;
|
||||||
|
use crate::util::Cowy;
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
/// Represents a Gemini document.
|
||||||
|
///
|
||||||
|
/// Provides convenient methods for programatically
|
||||||
|
/// creation of Gemini documents.
|
||||||
|
pub struct Document {
|
||||||
|
items: Vec<Item>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Document {
|
||||||
|
/// Creates an empty Gemini `Document`.
|
||||||
|
///
|
||||||
|
/// # Examples
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// let document = northstar::Document::new();
|
||||||
|
///
|
||||||
|
/// assert_eq!(document.to_string(), "");
|
||||||
|
/// ```
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self::default()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Adds an `item` to the document.
|
||||||
|
///
|
||||||
|
/// An `item` usually corresponds to a single line,
|
||||||
|
/// except in the case of preformatted text.
|
||||||
|
///
|
||||||
|
/// # Examples
|
||||||
|
///
|
||||||
|
/// ```compile_fail
|
||||||
|
/// use northstar::document::{Document, Item, Text};
|
||||||
|
///
|
||||||
|
/// let mut document = Document::new();
|
||||||
|
/// let text = Text::new_lossy("foo");
|
||||||
|
/// let item = Item::Text(text);
|
||||||
|
///
|
||||||
|
/// document.add_item(item);
|
||||||
|
///
|
||||||
|
/// assert_eq!(document.to_string(), "foo\n");
|
||||||
|
/// ```
|
||||||
|
fn add_item(&mut self, item: Item) -> &mut Self {
|
||||||
|
self.items.push(item);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Adds multiple `items` to the document.
|
||||||
|
///
|
||||||
|
/// This is a convenience wrapper around `add_item`.
|
||||||
|
///
|
||||||
|
/// # Examples
|
||||||
|
///
|
||||||
|
/// ```compile_fail
|
||||||
|
/// use northstar::document::{Document, Item, Text};
|
||||||
|
///
|
||||||
|
/// let mut document = Document::new();
|
||||||
|
/// let items = vec!["foo", "bar", "baz"]
|
||||||
|
/// .into_iter()
|
||||||
|
/// .map(Text::new_lossy)
|
||||||
|
/// .map(Item::Text);
|
||||||
|
///
|
||||||
|
/// document.add_items(items);
|
||||||
|
///
|
||||||
|
/// assert_eq!(document.to_string(), "foo\nbar\nbaz\n");
|
||||||
|
/// ```
|
||||||
|
fn add_items<I>(&mut self, items: I) -> &mut Self
|
||||||
|
where
|
||||||
|
I: IntoIterator<Item = Item>,
|
||||||
|
{
|
||||||
|
self.items.extend(items);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Adds a blank line to the document.
|
||||||
|
///
|
||||||
|
/// # Examples
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// let mut document = northstar::Document::new();
|
||||||
|
///
|
||||||
|
/// document.add_blank_line();
|
||||||
|
///
|
||||||
|
/// assert_eq!(document.to_string(), "\n");
|
||||||
|
/// ```
|
||||||
|
pub fn add_blank_line(&mut self) -> &mut Self {
|
||||||
|
self.add_item(Item::Text(Text::blank()))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Adds plain text to the document.
|
||||||
|
///
|
||||||
|
/// This function allows adding multiple lines at once.
|
||||||
|
///
|
||||||
|
/// It inserts a whitespace at the beginning of a line
|
||||||
|
/// if it starts with a character sequence that
|
||||||
|
/// would make it a non-plain text line (e.g. link, heading etc).
|
||||||
|
///
|
||||||
|
/// # Examples
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// let mut document = northstar::Document::new();
|
||||||
|
///
|
||||||
|
/// document.add_text("hello\n* world!");
|
||||||
|
///
|
||||||
|
/// assert_eq!(document.to_string(), "hello\n * world!\n");
|
||||||
|
/// ```
|
||||||
|
pub fn add_text(&mut self, text: &str) -> &mut Self {
|
||||||
|
let text = text
|
||||||
|
.lines()
|
||||||
|
.map(Text::new_lossy)
|
||||||
|
.map(Item::Text);
|
||||||
|
|
||||||
|
self.add_items(text);
|
||||||
|
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Adds a link to the document.
|
||||||
|
///
|
||||||
|
/// `uri`s that fail to parse are substituted with `.`.
|
||||||
|
///
|
||||||
|
/// Consecutive newlines in `label` will be replaced
|
||||||
|
/// with a single whitespace.
|
||||||
|
///
|
||||||
|
/// # Examples
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// let mut document = northstar::Document::new();
|
||||||
|
///
|
||||||
|
/// document.add_link("https://wikipedia.org", "Wiki\n\nWiki");
|
||||||
|
///
|
||||||
|
/// assert_eq!(document.to_string(), "=> https://wikipedia.org/ Wiki Wiki\n");
|
||||||
|
/// ```
|
||||||
|
pub fn add_link<'a, U>(&mut self, uri: U, label: impl Cowy<str>) -> &mut Self
|
||||||
|
where
|
||||||
|
U: TryInto<URIReference<'a>>,
|
||||||
|
{
|
||||||
|
let uri = uri
|
||||||
|
.try_into()
|
||||||
|
.map(URIReference::into_owned)
|
||||||
|
.or_else(|_| ".".try_into()).expect("Northstar BUG");
|
||||||
|
let label = LinkLabel::from_lossy(label);
|
||||||
|
let link = Link { uri, label: Some(label) };
|
||||||
|
let link = Item::Link(link);
|
||||||
|
|
||||||
|
self.add_item(link);
|
||||||
|
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Adds a link to the document, but without a label.
|
||||||
|
///
|
||||||
|
/// See `add_link` for details.
|
||||||
|
///
|
||||||
|
/// # Examples
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// let mut document = northstar::Document::new();
|
||||||
|
///
|
||||||
|
/// document.add_link_without_label("https://wikipedia.org");
|
||||||
|
///
|
||||||
|
/// assert_eq!(document.to_string(), "=> https://wikipedia.org/\n");
|
||||||
|
/// ```
|
||||||
|
pub fn add_link_without_label<'a, U>(&mut self, uri: U) -> &mut Self
|
||||||
|
where
|
||||||
|
U: TryInto<URIReference<'a>>,
|
||||||
|
{
|
||||||
|
let uri = uri
|
||||||
|
.try_into()
|
||||||
|
.map(URIReference::into_owned)
|
||||||
|
.or_else(|_| ".".try_into()).expect("Northstar BUG");
|
||||||
|
let link = Link {
|
||||||
|
uri,
|
||||||
|
label: None,
|
||||||
|
};
|
||||||
|
let link = Item::Link(link);
|
||||||
|
|
||||||
|
self.add_item(link);
|
||||||
|
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Adds a block of preformatted text.
|
||||||
|
///
|
||||||
|
/// Lines that start with ` ``` ` will be prependend with a whitespace.
|
||||||
|
///
|
||||||
|
/// # Examples
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// let mut document = northstar::Document::new();
|
||||||
|
///
|
||||||
|
/// document.add_preformatted("a\n b\n c");
|
||||||
|
///
|
||||||
|
/// assert_eq!(document.to_string(), "```\na\n b\n c\n```\n");
|
||||||
|
/// ```
|
||||||
|
pub fn add_preformatted(&mut self, preformatted_text: &str) -> &mut Self {
|
||||||
|
self.add_preformatted_with_alt("", preformatted_text)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Adds a block of preformatted text with an alt text.
|
||||||
|
///
|
||||||
|
/// Consecutive newlines in `alt` will be replaced
|
||||||
|
/// with a single whitespace.
|
||||||
|
///
|
||||||
|
/// `preformatted_text` lines that start with ` ``` `
|
||||||
|
/// will be prependend with a whitespace.
|
||||||
|
///
|
||||||
|
/// # Examples
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// let mut document = northstar::Document::new();
|
||||||
|
///
|
||||||
|
/// document.add_preformatted_with_alt("rust", "fn main() {\n}\n");
|
||||||
|
///
|
||||||
|
/// assert_eq!(document.to_string(), "```rust\nfn main() {\n}\n```\n");
|
||||||
|
/// ```
|
||||||
|
pub fn add_preformatted_with_alt(&mut self, alt: &str, preformatted_text: &str) -> &mut Self {
|
||||||
|
let alt = AltText::new_lossy(alt);
|
||||||
|
let lines = preformatted_text
|
||||||
|
.lines()
|
||||||
|
.map(PreformattedText::new_lossy)
|
||||||
|
.collect();
|
||||||
|
let preformatted = Preformatted {
|
||||||
|
alt,
|
||||||
|
lines,
|
||||||
|
};
|
||||||
|
let preformatted = Item::Preformatted(preformatted);
|
||||||
|
|
||||||
|
self.add_item(preformatted);
|
||||||
|
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Adds a heading.
|
||||||
|
///
|
||||||
|
/// Consecutive newlines in `text` will be replaced
|
||||||
|
/// with a single whitespace.
|
||||||
|
///
|
||||||
|
/// # Examples
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// use northstar::document::HeadingLevel::H1;
|
||||||
|
///
|
||||||
|
/// let mut document = northstar::Document::new();
|
||||||
|
///
|
||||||
|
/// document.add_heading(H1, "Welcome!");
|
||||||
|
///
|
||||||
|
/// assert_eq!(document.to_string(), "# Welcome!\n");
|
||||||
|
/// ```
|
||||||
|
pub fn add_heading(&mut self, level: HeadingLevel, text: impl Cowy<str>) -> &mut Self {
|
||||||
|
let text = HeadingText::new_lossy(text);
|
||||||
|
let heading = Heading {
|
||||||
|
level,
|
||||||
|
text,
|
||||||
|
};
|
||||||
|
let heading = Item::Heading(heading);
|
||||||
|
|
||||||
|
self.add_item(heading);
|
||||||
|
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Adds an unordered list item.
|
||||||
|
///
|
||||||
|
/// Consecutive newlines in `text` will be replaced
|
||||||
|
/// with a single whitespace.
|
||||||
|
///
|
||||||
|
/// # Examples
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// let mut document = northstar::Document::new();
|
||||||
|
///
|
||||||
|
/// document.add_unordered_list_item("milk");
|
||||||
|
/// document.add_unordered_list_item("eggs");
|
||||||
|
///
|
||||||
|
/// assert_eq!(document.to_string(), "* milk\n* eggs\n");
|
||||||
|
/// ```
|
||||||
|
pub fn add_unordered_list_item(&mut self, text: &str) -> &mut Self {
|
||||||
|
let item = UnorderedListItem::new_lossy(text);
|
||||||
|
let item = Item::UnorderedListItem(item);
|
||||||
|
|
||||||
|
self.add_item(item);
|
||||||
|
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Adds a quote.
|
||||||
|
///
|
||||||
|
/// This function allows adding multiple quote lines at once.
|
||||||
|
///
|
||||||
|
/// # Examples
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// let mut document = northstar::Document::new();
|
||||||
|
///
|
||||||
|
/// document.add_quote("I think,\ntherefore I am");
|
||||||
|
///
|
||||||
|
/// assert_eq!(document.to_string(), "> I think,\n> therefore I am\n");
|
||||||
|
/// ```
|
||||||
|
pub fn add_quote(&mut self, text: &str) -> &mut Self {
|
||||||
|
let quote = text
|
||||||
|
.lines()
|
||||||
|
.map(Quote::new_lossy)
|
||||||
|
.map(Item::Quote);
|
||||||
|
|
||||||
|
self.add_items(quote);
|
||||||
|
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for Document {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
for item in &self.items {
|
||||||
|
match item {
|
||||||
|
Item::Text(text) => writeln!(f, "{}", text.0)?,
|
||||||
|
Item::Link(link) => {
|
||||||
|
let separator = if link.label.is_some() {" "} else {""};
|
||||||
|
let label = link.label.as_ref().map(|label| label.0.as_str())
|
||||||
|
.unwrap_or("");
|
||||||
|
|
||||||
|
writeln!(f, "=> {}{}{}", link.uri, separator, label)?;
|
||||||
|
}
|
||||||
|
Item::Preformatted(preformatted) => {
|
||||||
|
writeln!(f, "```{}", preformatted.alt.0)?;
|
||||||
|
|
||||||
|
for line in &preformatted.lines {
|
||||||
|
writeln!(f, "{}", line.0)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
writeln!(f, "```")?
|
||||||
|
}
|
||||||
|
Item::Heading(heading) => {
|
||||||
|
let level = match heading.level {
|
||||||
|
HeadingLevel::H1 => "#",
|
||||||
|
HeadingLevel::H2 => "##",
|
||||||
|
HeadingLevel::H3 => "###",
|
||||||
|
};
|
||||||
|
|
||||||
|
writeln!(f, "{} {}", level, heading.text.0)?;
|
||||||
|
}
|
||||||
|
Item::UnorderedListItem(item) => writeln!(f, "* {}", item.0)?,
|
||||||
|
Item::Quote(quote) => writeln!(f, "> {}", quote.0)?,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum Item {
|
||||||
|
Text(Text),
|
||||||
|
Link(Link),
|
||||||
|
Preformatted(Preformatted),
|
||||||
|
Heading(Heading),
|
||||||
|
UnorderedListItem(UnorderedListItem),
|
||||||
|
Quote(Quote),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
struct Text(String);
|
||||||
|
|
||||||
|
impl Text {
|
||||||
|
fn blank() -> Self {
|
||||||
|
Self::default()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn new_lossy(line: impl Cowy<str>) -> Self {
|
||||||
|
Self(lossy_escaped_line(line, SPECIAL_STARTS))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Link {
|
||||||
|
uri: URIReference<'static>,
|
||||||
|
label: Option<LinkLabel>,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct LinkLabel(String);
|
||||||
|
|
||||||
|
impl LinkLabel {
|
||||||
|
fn from_lossy(line: impl Cowy<str>) -> Self {
|
||||||
|
let line = strip_newlines(line);
|
||||||
|
|
||||||
|
LinkLabel(line)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Preformatted {
|
||||||
|
alt: AltText,
|
||||||
|
lines: Vec<PreformattedText>,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct PreformattedText(String);
|
||||||
|
|
||||||
|
impl PreformattedText {
|
||||||
|
fn new_lossy(line: impl Cowy<str>) -> Self {
|
||||||
|
Self(lossy_escaped_line(line, &[PREFORMATTED_TOGGLE_START]))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct AltText(String);
|
||||||
|
|
||||||
|
impl AltText {
|
||||||
|
fn new_lossy(alt: &str) -> Self {
|
||||||
|
let alt = strip_newlines(alt);
|
||||||
|
|
||||||
|
Self(alt)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Heading {
|
||||||
|
level: HeadingLevel,
|
||||||
|
text: HeadingText,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The level of a heading.
|
||||||
|
pub enum HeadingLevel {
|
||||||
|
/// Heading level 1 (`#`)
|
||||||
|
H1,
|
||||||
|
/// Heading level 2 (`##`)
|
||||||
|
H2,
|
||||||
|
/// Heading level 3 (`###`)
|
||||||
|
H3,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct HeadingText(String);
|
||||||
|
|
||||||
|
impl HeadingText {
|
||||||
|
fn new_lossy(line: impl Cowy<str>) -> Self {
|
||||||
|
let line = strip_newlines(line);
|
||||||
|
|
||||||
|
Self(line)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct UnorderedListItem(String);
|
||||||
|
|
||||||
|
impl UnorderedListItem {
|
||||||
|
fn new_lossy(text: &str) -> Self {
|
||||||
|
let text = strip_newlines(text);
|
||||||
|
|
||||||
|
Self(text)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Quote(String);
|
||||||
|
|
||||||
|
impl Quote {
|
||||||
|
fn new_lossy(text: &str) -> Self {
|
||||||
|
Self(lossy_escaped_line(text, &[QUOTE_START]))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const LINK_START: &str = "=>";
|
||||||
|
const PREFORMATTED_TOGGLE_START: &str = "```";
|
||||||
|
const HEADING_START: &str = "#";
|
||||||
|
const UNORDERED_LIST_ITEM_START: &str = "*";
|
||||||
|
const QUOTE_START: &str = ">";
|
||||||
|
|
||||||
|
const SPECIAL_STARTS: &[&str] = &[
|
||||||
|
LINK_START,
|
||||||
|
PREFORMATTED_TOGGLE_START,
|
||||||
|
HEADING_START,
|
||||||
|
UNORDERED_LIST_ITEM_START,
|
||||||
|
QUOTE_START,
|
||||||
|
];
|
||||||
|
|
||||||
|
fn starts_with_any(s: &str, starts: &[&str]) -> bool {
|
||||||
|
for start in starts {
|
||||||
|
if s.starts_with(start) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
fn lossy_escaped_line(line: impl Cowy<str>, escape_starts: &[&str]) -> String {
|
||||||
|
let line_ref = line.as_ref();
|
||||||
|
let contains_newline = line_ref.contains('\n');
|
||||||
|
let has_special_start = starts_with_any(line_ref, escape_starts);
|
||||||
|
|
||||||
|
if !contains_newline && !has_special_start {
|
||||||
|
return line.into();
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut line = String::new();
|
||||||
|
|
||||||
|
if has_special_start {
|
||||||
|
line.push(' ');
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(line_ref) = line_ref.split('\n').next() {
|
||||||
|
line.push_str(line_ref);
|
||||||
|
}
|
||||||
|
|
||||||
|
line
|
||||||
|
}
|
||||||
|
|
||||||
|
fn strip_newlines(text: impl Cowy<str>) -> String {
|
||||||
|
if !text.as_ref().contains(&['\r', '\n'][..]) {
|
||||||
|
return text.into();
|
||||||
|
}
|
||||||
|
|
||||||
|
text.as_ref()
|
||||||
|
.lines()
|
||||||
|
.filter(|part| !part.is_empty())
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join(" ")
|
||||||
|
}
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
use anyhow::*;
|
use anyhow::*;
|
||||||
use mime::Mime;
|
use crate::Mime;
|
||||||
|
use crate::util::Cowy;
|
||||||
|
|
||||||
|
|
||||||
#[derive(Debug,Clone,PartialEq,Eq,Default)]
|
#[derive(Debug,Clone,PartialEq,Eq,Default)]
|
||||||
pub struct Meta(String);
|
pub struct Meta(String);
|
||||||
|
|
@ -9,7 +11,7 @@ impl Meta {
|
||||||
|
|
||||||
/// Creates a new "Meta" string.
|
/// Creates a new "Meta" string.
|
||||||
/// Fails if `meta` contains `\n`.
|
/// Fails if `meta` contains `\n`.
|
||||||
pub fn new(meta: impl AsRef<str> + Into<String>) -> Result<Self> {
|
pub fn new(meta: impl Cowy<str>) -> Result<Self> {
|
||||||
ensure!(!meta.as_ref().contains("\n"), "Meta must not contain newlines");
|
ensure!(!meta.as_ref().contains("\n"), "Meta must not contain newlines");
|
||||||
ensure!(meta.as_ref().len() <= Self::MAX_LEN, "Meta must not exceed {} bytes", Self::MAX_LEN);
|
ensure!(meta.as_ref().len() <= Self::MAX_LEN, "Meta must not exceed {} bytes", Self::MAX_LEN);
|
||||||
|
|
||||||
|
|
@ -20,7 +22,7 @@ impl Meta {
|
||||||
/// Truncates `meta` to before:
|
/// Truncates `meta` to before:
|
||||||
/// - the first occurrence of `\n`
|
/// - the first occurrence of `\n`
|
||||||
/// - the character that makes `meta` exceed `Meta::MAX_LEN`
|
/// - the character that makes `meta` exceed `Meta::MAX_LEN`
|
||||||
pub fn new_lossy(meta: impl AsRef<str> + Into<String>) -> Self {
|
pub fn new_lossy(meta: impl Cowy<str>) -> Self {
|
||||||
let meta = meta.as_ref();
|
let meta = meta.as_ref();
|
||||||
let truncate_pos = meta.char_indices().position(|(i, ch)| {
|
let truncate_pos = meta.char_indices().position(|(i, ch)| {
|
||||||
let is_newline = ch == '\n';
|
let is_newline = ch == '\n';
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,10 @@
|
||||||
|
use std::convert::TryInto;
|
||||||
|
|
||||||
use anyhow::*;
|
use anyhow::*;
|
||||||
use crate::types::{ResponseHeader, Body, Mime};
|
use uriparse::URIReference;
|
||||||
|
use crate::types::{ResponseHeader, Body, Mime, Document};
|
||||||
|
use crate::util::Cowy;
|
||||||
|
use crate::GEMINI_MIME;
|
||||||
|
|
||||||
pub struct Response {
|
pub struct Response {
|
||||||
header: ResponseHeader,
|
header: ResponseHeader,
|
||||||
|
|
@ -14,12 +19,16 @@ impl Response {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn input(prompt: impl AsRef<str> + Into<String>) -> Result<Self> {
|
pub fn document(document: Document) -> Self {
|
||||||
|
Self::success_with_body(&GEMINI_MIME, document)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn input(prompt: impl Cowy<str>) -> Result<Self> {
|
||||||
let header = ResponseHeader::input(prompt)?;
|
let header = ResponseHeader::input(prompt)?;
|
||||||
Ok(Self::new(header))
|
Ok(Self::new(header))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn input_lossy(prompt: impl AsRef<str> + Into<String>) -> Self {
|
pub fn input_lossy(prompt: impl Cowy<str>) -> Self {
|
||||||
let header = ResponseHeader::input_lossy(prompt);
|
let header = ResponseHeader::input_lossy(prompt);
|
||||||
Self::new(header)
|
Self::new(header)
|
||||||
}
|
}
|
||||||
|
|
@ -29,7 +38,25 @@ impl Response {
|
||||||
Self::new(header)
|
Self::new(header)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn server_error(reason: impl AsRef<str> + Into<String>) -> Result<Self> {
|
pub fn redirect_temporary_lossy<'a>(location: impl TryInto<URIReference<'a>>) -> Self {
|
||||||
|
let header = ResponseHeader::redirect_temporary_lossy(location);
|
||||||
|
Self::new(header)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a successful response with a preconfigured body
|
||||||
|
///
|
||||||
|
/// This is equivilent to:
|
||||||
|
///
|
||||||
|
/// ```ignore
|
||||||
|
/// Response::success(mime)
|
||||||
|
/// .with_body(body)
|
||||||
|
/// ```
|
||||||
|
pub fn success_with_body(mime: &Mime, body: impl Into<Body>) -> Self {
|
||||||
|
Self::success(mime)
|
||||||
|
.with_body(body)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn server_error(reason: impl Cowy<str>) -> Result<Self> {
|
||||||
let header = ResponseHeader::server_error(reason)?;
|
let header = ResponseHeader::server_error(reason)?;
|
||||||
Ok(Self::new(header))
|
Ok(Self::new(header))
|
||||||
}
|
}
|
||||||
|
|
@ -39,6 +66,11 @@ impl Response {
|
||||||
Self::new(header)
|
Self::new(header)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn bad_request_lossy(reason: impl Cowy<str>) -> Self {
|
||||||
|
let header = ResponseHeader::bad_request_lossy(reason);
|
||||||
|
Self::new(header)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn client_certificate_required() -> Self {
|
pub fn client_certificate_required() -> Self {
|
||||||
let header = ResponseHeader::client_certificate_required();
|
let header = ResponseHeader::client_certificate_required();
|
||||||
Self::new(header)
|
Self::new(header)
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,9 @@
|
||||||
|
use std::convert::TryInto;
|
||||||
|
|
||||||
use anyhow::*;
|
use anyhow::*;
|
||||||
use mime::Mime;
|
use uriparse::URIReference;
|
||||||
|
use crate::Mime;
|
||||||
|
use crate::util::Cowy;
|
||||||
use crate::types::{Status, Meta};
|
use crate::types::{Status, Meta};
|
||||||
|
|
||||||
#[derive(Debug,Clone)]
|
#[derive(Debug,Clone)]
|
||||||
|
|
@ -9,14 +13,14 @@ pub struct ResponseHeader {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ResponseHeader {
|
impl ResponseHeader {
|
||||||
pub fn input(prompt: impl AsRef<str> + Into<String>) -> Result<Self> {
|
pub fn input(prompt: impl Cowy<str>) -> Result<Self> {
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
status: Status::INPUT,
|
status: Status::INPUT,
|
||||||
meta: Meta::new(prompt).context("Invalid input prompt")?,
|
meta: Meta::new(prompt).context("Invalid input prompt")?,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn input_lossy(prompt: impl AsRef<str> + Into<String>) -> Self {
|
pub fn input_lossy(prompt: impl Cowy<str>) -> Self {
|
||||||
Self {
|
Self {
|
||||||
status: Status::INPUT,
|
status: Status::INPUT,
|
||||||
meta: Meta::new_lossy(prompt),
|
meta: Meta::new_lossy(prompt),
|
||||||
|
|
@ -30,14 +34,26 @@ impl ResponseHeader {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn server_error(reason: impl AsRef<str> + Into<String>) -> Result<Self> {
|
pub fn redirect_temporary_lossy<'a>(location: impl TryInto<URIReference<'a>>) -> Self {
|
||||||
|
let location = match location.try_into() {
|
||||||
|
Ok(location) => location,
|
||||||
|
Err(_) => return Self::bad_request_lossy("Invalid redirect location"),
|
||||||
|
};
|
||||||
|
|
||||||
|
Self {
|
||||||
|
status: Status::REDIRECT_TEMPORARY,
|
||||||
|
meta: Meta::new_lossy(location.to_string()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn server_error(reason: impl Cowy<str>) -> Result<Self> {
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
status: Status::PERMANENT_FAILURE,
|
status: Status::PERMANENT_FAILURE,
|
||||||
meta: Meta::new(reason).context("Invalid server error reason")?,
|
meta: Meta::new(reason).context("Invalid server error reason")?,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn server_error_lossy(reason: impl AsRef<str> + Into<String>) -> Self {
|
pub fn server_error_lossy(reason: impl Cowy<str>) -> Self {
|
||||||
Self {
|
Self {
|
||||||
status: Status::PERMANENT_FAILURE,
|
status: Status::PERMANENT_FAILURE,
|
||||||
meta: Meta::new_lossy(reason),
|
meta: Meta::new_lossy(reason),
|
||||||
|
|
@ -51,6 +67,13 @@ impl ResponseHeader {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn bad_request_lossy(reason: impl Cowy<str>) -> Self {
|
||||||
|
Self {
|
||||||
|
status: Status::BAD_REQUEST,
|
||||||
|
meta: Meta::new_lossy(reason),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn client_certificate_required() -> Self {
|
pub fn client_certificate_required() -> Self {
|
||||||
Self {
|
Self {
|
||||||
status: Status::CLIENT_CERTIFICATE_REQUIRED,
|
status: Status::CLIENT_CERTIFICATE_REQUIRED,
|
||||||
|
|
|
||||||
116
src/util.rs
116
src/util.rs
|
|
@ -1,14 +1,22 @@
|
||||||
use std::path::Path;
|
#[cfg(feature="serve_dir")]
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
#[cfg(feature="serve_dir")]
|
||||||
use mime::Mime;
|
use mime::Mime;
|
||||||
use percent_encoding::utf8_percent_encode;
|
|
||||||
use anyhow::*;
|
use anyhow::*;
|
||||||
|
#[cfg(feature="serve_dir")]
|
||||||
use tokio::{
|
use tokio::{
|
||||||
fs::{self, File},
|
fs::{self, File},
|
||||||
io,
|
io,
|
||||||
};
|
};
|
||||||
use crate::{GEMINI_MIME, GEMINI_MIME_STR, Response};
|
#[cfg(feature="serve_dir")]
|
||||||
use itertools::Itertools;
|
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;
|
||||||
|
use tokio::time;
|
||||||
|
|
||||||
|
#[cfg(feature="serve_dir")]
|
||||||
pub async fn serve_file<P: AsRef<Path>>(path: P, mime: &Mime) -> Result<Response> {
|
pub async fn serve_file<P: AsRef<Path>>(path: P, mime: &Mime) -> Result<Response> {
|
||||||
let path = path.as_ref();
|
let path = path.as_ref();
|
||||||
|
|
||||||
|
|
@ -20,9 +28,10 @@ pub async fn serve_file<P: AsRef<Path>>(path: P, mime: &Mime) -> Result<Response
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(Response::success(&mime).with_body(file))
|
Ok(Response::success_with_body(mime, file))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature="serve_dir")]
|
||||||
pub async fn serve_dir<D: AsRef<Path>, P: AsRef<Path>>(dir: D, virtual_path: &[P]) -> Result<Response> {
|
pub async fn serve_dir<D: AsRef<Path>, P: AsRef<Path>>(dir: D, virtual_path: &[P]) -> Result<Response> {
|
||||||
debug!("Dir: {}", dir.as_ref().display());
|
debug!("Dir: {}", dir.as_ref().display());
|
||||||
let dir = dir.as_ref().canonicalize()
|
let dir = dir.as_ref().canonicalize()
|
||||||
|
|
@ -48,9 +57,8 @@ pub async fn serve_dir<D: AsRef<Path>, P: AsRef<Path>>(dir: D, virtual_path: &[P
|
||||||
serve_dir_listing(path, virtual_path).await
|
serve_dir_listing(path, virtual_path).await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature="serve_dir")]
|
||||||
async fn serve_dir_listing<P: AsRef<Path>, B: AsRef<Path>>(path: P, virtual_path: &[B]) -> Result<Response> {
|
async fn serve_dir_listing<P: AsRef<Path>, B: AsRef<Path>>(path: P, virtual_path: &[B]) -> Result<Response> {
|
||||||
use std::fmt::Write;
|
|
||||||
|
|
||||||
let mut dir = match fs::read_dir(path).await {
|
let mut dir = match fs::read_dir(path).await {
|
||||||
Ok(dir) => dir,
|
Ok(dir) => dir,
|
||||||
Err(err) => match err.kind() {
|
Err(err) => match err.kind() {
|
||||||
|
|
@ -59,14 +67,14 @@ async fn serve_dir_listing<P: AsRef<Path>, B: AsRef<Path>>(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 listing = String::new();
|
let mut document = Document::new();
|
||||||
|
|
||||||
writeln!(listing, "# Index of /{}", breadcrumbs)?;
|
document.add_heading(H1, format!("Index of /{}", breadcrumbs.display()));
|
||||||
writeln!(listing)?;
|
document.add_blank_line();
|
||||||
|
|
||||||
if virtual_path.get(0).map(<_>::as_ref) != Some(Path::new("")) {
|
if virtual_path.get(0).map(<_>::as_ref) != Some(Path::new("")) {
|
||||||
writeln!(listing, "=> .. 📁 ../")?;
|
document.add_link("..", "📁 ../");
|
||||||
}
|
}
|
||||||
|
|
||||||
while let Some(entry) = dir.next_entry().await.context("Failed to list directory")? {
|
while let Some(entry) = dir.next_entry().await.context("Failed to list directory")? {
|
||||||
|
|
@ -75,33 +83,83 @@ async fn serve_dir_listing<P: AsRef<Path>, B: AsRef<Path>>(path: P, virtual_path
|
||||||
let is_dir = entry.file_type().await
|
let is_dir = entry.file_type().await
|
||||||
.with_context(|| format!("Failed to get file type of `{}`", entry.path().display()))?
|
.with_context(|| format!("Failed to get file type of `{}`", entry.path().display()))?
|
||||||
.is_dir();
|
.is_dir();
|
||||||
|
let trailing_slash = if is_dir { "/" } else { "" };
|
||||||
|
let uri = format!("./{}{}", file_name, trailing_slash);
|
||||||
|
|
||||||
writeln!(
|
document.add_link(uri.as_str(), format!("{icon} {name}{trailing_slash}",
|
||||||
listing,
|
|
||||||
"=> {link}{trailing_slash} {icon} {name}{trailing_slash}",
|
|
||||||
icon = if is_dir { '📁' } else { '📄' },
|
icon = if is_dir { '📁' } else { '📄' },
|
||||||
link = utf8_percent_encode(&file_name, percent_encoding::NON_ALPHANUMERIC),
|
|
||||||
trailing_slash = if is_dir { "/" } else { "" },
|
|
||||||
name = file_name,
|
name = file_name,
|
||||||
)?;
|
trailing_slash = trailing_slash
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(Response::success(&GEMINI_MIME).with_body(listing))
|
Ok(Response::document(document))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature="serve_dir")]
|
||||||
pub fn guess_mime_from_path<P: AsRef<Path>>(path: P) -> Mime {
|
pub fn guess_mime_from_path<P: AsRef<Path>>(path: P) -> Mime {
|
||||||
let path = path.as_ref();
|
let path = path.as_ref();
|
||||||
let extension = path.extension().and_then(|s| s.to_str());
|
let extension = path.extension().and_then(|s| s.to_str());
|
||||||
let mime = match extension {
|
let extension = match extension {
|
||||||
Some(extension) => match extension {
|
Some(extension) => extension,
|
||||||
"gemini" => GEMINI_MIME_STR,
|
None => return mime::APPLICATION_OCTET_STREAM,
|
||||||
"txt" => "text/plain",
|
|
||||||
"jpeg" | "jpg" | "jpe" => "image/jpeg",
|
|
||||||
"png" => "image/png",
|
|
||||||
_ => "application/octet-stream",
|
|
||||||
},
|
|
||||||
None => "application/octet-stream",
|
|
||||||
};
|
};
|
||||||
|
|
||||||
mime.parse::<Mime>().unwrap_or(mime::APPLICATION_OCTET_STREAM)
|
if let "gemini" | "gmi" = extension {
|
||||||
|
return crate::GEMINI_MIME.clone();
|
||||||
|
}
|
||||||
|
|
||||||
|
mime_guess::from_ext(extension).first_or_octet_stream()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A convenience trait alias for `AsRef<T> + Into<T::Owned>`,
|
||||||
|
/// most commonly used to accept `&str` or `String`:
|
||||||
|
///
|
||||||
|
/// `Cowy<str>` ⇔ `AsRef<str> + Into<String>`
|
||||||
|
pub trait Cowy<T>
|
||||||
|
where
|
||||||
|
Self: AsRef<T> + Into<T::Owned>,
|
||||||
|
T: ToOwned + ?Sized,
|
||||||
|
{}
|
||||||
|
|
||||||
|
impl<C, T> Cowy<T> for C
|
||||||
|
where
|
||||||
|
C: AsRef<T> + Into<T::Owned>,
|
||||||
|
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<crate::HandlerResponse>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl HandlerCatchUnwind {
|
||||||
|
pub(super) fn new(future: AssertUnwindSafe<crate::HandlerResponse>) -> Self {
|
||||||
|
Self { future }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Future for HandlerCatchUnwind {
|
||||||
|
type Output = Result<Result<Response>, Box<dyn std::any::Any + Send>>;
|
||||||
|
|
||||||
|
fn poll(
|
||||||
|
mut self: std::pin::Pin<&mut Self>,
|
||||||
|
cx: &mut std::task::Context
|
||||||
|
) -> Poll<Self::Output> {
|
||||||
|
match catch_unwind(AssertUnwindSafe(|| self.future.as_mut().poll(cx))) {
|
||||||
|
Ok(res) => res.map(Ok),
|
||||||
|
Err(e) => Poll::Ready(Err(e))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) async fn opt_timeout<T>(duration: Option<time::Duration>, future: impl Future<Output = T>) -> Result<T, time::error::Elapsed> {
|
||||||
|
match duration {
|
||||||
|
Some(duration) => time::timeout(duration, future).await,
|
||||||
|
None => Ok(future.await),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue