|
@ -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 {}) |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|