394 lines
16 KiB
Rust
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)))
|
|
}
|
|
}
|