embedded_nal_minimal_coapserver/
lib.rs

1//! A minimal CoAP server implementation built on [embedded_nal].
2//!
3//! Usage and operation
4//! -------------------
5//!
6//! Until the project has matured further, see [the example] for usage. The general steps are:
7//!
8//! [the example]: https://gitlab.com/chrysn/coap-message-demos/-/blob/master/examples/std_embedded_nal_minicoapserver.rs
9//!
10//! * Get a network stack with a UDP server socket that implments [embedded_nal::UdpFullStack]
11//!
12//! * Create a CoAP handler that implements [coap_handler::Handler]; the
13//!   [coap_handler::implementations] module contains some building blocks
14//!   (including some to combine handlers for individual resources into a handler that picks
15//!   sub-handlers from the URI path).
16//!
17//! * Whenever there is indication that a request might have come in, call [poll] with the stack,
18//!   the socket and the handler. This will accept the data from the socket, decode the CoAP
19//!   message, pass it to the handler, and send back the response.
20//!
21//!   It returns successful if a message was processed (or something came in that could be
22//!   ignored), propagates out errors from the socket, and returns [WouldBlock](embedded_nal::nb::Error::WouldBlock) if it
23//!   turms out the socket was *not* ready.
24//!
25//!   By applying the belowmentioned constraints and exercising some of the liberties designed into
26//!   CoAP, the server does not need to hold any state of its own.
27//!
28//! Caveats
29//! -------
30//!
31//! * The server does not perform any amplification mitigation (and the handler can't for lack of
32//!   remote information); use this only in environments where this is acceptable (e.g. in closed
33//!   networks).
34//!
35//! * The server does not perform any message deduplication. All handler functions must therefore
36//!   be idempotent.
37//!
38//! * The response logic is implemented using [nb](embedded_nal::nb) and does not attempt to store responses for
39//!   later invocations. If a request comes in and the response can't be sent right away, it is
40//!   discarded.
41//!
42//! * Being based on embedded-nal, it binds to the any-address but leaves the network stack to
43//!   choose the sending address; this leads to subtle bugs when runnign on a system with multiple
44//!   IP addresses.
45//!
46//! * Messages are created with as little copying as [embedded_nal] permits. For writable messages,
47//!   that means that they need to be written to in ascending CoAP option number. This is in
48//!   accordance with the implemented [coap_message::MinimalWritableMessage] and
49//!   [coap_message::MutableWritableMessage] traits.
50//!
51//!   That restriction enables this crate to not only be `no_std`, but to not require `alloc`
52//!   either.
53//!
54//! Roadmap
55//! -------
56//!
57//! The goal of this server is to become a component that can be used easily to bring CoAP
58//! connectivity to embedded devices at the bare minimum, while still being practically usable.
59//!
60//! This means that the amplification mitigation topic will need to be addressed, and that security
61//! backing must be catered for (probably by referring to an OSCORE/EDHOC mix-in).
62//!
63//! Other than that, this implementation's plan is to stay simple and utilize the optimizations
64//! CoAP offers, even if this means limiting the application (eg. to immediate responses, and to
65//! idempotent handlers).
66//!
67//! The server offers no support for sending requests, and minimal support for receiving responses
68//! through [poll_with_response_handler]. That interface is minimal by design and for user
69//! friendliness; it is expected to be used by a (necessarily somewhat more stateful) client
70//! implementation.
71#![no_std]
72
73mod message;
74
75use num_derive::{FromPrimitive, ToPrimitive};
76use num_traits::{FromPrimitive, ToPrimitive};
77use coap_message::{MinimalWritableMessage, error::RenderableOnMinimal};
78
79use embedded_nal::nb::Result;
80
81const COAP_VERSION: u8 = 1;
82
83/// Maximum size of a CoAP message we need to expect
84///
85/// Also used in creating an output buffer as it's allocated the same way anyway.
86const MAX_SIZE: usize = 1152;
87
88#[allow(clippy::upper_case_acronyms)] // because that's how they're written in CoAP
89#[derive(FromPrimitive, ToPrimitive, PartialEq)]
90enum Type {
91    CON = 0,
92    NON = 1,
93    ACK = 2,
94    RST = 3,
95}
96
97/// Attempt to process one message out of the given `socket` on a UDP `stack`.
98///
99/// Any CoAP requests are dispatched to the handler. A response is built immediately and sent.
100///
101/// Failure to perform any action immediately makes the function return `WouldBlock`, and it should
102/// be called again whenever there is indication that the network device is ready again. Any errors
103/// from the stack are propagated out. Errors in message processing (eg. invalid CoAP messages) are
104/// treated as per the protocol and are not indicated separately; they cause a successful return.
105///
106/// Note that the caveats in the module description apply.
107pub fn poll<ST>(
108    stack: &mut ST,
109    socket: &mut ST::UdpSocket,
110    handler: &mut impl coap_handler::Handler,
111) -> Result<(), ST::Error>
112where
113    ST: embedded_nal::UdpFullStack + ?Sized,
114{
115    fn all_responses_are_unexpected(
116        _: u16,
117        _: &[u8],
118        _: &coap_message_implementations::inmemory::Message<'_>,
119    ) -> bool {
120        false
121    }
122    poll_with_response_handler(stack, socket, handler, all_responses_are_unexpected)
123}
124
125/// Like [poll], but allowing a callback for response messages.
126///
127/// The `response_handler` will be called on every received CoAP response (including empty ACKs),
128/// with message ID, token and the message. It should return true if the response was expected, and
129/// false if not; this influences whether a CON will be responded to with an ACK or an RST.
130///
131/// Users should not rely on the message argument to the handler to be precisely the indicated
132/// type; it may change to any implementation of [coap_message::ReadableMessage].
133///
134/// ## Use in multiple locations
135///
136/// This function can be suitable to be used in multiple spots, especially when an application is
137/// built in a single-threaded way and wants to use the same stack to process requests and
138/// responses.
139///
140/// From a stack usage perspective, this should be fine as far as this function is concerned: The
141/// CoAP processing, especially any heavy cryptographic processing, will only ever happen on one
142/// message buffer that is kept. If a CoAP request is sent from a place with large stack usage that
143/// can tolerate encryption of a client-side request but not the server-side handling, a suitable
144/// handler could just return 5.03 with a Max-Age indicating the time for which the client would
145/// wait for responses (i.e., until it can be expected that the server side handler is available
146/// again). Note that this is not compatible with the behavior prescribed for a Resource Directory
147/// at https://datatracker.ietf.org/doc/html/rfc9176#section-5.1-6 -- to get that behavior, at
148/// least the .well-known/core handler needs to be in place. This implementation can not, at the
149/// arrival of a request, decide to err out from the poll step, notify the application of the
150/// request's cancellation, and then take up the most recent incoming request again (because it
151/// would need to store it somewhere or merely peek at the stack). A possible workaround (that is
152/// not following the letter but the spirit of the RFC) is for the stub handler to respons 5.03
153/// with a Max-Age of 0, and make the application cancel the request to return to the idle loop;
154/// that would be suitable primarily for the simple registration procedure itself. This is a big
155/// FIXME, but may best be addressed by a more asynchronous processing model.
156///
157/// \<brooding>
158///
159/// If we were worried about how different response_handler caused this code to be monomorphized
160/// multiple times (same handler but different response handlers, as would be the case if this were
161/// called both from an idle-loop context and from different responses being expected), we could
162/// factor out a workhorse function that takes a `&mut dyn for<'a> FnMut(...)`. The
163/// monomorphization wrapper would then just put the actual Fn into an Option<>, store it on stack,
164/// and build a FnMut with a reference to the option that panics if it's empty (or no-ops so we
165/// don't need all_responses_are_unexpected) -- that weirdness will be required because we can only
166/// use dyn through references, and there's no `&owned T` (it's called Box<>, but that only works
167/// with a heap, and I don't know of a type that behaves the same just on the stack and dropping
168/// the reference drops the data in-place with out-of-stack-frame cleanups disabled). A plain
169/// no_alloc Box will probably [not work yet](https://github.com/rjsberry/no_alloc/issues/8).
170///
171/// \</brooding>
172pub fn poll_with_response_handler<ST>(
173    stack: &mut ST,
174    socket: &mut ST::UdpSocket,
175    handler: &mut impl coap_handler::Handler,
176    response_handler: impl for<'a> FnOnce(
177        u16,
178        &'a [u8],
179        &'a coap_message_implementations::inmemory::Message<'a>,
180    ) -> bool,
181) -> Result<(), ST::Error>
182where
183    ST: embedded_nal::UdpFullStack + ?Sized,
184{
185    // Receive step
186    let (extracted, addr, t_in, msgid, token) = {
187        // too bad it needs to be initialized -- but see
188        // https://github.com/rust-embedded-community/embedded-nal/issues/12 & co
189        let mut buf: [u8; MAX_SIZE] = [0; MAX_SIZE];
190
191        let (len, addr) = stack.receive(socket, &mut buf)?;
192        let buf = &mut buf[..len];
193
194        // All the UDP-specific format parsing
195        if len < 4 {
196            // Ignoring too short a request
197            return Ok(());
198        }
199        let ver = buf[0] >> 6;
200        if ver != COAP_VERSION {
201            // Mismatching version: MUST be silently ignored
202            return Ok(());
203        }
204        let t_in = Type::from_u8((buf[0] >> 4) & 0x03)
205            .expect("Success guaranteed by numberof variants and input size");
206        let tkl = buf[0] & 0x0f;
207        let tkl: usize = tkl.into();
208        let code = buf[1];
209        let msgid = u16::from_be_bytes([buf[2], buf[3]]);
210
211        if len < 4 + tkl {
212            // Another form of too short a message
213            return Ok(());
214        }
215        let token = match heapless::Vec::<_, 8>::from_slice(&buf[4..4 + tkl]) {
216            Ok(t) => t,
217            // MUST be processed as a message format error -- silently ignoring them.
218            _ => return Ok(()),
219        };
220        let tail = &buf[4 + tkl..];
221
222        // Type of empty response to send in non-request cases
223        let mut immediate_response = match t_in {
224            Type::CON => Some(Type::RST),
225            _ => None,
226        };
227
228        let msg = coap_message_implementations::inmemory::Message::new(code, tail);
229
230        if matches!(
231            coap_numbers::code::classify(code),
232            coap_numbers::code::Range::Response(_) | coap_numbers::code::Range::Empty
233        ) {
234            let was_expected = response_handler(msgid, &token, &msg);
235            if was_expected && t_in == Type::CON {
236                immediate_response = Some(Type::ACK)
237            }
238        }
239
240        if !matches!(
241            coap_numbers::code::classify(code),
242            coap_numbers::code::Range::Request
243        ) {
244            // No high-level processing from the server side, but at least a RST should be set if
245            // indicated.
246            if let Some(t_response) = immediate_response {
247                let empty_tkl = 0;
248                buf[0] = (COAP_VERSION << 6) | (t_response.to_u8().unwrap() << 4) | empty_tkl;
249                stack.send_to(socket, addr, &buf[..4])?;
250            }
251            return Ok(());
252        }
253
254        if t_in == Type::ACK || t_in == Type::RST {
255            // These should never be responses; ignoring them as protocol errors.
256            return Ok(());
257        }
258
259        (handler.extract_request_data(&msg), addr, t_in, msgid, token)
260    };
261
262    // Send step
263    {
264        // Could make this smaller based on asking the handler
265        let mut buf: [u8; MAX_SIZE] = [0; MAX_SIZE];
266
267        let t_out = if t_in == Type::CON {
268            Type::ACK
269        } else {
270            Type::NON
271        };
272        buf[0] = (COAP_VERSION << 6) | ((t_out.to_u8().unwrap()) << 4) | (token.len() as u8);
273        buf[2..4].copy_from_slice(&msgid.to_be_bytes());
274        buf[4..4 + token.len()].copy_from_slice(&token);
275        let (header, tokentail) = buf.split_at_mut(4);
276        let code = &mut header[1];
277        let tail = &mut tokentail[token.len()..];
278
279        let mut message = message::Message::new(code, tail);
280        let mut double_fault = false;
281        match extracted {
282            Ok(extracted) => {
283                if let Err(e) = handler.build_response(&mut message, extracted) {
284                    // As this function is available on the known type, we can safely err even
285                    // after partial writes, even though no rewinding trait is available so far.
286                    message.reset();
287                    double_fault = e.render(&mut message).is_err();
288                }
289            }
290            Err(e) => {
291                double_fault = e.render(&mut message).is_err();
292            }
293        }
294        if double_fault {
295            message.set_code(coap_numbers::code::INTERNAL_SERVER_ERROR);
296        }
297        let written = 4 + token.len() + message.finish();
298
299        // Note that this `?` not only propagates errors but also WouldBlock -- and by doing so
300        // discards the incoming request.
301        stack.send_to(socket, addr, &buf[..written])?;
302    };
303
304    Ok(())
305}