Skip to content

Commit c371639

Browse files
committed
migrate statics-handler to axum
1 parent 1dce02e commit c371639

File tree

5 files changed

+90
-100
lines changed

5 files changed

+90
-100
lines changed

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,7 @@ tower = "0.4.11"
9797
tower-service = "0.3.2"
9898
tower-http = { version = "0.3.4", features = ["trace"] }
9999
mime = "0.3.16"
100+
httpdate = "1.0.2"
100101

101102
# NOTE: if you change this, also double-check that the comment in `queue_builder::remove_tempdirs` is still accurate.
102103
tempfile = "3.1.0"

src/web/routes.rs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,10 @@ pub(super) fn build_axum_routes() -> AxumRouter {
4949
"/favicon.ico",
5050
get_static(|| async { Redirect::permanent("/-/static/favicon.ico") }),
5151
)
52+
.route(
53+
"/-/static/*path",
54+
get_static(super::statics::static_handler),
55+
)
5256
.route(
5357
"/sitemap.xml",
5458
get_internal(super::sitemap::sitemapindex_handler),
@@ -123,8 +127,6 @@ pub(super) fn build_routes() -> Routes {
123127
PermanentRedirect("/-/static/opensearch.xml"),
124128
);
125129

126-
routes.static_resource("/-/static/:single", super::statics::static_handler);
127-
routes.static_resource("/-/static/*", super::statics::static_handler);
128130
routes.internal_page(
129131
"/-/rustdoc.static/:single",
130132
super::rustdoc::static_asset_handler,

src/web/rustdoc.rs

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@ use crate::{
77
utils,
88
web::{
99
cache::CachePolicy, crate_details::CrateDetails, csp::Csp, error::Nope, file::File,
10-
match_version, metrics::RenderingTimesRecorder, parse_url_with_params, redirect_base,
11-
report_error, MatchSemver, MetaData,
10+
match_version, metrics::RenderingTimesRecorder, parse_url_with_params, redirect,
11+
redirect_base, report_error, MatchSemver, MetaData,
1212
},
1313
Config, Metrics, Storage, RUSTDOC_STATIC_STORAGE_PREFIX,
1414
};
@@ -44,6 +44,23 @@ static DOC_RUST_LANG_ORG_REDIRECTS: Lazy<HashMap<&str, &str>> = Lazy::new(|| {
4444
])
4545
});
4646

47+
fn ico_handler(req: &mut Request) -> IronResult<Response> {
48+
if let Some(&"favicon.ico") = req.url.path().last() {
49+
// if we're looking for exactly "favicon.ico", we need to defer to the handler that
50+
// actually serves it, so return a 404 here to make the main handler carry on
51+
Err(Nope::ResourceNotFound.into())
52+
} else {
53+
// if we're looking for something like "favicon-20190317-1.35.0-nightly-c82834e2b.ico",
54+
// redirect to the plain one so that the above branch can trigger with the correct filename
55+
let url = ctry!(
56+
req,
57+
Url::parse(&format!("{}/favicon.ico", redirect_base(req))),
58+
);
59+
60+
Ok(redirect(url))
61+
}
62+
}
63+
4764
/// Handler called for `/:crate` and `/:crate/:version` URLs. Automatically redirects to the docs
4865
/// or crate details page based on whether the given crate version was successfully built.
4966
pub fn rustdoc_redirector_handler(req: &mut Request) -> IronResult<Response> {
@@ -119,7 +136,7 @@ pub fn rustdoc_redirector_handler(req: &mut Request) -> IronResult<Response> {
119136
// route .ico files into their dedicated handler so that docs.rs's favicon is always
120137
// displayed
121138
rendering_time.step("serve ICO");
122-
return super::statics::ico_handler(req);
139+
return ico_handler(req);
123140
}
124141

125142
let router = extension!(req, Router);

src/web/statics.rs

Lines changed: 64 additions & 95 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,23 @@
1-
use super::{cache::CachePolicy, error::Nope, redirect, redirect_base};
1+
use super::{
2+
cache::CachePolicy,
3+
error::{AxumNope, AxumResult},
4+
};
25
use crate::utils::report_error;
36
use anyhow::Context;
4-
use chrono::prelude::*;
5-
use iron::{
6-
headers::{ContentLength, ContentType, LastModified},
7-
status::Status,
8-
IronResult, Request, Response, Url,
7+
use axum::{
8+
extract::{Extension, Path},
9+
http::{
10+
header::{CONTENT_LENGTH, CONTENT_TYPE, LAST_MODIFIED},
11+
StatusCode,
12+
},
13+
response::{IntoResponse, Response},
914
};
15+
use chrono::prelude::*;
16+
use httpdate::fmt_http_date;
17+
use mime::Mime;
1018
use mime_guess::MimeGuess;
11-
use std::{ffi::OsStr, fs, path::Path};
19+
use std::{ffi::OsStr, path, time::SystemTime};
20+
use tokio::fs;
1221

1322
const VENDORED_CSS: &str = include_str!(concat!(env!("OUT_DIR"), "/vendored.css"));
1423
const STYLE_CSS: &str = include_str!(concat!(env!("OUT_DIR"), "/style.css"));
@@ -17,29 +26,27 @@ const RUSTDOC_2021_12_05_CSS: &str =
1726
include_str!(concat!(env!("OUT_DIR"), "/rustdoc-2021-12-05.css"));
1827
const STATIC_SEARCH_PATHS: &[&str] = &["static", "vendor"];
1928

20-
pub(crate) fn static_handler(req: &mut Request) -> IronResult<Response> {
21-
let mut file = req.url.path();
22-
file.drain(..2).for_each(std::mem::drop);
23-
let file = file.join("/");
24-
25-
Ok(match file.as_str() {
26-
"vendored.css" => serve_resource(VENDORED_CSS, ContentType("text/css".parse().unwrap())),
27-
"style.css" => serve_resource(STYLE_CSS, ContentType("text/css".parse().unwrap())),
28-
"rustdoc.css" => serve_resource(RUSTDOC_CSS, ContentType("text/css".parse().unwrap())),
29-
"rustdoc-2021-12-05.css" => serve_resource(
30-
RUSTDOC_2021_12_05_CSS,
31-
ContentType("text/css".parse().unwrap()),
32-
),
33-
file => serve_file(file)?,
29+
pub(crate) async fn static_handler(Path(path): Path<String>) -> AxumResult<impl IntoResponse> {
30+
let text_css: Mime = "text/css".parse().unwrap();
31+
32+
Ok(match path.as_str() {
33+
"/vendored.css" => build_response(VENDORED_CSS, text_css),
34+
"/style.css" => build_response(STYLE_CSS, text_css),
35+
"/rustdoc.css" => build_response(RUSTDOC_CSS, text_css),
36+
"/rustdoc-2021-12-05.css" => build_response(RUSTDOC_2021_12_05_CSS, text_css),
37+
file => match serve_file(&file[1..]).await {
38+
Ok(response) => response.into_response(),
39+
Err(err) => return Err(err),
40+
},
3441
})
3542
}
3643

37-
fn serve_file(file: &str) -> IronResult<Response> {
44+
async fn serve_file(file: &str) -> AxumResult<impl IntoResponse> {
3845
// Find the first path that actually exists
3946
let path = STATIC_SEARCH_PATHS
4047
.iter()
4148
.find_map(|root| {
42-
let path = Path::new(root).join(file);
49+
let path = path::Path::new(root).join(file);
4350
if !path.exists() {
4451
return None;
4552
}
@@ -55,96 +62,59 @@ fn serve_file(file: &str) -> IronResult<Response> {
5562
None
5663
}
5764
})
58-
.ok_or(Nope::ResourceNotFound)?;
65+
.ok_or(AxumNope::ResourceNotFound)?;
5966

6067
let contents = fs::read(&path)
68+
.await
6169
.with_context(|| format!("failed to read static file {}", path.display()))
6270
.map_err(|e| {
6371
report_error(&e);
64-
Nope::InternalServerError
72+
AxumNope::InternalServerError
6573
})?;
6674

6775
// If we can detect the file's mime type, set it
6876
// MimeGuess misses a lot of the file types we need, so there's a small wrapper
6977
// around it
70-
let mut content_type = path
71-
.extension()
72-
.and_then(OsStr::to_str)
73-
.and_then(|ext| match ext {
74-
"eot" => Some(ContentType(
75-
"application/vnd.ms-fontobject".parse().unwrap(),
76-
)),
77-
"woff2" => Some(ContentType("application/font-woff2".parse().unwrap())),
78-
"ttf" => Some(ContentType("application/x-font-ttf".parse().unwrap())),
79-
80-
_ => MimeGuess::from_path(&path)
81-
.first()
82-
.map(|mime| ContentType(mime.as_ref().parse().unwrap())),
83-
});
84-
85-
if file == "opensearch.xml" {
86-
content_type = Some(ContentType(
87-
"application/opensearchdescription+xml".parse().unwrap(),
88-
));
89-
}
78+
let content_type: Mime = if file == "opensearch.xml" {
79+
"application/opensearchdescription+xml".parse().unwrap()
80+
} else {
81+
path.extension()
82+
.and_then(OsStr::to_str)
83+
.and_then(|ext| match ext {
84+
"eot" => Some("application/vnd.ms-fontobject".parse().unwrap()),
85+
"woff2" => Some("application/font-woff2".parse().unwrap()),
86+
"ttf" => Some("application/x-font-ttf".parse().unwrap()),
87+
_ => MimeGuess::from_path(&path).first(),
88+
})
89+
.unwrap_or(mime::APPLICATION_OCTET_STREAM)
90+
};
9091

91-
Ok(serve_resource(contents, content_type))
92+
Ok(build_response(contents, content_type))
9293
}
9394

94-
fn serve_resource<R, C>(resource: R, content_type: C) -> Response
95+
fn build_response<R>(resource: R, content_type: Mime) -> Response
9596
where
9697
R: AsRef<[u8]>,
97-
C: Into<Option<ContentType>>,
9898
{
99-
let mut response = Response::with((Status::Ok, resource.as_ref()));
100-
101-
response
102-
.extensions
103-
.insert::<CachePolicy>(CachePolicy::ForeverInCdnAndBrowser);
104-
105-
response
106-
.headers
107-
.set(ContentLength(resource.as_ref().len() as u64));
108-
response.headers.set(LastModified(
109-
Utc::now()
110-
.format("%a, %d %b %Y %T %Z")
111-
.to_string()
112-
.parse()
113-
.unwrap(),
114-
));
115-
116-
if let Some(content_type) = content_type.into() {
117-
response.headers.set(content_type);
118-
}
119-
120-
response
121-
}
122-
123-
pub(super) fn ico_handler(req: &mut Request) -> IronResult<Response> {
124-
if let Some(&"favicon.ico") = req.url.path().last() {
125-
// if we're looking for exactly "favicon.ico", we need to defer to the handler that
126-
// actually serves it, so return a 404 here to make the main handler carry on
127-
Err(Nope::ResourceNotFound.into())
128-
} else {
129-
// if we're looking for something like "favicon-20190317-1.35.0-nightly-c82834e2b.ico",
130-
// redirect to the plain one so that the above branch can trigger with the correct filename
131-
let url = ctry!(
132-
req,
133-
Url::parse(&format!("{}/favicon.ico", redirect_base(req))),
134-
);
135-
136-
Ok(redirect(url))
137-
}
99+
(
100+
StatusCode::OK,
101+
Extension(CachePolicy::ForeverInCdnAndBrowser),
102+
[
103+
(CONTENT_LENGTH, resource.as_ref().len().to_string()),
104+
(CONTENT_TYPE, content_type.to_string()),
105+
(LAST_MODIFIED, fmt_http_date(SystemTime::from(Utc::now()))),
106+
],
107+
resource.as_ref().to_vec(),
108+
)
109+
.into_response()
138110
}
139111

140112
#[cfg(test)]
141113
mod tests {
142-
use iron::status::Status;
143-
144114
use super::{serve_file, STATIC_SEARCH_PATHS, STYLE_CSS, VENDORED_CSS};
145115
use crate::{
146116
test::{assert_cache_control, wrapper},
147-
web::cache::CachePolicy,
117+
web::{cache::CachePolicy, error::AxumNope},
148118
};
149119
use reqwest::StatusCode;
150120
use std::fs;
@@ -276,8 +246,8 @@ mod tests {
276246
});
277247
}
278248

279-
#[test]
280-
fn directory_traversal() {
249+
#[tokio::test]
250+
async fn directory_traversal() {
281251
const PATHS: &[&str] = &[
282252
"../LICENSE",
283253
"%2e%2e%2fLICENSE",
@@ -293,9 +263,8 @@ mod tests {
293263
// Still, the test ensures the underlying function called by the request handler to
294264
// serve the file also includes protection for path traversal, in the event we switch
295265
// to a framework that doesn't include builtin protection in the future.
296-
assert_eq!(
297-
Some(Status::NotFound),
298-
serve_file(path).unwrap_err().response.status,
266+
assert!(
267+
matches!(serve_file(path).await, Err(AxumNope::ResourceNotFound)),
299268
"{} did not return a 404",
300269
path
301270
);

0 commit comments

Comments
 (0)