main.rs
1 use std::fs; 2 use std::io::{self, Read, Write}; 3 use std::net::{TcpListener, TcpStream}; 4 use std::path::{Path, PathBuf, Component}; 5 use std::thread; 6 7 // Define the Gopher item types 8 enum GopherItemType { 9 TextFile, // 0 10 Directory, // 1 11 BinaryFile, // 9 12 GifImage, // g 13 Image, // I 14 Html, // h 15 InfoLine, // i 16 Error, // 3 17 } 18 19 impl GopherItemType { 20 fn to_char(&self) -> char { 21 match self { 22 GopherItemType::TextFile => '0', 23 GopherItemType::Directory => '1', 24 GopherItemType::BinaryFile => '9', 25 GopherItemType::GifImage => 'g', 26 GopherItemType::Image => 'I', 27 GopherItemType::Html => 'h', 28 GopherItemType::InfoLine => 'i', 29 GopherItemType::Error => '3', 30 } 31 } 32 } 33 34 // Determine the Gopher item type based on file extension 35 fn get_item_type(path: &Path) -> GopherItemType { 36 if path.is_dir() { 37 return GopherItemType::Directory; 38 } 39 40 // Get file extension if it exists 41 match path.extension().and_then(|ext| ext.to_str()) { 42 Some(ext) => { 43 match ext.to_lowercase().as_str() { 44 "txt" | "md" => GopherItemType::TextFile, 45 "gif" => GopherItemType::GifImage, 46 "jpg" | "jpeg" | "png" | "bmp" | "webp" => GopherItemType::Image, 47 "html" | "htm" => GopherItemType::Html, 48 _ => GopherItemType::BinaryFile, // Default to binary for unknown extensions 49 } 50 }, 51 None => GopherItemType::TextFile, // Default to text file if no extension 52 } 53 } 54 55 // Sanitize path to prevent directory traversal attacks 56 fn sanitize_path(input: &str) -> PathBuf { 57 let path = PathBuf::from(input); 58 path.components() 59 .filter(|component| { 60 match component { 61 Component::ParentDir => false, // Remove .. components 62 Component::Normal(_) => true, 63 Component::CurDir => false, // Remove . components 64 _ => true, 65 } 66 }) 67 .collect() 68 } 69 70 fn main() -> io::Result<()> { 71 // Create directory for our gopher files if it doesn't exist 72 let content_dir = "gopher_content"; 73 if !Path::new(content_dir).exists() { 74 fs::create_dir(content_dir)?; 75 } 76 77 // Create images directory if it doesn't exist 78 let images_dir = format!("{}/images", content_dir); 79 if !Path::new(&images_dir).exists() { 80 fs::create_dir(&images_dir)?; 81 } 82 83 // Create sample files if they don't exist 84 create_sample_files(content_dir)?; 85 86 // Start server 87 let listener = TcpListener::bind("0.0.0.0:7070")?; 88 println!("Gopher server listening on port 7070"); 89 println!("Serving files from directory: {}", content_dir); 90 91 for stream in listener.incoming() { 92 match stream { 93 Ok(stream) => { 94 let content_dir = content_dir.to_string(); 95 thread::spawn(move || { 96 if let Err(e) = handle_client(stream, &content_dir) { 97 eprintln!("Error handling client: {}", e); 98 } 99 }); 100 } 101 Err(e) => { 102 eprintln!("Connection failed: {}", e); 103 } 104 } 105 } 106 107 Ok(()) 108 } 109 110 fn create_sample_files(content_dir: &str) -> io::Result<()> { 111 // Create about.txt if it doesn't exist 112 let about_path = format!("{}/about.txt", content_dir); 113 if !Path::new(&about_path).exists() { 114 fs::write( 115 about_path, 116 "This is the about page for my gopher server.\n\nIt's a simple Rust implementation!" 117 )?; 118 } 119 120 // Create welcome.txt if it doesn't exist 121 let welcome_path = format!("{}/welcome.txt", content_dir); 122 if !Path::new(&welcome_path).exists() { 123 fs::write( 124 welcome_path, 125 "Welcome to my Gopher server!\n\nThis server is written in Rust." 126 )?; 127 } 128 129 Ok(()) 130 } 131 132 fn handle_client(mut stream: TcpStream, content_dir: &str) -> io::Result<()> { 133 let mut buffer = [0; 1024]; 134 135 // Add a timeout for read operations to prevent hanging connections 136 stream.set_read_timeout(Some(std::time::Duration::from_secs(5)))?; 137 138 let bytes_read = match stream.read(&mut buffer) { 139 Ok(n) => n, 140 Err(e) => { 141 eprintln!("Error reading from stream: {}", e); 142 return Err(e); 143 } 144 }; 145 146 // Convert buffer to a string and trim trailing whitespace 147 let selector = if bytes_read > 0 { 148 match String::from_utf8(buffer[0..bytes_read].to_vec()) { 149 Ok(s) => s.trim_end_matches(&['\r', '\n'][..]).to_string(), 150 Err(_) => { 151 // Invalid UTF-8 request 152 eprintln!("Received invalid UTF-8 request"); 153 let error_msg = format!("{}Error: Invalid request\r\n.\r\n", GopherItemType::Error.to_char()); 154 stream.write_all(error_msg.as_bytes())?; 155 return Ok(()); 156 } 157 } 158 } else { 159 String::new() 160 }; 161 162 println!("Received selector: '{}'", selector); 163 164 // Empty selector or just a newline means serve the root menu 165 if selector.is_empty() || selector == "\r\n" || selector == "\n" { 166 serve_menu(&mut stream, content_dir, "")?; 167 } else { 168 // Clean the selector by removing any leading '/' 169 let clean_selector = selector.trim_start_matches('/'); 170 171 // Sanitize path to prevent directory traversal 172 let safe_path = sanitize_path(clean_selector); 173 let path = format!("{}/{}", content_dir, safe_path.to_string_lossy()); 174 175 // Check if it's a directory request 176 if Path::new(&path).is_dir() { 177 serve_menu(&mut stream, content_dir, &safe_path.to_string_lossy())?; 178 } else { 179 serve_content(&mut stream, content_dir, &safe_path.to_string_lossy())?; 180 } 181 } 182 183 Ok(()) 184 } 185 186 fn serve_menu(stream: &mut TcpStream, content_dir: &str, subdir: &str) -> io::Result<()> { 187 println!("Serving menu for directory: {}", if subdir.is_empty() { "root" } else { subdir }); 188 189 // Build the full path to the directory 190 let dir_path = if subdir.is_empty() { 191 PathBuf::from(content_dir) 192 } else { 193 // Sanitize subdir path 194 let safe_subdir = sanitize_path(subdir); 195 PathBuf::from(format!("{}/{}", content_dir, safe_subdir.to_string_lossy())) 196 }; 197 198 // Check if directory exists and is actually a directory 199 if !dir_path.exists() || !dir_path.is_dir() { 200 let error_msg = format!("{}Error: Directory not found\r\n.\r\n", GopherItemType::Error.to_char()); 201 stream.write_all(error_msg.as_bytes())?; 202 return Ok(()); 203 } 204 205 // Start building the menu 206 let mut menu = String::from(format!("{}Welcome to my Gopher Server!\r\n", GopherItemType::InfoLine.to_char())); 207 menu.push_str(&format!("{}----------------------------\r\n", GopherItemType::InfoLine.to_char())); 208 209 // If we're in a subdirectory, add a link to go back up 210 if !subdir.is_empty() { 211 // Get parent directory 212 let safe_subdir = sanitize_path(subdir); 213 let parent = safe_subdir.parent().map_or("", |p| p.to_str().unwrap_or("")); 214 215 let selector = if parent.is_empty() { "/".to_string() } else { format!("/{}", parent) }; 216 menu.push_str(&format!("{}Go Back to Parent Directory\t{}\t45.76.84.191\t7070\r\n", 217 GopherItemType::Directory.to_char(), selector)); 218 } 219 220 // Read directory and add all entries to the menu 221 match fs::read_dir(&dir_path) { 222 Ok(entries) => { 223 let mut entries: Vec<_> = entries.filter_map(Result::ok).collect(); 224 225 // Sort entries - directories first, then files 226 entries.sort_by(|a, b| { 227 let a_is_dir = a.path().is_dir(); 228 let b_is_dir = b.path().is_dir(); 229 230 if a_is_dir && !b_is_dir { 231 std::cmp::Ordering::Less 232 } else if !a_is_dir && b_is_dir { 233 std::cmp::Ordering::Greater 234 } else { 235 // Both are the same type, sort by name 236 a.file_name().cmp(&b.file_name()) 237 } 238 }); 239 240 for entry in entries { 241 let path = entry.path(); 242 243 if let Some(filename) = path.file_name() { 244 if let Some(filename_str) = filename.to_str() { 245 // Skip hidden files (starting with .) 246 if filename_str.starts_with('.') { 247 continue; 248 } 249 250 // Determine the item type 251 let item_type = get_item_type(&path); 252 253 // Create the selector path 254 let selector = if subdir.is_empty() { 255 format!("/{}", filename_str) 256 } else { 257 format!("/{}/{}", subdir, filename_str) 258 }; 259 260 // Add to menu with the appropriate item type 261 menu.push_str(&format!( 262 "{}{}\t{}\t45.76.84.191\t7070\r\n", 263 item_type.to_char(), 264 filename_str, 265 selector 266 )); 267 } 268 } 269 } 270 } 271 Err(e) => { 272 eprintln!("Error reading directory: {}", e); 273 menu.push_str(&format!("{}Error reading directory: {}\r\n", 274 GopherItemType::InfoLine.to_char(), e)); 275 } 276 } 277 278 // End of menu marker 279 menu.push_str(".\r\n"); 280 281 stream.write_all(menu.as_bytes())?; 282 Ok(()) 283 } 284 285 fn serve_content(stream: &mut TcpStream, content_dir: &str, selector: &str) -> io::Result<()> { 286 // Sanitize the selector to prevent path traversal 287 let safe_path = sanitize_path(selector); 288 let path = format!("{}/{}", content_dir, safe_path.to_string_lossy()); 289 let path_buf = PathBuf::from(&path); 290 291 println!("Requested file: '{}', serving from path: '{}'", selector, path); 292 293 if path_buf.exists() && path_buf.is_file() { 294 // Determine if we're serving a text or binary file 295 let item_type = get_item_type(&path_buf); 296 297 match item_type { 298 GopherItemType::TextFile | GopherItemType::Html => { 299 // For text files, send the content as is 300 match fs::read_to_string(&path) { 301 Ok(content) => { 302 println!("Serving text file: {}", path); 303 stream.write_all(content.as_bytes())?; 304 }, 305 Err(e) => { 306 eprintln!("Error reading text file: {}", e); 307 let error_msg = format!("{}Error reading file: {}\r\n", 308 GopherItemType::Error.to_char(), e); 309 stream.write_all(error_msg.as_bytes())?; 310 } 311 } 312 }, 313 // For binary files, GIFs and other images, send raw bytes 314 _ => { 315 println!("Serving binary file: {}", path); 316 match fs::read(&path) { 317 Ok(content) => { 318 stream.write_all(&content)?; 319 }, 320 Err(e) => { 321 eprintln!("Error reading binary file: {}", e); 322 let error_msg = format!("{}Error reading file: {}\r\n", 323 GopherItemType::Error.to_char(), e); 324 stream.write_all(error_msg.as_bytes())?; 325 } 326 } 327 } 328 } 329 } else { 330 println!("File not found or is a directory"); 331 let error_msg = format!("{}Error: File '{}' not found or is a directory.\r\n", 332 GopherItemType::Error.to_char(), selector); 333 stream.write_all(error_msg.as_bytes())?; 334 } 335 336 Ok(()) 337 }