diff --git a/README.md b/README.md index e9b4ff93..ed486057 100644 --- a/README.md +++ b/README.md @@ -667,6 +667,8 @@ To pass an Authorization header through to the mirror url, set `$NVM_AUTH_HEADER NVM_AUTH_HEADER="Bearer secret-token" nvm install node ``` +`$NVM_AUTH_HEADER` is the recommended way to authenticate to a mirror. Credentials embedded in the mirror URL itself (`https://user:password@host/...`) also work, and nvm prints such URLs with the credentials masked as `***@`. + ### Platforms without official binaries nodejs.org does not publish a binary for every platform and architecture nvm can detect. Where it does not, `nvm install` falls back by default to compiling from source, which is slow and needs a C++ toolchain (see [Important Notes](#important-notes)); set `$NVM_NO_SOURCE_FALLBACK` to `1` to make a missing binary an error instead. diff --git a/nvm.sh b/nvm.sh index 605945e4..71f5661a 100755 --- a/nvm.sh +++ b/nvm.sh @@ -3168,17 +3168,17 @@ nvm_download_artifact() { nvm_err "Removing the broken local cache..." command rm -rf "${TARBALL}" fi - nvm_err "Downloading ${TARBALL_URL}..." + nvm_err "Downloading $(nvm_sanitize_url "${TARBALL_URL}")..." # stdout is reserved for the tarball path, so nothing the downloader prints may reach it nvm_download -L -C - "${PROGRESS_BAR}" "${TARBALL_URL}" -o "${TARBALL}" >&2 || { command rm -rf "${TARBALL}" "${tmpdir}" - nvm_err "download from ${TARBALL_URL} failed" + nvm_err "download from $(nvm_sanitize_url "${TARBALL_URL}") failed" return 4 } if nvm_grep '404 Not Found' "${TARBALL}" >/dev/null; then command rm -rf "${TARBALL}" "${tmpdir}" - nvm_err "HTTP 404 at URL ${TARBALL_URL}" + nvm_err "HTTP 404 at URL $(nvm_sanitize_url "${TARBALL_URL}")" return 5 fi @@ -3697,6 +3697,12 @@ nvm_sanitize_path() { nvm_echo "${SANITIZED_PATH}" } +# Replace the `user:password@` part of any URL with `***@`, for printing. +# Like curl and wget, everything up to the last `@` before the path is userinfo. +nvm_sanitize_url() { + nvm_echo "${1-}" | command sed -e 's#://[^/[:space:]]*@#://***@#g' +} + nvm_is_natural_num() { if [ -z "$1" ]; then return 4 @@ -4093,8 +4099,8 @@ nvm() { nvm_err "\${PATH}: $(nvm_sanitize_path "${PATH}")" nvm_err "\$PREFIX: '$(nvm_sanitize_path "${PREFIX-}")'" nvm_err "\${NPM_CONFIG_PREFIX}: '$(nvm_sanitize_path "${NPM_CONFIG_PREFIX-}")'" - nvm_err "\$NVM_NODEJS_ORG_MIRROR: '${NVM_NODEJS_ORG_MIRROR-}'" - nvm_err "\$NVM_IOJS_ORG_MIRROR: '${NVM_IOJS_ORG_MIRROR-}'" + nvm_err "\$NVM_NODEJS_ORG_MIRROR: '$(nvm_sanitize_url "${NVM_NODEJS_ORG_MIRROR-}")'" + nvm_err "\$NVM_IOJS_ORG_MIRROR: '$(nvm_sanitize_url "${NVM_IOJS_ORG_MIRROR-}")'" nvm_err "shell version: '$(${SHELL} --version | command head -n 1)'" nvm_err "uname -a: '$(command uname -a | command awk '{$2=""; print}' | command xargs)'" nvm_err "checksum binary: '$(nvm_get_checksum_binary 2>/dev/null)'" @@ -5480,7 +5486,7 @@ nvm() { nvm_install_lock_name nvm_acquire_install_lock nvm_release_install_lock \ nvm_list_aliases nvm_make_alias nvm_print_alias_file nvm_print_alias_path \ nvm_print_default_alias nvm_print_formatted_alias nvm_resolve_local_alias \ - nvm_replace_literal nvm_sanitize_path nvm_has_colors nvm_has_italics nvm_process_parameters \ + nvm_replace_literal nvm_sanitize_path nvm_sanitize_url nvm_has_colors nvm_has_italics nvm_process_parameters \ nvm_node_version_has_solaris_binary nvm_iojs_version_has_solaris_binary \ nvm_curl_libz_support nvm_command_info nvm_is_zsh nvm_stdout_is_terminal \ nvm_npmrc_bad_news_bears nvm_sanitize_auth_header \ diff --git a/test/fast/Unit tests/mirror URL credentials are masked in output b/test/fast/Unit tests/mirror URL credentials are masked in output new file mode 100755 index 00000000..34a211f7 --- /dev/null +++ b/test/fast/Unit tests/mirror URL credentials are masked in output @@ -0,0 +1,76 @@ +#!/bin/sh + +WORK="$PWD/mirror-url-credentials-work.$$" +ORIG_NVM_DIR="${NVM_DIR-}" + +cleanup() { + unset -f nvm_download nvm_get_checksum nvm_compare_checksum + unset NVM_NODEJS_ORG_MIRROR NVM_IOJS_ORG_MIRROR + NVM_DIR="${ORIG_NVM_DIR}" + rm -rf "${WORK}" +} +die () { echo "$@" ; cleanup ; exit 1; } + +# args: description, output +assert_masked() { + case "${2}" in + *s3cr3t* | *ci-bot*) die "${1} printed the mirror URL credentials; got >${2}<" ;; + esac + case "${2}" in + *'://***@mirror.example.com/dist'*) ;; + *) die "${1} did not print the masked mirror URL; got >${2}<" ;; + esac +} + +\. ../../../nvm.sh + +mkdir -p "${WORK}" + +NVM_NODEJS_ORG_MIRROR='https://ci-bot:s3cr3t@mirror.example.com/dist' +NVM_IOJS_ORG_MIRROR='https://ci-bot:s3cr3t@mirror.example.com/dist' +export NVM_NODEJS_ORG_MIRROR NVM_IOJS_ORG_MIRROR + +assert_masked 'nvm debug' "$(nvm debug 2>&1)" + +NVM_DIR="${WORK}" +nvm_get_checksum() { nvm_echo 'fake_checksum'; } +nvm_compare_checksum() { return 0; } + +# nvm_download still receives the unmasked URL +nvm_download() { + case "$*" in + *"${NVM_NODEJS_ORG_MIRROR}/v20.0.0/"*) ;; + *) nvm_err 'nvm_download did not receive the mirror URL credentials' ;; + esac + return "${DOWNLOAD_EXIT_CODE-0}" +} +OUTPUT="$(nvm_download_artifact node source std v20.0.0 2>&1)" +assert_masked 'nvm_download_artifact' "${OUTPUT}" +case "${OUTPUT}" in + *'did not receive'*) die "${OUTPUT}" ;; +esac + +OUTPUT="$(DOWNLOAD_EXIT_CODE=1 nvm_download_artifact node source std v20.0.0 2>&1)" +case "${OUTPUT}" in + *'download from '*' failed'*) ;; + *) die "expected a download failure message; got >${OUTPUT}<" ;; +esac +assert_masked 'nvm_download_artifact, on a failed download,' "${OUTPUT#*...}" + +nvm_download() { + while [ "$#" -gt 0 ]; do + if [ "$1" = '-o' ]; then + nvm_echo '404 Not Found' > "$2" + return 0 + fi + shift + done +} +OUTPUT="$(nvm_download_artifact node source std v20.0.0 2>&1)" +case "${OUTPUT}" in + *'HTTP 404 at URL '*) ;; + *) die "expected an HTTP 404 message; got >${OUTPUT}<" ;; +esac +assert_masked 'nvm_download_artifact, on a 404,' "${OUTPUT#*...}" + +cleanup diff --git a/test/fast/Unit tests/nvm_sanitize_url b/test/fast/Unit tests/nvm_sanitize_url new file mode 100755 index 00000000..d0fdc3c0 --- /dev/null +++ b/test/fast/Unit tests/nvm_sanitize_url @@ -0,0 +1,31 @@ +#!/bin/sh + +die () { echo "$@" ; exit 1; } + +\. ../../../nvm.sh + +check() { + ACTUAL="$(nvm_sanitize_url "${1}")" + [ "${ACTUAL}" = "${2}" ] || die "nvm_sanitize_url '${1}': expected >${2}<, got >${ACTUAL}<" +} + +# URLs without userinfo are unchanged +check '' '' +check 'https://nodejs.org/dist' 'https://nodejs.org/dist' +check 'https://nodejs.org' 'https://nodejs.org' +check 'http://127.0.0.1:8080/dist/v20.0.0/node-v20.0.0.tar.xz' 'http://127.0.0.1:8080/dist/v20.0.0/node-v20.0.0.tar.xz' +check 'https://[::1]:8080/dist' 'https://[::1]:8080/dist' +check 'https://nodejs.org/a@b/dist' 'https://nodejs.org/a@b/dist' + +# userinfo is masked +check 'https://user:pw@nexus.example.com/repository/nodejs-dist' 'https://***@nexus.example.com/repository/nodejs-dist' +check 'https://user:pw@nexus.example.com' 'https://***@nexus.example.com' +check 'https://token@nexus.example.com:8443/dist' 'https://***@nexus.example.com:8443/dist' +check 'http://user:p%40w@[::1]:8080/dist' 'http://***@[::1]:8080/dist' +# everything up to the last `@` before the path is userinfo; an `@` in the path is kept +check 'https://user:p@w@nexus.example.com/a@b' 'https://***@nexus.example.com/a@b' + +# every URL in multiline text is masked +check 'URL:http://user:pw@a.example.com/x [1/1] +https://user:pw@b.example.com/y' 'URL:http://***@a.example.com/x [1/1] +https://***@b.example.com/y'