mirror of
https://github.com/element-hq/matrix-authentication-service.git
synced 2026-06-07 08:52:26 +00:00
Simple error middleware
This commit is contained in:
Generated
+7
@@ -1255,6 +1255,7 @@ dependencies = [
|
||||
"csrf",
|
||||
"data-encoding",
|
||||
"figment",
|
||||
"mime",
|
||||
"oauth2-types",
|
||||
"serde",
|
||||
"tera",
|
||||
@@ -1273,6 +1274,12 @@ version = "2.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b16bd47d9e329435e309c58469fe0791c2d0d1ba96ec0954152a5ae2b04387dc"
|
||||
|
||||
[[package]]
|
||||
name = "mime"
|
||||
version = "0.3.16"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d"
|
||||
|
||||
[[package]]
|
||||
name = "num-integer"
|
||||
version = "0.1.44"
|
||||
|
||||
@@ -21,3 +21,4 @@ csrf = "0.4.0"
|
||||
data-encoding = "2.3.2"
|
||||
time = "0.2.27"
|
||||
tide-tracing = "0.0.11"
|
||||
mime = "0.3.16"
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
use std::convert::TryInto;
|
||||
|
||||
use async_trait::async_trait;
|
||||
use serde::Deserialize;
|
||||
use thiserror::Error;
|
||||
@@ -102,6 +100,7 @@ pub fn install(app: &mut Server<State>) {
|
||||
let mut views = tide::with_state(state.clone());
|
||||
views.with(state.session_middleware());
|
||||
views.with(crate::middlewares::csrf);
|
||||
views.with(crate::middlewares::errors);
|
||||
views.at("/").get(self::views::index::get);
|
||||
views
|
||||
.at("/login")
|
||||
|
||||
@@ -0,0 +1,184 @@
|
||||
use std::cmp::Reverse;
|
||||
use std::future::Future;
|
||||
use std::pin::Pin;
|
||||
|
||||
use mime::{Mime, STAR};
|
||||
use serde::Serialize;
|
||||
use tera::Context;
|
||||
use tide::{
|
||||
http::headers::{ACCEPT, LOCATION},
|
||||
Body, Request, StatusCode,
|
||||
};
|
||||
use tracing::debug;
|
||||
|
||||
use crate::state::State;
|
||||
use crate::templates::common_context;
|
||||
|
||||
/// Get the weight parameter for a mime type from 0 to 1000
|
||||
fn get_weight(mime: &Mime) -> usize {
|
||||
let q = mime
|
||||
.get_param("q")
|
||||
.map(|q| q.as_str().parse().unwrap_or(0.0))
|
||||
.unwrap_or(1.0_f64)
|
||||
.min(1.0)
|
||||
.max(0.0);
|
||||
|
||||
// Weight have a 3 digit precision so we can multiply by 1000 and cast to int
|
||||
(q * 1000.0) as _
|
||||
}
|
||||
|
||||
/// Find what content type should be used for a given request
|
||||
fn preferred_mime_type<'a>(
|
||||
request: &Request<State>,
|
||||
supported_types: &'a [Mime],
|
||||
) -> Option<&'a Mime> {
|
||||
let accept = request.header(ACCEPT)?;
|
||||
// Parse the Accept header as a list of mime types with their associated weight
|
||||
let accepted_types: Vec<(Mime, usize)> = {
|
||||
let v: Option<Vec<_>> = accept
|
||||
.into_iter()
|
||||
.map(|value| value.as_str().split(','))
|
||||
.flatten()
|
||||
.map(|mime| {
|
||||
mime.trim().parse().ok().map(|mime| {
|
||||
let q = get_weight(&mime);
|
||||
(mime, q)
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
let mut v = v?;
|
||||
v.sort_by_key(|(_, weight)| Reverse(*weight));
|
||||
v
|
||||
};
|
||||
|
||||
// For each supported content type, find out if it is accepted with what weight and specificity
|
||||
let mut types: Vec<_> = supported_types
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter_map(|(index, supported)| {
|
||||
accepted_types.iter().find_map(|(accepted, weight)| {
|
||||
if accepted.type_() == supported.type_()
|
||||
&& accepted.subtype() == supported.subtype()
|
||||
{
|
||||
// Accept: text/html
|
||||
Some((supported, *weight, 2_usize, index))
|
||||
} else if accepted.type_() == supported.type_() && accepted.subtype() == STAR {
|
||||
// Accept: text/*
|
||||
Some((supported, *weight, 1, index))
|
||||
} else if accepted.type_() == STAR && accepted.subtype() == STAR {
|
||||
// Accept: */*
|
||||
Some((supported, *weight, 0, index))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
|
||||
types.sort_by_key(|(_, weight, specificity, index)| {
|
||||
(Reverse(*weight), Reverse(*specificity), *index)
|
||||
});
|
||||
|
||||
types.first().map(|(mime, _, _, _)| *mime)
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct ErrorContext {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
code: Option<String>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
description: Option<String>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
details: Option<String>,
|
||||
}
|
||||
|
||||
impl ErrorContext {
|
||||
fn should_render(&self) -> bool {
|
||||
self.code.is_some() || self.description.is_some() || self.details.is_some()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn middleware<'a>(
|
||||
request: tide::Request<State>,
|
||||
next: tide::Next<'a, State>,
|
||||
) -> Pin<Box<dyn Future<Output = tide::Result> + Send + 'a>> {
|
||||
Box::pin(async {
|
||||
let content_type = preferred_mime_type(
|
||||
&request,
|
||||
&[mime::TEXT_PLAIN, mime::TEXT_HTML, mime::APPLICATION_JSON],
|
||||
);
|
||||
debug!("Content-Type from Accept: {:?}", content_type);
|
||||
|
||||
// TODO: We should not clone here
|
||||
let templates = request.state().templates().clone();
|
||||
|
||||
// TODO: This context should probably be comptuted somewhere else
|
||||
let pctx = common_context(&request).await?.clone();
|
||||
|
||||
let mut response = next.run(request).await;
|
||||
|
||||
// Find out what message should be displayed from the response status code
|
||||
let (code, description) = match response.status() {
|
||||
StatusCode::NotFound => (Some("Not found".to_string()), None),
|
||||
StatusCode::MethodNotAllowed => (Some("Method not allowed".to_string()), None),
|
||||
StatusCode::Found
|
||||
| StatusCode::PermanentRedirect
|
||||
| StatusCode::TemporaryRedirect
|
||||
| StatusCode::SeeOther => {
|
||||
let description = response.header(LOCATION).map(|loc| format!("To {}", loc));
|
||||
(Some("Redirecting".to_string()), description)
|
||||
}
|
||||
StatusCode::InternalServerError => (Some("Internal server error".to_string()), None),
|
||||
_ => (None, None),
|
||||
};
|
||||
|
||||
// If there is an error associated to the response, format it in a nice way with
|
||||
// a backtrace if we have one
|
||||
let details = response.take_error().map(|err| {
|
||||
format!(
|
||||
"{}{}",
|
||||
err,
|
||||
err.backtrace()
|
||||
.map(|bt| format!("\nBacktrace:\n{}", bt.to_string()))
|
||||
.unwrap_or_default()
|
||||
)
|
||||
});
|
||||
|
||||
let error_context = ErrorContext {
|
||||
code,
|
||||
description,
|
||||
details,
|
||||
};
|
||||
|
||||
// This is the case if one of the code, description or details is not None
|
||||
if error_context.should_render() {
|
||||
match content_type {
|
||||
Some(c) if c == &mime::APPLICATION_JSON => {
|
||||
response.set_body(Body::from_json(&error_context)?);
|
||||
response.set_content_type("application/json");
|
||||
}
|
||||
Some(c) if c == &mime::TEXT_HTML => {
|
||||
let mut ctx = Context::from_serialize(&error_context)?;
|
||||
ctx.extend(pctx);
|
||||
response.set_body(templates.render("error.html", &ctx)?);
|
||||
response.set_content_type("text/html");
|
||||
}
|
||||
Some(c) if c == &mime::TEXT_PLAIN => {
|
||||
let mut ctx = Context::from_serialize(&error_context)?;
|
||||
ctx.extend(pctx);
|
||||
response.set_body(templates.render("error.txt", &ctx)?);
|
||||
response.set_content_type("text/plain");
|
||||
}
|
||||
_ => {
|
||||
response.set_body("Unsupported Content-Type in Accept header");
|
||||
response.set_content_type("text/plain");
|
||||
response.set_status(StatusCode::NotAcceptable);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(response)
|
||||
})
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
mod csrf;
|
||||
mod errors;
|
||||
|
||||
pub use self::csrf::middleware as csrf;
|
||||
pub use self::errors::middleware as errors;
|
||||
|
||||
@@ -6,7 +6,7 @@ use tracing::info;
|
||||
use crate::state::State;
|
||||
|
||||
pub fn load() -> Result<Tera, tera::Error> {
|
||||
let path = format!("{}/templates/**/*.html", env!("CARGO_MANIFEST_DIR"));
|
||||
let path = format!("{}/templates/**/*.{{html,txt}}", env!("CARGO_MANIFEST_DIR"));
|
||||
info!(%path, "Loading templates");
|
||||
Tera::new(&path)
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@0.9.3/css/bulma.min.css">
|
||||
</head>
|
||||
<body>
|
||||
<nav class="navbar is-primary" role="navigation" aria-label="main navigation">
|
||||
<nav class="navbar is-dark" role="navigation" aria-label="main navigation">
|
||||
<div class="container">
|
||||
<div class="navbar-brand">
|
||||
<a class="navbar-item" href="/">
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<section class="hero is-danger">
|
||||
<div class="hero-body">
|
||||
<div class="container">
|
||||
{% if code %}
|
||||
<p class="title">
|
||||
{{ code }}
|
||||
</p>
|
||||
{% endif %}
|
||||
{% if description %}
|
||||
<p class="subtitle">
|
||||
{{ description }}
|
||||
</p>
|
||||
{% endif %}
|
||||
{% if details %}
|
||||
<pre><code>{{ details }}</code></pre>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,11 @@
|
||||
{% if code %}
|
||||
{{- code }}
|
||||
{% endif %}
|
||||
|
||||
{%- if description %}
|
||||
{{ description }}
|
||||
{% endif %}
|
||||
|
||||
{%- if details %}
|
||||
{{ details }}
|
||||
{% endif %}
|
||||
Reference in New Issue
Block a user