main.rs (29153B)
1 #![forbid(unsafe_code)] 2 3 mod certificates; 4 mod codes; 5 mod metadata; 6 use codes::*; 7 use metadata::{FileOptions, PresetMeta}; 8 9 use { 10 once_cell::sync::Lazy, 11 percent_encoding::{percent_decode_str, percent_encode, AsciiSet, CONTROLS}, 12 rcgen::{CertificateParams, DnType, KeyPair}, 13 std::{ 14 borrow::Cow, 15 error::Error, 16 ffi::OsStr, 17 fmt::Write, 18 fs::{self, File}, 19 io::Write as _, 20 net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr}, 21 path::{self, Component, Path, PathBuf}, 22 sync::Arc, 23 }, 24 tokio::{ 25 io::{AsyncReadExt, AsyncWriteExt}, 26 net::{TcpListener, TcpStream}, 27 runtime::Runtime, 28 sync::Mutex, 29 }, 30 tokio_rustls::{ 31 rustls::{server::ServerConfig, version::TLS13}, 32 server::TlsStream, 33 TlsAcceptor, 34 }, 35 url::{Host, Url}, 36 }; 37 38 #[cfg(unix)] 39 use { 40 std::os::unix::fs::{FileTypeExt, PermissionsExt}, 41 tokio::net::{UnixListener, UnixStream}, 42 }; 43 44 static DEFAULT_PORT: u16 = 1965; 45 46 fn main() { 47 env_logger::Builder::from_env( 48 // by default only turn on logging for agate 49 env_logger::Env::default().default_filter_or("agate=info"), 50 ) 51 .init(); 52 Runtime::new() 53 .expect("could not start tokio runtime") 54 .block_on(async { 55 let default = PresetMeta::Parameters( 56 ARGS.language 57 .as_ref() 58 .map_or(String::new(), |lang| format!(";lang={lang}")), 59 ); 60 let mimetypes = Arc::new(Mutex::new(FileOptions::new(default))); 61 62 // some systems automatically listen in dual stack if the IPv6 unspecified 63 // address is used, so don't fail if the second unspecified address gets 64 // an error when trying to start 65 let mut listening_unspecified = false; 66 67 let mut handles = vec![]; 68 for addr in &ARGS.addrs { 69 let arc = mimetypes.clone(); 70 71 let listener = match TcpListener::bind(addr).await { 72 Err(e) => { 73 if !(addr.ip().is_unspecified() && listening_unspecified) { 74 panic!("Failed to listen on {addr}: {e}") 75 } else { 76 // already listening on the other unspecified address 77 log::warn!("Could not start listener on {}, but already listening on another unspecified address. Probably your system automatically listens in dual stack?", addr); 78 continue; 79 } 80 } 81 Ok(listener) => listener, 82 }; 83 listening_unspecified |= addr.ip().is_unspecified(); 84 85 handles.push(tokio::spawn(async move { 86 log::info!("Started listener on {}", addr); 87 88 loop { 89 let (stream, _) = listener.accept().await.unwrap_or_else(|e| { 90 panic!("could not accept new connection on {addr}: {e}") 91 }); 92 let arc = arc.clone(); 93 tokio::spawn(async { 94 match RequestHandle::new(stream, arc).await { 95 Ok(handle) => match handle.handle().await { 96 Ok(info) => log::info!("{}", info), 97 Err(err) => log::warn!("{}", err), 98 }, 99 Err(log_line) => { 100 log::warn!("{}", log_line); 101 } 102 } 103 }); 104 } 105 })) 106 }; 107 108 #[cfg(unix)] 109 for socketpath in &ARGS.sockets { 110 let arc = mimetypes.clone(); 111 112 if socketpath.exists() && socketpath.metadata() 113 .expect("Failed to get existing socket metadata") 114 .file_type() 115 .is_socket() { 116 log::warn!("Socket already exists, attempting to remove {}", socketpath.display()); 117 let _ = std::fs::remove_file(socketpath); 118 } 119 120 let listener = match UnixListener::bind(socketpath) { 121 Err(e) => { 122 panic!("Failed to listen on {}: {}", socketpath.display(), e) 123 } 124 Ok(listener) => listener, 125 }; 126 127 handles.push(tokio::spawn(async move { 128 log::info!("Started listener on {}", socketpath.display()); 129 130 loop { 131 let (stream, _) = listener.accept().await.unwrap_or_else(|e| { 132 panic!("could not accept new connection on {}: {}", socketpath.display(), e) 133 }); 134 let arc = arc.clone(); 135 tokio::spawn(async { 136 match RequestHandle::new_unix(stream, arc).await { 137 Ok(handle) => match handle.handle().await { 138 Ok(info) => log::info!("{}", info), 139 Err(err) => log::warn!("{}", err), 140 }, 141 Err(log_line) => { 142 log::warn!("{}", log_line); 143 } 144 } 145 }); 146 } 147 })) 148 }; 149 150 futures_util::future::join_all(handles).await; 151 }); 152 } 153 154 type Result<T = (), E = Box<dyn Error + Send + Sync>> = std::result::Result<T, E>; 155 156 static ARGS: Lazy<Args> = Lazy::new(|| { 157 args().unwrap_or_else(|s| { 158 eprintln!("{s}"); 159 std::process::exit(1); 160 }) 161 }); 162 163 struct Args { 164 addrs: Vec<SocketAddr>, 165 #[cfg(unix)] 166 sockets: Vec<PathBuf>, 167 content_dir: PathBuf, 168 certs: Arc<certificates::CertStore>, 169 hostnames: Vec<Host>, 170 language: Option<String>, 171 serve_secret: bool, 172 log_ips: bool, 173 only_tls13: bool, 174 central_config: bool, 175 skip_port_check: bool, 176 } 177 178 fn args() -> Result<Args> { 179 let args: Vec<String> = std::env::args().collect(); 180 let mut opts = getopts::Options::new(); 181 opts.optopt( 182 "", 183 "content", 184 "Root of the content directory (default ./content/)", 185 "DIR", 186 ); 187 opts.optopt( 188 "", 189 "certs", 190 "Root of the certificate directory (default ./.certificates/)", 191 "DIR", 192 ); 193 opts.optmulti( 194 "", 195 "addr", 196 &format!("Address to listen on (default 0.0.0.0:{DEFAULT_PORT} and [::]:{DEFAULT_PORT}; multiple occurences means listening on multiple interfaces)"), 197 "IP:PORT", 198 ); 199 #[cfg(unix)] 200 opts.optmulti( 201 "", 202 "socket", 203 "Unix socket to listen on (multiple occurences means listening on multiple sockets)", 204 "PATH", 205 ); 206 opts.optmulti( 207 "", 208 "hostname", 209 "Domain name of this Gemini server, enables checking hostname and port in requests. (multiple occurences means basic vhosts)", 210 "NAME", 211 ); 212 opts.optopt( 213 "", 214 "lang", 215 "RFC 4646 Language code for text/gemini documents", 216 "LANG", 217 ); 218 opts.optflag("h", "help", "Print this help text and exit."); 219 opts.optflag("V", "version", "Print version information and exit."); 220 opts.optflag( 221 "3", 222 "only-tls13", 223 "Only use TLSv1.3 (default also allows TLSv1.2)", 224 ); 225 opts.optflag( 226 "", 227 "serve-secret", 228 "Enable serving secret files (files/directories starting with a dot)", 229 ); 230 opts.optflag("", "log-ip", "Output the remote IP address when logging."); 231 opts.optflag( 232 "C", 233 "central-conf", 234 "Use a central .meta file in the content root directory. Decentral config files will be ignored.", 235 ); 236 opts.optflag( 237 "e", 238 "ed25519", 239 "Generate keys using the Ed25519 signature algorithm instead of the default ECDSA.", 240 ); 241 opts.optflag( 242 "", 243 "skip-port-check", 244 "Skip URL port check even when a hostname is specified.", 245 ); 246 247 let matches = opts.parse(&args[1..]).map_err(|f| f.to_string())?; 248 249 if matches.opt_present("h") { 250 eprintln!("{}", opts.usage(&format!("Usage: {} [options]", &args[0]))); 251 std::process::exit(0); 252 } 253 254 if matches.opt_present("V") { 255 eprintln!("agate {}", env!("CARGO_PKG_VERSION")); 256 std::process::exit(0); 257 } 258 259 // try to open the certificate directory 260 let certs_path = matches.opt_get_default("certs", ".certificates".to_string())?; 261 let (certs, certs_path) = match check_path(certs_path.clone()) { 262 // the directory exists, try to load certificates 263 Ok(certs_path) => match certificates::CertStore::load_from(&certs_path) { 264 // all is good 265 Ok(certs) => (Some(certs), certs_path), 266 // the certificate directory did not contain certificates, but we can generate some 267 // because the hostname option was given 268 Err(certificates::CertLoadError::Empty) if matches.opt_present("hostname") => { 269 (None, certs_path) 270 } 271 // failed loading certificates or missing hostname to generate them 272 Err(e) => return Err(e.into()), 273 }, 274 // the directory does not exist 275 Err(_) => { 276 // since certificate management should be automated, we are going to create the directory too 277 log::info!( 278 "The certificate directory {:?} does not exist, creating it.", 279 certs_path 280 ); 281 std::fs::create_dir(&certs_path).expect("could not create certificate directory"); 282 // we just created the directory, skip loading from it 283 (None, PathBuf::from(certs_path)) 284 } 285 }; 286 287 // If we have not loaded any certificates yet, we have to try to reload them later. 288 // This ensures we get the right error message. 289 let mut reload_certs = certs.is_none(); 290 291 let mut hostnames = vec![]; 292 for s in matches.opt_strs("hostname") { 293 // normalize hostname, add punycoding if necessary 294 let hostname = Host::parse(&s)?; 295 296 // check if we have a certificate for that domain 297 if let Host::Domain(ref domain) = hostname { 298 if !matches!(certs, Some(ref certs) if certs.has_domain(domain)) { 299 log::info!("No certificate or key found for {:?}, generating them.", s); 300 301 let mut cert_params = CertificateParams::new(vec![domain.clone()])?; 302 cert_params 303 .distinguished_name 304 .push(DnType::CommonName, domain); 305 306 // <CertificateParams as Default>::default() already implements a 307 // date in the far future from the time of writing: 4096-01-01 308 309 let key_pair = if matches.opt_present("e") { 310 KeyPair::generate_for(&rcgen::PKCS_ED25519) 311 } else { 312 KeyPair::generate() 313 }?; 314 315 // generate the certificate with the configuration 316 let cert = cert_params.self_signed(&key_pair)?; 317 318 // make sure the certificate directory exists 319 fs::create_dir(certs_path.join(domain))?; 320 // write certificate data to disk 321 let mut cert_file = File::create(certs_path.join(format!( 322 "{}/{}", 323 domain, 324 certificates::CERT_FILE_NAME 325 )))?; 326 cert_file.write_all(cert.der())?; 327 // write key data to disk 328 let key_file_path = 329 certs_path.join(format!("{}/{}", domain, certificates::KEY_FILE_NAME)); 330 let mut key_file = File::create(&key_file_path)?; 331 #[cfg(unix)] 332 { 333 // set permissions so only owner can read 334 match key_file.set_permissions(std::fs::Permissions::from_mode(0o400)) { 335 Ok(_) => (), 336 Err(_) => log::warn!( 337 "could not set permissions for new key file {}", 338 key_file_path.display() 339 ), 340 } 341 } 342 key_file.write_all(key_pair.serialized_der())?; 343 344 reload_certs = true; 345 } 346 } 347 348 hostnames.push(hostname); 349 } 350 351 // if new certificates were generated, reload the certificate store 352 let certs = if reload_certs { 353 certificates::CertStore::load_from(&certs_path)? 354 } else { 355 // there must already have been certificates loaded 356 certs.unwrap() 357 }; 358 359 // parse listening addresses 360 let mut addrs = vec![]; 361 for i in matches.opt_strs("addr") { 362 addrs.push(i.parse()?); 363 } 364 365 #[cfg_attr(not(unix), allow(unused_mut))] 366 let mut empty = addrs.is_empty(); 367 368 #[cfg(unix)] 369 let mut sockets = vec![]; 370 #[cfg(unix)] 371 { 372 for i in matches.opt_strs("socket") { 373 sockets.push(i.parse()?); 374 } 375 376 empty &= sockets.is_empty(); 377 } 378 379 if empty { 380 addrs = vec![ 381 SocketAddr::new(IpAddr::V6(Ipv6Addr::UNSPECIFIED), DEFAULT_PORT), 382 SocketAddr::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), DEFAULT_PORT), 383 ]; 384 } 385 386 Ok(Args { 387 addrs, 388 #[cfg(unix)] 389 sockets, 390 content_dir: check_path(matches.opt_get_default("content", "content".into())?)?, 391 certs: Arc::new(certs), 392 hostnames, 393 language: matches.opt_str("lang"), 394 serve_secret: matches.opt_present("serve-secret"), 395 log_ips: matches.opt_present("log-ip"), 396 only_tls13: matches.opt_present("only-tls13"), 397 central_config: matches.opt_present("central-conf"), 398 skip_port_check: matches.opt_present("skip-port-check"), 399 }) 400 } 401 402 fn check_path(s: String) -> Result<PathBuf, String> { 403 let p = PathBuf::from(s); 404 if p.as_path().exists() { 405 Ok(p) 406 } else { 407 Err(format!("No such file: {p:?}")) 408 } 409 } 410 411 /// TLS configuration. 412 static TLS: Lazy<TlsAcceptor> = Lazy::new(acceptor); 413 414 fn acceptor() -> TlsAcceptor { 415 let config = if ARGS.only_tls13 { 416 ServerConfig::builder_with_protocol_versions(&[&TLS13]) 417 } else { 418 ServerConfig::builder() 419 } 420 .with_no_client_auth() 421 .with_cert_resolver(ARGS.certs.clone()); 422 TlsAcceptor::from(Arc::new(config)) 423 } 424 425 struct RequestHandle<T> { 426 stream: TlsStream<T>, 427 local_port_check: Option<u16>, 428 log_line: String, 429 metadata: Arc<Mutex<FileOptions>>, 430 } 431 432 impl RequestHandle<TcpStream> { 433 /// Creates a new request handle for the given stream. If establishing the TLS 434 /// session fails, returns a corresponding log line. 435 async fn new(stream: TcpStream, metadata: Arc<Mutex<FileOptions>>) -> Result<Self, String> { 436 let local_addr = stream.local_addr().unwrap().to_string(); 437 438 // try to get the remote IP address if desired 439 let peer_addr = if ARGS.log_ips { 440 stream 441 .peer_addr() 442 .map_err(|_| { 443 format!( 444 // use nonexistent status code 01 if peer IP is unknown 445 "{local_addr} - \"\" 01 \"IP error\" error:could not get peer address", 446 ) 447 })? 448 .ip() 449 .to_string() 450 } else { 451 // Do not log IP address, but something else so columns still line up. 452 "-".into() 453 }; 454 455 let log_line = format!("{local_addr} {peer_addr}",); 456 457 let local_port_check = if ARGS.skip_port_check { 458 None 459 } else { 460 Some(stream.local_addr().unwrap().port()) 461 }; 462 463 match TLS.accept(stream).await { 464 Ok(stream) => Ok(Self { 465 stream, 466 local_port_check, 467 log_line, 468 metadata, 469 }), 470 // use nonexistent status code 00 if connection was not established 471 Err(e) => Err(format!("{log_line} \"\" 00 \"TLS error\" error:{e}")), 472 } 473 } 474 } 475 476 #[cfg(unix)] 477 impl RequestHandle<UnixStream> { 478 async fn new_unix( 479 stream: UnixStream, 480 metadata: Arc<Mutex<FileOptions>>, 481 ) -> Result<Self, String> { 482 let log_line = format!( 483 "unix:{} -", 484 stream 485 .local_addr() 486 .ok() 487 .and_then(|addr| Some(addr.as_pathname()?.to_string_lossy().into_owned())) 488 .unwrap_or_default() 489 ); 490 491 match TLS.accept(stream).await { 492 Ok(stream) => Ok(Self { 493 stream, 494 // TODO add port check for unix sockets, requires extra arg for port 495 local_port_check: None, 496 log_line, 497 metadata, 498 }), 499 // use nonexistent status code 00 if connection was not established 500 Err(e) => Err(format!("{} \"\" 00 \"TLS error\" error:{}", log_line, e)), 501 } 502 } 503 } 504 505 impl<T> RequestHandle<T> 506 where 507 T: AsyncWriteExt + AsyncReadExt + Unpin, 508 { 509 /// Do the necessary actions to handle this request. Returns a corresponding 510 /// log line as Err or Ok, depending on if the request finished with or 511 /// without errors. 512 async fn handle(mut self) -> Result<String, String> { 513 // not already in error condition 514 let result = match self.parse_request().await { 515 Ok(url) => self.send_response(url).await, 516 Err((status, msg)) => self.send_header(status, msg).await, 517 }; 518 519 let close_result = self.stream.shutdown().await; 520 521 match (result, close_result) { 522 (Err(e), _) => Err(format!("{} error:{}", self.log_line, e)), 523 (Ok(_), Err(e)) => Err(format!("{} error:{}", self.log_line, e)), 524 (Ok(_), Ok(_)) => Ok(self.log_line), 525 } 526 } 527 528 /// Return the URL requested by the client. 529 async fn parse_request(&mut self) -> std::result::Result<Url, (u8, &'static str)> { 530 // Because requests are limited to 1024 bytes (plus 2 bytes for CRLF), we 531 // can use a fixed-sized buffer on the stack, avoiding allocations and 532 // copying, and stopping bad clients from making us use too much memory. 533 let mut request = [0; 1026]; 534 let mut buf = &mut request[..]; 535 let mut len = 0; 536 537 // Read until CRLF, end-of-stream, or there's no buffer space left. 538 // 539 // Since neither CR nor LF can be part of a URI according to 540 // ISOC-RFC 3986, we could use BufRead::read_line here, but that does 541 // not allow us to cap the number of read bytes at 1024+2. 542 let result = loop { 543 let Ok(bytes_read) = self.stream.read(buf).await else { 544 break Err((BAD_REQUEST, "Request ended unexpectedly")); 545 }; 546 len += bytes_read; 547 if request[..len].ends_with(b"\r\n") { 548 break Ok(()); 549 } else if bytes_read == 0 { 550 break Err((BAD_REQUEST, "Request ended unexpectedly")); 551 } 552 buf = &mut request[len..]; 553 } 554 .and_then(|()| { 555 std::str::from_utf8(&request[..len - 2]).or(Err((BAD_REQUEST, "Non-UTF-8 request"))) 556 }); 557 558 let request = result.map_err(|e| { 559 // write empty request to log line for uniformity 560 write!(self.log_line, " \"\"").unwrap(); 561 e 562 })?; 563 564 // log literal request (might be different from or not an actual URL) 565 write!(self.log_line, " \"{request}\"").unwrap(); 566 567 let mut url = Url::parse(request).or(Err((BAD_REQUEST, "Invalid URL")))?; 568 569 // Validate the URL: 570 // correct scheme 571 if url.scheme() != "gemini" { 572 return Err((PROXY_REQUEST_REFUSED, "Unsupported URL scheme")); 573 } 574 575 // no userinfo and no fragment 576 if url.password().is_some() || !url.username().is_empty() || url.fragment().is_some() { 577 return Err((BAD_REQUEST, "URL contains fragment or userinfo")); 578 } 579 580 // correct host 581 let Some(domain) = url.domain() else { 582 return Err((BAD_REQUEST, "URL does not contain a domain")); 583 }; 584 // because the gemini scheme is not special enough for WHATWG, normalize 585 // it ourselves 586 let host = Host::parse( 587 &percent_decode_str(domain) 588 .decode_utf8() 589 .or(Err((BAD_REQUEST, "Invalid URL")))?, 590 ) 591 .or(Err((BAD_REQUEST, "Invalid URL")))?; 592 // TODO: simplify when <https://github.com/servo/rust-url/issues/586> resolved 593 url.set_host(Some(&host.to_string())) 594 .expect("invalid domain?"); 595 // do not use "contains" here since it requires the same type and does 596 // not allow to check for Host<&str> if the vec contains Hostname<String> 597 if !ARGS.hostnames.is_empty() && !ARGS.hostnames.iter().any(|h| h == &host) { 598 return Err((PROXY_REQUEST_REFUSED, "Proxy request refused")); 599 } 600 601 // correct port 602 if let Some(expected_port) = self.local_port_check { 603 if let Some(port) = url.port() { 604 // Validate that the port in the URL is the same as for the stream this request 605 // came in on. 606 if port != expected_port { 607 return Err((PROXY_REQUEST_REFUSED, "Proxy request refused")); 608 } 609 } 610 } 611 Ok(url) 612 } 613 614 /// Send the client the file located at the requested URL. 615 async fn send_response(&mut self, url: Url) -> Result { 616 let mut path = std::path::PathBuf::from(&ARGS.content_dir); 617 618 if ARGS.hostnames.len() > 1 { 619 // basic vhosts, existence of host_str was checked by parse_request already 620 path.push(url.host_str().expect("no hostname")); 621 } 622 623 if let Some(mut segments) = url.path_segments() { 624 // append percent-decoded path segments 625 for segment in segments.clone() { 626 // To prevent directory traversal attacks, we need to 627 // check that each filesystem path component in the URL 628 // path segment is a normal component (not the root 629 // directory, the parent directory, a drive label, or 630 // another special component). Furthermore, since path 631 // separators (e.g. the escaped forward slash %2F) in a 632 // single URL path segment are non-structural, the URL 633 // path segment should not contain multiple filesystem 634 // path components. 635 let decoded = percent_decode_str(segment).decode_utf8()?; 636 let mut components = Path::new(decoded.as_ref()).components(); 637 // the first component must be a normal component; if 638 // so, push it onto the PathBuf 639 match components.next() { 640 None => (), 641 Some(Component::Normal(c)) => path.push(c), 642 Some(_) => return self.send_header(NOT_FOUND, "Not found, sorry.").await, 643 } 644 // there must not be more than one component 645 if components.next().is_some() { 646 return self.send_header(NOT_FOUND, "Not found, sorry.").await; 647 } 648 // even if it's one component, there may be trailing path 649 // separators at the end 650 if decoded.ends_with(path::is_separator) { 651 return self.send_header(NOT_FOUND, "Not found, sorry.").await; 652 } 653 } 654 // check if hiding files is disabled 655 if !ARGS.serve_secret 656 // there is a configuration for this file, assume it should be served 657 && !self.metadata.lock().await.exists(&path) 658 // check if file or directory is hidden 659 && segments.any(|segment| segment.starts_with('.')) 660 { 661 return self 662 .send_header(GONE, "If I told you, it would not be a secret.") 663 .await; 664 } 665 } 666 667 if let Ok(metadata) = tokio::fs::metadata(&path).await { 668 if metadata.is_dir() { 669 if url.path().ends_with('/') || url.path().is_empty() { 670 // if the path ends with a slash or the path is empty, the links will work the same 671 // without a redirect 672 // use `push` instead of `join` because the changed path is used later 673 path.push("index.gmi"); 674 if !path.exists() { 675 path.pop(); 676 // try listing directory 677 return self.list_directory(&path).await; 678 } 679 } else { 680 // if client is not redirected, links may not work as expected without trailing slash 681 let mut url = url; 682 url.set_path(&format!("{}/", url.path())); 683 return self.send_header(REDIRECT_PERMANENT, url.as_str()).await; 684 } 685 } 686 } 687 688 let data = self.metadata.lock().await.get(&path); 689 690 if let PresetMeta::FullHeader(status, meta) = data { 691 self.send_header(status, &meta).await?; 692 // do not try to access the file 693 return Ok(()); 694 } 695 696 // Make sure the file opens successfully before sending a success header. 697 let mut file = match tokio::fs::File::open(&path).await { 698 Ok(file) => file, 699 Err(e) => { 700 self.send_header(NOT_FOUND, "Not found, sorry.").await?; 701 return Err(e.into()); 702 } 703 }; 704 705 // Send header. 706 let mime = match data { 707 // this was already handled before opening the file 708 PresetMeta::FullHeader(..) => unreachable!(), 709 // treat this as the full MIME type 710 PresetMeta::FullMime(mime) => mime.clone(), 711 // guess the MIME type and add the parameters 712 PresetMeta::Parameters(params) => { 713 if path.extension() == Some(OsStr::new("gmi")) { 714 format!("text/gemini{params}") 715 } else { 716 let mime = mime_guess::from_path(&path).first_or_octet_stream(); 717 format!("{}{}", mime.essence_str(), params) 718 } 719 } 720 }; 721 self.send_header(SUCCESS, &mime).await?; 722 723 // Send body. 724 tokio::io::copy(&mut file, &mut self.stream).await?; 725 Ok(()) 726 } 727 728 async fn list_directory(&mut self, path: &Path) -> Result { 729 // https://url.spec.whatwg.org/#path-percent-encode-set 730 const ENCODE_SET: AsciiSet = CONTROLS 731 .add(b' ') 732 .add(b'"') 733 .add(b'#') 734 .add(b'<') 735 .add(b'>') 736 .add(b'?') 737 .add(b'`') 738 .add(b'{') 739 .add(b'}'); 740 741 // check if directory listing is enabled by getting preamble 742 let Ok(preamble) = std::fs::read_to_string(path.join(".directory-listing-ok")) else { 743 self.send_header(NOT_FOUND, "Directory index disabled.") 744 .await?; 745 return Ok(()); 746 }; 747 748 log::info!("Listing directory {:?}", path); 749 750 self.send_header(SUCCESS, "text/gemini").await?; 751 self.stream.write_all(preamble.as_bytes()).await?; 752 753 let mut entries = tokio::fs::read_dir(path).await?; 754 let mut lines = vec![]; 755 while let Some(entry) = entries.next_entry().await? { 756 let mut name = entry 757 .file_name() 758 .into_string() 759 .or(Err("Non-Unicode filename"))?; 760 if name.starts_with('.') { 761 continue; 762 } 763 if entry.file_type().await?.is_dir() { 764 name += "/"; 765 } 766 let line = match percent_encode(name.as_bytes(), &ENCODE_SET).into() { 767 Cow::Owned(url) => format!("=> {url} {name}\n"), 768 Cow::Borrowed(url) => format!("=> {url}\n"), // url and name are identical 769 }; 770 lines.push(line); 771 } 772 lines.sort(); 773 for line in lines { 774 self.stream.write_all(line.as_bytes()).await?; 775 } 776 Ok(()) 777 } 778 779 async fn send_header(&mut self, status: u8, meta: &str) -> Result { 780 // add response status and response meta 781 write!(self.log_line, " {status} \"{meta}\"")?; 782 783 self.stream 784 .write_all(format!("{status} {meta}\r\n").as_bytes()) 785 .await?; 786 Ok(()) 787 } 788 }