Skip to content

Commit

Permalink
Catch secure-index anywhere in the subdirectories
Browse files Browse the repository at this point in the history
Store secure-index in user's session info
Create custom responses for not found and restricted
Add todo sections
  • Loading branch information
dormant-user committed Mar 12, 2024
1 parent 825c2a7 commit 5ea21e1
Show file tree
Hide file tree
Showing 13 changed files with 113 additions and 48 deletions.
2 changes: 2 additions & 0 deletions src/constant.rs
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ pub fn build_info() -> Cargo {
pub struct Session {
pub tracker: Mutex<HashMap<String, String>>,
pub mapping: Mutex<HashMap<String, String>>,
pub secured_dir: Mutex<HashMap<String, String>>
}


Expand All @@ -77,6 +78,7 @@ pub fn session_info() -> Arc<Session> {
Arc::new(Session {
tracker: Mutex::new(HashMap::new()),
mapping: Mutex::new(HashMap::new()),
secured_dir: Mutex::new(HashMap::new())
})
}

Expand Down
34 changes: 29 additions & 5 deletions src/routes/auth.rs
Original file line number Diff line number Diff line change
Expand Up @@ -164,13 +164,32 @@ pub async fn home(request: HttpRequest,
let listing_page = squire::content::get_all_stream_content(&config, &auth_response);
let listing = template.get_template("listing").unwrap();

// Can do a one-liner like below, but it will be ugly
//
// let secure_dir = listing_page.secured_directories
// .first().unwrap_or(&HashMap::from([("".to_string(), "".to_string())]))
// .get("path").unwrap_or(&"home".to_string())

let secure_dir = if listing_page.secured_directories.len() == 1 {
let dir = listing_page.secured_directories.first().unwrap().get("path").unwrap();
log::debug!("Secure Directory: {}", &dir);
dir
} else {
log::debug!("Secure Directory unknown/multiple");
"home"
};
let mut stored_secure_dir = session.secured_dir.lock().unwrap();
if !stored_secure_dir.is_empty() {
stored_secure_dir.remove("href");
}
stored_secure_dir.insert("href".to_string(), secure_dir.to_string());
HttpResponse::build(StatusCode::OK)
.content_type("text/html; charset=utf-8")
.body(
listing.render(minijinja::context!(
files => listing_page.files,
user => auth_response.username,
secure_index => constant::SECURE_INDEX,
secure_index => secure_dir,
directories => listing_page.directories,
secured_directories => listing_page.secured_directories
)).unwrap()
Expand All @@ -193,16 +212,21 @@ pub async fn error(template: web::Data<Arc<minijinja::Environment<'static>>>,
if let Some(detail) = request.cookie("detail") {
log::info!("Error response for /error: {}", detail.value());
let session = template.get_template("session").unwrap();
return HttpResponse::build(StatusCode::OK)
return HttpResponse::build(StatusCode::UNAUTHORIZED)
.content_type("text/html; charset=utf-8")
.body(session.render(minijinja::context!(reason => detail.value())).unwrap());
}

log::info!("Sending unauthorized response for /error");
let unauthorized = template.get_template("unauthorized").unwrap();
HttpResponse::build(StatusCode::OK)
let error = template.get_template("error").unwrap();
HttpResponse::build(StatusCode::UNAUTHORIZED)
.content_type("text/html; charset=utf-8")
.body(unauthorized.render(minijinja::context!()).unwrap()) // no arguments to render
.body(error.render(minijinja::context!(
title => "LOGIN FAILED",
description => "USER ERROR - REPLACE USER",
help => r"Forgot Password?\n\nRelax and try to remember your password.",
button_text => "LOGIN", button_link => "/"
)).unwrap())
}

/// Constructs an `HttpResponse` for failed `session_token` verification.
Expand Down
51 changes: 29 additions & 22 deletions src/routes/media.rs
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ fn subtitles(true_path: PathBuf, relative_path: &String) -> Subtitles {
/// * `fernet` - Fernet object to encrypt the auth payload that will be set as `session_token` cookie.
/// * `session` - Session struct that holds the `session_mapping` and `session_tracker` to handle sessions.
/// * `config` - Configuration data for the application.
/// * `template` - Configuration container for the loaded templates.
///
/// # Returns
///
Expand All @@ -85,15 +86,17 @@ pub async fn track(request: HttpRequest,
info: web::Query<Payload>,
fernet: web::Data<Arc<Fernet>>,
session: web::Data<Arc<constant::Session>>,
config: web::Data<Arc<squire::settings::Config>>) -> HttpResponse {
config: web::Data<Arc<squire::settings::Config>>,
template: web::Data<Arc<minijinja::Environment<'static>>>) -> HttpResponse {
let auth_response = squire::authenticator::verify_token(&request, &config, &fernet, &session);
if !auth_response.ok {
return routes::auth::failed_auth(auth_response, &config);
}
if !squire::authenticator::verify_secure_index(&PathBuf::from(&info.file), &auth_response.username) {
return HttpResponse::Unauthorized().json(routes::auth::DetailError {
detail: format!("This content is not accessible as it does not belong to the user profile '{}'", auth_response.username)
})
return squire::responses::restricted(
template.get_template("error").unwrap(),
&auth_response.username
)
}
squire::logger::log_connection(&request, &session);
log::debug!("{}", auth_response.detail);
Expand All @@ -104,9 +107,8 @@ pub async fn track(request: HttpRequest,
Ok(content) => HttpResponse::Ok()
.content_type("text/plain")
.body(content),
Err(_) => HttpResponse::NotFound().json(routes::auth::DetailError {
detail: format!("'{}' was not found", &info.file)
})
Err(_) => squire::responses::not_found(template.get_template("error").unwrap(),
&format!("'{}' was not found", &info.file))
}
}

Expand Down Expand Up @@ -159,16 +161,16 @@ pub async fn stream(request: HttpRequest,
log::debug!("{}", auth_response.detail);
let filepath = media_path.to_string();
if !squire::authenticator::verify_secure_index(&PathBuf::from(&filepath), &auth_response.username) {
return HttpResponse::Unauthorized().json(routes::auth::DetailError {
detail: format!("This content is not accessible as it does not belong to the user profile '{}'", auth_response.username)
})
return squire::responses::restricted(
template.get_template("error").unwrap(),
&auth_response.username
)
}
// True path of the media file
let __target = config.media_source.join(&filepath);
if !__target.exists() {
return HttpResponse::NotFound().json(routes::auth::DetailError {
detail: format!("'{}' was not found", filepath)
});
return squire::responses::not_found(template.get_template("error").unwrap(),
&format!("'{}' was not found", filepath));
}
// True path of the media file as a String
let __target_str = __target.to_string_lossy().to_string();
Expand Down Expand Up @@ -234,7 +236,7 @@ pub async fn stream(request: HttpRequest,
custom_title => custom_title,
files => listing_page.files,
user => auth_response.username,
secure_index => constant::SECURE_INDEX,
secure_index => session.secured_dir.lock().unwrap().get("href").unwrap_or(&"home".to_string()),
directories => listing_page.directories,
secured_directories => listing_page.secured_directories
)).unwrap());
Expand All @@ -253,26 +255,31 @@ pub async fn stream(request: HttpRequest,
///
/// * `request` - A reference to the Actix web `HttpRequest` object.
/// * `info` - The query parameter containing the file information.
/// * `config` - Configuration data for the application.
/// * `fernet` - Fernet object to encrypt the auth payload that will be set as `session_token` cookie.
/// * `session` - Session struct that holds the `session_mapping` and `session_tracker` to handle sessions.
/// * `config` - Configuration data for the application.
/// * `template` - Configuration container for the loaded templates.
///
/// # Returns
///
/// Returns an `HttpResponse` containing the media content or an error response.
#[get("/media")]
pub async fn streaming_endpoint(request: HttpRequest, info: web::Query<Payload>,
config: web::Data<Arc<squire::settings::Config>>,
pub async fn streaming_endpoint(request: HttpRequest,
info: web::Query<Payload>,
fernet: web::Data<Arc<Fernet>>,
session: web::Data<Arc<constant::Session>>) -> HttpResponse {
session: web::Data<Arc<constant::Session>>,
config: web::Data<Arc<squire::settings::Config>>,
template: web::Data<Arc<minijinja::Environment<'static>>>) -> HttpResponse {
let auth_response = squire::authenticator::verify_token(&request, &config, &fernet, &session);
if !auth_response.ok {
return routes::auth::failed_auth(auth_response, &config);
}
let media_path = config.media_source.join(&info.file);
if !squire::authenticator::verify_secure_index(&media_path, &auth_response.username) {
return HttpResponse::Unauthorized().json(routes::auth::DetailError {
detail: format!("This content is not accessible as it does not belong to the user profile '{}'", auth_response.username)
})
return squire::responses::restricted(
template.get_template("error").unwrap(),
&auth_response.username
)
}
squire::logger::log_connection(&request, &session);
let host = request.connection_info().host().to_owned();
Expand All @@ -288,5 +295,5 @@ pub async fn streaming_endpoint(request: HttpRequest, info: web::Query<Payload>,
}
let error = format!("File {:?} not found", media_path);
log::error!("{}", error);
HttpResponse::NotFound().body(error)
squire::responses::not_found(template.get_template("error").unwrap(), &error)
}
4 changes: 3 additions & 1 deletion src/routes/upload.rs
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,8 @@ pub async fn upload_files(request: HttpRequest,
}
let upload_path = config.media_source.join(format!("{}_{}", &auth_response.username, constant::SECURE_INDEX));
if !upload_path.exists() {
// todo: either remove this and use session's secure_dir (but that will work if only one is available) - convenient regardless of user BS
// or totally remove the session specific secure_dir logic and create one for each user upon login - more precise but may lead to multiple
match std::fs::create_dir(&upload_path) {
Ok(_) => log::info!("'{}' has been created", &upload_path.to_str().unwrap()),
Err(err) => log::error!("{}", err)
Expand All @@ -111,6 +113,6 @@ pub async fn upload_files(request: HttpRequest,
.content_type("text/html; charset=utf-8")
.body(landing.render(minijinja::context!(
user => auth_response.username,
secure_index => constant::SECURE_INDEX
secure_index => session.secured_dir.lock().unwrap().get("href").unwrap_or(&"home".to_string())
)).unwrap())
}
4 changes: 2 additions & 2 deletions src/squire/authenticator.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use std::collections::HashMap;
use std::path::PathBuf;
use std::path::Path;
use std::sync::Arc;

use actix_web::{HttpRequest, web};
Expand Down Expand Up @@ -213,7 +213,7 @@ pub fn verify_token(
/// # Returns
///
/// Returns a boolean value to indicate if the access can be granted.
pub fn verify_secure_index(path: &PathBuf, username: &String) -> bool {
pub fn verify_secure_index(path: &Path, username: &String) -> bool {
for dir in path.iter() {
let child = dir.to_string_lossy().to_string();
if child.ends_with(constant::SECURE_INDEX) && child != format!("{}_{}", username, constant::SECURE_INDEX) {
Expand Down
17 changes: 9 additions & 8 deletions src/squire/content.rs
Original file line number Diff line number Diff line change
Expand Up @@ -101,14 +101,15 @@ fn get_folder_font(structure: &Path,
let mut entry_map = HashMap::new();
entry_map.insert("path".to_string(), format!("stream/{}", &directory));
let depth = &structure.iter().count();
if let Some(first_component) = &structure.iter().next() {
for component in structure.iter() {
let secured = format!("{}_{}", &auth_response.username, constant::SECURE_INDEX);
if secured == first_component.to_string_lossy() {
entry_map.insert("name".to_string(), auth_response.username.to_owned());
if secured == component.to_string_lossy() {
entry_map.insert("name".to_string(), directory);
entry_map.insert("font".to_string(), "fa-solid fa-lock".to_string());
entry_map.insert("secured".to_string(), "true".to_string());
return entry_map;
} else if first_component.to_string_lossy().ends_with(constant::SECURE_INDEX) {
// Return an empty hashmap if the
} else if component.to_string_lossy().ends_with(constant::SECURE_INDEX) {
// If the path has secure index value (includes folder trees / subdirectories)
return HashMap::new();
}
}
Expand Down Expand Up @@ -164,11 +165,11 @@ pub fn get_all_stream_content(config: &settings::Config, auth_response: &authent
.collect::<Vec<_>>().iter().rev()
.collect::<PathBuf>();
let entry_map = get_folder_font(&skimmed, auth_response);
if payload.directories.contains(&entry_map) || entry_map.is_empty() { continue; }
if payload.secured_directories.contains(&entry_map) || entry_map.is_empty() { continue; }
if entry_map.get("font").unwrap_or(&"".to_string()) == "fa-solid fa-lock" {
if entry_map.get("secured").unwrap_or(&"".to_string()) == "true" {
if payload.secured_directories.contains(&entry_map) || entry_map.is_empty() { continue; }
payload.secured_directories.push(entry_map);
} else {
if payload.directories.contains(&entry_map) || entry_map.is_empty() { continue; }
payload.directories.push(entry_map);
}
}
Expand Down
2 changes: 2 additions & 0 deletions src/squire/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,5 @@ pub mod content;
pub mod authenticator;
/// Module that handles parsing command line arguments.
pub mod parser;
/// Module that handles custom error responses to the user.
pub mod responses;
27 changes: 27 additions & 0 deletions src/squire/responses.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
use actix_web::http::StatusCode;
use actix_web::HttpResponse;
use minijinja::Template;

// todo: write docstrings

pub fn not_found(error: Template, description: &String) -> HttpResponse {
HttpResponse::build(StatusCode::NOT_FOUND)
.content_type("text/html; charset=utf-8")
.body(error.render(minijinja::context!(
title => "CONTENT UNAVAILABLE",
description => description,
help => r"Lost your way?\n\nHit the HOME button to navigate back to home page.",
button_text => "HOME", button_link => "/home"
)).unwrap())
}

pub fn restricted(error: Template, username: &String) -> HttpResponse {
HttpResponse::build(StatusCode::UNAUTHORIZED)
.content_type("text/html; charset=utf-8")
.body(error.render(minijinja::context!(
title => "RESTRICTED SECTION",
description => format!("This content is not accessible, as it does not belong to the user profile '{}'", username),
help => r"Lost your way?\n\nHit the HOME button to navigate back to home page.",
button_text => "HOME", button_link => "/home"
)).unwrap())
}
8 changes: 4 additions & 4 deletions src/templates/unauthorized.rs → src/templates/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -67,16 +67,16 @@ pub fn get_content() -> String {
</noscript>
</head>
<body>
<h2 style="margin-top:5%">LOGIN FAILED</h2>
<h3>USER ERROR - REPLACE USER</h3>
<h2 style="margin-top:5%">{{ title }}</h2>
<h3>{{ description }}</h3>
<p>
<img src="https://thevickypedia.github.io/open-source/images/gif/lockscape.gif"
onerror="this.src='https://vigneshrao.com/open-source/images/gif/lockscape.gif'"
width="200" height="170" alt="Image" class="center">
</p>
<button style="text-align:center" onClick="window.location.href = '/';">LOGIN</button>
<button style="text-align:center" onClick="window.location.href = '{{ button_link }}';">{{ button_text }}</button>
<br>
<button style="text-align:center" onClick="alert('Forgot Password?\n\nRelax and try to remember your password.');">HELP
<button style="text-align:center" onClick="alert('{{ help }}');">HELP
</button>
<h4>Click <a href="https://vigneshrao.com/contact">HERE</a> to reach out.</h4>
</body>
Expand Down
2 changes: 1 addition & 1 deletion src/templates/landing.rs
Original file line number Diff line number Diff line change
Expand Up @@ -284,7 +284,7 @@ pub fn get_content() -> String {
window.location.href = window.location.origin + "/home";
}
function goSecure() {
window.location.href = window.location.origin + '/stream/{{ user }}_{{ secure_index }}';
window.location.href = window.location.origin + '/{{ secure_index }}';
}
function logOut() {
window.location.href = window.location.origin + "/logout";
Expand Down
2 changes: 1 addition & 1 deletion src/templates/listing.rs
Original file line number Diff line number Diff line change
Expand Up @@ -199,7 +199,7 @@ pub fn get_content() -> String {
window.location.href = window.location.origin + "/home";
}
function goSecure() {
window.location.href = window.location.origin + '/stream/{{ user }}_{{ secure_index }}';
window.location.href = window.location.origin + '/{{ secure_index }}';
}
function logOut() {
window.location.href = window.location.origin + "/logout";
Expand Down
6 changes: 3 additions & 3 deletions src/templates/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ mod listing;
mod logout;
/// Session page template that is served as HTML response when invalid/expired session tokens are received.
mod session;
/// Unauthorized page template that is served as HTML response after failed authentication.
mod unauthorized;
/// Error page template that is served as HTML response for any error message to be conveyed.
mod error;
mod upload;

/// Loads all the HTML templates' content into a Jinja Environment
Expand All @@ -27,7 +27,7 @@ pub fn environment() -> Arc<minijinja::Environment<'static>> {
env.add_template_owned("listing", listing::get_content()).unwrap();
env.add_template_owned("logout", logout::get_content()).unwrap();
env.add_template_owned("session", session::get_content()).unwrap();
env.add_template_owned("unauthorized", unauthorized::get_content()).unwrap();
env.add_template_owned("error", error::get_content()).unwrap();
env.add_template_owned("upload", upload::get_content()).unwrap();
Arc::new(env)
}
2 changes: 1 addition & 1 deletion src/templates/upload.rs
Original file line number Diff line number Diff line change
Expand Up @@ -489,7 +489,7 @@ pub fn get_content() -> String {
window.location.href = window.location.origin + "/home";
}
function goSecure() {
window.location.href = window.location.origin + '/stream/{{ user }}_{{ secure_index }}';
window.location.href = window.location.origin + '/{{ secure_index }}';
}
function logOut() {
window.location.href = window.location.origin + "/logout";
Expand Down

0 comments on commit 5ea21e1

Please sign in to comment.