libcoap_rs/message/
request.rs

1// SPDX-License-Identifier: BSD-2-Clause
2/*
3 * request.rs - Types wrapping messages into requests.
4 * This file is part of the libcoap-rs crate, see the README and LICENSE files for
5 * more information and terms of use.
6 * Copyright © 2021-2023 The NAMIB Project Developers, all rights reserved.
7 * See the README as well as the LICENSE file for more information.
8 */
9
10use std::fmt::{Display, Formatter};
11use std::str::FromStr;
12
13use url::Url;
14
15use crate::{
16    error::{MessageConversionError, MessageTypeError, OptionValueError},
17    message::{CoapMessage, CoapMessageCommon, CoapOption},
18    protocol::{
19        CoapMatch, CoapMessageCode, CoapMessageType, CoapOptionType, CoapRequestCode, ContentFormat, ETag, HopLimit,
20        NoResponse, Observe,
21    },
22    types::{CoapUri, CoapUriHost, CoapUriScheme},
23};
24
25pub const MAX_URI_SEGMENT_LENGTH: usize = 255;
26pub const MAX_PROXY_URI_LENGTH: usize = 1034;
27
28/// Internal representation of a CoAP URI that can be used for requests
29#[derive(Clone, Eq, PartialEq, Hash, Debug)]
30enum CoapRequestUri {
31    Request(CoapUri),
32    Proxy(CoapUri),
33}
34
35impl Display for CoapRequestUri {
36    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
37        match self {
38            CoapRequestUri::Request(v) => f.write_fmt(format_args!("Request URI: {}", v)),
39            CoapRequestUri::Proxy(v) => f.write_fmt(format_args!("Proxy URI: {}", v)),
40        }
41    }
42}
43
44impl CoapRequestUri {
45    /// Creates a new request URI from the given [CoapUri], returning an [OptionValueError] if the URI
46    /// contains invalid values for request URIs.
47    // Using unwrap_or_else here will give us an error because we want to use an iterator that
48    // outlives its Vec, so we have to use unwrap_or here.
49    #[allow(clippy::or_fun_call)]
50    pub fn new_request_uri(uri: CoapUri) -> Result<CoapRequestUri, OptionValueError> {
51        if uri
52            .path_iter()
53            .unwrap_or(vec![].iter())
54            .chain(uri.query_iter().unwrap_or(vec![].iter()))
55            .any(|x| x.len() > MAX_URI_SEGMENT_LENGTH)
56        {
57            return Err(OptionValueError::TooLong);
58        }
59        Ok(CoapRequestUri::Request(uri))
60    }
61
62    /// Creates a new request proxy URI from the given CoapUri, returning an OptionValueError if
63    /// the URI contains invalid values for proxy URIs.
64    pub fn new_proxy_uri(uri: CoapUri) -> Result<CoapRequestUri, OptionValueError> {
65        if uri.scheme().is_none() || uri.host().is_none() {
66            return Err(OptionValueError::IllegalValue);
67        }
68        if CoapRequestUri::generate_proxy_uri_string(&uri).len() > MAX_PROXY_URI_LENGTH {
69            return Err(OptionValueError::TooLong);
70        }
71        Ok(CoapRequestUri::Proxy(uri))
72    }
73
74    /// Generate a proxy URI string corresponding to this request URI.
75    fn generate_proxy_uri_string(uri: &CoapUri) -> String {
76        let mut proxy_uri_string = format!(
77            "{}://{}",
78            uri.scheme().unwrap().to_string().as_str(),
79            uri.host().unwrap().to_string().as_str()
80        );
81        if let Some(port) = uri.port() {
82            proxy_uri_string.push_str(format!(":{}", port).as_str());
83        }
84        if let Some(path) = uri.path_iter() {
85            path.for_each(|path_component| {
86                proxy_uri_string.push_str(format!("/{}", path_component).as_str());
87            });
88        }
89        if let Some(query) = uri.query_iter() {
90            let mut separator_char = '?';
91            query.for_each(|query_option| {
92                proxy_uri_string.push_str(format!("{}{}", separator_char, query_option).as_str());
93                separator_char = '&';
94            });
95        }
96        proxy_uri_string
97    }
98
99    /// Converts this request URI into a [`Vec<CoapOption>`] that can be added to a message.
100    pub fn into_options(self) -> Vec<CoapOption> {
101        let mut options = Vec::new();
102        match self {
103            CoapRequestUri::Request(mut uri) => {
104                if let Some(host) = uri.host() {
105                    options.push(CoapOption::UriHost(host.to_string()))
106                }
107                if let Some(port) = uri.port() {
108                    options.push(CoapOption::UriPort(port))
109                }
110                if let Some(path) = uri.drain_path_iter() {
111                    options.extend(path.map(CoapOption::UriPath))
112                }
113                if let Some(query) = uri.drain_query_iter() {
114                    options.extend(query.map(CoapOption::UriQuery))
115                }
116            },
117            CoapRequestUri::Proxy(uri) => {
118                options.push(CoapOption::ProxyUri(CoapRequestUri::generate_proxy_uri_string(&uri)))
119            },
120        }
121        options
122    }
123
124    /// Returns an immutable reference to the underlying URI.
125    pub fn as_uri(&self) -> &CoapUri {
126        match self {
127            CoapRequestUri::Request(uri) => uri,
128            CoapRequestUri::Proxy(uri) => uri,
129        }
130    }
131}
132
133impl TryFrom<CoapUri> for CoapRequestUri {
134    type Error = OptionValueError;
135
136    fn try_from(value: CoapUri) -> Result<Self, Self::Error> {
137        CoapRequestUri::new_request_uri(value)
138    }
139}
140
141/// Representation of a CoAP request message.
142///
143/// This struct wraps around the more direct [CoapMessage] and allows easier definition of typical
144/// options used in requests.
145#[derive(Debug, Clone, Eq, PartialEq, Hash)]
146pub struct CoapRequest {
147    pdu: CoapMessage,
148    uri: Option<CoapRequestUri>,
149    accept: Option<ContentFormat>,
150    etag: Option<Vec<ETag>>,
151    if_match: Option<Vec<CoapMatch>>,
152    content_format: Option<ContentFormat>,
153    if_none_match: bool,
154    hop_limit: Option<HopLimit>,
155    no_response: Option<NoResponse>,
156    observe: Option<Observe>,
157}
158
159impl CoapRequest {
160    /// Creates a new CoAP request with the given message type and code.
161    ///
162    /// Returns an error if the given message type is not allowed for CoAP requests (the only
163    /// allowed message types are [CoapMessageType::Con] and [CoapMessageType::Non]).
164    pub fn new(type_: CoapMessageType, code: CoapRequestCode) -> Result<CoapRequest, MessageTypeError> {
165        match type_ {
166            CoapMessageType::Con | CoapMessageType::Non => {},
167            v => return Err(MessageTypeError::InvalidForMessageCode(v)),
168        }
169        Ok(CoapRequest {
170            pdu: CoapMessage::new(type_, code.into()),
171            uri: None,
172            accept: None,
173            etag: None,
174            if_match: None,
175            content_format: None,
176            if_none_match: false,
177            hop_limit: None,
178            no_response: None,
179            observe: None,
180        })
181    }
182
183    /// Returns the "Accept" option value for this request.
184    pub fn accept(&self) -> Option<ContentFormat> {
185        self.accept
186    }
187
188    /// Sets the "Accept" option value for this request.
189    ///
190    /// This option indicates the acceptable content formats for the response.
191    ///
192    /// See [RFC 7252, Section 5.10.4](https://datatracker.ietf.org/doc/html/rfc7252#section-5.10.4)
193    /// for more information.
194    pub fn set_accept(&mut self, accept: Option<ContentFormat>) {
195        self.accept = accept
196    }
197
198    /// Returns the "ETag" option value for this request.
199    pub fn etag(&self) -> Option<&Vec<ETag>> {
200        self.etag.as_ref()
201    }
202
203    /// Sets the "ETag" option value for this request.
204    ///
205    /// This option can be used to request a specific representation of the requested resource.
206    ///
207    /// The server may send an ETag value alongside a response, which the client can then set here
208    /// to request the given representation.
209    ///
210    /// See [RFC 7252, Section 5.10.6](https://datatracker.ietf.org/doc/html/rfc7252#section-5.10.6)
211    /// for more information.
212    pub fn set_etag(&mut self, etag: Option<Vec<ETag>>) {
213        self.etag = etag
214    }
215
216    /// Returns the "If-Match" option value for this request.
217    pub fn if_match(&self) -> Option<&Vec<CoapMatch>> {
218        self.if_match.as_ref()
219    }
220
221    /// Sets the "If-Match" option value for this request.
222    ///
223    /// This option indicates a match expression that must be fulfilled in order to perform the
224    /// request.
225    ///
226    /// See [RFC 7252, Section 5.10.8.1](https://datatracker.ietf.org/doc/html/rfc7252#section-5.10.8.1)
227    /// for more information.
228    pub fn set_if_match(&mut self, if_match: Option<Vec<CoapMatch>>) {
229        self.if_match = if_match
230    }
231
232    /// Returns the "Content-Format" option value for this request.
233    pub fn content_format(&self) -> Option<ContentFormat> {
234        self.content_format
235    }
236
237    /// Sets the "Content-Format" option value for this request.
238    ///
239    /// This option indicates the content format of the body of this message. It is not to be
240    /// confused with the "Accept" option, which indicates the format that the body of the response
241    /// to this message should have.
242    ///
243    /// See [RFC 7252, Section 5.10.3](https://datatracker.ietf.org/doc/html/rfc7252#section-5.10.3)
244    /// for more information.
245    pub fn set_content_format(&mut self, content_format: Option<ContentFormat>) {
246        self.content_format = content_format;
247    }
248
249    /// Returns the "If-None-Match" option value of this request.
250    pub fn if_none_match(&self) -> bool {
251        self.if_none_match
252    }
253
254    /// Sets the "If-None-Match" option value for this request.
255    ///
256    /// This option indicates that no match expression may be fulfilled in order for this request
257    /// to be fulfilled.
258    ///
259    /// It is usually nonsensical to set this value to `true` if an If-Match-Expression has been set.
260    ///
261    /// See [RFC 7252, Section 5.10.8.2](https://datatracker.ietf.org/doc/html/rfc7252#section-5.10.8.2)
262    /// for more information.
263    pub fn set_if_none_match(&mut self, if_none_match: bool) {
264        self.if_none_match = if_none_match
265    }
266
267    /// Returns the "Hop-Limit" option value of this request.
268    pub fn hop_limit(&self) -> Option<HopLimit> {
269        self.hop_limit
270    }
271
272    /// Sets the "Hop-Limit" option value for this request.
273    ///
274    /// This option is mainly used to prevent proxying loops and specifies the maximum number of
275    /// proxies that the request may pass.
276    ///
277    /// This option is defined in [RFC 8768](https://datatracker.ietf.org/doc/html/rfc8768) and is
278    /// not part of the main CoAP spec. Some peers may therefore not support this option.
279    pub fn set_hop_limit(&mut self, hop_limit: Option<HopLimit>) {
280        self.hop_limit = hop_limit;
281    }
282
283    /// Returns the "No-Response" option value for this request.
284    pub fn no_response(&self) -> Option<NoResponse> {
285        self.no_response
286    }
287
288    /// Sets the "No-Response" option value for this request.
289    ///
290    /// This option indicates that the client performing this request does not wish to receive a
291    /// response for this request.
292    ///
293    /// This option is defined in [RFC 7967](https://datatracker.ietf.org/doc/html/rfc7967) and is
294    /// not part of the main CoAP spec. Some peers may therefore not support this option.
295    pub fn set_no_response(&mut self, no_response: Option<NoResponse>) {
296        self.no_response = no_response;
297    }
298
299    /// Returns the "Observe" option value for this request.
300    pub fn observe(&self) -> Option<Observe> {
301        self.observe
302    }
303
304    /// Sets the "Observe" option value for this request.
305    ///
306    /// This option indicates that the client performing this request wishes to be notified of
307    /// changes to the requested resource.
308    ///
309    /// This option is defined in [RFC 7641](https://datatracker.ietf.org/doc/html/rfc7641) and is
310    /// not part of the main CoAP spec. Some peers may therefore not support this option.
311    pub fn set_observe(&mut self, observe: Option<Observe>) {
312        self.observe = observe;
313    }
314
315    /// Returns the CoAP URI that is requested (either a normal request URI or a proxy URI)
316    pub fn uri(&self) -> Option<&CoapUri> {
317        self.uri.as_ref().map(|v| v.as_uri())
318    }
319
320    /// Sets the URI requested in this request.
321    ///
322    /// The request URI must not have a scheme defined, and path segments, query segments and the
323    /// host itself each have to be smaller than 255 characters.
324    ///
325    /// If the URI has an invalid format, an [OptionValueError] is returned.
326    ///
327    /// This method overrides any previously set proxy URI.
328    pub fn set_uri<U: Into<CoapUri>>(&mut self, uri: Option<U>) -> Result<(), OptionValueError> {
329        let uri = uri.map(Into::into);
330        if let Some(uri) = uri {
331            self.uri = Some(CoapRequestUri::new_request_uri(uri)?)
332        }
333        Ok(())
334    }
335
336    /// Sets the proxy URI requested in this request.
337    ///
338    /// The proxy URI must be an absolute URL with a schema valid for CoAP proxying (CoAP(s) or
339    /// HTTP(s)),
340    /// The proxy URI must not be longer than 1023 characters.
341    ///
342    /// If the URI has an invalid format, an [OptionValueError] is returned.
343    ///
344    /// This method overrides any previously set request URI.
345    pub fn set_proxy_uri<U: Into<CoapUri>>(&mut self, uri: Option<U>) -> Result<(), OptionValueError> {
346        let uri = uri.map(Into::into);
347        if let Some(uri) = uri {
348            self.uri = Some(CoapRequestUri::new_proxy_uri(uri)?)
349        }
350        Ok(())
351    }
352
353    /// Parses the given [CoapMessage] into a CoapRequest.
354    ///
355    /// Returns a [MessageConversionError] if the provided PDU cannot be parsed into a request.
356    pub fn from_message(mut pdu: CoapMessage) -> Result<CoapRequest, MessageConversionError> {
357        let mut host = None;
358        let mut port = None;
359        let mut path = None;
360        let mut query = None;
361        let mut proxy_scheme = None;
362        let mut proxy_uri = None;
363        let mut content_format = None;
364        let mut etag = None;
365        let mut if_match = None;
366        let mut if_none_match = false;
367        let mut accept = None;
368        let mut hop_limit = None;
369        let mut no_response = None;
370        let mut observe = None;
371        let mut additional_opts = Vec::new();
372        for option in pdu.options_iter() {
373            match option {
374                CoapOption::IfMatch(value) => {
375                    if if_match.is_none() {
376                        if_match = Some(Vec::new());
377                    }
378                    if_match.as_mut().unwrap().push(value.clone());
379                },
380                CoapOption::IfNoneMatch => {
381                    if if_none_match {
382                        return Err(MessageConversionError::NonRepeatableOptionRepeated(
383                            CoapOptionType::IfNoneMatch,
384                        ));
385                    }
386                    if_none_match = true;
387                },
388                CoapOption::UriHost(value) => {
389                    if host.is_some() {
390                        return Err(MessageConversionError::NonRepeatableOptionRepeated(
391                            CoapOptionType::UriHost,
392                        ));
393                    }
394                    host = Some(value.clone());
395                },
396                CoapOption::UriPort(value) => {
397                    if port.is_some() {
398                        return Err(MessageConversionError::NonRepeatableOptionRepeated(
399                            CoapOptionType::UriPort,
400                        ));
401                    }
402                    port = Some(*value);
403                },
404                CoapOption::UriPath(value) => {
405                    if path.is_none() {
406                        path = Some(Vec::new());
407                    }
408                    path.as_mut().unwrap().push(value.clone());
409                },
410                CoapOption::UriQuery(value) => {
411                    if query.is_none() {
412                        query = Some(Vec::new());
413                    }
414                    query.as_mut().unwrap().push(value.clone());
415                },
416                CoapOption::LocationPath(_) => {
417                    return Err(MessageConversionError::InvalidOptionForMessageType(
418                        CoapOptionType::LocationPath,
419                    ));
420                },
421                CoapOption::LocationQuery(_) => {
422                    return Err(MessageConversionError::InvalidOptionForMessageType(
423                        CoapOptionType::LocationQuery,
424                    ));
425                },
426                CoapOption::ProxyUri(uri) => {
427                    if proxy_uri.is_some() {
428                        return Err(MessageConversionError::NonRepeatableOptionRepeated(
429                            CoapOptionType::ProxyUri,
430                        ));
431                    }
432                    proxy_uri = Some(uri.clone())
433                },
434                CoapOption::ProxyScheme(scheme) => {
435                    if proxy_scheme.is_some() {
436                        return Err(MessageConversionError::NonRepeatableOptionRepeated(
437                            CoapOptionType::ProxyScheme,
438                        ));
439                    }
440                    proxy_scheme = Some(CoapUriScheme::from_str(scheme)?)
441                },
442                CoapOption::ContentFormat(cformat) => {
443                    if content_format.is_some() {
444                        return Err(MessageConversionError::NonRepeatableOptionRepeated(
445                            CoapOptionType::ContentFormat,
446                        ));
447                    }
448                    content_format = Some(*cformat)
449                },
450                CoapOption::Accept(value) => {
451                    if accept.is_some() {
452                        return Err(MessageConversionError::NonRepeatableOptionRepeated(
453                            CoapOptionType::Accept,
454                        ));
455                    }
456                    accept = Some(*value);
457                },
458                // libcoap handles blockwise transfer for us (for now).
459                CoapOption::Size1(_) => {},
460                CoapOption::Size2(_) => {
461                    return Err(MessageConversionError::InvalidOptionForMessageType(
462                        CoapOptionType::Size2,
463                    ));
464                },
465                // libcoap handles blockwise transfer for us (for now).
466                CoapOption::Block1(_) => {},
467                CoapOption::Block2(_) => {
468                    return Err(MessageConversionError::InvalidOptionForMessageType(
469                        CoapOptionType::Block2,
470                    ));
471                },
472                CoapOption::HopLimit(value) => {
473                    if hop_limit.is_some() {
474                        return Err(MessageConversionError::NonRepeatableOptionRepeated(
475                            CoapOptionType::HopLimit,
476                        ));
477                    }
478                    hop_limit = Some(*value);
479                },
480                CoapOption::NoResponse(value) => {
481                    if no_response.is_some() {
482                        return Err(MessageConversionError::NonRepeatableOptionRepeated(
483                            CoapOptionType::NoResponse,
484                        ));
485                    }
486                    no_response = Some(*value);
487                },
488                CoapOption::ETag(value) => {
489                    if etag.is_none() {
490                        etag = Some(Vec::new());
491                    }
492                    etag.as_mut().unwrap().push(value.clone());
493                },
494                CoapOption::MaxAge(_value) => {
495                    return Err(MessageConversionError::InvalidOptionForMessageType(
496                        CoapOptionType::MaxAge,
497                    ));
498                },
499                CoapOption::Observe(value) => {
500                    if observe.is_some() {
501                        return Err(MessageConversionError::NonRepeatableOptionRepeated(
502                            CoapOptionType::MaxAge,
503                        ));
504                    }
505                    observe = Some(*value);
506                },
507                // TODO maybe we can save some copies here if we use into_iter for the options instead.
508                CoapOption::Other(n, v) => {
509                    additional_opts.push(CoapOption::Other(*n, v.clone()));
510                },
511            }
512        }
513        pdu.clear_options();
514        for opt in additional_opts {
515            pdu.add_option(opt);
516        }
517        if proxy_scheme.is_some() && proxy_uri.is_some() {
518            return Err(MessageConversionError::InvalidOptionCombination(
519                CoapOptionType::ProxyScheme,
520                CoapOptionType::ProxyUri,
521            ));
522        }
523        let uri = if let Some(proxy_uri) = proxy_uri {
524            Some(CoapUri::try_from_url(Url::parse(&proxy_uri)?)?)
525        } else {
526            Some(CoapUri::new(
527                proxy_scheme,
528                host.map(|v| CoapUriHost::from_str(v.as_str()).unwrap()),
529                port,
530                path,
531                query,
532            ))
533        }
534        .map(|uri| {
535            if uri.scheme().is_some() {
536                CoapRequestUri::new_proxy_uri(uri)
537            } else {
538                CoapRequestUri::new_request_uri(uri)
539            }
540        });
541        let uri = if let Some(uri) = uri {
542            Some(uri.map_err(|e| MessageConversionError::InvalidOptionValue(None, e))?)
543        } else {
544            None
545        };
546        Ok(CoapRequest {
547            pdu,
548            uri,
549            accept,
550            etag,
551            if_match,
552            content_format,
553            if_none_match,
554            hop_limit,
555            no_response,
556            observe,
557        })
558    }
559
560    /// Converts this request into a [CoapMessage] that can be sent over a [CoapSession](crate::session::CoapSession).
561    pub fn into_message(mut self) -> CoapMessage {
562        if let Some(req_uri) = self.uri {
563            req_uri.into_options().into_iter().for_each(|v| self.pdu.add_option(v));
564        }
565        if let Some(accept) = self.accept {
566            self.pdu.add_option(CoapOption::Accept(accept))
567        }
568        if let Some(etags) = self.etag {
569            for etag in etags {
570                self.pdu.add_option(CoapOption::ETag(etag));
571            }
572        }
573        if let Some(if_match) = self.if_match {
574            for match_expr in if_match {
575                self.pdu.add_option(CoapOption::IfMatch(match_expr));
576            }
577        }
578        if let Some(content_format) = self.content_format {
579            self.pdu.add_option(CoapOption::ContentFormat(content_format));
580        }
581        if self.if_none_match {
582            self.pdu.add_option(CoapOption::IfNoneMatch);
583        }
584        if let Some(hop_limit) = self.hop_limit {
585            self.pdu.add_option(CoapOption::HopLimit(hop_limit));
586        }
587        if let Some(no_response) = self.no_response {
588            self.pdu.add_option(CoapOption::NoResponse(no_response));
589        }
590        if let Some(observe) = self.observe {
591            self.pdu.add_option(CoapOption::Observe(observe));
592        }
593        self.pdu
594    }
595}
596
597impl CoapMessageCommon for CoapRequest {
598    /// Sets the message code of this request.
599    ///
600    /// # Panics
601    /// Panics if the provided message code is not a request code.
602    fn set_code<C: Into<CoapMessageCode>>(&mut self, code: C) {
603        match code.into() {
604            CoapMessageCode::Request(req) => self.pdu.set_code(CoapMessageCode::Request(req)),
605            CoapMessageCode::Response(_) | CoapMessageCode::Empty => {
606                panic!("attempted to set message code of request to value that is not a request code")
607            },
608        }
609    }
610
611    fn as_message(&self) -> &CoapMessage {
612        &self.pdu
613    }
614
615    fn as_message_mut(&mut self) -> &mut CoapMessage {
616        &mut self.pdu
617    }
618}