DeterministicESPAsyncWebServer v6.28.0
Zero-allocation, bounded-execution async HTTP server for ESP32
Loading...
Searching...
No Matches
coap.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 coap.cpp
6 * @brief CoAP server (RFC 7252): resource table, message codec, and the UDP binding.
7 */
8
10
11#if DETWS_ENABLE_COAP
12
14#include <string.h>
15#if DETWS_ENABLE_COAP_OBSERVE
16#include "services/clock.h" // detws_millis() for Observe notification message IDs / sequencing
17#endif
18
19// CoAP option numbers we understand (RFC 7252 §5.10, RFC 7959). Others are skipped. Wire values
20// compared against the parsed option number, so integer constants in a namespacing struct.
21struct CoapOpt
22{
23 static constexpr uint8_t COAP_OPT_OBSERVE = 6;
24 static constexpr uint8_t COAP_OPT_URI_PATH = 11;
25 static constexpr uint8_t COAP_OPT_CONTENT_FORMAT = 12;
26 static constexpr uint8_t COAP_OPT_URI_QUERY = 15;
27 static constexpr uint8_t COAP_OPT_BLOCK2 = 23; ///< block-wise responses (RFC 7959)
28 static constexpr uint8_t COAP_OPT_BLOCK1 = 27; ///< block-wise request uploads (RFC 7959)
29};
30
31static const uint8_t COAP_PAYLOAD_MARKER = 0xFF;
32static const size_t COAP_MAX_TOKEN = 8;
33
34// ---------------------------------------------------------------------------
35// Resource table + reconstruction scratch (all in BSS - no heap)
36// ---------------------------------------------------------------------------
37
38struct CoapResource
39{
40 const char *path;
41 uint8_t methods;
42 CoapHandler handler;
43};
44
45#if DETWS_ENABLE_COAP_OBSERVE
46// An Observe registration (RFC 7641): a client awaiting notifications on a resource.
47struct CoapObserver
48{
49 bool active;
50 char ip[16];
51 uint16_t port;
52 uint8_t token[8];
53 uint8_t tkl;
54 int res_idx;
55 uint32_t seq; // last Observe value sent
56};
57#endif
58
59// All CoAP server state, owned by one instance. Grouping what were scattered file-scope
60// mutables means state flows explicitly through the call graph (readers take a
61// `const CoapCtx&` and cannot mutate the table), and nothing here is ambient.
62struct CoapCtx
63{
64 CoapResource res[DETWS_COAP_MAX_RESOURCES]; // resource table: set before begin, read-only during dispatch
65 size_t res_count = 0;
66
67 char path[DETWS_COAP_MAX_PATH]; // scratch: reconstructed Uri-Path of the request in flight
68 char query[DETWS_COAP_MAX_QUERY]; // scratch: reconstructed Uri-Query
69 uint8_t pl[DETWS_COAP_MAX_PAYLOAD]; // scratch: handler response body
70 uint8_t tx[DETWS_COAP_MSG_BUF_SIZE]; // scratch: outbound response (request buffer is transport-owned)
71
72#if DETWS_ENABLE_COAP_OBSERVE
73 uint16_t port = DETWS_COAP_OBSERVE_PORT; // UDP port the observe transport notifies from
74 CoapObserver obs[DETWS_COAP_MAX_OBSERVERS];
75 // Last-request fields recorded by coap_server_process_ex() for the Observe transport.
76 int last_observe = -1;
77 uint8_t last_method = 0;
78 uint8_t last_token[8];
79 uint8_t last_tkl = 0;
80#endif
81
82#if DETWS_ENABLE_COAP_BLOCK
83 // Single in-flight Block1 (request upload) reassembly (RFC 7959); one transfer at a time.
84 uint8_t b1[DETWS_COAP_BLOCK1_MAX];
85 size_t b1_len = 0; // bytes reassembled so far (also the next expected offset)
86 uint8_t b1_szx = 0; // negotiated block-size exponent for this transfer
87#endif
88};
89
90static CoapCtx s_coap;
91
92void coap_server_init()
93{
94 s_coap.res_count = 0;
95 memset(s_coap.res, 0, sizeof(s_coap.res));
96#if DETWS_ENABLE_COAP_BLOCK
97 s_coap.b1_len = 0;
98 s_coap.b1_szx = 0;
99#endif
100}
101
102bool coap_server_add_resource(const char *path, uint8_t methods, CoapHandler handler)
103{
104 if (s_coap.res_count >= DETWS_COAP_MAX_RESOURCES || !path || !handler)
105 return false;
106 s_coap.res[s_coap.res_count].path = path;
107 s_coap.res[s_coap.res_count].methods = methods;
108 s_coap.res[s_coap.res_count].handler = handler;
109 s_coap.res_count++;
110 return true;
111}
112
113// ---------------------------------------------------------------------------
114// Helpers
115// ---------------------------------------------------------------------------
116
117// Minimal big-endian unsigned decode of an option value (used for Content-Format).
118static uint32_t opt_uint(const uint8_t *v, size_t n)
119{
120 uint32_t r = 0;
121 for (size_t i = 0; i < n; i++)
122 r = (r << 8) | v[i];
123 return r;
124}
125
126// Append one path/query segment to a reconstruction buffer, prefixed by @p sep
127// (pass '\0' for no separator, e.g. the first Uri-Query segment). Returns false
128// (and leaves the buffer NUL-terminated) if it would overflow.
129static bool seg_append(char *buf, size_t cap, size_t *len, char sep, const uint8_t *seg, size_t seglen)
130{
131 size_t need = (sep ? 1u : 0u) + seglen + 1u; // optional separator + segment + NUL
132 if (*len + need > cap)
133 return false;
134 if (sep)
135 buf[(*len)++] = sep;
136 memcpy(buf + *len, seg, seglen);
137 *len += seglen;
138 buf[*len] = '\0';
139 return true;
140}
141
142static const CoapResource *find_resource(const CoapCtx &c, const char *path)
143{
144 for (size_t i = 0; i < c.res_count; i++)
145 if (strcmp(c.res[i].path, path) == 0)
146 return &c.res[i];
147 return nullptr;
148}
149
150// Emit a bare message: 4-byte header + token, no options/payload. Used for RST and
151// for error responses (4.04 / 4.05 / 5.01) that carry no diagnostic payload.
152static size_t emit_header(uint8_t *out, size_t cap, CoapType type, uint8_t code, uint16_t mid, const uint8_t *token,
153 uint8_t tkl)
154{
155 if (cap < (size_t)(4 + tkl))
156 return 0;
157 out[0] = (uint8_t)((1 << 6) | ((uint8_t)type << 4) | tkl); // Ver=1
158 out[1] = code;
159 out[2] = (uint8_t)(mid >> 8);
160 out[3] = (uint8_t)(mid & 0xFF);
161 if (tkl)
162 memcpy(out + 4, token, tkl);
163 return (size_t)(4 + tkl);
164}
165
166// Encode an unsigned value into 0..3 big-endian bytes, dropping leading zeros
167// (CoAP's minimal uint option encoding). Returns the byte count (0 when v == 0).
168static uint8_t enc_uint_minimal(uint32_t v, uint8_t out[3])
169{
170 uint8_t k = 0;
171 if (v & 0xFF0000)
172 out[k++] = (uint8_t)(v >> 16);
173 if (v & 0xFFFF00)
174 out[k++] = (uint8_t)(v >> 8);
175 if (v)
176 out[k++] = (uint8_t)v;
177 return k;
178}
179
180// Append one option whose number is @p opt_num (>= @p *last_opt) and whose value
181// is @p vlen bytes. Handles an extended option delta (13..268, one extension
182// byte); the value length is always < 13 for the options this server emits, so no
183// extended length is needed. Stops without writing if it would overflow @p cap.
184// Returns the new length.
185static size_t append_opt(uint8_t *resp, size_t cap, size_t n, uint32_t *last_opt, uint32_t opt_num, const uint8_t *val,
186 uint8_t vlen)
187{
188 uint32_t delta = opt_num - *last_opt;
189 uint8_t dn = (uint8_t)(delta < 13 ? delta : 13);
190 bool ext = delta >= 13;
191 if (n + 1u + (ext ? 1u : 0u) + vlen > cap)
192 return n;
193 resp[n++] = (uint8_t)((dn << 4) | vlen);
194 if (ext)
195 resp[n++] = (uint8_t)(delta - 13);
196 for (uint8_t i = 0; i < vlen; i++)
197 resp[n++] = val[i];
198 *last_opt = opt_num;
199 return n;
200}
201
202// Append options (Observe, Content-Format, Block2, Block1 - ascending order) and
203// the payload to a message already holding @p n header+token bytes. @p observe_seq
204// < 0 omits the Observe option (also omitted unless @p code is a 2.xx success);
205// @p block2_val / @p block1_val < 0 omit the respective Block option (their value
206// is the raw RFC 7959 option uint, (NUM<<4)|(M<<3)|SZX). Stops (shipping what fits)
207// rather than overflowing @p cap. Returns the new length.
208static size_t emit_options_payload(uint8_t *resp, size_t cap, size_t n, uint8_t code, int32_t observe_seq,
209 CoapContentFormat content_format, int32_t block2_val, int32_t block1_val,
210 const uint8_t *payload, size_t payload_len)
211{
212 uint32_t last_opt = 0;
213 uint8_t v[3];
214
215 if (observe_seq >= 0 && (code >> 5) == 2)
216 n = append_opt(resp, cap, n, &last_opt, CoapOpt::COAP_OPT_OBSERVE, v,
217 enc_uint_minimal((uint32_t)observe_seq & 0xFFFFFF, v));
218
219 if (content_format != CoapContentFormat::COAP_CF_NONE)
220 n = append_opt(resp, cap, n, &last_opt, CoapOpt::COAP_OPT_CONTENT_FORMAT, v,
221 enc_uint_minimal((uint16_t)content_format, v));
222
223#if DETWS_ENABLE_COAP_BLOCK
224 if (block2_val >= 0)
225 n = append_opt(resp, cap, n, &last_opt, CoapOpt::COAP_OPT_BLOCK2, v, enc_uint_minimal((uint32_t)block2_val, v));
226 if (block1_val >= 0)
227 n = append_opt(resp, cap, n, &last_opt, CoapOpt::COAP_OPT_BLOCK1, v, enc_uint_minimal((uint32_t)block1_val, v));
228#else
229 (void)block2_val;
230 (void)block1_val;
231#endif
232
233 if (payload_len)
234 {
235 if (n + 1 + payload_len > cap)
236 return n;
237 resp[n++] = COAP_PAYLOAD_MARKER;
238 memcpy(resp + n, payload, payload_len);
239 n += payload_len;
240 }
241 return n;
242}
243
244// ---------------------------------------------------------------------------
245// Core processing (no sockets, no heap)
246// ---------------------------------------------------------------------------
247
248size_t coap_server_process_ex(const uint8_t *req, size_t req_len, uint8_t *resp, size_t resp_cap, int32_t observe_seq)
249{
250 if (req_len < 4)
251 return 0; // too short for a header
252
253 uint8_t ver = (req[0] >> 6) & 0x03;
254 CoapType type = (CoapType)((req[0] >> 4) & 0x03);
255 uint8_t tkl = req[0] & 0x0F;
256 uint8_t code = req[1];
257 uint16_t mid = (uint16_t)((req[2] << 8) | req[3]);
258
259 // Reply type for a request: piggybacked ACK for CON, NON for NON. ACK/RST we
260 // receive are not requests - ignore them.
261 if (type != CoapType::COAP_TYPE_CON && type != CoapType::COAP_TYPE_NON)
262 return 0;
263 CoapType rsp_type = (type == CoapType::COAP_TYPE_CON) ? CoapType::COAP_TYPE_ACK : CoapType::COAP_TYPE_NON;
264
265 // A malformed message: bad version or reserved TKL (9..15). RFC 7252 §4.2:
266 // reject a CON with an RST; stay silent for a NON.
267 if (ver != 1 || tkl > COAP_MAX_TOKEN)
268 return (type == CoapType::COAP_TYPE_CON)
269 ? emit_header(resp, resp_cap, CoapType::COAP_TYPE_RST, 0, mid, nullptr, 0)
270 : 0;
271
272 const uint8_t *p = req + 4;
273 const uint8_t *end = req + req_len;
274 if (p + tkl > end)
275 return (type == CoapType::COAP_TYPE_CON)
276 ? emit_header(resp, resp_cap, CoapType::COAP_TYPE_RST, 0, mid, nullptr, 0)
277 : 0;
278 const uint8_t *token = p;
279 p += tkl;
280
281 // An empty message (Code 0.00): CON is a ping -> RST; anything else -> ignore.
282 if (code == 0)
283 return (type == CoapType::COAP_TYPE_CON)
284 ? emit_header(resp, resp_cap, CoapType::COAP_TYPE_RST, 0, mid, nullptr, 0)
285 : 0;
286
287#if DETWS_ENABLE_COAP_OBSERVE
288 s_coap.last_observe = -1;
289 s_coap.last_method = code;
290 s_coap.last_tkl = tkl;
291 if (tkl)
292 memcpy(s_coap.last_token, token, tkl);
293#endif
294
295 // Decode options, reconstructing Uri-Path / Uri-Query and reading Content-Format.
296 size_t path_len = 0;
297 size_t query_len = 0;
298 s_coap.path[0] = '\0';
299 s_coap.query[0] = '\0';
300 CoapContentFormat req_cf = CoapContentFormat::COAP_CF_NONE;
301 const uint8_t *payload = nullptr;
302 size_t payload_len = 0;
303 uint32_t opt_num = 0;
304 bool bad = false;
305 bool bad_option = false; // unrecognized critical (odd-numbered) option seen (RFC 7252 5.4.1)
306#if DETWS_ENABLE_COAP_BLOCK
307 int32_t req_block1 = -1; // request Block1 option value (RFC 7959), or -1 if absent
308 int32_t req_block2 = -1; // request Block2 option value, or -1 if absent
309#endif
310
311 while (p < end)
312 {
313 uint8_t b = *p++;
314 if (b == COAP_PAYLOAD_MARKER)
315 {
316 payload = p;
317 payload_len = (size_t)(end - p);
318 break;
319 }
320 uint32_t delta = b >> 4;
321 uint32_t olen = b & 0x0F;
322 if (delta == 15 || olen == 15) // 15 is reserved outside the payload marker
323 {
324 bad = true;
325 break;
326 }
327 // Extended delta / length (13 -> +1 byte; 14 -> +2 bytes).
328 if (delta == 13)
329 {
330 if (p >= end)
331 {
332 bad = true;
333 break;
334 }
335 delta = (uint32_t)(*p++) + 13;
336 }
337 else if (delta == 14)
338 {
339 if (p + 2 > end)
340 {
341 bad = true;
342 break;
343 }
344 delta = (uint32_t)((p[0] << 8) | p[1]) + 269;
345 p += 2;
346 }
347 if (olen == 13)
348 {
349 if (p >= end)
350 {
351 bad = true;
352 break;
353 }
354 olen = (uint32_t)(*p++) + 13;
355 }
356 else if (olen == 14)
357 {
358 if (p + 2 > end)
359 {
360 bad = true;
361 break;
362 }
363 olen = (uint32_t)((p[0] << 8) | p[1]) + 269;
364 p += 2;
365 }
366 if (p + olen > end)
367 {
368 bad = true;
369 break;
370 }
371 opt_num += delta;
372 const uint8_t *val = p;
373 p += olen;
374
375 switch (opt_num)
376 {
377 case CoapOpt::COAP_OPT_URI_PATH:
378 if (!seg_append(s_coap.path, sizeof(s_coap.path), &path_len, '/', val, olen))
379 bad = true;
380 break;
381 case CoapOpt::COAP_OPT_URI_QUERY:
382 if (!seg_append(s_coap.query, sizeof(s_coap.query), &query_len, query_len ? '&' : '\0', val, olen))
383 bad = true;
384 break;
385 case CoapOpt::COAP_OPT_CONTENT_FORMAT:
386 req_cf = (CoapContentFormat)opt_uint(val, olen);
387 break;
388#if DETWS_ENABLE_COAP_OBSERVE
389 case CoapOpt::COAP_OPT_OBSERVE:
390 s_coap.last_observe = (int)opt_uint(val, olen); // 0 = register, 1 = deregister
391 break;
392#endif
393#if DETWS_ENABLE_COAP_BLOCK
394 case CoapOpt::COAP_OPT_BLOCK1: // block-wise request uploads (RFC 7959)
395 if (olen > 3)
396 bad = true;
397 else
398 req_block1 = (int32_t)opt_uint(val, olen);
399 break;
400 case CoapOpt::COAP_OPT_BLOCK2: // block-wise responses (RFC 7959)
401 if (olen > 3)
402 bad = true;
403 else
404 req_block2 = (int32_t)opt_uint(val, olen);
405 break;
406#endif
407 default:
408 // RFC 7252 5.4.1: an unrecognized option of class "critical" (odd option
409 // number) in a request must cause a 4.02 (Bad Option). Elective (even)
410 // unknown options are silently ignored. Block1/Block2 are critical, so a
411 // build without COAP_BLOCK correctly rejects them here.
412 if (opt_num & 1)
413 bad_option = true;
414 break;
415 }
416 if (bad)
417 break;
418 }
419
420 if (bad)
421 return emit_header(resp, resp_cap, rsp_type, (uint8_t)CoapResponseCode::COAP_RSP_BAD_REQUEST, mid, token, tkl);
422 if (bad_option)
423 return emit_header(resp, resp_cap, rsp_type, (uint8_t)CoapResponseCode::COAP_RSP_BAD_OPTION, mid, token, tkl);
424
425 if (path_len == 0)
426 {
427 s_coap.path[0] = '/';
428 s_coap.path[1] = '\0';
429 }
430
431 // Only class-0 GET/POST/PUT/DELETE are supported request methods. RFC 7252 5.8:
432 // "A request with an unrecognized or unsupported Method Code MUST generate a 4.05
433 // (Method Not Allowed) piggybacked response."
434 if ((code >> 5) != 0 || code < (uint8_t)CoapMethod::COAP_GET || code > (uint8_t)CoapMethod::COAP_DELETE)
435 return emit_header(resp, resp_cap, rsp_type, (uint8_t)CoapResponseCode::COAP_RSP_METHOD_NOT_ALLOWED, mid, token,
436 tkl);
437
438 // The response the emit path below serializes (block-wise if large). Filled
439 // either by the .well-known/core discovery listing or by a resource handler.
440 CoapResponse cresp;
441 cresp.code = (uint8_t)CoapResponseCode::COAP_RSP_CONTENT;
442 cresp.content_format = CoapContentFormat::COAP_CF_NONE;
443 cresp.payload = s_coap.pl;
444 cresp.payload_cap = sizeof(s_coap.pl);
445 cresp.payload_len = 0;
446 int32_t block1_echo = -1; // Block1 option to echo on the final-block response
447
448 // RFC 6690: GET /.well-known/core returns the CoRE Link Format listing of the
449 // registered resources, e.g. "</info>,</led>". Block2 (below) pages it if large.
450 if (strcmp(s_coap.path, "/.well-known/core") == 0)
451 {
452 if (code != (uint8_t)CoapMethod::COAP_GET)
453 return emit_header(resp, resp_cap, rsp_type, (uint8_t)CoapResponseCode::COAP_RSP_METHOD_NOT_ALLOWED, mid,
454 token, tkl);
455 size_t pl = 0;
456 for (size_t i = 0; i < s_coap.res_count; i++)
457 {
458 const char *rpath = s_coap.res[i].path;
459 size_t plen = strnlen(rpath, sizeof(s_coap.pl));
460 size_t need = (pl ? 1u : 0u) + 2u + plen; // optional ',' + '<' + path + '>'
461 if (pl + need > sizeof(s_coap.pl))
462 break; // listing exceeds the payload buffer; truncate at a resource boundary
463 if (pl)
464 s_coap.pl[pl++] = ',';
465 s_coap.pl[pl++] = '<';
466 memcpy(s_coap.pl + pl, rpath, plen);
467 pl += plen;
468 s_coap.pl[pl++] = '>';
469 }
470 cresp.content_format = CoapContentFormat::COAP_CF_LINK;
471 cresp.payload_len = pl;
472 }
473 else
474 {
475 const CoapResource *r = find_resource(s_coap, s_coap.path);
476 if (!r)
477 return emit_header(resp, resp_cap, rsp_type, (uint8_t)CoapResponseCode::COAP_RSP_NOT_FOUND, mid, token,
478 tkl);
479 if (!(r->methods & (1u << code)))
480 return emit_header(resp, resp_cap, rsp_type, (uint8_t)CoapResponseCode::COAP_RSP_METHOD_NOT_ALLOWED, mid,
481 token, tkl);
482
483 const uint8_t *eff_payload = payload;
484 size_t eff_payload_len = payload_len;
485
486#if DETWS_ENABLE_COAP_BLOCK
487 // --- Block1: reassemble a chunked POST/PUT payload (RFC 7959 §2.5) ---
488 if (req_block1 >= 0 && (code == (uint8_t)CoapMethod::COAP_POST || code == (uint8_t)CoapMethod::COAP_PUT))
489 {
490 uint32_t b = (uint32_t)req_block1;
491 uint32_t num = b >> 4;
492 uint8_t more = (uint8_t)((b >> 3) & 1);
493 uint8_t szx = (uint8_t)(b & 7);
494 if (szx == 7) // reserved block size
495 return emit_header(resp, resp_cap, rsp_type, (uint8_t)CoapResponseCode::COAP_RSP_BAD_OPTION, mid, token,
496 tkl);
497 uint32_t bsize = 1u << (szx + 4);
498 if (num == 0) // first block starts a fresh transfer
499 {
500 s_coap.b1_len = 0;
501 s_coap.b1_szx = szx;
502 }
503 // The block size is fixed for a transfer; an offset gap means a lost or
504 // reordered block. Either way the reassembly cannot continue: 4.08.
505 if (szx != s_coap.b1_szx || (size_t)num * bsize != s_coap.b1_len)
506 {
507 s_coap.b1_len = 0;
508 return emit_header(resp, resp_cap, rsp_type, (uint8_t)CoapResponseCode::COAP_RSP_REQUEST_INCOMPLETE,
509 mid, token, tkl);
510 }
511 if (s_coap.b1_len + payload_len > sizeof(s_coap.b1))
512 {
513 s_coap.b1_len = 0;
514 return emit_header(resp, resp_cap, rsp_type, (uint8_t)CoapResponseCode::COAP_RSP_REQUEST_TOO_LARGE, mid,
515 token, tkl);
516 }
517 if (payload_len)
518 memcpy(s_coap.b1 + s_coap.b1_len, payload, payload_len);
519 s_coap.b1_len += payload_len;
520
521 if (more)
522 {
523 // More blocks coming: acknowledge with 2.31 Continue + Block1 echo
524 // (no representation yet; the handler runs only on the final block).
525 size_t cn = emit_header(resp, resp_cap, rsp_type, (uint8_t)CoapResponseCode::COAP_RSP_CONTINUE, mid,
526 token, tkl);
527 if (cn == 0)
528 return 0;
529 return emit_options_payload(resp, resp_cap, cn, (uint8_t)CoapResponseCode::COAP_RSP_CONTINUE, -1,
530 CoapContentFormat::COAP_CF_NONE, -1,
531 (int32_t)((num << 4) | (1u << 3) | szx), nullptr, 0);
532 }
533 // Final block: hand the whole reassembled payload to the handler.
534 eff_payload = s_coap.b1;
535 eff_payload_len = s_coap.b1_len;
536 block1_echo = (int32_t)((num << 4) | szx); // More = 0
537 }
538#endif
539
540 CoapRequest creq;
541 creq.method = (CoapMethod)code;
542 creq.path = s_coap.path;
543 creq.query = s_coap.query;
544 creq.payload = eff_payload;
545 creq.payload_len = eff_payload_len;
546 creq.content_format = req_cf;
547
548 r->handler(&creq, &cresp);
549 if (cresp.payload_len > sizeof(s_coap.pl))
550 cresp.payload_len = sizeof(s_coap.pl); // defensive clamp
551 }
552
553 int32_t block2_echo = -1;
554
555#if DETWS_ENABLE_COAP_BLOCK
556 if (block1_echo >= 0)
557 s_coap.b1_len = 0; // the reassembled upload has been handed to the handler; clear it
558
559 // --- Block2: serve a (large or explicitly requested) representation one
560 // block at a time (RFC 7959 §2.4). Applies only to a successful body. ---
561 if ((cresp.code >> 5) == 2)
562 {
563 uint8_t szx = DETWS_COAP_BLOCK_SZX_MAX;
564 uint32_t num = 0;
565 bool block_wise = false;
566 if (req_block2 >= 0)
567 {
568 uint32_t b = (uint32_t)req_block2;
569 num = b >> 4;
570 szx = (uint8_t)(b & 7);
571 if (szx == 7)
572 return emit_header(resp, resp_cap, rsp_type, (uint8_t)CoapResponseCode::COAP_RSP_BAD_OPTION, mid, token,
573 tkl);
574 if (szx > DETWS_COAP_BLOCK_SZX_MAX)
576 block_wise = true; // the client asked for block-wise transfer
577 }
578 else if (cresp.payload_len > (size_t)(1u << (szx + 4)))
579 {
580 block_wise = true; // body too large for one block -> start at block 0
581 }
582 if (block_wise)
583 {
584 uint32_t bsize = 1u << (szx + 4);
585 size_t off = (size_t)num * bsize;
586 // A block number past the end of the representation is a bad request.
587 if (off > cresp.payload_len || (off == cresp.payload_len && num > 0))
588 return emit_header(resp, resp_cap, rsp_type, (uint8_t)CoapResponseCode::COAP_RSP_BAD_REQUEST, mid,
589 token, tkl);
590 size_t this_len = cresp.payload_len - off;
591 uint8_t more = 0;
592 if (this_len > bsize)
593 {
594 this_len = bsize;
595 more = 1;
596 }
597 block2_echo = (int32_t)((num << 4) | ((uint32_t)more << 3) | szx);
598 cresp.payload += off;
599 cresp.payload_len = this_len;
600 }
601 }
602#endif
603
604 // Build the response: header + token + options (ascending order) + payload.
605 size_t n = emit_header(resp, resp_cap, rsp_type, cresp.code, mid, token, tkl);
606 if (n == 0)
607 return 0;
608 return emit_options_payload(resp, resp_cap, n, cresp.code, observe_seq, cresp.content_format, block2_echo,
609 block1_echo, cresp.payload, cresp.payload_len);
610}
611
612size_t coap_server_process(const uint8_t *req, size_t req_len, uint8_t *resp, size_t resp_cap)
613{
614 return coap_server_process_ex(req, req_len, resp, resp_cap, -1); // no Observe option
615}
616
617// ---------------------------------------------------------------------------
618// UDP transport (det_udp_listen is a host stub on non-Arduino builds)
619// ---------------------------------------------------------------------------
620
621#if DETWS_ENABLE_COAP_OBSERVE
622// ---------------------------------------------------------------------------
623// Observe registry + notifications (RFC 7641)
624// ---------------------------------------------------------------------------
625
626static int find_resource_index(const CoapCtx &c, const char *path)
627{
628 for (size_t i = 0; i < c.res_count; i++)
629 if (strcmp(c.res[i].path, path) == 0)
630 return (int)i;
631 return -1;
632}
633
634static bool same_token(const CoapObserver *o, const uint8_t *token, uint8_t tkl)
635{
636 return o->tkl == tkl && (tkl == 0 || memcmp(o->token, token, tkl) == 0);
637}
638
639// Find/refresh an observer; create one on first registration. Returns its slot or -1.
640static int obs_register(const char *ip, uint16_t port, const uint8_t *token, uint8_t tkl, int res_idx)
641{
642 for (int i = 0; i < DETWS_COAP_MAX_OBSERVERS; i++)
643 if (s_coap.obs[i].active && s_coap.obs[i].res_idx == res_idx && s_coap.obs[i].port == port &&
644 same_token(&s_coap.obs[i], token, tkl) && strcmp(s_coap.obs[i].ip, ip) == 0)
645 return i; // already observing
646 for (int i = 0; i < DETWS_COAP_MAX_OBSERVERS; i++)
647 if (!s_coap.obs[i].active)
648 {
649 s_coap.obs[i].active = true;
650 strncpy(s_coap.obs[i].ip, ip, sizeof(s_coap.obs[i].ip) - 1);
651 s_coap.obs[i].ip[sizeof(s_coap.obs[i].ip) - 1] = '\0';
652 s_coap.obs[i].port = port;
653 s_coap.obs[i].tkl = tkl;
654 if (tkl)
655 memcpy(s_coap.obs[i].token, token, tkl);
656 s_coap.obs[i].res_idx = res_idx;
657 s_coap.obs[i].seq = 1;
658 return i;
659 }
660 return -1; // registry full -> observation declined (resource still returned)
661}
662
663// Remove a specific observation (deregister GET) or every observation from a peer
664// (a Reset). Pass token=nullptr to drop all of @p ip:@p port.
665static void obs_remove(const char *ip, uint16_t port, const uint8_t *token, uint8_t tkl)
666{
667 for (int i = 0; i < DETWS_COAP_MAX_OBSERVERS; i++)
668 if (s_coap.obs[i].active && s_coap.obs[i].port == port && strcmp(s_coap.obs[i].ip, ip) == 0 &&
669 (token == nullptr || same_token(&s_coap.obs[i], token, tkl)))
670 s_coap.obs[i].active = false;
671}
672
673void coap_notify(const char *path)
674{
675 int ridx = find_resource_index(s_coap, path);
676 if (ridx < 0)
677 return;
678 for (int i = 0; i < DETWS_COAP_MAX_OBSERVERS; i++)
679 {
680 if (!s_coap.obs[i].active || s_coap.obs[i].res_idx != ridx)
681 continue;
682 // Re-render the resource via its GET handler.
683 CoapRequest creq;
684 creq.method = CoapMethod::COAP_GET;
685 creq.path = s_coap.res[ridx].path;
686 creq.query = "";
687 creq.payload = nullptr;
688 creq.payload_len = 0;
689 creq.content_format = CoapContentFormat::COAP_CF_NONE;
690 CoapResponse cresp;
691 cresp.code = (uint8_t)CoapResponseCode::COAP_RSP_CONTENT;
692 cresp.content_format = CoapContentFormat::COAP_CF_NONE;
693 cresp.payload = s_coap.pl;
694 cresp.payload_cap = sizeof(s_coap.pl);
695 cresp.payload_len = 0;
696 s_coap.res[ridx].handler(&creq, &cresp);
697 if (cresp.payload_len > sizeof(s_coap.pl))
698 cresp.payload_len = sizeof(s_coap.pl);
699
700 // Build a NON notification: header + token + Observe(seq) + body.
701 uint16_t mid = (uint16_t)detws_millis();
702 s_coap.obs[i].seq = (s_coap.obs[i].seq + 1) & 0xFFFFFF;
703 size_t n = emit_header(s_coap.tx, sizeof(s_coap.tx), CoapType::COAP_TYPE_NON, cresp.code, mid,
704 s_coap.obs[i].token, s_coap.obs[i].tkl);
705 if (n)
706 n = emit_options_payload(s_coap.tx, sizeof(s_coap.tx), n, cresp.code, (int32_t)s_coap.obs[i].seq,
707 cresp.content_format, -1, -1, cresp.payload, cresp.payload_len);
708 if (!n || !det_udp_listener_sendto(s_coap.port, s_coap.obs[i].ip, s_coap.obs[i].port, s_coap.tx, n))
709 s_coap.obs[i].active = false; // unreachable -> drop the observer
710 }
711}
712
713static void coap_udp_handler(const uint8_t *data, size_t len, struct DetUdpPeer *peer, void *ctx)
714{
715 (void)ctx;
716 char ip[16];
717 uint16_t pport = 0;
718 bool have_peer = det_udp_peer_addr(peer, ip, sizeof(ip), &pport);
719
720 // A Reset from a client rejects our notification -> drop its observations.
721 if (len >= 1 && ((data[0] >> 4) & 0x03) == (uint8_t)CoapType::COAP_TYPE_RST)
722 {
723 if (have_peer)
724 obs_remove(ip, pport, nullptr, 0);
725 return;
726 }
727
728 size_t rn = coap_server_process_ex(data, len, s_coap.tx, sizeof(s_coap.tx), -1);
729 if (!rn)
730 return;
731
732 if (s_coap.last_method == (uint8_t)CoapMethod::COAP_GET && s_coap.last_observe == 0 && have_peer)
733 {
734 int ridx = find_resource_index(s_coap, s_coap.path);
735 if (ridx >= 0)
736 {
737 int slot = obs_register(ip, pport, s_coap.last_token, s_coap.last_tkl, ridx);
738 if (slot >= 0)
739 {
740 // Re-encode the response carrying the Observe option (registration ack).
741 size_t rn2 =
742 coap_server_process_ex(data, len, s_coap.tx, sizeof(s_coap.tx), (int32_t)s_coap.obs[slot].seq);
743 if (rn2)
744 rn = rn2;
745 }
746 }
747 }
748 else if (s_coap.last_observe == 1 && have_peer)
749 {
750 obs_remove(ip, pport, s_coap.last_token, s_coap.last_tkl);
751 }
752
753 det_udp_send(peer, s_coap.tx, rn);
754}
755
756void coap_server_begin_udp(uint16_t port)
757{
758 s_coap.port = port;
759 for (int i = 0; i < DETWS_COAP_MAX_OBSERVERS; i++)
760 s_coap.obs[i].active = false;
761 det_udp_listen(port, coap_udp_handler, nullptr);
762}
763
764#else // Observe disabled: the basic request/response handler
765
766static void coap_udp_handler(const uint8_t *data, size_t len, struct DetUdpPeer *peer, void *ctx)
767{
768 (void)ctx;
769 size_t rn = coap_server_process(data, len, s_coap.tx, sizeof(s_coap.tx));
770 if (rn)
771 det_udp_send(peer, s_coap.tx, rn);
772}
773
774void coap_server_begin_udp(uint16_t port)
775{
776 det_udp_listen(port, coap_udp_handler, nullptr);
777}
778
779#endif // DETWS_ENABLE_COAP_OBSERVE
780
781#endif // DETWS_ENABLE_COAP
#define DETWS_COAP_BLOCK1_MAX
Reassembly buffer for a block-wise (Block1) request upload, in bytes.
#define DETWS_COAP_MAX_PAYLOAD
Maximum CoAP request/response payload in bytes.
#define DETWS_COAP_OBSERVE_PORT
Default UDP port the CoAP observe transport notifies from (IANA well-known 5683).
#define DETWS_COAP_MAX_RESOURCES
Maximum registered CoAP resources (the server's fixed routing table).
#define DETWS_COAP_MAX_PATH
Maximum reconstructed Uri-Path length, including separators and the leading '/'.
#define DETWS_COAP_MAX_QUERY
Maximum reconstructed Uri-Query length (segments joined by '&').
#define DETWS_COAP_MSG_BUF_SIZE
Static response-datagram buffer for the CoAP UDP server.
#define DETWS_COAP_MAX_OBSERVERS
Maximum simultaneous CoAP observers (one slot per observed resource per client).
#define DETWS_COAP_BLOCK_SZX_MAX
Largest block-size exponent (SZX) the server will use: block size = 2^(SZX+4) bytes,...
Pluggable monotonic clock for all library timing.
uint32_t detws_millis(void)
The library's monotonic time at 1000 Hz (milliseconds).
Definition clock.h:71
Zero-heap CoAP server (RFC 7252): message codec + a fixed resource table.
bool det_udp_peer_addr(const struct DetUdpPeer *peer, char *ip_out, size_t ip_cap, uint16_t *port_out)
Copy a received peer's source IPv4 address (dotted-quad) and port out.
Definition udp.cpp:224
bool det_udp_send(struct DetUdpPeer *peer, const uint8_t *data, size_t len)
Send a datagram back to the peer captured during the handler call.
Definition udp.cpp:178
bool det_udp_listen(uint16_t port, DetUdpHandler handler, void *ctx)
Bind a UDP port and route incoming datagrams to handler.
Definition udp.cpp:151
bool det_udp_listener_sendto(uint16_t listen_port, const char *dst_ip, uint16_t dst_port, const uint8_t *data, size_t len)
Send a datagram from the listener bound on listen_port to an address.
Definition udp.cpp:234
Layer 4 (Transport) - connectionless UDP datagram service.