diff --git a/.Rbuildignore b/.Rbuildignore index 4ad8b16b..d2a67aa4 100644 --- a/.Rbuildignore +++ b/.Rbuildignore @@ -11,3 +11,5 @@ docs ^docs$ ^pkgdown$ ^\.github$ +^.*\.code-workspace$ +^revdep$ diff --git a/DESCRIPTION b/DESCRIPTION index d2fddd83..a06b148a 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -1,6 +1,6 @@ Package: dataone Version: 2.3.0 -Date: 2025-10-08 +Date: 2025-12-05 Title: R Interface to the DataONE REST API Authors@R: c( person("Matthew B.", "Jones", role = c("aut","cre"), email = "jones@nceas.ucsb.edu", comment=c(ORCID = "0000-0003-0077-4738")), person("Peter", "Slaughter", role = "aut", email = "slaughter@nceas.ucsb.edu", comment=c(ORCID = "0000-0002-2192-403X")), diff --git a/NEWS b/NEWS index e4bc4b3e..b0522dca 100644 --- a/NEWS +++ b/NEWS @@ -1,3 +1,11 @@ +# Version 2.3.0 + +## Bug Fixes + +* Fix documentaion links and cross-references (#306) +* isCertExpired() test produces timezone mismatch warning (#304) +* Fix windows TLS error by forcing TLS 1.2 client side (#308) + # Version 2.2.2 ## New Features and functions diff --git a/R/CNode.R b/R/CNode.R index 04465be7..22df5d65 100644 --- a/R/CNode.R +++ b/R/CNode.R @@ -107,11 +107,16 @@ setMethod("CNode", signature("character"), function(x) { else stop(sprintf("Unknown DataONE environment: %s", x)) } - ## create new D1Client object and insert uri endpoint + ## create new CNode object and insert uri endpoint result <- new("CNode") # Get the node listing for just this CN using just the baseURL, as we don't know the API version number # yet that is needed to construct the service URL. - response <- GET(CN_URI) + nconfig <- httr::config() + if (is_windows()) { + # On windows, TLS 1.3 is not supported, so we need to force TLS 1.2 + nconfig <- c(nconfig, config(sslversion = 6L) ) # 6L corresponds to CURL_SSLVERSION_TLSv1_2 + } + response <- GET(CN_URI, config = nconfig) if(response$status_code != "200") { stop(sprintf("Error accessing %s: %s\n", CN_URI, getErrorDescription(response))) } @@ -161,7 +166,7 @@ setGeneric("listFormats", function(x, ...) { #' @export setMethod("listFormats", signature("CNode"), function(x) { url <- paste(x@endpoint,"formats",sep="/") - response <- GET(url, user_agent(get_user_agent())) + response <- auth_get(url, nconfig = user_agent(get_user_agent()), node=x) # Use charset 'utf-8' if not specified in response headers charset <- "utf-8" if("content-type" %in% names(response$headers)) { @@ -225,7 +230,7 @@ setGeneric("getFormat", function(x, ...) { #' @export setMethod("getFormat", signature("CNode"), function(x, formatId) { url <- paste(x@endpoint,"formats", URLencode(formatId, reserved=T), sep="/") - response <- GET(url, user_agent(get_user_agent())) + response <- auth_get(url, nconfig = user_agent(get_user_agent()), node=x) if(response$status_code != "200") { return(NULL) @@ -250,7 +255,7 @@ setMethod("getFormat", signature("CNode"), function(x, formatId) { #' } setMethod("getChecksum", signature("CNode"), function(x, pid, ...) { url <- paste(x@endpoint, "checksum", URLencode(pid, reserved=T), sep="/") - response <- GET(url, user_agent(get_user_agent())) + response <- auth_get(url, nconfig = user_agent(get_user_agent()), node=x) if (is.raw(response$content)) { tmpres <- content(response, as="raw") resultText <- rawToChar(tmpres) @@ -295,8 +300,7 @@ setMethod("listNodes", signature("CNode"), function(x, url=as.character(NA), ... if(is.na(url)) { url <- paste(x@endpoint, "node", sep="/") } - # Don't need authorized access, so call GET directly vs auth_get - response <- GET(url) + response <- auth_get(url, nconfig = user_agent(get_user_agent()), node=x) if(response$status_code != "200") { return(NULL) } diff --git a/R/D1Client.R b/R/D1Client.R index dbb7840e..8faa79dc 100644 --- a/R/D1Client.R +++ b/R/D1Client.R @@ -223,7 +223,6 @@ setGeneric("getD1Object", function(x, identifier, ...) { #' @rdname getD1Object setMethod("getD1Object", "D1Client", function(x, identifier) { - #d1o <- get(x@cn, identifier) # Resolve the object location return(getDataObject(x, identifier)) }) diff --git a/R/D1Node.R b/R/D1Node.R index 37b4ca49..943c99d6 100644 --- a/R/D1Node.R +++ b/R/D1Node.R @@ -267,7 +267,7 @@ setMethod("getQueryEngineDescription", signature("D1Node"), function(x, queryEng url <- paste(x@endpoint, "query", queryEngineName, sep="/") # Send the request - response<-GET(url) + response <- auth_get(url, node=x) if(response$status_code != "200") { warning(sprintf("Error getting query engine description %s\n", getErrorDescription(response))) return(list()) @@ -439,7 +439,12 @@ setMethod("listObjects", signature("D1Node"), function(x, url <- paste(x@endpoint, "object", sep="/") # Send the request - response<-GET(url, query=params) + nconfig <- httr::config() + if (is_windows()) { + # On windows, TLS 1.3 is not supported, so we need to force TLS 1.2 + nconfig <- c(nconfig, config(sslversion = 6L) ) # 6L corresponds to CURL_SSLVERSION_TLSv1_2 + } + response<-GET(url, config = nconfig, query=params) if (is.raw(response$content)) { tmpres <- content(response, as="raw") resultText <- rawToChar(tmpres) @@ -480,7 +485,7 @@ setMethod("listQueryEngines", signature("D1Node"), function(x) { url <- paste(x@endpoint, "query", sep="/") # Send the request - response<-GET(url) + response <- auth_get(url, node=x) if (is.raw(response$content)) { tmpres <- content(response, as="raw") resultText <- rawToChar(tmpres) @@ -582,7 +587,7 @@ setMethod("ping", signature("D1Node"), function(x) { url <- paste(x@endpoint, "monitor/ping", sep="/") # Send the request - response<-GET(url) + response <- auth_get(url, node=x) if (response$status_code == 200) { return(TRUE) diff --git a/R/MNode.R b/R/MNode.R index fa7a824a..e81e5c9d 100644 --- a/R/MNode.R +++ b/R/MNode.R @@ -201,7 +201,12 @@ setMethod("getCapabilities", signature("MNode"), function(x) { url <- paste(x@endpoint, "node", sep="/") # Don't need privileged access, so call GET directly vs auth_get - response <- GET(url, user_agent(get_user_agent())) + nconfig <- user_agent(get_user_agent()) + if (is_windows()) { + # On windows, TLS 1.3 is not supported, so we need to force TLS 1.2 + nconfig <- c(nconfig, config(sslversion = 6L) ) # 6L corresponds to CURL_SSLVERSION_TLSv1_2 + } + response <- GET(url, config = nconfig) # Use charset 'utf-8' if not specified in response headers charset <- "utf-8" if(response$status_code != "200") { diff --git a/R/auth_request.R b/R/auth_request.R index 1246825b..5c68c20d 100644 --- a/R/auth_request.R +++ b/R/auth_request.R @@ -43,6 +43,10 @@ auth_get <- function(url, nconfig=config(), node, path = NULL) { if (missing(url) || missing(node)) { stop("Error: url or node is missing. Please report this error.") } + if (is_windows()) { + # On windows, TLS 1.3 is not supported, so we need to force TLS 1.2 + nconfig <- c(nconfig, config(sslversion = 6L) ) # 6L corresponds to CURL_SSLVERSION_TLSv1_2 + } am <- AuthenticationManager() if(isAuthValid(am, node)) { if(getAuthMethod(am, node) == "token") { @@ -125,6 +129,10 @@ auth_head <- function(url, nconfig=config(), node) { if (missing(url) || missing(node)) { stop("Error: url or node is missing. Please report this error.") } + if (is_windows()) { + # On windows, TLS 1.3 is not supported, so we need to force TLS 1.2 + nconfig <- c(nconfig, config(sslversion = 6L) ) # 6L corresponds to CURL_SSLVERSION_TLSv1_2 + } am <- AuthenticationManager() if(isAuthValid(am, node)) { if(getAuthMethod(am, node) == "token") { @@ -159,7 +167,11 @@ auth_head <- function(url, nconfig=config(), node) { #' @return the response object from the method #' @import httr auth_put_post_delete <- function(method, url, encode="multipart", body=NULL, node) { - + nconfig <- httr::config(user_agent(get_user_agent())) + if (is_windows()) { + # On windows, TLS 1.3 is not supported, so we need to force TLS 1.2 + nconfig <- c(config(sslversion = 6L) ) # 6L corresponds to CURL_SSLVERSION_TLSv1_2 + } am <- AuthenticationManager() if(!missing(node) && isAuthValid(am, node)) { if(getAuthMethod(am, node) == "token") { @@ -167,15 +179,15 @@ auth_put_post_delete <- function(method, url, encode="multipart", body=NULL, nod authToken <- getToken(am, node) switch(method, post={ - response=POST(url, encode=encode, body=body, config(tcp_keepalive = as.numeric(1)), add_headers(Authorization = sprintf("Bearer %s", authToken)), user_agent(get_user_agent())) + response=POST(url, encode=encode, body=body, nconfig, config(tcp_keepalive = as.numeric(1)), add_headers(Authorization = sprintf("Bearer %s", authToken))) return(response) }, put={ - response=PUT(url, encode=encode, body=body, add_headers(Authorization = sprintf("Bearer %s", authToken)), user_agent(get_user_agent())) + response=PUT(url, encode=encode, body=body, nconfig, add_headers(Authorization = sprintf("Bearer %s", authToken))) return(response) }, delete={ - response=DELETE(url, encode=encode, body=body, add_headers(Authorization = sprintf("Bearer %s", authToken)), user_agent(get_user_agent())) + response=DELETE(url, encode=encode, body=body, nconfig, add_headers(Authorization = sprintf("Bearer %s", authToken))) return(response)}, { stop('Method not supported.') @@ -186,15 +198,15 @@ auth_put_post_delete <- function(method, url, encode="multipart", body=NULL, nod cert <- getCert(am) switch(method, post={ - response=POST(url, encode=encode, body=body, config=config(sslcert = cert), user_agent(get_user_agent())) + response=POST(url, encode=encode, body=body, nconfig, config=config(sslcert = cert)) return(response) }, put={ - response=PUT(url, encode=encode, body=body, config=config(sslcert = cert), user_agent(get_user_agent())) + response=PUT(url, encode=encode, body=body, nconfig, config=config(sslcert = cert)) return(response) }, delete={ - response=DELETE(url, encode=encode, body=body, config=config(sslcert = cert), user_agent(get_user_agent())) + response=DELETE(url, encode=encode, body=body, nconfig, config=config(sslcert = cert)) return(response)}, { stop('Method not supported.') @@ -277,4 +289,11 @@ get_user_agent <- function() { paste(info$R.version$major, info$R.version$minor, sep="."), httrVersion) return(local_agent) +} + +#' Is the OS Windows? +#' @description Check if the current operating system is Windows. +#' @return TRUE if the OS is Windows, FALSE otherwise. +is_windows <- function() { + return(.Platform$OS.type == "windows") } \ No newline at end of file diff --git a/README.md b/README.md index f2638226..47221ac3 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ - [Package source code on Github](https://github.com/DataONEorg/rdataone) - [**Submit Bugs and feature requests**](https://github.com/DataONEorg/rdataone/issues) -Provides read and write access to data and metadata from the [DataONE network +Provides read and write access to data and metadata from the global [DataONE network of data repositories](https://www.dataone.org/network/), including the [KNB Data Repository](https://knb.ecoinformatics.org), [Dryad](https://datadryad.org/), and the NSF [Arctic Data Center](https://arcticdata.io) and dozens of other @@ -73,6 +73,38 @@ library(dataone) The *dataone* R package should be available for use at this point. +### Windows issues with TLS 1.3 + +Some users report problems with curl on Windows recently failing to connect when using TLS 1.3 connections, with the following error: + +``` +> library(dataone) +> cn <- CNode("PROD") +Error in curl::curl_fetch_memory(url, handle = handle) : + Failure when receiving data from the peer [cn.dataone.org]: +schannel: failed to read data from server: SEC_E_CONTEXT_EXPIRED (0x80090317) - The context has expired and can no longer be used. +``` + +This seems to be associated with changes to the Schannel SSL backend on Windows in how it handles requests for client-side x509 certificates under TLS1.3, which no longer functions properly under Windows. Switching to using the OpenSSL backend (as described in [issue #308](https://github.com/DataONEorg/rdataone/issues/308#issue-3471229735)) seems to fix the problem but is probably not a great long-term solution. Here's a workaround by setting an environment variable to tell curl to use the OpenSSL backend: + +``` +write('CURL_SSL_BACKEND=openssl', file = "~/.Renviron", append = TRUE) +``` +Restart the R session and verify that OpenSSL is now active (no longer in parenthesis): + +``` +> curl::curl_version()$ssl_version +Initiating curl with CURL_SSL_BACKEND: openssl +[1] "OpenSSL/3.5.0 (Schannel)" +``` + +Now, try the code again and it should work: + +``` +> library(dataone) +> cn <- CNode("PROD") +``` + ## Quick Start See the full manual (`help(dataone)`) for documentation. diff --git a/cran-comments.md b/cran-comments.md index e8c1c86e..eee356ef 100644 --- a/cran-comments.md +++ b/cran-comments.md @@ -1,31 +1,17 @@ -## Test environments +Dear CRAN maintainers, - * macOS 13.7.6: R 4.5.1 - - * Ubuntu 18.04 R 4.1.3 - * Windows (via win-builder): x86_64-w64-mingw32 (64-bit) R Under development (unstable) (2022-06-08 r82470 ucrt) - * Windows (via win-builder): x86_64-w64-mingw32 (64-bit) R 4.2.0 (2022-04-22 ucrt) - * Windows (via win-builder): x86_64-w64-mingw32 (64-bit) R 4.1.3 (2022-03-10) - * rhub::check_for_cran() - * Windows Server 2008 R2 SP1, R-release, 32/64 bit - * Windows Server 2022, R-devel, 64 bit - * Fedora Linux, R-devel, clang, gfortran - * Fedora Linux, R-devel, GCC - * macOS 10.13.6 High Sierra, R-release, CRAN's setup - * Apple Silicon (M1), macOS 11.6 Big Sur, R-release +Please find a new release of the R package 'dataone' for your consideration. This package was recently archived because I had been unable to fix issues with TLS 1.3 support on Windows clients. I have temporarily disabled TLS 1.3 support and fallback to TLS 1.2 in the underlying 'curl' package on windows clients, which has resolved the issue. I will continue to investigate a more permanent solution for future releases, but it seems that Windows clients are not yet fully compatible with TLS 1.3 (see https://windowsforum.com/threads/tls-1-3-iis-express-on-windows-11-mtls-breakage-workarounds-and-outlook.379408/). -## Changes since last release +## Test environments -* Remove hash dependency (#293) -* Add support for new method signature for D1Client (#252) -* Ensure rightsHolder persists when uploading a data package (#292) -* Fix bug where 'publc = TRUE' argument did not set public read on all objects (#285) -* Account for edge case errors in 'archive()' (#236) +* Windows (via win-builder): x86_64-w64-mingw32 (64-bit) R version 4.5.2 (2025-10-31 ucrt) +* Windows (via win-builder): x86_64-w64-mingw32 (64-bit) R Under development (unstable) (2025-12-04 r89100 ucrt) +* macOS Sequoia 15.7.2: aarch64-apple-darwin20 R version 4.5.2 (2025-10-31) +* macOS Ventura 13.7.6: aarch64-apple-darwin20 R version 4.5.2 (2025-10-31) +* Ubuntu 24.04.3 LTS: x86_64-pc-linux-gnu R version 4.5.2 (2025-10-31) +* Ubuntu 24.04.3 LTS: x86_64-pc-linux-gnu R version 4.4.3 (2025-02-28) +* Ubuntu 24.04.3 LTS: x86_64-pc-linux-gnu R Under development (unstable) (2025-12-02 r89085) ## R CMD check results -* There were no NOTEs, ERRORs, or WARNINGs. - -## Downstream dependencies - -* The downstream dependencies have been checked with revdepcheck::revdep_check(), without any problems being reported. \ No newline at end of file +* There were no NOTEs, ERRORs, or WARNINGs except for those related to this being a new package submission after archive, and a false positive on a spelling check for a proper noun. \ No newline at end of file diff --git a/inst/WORDLIST b/inst/WORDLIST index f6cbb895..0959a51e 100644 --- a/inst/WORDLIST +++ b/inst/WORDLIST @@ -1,68 +1,111 @@ -aaa -aaddc ABI -accessPolicy AccessPolicy Acknowledgements Allard -asDataFrame AuthenticationManager Bagit -baseURL Biocomplexity Budden -cdf -CertficateManager -CertificateManager -changePermission -CharacterEncoding -cilogon CILogon -CiTO -cn +CMD CN CNCore CNode CNs -config +CertficateManager +CertificateManager +CharacterEncoding +CiTO +DATANET +DDTHH +DEV +DIBBS +DataFrame +DataONE +DataObject +DataObjects +DataPackage +EML +EMLParser +FGDC +FormatId +GSI +Github +Globus +Gries +Habermann +HomeBrew +Http +JWT +KNB +Langauge +MNStorage +MNode +MNs +MacPorts +MetacatUI +Michener +NCEAS +ORCID +PID +PLR +POSIXct +Parsers +RCurl +Redland +ResourceMap +Rprofile +SHA +SIDs +SSL +Schildhauer +Solr +SystemMetada +SystemMetadata +SytemMetadata +THe +TableDescriber +UUID +Vieglais +XMLInternalDocument +XPath +YYYY +Yeboah +aaa +aaddc +accessPolicy +asDataFrame +bagit +baseURL +cdf +changePermission +cilogon +cn createObject csv data's dataFrame -DataFrame -DATANET -DataObject -DataObjects dataONE -DataONE datapack datapackage -DataPackage datasource +datatracker dateUploaded -DDTHH dev -DEV -DIBBS dirPath discoverable doi -DOI ecoinformatics eml -EML -EMLParser env -FGDC fileName filesystem -formatId formatID -FormatId +formatId formatIdentifier generateIdentifier -getCapabilities getCN +getCapabilities getD getData getDataObject @@ -75,102 +118,55 @@ getOption getPackage getQueryEngineDescription getSystemMetadata -Github -Globus -Gries -GSI -Habermann hasReservation -HomeBrew http -Http https httr +ietf implementers -ITK json knb -KNB -Langauge +kunze lazyLoad -lazyloading libcurl listFormats -listNodes lucene -MacPorts -Michener mmm mn -MNode -MNs -MNStorage nceas -NCEAS obsoletedBy obsoletedByPid openssl -ORCID ouput packageId params -Parsers pid -PID -PLR -POSIXct pre -programmatically proxied -queryEngineDescription -RCurl +rJava redland -Redland reserveIdentifier resourceMap -ResourceMap resultList -rightsholder rightsHolder -rJava +rightsholder rnahf ropensci -Rprofile -Schildhauer searchTerm searchTerms seriesId setMNodeId -SHA sid -SIDs solr -Solr solrQuery -SSL -Subclasses submitters superceded -sysmeta -SystemMetada systemmetadata -SystemMetadata -SytemMetadata tableDescriber -TableDescriber -THe ubuntu un unicode updateObject updateSystemMetadata uploadDataPackage -URI uuid -UUID -Vieglais -www -XMLInternalDocument -XPath -Yeboah -YYYY diff --git a/man/is_windows.Rd b/man/is_windows.Rd new file mode 100644 index 00000000..b9eebde1 --- /dev/null +++ b/man/is_windows.Rd @@ -0,0 +1,14 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/auth_request.R +\name{is_windows} +\alias{is_windows} +\title{Is the OS Windows?} +\usage{ +is_windows() +} +\value{ +TRUE if the OS is Windows, FALSE otherwise. +} +\description{ +Check if the current operating system is Windows. +} diff --git a/tests/testthat/helper-base.R b/tests/testthat/helper-base.R index 4c927e77..0aa243fb 100644 --- a/tests/testthat/helper-base.R +++ b/tests/testthat/helper-base.R @@ -36,4 +36,12 @@ if(is.null(mnKNB)) servicesDown <- TRUE if(is.null(d1cProd)) servicesDown <- TRUE if(is.null(d1cTestKNB)) servicesDown <- TRUE if(is.null(d1cTest)) servicesDown <- TRUE -if(is.null(mnTest)) servicesDown <- TRUE \ No newline at end of file +if(is.null(mnTest)) servicesDown <- TRUE + +skip_if_down <- function() { + if (servicesDown) { + skip("Not run when services are down.") + } else { + invisible() + } +} diff --git a/tests/testthat/test.AuthenticationManager.R b/tests/testthat/test.AuthenticationManager.R index 21713874..0625d997 100644 --- a/tests/testthat/test.AuthenticationManager.R +++ b/tests/testthat/test.AuthenticationManager.R @@ -1,5 +1,6 @@ test_that("AuthenticationManager isAuthValid() for v2 node works", { skip_on_cran() + skip_if_down() library(dataone) am <- AuthenticationManager() expect_false(is.null(am)) @@ -18,6 +19,7 @@ test_that("AuthenticationManager isAuthValid() for v2 node works", { test_that("AuthenticationManager getAuthMethod(), getToken(), getCert() work", { skip_on_cran() + skip_if_down() am <- AuthenticationManager() expect_false(is.null(cm)) expect_false(is.null(am)) @@ -39,6 +41,7 @@ test_that("AuthenticationManager getAuthMethod(), getToken(), getCert() work", { test_that("getAuthExpires() works", { skip_on_cran() + skip_if_down() am <- AuthenticationManager() expect_false(is.null(cm)) expect_false(is.null(am)) @@ -61,10 +64,11 @@ test_that("getAuthExpires() works", { test_that("isCertExpired() works", { skip_on_cran() + skip_if_down() am <- AuthenticationManager() cn <- CNode("STAGING") # Suppress openssl, cert missing warnings - suppressMessages(authValid <- dataone:::isAuthValid(am, cn)) + authValid <- dataone:::isAuthValid(am, cn) if(authValid) { expDate <- dataone:::getAuthExpires(am, cn) ct <- as.POSIXct(Sys.time(), tz=format(expDate, "%Z")) @@ -80,6 +84,7 @@ test_that("isCertExpired() works", { test_that("getAuthSubject() works", { skip_on_cran() + skip_if_down() am <- AuthenticationManager() cn <- CNode("STAGING") # Suppress openssl, cert missing warnings @@ -96,6 +101,7 @@ test_that("getAuthSubject() works", { test_that("obscureAuth(), restoreAuth() work", { skip_on_cran() + skip_if_down() am <- AuthenticationManager() cn <- CNode("STAGING") # Disable authentication diff --git a/tests/testthat/test.CNode.R b/tests/testthat/test.CNode.R index af0c7b32..c1d1d068 100644 --- a/tests/testthat/test.CNode.R +++ b/tests/testthat/test.CNode.R @@ -4,7 +4,7 @@ test_that("dataone library loads", { test_that("CNode constructors", { skip_on_cran() library(dataone) - # If not specified, "PROD" environment is used. + expect_true(!is.null(cnProd)) expect_match(cnProd@endpoint, "https://cn.dataone.org/cn") expect_match(cnProd@endpoint, "https://cn.dataone.org/cn") # Skip unstable test environments. @@ -34,7 +34,6 @@ test_that("CNode getObject()", { pid <- "aceasdata.3.2" obj <- getObject(cnProd, pid) if(is.null(obj) || class(obj) != "raw") { - skip_on_cra() skip(sprintf("Unable to retrieve pid %s from production CN, skipping test\n", pid)) } xml <- xmlParseDoc(rawToChar(obj), asText=TRUE)