From c8d3edfbbae4b7621a4ea282e99775088f3e0734 Mon Sep 17 00:00:00 2001 From: Fuwn Date: Wed, 18 Feb 2026 07:31:18 +0000 Subject: feat(response): Add input/sensitive-input handling --- Cargo.lock | 2 + Cargo.toml | 3 + src/response.rs | 170 +++++++++++++++++++++++++++++++++++++++++++++++++++++++- 3 files changed, 174 insertions(+), 1 deletion(-) diff --git a/Cargo.lock b/Cargo.lock index f262d97..533975b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2142,6 +2142,7 @@ dependencies = [ "germ", "log", "pretty_env_logger", + "serde", "tokio", "url", "vergen", @@ -2154,6 +2155,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" dependencies = [ "serde_core", + "serde_derive", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 9b828d1..9e90740 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -43,6 +43,9 @@ url = "2.5.4" # Markdown Encoding comrak = "0.29.0" +# Form Parsing +serde = { version = "1", features = ["derive"] } + [build-dependencies] # Compile-time Environment Variables vergen = { version = "8.3.2", features = ["git", "gitoxide"] } diff --git a/src/response.rs b/src/response.rs index 779bebc..9839934 100644 --- a/src/response.rs +++ b/src/response.rs @@ -11,9 +11,24 @@ use { const CSS: &str = include_str!("../default.css"); +#[derive(serde::Deserialize)] +pub struct InputSubmission { + input: String, + target: Option, +} + +fn html_escape(input: &str) -> String { + input + .replace('&', "&") + .replace('"', """) + .replace('<', "<") + .replace('>', ">") +} + #[allow(clippy::future_not_send, clippy::too_many_lines)] pub async fn default( http_request: actix_web::HttpRequest, + input_submission: Option>, ) -> Result { if ["/proxy", "/proxy/", "/x", "/x/", "/raw", "/raw/", "/nocss", "/nocss/"] .contains(&http_request.path()) @@ -28,7 +43,19 @@ pub async fn default( } let mut configuration = configuration::Configuration::new(); - let url = match url_from_path( + let submitted_input = + if *http_request.method() == actix_web::http::Method::POST { + input_submission.as_ref().map(|submission| submission.input.clone()) + } else { + None + }; + let submitted_target = + if *http_request.method() == actix_web::http::Method::POST { + input_submission.as_ref().and_then(|submission| submission.target.clone()) + } else { + None + }; + let mut url = match url_from_path( &format!("{}{}", http_request.path(), { if !http_request.query_string().is_empty() || http_request.uri().to_string().ends_with('?') @@ -50,6 +77,25 @@ pub async fn default( ); } }; + + if let Some(target) = submitted_target { + if let Ok(parsed_target) = url::Url::parse(&target) { + if parsed_target.scheme() == "gemini" { + url = parsed_target; + } + } + } + + if let Some(input) = submitted_input { + let input = input + .replace("\r\n", "\n") + .replace('\r', "\n") + .replace('\t', "%09") + .replace('\n', "%0A"); + + url.set_query(Some(&input)); + } + let mut timer = Instant::now(); let mut response = match germ::request::request(&url).await { Ok(response) => response, @@ -106,6 +152,128 @@ pub async fn default( } } + if *response.status() == germ::request::Status::Input + || *response.status() == germ::request::Status::SensitiveInput + { + if configuration.is_raw() { + return Ok( + HttpResponse::Ok() + .content_type(format!("text/plain; charset={charset}")) + .body(response.meta().to_string()), + ); + } + + let mut html_context = format!( + r#""#, + if language.is_empty() { + String::new() + } else { + format!(" lang=\"{language}\"") + } + ); + + if !configuration.is_no_css() { + if let Some(css) = &ENVIRONMENT.css_external { + for stylesheet in css.split(',').filter(|s| !s.is_empty()) { + let _ = write!( + &mut html_context, + "", + ); + } + } else { + let _ = write!( + &mut html_context, + r#""# + ); + + if let Some(primary) = &ENVIRONMENT.primary_colour { + let _ = write!( + &mut html_context, + "" + ); + } else { + let _ = write!( + &mut html_context, + "" + ); + } + } + } + + if let Some(favicon) = &ENVIRONMENT.favicon_external { + let _ = write!( + &mut html_context, + "", + ); + } + + if let Some(head) = &ENVIRONMENT.head { + html_context.push_str(head); + } + + let _ = write!( + &mut html_context, + "{}", + html_escape(&response.meta()), + ); + + if !http_request.path().starts_with("/proxy") { + if let Some(header) = &ENVIRONMENT.header { + let _ = write!( + &mut html_context, + "
{header}
" + ); + } + } + + if let (Some(status), Some(redirected_to)) = + (redirect_response_status, redirect_url.clone()) + { + let _ = write!( + &mut html_context, + "
This page {} redirects to {}.
", + if status == germ::request::Status::PermanentRedirect { + "permanently" + } else { + "temporarily" + }, + redirected_to, + redirected_to + ); + } + + let input_url = redirect_url.unwrap_or_else(|| url.clone()); + let input_field = + if *response.status() == germ::request::Status::SensitiveInput { + "" + } else { + "" + }; + let _ = write!( + &mut html_context, + "

{}

{}
", + html_escape(&response.meta()), + html_escape(&http_request.uri().to_string()), + html_escape(input_url.as_ref()), + input_field, + ); + let mut response_builder = HttpResponse::Ok(); + + if *response.status() == germ::request::Status::SensitiveInput { + response_builder + .insert_header((actix_web::http::header::CACHE_CONTROL, "no-store")); + } + + return Ok( + response_builder + .content_type(format!("text/html; charset={charset}")) + .body(html_context), + ); + } + let mut html_context = if configuration.is_raw() { String::new() } else { -- cgit v1.2.3