Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(npm): use configured auth for tarball urls instead of scope auth #24111

Merged
merged 1 commit into from
Jun 5, 2024
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
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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice cleanup 👍

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"
Comment on lines +186 to +189
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great, thanks!

),
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/basic@1.0.0
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/basic@1.0.0
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/basic@1.0.0
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