app: make it possible to serve lldap behind a sub-path

This commit is contained in:
MinerSebas
2023-12-05 10:24:25 +01:00
committed by nitnelave
parent ec0737c58a
commit 70d85524db
6 changed files with 88 additions and 35 deletions
+3 -2
View File
@@ -4,7 +4,8 @@
<head> <head>
<meta charset="utf-8" /> <meta charset="utf-8" />
<title>LLDAP Administration</title> <title>LLDAP Administration</title>
<script src="/static/main.js" type="module" defer></script> <base href="/">
<script src="static/main.js" type="module" defer></script>
<link <link
href="https://cdn.jsdelivr.net/npm/bootstrap-dark-5@1.1.3/dist/css/bootstrap-nightshade.min.css" href="https://cdn.jsdelivr.net/npm/bootstrap-dark-5@1.1.3/dist/css/bootstrap-nightshade.min.css"
rel="preload stylesheet" rel="preload stylesheet"
@@ -33,7 +34,7 @@
href="https://fonts.googleapis.com/css2?family=Bebas+Neue&display=swap" /> href="https://fonts.googleapis.com/css2?family=Bebas+Neue&display=swap" />
<link <link
rel="stylesheet" rel="stylesheet"
href="/static/style.css" /> href="static/style.css" />
<script> <script>
function inDarkMode(){ function inDarkMode(){
return darkmode.inDarkMode; return darkmode.inDarkMode;
+1 -1
View File
@@ -268,7 +268,7 @@ impl App {
<header class="p-2 mb-3 border-bottom"> <header class="p-2 mb-3 border-bottom">
<div class="container"> <div class="container">
<div class="d-flex flex-wrap align-items-center justify-content-center justify-content-lg-start"> <div class="d-flex flex-wrap align-items-center justify-content-center justify-content-lg-start">
<a href="/" class="d-flex align-items-center mt-2 mb-lg-0 me-md-5 text-decoration-none"> <a href={yew_router::utils::base_url().unwrap_or("/".to_string())} class="d-flex align-items-center mt-2 mb-lg-0 me-md-5 text-decoration-none">
<h2>{"LLDAP"}</h2> <h2>{"LLDAP"}</h2>
</a> </a>
+29 -17
View File
@@ -18,6 +18,10 @@ fn get_claims_from_jwt(jwt: &str) -> Result<JWTClaims> {
const NO_BODY: Option<()> = None; const NO_BODY: Option<()> = None;
fn base_url() -> String {
yew_router::utils::base_url().unwrap_or_default()
}
async fn call_server( async fn call_server(
url: &str, url: &str,
body: Option<impl Serialize>, body: Option<impl Serialize>,
@@ -97,7 +101,7 @@ impl HostService {
}; };
let request_body = QueryType::build_query(variables); let request_body = QueryType::build_query(variables);
call_server_json_with_error_message::<graphql_client::Response<_>, _>( call_server_json_with_error_message::<graphql_client::Response<_>, _>(
"/api/graphql", &(base_url() + "/api/graphql"),
Some(request_body), Some(request_body),
error_message, error_message,
) )
@@ -109,7 +113,7 @@ impl HostService {
request: login::ClientLoginStartRequest, request: login::ClientLoginStartRequest,
) -> Result<Box<login::ServerLoginStartResponse>> { ) -> Result<Box<login::ServerLoginStartResponse>> {
call_server_json_with_error_message( call_server_json_with_error_message(
"/auth/opaque/login/start", &(base_url() + "/auth/opaque/login/start"),
Some(request), Some(request),
"Could not start authentication: ", "Could not start authentication: ",
) )
@@ -118,7 +122,7 @@ impl HostService {
pub async fn login_finish(request: login::ClientLoginFinishRequest) -> Result<(String, bool)> { pub async fn login_finish(request: login::ClientLoginFinishRequest) -> Result<(String, bool)> {
call_server_json_with_error_message::<login::ServerLoginResponse, _>( call_server_json_with_error_message::<login::ServerLoginResponse, _>(
"/auth/opaque/login/finish", &(base_url() + "/auth/opaque/login/finish"),
Some(request), Some(request),
"Could not finish authentication", "Could not finish authentication",
) )
@@ -130,7 +134,7 @@ impl HostService {
request: registration::ClientRegistrationStartRequest, request: registration::ClientRegistrationStartRequest,
) -> Result<Box<registration::ServerRegistrationStartResponse>> { ) -> Result<Box<registration::ServerRegistrationStartResponse>> {
call_server_json_with_error_message( call_server_json_with_error_message(
"/auth/opaque/register/start", &(base_url() + "/auth/opaque/register/start"),
Some(request), Some(request),
"Could not start registration: ", "Could not start registration: ",
) )
@@ -141,7 +145,7 @@ impl HostService {
request: registration::ClientRegistrationFinishRequest, request: registration::ClientRegistrationFinishRequest,
) -> Result<()> { ) -> Result<()> {
call_server_empty_response_with_error_message( call_server_empty_response_with_error_message(
"/auth/opaque/register/finish", &(base_url() + "/auth/opaque/register/finish"),
Some(request), Some(request),
"Could not finish registration", "Could not finish registration",
) )
@@ -150,7 +154,7 @@ impl HostService {
pub async fn refresh() -> Result<(String, bool)> { pub async fn refresh() -> Result<(String, bool)> {
call_server_json_with_error_message::<login::ServerLoginResponse, _>( call_server_json_with_error_message::<login::ServerLoginResponse, _>(
"/auth/refresh", &(base_url() + "/auth/refresh"),
NO_BODY, NO_BODY,
"Could not start authentication: ", "Could not start authentication: ",
) )
@@ -160,13 +164,21 @@ impl HostService {
// The `_request` parameter is to make it the same shape as the other functions. // The `_request` parameter is to make it the same shape as the other functions.
pub async fn logout() -> Result<()> { pub async fn logout() -> Result<()> {
call_server_empty_response_with_error_message("/auth/logout", NO_BODY, "Could not logout") call_server_empty_response_with_error_message(
.await &(base_url() + "/auth/logout"),
NO_BODY,
"Could not logout",
)
.await
} }
pub async fn reset_password_step1(username: String) -> Result<()> { pub async fn reset_password_step1(username: String) -> Result<()> {
call_server_empty_response_with_error_message( call_server_empty_response_with_error_message(
&format!("/auth/reset/step1/{}", url_escape::encode_query(&username)), &format!(
"{}/auth/reset/step1/{}",
base_url(),
url_escape::encode_query(&username)
),
NO_BODY, NO_BODY,
"Could not initiate password reset", "Could not initiate password reset",
) )
@@ -177,7 +189,7 @@ impl HostService {
token: String, token: String,
) -> Result<lldap_auth::password_reset::ServerPasswordResetResponse> { ) -> Result<lldap_auth::password_reset::ServerPasswordResetResponse> {
call_server_json_with_error_message( call_server_json_with_error_message(
&format!("/auth/reset/step2/{}", token), &format!("{}/auth/reset/step2/{}", base_url(), token),
NO_BODY, NO_BODY,
"Could not validate token", "Could not validate token",
) )
@@ -185,13 +197,13 @@ impl HostService {
} }
pub async fn probe_password_reset() -> Result<bool> { pub async fn probe_password_reset() -> Result<bool> {
Ok( Ok(gloo_net::http::Request::get(
gloo_net::http::Request::get("/auth/reset/step1/lldap_unlikely_very_long_user_name") &(base_url() + "/auth/reset/step1/lldap_unlikely_very_long_user_name"),
.header("Content-Type", "application/json")
.send()
.await?
.status()
!= http::StatusCode::NOT_FOUND,
) )
.header("Content-Type", "application/json")
.send()
.await?
.status()
!= http::StatusCode::NOT_FOUND)
} }
} }
+3 -2
View File
@@ -22,10 +22,11 @@ pub fn set_cookie(cookie_name: &str, value: &str, expiration: &DateTime<Utc>) ->
.map_err(|_| anyhow!("Document is not an HTMLDocument")) .map_err(|_| anyhow!("Document is not an HTMLDocument"))
})?; })?;
let cookie_string = format!( let cookie_string = format!(
"{}={}; expires={}; sameSite=Strict; path=/", "{}={}; expires={}; sameSite=Strict; path={}/",
cookie_name, cookie_name,
value, value,
expiration.to_rfc2822() expiration.to_rfc2822(),
yew_router::utils::base_url().unwrap_or_default()
); );
doc.set_cookie(&cookie_string) doc.set_cookie(&cookie_string)
.map_err(|_| anyhow!("Could not set cookie")) .map_err(|_| anyhow!("Could not set cookie"))
+22 -7
View File
@@ -112,13 +112,17 @@ where
"Invalid refresh token".to_string(), "Invalid refresh token".to_string(),
))); )));
} }
let mut path = data.server_url.path().to_string();
if !path.ends_with('/') {
path.push('/');
};
let groups = data.get_readonly_handler().get_user_groups(&user).await?; let groups = data.get_readonly_handler().get_user_groups(&user).await?;
let token = create_jwt(data.get_tcp_handler(), jwt_key, &user, groups).await; let token = create_jwt(data.get_tcp_handler(), jwt_key, &user, groups).await;
Ok(HttpResponse::Ok() Ok(HttpResponse::Ok()
.cookie( .cookie(
Cookie::build("token", token.as_str()) Cookie::build("token", token.as_str())
.max_age(1.days()) .max_age(1.days())
.path("/") .path(&path)
.http_only(true) .http_only(true)
.same_site(SameSite::Strict) .same_site(SameSite::Strict)
.finish(), .finish(),
@@ -239,12 +243,16 @@ where
.await; .await;
let groups = HashSet::new(); let groups = HashSet::new();
let token = create_jwt(data.get_tcp_handler(), &data.jwt_key, &user_id, groups).await; let token = create_jwt(data.get_tcp_handler(), &data.jwt_key, &user_id, groups).await;
let mut path = data.server_url.path().to_string();
if !path.ends_with('/') {
path.push('/');
};
Ok(HttpResponse::Ok() Ok(HttpResponse::Ok()
.cookie( .cookie(
Cookie::build("token", token.as_str()) Cookie::build("token", token.as_str())
.max_age(5.minutes()) .max_age(5.minutes())
// Cookie is only valid to reset the password. // Cookie is only valid to reset the password.
.path("/auth") .path(format!("{}auth", path))
.http_only(true) .http_only(true)
.same_site(SameSite::Strict) .same_site(SameSite::Strict)
.finish(), .finish(),
@@ -284,11 +292,15 @@ where
for jwt_hash in new_blacklisted_jwt_hashes { for jwt_hash in new_blacklisted_jwt_hashes {
jwt_blacklist.insert(jwt_hash); jwt_blacklist.insert(jwt_hash);
} }
let mut path = data.server_url.path().to_string();
if !path.ends_with('/') {
path.push('/');
};
Ok(HttpResponse::Ok() Ok(HttpResponse::Ok()
.cookie( .cookie(
Cookie::build("token", "") Cookie::build("token", "")
.max_age(0.days()) .max_age(0.days())
.path("/") .path(&path)
.http_only(true) .http_only(true)
.same_site(SameSite::Strict) .same_site(SameSite::Strict)
.finish(), .finish(),
@@ -296,7 +308,7 @@ where
.cookie( .cookie(
Cookie::build("refresh_token", "") Cookie::build("refresh_token", "")
.max_age(0.days()) .max_age(0.days())
.path("/auth") .path(format!("{}auth", path))
.http_only(true) .http_only(true)
.same_site(SameSite::Strict) .same_site(SameSite::Strict)
.finish(), .finish(),
@@ -351,12 +363,15 @@ where
let (refresh_token, max_age) = data.get_tcp_handler().create_refresh_token(name).await?; let (refresh_token, max_age) = data.get_tcp_handler().create_refresh_token(name).await?;
let token = create_jwt(data.get_tcp_handler(), &data.jwt_key, name, groups).await; let token = create_jwt(data.get_tcp_handler(), &data.jwt_key, name, groups).await;
let refresh_token_plus_name = refresh_token + "+" + name.as_str(); let refresh_token_plus_name = refresh_token + "+" + name.as_str();
let mut path = data.server_url.path().to_string();
if !path.ends_with('/') {
path.push('/');
};
Ok(HttpResponse::Ok() Ok(HttpResponse::Ok()
.cookie( .cookie(
Cookie::build("token", token.as_str()) Cookie::build("token", token.as_str())
.max_age(1.days()) .max_age(1.days())
.path("/") .path(&path)
.http_only(true) .http_only(true)
.same_site(SameSite::Strict) .same_site(SameSite::Strict)
.finish(), .finish(),
@@ -364,7 +379,7 @@ where
.cookie( .cookie(
Cookie::build("refresh_token", refresh_token_plus_name.clone()) Cookie::build("refresh_token", refresh_token_plus_name.clone())
.max_age(max_age.num_days().days()) .max_age(max_age.num_days().days())
.path("/auth") .path(format!("{}auth", path))
.http_only(true) .http_only(true)
.same_site(SameSite::Strict) .same_site(SameSite::Strict)
.finish(), .finish(),
+30 -6
View File
@@ -12,7 +12,7 @@ use crate::{
tcp_backend_handler::*, tcp_backend_handler::*,
}, },
}; };
use actix_files::{Files, NamedFile}; use actix_files::Files;
use actix_http::{header, HttpServiceBuilder}; use actix_http::{header, HttpServiceBuilder};
use actix_server::ServerBuilder; use actix_server::ServerBuilder;
use actix_service::map_config; use actix_service::map_config;
@@ -21,13 +21,22 @@ use anyhow::{Context, Result};
use hmac::Hmac; use hmac::Hmac;
use sha2::Sha512; use sha2::Sha512;
use std::collections::HashSet; use std::collections::HashSet;
use std::path::PathBuf;
use std::sync::RwLock; use std::sync::RwLock;
use tracing::info; use tracing::info;
async fn index() -> actix_web::Result<NamedFile> { async fn index<Backend>(data: web::Data<AppState<Backend>>) -> actix_web::Result<impl Responder> {
let path = PathBuf::from(r"app/index.html"); let mut file = std::fs::read_to_string(r"./app/index.html")?;
Ok(NamedFile::open(path)?)
if data.server_url.path() != "/" {
file = file.replace(
"<base href=\"/\">",
format!("<base href=\"{}/\">", data.server_url.path()).as_str(),
);
}
Ok(file
.customize()
.insert_header((header::CONTENT_TYPE, "text/html; charset=utf-8")))
} }
#[derive(thiserror::Error, Debug)] #[derive(thiserror::Error, Debug)]
@@ -68,6 +77,20 @@ pub(crate) fn error_to_http_response(error: TcpError) -> HttpResponse {
.body(error.to_string()) .body(error.to_string())
} }
async fn main_js_handler<Backend>(
data: web::Data<AppState<Backend>>,
) -> actix_web::Result<impl Responder> {
let mut file = std::fs::read_to_string(r"./app/static/main.js")?;
if data.server_url.path() != "/" {
file = file.replace("/pkg/", format!("{}/pkg/", data.server_url.path()).as_str());
}
Ok(file
.customize()
.insert_header((header::CONTENT_TYPE, "text/javascript")))
}
async fn wasm_handler() -> actix_web::Result<impl Responder> { async fn wasm_handler() -> actix_web::Result<impl Responder> {
Ok(actix_files::NamedFile::open_async("./app/pkg/lldap_app_bg.wasm").await?) Ok(actix_files::NamedFile::open_async("./app/pkg/lldap_app_bg.wasm").await?)
} }
@@ -118,6 +141,7 @@ fn http_config<Backend>(
web::resource("/pkg/lldap_app_bg.wasm.gz").route(web::route().to(wasm_handler_compressed)), web::resource("/pkg/lldap_app_bg.wasm.gz").route(web::route().to(wasm_handler_compressed)),
) )
.service(web::resource("/pkg/lldap_app_bg.wasm").route(web::route().to(wasm_handler))) .service(web::resource("/pkg/lldap_app_bg.wasm").route(web::route().to(wasm_handler)))
.service(web::resource("/static/main.js").route(web::route().to(main_js_handler::<Backend>)))
// Serve the /pkg path with the compiled WASM app. // Serve the /pkg path with the compiled WASM app.
.service(Files::new("/pkg", "./app/pkg")) .service(Files::new("/pkg", "./app/pkg"))
// Serve static files // Serve static files
@@ -125,7 +149,7 @@ fn http_config<Backend>(
// Serve static fonts // Serve static fonts
.service(Files::new("/static/fonts", "./app/static/fonts")) .service(Files::new("/static/fonts", "./app/static/fonts"))
// Default to serve index.html for unknown routes, to support routing. // Default to serve index.html for unknown routes, to support routing.
.default_service(web::route().guard(guard::Get()).to(index)); .default_service(web::route().guard(guard::Get()).to(index::<Backend>));
} }
pub(crate) struct AppState<Backend> { pub(crate) struct AppState<Backend> {