Initial commit.

This commit is contained in:
2024-08-17 13:49:53 +03:00
commit e989633e03
16 changed files with 1114 additions and 0 deletions

8
.gitignore vendored Normal file
View File

@@ -0,0 +1,8 @@
/target
# Added by cargo
#
# already existing elements were commented out
#/target

5
.idea/.gitignore generated vendored Normal file
View File

@@ -0,0 +1,5 @@
# Default ignored files
/shelf/
/workspace.xml
# Editor-based HTTP Client requests
/httpRequests/

11
.idea/gemini-grs.iml generated Normal file
View File

@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="EMPTY_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" />
<excludeFolder url="file://$MODULE_DIR$/target" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

8
.idea/modules.xml generated Normal file
View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/gemini-grs.iml" filepath="$PROJECT_DIR$/.idea/gemini-grs.iml" />
</modules>
</component>
</project>

6
.idea/vcs.xml generated Normal file
View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" />
</component>
</project>

551
Cargo.lock generated Normal file
View File

@@ -0,0 +1,551 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 3
[[package]]
name = "aho-corasick"
version = "1.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916"
dependencies = [
"memchr",
]
[[package]]
name = "anstream"
version = "0.6.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "64e15c1ab1f89faffbf04a634d5e1962e9074f2741eef6d97f3c4e322426d526"
dependencies = [
"anstyle",
"anstyle-parse",
"anstyle-query",
"anstyle-wincon",
"colorchoice",
"is_terminal_polyfill",
"utf8parse",
]
[[package]]
name = "anstyle"
version = "1.0.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1bec1de6f59aedf83baf9ff929c98f2ad654b97c9510f4e70cf6f661d49fd5b1"
[[package]]
name = "anstyle-parse"
version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eb47de1e80c2b463c735db5b217a0ddc39d612e7ac9e2e96a5aed1f57616c1cb"
dependencies = [
"utf8parse",
]
[[package]]
name = "anstyle-query"
version = "1.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6d36fc52c7f6c869915e99412912f22093507da8d9e942ceaf66fe4b7c14422a"
dependencies = [
"windows-sys",
]
[[package]]
name = "anstyle-wincon"
version = "3.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5bf74e1b6e971609db8ca7a9ce79fd5768ab6ae46441c572e46cf596f59e57f8"
dependencies = [
"anstyle",
"windows-sys",
]
[[package]]
name = "anyhow"
version = "1.0.86"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da"
[[package]]
name = "bitflags"
version = "2.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de"
[[package]]
name = "byteorder"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
[[package]]
name = "cc"
version = "1.1.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72db2f7947ecee9b03b510377e8bb9077afa27176fdbff55c51027e976fdcc48"
dependencies = [
"shlex",
]
[[package]]
name = "cfg-if"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "colorchoice"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d3fd119d74b830634cea2a0f58bbd0d54540518a14397557951e79340abc28c0"
[[package]]
name = "env_filter"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4f2c92ceda6ceec50f43169f9ee8424fe2db276791afde7b2cd8bc084cb376ab"
dependencies = [
"log",
"regex",
]
[[package]]
name = "env_logger"
version = "0.11.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e13fa619b91fb2381732789fc5de83b45675e882f66623b7d8cb4f643017018d"
dependencies = [
"anstream",
"anstyle",
"env_filter",
"humantime",
"log",
]
[[package]]
name = "foreign-types"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1"
dependencies = [
"foreign-types-shared",
]
[[package]]
name = "foreign-types-shared"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b"
[[package]]
name = "form_urlencoded"
version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456"
dependencies = [
"percent-encoding",
]
[[package]]
name = "gemini-grs"
version = "0.1.0"
dependencies = [
"anyhow",
"env_logger",
"hex",
"log",
"nanoid",
"openssl",
"path-clean",
"url",
]
[[package]]
name = "getrandom"
version = "0.2.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7"
dependencies = [
"cfg-if",
"libc",
"wasi",
]
[[package]]
name = "hex"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
[[package]]
name = "humantime"
version = "2.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4"
[[package]]
name = "idna"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6"
dependencies = [
"unicode-bidi",
"unicode-normalization",
]
[[package]]
name = "is_terminal_polyfill"
version = "1.70.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf"
[[package]]
name = "libc"
version = "0.2.156"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a5f43f184355eefb8d17fc948dbecf6c13be3c141f20d834ae842193a448c72a"
[[package]]
name = "log"
version = "0.4.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24"
[[package]]
name = "memchr"
version = "2.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
[[package]]
name = "nanoid"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3ffa00dec017b5b1a8b7cf5e2c008bfda1aa7e0697ac1508b491fdf2622fb4d8"
dependencies = [
"rand",
]
[[package]]
name = "once_cell"
version = "1.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92"
[[package]]
name = "openssl"
version = "0.10.66"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9529f4786b70a3e8c61e11179af17ab6188ad8d0ded78c5529441ed39d4bd9c1"
dependencies = [
"bitflags",
"cfg-if",
"foreign-types",
"libc",
"once_cell",
"openssl-macros",
"openssl-sys",
]
[[package]]
name = "openssl-macros"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "openssl-sys"
version = "0.9.103"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f9e8deee91df40a943c71b917e5874b951d32a802526c85721ce3b776c929d6"
dependencies = [
"cc",
"libc",
"pkg-config",
"vcpkg",
]
[[package]]
name = "path-clean"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "17359afc20d7ab31fdb42bb844c8b3bb1dabd7dcf7e68428492da7f16966fcef"
[[package]]
name = "percent-encoding"
version = "2.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e"
[[package]]
name = "pkg-config"
version = "0.3.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec"
[[package]]
name = "ppv-lite86"
version = "0.2.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04"
dependencies = [
"zerocopy",
]
[[package]]
name = "proc-macro2"
version = "1.0.86"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.36"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7"
dependencies = [
"proc-macro2",
]
[[package]]
name = "rand"
version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
dependencies = [
"libc",
"rand_chacha",
"rand_core",
]
[[package]]
name = "rand_chacha"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
dependencies = [
"ppv-lite86",
"rand_core",
]
[[package]]
name = "rand_core"
version = "0.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
dependencies = [
"getrandom",
]
[[package]]
name = "regex"
version = "1.10.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4219d74c6b67a3654a9fbebc4b419e22126d13d2f3c4a07ee0cb61ff79a79619"
dependencies = [
"aho-corasick",
"memchr",
"regex-automata",
"regex-syntax",
]
[[package]]
name = "regex-automata"
version = "0.4.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "38caf58cc5ef2fed281f89292ef23f6365465ed9a41b7a7754eb4e26496c92df"
dependencies = [
"aho-corasick",
"memchr",
"regex-syntax",
]
[[package]]
name = "regex-syntax"
version = "0.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b"
[[package]]
name = "shlex"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
[[package]]
name = "syn"
version = "2.0.74"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fceb41e3d546d0bd83421d3409b1460cc7444cd389341a4c880fe7a042cb3d7"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "tinyvec"
version = "1.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "445e881f4f6d382d5f27c034e25eb92edd7c784ceab92a0937db7f2e9471b938"
dependencies = [
"tinyvec_macros",
]
[[package]]
name = "tinyvec_macros"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
[[package]]
name = "unicode-bidi"
version = "0.3.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08f95100a766bf4f8f28f90d77e0a5461bbdb219042e7679bebe79004fed8d75"
[[package]]
name = "unicode-ident"
version = "1.0.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b"
[[package]]
name = "unicode-normalization"
version = "0.1.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a56d1686db2308d901306f92a263857ef59ea39678a5458e7cb17f01415101f5"
dependencies = [
"tinyvec",
]
[[package]]
name = "url"
version = "2.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "22784dbdf76fdde8af1aeda5622b546b422b6fc585325248a2bf9f5e41e94d6c"
dependencies = [
"form_urlencoded",
"idna",
"percent-encoding",
]
[[package]]
name = "utf8parse"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
[[package]]
name = "vcpkg"
version = "0.2.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
[[package]]
name = "wasi"
version = "0.11.0+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
[[package]]
name = "windows-sys"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
dependencies = [
"windows-targets",
]
[[package]]
name = "windows-targets"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
dependencies = [
"windows_aarch64_gnullvm",
"windows_aarch64_msvc",
"windows_i686_gnu",
"windows_i686_gnullvm",
"windows_i686_msvc",
"windows_x86_64_gnu",
"windows_x86_64_gnullvm",
"windows_x86_64_msvc",
]
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
[[package]]
name = "windows_aarch64_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
[[package]]
name = "windows_i686_gnu"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
[[package]]
name = "windows_i686_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
[[package]]
name = "windows_i686_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
[[package]]
name = "windows_x86_64_gnu"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
[[package]]
name = "windows_x86_64_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
[[package]]
name = "zerocopy"
version = "0.7.35"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0"
dependencies = [
"byteorder",
"zerocopy-derive",
]
[[package]]
name = "zerocopy-derive"
version = "0.7.35"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e"
dependencies = [
"proc-macro2",
"quote",
"syn",
]

14
Cargo.toml Normal file
View File

@@ -0,0 +1,14 @@
[package]
name = "gemini-grs"
version = "0.1.0"
edition = "2021"
[dependencies]
anyhow = "1.0.86"
env_logger = "0.11.5"
hex = "0.4.3"
log = "0.4.22"
nanoid = "0.4.0"
openssl = "0.10.66"
path-clean = "1.0.1"
url = "2.5.2"

51
README.md Normal file
View File

@@ -0,0 +1,51 @@
# gemini-grs
A Gemini protocol server written in Rust.
## TODO
- [x] Configuration via environment variables
- [x] Proper logging
- [x] Serve images
- [ ] Read and send file in chunks instead of serving whole file from memory
- [ ] Expand to serve other protocols (spartan, scroll, titan etc.)
- [ ] Async I/O?
## External dependencies
OpenSSL
## Building
```shell
cargo build --release
```
## Running
Generate your TLS keys:
```shell
openssl genrsa -out key.pem 2048
openssl req -new -key key.pem -out request.pem
openssl x509 -req -days 36500 -in request.pem -signkey key.pem -out cert.pem
```
Environment variables with examples:
- GEMINI_SERVER_HOSTNAME="0.0.0.0:1965"
- GEMINI_SERVER_TLS_KEY_FILENAME="/where/you/put/key.pem"
- GEMINI_SERVER_TLS_CERT_FILENAME="/where/you/put/cert.pem"
- GEMINI_SERVER_ROOT_DIRECTORY="/files/to/serve"
- RUST_LOG="debug"
Example command:
```shell
GEMINI_SERVER_TLS_KEY_FILENAME=/home/user/server/key.pem \
GEMINI_SERVER_TLS_CERT_FILENAME=/home/user/server/cert.pem \
GEMINI_SERVER_ROOT_DIRECTORY=/gemini-root \
RUST_LOG=debug \
./target/release/gemini-grs
```

9
src/fs.rs Normal file
View File

@@ -0,0 +1,9 @@
use std::fs::File;
use std::io::{self, Read};
pub fn read_file_as_bytes(path: &str) -> io::Result<Vec<u8>> {
let mut file = File::open(path)?;
let mut buffer = Vec::new();
file.read_to_end(&mut buffer)?;
Ok(buffer)
}

63
src/gemini/mod.rs Normal file
View File

@@ -0,0 +1,63 @@
use std::fmt;
use std::fmt::Debug;
use anyhow::{anyhow, Context, Result};
use url::Url;
pub mod server;
pub mod tls;
#[derive(Debug)]
pub struct GeminiUrl {
pub scheme: String,
pub hostname: String,
pub port: u16,
pub path: String,
}
impl fmt::Display for GeminiUrl {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"{}://{}:{}{}",
self.scheme, self.hostname, self.port, self.path
)
}
}
impl GeminiUrl {
/// Default scheme: "gemini://"
/// Port: 1965
/// Path: "/"
pub fn new(input: &str) -> Result<GeminiUrl> {
let input_with_scheme = if input.contains("://") {
input.to_string()
} else {
format!("gemini://{}", input)
};
let parsed = Url::parse(&input_with_scheme)
.with_context(|| format!("Invalid request URL {}", &input_with_scheme))?;
if parsed.scheme() != "gemini" {
return Err(anyhow!(
"Invalid request (URL protocol should be gemini://)"
));
}
let hostname = parsed
.host_str()
.ok_or_else(|| anyhow!("Invalid request (URL must have a host)"))?;
let mut parsed = GeminiUrl {
scheme: parsed.scheme().to_string(),
hostname: hostname.to_string(),
port: parsed.port().unwrap_or(1965),
path: parsed.path().to_string(),
};
if parsed.path.is_empty() {
parsed.path = String::from("/")
};
Ok(parsed)
}
pub fn to_url(&self) -> Url {
Url::parse(&self.to_string()).unwrap()
}
}

233
src/gemini/server.rs Normal file
View File

@@ -0,0 +1,233 @@
use std::io::{Read, Write};
use std::net::{IpAddr, TcpListener, TcpStream};
use std::path::PathBuf;
use std::thread;
use anyhow::{anyhow, Context, Result};
use log;
use log::{error, info};
use openssl::ssl::{SslAcceptor, SslStream};
use path_clean::PathClean;
use crate::fs::read_file_as_bytes;
use crate::gemini::{GeminiUrl, tls};
use crate::nanoid::nanoid;
use crate::time::now_unix_millis;
struct GeminiSession {
pub ip: IpAddr,
pub tls_id: Option<String>,
pub stream: SslStream<TcpStream>,
pub id: String,
pub timestamp: u128,
}
struct GeminiResponse {
code: u8,
mime_type: Option<String>,
data: Option<Vec<u8>>,
}
impl GeminiResponse {
pub fn new(
srv_root: &str,
gemini_session: &GeminiSession,
path: &String,
) -> Result<GeminiResponse> {
let filename = resolve_path(srv_root, path)?;
if !filename.is_file() {
return Ok(GeminiResponse {
code: 51, // NOT FOUND
mime_type: None,
data: None,
});
}
let mime_type = Self::guess_mime_type(PathBuf::from(&filename));
if mime_type.is_none() {
log_info(
gemini_session,
&format!("Cannot guess mime type {:?}", filename),
);
return Ok(GeminiResponse {
code: 51,
mime_type: None,
data: None,
});
}
log_info(gemini_session, &format!("Opening file {:?}", filename));
let file_str = filename
.to_str()
.ok_or_else(|| anyhow!("Failed to convert path to string"))?;
// let data = read_file_to_string(file_str)
// .with_context(|| format!("Failed to read file {}", path))?;
let data = read_file_as_bytes(file_str)
.with_context(|| format!("Failed to read file {}", path))?;
Ok(GeminiResponse {
code: 20,
mime_type,
data: Some(data),
})
}
fn guess_mime_type(path: PathBuf) -> Option<String> {
match path.extension() {
Some(e) => match e.to_str().unwrap_or("") {
"gmi" => Some(String::from("text/gemini")),
"jpg" => Some(String::from("image/jpeg")),
"jpeg" => Some(String::from("image/jpeg")),
"png" => Some(String::from("image/png")),
_ => None,
},
None => None,
}
}
pub fn get_bytes(&self) -> Vec<u8> {
let header = match &self.mime_type {
Some(e) => format!("{}\t{}\r\n", self.code, e),
None => format!("{}\r\n", self.code),
};
// Convert the header to bytes and prepare the response vector
let mut response_bytes = header.into_bytes();
// Extend the response with the data if it exists
if let Some(e) = &self.data {
response_bytes.extend(e);
}
response_bytes
}
}
pub fn start_server(
bind_address: &str,
key_file: &str,
cert_file: &str,
srv_root: &str,
) -> Result<()> {
let acceptor = tls::create_tls_acceptor(key_file, cert_file).context("TLS error")?;
let listener = TcpListener::bind(bind_address).context("TCP bind error")?;
info!("Gemini server listening to {}", &bind_address);
for stream in listener.incoming() {
// The stream will be dropped at the end of
// each loop iteration, which will close
// the stream automatically. No housekeeping
// necessary.
// Each stream is handled in a new thread.
// Async is under consideration for now.
match stream {
Err(e) => {
error!("Failed to accept connection: {:?}", e);
}
Ok(stream) => {
let acceptor = acceptor.clone();
let srv_root = srv_root.to_string();
let handle = thread::spawn(move || {
if let Err(e) = initiate_connection(&acceptor, stream, &srv_root) {
error!("Connection handling error: {:?}", e);
}
});
if let Err(e) = handle.join() {
error!("Thread join error while handling stream: {:?}", e);
}
}
}
}
Ok(())
}
fn log_info(session: &GeminiSession, message: &str) {
info!("{} {} {}", session.id, session.ip.to_string(), message);
}
/// Resolve final absolute path
/// of the file to serve.
/// Final path must always begin with
/// root path (no escape outside root!).
/// If final path is a directory, append
/// index.gmi.
fn resolve_path(root_path: &str, input: &String) -> Result<PathBuf> {
let path1 = PathBuf::from(root_path);
let mut path2 = PathBuf::from(input);
if path2.is_absolute() {
path2 = PathBuf::from(path2.strip_prefix("/")?);
}
let mut final_path = path1.join(path2).clean();
if !final_path.starts_with(path1) {
return Err(anyhow!("Invalid path {:?} -> {:?}", input, final_path));
}
if final_path.is_dir() {
final_path = final_path.join("index.gmi");
}
Ok(final_path)
}
fn handle_gemini_session(srv_root: &str, gemini_session: &mut GeminiSession) -> Result<()> {
let mut buffer = [0; 1024];
let bytes_read = gemini_session
.stream
.read(&mut buffer)
.context("Invalid request (failed to read input stream)")?;
let received = String::from_utf8(Vec::from(&buffer[..bytes_read]))
.context("Invalid request (failed to convert input to UTF8)")?;
let request_data = received.trim();
let url = GeminiUrl::new(request_data)?;
log_info(
gemini_session,
&format!(
"New request from {}{} {}",
gemini_session.ip,
match &gemini_session.tls_id {
Some(x) => format!(" TLS digest {}", x),
None => String::from(""),
},
request_data
),
);
let response = GeminiResponse::new(srv_root, gemini_session, &url.path)?;
let response_raw = response.get_bytes();
log_info(
gemini_session,
&format!(
"Reply Code {} Response length {} bytes",
response.code,
response_raw.len()
),
);
gemini_session
.stream
.write_all(&response_raw)
.context("Failed to write response to stream")?;
Ok(())
}
fn initiate_connection(acceptor: &SslAcceptor, stream: TcpStream, srv_root: &str) -> Result<()> {
let timestamp1 = now_unix_millis();
let ip = stream
.peer_addr()
.context("Failed to get peer IP address from TCP stream")?
.ip();
let stream = acceptor
.accept(stream)
.with_context(|| format!("{} Failed to establish SSL connection", ip))?;
let mut gemini_session = GeminiSession {
ip,
tls_id: tls::get_peer_certificate_digest(&stream),
stream,
id: nanoid(),
timestamp: now_unix_millis(),
};
handle_gemini_session(srv_root, &mut gemini_session)?;
log_info(
&gemini_session,
&format!("Finished ({}ms)", now_unix_millis() - timestamp1),
);
Ok(())
}

70
src/gemini/tls.rs Normal file
View File

@@ -0,0 +1,70 @@
use std::net::TcpStream;
use hex::ToHex;
use openssl::hash::MessageDigest;
use openssl::ssl::{SslAcceptor, SslFiletype, SslMethod, SslStream, SslVerifyMode};
pub fn create_tls_acceptor(key_filename: &str, cert_filename: &str) -> anyhow::Result<SslAcceptor> {
let mut acceptor = SslAcceptor::mozilla_intermediate(SslMethod::tls())?;
acceptor.set_private_key_file(key_filename, SslFiletype::PEM)?;
acceptor.set_certificate_chain_file(cert_filename)?;
// Enable peer verification
acceptor.set_verify(SslVerifyMode::PEER);
// Custom verification callback
acceptor.set_verify_callback(SslVerifyMode::PEER, |preverify_ok, x509_ctx| {
if preverify_ok {
return true;
}
// Check if it's a self-signed certificate
let cert = match x509_ctx.current_cert() {
Some(cert) => cert,
None => return false,
};
let issuer_name = cert.issuer_name().to_der();
let subject_name = cert.subject_name().to_der();
let is_self_signed = match (issuer_name, subject_name) {
(Ok(issuer), Ok(subject)) => issuer == subject,
_ => return false,
};
if is_self_signed {
let public_key = match cert.public_key() {
Ok(key) => key,
Err(_) => return false,
};
if cert.verify(&public_key).is_ok() {
let now = match openssl::asn1::Asn1Time::days_from_now(0) {
Ok(time) => time,
Err(_) => return false,
};
if cert.not_before() <= now && cert.not_after() >= now {
return true;
}
}
}
false
});
// Initialize the session ID context
let context = &crate::nanoid::nanoid().into_bytes();
acceptor.set_session_id_context(context)?;
acceptor.check_private_key()?;
Ok(acceptor.build())
}
pub fn get_peer_certificate_digest(stream: &SslStream<TcpStream>) -> Option<String> {
if let Some(cert) = stream.ssl().peer_certificate() {
if let Ok(digest) = cert.digest(MessageDigest::ripemd160()) {
Some(digest.encode_hex::<String>())
} else {
None
}
} else {
None
}
}

4
src/lib.rs Normal file
View File

@@ -0,0 +1,4 @@
pub mod fs;
pub mod gemini;
pub mod nanoid;
pub mod time;

61
src/main.rs Normal file
View File

@@ -0,0 +1,61 @@
use std::env;
use anyhow::{Context, Result};
use log::error;
use gemini_grs::gemini;
#[derive(Debug)]
pub struct EnvConfig {
pub hostname: String,
pub key_path: String,
pub cert_path: String,
pub server_root: String,
}
impl EnvConfig {
pub fn from_env() -> Result<Self> {
let hostname = env::var("GEMINI_SERVER_HOSTNAME").unwrap_or("127.0.0.1:1965".to_string());
let key_path = env::var("GEMINI_SERVER_TLS_KEY_FILENAME").with_context(|| {
"Missing environment variable GEMINI_SERVER_TLS_KEY_FILENAME".to_string()
})?;
let cert_path = env::var("GEMINI_SERVER_TLS_CERT_FILENAME").with_context(|| {
"Missing environment variable GEMINI_SERVER_TLS_CERT_FILENAME".to_string()
})?;
let server_root = env::var("GEMINI_SERVER_ROOT_DIRECTORY").with_context(|| {
"Missing environment variable GEMINI_SERVER_ROOT_DIRECTORY".to_string()
})?;
Ok(Self {
hostname,
key_path,
cert_path,
server_root,
})
}
}
fn main() {
let exit_code = run();
std::process::exit(exit_code);
}
fn run() -> i32 {
env_logger::init();
let env_config = match EnvConfig::from_env() {
Err(e) => {
error!("{:#?}", e);
return 1;
}
Ok(x) => x,
};
if let Err(e) = gemini::server::start_server(
&env_config.hostname,
&env_config.key_path,
&env_config.cert_path,
&env_config.server_root,
) {
error!("{:#?}", e);
return 1;
}
0
}

11
src/nanoid.rs Normal file
View File

@@ -0,0 +1,11 @@
use nanoid::nanoid;
pub fn nanoid() -> String {
let alphabet = [
'1', '2', '3', '4', '5', '6', '7', '8', '9', '0', 'a', 'b', 'c', 'd', 'e', 'f',
'g','h','i','j','k','l','m','n','o','p','q','r','s','t','u','v','w','x','y','z',
'A','B','C','D','E','F','G','H','I','J','K','L','M','N','O','P','Q','R','S','T',
'U','V','W','X','Y','Z',
];
nanoid!(16, &alphabet)
}

9
src/time.rs Normal file
View File

@@ -0,0 +1,9 @@
use std::time::{SystemTime, UNIX_EPOCH};
// returns the time since the epoch in milliseconds
pub fn now_unix_millis() -> u128 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_millis()
}