e523398fa8
- 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
208 lines
6.5 KiB
C
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);
|
|
}
|