diff options
Diffstat (limited to 'src/cepis-http.c')
| -rw-r--r-- | src/cepis-http.c | 263 |
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; +} |