Merged Meta, Status, and ResponseHeader into Response
This commit is contained in:
parent
a92b3788e2
commit
f922f8c70d
|
@ -59,41 +59,33 @@ impl Handler {
|
|||
HandlerCatchUnwind::new(fut_handle).await
|
||||
.unwrap_or_else(|err| {
|
||||
error!("Handler failed: {:?}", err);
|
||||
Response::server_error("").unwrap()
|
||||
Response::temporary_failure("")
|
||||
})
|
||||
},
|
||||
Self::StaticHandler(response) => {
|
||||
let body = response.as_ref();
|
||||
match body {
|
||||
None => Response::new(response.header().clone()),
|
||||
Some(Body::Bytes(bytes)) => {
|
||||
Response::new(response.header().clone())
|
||||
.with_body(bytes.clone())
|
||||
},
|
||||
match &response.body {
|
||||
None => Response::new(response.status, &response.meta),
|
||||
Some(Body::Bytes(bytes)) => Response::success(&response.meta, bytes.clone()),
|
||||
_ => {
|
||||
error!(concat!(
|
||||
"Cannot construct a static handler with a reader-based body! ",
|
||||
" 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."
|
||||
));
|
||||
Response::server_error(
|
||||
Response::permanent_failure(
|
||||
"Very bad server error, go tell the sysadmin to look at the logs."
|
||||
).unwrap()
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
#[cfg(feature = "serve_dir")]
|
||||
Self::FilesHandler(path) => {
|
||||
let resp = if path.is_dir() {
|
||||
if path.is_dir() {
|
||||
crate::util::serve_dir(path, request.trailing_segments()).await
|
||||
} else {
|
||||
let mime = crate::util::guess_mime_from_path(&path);
|
||||
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,
|
||||
Err(e) => {
|
||||
error!("Handler panic! {:?}", e);
|
||||
Poll::Ready(Response::server_error(""))
|
||||
Poll::Ready(Ok(Response::temporary_failure("")))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
57
src/lib.rs
57
src/lib.rs
|
@ -14,6 +14,8 @@ use std::{
|
|||
};
|
||||
#[cfg(any(feature = "gemini_srv", feature = "user_management"))]
|
||||
use std::path::PathBuf;
|
||||
#[cfg(feature="ratelimiting")]
|
||||
use std::net::IpAddr;
|
||||
use tokio::{
|
||||
io,
|
||||
io::BufReader,
|
||||
|
@ -217,15 +219,12 @@ impl ServerInner {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
async fn send_response(&self, mut response: Response, stream: &mut (impl AsyncWrite + Unpin + Send)) -> Result<()> {
|
||||
let maybe_body = response.take_body();
|
||||
let header = response.header();
|
||||
|
||||
async fn send_response(&self, response: Response, stream: &mut (impl AsyncWrite + Unpin + Send)) -> Result<()> {
|
||||
let use_complex_timeout =
|
||||
header.status.is_success() &&
|
||||
maybe_body.is_some() &&
|
||||
header.meta.as_str() != "text/plain" &&
|
||||
header.meta.as_str() != "text/gemini" &&
|
||||
response.is_success() &&
|
||||
response.body.is_some() &&
|
||||
response.meta != "text/plain" &&
|
||||
response.meta != "text/gemini" &&
|
||||
self.complex_timeout.is_some();
|
||||
|
||||
let send_general_timeout;
|
||||
|
@ -244,13 +243,13 @@ impl ServerInner {
|
|||
|
||||
opt_timeout(send_general_timeout, async {
|
||||
// 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
|
||||
.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))
|
||||
opt_timeout(send_body_timeout, send_response_body(response.body, stream))
|
||||
.await
|
||||
.context("Timed out while sending 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> {
|
||||
if let Some((_, limiter)) = self.rate_limits.match_request(req) {
|
||||
if let Err(when) = limiter.check_key(addr) {
|
||||
return Some(Response::new(ResponseHeader {
|
||||
status: Status::SLOW_DOWN,
|
||||
meta: Meta::new(when.as_secs().to_string()).unwrap()
|
||||
}))
|
||||
return Some(Response::slow_down(when.as_secs()))
|
||||
}
|
||||
}
|
||||
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!(
|
||||
"{status} {meta}\r\n",
|
||||
status = header.status.code(),
|
||||
meta = header.meta.as_str(),
|
||||
status = response.status,
|
||||
meta = meta,
|
||||
);
|
||||
|
||||
stream.write_all(header.as_bytes()).await?;
|
||||
|
@ -695,21 +699,16 @@ async fn send_response_header(header: &ResponseHeader, stream: &mut (impl AsyncW
|
|||
Ok(())
|
||||
}
|
||||
|
||||
async fn maybe_send_response_body(maybe_body: Option<Body>, stream: &mut (impl AsyncWrite + Unpin + Send)) -> Result<()> {
|
||||
if let Some(body) = maybe_body {
|
||||
send_response_body(body, stream).await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
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?; },
|
||||
async fn send_response_body(mut body: Option<Body>, stream: &mut (impl AsyncWrite + Unpin + Send)) -> Result<()> {
|
||||
match &mut body {
|
||||
Some(Body::Bytes(ref bytes)) => stream.write_all(&bytes).await?,
|
||||
Some(Body::Reader(ref mut reader)) => { io::copy(reader, stream).await?; },
|
||||
None => {},
|
||||
}
|
||||
|
||||
if body.is_some() {
|
||||
stream.flush().await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
@ -1,17 +1,8 @@
|
|||
pub use uriparse::URIReference;
|
||||
|
||||
mod meta;
|
||||
pub use self::meta::Meta;
|
||||
|
||||
mod request;
|
||||
pub use request::Request;
|
||||
|
||||
mod response_header;
|
||||
pub use response_header::ResponseHeader;
|
||||
|
||||
mod status;
|
||||
pub use status::{Status, StatusCategory};
|
||||
|
||||
mod response;
|
||||
pub use response::Response;
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -1,101 +1,148 @@
|
|||
use std::convert::TryInto;
|
||||
use std::borrow::Borrow;
|
||||
|
||||
use anyhow::*;
|
||||
use uriparse::URIReference;
|
||||
use crate::types::{ResponseHeader, Body, Document};
|
||||
use crate::util::Cowy;
|
||||
use crate::types::{Body, Document};
|
||||
|
||||
pub struct Response {
|
||||
header: ResponseHeader,
|
||||
body: Option<Body>,
|
||||
pub status: u8,
|
||||
pub meta: String,
|
||||
pub body: Option<Body>,
|
||||
}
|
||||
|
||||
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 {
|
||||
header,
|
||||
status,
|
||||
meta: meta.to_string(),
|
||||
body: None,
|
||||
}
|
||||
}
|
||||
|
||||
#[deprecated(
|
||||
since = "0.4.0",
|
||||
note = "Deprecated in favor of Response::success_gemini() or Document::into()"
|
||||
)]
|
||||
pub fn document(document: impl Borrow<Document>) -> Self {
|
||||
Self::success_gemini(document)
|
||||
/// Create a INPUT (10) response with a given prompt
|
||||
///
|
||||
/// Use [`Response::sensitive_input()`] for collecting any sensitive input, as input
|
||||
/// collected by this request may be logged.
|
||||
pub fn input(prompt: impl ToString) -> Self {
|
||||
Self::new(10, prompt)
|
||||
}
|
||||
|
||||
pub fn input(prompt: impl Cowy<str>) -> Result<Self> {
|
||||
let header = ResponseHeader::input(prompt)?;
|
||||
Ok(Self::new(header))
|
||||
/// Create a SENSITIVE INPUT (11) response with a given prompt
|
||||
///
|
||||
/// 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 {
|
||||
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
|
||||
/// Create a SUCCESS (20) response with a given body and MIME
|
||||
pub fn success(mime: impl ToString, body: impl Into<Body>) -> Self {
|
||||
Self {
|
||||
header: ResponseHeader::success(mime),
|
||||
status: 20,
|
||||
meta: mime.to_string(),
|
||||
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 {
|
||||
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 {
|
||||
Self::success("text/plain", body)
|
||||
}
|
||||
|
||||
pub fn server_error(reason: impl Cowy<str>) -> Result<Self> {
|
||||
let header = ResponseHeader::server_error(reason)?;
|
||||
Ok(Self::new(header))
|
||||
/// Create a REDIRECT - TEMPORARY (30) response with a destination
|
||||
pub fn redirect_temporary(dest: impl ToString) -> Self {
|
||||
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 {
|
||||
let header = ResponseHeader::not_found();
|
||||
Self::new(header)
|
||||
Self::new(51, String::new())
|
||||
}
|
||||
|
||||
pub fn bad_request_lossy(reason: impl Cowy<str>) -> Self {
|
||||
let header = ResponseHeader::bad_request_lossy(reason);
|
||||
Self::new(header)
|
||||
/// Create a GONE (52) response with a human readable error
|
||||
///
|
||||
/// 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 {
|
||||
let header = ResponseHeader::client_certificate_required();
|
||||
Self::new(header)
|
||||
/// Create a PROXY REQUEST REFUSED (53) response with a human readable error
|
||||
///
|
||||
/// 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 {
|
||||
let header = ResponseHeader::certificate_not_authorized();
|
||||
Self::new(header)
|
||||
/// Create a BAD REQUEST (59) response with a human readable error
|
||||
pub fn bad_request(reason: impl ToString) -> Self {
|
||||
Self::new(59, reason)
|
||||
}
|
||||
|
||||
pub fn with_body(mut self, body: impl Into<Body>) -> Self {
|
||||
self.body = Some(body.into());
|
||||
self
|
||||
/// Create a CLIENT CERTIFICATE REQUIRED (60) response with a human readable error
|
||||
pub fn client_certificate_required(reason: impl ToString) -> Self {
|
||||
Self::new(60, reason)
|
||||
}
|
||||
|
||||
pub const fn header(&self) -> &ResponseHeader {
|
||||
&self.header
|
||||
/// Create a CERTIFICATE NOT AUTHORIZED (61) response with a human readable error
|
||||
pub fn certificate_not_authorized(reason: impl ToString) -> Self {
|
||||
Self::new(61, reason)
|
||||
}
|
||||
|
||||
pub fn take_body(&mut self) -> Option<Body> {
|
||||
self.body.take()
|
||||
/// Create a CERTIFICATE NOT VALID (62) response with a human readable error
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -165,7 +165,7 @@ impl UserManagementRoutes for crate::Server {
|
|||
if let Some(input) = request.input().map(str::to_owned) {
|
||||
(handler.clone())(request, user, input).await
|
||||
} 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> {
|
||||
Ok(match request.user::<UserData>()? {
|
||||
User::Unauthenticated => {
|
||||
Response::client_certificate_required()
|
||||
Response::client_certificate_required("Please select a client certificate to proceed.")
|
||||
},
|
||||
User::NotSignedIn(nsi) => {
|
||||
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())
|
||||
}
|
||||
} else {
|
||||
Response::input_lossy("Please pick a username")
|
||||
Response::input("Please pick a username")
|
||||
}
|
||||
},
|
||||
User::SignedIn(user) => {
|
||||
|
@ -305,14 +305,14 @@ async fn handle_login<UserData: Serialize + DeserializeOwned + Default>(request:
|
|||
Err(e) => return Err(e.into()),
|
||||
}
|
||||
} else {
|
||||
Response::input_lossy("Please enter your password")
|
||||
Response::sensitive_input("Please enter your password")
|
||||
}
|
||||
} else if let Some(username) = request.input() {
|
||||
Response::redirect_temporary_lossy(
|
||||
Response::redirect_temporary(
|
||||
format!("/account/login/{}", username).as_str()
|
||||
)
|
||||
} else {
|
||||
Response::input_lossy("Please enter your username")
|
||||
Response::input("Please enter your username")
|
||||
}
|
||||
},
|
||||
User::SignedIn(user) => {
|
||||
|
@ -336,7 +336,7 @@ async fn handle_password<UserData: Serialize + DeserializeOwned + Default>(reque
|
|||
user.set_password(password)?;
|
||||
Response::success_gemini(include_str!("pages/password/success.gmi"))
|
||||
} else {
|
||||
Response::input(
|
||||
Response::sensitive_input(
|
||||
format!("Please enter a {}password",
|
||||
if user.has_password() {
|
||||
"new "
|
||||
|
@ -344,7 +344,7 @@ async fn handle_password<UserData: Serialize + DeserializeOwned + Default>(reque
|
|||
""
|
||||
}
|
||||
)
|
||||
)?
|
||||
)
|
||||
}
|
||||
},
|
||||
})
|
||||
|
|
36
src/util.rs
36
src/util.rs
|
@ -14,7 +14,7 @@ use std::future::Future;
|
|||
use tokio::time;
|
||||
|
||||
#[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 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() {
|
||||
std::io::ErrorKind::PermissionDenied => {
|
||||
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!()),
|
||||
}
|
||||
};
|
||||
|
||||
Ok(Response::success(mime, file))
|
||||
Response::success(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]) -> Response {
|
||||
debug!("Dir: {}", dir.as_ref().display());
|
||||
let dir = dir.as_ref();
|
||||
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() {
|
||||
std::io::ErrorKind::NotFound => {
|
||||
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 => {
|
||||
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!()),
|
||||
}
|
||||
|
@ -61,12 +61,12 @@ pub async fn serve_dir<D: AsRef<Path>, P: AsRef<Path>>(dir: D, virtual_path: &[P
|
|||
Ok(dir) => dir,
|
||||
Err(e) => {
|
||||
match e.kind() {
|
||||
std::io::ErrorKind::NotFound => return Ok(Response::not_found()),
|
||||
std::io::ErrorKind::NotFound => return Response::not_found(),
|
||||
std::io::ErrorKind::PermissionDenied => {
|
||||
// Runs when asked to serve a file in a restricted dir
|
||||
// i.e. not /noaccess, but /noaccess/file
|
||||
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!()),
|
||||
}
|
||||
|
@ -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) {
|
||||
return Ok(Response::not_found());
|
||||
return Response::not_found();
|
||||
}
|
||||
|
||||
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")]
|
||||
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 {
|
||||
Ok(dir) => dir,
|
||||
Err(err) => match err.kind() {
|
||||
io::ErrorKind::NotFound => return Ok(Response::not_found()),
|
||||
io::ErrorKind::NotFound => return Response::not_found(),
|
||||
std::io::ErrorKind::PermissionDenied => {
|
||||
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!()),
|
||||
}
|
||||
|
@ -109,12 +109,10 @@ async fn serve_dir_listing<P: AsRef<Path>, B: AsRef<Path>>(path: P, virtual_path
|
|||
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 = file_name.to_string_lossy();
|
||||
let is_dir = entry.file_type().await
|
||||
.with_context(|| format!("Failed to get file type of `{}`", entry.path().display()))?
|
||||
.is_dir();
|
||||
let is_dir = entry.file_type().await.unwrap().is_dir();
|
||||
let trailing_slash = if is_dir { "/" } else { "" };
|
||||
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")]
|
||||
|
@ -146,7 +144,7 @@ pub fn guess_mime_from_path<P: AsRef<Path>>(path: P) -> &'static str {
|
|||
|
||||
#[cfg(feature="serve_dir")]
|
||||
/// 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!(
|
||||
concat!(
|
||||
"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,
|
||||
err
|
||||
);
|
||||
Response::server_error("Unexpected error")
|
||||
Response::temporary_failure("Unexpected error")
|
||||
}
|
||||
|
||||
/// A convenience trait alias for `AsRef<T> + Into<T::Owned>`,
|
||||
|
|
Loading…
Reference in a new issue