aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Cargo.lock1
-rw-r--r--Cargo.toml3
-rw-r--r--Configuration.md32
-rw-r--r--src/environment.rs9
-rw-r--r--src/http09.rs100
-rw-r--r--src/main.rs5
6 files changed, 150 insertions, 0 deletions
diff --git a/Cargo.lock b/Cargo.lock
index 7fdc070..8ddded3 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -2142,6 +2142,7 @@ dependencies = [
"germ",
"log",
"pretty_env_logger",
+ "tokio",
"url",
"vergen",
]
diff --git a/Cargo.toml b/Cargo.toml
index b376fbe..c6500f9 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -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))