diff options
| -rw-r--r-- | Cargo.lock | 1 | ||||
| -rw-r--r-- | Cargo.toml | 3 | ||||
| -rw-r--r-- | Configuration.md | 32 | ||||
| -rw-r--r-- | src/environment.rs | 9 | ||||
| -rw-r--r-- | src/http09.rs | 100 | ||||
| -rw-r--r-- | src/main.rs | 5 |
6 files changed, 150 insertions, 0 deletions
@@ -2142,6 +2142,7 @@ dependencies = [ "germ", "log", "pretty_env_logger", + "tokio", "url", "vergen", ] @@ -27,6 +27,9 @@ germ = { version = "0.4.7", features = ["ast", "meta"] } # HTTP actix-web = "4.11.0" +# Async Runtime +tokio = { version = "1", features = ["net", "io-util"] } + # Logging pretty_env_logger = "0.5.0" log = "0.4.27" diff --git a/Configuration.md b/Configuration.md index 1f7fdc7..3e1fe1b 100644 --- a/Configuration.md +++ b/Configuration.md @@ -158,6 +158,38 @@ PRIMARY_COLOUR=red PRIMARY_COLOUR=#ff0000 ``` +## `HTTP09` + +Enable a separate HTTP/0.9 TCP server alongside the main HTTP server + +HTTP/0.9 is the simplest version of HTTP. Requests are a bare `GET /path` line, +and responses are the raw body with no status line or headers. The server returns +the proxied Gemini content directly (text/gemini for text, raw bytes for images). + +This configuration value defaults to `false`. + +```dotenv +HTTP09=true +``` + +## `HTTP09_PORT` + +Bind the HTTP/0.9 server to a custom port + +If no `HTTP09_PORT` is provided or it could not be parsed appropriately as an +unsigned 16-bit integer, `HTTP09_PORT` will default to `90`. + +```dotenv +HTTP09_PORT=9009 +``` + +### Testing + +```sh +echo "GET /" | nc localhost 9009 +curl --http0.9 http://localhost:9009/ +``` + ## `CONDENSE_LINKS_AT_HEADING` This configuration option is similar to `CONDENSE_LINKS`, but only condenses diff --git a/src/environment.rs b/src/environment.rs index dd10171..bd99081 100644 --- a/src/environment.rs +++ b/src/environment.rs @@ -17,6 +17,8 @@ pub struct Environment { pub proxy_by_default: bool, pub keep_gemini: Option<Vec<String>>, pub embed_images: Option<String>, + pub http09: bool, + pub http09_port: u16, } impl Environment { @@ -51,6 +53,13 @@ impl Environment { .ok() .map(|s| s.split(',').map(String::from).collect()), embed_images: std::env::var("EMBED_IMAGES").ok(), + http09: std::env::var("HTTP09") + .map(|v| v.to_lowercase() == "true") + .unwrap_or(false), + http09_port: std::env::var("HTTP09_PORT") + .ok() + .and_then(|p| p.parse().ok()) + .unwrap_or(90), } } } diff --git a/src/http09.rs b/src/http09.rs new file mode 100644 index 0000000..b18cd96 --- /dev/null +++ b/src/http09.rs @@ -0,0 +1,100 @@ +use { + crate::{environment::ENVIRONMENT, url::from_path}, + tokio::{ + io::{AsyncBufReadExt, AsyncWriteExt, BufReader}, + net::TcpListener, + }, +}; + +pub async fn serve() { + let address = format!("0.0.0.0:{}", ENVIRONMENT.http09_port); + let listener = match TcpListener::bind(&address).await { + Ok(listener) => { + info!("HTTP/0.9 server listening on {address}"); + + listener + } + Err(error) => { + error!("failed to bind HTTP/0.9 server to {address}: {error}"); + + return; + } + }; + + loop { + let (stream, peer) = match listener.accept().await { + Ok(connection) => connection, + Err(error) => { + warn!("HTTP/0.9 accept error: {error}"); + + continue; + } + }; + + tokio::spawn(async move { + if let Err(error) = handle(stream).await { + warn!("HTTP/0.9 error from {peer}: {error}"); + } + }); + } +} + +async fn handle( + stream: tokio::net::TcpStream, +) -> Result<(), Box<dyn std::error::Error + Send + Sync>> { + let (reader, mut writer) = stream.into_split(); + let mut reader = BufReader::new(reader); + let mut request_line = String::new(); + + reader.read_line(&mut request_line).await?; + + let path = parse_request(&request_line)?; + let mut configuration = crate::response::configuration::Configuration::new(); + let url = from_path(&path, false, &mut configuration)?; + let mut response = germ::request::request(&url).await?; + + if *response.status() == germ::request::Status::PermanentRedirect + || *response.status() == germ::request::Status::TemporaryRedirect + { + let redirect = if response.meta().starts_with('/') { + format!( + "gemini://{}{}", + url.domain().unwrap_or_default(), + response.meta() + ) + } else { + response.meta().to_string() + }; + + response = germ::request::request(&url::Url::parse(&redirect)?).await?; + } + + if response.meta().starts_with("image/") { + if let Some(bytes) = response.content_bytes() { + writer.write_all(bytes).await?; + } + } else if let Some(content) = response.content() { + writer.write_all(content.as_bytes()).await?; + } + + writer.shutdown().await?; + + Ok(()) +} + +fn parse_request( + line: &str, +) -> Result<String, Box<dyn std::error::Error + Send + Sync>> { + let line = line.trim(); + + line.strip_prefix("GET ").map_or_else( + || { + if line.starts_with('/') { + Ok(line.to_string()) + } else { + Err(format!("invalid HTTP/0.9 request: {line}").into()) + } + }, + |path| Ok(path.split_whitespace().next().unwrap_or("/").to_string()), + ) +} diff --git a/src/main.rs b/src/main.rs index a39e83e..c0e500c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -12,6 +12,7 @@ mod environment; mod html; +mod http09; mod response; mod url; @@ -29,6 +30,10 @@ async fn main() -> std::io::Result<()> { ) .init(); + if environment::ENVIRONMENT.http09 { + tokio::spawn(http09::serve()); + } + actix_web::HttpServer::new(move || { actix_web::App::new() .default_service(web::get().to(default)) |