Files
Nexus-Guard-Suite/apps/device-agent-embedded/main/heartbeat.c
T
datadunia e523398fa8 feat: add ESP32 embedded agent + namespace VRF reference docs + update android-agent submodule
- apps/device-agent-embedded/: ESP32 WireGuard agent (C, ESP-IDF, mbedTLS)
- reference/namespace-vrf/: Linux namespace/VRF network isolation docs
- apps/android-agent: VPN permission check, StartResult sealed class
2026-09-13 14:39:24 +07:00

208 lines
6.5 KiB
C

/**
* @file heartbeat.c
* @brief HTTP Heartbeat Loop
*/
#include <string.h>
#include <stdlib.h>
#include <time.h>
#include "heartbeat.h"
#include "config.h"
#include "esp_log.h"
#include "esp_http_client.h"
#include "cJSON.h"
static const char *TAG = "nexus_heartbeat";
/* Retry config */
#define MAX_RETRIES 3
#define CONNECT_TIMEOUT_MS 10000
#define READ_TIMEOUT_MS 30000
/**
* @brief Convert epoch seconds to ISO8601 string
* @param epoch Unix timestamp
* @param buf Output buffer (at least 25 bytes)
* @param buf_len Buffer length
*/
static void epoch_to_iso8601(int64_t epoch, char *buf, size_t buf_len)
{
time_t t = (time_t)epoch;
struct tm tm_info;
gmtime_r(&t, &tm_info);
strftime(buf, buf_len, "%Y-%m-%dT%H:%M:%SZ", &tm_info);
}
/**
* @brief HTTP event handler for heartbeat
*/
static esp_err_t heartbeat_event_handler(esp_http_client_event_t *evt)
{
switch (evt->event_id) {
case HTTP_EVENT_ERROR:
ESP_LOGD(TAG, "HTTP_EVENT_ERROR");
break;
case HTTP_EVENT_ON_CONNECTED:
ESP_LOGD(TAG, "HTTP_EVENT_CONNECTED");
break;
case HTTP_EVENT_ON_HEADER:
break;
case HTTP_EVENT_ON_DATA:
ESP_LOGD(TAG, "HTTP_EVENT_ON_DATA, len=%d", evt->data_len);
break;
case HTTP_EVENT_ON_FINISH:
ESP_LOGD(TAG, "HTTP_EVENT_FINISHED");
break;
case HTTP_EVENT_DISCONNECTED:
ESP_LOGD(TAG, "HTTP_EVENT_DISCONNECTED");
break;
default:
break;
}
return ESP_OK;
}
/**
* @brief Helper: copy cJSON string to char buffer
*/
static void copy_json_str(const cJSON *json, const char *key, char *buf, size_t buf_len)
{
cJSON *item = cJSON_GetObjectItemCaseSensitive(json, key);
if (cJSON_IsString(item) && item->valuestring != NULL) {
strncpy(buf, item->valuestring, buf_len - 1);
buf[buf_len - 1] = '\0';
}
}
esp_err_t heartbeat_send(const wg_config_t *config, const char *status,
bool tunnel_up, int64_t last_handshake)
{
if (config == NULL || status == NULL) {
return ESP_ERR_INVALID_ARG;
}
/* Convert last_handshake to ISO8601 */
char handshake_str[25] = "1970-01-01T00:00:00Z";
if (last_handshake > 0) {
epoch_to_iso8601(last_handshake, handshake_str, sizeof(handshake_str));
}
/* Build request JSON */
cJSON *req_json = cJSON_CreateObject();
cJSON_AddStringToObject(req_json, "device_id", config->device_id);
cJSON_AddStringToObject(req_json, "status", status);
cJSON_AddBoolToObject(req_json, "tunnel_up", tunnel_up);
cJSON_AddStringToObject(req_json, "last_handshake", handshake_str);
char *req_body = cJSON_PrintUnformatted(req_json);
cJSON_Delete(req_json);
if (req_body == NULL) {
ESP_LOGE(TAG, "Failed to create heartbeat JSON");
return ESP_ERR_NO_MEM;
}
/* Build URL — use server_wg_ip if available, else we need external URL
* For simplicity, we store server_url elsewhere or use the configured endpoint.
* Since we don't have server_url in wg_config_t, we construct from endpoint.
* TODO: store server_url in config or pass as param */
char url[192];
/* Use server_wg_ip to build the heartbeat URL */
if (strlen(config->server_wg_ip) > 0) {
snprintf(url, sizeof(url), "http://%s:8080/api/v1/heartbeat", config->server_wg_ip);
} else {
/* Fallback — shouldn't happen in normal operation */
ESP_LOGE(TAG, "No server_wg_ip configured");
free(req_body);
return ESP_ERR_INVALID_STATE;
}
esp_err_t final_err = ESP_FAIL;
for (int attempt = 1; attempt <= MAX_RETRIES; attempt++) {
ESP_LOGD(TAG, "Heartbeat attempt %d/%d...", attempt, MAX_RETRIES);
esp_http_client_config_t http_config = {
.url = url,
.event_handler = heartbeat_event_handler,
.timeout_ms = CONNECT_TIMEOUT_MS,
};
esp_http_client_handle_t client = esp_http_client_init(&http_config);
if (client == NULL) {
ESP_LOGE(TAG, "Failed to init HTTP client");
continue;
}
esp_http_client_set_method(client, HTTP_METHOD_POST);
esp_http_client_set_header(client, "Content-Type", "application/json");
esp_http_client_set_post_field(client, req_body, strlen(req_body));
esp_err_t err = esp_http_client_open(client, strlen(req_body));
if (err != ESP_OK) {
ESP_LOGE(TAG, "HTTP open failed: %s", esp_err_to_name(err));
esp_http_client_cleanup(client);
vTaskDelay(pdMS_TO_TICKS(1000 * attempt));
continue;
}
int written = esp_http_client_write(client, req_body, strlen(req_body));
if (written < 0) {
ESP_LOGE(TAG, "HTTP write failed");
esp_http_client_close(client);
esp_http_client_cleanup(client);
vTaskDelay(pdMS_TO_TICKS(1000 * attempt));
continue;
}
int content_length = esp_http_client_fetch_headers(client);
int status_code = esp_http_client_get_status_code(client);
if (status_code != 200) {
ESP_LOGW(TAG, "Heartbeat failed, HTTP %d", status_code);
esp_http_client_close(client);
esp_http_client_cleanup(client);
vTaskDelay(pdMS_TO_TICKS(1000 * attempt));
continue;
}
/* Read response body for config sync */
if (content_length > 0) {
char *response_buf = malloc(content_length + 1);
if (response_buf != NULL) {
int read_len = esp_http_client_read(client, response_buf, content_length);
response_buf[read_len] = '\0';
/* TODO: Parse config sync fields if config_changed is detected
* For now, log the response size */
ESP_LOGD(TAG, "Heartbeat response: %d bytes", read_len);
free(response_buf);
}
}
esp_http_client_close(client);
esp_http_client_cleanup(client);
final_err = ESP_OK;
break;
}
free(req_body);
return final_err;
}
bool heartbeat_config_changed(const char *old_hash, const char *new_hash)
{
if (old_hash == NULL || new_hash == NULL) {
/* If either is NULL, treat as changed */
return true;
}
/* Empty new hash means no hash provided — treat as changed */
if (strlen(new_hash) == 0) {
return true;
}
/* Simple string comparison */
return (strcmp(old_hash, new_hash) != 0);
}