Skip to content

Commit fe2ee91

Browse files
committedMay 3, 2019
always redirect version-catchall URLs to their latest version
1 parent a31865e commit fe2ee91

File tree

4 files changed

+100
-25
lines changed

4 files changed

+100
-25
lines changed
 

Diff for: ‎src/web/crate_details.rs

+21-7
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,11 @@
22

33

44
use super::pool::Pool;
5-
use super::{MetaData, duration_to_str, match_version, render_markdown};
5+
use super::{MetaData, duration_to_str, match_version, render_markdown, MatchVersion};
66
use super::error::Nope;
77
use super::page::Page;
88
use iron::prelude::*;
9-
use iron::status;
9+
use iron::{Url, status};
1010
use std::collections::BTreeMap;
1111
use time;
1212
use rustc_serialize::json::{Json, ToJson};
@@ -229,14 +229,28 @@ pub fn crate_details_handler(req: &mut Request) -> IronResult<Response> {
229229

230230
let conn = extension!(req, Pool);
231231

232-
match_version(&conn, &name, req_version)
233-
.and_then(|version| CrateDetails::new(&conn, &name, &version))
234-
.ok_or(IronError::new(Nope::CrateNotFound, status::NotFound))
235-
.and_then(|details| {
232+
match match_version(&conn, &name, req_version) {
233+
MatchVersion::Exact(version) => {
234+
let details = CrateDetails::new(&conn, &name, &version);
235+
236236
Page::new(details)
237237
.set_true("show_package_navigation")
238238
.set_true("javascript_highlightjs")
239239
.set_true("package_navigation_crate_tab")
240240
.to_resp("crate_details")
241-
})
241+
}
242+
MatchVersion::Semver(version) => {
243+
let url = ctry!(Url::parse(&format!("{}://{}:{}/crate/{}/{}",
244+
req.url.scheme(),
245+
req.url.host(),
246+
req.url.port(),
247+
name,
248+
version)[..]));
249+
250+
Ok(super::redirect(url))
251+
}
252+
MatchVersion::None => {
253+
Err(IronError::new(Nope::CrateNotFound, status::NotFound))
254+
}
255+
}
242256
}

Diff for: ‎src/web/mod.rs

+43-10
Original file line numberDiff line numberDiff line change
@@ -50,8 +50,9 @@ use std::error::Error;
5050
use std::time::Duration;
5151
use std::path::PathBuf;
5252
use iron::prelude::*;
53-
use iron::{self, Handler, status};
54-
use iron::headers::{CacheControl, CacheDirective, ContentType};
53+
use iron::{self, Handler, Url, status};
54+
use iron::headers::{Expires, HttpDate, CacheControl, CacheDirective, ContentType};
55+
use iron::modifiers::Redirect;
5556
use router::{Router, NoRoute};
5657
use staticfile::Static;
5758
use handlebars_iron::{HandlebarsEngine, DirectorySource};
@@ -258,9 +259,34 @@ impl Handler for CratesfyiHandler {
258259
}
259260
}
260261

262+
/// Represents the possible results of attempting to load a version requirement.
263+
enum MatchVersion {
264+
/// `match_version` was given an exact version, which matched a saved crate version.
265+
Exact(String),
266+
/// `match_version` was given a semver version requirement, which matched the given saved crate
267+
/// version.
268+
Semver(String),
269+
/// `match_version` was given a version requirement which did not match any saved crate
270+
/// versions.
271+
None,
272+
}
261273

274+
impl MatchVersion {
275+
/// Convert this `MatchVersion` into an `Option`, discarding whether the matched version came
276+
/// from an exact version number or a semver requirement.
277+
pub fn into_option(self) -> Option<String> {
278+
match self {
279+
MatchVersion::Exact(v) | MatchVersion::Semver(v) => Some(v),
280+
MatchVersion::None => None,
281+
}
282+
}
283+
}
262284

263-
fn match_version(conn: &Connection, name: &str, version: Option<&str>) -> Option<String> {
285+
/// Checks the database for crate releases that match the given name and version.
286+
///
287+
/// `version` may be an exact version number or loose semver version requirement. The return value
288+
/// will indicate whether the given version exactly matched a version number from the database.
289+
fn match_version(conn: &Connection, name: &str, version: Option<&str>) -> MatchVersion {
264290

265291
// version is an Option<&str> from router::Router::get
266292
// need to decode first
@@ -278,7 +304,7 @@ fn match_version(conn: &Connection, name: &str, version: Option<&str>) -> Option
278304
let mut versions = Vec::new();
279305
let rows = conn.query("SELECT versions FROM crates WHERE name = $1", &[&name]).unwrap();
280306
if rows.len() == 0 {
281-
return None;
307+
return MatchVersion::None;
282308
}
283309
let versions_json: Json = rows.get(0).get(0);
284310
for version in versions_json.as_array().unwrap() {
@@ -293,14 +319,14 @@ fn match_version(conn: &Connection, name: &str, version: Option<&str>) -> Option
293319
// we can't expect users to use semver in query
294320
for version in &versions {
295321
if version == &req_version {
296-
return Some(version.clone());
322+
return MatchVersion::Exact(version.clone());
297323
}
298324
}
299325

300326
// Now try to match with semver
301327
let req_sem_ver = match VersionReq::parse(&req_version) {
302328
Ok(v) => v,
303-
Err(_) => return None,
329+
Err(_) => return MatchVersion::None,
304330
};
305331

306332
// we need to sort versions first
@@ -312,7 +338,7 @@ fn match_version(conn: &Connection, name: &str, version: Option<&str>) -> Option
312338
// but check result just in case
313339
let version = match Version::parse(&version) {
314340
Ok(v) => v,
315-
Err(_) => return None,
341+
Err(_) => return MatchVersion::None,
316342
};
317343
versions_sem.push(version);
318344
}
@@ -325,16 +351,16 @@ fn match_version(conn: &Connection, name: &str, version: Option<&str>) -> Option
325351
// semver is acting weird for '*' (any) range if a crate only have pre-release versions
326352
// return first version if requested version is '*'
327353
if req_version == "*" && !versions_sem.is_empty() {
328-
return Some(format!("{}", versions_sem[0]));
354+
return MatchVersion::Semver(format!("{}", versions_sem[0]));
329355
}
330356

331357
for version in &versions_sem {
332358
if req_sem_ver.matches(&version) {
333-
return Some(format!("{}", version));
359+
return MatchVersion::Semver(format!("{}", version));
334360
}
335361
}
336362

337-
None
363+
MatchVersion::None
338364
}
339365

340366

@@ -431,7 +457,14 @@ fn duration_to_str(ts: time::Timespec) -> String {
431457

432458
}
433459

460+
/// Creates a `Response` which redirects to the given path on the scheme/host/port from the given
461+
/// `Request`.
462+
fn redirect(url: Url) -> Response {
463+
let mut resp = Response::with((status::Found, Redirect(url)));
464+
resp.headers.set(Expires(HttpDate(time::now())));
434465

466+
resp
467+
}
435468

436469
fn style_css_handler(_: &mut Request) -> IronResult<Response> {
437470
let mut response = Response::with((status::Ok, STYLE_CSS));

Diff for: ‎src/web/releases.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -484,7 +484,7 @@ pub fn search_handler(req: &mut Request) -> IronResult<Response> {
484484
}
485485

486486

487-
if let Some(version) = match_version(&conn, &query, None) {
487+
if let Some(version) = match_version(&conn, &query, None).into_option() {
488488
// FIXME: This is a super dirty way to check if crate have rustdocs generated.
489489
// match_version should handle this instead of this code block.
490490
// This block is introduced to fix #163

Diff for: ‎src/web/rustdoc.rs

+35-7
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ use iron::prelude::*;
99
use iron::{status, Url};
1010
use iron::modifiers::Redirect;
1111
use router::Router;
12-
use super::match_version;
12+
use super::{match_version, MatchVersion};
1313
use super::error::Nope;
1414
use super::page::Page;
1515
use rustc_serialize::json::{Json, ToJson};
@@ -120,7 +120,7 @@ pub fn rustdoc_redirector_handler(req: &mut Request) -> IronResult<Response> {
120120

121121
let conn = extension!(req, Pool);
122122

123-
let version = match match_version(&conn, &crate_name, req_version) {
123+
let version = match match_version(&conn, &crate_name, req_version).into_option() {
124124
Some(v) => v,
125125
None => return Err(IronError::new(Nope::CrateNotFound, status::NotFound)),
126126
};
@@ -153,17 +153,35 @@ pub fn rustdoc_html_server_handler(req: &mut Request) -> IronResult<Response> {
153153

154154
let router = extension!(req, Router);
155155
let name = router.find("crate").unwrap_or("").to_string();
156-
let version = router.find("version");
156+
let url_version = router.find("version");
157+
let version; // pre-declaring it to enforce drop order relative to `req_path`
157158
let conn = extension!(req, Pool);
158-
let version = try!(match_version(&conn, &name, version)
159-
.ok_or(IronError::new(Nope::ResourceNotFound, status::NotFound)));
159+
160160
let mut req_path = req.url.path();
161161

162162
// remove name and version from path
163163
for _ in 0..2 {
164164
req_path.remove(0);
165165
}
166166

167+
version = match match_version(&conn, &name, url_version) {
168+
MatchVersion::Exact(v) => v,
169+
MatchVersion::Semver(v) => {
170+
// to prevent cloudfront caching the wrong artifacts on URLs with loose semver
171+
// versions, redirect the browser to the returned version instead of loading it
172+
// immediately
173+
let url = ctry!(Url::parse(&format!("{}://{}:{}/{}/{}/{}",
174+
req.url.scheme(),
175+
req.url.host(),
176+
req.url.port(),
177+
name,
178+
v,
179+
req_path.join("/"))[..]));
180+
return Ok(super::redirect(url));
181+
}
182+
MatchVersion::None => return Err(IronError::new(Nope::ResourceNotFound, status::NotFound)),
183+
};
184+
167185
// docs have "rustdoc" prefix in database
168186
req_path.insert(0, "rustdoc");
169187

@@ -240,7 +258,7 @@ pub fn badge_handler(req: &mut Request) -> IronResult<Response> {
240258
let conn = extension!(req, Pool);
241259

242260
let options = match match_version(&conn, &name, Some(&version)) {
243-
Some(version) => {
261+
MatchVersion::Exact(version) => {
244262
let rows = ctry!(conn.query("SELECT rustdoc_status
245263
FROM releases
246264
INNER JOIN crates ON crates.id = releases.crate_id
@@ -260,7 +278,17 @@ pub fn badge_handler(req: &mut Request) -> IronResult<Response> {
260278
}
261279
}
262280
}
263-
None => {
281+
MatchVersion::Semver(version) => {
282+
let url = ctry!(Url::parse(&format!("{}://{}:{}/{}/badge.svg?version={}",
283+
req.url.scheme(),
284+
req.url.host(),
285+
req.url.port(),
286+
name,
287+
version)[..]));
288+
289+
return Ok(super::redirect(url));
290+
}
291+
MatchVersion::None => {
264292
BadgeOptions {
265293
subject: "docs".to_owned(),
266294
status: "no builds".to_owned(),

0 commit comments

Comments
 (0)
Please sign in to comment.