From d855ca9e0709845095cdea08a66f02248f1bbde4 Mon Sep 17 00:00:00 2001 From: Jonny Yu Date: Fri, 16 Oct 2026 22:24:39 -0700 Subject: [PATCH] Support ~/.netrc authentication for the HTTP provider Pass --netrc-optional to curl so hosts with a netrc entry are fetched with credentials. On 401/403, hint that a ~/.netrc entry can be used. Co-Authored-By: Claude Sonnet 5.5 --- src/curl.rs | 4 ++++ src/http_provider.rs | 21 +++++++++++++++++++-- tests/dotslash_tests.rs | 2 +- website/docs/dotslash-file.md | 13 +++++++++++++ 4 files changed, 37 insertions(+), 3 deletions(-) diff --git a/src/curl.rs b/src/curl.rs index fb6e88d..8a8c6eb 100644 --- a/src/curl.rs +++ b/src/curl.rs @@ -317,6 +317,10 @@ impl CurlCommand<'_> { curl_command.arg("--ssl-revoke-best-effort"); } + // Use credentials from ~/.netrc (%USERPROFILE%\_netrc or .netrc on + // Windows) for hosts that have an entry. Missing file is not an error. + curl_command.arg("--netrc-optional"); + curl_command.arg("--user-agent"); curl_command.arg(USER_AGENT); diff --git a/src/http_provider.rs b/src/http_provider.rs index 4b4b48d..991fbd5 100644 --- a/src/http_provider.rs +++ b/src/http_provider.rs @@ -10,15 +10,20 @@ use std::path::Path; -use anyhow::Context as _; use serde::Deserialize; use serde_json::Value; use crate::config::ArtifactEntry; use crate::curl::CurlCommand; +use crate::curl::CurlError; use crate::curl::FetchContext; use crate::provider::Provider; use crate::util::FileLock; +use crate::util::HttpStatus; + +const AUTH_HINT: &str = "the server rejected the request as unauthorized; to authenticate, add a \ + `machine login password ` entry to your ~/.netrc file \ + (%USERPROFILE%\\_netrc on Windows)"; pub struct HttpProvider {} @@ -47,7 +52,19 @@ impl Provider for HttpProvider { }; curl_cmd .get_request(destination, &fetch_context) - .with_context(|| format!("failed to fetch `{}`", url))?; + .map_err(|e| { + let is_auth_error = matches!( + e, + CurlError::HttpStatus(_, HttpStatus::Unauthorized | HttpStatus::Forbidden) + ); + let err = anyhow::Error::new(e); + let err = if is_auth_error { + err.context(AUTH_HINT) + } else { + err + }; + err.context(format!("failed to fetch `{}`", url)) + })?; Ok(()) } } diff --git a/tests/dotslash_tests.rs b/tests/dotslash_tests.rs index a59817c..23eae1b 100644 --- a/tests/dotslash_tests.rs +++ b/tests/dotslash_tests.rs @@ -338,7 +338,7 @@ caused by: no providers succeeded. warnings: failed to fetch artifact: failed to fetch `[PROVIDER_URL]` Caused by: - 0: `curl --location --retry 3 --fail --silent --show-error {}--user-agent [DOTSLASH_USER_AGENT] [PROVIDER_URL] --output [OUTPUT_FILE][..]` + 0: `curl --location --retry 3 --fail --silent --show-error {}--netrc-optional --user-agent [DOTSLASH_USER_AGENT] [PROVIDER_URL] --output [OUTPUT_FILE][..]` 1: 404 Not Found ", if cfg!(windows) { "--ssl-revoke-best-effort "} else {""}), ); diff --git a/website/docs/dotslash-file.md b/website/docs/dotslash-file.md index a9e949e..0ae9ed0 100644 --- a/website/docs/dotslash-file.md +++ b/website/docs/dotslash-file.md @@ -262,6 +262,19 @@ Mozilla/5.0 (compatible; DotSlash/0.1.0; +https://dotslash-cli.com) ::: +#### Authentication + +`curl` is invoked with `--netrc-optional`, so URLs that require HTTP basic +authentication work when the host has an entry in your `~/.netrc` file: + +```text +machine artifacts.example.com login myuser password mytoken +``` + +On Windows, curl also reads `%USERPROFILE%\_netrc` and `%USERPROFILE%\.netrc`. +Hosts without an entry are fetched without credentials. Credentials are never +stored in the DotSlash file. + Note that in order to facilitate creating a DotSlash file by hand, you can use DotSlash's `create-url-entry` subcommand to generate the boilerplate for a platform entry based on a URL as follows: