/* * SPDX-FileCopyrightText: 2024-2026 Espressif Systems (Shanghai) CO LTD * * SPDX-License-Identifier: Apache-2.0 */ #include "platform.h" #include #include #include #include #include #include "esp_crt_bundle.h" #include "esp_http_client.h" #include "esp_log.h" #include "freertos/FreeRTOS.h" #include "freertos/task.h" #ifndef CONFIG_TQ_NETWORK_DIAGNOSTICS_ENABLE #define CONFIG_TQ_NETWORK_DIAGNOSTICS_ENABLE 0 #endif #if CONFIG_TQ_NETWORK_DIAGNOSTICS_ENABLE #include #include #include #include "esp_heap_caps.h" #include "esp_netif.h" #include "esp_timer.h" #include "esp_wifi.h" #include "freertos/semphr.h" #include "ping/ping_sock.h" #endif #define PLATFORM_HTTPS_DEFAULT_TIMEOUT_MS 10000 /* lwIP getaddrinfo() already performs bounded DNS retransmission internally and * can block for roughly 14 seconds on a stale STA data path. Retrying the same * lookup here only duplicates that delay before control-plane recovery runs. */ #define PLATFORM_HTTPS_DNS_RETRY_COUNT 1 #define PLATFORM_HTTPS_DNS_RETRY_DELAY_MS 500 #define PLATFORM_HTTPS_HOST_MAX_LEN 256 #if CONFIG_TQ_NETWORK_DIAGNOSTICS_ENABLE #define PLATFORM_HTTPS_PROBE_PING_TIMEOUT_MS 400 #define PLATFORM_HTTPS_PROBE_TCP_TIMEOUT_MS 800 #endif static const char *TAG = "platform_https"; static const char *dns_error_name(int err); #if CONFIG_TQ_NETWORK_DIAGNOSTICS_ENABLE static volatile uint32_t s_dns_lookup_count; static volatile uint32_t s_dns_lookup_failure_count; static volatile uint32_t s_dns_lookup_active; static struct sockaddr_storage s_last_resolved_addr; static socklen_t s_last_resolved_addr_len; static char s_last_resolved_host[PLATFORM_HTTPS_HOST_MAX_LEN]; static portMUX_TYPE s_probe_cache_lock = portMUX_INITIALIZER_UNLOCKED; typedef struct { SemaphoreHandle_t done; volatile uint32_t replies; } https_ping_probe_context_t; static void https_ping_success(esp_ping_handle_t handle, void *args) { (void)handle; https_ping_probe_context_t *context = (https_ping_probe_context_t *)args; if (context != NULL) { context->replies++; } } static void https_ping_end(esp_ping_handle_t handle, void *args) { (void)handle; https_ping_probe_context_t *context = (https_ping_probe_context_t *)args; if (context != NULL && context->done != NULL) { (void)xSemaphoreGive(context->done); } } static void run_ipv4_ping_probe(const char *label, const esp_ip4_addr_t *target) { if (target == NULL || target->addr == 0U) { ESP_LOGW(TAG, "Network probe ping label=%s skipped: no IPv4 target", label); return; } https_ping_probe_context_t context = { .done = xSemaphoreCreateBinary(), }; if (context.done == NULL) { ESP_LOGW(TAG, "Network probe ping label=%s failed: no semaphore memory", label); return; } esp_ping_config_t config = ESP_PING_DEFAULT_CONFIG(); config.count = 1; config.interval_ms = 0; config.timeout_ms = PLATFORM_HTTPS_PROBE_PING_TIMEOUT_MS; ip_addr_set_ip4_u32(&config.target_addr, target->addr); esp_ping_callbacks_t callbacks = { .cb_args = &context, .on_ping_success = https_ping_success, .on_ping_timeout = NULL, .on_ping_end = https_ping_end, }; esp_ping_handle_t session = NULL; int64_t started_us = esp_timer_get_time(); esp_err_t err = esp_ping_new_session(&config, &callbacks, &session); if (err == ESP_OK) { err = esp_ping_start(session); } if (err == ESP_OK) { TickType_t wait_ticks = pdMS_TO_TICKS(PLATFORM_HTTPS_PROBE_PING_TIMEOUT_MS + 300); if (wait_ticks == 0) { wait_ticks = 1; } if (xSemaphoreTake(context.done, wait_ticks) != pdTRUE) { err = ESP_ERR_TIMEOUT; } } if (session != NULL) { (void)esp_ping_stop(session); (void)esp_ping_delete_session(session); } ESP_LOGW(TAG, "Network probe ping label=%s target=" IPSTR " result=%s replies=%u elapsed_ms=%lld", label, IP2STR(target), err == ESP_OK && context.replies > 0U ? "reachable" : (err == ESP_OK ? "no_reply" : esp_err_to_name(err)), (unsigned)context.replies, (long long)((esp_timer_get_time() - started_us) / 1000)); vSemaphoreDelete(context.done); } static void cache_resolved_endpoint(const char *host, const struct addrinfo *addr) { const struct addrinfo *selected = addr; while (selected != NULL && selected->ai_family != AF_INET && selected->ai_family != AF_INET6) { selected = selected->ai_next; } if (host == NULL || selected == NULL || selected->ai_addr == NULL || selected->ai_addrlen > sizeof(s_last_resolved_addr)) { return; } struct sockaddr_storage endpoint = {0}; memcpy(&endpoint, selected->ai_addr, selected->ai_addrlen); if (selected->ai_family == AF_INET) { ((struct sockaddr_in *)&endpoint)->sin_port = htons(443); } else { ((struct sockaddr_in6 *)&endpoint)->sin6_port = htons(443); } portENTER_CRITICAL(&s_probe_cache_lock); s_last_resolved_addr = endpoint; s_last_resolved_addr_len = selected->ai_addrlen; strlcpy(s_last_resolved_host, host, sizeof(s_last_resolved_host)); portEXIT_CRITICAL(&s_probe_cache_lock); } static void run_cached_endpoint_probe(void) { struct sockaddr_storage endpoint = {0}; socklen_t endpoint_len = 0; char host[PLATFORM_HTTPS_HOST_MAX_LEN] = {0}; portENTER_CRITICAL(&s_probe_cache_lock); endpoint = s_last_resolved_addr; endpoint_len = s_last_resolved_addr_len; strlcpy(host, s_last_resolved_host, sizeof(host)); portEXIT_CRITICAL(&s_probe_cache_lock); if (endpoint_len == 0 || host[0] == '\0') { ESP_LOGW(TAG, "Network probe cached TCP skipped: no successful DNS result cached"); return; } char address[64] = {0}; void *address_ptr = endpoint.ss_family == AF_INET ? (void *)&((struct sockaddr_in *)&endpoint)->sin_addr : (void *)&((struct sockaddr_in6 *)&endpoint)->sin6_addr; const char *formatted = inet_ntop(endpoint.ss_family, address_ptr, address, sizeof(address)); int64_t started_us = esp_timer_get_time(); int sock = socket(endpoint.ss_family, SOCK_STREAM, IPPROTO_TCP); int socket_error = 0; bool connected = false; if (sock >= 0) { int flags = fcntl(sock, F_GETFL, 0); if (flags >= 0 && fcntl(sock, F_SETFL, flags | O_NONBLOCK) == 0) { int connect_result = connect(sock, (const struct sockaddr *)&endpoint, endpoint_len); if (connect_result == 0) { connected = true; } else if (errno == EINPROGRESS) { fd_set write_set; FD_ZERO(&write_set); FD_SET(sock, &write_set); struct timeval timeout = { .tv_sec = 0, .tv_usec = PLATFORM_HTTPS_PROBE_TCP_TIMEOUT_MS * 1000, }; int selected = select(sock + 1, NULL, &write_set, NULL, &timeout); socklen_t error_len = sizeof(socket_error); if (selected > 0 && getsockopt(sock, SOL_SOCKET, SO_ERROR, &socket_error, &error_len) == 0 && socket_error == 0) { connected = true; } else if (selected == 0) { socket_error = ETIMEDOUT; } else if (socket_error == 0) { socket_error = errno; } } else { socket_error = errno; } } else { socket_error = errno; } close(sock); } else { socket_error = errno; } ESP_LOGW(TAG, "Network probe cached TCP host=%s address=%s:443 result=%s socket_error=%d elapsed_ms=%lld", host, formatted != NULL ? address : "unknown", connected ? "reachable" : "failed", socket_error, (long long)((esp_timer_get_time() - started_us) / 1000)); } static void run_dns_failure_network_probes(const esp_netif_ip_info_t *ip, const esp_netif_dns_info_t *fallback_dns) { ESP_LOGW(TAG, "Network probe bundle started after DNS failure"); run_ipv4_ping_probe("gateway", ip != NULL ? &ip->gw : NULL); if (fallback_dns != NULL && fallback_dns->ip.type == ESP_IPADDR_TYPE_V4) { run_ipv4_ping_probe("fallback_dns", &fallback_dns->ip.u_addr.ip4); } else { ESP_LOGW(TAG, "Network probe fallback DNS ping skipped: no IPv4 address"); } run_cached_endpoint_probe(); ESP_LOGW(TAG, "Network probe bundle completed"); } static const char *dhcp_status_name(esp_netif_dhcp_status_t status) { switch (status) { case ESP_NETIF_DHCP_INIT: return "init"; case ESP_NETIF_DHCP_STARTED: return "started"; case ESP_NETIF_DHCP_STOPPED: return "stopped"; default: return "unknown"; } } static const char *format_dns_address(const esp_netif_dns_info_t *dns, char *buffer, size_t buffer_size) { if (dns == NULL || buffer == NULL || buffer_size == 0U) { return "invalid"; } if (dns->ip.type == ESP_IPADDR_TYPE_V6) { int written = snprintf(buffer, buffer_size, IPV6STR, IPV62STR(dns->ip.u_addr.ip6)); return written >= 0 && (size_t)written < buffer_size ? buffer : "unknown"; } int written = snprintf(buffer, buffer_size, IPSTR, IP2STR(&dns->ip.u_addr.ip4)); return written >= 0 && (size_t)written < buffer_size ? buffer : "unknown"; } static void log_dns_failure_diagnostics(const char *host, int attempt, int dns_err, uint32_t lookup_id, uint32_t elapsed_ms) { esp_netif_t *sta = esp_netif_get_handle_from_ifkey("WIFI_STA_DEF"); esp_netif_ip_info_t ip = {0}; esp_netif_dns_info_t dns_main = {0}; esp_netif_dns_info_t dns_backup = {0}; esp_netif_dns_info_t dns_fallback = {0}; esp_netif_dhcp_status_t dhcp_status = ESP_NETIF_DHCP_INIT; esp_err_t ip_err = sta != NULL ? esp_netif_get_ip_info(sta, &ip) : ESP_ERR_NOT_FOUND; esp_err_t main_err = esp_netif_get_dns_info(sta, ESP_NETIF_DNS_MAIN, &dns_main); esp_err_t backup_err = esp_netif_get_dns_info(sta, ESP_NETIF_DNS_BACKUP, &dns_backup); esp_err_t fallback_err = esp_netif_get_dns_info(sta, ESP_NETIF_DNS_FALLBACK, &dns_fallback); esp_err_t dhcp_err = sta != NULL ? esp_netif_dhcpc_get_status(sta, &dhcp_status) : ESP_ERR_NOT_FOUND; wifi_ap_record_t ap = {0}; esp_err_t ap_err = esp_wifi_sta_get_ap_info(&ap); char main_buffer[48] = {0}; char backup_buffer[48] = {0}; char fallback_buffer[48] = {0}; const char *main_address = main_err == ESP_OK ? format_dns_address(&dns_main, main_buffer, sizeof(main_buffer)) : "unavailable"; const char *backup_address = backup_err == ESP_OK ? format_dns_address(&dns_backup, backup_buffer, sizeof(backup_buffer)) : "unavailable"; const char *fallback_address = fallback_err == ESP_OK ? format_dns_address(&dns_fallback, fallback_buffer, sizeof(fallback_buffer)) : "unavailable"; ESP_LOGW(TAG, "DNS diagnostics lookup=%u attempt=%d elapsed_ms=%u host=%s err=%s(%d) active=%u failures=%u sta=%d ip=" IPSTR " netmask=" IPSTR " gw=" IPSTR " dhcp=%d/%s dns(main=%s backup=%s fallback=%s status=%s/%s/%s) rssi=%d ap=%s heap(internal=%u largest=%u dma_largest=%u)", (unsigned)lookup_id, attempt, (unsigned)elapsed_ms, host != NULL ? host : "-", dns_error_name(dns_err), dns_err, (unsigned)__atomic_load_n(&s_dns_lookup_active, __ATOMIC_RELAXED), (unsigned)__atomic_load_n(&s_dns_lookup_failure_count, __ATOMIC_RELAXED), sta != NULL ? 1 : 0, IP2STR(&ip.ip), IP2STR(&ip.netmask), IP2STR(&ip.gw), dhcp_err == ESP_OK ? (int)dhcp_status : -1, dhcp_err == ESP_OK ? dhcp_status_name(dhcp_status) : esp_err_to_name(dhcp_err), main_address, backup_address, fallback_address, esp_err_to_name(main_err), esp_err_to_name(backup_err), esp_err_to_name(fallback_err), ap_err == ESP_OK ? (int)ap.rssi : 0, esp_err_to_name(ap_err), (unsigned)heap_caps_get_free_size(MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT), (unsigned)heap_caps_get_largest_free_block(MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT), (unsigned)heap_caps_get_largest_free_block(MALLOC_CAP_DMA | MALLOC_CAP_8BIT)); if (ip_err != ESP_OK) { ESP_LOGW(TAG, "DNS diagnostics STA IP query failed: %s", esp_err_to_name(ip_err)); } if (attempt == 1) { run_dns_failure_network_probes(ip_err == ESP_OK ? &ip : NULL, fallback_err == ESP_OK ? &dns_fallback : NULL); } } #endif static int effective_positive_or_zero(int value) { return value > 0 ? value : 0; } static const char *dns_error_name(int err) { switch (err) { #ifdef EAI_NONAME case EAI_NONAME: return "EAI_NONAME"; #endif #ifdef EAI_SERVICE case EAI_SERVICE: return "EAI_SERVICE"; #endif #ifdef EAI_FAIL case EAI_FAIL: return "EAI_FAIL"; #endif #ifdef EAI_MEMORY case EAI_MEMORY: return "EAI_MEMORY"; #endif #ifdef EAI_FAMILY case EAI_FAMILY: return "EAI_FAMILY"; #endif default: return "EAI_UNKNOWN"; } } static esp_err_t parse_url_host(const char *url, char *host, size_t host_capacity) { /* 仅提取 DNS 主机部分,同时兼容带方括号的 IPv6 字面量。 */ if (url == NULL || host == NULL || host_capacity == 0U) { return ESP_ERR_INVALID_ARG; } const char *host_begin = strstr(url, "://"); host_begin = host_begin != NULL ? host_begin + 3 : url; if (host_begin[0] == '\0') { return ESP_ERR_INVALID_ARG; } const char *host_end = NULL; if (host_begin[0] == '[') { host_begin++; host_end = strchr(host_begin, ']'); if (host_end == NULL) { return ESP_ERR_INVALID_ARG; } } else { host_end = host_begin; while (host_end[0] != '\0' && host_end[0] != '/' && host_end[0] != '?' && host_end[0] != '#' && host_end[0] != ':') { host_end++; } } size_t host_len = (size_t)(host_end - host_begin); if (host_len == 0U || host_len >= host_capacity) { return ESP_ERR_INVALID_ARG; } memcpy(host, host_begin, host_len); host[host_len] = '\0'; return ESP_OK; } static esp_err_t resolve_host_before_open(const char *url) { /* 在创建 HTTP client 前显式解析 DNS,便于提供稳定错误码和网络诊断。 */ char host[PLATFORM_HTTPS_HOST_MAX_LEN]; esp_err_t err = parse_url_host(url, host, sizeof(host)); if (err != ESP_OK) { return err; } #if CONFIG_TQ_NETWORK_DIAGNOSTICS_ENABLE uint32_t lookup_id = __atomic_add_fetch(&s_dns_lookup_count, 1U, __ATOMIC_RELAXED); (void)__atomic_add_fetch(&s_dns_lookup_active, 1U, __ATOMIC_RELAXED); #endif for (int attempt = 1; attempt <= PLATFORM_HTTPS_DNS_RETRY_COUNT; ++attempt) { struct addrinfo hints = { .ai_family = AF_UNSPEC, .ai_socktype = SOCK_STREAM, }; struct addrinfo *addr = NULL; #if CONFIG_TQ_NETWORK_DIAGNOSTICS_ENABLE int64_t attempt_started_us = esp_timer_get_time(); #endif int dns_err = getaddrinfo(host, NULL, &hints, &addr); if (dns_err == 0 && addr != NULL) { #if CONFIG_TQ_NETWORK_DIAGNOSTICS_ENABLE uint32_t elapsed_ms = (uint32_t)((esp_timer_get_time() - attempt_started_us) / 1000); cache_resolved_endpoint(host, addr); if (__atomic_load_n(&s_dns_lookup_failure_count, __ATOMIC_RELAXED) > 0U || elapsed_ms >= 1000U) { ESP_LOGI(TAG, "DNS resolved lookup=%u attempt=%d elapsed_ms=%u host=%s", (unsigned)lookup_id, attempt, (unsigned)elapsed_ms, host); } #endif freeaddrinfo(addr); #if CONFIG_TQ_NETWORK_DIAGNOSTICS_ENABLE (void)__atomic_sub_fetch(&s_dns_lookup_active, 1U, __ATOMIC_RELAXED); #endif return ESP_OK; } if (addr != NULL) { freeaddrinfo(addr); } #if CONFIG_TQ_NETWORK_DIAGNOSTICS_ENABLE (void)__atomic_add_fetch(&s_dns_lookup_failure_count, 1U, __ATOMIC_RELAXED); uint32_t elapsed_ms = (uint32_t)((esp_timer_get_time() - attempt_started_us) / 1000); log_dns_failure_diagnostics(host, attempt, dns_err, lookup_id, elapsed_ms); #endif if (dns_err == EAI_MEMORY) { ESP_LOGW(TAG, "DNS resolve failed host=%s err=%s(%d)", host, dns_error_name(dns_err), dns_err); #if CONFIG_TQ_NETWORK_DIAGNOSTICS_ENABLE (void)__atomic_sub_fetch(&s_dns_lookup_active, 1U, __ATOMIC_RELAXED); #endif return ESP_ERR_NO_MEM; } if (attempt < PLATFORM_HTTPS_DNS_RETRY_COUNT) { ESP_LOGW(TAG, "DNS resolve failed host=%s err=%s(%d), retrying", host, dns_error_name(dns_err), dns_err); vTaskDelay(pdMS_TO_TICKS(PLATFORM_HTTPS_DNS_RETRY_DELAY_MS)); } else { ESP_LOGW(TAG, "DNS resolve failed host=%s err=%s(%d) after %d attempts", host, dns_error_name(dns_err), dns_err, PLATFORM_HTTPS_DNS_RETRY_COUNT); } } #if CONFIG_TQ_NETWORK_DIAGNOSTICS_ENABLE (void)__atomic_sub_fetch(&s_dns_lookup_active, 1U, __ATOMIC_RELAXED); #endif return ESP_ERR_HTTP_CONNECT; } static esp_err_t write_all(esp_http_client_handle_t client, const char *payload) { /* esp_http_client_write 允许短写,循环直到完整 payload 发出或明确失败。 */ size_t payload_len = strlen(payload); if (payload_len > (size_t)INT_MAX) { return ESP_ERR_INVALID_SIZE; } size_t total_written = 0U; while (total_written < payload_len) { int written = esp_http_client_write(client, payload + total_written, (int)(payload_len - total_written)); if (written <= 0) { return ESP_FAIL; } total_written += (size_t)written; } return ESP_OK; } esp_err_t platform_https_post_json(const platform_https_post_json_request_t *request, platform_https_response_t *response) { /* 先清空调用方响应快照,确保任何提前失败都不会暴露上次请求的旧数据。 */ if (response != NULL) { response->body_len = 0U; response->status_code = -1; if (response->body != NULL && response->body_capacity > 0U) { response->body[0] = '\0'; } } if (request == NULL || response == NULL || request->url == NULL || request->url[0] == '\0' || request->payload == NULL || response->body == NULL || response->body_capacity < 2U || (request->header_count > 0U && request->headers == NULL)) { return ESP_ERR_INVALID_ARG; } esp_err_t err = resolve_host_before_open(request->url); if (err != ESP_OK) { return err; } esp_http_client_config_t cfg = { .url = request->url, .method = HTTP_METHOD_POST, .timeout_ms = request->timeout_ms > 0 ? request->timeout_ms : PLATFORM_HTTPS_DEFAULT_TIMEOUT_MS, .crt_bundle_attach = esp_crt_bundle_attach, .buffer_size_tx = effective_positive_or_zero(request->tx_buffer_size), .buffer_size = effective_positive_or_zero(request->rx_buffer_size), }; esp_http_client_handle_t client = esp_http_client_init(&cfg); if (client == NULL) { return ESP_ERR_NO_MEM; } esp_http_client_set_header(client, "Content-Type", "application/json"); for (size_t i = 0; i < request->header_count; ++i) { const platform_https_header_t *header = &request->headers[i]; if (header->name == NULL || header->name[0] == '\0' || header->value == NULL) { esp_http_client_cleanup(client); return ESP_ERR_INVALID_ARG; } esp_http_client_set_header(client, header->name, header->value); } size_t payload_len = strlen(request->payload); if (payload_len > (size_t)INT_MAX) { esp_http_client_cleanup(client); return ESP_ERR_INVALID_SIZE; } err = esp_http_client_open(client, (int)payload_len); if (err != ESP_OK) { ESP_LOGW(TAG, "HTTPS open failed: %s", esp_err_to_name(err)); esp_http_client_cleanup(client); return err; } err = write_all(client, request->payload); if (err != ESP_OK) { ESP_LOGW(TAG, "HTTPS write failed: %s", esp_err_to_name(err)); esp_http_client_close(client); esp_http_client_cleanup(client); return err; } int64_t content_length = esp_http_client_fetch_headers(client); int status = esp_http_client_get_status_code(client); response->status_code = status; size_t max_read = response->body_capacity - 1U; if (content_length >= 0 && (uint64_t)content_length > (uint64_t)max_read) { esp_http_client_close(client); esp_http_client_cleanup(client); return ESP_ERR_INVALID_SIZE; } size_t total_read = 0U; while (true) { if (total_read >= max_read) { uint8_t overflow = 0U; int overflow_len = esp_http_client_read(client, (char *)&overflow, 1); if (overflow_len > 0) { esp_http_client_close(client); esp_http_client_cleanup(client); return ESP_ERR_INVALID_SIZE; } if (overflow_len < 0) { esp_http_client_close(client); esp_http_client_cleanup(client); return ESP_ERR_INVALID_RESPONSE; } break; } int read_len = esp_http_client_read(client, response->body + total_read, (int)(max_read - total_read)); if (read_len < 0) { esp_http_client_close(client); esp_http_client_cleanup(client); return ESP_ERR_INVALID_RESPONSE; } if (read_len == 0) { break; } total_read += (size_t)read_len; } esp_http_client_close(client); esp_http_client_cleanup(client); response->body[total_read] = '\0'; response->body_len = total_read; return ESP_OK; } bool platform_https_is_connect_error(esp_err_t err) { return err == ESP_ERR_HTTP_CONNECT; }