diff --git a/DESCRIPTION b/DESCRIPTION index 997b511d..994cd0ea 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; @@ -21,12 +21,20 @@ Depends: Imports: Rcpp (>= 0.11.0), utils, + R6, promises, later (>= 0.7.3) 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 + callr, + curl +Collate: + 'RcppExports.R' + 'httpuv.R' + 'server.R' + 'static_paths.R' + 'utils.R' diff --git a/NAMESPACE b/NAMESPACE index 7c651ef3..093922b6 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -1,5 +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) @@ -7,17 +11,21 @@ export(encodeURIComponent) export(getRNGState) export(interrupt) export(ipFamily) +export(listServers) export(rawToBase64) export(runServer) export(service) export(startDaemonizedServer) export(startPipeServer) export(startServer) +export(staticPath) +export(staticPathOptions) export(stopAllServers) export(stopDaemonizedServer) export(stopServer) exportClasses(WebSocket) import(methods) +importFrom(R6,R6Class) importFrom(Rcpp,evalCpp) importFrom(later,run_now) importFrom(promises,"%...!%") 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)) diff --git a/R/RcppExports.R b/R/RcppExports.R index 4a552a35..f5ec254e 100644 --- a/R/RcppExports.R +++ b/R/RcppExports.R @@ -9,40 +9,36 @@ 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, 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) { - .Call('_httpuv_makePipeServer', PACKAGE = 'httpuv', name, mask, onHeaders, onBodyData, onRequest, onWSOpen, onWSMessage, onWSClose) +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) } -#' 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 -#' -#' 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')) +getStaticPaths_ <- function(handle) { + .Call('_httpuv_getStaticPaths_', PACKAGE = 'httpuv', handle) +} + +setStaticPaths_ <- function(handle, sp) { + .Call('_httpuv_setStaticPaths_', PACKAGE = 'httpuv', handle, sp) +} + +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) { diff --git a/R/httpuv.R b/R/httpuv.R index a7eb8bba..a5c439c0 100644 --- a/R/httpuv.R +++ b/R/httpuv.R @@ -170,7 +170,9 @@ AppWrapper <- setRefClass( fields = list( .app = 'ANY', .wsconns = 'environment', - .supportsOnHeaders = 'logical' + .supportsOnHeaders = 'logical', + .staticPaths = 'list', + .staticPathOptions = 'ANY' ), methods = list( initialize = function(app) { @@ -181,6 +183,35 @@ 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" || + 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 <<- normalizeStaticPathOptions(.app$staticPathOptions) + } else { + stop("staticPathOptions must be an object of class staticPathOptions.") + } }, onHeaders = function(req) { if (!.supportsOnHeaders) @@ -384,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 @@ -402,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( @@ -427,26 +480,38 @@ 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) { - - appWrapper <- AppWrapper$new(app) - server <- makeTcpServer( - host, port, - appWrapper$onHeaders, - appWrapper$onBodyData, - appWrapper$call, - appWrapper$onWSOpen, - appWrapper$onWSMessage, - appWrapper$onWSClose - ) - - 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 @@ -460,21 +525,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 @@ -625,16 +676,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 - # 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..9c452ddb --- /dev/null +++ b/R/server.R @@ -0,0 +1,352 @@ +#' @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, + public = list( + stop = function() { + if (!private$running) return(invisible()) + + stopServer_(private$handle) + private$running <- FALSE + deregisterServer(self) + invisible() + }, + 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() { + 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) + + 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( + appWrapper = NULL, + handle = NULL, + running = FALSE + ) +) + + +#' 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, + 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$.staticPaths, + private$appWrapper$.staticPathOptions + ) + + if (is.null(private$handle)) { + stop("Failed to create server") + } + + private$running <- TRUE + registerServer(self) + }, + getHost = function() { + private$host + }, + getPort = function() { + private$port + } + ), + private = list( + host = NULL, + port = NULL + ) +) + + +#' 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(mask)) { + mask <- -1 + } + private$mask <- mask + 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, + private$appWrapper$.staticPaths, + private$appWrapper$.staticPathOptions + ) + + # Save the full path. normalizePath must be called after makePipeServer + private$name <- normalizePath(name) + + if (is.null(private$handle)) { + stop("Failed to create server") + } + }, + getName = function() { + private$name + }, + getMask = function() { + private$mask + } + ), + private = list( + name = NULL, + mask = NULL + ) +) + + +#' Stop a server +#' +#' 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 server A server object 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 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() + +#' List all running httpuv servers +#' +#' This returns a list of all running httpuv server applications. +#' +#' @export +listServers <- function() { + .globals$servers +} + +registerServer <- function(server) { + .globals$servers[[length(.globals$servers) + 1]] <- server +} + +deregisterServer <- function(server) { + for (i in seq_along(.globals$servers)) { + if (identical(server, .globals$servers[[i]])) { + .globals$servers[[i]] <- NULL + return() + } + } + + warning("Unable to deregister server: server not found in list of running servers.") +} + + + + +#' 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/R/static_paths.R b/R/static_paths.R new file mode 100644 index 00000000..d34160c0 --- /dev/null +++ b/R/static_paths.R @@ -0,0 +1,283 @@ +#' Create a 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. +#' @inheritParams staticPathOptions +#' +#' @seealso \code{\link{staticPathOptions}}. +#' +#' @export +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.") + } + + path <- normalizePath(path, winslash = "/", mustWork = TRUE) + + structure( + list( + path = path, + options = normalizeStaticPathOptions(staticPathOptions( + indexhtml = indexhtml, + fallthrough = fallthrough, + html_charset = html_charset, + headers = headers, + validation = validation + )) + ), + 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", + format_opts(x$options) + ) +} + +#' Create options for static paths +#' +#' +#' @param indexhtml If an index.html file is present, should it be served up +#' 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 +#' 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, + fallthrough = FALSE, + html_charset = "utf-8", + headers = list(), + validation = character(0) +) { + res <- structure( + list( + indexhtml = indexhtml, + fallthrough = fallthrough, + html_charset = html_charset, + headers = headers, + validation = validation + ), + class = "staticPathOptions" + ) + + normalizeStaticPathOptions(res) +} + +#' @export +print.staticPathOptions <- function(x, ...) { + cat(format(x, ...), sep = "\n") + invisible(x) +} + + +#' @export +format.staticPathOptions <- function(x, ...) { + paste0( + "\n", + format_opts(x, format_empty = "") + ) +} + +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 + 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( + " 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" + ) +} + + +# 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. This function +# is idempotent. +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 '/' and no trailing '/'. + 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) + } + # Strip trailing slashes, except when the path is just "/". + if (path != "/") { + path <- sub("/+$", "", path) + } + + path + }, "") + + 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) + } + + # 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 <- "" + } + } + + # 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) + # 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)) { + 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) { + 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 + # than "" will be parsed. + if (length(opts$validation) == 1) { + 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) + } + } + } + + attr(opts, "normalized") <- TRUE + opts +} diff --git a/R/utils.R b/R/utils.R index de470c45..72bcf393 100644 --- a/R/utils.R +++ b/R/utils.R @@ -12,3 +12,26 @@ httpuv_version <- local({ version } }) + +# 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)) +} + +# 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/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/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/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/startServer.Rd b/man/startServer.Rd index 60a19886..933ebca9 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) @@ -34,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. @@ -61,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 @@ -79,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 @@ -88,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( @@ -102,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}}. } diff --git a/man/staticPath.Rd b/man/staticPath.Rd new file mode 100644 index 00000000..bc13ccdf --- /dev/null +++ b/man/staticPath.Rd @@ -0,0 +1,47 @@ +% 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, indexhtml = NULL, fallthrough = NULL, + html_charset = NULL, headers = NULL, validation = 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 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 +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. 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..a4f090f9 --- /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 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 +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 +} 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 e6658dec..b4b07db8 100644 --- a/man/stopDaemonizedServer.Rd +++ b/man/stopDaemonizedServer.Rd @@ -1,13 +1,13 @@ % 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)} \usage{ -stopDaemonizedServer(handle) +stopDaemonizedServer(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{ diff --git a/man/stopServer.Rd b/man/stopServer.Rd index 550cdd89..f9493303 100644 --- a/man/stopServer.Rd +++ b/man/stopServer.Rd @@ -1,19 +1,19 @@ % 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 +\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/RcppExports.cpp b/src/RcppExports.cpp index 37ca355d..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); -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::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,13 +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_result_gen = Rcpp::wrap(makeTcpServer(host, port, onHeaders, onBodyData, onRequest, onWSOpen, onWSMessage, onWSClose)); + 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); -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::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; @@ -61,27 +63,78 @@ 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::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 } -// 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 } -// stopAllServers -void stopAllServers(); -RcppExport SEXP _httpuv_stopAllServers() { +// getStaticPaths_ +Rcpp::List getStaticPaths_(std::string handle); +RcppExport SEXP _httpuv_getStaticPaths_(SEXP handleSEXP) { BEGIN_RCPP + Rcpp::RObject rcpp_result_gen; Rcpp::RNGScope rcpp_rngScope_gen; - stopAllServers(); - return R_NilValue; + Rcpp::traits::input_parameter< std::string >::type handle(handleSEXP); + rcpp_result_gen = Rcpp::wrap(getStaticPaths_(handle)); + return rcpp_result_gen; +END_RCPP +} +// 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::List >::type sp(spSEXP); + rcpp_result_gen = Rcpp::wrap(setStaticPaths_(handle, sp)); + return rcpp_result_gen; +END_RCPP +} +// removeStaticPaths_ +Rcpp::List 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 +} +// 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 @@ -187,10 +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, 8}, - {"_httpuv_makePipeServer", (DL_FUNC) &_httpuv_makePipeServer, 8}, - {"_httpuv_stopServer", (DL_FUNC) &_httpuv_stopServer, 1}, - {"_httpuv_stopAllServers", (DL_FUNC) &_httpuv_stopAllServers, 0}, + {"_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}, diff --git a/src/filedatasource-unix.cpp b/src/filedatasource-unix.cpp index 4d4d7a68..786b22b3 100644 --- a/src/filedatasource-unix.cpp +++ b/src/filedatasource-unix.cpp @@ -6,29 +6,43 @@ #include #include -int FileDataSource::initialize(const std::string& path, bool owned) { - ASSERT_MAIN_THREAD() +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) { - REprintf("Error opening file: %d\n", errno); - 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)) { - REprintf("Error opening path: %d\n", errno); + _lastErrorMessage = "Error opening path " + path + ": " + toString(errno) + "\n"; + ::close(_fd); + 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; 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 %s: %d\n", path.c_str(), errno); // It's OK to continue } - return 0; + return FDS_OK; } } @@ -66,4 +80,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..d3a648a5 100644 --- a/src/filedatasource-win.cpp +++ b/src/filedatasource-win.cpp @@ -8,8 +8,9 @@ // 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) { - ASSERT_MAIN_THREAD() + +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; if (owned) @@ -24,17 +25,35 @@ int FileDataSource::initialize(const std::string& path, bool owned) { NULL); if (_hFile == INVALID_HANDLE_VALUE) { - REprintf("Error opening file: %d\n", GetLastError()); - 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; + } } + if (!GetFileSizeEx(_hFile, &_length)) { CloseHandle(_hFile); - REprintf("Error retrieving file size: %d\n", GetLastError()); - return 1; + _lastErrorMessage = "Error retrieving file size for " + path + ": " + toString(GetLastError()) + "\n"; + return FDS_ERROR; } - return 0; + return FDS_OK; } uint64_t FileDataSource::size() const { @@ -73,4 +92,9 @@ void FileDataSource::close() { } } +std::string FileDataSource::lastErrorMessage() const { + return _lastErrorMessage; +} + + #endif // #ifdef _WIN32 diff --git a/src/filedatasource.h b/src/filedatasource.h index 3849ea86..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; @@ -11,15 +21,17 @@ class FileDataSource : public DataSource { int _fd; off_t _length; #endif + std::string _lastErrorMessage = ""; 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); void close(); + std::string lastErrorMessage() const; }; #endif // FILEDATASOURCE_H diff --git a/src/fs.h b/src/fs.h new file mode 100644 index 00000000..b84c9f49 --- /dev/null +++ b/src/fs.h @@ -0,0 +1,66 @@ +#ifndef FS_H +#define FS_H + +#include + +#ifdef _WIN32 +#include +#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 + + 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 + + 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/httprequest.cpp b/src/httprequest.cpp index 6b8069cc..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,6 +288,23 @@ int HttpRequest::_on_headers_complete(http_parser* pParser) { trace("HttpRequest::_on_headers_complete"); updateUpgradeStatus(); + // 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()); + + 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) + ); + _background_queue->push(cb); + return 0; + } + + boost::function)> schedule_bg_callback( boost::bind(&HttpRequest::_schedule_on_headers_complete_complete, shared_from_this(), _1) ); @@ -333,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: @@ -363,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/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/httpuv.cpp b/src/httpuv.cpp index 4fd3b65e..d47607a8 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, @@ -239,7 +244,9 @@ 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::List staticPaths, + Rcpp::List staticPathOptions) { using namespace Rcpp; register_main_thread(); @@ -248,7 +255,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, + staticPaths, staticPathOptions), auto_deleter_main ); @@ -292,7 +300,9 @@ Rcpp::RObject makePipeServer(const std::string& name, Rcpp::Function onRequest, Rcpp::Function onWSOpen, Rcpp::Function onWSMessage, - Rcpp::Function onWSClose) { + Rcpp::Function onWSClose, + Rcpp::List staticPaths, + Rcpp::List staticPathOptions) { using namespace Rcpp; register_main_thread(); @@ -301,7 +311,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, + staticPaths, staticPathOptions), auto_deleter_main ); @@ -338,7 +349,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. @@ -358,53 +369,65 @@ 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); +} + +void stop_loop_timer_cb(uv_timer_t* handle) { + uv_stop(handle->loop); +} + + +// ============================================================================ +// 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); } -//' 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() { +Rcpp::List getStaticPaths_(std::string handle) { ASSERT_MAIN_THREAD() + return get_pWebApplication(handle)->getStaticPathManager().pathsAsRObject(); +} - if (!io_thread_running.get()) - return; - - // Each call to stopServer also removes it from the pServers list. - while (pServers.size() > 0) { - stopServer(pServers[0]); - } +// [[Rcpp::export]] +Rcpp::List setStaticPaths_(std::string handle, Rcpp::List sp) { + ASSERT_MAIN_THREAD() + get_pWebApplication(handle)->getStaticPathManager().set(sp); + return getStaticPaths_(handle); +} - uv_async_send(&async_stop_io_loop); +// [[Rcpp::export]] +Rcpp::List removeStaticPaths_(std::string handle, Rcpp::CharacterVector paths) { + ASSERT_MAIN_THREAD() + get_pWebApplication(handle)->getStaticPathManager().remove(paths); + return getStaticPaths_(handle); +} - trace("io_thread stopped"); - uv_thread_join(&io_thread_id); +// [[Rcpp::export]] +Rcpp::List getStaticPathOptions_(std::string handle) { + ASSERT_MAIN_THREAD() + return get_pWebApplication(handle)->getStaticPathManager().getOptions().asRObject(); } -void stop_loop_timer_cb(uv_timer_t* handle) { - uv_stop(handle->loop); + +// [[Rcpp::export]] +Rcpp::List setStaticPathOptions_(std::string handle, Rcpp::List opts) { + ASSERT_MAIN_THREAD() + get_pWebApplication(handle)->getStaticPathManager().setOptions(opts); + return getStaticPathOptions_(handle); } 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/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/staticpath.cpp b/src/staticpath.cpp new file mode 100644 index 00000000..77277b4d --- /dev/null +++ b/src/staticpath.cpp @@ -0,0 +1,384 @@ +#include "staticpath.h" +#include "thread.h" +#include "utils.h" +#include "constants.h" +#include +#include + +// ============================================================================ +// StaticPathOptions +// ============================================================================ + +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() + + std::string obj_class = options.attr("class"); + if (obj_class != "staticPathOptions") { + throw Rcpp::exception("staticPath options object must have class 'staticPathOptions'."); + } + + // 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["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; + 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); + } + } + if (options.containsElementNamed("html_charset")) { + temp = options["html_charset"]; + if (!temp.isNULL()) { + 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()) { + validation = 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), + _["html_charset"] = optional_wrap(html_charset), + _["headers"] = optional_wrap(headers), + _["validation"] = optional_wrap(validation) + ); + + obj.attr("class") = "staticPathOptions"; + + return obj; +} + +// Merge StaticPathOptions object `a` with `b`. Values in `a` take precedence. +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; + 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; +} + +// Check if a set of request headers satisfies the condition specified by +// `validation`. +bool StaticPathOptions::validateRequestHeaders(const RequestHeaders& headers) const { + if (validation == boost::none) { + 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] != "==") { + 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]) { + return true; + } + + return false; +} + + +// ============================================================================ +// StaticPath +// ============================================================================ + +StaticPath::StaticPath(const Rcpp::List& sp) { + ASSERT_MAIN_THREAD() + 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."); + } +} + +Rcpp::List StaticPath::asRObject() const { + ASSERT_MAIN_THREAD() + using namespace Rcpp; + + List obj = List::create( + _["path"] = path, + _["options"] = options.asRObject() + ); + + obj.attr("class") = "staticPath"; + + return obj; +} + + +// ============================================================================ +// StaticPathManager +// ============================================================================ +StaticPathManager::StaticPathManager() { + uv_mutex_init(&mutex); +} + +StaticPathManager::StaticPathManager(const Rcpp::List& path_list, const Rcpp::List& options_list) { + ASSERT_MAIN_THREAD() + uv_mutex_init(&mutex); + + this->options = StaticPathOptions(options_list); + + if (path_list.size() == 0) { + return; + } + + Rcpp::CharacterVector names = path_list.names(); + if (names.isNULL()) { + 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(path_list[i]); + StaticPath staticpath(sp); + + this->path_map.insert( + std::pair(name, staticpath) + ); + } +} + + +// Returns a StaticPath object, which has its options merged with the overall ones. +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()) { + return boost::none; + } + + // 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 StaticPathManager::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 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); + if (it != path_map.end()) { + it->second = sp; + } + + // Otherwise, insert the pair. + path_map.insert( + std::pair(path, sp) + ); +} + +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 StaticPathManager::set(const Rcpp::List& pmap) { + ASSERT_MAIN_THREAD() + std::map pmap2 = toMap(pmap); + set(pmap2); +} + + +void StaticPathManager::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 StaticPathManager::remove(const std::vector& paths) { + std::vector::const_iterator it; + for (it = paths.begin(); it != paths.end(); it++) { + remove(*it); + } +} + +void StaticPathManager::remove(const Rcpp::CharacterVector& paths) { + ASSERT_MAIN_THREAD() + std::vector paths_vec = Rcpp::as>(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> StaticPathManager::matchStaticPath( + const std::string& url_path) const +{ + + if (url_path.empty()) { + return boost::none; + } + + std::string path = url_path; + + 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 = ""; + + 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 + // 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 a staticPath. + 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); + } +} + +const StaticPathOptions& StaticPathManager::getOptions() const { + return options; +}; + +void StaticPathManager::setOptions(const Rcpp::List& opts) { + options.setOptions(opts); +}; + +// 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; + + 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/staticpath.h b/src/staticpath.h new file mode 100644 index 00000000..73e469da --- /dev/null +++ b/src/staticpath.h @@ -0,0 +1,80 @@ +#ifndef STATICPATH_HPP +#define STATICPATH_HPP + +#include +#include +#include +#include +#include "thread.h" +#include "constants.h" + +class StaticPathOptions { +public: + 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); + + void setOptions(const Rcpp::List& options); + + Rcpp::List asRObject() const; + + static StaticPathOptions merge(const StaticPathOptions& a, const StaticPathOptions& b); + + bool validateRequestHeaders(const RequestHeaders& headers) const; +}; + + +class StaticPath { +public: + std::string path; + StaticPathOptions options; + + StaticPath(const Rcpp::List& sp); + + Rcpp::List asRObject() const; +}; + + +class StaticPathManager { + std::map path_map; + // Mutex is used whenever path_map is accessed. + mutable uv_mutex_t mutex; + + StaticPathOptions options; + +public: + 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; + + 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); + + boost::optional> matchStaticPath( + const std::string& url_path) const; + + + const StaticPathOptions& getOptions() const; + void setOptions(const Rcpp::List& opts); + + Rcpp::List pathsAsRObject() const; +}; + +#endif diff --git a/src/utils.h b/src/utils.h index eeeb8564..b0ad182e 100644 --- a/src/utils.h +++ b/src/utils.h @@ -5,8 +5,11 @@ #include #include #include +#include #include #include +#include +#include #include "thread.h" // A callback for deleting objects on the main thread using later(). This is @@ -78,4 +81,169 @@ 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(); +} + +// 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]); + 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) + ); + } + + 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) ); +} + +// 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) { + return R_NilValue; + } + return Rcpp::wrap(value.get()); +} + + +// 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 (unsigned int i=0; i +// ============================================================================ +// Utility functions +// ============================================================================ + std::string normalizeHeaderName(const std::string& name) { std::string result = name; for (std::string::iterator it = result.begin(); @@ -87,13 +94,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) @@ -103,6 +105,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. @@ -173,8 +189,13 @@ 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"])); + FileDataSourceResult ret = pFDS->initialize( + Rcpp::as(response["bodyFile"]), + Rcpp::as(response["bodyFileOwned"]) + ); + if (ret != FDS_OK) { + REprintf(pFDS->lastErrorMessage().c_str()); + } pDataSource = pFDS; } else if (Rf_isString(response["body"])) { @@ -212,6 +233,28 @@ void invokeResponseFun(boost::function)> fu } +// ============================================================================ +// Methods +// ============================================================================ + +RWebApplication::RWebApplication( + Rcpp::Function onHeaders, + Rcpp::Function onBodyData, + Rcpp::Function onRequest, + Rcpp::Function onWSOpen, + Rcpp::Function onWSMessage, + Rcpp::Function onWSClose, + Rcpp::List staticPaths, + Rcpp::List staticPathOptions) : + _onHeaders(onHeaders), _onBodyData(onBodyData), _onRequest(onRequest), + _onWSOpen(onWSOpen), _onWSMessage(onWSMessage), _onWSClose(onWSClose) +{ + ASSERT_MAIN_THREAD() + + _staticPathManager = StaticPathManager(staticPaths, staticPathOptions); +} + + void RWebApplication::onHeaders(boost::shared_ptr pRequest, boost::function)> callback) { @@ -364,3 +407,172 @@ 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. + +boost::shared_ptr error_response(boost::shared_ptr pRequest, int code) { + std::string description = getStatusDescription(code); + std::string content = toString(code) + " " + description + "\n"; + + std::vector responseData(content.begin(), content.end()); + + // Freed in on_response_written + DataSource* pDataSource = new InMemoryDataSource(responseData); + + return boost::shared_ptr( + new HttpResponse(pRequest, code, description, pDataSource), + auto_deleter_background + ); +} + + +boost::shared_ptr RWebApplication::staticFileResponse( + boost::shared_ptr pRequest +) { + 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 + // StaticPathManager. + if (pRequest->hasHeader("Connection", "Upgrade", true)) { + return nullptr; + } + + // Strip off query string + std::pair url_query = splitQueryString(pRequest->url()); + std::string url_path = doDecodeURI(url_query.first, true); + + boost::optional> sp_pair = + _staticPathManager.matchStaticPath(url_path); + + if (!sp_pair) { + // 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") { + 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); + } + + // 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 + std::string local_path = sp.path; + if (subpath != "") { + local_path += "/" + subpath; + } + + if (is_directory(local_path)) { + if (sp.options.indexhtml.get()) { + local_path = local_path + "/" + "index.html"; + } + } + + // Self-frees when response is written + FileDataSource* pDataSource = new FileDataSource(); + FileDataSourceResult ret = pDataSource->initialize(local_path, false); + + if (ret != FDS_OK) { + // Couldn't read the file + delete pDataSource; + + 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, 500); + } + } + + int file_size = pDataSource->size(); + + // Use local_path instead of subpath, because if the subpath is "/foo/" and + // *(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"; + } else if (content_type == "text/html") { + // Add the encoding if specified by the options. + if (sp.options.html_charset.get() != "") { + content_type = "text/html; charset=" + sp.options.html_charset.get(); + } + } + + 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 + ); + + ResponseHeaders& respHeaders = pResponse->headers(); + + // Add any extra headers + const 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 + // 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)); + + return pResponse; +} + +StaticPathManager& RWebApplication::getStaticPathManager() { + return _staticPathManager; +} diff --git a/src/webapplication.h b/src/webapplication.h index b45b0bc4..bc236465 100644 --- a/src/webapplication.h +++ b/src/webapplication.h @@ -6,6 +6,7 @@ #include #include "websockets.h" #include "thread.h" +#include "staticpath.h" class HttpRequest; class HttpResponse; @@ -27,6 +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; + virtual StaticPathManager& getStaticPathManager() = 0; }; @@ -39,17 +44,17 @@ class RWebApplication : public WebApplication { Rcpp::Function _onWSMessage; Rcpp::Function _onWSClose; + StaticPathManager _staticPathManager; + public: RWebApplication(Rcpp::Function onHeaders, Rcpp::Function onBodyData, 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::List staticPaths, + Rcpp::List staticPathOptions); virtual ~RWebApplication() { ASSERT_MAIN_THREAD() @@ -69,6 +74,10 @@ class RWebApplication : public WebApplication { boost::shared_ptr > data, boost::function error_callback); virtual void onWSClose(boost::shared_ptr conn); + + virtual boost::shared_ptr staticFileResponse( + boost::shared_ptr pRequest); + virtual StaticPathManager& getStaticPathManager(); }; 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/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-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..4060f945 --- /dev/null +++ b/tests/testthat/test-static-paths.R @@ -0,0 +1,543 @@ +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 = "", + 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) + # 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)) + + + 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 ..", { + 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()) + + # 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))) + + 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..b52eed88 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()) @@ -63,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 ) } @@ -263,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() +}) 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)