summaryrefslogtreecommitdiffstats
path: root/src/cepis-http.c
diff options
context:
space:
mode:
authorMurilo Ijanc <[email protected]>2026-07-26 12:49:48 -0300
committerMurilo Ijanc <[email protected]>2026-07-26 12:49:48 -0300
commitf979d07d23098b96e758b3feb555cd06d85d9b2d (patch)
tree47298197a17a3d6c438a1f4cc7cb61afd390cf05 /src/cepis-http.c
downloadcobol-f979d07d23098b96e758b3feb555cd06d85d9b2d.tar.gz
Initial importv0.1.0
Diffstat (limited to 'src/cepis-http.c')
-rw-r--r--src/cepis-http.c263
1 files changed, 263 insertions, 0 deletions
diff --git a/src/cepis-http.c b/src/cepis-http.c
new file mode 100644
index 0000000..10fa023
--- /dev/null
+++ b/src/cepis-http.c
@@ -0,0 +1,263 @@
+/*
+ * Transport primitives for the cep.is COBOL client.
+ *
+ * COBOL has no HTTP, and `curl_easy_setopt` is variadic, which a COBOL
+ * CALL cannot express portably. Everything variadic therefore lives here,
+ * behind four fixed-signature entry points that a COBOL CALL can reach:
+ *
+ * cepis_http_get one GET, no retrying -- the policy stays in COBOL
+ * cepis_sleep_ms the backoff wait
+ * cepis_monotonic_ms the clock CFG-TOTAL-TIMEOUT-MS is measured on
+ * cepis_http_date_delta the RFC 9110 HTTP-date form of Retry-After
+ *
+ * All scalar arguments are `int` or `long long` passed BY REFERENCE, which
+ * COBOL declares as PIC S9(9) COMP-5 and PIC S9(18) COMP-5. All string
+ * arguments in are NUL-terminated by the caller. All string arguments out
+ * are blank-filled to their capacity, so the COBOL field they land in is a
+ * normal space-padded PIC X.
+ */
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <strings.h>
+#include <time.h>
+
+#include <curl/curl.h>
+
+#define CEPIS_OK_TRANSPORT 0
+#define CEPIS_ERR_TRANSPORT 1
+#define CEPIS_ERR_TOO_LARGE 2
+
+struct cepis_body {
+ char *buf;
+ long cap;
+ long len;
+ int overflow;
+};
+
+struct cepis_header {
+ char *buf;
+ long cap;
+ int seen;
+};
+
+static int cepis_curl_ready;
+
+static void cepis_blank(char *dst, long cap)
+{
+ if (dst && cap > 0) {
+ memset(dst, ' ', (size_t)cap);
+ }
+}
+
+static void cepis_put(char *dst, long cap, const char *src)
+{
+ size_t n;
+
+ cepis_blank(dst, cap);
+ if (!dst || cap <= 0 || !src) {
+ return;
+ }
+ n = strlen(src);
+ if (n > (size_t)cap) {
+ n = (size_t)cap;
+ }
+ memcpy(dst, src, n);
+}
+
+static size_t cepis_on_body(char *ptr, size_t size, size_t nmemb, void *userdata)
+{
+ struct cepis_body *sink = (struct cepis_body *)userdata;
+ size_t n = size * nmemb;
+
+ if (sink->len + (long)n > sink->cap) {
+ sink->overflow = 1;
+ return 0;
+ }
+ memcpy(sink->buf + sink->len, ptr, n);
+ sink->len += (long)n;
+ return n;
+}
+
+static size_t cepis_on_header(char *ptr, size_t size, size_t nmemb, void *userdata)
+{
+ struct cepis_header *hdr = (struct cepis_header *)userdata;
+ size_t n = size * nmemb;
+ static const char name[] = "retry-after:";
+ size_t name_len = sizeof(name) - 1;
+ size_t start, end, len;
+
+ if (hdr->seen || n <= name_len || strncasecmp(ptr, name, name_len) != 0) {
+ return n;
+ }
+ start = name_len;
+ end = n;
+ while (start < end && (ptr[start] == ' ' || ptr[start] == '\t')) {
+ start++;
+ }
+ while (end > start && (ptr[end - 1] == '\r' || ptr[end - 1] == '\n' ||
+ ptr[end - 1] == ' ' || ptr[end - 1] == '\t')) {
+ end--;
+ }
+ len = end - start;
+ if (len > (size_t)hdr->cap) {
+ len = (size_t)hdr->cap;
+ }
+ memcpy(hdr->buf, ptr + start, len);
+ hdr->seen = 1;
+ return n;
+}
+
+/*
+ * Perform one GET. Returns 0 when the exchange completed (whatever the
+ * status), 1 when it never completed, 2 when the body did not fit.
+ */
+int cepis_http_get(const char *url,
+ const char *user_agent,
+ const int *timeout_ms,
+ int *http_status,
+ char *retry_after,
+ const int *retry_after_cap,
+ char *body,
+ const int *body_cap,
+ int *body_len,
+ char *error_msg,
+ const int *error_cap)
+{
+ CURL *curl;
+ CURLcode rc;
+ struct curl_slist *headers = NULL;
+ struct cepis_body sink;
+ struct cepis_header hdr;
+ char errbuf[CURL_ERROR_SIZE];
+ long status = 0;
+ int result = CEPIS_ERR_TRANSPORT;
+
+ *http_status = 0;
+ *body_len = 0;
+ cepis_blank(retry_after, *retry_after_cap);
+ cepis_blank(error_msg, *error_cap);
+
+ if (!cepis_curl_ready) {
+ if (curl_global_init(CURL_GLOBAL_DEFAULT) != CURLE_OK) {
+ cepis_put(error_msg, *error_cap, "curl_global_init failed");
+ return CEPIS_ERR_TRANSPORT;
+ }
+ cepis_curl_ready = 1;
+ }
+
+ curl = curl_easy_init();
+ if (!curl) {
+ cepis_put(error_msg, *error_cap, "curl_easy_init failed");
+ return CEPIS_ERR_TRANSPORT;
+ }
+
+ sink.buf = body;
+ sink.cap = *body_cap;
+ sink.len = 0;
+ sink.overflow = 0;
+
+ hdr.buf = retry_after;
+ hdr.cap = *retry_after_cap;
+ hdr.seen = 0;
+
+ errbuf[0] = '\0';
+
+ headers = curl_slist_append(headers,
+ "Accept: application/json, text/csv;q=0.9, */*;q=0.1");
+ headers = curl_slist_append(headers, "Accept-Encoding: identity");
+
+ curl_easy_setopt(curl, CURLOPT_URL, url);
+ curl_easy_setopt(curl, CURLOPT_HTTPGET, 1L);
+ curl_easy_setopt(curl, CURLOPT_USERAGENT, user_agent);
+ curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
+ curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L);
+ curl_easy_setopt(curl, CURLOPT_MAXREDIRS, 5L);
+ curl_easy_setopt(curl, CURLOPT_NOSIGNAL, 1L);
+ curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT_MS, (long)*timeout_ms);
+ curl_easy_setopt(curl, CURLOPT_TIMEOUT_MS, (long)*timeout_ms);
+ curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, cepis_on_body);
+ curl_easy_setopt(curl, CURLOPT_WRITEDATA, &sink);
+ curl_easy_setopt(curl, CURLOPT_HEADERFUNCTION, cepis_on_header);
+ curl_easy_setopt(curl, CURLOPT_HEADERDATA, &hdr);
+ curl_easy_setopt(curl, CURLOPT_ERRORBUFFER, errbuf);
+
+ rc = curl_easy_perform(curl);
+
+ if (rc == CURLE_OK) {
+ curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &status);
+ *http_status = (int)status;
+ *body_len = (int)sink.len;
+ result = CEPIS_OK_TRANSPORT;
+ } else if (sink.overflow) {
+ cepis_put(error_msg, *error_cap, "response body exceeds the client buffer");
+ result = CEPIS_ERR_TOO_LARGE;
+ } else {
+ cepis_put(error_msg, *error_cap,
+ errbuf[0] ? errbuf : curl_easy_strerror(rc));
+ result = CEPIS_ERR_TRANSPORT;
+ }
+
+ curl_slist_free_all(headers);
+ curl_easy_cleanup(curl);
+ return result;
+}
+
+/* Sleep for the backoff. Negative and zero waits return at once. */
+void cepis_sleep_ms(const int *ms)
+{
+ struct timespec req;
+
+ if (*ms <= 0) {
+ return;
+ }
+ req.tv_sec = *ms / 1000;
+ req.tv_nsec = (long)(*ms % 1000) * 1000000L;
+ while (nanosleep(&req, &req) == -1) {
+ break;
+ }
+}
+
+/*
+ * Milliseconds off a monotonic clock. Wall time would let a clock step
+ * during an NTP correction shorten or extend CFG-TOTAL-TIMEOUT-MS.
+ */
+void cepis_monotonic_ms(long long *out)
+{
+ struct timespec now;
+
+ if (clock_gettime(CLOCK_MONOTONIC, &now) != 0) {
+ *out = 0;
+ return;
+ }
+ *out = (long long)now.tv_sec * 1000LL + (long long)(now.tv_nsec / 1000000L);
+}
+
+/*
+ * Seconds from now until an HTTP-date, the second form Retry-After may
+ * take. Returns 0 and sets *seconds when the date parses, 1 otherwise.
+ * The numeric form is handled in COBOL, where it belongs.
+ */
+int cepis_http_date_delta(const char *value, int *seconds)
+{
+ time_t target;
+ time_t now;
+ double delta;
+
+ *seconds = 0;
+ target = curl_getdate(value, NULL);
+ if (target == (time_t)-1) {
+ return 1;
+ }
+ now = time(NULL);
+ delta = difftime(target, now);
+ if (delta < 0) {
+ delta = 0;
+ }
+ if (delta > 2147483000.0) {
+ delta = 2147483000.0;
+ }
+ *seconds = (int)delta;
+ return 0;
+}