FastLED 3.10.8
Loading...
Searching...
No Matches
fetch_request.cpp.hpp
Go to the documentation of this file.
1#pragma once
2
4#include "fl/task/executor.h"
5#include "fl/log/log.h"
6#include "fl/stl/int.h"
7
8#ifdef FASTLED_HAS_NETWORKING
9
10#include "fl/stl/cstring.h" // For memcpy
11
12// Platform-specific socket includes
13#ifdef FL_IS_WIN
14 // Minimize Windows headers to avoid conflicts with Arduino types
15 #ifndef WIN32_LEAN_AND_MEAN
16 #define WIN32_LEAN_AND_MEAN
17 #endif
18 #ifndef NOGDI
19 #define NOGDI // Exclude GDI headers (prevents CRGB conflict)
20 #endif
21 #ifndef NOMINMAX
22 #define NOMINMAX // Exclude min/max macros
23 #endif
24
25 // Undef Arduino macros that conflict with Windows SDK headers
26 #ifdef INPUT
27 #undef INPUT
28 #endif
29 #ifdef OUTPUT
30 #undef OUTPUT
31 #endif
32
33 // Prevent Windows from typedef'ing types that conflict with Arduino
34 #define boolean _FL_BOOLEAN_WORKAROUND
35 #define CRGB _FL_CRGB_WORKAROUND
36
37 // IWYU pragma: begin_keep
38 #include <winsock2.h>
39 #include <ws2tcpip.h>
40 // IWYU pragma: end_keep
41
42 // Undefine workarounds
43 #undef boolean
44 #undef CRGB
45
46 #define SOCKET_ERROR_WOULD_BLOCK WSAEWOULDBLOCK
47 #define GET_SOCKET_ERROR() WSAGetLastError()
48 #define CLOSE_SOCKET(s) closesocket(s)
49#else
50 // IWYU pragma: begin_keep
51 #include <sys/socket.h>
52 #include <sys/select.h>
53 #include <sys/time.h>
54 #include <netinet/in.h>
55 #include <netdb.h>
56 #include <unistd.h>
57 #include <fcntl.h>
58 #include <errno.h>
59#include "fl/stl/noexcept.h"
60 // IWYU pragma: end_keep
61 #define SOCKET_ERROR_WOULD_BLOCK EWOULDBLOCK
62 #define GET_SOCKET_ERROR() errno
63 #define CLOSE_SOCKET(s) close(s)
64#endif
65
66namespace fl {
67namespace net {
68namespace http {
69
70namespace {
71
72// The value socket() returns on failure: INVALID_SOCKET on Winsock, -1 on POSIX.
73#ifdef FL_IS_WIN
74constexpr fl::uptr kInvalidSocketHandle = static_cast<fl::uptr>(INVALID_SOCKET);
75#else
76constexpr int kInvalidSocketHandle = -1;
77#endif
78
79// ASCII case-insensitive equality; HTTP header names are case-insensitive.
80bool header_name_equals(const fl::string& name, const char* reserved) FL_NO_EXCEPT {
81 fl::size i = 0;
82 for (; i < name.size() && reserved[i] != '\0'; ++i) {
83 char a = name[i];
84 char b = reserved[i];
85 if (a >= 'A' && a <= 'Z') a = static_cast<char>(a - 'A' + 'a');
86 if (b >= 'A' && b <= 'Z') b = static_cast<char>(b - 'A' + 'a');
87 if (a != b) {
88 return false;
89 }
90 }
91 return i == name.size() && reserved[i] == '\0';
92}
93
94// CR or LF would end the current request line or header and let the rest of
95// the field be read as a new one.
96bool has_line_break(const fl::string& field) FL_NO_EXCEPT {
97 for (fl::size i = 0; i < field.size(); ++i) {
98 if (field[i] == '\r' || field[i] == '\n') {
99 return true;
100 }
101 }
102 return false;
103}
104
105// Headers FetchRequest writes itself: Host, Connection, and the body framing.
106bool is_reserved_request_header(const fl::string& name) FL_NO_EXCEPT {
107 return header_name_equals(name, "host") ||
108 header_name_equals(name, "connection") ||
109 header_name_equals(name, "content-length") ||
110 header_name_equals(name, "transfer-encoding");
111}
112
113} // namespace
114
115namespace detail {
116
117bool build_http_request(const RequestOptions& options, const fl::string& path,
118 const fl::string& host, fl::string* out) FL_NO_EXCEPT {
119 if (has_line_break(options.method) || has_line_break(path) || has_line_break(host)) {
120 return false;
121 }
122 for (const auto& header : options.headers) {
123 if (has_line_break(header.first) || has_line_break(header.second)) {
124 return false;
125 }
126 }
127 fl::string request = options.method.empty() ? fl::string("GET") : options.method;
128 request += " " + path + " HTTP/1.1\r\n";
129 request += "Host: " + host + "\r\n";
130 for (const auto& header : options.headers) {
131 if (is_reserved_request_header(header.first)) {
132 continue;
133 }
134 request += header.first + ": " + header.second + "\r\n";
135 }
136 if (!options.body.empty()) {
137 request += "Content-Length: ";
138 request.append(static_cast<u32>(options.body.size()));
139 request += "\r\n";
140 }
141 request += "Connection: close\r\n\r\n";
142 request += options.body;
143 *out = request;
144 return true;
145}
146
147} // namespace detail
148
149
150FetchRequest::FetchRequest(const fl::string& url, const FetchOptions& opts, fl::task::Promise<Response> prom) FL_NO_EXCEPT
151 : mState(DNS_LOOKUP)
152 , mPromise(prom)
153 , mOptions(opts.options())
154 , mParsedUrl(url)
155 , mHostname()
156 , mPort(80)
157 , mPath("/")
158 , mSocketFd(kInvalidSocketHandle)
159 , mDnsResult(nullptr)
160 , mRequestBuffer()
161 , mResponseBuffer()
162 , mBytesSent(0)
163 , mStateStartTime(fl::millis())
164{
165 if (!mParsedUrl.isValid() || mParsedUrl.host().empty()) {
166 complete_error("Invalid URL");
167 return;
168 }
169 mHostname = fl::string(mParsedUrl.host().data(), mParsedUrl.host().size());
170 mPort = mParsedUrl.port();
171 fl::string_view p = mParsedUrl.path();
172 mPath = p.empty() ? fl::string("/") : fl::string(p.data(), p.size());
173}
174
176 close_socket();
177}
178
180 switch (mState) {
181 case DNS_LOOKUP:
183 break;
184 case CONNECTING:
186 break;
187 case SENDING:
189 break;
190 case RECEIVING:
192 break;
193 case COMPLETED:
194 case FAILED:
195 // Done - no-op
196 break;
197 }
198}
199
201 // Pump async system before DNS to keep server responsive
202 fl::task::run(1000);
203
204 FL_WARN("[FETCH] Resolving hostname: " << mHostname);
205
206 // DNS lookup (blocking 10-100ms, but acceptable)
207 // Note: localhost is typically instant due to OS caching
208 mDnsResult = gethostbyname(mHostname.c_str());
209
210 // Pump async system after DNS to resume server pumping
211 fl::task::run(1000);
212
213 if (!mDnsResult) {
214 complete_error("DNS lookup failed");
215 return;
216 }
217
218 // Create socket
219 mSocketFd = socket(AF_INET, SOCK_STREAM, 0);
220 if (mSocketFd == kInvalidSocketHandle) {
221 complete_error("Failed to create socket");
222 return;
223 }
224
225 // Set non-blocking mode
226#ifdef FL_IS_WIN
227 u_long mode = 1; // 1 = non-blocking
228 ioctlsocket(mSocketFd, FIONBIO, &mode);
229#else
230 int flags = fcntl(mSocketFd, F_GETFL, 0);
231 fcntl(mSocketFd, F_SETFL, flags | O_NONBLOCK);
232#endif
233
234 // Initiate non-blocking connect
235 sockaddr_in server_addr{};
236 server_addr.sin_family = AF_INET;
237 server_addr.sin_port = htons(static_cast<u16>(mPort));
238 // Use memcpy to extract address pointer — h_addr_list[0] may be misaligned
239 // on some platforms (macOS arm64 UBSan flags this)
240 char *addr_ptr = nullptr;
241 fl::memcpy(&addr_ptr, mDnsResult->h_addr_list, sizeof(addr_ptr));
242 fl::memcpy(&server_addr.sin_addr, addr_ptr, mDnsResult->h_length);
243
244 FL_WARN("[FETCH] Waiting for connection to " << mHostname << ":" << mPort);
245
246 connect(mSocketFd, (sockaddr*)&server_addr, sizeof(server_addr));
247 // connect() returns immediately with EINPROGRESS/WSAEWOULDBLOCK (expected)
248
251}
252
254 fd_set write_fds;
255 FD_ZERO(&write_fds);
256 FD_SET(mSocketFd, &write_fds);
257
258 struct timeval timeout;
259 timeout.tv_sec = 0;
260 timeout.tv_usec = 0; // Non-blocking check
261
262 // nfds is ignored by Winsock and is the highest fd + 1 on POSIX.
263 int result = select(static_cast<int>(mSocketFd + 1), nullptr, &write_fds, nullptr, &timeout);
264
265 if (result > 0) {
266 // Check for connection errors
267 int sock_err = 0;
268 socklen_t len = sizeof(sock_err);
269 getsockopt(mSocketFd, SOL_SOCKET, SO_ERROR, (char*)&sock_err, &len);
270
271 if (sock_err != 0) {
272 complete_error("Connection failed");
273 return;
274 }
275
276 // Connected! Build the HTTP request from the caller's options. This
277 // used to hardcode a bare GET, so fetch_post() and custom headers and
278 // bodies were silently dropped on native targets.
280 complete_error("Invalid request: CR or LF in the method, path, host or a header");
281 return;
282 }
283
284 mBytesSent = 0;
285 mState = SENDING;
287 } else if (result < 0) {
288 complete_error("select() failed during connection");
289 } else {
290 // Timeout check (5 seconds)
291 if (fl::millis() - mStateStartTime > 5000) {
292 complete_error("Connection timeout");
293 }
294 // else: Not ready yet, try again on next update()
295 }
296}
297
299 ssize_t sent = send(mSocketFd,
300 mRequestBuffer.c_str() + mBytesSent,
301 mRequestBuffer.size() - mBytesSent,
302 0);
303
304 if (sent > 0) {
305 mBytesSent += sent;
306 if (mBytesSent >= mRequestBuffer.size()) {
307 FL_WARN("[FETCH] Waiting for HTTP response...");
310 }
311 } else if (sent < 0) {
312 int err = GET_SOCKET_ERROR();
313 if (err != SOCKET_ERROR_WOULD_BLOCK) {
314 complete_error("Send failed");
315 }
316 // EWOULDBLOCK: Try again on next update()
317 }
318}
319
321 char buffer[4096];
322 ssize_t bytes = recv(mSocketFd, buffer, sizeof(buffer), 0);
323
324 if (bytes > 0) {
325 mResponseBuffer.append(buffer, static_cast<size_t>(bytes));
326 mStateStartTime = fl::millis(); // Reset timeout on data received
327 } else if (bytes == 0) {
328 // Connection closed - parse response and complete
329 Response resp = parse_http_response(mResponseBuffer);
330 complete_success(resp);
331 } else {
332 int err = GET_SOCKET_ERROR();
333 if (err == SOCKET_ERROR_WOULD_BLOCK) {
334 // Check timeout (5 seconds)
335 if (fl::millis() - mStateStartTime > 5000) {
336 complete_error("Response timeout");
337 }
338 // else: Not ready, try again on next update()
339 } else {
340 complete_error("Receive failed");
341 }
342 }
343}
344
346 const char* data = raw.c_str();
347 const size_t len = raw.size();
348
349 // Find end of headers (double CRLF)
350 size_t header_end = raw.find("\r\n\r\n");
351 if (header_end == fl::string::npos) {
352 return Response(500, "Internal Server Error");
353 }
354
355 // Parse status line: "HTTP/1.1 200 OK\r\n"
356 size_t first_space = raw.find(' ');
357 size_t second_space = raw.find(' ', first_space + 1);
358 if (first_space == fl::string::npos || second_space == fl::string::npos ||
359 first_space >= header_end || second_space >= header_end) {
360 return Response(500, "Internal Server Error");
361 }
362
363 // Parse status code from digits without allocating
364 int status_code = 0;
365 for (size_t i = first_space + 1; i < second_space; ++i) {
366 status_code = status_code * 10 + (data[i] - '0');
367 }
368
369 // Status text ends at first \r\n
370 size_t status_line_end = raw.find("\r\n");
371 size_t st_len = (status_line_end != fl::string::npos ? status_line_end : header_end) - (second_space + 1);
372
373 Response resp(status_code, fl::string(data + second_space + 1, st_len));
374 resp.set_body(fl::string(data + header_end + 4, len - header_end - 4));
375
376 // Parse response headers using indices into raw
377 size_t pos = status_line_end + 2; // Skip past status line CRLF
378 while (pos < header_end) {
379 size_t line_end = raw.find("\r\n", pos);
380 if (line_end == fl::string::npos || line_end > header_end) {
381 line_end = header_end;
382 }
383
384 // Find colon separator
385 size_t colon = fl::string::npos;
386 for (size_t i = pos; i < line_end; ++i) {
387 if (data[i] == ':') {
388 colon = i;
389 break;
390 }
391 }
392
393 if (colon != fl::string::npos) {
394 // Build lowercase header name
395 fl::string name(data + pos, colon - pos);
396 for (size_t i = 0; i < name.size(); ++i) {
397 if (name[i] >= 'A' && name[i] <= 'Z') {
398 name[i] += ('a' - 'A');
399 }
400 }
401
402 // Value: skip colon and optional leading space
403 size_t val_start = colon + 1;
404 if (val_start < line_end && data[val_start] == ' ') {
405 ++val_start;
406 }
407 resp.set_header(name, fl::string(data + val_start, line_end - val_start));
408 }
409
410 pos = line_end + 2;
411 }
412
413 return resp;
414}
415
417 close_socket();
418 mState = COMPLETED;
419
420 if (mPromise.valid() && !mPromise.is_completed()) {
421 mPromise.complete_with_value(resp);
422 }
423}
424
425void FetchRequest::complete_error(const char* message) FL_NO_EXCEPT {
426 close_socket();
427 mState = FAILED;
428
429 if (mPromise.valid() && !mPromise.is_completed()) {
430 mPromise.complete_with_error(fl::task::Error(message));
431 }
432}
433
435 if (mSocketFd != kInvalidSocketHandle) {
436 CLOSE_SOCKET(mSocketFd);
437 mSocketFd = kInvalidSocketHandle;
438 }
439}
440
441} // namespace http
442} // namespace net
443} // namespace fl
444
445#endif // FASTLED_HAS_NETWORKING
uint8_t pos
Definition Blur.ino:11
Fetch options builder (fluent interface)
Definition fetch.h:195
void handle_dns_lookup() FL_NO_EXCEPT
void close_socket() FL_NO_EXCEPT
void update() FL_NO_EXCEPT
Pump the state machine (called by fl::task every update interval)
void handle_sending() FL_NO_EXCEPT
void handle_connecting() FL_NO_EXCEPT
Response parse_http_response(const fl::string &raw) FL_NO_EXCEPT
~FetchRequest() FL_NO_EXCEPT
Destructor - closes socket if still open.
FetchRequest(const fl::string &url, const FetchOptions &opts, fl::task::Promise< Response > promise) FL_NO_EXCEPT
Construct a new fetch request.
void handle_receiving() FL_NO_EXCEPT
@ DNS_LOOKUP
Resolving hostname (brief blocking ~10-100ms)
@ RECEIVING
Receiving HTTP response (non-blocking)
@ SENDING
Sending HTTP request (non-blocking)
@ CONNECTING
Waiting for socket connection (non-blocking)
@ COMPLETED
Successfully completed.
void complete_error(const char *message) FL_NO_EXCEPT
void complete_success(const Response &resp) FL_NO_EXCEPT
HTTP response class (unified interface)
Definition fetch.h:78
constexpr bool empty() const FL_NO_EXCEPT
string & append(const bitset_fixed< N > &bs) FL_NO_EXCEPT
Definition string.h:293
static constexpr fl::size npos
Definition string.h:204
Task executor — runs registered task runners and manages the run loop.
#define FL_WARN(...)
Definition log.h:437
Centralized logging categories for FastLED hardware interfaces and subsystems.
bool build_http_request(const RequestOptions &options, const fl::string &path, const fl::string &host, fl::string *out) FL_NO_EXCEPT
The HTTP/1.1 request FetchRequest sends: options.method (GET when empty) on the request line,...
void run(fl::u32 microseconds, ExecFlags flags) FL_NO_EXCEPT
Run selected task subsystems.
u8 u8 fract8 blur_amount FL_NO_EXCEPT
Definition blur.h:186
fl::u32 millis() FL_NO_EXCEPT
Universal millisecond timer - returns milliseconds since system startup.
FASTLED_FORCE_INLINE void * memcpy(void *dest, const void *src, size_t n) FL_NO_EXCEPT
Definition cstring.h:136
expected< T, E > result
Alias for expected (Rust-style naming)
Definition result.h:31
Base definition for an LED controller.
Definition crgb.hpp:179