♻️ Simple & Efficient Gemini-to-HTTP Proxy
fuwn.net
proxy
gemini-protocol
protocol
gemini
http
rust
1use {
2 crate::{environment::ENVIRONMENT, url::from_path},
3 tokio::{
4 io::{AsyncBufReadExt, AsyncWriteExt, BufReader},
5 net::TcpListener,
6 },
7};
8
9pub 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
42async 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
85fn 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}