/* * 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 #include #include #include #include #include #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; }