-
Notifications
You must be signed in to change notification settings - Fork 65
OCAP mode
Modern versions of Rserve support Object-Capability (OCAP) mode which is far more safe and versatile than the eval-based protocol. It prevents arbitrary code execution and thus provides a way to ensure security guarantees for applications.
OCAPs can be seen as R functions that have been registered by the server to be made available to the client. In OCAP mode the only supported Rserve message command is CMD_OCcall
which is a call to a previously exposed OCAP.
When a connection is initiated, Rserve will evaluate oc.init()
and return the result to the client. The purpose of the function is to create any OCAPs that should be available and return them to the client. There is no required structure, but often by convention the function returns a named list of OCAPs to expose - or just a single OCAP.
Note that OCAPs can be created by any R code at any point, so common approach (e.g. as used by RCloud) is for oc.init()
to expose a single function such as authenticate(user, password)
which when called then registers and returns new OCAPs depending on the permissions that the application wishes to grant to the client.
The payload to CMD_OCcall
is a single REXP
which is a call with the name of the function being the OCAP reference. In the server the reference is replaced by the function definition and the call is evaluated. Therefore from R perspective OCAP calls are simply regular function calls. The client is expected to create the valid call (see rserve.js for a full JavaScript/R RPC OCAP support).
An OCAP is created from an R function using the Rserve::ocap
function.
A simple (toy) example replicating the parse/eval functionality of "classic" Rserve using OCAP with authentication:
library(Rserve)
parseEval <- function(s) {
tryCatch(eval(parse(text=s), .GlobalEnv),
error=function(e) e)
}
auth <- function(user, pass) {
if (user == "mike" && pass == "mypwd")
list(parseEval=ocap(parseEval))
else
"sorry, unauthorized!"
}
oc.init <- function() ocap(auth)
run.Rserve(qap.oc=TRUE)
Example with RSclient:
> library(RSclient)
> wrap = function(ocap) function(...) RS.eval.qap(c, as.call(list(ocap, ...)))
> c = RS.connect()
> auth = wrap(attr(c,"capabilities"))
> auth("foo", "bar")
[1] "sorry, unauthorized!"
> (cap = auth("mike", "mypwd"))
$parseEval
[1] "Ixhg_wNLAYdlKz1BqG5WTLxRMU9u"
attr(,"class")
[1] "OCref"
> f = lapply(cap, wrap)
> f$parseEval("R.version.string")
[1] "R version 4.1.0 (2021-05-18)"
> f$parseEval("ls()")
[1] "auth" "oc.init" "parseEval"
In real applications the main point is to NOT expose eval
but instead only expose functions that perform the actions desired.
The OCAP mode is enabled using qap.oc
(alias reserve.oc
) or websockets.qap.oc
configuration options (the latter tunnels QAP protocol through the WebSockets protocol).
IMPORTANT: The OCAP mode provides security at the R level, however, communication-channel attacks are a separate matter. You should always use TLS for communication to address those concerns and well as privacy of the communication. Otherwise you may need to worry about issues such as replay attacks and provide a nonce with the authentication OCAP and require payload hashes or signing as part of your authentication.
NOTE: not all clients support the OCAP mode and/or OOB messages. The most complete client which is used in production with OCAP mode and all its features is the JavaScript rserve-js client. Many of the features were driven by the needs of RCloud. The Java client (REngine) has been updated to support OCAP mode as well.
Besides being very secure, Rserve in OCAP mode has several interesting new features as well:
If oob enable
configuration is set then out-of-band (OOB) message support is enabled. This enables OCAPs (or R itself - see below) to send additional messages to the client during the evaluation of OCAPs. Those messages are considered OOB because the QAP protocol is a strict request-response protocol so a request is expected to be followed by a response, but OOB messages are not responses and thus arrive before the actual response - hence out-of-band.
The most simple way to send OOB messages is the self.oobSend()
function (see Rserve documentation) which is one-way communication from the server to the client.
In addition to a purely informative OOB "send" messages as above, OCAPs can also request OOB messages to provide response from the client to the server using the self.oobMessage()
function. This function does not return until the client provides the answer. Note that it is still legal for the client to call another OCAP before a response to the message is sent in which case a new nested evaluation occurs (see nesting below)).
If console.oob
configuration is enabled then R console API results in following OOB send messages:
-
"console.out"
: regular output -
"console.err"
: error output -
"console.reset"
: console was re-set (nopayload
) -
"console.msg"
: message output
All messages are send OOB messages (no reply expected) of the form list(stream, payload)
or list(stream, context, payload)
depending whether contexts are enabled (see below). stream
is one of the "console..."
strings above and the payload
is a string (except for "console.reset"
which has no payload).
In addition, if console.input
configuration is enabled as well then the ReadConsole
callback (e.g. via readLines()
) results in OOB message "console.in"
requesting the input from the client.
If forward.stdio enable
is set then Rserve uses OOB send messages to relay content appearing on stdout
and stderr
streams of the process. Note that this is different from the regular R console. As above the messages are of the form list(stream, payload)
or list(stream, context, payload)
depending whether contexts are enabled (see below) where stream
is either "stdout"
or "stderr"
and the payload
is a string.
Some applications may want to maintain multiple output contexts. If io.use.context
configuration option is enabled then console and stdio forwarding messages pass-through an additional argument that is controlled by the application that we call the "context". It is an arbitrary R object (typically a string or integer) that is set using the Rserve.context()
function in the OCAP and thus allows to identify the source of particular output. In addition, the Rserve.eval function can set the context just for the duration of the evaluation.
OCAP calls can be nested as follows: If an OCAP issues an OOB message then it is legal for another OCAP function to be called by the client while the first OCAP waits for the message. The second OCAP must then respond first in a stack-like fashion.
If use.idle.callback
configuration option is enabled and a function .ocap.idle
exists in the global environment then this function will be called periodically when Rserve OCAP connection is idle (i.e. if no connection nor input is active for more than 200ms).
When implementing R GUIs it is often desirable to retain responsiveness of the GUI even if the R compute process is busy. Since R does not allow multi-threading this requires special handling. Rserve enables this by providing a separation of a compute and control processes while utilising one communication channel. The API to spawn a new subprocess (dubbed the "compute" process) has only been defined at C level, but works as follows:
res <- .Call(Rserve:::Rserve_fork_compute, expression)
will fork a new subprocess, evaluate expression
in that process and return the result. The expression should be seen as a sort of oc.init()
for the process as its main purpose is to register new OCAPs that are specific to the compute process. OCAPs defined in each process are specific to that process and that is what allows Rserve to run each OCAP in the correct process even though all OCAPs are sent through the same channel. From the client's point of view there is still only one connection, Rserve multiplexes messages to the corresponding process automatically depending on the process that registered the OCAP.
Hint: bquote()
can be used as the expression
to allow the use of dotted symbols to pass information from the control process to the child compute process. It is not strictly necessary since the one process is a fork of the other, but allows passing of non-global values.
PS: forking is only available on modern unix operating systems (Linux, macOS, ...), so unfortunately Windows does not support it (other than in WSL).