DeterministicESPAsyncWebServer v6.28.0
Zero-allocation, bounded-execution async HTTP server for ESP32
Loading...
Searching...
No Matches
ota_service.cpp
Go to the documentation of this file.
1// Copyright (C) 2026 Douglas Quigg (dstroy0) <dquigg123@gmail.com>
2// SPDX-License-Identifier: AGPL-3.0-or-later
3
4/**
5 * @file ota_service.cpp
6 * @brief Authenticated streaming OTA firmware update (DETWS_ENABLE_OTA).
7 */
8
9#include "ota_service.h"
10#include "services/clock.h" // dwsdelay
11
12#if DETWS_ENABLE_OTA && defined(ARDUINO)
13
14#include "dwserver.h"
18#include <Arduino.h>
19#include <Update.h>
20#include <string.h>
21
22// All OTA-service state, owned by one instance (internal linkage): the server handle, the
23// route path, the Basic-auth credentials, and the per-upload flags (one upload at a time on
24// this single-task device). Grouped so it is one named owner, unreachable cross-TU.
25struct OtaCtx
26{
27 DetWebServer *server = nullptr;
28 const char *path = nullptr;
29 char user[MAX_AUTH_LEN] = {0};
30 char pass[MAX_AUTH_LEN] = {0};
31 bool authed = false; ///< Credentials validated for the current upload.
32 bool active = false; ///< Update.begin() succeeded for the current upload.
33 bool error = false; ///< A write failed during the current upload.
34};
35static OtaCtx s_ota;
36
37/// @brief Validate the request's HTTP Basic credentials against s_ota.user/s_ota.pass.
38static bool ota_check_auth(HttpReq *req)
39{
40 const char *h = http_get_header(req, "Authorization");
41 if (!h || strncmp(h, "Basic ", 6) != 0)
42 return false;
43
44 uint8_t decoded[MAX_AUTH_LEN * 2 + 2];
45 size_t n = base64_decode(h + 6, decoded, sizeof(decoded) - 1);
46 if (n == 0)
47 return false;
48 decoded[n] = '\0';
49
50 const char *colon = (const char *)memchr(decoded, ':', n);
51 if (!colon)
52 return false;
53 size_t ulen = (size_t)(colon - (const char *)decoded);
54 const char *pass = colon + 1;
55 return (ulen == strnlen(s_ota.user, sizeof(s_ota.user))) && (memcmp(decoded, s_ota.user, ulen) == 0) &&
56 (strcmp(pass, s_ota.pass) == 0);
57}
58
59/// @brief Stream-begin hook: accept POST @p s_ota.path; begin Update if authorized.
60static bool ota_stream_begin(HttpReq *req)
61{
62 if (strcmp(req->method, "POST") != 0)
63 return false;
64 if (!s_ota.path || strcmp(req->path, s_ota.path) != 0)
65 return false;
66
67 s_ota.authed = ota_check_auth(req);
68 s_ota.active = false;
69 s_ota.error = false;
70 if (s_ota.authed)
71 {
72 if (Update.begin(UPDATE_SIZE_UNKNOWN))
73 s_ota.active = true;
74 else
75 s_ota.error = true;
76 }
77 // Stream regardless so the body is consumed and the route handler can reply;
78 // when unauthorized/!active the data hook simply discards.
79 return true;
80}
81
82/// @brief Stream-data hook: write one chunk of the image to Update.
83static void ota_stream_data(HttpReq *req, const uint8_t *data, size_t len)
84{
85 (void)req; // a single OTA image streams at a time
86 if (s_ota.authed && s_ota.active && !s_ota.error)
87 {
88 if (Update.write((uint8_t *)data, len) != len)
89 s_ota.error = true;
90 }
91}
92
93/// @brief Route handler (runs at ParseState::PARSE_COMPLETE): finalize and reply, then reboot.
94static void ota_handle(uint8_t slot_id, HttpReq *req)
95{
96 if (!req->body_streaming)
97 {
98 s_ota.server->send(slot_id, 400, DET_MIME_TEXT_PLAIN, "POST a raw firmware image");
99 return;
100 }
101 if (!s_ota.authed)
102 {
103 s_ota.server->send(slot_id, 401, DET_MIME_TEXT_PLAIN, "Unauthorized");
104 return;
105 }
106 bool ok = s_ota.active && !s_ota.error && Update.end(true);
107 if (!ok)
108 {
109 if (s_ota.active)
110 Update.abort();
111 s_ota.server->send(slot_id, 400, DET_MIME_TEXT_PLAIN, "Update failed");
112 return;
113 }
114 s_ota.server->send(slot_id, 200, DET_MIME_TEXT_PLAIN, "OK - rebooting");
115 dwsdelay(150); // let the response flush before the reboot
116 ESP.restart();
117}
118
119void detws_ota_begin(DetWebServer &server, const char *path, const char *user, const char *pass)
120{
121 s_ota.server = &server;
122 s_ota.path = path;
123 strncpy(s_ota.user, user ? user : "", sizeof(s_ota.user) - 1);
124 s_ota.user[sizeof(s_ota.user) - 1] = '\0';
125 strncpy(s_ota.pass, pass ? pass : "", sizeof(s_ota.pass) - 1);
126 s_ota.pass[sizeof(s_ota.pass) - 1] = '\0';
127
128 http_parser_set_stream_hooks(ota_stream_begin, ota_stream_data);
129 server.on(path, HttpMethod::HTTP_POST, ota_handle);
130}
131
132#else
133
134void detws_ota_begin(DetWebServer &server, const char *path, const char *user, const char *pass)
135{
136 (void)server;
137 (void)path;
138 (void)user;
139 (void)pass;
140}
141
142#endif // DETWS_ENABLE_OTA && ARDUINO
#define MAX_AUTH_LEN
Maximum username or password length for HTTP Basic Authentication.
size_t base64_decode(const char *src, uint8_t *dst, size_t dst_cap)
Decode a null-terminated Base64 string.
Definition base64.cpp:65
Base64 encoder/decoder.
Single-port HTTP server with deterministic, zero-allocation execution.
Definition dwserver.h:348
void on(const char *path, HttpMethod method, Handler callback)
Register a route handler.
Definition dwserver.cpp:759
Pluggable monotonic clock for all library timing.
void dwsdelay(uint32_t ms)
Block for at least ms milliseconds - the library's single delay primitive.
Definition clock.h:87
Layer 7 (Application) - public HTTP routing API.
@ HTTP_POST
Non-idempotent create / action.
const char * http_get_header(const HttpReq *req, const char *key)
Look up a header value by name (case-insensitive).
Standalone HTTP/1.1 request parser - no transport dependency.
Shared HTTP Content-Type ("MIME") string constants (one source of truth).
void detws_ota_begin(DetWebServer &server, const char *path, const char *user, const char *pass)
Register an authenticated streaming OTA endpoint.
Optional authenticated OTA firmware update (DETWS_ENABLE_OTA).
Fully-parsed HTTP/1.1 request.
char method[DETWS_METHOD_BUF_SIZE]
HTTP method, null-terminated (OPTIONS, or WebDAV methods when enabled).
char path[MAX_PATH_LEN]
URL path, null-terminated; no query string.