Files
t-embed-insecam-widget/main/httpstream.c
T
Juli 65a391940b insecam desk widget for the LilyGO T-Embed
ESP-IDF 5.5 + LVGL 9 firmware that plays random public cameras from
insecam.org on the T-Embed's 320x170 screen, live MJPEG where available,
a new camera every 20 seconds. Knob steps through cameras, LED ring shows
the countdown. Auto-detects the original vs CC1101 board, reuses Wi-Fi
credentials from NVS and falls back to a captive-portal setup hotspot.
2026-09-17 00:38:09 +02:00

225 lines
8.1 KiB
C

#include "httpstream.h"
#include "esp_log.h"
#include "lwip/netdb.h"
#include "lwip/sockets.h"
#include <ctype.h>
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
static const char *TAG = "http";
#define RX_CAP 4096
#define MAX_REDIRECTS 3
#define HEADER_TIMEOUT_MS 5000
typedef struct { char host[128]; uint16_t port; char path[512]; } url_t;
static bool parse_url(const char *url, url_t *u)
{
if (strncasecmp(url, "http://", 7)) return false; // https cameras aren't worth TLS here
const char *p = url + 7, *end = p + strcspn(p, "/?");
const char *colon = memchr(p, ':', end - p);
size_t hlen = (colon ? colon : end) - p;
if (!hlen || hlen >= sizeof u->host) return false;
memcpy(u->host, p, hlen);
u->host[hlen] = 0;
u->port = colon ? atoi(colon + 1) : 80;
if (!u->port) return false;
if (*end == 0) strcpy(u->path, "/");
else if (*end == '?') snprintf(u->path, sizeof u->path, "/%s", end);
else strlcpy(u->path, end, sizeof u->path);
return true;
}
static int connect_timeout(const url_t *u, int timeout_ms)
{
char port[8];
snprintf(port, sizeof port, "%u", u->port);
struct addrinfo hints = {.ai_family = AF_INET, .ai_socktype = SOCK_STREAM}, *res = NULL;
if (getaddrinfo(u->host, port, &hints, &res) != 0 || !res) {
ESP_LOGW(TAG, "dns failed for %s", u->host);
return -1;
}
int fd = socket(res->ai_family, res->ai_socktype, res->ai_protocol);
if (fd < 0) { freeaddrinfo(res); return -1; }
fcntl(fd, F_SETFL, fcntl(fd, F_GETFL) | O_NONBLOCK);
int rc = connect(fd, res->ai_addr, res->ai_addrlen);
freeaddrinfo(res);
if (rc < 0 && errno != EINPROGRESS) { close(fd); return -1; }
if (rc < 0) {
fd_set wf;
FD_ZERO(&wf);
FD_SET(fd, &wf);
struct timeval tv = {.tv_sec = timeout_ms / 1000, .tv_usec = (timeout_ms % 1000) * 1000};
int so_err = 0;
socklen_t slen = sizeof so_err;
if (select(fd + 1, NULL, &wf, NULL, &tv) <= 0 ||
getsockopt(fd, SOL_SOCKET, SO_ERROR, &so_err, &slen) < 0 || so_err) {
close(fd);
return -1;
}
}
fcntl(fd, F_SETFL, fcntl(fd, F_GETFL) & ~O_NONBLOCK);
return fd;
}
static int recv_more(httpstream_t *s)
{
if (s->pos && s->pos == s->len) s->pos = s->len = 0;
if (s->len == s->cap) return HTTPSTREAM_ERR; // caller must drain first
int n = recv(s->fd, s->buf + s->len, s->cap - s->len, 0);
if (n > 0) { s->len += n; return n; }
if (n == 0) return HTTPSTREAM_EOF;
return (errno == EAGAIN || errno == EWOULDBLOCK) ? HTTPSTREAM_TIMEOUT : HTTPSTREAM_ERR;
}
// Reads one CRLF-terminated line from the buffer into out (without CRLF); pulls more data as needed.
static int read_line(httpstream_t *s, char *out, size_t cap)
{
size_t n = 0;
while (1) {
while (s->pos < s->len) {
char c = s->buf[s->pos++];
if (c == '\n') { out[n] = 0; if (n && out[n - 1] == '\r') out[n - 1] = 0; return 1; }
if (n + 1 < cap) out[n++] = c;
}
int r = recv_more(s);
if (r <= 0) return r;
}
}
static int read_headers(httpstream_t *s, char *location, size_t loc_cap)
{
char line[512];
int r = read_line(s, line, sizeof line);
if (r <= 0) return r;
if (strncmp(line, "HTTP/", 5) || strlen(line) < 12) return HTTPSTREAM_ERR;
s->status = atoi(line + 9);
s->content_length = -1;
s->chunked = false;
s->content_type[0] = 0;
location[0] = 0;
while (1) {
r = read_line(s, line, sizeof line);
if (r <= 0) return r;
if (!line[0]) return 1;
char *v = strchr(line, ':');
if (!v) continue;
*v++ = 0;
while (*v == ' ' || *v == '\t') v++;
if (!strcasecmp(line, "Content-Length")) s->content_length = atoll(v);
else if (!strcasecmp(line, "Content-Type")) strlcpy(s->content_type, v, sizeof s->content_type);
else if (!strcasecmp(line, "Transfer-Encoding") && strcasestr(v, "chunked")) s->chunked = true;
else if (!strcasecmp(line, "Location")) strlcpy(location, v, loc_cap);
}
}
esp_err_t httpstream_open(httpstream_t *s, const char *url, const char *user_agent,
int connect_timeout_ms, int rx_timeout_ms)
{
char current[640], location[512];
strlcpy(current, url, sizeof current);
memset(s, 0, sizeof *s);
s->fd = -1;
s->rx_timeout_ms = rx_timeout_ms;
s->buf = malloc(RX_CAP);
if (!s->buf) return ESP_ERR_NO_MEM;
s->cap = RX_CAP;
for (int hop = 0; hop <= MAX_REDIRECTS; hop++) {
url_t u;
if (!parse_url(current, &u)) { httpstream_close(s); return ESP_ERR_INVALID_ARG; }
s->fd = connect_timeout(&u, connect_timeout_ms);
if (s->fd < 0) { httpstream_close(s); return ESP_ERR_TIMEOUT; }
// generous while waiting for the status line; the caller's slice timeout applies to the body
struct timeval tv = {.tv_sec = HEADER_TIMEOUT_MS / 1000, .tv_usec = 0};
setsockopt(s->fd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof tv);
setsockopt(s->fd, SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof tv);
strlcpy(s->host, u.host, sizeof s->host);
s->port = u.port;
char hosthdr[140], req[900];
if (u.port == 80) strlcpy(hosthdr, u.host, sizeof hosthdr);
else snprintf(hosthdr, sizeof hosthdr, "%s:%u", u.host, u.port);
int n = snprintf(req, sizeof req,
"GET %s HTTP/1.1\r\nHost: %s\r\nUser-Agent: %s\r\nAccept: */*\r\nConnection: close\r\n\r\n",
u.path, hosthdr, user_agent);
if (n <= 0 || n >= (int)sizeof req || send(s->fd, req, n, 0) != n) { httpstream_close(s); return ESP_FAIL; }
s->len = s->pos = 0;
int r = read_headers(s, location, sizeof location);
if (r <= 0) { httpstream_close(s); return r == HTTPSTREAM_TIMEOUT ? ESP_ERR_TIMEOUT : ESP_FAIL; }
if ((s->status == 301 || s->status == 302 || s->status == 303 || s->status == 307 || s->status == 308) && location[0]) {
close(s->fd);
s->fd = -1;
if (location[0] == '/') snprintf(current, sizeof current, "http://%s:%u%s", u.host, u.port, location);
else strlcpy(current, location, sizeof current);
ESP_LOGI(TAG, "redirect -> %s", current);
continue;
}
s->chunk_left = 0;
s->body_read = 0;
s->eof = false;
tv.tv_sec = rx_timeout_ms / 1000;
tv.tv_usec = (rx_timeout_ms % 1000) * 1000;
setsockopt(s->fd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof tv);
return ESP_OK;
}
httpstream_close(s);
return ESP_FAIL;
}
static int read_raw(httpstream_t *s, uint8_t *out, size_t n)
{
if (s->pos == s->len) {
int r = recv_more(s);
if (r <= 0) return r;
}
size_t avail = s->len - s->pos;
if (n > avail) n = avail;
memcpy(out, s->buf + s->pos, n);
s->pos += n;
return n;
}
int httpstream_read(httpstream_t *s, uint8_t *out, size_t n)
{
if (s->eof || s->fd < 0) return HTTPSTREAM_EOF;
if (!s->chunked) {
if (s->content_length >= 0) {
int64_t left = s->content_length - s->body_read;
if (left <= 0) { s->eof = true; return HTTPSTREAM_EOF; }
if ((int64_t)n > left) n = left;
}
int r = read_raw(s, out, n);
if (r == HTTPSTREAM_EOF) s->eof = true;
if (r > 0) s->body_read += r;
return r;
}
// chunked
while (s->chunk_left == 0) {
char line[64];
int r = read_line(s, line, sizeof line);
if (r <= 0) return r;
if (!line[0]) continue; // CRLF after the previous chunk
s->chunk_left = strtoll(line, NULL, 16);
if (s->chunk_left == 0) { s->eof = true; return HTTPSTREAM_EOF; }
}
if ((int64_t)n > s->chunk_left) n = s->chunk_left;
int r = read_raw(s, out, n);
if (r > 0) { s->chunk_left -= r; s->body_read += r; }
else if (r == HTTPSTREAM_EOF) s->eof = true;
return r;
}
void httpstream_close(httpstream_t *s)
{
if (s->fd >= 0) close(s->fd);
s->fd = -1;
free(s->buf);
s->buf = NULL;
s->eof = true;
}