1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
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;
}
|