diff --git a/NAMESPACE b/NAMESPACE index fb1581e8e..bc14fb983 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -59,6 +59,7 @@ export(cv_reset) export(cv_signal) export(cv_value) export(dial) +export(echo_server) export(ip_addr) export(is_aio) export(is_error_value) diff --git a/R/server.R b/R/server.R new file mode 100644 index 000000000..02c84567b --- /dev/null +++ b/R/server.R @@ -0,0 +1,53 @@ +# nanonext - server - HTTP REST Server ----------------------------------------- + +#' Simple Async HTTP Echo Server +#' +#' Creates a simple HTTP echo server that runs entirely asynchronously and +#' echoes back all request information including method, headers, URI, and data. +#' +#' @param url full http address including hostname and port at which to host +#' the echo server. Default is "http://127.0.0.1:5556/echo". +#' +#' @return An external pointer to the server thread. The server runs asynchronously +#' in the background. The thread will be automatically destroyed when the +#' returned object is garbage collected. +#' +#' @details This server echoes back the complete HTTP request information as JSON, +#' including: +#' \itemize{ +#' \item HTTP method (GET, POST, etc.) +#' \item All request headers +#' \item Request URI/path +#' \item Request body data +#' \item Server timestamp +#' } +#' +#' The server accepts requests using any HTTP method and responds with +#' status 200 OK and Content-Type application/json. +#' +#' @examples +#' if (interactive()) { +#' +#' # Start echo server (returns immediately, server runs in background) +#' server_thread <- echo_server("http://127.0.0.1:5556/echo") +#' +#' # Test with GET request +#' ncurl("http://127.0.0.1:5556/echo?param=value") +#' +#' # Test with POST request including headers and data +#' ncurl("http://127.0.0.1:5556/echo", +#' method = "POST", +#' headers = c("Content-Type" = "application/json", "X-Custom" = "test"), +#' data = '{"message": "hello world"}') +#' +#' # Server will automatically stop when server_thread is garbage collected +#' # or you can explicitly remove it: +#' rm(server_thread) +#' gc() +#' +#' } +#' +#' @export +#' +echo_server <- function(url = "http://127.0.0.1:5556/echo") + .Call(rnng_echo_server, url) diff --git a/man/echo_server.Rd b/man/echo_server.Rd new file mode 100644 index 000000000..df7a048e2 --- /dev/null +++ b/man/echo_server.Rd @@ -0,0 +1,59 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/server.R +\name{echo_server} +\alias{echo_server} +\title{Simple Async HTTP Echo Server} +\usage{ +echo_server(url = "http://127.0.0.1:5556/echo") +} +\arguments{ +\item{url}{full http address including hostname and port at which to host +the echo server. Default is "http://127.0.0.1:5556/echo".} +} +\value{ +An external pointer to the server thread. The server runs asynchronously +in the background. The thread will be automatically destroyed when the +returned object is garbage collected. +} +\description{ +Creates a simple HTTP echo server that runs entirely asynchronously and +echoes back all request information including method, headers, URI, and data. +} +\details{ +This server echoes back the complete HTTP request information as JSON, +including: +\itemize{ +\item HTTP method (GET, POST, etc.) +\item All request headers +\item Request URI/path +\item Request body data +\item Server timestamp +} + +\if{html}{\out{
}}\preformatted{The server accepts requests using any HTTP method and responds with +status 200 OK and Content-Type application/json. +}\if{html}{\out{
}} +} +\examples{ +if (interactive()) { + +# Start echo server (returns immediately, server runs in background) +server_thread <- echo_server("http://127.0.0.1:5556/echo") + +# Test with GET request +ncurl("http://127.0.0.1:5556/echo?param=value") + +# Test with POST request including headers and data +ncurl("http://127.0.0.1:5556/echo", + method = "POST", + headers = c("Content-Type" = "application/json", "X-Custom" = "test"), + data = '{"message": "hello world"}') + +# Server will automatically stop when server_thread is garbage collected +# or you can explicitly remove it: +rm(server_thread) +gc() + +} + +} diff --git a/src/init.c b/src/init.c index 8e5caea28..789c8b438 100644 --- a/src/init.c +++ b/src/init.c @@ -124,6 +124,7 @@ static const R_CallMethodDef callMethods[] = { {"rnng_dial", (DL_FUNC) &rnng_dial, 5}, {"rnng_dialer_close", (DL_FUNC) &rnng_dialer_close, 1}, {"rnng_dialer_start", (DL_FUNC) &rnng_dialer_start, 2}, + {"rnng_echo_server", (DL_FUNC) &rnng_echo_server, 1}, {"rnng_eval_safe", (DL_FUNC) &rnng_eval_safe, 1}, {"rnng_fini", (DL_FUNC) &rnng_fini, 0}, {"rnng_fini_priors", (DL_FUNC) &rnng_fini_priors, 0}, diff --git a/src/nanonext.h b/src/nanonext.h index ab4d50237..87f7f433b 100644 --- a/src/nanonext.h +++ b/src/nanonext.h @@ -361,6 +361,7 @@ SEXP rnng_cv_wait_safe(SEXP); SEXP rnng_dial(SEXP, SEXP, SEXP, SEXP, SEXP); SEXP rnng_dialer_close(SEXP); SEXP rnng_dialer_start(SEXP, SEXP); +SEXP rnng_echo_server(SEXP); SEXP rnng_eval_safe(SEXP); SEXP rnng_fini(void); SEXP rnng_fini_priors(void); diff --git a/src/server.c b/src/server.c new file mode 100644 index 000000000..28cdbbe0b --- /dev/null +++ b/src/server.c @@ -0,0 +1,228 @@ +// nanonext - HTTP REST Sever -------------------------------------------------- + +#include +#define NANONEXT_HTTP +#define NANONEXT_IO +#include "nanonext.h" + +// Echo server ----------------------------------------------------------------- + +static void nano_printf(const int err, const char *fmt, ...) { + + char buf[NANONEXT_INIT_BUFSIZE]; + va_list arg_ptr; + + va_start(arg_ptr, fmt); + int bytes = vsnprintf(buf, NANONEXT_INIT_BUFSIZE, fmt, arg_ptr); + va_end(arg_ptr); + + if (write(err ? STDERR_FILENO : STDOUT_FILENO, buf, (size_t) bytes)) {}; + +} + +static void fatal(const char *reason, int xc) { + nano_printf(1, "%s: %s\n", reason, nng_strerror(xc)); +} + +void echo_handle(nng_aio *aio) { + + nng_http_req *req = nng_aio_get_input(aio, 0); + nng_http_res *res; + const char *method, *uri, *version; + void *data; + size_t sz; + char *response_json; + size_t response_len; + int xc; + + if ((xc = nng_http_res_alloc(&res))) { + nng_aio_finish(aio, xc); + return; + } + + // Get request information + method = nng_http_req_get_method(req); + uri = nng_http_req_get_uri(req); + version = nng_http_req_get_version(req); + nng_http_req_get_data(req, &data, &sz); + + // Start building JSON response + response_len = 4096; // Start with reasonable buffer size + response_json = malloc(response_len); + if (!response_json) { + nng_http_res_free(res); + nng_aio_finish(aio, NNG_ENOMEM); + return; + } + + // Build JSON response with escaped strings + int written = snprintf(response_json, response_len, + "{\n" + " \"method\": \"%s\",\n" + " \"uri\": \"%s\",\n" + " \"version\": \"%s\",\n" + " \"headers\": {\n", + method ? method : "UNKNOWN", + uri ? uri : "/", + version ? version : "HTTP/1.1" + ); + + // Add headers (simplified - would need more robust header enumeration) + const char *content_type = nng_http_req_get_header(req, "Content-Type"); + const char *user_agent = nng_http_req_get_header(req, "User-Agent"); + const char *host = nng_http_req_get_header(req, "Host"); + const char *authorization = nng_http_req_get_header(req, "Authorization"); + + if (content_type || user_agent || host || authorization) { + if (content_type) { + written += snprintf(response_json + written, response_len - written, + " \"Content-Type\": \"%s\"", content_type); + if (user_agent || host || authorization) written += snprintf(response_json + written, response_len - written, ",\n"); + else written += snprintf(response_json + written, response_len - written, "\n"); + } + if (user_agent) { + written += snprintf(response_json + written, response_len - written, + " \"User-Agent\": \"%s\"", user_agent); + if (host || authorization) written += snprintf(response_json + written, response_len - written, ",\n"); + else written += snprintf(response_json + written, response_len - written, "\n"); + } + if (host) { + written += snprintf(response_json + written, response_len - written, + " \"Host\": \"%s\"", host); + if (authorization) written += snprintf(response_json + written, response_len - written, ",\n"); + else written += snprintf(response_json + written, response_len - written, "\n"); + } + if (authorization) { + written += snprintf(response_json + written, response_len - written, + " \"Authorization\": \"%s\"\n", authorization); + } + } + + written += snprintf(response_json + written, response_len - written, + " },\n" + " \"data_size\": %zu,\n", + sz + ); + + // Add data if present (limit size for safety) + if (data && sz > 0) { + written += snprintf(response_json + written, response_len - written, + " \"data\": \""); + + // Add data content (escape and truncate if needed) + size_t data_limit = (sz < 1000) ? sz : 1000; // Limit to 1000 bytes + for (size_t i = 0; i < data_limit && written < response_len - 100; i++) { + char c = ((char*)data)[i]; + if (c == '"') { + written += snprintf(response_json + written, response_len - written, "\\\""); + } else if (c == '\\') { + written += snprintf(response_json + written, response_len - written, "\\\\"); + } else if (c == '\n') { + written += snprintf(response_json + written, response_len - written, "\\n"); + } else if (c == '\r') { + written += snprintf(response_json + written, response_len - written, "\\r"); + } else if (c == '\t') { + written += snprintf(response_json + written, response_len - written, "\\t"); + } else if (c >= 32 && c < 127) { + response_json[written++] = c; + } else { + written += snprintf(response_json + written, response_len - written, "\\u%04x", (unsigned char)c); + } + } + + if (sz > data_limit) { + written += snprintf(response_json + written, response_len - written, + "... (truncated, showing %zu of %zu bytes)", data_limit, sz); + } + + written += snprintf(response_json + written, response_len - written, "\",\n"); + } else { + written += snprintf(response_json + written, response_len - written, + " \"data\": null,\n"); + } + + // Add timestamp + time_t now; + time(&now); + written += snprintf(response_json + written, response_len - written, + " \"timestamp\": \"%s\",\n" + " \"server\": \"nanonext echo server\"\n" + "}", + ctime(&now) + ); + + // Remove newline from ctime + char *newline = strchr(response_json + written - 50, '\n'); + if (newline) *newline = ' '; + + // Set response headers + nng_http_res_set_status(res, NNG_HTTP_STATUS_OK); + nng_http_res_set_header(res, "Content-Type", "application/json"); + nng_http_res_set_header(res, "Server", "nanonext-echo/1.0"); + + // Set response body + if ((xc = nng_http_res_copy_data(res, response_json, strlen(response_json)))) { + free(response_json); + nng_http_res_free(res); + nng_aio_finish(aio, xc); + return; + } + + free(response_json); + nng_aio_set_output(aio, 0, res); + nng_aio_finish(aio, 0); +} + +void echo_start(void *arg) { + + const char *addr = (const char *) arg; + nng_http_server *server; + nng_http_handler *handler; + nng_url *url; + int xc; + + if ((xc = nng_url_parse(&url, addr))) + fatal("nng_url_parse", xc); + + if ((xc = nng_http_server_hold(&server, url))) + fatal("nng_http_server_hold", xc); + + if ((xc = nng_http_handler_alloc(&handler, url->u_path, echo_handle))) + fatal("nng_http_handler_alloc", xc); + + // Accept all HTTP methods + if ((xc = nng_http_handler_set_method(handler, NULL))) + fatal("nng_http_handler_set_method", xc); + + if ((xc = nng_http_handler_collect_body(handler, true, 1024 * 128))) + fatal("nng_http_handler_collect_body", xc); + + if ((xc = nng_http_server_add_handler(server, handler))) + fatal("nng_http_handler_add_handler", xc); + + if ((xc = nng_http_server_start(server))) + fatal("nng_http_server_start", xc); + + nng_url_free(url); +} + +static void echo_thread_finalizer(SEXP xptr) { + if (NANO_PTR(xptr) == NULL) return; + nng_thread *thr = (nng_thread *) NANO_PTR(xptr); + nng_thread_destroy(thr); +} + +SEXP rnng_echo_server(SEXP url) { + + const char *addr = CHAR(STRING_ELT(url, 0)); + nng_thread *thr; + int xc; + + if ((xc = nng_thread_create(&thr, echo_start, (void *) addr))) + ERROR_OUT(xc); + + SEXP xptr = R_MakeExternalPtr(thr, R_NilValue, R_NilValue); + R_RegisterCFinalizerEx(xptr, echo_thread_finalizer, TRUE); + + return xptr; +}