Feat: add gandi api methods
This commit is contained in:
+276
@@ -0,0 +1,276 @@
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <curl/curl.h>
|
||||
#include <cjson/cJSON.h>
|
||||
#include <ctype.h>
|
||||
#include "gandi.h"
|
||||
#include "log.h"
|
||||
|
||||
#define GANDI_API_BASE "https://api.gandi.net/v5/livedns"
|
||||
|
||||
static char s_api_key[256] = {0};
|
||||
|
||||
typedef struct {
|
||||
char *buf;
|
||||
size_t len;
|
||||
size_t cap;
|
||||
} DynBuf;
|
||||
|
||||
static size_t write_cb(void *data, size_t size, size_t nmemb, void *userp) {
|
||||
DynBuf *out = (DynBuf *)userp;
|
||||
size_t total = size * nmemb;
|
||||
|
||||
if (out->len + total + 1 > out->cap) {
|
||||
size_t new_cap = out->cap ? out->cap * 2 : 4096;
|
||||
while (new_cap < out->len + total + 1) new_cap *= 2;
|
||||
char *tmp = realloc(out->buf, new_cap);
|
||||
if (!tmp) return 0;
|
||||
out->buf = tmp;
|
||||
out->cap = new_cap;
|
||||
}
|
||||
|
||||
memcpy(out->buf + out->len, data, total);
|
||||
out->len += total;
|
||||
out->buf[out->len] = '\0';
|
||||
return total;
|
||||
}
|
||||
|
||||
int gandi_init(const char *api_key) {
|
||||
if (!api_key || api_key[0] == '\0') return -1;
|
||||
strncpy(s_api_key, api_key, sizeof(s_api_key) - 1);
|
||||
s_api_key[sizeof(s_api_key) - 1] = '\0';
|
||||
return 0;
|
||||
}
|
||||
|
||||
static CURL *make_curl(DynBuf *buf, struct curl_slist **headers_out) {
|
||||
CURL *curl = curl_easy_init();
|
||||
if (!curl) return NULL;
|
||||
|
||||
char auth_header[320];
|
||||
snprintf(auth_header, sizeof(auth_header), "Authorization: Bearer %s", s_api_key);
|
||||
|
||||
struct curl_slist *headers = NULL;
|
||||
headers = curl_slist_append(headers, auth_header);
|
||||
headers = curl_slist_append(headers, "Content-Type: application/json");
|
||||
|
||||
curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
|
||||
curl_easy_setopt(curl, CURLOPT_CAINFO, "/etc/ssl/certs/ca-certificates.crt");
|
||||
curl_easy_setopt(curl, CURLOPT_CAPATH, "/etc/ssl/certs");
|
||||
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_cb);
|
||||
curl_easy_setopt(curl, CURLOPT_WRITEDATA, buf);
|
||||
curl_easy_setopt(curl, CURLOPT_TIMEOUT, 10L);
|
||||
curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L);
|
||||
|
||||
*headers_out = headers;
|
||||
return curl;
|
||||
}
|
||||
|
||||
static int check_http_code(CURL *curl) {
|
||||
long code = 0;
|
||||
curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &code);
|
||||
|
||||
if (code == 401 || code == 403) return -2;
|
||||
if (code < 200 || code >= 300) return -1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int patch_record(
|
||||
const char *domain,
|
||||
const char *rec_name,
|
||||
const char *old_ip,
|
||||
const char *new_ip
|
||||
) {
|
||||
cJSON *patch = cJSON_CreateObject();
|
||||
cJSON *add_arr = cJSON_CreateArray();
|
||||
cJSON *rem_arr = cJSON_CreateArray();
|
||||
cJSON_AddItemToArray(add_arr, cJSON_CreateString(new_ip));
|
||||
cJSON_AddItemToArray(rem_arr, cJSON_CreateString(old_ip));
|
||||
cJSON_AddItemToObject(patch, "add_rrset_values", add_arr);
|
||||
cJSON_AddItemToObject(patch, "remove_rrset_values", rem_arr);
|
||||
char *patch_str = cJSON_PrintUnformatted(patch);
|
||||
cJSON_Delete(patch);
|
||||
|
||||
char url[512];
|
||||
snprintf(url, sizeof(url),
|
||||
"%s/domains/%s/records/%s/A", GANDI_API_BASE, domain, rec_name);
|
||||
|
||||
DynBuf buf = {0};
|
||||
struct curl_slist *headers = NULL;
|
||||
CURL *curl = make_curl(&buf, &headers);
|
||||
if (!curl) { free(patch_str); return -1; }
|
||||
|
||||
curl_easy_setopt(curl, CURLOPT_URL, url);
|
||||
curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, "PATCH");
|
||||
curl_easy_setopt(curl, CURLOPT_POSTFIELDS, patch_str);
|
||||
|
||||
CURLcode res = curl_easy_perform(curl);
|
||||
int ret = (res == CURLE_OK) ? check_http_code(curl) : -1;
|
||||
|
||||
curl_slist_free_all(headers);
|
||||
curl_easy_cleanup(curl);
|
||||
free(buf.buf);
|
||||
free(patch_str);
|
||||
return ret;
|
||||
}
|
||||
|
||||
int gandi_get_self_ip(const char *domain, const char *record_name, char *out_ip) {
|
||||
char url[512];
|
||||
snprintf(url, sizeof(url),
|
||||
"%s/domains/%s/records/%s/A", GANDI_API_BASE, domain, record_name);
|
||||
|
||||
DynBuf buf = {0};
|
||||
struct curl_slist *headers = NULL;
|
||||
CURL *curl = make_curl(&buf, &headers);
|
||||
if (!curl) return -1;
|
||||
|
||||
curl_easy_setopt(curl, CURLOPT_URL, url);
|
||||
|
||||
int ret = -1;
|
||||
|
||||
CURLcode cres = curl_easy_perform(curl);
|
||||
if (cres != CURLE_OK) {
|
||||
log_error("gandi: curl error for %s: (%d) %s",
|
||||
url, (int)cres, curl_easy_strerror(cres));
|
||||
ret = -1;
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
ret = check_http_code(curl);
|
||||
if (ret == -2) log_error("gandi: auth error (HTTP 401/403) for %s", url);
|
||||
else if (ret != 0) log_error("gandi: non-2xx HTTP response for %s", url);
|
||||
if (ret != 0) goto cleanup;
|
||||
|
||||
if (!buf.buf || buf.buf[0] == '\0') {
|
||||
log_error("gandi: empty response body for %s", url);
|
||||
ret = -1;
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
cJSON *root = cJSON_Parse(buf.buf);
|
||||
if (!root) {
|
||||
log_error("gandi: JSON parse failed for %s", url);
|
||||
ret = -1;
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
cJSON *values = cJSON_GetObjectItem(root, "rrset_values");
|
||||
if (!cJSON_IsArray(values) || cJSON_GetArraySize(values) <= 0) {
|
||||
log_warn("gandi: rrset_values missing/empty for %s (record %s.%s type A)",
|
||||
url, record_name, domain);
|
||||
cJSON_Delete(root);
|
||||
ret = -3;
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
const char *ip = cJSON_GetStringValue(cJSON_GetArrayItem(values, 0));
|
||||
if (!ip || ip[0] == '\0') {
|
||||
log_warn("gandi: rrset_values[0] not a string for %s", url);
|
||||
cJSON_Delete(root);
|
||||
ret = -3;
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
strncpy(out_ip, ip, GANDI_MAX_IP_LEN - 1);
|
||||
out_ip[GANDI_MAX_IP_LEN - 1] = '\0';
|
||||
|
||||
cJSON_Delete(root);
|
||||
ret = 0;
|
||||
|
||||
cleanup:
|
||||
curl_slist_free_all(headers);
|
||||
curl_easy_cleanup(curl);
|
||||
free(buf.buf);
|
||||
return ret;
|
||||
}
|
||||
|
||||
int gandi_update_domains(const char *domain, const char *old_ip, const char *new_ip) {
|
||||
char url[512];
|
||||
snprintf(url, sizeof(url),
|
||||
"%s/domains/%s/records", GANDI_API_BASE, domain);
|
||||
|
||||
DynBuf buf = {0};
|
||||
struct curl_slist *headers = NULL;
|
||||
CURL *curl = make_curl(&buf, &headers);
|
||||
if (!curl) return -1;
|
||||
|
||||
curl_easy_setopt(curl, CURLOPT_URL, url);
|
||||
|
||||
int ret = -1;
|
||||
int matched = 0;
|
||||
int failed = 0;
|
||||
int n = 0;
|
||||
cJSON *records = NULL;
|
||||
|
||||
if (curl_easy_perform(curl) != CURLE_OK) goto cleanup;
|
||||
|
||||
ret = check_http_code(curl);
|
||||
if (ret == -2) log_error("gandi: auth error (HTTP 401/403) for %s", url);
|
||||
else if (ret != 0) log_error("gandi: non-2xx HTTP response for %s", url);
|
||||
if (ret != 0) goto cleanup;
|
||||
|
||||
records = cJSON_Parse(buf.buf);
|
||||
if (!records || !cJSON_IsArray(records)) {
|
||||
ret = -1;
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
n = cJSON_GetArraySize(records);
|
||||
for (int i = 0; i < n; i++) {
|
||||
cJSON *rec = cJSON_GetArrayItem(records, i);
|
||||
cJSON *type = cJSON_GetObjectItem(rec, "rrset_type");
|
||||
if (!cJSON_IsString(type) || strcmp(type->valuestring, "A") != 0) continue;
|
||||
|
||||
cJSON *values = cJSON_GetObjectItem(rec, "rrset_values");
|
||||
if (!cJSON_IsArray(values)) continue;
|
||||
|
||||
int has_old = 0;
|
||||
int vcount = cJSON_GetArraySize(values);
|
||||
for (int j = 0; j < vcount; j++) {
|
||||
const char *v = cJSON_GetStringValue(cJSON_GetArrayItem(values, j));
|
||||
if (v && strcmp(v, old_ip) == 0) { has_old = 1; break; }
|
||||
}
|
||||
if (!has_old) continue;
|
||||
|
||||
cJSON *name_item = cJSON_GetObjectItem(rec, "rrset_name");
|
||||
const char *rec_name = cJSON_IsString(name_item) ? name_item->valuestring : "@";
|
||||
|
||||
int pret = patch_record(domain, rec_name, old_ip, new_ip);
|
||||
if (pret == 0) {
|
||||
log_info("gandi: [%s] updated record \"%s\" A: %s -> %s",
|
||||
domain, rec_name, old_ip, new_ip);
|
||||
matched++;
|
||||
} else {
|
||||
log_error("gandi: [%s] failed to patch record \"%s\" (err %d)",
|
||||
domain, rec_name, pret);
|
||||
failed++;
|
||||
}
|
||||
}
|
||||
|
||||
if (matched == 0 && failed == 0) ret = -3;
|
||||
else if (failed > 0) ret = -1;
|
||||
else ret = 0;
|
||||
|
||||
cleanup:
|
||||
curl_slist_free_all(headers);
|
||||
curl_easy_cleanup(curl);
|
||||
free(buf.buf);
|
||||
cJSON_Delete(records);
|
||||
return ret;
|
||||
}
|
||||
|
||||
int gandi_update_record(
|
||||
const char *domain,
|
||||
const char *record_name,
|
||||
const char *old_ip,
|
||||
const char *new_ip
|
||||
) {
|
||||
int ret = patch_record(domain, record_name, old_ip, new_ip);
|
||||
if (ret == 0)
|
||||
log_info("gandi: [%s] updated record \"%s\" A: %s -> %s",
|
||||
domain, record_name, old_ip, new_ip);
|
||||
else
|
||||
log_error("gandi: [%s] failed to patch record \"%s\" (err %d)",
|
||||
domain, record_name, ret);
|
||||
return ret;
|
||||
}
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
#ifndef GANDI_H
|
||||
#define GANDI_H
|
||||
|
||||
#define GANDI_MAX_IP_LEN 46
|
||||
|
||||
/*
|
||||
* Stores the API key internally.
|
||||
* Must be called once at startup before calling other gandi methods.
|
||||
*
|
||||
* Returns 0 on success.
|
||||
* Returns -1 if api_key is NULL or empty.
|
||||
*/
|
||||
int gandi_init(const char *api_key);
|
||||
|
||||
/*
|
||||
* Fetches the current IPv4 addr from the special gandi fqdn
|
||||
*
|
||||
* Returns 0 on success.
|
||||
* Returns -1 on network or curl error.
|
||||
* Returns -2 on API auth error (401/403).
|
||||
* Returns -3 if the record was not found or has no values.
|
||||
*/
|
||||
int gandi_get_self_ip(const char *domain, const char *record_name, char *out_ip);
|
||||
|
||||
/*
|
||||
* Fetches all records of all domains,
|
||||
* looks for all A records where rrset_value
|
||||
* is set to `old_ip` and patches each one to `new_ip`.
|
||||
*
|
||||
* Returns 0 on success (all records updated).
|
||||
* Returns -1 on network or curl error, or if any patch failed.
|
||||
* Returns -2 on API auth error (401/403).
|
||||
* Returns -3 if no A records had old_ip.
|
||||
*/
|
||||
int gandi_update_domains(const char *fqdn, const char *old_ip, const char *new_ip);
|
||||
|
||||
/*
|
||||
* Patches an A record's rrset_value from `old_ip` to `new_ip`.
|
||||
*
|
||||
* Returns 0 on success.
|
||||
* Returns -1 on network or curl error.
|
||||
* Returns -2 on API auth error (401/403).
|
||||
*/
|
||||
int gandi_update_record(const char *domain, const char *record_name,
|
||||
const char *old_ip, const char *new_ip);
|
||||
|
||||
#endif /* GANDI_H */
|
||||
Reference in New Issue
Block a user