♻️ 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!: Overhaul KEEP_GEMINI configuration option

Fuwn 22bce5a8 e4d2e7df

+79 -87
+8 -14
Configuration.md
··· 37 37 CSS_EXTERNAL=https://cdnjs.cloudflare.com/ajax/libs/mini.css/3.0.1/mini-default.min.css 38 38 ``` 39 39 40 - ## `KEEP_GEMINI_EXACT` 40 + ## `KEEP_GEMINI` 41 41 42 - A comma-separated list of Gemini URIs to keep as is when proxying. 42 + A comma-separated list of Gemini URL fragments to keep as is when proxying 43 + 44 + Wildcards are supported using the `*` character, and exceptions can be made 45 + using the `!` character 43 46 44 47 ```dotenv 45 - # These two URIs will be kept pointing to their original Gemini URIs when 46 - # proxied instead of being replaced with their proxied equivalents. 47 - KEEP_GEMINI_EXACT=gemini://fuwn.me/something,gemini://fuwn.me/another 48 + # These rules ensure that all Gemini URLs will be left untouched in the proxied 49 + # HTML response except for URLs under the "fuwn.me" domain 50 + KEEP_GEMINI=!*fuwn.me/*,gemini://* 48 51 ``` 49 52 50 53 ## `HEAD` ··· 53 56 54 57 ```dotenv 55 58 HEAD=<script>/* September */</script><style>/* September */</style> 56 - ``` 57 - 58 - ## `KEEP_GEMINI_DOMAIN` 59 - 60 - Similar to `KEEP_GEMINI_EXACT`, except matches based on entire domain or domains 61 - instead of exact URIs 62 - 63 - ```dotenv 64 - KEEP_GEMINI_DOMAIN=fuwn.me,example.com 65 59 ``` 66 60 67 61 ## `PROXY_BY_DEFAULT`
-2
docker-compose.yaml
··· 5 5 environment: 6 6 - "ROOT=gemini://fuwn.me" 7 7 - "CSS_EXTERNAL=https://example.com/style.css" 8 - - "KEEP_GEMINI_EXACT=gemini://fuwn.me/skills" 9 - # - "KEEP_GEMINI_DOMAIN=fuwn.me" 10 8 - "PROXY_BY_DEFAULT=true" 11 9 image: "fuwn/september:latest"
+26 -25
src/html.rs
··· 1 1 use { 2 + crate::url::matches_pattern, 2 3 germ::ast::Node, 3 4 std::{env::var, fmt::Write}, 4 5 url::Url, 5 6 }; 6 7 7 8 fn link_from_host_href(url: &Url, href: &str) -> Option<String> { 8 - Some(format!( 9 - "gemini://{}{}{}", 10 - url.domain()?, 11 - { if href.starts_with('/') { "" } else { "/" } }, 12 - href 13 - )) 9 + if href.starts_with("/proxy/") { 10 + Some(format!("gemini://{}", href.replace("/proxy/", ""))) 11 + } else { 12 + Some(format!( 13 + "gemini://{}{}{}", 14 + url.domain()?, 15 + { if href.starts_with('/') { "" } else { "/" } }, 16 + href 17 + )) 18 + } 14 19 } 15 20 16 21 fn safe(text: &str) -> String { ··· 181 186 } 182 187 } 183 188 184 - if let Ok(keeps) = var("KEEP_GEMINI_EXACT") { 185 - let mut keeps = keeps.split(','); 189 + if let Ok(keeps) = var("KEEP_GEMINI") { 190 + let patterns = keeps.split(',').collect::<Vec<_>>(); 186 191 187 192 if (href.starts_with('/') || !href.contains("://")) && !surface { 188 193 let temporary_href = link_from_host_href(url, &href)?; 194 + let should_exclude = patterns 195 + .iter() 196 + .filter(|p| p.starts_with('!')) 197 + .any(|p| matches_pattern(&p[1..], &temporary_href)); 189 198 190 - if keeps.any(|k| k == &*temporary_href) { 191 - href = temporary_href; 192 - } 193 - } 194 - } 195 - 196 - if let Ok(keeps) = var("KEEP_GEMINI_DOMAIN") { 197 - let host = if let Some(host) = url.host() { 198 - host.to_string() 199 - } else { 200 - return None; 201 - }; 199 + if !should_exclude { 200 + let should_include = patterns 201 + .iter() 202 + .filter(|p| !p.starts_with('!')) 203 + .any(|p| matches_pattern(p, &temporary_href)); 202 204 203 - if (href.starts_with('/') 204 - || !href.contains("://") && keeps.split(',').any(|k| k == &*host)) 205 - && !surface 206 - { 207 - href = link_from_host_href(url, &href)?; 205 + if should_include { 206 + href = temporary_href; 207 + } 208 + } 208 209 } 209 210 } 210 211
+3 -46
src/response.rs
··· 1 1 pub mod configuration; 2 2 3 3 use { 4 - crate::url::from_path as url_from_path, 4 + crate::url::{from_path as url_from_path, matches_pattern}, 5 5 actix_web::{Error, HttpResponse}, 6 6 std::{env::var, fmt::Write, time::Instant}, 7 7 }; ··· 262 262 263 263 if let Ok(plain_texts) = var("PLAIN_TEXT_ROUTE") { 264 264 if plain_texts.split(',').any(|r| { 265 - path_matches_pattern(r, http_request.path()) 266 - || path_matches_pattern(r, http_request.path().trim_end_matches('/')) 265 + matches_pattern(r, http_request.path()) 266 + || matches_pattern(r, http_request.path().trim_end_matches('/')) 267 267 }) { 268 268 return Ok(HttpResponse::Ok().body( 269 269 response.content().as_ref().map_or_else(String::default, String::clone), ··· 277 277 .body(html_context), 278 278 ) 279 279 } 280 - 281 - fn path_matches_pattern(pattern: &str, path: &str) -> bool { 282 - if !pattern.contains('*') { 283 - return path == pattern; 284 - } 285 - 286 - let parts: Vec<&str> = pattern.split('*').collect(); 287 - let mut position = if pattern.starts_with('*') { 288 - 0 289 - } else { 290 - let first = parts.first().unwrap(); 291 - 292 - if !path.starts_with(first) { 293 - return false; 294 - } 295 - 296 - first.len() 297 - }; 298 - 299 - let mid_end = parts.len().saturating_sub(1); 300 - 301 - for part in &parts[1..mid_end] { 302 - if part.is_empty() { 303 - continue; 304 - } 305 - 306 - if let Some(found) = path[position..].find(part) { 307 - position += found + part.len(); 308 - } else { 309 - return false; 310 - } 311 - } 312 - 313 - if !pattern.ends_with('*') { 314 - let last = parts.last().unwrap(); 315 - 316 - if !path[position..].ends_with(last) { 317 - return false; 318 - } 319 - } 320 - 321 - true 322 - }
+42
src/url.rs
··· 57 57 ) 58 58 }) 59 59 } 60 + 61 + pub fn matches_pattern(pattern: &str, path: &str) -> bool { 62 + if !pattern.contains('*') { 63 + return path == pattern; 64 + } 65 + 66 + let parts: Vec<&str> = pattern.split('*').collect(); 67 + let mut position = if pattern.starts_with('*') { 68 + 0 69 + } else { 70 + let first = parts.first().unwrap(); 71 + 72 + if !path.starts_with(first) { 73 + return false; 74 + } 75 + 76 + first.len() 77 + }; 78 + let before_last = parts.len().saturating_sub(1); 79 + 80 + for part in &parts[1..before_last] { 81 + if part.is_empty() { 82 + continue; 83 + } 84 + 85 + if let Some(found) = path[position..].find(part) { 86 + position += found + part.len(); 87 + } else { 88 + return false; 89 + } 90 + } 91 + 92 + if !pattern.ends_with('*') { 93 + let last = parts.last().unwrap(); 94 + 95 + if !path[position..].ends_with(last) { 96 + return false; 97 + } 98 + } 99 + 100 + true 101 + }