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
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
|
/*
MORGEMRS :: Morgana's Gemini Rust Server
Copyright (C) 2026 Morgana <morgana@icolotl.com>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
use std::fs::{self, DirEntry};
use std::io::{Read, Write};
use std::net::{Ipv4Addr, TcpListener, TcpStream};
use std::path::{Path, PathBuf};
use std::sync::Arc;
use rustls::{
ServerConfig, ServerConnection, Stream,
pki_types::{CertificateDer, PrivateKeyDer, pem::PemObject},
};
// Change this to the proper domain and IP as required
const HOST: Ipv4Addr = Ipv4Addr::new(127, 0, 0, 1);
const PREFIX: &str = "gemini://localhost";
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((HOST, 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(PREFIX)
else {
let _ = writeln!(tls_stream, "53 Proxy Not Supported\r\n");
continue;
};
// Clean up URI
uri = uri.trim_matches('/');
// Ensure file exists and remove intermediate uri components
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);
}
|