mirror of
https://github.com/rstudio/shiny.git
synced 2026-02-06 20:55:24 -05:00
270 lines
7.2 KiB
R
270 lines
7.2 KiB
R
#' @include globals.R
|
|
NULL
|
|
|
|
reactLogHandler <- function(req) {
|
|
if (!identical(req$PATH_INFO, '/reactlog'))
|
|
return(NULL)
|
|
|
|
if (!isTRUE(getOption('shiny.reactlog'))) {
|
|
return(NULL)
|
|
}
|
|
|
|
sessionToken <- parseQueryString(req$QUERY_STRING)$s
|
|
|
|
return(httpResponse(
|
|
status=200,
|
|
content=list(file=renderReactLog(sessionToken), owned=TRUE)
|
|
))
|
|
}
|
|
|
|
sessionHandler <- function(req) {
|
|
path <- req$PATH_INFO
|
|
if (is.null(path))
|
|
return(NULL)
|
|
|
|
matches <- regmatches(path, regexec('^(/session/([0-9a-f]+))(/.*)$', path))
|
|
if (length(matches[[1]]) == 0)
|
|
return(NULL)
|
|
|
|
session <- matches[[1]][3]
|
|
subpath <- matches[[1]][4]
|
|
|
|
shinysession <- appsByToken$get(session)
|
|
if (is.null(shinysession))
|
|
return(NULL)
|
|
|
|
subreq <- as.environment(as.list(req, all.names=TRUE))
|
|
subreq$PATH_INFO <- subpath
|
|
subreq$SCRIPT_NAME <- paste(subreq$SCRIPT_NAME, matches[[1]][2], sep='')
|
|
|
|
withReactiveDomain(shinysession, {
|
|
shinysession$handleRequest(subreq)
|
|
})
|
|
}
|
|
|
|
apiHandler <- function(serverFuncSource) {
|
|
function(req) {
|
|
path <- req$PATH_INFO
|
|
if (is.null(path))
|
|
return(NULL)
|
|
|
|
matches <- regmatches(path, regexec('^/api/(.*)$', path))
|
|
if (length(matches[[1]]) == 0)
|
|
return(NULL)
|
|
|
|
apiName <- matches[[1]][2]
|
|
|
|
sharedSecret <- getOption('shiny.sharedSecret')
|
|
if (!is.null(sharedSecret)
|
|
&& !identical(sharedSecret, req$HTTP_SHINY_SHARED_SECRET)) {
|
|
stop("Incorrect shared secret")
|
|
}
|
|
|
|
if (!is.null(getOption("shiny.observer.error", NULL))) {
|
|
warning(
|
|
call. = FALSE,
|
|
"options(shiny.observer.error) is no longer supported; please unset it!"
|
|
)
|
|
stopApp()
|
|
}
|
|
|
|
# need to give a fake websocket to the session
|
|
ws <- list(
|
|
request = req,
|
|
sendMessage = function(...) {
|
|
#print(list(...))
|
|
}
|
|
)
|
|
|
|
# Accept JSON query string and/or JSON body as input values
|
|
inputVals <- c(
|
|
parseQueryStringJSON(req$QUERY_STRING),
|
|
parseJSONBody(req)
|
|
)
|
|
|
|
shinysession <- ShinySession$new(ws)
|
|
on.exit({
|
|
try({
|
|
# Clean up the session. Very important, so that observers
|
|
# and such don't hang around, and to let memory get gc'd.
|
|
shinysession$wsClosed()
|
|
appsByToken$remove(shinysession$token)
|
|
})
|
|
}, add = TRUE)
|
|
appsByToken$set(shinysession$token, shinysession)
|
|
shinysession$setShowcase(.globals$showcaseDefault)
|
|
|
|
serverFunc <- withReactiveDomain(NULL, serverFuncSource())
|
|
|
|
tryCatch({
|
|
withReactiveDomain(shinysession, {
|
|
shinysession$manageInputs(inputVals)
|
|
do.call(serverFunc, argsForServerFunc(serverFunc, shinysession))
|
|
result <- NULL
|
|
shinysession$enableApi(apiName, function(value) {
|
|
result <<- try(withLogErrors(value), silent = TRUE)
|
|
})
|
|
flushReact()
|
|
resultToResponse(result)
|
|
})
|
|
}, error = function(e) {
|
|
return(httpResponse(
|
|
status=500,
|
|
content=htmlEscape(conditionMessage(e))
|
|
))
|
|
})
|
|
}
|
|
}
|
|
|
|
apiWsHandler <- function(serverFuncSource) {
|
|
function(ws) {
|
|
path <- ws$request$PATH_INFO
|
|
if (is.null(path))
|
|
return(NULL)
|
|
|
|
matches <- regmatches(path, regexec('^/api/(.*)$', path))
|
|
if (length(matches[[1]]) == 0)
|
|
return(NULL)
|
|
|
|
apiName <- matches[[1]][2]
|
|
|
|
sharedSecret <- getOption('shiny.sharedSecret')
|
|
if (!is.null(sharedSecret)
|
|
&& !identical(sharedSecret, ws$request$HTTP_SHINY_SHARED_SECRET)) {
|
|
ws$close()
|
|
return(TRUE)
|
|
}
|
|
|
|
if (!is.null(getOption("shiny.observer.error", NULL))) {
|
|
warning(
|
|
call. = FALSE,
|
|
"options(shiny.observer.error) is no longer supported; please unset it!"
|
|
)
|
|
stopApp()
|
|
}
|
|
|
|
inputVals <- parseQueryStringJSON(ws$request$QUERY_STRING)
|
|
|
|
# Give a fake websocket to suppress messages from session
|
|
shinysession <- ShinySession$new(list(
|
|
request = ws$request,
|
|
sendMessage = function(...) {
|
|
#print(list(...))
|
|
}
|
|
))
|
|
appsByToken$set(shinysession$token, shinysession)
|
|
shinysession$setShowcase(.globals$showcaseDefault)
|
|
|
|
serverFunc <- withReactiveDomain(NULL, serverFuncSource())
|
|
|
|
tryCatch({
|
|
withReactiveDomain(shinysession, {
|
|
shinysession$manageInputs(inputVals)
|
|
do.call(serverFunc, argsForServerFunc(serverFunc, shinysession))
|
|
shinysession$enableApi(apiName, function(value) {
|
|
resp <- resultToResponse(value)
|
|
if (resp$status != 200L) {
|
|
warning("Error: ", responseToContent(resp))
|
|
ws$close()
|
|
} else {
|
|
content <- responseToContent(resp)
|
|
if (grepl("^image/", resp$content_type)) {
|
|
content <- paste0("data:", resp$content_type, ";base64,",
|
|
httpuv::rawToBase64(content))
|
|
}
|
|
try(ws$send(content), silent=TRUE)
|
|
}
|
|
})
|
|
flushReact()
|
|
})
|
|
}, error = function(e) {
|
|
ws$close()
|
|
})
|
|
|
|
ws$onClose(function() {
|
|
# Clean up the session. Very important, so that observers
|
|
# and such don't hang around, and to let memory get gc'd.
|
|
shinysession$wsClosed()
|
|
appsByToken$remove(shinysession$token)
|
|
})
|
|
|
|
# TODO: What to do on ws$onMessage?
|
|
}
|
|
}
|
|
|
|
parseJSONBody <- function(req) {
|
|
if (identical(req[["REQUEST_METHOD"]], "POST")) {
|
|
if (isTRUE(grepl(perl=TRUE, "^(text|application)/json(;\\s*charset\\s*=\\s*utf-8)?$", req[["HTTP_CONTENT_TYPE"]]))) {
|
|
tmp <- file("", "w+b")
|
|
on.exit(close(tmp))
|
|
|
|
input_file <- req[["rook.input"]]
|
|
while (TRUE) {
|
|
chunk <- input_file$read(8192L)
|
|
if (length(chunk) == 0)
|
|
break
|
|
writeBin(chunk, tmp)
|
|
}
|
|
|
|
return(jsonlite::fromJSON(tmp))
|
|
}
|
|
|
|
if (is.null(req[["HTTP_CONTENT_TYPE"]])) {
|
|
if (!is.null(req[["rook.input"]]) && length(req[["rook.input"]]$read(1L)) > 0) {
|
|
stop("Invalid POST request (body provided without content type)")
|
|
}
|
|
return()
|
|
}
|
|
|
|
stop("Invalid POST request (content type not supported)")
|
|
}
|
|
}
|
|
|
|
resultToResponse <- function(result) {
|
|
if (inherits(result, "httpResponse")) {
|
|
return(result)
|
|
} else if (inherits(result, "try-error")) {
|
|
return(httpResponse(
|
|
status=500,
|
|
content_type="text/plain",
|
|
content=conditionMessage(attr(result, "condition"))
|
|
))
|
|
} else if (!is.null(attr(result, "content.type"))) {
|
|
return(httpResponse(
|
|
status=200L,
|
|
content_type=attr(result, "content.type"),
|
|
content=result
|
|
))
|
|
} else {
|
|
return(httpResponse(
|
|
status=200L,
|
|
content_type="application/json",
|
|
content=toJSON(result, pretty=TRUE)
|
|
))
|
|
}
|
|
}
|
|
|
|
responseToContent <- function(result) {
|
|
ct <- result$content_type
|
|
textMode <- grepl("^text/", ct) || ct == "application/json" ||
|
|
grepl("^application/xml($|\\+)", ct)
|
|
|
|
# TODO: Make sure text is UTF-8
|
|
|
|
if ("file" %in% names(result$content)) {
|
|
filename <- result$content$file
|
|
if ("owned" %in% names(result$content) && result$content$owned) {
|
|
on.exit(unlink(filename), add = TRUE)
|
|
}
|
|
if (textMode)
|
|
return(paste(readLines(filename), collapse = "\n"))
|
|
else
|
|
return(readBin(filename, raw(), file.info(filename)$size))
|
|
} else {
|
|
if (textMode)
|
|
return(paste(result$content, collapse = "\n"))
|
|
else
|
|
return(result$content)
|
|
}
|
|
}
|