mirror of
https://github.com/hanzoai/datastore.git
synced 2026-09-26 13:55:07 +00:00
800 lines
26 KiB
C
800 lines
26 KiB
C
/**
|
|
* Port of librdkafka's HTTP client (rdhttp.c) on top of a Poco-based
|
|
* transport (rdhttp_poco_transport.cpp) instead of libcurl.
|
|
*
|
|
* Also implements the few libcurl functions that
|
|
* rdkafka_sasl_oauthbearer_oidc.c calls directly (curl_slist_append,
|
|
* curl_slist_free_all, curl_easy_escape): their declarations come from
|
|
* rdhttp_curl_compat.h (via rdhttp.h), libcurl is not included nor linked.
|
|
*/
|
|
|
|
#include "rdkafka_int.h"
|
|
#include "rdunittest.h"
|
|
|
|
#include <ctype.h>
|
|
#include <stdarg.h>
|
|
|
|
#include "rdhttp.h"
|
|
|
|
#include "rdhttp_poco_transport.h"
|
|
|
|
/** Maximum response size, increase as necessary. */
|
|
#define RD_HTTP_RESPONSE_SIZE_MAX 1024 * 1024 * 500 /* 500kb */
|
|
|
|
|
|
/** Per-request state, stored in rd_http_req_t.hreq_curl, which is opaque
|
|
* to the users of this API. */
|
|
typedef struct rd_http_state_s {
|
|
char *url;
|
|
char **headers; /**< "Name: value" strings */
|
|
size_t headers_cnt;
|
|
char *post_fields;
|
|
size_t post_fields_size;
|
|
int timeout_s;
|
|
const char *ca_location; /**< Points into rk_conf */
|
|
const char *ca_pem; /**< Points into rk_conf */
|
|
char *content_type; /**< Response content type */
|
|
} rd_http_state_t;
|
|
|
|
#define RD_HTTP_STATE(hreq) ((rd_http_state_t *)(hreq)->hreq_curl)
|
|
|
|
|
|
void rd_http_error_destroy(rd_http_error_t *herr) {
|
|
rd_free(herr);
|
|
}
|
|
|
|
static rd_http_error_t *rd_http_error_new(int code, const char *fmt, ...)
|
|
RD_FORMAT(printf, 2, 3);
|
|
static rd_http_error_t *rd_http_error_new(int code, const char *fmt, ...) {
|
|
size_t len = 0;
|
|
rd_http_error_t *herr;
|
|
va_list ap;
|
|
|
|
va_start(ap, fmt);
|
|
|
|
if (fmt && *fmt) {
|
|
va_list ap2;
|
|
va_copy(ap2, ap);
|
|
len = rd_vsnprintf(NULL, 0, fmt, ap2);
|
|
va_end(ap2);
|
|
}
|
|
|
|
/* Use single allocation for both herr and the error string */
|
|
herr = rd_malloc(sizeof(*herr) + len + 1);
|
|
herr->code = code;
|
|
herr->errstr = herr->data;
|
|
|
|
if (len > 0)
|
|
rd_vsnprintf(herr->errstr, len + 1, fmt, ap);
|
|
else
|
|
herr->errstr[0] = '\0';
|
|
|
|
va_end(ap);
|
|
|
|
return herr;
|
|
}
|
|
|
|
/**
|
|
* @brief Same as rd_http_error_new() but reads the error string from the
|
|
* provided buffer.
|
|
*/
|
|
static rd_http_error_t *rd_http_error_new_from_buf(int code,
|
|
const rd_buf_t *rbuf) {
|
|
rd_http_error_t *herr;
|
|
rd_slice_t slice;
|
|
size_t len = rd_buf_len(rbuf);
|
|
|
|
if (len == 0)
|
|
return rd_http_error_new(
|
|
code, "Server did not provide an error string");
|
|
|
|
|
|
/* Use single allocation for both herr and the error string */
|
|
herr = rd_malloc(sizeof(*herr) + len + 1);
|
|
herr->code = code;
|
|
herr->errstr = herr->data;
|
|
rd_slice_init_full(&slice, rbuf);
|
|
rd_slice_read(&slice, herr->errstr, len);
|
|
herr->errstr[len] = '\0';
|
|
|
|
return herr;
|
|
}
|
|
|
|
void rd_http_req_destroy(rd_http_req_t *hreq) {
|
|
rd_http_state_t *state = RD_HTTP_STATE(hreq);
|
|
|
|
if (state) {
|
|
size_t i;
|
|
for (i = 0; i < state->headers_cnt; i++)
|
|
rd_free(state->headers[i]);
|
|
RD_IF_FREE(state->headers, rd_free);
|
|
RD_IF_FREE(state->post_fields, rd_free);
|
|
RD_IF_FREE(state->content_type, rd_free);
|
|
RD_IF_FREE(state->url, rd_free);
|
|
rd_free(state);
|
|
hreq->hreq_curl = NULL;
|
|
}
|
|
RD_IF_FREE(hreq->hreq_buf, rd_buf_destroy_free);
|
|
}
|
|
|
|
|
|
/**
|
|
* @brief Transport write callback. Writes the received bytes
|
|
* to the hreq's buffer.
|
|
*/
|
|
static size_t
|
|
rd_http_req_write_cb(char *ptr, size_t size, size_t nmemb, void *userdata) {
|
|
rd_http_req_t *hreq = (rd_http_req_t *)userdata;
|
|
|
|
if (unlikely(rd_buf_len(hreq->hreq_buf) + nmemb >
|
|
RD_HTTP_RESPONSE_SIZE_MAX))
|
|
return 0;
|
|
|
|
rd_buf_write(hreq->hreq_buf, ptr, nmemb);
|
|
|
|
return nmemb;
|
|
}
|
|
|
|
static void rd_http_req_add_header(rd_http_req_t *hreq, const char *header) {
|
|
rd_http_state_t *state = RD_HTTP_STATE(hreq);
|
|
|
|
state->headers =
|
|
rd_realloc(state->headers,
|
|
sizeof(*state->headers) * (state->headers_cnt + 1));
|
|
state->headers[state->headers_cnt++] = rd_strdup(header);
|
|
}
|
|
|
|
/**
|
|
* @brief Check if the error returned from HTTP(S) is temporary or not.
|
|
*
|
|
* @returns If the \p error_code is temporary, return rd_true,
|
|
* otherwise return rd_false.
|
|
*
|
|
* @locality Any thread.
|
|
* @locks None.
|
|
* @locks_acquired None.
|
|
*/
|
|
static rd_bool_t rd_http_is_failure_temporary(int error_code) {
|
|
switch (error_code) {
|
|
case 408: /**< Request timeout */
|
|
case 425: /**< Too early */
|
|
case 429: /**< Too many requests */
|
|
case 500: /**< Internal server error */
|
|
case 502: /**< Bad gateway */
|
|
case 503: /**< Service unavailable */
|
|
case 504: /**< Gateway timeout */
|
|
return rd_true;
|
|
|
|
default:
|
|
return rd_false;
|
|
}
|
|
}
|
|
|
|
rd_http_error_t *
|
|
rd_http_req_init(rd_kafka_t *rk, rd_http_req_t *hreq, const char *url) {
|
|
rd_http_state_t *state;
|
|
|
|
memset(hreq, 0, sizeof(*hreq));
|
|
|
|
state = rd_calloc(1, sizeof(*state));
|
|
state->url = rd_strdup(url);
|
|
state->timeout_s = 30;
|
|
if (rk) {
|
|
state->ca_location = rk->rk_conf.https.ca_location;
|
|
state->ca_pem = rk->rk_conf.https.ca_pem;
|
|
}
|
|
|
|
hreq->hreq_curl = (CURL *)state;
|
|
hreq->hreq_buf = rd_buf_new(1, 1024);
|
|
|
|
return NULL;
|
|
}
|
|
|
|
/**
|
|
* @brief Synchronously (blockingly) perform the HTTP operation.
|
|
*/
|
|
rd_http_error_t *rd_http_req_perform_sync(rd_http_req_t *hreq) {
|
|
rd_http_state_t *state = RD_HTTP_STATE(hreq);
|
|
long code = 0;
|
|
int r;
|
|
|
|
/* Discard the response of a previous (failed) attempt, if any. */
|
|
if (rd_buf_len(hreq->hreq_buf) > 0) {
|
|
rd_buf_destroy_free(hreq->hreq_buf);
|
|
hreq->hreq_buf = rd_buf_new(1, 1024);
|
|
}
|
|
RD_IF_FREE(state->content_type, rd_free);
|
|
state->content_type = NULL;
|
|
|
|
r = rd_http_poco_perform(
|
|
state->url, (const char **)state->headers, state->headers_cnt,
|
|
state->post_fields, state->post_fields_size, state->timeout_s,
|
|
state->ca_location, state->ca_pem, rd_http_req_write_cb, hreq,
|
|
&code, &state->content_type, hreq->hreq_curl_errstr,
|
|
sizeof(hreq->hreq_curl_errstr));
|
|
if (unlikely(r != 0))
|
|
return rd_http_error_new(-1, "%s", hreq->hreq_curl_errstr);
|
|
|
|
hreq->hreq_code = (int)code;
|
|
if (hreq->hreq_code >= 400)
|
|
return rd_http_error_new_from_buf(hreq->hreq_code,
|
|
hreq->hreq_buf);
|
|
|
|
return NULL;
|
|
}
|
|
|
|
|
|
int rd_http_req_get_code(const rd_http_req_t *hreq) {
|
|
return hreq->hreq_code;
|
|
}
|
|
|
|
const char *rd_http_req_get_content_type(rd_http_req_t *hreq) {
|
|
return RD_HTTP_STATE(hreq)->content_type;
|
|
}
|
|
|
|
|
|
/**
|
|
* @brief Perform a blocking HTTP(S) request to \p url.
|
|
* Retries the request \p retries times with linear backoff.
|
|
* Interval of \p retry_ms milliseconds is used between retries.
|
|
*
|
|
* @param url The URL to perform the request to.
|
|
* @param headers_array Array of HTTP(S) headers to set, each element
|
|
* is a string in the form "key: value"
|
|
* @param headers_array_cnt Number of elements in \p headers_array.
|
|
* @param timeout_s Timeout in seconds for the request, 0 means default
|
|
* `rd_http_req_init()` timeout.
|
|
* @param retries Number of retries to perform on failure.
|
|
* @param retry_ms Milliseconds to wait between retries.
|
|
* @param rbufp (out) Pointer to a buffer that will be filled with the response.
|
|
* @param content_type (out, optional) Pointer to a string that will be filled
|
|
* with the content type of the response, if not NULL.
|
|
* @param response_code (out, optional) Pointer to an integer that will be
|
|
* filled with the HTTP response code, if not NULL.
|
|
*
|
|
* @return Returns NULL on success (HTTP response code < 400), or an error
|
|
* object on transport or HTTP error.
|
|
*
|
|
* @remark Returned error object, when non-NULL, must be destroyed
|
|
* by calling rd_http_error_destroy().
|
|
*
|
|
* @locality Any thread.
|
|
* @locks None.
|
|
* @locks_acquired None.
|
|
*/
|
|
rd_http_error_t *rd_http_get(rd_kafka_t *rk,
|
|
const char *url,
|
|
char **headers_array,
|
|
size_t headers_array_cnt,
|
|
int timeout_s,
|
|
int retries,
|
|
int retry_ms,
|
|
rd_buf_t **rbufp,
|
|
char **content_type,
|
|
int *response_code) {
|
|
rd_http_req_t hreq;
|
|
rd_http_error_t *herr = NULL;
|
|
char *header;
|
|
int i;
|
|
size_t len, j;
|
|
|
|
*rbufp = NULL;
|
|
if (content_type)
|
|
*content_type = NULL;
|
|
if (response_code)
|
|
*response_code = -1;
|
|
|
|
herr = rd_http_req_init(rk, &hreq, url);
|
|
if (unlikely(herr != NULL))
|
|
return herr;
|
|
|
|
for (j = 0; j < headers_array_cnt; j++) {
|
|
header = headers_array[j];
|
|
if (header && *header)
|
|
rd_http_req_add_header(&hreq, header);
|
|
}
|
|
if (timeout_s > 0)
|
|
RD_HTTP_STATE(&hreq)->timeout_s = timeout_s;
|
|
|
|
for (i = 0; i <= retries; i++) {
|
|
if (rd_kafka_terminating(rk)) {
|
|
herr = rd_http_error_new(-1, "Terminating");
|
|
goto done;
|
|
}
|
|
|
|
herr = rd_http_req_perform_sync(&hreq);
|
|
len = rd_buf_len(hreq.hreq_buf);
|
|
|
|
if (!herr) {
|
|
if (len > 0)
|
|
break; /* Success */
|
|
/* Empty response */
|
|
goto done;
|
|
}
|
|
|
|
/* Retry if HTTP(S) request returns temporary error and there
|
|
* are remaining retries, else fail. */
|
|
if (i == retries || !rd_http_is_failure_temporary(herr->code)) {
|
|
goto done;
|
|
}
|
|
|
|
/* Retry */
|
|
rd_http_error_destroy(herr);
|
|
rd_usleep(retry_ms * 1000 * (i + 1), &rk->rk_terminate);
|
|
}
|
|
|
|
*rbufp = hreq.hreq_buf;
|
|
hreq.hreq_buf = NULL;
|
|
|
|
if (content_type) {
|
|
const char *ct = rd_http_req_get_content_type(&hreq);
|
|
if (ct && *ct)
|
|
*content_type = rd_strdup(ct);
|
|
else
|
|
*content_type = NULL;
|
|
}
|
|
if (response_code)
|
|
*response_code = hreq.hreq_code;
|
|
|
|
done:
|
|
rd_http_req_destroy(&hreq);
|
|
return herr;
|
|
}
|
|
|
|
|
|
/**
|
|
* @brief Extract the JSON object from \p hreq and return it in \p *jsonp.
|
|
*
|
|
* @returns Returns NULL on success, or an JSON parsing error - this
|
|
* error object must be destroyed by calling rd_http_error_destroy().
|
|
*/
|
|
rd_http_error_t *rd_http_parse_json(rd_http_req_t *hreq, cJSON **jsonp) {
|
|
size_t len;
|
|
char *raw_json;
|
|
const char *end = NULL;
|
|
rd_slice_t slice;
|
|
rd_http_error_t *herr = NULL;
|
|
|
|
/* cJSON requires the entire input to parse in contiguous memory. */
|
|
rd_slice_init_full(&slice, hreq->hreq_buf);
|
|
len = rd_buf_len(hreq->hreq_buf);
|
|
|
|
raw_json = rd_malloc(len + 1);
|
|
rd_slice_read(&slice, raw_json, len);
|
|
raw_json[len] = '\0';
|
|
|
|
/* Parse JSON */
|
|
*jsonp = kafka_cJSON_ParseWithOpts(raw_json, &end, 0);
|
|
|
|
if (!*jsonp)
|
|
herr = rd_http_error_new(hreq->hreq_code,
|
|
"Failed to parse JSON response "
|
|
"at %" PRIusz "/%" PRIusz,
|
|
(size_t)(end - raw_json), len);
|
|
rd_free(raw_json);
|
|
return herr;
|
|
}
|
|
|
|
|
|
/**
|
|
* @brief Perform a blocking HTTP(S) request to \p url with
|
|
* HTTP(S) headers and data with \p timeout_s.
|
|
* If the HTTP(S) request fails, will retry another \p retries times
|
|
* with multiplying backoff \p retry_ms.
|
|
*
|
|
* @returns The result will be returned in \p *jsonp.
|
|
* Returns NULL on success (HTTP response code < 400), or an error
|
|
* object on transport, HTTP error or a JSON parsing error - this
|
|
* error object must be destroyed by calling rd_http_error_destroy().
|
|
*
|
|
* @locality Any thread.
|
|
*/
|
|
rd_http_error_t *rd_http_post_expect_json(rd_kafka_t *rk,
|
|
const char *url,
|
|
const struct curl_slist *headers,
|
|
const char *post_fields,
|
|
size_t post_fields_size,
|
|
int timeout_s,
|
|
int retries,
|
|
int retry_ms,
|
|
cJSON **jsonp) {
|
|
rd_http_error_t *herr;
|
|
rd_http_req_t hreq;
|
|
rd_http_state_t *state;
|
|
const struct curl_slist *hdr;
|
|
int i;
|
|
size_t len;
|
|
const char *content_type;
|
|
|
|
herr = rd_http_req_init(rk, &hreq, url);
|
|
if (unlikely(herr != NULL))
|
|
return herr;
|
|
|
|
for (hdr = headers; hdr; hdr = hdr->next)
|
|
rd_http_req_add_header(&hreq, hdr->data);
|
|
|
|
state = RD_HTTP_STATE(&hreq);
|
|
state->timeout_s = timeout_s;
|
|
|
|
state->post_fields = rd_malloc(post_fields_size > 0 ? post_fields_size
|
|
: 1);
|
|
memcpy(state->post_fields, post_fields, post_fields_size);
|
|
state->post_fields_size = post_fields_size;
|
|
|
|
for (i = 0; i <= retries; i++) {
|
|
if (rd_kafka_terminating(rk)) {
|
|
rd_http_req_destroy(&hreq);
|
|
return rd_http_error_new(-1, "Terminating");
|
|
}
|
|
|
|
herr = rd_http_req_perform_sync(&hreq);
|
|
len = rd_buf_len(hreq.hreq_buf);
|
|
|
|
if (!herr) {
|
|
if (len > 0)
|
|
break; /* Success */
|
|
/* Empty response */
|
|
rd_http_req_destroy(&hreq);
|
|
return NULL;
|
|
}
|
|
/* Retry if HTTP(S) request returns temporary error and there
|
|
* are remaining retries, else fail. */
|
|
if (i == retries || !rd_http_is_failure_temporary(herr->code)) {
|
|
rd_http_req_destroy(&hreq);
|
|
return herr;
|
|
}
|
|
|
|
/* Retry */
|
|
rd_http_error_destroy(herr);
|
|
rd_usleep(retry_ms * 1000 * (i + 1), &rk->rk_terminate);
|
|
}
|
|
|
|
content_type = rd_http_req_get_content_type(&hreq);
|
|
|
|
if (!content_type || rd_strncasecmp(content_type, "application/json",
|
|
strlen("application/json"))) {
|
|
if (!herr)
|
|
herr = rd_http_error_new(
|
|
hreq.hreq_code, "Response is not JSON encoded: %s",
|
|
content_type ? content_type : "(n/a)");
|
|
rd_http_req_destroy(&hreq);
|
|
return herr;
|
|
}
|
|
|
|
herr = rd_http_parse_json(&hreq, jsonp);
|
|
|
|
rd_http_req_destroy(&hreq);
|
|
|
|
return herr;
|
|
}
|
|
|
|
/**
|
|
* @brief Append \p params to \p url, taking care of existing query parameters
|
|
* and hash fragments. \p params must be already URL encoded.
|
|
*
|
|
* @returns A newly allocated string with the appended parameters or NULL
|
|
* on error.
|
|
*/
|
|
char *rd_http_get_params_append(const char *url, const char *params) {
|
|
const char *host, *p, *query;
|
|
const char *hash;
|
|
size_t url_len, base_len, query_len, params_len;
|
|
rd_bool_t need_slash = rd_true;
|
|
char *result, *w;
|
|
|
|
p = strstr(url, "://");
|
|
if (!p || p == url)
|
|
return NULL;
|
|
host = p + 3;
|
|
if (!*host || *host == '/' || *host == '?' || *host == '#')
|
|
return NULL;
|
|
|
|
/* The hash fragment, if any, is dropped. */
|
|
hash = strchr(url, '#');
|
|
url_len = hash ? (size_t)(hash - url) : strlen(url);
|
|
|
|
/* The base part ends where the query string starts and includes
|
|
* the path, if any. */
|
|
query = NULL;
|
|
base_len = url_len;
|
|
for (p = host; p < url + url_len; p++) {
|
|
if (*p == '/')
|
|
need_slash = rd_false;
|
|
if (*p == '?') {
|
|
query = p + 1;
|
|
base_len = (size_t)(p - url);
|
|
break;
|
|
}
|
|
}
|
|
|
|
query_len = query ? (size_t)(url + url_len - query) : 0;
|
|
/* Drop trailing '&' of the existing query string. */
|
|
while (query_len > 0 && query[query_len - 1] == '&')
|
|
query_len--;
|
|
params_len = strlen(params);
|
|
|
|
result = w = rd_malloc(base_len + 4 + query_len + params_len);
|
|
memcpy(w, url, base_len);
|
|
w += base_len;
|
|
if (need_slash)
|
|
*(w++) = '/';
|
|
if (query_len > 0 || params_len > 0)
|
|
*(w++) = '?';
|
|
memcpy(w, query, query_len);
|
|
w += query_len;
|
|
if (query_len > 0 && params_len > 0)
|
|
*(w++) = '&';
|
|
memcpy(w, params, params_len);
|
|
w += params_len;
|
|
*w = '\0';
|
|
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* @brief Same as rd_http_get() but requires a JSON response.
|
|
* The response is parsed and a JSON object is returned in \p *jsonp.
|
|
*
|
|
* Same error semantics as rd_http_get().
|
|
*/
|
|
rd_http_error_t *rd_http_get_json(rd_kafka_t *rk,
|
|
const char *url,
|
|
char **headers_array,
|
|
size_t headers_array_cnt,
|
|
int timeout_s,
|
|
int retries,
|
|
int retry_ms,
|
|
cJSON **jsonp) {
|
|
rd_http_error_t *herr;
|
|
int response_code;
|
|
char *content_type;
|
|
rd_buf_t *rbuf;
|
|
char **headers_array_new =
|
|
rd_calloc(headers_array_cnt + 1, sizeof(*headers_array_new));
|
|
rd_http_req_t hreq;
|
|
|
|
*jsonp = NULL;
|
|
|
|
memcpy(headers_array_new, headers_array,
|
|
headers_array_cnt * sizeof(*headers_array_new));
|
|
headers_array_new[headers_array_cnt++] = "Accept: application/json";
|
|
|
|
herr = rd_http_get(rk, url, headers_array_new, headers_array_cnt,
|
|
timeout_s, retries, retry_ms, &rbuf, &content_type,
|
|
&response_code);
|
|
rd_free(headers_array_new);
|
|
|
|
if (unlikely(herr != NULL))
|
|
return herr;
|
|
|
|
if (!content_type || rd_strncasecmp(content_type, "application/json",
|
|
strlen("application/json"))) {
|
|
herr = rd_http_error_new(response_code,
|
|
"Response is not JSON encoded: %s",
|
|
content_type ? content_type : "(n/a)");
|
|
RD_IF_FREE(rbuf, rd_buf_destroy_free);
|
|
RD_IF_FREE(content_type, rd_free);
|
|
return herr;
|
|
}
|
|
|
|
memset(&hreq, 0, sizeof(hreq));
|
|
hreq.hreq_buf = rbuf;
|
|
hreq.hreq_code = response_code;
|
|
herr = rd_http_parse_json(&hreq, jsonp);
|
|
RD_IF_FREE(rbuf, rd_buf_destroy_free);
|
|
RD_IF_FREE(content_type, rd_free);
|
|
|
|
return herr;
|
|
}
|
|
|
|
|
|
void rd_http_global_init(void) {
|
|
}
|
|
|
|
|
|
/**
|
|
* @name Implementation of the libcurl functions used by
|
|
* rdkafka_sasl_oauthbearer_oidc.c.
|
|
*
|
|
* The returned memory is allocated with malloc: rd_free and curl_free
|
|
* are both compatible with it.
|
|
*/
|
|
|
|
struct curl_slist *curl_slist_append(struct curl_slist *list,
|
|
const char *data) {
|
|
struct curl_slist *item, *last;
|
|
|
|
item = malloc(sizeof(*item));
|
|
if (!item)
|
|
return NULL;
|
|
item->data = strdup(data);
|
|
if (!item->data) {
|
|
free(item);
|
|
return NULL;
|
|
}
|
|
item->next = NULL;
|
|
|
|
if (!list)
|
|
return item;
|
|
last = list;
|
|
while (last->next)
|
|
last = last->next;
|
|
last->next = item;
|
|
return list;
|
|
}
|
|
|
|
void curl_slist_free_all(struct curl_slist *list) {
|
|
while (list) {
|
|
struct curl_slist *next = list->next;
|
|
free(list->data);
|
|
free(list);
|
|
list = next;
|
|
}
|
|
}
|
|
|
|
char *curl_easy_escape(CURL *handle, const char *string, int length) {
|
|
static const char hex[] = "0123456789ABCDEF";
|
|
size_t len = length > 0 ? (size_t)length : strlen(string);
|
|
char *result, *w;
|
|
size_t i;
|
|
|
|
(void)handle;
|
|
|
|
result = w = malloc(len * 3 + 1);
|
|
if (!result)
|
|
return NULL;
|
|
for (i = 0; i < len; i++) {
|
|
unsigned char c = (unsigned char)string[i];
|
|
if (isalnum(c) || c == '-' || c == '.' || c == '_' ||
|
|
c == '~') {
|
|
*(w++) = (char)c;
|
|
} else {
|
|
*(w++) = '%';
|
|
*(w++) = hex[c >> 4];
|
|
*(w++) = hex[c & 0xF];
|
|
}
|
|
}
|
|
*w = '\0';
|
|
return result;
|
|
}
|
|
|
|
void curl_free(void *p) {
|
|
free(p);
|
|
}
|
|
|
|
|
|
int unittest_http_get(void) {
|
|
const char *base_url = rd_getenv("RD_UT_HTTP_URL", NULL);
|
|
char *error_url;
|
|
size_t error_url_size;
|
|
cJSON *json, *jval;
|
|
rd_http_error_t *herr;
|
|
rd_bool_t empty;
|
|
rd_kafka_t *rk;
|
|
|
|
if (!base_url || !*base_url)
|
|
RD_UT_SKIP("RD_UT_HTTP_URL environment variable not set");
|
|
|
|
RD_UT_BEGIN();
|
|
|
|
rk = rd_calloc(1, sizeof(*rk));
|
|
error_url_size = strlen(base_url) + strlen("/error") + 1;
|
|
error_url = rd_alloca(error_url_size);
|
|
rd_snprintf(error_url, error_url_size, "%s/error", base_url);
|
|
|
|
/* Try the base url first, parse its JSON and extract a key-value. */
|
|
json = NULL;
|
|
herr = rd_http_get_json(rk, base_url, NULL, 0, 5, 1, 1000, &json);
|
|
RD_UT_ASSERT(!herr, "Expected get_json(%s) to succeed, got: %s",
|
|
base_url, herr->errstr);
|
|
|
|
empty = rd_true;
|
|
kafka_cJSON_ArrayForEach(jval, json) {
|
|
empty = rd_false;
|
|
break;
|
|
}
|
|
RD_UT_ASSERT(!empty, "Expected non-empty JSON response from %s",
|
|
base_url);
|
|
RD_UT_SAY(
|
|
"URL %s returned no error and a non-empty "
|
|
"JSON object/array as expected",
|
|
base_url);
|
|
kafka_cJSON_Delete(json);
|
|
|
|
|
|
/* Try the error URL, verify error code. */
|
|
json = NULL;
|
|
herr = rd_http_get_json(rk, error_url, NULL, 0, 5, 1, 1000, &json);
|
|
RD_UT_ASSERT(herr != NULL, "Expected get_json(%s) to fail", error_url);
|
|
RD_UT_ASSERT(herr->code >= 400,
|
|
"Expected get_json(%s) error code >= "
|
|
"400, got %d",
|
|
error_url, herr->code);
|
|
RD_UT_SAY(
|
|
"Error URL %s returned code %d, errstr \"%s\" "
|
|
"and %s JSON object as expected",
|
|
error_url, herr->code, herr->errstr, json ? "a" : "no");
|
|
/* Check if there's a JSON document returned */
|
|
if (json)
|
|
kafka_cJSON_Delete(json);
|
|
rd_http_error_destroy(herr);
|
|
rd_free(rk);
|
|
|
|
RD_UT_PASS();
|
|
}
|
|
|
|
int unittest_http_get_params_append(void) {
|
|
rd_kafka_t *rk;
|
|
char *res;
|
|
RD_UT_BEGIN();
|
|
char *tests[] = {"http://localhost:1234",
|
|
"",
|
|
"http://localhost:1234/",
|
|
|
|
"http://localhost:1234/",
|
|
"a=1",
|
|
"http://localhost:1234/?a=1",
|
|
|
|
"https://localhost:1234/",
|
|
"a=1&b=2",
|
|
"https://localhost:1234/?a=1&b=2",
|
|
|
|
"http://mydomain.com/?a=1",
|
|
"c=hi",
|
|
"http://mydomain.com/?a=1&c=hi",
|
|
|
|
"https://mydomain.com/?",
|
|
"c=hi",
|
|
"https://mydomain.com/?c=hi",
|
|
|
|
"http://localhost:1234/path?a=1&b=2#&c=3",
|
|
"c=hi",
|
|
"http://localhost:1234/path?a=1&b=2&c=hi",
|
|
|
|
"http://localhost:1234#?c=3",
|
|
"a=1",
|
|
"http://localhost:1234/?a=1",
|
|
|
|
"https://otherdomain.io/path?a=1&#c=3",
|
|
"b=2",
|
|
"https://otherdomain.io/path?a=1&b=2",
|
|
NULL};
|
|
|
|
|
|
res = rd_http_get_params_append("", "");
|
|
RD_UT_ASSERT(!res, "Expected NULL result, got: \"%s\"", res);
|
|
res = rd_http_get_params_append("", "a=2&b=3");
|
|
RD_UT_ASSERT(!res, "Expected NULL result, got: \"%s\"", res);
|
|
|
|
char **test = tests;
|
|
rk = rd_calloc(1, sizeof(*rk));
|
|
while (test[0]) {
|
|
res = rd_http_get_params_append(test[0], test[1]);
|
|
RD_UT_ASSERT(!strcmp(res, test[2]),
|
|
"Expected \"%s\", got: \"%s\"", test[2], res);
|
|
rd_free(res);
|
|
test += 3;
|
|
}
|
|
rd_free(rk);
|
|
|
|
RD_UT_PASS();
|
|
}
|
|
|
|
/**
|
|
* @brief Unittest. Requires a (local) webserver to be set with env var
|
|
* RD_UT_HTTP_URL=http://localhost:1234/some-path
|
|
*
|
|
* This server must return a JSON object or array containing at least one
|
|
* object on the main URL with a 2xx response code,
|
|
* and 4xx response on $RD_UT_HTTP_URL/error (with whatever type of body).
|
|
*/
|
|
|
|
int unittest_http(void) {
|
|
int fails = 0;
|
|
|
|
fails += unittest_http_get();
|
|
fails += unittest_http_get_params_append();
|
|
|
|
return fails;
|
|
}
|