/* MORGEMRS :: Morgana's Gemini Rust Server Copyright (C) 2026 Morgana This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . */ use std::fs::{self, DirEntry}; use std::io::{Read, Write}; use std::net::{Ipv4Addr, TcpListener, TcpStream}; use std::path::{Path, PathBuf}; use std::sync::Arc; use rustls::{ ServerConfig, ServerConnection, Stream, pki_types::{CertificateDer, PrivateKeyDer, pem::PemObject}, }; // Change this to the proper domain and IP as required const HOST: Ipv4Addr = Ipv4Addr::new(127, 0, 0, 1); const PREFIX: &str = "gemini://localhost"; fn main() { println!("Starting MorGemRS Server..."); // Path to Gemini content to be hosted let root: PathBuf = fs::canonicalize("content").unwrap(); // Load server certificate chains and private keys let certs = CertificateDer::pem_file_iter("cert/morgemrs.crt") .unwrap() .map(|cert| cert.unwrap()) .collect(); let private_key = PrivateKeyDer::from_pem_file("cert/morgemrs.key").unwrap(); let config = Arc::new( ServerConfig::builder() .with_no_client_auth() .with_single_cert(certs, private_key) .unwrap(), ); // Begin listening for connections let listener = TcpListener::bind((HOST, 1965)).unwrap(); println!("Successfully Started!"); for mut tcp_stream in listener.incoming().flatten() { // Connect with TLS let mut conn = ServerConnection::new(config.clone()).unwrap(); let mut tls_stream = Stream::new(&mut conn, &mut tcp_stream); // Gemini URIs can be 1024 bytes long + 2 CRLF bytes + 1 overflow byte == 1027b let mut buffer = [0; 1027]; // Did we successfully recieve data? let Ok(n) = tls_stream.read(&mut buffer[..]) else { let _ = writeln!(tls_stream, "59\r\n"); continue; }; // Did we recieve too much content? if n == 1027 { let _ = writeln!(tls_stream, "59\r\n"); continue; } // Is the data utf-8? let Ok(raw_uri) = String::from_utf8(buffer.to_vec()) else { let _ = writeln!(tls_stream, "59\r\n"); continue; }; // Trim nulls and whitespace from buffer // Reject proxy requests let Some(mut uri) = raw_uri .trim_end_matches(|c: char| c == char::from(0) || c.is_whitespace()) .strip_prefix(PREFIX) else { let _ = writeln!(tls_stream, "53 Proxy Not Supported\r\n"); continue; }; // Clean up URI uri = uri.trim_matches('/'); // Ensure file exists and remove intermediate uri components let Ok(path) = root.join(uri).canonicalize() else { let _ = writeln!(tls_stream, "51 Not Found\r\n"); continue; }; // Prevent access to files not in the route if !path.starts_with(&root) { let _ = writeln!(tls_stream, "51 Not Found\rpath\n"); continue; } // All checks passed, serve the content if path.is_dir() { serve_dir(&path, uri, &mut tls_stream); } else if path.is_file() { serve_file(&path, &mut tls_stream); } else { let _ = writeln!(tls_stream, "51 Not Found\r\n"); } } } fn serve_dir(dir: &Path, uri: &str, tls_stream: &mut Stream<'_, ServerConnection, TcpStream>) { // If the directory has a `index.gem` file, serve that instead. let index_path = dir.join("index.gem"); if index_path.is_file() { serve_file(&index_path, tls_stream); return; } // Produce a list of links to directory contents let Ok(read_dir) = dir.read_dir() else { let _ = writeln!(tls_stream, "50 Server Error\r\n"); return; }; let contents: Vec = read_dir.flatten().collect(); let content_string = contents .iter() .flat_map(|f| f.file_name().into_string()) .map(|s| { if uri.is_empty() { format!("=> /{}", s) // Prevent leading double slash with empty URI } else { format!("=> /{}/{} ", uri, s) } }) .collect::>() .join("\r\n"); let _ = writeln!(tls_stream, "20 text/gemini\r\n{}", content_string); } fn serve_file(file: &Path, tls_stream: &mut Stream<'_, ServerConnection, TcpStream>) { // Serve file contents (naive, assumes gemtext) let Ok(contents) = fs::read_to_string(file) else { let _ = writeln!(tls_stream, "50 Server Error\r\n"); return; }; let _ = writeln!(tls_stream, "20 text/gemini\r\n{}", contents); }