#include "internal.h" #include #include #include #include #include #include #include #include #include #define CEPIS_ACCEPT_HEADER \ "Accept: application/json, text/csv;q=0.9, */*;q=0.1" static const long RETRIABLE_STATUS[] = {429, 500, 502, 503, 504}; #define RETRIABLE_COUNT (sizeof(RETRIABLE_STATUS) / sizeof(RETRIABLE_STATUS[0])) struct cepis_client { char *base_url; char *user_agent; double timeout; double backoff_base; double backoff_max; double total_timeout; int max_attempts; uint64_t rng; CURL *curl; struct curl_slist *headers; }; typedef struct { long status; double retry_after; cepis_buf_t body; } cepis_response_t; const char *cepis_status_str(cepis_status_t status) { switch (status) { case CEPIS_OK: return "ok"; case CEPIS_ERR_INVALID_CEP: return "invalid CEP"; case CEPIS_ERR_NOT_FOUND: return "CEP not found"; case CEPIS_ERR_RATE_LIMITED: return "rate limited"; case CEPIS_ERR_SERVER: return "server error"; case CEPIS_ERR_NETWORK: return "network error"; case CEPIS_ERR_INVALID_RESPONSE: return "invalid response"; case CEPIS_ERR_CONFIG: return "invalid configuration"; case CEPIS_ERR_NOMEM: return "out of memory"; } return "unknown error"; } cepis_status_t cepis_global_init(void) { if (curl_global_init(CURL_GLOBAL_DEFAULT) != CURLE_OK) return CEPIS_ERR_NETWORK; return CEPIS_OK; } void cepis_global_cleanup(void) { curl_global_cleanup(); } cepis_status_t cepis_normalize_cep(const char *value, char out[CEPIS_CEP_LEN + 1]) { char digits[CEPIS_CEP_LEN + 1]; size_t n = 0; size_t i; if (value == NULL || out == NULL) return CEPIS_ERR_INVALID_CEP; for (i = 0; value[i] != '\0'; i++) { char c = value[i]; if (c == ' ' || c == '\t' || c == '\n' || c == '\r' || c == '-' || c == '.') continue; if (c < '0' || c > '9') return CEPIS_ERR_INVALID_CEP; if (n >= CEPIS_CEP_LEN) return CEPIS_ERR_INVALID_CEP; digits[n++] = c; } if (n != CEPIS_CEP_LEN) return CEPIS_ERR_INVALID_CEP; digits[CEPIS_CEP_LEN] = '\0'; memcpy(out, digits, CEPIS_CEP_LEN + 1); return CEPIS_OK; } cepis_status_t cepis_format_cep(const char *cep, char out[CEPIS_CEP_LEN + 2]) { char digits[CEPIS_CEP_LEN + 1]; cepis_status_t status = cepis_normalize_cep(cep, digits); if (status != CEPIS_OK) return status; memcpy(out, digits, 5); out[5] = '-'; memcpy(out + 6, digits + 5, 3); out[CEPIS_CEP_LEN + 1] = '\0'; return CEPIS_OK; } void cepis_config_init(cepis_config_t *config) { if (config == NULL) return; config->base_url = CEPIS_DEFAULT_BASE_URL; config->user_agent = NULL; config->timeout = 10.0; config->max_attempts = 3; config->backoff_base = 0.5; config->backoff_max = 8.0; config->total_timeout = 0.0; } static char *trim_base_url(const char *base_url) { size_t len = strlen(base_url); char *copy; while (len > 0 && base_url[len - 1] == '/') len--; copy = malloc(len + 1); if (copy == NULL) return NULL; memcpy(copy, base_url, len); copy[len] = '\0'; return copy; } cepis_status_t cepis_client_new(const cepis_config_t *config, cepis_client_t **out, cepis_error_t *err) { cepis_config_t defaults; cepis_client_t *client; if (out == NULL) return cepis_set_error(err, CEPIS_ERR_CONFIG, 0, -1.0, "out must not be NULL"); if (config == NULL) { cepis_config_init(&defaults); config = &defaults; } if (config->base_url == NULL || config->base_url[0] == '\0') return cepis_set_error(err, CEPIS_ERR_CONFIG, 0, -1.0, "base_url must not be empty"); if (config->max_attempts < 1) return cepis_set_error(err, CEPIS_ERR_CONFIG, 0, -1.0, "max_attempts must be >= 1"); if (!(config->timeout > 0.0)) return cepis_set_error(err, CEPIS_ERR_CONFIG, 0, -1.0, "timeout must be > 0"); if (config->backoff_base < 0.0 || config->backoff_max < 0.0) return cepis_set_error(err, CEPIS_ERR_CONFIG, 0, -1.0, "backoff values must be >= 0"); if (config->total_timeout < 0.0) return cepis_set_error(err, CEPIS_ERR_CONFIG, 0, -1.0, "total_timeout must be >= 0"); if (curl_global_init(CURL_GLOBAL_DEFAULT) != CURLE_OK) return cepis_set_error(err, CEPIS_ERR_NETWORK, 0, -1.0, "could not initialise libcurl"); client = calloc(1, sizeof(*client)); if (client == NULL) { curl_global_cleanup(); return cepis_set_error(err, CEPIS_ERR_NOMEM, 0, -1.0, "out of memory"); } client->timeout = config->timeout; client->max_attempts = config->max_attempts; client->backoff_base = config->backoff_base; client->backoff_max = config->backoff_max; client->total_timeout = config->total_timeout; client->rng = (uint64_t)time(NULL) ^ ((uint64_t)(uintptr_t)client << 16); if (client->rng == 0) client->rng = 0x9E3779B97F4A7C15ULL; client->base_url = trim_base_url(config->base_url); if (config->user_agent != NULL && config->user_agent[0] != '\0') { client->user_agent = cepis_strdup(config->user_agent); } else { client->user_agent = cepis_strdup("cepis-c/" CEPIS_VERSION " (+https://cep.is)"); } client->curl = curl_easy_init(); client->headers = curl_slist_append(NULL, CEPIS_ACCEPT_HEADER); if (client->base_url == NULL || client->user_agent == NULL || client->curl == NULL || client->headers == NULL) { cepis_client_free(client); return cepis_set_error(err, CEPIS_ERR_NOMEM, 0, -1.0, "out of memory"); } *out = client; return CEPIS_OK; } void cepis_client_free(cepis_client_t *client) { if (client == NULL) return; if (client->curl != NULL) curl_easy_cleanup(client->curl); if (client->headers != NULL) curl_slist_free_all(client->headers); free(client->base_url); free(client->user_agent); free(client); curl_global_cleanup(); } const char *cepis_client_base_url(const cepis_client_t *client) { return client == NULL ? NULL : client->base_url; } const char *cepis_client_user_agent(const cepis_client_t *client) { return client == NULL ? NULL : client->user_agent; } static double monotonic_now(void) { struct timespec ts; if (clock_gettime(CLOCK_MONOTONIC, &ts) != 0) return 0.0; return (double)ts.tv_sec + (double)ts.tv_nsec / 1e9; } static void sleep_seconds(double seconds) { struct timespec request; if (seconds <= 0.0) return; if (seconds > 3600.0) seconds = 3600.0; request.tv_sec = (time_t)seconds; request.tv_nsec = (long)((seconds - (double)request.tv_sec) * 1e9); if (request.tv_nsec < 0) request.tv_nsec = 0; if (request.tv_nsec > 999999999L) request.tv_nsec = 999999999L; while (nanosleep(&request, &request) == -1 && errno == EINTR) ; } static double next_uniform(cepis_client_t *client) { uint64_t x = client->rng; x ^= x >> 12; x ^= x << 25; x ^= x >> 27; client->rng = x; x *= 0x2545F4914F6CDD1DULL; return (double)(x >> 11) / 9007199254740992.0; } static double backoff_delay(cepis_client_t *client, int attempt, double retry_after) { double ceiling; if (retry_after >= 0.0) return retry_after < client->backoff_max ? retry_after : client->backoff_max; ceiling = client->backoff_base * pow(2.0, (double)(attempt - 1)); if (ceiling > client->backoff_max) ceiling = client->backoff_max; return next_uniform(client) * ceiling; } static double parse_retry_after(const char *value, size_t len) { char text[128]; const char *formats[] = {"%a, %d %b %Y %H:%M:%S", "%A, %d-%b-%y %H:%M:%S", "%a %b %d %H:%M:%S %Y"}; char *stop = NULL; double seconds; size_t i; while (len > 0 && (*value == ' ' || *value == '\t')) { value++; len--; } while (len > 0 && (value[len - 1] == ' ' || value[len - 1] == '\t' || value[len - 1] == '\r' || value[len - 1] == '\n')) len--; if (len == 0 || len >= sizeof(text)) return -1.0; memcpy(text, value, len); text[len] = '\0'; seconds = strtod(text, &stop); if (stop != NULL && *stop == '\0' && stop != text) return seconds < 0.0 ? 0.0 : seconds; for (i = 0; i < sizeof(formats) / sizeof(formats[0]); i++) { struct tm parsed; time_t target; memset(&parsed, 0, sizeof(parsed)); if (strptime(text, formats[i], &parsed) == NULL) continue; target = timegm(&parsed); if (target == (time_t)-1) continue; seconds = difftime(target, time(NULL)); return seconds < 0.0 ? 0.0 : seconds; } return -1.0; } static size_t write_body(char *ptr, size_t size, size_t nmemb, void *userdata) { cepis_buf_t *body = userdata; size_t total = size * nmemb; if (!cepis_buf_append(body, ptr, total)) return 0; return total; } static size_t read_header(char *ptr, size_t size, size_t nmemb, void *userdata) { cepis_response_t *response = userdata; size_t total = size * nmemb; static const char name[] = "Retry-After:"; size_t name_len = sizeof(name) - 1; if (total > name_len && strncasecmp(ptr, name, name_len) == 0) response->retry_after = parse_retry_after(ptr + name_len, total - name_len); return total; } static bool is_retriable(long status) { size_t i; for (i = 0; i < RETRIABLE_COUNT; i++) if (RETRIABLE_STATUS[i] == status) return true; return false; } static bool is_accepted(long status, const long *accepted, size_t count) { size_t i; for (i = 0; i < count; i++) if (accepted[i] == status) return true; return false; } static void apply_options(cepis_client_t *client, const char *url, cepis_response_t *response, char *error_buffer, double budget) { CURL *curl = client->curl; long low_speed_time = (long)ceil(client->timeout); if (low_speed_time < 1) low_speed_time = 1; curl_easy_reset(curl); curl_easy_setopt(curl, CURLOPT_URL, url); curl_easy_setopt(curl, CURLOPT_HTTPGET, 1L); curl_easy_setopt(curl, CURLOPT_NOSIGNAL, 1L); curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L); curl_easy_setopt(curl, CURLOPT_MAXREDIRS, 5L); curl_easy_setopt(curl, CURLOPT_USERAGENT, client->user_agent); curl_easy_setopt(curl, CURLOPT_HTTPHEADER, client->headers); curl_easy_setopt(curl, CURLOPT_ACCEPT_ENCODING, "identity"); curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT_MS, (long)(client->timeout * 1000.0)); curl_easy_setopt(curl, CURLOPT_LOW_SPEED_LIMIT, 1L); curl_easy_setopt(curl, CURLOPT_LOW_SPEED_TIME, low_speed_time); curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_body); curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response->body); curl_easy_setopt(curl, CURLOPT_HEADERFUNCTION, read_header); curl_easy_setopt(curl, CURLOPT_HEADERDATA, response); curl_easy_setopt(curl, CURLOPT_ERRORBUFFER, error_buffer); if (budget > 0.0) { long ms = (long)(budget * 1000.0); curl_easy_setopt(curl, CURLOPT_TIMEOUT_MS, ms > 1 ? ms : 1L); } } static cepis_status_t client_request(cepis_client_t *client, const char *url, const long *accepted, size_t accepted_count, cepis_response_t *response, cepis_error_t *err) { char error_buffer[CURL_ERROR_SIZE]; double deadline = client->total_timeout > 0.0 ? monotonic_now() + client->total_timeout : 0.0; long last_status = 0; double last_retry_after = -1.0; int attempt; error_buffer[0] = '\0'; response->status = 0; response->retry_after = -1.0; cepis_buf_init(&response->body); for (attempt = 1; attempt <= client->max_attempts; attempt++) { double budget = 0.0; CURLcode code; double wait; if (deadline > 0.0) { budget = deadline - monotonic_now(); if (budget <= 0.0) break; } response->body.len = 0; if (response->body.data != NULL) response->body.data[0] = '\0'; response->retry_after = -1.0; error_buffer[0] = '\0'; apply_options(client, url, response, error_buffer, budget); code = curl_easy_perform(client->curl); if (code == CURLE_OK) { long status = 0; curl_easy_getinfo(client->curl, CURLINFO_RESPONSE_CODE, &status); if (is_accepted(status, accepted, accepted_count) || !is_retriable(status)) { response->status = status; return CEPIS_OK; } last_status = status; last_retry_after = response->retry_after; } else { last_status = 0; last_retry_after = -1.0; if (error_buffer[0] == '\0') snprintf(error_buffer, sizeof(error_buffer), "%s", curl_easy_strerror(code)); } if (attempt == client->max_attempts) break; wait = backoff_delay(client, attempt, last_retry_after); if (deadline > 0.0) { double remaining = deadline - monotonic_now(); if (remaining <= 0.0) break; if (wait > remaining) wait = remaining; } sleep_seconds(wait); } cepis_buf_free(&response->body); if (last_status == 429) return cepis_set_error(err, CEPIS_ERR_RATE_LIMITED, last_status, last_retry_after, "rate limit reached at %s (after %d " "attempt(s))", url, client->max_attempts); if (last_status != 0) return cepis_set_error(err, CEPIS_ERR_SERVER, last_status, -1.0, "the API answered %ld at %s (after %d " "attempt(s))", last_status, url, client->max_attempts); return cepis_set_error(err, CEPIS_ERR_NETWORK, 0, -1.0, "could not reach %s (after %d attempt(s)): %s", url, client->max_attempts, error_buffer[0] != '\0' ? error_buffer : "request aborted"); } static void api_message(const cepis_buf_t *body, char *out, size_t out_size, const char *fallback) { json_value_t *root = json_parse(body->data, body->len); const char *message = json_as_string(json_object_get(root, "mensagem")); snprintf(out, out_size, "%s", message != NULL ? message : fallback); json_free(root); } static cepis_status_t build_url(const cepis_client_t *client, const char *path, cepis_buf_t *out) { cepis_buf_init(out); if (!cepis_buf_append_str(out, client->base_url) || !cepis_buf_append_str(out, path)) { cepis_buf_free(out); return CEPIS_ERR_NOMEM; } return CEPIS_OK; } cepis_status_t cepis_lookup(cepis_client_t *client, const char *cep, cepis_address_t **out, cepis_error_t *err) { char number[CEPIS_CEP_LEN + 1]; char path[CEPIS_CEP_LEN + 2]; char message[CEPIS_ERROR_MESSAGE_MAX]; char fallback[CEPIS_ERROR_MESSAGE_MAX]; static const long accepted[] = {200}; cepis_response_t response; cepis_address_t *address; cepis_buf_t url; json_value_t *root; cepis_status_t status; if (client == NULL || out == NULL) return cepis_set_error(err, CEPIS_ERR_CONFIG, 0, -1.0, "client and out must not be NULL"); if (cepis_normalize_cep(cep, number) != CEPIS_OK) return cepis_set_error(err, CEPIS_ERR_INVALID_CEP, 0, -1.0, "CEP must be 8 digits, got \"%s\"", cep != NULL ? cep : "(null)"); snprintf(path, sizeof(path), "/%s", number); status = build_url(client, path, &url); if (status != CEPIS_OK) return cepis_set_error(err, status, 0, -1.0, "out of memory"); status = client_request(client, url.data, accepted, 1, &response, err); cepis_buf_free(&url); if (status != CEPIS_OK) return status; if (response.status == 404) { cepis_buf_free(&response.body); return cepis_set_error( err, CEPIS_ERR_NOT_FOUND, 404, -1.0, "CEP %s does not exist in the DNE database", number); } if (response.status == 400) { snprintf(fallback, sizeof(fallback), "CEP %s rejected", number); api_message(&response.body, message, sizeof(message), fallback); cepis_buf_free(&response.body); return cepis_set_error(err, CEPIS_ERR_INVALID_CEP, 400, -1.0, "%s", message); } if (response.status != 200) { long seen = response.status; cepis_buf_free(&response.body); return cepis_set_error(err, CEPIS_ERR_INVALID_RESPONSE, seen, -1.0, "the API answered %ld", seen); } root = json_parse(response.body.data, response.body.len); if (root == NULL || root->type != JSON_OBJECT) { json_free(root); status = cepis_set_error(err, CEPIS_ERR_INVALID_RESPONSE, 200, -1.0, "response is not a JSON object - " "something between you and " "the API is answering instead of it"); cepis_buf_free(&response.body); return status; } address = cepis_address_from_json(root, response.body.data, response.body.len); json_free(root); cepis_buf_free(&response.body); if (address == NULL) return cepis_set_error(err, CEPIS_ERR_NOMEM, 0, -1.0, "out of memory"); *out = address; return CEPIS_OK; } cepis_status_t cepis_health(cepis_client_t *client, cepis_health_t **out, cepis_error_t *err) { static const long accepted[] = {200, 503}; cepis_response_t response; cepis_health_t *health; cepis_buf_t url; json_value_t *root; cepis_status_t status; if (client == NULL || out == NULL) return cepis_set_error(err, CEPIS_ERR_CONFIG, 0, -1.0, "client and out must not be NULL"); status = build_url(client, "/health", &url); if (status != CEPIS_OK) return cepis_set_error(err, status, 0, -1.0, "out of memory"); status = client_request(client, url.data, accepted, 2, &response, err); cepis_buf_free(&url); if (status != CEPIS_OK) return status; root = json_parse(response.body.data, response.body.len); health = cepis_health_from_json(root); json_free(root); cepis_buf_free(&response.body); if (health == NULL) return cepis_set_error(err, CEPIS_ERR_INVALID_RESPONSE, response.status, -1.0, "health response is not a JSON object"); *out = health; return CEPIS_OK; } static bool blank(const char *text) { size_t i; if (text == NULL) return true; for (i = 0; text[i] != '\0'; i++) if (text[i] != ' ' && text[i] != '\t' && text[i] != '\r' && text[i] != '\n') return false; return true; } static size_t column_index(const csv_row_t *header, const char *name) { size_t i; for (i = 0; i < header->count; i++) if (strcmp(header->fields[i], name) == 0) return i; return (size_t)-1; } static cepis_status_t chunk_url(cepis_client_t *client, const char *const *ceps, size_t count, cepis_buf_t *out) { cepis_buf_t joined; char *escaped; size_t i; cepis_buf_init(&joined); for (i = 0; i < count; i++) { const char *value = ceps[i] != NULL ? ceps[i] : ""; if ((i > 0 && !cepis_buf_append_str(&joined, ",")) || !cepis_buf_append_str(&joined, value)) { cepis_buf_free(&joined); return CEPIS_ERR_NOMEM; } } escaped = curl_easy_escape(client->curl, joined.data ? joined.data : "", (int)joined.len); cepis_buf_free(&joined); if (escaped == NULL) return CEPIS_ERR_NOMEM; cepis_buf_init(out); if (!cepis_buf_append_str(out, client->base_url) || !cepis_buf_append_str(out, "/csv?ceps=") || !cepis_buf_append_str(out, escaped)) { cepis_buf_free(out); curl_free(escaped); return CEPIS_ERR_NOMEM; } curl_free(escaped); return CEPIS_OK; } static cepis_status_t fill_chunk(cepis_client_t *client, const char *const *ceps, size_t count, cepis_address_t **slots, cepis_error_t *err) { static const long accepted[] = {200}; char message[CEPIS_ERROR_MESSAGE_MAX]; cepis_response_t response; csv_reader_t *reader; csv_row_t header; cepis_buf_t url; size_t localidade; size_t index; cepis_status_t status; status = chunk_url(client, ceps, count, &url); if (status != CEPIS_OK) return cepis_set_error(err, status, 0, -1.0, "out of memory"); status = client_request(client, url.data, accepted, 1, &response, err); cepis_buf_free(&url); if (status != CEPIS_OK) return status; if (response.status == 400) { api_message(&response.body, message, sizeof(message), "the batch was rejected"); cepis_buf_free(&response.body); return cepis_set_error(err, CEPIS_ERR_INVALID_CEP, 400, -1.0, "%s", message); } if (response.status != 200) { long seen = response.status; cepis_buf_free(&response.body); return cepis_set_error(err, CEPIS_ERR_INVALID_RESPONSE, seen, -1.0, "the API answered %ld", seen); } reader = csv_reader_new(response.body.data ? response.body.data : "", response.body.len); if (reader == NULL) { cepis_buf_free(&response.body); return cepis_set_error(err, CEPIS_ERR_NOMEM, 0, -1.0, "out of memory"); } if (csv_reader_next(reader, &header) != 1) { csv_reader_free(reader); cepis_buf_free(&response.body); return cepis_set_error(err, CEPIS_ERR_INVALID_RESPONSE, 200, -1.0, "the batch response has no header"); } localidade = column_index(&header, "localidade"); status = CEPIS_OK; for (index = 0; index < count; index++) { csv_row_t record; int read = csv_reader_next(reader, &record); if (read != 1) break; if (localidade != (size_t)-1 && localidade < record.count && !blank(record.fields[localidade])) { slots[index] = cepis_address_from_csv(&header, &record); if (slots[index] == NULL) { csv_row_free(&record); status = cepis_set_error(err, CEPIS_ERR_NOMEM, 0, -1.0, "out of memory"); break; } } csv_row_free(&record); } csv_row_free(&header); csv_reader_free(reader); cepis_buf_free(&response.body); return status; } cepis_status_t cepis_lookup_many(cepis_client_t *client, const char *const *ceps, size_t count, cepis_address_list_t *out, cepis_error_t *err) { cepis_address_list_t list = {NULL, 0}; size_t start; if (client == NULL || out == NULL) return cepis_set_error(err, CEPIS_ERR_CONFIG, 0, -1.0, "client and out must not be NULL"); if (count == 0) { out->items = NULL; out->count = 0; return CEPIS_OK; } if (ceps == NULL) return cepis_set_error(err, CEPIS_ERR_CONFIG, 0, -1.0, "ceps must not be NULL"); list.items = calloc(count, sizeof(*list.items)); if (list.items == NULL) return cepis_set_error(err, CEPIS_ERR_NOMEM, 0, -1.0, "out of memory"); list.count = count; for (start = 0; start < count; start += CEPIS_MAX_BATCH) { size_t size = count - start; cepis_status_t status; if (size > CEPIS_MAX_BATCH) size = CEPIS_MAX_BATCH; status = fill_chunk(client, ceps + start, size, list.items + start, err); if (status != CEPIS_OK) { cepis_address_list_free(&list); return status; } } *out = list; return CEPIS_OK; }