summaryrefslogtreecommitdiff
path: root/src/main.rs
blob: b25cc07a012fcb82421790a438663f4ae63be110 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
use std::fs::{self, DirEntry};
use std::io::Write;
use std::net::TcpStream;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use std::{io::Read, net::TcpListener};

use rustls::ServerConnection;
use rustls::{
    ServerConfig, Stream,
    pki_types::{CertificateDer, PrivateKeyDer, pem::PemObject},
};

fn main() {
    println!("Starting MorGemRS Server...");

    // Path to Gemini content to be hosted
    let root: PathBuf = fs::canonicalize("content").unwrap();

    // Load server certificate chains and private keys
    let certs = CertificateDer::pem_file_iter("cert/morgemrs.crt")
        .unwrap()
        .map(|cert| cert.unwrap())
        .collect();
    let private_key = PrivateKeyDer::from_pem_file("cert/morgemrs.key").unwrap();
    let config = Arc::new(
        ServerConfig::builder()
            .with_no_client_auth()
            .with_single_cert(certs, private_key)
            .unwrap(),
    );

    // Begin listening for connections
    let listener = TcpListener::bind("127.0.0.1:1965").unwrap();
    println!("Successfully Started!");
    for mut tcp_stream in listener.incoming().flatten() {
        // Connect with TLS
        let mut conn = ServerConnection::new(config.clone()).unwrap();
        let mut tls_stream = Stream::new(&mut conn, &mut tcp_stream);
        // Gemini URIs can be 1024 bytes long + 2 CRLF bytes + 1 overflow byte == 1027b
        let mut buffer = [0; 1027];

        // Did we successfully recieve data?
        let Ok(n) = tls_stream.read(&mut buffer[..]) else {
            let _ = writeln!(tls_stream, "59\r\n");
            continue;
        };

        // Did we recieve too much content?
        if n == 1027 {
            let _ = writeln!(tls_stream, "59\r\n");
            continue;
        }

        // Is the data utf-8?
        let Ok(raw_uri) = String::from_utf8(buffer.to_vec()) else {
            let _ = writeln!(tls_stream, "59\r\n");
            continue;
        };

        // Trim nulls and whitespace from buffer
        // Reject proxy requests
        let Some(mut uri) = raw_uri
            .trim_end_matches(|c: char| c == char::from(0) || c.is_whitespace())
            .strip_prefix("gemini://localhost")
        else {
            let _ = writeln!(tls_stream, "53 Proxy Not Supported\r\n");
            continue;
        };

        // Clean up URI
        uri = uri.trim_matches('/');
        let Ok(path) = root.join(uri).canonicalize() else {
            let _ = writeln!(tls_stream, "51 Not Found\r\n");
            continue;
        };

        // Prevent access to files not in the route
        if !path.starts_with(&root) {
            let _ = writeln!(tls_stream, "51 Not Found\rpath\n");
            continue;
        }

        // All checks passed, serve the content
        if path.is_dir() {
            serve_dir(&path, uri, &mut tls_stream);
        } else if path.is_file() {
            serve_file(&path, &mut tls_stream);
        } else {
            let _ = writeln!(tls_stream, "51 Not Found\r\n");
        }
    }
}

fn serve_dir(dir: &Path, uri: &str, tls_stream: &mut Stream<'_, ServerConnection, TcpStream>) {
    // If the directory has a `index.gem` file, serve that instead.
    let index_path = dir.join("index.gem");
    if index_path.is_file() {
        serve_file(&index_path, tls_stream);
        return;
    }

    // Produce a list of links to directory contents
    let Ok(read_dir) = dir.read_dir() else {
        let _ = writeln!(tls_stream, "50 Server Error\r\n");
        return;
    };
    let contents: Vec<DirEntry> = read_dir.flatten().collect();
    let content_string = contents
        .iter()
        .flat_map(|f| f.file_name().into_string())
        .map(|s| {
            if uri.is_empty() {
                format!("=> /{}", s) // Prevent leading double slash with empty URI
            } else {
                format!("=> /{}/{} ", uri, s)
            }
        })
        .collect::<Vec<String>>()
        .join("\r\n");
    let _ = writeln!(tls_stream, "20 text/gemini\r\n{}", content_string);
}

fn serve_file(file: &Path, tls_stream: &mut Stream<'_, ServerConnection, TcpStream>) {
    // Serve file contents (naive, assumes gemtext)
    let Ok(contents) = fs::read_to_string(file) else {
        let _ = writeln!(tls_stream, "50 Server Error\r\n");
        return;
    };
    let _ = writeln!(tls_stream, "20 text/gemini\r\n{}", contents);
}