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

119
src/request.rs Normal file
View file

@ -0,0 +1,119 @@
use path_security::validate_path;
use std::path::{Path, PathBuf};
pub fn parse_gemini_url(request: &str, expected_host: &str) -> Result<String, ()> {
if let Some(url) = request.strip_prefix("gemini://") {
let host_end = url.find('/').unwrap_or(url.len());
let host = &url[..host_end];
if host != expected_host {
return Err(()); // Hostname mismatch
}
let path = if host_end < url.len() { &url[host_end..] } else { "/" };
Ok(path.trim().to_string())
} else {
Err(())
}
}
pub fn resolve_file_path(path: &str, dir: &str) -> Result<PathBuf, ()> {
let file_path_str = if path == "/" {
"index.gmi".to_string()
} else if path.ends_with('/') {
format!("{}index.gmi", &path[1..])
} else {
path[1..].to_string()
};
match validate_path(Path::new(&file_path_str), Path::new(dir)) {
Ok(safe_path) => Ok(safe_path),
Err(_) => Err(()),
}
}
pub fn get_mime_type(file_path: &Path) -> &str {
if let Some(ext) = file_path.extension() {
match ext.to_str() {
Some("gmi") => "text/gemini",
Some("txt") => "text/plain",
Some("html") => "text/html",
Some("png") => "image/png",
Some("jpg") | Some("jpeg") => "image/jpeg",
Some("webp") => "image/webp",
Some("gif") => "image/gif",
_ => "application/octet-stream",
}
} else {
"application/octet-stream"
}
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::TempDir;
#[test]
fn test_parse_gemini_url_valid() {
assert_eq!(parse_gemini_url("gemini://gemini.jeena.net/", "gemini.jeena.net"), Ok("/".to_string()));
assert_eq!(parse_gemini_url("gemini://gemini.jeena.net/posts/test", "gemini.jeena.net"), Ok("/posts/test".to_string()));
}
#[test]
fn test_parse_gemini_url_invalid_host() {
assert!(parse_gemini_url("gemini://foo.com/", "gemini.jeena.net").is_err());
}
#[test]
fn test_parse_gemini_url_no_prefix() {
assert!(parse_gemini_url("http://gemini.jeena.net/", "gemini.jeena.net").is_err());
}
#[test]
fn test_resolve_file_path_root() {
let temp_dir = TempDir::new().unwrap();
assert!(resolve_file_path("/", temp_dir.path().to_str().unwrap()).is_ok());
}
#[test]
fn test_resolve_file_path_directory() {
let temp_dir = TempDir::new().unwrap();
std::fs::create_dir(temp_dir.path().join("test")).unwrap();
assert!(resolve_file_path("/test/", temp_dir.path().to_str().unwrap()).is_ok());
}
#[test]
fn test_resolve_file_path_file() {
let temp_dir = TempDir::new().unwrap();
assert!(resolve_file_path("/test.gmi", temp_dir.path().to_str().unwrap()).is_ok());
}
#[test]
fn test_resolve_file_path_traversal() {
let temp_dir = TempDir::new().unwrap();
assert!(resolve_file_path("/../etc/passwd", temp_dir.path().to_str().unwrap()).is_err());
}
#[test]
fn test_get_mime_type_gmi() {
let path = Path::new("test.gmi");
assert_eq!(get_mime_type(path), "text/gemini");
}
#[test]
fn test_get_mime_type_png() {
let path = Path::new("test.png");
assert_eq!(get_mime_type(path), "image/png");
}
#[test]
fn test_get_mime_type_unknown() {
let path = Path::new("test.xyz");
assert_eq!(get_mime_type(path), "application/octet-stream");
}
#[test]
fn test_get_mime_type_no_extension() {
let path = Path::new("test");
assert_eq!(get_mime_type(path), "application/octet-stream");
}
}