summaryrefslogtreecommitdiff
path: root/cli/file_fetcher.rs
diff options
context:
space:
mode:
authorDavid Sherret <dsherret@users.noreply.github.com>2022-12-14 08:47:18 -0500
committerGitHub <noreply@github.com>2022-12-14 08:47:18 -0500
commit4a64ca850131a0aa07e8c781e6e194246f94eeb6 (patch)
treee8c6ac26e86813b33743475c366caaa58fe5bfdb /cli/file_fetcher.rs
parentf9db129bdf1e09f6d5faaa73ad0cad27d2418798 (diff)
chore: fix recent regression with `deno upgrade` not handling redirects (#17045)
Diffstat (limited to 'cli/file_fetcher.rs')
-rw-r--r--cli/file_fetcher.rs574
1 files changed, 570 insertions, 4 deletions
diff --git a/cli/file_fetcher.rs b/cli/file_fetcher.rs
index 12f39c7e3..539f9ccd3 100644
--- a/cli/file_fetcher.rs
+++ b/cli/file_fetcher.rs
@@ -4,6 +4,7 @@ use crate::args::CacheSetting;
use crate::auth_tokens::AuthTokens;
use crate::cache::HttpCache;
use crate::colors;
+use crate::http_util::resolve_redirect_from_response;
use crate::http_util::CacheSemantics;
use crate::http_util::FetchOnceArgs;
use crate::http_util::FetchOnceResult;
@@ -21,6 +22,11 @@ use deno_core::futures;
use deno_core::futures::future::FutureExt;
use deno_core::parking_lot::Mutex;
use deno_core::ModuleSpecifier;
+use deno_runtime::deno_fetch::reqwest::header::HeaderValue;
+use deno_runtime::deno_fetch::reqwest::header::ACCEPT;
+use deno_runtime::deno_fetch::reqwest::header::AUTHORIZATION;
+use deno_runtime::deno_fetch::reqwest::header::IF_NONE_MATCH;
+use deno_runtime::deno_fetch::reqwest::StatusCode;
use deno_runtime::deno_web::BlobStore;
use deno_runtime::permissions::Permissions;
use log::debug;
@@ -457,14 +463,16 @@ impl FileFetcher {
let file_fetcher = self.clone();
// A single pass of fetch either yields code or yields a redirect.
async move {
- match client
- .fetch_once(FetchOnceArgs {
+ match fetch_once(
+ &client,
+ FetchOnceArgs {
url: specifier.clone(),
maybe_accept: maybe_accept.clone(),
maybe_etag,
maybe_auth_token,
- })
- .await?
+ },
+ )
+ .await?
{
FetchOnceResult::NotModified => {
let file = file_fetcher.fetch_cached(&specifier, 10)?.unwrap();
@@ -627,17 +635,99 @@ impl FileFetcher {
}
}
+/// Asynchronously fetches the given HTTP URL one pass only.
+/// If no redirect is present and no error occurs,
+/// yields Code(ResultPayload).
+/// If redirect occurs, does not follow and
+/// yields Redirect(url).
+async fn fetch_once(
+ http_client: &HttpClient,
+ args: FetchOnceArgs,
+) -> Result<FetchOnceResult, AnyError> {
+ let mut request = http_client.get_no_redirect(args.url.clone());
+
+ if let Some(etag) = args.maybe_etag {
+ let if_none_match_val = HeaderValue::from_str(&etag)?;
+ request = request.header(IF_NONE_MATCH, if_none_match_val);
+ }
+ if let Some(auth_token) = args.maybe_auth_token {
+ let authorization_val = HeaderValue::from_str(&auth_token.to_string())?;
+ request = request.header(AUTHORIZATION, authorization_val);
+ }
+ if let Some(accept) = args.maybe_accept {
+ let accepts_val = HeaderValue::from_str(&accept)?;
+ request = request.header(ACCEPT, accepts_val);
+ }
+ let response = request.send().await?;
+
+ if response.status() == StatusCode::NOT_MODIFIED {
+ return Ok(FetchOnceResult::NotModified);
+ }
+
+ let mut result_headers = HashMap::new();
+ let response_headers = response.headers();
+
+ if let Some(warning) = response_headers.get("X-Deno-Warning") {
+ log::warn!(
+ "{} {}",
+ crate::colors::yellow("Warning"),
+ warning.to_str().unwrap()
+ );
+ }
+
+ for key in response_headers.keys() {
+ let key_str = key.to_string();
+ let values = response_headers.get_all(key);
+ let values_str = values
+ .iter()
+ .map(|e| e.to_str().unwrap().to_string())
+ .collect::<Vec<String>>()
+ .join(",");
+ result_headers.insert(key_str, values_str);
+ }
+
+ if response.status().is_redirection() {
+ let new_url = resolve_redirect_from_response(&args.url, &response)?;
+ return Ok(FetchOnceResult::Redirect(new_url, result_headers));
+ }
+
+ if response.status().is_client_error() || response.status().is_server_error()
+ {
+ let err = if response.status() == StatusCode::NOT_FOUND {
+ custom_error(
+ "NotFound",
+ format!("Import '{}' failed, not found.", args.url),
+ )
+ } else {
+ generic_error(format!(
+ "Import '{}' failed: {}",
+ args.url,
+ response.status()
+ ))
+ };
+ return Err(err);
+ }
+
+ let body = response.bytes().await?.to_vec();
+
+ Ok(FetchOnceResult::Code(body, result_headers))
+}
+
#[cfg(test)]
mod tests {
use crate::cache::CachedUrlMetadata;
use crate::http_util::HttpClient;
+ use crate::version;
use super::*;
use deno_core::error::get_custom_error_class;
use deno_core::resolve_url;
use deno_core::resolve_url_or_path;
+ use deno_core::url::Url;
+ use deno_runtime::deno_fetch::create_http_client;
use deno_runtime::deno_web::Blob;
use deno_runtime::deno_web::InMemoryBlobPart;
+ use std::fs::read;
use test_util::TempDir;
fn setup(
@@ -1640,4 +1730,480 @@ mod tests {
\u{5E2}\u{5D5}\u{5DC}\u{5DD}\");\u{A}";
test_fetch_remote_encoded("windows-1255", "windows-1255", expected).await;
}
+
+ fn create_test_client() -> HttpClient {
+ HttpClient::from_client(
+ create_http_client(
+ "test_client".to_string(),
+ None,
+ vec![],
+ None,
+ None,
+ None,
+ )
+ .unwrap(),
+ )
+ }
+
+ #[tokio::test]
+ async fn test_fetch_string() {
+ let _http_server_guard = test_util::http_server();
+ // Relies on external http server. See target/debug/test_server
+ let url = Url::parse("http://127.0.0.1:4545/assets/fixture.json").unwrap();
+ let client = create_test_client();
+ let result = fetch_once(
+ &client,
+ FetchOnceArgs {
+ url,
+ maybe_accept: None,
+ maybe_etag: None,
+ maybe_auth_token: None,
+ },
+ )
+ .await;
+ if let Ok(FetchOnceResult::Code(body, headers)) = result {
+ assert!(!body.is_empty());
+ assert_eq!(headers.get("content-type").unwrap(), "application/json");
+ assert_eq!(headers.get("etag"), None);
+ assert_eq!(headers.get("x-typescript-types"), None);
+ } else {
+ panic!();
+ }
+ }
+
+ #[tokio::test]
+ async fn test_fetch_gzip() {
+ let _http_server_guard = test_util::http_server();
+ // Relies on external http server. See target/debug/test_server
+ let url = Url::parse("http://127.0.0.1:4545/run/import_compression/gziped")
+ .unwrap();
+ let client = create_test_client();
+ let result = fetch_once(
+ &client,
+ FetchOnceArgs {
+ url,
+ maybe_accept: None,
+ maybe_etag: None,
+ maybe_auth_token: None,
+ },
+ )
+ .await;
+ if let Ok(FetchOnceResult::Code(body, headers)) = result {
+ assert_eq!(String::from_utf8(body).unwrap(), "console.log('gzip')");
+ assert_eq!(
+ headers.get("content-type").unwrap(),
+ "application/javascript"
+ );
+ assert_eq!(headers.get("etag"), None);
+ assert_eq!(headers.get("x-typescript-types"), None);
+ } else {
+ panic!();
+ }
+ }
+
+ #[tokio::test]
+ async fn test_fetch_with_etag() {
+ let _http_server_guard = test_util::http_server();
+ let url = Url::parse("http://127.0.0.1:4545/etag_script.ts").unwrap();
+ let client = create_test_client();
+ let result = fetch_once(
+ &client,
+ FetchOnceArgs {
+ url: url.clone(),
+ maybe_accept: None,
+ maybe_etag: None,
+ maybe_auth_token: None,
+ },
+ )
+ .await;
+ if let Ok(FetchOnceResult::Code(body, headers)) = result {
+ assert!(!body.is_empty());
+ assert_eq!(String::from_utf8(body).unwrap(), "console.log('etag')");
+ assert_eq!(
+ headers.get("content-type").unwrap(),
+ "application/typescript"
+ );
+ assert_eq!(headers.get("etag").unwrap(), "33a64df551425fcc55e");
+ } else {
+ panic!();
+ }
+
+ let res = fetch_once(
+ &client,
+ FetchOnceArgs {
+ url,
+ maybe_accept: None,
+ maybe_etag: Some("33a64df551425fcc55e".to_string()),
+ maybe_auth_token: None,
+ },
+ )
+ .await;
+ assert_eq!(res.unwrap(), FetchOnceResult::NotModified);
+ }
+
+ #[tokio::test]
+ async fn test_fetch_brotli() {
+ let _http_server_guard = test_util::http_server();
+ // Relies on external http server. See target/debug/test_server
+ let url = Url::parse("http://127.0.0.1:4545/run/import_compression/brotli")
+ .unwrap();
+ let client = create_test_client();
+ let result = fetch_once(
+ &client,
+ FetchOnceArgs {
+ url,
+ maybe_accept: None,
+ maybe_etag: None,
+ maybe_auth_token: None,
+ },
+ )
+ .await;
+ if let Ok(FetchOnceResult::Code(body, headers)) = result {
+ assert!(!body.is_empty());
+ assert_eq!(String::from_utf8(body).unwrap(), "console.log('brotli');");
+ assert_eq!(
+ headers.get("content-type").unwrap(),
+ "application/javascript"
+ );
+ assert_eq!(headers.get("etag"), None);
+ assert_eq!(headers.get("x-typescript-types"), None);
+ } else {
+ panic!();
+ }
+ }
+
+ #[tokio::test]
+ async fn test_fetch_accept() {
+ let _http_server_guard = test_util::http_server();
+ // Relies on external http server. See target/debug/test_server
+ let url = Url::parse("http://127.0.0.1:4545/echo_accept").unwrap();
+ let client = create_test_client();
+ let result = fetch_once(
+ &client,
+ FetchOnceArgs {
+ url,
+ maybe_accept: Some("application/json".to_string()),
+ maybe_etag: None,
+ maybe_auth_token: None,
+ },
+ )
+ .await;
+ if let Ok(FetchOnceResult::Code(body, _)) = result {
+ assert_eq!(body, r#"{"accept":"application/json"}"#.as_bytes());
+ } else {
+ panic!();
+ }
+ }
+
+ #[tokio::test]
+ async fn test_fetch_once_with_redirect() {
+ let _http_server_guard = test_util::http_server();
+ // Relies on external http server. See target/debug/test_server
+ let url = Url::parse("http://127.0.0.1:4546/assets/fixture.json").unwrap();
+ // Dns resolver substitutes `127.0.0.1` with `localhost`
+ let target_url =
+ Url::parse("http://localhost:4545/assets/fixture.json").unwrap();
+ let client = create_test_client();
+ let result = fetch_once(
+ &client,
+ FetchOnceArgs {
+ url,
+ maybe_accept: None,
+ maybe_etag: None,
+ maybe_auth_token: None,
+ },
+ )
+ .await;
+ if let Ok(FetchOnceResult::Redirect(url, _)) = result {
+ assert_eq!(url, target_url);
+ } else {
+ panic!();
+ }
+ }
+
+ #[tokio::test]
+ async fn test_fetch_with_cafile_string() {
+ let _http_server_guard = test_util::http_server();
+ // Relies on external http server. See target/debug/test_server
+ let url = Url::parse("https://localhost:5545/assets/fixture.json").unwrap();
+
+ let client = HttpClient::from_client(
+ create_http_client(
+ version::get_user_agent(),
+ None,
+ vec![read(
+ test_util::testdata_path()
+ .join("tls/RootCA.pem")
+ .to_str()
+ .unwrap(),
+ )
+ .unwrap()],
+ None,
+ None,
+ None,
+ )
+ .unwrap(),
+ );
+ let result = fetch_once(
+ &client,
+ FetchOnceArgs {
+ url,
+ maybe_accept: None,
+ maybe_etag: None,
+ maybe_auth_token: None,
+ },
+ )
+ .await;
+ if let Ok(FetchOnceResult::Code(body, headers)) = result {
+ assert!(!body.is_empty());
+ assert_eq!(headers.get("content-type").unwrap(), "application/json");
+ assert_eq!(headers.get("etag"), None);
+ assert_eq!(headers.get("x-typescript-types"), None);
+ } else {
+ panic!();
+ }
+ }
+
+ #[tokio::test]
+ async fn test_fetch_with_default_certificate_store() {
+ let _http_server_guard = test_util::http_server();
+ // Relies on external http server with a valid mozilla root CA cert.
+ let url = Url::parse("https://deno.land").unwrap();
+ let client = HttpClient::from_client(
+ create_http_client(
+ version::get_user_agent(),
+ None, // This will load mozilla certs by default
+ vec![],
+ None,
+ None,
+ None,
+ )
+ .unwrap(),
+ );
+
+ let result = fetch_once(
+ &client,
+ FetchOnceArgs {
+ url,
+ maybe_accept: None,
+ maybe_etag: None,
+ maybe_auth_token: None,
+ },
+ )
+ .await;
+
+ println!("{:?}", result);
+ if let Ok(FetchOnceResult::Code(body, _headers)) = result {
+ assert!(!body.is_empty());
+ } else {
+ panic!();
+ }
+ }
+
+ // TODO(@justinmchase): Windows should verify certs too and fail to make this request without ca certs
+ #[cfg(not(windows))]
+ #[tokio::test]
+ #[ignore] // https://github.com/denoland/deno/issues/12561
+ async fn test_fetch_with_empty_certificate_store() {
+ use deno_runtime::deno_tls::rustls::RootCertStore;
+
+ let _http_server_guard = test_util::http_server();
+ // Relies on external http server with a valid mozilla root CA cert.
+ let url = Url::parse("https://deno.land").unwrap();
+ let client = HttpClient::new(
+ Some(RootCertStore::empty()), // no certs loaded at all
+ None,
+ )
+ .unwrap();
+
+ let result = fetch_once(
+ &client,
+ FetchOnceArgs {
+ url,
+ maybe_accept: None,
+ maybe_etag: None,
+ maybe_auth_token: None,
+ },
+ )
+ .await;
+
+ if let Ok(FetchOnceResult::Code(_body, _headers)) = result {
+ // This test is expected to fail since to CA certs have been loaded
+ panic!();
+ }
+ }
+
+ #[tokio::test]
+ async fn test_fetch_with_cafile_gzip() {
+ let _http_server_guard = test_util::http_server();
+ // Relies on external http server. See target/debug/test_server
+ let url =
+ Url::parse("https://localhost:5545/run/import_compression/gziped")
+ .unwrap();
+ let client = HttpClient::from_client(
+ create_http_client(
+ version::get_user_agent(),
+ None,
+ vec![read(
+ test_util::testdata_path()
+ .join("tls/RootCA.pem")
+ .to_str()
+ .unwrap(),
+ )
+ .unwrap()],
+ None,
+ None,
+ None,
+ )
+ .unwrap(),
+ );
+ let result = fetch_once(
+ &client,
+ FetchOnceArgs {
+ url,
+ maybe_accept: None,
+ maybe_etag: None,
+ maybe_auth_token: None,
+ },
+ )
+ .await;
+ if let Ok(FetchOnceResult::Code(body, headers)) = result {
+ assert_eq!(String::from_utf8(body).unwrap(), "console.log('gzip')");
+ assert_eq!(
+ headers.get("content-type").unwrap(),
+ "application/javascript"
+ );
+ assert_eq!(headers.get("etag"), None);
+ assert_eq!(headers.get("x-typescript-types"), None);
+ } else {
+ panic!();
+ }
+ }
+
+ #[tokio::test]
+ async fn test_fetch_with_cafile_with_etag() {
+ let _http_server_guard = test_util::http_server();
+ let url = Url::parse("https://localhost:5545/etag_script.ts").unwrap();
+ let client = HttpClient::from_client(
+ create_http_client(
+ version::get_user_agent(),
+ None,
+ vec![read(
+ test_util::testdata_path()
+ .join("tls/RootCA.pem")
+ .to_str()
+ .unwrap(),
+ )
+ .unwrap()],
+ None,
+ None,
+ None,
+ )
+ .unwrap(),
+ );
+ let result = fetch_once(
+ &client,
+ FetchOnceArgs {
+ url: url.clone(),
+ maybe_accept: None,
+ maybe_etag: None,
+ maybe_auth_token: None,
+ },
+ )
+ .await;
+ if let Ok(FetchOnceResult::Code(body, headers)) = result {
+ assert!(!body.is_empty());
+ assert_eq!(String::from_utf8(body).unwrap(), "console.log('etag')");
+ assert_eq!(
+ headers.get("content-type").unwrap(),
+ "application/typescript"
+ );
+ assert_eq!(headers.get("etag").unwrap(), "33a64df551425fcc55e");
+ assert_eq!(headers.get("x-typescript-types"), None);
+ } else {
+ panic!();
+ }
+
+ let res = fetch_once(
+ &client,
+ FetchOnceArgs {
+ url,
+ maybe_accept: None,
+ maybe_etag: Some("33a64df551425fcc55e".to_string()),
+ maybe_auth_token: None,
+ },
+ )
+ .await;
+ assert_eq!(res.unwrap(), FetchOnceResult::NotModified);
+ }
+
+ #[tokio::test]
+ async fn test_fetch_with_cafile_brotli() {
+ let _http_server_guard = test_util::http_server();
+ // Relies on external http server. See target/debug/test_server
+ let url =
+ Url::parse("https://localhost:5545/run/import_compression/brotli")
+ .unwrap();
+ let client = HttpClient::from_client(
+ create_http_client(
+ version::get_user_agent(),
+ None,
+ vec![read(
+ test_util::testdata_path()
+ .join("tls/RootCA.pem")
+ .to_str()
+ .unwrap(),
+ )
+ .unwrap()],
+ None,
+ None,
+ None,
+ )
+ .unwrap(),
+ );
+ let result = fetch_once(
+ &client,
+ FetchOnceArgs {
+ url,
+ maybe_accept: None,
+ maybe_etag: None,
+ maybe_auth_token: None,
+ },
+ )
+ .await;
+ if let Ok(FetchOnceResult::Code(body, headers)) = result {
+ assert!(!body.is_empty());
+ assert_eq!(String::from_utf8(body).unwrap(), "console.log('brotli');");
+ assert_eq!(
+ headers.get("content-type").unwrap(),
+ "application/javascript"
+ );
+ assert_eq!(headers.get("etag"), None);
+ assert_eq!(headers.get("x-typescript-types"), None);
+ } else {
+ panic!();
+ }
+ }
+
+ #[tokio::test]
+ async fn bad_redirect() {
+ let _g = test_util::http_server();
+ let url_str = "http://127.0.0.1:4545/bad_redirect";
+ let url = Url::parse(url_str).unwrap();
+ let client = create_test_client();
+ let result = fetch_once(
+ &client,
+ FetchOnceArgs {
+ url,
+ maybe_accept: None,
+ maybe_etag: None,
+ maybe_auth_token: None,
+ },
+ )
+ .await;
+ assert!(result.is_err());
+ let err = result.unwrap_err();
+ // Check that the error message contains the original URL
+ assert!(err.to_string().contains(url_str));
+ }
}