Based on https://github.com/tslabs/zx-evo/tree/master/pentevo/esp32, extended with an SMB2/3 server that exposes the ZX Evolution's real SD card as a network share (browse/download/upload/delete), plus assorted stability and protocol-correctness fixes in vfs_bridge.cpp and the vendored libsmb2 - see firmware/README.md for the detailed changelog and current status. Also adds: - firmware/: ready-to-flash compiled binaries + flashing instructions - hardware/: adapter board connection diagram, PCB photo, and Gerber files - src/third_party/libsmb2: vendored from sahlberg/libsmb2 (LGPL-2.1, see its own COPYING/LICENCE files), de-submoduled to a plain snapshot with local patches applied directly Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
315 lines
9.7 KiB
C++
315 lines
9.7 KiB
C++
#include <stdio.h>
|
|
#include <string.h>
|
|
#include <strings.h>
|
|
#include <sys/socket.h>
|
|
#include <sys/time.h>
|
|
#include <netdb.h>
|
|
#include <netinet/in.h>
|
|
#include <unistd.h>
|
|
#include <fcntl.h>
|
|
#include <errno.h>
|
|
|
|
#include "freertos/FreeRTOS.h"
|
|
#include "freertos/task.h"
|
|
#include "esp_log.h"
|
|
#include "esp_tls.h"
|
|
#include "esp_crt_bundle.h"
|
|
|
|
#include "main.h"
|
|
#include "at_uart.h"
|
|
#include "at_cip.h"
|
|
#include "wifi.h"
|
|
|
|
static const char *TAG = "at_cip";
|
|
|
|
static int cip_sock = -1;
|
|
static int cip_type = CIP_TYPE_NONE;
|
|
static char cip_host[64];
|
|
static int cip_port = 0;
|
|
static esp_tls_t *cip_tls = NULL;
|
|
|
|
void at_cip_init()
|
|
{
|
|
cip_sock = -1;
|
|
cip_type = CIP_TYPE_NONE;
|
|
cip_host[0] = 0;
|
|
cip_port = 0;
|
|
cip_tls = NULL;
|
|
}
|
|
|
|
bool at_cip_is_connected()
|
|
{
|
|
return cip_sock >= 0 || cip_tls != NULL;
|
|
}
|
|
|
|
void at_cip_status_line(char *buf, size_t buflen)
|
|
{
|
|
if (!at_cip_is_connected())
|
|
{
|
|
snprintf(buf, buflen, "STATUS:0");
|
|
return;
|
|
}
|
|
|
|
const char *type_str = cip_type == CIP_TYPE_UDP ? "UDP" : cip_type == CIP_TYPE_SSL ? "SSL" : "TCP";
|
|
snprintf(buf, buflen, "STATUS:3\r\n+CIPSTATUS:0,\"%s\",\"%s\",%d,0,0", type_str, cip_host, cip_port);
|
|
}
|
|
|
|
// Resolves and connects a raw socket via the plain BSD gethostbyname()/connect()
|
|
// path. Shared by plain TCP and (as of the SSL support below) also by the TLS
|
|
// path: esp_tls's own built-in connect step goes through getaddrinfo() instead,
|
|
// which crashes (LoadProhibited, NULL FreeRTOS queue handle deep inside lwIP's
|
|
// tcpip_task message-passing - confirmed via backtrace against esp-spi.elf,
|
|
// root cause not fully understood) the first time it's exercised from this
|
|
// task. gethostbyname() uses a different, older lwIP DNS code path that has
|
|
// always worked fine here, so SSL connects through it too and only hands
|
|
// esp_tls an already-open socket, skipping esp_tls's DNS/connect step entirely.
|
|
static int at_cip_connect_raw_tcp(int sock_type, const char *host, int port, bool set_rcv_timeout)
|
|
{
|
|
// gethostbyname() (and, for the SSL caller, esp_tls's own getaddrinfo()
|
|
// fallback below) both post through tcpip_task's mailbox - if this is the
|
|
// very first network command since boot (no AT+CWJAP/AT+CWLAP yet), that
|
|
// mailbox doesn't exist yet and the post crashes with a NULL-queue-handle
|
|
// LoadProhibited (EXCVADDR=0x54, confirmed via console backtrace: reproduced
|
|
// with a plain AT+CIPSTART="TCP" to a literal IP right after a fresh boot,
|
|
// fixed by ensuring the WiFi stack is up first, same lazy-init used by
|
|
// AT+CWLAP - safe to call again if already initialized).
|
|
if (!net_mtx)
|
|
initialize_wifi();
|
|
|
|
struct hostent *server = gethostbyname(host);
|
|
if (!server)
|
|
{
|
|
ESP_LOGW(TAG, "DNS lookup failed for '%s'", host);
|
|
return -1;
|
|
}
|
|
|
|
int sock = socket(AF_INET, sock_type, 0);
|
|
if (sock < 0)
|
|
{
|
|
ESP_LOGW(TAG, "socket() failed");
|
|
return -1;
|
|
}
|
|
|
|
struct sockaddr_in addr;
|
|
memset(&addr, 0, sizeof(addr));
|
|
addr.sin_family = AF_INET;
|
|
addr.sin_port = htons(port);
|
|
memcpy(&addr.sin_addr.s_addr, server->h_addr, server->h_length);
|
|
|
|
// A short receive timeout lets at_cip_rx_task poll periodically instead of
|
|
// blocking forever, so it notices at_cip_close() promptly. Skipped here for
|
|
// the SSL caller: applying it before the TLS handshake made mbedtls's own
|
|
// blocking reads see spurious timeouts mid-handshake (a partial/timed-out
|
|
// read on a socket mbedtls expects to block on desyncs its record parser -
|
|
// confirmed by this exact change turning consistent MBEDTLS_ERR_SSL_INVALID_RECORD
|
|
// handshake failures into a clean handshake). The SSL path sets this itself
|
|
// after the handshake completes, once the socket moves to steady-state reads.
|
|
if (set_rcv_timeout)
|
|
{
|
|
struct timeval tv = { 0, 200 * 1000 };
|
|
setsockopt(sock, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
|
|
}
|
|
|
|
if (sock_type == SOCK_DGRAM)
|
|
return sock; // UDP: no connect() handshake to wait on
|
|
|
|
// Bound the connect() itself with an explicit timeout via a non-blocking
|
|
// connect + select(): an unreachable-but-not-immediately-rejected address
|
|
// (e.g. a black-holed IP) can otherwise leave the OS/lwip TCP SYN retry
|
|
// logic blocking this call for a minute or more, freezing the whole AT
|
|
// command interface for that entire time.
|
|
int flags = fcntl(sock, F_GETFL, 0);
|
|
fcntl(sock, F_SETFL, flags | O_NONBLOCK);
|
|
|
|
int rc = connect(sock, (struct sockaddr*)&addr, sizeof(addr));
|
|
|
|
if (rc < 0 && errno != EINPROGRESS)
|
|
{
|
|
ESP_LOGW(TAG, "connect() to %s:%d failed immediately", host, port);
|
|
close(sock);
|
|
return -1;
|
|
}
|
|
|
|
if (rc < 0) // EINPROGRESS - wait for it to complete
|
|
{
|
|
fd_set wfds;
|
|
FD_ZERO(&wfds);
|
|
FD_SET(sock, &wfds);
|
|
struct timeval conn_tv = { 10, 0 }; // 10s connect timeout
|
|
|
|
int sel = select(sock + 1, NULL, &wfds, NULL, &conn_tv);
|
|
|
|
if (sel <= 0)
|
|
{
|
|
ESP_LOGW(TAG, "connect() to %s:%d timed out", host, port);
|
|
close(sock);
|
|
return -1;
|
|
}
|
|
|
|
int so_err = 0;
|
|
socklen_t so_len = sizeof(so_err);
|
|
getsockopt(sock, SOL_SOCKET, SO_ERROR, &so_err, &so_len);
|
|
|
|
if (so_err != 0)
|
|
{
|
|
ESP_LOGW(TAG, "connect() to %s:%d failed: errno %d", host, port, so_err);
|
|
close(sock);
|
|
return -1;
|
|
}
|
|
}
|
|
|
|
fcntl(sock, F_SETFL, flags); // back to blocking for the (rare) case send() would need it
|
|
return sock;
|
|
}
|
|
|
|
static int at_cip_start_ssl(const char *host, int port)
|
|
{
|
|
int sock = at_cip_connect_raw_tcp(SOCK_STREAM, host, port, false);
|
|
if (sock < 0)
|
|
return -1;
|
|
|
|
// Kept local until the handshake fully succeeds below, deliberately not
|
|
// written to the global cip_tls yet: at_cip_is_connected() (checked by
|
|
// at_cip_rx_task's poll loop, running concurrently in its own task) goes by
|
|
// cip_tls != NULL, and that task calling esp_tls_conn_read() on a context
|
|
// that's mid-handshake raced with esp_tls_conn_new_sync() below over the
|
|
// same mbedtls ssl_context - confirmed via mbedtls debug log showing
|
|
// interleaved flush-output calls and a corrupted ssl->out_left, manifesting
|
|
// as MBEDTLS_ERR_SSL_INTERNAL_ERROR then MBEDTLS_ERR_SSL_INVALID_RECORD.
|
|
// The plain-TCP path never had this problem because it keeps its socket in
|
|
// a local variable the same way, only publishing to cip_sock at the very end.
|
|
esp_tls_t *tls = esp_tls_init();
|
|
if (!tls)
|
|
{
|
|
ESP_LOGW(TAG, "esp_tls_init failed");
|
|
close(sock);
|
|
return -1;
|
|
}
|
|
|
|
esp_tls_set_conn_sockfd(tls, sock);
|
|
esp_tls_set_conn_state(tls, ESP_TLS_CONNECTING); // skip esp_tls's own DNS/connect step - see at_cip_connect_raw_tcp()
|
|
|
|
esp_tls_cfg_t cfg = {};
|
|
cfg.crt_bundle_attach = esp_crt_bundle_attach;
|
|
cfg.timeout_ms = 10000; // matches the connect timeout already spent above
|
|
|
|
int rc = esp_tls_conn_new_sync(host, strlen(host), port, &cfg, tls);
|
|
if (rc != 1)
|
|
{
|
|
ESP_LOGW(TAG, "TLS handshake with %s:%d failed", host, port);
|
|
esp_tls_conn_destroy(tls); // also closes sock, it now owns the fd
|
|
return -1;
|
|
}
|
|
|
|
// Now that the handshake is done, throttle reads the same way the plain-TCP
|
|
// path does, so at_cip_rx_task's poll loop notices at_cip_close() promptly.
|
|
struct timeval tv = { 0, 200 * 1000 };
|
|
setsockopt(sock, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
|
|
|
|
cip_tls = tls; // now safe to publish: handshake is done, at_cip_rx_task may use it
|
|
cip_type = CIP_TYPE_SSL;
|
|
strlcpy(cip_host, host, sizeof(cip_host));
|
|
cip_port = port;
|
|
|
|
ESP_LOGI(TAG, "connected SSL %s:%d", host, port);
|
|
return 0;
|
|
}
|
|
|
|
int at_cip_start(const char *type, const char *host, int port)
|
|
{
|
|
if (at_cip_is_connected())
|
|
return -2; // already connected
|
|
|
|
if (!strcasecmp(type, "SSL"))
|
|
return at_cip_start_ssl(host, port);
|
|
|
|
int sock_type = 0;
|
|
|
|
if (!strcasecmp(type, "TCP"))
|
|
sock_type = SOCK_STREAM;
|
|
else if (!strcasecmp(type, "UDP"))
|
|
sock_type = SOCK_DGRAM;
|
|
else
|
|
{
|
|
ESP_LOGW(TAG, "unknown type '%s'", type);
|
|
return -1;
|
|
}
|
|
|
|
int sock = at_cip_connect_raw_tcp(sock_type, host, port, true);
|
|
if (sock < 0)
|
|
return -1;
|
|
|
|
cip_sock = sock;
|
|
cip_type = (sock_type == SOCK_DGRAM) ? CIP_TYPE_UDP : CIP_TYPE_TCP;
|
|
strlcpy(cip_host, host, sizeof(cip_host));
|
|
cip_port = port;
|
|
|
|
ESP_LOGI(TAG, "connected %s %s:%d", cip_type == CIP_TYPE_UDP ? "UDP" : "TCP", host, port);
|
|
return 0;
|
|
}
|
|
|
|
int at_cip_send(const uint8_t *data, size_t len)
|
|
{
|
|
if (cip_tls) return (int)esp_tls_conn_write(cip_tls, data, len);
|
|
if (cip_sock < 0) return -1;
|
|
return send(cip_sock, data, len, 0);
|
|
}
|
|
|
|
void at_cip_close()
|
|
{
|
|
if (!at_cip_is_connected()) return;
|
|
|
|
if (cip_tls)
|
|
{
|
|
esp_tls_conn_destroy(cip_tls);
|
|
cip_tls = NULL;
|
|
}
|
|
else
|
|
{
|
|
close(cip_sock);
|
|
cip_sock = -1;
|
|
}
|
|
|
|
cip_type = CIP_TYPE_NONE;
|
|
cip_host[0] = 0;
|
|
cip_port = 0;
|
|
}
|
|
|
|
void at_cip_rx_task(void *arg)
|
|
{
|
|
uint8_t buf[1024];
|
|
|
|
while (1)
|
|
{
|
|
if (!at_cip_is_connected())
|
|
{
|
|
vTaskDelay(pdMS_TO_TICKS(100));
|
|
continue;
|
|
}
|
|
|
|
esp_tls_t *tls = cip_tls; // snapshot; at_cip_close() may change these concurrently
|
|
int sock = cip_sock;
|
|
int n = tls ? (int)esp_tls_conn_read(tls, buf, sizeof(buf)) : recv(sock, buf, sizeof(buf), 0);
|
|
|
|
if (n > 0)
|
|
{
|
|
char hdr[24];
|
|
snprintf(hdr, sizeof(hdr), "\r\n+IPD,%d:", n);
|
|
at_send_str(hdr);
|
|
at_send_raw(buf, n);
|
|
}
|
|
else if (n == 0)
|
|
{
|
|
// Peer closed (TCP/SSL only - UDP recv() doesn't return 0 on a connected socket).
|
|
at_cip_close();
|
|
at_send_str("\r\nCLOSED\r\n");
|
|
}
|
|
// n < 0: either the SO_RCVTIMEO timeout expired (EAGAIN/EWOULDBLOCK, expected -
|
|
// esp_tls_conn_read() surfaces the same underlying-socket timeout as a generic
|
|
// negative return too, not distinguished from a real error here, matching how
|
|
// the plain-TCP path already treats every negative read as "retry") or the
|
|
// connection was closed out from under us by at_cip_close() (also fine,
|
|
// at_cip_is_connected() will be false at the top next time).
|
|
}
|
|
}
|