mirror of
https://github.com/DefectingCat/candy
synced 2025-07-16 17:21:34 +00:00
feat(reverse): add custom not found
This commit is contained in:
@ -1,21 +1,111 @@
|
|||||||
use std::time::Duration;
|
use std::{path::PathBuf, time::Duration};
|
||||||
|
|
||||||
|
use anyhow::Context;
|
||||||
use axum::{
|
use axum::{
|
||||||
body::Body,
|
body::Body,
|
||||||
extract::{Path, Request},
|
extract::{Path, Request},
|
||||||
response::{IntoResponse, Response},
|
response::{IntoResponse, Response},
|
||||||
};
|
};
|
||||||
use axum_extra::extract::Host;
|
use axum_extra::extract::Host;
|
||||||
use http::{HeaderName, Uri};
|
use http::{
|
||||||
|
HeaderName, HeaderValue, StatusCode, Uri,
|
||||||
|
header::{CONTENT_TYPE, ETAG, IF_NONE_MATCH},
|
||||||
|
};
|
||||||
|
use mime_guess::from_path;
|
||||||
use reqwest::Client;
|
use reqwest::Client;
|
||||||
|
use tokio::fs::File;
|
||||||
|
use tokio_util::io::ReaderStream;
|
||||||
|
|
||||||
use crate::{http::serve::resolve_parent_path, utils::parse_port_from_host};
|
use crate::{
|
||||||
|
http::serve::{calculate_etag, resolve_parent_path},
|
||||||
|
utils::parse_port_from_host,
|
||||||
|
};
|
||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
HOSTS,
|
HOSTS,
|
||||||
error::{RouteError, RouteResult},
|
error::{RouteError, RouteResult},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
macro_rules! custom_not_found {
|
||||||
|
($host_config:expr, $request:expr) => {{
|
||||||
|
let page = $host_config
|
||||||
|
.not_found_page
|
||||||
|
.as_ref()
|
||||||
|
.ok_or(RouteError::RouteNotFound())?;
|
||||||
|
let root = $host_config
|
||||||
|
.root
|
||||||
|
.as_ref()
|
||||||
|
.ok_or(RouteError::InternalError())?;
|
||||||
|
let path = format!("{}/{}", root, page.page);
|
||||||
|
tracing::debug!("custom not found path: {:?}", path);
|
||||||
|
|
||||||
|
let file = File::open(path.clone())
|
||||||
|
.await
|
||||||
|
.with_context(|| "open file failed")?;
|
||||||
|
|
||||||
|
let etag = calculate_etag(&file, path.as_str()).await?;
|
||||||
|
let mut response = Response::builder();
|
||||||
|
let mut not_modified = false;
|
||||||
|
|
||||||
|
if let Some(if_none_match) = $request.headers().get(IF_NONE_MATCH)
|
||||||
|
&& if_none_match
|
||||||
|
.to_str()
|
||||||
|
.with_context(|| "parse if-none-match failed")?
|
||||||
|
== etag
|
||||||
|
{
|
||||||
|
response = response.status(StatusCode::NOT_MODIFIED);
|
||||||
|
not_modified = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
let stream = if not_modified {
|
||||||
|
let empty = File::open(PathBuf::from("/dev/null"))
|
||||||
|
.await
|
||||||
|
.with_context(|| "open /dev/null failed")?;
|
||||||
|
ReaderStream::new(empty)
|
||||||
|
} else {
|
||||||
|
ReaderStream::new(file)
|
||||||
|
};
|
||||||
|
|
||||||
|
let body = Body::from_stream(stream);
|
||||||
|
|
||||||
|
let mime = from_path(path).first_or_octet_stream();
|
||||||
|
response
|
||||||
|
.headers_mut()
|
||||||
|
.with_context(|| "insert header failed")?
|
||||||
|
.insert(
|
||||||
|
CONTENT_TYPE,
|
||||||
|
HeaderValue::from_str(mime.as_ref()).with_context(|| "insert header failed")?,
|
||||||
|
);
|
||||||
|
response
|
||||||
|
.headers_mut()
|
||||||
|
.with_context(|| "insert header failed")?
|
||||||
|
.insert(
|
||||||
|
ETAG,
|
||||||
|
HeaderValue::from_str(&etag).with_context(|| "insert header failed")?,
|
||||||
|
);
|
||||||
|
|
||||||
|
let response = response
|
||||||
|
.body(body)
|
||||||
|
.with_context(|| "Failed to build HTTP response with body")?;
|
||||||
|
Ok(response)
|
||||||
|
}};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Handles the reverse proxy logic for incoming requests.
|
||||||
|
/// This function:
|
||||||
|
/// 1. Extracts the request path, host, and other details.
|
||||||
|
/// 2. Resolves the parent path and proxy configuration.
|
||||||
|
/// 3. Forwards the request to the configured proxy server.
|
||||||
|
/// 4. Returns the response from the proxy server to the client.
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
/// * `req_uri` - The URI of the incoming request.
|
||||||
|
/// * `path` - Optional path parameter extracted from the request.
|
||||||
|
/// * `host` - The host header from the request.
|
||||||
|
/// * `req` - The incoming HTTP request.
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
/// A `RouteResult` containing the response from the proxy server or an error.
|
||||||
#[axum::debug_handler]
|
#[axum::debug_handler]
|
||||||
pub async fn serve(
|
pub async fn serve(
|
||||||
req_uri: Uri,
|
req_uri: Uri,
|
||||||
@ -42,8 +132,7 @@ pub async fn serve(
|
|||||||
.ok_or(RouteError::RouteNotFound())?;
|
.ok_or(RouteError::RouteNotFound())?;
|
||||||
tracing::debug!("proxy pass: {:?}", proxy_config);
|
tracing::debug!("proxy pass: {:?}", proxy_config);
|
||||||
let Some(ref proxy_pass) = proxy_config.proxy_pass else {
|
let Some(ref proxy_pass) = proxy_config.proxy_pass else {
|
||||||
// return custom_not_found!(host_route, request).await;
|
return custom_not_found!(proxy_config, req);
|
||||||
return Err(RouteError::RouteNotFound());
|
|
||||||
};
|
};
|
||||||
let uri = format!("{proxy_pass}{path_query}");
|
let uri = format!("{proxy_pass}{path_query}");
|
||||||
tracing::debug!("reverse proxy uri: {:?}", &uri);
|
tracing::debug!("reverse proxy uri: {:?}", &uri);
|
||||||
@ -99,6 +188,8 @@ pub async fn serve(
|
|||||||
Ok(res)
|
Ok(res)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Checks if a given header should be excluded from being forwarded in the reverse proxy.
|
||||||
|
/// Headers like "host", "connection", etc., are typically excluded to avoid conflicts or security issues.
|
||||||
fn is_exclude_header(name: &HeaderName) -> bool {
|
fn is_exclude_header(name: &HeaderName) -> bool {
|
||||||
matches!(
|
matches!(
|
||||||
name.as_str(),
|
name.as_str(),
|
||||||
@ -113,6 +204,8 @@ fn is_exclude_header(name: &HeaderName) -> bool {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Copies headers from one `HeaderMap` to another, excluding headers specified in `is_exclude_header`.
|
||||||
|
/// This ensures only relevant headers are forwarded, avoiding conflicts or security issues.
|
||||||
fn copy_headers(from: &http::HeaderMap, to: &mut http::HeaderMap) {
|
fn copy_headers(from: &http::HeaderMap, to: &mut http::HeaderMap) {
|
||||||
for (name, value) in from.iter() {
|
for (name, value) in from.iter() {
|
||||||
if !is_exclude_header(name) {
|
if !is_exclude_header(name) {
|
||||||
|
@ -2,18 +2,16 @@ use std::{path::PathBuf, str::FromStr, time::UNIX_EPOCH};
|
|||||||
|
|
||||||
use anyhow::{Context, anyhow};
|
use anyhow::{Context, anyhow};
|
||||||
use axum::{
|
use axum::{
|
||||||
|
body::Body,
|
||||||
extract::{Path, Request},
|
extract::{Path, Request},
|
||||||
response::{IntoResponse, Response},
|
response::{IntoResponse, Response},
|
||||||
};
|
};
|
||||||
use axum_extra::extract::Host;
|
use axum_extra::extract::Host;
|
||||||
use dashmap::mapref::one::Ref;
|
use dashmap::mapref::one::Ref;
|
||||||
use futures_util::StreamExt;
|
|
||||||
use http::{
|
use http::{
|
||||||
HeaderValue, StatusCode, Uri,
|
HeaderValue, StatusCode, Uri,
|
||||||
header::{CONTENT_TYPE, ETAG, IF_NONE_MATCH},
|
header::{CONTENT_TYPE, ETAG, IF_NONE_MATCH},
|
||||||
};
|
};
|
||||||
use http_body_util::StreamBody;
|
|
||||||
use hyper::body::Frame;
|
|
||||||
use mime_guess::from_path;
|
use mime_guess::from_path;
|
||||||
use tokio::fs::{self, File};
|
use tokio::fs::{self, File};
|
||||||
use tokio_util::io::ReaderStream;
|
use tokio_util::io::ReaderStream;
|
||||||
@ -51,7 +49,7 @@ macro_rules! custom_not_found {
|
|||||||
let path = format!("{}/{}", root, page.page);
|
let path = format!("{}/{}", root, page.page);
|
||||||
let status = StatusCode::from_str(page.status.to_string().as_ref())
|
let status = StatusCode::from_str(page.status.to_string().as_ref())
|
||||||
.map_err(|_| RouteError::BadRequest())?;
|
.map_err(|_| RouteError::BadRequest())?;
|
||||||
debug!("custom not found path: {:?}", path);
|
tracing::debug!("custom not found path: {:?}", path);
|
||||||
match stream_file(path.into(), $request, Some(status)).await {
|
match stream_file(path.into(), $request, Some(status)).await {
|
||||||
Ok(res) => RouteResult::Ok(res),
|
Ok(res) => RouteResult::Ok(res),
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
@ -234,7 +232,7 @@ async fn stream_file(
|
|||||||
.await
|
.await
|
||||||
.with_context(|| "open file failed")?;
|
.with_context(|| "open file failed")?;
|
||||||
|
|
||||||
let path_str = path.to_str().ok_or(anyhow!(""))?;
|
let path_str = path.to_str().ok_or(anyhow!("convert path to str failed"))?;
|
||||||
let etag = calculate_etag(&file, path_str).await?;
|
let etag = calculate_etag(&file, path_str).await?;
|
||||||
|
|
||||||
let mut response = Response::builder();
|
let mut response = Response::builder();
|
||||||
@ -260,8 +258,9 @@ async fn stream_file(
|
|||||||
} else {
|
} else {
|
||||||
ReaderStream::new(file)
|
ReaderStream::new(file)
|
||||||
};
|
};
|
||||||
let stream = stream.map(|res| res.map(Frame::data));
|
// let stream = stream.map(|res| res.map(Frame::data));
|
||||||
let body = StreamBody::new(stream);
|
// let body = StreamBody::new(stream);
|
||||||
|
let body = Body::from_stream(stream);
|
||||||
|
|
||||||
let mime = from_path(path).first_or_octet_stream();
|
let mime = from_path(path).first_or_octet_stream();
|
||||||
response
|
response
|
||||||
@ -281,11 +280,13 @@ async fn stream_file(
|
|||||||
if let Some(status) = status {
|
if let Some(status) = status {
|
||||||
response = response.status(status);
|
response = response.status(status);
|
||||||
}
|
}
|
||||||
let response = response.body(body).with_context(|| "")?;
|
let response = response
|
||||||
|
.body(body)
|
||||||
|
.with_context(|| "Failed to build HTTP response with body")?;
|
||||||
Ok(response)
|
Ok(response)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn calculate_etag(file: &File, path: &str) -> anyhow::Result<String> {
|
pub async fn calculate_etag(file: &File, path: &str) -> anyhow::Result<String> {
|
||||||
// calculate file metadata as etag
|
// calculate file metadata as etag
|
||||||
let metadata = file
|
let metadata = file
|
||||||
.metadata()
|
.metadata()
|
||||||
|
Reference in New Issue
Block a user