♻️ Simple & Efficient Gemini-to-HTTP Proxy fuwn.net
proxy gemini-protocol protocol gemini http rust
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat(response): Add input/sensitive-input handling

Fuwn c8d3edfb 854ac15e

+174 -1
+2
Cargo.lock
··· 2142 2142 "germ", 2143 2143 "log", 2144 2144 "pretty_env_logger", 2145 + "serde", 2145 2146 "tokio", 2146 2147 "url", 2147 2148 "vergen", ··· 2154 2155 checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" 2155 2156 dependencies = [ 2156 2157 "serde_core", 2158 + "serde_derive", 2157 2159 ] 2158 2160 2159 2161 [[package]]
+3
Cargo.toml
··· 43 43 # Markdown Encoding 44 44 comrak = "0.29.0" 45 45 46 + # Form Parsing 47 + serde = { version = "1", features = ["derive"] } 48 + 46 49 [build-dependencies] 47 50 # Compile-time Environment Variables 48 51 vergen = { version = "8.3.2", features = ["git", "gitoxide"] }
+169 -1
src/response.rs
··· 11 11 12 12 const CSS: &str = include_str!("../default.css"); 13 13 14 + #[derive(serde::Deserialize)] 15 + pub struct InputSubmission { 16 + input: String, 17 + target: Option<String>, 18 + } 19 + 20 + fn html_escape(input: &str) -> String { 21 + input 22 + .replace('&', "&amp;") 23 + .replace('"', "&quot;") 24 + .replace('<', "&lt;") 25 + .replace('>', "&gt;") 26 + } 27 + 14 28 #[allow(clippy::future_not_send, clippy::too_many_lines)] 15 29 pub async fn default( 16 30 http_request: actix_web::HttpRequest, 31 + input_submission: Option<actix_web::web::Form<InputSubmission>>, 17 32 ) -> Result<HttpResponse, Error> { 18 33 if ["/proxy", "/proxy/", "/x", "/x/", "/raw", "/raw/", "/nocss", "/nocss/"] 19 34 .contains(&http_request.path()) ··· 28 43 } 29 44 30 45 let mut configuration = configuration::Configuration::new(); 31 - let url = match url_from_path( 46 + let submitted_input = 47 + if *http_request.method() == actix_web::http::Method::POST { 48 + input_submission.as_ref().map(|submission| submission.input.clone()) 49 + } else { 50 + None 51 + }; 52 + let submitted_target = 53 + if *http_request.method() == actix_web::http::Method::POST { 54 + input_submission.as_ref().and_then(|submission| submission.target.clone()) 55 + } else { 56 + None 57 + }; 58 + let mut url = match url_from_path( 32 59 &format!("{}{}", http_request.path(), { 33 60 if !http_request.query_string().is_empty() 34 61 || http_request.uri().to_string().ends_with('?') ··· 50 77 ); 51 78 } 52 79 }; 80 + 81 + if let Some(target) = submitted_target { 82 + if let Ok(parsed_target) = url::Url::parse(&target) { 83 + if parsed_target.scheme() == "gemini" { 84 + url = parsed_target; 85 + } 86 + } 87 + } 88 + 89 + if let Some(input) = submitted_input { 90 + let input = input 91 + .replace("\r\n", "\n") 92 + .replace('\r', "\n") 93 + .replace('\t', "%09") 94 + .replace('\n', "%0A"); 95 + 96 + url.set_query(Some(&input)); 97 + } 98 + 53 99 let mut timer = Instant::now(); 54 100 let mut response = match germ::request::request(&url).await { 55 101 Ok(response) => response, ··· 104 150 .body(content_bytes.to_vec()), 105 151 ); 106 152 } 153 + } 154 + 155 + if *response.status() == germ::request::Status::Input 156 + || *response.status() == germ::request::Status::SensitiveInput 157 + { 158 + if configuration.is_raw() { 159 + return Ok( 160 + HttpResponse::Ok() 161 + .content_type(format!("text/plain; charset={charset}")) 162 + .body(response.meta().to_string()), 163 + ); 164 + } 165 + 166 + let mut html_context = format!( 167 + r#"<!DOCTYPE html><html{}><head><meta name="viewport" content="width=device-width, initial-scale=1.0">"#, 168 + if language.is_empty() { 169 + String::new() 170 + } else { 171 + format!(" lang=\"{language}\"") 172 + } 173 + ); 174 + 175 + if !configuration.is_no_css() { 176 + if let Some(css) = &ENVIRONMENT.css_external { 177 + for stylesheet in css.split(',').filter(|s| !s.is_empty()) { 178 + let _ = write!( 179 + &mut html_context, 180 + "<link rel=\"stylesheet\" type=\"text/css\" href=\"{stylesheet}\">", 181 + ); 182 + } 183 + } else { 184 + let _ = write!( 185 + &mut html_context, 186 + r#"<link rel="stylesheet" href="https://latex.vercel.app/style.css"><style>{CSS}</style>"# 187 + ); 188 + 189 + if let Some(primary) = &ENVIRONMENT.primary_colour { 190 + let _ = write!( 191 + &mut html_context, 192 + "<style>:root {{ --primary: {primary} }}</style>" 193 + ); 194 + } else { 195 + let _ = write!( 196 + &mut html_context, 197 + "<style>:root {{ --primary: var(--base0D); }}</style>" 198 + ); 199 + } 200 + } 201 + } 202 + 203 + if let Some(favicon) = &ENVIRONMENT.favicon_external { 204 + let _ = write!( 205 + &mut html_context, 206 + "<link rel=\"icon\" type=\"image/x-icon\" href=\"{favicon}\">", 207 + ); 208 + } 209 + 210 + if let Some(head) = &ENVIRONMENT.head { 211 + html_context.push_str(head); 212 + } 213 + 214 + let _ = write!( 215 + &mut html_context, 216 + "<title>{}</title></head><body>", 217 + html_escape(&response.meta()), 218 + ); 219 + 220 + if !http_request.path().starts_with("/proxy") { 221 + if let Some(header) = &ENVIRONMENT.header { 222 + let _ = write!( 223 + &mut html_context, 224 + "<big><blockquote>{header}</blockquote></big>" 225 + ); 226 + } 227 + } 228 + 229 + if let (Some(status), Some(redirected_to)) = 230 + (redirect_response_status, redirect_url.clone()) 231 + { 232 + let _ = write!( 233 + &mut html_context, 234 + "<blockquote>This page {} redirects to <a \ 235 + href=\"{}\">{}</a>.</blockquote>", 236 + if status == germ::request::Status::PermanentRedirect { 237 + "permanently" 238 + } else { 239 + "temporarily" 240 + }, 241 + redirected_to, 242 + redirected_to 243 + ); 244 + } 245 + 246 + let input_url = redirect_url.unwrap_or_else(|| url.clone()); 247 + let input_field = 248 + if *response.status() == germ::request::Status::SensitiveInput { 249 + "<input name=\"input\" type=\"password\" autofocus>" 250 + } else { 251 + "<textarea name=\"input\" rows=\"8\" autofocus></textarea>" 252 + }; 253 + let _ = write!( 254 + &mut html_context, 255 + "<p>{}</p><form method=\"post\" action=\"{}\"><input type=\"hidden\" \ 256 + name=\"target\" value=\"{}\">{}<button \ 257 + type=\"submit\">Submit</button></form></body></html>", 258 + html_escape(&response.meta()), 259 + html_escape(&http_request.uri().to_string()), 260 + html_escape(input_url.as_ref()), 261 + input_field, 262 + ); 263 + let mut response_builder = HttpResponse::Ok(); 264 + 265 + if *response.status() == germ::request::Status::SensitiveInput { 266 + response_builder 267 + .insert_header((actix_web::http::header::CACHE_CONTROL, "no-store")); 268 + } 269 + 270 + return Ok( 271 + response_builder 272 + .content_type(format!("text/html; charset={charset}")) 273 + .body(html_context), 274 + ); 107 275 } 108 276 109 277 let mut html_context = if configuration.is_raw() {