This commit is contained in:
dece 2021-11-12 20:39:24 +01:00
commit 71af1f03e5
4 changed files with 1444 additions and 0 deletions

1
.gitignore vendored Normal file
View file

@ -0,0 +1 @@
/target

673
Cargo.lock generated Normal file
View file

@ -0,0 +1,673 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 3
[[package]]
name = "aho-corasick"
version = "0.7.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e37cfd5e7657ada45f742d6e99ca5788580b5c529dc78faf11ece6dc702656f"
dependencies = [
"memchr",
]
[[package]]
name = "ansi_term"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ee49baf6cb617b853aa8d93bf420db2383fab46d314482ca2803b40d5fde979b"
dependencies = [
"winapi",
]
[[package]]
name = "atty"
version = "0.2.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8"
dependencies = [
"hermit-abi",
"libc",
"winapi",
]
[[package]]
name = "autocfg"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a"
[[package]]
name = "base64"
version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd"
[[package]]
name = "bitflags"
version = "1.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
[[package]]
name = "bumpalo"
version = "3.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f1e260c3a9040a7c19a12468758f4c16f31a81a1fe087482be9570ec864bb6c"
[[package]]
name = "cc"
version = "1.0.72"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "22a9137b95ea06864e018375b72adfb7db6e6f68cfc8df5a04d00288050485ee"
[[package]]
name = "cfg-if"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "chrono"
version = "0.4.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "670ad68c9088c2a963aaa298cb369688cf3f9465ce5e2d4ca10e6e0098a1ce73"
dependencies = [
"num-integer",
"num-traits",
]
[[package]]
name = "clap"
version = "2.33.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "37e58ac78573c40708d45522f0d80fa2f01cc4f9b4e2bf749807255454312002"
dependencies = [
"ansi_term",
"atty",
"bitflags",
"strsim",
"textwrap",
"unicode-width",
"vec_map",
]
[[package]]
name = "data-encoding"
version = "2.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3ee2393c4a91429dffb4bedf19f4d6abf27d8a732c8ce4980305d782e5426d57"
[[package]]
name = "der-oid-macro"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c73af209b6a5dc8ca7cbaba720732304792cddc933cfea3d74509c2b1ef2f436"
dependencies = [
"num-bigint",
"num-traits",
"syn",
]
[[package]]
name = "der-parser"
version = "6.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9807efb310ce4ea172924f3a69d82f9fd6c9c3a19336344591153e665b31c43e"
dependencies = [
"der-oid-macro",
"nom",
"num-bigint",
"num-traits",
"rusticata-macros",
]
[[package]]
name = "env_logger"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b2cf0344971ee6c64c31be0d530793fba457d322dfec2810c453d0ef228f9c3"
dependencies = [
"atty",
"humantime",
"log",
"regex",
"termcolor",
]
[[package]]
name = "form_urlencoded"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5fc25a87fa4fd2094bffb06925852034d90a17f0d1e05197d4956d3555752191"
dependencies = [
"matches",
"percent-encoding",
]
[[package]]
name = "hermit-abi"
version = "0.1.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33"
dependencies = [
"libc",
]
[[package]]
name = "humantime"
version = "2.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4"
[[package]]
name = "idna"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "418a0a6fab821475f634efe3ccc45c013f742efe03d853e8d3355d5cb850ecf8"
dependencies = [
"matches",
"unicode-bidi",
"unicode-normalization",
]
[[package]]
name = "js-sys"
version = "0.3.55"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7cc9ffccd38c451a86bf13657df244e9c3f37493cce8e5e21e940963777acc84"
dependencies = [
"wasm-bindgen",
]
[[package]]
name = "lazy_static"
version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
[[package]]
name = "libc"
version = "0.2.107"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fbe5e23404da5b4f555ef85ebed98fb4083e55a00c317800bc2a50ede9f3d219"
[[package]]
name = "log"
version = "0.4.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "51b9bbe6c47d51fc3e1a9b945965946b4c44142ab8792c50835a980d362c2710"
dependencies = [
"cfg-if",
]
[[package]]
name = "matches"
version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a3e378b66a060d48947b590737b30a1be76706c8dd7b8ba0f2fe3989c68a853f"
[[package]]
name = "memchr"
version = "2.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "308cc39be01b73d0d18f82a0e7b2a3df85245f84af96fdddc5d202d27e47b86a"
[[package]]
name = "minimal-lexical"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
[[package]]
name = "mio"
version = "0.7.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8067b404fe97c70829f082dec8bcf4f71225d7eaea1d8645349cb76fa06205cc"
dependencies = [
"libc",
"log",
"miow",
"ntapi",
"winapi",
]
[[package]]
name = "miow"
version = "0.3.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b9f1c5b025cda876f66ef43a113f91ebc9f4ccef34843000e0adf6ebbab84e21"
dependencies = [
"winapi",
]
[[package]]
name = "nom"
version = "7.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1b1d11e1ef389c76fe5b81bcaf2ea32cf88b62bc494e19f493d0b30e7a930109"
dependencies = [
"memchr",
"minimal-lexical",
"version_check",
]
[[package]]
name = "ntapi"
version = "0.3.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f6bb902e437b6d86e03cce10a7e2af662292c5dfef23b65899ea3ac9354ad44"
dependencies = [
"winapi",
]
[[package]]
name = "num-bigint"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f93ab6289c7b344a8a9f60f88d80aa20032336fe78da341afc91c8a2341fc75f"
dependencies = [
"autocfg",
"num-integer",
"num-traits",
]
[[package]]
name = "num-integer"
version = "0.1.44"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d2cc698a63b549a70bc047073d2949cce27cd1c7b0a4a862d08a8031bc2801db"
dependencies = [
"autocfg",
"num-traits",
]
[[package]]
name = "num-traits"
version = "0.2.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a64b1ec5cda2586e284722486d802acf1f7dbdc623e2bfc57e65ca1cd099290"
dependencies = [
"autocfg",
]
[[package]]
name = "oid-registry"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fe554cb2393bc784fd678c82c84cc0599c31ceadc7f03a594911f822cb8d1815"
dependencies = [
"der-parser",
]
[[package]]
name = "once_cell"
version = "1.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "692fcb63b64b1758029e0a96ee63e049ce8c5948587f2f7208df04625e5f6b56"
[[package]]
name = "opal"
version = "0.1.0"
dependencies = [
"clap",
"env_logger",
"log",
"mio",
"percent-encoding",
"rustls",
"rustls-pemfile",
"url",
"x509-parser",
]
[[package]]
name = "percent-encoding"
version = "2.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e"
[[package]]
name = "proc-macro2"
version = "1.0.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ba508cc11742c0dc5c1659771673afbab7a0efab23aa17e854cbab0837ed0b43"
dependencies = [
"unicode-xid",
]
[[package]]
name = "quote"
version = "1.0.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "38bc8cc6a5f2e3655e0899c1b848643b2562f853f114bfec7be120678e3ace05"
dependencies = [
"proc-macro2",
]
[[package]]
name = "regex"
version = "1.5.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d07a8629359eb56f1e2fb1652bb04212c072a87ba68546a04065d525673ac461"
dependencies = [
"aho-corasick",
"memchr",
"regex-syntax",
]
[[package]]
name = "regex-syntax"
version = "0.6.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b"
[[package]]
name = "ring"
version = "0.16.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3053cf52e236a3ed746dfc745aa9cacf1b791d846bdaf412f60a8d7d6e17c8fc"
dependencies = [
"cc",
"libc",
"once_cell",
"spin",
"untrusted",
"web-sys",
"winapi",
]
[[package]]
name = "rusticata-macros"
version = "4.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "65c52377bb2288aa522a0c8208947fada1e0c76397f108cc08f57efe6077b50d"
dependencies = [
"nom",
]
[[package]]
name = "rustls"
version = "0.20.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dac4581f0fc0e0efd529d069e8189ec7b90b8e7680e21beb35141bdc45f36040"
dependencies = [
"log",
"ring",
"sct",
"webpki",
]
[[package]]
name = "rustls-pemfile"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5eebeaeb360c87bfb72e84abdb3447159c0eaececf1bef2aecd65a8be949d1c9"
dependencies = [
"base64",
]
[[package]]
name = "sct"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d53dcdb7c9f8158937a7981b48accfd39a43af418591a5d008c7b22b5e1b7ca4"
dependencies = [
"ring",
"untrusted",
]
[[package]]
name = "spin"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d"
[[package]]
name = "strsim"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a"
[[package]]
name = "syn"
version = "1.0.81"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f2afee18b8beb5a596ecb4a2dce128c719b4ba399d34126b9e4396e3f9860966"
dependencies = [
"proc-macro2",
"quote",
"unicode-xid",
]
[[package]]
name = "termcolor"
version = "1.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2dfed899f0eb03f32ee8c6a0aabdb8a7949659e3466561fc0adf54e26d88c5f4"
dependencies = [
"winapi-util",
]
[[package]]
name = "textwrap"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060"
dependencies = [
"unicode-width",
]
[[package]]
name = "thiserror"
version = "1.0.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "854babe52e4df1653706b98fcfc05843010039b406875930a70e4d9644e5c417"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
version = "1.0.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aa32fd3f627f367fe16f893e2597ae3c05020f8bba2666a4e6ea73d377e5714b"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "tinyvec"
version = "1.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2c1c1d5a42b6245520c249549ec267180beaffcc0615401ac8e31853d4b6d8d2"
dependencies = [
"tinyvec_macros",
]
[[package]]
name = "tinyvec_macros"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c"
[[package]]
name = "unicode-bidi"
version = "0.3.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1a01404663e3db436ed2746d9fefef640d868edae3cceb81c3b8d5732fda678f"
[[package]]
name = "unicode-normalization"
version = "0.1.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d54590932941a9e9266f0832deed84ebe1bf2e4c9e4a3554d393d18f5e854bf9"
dependencies = [
"tinyvec",
]
[[package]]
name = "unicode-width"
version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3ed742d4ea2bd1176e236172c8429aaf54486e7ac098db29ffe6529e0ce50973"
[[package]]
name = "unicode-xid"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3"
[[package]]
name = "untrusted"
version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a"
[[package]]
name = "url"
version = "2.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a507c383b2d33b5fc35d1861e77e6b383d158b2da5e14fe51b83dfedf6fd578c"
dependencies = [
"form_urlencoded",
"idna",
"matches",
"percent-encoding",
]
[[package]]
name = "vec_map"
version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191"
[[package]]
name = "version_check"
version = "0.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5fecdca9a5291cc2b8dcf7dc02453fee791a280f3743cb0905f8822ae463b3fe"
[[package]]
name = "wasm-bindgen"
version = "0.2.78"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "632f73e236b219150ea279196e54e610f5dbafa5d61786303d4da54f84e47fce"
dependencies = [
"cfg-if",
"wasm-bindgen-macro",
]
[[package]]
name = "wasm-bindgen-backend"
version = "0.2.78"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a317bf8f9fba2476b4b2c85ef4c4af8ff39c3c7f0cdfeed4f82c34a880aa837b"
dependencies = [
"bumpalo",
"lazy_static",
"log",
"proc-macro2",
"quote",
"syn",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-macro"
version = "0.2.78"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d56146e7c495528bf6587663bea13a8eb588d39b36b679d83972e1a2dbbdacf9"
dependencies = [
"quote",
"wasm-bindgen-macro-support",
]
[[package]]
name = "wasm-bindgen-macro-support"
version = "0.2.78"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7803e0eea25835f8abdc585cd3021b3deb11543c6fe226dcd30b228857c5c5ab"
dependencies = [
"proc-macro2",
"quote",
"syn",
"wasm-bindgen-backend",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-shared"
version = "0.2.78"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0237232789cf037d5480773fe568aac745bfe2afbc11a863e97901780a6b47cc"
[[package]]
name = "web-sys"
version = "0.3.55"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "38eb105f1c59d9eaa6b5cdc92b859d85b926e82cb2e0945cd0c9259faa6fe9fb"
dependencies = [
"js-sys",
"wasm-bindgen",
]
[[package]]
name = "webpki"
version = "0.22.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f095d78192e208183081cc07bc5515ef55216397af48b873e5edcd72637fa1bd"
dependencies = [
"ring",
"untrusted",
]
[[package]]
name = "winapi"
version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
dependencies = [
"winapi-i686-pc-windows-gnu",
"winapi-x86_64-pc-windows-gnu",
]
[[package]]
name = "winapi-i686-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
[[package]]
name = "winapi-util"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178"
dependencies = [
"winapi",
]
[[package]]
name = "winapi-x86_64-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
[[package]]
name = "x509-parser"
version = "0.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ffc90836a84cb72e6934137b1504d0cae304ef5d83904beb0c8d773bbfe256ed"
dependencies = [
"base64",
"chrono",
"data-encoding",
"der-parser",
"lazy_static",
"nom",
"oid-registry",
"rusticata-macros",
"thiserror",
]

18
Cargo.toml Normal file
View file

@ -0,0 +1,18 @@
[package]
name = "opal"
version = "0.1.0"
edition = "2021"
[profile.release]
lto = true
[dependencies]
clap = "2.33"
env_logger = "0.9"
log = "0.4"
mio = { version = "0.7", features = ["os-poll", "net"] }
percent-encoding = "2.1"
rustls = { version = "0.20", features = ["dangerous_configuration"] }
rustls-pemfile = "0.2"
url = "2.2"
x509-parser = "0.12"

752
src/main.rs Normal file
View file

@ -0,0 +1,752 @@
use std::collections::HashMap;
use std::fs;
use std::io;
use std::io::{Read, Write};
use std::net;
use std::path;
use std::process;
use std::sync::Arc;
#[macro_use]
extern crate clap;
use log::{debug, error, info};
/// General CGI configuration.
struct CgiConfig {
root: String,
}
/// TCP server, listening for clients opening TLS connections.
struct Server<'a> {
config: Arc<rustls::ServerConfig>,
listener: mio::net::TcpListener,
connections: HashMap<mio::Token, Connection<'a>>,
next_id: usize,
cgi_config: &'a CgiConfig,
}
impl<'a> Server<'a> {
/// Create a new Server.
fn new(
listener: mio::net::TcpListener,
config: Arc<rustls::ServerConfig>,
cgi_config: &'a CgiConfig,
) -> Self {
Server {
config,
listener,
connections: HashMap::new(),
next_id: 2,
cgi_config,
}
}
/// Accept incoming client connections forever.
fn accept(&mut self, registry: &mio::Registry) -> Result<(), io::Error> {
loop {
match self.listener.accept() {
Ok((socket, addr)) => {
debug!("Connection from {:?}", addr);
let tls = match rustls::ServerConnection::new(Arc::clone(&self.config)) {
Ok(c) => c,
Err(err) => {
error!("Could not create server connection: {}", err);
continue;
}
};
let token = mio::Token(self.next_id);
self.next_id += 1;
let mut connection = Connection::new(socket, token, tls, self.cgi_config);
connection.register(registry);
self.connections.insert(token, connection);
}
Err(ref err) if err.kind() == io::ErrorKind::WouldBlock => return Ok(()),
Err(err) => {
error!("Error while accepting connection: {}", err);
return Err(err);
}
}
}
}
/// Pass MIO events to corresponding connections.
fn handle_event(&mut self, registry: &mio::Registry, event: &mio::event::Event) {
let token = event.token();
if self.connections.contains_key(&token) {
self.connections
.get_mut(&token)
.unwrap()
.ready(registry, event);
if self.connections[&token].state == ConnectionState::Closed {
self.connections.remove(&token);
}
}
}
}
/// Connection state, mostly used for graceful shutdowns.
#[derive(PartialEq)]
enum ConnectionState {
Open,
Closing,
Closed,
}
/// A once open connection; hold the TCP and TLS states, as well as the incoming client data.
struct Connection<'a> {
socket: mio::net::TcpStream,
token: mio::Token,
state: ConnectionState,
tls: rustls::ServerConnection,
buffer: Vec<u8>,
received: usize,
cgi_config: &'a CgiConfig,
}
impl<'a> Connection<'a> {
/// Create a new Connection.
fn new(
socket: mio::net::TcpStream,
token: mio::Token,
tls_connection: rustls::ServerConnection,
cgi_config: &'a CgiConfig,
) -> Self {
Connection {
socket,
token,
state: ConnectionState::Open,
tls: tls_connection,
buffer: vec![0; 1026],
received: 0,
cgi_config,
}
}
/// Process an event.
fn ready(&mut self, registry: &mio::Registry, event: &mio::event::Event) {
if event.is_readable() {
self.read_tls();
self.read_plain();
}
if event.is_writable() {
self.write_tls_with_errors();
}
if self.state == ConnectionState::Closing {
if let Err(err) = self.socket.shutdown(net::Shutdown::Both) {
error!("Could not properly shutdown socket: {}", err);
}
self.state = ConnectionState::Closed;
registry.deregister(&mut self.socket).unwrap();
} else {
let event_set = self.event_set();
registry
.reregister(&mut self.socket, self.token, event_set)
.unwrap();
}
}
/// Read data from the TLS tunnel; if enough data is read, new packets are processed and can be
/// later read with `read_plain`.
fn read_tls(&mut self) {
match self.tls.read_tls(&mut self.socket) {
Err(err) => {
if err.kind() != io::ErrorKind::WouldBlock {
error!("TLS read error: {}", err);
self.state = ConnectionState::Closing;
}
return;
}
Ok(num_bytes) if num_bytes == 0 => {
self.state = ConnectionState::Closing;
return;
}
_ => {}
};
if let Err(err) = self.tls.process_new_packets() {
error!("Can't process packet: {}", err);
self.write_tls_with_errors();
self.state = ConnectionState::Closing;
}
}
/// Process packets with incoming data from a client.
fn read_plain(&mut self) {
if let Ok(io_state) = self.tls.process_new_packets() {
let to_read = io_state.plaintext_bytes_to_read();
if to_read > 0 {
let mut buffer = vec![0u8; to_read];
self.tls.reader().read(&mut buffer).unwrap();
self.handle_incoming_data(&buffer);
}
}
}
/// Process received client data as a Gemini request; it either is a self-contained
fn handle_incoming_data(&mut self, data: &[u8]) {
// The connection buffer should never exceed 1026 bytes: 1024 URL bytes plus \r\n.
if data.len() + self.received > 1026 {
error!("URL queried is longer 1024 bytes, discarding.");
self.state = ConnectionState::Closing;
return;
}
// If the URL requested is contained within that single data packet, process it without
// copying stuff.
if self.received == 0 && data.ends_with(b"\r\n") {
self.process_buffer(data);
}
// Else append received data into the connection buffer and try to process it.
else {
let buffer_end = self.received + data.len();
self.buffer[self.received..buffer_end].copy_from_slice(data);
self.received = buffer_end;
if self.buffer[..self.received].ends_with(b"\r\n") {
self.process_buffer(&self.buffer.clone());
}
}
}
/// Respond to a client request. Whether the request succeeds or not, a response is sent and
/// the connection is closed.
fn process_buffer(&mut self, buffer: &[u8]) {
// Get appropriate response from either Opal or the CGI process.
let response: Vec<u8> = match self.get_response(buffer) {
Ok((url, data)) => {
info!("\"{}\" → reply {} bytes", url, data.len());
data
}
Err((url, code, meta)) => {
info!(
"\"{}\" → {} \"{}\"",
url.or(Some("<invalid URL>".to_string())).unwrap(),
code,
meta
);
format!("{} {}\r\n", code, meta).as_bytes().to_vec()
}
};
// Whether the request succeeded or not, send the response.
if let Err(err) = self.tls.writer().write_all(&response) {
error!("Error while writing TLS data: {}", err);
}
// Properly close the connection.
self.tls.send_close_notify();
self.state = ConnectionState::Closing;
}
/// Process a client request.
///
/// If the CGI process returns successfully, return the requested URL with the process output
/// so that it can be sent back to the client. If an error occurs outside of the CGI process,
/// return a 3-uple with the URL (if it could be parsed correctly), a Gemini error code and an
/// explanation string to provide to the client.
fn get_response(
&self,
request: &[u8],
) -> Result<(String, Vec<u8>), (Option<String>, u8, &str)> {
// Convert the URL to UTF-8.
let url_str = match std::str::from_utf8(&request[..request.len() - 2]) {
Ok(s) => s,
_ => return Err((None, 59, "URL is not valid UTF-8")),
};
// Parse the URL. The `url` crate normalizes ".." and "/" elements here.
let url = match url::Url::parse(url_str) {
Ok(u) => u,
Err(err) => {
error!("Failed to parse \"{}\" as URL: {}", url_str, err);
return Err((Some(url_str.to_string()), 59, "Invalid URL"));
}
};
// Get the script path, optionally with CGI's "path info".
let (script_path, path_info) = self.validate_script_path(&url)?;
debug!("Script path: \"{}\"", script_path.to_string_lossy());
// Define a generic "temp failure" error for any other issue.
let cgi_error = Err((Some(url_str.to_string()), 40, "Temporary failure"));
// Execute script and return its output.
let env = match self.get_cgi_envs(&url, &script_path, &path_info) {
Some(e) => e,
None => {
error!("Can't get required environment variables.");
return cgi_error;
}
};
let output = match process::Command::new(&script_path)
.env_clear()
.envs(env)
.output()
{
Ok(o) => o,
Err(err) => {
error!("Can't execute script: {}", err);
return cgi_error;
}
};
Ok((url_str.to_string(), output.stdout))
}
/// Return a validated script path from the requested URL along with CGI PATH_INFO.
///
/// A valid path points to an existing, executable file, located within the CGI scripts root.
/// If any of these condition fails, log the reason and return an appropriate 3-uple for
/// `get_response`.
fn validate_script_path(
&self,
url: &url::Url,
) -> Result<(path::PathBuf, Option<String>), (Option<String>, u8, &str)> {
// Define a generic "not found" error for most path issues.
let not_found = Err((Some(url.as_str().to_string()), 51, "Not found"));
// Find script path from our CGI root and the request.
let mut path = path::PathBuf::from(&self.cgi_config.root);
let mut segments = match url.path_segments() {
Some(s) => s,
None => {
error!("Can't get path segments from URL");
return not_found;
}
};
// We incrementally push path segments after our CGI root to find the first path that
// represents an executable file.
let mut found_script = false;
loop {
let segment = segments.next();
if segment.is_none() {
break;
}
let decoded_segment =
match percent_encoding::percent_decode_str(segment.unwrap()).decode_utf8() {
Ok(s) => s,
Err(err) => {
error!("Path segment decoded into invalid UTF-8: {}", err);
return not_found;
}
};
path.push(decoded_segment.into_owned());
// If that path is not an executable file, continue with the next segment.
if path.is_file() && is_executable(&path) {
found_script = true;
break;
}
}
if !found_script {
error!("No script found along path \"{}\".", path.to_string_lossy());
return not_found;
}
// Collect the remaining segments into the CGI "path info" value.
let rem_segments = segments.collect::<Vec<&str>>();
let path_info = if rem_segments.len() > 0 {
Some(String::from("/") + &rem_segments.join("/"))
} else {
None
};
// Just for safety, check that the now-canonicalized path is within the CGI root.
if !path.starts_with(&self.cgi_config.root) {
debug!(
"Script path \"{}\" is outside of CGI root dir \"{}\".",
path.to_string_lossy(),
self.cgi_config.root
);
return not_found;
}
Ok((path, path_info))
}
/// Build environment variables for the CGI process.
fn get_cgi_envs(
&self,
url: &url::Url,
script_path: &path::PathBuf,
path_info: &Option<String>,
) -> Option<HashMap<String, String>> {
// Common, nothing-to-compute elements.
let mut envs: HashMap<String, String> = HashMap::from([
(String::from("GATEWAY_INTERFACE"), String::from("CGI/1.1")),
(String::from("REQUEST_METHOD"), String::new()),
(String::from("SERVER_PROTOCOL"), String::from("GEMINI")),
(
String::from("SERVER_SOFTWARE"),
format!("opal/{}", crate_version!()),
),
(
String::from("GEMINI_DOCUMENT_ROOT"),
self.cgi_config.root.to_string(),
),
(
String::from("GEMINI_SCRIPT_FILENAME"),
script_path.to_string_lossy().to_string(),
),
(String::from("GEMINI_URL"), url.to_string()),
(String::from("GEMINI_URL_PATH"), url.path().to_string()),
]);
// Variables that must be there but might not be available for some reason: this makes the
// whole execution fail.
match self.socket.peer_addr() {
Ok(address) => {
let ip = address.ip();
envs.insert(String::from("REMOTE_ADDR"), ip.to_string());
envs.insert(String::from("REMOTE_HOST"), ip.to_string());
}
Err(err) => {
error!("Can't get peer address for CGI envs: {}", err);
return None;
}
};
match self.socket.local_addr() {
Ok(address) => {
envs.insert(String::from("SERVER_PORT"), address.port().to_string());
}
Err(err) => {
error!("Can't get local address for CGI envs: {}", err);
return None;
}
}
let root_len = self.cgi_config.root.len();
envs.insert(
String::from("SCRIPT_NAME"),
script_path.to_str().unwrap()[root_len..].to_string(),
);
match self.tls.sni_hostname() {
Some(hostname) => {
envs.insert(String::from("SERVER_NAME"), hostname.to_string());
}
None => {
error!("Can't get SNI hostname for SERVER_NAME.");
return None;
}
};
// Optional variables.
if let Some(path_info) = path_info {
let percent_decode = percent_encoding::percent_decode_str(path_info);
match percent_decode.decode_utf8() {
Ok(path_info) => {
envs.insert(String::from("PATH_INFO"), path_info.to_string());
}
Err(err) => {
error!("CGI PATH_INFO decoded into invalid UTF-8: {}", err);
}
};
}
if let Some(query) = url.query() {
envs.insert(String::from("QUERY_STRING"), query.to_string());
}
// Variables related to client certificates.
if let Some(certs) = self.tls.peer_certificates() {
debug!("0");
if certs.len() > 0 {
debug!("1");
envs.insert(String::from("AUTH_TYPE"), String::from("Certificate"));
if let Ok((_, cert)) = x509_parser::parse_x509_certificate(&certs[0].0) {
envs.insert(String::from("REMOTE_USER"), cert.subject().to_string());
envs.insert(String::from("TLS_CLIENT_ISSUER"), cert.issuer().to_string());
let hash_algo = cert.signature_algorithm.algorithm.to_string();
let hash_value = format!("{:?}", cert.signature_value.data);
envs.insert(
String::from("TLS_CLIENT_HASH"),
format!("{}:{}", hash_algo, hash_value),
);
}
}
}
// CGI standard
// AUTH_TYPE: OK
// CONTENT_LENGTH: not affected
// CONTENT_TYPE: not affected
// GATEWAY_INTERFACE: OK
// PATH_INFO: OK, decoded
// PATH_TRANSLATED: TODO useful?
// QUERY_STRING: OK still URL-encoded like the standard asks
// REMOTE_ADDR: OK
// REMOTE_HOST: use REMOTE_ADDR
// REMOTE_IDENT: not affected
// REMOTE_USER: TODO
// REQUEST_METHOD: empty string for compatibility
// SCRIPT_NAME: OK
// SERVER_NAME: OK
// SERVER_PORT: OK
// SERVER_PROTOCOL: OK
// SERVER_SOFTWARE: OK
// Additionally proposed by gmid
// GEMINI_DOCUMENT_ROOT: OK
// GEMINI_SCRIPT_FILENAME: OK
// GEMINI_URL: OK
// GEMINI_URL_PATH: OK
// TLS_CLIENT_ISSUER
// TLS_CLIENT_HASH
// TLS_VERSION
// TLS_CIPHER
// TLS_CIPHER_STRENGTH
// TLS_CLIENT_NOT_AFTER
// TLS_CLIENT_NOT_BEFORE
Some(envs)
}
/// Write TLS data in the TCP socket.
fn write_tls(&mut self) -> io::Result<usize> {
self.tls.write_tls(&mut self.socket)
}
/// Call `write_tls` and mark connection as closing on error.
fn write_tls_with_errors(&mut self) {
if let Err(err) = self.write_tls() {
error!("TLS write error after errors: {}", err);
self.state = ConnectionState::Closing;
}
}
/// Register the connection into the MIO registry using its own token.
fn register(&mut self, registry: &mio::Registry) {
let event_set = self.event_set();
registry
.register(&mut self.socket, self.token, event_set)
.unwrap();
}
/// Return what IO events we're currently waiting for, based on wants_read/wants_write.
fn event_set(&self) -> mio::Interest {
let r = self.tls.wants_read();
let w = self.tls.wants_write();
if r && w {
mio::Interest::READABLE | mio::Interest::WRITABLE
} else if w {
mio::Interest::WRITABLE
} else {
mio::Interest::READABLE
}
}
}
/// Return true if this path has executable bits set on Unix systems.
///
/// If for some reason we can't get the mode information (not on Unix or some error occured),
/// return false.
fn is_executable(path: &path::Path) -> bool {
match fs::metadata(path) {
Ok(metadata) => {
use std::os::unix::fs::PermissionsExt;
let mode = metadata.permissions().mode();
mode & 0o111 != 0
}
Err(err) => {
error!(
"Can't get metadata for \"{}\": {}",
path.to_string_lossy(),
err
);
false
}
}
}
/// Load a PEM certificate(s) from disk.
fn load_certificate(path: &str) -> Option<Vec<rustls::Certificate>> {
let cert_file = match fs::File::open(path) {
Ok(f) => f,
Err(err) => {
error!("Can't open certificate file at {}: {}", path, err);
return None;
}
};
let mut reader = io::BufReader::new(cert_file);
Some(
rustls_pemfile::certs(&mut reader)
.unwrap()
.iter()
.map(|v| rustls::Certificate(v.clone()))
.collect(),
)
}
/// Load a PEM private key from disk.
fn load_private_key(path: &str) -> Option<rustls::PrivateKey> {
let key_file = match fs::File::open(path) {
Ok(f) => f,
Err(err) => {
error!("Can't open private key file at {}: {}", path, err);
return None;
}
};
let mut reader = io::BufReader::new(key_file);
loop {
match rustls_pemfile::read_one(&mut reader) {
Ok(Some(key)) => match key {
rustls_pemfile::Item::RSAKey(key) => return Some(rustls::PrivateKey(key)),
rustls_pemfile::Item::PKCS8Key(key) => return Some(rustls::PrivateKey(key)),
_ => debug!("Ignored unknown private key type."),
},
Ok(None) => {
error!("No key found in file.");
return None;
}
Err(err) => {
error!("Can't parse private key data: {}", err);
return None;
}
}
}
}
/// A `ClientCertVerifier` for Gemini: certificate is optional, and it is not verified against a
/// certificate chain.
struct GeminiClientCertVerifier {}
impl rustls::verify::ClientCertVerifier for GeminiClientCertVerifier {
fn client_auth_mandatory(&self) -> Option<bool> {
Some(false)
}
fn client_auth_root_subjects(&self) -> Option<rustls::DistinguishedNames> {
Some(vec![])
}
fn verify_client_cert(
&self,
end_entity: &rustls::Certificate,
intermediates: &[rustls::Certificate],
now: std::time::SystemTime,
) -> Result<rustls::verify::ClientCertVerifier, rustls::Error> {
todo!()
}
}
fn main() {
// Get command-line args. Opal does not use config files.
let matches = clap::App::new("Opal")
.version(crate_version!())
.about("Gemini CGI-only server")
.arg(
clap::Arg::with_name("address")
.required(true)
.short("a")
.long("address")
.help("Address to listen to")
.takes_value(true),
)
.arg(
clap::Arg::with_name("cert")
.required(true)
.short("c")
.long("cert")
.help("Path to certificate")
.takes_value(true),
)
.arg(
clap::Arg::with_name("key")
.required(true)
.short("k")
.long("key")
.help("Path to private key")
.takes_value(true),
)
.arg(
clap::Arg::with_name("root_path")
.required(true)
.short("p")
.long("root-path")
.help("Path to CGI scripts root")
.takes_value(true),
)
.get_matches();
// Setup logging pretty much just like Agate.
let log_config = env_logger::Env::default().default_filter_or("opal=info");
env_logger::Builder::from_env(log_config).init();
// Process command-line args.
info!("Starting Opal");
let addr = match matches
.value_of("address")
.unwrap()
.parse::<net::SocketAddr>()
{
Ok(a) => a,
Err(err) => {
error!(
"Invalid listening address (wrong format or port missing?): {}",
err
);
return;
}
};
let cert_verif = GeminiClientCertVerifier {};
let server_certs = match load_certificate(matches.value_of("cert").unwrap()) {
Some(v) if v.len() > 0 => v,
Some(_) => {
error!("No valid certificate found.");
return;
}
None => return,
};
let server_key = match load_private_key(matches.value_of("key").unwrap()) {
Some(k) => k,
None => return,
};
let cgi_root = match fs::canonicalize(matches.value_of("root_path").unwrap()) {
Ok(p) => p.to_str().unwrap().to_string(),
Err(err) => {
error!("Invalid CGI root path: {}", err);
return;
}
};
let cgi_config = CgiConfig { root: cgi_root };
// Setup TLS server.
let config = Arc::new(
match rustls::ServerConfig::builder()
.with_safe_defaults()
.with_client_cert_verifier(cert_verif)
.with_single_cert(server_certs, server_key)
{
Ok(c) => c,
Err(err) => {
error!("Invalid certificate or private key provided: {}", err);
return;
}
},
);
let mut listener = match mio::net::TcpListener::bind(addr) {
Ok(l) => l,
Err(err) => {
error!("Can't listen on port: {}", err);
return;
}
};
let listener_token = mio::Token(0);
let mut poll = mio::Poll::new().unwrap();
if let Err(err) =
poll.registry()
.register(&mut listener, listener_token, mio::Interest::READABLE)
{
error!("Can't setup poll listener: {}", err);
return;
}
let mut server = Server::new(listener, config, &cgi_config);
let mut events = mio::Events::with_capacity(256);
loop {
if let Err(err) = poll.poll(&mut events, None) {
debug!("Poll failed: {}", err);
continue;
}
for event in events.iter() {
if event.token() == listener_token {
if let Err(err) = server.accept(poll.registry()) {
error!("Could not accept socket: {}", err);
}
} else {
server.handle_event(poll.registry(), &event);
}
}
}
}