v0.25.1
This commit is contained in:
1 parent
87e571c170
commit
fa7abc9e53
308 files changed
+49746
-50237
No files matched your search
@@ -0,0 +1,297 @@
|
||||
use super::{
|
||||
fastcgi::{method_not_allowed_response, php_for_path, php_response, protected_script_without_handler},
|
||||
filesystem::{list_directory_sync, path_components},
|
||||
htaccess,
|
||||
response::{
|
||||
add_service_headers, builtin_error_response, directory_listing, directory_redirect,
|
||||
file_response, io_response, status_response,
|
||||
},
|
||||
routing::{
|
||||
apply_rewrites, open_components, resolve_try_files, select_precompressed, RewriteOutcome,
|
||||
TryFilesOutcome,
|
||||
},
|
||||
security::{hardening_denied_method, hardening_denied_path, strip_unnecessary_headers},
|
||||
CompiledService, RequestContext,
|
||||
};
|
||||
use axum::{body::Body, response::Response};
|
||||
use http::{
|
||||
header::{CACHE_CONTROL, CONTENT_LENGTH, CONTENT_TYPE},
|
||||
HeaderMap, HeaderValue, Method, StatusCode, Uri,
|
||||
};
|
||||
use std::{io, path::PathBuf};
|
||||
|
||||
async fn serve_inner(
|
||||
service: &CompiledService,
|
||||
method: Method,
|
||||
headers: HeaderMap,
|
||||
uri: Uri,
|
||||
path: &str,
|
||||
body: Body,
|
||||
context: RequestContext,
|
||||
) -> Response {
|
||||
if crate::acl::validate_decoded_routing_path(path).is_err() {
|
||||
return status_response(StatusCode::BAD_REQUEST, "RustProxy: invalid Web Server path\n");
|
||||
}
|
||||
if hardening_denied_method(service, &method) || hardening_denied_path(service, path) {
|
||||
return builtin_error_response(StatusCode::FORBIDDEN, &method);
|
||||
}
|
||||
if method != Method::GET && method != Method::HEAD && service.php.is_none() {
|
||||
return method_not_allowed_response();
|
||||
}
|
||||
|
||||
if path_components(path).ok().is_some_and(|components| components.iter().any(|component| htaccess::protected_component(service.htaccess.as_ref(), component)))
|
||||
|| htaccess::denied(service.htaccess.as_ref(), path)
|
||||
{
|
||||
return builtin_error_response(StatusCode::FORBIDDEN, &method);
|
||||
}
|
||||
|
||||
let (rewritten_path, rewritten_query) = match apply_rewrites(service, path, uri.query()) {
|
||||
Ok(RewriteOutcome::Redirect(response)) => return response,
|
||||
Ok(RewriteOutcome::Internal { path, query }) => (path, query),
|
||||
Err(_) => {
|
||||
return status_response(
|
||||
StatusCode::BAD_REQUEST,
|
||||
"RustProxy: invalid Web Server rewrite target\n",
|
||||
)
|
||||
}
|
||||
};
|
||||
let (rewritten_path, rewritten_query) = match htaccess::apply_rewrites(
|
||||
service.htaccess.as_ref(),
|
||||
service,
|
||||
&method,
|
||||
&headers,
|
||||
&uri,
|
||||
&context,
|
||||
&rewritten_path,
|
||||
rewritten_query.as_deref(),
|
||||
).await {
|
||||
Ok(htaccess::RewriteOutcome::Redirect(response)) => return response,
|
||||
Ok(htaccess::RewriteOutcome::Forbidden) => return builtin_error_response(StatusCode::FORBIDDEN, &method),
|
||||
Ok(htaccess::RewriteOutcome::Internal { path, query }) => (path, query),
|
||||
Err(_) => return status_response(StatusCode::BAD_REQUEST, "RustProxy: invalid .htaccess rewrite target\n"),
|
||||
};
|
||||
if crate::acl::validate_decoded_routing_path(&rewritten_path).is_err()
|
||||
|| hardening_denied_path(service, &rewritten_path)
|
||||
|| path_components(&rewritten_path).ok().is_some_and(|components| components.iter().any(|component| htaccess::protected_component(service.htaccess.as_ref(), component)))
|
||||
|| htaccess::denied(service.htaccess.as_ref(), &rewritten_path)
|
||||
{
|
||||
return builtin_error_response(StatusCode::FORBIDDEN, &method);
|
||||
}
|
||||
let (effective_path, effective_query) = match resolve_try_files(
|
||||
service,
|
||||
&rewritten_path,
|
||||
rewritten_query.as_deref(),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(TryFilesOutcome::Path { path, query }) => (path, query),
|
||||
Ok(TryFilesOutcome::Status(status)) => return builtin_error_response(status, &method),
|
||||
Err(_) => {
|
||||
return status_response(
|
||||
StatusCode::BAD_REQUEST,
|
||||
"RustProxy: invalid Web Server try_files target\n",
|
||||
)
|
||||
}
|
||||
};
|
||||
|
||||
let components = match path_components(&effective_path) {
|
||||
Ok(components) => components,
|
||||
Err(error) => return io_response(&error),
|
||||
};
|
||||
if hardening_denied_path(service, &effective_path)
|
||||
|| components.iter().any(|component| htaccess::protected_component(service.htaccess.as_ref(), component))
|
||||
|| htaccess::denied(service.htaccess.as_ref(), &effective_path)
|
||||
{
|
||||
return builtin_error_response(StatusCode::FORBIDDEN, &method);
|
||||
}
|
||||
let opened = match open_components(service, components.clone()).await {
|
||||
Ok(opened) => opened,
|
||||
Err(error) => return io_response(&error),
|
||||
};
|
||||
let mut request_body = Some(body);
|
||||
|
||||
if opened.metadata.is_file() {
|
||||
let public_path = PathBuf::from(&effective_path);
|
||||
if let Some(php) = php_for_path(service, &public_path) {
|
||||
return php_response(
|
||||
service,
|
||||
php,
|
||||
method,
|
||||
headers,
|
||||
uri,
|
||||
effective_path,
|
||||
effective_query,
|
||||
request_body.take().unwrap_or_else(Body::empty),
|
||||
context,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
if protected_script_without_handler(service, &public_path) {
|
||||
return builtin_error_response(StatusCode::FORBIDDEN, &method);
|
||||
}
|
||||
if method != Method::GET && method != Method::HEAD {
|
||||
return method_not_allowed_response();
|
||||
}
|
||||
if let Some((compressed, encoding)) =
|
||||
select_precompressed(service, &components, &headers).await
|
||||
{
|
||||
return file_response(
|
||||
service,
|
||||
compressed,
|
||||
&public_path,
|
||||
&method,
|
||||
&headers,
|
||||
Some(encoding),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
return file_response(service, opened, &public_path, &method, &headers, None).await;
|
||||
}
|
||||
|
||||
if !uri.path().ends_with('/') {
|
||||
return directory_redirect(&uri, &method);
|
||||
}
|
||||
|
||||
for index in htaccess::index_files(service.htaccess.as_ref(), &components, &service.index) {
|
||||
let mut candidate = components.clone();
|
||||
candidate.push(index.clone());
|
||||
match open_components(service, candidate.clone()).await {
|
||||
Ok(index_opened) if index_opened.metadata.is_file() => {
|
||||
let mut script_path = effective_path.trim_end_matches('/').to_owned();
|
||||
script_path.push('/');
|
||||
script_path.push_str(index);
|
||||
if hardening_denied_path(service, &script_path) {
|
||||
return builtin_error_response(StatusCode::FORBIDDEN, &method);
|
||||
}
|
||||
let public_path = PathBuf::from(&script_path);
|
||||
if let Some(php) = php_for_path(service, &public_path) {
|
||||
return php_response(
|
||||
service,
|
||||
php,
|
||||
method,
|
||||
headers,
|
||||
uri,
|
||||
script_path,
|
||||
effective_query,
|
||||
request_body.take().unwrap_or_else(Body::empty),
|
||||
context,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
if protected_script_without_handler(service, &public_path) {
|
||||
return builtin_error_response(StatusCode::FORBIDDEN, &method);
|
||||
}
|
||||
if method != Method::GET && method != Method::HEAD {
|
||||
return method_not_allowed_response();
|
||||
}
|
||||
if let Some((compressed, encoding)) =
|
||||
select_precompressed(service, &candidate, &headers).await
|
||||
{
|
||||
return file_response(
|
||||
service,
|
||||
compressed,
|
||||
&public_path,
|
||||
&method,
|
||||
&headers,
|
||||
Some(encoding),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
return file_response(
|
||||
service,
|
||||
index_opened,
|
||||
&public_path,
|
||||
&method,
|
||||
&headers,
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
Ok(_) => continue,
|
||||
Err(error)
|
||||
if matches!(
|
||||
error.kind(),
|
||||
io::ErrorKind::NotFound | io::ErrorKind::NotADirectory
|
||||
) =>
|
||||
{
|
||||
continue
|
||||
}
|
||||
Err(error) => return io_response(&error),
|
||||
}
|
||||
}
|
||||
|
||||
if method != Method::GET && method != Method::HEAD {
|
||||
return method_not_allowed_response();
|
||||
}
|
||||
if !htaccess::directory_listing(service.htaccess.as_ref(), &components, service.directory_listing) {
|
||||
return builtin_error_response(StatusCode::FORBIDDEN, &method);
|
||||
}
|
||||
let service_for_list = service.clone();
|
||||
let components_for_list = components.clone();
|
||||
let entries = match tokio::task::spawn_blocking(move || {
|
||||
list_directory_sync(&service_for_list, &components_for_list)
|
||||
})
|
||||
.await
|
||||
{
|
||||
Ok(Ok(entries)) => entries,
|
||||
Ok(Err(error)) => return io_response(&error),
|
||||
Err(_) => {
|
||||
return status_response(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
"RustProxy: Web Server directory task failed\n",
|
||||
)
|
||||
}
|
||||
};
|
||||
let entries: Vec<_> = entries
|
||||
.into_iter()
|
||||
.filter(|entry| {
|
||||
if htaccess::protected_component(service.htaccess.as_ref(), &entry.name) {
|
||||
return false;
|
||||
}
|
||||
let child_path = if effective_path.ends_with('/') {
|
||||
format!("{effective_path}{}", entry.name)
|
||||
} else {
|
||||
format!("{effective_path}/{}", entry.name)
|
||||
};
|
||||
!hardening_denied_path(service, &child_path)
|
||||
})
|
||||
.collect();
|
||||
let listing = directory_listing(&effective_path, &entries);
|
||||
let mut response = Response::new(if method == Method::HEAD {
|
||||
Body::empty()
|
||||
} else {
|
||||
Body::from(listing.clone())
|
||||
});
|
||||
response.headers_mut().insert(
|
||||
CONTENT_TYPE,
|
||||
HeaderValue::from_static("text/html; charset=utf-8"),
|
||||
);
|
||||
response.headers_mut().insert(
|
||||
CONTENT_LENGTH,
|
||||
HeaderValue::from_str(&listing.len().to_string()).unwrap(),
|
||||
);
|
||||
response
|
||||
.headers_mut()
|
||||
.insert(CACHE_CONTROL, HeaderValue::from_static("no-store"));
|
||||
response
|
||||
}
|
||||
|
||||
/// Serve a decoded, validated routing path from a compiled Web Service.
|
||||
/// The caller is responsible for normal vhost/location ACL/auth/routing selection;
|
||||
/// this function performs filesystem handling plus optional Web Service routing,
|
||||
/// precompressed delivery and bounded FastCGI/PHP-FPM execution.
|
||||
pub async fn serve(
|
||||
service: &CompiledService,
|
||||
method: Method,
|
||||
headers: HeaderMap,
|
||||
uri: Uri,
|
||||
path: &str,
|
||||
body: Body,
|
||||
context: RequestContext,
|
||||
) -> Response {
|
||||
let mut response = serve_inner(service, method, headers, uri, path, body, context).await;
|
||||
add_service_headers(&mut response, service);
|
||||
htaccess::apply_response(service.htaccess.as_ref(), &mut response, path);
|
||||
strip_unnecessary_headers(&mut response, service);
|
||||
response
|
||||
}
|
||||
Reference in new issue
Block a user