1
0
Fork 0

Merge branch 'stefan0xC-forward-to-admin-login'

Dieser Commit ist enthalten in:
Daniel García 2022-12-01 22:41:00 +01:00
Commit 59eaa0aa0d
Es konnte kein GPG-Schlüssel zu dieser Signatur gefunden werden
GPG-Schlüssel-ID: FC8A7D14C3CD543A
5 geänderte Dateien mit 57 neuen und 53 gelöschten Zeilen

Datei anzeigen

@ -6,10 +6,10 @@ use std::env;
use rocket::serde::json::Json; use rocket::serde::json::Json;
use rocket::{ use rocket::{
form::Form, form::Form,
http::{Cookie, CookieJar, SameSite, Status}, http::{Cookie, CookieJar, MediaType, SameSite, Status},
request::{self, FromRequest, Outcome, Request}, request::{FromRequest, Outcome, Request},
response::{content::RawHtml as Html, Redirect}, response::{content::RawHtml as Html, Redirect},
Route, Catcher, Route,
}; };
use crate::{ use crate::{
@ -31,7 +31,6 @@ pub fn routes() -> Vec<Route> {
} }
routes![ routes![
admin_login,
get_users_json, get_users_json,
get_user_json, get_user_json,
post_admin_login, post_admin_login,
@ -57,6 +56,14 @@ pub fn routes() -> Vec<Route> {
] ]
} }
pub fn catchers() -> Vec<Catcher> {
if !CONFIG.disable_admin_token() && !CONFIG.is_admin_token_set() {
catchers![]
} else {
catchers![admin_login]
}
}
static DB_TYPE: Lazy<&str> = Lazy::new(|| { static DB_TYPE: Lazy<&str> = Lazy::new(|| {
DbConnType::from_url(&CONFIG.database_url()) DbConnType::from_url(&CONFIG.database_url())
.map(|t| match t { .map(|t| match t {
@ -87,17 +94,6 @@ fn admin_path() -> String {
format!("{}{}", CONFIG.domain_path(), ADMIN_PATH) format!("{}{}", CONFIG.domain_path(), ADMIN_PATH)
} }
struct Referer(Option<String>);
#[rocket::async_trait]
impl<'r> FromRequest<'r> for Referer {
type Error = ();
async fn from_request(request: &'r Request<'_>) -> request::Outcome<Self, Self::Error> {
Outcome::Success(Referer(request.headers().get_one("Referer").map(str::to_string)))
}
}
#[derive(Debug)] #[derive(Debug)]
struct IpHeader(Option<String>); struct IpHeader(Option<String>);
@ -120,25 +116,8 @@ impl<'r> FromRequest<'r> for IpHeader {
} }
} }
/// Used for `Location` response headers, which must specify an absolute URI fn admin_url() -> String {
/// (see https://tools.ietf.org/html/rfc2616#section-14.30). format!("{}{}", CONFIG.domain_origin(), admin_path())
fn admin_url(referer: Referer) -> String {
// If we get a referer use that to make it work when, DOMAIN is not set
if let Some(mut referer) = referer.0 {
if let Some(start_index) = referer.find(ADMIN_PATH) {
referer.truncate(start_index + ADMIN_PATH.len());
return referer;
}
}
if CONFIG.domain_set() {
// Don't use CONFIG.domain() directly, since the user may want to keep a
// trailing slash there, particularly when running under a subpath.
format!("{}{}{}", CONFIG.domain_origin(), CONFIG.domain_path(), ADMIN_PATH)
} else {
// Last case, when no referer or domain set, technically invalid but better than nothing
ADMIN_PATH.to_string()
}
} }
#[derive(Responder)] #[derive(Responder)]
@ -151,18 +130,23 @@ enum AdminResponse {
TooManyRequests(ApiResult<Html<String>>), TooManyRequests(ApiResult<Html<String>>),
} }
#[get("/", rank = 2)] #[catch(401)]
fn admin_login() -> ApiResult<Html<String>> { fn admin_login(request: &Request<'_>) -> ApiResult<Html<String>> {
render_admin_login(None) if request.format() == Some(&MediaType::JSON) {
err_code!("Authorization failed.", Status::Unauthorized.code);
}
let redirect = request.segments::<std::path::PathBuf>(0..).unwrap_or_default().display().to_string();
render_admin_login(None, Some(redirect))
} }
fn render_admin_login(msg: Option<&str>) -> ApiResult<Html<String>> { fn render_admin_login(msg: Option<&str>, redirect: Option<String>) -> ApiResult<Html<String>> {
// If there is an error, show it // If there is an error, show it
let msg = msg.map(|msg| format!("Error: {msg}")); let msg = msg.map(|msg| format!("Error: {msg}"));
let json = json!({ let json = json!({
"page_content": "admin/login", "page_content": "admin/login",
"version": VERSION, "version": VERSION,
"error": msg, "error": msg,
"redirect": redirect,
"urlpath": CONFIG.domain_path() "urlpath": CONFIG.domain_path()
}); });
@ -174,20 +158,25 @@ fn render_admin_login(msg: Option<&str>) -> ApiResult<Html<String>> {
#[derive(FromForm)] #[derive(FromForm)]
struct LoginForm { struct LoginForm {
token: String, token: String,
redirect: Option<String>,
} }
#[post("/", data = "<data>")] #[post("/", data = "<data>")]
fn post_admin_login(data: Form<LoginForm>, cookies: &CookieJar<'_>, ip: ClientIp) -> AdminResponse { fn post_admin_login(data: Form<LoginForm>, cookies: &CookieJar<'_>, ip: ClientIp) -> Result<Redirect, AdminResponse> {
let data = data.into_inner(); let data = data.into_inner();
let redirect = data.redirect;
if crate::ratelimit::check_limit_admin(&ip.ip).is_err() { if crate::ratelimit::check_limit_admin(&ip.ip).is_err() {
return AdminResponse::TooManyRequests(render_admin_login(Some("Too many requests, try again later."))); return Err(AdminResponse::TooManyRequests(render_admin_login(
Some("Too many requests, try again later."),
redirect,
)));
} }
// If the token is invalid, redirect to login page // If the token is invalid, redirect to login page
if !_validate_token(&data.token) { if !_validate_token(&data.token) {
error!("Invalid admin token. IP: {}", ip.ip); error!("Invalid admin token. IP: {}", ip.ip);
AdminResponse::Unauthorized(render_admin_login(Some("Invalid admin token, please try again."))) Err(AdminResponse::Unauthorized(render_admin_login(Some("Invalid admin token, please try again."), redirect)))
} else { } else {
// If the token received is valid, generate JWT and save it as a cookie // If the token received is valid, generate JWT and save it as a cookie
let claims = generate_admin_claims(); let claims = generate_admin_claims();
@ -201,7 +190,11 @@ fn post_admin_login(data: Form<LoginForm>, cookies: &CookieJar<'_>, ip: ClientIp
.finish(); .finish();
cookies.add(cookie); cookies.add(cookie);
AdminResponse::Ok(render_admin_page()) if let Some(redirect) = redirect {
Ok(Redirect::to(format!("{}{}", admin_path(), redirect)))
} else {
Err(AdminResponse::Ok(render_admin_page()))
}
} }
} }
@ -258,7 +251,7 @@ fn render_admin_page() -> ApiResult<Html<String>> {
Ok(Html(text)) Ok(Html(text))
} }
#[get("/", rank = 1)] #[get("/")]
fn admin_page(_token: AdminToken) -> ApiResult<Html<String>> { fn admin_page(_token: AdminToken) -> ApiResult<Html<String>> {
render_admin_page() render_admin_page()
} }
@ -314,9 +307,9 @@ async fn test_smtp(data: Json<InviteData>, _token: AdminToken) -> EmptyResult {
} }
#[get("/logout")] #[get("/logout")]
fn logout(cookies: &CookieJar<'_>, referer: Referer) -> Redirect { fn logout(cookies: &CookieJar<'_>) -> Redirect {
cookies.remove(Cookie::build(COOKIE_NAME, "").path(admin_path()).finish()); cookies.remove(Cookie::build(COOKIE_NAME, "").path(admin_path()).finish());
Redirect::temporary(admin_url(referer)) Redirect::to(admin_path())
} }
#[get("/users")] #[get("/users")]
@ -639,7 +632,7 @@ async fn diagnostics(_token: AdminToken, ip_header: IpHeader, mut conn: DbConn)
"uses_proxy": uses_proxy, "uses_proxy": uses_proxy,
"db_type": *DB_TYPE, "db_type": *DB_TYPE,
"db_version": get_sql_server_version(&mut conn).await, "db_version": get_sql_server_version(&mut conn).await,
"admin_url": format!("{}/diagnostics", admin_url(Referer(None))), "admin_url": format!("{}/diagnostics", admin_url()),
"overrides": &CONFIG.get_overrides().join(", "), "overrides": &CONFIG.get_overrides().join(", "),
"server_time_local": Local::now().format("%Y-%m-%d %H:%M:%S %Z").to_string(), "server_time_local": Local::now().format("%Y-%m-%d %H:%M:%S %Z").to_string(),
"server_time": Utc::now().format("%Y-%m-%d %H:%M:%S UTC").to_string(), // Run the date/time check as the last item to minimize the difference "server_time": Utc::now().format("%Y-%m-%d %H:%M:%S UTC").to_string(), // Run the date/time check as the last item to minimize the difference
@ -681,15 +674,15 @@ pub struct AdminToken {}
impl<'r> FromRequest<'r> for AdminToken { impl<'r> FromRequest<'r> for AdminToken {
type Error = &'static str; type Error = &'static str;
async fn from_request(request: &'r Request<'_>) -> request::Outcome<Self, Self::Error> { async fn from_request(request: &'r Request<'_>) -> Outcome<Self, Self::Error> {
if CONFIG.disable_admin_token() { if CONFIG.disable_admin_token() {
Outcome::Success(AdminToken {}) Outcome::Success(Self {})
} else { } else {
let cookies = request.cookies(); let cookies = request.cookies();
let access_token = match cookies.get(COOKIE_NAME) { let access_token = match cookies.get(COOKIE_NAME) {
Some(cookie) => cookie.value(), Some(cookie) => cookie.value(),
None => return Outcome::Forward(()), // If there is no cookie, redirect to login None => return Outcome::Failure((Status::Unauthorized, "Unauthorized")),
}; };
let ip = match ClientIp::from_request(request).await { let ip = match ClientIp::from_request(request).await {
@ -701,10 +694,10 @@ impl<'r> FromRequest<'r> for AdminToken {
// Remove admin cookie // Remove admin cookie
cookies.remove(Cookie::build(COOKIE_NAME, "").path(admin_path()).finish()); cookies.remove(Cookie::build(COOKIE_NAME, "").path(admin_path()).finish());
error!("Invalid or expired admin JWT. IP: {}.", ip); error!("Invalid or expired admin JWT. IP: {}.", ip);
return Outcome::Forward(()); return Outcome::Failure((Status::Unauthorized, "Session expired"));
} }
Outcome::Success(AdminToken {}) Outcome::Success(Self {})
} }
} }
} }

Datei anzeigen

@ -9,6 +9,7 @@ use rocket::serde::json::Json;
use serde_json::Value; use serde_json::Value;
pub use crate::api::{ pub use crate::api::{
admin::catchers as admin_catchers,
admin::routes as admin_routes, admin::routes as admin_routes,
core::catchers as core_catchers, core::catchers as core_catchers,
core::purge_sends, core::purge_sends,

Datei anzeigen

@ -436,6 +436,7 @@ async fn launch_rocket(pool: db::DbPool, extra_debug: bool) -> Result<(), Error>
.mount([basepath, "/notifications"].concat(), api::notifications_routes()) .mount([basepath, "/notifications"].concat(), api::notifications_routes())
.register([basepath, "/"].concat(), api::web_catchers()) .register([basepath, "/"].concat(), api::web_catchers())
.register([basepath, "/api"].concat(), api::core_catchers()) .register([basepath, "/api"].concat(), api::core_catchers())
.register([basepath, "/admin"].concat(), api::admin_catchers())
.manage(pool) .manage(pool)
.manage(api::start_notification_server()) .manage(api::start_notification_server())
.attach(util::AppHeaders()) .attach(util::AppHeaders())

Datei anzeigen

@ -352,7 +352,13 @@
supportString += "* Reverse proxy and version: \n"; supportString += "* Reverse proxy and version: \n";
supportString += "* Other relevant information: \n"; supportString += "* Other relevant information: \n";
let jsonResponse = await fetch('{{urlpath}}/admin/diagnostics/config'); let jsonResponse = await fetch('{{urlpath}}/admin/diagnostics/config', {
'headers': { 'Accept': 'application/json' }
});
if (!jsonResponse.ok) {
alert("Generation failed: " + jsonResponse.statusText);
throw new Error(jsonResponse);
}
const configJson = await jsonResponse.json(); const configJson = await jsonResponse.json();
supportString += "\n### Config (Generated via diagnostics page)\n<details><summary>Show Running Config</summary>\n" supportString += "\n### Config (Generated via diagnostics page)\n<details><summary>Show Running Config</summary>\n"
supportString += "\n**Environment settings which are overridden:** {{page_data.overrides}}\n" supportString += "\n**Environment settings which are overridden:** {{page_data.overrides}}\n"

Datei anzeigen

@ -12,8 +12,11 @@
<h6 class="mb-0 text-white">Authentication key needed to continue</h6> <h6 class="mb-0 text-white">Authentication key needed to continue</h6>
<small>Please provide it below:</small> <small>Please provide it below:</small>
<form class="form-inline" method="post"> <form class="form-inline" method="post" action="{{urlpath}}/admin">
<input type="password" class="form-control w-50 mr-2" name="token" placeholder="Enter admin token" autofocus="autofocus"> <input type="password" class="form-control w-50 mr-2" name="token" placeholder="Enter admin token" autofocus="autofocus">
{{#if redirect}}
<input type="hidden" id="redirect" name="redirect" value="/{{redirect}}">
{{/if}}
<button type="submit" class="btn btn-primary">Enter</button> <button type="submit" class="btn btn-primary">Enter</button>
</form> </form>
</div> </div>