Skip to main content

kipuka_coap/
server.rs

1//! CoAP message parsing, encoding, and EST-coaps URI routing.
2//!
3//! This module implements the CoAP message format (RFC 7252 §3) and maps
4//! CoAP URI paths to EST operations per RFC 9148 §5.1.
5//!
6//! # CoAP Message Format
7//!
8//! ```text
9//!  0                   1                   2                   3
10//!  0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
11//! +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
12//! |Ver| T |  TKL  |      Code     |          Message ID           |
13//! +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
14//! |   Token (if any, TKL bytes) ...
15//! +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
16//! |   Options (if any) ...
17//! +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
18//! |1 1 1 1 1 1 1 1|    Payload (if any) ...
19//! +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
20//! ```
21//!
22//! # EST-coaps URI Mapping
23//!
24//! RFC 9148 §5.1 defines abbreviated URI paths for EST operations:
25//!
26//! | CoAP Path | EST Operation | HTTP Method |
27//! |-----------|---------------|-------------|
28//! | `/sen`    | simpleenroll  | POST        |
29//! | `/sren`   | simplereenroll| POST        |
30//! | `/skg`    | serverkeygen  | POST        |
31//! | `/att`    | csrattrs      | GET         |
32//! | `/cacerts`| cacerts       | GET         |
33//! | `/crts`   | cacerts       | GET (alias) |
34
35use crate::block::{BlockAssembler, BlockDisassembler, szx_from_block_size};
36use crate::dtls::{DtlsConnection, DtlsContext, DtlsSessionCache};
37use crate::{CoapError, CoapResult};
38use std::collections::HashMap;
39use std::net::SocketAddr;
40use std::sync::Arc;
41use std::time::{Duration, Instant};
42use tokio::net::UdpSocket;
43use tokio::sync::Mutex;
44use tracing::{debug, info, warn};
45
46/// CoAP protocol version (RFC 7252 §3).
47///
48/// The only defined version is 1. Other values are reserved.
49pub const COAP_VERSION: u8 = 1;
50
51/// Payload marker byte (RFC 7252 §3).
52///
53/// The byte 0xFF separates CoAP options from the payload.
54const PAYLOAD_MARKER: u8 = 0xFF;
55
56// --- CoAP Option Numbers (RFC 7252 §5.10) ---
57
58/// Uri-Host option (RFC 7252 §5.10.1).
59pub const OPTION_URI_HOST: u16 = 3;
60
61/// Uri-Port option (RFC 7252 §5.10.1).
62pub const OPTION_URI_PORT: u16 = 7;
63
64/// Uri-Path option (RFC 7252 §5.10.1).
65///
66/// Each Uri-Path option contains one path segment. Multiple options
67/// are concatenated with `/` to form the full URI path.
68pub const OPTION_URI_PATH: u16 = 11;
69
70/// Content-Format option (RFC 7252 §5.10.3).
71///
72/// Contains the CoAP content-format ID (see [`crate::content_format`]).
73pub const OPTION_CONTENT_FORMAT: u16 = 12;
74
75/// Uri-Query option (RFC 7252 §5.10.1).
76pub const OPTION_URI_QUERY: u16 = 15;
77
78/// Block2 option (RFC 7959 §2.1).
79///
80/// Controls response payload block-wise transfer (server to client).
81pub const OPTION_BLOCK2: u16 = 23;
82
83/// Block1 option (RFC 7959 §2.1).
84///
85/// Controls request payload block-wise transfer (client to server).
86pub const OPTION_BLOCK1: u16 = 27;
87
88/// Size2 option (RFC 7959 §4).
89///
90/// Indicates the total size of the response payload.
91pub const OPTION_SIZE2: u16 = 28;
92
93/// Size1 option (RFC 7959 §4).
94///
95/// Indicates the total size of the request payload.
96pub const OPTION_SIZE1: u16 = 60;
97
98/// CoAP request/response method codes (RFC 7252 §5.8, §12.1).
99#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
100pub enum CoapMethod {
101    /// 0.01 GET
102    Get,
103    /// 0.02 POST
104    Post,
105    /// 0.03 PUT
106    Put,
107    /// 0.04 DELETE
108    Delete,
109    /// 0.05 FETCH (RFC 8132)
110    Fetch,
111    /// 0.06 PATCH (RFC 8132)
112    Patch,
113    /// 0.07 iPATCH (RFC 8132)
114    IPatch,
115}
116
117impl CoapMethod {
118    /// Converts a CoAP code byte to a method, if it represents a request.
119    ///
120    /// Request codes have class 0 (code byte 0.01 through 0.07).
121    pub fn from_code(code: &CoapCode) -> Option<Self> {
122        if code.class != 0 {
123            return None;
124        }
125        match code.detail {
126            1 => Some(Self::Get),
127            2 => Some(Self::Post),
128            3 => Some(Self::Put),
129            4 => Some(Self::Delete),
130            5 => Some(Self::Fetch),
131            6 => Some(Self::Patch),
132            7 => Some(Self::IPatch),
133            _ => None,
134        }
135    }
136
137    /// Returns the CoAP code for this method.
138    pub fn to_code(&self) -> CoapCode {
139        let detail = match self {
140            Self::Get => 1,
141            Self::Post => 2,
142            Self::Put => 3,
143            Self::Delete => 4,
144            Self::Fetch => 5,
145            Self::Patch => 6,
146            Self::IPatch => 7,
147        };
148        CoapCode { class: 0, detail }
149    }
150}
151
152/// CoAP message type (RFC 7252 §3).
153#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
154pub enum CoapMessageType {
155    /// Confirmable (CON): requires acknowledgement.
156    Confirmable,
157    /// Non-confirmable (NON): fire-and-forget.
158    NonConfirmable,
159    /// Acknowledgement (ACK): confirms receipt of CON.
160    Acknowledgement,
161    /// Reset (RST): indicates message was received but cannot be processed.
162    Reset,
163}
164
165impl CoapMessageType {
166    /// Decodes the message type from the 2-bit T field.
167    fn from_bits(bits: u8) -> CoapResult<Self> {
168        match bits {
169            0 => Ok(Self::Confirmable),
170            1 => Ok(Self::NonConfirmable),
171            2 => Ok(Self::Acknowledgement),
172            3 => Ok(Self::Reset),
173            _ => Err(CoapError::InvalidMessage(format!(
174                "Invalid message type: {bits}"
175            ))),
176        }
177    }
178
179    /// Encodes the message type to the 2-bit T field.
180    fn to_bits(self) -> u8 {
181        match self {
182            Self::Confirmable => 0,
183            Self::NonConfirmable => 1,
184            Self::Acknowledgement => 2,
185            Self::Reset => 3,
186        }
187    }
188}
189
190/// CoAP response/request code (RFC 7252 §3, §5.9).
191///
192/// Encoded as a single byte: upper 3 bits = class, lower 5 bits = detail.
193/// Conventionally written as `class.detail` (e.g., 2.05 = Content).
194#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
195pub struct CoapCode {
196    /// Code class (0 = request, 2 = success, 4 = client error, 5 = server error).
197    pub class: u8,
198    /// Code detail within the class.
199    pub detail: u8,
200}
201
202impl CoapCode {
203    /// 4.00 Bad Request.
204    pub const BAD_REQUEST: Self = Self {
205        class: 4,
206        detail: 0,
207    };
208    /// 2.01 Created — enrollment succeeded.
209    pub const CREATED: Self = Self {
210        class: 2,
211        detail: 1,
212    };
213    /// 2.05 Content — response with payload.
214    pub const CONTENT: Self = Self {
215        class: 2,
216        detail: 5,
217    };
218    /// 4.01 Unauthorized — client authentication required.
219    pub const UNAUTHORIZED: Self = Self {
220        class: 4,
221        detail: 1,
222    };
223    /// 4.03 Forbidden — authenticated client not authorized for the request.
224    pub const FORBIDDEN: Self = Self {
225        class: 4,
226        detail: 3,
227    };
228    /// 4.04 Not Found — unknown URI path.
229    pub const NOT_FOUND: Self = Self {
230        class: 4,
231        detail: 4,
232    };
233    /// 4.05 Method Not Allowed — wrong method for resource.
234    pub const METHOD_NOT_ALLOWED: Self = Self {
235        class: 4,
236        detail: 5,
237    };
238    /// 4.15 Unsupported Content-Format.
239    pub const UNSUPPORTED_CONTENT_FORMAT: Self = Self {
240        class: 4,
241        detail: 15,
242    };
243    /// 5.00 Internal Server Error.
244    pub const INTERNAL_SERVER_ERROR: Self = Self {
245        class: 5,
246        detail: 0,
247    };
248
249    /// Encodes the code as a single byte: class in bits 7-5, detail in bits 4-0.
250    pub fn to_byte(&self) -> u8 {
251        ((self.class & 0x07) << 5) | (self.detail & 0x1F)
252    }
253
254    /// Decodes a code from a single byte.
255    pub fn from_byte(byte: u8) -> Self {
256        Self {
257            class: (byte >> 5) & 0x07,
258            detail: byte & 0x1F,
259        }
260    }
261
262    /// Returns whether this code represents a request (class 0).
263    pub fn is_request(&self) -> bool {
264        self.class == 0
265    }
266
267    /// Returns whether this code represents a success response (class 2).
268    pub fn is_success(&self) -> bool {
269        self.class == 2
270    }
271
272    /// Returns whether this code represents a client error (class 4).
273    pub fn is_client_error(&self) -> bool {
274        self.class == 4
275    }
276
277    /// Returns whether this code represents a server error (class 5).
278    pub fn is_server_error(&self) -> bool {
279        self.class == 5
280    }
281}
282
283impl std::fmt::Display for CoapCode {
284    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
285        write!(f, "{}.{:02}", self.class, self.detail)
286    }
287}
288
289/// A single CoAP option (RFC 7252 §3.1).
290///
291/// Options are TLV-encoded in messages, sorted by option number.
292/// Repeated option numbers create multiple values for that option.
293#[derive(Debug, Clone, PartialEq, Eq)]
294pub struct CoapOption {
295    /// Option number (determines semantics).
296    pub number: u16,
297    /// Option value (interpretation depends on the option number).
298    pub value: Vec<u8>,
299}
300
301impl CoapOption {
302    /// Creates a new option with the given number and value.
303    pub fn new(number: u16, value: Vec<u8>) -> Self {
304        Self { number, value }
305    }
306
307    /// Creates an empty option (zero-length value).
308    pub fn empty(number: u16) -> Self {
309        Self {
310            number,
311            value: Vec::new(),
312        }
313    }
314
315    /// Interprets the option value as a variable-length unsigned integer.
316    ///
317    /// RFC 7252 §3.2: Options with format "uint" use 0-4 bytes in
318    /// network byte order.
319    pub fn value_as_uint(&self) -> u32 {
320        let mut result: u32 = 0;
321        for &byte in &self.value {
322            result = (result << 8) | u32::from(byte);
323        }
324        result
325    }
326
327    /// Creates an option with a uint value encoded in the minimum number of bytes.
328    pub fn from_uint(number: u16, value: u32) -> Self {
329        let bytes = if value == 0 {
330            Vec::new()
331        } else if value <= 0xFF {
332            vec![value as u8]
333        } else if value <= 0xFFFF {
334            vec![(value >> 8) as u8, value as u8]
335        } else if value <= 0xFF_FFFF {
336            vec![(value >> 16) as u8, (value >> 8) as u8, value as u8]
337        } else {
338            vec![
339                (value >> 24) as u8,
340                (value >> 16) as u8,
341                (value >> 8) as u8,
342                value as u8,
343            ]
344        };
345        Self {
346            number,
347            value: bytes,
348        }
349    }
350
351    /// Interprets the option value as a UTF-8 string.
352    pub fn value_as_str(&self) -> CoapResult<&str> {
353        std::str::from_utf8(&self.value)
354            .map_err(|e| CoapError::InvalidMessage(format!("Invalid UTF-8 in option: {e}")))
355    }
356}
357
358/// A parsed CoAP message (RFC 7252 §3).
359///
360/// Represents a complete CoAP datagram including the fixed header, token,
361/// options, and optional payload.
362#[derive(Debug, Clone, PartialEq, Eq)]
363pub struct CoapMessage {
364    /// Protocol version (must be 1).
365    pub version: u8,
366    /// Message type (CON, NON, ACK, RST).
367    pub msg_type: CoapMessageType,
368    /// Request/response code.
369    pub code: CoapCode,
370    /// Message ID for matching requests to responses.
371    pub message_id: u16,
372    /// Token for correlating requests and responses (0-8 bytes).
373    pub token: Vec<u8>,
374    /// CoAP options, sorted by option number.
375    pub options: Vec<CoapOption>,
376    /// Message payload (after the 0xFF marker).
377    pub payload: Vec<u8>,
378}
379
380impl CoapMessage {
381    /// Parses a raw UDP datagram into a CoAP message.
382    ///
383    /// RFC 7252 §3: The message format is a compact binary encoding with
384    /// a 4-byte fixed header followed by a variable-length token, options,
385    /// and payload.
386    pub fn parse(data: &[u8]) -> CoapResult<Self> {
387        if data.len() < 4 {
388            return Err(CoapError::InvalidMessage(format!(
389                "Message too short: {} bytes (minimum 4)",
390                data.len()
391            )));
392        }
393
394        // Byte 0: Ver (2 bits) | T (2 bits) | TKL (4 bits)
395        let version = (data[0] >> 6) & 0x03;
396        if version != COAP_VERSION {
397            return Err(CoapError::InvalidMessage(format!(
398                "Unsupported CoAP version: {version}"
399            )));
400        }
401
402        let msg_type = CoapMessageType::from_bits((data[0] >> 4) & 0x03)?;
403        let tkl = (data[0] & 0x0F) as usize;
404
405        if tkl > 8 {
406            return Err(CoapError::InvalidMessage(format!(
407                "Token length {tkl} exceeds maximum of 8"
408            )));
409        }
410
411        // Byte 1: Code
412        let code = CoapCode::from_byte(data[1]);
413
414        // Bytes 2-3: Message ID
415        let message_id = u16::from_be_bytes([data[2], data[3]]);
416
417        // Token
418        let token_end = 4 + tkl;
419        if data.len() < token_end {
420            return Err(CoapError::InvalidMessage(format!(
421                "Message truncated in token: need {} bytes, have {}",
422                token_end,
423                data.len()
424            )));
425        }
426        let token = data[4..token_end].to_vec();
427
428        // Options and payload
429        let mut pos = token_end;
430        let mut options = Vec::new();
431        let mut current_option_number: u16 = 0;
432
433        while pos < data.len() {
434            // Check for payload marker
435            if data[pos] == PAYLOAD_MARKER {
436                pos += 1;
437                break;
438            }
439
440            // Parse option delta and length (RFC 7252 §3.1)
441            let option_byte = data[pos];
442            pos += 1;
443
444            let mut delta = u16::from((option_byte >> 4) & 0x0F);
445            let mut length = u16::from(option_byte & 0x0F);
446
447            // Extended delta
448            match delta {
449                13 => {
450                    if pos >= data.len() {
451                        return Err(CoapError::InvalidMessage(
452                            "Truncated option delta (13)".to_string(),
453                        ));
454                    }
455                    delta = u16::from(data[pos]) + 13;
456                    pos += 1;
457                }
458                14 => {
459                    if pos + 1 >= data.len() {
460                        return Err(CoapError::InvalidMessage(
461                            "Truncated option delta (14)".to_string(),
462                        ));
463                    }
464                    delta = u16::from_be_bytes([data[pos], data[pos + 1]])
465                        .checked_add(269)
466                        .ok_or_else(|| CoapError::InvalidMessage("option value overflow".into()))?;
467                    pos += 2;
468                }
469                15 => {
470                    return Err(CoapError::InvalidMessage(
471                        "Reserved option delta value 15".to_string(),
472                    ));
473                }
474                _ => {}
475            }
476
477            // Extended length
478            match length {
479                13 => {
480                    if pos >= data.len() {
481                        return Err(CoapError::InvalidMessage(
482                            "Truncated option length (13)".to_string(),
483                        ));
484                    }
485                    length = u16::from(data[pos]) + 13;
486                    pos += 1;
487                }
488                14 => {
489                    if pos + 1 >= data.len() {
490                        return Err(CoapError::InvalidMessage(
491                            "Truncated option length (14)".to_string(),
492                        ));
493                    }
494                    length = u16::from_be_bytes([data[pos], data[pos + 1]])
495                        .checked_add(269)
496                        .ok_or_else(|| CoapError::InvalidMessage("option value overflow".into()))?;
497                    pos += 2;
498                }
499                15 => {
500                    return Err(CoapError::InvalidMessage(
501                        "Reserved option length value 15".to_string(),
502                    ));
503                }
504                _ => {}
505            }
506
507            current_option_number = current_option_number
508                .checked_add(delta)
509                .ok_or_else(|| CoapError::InvalidMessage("option number overflow".into()))?;
510            let length = length as usize;
511
512            if pos + length > data.len() {
513                return Err(CoapError::InvalidMessage(format!(
514                    "Truncated option value: need {} bytes at offset {}, have {}",
515                    length,
516                    pos,
517                    data.len() - pos
518                )));
519            }
520
521            let value = data[pos..pos + length].to_vec();
522            pos += length;
523
524            options.push(CoapOption {
525                number: current_option_number,
526                value,
527            });
528        }
529
530        // Remaining bytes after payload marker are the payload.
531        let payload = if pos < data.len() {
532            data[pos..].to_vec()
533        } else {
534            Vec::new()
535        };
536
537        Ok(Self {
538            version,
539            msg_type,
540            code,
541            message_id,
542            token,
543            options,
544            payload,
545        })
546    }
547
548    /// Serializes this CoAP message to bytes suitable for UDP transmission.
549    ///
550    /// RFC 7252 §3: Options are encoded using delta compression relative
551    /// to the previous option number.
552    pub fn encode(&self) -> Vec<u8> {
553        let mut buf = Vec::with_capacity(4 + self.token.len() + self.payload.len() + 32);
554
555        // Byte 0: Ver (2) | T (2) | TKL (4)
556        let tkl = self.token.len().min(8) as u8;
557        buf.push((COAP_VERSION << 6) | (self.msg_type.to_bits() << 4) | tkl);
558
559        // Byte 1: Code
560        buf.push(self.code.to_byte());
561
562        // Bytes 2-3: Message ID
563        buf.extend_from_slice(&self.message_id.to_be_bytes());
564
565        // Token
566        buf.extend_from_slice(&self.token[..tkl as usize]);
567
568        // Options (must be sorted by option number for delta encoding)
569        let mut sorted_options = self.options.clone();
570        sorted_options.sort_by_key(|o| o.number);
571
572        let mut prev_number: u16 = 0;
573        for opt in &sorted_options {
574            let delta = opt.number - prev_number;
575            let length = opt.value.len() as u16;
576            prev_number = opt.number;
577
578            // Encode delta
579            let (delta_nibble, delta_ext) = encode_option_header_value(delta);
580            // Encode length
581            let (length_nibble, length_ext) = encode_option_header_value(length);
582
583            buf.push((delta_nibble << 4) | length_nibble);
584            buf.extend_from_slice(&delta_ext);
585            buf.extend_from_slice(&length_ext);
586            buf.extend_from_slice(&opt.value);
587        }
588
589        // Payload (preceded by 0xFF marker if non-empty)
590        if !self.payload.is_empty() {
591            buf.push(PAYLOAD_MARKER);
592            buf.extend_from_slice(&self.payload);
593        }
594
595        buf
596    }
597
598    /// Extracts the URI path from Uri-Path options.
599    ///
600    /// RFC 7252 §5.10.1: Multiple Uri-Path options are joined with `/`
601    /// to reconstruct the full path.
602    pub fn uri_path(&self) -> String {
603        let segments: Vec<&str> = self
604            .options
605            .iter()
606            .filter(|o| o.number == OPTION_URI_PATH)
607            .filter_map(|o| std::str::from_utf8(&o.value).ok())
608            .collect();
609
610        if segments.is_empty() {
611            "/".to_string()
612        } else {
613            format!("/{}", segments.join("/"))
614        }
615    }
616
617    /// Returns the Content-Format option value, if present.
618    pub fn content_format(&self) -> Option<u16> {
619        self.options
620            .iter()
621            .find(|o| o.number == OPTION_CONTENT_FORMAT)
622            .map(|o| o.value_as_uint() as u16)
623    }
624
625    /// Returns the Block1 option, if present.
626    pub fn block1(&self) -> Option<crate::block::BlockOption> {
627        self.options
628            .iter()
629            .find(|o| o.number == OPTION_BLOCK1)
630            .map(|o| crate::block::BlockOption::decode(o.value_as_uint()))
631    }
632
633    /// Returns the Block2 option, if present.
634    pub fn block2(&self) -> Option<crate::block::BlockOption> {
635        self.options
636            .iter()
637            .find(|o| o.number == OPTION_BLOCK2)
638            .map(|o| crate::block::BlockOption::decode(o.value_as_uint()))
639    }
640}
641
642/// Encodes a delta or length value into the option header nibble and
643/// optional extended bytes per RFC 7252 §3.1.
644fn encode_option_header_value(value: u16) -> (u8, Vec<u8>) {
645    if value < 13 {
646        (value as u8, Vec::new())
647    } else if value < 269 {
648        (13, vec![(value - 13) as u8])
649    } else {
650        let extended = value - 269;
651        (14, extended.to_be_bytes().to_vec())
652    }
653}
654
655/// EST operations per RFC 7030, reused from the `kipuka-est` crate.
656///
657/// Duplicated here to avoid a dependency on `kipuka-est` from the CoAP
658/// transport layer. The router maps CoAP paths to these operations.
659#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
660pub enum EstOperation {
661    /// Retrieve CA certificates (RFC 7030 §4.1).
662    CaCerts,
663    /// Simple enrollment (RFC 7030 §4.2).
664    SimpleEnroll,
665    /// Simple re-enrollment (RFC 7030 §4.2.2).
666    SimpleReenroll,
667    /// Server-side key generation (RFC 7030 §4.4).
668    ServerKeygen,
669    /// CSR attributes (RFC 7030 §4.5).
670    CsrAttrs,
671}
672
673/// CoAP request representing an EST-coaps operation.
674///
675/// RFC 9148 §5.1: EST-coaps URIs follow the pattern:
676///   coaps://host/.well-known/est/{operation}
677/// where {operation} uses the abbreviated names from RFC 9148 §5.1:
678///   - /cacerts   -> /cacerts (GET)
679///   - /sen       -> /simpleenroll (POST)
680///   - /sren      -> /simplereenroll (POST)
681///   - /skg       -> /serverkeygen (POST)
682///   - /att       -> /csrattrs (GET)
683///   - /crts      -> /cacerts (GET, alias)
684#[derive(Debug)]
685pub struct CoapEstRequest {
686    /// The decoded EST operation.
687    pub operation: EstOperation,
688    /// The optional EST label preceding the operation segment.
689    ///
690    /// RFC 9148 §5.1 / RFC 7030 §3.2.2 allow an arbitrary label between the
691    /// `/.well-known/est/` prefix and the operation (e.g.
692    /// `/.well-known/est/{label}/sen`).  `None` means the unlabeled default
693    /// endpoint.  The label selects the CA and the per-label access-control
694    /// policy applied at issuance.
695    pub label: Option<String>,
696    /// The CoAP method used.
697    pub method: CoapMethod,
698    /// The original CoAP message.
699    pub message: CoapMessage,
700}
701
702/// Routes CoAP URI paths to EST operations.
703///
704/// RFC 9148 §5.1: EST-coaps uses abbreviated path names under
705/// `/.well-known/est/` to reduce URI size for constrained devices.
706///
707/// The router strips the well-known prefix and maps the final path
708/// segment to an [`EstOperation`].
709pub struct CoapEstRouter;
710
711impl CoapEstRouter {
712    /// Maps a CoAP URI path to an EST operation.
713    ///
714    /// Recognizes both the abbreviated RFC 9148 paths and the full-length
715    /// path segments from the well-known prefix.
716    ///
717    /// # Path Recognition
718    ///
719    /// The router accepts paths with or without the `/.well-known/est/`
720    /// prefix, recognizing these final segments:
721    /// - `sen` → SimpleEnroll
722    /// - `sren` → SimpleReenroll
723    /// - `skg` → ServerKeygen
724    /// - `att` → CsrAttrs
725    /// - `cacerts` or `crts` → CaCerts
726    pub fn route(path: &str) -> CoapResult<EstOperation> {
727        Self::route_with_label(path).map(|(_, op)| op)
728    }
729
730    /// Maps a CoAP URI path to an optional EST label and an EST operation.
731    ///
732    /// RFC 7030 §3.2.2 / RFC 9148 §5.1 permit an operator-defined label
733    /// between the `/.well-known/est/` prefix and the operation segment:
734    ///
735    /// ```text
736    /// /.well-known/est/{label}/sen   -> (Some("{label}"), SimpleEnroll)
737    /// /.well-known/est/sen           -> (None,            SimpleEnroll)
738    /// ```
739    ///
740    /// The label selects the CA and the per-label access-control policy that
741    /// is enforced at issuance (NIAP CA PP FDP_ACF.1).  Without label support
742    /// the CoAP transport could only ever hit the unlabeled default endpoint,
743    /// which carries no policy — so any per-label authorization an operator
744    /// configured for HTTP would be silently unenforced over CoAP.
745    pub fn route_with_label(path: &str) -> CoapResult<(Option<String>, EstOperation)> {
746        // Strip the well-known prefix and surrounding slashes, then split into
747        // path segments.  What remains is either `{operation}` or
748        // `{label}/{operation}`.
749        let remainder = path
750            .trim_start_matches('/')
751            .trim_start_matches(".well-known/est/")
752            .trim_start_matches(".well-known/est")
753            .trim_matches('/');
754
755        let segments: Vec<&str> = remainder.split('/').filter(|s| !s.is_empty()).collect();
756
757        let (label, op_segment) = match segments.as_slice() {
758            [op] => (None, *op),
759            [label, op] => (Some((*label).to_string()), *op),
760            _ => {
761                return Err(CoapError::ResourceNotFound(format!(
762                    "Unknown EST-coaps path: {path}"
763                )));
764            }
765        };
766
767        let operation = match op_segment {
768            "sen" | "simpleenroll" => EstOperation::SimpleEnroll,
769            "sren" | "simplereenroll" => EstOperation::SimpleReenroll,
770            "skg" | "serverkeygen" => EstOperation::ServerKeygen,
771            "att" | "csrattrs" => EstOperation::CsrAttrs,
772            "cacerts" | "crts" => EstOperation::CaCerts,
773            _ => {
774                return Err(CoapError::ResourceNotFound(format!(
775                    "Unknown EST-coaps path: {path}"
776                )));
777            }
778        };
779
780        Ok((label, operation))
781    }
782
783    /// Routes a full CoAP message to an EST operation.
784    ///
785    /// Extracts the URI path from the message options and resolves the
786    /// corresponding EST operation. Also validates that the CoAP method
787    /// is appropriate for the operation.
788    pub fn route_message(message: CoapMessage) -> CoapResult<CoapEstRequest> {
789        let path = message.uri_path();
790        let (label, operation) = Self::route_with_label(&path)?;
791
792        let method = CoapMethod::from_code(&message.code).ok_or_else(|| {
793            CoapError::UnsupportedMethod(format!("Code {} is not a request", message.code))
794        })?;
795
796        // Validate method against operation.
797        match operation {
798            EstOperation::CaCerts | EstOperation::CsrAttrs => {
799                if method != CoapMethod::Get && method != CoapMethod::Fetch {
800                    return Err(CoapError::UnsupportedMethod(format!(
801                        "{path} requires GET or FETCH, got {method:?}"
802                    )));
803                }
804            }
805            EstOperation::SimpleEnroll
806            | EstOperation::SimpleReenroll
807            | EstOperation::ServerKeygen => {
808                if method != CoapMethod::Post {
809                    return Err(CoapError::UnsupportedMethod(format!(
810                        "{path} requires POST, got {method:?}"
811                    )));
812                }
813            }
814        }
815
816        Ok(CoapEstRequest {
817            operation,
818            label,
819            method,
820            message,
821        })
822    }
823}
824
825#[cfg(test)]
826mod tests {
827    use super::*;
828
829    // --- CoapCode tests ---
830
831    #[test]
832    fn test_code_byte_roundtrip() {
833        let codes = [
834            CoapCode::CREATED,
835            CoapCode::CONTENT,
836            CoapCode::NOT_FOUND,
837            CoapCode::METHOD_NOT_ALLOWED,
838            CoapCode::UNSUPPORTED_CONTENT_FORMAT,
839            CoapCode::INTERNAL_SERVER_ERROR,
840        ];
841
842        for code in codes {
843            let byte = code.to_byte();
844            let decoded = CoapCode::from_byte(byte);
845            assert_eq!(decoded, code, "roundtrip failed for {code}");
846        }
847    }
848
849    #[test]
850    fn test_code_classification() {
851        assert!(CoapCode::from_byte(0x01).is_request()); // 0.01 GET
852        assert!(CoapCode::CONTENT.is_success());
853        assert!(CoapCode::NOT_FOUND.is_client_error());
854        assert!(CoapCode::INTERNAL_SERVER_ERROR.is_server_error());
855    }
856
857    #[test]
858    fn test_code_display() {
859        assert_eq!(CoapCode::CONTENT.to_string(), "2.05");
860        assert_eq!(CoapCode::NOT_FOUND.to_string(), "4.04");
861        assert_eq!(CoapCode::CREATED.to_string(), "2.01");
862    }
863
864    // --- CoapOption tests ---
865
866    #[test]
867    fn test_option_uint_encoding() {
868        let opt = CoapOption::from_uint(OPTION_CONTENT_FORMAT, 285);
869        assert_eq!(opt.value_as_uint(), 285);
870
871        let opt_zero = CoapOption::from_uint(OPTION_CONTENT_FORMAT, 0);
872        assert_eq!(opt_zero.value_as_uint(), 0);
873        assert!(opt_zero.value.is_empty());
874    }
875
876    #[test]
877    fn test_option_string_value() {
878        let opt = CoapOption::new(OPTION_URI_PATH, b"sen".to_vec());
879        assert_eq!(opt.value_as_str().unwrap(), "sen");
880    }
881
882    // --- CoapMessage parse/encode tests ---
883
884    #[test]
885    fn test_parse_minimal_message() {
886        // Minimal CON GET with no token, no options, no payload
887        // Ver=1, T=0 (CON), TKL=0, Code=0.01 (GET), MID=0x1234
888        let data = [0x40, 0x01, 0x12, 0x34];
889        let msg = CoapMessage::parse(&data).unwrap();
890
891        assert_eq!(msg.version, 1);
892        assert_eq!(msg.msg_type, CoapMessageType::Confirmable);
893        assert_eq!(
894            msg.code,
895            CoapCode {
896                class: 0,
897                detail: 1
898            }
899        );
900        assert_eq!(msg.message_id, 0x1234);
901        assert!(msg.token.is_empty());
902        assert!(msg.options.is_empty());
903        assert!(msg.payload.is_empty());
904    }
905
906    #[test]
907    fn test_parse_message_with_token() {
908        // CON GET, TKL=4, token=[0xDE,0xAD,0xBE,0xEF]
909        let data = [0x44, 0x01, 0x00, 0x01, 0xDE, 0xAD, 0xBE, 0xEF];
910        let msg = CoapMessage::parse(&data).unwrap();
911
912        assert_eq!(msg.token, vec![0xDE, 0xAD, 0xBE, 0xEF]);
913    }
914
915    #[test]
916    fn test_parse_message_with_payload() {
917        // CON POST, TKL=0, no options, payload "hello"
918        let mut data = vec![0x40, 0x02, 0x00, 0x01];
919        data.push(PAYLOAD_MARKER);
920        data.extend_from_slice(b"hello");
921
922        let msg = CoapMessage::parse(&data).unwrap();
923        assert_eq!(msg.payload, b"hello");
924    }
925
926    #[test]
927    fn test_parse_encode_roundtrip() {
928        let original = CoapMessage {
929            version: 1,
930            msg_type: CoapMessageType::Confirmable,
931            code: CoapMethod::Post.to_code(),
932            message_id: 0xABCD,
933            token: vec![0x01, 0x02],
934            options: vec![
935                CoapOption::new(OPTION_URI_PATH, b".well-known".to_vec()),
936                CoapOption::new(OPTION_URI_PATH, b"est".to_vec()),
937                CoapOption::new(OPTION_URI_PATH, b"sen".to_vec()),
938                CoapOption::from_uint(OPTION_CONTENT_FORMAT, 285),
939            ],
940            payload: vec![0x30, 0x82, 0x01, 0x00],
941        };
942
943        let encoded = original.encode();
944        let decoded = CoapMessage::parse(&encoded).unwrap();
945
946        assert_eq!(decoded.version, original.version);
947        assert_eq!(decoded.msg_type, original.msg_type);
948        assert_eq!(decoded.code, original.code);
949        assert_eq!(decoded.message_id, original.message_id);
950        assert_eq!(decoded.token, original.token);
951        assert_eq!(decoded.payload, original.payload);
952        assert_eq!(decoded.options.len(), original.options.len());
953    }
954
955    #[test]
956    fn test_parse_too_short() {
957        let err = CoapMessage::parse(&[0x40, 0x01]).unwrap_err();
958        assert!(matches!(err, CoapError::InvalidMessage(_)));
959    }
960
961    #[test]
962    fn test_parse_bad_version() {
963        // Version 2 (invalid)
964        let data = [0x80, 0x01, 0x00, 0x01];
965        let err = CoapMessage::parse(&data).unwrap_err();
966        assert!(matches!(err, CoapError::InvalidMessage(_)));
967    }
968
969    #[test]
970    fn test_parse_tkl_too_large() {
971        // TKL=9 (invalid, max is 8)
972        let data = [0x49, 0x01, 0x00, 0x01, 0, 0, 0, 0, 0, 0, 0, 0, 0];
973        let err = CoapMessage::parse(&data).unwrap_err();
974        assert!(matches!(err, CoapError::InvalidMessage(_)));
975    }
976
977    // --- URI path extraction ---
978
979    #[test]
980    fn test_uri_path_extraction() {
981        let msg = CoapMessage {
982            version: 1,
983            msg_type: CoapMessageType::Confirmable,
984            code: CoapMethod::Post.to_code(),
985            message_id: 1,
986            token: vec![],
987            options: vec![
988                CoapOption::new(OPTION_URI_PATH, b".well-known".to_vec()),
989                CoapOption::new(OPTION_URI_PATH, b"est".to_vec()),
990                CoapOption::new(OPTION_URI_PATH, b"sen".to_vec()),
991            ],
992            payload: vec![],
993        };
994
995        assert_eq!(msg.uri_path(), "/.well-known/est/sen");
996    }
997
998    #[test]
999    fn test_uri_path_empty() {
1000        let msg = CoapMessage {
1001            version: 1,
1002            msg_type: CoapMessageType::Confirmable,
1003            code: CoapMethod::Get.to_code(),
1004            message_id: 1,
1005            token: vec![],
1006            options: vec![],
1007            payload: vec![],
1008        };
1009
1010        assert_eq!(msg.uri_path(), "/");
1011    }
1012
1013    // --- EST routing tests ---
1014
1015    #[test]
1016    fn test_route_abbreviated_paths() {
1017        assert_eq!(
1018            CoapEstRouter::route("/sen").unwrap(),
1019            EstOperation::SimpleEnroll
1020        );
1021        assert_eq!(
1022            CoapEstRouter::route("/sren").unwrap(),
1023            EstOperation::SimpleReenroll
1024        );
1025        assert_eq!(
1026            CoapEstRouter::route("/skg").unwrap(),
1027            EstOperation::ServerKeygen
1028        );
1029        assert_eq!(
1030            CoapEstRouter::route("/att").unwrap(),
1031            EstOperation::CsrAttrs
1032        );
1033        assert_eq!(
1034            CoapEstRouter::route("/cacerts").unwrap(),
1035            EstOperation::CaCerts
1036        );
1037        assert_eq!(
1038            CoapEstRouter::route("/crts").unwrap(),
1039            EstOperation::CaCerts
1040        );
1041    }
1042
1043    #[test]
1044    fn test_route_well_known_prefix() {
1045        assert_eq!(
1046            CoapEstRouter::route("/.well-known/est/sen").unwrap(),
1047            EstOperation::SimpleEnroll
1048        );
1049        assert_eq!(
1050            CoapEstRouter::route("/.well-known/est/cacerts").unwrap(),
1051            EstOperation::CaCerts
1052        );
1053    }
1054
1055    #[test]
1056    fn test_route_full_names() {
1057        assert_eq!(
1058            CoapEstRouter::route("/simpleenroll").unwrap(),
1059            EstOperation::SimpleEnroll
1060        );
1061        assert_eq!(
1062            CoapEstRouter::route("/simplereenroll").unwrap(),
1063            EstOperation::SimpleReenroll
1064        );
1065        assert_eq!(
1066            CoapEstRouter::route("/serverkeygen").unwrap(),
1067            EstOperation::ServerKeygen
1068        );
1069        assert_eq!(
1070            CoapEstRouter::route("/csrattrs").unwrap(),
1071            EstOperation::CsrAttrs
1072        );
1073    }
1074
1075    #[test]
1076    fn test_route_unknown_path() {
1077        let err = CoapEstRouter::route("/unknown").unwrap_err();
1078        assert!(matches!(err, CoapError::ResourceNotFound(_)));
1079    }
1080
1081    #[test]
1082    fn test_route_with_label_unlabeled() {
1083        // No label: the sole segment is the operation.
1084        assert_eq!(
1085            CoapEstRouter::route_with_label("/.well-known/est/sen").unwrap(),
1086            (None, EstOperation::SimpleEnroll)
1087        );
1088        assert_eq!(
1089            CoapEstRouter::route_with_label("/sen").unwrap(),
1090            (None, EstOperation::SimpleEnroll)
1091        );
1092    }
1093
1094    #[test]
1095    fn test_route_with_label_labeled() {
1096        // A label precedes the operation and is extracted verbatim.
1097        assert_eq!(
1098            CoapEstRouter::route_with_label("/.well-known/est/devices/sen").unwrap(),
1099            (Some("devices".to_string()), EstOperation::SimpleEnroll)
1100        );
1101        assert_eq!(
1102            CoapEstRouter::route_with_label("/.well-known/est/mylabel/simplereenroll").unwrap(),
1103            (Some("mylabel".to_string()), EstOperation::SimpleReenroll)
1104        );
1105        assert_eq!(
1106            CoapEstRouter::route_with_label("/mylabel/cacerts").unwrap(),
1107            (Some("mylabel".to_string()), EstOperation::CaCerts)
1108        );
1109    }
1110
1111    #[test]
1112    fn test_route_with_label_unknown_operation() {
1113        // A valid-looking label with an unknown operation is still Not Found.
1114        let err = CoapEstRouter::route_with_label("/.well-known/est/mylabel/bogus").unwrap_err();
1115        assert!(matches!(err, CoapError::ResourceNotFound(_)));
1116    }
1117
1118    #[test]
1119    fn test_route_with_label_too_many_segments() {
1120        // More than {label}/{operation} is not a recognised EST-coaps path.
1121        let err = CoapEstRouter::route_with_label("/.well-known/est/a/b/sen").unwrap_err();
1122        assert!(matches!(err, CoapError::ResourceNotFound(_)));
1123    }
1124
1125    // --- Block option accessors ---
1126
1127    #[test]
1128    fn test_message_block1_option() {
1129        let block = crate::block::BlockOption {
1130            num: 3,
1131            more: true,
1132            szx: 5,
1133        };
1134
1135        let msg = CoapMessage {
1136            version: 1,
1137            msg_type: CoapMessageType::Confirmable,
1138            code: CoapMethod::Post.to_code(),
1139            message_id: 1,
1140            token: vec![],
1141            options: vec![CoapOption::from_uint(OPTION_BLOCK1, block.encode())],
1142            payload: vec![],
1143        };
1144
1145        let extracted = msg.block1().unwrap();
1146        assert_eq!(extracted, block);
1147    }
1148
1149    #[test]
1150    fn test_message_content_format() {
1151        let msg = CoapMessage {
1152            version: 1,
1153            msg_type: CoapMessageType::Confirmable,
1154            code: CoapMethod::Post.to_code(),
1155            message_id: 1,
1156            token: vec![],
1157            options: vec![CoapOption::from_uint(
1158                OPTION_CONTENT_FORMAT,
1159                u32::from(crate::content_format::APPLICATION_PKCS10),
1160            )],
1161            payload: vec![],
1162        };
1163
1164        assert_eq!(
1165            msg.content_format(),
1166            Some(crate::content_format::APPLICATION_PKCS10)
1167        );
1168    }
1169
1170    // --- Route message integration test ---
1171
1172    #[test]
1173    fn test_route_message_post_simpleenroll() {
1174        let msg = CoapMessage {
1175            version: 1,
1176            msg_type: CoapMessageType::Confirmable,
1177            code: CoapMethod::Post.to_code(),
1178            message_id: 42,
1179            token: vec![0x01],
1180            options: vec![
1181                CoapOption::new(OPTION_URI_PATH, b".well-known".to_vec()),
1182                CoapOption::new(OPTION_URI_PATH, b"est".to_vec()),
1183                CoapOption::new(OPTION_URI_PATH, b"sen".to_vec()),
1184            ],
1185            payload: vec![0x30],
1186        };
1187
1188        let req = CoapEstRouter::route_message(msg).unwrap();
1189        assert_eq!(req.operation, EstOperation::SimpleEnroll);
1190        assert_eq!(req.method, CoapMethod::Post);
1191    }
1192
1193    #[test]
1194    fn test_route_message_get_cacerts() {
1195        let msg = CoapMessage {
1196            version: 1,
1197            msg_type: CoapMessageType::Confirmable,
1198            code: CoapMethod::Get.to_code(),
1199            message_id: 43,
1200            token: vec![],
1201            options: vec![CoapOption::new(OPTION_URI_PATH, b"cacerts".to_vec())],
1202            payload: vec![],
1203        };
1204
1205        let req = CoapEstRouter::route_message(msg).unwrap();
1206        assert_eq!(req.operation, EstOperation::CaCerts);
1207        assert_eq!(req.method, CoapMethod::Get);
1208    }
1209
1210    #[test]
1211    fn test_route_message_wrong_method() {
1212        let msg = CoapMessage {
1213            version: 1,
1214            msg_type: CoapMessageType::Confirmable,
1215            code: CoapMethod::Get.to_code(), // GET on /sen is wrong
1216            message_id: 44,
1217            token: vec![],
1218            options: vec![CoapOption::new(OPTION_URI_PATH, b"sen".to_vec())],
1219            payload: vec![],
1220        };
1221
1222        let err = CoapEstRouter::route_message(msg).unwrap_err();
1223        assert!(matches!(err, CoapError::UnsupportedMethod(_)));
1224    }
1225
1226    // --- Encode/parse round-trip with options ---
1227
1228    #[test]
1229    fn test_encode_parse_roundtrip_with_large_option_delta() {
1230        let msg = CoapMessage {
1231            version: 1,
1232            msg_type: CoapMessageType::Acknowledgement,
1233            code: CoapCode::CONTENT,
1234            message_id: 999,
1235            token: vec![0xAA, 0xBB, 0xCC],
1236            options: vec![
1237                CoapOption::new(OPTION_URI_PATH, b"test".to_vec()), // 11
1238                CoapOption::from_uint(OPTION_CONTENT_FORMAT, 285),  // 12
1239                CoapOption::from_uint(OPTION_SIZE1, 4096),          // 60
1240            ],
1241            payload: b"response-body".to_vec(),
1242        };
1243
1244        let bytes = msg.encode();
1245        let parsed = CoapMessage::parse(&bytes).unwrap();
1246
1247        assert_eq!(parsed.version, 1);
1248        assert_eq!(parsed.msg_type, CoapMessageType::Acknowledgement);
1249        assert_eq!(parsed.code, CoapCode::CONTENT);
1250        assert_eq!(parsed.message_id, 999);
1251        assert_eq!(parsed.token, vec![0xAA, 0xBB, 0xCC]);
1252        assert_eq!(parsed.payload, b"response-body");
1253
1254        // Verify option values
1255        assert_eq!(parsed.content_format(), Some(285));
1256        let size1 = parsed
1257            .options
1258            .iter()
1259            .find(|o| o.number == OPTION_SIZE1)
1260            .unwrap();
1261        assert_eq!(size1.value_as_uint(), 4096);
1262    }
1263}
1264
1265// ---------------------------------------------------------------------------
1266// CoapServer — UDP/DTLS listener for EST-coaps
1267// ---------------------------------------------------------------------------
1268
1269/// Audit metadata returned by EST handlers for NIAP FAU_GEN.1 compliance.
1270///
1271/// The `EstHandler` trait is synchronous, so handlers cannot call async
1272/// audit-recording functions directly.  Instead, they populate an
1273/// `AuditInfo` value which the async server loop records after sending the
1274/// CoAP response.
1275#[derive(Debug, Clone)]
1276pub struct AuditInfo {
1277    /// Audit event type string (e.g. `"coap_simpleenroll"`).
1278    pub event_type: String,
1279    /// Human-readable detail (e.g. serial number / subject DN).
1280    pub detail: String,
1281}
1282
1283/// Structured response from an EST handler.
1284///
1285/// Combines the CoAP response payload, content-format, and optional audit
1286/// metadata in a single return value.
1287#[derive(Debug, Clone)]
1288pub struct EstResponse {
1289    /// DER-encoded response payload.
1290    pub payload: Vec<u8>,
1291    /// CoAP Content-Format ID for the response.
1292    pub content_format: u16,
1293    /// Optional audit event to record asynchronously.
1294    pub audit_event: Option<AuditInfo>,
1295}
1296
1297/// Callback trait for processing EST operations.
1298///
1299/// Implementors handle the actual EST logic (certificate issuance, CSR
1300/// attribute retrieval, etc.) while the [`CoapDtlsServer`] handles CoAP/DTLS
1301/// transport concerns.
1302///
1303/// The callback receives the EST operation, request payload, and optional
1304/// client certificate information (from DTLS mTLS). It returns an
1305/// [`EstResponse`] containing the response payload, content-format, and
1306/// optional audit metadata for NIAP FAU_GEN.1 event recording.
1307pub trait EstHandler: Send + Sync + 'static {
1308    /// Processes an EST operation and returns the response.
1309    ///
1310    /// # Arguments
1311    ///
1312    /// * `operation` - The EST operation being requested.
1313    /// * `label` - The optional EST label from the request URI (selects the CA
1314    ///   and per-label access-control policy); `None` for the default endpoint.
1315    /// * `payload` - The request body (e.g., PKCS#10 CSR for enrollment).
1316    /// * `content_format` - The CoAP content-format of the request payload.
1317    /// * `client_cert` - Client certificate from DTLS mTLS, if presented.
1318    ///
1319    /// # Returns
1320    ///
1321    /// An [`EstResponse`] on success, or a `CoapError` on failure.
1322    fn handle(
1323        &self,
1324        operation: EstOperation,
1325        label: Option<&str>,
1326        payload: &[u8],
1327        content_format: Option<u16>,
1328        client_cert: Option<&crate::dtls::ClientCertInfo>,
1329    ) -> Result<EstResponse, CoapError>;
1330}
1331
1332/// Cached response entry: (disassembler, content-format, response code, timestamp).
1333type TransferKey = (SocketAddr, Vec<u8>, String, u8);
1334type ExchangeEntry = (Vec<u8>, Option<Vec<u8>>, Instant);
1335type ResponseCacheEntry = (BlockDisassembler, u16, CoapCode, Instant);
1336
1337/// CoAP/DTLS server for EST-coaps (RFC 9148).
1338///
1339/// Binds a UDP socket, accepts DTLS connections, parses CoAP messages,
1340/// routes EST operations, and handles block-wise transfer for large payloads.
1341///
1342/// # Architecture
1343///
1344/// ```text
1345/// UDP socket
1346///   └─ DTLS decrypt (per-peer via DtlsContext + DtlsSessionCache)
1347///       └─ CoAP parse (CoapMessage::parse)
1348///           └─ EST routing (CoapEstRouter::route_message)
1349///               └─ Block assembly (BlockAssembler for multi-block requests)
1350///               └─ EST handler callback
1351///               └─ Block disassembly (BlockDisassembler for large responses)
1352///           └─ CoAP encode (CoapMessage::encode)
1353///       └─ DTLS encrypt
1354///   └─ UDP send
1355/// ```
1356pub struct CoapDtlsServer {
1357    /// Bound UDP socket for CoAP/DTLS.
1358    socket: Arc<UdpSocket>,
1359    /// DTLS server context (certificate, key, CA).
1360    dtls_ctx: DtlsContext,
1361    /// Cache of established DTLS sessions for session resumption.
1362    session_cache: Mutex<DtlsSessionCache>,
1363    /// Active DTLS connections indexed by peer address.
1364    connections: Mutex<HashMap<SocketAddr, DtlsConnection>>,
1365    /// In-progress block-wise request assemblies.
1366    ///
1367    /// Keyed by `(peer_addr, message_token_hash)` to distinguish concurrent
1368    /// block transfers from the same or different peers.  Each entry is
1369    /// paired with an [`Instant`] recording the last activity; entries
1370    /// older than `block_ttl` are reaped on each incoming datagram.
1371    ///
1372    /// Bounded to `max_block_assemblers` entries to prevent resource
1373    /// exhaustion from peers that start but never complete block transfers.
1374    block_assemblers: Mutex<HashMap<TransferKey, (BlockAssembler, Instant)>>,
1375    /// Cached multi-block responses awaiting subsequent Block2 GETs.
1376    ///
1377    /// Keyed by `(peer_addr, token_hash)`.  Entries older than `block_ttl`
1378    /// are reaped alongside stale block assemblers.  Capped at
1379    /// `max_block_assemblers` entries.
1380    response_cache: Mutex<HashMap<TransferKey, ResponseCacheEntry>>,
1381    /// Configured block size exponent for response fragmentation.
1382    block_szx: u8,
1383    exchanges: Mutex<HashMap<(SocketAddr, u16), ExchangeEntry>>,
1384    /// Maximum reassembled payload size.
1385    max_payload: usize,
1386    /// Maximum number of concurrent block-wise assemblers (DoS guard).
1387    max_block_assemblers: usize,
1388    /// Time-to-live for block assembler and response cache entries.
1389    block_ttl: Duration,
1390}
1391
1392impl CoapDtlsServer {
1393    /// Creates and binds a new CoAP/DTLS server.
1394    ///
1395    /// # Arguments
1396    ///
1397    /// * `listen_addr` - UDP address to bind (e.g., `"0.0.0.0:5684"`).
1398    /// * `cert_pem` - PEM-encoded server certificate for DTLS.
1399    /// * `key_pem` - PEM-encoded server private key for DTLS.
1400    /// * `ca_pem` - PEM-encoded CA certificate for client cert verification.
1401    /// * `block_size` - CoAP block size in bytes (must be a power of 2, 16..=1024).
1402    /// * `max_payload` - Maximum reassembled payload size in bytes.
1403    /// * `max_sessions` - Maximum concurrent DTLS sessions.
1404    /// * `session_timeout` - DTLS session TTL for resumption.
1405    pub async fn bind(
1406        listen_addr: &str,
1407        cert_pem: &[u8],
1408        key_pem: &[u8],
1409        ca_pem: &[u8],
1410        block_size: u16,
1411        max_payload: usize,
1412        max_sessions: usize,
1413        session_timeout: Duration,
1414    ) -> Result<Self, CoapError> {
1415        let socket = UdpSocket::bind(listen_addr).await.map_err(|e| {
1416            CoapError::Internal(format!("Failed to bind UDP socket on {listen_addr}: {e}"))
1417        })?;
1418
1419        let dtls_ctx = DtlsContext::new(cert_pem, key_pem, ca_pem)?;
1420
1421        let block_szx =
1422            szx_from_block_size(block_size as usize).unwrap_or(crate::block::DEFAULT_SZX);
1423
1424        info!(
1425            listen_addr = %listen_addr,
1426            block_size = block_size,
1427            max_payload = max_payload,
1428            max_sessions = max_sessions,
1429            "CoAP/DTLS server bound"
1430        );
1431
1432        Ok(Self {
1433            socket: Arc::new(socket),
1434            dtls_ctx,
1435            session_cache: Mutex::new(DtlsSessionCache::new(max_sessions, session_timeout)),
1436            connections: Mutex::new(HashMap::new()),
1437            block_assemblers: Mutex::new(HashMap::new()),
1438            response_cache: Mutex::new(HashMap::new()),
1439            block_szx,
1440            exchanges: Mutex::new(HashMap::new()),
1441            max_payload,
1442            // Cap assemblers at 2x session limit — each peer should have at
1443            // most one active block transfer, but allow headroom for token
1444            // hash collisions and rapid reconnects.
1445            max_block_assemblers: max_sessions.saturating_mul(2),
1446            block_ttl: Duration::from_secs(60),
1447        })
1448    }
1449
1450    /// Returns the local address the server is bound to.
1451    pub fn local_addr(&self) -> Result<SocketAddr, CoapError> {
1452        self.socket
1453            .local_addr()
1454            .map_err(|e| CoapError::Internal(format!("Failed to get local address: {e}")))
1455    }
1456
1457    /// Runs the CoAP/DTLS server main loop.
1458    ///
1459    /// Receives UDP datagrams, performs DTLS processing, parses CoAP messages,
1460    /// routes to the EST handler, and sends responses. This method runs
1461    /// indefinitely until the provided cancellation token is signalled or
1462    /// an unrecoverable error occurs.
1463    ///
1464    /// # Arguments
1465    ///
1466    /// * `handler` - EST operation handler that processes enrollment requests.
1467    pub async fn run<H: EstHandler>(&self, handler: Arc<H>) -> Result<(), CoapError> {
1468        let mut buf = vec![0u8; 65535];
1469
1470        info!("CoAP/DTLS server main loop started");
1471
1472        let mut maintenance = tokio::time::interval(Duration::from_millis(100));
1473        maintenance.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip);
1474        loop {
1475            let received = tokio::select! {
1476                // A ready timer takes precedence even under continuous UDP traffic.
1477                biased;
1478                _ = maintenance.tick() => {
1479                let packets = {
1480                    let mut conns = self.connections.lock().await;
1481                    let mut packets = Vec::new();
1482                    conns.retain(|peer, conn| {
1483                        if !conn.is_handshake_complete()
1484                            && conn.created_at.elapsed() > Duration::from_secs(10)
1485                        {
1486                            return false;
1487                        }
1488                        if conn.handle_timeout().is_err() {
1489                            return false;
1490                        }
1491                        packets.extend(conn.outgoing().into_iter().map(|packet| (*peer, packet)));
1492                        true
1493                    });
1494                    packets
1495                };
1496                for (peer, packet) in packets {
1497                    let _ = self.socket.send_to(&packet, peer).await;
1498                }
1499                    continue;
1500                }
1501                result = self.socket.recv_from(&mut buf) => result,
1502            };
1503            let (len, peer_addr) = match received {
1504                Ok(result) => result,
1505                Err(e) => {
1506                    warn!(error = %e, "UDP recv_from failed, continuing");
1507                    continue;
1508                }
1509            };
1510
1511            let datagram = &buf[..len];
1512            debug!(
1513                peer = %peer_addr,
1514                len = len,
1515                "Received UDP datagram"
1516            );
1517
1518            let incoming = {
1519                let mut conns = self.connections.lock().await;
1520                let cache = self.session_cache.lock().await;
1521                conns.retain(|_, c| {
1522                    c.created_at.elapsed()
1523                        < if c.is_handshake_complete() {
1524                            cache.ttl()
1525                        } else {
1526                            Duration::from_secs(10)
1527                        }
1528                });
1529                if !conns.contains_key(&peer_addr) {
1530                    if conns.len() >= cache.max_sessions() {
1531                        continue;
1532                    }
1533                    self.block_assemblers
1534                        .lock()
1535                        .await
1536                        .retain(|key, _| key.0 != peer_addr);
1537                    self.response_cache
1538                        .lock()
1539                        .await
1540                        .retain(|key, _| key.0 != peer_addr);
1541                    self.exchanges
1542                        .lock()
1543                        .await
1544                        .retain(|key, _| key.0 != peer_addr);
1545                    conns.insert(peer_addr, DtlsConnection::new(&self.dtls_ctx, peer_addr)?);
1546                }
1547                let conn = conns.get_mut(&peer_addr).unwrap();
1548                match conn.receive(datagram) {
1549                    Ok(messages) => Some((messages, conn.outgoing())),
1550                    Err(e) => {
1551                        debug!(error = %e, "DTLS datagram rejected");
1552                        conns.remove(&peer_addr);
1553                        None
1554                    }
1555                }
1556            };
1557            if let Some((messages, outgoing)) = incoming {
1558                for packet in outgoing {
1559                    self.socket
1560                        .send_to(&packet, peer_addr)
1561                        .await
1562                        .map_err(|e| CoapError::Internal(e.to_string()))?;
1563                }
1564                for message in messages {
1565                    if let Err(e) = self
1566                        .process_datagram(&message, peer_addr, handler.clone())
1567                        .await
1568                    {
1569                        warn!(error = %e, "CoAP request failed");
1570                    }
1571                }
1572            }
1573        }
1574    }
1575
1576    /// Reaps stale block assembler and response cache entries.
1577    ///
1578    /// Called at the start of every datagram to bound memory usage from
1579    /// abandoned block transfers.
1580    async fn reap_stale_entries(&self) {
1581        let deadline = Instant::now() - self.block_ttl;
1582
1583        {
1584            let mut assemblers = self.block_assemblers.lock().await;
1585            let before = assemblers.len();
1586            assemblers.retain(|_key, (_asm, ts)| *ts > deadline);
1587            let reaped = before - assemblers.len();
1588            if reaped > 0 {
1589                debug!(reaped = reaped, "Reaped stale block assemblers");
1590            }
1591        }
1592
1593        {
1594            let mut cache = self.response_cache.lock().await;
1595            let before = cache.len();
1596            cache.retain(|_key, (_disasm, _cf, _code, ts)| *ts > deadline);
1597            let reaped = before - cache.len();
1598            if reaped > 0 {
1599                debug!(reaped = reaped, "Reaped stale response cache entries");
1600            }
1601        }
1602    }
1603
1604    /// Processes a single received datagram (after DTLS decryption).
1605    async fn process_datagram(
1606        &self,
1607        data: &[u8],
1608        peer_addr: SocketAddr,
1609        handler: Arc<dyn EstHandler>,
1610    ) -> Result<(), CoapError> {
1611        // Reap stale block assembler and response cache entries.
1612        self.reap_stale_entries().await;
1613
1614        let message = CoapMessage::parse(data)?;
1615
1616        if message.msg_type != CoapMessageType::Confirmable
1617            && message.msg_type != CoapMessageType::NonConfirmable
1618        {
1619            return Ok(());
1620        }
1621        if message.payload.len() > self.max_payload {
1622            return self
1623                .send_response(
1624                    peer_addr,
1625                    &self.build_error_response(
1626                        &message.token,
1627                        message.message_id,
1628                        &CoapError::PayloadTooLarge {
1629                            size: message.payload.len(),
1630                            max: self.max_payload,
1631                        },
1632                    ),
1633                )
1634                .await;
1635        }
1636        {
1637            let mut exchanges = self.exchanges.lock().await;
1638            exchanges.retain(|_, (_, _, time)| time.elapsed() < Duration::from_secs(247));
1639            if let Some((request, response, _)) = exchanges.get(&(peer_addr, message.message_id)) {
1640                if request != data {
1641                    return Err(CoapError::InvalidMessage(
1642                        "message ID reused during exchange lifetime".into(),
1643                    ));
1644                }
1645                let response = response.clone();
1646                drop(exchanges);
1647                if let Some(response) = response {
1648                    self.send_encoded(peer_addr, &response).await?;
1649                }
1650                return Ok(());
1651            }
1652            if exchanges.len() >= self.max_block_assemblers.saturating_mul(16) {
1653                return Err(CoapError::Internal("exchange capacity exhausted".into()));
1654            }
1655            exchanges.insert(
1656                (peer_addr, message.message_id),
1657                (data.to_vec(), None, Instant::now()),
1658            );
1659        }
1660
1661        debug!(
1662            peer = %peer_addr,
1663            msg_type = ?message.msg_type,
1664            code = %message.code,
1665            path = %message.uri_path(),
1666            "Parsed CoAP message"
1667        );
1668
1669        // Only process request messages (class 0).
1670        if !message.code.is_request() {
1671            debug!(peer = %peer_addr, "Ignoring non-request message");
1672            return Ok(());
1673        }
1674
1675        // --- Handle Block2 continuation requests (GET with Block2 option) ---
1676        //
1677        // When a client requests a subsequent block of a multi-block response,
1678        // the Block2 option carries the requested block number.  Look up the
1679        // cached disassembler for this peer/token and return the requested block.
1680        if let Some(block2) = message.block2()
1681            && block2.num > 0
1682        {
1683            let cache_key = (
1684                peer_addr,
1685                message.token.clone(),
1686                message.uri_path(),
1687                message.code.to_byte(),
1688            );
1689            let mut cache = self.response_cache.lock().await;
1690            if let Some((disasm, cf, code, ts)) = cache.get_mut(&cache_key) {
1691                // Refresh TTL on each access.
1692                *ts = Instant::now();
1693                if let Some((block_data, block_opt)) = disasm.get_block(block2.num) {
1694                    let is_last = !block_opt.more;
1695                    let response = CoapMessage {
1696                        version: COAP_VERSION,
1697                        msg_type: CoapMessageType::Acknowledgement,
1698                        code: *code,
1699                        message_id: message.message_id,
1700                        token: message.token.clone(),
1701                        options: vec![
1702                            CoapOption::from_uint(OPTION_CONTENT_FORMAT, u32::from(*cf)),
1703                            CoapOption::from_uint(OPTION_BLOCK2, block_opt.encode()),
1704                            CoapOption::from_uint(OPTION_SIZE2, disasm.payload_len() as u32),
1705                        ],
1706                        payload: block_data,
1707                    };
1708                    // If this is the final block, remove the cache entry.
1709                    if is_last {
1710                        cache.remove(&cache_key);
1711                    }
1712                    drop(cache);
1713                    self.send_response(peer_addr, &response).await?;
1714                    return Ok(());
1715                } else {
1716                    // Block number out of range — remove the stale entry.
1717                    cache.remove(&cache_key);
1718                }
1719            }
1720            drop(cache);
1721            return self
1722                .send_response(
1723                    peer_addr,
1724                    &self.build_error_response(
1725                        &message.token,
1726                        message.message_id,
1727                        &CoapError::InvalidMessage("unknown Block2 exchange".into()),
1728                    ),
1729                )
1730                .await;
1731        }
1732
1733        // Check for block-wise request (Block1).
1734        let block1 = message.block1();
1735        let content_format = message.content_format();
1736        let msg_token = message.token.clone();
1737        let msg_id = message.message_id;
1738
1739        // Route the message to determine the EST operation.
1740        let est_request = match CoapEstRouter::route_message(message) {
1741            Ok(req) => req,
1742            Err(e) => {
1743                let response = self.build_error_response(&msg_token, msg_id, &e);
1744                self.send_response(peer_addr, &response).await?;
1745                return Ok(());
1746            }
1747        };
1748
1749        // Handle block-wise assembly for multi-block requests.
1750        let request_payload = if let Some(block) = block1 {
1751            let assembler_key = (
1752                peer_addr,
1753                msg_token.clone(),
1754                est_request.message.uri_path(),
1755                est_request.message.code.to_byte(),
1756            );
1757            let mut assemblers = self.block_assemblers.lock().await;
1758
1759            // Reject new block transfers when the assembler table is full.
1760            // This prevents resource exhaustion from peers that start but
1761            // never complete block transfers.
1762            if !assemblers.contains_key(&assembler_key)
1763                && assemblers.len() >= self.max_block_assemblers
1764            {
1765                warn!(
1766                    peer = %peer_addr,
1767                    active = assemblers.len(),
1768                    limit = self.max_block_assemblers,
1769                    "Block assembler limit reached, rejecting new transfer"
1770                );
1771                return Err(CoapError::Internal(
1772                    "too many concurrent block transfers".into(),
1773                ));
1774            }
1775
1776            let (assembler, ts) = assemblers
1777                .entry(assembler_key.clone())
1778                .or_insert_with(|| (BlockAssembler::new(self.max_payload), Instant::now()));
1779
1780            // Refresh the timestamp on each block received.
1781            *ts = Instant::now();
1782
1783            let complete = assembler.process_block(&block, &est_request.message.payload)?;
1784
1785            if !complete {
1786                // Send a 2.31 Continue acknowledgement for intermediate blocks.
1787                let ack = CoapMessage {
1788                    version: COAP_VERSION,
1789                    msg_type: CoapMessageType::Acknowledgement,
1790                    code: CoapCode {
1791                        class: 2,
1792                        detail: 31,
1793                    }, // 2.31 Continue
1794                    message_id: msg_id,
1795                    token: msg_token.clone(),
1796                    options: vec![CoapOption::from_uint(OPTION_BLOCK1, block.encode())],
1797                    payload: Vec::new(),
1798                };
1799                self.send_response(peer_addr, &ack).await?;
1800                return Ok(());
1801            }
1802
1803            // All blocks received — remove the assembler and extract the full payload.
1804            let (assembler, _ts) = assemblers.remove(&assembler_key).unwrap();
1805            assembler.into_payload()?
1806        } else {
1807            est_request.message.payload.clone()
1808        };
1809
1810        if request_payload.len() > self.max_payload {
1811            let e = CoapError::PayloadTooLarge {
1812                size: request_payload.len(),
1813                max: self.max_payload,
1814            };
1815            self.send_response(
1816                peer_addr,
1817                &self.build_error_response(&msg_token, msg_id, &e),
1818            )
1819            .await?;
1820            return Ok(());
1821        }
1822
1823        // Retrieve client cert info from the DTLS connection (if any).
1824        let client_cert = {
1825            let conns = self.connections.lock().await;
1826            conns.get(&peer_addr).and_then(|c| c.client_cert().cloned())
1827        };
1828
1829        // Dispatch to the EST handler.
1830        let operation = est_request.operation;
1831        let label = est_request.label.clone();
1832        let est_response = match tokio::task::spawn_blocking(move || {
1833            handler.handle(
1834                operation,
1835                label.as_deref(),
1836                &request_payload,
1837                content_format,
1838                client_cert.as_ref(),
1839            )
1840        })
1841        .await
1842        .map_err(|e| CoapError::Internal(e.to_string()))?
1843        {
1844            Ok(result) => result,
1845            Err(e) => {
1846                let response = self.build_error_response(&msg_token, msg_id, &e);
1847                self.send_response(peer_addr, &response).await?;
1848                return Ok(());
1849            }
1850        };
1851
1852        // Extract audit info before consuming the response fields.
1853        let audit_event = est_response.audit_event.clone();
1854        let response_payload = est_response.payload;
1855        if response_payload.len() > self.max_payload {
1856            return Err(CoapError::PayloadTooLarge {
1857                size: response_payload.len(),
1858                max: self.max_payload,
1859            });
1860        }
1861        let response_cf = est_response.content_format;
1862
1863        // Determine response code based on operation.
1864        let response_code = match est_request.operation {
1865            EstOperation::SimpleEnroll
1866            | EstOperation::SimpleReenroll
1867            | EstOperation::ServerKeygen => CoapCode::CREATED,
1868            EstOperation::CaCerts | EstOperation::CsrAttrs => CoapCode::CONTENT,
1869        };
1870
1871        // Handle block-wise response (Block2) for large payloads.
1872        let block_size = crate::block::block_size_from_szx(self.block_szx);
1873        if response_payload.len() > block_size {
1874            // Start block-wise transfer — send the first block.
1875            let disasm = BlockDisassembler::new(response_payload, self.block_szx);
1876            if let Some((block_data, block_opt)) = disasm.get_block(0) {
1877                let response = CoapMessage {
1878                    version: COAP_VERSION,
1879                    msg_type: CoapMessageType::Acknowledgement,
1880                    code: response_code,
1881                    message_id: msg_id,
1882                    token: msg_token.clone(),
1883                    options: vec![
1884                        CoapOption::from_uint(OPTION_CONTENT_FORMAT, u32::from(response_cf)),
1885                        CoapOption::from_uint(OPTION_BLOCK2, block_opt.encode()),
1886                        CoapOption::from_uint(OPTION_SIZE2, disasm.payload_len() as u32),
1887                    ],
1888                    payload: block_data,
1889                };
1890                self.send_response(peer_addr, &response).await?;
1891            }
1892
1893            // Cache the disassembler for subsequent Block2 requests.
1894            let cache_key = (
1895                peer_addr,
1896                msg_token.clone(),
1897                est_request.message.uri_path(),
1898                est_request.message.code.to_byte(),
1899            );
1900            let mut cache = self.response_cache.lock().await;
1901            // Enforce the same size cap as block assemblers.
1902            if cache.len() < self.max_block_assemblers {
1903                cache.insert(
1904                    cache_key,
1905                    (disasm, response_cf, response_code, Instant::now()),
1906                );
1907            } else {
1908                warn!(
1909                    peer = %peer_addr,
1910                    active = cache.len(),
1911                    limit = self.max_block_assemblers,
1912                    "Response cache full, client must re-request"
1913                );
1914            }
1915        } else {
1916            // Single-block response.
1917            let response = CoapMessage {
1918                version: COAP_VERSION,
1919                msg_type: CoapMessageType::Acknowledgement,
1920                code: response_code,
1921                message_id: msg_id,
1922                token: msg_token,
1923                options: vec![CoapOption::from_uint(
1924                    OPTION_CONTENT_FORMAT,
1925                    u32::from(response_cf),
1926                )],
1927                payload: response_payload,
1928            };
1929            self.send_response(peer_addr, &response).await?;
1930        }
1931
1932        // Record audit event asynchronously (NIAP FAU_GEN.1).
1933        if let Some(audit) = audit_event {
1934            info!(
1935                event_type = %audit.event_type,
1936                detail = %audit.detail,
1937                peer = %peer_addr,
1938                "CoAP audit event"
1939            );
1940            // When integrated with AppState, the caller can invoke
1941            // state.record_audit_event(&audit.event_type, &audit.detail).await
1942            // from the server loop after this method returns.
1943        }
1944
1945        Ok(())
1946    }
1947
1948    /// Builds a CoAP error response from a CoapError.
1949    fn build_error_response(
1950        &self,
1951        token: &[u8],
1952        message_id: u16,
1953        error: &CoapError,
1954    ) -> CoapMessage {
1955        let code = match error {
1956            CoapError::InvalidMessage(_) => CoapCode::BAD_REQUEST,
1957            CoapError::ResourceNotFound(_) => CoapCode::NOT_FOUND,
1958            CoapError::UnsupportedMethod(_) => CoapCode::METHOD_NOT_ALLOWED,
1959            CoapError::UnsupportedContentFormat(_) => CoapCode::UNSUPPORTED_CONTENT_FORMAT,
1960            CoapError::Unauthorized(_) => CoapCode::UNAUTHORIZED,
1961            CoapError::Forbidden(_) => CoapCode::FORBIDDEN,
1962            CoapError::PayloadTooLarge { .. } => CoapCode {
1963                class: 4,
1964                detail: 13, // 4.13 Request Entity Too Large
1965            },
1966            _ => CoapCode::INTERNAL_SERVER_ERROR,
1967        };
1968
1969        CoapMessage {
1970            version: COAP_VERSION,
1971            msg_type: CoapMessageType::Acknowledgement,
1972            code,
1973            message_id,
1974            token: token.to_vec(),
1975            options: Vec::new(),
1976            payload: error.to_string().into_bytes(),
1977        }
1978    }
1979
1980    /// Sends a CoAP response to the given peer.
1981    ///
1982    /// Encrypts the response using the authenticated peer DTLS connection.
1983    async fn send_response(
1984        &self,
1985        peer_addr: SocketAddr,
1986        message: &CoapMessage,
1987    ) -> Result<(), CoapError> {
1988        let encoded = message.encode();
1989        debug!(
1990            peer = %peer_addr,
1991            code = %message.code,
1992            len = encoded.len(),
1993            "Sending CoAP response"
1994        );
1995
1996        if let Some((_, response, _)) = self
1997            .exchanges
1998            .lock()
1999            .await
2000            .get_mut(&(peer_addr, message.message_id))
2001        {
2002            *response = Some(encoded.clone());
2003        }
2004        self.send_encoded(peer_addr, &encoded).await
2005    }
2006
2007    async fn send_encoded(&self, peer_addr: SocketAddr, encoded: &[u8]) -> Result<(), CoapError> {
2008        let outgoing = {
2009            let mut connections = self.connections.lock().await;
2010            let connection = connections
2011                .get_mut(&peer_addr)
2012                .ok_or_else(|| CoapError::Unauthorized("DTLS session required".into()))?;
2013            connection.encrypt(encoded)?;
2014            connection.outgoing()
2015        };
2016        for packet in outgoing {
2017            self.socket
2018                .send_to(&packet, peer_addr)
2019                .await
2020                .map_err(|e| CoapError::Internal(e.to_string()))?;
2021        }
2022
2023        Ok(())
2024    }
2025}
2026
2027#[cfg(test)]
2028mod server_tests {
2029    use super::*;
2030
2031    /// A test EST handler that echoes the operation name.
2032    struct EchoHandler;
2033
2034    impl EstHandler for EchoHandler {
2035        fn handle(
2036            &self,
2037            operation: EstOperation,
2038            _label: Option<&str>,
2039            _payload: &[u8],
2040            _content_format: Option<u16>,
2041            _client_cert: Option<&crate::dtls::ClientCertInfo>,
2042        ) -> Result<EstResponse, CoapError> {
2043            let name = match operation {
2044                EstOperation::CaCerts => "cacerts",
2045                EstOperation::SimpleEnroll => "simpleenroll",
2046                EstOperation::SimpleReenroll => "simplereenroll",
2047                EstOperation::ServerKeygen => "serverkeygen",
2048                EstOperation::CsrAttrs => "csrattrs",
2049            };
2050            Ok(EstResponse {
2051                payload: name.as_bytes().to_vec(),
2052                content_format: crate::content_format::APPLICATION_PKCS7_MIME_CERTS_ONLY,
2053                audit_event: Some(AuditInfo {
2054                    event_type: format!("coap_{name}"),
2055                    detail: format!("test operation: {name}"),
2056                }),
2057            })
2058        }
2059    }
2060
2061    #[test]
2062    fn test_echo_handler() {
2063        let handler = EchoHandler;
2064        let resp = handler
2065            .handle(EstOperation::CaCerts, None, &[], None, None)
2066            .unwrap();
2067        assert_eq!(resp.payload, b"cacerts");
2068        assert_eq!(
2069            resp.content_format,
2070            crate::content_format::APPLICATION_PKCS7_MIME_CERTS_ONLY
2071        );
2072        let audit = resp.audit_event.unwrap();
2073        assert_eq!(audit.event_type, "coap_cacerts");
2074    }
2075
2076    #[test]
2077    fn test_build_error_response_not_found() {
2078        // We can't construct a CoapServer without binding, so test the
2079        // error-to-code mapping logic directly.
2080        let err = CoapError::ResourceNotFound("test".to_string());
2081        let code = match &err {
2082            CoapError::InvalidMessage(_) => CoapCode::BAD_REQUEST,
2083            CoapError::ResourceNotFound(_) => CoapCode::NOT_FOUND,
2084            _ => CoapCode::INTERNAL_SERVER_ERROR,
2085        };
2086        assert_eq!(code, CoapCode::NOT_FOUND);
2087    }
2088
2089    #[test]
2090    fn test_build_error_response_method_not_allowed() {
2091        let err = CoapError::UnsupportedMethod("test".to_string());
2092        let code = match &err {
2093            CoapError::UnsupportedMethod(_) => CoapCode::METHOD_NOT_ALLOWED,
2094            _ => CoapCode::INTERNAL_SERVER_ERROR,
2095        };
2096        assert_eq!(code, CoapCode::METHOD_NOT_ALLOWED);
2097    }
2098
2099    #[tokio::test]
2100    async fn authenticated_dtls_dispatch_and_duplicate_replay() {
2101        let (cert, key) = generate_test_cert();
2102        let server = Arc::new(
2103            CoapDtlsServer::bind(
2104                "127.0.0.1:0",
2105                &cert,
2106                &key,
2107                &cert,
2108                64,
2109                1024,
2110                4,
2111                Duration::from_secs(60),
2112            )
2113            .await
2114            .unwrap(),
2115        );
2116        let address = server.local_addr().unwrap();
2117        struct Counting(std::sync::atomic::AtomicUsize);
2118        impl EstHandler for Counting {
2119            fn handle(
2120                &self,
2121                _: EstOperation,
2122                _: Option<&str>,
2123                payload: &[u8],
2124                _: Option<u16>,
2125                cert: Option<&crate::dtls::ClientCertInfo>,
2126            ) -> Result<EstResponse, CoapError> {
2127                assert!(cert.is_some());
2128                self.0.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
2129                Ok(EstResponse {
2130                    payload: if payload == b"fragment" {
2131                        vec![42; 128]
2132                    } else {
2133                        b"issued".to_vec()
2134                    },
2135                    content_format: 281,
2136                    audit_event: None,
2137                })
2138            }
2139        }
2140        let handler = Arc::new(Counting(std::sync::atomic::AtomicUsize::new(0)));
2141        let running = {
2142            let server = server.clone();
2143            let handler = handler.clone();
2144            tokio::spawn(async move { server.run(handler).await })
2145        };
2146        let request = CoapMessage {
2147            version: 1,
2148            msg_type: CoapMessageType::Confirmable,
2149            code: CoapMethod::Post.to_code(),
2150            message_id: 4,
2151            token: vec![1],
2152            options: vec![CoapOption::new(OPTION_URI_PATH, b"sen".to_vec())],
2153            payload: vec![1],
2154        }
2155        .encode();
2156        let raw = UdpSocket::bind("127.0.0.1:0").await.unwrap();
2157        raw.send_to(&request, address).await.unwrap();
2158        tokio::time::sleep(Duration::from_millis(30)).await;
2159        assert_eq!(handler.0.load(std::sync::atomic::Ordering::SeqCst), 0);
2160        // Keep UDP continuously readable while the first handshake flight is
2161        // deliberately dropped below. Retransmission must not depend on idleness.
2162        let noise = tokio::spawn(async move {
2163            let mut interval = tokio::time::interval(Duration::from_millis(5));
2164            loop {
2165                interval.tick().await;
2166                raw.send_to(b"unrelated invalid datagram", address)
2167                    .await
2168                    .unwrap();
2169            }
2170        });
2171        tokio::task::spawn_blocking(move || {
2172            struct Datagram(std::net::UdpSocket, bool);
2173            impl std::io::Read for Datagram {
2174                fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
2175                    if self.1 {
2176                        let _ = self.0.recv(buf)?;
2177                        self.1 = false;
2178                    }
2179                    self.0.recv(buf)
2180                }
2181            }
2182            impl std::io::Write for Datagram {
2183                fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
2184                    self.0.send(buf)
2185                }
2186                fn flush(&mut self) -> std::io::Result<()> {
2187                    Ok(())
2188                }
2189            }
2190            let socket = std::net::UdpSocket::bind("127.0.0.1:0").unwrap();
2191            socket.connect(address).unwrap();
2192            socket
2193                .set_read_timeout(Some(Duration::from_secs(3)))
2194                .unwrap();
2195            let mut ctx =
2196                openssl::ssl::SslContext::builder(openssl::ssl::SslMethod::dtls_client()).unwrap();
2197            let certificate = openssl::x509::X509::from_pem(&cert).unwrap();
2198            ctx.set_certificate(&certificate).unwrap();
2199            ctx.set_private_key(&openssl::pkey::PKey::private_key_from_pem(&key).unwrap())
2200                .unwrap();
2201            ctx.cert_store_mut().add_cert(certificate).unwrap();
2202            ctx.set_verify(openssl::ssl::SslVerifyMode::PEER);
2203            let mut ssl = openssl::ssl::Ssl::new(&ctx.build()).unwrap();
2204            ssl.set_mtu(1200).unwrap();
2205            let mut stream = openssl::ssl::SslStream::new(ssl, Datagram(socket, true)).unwrap();
2206            stream.connect().unwrap();
2207            let mut buf = [0; 1024];
2208            stream.ssl_write(&request).unwrap();
2209            let first = stream.ssl_read(&mut buf).unwrap();
2210            let response = buf[..first].to_vec();
2211            assert_eq!(CoapMessage::parse(&response).unwrap().payload, b"issued");
2212            stream.ssl_write(&request).unwrap();
2213            let second = stream.ssl_read(&mut buf).unwrap();
2214            assert_eq!(&buf[..second], &response);
2215            let mut message = CoapMessage::parse(&request).unwrap();
2216            message.message_id = 5;
2217            message.payload = vec![1; 1025];
2218            stream.ssl_write(&message.encode()).unwrap();
2219            let n = stream.ssl_read(&mut buf).unwrap();
2220            assert_eq!(
2221                CoapMessage::parse(&buf[..n]).unwrap().code,
2222                CoapCode {
2223                    class: 4,
2224                    detail: 13
2225                }
2226            );
2227            message.message_id = 6;
2228            message.token = vec![1, 0];
2229            message.payload = b"fragment".to_vec();
2230            stream.ssl_write(&message.encode()).unwrap();
2231            let n = stream.ssl_read(&mut buf).unwrap();
2232            assert_eq!(CoapMessage::parse(&buf[..n]).unwrap().payload, vec![42; 64]);
2233            message.message_id = 7;
2234            message.token = vec![0, 31];
2235            message.payload.clear();
2236            message.options.push(CoapOption::from_uint(
2237                OPTION_BLOCK2,
2238                crate::block::BlockOption {
2239                    num: 1,
2240                    more: false,
2241                    szx: 2,
2242                }
2243                .encode(),
2244            ));
2245            stream.ssl_write(&message.encode()).unwrap();
2246            let n = stream.ssl_read(&mut buf).unwrap();
2247            assert_eq!(
2248                CoapMessage::parse(&buf[..n]).unwrap().code,
2249                CoapCode::BAD_REQUEST
2250            );
2251            message.message_id = 8;
2252            message.token = vec![1, 0];
2253            stream.ssl_write(&message.encode()).unwrap();
2254            let n = stream.ssl_read(&mut buf).unwrap();
2255            assert_eq!(CoapMessage::parse(&buf[..n]).unwrap().payload, vec![42; 64]);
2256        })
2257        .await
2258        .unwrap();
2259        assert_eq!(handler.0.load(std::sync::atomic::Ordering::SeqCst), 2);
2260        noise.abort();
2261        running.abort();
2262    }
2263
2264    /// Generates a self-signed test certificate and key in PEM format.
2265    fn generate_test_cert() -> (Vec<u8>, Vec<u8>) {
2266        use openssl::asn1::Asn1Time;
2267        use openssl::bn::BigNum;
2268        use openssl::hash::MessageDigest;
2269        use openssl::pkey::PKey;
2270        use openssl::rsa::Rsa;
2271        use openssl::x509::{X509Builder, X509NameBuilder};
2272
2273        let rsa = Rsa::generate(2048).unwrap();
2274        let key = PKey::from_rsa(rsa).unwrap();
2275
2276        let mut name_builder = X509NameBuilder::new().unwrap();
2277        name_builder
2278            .append_entry_by_text("CN", "test-coap-server")
2279            .unwrap();
2280        let name = name_builder.build();
2281
2282        let mut builder = X509Builder::new().unwrap();
2283        builder.set_version(2).unwrap();
2284        builder.set_subject_name(&name).unwrap();
2285        builder.set_issuer_name(&name).unwrap();
2286        builder.set_pubkey(&key).unwrap();
2287
2288        let serial = BigNum::from_u32(1).unwrap();
2289        builder
2290            .set_serial_number(&serial.to_asn1_integer().unwrap())
2291            .unwrap();
2292
2293        let not_before = Asn1Time::days_from_now(0).unwrap();
2294        let not_after = Asn1Time::days_from_now(365).unwrap();
2295        builder.set_not_before(&not_before).unwrap();
2296        builder.set_not_after(&not_after).unwrap();
2297
2298        builder.sign(&key, MessageDigest::sha256()).unwrap();
2299        let cert = builder.build();
2300
2301        (
2302            cert.to_pem().unwrap(),
2303            key.private_key_to_pem_pkcs8().unwrap(),
2304        )
2305    }
2306}