From fff26850dcf56fb5aa3a3161ef1e855a09e6f3a9 Mon Sep 17 00:00:00 2001
From: shikokuchuo <53399081+shikokuchuo@users.noreply.github.com>
Date: Mon, 15 Sep 2025 14:26:49 -0400
Subject: [PATCH] Echo server concept
---
NAMESPACE | 1 +
R/server.R | 53 +++++++++++
man/echo_server.Rd | 59 ++++++++++++
src/init.c | 1 +
src/nanonext.h | 1 +
src/server.c | 228 +++++++++++++++++++++++++++++++++++++++++++++
6 files changed, 343 insertions(+)
create mode 100644 R/server.R
create mode 100644 man/echo_server.Rd
create mode 100644 src/server.c
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;
+}