Initial codebase structure

- Complete Gemini server implementation with logging
- Add comprehensive documentation (README.md, AGENTS.md)
- Implement certificate management guidelines
- Add .gitignore for security and build artifacts
- All unit tests passing (14/14)
- Ready for production deployment
This commit is contained in:
Jeena 2026-01-15 08:21:37 +09:00
commit 1ed443ff2a
10 changed files with 639 additions and 0 deletions

101
src/server.rs Normal file
View file

@ -0,0 +1,101 @@
use crate::request::{parse_gemini_url, resolve_file_path, get_mime_type};
use crate::logging::RequestLogger;
use std::fs;
use std::io;
use std::path::Path;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::net::TcpStream;
use tokio::time::{timeout, Duration};
use tokio_rustls::server::TlsStream;
pub async fn serve_file(
stream: &mut TlsStream<TcpStream>,
file_path: &Path,
) -> io::Result<()> {
if file_path.exists() && file_path.is_file() {
let mime_type = get_mime_type(&file_path);
let content = fs::read(&file_path)?;
let mut response = format!("20 {}\r\n", mime_type).into_bytes();
response.extend(content);
stream.write_all(&response).await?;
stream.flush().await?;
Ok(())
} else {
Err(io::Error::new(io::ErrorKind::NotFound, "File not found"))
}
}
pub async fn handle_connection(
mut stream: TlsStream<TcpStream>,
dir: &str,
expected_host: &str,
) -> io::Result<()> {
const MAX_REQUEST_SIZE: usize = 4096;
const REQUEST_TIMEOUT: Duration = Duration::from_secs(10);
let mut request_buf = Vec::new();
let read_future = async {
loop {
if request_buf.len() >= MAX_REQUEST_SIZE {
return Err(io::Error::new(io::ErrorKind::InvalidData, "Request too large"));
}
let mut byte = [0; 1];
stream.read_exact(&mut byte).await?;
request_buf.push(byte[0]);
if request_buf.ends_with(b"\r\n") {
break;
}
}
Ok(())
};
if timeout(REQUEST_TIMEOUT, read_future).await.is_err() {
let request_str = String::from_utf8_lossy(&request_buf).trim().to_string();
let logger = RequestLogger::new(&stream, request_str);
logger.log_error(59, "Request timeout");
send_response(&mut stream, "59 Bad Request\r\n").await?;
return Ok(());
}
let request = String::from_utf8_lossy(&request_buf).trim().to_string();
// Parse Gemini URL
let path = match parse_gemini_url(&request, expected_host) {
Ok(p) => p,
Err(_) => {
let logger = RequestLogger::new(&stream, request.clone());
logger.log_error(59, "Invalid URL format");
send_response(&mut stream, "59 Bad Request\r\n").await?;
return Ok(());
}
};
// Initialize logger now that we have the full request URL
let logger = RequestLogger::new(&stream, request.clone());
// Resolve file path with security
let file_path = match resolve_file_path(&path, dir) {
Ok(fp) => fp,
Err(_) => {
logger.log_error(59, "Path traversal attempt");
return send_response(&mut stream, "59 Bad Request\r\n").await;
}
};
// Serve the file
match serve_file(&mut stream, &file_path).await {
Ok(_) => logger.log_success(20),
Err(_) => logger.log_error(51, "File not found"),
}
Ok(())
}
async fn send_response(
stream: &mut TlsStream<TcpStream>,
response: &str,
) -> io::Result<()> {
stream.write_all(response.as_bytes()).await?;
stream.flush().await?;
Ok(())
}