feat: add HTTP CONNECT proxy support for Telegram and Claude API
Enable ESP32-S3 to reach api.telegram.org and api.anthropic.com through an HTTP CONNECT proxy (e.g. Clash Verge), required in regions where these services are blocked. - New proxy module (http_proxy.c/h): CONNECT tunnel + TLS via esp_tls with pre-connected socket injection (esp_tls_set_conn_sockfd) - Telegram and LLM modules split into direct/proxy paths - CLI commands: set_proxy <host> <port>, clear_proxy - Proxy config persisted in NVS - Fix TLS buffer: MBEDTLS_SSL_IN_CONTENT_LEN 4096 → 16384 - Increase task stacks for TLS overhead (poll 12KB, agent 12KB, outbound 8KB) - Default model changed to claude-opus-4-6 - Capture raw error body for non-200 API responses Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -12,6 +12,7 @@ idf_component_register(
|
|||||||
"gateway/ws_server.c"
|
"gateway/ws_server.c"
|
||||||
"cli/serial_cli.c"
|
"cli/serial_cli.c"
|
||||||
"ota/ota_manager.c"
|
"ota/ota_manager.c"
|
||||||
|
"proxy/http_proxy.c"
|
||||||
INCLUDE_DIRS
|
INCLUDE_DIRS
|
||||||
"."
|
"."
|
||||||
REQUIRES
|
REQUIRES
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
#include "llm/llm_proxy.h"
|
#include "llm/llm_proxy.h"
|
||||||
#include "memory/memory_store.h"
|
#include "memory/memory_store.h"
|
||||||
#include "memory/session_mgr.h"
|
#include "memory/session_mgr.h"
|
||||||
|
#include "proxy/http_proxy.h"
|
||||||
|
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
@@ -169,6 +170,33 @@ static int cmd_heap_info(int argc, char **argv)
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* --- set_proxy command --- */
|
||||||
|
static struct {
|
||||||
|
struct arg_str *host;
|
||||||
|
struct arg_int *port;
|
||||||
|
struct arg_end *end;
|
||||||
|
} proxy_args;
|
||||||
|
|
||||||
|
static int cmd_set_proxy(int argc, char **argv)
|
||||||
|
{
|
||||||
|
int nerrors = arg_parse(argc, argv, (void **)&proxy_args);
|
||||||
|
if (nerrors != 0) {
|
||||||
|
arg_print_errors(stderr, proxy_args.end, argv[0]);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
http_proxy_set(proxy_args.host->sval[0], (uint16_t)proxy_args.port->ival[0]);
|
||||||
|
printf("Proxy set. Restart to apply.\n");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- clear_proxy command --- */
|
||||||
|
static int cmd_clear_proxy(int argc, char **argv)
|
||||||
|
{
|
||||||
|
http_proxy_clear();
|
||||||
|
printf("Proxy cleared. Restart to apply.\n");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
/* --- restart command --- */
|
/* --- restart command --- */
|
||||||
static int cmd_restart(int argc, char **argv)
|
static int cmd_restart(int argc, char **argv)
|
||||||
{
|
{
|
||||||
@@ -291,6 +319,26 @@ esp_err_t serial_cli_init(void)
|
|||||||
};
|
};
|
||||||
esp_console_cmd_register(&heap_cmd);
|
esp_console_cmd_register(&heap_cmd);
|
||||||
|
|
||||||
|
/* set_proxy */
|
||||||
|
proxy_args.host = arg_str1(NULL, NULL, "<host>", "Proxy host/IP");
|
||||||
|
proxy_args.port = arg_int1(NULL, NULL, "<port>", "Proxy port");
|
||||||
|
proxy_args.end = arg_end(2);
|
||||||
|
esp_console_cmd_t proxy_cmd = {
|
||||||
|
.command = "set_proxy",
|
||||||
|
.help = "Set HTTP proxy (e.g. set_proxy 192.168.1.83 7897)",
|
||||||
|
.func = &cmd_set_proxy,
|
||||||
|
.argtable = &proxy_args,
|
||||||
|
};
|
||||||
|
esp_console_cmd_register(&proxy_cmd);
|
||||||
|
|
||||||
|
/* clear_proxy */
|
||||||
|
esp_console_cmd_t clear_proxy_cmd = {
|
||||||
|
.command = "clear_proxy",
|
||||||
|
.help = "Remove proxy configuration",
|
||||||
|
.func = &cmd_clear_proxy,
|
||||||
|
};
|
||||||
|
esp_console_cmd_register(&clear_proxy_cmd);
|
||||||
|
|
||||||
/* restart */
|
/* restart */
|
||||||
esp_console_cmd_t restart_cmd = {
|
esp_console_cmd_t restart_cmd = {
|
||||||
.command = "restart",
|
.command = "restart",
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
#include "llm_proxy.h"
|
#include "llm_proxy.h"
|
||||||
#include "mimi_config.h"
|
#include "mimi_config.h"
|
||||||
|
#include "proxy/http_proxy.h"
|
||||||
|
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
@@ -131,6 +132,110 @@ esp_err_t llm_proxy_init(void)
|
|||||||
return ESP_OK;
|
return ESP_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ── Direct path: esp_http_client ───────────────────────────── */
|
||||||
|
|
||||||
|
static esp_err_t llm_chat_direct(const char *post_data, sse_ctx_t *ctx, int *out_status)
|
||||||
|
{
|
||||||
|
esp_http_client_config_t config = {
|
||||||
|
.url = MIMI_LLM_API_URL,
|
||||||
|
.event_handler = http_event_handler,
|
||||||
|
.user_data = ctx,
|
||||||
|
.timeout_ms = 120 * 1000,
|
||||||
|
.buffer_size = 4096,
|
||||||
|
.buffer_size_tx = 4096,
|
||||||
|
.crt_bundle_attach = esp_crt_bundle_attach,
|
||||||
|
};
|
||||||
|
|
||||||
|
esp_http_client_handle_t client = esp_http_client_init(&config);
|
||||||
|
if (!client) return ESP_FAIL;
|
||||||
|
|
||||||
|
esp_http_client_set_method(client, HTTP_METHOD_POST);
|
||||||
|
esp_http_client_set_header(client, "Content-Type", "application/json");
|
||||||
|
esp_http_client_set_header(client, "x-api-key", s_api_key);
|
||||||
|
esp_http_client_set_header(client, "anthropic-version", MIMI_LLM_API_VERSION);
|
||||||
|
esp_http_client_set_post_field(client, post_data, strlen(post_data));
|
||||||
|
|
||||||
|
esp_err_t err = esp_http_client_perform(client);
|
||||||
|
*out_status = esp_http_client_get_status_code(client);
|
||||||
|
esp_http_client_cleanup(client);
|
||||||
|
return err;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Proxy path: manual HTTP over CONNECT tunnel ────────────── */
|
||||||
|
|
||||||
|
static esp_err_t llm_chat_via_proxy(const char *post_data, sse_ctx_t *ctx, int *out_status)
|
||||||
|
{
|
||||||
|
proxy_conn_t *conn = proxy_conn_open("api.anthropic.com", 443, 30000);
|
||||||
|
if (!conn) return ESP_ERR_HTTP_CONNECT;
|
||||||
|
|
||||||
|
/* Build HTTP request */
|
||||||
|
int body_len = strlen(post_data);
|
||||||
|
char header[512];
|
||||||
|
int hlen = snprintf(header, sizeof(header),
|
||||||
|
"POST /v1/messages HTTP/1.1\r\n"
|
||||||
|
"Host: api.anthropic.com\r\n"
|
||||||
|
"Content-Type: application/json\r\n"
|
||||||
|
"x-api-key: %s\r\n"
|
||||||
|
"anthropic-version: %s\r\n"
|
||||||
|
"Content-Length: %d\r\n"
|
||||||
|
"Connection: close\r\n\r\n",
|
||||||
|
s_api_key, MIMI_LLM_API_VERSION, body_len);
|
||||||
|
|
||||||
|
if (proxy_conn_write(conn, header, hlen) < 0 ||
|
||||||
|
proxy_conn_write(conn, post_data, body_len) < 0) {
|
||||||
|
proxy_conn_close(conn);
|
||||||
|
return ESP_ERR_HTTP_WRITE_DATA;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Read response — first line is status */
|
||||||
|
size_t raw_len = 0;
|
||||||
|
size_t raw_cap = 32768;
|
||||||
|
char *raw = calloc(1, raw_cap);
|
||||||
|
if (!raw) { proxy_conn_close(conn); return ESP_ERR_NO_MEM; }
|
||||||
|
|
||||||
|
while (1) {
|
||||||
|
if (raw_len + 4096 >= raw_cap) {
|
||||||
|
raw_cap *= 2;
|
||||||
|
char *tmp = realloc(raw, raw_cap);
|
||||||
|
if (!tmp) break;
|
||||||
|
raw = tmp;
|
||||||
|
}
|
||||||
|
int n = proxy_conn_read(conn, raw + raw_len, 4096, 120000);
|
||||||
|
if (n <= 0) break;
|
||||||
|
raw_len += n;
|
||||||
|
}
|
||||||
|
raw[raw_len] = '\0';
|
||||||
|
proxy_conn_close(conn);
|
||||||
|
|
||||||
|
/* Parse status line */
|
||||||
|
*out_status = 0;
|
||||||
|
if (strncmp(raw, "HTTP/", 5) == 0) {
|
||||||
|
const char *sp = strchr(raw, ' ');
|
||||||
|
if (sp) *out_status = atoi(sp + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Find body after \r\n\r\n */
|
||||||
|
char *body = strstr(raw, "\r\n\r\n");
|
||||||
|
if (body) {
|
||||||
|
body += 4;
|
||||||
|
size_t body_len = raw_len - (body - raw);
|
||||||
|
if (*out_status == 200) {
|
||||||
|
/* Feed body to SSE parser */
|
||||||
|
sse_feed(ctx, body, body_len);
|
||||||
|
} else {
|
||||||
|
/* For error responses, capture raw body */
|
||||||
|
size_t copy_len = body_len < ctx->resp_cap - 1 ? body_len : ctx->resp_cap - 1;
|
||||||
|
memcpy(ctx->response, body, copy_len);
|
||||||
|
ctx->response[copy_len] = '\0';
|
||||||
|
ctx->resp_len = copy_len;
|
||||||
|
ESP_LOGE(TAG, "API error body: %.500s", body);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
free(raw);
|
||||||
|
return ESP_OK;
|
||||||
|
}
|
||||||
|
|
||||||
esp_err_t llm_chat(const char *system_prompt, const char *messages_json,
|
esp_err_t llm_chat(const char *system_prompt, const char *messages_json,
|
||||||
char *response_buf, size_t buf_size)
|
char *response_buf, size_t buf_size)
|
||||||
{
|
{
|
||||||
@@ -182,33 +287,14 @@ esp_err_t llm_chat(const char *system_prompt, const char *messages_json,
|
|||||||
return ESP_ERR_NO_MEM;
|
return ESP_ERR_NO_MEM;
|
||||||
}
|
}
|
||||||
|
|
||||||
esp_http_client_config_t config = {
|
esp_err_t err;
|
||||||
.url = MIMI_LLM_API_URL,
|
int status = 0;
|
||||||
.event_handler = http_event_handler,
|
|
||||||
.user_data = &ctx,
|
|
||||||
.timeout_ms = 120 * 1000, /* 2 min timeout for long responses */
|
|
||||||
.buffer_size = 4096,
|
|
||||||
.buffer_size_tx = 4096,
|
|
||||||
.crt_bundle_attach = esp_crt_bundle_attach,
|
|
||||||
};
|
|
||||||
|
|
||||||
esp_http_client_handle_t client = esp_http_client_init(&config);
|
if (http_proxy_is_enabled()) {
|
||||||
if (!client) {
|
err = llm_chat_via_proxy(post_data, &ctx, &status);
|
||||||
free(post_data);
|
} else {
|
||||||
free(ctx.response);
|
err = llm_chat_direct(post_data, &ctx, &status);
|
||||||
snprintf(response_buf, buf_size, "Error: HTTP client init failed");
|
|
||||||
return ESP_FAIL;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
esp_http_client_set_method(client, HTTP_METHOD_POST);
|
|
||||||
esp_http_client_set_header(client, "Content-Type", "application/json");
|
|
||||||
esp_http_client_set_header(client, "x-api-key", s_api_key);
|
|
||||||
esp_http_client_set_header(client, "anthropic-version", MIMI_LLM_API_VERSION);
|
|
||||||
esp_http_client_set_post_field(client, post_data, strlen(post_data));
|
|
||||||
|
|
||||||
esp_err_t err = esp_http_client_perform(client);
|
|
||||||
int status = esp_http_client_get_status_code(client);
|
|
||||||
esp_http_client_cleanup(client);
|
|
||||||
free(post_data);
|
free(post_data);
|
||||||
|
|
||||||
if (err != ESP_OK) {
|
if (err != ESP_OK) {
|
||||||
@@ -221,7 +307,6 @@ esp_err_t llm_chat(const char *system_prompt, const char *messages_json,
|
|||||||
if (status != 200) {
|
if (status != 200) {
|
||||||
ESP_LOGE(TAG, "API returned status %d", status);
|
ESP_LOGE(TAG, "API returned status %d", status);
|
||||||
if (ctx.resp_len > 0) {
|
if (ctx.resp_len > 0) {
|
||||||
/* Response might contain error info */
|
|
||||||
snprintf(response_buf, buf_size, "API error (HTTP %d): %.200s", status, ctx.response);
|
snprintf(response_buf, buf_size, "API error (HTTP %d): %.200s", status, ctx.response);
|
||||||
} else {
|
} else {
|
||||||
snprintf(response_buf, buf_size, "API error (HTTP %d)", status);
|
snprintf(response_buf, buf_size, "API error (HTTP %d)", status);
|
||||||
|
|||||||
@@ -19,6 +19,7 @@
|
|||||||
#include "memory/session_mgr.h"
|
#include "memory/session_mgr.h"
|
||||||
#include "gateway/ws_server.h"
|
#include "gateway/ws_server.h"
|
||||||
#include "cli/serial_cli.h"
|
#include "cli/serial_cli.h"
|
||||||
|
#include "proxy/http_proxy.h"
|
||||||
|
|
||||||
static const char *TAG = "mimi";
|
static const char *TAG = "mimi";
|
||||||
|
|
||||||
@@ -100,6 +101,7 @@ void app_main(void)
|
|||||||
ESP_ERROR_CHECK(memory_store_init());
|
ESP_ERROR_CHECK(memory_store_init());
|
||||||
ESP_ERROR_CHECK(session_mgr_init());
|
ESP_ERROR_CHECK(session_mgr_init());
|
||||||
ESP_ERROR_CHECK(wifi_manager_init());
|
ESP_ERROR_CHECK(wifi_manager_init());
|
||||||
|
ESP_ERROR_CHECK(http_proxy_init());
|
||||||
ESP_ERROR_CHECK(telegram_bot_init());
|
ESP_ERROR_CHECK(telegram_bot_init());
|
||||||
ESP_ERROR_CHECK(llm_proxy_init());
|
ESP_ERROR_CHECK(llm_proxy_init());
|
||||||
ESP_ERROR_CHECK(agent_loop_init());
|
ESP_ERROR_CHECK(agent_loop_init());
|
||||||
|
|||||||
@@ -10,18 +10,18 @@
|
|||||||
/* Telegram Bot */
|
/* Telegram Bot */
|
||||||
#define MIMI_TG_POLL_TIMEOUT_S 30
|
#define MIMI_TG_POLL_TIMEOUT_S 30
|
||||||
#define MIMI_TG_MAX_MSG_LEN 4096
|
#define MIMI_TG_MAX_MSG_LEN 4096
|
||||||
#define MIMI_TG_POLL_STACK (8 * 1024)
|
#define MIMI_TG_POLL_STACK (12 * 1024)
|
||||||
#define MIMI_TG_POLL_PRIO 5
|
#define MIMI_TG_POLL_PRIO 5
|
||||||
#define MIMI_TG_POLL_CORE 0
|
#define MIMI_TG_POLL_CORE 0
|
||||||
|
|
||||||
/* Agent Loop */
|
/* Agent Loop */
|
||||||
#define MIMI_AGENT_STACK (8 * 1024)
|
#define MIMI_AGENT_STACK (12 * 1024)
|
||||||
#define MIMI_AGENT_PRIO 6
|
#define MIMI_AGENT_PRIO 6
|
||||||
#define MIMI_AGENT_CORE 1
|
#define MIMI_AGENT_CORE 1
|
||||||
#define MIMI_AGENT_MAX_HISTORY 20
|
#define MIMI_AGENT_MAX_HISTORY 20
|
||||||
|
|
||||||
/* LLM */
|
/* LLM */
|
||||||
#define MIMI_LLM_DEFAULT_MODEL "claude-opus-4-5-20251101"
|
#define MIMI_LLM_DEFAULT_MODEL "claude-opus-4-6"
|
||||||
#define MIMI_LLM_MAX_TOKENS 4096
|
#define MIMI_LLM_MAX_TOKENS 4096
|
||||||
#define MIMI_LLM_API_URL "https://api.anthropic.com/v1/messages"
|
#define MIMI_LLM_API_URL "https://api.anthropic.com/v1/messages"
|
||||||
#define MIMI_LLM_API_VERSION "2023-06-01"
|
#define MIMI_LLM_API_VERSION "2023-06-01"
|
||||||
@@ -29,7 +29,7 @@
|
|||||||
|
|
||||||
/* Message Bus */
|
/* Message Bus */
|
||||||
#define MIMI_BUS_QUEUE_LEN 8
|
#define MIMI_BUS_QUEUE_LEN 8
|
||||||
#define MIMI_OUTBOUND_STACK (4 * 1024)
|
#define MIMI_OUTBOUND_STACK (8 * 1024)
|
||||||
#define MIMI_OUTBOUND_PRIO 5
|
#define MIMI_OUTBOUND_PRIO 5
|
||||||
#define MIMI_OUTBOUND_CORE 0
|
#define MIMI_OUTBOUND_CORE 0
|
||||||
|
|
||||||
@@ -57,6 +57,7 @@
|
|||||||
#define MIMI_NVS_WIFI "wifi_config"
|
#define MIMI_NVS_WIFI "wifi_config"
|
||||||
#define MIMI_NVS_TG "tg_config"
|
#define MIMI_NVS_TG "tg_config"
|
||||||
#define MIMI_NVS_LLM "llm_config"
|
#define MIMI_NVS_LLM "llm_config"
|
||||||
|
#define MIMI_NVS_PROXY "proxy_config"
|
||||||
|
|
||||||
/* NVS Keys */
|
/* NVS Keys */
|
||||||
#define MIMI_NVS_KEY_SSID "ssid"
|
#define MIMI_NVS_KEY_SSID "ssid"
|
||||||
@@ -64,3 +65,5 @@
|
|||||||
#define MIMI_NVS_KEY_TG_TOKEN "bot_token"
|
#define MIMI_NVS_KEY_TG_TOKEN "bot_token"
|
||||||
#define MIMI_NVS_KEY_API_KEY "api_key"
|
#define MIMI_NVS_KEY_API_KEY "api_key"
|
||||||
#define MIMI_NVS_KEY_MODEL "model"
|
#define MIMI_NVS_KEY_MODEL "model"
|
||||||
|
#define MIMI_NVS_KEY_PROXY_HOST "host"
|
||||||
|
#define MIMI_NVS_KEY_PROXY_PORT "port"
|
||||||
|
|||||||
234
main/proxy/http_proxy.c
Normal file
234
main/proxy/http_proxy.c
Normal file
@@ -0,0 +1,234 @@
|
|||||||
|
#include "http_proxy.h"
|
||||||
|
#include "mimi_config.h"
|
||||||
|
|
||||||
|
#include <string.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <errno.h>
|
||||||
|
#include <sys/socket.h>
|
||||||
|
#include <netdb.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
|
||||||
|
#include "esp_log.h"
|
||||||
|
#include "nvs.h"
|
||||||
|
#include "esp_tls.h"
|
||||||
|
#include "esp_crt_bundle.h"
|
||||||
|
|
||||||
|
static const char *TAG = "proxy";
|
||||||
|
|
||||||
|
/* ── Config (cached from NVS) ─────────────────────────────────── */
|
||||||
|
|
||||||
|
static char s_proxy_host[64] = {0};
|
||||||
|
static uint16_t s_proxy_port = 0;
|
||||||
|
|
||||||
|
esp_err_t http_proxy_init(void)
|
||||||
|
{
|
||||||
|
nvs_handle_t nvs;
|
||||||
|
esp_err_t err = nvs_open(MIMI_NVS_PROXY, NVS_READONLY, &nvs);
|
||||||
|
if (err == ESP_OK) {
|
||||||
|
size_t len = sizeof(s_proxy_host);
|
||||||
|
nvs_get_str(nvs, MIMI_NVS_KEY_PROXY_HOST, s_proxy_host, &len);
|
||||||
|
nvs_get_u16(nvs, MIMI_NVS_KEY_PROXY_PORT, &s_proxy_port);
|
||||||
|
nvs_close(nvs);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (s_proxy_host[0] && s_proxy_port) {
|
||||||
|
ESP_LOGI(TAG, "Proxy configured: %s:%d", s_proxy_host, s_proxy_port);
|
||||||
|
} else {
|
||||||
|
ESP_LOGI(TAG, "No proxy configured (direct connection)");
|
||||||
|
}
|
||||||
|
return ESP_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool http_proxy_is_enabled(void)
|
||||||
|
{
|
||||||
|
return s_proxy_host[0] != '\0' && s_proxy_port != 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
esp_err_t http_proxy_set(const char *host, uint16_t port)
|
||||||
|
{
|
||||||
|
nvs_handle_t nvs;
|
||||||
|
ESP_ERROR_CHECK(nvs_open(MIMI_NVS_PROXY, NVS_READWRITE, &nvs));
|
||||||
|
ESP_ERROR_CHECK(nvs_set_str(nvs, MIMI_NVS_KEY_PROXY_HOST, host));
|
||||||
|
ESP_ERROR_CHECK(nvs_set_u16(nvs, MIMI_NVS_KEY_PROXY_PORT, port));
|
||||||
|
ESP_ERROR_CHECK(nvs_commit(nvs));
|
||||||
|
nvs_close(nvs);
|
||||||
|
|
||||||
|
strncpy(s_proxy_host, host, sizeof(s_proxy_host) - 1);
|
||||||
|
s_proxy_port = port;
|
||||||
|
ESP_LOGI(TAG, "Proxy set to %s:%d", s_proxy_host, s_proxy_port);
|
||||||
|
return ESP_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
esp_err_t http_proxy_clear(void)
|
||||||
|
{
|
||||||
|
nvs_handle_t nvs;
|
||||||
|
ESP_ERROR_CHECK(nvs_open(MIMI_NVS_PROXY, NVS_READWRITE, &nvs));
|
||||||
|
nvs_erase_key(nvs, MIMI_NVS_KEY_PROXY_HOST);
|
||||||
|
nvs_erase_key(nvs, MIMI_NVS_KEY_PROXY_PORT);
|
||||||
|
nvs_commit(nvs);
|
||||||
|
nvs_close(nvs);
|
||||||
|
|
||||||
|
s_proxy_host[0] = '\0';
|
||||||
|
s_proxy_port = 0;
|
||||||
|
ESP_LOGI(TAG, "Proxy cleared");
|
||||||
|
return ESP_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Proxied TLS connection ───────────────────────────────────── */
|
||||||
|
|
||||||
|
struct proxy_conn {
|
||||||
|
int sock; /* raw TCP socket (for timeout control) */
|
||||||
|
esp_tls_t *tls; /* esp_tls handle owns TLS + socket lifecycle */
|
||||||
|
};
|
||||||
|
|
||||||
|
/* Read a line from socket (up to CR-LF). Returns length or -1. */
|
||||||
|
static int sock_read_line(int fd, char *buf, int max, int timeout_ms)
|
||||||
|
{
|
||||||
|
int pos = 0;
|
||||||
|
struct timeval tv = { .tv_sec = timeout_ms / 1000, .tv_usec = (timeout_ms % 1000) * 1000 };
|
||||||
|
setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
|
||||||
|
|
||||||
|
while (pos < max - 1) {
|
||||||
|
char c;
|
||||||
|
int r = recv(fd, &c, 1, 0);
|
||||||
|
if (r <= 0) return -1;
|
||||||
|
if (c == '\n') { buf[pos] = '\0'; return pos; }
|
||||||
|
if (c != '\r') buf[pos++] = c;
|
||||||
|
}
|
||||||
|
buf[pos] = '\0';
|
||||||
|
return pos;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Open TCP + CONNECT tunnel, returns socket fd or -1 */
|
||||||
|
static int open_connect_tunnel(const char *host, int port, int timeout_ms)
|
||||||
|
{
|
||||||
|
struct addrinfo hints = { .ai_family = AF_INET, .ai_socktype = SOCK_STREAM };
|
||||||
|
struct addrinfo *res = NULL;
|
||||||
|
char port_str[8];
|
||||||
|
snprintf(port_str, sizeof(port_str), "%d", s_proxy_port);
|
||||||
|
|
||||||
|
if (getaddrinfo(s_proxy_host, port_str, &hints, &res) != 0 || !res) {
|
||||||
|
ESP_LOGE(TAG, "DNS resolve failed for proxy %s", s_proxy_host);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
int sock = socket(AF_INET, SOCK_STREAM, 0);
|
||||||
|
if (sock < 0) { freeaddrinfo(res); return -1; }
|
||||||
|
|
||||||
|
struct timeval tv = { .tv_sec = timeout_ms / 1000, .tv_usec = (timeout_ms % 1000) * 1000 };
|
||||||
|
setsockopt(sock, SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof(tv));
|
||||||
|
setsockopt(sock, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
|
||||||
|
|
||||||
|
if (connect(sock, res->ai_addr, res->ai_addrlen) != 0) {
|
||||||
|
ESP_LOGE(TAG, "TCP connect to proxy %s:%d failed", s_proxy_host, s_proxy_port);
|
||||||
|
freeaddrinfo(res); close(sock); return -1;
|
||||||
|
}
|
||||||
|
freeaddrinfo(res);
|
||||||
|
ESP_LOGI(TAG, "Connected to proxy %s:%d", s_proxy_host, s_proxy_port);
|
||||||
|
|
||||||
|
char req[256];
|
||||||
|
int len = snprintf(req, sizeof(req),
|
||||||
|
"CONNECT %s:%d HTTP/1.1\r\nHost: %s:%d\r\n\r\n", host, port, host, port);
|
||||||
|
|
||||||
|
if (send(sock, req, len, 0) != len) {
|
||||||
|
ESP_LOGE(TAG, "Failed to send CONNECT"); close(sock); return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
char line[256];
|
||||||
|
if (sock_read_line(sock, line, sizeof(line), timeout_ms) < 0) {
|
||||||
|
ESP_LOGE(TAG, "No response from proxy"); close(sock); return -1;
|
||||||
|
}
|
||||||
|
if (strstr(line, "200") == NULL) {
|
||||||
|
ESP_LOGE(TAG, "CONNECT rejected: %s", line); close(sock); return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Consume remaining response headers */
|
||||||
|
while (sock_read_line(sock, line, sizeof(line), timeout_ms) > 0) { }
|
||||||
|
|
||||||
|
ESP_LOGI(TAG, "CONNECT tunnel established to %s:%d", host, port);
|
||||||
|
return sock;
|
||||||
|
}
|
||||||
|
|
||||||
|
proxy_conn_t *proxy_conn_open(const char *host, int port, int timeout_ms)
|
||||||
|
{
|
||||||
|
if (!http_proxy_is_enabled()) {
|
||||||
|
ESP_LOGE(TAG, "proxy_conn_open called but no proxy configured");
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
int sock = open_connect_tunnel(host, port, timeout_ms);
|
||||||
|
if (sock < 0) return NULL;
|
||||||
|
|
||||||
|
proxy_conn_t *conn = calloc(1, sizeof(*conn));
|
||||||
|
if (!conn) { close(sock); return NULL; }
|
||||||
|
conn->sock = sock;
|
||||||
|
|
||||||
|
/* ── TLS handshake via esp_tls over tunnel ───────────────── */
|
||||||
|
conn->tls = esp_tls_init();
|
||||||
|
if (!conn->tls) {
|
||||||
|
ESP_LOGE(TAG, "esp_tls_init failed");
|
||||||
|
close(sock); free(conn); return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Inject our CONNECT-tunnel socket and skip TCP connect phase */
|
||||||
|
esp_tls_set_conn_sockfd(conn->tls, sock);
|
||||||
|
esp_tls_set_conn_state(conn->tls, ESP_TLS_CONNECTING);
|
||||||
|
|
||||||
|
esp_tls_cfg_t cfg = {
|
||||||
|
.crt_bundle_attach = esp_crt_bundle_attach,
|
||||||
|
.timeout_ms = timeout_ms,
|
||||||
|
};
|
||||||
|
|
||||||
|
int ret = esp_tls_conn_new_sync(host, strlen(host), port, &cfg, conn->tls);
|
||||||
|
if (ret <= 0) {
|
||||||
|
ESP_LOGE(TAG, "TLS handshake failed over proxy tunnel");
|
||||||
|
esp_tls_conn_destroy(conn->tls);
|
||||||
|
/* esp_tls_conn_destroy closes the socket */
|
||||||
|
free(conn);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
ESP_LOGI(TAG, "TLS handshake OK with %s:%d via proxy", host, port);
|
||||||
|
return conn;
|
||||||
|
}
|
||||||
|
|
||||||
|
int proxy_conn_write(proxy_conn_t *conn, const char *data, int len)
|
||||||
|
{
|
||||||
|
int written = 0;
|
||||||
|
while (written < len) {
|
||||||
|
ssize_t ret = esp_tls_conn_write(conn->tls, data + written, len - written);
|
||||||
|
if (ret > 0) {
|
||||||
|
written += (int)ret;
|
||||||
|
} else if (ret == ESP_TLS_ERR_SSL_WANT_WRITE) {
|
||||||
|
continue;
|
||||||
|
} else {
|
||||||
|
ESP_LOGE(TAG, "esp_tls_conn_write error: %d", (int)ret);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return written;
|
||||||
|
}
|
||||||
|
|
||||||
|
int proxy_conn_read(proxy_conn_t *conn, char *buf, int len, int timeout_ms)
|
||||||
|
{
|
||||||
|
struct timeval tv = { .tv_sec = timeout_ms / 1000, .tv_usec = (timeout_ms % 1000) * 1000 };
|
||||||
|
setsockopt(conn->sock, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
|
||||||
|
|
||||||
|
ssize_t ret = esp_tls_conn_read(conn->tls, buf, len);
|
||||||
|
if (ret == ESP_TLS_ERR_SSL_WANT_READ) return 0;
|
||||||
|
if (ret == 0) return 0;
|
||||||
|
if (ret < 0) {
|
||||||
|
ESP_LOGE(TAG, "esp_tls_conn_read error: %d", (int)ret);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
return (int)ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
void proxy_conn_close(proxy_conn_t *conn)
|
||||||
|
{
|
||||||
|
if (!conn) return;
|
||||||
|
if (conn->tls) {
|
||||||
|
esp_tls_conn_destroy(conn->tls);
|
||||||
|
}
|
||||||
|
free(conn);
|
||||||
|
}
|
||||||
48
main/proxy/http_proxy.h
Normal file
48
main/proxy/http_proxy.h
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "esp_err.h"
|
||||||
|
#include <stddef.h>
|
||||||
|
#include <stdbool.h>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize proxy module — loads config from NVS.
|
||||||
|
*/
|
||||||
|
esp_err_t http_proxy_init(void);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true if a proxy host:port is configured.
|
||||||
|
*/
|
||||||
|
bool http_proxy_is_enabled(void);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save proxy host and port to NVS.
|
||||||
|
*/
|
||||||
|
esp_err_t http_proxy_set(const char *host, uint16_t port);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove proxy config from NVS.
|
||||||
|
*/
|
||||||
|
esp_err_t http_proxy_clear(void);
|
||||||
|
|
||||||
|
/* ── Proxied HTTPS connection ─────────────────────────────────── */
|
||||||
|
|
||||||
|
typedef struct proxy_conn proxy_conn_t;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Open an HTTPS connection through the configured proxy.
|
||||||
|
* 1) TCP connect to proxy
|
||||||
|
* 2) Send HTTP CONNECT to target host:port
|
||||||
|
* 3) TLS handshake over the tunnel
|
||||||
|
*
|
||||||
|
* Returns NULL on failure.
|
||||||
|
*/
|
||||||
|
proxy_conn_t *proxy_conn_open(const char *host, int port, int timeout_ms);
|
||||||
|
|
||||||
|
/** Write raw bytes through the TLS tunnel. Returns bytes written or -1. */
|
||||||
|
int proxy_conn_write(proxy_conn_t *conn, const char *data, int len);
|
||||||
|
|
||||||
|
/** Read raw bytes from the TLS tunnel. Returns bytes read or -1. */
|
||||||
|
int proxy_conn_read(proxy_conn_t *conn, char *buf, int len, int timeout_ms);
|
||||||
|
|
||||||
|
/** Close and free the connection. */
|
||||||
|
void proxy_conn_close(proxy_conn_t *conn);
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
#include "telegram_bot.h"
|
#include "telegram_bot.h"
|
||||||
#include "mimi_config.h"
|
#include "mimi_config.h"
|
||||||
#include "bus/message_bus.h"
|
#include "bus/message_bus.h"
|
||||||
|
#include "proxy/http_proxy.h"
|
||||||
|
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
@@ -43,7 +44,76 @@ static esp_err_t http_event_handler(esp_http_client_event_t *evt)
|
|||||||
return ESP_OK;
|
return ESP_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
static char *tg_api_call(const char *method, const char *post_data)
|
/* ── Proxy path: manual HTTP over CONNECT tunnel ────────────── */
|
||||||
|
|
||||||
|
static char *tg_api_call_via_proxy(const char *path, const char *post_data)
|
||||||
|
{
|
||||||
|
proxy_conn_t *conn = proxy_conn_open("api.telegram.org", 443,
|
||||||
|
(MIMI_TG_POLL_TIMEOUT_S + 5) * 1000);
|
||||||
|
if (!conn) return NULL;
|
||||||
|
|
||||||
|
/* Build HTTP request */
|
||||||
|
char header[512];
|
||||||
|
int hlen;
|
||||||
|
if (post_data) {
|
||||||
|
hlen = snprintf(header, sizeof(header),
|
||||||
|
"POST /bot%s/%s HTTP/1.1\r\n"
|
||||||
|
"Host: api.telegram.org\r\n"
|
||||||
|
"Content-Type: application/json\r\n"
|
||||||
|
"Content-Length: %d\r\n"
|
||||||
|
"Connection: close\r\n\r\n",
|
||||||
|
s_bot_token, path, (int)strlen(post_data));
|
||||||
|
} else {
|
||||||
|
hlen = snprintf(header, sizeof(header),
|
||||||
|
"GET /bot%s/%s HTTP/1.1\r\n"
|
||||||
|
"Host: api.telegram.org\r\n"
|
||||||
|
"Connection: close\r\n\r\n",
|
||||||
|
s_bot_token, path);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (proxy_conn_write(conn, header, hlen) < 0) {
|
||||||
|
proxy_conn_close(conn);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
if (post_data && proxy_conn_write(conn, post_data, strlen(post_data)) < 0) {
|
||||||
|
proxy_conn_close(conn);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Read response — accumulate until connection close */
|
||||||
|
size_t cap = 4096, len = 0;
|
||||||
|
char *buf = calloc(1, cap);
|
||||||
|
if (!buf) { proxy_conn_close(conn); return NULL; }
|
||||||
|
|
||||||
|
int timeout = (MIMI_TG_POLL_TIMEOUT_S + 5) * 1000;
|
||||||
|
while (1) {
|
||||||
|
if (len + 1024 >= cap) {
|
||||||
|
cap *= 2;
|
||||||
|
char *tmp = realloc(buf, cap);
|
||||||
|
if (!tmp) break;
|
||||||
|
buf = tmp;
|
||||||
|
}
|
||||||
|
int n = proxy_conn_read(conn, buf + len, cap - len - 1, timeout);
|
||||||
|
if (n <= 0) break;
|
||||||
|
len += n;
|
||||||
|
}
|
||||||
|
buf[len] = '\0';
|
||||||
|
proxy_conn_close(conn);
|
||||||
|
|
||||||
|
/* Skip HTTP headers — find \r\n\r\n */
|
||||||
|
char *body = strstr(buf, "\r\n\r\n");
|
||||||
|
if (!body) { free(buf); return NULL; }
|
||||||
|
body += 4;
|
||||||
|
|
||||||
|
/* Return just the body */
|
||||||
|
char *result = strdup(body);
|
||||||
|
free(buf);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Direct path: esp_http_client ───────────────────────────── */
|
||||||
|
|
||||||
|
static char *tg_api_call_direct(const char *method, const char *post_data)
|
||||||
{
|
{
|
||||||
char url[256];
|
char url[256];
|
||||||
snprintf(url, sizeof(url), "https://api.telegram.org/bot%s/%s", s_bot_token, method);
|
snprintf(url, sizeof(url), "https://api.telegram.org/bot%s/%s", s_bot_token, method);
|
||||||
@@ -89,6 +159,14 @@ static char *tg_api_call(const char *method, const char *post_data)
|
|||||||
return resp.buf;
|
return resp.buf;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static char *tg_api_call(const char *method, const char *post_data)
|
||||||
|
{
|
||||||
|
if (http_proxy_is_enabled()) {
|
||||||
|
return tg_api_call_via_proxy(method, post_data);
|
||||||
|
}
|
||||||
|
return tg_api_call_direct(method, post_data);
|
||||||
|
}
|
||||||
|
|
||||||
static void process_updates(const char *json_str)
|
static void process_updates(const char *json_str)
|
||||||
{
|
{
|
||||||
cJSON *root = cJSON_Parse(json_str);
|
cJSON *root = cJSON_Parse(json_str);
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ CONFIG_LWIP_TCPIP_RECVMBOX_SIZE=16
|
|||||||
# TLS optimization (PSRAM allocation + small buffers)
|
# TLS optimization (PSRAM allocation + small buffers)
|
||||||
CONFIG_MBEDTLS_EXTERNAL_MEM_ALLOC=y
|
CONFIG_MBEDTLS_EXTERNAL_MEM_ALLOC=y
|
||||||
CONFIG_MBEDTLS_DYNAMIC_FREE_CONFIG_DATA=y
|
CONFIG_MBEDTLS_DYNAMIC_FREE_CONFIG_DATA=y
|
||||||
CONFIG_MBEDTLS_SSL_IN_CONTENT_LEN=4096
|
CONFIG_MBEDTLS_SSL_IN_CONTENT_LEN=16384
|
||||||
CONFIG_MBEDTLS_SSL_OUT_CONTENT_LEN=4096
|
CONFIG_MBEDTLS_SSL_OUT_CONTENT_LEN=4096
|
||||||
|
|
||||||
# WebSocket support
|
# WebSocket support
|
||||||
|
|||||||
Reference in New Issue
Block a user