ProtoCore v0.0.2
Deterministic, zero-heap network stack for embedded targets
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 PC_ENABLE_COAP
12
14#include <string.h>
15#if PC_ENABLE_COAP_OBSERVE || PC_COAP_DEDUP_ENTRIES > 0
16#include "services/system/clock.h" // pc_millis(): Observe notification sequencing + dedup entry freshness
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 PC_COAP_DEDUP_ENTRIES > 0
46// A cached response for message de-duplication (RFC 7252 §4.5). Keyed by the FULL source endpoint
47// (address string + port) and the Message-ID - never a hash, so two peers cannot collide onto one
48// entry and be handed each other's cached response. A retransmitted Confirmable request that hits a
49// fresh entry is re-answered from `resp` without re-running its handler, so a client's CON retransmit
50// cannot execute a non-idempotent request twice.
51struct CoapDedupEntry
52{
53 bool valid;
54 char ip[16];
55 uint16_t port;
56 uint16_t mid;
57 uint32_t stamp_ms; // pc_millis() at store; the entry expires after PC_COAP_DEDUP_LIFETIME_MS
58 uint16_t len; // cached response length
59 uint8_t resp[PC_COAP_DEDUP_RESP_MAX];
60};
61#endif
62
63#if PC_ENABLE_COAP_OBSERVE
64// An Observe registration (RFC 7641): a client awaiting notifications on a resource.
65struct CoapObserver
66{
67 bool active;
68 char ip[16];
69 uint16_t port;
70 uint8_t token[8];
71 uint8_t tkl;
72 int res_idx;
73 uint32_t seq; // last Observe value sent
74};
75#endif
76
77// All CoAP server state, owned by one instance. Grouping what were scattered file-scope
78// mutables means state flows explicitly through the call graph (readers take a
79// `const CoapCtx&` and cannot mutate the table), and nothing here is ambient.
80struct CoapCtx
81{
82 CoapResource res[PC_COAP_MAX_RESOURCES]; // resource table: set before begin, read-only during dispatch
83 size_t res_count = 0;
84
85 char path[PC_COAP_MAX_PATH]; // scratch: reconstructed Uri-Path of the request in flight
86 char query[PC_COAP_MAX_QUERY]; // scratch: reconstructed Uri-Query
87 uint8_t pl[PC_COAP_MAX_PAYLOAD]; // scratch: handler response body
88 uint8_t tx[PC_COAP_MSG_BUF_SIZE]; // scratch: outbound response (request buffer is transport-owned)
89
90#if PC_COAP_DEDUP_ENTRIES > 0
91 CoapDedupEntry dedup[PC_COAP_DEDUP_ENTRIES]; // message de-duplication cache (RFC 7252 §4.5)
92#endif
93
94#if PC_ENABLE_COAP_OBSERVE
95 uint16_t port = PC_COAP_OBSERVE_PORT; // UDP port the observe transport notifies from
96 CoapObserver obs[PC_COAP_MAX_OBSERVERS];
97 // Last-request fields recorded by pc_coap_server_process_ex() for the Observe transport.
98 int last_observe = -1;
99 uint8_t last_method = 0;
100 uint8_t last_token[8];
101 uint8_t last_tkl = 0;
102#endif
103
104#if PC_ENABLE_COAP_BLOCK
105 // Single in-flight Block1 (request upload) reassembly (RFC 7959); one transfer at a time.
106 uint8_t b1[PC_COAP_BLOCK1_MAX];
107 size_t b1_len = 0; // bytes reassembled so far (also the next expected offset)
108 uint8_t b1_szx = 0; // negotiated block-size exponent for this transfer
109#endif
110};
111
112static CoapCtx s_coap;
113
114void pc_coap_server_reset()
115{
116 s_coap.res_count = 0;
117 memset(s_coap.res, 0, sizeof(s_coap.res));
118#if PC_ENABLE_COAP_BLOCK
119 s_coap.b1_len = 0;
120 s_coap.b1_szx = 0;
121#endif
122#if PC_COAP_DEDUP_ENTRIES > 0
123 for (size_t i = 0; i < PC_COAP_DEDUP_ENTRIES; i++)
124 {
125 s_coap.dedup[i].valid = false;
126 }
127#endif
128}
129
130bool pc_coap_server_add_resource(const char *path, uint8_t methods, CoapHandler handler)
131{
132 if (s_coap.res_count >= PC_COAP_MAX_RESOURCES || !path || !handler)
133 {
134 return false;
135 }
136 s_coap.res[s_coap.res_count].path = path;
137 s_coap.res[s_coap.res_count].methods = methods;
138 s_coap.res[s_coap.res_count].handler = handler;
139 s_coap.res_count++;
140 return true;
141}
142
143// ---------------------------------------------------------------------------
144// Helpers
145// ---------------------------------------------------------------------------
146
147// Minimal big-endian unsigned decode of an option value (used for Content-Format).
148static uint32_t opt_uint(const uint8_t *v, size_t n)
149{
150 uint32_t r = 0;
151 for (size_t i = 0; i < n; i++)
152 {
153 r = (r << 8) | v[i];
154 }
155 return r;
156}
157
158// Append one path/query segment to a reconstruction buffer, prefixed by @p sep
159// (pass '\0' for no separator, e.g. the first Uri-Query segment). Returns false
160// (and leaves the buffer NUL-terminated) if it would overflow.
161static bool seg_append(char *buf, size_t cap, size_t *len, char sep, const uint8_t *seg, size_t seglen)
162{
163 size_t need = (sep ? 1u : 0u) + seglen + 1u; // optional separator + segment + NUL
164 if (*len + need > cap)
165 {
166 return false;
167 }
168 if (sep)
169 {
170 buf[(*len)++] = sep;
171 }
172 memcpy(buf + *len, seg, seglen);
173 *len += seglen;
174 buf[*len] = '\0';
175 return true;
176}
177
178static const CoapResource *find_resource(const CoapCtx &c, const char *path)
179{
180 for (size_t i = 0; i < c.res_count; i++)
181 {
182 if (strcmp(c.res[i].path, path) == 0)
183 {
184 return &c.res[i];
185 }
186 }
187 return nullptr;
188}
189
190// Emit a bare message: 4-byte header + token, no options/payload. Used for RST and
191// for error responses (4.04 / 4.05 / 5.01) that carry no diagnostic payload.
192static size_t emit_header(uint8_t *out, size_t cap, CoapType type, uint8_t code, uint16_t mid, const uint8_t *token,
193 uint8_t tkl)
194{
195 if (cap < (size_t)(4 + tkl))
196 {
197 return 0;
198 }
199 out[0] = (uint8_t)((1 << 6) | ((uint8_t)type << 4) | tkl); // Ver=1
200 out[1] = code;
201 out[2] = (uint8_t)(mid >> 8);
202 out[3] = (uint8_t)(mid & 0xFF);
203 if (tkl)
204 {
205 memcpy(out + 4, token, tkl);
206 }
207 return (size_t)(4 + tkl);
208}
209
210// Encode an unsigned value into 0..3 big-endian bytes, dropping leading zeros
211// (CoAP's minimal uint option encoding). Returns the byte count (0 when v == 0).
212static uint8_t enc_uint_minimal(uint32_t v, uint8_t out[3])
213{
214 uint8_t k = 0;
215 if (v & 0xFF0000)
216 {
217 out[k++] = (uint8_t)(v >> 16);
218 }
219 if (v & 0xFFFF00)
220 {
221 out[k++] = (uint8_t)(v >> 8);
222 }
223 if (v)
224 {
225 out[k++] = (uint8_t)v;
226 }
227 return k;
228}
229
230// Append one option whose number is @p opt_num (>= @p *last_opt) and whose value
231// is @p vlen bytes. Handles an extended option delta (13..268, one extension
232// byte); the value length is always < 13 for the options this server emits, so no
233// extended length is needed. Stops without writing if it would overflow @p cap.
234// Returns the new length.
235static 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,
236 uint8_t vlen)
237{
238 uint32_t delta = opt_num - *last_opt;
239 uint8_t dn = (uint8_t)(delta < 13 ? delta : 13);
240 bool ext = delta >= 13;
241 if (n + 1u + (ext ? 1u : 0u) + vlen > cap)
242 {
243 return n;
244 }
245 resp[n++] = (uint8_t)((dn << 4) | vlen);
246 if (ext)
247 {
248 resp[n++] = (uint8_t)(delta - 13);
249 }
250 for (uint8_t i = 0; i < vlen; i++)
251 {
252 resp[n++] = val[i];
253 }
254 *last_opt = opt_num;
255 return n;
256}
257
258// Append options (Observe, Content-Format, Block2, Block1 - ascending order) and
259// the payload to a message already holding @p n header+token bytes. @p observe_seq
260// < 0 omits the Observe option (also omitted unless @p code is a 2.xx success);
261// @p block2_val / @p block1_val < 0 omit the respective Block option (their value
262// is the raw RFC 7959 option uint, (NUM<<4)|(M<<3)|SZX). Stops (shipping what fits)
263// rather than overflowing @p cap. Returns the new length.
264static size_t emit_options_payload(uint8_t *resp, size_t cap, size_t n, uint8_t code, int32_t observe_seq,
265 CoapContentFormat content_format, int32_t block2_val, int32_t block1_val,
266 const uint8_t *payload, size_t payload_len)
267{
268 uint32_t last_opt = 0;
269 uint8_t v[3];
270
271 if (observe_seq >= 0 && (code >> 5) == 2)
272 {
273 n = append_opt(resp, cap, n, &last_opt, CoapOpt::COAP_OPT_OBSERVE, v,
274 enc_uint_minimal((uint32_t)observe_seq & 0xFFFFFF, v));
275 }
276
277 if (content_format != CoapContentFormat::COAP_CF_NONE)
278 {
279 n = append_opt(resp, cap, n, &last_opt, CoapOpt::COAP_OPT_CONTENT_FORMAT, v,
280 enc_uint_minimal((uint16_t)content_format, v));
281 }
282
283#if PC_ENABLE_COAP_BLOCK
284 if (block2_val >= 0)
285 {
286 n = append_opt(resp, cap, n, &last_opt, CoapOpt::COAP_OPT_BLOCK2, v, enc_uint_minimal((uint32_t)block2_val, v));
287 }
288 if (block1_val >= 0)
289 {
290 n = append_opt(resp, cap, n, &last_opt, CoapOpt::COAP_OPT_BLOCK1, v, enc_uint_minimal((uint32_t)block1_val, v));
291 }
292#else
293 (void)block2_val;
294 (void)block1_val;
295#endif
296
297 if (payload_len)
298 {
299 if (n + 1 + payload_len > cap)
300 {
301 return n;
302 }
303 resp[n++] = COAP_PAYLOAD_MARKER;
304 memcpy(resp + n, payload, payload_len);
305 n += payload_len;
306 }
307 return n;
308}
309
310// ---------------------------------------------------------------------------
311// Core processing (no sockets, no heap)
312// ---------------------------------------------------------------------------
313
314size_t pc_coap_server_process_ex(const uint8_t *req, size_t req_len, uint8_t *resp, size_t pc_resp_cap,
315 int32_t observe_seq)
316{
317 if (req_len < 4)
318 {
319 return 0; // too short for a header
320 }
321
322 uint8_t ver = (req[0] >> 6) & 0x03;
323 CoapType type = (CoapType)((req[0] >> 4) & 0x03);
324 uint8_t tkl = req[0] & 0x0F;
325 uint8_t code = req[1];
326 uint16_t mid = (uint16_t)((req[2] << 8) | req[3]);
327
328 // Reply type for a request: piggybacked ACK for CON, NON for NON. ACK/RST we
329 // receive are not requests - ignore them.
330 if (type != CoapType::COAP_TYPE_CON && type != CoapType::COAP_TYPE_NON)
331 {
332 return 0;
333 }
334 CoapType rsp_type = (type == CoapType::COAP_TYPE_CON) ? CoapType::COAP_TYPE_ACK : CoapType::COAP_TYPE_NON;
335
336 // A malformed message: bad version or reserved TKL (9..15). RFC 7252 §4.2:
337 // reject a CON with an RST; stay silent for a NON.
338 if (ver != 1 || tkl > COAP_MAX_TOKEN)
339 {
340 return (type == CoapType::COAP_TYPE_CON)
341 ? emit_header(resp, pc_resp_cap, CoapType::COAP_TYPE_RST, 0, mid, nullptr, 0)
342 : 0;
343 }
344
345 const uint8_t *p = req + 4;
346 const uint8_t *end = req + req_len;
347 if (p + tkl > end)
348 {
349 return (type == CoapType::COAP_TYPE_CON)
350 ? emit_header(resp, pc_resp_cap, CoapType::COAP_TYPE_RST, 0, mid, nullptr, 0)
351 : 0;
352 }
353 const uint8_t *token = p;
354 p += tkl;
355
356 // An empty message (Code 0.00): CON is a ping -> RST; anything else -> ignore.
357 if (code == 0)
358 {
359 return (type == CoapType::COAP_TYPE_CON)
360 ? emit_header(resp, pc_resp_cap, CoapType::COAP_TYPE_RST, 0, mid, nullptr, 0)
361 : 0;
362 }
363
364#if PC_ENABLE_COAP_OBSERVE
365 s_coap.last_observe = -1;
366 s_coap.last_method = code;
367 s_coap.last_tkl = tkl;
368 if (tkl)
369 {
370 memcpy(s_coap.last_token, token, tkl);
371 }
372#endif
373
374 // Decode options, reconstructing Uri-Path / Uri-Query and reading Content-Format.
375 size_t path_len = 0;
376 size_t query_len = 0;
377 s_coap.path[0] = '\0';
378 s_coap.query[0] = '\0';
379 CoapContentFormat req_cf = CoapContentFormat::COAP_CF_NONE;
380 const uint8_t *payload = nullptr;
381 size_t payload_len = 0;
382 uint32_t opt_num = 0;
383 bool bad = false;
384 bool bad_option = false; // unrecognized critical (odd-numbered) option seen (RFC 7252 5.4.1)
385#if PC_ENABLE_COAP_BLOCK
386 int32_t req_block1 = -1; // request Block1 option value (RFC 7959), or -1 if absent
387 int32_t req_block2 = -1; // request Block2 option value, or -1 if absent
388#endif
389
390 while (p < end)
391 {
392 uint8_t b = *p++;
393 if (b == COAP_PAYLOAD_MARKER)
394 {
395 payload = p;
396 payload_len = (size_t)(end - p);
397 break;
398 }
399 uint32_t delta = b >> 4;
400 uint32_t olen = b & 0x0F;
401 if (delta == 15 || olen == 15) // 15 is reserved outside the payload marker
402 {
403 bad = true;
404 break;
405 }
406 // Extended delta / length (13 -> +1 byte; 14 -> +2 bytes).
407 if (delta == 13)
408 {
409 if (p >= end)
410 {
411 bad = true;
412 break;
413 }
414 delta = (uint32_t)(*p++) + 13;
415 }
416 else if (delta == 14)
417 {
418 if (p + 2 > end)
419 {
420 bad = true;
421 break;
422 }
423 delta = (uint32_t)((p[0] << 8) | p[1]) + 269;
424 p += 2;
425 }
426 if (olen == 13)
427 {
428 if (p >= end)
429 {
430 bad = true;
431 break;
432 }
433 olen = (uint32_t)(*p++) + 13;
434 }
435 else if (olen == 14)
436 {
437 if (p + 2 > end)
438 {
439 bad = true;
440 break;
441 }
442 olen = (uint32_t)((p[0] << 8) | p[1]) + 269;
443 p += 2;
444 }
445 if (p + olen > end)
446 {
447 bad = true;
448 break;
449 }
450 opt_num += delta;
451 const uint8_t *val = p;
452 p += olen;
453
454 switch (opt_num)
455 {
456 case CoapOpt::COAP_OPT_URI_PATH:
457 if (!seg_append(s_coap.path, sizeof(s_coap.path), &path_len, '/', val, olen))
458 {
459 bad = true;
460 }
461 break;
462 case CoapOpt::COAP_OPT_URI_QUERY:
463 if (!seg_append(s_coap.query, sizeof(s_coap.query), &query_len, query_len ? '&' : '\0', val, olen))
464 {
465 bad = true;
466 }
467 break;
468 case CoapOpt::COAP_OPT_CONTENT_FORMAT:
469 req_cf = (CoapContentFormat)opt_uint(val, olen);
470 break;
471#if PC_ENABLE_COAP_OBSERVE
472 case CoapOpt::COAP_OPT_OBSERVE:
473 s_coap.last_observe = (int)opt_uint(val, olen); // 0 = register, 1 = deregister
474 break;
475#endif
476#if PC_ENABLE_COAP_BLOCK
477 case CoapOpt::COAP_OPT_BLOCK1: // block-wise request uploads (RFC 7959)
478 if (olen > 3)
479 {
480 bad = true;
481 }
482 else
483 {
484 req_block1 = (int32_t)opt_uint(val, olen);
485 }
486 break;
487 case CoapOpt::COAP_OPT_BLOCK2: // block-wise responses (RFC 7959)
488 if (olen > 3)
489 {
490 bad = true;
491 }
492 else
493 {
494 req_block2 = (int32_t)opt_uint(val, olen);
495 }
496 break;
497#endif
498 default:
499 // RFC 7252 5.4.1: an unrecognized option of class "critical" (odd option
500 // number) in a request must cause a 4.02 (Bad Option). Elective (even)
501 // unknown options are silently ignored. Block1/Block2 are critical, so a
502 // build without COAP_BLOCK correctly rejects them here.
503 if (opt_num & 1)
504 {
505 bad_option = true;
506 }
507 break;
508 }
509 if (bad)
510 {
511 break;
512 }
513 }
514
515 if (bad)
516 {
517 return emit_header(resp, pc_resp_cap, rsp_type, (uint8_t)CoapResponseCode::COAP_RSP_BAD_REQUEST, mid, token,
518 tkl);
519 }
520 if (bad_option)
521 {
522 return emit_header(resp, pc_resp_cap, rsp_type, (uint8_t)CoapResponseCode::COAP_RSP_BAD_OPTION, mid, token,
523 tkl);
524 }
525
526 if (path_len == 0)
527 {
528 s_coap.path[0] = '/';
529 s_coap.path[1] = '\0';
530 }
531
532 // Only class-0 GET/POST/PUT/DELETE are supported request methods. RFC 7252 5.8:
533 // "A request with an unrecognized or unsupported Method Code MUST generate a 4.05
534 // (Method Not Allowed) piggybacked response."
535 // GCOVR_EXCL_START the `code < COAP_GET` operand cannot be taken: code is unsigned and code == 0
536 // (the empty message) already returned above, so nothing below COAP_GET reaches this line.
537 if ((code >> 5) != 0 || code < (uint8_t)CoapMethod::COAP_GET || code > (uint8_t)CoapMethod::COAP_DELETE)
538 {
539 return emit_header(resp, pc_resp_cap, rsp_type, (uint8_t)CoapResponseCode::COAP_RSP_METHOD_NOT_ALLOWED, mid,
540 token, tkl);
541 }
542 // GCOVR_EXCL_STOP
543
544 // The response the emit path below serializes (block-wise if large). Filled
545 // either by the .well-known/core discovery listing or by a resource handler.
546 CoapResponse cresp;
547 cresp.code = (uint8_t)CoapResponseCode::COAP_RSP_CONTENT;
548 cresp.content_format = CoapContentFormat::COAP_CF_NONE;
549 cresp.payload = s_coap.pl;
550 cresp.payload_cap = sizeof(s_coap.pl);
551 cresp.payload_len = 0;
552 int32_t block1_echo = -1; // Block1 option to echo on the final-block response
553
554 // RFC 6690: GET /.well-known/core returns the CoRE Link Format listing of the
555 // registered resources, e.g. "</info>,</led>". Block2 (below) pages it if large.
556 if (strcmp(s_coap.path, "/.well-known/core") == 0)
557 {
558 if (code != (uint8_t)CoapMethod::COAP_GET)
559 {
560 return emit_header(resp, pc_resp_cap, rsp_type, (uint8_t)CoapResponseCode::COAP_RSP_METHOD_NOT_ALLOWED, mid,
561 token, tkl);
562 }
563 size_t pl = 0;
564 for (size_t i = 0; i < s_coap.res_count; i++)
565 {
566 const char *rpath = s_coap.res[i].path;
567 size_t plen = strnlen(rpath, sizeof(s_coap.pl));
568 size_t need = (pl ? 1u : 0u) + 2u + plen; // optional ',' + '<' + path + '>'
569 if (pl + need > sizeof(s_coap.pl))
570 {
571 break; // listing exceeds the payload buffer; truncate at a resource boundary
572 }
573 if (pl)
574 {
575 s_coap.pl[pl++] = ',';
576 }
577 s_coap.pl[pl++] = '<';
578 memcpy(s_coap.pl + pl, rpath, plen);
579 pl += plen;
580 s_coap.pl[pl++] = '>';
581 }
582 cresp.content_format = CoapContentFormat::COAP_CF_LINK;
583 cresp.payload_len = pl;
584 }
585 else
586 {
587 const CoapResource *r = find_resource(s_coap, s_coap.path);
588 if (!r)
589 {
590 return emit_header(resp, pc_resp_cap, rsp_type, (uint8_t)CoapResponseCode::COAP_RSP_NOT_FOUND, mid, token,
591 tkl);
592 }
593 if (!(r->methods & (1u << code)))
594 {
595 return emit_header(resp, pc_resp_cap, rsp_type, (uint8_t)CoapResponseCode::COAP_RSP_METHOD_NOT_ALLOWED, mid,
596 token, tkl);
597 }
598
599 const uint8_t *eff_payload = payload;
600 size_t eff_payload_len = payload_len;
601
602#if PC_ENABLE_COAP_BLOCK
603 // --- Block1: reassemble a chunked POST/PUT payload (RFC 7959 §2.5) ---
604 if (req_block1 >= 0 && (code == (uint8_t)CoapMethod::COAP_POST || code == (uint8_t)CoapMethod::COAP_PUT))
605 {
606 uint32_t b = (uint32_t)req_block1;
607 uint32_t num = b >> 4;
608 uint8_t more = (uint8_t)((b >> 3) & 1);
609 uint8_t szx = (uint8_t)(b & 7);
610 if (szx == 7) // reserved block size
611 {
612 return emit_header(resp, pc_resp_cap, rsp_type, (uint8_t)CoapResponseCode::COAP_RSP_BAD_OPTION, mid,
613 token, tkl);
614 }
615 uint32_t bsize = 1u << (szx + 4);
616 if (num == 0) // first block starts a fresh transfer
617 {
618 s_coap.b1_len = 0;
619 s_coap.b1_szx = szx;
620 }
621 // The block size is fixed for a transfer; an offset gap means a lost or
622 // reordered block. Either way the reassembly cannot continue: 4.08.
623 if (szx != s_coap.b1_szx || (size_t)num * bsize != s_coap.b1_len)
624 {
625 s_coap.b1_len = 0;
626 return emit_header(resp, pc_resp_cap, rsp_type, (uint8_t)CoapResponseCode::COAP_RSP_REQUEST_INCOMPLETE,
627 mid, token, tkl);
628 }
629 if (s_coap.b1_len + payload_len > sizeof(s_coap.b1))
630 {
631 s_coap.b1_len = 0;
632 return emit_header(resp, pc_resp_cap, rsp_type, (uint8_t)CoapResponseCode::COAP_RSP_REQUEST_TOO_LARGE,
633 mid, token, tkl);
634 }
635 if (payload_len)
636 {
637 memcpy(s_coap.b1 + s_coap.b1_len, payload, payload_len);
638 }
639 s_coap.b1_len += payload_len;
640
641 if (more)
642 {
643 // More blocks coming: acknowledge with 2.31 Continue + Block1 echo
644 // (no representation yet; the handler runs only on the final block).
645 size_t cn = emit_header(resp, pc_resp_cap, rsp_type, (uint8_t)CoapResponseCode::COAP_RSP_CONTINUE, mid,
646 token, tkl);
647 if (cn == 0)
648 {
649 return 0;
650 }
651 return emit_options_payload(resp, pc_resp_cap, cn, (uint8_t)CoapResponseCode::COAP_RSP_CONTINUE, -1,
652 CoapContentFormat::COAP_CF_NONE, -1,
653 (int32_t)((num << 4) | (1u << 3) | szx), nullptr, 0);
654 }
655 // Final block: hand the whole reassembled payload to the handler.
656 eff_payload = s_coap.b1;
657 eff_payload_len = s_coap.b1_len;
658 block1_echo = (int32_t)((num << 4) | szx); // More = 0
659 }
660#endif
661
662 CoapRequest creq;
663 creq.method = (CoapMethod)code;
664 creq.path = s_coap.path;
665 creq.query = s_coap.query;
666 creq.payload = eff_payload;
667 creq.payload_len = eff_payload_len;
668 creq.content_format = req_cf;
669
670 r->handler(&creq, &cresp);
671 if (cresp.payload_len > sizeof(s_coap.pl))
672 {
673 cresp.payload_len = sizeof(s_coap.pl); // defensive clamp
674 }
675 }
676
677 int32_t block2_echo = -1;
678
679#if PC_ENABLE_COAP_BLOCK
680 if (block1_echo >= 0)
681 {
682 s_coap.b1_len = 0; // the reassembled upload has been handed to the handler; clear it
683 }
684
685 // --- Block2: serve a (large or explicitly requested) representation one
686 // block at a time (RFC 7959 §2.4). Applies only to a successful body. ---
687 if ((cresp.code >> 5) == 2)
688 {
689 uint8_t szx = PC_COAP_BLOCK_SZX_MAX;
690 uint32_t num = 0;
691 bool block_wise = false;
692 if (req_block2 >= 0)
693 {
694 uint32_t b = (uint32_t)req_block2;
695 num = b >> 4;
696 szx = (uint8_t)(b & 7);
697 if (szx == 7)
698 {
699 return emit_header(resp, pc_resp_cap, rsp_type, (uint8_t)CoapResponseCode::COAP_RSP_BAD_OPTION, mid,
700 token, tkl);
701 }
702 if (szx > PC_COAP_BLOCK_SZX_MAX)
703 {
705 }
706 block_wise = true; // the client asked for block-wise transfer
707 }
708 else if (cresp.payload_len > (size_t)(1u << (szx + 4)))
709 {
710 block_wise = true; // body too large for one block -> start at block 0
711 }
712 if (block_wise)
713 {
714 uint32_t bsize = 1u << (szx + 4);
715 size_t off = (size_t)num * bsize;
716 // A block number past the end of the representation is a bad request.
717 if (off > cresp.payload_len || (off == cresp.payload_len && num > 0))
718 {
719 return emit_header(resp, pc_resp_cap, rsp_type, (uint8_t)CoapResponseCode::COAP_RSP_BAD_REQUEST, mid,
720 token, tkl);
721 }
722 size_t this_len = cresp.payload_len - off;
723 uint8_t more = 0;
724 if (this_len > bsize)
725 {
726 this_len = bsize;
727 more = 1;
728 }
729 block2_echo = (int32_t)((num << 4) | ((uint32_t)more << 3) | szx);
730 cresp.payload += off;
731 cresp.payload_len = this_len;
732 }
733 }
734#endif
735
736 // Build the response: header + token + options (ascending order) + payload.
737 size_t n = emit_header(resp, pc_resp_cap, rsp_type, cresp.code, mid, token, tkl);
738 if (n == 0)
739 {
740 return 0;
741 }
742 return emit_options_payload(resp, pc_resp_cap, n, cresp.code, observe_seq, cresp.content_format, block2_echo,
743 block1_echo, cresp.payload, cresp.payload_len);
744}
745
746size_t pc_coap_server_process(const uint8_t *req, size_t req_len, uint8_t *resp, size_t pc_resp_cap)
747{
748 return pc_coap_server_process_ex(req, req_len, resp, pc_resp_cap, -1); // no Observe option
749}
750
751#if PC_COAP_DEDUP_ENTRIES > 0
752// ---------------------------------------------------------------------------
753// Message de-duplication cache (RFC 7252 §4.5) - host-testable core
754// ---------------------------------------------------------------------------
755
756bool pc_coap_dedup_lookup(const char *src_ip, uint16_t src_port, uint16_t mid, const uint8_t **out, size_t *out_len)
757{
758 if (!src_ip)
759 {
760 return false;
761 }
762 uint32_t now = pc_millis();
763 for (size_t i = 0; i < PC_COAP_DEDUP_ENTRIES; i++)
764 {
765 const CoapDedupEntry &e = s_coap.dedup[i];
766 if (e.valid && e.mid == mid && e.port == src_port && (now - e.stamp_ms) < PC_COAP_DEDUP_LIFETIME_MS &&
767 strncmp(e.ip, src_ip, sizeof(e.ip)) == 0)
768 {
769 if (out)
770 {
771 *out = e.resp;
772 }
773 if (out_len)
774 {
775 *out_len = e.len;
776 }
777 return true;
778 }
779 }
780 return false;
781}
782
783void pc_coap_dedup_store(const char *src_ip, uint16_t src_port, uint16_t mid, const uint8_t *resp, size_t len)
784{
785 if (!src_ip || !resp || len == 0 || len > PC_COAP_DEDUP_RESP_MAX)
786 {
787 return; // an over-large response is not cached - a retransmission simply re-processes it
788 }
789 uint32_t now = pc_millis();
790 // Prefer the entry already holding this key, then a free / expired slot, else evict the oldest.
791 size_t victim = 0;
792 uint32_t oldest = 0;
793 for (size_t i = 0; i < PC_COAP_DEDUP_ENTRIES; i++)
794 {
795 const CoapDedupEntry &e = s_coap.dedup[i];
796 if (e.valid && e.mid == mid && e.port == src_port && strncmp(e.ip, src_ip, sizeof(e.ip)) == 0)
797 {
798 victim = i;
799 break;
800 }
801 if (!e.valid || (now - e.stamp_ms) >= PC_COAP_DEDUP_LIFETIME_MS)
802 {
803 victim = i;
804 break;
805 }
806 uint32_t age = now - e.stamp_ms;
807 if (age >= oldest)
808 {
809 oldest = age;
810 victim = i;
811 }
812 }
813 CoapDedupEntry &e = s_coap.dedup[victim];
814 size_t iplen = strnlen(src_ip, sizeof(e.ip) - 1);
815 memcpy(e.ip, src_ip, iplen);
816 e.ip[iplen] = 0;
817 e.port = src_port;
818 e.mid = mid;
819 e.stamp_ms = now;
820 e.len = (uint16_t)len;
821 memcpy(e.resp, resp, len);
822 e.valid = true;
823}
824#endif // PC_COAP_DEDUP_ENTRIES > 0
825
826// ---------------------------------------------------------------------------
827// UDP transport (pc_udp_listen is a host stub on non-Arduino builds)
828// ---------------------------------------------------------------------------
829
830#if PC_COAP_DEDUP_ENTRIES > 0
831// If @p data is a Confirmable request already answered for this peer (RFC 7252 §4.5), resend the cached
832// response and return true so the caller stops (the handler is not re-run). Else return false.
833static bool coap_dedup_replay(const uint8_t *data, size_t len, const struct pc_udp_peer *peer, const char *ip,
834 uint16_t port, bool have_peer)
835{
836 if (!have_peer || len < 4 || ((data[0] >> 4) & 0x03) != (uint8_t)CoapType::COAP_TYPE_CON)
837 {
838 return false;
839 }
840 uint16_t mid = (uint16_t)((data[2] << 8) | data[3]);
841 const uint8_t *cached = nullptr;
842 size_t clen = 0;
843 if (!pc_coap_dedup_lookup(ip, port, mid, &cached, &clen))
844 {
845 return false;
846 }
847 pc_udp_send(peer, cached, clen);
848 return true;
849}
850
851// Cache the response just sent for a Confirmable request so its retransmission is deduplicated.
852static void coap_dedup_remember(const uint8_t *data, size_t len, const char *ip, uint16_t port, bool have_peer,
853 const uint8_t *resp, size_t resp_len)
854{
855 if (!have_peer || len < 4 || ((data[0] >> 4) & 0x03) != (uint8_t)CoapType::COAP_TYPE_CON)
856 {
857 return;
858 }
859 uint16_t mid = (uint16_t)((data[2] << 8) | data[3]);
860 pc_coap_dedup_store(ip, port, mid, resp, resp_len);
861}
862#endif // PC_COAP_DEDUP_ENTRIES > 0
863
864#if PC_ENABLE_COAP_OBSERVE
865// ---------------------------------------------------------------------------
866// Observe registry + notifications (RFC 7641)
867// ---------------------------------------------------------------------------
868
869static int find_resource_index(const CoapCtx &c, const char *path)
870{
871 for (size_t i = 0; i < c.res_count; i++)
872 {
873 if (strcmp(c.res[i].path, path) == 0)
874 {
875 return (int)i;
876 }
877 }
878 return -1;
879}
880
881static bool same_token(const CoapObserver *o, const uint8_t *token, uint8_t tkl)
882{
883 return o->tkl == tkl && (tkl == 0 || memcmp(o->token, token, tkl) == 0);
884}
885
886// Find/refresh an observer; create one on first registration. Returns its slot or -1.
887static int obs_register(const char *ip, uint16_t port, const uint8_t *token, uint8_t tkl, int res_idx)
888{
889 for (int i = 0; i < PC_COAP_MAX_OBSERVERS; i++)
890 {
891 if (s_coap.obs[i].active && s_coap.obs[i].res_idx == res_idx && s_coap.obs[i].port == port &&
892 same_token(&s_coap.obs[i], token, tkl) && strcmp(s_coap.obs[i].ip, ip) == 0)
893 {
894 return i; // already observing
895 }
896 }
897 for (int i = 0; i < PC_COAP_MAX_OBSERVERS; i++)
898 {
899 if (!s_coap.obs[i].active)
900 {
901 s_coap.obs[i].active = true;
902 strncpy(s_coap.obs[i].ip, ip, sizeof(s_coap.obs[i].ip) - 1);
903 s_coap.obs[i].ip[sizeof(s_coap.obs[i].ip) - 1] = '\0';
904 s_coap.obs[i].port = port;
905 s_coap.obs[i].tkl = tkl;
906 if (tkl)
907 {
908 memcpy(s_coap.obs[i].token, token, tkl);
909 }
910 s_coap.obs[i].res_idx = res_idx;
911 s_coap.obs[i].seq = 1;
912 return i;
913 }
914 }
915 return -1; // registry full -> observation declined (resource still returned)
916}
917
918// Remove a specific observation (deregister GET) or every observation from a peer
919// (a Reset). Pass token=nullptr to drop all of @p ip:@p port.
920static void obs_remove(const char *ip, uint16_t port, const uint8_t *token, uint8_t tkl)
921{
922 for (int i = 0; i < PC_COAP_MAX_OBSERVERS; i++)
923 {
924 if (s_coap.obs[i].active && s_coap.obs[i].port == port && strcmp(s_coap.obs[i].ip, ip) == 0 &&
925 (token == nullptr || same_token(&s_coap.obs[i], token, tkl)))
926 {
927 s_coap.obs[i].active = false;
928 }
929 }
930}
931
932void pc_coap_notify(const char *path)
933{
934 int ridx = find_resource_index(s_coap, path);
935 if (ridx < 0)
936 {
937 return;
938 }
939 for (int i = 0; i < PC_COAP_MAX_OBSERVERS; i++)
940 {
941 if (!s_coap.obs[i].active || s_coap.obs[i].res_idx != ridx)
942 {
943 continue;
944 }
945 // Re-render the resource via its GET handler.
946 CoapRequest creq;
947 creq.method = CoapMethod::COAP_GET;
948 creq.path = s_coap.res[ridx].path;
949 creq.query = "";
950 creq.payload = nullptr;
951 creq.payload_len = 0;
952 creq.content_format = CoapContentFormat::COAP_CF_NONE;
953 CoapResponse cresp;
954 cresp.code = (uint8_t)CoapResponseCode::COAP_RSP_CONTENT;
955 cresp.content_format = CoapContentFormat::COAP_CF_NONE;
956 cresp.payload = s_coap.pl;
957 cresp.payload_cap = sizeof(s_coap.pl);
958 cresp.payload_len = 0;
959 s_coap.res[ridx].handler(&creq, &cresp);
960 if (cresp.payload_len > sizeof(s_coap.pl))
961 {
962 cresp.payload_len = sizeof(s_coap.pl);
963 }
964
965 // Build a NON notification: header + token + Observe(seq) + body.
966 uint16_t mid = (uint16_t)pc_millis();
967 s_coap.obs[i].seq = (s_coap.obs[i].seq + 1) & 0xFFFFFF;
968 size_t n = emit_header(s_coap.tx, sizeof(s_coap.tx), CoapType::COAP_TYPE_NON, cresp.code, mid,
969 s_coap.obs[i].token, s_coap.obs[i].tkl);
970 // GCOVR_EXCL_START the n == 0 operand of both tests is unreachable: protocore_config.h enforces
971 // PC_COAP_MSG_BUF_SIZE >= PC_COAP_MAX_PAYLOAD + 16 (>= 17), so emit_header() above always
972 // has room for the 4-byte header plus a token of at most 8 bytes and never returns 0.
973 if (n)
974 {
975 n = emit_options_payload(s_coap.tx, sizeof(s_coap.tx), n, cresp.code, (int32_t)s_coap.obs[i].seq,
976 cresp.content_format, -1, -1, cresp.payload, cresp.payload_len);
977 }
978 if (!n || !pc_udp_listener_sendto(s_coap.port, s_coap.obs[i].ip, s_coap.obs[i].port, s_coap.tx, n))
979 {
980 s_coap.obs[i].active = false; // unreachable -> drop the observer
981 }
982 // GCOVR_EXCL_STOP
983 }
984}
985
986static void coap_udp_handler(const uint8_t *data, size_t len, const struct pc_udp_peer *peer, void *ctx)
987{
988 (void)ctx;
989 char ip[16];
990 uint16_t pport = 0;
991 bool have_peer = pc_udp_peer_addr(peer, ip, sizeof(ip), &pport);
992
993 // A Reset from a client rejects our notification -> drop its observations.
994 if (len >= 1 && ((data[0] >> 4) & 0x03) == (uint8_t)CoapType::COAP_TYPE_RST)
995 {
996 if (have_peer) // GCOVR_EXCL_LINE host mock always supplies a peer; a null addr is ESP32/lwIP only
997 {
998 obs_remove(ip, pport, nullptr, 0);
999 }
1000 return;
1001 }
1002
1003#if PC_COAP_DEDUP_ENTRIES > 0
1004 // A retransmitted CON we already answered is re-answered from the cache without re-dispatching.
1005 if (coap_dedup_replay(data, len, peer, ip, pport, have_peer))
1006 {
1007 return;
1008 }
1009#endif
1010
1011 size_t rn = pc_coap_server_process_ex(data, len, s_coap.tx, sizeof(s_coap.tx), -1);
1012 if (!rn)
1013 {
1014 return;
1015 }
1016
1017 // The have_peer operand of this test and of the `else if` below can only be false on an ESP32
1018 // lwIP receive with a null source address; the host UDP mock always injects a peer.
1019 if (s_coap.last_method == (uint8_t)CoapMethod::COAP_GET && s_coap.last_observe == 0 && have_peer) // GCOVR_EXCL_LINE
1020 {
1021 int ridx = find_resource_index(s_coap, s_coap.path);
1022 if (ridx >= 0)
1023 {
1024 int slot = obs_register(ip, pport, s_coap.last_token, s_coap.last_tkl, ridx);
1025 if (slot >= 0)
1026 {
1027 // Re-encode the response carrying the Observe option (registration ack).
1028 size_t rn2 =
1029 pc_coap_server_process_ex(data, len, s_coap.tx, sizeof(s_coap.tx), (int32_t)s_coap.obs[slot].seq);
1030 // rn2 == 0 is unreachable: the re-encode differs from the call above - which already
1031 // returned rn > 0 - only by the Observe option, which append_opt() drops rather than
1032 // failing on, so the same request into the same buffer cannot now produce 0 bytes.
1033 if (rn2) // GCOVR_EXCL_LINE
1034 {
1035 rn = rn2;
1036 }
1037 }
1038 }
1039 }
1040 else if (s_coap.last_observe == 1 && have_peer) // GCOVR_EXCL_LINE have_peer is always true on the host
1041 {
1042 obs_remove(ip, pport, s_coap.last_token, s_coap.last_tkl);
1043 }
1044
1045#if PC_COAP_DEDUP_ENTRIES > 0
1046 coap_dedup_remember(data, len, ip, pport, have_peer, s_coap.tx, rn); // cache for a future retransmission
1047#endif
1048 pc_udp_send(peer, s_coap.tx, rn);
1049}
1050
1051void pc_coap_server_begin(uint16_t port)
1052{
1053 s_coap.port = port;
1054 for (int i = 0; i < PC_COAP_MAX_OBSERVERS; i++)
1055 {
1056 s_coap.obs[i].active = false;
1057 }
1058 pc_udp_listen(port, coap_udp_handler, nullptr);
1059}
1060
1061#else // Observe disabled: the basic request/response handler
1062
1063static void coap_udp_handler(const uint8_t *data, size_t len, const struct pc_udp_peer *peer, void *ctx)
1064{
1065 (void)ctx;
1066#if PC_COAP_DEDUP_ENTRIES > 0
1067 char ip[16];
1068 uint16_t pport = 0;
1069 bool have_peer = pc_udp_peer_addr(peer, ip, sizeof(ip), &pport);
1070 if (coap_dedup_replay(data, len, peer, ip, pport, have_peer))
1071 {
1072 return;
1073 }
1074#endif
1075 size_t rn = pc_coap_server_process(data, len, s_coap.tx, sizeof(s_coap.tx));
1076 if (rn)
1077 {
1078#if PC_COAP_DEDUP_ENTRIES > 0
1079 coap_dedup_remember(data, len, ip, pport, have_peer, s_coap.tx, rn);
1080#endif
1081 pc_udp_send(peer, s_coap.tx, rn);
1082 }
1083}
1084
1085void pc_coap_server_begin(uint16_t port)
1086{
1087 pc_udp_listen(port, coap_udp_handler, nullptr);
1088}
1089
1090#endif // PC_ENABLE_COAP_OBSERVE
1091
1092#endif // PC_ENABLE_COAP
Pluggable monotonic clock for all library timing.
uint32_t pc_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.
#define PC_COAP_DEDUP_RESP_MAX
Largest cached response the dedup cache retains per entry; a bigger response is not cached (a retrans...
#define PC_COAP_MAX_RESOURCES
Maximum registered CoAP resources (the server's fixed routing table).
#define PC_COAP_BLOCK_SZX_MAX
Largest block-size exponent (SZX) the server will use: block size = 2^(SZX+4) bytes,...
#define PC_COAP_MSG_BUF_SIZE
Static response-datagram buffer for the CoAP UDP server.
#define PC_COAP_DEDUP_ENTRIES
CoAP message de-duplication cache size (RFC 7252 sec 4.5). A Confirmable request the server has alrea...
#define PC_COAP_BLOCK1_MAX
Reassembly buffer for a block-wise (Block1) request upload, in bytes.
#define PC_COAP_OBSERVE_PORT
Default UDP port the CoAP observe transport notifies from (IANA well-known 5683).
#define PC_COAP_MAX_PATH
Maximum reconstructed Uri-Path length, including separators and the leading '/'.
#define PC_COAP_MAX_PAYLOAD
Maximum CoAP request/response payload in bytes.
#define PC_COAP_MAX_OBSERVERS
Maximum simultaneous CoAP observers (one slot per observed resource per client).
#define PC_COAP_MAX_QUERY
Maximum reconstructed Uri-Query length (segments joined by '&').
#define PC_COAP_DEDUP_LIFETIME_MS
How long (ms) a dedup entry stays fresh - RFC 7252 EXCHANGE_LIFETIME (~247 s) by default,...
bool pc_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:407
bool pc_udp_listen(uint16_t port, pc_udp_handler handler, void *ctx)
Bind a UDP port and route incoming datagrams to handler.
Definition udp.cpp:226
bool pc_udp_send(const struct pc_udp_peer *peer, const uint8_t *data, size_t len)
Send a datagram back to the peer captured during the handler call.
Definition udp.cpp:336
bool pc_udp_peer_addr(const struct pc_udp_peer *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:393
Layer 4 (Transport) - connectionless UDP datagram service.