1
// SPDX-License-Identifier: BSD-2-Clause
2
/*
3
 * Copyright © The libcoap-rs Contributors, all rights reserved.
4
 * This file is part of the libcoap-rs project, see the README file for
5
 * general information on this project and the NOTICE.md and LICENSE files
6
 * for information regarding copyright ownership and terms of use.
7
 *
8
 * message/request.rs - Types wrapping messages into requests.
9
 */
10

            
11
use std::str::FromStr;
12

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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