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

            
10
use std::str::FromStr;
11

            
12
use crate::{
13
    error::{MessageConversionError, MessageTypeError},
14
    message::{CoapMessage, CoapMessageCommon, CoapOption},
15
    protocol::{
16
        CoapMatch, CoapMessageCode, CoapMessageType, CoapOptionType, CoapRequestCode, ContentFormat, ETag, HopLimit,
17
        NoResponse, Observe,
18
    },
19
    types::{CoapUri, CoapUriScheme},
20
};
21
use crate::error::OptionValueError;
22
use crate::message::{construct_path_string, construct_query_string};
23
use crate::session::CoapSessionCommon;
24

            
25
/// Representation of a CoAP request message.
26
///
27
/// This struct wraps around the more direct [CoapMessage] and allows easier definition of typical
28
/// options used in requests.
29
#[derive(Debug, Clone, Eq, PartialEq)]
30
pub struct CoapRequest {
31
    pdu: CoapMessage,
32
    uri: CoapUri,
33
    accept: Option<ContentFormat>,
34
    etag: Option<Vec<ETag>>,
35
    if_match: Option<Vec<CoapMatch>>,
36
    content_format: Option<ContentFormat>,
37
    if_none_match: bool,
38
    hop_limit: Option<HopLimit>,
39
    no_response: Option<NoResponse>,
40
    observe: Option<Observe>,
41
}
42

            
43
impl CoapRequest {
44
    /// Creates a new CoAP request with the given message type and code.
45
    ///
46
    /// Returns an error if the given message type is not allowed for CoAP requests (the only
47
    /// allowed message types are [CoapMessageType::Con] and [CoapMessageType::Non]) or the request
48
    /// URI is malformed.
49
233
    pub fn new(type_: CoapMessageType, code: CoapRequestCode, uri: CoapUri) -> Result<CoapRequest, MessageTypeError> {
50
233
        match type_ {
51
233
            CoapMessageType::Con | CoapMessageType::Non => {},
52
            v => return Err(MessageTypeError::InvalidForMessageCode(v)),
53
        }
54
233
        Ok(CoapRequest {
55
233
            pdu: CoapMessage::new(type_, code.into()),
56
233
            uri,
57
233
            accept: None,
58
233
            etag: None,
59
233
            if_match: None,
60
233
            content_format: None,
61
233
            if_none_match: false,
62
233
            hop_limit: None,
63
233
            no_response: None,
64
233
            observe: None,
65
233
        })
66
233
    }
67

            
68
    /// Returns the "Accept" option value for this request.
69
    pub fn accept(&self) -> Option<ContentFormat> {
70
        self.accept
71
    }
72

            
73
    /// Sets the "Accept" option value for this request.
74
    ///
75
    /// This option indicates the acceptable content formats for the response.
76
    ///
77
    /// See [RFC 7252, Section 5.10.4](https://datatracker.ietf.org/doc/html/rfc7252#section-5.10.4)
78
    /// for more information.
79
    pub fn set_accept(&mut self, accept: Option<ContentFormat>) {
80
        self.accept = accept
81
    }
82

            
83
    /// Returns the "ETag" option value for this request.
84
    pub fn etag(&self) -> Option<&Vec<ETag>> {
85
        self.etag.as_ref()
86
    }
87

            
88
    /// Sets the "ETag" option value for this request.
89
    ///
90
    /// This option can be used to request a specific representation of the requested resource.
91
    ///
92
    /// The server may send an ETag value alongside a response, which the client can then set here
93
    /// to request the given representation.
94
    ///
95
    /// See [RFC 7252, Section 5.10.6](https://datatracker.ietf.org/doc/html/rfc7252#section-5.10.6)
96
    /// for more information.
97
    pub fn set_etag(&mut self, etag: Option<Vec<ETag>>) {
98
        self.etag = etag
99
    }
100

            
101
    /// Returns the "If-Match" option value for this request.
102
    pub fn if_match(&self) -> Option<&Vec<CoapMatch>> {
103
        self.if_match.as_ref()
104
    }
105

            
106
    /// Sets the "If-Match" option value for this request.
107
    ///
108
    /// This option indicates a match expression that must be fulfilled in order to perform the
109
    /// request.
110
    ///
111
    /// See [RFC 7252, Section 5.10.8.1](https://datatracker.ietf.org/doc/html/rfc7252#section-5.10.8.1)
112
    /// for more information.
113
    pub fn set_if_match(&mut self, if_match: Option<Vec<CoapMatch>>) {
114
        self.if_match = if_match
115
    }
116

            
117
    /// Returns the "Content-Format" option value for this request.
118
    pub fn content_format(&self) -> Option<ContentFormat> {
119
        self.content_format
120
    }
121

            
122
    /// Sets the "Content-Format" option value for this request.
123
    ///
124
    /// This option indicates the content format of the body of this message. It is not to be
125
    /// confused with the "Accept" option, which indicates the format that the body of the response
126
    /// to this message should have.
127
    ///
128
    /// See [RFC 7252, Section 5.10.3](https://datatracker.ietf.org/doc/html/rfc7252#section-5.10.3)
129
    /// for more information.
130
    pub fn set_content_format(&mut self, content_format: Option<ContentFormat>) {
131
        self.content_format = content_format;
132
    }
133

            
134
    /// Returns the "If-None-Match" option value of this request.
135
    pub fn if_none_match(&self) -> bool {
136
        self.if_none_match
137
    }
138

            
139
    /// Sets the "If-None-Match" option value for this request.
140
    ///
141
    /// This option indicates that no match expression may be fulfilled in order for this request
142
    /// to be fulfilled.
143
    ///
144
    /// It is usually nonsensical to set this value to `true` if an If-Match-Expression has been set.
145
    ///
146
    /// See [RFC 7252, Section 5.10.8.2](https://datatracker.ietf.org/doc/html/rfc7252#section-5.10.8.2)
147
    /// for more information.
148
    pub fn set_if_none_match(&mut self, if_none_match: bool) {
149
        self.if_none_match = if_none_match
150
    }
151

            
152
    /// Returns the "Hop-Limit" option value of this request.
153
    pub fn hop_limit(&self) -> Option<HopLimit> {
154
        self.hop_limit
155
    }
156

            
157
    /// Sets the "Hop-Limit" option value for this request.
158
    ///
159
    /// This option is mainly used to prevent proxying loops and specifies the maximum number of
160
    /// proxies that the request may pass.
161
    ///
162
    /// This option is defined in [RFC 8768](https://datatracker.ietf.org/doc/html/rfc8768) and is
163
    /// not part of the main CoAP spec. Some peers may therefore not support this option.
164
    pub fn set_hop_limit(&mut self, hop_limit: Option<HopLimit>) {
165
        self.hop_limit = hop_limit;
166
    }
167

            
168
    /// Returns the "No-Response" option value for this request.
169
    pub fn no_response(&self) -> Option<NoResponse> {
170
        self.no_response
171
    }
172

            
173
    /// Sets the "No-Response" option value for this request.
174
    ///
175
    /// This option indicates that the client performing this request does not wish to receive a
176
    /// response for this request.
177
    ///
178
    /// This option is defined in [RFC 7967](https://datatracker.ietf.org/doc/html/rfc7967) and is
179
    /// not part of the main CoAP spec. Some peers may therefore not support this option.
180
    pub fn set_no_response(&mut self, no_response: Option<NoResponse>) {
181
        self.no_response = no_response;
182
    }
183

            
184
    /// Returns the "Observe" option value for this request.
185
    pub fn observe(&self) -> Option<Observe> {
186
        self.observe
187
    }
188

            
189
    /// Sets the "Observe" option value for this request.
190
    ///
191
    /// This option indicates that the client performing this request wishes to be notified of
192
    /// changes to the requested resource.
193
    ///
194
    /// This option is defined in [RFC 7641](https://datatracker.ietf.org/doc/html/rfc7641) and is
195
    /// not part of the main CoAP spec. Some peers may therefore not support this option.
196
    pub fn set_observe(&mut self, observe: Option<Observe>) {
197
        self.observe = observe;
198
    }
199

            
200
    /// Returns the CoAP URI that is requested.
201
    pub fn uri(&self) -> &CoapUri {
202
        &self.uri
203
    }
204

            
205
    /// Parses the given [CoapMessage] into a CoapRequest.
206
    ///
207
    /// Returns a [MessageConversionError] if the provided PDU cannot be parsed into a request.
208
25
    pub fn from_message<'a>(
209
25
        mut pdu: CoapMessage,
210
25
        session: &impl CoapSessionCommon<'a>,
211
25
    ) -> Result<CoapRequest, MessageConversionError> {
212
25
        let mut host = None;
213
25
        let mut port = None;
214
25
        let mut path = None;
215
25
        let mut query = None;
216
25
        let mut proxy_scheme = None;
217
25
        let mut proxy_uri = None;
218
25
        let mut content_format = None;
219
25
        let mut etag = None;
220
25
        let mut if_match = None;
221
25
        let mut if_none_match = false;
222
25
        let mut accept = None;
223
25
        let mut hop_limit = None;
224
25
        let mut no_response = None;
225
25
        let mut observe = None;
226
25
        let mut additional_opts = Vec::new();
227
50
        for option in pdu.options_iter() {
228
50
            match option {
229
                CoapOption::IfMatch(value) => {
230
                    if if_match.is_none() {
231
                        if_match = Some(Vec::new());
232
                    }
233
                    if_match.as_mut().unwrap().push(value.clone());
234
                },
235
                CoapOption::IfNoneMatch => {
236
                    if if_none_match {
237
                        return Err(MessageConversionError::NonRepeatableOptionRepeated(
238
                            CoapOptionType::IfNoneMatch,
239
                        ));
240
                    }
241
                    if_none_match = true;
242
                },
243
                CoapOption::UriHost(value) => {
244
                    if host.is_some() {
245
                        return Err(MessageConversionError::NonRepeatableOptionRepeated(
246
                            CoapOptionType::UriHost,
247
                        ));
248
                    }
249
                    host = Some(value.clone().into_bytes());
250
                },
251
                CoapOption::UriPort(value) => {
252
                    if port.is_some() {
253
                        return Err(MessageConversionError::NonRepeatableOptionRepeated(
254
                            CoapOptionType::UriPort,
255
                        ));
256
                    }
257
                    port = Some(*value);
258
                },
259
25
                CoapOption::UriPath(value) => {
260
25
                    if path.is_none() {
261
25
                        path = Some(Vec::new());
262
25
                    }
263
25
                    path.as_mut().unwrap().push(value.clone());
264
                },
265
                CoapOption::UriQuery(value) => {
266
                    if query.is_none() {
267
                        query = Some(Vec::new());
268
                    }
269
                    query.as_mut().unwrap().push(value.clone());
270
                },
271
                CoapOption::LocationPath(_) => {
272
                    return Err(MessageConversionError::InvalidOptionForMessageType(
273
                        CoapOptionType::LocationPath,
274
                    ));
275
                },
276
                CoapOption::LocationQuery(_) => {
277
                    return Err(MessageConversionError::InvalidOptionForMessageType(
278
                        CoapOptionType::LocationQuery,
279
                    ));
280
                },
281
                CoapOption::ProxyUri(uri) => {
282
                    if proxy_uri.is_some() {
283
                        return Err(MessageConversionError::NonRepeatableOptionRepeated(
284
                            CoapOptionType::ProxyUri,
285
                        ));
286
                    }
287
                    proxy_uri = Some(uri.clone())
288
                },
289
                CoapOption::ProxyScheme(scheme) => {
290
                    if proxy_scheme.is_some() {
291
                        return Err(MessageConversionError::NonRepeatableOptionRepeated(
292
                            CoapOptionType::ProxyScheme,
293
                        ));
294
                    }
295
                    proxy_scheme = Some(CoapUriScheme::from_str(scheme)?)
296
                },
297
                CoapOption::ContentFormat(cformat) => {
298
                    if content_format.is_some() {
299
                        return Err(MessageConversionError::NonRepeatableOptionRepeated(
300
                            CoapOptionType::ContentFormat,
301
                        ));
302
                    }
303
                    content_format = Some(*cformat)
304
                },
305
                CoapOption::Accept(value) => {
306
                    if accept.is_some() {
307
                        return Err(MessageConversionError::NonRepeatableOptionRepeated(
308
                            CoapOptionType::Accept,
309
                        ));
310
                    }
311
                    accept = Some(*value);
312
                },
313
                // libcoap handles blockwise transfer for us (for now).
314
                CoapOption::Size1(_) => {},
315
                CoapOption::Size2(_) => {
316
                    return Err(MessageConversionError::InvalidOptionForMessageType(
317
                        CoapOptionType::Size2,
318
                    ));
319
                },
320
                // libcoap handles blockwise transfer for us (for now).
321
                CoapOption::Block1(_) => {},
322
                CoapOption::Block2(_) => {
323
                    return Err(MessageConversionError::InvalidOptionForMessageType(
324
                        CoapOptionType::Block2,
325
                    ));
326
                },
327
                // libcoap handles blockwise transfer for us (for now).
328
                CoapOption::QBlock1(_) => {},
329
                CoapOption::QBlock2(_) => {},
330
                CoapOption::HopLimit(value) => {
331
                    if hop_limit.is_some() {
332
                        return Err(MessageConversionError::NonRepeatableOptionRepeated(
333
                            CoapOptionType::HopLimit,
334
                        ));
335
                    }
336
                    hop_limit = Some(*value);
337
                },
338
                CoapOption::NoResponse(value) => {
339
                    if no_response.is_some() {
340
                        return Err(MessageConversionError::NonRepeatableOptionRepeated(
341
                            CoapOptionType::NoResponse,
342
                        ));
343
                    }
344
                    no_response = Some(*value);
345
                },
346
                CoapOption::ETag(value) => {
347
                    if etag.is_none() {
348
                        etag = Some(Vec::new());
349
                    }
350
                    etag.as_mut().unwrap().push(value.clone());
351
                },
352
                CoapOption::MaxAge(_value) => {
353
                    return Err(MessageConversionError::InvalidOptionForMessageType(
354
                        CoapOptionType::MaxAge,
355
                    ));
356
                },
357
                CoapOption::Observe(value) => {
358
                    if observe.is_some() {
359
                        return Err(MessageConversionError::NonRepeatableOptionRepeated(
360
                            CoapOptionType::MaxAge,
361
                        ));
362
                    }
363
                    observe = Some(*value);
364
                },
365
                // Handling of echo options is automatically done by libcoap (see man coap_send)
366
                CoapOption::Echo(_) => {},
367
                // Handling of request tag options is automatically done by libcoap (see man
368
                // coap_send)
369
25
                CoapOption::RTag(_) => {},
370
                // OSCORE is currently not supported, and even if it should probably be handled by
371
                // libcoap, so I'm unsure whether we have to expose this.
372
                CoapOption::Oscore(_v) => {},
373
                // TODO maybe we can save some copies here if we use into_iter for the options instead.
374
                CoapOption::Other(n, v) => {
375
                    additional_opts.push(CoapOption::Other(*n, v.clone()));
376
                },
377
            }
378
        }
379
25
        pdu.clear_options();
380
25
        for opt in additional_opts {
381
            pdu.add_option(opt);
382
        }
383
25
        if proxy_scheme.is_some() && proxy_uri.is_some() {
384
            return Err(MessageConversionError::InvalidOptionCombination(
385
                CoapOptionType::ProxyScheme,
386
                CoapOptionType::ProxyUri,
387
            ));
388
25
        }
389
25
        let uri = if let Some(v) = proxy_uri {
390
            CoapUri::try_from_str_proxy(v.as_str())
391
        } else {
392
25
            let path_str = path.map(construct_path_string);
393
25
            let query_str = query.map(construct_query_string);
394
25

            
395
25
            match proxy_scheme {
396
                Some(scheme) => CoapUri::new_proxy(
397
                    scheme,
398
                    host.as_deref().unwrap_or(&[]),
399
                    port.unwrap_or(0),
400
                    path_str.as_ref().map(|v| v.as_bytes()),
401
                    query_str.as_ref().map(|v| v.as_bytes()),
402
                ),
403
25
                None => CoapUri::new(
404
25
                    session.proto().into(),
405
25
                    host.as_deref().unwrap_or(&[]),
406
25
                    port.unwrap_or(0),
407
25
                    path_str.as_ref().map(|v| v.as_bytes()),
408
25
                    query_str.as_ref().map(|v| v.as_bytes()),
409
25
                ),
410
            }
411
        }
412
25
        .map_err(|e| MessageConversionError::InvalidOptionValue(None, OptionValueError::UriParsing(e)))?;
413

            
414
25
        Ok(CoapRequest {
415
25
            pdu,
416
25
            uri,
417
25
            accept,
418
25
            etag,
419
25
            if_match,
420
25
            content_format,
421
25
            if_none_match,
422
25
            hop_limit,
423
25
            no_response,
424
25
            observe,
425
25
        })
426
25
    }
427

            
428
    /// Converts this request into a [CoapMessage] that can be sent over a [CoapSession](crate::session::CoapSession).
429
233
    pub fn into_message(mut self) -> CoapMessage {
430
233
        if self.uri.is_proxy() {
431
            self.pdu.add_option(CoapOption::ProxyScheme(
432
                self.uri.scheme().expect("Parsed CoAP URI must have scheme").to_string(),
433
            ))
434
233
        }
435
233
        self.uri.into_options().into_iter().for_each(|v| self.pdu.add_option(v));
436
233
        if let Some(accept) = self.accept {
437
            self.pdu.add_option(CoapOption::Accept(accept))
438
233
        }
439
233
        if let Some(etags) = self.etag {
440
            for etag in etags {
441
                self.pdu.add_option(CoapOption::ETag(etag));
442
            }
443
233
        }
444
233
        if let Some(if_match) = self.if_match {
445
            for match_expr in if_match {
446
                self.pdu.add_option(CoapOption::IfMatch(match_expr));
447
            }
448
233
        }
449
233
        if let Some(content_format) = self.content_format {
450
            self.pdu.add_option(CoapOption::ContentFormat(content_format));
451
233
        }
452
233
        if self.if_none_match {
453
            self.pdu.add_option(CoapOption::IfNoneMatch);
454
233
        }
455
233
        if let Some(hop_limit) = self.hop_limit {
456
            self.pdu.add_option(CoapOption::HopLimit(hop_limit));
457
233
        }
458
233
        if let Some(no_response) = self.no_response {
459
            self.pdu.add_option(CoapOption::NoResponse(no_response));
460
233
        }
461
233
        if let Some(observe) = self.observe {
462
            self.pdu.add_option(CoapOption::Observe(observe));
463
233
        }
464
233
        self.pdu
465
233
    }
466
}
467

            
468
impl CoapMessageCommon for CoapRequest {
469
    /// Sets the message code of this request.
470
    ///
471
    /// # Panics
472
    /// Panics if the provided message code is not a request code.
473
    fn set_code<C: Into<CoapMessageCode>>(&mut self, code: C) {
474
        match code.into() {
475
            CoapMessageCode::Request(req) => self.pdu.set_code(CoapMessageCode::Request(req)),
476
            CoapMessageCode::Response(_) | CoapMessageCode::Empty => {
477
                panic!("attempted to set message code of request to value that is not a request code")
478
            },
479
        }
480
    }
481

            
482
932
    fn as_message(&self) -> &CoapMessage {
483
932
        &self.pdu
484
932
    }
485

            
486
466
    fn as_message_mut(&mut self) -> &mut CoapMessage {
487
466
        &mut self.pdu
488
466
    }
489
}