agate

Simple gemini server for static files
git clone https://github.com/mbrubeck/agate.git
Log | Files | Refs | README

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 }