libcoap_rs/session/
client.rs

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 * session/client.rs - Types relating to client-side CoAP sessions.
9 */
10
11use std::{
12    cell::{Ref, RefMut},
13    net::SocketAddr,
14};
15
16#[cfg(feature = "oscore")]
17use libcoap_sys::coap_new_client_session_oscore;
18use libcoap_sys::{
19    coap_new_client_session, coap_proto_t_COAP_PROTO_DTLS, coap_proto_t_COAP_PROTO_TCP, coap_proto_t_COAP_PROTO_UDP,
20    coap_register_event_handler, coap_session_get_app_data, coap_session_get_context, coap_session_get_type,
21    coap_session_init_token, coap_session_release, coap_session_set_app_data, coap_session_t,
22    coap_session_type_t_COAP_SESSION_TYPE_CLIENT, coap_session_type_t_COAP_SESSION_TYPE_HELLO,
23    coap_session_type_t_COAP_SESSION_TYPE_NONE, coap_session_type_t_COAP_SESSION_TYPE_SERVER, COAP_TOKEN_DEFAULT_MAX,
24};
25
26use super::{CoapSessionCommon, CoapSessionInner, CoapSessionInnerProvider};
27#[cfg(feature = "dtls")]
28use crate::crypto::ClientCryptoContext;
29#[cfg(feature = "oscore")]
30use crate::oscore::OscoreConf;
31use crate::{
32    context::CoapContext,
33    error::SessionCreationError,
34    event::event_handler_callback,
35    mem::{CoapFfiRcCell, DropInnerExclusively},
36    prng::coap_prng_try_fill,
37    types::CoapAddress,
38};
39
40#[derive(Debug)]
41struct CoapClientSessionInner<'a> {
42    inner: CoapSessionInner<'a>,
43    #[cfg(feature = "dtls")]
44    // This field is actually referred to be libcoap, so it isn't actually unused.
45    #[allow(unused)]
46    crypto_ctx: Option<ClientCryptoContext<'a>>,
47}
48
49impl<'a> CoapClientSessionInner<'a> {
50    /// Initializes a new [`CoapClientSessionInner`] for an unencrypted session from its raw counterpart
51    /// with the provided initial information.
52    ///
53    /// Also initializes the message token to a random value to prevent off-path response spoofing
54    /// (see [RFC 7252, section 5.3.1](https://datatracker.ietf.org/doc/html/rfc7252#section-5.3.1)).
55    ///
56    /// # Safety
57    /// The provided pointer for `raw_session` must be valid and point to the newly constructed raw
58    /// session.
59    unsafe fn new(raw_session: *mut coap_session_t) -> CoapFfiRcCell<CoapClientSessionInner<'a>> {
60        // For insecure protocols, generate a random initial token to prevent off-path response
61        // spoofing, see https://datatracker.ietf.org/doc/html/rfc7252#section-5.3.1
62        let mut token = [0; COAP_TOKEN_DEFAULT_MAX as usize];
63        coap_prng_try_fill(&mut token).expect("unable to generate random initial token");
64        coap_session_init_token(raw_session, token.len(), token.as_ptr());
65
66        let inner_session = CoapFfiRcCell::new(CoapClientSessionInner {
67            inner: CoapSessionInner::new(raw_session),
68            #[cfg(feature = "dtls")]
69            crypto_ctx: None,
70        });
71
72        // SAFETY: raw session is valid, inner session pointer must be valid as it was just created
73        // from one of Rust's smart pointers.
74        coap_session_set_app_data(raw_session, inner_session.create_raw_weak());
75
76        inner_session
77    }
78
79    /// Initializes a new [`CoapClientSessionInner`] for an encrypted session from its raw counterpart
80    /// with the provided initial information.
81    ///
82    /// # Safety
83    /// The provided pointer for `raw_session` must be valid and point to the newly constructed raw
84    /// session.
85    #[cfg(feature = "dtls")]
86    unsafe fn new_with_crypto_ctx(
87        raw_session: *mut coap_session_t,
88        crypto_ctx: ClientCryptoContext<'a>,
89    ) -> CoapFfiRcCell<CoapClientSessionInner<'a>> {
90        let inner_session = CoapFfiRcCell::new(CoapClientSessionInner {
91            inner: CoapSessionInner::new(raw_session),
92            crypto_ctx: Some(crypto_ctx),
93        });
94
95        // SAFETY: raw session is valid, inner session pointer must be valid as it was just created
96        // from one of Rust's smart pointers.
97        coap_session_set_app_data(raw_session, inner_session.create_raw_weak());
98
99        inner_session
100    }
101}
102
103/// Representation of a client-side CoAP session.
104#[derive(Debug, Clone)]
105pub struct CoapClientSession<'a> {
106    inner: CoapFfiRcCell<CoapClientSessionInner<'a>>,
107}
108
109impl CoapClientSession<'_> {
110    /// Create a new DTLS encrypted session with the given peer `addr` using the given `crypto_ctx`.
111    ///
112    /// # Errors
113    /// Will return a [SessionCreationError] if libcoap was unable to create a session (most likely
114    /// because it was not possible to bind to a port).
115    #[cfg(feature = "dtls")]
116    pub fn connect_dtls<'a>(
117        ctx: &mut CoapContext<'a>,
118        addr: SocketAddr,
119        crypto_ctx: impl Into<ClientCryptoContext<'a>>,
120    ) -> Result<CoapClientSession<'a>, SessionCreationError> {
121        let crypto_ctx = crypto_ctx.into();
122        // SAFETY: The returned raw session lives for as long as the constructed
123        // CoapClientSessionInner does, which is limited to the lifetime of crypto_ctx.
124        // When the CoapClientSessionInner instance is dropped, the session is dropped before the
125        // crypto context is.
126        let raw_session = unsafe {
127            match &crypto_ctx {
128                #[cfg(feature = "dtls-psk")]
129                ClientCryptoContext::Psk(psk_ctx) => {
130                    psk_ctx.create_raw_session(ctx, &addr.into(), coap_proto_t_COAP_PROTO_DTLS)?
131                },
132                #[cfg(feature = "dtls-pki")]
133                ClientCryptoContext::Pki(pki_ctx) => {
134                    pki_ctx.create_raw_session(ctx, &addr.into(), coap_proto_t_COAP_PROTO_DTLS)?
135                },
136                #[cfg(feature = "dtls-rpk")]
137                ClientCryptoContext::Rpk(rpk_ctx) => {
138                    rpk_ctx.create_raw_session(ctx, &addr.into(), coap_proto_t_COAP_PROTO_DTLS)?
139                },
140            }
141        };
142
143        // SAFETY: raw_session was just checked to be valid pointer.
144        Ok(CoapClientSession {
145            inner: unsafe { CoapClientSessionInner::new_with_crypto_ctx(raw_session.as_ptr(), crypto_ctx) },
146        })
147    }
148
149    /// Create a new unencrypted session with the given peer over UDP.
150    ///
151    /// # Errors
152    /// Will return a [SessionCreationError] if libcoap was unable to create a session (most likely
153    /// because it was not possible to bind to a port).
154    pub fn connect_udp<'a>(
155        ctx: &mut CoapContext<'a>,
156        addr: SocketAddr,
157    ) -> Result<CoapClientSession<'a>, SessionCreationError> {
158        // SAFETY: self.raw_context is guaranteed to be valid, local_if can be null.
159        let session = unsafe {
160            coap_new_client_session(
161                ctx.as_mut_raw_context(),
162                std::ptr::null(),
163                CoapAddress::from(addr).as_raw_address(),
164                coap_proto_t_COAP_PROTO_UDP,
165            )
166        };
167        if session.is_null() {
168            return Err(SessionCreationError::Unknown);
169        }
170        // SAFETY: Session was just checked for validity.
171        Ok(CoapClientSession {
172            inner: unsafe { CoapClientSessionInner::new(session) },
173        })
174    }
175
176    /// Create a new unencrypted session with the given peer over TCP.
177    ///
178    /// # Errors
179    /// Will return a [SessionCreationError] if libcoap was unable to create a session (most likely
180    /// because it was not possible to bind to a port).
181    pub fn connect_tcp<'a>(
182        ctx: &mut CoapContext<'a>,
183        addr: SocketAddr,
184    ) -> Result<CoapClientSession<'a>, SessionCreationError> {
185        // SAFETY: self.raw_context is guaranteed to be valid, local_if can be null.
186        let session = unsafe {
187            coap_new_client_session(
188                ctx.as_mut_raw_context(),
189                std::ptr::null(),
190                CoapAddress::from(addr).as_raw_address(),
191                coap_proto_t_COAP_PROTO_TCP,
192            )
193        };
194        if session.is_null() {
195            return Err(SessionCreationError::Unknown);
196        }
197        // SAFETY: Session was just checked for validity.
198        Ok(CoapClientSession {
199            inner: unsafe { CoapClientSessionInner::new(session) },
200        })
201    }
202
203    /// Create an encrypted session with the given peer over UDP using OSCORE.
204    ///
205    /// # Errors
206    /// Will return a [SessionCreationError] if libcoap was unable to create a session
207    /// (most likely because it was not possible to bind to a port).
208    #[cfg(feature = "oscore")]
209    pub fn connect_oscore<'a>(
210        ctx: &mut CoapContext<'a>,
211        addr: SocketAddr,
212        oscore_conf: OscoreConf,
213    ) -> Result<CoapClientSession<'a>, SessionCreationError> {
214        // SAFETY: self.raw_context is guaranteed to be valid, local_if can be null.
215        // OscoreConf raw_conf should be valid, else we return an error.
216        //
217        // coap_new_client_session_oscore should free the raw_conf:
218        // [libcoap docs](https://libcoap.net/doc/reference/4.3.5/group__oscore.html#ga65ac1a57ebc037b4d14538c8e21c28a7)
219        let session = unsafe {
220            coap_new_client_session_oscore(
221                ctx.as_mut_raw_context(),
222                std::ptr::null(),
223                CoapAddress::from(addr).as_raw_address(),
224                coap_proto_t_COAP_PROTO_UDP,
225                oscore_conf.into_raw_conf().0,
226            )
227        };
228
229        if session.is_null() {
230            return Err(SessionCreationError::Unknown);
231        }
232        // SAFETY: Session was just checked for validity.
233        Ok(CoapClientSession {
234            inner: unsafe { CoapClientSessionInner::new(session) },
235        })
236    }
237
238    /// Restores a [CoapClientSession] from its raw counterpart.
239    ///
240    /// Note that it is not possible to statically infer the lifetime of the created session from
241    /// the raw pointer, i.e., the session will be created with an arbitrary lifetime.
242    /// Therefore, callers of this function should ensure that the created session instance does not
243    /// outlive the context it is bound to.
244    /// Failing to do so will result in a panic/abort in the context destructor as it is unable to
245    /// claim exclusive ownership of the client session.
246    ///
247    /// # Panics
248    ///
249    /// Panics if the given pointer is a null pointer or the raw session is not a client-side
250    /// session with app data.
251    ///
252    /// # Safety
253    /// The provided pointer must be valid, the provided session's app data must be a valid argument
254    /// to [`CoapFfiRawCell<CoapClientSessionInner>::clone_raw_rc`].
255    pub(crate) unsafe fn from_raw<'a>(raw_session: *mut coap_session_t) -> CoapClientSession<'a> {
256        assert!(!raw_session.is_null(), "provided raw session was null");
257        let raw_session_type = coap_session_get_type(raw_session);
258        // Variant names are named by bindgen, we have no influence on this.
259        // Ref: https://github.com/rust-lang/rust/issues/39371
260        #[allow(non_upper_case_globals)]
261        match raw_session_type {
262            coap_session_type_t_COAP_SESSION_TYPE_NONE => panic!("provided session has no type"),
263            coap_session_type_t_COAP_SESSION_TYPE_CLIENT => {
264                let raw_app_data_ptr = coap_session_get_app_data(raw_session);
265                assert!(!raw_app_data_ptr.is_null(), "provided raw session has no app data");
266                let inner = CoapFfiRcCell::clone_raw_rc(raw_app_data_ptr);
267                CoapClientSession { inner }
268            },
269            coap_session_type_t_COAP_SESSION_TYPE_SERVER | coap_session_type_t_COAP_SESSION_TYPE_HELLO => {
270                panic!("attempted to create CoapClientSession from raw server session")
271            },
272            _ => unreachable!("unknown session type"),
273        }
274    }
275}
276
277impl DropInnerExclusively for CoapClientSession<'_> {
278    fn drop_exclusively(self) {
279        self.inner.drop_exclusively();
280    }
281}
282
283impl Drop for CoapClientSessionInner<'_> {
284    fn drop(&mut self) {
285        // SAFETY:
286        // - raw_session is always valid as long as we are not dropped yet (as this is the only
287        //   function that calls coap_session_release on client-side sessions).
288        // - Application data validity is asserted.
289        // - For event handling access, see later comment.
290        unsafe {
291            let app_data = coap_session_get_app_data(self.inner.raw_session);
292            assert!(!app_data.is_null());
293            // Recreate weak pointer instance so that it can be dropped (which in turn reduces the
294            // weak reference count, avoiding memory leaks).
295            CoapFfiRcCell::<CoapClientSessionInner>::raw_ptr_to_weak(app_data);
296            // We need to temporarily disable event handling so that our own event handler does not
297            // access this already partially invalid session (and recursively also calls this Drop
298            // implementation), causing a SIGABRT.
299            // This is fine, because:
300            // - While this destructor is called, nothing is concurrently accessing the raw context
301            //   (as libcoap is single-threaded and all types are !Send)
302            // - The only way this could be problematic would be if libcoap assumed sessions to be
303            //   unchanging during a call to coap_io_process. However, this would be considered a
304            //   bug in libcoap (as the documentation does not explicitly forbid this AFAIK).
305            let raw_context = coap_session_get_context(self.inner.raw_session);
306            assert!(!raw_context.is_null());
307            coap_register_event_handler(raw_context, None);
308            // Let libcoap do its cleanup of the raw session and free the associated memory.
309            coap_session_release(self.inner.raw_session);
310            // Restore event handler.
311            coap_register_event_handler(raw_context, Some(event_handler_callback));
312        }
313    }
314}
315
316impl<'a> CoapSessionInnerProvider<'a> for CoapClientSession<'a> {
317    fn inner_ref<'b>(&'b self) -> Ref<'b, CoapSessionInner<'a>> {
318        Ref::map(self.inner.borrow(), |v| &v.inner)
319    }
320
321    fn inner_mut<'b>(&'b self) -> RefMut<'b, CoapSessionInner<'a>> {
322        RefMut::map(self.inner.borrow_mut(), |v| &mut v.inner)
323    }
324}
325
326impl<'a, T: CoapSessionCommon<'a>> PartialEq<T> for CoapClientSession<'_> {
327    fn eq(&self, other: &T) -> bool {
328        // SAFETY: Pointers are only compared, never accessed.
329        self.if_index() == other.if_index()
330            && unsafe { self.raw_session() == other.raw_session() }
331            && self.addr_local() == other.addr_local()
332            && self.addr_remote() == other.addr_remote()
333    }
334}
335
336impl Eq for CoapClientSession<'_> {}