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}