use axum::extract::Request;
use axum::http::{header, HeaderMap, HeaderValue, Method, StatusCode};
use axum::middleware::{self, Next};
use axum::response::Response;
use axum::Router;
use clap::Parser;
use std::net::{IpAddr, Ipv4Addr, SocketAddr};
use std::path::PathBuf;
use tower_http::services::ServeDir;
use tracing::info;
mod ext;
mod server;
#[derive(Clone, Parser)]
#[command(version, about = "A basic HTTP file server")]
pub struct Config {
#[arg(short = 'a', long = "addr", conflicts_with_all = ["port", "public"])]
addr: Option<SocketAddr>,
#[arg(short = 'p', long = "port")]
port: Option<u16>,
#[arg(long = "public")]
public: bool,
#[arg(default_value = ".")]
root_dir: PathBuf,
#[arg(short = 'x')]
use_extensions: bool,
}
impl Config {
fn listen_addr(&self) -> SocketAddr {
if let Some(addr) = self.addr {
return addr;
}
let ip = if self.public {
IpAddr::V4(Ipv4Addr::UNSPECIFIED)
} else {
IpAddr::V4(Ipv4Addr::LOCALHOST)
};
let port = self.port.unwrap_or(4000);
SocketAddr::new(ip, port)
}
}
#[tokio::main]
async fn main() {
tracing_subscriber::fmt()
.with_env_filter(
tracing_subscriber::EnvFilter::try_from_default_env()
.unwrap_or_else(|_| "basic_http_server=info".parse().unwrap()),
)
.with_target(false)
.without_time()
.init();
let config = Config::parse();
let addr = config.listen_addr();
info!("basic-http-server {}", env!("CARGO_PKG_VERSION"));
info!("addr: http://{}", addr);
info!("root dir: {}", config.root_dir.display());
info!("extensions: {}", config.use_extensions);
let app = build_router(&config);
let listener = tokio::net::TcpListener::bind(addr)
.await
.expect("failed to bind address");
let local_addr = listener.local_addr().unwrap();
info!("listening on {}", local_addr);
eprintln!("listening on {}", local_addr);
axum::serve(listener, app)
.await
.expect("server error");
}
fn build_router(config: &Config) -> Router {
let serve_dir = ServeDir::new(&config.root_dir)
.append_index_html_on_directories(true);
if config.use_extensions {
let config_clone = config.clone();
Router::new()
.fallback_service(serve_dir)
.layer(middleware::from_fn(method_filter))
.layer(middleware::from_fn(cache_control))
.layer(middleware::from_fn_with_state(
config_clone.root_dir.clone(),
ext::source_text_middleware,
))
.layer(middleware::from_fn_with_state(
config_clone.root_dir.clone(),
ext::dir_list_middleware,
))
.layer(middleware::from_fn_with_state(
config_clone.root_dir.clone(),
ext::markdown_middleware,
))
} else {
Router::new()
.fallback_service(serve_dir)
.layer(middleware::from_fn(method_filter))
.layer(middleware::from_fn(cache_control))
.layer(middleware::from_fn(not_found_html))
}
}
async fn cache_control(req: Request, next: Next) -> Response {
let mut resp = next.run(req).await;
resp.headers_mut().insert(
header::CACHE_CONTROL,
HeaderValue::from_static("no-cache"),
);
resp
}
async fn method_filter(req: Request, next: Next) -> Response {
if req.method() != Method::GET && req.method() != Method::HEAD {
let mut headers = HeaderMap::new();
headers.insert(header::ALLOW, HeaderValue::from_static("GET, HEAD"));
return server::error_response_with_headers(StatusCode::METHOD_NOT_ALLOWED, headers);
}
next.run(req).await
}
async fn not_found_html(req: Request, next: Next) -> Response {
let resp = next.run(req).await;
if resp.status() == StatusCode::NOT_FOUND {
return server::error_response(StatusCode::NOT_FOUND);
}
resp
}