From 0e179416db544e99f86bbbbdd4fc9d592124f02b Mon Sep 17 00:00:00 2001 From: Petar Dimitrijevic Date: Tue, 4 Jul 2023 00:24:50 +0200 Subject: [PATCH] feat(transport): Implement support for CoAP over tcp (#12) * Implement support for CoAP over tcp Implement the missing structures. Add tests. * Tests for CoAP over TCP. * Fix the license comment to point to the tcp module. * Update comments Fix copy/paste comments. Everything should reffer to the TCP functionality. --- libcoap/src/context.rs | 18 ++++-- libcoap/src/event.rs | 9 ++- libcoap/src/session/client.rs | 35 +++++++++++ libcoap/src/transport/mod.rs | 16 +++++ libcoap/src/transport/tcp.rs | 81 +++++++++++++++++++++++-- libcoap/tests/tcp_client_server_test.rs | 41 +++++++++++++ 6 files changed, 189 insertions(+), 11 deletions(-) create mode 100644 libcoap/tests/tcp_client_server_test.rs diff --git a/libcoap/src/context.rs b/libcoap/src/context.rs index d54b96ac..2cd72a23 100644 --- a/libcoap/src/context.rs +++ b/libcoap/src/context.rs @@ -41,7 +41,7 @@ use crate::{ error::{ContextCreationError, EndpointCreationError, IoProcessError}, resource::{CoapResource, UntypedCoapResource}, session::session_response_handler, - transport::{CoapEndpoint, CoapUdpEndpoint}, + transport::{CoapEndpoint, CoapUdpEndpoint, CoapTcpEndpoint}, }; #[derive(Debug)] @@ -239,10 +239,16 @@ impl CoapContext<'_> { Ok(()) } - /// TODO + /// Creates a new TCP endpoint that is bound to the given address. #[cfg(feature = "tcp")] - pub fn add_endpoint_tcp(&mut self, _addr: SocketAddr) -> Result<(), EndpointCreationError> { - todo!() + pub fn add_endpoint_tcp(&mut self, addr: SocketAddr) -> Result<(), EndpointCreationError> { + // SAFETY: Because we never return an owned reference to the endpoint, it cannot outlive the + // context it is bound to (i.e. this one). + let endpoint = unsafe { CoapTcpEndpoint::new(self, addr)? }.into(); + let mut inner_ref = self.inner.borrow_mut(); + inner_ref.endpoints.push(endpoint); + // Cannot fail, we just pushed to the Vec. + Ok(()) } /// Creates a new DTLS endpoint that is bound to the given address. @@ -607,7 +613,7 @@ impl CoapContext<'_> { /// (will cause an abort on drop) /// - Calling `coap_free_context()` on this context (for obvious reasons, this will probably /// cause a segfault if you don't immediately [std::mem::forget()] the CoapContext and never - /// use anything related to the context again, but why would you do that?) + /// use anything related to the context again, but why would you do that?) // Kept here for consistency, even though it is unused. #[allow(unused)] pub(crate) unsafe fn as_raw_context(&self) -> &coap_context_t { @@ -630,7 +636,7 @@ impl CoapContext<'_> { /// (will cause an abort on drop) /// - Calling `coap_free_context()` on this context (for obvious reasons, this will probably /// cause a segfault if you don't immediately [std::mem::forget()] the CoapContext and never - /// use anything related to the context again, but why would you do that?) + /// use anything related to the context again, but why would you do that?) pub(crate) unsafe fn as_mut_raw_context(&mut self) -> &mut coap_context_t { // SAFETY: raw_context is checked to be a valid pointer on struct instantiation, cannot be // freed by anything outside of here (assuming the contract of this function is kept), and diff --git a/libcoap/src/event.rs b/libcoap/src/event.rs index 5ea9d6d4..7b808806 100644 --- a/libcoap/src/event.rs +++ b/libcoap/src/event.rs @@ -12,6 +12,7 @@ use std::fmt::Debug; use libcoap_sys::{coap_event_t, coap_session_get_context, coap_session_t}; +use libcoap_sys::{coap_session_get_type, coap_session_type_t}; use crate::context::CoapContext; use crate::session::CoapSession; @@ -110,11 +111,17 @@ pub trait CoapEventHandler: Debug { // This should be fine as we don't provide this type to a FFI function, we only read from it. #[allow(improper_ctypes_definitions)] pub(crate) unsafe extern "C" fn event_handler_callback(raw_session: *mut coap_session_t, event: coap_event_t) -> i32 { - let session: CoapSession = if event == coap_event_t::COAP_EVENT_SERVER_SESSION_NEW { + let raw_session_type = coap_session_get_type(raw_session); + + let session: CoapSession = if event == coap_event_t::COAP_EVENT_SERVER_SESSION_NEW + || (event == coap_event_t::COAP_EVENT_TCP_CONNECTED + && raw_session_type == coap_session_type_t::COAP_SESSION_TYPE_SERVER) + { CoapServerSession::initialize_raw(raw_session).into() } else { CoapSession::from_raw(raw_session) }; + // SAFETY: Pointer is always valid as long as there is no bug in libcoap. let context = CoapContext::from_raw(coap_session_get_context(raw_session)); context.handle_event(session, event); diff --git a/libcoap/src/session/client.rs b/libcoap/src/session/client.rs index 2d023c62..d043ace5 100644 --- a/libcoap/src/session/client.rs +++ b/libcoap/src/session/client.rs @@ -146,6 +146,41 @@ impl CoapClientSession<'_> { }) } + + /// Create a new unencrypted session with the given peer over TCP. + /// + /// # Errors + /// Will return a [SessionCreationError] if libcoap was unable to create a session (most likely + /// because it was not possible to bind to a port). + pub fn connect_tcp<'a>( + ctx: &mut CoapContext<'a>, + addr: SocketAddr, + ) -> Result, SessionCreationError> { + // SAFETY: self.raw_context is guaranteed to be valid, local_if can be null. + let session = unsafe { + coap_new_client_session( + ctx.as_mut_raw_context(), + std::ptr::null(), + CoapAddress::from(addr).as_raw_address(), + coap_proto_t::COAP_PROTO_TCP, + ) + }; + if session.is_null() { + return Err(SessionCreationError::Unknown); + } + // SAFETY: Session was just checked for validity, no crypto info was provided to + // coap_new_client_session(). + Ok(unsafe { + CoapClientSession::new( + session as *mut coap_session_t, + #[cfg(feature = "dtls")] + None, + #[cfg(feature = "dtls")] + None, + ) + }) + } + /// Initializes a new CoapClientSession from its raw counterpart with the provided initial /// information. /// diff --git a/libcoap/src/transport/mod.rs b/libcoap/src/transport/mod.rs index 90d5ffc0..128fedcd 100644 --- a/libcoap/src/transport/mod.rs +++ b/libcoap/src/transport/mod.rs @@ -15,6 +15,9 @@ use libcoap_sys::{coap_endpoint_set_default_mtu, coap_endpoint_t}; pub use dtls::CoapDtlsEndpoint; pub use udp::CoapUdpEndpoint; +#[cfg(feature = "tcp")] +pub use tcp::CoapTcpEndpoint; + #[cfg(feature = "dtls")] mod dtls; #[cfg(feature = "tcp")] @@ -63,6 +66,8 @@ pub trait EndpointCommon { #[derive(Debug)] pub enum CoapEndpoint { Udp(CoapUdpEndpoint), + #[cfg(feature = "tcp")] + Tcp(CoapTcpEndpoint), #[cfg(feature = "dtls")] Dtls(CoapDtlsEndpoint), } @@ -73,6 +78,13 @@ impl From for CoapEndpoint { } } +#[cfg(feature = "tcp")] +impl From for CoapEndpoint { + fn from(ep: CoapTcpEndpoint) -> Self { + CoapEndpoint::Tcp(ep) + } +} + #[cfg(feature = "dtls")] impl From for CoapEndpoint { fn from(ep: CoapDtlsEndpoint) -> Self { @@ -84,6 +96,8 @@ impl EndpointCommon for CoapEndpoint { unsafe fn as_raw_endpoint(&self) -> &coap_endpoint_t { match self { CoapEndpoint::Udp(ep) => ep.as_raw_endpoint(), + #[cfg(feature = "tcp")] + CoapEndpoint::Tcp(ep) => ep.as_raw_endpoint(), #[cfg(feature = "dtls")] CoapEndpoint::Dtls(ep) => ep.as_raw_endpoint(), } @@ -92,6 +106,8 @@ impl EndpointCommon for CoapEndpoint { unsafe fn as_mut_raw_endpoint(&mut self) -> &mut coap_endpoint_t { match self { CoapEndpoint::Udp(ep) => ep.as_mut_raw_endpoint(), + #[cfg(feature = "tcp")] + CoapEndpoint::Tcp(ep) => ep.as_mut_raw_endpoint(), #[cfg(feature = "dtls")] CoapEndpoint::Dtls(ep) => ep.as_mut_raw_endpoint(), } diff --git a/libcoap/src/transport/tcp.rs b/libcoap/src/transport/tcp.rs index 1a41cc2d..ffda6d57 100644 --- a/libcoap/src/transport/tcp.rs +++ b/libcoap/src/transport/tcp.rs @@ -1,13 +1,86 @@ // SPDX-License-Identifier: BSD-2-Clause /* - * transport/dtls.rs - transport-specific code for TCP. + * transport/tcp.rs - transport-specific code for TCP. * This file is part of the libcoap-rs crate, see the README and LICENSE files for * more information and terms of use. * Copyright © 2021-2023 The NAMIB Project Developers, all rights reserved. * See the README as well as the LICENSE file for more information. */ -/// TODO -#[allow(dead_code)] +use libcoap_sys::{coap_endpoint_t, coap_free_endpoint, coap_new_endpoint, coap_proto_t::COAP_PROTO_TCP}; + +use crate::{context::CoapContext, error::EndpointCreationError, transport::EndpointCommon, types::CoapAddress}; + +use std::net::SocketAddr; + #[cfg(feature = "tcp")] -pub struct CoapTcpEndpoint {} +#[derive(Debug)] +pub struct CoapTcpEndpoint { + raw_endpoint: *mut coap_endpoint_t, +} + +impl CoapTcpEndpoint { + /// Creates a new CoapTcpEndpoint and binds it to the supplied SocketAddr. + /// + /// This is an unsafe function (see #Safety for an explanation of why) used internally by + /// libcoap-rs to instantiate new endpoints. You should most likely not use this function, and + /// use one of the following alternatives instead: + /// - If you just want to add an endpoint to the coap context, use [CoapContext::add_endpoint_tcp()]. + /// - If you need to modify the underlying [coap_endpoint_t] directly (in an unsafe manner), use + /// [CoapContext::add_endpoint_tcp()] to instantiate the endpoint and then [as_mut_raw_endpoint()] + /// to access the underlying struct. + /// + /// # Safety + /// All endpoint types defined in this crate contain a [coap_endpoint_t] instance, + /// which is the representation of endpoints used by the underlying libcoap C library. + /// + /// On instantiation, these [coap_endpoint_t] instances are bound to a context, which includes + /// adding them to a list maintained by the [CoapContext] (or – to be more specific – the + /// underlying [libcoap_sys::coap_context_t]. + /// + /// When the context that this endpoint is bound to is dropped, the context calls [libcoap_sys::coap_free_context()], + /// which will not only free the context, but also all [coap_endpoint_t] instances associated + /// with it, including the one this struct points to. + /// + /// Therefore, if you decide to use this function anyway, you have to ensure that the + /// CoapContext lives at least as long as this struct does. + /// Also note that unlike [CoapContext::add_endpoint_tcp()], this function does not add the + /// endpoint to the [CoapContext::endpoints] vector, while the underlying [coap_endpoint_t] is + /// added to the underlying [libcoap_sys::coap_context_t] + pub(crate) unsafe fn new(context: &mut CoapContext, addr: SocketAddr) -> Result { + let endpoint = coap_new_endpoint( + context.as_mut_raw_context(), + CoapAddress::from(addr).as_raw_address(), + COAP_PROTO_TCP, + ); + if endpoint.is_null() { + return Err(EndpointCreationError::Unknown); + } + Ok(Self { raw_endpoint: endpoint }) + } +} + +impl EndpointCommon for CoapTcpEndpoint { + unsafe fn as_raw_endpoint(&self) -> &coap_endpoint_t { + // SAFETY: raw_endpoint is checked to be a valid pointer on struct instantiation, cannot be + // freed by anything outside of here (assuming the contract of this function is kept), and + // the default (elided) lifetimes are correct (the pointer is valid as long as the endpoint + // is). + &*self.raw_endpoint + } + + unsafe fn as_mut_raw_endpoint(&mut self) -> &mut coap_endpoint_t { + // SAFETY: raw_endpoint is checked to be a valid pointer on struct instantiation, is not + // freed by anything outside of here (assuming the contract of this function is kept), and + // the default (elided) lifetimes are correct (the pointer is valid as long as the endpoint + // is). + &mut *self.raw_endpoint + } +} + +impl Drop for CoapTcpEndpoint { + fn drop(&mut self) { + // SAFETY: Raw endpoint is guaranteed to exist for as long as the container exists. + unsafe { coap_free_endpoint(self.raw_endpoint) } + } +} diff --git a/libcoap/tests/tcp_client_server_test.rs b/libcoap/tests/tcp_client_server_test.rs new file mode 100644 index 00000000..258f65dd --- /dev/null +++ b/libcoap/tests/tcp_client_server_test.rs @@ -0,0 +1,41 @@ +// SPDX-License-Identifier: BSD-2-Clause +/* + * tcp_client_server_test.rs - Tests for TCP clients+servers. + * This file is part of the libcoap-rs crate, see the README and LICENSE files for + * more information and terms of use. + * Copyright © 2021-2023 The NAMIB Project Developers, all rights reserved. + * See the README as well as the LICENSE file for more information. + */ + +use libcoap_rs::session::CoapClientSession; +use libcoap_rs::{ + message::CoapMessageCommon, + protocol::{CoapMessageCode, CoapResponseCode}, + session::CoapSessionCommon, + CoapContext, +}; +use std::time::Duration; + +mod common; + +#[test] +pub fn basic_client_server_request() { + let server_address = common::get_unused_server_addr(); + + let server_handle = common::spawn_test_server(move |context| context.add_endpoint_tcp(server_address).unwrap()); + + let mut context = CoapContext::new().unwrap(); + let session = CoapClientSession::connect_tcp(&mut context, server_address).unwrap(); + + let request = common::gen_test_request(server_address); + let req_handle = session.send_request(request).unwrap(); + loop { + assert!(context.do_io(Some(Duration::from_secs(10))).expect("error during IO") <= Duration::from_secs(10)); + for response in session.poll_handle(&req_handle) { + assert_eq!(response.code(), CoapMessageCode::Response(CoapResponseCode::Content)); + assert_eq!(response.data().unwrap().as_ref(), "Hello World!".as_bytes()); + server_handle.join().unwrap(); + return; + } + } +}