ProtoCore v0.0.2
Deterministic, zero-heap network stack for embedded targets
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 (PC_ENABLE_OTA).
7 */
8
9#include "ota_service.h"
10#include "services/system/clock.h" // pcdelay
11
12#if PC_ENABLE_OTA && defined(ARDUINO)
13
16#include "protocore.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 PC *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 {
43 return false;
44 }
45
46 uint8_t decoded[MAX_AUTH_LEN * 2 + 2];
47 size_t n = pc_base64_decode(h + 6, decoded, sizeof(decoded) - 1);
48 if (n == 0)
49 {
50 return false;
51 }
52 decoded[n] = '\0';
53
54 const char *colon = (const char *)memchr(decoded, ':', n);
55 if (!colon)
56 {
57 return false;
58 }
59 size_t ulen = (size_t)(colon - (const char *)decoded);
60 const char *pass = colon + 1;
61 return (ulen == strnlen(s_ota.user, sizeof(s_ota.user))) && (memcmp(decoded, s_ota.user, ulen) == 0) &&
62 (strcmp(pass, s_ota.pass) == 0);
63}
64
65/// @brief Stream-begin hook: accept POST @p s_ota.path; begin Update if authorized.
66static bool ota_stream_begin(HttpReq *req)
67{
68 if (strcmp(req->method, "POST") != 0)
69 {
70 return false;
71 }
72 if (!s_ota.path || strcmp(req->path, s_ota.path) != 0)
73 {
74 return false;
75 }
76
77 s_ota.authed = ota_check_auth(req);
78 s_ota.active = false;
79 s_ota.error = false;
80 if (s_ota.authed)
81 {
82 if (Update.begin(UPDATE_SIZE_UNKNOWN))
83 {
84 s_ota.active = true;
85 }
86 else
87 {
88 s_ota.error = true;
89 }
90 }
91 // Stream regardless so the body is consumed and the route handler can reply;
92 // when unauthorized/!active the data hook simply discards.
93 return true;
94}
95
96/// @brief Stream-data hook: write one chunk of the image to Update.
97static void ota_stream_data(HttpReq *req, const uint8_t *data, size_t len)
98{
99 (void)req; // a single OTA image streams at a time
100 if (s_ota.authed && s_ota.active && !s_ota.error)
101 {
102 if (Update.write((uint8_t *)data, len) != len)
103 {
104 s_ota.error = true;
105 }
106 }
107}
108
109/// @brief Route handler (runs at ParseState::PARSE_COMPLETE): finalize and reply, then reboot.
110static void ota_handle(uint8_t slot_id, HttpReq *req)
111{
112 if (!req->body_streaming)
113 {
114 s_ota.server->send(slot_id, 400, PC_MIME_TEXT_PLAIN, "POST a raw firmware image");
115 return;
116 }
117 if (!s_ota.authed)
118 {
119 s_ota.server->send(slot_id, 401, PC_MIME_TEXT_PLAIN, "Unauthorized");
120 return;
121 }
122 bool ok = s_ota.active && !s_ota.error && Update.end(true);
123 if (!ok)
124 {
125 if (s_ota.active)
126 {
127 Update.abort();
128 }
129 s_ota.server->send(slot_id, 400, PC_MIME_TEXT_PLAIN, "Update failed");
130 return;
131 }
132 s_ota.server->send(slot_id, 200, PC_MIME_TEXT_PLAIN, "OK - rebooting");
133 pcdelay(150); // let the response flush before the reboot
134 ESP.restart();
135}
136
137void pc_ota_begin(PC &server, const char *path, const char *user, const char *pass)
138{
139 s_ota.server = &server;
140 s_ota.path = path;
141 strncpy(s_ota.user, user ? user : "", sizeof(s_ota.user) - 1);
142 s_ota.user[sizeof(s_ota.user) - 1] = '\0';
143 strncpy(s_ota.pass, pass ? pass : "", sizeof(s_ota.pass) - 1);
144 s_ota.pass[sizeof(s_ota.pass) - 1] = '\0';
145
146 http_parser_set_stream_hooks(ota_stream_begin, ota_stream_data);
147 server.on(path, HttpMethod::HTTP_POST, ota_handle);
148}
149
150#else
151
152void pc_ota_begin(PC &server, const char *path, const char *user, const char *pass)
153{
154 (void)server;
155 (void)path;
156 (void)user;
157 (void)pass;
158}
159
160#endif // PC_ENABLE_OTA && ARDUINO
size_t pc_base64_decode(const char *src, uint8_t *dst, size_t dst_cap)
Decode a null-terminated Base64 string.
Definition base64.cpp:217
Base64 encoder/decoder.
Single-port HTTP server with deterministic, zero-allocation execution.
Definition protocore.h:348
void on(const char *path, HttpMethod method, Handler callback)
Register a route handler.
Pluggable monotonic clock for all library timing.
void pcdelay(uint32_t ms)
Block for at least ms milliseconds - the library's single delay primitive.
Definition clock.h:89
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 pc_ota_begin(PC &server, const char *path, const char *user, const char *pass)
Register an authenticated streaming OTA endpoint.
Optional authenticated OTA firmware update (PC_ENABLE_OTA).
Layer 7 (Application) - public HTTP routing API.
@ HTTP_POST
Non-idempotent create / action.
#define MAX_AUTH_LEN
Maximum username or password length for HTTP Basic Authentication.
Fully-parsed HTTP/1.1 request.
char path[MAX_PATH_LEN]
URL path, null-terminated; no query string.
char method[PC_METHOD_BUF_SIZE]
HTTP method, null-terminated (OPTIONS, or WebDAV methods when enabled).