Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 25 additions & 5 deletions src/web/crate_details.rs
Original file line number Diff line number Diff line change
Expand Up @@ -719,8 +719,8 @@ mod tests {
use anyhow::Error;
use kuchikiki::traits::TendrilSink;
use pretty_assertions::assert_eq;
use reqwest::StatusCode;
use std::collections::HashMap;
use test_case::test_case;

async fn release_build_status(
conn: &mut sqlx::PgConnection,
Expand Down Expand Up @@ -2062,6 +2062,26 @@ mod tests {
});
}

#[tokio::test(flavor = "multi_thread")]
#[test_case("/crate/rayon/^1.11.0", "/crate/rayon/1.11.0")]
#[test_case("/crate/rayon/%5E1.11.0", "/crate/rayon/1.11.0")]
#[test_case("/crate/rayon", "/crate/rayon/latest"; "without trailing slash")]
#[test_case("/crate/rayon/", "/crate/rayon/latest")]
async fn test_version_redirects(path: &str, expected_target: &str) -> anyhow::Result<()> {
let env = TestEnvironment::new().await?;
env.fake_release()
.await
.name("rayon")
.version("1.11.0")
.create()
.await?;
let web = env.web_app().await;

web.assert_redirect(path, expected_target).await?;

Ok(())
}

#[test]
fn readme() {
async_wrapper(|env| async move {
Expand Down Expand Up @@ -2199,10 +2219,10 @@ path = "src/lib.rs"
.create()
.await?;

assert_eq!(
env.web_app().await.get("/crate/dummy%3E").await?.status(),
StatusCode::FOUND
);
env.web_app()
.await
.assert_redirect_unchecked("/crate/dummy%3E", "/crate/dummy%3E/latest")
.await?;

Ok(())
})
Expand Down
32 changes: 30 additions & 2 deletions src/web/escaped_uri.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use crate::web::encode_url_path;
use crate::web::{encode_url_path, url_decode};
use askama::filters::HtmlSafe;
use http::{Uri, uri::PathAndQuery};
use std::{borrow::Borrow, fmt::Display, iter, str::FromStr};
Expand All @@ -8,6 +8,8 @@ use url::form_urlencoded;
///
/// Ensures that the path part is always properly percent-encoded, including some characters
/// that http::Uri would allow, but we still want to encode, like umlauts.
/// Also ensures that some characters are _not_ encoded that sometimes arrive percent-encoded
/// from browsers, so we then can easily compare URIs, knowing they are encoded the same way.
///
/// Also we support fragments, with http::Uri doesn't support yet.
/// See https://github.com/hyperium/http/issues/775
Expand All @@ -20,7 +22,16 @@ pub struct EscapedURI {
impl EscapedURI {
pub fn from_uri(uri: Uri) -> Self {
if uri.path_and_query().is_some() {
let encoded_path = encode_url_path(uri.path());
let encoded_path = encode_url_path(
// we re-encode the path so we know all EscapedURI instances are comparable and
// encoded the same way.
// Example: "^" is not escaped when axum generates an Uri, we also didn't do it
// for a long time so we have nicers URLs with caret, since it's supported by
// most browsers to be shown in the URL bar.
// But: the actual request will have it encoded, which means the `Uri`
// we get from axum when handling the request will have it encoded.
&url_decode(uri.path()).expect("was in Uri, so has to have been correct"),
);
if uri.path() == encoded_path {
Self {
uri,
Expand Down Expand Up @@ -223,6 +234,22 @@ impl From<Uri> for EscapedURI {
}
}

impl TryFrom<String> for EscapedURI {
type Error = http::uri::InvalidUri;

fn try_from(value: String) -> Result<Self, Self::Error> {
value.parse()
}
}

impl TryFrom<&str> for EscapedURI {
type Error = http::uri::InvalidUri;

fn try_from(value: &str) -> Result<Self, Self::Error> {
value.parse()
}
}

impl PartialEq<String> for &EscapedURI {
fn eq(&self, other: &String) -> bool {
*self == other
Expand Down Expand Up @@ -286,6 +313,7 @@ mod tests {
}

#[test_case("/something" => "/something"; "plain path")]
#[test_case("/semver/%5E1.2.3" => "/semver/^1.2.3"; "we encode less")]
#[test_case("/somethingäöü" => "/something%C3%A4%C3%B6%C3%BC"; "path with umlauts")]
fn test_escaped_uri_encodes_path_from_uri(path: &str) -> String {
let uri: Uri = path.parse().unwrap();
Expand Down
22 changes: 10 additions & 12 deletions src/web/extractors/rustdoc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ use crate::{
db::BuildId,
web::{
MatchedRelease, MetaData, ReqVersion, error::AxumNope, escaped_uri::EscapedURI,
extractors::Path,
extractors::Path, url_decode,
},
};
use anyhow::Result;
Expand All @@ -15,7 +15,6 @@ use axum::{
};
use itertools::Itertools as _;
use serde::Deserialize;
use std::borrow::Cow;

const INDEX_HTML: &str = "index.html";
const FOLDER_AND_INDEX_HTML: &str = "/index.html";
Expand Down Expand Up @@ -48,7 +47,7 @@ pub(crate) struct RustdocParams {
// optional behaviour marker
page_kind: Option<PageKind>,

original_uri: Option<Uri>,
original_uri: Option<EscapedURI>,
name: String,
req_version: ReqVersion,
doc_target: Option<String>,
Expand Down Expand Up @@ -277,13 +276,16 @@ impl RustdocParams {
})
}

pub(crate) fn original_uri(&self) -> Option<&Uri> {
pub(crate) fn original_uri(&self) -> Option<&EscapedURI> {
self.original_uri.as_ref()
}
pub(crate) fn with_original_uri(self, original_uri: impl Into<Uri>) -> Self {
pub(crate) fn with_original_uri(self, original_uri: impl Into<EscapedURI>) -> Self {
self.with_maybe_original_uri(Some(original_uri))
}
pub(crate) fn with_maybe_original_uri(self, original_uri: Option<impl Into<Uri>>) -> Self {
pub(crate) fn with_maybe_original_uri(
self,
original_uri: Option<impl Into<EscapedURI>>,
) -> Self {
self.update(|mut params| {
params.original_uri = original_uri.map(Into::into);
params
Expand All @@ -292,7 +294,7 @@ impl RustdocParams {
#[cfg(test)]
pub(crate) fn try_with_original_uri<V>(self, original_uri: V) -> Result<Self>
where
V: TryInto<Uri>,
V: TryInto<EscapedURI>,
V::Error: std::error::Error + Send + Sync + 'static,
{
use anyhow::Context as _;
Expand Down Expand Up @@ -711,10 +713,6 @@ fn get_file_extension(path: &str) -> Option<&str> {
})
}

fn url_decode<'a>(input: &'a str) -> Result<Cow<'a, str>> {
Ok(percent_encoding::percent_decode(input.as_bytes()).decode_utf8()?)
}

fn generate_rustdoc_url(name: &str, version: &ReqVersion, path: &str) -> EscapedURI {
EscapedURI::from_path(format!("/{}/{}/{}", name, version, path))
}
Expand Down Expand Up @@ -1148,7 +1146,7 @@ mod tests {
.with_req_version(ReqVersion::Latest)
.with_maybe_doc_target(target)
.with_maybe_inner_path(path)
.try_with_original_uri(&dummy_path)
.try_with_original_uri(&dummy_path[..])
.unwrap()
.with_default_target(DEFAULT_TARGET)
.with_target_name(KRATE)
Expand Down
4 changes: 4 additions & 0 deletions src/web/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,10 @@ pub(crate) fn encode_url_path(path: &str) -> String {
utf8_percent_encode(path, PATH).to_string()
}

pub(crate) fn url_decode<'a>(input: &'a str) -> Result<Cow<'a, str>> {
Ok(percent_encoding::percent_decode(input.as_bytes()).decode_utf8()?)
}

const DEFAULT_BIND: SocketAddr = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)), 3000);

/// Represents a version identifier in a request in the original state.
Expand Down
6 changes: 5 additions & 1 deletion src/web/routes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -193,10 +193,14 @@ pub(super) fn build_axum_routes() -> AxumRouter {
"/releases/failures/{page}",
get_internal(super::releases::releases_failures_by_stars_handler),
)
.route_with_tsr(
.route(
"/crate/{name}",
get_internal(super::crate_details::crate_details_handler),
)
.route(
"/crate/{name}/",
get_internal(super::crate_details::crate_details_handler),
)
.route_with_tsr(
"/crate/{name}/{version}",
get_internal(super::crate_details::crate_details_handler),
Expand Down
2 changes: 1 addition & 1 deletion src/web/rustdoc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -220,7 +220,7 @@ pub(crate) async fn rustdoc_redirector_handler(
let params = params.with_page_kind(PageKind::Rustdoc);

fn redirect_to_doc(
original_uri: Option<&Uri>,
original_uri: Option<&EscapedURI>,
url: EscapedURI,
cache_policy: CachePolicy,
path_in_crate: Option<&str>,
Expand Down
Loading