Compare commits

...

2 Commits

Author SHA1 Message Date
FoxyHunter c419bb38e9 Feat: add gandi api methods 2026-06-01 00:07:11 +02:00
FoxyHunter 29ecb39719 Fix: add missing semicolons & correct env naming 2026-06-01 00:06:50 +02:00
3 changed files with 327 additions and 4 deletions
+4 -4
View File
@@ -54,7 +54,7 @@ static int load_env_strs(
char *str = NULL;
if (load_env_str(env_name, &str, 0)) return -1;
*count = 1
*count = 1;
for (char *p = str; *p; p++)
if (*p == ',') (*count)++;
@@ -75,7 +75,7 @@ static int load_env_strs(
char **arr = malloc(*count * sizeof(char *));
if (!arr) { log_error("Memory allocation failed"); free(str); return -1; }
int i = 0
int i = 0;
char *token = strtok(str, ",");
while (token) {
arr[i++] = strdup(token);
@@ -93,8 +93,8 @@ int config_load(Config *cfg) {
if (load_env_str(ENV_DNS_KEEPER_DOMAIN, &cfg->dns_keeper_domain, 0)) return -1;
if (load_env_str(ENV_DNS_KEEPER_RECORD, &cfg->dns_keeper_record, 0)) return -1;
if (strlen(cfg->dns_keeper_record) + 1 + strlen(cfg->dns_keeper_domain) > CONFIG_MAX_DOMAIN_LEN) {
log_error("%s.%s exceeds max FQDN length (%d)",
ENV_SELF_RECORD, ENV_SELF_DOMAIN, CONFIG_MAX_FQDN_LEN);
log_error("%s.%s exceeds max domain length (%d)",
ENV_DNS_KEEPER_RECORD, ENV_DNS_KEEPER_DOMAIN, CONFIG_MAX_DOMAIN_LEN);
return -1;
}
+276
View File
@@ -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
View File
@@ -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 */