static-site-server-rs/src/static_http_server.rs

394 lines
16 KiB
Rust

extern crate chrono;
extern crate http;
extern crate mime_guess;
extern crate serde;
use chrono::offset::Utc;
use chrono::DateTime;
use http::status::StatusCode;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::convert::TryFrom;
use std::io::{self, prelude::*};
use std::net::TcpStream;
use std::path::Path;
use std::time::{Duration, SystemTime};
use crate::http_request::HttpRequest;
use crate::http_response::HttpResponse;
use crate::server_config::ServerConfig;
use crate::server_routes_scan::ServerRoutesScan;
use crate::static_http_server_route::{ServingMode, StaticHttpServerRoute};
use crate::subdomain_info::SubdomainInfo;
use crate::text_template::{InterruptDetermination, TextTemplate, TextTemplateRenderingError};
use crate::utils::{
content_type_for_exension_osstr, datetime_to_secs, parse_modified_since_or_epoch,
search_for_file,
};
#[derive(Clone, Deserialize, Serialize, Debug)]
pub struct StaticHttpServer {
pub config: ServerConfig,
pub scan: ServerRoutesScan,
}
impl StaticHttpServer {
pub fn new(server_config: ServerConfig) -> Self {
let mut this = Self {
config: server_config,
scan: ServerRoutesScan::default(),
};
this.check_for_changes();
this
}
pub fn check_for_changes(&mut self) {
self.scan.check_for_changes_mut(&self.config)
}
pub fn handle_stream(self, mut stream: TcpStream) {
let mut buffer: [u8; 2048] = [0; 2048];
if stream.read(&mut buffer).unwrap_or(0) > 0 {
match HttpRequest::try_from(buffer.as_ref()) {
Ok(request) => {
if let Err(err) = self.handle_request(&request, &stream) {
eprintln!("Warn: Could not yield a response successfully: {:?}", err);
}
}
Err(err) => {
eprintln!("Warn: Could not parse request: {:?}", err);
}
}
} else if let Err(err) = self.generate_404(None).send_reply(&stream) {
eprintln!("Warn: Could print 404 for empty request: {:?}", err);
}
let _ = stream.shutdown(std::net::Shutdown::Both);
}
fn log_request(
&self,
system_time: &SystemTime,
http_request: &HttpRequest,
stream_peer_addr_str: &str,
) {
let datetime: DateTime<Utc> = (*system_time).into();
let bt = format!("{}:{}", self.config.bind, self.config.port);
let now_formatted = datetime.format("%Y-%m-%d %T");
println!(
"[{}] => ... {} {} request: {}{} by {}",
now_formatted,
http_request.http,
http_request.verb,
http_request.headers.get("host").unwrap_or(&bt),
http_request.path,
stream_peer_addr_str,
);
}
fn log_response(
&self,
system_time: &SystemTime,
system_time_start: &SystemTime,
http_request: &HttpRequest,
response: &HttpResponse,
stream_peer_addr_str: &str,
) -> io::Result<()> {
let total_time = system_time
.duration_since(*system_time_start)
.map_err(|_| std::io::ErrorKind::InvalidData)?;
let datetime: DateTime<Utc> = (*system_time).into();
let now_formatted = datetime.format("%Y-%m-%d %T");
let bt = format!("{}:{}", self.config.bind, self.config.port);
println!(
"[{}] <= {:?} {} {} request: {}{} by {}; replied {} bytes on {} microsseconds",
now_formatted,
response.http_code,
http_request.http,
http_request.verb,
http_request.headers.get("host").unwrap_or(&bt),
http_request.path,
stream_peer_addr_str,
response.payload.len(),
total_time.as_micros(),
);
Ok(())
}
fn handle_request(
&self,
http_request: &HttpRequest,
stream: &TcpStream,
) -> std::io::Result<()> {
let bt = format!("{}:{}", self.config.bind, self.config.port);
let host = http_request.headers.get("host").unwrap_or(&bt);
let subhost = host.chars().skip_while(|c| *c != '.').collect::<String>();
let system_time_start = SystemTime::now();
let stream_peer_addr_str = stream
.peer_addr()
.map(|x| format!("{}", x))
.unwrap_or_else(|_| "[?]".to_owned());
if self.config.log_accesses {
self.log_request(&system_time_start, http_request, &stream_peer_addr_str);
}
let response: HttpResponse = self
.scan
.graph
.get(host)
.or_else(|| self.scan.subdomains.get(&subhost))
.and_then(|routes| {
let mut response_: Option<HttpResponse> = None;
for route in routes.iter() {
if http_request.path.starts_with(&route.prefix) {
response_ = Some(
self.get_for_route(http_request, route)
.with_hsts(route.hsts),
);
break;
}
}
response_
})
.unwrap_or_else(|| self.generate_404(None));
if self.config.log_accesses
|| (self.config.log_redirects && response.http_code == StatusCode::FOUND)
|| (self.config.log_not_founds && response.http_code == StatusCode::NOT_FOUND)
|| (self.config.log_server_errors
&& response.http_code == StatusCode::INTERNAL_SERVER_ERROR)
{
if !self.config.log_accesses {
self.log_request(&system_time_start, http_request, &stream_peer_addr_str);
}
let system_time = SystemTime::now();
self.log_response(
&system_time,
&system_time_start,
http_request,
&response,
&stream_peer_addr_str,
)?;
}
response.send_reply(stream)
}
fn generate_404(&self, route_opt: Option<&StaticHttpServerRoute>) -> HttpResponse {
route_opt
.and_then(|route| route.generate_404())
.or_else(|| self.config.generate_404())
.unwrap_or_else(HttpResponse::generate_404)
}
fn get_for_route(
&self,
http_request: &HttpRequest,
route: &StaticHttpServerRoute,
) -> HttpResponse {
let bt = format!("{}:{}", self.config.bind, self.config.port);
let requested_file = http_request.path[route.prefix.len()..].to_string();
let host = http_request.headers.get("host").unwrap_or(&bt);
let subhostroot = host.chars().skip_while(|c| *c != '.').collect::<String>();
let subhostpart = host.chars().take_while(|c| *c != '.').collect::<String>();
let (discriminator_folder, domain_root): (Option<&str>, &str) =
if route.domain != *host && route.domain.ends_with(&subhostroot) {
(Some(&subhostpart), &subhostroot)
} else {
(None, host)
};
match route.mode {
ServingMode::Serving => search_for_file(
&route.path,
&requested_file,
&self.config.try_files,
discriminator_folder,
)
.filter_map(|canonical_path| {
self.generate_file_response(
&canonical_path,
http_request.headers.get("if-modified-since"),
)
})
.next(),
ServingMode::RedirectTo => Some(HttpResponse::generate_302(&route.redirect)),
ServingMode::RedirectKeepingRoute => Some(HttpResponse::generate_302(format!(
"{}/{}",
route.redirect.trim_end_matches('/'),
requested_file.trim_start_matches('/')
))),
ServingMode::RedirectKeepingRouteAndSubdomain => {
Some(HttpResponse::generate_302(format!(
"https://{}.{}/{}",
subhostpart,
route.redirect,
requested_file.trim_start_matches('/')
)))
}
ServingMode::Templating => search_for_file(
&route.path,
&requested_file,
&self.config.try_files,
discriminator_folder,
)
.filter_map(|canonical_path| {
self.generate_file_response(
&canonical_path,
http_request.headers.get("if-modified-since"),
)
})
.next()
.or_else(|| {
search_for_file(
&route.template_path,
&requested_file,
&self.config.try_templates,
discriminator_folder,
)
.chain(search_for_file(
&route.template_path,
"",
&self.config.try_templates,
discriminator_folder,
))
.flat_map(|path_template| {
search_for_file(
&route.path,
&requested_file,
&self.config.try_data,
discriminator_folder,
)
.map(move |path_data| (path_template.to_owned(), path_data))
})
.filter_map(|(path_template, path_data)| {
self.generate_template_reponse(
&path_data,
&path_template,
http_request.headers.get("if-modified-since"),
&SubdomainInfo::new(
host,
&subhostroot,
&subhostpart,
domain_root,
discriminator_folder,
),
route,
(&http_request.query_args, &http_request.headers),
)
})
.next()
}),
}
.unwrap_or_else(|| self.generate_404(Some(route)))
}
fn get_files_max_age(paths: &[&Path]) -> Option<u64> {
paths
.iter()
.map(|path| {
path.metadata()
.ok()
.and_then(|metadata| metadata.modified().ok())
.and_then(|systime| systime.duration_since(SystemTime::UNIX_EPOCH).ok())
.map(|duration| duration.as_secs())
})
.reduce(|acc, x| acc.and_then(|acc1| x.map(|x1| u64::max(acc1, x1))))
.flatten()
}
fn generate_file_response(
&self,
path: &Path,
modified_since: Option<&String>,
) -> Option<HttpResponse> {
let modified_since_parsed: DateTime<Utc> = parse_modified_since_or_epoch(modified_since);
let browser_cache = datetime_to_secs(modified_since_parsed);
let file_time_opt = Self::get_files_max_age(&[path]).map(|x| x as i64);
file_time_opt.and_then(|file_time| {
(browser_cache == file_time)
.then(|| HttpResponse::generate_304(modified_since_parsed.into()))
.or_else(|| {
std::fs::read(path).ok().map(|content| {
HttpResponse::new(
StatusCode::OK,
content.as_slice(),
content_type_for_exension_osstr(path.extension()),
Some(SystemTime::UNIX_EPOCH + Duration::from_secs(file_time as u64)),
None,
false,
)
})
})
})
}
fn generate_template_reponse(
&self,
path_data: &Path,
path_template: &Path,
modified_since: Option<&String>,
subdomain: &SubdomainInfo,
route: &StaticHttpServerRoute,
(query, headers): (&HashMap<String, String>, &HashMap<String, String>),
) -> Option<HttpResponse> {
let modified_since_parsed: DateTime<Utc> = parse_modified_since_or_epoch(modified_since);
let browser_cache = datetime_to_secs(modified_since_parsed);
let file_time_opt = Self::get_files_max_age(&[path_template, path_data]).map(|x| x as i64);
path_data
.extension()
.and_then(|ext_osstr| ext_osstr.to_str())
.and_then(|ext_str| self.config.datum_extension_parser.get(ext_str))
.and_then(|p| {
file_time_opt.and_then(|file_time| {
(browser_cache == file_time)
.then(|| HttpResponse::generate_304(modified_since_parsed.into()))
.or_else(|| {
std::fs::read(path_template)
.map_err(|tplerr| format!("{:?}", tplerr))
.and_then(|content_template| {
String::from_utf8(content_template)
.map_err(|tplerr| format!("{:?}", tplerr))
.and_then(|string_template| {
TextTemplate::try_from(string_template.as_ref())
.map_err(|tplerr| format!("{:?}", tplerr))
})
.and_then(|tpl| {
std::fs::read(path_data)
.map_err(|tplerr| format!("{:?}", tplerr))
.and_then(|content_data| {
String::from_utf8(content_data)
.map_err(|tplerr| format!("{:?}", tplerr))
.and_then(|string_data| {
tpl.render(subdomain, query, headers, *p, &string_data)
.map(|rnd| {
HttpResponse::from(rnd.with_time(Some(
SystemTime::UNIX_EPOCH
+ Duration::from_secs(file_time as u64),
)))
})
.or_else(|err| match err {
TextTemplateRenderingError::InterruptedRenderingWith(determination) =>{
match determination {
InterruptDetermination::RenderedResponse(resp) => Ok(resp),
InterruptDetermination::Return404 => Ok(self.generate_404(Some(route))),
}
},
e => Err(e),
})
.map_err(|tplerr| {
format!("{:?}", tplerr)
})
})
})
})
})
.or_else(Self::string_err_to_500)
.ok()
})
})
})
}
fn string_err_to_500(err: String) -> Result<HttpResponse, String> {
Ok::<HttpResponse, String>(HttpResponse::generate_500(Some(&err)))
}
}