2 Commits

Author SHA1 Message Date
xfy
f0da74a06e feat: add lua script execture 2025-06-29 20:24:13 +08:00
xfy
a5041197c7 feat: add 301 forward for auto index 2025-06-29 19:01:43 +08:00
3 changed files with 83 additions and 13 deletions

27
src/http/lua.rs Normal file
View File

@ -0,0 +1,27 @@
pub async fn lua(
req_uri: Uri,
path: Option<Path<String>>,
Host(host): Host,
mut req: Request<Body>,
) -> RouteResult<impl IntoResponse> {
let req_path = req.uri().path();
let path_query = req
.uri()
.path_and_query()
.map(|v| v.as_str())
.unwrap_or(req_path);
let scheme = req.uri().scheme_str().unwrap_or("http");
let port = parse_port_from_host(&host, scheme).ok_or(RouteError::BadRequest())?;
let route_map = &HOSTS.get(&port).ok_or(RouteError::BadRequest())?.route_map;
tracing::debug!("Route map entries: {:?}", route_map);
let parent_path = resolve_parent_path(&req_uri, path.as_ref());
let route_config = route_map
.get(&parent_path)
.ok_or(RouteError::RouteNotFound())?;
let lua_script = route_config
.lua_script
.as_ref()
.ok_or(RouteError::InternalError())?;
}

View File

@ -4,6 +4,7 @@ use anyhow::anyhow;
use axum::{Router, extract::DefaultBodyLimit, middleware, routing::get};
use axum_server::{Handle, tls_rustls::RustlsConfig};
use dashmap::DashMap;
use mlua::Lua;
use tower::ServiceBuilder;
use tower_http::{compression::CompressionLayer, timeout::TimeoutLayer};
use tracing::{debug, info, warn};
@ -33,6 +34,9 @@ pub mod reverse_proxy;
/// }
pub static HOSTS: LazyLock<DashMap<u16, SettingHost>> = LazyLock::new(DashMap::new);
/// lua 脚本执行器
pub static LUA_EXECUTOR: LazyLock<Lua> = LazyLock::new(Lua::new);
pub async fn make_server(host: SettingHost) -> anyhow::Result<()> {
let mut router = Router::new();
let host_to_save = host.clone();
@ -42,6 +46,7 @@ pub async fn make_server(host: SettingHost) -> anyhow::Result<()> {
for host_route in &host.route {
// lua script
if let Some(lua_path) = &host_route.lua_script {
// papare lua script
continue;
}

View File

@ -15,7 +15,7 @@ use axum_extra::extract::Host;
use dashmap::mapref::one::Ref;
use http::{
HeaderMap, HeaderValue, StatusCode, Uri,
header::{CONTENT_TYPE, ETAG, IF_NONE_MATCH},
header::{CONTENT_TYPE, ETAG, IF_NONE_MATCH, LOCATION},
};
use mime_guess::from_path;
use tokio::fs::{self, File};
@ -75,7 +75,7 @@ async fn custom_page(
match stream_file(path.into(), request, Some(status)).await {
Ok(res) => RouteResult::Ok(res),
Err(e) => {
println!("Failed to stream file: {:?}", e);
error!("Failed to stream file: {:?}", e);
RouteResult::Err(RouteError::InternalError())
}
}
@ -149,7 +149,7 @@ pub async fn serve(
#[allow(clippy::unnecessary_to_owned)]
let path = path.to_string();
if path.contains('.') {
(root.into(), vec![format!("{}/{}", root, path)])
(root.into(), vec![format!("{}{}", root, path)])
} else {
generate_default_index(&host_route, &format!("{root}/{path}"))
}
@ -157,6 +157,7 @@ pub async fn serve(
generate_default_index(&host_route, root)
};
debug!("request index file {:?}", path_arr);
debug!("req_path: {:?}", req_path);
// Try each candidate path in order:
// - Return the first successfully streamed file.
// - If all fail, return a `RouteNotFound` error.
@ -172,6 +173,26 @@ pub async fn serve(
let path_exists = match path_exists {
Some(path_exists) => path_exists,
None => {
let uri_path = uri.path();
// 如果请求路径不以 / 结尾,则返回 301 Moved Permanently 状态码
if !uri_path.ends_with('/') {
let mut response = Response::builder();
let stream = empty_stream().await?;
let body = Body::from_stream(stream);
response
.headers_mut()
.with_context(|| "insert header failed")?
.insert(
LOCATION,
HeaderValue::from_str(format!("{uri_path}/").as_str())
.with_context(|| "insert header failed")?,
);
response = response.status(StatusCode::MOVED_PERMANENTLY);
let response = response
.body(body)
.with_context(|| "Failed to build HTTP response with body")?;
return Ok(response);
}
// 生成自动目录索引
if host_route.auto_index {
// HTML 中的标题路径,需要移除掉配置文件中的 root = "./html" 字段
@ -267,16 +288,8 @@ async fn stream_file(
}
}
#[cfg(windows)]
let null = PathBuf::from("NUL");
#[cfg(not(windows))]
let null = PathBuf::from("/dev/null");
let stream = if not_modified {
let empty = File::open(null)
.await
.with_context(|| "open /dev/null failed")?;
ReaderStream::new(empty)
empty_stream().await?
} else {
ReaderStream::new(file)
};
@ -598,7 +611,11 @@ async fn list_dir(host_root_str: &str, path: &PathBuf) -> anyhow::Result<Vec<Dir
.strip_prefix(&host_root_str)
.ok_or(anyhow!("strip prefix failed"))?
.to_string();
let path = format!("./{path}");
let path = if is_dir {
format!("./{path}/")
} else {
format!("./{path}")
};
// 创建并返回目录条目信息
let dir = DirList {
name,
@ -619,3 +636,24 @@ async fn list_dir(host_root_str: &str, path: &PathBuf) -> anyhow::Result<Vec<Dir
Ok(list)
}
/// 创建一个空数据流,用于返回空响应或占位数据
///
/// 在不同操作系统上,会自动选择对应的空设备文件:
/// - Windows: NUL
/// - Unix/Linux: /dev/null
///
/// 返回一个异步流,内容为一个空文件的数据流
///
/// # 错误处理
/// 如果无法打开空设备文件,会返回带有上下文信息的错误
pub async fn empty_stream() -> anyhow::Result<ReaderStream<File>> {
#[cfg(windows)]
let null = PathBuf::from("NUL");
#[cfg(not(windows))]
let null = PathBuf::from("/dev/null");
let empty = File::open(null)
.await
.with_context(|| "open /dev/null failed")?;
Ok(ReaderStream::new(empty))
}