mirror of
https://github.com/telemt/telemt.git
synced 2026-10-30 12:30:34 +03:00
WEB
Co-Authored-By: brekotis <93345790+brekotis@users.noreply.github.com> Co-Authored-By: John Preston <17900494+john-preston@users.noreply.github.com>
This commit is contained in:
@@ -0,0 +1,284 @@
|
||||
use std::error::Error;
|
||||
use std::net::SocketAddr;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
use bytes::Bytes;
|
||||
use http_body_util::{BodyExt, Empty};
|
||||
use hyper::header::{self, HeaderName, HeaderValue};
|
||||
use hyper::{Method, Request, StatusCode, Uri};
|
||||
use hyper_util::rt::TokioIo;
|
||||
use tokio::net::TcpStream;
|
||||
|
||||
use super::{
|
||||
BoxError, HttpBody, HttpResponse, bad_gateway, full_response, generic_not_found,
|
||||
insert_header,
|
||||
};
|
||||
use crate::config::{WebRuntimeDecoy, WebRuntimeVhost};
|
||||
use crate::web::manager::WebProcessRuntime;
|
||||
|
||||
/// Serves the configured ordinary site after optionally removing carrier material.
|
||||
pub(super) async fn serve_decoy<B>(
|
||||
mut request: Request<B>,
|
||||
vhost: Arc<WebRuntimeVhost>,
|
||||
sanitize_transport: bool,
|
||||
runtime: &WebProcessRuntime,
|
||||
) -> HttpResponse
|
||||
where
|
||||
B: hyper::body::Body<Data = Bytes> + Send + 'static,
|
||||
B::Error: Error + Send + Sync + 'static,
|
||||
{
|
||||
if sanitize_transport {
|
||||
sanitize_transport_request(&mut request);
|
||||
}
|
||||
let (parts, body) = request.into_parts();
|
||||
let body = if sanitize_transport {
|
||||
Empty::<Bytes>::new()
|
||||
.map_err(|never| -> BoxError { match never {} })
|
||||
.boxed_unsync()
|
||||
} else {
|
||||
body.map_err(|error| -> BoxError { Box::new(error) })
|
||||
.boxed_unsync()
|
||||
};
|
||||
let request = Request::from_parts(parts, body);
|
||||
match &vhost.decoy {
|
||||
WebRuntimeDecoy::StaticDirectory(site) => serve_static(request, site),
|
||||
WebRuntimeDecoy::HttpUpstream { addr, authority } => {
|
||||
proxy_to_upstream(
|
||||
request,
|
||||
*addr,
|
||||
authority,
|
||||
Duration::from_secs(vhost.decoy_header_secs),
|
||||
runtime,
|
||||
)
|
||||
.await
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn serve_static<B>(request: Request<B>, site: &crate::config::WebStaticSite) -> HttpResponse {
|
||||
if !matches!(*request.method(), Method::GET | Method::HEAD) {
|
||||
return static_entry(request, site, None, StatusCode::NOT_FOUND);
|
||||
}
|
||||
let path = request.uri().path();
|
||||
let resolved = resolve_static_path(path, site);
|
||||
let status = if resolved.is_some() {
|
||||
StatusCode::OK
|
||||
} else {
|
||||
StatusCode::NOT_FOUND
|
||||
};
|
||||
static_entry(request, site, resolved, status)
|
||||
}
|
||||
|
||||
fn static_entry<B>(
|
||||
request: Request<B>,
|
||||
site: &crate::config::WebStaticSite,
|
||||
route: Option<&str>,
|
||||
status: StatusCode,
|
||||
) -> HttpResponse {
|
||||
let fallback = format!("/{}", site.index);
|
||||
let not_found = site.assets.contains_key("/404.html").then_some("/404.html");
|
||||
let route = route.or(not_found).unwrap_or(&fallback);
|
||||
let Some(asset) = site.assets.get(route) else {
|
||||
return generic_not_found();
|
||||
};
|
||||
let not_modified = status == StatusCode::OK
|
||||
&& request
|
||||
.headers()
|
||||
.get(header::IF_NONE_MATCH)
|
||||
.and_then(|value| value.to_str().ok())
|
||||
== Some(asset.etag.as_str());
|
||||
let response_body = if request.method() == Method::HEAD || not_modified {
|
||||
Bytes::new()
|
||||
} else {
|
||||
asset.body.clone()
|
||||
};
|
||||
let mut response = full_response(
|
||||
if not_modified {
|
||||
StatusCode::NOT_MODIFIED
|
||||
} else {
|
||||
status
|
||||
},
|
||||
response_body,
|
||||
);
|
||||
insert_header(&mut response, header::CONTENT_TYPE, asset.content_type);
|
||||
insert_header(&mut response, header::ETAG, &asset.etag);
|
||||
insert_header(
|
||||
&mut response,
|
||||
header::CONTENT_LENGTH,
|
||||
&asset.body.len().to_string(),
|
||||
);
|
||||
response.headers_mut().insert(
|
||||
header::CACHE_CONTROL,
|
||||
if status.is_client_error() || request.uri().query().is_some() {
|
||||
HeaderValue::from_static("no-store")
|
||||
} else {
|
||||
HeaderValue::from_static("public, max-age=300")
|
||||
},
|
||||
);
|
||||
response.headers_mut().insert(
|
||||
header::CONTENT_SECURITY_POLICY,
|
||||
HeaderValue::from_static("default-src 'self'; style-src 'self'; img-src 'self'; worker-src 'none'; frame-ancestors 'none'; base-uri 'none'; form-action 'none'"),
|
||||
);
|
||||
response.headers_mut().insert(
|
||||
header::REFERRER_POLICY,
|
||||
HeaderValue::from_static("strict-origin-when-cross-origin"),
|
||||
);
|
||||
response.headers_mut().insert(
|
||||
header::X_CONTENT_TYPE_OPTIONS,
|
||||
HeaderValue::from_static("nosniff"),
|
||||
);
|
||||
response.headers_mut().insert(header::X_FRAME_OPTIONS, HeaderValue::from_static("DENY"));
|
||||
response
|
||||
}
|
||||
|
||||
fn resolve_static_path<'a>(
|
||||
path: &str,
|
||||
site: &'a crate::config::WebStaticSite,
|
||||
) -> Option<&'a str> {
|
||||
if !path.starts_with('/')
|
||||
|| path.contains('\\')
|
||||
|| path.contains("//")
|
||||
|| path.split('/').any(|part| matches!(part, "." | ".."))
|
||||
{
|
||||
return None;
|
||||
}
|
||||
let root;
|
||||
let route = if path == "/" {
|
||||
root = format!("/{}", site.index);
|
||||
root.as_str()
|
||||
} else {
|
||||
path
|
||||
};
|
||||
if site.assets.contains_key(route) {
|
||||
return site.assets.get_key_value(route).map(|(key, _)| key.as_str());
|
||||
}
|
||||
if route == "/favicon.ico" && site.assets.contains_key("/favicon.svg") {
|
||||
return Some("/favicon.svg");
|
||||
}
|
||||
if !route.rsplit('/').next().unwrap_or_default().contains('.') {
|
||||
let html = format!("{route}.html");
|
||||
return site
|
||||
.assets
|
||||
.get_key_value(&html)
|
||||
.map(|(key, _)| key.as_str());
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
async fn proxy_to_upstream(
|
||||
mut request: Request<HttpBody>,
|
||||
addr: SocketAddr,
|
||||
authority: &str,
|
||||
header_timeout: Duration,
|
||||
runtime: &WebProcessRuntime,
|
||||
) -> HttpResponse {
|
||||
remove_hop_by_hop(request.headers_mut());
|
||||
if let Ok(host) = HeaderValue::from_str(authority) {
|
||||
request.headers_mut().insert(header::HOST, host);
|
||||
}
|
||||
let path_and_query = request
|
||||
.uri()
|
||||
.path_and_query()
|
||||
.map(|value| value.as_str())
|
||||
.unwrap_or("/");
|
||||
let Ok(uri) = path_and_query.parse::<Uri>() else {
|
||||
return bad_gateway();
|
||||
};
|
||||
*request.uri_mut() = uri;
|
||||
let stream = match tokio::time::timeout(header_timeout, TcpStream::connect(addr)).await {
|
||||
Ok(Ok(stream)) => stream,
|
||||
_ => return bad_gateway(),
|
||||
};
|
||||
let max_header_bytes = runtime
|
||||
.active_generation()
|
||||
.config()
|
||||
.web
|
||||
.limits
|
||||
.max_header_bytes;
|
||||
let mut builder = hyper::client::conn::http1::Builder::new();
|
||||
builder.max_buf_size(max_header_bytes);
|
||||
let (mut sender, connection) = match tokio::time::timeout(
|
||||
header_timeout,
|
||||
builder.handshake(TokioIo::new(stream)),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(Ok(parts)) => parts,
|
||||
_ => return bad_gateway(),
|
||||
};
|
||||
runtime.spawn_auxiliary(async move {
|
||||
let _ = connection.await;
|
||||
});
|
||||
let mut response = match tokio::time::timeout(header_timeout, sender.send_request(request)).await
|
||||
{
|
||||
Ok(Ok(response)) => response,
|
||||
_ => return bad_gateway(),
|
||||
};
|
||||
remove_hop_by_hop(response.headers_mut());
|
||||
response.map(|body| {
|
||||
body.map_err(|error| -> BoxError { Box::new(error) })
|
||||
.boxed_unsync()
|
||||
})
|
||||
}
|
||||
|
||||
fn sanitize_transport_request<B>(request: &mut Request<B>) {
|
||||
for name in [
|
||||
header::AUTHORIZATION,
|
||||
header::CONTENT_LENGTH,
|
||||
header::CONTENT_TYPE,
|
||||
header::UPGRADE,
|
||||
HeaderName::from_static("sec-websocket-key"),
|
||||
HeaderName::from_static("sec-websocket-protocol"),
|
||||
HeaderName::from_static("sec-websocket-version"),
|
||||
HeaderName::from_static("x-down-cursor"),
|
||||
HeaderName::from_static("x-lane-id"),
|
||||
HeaderName::from_static("x-up-seq"),
|
||||
] {
|
||||
request.headers_mut().remove(name);
|
||||
}
|
||||
request
|
||||
.headers_mut()
|
||||
.insert(header::CONNECTION, HeaderValue::from_static("close"));
|
||||
}
|
||||
|
||||
fn remove_hop_by_hop(headers: &mut hyper::HeaderMap) {
|
||||
let nominated = headers
|
||||
.get_all(header::CONNECTION)
|
||||
.iter()
|
||||
.filter_map(|value| value.to_str().ok())
|
||||
.flat_map(|value| value.split(','))
|
||||
.filter_map(|value| HeaderName::from_bytes(value.trim().as_bytes()).ok())
|
||||
.collect::<Vec<_>>();
|
||||
for name in nominated {
|
||||
headers.remove(name);
|
||||
}
|
||||
for name in [
|
||||
header::CONNECTION,
|
||||
header::PROXY_AUTHENTICATE,
|
||||
header::PROXY_AUTHORIZATION,
|
||||
header::TE,
|
||||
header::TRAILER,
|
||||
header::TRANSFER_ENCODING,
|
||||
header::UPGRADE,
|
||||
HeaderName::from_static("keep-alive"),
|
||||
HeaderName::from_static("proxy-connection"),
|
||||
] {
|
||||
headers.remove(name);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn static_resolver_rejects_rewritten_paths() {
|
||||
let site = crate::config::WebStaticSite {
|
||||
assets: std::collections::BTreeMap::new(),
|
||||
index: "index.html".to_string(),
|
||||
};
|
||||
assert!(resolve_static_path("/../index.html", &site).is_none());
|
||||
assert!(resolve_static_path("//index.html", &site).is_none());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user