683 lines
24 KiB
C
683 lines
24 KiB
C
/*
|
|
* SPDX-FileCopyrightText: 2024-2026 Espressif Systems (Shanghai) CO LTD
|
|
*
|
|
* SPDX-License-Identifier: Apache-2.0
|
|
*/
|
|
|
|
#include "platform.h"
|
|
|
|
#include <limits.h>
|
|
#include <netdb.h>
|
|
#include <stdint.h>
|
|
#include <string.h>
|
|
#include <sys/socket.h>
|
|
|
|
#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 <errno.h>
|
|
#include <fcntl.h>
|
|
#include <stdio.h>
|
|
|
|
#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;
|
|
}
|