Skip to content

Commit

Permalink
fix(npm): use configured auth for tarball urls instead of scope auth (#…
Browse files Browse the repository at this point in the history
…24111)

Deno was using the scope auth for the tarball urls, which is not always
correct. We are going to do a release immediately for this issue.
  • Loading branch information
dsherret authored Jun 5, 2024
1 parent 0db73f6 commit 566adb7
Show file tree
Hide file tree
Showing 35 changed files with 194 additions and 61 deletions.
4 changes: 2 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ deno_emit = "=0.42.0"
deno_graph = { version = "=0.78.0", features = ["tokio_executor"] }
deno_lint = { version = "=0.60.0", features = ["docs"] }
deno_lockfile.workspace = true
deno_npm = "=0.21.1"
deno_npm = "=0.21.2"
deno_runtime = { workspace = true, features = ["include_js_files_for_snapshotting"] }
deno_semver = "=0.5.4"
deno_task_shell = "=0.16.1"
Expand Down
1 change: 1 addition & 0 deletions cli/args/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -619,6 +619,7 @@ pub fn create_default_npmrc() -> Arc<ResolvedNpmRc> {
config: Default::default(),
},
scopes: Default::default(),
registry_configs: Default::default(),
})
}

Expand Down
56 changes: 38 additions & 18 deletions cli/http_util.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ use crate::version::get_user_agent;
use cache_control::Cachability;
use cache_control::CacheControl;
use chrono::DateTime;
use deno_core::anyhow::bail;
use deno_core::error::custom_error;
use deno_core::error::generic_error;
use deno_core::error::AnyError;
Expand All @@ -30,6 +29,7 @@ use std::sync::Arc;
use std::thread::ThreadId;
use std::time::Duration;
use std::time::SystemTime;
use thiserror::Error;

// TODO(ry) HTTP headers are not unique key, value pairs. There may be more than
// one header line with the same key. This should be changed to something like
Expand Down Expand Up @@ -260,6 +260,27 @@ impl HttpClientProvider {
}
}

#[derive(Debug, Error)]
#[error("Bad response: {:?}{}", .status_code, .response_text.as_ref().map(|s| format!("\n\n{}", s)).unwrap_or_else(String::new))]
pub struct BadResponseError {
pub status_code: StatusCode,
pub response_text: Option<String>,
}

#[derive(Debug, Error)]
pub enum DownloadError {
#[error(transparent)]
Reqwest(#[from] reqwest::Error),
#[error(transparent)]
ToStr(#[from] reqwest::header::ToStrError),
#[error("Redirection from '{}' did not provide location header", .request_url)]
NoRedirectHeader { request_url: Url },
#[error("Too many redirects.")]
TooManyRedirects,
#[error(transparent)]
BadResponse(#[from] BadResponseError),
}

#[derive(Debug)]
pub struct HttpClient {
#[allow(clippy::disallowed_types)] // reqwest::Client allowed here
Expand Down Expand Up @@ -409,7 +430,7 @@ impl HttpClient {
url: impl reqwest::IntoUrl,
maybe_header: Option<(HeaderName, HeaderValue)>,
progress_guard: &UpdateGuard,
) -> Result<Option<Vec<u8>>, AnyError> {
) -> Result<Option<Vec<u8>>, DownloadError> {
self
.download_inner(url, maybe_header, Some(progress_guard))
.await
Expand All @@ -429,34 +450,33 @@ impl HttpClient {
url: impl reqwest::IntoUrl,
maybe_header: Option<(HeaderName, HeaderValue)>,
progress_guard: Option<&UpdateGuard>,
) -> Result<Option<Vec<u8>>, AnyError> {
) -> Result<Option<Vec<u8>>, DownloadError> {
let response = self.get_redirected_response(url, maybe_header).await?;

if response.status() == 404 {
return Ok(None);
} else if !response.status().is_success() {
let status = response.status();
let maybe_response_text = response.text().await.ok();
bail!(
"Bad response: {:?}{}",
status,
match maybe_response_text {
Some(text) => format!("\n\n{text}"),
None => String::new(),
}
);
return Err(DownloadError::BadResponse(BadResponseError {
status_code: status,
response_text: maybe_response_text
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty()),
}));
}

get_response_body_with_progress(response, progress_guard)
.await
.map(Some)
.map_err(Into::into)
}

async fn get_redirected_response(
&self,
url: impl reqwest::IntoUrl,
mut maybe_header: Option<(HeaderName, HeaderValue)>,
) -> Result<reqwest::Response, AnyError> {
) -> Result<reqwest::Response, DownloadError> {
let mut url = url.into_url()?;
let mut builder = self.get(url.clone());
if let Some((header_name, header_value)) = maybe_header.as_ref() {
Expand Down Expand Up @@ -486,7 +506,7 @@ impl HttpClient {
return Ok(new_response);
}
}
Err(custom_error("Http", "Too many redirects."))
Err(DownloadError::TooManyRedirects)
} else {
Ok(response)
}
Expand All @@ -496,7 +516,7 @@ impl HttpClient {
async fn get_response_body_with_progress(
response: reqwest::Response,
progress_guard: Option<&UpdateGuard>,
) -> Result<Vec<u8>, AnyError> {
) -> Result<Vec<u8>, reqwest::Error> {
if let Some(progress_guard) = progress_guard {
if let Some(total_size) = response.content_length() {
progress_guard.set_total_size(total_size);
Expand Down Expand Up @@ -546,17 +566,17 @@ fn resolve_url_from_location(base_url: &Url, location: &str) -> Url {
fn resolve_redirect_from_response(
request_url: &Url,
response: &reqwest::Response,
) -> Result<Url, AnyError> {
) -> Result<Url, DownloadError> {
debug_assert!(response.status().is_redirection());
if let Some(location) = response.headers().get(LOCATION) {
let location_string = location.to_str()?;
log::debug!("Redirecting to {:?}...", &location_string);
let new_url = resolve_url_from_location(request_url, location_string);
Ok(new_url)
} else {
Err(generic_error(format!(
"Redirection from '{request_url}' did not provide location header"
)))
Err(DownloadError::NoRedirectHeader {
request_url: request_url.clone(),
})
}
}

Expand Down
41 changes: 34 additions & 7 deletions cli/npm/managed/cache/tarball.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,11 @@ use deno_npm::npm_rc::ResolvedNpmRc;
use deno_npm::registry::NpmPackageVersionDistInfo;
use deno_runtime::deno_fs::FileSystem;
use deno_semver::package::PackageNv;
use reqwest::StatusCode;
use reqwest::Url;

use crate::args::CacheSetting;
use crate::http_util::DownloadError;
use crate::http_util::HttpClientProvider;
use crate::npm::common::maybe_auth_header_for_npm_registry;
use crate::util::progress_bar::ProgressBar;
Expand Down Expand Up @@ -138,8 +141,6 @@ impl TarballCache {
let tarball_cache = self.clone();
async move {
let registry_url = tarball_cache.npmrc.get_registry_url(&package_nv.name);
let registry_config =
tarball_cache.npmrc.get_registry_config(&package_nv.name).clone();
let package_folder =
tarball_cache.cache.package_folder_for_nv_and_url(&package_nv, registry_url);
let should_use_cache = tarball_cache.cache.should_use_cache_for_package(&package_nv);
Expand All @@ -161,14 +162,40 @@ impl TarballCache {
bail!("Tarball URL was empty.");
}

let maybe_auth_header =
maybe_auth_header_for_npm_registry(&registry_config);
// IMPORTANT: npm registries may specify tarball URLs at different URLS than the
// registry, so we MUST get the auth for the tarball URL and not the registry URL.
let tarball_uri = Url::parse(&dist.tarball)?;
let maybe_registry_config =
tarball_cache.npmrc.tarball_config(&tarball_uri);
let maybe_auth_header = maybe_registry_config.and_then(|c| maybe_auth_header_for_npm_registry(c));

let guard = tarball_cache.progress_bar.update(&dist.tarball);
let maybe_bytes = tarball_cache.http_client_provider
let result = tarball_cache.http_client_provider
.get_or_create()?
.download_with_progress(&dist.tarball, maybe_auth_header, &guard)
.await?;
.download_with_progress(tarball_uri, maybe_auth_header, &guard)
.await;
let maybe_bytes = match result {
Ok(maybe_bytes) => maybe_bytes,
Err(DownloadError::BadResponse(err)) => {
if err.status_code == StatusCode::UNAUTHORIZED
&& maybe_registry_config.is_none()
&& tarball_cache.npmrc.get_registry_config(&package_nv.name).auth_token.is_some()
{
bail!(
concat!(
"No auth for tarball URI, but present for scoped registry.\n\n",
"Tarball URI: {}\n",
"Scope URI: {}\n\n",
"More info here: https://github.com/npm/cli/wiki/%22No-auth-for-URI,-but-auth-present-for-scoped-registry%22"
),
dist.tarball,
registry_url,
)
}
return Err(err.into())
},
Err(err) => return Err(err.into()),
};
match maybe_bytes {
Some(bytes) => {
let extraction_mode = if should_use_cache || !package_folder_exists {
Expand Down
4 changes: 2 additions & 2 deletions tests/integration/lsp_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8896,8 +8896,8 @@ fn lsp_npmrc() {
temp_dir.write(
temp_dir.path().join(".npmrc"),
"\
@denotest:registry=http://127.0.0.1:4261/
//127.0.0.1:4261/:_authToken=private-reg-token
@denotest:registry=http://localhost:4261/
//localhost:4261/:_authToken=private-reg-token
",
);
let file = source_file(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
module.exports = () => 'hi_private1';
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"name": "@denotest/tarballs-privateserver2",
"version": "1.0.0"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
module.exports = () => 'hi_private2';
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"name": "@denotest/tarballs-privateserver2",
"version": "1.0.0"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
// this is a special package that the test server serves tarballs from the second private registry server
module.exports = () => 'hi';
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"name": "@denotest/tarballs-privateserver2",
"version": "1.0.0"
}
8 changes: 4 additions & 4 deletions tests/specs/compile/npmrc/.npmrc
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
@denotest:registry=http://127.0.0.1:4261/
//127.0.0.1:4261/:_authToken=private-reg-token
@denotest2:registry=http://127.0.0.1:4262/
//127.0.0.1:4262/:_authToken=private-reg-token2
@denotest:registry=http://localhost:4261/
//localhost:4261/:_authToken=private-reg-token
@denotest2:registry=http://localhost:4262/
//localhost:4262/:_authToken=private-reg-token2
4 changes: 2 additions & 2 deletions tests/specs/compile/npmrc/install.out
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
⚠️ `deno install` behavior will change in Deno 2. To preserve the current behavior use the `-g` or `--global` flag.
[UNORDERED_START]
Download http://127.0.0.1:4261/@denotest/basic
Download http://127.0.0.1:4262/@denotest2/basic
Download http://localhost:4261/@denotest/basic
Download http://localhost:4262/@denotest2/basic
Download http://localhost:4261/@denotest/basic/1.0.0.tgz
Download http://localhost:4262/@denotest2/basic/1.0.0.tgz
Initialize @denotest2/[email protected]
Expand Down
8 changes: 4 additions & 4 deletions tests/specs/npm/npmrc/.npmrc
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
@denotest:registry=http://127.0.0.1:4261/
//127.0.0.1:4261/:_authToken=private-reg-token
@denotest2:registry=http://127.0.0.1:4262/
//127.0.0.1:4262/:_authToken=private-reg-token2
@denotest:registry=http://localhost:4261/
//localhost:4261/:_authToken=private-reg-token
@denotest2:registry=http://localhost:4262/
//localhost:4262/:_authToken=private-reg-token2
4 changes: 2 additions & 2 deletions tests/specs/npm/npmrc/install.out
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
⚠️ `deno install` behavior will change in Deno 2. To preserve the current behavior use the `-g` or `--global` flag.
[UNORDERED_START]
Download http://127.0.0.1:4261/@denotest/basic
Download http://127.0.0.1:4262/@denotest2/basic
Download http://localhost:4261/@denotest/basic
Download http://localhost:4262/@denotest2/basic
Download http://localhost:4261/@denotest/basic/1.0.0.tgz
Download http://localhost:4262/@denotest2/basic/1.0.0.tgz
Initialize @denotest2/[email protected]
Expand Down
4 changes: 2 additions & 2 deletions tests/specs/npm/npmrc_bad_registry_config/.npmrc
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
@denotest:registry=http://127.0.0.1:4261/
@denotest:registry=http://localhost:4261/
; This configuration is wrong - the registry URL must
; be exactly the same as registry configured for the scope,
; not root url + scope name.
//127.0.0.1:4261/denotest/:_authToken=invalid-token
//localhost:4261/denotest/:_authToken=invalid-token
4 changes: 2 additions & 2 deletions tests/specs/npm/npmrc_bad_registry_config/main.out
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
⚠️ `deno install` behavior will change in Deno 2. To preserve the current behavior use the `-g` or `--global` flag.
Download http://127.0.0.1:4261/@denotest/basic
error: Error getting response at http://127.0.0.1:4261/@denotest/basic for package "@denotest/basic": Bad response: 401
Download http://localhost:4261/@denotest/basic
error: Error getting response at http://localhost:4261/@denotest/basic for package "@denotest/basic": Bad response: 401
[WILDCARD]
4 changes: 2 additions & 2 deletions tests/specs/npm/npmrc_bad_token/.npmrc
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
@denotest:registry=http://127.0.0.1:4261/
//127.0.0.1:4261/:_authToken=invalid-token
@denotest:registry=http://localhost:4261/
//localhost:4261/:_authToken=invalid-token
4 changes: 2 additions & 2 deletions tests/specs/npm/npmrc_bad_token/main.out
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
⚠️ `deno install` behavior will change in Deno 2. To preserve the current behavior use the `-g` or `--global` flag.
Download http://127.0.0.1:4261/@denotest/basic
error: Error getting response at http://127.0.0.1:4261/@denotest/basic for package "@denotest/basic": Bad response: 401
Download http://localhost:4261/@denotest/basic
error: Error getting response at http://localhost:4261/@denotest/basic for package "@denotest/basic": Bad response: 401
[WILDCARD]
8 changes: 4 additions & 4 deletions tests/specs/npm/npmrc_basic_auth/.npmrc
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
@denotest:registry=http://127.0.0.1:4261/
//127.0.0.1:4261/:_auth=ZGVubzpsYW5k
@denotest2:registry=http://127.0.0.1:4262/
//127.0.0.1:4262/:_auth=ZGVubzpsYW5kMg==
@denotest:registry=http://localhost:4261/
//localhost:4261/:_auth=ZGVubzpsYW5k
@denotest2:registry=http://localhost:4262/
//localhost:4262/:_auth=ZGVubzpsYW5kMg==
4 changes: 2 additions & 2 deletions tests/specs/npm/npmrc_basic_auth/install.out
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
⚠️ `deno install` behavior will change in Deno 2. To preserve the current behavior use the `-g` or `--global` flag.
[UNORDERED_START]
Download http://127.0.0.1:4261/@denotest/basic
Download http://127.0.0.1:4262/@denotest2/basic
Download http://localhost:4261/@denotest/basic
Download http://localhost:4262/@denotest2/basic
Download http://localhost:4261/@denotest/basic/1.0.0.tgz
Download http://localhost:4262/@denotest2/basic/1.0.0.tgz
Initialize @denotest2/[email protected]
Expand Down
4 changes: 2 additions & 2 deletions tests/specs/npm/npmrc_deno_json/.npmrc
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
@denotest:registry=http://127.0.0.1:4261/
//127.0.0.1:4261/:_authToken=private-reg-token
@denotest:registry=http://localhost:4261/
//localhost:4261/:_authToken=private-reg-token
2 changes: 1 addition & 1 deletion tests/specs/npm/npmrc_deno_json/main.out
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
Download http://127.0.0.1:4261/@denotest/basic
Download http://localhost:4261/@denotest/basic
Download http://localhost:4261/@denotest/basic/1.0.0.tgz
0
42
4 changes: 2 additions & 2 deletions tests/specs/npm/npmrc_not_next_to_package_json/.npmrc
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
@denotest:registry=http://127.0.0.1:4261/
//127.0.0.1:4261/:_authToken=private-reg-token
@denotest:registry=http://localhost:4261/
//localhost:4261/:_authToken=private-reg-token
17 changes: 17 additions & 0 deletions tests/specs/npm/npmrc_tarball_other_server/__test__.jsonc
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"tempDir": true,
"tests": {
"auth_success": {
"cwd": "success",
"args": "run --node-modules-dir -A main.js",
"output": "success/main.out",
"exitCode": 1
},
"auth_fail": {
"cwd": "fail",
"args": "run --node-modules-dir -A main.js",
"output": "fail/main.out",
"exitCode": 1
}
}
}
2 changes: 2 additions & 0 deletions tests/specs/npm/npmrc_tarball_other_server/fail/.npmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
@denotest:registry=http://localhost:4261/
//localhost:4261/:_authToken=private-reg-token
3 changes: 3 additions & 0 deletions tests/specs/npm/npmrc_tarball_other_server/fail/main.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import getValue from "@denotest/tarballs-privateserver2";

console.log(getValue());
Loading

0 comments on commit 566adb7

Please sign in to comment.