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 = (*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 = (*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::(); 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 = 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::(); let subhostpart = host.chars().take_while(|c| *c != '.').collect::(); 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 { 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 { let modified_since_parsed: DateTime = 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, &HashMap), ) -> Option { let modified_since_parsed: DateTime = 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 { Ok::(HttpResponse::generate_500(Some(&err))) } }