diff options
author | David Sherret <dsherret@users.noreply.github.com> | 2022-12-14 08:47:18 -0500 |
---|---|---|
committer | GitHub <noreply@github.com> | 2022-12-14 08:47:18 -0500 |
commit | 4a64ca850131a0aa07e8c781e6e194246f94eeb6 (patch) | |
tree | e8c6ac26e86813b33743475c366caaa58fe5bfdb /cli/file_fetcher.rs | |
parent | f9db129bdf1e09f6d5faaa73ad0cad27d2418798 (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.rs | 574 |
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)); + } } |