diff --git a/doc/NEWS.Rd b/doc/NEWS.Rd index 1ebd0b3682..878faf9544 100644 --- a/doc/NEWS.Rd +++ b/doc/NEWS.Rd @@ -125,6 +125,11 @@ \subsection{Utilities}{ \itemize{ + \item The default HTTP user agent on Linux now includes + machine-readable distribution identifiers from \file{os-release} when + available. The \samp{libcurl} download method now uses the same default + user agent as the other HTTP methods. + \item \code{tools::texi2pdf()} now reports BibTeX warnings; in particular, missing references are now noted by \command{R CMD check} when re-building PDF vignettes. diff --git a/src/library/base/man/options.Rd b/src/library/base/man/options.Rd index 114bdb27c1..2dba451c56 100644 --- a/src/library/base/man/options.Rd +++ b/src/library/base/man/options.Rd @@ -707,12 +707,10 @@ getOption(x, default = NULL) HTTP(S) requests by \code{\link{download.file}}, \code{\link{url}} and \code{\link{curlGetHeaders}}, or \code{NULL} when requests will be made without a user agent header. The default is - \code{"R (\var{version} \var{platform} \var{arch} \var{os})"} - except when \samp{libcurl} is used when it is - \code{"libcurl/\var{version}"} for the \samp{libcurl} version in use. - With \samp{libcurl}, the \code{"R "} prefix is reserved: - \code{HTTPUserAgent} with the prefix will be ignored and the default will - be used.} + \code{"R (\var{version} \var{platform} \var{arch} \var{os})"}. + On Linux, machine-readable distribution identifiers from + \file{/etc/os-release} or \file{/usr/lib/os-release} are appended when + available.} \item{\code{install.lock}:}{logical: should per-directory package locking be used by \code{\link{install.packages}}? Most useful diff --git a/src/library/utils/R/readhttp.R b/src/library/utils/R/readhttp.R index e441ea7882..ffef100132 100644 --- a/src/library/utils/R/readhttp.R +++ b/src/library/utils/R/readhttp.R @@ -1,7 +1,7 @@ # File src/library/utils/R/readhttp.R # Part of the R package, https://www.R-project.org # -# Copyright (C) 1995-2012 The R Core Team +# Copyright (C) 1995-2026 The R Core Team # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -26,6 +26,35 @@ } +.linuxOSRelease <- function(files = c("/etc/os-release", "/usr/lib/os-release"), + os = R.version$os) +{ + if (!startsWith(os, "linux")) return(character()) + + files <- files[file_test("-f", files)] + if (!length(files)) return(character()) + lines <- tryCatch(suppressWarnings(readLines(files[1L], warn = FALSE)), + error = function(e) character()) + if (!length(lines)) return(character()) + + value <- function(key) { + ## os-release: the last assignment of a key wins + line <- rev(grep(paste0("^", key, "="), lines, value = TRUE))[1L] + if (is.na(line)) return("") + ans <- trimws(substring(line, nchar(key) + 2L)) + if (grepl('^".*"$', ans) || grepl("^'.*'$", ans)) + ans <- substring(ans, 2L, nchar(ans) - 1L) + if (!grepl("^[A-Za-z0-9._-]+$", ans)) "" else ans + } + + ans <- c(distro = value("ID"), version = value("VERSION_ID"), + codename = value("VERSION_CODENAME")) + ans[nzchar(ans)] +} + defaultUserAgent <- function() { @@ -34,6 +63,11 @@ if (length(grep("^darwin", R.version$os))) ## use run-time (vs build) gsub(" ","",tolower(system("uname -sr 2>/dev/null||echo darwin", intern=TRUE))) else R.version$os) + distro <- .linuxOSRelease() + if (length(distro)) + Rdetails <- paste0(Rdetails, "; ", + paste(names(distro), distro, sep = "=", + collapse = "; ")) paste0("R (", Rdetails, ")") } diff --git a/src/library/utils/tests/user-agent.R b/src/library/utils/tests/user-agent.R new file mode 100644 index 0000000000..4bb2847f8d --- /dev/null +++ b/src/library/utils/tests/user-agent.R @@ -0,0 +1,49 @@ +## Test machine-readable Linux distribution details in the default user agent. + +write_os_release <- function(lines) { + path <- tempfile() + writeLines(lines, path) + path +} + +ubuntu <- write_os_release(c( + 'NAME="Ubuntu"', + 'ID=ubuntu', + 'VERSION_ID="26.04"', + 'VERSION_CODENAME=resolute' +)) +info <- utils:::.linuxOSRelease(ubuntu, os = "linux-gnu") +stopifnot(identical(info, c( + distro = "ubuntu", version = "26.04", codename = "resolute" +))) + +alpine <- write_os_release(c("ID=alpine", 'VERSION_ID="3.24.1"')) +info <- utils:::.linuxOSRelease(alpine, os = "linux-musl") +stopifnot(identical(info, c(distro = "alpine", version = "3.24.1"))) + +unsafe <- write_os_release(c( + "ID=example", "VERSION_ID=1.0", "VERSION_CODENAME=bad/value" +)) +info <- utils:::.linuxOSRelease(unsafe, os = "linux-gnu") +stopifnot(identical(info, c(distro = "example", version = "1.0"))) + +dups <- write_os_release(c("ID=first", "ID=last", 'VERSION_ID="2.0" ')) +info <- utils:::.linuxOSRelease(dups, os = "linux-gnu") +stopifnot(identical(info, c(distro = "last", version = "2.0"))) + +rolling <- write_os_release("ID=arch") +info <- utils:::.linuxOSRelease(rolling, os = "linux-gnu") +stopifnot(identical(info, c(distro = "arch"))) + +stopifnot(!length(utils:::.linuxOSRelease(ubuntu, os = "darwin23"))) +stopifnot(!length(utils:::.linuxOSRelease(tempfile(), os = "linux-gnu"))) + +dir <- tempfile(); dir.create(dir) +info <- withCallingHandlers(utils:::.linuxOSRelease(dir, os = "linux-gnu"), + warning = function(w) stop(w)) +stopifnot(!length(info)) + +unlink(c(ubuntu, alpine, unsafe, dups, rolling, dir), recursive = TRUE) diff --git a/src/modules/internet/libcurl.c b/src/modules/internet/libcurl.c index 2b07785d2a..e65e01c6e6 100644 --- a/src/modules/internet/libcurl.c +++ b/src/modules/internet/libcurl.c @@ -288,11 +288,8 @@ void curlCommon(CURL *hnd, int redirect, int verify) if (TYPEOF(sua) == STRSXP && LENGTH(sua) == 1 ) { const void *vmax = vmaxget(); const char *p = translateChar(STRING_ELT(sua, 0)); - if (p[0] && p[1] && p[2] && p[0] == 'R' && p[1] == ' ' && p[2] == '(') { - } else { - Default = 0; - curl_easy_setopt(hnd, CURLOPT_USERAGENT, p); - } + Default = 0; + curl_easy_setopt(hnd, CURLOPT_USERAGENT, p); vmaxset(vmax); } if (Default) { diff --git a/tests/download.file.R b/tests/download.file.R index fa73fceda9..2465fc33be 100644 --- a/tests/download.file.R +++ b/tests/download.file.R @@ -61,6 +61,12 @@ with_options <- function(opts, expr) { tests <- function() { cat("- User agent is still set\n") + with_options(list(HTTPUserAgent = utils:::defaultUserAgent()), { + h <- get_headers() + stopifnot(grep_hdr("User-Agent.*R \\(", h)) + }) + with_options(list(HTTPUserAgent = "foobar"), { h <- get_headers() stopifnot(grep_hdr("User-Agent.*foobar", h))