# This file contains a general toolkit for routing and combining bits of # HTTP-handling logic. It is similar in spirit to Rook (and Rack, and WSGI, and # Connect, and...) but adds cascading and routing. # # This file is called "middleware" because that's the term used for these bits # of logic in these other frameworks. However, our code uses the word "handler" # so we'll stick to that for the rest of this document; just know that they're # basically the same concept. # # ## Intro to handlers # # A **handler** (or sometimes, **httpHandler**) is a function that takes a # `req` parameter--a request object as described in the Rook specification--and # returns `NULL`, or an `httpResponse`. # ## ------------------------------------------------------------------------ httpResponse <- function(status = 200, content_type = "text/html; charset=UTF-8", content = "", headers = list()) { # Make sure it's a list, not a vector headers <- as.list(headers) if (is.null(headers$`X-UA-Compatible`)) headers$`X-UA-Compatible` <- "IE=edge,chrome=1" resp <- list(status = status, content_type = content_type, content = content, headers = headers) class(resp) <- 'httpResponse' return(resp) } # # You can think of a web application as being simply an aggregation of these # functions, each of which performs one kind of duty. Each handler in turn gets # a look at the request and can decide whether it knows how to handle it. If # so, it returns an `httpResponse` and processing terminates; if not, it # returns `NULL` and the next handler gets to execute. If the final handler # returns `NULL`, a 404 response should be returned. # # We have a similar construct for websockets: **websocket handlers** or # **wsHandlers**. These take a single `ws` argument which is the websocket # connection that was just opened, and they can either return `TRUE` if they # are handling the connection, and `NULL` to pass responsibility on to the next # wsHandler. # # ### Combining handlers # # Since it's so common for httpHandlers to be invoked in this "cascading" # fashion, we'll introduce a function that takes zero or more handlers and # returns a single handler. And while we're at it, making a directory of static # content available is such a common thing to do, we'll allow strings # representing paths to be used instead of handlers; any such strings we # encounter will be converted into `staticHandler` objects. # ## ------------------------------------------------------------------------ joinHandlers <- function(handlers) { # Zero handlers; return a null handler if (length(handlers) == 0) return(function(req) NULL) # Just one handler (function)? Return it. if (is.function(handlers)) return(handlers) handlers <- lapply(handlers, function(h) { if (is.character(h)) return(staticHandler(h)) else return(h) }) # Filter out NULL handlers <- handlers[!sapply(handlers, is.null)] if (length(handlers) == 0) return(function(req) NULL) if (length(handlers) == 1) return(handlers[[1]]) function(req) { for (handler in handlers) { response <- handler(req) if (!is.null(response)) return(response) } return(NULL) } } # # Note that we don't have an equivalent of `joinHandlers` for wsHandlers. It's # easy to imagine it, we just haven't needed one. # # ### Handler routing # # Handlers do not have a built-in notion of routing. Conceptually, given a list # of handlers, all the handlers are peers and they all get to see every request # (well, up until the point that a handler returns a response). # # You could implement routing in each handler by checking the request's # `PATH_INFO` field, but since it's such a common need, let's make it simple by # introducing a `routeHandler` function. This is a handler # [decorator](http://en.wikipedia.org/wiki/Decorator_pattern) and it's # responsible for 1) filtering out requests that don't match the given route, # and 2) temporarily modifying the request object to take the matched part of # the route off of the `PATH_INFO` (and add it to the end of `SCRIPT_NAME`). # This way, the handler doesn't need to figure out about what part of its URL # path has already been matched via routing. # # (BTW, it's safe for `routeHandler` calls to nest.) # ## ------------------------------------------------------------------------ routeHandler <- function(prefix, handler) { force(prefix) force(handler) if (identical("", prefix)) return(handler) if (length(prefix) != 1 || !isTRUE(grepl("^/[^\\]+$", prefix))) { stop("Invalid URL prefix \"", prefix, "\"") } pathPattern <- paste("^\\Q", prefix, "\\E/", sep = "") function(req) { if (isTRUE(grepl(pathPattern, req$PATH_INFO))) { origScript <- req$SCRIPT_NAME origPath <- req$PATH_INFO on.exit({ req$SCRIPT_NAME <- origScript req$PATH_INFO <- origPath }, add = TRUE) pathInfo <- substr(req$PATH_INFO, nchar(prefix)+1, nchar(req$PATH_INFO)) req$SCRIPT_NAME <- paste(req$SCRIPT_NAME, prefix, sep = "") req$PATH_INFO <- pathInfo return(handler(req)) } else { return(NULL) } } } # # We have a version for websocket handlers as well. Pity about the copy/paste # job. # ## ------------------------------------------------------------------------ routeWSHandler <- function(prefix, wshandler) { force(prefix) force(wshandler) if (identical("", prefix)) return(wshandler) if (length(prefix) != 1 || !isTRUE(grepl("^/[^\\]+$", prefix))) { stop("Invalid URL prefix \"", prefix, "\"") } pathPattern <- paste("^\\Q", prefix, "\\E/", sep = "") function(ws) { req <- ws$request if (isTRUE(grepl(pathPattern, req$PATH_INFO))) { origScript <- req$SCRIPT_NAME origPath <- req$PATH_INFO on.exit({ req$SCRIPT_NAME <- origScript req$PATH_INFO <- origPath }, add = TRUE) pathInfo <- substr(req$PATH_INFO, nchar(prefix)+1, nchar(req$PATH_INFO)) req$SCRIPT_NAME <- paste(req$SCRIPT_NAME, prefix, sep = "") req$PATH_INFO <- pathInfo return(wshandler(ws)) } else { return(NULL) } } } # # ### Handler implementations # # Now let's actually write some handlers. Note that these functions aren't # *themselves* handlers, you call them and they *return* a handler. Handler # factory functions, if you will. # # Here's one that serves up static assets from a directory. # ## ------------------------------------------------------------------------ staticHandler <- function(root) { force(root) return(function(req) { if (!identical(req$REQUEST_METHOD, 'GET')) return(NULL) path <- req$PATH_INFO if (is.null(path)) return(httpResponse(400, content="
Shared secret mismatch
', headers=list('Content-Type' = 'text/html'))) } # Catch HEAD requests. For the purposes of handler functions, they # should be treated like GET. The difference is that they shouldn't # return a body in the http response. head_request <- FALSE if (identical(req$REQUEST_METHOD, "HEAD")) { head_request <- TRUE req$REQUEST_METHOD <- "GET" } response <- handler(req) if (is.null(response)) response <- httpResponse(404, content="