civetwebR

An embedded, high-performance HTTP server for R based on the CivetWeb C library.

Key Features

WebSockets

civetwebR makes it easy to build stateful, reactive applications:

on_ws("connect", function(req) {
  # before the handshake: TRUE accepts, FALSE refuses with 403,
  # list(status = 401L) refuses with that status
  identical(req_header(req, "Origin"), "https://app.example")
})

on_ws("open", function(req) {
  message(sprintf("Client %s connected from %s", req$id, req$remote_addr))
})

on_ws("message", function(req) {
  msg <- rawToChar(req$body)
  ws_send(req$id, paste("Echo:", msg))
})

on_ws("close", function(req) {
  message(sprintf("Client %s disconnected", req$id))
})

Installation

# remotes::install_github("cornball-ai/civetwebR")
install.packages("civetwebR", repos = c('https://cornball-ai.r-universe.dev', 'https://cloud.r-project.org'))

Usage

library(civetwebR)

# Start a server on port 8080
serve(port = 8080, host = "127.0.0.1")

Handlers

handle("GET", "/path", function(req) "ok")

Static Files

Serve files directly from disk:

serve(
  port = 8080,
  static = list(
    list(dir = "public", prefix = "/")
  )
)

Routing Groups

Group routes under a common prefix:

group("/api", {
  handle("GET", "/hello", function(req) "hi")
})

Request

req <- list(
  method = "...",
  path   = "..."
)

Response

Character:

"ok"

→ 200 text/plain

List:

list(
  status = 200L,
  headers = list(),
  body = "data"
)

Event API

serve() and run_server() are loops over a lower-level driver that an application with its own event loop can call directly. One call to next_event() returns one event, or NULL after the timeout:

srv <- start_server(port = 0L, register = FALSE,
                    static_dirs = c("/assets/" = "www"))
server_port(srv)   # the port the OS chose

repeat {
  ev <- next_event(100L, srv)
  if (is.null(ev)) next
  switch(ev$event,
    request    = send_response(ev$id, list(status = 200L, body = "ok"), srv),
    ws_connect = send_response(ev$id, TRUE, srv),
    ws_open    = message("open ", ev$id, " from ", ev$remote_addr),
    ws_message = ws_send(ev$id, ev$body, srv),
    ws_close   = message("closed ", ev$id)
  )
}

Notes

TLS

Pass tls_cert, a PEM file holding the certificate (and its chain) followed by the private key, and the port speaks https and wss through the bundled Mbed TLS:

serve(port = 8443, host = "0.0.0.0", tls_cert = "server.pem")
srv <- start_server(port = 8443, tls_cert = "server.pem")

A self-signed certificate for local use:

openssl req -x509 -newkey rsa:2048 -nodes -days 365 -subj /CN=localhost \
  -keyout key.pem -out crt.pem && cat crt.pem key.pem > server.pem

has_tls() reports whether TLS was compiled in.

Vendored CivetWeb and Mbed TLS

src/civetweb.c and src/civetweb/*.inl are CivetWeb at the commit in src/civetweb/COMMIT, with the patches in tools/patches/ applied:

src/civetweb/external_mg_cry_internal_impl.inl replaces CivetWeb's error logging, which would otherwise write to stderr or a log file. tools/vendor-civetweb.sh [<sha>] re-fetches CivetWeb and reapplies the patches.

src/mbedtls/ is Mbed TLS at the release in src/mbedtls/VERSION, fetched and checksum-verified by tools/vendor-mbedtls.sh [<version>], which then applies tools/patches/mbedtls/*.patch (the same pragma removal). src/civetweb/civetwebr_mbedtls_config.h holds this package's few changes to its default configuration. Copyright notices for both libraries are in inst/COPYRIGHTS.

Checks

CI runs R CMD check on Linux, macOS and Windows and, in the r-hub containers that mirror CRAN's additional checks, under clang's and gcc's AddressSanitizer and UndefinedBehaviorSanitizer and under valgrind. tools/sanitize.sh runs the same two memory checks locally over the whole test suite:

tools/sanitize.sh

License

MIT