configuration options, error handling
This commit is contained in:
parent
87d61457bd
commit
4a9c6c11ac
4 changed files with 181 additions and 75 deletions
70
src/main.rs
70
src/main.rs
|
@ -1,18 +1,68 @@
|
||||||
mod server;
|
mod server;
|
||||||
use std::env;
|
use std::env;
|
||||||
use std::path::Path;
|
|
||||||
|
|
||||||
fn read_config() -> server::ServerConfig {
|
fn help() {
|
||||||
let mut config = server::ServerConfig::new(
|
let version = match option_env!("CARGO_PKG_VERSION") {
|
||||||
"klockenschooster.de".to_string(),
|
Some(v) => v,
|
||||||
"/home/jw/code/projects/sheldond/doc".to_string(),
|
None => "",
|
||||||
);
|
};
|
||||||
config.add_addr("127.0.0.1:1965".to_string());
|
|
||||||
config.add_addr("[::1]:1965".to_string());
|
println!("usage: sheldond {}", version);
|
||||||
return config;
|
println!(" -h, --help\t\tdisplay this message");
|
||||||
|
println!(" -l, --listen\t\tadd a listening address (you can define multiple)");
|
||||||
|
println!(" -d, --default-host\tdefault hostname to listen for");
|
||||||
|
println!(" -g, --gem-root\tpath to the gemini root, aka the folder to serve files from");
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_args() -> Option<server::ServerConfig> {
|
||||||
|
let mut has_addr = false;
|
||||||
|
let mut has_host = false;
|
||||||
|
let mut has_root = false;
|
||||||
|
|
||||||
|
let mut config = server::ServerConfig::new();
|
||||||
|
let mut args = env::args();
|
||||||
|
loop {
|
||||||
|
match args.next() {
|
||||||
|
Some(arg) => {
|
||||||
|
if arg == "-h" || arg == "--help" {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
if arg == "-l" || arg == "--listen" {
|
||||||
|
let addr = args.next().unwrap();
|
||||||
|
config.add_addr(addr);
|
||||||
|
has_addr = true;
|
||||||
|
}
|
||||||
|
if arg == "-d" || arg == "--default-host" {
|
||||||
|
let host = args.next().unwrap();
|
||||||
|
config.set_default_host(host);
|
||||||
|
has_host = true;
|
||||||
|
}
|
||||||
|
if arg == "-g" || arg == "--gem-root" {
|
||||||
|
let gem_root = args.next().unwrap();
|
||||||
|
config.set_gem_root(gem_root);
|
||||||
|
has_root = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => break,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !has_addr || !has_host || !has_root {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
Some(config)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
let server = server::Server::new(&read_config());
|
let config = match parse_args() {
|
||||||
|
Some(config) => config,
|
||||||
|
None => {
|
||||||
|
help();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let server = server::Server::new(&config);
|
||||||
server.serve();
|
server.serve();
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,32 +1,89 @@
|
||||||
use crate::server::response;
|
use crate::server::response;
|
||||||
use crate::server::ServerConfig;
|
use crate::server::ServerConfig;
|
||||||
use openssl::ssl::{SslAcceptor, SslFiletype, SslMethod, SslStream};
|
use openssl::ssl::SslStream;
|
||||||
use std::fs::File;
|
use std::fs::File;
|
||||||
use std::io::{copy, BufReader, BufWriter};
|
use std::io::{copy, BufReader, BufWriter};
|
||||||
use std::net::TcpStream;
|
use std::net::TcpStream;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use url::Url;
|
use url::Url;
|
||||||
|
|
||||||
pub fn handle(config: &ServerConfig, url: Url, stream: &mut SslStream<TcpStream>) {
|
fn send_header(stream: &mut SslStream<TcpStream>, header: &response::Header) {
|
||||||
if url.scheme() != "gemini" {
|
match stream.ssl_write(&header.to_vec()) {
|
||||||
stream.ssl_write(&response::invalid_protocol().to_vec());
|
Ok(_s) => {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
Err(_e) => {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
let rel_path = Path::new(url.path()).strip_prefix("/").unwrap();
|
pub fn handle_request(config: &ServerConfig, mut stream: SslStream<TcpStream>) {
|
||||||
let path = config.www_root.join(rel_path);
|
let mut buffer = [0; 1026];
|
||||||
|
match stream.ssl_read(&mut buffer) {
|
||||||
let file = match File::open(&path) {
|
Ok(s) => {
|
||||||
Ok(file) => file,
|
if s == 0 {
|
||||||
|
send_header(&mut stream, &response::bad_request());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
Err(_) => {
|
Err(_) => {
|
||||||
stream.ssl_write(&response::not_found().to_vec());
|
send_header(&mut stream, &response::bad_request());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
stream.ssl_write(&response::Header::new(response::Status::Success, "text/gemini").to_vec());
|
let request = match String::from_utf8(buffer.to_vec()) {
|
||||||
|
Ok(request) => request,
|
||||||
|
Err(_) => {
|
||||||
|
send_header(&mut stream, &response::bad_request());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let location = match Url::parse(&request) {
|
||||||
|
Ok(url) => url,
|
||||||
|
Err(_) => config.default_host.join(&request).unwrap(),
|
||||||
|
};
|
||||||
|
|
||||||
|
handle_response(config, location, &mut stream);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_response(config: &ServerConfig, url: Url, mut stream: &mut SslStream<TcpStream>) {
|
||||||
|
if url.scheme() != "gemini" {
|
||||||
|
send_header(&mut stream, &response::permanent_failure());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if url.host() != config.default_host.host() {
|
||||||
|
send_header(&mut stream, &response::proxy_request_refused());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let rel_path = match Path::new(url.path()).strip_prefix("/") {
|
||||||
|
Ok(path) => path,
|
||||||
|
Err(_) => {
|
||||||
|
send_header(&mut stream, &response::bad_request());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let path = config.gem_root.join(rel_path);
|
||||||
|
|
||||||
|
let file = match File::open(&path) {
|
||||||
|
Ok(file) => file,
|
||||||
|
Err(_) => {
|
||||||
|
send_header(&mut stream, &response::not_found());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let header = response::Header::new(response::Status::Success, "text/gemini");
|
||||||
|
send_header(&mut stream, &header);
|
||||||
|
|
||||||
let mut buf_file = BufReader::new(file);
|
let mut buf_file = BufReader::new(file);
|
||||||
let mut buf_stream = BufWriter::new(stream);
|
let mut buf_stream = BufWriter::new(stream);
|
||||||
copy(&mut buf_file, &mut buf_stream);
|
match copy(&mut buf_file, &mut buf_stream) {
|
||||||
|
Ok(_s) => {}
|
||||||
|
Err(_e) => {}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
use openssl::ssl::{SslAcceptor, SslFiletype, SslMethod, SslStream};
|
use openssl::ssl::{SslAcceptor, SslFiletype, SslMethod};
|
||||||
use std::collections::HashMap;
|
use std::net::{SocketAddr, TcpListener};
|
||||||
use std::net::{SocketAddr, TcpListener, TcpStream};
|
use std::path::PathBuf;
|
||||||
use std::path::{Path, PathBuf};
|
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::thread;
|
use std::thread;
|
||||||
use std::vec::Vec;
|
use std::vec::Vec;
|
||||||
|
@ -13,24 +12,38 @@ pub mod response;
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct ServerConfig {
|
pub struct ServerConfig {
|
||||||
default_host: Url,
|
default_host: Url,
|
||||||
www_root: PathBuf,
|
gem_root: PathBuf,
|
||||||
addrs: Vec<SocketAddr>,
|
addrs: Vec<SocketAddr>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ServerConfig {
|
impl ServerConfig {
|
||||||
pub fn new(default_host: String, www_root: String) -> ServerConfig {
|
pub fn new() -> ServerConfig {
|
||||||
let mut url = Url::parse("gemini://default").unwrap();
|
|
||||||
url.set_host(Some(default_host.as_str()));
|
|
||||||
|
|
||||||
ServerConfig {
|
ServerConfig {
|
||||||
default_host: url,
|
default_host: Url::parse("gemini://localhost").unwrap(),
|
||||||
www_root: PathBuf::from(www_root),
|
gem_root: PathBuf::from(""),
|
||||||
addrs: Vec::new(),
|
addrs: Vec::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn set_default_host(&mut self, default_host: String) {
|
||||||
|
let mut url = Url::parse("gemini://default").unwrap();
|
||||||
|
|
||||||
|
match url.set_host(Some(default_host.as_str())) {
|
||||||
|
Ok(_) => {}
|
||||||
|
Err(e) => panic!(e),
|
||||||
|
};
|
||||||
|
self.default_host = url;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_gem_root(&mut self, gem_root: String) {
|
||||||
|
self.gem_root = PathBuf::from(gem_root);
|
||||||
|
}
|
||||||
|
|
||||||
pub fn add_addr(&mut self, addr: String) {
|
pub fn add_addr(&mut self, addr: String) {
|
||||||
self.addrs.push(addr.parse().unwrap());
|
self.addrs.push(match addr.parse() {
|
||||||
|
Ok(addr) => addr,
|
||||||
|
Err(e) => panic!(e),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -42,11 +55,21 @@ pub struct Server {
|
||||||
impl Server {
|
impl Server {
|
||||||
pub fn new(config: &ServerConfig) -> Server {
|
pub fn new(config: &ServerConfig) -> Server {
|
||||||
Server {
|
Server {
|
||||||
acceptor: build_acceptor(),
|
acceptor: Server::build_acceptor(),
|
||||||
config: config.clone(),
|
config: config.clone(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn build_acceptor() -> std::sync::Arc<SslAcceptor> {
|
||||||
|
let mut acceptor = SslAcceptor::mozilla_intermediate_v5(SslMethod::tls()).unwrap();
|
||||||
|
acceptor
|
||||||
|
.set_private_key_file("doc/key.pem", SslFiletype::PEM)
|
||||||
|
.unwrap();
|
||||||
|
acceptor.set_certificate_chain_file("doc/cert.pem").unwrap();
|
||||||
|
acceptor.check_private_key().unwrap();
|
||||||
|
return Arc::new(acceptor.build());
|
||||||
|
}
|
||||||
|
|
||||||
pub fn serve(&self) {
|
pub fn serve(&self) {
|
||||||
let listener = TcpListener::bind(&self.config.addrs[..]).unwrap();
|
let listener = TcpListener::bind(&self.config.addrs[..]).unwrap();
|
||||||
|
|
||||||
|
@ -57,34 +80,11 @@ impl Server {
|
||||||
let config = self.config.clone();
|
let config = self.config.clone();
|
||||||
thread::spawn(move || {
|
thread::spawn(move || {
|
||||||
let stream = acceptor.accept(stream).unwrap();
|
let stream = acceptor.accept(stream).unwrap();
|
||||||
handle_client(&config, stream);
|
handler::handle_request(&config, stream);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
Err(e) => { /* connection failed */ }
|
Err(_) => { /* connection failed */ }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn handle_client(config: &ServerConfig, mut stream: SslStream<TcpStream>) {
|
|
||||||
let mut buffer = [0; 1026];
|
|
||||||
stream.ssl_read(&mut buffer);
|
|
||||||
let request = String::from_utf8(buffer.to_vec()).unwrap();
|
|
||||||
|
|
||||||
let location = match Url::parse(&request) {
|
|
||||||
Ok(url) => url,
|
|
||||||
Err(e) => config.default_host.join(&request).unwrap(),
|
|
||||||
};
|
|
||||||
|
|
||||||
handler::handle(config, location, &mut stream);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn build_acceptor() -> std::sync::Arc<SslAcceptor> {
|
|
||||||
let mut acceptor = SslAcceptor::mozilla_intermediate_v5(SslMethod::tls()).unwrap();
|
|
||||||
acceptor
|
|
||||||
.set_private_key_file("doc/key.pem", SslFiletype::PEM)
|
|
||||||
.unwrap();
|
|
||||||
acceptor.set_certificate_chain_file("doc/cert.pem").unwrap();
|
|
||||||
acceptor.check_private_key().unwrap();
|
|
||||||
return Arc::new(acceptor.build());
|
|
||||||
}
|
|
||||||
|
|
|
@ -2,12 +2,11 @@ use std::vec::Vec;
|
||||||
|
|
||||||
#[derive(Copy, Clone)]
|
#[derive(Copy, Clone)]
|
||||||
pub enum Status {
|
pub enum Status {
|
||||||
Input = 10,
|
|
||||||
Success = 20,
|
Success = 20,
|
||||||
Redirect = 30,
|
|
||||||
TemporaryFailure = 40,
|
|
||||||
PermanentFailure = 50,
|
PermanentFailure = 50,
|
||||||
ClientCertificateRequired = 60,
|
NotFound = 51,
|
||||||
|
ProxyRequestRefused = 53,
|
||||||
|
BadRequest = 59,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct Header {
|
pub struct Header {
|
||||||
|
@ -32,18 +31,18 @@ impl Header {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn invalid_protocol() -> Header {
|
pub fn permanent_failure() -> Header {
|
||||||
Header::new(Status::PermanentFailure, "this protocol is not supported")
|
Header::new(Status::PermanentFailure, "permanent failure")
|
||||||
}
|
|
||||||
|
|
||||||
pub fn not_understood() -> Header {
|
|
||||||
Header::new(Status::PermanentFailure, "request not understood")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn not_found() -> Header {
|
pub fn not_found() -> Header {
|
||||||
Header::new(Status::PermanentFailure, "resource not found")
|
Header::new(Status::NotFound, "not found")
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn internal_error() -> Header {
|
pub fn proxy_request_refused() -> Header {
|
||||||
Header::new(Status::PermanentFailure, "internal server error")
|
Header::new(Status::ProxyRequestRefused, "proxy request refused")
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn bad_request() -> Header {
|
||||||
|
Header::new(Status::BadRequest, "bad request")
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue