Merged Meta, Status, and ResponseHeader into Response

This commit is contained in:
Emii Tatsuo 2020-12-01 16:36:29 -05:00
parent a92b3788e2
commit f922f8c70d
Signed by: Emi
GPG Key ID: 68FAB2E2E6DFC98B
9 changed files with 162 additions and 444 deletions

View File

@ -59,41 +59,33 @@ impl Handler {
HandlerCatchUnwind::new(fut_handle).await HandlerCatchUnwind::new(fut_handle).await
.unwrap_or_else(|err| { .unwrap_or_else(|err| {
error!("Handler failed: {:?}", err); error!("Handler failed: {:?}", err);
Response::server_error("").unwrap() Response::temporary_failure("")
}) })
}, },
Self::StaticHandler(response) => { Self::StaticHandler(response) => {
let body = response.as_ref(); match &response.body {
match body { None => Response::new(response.status, &response.meta),
None => Response::new(response.header().clone()), Some(Body::Bytes(bytes)) => Response::success(&response.meta, bytes.clone()),
Some(Body::Bytes(bytes)) => {
Response::new(response.header().clone())
.with_body(bytes.clone())
},
_ => { _ => {
error!(concat!( error!(concat!(
"Cannot construct a static handler with a reader-based body! ", "Cannot construct a static handler with a reader-based body! ",
" We're sending a response so that the client doesn't crash, but", " We're sending a response so that the client doesn't crash, but",
" given that this is a release build you should really fix this." " given that this is a release build you should really fix this."
)); ));
Response::server_error( Response::permanent_failure(
"Very bad server error, go tell the sysadmin to look at the logs." "Very bad server error, go tell the sysadmin to look at the logs."
).unwrap() )
} }
} }
}, },
#[cfg(feature = "serve_dir")] #[cfg(feature = "serve_dir")]
Self::FilesHandler(path) => { Self::FilesHandler(path) => {
let resp = if path.is_dir() { if path.is_dir() {
crate::util::serve_dir(path, request.trailing_segments()).await crate::util::serve_dir(path, request.trailing_segments()).await
} else { } else {
let mime = crate::util::guess_mime_from_path(&path); let mime = crate::util::guess_mime_from_path(&path);
crate::util::serve_file(path, &mime).await crate::util::serve_file(path, &mime).await
}; }
resp.unwrap_or_else(|e| {
warn!("Unexpected error serving from {}: {:?}", path.display(), e);
Response::server_error("").unwrap()
})
}, },
} }
} }
@ -204,7 +196,7 @@ impl Future for HandlerCatchUnwind {
Ok(res) => res, Ok(res) => res,
Err(e) => { Err(e) => {
error!("Handler panic! {:?}", e); error!("Handler panic! {:?}", e);
Poll::Ready(Response::server_error("")) Poll::Ready(Ok(Response::temporary_failure("")))
} }
} }
} }

View File

@ -14,6 +14,8 @@ use std::{
}; };
#[cfg(any(feature = "gemini_srv", feature = "user_management"))] #[cfg(any(feature = "gemini_srv", feature = "user_management"))]
use std::path::PathBuf; use std::path::PathBuf;
#[cfg(feature="ratelimiting")]
use std::net::IpAddr;
use tokio::{ use tokio::{
io, io,
io::BufReader, io::BufReader,
@ -217,15 +219,12 @@ impl ServerInner {
Ok(()) Ok(())
} }
async fn send_response(&self, mut response: Response, stream: &mut (impl AsyncWrite + Unpin + Send)) -> Result<()> { async fn send_response(&self, response: Response, stream: &mut (impl AsyncWrite + Unpin + Send)) -> Result<()> {
let maybe_body = response.take_body();
let header = response.header();
let use_complex_timeout = let use_complex_timeout =
header.status.is_success() && response.is_success() &&
maybe_body.is_some() && response.body.is_some() &&
header.meta.as_str() != "text/plain" && response.meta != "text/plain" &&
header.meta.as_str() != "text/gemini" && response.meta != "text/gemini" &&
self.complex_timeout.is_some(); self.complex_timeout.is_some();
let send_general_timeout; let send_general_timeout;
@ -244,13 +243,13 @@ impl ServerInner {
opt_timeout(send_general_timeout, async { opt_timeout(send_general_timeout, async {
// Send the header // Send the header
opt_timeout(send_header_timeout, send_response_header(response.header(), stream)) opt_timeout(send_header_timeout, send_response_header(&response, stream))
.await .await
.context("Timed out while sending response header")? .context("Timed out while sending response header")?
.context("Failed to write response header")?; .context("Failed to write response header")?;
// Send the body // Send the body
opt_timeout(send_body_timeout, maybe_send_response_body(maybe_body, stream)) opt_timeout(send_body_timeout, send_response_body(response.body, stream))
.await .await
.context("Timed out while sending response body")? .context("Timed out while sending response body")?
.context("Failed to write response body")?; .context("Failed to write response body")?;
@ -267,10 +266,7 @@ impl ServerInner {
fn check_rate_limits(&self, addr: IpAddr, req: &Request) -> Option<Response> { fn check_rate_limits(&self, addr: IpAddr, req: &Request) -> Option<Response> {
if let Some((_, limiter)) = self.rate_limits.match_request(req) { if let Some((_, limiter)) = self.rate_limits.match_request(req) {
if let Err(when) = limiter.check_key(addr) { if let Err(when) = limiter.check_key(addr) {
return Some(Response::new(ResponseHeader { return Some(Response::slow_down(when.as_secs()))
status: Status::SLOW_DOWN,
meta: Meta::new(when.as_secs().to_string()).unwrap()
}))
} }
} }
None None
@ -682,11 +678,19 @@ impl Default for Server {
} }
} }
async fn send_response_header(header: &ResponseHeader, stream: &mut (impl AsyncWrite + Unpin + Send)) -> Result<()> { async fn send_response_header(response: &Response, stream: &mut (impl AsyncWrite + Unpin + Send)) -> Result<()> {
let meta = if response.meta.len() > 1024 {
warn!("Attempted to send response with META exceeding maximum length, truncating");
&response.meta[..1024]
} else {
&response.meta[..]
};
let header = format!( let header = format!(
"{status} {meta}\r\n", "{status} {meta}\r\n",
status = header.status.code(), status = response.status,
meta = header.meta.as_str(), meta = meta,
); );
stream.write_all(header.as_bytes()).await?; stream.write_all(header.as_bytes()).await?;
@ -695,22 +699,17 @@ async fn send_response_header(header: &ResponseHeader, stream: &mut (impl AsyncW
Ok(()) Ok(())
} }
async fn maybe_send_response_body(maybe_body: Option<Body>, stream: &mut (impl AsyncWrite + Unpin + Send)) -> Result<()> { async fn send_response_body(mut body: Option<Body>, stream: &mut (impl AsyncWrite + Unpin + Send)) -> Result<()> {
if let Some(body) = maybe_body { match &mut body {
send_response_body(body, stream).await?; Some(Body::Bytes(ref bytes)) => stream.write_all(&bytes).await?,
Some(Body::Reader(ref mut reader)) => { io::copy(reader, stream).await?; },
None => {},
} }
Ok(()) if body.is_some() {
} stream.flush().await?;
async fn send_response_body(body: Body, stream: &mut (impl AsyncWrite + Unpin + Send)) -> Result<()> {
match body {
Body::Bytes(bytes) => stream.write_all(&bytes).await?,
Body::Reader(mut reader) => { io::copy(&mut reader, stream).await?; },
} }
stream.flush().await?;
Ok(()) Ok(())
} }

View File

@ -1,17 +1,8 @@
pub use uriparse::URIReference; pub use uriparse::URIReference;
mod meta;
pub use self::meta::Meta;
mod request; mod request;
pub use request::Request; pub use request::Request;
mod response_header;
pub use response_header::ResponseHeader;
mod status;
pub use status::{Status, StatusCategory};
mod response; mod response;
pub use response::Response; pub use response::Response;

View File

@ -1,130 +0,0 @@
use anyhow::*;
use crate::util::Cowy;
#[derive(Debug,Clone,PartialEq,Eq,Default)]
pub struct Meta(String);
impl Meta {
pub const MAX_LEN: usize = 1024;
/// Creates a new "Meta" string.
/// Fails if `meta` contains `\n`.
pub fn new(meta: impl Cowy<str>) -> Result<Self> {
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);
Ok(Self(meta.into()))
}
/// Creates a new "Meta" string.
/// Truncates `meta` to before:
/// - the first occurrence of `\n`
/// - the character that makes `meta` exceed `Meta::MAX_LEN`
pub fn new_lossy(meta: impl Cowy<str>) -> Self {
let meta = meta.as_ref();
let truncate_pos = meta.char_indices().position(|(i, ch)| {
let is_newline = ch == '\n';
let exceeds_limit = (i + ch.len_utf8()) > Self::MAX_LEN;
is_newline || exceeds_limit
});
let meta: String = match truncate_pos {
None => meta.into(),
Some(truncate_pos) => meta.get(..truncate_pos).unwrap().into(),
};
Self(meta)
}
pub fn empty() -> Self {
Self::default()
}
pub fn as_str(&self) -> &str {
&self.0
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::iter::repeat;
#[test]
fn new_rejects_newlines() {
let meta = "foo\nbar";
let meta = Meta::new(meta);
assert!(meta.is_err());
}
#[test]
fn new_accepts_max_len() {
let meta: String = repeat('x').take(Meta::MAX_LEN).collect();
let meta = Meta::new(meta);
assert!(meta.is_ok());
}
#[test]
fn new_rejects_exceeding_max_len() {
let meta: String = repeat('x').take(Meta::MAX_LEN + 1).collect();
let meta = Meta::new(meta);
assert!(meta.is_err());
}
#[test]
fn new_lossy_truncates() {
let meta = "foo\r\nbar\nquux";
let meta = Meta::new_lossy(meta);
assert_eq!(meta.as_str(), "foo\r");
}
#[test]
fn new_lossy_no_truncate() {
let meta = "foo bar\r";
let meta = Meta::new_lossy(meta);
assert_eq!(meta.as_str(), "foo bar\r");
}
#[test]
fn new_lossy_empty() {
let meta = "";
let meta = Meta::new_lossy(meta);
assert_eq!(meta.as_str(), "");
}
#[test]
fn new_lossy_truncates_to_empty() {
let meta = "\n\n\n";
let meta = Meta::new_lossy(meta);
assert_eq!(meta.as_str(), "");
}
#[test]
fn new_lossy_truncates_to_max_len() {
let meta: String = repeat('x').take(Meta::MAX_LEN + 1).collect();
let meta = Meta::new_lossy(meta);
assert_eq!(meta.as_str().len(), Meta::MAX_LEN);
}
#[test]
fn new_lossy_truncates_multi_byte_sequences() {
let mut meta: String = repeat('x').take(Meta::MAX_LEN - 1).collect();
meta.push('🦀');
assert_eq!(meta.len(), Meta::MAX_LEN + 3);
let meta = Meta::new_lossy(meta);
assert_eq!(meta.as_str().len(), Meta::MAX_LEN - 1);
}
}

View File

@ -1,101 +1,148 @@
use std::convert::TryInto;
use std::borrow::Borrow; use std::borrow::Borrow;
use anyhow::*; use crate::types::{Body, Document};
use uriparse::URIReference;
use crate::types::{ResponseHeader, Body, Document};
use crate::util::Cowy;
pub struct Response { pub struct Response {
header: ResponseHeader, pub status: u8,
body: Option<Body>, pub meta: String,
pub body: Option<Body>,
} }
impl Response { impl Response {
pub const fn new(header: ResponseHeader) -> Self {
/// Create a response with a given status and meta
pub fn new(status: u8, meta: impl ToString) -> Self {
Self { Self {
header, status,
meta: meta.to_string(),
body: None, body: None,
} }
} }
#[deprecated( /// Create a INPUT (10) response with a given prompt
since = "0.4.0", ///
note = "Deprecated in favor of Response::success_gemini() or Document::into()" /// Use [`Response::sensitive_input()`] for collecting any sensitive input, as input
)] /// collected by this request may be logged.
pub fn document(document: impl Borrow<Document>) -> Self { pub fn input(prompt: impl ToString) -> Self {
Self::success_gemini(document) Self::new(10, prompt)
} }
pub fn input(prompt: impl Cowy<str>) -> Result<Self> { /// Create a SENSITIVE INPUT (11) response with a given prompt
let header = ResponseHeader::input(prompt)?; ///
Ok(Self::new(header)) /// See also [`Response::input()`] for unsensitive inputs
pub fn sensitive_input(prompt: impl ToString) -> Self {
Self::new(11, prompt)
} }
pub fn input_lossy(prompt: impl Cowy<str>) -> Self { /// Create a SUCCESS (20) response with a given body and MIME
let header = ResponseHeader::input_lossy(prompt);
Self::new(header)
}
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 given body and MIME
pub fn success(mime: impl ToString, body: impl Into<Body>) -> Self { pub fn success(mime: impl ToString, body: impl Into<Body>) -> Self {
Self { Self {
header: ResponseHeader::success(mime), status: 20,
meta: mime.to_string(),
body: Some(body.into()), body: Some(body.into()),
} }
} }
/// Create a successful response with a `text/gemini` MIME /// Create a SUCCESS (20) response with a `text/gemini` MIME
pub fn success_gemini(body: impl Into<Body>) -> Self { pub fn success_gemini(body: impl Into<Body>) -> Self {
Self::success("text/gemini", body) Self::success("text/gemini", body)
} }
/// Create a successful response with a `text/plain` MIME /// Create a SUCCESS (20) response with a `text/plain` MIME
pub fn success_plain(body: impl Into<Body>) -> Self { pub fn success_plain(body: impl Into<Body>) -> Self {
Self::success("text/plain", body) Self::success("text/plain", body)
} }
pub fn server_error(reason: impl Cowy<str>) -> Result<Self> { /// Create a REDIRECT - TEMPORARY (30) response with a destination
let header = ResponseHeader::server_error(reason)?; pub fn redirect_temporary(dest: impl ToString) -> Self {
Ok(Self::new(header)) Self::new(30, dest)
} }
/// Create a REDIRECT - PERMANENT (31) response with a destination
pub fn redirect_permanent(dest: impl ToString) -> Self {
Self::new(31, dest)
}
/// Create a TEMPORARY FAILURE (40) response with a human readable error
pub fn temporary_failure(reason: impl ToString) -> Self {
Self::new(40, reason)
}
/// Create a SERVER UNAVAILABLE (41) response with a human readable error
///
/// Used to denote that the server is temporarily unavailable, for example due to
/// heavy load, or maintenance
pub fn server_unavailable(reason: impl ToString) -> Self {
Self::new(41, reason)
}
/// Create a CGI ERROR (42) response with a human readable error
pub fn cgi_error(reason: impl ToString) -> Self {
Self::new(42, reason)
}
/// Create a PROXY ERROR (43) response with a human readable error
pub fn proxy_error(reason: impl ToString) -> Self {
Self::new(43, reason)
}
/// Create a SLOW DOWN (44) response with a wait time in seconds
///
/// Used to denote that the user should wait a certain number of seconds before
/// sending another request, often for ratelimiting purposes
pub fn slow_down(wait: u64) -> Self {
Self::new(44, wait)
}
/// Create a PERMANENT FAILURE (50) response with a human readable error
pub fn permanent_failure(reason: impl ToString) -> Self {
Self::new(50, reason)
}
/// Create a NOT FOUND (51) response with no further information
///
/// Essentially a 404
pub fn not_found() -> Self { pub fn not_found() -> Self {
let header = ResponseHeader::not_found(); Self::new(51, String::new())
Self::new(header)
} }
pub fn bad_request_lossy(reason: impl Cowy<str>) -> Self { /// Create a GONE (52) response with a human readable error
let header = ResponseHeader::bad_request_lossy(reason); ///
Self::new(header) /// For when a resource used to be here, but never will be again
pub fn gone(reason: impl ToString) -> Self {
Self::new(52, reason)
} }
pub fn client_certificate_required() -> Self { /// Create a PROXY REQUEST REFUSED (53) response with a human readable error
let header = ResponseHeader::client_certificate_required(); ///
Self::new(header) /// The server does not serve content on this domain
pub fn proxy_request_refused(reason: impl ToString) -> Self {
Self::new(53, reason)
} }
pub fn certificate_not_authorized() -> Self { /// Create a BAD REQUEST (59) response with a human readable error
let header = ResponseHeader::certificate_not_authorized(); pub fn bad_request(reason: impl ToString) -> Self {
Self::new(header) Self::new(59, reason)
} }
pub fn with_body(mut self, body: impl Into<Body>) -> Self { /// Create a CLIENT CERTIFICATE REQUIRED (60) response with a human readable error
self.body = Some(body.into()); pub fn client_certificate_required(reason: impl ToString) -> Self {
self Self::new(60, reason)
} }
pub const fn header(&self) -> &ResponseHeader { /// Create a CERTIFICATE NOT AUTHORIZED (61) response with a human readable error
&self.header pub fn certificate_not_authorized(reason: impl ToString) -> Self {
Self::new(61, reason)
} }
pub fn take_body(&mut self) -> Option<Body> { /// Create a CERTIFICATE NOT VALID (62) response with a human readable error
self.body.take() pub fn certificate_not_valid(reason: impl ToString) -> Self {
Self::new(62, reason)
}
/// True if the response is a SUCCESS (10) response
pub const fn is_success(&self) -> bool {
self.status == 10
} }
} }

View File

@ -1,97 +0,0 @@
use std::convert::TryInto;
use anyhow::*;
use uriparse::URIReference;
use crate::util::Cowy;
use crate::types::{Status, Meta};
#[derive(Debug,Clone)]
pub struct ResponseHeader {
pub status: Status,
pub meta: Meta,
}
impl ResponseHeader {
pub fn input(prompt: impl Cowy<str>) -> Result<Self> {
Ok(Self {
status: Status::INPUT,
meta: Meta::new(prompt).context("Invalid input prompt")?,
})
}
pub fn input_lossy(prompt: impl Cowy<str>) -> Self {
Self {
status: Status::INPUT,
meta: Meta::new_lossy(prompt),
}
}
pub fn success(mime: impl ToString) -> Self {
Self {
status: Status::SUCCESS,
meta: Meta::new_lossy(mime.to_string()),
}
}
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 {
status: Status::PERMANENT_FAILURE,
meta: Meta::new(reason).context("Invalid server error reason")?,
})
}
pub fn server_error_lossy(reason: impl Cowy<str>) -> Self {
Self {
status: Status::PERMANENT_FAILURE,
meta: Meta::new_lossy(reason),
}
}
pub fn not_found() -> Self {
Self {
status: Status::NOT_FOUND,
meta: Meta::new_lossy("Not found"),
}
}
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 {
Self {
status: Status::CLIENT_CERTIFICATE_REQUIRED,
meta: Meta::new_lossy("No certificate provided"),
}
}
pub fn certificate_not_authorized() -> Self {
Self {
status: Status::CERTIFICATE_NOT_AUTHORIZED,
meta: Meta::new_lossy("Your certificate is not authorized to view this content"),
}
}
pub const fn status(&self) -> &Status {
&self.status
}
pub const fn meta(&self) -> &Meta {
&self.meta
}
}

View File

@ -1,82 +0,0 @@
#[derive(Debug,Copy,Clone,PartialEq,Eq)]
pub struct Status(u8);
impl Status {
pub const INPUT: Self = Self(10);
pub const SENSITIVE_INPUT: Self = Self(11);
pub const SUCCESS: Self = Self(20);
pub const REDIRECT_TEMPORARY: Self = Self(30);
pub const REDIRECT_PERMANENT: Self = Self(31);
pub const TEMPORARY_FAILURE: Self = Self(40);
pub const SERVER_UNAVAILABLE: Self = Self(41);
pub const CGI_ERROR: Self = Self(42);
pub const PROXY_ERROR: Self = Self(43);
pub const SLOW_DOWN: Self = Self(44);
pub const PERMANENT_FAILURE: Self = Self(50);
pub const NOT_FOUND: Self = Self(51);
pub const GONE: Self = Self(52);
pub const PROXY_REQUEST_REFUSED: Self = Self(53);
pub const BAD_REQUEST: Self = Self(59);
pub const CLIENT_CERTIFICATE_REQUIRED: Self = Self(60);
pub const CERTIFICATE_NOT_AUTHORIZED: Self = Self(61);
pub const CERTIFICATE_NOT_VALID: Self = Self(62);
pub const fn code(&self) -> u8 {
self.0
}
pub fn is_success(&self) -> bool {
self.category().is_success()
}
#[allow(clippy::missing_const_for_fn)]
pub fn category(&self) -> StatusCategory {
let class = self.0 / 10;
match class {
1 => StatusCategory::Input,
2 => StatusCategory::Success,
3 => StatusCategory::Redirect,
4 => StatusCategory::TemporaryFailure,
5 => StatusCategory::PermanentFailure,
6 => StatusCategory::ClientCertificateRequired,
_ => StatusCategory::PermanentFailure,
}
}
}
#[derive(Copy,Clone,PartialEq,Eq)]
pub enum StatusCategory {
Input,
Success,
Redirect,
TemporaryFailure,
PermanentFailure,
ClientCertificateRequired,
}
impl StatusCategory {
pub fn is_input(&self) -> bool {
*self == Self::Input
}
pub fn is_success(&self) -> bool {
*self == Self::Success
}
pub fn redirect(&self) -> bool {
*self == Self::Redirect
}
pub fn is_temporary_failure(&self) -> bool {
*self == Self::TemporaryFailure
}
pub fn is_permanent_failure(&self) -> bool {
*self == Self::PermanentFailure
}
pub fn is_client_certificate_required(&self) -> bool {
*self == Self::ClientCertificateRequired
}
}

View File

@ -165,7 +165,7 @@ impl UserManagementRoutes for crate::Server {
if let Some(input) = request.input().map(str::to_owned) { if let Some(input) = request.input().map(str::to_owned) {
(handler.clone())(request, user, input).await (handler.clone())(request, user, input).await
} else { } else {
Response::input(prompt) Ok(Response::input(prompt))
} }
} }
}) })
@ -207,7 +207,7 @@ async fn handle_base<UserData: Serialize + DeserializeOwned>(request: Request) -
async fn handle_ask_cert<UserData: Serialize + DeserializeOwned>(request: Request) -> Result<Response> { async fn handle_ask_cert<UserData: Serialize + DeserializeOwned>(request: Request) -> Result<Response> {
Ok(match request.user::<UserData>()? { Ok(match request.user::<UserData>()? {
User::Unauthenticated => { User::Unauthenticated => {
Response::client_certificate_required() Response::client_certificate_required("Please select a client certificate to proceed.")
}, },
User::NotSignedIn(nsi) => { User::NotSignedIn(nsi) => {
let segments = request.trailing_segments().iter().map(String::as_str).collect::<Vec<&str>>(); let segments = request.trailing_segments().iter().map(String::as_str).collect::<Vec<&str>>();
@ -270,7 +270,7 @@ async fn handle_register<UserData: Serialize + DeserializeOwned + Default>(reque
Err(e) => return Err(e.into()) Err(e) => return Err(e.into())
} }
} else { } else {
Response::input_lossy("Please pick a username") Response::input("Please pick a username")
} }
}, },
User::SignedIn(user) => { User::SignedIn(user) => {
@ -305,14 +305,14 @@ async fn handle_login<UserData: Serialize + DeserializeOwned + Default>(request:
Err(e) => return Err(e.into()), Err(e) => return Err(e.into()),
} }
} else { } else {
Response::input_lossy("Please enter your password") Response::sensitive_input("Please enter your password")
} }
} else if let Some(username) = request.input() { } else if let Some(username) = request.input() {
Response::redirect_temporary_lossy( Response::redirect_temporary(
format!("/account/login/{}", username).as_str() format!("/account/login/{}", username).as_str()
) )
} else { } else {
Response::input_lossy("Please enter your username") Response::input("Please enter your username")
} }
}, },
User::SignedIn(user) => { User::SignedIn(user) => {
@ -336,7 +336,7 @@ async fn handle_password<UserData: Serialize + DeserializeOwned + Default>(reque
user.set_password(password)?; user.set_password(password)?;
Response::success_gemini(include_str!("pages/password/success.gmi")) Response::success_gemini(include_str!("pages/password/success.gmi"))
} else { } else {
Response::input( Response::sensitive_input(
format!("Please enter a {}password", format!("Please enter a {}password",
if user.has_password() { if user.has_password() {
"new " "new "
@ -344,7 +344,7 @@ async fn handle_password<UserData: Serialize + DeserializeOwned + Default>(reque
"" ""
} }
) )
)? )
} }
}, },
}) })

View File

@ -14,7 +14,7 @@ use std::future::Future;
use tokio::time; use tokio::time;
#[cfg(feature="serve_dir")] #[cfg(feature="serve_dir")]
pub async fn serve_file<P: AsRef<Path>>(path: P, mime: &str) -> Result<Response> { pub async fn serve_file<P: AsRef<Path>>(path: P, mime: &str) -> Response {
let path = path.as_ref(); let path = path.as_ref();
let file = match File::open(path).await { let file = match File::open(path).await {
@ -22,17 +22,17 @@ pub async fn serve_file<P: AsRef<Path>>(path: P, mime: &str) -> Result<Response>
Err(err) => match err.kind() { Err(err) => match err.kind() {
std::io::ErrorKind::PermissionDenied => { std::io::ErrorKind::PermissionDenied => {
warn!("Asked to serve {}, but permission denied by OS", path.display()); warn!("Asked to serve {}, but permission denied by OS", path.display());
return Ok(Response::not_found()); return Response::not_found();
}, },
_ => return warn_unexpected(err, path, line!()), _ => return warn_unexpected(err, path, line!()),
} }
}; };
Ok(Response::success(mime, file)) Response::success(mime, file)
} }
#[cfg(feature="serve_dir")] #[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]) -> Response {
debug!("Dir: {}", dir.as_ref().display()); debug!("Dir: {}", dir.as_ref().display());
let dir = dir.as_ref(); let dir = dir.as_ref();
let dir = match dir.canonicalize() { let dir = match dir.canonicalize() {
@ -41,11 +41,11 @@ pub async fn serve_dir<D: AsRef<Path>, P: AsRef<Path>>(dir: D, virtual_path: &[P
match e.kind() { match e.kind() {
std::io::ErrorKind::NotFound => { std::io::ErrorKind::NotFound => {
warn!("Path {} not found. Check your configuration.", dir.display()); warn!("Path {} not found. Check your configuration.", dir.display());
return Response::server_error("Server incorrectly configured") return Response::temporary_failure("Server incorrectly configured")
}, },
std::io::ErrorKind::PermissionDenied => { std::io::ErrorKind::PermissionDenied => {
warn!("Permission denied for {}. Check that the server has access.", dir.display()); warn!("Permission denied for {}. Check that the server has access.", dir.display());
return Response::server_error("Server incorrectly configured") return Response::temporary_failure("Server incorrectly configured")
}, },
_ => return warn_unexpected(e, dir, line!()), _ => return warn_unexpected(e, dir, line!()),
} }
@ -61,12 +61,12 @@ pub async fn serve_dir<D: AsRef<Path>, P: AsRef<Path>>(dir: D, virtual_path: &[P
Ok(dir) => dir, Ok(dir) => dir,
Err(e) => { Err(e) => {
match e.kind() { match e.kind() {
std::io::ErrorKind::NotFound => return Ok(Response::not_found()), std::io::ErrorKind::NotFound => return Response::not_found(),
std::io::ErrorKind::PermissionDenied => { std::io::ErrorKind::PermissionDenied => {
// Runs when asked to serve a file in a restricted dir // Runs when asked to serve a file in a restricted dir
// i.e. not /noaccess, but /noaccess/file // i.e. not /noaccess, but /noaccess/file
warn!("Asked to serve {}, but permission denied by OS", path.display()); warn!("Asked to serve {}, but permission denied by OS", path.display());
return Ok(Response::not_found()); return Response::not_found();
}, },
_ => return warn_unexpected(e, path.as_ref(), line!()), _ => return warn_unexpected(e, path.as_ref(), line!()),
} }
@ -74,7 +74,7 @@ pub async fn serve_dir<D: AsRef<Path>, P: AsRef<Path>>(dir: D, virtual_path: &[P
}; };
if !path.starts_with(&dir) { if !path.starts_with(&dir) {
return Ok(Response::not_found()); return Response::not_found();
} }
if !path.is_dir() { if !path.is_dir() {
@ -86,14 +86,14 @@ pub async fn serve_dir<D: AsRef<Path>, P: AsRef<Path>>(dir: D, virtual_path: &[P
} }
#[cfg(feature="serve_dir")] #[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]) -> Response {
let mut dir = match fs::read_dir(path.as_ref()).await { let mut dir = match fs::read_dir(path.as_ref()).await {
Ok(dir) => dir, Ok(dir) => dir,
Err(err) => match err.kind() { Err(err) => match err.kind() {
io::ErrorKind::NotFound => return Ok(Response::not_found()), io::ErrorKind::NotFound => return Response::not_found(),
std::io::ErrorKind::PermissionDenied => { std::io::ErrorKind::PermissionDenied => {
warn!("Asked to serve {}, but permission denied by OS", path.as_ref().display()); warn!("Asked to serve {}, but permission denied by OS", path.as_ref().display());
return Ok(Response::not_found()); return Response::not_found();
}, },
_ => return warn_unexpected(err, path.as_ref(), line!()), _ => return warn_unexpected(err, path.as_ref(), line!()),
} }
@ -109,12 +109,10 @@ async fn serve_dir_listing<P: AsRef<Path>, B: AsRef<Path>>(path: P, virtual_path
document.add_link("..", "📁 ../"); document.add_link("..", "📁 ../");
} }
while let Some(entry) = dir.next_entry().await.context("Failed to list directory")? { while let Some(entry) = dir.next_entry().await.expect("Failed to list directory") {
let file_name = entry.file_name(); let file_name = entry.file_name();
let file_name = file_name.to_string_lossy(); let file_name = file_name.to_string_lossy();
let is_dir = entry.file_type().await let is_dir = entry.file_type().await.unwrap().is_dir();
.with_context(|| format!("Failed to get file type of `{}`", entry.path().display()))?
.is_dir();
let trailing_slash = if is_dir { "/" } else { "" }; let trailing_slash = if is_dir { "/" } else { "" };
let uri = format!("./{}{}", file_name, trailing_slash); let uri = format!("./{}{}", file_name, trailing_slash);
@ -125,7 +123,7 @@ async fn serve_dir_listing<P: AsRef<Path>, B: AsRef<Path>>(path: P, virtual_path
)); ));
} }
Ok(document.into()) document.into()
} }
#[cfg(feature="serve_dir")] #[cfg(feature="serve_dir")]
@ -146,7 +144,7 @@ pub fn guess_mime_from_path<P: AsRef<Path>>(path: P) -> &'static str {
#[cfg(feature="serve_dir")] #[cfg(feature="serve_dir")]
/// Print a warning to the log asking to file an issue and respond with "Unexpected Error" /// Print a warning to the log asking to file an issue and respond with "Unexpected Error"
pub (crate) fn warn_unexpected(err: impl std::fmt::Debug, path: &Path, line: u32) -> Result<Response> { pub (crate) fn warn_unexpected(err: impl std::fmt::Debug, path: &Path, line: u32) -> Response {
warn!( warn!(
concat!( concat!(
"Unexpected error serving path {} at util.rs:{}, please report to ", "Unexpected error serving path {} at util.rs:{}, please report to ",
@ -157,7 +155,7 @@ pub (crate) fn warn_unexpected(err: impl std::fmt::Debug, path: &Path, line: u32
line, line,
err err
); );
Response::server_error("Unexpected error") Response::temporary_failure("Unexpected error")
} }
/// A convenience trait alias for `AsRef<T> + Into<T::Owned>`, /// A convenience trait alias for `AsRef<T> + Into<T::Owned>`,