-
Notifications
You must be signed in to change notification settings - Fork 51
Expand file tree
/
Copy pathmain.rs
More file actions
173 lines (153 loc) · 5.8 KB
/
main.rs
File metadata and controls
173 lines (153 loc) · 5.8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
//! A simple HTTP server, for learning and local development.
//!
//! This server demonstrates how to build an async HTTP file server with axum,
//! tower-http, and tokio. It serves static files from a root directory, with
//! optional developer extensions enabled by the `-x` flag.
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;
/// A basic HTTP file server.
#[derive(Clone, Parser)]
#[command(version, about = "A basic HTTP file server")]
pub struct Config {
/// The IP:PORT combination.
#[arg(short = 'a', long = "addr", conflicts_with_all = ["port", "public"])]
addr: Option<SocketAddr>,
/// Port number.
#[arg(short = 'p', long = "port")]
port: Option<u16>,
/// Listen on all interfaces (0.0.0.0) instead of localhost.
#[arg(long = "public")]
public: bool,
/// The root directory for serving files.
#[arg(default_value = ".")]
root_dir: PathBuf,
/// Enable developer extensions.
#[arg(short = 'x')]
use_extensions: bool,
}
impl Config {
/// Resolve the listen address from the combination of --addr, --port, and --public.
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() {
// Initialize tracing. Default to "info" for this crate unless RUST_LOG is set.
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);
// Also print to stderr without buffering, for integration test harness.
eprintln!("listening on {}", local_addr);
axum::serve(listener, app)
.await
.expect("server error");
}
/// Build the axum router.
///
/// The core server uses `tower_http::ServeDir` for static file serving. When
/// extensions are enabled, tower middleware layers are added for each extension
/// feature. The extensions are in the `ext` module.
fn build_router(config: &Config) -> Router {
// ServeDir handles: static files with streaming, MIME detection,
// Content-Length, index.html fallback, and trailing-slash redirects.
let serve_dir = ServeDir::new(&config.root_dir)
.append_index_html_on_directories(true);
// When extensions are enabled, wrap ServeDir with extension middleware.
// When disabled, the router is a clean, minimal static file server.
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))
}
}
/// Middleware that sets `Cache-Control: no-cache` on all responses.
///
/// This ensures clients always revalidate with the server before using a
/// cached copy. They still benefit from conditional requests (304 Not
/// Modified via `Last-Modified` / `ETag`), but won't silently serve stale
/// files — important for a local development server.
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
}
/// Middleware that enforces GET-only requests.
///
/// Returns 405 Method Not Allowed with an `Allow: GET` header for any
/// non-GET request.
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
}
/// Middleware that replaces non-HTML 404 responses with HTML error pages.
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
}