#include "httpstream.h" #include "esp_log.h" #include "lwip/netdb.h" #include "lwip/sockets.h" #include #include #include #include #include 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; }