From 69d3bc9274f06e1079f3741e4c4745e9e65e3ea2 Mon Sep 17 00:00:00 2001 From: Winston Chang Date: Tue, 9 Oct 2018 14:33:08 -0500 Subject: [PATCH 01/60] Basic static file serving on background thread --- R/httpuv.R | 36 ++++++++- R/utils.R | 20 +++++ src/filedatasource-unix.cpp | 19 ++++- src/filedatasource-win.cpp | 10 ++- src/filedatasource.h | 2 + src/httprequest.cpp | 12 +++ src/httpuv.cpp | 12 ++- src/webapplication.cpp | 154 +++++++++++++++++++++++++++++++++++- src/webapplication.h | 24 ++++-- 9 files changed, 270 insertions(+), 19 deletions(-) diff --git a/R/httpuv.R b/R/httpuv.R index a7eb8bba..627b7789 100644 --- a/R/httpuv.R +++ b/R/httpuv.R @@ -248,6 +248,39 @@ AppWrapper <- setRefClass( for (handler in ws$.closeCallbacks) { handler() } + }, + getStaticPaths = function() { + # This method always returns a named character vector. + # .app$staticPaths must be NULL, a named list (of strings), or a named + # character vector. + + # If .app is a reference class, accessing .app$staticPaths can error if + # not present. + if (class(try(.app$staticPaths, silent = TRUE)) == "try-error") { + return(empty_named_vec()) + } + + paths <- .app$staticPaths + + if (is.null(paths) || length(paths) == 0) { + return(empty_named_vec()) + } + + if (any_unnamed(paths)) { + stop(".app$staticPaths must be a named character vector, NULL, a or named list (of strings).") + } + + # If list, convert to vector. + if (is.list(paths)) { + paths <- unlist(paths, recursive = FALSE) + } + + # Make sure it's a named character vector. + if (is.character(paths)) { + return(paths) + } + + stop(".app$staticPaths must be a named character vector, NULL, a or named list (of strings).") } ) ) @@ -440,7 +473,8 @@ startServer <- function(host, port, app) { appWrapper$call, appWrapper$onWSOpen, appWrapper$onWSMessage, - appWrapper$onWSClose + appWrapper$onWSClose, + appWrapper$getStaticPaths ) if (is.null(server)) { diff --git a/R/utils.R b/R/utils.R index de470c45..f6073557 100644 --- a/R/utils.R +++ b/R/utils.R @@ -12,3 +12,23 @@ httpuv_version <- local({ version } }) + + +# Return a zero-element named character vector +empty_named_vec <- function() { + c(a = "")[0] +} + +# Given a vector/list, return TRUE if any elements are unnamed, FALSE otherwise. +any_unnamed <- function(x) { + # Zero-length vector + if (length(x) == 0) return(FALSE) + + nms <- names(x) + + # List with no name attribute + if (is.null(nms)) return(TRUE) + + # List with name attribute; check for any "" + any(!nzchar(nms)) +} diff --git a/src/filedatasource-unix.cpp b/src/filedatasource-unix.cpp index 4d4d7a68..80eeaa47 100644 --- a/src/filedatasource-unix.cpp +++ b/src/filedatasource-unix.cpp @@ -7,24 +7,30 @@ #include int FileDataSource::initialize(const std::string& path, bool owned) { - ASSERT_MAIN_THREAD() + // This can be called from either the main thread or background thread. _fd = open(path.c_str(), O_RDONLY); if (_fd == -1) { - REprintf("Error opening file: %d\n", errno); + std::ostringstream ss; + ss << "Error opening file: " << errno << "\n"; + _lastErrorMessage = ss.str(); return 1; } else { struct stat info = {0}; if (fstat(_fd, &info)) { - REprintf("Error opening path: %d\n", errno); + std::ostringstream ss; + ss << "Error opening path: " << errno << "\n"; + _lastErrorMessage = ss.str(); ::close(_fd); return 1; } _length = info.st_size; if (owned && unlink(path.c_str())) { - REprintf("Couldn't delete temp file: %d\n", errno); + // Print this (on either main or background thread), since we're not + // returning 1 to indicate an error. + err_printf("Couldn't delete temp file: %d\n", errno); // It's OK to continue } @@ -66,4 +72,9 @@ void FileDataSource::close() { _fd = -1; } +std::string FileDataSource::lastErrorMessage() const { + return _lastErrorMessage; +} + + #endif // #ifndef _WIN32 diff --git a/src/filedatasource-win.cpp b/src/filedatasource-win.cpp index cf174295..2270c1d9 100644 --- a/src/filedatasource-win.cpp +++ b/src/filedatasource-win.cpp @@ -9,7 +9,7 @@ // using the POSIX file functions. int FileDataSource::initialize(const std::string& path, bool owned) { - ASSERT_MAIN_THREAD() + // This can be called from either the main thread or background thread. DWORD flags = FILE_FLAG_SEQUENTIAL_SCAN; if (owned) @@ -24,13 +24,17 @@ int FileDataSource::initialize(const std::string& path, bool owned) { NULL); if (_hFile == INVALID_HANDLE_VALUE) { - REprintf("Error opening file: %d\n", GetLastError()); + std::ostringstream ss; + ss << "Error opening file: " << GetLastError() << "\n"; + _lastErrorMessage = ss.str(); return 1; } if (!GetFileSizeEx(_hFile, &_length)) { CloseHandle(_hFile); - REprintf("Error retrieving file size: %d\n", GetLastError()); + std::ostringstream ss; + ss << "Error retrieving file size: " << GetLastError() << "\n"; + _lastErrorMessage = ss.str(); return 1; } diff --git a/src/filedatasource.h b/src/filedatasource.h index 3849ea86..93f038a9 100644 --- a/src/filedatasource.h +++ b/src/filedatasource.h @@ -11,6 +11,7 @@ class FileDataSource : public DataSource { int _fd; off_t _length; #endif + std::string _lastErrorMessage = ""; public: FileDataSource() {} @@ -20,6 +21,7 @@ class FileDataSource : public DataSource { uv_buf_t getData(size_t bytesDesired); void freeData(uv_buf_t buffer); void close(); + std::string lastErrorMessage() const; }; #endif // FILEDATASOURCE_H diff --git a/src/httprequest.cpp b/src/httprequest.cpp index 6b8069cc..742277be 100644 --- a/src/httprequest.cpp +++ b/src/httprequest.cpp @@ -288,6 +288,18 @@ int HttpRequest::_on_headers_complete(http_parser* pParser) { trace("HttpRequest::_on_headers_complete"); updateUpgradeStatus(); + // Attempt static serving here + if (_pWebApplication->isStaticPath(_url)) { + boost::shared_ptr pResponse = + _pWebApplication->staticFileResponse(shared_from_this(), _url); + + // TODO: Should this be called asynchronously? + // Skip over the webapplication code, which calls back into R on the main thread. + _on_headers_complete_complete(pResponse); + return 0; + } + + boost::function)> schedule_bg_callback( boost::bind(&HttpRequest::_schedule_on_headers_complete_complete, shared_from_this(), _1) ); diff --git a/src/httpuv.cpp b/src/httpuv.cpp index 4fd3b65e..7add4f26 100644 --- a/src/httpuv.cpp +++ b/src/httpuv.cpp @@ -239,7 +239,8 @@ Rcpp::RObject makeTcpServer(const std::string& host, int port, Rcpp::Function onRequest, Rcpp::Function onWSOpen, Rcpp::Function onWSMessage, - Rcpp::Function onWSClose) { + Rcpp::Function onWSClose, + Rcpp::Function getStaticPaths) { using namespace Rcpp; register_main_thread(); @@ -248,7 +249,8 @@ Rcpp::RObject makeTcpServer(const std::string& host, int port, // this should be deleted when it goes out of scope. boost::shared_ptr pHandler( new RWebApplication(onHeaders, onBodyData, onRequest, - onWSOpen, onWSMessage, onWSClose), + onWSOpen, onWSMessage, onWSClose, + getStaticPaths), auto_deleter_main ); @@ -292,7 +294,8 @@ Rcpp::RObject makePipeServer(const std::string& name, Rcpp::Function onRequest, Rcpp::Function onWSOpen, Rcpp::Function onWSMessage, - Rcpp::Function onWSClose) { + Rcpp::Function onWSClose, + Rcpp::Function getStaticPaths) { using namespace Rcpp; register_main_thread(); @@ -301,7 +304,8 @@ Rcpp::RObject makePipeServer(const std::string& name, // this should be deleted when it goes out of scope. boost::shared_ptr pHandler( new RWebApplication(onHeaders, onBodyData, onRequest, - onWSOpen, onWSMessage, onWSClose), + onWSOpen, onWSMessage, onWSClose, + getStaticPaths), auto_deleter_main ); diff --git a/src/webapplication.cpp b/src/webapplication.cpp index b6711060..b8656d94 100644 --- a/src/webapplication.cpp +++ b/src/webapplication.cpp @@ -8,6 +8,32 @@ #include "utils.h" #include +std::map toStringMap(Rcpp::CharacterVector x) { + ASSERT_MAIN_THREAD() + + std::map strmap; + + if (x.size() == 0) { + return strmap; + } + + Rcpp::CharacterVector names = x.names(); + if (names.isNULL()) { + throw Rcpp::exception("Error converting CharacterVector to map: vector does not have names."); + } + + + for (int i=0; i(names[i]); + std::string value = Rcpp::as(x[i]); + strmap.insert( + std::pair(name, value) + ); + } + + return strmap; +} + std::string normalizeHeaderName(const std::string& name) { std::string result = name; for (std::string::iterator it = result.begin(); @@ -173,8 +199,11 @@ boost::shared_ptr listToResponse( // - body: Character vector (which is charToRaw-ed) or raw vector, or NULL if (std::find(names.begin(), names.end(), "bodyFile") != names.end()) { FileDataSource* pFDS = new FileDataSource(); - pFDS->initialize(Rcpp::as(response["bodyFile"]), - Rcpp::as(response["bodyFileOwned"])); + int ret = pFDS->initialize(Rcpp::as(response["bodyFile"]), + Rcpp::as(response["bodyFileOwned"])); + if (ret != 0) { + REprintf(pFDS->lastErrorMessage().c_str()); + } pDataSource = pFDS; } else if (Rf_isString(response["body"])) { @@ -211,6 +240,23 @@ void invokeResponseFun(boost::function)> fu fun(pResponse); } +RWebApplication::RWebApplication( + Rcpp::Function onHeaders, + Rcpp::Function onBodyData, + Rcpp::Function onRequest, + Rcpp::Function onWSOpen, + Rcpp::Function onWSMessage, + Rcpp::Function onWSClose, + Rcpp::Function getStaticPaths) : + _onHeaders(onHeaders), _onBodyData(onBodyData), _onRequest(onRequest), + _onWSOpen(onWSOpen), _onWSMessage(onWSMessage), _onWSClose(onWSClose), + _getStaticPaths(getStaticPaths) +{ + ASSERT_MAIN_THREAD() + + _staticPaths = toStringMap(Rcpp::CharacterVector(_getStaticPaths())); +} + void RWebApplication::onHeaders(boost::shared_ptr pRequest, boost::function)> callback) @@ -364,3 +410,107 @@ void RWebApplication::onWSClose(boost::shared_ptr pConn) { ASSERT_MAIN_THREAD() _onWSClose(externalize_shared_ptr(pConn)); } + + +// ============================================================================ +// Static file serving +// ============================================================================ +// +// Unlike most of the methods for an RWebApplication, these ones are called on +// the background thread. + +bool RWebApplication::isStaticPath(const std::string& url_path) { + ASSERT_BACKGROUND_THREAD() + + // Strip off leading '/' if present + size_t start_idx = 0; + if (url_path.at(0) == '/') { + start_idx = 1; + } + + // Trim off last part of path + size_t found_idx = url_path.find_last_of('/'); + + if (found_idx <= 0) { + return false; + } + + std::string dirname = url_path.substr(start_idx, found_idx - 1); + std::string filename = url_path.substr(found_idx + 1); + + std::map::iterator it = _staticPaths.find(dirname); + if (it != _staticPaths.end()) { + return true; + } else { + return false; + } +} + + +boost::shared_ptr response_404(boost::shared_ptr pRequest) { + std::string content = "404 file not found"; + std::vector responseData(content.begin(), content.end()); + + // Freed in on_response_written + DataSource* pDataSource = new InMemoryDataSource(responseData); + + return boost::shared_ptr( + new HttpResponse(pRequest, 404, getStatusDescription(404), pDataSource), + auto_deleter_background + ); +} + + +boost::shared_ptr RWebApplication::staticFileResponse( + boost::shared_ptr pRequest, + const std::string& url_path) +{ + ASSERT_BACKGROUND_THREAD() + + // Strip off leading '/' if present + size_t start_idx = 0; + if (url_path.at(0) == '/') { + start_idx = 1; + } + + // Get dirname and filename from url + size_t found_idx = url_path.find_last_of('/'); + if (found_idx <= 0) { + return response_404(pRequest); + } + + std::string url_dirname = url_path.substr(start_idx, found_idx - 1); + std::string filename = url_path.substr(found_idx + 1); + + std::string local_dirname = ""; + + std::map::iterator it = _staticPaths.find(url_dirname); + if (it != _staticPaths.end()) { + local_dirname = it->second; + } + + if (local_dirname == "") { + // Typically shouldn't get here, since user should have checked for + // existence using isStaticPath(). + return response_404(pRequest); + } + + std::string local_path = local_dirname + "/" + filename; + + // Self-frees when response is written + FileDataSource* pDataSource = new FileDataSource(); + // TODO: Figure out how to deal with `owned` parameter. + int ret = pDataSource->initialize(local_path, false); + + if (ret != 0) { + // Couldn't read the file + delete pDataSource; + return response_404(pRequest); + } + + return boost::shared_ptr( + new HttpResponse(pRequest, 200, getStatusDescription(200), pDataSource), + auto_deleter_background + ); + +} diff --git a/src/webapplication.h b/src/webapplication.h index b45b0bc4..f0e608be 100644 --- a/src/webapplication.h +++ b/src/webapplication.h @@ -27,6 +27,11 @@ class WebApplication { boost::shared_ptr > data, boost::function error_callback) = 0; virtual void onWSClose(boost::shared_ptr) = 0; + virtual bool isStaticPath(const std::string& url_path) = 0; + virtual boost::shared_ptr staticFileResponse( + boost::shared_ptr pRequest, + const std::string& url_path + ) = 0; }; @@ -38,6 +43,12 @@ class RWebApplication : public WebApplication { Rcpp::Function _onWSOpen; Rcpp::Function _onWSMessage; Rcpp::Function _onWSClose; + Rcpp::Function _getStaticPaths; + + // TODO: need to be careful with this - it's created and destroyed on the + // main thread, but when it is accessed, it is always on the background thread. + // Probably need to lock at those times. + std::map _staticPaths; public: RWebApplication(Rcpp::Function onHeaders, @@ -45,11 +56,8 @@ class RWebApplication : public WebApplication { Rcpp::Function onRequest, Rcpp::Function onWSOpen, Rcpp::Function onWSMessage, - Rcpp::Function onWSClose) : - _onHeaders(onHeaders), _onBodyData(onBodyData), _onRequest(onRequest), - _onWSOpen(onWSOpen), _onWSMessage(onWSMessage), _onWSClose(onWSClose) { - ASSERT_MAIN_THREAD() - } + Rcpp::Function onWSClose, + Rcpp::Function getStaticPaths); virtual ~RWebApplication() { ASSERT_MAIN_THREAD() @@ -69,6 +77,12 @@ class RWebApplication : public WebApplication { boost::shared_ptr > data, boost::function error_callback); virtual void onWSClose(boost::shared_ptr conn); + + virtual bool isStaticPath(const std::string& url_path); + virtual boost::shared_ptr staticFileResponse( + boost::shared_ptr pRequest, + const std::string& url_path + ); }; From 826d1f0500fd0831939d4b73365efc24a2fbf570 Mon Sep 17 00:00:00 2001 From: Winston Chang Date: Tue, 9 Oct 2018 14:33:36 -0500 Subject: [PATCH 02/60] Re-document --- DESCRIPTION | 2 +- R/RcppExports.R | 8 ++++---- man/startServer.Rd | 1 - src/RcppExports.cpp | 20 +++++++++++--------- 4 files changed, 16 insertions(+), 15 deletions(-) diff --git a/DESCRIPTION b/DESCRIPTION index 997b511d..b4938362 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -26,7 +26,7 @@ Imports: LinkingTo: Rcpp, BH, later URL: https://github.com/rstudio/httpuv SystemRequirements: GNU make -RoxygenNote: 6.0.1.9000 +RoxygenNote: 6.1.0.9000 Suggests: testthat, callr diff --git a/R/RcppExports.R b/R/RcppExports.R index 4a552a35..21e07da3 100644 --- a/R/RcppExports.R +++ b/R/RcppExports.R @@ -9,12 +9,12 @@ closeWS <- function(conn, code, reason) { invisible(.Call('_httpuv_closeWS', PACKAGE = 'httpuv', conn, code, reason)) } -makeTcpServer <- function(host, port, onHeaders, onBodyData, onRequest, onWSOpen, onWSMessage, onWSClose) { - .Call('_httpuv_makeTcpServer', PACKAGE = 'httpuv', host, port, onHeaders, onBodyData, onRequest, onWSOpen, onWSMessage, onWSClose) +makeTcpServer <- function(host, port, onHeaders, onBodyData, onRequest, onWSOpen, onWSMessage, onWSClose, getStaticPaths) { + .Call('_httpuv_makeTcpServer', PACKAGE = 'httpuv', host, port, onHeaders, onBodyData, onRequest, onWSOpen, onWSMessage, onWSClose, getStaticPaths) } -makePipeServer <- function(name, mask, onHeaders, onBodyData, onRequest, onWSOpen, onWSMessage, onWSClose) { - .Call('_httpuv_makePipeServer', PACKAGE = 'httpuv', name, mask, onHeaders, onBodyData, onRequest, onWSOpen, onWSMessage, onWSClose) +makePipeServer <- function(name, mask, onHeaders, onBodyData, onRequest, onWSOpen, onWSMessage, onWSClose, getStaticPaths) { + .Call('_httpuv_makePipeServer', PACKAGE = 'httpuv', name, mask, onHeaders, onBodyData, onRequest, onWSOpen, onWSMessage, onWSClose, getStaticPaths) } #' Stop a server diff --git a/man/startServer.Rd b/man/startServer.Rd index 60a19886..53cc8493 100644 --- a/man/startServer.Rd +++ b/man/startServer.Rd @@ -3,7 +3,6 @@ \name{startServer} \alias{startServer} \alias{startPipeServer} -\alias{startPipeServer} \title{Create an HTTP/WebSocket server} \usage{ startServer(host, port, app) diff --git a/src/RcppExports.cpp b/src/RcppExports.cpp index 37ca355d..ffcfbb7f 100644 --- a/src/RcppExports.cpp +++ b/src/RcppExports.cpp @@ -30,8 +30,8 @@ BEGIN_RCPP END_RCPP } // makeTcpServer -Rcpp::RObject makeTcpServer(const std::string& host, int port, Rcpp::Function onHeaders, Rcpp::Function onBodyData, Rcpp::Function onRequest, Rcpp::Function onWSOpen, Rcpp::Function onWSMessage, Rcpp::Function onWSClose); -RcppExport SEXP _httpuv_makeTcpServer(SEXP hostSEXP, SEXP portSEXP, SEXP onHeadersSEXP, SEXP onBodyDataSEXP, SEXP onRequestSEXP, SEXP onWSOpenSEXP, SEXP onWSMessageSEXP, SEXP onWSCloseSEXP) { +Rcpp::RObject makeTcpServer(const std::string& host, int port, Rcpp::Function onHeaders, Rcpp::Function onBodyData, Rcpp::Function onRequest, Rcpp::Function onWSOpen, Rcpp::Function onWSMessage, Rcpp::Function onWSClose, Rcpp::Function getStaticPaths); +RcppExport SEXP _httpuv_makeTcpServer(SEXP hostSEXP, SEXP portSEXP, SEXP onHeadersSEXP, SEXP onBodyDataSEXP, SEXP onRequestSEXP, SEXP onWSOpenSEXP, SEXP onWSMessageSEXP, SEXP onWSCloseSEXP, SEXP getStaticPathsSEXP) { BEGIN_RCPP Rcpp::RObject rcpp_result_gen; Rcpp::RNGScope rcpp_rngScope_gen; @@ -43,13 +43,14 @@ BEGIN_RCPP Rcpp::traits::input_parameter< Rcpp::Function >::type onWSOpen(onWSOpenSEXP); Rcpp::traits::input_parameter< Rcpp::Function >::type onWSMessage(onWSMessageSEXP); Rcpp::traits::input_parameter< Rcpp::Function >::type onWSClose(onWSCloseSEXP); - rcpp_result_gen = Rcpp::wrap(makeTcpServer(host, port, onHeaders, onBodyData, onRequest, onWSOpen, onWSMessage, onWSClose)); + Rcpp::traits::input_parameter< Rcpp::Function >::type getStaticPaths(getStaticPathsSEXP); + rcpp_result_gen = Rcpp::wrap(makeTcpServer(host, port, onHeaders, onBodyData, onRequest, onWSOpen, onWSMessage, onWSClose, getStaticPaths)); return rcpp_result_gen; END_RCPP } // makePipeServer -Rcpp::RObject makePipeServer(const std::string& name, int mask, Rcpp::Function onHeaders, Rcpp::Function onBodyData, Rcpp::Function onRequest, Rcpp::Function onWSOpen, Rcpp::Function onWSMessage, Rcpp::Function onWSClose); -RcppExport SEXP _httpuv_makePipeServer(SEXP nameSEXP, SEXP maskSEXP, SEXP onHeadersSEXP, SEXP onBodyDataSEXP, SEXP onRequestSEXP, SEXP onWSOpenSEXP, SEXP onWSMessageSEXP, SEXP onWSCloseSEXP) { +Rcpp::RObject makePipeServer(const std::string& name, int mask, Rcpp::Function onHeaders, Rcpp::Function onBodyData, Rcpp::Function onRequest, Rcpp::Function onWSOpen, Rcpp::Function onWSMessage, Rcpp::Function onWSClose, Rcpp::Function getStaticPaths); +RcppExport SEXP _httpuv_makePipeServer(SEXP nameSEXP, SEXP maskSEXP, SEXP onHeadersSEXP, SEXP onBodyDataSEXP, SEXP onRequestSEXP, SEXP onWSOpenSEXP, SEXP onWSMessageSEXP, SEXP onWSCloseSEXP, SEXP getStaticPathsSEXP) { BEGIN_RCPP Rcpp::RObject rcpp_result_gen; Rcpp::RNGScope rcpp_rngScope_gen; @@ -61,7 +62,8 @@ BEGIN_RCPP Rcpp::traits::input_parameter< Rcpp::Function >::type onWSOpen(onWSOpenSEXP); Rcpp::traits::input_parameter< Rcpp::Function >::type onWSMessage(onWSMessageSEXP); Rcpp::traits::input_parameter< Rcpp::Function >::type onWSClose(onWSCloseSEXP); - rcpp_result_gen = Rcpp::wrap(makePipeServer(name, mask, onHeaders, onBodyData, onRequest, onWSOpen, onWSMessage, onWSClose)); + Rcpp::traits::input_parameter< Rcpp::Function >::type getStaticPaths(getStaticPathsSEXP); + rcpp_result_gen = Rcpp::wrap(makePipeServer(name, mask, onHeaders, onBodyData, onRequest, onWSOpen, onWSMessage, onWSClose, getStaticPaths)); return rcpp_result_gen; END_RCPP } @@ -187,8 +189,8 @@ RcppExport SEXP httpuv_decodeURIComponent(SEXP); static const R_CallMethodDef CallEntries[] = { {"_httpuv_sendWSMessage", (DL_FUNC) &_httpuv_sendWSMessage, 3}, {"_httpuv_closeWS", (DL_FUNC) &_httpuv_closeWS, 3}, - {"_httpuv_makeTcpServer", (DL_FUNC) &_httpuv_makeTcpServer, 8}, - {"_httpuv_makePipeServer", (DL_FUNC) &_httpuv_makePipeServer, 8}, + {"_httpuv_makeTcpServer", (DL_FUNC) &_httpuv_makeTcpServer, 9}, + {"_httpuv_makePipeServer", (DL_FUNC) &_httpuv_makePipeServer, 9}, {"_httpuv_stopServer", (DL_FUNC) &_httpuv_stopServer, 1}, {"_httpuv_stopAllServers", (DL_FUNC) &_httpuv_stopAllServers, 0}, {"_httpuv_base64encode", (DL_FUNC) &_httpuv_base64encode, 1}, @@ -200,7 +202,7 @@ static const R_CallMethodDef CallEntries[] = { {"_httpuv_invokeCppCallback", (DL_FUNC) &_httpuv_invokeCppCallback, 2}, {"_httpuv_getRNGState", (DL_FUNC) &_httpuv_getRNGState, 0}, {"_httpuv_wsconn_address", (DL_FUNC) &_httpuv_wsconn_address, 1}, - {"httpuv_decodeURIComponent", (DL_FUNC) &httpuv_decodeURIComponent, 1}, + {"httpuv_decodeURIComponent", (DL_FUNC) &httpuv_decodeURIComponent, 1}, {NULL, NULL, 0} }; From 44665eabdf01b7808decfd1c7f89f2700de7f2d7 Mon Sep 17 00:00:00 2001 From: Winston Chang Date: Wed, 10 Oct 2018 14:33:25 -0500 Subject: [PATCH 03/60] Add support for static serving of subdirs This also adds more checks for the paths specified by the user. --- R/httpuv.R | 62 +++++++++++++++++++++++--------- src/webapplication.cpp | 80 ++++++++++++++++++++++-------------------- src/webapplication.h | 2 ++ 3 files changed, 88 insertions(+), 56 deletions(-) diff --git a/R/httpuv.R b/R/httpuv.R index 627b7789..1f94072a 100644 --- a/R/httpuv.R +++ b/R/httpuv.R @@ -170,7 +170,8 @@ AppWrapper <- setRefClass( fields = list( .app = 'ANY', .wsconns = 'environment', - .supportsOnHeaders = 'logical' + .supportsOnHeaders = 'logical', + .staticPaths = 'character' ), methods = list( initialize = function(app) { @@ -181,6 +182,22 @@ AppWrapper <- setRefClass( # .app$onHeaders can error (e.g. if .app is a reference class) .supportsOnHeaders <<- isTRUE(try(!is.null(.app$onHeaders), silent=TRUE)) + + # staticPaths are saved in a field on this object, because they are read + # from the app object only during initialization. This is the only time + # it makes sense to read them from the app object, since they're + # subsequently used on the background thread, and for performance + # reasons it can't call back into R. Note that if the app object is a + # reference object and app$staticPaths is changed later, it will have no + # effect on the behavior of the application. + # + # If .app is a reference class, accessing .app$staticPaths can error if + # not present. + if (class(try(.app$staticPaths, silent = TRUE)) == "try-error") { + .staticPaths <<- .normalizeStaticPaths(NULL) + } else { + .staticPaths <<- .normalizeStaticPaths(.app$staticPaths) + } }, onHeaders = function(req) { if (!.supportsOnHeaders) @@ -250,24 +267,17 @@ AppWrapper <- setRefClass( } }, getStaticPaths = function() { - # This method always returns a named character vector. - # .app$staticPaths must be NULL, a named list (of strings), or a named - # character vector. - - # If .app is a reference class, accessing .app$staticPaths can error if - # not present. - if (class(try(.app$staticPaths, silent = TRUE)) == "try-error") { - return(empty_named_vec()) - } - - paths <- .app$staticPaths + .staticPaths + }, + .normalizeStaticPaths = function(paths) { + # This function always returns a named character vector. if (is.null(paths) || length(paths) == 0) { return(empty_named_vec()) } if (any_unnamed(paths)) { - stop(".app$staticPaths must be a named character vector, NULL, a or named list (of strings).") + stop("staticPaths must be a named character vector, a named list of strings, or NULL.") } # If list, convert to vector. @@ -275,13 +285,31 @@ AppWrapper <- setRefClass( paths <- unlist(paths, recursive = FALSE) } - # Make sure it's a named character vector. - if (is.character(paths)) { - return(paths) + if (!is.character(paths)) { + stop("staticPaths must be a named character vector, a named list of strings, or NULL.") } - stop(".app$staticPaths must be a named character vector, NULL, a or named list (of strings).") + # If we got here, it is a named character vector. + + # Make sure paths have a leading '/'. Save in a separate var because + # we later call normalizePath(), which drops names. + path_names <- vapply(names(paths), function(path) { + if (path == "") { + stop("All paths must be non-empty strings.") + } + # Ensure there's a leading / for every path + if (substr(path, 1, 1) != "/") { + path <- paste0("/", path) + } + path + }, "") + + paths <- normalizePath(paths, mustWork = TRUE) + names(paths) <- path_names + + paths } + ) ) diff --git a/src/webapplication.cpp b/src/webapplication.cpp index b8656d94..60e7415c 100644 --- a/src/webapplication.cpp +++ b/src/webapplication.cpp @@ -26,6 +26,10 @@ std::map toStringMap(Rcpp::CharacterVector x) { for (int i=0; i(names[i]); std::string value = Rcpp::as(x[i]); + if (name == "") { + throw Rcpp::exception("Error converting CharacterVector to map: element has empty name."); + } + strmap.insert( std::pair(name, value) ); @@ -419,31 +423,48 @@ void RWebApplication::onWSClose(boost::shared_ptr pConn) { // Unlike most of the methods for an RWebApplication, these ones are called on // the background thread. -bool RWebApplication::isStaticPath(const std::string& url_path) { + +// Returns a pair where the first element is the local directory, and the +// second element is the filename. +std::pair RWebApplication::_matchStaticPath(const std::string& url_path) const { ASSERT_BACKGROUND_THREAD() - // Strip off leading '/' if present - size_t start_idx = 0; - if (url_path.at(0) == '/') { - start_idx = 1; - } + std::string path = url_path; + size_t last_split_idx = std::string::npos; - // Trim off last part of path - size_t found_idx = url_path.find_last_of('/'); + // This loop splits the string on '/', starting with the last one, and then + // searches for a match in _staticPaths of the first part. If found, it + // returns a pair with the part before the slash, and the part after the + // slash. If not found, it splits on the previous '/' and searches again, + // and so on, until there are no more to split on. + while (true) { + // Split the string on '/' + size_t found_idx = path.find_last_of('/', last_split_idx); - if (found_idx <= 0) { - return false; + if (found_idx <= 0) { + return std::pair("", ""); + } + + std::string pre_slash = path.substr(0, found_idx); + std::string post_slash = path.substr(found_idx + 1); + + std::map::const_iterator it = _staticPaths.find(pre_slash); + if (it != _staticPaths.end()) { + // Pair with dirname, filename + return std::pair(it->second, post_slash); + } + + last_split_idx = found_idx - 1 ; } +} - std::string dirname = url_path.substr(start_idx, found_idx - 1); - std::string filename = url_path.substr(found_idx + 1); - std::map::iterator it = _staticPaths.find(dirname); - if (it != _staticPaths.end()) { - return true; - } else { +bool RWebApplication::isStaticPath(const std::string& url_path) { + ASSERT_BACKGROUND_THREAD() + if (_matchStaticPath(url_path).first == "") return false; - } + else + return true; } @@ -467,27 +488,9 @@ boost::shared_ptr RWebApplication::staticFileResponse( { ASSERT_BACKGROUND_THREAD() - // Strip off leading '/' if present - size_t start_idx = 0; - if (url_path.at(0) == '/') { - start_idx = 1; - } - - // Get dirname and filename from url - size_t found_idx = url_path.find_last_of('/'); - if (found_idx <= 0) { - return response_404(pRequest); - } - - std::string url_dirname = url_path.substr(start_idx, found_idx - 1); - std::string filename = url_path.substr(found_idx + 1); - - std::string local_dirname = ""; - - std::map::iterator it = _staticPaths.find(url_dirname); - if (it != _staticPaths.end()) { - local_dirname = it->second; - } + std::pair static_path = _matchStaticPath(url_path); + std::string local_dirname = static_path.first; + std::string filename = static_path.second; if (local_dirname == "") { // Typically shouldn't get here, since user should have checked for @@ -512,5 +515,4 @@ boost::shared_ptr RWebApplication::staticFileResponse( new HttpResponse(pRequest, 200, getStatusDescription(200), pDataSource), auto_deleter_background ); - } diff --git a/src/webapplication.h b/src/webapplication.h index f0e608be..1ee6cb31 100644 --- a/src/webapplication.h +++ b/src/webapplication.h @@ -50,6 +50,8 @@ class RWebApplication : public WebApplication { // Probably need to lock at those times. std::map _staticPaths; + std::pair _matchStaticPath(const std::string& url_path) const; + public: RWebApplication(Rcpp::Function onHeaders, Rcpp::Function onBodyData, From b1432ba5bf06a911d62ae0ef19795807730af21a Mon Sep 17 00:00:00 2001 From: Winston Chang Date: Thu, 11 Oct 2018 14:57:04 -0500 Subject: [PATCH 04/60] Use queue to call _on_headers_complete_complete --- src/httprequest.cpp | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/httprequest.cpp b/src/httprequest.cpp index 742277be..91f85da1 100644 --- a/src/httprequest.cpp +++ b/src/httprequest.cpp @@ -293,9 +293,13 @@ int HttpRequest::_on_headers_complete(http_parser* pParser) { boost::shared_ptr pResponse = _pWebApplication->staticFileResponse(shared_from_this(), _url); - // TODO: Should this be called asynchronously? - // Skip over the webapplication code, which calls back into R on the main thread. - _on_headers_complete_complete(pResponse); + // Skip over the webapplication code (which calls back into R on the main + // thread). Just add a call to _on_headers_complete_complete to the queue + // on the background thread. + boost::function cb( + boost::bind(&HttpRequest::_on_headers_complete_complete, shared_from_this(), pResponse) + ); + _background_queue->push(cb); return 0; } From cd7e4d54646da1192e4fadb6a2c0c4dfd51ee798 Mon Sep 17 00:00:00 2001 From: Winston Chang Date: Thu, 11 Oct 2018 15:05:21 -0500 Subject: [PATCH 05/60] Fix indentation --- src/webapplication.cpp | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/webapplication.cpp b/src/webapplication.cpp index 60e7415c..65593a60 100644 --- a/src/webapplication.cpp +++ b/src/webapplication.cpp @@ -469,16 +469,16 @@ bool RWebApplication::isStaticPath(const std::string& url_path) { boost::shared_ptr response_404(boost::shared_ptr pRequest) { - std::string content = "404 file not found"; - std::vector responseData(content.begin(), content.end()); + std::string content = "404 file not found"; + std::vector responseData(content.begin(), content.end()); - // Freed in on_response_written - DataSource* pDataSource = new InMemoryDataSource(responseData); + // Freed in on_response_written + DataSource* pDataSource = new InMemoryDataSource(responseData); - return boost::shared_ptr( - new HttpResponse(pRequest, 404, getStatusDescription(404), pDataSource), - auto_deleter_background - ); + return boost::shared_ptr( + new HttpResponse(pRequest, 404, getStatusDescription(404), pDataSource), + auto_deleter_background + ); } From ff27d6c6520a5a9b7495413719810b97d083b5a3 Mon Sep 17 00:00:00 2001 From: Winston Chang Date: Thu, 11 Oct 2018 16:00:30 -0500 Subject: [PATCH 06/60] Validate request type and absence of request body --- src/httprequest.cpp | 23 ++++++++++++----------- src/httprequest.h | 6 +++--- src/webapplication.cpp | 35 ++++++++++++++++++++++++----------- src/webapplication.h | 8 ++------ 4 files changed, 41 insertions(+), 31 deletions(-) diff --git a/src/httprequest.cpp b/src/httprequest.cpp index 91f85da1..57fab5f8 100644 --- a/src/httprequest.cpp +++ b/src/httprequest.cpp @@ -31,14 +31,14 @@ void on_alloc(uv_handle_t* handle, size_t suggested_size, uv_buf_t* buf) { } // Does a header field `name` exist? -bool HttpRequest::_hasHeader(const std::string& name) const { +bool HttpRequest::hasHeader(const std::string& name) const { return _headers.find(name) != _headers.end(); } // Does a header field `name` exist and have a particular value? If ci is // true, do a case-insensitive comparison of the value (fields are always // case- insensitive.) -bool HttpRequest::_hasHeader(const std::string& name, const std::string& value, bool ci) const { +bool HttpRequest::hasHeader(const std::string& name, const std::string& value, bool ci) const { RequestHeaders::const_iterator item = _headers.find(name); if (item == _headers.end()) return false; @@ -288,14 +288,15 @@ int HttpRequest::_on_headers_complete(http_parser* pParser) { trace("HttpRequest::_on_headers_complete"); updateUpgradeStatus(); - // Attempt static serving here - if (_pWebApplication->isStaticPath(_url)) { - boost::shared_ptr pResponse = - _pWebApplication->staticFileResponse(shared_from_this(), _url); + // Attempt static serving here. If the request is for a static path, this + // will be a response object; if not, it will be nullptr. + boost::shared_ptr pResponse = + _pWebApplication->staticFileResponse(shared_from_this()); - // Skip over the webapplication code (which calls back into R on the main - // thread). Just add a call to _on_headers_complete_complete to the queue - // on the background thread. + if (pResponse) { + // The request was for a static path. Skip over the webapplication code + // (which calls back into R on the main thread). Just add a call to + // _on_headers_complete_complete to the queue on the background thread. boost::function cb( boost::bind(&HttpRequest::_on_headers_complete_complete, shared_from_this(), pResponse) ); @@ -349,7 +350,7 @@ void HttpRequest::_on_headers_complete_complete(boost::shared_ptr int result = 0; if (pResponse) { - bool bodyExpected = _hasHeader("Content-Length") || _hasHeader("Transfer-Encoding"); + bool bodyExpected = hasHeader("Content-Length") || hasHeader("Transfer-Encoding"); bool shouldKeepAlive = http_should_keep_alive(&_parser); // There are two reasons we might want to send a message and close: @@ -379,7 +380,7 @@ void HttpRequest::_on_headers_complete_complete(boost::shared_ptr else { // If the request is Expect: Continue, and the app didn't say otherwise, // then give it what it wants - if (_hasHeader("Expect", "100-continue")) { + if (hasHeader("Expect", "100-continue")) { pResponse = boost::shared_ptr( new HttpResponse(shared_from_this(), 100, "Continue", (DataSource*)NULL), auto_deleter_background diff --git a/src/httprequest.h b/src/httprequest.h index b7670659..66cd8208 100644 --- a/src/httprequest.h +++ b/src/httprequest.h @@ -64,9 +64,6 @@ class HttpRequest : public WebSocketConnectionCallbacks, // true after the headers are complete. bool _is_upgrade; - bool _hasHeader(const std::string& name) const; - bool _hasHeader(const std::string& name, const std::string& value, bool ci = false) const; - void _parse_http_data(char* buf, const ssize_t n); // Parse data that has been stored in the buffer. void _parse_http_data_from_buffer(); @@ -132,6 +129,9 @@ class HttpRequest : public WebSocketConnectionCallbacks, std::string url() const; const RequestHeaders& headers() const; + bool hasHeader(const std::string& name) const; + bool hasHeader(const std::string& name, const std::string& value, bool ci = false) const; + // Is the request an Upgrade (i.e. WebSocket connection)? bool isUpgrade() const; diff --git a/src/webapplication.cpp b/src/webapplication.cpp index 65593a60..dd789fde 100644 --- a/src/webapplication.cpp +++ b/src/webapplication.cpp @@ -468,34 +468,47 @@ bool RWebApplication::isStaticPath(const std::string& url_path) { } -boost::shared_ptr response_404(boost::shared_ptr pRequest) { - std::string content = "404 file not found"; +boost::shared_ptr error_response(boost::shared_ptr pRequest, int code) { + std::ostringstream ss; + std::string description = getStatusDescription(code); + ss << code << " " << description << "\n"; + std::string content = ss.str(); + std::vector responseData(content.begin(), content.end()); // Freed in on_response_written DataSource* pDataSource = new InMemoryDataSource(responseData); return boost::shared_ptr( - new HttpResponse(pRequest, 404, getStatusDescription(404), pDataSource), + new HttpResponse(pRequest, code, description, pDataSource), auto_deleter_background ); } boost::shared_ptr RWebApplication::staticFileResponse( - boost::shared_ptr pRequest, - const std::string& url_path) -{ + boost::shared_ptr pRequest +) { ASSERT_BACKGROUND_THREAD() - std::pair static_path = _matchStaticPath(url_path); + std::pair static_path = _matchStaticPath(pRequest->url()); std::string local_dirname = static_path.first; std::string filename = static_path.second; if (local_dirname == "") { - // Typically shouldn't get here, since user should have checked for - // existence using isStaticPath(). - return response_404(pRequest); + // This was not a static path. + return nullptr; + } + + // Check that method is GET or HEAD; error otherwise. + std::string method = pRequest->method(); + if (method != "GET" && method != "HEAD") { + return error_response(pRequest, 400); + } + + // Make sure that there's no message body. + if (pRequest->hasHeader("Content-Length") || pRequest->hasHeader("Transfer-Encoding")) { + return error_response(pRequest, 400); } std::string local_path = local_dirname + "/" + filename; @@ -508,7 +521,7 @@ boost::shared_ptr RWebApplication::staticFileResponse( if (ret != 0) { // Couldn't read the file delete pDataSource; - return response_404(pRequest); + return error_response(pRequest, 404); } return boost::shared_ptr( diff --git a/src/webapplication.h b/src/webapplication.h index 1ee6cb31..a7814baa 100644 --- a/src/webapplication.h +++ b/src/webapplication.h @@ -29,9 +29,7 @@ class WebApplication { virtual void onWSClose(boost::shared_ptr) = 0; virtual bool isStaticPath(const std::string& url_path) = 0; virtual boost::shared_ptr staticFileResponse( - boost::shared_ptr pRequest, - const std::string& url_path - ) = 0; + boost::shared_ptr pRequest) = 0; }; @@ -82,9 +80,7 @@ class RWebApplication : public WebApplication { virtual bool isStaticPath(const std::string& url_path); virtual boost::shared_ptr staticFileResponse( - boost::shared_ptr pRequest, - const std::string& url_path - ); + boost::shared_ptr pRequest); }; From bfcd28ca511706d7d67043015076efd9768eb4df Mon Sep 17 00:00:00 2001 From: Winston Chang Date: Mon, 15 Oct 2018 10:21:51 -0500 Subject: [PATCH 07/60] Add ability to dynamically add/remove static paths --- NAMESPACE | 3 ++ R/RcppExports.R | 13 +++++++++ R/httpuv.R | 59 ++++++++++--------------------------- R/utils.R | 42 +++++++++++++++++++++++++++ src/RcppExports.cpp | 38 ++++++++++++++++++++++++ src/httpuv.cpp | 45 ++++++++++++++++++++++++++++ src/utils.cpp | 49 +++++++++++++++++++++++++++++++ src/utils.h | 5 ++++ src/webapplication.cpp | 66 +++++++++++++++++++++++------------------- src/webapplication.h | 13 +++++++++ 10 files changed, 260 insertions(+), 73 deletions(-) diff --git a/NAMESPACE b/NAMESPACE index 7c651ef3..334c8b65 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -1,13 +1,16 @@ # Generated by roxygen2: do not edit by hand +export(addStaticPaths) export(decodeURI) export(decodeURIComponent) export(encodeURI) export(encodeURIComponent) export(getRNGState) +export(getStaticPaths) export(interrupt) export(ipFamily) export(rawToBase64) +export(removeStaticPaths) export(runServer) export(service) export(startDaemonizedServer) diff --git a/R/RcppExports.R b/R/RcppExports.R index 21e07da3..6b8d593b 100644 --- a/R/RcppExports.R +++ b/R/RcppExports.R @@ -45,6 +45,19 @@ stopAllServers <- function() { invisible(.Call('_httpuv_stopAllServers', PACKAGE = 'httpuv')) } +#' @export +getStaticPaths <- function(handle) { + .Call('_httpuv_getStaticPaths', PACKAGE = 'httpuv', handle) +} + +addStaticPaths_ <- function(handle, paths) { + .Call('_httpuv_addStaticPaths_', PACKAGE = 'httpuv', handle, paths) +} + +removeStaticPaths_ <- function(handle, paths) { + .Call('_httpuv_removeStaticPaths_', PACKAGE = 'httpuv', handle, paths) +} + base64encode <- function(x) { .Call('_httpuv_base64encode', PACKAGE = 'httpuv', x) } diff --git a/R/httpuv.R b/R/httpuv.R index 1f94072a..825530f7 100644 --- a/R/httpuv.R +++ b/R/httpuv.R @@ -194,9 +194,9 @@ AppWrapper <- setRefClass( # If .app is a reference class, accessing .app$staticPaths can error if # not present. if (class(try(.app$staticPaths, silent = TRUE)) == "try-error") { - .staticPaths <<- .normalizeStaticPaths(NULL) + .staticPaths <<- normalizeStaticPaths(NULL) } else { - .staticPaths <<- .normalizeStaticPaths(.app$staticPaths) + .staticPaths <<- normalizeStaticPaths(.app$staticPaths) } }, onHeaders = function(req) { @@ -268,48 +268,7 @@ AppWrapper <- setRefClass( }, getStaticPaths = function() { .staticPaths - }, - .normalizeStaticPaths = function(paths) { - # This function always returns a named character vector. - - if (is.null(paths) || length(paths) == 0) { - return(empty_named_vec()) - } - - if (any_unnamed(paths)) { - stop("staticPaths must be a named character vector, a named list of strings, or NULL.") - } - - # If list, convert to vector. - if (is.list(paths)) { - paths <- unlist(paths, recursive = FALSE) - } - - if (!is.character(paths)) { - stop("staticPaths must be a named character vector, a named list of strings, or NULL.") - } - - # If we got here, it is a named character vector. - - # Make sure paths have a leading '/'. Save in a separate var because - # we later call normalizePath(), which drops names. - path_names <- vapply(names(paths), function(path) { - if (path == "") { - stop("All paths must be non-empty strings.") - } - # Ensure there's a leading / for every path - if (substr(path, 1, 1) != "/") { - path <- paste0("/", path) - } - path - }, "") - - paths <- normalizePath(paths, mustWork = TRUE) - names(paths) <- path_names - - paths } - ) ) @@ -698,6 +657,20 @@ startDaemonizedServer <- startServer stopDaemonizedServer <- stopServer + +#' @export +addStaticPaths <- function(handle, paths) { + paths <- normalizeStaticPaths(paths) + invisible(addStaticPaths_(handle, paths)) +} + +#' @export +removeStaticPaths <- function(handle, paths) { + paths <- as.character(paths) + invisible(removeStaticPaths_(handle, paths)) +} + + # Needed so that Rcpp registers the 'httpuv_decodeURIComponent' symbol legacy_dummy <- function(value){ .Call('httpuv_decodeURIComponent', PACKAGE = "httpuv", value) diff --git a/R/utils.R b/R/utils.R index f6073557..ba14884a 100644 --- a/R/utils.R +++ b/R/utils.R @@ -32,3 +32,45 @@ any_unnamed <- function(x) { # List with name attribute; check for any "" any(!nzchar(nms)) } + + +# This function always returns a named character vector of path mappings. The +# names will all start with "/". +normalizeStaticPaths <- function(paths) { + if (is.null(paths) || length(paths) == 0) { + return(empty_named_vec()) + } + + if (any_unnamed(paths)) { + stop("paths must be a named character vector, a named list of strings, or NULL.") + } + + # If list, convert to vector. + if (is.list(paths)) { + paths <- unlist(paths, recursive = FALSE) + } + + if (!is.character(paths)) { + stop("paths must be a named character vector, a named list of strings, or NULL.") + } + + # If we got here, it is a named character vector. + + # Make sure paths have a leading '/'. Save in a separate var because + # we later call normalizePath(), which drops names. + path_names <- vapply(names(paths), function(path) { + if (path == "") { + stop("All paths must be non-empty strings.") + } + # Ensure there's a leading / for every path + if (substr(path, 1, 1) != "/") { + path <- paste0("/", path) + } + path + }, "") + + paths <- normalizePath(paths, mustWork = TRUE) + names(paths) <- path_names + + paths +} \ No newline at end of file diff --git a/src/RcppExports.cpp b/src/RcppExports.cpp index ffcfbb7f..56bf9c8d 100644 --- a/src/RcppExports.cpp +++ b/src/RcppExports.cpp @@ -86,6 +86,41 @@ BEGIN_RCPP return R_NilValue; END_RCPP } +// getStaticPaths +Rcpp::CharacterVector getStaticPaths(std::string handle); +RcppExport SEXP _httpuv_getStaticPaths(SEXP handleSEXP) { +BEGIN_RCPP + Rcpp::RObject rcpp_result_gen; + Rcpp::RNGScope rcpp_rngScope_gen; + Rcpp::traits::input_parameter< std::string >::type handle(handleSEXP); + rcpp_result_gen = Rcpp::wrap(getStaticPaths(handle)); + return rcpp_result_gen; +END_RCPP +} +// addStaticPaths_ +Rcpp::CharacterVector addStaticPaths_(std::string handle, Rcpp::CharacterVector paths); +RcppExport SEXP _httpuv_addStaticPaths_(SEXP handleSEXP, SEXP pathsSEXP) { +BEGIN_RCPP + Rcpp::RObject rcpp_result_gen; + Rcpp::RNGScope rcpp_rngScope_gen; + Rcpp::traits::input_parameter< std::string >::type handle(handleSEXP); + Rcpp::traits::input_parameter< Rcpp::CharacterVector >::type paths(pathsSEXP); + rcpp_result_gen = Rcpp::wrap(addStaticPaths_(handle, paths)); + return rcpp_result_gen; +END_RCPP +} +// removeStaticPaths_ +Rcpp::CharacterVector removeStaticPaths_(std::string handle, Rcpp::CharacterVector paths); +RcppExport SEXP _httpuv_removeStaticPaths_(SEXP handleSEXP, SEXP pathsSEXP) { +BEGIN_RCPP + Rcpp::RObject rcpp_result_gen; + Rcpp::RNGScope rcpp_rngScope_gen; + Rcpp::traits::input_parameter< std::string >::type handle(handleSEXP); + Rcpp::traits::input_parameter< Rcpp::CharacterVector >::type paths(pathsSEXP); + rcpp_result_gen = Rcpp::wrap(removeStaticPaths_(handle, paths)); + return rcpp_result_gen; +END_RCPP +} // base64encode std::string base64encode(const Rcpp::RawVector& x); RcppExport SEXP _httpuv_base64encode(SEXP xSEXP) { @@ -193,6 +228,9 @@ static const R_CallMethodDef CallEntries[] = { {"_httpuv_makePipeServer", (DL_FUNC) &_httpuv_makePipeServer, 9}, {"_httpuv_stopServer", (DL_FUNC) &_httpuv_stopServer, 1}, {"_httpuv_stopAllServers", (DL_FUNC) &_httpuv_stopAllServers, 0}, + {"_httpuv_getStaticPaths", (DL_FUNC) &_httpuv_getStaticPaths, 1}, + {"_httpuv_addStaticPaths_", (DL_FUNC) &_httpuv_addStaticPaths_, 2}, + {"_httpuv_removeStaticPaths_", (DL_FUNC) &_httpuv_removeStaticPaths_, 2}, {"_httpuv_base64encode", (DL_FUNC) &_httpuv_base64encode, 1}, {"_httpuv_encodeURI", (DL_FUNC) &_httpuv_encodeURI, 1}, {"_httpuv_encodeURIComponent", (DL_FUNC) &_httpuv_encodeURIComponent, 1}, diff --git a/src/httpuv.cpp b/src/httpuv.cpp index 7add4f26..575e1bda 100644 --- a/src/httpuv.cpp +++ b/src/httpuv.cpp @@ -17,6 +17,7 @@ #include "thread.h" #include "httpuv.h" #include "auto_deleter.h" +#include "socket.h" #include @@ -232,6 +233,10 @@ void closeWS(SEXP conn, } +// ============================================================================ +// Create/stop servers +// ============================================================================ + // [[Rcpp::export]] Rcpp::RObject makeTcpServer(const std::string& host, int port, Rcpp::Function onHeaders, @@ -412,6 +417,46 @@ void stop_loop_timer_cb(uv_timer_t* handle) { } +// ============================================================================ +// Static file serving +// ============================================================================ + +boost::shared_ptr get_pWebApplication(uv_stream_t* pServer) { + // Copy the Socket shared_ptr + boost::shared_ptr pSocket(*(boost::shared_ptr*)pServer->data); + return pSocket->pWebApplication; +} + +boost::shared_ptr get_pWebApplication(std::string handle) { + uv_stream_t* pServer = internalize_str(handle); + return get_pWebApplication(pServer); +} + +//' @export +// [[Rcpp::export]] +Rcpp::CharacterVector getStaticPaths(std::string handle) { + ASSERT_MAIN_THREAD() + std::map paths = get_pWebApplication(handle)->getStaticPaths(); + return toCharacterVector(paths); +} + +// [[Rcpp::export]] +Rcpp::CharacterVector addStaticPaths_(std::string handle, Rcpp::CharacterVector paths) { + ASSERT_MAIN_THREAD() + std::map new_paths = toStringMap(paths); + std::map all_paths = get_pWebApplication(handle)->addStaticPaths(new_paths); + return toCharacterVector(all_paths); +} + +// [[Rcpp::export]] +Rcpp::CharacterVector removeStaticPaths_(std::string handle, Rcpp::CharacterVector paths) { + ASSERT_MAIN_THREAD() + std::vector rm_paths = Rcpp::as>(paths); + std::map all_paths = get_pWebApplication(handle)->removeStaticPaths(rm_paths); + return toCharacterVector(all_paths); +} + + // ============================================================================ // Miscellaneous utility functions // ============================================================================ diff --git a/src/utils.cpp b/src/utils.cpp index 07580c22..5bd7a587 100644 --- a/src/utils.cpp +++ b/src/utils.cpp @@ -1,8 +1,57 @@ #include "utils.h" #include +#include void trace(const std::string& msg) { #ifdef DEBUG_TRACE std::cerr << msg << std::endl; #endif } + + +std::map toStringMap(Rcpp::CharacterVector x) { + ASSERT_MAIN_THREAD() + + std::map strmap; + + if (x.size() == 0) { + return strmap; + } + + Rcpp::CharacterVector names = x.names(); + if (names.isNULL()) { + throw Rcpp::exception("Error converting CharacterVector to map: vector does not have names."); + } + + + for (int i=0; i(names[i]); + std::string value = Rcpp::as(x[i]); + if (name == "") { + throw Rcpp::exception("Error converting CharacterVector to map: element has empty name."); + } + + strmap.insert( + std::pair(name, value) + ); + } + + return strmap; +} + + +Rcpp::CharacterVector toCharacterVector(const std::map& strmap) { + ASSERT_MAIN_THREAD() + + Rcpp::CharacterVector values(strmap.size()); + Rcpp::CharacterVector names(strmap.size()); + + std::map::const_iterator it = strmap.begin(); + for (int i=0; it != strmap.end(); i++, it++) { + values[i] = it->second; + names[i] = it->first; + } + + values.attr("names") = names; + return values; +} diff --git a/src/utils.h b/src/utils.h index eeeb8564..d51cf9e9 100644 --- a/src/utils.h +++ b/src/utils.h @@ -7,6 +7,7 @@ #include #include #include +#include #include "thread.h" // A callback for deleting objects on the main thread using later(). This is @@ -78,4 +79,8 @@ inline std::string to_lower(const std::string& str) { return lowered; } +// Convert between map and CharacterVector +std::map toStringMap(Rcpp::CharacterVector x); +Rcpp::CharacterVector toCharacterVector(const std::map& strmap); + #endif diff --git a/src/webapplication.cpp b/src/webapplication.cpp index dd789fde..b83c0601 100644 --- a/src/webapplication.cpp +++ b/src/webapplication.cpp @@ -8,36 +8,6 @@ #include "utils.h" #include -std::map toStringMap(Rcpp::CharacterVector x) { - ASSERT_MAIN_THREAD() - - std::map strmap; - - if (x.size() == 0) { - return strmap; - } - - Rcpp::CharacterVector names = x.names(); - if (names.isNULL()) { - throw Rcpp::exception("Error converting CharacterVector to map: vector does not have names."); - } - - - for (int i=0; i(names[i]); - std::string value = Rcpp::as(x[i]); - if (name == "") { - throw Rcpp::exception("Error converting CharacterVector to map: element has empty name."); - } - - strmap.insert( - std::pair(name, value) - ); - } - - return strmap; -} - std::string normalizeHeaderName(const std::string& name) { std::string result = name; for (std::string::iterator it = result.begin(); @@ -529,3 +499,39 @@ boost::shared_ptr RWebApplication::staticFileResponse( auto_deleter_background ); } + + +std::map RWebApplication::getStaticPaths() const { + // TODO: always lock staticPaths + return _staticPaths; +}; + +std::map RWebApplication::addStaticPaths( + const std::map& paths +) { + + std::map::const_iterator it; + for (it = paths.begin(); it != paths.end(); it++) { + _staticPaths[it->first] = it->second; + } + + return _staticPaths; +}; + +std::map RWebApplication::removeStaticPaths( + const std::vector& paths +) { + + std::vector::const_iterator path_it = paths.begin(); + + for (path_it = paths.begin(); path_it != paths.end(); path_it++) { + std::map::const_iterator static_paths_it = _staticPaths.find(*path_it); + if (static_paths_it == _staticPaths.end()) { + err_printf("Tried to remove static path, but it wasn't present: %s\n", path_it->c_str()); + } else { + _staticPaths.erase(static_paths_it); + } + } + + return _staticPaths; +}; diff --git a/src/webapplication.h b/src/webapplication.h index a7814baa..ab785270 100644 --- a/src/webapplication.h +++ b/src/webapplication.h @@ -30,6 +30,12 @@ class WebApplication { virtual bool isStaticPath(const std::string& url_path) = 0; virtual boost::shared_ptr staticFileResponse( boost::shared_ptr pRequest) = 0; + // Get current set of static paths. + virtual std::map getStaticPaths() const = 0; + virtual std::map addStaticPaths( + const std::map& paths) = 0; + virtual std::map removeStaticPaths( + const std::vector& paths) = 0; }; @@ -41,6 +47,8 @@ class RWebApplication : public WebApplication { Rcpp::Function _onWSOpen; Rcpp::Function _onWSMessage; Rcpp::Function _onWSClose; + // Note this is different from the public getStaticPaths function - this is + // the R function passed in that is used for initialization only. Rcpp::Function _getStaticPaths; // TODO: need to be careful with this - it's created and destroyed on the @@ -81,6 +89,11 @@ class RWebApplication : public WebApplication { virtual bool isStaticPath(const std::string& url_path); virtual boost::shared_ptr staticFileResponse( boost::shared_ptr pRequest); + virtual std::map getStaticPaths() const; + virtual std::map addStaticPaths( + const std::map& paths); + virtual std::map removeStaticPaths( + const std::vector& paths); }; From 9a61ae0abc351ee6ba562908fbfc8e24eb165a07 Mon Sep 17 00:00:00 2001 From: Winston Chang Date: Mon, 15 Oct 2018 13:40:27 -0500 Subject: [PATCH 08/60] Represent running servers as R6 objects --- DESCRIPTION | 6 ++ NAMESPACE | 6 +- R/RcppExports.R | 20 ++---- R/httpuv.R | 58 +---------------- R/server.R | 122 ++++++++++++++++++++++++++++++++++++ httpuv.Rproj | 2 +- man/stopDaemonizedServer.Rd | 6 +- man/stopServer.Rd | 4 +- src/RcppExports.cpp | 20 +++--- src/httpuv.cpp | 22 ++----- 10 files changed, 156 insertions(+), 110 deletions(-) create mode 100644 R/server.R diff --git a/DESCRIPTION b/DESCRIPTION index b4938362..bdc35989 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -21,6 +21,7 @@ Depends: Imports: Rcpp (>= 0.11.0), utils, + R6, promises, later (>= 0.7.3) LinkingTo: Rcpp, BH, later @@ -30,3 +31,8 @@ RoxygenNote: 6.1.0.9000 Suggests: testthat, callr +Collate: + 'RcppExports.R' + 'server.R' + 'httpuv.R' + 'utils.R' diff --git a/NAMESPACE b/NAMESPACE index 334c8b65..98a9b2c7 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -1,16 +1,15 @@ # Generated by roxygen2: do not edit by hand -export(addStaticPaths) +export(WebServer) export(decodeURI) export(decodeURIComponent) export(encodeURI) export(encodeURIComponent) export(getRNGState) -export(getStaticPaths) +export(getStaticPaths_) export(interrupt) export(ipFamily) export(rawToBase64) -export(removeStaticPaths) export(runServer) export(service) export(startDaemonizedServer) @@ -21,6 +20,7 @@ export(stopDaemonizedServer) export(stopServer) exportClasses(WebSocket) import(methods) +importFrom(R6,R6Class) importFrom(Rcpp,evalCpp) importFrom(later,run_now) importFrom(promises,"%...!%") diff --git a/R/RcppExports.R b/R/RcppExports.R index 6b8d593b..8408545e 100644 --- a/R/RcppExports.R +++ b/R/RcppExports.R @@ -17,20 +17,8 @@ makePipeServer <- function(name, mask, onHeaders, onBodyData, onRequest, onWSOpe .Call('_httpuv_makePipeServer', PACKAGE = 'httpuv', name, mask, onHeaders, onBodyData, onRequest, onWSOpen, onWSMessage, onWSClose, getStaticPaths) } -#' Stop a server -#' -#' Given a handle that was returned from a previous invocation of -#' \code{\link{startServer}} or \code{\link{startPipeServer}}, this closes all -#' open connections for that server and unbinds the port. -#' -#' @param handle A handle that was previously returned from -#' \code{\link{startServer}} or \code{\link{startPipeServer}}. -#' -#' @seealso \code{\link{stopAllServers}} to stop all servers. -#' -#' @export -stopServer <- function(handle) { - invisible(.Call('_httpuv_stopServer', PACKAGE = 'httpuv', handle)) +stopServer_ <- function(handle) { + invisible(.Call('_httpuv_stopServer_', PACKAGE = 'httpuv', handle)) } #' Stop all applications @@ -46,8 +34,8 @@ stopAllServers <- function() { } #' @export -getStaticPaths <- function(handle) { - .Call('_httpuv_getStaticPaths', PACKAGE = 'httpuv', handle) +getStaticPaths_ <- function(handle) { + .Call('_httpuv_getStaticPaths_', PACKAGE = 'httpuv', handle) } addStaticPaths_ <- function(handle, paths) { diff --git a/R/httpuv.R b/R/httpuv.R index 825530f7..857e9200 100644 --- a/R/httpuv.R +++ b/R/httpuv.R @@ -451,23 +451,7 @@ WebSocket <- setRefClass( #' } #' @export startServer <- function(host, port, app) { - - appWrapper <- AppWrapper$new(app) - server <- makeTcpServer( - host, port, - appWrapper$onHeaders, - appWrapper$onBodyData, - appWrapper$call, - appWrapper$onWSOpen, - appWrapper$onWSMessage, - appWrapper$onWSClose, - appWrapper$getStaticPaths - ) - - if (is.null(server)) { - stop("Failed to create server") - } - return(server) + WebServer$new(host, port, app) } #' @param name A string that indicates the path for the domain socket (on @@ -481,21 +465,7 @@ startServer <- function(host, port, app) { #' @rdname startServer #' @export startPipeServer <- function(name, mask, app) { - - appWrapper <- AppWrapper$new(app) - if (is.null(mask)) - mask <- -1 - server <- makePipeServer(name, mask, - appWrapper$onHeaders, - appWrapper$onBodyData, - appWrapper$call, - appWrapper$onWSOpen, - appWrapper$onWSMessage, - appWrapper$onWSClose) - if (is.null(server)) { - stop("Failed to create server") - } - return(server) + PipeServer$new(name, mask, app) } #' Process requests @@ -646,30 +616,6 @@ rawToBase64 <- function(x) { #' @export startDaemonizedServer <- startServer -#' Stop a running daemonized server in Unix environments (deprecated) -#' -#' This function will be removed in a future release of httpuv. Instead, use -#' \code{\link{stopServer}}. -#' -#' @inheritParams stopServer -#' -#' @export -stopDaemonizedServer <- stopServer - - - -#' @export -addStaticPaths <- function(handle, paths) { - paths <- normalizeStaticPaths(paths) - invisible(addStaticPaths_(handle, paths)) -} - -#' @export -removeStaticPaths <- function(handle, paths) { - paths <- as.character(paths) - invisible(removeStaticPaths_(handle, paths)) -} - # Needed so that Rcpp registers the 'httpuv_decodeURIComponent' symbol legacy_dummy <- function(value){ diff --git a/R/server.R b/R/server.R new file mode 100644 index 00000000..ab771d38 --- /dev/null +++ b/R/server.R @@ -0,0 +1,122 @@ +#' @importFrom R6 R6Class +Server <- R6Class("Server", + cloneable = FALSE, + public = list( + stop = function() { + # TODO: what if is already running? + stopServer_(private$handle) + }, + getStaticPaths = function() { + getStaticPaths_(private$handle) + }, + addStaticPaths = function(paths) { + paths <- normalizeStaticPaths(paths) + invisible(addStaticPaths_(private$handle, paths)) + }, + removeStaticPaths = function(paths) { + paths <- as.character(paths) + invisible(removeStaticPaths_(private$handle, paths)) + } + ), + private = list( + appWrapper = NULL, + handle = NULL + ) +) + + +#' @export +WebServer <- R6Class("WebServer", + cloneable = FALSE, + inherit = Server, + public = list( + initialize = function(host, port, app) { + private$host <- host + private$port <- port + private$appWrapper <- AppWrapper$new(app) + + private$handle <- makeTcpServer( + host, port, + private$appWrapper$onHeaders, + private$appWrapper$onBodyData, + private$appWrapper$call, + private$appWrapper$onWSOpen, + private$appWrapper$onWSMessage, + private$appWrapper$onWSClose, + private$appWrapper$getStaticPaths + ) + + if (is.null(private$handle)) { + stop("Failed to create server") + } + } + ), + private = list( + host = NULL, + port = NULL + ) +) + + +#' @export +PipeServer <- R6Class("PipeServer", + cloneable = FALSE, + inherit = Server, + public = list( + initialize = function(name, mask, app) { + private$name <- name + if (is.null(private$mask)) { + private$mask <- -1 + } + private$appWrapper <- AppWrapper$new(app) + + private$handle <- makePipeServer( + name, mask, + private$appWrapper$onHeaders, + private$appWrapper$onBodyData, + private$appWrapper$call, + private$appWrapper$onWSOpen, + private$appWrapper$onWSMessage, + private$appWrapper$onWSClose + ) + if (is.null(private$handle)) { + stop("Failed to create server") + } + } + ), + private = list( + name = NULL, + mask = NULL + ) +) + + +#' Stop a server +#' +#' Given a handle that was returned from a previous invocation of +#' \code{\link{startServer}} or \code{\link{startPipeServer}}, this closes all +#' open connections for that server and unbinds the port. +#' +#' @param handle A handle that was previously returned from +#' \code{\link{startServer}} or \code{\link{startPipeServer}}. +#' +#' @seealso \code{\link{stopAllServers}} to stop all servers. +#' +#' @export +stopServer <- function(server) { + if (!inherits(server, "Server")) { + stop("Object must be an object of class Server.") + } + server$stop() +} + + +#' Stop a running daemonized server in Unix environments (deprecated) +#' +#' This function will be removed in a future release of httpuv. Instead, use +#' \code{\link{stopServer}}. +#' +#' @inheritParams stopServer +#' +#' @export +stopDaemonizedServer <- stopServer diff --git a/httpuv.Rproj b/httpuv.Rproj index 8e3521a1..4759b8e8 100644 --- a/httpuv.Rproj +++ b/httpuv.Rproj @@ -16,4 +16,4 @@ AutoAppendNewline: Yes BuildType: Package PackageInstallArgs: --no-multiarch --with-keep.source -PackageRoxygenize: rd,namespace +PackageRoxygenize: rd,collate,namespace diff --git a/man/stopDaemonizedServer.Rd b/man/stopDaemonizedServer.Rd index e6658dec..b73c9a38 100644 --- a/man/stopDaemonizedServer.Rd +++ b/man/stopDaemonizedServer.Rd @@ -4,11 +4,7 @@ \alias{stopDaemonizedServer} \title{Stop a running daemonized server in Unix environments (deprecated)} \usage{ -stopDaemonizedServer(handle) -} -\arguments{ -\item{handle}{A handle that was previously returned from -\code{\link{startServer}} or \code{\link{startPipeServer}}.} +stopDaemonizedServer(server) } \description{ This function will be removed in a future release of httpuv. Instead, use diff --git a/man/stopServer.Rd b/man/stopServer.Rd index 550cdd89..e248938b 100644 --- a/man/stopServer.Rd +++ b/man/stopServer.Rd @@ -1,10 +1,10 @@ % Generated by roxygen2: do not edit by hand -% Please edit documentation in R/RcppExports.R +% Please edit documentation in R/server.R \name{stopServer} \alias{stopServer} \title{Stop a server} \usage{ -stopServer(handle) +stopServer(server) } \arguments{ \item{handle}{A handle that was previously returned from diff --git a/src/RcppExports.cpp b/src/RcppExports.cpp index 56bf9c8d..828ebbfc 100644 --- a/src/RcppExports.cpp +++ b/src/RcppExports.cpp @@ -67,13 +67,13 @@ BEGIN_RCPP return rcpp_result_gen; END_RCPP } -// stopServer -void stopServer(std::string handle); -RcppExport SEXP _httpuv_stopServer(SEXP handleSEXP) { +// stopServer_ +void stopServer_(std::string handle); +RcppExport SEXP _httpuv_stopServer_(SEXP handleSEXP) { BEGIN_RCPP Rcpp::RNGScope rcpp_rngScope_gen; Rcpp::traits::input_parameter< std::string >::type handle(handleSEXP); - stopServer(handle); + stopServer_(handle); return R_NilValue; END_RCPP } @@ -86,14 +86,14 @@ BEGIN_RCPP return R_NilValue; END_RCPP } -// getStaticPaths -Rcpp::CharacterVector getStaticPaths(std::string handle); -RcppExport SEXP _httpuv_getStaticPaths(SEXP handleSEXP) { +// getStaticPaths_ +Rcpp::CharacterVector getStaticPaths_(std::string handle); +RcppExport SEXP _httpuv_getStaticPaths_(SEXP handleSEXP) { BEGIN_RCPP Rcpp::RObject rcpp_result_gen; Rcpp::RNGScope rcpp_rngScope_gen; Rcpp::traits::input_parameter< std::string >::type handle(handleSEXP); - rcpp_result_gen = Rcpp::wrap(getStaticPaths(handle)); + rcpp_result_gen = Rcpp::wrap(getStaticPaths_(handle)); return rcpp_result_gen; END_RCPP } @@ -226,9 +226,9 @@ static const R_CallMethodDef CallEntries[] = { {"_httpuv_closeWS", (DL_FUNC) &_httpuv_closeWS, 3}, {"_httpuv_makeTcpServer", (DL_FUNC) &_httpuv_makeTcpServer, 9}, {"_httpuv_makePipeServer", (DL_FUNC) &_httpuv_makePipeServer, 9}, - {"_httpuv_stopServer", (DL_FUNC) &_httpuv_stopServer, 1}, + {"_httpuv_stopServer_", (DL_FUNC) &_httpuv_stopServer_, 1}, {"_httpuv_stopAllServers", (DL_FUNC) &_httpuv_stopAllServers, 0}, - {"_httpuv_getStaticPaths", (DL_FUNC) &_httpuv_getStaticPaths, 1}, + {"_httpuv_getStaticPaths_", (DL_FUNC) &_httpuv_getStaticPaths_, 1}, {"_httpuv_addStaticPaths_", (DL_FUNC) &_httpuv_addStaticPaths_, 2}, {"_httpuv_removeStaticPaths_", (DL_FUNC) &_httpuv_removeStaticPaths_, 2}, {"_httpuv_base64encode", (DL_FUNC) &_httpuv_base64encode, 1}, diff --git a/src/httpuv.cpp b/src/httpuv.cpp index 575e1bda..d6fed3a5 100644 --- a/src/httpuv.cpp +++ b/src/httpuv.cpp @@ -347,7 +347,7 @@ Rcpp::RObject makePipeServer(const std::string& name, } -void stopServer(uv_stream_t* pServer) { +void stopServer_(uv_stream_t* pServer) { ASSERT_MAIN_THREAD() // Remove it from the list of running servers. @@ -367,23 +367,11 @@ void stopServer(uv_stream_t* pServer) { ); } -//' Stop a server -//' -//' Given a handle that was returned from a previous invocation of -//' \code{\link{startServer}} or \code{\link{startPipeServer}}, this closes all -//' open connections for that server and unbinds the port. -//' -//' @param handle A handle that was previously returned from -//' \code{\link{startServer}} or \code{\link{startPipeServer}}. -//' -//' @seealso \code{\link{stopAllServers}} to stop all servers. -//' -//' @export // [[Rcpp::export]] -void stopServer(std::string handle) { +void stopServer_(std::string handle) { ASSERT_MAIN_THREAD() uv_stream_t* pServer = internalize_str(handle); - stopServer(pServer); + stopServer_(pServer); } //' Stop all applications @@ -403,7 +391,7 @@ void stopAllServers() { // Each call to stopServer also removes it from the pServers list. while (pServers.size() > 0) { - stopServer(pServers[0]); + stopServer_(pServers[0]); } uv_async_send(&async_stop_io_loop); @@ -434,7 +422,7 @@ boost::shared_ptr get_pWebApplication(std::string handle) { //' @export // [[Rcpp::export]] -Rcpp::CharacterVector getStaticPaths(std::string handle) { +Rcpp::CharacterVector getStaticPaths_(std::string handle) { ASSERT_MAIN_THREAD() std::map paths = get_pWebApplication(handle)->getStaticPaths(); return toCharacterVector(paths); From adafbdd9aefa99940e835d18e6c5df859c4f05b7 Mon Sep 17 00:00:00 2001 From: Winston Chang Date: Mon, 15 Oct 2018 15:24:43 -0500 Subject: [PATCH 09/60] Add global registry of running servers --- DESCRIPTION | 2 +- NAMESPACE | 2 ++ R/RcppExports.R | 12 ------- R/server.R | 72 +++++++++++++++++++++++++++++++++++-- man/stopAllServers.Rd | 4 +-- man/stopDaemonizedServer.Rd | 2 +- src/RcppExports.cpp | 10 ------ src/httpuv.cpp | 26 -------------- 8 files changed, 75 insertions(+), 55 deletions(-) diff --git a/DESCRIPTION b/DESCRIPTION index bdc35989..adc940e9 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -33,6 +33,6 @@ Suggests: callr Collate: 'RcppExports.R' - 'server.R' 'httpuv.R' + 'server.R' 'utils.R' diff --git a/NAMESPACE b/NAMESPACE index 98a9b2c7..e7d12e97 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -1,5 +1,6 @@ # Generated by roxygen2: do not edit by hand +export(PipeServer) export(WebServer) export(decodeURI) export(decodeURIComponent) @@ -9,6 +10,7 @@ export(getRNGState) export(getStaticPaths_) export(interrupt) export(ipFamily) +export(listServers) export(rawToBase64) export(runServer) export(service) diff --git a/R/RcppExports.R b/R/RcppExports.R index 8408545e..d74c3298 100644 --- a/R/RcppExports.R +++ b/R/RcppExports.R @@ -21,18 +21,6 @@ stopServer_ <- function(handle) { invisible(.Call('_httpuv_stopServer_', PACKAGE = 'httpuv', handle)) } -#' Stop all applications -#' -#' This will stop all applications which were created by -#' \code{\link{startServer}} or \code{\link{startPipeServer}}. -#' -#' @seealso \code{\link{stopServer}} to stop a specific server. -#' -#' @export -stopAllServers <- function() { - invisible(.Call('_httpuv_stopAllServers', PACKAGE = 'httpuv')) -} - #' @export getStaticPaths_ <- function(handle) { .Call('_httpuv_getStaticPaths_', PACKAGE = 'httpuv', handle) diff --git a/R/server.R b/R/server.R index ab771d38..ff746d35 100644 --- a/R/server.R +++ b/R/server.R @@ -1,10 +1,22 @@ +#' @include httpuv.R + #' @importFrom R6 R6Class Server <- R6Class("Server", cloneable = FALSE, public = list( stop = function() { - # TODO: what if is already running? + if (!private$running) { + stop("Server is already stopped.") + } stopServer_(private$handle) + private$running <- FALSE + deregisterServer(self) + }, + isRunning = function() { + # This doesn't map exactly to whether the app is running, since the + # server's uv_loop runs on the background thread. This could be changed + # to something that queries the C++ side about what's running. + private$running }, getStaticPaths = function() { getStaticPaths_(private$handle) @@ -20,7 +32,8 @@ Server <- R6Class("Server", ), private = list( appWrapper = NULL, - handle = NULL + handle = NULL, + running = FALSE ) ) @@ -49,6 +62,12 @@ WebServer <- R6Class("WebServer", if (is.null(private$handle)) { stop("Failed to create server") } + + private$running <- TRUE + registerServer(self) + }, + getID = function() { + paste0(private$host, ":", private$port) } ), private = list( @@ -64,7 +83,6 @@ PipeServer <- R6Class("PipeServer", inherit = Server, public = list( initialize = function(name, mask, app) { - private$name <- name if (is.null(private$mask)) { private$mask <- -1 } @@ -79,9 +97,16 @@ PipeServer <- R6Class("PipeServer", private$appWrapper$onWSMessage, private$appWrapper$onWSClose ) + + # Save the full path. normalizePath must be called after makePipeServer + private$name <- normalizePath(name) + if (is.null(private$handle)) { stop("Failed to create server") } + }, + getID = function() { + private$name } ), private = list( @@ -111,6 +136,47 @@ stopServer <- function(server) { } +#' Stop all servers +#' +#' This will stop all applications which were created by +#' \code{\link{startServer}} or \code{\link{startPipeServer}}. +#' +#' @seealso \code{\link{stopServer}} to stop a specific server. +#' +#' @export +stopAllServers <- function() { + lapply(.globals$servers, function(server) { + server$stop() + }) + invisible() +} + + +.globals$servers <- list() + +#' @export +listServers <- function() { + .globals$servers +} + +registerServer <- function(server) { + id <- server$getID() + .globals$servers[[id]] <- server +} + +deregisterServer <- function(server) { + if (is.character(server)) { + id <- server + } else { + # Assume it's a Server object + id <- server$getID() + } + .globals$servers[[id]] <- NULL +} + + + + #' Stop a running daemonized server in Unix environments (deprecated) #' #' This function will be removed in a future release of httpuv. Instead, use diff --git a/man/stopAllServers.Rd b/man/stopAllServers.Rd index 33f1a6b1..0bd039bf 100644 --- a/man/stopAllServers.Rd +++ b/man/stopAllServers.Rd @@ -1,8 +1,8 @@ % Generated by roxygen2: do not edit by hand -% Please edit documentation in R/RcppExports.R +% Please edit documentation in R/server.R \name{stopAllServers} \alias{stopAllServers} -\title{Stop all applications} +\title{Stop all servers} \usage{ stopAllServers() } diff --git a/man/stopDaemonizedServer.Rd b/man/stopDaemonizedServer.Rd index b73c9a38..07817804 100644 --- a/man/stopDaemonizedServer.Rd +++ b/man/stopDaemonizedServer.Rd @@ -1,5 +1,5 @@ % Generated by roxygen2: do not edit by hand -% Please edit documentation in R/httpuv.R +% Please edit documentation in R/server.R \name{stopDaemonizedServer} \alias{stopDaemonizedServer} \title{Stop a running daemonized server in Unix environments (deprecated)} diff --git a/src/RcppExports.cpp b/src/RcppExports.cpp index 828ebbfc..ed6ab846 100644 --- a/src/RcppExports.cpp +++ b/src/RcppExports.cpp @@ -77,15 +77,6 @@ BEGIN_RCPP return R_NilValue; END_RCPP } -// stopAllServers -void stopAllServers(); -RcppExport SEXP _httpuv_stopAllServers() { -BEGIN_RCPP - Rcpp::RNGScope rcpp_rngScope_gen; - stopAllServers(); - return R_NilValue; -END_RCPP -} // getStaticPaths_ Rcpp::CharacterVector getStaticPaths_(std::string handle); RcppExport SEXP _httpuv_getStaticPaths_(SEXP handleSEXP) { @@ -227,7 +218,6 @@ static const R_CallMethodDef CallEntries[] = { {"_httpuv_makeTcpServer", (DL_FUNC) &_httpuv_makeTcpServer, 9}, {"_httpuv_makePipeServer", (DL_FUNC) &_httpuv_makePipeServer, 9}, {"_httpuv_stopServer_", (DL_FUNC) &_httpuv_stopServer_, 1}, - {"_httpuv_stopAllServers", (DL_FUNC) &_httpuv_stopAllServers, 0}, {"_httpuv_getStaticPaths_", (DL_FUNC) &_httpuv_getStaticPaths_, 1}, {"_httpuv_addStaticPaths_", (DL_FUNC) &_httpuv_addStaticPaths_, 2}, {"_httpuv_removeStaticPaths_", (DL_FUNC) &_httpuv_removeStaticPaths_, 2}, diff --git a/src/httpuv.cpp b/src/httpuv.cpp index d6fed3a5..658ea1d1 100644 --- a/src/httpuv.cpp +++ b/src/httpuv.cpp @@ -374,32 +374,6 @@ void stopServer_(std::string handle) { stopServer_(pServer); } -//' Stop all applications -//' -//' This will stop all applications which were created by -//' \code{\link{startServer}} or \code{\link{startPipeServer}}. -//' -//' @seealso \code{\link{stopServer}} to stop a specific server. -//' -//' @export -// [[Rcpp::export]] -void stopAllServers() { - ASSERT_MAIN_THREAD() - - if (!io_thread_running.get()) - return; - - // Each call to stopServer also removes it from the pServers list. - while (pServers.size() > 0) { - stopServer_(pServers[0]); - } - - uv_async_send(&async_stop_io_loop); - - trace("io_thread stopped"); - uv_thread_join(&io_thread_id); -} - void stop_loop_timer_cb(uv_timer_t* handle) { uv_stop(handle->loop); } From 97f4ac14671d9aace9ad36db4dc014b66b8a016b Mon Sep 17 00:00:00 2001 From: Winston Chang Date: Mon, 15 Oct 2018 16:33:51 -0500 Subject: [PATCH 10/60] Add toString template function --- src/filedatasource-unix.cpp | 8 ++------ src/filedatasource-win.cpp | 8 ++------ src/utils.h | 7 +++++++ src/webapplication.cpp | 4 +--- 4 files changed, 12 insertions(+), 15 deletions(-) diff --git a/src/filedatasource-unix.cpp b/src/filedatasource-unix.cpp index 80eeaa47..7d81c597 100644 --- a/src/filedatasource-unix.cpp +++ b/src/filedatasource-unix.cpp @@ -11,17 +11,13 @@ int FileDataSource::initialize(const std::string& path, bool owned) { _fd = open(path.c_str(), O_RDONLY); if (_fd == -1) { - std::ostringstream ss; - ss << "Error opening file: " << errno << "\n"; - _lastErrorMessage = ss.str(); + _lastErrorMessage = "Error opening file: " + toString(errno) + "\n"; return 1; } else { struct stat info = {0}; if (fstat(_fd, &info)) { - std::ostringstream ss; - ss << "Error opening path: " << errno << "\n"; - _lastErrorMessage = ss.str(); + _lastErrorMessage = "Error opening path: " + toString(errno) + "\n"; ::close(_fd); return 1; } diff --git a/src/filedatasource-win.cpp b/src/filedatasource-win.cpp index 2270c1d9..a102f0f7 100644 --- a/src/filedatasource-win.cpp +++ b/src/filedatasource-win.cpp @@ -24,17 +24,13 @@ int FileDataSource::initialize(const std::string& path, bool owned) { NULL); if (_hFile == INVALID_HANDLE_VALUE) { - std::ostringstream ss; - ss << "Error opening file: " << GetLastError() << "\n"; - _lastErrorMessage = ss.str(); + _lastErrorMessage = "Error opening file: " + toString(GetLastError()) + "\n"; return 1; } if (!GetFileSizeEx(_hFile, &_length)) { CloseHandle(_hFile); - std::ostringstream ss; - ss << "Error retrieving file size: " << GetLastError() << "\n"; - _lastErrorMessage = ss.str(); + _lastErrorMessage = "Error retrieving file size: " + toString(GetLastError()) + "\n"; return 1; } diff --git a/src/utils.h b/src/utils.h index d51cf9e9..4015c50a 100644 --- a/src/utils.h +++ b/src/utils.h @@ -79,6 +79,13 @@ inline std::string to_lower(const std::string& str) { return lowered; } +template +std::string toString(T x) { + std::stringstream ss; + ss << x; + return ss.str(); +} + // Convert between map and CharacterVector std::map toStringMap(Rcpp::CharacterVector x); Rcpp::CharacterVector toCharacterVector(const std::map& strmap); diff --git a/src/webapplication.cpp b/src/webapplication.cpp index b83c0601..e29f5913 100644 --- a/src/webapplication.cpp +++ b/src/webapplication.cpp @@ -439,10 +439,8 @@ bool RWebApplication::isStaticPath(const std::string& url_path) { boost::shared_ptr error_response(boost::shared_ptr pRequest, int code) { - std::ostringstream ss; std::string description = getStatusDescription(code); - ss << code << " " << description << "\n"; - std::string content = ss.str(); + std::string content = toString(code) + " " + description + "\n"; std::vector responseData(content.begin(), content.end()); From be69608062ce68a63260e24fdaa4b45f91c968e0 Mon Sep 17 00:00:00 2001 From: Winston Chang Date: Tue, 16 Oct 2018 10:31:33 -0500 Subject: [PATCH 11/60] Set Content-Length for both HEAD and GET --- src/webapplication.cpp | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/src/webapplication.cpp b/src/webapplication.cpp index e29f5913..381d2782 100644 --- a/src/webapplication.cpp +++ b/src/webapplication.cpp @@ -492,10 +492,29 @@ boost::shared_ptr RWebApplication::staticFileResponse( return error_response(pRequest, 404); } - return boost::shared_ptr( + int file_size = pDataSource->size(); + + if (method == "HEAD") { + // For HEAD requests, we created the FileDataSource to get the size and + // validate that the file exists, but don't actually send the file's data. + delete pDataSource; + pDataSource = NULL; + } + + boost::shared_ptr pResponse( new HttpResponse(pRequest, 200, getStatusDescription(200), pDataSource), auto_deleter_background ); + + // Set the Content-Length here so that both GET and HEAD requests will get + // it. If we didn't set it here, the response for the GET would + // automatically set the Content-Length (by using the FileDataSource), but + // the response for the HEAD would not. + pResponse->headers().push_back( + std::make_pair("Content-Length", toString(file_size)) + ); + + return pResponse; } From 7a180dc7e24379af604bf6456ecc2eef527014d2 Mon Sep 17 00:00:00 2001 From: Winston Chang Date: Tue, 16 Oct 2018 12:04:51 -0500 Subject: [PATCH 12/60] Automatically add Content-Type header based on extension --- src/mime.cpp | 562 +++++++++++++++++++++++++++++++++++++++++ src/mime.h | 4 + src/utils.h | 25 ++ src/webapplication.cpp | 10 +- tools/update_mime.R | 41 +++ 5 files changed, 639 insertions(+), 3 deletions(-) create mode 100644 src/mime.cpp create mode 100644 src/mime.h create mode 100644 tools/update_mime.R diff --git a/src/mime.cpp b/src/mime.cpp new file mode 100644 index 00000000..18d5fc86 --- /dev/null +++ b/src/mime.cpp @@ -0,0 +1,562 @@ +// Do not edit. +// Generated by ../tools/update_mime.R using R package mime 0.6 + +#include +#include + +const std::map mime_map = { + {"ez", "application/andrew-inset"}, + {"anx", "application/annodex"}, + {"atom", "application/atom+xml"}, + {"atomcat", "application/atomcat+xml"}, + {"atomsrv", "application/atomserv+xml"}, + {"lin", "application/bbolin"}, + {"cu", "application/cu-seeme"}, + {"davmount", "application/davmount+xml"}, + {"dcm", "application/dicom"}, + {"tsp", "application/dsptype"}, + {"es", "application/ecmascript"}, + {"otf", "application/font-sfnt"}, + {"ttf", "application/font-sfnt"}, + {"pfr", "application/font-tdpfr"}, + {"woff", "application/font-woff"}, + {"spl", "application/futuresplash"}, + {"gz", "application/gzip"}, + {"hta", "application/hta"}, + {"jar", "application/java-archive"}, + {"ser", "application/java-serialized-object"}, + {"class", "application/java-vm"}, + {"js", "application/javascript"}, + {"json", "application/json"}, + {"m3g", "application/m3g"}, + {"hqx", "application/mac-binhex40"}, + {"cpt", "application/mac-compactpro"}, + {"nb", "application/mathematica"}, + {"nbp", "application/mathematica"}, + {"mbox", "application/mbox"}, + {"mdb", "application/msaccess"}, + {"doc", "application/msword"}, + {"dot", "application/msword"}, + {"mxf", "application/mxf"}, + {"bin", "application/octet-stream"}, + {"deploy", "application/octet-stream"}, + {"msu", "application/octet-stream"}, + {"msp", "application/octet-stream"}, + {"oda", "application/oda"}, + {"opf", "application/oebps-package+xml"}, + {"ogx", "application/ogg"}, + {"one", "application/onenote"}, + {"onetoc2", "application/onenote"}, + {"onetmp", "application/onenote"}, + {"onepkg", "application/onenote"}, + {"pdf", "application/pdf"}, + {"pgp", "application/pgp-encrypted"}, + {"key", "application/pgp-keys"}, + {"sig", "application/pgp-signature"}, + {"prf", "application/pics-rules"}, + {"ps", "application/postscript"}, + {"ai", "application/postscript"}, + {"eps", "application/postscript"}, + {"epsi", "application/postscript"}, + {"epsf", "application/postscript"}, + {"eps2", "application/postscript"}, + {"eps3", "application/postscript"}, + {"rar", "application/rar"}, + {"rdf", "application/rdf+xml"}, + {"rtf", "application/rtf"}, + {"stl", "application/sla"}, + {"smi", "application/smil+xml"}, + {"smil", "application/smil+xml"}, + {"xhtml", "application/xhtml+xml"}, + {"xht", "application/xhtml+xml"}, + {"xml", "application/xml"}, + {"xsd", "application/xml"}, + {"xsl", "application/xslt+xml"}, + {"xslt", "application/xslt+xml"}, + {"xspf", "application/xspf+xml"}, + {"zip", "application/zip"}, + {"apk", "application/vnd.android.package-archive"}, + {"cdy", "application/vnd.cinderella"}, + {"deb", "application/vnd.debian.binary-package"}, + {"ddeb", "application/vnd.debian.binary-package"}, + {"udeb", "application/vnd.debian.binary-package"}, + {"sfd", "application/vnd.font-fontforge-sfd"}, + {"kml", "application/vnd.google-earth.kml+xml"}, + {"kmz", "application/vnd.google-earth.kmz"}, + {"xul", "application/vnd.mozilla.xul+xml"}, + {"xls", "application/vnd.ms-excel"}, + {"xlb", "application/vnd.ms-excel"}, + {"xlt", "application/vnd.ms-excel"}, + {"xlam", "application/vnd.ms-excel.addin.macroEnabled.12"}, + {"xlsb", "application/vnd.ms-excel.sheet.binary.macroEnabled.12"}, + {"xlsm", "application/vnd.ms-excel.sheet.macroEnabled.12"}, + {"xltm", "application/vnd.ms-excel.template.macroEnabled.12"}, + {"eot", "application/vnd.ms-fontobject"}, + {"thmx", "application/vnd.ms-officetheme"}, + {"cat", "application/vnd.ms-pki.seccat"}, + {"ppt", "application/vnd.ms-powerpoint"}, + {"pps", "application/vnd.ms-powerpoint"}, + {"ppam", "application/vnd.ms-powerpoint.addin.macroEnabled.12"}, + {"pptm", "application/vnd.ms-powerpoint.presentation.macroEnabled.12"}, + {"sldm", "application/vnd.ms-powerpoint.slide.macroEnabled.12"}, + {"ppsm", "application/vnd.ms-powerpoint.slideshow.macroEnabled.12"}, + {"potm", "application/vnd.ms-powerpoint.template.macroEnabled.12"}, + {"docm", "application/vnd.ms-word.document.macroEnabled.12"}, + {"dotm", "application/vnd.ms-word.template.macroEnabled.12"}, + {"odc", "application/vnd.oasis.opendocument.chart"}, + {"odb", "application/vnd.oasis.opendocument.database"}, + {"odf", "application/vnd.oasis.opendocument.formula"}, + {"odg", "application/vnd.oasis.opendocument.graphics"}, + {"otg", "application/vnd.oasis.opendocument.graphics-template"}, + {"odi", "application/vnd.oasis.opendocument.image"}, + {"odp", "application/vnd.oasis.opendocument.presentation"}, + {"otp", "application/vnd.oasis.opendocument.presentation-template"}, + {"ods", "application/vnd.oasis.opendocument.spreadsheet"}, + {"ots", "application/vnd.oasis.opendocument.spreadsheet-template"}, + {"odt", "application/vnd.oasis.opendocument.text"}, + {"odm", "application/vnd.oasis.opendocument.text-master"}, + {"ott", "application/vnd.oasis.opendocument.text-template"}, + {"oth", "application/vnd.oasis.opendocument.text-web"}, + {"pptx", "application/vnd.openxmlformats-officedocument.presentationml.presentation"}, + {"sldx", "application/vnd.openxmlformats-officedocument.presentationml.slide"}, + {"ppsx", "application/vnd.openxmlformats-officedocument.presentationml.slideshow"}, + {"potx", "application/vnd.openxmlformats-officedocument.presentationml.template"}, + {"xlsx", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"}, + {"xltx", "application/vnd.openxmlformats-officedocument.spreadsheetml.template"}, + {"docx", "application/vnd.openxmlformats-officedocument.wordprocessingml.document"}, + {"dotx", "application/vnd.openxmlformats-officedocument.wordprocessingml.template"}, + {"cod", "application/vnd.rim.cod"}, + {"mmf", "application/vnd.smaf"}, + {"sdc", "application/vnd.stardivision.calc"}, + {"sds", "application/vnd.stardivision.chart"}, + {"sda", "application/vnd.stardivision.draw"}, + {"sdd", "application/vnd.stardivision.impress"}, + {"sdf", "application/vnd.stardivision.math"}, + {"sdw", "application/vnd.stardivision.writer"}, + {"sgl", "application/vnd.stardivision.writer-global"}, + {"sxc", "application/vnd.sun.xml.calc"}, + {"stc", "application/vnd.sun.xml.calc.template"}, + {"sxd", "application/vnd.sun.xml.draw"}, + {"std", "application/vnd.sun.xml.draw.template"}, + {"sxi", "application/vnd.sun.xml.impress"}, + {"sti", "application/vnd.sun.xml.impress.template"}, + {"sxm", "application/vnd.sun.xml.math"}, + {"sxw", "application/vnd.sun.xml.writer"}, + {"sxg", "application/vnd.sun.xml.writer.global"}, + {"stw", "application/vnd.sun.xml.writer.template"}, + {"sis", "application/vnd.symbian.install"}, + {"cap", "application/vnd.tcpdump.pcap"}, + {"pcap", "application/vnd.tcpdump.pcap"}, + {"vsd", "application/vnd.visio"}, + {"vst", "application/vnd.visio"}, + {"vsw", "application/vnd.visio"}, + {"vss", "application/vnd.visio"}, + {"wbxml", "application/vnd.wap.wbxml"}, + {"wmlc", "application/vnd.wap.wmlc"}, + {"wmlsc", "application/vnd.wap.wmlscriptc"}, + {"wpd", "application/vnd.wordperfect"}, + {"wp5", "application/vnd.wordperfect5.1"}, + {"wk", "application/x-123"}, + {"7z", "application/x-7z-compressed"}, + {"abw", "application/x-abiword"}, + {"dmg", "application/x-apple-diskimage"}, + {"bcpio", "application/x-bcpio"}, + {"torrent", "application/x-bittorrent"}, + {"cab", "application/x-cab"}, + {"cbr", "application/x-cbr"}, + {"cbz", "application/x-cbz"}, + {"cdf", "application/x-cdf"}, + {"cda", "application/x-cdf"}, + {"vcd", "application/x-cdlink"}, + {"pgn", "application/x-chess-pgn"}, + {"mph", "application/x-comsol"}, + {"cpio", "application/x-cpio"}, + {"csh", "application/x-csh"}, + {"dcr", "application/x-director"}, + {"dir", "application/x-director"}, + {"dxr", "application/x-director"}, + {"dms", "application/x-dms"}, + {"wad", "application/x-doom"}, + {"dvi", "application/x-dvi"}, + {"pfa", "application/x-font"}, + {"pfb", "application/x-font"}, + {"gsf", "application/x-font"}, + {"pcf", "application/x-font-pcf"}, + {"pcf.Z", "application/x-font-pcf"}, + {"mm", "application/x-freemind"}, + {"gan", "application/x-ganttproject"}, + {"gnumeric", "application/x-gnumeric"}, + {"sgf", "application/x-go-sgf"}, + {"gcf", "application/x-graphing-calculator"}, + {"gtar", "application/x-gtar"}, + {"tgz", "application/x-gtar-compressed"}, + {"taz", "application/x-gtar-compressed"}, + {"hdf", "application/x-hdf"}, + {"hwp", "application/x-hwp"}, + {"ica", "application/x-ica"}, + {"info", "application/x-info"}, + {"ins", "application/x-internet-signup"}, + {"isp", "application/x-internet-signup"}, + {"iii", "application/x-iphone"}, + {"iso", "application/x-iso9660-image"}, + {"jam", "application/x-jam"}, + {"jnlp", "application/x-java-jnlp-file"}, + {"jmz", "application/x-jmol"}, + {"chrt", "application/x-kchart"}, + {"kil", "application/x-killustrator"}, + {"skp", "application/x-koan"}, + {"skd", "application/x-koan"}, + {"skt", "application/x-koan"}, + {"skm", "application/x-koan"}, + {"kpr", "application/x-kpresenter"}, + {"kpt", "application/x-kpresenter"}, + {"ksp", "application/x-kspread"}, + {"kwd", "application/x-kword"}, + {"kwt", "application/x-kword"}, + {"latex", "application/x-latex"}, + {"lha", "application/x-lha"}, + {"lyx", "application/x-lyx"}, + {"lzh", "application/x-lzh"}, + {"lzx", "application/x-lzx"}, + {"frm", "application/x-maker"}, + {"maker", "application/x-maker"}, + {"frame", "application/x-maker"}, + {"fm", "application/x-maker"}, + {"fb", "application/x-maker"}, + {"book", "application/x-maker"}, + {"fbdoc", "application/x-maker"}, + {"mif", "application/x-mif"}, + {"m3u8", "application/x-mpegURL"}, + {"application", "application/x-ms-application"}, + {"manifest", "application/x-ms-manifest"}, + {"wmd", "application/x-ms-wmd"}, + {"wmz", "application/x-ms-wmz"}, + {"com", "application/x-msdos-program"}, + {"exe", "application/x-msdos-program"}, + {"bat", "application/x-msdos-program"}, + {"dll", "application/x-msdos-program"}, + {"msi", "application/x-msi"}, + {"nc", "application/x-netcdf"}, + {"pac", "application/x-ns-proxy-autoconfig"}, + {"nwc", "application/x-nwc"}, + {"o", "application/x-object"}, + {"oza", "application/x-oz-application"}, + {"p7r", "application/x-pkcs7-certreqresp"}, + {"crl", "application/x-pkcs7-crl"}, + {"pyc", "application/x-python-code"}, + {"pyo", "application/x-python-code"}, + {"qgs", "application/x-qgis"}, + {"shp", "application/x-qgis"}, + {"shx", "application/x-qgis"}, + {"qtl", "application/x-quicktimeplayer"}, + {"rdp", "application/x-rdp"}, + {"rpm", "application/x-redhat-package-manager"}, + {"rss", "application/x-rss+xml"}, + {"rb", "application/x-ruby"}, + {"sci", "application/x-scilab"}, + {"sce", "application/x-scilab"}, + {"xcos", "application/x-scilab-xcos"}, + {"sh", "application/x-sh"}, + {"shar", "application/x-shar"}, + {"swf", "application/x-shockwave-flash"}, + {"swfl", "application/x-shockwave-flash"}, + {"scr", "application/x-silverlight"}, + {"sql", "application/x-sql"}, + {"sit", "application/x-stuffit"}, + {"sitx", "application/x-stuffit"}, + {"sv4cpio", "application/x-sv4cpio"}, + {"sv4crc", "application/x-sv4crc"}, + {"tar", "application/x-tar"}, + {"tcl", "application/x-tcl"}, + {"gf", "application/x-tex-gf"}, + {"pk", "application/x-tex-pk"}, + {"texinfo", "application/x-texinfo"}, + {"texi", "application/x-texinfo"}, + {"~", "application/x-trash"}, + {"%", "application/x-trash"}, + {"bak", "application/x-trash"}, + {"old", "application/x-trash"}, + {"sik", "application/x-trash"}, + {"t", "application/x-troff"}, + {"tr", "application/x-troff"}, + {"roff", "application/x-troff"}, + {"man", "application/x-troff-man"}, + {"me", "application/x-troff-me"}, + {"ms", "application/x-troff-ms"}, + {"ustar", "application/x-ustar"}, + {"src", "application/x-wais-source"}, + {"wz", "application/x-wingz"}, + {"crt", "application/x-x509-ca-cert"}, + {"xcf", "application/x-xcf"}, + {"fig", "application/x-xfig"}, + {"xpi", "application/x-xpinstall"}, + {"xz", "application/x-xz"}, + {"amr", "audio/amr"}, + {"awb", "audio/amr-wb"}, + {"axa", "audio/annodex"}, + {"au", "audio/basic"}, + {"snd", "audio/basic"}, + {"csd", "audio/csound"}, + {"orc", "audio/csound"}, + {"sco", "audio/csound"}, + {"flac", "audio/flac"}, + {"mid", "audio/midi"}, + {"midi", "audio/midi"}, + {"kar", "audio/midi"}, + {"mpga", "audio/mpeg"}, + {"mpega", "audio/mpeg"}, + {"mp2", "audio/mpeg"}, + {"mp3", "audio/mpeg"}, + {"m4a", "audio/mpeg"}, + {"m3u", "audio/mpegurl"}, + {"oga", "audio/ogg"}, + {"ogg", "audio/ogg"}, + {"opus", "audio/ogg"}, + {"spx", "audio/ogg"}, + {"sid", "audio/prs.sid"}, + {"aif", "audio/x-aiff"}, + {"aiff", "audio/x-aiff"}, + {"aifc", "audio/x-aiff"}, + {"gsm", "audio/x-gsm"}, + {"wma", "audio/x-ms-wma"}, + {"wax", "audio/x-ms-wax"}, + {"ra", "audio/x-pn-realaudio"}, + {"rm", "audio/x-pn-realaudio"}, + {"ram", "audio/x-pn-realaudio"}, + {"pls", "audio/x-scpls"}, + {"sd2", "audio/x-sd2"}, + {"wav", "audio/x-wav"}, + {"alc", "chemical/x-alchemy"}, + {"cac", "chemical/x-cache"}, + {"cache", "chemical/x-cache"}, + {"csf", "chemical/x-cache-csf"}, + {"cbin", "chemical/x-cactvs-binary"}, + {"cascii", "chemical/x-cactvs-binary"}, + {"ctab", "chemical/x-cactvs-binary"}, + {"cdx", "chemical/x-cdx"}, + {"cer", "chemical/x-cerius"}, + {"c3d", "chemical/x-chem3d"}, + {"chm", "chemical/x-chemdraw"}, + {"cif", "chemical/x-cif"}, + {"cmdf", "chemical/x-cmdf"}, + {"cml", "chemical/x-cml"}, + {"cpa", "chemical/x-compass"}, + {"bsd", "chemical/x-crossfire"}, + {"csml", "chemical/x-csml"}, + {"csm", "chemical/x-csml"}, + {"ctx", "chemical/x-ctx"}, + {"cxf", "chemical/x-cxf"}, + {"cef", "chemical/x-cxf"}, + {"emb", "chemical/x-embl-dl-nucleotide"}, + {"embl", "chemical/x-embl-dl-nucleotide"}, + {"spc", "chemical/x-galactic-spc"}, + {"inp", "chemical/x-gamess-input"}, + {"gam", "chemical/x-gamess-input"}, + {"gamin", "chemical/x-gamess-input"}, + {"fch", "chemical/x-gaussian-checkpoint"}, + {"fchk", "chemical/x-gaussian-checkpoint"}, + {"cub", "chemical/x-gaussian-cube"}, + {"gau", "chemical/x-gaussian-input"}, + {"gjc", "chemical/x-gaussian-input"}, + {"gjf", "chemical/x-gaussian-input"}, + {"gal", "chemical/x-gaussian-log"}, + {"gcg", "chemical/x-gcg8-sequence"}, + {"gen", "chemical/x-genbank"}, + {"hin", "chemical/x-hin"}, + {"istr", "chemical/x-isostar"}, + {"ist", "chemical/x-isostar"}, + {"jdx", "chemical/x-jcamp-dx"}, + {"dx", "chemical/x-jcamp-dx"}, + {"kin", "chemical/x-kinemage"}, + {"mcm", "chemical/x-macmolecule"}, + {"mmd", "chemical/x-macromodel-input"}, + {"mmod", "chemical/x-macromodel-input"}, + {"mol", "chemical/x-mdl-molfile"}, + {"rd", "chemical/x-mdl-rdfile"}, + {"rxn", "chemical/x-mdl-rxnfile"}, + {"sd", "chemical/x-mdl-sdfile"}, + {"tgf", "chemical/x-mdl-tgf"}, + {"mcif", "chemical/x-mmcif"}, + {"mol2", "chemical/x-mol2"}, + {"b", "chemical/x-molconn-Z"}, + {"gpt", "chemical/x-mopac-graph"}, + {"mop", "chemical/x-mopac-input"}, + {"mopcrt", "chemical/x-mopac-input"}, + {"mpc", "chemical/x-mopac-input"}, + {"zmt", "chemical/x-mopac-input"}, + {"moo", "chemical/x-mopac-out"}, + {"mvb", "chemical/x-mopac-vib"}, + {"asn", "chemical/x-ncbi-asn1"}, + {"prt", "chemical/x-ncbi-asn1-ascii"}, + {"ent", "chemical/x-ncbi-asn1-ascii"}, + {"val", "chemical/x-ncbi-asn1-binary"}, + {"aso", "chemical/x-ncbi-asn1-binary"}, + {"pdb", "chemical/x-pdb"}, + {"ros", "chemical/x-rosdal"}, + {"sw", "chemical/x-swissprot"}, + {"vms", "chemical/x-vamas-iso14976"}, + {"vmd", "chemical/x-vmd"}, + {"xtel", "chemical/x-xtel"}, + {"xyz", "chemical/x-xyz"}, + {"gif", "image/gif"}, + {"ief", "image/ief"}, + {"jp2", "image/jp2"}, + {"jpg2", "image/jp2"}, + {"jpeg", "image/jpeg"}, + {"jpg", "image/jpeg"}, + {"jpe", "image/jpeg"}, + {"jpm", "image/jpm"}, + {"jpx", "image/jpx"}, + {"jpf", "image/jpx"}, + {"pcx", "image/pcx"}, + {"png", "image/png"}, + {"svg", "image/svg+xml"}, + {"svgz", "image/svg+xml"}, + {"tiff", "image/tiff"}, + {"tif", "image/tiff"}, + {"djvu", "image/vnd.djvu"}, + {"djv", "image/vnd.djvu"}, + {"ico", "image/vnd.microsoft.icon"}, + {"wbmp", "image/vnd.wap.wbmp"}, + {"cr2", "image/x-canon-cr2"}, + {"crw", "image/x-canon-crw"}, + {"ras", "image/x-cmu-raster"}, + {"cdr", "image/x-coreldraw"}, + {"pat", "image/x-coreldrawpattern"}, + {"cdt", "image/x-coreldrawtemplate"}, + {"erf", "image/x-epson-erf"}, + {"art", "image/x-jg"}, + {"jng", "image/x-jng"}, + {"bmp", "image/x-ms-bmp"}, + {"nef", "image/x-nikon-nef"}, + {"orf", "image/x-olympus-orf"}, + {"psd", "image/x-photoshop"}, + {"pnm", "image/x-portable-anymap"}, + {"pbm", "image/x-portable-bitmap"}, + {"pgm", "image/x-portable-graymap"}, + {"ppm", "image/x-portable-pixmap"}, + {"rgb", "image/x-rgb"}, + {"xbm", "image/x-xbitmap"}, + {"xpm", "image/x-xpixmap"}, + {"xwd", "image/x-xwindowdump"}, + {"eml", "message/rfc822"}, + {"igs", "model/iges"}, + {"iges", "model/iges"}, + {"msh", "model/mesh"}, + {"mesh", "model/mesh"}, + {"silo", "model/mesh"}, + {"wrl", "model/vrml"}, + {"vrml", "model/vrml"}, + {"x3dv", "model/x3d+vrml"}, + {"x3d", "model/x3d+xml"}, + {"x3db", "model/x3d+binary"}, + {"appcache", "text/cache-manifest"}, + {"ics", "text/calendar"}, + {"icz", "text/calendar"}, + {"css", "text/css"}, + {"csv", "text/csv"}, + {"323", "text/h323"}, + {"html", "text/html"}, + {"htm", "text/html"}, + {"shtml", "text/html"}, + {"uls", "text/iuls"}, + {"mml", "text/mathml"}, + {"md", "text/markdown"}, + {"markdown", "text/markdown"}, + {"asc", "text/plain"}, + {"txt", "text/plain"}, + {"text", "text/plain"}, + {"pot", "text/plain"}, + {"brf", "text/plain"}, + {"srt", "text/plain"}, + {"rtx", "text/richtext"}, + {"sct", "text/scriptlet"}, + {"wsc", "text/scriptlet"}, + {"tm", "text/texmacs"}, + {"tsv", "text/tab-separated-values"}, + {"ttl", "text/turtle"}, + {"vcf", "text/vcard"}, + {"vcard", "text/vcard"}, + {"jad", "text/vnd.sun.j2me.app-descriptor"}, + {"wml", "text/vnd.wap.wml"}, + {"wmls", "text/vnd.wap.wmlscript"}, + {"bib", "text/x-bibtex"}, + {"boo", "text/x-boo"}, + {"h++", "text/x-c++hdr"}, + {"hpp", "text/x-c++hdr"}, + {"hxx", "text/x-c++hdr"}, + {"hh", "text/x-c++hdr"}, + {"c++", "text/x-c++src"}, + {"cpp", "text/x-c++src"}, + {"cxx", "text/x-c++src"}, + {"cc", "text/x-c++src"}, + {"h", "text/x-chdr"}, + {"htc", "text/x-component"}, + {"c", "text/x-csrc"}, + {"d", "text/x-dsrc"}, + {"diff", "text/x-diff"}, + {"patch", "text/x-diff"}, + {"hs", "text/x-haskell"}, + {"java", "text/x-java"}, + {"ly", "text/x-lilypond"}, + {"lhs", "text/x-literate-haskell"}, + {"moc", "text/x-moc"}, + {"p", "text/x-pascal"}, + {"pas", "text/x-pascal"}, + {"gcd", "text/x-pcs-gcd"}, + {"pl", "text/x-perl"}, + {"pm", "text/x-perl"}, + {"py", "text/x-python"}, + {"scala", "text/x-scala"}, + {"etx", "text/x-setext"}, + {"sfv", "text/x-sfv"}, + {"tk", "text/x-tcl"}, + {"tex", "text/x-tex"}, + {"ltx", "text/x-tex"}, + {"sty", "text/x-tex"}, + {"cls", "text/x-tex"}, + {"vcs", "text/x-vcalendar"}, + {"3gp", "video/3gpp"}, + {"axv", "video/annodex"}, + {"dl", "video/dl"}, + {"dif", "video/dv"}, + {"dv", "video/dv"}, + {"fli", "video/fli"}, + {"gl", "video/gl"}, + {"mpeg", "video/mpeg"}, + {"mpg", "video/mpeg"}, + {"mpe", "video/mpeg"}, + {"ts", "video/MP2T"}, + {"mp4", "video/mp4"}, + {"qt", "video/quicktime"}, + {"mov", "video/quicktime"}, + {"ogv", "video/ogg"}, + {"webm", "video/webm"}, + {"mxu", "video/vnd.mpegurl"}, + {"flv", "video/x-flv"}, + {"lsf", "video/x-la-asf"}, + {"lsx", "video/x-la-asf"}, + {"mng", "video/x-mng"}, + {"asf", "video/x-ms-asf"}, + {"asx", "video/x-ms-asf"}, + {"wm", "video/x-ms-wm"}, + {"wmv", "video/x-ms-wmv"}, + {"wmx", "video/x-ms-wmx"}, + {"wvx", "video/x-ms-wvx"}, + {"avi", "video/x-msvideo"}, + {"movie", "video/x-sgi-movie"}, + {"mpv", "video/x-matroska"}, + {"mkv", "video/x-matroska"}, + {"ice", "x-conference/x-cooltalk"}, + {"sisx", "x-epoc/x-sisx-app"}, + {"vrm", "x-world/x-vrml"} +}; + +std::string find_mime_type(const std::string& ext) { + std::map::const_iterator it = mime_map.find(ext); + if (it == mime_map.end()) { + return ""; + } + + return it->second; +} diff --git a/src/mime.h b/src/mime.h new file mode 100644 index 00000000..d8f09519 --- /dev/null +++ b/src/mime.h @@ -0,0 +1,4 @@ +#include + +// Given a file extension, get the corresponding mime type +std::string find_mime_type(const std::string& ext); diff --git a/src/utils.h b/src/utils.h index 4015c50a..18608f76 100644 --- a/src/utils.h +++ b/src/utils.h @@ -90,4 +90,29 @@ std::string toString(T x) { std::map toStringMap(Rcpp::CharacterVector x); Rcpp::CharacterVector toCharacterVector(const std::map& strmap); + +// Given a path, return just the filename part. +inline std::string basename(const std::string &path) { + // TODO: handle Windows separators + size_t found_idx = path.find_last_of('/'); + + if (found_idx <= 0) { + return path; + } else { + return path.substr(found_idx + 1); + } +} + +// Given a filename, return the extension. +inline std::string find_extension(const std::string &filename) { + size_t found_idx = filename.find_last_of('.'); + + if (found_idx <= 0) { + return ""; + } else { + return filename.substr(found_idx + 1); + } +} + + #endif diff --git a/src/webapplication.cpp b/src/webapplication.cpp index 381d2782..197b60a8 100644 --- a/src/webapplication.cpp +++ b/src/webapplication.cpp @@ -6,6 +6,7 @@ #include "http.h" #include "thread.h" #include "utils.h" +#include "mime.h" #include std::string normalizeHeaderName(const std::string& name) { @@ -493,6 +494,7 @@ boost::shared_ptr RWebApplication::staticFileResponse( } int file_size = pDataSource->size(); + std::string mime_type = find_mime_type(find_extension(filename)); if (method == "HEAD") { // For HEAD requests, we created the FileDataSource to get the size and @@ -506,13 +508,15 @@ boost::shared_ptr RWebApplication::staticFileResponse( auto_deleter_background ); + ResponseHeaders& respHeaders = pResponse->headers(); // Set the Content-Length here so that both GET and HEAD requests will get // it. If we didn't set it here, the response for the GET would // automatically set the Content-Length (by using the FileDataSource), but // the response for the HEAD would not. - pResponse->headers().push_back( - std::make_pair("Content-Length", toString(file_size)) - ); + respHeaders.push_back(std::make_pair("Content-Length", toString(file_size))); + if (mime_type != "") { + respHeaders.push_back(std::make_pair("Content-Type", mime_type)); + } return pResponse; } diff --git a/tools/update_mime.R b/tools/update_mime.R new file mode 100644 index 00000000..6591f9ee --- /dev/null +++ b/tools/update_mime.R @@ -0,0 +1,41 @@ + +library(rprojroot) + +dest_file <- rprojroot::find_package_root_file("src", "mime.cpp") + +mime_map_text <- mapply( + function(ext, mime_type) { + sprintf(' {"%s", "%s"}', ext, mime_type); + }, + names(mime::mimemap), + mime::mimemap, + SIMPLIFY = TRUE, + USE.NAMES = FALSE +) +mime_map_text <- paste(mime_map_text, collapse = ",\n") + + +output <- glue(.open = "[", .close = "]", ' +// Do not edit. +// Generated by ../tools/update_mime.R using R package mime [packageVersion("mime")] + +#include +#include + +const std::map mime_map = { +[mime_map_text] +}; + +std::string find_mime_type(const std::string& ext) { + std::map::const_iterator it = mime_map.find(ext); + if (it == mime_map.end()) { + return ""; + } + + return it->second; +} + +' +) + +cat(output, file = dest_file) From 6a9d5a6e550b830059f668279a747040c49a617b Mon Sep 17 00:00:00 2001 From: Winston Chang Date: Tue, 16 Oct 2018 22:35:00 -0500 Subject: [PATCH 13/60] Bump version to 1.4.5.9001 --- DESCRIPTION | 2 +- NEWS.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/DESCRIPTION b/DESCRIPTION index adc940e9..f4ac829d 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -1,7 +1,7 @@ Package: httpuv Type: Package Title: HTTP and WebSocket Server Library -Version: 1.4.5.9000 +Version: 1.4.5.9001 Author: Joe Cheng, Hector Corrada Bravo [ctb], Jeroen Ooms [ctb], Winston Chang [ctb] Copyright: RStudio, Inc.; Joyent, Inc.; Nginx Inc.; Igor Sysoev; Niels Provos; diff --git a/NEWS.md b/NEWS.md index 1d7e22ea..b5acb8b6 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,4 +1,4 @@ -httpuv 1.4.5.9000 +httpuv 1.4.5.9001 ============ * Fixed [#168](https://github.com/rstudio/httpuv/issues/168): A SIGPIPE signal on the httpuv background thread could cause the process to quit. This can happen in some instances when the server is under heavy load. ([#169](https://github.com/rstudio/httpuv/pull/169)) From af2da8f73107719a1d2e48f85113274d293e1dbd Mon Sep 17 00:00:00 2001 From: Winston Chang Date: Wed, 17 Oct 2018 09:35:35 -0500 Subject: [PATCH 14/60] Remove unused function --- src/webapplication.cpp | 9 --------- src/webapplication.h | 2 -- 2 files changed, 11 deletions(-) diff --git a/src/webapplication.cpp b/src/webapplication.cpp index 197b60a8..94340663 100644 --- a/src/webapplication.cpp +++ b/src/webapplication.cpp @@ -430,15 +430,6 @@ std::pair RWebApplication::_matchStaticPath(const std: } -bool RWebApplication::isStaticPath(const std::string& url_path) { - ASSERT_BACKGROUND_THREAD() - if (_matchStaticPath(url_path).first == "") - return false; - else - return true; -} - - boost::shared_ptr error_response(boost::shared_ptr pRequest, int code) { std::string description = getStatusDescription(code); std::string content = toString(code) + " " + description + "\n"; diff --git a/src/webapplication.h b/src/webapplication.h index ab785270..d912f8f7 100644 --- a/src/webapplication.h +++ b/src/webapplication.h @@ -27,7 +27,6 @@ class WebApplication { boost::shared_ptr > data, boost::function error_callback) = 0; virtual void onWSClose(boost::shared_ptr) = 0; - virtual bool isStaticPath(const std::string& url_path) = 0; virtual boost::shared_ptr staticFileResponse( boost::shared_ptr pRequest) = 0; // Get current set of static paths. @@ -86,7 +85,6 @@ class RWebApplication : public WebApplication { boost::function error_callback); virtual void onWSClose(boost::shared_ptr conn); - virtual bool isStaticPath(const std::string& url_path); virtual boost::shared_ptr staticFileResponse( boost::shared_ptr pRequest); virtual std::map getStaticPaths() const; From 6f9087f14df6c15a968c56fc8a12d4281b08e03c Mon Sep 17 00:00:00 2001 From: Winston Chang Date: Wed, 17 Oct 2018 09:37:07 -0500 Subject: [PATCH 15/60] Decode URL for static paths --- src/httpuv.h | 3 +++ src/webapplication.cpp | 3 ++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/httpuv.h b/src/httpuv.h index 4e735661..097fefb4 100644 --- a/src/httpuv.h +++ b/src/httpuv.h @@ -7,4 +7,7 @@ void invokeCppCallback(Rcpp::List data, SEXP callback_xptr); +std::string doEncodeURI(std::string value, bool encodeReserved); +std::string doDecodeURI(std::string value, bool component); + #endif diff --git a/src/webapplication.cpp b/src/webapplication.cpp index 94340663..92d59895 100644 --- a/src/webapplication.cpp +++ b/src/webapplication.cpp @@ -451,7 +451,8 @@ boost::shared_ptr RWebApplication::staticFileResponse( ) { ASSERT_BACKGROUND_THREAD() - std::pair static_path = _matchStaticPath(pRequest->url()); + std::string url_path = doDecodeURI(pRequest->url(), true); + std::pair static_path = _matchStaticPath(url_path); std::string local_dirname = static_path.first; std::string filename = static_path.second; From 149970ba41d122f395e41bbee880a6c9301193fa Mon Sep 17 00:00:00 2001 From: Winston Chang Date: Wed, 17 Oct 2018 09:42:39 -0500 Subject: [PATCH 16/60] Use application/octet-stream as default mime type --- src/webapplication.cpp | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/webapplication.cpp b/src/webapplication.cpp index 92d59895..f549881a 100644 --- a/src/webapplication.cpp +++ b/src/webapplication.cpp @@ -487,6 +487,9 @@ boost::shared_ptr RWebApplication::staticFileResponse( int file_size = pDataSource->size(); std::string mime_type = find_mime_type(find_extension(filename)); + if (mime_type == "") { + mime_type = "application/octet-stream"; + } if (method == "HEAD") { // For HEAD requests, we created the FileDataSource to get the size and @@ -506,9 +509,7 @@ boost::shared_ptr RWebApplication::staticFileResponse( // automatically set the Content-Length (by using the FileDataSource), but // the response for the HEAD would not. respHeaders.push_back(std::make_pair("Content-Length", toString(file_size))); - if (mime_type != "") { - respHeaders.push_back(std::make_pair("Content-Type", mime_type)); - } + respHeaders.push_back(std::make_pair("Content-Type", mime_type)); return pResponse; } From 0fed3bc88a093838a3bf3608ce710435b660953f Mon Sep 17 00:00:00 2001 From: Winston Chang Date: Wed, 17 Oct 2018 09:59:18 -0500 Subject: [PATCH 17/60] Use numeric indexing for list of running servers --- R/server.R | 23 +++++++++-------------- 1 file changed, 9 insertions(+), 14 deletions(-) diff --git a/R/server.R b/R/server.R index ff746d35..bac81113 100644 --- a/R/server.R +++ b/R/server.R @@ -11,6 +11,7 @@ Server <- R6Class("Server", stopServer_(private$handle) private$running <- FALSE deregisterServer(self) + invisible() }, isRunning = function() { # This doesn't map exactly to whether the app is running, since the @@ -65,9 +66,6 @@ WebServer <- R6Class("WebServer", private$running <- TRUE registerServer(self) - }, - getID = function() { - paste0(private$host, ":", private$port) } ), private = list( @@ -104,9 +102,6 @@ PipeServer <- R6Class("PipeServer", if (is.null(private$handle)) { stop("Failed to create server") } - }, - getID = function() { - private$name } ), private = list( @@ -160,18 +155,18 @@ listServers <- function() { } registerServer <- function(server) { - id <- server$getID() - .globals$servers[[id]] <- server + .globals$servers[[length(.globals$servers) + 1]] <- server } deregisterServer <- function(server) { - if (is.character(server)) { - id <- server - } else { - # Assume it's a Server object - id <- server$getID() + for (i in seq_along(.globals$servers)) { + if (identical(server, .globals$servers[[i]])) { + .globals$servers[[i]] <- NULL + return() + } } - .globals$servers[[id]] <- NULL + + warning("Unable to deregister server: server not found in list of running servers.") } From 5a104ddacfcbfbba994df728744264f3eaf24841 Mon Sep 17 00:00:00 2001 From: Winston Chang Date: Wed, 17 Oct 2018 10:03:01 -0500 Subject: [PATCH 18/60] Fixes for R CMD check --- NAMESPACE | 3 --- R/RcppExports.R | 1 - R/server.R | 16 +++++++++------- man/listServers.Rd | 11 +++++++++++ man/stopDaemonizedServer.Rd | 4 ++++ man/stopServer.Rd | 6 +++--- src/httpuv.cpp | 1 - 7 files changed, 27 insertions(+), 15 deletions(-) create mode 100644 man/listServers.Rd diff --git a/NAMESPACE b/NAMESPACE index e7d12e97..23adcc66 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -1,13 +1,10 @@ # Generated by roxygen2: do not edit by hand -export(PipeServer) -export(WebServer) export(decodeURI) export(decodeURIComponent) export(encodeURI) export(encodeURIComponent) export(getRNGState) -export(getStaticPaths_) export(interrupt) export(ipFamily) export(listServers) diff --git a/R/RcppExports.R b/R/RcppExports.R index d74c3298..72aa7df7 100644 --- a/R/RcppExports.R +++ b/R/RcppExports.R @@ -21,7 +21,6 @@ stopServer_ <- function(handle) { invisible(.Call('_httpuv_stopServer_', PACKAGE = 'httpuv', handle)) } -#' @export getStaticPaths_ <- function(handle) { .Call('_httpuv_getStaticPaths_', PACKAGE = 'httpuv', handle) } diff --git a/R/server.R b/R/server.R index bac81113..03213d05 100644 --- a/R/server.R +++ b/R/server.R @@ -39,7 +39,6 @@ Server <- R6Class("Server", ) -#' @export WebServer <- R6Class("WebServer", cloneable = FALSE, inherit = Server, @@ -75,7 +74,6 @@ WebServer <- R6Class("WebServer", ) -#' @export PipeServer <- R6Class("PipeServer", cloneable = FALSE, inherit = Server, @@ -112,12 +110,12 @@ PipeServer <- R6Class("PipeServer", #' Stop a server -#' -#' Given a handle that was returned from a previous invocation of +#' +#' Given a server object that was returned from a previous invocation of #' \code{\link{startServer}} or \code{\link{startPipeServer}}, this closes all -#' open connections for that server and unbinds the port. -#' -#' @param handle A handle that was previously returned from +#' open connections for that server and unbinds the port. +#' +#' @param server A server object that was previously returned from #' \code{\link{startServer}} or \code{\link{startPipeServer}}. #' #' @seealso \code{\link{stopAllServers}} to stop all servers. @@ -149,6 +147,10 @@ stopAllServers <- function() { .globals$servers <- list() +#' List all running httpuv servers +#' +#' This returns a list of all running httpuv server applications. +#' #' @export listServers <- function() { .globals$servers diff --git a/man/listServers.Rd b/man/listServers.Rd new file mode 100644 index 00000000..7d2433bd --- /dev/null +++ b/man/listServers.Rd @@ -0,0 +1,11 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/server.R +\name{listServers} +\alias{listServers} +\title{List all running httpuv servers} +\usage{ +listServers() +} +\description{ +This returns a list of all running httpuv server applications. +} diff --git a/man/stopDaemonizedServer.Rd b/man/stopDaemonizedServer.Rd index 07817804..b4b07db8 100644 --- a/man/stopDaemonizedServer.Rd +++ b/man/stopDaemonizedServer.Rd @@ -6,6 +6,10 @@ \usage{ stopDaemonizedServer(server) } +\arguments{ +\item{server}{A server object that was previously returned from +\code{\link{startServer}} or \code{\link{startPipeServer}}.} +} \description{ This function will be removed in a future release of httpuv. Instead, use \code{\link{stopServer}}. diff --git a/man/stopServer.Rd b/man/stopServer.Rd index e248938b..f9493303 100644 --- a/man/stopServer.Rd +++ b/man/stopServer.Rd @@ -7,13 +7,13 @@ stopServer(server) } \arguments{ -\item{handle}{A handle that was previously returned from +\item{server}{A server object that was previously returned from \code{\link{startServer}} or \code{\link{startPipeServer}}.} } \description{ -Given a handle that was returned from a previous invocation of +Given a server object that was returned from a previous invocation of \code{\link{startServer}} or \code{\link{startPipeServer}}, this closes all -open connections for that server and unbinds the port. +open connections for that server and unbinds the port. } \seealso{ \code{\link{stopAllServers}} to stop all servers. diff --git a/src/httpuv.cpp b/src/httpuv.cpp index 658ea1d1..ce6a448b 100644 --- a/src/httpuv.cpp +++ b/src/httpuv.cpp @@ -394,7 +394,6 @@ boost::shared_ptr get_pWebApplication(std::string handle) { return get_pWebApplication(pServer); } -//' @export // [[Rcpp::export]] Rcpp::CharacterVector getStaticPaths_(std::string handle) { ASSERT_MAIN_THREAD() From 1301eb2c358c6d0c00bea18e4434a5e7243add94 Mon Sep 17 00:00:00 2001 From: Winston Chang Date: Thu, 18 Oct 2018 13:46:12 -0500 Subject: [PATCH 19/60] Add staticPath class for finer control --- DESCRIPTION | 1 + NAMESPACE | 3 + R/RcppExports.R | 4 +- R/httpuv.R | 2 +- R/server.R | 4 +- R/static_paths.R | 97 ++++++++++++++++++++++++++++ R/utils.R | 42 ------------ man/staticPath.Rd | 24 +++++++ src/RcppExports.cpp | 16 ++--- src/httpuv.cpp | 19 +++--- src/staticpaths.cpp | 142 +++++++++++++++++++++++++++++++++++++++++ src/staticpaths.h | 48 ++++++++++++++ src/utils.cpp | 49 -------------- src/utils.h | 36 +++++++++-- src/webapplication.cpp | 69 ++++++-------------- src/webapplication.h | 23 ++----- 16 files changed, 394 insertions(+), 185 deletions(-) create mode 100644 R/static_paths.R create mode 100644 man/staticPath.Rd create mode 100644 src/staticpaths.cpp create mode 100644 src/staticpaths.h diff --git a/DESCRIPTION b/DESCRIPTION index f4ac829d..7f858694 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -35,4 +35,5 @@ Collate: 'RcppExports.R' 'httpuv.R' 'server.R' + 'static_paths.R' 'utils.R' diff --git a/NAMESPACE b/NAMESPACE index 23adcc66..b6945439 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -1,5 +1,7 @@ # Generated by roxygen2: do not edit by hand +S3method(format,staticPath) +S3method(print,staticPath) export(decodeURI) export(decodeURIComponent) export(encodeURI) @@ -14,6 +16,7 @@ export(service) export(startDaemonizedServer) export(startPipeServer) export(startServer) +export(staticPath) export(stopAllServers) export(stopDaemonizedServer) export(stopServer) diff --git a/R/RcppExports.R b/R/RcppExports.R index 72aa7df7..c4d4db46 100644 --- a/R/RcppExports.R +++ b/R/RcppExports.R @@ -25,8 +25,8 @@ getStaticPaths_ <- function(handle) { .Call('_httpuv_getStaticPaths_', PACKAGE = 'httpuv', handle) } -addStaticPaths_ <- function(handle, paths) { - .Call('_httpuv_addStaticPaths_', PACKAGE = 'httpuv', handle, paths) +setStaticPaths_ <- function(handle, sp) { + .Call('_httpuv_setStaticPaths_', PACKAGE = 'httpuv', handle, sp) } removeStaticPaths_ <- function(handle, paths) { diff --git a/R/httpuv.R b/R/httpuv.R index 857e9200..5fd589b9 100644 --- a/R/httpuv.R +++ b/R/httpuv.R @@ -171,7 +171,7 @@ AppWrapper <- setRefClass( .app = 'ANY', .wsconns = 'environment', .supportsOnHeaders = 'logical', - .staticPaths = 'character' + .staticPaths = 'list' ), methods = list( initialize = function(app) { diff --git a/R/server.R b/R/server.R index 03213d05..a2ddb2fa 100644 --- a/R/server.R +++ b/R/server.R @@ -22,9 +22,9 @@ Server <- R6Class("Server", getStaticPaths = function() { getStaticPaths_(private$handle) }, - addStaticPaths = function(paths) { + setStaticPaths = function(paths) { paths <- normalizeStaticPaths(paths) - invisible(addStaticPaths_(private$handle, paths)) + invisible(setStaticPaths_(private$handle, paths)) }, removeStaticPaths = function(paths) { paths <- as.character(paths) diff --git a/R/static_paths.R b/R/static_paths.R new file mode 100644 index 00000000..c1bc56bf --- /dev/null +++ b/R/static_paths.R @@ -0,0 +1,97 @@ +#' Create a staticPath object +#' +#' This function creates a \code{staticPath} object. +#' +#' @param path The local path. +#' @param index If an index.html file is present, should it be served up when +#' the client requests \code{/} or any subdirectory? +#' @param fallthrough With the default value, \code{FALSE}, if a request is made +#' for a file that doesn't exist, then httpuv will immediately send a 404 +#' response from the background I/O thread, without needing to call back into +#' the main R thread. This offers the best performance. If the value is +#' \code{TRUE}, then instead of sending a 404 response, httpuv will call the +#' application's \code{call} function, and allow it to handle the request. +#' @export +staticPath <- function(path, index = TRUE, fallthrough = FALSE) { + if (!is.character(path) || length(path) != 1 || path == "") { + stop("`path` must be a non-empty string.") + } + + path <- normalizePath(path, mustWork = TRUE) + + structure( + list( + path = path, + index = index, + fallthrough = fallthrough + ), + class = "staticPath" + ) +} + +as.staticPath <- function(path) { + UseMethod("as.staticPath", path) +} + +as.staticPath.staticPath <- function(path) { + path +} + +as.staticPath.character <- function(path) { + staticPath(path) +} + +as.staticPath.default <- function(path) { + stop("Cannot convert object of class ", class(path), " to a staticPath object.") +} + +#' @export +print.staticPath <- function(x, ...) { + cat(format(x, ...), sep = "\n") + invisible(x) +} + +#' @export +format.staticPath <- function(x, ...) { + ret <- paste0( + "\n", + " Local path: ", x$path, "\n", + " Use index.html: ", x$index, "\n", + " Fallthrough to R: ", x$fallthrough + ) +} + +# This function always returns a named list of staticPath objects. The names +# will all start with "/". The input can be a named character vector or a +# named list containing a mix of strings and staticPath objects. +normalizeStaticPaths <- function(paths) { + if (is.null(paths) || length(paths) == 0) { + return(list()) + } + + if (any_unnamed(paths)) { + stop("paths must be a named character vector, a named list containing strings and staticPath objects, or NULL.") + } + + if (!is.character(paths) && !is.list(paths)) { + stop("paths must be a named character vector, a named list containing strings and staticPath objects, or NULL.") + } + + # Convert to list of staticPath objects. Need this verbose wrapping of + # as.staticPath because of S3 dispatch for non-registered methods. + paths <- lapply(paths, function(path) as.staticPath(path)) + + # Make sure URL paths have a leading '/'. + names(paths) <- vapply(names(paths), function(path) { + if (path == "") { + stop("All paths must be non-empty strings.") + } + # Ensure there's a leading / for every path + if (substr(path, 1, 1) != "/") { + path <- paste0("/", path) + } + path + }, "") + + paths +} diff --git a/R/utils.R b/R/utils.R index ba14884a..f6073557 100644 --- a/R/utils.R +++ b/R/utils.R @@ -32,45 +32,3 @@ any_unnamed <- function(x) { # List with name attribute; check for any "" any(!nzchar(nms)) } - - -# This function always returns a named character vector of path mappings. The -# names will all start with "/". -normalizeStaticPaths <- function(paths) { - if (is.null(paths) || length(paths) == 0) { - return(empty_named_vec()) - } - - if (any_unnamed(paths)) { - stop("paths must be a named character vector, a named list of strings, or NULL.") - } - - # If list, convert to vector. - if (is.list(paths)) { - paths <- unlist(paths, recursive = FALSE) - } - - if (!is.character(paths)) { - stop("paths must be a named character vector, a named list of strings, or NULL.") - } - - # If we got here, it is a named character vector. - - # Make sure paths have a leading '/'. Save in a separate var because - # we later call normalizePath(), which drops names. - path_names <- vapply(names(paths), function(path) { - if (path == "") { - stop("All paths must be non-empty strings.") - } - # Ensure there's a leading / for every path - if (substr(path, 1, 1) != "/") { - path <- paste0("/", path) - } - path - }, "") - - paths <- normalizePath(paths, mustWork = TRUE) - names(paths) <- path_names - - paths -} \ No newline at end of file diff --git a/man/staticPath.Rd b/man/staticPath.Rd new file mode 100644 index 00000000..7b22ec93 --- /dev/null +++ b/man/staticPath.Rd @@ -0,0 +1,24 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/static_paths.R +\name{staticPath} +\alias{staticPath} +\title{Create a staticPath object} +\usage{ +staticPath(path, index = TRUE, fallthrough = FALSE) +} +\arguments{ +\item{path}{The local path.} + +\item{index}{If an index.html file is present, should it be served up when +the client requests \code{/} or any subdirectory?} + +\item{fallthrough}{With the default value, \code{FALSE}, if a request is made +for a file that doesn't exist, then httpuv will immediately send a 404 +response from the background I/O thread, without needing to call back into +the main R thread. This offers the best performance. If the value is +\code{TRUE}, then instead of sending a 404 response, httpuv will call the +application's \code{call} function, and allow it to handle the request.} +} +\description{ +This function creates a \code{staticPath} object. +} diff --git a/src/RcppExports.cpp b/src/RcppExports.cpp index ed6ab846..f1173577 100644 --- a/src/RcppExports.cpp +++ b/src/RcppExports.cpp @@ -78,7 +78,7 @@ BEGIN_RCPP END_RCPP } // getStaticPaths_ -Rcpp::CharacterVector getStaticPaths_(std::string handle); +Rcpp::List getStaticPaths_(std::string handle); RcppExport SEXP _httpuv_getStaticPaths_(SEXP handleSEXP) { BEGIN_RCPP Rcpp::RObject rcpp_result_gen; @@ -88,20 +88,20 @@ BEGIN_RCPP return rcpp_result_gen; END_RCPP } -// addStaticPaths_ -Rcpp::CharacterVector addStaticPaths_(std::string handle, Rcpp::CharacterVector paths); -RcppExport SEXP _httpuv_addStaticPaths_(SEXP handleSEXP, SEXP pathsSEXP) { +// setStaticPaths_ +Rcpp::List setStaticPaths_(std::string handle, Rcpp::List sp); +RcppExport SEXP _httpuv_setStaticPaths_(SEXP handleSEXP, SEXP spSEXP) { BEGIN_RCPP Rcpp::RObject rcpp_result_gen; Rcpp::RNGScope rcpp_rngScope_gen; Rcpp::traits::input_parameter< std::string >::type handle(handleSEXP); - Rcpp::traits::input_parameter< Rcpp::CharacterVector >::type paths(pathsSEXP); - rcpp_result_gen = Rcpp::wrap(addStaticPaths_(handle, paths)); + Rcpp::traits::input_parameter< Rcpp::List >::type sp(spSEXP); + rcpp_result_gen = Rcpp::wrap(setStaticPaths_(handle, sp)); return rcpp_result_gen; END_RCPP } // removeStaticPaths_ -Rcpp::CharacterVector removeStaticPaths_(std::string handle, Rcpp::CharacterVector paths); +Rcpp::List removeStaticPaths_(std::string handle, Rcpp::CharacterVector paths); RcppExport SEXP _httpuv_removeStaticPaths_(SEXP handleSEXP, SEXP pathsSEXP) { BEGIN_RCPP Rcpp::RObject rcpp_result_gen; @@ -219,7 +219,7 @@ static const R_CallMethodDef CallEntries[] = { {"_httpuv_makePipeServer", (DL_FUNC) &_httpuv_makePipeServer, 9}, {"_httpuv_stopServer_", (DL_FUNC) &_httpuv_stopServer_, 1}, {"_httpuv_getStaticPaths_", (DL_FUNC) &_httpuv_getStaticPaths_, 1}, - {"_httpuv_addStaticPaths_", (DL_FUNC) &_httpuv_addStaticPaths_, 2}, + {"_httpuv_setStaticPaths_", (DL_FUNC) &_httpuv_setStaticPaths_, 2}, {"_httpuv_removeStaticPaths_", (DL_FUNC) &_httpuv_removeStaticPaths_, 2}, {"_httpuv_base64encode", (DL_FUNC) &_httpuv_base64encode, 1}, {"_httpuv_encodeURI", (DL_FUNC) &_httpuv_encodeURI, 1}, diff --git a/src/httpuv.cpp b/src/httpuv.cpp index ce6a448b..81b1b78c 100644 --- a/src/httpuv.cpp +++ b/src/httpuv.cpp @@ -395,26 +395,23 @@ boost::shared_ptr get_pWebApplication(std::string handle) { } // [[Rcpp::export]] -Rcpp::CharacterVector getStaticPaths_(std::string handle) { +Rcpp::List getStaticPaths_(std::string handle) { ASSERT_MAIN_THREAD() - std::map paths = get_pWebApplication(handle)->getStaticPaths(); - return toCharacterVector(paths); + return get_pWebApplication(handle)->getStaticPaths().asRObject(); } // [[Rcpp::export]] -Rcpp::CharacterVector addStaticPaths_(std::string handle, Rcpp::CharacterVector paths) { +Rcpp::List setStaticPaths_(std::string handle, Rcpp::List sp) { ASSERT_MAIN_THREAD() - std::map new_paths = toStringMap(paths); - std::map all_paths = get_pWebApplication(handle)->addStaticPaths(new_paths); - return toCharacterVector(all_paths); + get_pWebApplication(handle)->getStaticPaths().set(sp); + return getStaticPaths_(handle); } // [[Rcpp::export]] -Rcpp::CharacterVector removeStaticPaths_(std::string handle, Rcpp::CharacterVector paths) { +Rcpp::List removeStaticPaths_(std::string handle, Rcpp::CharacterVector paths) { ASSERT_MAIN_THREAD() - std::vector rm_paths = Rcpp::as>(paths); - std::map all_paths = get_pWebApplication(handle)->removeStaticPaths(rm_paths); - return toCharacterVector(all_paths); + get_pWebApplication(handle)->getStaticPaths().remove(paths); + return getStaticPaths_(handle); } diff --git a/src/staticpaths.cpp b/src/staticpaths.cpp new file mode 100644 index 00000000..6998a890 --- /dev/null +++ b/src/staticpaths.cpp @@ -0,0 +1,142 @@ +#include "staticpaths.h" +#include "thread.h" +#include "utils.h" +#include + +// ============================================================================ +// StaticPath +// ============================================================================ + +StaticPath::StaticPath(const Rcpp::List& sp) { + ASSERT_MAIN_THREAD() + path = Rcpp::as(sp["path"]); + index = Rcpp::as (sp["index"]); + fallthrough = Rcpp::as (sp["fallthrough"]); +} + +Rcpp::List StaticPath::asRObject() const { + ASSERT_MAIN_THREAD() + using namespace Rcpp; + + List obj = List::create( + _["path"] = path, + _["index"] = index, + _["fallthrough"] = fallthrough + ); + + obj.attr("class") = "staticPath"; + + return obj; +} + + +// ============================================================================ +// StaticPaths +// ============================================================================ +StaticPaths::StaticPaths() { + uv_mutex_init(&mutex); +} + +StaticPaths::StaticPaths(const Rcpp::List& source) { + ASSERT_MAIN_THREAD() + uv_mutex_init(&mutex); + + if (source.size() == 0) { + return; + } + + Rcpp::CharacterVector names = source.names(); + if (names.isNULL()) { + throw Rcpp::exception("Error processing static paths."); + } + + for (int i=0; i(names[i]); + if (name == "") { + throw Rcpp::exception("Error processing static paths."); + } + + Rcpp::List sp(source[i]); + StaticPath staticpath(sp); + + path_map.insert( + std::pair(name, staticpath) + ); + } +} + + +boost::optional StaticPaths::get(const std::string& path) const { + guard guard(mutex); + std::map::const_iterator it = path_map.find(path); + if (it == path_map.end()) { + return boost::none; + } + return it->second; +} + +boost::optional StaticPaths::get(const Rcpp::CharacterVector& path) const { + ASSERT_MAIN_THREAD() + if (path.size() != 1) { + throw Rcpp::exception("Can only get a single StaticPath object."); + } + return get(Rcpp::as(path)); +} + + +void StaticPaths::set(const std::string& path, const StaticPath& sp) { + guard guard(mutex); + path_map.insert( + std::pair(path, sp) + ); +} + +void StaticPaths::set(const std::map& pmap) { + std::map::const_iterator it; + for (it = pmap.begin(); it != pmap.end(); it++) { + set(it->first, it->second); + } +} + +void StaticPaths::set(const Rcpp::List& pmap) { + ASSERT_MAIN_THREAD() + std::map pmap2 = toMap(pmap); + set(pmap2); +} + + +void StaticPaths::remove(const std::string& path) { + guard guard(mutex); + std::map::const_iterator it = path_map.find(path); + if (it != path_map.end()) { + path_map.erase(it); + } +} + +void StaticPaths::remove(const std::vector& paths) { + std::vector::const_iterator it; + for (it = paths.begin(); it != paths.end(); it++) { + remove(*it); + } +} + +void StaticPaths::remove(const Rcpp::CharacterVector& paths) { + ASSERT_MAIN_THREAD() + std::vector paths_vec = Rcpp::as>(paths); + remove(paths_vec); +} + + +Rcpp::List StaticPaths::asRObject() const { + ASSERT_MAIN_THREAD() + guard guard(mutex); + Rcpp::List obj; + + std::map::const_iterator it; + for (it = path_map.begin(); it != path_map.end(); it++) { + obj[it->first] = it->second.asRObject(); + } + + return obj; +} + diff --git a/src/staticpaths.h b/src/staticpaths.h new file mode 100644 index 00000000..7819ebb6 --- /dev/null +++ b/src/staticpaths.h @@ -0,0 +1,48 @@ +#ifndef STATICPATHS_HPP +#define STATICPATHS_HPP + +#include +#include +#include +#include +#include "thread.h" + +class StaticPath { +public: + std::string path; + bool index; + bool fallthrough; + + StaticPath(std::string _path, bool _index, bool _fallthrough) : + path(_path), index(_index), fallthrough(_fallthrough) { + } + + StaticPath(const Rcpp::List& sp); + + Rcpp::List asRObject() const; +}; + + +class StaticPaths { + std::map path_map; + mutable uv_mutex_t mutex; + +public: + StaticPaths(); + StaticPaths(const Rcpp::List& source); + + boost::optional get(const std::string& path) const; + boost::optional get(const Rcpp::CharacterVector& path) const; + + void set(const std::string& path, const StaticPath& sp); + void set(const std::map& pmap); + void set(const Rcpp::List& pmap); + + void remove(const std::string& path); + void remove(const std::vector& paths); + void remove(const Rcpp::CharacterVector& paths); + + Rcpp::List asRObject() const; +}; + +#endif diff --git a/src/utils.cpp b/src/utils.cpp index 5bd7a587..07580c22 100644 --- a/src/utils.cpp +++ b/src/utils.cpp @@ -1,57 +1,8 @@ #include "utils.h" #include -#include void trace(const std::string& msg) { #ifdef DEBUG_TRACE std::cerr << msg << std::endl; #endif } - - -std::map toStringMap(Rcpp::CharacterVector x) { - ASSERT_MAIN_THREAD() - - std::map strmap; - - if (x.size() == 0) { - return strmap; - } - - Rcpp::CharacterVector names = x.names(); - if (names.isNULL()) { - throw Rcpp::exception("Error converting CharacterVector to map: vector does not have names."); - } - - - for (int i=0; i(names[i]); - std::string value = Rcpp::as(x[i]); - if (name == "") { - throw Rcpp::exception("Error converting CharacterVector to map: element has empty name."); - } - - strmap.insert( - std::pair(name, value) - ); - } - - return strmap; -} - - -Rcpp::CharacterVector toCharacterVector(const std::map& strmap) { - ASSERT_MAIN_THREAD() - - Rcpp::CharacterVector values(strmap.size()); - Rcpp::CharacterVector names(strmap.size()); - - std::map::const_iterator it = strmap.begin(); - for (int i=0; it != strmap.end(); i++, it++) { - values[i] = it->second; - names[i] = it->first; - } - - values.attr("names") = names; - return values; -} diff --git a/src/utils.h b/src/utils.h index 18608f76..8429d99f 100644 --- a/src/utils.h +++ b/src/utils.h @@ -86,11 +86,6 @@ std::string toString(T x) { return ss.str(); } -// Convert between map and CharacterVector -std::map toStringMap(Rcpp::CharacterVector x); -Rcpp::CharacterVector toCharacterVector(const std::map& strmap); - - // Given a path, return just the filename part. inline std::string basename(const std::string &path) { // TODO: handle Windows separators @@ -114,5 +109,36 @@ inline std::string find_extension(const std::string &filename) { } } +// This is used for converting an Rcpp named vector (T2) to a std::map. +template +std::map toMap(T2 x) { + ASSERT_MAIN_THREAD() + + std::map strmap; + + if (x.size() == 0) { + return strmap; + } + + Rcpp::CharacterVector names = x.names(); + if (names.isNULL()) { + throw Rcpp::exception("Error converting R object to map: vector does not have names."); + } + + for (int i=0; i(names[i]); + T value = Rcpp::as (x[i]); + if (name == "") { + throw Rcpp::exception("Error converting R object to map: element has empty name."); + } + + strmap.insert( + std::pair(name, value) + ); + } + + return strmap; +} + #endif diff --git a/src/webapplication.cpp b/src/webapplication.cpp index f549881a..63c7510f 100644 --- a/src/webapplication.cpp +++ b/src/webapplication.cpp @@ -7,6 +7,7 @@ #include "thread.h" #include "utils.h" #include "mime.h" +#include "staticpaths.h" #include std::string normalizeHeaderName(const std::string& name) { @@ -229,7 +230,7 @@ RWebApplication::RWebApplication( { ASSERT_MAIN_THREAD() - _staticPaths = toStringMap(Rcpp::CharacterVector(_getStaticPaths())); + _staticPaths = StaticPaths(Rcpp::List(_getStaticPaths())); } @@ -394,10 +395,11 @@ void RWebApplication::onWSClose(boost::shared_ptr pConn) { // Unlike most of the methods for an RWebApplication, these ones are called on // the background thread. - -// Returns a pair where the first element is the local directory, and the -// second element is the filename. -std::pair RWebApplication::_matchStaticPath(const std::string& url_path) const { +// Returns a pair where the first element is the StaticPath object, and the +// second element is the filename portion of the input url_path. +boost::optional> RWebApplication::_matchStaticPath( + const std::string& url_path +) const { ASSERT_BACKGROUND_THREAD() std::string path = url_path; @@ -413,16 +415,16 @@ std::pair RWebApplication::_matchStaticPath(const std: size_t found_idx = path.find_last_of('/', last_split_idx); if (found_idx <= 0) { - return std::pair("", ""); + return boost::none; } std::string pre_slash = path.substr(0, found_idx); std::string post_slash = path.substr(found_idx + 1); - std::map::const_iterator it = _staticPaths.find(pre_slash); - if (it != _staticPaths.end()) { + boost::optional sp = _staticPaths.get(pre_slash); + if (sp) { // Pair with dirname, filename - return std::pair(it->second, post_slash); + return std::pair(*sp, post_slash); } last_split_idx = found_idx - 1 ; @@ -452,11 +454,10 @@ boost::shared_ptr RWebApplication::staticFileResponse( ASSERT_BACKGROUND_THREAD() std::string url_path = doDecodeURI(pRequest->url(), true); - std::pair static_path = _matchStaticPath(url_path); - std::string local_dirname = static_path.first; - std::string filename = static_path.second; - if (local_dirname == "") { + boost::optional> sp_pair = _matchStaticPath(url_path); + + if (!sp_pair) { // This was not a static path. return nullptr; } @@ -472,11 +473,13 @@ boost::shared_ptr RWebApplication::staticFileResponse( return error_response(pRequest, 400); } - std::string local_path = local_dirname + "/" + filename; + const StaticPath& sp = sp_pair->first; + const std::string& filename = sp_pair->second; + + std::string local_path = sp.path + "/" + filename; // Self-frees when response is written FileDataSource* pDataSource = new FileDataSource(); - // TODO: Figure out how to deal with `owned` parameter. int ret = pDataSource->initialize(local_path, false); if (ret != 0) { @@ -514,38 +517,6 @@ boost::shared_ptr RWebApplication::staticFileResponse( return pResponse; } - -std::map RWebApplication::getStaticPaths() const { - // TODO: always lock staticPaths - return _staticPaths; -}; - -std::map RWebApplication::addStaticPaths( - const std::map& paths -) { - - std::map::const_iterator it; - for (it = paths.begin(); it != paths.end(); it++) { - _staticPaths[it->first] = it->second; - } - +StaticPaths& RWebApplication::getStaticPaths() { return _staticPaths; -}; - -std::map RWebApplication::removeStaticPaths( - const std::vector& paths -) { - - std::vector::const_iterator path_it = paths.begin(); - - for (path_it = paths.begin(); path_it != paths.end(); path_it++) { - std::map::const_iterator static_paths_it = _staticPaths.find(*path_it); - if (static_paths_it == _staticPaths.end()) { - err_printf("Tried to remove static path, but it wasn't present: %s\n", path_it->c_str()); - } else { - _staticPaths.erase(static_paths_it); - } - } - - return _staticPaths; -}; +} diff --git a/src/webapplication.h b/src/webapplication.h index d912f8f7..c4a2cbfa 100644 --- a/src/webapplication.h +++ b/src/webapplication.h @@ -6,6 +6,7 @@ #include #include "websockets.h" #include "thread.h" +#include "staticpaths.h" class HttpRequest; class HttpResponse; @@ -27,14 +28,10 @@ class WebApplication { boost::shared_ptr > data, boost::function error_callback) = 0; virtual void onWSClose(boost::shared_ptr) = 0; + virtual boost::shared_ptr staticFileResponse( boost::shared_ptr pRequest) = 0; - // Get current set of static paths. - virtual std::map getStaticPaths() const = 0; - virtual std::map addStaticPaths( - const std::map& paths) = 0; - virtual std::map removeStaticPaths( - const std::vector& paths) = 0; + virtual StaticPaths& getStaticPaths() = 0; }; @@ -50,12 +47,10 @@ class RWebApplication : public WebApplication { // the R function passed in that is used for initialization only. Rcpp::Function _getStaticPaths; - // TODO: need to be careful with this - it's created and destroyed on the - // main thread, but when it is accessed, it is always on the background thread. - // Probably need to lock at those times. - std::map _staticPaths; + StaticPaths _staticPaths; - std::pair _matchStaticPath(const std::string& url_path) const; + boost::optional> _matchStaticPath( + const std::string& url_path) const; public: RWebApplication(Rcpp::Function onHeaders, @@ -87,11 +82,7 @@ class RWebApplication : public WebApplication { virtual boost::shared_ptr staticFileResponse( boost::shared_ptr pRequest); - virtual std::map getStaticPaths() const; - virtual std::map addStaticPaths( - const std::map& paths); - virtual std::map removeStaticPaths( - const std::vector& paths); + virtual StaticPaths& getStaticPaths(); }; From eb52daaf5d750e4d591a5faa607376dd4d2d5722 Mon Sep 17 00:00:00 2001 From: Winston Chang Date: Thu, 18 Oct 2018 13:58:18 -0500 Subject: [PATCH 20/60] Implement fallthrough --- src/webapplication.cpp | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/webapplication.cpp b/src/webapplication.cpp index 63c7510f..44facb04 100644 --- a/src/webapplication.cpp +++ b/src/webapplication.cpp @@ -485,7 +485,12 @@ boost::shared_ptr RWebApplication::staticFileResponse( if (ret != 0) { // Couldn't read the file delete pDataSource; - return error_response(pRequest, 404); + + if (sp.fallthrough) { + return nullptr; + } else { + return error_response(pRequest, 404); + } } int file_size = pDataSource->size(); From 137499156d7490651b6afc5ac75a3dd23d1b8ddd Mon Sep 17 00:00:00 2001 From: Winston Chang Date: Thu, 18 Oct 2018 15:50:05 -0500 Subject: [PATCH 21/60] Allow replacing static paths in a running app. --- src/staticpaths.cpp | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/staticpaths.cpp b/src/staticpaths.cpp index 6998a890..4ae04c71 100644 --- a/src/staticpaths.cpp +++ b/src/staticpaths.cpp @@ -86,6 +86,13 @@ boost::optional StaticPaths::get(const Rcpp::CharacterVector& void StaticPaths::set(const std::string& path, const StaticPath& sp) { guard guard(mutex); + // If the key already exists, replace the value. + std::map::iterator it = path_map.find(path); + if (it != path_map.end()) { + it->second = sp; + } + + // Otherwise, insert the pair. path_map.insert( std::pair(path, sp) ); From 516b4364b2083a84bf94d64f9be2df72c8bc36c4 Mon Sep 17 00:00:00 2001 From: Winston Chang Date: Thu, 18 Oct 2018 22:27:12 -0500 Subject: [PATCH 22/60] Add index.html support --- src/filedatasource-unix.cpp | 7 ++++ src/fs.h | 57 ++++++++++++++++++++++++++ src/staticpaths.cpp | 80 +++++++++++++++++++++++++++++++++++++ src/staticpaths.h | 3 ++ src/utils.h | 23 ----------- src/webapplication.cpp | 63 ++++++++++------------------- src/webapplication.h | 3 -- 7 files changed, 168 insertions(+), 68 deletions(-) create mode 100644 src/fs.h diff --git a/src/filedatasource-unix.cpp b/src/filedatasource-unix.cpp index 7d81c597..dab74f51 100644 --- a/src/filedatasource-unix.cpp +++ b/src/filedatasource-unix.cpp @@ -21,6 +21,13 @@ int FileDataSource::initialize(const std::string& path, bool owned) { ::close(_fd); return 1; } + + if (S_ISDIR(info.st_mode)) { + _lastErrorMessage = "File data source is a directory.\n"; + ::close(_fd); + return 1; + } + _length = info.st_size; if (owned && unlink(path.c_str())) { diff --git a/src/fs.h b/src/fs.h new file mode 100644 index 00000000..509b0a9f --- /dev/null +++ b/src/fs.h @@ -0,0 +1,57 @@ +#ifndef FS_H +#define FS_H + +#include + +#ifdef _WIN32 +#else +#include +#endif + +// ============================================================================ +// Filesystem-related functions +// ============================================================================ + +// Given a path, return just the filename part. +inline std::string basename(const std::string &path) { + // TODO: handle Windows separators + size_t found_idx = path.find_last_of('/'); + + if (found_idx == std::string::npos) { + return path; + } else { + return path.substr(found_idx + 1); + } +} + +// Given a filename, return the extension. +inline std::string find_extension(const std::string &filename) { + size_t found_idx = filename.find_last_of('.'); + + if (found_idx <= 0) { + return ""; + } else { + return filename.substr(found_idx + 1); + } +} + +inline bool is_directory(const std::string &filename) { +#ifdef _WIN32 + + // TODO: implement + +#else + + struct stat sb; + + if (stat(filename.c_str(), &sb) == 0 && S_ISDIR(sb.st_mode)) { + return true; + } else { + return false; + } + +#endif +} + + +#endif diff --git a/src/staticpaths.cpp b/src/staticpaths.cpp index 4ae04c71..de656085 100644 --- a/src/staticpaths.cpp +++ b/src/staticpaths.cpp @@ -133,6 +133,86 @@ void StaticPaths::remove(const Rcpp::CharacterVector& paths) { remove(paths_vec); } +// Given a URL path, this returns a pair where the first element is a matching +// StaticPath object, and the second element is the portion of the url_path that +// comes after the match for the static path. +// +// For example, if: +// - The input url_path is "/foo/bar/page.html" +// - There is a StaticPath object (call it `s`) for which s.path == "/foo" +// Then: +// - This function returns a pair consisting of +// +// If there are multiple potential static path matches, for example "/foo" and +// "/foo/bar", then this will match the most specific (longest) path. +// +// If url_path has a trailing "/", it is stripped off. If the url_path matches +// a static path in its entirety (e.g., the url_path is "/foo" or "/foo/" and +// there is a static path "/foo"), then the returned pair consists of the +// matching StaticPath object and an empty string "". +// +// If no matching static path is found, then it returns boost::none. +// +boost::optional> StaticPaths::matchStaticPath( + const std::string& url_path) const +{ + + if (url_path.empty()) { + return boost::none; + } + + std::string path = url_path; + size_t found_idx = std::string::npos; + + std::string pre_slash; + std::string post_slash; + + // Strip off a trailing slash. A path like "/foo/bar/" => "/foo/bar". + // One exception: don't alter it if the path is just "/". + if (path.length() > 1 && path.at(path.length() - 1) == '/') { + path = path.substr(0, path.length() - 1); + } + + pre_slash = path; + post_slash = ""; + + // This loop searches for a match in path_map of pre_slash, the part before + // the last split-on '/'. If found, it returns a pair with the part before + // the slash, and the part after the slash. If not found, it splits on the + // previous '/' and searches again, and so on, until there are no more to + // split on. + while (true) { + // Check if the part before the split-on '/' is in the staticPaths. + boost::optional sp = this->get(pre_slash); + if (sp) { + return std::pair(*sp, post_slash); + } + + if (found_idx == 0) { + // We get here after checking the leading '/'. + return boost::none; + } + + // Split the string on '/' + found_idx = path.find_last_of('/', found_idx - 1); + + if (found_idx == std::string::npos) { + // This is an extra check that could only be hit if the first character + // of the URL is not a slash. Shouldn't be possible to get here because + // the http parser will throw an "invalid URL" error when it encounters + // such a URL, but we'll check just in case. + return boost::none; + } + + pre_slash = path.substr(0, found_idx); + if (pre_slash == "") { + // Special case if we've hit the leading slash. + pre_slash = "/"; + } + post_slash = path.substr(found_idx + 1); + } +} + Rcpp::List StaticPaths::asRObject() const { ASSERT_MAIN_THREAD() diff --git a/src/staticpaths.h b/src/staticpaths.h index 7819ebb6..7c3be9e1 100644 --- a/src/staticpaths.h +++ b/src/staticpaths.h @@ -42,6 +42,9 @@ class StaticPaths { void remove(const std::vector& paths); void remove(const Rcpp::CharacterVector& paths); + boost::optional> matchStaticPath( + const std::string& url_path) const; + Rcpp::List asRObject() const; }; diff --git a/src/utils.h b/src/utils.h index 8429d99f..fa30c8e1 100644 --- a/src/utils.h +++ b/src/utils.h @@ -86,29 +86,6 @@ std::string toString(T x) { return ss.str(); } -// Given a path, return just the filename part. -inline std::string basename(const std::string &path) { - // TODO: handle Windows separators - size_t found_idx = path.find_last_of('/'); - - if (found_idx <= 0) { - return path; - } else { - return path.substr(found_idx + 1); - } -} - -// Given a filename, return the extension. -inline std::string find_extension(const std::string &filename) { - size_t found_idx = filename.find_last_of('.'); - - if (found_idx <= 0) { - return ""; - } else { - return filename.substr(found_idx + 1); - } -} - // This is used for converting an Rcpp named vector (T2) to a std::map. template std::map toMap(T2 x) { diff --git a/src/webapplication.cpp b/src/webapplication.cpp index 44facb04..0c313263 100644 --- a/src/webapplication.cpp +++ b/src/webapplication.cpp @@ -8,6 +8,7 @@ #include "utils.h" #include "mime.h" #include "staticpaths.h" +#include "fs.h" #include std::string normalizeHeaderName(const std::string& name) { @@ -395,43 +396,6 @@ void RWebApplication::onWSClose(boost::shared_ptr pConn) { // Unlike most of the methods for an RWebApplication, these ones are called on // the background thread. -// Returns a pair where the first element is the StaticPath object, and the -// second element is the filename portion of the input url_path. -boost::optional> RWebApplication::_matchStaticPath( - const std::string& url_path -) const { - ASSERT_BACKGROUND_THREAD() - - std::string path = url_path; - size_t last_split_idx = std::string::npos; - - // This loop splits the string on '/', starting with the last one, and then - // searches for a match in _staticPaths of the first part. If found, it - // returns a pair with the part before the slash, and the part after the - // slash. If not found, it splits on the previous '/' and searches again, - // and so on, until there are no more to split on. - while (true) { - // Split the string on '/' - size_t found_idx = path.find_last_of('/', last_split_idx); - - if (found_idx <= 0) { - return boost::none; - } - - std::string pre_slash = path.substr(0, found_idx); - std::string post_slash = path.substr(found_idx + 1); - - boost::optional sp = _staticPaths.get(pre_slash); - if (sp) { - // Pair with dirname, filename - return std::pair(*sp, post_slash); - } - - last_split_idx = found_idx - 1 ; - } -} - - boost::shared_ptr error_response(boost::shared_ptr pRequest, int code) { std::string description = getStatusDescription(code); std::string content = toString(code) + " " + description + "\n"; @@ -455,13 +419,16 @@ boost::shared_ptr RWebApplication::staticFileResponse( std::string url_path = doDecodeURI(pRequest->url(), true); - boost::optional> sp_pair = _matchStaticPath(url_path); + boost::optional> sp_pair = + _staticPaths.matchStaticPath(url_path); if (!sp_pair) { // This was not a static path. return nullptr; } + // If we get here, we've matched a static path. + // Check that method is GET or HEAD; error otherwise. std::string method = pRequest->method(); if (method != "GET" && method != "HEAD") { @@ -473,10 +440,18 @@ boost::shared_ptr RWebApplication::staticFileResponse( return error_response(pRequest, 400); } - const StaticPath& sp = sp_pair->first; - const std::string& filename = sp_pair->second; + const StaticPath& sp = sp_pair->first; + // Note that the subpath may include leading dirs, as in "foo/bar/abc.txt". + const std::string& subpath = sp_pair->second; - std::string local_path = sp.path + "/" + filename; + // Path to local file on disk + std::string local_path = sp.path + "/" + subpath; + + if (is_directory(local_path)) { + if (sp.index) { + local_path = local_path + "/" + "index.html"; + } + } // Self-frees when response is written FileDataSource* pDataSource = new FileDataSource(); @@ -494,7 +469,11 @@ boost::shared_ptr RWebApplication::staticFileResponse( } int file_size = pDataSource->size(); - std::string mime_type = find_mime_type(find_extension(filename)); + + // Use local_path instead of subpath, because if the subpath is "/foo/" and + // sp.index is true, then the local_path will be "/foo/index.html". We need + // to use the latter to determine mime type. + std::string mime_type = find_mime_type(find_extension(basename(local_path))); if (mime_type == "") { mime_type = "application/octet-stream"; } diff --git a/src/webapplication.h b/src/webapplication.h index c4a2cbfa..132af1f3 100644 --- a/src/webapplication.h +++ b/src/webapplication.h @@ -49,9 +49,6 @@ class RWebApplication : public WebApplication { StaticPaths _staticPaths; - boost::optional> _matchStaticPath( - const std::string& url_path) const; - public: RWebApplication(Rcpp::Function onHeaders, Rcpp::Function onBodyData, From 1f47553b3720bd61dc0df6fb0d38bf2de9fb418a Mon Sep 17 00:00:00 2001 From: Winston Chang Date: Thu, 18 Oct 2018 22:33:00 -0500 Subject: [PATCH 23/60] Remove unused function --- R/utils.R | 6 ------ 1 file changed, 6 deletions(-) diff --git a/R/utils.R b/R/utils.R index f6073557..148abc23 100644 --- a/R/utils.R +++ b/R/utils.R @@ -13,12 +13,6 @@ httpuv_version <- local({ } }) - -# Return a zero-element named character vector -empty_named_vec <- function() { - c(a = "")[0] -} - # Given a vector/list, return TRUE if any elements are unnamed, FALSE otherwise. any_unnamed <- function(x) { # Zero-length vector From 90256965941fb6f6fdee398c42018f437642480f Mon Sep 17 00:00:00 2001 From: Winston Chang Date: Thu, 18 Oct 2018 23:33:04 -0500 Subject: [PATCH 24/60] Don't add trailing slash --- src/webapplication.cpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/webapplication.cpp b/src/webapplication.cpp index 0c313263..b8f728bf 100644 --- a/src/webapplication.cpp +++ b/src/webapplication.cpp @@ -445,7 +445,10 @@ boost::shared_ptr RWebApplication::staticFileResponse( const std::string& subpath = sp_pair->second; // Path to local file on disk - std::string local_path = sp.path + "/" + subpath; + std::string local_path = sp.path; + if (subpath != "") { + local_path += "/" + subpath; + } if (is_directory(local_path)) { if (sp.index) { From a9aca64ba58fa3604e4ca13f38c7828d3d46efe4 Mon Sep 17 00:00:00 2001 From: Winston Chang Date: Thu, 18 Oct 2018 23:33:39 -0500 Subject: [PATCH 25/60] Ensure StaticPath paths don't have trailing slash --- src/staticpaths.cpp | 4 ++++ src/staticpaths.h | 6 +++++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/src/staticpaths.cpp b/src/staticpaths.cpp index de656085..73ae86fe 100644 --- a/src/staticpaths.cpp +++ b/src/staticpaths.cpp @@ -12,6 +12,10 @@ StaticPath::StaticPath(const Rcpp::List& sp) { path = Rcpp::as(sp["path"]); index = Rcpp::as (sp["index"]); fallthrough = Rcpp::as (sp["fallthrough"]); + + if (path.at(path.length() - 1) == '/') { + throw std::runtime_error("Static path must not have trailing slash."); + } } Rcpp::List StaticPath::asRObject() const { diff --git a/src/staticpaths.h b/src/staticpaths.h index 7c3be9e1..fc107683 100644 --- a/src/staticpaths.h +++ b/src/staticpaths.h @@ -14,7 +14,11 @@ class StaticPath { bool fallthrough; StaticPath(std::string _path, bool _index, bool _fallthrough) : - path(_path), index(_index), fallthrough(_fallthrough) { + path(_path), index(_index), fallthrough(_fallthrough) + { + if (path.at(path.length() - 1) == '/') { + throw std::runtime_error("Static path must not have trailing slash."); + } } StaticPath(const Rcpp::List& sp); From e38680fa5733582c80ae780e7ce284d34d6405c0 Mon Sep 17 00:00:00 2001 From: Winston Chang Date: Fri, 19 Oct 2018 10:29:21 -0500 Subject: [PATCH 26/60] Rename 'index' parameter to 'indexhtml' --- R/static_paths.R | 8 ++++---- src/staticpaths.cpp | 4 ++-- src/staticpaths.h | 6 +++--- src/webapplication.cpp | 6 +++--- 4 files changed, 12 insertions(+), 12 deletions(-) diff --git a/R/static_paths.R b/R/static_paths.R index c1bc56bf..ef6491e8 100644 --- a/R/static_paths.R +++ b/R/static_paths.R @@ -3,7 +3,7 @@ #' This function creates a \code{staticPath} object. #' #' @param path The local path. -#' @param index If an index.html file is present, should it be served up when +#' @param indexhtml If an index.html file is present, should it be served up when #' the client requests \code{/} or any subdirectory? #' @param fallthrough With the default value, \code{FALSE}, if a request is made #' for a file that doesn't exist, then httpuv will immediately send a 404 @@ -12,7 +12,7 @@ #' \code{TRUE}, then instead of sending a 404 response, httpuv will call the #' application's \code{call} function, and allow it to handle the request. #' @export -staticPath <- function(path, index = TRUE, fallthrough = FALSE) { +staticPath <- function(path, indexhtml = TRUE, fallthrough = FALSE) { if (!is.character(path) || length(path) != 1 || path == "") { stop("`path` must be a non-empty string.") } @@ -22,7 +22,7 @@ staticPath <- function(path, index = TRUE, fallthrough = FALSE) { structure( list( path = path, - index = index, + indexhtml = indexhtml, fallthrough = fallthrough ), class = "staticPath" @@ -56,7 +56,7 @@ format.staticPath <- function(x, ...) { ret <- paste0( "\n", " Local path: ", x$path, "\n", - " Use index.html: ", x$index, "\n", + " Use index.html: ", x$indexhtml, "\n", " Fallthrough to R: ", x$fallthrough ) } diff --git a/src/staticpaths.cpp b/src/staticpaths.cpp index 73ae86fe..61323316 100644 --- a/src/staticpaths.cpp +++ b/src/staticpaths.cpp @@ -10,7 +10,7 @@ StaticPath::StaticPath(const Rcpp::List& sp) { ASSERT_MAIN_THREAD() path = Rcpp::as(sp["path"]); - index = Rcpp::as (sp["index"]); + indexhtml = Rcpp::as (sp["indexhtml"]); fallthrough = Rcpp::as (sp["fallthrough"]); if (path.at(path.length() - 1) == '/') { @@ -24,7 +24,7 @@ Rcpp::List StaticPath::asRObject() const { List obj = List::create( _["path"] = path, - _["index"] = index, + _["indexhtml"] = indexhtml, _["fallthrough"] = fallthrough ); diff --git a/src/staticpaths.h b/src/staticpaths.h index fc107683..7d305b62 100644 --- a/src/staticpaths.h +++ b/src/staticpaths.h @@ -10,11 +10,11 @@ class StaticPath { public: std::string path; - bool index; + bool indexhtml; bool fallthrough; - StaticPath(std::string _path, bool _index, bool _fallthrough) : - path(_path), index(_index), fallthrough(_fallthrough) + StaticPath(std::string _path, bool _indexhtml, bool _fallthrough) : + path(_path), indexhtml(_indexhtml), fallthrough(_fallthrough) { if (path.at(path.length() - 1) == '/') { throw std::runtime_error("Static path must not have trailing slash."); diff --git a/src/webapplication.cpp b/src/webapplication.cpp index b8f728bf..87add779 100644 --- a/src/webapplication.cpp +++ b/src/webapplication.cpp @@ -451,7 +451,7 @@ boost::shared_ptr RWebApplication::staticFileResponse( } if (is_directory(local_path)) { - if (sp.index) { + if (sp.indexhtml) { local_path = local_path + "/" + "index.html"; } } @@ -474,8 +474,8 @@ boost::shared_ptr RWebApplication::staticFileResponse( int file_size = pDataSource->size(); // Use local_path instead of subpath, because if the subpath is "/foo/" and - // sp.index is true, then the local_path will be "/foo/index.html". We need - // to use the latter to determine mime type. + // sp.indexhtml is true, then the local_path will be "/foo/index.html". We + // need to use the latter to determine mime type. std::string mime_type = find_mime_type(find_extension(basename(local_path))); if (mime_type == "") { mime_type = "application/octet-stream"; From ef53cb8d881b064b7132fa18c09480f194fa8b13 Mon Sep 17 00:00:00 2001 From: Winston Chang Date: Fri, 19 Oct 2018 10:29:57 -0500 Subject: [PATCH 27/60] Dpn't attempt static serving when Connection: Upgrade header is present --- src/webapplication.cpp | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/webapplication.cpp b/src/webapplication.cpp index 87add779..93553dd6 100644 --- a/src/webapplication.cpp +++ b/src/webapplication.cpp @@ -417,6 +417,12 @@ boost::shared_ptr RWebApplication::staticFileResponse( ) { ASSERT_BACKGROUND_THREAD() + // If it has a Connection: Upgrade header, don't try to serve a static file. + // Just fall through. + if (pRequest->hasHeader("Connection", "Upgrade", true)) { + return nullptr; + } + std::string url_path = doDecodeURI(pRequest->url(), true); boost::optional> sp_pair = From 32285ed7bfc2b6b7559ed198ee582bd70e85f8b2 Mon Sep 17 00:00:00 2001 From: Winston Chang Date: Fri, 19 Oct 2018 10:47:38 -0500 Subject: [PATCH 28/60] Always specify that encoding is UTF-8 for static HTML files --- src/webapplication.cpp | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/webapplication.cpp b/src/webapplication.cpp index 93553dd6..d09ec528 100644 --- a/src/webapplication.cpp +++ b/src/webapplication.cpp @@ -482,9 +482,12 @@ boost::shared_ptr RWebApplication::staticFileResponse( // Use local_path instead of subpath, because if the subpath is "/foo/" and // sp.indexhtml is true, then the local_path will be "/foo/index.html". We // need to use the latter to determine mime type. - std::string mime_type = find_mime_type(find_extension(basename(local_path))); - if (mime_type == "") { - mime_type = "application/octet-stream"; + std::string content_type = find_mime_type(find_extension(basename(local_path))); + if (content_type == "") { + content_type = "application/octet-stream"; + } else if (content_type == "text/html") { + // Always specify that encoding UTF-8 for HTML. + content_type = "text/html; charset=utf-8"; } if (method == "HEAD") { @@ -505,7 +508,7 @@ boost::shared_ptr RWebApplication::staticFileResponse( // automatically set the Content-Length (by using the FileDataSource), but // the response for the HEAD would not. respHeaders.push_back(std::make_pair("Content-Length", toString(file_size))); - respHeaders.push_back(std::make_pair("Content-Type", mime_type)); + respHeaders.push_back(std::make_pair("Content-Type", content_type)); return pResponse; } From ba702a2aa61ed666f39bc47ea8fafc603becff0b Mon Sep 17 00:00:00 2001 From: Winston Chang Date: Fri, 19 Oct 2018 14:25:30 -0500 Subject: [PATCH 29/60] Strip trailing slashes from staticPath names --- R/static_paths.R | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/R/static_paths.R b/R/static_paths.R index ef6491e8..9d0930a7 100644 --- a/R/static_paths.R +++ b/R/static_paths.R @@ -81,7 +81,7 @@ normalizeStaticPaths <- function(paths) { # as.staticPath because of S3 dispatch for non-registered methods. paths <- lapply(paths, function(path) as.staticPath(path)) - # Make sure URL paths have a leading '/'. + # Make sure URL paths have a leading '/' and no trailing '/'. names(paths) <- vapply(names(paths), function(path) { if (path == "") { stop("All paths must be non-empty strings.") @@ -90,6 +90,9 @@ normalizeStaticPaths <- function(paths) { if (substr(path, 1, 1) != "/") { path <- paste0("/", path) } + # Strip trailing slashes + path <- sub("/+$", "", path) + path }, "") From ffb728e772f83235ca6556cf66a82d1cd03ea645 Mon Sep 17 00:00:00 2001 From: Winston Chang Date: Fri, 19 Oct 2018 14:39:49 -0500 Subject: [PATCH 30/60] Strip off query string for static serving --- src/webapplication.cpp | 29 +++++++++++++++++++++-------- 1 file changed, 21 insertions(+), 8 deletions(-) diff --git a/src/webapplication.cpp b/src/webapplication.cpp index d09ec528..d1e94714 100644 --- a/src/webapplication.cpp +++ b/src/webapplication.cpp @@ -90,13 +90,8 @@ Rcpp::List errorResponse() { ); } -void requestToEnv(boost::shared_ptr pRequest, Rcpp::Environment* pEnv) { - ASSERT_MAIN_THREAD() - using namespace Rcpp; - - Environment& env = *pEnv; - - std::string url = pRequest->url(); +// Given a URL path like "/foo?abc=123", removes the '?' and everything after. +std::pair splitQueryString(const std::string& url) { size_t qsIndex = url.find('?'); std::string path, queryString; if (qsIndex == std::string::npos) @@ -106,6 +101,20 @@ void requestToEnv(boost::shared_ptr pRequest, Rcpp::Environment* pE queryString = url.substr(qsIndex); } + return std::pair(path, queryString); +} + + +void requestToEnv(boost::shared_ptr pRequest, Rcpp::Environment* pEnv) { + ASSERT_MAIN_THREAD() + using namespace Rcpp; + + Environment& env = *pEnv; + + std::pair url_query = splitQueryString(pRequest->url()); + std::string& path = url_query.first; + std::string& queryString = url_query.second; + // When making assignments into the Environment, the value must be wrapped // in a Rcpp object -- letting Rcpp automatically do the wrapping can result // in an object being GC'd too early. @@ -423,7 +432,11 @@ boost::shared_ptr RWebApplication::staticFileResponse( return nullptr; } - std::string url_path = doDecodeURI(pRequest->url(), true); + std::string url = doDecodeURI(pRequest->url(), true); + + // Strip off query string + std::pair url_query = splitQueryString(url); + std::string& url_path = url_query.first; boost::optional> sp_pair = _staticPaths.matchStaticPath(url_path); From 99715f6fed87d2e003acebb68a6875f1173e0490 Mon Sep 17 00:00:00 2001 From: Winston Chang Date: Fri, 19 Oct 2018 21:34:13 -0500 Subject: [PATCH 31/60] Rename StaticPaths StaticPathList --- src/httpuv.cpp | 6 ++--- src/{staticpaths.cpp => staticpath.cpp} | 30 ++++++++++++------------- src/{staticpaths.h => staticpath.h} | 11 ++++----- src/webapplication.cpp | 10 ++++----- src/webapplication.h | 10 ++++----- 5 files changed, 34 insertions(+), 33 deletions(-) rename src/{staticpaths.cpp => staticpath.cpp} (88%) rename src/{staticpaths.h => staticpath.h} (87%) diff --git a/src/httpuv.cpp b/src/httpuv.cpp index 81b1b78c..b5e73e67 100644 --- a/src/httpuv.cpp +++ b/src/httpuv.cpp @@ -397,20 +397,20 @@ boost::shared_ptr get_pWebApplication(std::string handle) { // [[Rcpp::export]] Rcpp::List getStaticPaths_(std::string handle) { ASSERT_MAIN_THREAD() - return get_pWebApplication(handle)->getStaticPaths().asRObject(); + return get_pWebApplication(handle)->getStaticPathList().asRObject(); } // [[Rcpp::export]] Rcpp::List setStaticPaths_(std::string handle, Rcpp::List sp) { ASSERT_MAIN_THREAD() - get_pWebApplication(handle)->getStaticPaths().set(sp); + get_pWebApplication(handle)->getStaticPathList().set(sp); return getStaticPaths_(handle); } // [[Rcpp::export]] Rcpp::List removeStaticPaths_(std::string handle, Rcpp::CharacterVector paths) { ASSERT_MAIN_THREAD() - get_pWebApplication(handle)->getStaticPaths().remove(paths); + get_pWebApplication(handle)->getStaticPathList().remove(paths); return getStaticPaths_(handle); } diff --git a/src/staticpaths.cpp b/src/staticpath.cpp similarity index 88% rename from src/staticpaths.cpp rename to src/staticpath.cpp index 61323316..531d8651 100644 --- a/src/staticpaths.cpp +++ b/src/staticpath.cpp @@ -1,4 +1,4 @@ -#include "staticpaths.h" +#include "staticpath.h" #include "thread.h" #include "utils.h" #include @@ -35,13 +35,13 @@ Rcpp::List StaticPath::asRObject() const { // ============================================================================ -// StaticPaths +// StaticPathList // ============================================================================ -StaticPaths::StaticPaths() { +StaticPathList::StaticPathList() { uv_mutex_init(&mutex); } -StaticPaths::StaticPaths(const Rcpp::List& source) { +StaticPathList::StaticPathList(const Rcpp::List& source) { ASSERT_MAIN_THREAD() uv_mutex_init(&mutex); @@ -70,7 +70,7 @@ StaticPaths::StaticPaths(const Rcpp::List& source) { } -boost::optional StaticPaths::get(const std::string& path) const { +boost::optional StaticPathList::get(const std::string& path) const { guard guard(mutex); std::map::const_iterator it = path_map.find(path); if (it == path_map.end()) { @@ -79,7 +79,7 @@ boost::optional StaticPaths::get(const std::string& path) con return it->second; } -boost::optional StaticPaths::get(const Rcpp::CharacterVector& path) const { +boost::optional StaticPathList::get(const Rcpp::CharacterVector& path) const { ASSERT_MAIN_THREAD() if (path.size() != 1) { throw Rcpp::exception("Can only get a single StaticPath object."); @@ -88,7 +88,7 @@ boost::optional StaticPaths::get(const Rcpp::CharacterVector& } -void StaticPaths::set(const std::string& path, const StaticPath& sp) { +void StaticPathList::set(const std::string& path, const StaticPath& sp) { guard guard(mutex); // If the key already exists, replace the value. std::map::iterator it = path_map.find(path); @@ -102,21 +102,21 @@ void StaticPaths::set(const std::string& path, const StaticPath& sp) { ); } -void StaticPaths::set(const std::map& pmap) { +void StaticPathList::set(const std::map& pmap) { std::map::const_iterator it; for (it = pmap.begin(); it != pmap.end(); it++) { set(it->first, it->second); } } -void StaticPaths::set(const Rcpp::List& pmap) { +void StaticPathList::set(const Rcpp::List& pmap) { ASSERT_MAIN_THREAD() std::map pmap2 = toMap(pmap); set(pmap2); } -void StaticPaths::remove(const std::string& path) { +void StaticPathList::remove(const std::string& path) { guard guard(mutex); std::map::const_iterator it = path_map.find(path); if (it != path_map.end()) { @@ -124,14 +124,14 @@ void StaticPaths::remove(const std::string& path) { } } -void StaticPaths::remove(const std::vector& paths) { +void StaticPathList::remove(const std::vector& paths) { std::vector::const_iterator it; for (it = paths.begin(); it != paths.end(); it++) { remove(*it); } } -void StaticPaths::remove(const Rcpp::CharacterVector& paths) { +void StaticPathList::remove(const Rcpp::CharacterVector& paths) { ASSERT_MAIN_THREAD() std::vector paths_vec = Rcpp::as>(paths); remove(paths_vec); @@ -157,7 +157,7 @@ void StaticPaths::remove(const Rcpp::CharacterVector& paths) { // // If no matching static path is found, then it returns boost::none. // -boost::optional> StaticPaths::matchStaticPath( +boost::optional> StaticPathList::matchStaticPath( const std::string& url_path) const { @@ -186,7 +186,7 @@ boost::optional> StaticPaths::matchSta // previous '/' and searches again, and so on, until there are no more to // split on. while (true) { - // Check if the part before the split-on '/' is in the staticPaths. + // Check if the part before the split-on '/' is in the staticPath. boost::optional sp = this->get(pre_slash); if (sp) { return std::pair(*sp, post_slash); @@ -218,7 +218,7 @@ boost::optional> StaticPaths::matchSta } -Rcpp::List StaticPaths::asRObject() const { +Rcpp::List StaticPathList::asRObject() const { ASSERT_MAIN_THREAD() guard guard(mutex); Rcpp::List obj; diff --git a/src/staticpaths.h b/src/staticpath.h similarity index 87% rename from src/staticpaths.h rename to src/staticpath.h index 7d305b62..cb52d871 100644 --- a/src/staticpaths.h +++ b/src/staticpath.h @@ -1,5 +1,5 @@ -#ifndef STATICPATHS_HPP -#define STATICPATHS_HPP +#ifndef STATICPATH_HPP +#define STATICPATH_HPP #include #include @@ -27,13 +27,14 @@ class StaticPath { }; -class StaticPaths { +class StaticPathList { std::map path_map; + // Mutex is used whenever path_map is accessed. mutable uv_mutex_t mutex; public: - StaticPaths(); - StaticPaths(const Rcpp::List& source); + StaticPathList(); + StaticPathList(const Rcpp::List& source); boost::optional get(const std::string& path) const; boost::optional get(const Rcpp::CharacterVector& path) const; diff --git a/src/webapplication.cpp b/src/webapplication.cpp index d1e94714..7e901cd5 100644 --- a/src/webapplication.cpp +++ b/src/webapplication.cpp @@ -7,7 +7,7 @@ #include "thread.h" #include "utils.h" #include "mime.h" -#include "staticpaths.h" +#include "staticpath.h" #include "fs.h" #include @@ -240,7 +240,7 @@ RWebApplication::RWebApplication( { ASSERT_MAIN_THREAD() - _staticPaths = StaticPaths(Rcpp::List(_getStaticPaths())); + _staticPathList = StaticPathList(Rcpp::List(_getStaticPaths())); } @@ -439,7 +439,7 @@ boost::shared_ptr RWebApplication::staticFileResponse( std::string& url_path = url_query.first; boost::optional> sp_pair = - _staticPaths.matchStaticPath(url_path); + _staticPathList.matchStaticPath(url_path); if (!sp_pair) { // This was not a static path. @@ -526,6 +526,6 @@ boost::shared_ptr RWebApplication::staticFileResponse( return pResponse; } -StaticPaths& RWebApplication::getStaticPaths() { - return _staticPaths; +StaticPathList& RWebApplication::getStaticPathList() { + return _staticPathList; } diff --git a/src/webapplication.h b/src/webapplication.h index 132af1f3..efd700fe 100644 --- a/src/webapplication.h +++ b/src/webapplication.h @@ -6,7 +6,7 @@ #include #include "websockets.h" #include "thread.h" -#include "staticpaths.h" +#include "staticpath.h" class HttpRequest; class HttpResponse; @@ -31,7 +31,7 @@ class WebApplication { virtual boost::shared_ptr staticFileResponse( boost::shared_ptr pRequest) = 0; - virtual StaticPaths& getStaticPaths() = 0; + virtual StaticPathList& getStaticPathList() = 0; }; @@ -43,11 +43,11 @@ class RWebApplication : public WebApplication { Rcpp::Function _onWSOpen; Rcpp::Function _onWSMessage; Rcpp::Function _onWSClose; - // Note this is different from the public getStaticPaths function - this is + // Note this differs from the public getStaticPathList function - this is // the R function passed in that is used for initialization only. Rcpp::Function _getStaticPaths; - StaticPaths _staticPaths; + StaticPathList _staticPathList; public: RWebApplication(Rcpp::Function onHeaders, @@ -79,7 +79,7 @@ class RWebApplication : public WebApplication { virtual boost::shared_ptr staticFileResponse( boost::shared_ptr pRequest); - virtual StaticPaths& getStaticPaths(); + virtual StaticPathList& getStaticPathList(); }; From 3942853670ea6f44919e0e1c8cfec91dbe4a32a0 Mon Sep 17 00:00:00 2001 From: Winston Chang Date: Thu, 8 Nov 2018 17:28:48 -0600 Subject: [PATCH 32/60] Change API for static path options --- NAMESPACE | 3 ++ R/RcppExports.R | 16 ++++-- R/httpuv.R | 23 +++++--- R/server.R | 31 ++++++++--- R/static_paths.R | 73 +++++++++++++++++++++++-- R/utils.R | 9 ++++ man/staticPath.Rd | 5 +- src/RcppExports.cpp | 49 +++++++++++++---- src/httpuv.cpp | 24 +++++++-- src/staticpath.cpp | 117 ++++++++++++++++++++++++++++++++++------- src/staticpath.h | 43 +++++++++------ src/utils.h | 30 +++++++++-- src/webapplication.cpp | 30 +++++++---- src/webapplication.h | 6 +-- 14 files changed, 367 insertions(+), 92 deletions(-) diff --git a/NAMESPACE b/NAMESPACE index b6945439..093922b6 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -1,7 +1,9 @@ # Generated by roxygen2: do not edit by hand S3method(format,staticPath) +S3method(format,staticPathOptions) S3method(print,staticPath) +S3method(print,staticPathOptions) export(decodeURI) export(decodeURIComponent) export(encodeURI) @@ -17,6 +19,7 @@ export(startDaemonizedServer) export(startPipeServer) export(startServer) export(staticPath) +export(staticPathOptions) export(stopAllServers) export(stopDaemonizedServer) export(stopServer) diff --git a/R/RcppExports.R b/R/RcppExports.R index c4d4db46..f5ec254e 100644 --- a/R/RcppExports.R +++ b/R/RcppExports.R @@ -9,12 +9,12 @@ closeWS <- function(conn, code, reason) { invisible(.Call('_httpuv_closeWS', PACKAGE = 'httpuv', conn, code, reason)) } -makeTcpServer <- function(host, port, onHeaders, onBodyData, onRequest, onWSOpen, onWSMessage, onWSClose, getStaticPaths) { - .Call('_httpuv_makeTcpServer', PACKAGE = 'httpuv', host, port, onHeaders, onBodyData, onRequest, onWSOpen, onWSMessage, onWSClose, getStaticPaths) +makeTcpServer <- function(host, port, onHeaders, onBodyData, onRequest, onWSOpen, onWSMessage, onWSClose, staticPaths, staticPathOptions) { + .Call('_httpuv_makeTcpServer', PACKAGE = 'httpuv', host, port, onHeaders, onBodyData, onRequest, onWSOpen, onWSMessage, onWSClose, staticPaths, staticPathOptions) } -makePipeServer <- function(name, mask, onHeaders, onBodyData, onRequest, onWSOpen, onWSMessage, onWSClose, getStaticPaths) { - .Call('_httpuv_makePipeServer', PACKAGE = 'httpuv', name, mask, onHeaders, onBodyData, onRequest, onWSOpen, onWSMessage, onWSClose, getStaticPaths) +makePipeServer <- function(name, mask, onHeaders, onBodyData, onRequest, onWSOpen, onWSMessage, onWSClose, staticPaths, staticPathOptions) { + .Call('_httpuv_makePipeServer', PACKAGE = 'httpuv', name, mask, onHeaders, onBodyData, onRequest, onWSOpen, onWSMessage, onWSClose, staticPaths, staticPathOptions) } stopServer_ <- function(handle) { @@ -33,6 +33,14 @@ removeStaticPaths_ <- function(handle, paths) { .Call('_httpuv_removeStaticPaths_', PACKAGE = 'httpuv', handle, paths) } +getStaticPathOptions_ <- function(handle) { + .Call('_httpuv_getStaticPathOptions_', PACKAGE = 'httpuv', handle) +} + +setStaticPathOptions_ <- function(handle, opts) { + .Call('_httpuv_setStaticPathOptions_', PACKAGE = 'httpuv', handle, opts) +} + base64encode <- function(x) { .Call('_httpuv_base64encode', PACKAGE = 'httpuv', x) } diff --git a/R/httpuv.R b/R/httpuv.R index 5fd589b9..f7733eac 100644 --- a/R/httpuv.R +++ b/R/httpuv.R @@ -171,7 +171,8 @@ AppWrapper <- setRefClass( .app = 'ANY', .wsconns = 'environment', .supportsOnHeaders = 'logical', - .staticPaths = 'list' + .staticPaths = 'list', + .staticPathOptions = 'ANY' ), methods = list( initialize = function(app) { @@ -193,11 +194,24 @@ AppWrapper <- setRefClass( # # If .app is a reference class, accessing .app$staticPaths can error if # not present. - if (class(try(.app$staticPaths, silent = TRUE)) == "try-error") { - .staticPaths <<- normalizeStaticPaths(NULL) + if (class(try(.app$staticPaths, silent = TRUE)) == "try-error" || + is.null(.app$staticPaths)) + { + .staticPaths <<- list() } else { .staticPaths <<- normalizeStaticPaths(.app$staticPaths) } + + if (class(try(.app$staticPathOptions, silent = TRUE)) == "try-error" || + is.null(.app$staticPathOptions)) + { + # Use defaults + .staticPathOptions <<- staticPathOptions() + } else if (inherits(.app$staticPathOptions, "staticPathOptions")) { + .staticPathOptions <<- .app$staticPathOptions + } else { + stop("staticPathOptions must be an object of class staticPathOptions.") + } }, onHeaders = function(req) { if (!.supportsOnHeaders) @@ -265,9 +279,6 @@ AppWrapper <- setRefClass( for (handler in ws$.closeCallbacks) { handler() } - }, - getStaticPaths = function() { - .staticPaths } ) ) diff --git a/R/server.R b/R/server.R index a2ddb2fa..3b67406b 100644 --- a/R/server.R +++ b/R/server.R @@ -22,13 +22,29 @@ Server <- R6Class("Server", getStaticPaths = function() { getStaticPaths_(private$handle) }, - setStaticPaths = function(paths) { + setStaticPath = function(..., .list = NULL) { + paths <- c(list(...), .list) + paths <- drop_duplicate_names(paths) paths <- normalizeStaticPaths(paths) invisible(setStaticPaths_(private$handle, paths)) }, - removeStaticPaths = function(paths) { - paths <- as.character(paths) - invisible(removeStaticPaths_(private$handle, paths)) + removeStaticPath = function(path) { + path <- as.character(path) + invisible(removeStaticPaths_(private$handle, path)) + }, + getStaticPathOptions = function() { + getStaticPathOptions_(private$handle) + }, + setStaticPathOption = function(..., .list = NULL) { + opts <- c(list(...), .list) + opts <- drop_duplicate_names(opts) + + unknown_opt_idx <- !(names(opts) %in% names(formals(staticPathOptions))) + if (any(unknown_opt_idx)) { + stop("Unknown options: ", paste(names(opts)[unknown_opt_idx], ", ")) + } + + invisible(setStaticPathOptions_(private$handle, opts)) } ), private = list( @@ -56,7 +72,8 @@ WebServer <- R6Class("WebServer", private$appWrapper$onWSOpen, private$appWrapper$onWSMessage, private$appWrapper$onWSClose, - private$appWrapper$getStaticPaths + private$appWrapper$.staticPaths, + private$appWrapper$.staticPathOptions ) if (is.null(private$handle)) { @@ -91,7 +108,9 @@ PipeServer <- R6Class("PipeServer", private$appWrapper$call, private$appWrapper$onWSOpen, private$appWrapper$onWSMessage, - private$appWrapper$onWSClose + private$appWrapper$onWSClose, + private$appWrapper$.staticPaths, + private$appWrapper$.staticPathOptions ) # Save the full path. normalizePath must be called after makePipeServer diff --git a/R/static_paths.R b/R/static_paths.R index 9d0930a7..24313f06 100644 --- a/R/static_paths.R +++ b/R/static_paths.R @@ -12,7 +12,14 @@ #' \code{TRUE}, then instead of sending a 404 response, httpuv will call the #' application's \code{call} function, and allow it to handle the request. #' @export -staticPath <- function(path, indexhtml = TRUE, fallthrough = FALSE) { +staticPath <- function( + path, + indexhtml = NULL, + fallthrough = NULL, + html_charset = NULL, + headers = NULL, + validation = NULL +) { if (!is.character(path) || length(path) != 1 || path == "") { stop("`path` must be a non-empty string.") } @@ -22,8 +29,13 @@ staticPath <- function(path, indexhtml = TRUE, fallthrough = FALSE) { structure( list( path = path, - indexhtml = indexhtml, - fallthrough = fallthrough + options = staticPathOptions( + indexhtml = indexhtml, + fallthrough = fallthrough, + html_charset = html_charset, + headers = headers, + validation = validation + ) ), class = "staticPath" ) @@ -53,14 +65,65 @@ print.staticPath <- function(x, ...) { #' @export format.staticPath <- function(x, ...) { + format_option <- function(opt) { + if (is.null(opt)) { + "" + } else { + as.character(opt) + } + } ret <- paste0( "\n", " Local path: ", x$path, "\n", - " Use index.html: ", x$indexhtml, "\n", - " Fallthrough to R: ", x$fallthrough + " Use index.html: ", format_option(x$options$indexhtml), "\n", + " Fallthrough to R: ", format_option(x$options$fallthrough) + ) +} + +#' @export +staticPathOptions <- function( + indexhtml = TRUE, + fallthrough = FALSE, + html_charset = "utf-8", + headers = list(), + validation = list() +) { + structure( + list( + indexhtml = indexhtml, + fallthrough = fallthrough, + html_charset = html_charset, + headers = headers, + validation = validation + ), + class = "staticPathOptions" ) } +#' @export +print.staticPathOptions <- function(x, ...) { + cat(format(x, ...), sep = "\n") + invisible(x) +} + + +#' @export +format.staticPathOptions <- function(x, ...) { + format_option <- function(opt) { + if (is.null(opt)) { + "" + } else { + as.character(opt) + } + } + ret <- paste0( + "\n", + " Use index.html: ", format_option(x$indexhtml), "\n", + " Fallthrough to R: ", format_option(x$fallthrough) + ) +} + + # This function always returns a named list of staticPath objects. The names # will all start with "/". The input can be a named character vector or a # named list containing a mix of strings and staticPath objects. diff --git a/R/utils.R b/R/utils.R index 148abc23..72bcf393 100644 --- a/R/utils.R +++ b/R/utils.R @@ -26,3 +26,12 @@ any_unnamed <- function(x) { # List with name attribute; check for any "" any(!nzchar(nms)) } + +# Given a vector with multiple keys with the same name, drop any duplicated +# names. For example, with an input like list(a=1, a=2), returns list(a=1). +drop_duplicate_names <- function(x) { + if (any_unnamed(x)) { + stop("All items must be named.") + } + x[unique(names(x))] +} diff --git a/man/staticPath.Rd b/man/staticPath.Rd index 7b22ec93..bb2dd192 100644 --- a/man/staticPath.Rd +++ b/man/staticPath.Rd @@ -4,12 +4,13 @@ \alias{staticPath} \title{Create a staticPath object} \usage{ -staticPath(path, index = TRUE, fallthrough = FALSE) +staticPath(path, indexhtml = NULL, fallthrough = NULL, + html_charset = NULL, headers = NULL, validation = NULL) } \arguments{ \item{path}{The local path.} -\item{index}{If an index.html file is present, should it be served up when +\item{indexhtml}{If an index.html file is present, should it be served up when the client requests \code{/} or any subdirectory?} \item{fallthrough}{With the default value, \code{FALSE}, if a request is made diff --git a/src/RcppExports.cpp b/src/RcppExports.cpp index f1173577..c5f2670c 100644 --- a/src/RcppExports.cpp +++ b/src/RcppExports.cpp @@ -30,8 +30,8 @@ BEGIN_RCPP END_RCPP } // makeTcpServer -Rcpp::RObject makeTcpServer(const std::string& host, int port, Rcpp::Function onHeaders, Rcpp::Function onBodyData, Rcpp::Function onRequest, Rcpp::Function onWSOpen, Rcpp::Function onWSMessage, Rcpp::Function onWSClose, Rcpp::Function getStaticPaths); -RcppExport SEXP _httpuv_makeTcpServer(SEXP hostSEXP, SEXP portSEXP, SEXP onHeadersSEXP, SEXP onBodyDataSEXP, SEXP onRequestSEXP, SEXP onWSOpenSEXP, SEXP onWSMessageSEXP, SEXP onWSCloseSEXP, SEXP getStaticPathsSEXP) { +Rcpp::RObject makeTcpServer(const std::string& host, int port, Rcpp::Function onHeaders, Rcpp::Function onBodyData, Rcpp::Function onRequest, Rcpp::Function onWSOpen, Rcpp::Function onWSMessage, Rcpp::Function onWSClose, Rcpp::List staticPaths, Rcpp::List staticPathOptions); +RcppExport SEXP _httpuv_makeTcpServer(SEXP hostSEXP, SEXP portSEXP, SEXP onHeadersSEXP, SEXP onBodyDataSEXP, SEXP onRequestSEXP, SEXP onWSOpenSEXP, SEXP onWSMessageSEXP, SEXP onWSCloseSEXP, SEXP staticPathsSEXP, SEXP staticPathOptionsSEXP) { BEGIN_RCPP Rcpp::RObject rcpp_result_gen; Rcpp::RNGScope rcpp_rngScope_gen; @@ -43,14 +43,15 @@ BEGIN_RCPP Rcpp::traits::input_parameter< Rcpp::Function >::type onWSOpen(onWSOpenSEXP); Rcpp::traits::input_parameter< Rcpp::Function >::type onWSMessage(onWSMessageSEXP); Rcpp::traits::input_parameter< Rcpp::Function >::type onWSClose(onWSCloseSEXP); - Rcpp::traits::input_parameter< Rcpp::Function >::type getStaticPaths(getStaticPathsSEXP); - rcpp_result_gen = Rcpp::wrap(makeTcpServer(host, port, onHeaders, onBodyData, onRequest, onWSOpen, onWSMessage, onWSClose, getStaticPaths)); + Rcpp::traits::input_parameter< Rcpp::List >::type staticPaths(staticPathsSEXP); + Rcpp::traits::input_parameter< Rcpp::List >::type staticPathOptions(staticPathOptionsSEXP); + rcpp_result_gen = Rcpp::wrap(makeTcpServer(host, port, onHeaders, onBodyData, onRequest, onWSOpen, onWSMessage, onWSClose, staticPaths, staticPathOptions)); return rcpp_result_gen; END_RCPP } // makePipeServer -Rcpp::RObject makePipeServer(const std::string& name, int mask, Rcpp::Function onHeaders, Rcpp::Function onBodyData, Rcpp::Function onRequest, Rcpp::Function onWSOpen, Rcpp::Function onWSMessage, Rcpp::Function onWSClose, Rcpp::Function getStaticPaths); -RcppExport SEXP _httpuv_makePipeServer(SEXP nameSEXP, SEXP maskSEXP, SEXP onHeadersSEXP, SEXP onBodyDataSEXP, SEXP onRequestSEXP, SEXP onWSOpenSEXP, SEXP onWSMessageSEXP, SEXP onWSCloseSEXP, SEXP getStaticPathsSEXP) { +Rcpp::RObject makePipeServer(const std::string& name, int mask, Rcpp::Function onHeaders, Rcpp::Function onBodyData, Rcpp::Function onRequest, Rcpp::Function onWSOpen, Rcpp::Function onWSMessage, Rcpp::Function onWSClose, Rcpp::List staticPaths, Rcpp::List staticPathOptions); +RcppExport SEXP _httpuv_makePipeServer(SEXP nameSEXP, SEXP maskSEXP, SEXP onHeadersSEXP, SEXP onBodyDataSEXP, SEXP onRequestSEXP, SEXP onWSOpenSEXP, SEXP onWSMessageSEXP, SEXP onWSCloseSEXP, SEXP staticPathsSEXP, SEXP staticPathOptionsSEXP) { BEGIN_RCPP Rcpp::RObject rcpp_result_gen; Rcpp::RNGScope rcpp_rngScope_gen; @@ -62,8 +63,9 @@ BEGIN_RCPP Rcpp::traits::input_parameter< Rcpp::Function >::type onWSOpen(onWSOpenSEXP); Rcpp::traits::input_parameter< Rcpp::Function >::type onWSMessage(onWSMessageSEXP); Rcpp::traits::input_parameter< Rcpp::Function >::type onWSClose(onWSCloseSEXP); - Rcpp::traits::input_parameter< Rcpp::Function >::type getStaticPaths(getStaticPathsSEXP); - rcpp_result_gen = Rcpp::wrap(makePipeServer(name, mask, onHeaders, onBodyData, onRequest, onWSOpen, onWSMessage, onWSClose, getStaticPaths)); + Rcpp::traits::input_parameter< Rcpp::List >::type staticPaths(staticPathsSEXP); + Rcpp::traits::input_parameter< Rcpp::List >::type staticPathOptions(staticPathOptionsSEXP); + rcpp_result_gen = Rcpp::wrap(makePipeServer(name, mask, onHeaders, onBodyData, onRequest, onWSOpen, onWSMessage, onWSClose, staticPaths, staticPathOptions)); return rcpp_result_gen; END_RCPP } @@ -112,6 +114,29 @@ BEGIN_RCPP return rcpp_result_gen; END_RCPP } +// getStaticPathOptions_ +Rcpp::List getStaticPathOptions_(std::string handle); +RcppExport SEXP _httpuv_getStaticPathOptions_(SEXP handleSEXP) { +BEGIN_RCPP + Rcpp::RObject rcpp_result_gen; + Rcpp::RNGScope rcpp_rngScope_gen; + Rcpp::traits::input_parameter< std::string >::type handle(handleSEXP); + rcpp_result_gen = Rcpp::wrap(getStaticPathOptions_(handle)); + return rcpp_result_gen; +END_RCPP +} +// setStaticPathOptions_ +Rcpp::List setStaticPathOptions_(std::string handle, Rcpp::List opts); +RcppExport SEXP _httpuv_setStaticPathOptions_(SEXP handleSEXP, SEXP optsSEXP) { +BEGIN_RCPP + Rcpp::RObject rcpp_result_gen; + Rcpp::RNGScope rcpp_rngScope_gen; + Rcpp::traits::input_parameter< std::string >::type handle(handleSEXP); + Rcpp::traits::input_parameter< Rcpp::List >::type opts(optsSEXP); + rcpp_result_gen = Rcpp::wrap(setStaticPathOptions_(handle, opts)); + return rcpp_result_gen; +END_RCPP +} // base64encode std::string base64encode(const Rcpp::RawVector& x); RcppExport SEXP _httpuv_base64encode(SEXP xSEXP) { @@ -215,12 +240,14 @@ RcppExport SEXP httpuv_decodeURIComponent(SEXP); static const R_CallMethodDef CallEntries[] = { {"_httpuv_sendWSMessage", (DL_FUNC) &_httpuv_sendWSMessage, 3}, {"_httpuv_closeWS", (DL_FUNC) &_httpuv_closeWS, 3}, - {"_httpuv_makeTcpServer", (DL_FUNC) &_httpuv_makeTcpServer, 9}, - {"_httpuv_makePipeServer", (DL_FUNC) &_httpuv_makePipeServer, 9}, + {"_httpuv_makeTcpServer", (DL_FUNC) &_httpuv_makeTcpServer, 10}, + {"_httpuv_makePipeServer", (DL_FUNC) &_httpuv_makePipeServer, 10}, {"_httpuv_stopServer_", (DL_FUNC) &_httpuv_stopServer_, 1}, {"_httpuv_getStaticPaths_", (DL_FUNC) &_httpuv_getStaticPaths_, 1}, {"_httpuv_setStaticPaths_", (DL_FUNC) &_httpuv_setStaticPaths_, 2}, {"_httpuv_removeStaticPaths_", (DL_FUNC) &_httpuv_removeStaticPaths_, 2}, + {"_httpuv_getStaticPathOptions_", (DL_FUNC) &_httpuv_getStaticPathOptions_, 1}, + {"_httpuv_setStaticPathOptions_", (DL_FUNC) &_httpuv_setStaticPathOptions_, 2}, {"_httpuv_base64encode", (DL_FUNC) &_httpuv_base64encode, 1}, {"_httpuv_encodeURI", (DL_FUNC) &_httpuv_encodeURI, 1}, {"_httpuv_encodeURIComponent", (DL_FUNC) &_httpuv_encodeURIComponent, 1}, @@ -230,7 +257,7 @@ static const R_CallMethodDef CallEntries[] = { {"_httpuv_invokeCppCallback", (DL_FUNC) &_httpuv_invokeCppCallback, 2}, {"_httpuv_getRNGState", (DL_FUNC) &_httpuv_getRNGState, 0}, {"_httpuv_wsconn_address", (DL_FUNC) &_httpuv_wsconn_address, 1}, - {"httpuv_decodeURIComponent", (DL_FUNC) &httpuv_decodeURIComponent, 1}, + {"httpuv_decodeURIComponent", (DL_FUNC) &httpuv_decodeURIComponent, 1}, {NULL, NULL, 0} }; diff --git a/src/httpuv.cpp b/src/httpuv.cpp index b5e73e67..5bbbab72 100644 --- a/src/httpuv.cpp +++ b/src/httpuv.cpp @@ -245,7 +245,8 @@ Rcpp::RObject makeTcpServer(const std::string& host, int port, Rcpp::Function onWSOpen, Rcpp::Function onWSMessage, Rcpp::Function onWSClose, - Rcpp::Function getStaticPaths) { + Rcpp::List staticPaths, + Rcpp::List staticPathOptions) { using namespace Rcpp; register_main_thread(); @@ -255,7 +256,7 @@ Rcpp::RObject makeTcpServer(const std::string& host, int port, boost::shared_ptr pHandler( new RWebApplication(onHeaders, onBodyData, onRequest, onWSOpen, onWSMessage, onWSClose, - getStaticPaths), + staticPaths, staticPathOptions), auto_deleter_main ); @@ -300,7 +301,8 @@ Rcpp::RObject makePipeServer(const std::string& name, Rcpp::Function onWSOpen, Rcpp::Function onWSMessage, Rcpp::Function onWSClose, - Rcpp::Function getStaticPaths) { + Rcpp::List staticPaths, + Rcpp::List staticPathOptions) { using namespace Rcpp; register_main_thread(); @@ -310,7 +312,7 @@ Rcpp::RObject makePipeServer(const std::string& name, boost::shared_ptr pHandler( new RWebApplication(onHeaders, onBodyData, onRequest, onWSOpen, onWSMessage, onWSClose, - getStaticPaths), + staticPaths, staticPathOptions), auto_deleter_main ); @@ -414,6 +416,20 @@ Rcpp::List removeStaticPaths_(std::string handle, Rcpp::CharacterVector paths) { return getStaticPaths_(handle); } +// [[Rcpp::export]] +Rcpp::List getStaticPathOptions_(std::string handle) { + ASSERT_MAIN_THREAD() + return get_pWebApplication(handle)->getStaticPathList().getOptions().asRObject(); +} + + +// [[Rcpp::export]] +Rcpp::List setStaticPathOptions_(std::string handle, Rcpp::List opts) { + ASSERT_MAIN_THREAD() + get_pWebApplication(handle)->getStaticPathList().setOptions(opts); + return getStaticPathOptions_(handle); +} + // ============================================================================ // Miscellaneous utility functions diff --git a/src/staticpath.cpp b/src/staticpath.cpp index 531d8651..1f2288a3 100644 --- a/src/staticpath.cpp +++ b/src/staticpath.cpp @@ -3,15 +3,77 @@ #include "utils.h" #include +// ============================================================================ +// StaticPathOptions +// ============================================================================ + +// (Use boost::optional instead of optional_as) +StaticPathOptions::StaticPathOptions(const Rcpp::List& options) { + ASSERT_MAIN_THREAD() + + std::string obj_class = options.attr("class"); + if (obj_class != "staticPathOptions") { + throw Rcpp::exception("staticPath options object must have class 'staticPathOptions'."); + } + + // There's probably a more concise way to do this assignment. + Rcpp::RObject temp; + temp = options["indexhtml"]; indexhtml = optional_as(temp); + temp = options["fallthrough"]; fallthrough = optional_as(temp); +} + +void StaticPathOptions::setOptions(const Rcpp::List& options) { + ASSERT_MAIN_THREAD() + Rcpp::RObject temp; + if (options.containsElementNamed("indexhtml")) { + temp = options["indexhtml"]; + if (!temp.isNULL()) { + indexhtml = optional_as(temp); + } + } + if (options.containsElementNamed("fallthrough")) { + temp = options["fallthrough"]; + if (!temp.isNULL()) { + fallthrough = optional_as(temp); + } + } +} + +Rcpp::List StaticPathOptions::asRObject() const { + ASSERT_MAIN_THREAD() + using namespace Rcpp; + + List obj = List::create( + _["indexhtml"] = optional_wrap(indexhtml), + _["fallthrough"] = optional_wrap(fallthrough) + ); + + obj.attr("class") = "staticPathOptions"; + + return obj; +} + +StaticPathOptions StaticPathOptions::merge( + const StaticPathOptions& a, + const StaticPathOptions& b) +{ + StaticPathOptions new_sp = a; + if (new_sp.indexhtml == boost::none) new_sp.indexhtml = b.indexhtml; + if (new_sp.fallthrough == boost::none) new_sp.fallthrough = b.fallthrough; + return new_sp; +} + + // ============================================================================ // StaticPath // ============================================================================ StaticPath::StaticPath(const Rcpp::List& sp) { ASSERT_MAIN_THREAD() - path = Rcpp::as(sp["path"]); - indexhtml = Rcpp::as (sp["indexhtml"]); - fallthrough = Rcpp::as (sp["fallthrough"]); + path = Rcpp::as(sp["path"]); + + Rcpp::List options_list = sp["options"]; + options = StaticPathOptions(options_list); if (path.at(path.length() - 1) == '/') { throw std::runtime_error("Static path must not have trailing slash."); @@ -23,11 +85,10 @@ Rcpp::List StaticPath::asRObject() const { using namespace Rcpp; List obj = List::create( - _["path"] = path, - _["indexhtml"] = indexhtml, - _["fallthrough"] = fallthrough + _["path"] = path, + _["options"] = options.asRObject() ); - + obj.attr("class") = "staticPath"; return obj; @@ -41,45 +102,53 @@ StaticPathList::StaticPathList() { uv_mutex_init(&mutex); } -StaticPathList::StaticPathList(const Rcpp::List& source) { +StaticPathList::StaticPathList(const Rcpp::List& path_list, const Rcpp::List& options_list) { ASSERT_MAIN_THREAD() uv_mutex_init(&mutex); - if (source.size() == 0) { + this->options = StaticPathOptions(options_list); + + if (path_list.size() == 0) { return; } - Rcpp::CharacterVector names = source.names(); + Rcpp::CharacterVector names = path_list.names(); if (names.isNULL()) { - throw Rcpp::exception("Error processing static paths."); + throw Rcpp::exception("Error processing static paths: all static paths must be named."); } - for (int i=0; i(names[i]); if (name == "") { throw Rcpp::exception("Error processing static paths."); } - Rcpp::List sp(source[i]); + Rcpp::List sp(path_list[i]); StaticPath staticpath(sp); - path_map.insert( + this->path_map.insert( std::pair(name, staticpath) ); } } -boost::optional StaticPathList::get(const std::string& path) const { +// Returns a StaticPath object, which has its options merged with the overall ones. +boost::optional StaticPathList::get(const std::string& path) const { guard guard(mutex); std::map::const_iterator it = path_map.find(path); if (it == path_map.end()) { return boost::none; } - return it->second; + + // Get a copy of the StaticPath object; we'll modify the options in the copy + // by merging it with the overall options. + StaticPath sp = it->second; + sp.options = StaticPathOptions::merge(sp.options, this->options); + return sp; } -boost::optional StaticPathList::get(const Rcpp::CharacterVector& path) const { +boost::optional StaticPathList::get(const Rcpp::CharacterVector& path) const { ASSERT_MAIN_THREAD() if (path.size() != 1) { throw Rcpp::exception("Can only get a single StaticPath object."); @@ -157,7 +226,7 @@ void StaticPathList::remove(const Rcpp::CharacterVector& paths) { // // If no matching static path is found, then it returns boost::none. // -boost::optional> StaticPathList::matchStaticPath( +boost::optional> StaticPathList::matchStaticPath( const std::string& url_path) const { @@ -187,9 +256,9 @@ boost::optional> StaticPathList::match // split on. while (true) { // Check if the part before the split-on '/' is in the staticPath. - boost::optional sp = this->get(pre_slash); + boost::optional sp = this->get(pre_slash); if (sp) { - return std::pair(*sp, post_slash); + return std::pair(*sp, post_slash); } if (found_idx == 0) { @@ -217,7 +286,15 @@ boost::optional> StaticPathList::match } } +const StaticPathOptions& StaticPathList::getOptions() const { + return options; +}; + +void StaticPathList::setOptions(const Rcpp::List& opts) { + options.setOptions(opts); +}; +// Returns the R objects, without option merging. Rcpp::List StaticPathList::asRObject() const { ASSERT_MAIN_THREAD() guard guard(mutex); diff --git a/src/staticpath.h b/src/staticpath.h index cb52d871..80ee78b0 100644 --- a/src/staticpath.h +++ b/src/staticpath.h @@ -7,19 +7,26 @@ #include #include "thread.h" +class StaticPathOptions { +public: + boost::optional indexhtml = boost::none; + boost::optional fallthrough = boost::none; + + StaticPathOptions() {}; + StaticPathOptions(const Rcpp::List& options); + + void setOptions(const Rcpp::List& options); + + Rcpp::List asRObject() const; + + static StaticPathOptions merge(const StaticPathOptions& a, const StaticPathOptions& b); +}; + + class StaticPath { public: std::string path; - bool indexhtml; - bool fallthrough; - - StaticPath(std::string _path, bool _indexhtml, bool _fallthrough) : - path(_path), indexhtml(_indexhtml), fallthrough(_fallthrough) - { - if (path.at(path.length() - 1) == '/') { - throw std::runtime_error("Static path must not have trailing slash."); - } - } + StaticPathOptions options; StaticPath(const Rcpp::List& sp); @@ -32,12 +39,14 @@ class StaticPathList { // Mutex is used whenever path_map is accessed. mutable uv_mutex_t mutex; + StaticPathOptions options; + public: - StaticPathList(); - StaticPathList(const Rcpp::List& source); + StaticPathList(); + StaticPathList(const Rcpp::List& path_list, const Rcpp::List& options_list); - boost::optional get(const std::string& path) const; - boost::optional get(const Rcpp::CharacterVector& path) const; + boost::optional get(const std::string& path) const; + boost::optional get(const Rcpp::CharacterVector& path) const; void set(const std::string& path, const StaticPath& sp); void set(const std::map& pmap); @@ -47,9 +56,13 @@ class StaticPathList { void remove(const std::vector& paths); void remove(const Rcpp::CharacterVector& paths); - boost::optional> matchStaticPath( + boost::optional> matchStaticPath( const std::string& url_path) const; + + const StaticPathOptions& getOptions() const; + void setOptions(const Rcpp::List& opts); + Rcpp::List asRObject() const; }; diff --git a/src/utils.h b/src/utils.h index fa30c8e1..1b608bc0 100644 --- a/src/utils.h +++ b/src/utils.h @@ -8,6 +8,7 @@ #include #include #include +#include #include "thread.h" // A callback for deleting objects on the main thread using later(). This is @@ -87,11 +88,11 @@ std::string toString(T x) { } // This is used for converting an Rcpp named vector (T2) to a std::map. -template -std::map toMap(T2 x) { +template +std::map toMap(T2 x) { ASSERT_MAIN_THREAD() - std::map strmap; + std::map strmap; if (x.size() == 0) { return strmap; @@ -104,18 +105,37 @@ std::map toMap(T2 x) { for (int i=0; i(names[i]); - T value = Rcpp::as (x[i]); + T1 value = Rcpp::as (x[i]); if (name == "") { throw Rcpp::exception("Error converting R object to map: element has empty name."); } strmap.insert( - std::pair(name, value) + std::pair(name, value) ); } return strmap; } +// A wrapper for Rcpp::as. If the R value is NULL, this returns boost::none; +// otherwise it returns the usual value that Rcpp::as returns, wrapped in +// boost::optional. +template +boost::optional optional_as(T2 value) { + if (value.isNULL()) { + return boost::none; + } + return boost::optional( Rcpp::as(value) ); +} + +template +Rcpp::RObject optional_wrap(boost::optional value) { + if (value == boost::none) { + return R_NilValue; + } + return Rcpp::wrap(*value); +} + #endif diff --git a/src/webapplication.cpp b/src/webapplication.cpp index 7e901cd5..77163137 100644 --- a/src/webapplication.cpp +++ b/src/webapplication.cpp @@ -11,6 +11,10 @@ #include "fs.h" #include +// ============================================================================ +// Utility functions +// ============================================================================ + std::string normalizeHeaderName(const std::string& name) { std::string result = name; for (std::string::iterator it = result.begin(); @@ -226,6 +230,11 @@ void invokeResponseFun(boost::function)> fu fun(pResponse); } + +// ============================================================================ +// Methods +// ============================================================================ + RWebApplication::RWebApplication( Rcpp::Function onHeaders, Rcpp::Function onBodyData, @@ -233,14 +242,14 @@ RWebApplication::RWebApplication( Rcpp::Function onWSOpen, Rcpp::Function onWSMessage, Rcpp::Function onWSClose, - Rcpp::Function getStaticPaths) : + Rcpp::List staticPaths, + Rcpp::List staticPathOptions) : _onHeaders(onHeaders), _onBodyData(onBodyData), _onRequest(onRequest), - _onWSOpen(onWSOpen), _onWSMessage(onWSMessage), _onWSClose(onWSClose), - _getStaticPaths(getStaticPaths) + _onWSOpen(onWSOpen), _onWSMessage(onWSMessage), _onWSClose(onWSClose) { ASSERT_MAIN_THREAD() - _staticPathList = StaticPathList(Rcpp::List(_getStaticPaths())); + _staticPathList = StaticPathList(staticPaths, staticPathOptions); } @@ -427,7 +436,7 @@ boost::shared_ptr RWebApplication::staticFileResponse( ASSERT_BACKGROUND_THREAD() // If it has a Connection: Upgrade header, don't try to serve a static file. - // Just fall through. + // Just fall through, even if the path is one that is in the StaticPathList. if (pRequest->hasHeader("Connection", "Upgrade", true)) { return nullptr; } @@ -438,7 +447,7 @@ boost::shared_ptr RWebApplication::staticFileResponse( std::pair url_query = splitQueryString(url); std::string& url_path = url_query.first; - boost::optional> sp_pair = + boost::optional> sp_pair = _staticPathList.matchStaticPath(url_path); if (!sp_pair) { @@ -470,7 +479,7 @@ boost::shared_ptr RWebApplication::staticFileResponse( } if (is_directory(local_path)) { - if (sp.indexhtml) { + if (*(sp.options.indexhtml)) { local_path = local_path + "/" + "index.html"; } } @@ -480,10 +489,11 @@ boost::shared_ptr RWebApplication::staticFileResponse( int ret = pDataSource->initialize(local_path, false); if (ret != 0) { + std::cout << pDataSource->lastErrorMessage() << "\n"; // Couldn't read the file delete pDataSource; - if (sp.fallthrough) { + if (*(sp.options.fallthrough)) { return nullptr; } else { return error_response(pRequest, 404); @@ -493,8 +503,8 @@ boost::shared_ptr RWebApplication::staticFileResponse( int file_size = pDataSource->size(); // Use local_path instead of subpath, because if the subpath is "/foo/" and - // sp.indexhtml is true, then the local_path will be "/foo/index.html". We - // need to use the latter to determine mime type. + // *(sp.options.indexhtml) is true, then the local_path will be + // "/foo/index.html". We need to use the latter to determine mime type. std::string content_type = find_mime_type(find_extension(basename(local_path))); if (content_type == "") { content_type = "application/octet-stream"; diff --git a/src/webapplication.h b/src/webapplication.h index efd700fe..0f0df347 100644 --- a/src/webapplication.h +++ b/src/webapplication.h @@ -43,9 +43,6 @@ class RWebApplication : public WebApplication { Rcpp::Function _onWSOpen; Rcpp::Function _onWSMessage; Rcpp::Function _onWSClose; - // Note this differs from the public getStaticPathList function - this is - // the R function passed in that is used for initialization only. - Rcpp::Function _getStaticPaths; StaticPathList _staticPathList; @@ -56,7 +53,8 @@ class RWebApplication : public WebApplication { Rcpp::Function onWSOpen, Rcpp::Function onWSMessage, Rcpp::Function onWSClose, - Rcpp::Function getStaticPaths); + Rcpp::List staticPaths, + Rcpp::List staticPathOptions); virtual ~RWebApplication() { ASSERT_MAIN_THREAD() From c81e63334236e6dac6b58a2dc0911c3ed3e8c9fb Mon Sep 17 00:00:00 2001 From: Winston Chang Date: Thu, 8 Nov 2018 22:53:20 -0600 Subject: [PATCH 33/60] StaticPathList -> StaticPathManager --- src/httpuv.cpp | 10 +++++----- src/staticpath.cpp | 34 ++++++++++++++++++---------------- src/staticpath.h | 8 ++++---- src/utils.h | 3 +++ src/webapplication.cpp | 11 ++++++----- src/webapplication.h | 6 +++--- 6 files changed, 39 insertions(+), 33 deletions(-) diff --git a/src/httpuv.cpp b/src/httpuv.cpp index 5bbbab72..d47607a8 100644 --- a/src/httpuv.cpp +++ b/src/httpuv.cpp @@ -399,34 +399,34 @@ boost::shared_ptr get_pWebApplication(std::string handle) { // [[Rcpp::export]] Rcpp::List getStaticPaths_(std::string handle) { ASSERT_MAIN_THREAD() - return get_pWebApplication(handle)->getStaticPathList().asRObject(); + return get_pWebApplication(handle)->getStaticPathManager().pathsAsRObject(); } // [[Rcpp::export]] Rcpp::List setStaticPaths_(std::string handle, Rcpp::List sp) { ASSERT_MAIN_THREAD() - get_pWebApplication(handle)->getStaticPathList().set(sp); + get_pWebApplication(handle)->getStaticPathManager().set(sp); return getStaticPaths_(handle); } // [[Rcpp::export]] Rcpp::List removeStaticPaths_(std::string handle, Rcpp::CharacterVector paths) { ASSERT_MAIN_THREAD() - get_pWebApplication(handle)->getStaticPathList().remove(paths); + get_pWebApplication(handle)->getStaticPathManager().remove(paths); return getStaticPaths_(handle); } // [[Rcpp::export]] Rcpp::List getStaticPathOptions_(std::string handle) { ASSERT_MAIN_THREAD() - return get_pWebApplication(handle)->getStaticPathList().getOptions().asRObject(); + return get_pWebApplication(handle)->getStaticPathManager().getOptions().asRObject(); } // [[Rcpp::export]] Rcpp::List setStaticPathOptions_(std::string handle, Rcpp::List opts) { ASSERT_MAIN_THREAD() - get_pWebApplication(handle)->getStaticPathList().setOptions(opts); + get_pWebApplication(handle)->getStaticPathManager().setOptions(opts); return getStaticPathOptions_(handle); } diff --git a/src/staticpath.cpp b/src/staticpath.cpp index 1f2288a3..1c22281c 100644 --- a/src/staticpath.cpp +++ b/src/staticpath.cpp @@ -53,6 +53,7 @@ Rcpp::List StaticPathOptions::asRObject() const { return obj; } +// Merge StaticPathOptions object `a` with `b`. Values in `a` take precedence. StaticPathOptions StaticPathOptions::merge( const StaticPathOptions& a, const StaticPathOptions& b) @@ -96,13 +97,13 @@ Rcpp::List StaticPath::asRObject() const { // ============================================================================ -// StaticPathList +// StaticPathManager // ============================================================================ -StaticPathList::StaticPathList() { +StaticPathManager::StaticPathManager() { uv_mutex_init(&mutex); } -StaticPathList::StaticPathList(const Rcpp::List& path_list, const Rcpp::List& options_list) { +StaticPathManager::StaticPathManager(const Rcpp::List& path_list, const Rcpp::List& options_list) { ASSERT_MAIN_THREAD() uv_mutex_init(&mutex); @@ -134,7 +135,7 @@ StaticPathList::StaticPathList(const Rcpp::List& path_list, const Rcpp::List& op // Returns a StaticPath object, which has its options merged with the overall ones. -boost::optional StaticPathList::get(const std::string& path) const { +boost::optional StaticPathManager::get(const std::string& path) const { guard guard(mutex); std::map::const_iterator it = path_map.find(path); if (it == path_map.end()) { @@ -148,7 +149,7 @@ boost::optional StaticPathList::get(const std::string& path) const { return sp; } -boost::optional StaticPathList::get(const Rcpp::CharacterVector& path) const { +boost::optional StaticPathManager::get(const Rcpp::CharacterVector& path) const { ASSERT_MAIN_THREAD() if (path.size() != 1) { throw Rcpp::exception("Can only get a single StaticPath object."); @@ -157,7 +158,7 @@ boost::optional StaticPathList::get(const Rcpp::CharacterVector& pat } -void StaticPathList::set(const std::string& path, const StaticPath& sp) { +void StaticPathManager::set(const std::string& path, const StaticPath& sp) { guard guard(mutex); // If the key already exists, replace the value. std::map::iterator it = path_map.find(path); @@ -171,21 +172,21 @@ void StaticPathList::set(const std::string& path, const StaticPath& sp) { ); } -void StaticPathList::set(const std::map& pmap) { +void StaticPathManager::set(const std::map& pmap) { std::map::const_iterator it; for (it = pmap.begin(); it != pmap.end(); it++) { set(it->first, it->second); } } -void StaticPathList::set(const Rcpp::List& pmap) { +void StaticPathManager::set(const Rcpp::List& pmap) { ASSERT_MAIN_THREAD() std::map pmap2 = toMap(pmap); set(pmap2); } -void StaticPathList::remove(const std::string& path) { +void StaticPathManager::remove(const std::string& path) { guard guard(mutex); std::map::const_iterator it = path_map.find(path); if (it != path_map.end()) { @@ -193,14 +194,14 @@ void StaticPathList::remove(const std::string& path) { } } -void StaticPathList::remove(const std::vector& paths) { +void StaticPathManager::remove(const std::vector& paths) { std::vector::const_iterator it; for (it = paths.begin(); it != paths.end(); it++) { remove(*it); } } -void StaticPathList::remove(const Rcpp::CharacterVector& paths) { +void StaticPathManager::remove(const Rcpp::CharacterVector& paths) { ASSERT_MAIN_THREAD() std::vector paths_vec = Rcpp::as>(paths); remove(paths_vec); @@ -226,7 +227,7 @@ void StaticPathList::remove(const Rcpp::CharacterVector& paths) { // // If no matching static path is found, then it returns boost::none. // -boost::optional> StaticPathList::matchStaticPath( +boost::optional> StaticPathManager::matchStaticPath( const std::string& url_path) const { @@ -286,16 +287,17 @@ boost::optional> StaticPathList::matchStaticP } } -const StaticPathOptions& StaticPathList::getOptions() const { +const StaticPathOptions& StaticPathManager::getOptions() const { return options; }; -void StaticPathList::setOptions(const Rcpp::List& opts) { +void StaticPathManager::setOptions(const Rcpp::List& opts) { options.setOptions(opts); }; -// Returns the R objects, without option merging. -Rcpp::List StaticPathList::asRObject() const { +// Returns a list of R objects that reflect the StaticPaths, without merging +// the overall options. +Rcpp::List StaticPathManager::pathsAsRObject() const { ASSERT_MAIN_THREAD() guard guard(mutex); Rcpp::List obj; diff --git a/src/staticpath.h b/src/staticpath.h index 80ee78b0..f23e70a9 100644 --- a/src/staticpath.h +++ b/src/staticpath.h @@ -34,7 +34,7 @@ class StaticPath { }; -class StaticPathList { +class StaticPathManager { std::map path_map; // Mutex is used whenever path_map is accessed. mutable uv_mutex_t mutex; @@ -42,8 +42,8 @@ class StaticPathList { StaticPathOptions options; public: - StaticPathList(); - StaticPathList(const Rcpp::List& path_list, const Rcpp::List& options_list); + StaticPathManager(); + StaticPathManager(const Rcpp::List& path_list, const Rcpp::List& options_list); boost::optional get(const std::string& path) const; boost::optional get(const Rcpp::CharacterVector& path) const; @@ -63,7 +63,7 @@ class StaticPathList { const StaticPathOptions& getOptions() const; void setOptions(const Rcpp::List& opts); - Rcpp::List asRObject() const; + Rcpp::List pathsAsRObject() const; }; #endif diff --git a/src/utils.h b/src/utils.h index 1b608bc0..83f42313 100644 --- a/src/utils.h +++ b/src/utils.h @@ -129,6 +129,9 @@ boost::optional optional_as(T2 value) { return boost::optional( Rcpp::as(value) ); } +// A wrapper for Rcpp::wrap. If the C++ value is boost::none, this returns the +// R value NULL; otherwise it returns the usual value that Rcpp::wrap returns, after +// unwrapping from the boost::optional. template Rcpp::RObject optional_wrap(boost::optional value) { if (value == boost::none) { diff --git a/src/webapplication.cpp b/src/webapplication.cpp index 77163137..7185f6a1 100644 --- a/src/webapplication.cpp +++ b/src/webapplication.cpp @@ -249,7 +249,7 @@ RWebApplication::RWebApplication( { ASSERT_MAIN_THREAD() - _staticPathList = StaticPathList(staticPaths, staticPathOptions); + _staticPathManager = StaticPathManager(staticPaths, staticPathOptions); } @@ -436,7 +436,8 @@ boost::shared_ptr RWebApplication::staticFileResponse( ASSERT_BACKGROUND_THREAD() // If it has a Connection: Upgrade header, don't try to serve a static file. - // Just fall through, even if the path is one that is in the StaticPathList. + // Just fall through, even if the path is one that is in the + // StaticPathManager. if (pRequest->hasHeader("Connection", "Upgrade", true)) { return nullptr; } @@ -448,7 +449,7 @@ boost::shared_ptr RWebApplication::staticFileResponse( std::string& url_path = url_query.first; boost::optional> sp_pair = - _staticPathList.matchStaticPath(url_path); + _staticPathManager.matchStaticPath(url_path); if (!sp_pair) { // This was not a static path. @@ -536,6 +537,6 @@ boost::shared_ptr RWebApplication::staticFileResponse( return pResponse; } -StaticPathList& RWebApplication::getStaticPathList() { - return _staticPathList; +StaticPathManager& RWebApplication::getStaticPathManager() { + return _staticPathManager; } diff --git a/src/webapplication.h b/src/webapplication.h index 0f0df347..bc236465 100644 --- a/src/webapplication.h +++ b/src/webapplication.h @@ -31,7 +31,7 @@ class WebApplication { virtual boost::shared_ptr staticFileResponse( boost::shared_ptr pRequest) = 0; - virtual StaticPathList& getStaticPathList() = 0; + virtual StaticPathManager& getStaticPathManager() = 0; }; @@ -44,7 +44,7 @@ class RWebApplication : public WebApplication { Rcpp::Function _onWSMessage; Rcpp::Function _onWSClose; - StaticPathList _staticPathList; + StaticPathManager _staticPathManager; public: RWebApplication(Rcpp::Function onHeaders, @@ -77,7 +77,7 @@ class RWebApplication : public WebApplication { virtual boost::shared_ptr staticFileResponse( boost::shared_ptr pRequest); - virtual StaticPathList& getStaticPathList(); + virtual StaticPathManager& getStaticPathManager(); }; From 3f8ef1732b6a3f4857d912432c7e22bf07e55186 Mon Sep 17 00:00:00 2001 From: Winston Chang Date: Thu, 8 Nov 2018 23:40:58 -0600 Subject: [PATCH 34/60] Add html_charset option --- R/static_paths.R | 14 ++++++++------ src/staticpath.cpp | 27 ++++++++++++++++++++------- src/staticpath.h | 12 ++++++++---- src/webapplication.cpp | 6 ++++-- 4 files changed, 40 insertions(+), 19 deletions(-) diff --git a/R/static_paths.R b/R/static_paths.R index 24313f06..a0a9e48a 100644 --- a/R/static_paths.R +++ b/R/static_paths.R @@ -67,7 +67,7 @@ print.staticPath <- function(x, ...) { format.staticPath <- function(x, ...) { format_option <- function(opt) { if (is.null(opt)) { - "" + "" } else { as.character(opt) } @@ -75,8 +75,9 @@ format.staticPath <- function(x, ...) { ret <- paste0( "\n", " Local path: ", x$path, "\n", - " Use index.html: ", format_option(x$options$indexhtml), "\n", - " Fallthrough to R: ", format_option(x$options$fallthrough) + " Use index.html: ", format_option(x$options$indexhtml), "\n", + " Fallthrough to R: ", format_option(x$options$fallthrough), "\n", + " HTML charset: ", format_option(x$options$html_charset), "\n" ) } @@ -111,15 +112,16 @@ print.staticPathOptions <- function(x, ...) { format.staticPathOptions <- function(x, ...) { format_option <- function(opt) { if (is.null(opt)) { - "" + "" } else { as.character(opt) } } ret <- paste0( "\n", - " Use index.html: ", format_option(x$indexhtml), "\n", - " Fallthrough to R: ", format_option(x$fallthrough) + " Use index.html: ", format_option(x$indexhtml), "\n", + " Fallthrough to R: ", format_option(x$fallthrough), "\n", + " HTML charset: ", format_option(x$html_charset), "\n" ) } diff --git a/src/staticpath.cpp b/src/staticpath.cpp index 1c22281c..a9930787 100644 --- a/src/staticpath.cpp +++ b/src/staticpath.cpp @@ -8,7 +8,11 @@ // ============================================================================ // (Use boost::optional instead of optional_as) -StaticPathOptions::StaticPathOptions(const Rcpp::List& options) { +StaticPathOptions::StaticPathOptions(const Rcpp::List& options) : + indexhtml(boost::none), + fallthrough(boost::none), + html_charset(boost::none) +{ ASSERT_MAIN_THREAD() std::string obj_class = options.attr("class"); @@ -18,8 +22,9 @@ StaticPathOptions::StaticPathOptions(const Rcpp::List& options) { // There's probably a more concise way to do this assignment. Rcpp::RObject temp; - temp = options["indexhtml"]; indexhtml = optional_as(temp); - temp = options["fallthrough"]; fallthrough = optional_as(temp); + temp = options["indexhtml"]; indexhtml = optional_as(temp); + temp = options["fallthrough"]; fallthrough = optional_as(temp); + temp = options["html_charset"]; html_charset = optional_as(temp); } void StaticPathOptions::setOptions(const Rcpp::List& options) { @@ -37,6 +42,12 @@ void StaticPathOptions::setOptions(const Rcpp::List& options) { fallthrough = optional_as(temp); } } + if (options.containsElementNamed("html_charset")) { + temp = options["html_charset"]; + if (!temp.isNULL()) { + html_charset = optional_as(temp); + } + } } Rcpp::List StaticPathOptions::asRObject() const { @@ -44,8 +55,9 @@ Rcpp::List StaticPathOptions::asRObject() const { using namespace Rcpp; List obj = List::create( - _["indexhtml"] = optional_wrap(indexhtml), - _["fallthrough"] = optional_wrap(fallthrough) + _["indexhtml"] = optional_wrap(indexhtml), + _["fallthrough"] = optional_wrap(fallthrough), + _["html_charset"] = optional_wrap(html_charset) ); obj.attr("class") = "staticPathOptions"; @@ -59,8 +71,9 @@ StaticPathOptions StaticPathOptions::merge( const StaticPathOptions& b) { StaticPathOptions new_sp = a; - if (new_sp.indexhtml == boost::none) new_sp.indexhtml = b.indexhtml; - if (new_sp.fallthrough == boost::none) new_sp.fallthrough = b.fallthrough; + if (new_sp.indexhtml == boost::none) new_sp.indexhtml = b.indexhtml; + if (new_sp.fallthrough == boost::none) new_sp.fallthrough = b.fallthrough; + if (new_sp.html_charset == boost::none) new_sp.html_charset = b.html_charset; return new_sp; } diff --git a/src/staticpath.h b/src/staticpath.h index f23e70a9..eb3fb0c4 100644 --- a/src/staticpath.h +++ b/src/staticpath.h @@ -9,10 +9,14 @@ class StaticPathOptions { public: - boost::optional indexhtml = boost::none; - boost::optional fallthrough = boost::none; - - StaticPathOptions() {}; + boost::optional indexhtml; + boost::optional fallthrough; + boost::optional html_charset; + StaticPathOptions() : + indexhtml(boost::none), + fallthrough(boost::none), + html_charset(boost::none) + { }; StaticPathOptions(const Rcpp::List& options); void setOptions(const Rcpp::List& options); diff --git a/src/webapplication.cpp b/src/webapplication.cpp index 7185f6a1..6a455803 100644 --- a/src/webapplication.cpp +++ b/src/webapplication.cpp @@ -510,8 +510,10 @@ boost::shared_ptr RWebApplication::staticFileResponse( if (content_type == "") { content_type = "application/octet-stream"; } else if (content_type == "text/html") { - // Always specify that encoding UTF-8 for HTML. - content_type = "text/html; charset=utf-8"; + // Add the encoding if specified by the options. + if (sp.options.html_charset) { + content_type = "text/html; charset=" + *(sp.options.html_charset); + } } if (method == "HEAD") { From c120921991bdc76c4df7947b4ecf87a6fc072c68 Mon Sep 17 00:00:00 2001 From: Winston Chang Date: Tue, 13 Nov 2018 11:52:19 -0600 Subject: [PATCH 35/60] Implement header validation option --- R/static_paths.R | 8 +++-- src/staticpath.cpp | 71 ++++++++++++++++++++++++++++++++++++++++-- src/staticpath.h | 7 ++++- src/webapplication.cpp | 5 +++ 4 files changed, 84 insertions(+), 7 deletions(-) diff --git a/R/static_paths.R b/R/static_paths.R index a0a9e48a..ce446a5b 100644 --- a/R/static_paths.R +++ b/R/static_paths.R @@ -77,7 +77,8 @@ format.staticPath <- function(x, ...) { " Local path: ", x$path, "\n", " Use index.html: ", format_option(x$options$indexhtml), "\n", " Fallthrough to R: ", format_option(x$options$fallthrough), "\n", - " HTML charset: ", format_option(x$options$html_charset), "\n" + " HTML charset: ", format_option(x$options$html_charset), "\n", + " Validation params: ", format_option(x$options$validation), "\n" ) } @@ -87,7 +88,7 @@ staticPathOptions <- function( fallthrough = FALSE, html_charset = "utf-8", headers = list(), - validation = list() + validation = NULL ) { structure( list( @@ -121,7 +122,8 @@ format.staticPathOptions <- function(x, ...) { "\n", " Use index.html: ", format_option(x$indexhtml), "\n", " Fallthrough to R: ", format_option(x$fallthrough), "\n", - " HTML charset: ", format_option(x$html_charset), "\n" + " HTML charset: ", format_option(x$html_charset), "\n", + " Validation params: ", format_option(x$validation), "\n" ) } diff --git a/src/staticpath.cpp b/src/staticpath.cpp index a9930787..9375b74e 100644 --- a/src/staticpath.cpp +++ b/src/staticpath.cpp @@ -1,20 +1,53 @@ #include "staticpath.h" #include "thread.h" #include "utils.h" +#include "constants.h" #include +#include // ============================================================================ // StaticPathOptions // ============================================================================ +// Takes an R object (should be a character vector with 3 elements), converts +// it to a native C++ vector, and checks that the validation string +// pattern is OK. Throws an exception if not. Finally, it returns the +// vector. +boost::optional> processValidationPattern(Rcpp::RObject pattern) { + ASSERT_MAIN_THREAD() + + boost::optional pattern_str_opt = optional_as(pattern); + if (pattern_str_opt == boost::none) { + return boost::none; + } + + const std::string& pattern_str = pattern_str_opt.get(); + + std::vector pattern_vec; + // Split a string like "aaa == bbb", collapsing whitespace. This is a + // little crude because it requires whitespace. + boost::split(pattern_vec, pattern_str, boost::algorithm::is_space(), boost::token_compress_on); + + if (pattern_vec.size() != 3 || + pattern_vec[1] != "==" || + pattern_vec[0].length() == 0 || + pattern_vec[2].length() == 2) + { + throw Rcpp::exception("Validation pattern must be of the form \"xx == yy\"."); + } + + return boost::optional>(pattern_vec); +} + // (Use boost::optional instead of optional_as) StaticPathOptions::StaticPathOptions(const Rcpp::List& options) : indexhtml(boost::none), fallthrough(boost::none), - html_charset(boost::none) + html_charset(boost::none), + validation(boost::none) { ASSERT_MAIN_THREAD() - + std::string obj_class = options.attr("class"); if (obj_class != "staticPathOptions") { throw Rcpp::exception("staticPath options object must have class 'staticPathOptions'."); @@ -25,6 +58,7 @@ StaticPathOptions::StaticPathOptions(const Rcpp::List& options) : temp = options["indexhtml"]; indexhtml = optional_as(temp); temp = options["fallthrough"]; fallthrough = optional_as(temp); temp = options["html_charset"]; html_charset = optional_as(temp); + temp = options["validation"]; validation = processValidationPattern(temp); } void StaticPathOptions::setOptions(const Rcpp::List& options) { @@ -48,6 +82,12 @@ void StaticPathOptions::setOptions(const Rcpp::List& options) { html_charset = optional_as(temp); } } + if (options.containsElementNamed("validation")) { + temp = options["validation"]; + if (!temp.isNULL()) { + validation = processValidationPattern(temp); + } + } } Rcpp::List StaticPathOptions::asRObject() const { @@ -57,7 +97,8 @@ Rcpp::List StaticPathOptions::asRObject() const { List obj = List::create( _["indexhtml"] = optional_wrap(indexhtml), _["fallthrough"] = optional_wrap(fallthrough), - _["html_charset"] = optional_wrap(html_charset) + _["html_charset"] = optional_wrap(html_charset), + _["validation"] = optional_wrap(validation) ); obj.attr("class") = "staticPathOptions"; @@ -74,9 +115,33 @@ StaticPathOptions StaticPathOptions::merge( if (new_sp.indexhtml == boost::none) new_sp.indexhtml = b.indexhtml; if (new_sp.fallthrough == boost::none) new_sp.fallthrough = b.fallthrough; if (new_sp.html_charset == boost::none) new_sp.html_charset = b.html_charset; + if (new_sp.validation == boost::none) new_sp.validation = b.validation; return new_sp; } +// Check if a set of request headers satisfies the condition specified by +// `validation`. +bool StaticPathOptions::validateRequestHeaders(const RequestHeaders& headers) const { + if (validation == boost::none) { + return true; + } + + const std::vector& pattern = validation.get(); + + if (pattern[1] != "==") { + // TODO: some sort of error here (background thread) + } + + RequestHeaders::const_iterator it = headers.find(pattern[0]); + if (it != headers.end() && + it->second == pattern[2]) + { + return true; + } + + return false; +} + // ============================================================================ // StaticPath diff --git a/src/staticpath.h b/src/staticpath.h index eb3fb0c4..618e70bc 100644 --- a/src/staticpath.h +++ b/src/staticpath.h @@ -6,16 +6,19 @@ #include #include #include "thread.h" +#include "constants.h" class StaticPathOptions { public: boost::optional indexhtml; boost::optional fallthrough; boost::optional html_charset; + boost::optional> validation; StaticPathOptions() : indexhtml(boost::none), fallthrough(boost::none), - html_charset(boost::none) + html_charset(boost::none), + validation(boost::none) { }; StaticPathOptions(const Rcpp::List& options); @@ -24,6 +27,8 @@ class StaticPathOptions { Rcpp::List asRObject() const; static StaticPathOptions merge(const StaticPathOptions& a, const StaticPathOptions& b); + + bool validateRequestHeaders(const RequestHeaders& headers) const; }; diff --git a/src/webapplication.cpp b/src/webapplication.cpp index 6a455803..dc3df3c8 100644 --- a/src/webapplication.cpp +++ b/src/webapplication.cpp @@ -473,6 +473,11 @@ boost::shared_ptr RWebApplication::staticFileResponse( // Note that the subpath may include leading dirs, as in "foo/bar/abc.txt". const std::string& subpath = sp_pair->second; + // Validate headers (if validation pattern was provided). + if (!sp.options.validateRequestHeaders(pRequest->headers())) { + return error_response(pRequest, 403); + } + // Path to local file on disk std::string local_path = sp.path; if (subpath != "") { From 198874ce0e9ef356c0e79369f6237bb2d5fa3ca7 Mon Sep 17 00:00:00 2001 From: Winston Chang Date: Wed, 14 Nov 2018 13:45:37 -0600 Subject: [PATCH 36/60] Process validation option in R instead of C++ --- R/httpuv.R | 2 +- R/server.R | 1 + R/static_paths.R | 53 +++++++++++++++++++++++++++++++++++++++++----- src/staticpath.cpp | 48 ++++++++++++----------------------------- 4 files changed, 64 insertions(+), 40 deletions(-) diff --git a/R/httpuv.R b/R/httpuv.R index f7733eac..3db09536 100644 --- a/R/httpuv.R +++ b/R/httpuv.R @@ -208,7 +208,7 @@ AppWrapper <- setRefClass( # Use defaults .staticPathOptions <<- staticPathOptions() } else if (inherits(.app$staticPathOptions, "staticPathOptions")) { - .staticPathOptions <<- .app$staticPathOptions + .staticPathOptions <<- normalizeStaticPathOptions(.app$staticPathOptions) } else { stop("staticPathOptions must be an object of class staticPathOptions.") } diff --git a/R/server.R b/R/server.R index 3b67406b..d6b057be 100644 --- a/R/server.R +++ b/R/server.R @@ -38,6 +38,7 @@ Server <- R6Class("Server", setStaticPathOption = function(..., .list = NULL) { opts <- c(list(...), .list) opts <- drop_duplicate_names(opts) + opts <- normalizeStaticPathOptions(opts) unknown_opt_idx <- !(names(opts) %in% names(formals(staticPathOptions))) if (any(unknown_opt_idx)) { diff --git a/R/static_paths.R b/R/static_paths.R index ce446a5b..915930a1 100644 --- a/R/static_paths.R +++ b/R/static_paths.R @@ -29,13 +29,13 @@ staticPath <- function( structure( list( path = path, - options = staticPathOptions( + options = normalizeStaticPathOptions(staticPathOptions( indexhtml = indexhtml, fallthrough = fallthrough, html_charset = html_charset, headers = headers, validation = validation - ) + )) ), class = "staticPath" ) @@ -74,7 +74,7 @@ format.staticPath <- function(x, ...) { } ret <- paste0( "\n", - " Local path: ", x$path, "\n", + " Local path: ", x$path, "\n", " Use index.html: ", format_option(x$options$indexhtml), "\n", " Fallthrough to R: ", format_option(x$options$fallthrough), "\n", " HTML charset: ", format_option(x$options$html_charset), "\n", @@ -115,7 +115,7 @@ format.staticPathOptions <- function(x, ...) { if (is.null(opt)) { "" } else { - as.character(opt) + paste(as.character(opt), collapse = " ") } } ret <- paste0( @@ -130,7 +130,8 @@ format.staticPathOptions <- function(x, ...) { # This function always returns a named list of staticPath objects. The names # will all start with "/". The input can be a named character vector or a -# named list containing a mix of strings and staticPath objects. +# named list containing a mix of strings and staticPath objects. This function +# is idempotent. normalizeStaticPaths <- function(paths) { if (is.null(paths) || length(paths) == 0) { return(list()) @@ -165,3 +166,45 @@ normalizeStaticPaths <- function(paths) { paths } + +# Takes a staticPathOptions object and modifies it so that the resulting +# object is easier to work with on the C++ side. The resulting object is not +# meant to be modified on the R side. This function is idempotent; if the +# object has already been normalized, it will not be modified. +normalizeStaticPathOptions <- function(opts) { + if (isTRUE(attr(opts, "normalized", exact = TRUE))) { + return(opts) + } + + if (!is.null(opts$validation)) { + if (!is.character(opts$validation) || length(opts$validation) != 0) { + "`validation` option must be a single-element character vector." + } + + fail <- FALSE + tryCatch( + p <- parse(text = opts$validation)[[1]], + error = function(e) fail <<- TRUE + ) + if (!fail) { + if (length(p) != 3 || + p[[1]] != as.symbol("==") || + !is.character(p[[2]]) || + length(p[[2]]) != 1 || + !is.character(p[[3]]) || + length(p[[3]]) != 1) + { + fail <- TRUE + } + } + if (fail) { + stop("`validation` must be a string of the form: '\"xxx\" == \"yyy\"'") + } + + # Turn it into a char vector for easier processing in C++ + opts$validation <- as.character(p) + } + + attr(opts, "normalized") <- TRUE + opts +} diff --git a/src/staticpath.cpp b/src/staticpath.cpp index 9375b74e..3e8269ec 100644 --- a/src/staticpath.cpp +++ b/src/staticpath.cpp @@ -9,35 +9,6 @@ // StaticPathOptions // ============================================================================ -// Takes an R object (should be a character vector with 3 elements), converts -// it to a native C++ vector, and checks that the validation string -// pattern is OK. Throws an exception if not. Finally, it returns the -// vector. -boost::optional> processValidationPattern(Rcpp::RObject pattern) { - ASSERT_MAIN_THREAD() - - boost::optional pattern_str_opt = optional_as(pattern); - if (pattern_str_opt == boost::none) { - return boost::none; - } - - const std::string& pattern_str = pattern_str_opt.get(); - - std::vector pattern_vec; - // Split a string like "aaa == bbb", collapsing whitespace. This is a - // little crude because it requires whitespace. - boost::split(pattern_vec, pattern_str, boost::algorithm::is_space(), boost::token_compress_on); - - if (pattern_vec.size() != 3 || - pattern_vec[1] != "==" || - pattern_vec[0].length() == 0 || - pattern_vec[2].length() == 2) - { - throw Rcpp::exception("Validation pattern must be of the form \"xx == yy\"."); - } - - return boost::optional>(pattern_vec); -} // (Use boost::optional instead of optional_as) StaticPathOptions::StaticPathOptions(const Rcpp::List& options) : @@ -53,12 +24,21 @@ StaticPathOptions::StaticPathOptions(const Rcpp::List& options) : throw Rcpp::exception("staticPath options object must have class 'staticPathOptions'."); } - // There's probably a more concise way to do this assignment. + // This seems to be a necessary intermediary for passing objects to + // `optional_as()`. Rcpp::RObject temp; + + temp = options.attr("normalized"); + boost::optional normalized = optional_as(temp); + if (!normalized || !normalized.get()) { + throw Rcpp::exception("staticPathOptions object must be normalized."); + } + + // There's probably a more concise way to do this assignment than by using temp. temp = options["indexhtml"]; indexhtml = optional_as(temp); temp = options["fallthrough"]; fallthrough = optional_as(temp); temp = options["html_charset"]; html_charset = optional_as(temp); - temp = options["validation"]; validation = processValidationPattern(temp); + temp = options["validation"]; validation = optional_as>(temp); } void StaticPathOptions::setOptions(const Rcpp::List& options) { @@ -85,7 +65,7 @@ void StaticPathOptions::setOptions(const Rcpp::List& options) { if (options.containsElementNamed("validation")) { temp = options["validation"]; if (!temp.isNULL()) { - validation = processValidationPattern(temp); + validation = optional_as>(temp); } } } @@ -128,11 +108,11 @@ bool StaticPathOptions::validateRequestHeaders(const RequestHeaders& headers) co const std::vector& pattern = validation.get(); - if (pattern[1] != "==") { + if (pattern[0] != "==") { // TODO: some sort of error here (background thread) } - RequestHeaders::const_iterator it = headers.find(pattern[0]); + RequestHeaders::const_iterator it = headers.find(pattern[1]); if (it != headers.end() && it->second == pattern[2]) { From 82d74170c822bc66e03ab753782645db5586a22d Mon Sep 17 00:00:00 2001 From: Winston Chang Date: Thu, 15 Nov 2018 23:56:18 -0600 Subject: [PATCH 37/60] Allow html_charset and validation to be unset but not inherit --- R/static_paths.R | 59 +++++++++++++++++++++++++----------------- src/staticpath.cpp | 14 ++++++---- src/webapplication.cpp | 8 +++--- 3 files changed, 48 insertions(+), 33 deletions(-) diff --git a/R/static_paths.R b/R/static_paths.R index 915930a1..76c1dbd4 100644 --- a/R/static_paths.R +++ b/R/static_paths.R @@ -69,7 +69,7 @@ format.staticPath <- function(x, ...) { if (is.null(opt)) { "" } else { - as.character(opt) + paste(as.character(opt), collapse = " ") } } ret <- paste0( @@ -88,7 +88,7 @@ staticPathOptions <- function( fallthrough = FALSE, html_charset = "utf-8", headers = list(), - validation = NULL + validation = character(0) ) { structure( list( @@ -176,33 +176,44 @@ normalizeStaticPathOptions <- function(opts) { return(opts) } + # html_charset can accept "" or character(0). But on the C++ side, we want + # "". + if (!is.null(opts$html_charset)) { + if (length(opts$html_charset) == 0) { + opts$html_charset <- "" + } + } + if (!is.null(opts$validation)) { - if (!is.character(opts$validation) || length(opts$validation) != 0) { - "`validation` option must be a single-element character vector." + if (!is.character(opts$validation) || length(opts$validation) > 1) { + "`validation` option must be a character vector with zero or one element." } - fail <- FALSE - tryCatch( - p <- parse(text = opts$validation)[[1]], - error = function(e) fail <<- TRUE - ) - if (!fail) { - if (length(p) != 3 || - p[[1]] != as.symbol("==") || - !is.character(p[[2]]) || - length(p[[2]]) != 1 || - !is.character(p[[3]]) || - length(p[[3]]) != 1) - { - fail <- TRUE + # If it's length 0, do nothing; if length 1, we need to parse it. + if (length(opts$validation) == 1) { + fail <- FALSE + tryCatch( + p <- parse(text = opts$validation)[[1]], + error = function(e) fail <<- TRUE + ) + if (!fail) { + if (length(p) != 3 || + p[[1]] != as.symbol("==") || + !is.character(p[[2]]) || + length(p[[2]]) != 1 || + !is.character(p[[3]]) || + length(p[[3]]) != 1) + { + fail <- TRUE + } + } + if (fail) { + stop("`validation` must be a string of the form: '\"xxx\" == \"yyy\"'") } - } - if (fail) { - stop("`validation` must be a string of the form: '\"xxx\" == \"yyy\"'") - } - # Turn it into a char vector for easier processing in C++ - opts$validation <- as.character(p) + # Turn it into a char vector for easier processing in C++ + opts$validation <- as.character(p) + } } attr(opts, "normalized") <- TRUE diff --git a/src/staticpath.cpp b/src/staticpath.cpp index 3e8269ec..4091a575 100644 --- a/src/staticpath.cpp +++ b/src/staticpath.cpp @@ -103,19 +103,23 @@ StaticPathOptions StaticPathOptions::merge( // `validation`. bool StaticPathOptions::validateRequestHeaders(const RequestHeaders& headers) const { if (validation == boost::none) { - return true; + throw std::runtime_error("Cannot validate request headers because validation pattern is not set."); } + // Should have the format {"==", "aaa", "bbb"}, or {} if there's no + // validation pattern. const std::vector& pattern = validation.get(); + if (pattern.size() == 0) { + return true; + } + if (pattern[0] != "==") { - // TODO: some sort of error here (background thread) + throw std::runtime_error("Validation only knows the == operator."); } RequestHeaders::const_iterator it = headers.find(pattern[1]); - if (it != headers.end() && - it->second == pattern[2]) - { + if (it != headers.end() && it->second == pattern[2]) { return true; } diff --git a/src/webapplication.cpp b/src/webapplication.cpp index dc3df3c8..a9592037 100644 --- a/src/webapplication.cpp +++ b/src/webapplication.cpp @@ -485,7 +485,7 @@ boost::shared_ptr RWebApplication::staticFileResponse( } if (is_directory(local_path)) { - if (*(sp.options.indexhtml)) { + if (sp.options.indexhtml.get()) { local_path = local_path + "/" + "index.html"; } } @@ -499,7 +499,7 @@ boost::shared_ptr RWebApplication::staticFileResponse( // Couldn't read the file delete pDataSource; - if (*(sp.options.fallthrough)) { + if (sp.options.fallthrough.get()) { return nullptr; } else { return error_response(pRequest, 404); @@ -516,8 +516,8 @@ boost::shared_ptr RWebApplication::staticFileResponse( content_type = "application/octet-stream"; } else if (content_type == "text/html") { // Add the encoding if specified by the options. - if (sp.options.html_charset) { - content_type = "text/html; charset=" + *(sp.options.html_charset); + if (sp.options.html_charset.get() != "") { + content_type = "text/html; charset=" + sp.options.html_charset.get(); } } From 730dd556984c3df92912b4cb3f1fa4011fcc847b Mon Sep 17 00:00:00 2001 From: Winston Chang Date: Fri, 16 Nov 2018 16:03:13 -0600 Subject: [PATCH 38/60] Documentation for staticPaths --- R/static_paths.R | 92 +++++++++++++++++++++++++++------------- man/staticPath.Rd | 28 ++++++++++-- man/staticPathOptions.Rd | 40 +++++++++++++++++ 3 files changed, 127 insertions(+), 33 deletions(-) create mode 100644 man/staticPathOptions.Rd diff --git a/R/static_paths.R b/R/static_paths.R index 76c1dbd4..7c0b90ea 100644 --- a/R/static_paths.R +++ b/R/static_paths.R @@ -1,16 +1,15 @@ #' Create a staticPath object #' -#' This function creates a \code{staticPath} object. +#' This function creates a \code{staticPath} object. Note that if any of the +#' arguments (other than \code{path}) are \code{NULL}, then that means that +#' for this particular static path, it should inherit the behavior from the +#' staticPathOptions set for the application as a whole. #' #' @param path The local path. -#' @param indexhtml If an index.html file is present, should it be served up when -#' the client requests \code{/} or any subdirectory? -#' @param fallthrough With the default value, \code{FALSE}, if a request is made -#' for a file that doesn't exist, then httpuv will immediately send a 404 -#' response from the background I/O thread, without needing to call back into -#' the main R thread. This offers the best performance. If the value is -#' \code{TRUE}, then instead of sending a 404 response, httpuv will call the -#' application's \code{call} function, and allow it to handle the request. +#' @inheritParams staticPathOptions +#' +#' @seealso \code{\link{staticPathOptions}}. +#' #' @export staticPath <- function( path, @@ -78,10 +77,36 @@ format.staticPath <- function(x, ...) { " Use index.html: ", format_option(x$options$indexhtml), "\n", " Fallthrough to R: ", format_option(x$options$fallthrough), "\n", " HTML charset: ", format_option(x$options$html_charset), "\n", + " Extra headers: ", format_option(x$options$headers), "\n", " Validation params: ", format_option(x$options$validation), "\n" ) } +#' Create options for static paths +#' +#' +#' @param indexhtml If an index.html file is present, should it be served up +#' when the client requests \code{/} or any subdirectory? +#' @param fallthrough With the default value, \code{FALSE}, if a request is made +#' for a file that doesn't exist, then httpuv will immediately send a 404 +#' response from the background I/O thread, without needing to call back into +#' the main R thread. This offers the best performance. If the value is +#' \code{TRUE}, then instead of sending a 404 response, httpuv will call the +#' application's \code{call} function, and allow it to handle the request. +#' @param html_charset When HTML files are served, the value that will be +#' provided for \code{charset} in the Content-Type header. For example, with +#' the default value, \code{"utf-8"}, the header is \code{Content-Type: +#' text/html; charset=utf-8}. If \code{""} is used, then no \code{charset} +#' will be added in the Content-Type header. +#' @param headers Additional headers and values that will be included in the response. +#' @param validation An optional validation pattern. Presently, the only type of +#' validation supported is an exact string match of a header. For example, if +#' \code{validation} is \code{'"abc" = "xyz"'}, then HTTP requests must have a +#' header named \code{abc} (case-insensitive) with the value \code{xyz} +#' (case-sensitive). If a request does not have a matching header, than httpuv +#' will give a 403 Forbidden response. If the \code{character(0)} (the +#' default), then no validation check will be performed. +#' #' @export staticPathOptions <- function( indexhtml = TRUE, @@ -123,6 +148,7 @@ format.staticPathOptions <- function(x, ...) { " Use index.html: ", format_option(x$indexhtml), "\n", " Fallthrough to R: ", format_option(x$fallthrough), "\n", " HTML charset: ", format_option(x$html_charset), "\n", + " Extra headers: ", format_option(x$headers), "\n", " Validation params: ", format_option(x$validation), "\n" ) } @@ -189,30 +215,36 @@ normalizeStaticPathOptions <- function(opts) { "`validation` option must be a character vector with zero or one element." } - # If it's length 0, do nothing; if length 1, we need to parse it. + # Both "" and character(0) result in character(0). Length-1 strings other + # than "" will be parsed. if (length(opts$validation) == 1) { - fail <- FALSE - tryCatch( - p <- parse(text = opts$validation)[[1]], - error = function(e) fail <<- TRUE - ) - if (!fail) { - if (length(p) != 3 || - p[[1]] != as.symbol("==") || - !is.character(p[[2]]) || - length(p[[2]]) != 1 || - !is.character(p[[3]]) || - length(p[[3]]) != 1) - { - fail <- TRUE + if (opts$validation == "") { + opts$validation <- character(0) + + } else { + fail <- FALSE + tryCatch( + p <- parse(text = opts$validation)[[1]], + error = function(e) fail <<- TRUE + ) + if (!fail) { + if (length(p) != 3 || + p[[1]] != as.symbol("==") || + !is.character(p[[2]]) || + length(p[[2]]) != 1 || + !is.character(p[[3]]) || + length(p[[3]]) != 1) + { + fail <- TRUE + } } + if (fail) { + stop("`validation` must be a string of the form: '\"xxx\" == \"yyy\"'") + } + + # Turn it into a char vector for easier processing in C++ + opts$validation <- as.character(p) } - if (fail) { - stop("`validation` must be a string of the form: '\"xxx\" == \"yyy\"'") - } - - # Turn it into a char vector for easier processing in C++ - opts$validation <- as.character(p) } } diff --git a/man/staticPath.Rd b/man/staticPath.Rd index bb2dd192..04539adb 100644 --- a/man/staticPath.Rd +++ b/man/staticPath.Rd @@ -10,8 +10,8 @@ staticPath(path, indexhtml = NULL, fallthrough = NULL, \arguments{ \item{path}{The local path.} -\item{indexhtml}{If an index.html file is present, should it be served up when -the client requests \code{/} or any subdirectory?} +\item{indexhtml}{If an index.html file is present, should it be served up +when the client requests \code{/} or any subdirectory?} \item{fallthrough}{With the default value, \code{FALSE}, if a request is made for a file that doesn't exist, then httpuv will immediately send a 404 @@ -19,7 +19,29 @@ response from the background I/O thread, without needing to call back into the main R thread. This offers the best performance. If the value is \code{TRUE}, then instead of sending a 404 response, httpuv will call the application's \code{call} function, and allow it to handle the request.} + +\item{html_charset}{When HTML files are served, the value that will be +provided for \code{charset} in the Content-Type header. For example, with +the default value, \code{"utf-8"}, the header is \code{Content-Type: +text/html; charset=utf-8}. If \code{""} is used, then no \code{charset} +will be added in the Content-Type header.} + +\item{headers}{Additional headers and values that will be included in the response.} + +\item{validation}{An optional validation pattern. Presently, the only type of +validation supported is an exact string match of a header. For example, if +\code{validation} is \code{'"abc" = "xyz"'}, then HTTP requests must have a +header named \code{abc} (case-insensitive) with the value \code{xyz} +(case-sensitive). If a request does not have a matching header, than httpuv +will give a 403 Forbidden response. If the \code{character(0)} (the +default), then no validation check will be performed.} } \description{ -This function creates a \code{staticPath} object. +This function creates a \code{staticPath} object. Note that if any of the +arguments (other than \code{path}) are \code{NULL}, then that means that +for this particular static path, it should inherit the behavior from the +staticPathOptions set for the application as a whole. +} +\seealso{ +\code{\link{staticPathOptions}}. } diff --git a/man/staticPathOptions.Rd b/man/staticPathOptions.Rd new file mode 100644 index 00000000..9363a2b2 --- /dev/null +++ b/man/staticPathOptions.Rd @@ -0,0 +1,40 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/static_paths.R +\name{staticPathOptions} +\alias{staticPathOptions} +\title{Create options for static paths} +\usage{ +staticPathOptions(indexhtml = TRUE, fallthrough = FALSE, + html_charset = "utf-8", headers = list(), + validation = character(0)) +} +\arguments{ +\item{indexhtml}{If an index.html file is present, should it be served up +when the client requests \code{/} or any subdirectory?} + +\item{fallthrough}{With the default value, \code{FALSE}, if a request is made +for a file that doesn't exist, then httpuv will immediately send a 404 +response from the background I/O thread, without needing to call back into +the main R thread. This offers the best performance. If the value is +\code{TRUE}, then instead of sending a 404 response, httpuv will call the +application's \code{call} function, and allow it to handle the request.} + +\item{html_charset}{When HTML files are served, the value that will be +provided for \code{charset} in the Content-Type header. For example, with +the default value, \code{"utf-8"}, the header is \code{Content-Type: +text/html; charset=utf-8}. If \code{""} is used, then no \code{charset} +will be added in the Content-Type header.} + +\item{headers}{Additional headers and values that will be included in the response.} + +\item{validation}{An optional validation pattern. Presently, the only type of +validation supported is an exact string match of a header. For example, if +\code{validation} is \code{'"abc" = "xyz"'}, then HTTP requests must have a +header named \code{abc} (case-insensitive) with the value \code{xyz} +(case-sensitive). If a request does not have a matching header, than httpuv +will give a 403 Forbidden response. If the \code{character(0)} (the +default), then no validation check will be performed.} +} +\description{ +Create options for static paths +} From 74f2e9f1558386cdd42ae0884d0e79f9a7c99d16 Mon Sep 17 00:00:00 2001 From: Winston Chang Date: Mon, 19 Nov 2018 16:18:03 -0600 Subject: [PATCH 39/60] Add Date header for static serving --- src/utils.h | 55 ++++++++++++++++++++++++++++++++++++++++++ src/webapplication.cpp | 1 + 2 files changed, 56 insertions(+) diff --git a/src/utils.h b/src/utils.h index 83f42313..8c1a878a 100644 --- a/src/utils.h +++ b/src/utils.h @@ -5,6 +5,7 @@ #include #include #include +#include #include #include #include @@ -141,4 +142,58 @@ Rcpp::RObject optional_wrap(boost::optional value) { } +// Return a date string in the format required for the HTTP Date header. For +// example: "Wed, 21 Oct 2015 07:28:00 GMT" +inline std::string http_date_string(const time_t& t) { + struct tm timeptr; + #ifdef _WIN32 + gmtime_s(&timeptr, &t); + #else + gmtime_r(&t, &timeptr); + #endif + + std::string day_name; + switch(timeptr.tm_wday) { + case 0: day_name = "Sun"; break; + case 1: day_name = "Mon"; break; + case 2: day_name = "Tue"; break; + case 3: day_name = "Wed"; break; + case 4: day_name = "Thu"; break; + case 5: day_name = "Fri"; break; + case 6: day_name = "Sat"; break; + default: day_name = "Err"; // Throw? + } + + std::string month_name; + switch(timeptr.tm_mon) { + case 0: month_name = "Jan"; break; + case 1: month_name = "Feb"; break; + case 2: month_name = "Mar"; break; + case 3: month_name = "Apr"; break; + case 4: month_name = "May"; break; + case 5: month_name = "Jun"; break; + case 6: month_name = "Jul"; break; + case 7: month_name = "Aug"; break; + case 8: month_name = "Sep"; break; + case 9: month_name = "Oct"; break; + case 10: month_name = "Nov"; break; + case 11: month_name = "Dec"; break; + default: month_name = "Err"; // Throw? + } + + const int maxlen = 30; + char res[maxlen]; + snprintf(res, maxlen, "%s, %02d %s %04d %02d:%02d:%02d GMT", + day_name.c_str(), + timeptr.tm_mday, + month_name.c_str(), + timeptr.tm_year + 1900, + timeptr.tm_hour, + timeptr.tm_min, + timeptr.tm_sec + ); + + return std::string(res); +} + #endif diff --git a/src/webapplication.cpp b/src/webapplication.cpp index a9592037..58b78bf9 100644 --- a/src/webapplication.cpp +++ b/src/webapplication.cpp @@ -540,6 +540,7 @@ boost::shared_ptr RWebApplication::staticFileResponse( // the response for the HEAD would not. respHeaders.push_back(std::make_pair("Content-Length", toString(file_size))); respHeaders.push_back(std::make_pair("Content-Type", content_type)); + respHeaders.push_back(std::make_pair("Date", http_date_string(time(NULL)))); return pResponse; } From da540b003b81c50603c51375b54895900504ab44 Mon Sep 17 00:00:00 2001 From: Winston Chang Date: Tue, 20 Nov 2018 11:44:56 -0600 Subject: [PATCH 40/60] Add support for custom headers --- R/static_paths.R | 54 +++++++++++++++++++++++++++++------------- src/staticpath.cpp | 13 ++++++++-- src/staticpath.h | 2 ++ src/utils.h | 50 ++++++++++++++++++++++++++++++++++++++ src/webapplication.cpp | 10 ++++++++ 5 files changed, 111 insertions(+), 18 deletions(-) diff --git a/R/static_paths.R b/R/static_paths.R index 7c0b90ea..b8f9edb5 100644 --- a/R/static_paths.R +++ b/R/static_paths.R @@ -64,21 +64,10 @@ print.staticPath <- function(x, ...) { #' @export format.staticPath <- function(x, ...) { - format_option <- function(opt) { - if (is.null(opt)) { - "" - } else { - paste(as.character(opt), collapse = " ") - } - } ret <- paste0( "\n", " Local path: ", x$path, "\n", - " Use index.html: ", format_option(x$options$indexhtml), "\n", - " Fallthrough to R: ", format_option(x$options$fallthrough), "\n", - " HTML charset: ", format_option(x$options$html_charset), "\n", - " Extra headers: ", format_option(x$options$headers), "\n", - " Validation params: ", format_option(x$options$validation), "\n" + format_opts(x$options) ) } @@ -115,7 +104,7 @@ staticPathOptions <- function( headers = list(), validation = character(0) ) { - structure( + res <- structure( list( indexhtml = indexhtml, fallthrough = fallthrough, @@ -125,6 +114,8 @@ staticPathOptions <- function( ), class = "staticPathOptions" ) + + normalizeStaticPathOptions(res) } #' @export @@ -136,15 +127,36 @@ print.staticPathOptions <- function(x, ...) { #' @export format.staticPathOptions <- function(x, ...) { + paste0( + "\n", + format_opts(x) + ) +} + +format_opts <- function(x) { format_option <- function(opt) { - if (is.null(opt)) { - "" + if (is.null(opt) || length(opt) == 0) { + "" + + } else if (!is.null(names(opt))) { + # Named character vector + lines <- mapply( + function(name, value) paste0(' "', name, '" = "', value, '"'), + names(opt), + opt, + SIMPLIFY = FALSE, + USE.NAMES = FALSE + ) + + lines <- paste(as.character(lines), collapse = "\n") + lines <- paste0("\n", lines) + lines + } else { paste(as.character(opt), collapse = " ") } } ret <- paste0( - "\n", " Use index.html: ", format_option(x$indexhtml), "\n", " Fallthrough to R: ", format_option(x$fallthrough), "\n", " HTML charset: ", format_option(x$html_charset), "\n", @@ -210,6 +222,16 @@ normalizeStaticPathOptions <- function(opts) { } } + # Can be a named list of strings, or a named character vector. On the C++ + # side, we want a named character vector. + if (is.list(opts$headers)) { + # Convert list to named character vector + opts$headers <- unlist(opts$headers, recursive = FALSE) + if (!is.character(opts$headers) || any_unnamed(opts$headers)) { + "`headers` option must be a named list or character vector." + } + } + if (!is.null(opts$validation)) { if (!is.character(opts$validation) || length(opts$validation) > 1) { "`validation` option must be a character vector with zero or one element." diff --git a/src/staticpath.cpp b/src/staticpath.cpp index 4091a575..bd52848e 100644 --- a/src/staticpath.cpp +++ b/src/staticpath.cpp @@ -9,12 +9,11 @@ // StaticPathOptions // ============================================================================ - -// (Use boost::optional instead of optional_as) StaticPathOptions::StaticPathOptions(const Rcpp::List& options) : indexhtml(boost::none), fallthrough(boost::none), html_charset(boost::none), + headers(boost::none), validation(boost::none) { ASSERT_MAIN_THREAD() @@ -38,9 +37,11 @@ StaticPathOptions::StaticPathOptions(const Rcpp::List& options) : temp = options["indexhtml"]; indexhtml = optional_as(temp); temp = options["fallthrough"]; fallthrough = optional_as(temp); temp = options["html_charset"]; html_charset = optional_as(temp); + temp = options["headers"]; headers = optional_as(temp); temp = options["validation"]; validation = optional_as>(temp); } + void StaticPathOptions::setOptions(const Rcpp::List& options) { ASSERT_MAIN_THREAD() Rcpp::RObject temp; @@ -62,6 +63,12 @@ void StaticPathOptions::setOptions(const Rcpp::List& options) { html_charset = optional_as(temp); } } + if (options.containsElementNamed("headers")) { + temp = options["headers"]; + if (!temp.isNULL()) { + headers = optional_as(temp); + } + } if (options.containsElementNamed("validation")) { temp = options["validation"]; if (!temp.isNULL()) { @@ -78,6 +85,7 @@ Rcpp::List StaticPathOptions::asRObject() const { _["indexhtml"] = optional_wrap(indexhtml), _["fallthrough"] = optional_wrap(fallthrough), _["html_charset"] = optional_wrap(html_charset), + _["headers"] = optional_wrap(headers), _["validation"] = optional_wrap(validation) ); @@ -95,6 +103,7 @@ StaticPathOptions StaticPathOptions::merge( if (new_sp.indexhtml == boost::none) new_sp.indexhtml = b.indexhtml; if (new_sp.fallthrough == boost::none) new_sp.fallthrough = b.fallthrough; if (new_sp.html_charset == boost::none) new_sp.html_charset = b.html_charset; + if (new_sp.headers == boost::none) new_sp.headers = b.headers; if (new_sp.validation == boost::none) new_sp.validation = b.validation; return new_sp; } diff --git a/src/staticpath.h b/src/staticpath.h index 618e70bc..73e469da 100644 --- a/src/staticpath.h +++ b/src/staticpath.h @@ -13,11 +13,13 @@ class StaticPathOptions { boost::optional indexhtml; boost::optional fallthrough; boost::optional html_charset; + boost::optional headers; boost::optional> validation; StaticPathOptions() : indexhtml(boost::none), fallthrough(boost::none), html_charset(boost::none), + headers(boost::none), validation(boost::none) { }; StaticPathOptions(const Rcpp::List& options); diff --git a/src/utils.h b/src/utils.h index 8c1a878a..f71e1bb1 100644 --- a/src/utils.h +++ b/src/utils.h @@ -142,6 +142,56 @@ Rcpp::RObject optional_wrap(boost::optional value) { } +// as() and wrap() for ResponseHeaders. Since the ResponseHeaders typedef is +// in constants.h and this file doesn't include constants.h, we'll define them +// using the actual vector type instead of the ResponseHeaders typedef. +// (constants.h doesn't include Rcpp.h so we can't define these functions +// there.) +namespace Rcpp { + template <> inline std::vector> as(SEXP x) { + ASSERT_MAIN_THREAD() + Rcpp::CharacterVector headers(x); + Rcpp::CharacterVector names = headers.names(); + + if (names.isNULL()) { + throw Rcpp::exception("All values must be named."); + } + + std::vector> result; + + for (int i=0; i(names[i]); + if (name == "") { + throw Rcpp::exception("All values must be named."); + } + + std::string value = Rcpp::as(headers[i]); + + result.push_back(std::make_pair(name, value)); + } + + return result; + } + + template <> inline SEXP wrap(const std::vector> &x) { + ASSERT_MAIN_THREAD() + + std::vector values(x.size()); + std::vector names(x.size()); + + for (int i=0; i RWebApplication::staticFileResponse( ); ResponseHeaders& respHeaders = pResponse->headers(); + + // Add any extra headers + ResponseHeaders extraRespHeaders = sp.options.headers.get(); + if (extraRespHeaders.size() != 0) { + ResponseHeaders::const_iterator it; + for (it = extraRespHeaders.begin(); it != extraRespHeaders.end(); it++) { + respHeaders.push_back(*it); + } + } + // Set the Content-Length here so that both GET and HEAD requests will get // it. If we didn't set it here, the response for the GET would // automatically set the Content-Length (by using the FileDataSource), but From 332672a7d9938d27d3abdf498ab4ebc69697eb91 Mon Sep 17 00:00:00 2001 From: Winston Chang Date: Tue, 20 Nov 2018 22:02:57 -0600 Subject: [PATCH 41/60] Disallow '..' in paths --- src/webapplication.cpp | 32 ++++++++++++++++++++++++-------- 1 file changed, 24 insertions(+), 8 deletions(-) diff --git a/src/webapplication.cpp b/src/webapplication.cpp index 0c8cb5bc..231018e6 100644 --- a/src/webapplication.cpp +++ b/src/webapplication.cpp @@ -452,12 +452,22 @@ boost::shared_ptr RWebApplication::staticFileResponse( _staticPathManager.matchStaticPath(url_path); if (!sp_pair) { - // This was not a static path. + // This was not a static path. Fall through to the R code to handle this + // path. return nullptr; } // If we get here, we've matched a static path. + const StaticPath& sp = sp_pair->first; + // Note that the subpath may include leading dirs, as in "foo/bar/abc.txt". + const std::string& subpath = sp_pair->second; + + // Validate headers (if validation pattern was provided). + if (!sp.options.validateRequestHeaders(pRequest->headers())) { + return error_response(pRequest, 403); + } + // Check that method is GET or HEAD; error otherwise. std::string method = pRequest->method(); if (method != "GET" && method != "HEAD") { @@ -469,13 +479,19 @@ boost::shared_ptr RWebApplication::staticFileResponse( return error_response(pRequest, 400); } - const StaticPath& sp = sp_pair->first; - // Note that the subpath may include leading dirs, as in "foo/bar/abc.txt". - const std::string& subpath = sp_pair->second; - - // Validate headers (if validation pattern was provided). - if (!sp.options.validateRequestHeaders(pRequest->headers())) { - return error_response(pRequest, 403); + // Disallow ".." in paths. (Browsers collapse them anyway, so no normal + // requests should contain them.) The ones we care about will always be + // between two slashes, as in "/foo/../bar", except in the case where it's + // at the end of the URL, as in "/foo/..". Paths like "/foo../" or "/..foo/" + // are OK. + if (url_path.find("/../") != std::string::npos || + (url_path.length() >= 3 && url_path.substr(url_path.length()-3, 3) == "/..") + ) { + if (sp.options.fallthrough.get()) { + return nullptr; + } else { + return error_response(pRequest, 400); + } } // Path to local file on disk From 5fa585d484d66b508e7dbf55b9992fd357ca8318 Mon Sep 17 00:00:00 2001 From: Winston Chang Date: Mon, 26 Nov 2018 16:28:09 -0600 Subject: [PATCH 42/60] Include filename in FileDataSource errors --- src/filedatasource-unix.cpp | 8 ++++---- src/filedatasource-win.cpp | 6 ++++-- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/src/filedatasource-unix.cpp b/src/filedatasource-unix.cpp index dab74f51..02a4ec12 100644 --- a/src/filedatasource-unix.cpp +++ b/src/filedatasource-unix.cpp @@ -11,19 +11,19 @@ int FileDataSource::initialize(const std::string& path, bool owned) { _fd = open(path.c_str(), O_RDONLY); if (_fd == -1) { - _lastErrorMessage = "Error opening file: " + toString(errno) + "\n"; + _lastErrorMessage = "Error opening file " + path + ": " + toString(errno) + "\n"; return 1; } else { struct stat info = {0}; if (fstat(_fd, &info)) { - _lastErrorMessage = "Error opening path: " + toString(errno) + "\n"; + _lastErrorMessage = "Error opening path " + path + ": " + toString(errno) + "\n"; ::close(_fd); return 1; } if (S_ISDIR(info.st_mode)) { - _lastErrorMessage = "File data source is a directory.\n"; + _lastErrorMessage = "File data source is a directory: " + path + "\n"; ::close(_fd); return 1; } @@ -33,7 +33,7 @@ int FileDataSource::initialize(const std::string& path, bool owned) { if (owned && unlink(path.c_str())) { // Print this (on either main or background thread), since we're not // returning 1 to indicate an error. - err_printf("Couldn't delete temp file: %d\n", errno); + err_printf("Couldn't delete temp file %s: %d\n", path.c_str(), errno); // It's OK to continue } diff --git a/src/filedatasource-win.cpp b/src/filedatasource-win.cpp index a102f0f7..b67031d5 100644 --- a/src/filedatasource-win.cpp +++ b/src/filedatasource-win.cpp @@ -24,13 +24,15 @@ int FileDataSource::initialize(const std::string& path, bool owned) { NULL); if (_hFile == INVALID_HANDLE_VALUE) { - _lastErrorMessage = "Error opening file: " + toString(GetLastError()) + "\n"; + _lastErrorMessage = "Error opening file " + path + ": " + toString(GetLastError()) + "\n"; return 1; } + // TODO: Error if it's a directory + if (!GetFileSizeEx(_hFile, &_length)) { CloseHandle(_hFile); - _lastErrorMessage = "Error retrieving file size: " + toString(GetLastError()) + "\n"; + _lastErrorMessage = "Error retrieving file size for " + path + ": " + toString(GetLastError()) + "\n"; return 1; } From 45cc37ebd14fe1dcfb9feaa409a0085ddec8c7cd Mon Sep 17 00:00:00 2001 From: Winston Chang Date: Tue, 27 Nov 2018 10:46:28 -0600 Subject: [PATCH 43/60] Minor cleanups --- R/server.R | 6 ++++++ R/static_paths.R | 16 ++++++++++++---- src/utils.h | 2 +- src/webapplication.cpp | 3 +-- 4 files changed, 20 insertions(+), 7 deletions(-) diff --git a/R/server.R b/R/server.R index d6b057be..99814d4c 100644 --- a/R/server.R +++ b/R/server.R @@ -83,6 +83,12 @@ WebServer <- R6Class("WebServer", private$running <- TRUE registerServer(self) + }, + getHost = function() { + private$host + }, + getPort = function() { + private$port } ), private = list( diff --git a/R/static_paths.R b/R/static_paths.R index b8f9edb5..bd271adb 100644 --- a/R/static_paths.R +++ b/R/static_paths.R @@ -196,8 +196,10 @@ normalizeStaticPaths <- function(paths) { if (substr(path, 1, 1) != "/") { path <- paste0("/", path) } - # Strip trailing slashes - path <- sub("/+$", "", path) + # Strip trailing slashes, except when the path is just "/". + if (path != "/") { + path <- sub("/+$", "\\1", path) + } path }, "") @@ -227,14 +229,20 @@ normalizeStaticPathOptions <- function(opts) { if (is.list(opts$headers)) { # Convert list to named character vector opts$headers <- unlist(opts$headers, recursive = FALSE) + # Special case: if opts$headers was an empty list before unlist(), it is + # now NULL. Replace it with an empty named character vector. + if (length(opts$headers) == 0) { + opts$headers <- c(a="a")[0] + } + if (!is.character(opts$headers) || any_unnamed(opts$headers)) { - "`headers` option must be a named list or character vector." + stop("`headers` option must be a named list or character vector.") } } if (!is.null(opts$validation)) { if (!is.character(opts$validation) || length(opts$validation) > 1) { - "`validation` option must be a character vector with zero or one element." + stop("`validation` option must be a character vector with zero or one element.") } # Both "" and character(0) result in character(0). Length-1 strings other diff --git a/src/utils.h b/src/utils.h index f71e1bb1..3f3da445 100644 --- a/src/utils.h +++ b/src/utils.h @@ -138,7 +138,7 @@ Rcpp::RObject optional_wrap(boost::optional value) { if (value == boost::none) { return R_NilValue; } - return Rcpp::wrap(*value); + return Rcpp::wrap(value.get()); } diff --git a/src/webapplication.cpp b/src/webapplication.cpp index 231018e6..f2537569 100644 --- a/src/webapplication.cpp +++ b/src/webapplication.cpp @@ -511,7 +511,6 @@ boost::shared_ptr RWebApplication::staticFileResponse( int ret = pDataSource->initialize(local_path, false); if (ret != 0) { - std::cout << pDataSource->lastErrorMessage() << "\n"; // Couldn't read the file delete pDataSource; @@ -552,7 +551,7 @@ boost::shared_ptr RWebApplication::staticFileResponse( ResponseHeaders& respHeaders = pResponse->headers(); // Add any extra headers - ResponseHeaders extraRespHeaders = sp.options.headers.get(); + const ResponseHeaders& extraRespHeaders = sp.options.headers.get(); if (extraRespHeaders.size() != 0) { ResponseHeaders::const_iterator it; for (it = extraRespHeaders.begin(); it != extraRespHeaders.end(); it++) { From 2243a49cfb651630084d3a80d5785ac093a0b397 Mon Sep 17 00:00:00 2001 From: Winston Chang Date: Wed, 28 Nov 2018 00:52:21 -0600 Subject: [PATCH 44/60] Add unit tests for static file serving --- tests/testthat/apps/content/data.txt | 2 + tests/testthat/apps/content/index.html | 3 + tests/testthat/apps/content/mtcars.csv | 33 ++ tests/testthat/apps/content/subdir/index.html | 1 + tests/testthat/apps/content_1/index.html | 1 + tests/testthat/helper-app.R | 130 +++++ tests/testthat/test-app.R | 46 ++ tests/testthat/test-static-paths.R | 541 ++++++++++++++++++ tests/testthat/test-traffic.R | 23 - 9 files changed, 757 insertions(+), 23 deletions(-) create mode 100644 tests/testthat/apps/content/data.txt create mode 100644 tests/testthat/apps/content/index.html create mode 100644 tests/testthat/apps/content/mtcars.csv create mode 100644 tests/testthat/apps/content/subdir/index.html create mode 100644 tests/testthat/apps/content_1/index.html create mode 100644 tests/testthat/helper-app.R create mode 100644 tests/testthat/test-app.R create mode 100644 tests/testthat/test-static-paths.R diff --git a/tests/testthat/apps/content/data.txt b/tests/testthat/apps/content/data.txt new file mode 100644 index 00000000..f5c71047 --- /dev/null +++ b/tests/testthat/apps/content/data.txt @@ -0,0 +1,2 @@ +This is a text file. + diff --git a/tests/testthat/apps/content/index.html b/tests/testthat/apps/content/index.html new file mode 100644 index 00000000..17b21214 --- /dev/null +++ b/tests/testthat/apps/content/index.html @@ -0,0 +1,3 @@ +This is the index file! + +Here's a UTF-8 emoji: 😀 diff --git a/tests/testthat/apps/content/mtcars.csv b/tests/testthat/apps/content/mtcars.csv new file mode 100644 index 00000000..2abcf283 --- /dev/null +++ b/tests/testthat/apps/content/mtcars.csv @@ -0,0 +1,33 @@ +"mpg","cyl","disp","hp","drat","wt","qsec","vs","am","gear","carb" +21,6,160,110,3.9,2.62,16.46,0,1,4,4 +21,6,160,110,3.9,2.875,17.02,0,1,4,4 +22.8,4,108,93,3.85,2.32,18.61,1,1,4,1 +21.4,6,258,110,3.08,3.215,19.44,1,0,3,1 +18.7,8,360,175,3.15,3.44,17.02,0,0,3,2 +18.1,6,225,105,2.76,3.46,20.22,1,0,3,1 +14.3,8,360,245,3.21,3.57,15.84,0,0,3,4 +24.4,4,146.7,62,3.69,3.19,20,1,0,4,2 +22.8,4,140.8,95,3.92,3.15,22.9,1,0,4,2 +19.2,6,167.6,123,3.92,3.44,18.3,1,0,4,4 +17.8,6,167.6,123,3.92,3.44,18.9,1,0,4,4 +16.4,8,275.8,180,3.07,4.07,17.4,0,0,3,3 +17.3,8,275.8,180,3.07,3.73,17.6,0,0,3,3 +15.2,8,275.8,180,3.07,3.78,18,0,0,3,3 +10.4,8,472,205,2.93,5.25,17.98,0,0,3,4 +10.4,8,460,215,3,5.424,17.82,0,0,3,4 +14.7,8,440,230,3.23,5.345,17.42,0,0,3,4 +32.4,4,78.7,66,4.08,2.2,19.47,1,1,4,1 +30.4,4,75.7,52,4.93,1.615,18.52,1,1,4,2 +33.9,4,71.1,65,4.22,1.835,19.9,1,1,4,1 +21.5,4,120.1,97,3.7,2.465,20.01,1,0,3,1 +15.5,8,318,150,2.76,3.52,16.87,0,0,3,2 +15.2,8,304,150,3.15,3.435,17.3,0,0,3,2 +13.3,8,350,245,3.73,3.84,15.41,0,0,3,4 +19.2,8,400,175,3.08,3.845,17.05,0,0,3,2 +27.3,4,79,66,4.08,1.935,18.9,1,1,4,1 +26,4,120.3,91,4.43,2.14,16.7,0,1,5,2 +30.4,4,95.1,113,3.77,1.513,16.9,1,1,5,2 +15.8,8,351,264,4.22,3.17,14.5,0,1,5,4 +19.7,6,145,175,3.62,2.77,15.5,0,1,5,6 +15,8,301,335,3.54,3.57,14.6,0,1,5,8 +21.4,4,121,109,4.11,2.78,18.6,1,1,4,2 diff --git a/tests/testthat/apps/content/subdir/index.html b/tests/testthat/apps/content/subdir/index.html new file mode 100644 index 00000000..b09907cd --- /dev/null +++ b/tests/testthat/apps/content/subdir/index.html @@ -0,0 +1 @@ +This is the index file for content/subdir/ diff --git a/tests/testthat/apps/content_1/index.html b/tests/testthat/apps/content_1/index.html new file mode 100644 index 00000000..10acc96e --- /dev/null +++ b/tests/testthat/apps/content_1/index.html @@ -0,0 +1 @@ +Index file for content1. diff --git a/tests/testthat/helper-app.R b/tests/testthat/helper-app.R new file mode 100644 index 00000000..3333ace7 --- /dev/null +++ b/tests/testthat/helper-app.R @@ -0,0 +1,130 @@ +library(curl) +library(promises) + + +random_open_port <- function(min = 3000, max = 9000, n = 20) { + # Unsafe port list from shiny::runApp() + valid_ports <- setdiff(min:max, c(3659, 4045, 6000, 6665:6669, 6697)) + + # Try up to n ports + for (port in sample(valid_ports, n)) { + handle <- NULL + + # Check if port is open + tryCatch( + handle <- httpuv::startServer("127.0.0.1", port, list()), + error = function(e) { } + ) + if (!is.null(handle)) { + httpuv::stopServer(handle) + return(port) + } + } + + stop("Cannot find an available port") +} + + +curl_fetch_async <- function(url, pool = NULL, data = NULL, handle = new_handle()) { + p <- promises::promise(function(resolve, reject) { + curl_fetch_multi(url, done = resolve, fail = reject, pool = pool, data = data, handle = handle) + }) + + finished <- FALSE + poll <- function() { + if (!finished) { + multi_run(timeout = 0, poll = TRUE, pool = pool) + later::later(poll, 0.01) + } + } + poll() + + p %>% finally(function() { + finished <<- TRUE + }) +} + + +# A way of sending an HTTP request using a socketConnection. This isn't as +# reliable as using curl, so we'll use it only when curl can't do what we want. +http_request_con_async <- function(request, host, port) { + resolve_fun <- NULL + reject_fun <- NULL + con <- NULL + + p <- promises::promise(function(resolve, reject) { + resolve_fun <<- resolve + reject_fun <<- reject + con <<- socketConnection(host, port) + writeLines(c(request, ""), con) + }) + + result <- NULL + # finished <- FALSE + poll <- function() { + result <<- readLines(con) + if (length(result) > 0) { + resolve_fun(result) + } else { + later::later(poll, 0.01) + } + } + poll() + + p %>% finally(function() { + close(con) + }) +} + + +wait_for_it <- function() { + while (!later::loop_empty()) { + later::run_now() + } +} + + +# Block until the promise is resolved/rejected. If resolved, return the value. +# If rejected, throw (yes throw, not return) the error. +extract <- function(promise) { + promise_value <- NULL + error <- NULL + promise %...>% + (function(value) promise_value <<- value) %...!% + (function(reason) error <<- reason) + + wait_for_it() + if (!is.null(error)) + stop(error) + else + promise_value +} + + +# Make an HTTP request using curl. +fetch <- function(url, handle = new_handle()) { + p <- curl_fetch_async(url, handle = handle) + extract(p) +} + +# Make an HTTP request using a socketConnection. Not as robust as fetch(), so +# we'll use this only when necessary. +http_request_con <- function(request, host, port) { + p <- http_request_con_async(request, host, port) + extract(p) +} + + +local_url <- function(path, port) { + stopifnot(grepl("^/", path)) + paste0("http://127.0.0.1:", port, path) +} + +parse_http_date <- function(x) { + strptime(x, format = "%a, %d %b %Y %H:%M:%S GMT", tz = "GMT") +} + +raw_file_content <- function(filename) { + size <- file.info(filename)$size + readBin(filename, "raw", n = size) +} diff --git a/tests/testthat/test-app.R b/tests/testthat/test-app.R new file mode 100644 index 00000000..30829536 --- /dev/null +++ b/tests/testthat/test-app.R @@ -0,0 +1,46 @@ +context("basic") + +test_that("Basic functionality", { + s1 <- startServer("127.0.0.1", random_open_port(), + list( + call = function(req) { + list( + status = 200L, + headers = list('Content-Type' = 'text/html'), + body = "server 1" + ) + } + ) + ) + expect_equal(length(listServers()), 1) + + s2 <- startServer("127.0.0.1", random_open_port(), + list( + call = function(req) { + list( + status = 200L, + headers = list('Content-Type' = 'text/html'), + body = "server 2" + ) + } + ) + ) + expect_equal(length(listServers()), 2) + + r1 <- fetch(local_url("/", s1$getPort())) + r2 <- fetch(local_url("/", s2$getPort())) + + expect_equal(r1$status_code, 200) + expect_equal(r2$status_code, 200) + + expect_identical(rawToChar(r1$content), "server 1") + expect_identical(rawToChar(r2$content), "server 2") + + expect_identical(parse_headers_list(r1$headers)$`content-type`, "text/html") + expect_identical(parse_headers_list(r1$headers)$`content-length`, "8") + + s1$stop() + expect_equal(length(listServers()), 1) + stopAllServers() + expect_equal(length(listServers()), 0) +}) diff --git a/tests/testthat/test-static-paths.R b/tests/testthat/test-static-paths.R new file mode 100644 index 00000000..065f7700 --- /dev/null +++ b/tests/testthat/test-static-paths.R @@ -0,0 +1,541 @@ +context("static") + +index_file_content <- raw_file_content(test_path("apps/content/index.html")) +subdir_index_file_content <- raw_file_content(test_path("apps/content/subdir/index.html")) +index_file_1_content <- raw_file_content(test_path("apps/content_1/index.html")) + +test_that("Basic static file serving", { + s <- startServer("127.0.0.1", random_open_port(), + list( + staticPaths = list( + # Testing out various leading and trailing slashes + "/" = test_path("apps/content"), + "/1" = test_path("apps/content"), + "/2/" = test_path("apps/content/"), + "3" = test_path("apps/content"), + "4/" = test_path("apps/content/") + ), + staticPathOptions = staticPathOptions( + headers = list("Test-Code-Path" = "C++") + ) + ) + ) + on.exit(s$stop()) + + # Fetch index.html + r <- fetch(local_url("/", s$getPort())) + expect_equal(r$status_code, 200) + expect_identical(r$content, index_file_content) + + # index.html for subdirectory + r_subdir <- fetch(local_url("/subdir", s$getPort())) + expect_equal(r_subdir$status_code, 200) + expect_identical(r_subdir$content, subdir_index_file_content) + + h <- parse_headers_list(r$headers) + expect_equal(as.integer(h$`content-length`), length(index_file_content)) + expect_equal(as.integer(h$`content-length`), length(r$content)) + expect_identical(h$`content-type`, "text/html; charset=utf-8") + expect_identical(h$`test-code-path`, "C++") + # Check that response time is within 1 minute of now. (Possible DST problems?) + expect_true(abs(as.numeric(parse_http_date(h$date)) - as.numeric(Sys.time())) < 60) + + + # Testing index for other paths + r1 <- fetch(local_url("/1", s$getPort())) + h1 <- parse_headers_list(r1$headers) + expect_identical(r$content, r1$content) + expect_identical(h$`content-length`, h1$`content-length`) + expect_identical(h$`content-type`, h1$`content-type`) + + r2 <- fetch(local_url("/1/", s$getPort())) + h2 <- parse_headers_list(r2$headers) + expect_identical(r$content, r2$content) + expect_identical(h$`content-length`, h2$`content-length`) + expect_identical(h$`content-type`, h2$`content-type`) + + r3 <- fetch(local_url("/1/index.html", s$getPort())) + h3 <- parse_headers_list(r3$headers) + expect_identical(r$content, r3$content) + expect_identical(h$`content-length`, h3$`content-length`) + expect_identical(h$`content-type`, h3$`content-type`) + + # Missing file (404) + r <- fetch(local_url("/foo", s$getPort())) + h <- parse_headers_list(r$headers) + expect_identical(rawToChar(r$content), "404 Not Found\n") + expect_equal(h$`content-length`, "14") + + # MIME types for other files + r <- fetch(local_url("/mtcars.csv", s$getPort())) + h <- parse_headers_list(r$headers) + expect_equal(h$`content-type`, "text/csv") + + r <- fetch(local_url("/data.txt", s$getPort())) + h <- parse_headers_list(r$headers) + expect_equal(h$`content-type`, "text/plain") +}) + + +test_that("Missing file fallthrough", { + s <- startServer("127.0.0.1", random_open_port(), + list( + call = function(req) { + return(list( + status = 404, + headers = list("Test-Code-Path" = "R"), + body = paste0("404 file not found: ", req$PATH_INFO) + )) + }, + staticPaths = list( + # Testing out various leading and trailing slashes + "/" = staticPath( + test_path("apps/content"), + indexhtml = FALSE, + fallthrough = TRUE + ) + ) + ) + ) + on.exit(s$stop()) + + r <- fetch(local_url("/", s$getPort())) + h <- parse_headers_list(r$headers) + expect_equal(r$status_code, 404) + expect_identical(h$`test-code-path`, "R") + expect_identical(rawToChar(r$content), "404 file not found: /") +}) + + +test_that("Longer paths override shorter ones", { + s <- startServer("127.0.0.1", random_open_port(), + list( + staticPaths = list( + # Testing out various leading and trailing slashes + "/" = test_path("apps/content"), + "/a" = staticPath( + test_path("apps/content"), + indexhtml = FALSE + ), + "/a/b" = staticPath( + test_path("apps/content"), + indexhtml = NULL + ), + "/a/b/c" = staticPath( + test_path("apps/content"), + indexhtml = TRUE + ) + ) + ) + ) + on.exit(s$stop()) + + r <- fetch(local_url("/", s$getPort())) + expect_equal(r$status_code, 200) + expect_identical(r$content, index_file_content) + + r <- fetch(local_url("/a/", s$getPort())) + expect_equal(r$status_code, 404) + + # When NULL, option values are not inherited from the parent dir, "/a"; + # they're inherited from the overall options for the app. + r <- fetch(local_url("/a/b", s$getPort())) + expect_equal(r$status_code, 200) + expect_identical(r$content, index_file_content) + + r <- fetch(local_url("/a/b/c", s$getPort())) + expect_equal(r$status_code, 200) + expect_identical(r$content, index_file_content) +}) + + +test_that("Options and option inheritance", { + s <- startServer("127.0.0.1", random_open_port(), + list( + call = function(req) { + return(list( + status = 404, + headers = list("Test-Code-Path" = "R"), + body = paste0("404 file not found: ", req$PATH_INFO) + )) + }, + staticPaths = list( + "/default" = staticPath(test_path("apps/content")), + # This path overrides options + "/override" = staticPath( + test_path("apps/content"), + indexhtml = FALSE, + fallthrough = TRUE, + html_charset = "ISO-8859-1", + headers = list("Test-Code-Path" = "C++2") + ), + # This path unsets some options + "/unset" = staticPath( + test_path("apps/content"), + html_charset = character(), + headers = list() + ) + ), + staticPathOptions = staticPathOptions( + indexhtml = TRUE, + fallthrough = FALSE, + headers = list("Test-Code-Path" = "C++") + ) + ) + ) + on.exit(s$stop()) + + r <- fetch(local_url("/default", s$getPort())) + h <- parse_headers_list(r$headers) + expect_equal(r$status_code, 200) + expect_identical(h$`content-type`, "text/html; charset=utf-8") + expect_identical(h$`test-code-path`, "C++") + expect_identical(r$content, index_file_content) + + r <- fetch(local_url("/override", s$getPort())) + h <- parse_headers_list(r$headers) + expect_equal(r$status_code, 404) + expect_identical(h$`test-code-path`, "R") + expect_identical(rawToChar(r$content), "404 file not found: /override") + + r <- fetch(local_url("/override/index.html", s$getPort())) + h <- parse_headers_list(r$headers) + expect_equal(r$status_code, 200) + expect_identical(h$`test-code-path`, "C++2") + expect_identical(h$`content-type`, "text/html; charset=ISO-8859-1") + + r <- fetch(local_url("/unset", s$getPort())) + h <- parse_headers_list(r$headers) + expect_equal(r$status_code, 200) + expect_false("test-code-path" %in% names(h)) + expect_identical(h$`content-type`, "text/html") + expect_identical(r$content, index_file_content) + + r <- fetch(local_url("/unset/index.html", s$getPort())) + h <- parse_headers_list(r$headers) + expect_equal(r$status_code, 200) + expect_false("test-code-path" %in% names(h)) + expect_identical(h$`content-type`, "text/html") + expect_identical(r$content, index_file_content) +}) + + +test_that("Header validation", { + s <- startServer("127.0.0.1", random_open_port(), + list( + call = function(req) { + if (!identical(req$HTTP_TEST_VALIDATION, "aaa")) { + return(list( + status = 403, + headers = list("Test-Code-Path" = "R"), + body = "403 Forbidden\n" + )) + } + return(list( + status = 200, + headers = list("Test-Code-Path" = "R"), + body = "200 OK\n" + )) + }, + staticPaths = list( + "/default" = staticPath(test_path("apps/content")), + # This path overrides validation + "/override" = staticPath( + test_path("apps/content"), + validation = c('"Test-Validation-1" == "bbb"') + ), + # This path unsets validation + "/unset" = staticPath( + test_path("apps/content"), + validation = character() + ), + # Fall through to R + "/fallthrough" = staticPath( + test_path("apps/content"), + fallthrough = TRUE + ) + ), + staticPathOptions = staticPathOptions( + headers = list("Test-Code-Path" = "C++"), + validation = c('"Test-Validation" == "aaa"') + ) + ) + ) + on.exit(s$stop()) + + r <- fetch(local_url("/default", s$getPort())) + h <- parse_headers_list(r$headers) + expect_equal(r$status_code, 403) + # This header doesn't get set. Should it? + expect_false("test-code-path" %in% names(h)) + expect_identical(rawToChar(r$content), "403 Forbidden\n") + + r <- fetch(local_url("/default", s$getPort()), + handle_setheaders(new_handle(), "test-validation" = "aaa")) + h <- parse_headers_list(r$headers) + expect_equal(r$status_code, 200) + expect_identical(h$`test-code-path`, "C++") + expect_identical(r$content, index_file_content) + + # Check case insensitive + r <- fetch(local_url("/default", s$getPort()), + handle_setheaders(new_handle(), "tesT-ValidatioN" = "aaa")) + expect_equal(r$status_code, 200) + + r <- fetch(local_url("/unset", s$getPort())) + h <- parse_headers_list(r$headers) + expect_equal(r$status_code, 200) + expect_identical(h$`test-code-path`, "C++") + expect_identical(r$content, index_file_content) + + # When fallthrough=TRUE, the header validation is still checked before falling + # through to the R code path. + r <- fetch(local_url("/fallthrough/missingfile", s$getPort())) + h <- parse_headers_list(r$headers) + expect_equal(r$status_code, 403) + # This header doesn't get set. Should it? + expect_false("test-code-path" %in% names(h)) + expect_identical(rawToChar(r$content), "403 Forbidden\n") + + r <- fetch(local_url("/fallthrough/missingfile", s$getPort()), + handle_setheaders(new_handle(), "test-validation" = "aaa")) + h <- parse_headers_list(r$headers) + expect_equal(r$status_code, 200) + expect_identical(h$`test-code-path`, "R") + expect_identical(rawToChar(r$content), "200 OK\n") +}) + + +test_that("Dynamically changing paths", { + s <- startServer("127.0.0.1", random_open_port(), + list( + call = function(req) { + list( + status = 500, + headers = list("Test-Code-Path" = "R"), + body = "500 Internal Server Error\n" + ) + }, + staticPaths = list( + "/static" = test_path("apps/content") + ) + ) + ) + on.exit(s$stop()) + + r <- fetch(local_url("/static", s$getPort())) + expect_equal(r$status_code, 200) + expect_identical(r$content, index_file_content) + + # Replace with different static path and options + s$setStaticPath( + "/static" = staticPath( + test_path("apps/content_1"), + indexhtml = FALSE + ) + ) + + r <- fetch(local_url("/static", s$getPort())) + expect_equal(r$status_code, 404) + + r <- fetch(local_url("/static/index.html", s$getPort())) + expect_equal(r$status_code, 200) + expect_identical(r$content, index_file_1_content) + + # Remove static path + s$removeStaticPath("/static") + + expect_equal(length(s$getStaticPaths()), 0) + + r <- fetch(local_url("/static", s$getPort())) + expect_equal(r$status_code, 500) + h <- parse_headers_list(r$headers) + expect_identical(h$`test-code-path`, "R") + expect_identical(rawToChar(r$content), "500 Internal Server Error\n") + + # Add static path + s$setStaticPath( + "/static_new" = test_path("apps/content") + ) + r <- fetch(local_url("/static_new", s$getPort())) + expect_equal(r$status_code, 200) + expect_identical(r$content, index_file_content) +}) + + +test_that("Dynamically changing options", { + s <- startServer("127.0.0.1", random_open_port(), + list( + call = function(req) { + list( + status = 500, + headers = list("Test-Code-Path" = "R"), + body = "500 Internal Server Error\n" + ) + }, + staticPaths = list( + "/static" = test_path("apps/content") + ) + ) + ) + on.exit(s$stop()) + + r <- fetch(local_url("/static", s$getPort())) + expect_equal(r$status_code, 200) + + s$setStaticPathOption(indexhtml = FALSE) + r <- fetch(local_url("/static", s$getPort())) + expect_equal(r$status_code, 404) + + s$setStaticPathOption(fallthrough = TRUE) + r <- fetch(local_url("/static", s$getPort())) + expect_equal(r$status_code, 500) + + s$setStaticPathOption( + indexhtml = TRUE, + headers = list("Test-Headers" = "aaa"), + validation = c('"Test-Validation" == "aaa"') + ) + r <- fetch(local_url("/static", s$getPort())) + expect_equal(r$status_code, 403) + r <- fetch(local_url("/static", s$getPort()), + handle_setheaders(new_handle(), "test-validation" = "aaa")) + h <- parse_headers_list(r$headers) + expect_equal(r$status_code, 200) + expect_identical(h$`test-headers`, "aaa") + + # Unset some options + s$setStaticPathOption( + headers = list(), + validation = character() + ) + r <- fetch(local_url("/static", s$getPort())) + h <- parse_headers_list(r$headers) + expect_equal(r$status_code, 200) + expect_false("test-headers" %in% h) +}) + + +test_that("Escaped characters in paths", { + # Need to create files with weird names + static_dir <- tempfile("httpuv_test") + dir.create(static_dir) + cat("This is file content.\n", file = file.path(static_dir, "file with space.txt")) + on.exit(unlink(static_dir, recursive = TRUE)) + + + s <- startServer("127.0.0.1", random_open_port(), + list( + call = function(req) { + list( + status = 500, + headers = list("Test-Code-Path" = "R"), + body = "500 Internal Server Error\n" + ) + }, + staticPaths = list( + "/static" = static_dir + ) + ) + ) + on.exit(s$stop(), add = TRUE) + + r <- fetch(local_url("/static/file%20with%20space.txt", s$getPort())) + expect_equal(r$status_code, 200) + expect_identical(rawToChar(r$content), "This is file content.\n") +}) + + +test_that("Paths with ..", { + # TODO: Figure out how to send a request with .. + + s <- startServer("127.0.0.1", random_open_port(), + list( + call = function(req) { + list( + status = 404, + headers = list("Test-Code-Path" = "R"), + body = "404 Not Found\n" + ) + }, + staticPaths = list( + "/static" = test_path("apps/content") + ) + ) + ) + on.exit(s$stop()) + + res <- http_request_con("GET /", "127.0.0.1", s$getPort()) + expect_identical(res[1], "HTTP/1.1 404 Not Found") + expect_true(any(grepl("^Test-Code-Path: R$", res, ignore.case = TRUE))) + + res <- http_request_con("GET /static", "127.0.0.1", s$getPort()) + expect_identical(res[1], "HTTP/1.1 200 OK") + + # The presence of a ".." path segment results in a 400. + res <- http_request_con("GET /static/..", "127.0.0.1", s$getPort()) + expect_identical(res[1], "HTTP/1.1 400 Bad Request") + + res <- http_request_con("GET /static/../", "127.0.0.1", s$getPort()) + expect_identical(res[1], "HTTP/1.1 400 Bad Request") + + res <- http_request_con("GET /static/../static", "127.0.0.1", s$getPort()) + expect_identical(res[1], "HTTP/1.1 400 Bad Request") + + # ".." is valid as part of a path segment (but we'll get 404's since the files + # don't actually exist). + res <- http_request_con("GET /static/..foo", "127.0.0.1", s$getPort()) + expect_identical(res[1], "HTTP/1.1 404 Not Found") + expect_false(any(grepl("^Test-Code-Path: R$", res, ignore.case = TRUE))) + + res <- http_request_con("GET /static/foo..", "127.0.0.1", s$getPort()) + expect_identical(res[1], "HTTP/1.1 404 Not Found") + expect_false(any(grepl("^Test-Code-Path: R$", res, ignore.case = TRUE))) + + res <- http_request_con("GET /static/foo../", "127.0.0.1", s$getPort()) + expect_identical(res[1], "HTTP/1.1 404 Not Found") + expect_false(any(grepl("^Test-Code-Path: R$", res, ignore.case = TRUE))) +}) + + +test_that("HEAD, POST, PUT requests", { + s <- startServer("127.0.0.1", random_open_port(), + list( + call = function(req) { + list( + status = 404, + headers = list("Test-Code-Path" = "R"), + body = "404 Not Found\n" + ) + }, + staticPaths = list( + "/static" = test_path("apps/content") + ) + ) + ) + on.exit(s$stop()) + + # The GET results, for comparison to HEAD. + r_get <- fetch(local_url("/static", s$getPort())) + h_get <- parse_headers_list(r_get$headers) + + # HEAD is OK. + # Note the weird interface for a HEAD request: + # https://github.com/jeroen/curl/issues/24 + r <- fetch(local_url("/static", s$getPort()), new_handle(nobody = TRUE)) + expect_equal(r$status_code, 200) + expect_true(length(r$content) == 0) # No message body for HEAD + h <- parse_headers_list(r$headers) + # Headers should match GET request, except for date. + expect_identical(h[setdiff(names(h), "date")], h_get[setdiff(names(h_get), "date")]) + + # POST and PUT are not OK + r <- fetch(local_url("/static", s$getPort()), + handle_setopt(new_handle(), customrequest = "POST")) + expect_equal(r$status_code, 400) + + r <- fetch(local_url("/static", s$getPort()), + handle_setopt(new_handle(), customrequest = "PUT")) + expect_equal(r$status_code, 400) +}) + diff --git a/tests/testthat/test-traffic.R b/tests/testthat/test-traffic.R index 232a0dba..984e3dc4 100644 --- a/tests/testthat/test-traffic.R +++ b/tests/testthat/test-traffic.R @@ -11,29 +11,6 @@ skip_if_not_possible <- function() { } } -random_open_port <- function(min = 3000, max = 9000, n = 20) { - # Unsafe port list from shiny::runApp() - valid_ports <- setdiff(min:max, c(3659, 4045, 6000, 6665:6669, 6697)) - - # Try up to n ports - for (port in sample(valid_ports, n)) { - handle <- NULL - - # Check if port is open - tryCatch( - handle <- httpuv::startServer("127.0.0.1", port, list()), - error = function(e) { } - ) - if (!is.null(handle)) { - httpuv::stopServer(handle) - return(port) - } - } - - stop("Cannot find an available port") -} - - parse_ab_output <- function(p) { text <- readLines(p$get_output_file()) From 360b41723b0c8c6aead941da11381b14094d04fb Mon Sep 17 00:00:00 2001 From: Winston Chang Date: Wed, 28 Nov 2018 13:35:39 -0600 Subject: [PATCH 45/60] Add curl to suggested packages --- DESCRIPTION | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/DESCRIPTION b/DESCRIPTION index 7f858694..994cd0ea 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -30,7 +30,8 @@ SystemRequirements: GNU make RoxygenNote: 6.1.0.9000 Suggests: testthat, - callr + callr, + curl Collate: 'RcppExports.R' 'httpuv.R' From a27dcf4425f29de1734a019e26737ed49229ac7e Mon Sep 17 00:00:00 2001 From: Winston Chang Date: Thu, 29 Nov 2018 12:20:43 -0600 Subject: [PATCH 46/60] Fixes for path handling in Windows --- R/static_paths.R | 2 +- src/filedatasource-win.cpp | 5 +++++ src/fs.h | 11 ++++++++++- 3 files changed, 16 insertions(+), 2 deletions(-) diff --git a/R/static_paths.R b/R/static_paths.R index bd271adb..9435e7a3 100644 --- a/R/static_paths.R +++ b/R/static_paths.R @@ -23,7 +23,7 @@ staticPath <- function( stop("`path` must be a non-empty string.") } - path <- normalizePath(path, mustWork = TRUE) + path <- normalizePath(path, winslash = "/", mustWork = TRUE) structure( list( diff --git a/src/filedatasource-win.cpp b/src/filedatasource-win.cpp index b67031d5..2ce77b4e 100644 --- a/src/filedatasource-win.cpp +++ b/src/filedatasource-win.cpp @@ -75,4 +75,9 @@ void FileDataSource::close() { } } +std::string FileDataSource::lastErrorMessage() const { + return _lastErrorMessage; +} + + #endif // #ifdef _WIN32 diff --git a/src/fs.h b/src/fs.h index 509b0a9f..b84c9f49 100644 --- a/src/fs.h +++ b/src/fs.h @@ -4,6 +4,7 @@ #include #ifdef _WIN32 +#include #else #include #endif @@ -38,7 +39,15 @@ inline std::string find_extension(const std::string &filename) { inline bool is_directory(const std::string &filename) { #ifdef _WIN32 - // TODO: implement + DWORD file_attr = GetFileAttributes(filename.c_str()); + if (file_attr == INVALID_FILE_ATTRIBUTES) { + return false; + } + if (file_attr & FILE_ATTRIBUTE_DIRECTORY) { + return true; + } + + return false; #else From e3cc505517c1a3a0ccf74d3b71dadb9d9e38bc7d Mon Sep 17 00:00:00 2001 From: Winston Chang Date: Thu, 29 Nov 2018 12:21:16 -0600 Subject: [PATCH 47/60] Minor cleanup --- src/staticpath.cpp | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/staticpath.cpp b/src/staticpath.cpp index bd52848e..c0685201 100644 --- a/src/staticpath.cpp +++ b/src/staticpath.cpp @@ -88,7 +88,7 @@ Rcpp::List StaticPathOptions::asRObject() const { _["headers"] = optional_wrap(headers), _["validation"] = optional_wrap(validation) ); - + obj.attr("class") = "staticPathOptions"; return obj; @@ -143,7 +143,7 @@ bool StaticPathOptions::validateRequestHeaders(const RequestHeaders& headers) co StaticPath::StaticPath(const Rcpp::List& sp) { ASSERT_MAIN_THREAD() path = Rcpp::as(sp["path"]); - + Rcpp::List options_list = sp["options"]; options = StaticPathOptions(options_list); @@ -295,7 +295,7 @@ void StaticPathManager::remove(const Rcpp::CharacterVector& paths) { // a static path in its entirety (e.g., the url_path is "/foo" or "/foo/" and // there is a static path "/foo"), then the returned pair consists of the // matching StaticPath object and an empty string "". -// +// // If no matching static path is found, then it returns boost::none. // boost::optional> StaticPathManager::matchStaticPath( @@ -327,7 +327,7 @@ boost::optional> StaticPathManager::matchStat // previous '/' and searches again, and so on, until there are no more to // split on. while (true) { - // Check if the part before the split-on '/' is in the staticPath. + // Check if the part before the split-on '/' is a staticPath. boost::optional sp = this->get(pre_slash); if (sp) { return std::pair(*sp, post_slash); @@ -377,7 +377,7 @@ Rcpp::List StaticPathManager::pathsAsRObject() const { for (it = path_map.begin(); it != path_map.end(); it++) { obj[it->first] = it->second.asRObject(); } - + return obj; } From b03901adb4492d1ba8f26de0ad7b4081e3a25535 Mon Sep 17 00:00:00 2001 From: Winston Chang Date: Thu, 29 Nov 2018 12:21:36 -0600 Subject: [PATCH 48/60] Fix signed/unsigned comparison warning --- src/utils.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils.h b/src/utils.h index 3f3da445..b0ad182e 100644 --- a/src/utils.h +++ b/src/utils.h @@ -179,7 +179,7 @@ namespace Rcpp { std::vector values(x.size()); std::vector names(x.size()); - for (int i=0; i Date: Thu, 29 Nov 2018 12:41:15 -0600 Subject: [PATCH 49/60] Code review feedback --- R/server.R | 6 ++---- R/static_paths.R | 4 ++-- man/staticPath.Rd | 2 +- man/staticPathOptions.Rd | 2 +- tests/testthat/test-static-paths.R | 2 +- 5 files changed, 7 insertions(+), 9 deletions(-) diff --git a/R/server.R b/R/server.R index 99814d4c..75c6e6ec 100644 --- a/R/server.R +++ b/R/server.R @@ -5,9 +5,8 @@ Server <- R6Class("Server", cloneable = FALSE, public = list( stop = function() { - if (!private$running) { - stop("Server is already stopped.") - } + if (!private$running) return(invisible()) + stopServer_(private$handle) private$running <- FALSE deregisterServer(self) @@ -24,7 +23,6 @@ Server <- R6Class("Server", }, setStaticPath = function(..., .list = NULL) { paths <- c(list(...), .list) - paths <- drop_duplicate_names(paths) paths <- normalizeStaticPaths(paths) invisible(setStaticPaths_(private$handle, paths)) }, diff --git a/R/static_paths.R b/R/static_paths.R index 9435e7a3..9132a8db 100644 --- a/R/static_paths.R +++ b/R/static_paths.R @@ -75,7 +75,7 @@ format.staticPath <- function(x, ...) { #' #' #' @param indexhtml If an index.html file is present, should it be served up -#' when the client requests \code{/} or any subdirectory? +#' when the client requests the static path or any subdirectory? #' @param fallthrough With the default value, \code{FALSE}, if a request is made #' for a file that doesn't exist, then httpuv will immediately send a 404 #' response from the background I/O thread, without needing to call back into @@ -198,7 +198,7 @@ normalizeStaticPaths <- function(paths) { } # Strip trailing slashes, except when the path is just "/". if (path != "/") { - path <- sub("/+$", "\\1", path) + path <- sub("/+$", "", path) } path diff --git a/man/staticPath.Rd b/man/staticPath.Rd index 04539adb..bc13ccdf 100644 --- a/man/staticPath.Rd +++ b/man/staticPath.Rd @@ -11,7 +11,7 @@ staticPath(path, indexhtml = NULL, fallthrough = NULL, \item{path}{The local path.} \item{indexhtml}{If an index.html file is present, should it be served up -when the client requests \code{/} or any subdirectory?} +when the client requests the static path or any subdirectory?} \item{fallthrough}{With the default value, \code{FALSE}, if a request is made for a file that doesn't exist, then httpuv will immediately send a 404 diff --git a/man/staticPathOptions.Rd b/man/staticPathOptions.Rd index 9363a2b2..a4f090f9 100644 --- a/man/staticPathOptions.Rd +++ b/man/staticPathOptions.Rd @@ -10,7 +10,7 @@ staticPathOptions(indexhtml = TRUE, fallthrough = FALSE, } \arguments{ \item{indexhtml}{If an index.html file is present, should it be served up -when the client requests \code{/} or any subdirectory?} +when the client requests the static path or any subdirectory?} \item{fallthrough}{With the default value, \code{FALSE}, if a request is made for a file that doesn't exist, then httpuv will immediately send a 404 diff --git a/tests/testthat/test-static-paths.R b/tests/testthat/test-static-paths.R index 065f7700..33a90e15 100644 --- a/tests/testthat/test-static-paths.R +++ b/tests/testthat/test-static-paths.R @@ -172,7 +172,7 @@ test_that("Options and option inheritance", { # This path unsets some options "/unset" = staticPath( test_path("apps/content"), - html_charset = character(), + html_charset = "", headers = list() ) ), From 984c70c061f45aa320bd31f405bfd7765b1b0bab Mon Sep 17 00:00:00 2001 From: Winston Chang Date: Thu, 29 Nov 2018 15:17:06 -0600 Subject: [PATCH 50/60] Add traffic tests for static paths --- tests/testthat/sample_app.R | 10 +++++- tests/testthat/test-static-paths.R | 4 +-- tests/testthat/test-traffic.R | 56 ++++++++++++++++++++++++++++-- 3 files changed, 64 insertions(+), 6 deletions(-) diff --git a/tests/testthat/sample_app.R b/tests/testthat/sample_app.R index 5951db47..8b245b70 100644 --- a/tests/testthat/sample_app.R +++ b/tests/testthat/sample_app.R @@ -1,5 +1,6 @@ library(httpuv) library(promises) +library(testthat) content <- list( status = 200L, @@ -67,6 +68,13 @@ app_handle <- startServer("0.0.0.0", app_port, } else { stop("Unknown request path:", req$PATH_INFO) } - } + }, + staticPaths = list( + "/static" = test_path("apps/content"), + "/static_fallthrough" = staticPath( + test_path("apps/content"), + fallthrough = TRUE + ) + ) ) ) diff --git a/tests/testthat/test-static-paths.R b/tests/testthat/test-static-paths.R index 33a90e15..ce06881c 100644 --- a/tests/testthat/test-static-paths.R +++ b/tests/testthat/test-static-paths.R @@ -447,8 +447,6 @@ test_that("Escaped characters in paths", { test_that("Paths with ..", { - # TODO: Figure out how to send a request with .. - s <- startServer("127.0.0.1", random_open_port(), list( call = function(req) { @@ -465,6 +463,8 @@ test_that("Paths with ..", { ) on.exit(s$stop()) + # Need to use http_request_con() instead of fetch() to send custom requests + # with "..". res <- http_request_con("GET /", "127.0.0.1", s$getPort()) expect_identical(res[1], "HTTP/1.1 404 Not Found") expect_true(any(grepl("^Test-Code-Path: R$", res, ignore.case = TRUE))) diff --git a/tests/testthat/test-traffic.R b/tests/testthat/test-traffic.R index 984e3dc4..b52eed88 100644 --- a/tests/testthat/test-traffic.R +++ b/tests/testthat/test-traffic.R @@ -40,9 +40,12 @@ parse_ab_output <- function(p) { # Launch sample_app process and return process object start_app <- function(port) { outfile <- tempfile() - callr::process$new( - "R", - args = c("-e", paste0("app_port <- ", port, "; source('sample_app.R'); service(Inf)")), + callr::r_bg( + function(app_port) { + source(testthat::test_path('sample_app.R'), local = TRUE) + service(Inf) + }, + args = list(app_port = port), stdout = outfile, stderr = outfile, supervise = TRUE ) } @@ -240,3 +243,50 @@ test_that("/body-error /async-error endpoints", { bench$kill() bencha$kill() }) + + +test_that("static paths", { + skip_if_not_possible() + port <- random_open_port() + p <- start_app(port) + + Sys.sleep(1) + expect_true(p$is_alive()) + + bench <- start_ab(port, "/static", n = 2000) + bencha <- start_ab(port, "/static/missing_file", n = 2000) + + bench$wait(20000) + bencha$wait(20000) + + results <- parse_ab_output(bench) + expect_false(results$hang) + expect_equal(results$completed, 2000) + + resultsa <- parse_ab_output(bencha) + expect_false(resultsa$hang) + expect_equal(resultsa$completed, 2000) + + bench$kill() + bencha$kill() + + + # Check fallthrough + bench <- start_ab(port, "/static_fallthrough", n = 2000) + bencha <- start_ab(port, "/static_fallthrough/missing_file", n = 1000) + + bench$wait(20000) + bencha$wait(20000) + + results <- parse_ab_output(bench) + expect_false(results$hang) + expect_equal(results$completed, 2000) + + resultsa <- parse_ab_output(bencha) + expect_false(resultsa$hang) + expect_equal(resultsa$completed, 1000) + + p$kill() + bench$kill() + bencha$kill() +}) From 3cbe07478cd2f6d17d6709e15575bec065021912 Mon Sep 17 00:00:00 2001 From: Winston Chang Date: Thu, 29 Nov 2018 15:29:07 -0600 Subject: [PATCH 51/60] Use better starting value for found_idx --- src/staticpath.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/staticpath.cpp b/src/staticpath.cpp index c0685201..77277b4d 100644 --- a/src/staticpath.cpp +++ b/src/staticpath.cpp @@ -307,7 +307,6 @@ boost::optional> StaticPathManager::matchStat } std::string path = url_path; - size_t found_idx = std::string::npos; std::string pre_slash; std::string post_slash; @@ -321,6 +320,8 @@ boost::optional> StaticPathManager::matchStat pre_slash = path; post_slash = ""; + size_t found_idx = path.length() + 1; + // This loop searches for a match in path_map of pre_slash, the part before // the last split-on '/'. If found, it returns a pair with the part before // the slash, and the part after the slash. If not found, it splits on the From f3e487d666bd22d717e63eaf5004ebf2a68ec3ae Mon Sep 17 00:00:00 2001 From: Winston Chang Date: Fri, 30 Nov 2018 12:22:23 -0600 Subject: [PATCH 52/60] Distinguish between missing file and read error --- src/filedatasource-unix.cpp | 17 +++++++++++------ src/filedatasource.h | 12 +++++++++++- src/webapplication.cpp | 22 ++++++++++++++-------- 3 files changed, 36 insertions(+), 15 deletions(-) diff --git a/src/filedatasource-unix.cpp b/src/filedatasource-unix.cpp index 02a4ec12..786b22b3 100644 --- a/src/filedatasource-unix.cpp +++ b/src/filedatasource-unix.cpp @@ -6,26 +6,31 @@ #include #include -int FileDataSource::initialize(const std::string& path, bool owned) { +FileDataSourceResult FileDataSource::initialize(const std::string& path, bool owned) { // This can be called from either the main thread or background thread. _fd = open(path.c_str(), O_RDONLY); if (_fd == -1) { - _lastErrorMessage = "Error opening file " + path + ": " + toString(errno) + "\n"; - return 1; + if (errno == ENOENT) { + _lastErrorMessage = "File does not exist: " + path + "\n"; + return FDS_NOT_EXIST; + } else { + _lastErrorMessage = "Error opening file " + path + ": " + toString(errno) + "\n"; + return FDS_ERROR; + } } else { struct stat info = {0}; if (fstat(_fd, &info)) { _lastErrorMessage = "Error opening path " + path + ": " + toString(errno) + "\n"; ::close(_fd); - return 1; + return FDS_ERROR; } if (S_ISDIR(info.st_mode)) { _lastErrorMessage = "File data source is a directory: " + path + "\n"; ::close(_fd); - return 1; + return FDS_ISDIR; } _length = info.st_size; @@ -37,7 +42,7 @@ int FileDataSource::initialize(const std::string& path, bool owned) { // It's OK to continue } - return 0; + return FDS_OK; } } diff --git a/src/filedatasource.h b/src/filedatasource.h index 93f038a9..7adc8846 100644 --- a/src/filedatasource.h +++ b/src/filedatasource.h @@ -3,6 +3,16 @@ #include "uvutil.h" + +// Status codes for FileDataSource::initialize(). +enum FileDataSourceResult { + FDS_OK = 0, // Initialization worked + FDS_NOT_EXIST, // File did not exist + FDS_ISDIR, // File is a directory + FDS_ERROR // Other error +}; + + class FileDataSource : public DataSource { #ifdef _WIN32 HANDLE _hFile; @@ -16,7 +26,7 @@ class FileDataSource : public DataSource { public: FileDataSource() {} - int initialize(const std::string& path, bool owned); + FileDataSourceResult initialize(const std::string& path, bool owned); uint64_t size() const; uv_buf_t getData(size_t bytesDesired); void freeData(uv_buf_t buffer); diff --git a/src/webapplication.cpp b/src/webapplication.cpp index f2537569..1aca7054 100644 --- a/src/webapplication.cpp +++ b/src/webapplication.cpp @@ -189,9 +189,11 @@ boost::shared_ptr listToResponse( // - body: Character vector (which is charToRaw-ed) or raw vector, or NULL if (std::find(names.begin(), names.end(), "bodyFile") != names.end()) { FileDataSource* pFDS = new FileDataSource(); - int ret = pFDS->initialize(Rcpp::as(response["bodyFile"]), - Rcpp::as(response["bodyFileOwned"])); - if (ret != 0) { + FileDataSourceResult ret = pFDS->initialize( + Rcpp::as(response["bodyFile"]), + Rcpp::as(response["bodyFileOwned"]) + ); + if (ret != FDS_OK) { REprintf(pFDS->lastErrorMessage().c_str()); } pDataSource = pFDS; @@ -508,16 +510,20 @@ boost::shared_ptr RWebApplication::staticFileResponse( // Self-frees when response is written FileDataSource* pDataSource = new FileDataSource(); - int ret = pDataSource->initialize(local_path, false); + FileDataSourceResult ret = pDataSource->initialize(local_path, false); - if (ret != 0) { + if (ret != FDS_OK) { // Couldn't read the file delete pDataSource; - if (sp.options.fallthrough.get()) { - return nullptr; + if (ret == FDS_NOT_EXIST || ret == FDS_ISDIR) { + if (sp.options.fallthrough.get()) { + return nullptr; + } else { + return error_response(pRequest, 404); + } } else { - return error_response(pRequest, 404); + return error_response(pRequest, 500); } } From 4f499a82f3acecabf435b7a073a8a47c4c4f5f40 Mon Sep 17 00:00:00 2001 From: Winston Chang Date: Fri, 30 Nov 2018 12:48:57 -0600 Subject: [PATCH 53/60] Implement missing file detection for Windows --- src/filedatasource-win.cpp | 29 +++++++++++++++++++++++------ 1 file changed, 23 insertions(+), 6 deletions(-) diff --git a/src/filedatasource-win.cpp b/src/filedatasource-win.cpp index 2ce77b4e..d3a648a5 100644 --- a/src/filedatasource-win.cpp +++ b/src/filedatasource-win.cpp @@ -8,7 +8,8 @@ // so we can use FILE_FLAG_DELETE_ON_CLOSE, which is not available // using the POSIX file functions. -int FileDataSource::initialize(const std::string& path, bool owned) { + +FileDataSourceResult FileDataSource::initialize(const std::string& path, bool owned) { // This can be called from either the main thread or background thread. DWORD flags = FILE_FLAG_SEQUENTIAL_SCAN; @@ -24,19 +25,35 @@ int FileDataSource::initialize(const std::string& path, bool owned) { NULL); if (_hFile == INVALID_HANDLE_VALUE) { - _lastErrorMessage = "Error opening file " + path + ": " + toString(GetLastError()) + "\n"; - return 1; + if (GetLastError() == ERROR_FILE_NOT_FOUND) { + _lastErrorMessage = "File does not exist: " + path + "\n"; + return FDS_NOT_EXIST; + + } else if (GetLastError() == ERROR_ACCESS_DENIED && + (GetFileAttributesA(path.c_str()) & FILE_ATTRIBUTE_DIRECTORY)) { + // Note that the condition tested here has a potential race between the + // CreateFile() call and the GetFileAttributesA() call. It's not clear + // to me how to try to open a file and then detect if the file is a + // directory, in an atomic operation. The probability of this race + // condition is very low, and the result is relatively harmless for our + // purposes: it will fall through to the generic FDS_ERROR path. + _lastErrorMessage = "File data source is a directory: " + path + "\n"; + return FDS_ISDIR; + + } else { + _lastErrorMessage = "Error opening file " + path + ": " + toString(GetLastError()) + "\n"; + return FDS_ERROR; + } } - // TODO: Error if it's a directory if (!GetFileSizeEx(_hFile, &_length)) { CloseHandle(_hFile); _lastErrorMessage = "Error retrieving file size for " + path + ": " + toString(GetLastError()) + "\n"; - return 1; + return FDS_ERROR; } - return 0; + return FDS_OK; } uint64_t FileDataSource::size() const { From 85a6c6a87488474c8f48d6299ffb9a3c44e4ab1c Mon Sep 17 00:00:00 2001 From: Winston Chang Date: Thu, 29 Nov 2018 12:25:35 -0600 Subject: [PATCH 54/60] Fix test for Windows --- tests/testthat/test-static-paths.R | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/testthat/test-static-paths.R b/tests/testthat/test-static-paths.R index ce06881c..4060f945 100644 --- a/tests/testthat/test-static-paths.R +++ b/tests/testthat/test-static-paths.R @@ -420,7 +420,9 @@ test_that("Escaped characters in paths", { # Need to create files with weird names static_dir <- tempfile("httpuv_test") dir.create(static_dir) - cat("This is file content.\n", file = file.path(static_dir, "file with space.txt")) + # Use writeBin() instead of cat() because in Windows, cat() will convert "\n" + # to "\r\n". + writeBin(charToRaw("This is file content.\n"), file.path(static_dir, "file with space.txt")) on.exit(unlink(static_dir, recursive = TRUE)) From 1910cf0a449510480569c23ff445c89268cbc815 Mon Sep 17 00:00:00 2001 From: Winston Chang Date: Fri, 30 Nov 2018 14:15:57 -0600 Subject: [PATCH 55/60] Decode URI after removing query string --- src/webapplication.cpp | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/webapplication.cpp b/src/webapplication.cpp index 1aca7054..7ce85185 100644 --- a/src/webapplication.cpp +++ b/src/webapplication.cpp @@ -444,11 +444,9 @@ boost::shared_ptr RWebApplication::staticFileResponse( return nullptr; } - std::string url = doDecodeURI(pRequest->url(), true); - // Strip off query string - std::pair url_query = splitQueryString(url); - std::string& url_path = url_query.first; + std::pair url_query = splitQueryString(pRequest->url()); + std::string url_path = doDecodeURI(url_query.first, true); boost::optional> sp_pair = _staticPathManager.matchStaticPath(url_path); From 8bdeea2ff0667ca96ec1a5560284caf6a0702058 Mon Sep 17 00:00:00 2001 From: Winston Chang Date: Fri, 30 Nov 2018 14:22:38 -0600 Subject: [PATCH 56/60] Add Date header in constructor for HttpResponse --- src/httpresponse.h | 2 ++ src/webapplication.cpp | 1 - 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/httpresponse.h b/src/httpresponse.h index 1f9dfd2c..7ebf27af 100644 --- a/src/httpresponse.h +++ b/src/httpresponse.h @@ -2,6 +2,7 @@ #define HTTPRESPONSE_HPP #include "uvutil.h" +#include "utils.h" #include "constants.h" #include #include @@ -30,6 +31,7 @@ class HttpResponse : public boost::enable_shared_from_this { _pBody(pBody), _closeAfterWritten(false) { + _headers.push_back(std::make_pair("Date", http_date_string(time(NULL)))); } ~HttpResponse(); diff --git a/src/webapplication.cpp b/src/webapplication.cpp index 7ce85185..4a375ff0 100644 --- a/src/webapplication.cpp +++ b/src/webapplication.cpp @@ -569,7 +569,6 @@ boost::shared_ptr RWebApplication::staticFileResponse( // the response for the HEAD would not. respHeaders.push_back(std::make_pair("Content-Length", toString(file_size))); respHeaders.push_back(std::make_pair("Content-Type", content_type)); - respHeaders.push_back(std::make_pair("Date", http_date_string(time(NULL)))); return pResponse; } From 55d6832c82ab83fe9d747ee72edc8e771052fdea Mon Sep 17 00:00:00 2001 From: Winston Chang Date: Fri, 30 Nov 2018 15:28:36 -0600 Subject: [PATCH 57/60] Fix PipeServer initialization --- R/server.R | 54 ++++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 52 insertions(+), 2 deletions(-) diff --git a/R/server.R b/R/server.R index 75c6e6ec..5d04da01 100644 --- a/R/server.R +++ b/R/server.R @@ -96,14 +96,58 @@ WebServer <- R6Class("WebServer", ) +#' PipeServer class +#' +#' This class represents a server running one application that listens on a +#' named pipe. +#' +#' @section Methods: +#' +#' \describe{ +#' \item{\code{initialize(name, mask, app)}}{ +#' Create a new \code{PipeServer} object. \code{app} is an httpuv application +#' object as described in \code{\link{startServer}}. +#' } +#' \item{\code{getName()}}{Return the value of \code{name} that was passed to +#' \code{initialize()}. +#' } +#' \item{\code{getMask()}}{Return the value of \code{mask} that was passed to +#' \code{initialize()}. +#' } +#' \item{\code{stop()}}{Stops a running server.} +#' \item{\code{isRunning()}}{Returns TRUE if the server is currently running.} +#' \item{\code{getStaticPaths()}}{Returns a list of \code{\link{staticPath}} +#' objects for the server. +#' } +#' \item{\code{setStaticPath(..., .list = NULL)}}{Sets a static path for the +#' current server. Each static path can be given as a named argument, or as +#' an named item in \code{.list}. If there already exists a static path with +#' the same name, it will be replaced. +#' } +#' \item{\code{removeStaticPath(path)}}{Removes a static path with the given +#' name. +#' } +#' \item{\code{getStaticPathOptions()}}{Returns a list of default +#' \code{staticPathOptions} for the current server. Each static path will +#' use these options by default, but they can be overridden for each static +#' path. +#' } +#' \item{\code{setStaticPathOption(..., .list = NULL)}}{Sets one or more +#' static path options. Each option can be given as a named argument, or as +#' a named item in \code{.list}. +#' } +#' } +#' +#' @keywords internal PipeServer <- R6Class("PipeServer", cloneable = FALSE, inherit = Server, public = list( initialize = function(name, mask, app) { - if (is.null(private$mask)) { - private$mask <- -1 + if (is.null(mask)) { + mask <- -1 } + private$mask <- mask private$appWrapper <- AppWrapper$new(app) private$handle <- makePipeServer( @@ -124,6 +168,12 @@ PipeServer <- R6Class("PipeServer", if (is.null(private$handle)) { stop("Failed to create server") } + }, + getName = function() { + private$name + }, + getMask = function() { + private$mask } ), private = list( From bc7a0612569bdbd5acf12733abe45bcc572c37ef Mon Sep 17 00:00:00 2001 From: Winston Chang Date: Fri, 30 Nov 2018 16:07:27 -0600 Subject: [PATCH 58/60] Document Server, WebServer, and PipeServer classes --- R/httpuv.R | 57 ++++++++++++++++++++++++++++--- R/server.R | 83 ++++++++++++++++++++++++++++++++++++++++++++++ man/PipeServer.Rd | 54 ++++++++++++++++++++++++++++++ man/Server.Rd | 45 +++++++++++++++++++++++++ man/WebServer.Rd | 54 ++++++++++++++++++++++++++++++ man/startServer.Rd | 54 +++++++++++++++++++++++++++--- 6 files changed, 339 insertions(+), 8 deletions(-) create mode 100644 man/PipeServer.Rd create mode 100644 man/Server.Rd create mode 100644 man/WebServer.Rd diff --git a/R/httpuv.R b/R/httpuv.R index 3db09536..a5c439c0 100644 --- a/R/httpuv.R +++ b/R/httpuv.R @@ -415,9 +415,16 @@ WebSocket <- setRefClass( #' If the port cannot be bound (most likely due to permissions or because it #' is already bound), an error is raised. #' +#' The application can also specify paths on the filesystem which will be +#' served from the background thread, without invoking \code{$call()} or +#' \code{$onHeaders()}. Files served this way will be only use a C++ code, +#' which is faster than going through R, and will not be blocked when R code +#' is executing. This can greatly improve performance when serving static +#' assets. +#' #' The \code{app} parameter is where your application logic will be provided #' to the server. This can be a list, environment, or reference class that -#' contains the following named functions/methods: +#' contains the following methods and fields: #' #' \describe{ #' \item{\code{call(req)}}{Process the given HTTP request, and return an @@ -433,18 +440,33 @@ WebSocket <- setRefClass( #' \item{\code{onWSOpen(ws)}}{Called back when a WebSocket connection is established. #' The given object can be used to be notified when a message is received from #' the client, to send messages to the client, etc. See \code{\link{WebSocket}}.} +#' \item{\code{staticPaths}}{ +#' A named list of paths that will be served without invoking +#' \code{call()} or \code{onHeaders}. The name of each one is the URL +#' path, and the value is either a string referring to a local path, or an +#' object created by the \code{\link{staticPath}} function. +#' } +#' \item{\code{staticPathOptions}}{ +#' A set of default options to use when serving static paths. If +#' not set or \code{NULL}, then it will use the result from calling +#' \code{\link{staticPathOptions}()} with no arguments. +#' } #' } #' #' The \code{startPipeServer} variant can be used instead of #' \code{startServer} to listen on a Unix domain socket or named pipe rather #' than a TCP socket (this is not common). -#' @seealso \code{\link{stopServer}}, \code{\link{runServer}} +#' +#' @return A \code{\link{WebServer}} or \code{\link{PipeServer}} object. +#' +#' @seealso \code{\link{stopServer}}, \code{\link{runServer}}, +#' \code{\link{listServers}}, \code{\link{stopAllServers}}. #' @aliases startPipeServer #' #' @examples #' \dontrun{ #' # A very basic application -#' handle <- startServer("0.0.0.0", 5000, +#' s <- startServer("0.0.0.0", 5000, #' list( #' call = function(req) { #' list( @@ -458,7 +480,34 @@ WebSocket <- setRefClass( #' ) #' ) #' -#' stopServer(handle) +#' s$stop() +#' +#' +#' # An application that serves static assets at the URL paths /assets and /lib +#' s <- startServer("0.0.0.0", 5000, +#' list( +#' call = function(req) { +#' list( +#' status = 200L, +#' headers = list( +#' 'Content-Type' = 'text/html' +#' ), +#' body = "Hello world!" +#' ) +#' }, +#' staticPaths = list( +#' "/assets" = "content/assets/" +#' "/lib" = staticPath( +#' "content/lib", +#' indexhtml = FALSE +#' ), +#' staticPathOptions = staticPathOptions( +#' indexhtml = TRUE +#' ) +#' ) +#' ) +#' +#' s$stop() #' } #' @export startServer <- function(host, port, app) { diff --git a/R/server.R b/R/server.R index 5d04da01..b8f4e81c 100644 --- a/R/server.R +++ b/R/server.R @@ -1,5 +1,45 @@ #' @include httpuv.R +NULL +# Note that the methods listed for Server, WebServer, and PipeServer were copied +# and pasted among all three, with a few additional methods added to WebServer +# and PipeServer. When changes are made in the future, make sure that they're +# duplicated among all three. + +#' Server class +#' +#' The \code{Server} class is the parent class for \code{\link{WebServer}} and +#' \code{\link{PipeServer}}. This class defines an interface and is not meant to +#' be instantiated. +#' +#' @section Methods: +#' +#' \describe{ +#' \item{\code{stop()}}{Stops a running server.} +#' \item{\code{isRunning()}}{Returns TRUE if the server is currently running.} +#' \item{\code{getStaticPaths()}}{Returns a list of \code{\link{staticPath}} +#' objects for the server. +#' } +#' \item{\code{setStaticPath(..., .list = NULL)}}{Sets a static path for the +#' current server. Each static path can be given as a named argument, or as +#' an named item in \code{.list}. If there already exists a static path with +#' the same name, it will be replaced. +#' } +#' \item{\code{removeStaticPath(path)}}{Removes a static path with the given +#' name. +#' } +#' \item{\code{getStaticPathOptions()}}{Returns a list of default +#' \code{staticPathOptions} for the current server. Each static path will +#' use these options by default, but they can be overridden for each static +#' path. +#' } +#' \item{\code{setStaticPathOption(..., .list = NULL)}}{Sets one or more +#' static path options. Each option can be given as a named argument, or as +#' a named item in \code{.list}. +#' } +#' } +#' +#' @keywords internal #' @importFrom R6 R6Class Server <- R6Class("Server", cloneable = FALSE, @@ -54,6 +94,49 @@ Server <- R6Class("Server", ) +#' WebServer class +#' +#' This class represents a web server running one application. Multiple servers +#' can be running at the same time. +#' +#' @section Methods: +#' +#' \describe{ +#' \item{\code{initialize(host, port, app)}}{ +#' Create a new \code{WebServer} object. \code{app} is an httpuv application +#' object as described in \code{\link{startServer}}. +#' } +#' \item{\code{getHost()}}{Return the value of \code{host} that was passed to +#' \code{initialize()}. +#' } +#' \item{\code{getPort()}}{Return the value of \code{port} that was passed to +#' \code{initialize()}. +#' } +#' \item{\code{stop()}}{Stops a running server.} +#' \item{\code{isRunning()}}{Returns TRUE if the server is currently running.} +#' \item{\code{getStaticPaths()}}{Returns a list of \code{\link{staticPath}} +#' objects for the server. +#' } +#' \item{\code{setStaticPath(..., .list = NULL)}}{Sets a static path for the +#' current server. Each static path can be given as a named argument, or as +#' an named item in \code{.list}. If there already exists a static path with +#' the same name, it will be replaced. +#' } +#' \item{\code{removeStaticPath(path)}}{Removes a static path with the given +#' name. +#' } +#' \item{\code{getStaticPathOptions()}}{Returns a list of default +#' \code{staticPathOptions} for the current server. Each static path will +#' use these options by default, but they can be overridden for each static +#' path. +#' } +#' \item{\code{setStaticPathOption(..., .list = NULL)}}{Sets one or more +#' static path options. Each option can be given as a named argument, or as +#' a named item in \code{.list}. +#' } +#' } +#' +#' @keywords internal WebServer <- R6Class("WebServer", cloneable = FALSE, inherit = Server, diff --git a/man/PipeServer.Rd b/man/PipeServer.Rd new file mode 100644 index 00000000..d3f290a0 --- /dev/null +++ b/man/PipeServer.Rd @@ -0,0 +1,54 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/server.R +\docType{data} +\name{PipeServer} +\alias{PipeServer} +\title{PipeServer class} +\format{An object of class \code{R6ClassGenerator} of length 24.} +\usage{ +PipeServer +} +\description{ +This class represents a server running one application that listens on a +named pipe. +} +\section{Methods}{ + + +\describe{ + \item{\code{initialize(name, mask, app)}}{ + Create a new \code{PipeServer} object. \code{app} is an httpuv application + object as described in \code{\link{startServer}}. + } + \item{\code{getName()}}{Return the value of \code{name} that was passed to + \code{initialize()}. + } + \item{\code{getMask()}}{Return the value of \code{mask} that was passed to + \code{initialize()}. + } + \item{\code{stop()}}{Stops a running server.} + \item{\code{isRunning()}}{Returns TRUE if the server is currently running.} + \item{\code{getStaticPaths()}}{Returns a list of \code{\link{staticPath}} + objects for the server. + } + \item{\code{setStaticPath(..., .list = NULL)}}{Sets a static path for the + current server. Each static path can be given as a named argument, or as + an named item in \code{.list}. If there already exists a static path with + the same name, it will be replaced. + } + \item{\code{removeStaticPath(path)}}{Removes a static path with the given + name. + } + \item{\code{getStaticPathOptions()}}{Returns a list of default + \code{staticPathOptions} for the current server. Each static path will + use these options by default, but they can be overridden for each static + path. + } + \item{\code{setStaticPathOption(..., .list = NULL)}}{Sets one or more + static path options. Each option can be given as a named argument, or as + a named item in \code{.list}. + } +} +} + +\keyword{internal} diff --git a/man/Server.Rd b/man/Server.Rd new file mode 100644 index 00000000..da291efc --- /dev/null +++ b/man/Server.Rd @@ -0,0 +1,45 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/server.R +\docType{data} +\name{Server} +\alias{Server} +\title{Server class} +\format{An object of class \code{R6ClassGenerator} of length 24.} +\usage{ +Server +} +\description{ +The \code{Server} class is the parent class for \code{\link{WebServer}} and +\code{\link{PipeServer}}. This class defines an interface and is not meant to +be instantiated. +} +\section{Methods}{ + + +\describe{ + \item{\code{stop()}}{Stops a running server.} + \item{\code{isRunning()}}{Returns TRUE if the server is currently running.} + \item{\code{getStaticPaths()}}{Returns a list of \code{\link{staticPath}} + objects for the server. + } + \item{\code{setStaticPath(..., .list = NULL)}}{Sets a static path for the + current server. Each static path can be given as a named argument, or as + an named item in \code{.list}. If there already exists a static path with + the same name, it will be replaced. + } + \item{\code{removeStaticPath(path)}}{Removes a static path with the given + name. + } + \item{\code{getStaticPathOptions()}}{Returns a list of default + \code{staticPathOptions} for the current server. Each static path will + use these options by default, but they can be overridden for each static + path. + } + \item{\code{setStaticPathOption(..., .list = NULL)}}{Sets one or more + static path options. Each option can be given as a named argument, or as + a named item in \code{.list}. + } +} +} + +\keyword{internal} diff --git a/man/WebServer.Rd b/man/WebServer.Rd new file mode 100644 index 00000000..d5bbd66b --- /dev/null +++ b/man/WebServer.Rd @@ -0,0 +1,54 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/server.R +\docType{data} +\name{WebServer} +\alias{WebServer} +\title{WebServer class} +\format{An object of class \code{R6ClassGenerator} of length 24.} +\usage{ +WebServer +} +\description{ +This class represents a web server running one application. Multiple servers +can be running at the same time. +} +\section{Methods}{ + + +\describe{ + \item{\code{initialize(host, port, app)}}{ + Create a new \code{WebServer} object. \code{app} is an httpuv application + object as described in \code{\link{startServer}}. + } + \item{\code{getHost()}}{Return the value of \code{host} that was passed to + \code{initialize()}. + } + \item{\code{getPort()}}{Return the value of \code{port} that was passed to + \code{initialize()}. + } + \item{\code{stop()}}{Stops a running server.} + \item{\code{isRunning()}}{Returns TRUE if the server is currently running.} + \item{\code{getStaticPaths()}}{Returns a list of \code{\link{staticPath}} + objects for the server. + } + \item{\code{setStaticPath(..., .list = NULL)}}{Sets a static path for the + current server. Each static path can be given as a named argument, or as + an named item in \code{.list}. If there already exists a static path with + the same name, it will be replaced. + } + \item{\code{removeStaticPath(path)}}{Removes a static path with the given + name. + } + \item{\code{getStaticPathOptions()}}{Returns a list of default + \code{staticPathOptions} for the current server. Each static path will + use these options by default, but they can be overridden for each static + path. + } + \item{\code{setStaticPathOption(..., .list = NULL)}}{Sets one or more + static path options. Each option can be given as a named argument, or as + a named item in \code{.list}. + } +} +} + +\keyword{internal} diff --git a/man/startServer.Rd b/man/startServer.Rd index 53cc8493..933ebca9 100644 --- a/man/startServer.Rd +++ b/man/startServer.Rd @@ -33,6 +33,8 @@ umask is left unchanged. (This parameter has no effect on Windows.)} \value{ A handle for this server that can be passed to \code{\link{stopServer}} to shut the server down. + +A \code{\link{WebServer}} or \code{\link{PipeServer}} object. } \description{ Creates an HTTP/WebSocket server on the specified host and port. @@ -60,9 +62,16 @@ Creates an HTTP/WebSocket server on the specified host and port. If the port cannot be bound (most likely due to permissions or because it is already bound), an error is raised. + The application can also specify paths on the filesystem which will be + served from the background thread, without invoking \code{$call()} or + \code{$onHeaders()}. Files served this way will be only use a C++ code, + which is faster than going through R, and will not be blocked when R code + is executing. This can greatly improve performance when serving static + assets. + The \code{app} parameter is where your application logic will be provided to the server. This can be a list, environment, or reference class that - contains the following named functions/methods: + contains the following methods and fields: \describe{ \item{\code{call(req)}}{Process the given HTTP request, and return an @@ -78,6 +87,17 @@ Creates an HTTP/WebSocket server on the specified host and port. \item{\code{onWSOpen(ws)}}{Called back when a WebSocket connection is established. The given object can be used to be notified when a message is received from the client, to send messages to the client, etc. See \code{\link{WebSocket}}.} + \item{\code{staticPaths}}{ + A named list of paths that will be served without invoking + \code{call()} or \code{onHeaders}. The name of each one is the URL + path, and the value is either a string referring to a local path, or an + object created by the \code{\link{staticPath}} function. + } + \item{\code{staticPathOptions}}{ + A set of default options to use when serving static paths. If + not set or \code{NULL}, then it will use the result from calling + \code{\link{staticPathOptions}()} with no arguments. + } } The \code{startPipeServer} variant can be used instead of @@ -87,7 +107,7 @@ Creates an HTTP/WebSocket server on the specified host and port. \examples{ \dontrun{ # A very basic application -handle <- startServer("0.0.0.0", 5000, +s <- startServer("0.0.0.0", 5000, list( call = function(req) { list( @@ -101,9 +121,35 @@ handle <- startServer("0.0.0.0", 5000, ) ) -stopServer(handle) +s$stop() + + +# An application that serves static assets at the URL paths /assets and /lib +s <- startServer("0.0.0.0", 5000, + list( + call = function(req) { + list( + status = 200L, + headers = list( + 'Content-Type' = 'text/html' + ), + body = "Hello world!" + ) + }, + staticPaths = list( + "/assets" = "content/assets/" + "/lib" = staticPath( + "content/lib", + indexhtml = FALSE + ), + staticPathOptions = staticPathOptions( + indexhtml = TRUE + ) + ) +) } } \seealso{ -\code{\link{stopServer}}, \code{\link{runServer}} +\code{\link{stopServer}}, \code{\link{runServer}}, + \code{\link{listServers}}, \code{\link{stopAllServers}}. } From 7bf3d73fa3be85ea56e42701dcc69bad24b15b2b Mon Sep 17 00:00:00 2001 From: Winston Chang Date: Fri, 30 Nov 2018 16:11:52 -0600 Subject: [PATCH 59/60] Don't get/set static paths for stopped app --- R/server.R | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/R/server.R b/R/server.R index b8f4e81c..9c452ddb 100644 --- a/R/server.R +++ b/R/server.R @@ -59,21 +59,31 @@ Server <- R6Class("Server", private$running }, getStaticPaths = function() { + if (!private$running) return(NULL) + getStaticPaths_(private$handle) }, setStaticPath = function(..., .list = NULL) { + if (!private$running) return(invisible()) + paths <- c(list(...), .list) paths <- normalizeStaticPaths(paths) invisible(setStaticPaths_(private$handle, paths)) }, removeStaticPath = function(path) { + if (!private$running) return(invisible()) + path <- as.character(path) invisible(removeStaticPaths_(private$handle, path)) }, getStaticPathOptions = function() { + if (!private$running) return(NULL) + getStaticPathOptions_(private$handle) }, setStaticPathOption = function(..., .list = NULL) { + if (!private$running) return(invisible()) + opts <- c(list(...), .list) opts <- drop_duplicate_names(opts) opts <- normalizeStaticPathOptions(opts) From bd77f361ddae01214d0e8cbc853f575f6907e05e Mon Sep 17 00:00:00 2001 From: Winston Chang Date: Mon, 3 Dec 2018 12:00:42 -0600 Subject: [PATCH 60/60] Fix display of empty options --- R/static_paths.R | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/R/static_paths.R b/R/static_paths.R index 9132a8db..d34160c0 100644 --- a/R/static_paths.R +++ b/R/static_paths.R @@ -129,14 +129,14 @@ print.staticPathOptions <- function(x, ...) { format.staticPathOptions <- function(x, ...) { paste0( "\n", - format_opts(x) + format_opts(x, format_empty = "") ) } -format_opts <- function(x) { +format_opts <- function(x, format_empty = "") { format_option <- function(opt) { if (is.null(opt) || length(opt) == 0) { - "" + format_empty } else if (!is.null(names(opt))) { # Named character vector