mirror of https://git.sr.ht/~garritfra/taurus
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
178 lines
5.3 KiB
178 lines
5.3 KiB
extern crate native_tls; |
|
extern crate url; |
|
|
|
mod config; |
|
mod error; |
|
mod gemini; |
|
|
|
use gemini::GeminiResponse; |
|
|
|
use native_tls::{Identity, TlsAcceptor, TlsStream}; |
|
use std::fs::File; |
|
use std::io::{self, Read}; |
|
use std::net::{TcpListener, TcpStream}; |
|
use std::path; |
|
use std::sync::Arc; |
|
use std::thread; |
|
|
|
use clap::{crate_authors, crate_description, crate_name, crate_version, App, Arg}; |
|
|
|
fn main() { |
|
if let Err(e) = run() { |
|
println!("Error: {}", e); |
|
std::process::exit(1); |
|
} |
|
} |
|
|
|
fn run() -> Result<(), error::TaurusError> { |
|
// CLI |
|
let matches = App::new(crate_name!()) |
|
.version(crate_version!()) |
|
.author(crate_authors!()) |
|
.about(crate_description!()) |
|
.arg( |
|
Arg::with_name("config") |
|
.long("config") |
|
.short("c") |
|
.help("Alternative config file location [default /etc/taurus/taurus.toml]") |
|
.next_line_help(true) |
|
.value_name("FILE"), |
|
) |
|
.get_matches(); |
|
|
|
let config_path = matches.value_of("config").map(|v| v.to_owned()); |
|
let config: config::Config = |
|
config::Config::load(config_path).map_err(error::TaurusError::InvalidConfig)?; |
|
|
|
// Defaults for configuration file |
|
let port = config.port.unwrap_or(1965); |
|
let cert_file = config |
|
.certificate_file |
|
.unwrap_or_else(|| "/etc/taurus/identity.pfx".to_owned()); |
|
let static_root = config |
|
.static_root |
|
.unwrap_or_else(|| "/var/www/gemini".to_owned()); |
|
|
|
// Read certificate |
|
let identity = read_file(&cert_file).map_err(error::TaurusError::NoIdentity)?; |
|
|
|
let identity = Identity::from_pkcs12(&identity, &config.certificate_password) |
|
.map_err(error::TaurusError::InvalidCertificate)?; |
|
|
|
let address = format!("0.0.0.0:{}", port); |
|
let listener = TcpListener::bind(address).map_err(|err| error::TaurusError::BindFailed(err))?; |
|
let acceptor = TlsAcceptor::new(identity).unwrap(); |
|
let acceptor = Arc::new(acceptor); |
|
|
|
println!("Info: Listening on port {}", port); |
|
|
|
for stream in listener.incoming() { |
|
match stream { |
|
Ok(stream) => { |
|
let acceptor = acceptor.clone(); |
|
let static_root = static_root.clone(); |
|
|
|
thread::spawn(move || match acceptor.accept(stream) { |
|
Ok(stream) => { |
|
if let Err(e) = handle_client(stream, &static_root) { |
|
println!("Error: can't handle client: {}", e); |
|
} |
|
} |
|
Err(e) => { |
|
println!("Error: can't handle stream: {}", e); |
|
} |
|
}); |
|
} |
|
Err(err) => println!("Error: {}", err), |
|
} |
|
} |
|
|
|
Ok(()) |
|
} |
|
|
|
/// Helper function to read a file into Vec |
|
fn read_file(file_path: &str) -> Result<Vec<u8>, io::Error> { |
|
let mut file = File::open(file_path)?; |
|
let mut buf = Vec::new(); |
|
|
|
file.read_to_end(&mut buf)?; |
|
|
|
Ok(buf) |
|
} |
|
|
|
/// Send file as a response |
|
fn write_file(path: &str) -> Result<GeminiResponse, error::TaurusError> { |
|
let extension = path::Path::new(path) |
|
.extension() |
|
.unwrap_or_else(|| std::ffi::OsStr::new("")) |
|
.to_str() |
|
.ok_or(error::TaurusError::InvalidUnicode)?; |
|
|
|
let mime_type = match extension { |
|
"gmi" => "text/gemini; charset=utf-8", |
|
ext => mime_guess::from_ext(ext) |
|
.first_raw() |
|
.unwrap_or("text/plain"), |
|
}; |
|
|
|
match read_file(path) { |
|
Ok(buf) => Ok(GeminiResponse::success(buf, mime_type)), |
|
Err(err) => { |
|
// Cannot read file or it doesn't exist |
|
|
|
println!("Error [{}]: {}", path, err); |
|
|
|
Ok(GeminiResponse::not_found()) |
|
} |
|
} |
|
} |
|
|
|
fn handle_client( |
|
mut stream: TlsStream<TcpStream>, |
|
static_root: &str, |
|
) -> Result<usize, error::TaurusError> { |
|
let mut buffer = [0; 1024]; |
|
|
|
stream |
|
.read(&mut buffer) |
|
.map_err(error::TaurusError::StreamReadFailed)?; |
|
|
|
let mut raw_request = String::from_utf8_lossy(&buffer[..]).to_mut().to_owned(); |
|
|
|
// TODO: Redundantly converted to owned and later referenced again |
|
if !raw_request.starts_with("gemini://") { |
|
raw_request = "gemini://".to_owned() + &raw_request; |
|
} |
|
|
|
let request = gemini::GeminiRequest::from_string(&raw_request).unwrap(); |
|
let url_path = request.file_path(); |
|
let file_path = path::Path::new(url_path); |
|
|
|
if file_path.has_root() { |
|
// File starts with `/` (*nix) or `\\` (Windows), decline it |
|
GeminiResponse::not_found().send(stream) |
|
} else { |
|
let path = path::Path::new(&static_root) |
|
.join(&file_path) |
|
.as_path() |
|
.to_owned(); |
|
|
|
// Check if file/dir exists |
|
if path.exists() { |
|
// If it's a directory, try to find index.gmi |
|
if path.is_dir() { |
|
let index_path = path |
|
.join("index.gmi") |
|
.to_str() |
|
.ok_or(error::TaurusError::InvalidUnicode)? |
|
.to_owned(); |
|
|
|
write_file(&index_path)?.send(stream) |
|
} else { |
|
write_file(path.to_str().ok_or(error::TaurusError::InvalidUnicode)?)?.send(stream) |
|
} |
|
} else { |
|
GeminiResponse::not_found().send(stream) |
|
} |
|
} |
|
}
|
|
|