♻️ 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: Support HTTP/0.9

Fuwn 1cb2515d d6e4bca9

+150
+1
Cargo.lock
··· 2142 2142 "germ", 2143 2143 "log", 2144 2144 "pretty_env_logger", 2145 + "tokio", 2145 2146 "url", 2146 2147 "vergen", 2147 2148 ]
+3
Cargo.toml
··· 27 27 # HTTP 28 28 actix-web = "4.11.0" 29 29 30 + # Async Runtime 31 + tokio = { version = "1", features = ["net", "io-util"] } 32 + 30 33 # Logging 31 34 pretty_env_logger = "0.5.0" 32 35 log = "0.4.27"
+32
Configuration.md
··· 158 158 PRIMARY_COLOUR=#ff0000 159 159 ``` 160 160 161 + ## `HTTP09` 162 + 163 + Enable a separate HTTP/0.9 TCP server alongside the main HTTP server 164 + 165 + HTTP/0.9 is the simplest version of HTTP. Requests are a bare `GET /path` line, 166 + and responses are the raw body with no status line or headers. The server returns 167 + the proxied Gemini content directly (text/gemini for text, raw bytes for images). 168 + 169 + This configuration value defaults to `false`. 170 + 171 + ```dotenv 172 + HTTP09=true 173 + ``` 174 + 175 + ## `HTTP09_PORT` 176 + 177 + Bind the HTTP/0.9 server to a custom port 178 + 179 + If no `HTTP09_PORT` is provided or it could not be parsed appropriately as an 180 + unsigned 16-bit integer, `HTTP09_PORT` will default to `90`. 181 + 182 + ```dotenv 183 + HTTP09_PORT=9009 184 + ``` 185 + 186 + ### Testing 187 + 188 + ```sh 189 + echo "GET /" | nc localhost 9009 190 + curl --http0.9 http://localhost:9009/ 191 + ``` 192 + 161 193 ## `CONDENSE_LINKS_AT_HEADING` 162 194 163 195 This configuration option is similar to `CONDENSE_LINKS`, but only condenses
+9
src/environment.rs
··· 17 17 pub proxy_by_default: bool, 18 18 pub keep_gemini: Option<Vec<String>>, 19 19 pub embed_images: Option<String>, 20 + pub http09: bool, 21 + pub http09_port: u16, 20 22 } 21 23 22 24 impl Environment { ··· 51 53 .ok() 52 54 .map(|s| s.split(',').map(String::from).collect()), 53 55 embed_images: std::env::var("EMBED_IMAGES").ok(), 56 + http09: std::env::var("HTTP09") 57 + .map(|v| v.to_lowercase() == "true") 58 + .unwrap_or(false), 59 + http09_port: std::env::var("HTTP09_PORT") 60 + .ok() 61 + .and_then(|p| p.parse().ok()) 62 + .unwrap_or(90), 54 63 } 55 64 } 56 65 }
+100
src/http09.rs
··· 1 + use { 2 + crate::{environment::ENVIRONMENT, url::from_path}, 3 + tokio::{ 4 + io::{AsyncBufReadExt, AsyncWriteExt, BufReader}, 5 + net::TcpListener, 6 + }, 7 + }; 8 + 9 + pub async fn serve() { 10 + let address = format!("0.0.0.0:{}", ENVIRONMENT.http09_port); 11 + let listener = match TcpListener::bind(&address).await { 12 + Ok(listener) => { 13 + info!("HTTP/0.9 server listening on {address}"); 14 + 15 + listener 16 + } 17 + Err(error) => { 18 + error!("failed to bind HTTP/0.9 server to {address}: {error}"); 19 + 20 + return; 21 + } 22 + }; 23 + 24 + loop { 25 + let (stream, peer) = match listener.accept().await { 26 + Ok(connection) => connection, 27 + Err(error) => { 28 + warn!("HTTP/0.9 accept error: {error}"); 29 + 30 + continue; 31 + } 32 + }; 33 + 34 + tokio::spawn(async move { 35 + if let Err(error) = handle(stream).await { 36 + warn!("HTTP/0.9 error from {peer}: {error}"); 37 + } 38 + }); 39 + } 40 + } 41 + 42 + async fn handle( 43 + stream: tokio::net::TcpStream, 44 + ) -> Result<(), Box<dyn std::error::Error + Send + Sync>> { 45 + let (reader, mut writer) = stream.into_split(); 46 + let mut reader = BufReader::new(reader); 47 + let mut request_line = String::new(); 48 + 49 + reader.read_line(&mut request_line).await?; 50 + 51 + let path = parse_request(&request_line)?; 52 + let mut configuration = crate::response::configuration::Configuration::new(); 53 + let url = from_path(&path, false, &mut configuration)?; 54 + let mut response = germ::request::request(&url).await?; 55 + 56 + if *response.status() == germ::request::Status::PermanentRedirect 57 + || *response.status() == germ::request::Status::TemporaryRedirect 58 + { 59 + let redirect = if response.meta().starts_with('/') { 60 + format!( 61 + "gemini://{}{}", 62 + url.domain().unwrap_or_default(), 63 + response.meta() 64 + ) 65 + } else { 66 + response.meta().to_string() 67 + }; 68 + 69 + response = germ::request::request(&url::Url::parse(&redirect)?).await?; 70 + } 71 + 72 + if response.meta().starts_with("image/") { 73 + if let Some(bytes) = response.content_bytes() { 74 + writer.write_all(bytes).await?; 75 + } 76 + } else if let Some(content) = response.content() { 77 + writer.write_all(content.as_bytes()).await?; 78 + } 79 + 80 + writer.shutdown().await?; 81 + 82 + Ok(()) 83 + } 84 + 85 + fn parse_request( 86 + line: &str, 87 + ) -> Result<String, Box<dyn std::error::Error + Send + Sync>> { 88 + let line = line.trim(); 89 + 90 + line.strip_prefix("GET ").map_or_else( 91 + || { 92 + if line.starts_with('/') { 93 + Ok(line.to_string()) 94 + } else { 95 + Err(format!("invalid HTTP/0.9 request: {line}").into()) 96 + } 97 + }, 98 + |path| Ok(path.split_whitespace().next().unwrap_or("/").to_string()), 99 + ) 100 + }
+5
src/main.rs
··· 12 12 13 13 mod environment; 14 14 mod html; 15 + mod http09; 15 16 mod response; 16 17 mod url; 17 18 ··· 28 29 .unwrap_or_else(|_| "actix_web=info".to_string()), 29 30 ) 30 31 .init(); 32 + 33 + if environment::ENVIRONMENT.http09 { 34 + tokio::spawn(http09::serve()); 35 + } 31 36 32 37 actix_web::HttpServer::new(move || { 33 38 actix_web::App::new()