diff --git a/examples/networking/coap/unicoap_message/main.c b/examples/networking/coap/unicoap_message/main.c index 63f49d5b68da..8aedad54449d 100644 --- a/examples/networking/coap/unicoap_message/main.c +++ b/examples/networking/coap/unicoap_message/main.c @@ -26,10 +26,10 @@ static void _example_handle_message(const unicoap_message_t* message) { - /* Use unicoap_message_is_request, unicoap_message_is_response, unicoap_message_is_signal - * to determine what class of message you are dealing with. + /* Use unicoap_message_code_is_request, unicoap_message_code_is_response, + * unicoap_message_code_is_signal to determine what class of message you are dealing with. * In this case, we expect a request. */ - assert(unicoap_message_is_request(message->code)); + assert(unicoap_message_code_is_request(message->code)); /* The unicoap message type supports different typed views of the CoAP message code: * message->method, message->status, and message->signal. diff --git a/examples/networking/coap/unicoap_server/Makefile b/examples/networking/coap/unicoap_server/Makefile new file mode 100644 index 000000000000..081c06e4895a --- /dev/null +++ b/examples/networking/coap/unicoap_server/Makefile @@ -0,0 +1,63 @@ +# Default Makefile, for simple unicoap server sample application + +# name of your application +APPLICATION = unicoap_server + +# If no BOARD is found in the environment, use this default: +BOARD ?= native + +# This has to be the absolute path to the RIOT base directory: +RIOTBASE ?= $(CURDIR)/../../../.. + +# Include packages that pull up and auto-init the link layer. +# NOTE: 6LoWPAN will be included if IEEE802.15.4 devices are present +USEMODULE += netdev_default + +# use GNRC by default +LWIP_IPV4 ?= 0 +LWIP_IPV6 ?= 0 + +ifeq (,$(filter 1, $(LWIP_IPV4) $(LWIP_IPV6))) + USEMODULE += auto_init_gnrc_netif + # Specify the mandatory networking modules + USEMODULE += gnrc_ipv6_default + # Additional networking modules that can be dropped if not needed + USEMODULE += gnrc_icmpv6_echo +else + ifeq (1,$(LWIP_IPV4)) + USEMODULE += ipv4_addr + + USEMODULE += lwip_arp + USEMODULE += lwip_ipv4 + USEMODULE += lwip_dhcp_auto + CFLAGS += -DETHARP_SUPPORT_STATIC_ENTRIES=1 + endif + + ifeq (1,$(LWIP_IPV6)) + USEMODULE += ipv6_addr + + USEMODULE += lwip_ipv6 + USEMODULE += lwip_ipv6_autoconfig + endif +endif + +USEMODULE += unicoap +USEMODULE += unicoap_resources_xfa + +# This module is needed for CoAP over UDP +USEMODULE += unicoap_driver_udp + +# This module is needed for CoAP over DTLS +USEMODULE += unicoap_driver_dtls + +# It is okay to import only either of the CoAP over UDP and CoAP over DTLS drivers. + +# Comment this out to disable code in RIOT that does safety checking +# which is not needed in a production environment but helps in the +# development process: +DEVELHELP ?= 1 + +# Change this to 0 show compiler invocation lines by default: +QUIET ?= 1 + +include $(RIOTBASE)/Makefile.include diff --git a/examples/networking/coap/unicoap_server/Makefile.ci b/examples/networking/coap/unicoap_server/Makefile.ci new file mode 100644 index 000000000000..d3841a237454 --- /dev/null +++ b/examples/networking/coap/unicoap_server/Makefile.ci @@ -0,0 +1,58 @@ +BOARD_INSUFFICIENT_MEMORY := \ + airfy-beacon \ + arduino-duemilanove \ + arduino-leonardo \ + arduino-mega2560 \ + arduino-nano \ + arduino-uno \ + atmega1284p \ + atmega328p \ + atmega328p-xplained-mini \ + atmega8 \ + atxmega-a3bu-xplained \ + blackpill-stm32f103c8 \ + bluepill-stm32f030c8 \ + bluepill-stm32f103c8 \ + calliope-mini \ + derfmega128 \ + hifive1 \ + hifive1b \ + i-nucleo-lrwan1 \ + im880b \ + mega-xplained \ + microbit \ + microduino-corerf \ + msb-430 \ + msb-430h \ + nrf51dongle \ + nucleo-c031c6 \ + nucleo-f030r8 \ + nucleo-f031k6 \ + nucleo-f042k6 \ + nucleo-f070rb \ + nucleo-f072rb \ + nucleo-f302r8 \ + nucleo-f303k8 \ + nucleo-f334r8 \ + nucleo-l011k4 \ + nucleo-l031k6 \ + nucleo-l053r8 \ + olimex-msp430-h1611 \ + olimex-msp430-h2618 \ + samd10-xmini \ + saml10-xpro \ + saml11-xpro \ + slstk3400a \ + stk3200 \ + stm32f030f4-demo \ + stm32f0discovery \ + stm32f7508-dk \ + stm32g0316-disco \ + stm32l0538-disco \ + stm32mp157c-dk2 \ + telosb \ + weact-g030f6 \ + yunjia-nrf51822 \ + z1 \ + zigduino \ + # diff --git a/examples/networking/coap/unicoap_server/README.md b/examples/networking/coap/unicoap_server/README.md new file mode 100644 index 000000000000..11c16c645215 --- /dev/null +++ b/examples/networking/coap/unicoap_server/README.md @@ -0,0 +1,31 @@ +# Sample Server Application With `unicoap` + +This a sample application demonstrating how you can define CoAP resources using `unicoap`, +the unified and modular CoAP suite in RIOT. Visit the +[`unicoap` server tutorial on doc.riot-os.org](http://doc.riot-os.org/group__net__unicoap__server__tutorial.html) +for a detailed code-level instructions. + +## Running the Example on RIOT's Native Board + +Create a tap interface (to which RIOT will connect): + +``` +$ sudo ip tuntap add tap0 mode tap user ${USER} +$ sudo ip link set tap0 up +``` + +To try this example on your host, run: +```sh +BOARD=native make flash term +``` +This will compile and run the application. +The application will print a network-layer address. + +In a second terminal session, you can run + +```sh +python3 client.py -m GET -u "coap://[fe80::c0:ff:ee%tap0]/greeting?name=RIOTer" +``` + +to send a CoAP request through the tap interface to the CoAP server where `fe80::c0:ff:ee` +is the link-layer address printed by the application. diff --git a/examples/networking/coap/unicoap_server/app.config b/examples/networking/coap/unicoap_server/app.config new file mode 100644 index 000000000000..f8077850c15f --- /dev/null +++ b/examples/networking/coap/unicoap_server/app.config @@ -0,0 +1,3 @@ +CONFIG_UNICOAP_DEBUG_LOGGING=n +CONFIG_UNICOAP_ASSIST=n +CONFIG_UNICOAP_CREATE_THREAD=y diff --git a/examples/networking/coap/unicoap_server/client.py b/examples/networking/coap/unicoap_server/client.py new file mode 100644 index 000000000000..40170aec1351 --- /dev/null +++ b/examples/networking/coap/unicoap_server/client.py @@ -0,0 +1,140 @@ +#!/usr/bin/env python3 + +import logging +import asyncio +from argparse import ArgumentParser + +from aiocoap import CON, NON, GET, PUT, POST, DELETE, PATCH, iPATCH, FETCH, Context, Message +import aiocoap.resource as resource +from aiocoap.transports.tinydtls import DTLSClientConnection + +logging.basicConfig(level=logging.DEBUG) + +print( + "usage: client.py" + + " -m " + + " -u " + + " [--type ] [--observe] [-p ]") + + +def _get(_list, i, default): + return _list[i] if len(_list) > i else default + + +def message_type(arg): + return {"CON": CON, "NON": NON}[arg] + + +def method(arg): + return { + "GET": GET, + "PUT": PUT, + "POST": POST, + "DELETE": DELETE, + "PATCH": PATCH, + "iPATCH": iPATCH, + "FETCH": FETCH + }[arg] + + +async def main(): + parser = ArgumentParser() + + parser.add_argument( + "-m", "--method", + help='GET|PUT|POST|DELETE|PATCH|iPATCH|FETCH', + required=True) + + parser.add_argument( + "-u", "--uri", + help='URI', + required=True) + + parser.add_argument( + "-mt", "--type", + help='NON|CON', + default="NON") + + parser.add_argument( + "--observe", + action="store_true", + help='Register for notifications', + default=False) + + parser.add_argument( + "--observe-cancel", + action="store_true", + help='Cancel notifications', + default=False) + + parser.add_argument( + "-p", "--payload", + help='Payload', + default=None) + + parser.add_argument( + "-to", "--timeout", + type=float, + help='Request timeout', + default="4") + + args = parser.parse_args() + + observeValue = None + + if args.observe and args.observe_cancel: + raise ValueError("cannot register for and cancel notifications") + + if args.observe: + observeValue = 0 + + elif args.observe_cancel: + observeValue = 1 + + print(f"using {message_type(args.type)} {method(args.method)} request") + print(f"timeout set to {args.timeout}s") + + port = 5600 + protocol = await Context.create_server_context(bind=("::", port), site=resource.Site()) + protocol.client_credentials.load_from_dict({ + '*': { + 'dtls': { + 'psk': b'secretPSK', + 'client-identity': b'Client_identity', + } + } + }) + + request = Message( + mtype=message_type(args.type), + code=method(args.method), + uri=args.uri, + payload=bytes(args.payload, 'utf-8') if args.payload else "", + observe=observeValue + ) + + try: + pr = protocol.request(request) + + async with asyncio.timeout(args.timeout): + r = await pr.response + print("response: %s\n%r" % (r.code, r.payload)) + + if args.observe: + print("waiting for resource notifications") + + async for r in pr.observation: + print("notification: %s\n%r" % (r, r.payload)) + break + + await protocol.shutdown() + + except TimeoutError: + print(f"error: timeout exceeded after waiting {args.timeout}s") + + except Exception as e: + print("error:") + print(e) + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/examples/networking/coap/unicoap_server/main.c b/examples/networking/coap/unicoap_server/main.c new file mode 100644 index 000000000000..96c5447dd24f --- /dev/null +++ b/examples/networking/coap/unicoap_server/main.c @@ -0,0 +1,302 @@ +/* + * Copyright (C) 2024-2025 Carl Seifert + * Copyright (C) 2024-2025 TU Dresden + * + * This file is subject to the terms and conditions of the GNU Lesser General + * Public License v2.1. See the file LICENSE in the top level directory for + * more details. + */ + +/** + * @file + * @ingroup examples + * @brief Sample `unicoap` server application + * @author Carl Seifert + */ + +#include "net/unicoap.h" + +/* This is needed for netifs_print_ipv6 in main below. */ +#include "net/netif.h" + +/* If you need CoAP over DTLS support, you need to include extra dependencies. What's more, + * you'll also need to load a DTLS credential for message encryption and verification. + * The `IS_USED(MODULE_UNICOAP_DRIVER_DTLS)` below checks if the @ref net_unicoap_drivers_dtls + * has been imported via the USEMODULE variable in the application Makefile. */ +#if IS_USED(MODULE_UNICOAP_DRIVER_DTLS) +# include "net/sock/dtls/creds.h" +# include "net/credman.h" +# include "net/dsm.h" +# include "unicoap_example_dtls.h" + +/* Example credential tag for credman. Tag together with the credential type needs to be unique. */ +# define EXAMPLE_DTLS_CREDENTIAL_TAG 42 /* This should answer your question. */ + +static const uint8_t psk_id_0[] = PSK_DEFAULT_IDENTITY; +static const uint8_t psk_key_0[] = PSK_DEFAULT_KEY; +static const credman_credential_t credential = { + .type = CREDMAN_TYPE_PSK, + .tag = EXAMPLE_DTLS_CREDENTIAL_TAG, + .params = { + .psk = { + .key = { .s = psk_key_0, .len = sizeof(psk_key_0) - 1, }, + .id = { .s = psk_id_0, .len = sizeof(psk_id_0) - 1, }, + } + }, +}; +#endif /* IS_USED(MODULE_UNICOAP_DRIVER_DTLS) */ + +static int handle_hello_request(unicoap_message_t* message, const unicoap_aux_t* aux, + unicoap_request_context_t* ctx, void* arg) { + (void)aux; + (void)arg; + + printf("app: %s /, %" PRIuSIZE " bytes\n", + unicoap_string_from_method(message->method), + unicoap_message_payload_get_size(message)); + + /* String literals in C are null-terminated. For null-terminated string, we can use the + * _string message initializers. */ + unicoap_response_init_string(message, UNICOAP_STATUS_CONTENT, "Hello, World!"); + + /* Respond. Note that you do not need to return the result of @ref unicoap_send_response. */ + return unicoap_send_response(message, ctx); +} + +/* + * Let's define a CoAP resource. To define a new resource statically (at compile-time), you + * use the @ref UNICOAP_RESOURCE macro. Note that you will need to supply an identifier. Let's + * call it `hello`. The identifier is required by the [implementation](@ref UNICOAP_RESOURCE), + * but not used otherwise. You can give it any name you want, as long as it is a unique C + * variable name. + */ + +UNICOAP_RESOURCE(hello) { + /* Each resource must be assigned a path. This will be the part that follows the host/domain + * and port in the CoAP URI. */ + .path = "/", + + /* Since we are using CoAP over UDP (or CoAP over DTLS, which relies on UDP for that matter), + * we can instruct `unicoap` to send confirmable responses. This is optional. For other + * transports, @ref UNICOAP_RESOURCE_FLAG_RELIABLE does not have an effect. + * In this case, we don't want to loose our precious greeting along the way. + * To send confirmable messages (CON) over UDP or DTLS, we pass the + * @ref UNICOAP_RESOURCE_FLAG_RELIABLE flag. */ + .flags = UNICOAP_RESOURCE_FLAG_RELIABLE, + + /* You must declare what CoAP methods you want to allow for each resource. */ + .methods = UNICOAP_METHODS(UNICOAP_METHOD_GET, UNICOAP_METHOD_PUT, UNICOAP_METHOD_POST), + + /* Optionally, you can also restrict the resource to a set of transports. */ + .protocols = UNICOAP_PROTOCOLS(UNICOAP_PROTO_DTLS, UNICOAP_PROTO_UDP), + + /* Finally, pass the handler and an optional argument. */ + .handler = handle_hello_request, + .handler_arg = NULL +}; + +/* Next, we create a more complex resource. */ + +static bool is_valid_name(const char* string, size_t length) { + for (size_t i = 0; i < length; i += 1) { + if (string[i] == 0) { + return false; + } + } + return true; +} + +static int handle_greeting_request(unicoap_message_t* message, const unicoap_aux_t* aux, + unicoap_request_context_t* ctx, void* arg) { + (void)aux; + (void)arg; + + printf("app: %s /greeting, %" PRIuSIZE " bytes\n", + unicoap_string_from_method(message->method), + unicoap_message_payload_get_size(message)); + + /* Our /greeting resource accepts a 'name' query parameter. + * If you GET /greeting?name=RIOTeer, we want to respond with "Hello, RIOTeer!". + * Besides, this URI is transmitted as two CoAP options: + * - `Uri-Path`: "greeting" + * - `Uri-Query`: "name=RIOTeer" + */ + + ssize_t res = 0; + const char* name = NULL; + + if ((res = unicoap_options_get_first_uri_query_by_name(message->options, "name", &name)) < 0) { + printf("error: could not get 'name' query: %" PRIdSIZE " (%s)\n", res, strerror(-res)); + /* Returning a status code here will result in a response being sent automatically. */ + return UNICOAP_STATUS_BAD_REQUEST; + } + + /* Validate any input. Here, we apply a 30 UTF-8 character limit. */ + if (res > 30 || !is_valid_name(name, res)) { + unicoap_response_init_string(message, UNICOAP_STATUS_BAD_REQUEST, "invalid 'name' query"); + return unicoap_send_response(message, ctx); + } + + /* Now we can craft out response. Let's start with the part that can fail. */ + + /* Allocate options on the stack. Provide the size of the buffer that will contain the + * response options. */ + UNICOAP_OPTIONS_ALLOC(options, 2); + + /* Set Content-Format option to text/plain */ + if (unicoap_options_set_content_format(&options, UNICOAP_FORMAT_TEXT) < 0) { + return UNICOAP_STATUS_INTERNAL_SERVER_ERROR; + } + + message->options = &options; + + /* Now, we create our payload. In theory, we could write "Hello, ", then the name, and then + * "! ..." into that buffer. Instead, we can use a vectored payload. */ + +#define static_strlen(string) sizeof(string) - 1 + + /* These are going to be the first and last payload chunks. */ +#define PREFIX "Hello, " + iolist_t list = { + .iol_base = PREFIX, + .iol_len = static_strlen(PREFIX), + }; + +#define SUFFIX "! Welcome to our itsy bitsy tiny CoAP server!" + iolist_t suffix = { + .iol_base = SUFFIX, + .iol_len = static_strlen(SUFFIX) + }; + + iolist_t name_chunk = { + .iol_next = &suffix, + .iol_base = (void*)name, + .iol_len = res + }; + list.iol_next = &name_chunk; + + unicoap_message_payload_set_chunks(message, &list); + unicoap_response_set_status(message, UNICOAP_STATUS_CONTENT); + + /* Respond. Note that you do not need to return the result of @ref unicoap_send_response. */ + return unicoap_send_response(message, ctx); +} + +UNICOAP_RESOURCE(greeting) { + .path = "/greeting", + + .flags = UNICOAP_RESOURCE_FLAG_RELIABLE, + + .methods = UNICOAP_METHODS(UNICOAP_METHOD_GET), + + .handler = handle_greeting_request, + .handler_arg = NULL +}; + +int main(void) { + /* By default, unicoap_init() is automatically called for you before main(). + * This is because auto_init_unicoap is part of the DEFAULT_MODULE makefile variable. + * You can opt out of this default behavior by setting DISABLE_MODULE += auto_init_unicoap. + * However, then you need to call unicoap_init() yourself. */ +#if !IS_USED(MODULE_AUTO_INIT_UNICOAP) + if (unicoap_init() < 0) { + printf("app: failed to initialize unicoap\n"); + } +#endif +#if IS_USED(MODULE_UNICOAP_DRIVER_UDP) + /* You can access the underlying transport handle. In the case of UDP, + * this will be a UDP sock provided by the sock API. */ + sock_udp_t* udp_socket = unicoap_transport_udp_get_socket(); + assert(udp_socket); + + /* An endpoint abstracts the transport handle. */ + unicoap_endpoint_t udp_local = { + .proto = UNICOAP_PROTO_UDP, + }; + + /* We can set the endpoint's UDP handle to the actual one. */ + sock_udp_get_local(udp_socket, &udp_local.udp_ep); + + printf("app: listening at "); + /* Because we're in possession of an endpoint now, we can print + * a debug description of it. */ + unicoap_print_endpoint(&udp_local); + printf("\n"); + + /* You can also add another socket to listen and send from another socket. */ + sock_udp_t other_udp_socket = { 0 }; + unicoap_endpoint_t other_endpoint = { + .proto = UNICOAP_PROTO_UDP, + .udp_ep = { +#if defined(SOCK_HAS_IPV6) + .family = AF_INET6, +#elif defined(SOCK_HAS_IPV4) + .family = AF_INET, +#endif + .netif = SOCK_ADDR_ANY_NETIF, + .port = 5682 + } + }; + + if (unicoap_transport_udp_add_socket(&other_udp_socket, &other_endpoint.udp_ep) < 0) { + printf("app: failed to add 2nd socket\n"); + } else { + printf("app: also listening "); + /* Because we're in possession of an endpoint now, we can print + * a debug description of it. */ + unicoap_print_endpoint(&other_endpoint); + printf("\n"); + } +#endif + + /* If DTLS is enabled, the CoAP over DTLS driver needs to be told + * at least one DTLS credential. */ +#if IS_USED(MODULE_UNICOAP_DRIVER_DTLS) + /* credman is a utility that manages DTLS credentials. */ + int res = credman_add(&credential); + if (res < 0 && res != CREDMAN_EXIST) { + /* Here we ignore duplicate credentials. */ + printf("app: cannot add credential to system: %d\n", res); + return 1; + } + + /* Just like with UDP, we can access the DTLS transport handle. */ + sock_dtls_t* dtls_socket = unicoap_transport_dtls_get_socket(); + assert(dtls_socket); + + unicoap_endpoint_t dtls_local = { + .proto = UNICOAP_PROTO_DTLS, + }; + + sock_udp_get_local(sock_dtls_get_udp_sock(dtls_socket), &dtls_local.dtls_ep); + + printf("app: listening at "); + unicoap_print_endpoint(&dtls_local); + printf("\n"); + + /* Here we tell the DTLS socket to use the credential known by the specified + * tag. Internally, the socket will retrieve the corresponding credential through + * the credential manager. */ + if ((res = sock_dtls_add_credential(dtls_socket, EXAMPLE_DTLS_CREDENTIAL_TAG)) < 0) { + printf("app: cannot add credential to DTLS sock: %d\n", res); + return 1; + } + + printf("app: using credential: type=PSK id=%s key=%s\n", + (char*)credential.params.psk.id.s, + (char*)credential.params.psk.key.s); +#endif + + printf("app: interfaces have ipv6=[ "); + netifs_print_ipv6(", "); + printf(" ]\n"); + + /* By default, unicoap_init() will create a background thread. If you do not want that, + * set CONFIG_UNICOAP_CREATE_THREAD to 0 and run the processing loop on a thread of your choice. + * Note that this function is not available when CONFIG_UNICOAP_CREATE_THREAD is 1. Multiple + * unicoap instances are not allowed. Note too that you can add multiple ports instead. */ +#if !CONFIG_UNICOAP_CREATE_THREAD + printf("app: running unicoap loop on main thread\n"); + unicoap_loop_run(); +#endif +} diff --git a/examples/networking/coap/unicoap_server/unicoap_example_dtls.h b/examples/networking/coap/unicoap_server/unicoap_example_dtls.h new file mode 100644 index 000000000000..9194e4dba7aa --- /dev/null +++ b/examples/networking/coap/unicoap_server/unicoap_example_dtls.h @@ -0,0 +1,55 @@ +/* + * Copyright (C) 2018 Inria + * + * This file is subject to the terms and conditions of the GNU Lesser + * General Public License v2.1. See the file LICENSE in the top level + * directory for more details. + */ + +/** + * @file + * @ingroup examples + * @brief PSK and RPK keys for the unicoap_server example. + * @author Raul Fuentes + */ + +#pragma once + +#ifdef __cplusplus +extern "C" { +#endif + +/* + * Default keys examples for tinyDTLS (for RIOT, Linux and Contiki) + */ +#define PSK_DEFAULT_IDENTITY "Client_identity" +#define PSK_DEFAULT_KEY "secretPSK" +#define PSK_OPTIONS "i:k:" +#define PSK_ID_MAXLEN 32 +#define PSK_MAXLEN 32 + +#ifdef CONFIG_DTLS_ECC +static const unsigned char ecdsa_priv_key[] = { + 0x41, 0xC1, 0xCB, 0x6B, 0x51, 0x24, 0x7A, 0x14, + 0x43, 0x21, 0x43, 0x5B, 0x7A, 0x80, 0xE7, 0x14, + 0x89, 0x6A, 0x33, 0xBB, 0xAD, 0x72, 0x94, 0xCA, + 0x40, 0x14, 0x55, 0xA1, 0x94, 0xA9, 0x49, 0xFA +}; + +static const unsigned char ecdsa_pub_key_x[] = { + 0x36, 0xDF, 0xE2, 0xC6, 0xF9, 0xF2, 0xED, 0x29, + 0xDA, 0x0A, 0x9A, 0x8F, 0x62, 0x68, 0x4E, 0x91, + 0x63, 0x75, 0xBA, 0x10, 0x30, 0x0C, 0x28, 0xC5, + 0xE4, 0x7C, 0xFB, 0xF2, 0x5F, 0xA5, 0x8F, 0x52 +}; + +static const unsigned char ecdsa_pub_key_y[] = { + 0x71, 0xA0, 0xD4, 0xFC, 0xDE, 0x1A, 0xB8, 0x78, + 0x5A, 0x3C, 0x78, 0x69, 0x35, 0xA7, 0xCF, 0xAB, + 0xE9, 0x3F, 0x98, 0x72, 0x09, 0xDA, 0xED, 0x0B, + 0x4F, 0xAB, 0xC3, 0x6F, 0xC7, 0x72, 0xF8, 0x29 +}; +#endif /* CONFIG_DTLS_ECC */ +#ifdef __cplusplus +} +#endif diff --git a/makefiles/pseudomodules.inc.mk b/makefiles/pseudomodules.inc.mk index 3089c3ef03fd..814c149ac6e4 100644 --- a/makefiles/pseudomodules.inc.mk +++ b/makefiles/pseudomodules.inc.mk @@ -518,6 +518,12 @@ PSEUDOMODULES += unicoap_driver_rfc7252_common # Alias for unicoap_driver_rfc7252_common_pdu, and is hence a pseudomodule PSEUDOMODULES += unicoap_driver_rfc7252_pdu +# XFA support for CoAP resource definitions in unicoap +PSEUDOMODULES += unicoap_resources_xfa + +# Common sock dependencies of sock-based CoAP drivers in unicoap +PSEUDOMODULES += unicoap_sock_support + PSEUDOMODULES += usbus_urb PSEUDOMODULES += vdd_lc_filter_% ## @defgroup pseudomodule_vfs_auto_format vfs_auto_format diff --git a/sys/auto_init/auto_init.c b/sys/auto_init/auto_init.c index cf968da3caa8..b2bdb806ac01 100644 --- a/sys/auto_init/auto_init.c +++ b/sys/auto_init/auto_init.c @@ -164,6 +164,11 @@ AUTO_INIT(gcoap_init, extern void auto_init_nanocoap_server(void); AUTO_INIT(auto_init_nanocoap_server, AUTO_INIT_PRIO_MOD_NANOCOAP); #endif +#if IS_USED(MODULE_AUTO_INIT_UNICOAP) +extern void unicoap_init(void); +AUTO_INIT(unicoap_init, + AUTO_INIT_PRIO_MOD_UNICOAP); +#endif #if IS_USED(MODULE_DEVFS) extern void auto_init_devfs(void); AUTO_INIT(auto_init_devfs, diff --git a/sys/auto_init/include/auto_init_priorities.h b/sys/auto_init/include/auto_init_priorities.h index bd19b5720be6..62f0be142642 100644 --- a/sys/auto_init/include/auto_init_priorities.h +++ b/sys/auto_init/include/auto_init_priorities.h @@ -184,6 +184,12 @@ extern "C" { */ #define AUTO_INIT_PRIO_MOD_GCOAP 1240 #endif +#ifndef AUTO_INIT_PRIO_MOD_UNICOAP +/** + * @brief `unicoap` priority + */ +#define AUTO_INIT_PRIO_MOD_UNICOAP 1245 +#endif #ifndef AUTO_INIT_PRIO_MOD_DEVFS /** * @brief DEVFS priority diff --git a/sys/include/net/unicoap.h b/sys/include/net/unicoap.h index 3f63ebd229d7..d68ee80a8ef4 100644 --- a/sys/include/net/unicoap.h +++ b/sys/include/net/unicoap.h @@ -13,6 +13,8 @@ #include "net/unicoap/config.h" /* IWYU pragma: export */ #include "net/unicoap/message.h" /* IWYU pragma: export */ #include "net/unicoap/options.h" /* IWYU pragma: export */ +#include "net/unicoap/application.h" /* IWYU pragma: export */ +#include "net/unicoap/transport.h" /* IWYU pragma: export */ /** * @addtogroup net_unicoap @@ -25,6 +27,98 @@ * @author Carl Seifert */ +/* MARK: - Controlling the unicoap instance */ +/** + * @name Controlling the unicoap instance + * @{ + */ +/** + * @brief Initializes the unicoap stack + * + * If you disable the `auto_init_unicoap` you will need to call this function manually. + * Otherwise, and provided `auto_init` is used, unicoap will be initialized automatically. + * + * @returns unicoap thread PID + */ +kernel_pid_t unicoap_init(void); + +/** + * @brief Tears down the unicoap stack, closing the background thread. + * + * Provided @ref CONFIG_UNICOAP_CREATE_THREAD is enabled, this function will also zombify the + * thread created on initialization. + * + * @returns Zero on success, `-1` otherwise + */ +int unicoap_deinit(void); + +#if !defined(DOXYGEN) && !IS_ACTIVE(CONFIG_UNICOAP_CREATE_THREAD) +/* Internal thread function */ +void* _unicoap_loop_run(void* arg); +#endif + +#if defined(DOXYGEN) || !IS_ACTIVE(CONFIG_UNICOAP_CREATE_THREAD) +/** + * @brief Runs `unicoap` processing loop + * + * This function never returns, unless explicitly instructed using @ref unicoap_deinit. + * + * @warning You must not call this function when @ref CONFIG_UNICOAP_CREATE_THREAD is enabled. + * If @ref CONFIG_UNICOAP_CREATE_THREAD is enabled, this function is not defined. + * + * @returns Never + */ +static inline void unicoap_loop_run(void) +{ + _unicoap_loop_run(NULL); +} +#endif + +/** @brief A job that can be enqueued and executed by the `unicoap` message processing loop. */ +typedef struct { + /** + * @brief Event that is posted on internal queue + * @warning Do not read or write. + * @internal + */ + event_t super; +} unicoap_job_t; + +/** + * @brief Initializes a @ref unicoap_job_t. + * + * @param func A function that must be of type `void (unicoap_job_t* job)`. + * @returns Designated initializer for @ref unicoap_job_t + */ +#define UNICOAP_JOB(func) { \ + .super = { \ + .handler = _UNICOAP_TRY_TYPECHECK_JOB_FUNC(func) \ + } \ +} + +/** + * @brief Schedules @p event to be run in the internal processing loop + * at the next possible instance + * + * @param[in,out] job Job to run on `unicoap` message processing loop + * + * @returns Negative error number in case of failure or zero on success. + * + * This function facilitates running client requests when @ref CONFIG_UNICOAP_CREATE_THREAD is + * disabled. + * + * @remark You can start enqueuing jobs even before the `unicoap` processing loop has been started. + * The jobs will become eligible for execution once the loop starts running, which usually is when + * the `unicoap` thread has been created. If `CONFIG_UNICOAP_CREATE_THREAD` is disabled, + * this will be after you have called @ref unicoap_loop_run, which blocks. + * + * ``` + * static unicoap_job_t sample = UNICOAP_JOB(my_handler); + * ``` + */ +int unicoap_loop_enqueue(unicoap_job_t* job); +/** @} */ + #ifdef __cplusplus extern "C" { #endif diff --git a/sys/include/net/unicoap/application.h b/sys/include/net/unicoap/application.h new file mode 100644 index 000000000000..6d31ade85f39 --- /dev/null +++ b/sys/include/net/unicoap/application.h @@ -0,0 +1,659 @@ +/* + * Copyright (C) 2024-2025 Carl Seifert + * Copyright (C) 2024-2025 TU Dresden + * + * This file is subject to the terms and conditions of the GNU Lesser General + * Public License v2.1. See the file LICENSE in the top level directory for + * more details. + */ + +#pragma once + +#include +#include + +#include "xfa.h" +#include "ztimer.h" +#include "event.h" + +#include "net/unicoap/constants.h" +#include "net/unicoap/message.h" +#include "net/unicoap/transport.h" +#include "net/unicoap/util_macros.h" + +/** + * @file + * @brief High-level CoAP client and sever API + * @ingroup net_unicoap + * @author Carl Seifert + */ + +#ifdef __cplusplus +extern "C" { +#endif + +/* MARK: - Utilities */ +/** + * @name Utilities + * @{ + */ +/** + * @brief Auxiliary exchange information + */ +typedef struct { + /** + * @brief The remote CoAP endpoint in this exchange + */ + const unicoap_endpoint_t* remote; + + /** + * @brief The local CoAP endpoint in this exchange + */ + const unicoap_endpoint_t* local; + + /** + * @brief Message properties + */ + const unicoap_message_properties_t* properties; +} unicoap_aux_t; +/** @} */ + +/** + * @addtogroup net_unicoap_server + * @{ + */ +/* MARK: - Responding to requests */ +/** + * @name Responding to requests + * @{ + */ +#ifndef DOXYGEN +/* Forward declaration */ +struct unicoap_resource; +#endif + +/** + * @brief Resource typealias + */ +typedef struct unicoap_resource unicoap_resource_t; + +/** + * @brief Request context used to send a response to a given request + */ +typedef struct { + /** + * @brief Resource this request was directed to + */ + const unicoap_resource_t* resource; + + /** + * @brief Internal + * + * @warning This is an internal property. Do not access from your application! + * @private + */ + void* _packet; + + /** + * @brief Internal + * + * @warning This is an internal property. Do not access from your application! + * @private + */ + void* _memo; +} unicoap_request_context_t; + +/** + * @brief Resource request handler + * + * This handler is called whenever a request reaches the given resource. If you use a certain + * handler for more than one resource, you may want to read @ref unicoap_request_context_t.resource. + * + * @param[in] request Request, safe to mutate and send response with mutated message. + * @param[in] aux Auxiliary data associated with the request + * @param[in] ctx Request context, use to send response. + * @param[in] arg Argument specified in [resource definition](@ref unicoap_resource_t) + * + * If you do not call @ref unicoap_send_response and return a negative value from the handler, + * an `Internal Server Error` response will be sent. + * + * @return `0`, status code or errno. + * @retval UNICOAP_IGNORING_REQUEST iff you don't want to respond + * @retval unicoap_status_t for an otherwise empty response + * + */ +typedef int (*unicoap_request_handler_t)(unicoap_message_t* request, const unicoap_aux_t* aux, + unicoap_request_context_t* ctx, void* arg); + +/** + * @brief Determines if the client is interested in a response + * + * @param[in] options Request options + * @param status The CoAP status code you would respond with + * + * @retval `false` if a response with that status code must be sent + * @retval `true` if sending a response with that status code is not mandatory + */ +bool unicoap_response_is_optional(unicoap_options_t* options, unicoap_status_t status); + +/** + * @brief Error number indicating the resource handler will not respond + * + * Return this error number from your request handler if you have determined you don't need to + * send a response using @ref unicoap_response_is_optional. `unicoap` does not check whether you + * have checked with @ref unicoap_response_is_optional. + */ +#define UNICOAP_IGNORING_REQUEST (-2042) + +/** + * @brief Sends a response to a request identifier by the given @p context + * + * Consumes @p response . + * + * @warning You MUST NOT call this method after having deferred a response. + * + * @param[in,out] response Response message to send + * @param[in,out] context Request context to respond in + * + * This function's return value can be used as a return value in @ref unicoap_request_handler_t. + * + * @retval Zero on success. + * @retval Negative integer on error + */ +int unicoap_send_response(unicoap_message_t* response, unicoap_request_context_t* context); + +/** + * @brief Maps a given @p errno to a CoAP response status code + * + * @param _errno Error number such as `-ENOENT` + * + * @return Status code + */ +unicoap_status_t unicoap_response_status_from_errno(int _errno); +/** @} */ + +/* MARK: - Registering CoAP resources */ +/** + * @name Registering CoAP resources + * @{ + */ +/** + * @brief Flags for enabling advanced features in server exchanges + * + * Specify these flags when creating a @ref unicoap_resource_t to modify transmission, + * block-wise, or resource observation behavior. + */ +typedef enum { + /** + * @brief Sets the type of the message to confirmable (`CON`), + * if an unreliable transport is used. + * + * This flag is ignored with reliable transports. For unreliable transports, a message + * sent with this flag will require an acknowledgement to be sent from the CoAP + * peer. + * + */ + UNICOAP_RESOURCE_FLAG_RELIABLE = 0x0001, + + /** + * @brief Makes this resource match paths located in the subtree of the given path. + * + * Example: + * A request with path `/laniakea/milky-way/solar-system/pluto` + * would match a resource with this flag and path `/laniakea/milky-way`. + */ + UNICOAP_RESOURCE_FLAG_MATCH_SUBTREE = 0x4000, + + /* TODO: Advanced features */ +} unicoap_resource_flags_t; + +/** + * @brief Prints resource flags + * + * @param flags Resource flags + */ +void unicoap_print_resource_flags(unicoap_resource_flags_t flags); + +/** + * @brief Allowed protocols bit field + * + * @note A value of @ref UNICOAP_PROTOCOLS_ALLOW_ALL (`0`) indicates + * no protocol checking is to be performed + * + * @see @ref UNICOAP_PROTOCOL_FLAG + * @see @ref UNICOAP_PROTOCOLS + */ +typedef uint8_t unicoap_proto_set_t; + +/** + * @brief Prints protocols bitfield + * + * @param protocols Set of allowed transports + */ +void unicoap_print_protocols(unicoap_proto_set_t protocols); + +/** + * @brief Allowed methods bit field + * + * @note A value of @ref UNICOAP_PROTOCOLS_ALLOW_ALL (`0xff`) indicates + * all methods are allowed. + * + * @see @ref UNICOAP_METHOD_FLAG + * @see @ref UNICOAP_METHODS + */ +typedef uint8_t unicoap_method_set_t; + +/** + * @brief Prints methods bitfield + * + * @param methods Set of CoAP methods + */ +void unicoap_print_methods(unicoap_method_set_t methods); + +/** + * @brief A type representing a CoAP resource + * + * This structure models a CoAP resource that can handle requests with a specified set + * of methods and allowed protocols. Normally, each resource listens for requests matching + * the given path. If you want to match all paths with a certain path prefix, + * see @ref UNICOAP_RESOURCE_FLAG_MATCH_SUBTREE. + */ +struct unicoap_resource { + /** + * @brief Resource path + * + * Must be a null-terminated string with slash-separated path components + * + * @warning This path must not have any trailing slash separators, apart from the root path `/` + */ + const char* path; + + /** + * @brief Request handler callback + * + * Function that will be executed once a new request directed at this resource reaches the + * server. Will be called from the `unicoap` thread. + */ + unicoap_request_handler_t handler; + + /** + * @brief Opaque optional argument for the request handler (nullable) + */ + void* handler_arg; + + /** + * @brief Flags for modifying resource behavior + * + * @see @ref unicoap_resource_flags_t + */ + unicoap_resource_flags_t flags; + + /** + * @brief Allowed request methods for this resource + * + * @see @ref unicoap_method_set_t + */ + unicoap_method_set_t methods; + + /** + * @brief Allowed transport protocols this resource can be reached over + * + * Use this property to, e.g., limit requests to encrypted transport protocols + * + * @see @ref unicoap_proto_set_t + */ + unicoap_proto_set_t protocols; +}; +/** @} */ + +/** + * @addtogroup net_unicoap_resources_xfa + * @{ + */ +/* MARK: - Defining a cross-file resource */ +/** + * @name Defining a cross-file resource + * @{ + */ +#if IS_USED(MODULE_UNICOAP_RESOURCES_XFA) || defined(DOXYGEN) +/** + * @brief CoAP cross-file resource definition + * + * @param name internal name of the resource entry, must be unique + * + * You must supply a constant initializer following the macro invocation. + * The name you supply is needed for technical reasons but has otherwise no meaning. + */ +# define UNICOAP_RESOURCE(name) \ + XFA_CONST(unicoap_resource_t, unicoap_resources_xfa, 0) CONCAT(unicoap_resource_, name) = +#else +# define UNICOAP_RESOURCE(name) \ + static_assert(false, \ + "The unicoap_resources_xfa module is missing, resource cannot be registered"); \ + unicoap_resource_t CONCAT(unicoap_resource_, name) = +#endif +/** @} */ +/** @} */ + +/* MARK: - Resource discovery */ +/** + * @name Resource discovery + * @{ + */ +/** + * @brief Builds a string in Constrained RESTful Environments (CoRE) Link Format + * + * You use this method to build a stringified list of resources registered with `unicoap`. + * To register a resource, use @ref unicoap_listener_register or @ref net_unicoap_resources_xfa. + * The string generated contains only resources available using the specified transport. When + * handling a request destined for `/.well-known/core`, you should call this method with the + * transport you received the request over. + * + * **Example**: + * ``` + * ;rt="temperature-c", + * ;rt="light-lux" + * ``` + * + * @see [Constrained RESTful Environments (CoRE) Link Format](https://datatracker.ietf.org/doc/html/rfc6690) + * + * @param[in,out] buffer The buffer that will contain the built string in CoRE Link Format + * @param capacity The capacity @p buffer in bytes + * @param proto The `unicoap` protocol number of the transport + * + * @returns Length of encoded resource list in bytes or negative integer on error + */ +ssize_t unicoap_resource_core_link_format_build(char* buffer, size_t capacity, + unicoap_proto_t proto); + +/** + * @brief Context information required to write a resource link + */ +typedef struct { + /** + * @brief Expected content format of resource + */ + unicoap_content_format_t content_format; + + /** + * @brief Current byte index of encoder + */ + size_t link_pos; + + /** @brief Boolean value indicating whether to initialize result list for first resource */ + bool uninitialized : 1; +} unicoap_link_encoder_ctx_t; + +/** + * @brief Handler function to write a resource link + * + * @param[in] resource Resource for link + * @param[out] buffer Buffer on which to write + * @param[in] capacity Remaining length for @p buffer + * @param[in] context Contextual information on what/how to write + * + * @retval Number of bytes written to @p buffer + * @retval `-1` on error + */ +typedef ssize_t (*unicoap_link_encoder_t)(const unicoap_resource_t* resource, char* buffer, + size_t capacity, unicoap_link_encoder_ctx_t* context); + +/** + * @brief Encodes given resource in Constrained RESTful Environments (CoRE) Link Format + * + * @param[in] resource The CoAP resource to encode + * @param[out] buffer Pointer to where encoded resource string will be written + * @param capacity The capacity of @p buffer in bytes + * @param[in,out] context Encoding context + * + * @pre @p buffer must no be `NULL`. + * + * @returns Length of encoded resource string in bytes or negative integer on error + * @retval `ENOBUFS` Buffer too small to encode resources in link format. + */ +ssize_t unicoap_resource_encode_link(const unicoap_resource_t* resource, char* buffer, + size_t capacity, unicoap_link_encoder_ctx_t* context); +/** @} */ + +/* MARK: - Matching request to resources */ +/** + * @name Matching request to resources + * @{ + */ +/** + * @brief Typealias for @ref unicoap_listener + */ +typedef struct unicoap_listener unicoap_listener_t; + +/** + * @brief Handler function for the request matcher strategy + * + * @param[in] path Requested path + * @param path_length Number of UTF-8 characters in @p path (excluding null-terminator) + * @param[in] listener Listener + * @param[out] resource Matching resource + * @param[in] request Request message + * @param[in] endpoint Remote endpoint the request originates from + * + * @retval Zero if resource is found and matcher determined request matches resource definition + * @retval Non-zero CoAP status code appropriate for the mismatch + */ +typedef int (*unicoap_request_matcher_t)(const char* path, size_t path_length, + const unicoap_listener_t* listener, + const unicoap_resource_t** resource, + const unicoap_message_t* request, + const unicoap_endpoint_t* endpoint); +/** + * @brief A modular collection of resources for a server + */ +struct unicoap_listener { + /** + * @brief Reference to contiguous array of resource belonging to this listener + */ + const unicoap_resource_t* resources; + + /** + * @brief The number of resources belonging in this listener + */ + size_t resource_count; + + /** + * @brief Function that picks a suitable request handler from a + * request. + * + * @note Leaving this `NULL` selects the default strategy that picks + * handlers by matching their `Uri-Path` to resource paths (as per + * the documentation of the @ref resources and @ref resource_count + * fields). + */ + unicoap_request_matcher_t request_matcher; + + /** + * @brief Encoder for Constrained RESTful Environments (CoRE) Link Format + */ + unicoap_link_encoder_t link_encoder; + + /** + * @brief Next listener in linked list + */ + unicoap_listener_t* next; + + /** + * @brief Allowed protocols for the resources grouped together by this resource + * + * Example: Set to `UNICOAP_PROTOCOL_FLAG(UNICOAP_PROTO_UDP) | UNICOAP_PROTOCOL_FLAG(UNICOAP_PROTO_DTLS)` + * to allow only CoAP over UDP and DTLS. Alternatively you can use + * `UNICOAP_PROTOCOLS(UNICOAP_PROTO_UDP, UNICOAP_PROTO_DTLS)`. + * + * @see @ref unicoap_proto_set_t + * @see @ref UNICOAP_PROTOCOL_FLAG + * @see @ref UNICOAP_PROTOCOLS + * @see @ref UNICOAP_PROTOCOLS_ALLOW_ALL + * @see @ref UNICOAP_PROTOCOLS_ALLOW_NONE + */ + unicoap_proto_set_t protocols; +}; + +/** + * @brief Makes unicoap listen for resources contained in listener + * + * @pre @p listener is a valid pointer to a single listener (that is, + * `listener->next == NULL`). You must ensure the memory pointed at remains initialized until + * the listener is deregistered. This may never happen or happen implicitly when `unicoap` is + * deinitialized. We recommend you statically allocate the listener and its resources. + * + * @note + * If you are tempted to register a pre-linked chain of listeners, + * consider placing all their resources in the resources array of a + * single listener instead. + * + * @param[in] listener Listener containing the resources. + * + */ +void unicoap_listener_register(unicoap_listener_t* listener); + +/** + * @brief Removes listener from unicoap + * + * @pre @p listener is a valid pointer to a single listener + * + * @param[in] listener Listener containing the resources. + * @returns Negative integer on error, zero on success. + * @retval `-ENOENT` if the given listener is not registered with `unicoap`. + */ +int unicoap_listener_deregister(unicoap_listener_t* listener); + +/** + * @brief Determines whether the complete Uri-Path matches the resources path. + * + * @param[in] resource Resource + * @param[in] path URI path such as `/foo/bar` + * @param length Number of UTF-8 characters in @p path + * + * @see @ref UNICOAP_RESOURCE_FLAG_MATCH_SUBTREE + * + * @note Multiple consecutive slashes are treated as one. + * https://pubs.opengroup.org/onlinepubs/009695399/basedefs/xbd_chap03.html#tag_03_266 + * + * @returns A boolean value indicating whether the specified path matches the resource definition. + */ +bool unicoap_resource_match_path_string(const unicoap_resource_t* resource, const char* path, size_t length); + +/** + * @brief Determines whether the complete Uri-Path matches the resources path. + * + * @param[in] resource Resource to check for matching path + * @param[in] options Options to read URI path from + * + * @remark + * If you want to call this function in a loop, consider reading the Uri-Path with + * @ref unicoap_options_copy_uri_path and then calling @ref unicoap_resource_match_path_string + * repeatedly. This is useful when you want to match a given request against a number of resources. + * + * This function obeys the @ref UNICOAP_RESOURCE_FLAG_MATCH_SUBTREE flag. + * + * @returns A boolean value indicating whether the given resource matches the path in @p options. + */ +bool unicoap_resource_match_path_options(const unicoap_resource_t* resource, + const unicoap_options_t* options); +/** @} */ + +/** + * @name Matching methods and protocols + * @{ + */ +/** + * @brief A bit set at the position dictated by @p method + * @param method The method to turn into a flag + * @returns `1 << method` + */ +#define UNICOAP_METHOD_FLAG(method) (1 << (method)) + +/** + * @brief Macro that builds a bit field where the i-th bit indicates the i-th request method (`0.0i`) + * @see @ref unicoap_resource_t.flags + * + * Use this macro to create a bitfield of allowed methods for a given + * CoAP resource: + * ```c + * // Example: Allow GET and PUT + * UNICOAP_METHODS(UNICOAP_METHOD_GET, UNICOAP_METHOD_PUT) + * ``` + * + * You can also use regular bitwise operators, such as OR, AND, XOR, and NOT. `UNICOAP_METHODS` is a + * homomorphism preserving the `|` operator. + * ``` + * UNICOAP_METHODS(UNICOAP_METHOD_GET) | UNICOAP_METHODS(UNICOAP_METHOD_PUT, UNICOAP_METHOD_POST) + * == + * UNICOAP_METHODS(UNICOAP_METHOD_GET, UNICOAP_METHOD_PUT, UNICOAP_METHOD_POST) + * ``` + */ +#define UNICOAP_METHODS(first_method, ...) UNICOAP_BITFIELD(first_method, __VA_ARGS__) + +/** + * @brief @ref unicoap_method_set_t value indicating all methods are allowed + */ +#define UNICOAP_METHODS_ALL (0xff) + +/** + * @brief Returns whether the method is allowed according to the given @p methods + * @param methods Methods bit field, each set bit's position corresponds to a method allowed + * @param method Method to be checked + */ +static inline bool unicoap_resource_match_method(unicoap_method_set_t methods, unicoap_method_t method) +{ + return (methods & UNICOAP_METHOD_FLAG(method)) != 0; +} + +/** + * @brief A bit set at the position dictated by @p proto + * @param proto The protocol number to turn into a flag + * @returns `1 << proto` + */ +#define UNICOAP_PROTOCOL_FLAG(proto) (1 << (proto)) + +/** + * @brief Macro creating a bit field describing the specified protocols. + * @see @ref unicoap_resource_t.flags + * + * Use this macro to create a bitfield of allowed protocols for a given + * CoAP resource: + * ```c + * // Example: Allow this resource to only be accessed over UDP and TCP + * UNICOAP_PROTOCOLS(UNICOAP_PROTO_UDP, UNICOAP_PROTO_TCP) + * ``` + */ +#define UNICOAP_PROTOCOLS(first_proto, ...) UNICOAP_BITFIELD(first_proto, __VA_ARGS__) + +/** + * @brief @ref unicoap_proto_set_t value indicating all protocols are allowed + */ +#define UNICOAP_PROTOCOLS_ALLOW_ALL (0) + +/** + * @brief @ref unicoap_proto_set_t value indicating no protocol is allowed + */ +#define UNICOAP_PROTOCOLS_ALLOW_NONE (1) + +/** + * @brief Returns whether the given @p proto is allowed according to the @p protocols argument + * @param protocols Protocols bit field, each set bit's position corresponds to a proto allowed + * @param proto Proto to be checked + * + * @see @ref UNICOAP_PROTOCOLS_ALLOW_ALL + */ +static inline bool unicoap_match_proto(unicoap_proto_set_t protocols, unicoap_proto_t proto) +{ + return protocols == UNICOAP_PROTOCOLS_ALLOW_ALL || + (protocols & UNICOAP_PROTOCOL_FLAG(proto)) != 0; +} +/** @} */ +/** @} */ + +#if !defined(DOXYGEN) +extern uint8_t unicoap_receiver_buffer[CONFIG_UNICOAP_PDU_SIZE_MAX]; +#endif + +#ifdef __cplusplus +} +#endif diff --git a/sys/include/net/unicoap/config.h b/sys/include/net/unicoap/config.h index f25eb077f028..199e65e2ee1b 100644 --- a/sys/include/net/unicoap/config.h +++ b/sys/include/net/unicoap/config.h @@ -33,13 +33,95 @@ * @{ */ /** - * @brief Enables debug logging in all `unicoap` source files, except where locally overwritten + * @brief A configuration option determining whether a dedicated thread is created for `unicoap` + * on initialization + * + * When this option is turned on, @ref unicoap_init will call @ref thread_create. The `unicoap` + * thread will execute @ref unicoap_loop_run. If you want to run unicoap on another thread + * (e.g., the main thread), disable this option and call @ref unicoap_loop_run yourself. * * **Default**: enabled */ +#if !defined(CONFIG_UNICOAP_CREATE_THREAD) || defined(DOXYGEN) +# define CONFIG_UNICOAP_CREATE_THREAD 1 +#endif + +/** + * @brief Enables debug logging in all `unicoap` source files, except where locally overwritten + * + * **Default**: disabled + */ #if !defined(CONFIG_UNICOAP_DEBUG_LOGGING) || defined(DOXYGEN) # define CONFIG_UNICOAP_DEBUG_LOGGING 0 #endif + +/** + * @brief Catches and prints API misuse + * + * When enabled, helpful warnings and error explanations are printed to stdout. Disable this + * setting and enable link-time optimization to strip logic from application binary. + * Trades safety for performance due to added conditional statements. + * + * **Default:** enabled. + * + * @warning unicoap will not honour safety fences, hence you may run into faulty memory behavior + * if your API usage deviates from the documented one. + */ + +#if !defined(CONFIG_UNICOAP_ASSIST) || defined(DOXYGEN) +# define CONFIG_UNICOAP_ASSIST 1 +#endif +/** @} */ + +/* MARK: - Ports and Sockets */ +/** + * @name Ports + * @{ + */ +/** + * @brief Default CoAP port + */ +#define UNICOAP_DEFAULT_COAP_PORT (5683) + +/** + * @brief Default CoAP secure port + */ +#define UNICOAP_DEFAULT_COAPS_PORT (5684) + +/** + * @brief UDP port + * @ingroup net_unicoap_drivers_udp + */ +#if !defined(CONFIG_UNICOAP_UDP_PORT) || defined(DOXYGEN) +# define CONFIG_UNICOAP_UDP_PORT UNICOAP_DEFAULT_COAP_PORT +#endif + +/** + * @brief DTLS port + * @ingroup net_unicoap_drivers_dtls + */ +#if !defined(CONFIG_UNICOAP_DTLS_PORT) || defined(DOXYGEN) +# define CONFIG_UNICOAP_DTLS_PORT UNICOAP_DEFAULT_COAPS_PORT +#endif + +/** + * @brief If enabled, guarantees @ref sock_udp_recv_buf never returns fragmented data, i.e., the + * entire datagram is always fully retrieved after the first call to @ref sock_udp_recv_buf. + * + * **Default**: Set if `gnrc_sock_udp` module is used (automatically used when UDP driver imported) + */ +#if !defined(CONFIG_UNICOAP_SOCK_ZERO_COPY_GUARANTEES) || defined(DOXYGEN) +# define CONFIG_UNICOAP_SOCK_ZERO_COPY_GUARANTEES IS_USED(MODULE_GNRC_SOCK_UDP) +#endif + +/** + * @brief Instructs the transport drivers to retrieve the local endpoint a PDU arrives at. + * + * **Default**: Enabled + */ +#if !defined(CONFIG_UNICOAP_GET_LOCAL_ENDPOINTS) || defined(DOXYGEN) +# define CONFIG_UNICOAP_GET_LOCAL_ENDPOINTS 1 +#endif /** @} */ /* MARK: - Limits */ @@ -105,6 +187,50 @@ static_assert(CONFIG_UNICOAP_GENERATED_TOKEN_LENGTH > 0, * @brief Numbers of bits needed to represent a given ETag's length */ #define UNICOAP_ETAG_LENGTH_FIXED_WIDTH 4 + +/** + * @brief Capacity of internal buffers. Set to maximum PDU size. + * + * **Default**: 128 + * + * @note In certain situations, such as when sending a request unreliably, + * this limit has no effect. Internally, it is used for, but is, in the future, not limited to, + * retransmission copies. + */ +#if !defined(CONFIG_UNICOAP_PDU_SIZE_MAX) || defined(DOXYGEN) +# define CONFIG_UNICOAP_PDU_SIZE_MAX (128) +#endif + +/** + * @brief Maximum length of a resource path string. Used for request matching. + * + * **Default**: 64 characters + * + * Normally, you could match a request's path against all resources' paths by comparing + * the individual `Uri-Path` options present in the request. Re-parsing the options for each + * resource the server hosts becomes expensive fast. Therefore, unicoap serializes the `Uri-Path` + * and then does consecutive`strncmp` calls. + * + * @see @ref unicoap_resource_match_path_string + * @see @ref unicoap_resource_match_path_options + */ +#if !defined(CONFIG_UNIOCOAP_PATH_LENGTH_MAX) || defined(DOXYGEN) +# define CONFIG_UNIOCOAP_PATH_LENGTH_MAX (64) +#endif + +static_assert(CONFIG_UNIOCOAP_PATH_LENGTH_MAX > 0, + "CONFIG_UNIOCOAP_PATH_LENGTH_MAX is zero: Path buffer too small"); + +/** + * @brief Maximum size of `/.well-known/core` payload. + * + * **Default**: 120 bytes + * + * Allowed to exceed the maximum [PDU size](@ref CONFIG_UNICOAP_PDU_SIZE_MAX) + */ +#if !defined(CONFIG_UNICOAP_WELL_KNOWN_CORE_SIZE_MAX) || defined(DOXYGEN) +# define CONFIG_UNICOAP_WELL_KNOWN_CORE_SIZE_MAX (120) +#endif /** @} */ /* MARK: - Timing */ @@ -145,16 +271,228 @@ static_assert(CONFIG_UNICOAP_GENERATED_TOKEN_LENGTH > 0, * @brief See @ref CONFIG_UNICOAP_OBSERVE_VALUE_WIDTH */ #if (CONFIG_UNICOAP_OBSERVE_VALUE_WIDTH == 3) -# define UNICOAP_OBS_TICK_EXPONENT (0) +# define UNICOAP_OBSERVE_TICK_EXPONENT (0) #elif (CONFIG_UNICOAP_OBSERVE_VALUE_WIDTH == 2) -# define UNICOAP_OBS_TICK_EXPONENT (6) +# define UNICOAP_OBSERVE_TICK_EXPONENT (6) #elif (CONFIG_UNICOAP_OBSERVE_VALUE_WIDTH == 1) -# define UNICOAP_OBS_TICK_EXPONENT (14) +# define UNICOAP_OBSERVE_TICK_EXPONENT (14) #else # error CONFIG_UNICOAP_OBSERVE_VALUE_WIDTH must not exceed 3 #endif /** @} */ +/* MARK: - Server */ +/** + * @name Server + * @{ + */ +/** + * @brief Prevents unicoap from sending a response if optional, as indicated by the `No-Response` + * option + * + * If enabled, responses prepared by the application will be disregarded and not sent. + * + * **Default**: off + */ +#if !defined(CONFIG_UNICOAP_PREVENT_OPTIONAL_RESPONSES) || defined(DOXYGEN) +# define CONFIG_UNICOAP_PREVENT_OPTIONAL_RESPONSES 0 +#endif +/** + * @brief Determines whether `unicoap` registers a default `/.well-known/core` resource. + * @see @ref net_unicoap_server + * + * **Default**: enabled (1) + */ +#if !defined(CONFIG_UNICOAP_WELL_KNOWN_CORE) || defined(DOXYGEN) +# define CONFIG_UNICOAP_WELL_KNOWN_CORE (1) +#endif +/** @} */ + +/* MARK: - RFC 7252 messaging */ +/** + * @name RFC 7252 messaging + * @{ + */ +/** + * @brief Used to calculate upper bound for timeout + * + * **Default**: 1500 + * + * This represents the `ACK_RANDOM_FACTOR` + * ([RFC 7252, section 4.2](https://tools.ietf.org/html/rfc7252#section-4.2)) + * multiplied by 1000, to avoid floating point arithmetic. + * + * @see + * @ref CONFIG_UNICOAP_TIMEOUT_ACK_MS + */ +#if !defined(CONFIG_UNICOAP_RANDOM_FACTOR_1000) || defined(DOXYGEN) +# define CONFIG_UNICOAP_RANDOM_FACTOR_1000 (1500) +#endif + +/** @brief Upper bound of range ACK timeouts are selected from */ +#define UNICOAP_TIMEOUT_ACK_RANGE_UPPER \ + ((uint32_t)CONFIG_UNICOAP_TIMEOUT_ACK_MS * CONFIG_UNICOAP_RANDOM_FACTOR_1000 / 1000) + +/** + * @brief Initial ACK timeout after which a given message will be retransmitted. + * + * **Unit**: milliseconds + * **Default:** 2000 + * + * @note The timeout doubles for subsequent retries. To avoid synchronization of retransmissions + * across hosts, the actual timeout is chosen randomly between + * [the ACK timeout](@ref CONFIG_UNICOAP_TIMEOUT_ACK_MS) and + * ([ACK timeout](@ref CONFIG_UNICOAP_TIMEOUT_ACK_MS) `*` + * [random factor](@ref CONFIG_UNICOAP_RANDOM_FACTOR_1000) / 1000). + */ +#if !defined(CONFIG_UNICOAP_TIMEOUT_ACK_MS) || DOXYGEN +# define CONFIG_UNICOAP_TIMEOUT_ACK_MS (2000) +#endif + +/** + * @brief Maximum number of retransmissions of a confirmable message + * + * **Default**: 4 + */ +#if !defined(CONFIG_UNICOAP_RETRANSMISSIONS_MAX) || DOXYGEN +# define CONFIG_UNICOAP_RETRANSMISSIONS_MAX (4) +#endif + +static_assert(CONFIG_UNICOAP_RETRANSMISSIONS_MAX < 32, + "CONFIG_UNICOAP_RETRANSMISSIONS_MAX must not exceed 31"); + +/** + * @brief Maximum number of parallel message IDs that are sent and watched for reset and + * acknowledgment messages + * + * **Default**: 2 transmissions + */ +#if !defined(CONFIG_UNICOAP_RFC7252_TRANSMISSIONS_MAX) || defined(DOXYGEN) +# define CONFIG_UNICOAP_RFC7252_TRANSMISSIONS_MAX 2 +#endif + +/* TODO: Client and advanced server features: Limit to exchange-layer state objects */ + +/** + * @brief Maximum number of internal buffers unicoap reserves. + * + * Used for retransmitting `CON` messages, and storing `ACK` messages when deduplicating. + * **Default**: 2 + * + * ## Guidance on common scenarios + * Developers of apps that serve responses reliably should consider how many clients will request + * the same resource at a time. `CON` responses will need to cached, i.e., a carbon copy will be + * created. If the client acknowledges the response directly and no retransmission is needed, + * the copy will be deleted. Otherwise, the copy is kept until an `ACK` arrives or the + * retransmission counter exceeds @ref CONFIG_UNICOAP_RFC7252_TRANSMISSIONS_MAX. + * + */ +#if !defined(CONFIG_UNICOAP_CARBON_COPIES_MAX) || defined(DOXYGEN) +# define CONFIG_UNICOAP_CARBON_COPIES_MAX (2) +#endif +/** @} */ + +/* MARK: - DTLS */ +/** + * @name DTLS + * @{ + */ +/** + * @brief Timeout for the DTLS handshake process. Set to 0 for infinite time + * + * **Default**: 3 ms + */ +#if !defined(CONFIG_UNICOAP_DTLS_HANDSHAKE_TIMEOUT_MS) || defined(DOXYGEN) +# define CONFIG_UNICOAP_DTLS_HANDSHAKE_TIMEOUT_MS (3 * MS_PER_SEC) +#endif + +/** + * @brief Number of minimum available session slots. If the count of available + * sessions falls below this threshold, the oldest used session will be + * closed after a timeout time. Set to 0 to deactivate this feature. + * + * **Default**: 1 + */ +#if !defined(CONFIG_UNICOAP_DTLS_MINIMUM_AVAILABLE_SESSIONS) || defined(DOXYGEN) +# define CONFIG_UNICOAP_DTLS_MINIMUM_AVAILABLE_SESSIONS (1) +#endif + +/** + * @brief Timeout for freeing up a session when minimum number of available + * sessions is not given. + * + * **Default**: 15 ms + */ +#if !defined(CONFIG_UNICOAP_DTLS_MINIMUM_AVAILABLE_SESSIONS_TIMEOUT_MS) || defined(DOXYGEN) +# define CONFIG_UNICOAP_DTLS_MINIMUM_AVAILABLE_SESSIONS_TIMEOUT_MS (15 * MS_PER_SEC) +#endif +/** @} */ + +/* MARK: - Stack sizes */ +/** + * @name Stack sizes + * @{ + */ +/** + * @brief Stack size for module thread + */ +#if !defined(UNICOAP_STACK_SIZE) || defined(DOXYGEN) + +/** + * @brief Extra stack memory to be used when CoAP over DTLS driver is used + */ +# if IS_USED(MODULE_UNICOAP_DRIVER_DTLS) +# define UNICOAP_DTLS_EXTRA_STACKSIZE (THREAD_STACKSIZE_DEFAULT) +# else +# define UNICOAP_DTLS_EXTRA_STACKSIZE (0) +# endif + +/** + * @brief Stack memory used by `unicoap` thread + * + * This parameter is relevant if you disable @ref CONFIG_UNICOAP_CREATE_THREAD + */ +# define UNICOAP_STACK_SIZE (THREAD_STACKSIZE_DEFAULT + DEBUG_EXTRA_STACKSIZE + \ + CONFIG_UNICOAP_OPTIONS_BUFFER_DEFAULT_CAPACITY + UNICOAP_DTLS_EXTRA_STACKSIZE) +#endif +/** @} */ + +/* TODO: Put the following into the Automatic Block-wise Transfers Group once available */ + +/** + * @brief Block size unicoap will suggest for Block1 and Block2 transfers + * + * **Default**: 32 bytes + */ +#if !defined(CONFIG_UNICOAP_BLOCK_SIZE) || defined(DOXYGEN) +# define CONFIG_UNICOAP_BLOCK_SIZE (32) +#endif + +#ifndef DOXYGEN +# ifdef CONFIG_UNICOAP_BLOCK_SZX +# error CONFIG_UNICOAP_BLOCK_SZX must not be configured manually. +# endif +# if CONFIG_UNICOAP_BLOCK_SIZE == 1024 +# define CONFIG_UNICOAP_BLOCK_SZX (6) +# elif CONFIG_UNICOAP_BLOCK_SIZE == 512 +# define CONFIG_UNICOAP_BLOCK_SZX (5) +# elif CONFIG_UNICOAP_BLOCK_SIZE == 256 +# define CONFIG_UNICOAP_BLOCK_SZX (4) +# elif CONFIG_UNICOAP_BLOCK_SIZE == 128 +# define CONFIG_UNICOAP_BLOCK_SZX (3) +# elif CONFIG_UNICOAP_BLOCK_SIZE == 64 +# define CONFIG_UNICOAP_BLOCK_SZX (2) +# elif CONFIG_UNICOAP_BLOCK_SIZE == 32 +# define CONFIG_UNICOAP_BLOCK_SZX (1) +# elif CONFIG_UNICOAP_BLOCK_SIZE == 16 +# define CONFIG_UNICOAP_BLOCK_SZX (0) +# else +# error CONFIG_UNICOAP_BLOCK_SIZE must be 1024, 512, 256, 128, 64, 32, or 16 +# endif +#endif + +/* TODO: Add static_asserts once other Block-wise parameters are available */ + #ifdef __cplusplus extern "C" { } diff --git a/sys/include/net/unicoap/message.h b/sys/include/net/unicoap/message.h index 7c9901fa4f83..7ae259ec7186 100644 --- a/sys/include/net/unicoap/message.h +++ b/sys/include/net/unicoap/message.h @@ -139,7 +139,7 @@ typedef struct { * @brief CoAP request method * * @pre Check if this is a request message before reading this property using - * @ref unicoap_message_is_request. + * @ref unicoap_message_code_is_request. */ unicoap_method_t method; @@ -147,7 +147,7 @@ typedef struct { * @brief CoAP response status * * @pre Check if this is a response message before reading this property using - * @ref unicoap_message_is_response. + * @ref unicoap_message_code_is_response. */ unicoap_status_t status; @@ -155,7 +155,7 @@ typedef struct { * @brief CoAP signal * * Check if this is a signaling message before reading this property using - * @ref unicoap_message_is_signal. + * @ref unicoap_message_code_is_signal. */ unicoap_signal_t signal; }; @@ -634,7 +634,7 @@ unicoap_message_t unicoap_message_alloc_string_with_options(uint8_t code, * * @return A boolean value indicating whether the given message is a signal */ -static inline bool unicoap_message_is_signal(uint8_t code) +static inline bool unicoap_message_code_is_signal(uint8_t code) { return unicoap_code_class(code) == UNICOAP_CODE_CLASS_SIGNAL; } @@ -687,7 +687,7 @@ const char* unicoap_string_from_signal(unicoap_signal_t signal); * @param code Message code * @return A boolean value indicating whether the given message is a request */ -static inline bool unicoap_message_is_request(uint8_t code) +static inline bool unicoap_message_code_is_request(uint8_t code) { return unicoap_code_class(code) == UNICOAP_CODE_CLASS_REQUEST; } @@ -893,7 +893,7 @@ unicoap_message_t unicoap_request_alloc_string_with_options(unicoap_method_t met * * @return A boolean value indicating whether the given message is a response */ -bool unicoap_message_is_response(uint8_t code); +bool unicoap_message_code_is_response(uint8_t code); /** * @brief Obtains response status from the given message's code diff --git a/sys/include/net/unicoap/transport.h b/sys/include/net/unicoap/transport.h new file mode 100644 index 000000000000..40667b38bb2a --- /dev/null +++ b/sys/include/net/unicoap/transport.h @@ -0,0 +1,464 @@ +/* + * Copyright (C) 2024-2025 Carl Seifert + * Copyright (C) 2024-2025 TU Dresden + * + * This file is subject to the terms and conditions of the GNU Lesser General + * Public License v2.1. See the file LICENSE in the top level directory for + * more details. + */ + +#pragma once + +#include +#include +#include "iolist.h" + +#include "timex.h" +#include "uri_parser.h" +#include "modules.h" + +#if IS_USED(MODULE_UNICOAP_SOCK_SUPPORT) || defined(DOXYGEN) +# include "net/sock.h" +# include "net/sock/async/event.h" +# include "net/sock/util.h" +# include "net/sock/udp.h" +# include "net/sock/tcp.h" +#endif + +#if IS_USED(MODULE_UNICOAP_DRIVER_DTLS) || defined(DOXYGEN) +# include "net/sock/dtls.h" +#endif + +/* MARK: unicoap_driver_extension_point */ + +#include "net/unicoap/message.h" +#include "net/unicoap/options.h" + +/** + * @defgroup net_unicoap_transport Transport Protocol Abstractions + * @ingroup net_unicoap + * @brief CoAP endpoint, resource identifiers, and schemes + * @{ + */ + +/** + * @file + * @brief CoAP transport protocol abstraction layer + * @author Carl Seifert + */ + +#ifdef __cplusplus +extern "C" { +#endif + +/* MARK: - CoAP URI schemes */ +/** + * @name CoAP URI schemes + * @{ + * The goal for CoAP is to have `coap` as the only scheme, with the underlying transport being + * inferred using + * [CoAP Transport Indication](https://datatracker.ietf.org/doc/draft-ietf-core-transport-indication). + */ +/** + * @brief Default CoAP URI scheme + */ +#define UNICOAP_SCHEME "coap" + +/** + * @brief URI scheme for CoAP over UDP + */ +#define UNICOAP_SCHEME_UDP "coap" + +/** + * @brief URI scheme for CoAP over DTLS + */ +#define UNICOAP_SCHEME_DTLS "coaps" + +/** + * @brief URI scheme for CoAP over TCP + */ +#define UNICOAP_SCHEME_TCP "coap+tcp" + +/** + * @brief URI scheme for CoAP over TLS + */ +#define UNICOAP_SCHEME_TLS "coaps+tcp" + +/** + * @brief URI scheme for CoAP over WebSockets + */ +#define UNICOAP_SCHEME_WEBSOCKET "coap+ws" + +/** + * @brief URI scheme for CoAP over secure WebSockets (over TLS) + */ +#define UNICOAP_SCHEME_WEBSOCKET_TLS "coaps+ws" + +/** + * @brief URI scheme for CoAP over GATT over Bluetooth Low Energy (BLE) + */ +#define UNICOAP_SCHEME_BLE_GATT "coap" + +/** + * @brief Domain for for CoAP over GATT over Bluetooth Low Energy (BLE) + * + * Example URI: `coap://001122334455.ble.arpa/.well-known/core`, + * with `001122334455` being the MAC address of the BLE device. + */ +#define UNICOAP_DOMAIN_BLE ".ble.arpa" + +/* MARK: unicoap_driver_extension_point */ +/** @} */ + +/* MARK: - Endpoints */ +/** + * @name Endpoints + * @{ + */ +/** + * @brief Unspecified protocol number + * + * Use this protocol number to mark endpoints uninitialized. + */ +#define UNICOAP_PROTO_UNSPECIFIED (0) + +/** + * @brief Protocol number flag indicating the transport is considered reliable on its own. + */ +#define UNICOAP_PROTO_FLAG_RELIABLE_TRANSPORT (1) + +/** + * @brief Transport protocol CoAP is used over. + */ +typedef enum { + /** @brief CoAP over UDP endpoint */ + UNICOAP_PROTO_UDP = 1 << 1, + + /** @brief CoAP over DTLS over UDP endpoint */ + UNICOAP_PROTO_DTLS = 2 << 1, + + /* MARK: unicoap_driver_extension_point */ +} __attribute__((__packed__)) unicoap_proto_t; + +/** + * @brief Determines whether the given transport protocol is reliable + * + * Used for enabling the Block-wise Extension for Reliable Transports (BERT). + * + * @param proto Protocol number for transport + * @returns A boolean value determining whether the transport is considered reliable + * + * @note BERT allows multiple blocks of size 1024 bytes to be included in a single CoAP message. + */ +static inline bool unicoap_transport_is_reliable(unicoap_proto_t proto) +{ + return proto & UNICOAP_PROTO_FLAG_RELIABLE_TRANSPORT; +} + +/** + * @brief Determines whether the given transport driver uses a transport layer socket in RIOT + * + * @param proto Protocol number for transport + * @returns A boolean value indicating whether the transport uses a transport layer socket in RIOT + * + * Used when parsing URIs and when printing an endpoint. If true, the socket endpoint can be + * casted to `struct _sock_tl_ep*`. + * + * ### Examples + * - @ref sock_udp + * - @ref sock_dtls + * - @ref sock_tcp + * + */ +static inline bool unicoap_transport_uses_sock_tl_ep(unicoap_proto_t proto) +{ + (void)proto; + /* If a new transport driver does not use RIOT's socket API, + * such as CoAP over GATT, return false here. */ + /* MARK: unicoap_driver_extension_point */ + return true; +} + +/** + * @brief A CoAP endpoint + */ +typedef struct { + /** @brief Protocol number */ + unicoap_proto_t proto; + + union { + /* union members are guaranteed to start at offset zero */ + +#if IS_USED(MODULE_UNICOAP_SOCK_SUPPORT) || defined(DOXYGEN) + /** @brief Transport layer endpoint */ + struct _sock_tl_ep _tl_ep; + + /* _tl_ep acts as a view on both udp_ep and dtls_ep. */ + + /** @brief RIOT sock UDP endpoint */ + sock_udp_ep_t udp_ep; + + /** @brief RIOT sock DTLS endpoint */ + sock_udp_ep_t dtls_ep; +#endif /* IS_USED(MODULE_UNICOAP_SOCK_SUPPORT) || defined(DOXYGEN) */ + + /* MARK: unicoap_driver_extension_point */ + }; +} unicoap_endpoint_t; + +#if IS_USED(MODULE_UNICOAP_SOCK_SUPPORT) || defined(DOXYGEN) +/** + * @brief Retrieves UDP endpoint from CoAP endpoint + * @pre @p endpoint is a CoAP over UDP endpoint (proto == @ref UNICOAP_PROTO_UDP) + * + * @memberof unicoap_endpoint_t + * + * @param[in,out] endpoint CoAP over UDP endpoint + * + * @returns @ref sock_udp_ep_t + */ +static inline sock_udp_ep_t* unicoap_endpoint_get_udp(unicoap_endpoint_t* endpoint) +{ + return &endpoint->udp_ep; +} + +/** + * @brief Retrieves DTLS endpoint from CoAP endpoint + * @pre @p endpoint is a CoAP over DTLS endpoint (proto == @ref UNICOAP_PROTO_DTLS) + * + * @memberof unicoap_endpoint_t + * + * @param[in,out] endpoint CoAP over DTLS endpoint + * + * @returns @ref sock_udp_ep_t + */ +static inline sock_udp_ep_t* unicoap_endpoint_get_dtls(unicoap_endpoint_t* endpoint) +{ + return &endpoint->dtls_ep; +} + +# ifndef DOXYGEN +/** + * @brief Private API. Retrieves transport layer endpoint from CoAP endpoint + * + * @memberof unicoap_endpoint_t + * + * @param[in,out] endpoint CoAP endpoint, pre-allocated + * + * @returns @ref _sock_tl_ep + */ +static inline struct _sock_tl_ep* _unicoap_endpoint_get_tl(unicoap_endpoint_t* endpoint) +{ + return &endpoint->_tl_ep; +} +# endif /* !defined(DOXYGEN) */ +#endif /* IS_USED(MODULE_UNICOAP_SOCK_SUPPORT) || defined(DOXYGEN) */ +/** @} */ + +/* MARK: - Conversions and Tools */ +/** + * @name Conversions and Tools + * @{ + */ + +/* TODO: Client: URI */ + +/** + * @brief Returns scheme from protocol number + * + * @param proto Protocol number + * @returns Null-terminated transport description, or `NULL` if there is no such protocol number + */ +const char* unicoap_string_from_proto(unicoap_proto_t proto); + +/** + * @brief Compares two endpoints + * + * @param[in] source Endpoint 1 + * @param[in] destination Endpoint 2 + * @returns A boolean value indicating whether the two endpoints are considered identical + * semantically + */ +bool unicoap_endpoint_is_equal(const unicoap_endpoint_t* source, + const unicoap_endpoint_t* destination); + +/** + * @brief Determines whether the given endpoint is a multicast endpoint + * + * @param[in] endpoint Endpoint to check + * @returns A boolean value indicating whether the endpoint is considered a multicast endpoint + */ +bool unicoap_endpoint_is_multicast(const unicoap_endpoint_t* endpoint); + +/** + * @brief Prints the given transport layer endpoint + * + * @param[in] ep Transport layer endpoint + */ +#if IS_USED(MODULE_UNICOAP_SOCK_SUPPORT) || defined(DOXYGEN) +void unicoap_print_sock_tl_ep(const struct _sock_tl_ep* ep); +#else +static inline void unicoap_print_sock_tl_ep(const void* ep) +{ + (void)ep; + assert(false); +} +#endif +/** + * @brief Prints the given CoAP endpoint + * + * @param[in] endpoint Endpoint + */ +void unicoap_print_endpoint(const unicoap_endpoint_t* endpoint); +/** @} */ +/** @} */ + +/** + * @addtogroup net_unicoap_drivers_udp + * @{ + */ +/* MARK: - Sockets */ +/** + * @name Sockets + * @{ + */ +/** @brief Returns the internal UDP socket */ +#if IS_USED(MODULE_UNICOAP_DRIVER_UDP) || defined(DOXYGEN) +sock_udp_t* unicoap_transport_udp_get_socket(void); +#else +static inline +void* unicoap_transport_udp_get_socket(void) +{ + return NULL; +} +#endif + +#if IS_USED(MODULE_UNICOAP_SOCK_SUPPORT) || defined(DOXYGEN) +/* sock_udp_t requires an implementation which is not present if someone uses unicoap + * without a driver, i.e., just message APIs. In this case, unicoap_sock_support is not imported. */ + +/** + * @brief Adds UDP socket for client and server functionality + * @pre @p socket must be pre-allocated and must remain allocated until the UDP driver has been + * deinitialized or the socket is closed by you. + * + * @param[in,out] socket Pre-allocated socket to use + * @param[in] local Initialized endpoint, does not need to outlive this function call + * + * You can call this function at any time after `unicoap` has been initialized. + * + * @returns `0` on success or negative error value otherwise. + */ +# if IS_USED(MODULE_UNICOAP_DRIVER_UDP) || defined(DOXYGEN) +int unicoap_transport_udp_add_socket(sock_udp_t* socket, sock_udp_ep_t* local); +# else +static inline +int unicoap_transport_udp_add_socket(sock_udp_t* socket, sock_udp_ep_t* local) +{ + (void)socket; + (void)local; + return 0; +} +# endif + +/** + * @brief Removes UDP socket previously added manually + * + * @param[in,out] socket The socket pointer you have previously passed to + * @ref unicoap_transport_udp_add_socket + * + * @returns `0`, indicating a success. Future versions of this API may return a negative error. + */ +# if IS_USED(MODULE_UNICOAP_DRIVER_UDP) || defined(DOXYGEN) +int unicoap_transport_udp_remove_socket(sock_udp_t* socket); +# else +static inline +int unicoap_transport_udp_remove_socket(sock_udp_t* socket) +{ + (void)socket; + return 0; +} +# endif +#endif /* IS_USED(MODULE_UNICOAP_SOCK_SUPPORT) || defined(DOXYGEN) */ +/** @} */ +/** @} */ + +/** + * @addtogroup net_unicoap_drivers_dtls + * @{ + */ +/* MARK: - Sockets */ +/** + * @name Sockets + * @{ + */ +/** @brief Returns the internal DTLS socket */ +#if IS_USED(MODULE_UNICOAP_DRIVER_DTLS) || defined(DOXYGEN) +sock_dtls_t* unicoap_transport_dtls_get_socket(void); +#else +static inline +void* unicoap_transport_dtls_get_socket(void) +{ + return NULL; +} +#endif + +#if IS_USED(MODULE_UNICOAP_SOCK_SUPPORT) || defined(DOXYGEN) +/* sock_udp_t requires an implementation which is not present if someone uses unicoap + * without a driver, i.e., just message APIs. In this case, unicoap_sock_support is not imported. */ + +/** + * @brief Adds DTLS socket for client and server functionality + * @pre Sockets must be pre-allocated and must remain allocated until the DTLS driver has been + * deinitialized or the socket is closed by you. + * + * @param[in,out] socket Pre-allocated socket to use + * @param[in,out] base_socket Pre-allocated UDP base socket to use + * @param[in] local Initialized endpoint, does not need to outlive this function call + * + * You can call this function at any time after `unicoap` has been initialized. + * + * @returns `0` on success or negative error value otherwise. The error value depends on the + * DTLS implementation. + */ +# if IS_USED(MODULE_UNICOAP_DRIVER_DTLS) || defined(DOXYGEN) +int unicoap_transport_dtls_add_socket(sock_dtls_t* socket, sock_udp_t* base_socket, + sock_udp_ep_t* local); +# else +static inline +int unicoap_transport_dtls_add_socket(sock_dtls_t* socket, sock_udp_t* base_socket, + sock_udp_ep_t* local) +{ + (void)socket; + (void)base_socket; + (void)local; + return 0; +} +# endif + +/** + * @brief Removes DTLS socket previously added manually + * + * @param[in,out] socket The socket pointer you have previously passed to + * @ref unicoap_transport_dtls_add_socket + * + * @returns `0`, indicating a success. Future versions of this API may return a negative error. + * + */ +# if IS_USED(MODULE_UNICOAP_DRIVER_DTLS) || defined(DOXYGEN) +int unicoap_transport_dtls_remove_socket(sock_dtls_t* socket); +# else +static inline +int unicoap_transport_dtls_remove_socket(sock_dtls_t* socket) +{ + (void)socket; + return 0; +} +# endif +#endif /* IS_USED(MODULE_UNICOAP_SOCK_SUPPORT) || defined(DOXYGEN) */ +/** @} */ +/** @} */ + +#ifdef __cplusplus +} +#endif diff --git a/sys/include/net/unicoap/util_macros.h b/sys/include/net/unicoap/util_macros.h index cfe9917f32ac..5e9c35b64072 100644 --- a/sys/include/net/unicoap/util_macros.h +++ b/sys/include/net/unicoap/util_macros.h @@ -23,6 +23,99 @@ # define UNICOAP_CODE_CLASS_DETAIL_FORMAT "%u.%02u" # define _CONCAT3(a, b, c) CONCAT3(a, b, c) + + +/* This is best explained by the following two examples. + * --> denotes a proprocessor evaluation step. + * + * __unicoap_bit(11) + * --> __unicoap_create_bit_or_nothing(11, CONCAT(__unicoap_two_empty_args_, 11)) + * --> __unicoap_create_bit_or_nothing(11, __unicoap_two_empty_args_11) + * \...... __VA_ARGS__ ......./ + * --> __take_second_arg(__unicoap_two_empty_args_11, (1 << (11)) |) + * \...... __VA_ARGS__ ......./ + * --> (1 << (11)) | + * + * + * __unicoap_bit() + * --> __unicoap_create_bit_or_nothing(, CONCAT(__unicoap_two_empty_args_,)) + * --> __unicoap_create_bit_or_nothing(, __unicoap_two_empty_args_) + \.. This is not a token ../ + as above but a real + preprocessor constant. + + * --> __unicoap_create_bit_or_nothing(,~,) + \....... Three arguments: empty, tilde, empty + * --> __take_second_arg(~,,) + * \...... __VA_ARGS__ followed by empty, former first ("offset"), argument + * --> + * \.... nothing/empty + * + * + * The last trailing OR slash is always followed by a zero: + * + * UNICOAP_BITFIELD(...) __unicoap_create_bitfield(__VA_ARGS__,,,,,,,,,,,,,,,) 0 + * + * The numerous commas ensure there are always at least 16 arguments, although they may be empty. + * See the explainer above how an expansion of __unicoap_bit works with an empty argument. + */ + +# define __unicoap_create_bit_or_nothing(offset, ...) \ + __take_second_arg(__VA_ARGS__, (1 << (offset)) |) + +# define __unicoap_two_empty_args_ ~, + +# define __unicoap_bit(offset, ...) \ + __unicoap_create_bit_or_nothing(offset, CONCAT(__unicoap_two_empty_args_, offset)) + +# define __unicoap_create_bitfield(_1, _2, _3, _4, _5, _6, _7, _8, _9, _10, _11, _12, _13, _14, \ + _15, _16, ...) \ + __unicoap_bit(_1) __unicoap_bit(_2) __unicoap_bit(_3) __unicoap_bit(_4) __unicoap_bit(_5) \ + __unicoap_bit(_6) __unicoap_bit(_7) __unicoap_bit(_8) __unicoap_bit(_9) \ + __unicoap_bit(_10) __unicoap_bit(_11) __unicoap_bit(_12) __unicoap_bit(_13) \ + __unicoap_bit(_14) __unicoap_bit(_15) __unicoap_bit(_16) +#endif + +/** + * @brief Creates a bitfield where each argument i indicates the i-th bit is to be set + * + * For example, `UNICOAP_BITFIELD(11, 8, 9)` will return a bitfield with the 11th, 8th, and 9th bit + * set, i.e. `(1 << 11) | (1 << 8) | (1 << 9)` + */ +#define UNICOAP_BITFIELD(...) __unicoap_create_bitfield(__VA_ARGS__,,,,,,,,,,,,,,,) 0 + +/* unicoap_job_t wraps event_t so that changing the internal event loop to something else later + * won't induce a source-breaking change. Hence, an initializer is required to hide that complexity. + * To allow compile-time initialization, we use a UNICOAP_JOB macro. A static inline function + * won't work as such a function won't be able to produce a constant expression. + * + * Because unicoap_job_t uses an event_t internally, the event callback must be casted. Since + * the only argument is a pointer and the return type is void, that cast can be considered safe. + * The problem that arises is that deviant function pointers will silently be casted to the + * same function prototype. Since we cannot use a static inline function to typecheck the + * function pointer, we had to come up with a macro that typechecks the argument to detect + * mismatches in the function pointer type. This is what the following macro does. + * + * It uses __builtin_choose_expr as a compile-time ternary expression and + * __builtin_types_compatible_p to verify that the given argument really matches the expected + * type. What is not possible, however, is diagnosing the error. + * FIXME: _UNICOAP_TRY_TYPECHECK_JOB_FUNC: Emit diagnostic in case of type mismatch. + * + */ +#ifndef DOXYGEN +# if defined(__has_builtin) +# if __has_builtin(__builtin_types_compatible_p) && __has_builtin(__builtin_choose_expr) +# define _UNICOAP_TRY_TYPECHECK_JOB_FUNC(func) \ + __builtin_choose_expr( \ + __builtin_types_compatible_p(__typeof__(void (unicoap_job_t* job)), __typeof__(func)), \ + (void (*)(event_t*))func, \ + ((void)0)/* unicoap_job func has incompatible type, must be void (unicoap_job_t* job) */) +# endif +# endif + +# if !defined(_UNICOAP_TRY_TYPECHECK_JOB_FUNC) +# define _UNICOAP_TRY_TYPECHECK_JOB_FUNC(func) (void (*)(event_t*))func +# endif #endif #ifdef __cplusplus diff --git a/sys/net/application_layer/unicoap/Kconfig b/sys/net/application_layer/unicoap/Kconfig index 94fec6d92cc8..c301dbd99f64 100644 --- a/sys/net/application_layer/unicoap/Kconfig +++ b/sys/net/application_layer/unicoap/Kconfig @@ -38,12 +38,41 @@ menu "CoAP Unified Suite (unicoap)" menu "Behaviors" + config UNICOAP_CREATE_THREAD + bool "Create dedicated thread for unicoap" + default y + help + When this option is turned on, unicoap_init will call thread_create. The unicoap thread will execute unicoap_loop_run. If you want to run unicoap on another thread (e.g., the main thread), disable this option and call unicoap_loop_run yourself. + config UNICOAP_DEBUG_LOGGING bool "Enable debug logging" default n help When enabled, debug logs are printed. Helps trace messages through layers. + config UNICOAP_ASSIST + bool "Enable assistant (API misuse, warnings, fixits)" + default n + help + When enabled, helpful warnings and error explanations are printed to stdout. Disable this setting and enable link-time optimization to strip logic from application binary. Enables safety measures in the parser and client and server API. Trades safety for performance due to added conditional statements. + + config UNICOAP_PREVENT_OPTIONAL_RESPONSES + bool "Prevent sending optional responses" + default n + help + Server only. Prevents unicoap from sending a response if optional, as indicated by the No-Response option. If enabled, responses prepared by the application will be disregarded and not sent. + + config UNICOAP_SOCK_ZERO_COPY_GUARANTEES + bool "Zero-copy sock guarantees" + help + If enabled, guarantees @ref sock_udp_recv_buf never returns fragmented data, i.e., the entire datagram is always fully retrieved after the first call to @ref sock_udp_recv_buf. Default value: gnrc_sock_udp is used. + + config UNICOAP_GET_LOCAL_ENDPOINTS + bool "Retrieve local endpoint" + default y + help + Instructs the transport drivers to retrieve the local endpoint a PDU arrives at. + endmenu comment "******************" @@ -58,4 +87,90 @@ menu "CoAP Unified Suite (unicoap)" endmenu + comment "******************" + comment "DRIVERS" + + menu "RFC 7252 messaging (CoAP over UDP and DTLS common)" + + comment "******************" + comment "BUFFERS" + + config UNICOAP_RFC7252_TRANSMISSIONS_MAX + int "Maximum number of parallel RFC 7252 (re-)transmissions" + range 0 9999999 + default 2 + help + Maximum number of parallel message IDs that are sent and watched for reset and acknowledgment messages. Must not exceed CONFIG_UNICOAP_MEMOS_MAX. + + config UNICOAP_CARBON_COPIES_MAX + int "Carbon copies: Maximum number of PDU copies for retransmissions (and piggybacked ACKs in deduplication)" + range 0 9999999 + default 2 + help + Used for retransmitting CON messages, and storing ACK messages when deduplicating. + + comment "******************" + comment "TIMING" + + config UNICOAP_TIMEOUT_ACK_MS + int "ACK timeout -> retransmission" + range 1 9999999 + default 2000 + help + Initial ACK timeout after which a given message will be retransmitted. + + config UNICOAP_RANDOM_FACTOR_1000 + int "ACK timeout upper bound factor * 1000" + range 1000 9999999 + default 1500 + help + This represents the ACK_RANDOM_FACTOR (RFC 7252, section 4.2, https://tools.ietf.org/html/rfc7252#section-4.2) multiplied by 1000, to avoid floating point arithmetic. + + config UNICOAP_RETRANSMISSIONS_MAX + int "Maximum retransmissions" + range 1 32 + default 4 + help + Maximum number of retransmissions of a confirmable message + + endmenu + + menu "CoAP over UDP" + config UNICOAP_UDP_PORT + int "UDP port" + default 5683 + range 1 65535 + endmenu + + menu "CoAP over DTLS" + config UNICOAP_DTLS_PORT + int "DTLS port" + default 5684 + range 1 65535 + + config UNICOAP_DTLS_HANDSHAKE_TIMEOUT_MS + int "DTLS handshake timeout (ms)" + range 0 9999999 + default 3000 + help + Timeout for the DTLS handshake process. Set to 0 for infinite time. + + config UNICOAP_DTLS_MINIMUM_AVAILABLE_SESSIONS + int "DTLS minimum number of available sessions at any time" + range 0 9999999 + default 1 + help + Number of minimum available sessions. If the count of available sessions falls below this threshold, the oldest used session will be closed after a timeout time. Set to 0 to deactivate this feature. + + config UNICOAP_DTLS_MINIMUM_AVAILABLE_SESSIONS_TIMEOUT_MS + int "DTLS session triage timeout (ms)" + range 0 9999999 + default 15000 + help + Timeout for freeing up a session when minimum number of available sessions is not given. + endmenu + + + # unicoap_driver_extension_point + endmenu # unicoap diff --git a/sys/net/application_layer/unicoap/Makefile.dep b/sys/net/application_layer/unicoap/Makefile.dep index 4f94dd261d1f..aeb3e22801a2 100644 --- a/sys/net/application_layer/unicoap/Makefile.dep +++ b/sys/net/application_layer/unicoap/Makefile.dep @@ -1,6 +1,30 @@ ifneq (,$(filter unicoap,$(USEMODULE))) # Vectored data support USEMODULE += iolist + + # Allocated buffer element lists + USEMODULE += bitfield + + # Message ID generation + USEMODULE += random + + # Retransmissions, etc + USEMODULE += ztimer + USEMODULE += ztimer_msec + + # Event loop + USEMODULE += event_thread + USEMODULE += event_callback + USEMODULE += event_timeout_ztimer + + # Synchronous methods + USEMODULE += sema + + USEMODULE += netif + USEMODULE += ipv6_addr + USEMODULE += ipv4_addr + + DEFAULT_MODULE += auto_init_unicoap endif # This variable ensures the "No drivers" warning gets printed only once @@ -33,14 +57,32 @@ ifneq (,$(filter unicoap_driver_udp,$(USEMODULE))) # CoAP over UDP uses RFC 7252 messaging layer USEMODULE += unicoap_driver_rfc7252_common - # FIXME: upcoming PR: CoAP over UDP driver implementation + # We're using the sock API + USEMODULE += unicoap_sock_support + USEMODULE += sock_udp endif ifneq (,$(filter unicoap_driver_dtls,$(USEMODULE))) # CoAP over DTLS uses RFC 7252 messaging layer USEMODULE += unicoap_driver_rfc7252_common - # FIXME: upcoming PR: CoAP over DTLS driver implementation + # We're using the sock API + USEMODULE += unicoap_sock_support + USEMODULE += sock_dtls + + # DTLS stuff + USEMODULE += tinydtls_sock_dtls + USEPKG += tinydtls + USEMODULE += prng_sha1prng + USEMODULE += dsm + USEMODULE += ztimer_usecf endif # MARK: unicoap_driver_extension_point + +ifneq (,$(filter unicoap_sock_support,$(USEMODULE))) + USEMODULE += sock_async + USEMODULE += sock_async_event + USEMODULE += sock_aux_local + USEMODULE += sock_util +endif diff --git a/sys/net/application_layer/unicoap/docs/message-example.doc.md b/sys/net/application_layer/unicoap/docs/message-example.doc.md index 54673868b9a2..4b93dd8e9763 100644 --- a/sys/net/application_layer/unicoap/docs/message-example.doc.md +++ b/sys/net/application_layer/unicoap/docs/message-example.doc.md @@ -52,9 +52,9 @@ printf("CoAP over UDP/DTLS has id=%i type=%s\n", ### Inspecting a Message You use the -@ref unicoap_message_is_request, -@ref unicoap_message_is_response, and -@ref unicoap_message_is_signal +@ref unicoap_message_code_is_request, +@ref unicoap_message_code_is_response, and +@ref unicoap_message_code_is_signal methods to check whether a given message is a request, response, or signaling message. The corresponding typed view of the code is accessible through diff --git a/sys/net/application_layer/unicoap/docs/resources-xfa.doc.md b/sys/net/application_layer/unicoap/docs/resources-xfa.doc.md new file mode 100644 index 000000000000..c91e18d1d0c6 --- /dev/null +++ b/sys/net/application_layer/unicoap/docs/resources-xfa.doc.md @@ -0,0 +1,23 @@ +@defgroup net_unicoap_resources_xfa Static Resource Definitions +@ingroup net_unicoap_server +@brief Define CoAP resources across files at compile time +@{ + +Module. Specify `USEMODULE += unicoap_resources_xfa` in your application's Makefile. + +Use @ref UNICOAP_RESOURCE to define resources across files as follows. The name you supply for +the resource must be unique among resource definitions but has otherwise no meaning. It is +needed purely for technical reasons involving the implementation of cross file arrays. +The resource macro must be followed by a constant @ref unicoap_resource_t initializer. +In the example below, we define +a resource reachable under /hello-world using the designated initializer. + +```c +UNICOAP_RESOURCE(hello_world_resource) { + .path = "/hello-world", + .handler = my_hello_world_handler, + .methods = UNICOAP_METHODS(UNICOAP_METHOD_GET) +}; +``` + +@} diff --git a/sys/net/application_layer/unicoap/docs/server-tutorial.doc.md b/sys/net/application_layer/unicoap/docs/server-tutorial.doc.md new file mode 100644 index 000000000000..58be7e88b94f --- /dev/null +++ b/sys/net/application_layer/unicoap/docs/server-tutorial.doc.md @@ -0,0 +1,409 @@ +@defgroup net_unicoap_server_example Writing a Server Application +@ingroup net_unicoap_server +@brief Learn to write a simple server application with `unicoap` +@{ + +Sample code. Find the sample code in `examples/networking/coap/unicoap_server`. + +In this tutorial, you will learn how you can create a simple CoAP server with `unicoap`. +We will support CoAP over UDP and DTLS and try out the application using RIOT's native board. +Hence, you will need a Linux host and support for tuntap (tap interfaces). Our goal is to create +a server that greets its users. + +## Getting Started + +To start, we will create a Makefile and add `unicoap` as a dependency. Because we want to support UDP +and DTLS, we need to add the respective drivers in the `Makefile`. + +```makefile +USEMODULE += unicoap +USEMODULE += unicoap_driver_udp +USEMODULE += unicoap_driver_dtls +``` + +Because RIOT allows you to switch the network backend, we need to specify one. +In this tutorial, we choose @ref net_gnrc. + +```makefile +# Include packages that pull up and auto-init the link layer. +# NOTE: 6LoWPAN will be included if IEEE802.15.4 devices are present +USEMODULE += netdev_default +# Automatically initialize GNRC upon startup +USEMODULE += auto_init_gnrc_netif +# Specify the mandatory networking modules +USEMODULE += gnrc_ipv6_default +# Additional networking modules that can be dropped if not needed +USEMODULE += gnrc_icmpv6_echo +``` + +In `main.c`, we include `unicoap`. + +```c +#include "net/unicoap.h" +``` + +## Implementing a Simple CoAP resource + +Let's define a CoAP resource. To define a new resource statically (at compile-time), we +use the @ref UNICOAP_RESOURCE macro. To make that work, we need to import the +@ref net_unicoap_resources_xfa module in our `Makefile`. + +```makefile +USEMODULE += unicoap_resources_xfa +``` + +```c +UNICOAP_RESOURCE(hello) { + // ... +}; +``` + +Note that you will need to supply an identifier. Let's +call it `hello`. You can give it any name you want, as long as it is a unique C +variable name. Each resource must be assigned a path. This will be the part that follows the +host/domain and port in the CoAP URI. To assign a path, set the @ref unicoap_resource_t.path +property to a static null-terminated UTF-8 string literal that starts with a leading slash. +Since we want to define a root instance, we set it to `/`. + +```c +UNICOAP_RESOURCE(hello) { + .path = "/", +}; +``` + +Next, we must specify what CoAP methods (akin to HTTP methods) we want to allow. Therefore, we list +all methods permitted as parameters to the @ref UNICOAP_METHODS macro. You cannot leave +@ref unicoap_resource_t.methods empty. If you later send a request that does not match the set of +methods, `unicoap` will reject that request with a `Method Not Allowed` CoAP response. + +```c +UNICOAP_RESOURCE(hello) { + .path = "/", + .methods = UNICOAP_METHODS(UNICOAP_METHOD_GET, UNICOAP_METHOD_FETCH), +}; +``` + +Optionally, you can also restrict the resource to a set of transports. This may be useful if you +consider encryption to be mandatory. In our case, the greeting will also be reachable over +unencrypted UDP. + +```c +UNICOAP_RESOURCE(hello) { + // ... + .protocols = UNICOAP_PROTOCOLS(UNICOAP_PROTO_DTLS, UNICOAP_PROTO_UDP), + // ... +}; +``` + +Next, we want to make sure our greeting arrives at the client. Since we are using CoAP over UDP (or +CoAP over DTLS, which relies on UDP for that matter), we instruct `unicoap` to send confirmable +responses. `unicoap` will retransmit our response until the client has acknowledged it. +To do this, we pass the @ref UNICOAP_RESOURCE_FLAG_RELIABLE flag. + + +```c +UNICOAP_RESOURCE(hello) { + .path = "/", + .methods = UNICOAP_METHODS(UNICOAP_METHOD_GET, UNICOAP_METHOD_FETCH), + .flags = UNICOAP_RESOURCE_FLAG_RELIABLE, +}; +``` + +Finally, we specify a function that is going to handle requests to `/`. We're going to implement +`handle_hello_request` next. + +```c +UNICOAP_RESOURCE(hello) { + .path = "/", + .methods = UNICOAP_METHODS(UNICOAP_METHOD_GET, UNICOAP_METHOD_FETCH), + .flags = UNICOAP_RESOURCE_FLAG_RELIABLE, + .handler = handle_hello_request +}; +``` + +`handle_hello_request` will take four arguments: the request, auxiliary information like the client +address, a context and an [optional argument](@ref unicoap_resource_t.handler_arg). + +```c +static int handle_hello_request(unicoap_message_t* message, const unicoap_aux_t* aux, + unicoap_request_context_t* ctx, void* arg) { + + // ... +} +``` + +Inside `handle_hello_request` we're going to log the request initially. We use +@ref unicoap_string_from_method to get a string representation of the CoAP method, i.e., the CoAP +message code, and @ref unicoap_message_payload_get_size for the number of payload bytes. +This will log a messaging like `GET /, 0 bytes`. + +```c +printf( + "app: %s /, %" PRIuSIZE " bytes\n", + unicoap_string_from_method(message->method), + unicoap_message_payload_get_size(message) +); +``` + +This version of the resource is going to be very simple. The root resource will simply respond +with a `Hello, World!` string. For static null-terminated strings, `unicoap` provides +convenience initializers. Note that we can reuse the memory of the `message` parameter. +Finally, we respond. You do not necessarily need to return the result of @ref unicoap_send_response. +However, the return value is always supposed to indicate a success (zero) or an error (negative +integer). Invoking send_response before and returning a status code, as well as not calling +send_response before and not returning a status code are treated as fatal errors. + +```c +unicoap_response_init_string(message, UNICOAP_STATUS_CONTENT, "Hello, World!"); +return unicoap_send_response(message, ctx); +``` + +## Creating a More Dynamic CoAP Resource + +The resource we implemented in the previous section was quite simple. Surely, visitors would enjoy +a personalized greeting, wouldn't they? Let's develop a `/greeting` resource that accepts a `name` +query parameter with their name. So if someone asks for `coap://...host.../greeting?name=RIOTeer`, +we would respond with `Hello, RIOTeer! Welcome to our itsy bitsy tiny CoAP server!`. + +The first step is to define a new resource. + +```c +UNICOAP_RESOURCE(greeting) { + .path = "/greeting", + .flags = UNICOAP_RESOURCE_FLAG_RELIABLE, + .methods = UNICOAP_METHODS(UNICOAP_METHOD_GET), + .handler = handle_greeting_request, +}; +``` + +In `handle_greeting_request`, we need to retrieve the `name` query parameter and return a +`Bad Request` response if our visitor did not leave their name in the request. +To do this, we use @ref unicoap_options_t::unicoap_options_get_first_uri_query_by_name. +As a simple shorthand you can return a CoAP status from the request handler as follows. +This will send a response without any payload attached. + +```c +static int handle_greeting_request(unicoap_message_t* message, const unicoap_aux_t* aux, + unicoap_request_context_t* ctx, void* arg) { + (void)aux; + (void)arg; + + ssize_t res = 0; + const char* name = NULL; + + if ((res = unicoap_options_get_first_uri_query_by_name(message->options, "name", &name)) < 0) { + printf("error: could not get 'name' query: %" PRIdSIZE " (%s)\n", res, strerror(-res)); + return UNICOAP_STATUS_BAD_REQUEST; + } + + // ... +} +``` + +We don't really trust our visitor yet, so let's validate their name. Only this time, we tell the +client the query value was invalid by leveraging the @ref unicoap_send_response function and a +dedicated error message. + +```c +// Validate any input. Here, we apply a 30 UTF-8 character limit. You should perform UTF-8 +// validation (is_valid_name). This also includes checking if there's a premature null terminator. +if (res > 30 || !is_valid_name(name, res)) { + unicoap_response_init_string(message, UNICOAP_STATUS_BAD_REQUEST, "invalid 'name' query"); + return unicoap_send_response(message, ctx); +} +``` + +Now we can craft our response. Proper responses have their `Content-Format` option set accordingly, +in our case `text/plain`. Setting options can fail, e.g., due to insufficient buffer capacity, hence +we do that first. +We allocate options on the stack through @ref UNICOAP_OPTIONS_ALLOC and set the format with +@ref unicoap_options_set_content_format. + +@remark +Note that the buffer capacity we specify when allocating +should be an upper estimate. If you do know how CoAP options are represented in memory, you can +set the capacity to exactly the size in bytes (Which, in this case, would prevent us from adding +another option. But since we only need the `Content-Format` option, we set the capacity to exactly +2 bytes.). + +```c +UNICOAP_OPTIONS_ALLOC(options, 2); + +// Set Content-Format option to text/plain +if (unicoap_options_set_content_format(&options, UNICOAP_FORMAT_TEXT) < 0) { + return UNICOAP_STATUS_INTERNAL_SERVER_ERROR; +} + +message->options = &options; +``` + +The next step is to create the greeting string. In theory, you could write the three parts +`Hello, `, then the name, and finally the suffix `! Welcome ......` into a buffer and send that. +But that would be too easy. In some cases, when you would need to create a large buffer, the +following technique might be preferable. Instead of a buffer, we create a vector, that is, a list +of payload _chunks_. The trick is that the network backend will copy these chunks into a +transmission buffer on its own, which is why we can avoid additional copy operations this way. + +These are going to be the first and last chunks: + +```c +#define PREFIX "Hello, " +iolist_t list = { + .iol_base = PREFIX, + .iol_len = static_strlen(PREFIX), +}; + +#define SUFFIX "! Welcome to our itsy bitsy tiny CoAP server!" +iolist_t suffix = { + .iol_base = SUFFIX, + .iol_len = static_strlen(SUFFIX) +}; +``` + +@note +`static_strlen` is defined in the sample code. + +Next, we add the dynamic middle part. + +```c +iolist_t name_chunk = { + .iol_next = &suffix, + .iol_base = (void*)name, + .iol_len = res +}; +list.iol_next = &name_chunk; +``` + +Through `.iol_next = &suffix`, we chain the suffix behind the middle part and using +`list.iol_next = &name_chunk` appends this chain of two chunks to the first chunk. Our vector is +done and we can finally send the response after having set the status and message payload. + +```c +unicoap_message_payload_set_chunks(message, &list); +unicoap_response_set_status(message, UNICOAP_STATUS_CONTENT); + +return unicoap_send_response(message, ctx); +``` + +## Supporting DTLS + +To enable Datagram Transport Layer Security, we already imported the +@ref net_unicoap_drivers_dtls. +However, we need to include additional headers to add a DTLS credential to `unicoap`. + +```c +#if IS_USED(MODULE_UNICOAP_DRIVER_DTLS) +# include "net/sock/dtls/creds.h" +# include "net/credman.h" +# include "net/dsm.h" +# include "unicoap_example_dtls.h" + +# define EXAMPLE_DTLS_CREDENTIAL_TAG 42 + +static const uint8_t psk_id_0[] = PSK_DEFAULT_IDENTITY; +static const uint8_t psk_key_0[] = PSK_DEFAULT_KEY; +static const credman_credential_t credential = { + .type = CREDMAN_TYPE_PSK, + .tag = EXAMPLE_DTLS_CREDENTIAL_TAG, + .params = { + .psk = { + .key = { .s = psk_key_0, .len = sizeof(psk_key_0) - 1, }, + .id = { .s = psk_id_0, .len = sizeof(psk_id_0) - 1, }, + } + }, +}; +#endif +``` + +In the `main` function, we then need to add this credential to the DTLS socket which we can retrieve +using @ref unicoap_transport_dtls_get_socket. + +```c +int main(void) { +# if IS_USED(MODULE_UNICOAP_DRIVER_DTLS) + int res = credman_add(&credential); + if (res < 0 && res != CREDMAN_EXIST) { + /* ignore duplicate credentials */ + printf("app: cannot add credential to system: %d\n", res); + return 1; + } + sock_dtls_t* dtls_socket = unicoap_transport_dtls_get_socket(); + assert(dtls_socket); + + if ((res = sock_dtls_add_credential(dtls_socket, EXAMPLE_DTLS_CREDENTIAL_TAG)) < 0) { + printf("app: cannot add credential to DTLS sock: %d\n", res); + return 1; + } +# endif +} +``` + +Now, your server is also reachable over an encrypted DTLS connection. Time to test it! + +## Testing the CoAP server + +The sample code includes a `client.py` script that uses [aiocoap](https://aiocoap.readthedocs.io). +To use it we need to compile the server, run it and then send a CoAP request to it. We're going to +use RIOT's `native` board for this, i.e., both the client and server will run on your linux host +and there will be no wireless network involved — no antenna needed. +To compile and run the app, run this shell command: + +```sh +cd RIOT/examples/networking/coap/unicoap_server +BOARD=native make -j flash term +``` + +This should show something similar to this: + +``` +RIOT/dist/tools/pyterm/pyterm + -ps RIOT/examples/networking/coap/unicoap_server/bin/native64/unicoap_server.elf + --process-args tap0 +Welcome to pyterm! +Type '/exit' to exit. +# RIOT native interrupts/signals initialized. +# RIOT native64 board initialized. +# RIOT native hardware initialization complete. +# coap: registered 2 XFA resources +# coap.transport.udp: zero_copy_guarantees=1 creating UDP sock, port=5683 if=0 family=inet6 +# coap.transport.dtls: creating DTLS sock, port=5684 if=0 family=inet6 +# main(): This is RIOT! (Version: 2025.04-devel-634-RED-unicoap-02-server-minimal) +# app: listening at UDP +# app: listening at DTLS +# app: using credential: type=PSK id=Client_identity key=secretPSK +# app: IPv6 address: fe80::c0:ff:ee +``` + +@remark +You might need to enable the tap interface: +```sh +sudo ip tuntap add tap0 mode tap user ${USER} +sudo ip link set tap0 up +``` + +@note +`unicoap` will emit debug logs depending on @ref CONFIG_UNICOAP_DEBUG_LOGGING and +@ref CONFIG_UNICOAP_ASSIST. These settings are enabled in the sample code in `app.config`. + +In a second terminal session, run + +```sh +python3 client.py -m GET -u "coap://[fe80::c0:ff:ee%tap0]/greeting?name=RIOTer" +``` + +You should see a number of debug logs from that script culminating in: + +``` +response: 2.05 Content +b'Hello, RIOTer! Welcome to our itsy bitsy tiny CoAP server!' +``` + +Congrats, your server is working! + +If you want to capture the actual CoAP messages being sent, open a third terminal session +and run `tcpdump -i tap0 -w coap-greeting.pcap`, then execute the client script as described above, +then terminate `tcpdump` with `CTRL+C`. You can now open `coap-greeting.pcap` with +[Wireshark](https://wireshark.org) to inspect CoAP messages. You should see a `NON` request followed +by a `CON` response which elicits another `ACK` message from the client. + +@} diff --git a/sys/net/application_layer/unicoap/docs/server.doc.md b/sys/net/application_layer/unicoap/docs/server.doc.md new file mode 100644 index 000000000000..334c31b2599f --- /dev/null +++ b/sys/net/application_layer/unicoap/docs/server.doc.md @@ -0,0 +1,153 @@ +@defgroup net_unicoap_server CoAP Server +@ingroup net_unicoap +@brief Register CoAP resources and handle requests asynchronously +@{ + +A CoAP resource behaves similar to an HTTP endpoint. + +## Registering a Resource + +You can either declare a resource using static cross-file arrays (1) or manually at runtime (2). + +1. This technique requires the @ref net_unicoap_resources_xfa module (XFA = cross-file arrays). + The identifier you specify with the @ref UNICOAP_RESOURCE macro just needs to be unique, + but isn't used otherwise. You will need to specify a path and a set of allowed request methods + using @ref UNICOAP_METHODS. + + ```c + UNICOAP_RESOURCE(my_resource) { + .path = "/led/state", + .handler = my_request_handler, + .methods = UNICOAP_METHODS(UNICOAP_METHOD_GET, UNICOAP_METHOD_PUT) + } + ``` + +2. Alternatively, you can create a new listener and manually add resources as follows. + Listeners are groups of resources. Internally, all XFA resources form a built-in listener. + + ```c + // First, declare your resources in an array + static unicoap_resource_t my_resources[] = { + { + .path = "/led/state", + .handler = my_request_handler, + .methods = UNICOAP_METHODS(UNICOAP_METHOD_GET, UNICOAP_METHOD_PUT) + } + }; + + // Then, create a listener encompassing your resources + static unicoap_listener_t my_listener = { + .resources = my_resources, + .resource_count = ARRAY_SIZE(my_resources), + .request_matcher = unicoap_resource_match_request_default, + }; + + // Finally, register your listener with unicoap + unicoap_listener_register(&my_listener); + + // Optionally, deregister your listener later + unicoap_listener_deregister(&my_listener); + ``` + +## Constrained RESTful Environments (CoRE) resource + +By default, `unicoap` registers a `/.well-known/core` resource, providing a list of available +resources. @ref CONFIG_UNICOAP_WELL_KNOWN_CORE allows you to customize the default behavior. + +You can also customize the link encoding per listener. By default, listeners with the +@ref unicoap_listener_t.link_encoder property unset, use the built-in link encoder. + +## Request-resource matching + +`unicoap` determines whether a request matches a resource in the following order. `unicoap` iterates +over all listeners and then over all resources registered with that listener. The order of listeners +follows the registration order, except the first listener is the built-in listener that handles +`/.well-known/core` and the second may be the XFA resources listener, if present. Once a matching +resource is found, no further ones are checked. + +Resources that do not specify the current mode of transport in their protocol set, are not +considered. In this case, `unicoap` behaves as if the resource does not exist. To restrict a +resource to a subset of transports, set @ref unicoap_resource_t::protocols to a value produced +by @ref UNICOAP_PROTOCOLS. Please note that `unicoap` considers an unset `protocols` property +to mean "All protocols". If you want to make this explicit, use @ref UNICOAP_PROTOCOLS_ALLOW_ALL or +@ref UNICOAP_PROTOCOLS_ALLOW_NONE. + +The server then checks the path of the resource. If you supplied the +@ref UNICOAP_RESOURCE_FLAG_MATCH_SUBTREE flag, a resource registered for `/foo/bar` will also match +subpaths such as `/foo/bar/zoo` and `/foo/bar/zoo/loo`. Then, `unicoap` checks whether the request +method is in the set of allowed methods you specified. If the method does not match, and none of the +other resources indicate a match, a `Method Not Allowed` CoAP response is sent. +If no matching resource is found, a `Not Found` CoAP response is returned. + +If the built-in +behavior does not fit your scenario, you can customize the matching algorithm per listener. +To do this, set the @ref unicoap_listener_t.request_matcher property. By default, if this property +is unset, the built-in matcher will be used. + +## Handling requests + +When registering a resource, you must specify a request handler, even if the resource's content +is statically known. To define a handler, implement a function that adheres to the signature of +@ref unicoap_request_handler_t. + +### Auxiliary Information + +In the handler, you are given access to an auxiliary information +object (`aux`), letting you retrieve the transport used, and both the local and client endpoint. +This may be helpful if you are using a driver able to receive messages at different addresses +(e.g., multiple sockets, ports, or multicast addresses). The `aux` also provides access to the +token, and transport-specific data, such as the message ID and type if UDP or DTLS is used. + +### Responding + +There are multiple techniques for responding. + +1. **Return status code**: If you don't want to send a response payload and have no + options you may want to set, you can just return a @ref unicoap_status_t. Note that this requires + you to complete all request processing before. If this is not desirable, e.g., if you are + performing sensitive operations that would open a timing side channel, consider using one of the + other techniques. + + ```c + int my_request_handler( + unicoap_message_t* message, const unicoap_aux_t* aux, + unicoap_request_context_t* ctx, void* arg + ) { + return UNICOAP_STATUS_NOT_IMPLEMENTED; + } + ``` + +2. **Call send**: Initialize a response message with the status code, and + optionally, also payload and options. You can repurpose the memory used by the `message` + parameter for the response, but you must not write into the payload buffer or mutate options. + If you do want to send options, please allocate options using @ref UNICOAP_OPTIONS_ALLOC. + Then, pass the message and the response context you were + given as part of the handler parameters to @ref unicoap_send_response. Finally, you may inspect + any errors that may have occurred while sending the response or continue processing the request. + You may also directly return the result of @ref unicoap_send_response. + Please be aware any processing performed inside this handler is executed in the server's + processing loop and will thus block it. Given an expensive operation, we encourage you to switch + to the third technique. Nevertheless, this should be the preferred response technique for most + applications. + + ```c + int my_request_handler( + unicoap_message_t* message, const unicoap_aux_t* aux, + unicoap_request_context_t* ctx, void* arg + ) { + unicoap_response_init_string(message, UNICOAP_STATUS_CONTENT, "Hello, World!"); + return unicoap_send_response(message, ctx); + } + ``` + +3. **Defer the response**: This technique is not available yet. + + + +@remark +There is no architectural limitation in the number of sockets or ports. If +multiple sockets or ports (depending on the transport, the term _handle_ or _input_ might be more +adequate) are supported. This would be indicated on the respective driver's documentation page. + + +@} diff --git a/sys/net/application_layer/unicoap/drivers/rfc7252/common/messaging/messaging.c b/sys/net/application_layer/unicoap/drivers/rfc7252/common/messaging/messaging.c new file mode 100644 index 000000000000..58a36227cf83 --- /dev/null +++ b/sys/net/application_layer/unicoap/drivers/rfc7252/common/messaging/messaging.c @@ -0,0 +1,822 @@ +/* + * Copyright (C) 2024-2025 Carl Seifert + * Copyright (C) 2024-2025 TU Dresden + * + * This file is subject to the terms and conditions of the GNU Lesser General + * Public License v2.1. See the file LICENSE in the top level directory for + * more details. + */ + +/** + * @file + * @ingroup net_unicoap_drivers_rfc7252_common + * @brief Messaging implementation of common RFC 7252 driver + * @author Carl Seifert + */ + +#include +#include +#include +#include "random.h" +#include "ztimer.h" +#include "container.h" + +#define ENABLE_DEBUG CONFIG_UNICOAP_DEBUG_LOGGING +#include "debug.h" +#include "private.h" + +#define MESSAGING_7252_DEBUG(...) _UNICOAP_PREFIX_DEBUG(".messaging.rfc7252", __VA_ARGS__) +#define PDU_7252_DEBUG(...) _UNICOAP_PREFIX_DEBUG(".pdu.rfc7252", __VA_ARGS__) + +/** @brief Message ID print format */ +#define UNICOAP_MESSAGE_ID_FORMAT "[MID %" PRIu16 "] " + +#define PACKET_7252_DEBUG(packet) \ + DEBUG("<%s %s mid=%" PRIu16 " token=", \ + unicoap_string_from_rfc7252_type(_get_type(packet)), \ + unicoap_string_from_code_class((packet)->message->code), \ + _get_id(packet) \ + ); \ + \ + _UNICOAP_DEBUG_HEX((packet)->properties.token, (packet)->properties.token_length); \ + \ + DEBUG(" code=" UNICOAP_CODE_CLASS_DETAIL_FORMAT " %s payload=(%" PRIuSIZE " bytes) " \ + "options=(%" PRIuSIZE "; %" PRIuSIZE " bytes)>", \ + unicoap_code_class((packet)->message->code), \ + unicoap_code_detail((packet)->message->code), \ + unicoap_string_from_code((packet)->message->code), \ + (packet)->message->payload_size, \ + (packet)->message->options ? (packet)->message->options->option_count : 0, \ + (packet)->message->options ? (packet)->message->options->storage_size : 0 \ + ) + +#define __IOLIST(data, size, next) (iolist_t){ .iol_base = data, .iol_len = size, .iol_next = next } + +typedef struct { + /** + * @brief Timeout that occurs when waiting for an acknowledgement message + */ + unicoap_scheduled_event_t ack_timeout; + + /** + * @brief Remote endpoint + */ + unicoap_endpoint_t endpoint; + +#if IS_USED(MODULE_UNICOAP_DRIVER_DTLS) + unicoap_sock_dtls_session_t dtls_session; +#endif + + /** + * @brief Copy of confirmable message PDU for retransmission + */ + uint8_t* pdu; + + /** + * @brief Size of @ref unicoap_messaging_state_udp_dtls_t.pdu + */ + size_t pdu_size; + + /** @brief Message ID */ + uint16_t id; + + /** + * @brief Number of remaining retransmission attempts of a confirmable (`CON`) message + */ + uint8_t remaining_retransmissions : 5; + + bool is_used : 1; +} _transmission_t; + +typedef struct { + /** + * @brief Message ID unicoap is going to use for the next outbound request or response + * + * May be accessed from user thread (client request) or thread running unicoap processing loop + * for inbound messages. + */ + atomic_uint next_message_id; + +#if CONFIG_UNICOAP_CARBON_COPIES_MAX > 0 + /** @brief Copy of PDU sent, used for retransmission and deduplication */ + uint8_t carbon_copies[CONFIG_UNICOAP_CARBON_COPIES_MAX][CONFIG_UNICOAP_PDU_SIZE_MAX]; +#endif + +#if CONFIG_UNICOAP_RFC7252_TRANSMISSIONS_MAX > 0 + _transmission_t transmissions[CONFIG_UNICOAP_RFC7252_TRANSMISSIONS_MAX]; +#endif +} _state_t; + +static inline void* _transmission_get_session(_transmission_t* transmission) +{ +#if IS_USED(MODULE_UNICOAP_DRIVER_DTLS) + return transmission->endpoint.proto == UNICOAP_PROTO_DTLS ? &transmission->dtls_session : NULL; +#else + (void)transmission; + return NULL; +#endif +} + +static inline void _transmission_set_session(_transmission_t* transmission, + const unicoap_sock_dtls_session_t* dtls_session) +{ +#if IS_USED(MODULE_UNICOAP_DRIVER_DTLS) + transmission->dtls_session = *dtls_session; +#else + (void)transmission; + (void)dtls_session; +#endif +} + +static inline uint16_t _get_id(const unicoap_packet_t* packet) +{ + return packet->properties.rfc7252.id; +} + +static inline const uint16_t* _get_id_ptr(const unicoap_packet_t* packet) +{ + return &packet->properties.rfc7252.id; +} + +static inline void _set_id(unicoap_packet_t* packet, uint16_t id) +{ + packet->properties.rfc7252.id = id; +} + +static inline unicoap_rfc7252_message_type_t _get_type(const unicoap_packet_t* packet) +{ + return packet->properties.rfc7252.type; +} + +static inline void _set_type(unicoap_packet_t* packet, unicoap_rfc7252_message_type_t type) +{ + packet->properties.rfc7252.type = type; +} + +static inline const unicoap_endpoint_t* _transmission_get_endpoint(const _transmission_t* transmission) { + return &transmission->endpoint; +} + +static _state_t _state = { 0 }; + +int unicoap_init_rfc7252_common(event_queue_t* queue) +{ + (void)queue; + atomic_init(&_state.next_message_id, (unsigned int)random_uint32()); + return 0; +} + +int unicoap_deinit_rfc7252_common(event_queue_t* queue) +{ + (void)queue; + memset(&_state, 0, sizeof(_state)); + return 0; +} + +static uint16_t _generate_message_id(void) +{ + return (uint16_t)atomic_fetch_add(&_state.next_message_id, 1); +} + +/* MARK: - (Re-)Transmissions */ + +static uint8_t* _carbon_copy_alloc_unsafe(void) +{ +#if CONFIG_UNICOAP_CARBON_COPIES_MAX > 0 + /* Find empty slot in list of open requests. */ + for (int i = 0; i < (int)ARRAY_SIZE(_state.carbon_copies); i += 1) { + /* Will never be zero due to CoAP version 1 bit. Zero indicates 'free' buffer */ + if (*_state.carbon_copies[i] == 0) { + /* Claim that buffer by setting first byte to 0xff. Later, we adjust that. */ + *_state.carbon_copies[i] = 0xff; + return _state.carbon_copies[i]; + } + } +#endif + MESSAGING_7252_DEBUG("no space to alloc PDU copy\n"); + return NULL; +} + +static uint8_t* _carbon_copy_alloc(void) +{ + unicoap_state_lock(); + uint8_t* carbon_copy = _carbon_copy_alloc_unsafe(); + unicoap_state_unlock(); + return carbon_copy; +} + +static inline void _carbon_copy_free(uint8_t* carbon_copy) +{ + /* Freeing does not require lock. We have sole access until the following line. */ + *carbon_copy = 0; +} + +static inline _transmission_t* _transmission_alloc_unsafe(void) +{ +#if CONFIG_UNICOAP_RFC7252_TRANSMISSIONS_MAX > 0 + /* Find empty slot in list of open requests. */ + for (int i = 0; i < (int)ARRAY_SIZE(_state.transmissions); i += 1) { + if (!_state.transmissions[i].is_used) { + _state.transmissions[i].is_used = true; + return &_state.transmissions[i]; + } + } +#endif + MESSAGING_7252_DEBUG("no space to alloc transmission state\n"); + return NULL; +} + +static _transmission_t* _transmission_create(const unicoap_endpoint_t* endpoint, + unicoap_packet_t* packet) +{ + assert(packet); + + unicoap_state_lock(); + _transmission_t* transmission = _transmission_alloc_unsafe(); + unicoap_state_unlock(); + if (!transmission) { + return NULL; + } + transmission->id = _get_id(packet); + + /* need to store endpoint by value. transmission may outlive endpoint's lifetime. */ + transmission->endpoint = *endpoint; /* use compiler-built-in copy primitive */ + + /* transmission stores session by value for retransmissions. + * packet stores session by reference. + * if we already have a session, + * reuse that session here. If we don't have one, the DTLS driver will _initialize_ one. + * we still have to store one by value */ + if (_packet_get_dtls_session(packet)) { + /* got session, copy into transmission for later retransmission */ + _transmission_set_session(transmission, _packet_get_dtls_session(packet)); + } + else { + /* transmission is memset' to all zero in _transmission_free, + * thus DTLS sessions stored by value is, too. + * We didn't get an existing session (likely a client request, so no session exists), + * so give the transport layer a transmission to initialize (remember, memset' to 0) + * This also has the benefit of not needing to copy the sessions into the transmission + * afterwards, because the transport layer already initialized that memory region. + * Sounds more complicated than it is. */ + _packet_set_dtls_session(packet, _transmission_get_session(transmission)); + } + + return transmission; +} + +static _transmission_t* _transmission_find(const unicoap_endpoint_t* endpoint, uint16_t id) +{ + (void)endpoint; + (void)id; +#if CONFIG_UNICOAP_RFC7252_TRANSMISSIONS_MAX > 0 + for (int i = 0; i < (int)ARRAY_SIZE(_state.transmissions); i += 1) { + _transmission_t* transmission = &_state.transmissions[i]; + if (transmission->is_used && + transmission->id == id && + unicoap_endpoint_is_equal(&transmission->endpoint, endpoint) + ) { + return transmission; + } + } +#endif + return NULL; +} + +static inline void _transmission_free(_transmission_t* transmission) +{ + MESSAGING_7252_DEBUG(UNICOAP_MESSAGE_ID_FORMAT "transmission ended\n", transmission->id); + if (transmission->pdu) { + _carbon_copy_free(transmission->pdu); + } + unicoap_event_cancel(&transmission->ack_timeout); + memset(transmission, 0, sizeof(_transmission_t)); + /* DTLS session gets purged automatically after a period of time. This avoids successive + * session establishments. */ +} + +static int _sendv(iolist_t* list, const unicoap_endpoint_t* remote, const unicoap_endpoint_t* local, + const unicoap_sock_dtls_session_t* dtls_session) +{ + (void)list; + (void)local; + (void)dtls_session; + assert(list); + assert(remote); + + switch (remote->proto) { +#if IS_USED(MODULE_UNICOAP_DRIVER_UDP) + case UNICOAP_PROTO_UDP: { + extern int unicoap_transport_sendv_udp(iolist_t * iolist, const sock_udp_ep_t* remote, + const sock_udp_ep_t* local); + return unicoap_transport_sendv_udp( + list, unicoap_endpoint_get_udp((unicoap_endpoint_t*)remote), + local ? unicoap_endpoint_get_udp((unicoap_endpoint_t*)local) : NULL); + } +#endif + +#if IS_USED(MODULE_UNICOAP_DRIVER_DTLS) + case UNICOAP_PROTO_DTLS: { + extern int unicoap_transport_sendv_dtls(iolist_t * iolist, const sock_udp_ep_t* remote, + const sock_udp_ep_t* local, + unicoap_sock_dtls_session_t* session); + return unicoap_transport_sendv_dtls( + list, unicoap_endpoint_get_udp((unicoap_endpoint_t*)remote), + local ? unicoap_endpoint_get_udp((unicoap_endpoint_t*)local) : NULL, + (unicoap_sock_dtls_session_t*)dtls_session); + } +#endif + + default: + MESSAGING_7252_DEBUG("unsupported protocol number\n"); + unicoap_assist_emit_diagnostic_missing_driver(remote->proto); + return -ENOTSUP; + } +} + +static inline int _send(uint8_t* pdu, size_t size, const unicoap_endpoint_t* remote, + const unicoap_endpoint_t* local, + const unicoap_sock_dtls_session_t* dtls_session) +{ + iolist_t list = __IOLIST(pdu, size, NULL); + return _sendv(&list, remote, local, dtls_session); +} + +static ssize_t _build_and_send_pdu(unicoap_packet_t* packet, uint8_t* carbon_copy) +{ + assert(packet); + assert(packet->message); + assert(packet->remote); + MESSAGING_7252_DEBUG("sending "); + PACKET_7252_DEBUG(packet); + DEBUG("\n"); + + ssize_t res = 0; + uint8_t header[UNICOAP_HEADER_SIZE_MAX]; + iolist_t lists[UNICOAP_PDU_IOLIST_COUNT]; + ssize_t size = 0; + + /* By default, we build a vector of CoAP PDU chunks. This saves another copy, as the network + * backend is going to copy anyway. However, if we need a carbon copy to later retransmit + * the very same message, it makes no sense to write the PDU into a vector, and to then copy all + * vector chunks into a larger buffer. In this case, we write the PDU directly into a contiguous + * buffer, the carbon copy buffer. To save space, we use the same _sendv function. + * In the contiguous case, that vector has only one element. This is fine as we allocate + * the iolists anyway. If we wanted to avoid this, we would need to dynamically allocate memory. + */ + + if (carbon_copy) { + if ((size = unicoap_pdu_build_rfc7252(carbon_copy, sizeof(_state.carbon_copies[0]), + packet->message, &packet->properties)) < 0) { + lists[0].iol_base = NULL; + lists[0].iol_len = 0; + return size; + } + + lists[0].iol_base = carbon_copy; + lists[0].iol_len = size; + lists[0].iol_next = NULL; + } else { + if ((size = unicoap_pdu_buildv_rfc7252(header, sizeof(header), packet->message, + &packet->properties, lists)) < 0) { + return size; + } + } + + if ((res = _sendv(&lists[0], packet->remote, packet->local, _packet_get_dtls_session(packet))) < 0) { + return res; + } + return size; +} + +static inline void _handle_ack(const unicoap_endpoint_t* remote, uint16_t id) +{ + MESSAGING_7252_DEBUG(UNICOAP_MESSAGE_ID_FORMAT "received ACK, ", id); + + _transmission_t* transmission = _transmission_find(remote, id); + if (transmission) { + DEBUG("stopping retransmission\n"); + _transmission_free(transmission); + } + else { + DEBUG("no message with ID, ignoring\n"); + } +} + +static void _handle_reset(const unicoap_endpoint_t* remote, uint16_t id) +{ + MESSAGING_7252_DEBUG(UNICOAP_MESSAGE_ID_FORMAT "received RST", id); + + _transmission_t* transmission = _transmission_find(remote, id); + + if (transmission) { + DEBUG("\n"); + _transmission_free(transmission); + } + else { + DEBUG(", no message with ID, ignoring\n"); + } +} + +static void _on_ack_timeout(unicoap_scheduled_event_t* _event) +{ + _transmission_t* transmission = container_of(_event, _transmission_t, ack_timeout); + assert(transmission); + assert(transmission->pdu); + assert(transmission->pdu_size > 0); + int res = 0; + + if (transmission->remaining_retransmissions == 0) { + MESSAGING_7252_DEBUG(UNICOAP_MESSAGE_ID_FORMAT + "ACK timeout, max retransmissions exceeded\n", transmission->id); + res = -ETIMEDOUT; + goto error; + } + + /* reduce retries remaining, double timeout and resend */ + transmission->remaining_retransmissions -= 1; + unsigned int i = CONFIG_UNICOAP_RETRANSMISSIONS_MAX - transmission->remaining_retransmissions; + uint32_t duration = (uint32_t)CONFIG_UNICOAP_TIMEOUT_ACK_MS << i; + if (CONFIG_UNICOAP_RANDOM_FACTOR_1000 > 1000) { + duration = random_uint32_range(duration, UNICOAP_TIMEOUT_ACK_RANGE_UPPER << i); + } + + MESSAGING_7252_DEBUG(UNICOAP_MESSAGE_ID_FORMAT + "ACK timeout, retransmitting now, waiting %" PRIu32 " ms, " + "%u attempts remaining\n", + transmission->id, duration, transmission->remaining_retransmissions); + + if ((res = _send(transmission->pdu, transmission->pdu_size, + _transmission_get_endpoint(transmission), NULL, + _transmission_get_session(transmission))) < 0) { + goto error; + } + + unicoap_event_reschedule(&transmission->ack_timeout, duration); + return; + +error: + /* TODO: Client: Signal failure to application waiting for response */ + MESSAGING_7252_DEBUG("error while on ACK timeout\n"); + return; +} + +/* MARK: - Empty Messages */ + +static int _send_empty_message(unicoap_packet_t* packet) +{ + packet->message->code = UNICOAP_CODE_EMPTY; + packet->message->options = NULL; + packet->message->payload = NULL; + packet->message->payload_size = 0; + + packet->properties.token = NULL; + packet->properties.token_length = 0; + + return (int)_build_and_send_pdu(packet, NULL); +} + +static inline int _acknowledge(unicoap_packet_t* packet) +{ + MESSAGING_7252_DEBUG(UNICOAP_MESSAGE_ID_FORMAT "sending empty ACK\n", _get_id(packet)); + _set_type(packet, UNICOAP_TYPE_ACK); + return _send_empty_message(packet); +} + +static inline int _reset(unicoap_packet_t* packet) +{ + MESSAGING_7252_DEBUG(UNICOAP_MESSAGE_ID_FORMAT "sending RST\n", _get_id(packet)); + _set_type(packet, UNICOAP_TYPE_RST); + return _send_empty_message(packet); +} + +/* MARK: - Message Processing */ + +#define _IGNORED (-7252) + +static int _process_messaging_layer(unicoap_packet_t* packet) +{ + assert(packet); + assert(packet->remote); + assert(packet->message); + + MESSAGING_7252_DEBUG("received "); + PACKET_7252_DEBUG(packet); + DEBUG("\n"); + + unicoap_message_t* message = packet->message; + + /* RFC 7252 (CoAP), Section 4.2 + * https://datatracker.ietf.org/doc/html/rfc7252#section-4.2 + * Rejecting an + * Acknowledgement or Reset message (including the case where the + * Acknowledgement carries a request or a code with a reserved class, or + * the Reset message is not Empty) is effected by silently ignoring it. + * More generally, recipients of Acknowledgement and Reset messages MUST + * NOT respond with either Acknowledgement or Reset messages. + */ + if (message->code == UNICOAP_CODE_EMPTY && message->payload_size != 0) { + MESSAGING_7252_DEBUG("received code 0.00, but message not empty, ignoring\n"); + return -EPROTO; + } + + switch (_get_type(packet)) { + case UNICOAP_TYPE_ACK: + _handle_ack(packet->remote, _get_id(packet)); + if (message->code == UNICOAP_CODE_EMPTY) { + return _IGNORED; + } + + else if (!unicoap_message_code_is_response(message->code)) { + MESSAGING_7252_DEBUG( + UNICOAP_MESSAGE_ID_FORMAT + "received ACK, expected code 0.00 or response code, got " + UNICOAP_CODE_CLASS_DETAIL_FORMAT + ", ignoring\n", + _get_id(packet), + unicoap_code_class(message->code), + unicoap_code_detail(message->code) + ); + return -EPROTO; + } + break; + + case UNICOAP_TYPE_RST: + _handle_reset(packet->remote, _get_id(packet)); + if (message->code != UNICOAP_CODE_EMPTY) { + MESSAGING_7252_DEBUG( + UNICOAP_MESSAGE_ID_FORMAT + "received RST, expected 0.00, got " + UNICOAP_CODE_CLASS_DETAIL_FORMAT + ", ignoring\n", + _get_id(packet), + unicoap_code_class(message->code), + unicoap_code_detail(message->code) + ); + return -EPROTO; + } + + /* if this RST is due to the client not being interested + * in receiving observe notifications anymore, deregister */ + /* TODO: Observe: Deregister by message ID */ + return _IGNORED; + + case UNICOAP_TYPE_CON: + if (message->code == UNICOAP_CODE_EMPTY) { + MESSAGING_7252_DEBUG(UNICOAP_MESSAGE_ID_FORMAT + "received empty CON (ping), sending RST (pong)\n", + _get_id(packet)); + _reset(packet); + return _IGNORED; + } + break; + + case UNICOAP_TYPE_NON: + if (message->code == UNICOAP_CODE_EMPTY) { + MESSAGING_7252_DEBUG(UNICOAP_MESSAGE_ID_FORMAT "received empty NON, ignoring\n", _get_id(packet)); + return _IGNORED; + } + break; + } + return 0; +} + +int unicoap_messaging_process_rfc7252(const uint8_t* pdu, size_t size, bool truncated, + unicoap_packet_t* packet) +{ + unicoap_options_t options = { 0 }; + unicoap_message_t message = { .options = &options }; + packet->message = &message; + + int res = 0; + + if ((res = unicoap_pdu_parse_rfc7252((uint8_t*)pdu, size, &message, &packet->properties)) < 0) { + PDU_7252_DEBUG("parsing error: %i (%s)\n", res, strerror(-res)); + return res; + } + + if ((res = _process_messaging_layer(packet)) < 0) { + if (res != _IGNORED) { + MESSAGING_7252_DEBUG("messaging error: %i (%s)\n", res, strerror(-res)); + } + return res; + } + + /* Successfully parsed message, i.e., truncated message could be parsed + * to a certain degree (cutoff directly after an option or somewhere in + * payload). The exchange (request/response) layer will discard that + * message anyway, but in case of truncated requests, the next layer will + * at least send a Size1 response indicating the request was too large + * and should've been transmitted block-wise. + * + * If the exchange layer is indeed incapable of handling a truncated + * message, we'll be notified via the event callback. + */ + unicoap_exchange_arg_t arg; + unicoap_messaging_flags_t flags; + + switch ((res = unicoap_exchange_preprocess(packet, &flags, &arg, truncated))) { + case UNICOAP_PREPROCESSING_SUCCESS_REQUEST: + break; + + /* This is a response the exchange layer expected. + * We may have attached a transmission, free that transmission here. */ + case UNICOAP_PREPROCESSING_SUCCESS_RESPONSE: { + _transmission_t* transmission = _transmission_find(packet->remote, _get_id(packet)); + if (transmission) { + _transmission_free(transmission); + + if (_get_type(packet) == UNICOAP_TYPE_CON) { + MESSAGING_7252_DEBUG(UNICOAP_MESSAGE_ID_FORMAT + "sending empty ACK for expected response\n", _get_id(packet)); + + /* We're going to need the response below, don't override it. */ + unicoap_message_t* message = packet->message; + unicoap_message_t m = *packet->message; + packet->message = &m; + _acknowledge(packet); + packet->message = message; + } + } + break; + } + + /* From RFC 7641, Section 3.6. Cancellation: + * A client that is no longer interested in receiving notifications for + * a resource can simply "forget" the observation. When the server then + * sends the next notification, the client will not recognize the token + * in the message and thus will return a Reset message. This causes the + * server to remove the associated entry from the list of observers. + * The entries in lists of observers are effectively "garbage collected" + * by the server. */ + case UNICOAP_PREPROCESSING_ERROR_TRUNCATED: + case UNICOAP_PREPROCESSING_ERROR_NOTIFICATION_UNEXPECTED: + /* In the case of a non-confirmable notification, + * rejecting the message with a Reset message is OPTIONAL. */ + goto reset; + + case UNICOAP_PREPROCESSING_ERROR_RESPONSE_UNEXPECTED: + MESSAGING_7252_DEBUG(UNICOAP_MESSAGE_ID_FORMAT "received unknown response\n", _get_id(packet)); + /* From RFC 7252, Section 4.3 Messages Transmitted without Reliability + * A Non-confirmable message always carries either a request or response and + * MUST NOT be Empty. A Non-confirmable message MUST NOT be + * acknowledged by the recipient. A recipient MUST reject the message + * if it lacks context to process the message properly, including the + * case where the message is Empty, uses a code with a reserved class + * (1, 6, or 7), or has a message format error. Rejecting a Non- + * confirmable message MAY involve sending a matching Reset message, and + * apart from the Reset message the rejected message MUST be silently + * ignored. */ + /* unicoap chooses not to send a reset message for NONs */ + if (_get_type(packet) == UNICOAP_TYPE_CON) { + /* From RFC 7252, Section 4.2 Messages Transmitted Reliably + * Confirmable message + * always carries either a request or response, unless it is used only + * to elicit a Reset message, in which case it is Empty. A recipient + * MUST either (a) acknowledge a Confirmable message with an + * Acknowledgement message or (b) reject the message if the recipient + * lacks context to process the message properly, [...] */ + goto reset; + } + return -EPROTO; + + default: + if (res < 0) { + return res; + } + break; + } + + /* processor is prewarmed, let's throw the message in the processor */ + if ((res = unicoap_exchange_process(packet, arg)) < 0) { + return res; + } + + return 0; + +reset: + /* using gotos here to avoid overhead of multiple identical function calls */ + _reset(packet); + return -EPROTO; +} + +/* MARK: - Message Transmission */ + +static void _format_separate(unicoap_packet_t* packet, unicoap_messaging_flags_t flags) +{ + /* requests and separate responses */ + _set_id(packet, _generate_message_id()); + _set_type(packet, + flags & UNICOAP_MESSAGING_FLAG_RELIABLE ? UNICOAP_TYPE_CON : UNICOAP_TYPE_NON); +} + +int unicoap_messaging_send_rfc7252(unicoap_packet_t* packet, unicoap_messaging_flags_t flags) +{ + assert(packet); + assert(packet->remote); + int res = 0; + uint8_t* carbon_copy = NULL; + _transmission_t* transmission = NULL; + + if (packet->properties.is_notification) { + /* notification, always use separate response style */ + _format_separate(packet, flags); + } + else if (unicoap_message_code_is_response(packet->message->code) && + /* Message properties in the packet are still those of the request (ID, token, type) */ + _get_type(packet) == UNICOAP_TYPE_CON) { + /* piggybacked response, immediate response style */ + _set_type(packet, UNICOAP_TYPE_ACK); + } + else { + _format_separate(packet, flags); + } + + switch (_get_type(packet)) { + case UNICOAP_TYPE_NON: + /* TODO: Client: remember message ID and watch out for RSTs */ + break; + + case UNICOAP_TYPE_CON: + /* If we send a confirmable message, we always need a transmission + * for retransmitting the original PDU and for tracking ACK timeouts + * (exponential back-off mechanism). */ + transmission = _transmission_create(packet->remote, packet); + + /* need a carbon copy buffer for storing the PDU copy for retransmission */ + if (!(carbon_copy = _carbon_copy_alloc())) { + res = -ENOBUFS; + goto error; + } + + transmission->remaining_retransmissions = CONFIG_UNICOAP_RETRANSMISSIONS_MAX; + transmission->pdu = carbon_copy; + + uint32_t duration = CONFIG_UNICOAP_TIMEOUT_ACK_MS; + if (CONFIG_UNICOAP_RANDOM_FACTOR_1000 > 1000) { + duration = random_uint32_range(duration, UNICOAP_TIMEOUT_ACK_RANGE_UPPER); + } + unicoap_event_schedule(&transmission->ack_timeout, _on_ack_timeout, duration); + break; + + case UNICOAP_TYPE_ACK: + break; + + default: + MESSAGING_7252_DEBUG("attempt to send unexpected message %s", + unicoap_string_from_rfc7252_type(_get_type(packet))); + assert(false); + break; + } + + if ((res = (int)_build_and_send_pdu(packet, carbon_copy)) < 0) { + goto error; + } + + if (transmission) { + MESSAGING_7252_DEBUG("created \n", res); + transmission->pdu_size = res; + } + return 0; + +error: + MESSAGING_7252_DEBUG("sending failed\n"); + if (transmission) { + _transmission_free(transmission); + } + return res; +} + +void unicoap_messaging_print_rfc7252_state(void) +{ +#if ENABLE_DEBUG + printf("\n\t- RFC 7252 transmissions (%" PRIuSIZE " total):\n", + (size_t)CONFIG_UNICOAP_RFC7252_TRANSMISSIONS_MAX); + +# if CONFIG_UNICOAP_RFC7252_TRANSMISSIONS_MAX > 0 + for (size_t i = 0; i < ARRAY_SIZE(_state.transmissions); i += 1) { + _transmission_t* transmission = &_state.transmissions[i]; + printf("\t\t- transmission #%" PRIuSIZE "\n", i); + + if (!transmission->is_used) { + continue; + } + + printf("\t\t\t- MID=%" PRIu16 "\n", transmission->id); + printf("\t\t\t- remaining_retransmissions=%u\n", transmission->remaining_retransmissions); + printf("\t\t\t- pdu=\n", transmission->pdu); + printf("\t\t\t- pdu_size=%" PRIuSIZE "\n", transmission->pdu_size); + } +#endif /* CONFIG_UNICOAP_RFC7252_TRANSMISSIONS_MAX > 0 */ + + printf("\n\t- RFC 7252 carbon copies (%" PRIuSIZE " total):\n", + (size_t)CONFIG_UNICOAP_CARBON_COPIES_MAX); + +# if CONFIG_UNICOAP_CARBON_COPIES_MAX > 0 + for (size_t i = 0; i < ARRAY_SIZE(_state.carbon_copies); i += 1) { + printf("\t\t- carbon_copy #%" PRIuSIZE " capacity=%" PRIuSIZE "B used=%u\n", i, + (size_t)CONFIG_UNICOAP_PDU_SIZE_MAX, *_state.carbon_copies[i] != 0); + } +# endif /* CONFIG_UNICOAP_CARBON_COPIES_MAX > 0 */ +#endif /* ENABLE_DEBUG */ +} diff --git a/sys/net/application_layer/unicoap/drivers/rfc7252/dtls/Makefile b/sys/net/application_layer/unicoap/drivers/rfc7252/dtls/Makefile index 501575d6d413..d2de99d8d26c 100644 --- a/sys/net/application_layer/unicoap/drivers/rfc7252/dtls/Makefile +++ b/sys/net/application_layer/unicoap/drivers/rfc7252/dtls/Makefile @@ -1,3 +1,3 @@ -MODULE = unicoap_driver_udp +MODULE = unicoap_driver_dtls include $(RIOTBASE)/Makefile.base diff --git a/sys/net/application_layer/unicoap/drivers/rfc7252/dtls/doc.md b/sys/net/application_layer/unicoap/drivers/rfc7252/dtls/doc.md index 891f540a54ed..bb182f94b9f8 100644 --- a/sys/net/application_layer/unicoap/drivers/rfc7252/dtls/doc.md +++ b/sys/net/application_layer/unicoap/drivers/rfc7252/dtls/doc.md @@ -12,21 +12,13 @@ Include these headers required for managing DTLS credentials. #include "net/dsm.h" ``` -Then, in your application, call @ref sock_dtls_add_credential to add a DTLS credential. - - - @see @ref unicoap_rfc7252_message_type_t This is the dependency graph of this driver: diff --git a/sys/net/application_layer/unicoap/drivers/rfc7252/dtls/transport.c b/sys/net/application_layer/unicoap/drivers/rfc7252/dtls/transport.c new file mode 100644 index 000000000000..cf3d30c727bc --- /dev/null +++ b/sys/net/application_layer/unicoap/drivers/rfc7252/dtls/transport.c @@ -0,0 +1,337 @@ +/* + * Copyright (C) 2024-2025 Carl Seifert + * Copyright (C) 2024-2025 TU Dresden + * + * This file is subject to the terms and conditions of the GNU Lesser General + * Public License v2.1. See the file LICENSE in the top level directory for + * more details. + */ + +/** + * @file + * @ingroup net_unicoap_drivers_dtls + * @brief Transport implementation of CoAP over DTLS driver + * @author Carl Seifert + */ + +#include +#include +#include "event.h" +#include "net/sock.h" +#include "net/sock/dtls.h" +#include "net/sock/async/types.h" +#include "net/sock/async/event.h" +#include "net/credman.h" +#include "net/dsm.h" +#include "net/unicoap/transport.h" + +#define ENABLE_DEBUG CONFIG_UNICOAP_DEBUG_LOGGING +#include "debug.h" +#include "private.h" + +#define DTLS_DEBUG(...) _UNICOAP_PREFIX_DEBUG(".transport.dtls", __VA_ARGS__) + +UNICOAP_DECL_RECEIVER_STORAGE_EXTERN; + +#define SOCK_DTLS_CLIENT_TAG (2) +static sock_udp_t _dtls_base_socket; +static sock_dtls_t _dtls_socket; +static kernel_pid_t _dtls_auth_waiting_thread; + +unicoap_scheduled_event_t _dtls_session_triage_event = { 0 }; + +extern int unicoap_messaging_process_rfc7252(const uint8_t* pdu, size_t size, bool truncated, + unicoap_packet_t* packet); + +/* Timeout function to free a session when too many session slots are occupied */ +static void _dtls_session_triage(unicoap_scheduled_event_t* event) +{ + (void)event; + sock_dtls_session_t session; + if (dsm_get_num_available_slots() < CONFIG_UNICOAP_DTLS_MINIMUM_AVAILABLE_SESSIONS) { + if (dsm_get_least_recently_used_session(&_dtls_socket, &session) != -1) { + DTLS_DEBUG("session triage: freeing least recently used session\n"); + dsm_remove(&_dtls_socket, &session); + sock_dtls_session_destroy(&_dtls_socket, &session); + } + } +} + +static void _dtls_on_event(sock_dtls_t* sock, sock_async_flags_t type, void* arg) +{DTLS_DEBUG("received event from network backend %x\n", type); + (void)arg; + sock_dtls_session_t session = { 0 }; + + if (type & SOCK_ASYNC_CONN_RECV) { + DTLS_DEBUG("establishing session\n"); + ssize_t res = sock_dtls_recv(sock, &session, unicoap_receiver_buffer, + sizeof(unicoap_receiver_buffer), + CONFIG_UNICOAP_DTLS_HANDSHAKE_TIMEOUT_MS * US_PER_MS); + + if (res != -SOCK_DTLS_HANDSHAKE) { + DTLS_DEBUG("could not establish DTLS session: %" PRIiSIZE " (%s)\n", res, + strerror(-(int)res)); + goto error; + } + + dsm_state_t prev_state = dsm_store(sock, &session, SESSION_STATE_ESTABLISHED, false); + + /* If session is already stored and the state was SESSION_STATE_HANDSHAKE + * before, the handshake has been initiated internally by a client request + * and another thread is waiting for the handshake. Send message to the + * waiting thread to inform about established session */ + if (prev_state == SESSION_STATE_HANDSHAKE) { + msg_t msg = { .type = DTLS_EVENT_CONNECTED }; + msg_send(&msg, _dtls_auth_waiting_thread); + } + else if (prev_state == NO_SPACE) { + /* No space in session management. Should not happen. If it occurs, + * we lost track of sessions. */ + DTLS_DEBUG("no space in session management\n"); + goto error; + } + + /* If not enough session slots left: set timeout to free session. */ + if (dsm_get_num_available_slots() < CONFIG_UNICOAP_DTLS_MINIMUM_AVAILABLE_SESSIONS) { + DTLS_DEBUG("session triage: fewer than %u session slots available in session mgmt," + " limiting session lifespan to %" PRIu32 " ms\n", + (unsigned int)CONFIG_UNICOAP_DTLS_MINIMUM_AVAILABLE_SESSIONS, + (uint32_t)CONFIG_UNICOAP_DTLS_MINIMUM_AVAILABLE_SESSIONS_TIMEOUT_MS); + unicoap_event_schedule(&_dtls_session_triage_event, _dtls_session_triage, + CONFIG_UNICOAP_DTLS_MINIMUM_AVAILABLE_SESSIONS_TIMEOUT_MS); + } + } + + if (type & SOCK_ASYNC_CONN_FIN) { + DTLS_DEBUG("closing session\n"); + if (sock_dtls_get_event_session(sock, &session)) { + /* Session is already destroyed, only remove it from session mgmt. */ + dsm_remove(sock, &session); + } + else { + DTLS_DEBUG("session was closed, but the corresponding session " + "could not be retrieved from the socket\n"); + return; + } + + DTLS_DEBUG("session ended, removing associated endpoint state\n"); + + unicoap_endpoint_t endpoint = { .proto = UNICOAP_PROTO_DTLS }; + sock_dtls_session_get_udp_ep(&session, unicoap_endpoint_get_dtls(&endpoint)); + unicoap_exchange_release_endpoint_state(&endpoint); + /* It is safe to ignore the result of exchange_release_endpoint state as this logic follows + * a best-effort philosophy. */ + } + + if (type & SOCK_ASYNC_CONN_RDY) { + DTLS_DEBUG("connection ready\n"); + } + + if (type & SOCK_ASYNC_MSG_RECV) { + DTLS_DEBUG("received encrypted datagram\n"); + sock_dtls_aux_rx_t aux_rx = { + .flags = IS_ACTIVE(CONFIG_UNICOAP_GET_LOCAL_ENDPOINTS) ? SOCK_AUX_GET_LOCAL : 0, + }; + + void* pdu = NULL; + void* buffer_ctx = NULL; + + ssize_t received = sock_dtls_recv_buf_aux(sock, &session, &pdu, &buffer_ctx, 0, &aux_rx); + if (received < 0) { + DTLS_DEBUG("recv failure: %" PRIdSIZE "\n", received); + return; + } + /* FIXME: sock_dtls_recv_buf_aux fails on second read due to sock->buf_ctx not being NULL */ + // sock->buf_ctx = NULL; + if (received == 0) { + return; + } + + assert(pdu); + + unicoap_endpoint_t remote = { .proto = UNICOAP_PROTO_DTLS }; + sock_dtls_session_get_udp_ep(&session, unicoap_endpoint_get_dtls(&remote)); + + unicoap_packet_t packet = { .remote = &remote, .dtls_session = &session }; + +#if IS_ACTIVE(CONFIG_UNICOAP_GET_LOCAL_ENDPOINTS) + unicoap_endpoint_t local = { .proto = UNICOAP_PROTO_DTLS }; + packet.local = &local; + + if (aux_rx.local.family != AF_UNSPEC) { + *unicoap_endpoint_get_udp(&local) = aux_rx.local; + } +#endif + + /* Truncated DTLS messages would already have gotten lost at verification */ + unicoap_messaging_process_rfc7252((uint8_t*)pdu, received, false, &packet); + } + + return; + +error: + sock_dtls_session_destroy(sock, &session); +} + +static ssize_t _dtls_authenticate(const sock_udp_ep_t* remote, sock_dtls_session_t* session, + uint32_t timeout) +{ + assert(session); + int res; + + /* prepare session */ + sock_dtls_session_set_udp_ep(session, remote); + dsm_state_t session_state = dsm_store(&_dtls_socket, session, SESSION_STATE_HANDSHAKE, true); + if (session_state == SESSION_STATE_ESTABLISHED) { + DTLS_DEBUG("auth: session already established\n"); + return 0; + } + if (session_state == NO_SPACE) { + DTLS_DEBUG("auth: no space in DTLS session mgmt\n"); + return -ENOBUFS; + } + + /* start handshake */ + _dtls_auth_waiting_thread = thread_getpid(); + res = sock_dtls_session_init(&_dtls_socket, remote, session); + if (res == 0) { + /* session already exists */ + _dtls_auth_waiting_thread = -1; + return res; + } + + msg_t msg; + bool is_timed_out = false; + do { + uint32_t start = ztimer_now(ZTIMER_MSEC); + res = ztimer_msg_receive_timeout(ZTIMER_MSEC, &msg, timeout); + + /* ensure whole timeout time for the case we receive other messages than + * DTLS_EVENT_CONNECTED */ + if (timeout != SOCK_NO_TIMEOUT) { + uint32_t diff = (ztimer_now(ZTIMER_MSEC) - start); + timeout = (diff > timeout) ? 0 : timeout - diff; + is_timed_out = (res < 0) || (timeout == 0); + } + } while (!is_timed_out && (msg.type != DTLS_EVENT_CONNECTED)); + + if (is_timed_out && (msg.type != DTLS_EVENT_CONNECTED)) { + DTLS_DEBUG("auth: timeout\n"); + dsm_remove(&_dtls_socket, session); + sock_dtls_session_destroy(&_dtls_socket, session); + return -ENOTCONN; + } + return 0; +} + +int unicoap_transport_sendv_dtls(iolist_t* iolist, const sock_udp_ep_t* remote, + const sock_udp_ep_t* local, sock_dtls_session_t* session) +{ + assert(remote); + ssize_t res = 0; + + if (!session) { + if ((res = _dtls_authenticate(remote, session, CONFIG_UNICOAP_DTLS_HANDSHAKE_TIMEOUT_MS)) + < 0) { + return res; + } + } + /* prepare session */ + sock_dtls_session_set_udp_ep(session, remote); + dsm_state_t session_state = dsm_store(&_dtls_socket, session, SESSION_STATE_HANDSHAKE, true); + if (session_state == NO_SPACE) { + return -1; + } + + DTLS_DEBUG("started sending\n"); + + if (unlikely(local)) { + sock_dtls_aux_tx_t aux_tx = { .flags = SOCK_AUX_SET_LOCAL, .local = *local }; + res = sock_dtls_sendv_aux(&_dtls_socket, session, iolist, SOCK_NO_TIMEOUT, &aux_tx); + } + else { + res = sock_dtls_sendv_aux(&_dtls_socket, session, iolist, + CONFIG_UNICOAP_DTLS_HANDSHAKE_TIMEOUT_MS * US_PER_MS, NULL); + } + DTLS_DEBUG("done sending\n"); + + switch (res) { + case -EHOSTUNREACH: + case -ENOTCONN: + case 0: + DTLS_DEBUG("DTLS sock not connected or remote unreachable. " + "Destroying session.\n"); + dsm_remove(&_dtls_socket, session); + sock_dtls_session_destroy(&_dtls_socket, session); + break; + default: + /* Temporary error. Keeping the DTLS session */ + break; + } + return 0; +} + +static int _add_socket(event_queue_t* queue, sock_dtls_t* socket, sock_udp_t* base_socket, + sock_udp_ep_t* local) +{ + DTLS_DEBUG("creating DTLS sock, port=%" PRIu16 " if=%" PRIu16 " family=%s\n", local->port, + local->netif, + local->family == AF_INET6 ? "inet6" : (local->family == AF_INET ? "inet" : "?")); + + if (sock_udp_create(base_socket, local, NULL, 0)) { + DTLS_DEBUG("error creating DTLS base (UDP) sock\n"); + return 0; + } + if (sock_dtls_create(socket, base_socket, CREDMAN_TAG_EMPTY, SOCK_DTLS_1_2, + SOCK_DTLS_SERVER) < 0) { + DTLS_DEBUG("error creating DTLS sock\n"); + sock_udp_close(base_socket); + return -1; + } + sock_dtls_event_init(socket, queue, _dtls_on_event, NULL); + + return 0; +} + +int unicoap_init_dtls(event_queue_t* queue) +{ + sock_udp_ep_t local = { + /* FIXME: Once the problems with IPv4/IPv6 dual stack use in RIOT are fixed, adapt these lines + * (and e.g. use AF_UNSPEC) */ +#if defined(SOCK_HAS_IPV6) + .family = AF_INET6, +#elif defined(SOCK_HAS_IPV4) + .family = AF_INET, +#endif + .netif = SOCK_ADDR_ANY_NETIF, + .port = CONFIG_UNICOAP_DTLS_PORT + }; + + return _add_socket(queue, &_dtls_socket, &_dtls_base_socket, &local); +} + +sock_dtls_t* unicoap_transport_dtls_get_socket(void) +{ + return &_dtls_socket; +} + +int unicoap_transport_dtls_add_socket(sock_dtls_t* socket, + sock_udp_t* base_socket, + sock_udp_ep_t* local) { + return _add_socket(sock_dtls_get_async_ctx(&_dtls_socket)->queue, socket, base_socket, local); +} + +int unicoap_transport_dtls_remove_socket(sock_dtls_t* socket) { + sock_udp_t* udp_socket = socket->udp_sock; + sock_dtls_close(socket); + sock_udp_close(udp_socket); + return 0; +} + +int unicoap_deinit_dtls(event_queue_t* queue) +{ + (void)queue; + sock_dtls_close(&_dtls_socket); + sock_udp_close(&_dtls_base_socket); + return 0; +} diff --git a/sys/net/application_layer/unicoap/drivers/rfc7252/udp/doc.md b/sys/net/application_layer/unicoap/drivers/rfc7252/udp/doc.md index f7b92430ac43..7215e62d1dfc 100644 --- a/sys/net/application_layer/unicoap/drivers/rfc7252/udp/doc.md +++ b/sys/net/application_layer/unicoap/drivers/rfc7252/udp/doc.md @@ -5,13 +5,10 @@ Module. Specify `USEMODULE += unicoap_driver_udp` in your application's Makefile. - @see @ref unicoap_rfc7252_message_type_t This is the dependency graph of this driver: diff --git a/sys/net/application_layer/unicoap/drivers/rfc7252/udp/transport.c b/sys/net/application_layer/unicoap/drivers/rfc7252/udp/transport.c new file mode 100644 index 000000000000..48d8b34b3cc0 --- /dev/null +++ b/sys/net/application_layer/unicoap/drivers/rfc7252/udp/transport.c @@ -0,0 +1,194 @@ +/* + * Copyright (C) 2024-2025 Carl Seifert + * Copyright (C) 2024-2025 TU Dresden + * + * This file is subject to the terms and conditions of the GNU Lesser General + * Public License v2.1. See the file LICENSE in the top level directory for + * more details. + */ + +/** + * @file + * @ingroup net_unicoap_drivers_udp + * @brief Transport implementation of CoAP over UDP driver + * @author Carl Seifert + */ + +#include +#include +#include "architecture.h" +#include "net/unicoap/transport.h" +#include "net/sock.h" +#include "net/sock/async/types.h" +#include "net/sock/async/event.h" + +#define ENABLE_DEBUG CONFIG_UNICOAP_DEBUG_LOGGING +#include "debug.h" +#include "private.h" + +#define UDP_DEBUG(...) _UNICOAP_PREFIX_DEBUG(".transport.udp", __VA_ARGS__) + +UNICOAP_DECL_RECEIVER_STORAGE_EXTERN; + +static sock_udp_t _udp_socket; + +extern int unicoap_messaging_process_rfc7252(const uint8_t* pdu, size_t size, bool truncated, + unicoap_packet_t* packet); + +static void _udp_on_event(sock_udp_t* sock, sock_async_flags_t type, void* arg) +{ + (void)arg; + unicoap_endpoint_t remote = { + .proto = UNICOAP_PROTO_UDP, + }; + + if (type & SOCK_ASYNC_MSG_RECV) { + void* stackbuf = NULL; + uint8_t* pdu; + void* buffer_ctx = NULL; + bool truncated = false; + ssize_t received = 0; + + sock_udp_aux_rx_t aux_rx = { + .flags = IS_ACTIVE(CONFIG_UNICOAP_GET_LOCAL_ENDPOINTS) ? SOCK_AUX_GET_LOCAL : 0, + }; + + if (IS_ACTIVE(CONFIG_UNICOAP_SOCK_ZERO_COPY_GUARANTEES)) { + received = sock_udp_recv_buf_aux(sock, &stackbuf, &buffer_ctx, 0, + unicoap_endpoint_get_udp(&remote), &aux_rx); + + if (received < 0) { + UDP_DEBUG("recv failure: %" PRIdSIZE "\n", received); + return; + } + if (received == 0) { + return; + } + assert(stackbuf); + pdu = stackbuf; + } + else { + pdu = unicoap_receiver_buffer; + while (true) { + ssize_t chunk_size = sock_udp_recv_buf_aux( + sock, &stackbuf, &buffer_ctx, 0, unicoap_endpoint_get_udp(&remote), &aux_rx); + if (chunk_size < 0) { + UDP_DEBUG("recv failure: %" PRIdSIZE "\n", chunk_size); + return; + } + if (chunk_size == 0) { + break; + } + + truncated = unicoap_transport_truncate_received((size_t*)&chunk_size, received); + memcpy(&pdu[received], stackbuf, chunk_size); + received += chunk_size; + } + } + + if (received == 0) { + return; + } + + unicoap_packet_t packet = { .remote = &remote }; + +# if IS_ACTIVE(CONFIG_UNICOAP_GET_LOCAL_ENDPOINTS) + unicoap_endpoint_t local = { .proto = UNICOAP_PROTO_UDP }; + packet.local = &local; + + if (aux_rx.local.family != AF_UNSPEC) { + *unicoap_endpoint_get_udp(&local) = aux_rx.local; + } +# endif + + unicoap_messaging_process_rfc7252(pdu, (size_t)received, truncated, &packet); + + if (IS_ACTIVE(CONFIG_UNICOAP_SOCK_ZERO_COPY_GUARANTEES)) { + received = sock_udp_recv_buf_aux(sock, &stackbuf, &buffer_ctx, 0, + unicoap_endpoint_get_udp(&remote), &aux_rx); + /* If the networking backends holds its zero-copy guarantee, then trying to read + * another chunk must not yield any more data. */ + assert(received == 0); + } + } +} + +int unicoap_transport_sendv_udp(iolist_t* iolist, const sock_udp_ep_t* remote, + const sock_udp_ep_t* local) +{ + assert(remote); + assert(iolist); + + UDP_DEBUG("sendv: %" PRIuSIZE " bytes\n", iolist_size(iolist)); + + int res = 0; + if (unlikely(local)) { + sock_udp_aux_tx_t aux_tx = { .flags = SOCK_AUX_SET_LOCAL, .local = *local }; + res = (int)sock_udp_sendv_aux(&_udp_socket, iolist, remote, &aux_tx); + } + else { + res = (int)sock_udp_sendv_aux(&_udp_socket, iolist, remote, NULL); + } + + if (res < 0) { + UDP_DEBUG("udp_sendv_aux failed: %i\n", res); + } + + return res; +} + +static int _add_socket(event_queue_t* queue, sock_udp_t* socket, sock_udp_ep_t* local) +{ + UDP_DEBUG("zero_copy_guarantees=%u creating UDP sock, port=%" PRIu16 " if=%" PRIu16 + " family=%s\n", + CONFIG_UNICOAP_SOCK_ZERO_COPY_GUARANTEES, local->port, local->netif, + local->family == AF_INET6 ? "inet6" : (local->family == AF_INET ? "inet" : "?")); + + int res = sock_udp_create(socket, local, NULL, 0); + if (res < 0) { + UDP_DEBUG("cannot create sock: %d (%s)\n", res, strerror(-res)); + return res; + } + + sock_udp_event_init(socket, queue, _udp_on_event, NULL); + + return 0; +} + +int unicoap_init_udp(event_queue_t* queue) +{ + sock_udp_ep_t local = { + /* FIXME: Once the problems with IPv4/IPv6 dual stack use in RIOT are fixed, adapt these lines + * (and e.g. use AF_UNSPEC) */ +#if defined(SOCK_HAS_IPV6) + .family = AF_INET6, +#elif defined(SOCK_HAS_IPV4) + .family = AF_INET, +#endif + .netif = SOCK_ADDR_ANY_NETIF, + .port = CONFIG_UNICOAP_UDP_PORT + }; + + return _add_socket(queue, &_udp_socket, &local); +} + +sock_udp_t* unicoap_transport_udp_get_socket(void) +{ + return &_udp_socket; +} + +int unicoap_transport_udp_add_socket(sock_udp_t* socket, sock_udp_ep_t* local) { + return _add_socket(sock_udp_get_async_ctx(&_udp_socket)->queue, socket, local); +} + +int unicoap_transport_udp_remove_socket(sock_udp_t* socket) { + sock_udp_close(socket); + return 0; +} + +int unicoap_deinit_udp(event_queue_t* queue) +{ + (void)queue; + sock_udp_close(&_udp_socket); + return 0; +} diff --git a/sys/net/application_layer/unicoap/endpoint.c b/sys/net/application_layer/unicoap/endpoint.c new file mode 100644 index 000000000000..891184828642 --- /dev/null +++ b/sys/net/application_layer/unicoap/endpoint.c @@ -0,0 +1,138 @@ +/* + * Copyright (C) 2024-2025 Carl Seifert + * Copyright (C) 2024-2025 TU Dresden + * + * This file is subject to the terms and conditions of the GNU Lesser General + * Public License v2.1. See the file LICENSE in the top level directory for + * more details. + */ + +/** + * @file + * @ingroup net_unicoap_transport + * @brief Endpoint abstraction and URI support implementation + * @author Carl Seifert + */ + +#include +#include +#include +#include "net/af.h" +#include "net/ipv4/addr.h" +#include "net/ipv6/addr.h" +#include "uri_parser.h" +#include "net/netif.h" +#include "net/unicoap/transport.h" +#include "net/unicoap/application.h" +#include "net/unicoap/message.h" + +#if IS_USED(MODULE_DNS) +# include "net/dns.h" +#endif + +#define ENABLE_DEBUG CONFIG_UNICOAP_DEBUG_LOGGING +#include "debug.h" +#include "private.h" + +#define STRLEN(str) (sizeof(str) - 1) + +#if IS_USED(MODULE_UNICOAP_SOCK_SUPPORT) +void unicoap_print_sock_tl_ep(const struct _sock_tl_ep* ep) +{ + (void)ep; + printf("port, ep->netif); + switch (ep->family) { + case AF_INET6: + printf("ipv6="); +# if SOCK_HAS_IPV6 && IS_USED(MODULE_IPV6_ADDR) + ipv6_addr_print((ipv6_addr_t*)ep->addr.ipv6); +# else + UNICOAP_DEBUG("SOCK_HAS_IPV6: v6 support missing, cannot print\n"); + printf("?"); +# endif /* SOCK_HAS_IPV6 && IS_USED(MODULE_IPV6_ADDR) */ + break; + case AF_INET: + printf("ipv4="); +# if SOCK_HAS_IPV4 && IS_USED(MODULE_IPV4_ADDR) + ipv4_addr_print((ipv4_addr_t*)ep->addr.ipv4); +# else + UNICOAP_DEBUG("SOCK_HAS_IPV4: v4 support missing, cannot print\n"); + printf("?"); +# endif /* SOCK_HAS_IPV6 && IS_USED(MODULE_IPV4_ADDR) */ + break; + + default: + printf("family=%i", ep->family); + break; + } + printf(">"); +} +#endif /* IS_USED(MODULE_UNICOAP_SOCK_SUPPORT) */ + +void unicoap_print_endpoint(const unicoap_endpoint_t* endpoint) +{ + printf("%s ", unicoap_string_from_proto(endpoint->proto)); + +#if IS_USED(MODULE_UNICOAP_SOCK_SUPPORT) + if (unicoap_transport_uses_sock_tl_ep(endpoint->proto)) { + unicoap_print_sock_tl_ep(&endpoint->_tl_ep); + return; + } +#endif /* IS_USED(MODULE_UNICOAP_SOCK_SUPPORT) */ + + printf("", endpoint->proto); +} + +const char* unicoap_string_from_proto(unicoap_proto_t proto) +{ + switch (proto) { + case UNICOAP_PROTO_UDP: + return "UDP"; + case UNICOAP_PROTO_DTLS: + return "DTLS"; + /* MARK: unicoap_driver_extension_point */ + default: + return "?"; + } +} + +bool unicoap_endpoint_is_equal(const unicoap_endpoint_t* lhs, + const unicoap_endpoint_t* rhs) +{ + (void)lhs; + (void)rhs; + if (lhs->proto != rhs->proto) { + return false; + } + switch (lhs->proto) { +#if IS_USED(MODULE_UNICOAP_SOCK_SUPPORT) + case UNICOAP_PROTO_UDP: + case UNICOAP_PROTO_DTLS: + return sock_tl_ep_equal(&lhs->_tl_ep, &rhs->_tl_ep); +#endif /* IS_USED(MODULE_UNICOAP_SOCK_SUPPORT) */ + /* MARK: unicoap_driver_extension_point */ + default: + assert(false); + return false; + } +} + +bool unicoap_endpoint_is_multicast(const unicoap_endpoint_t* endpoint) +{ + switch (endpoint->proto) { + case UNICOAP_PROTO_UDP: + case UNICOAP_PROTO_DTLS: +#if IS_USED(MODULE_UNICOAP_SOCK_SUPPORT) + return sock_udp_ep_is_multicast(&endpoint->udp_ep); +#else + UNICOAP_DEBUG("sock support is missing, cannot check if multicast addr, driver missing?\n"); + return false; +#endif + /* MARK: unicoap_driver_extension_point */ + default: + assert(false); + return false; + } +} + +/* TODO: Client: URI */ diff --git a/sys/net/application_layer/unicoap/include/private.h b/sys/net/application_layer/unicoap/include/private.h index b4d1bb26abfe..606d253f6db2 100644 --- a/sys/net/application_layer/unicoap/include/private.h +++ b/sys/net/application_layer/unicoap/include/private.h @@ -11,14 +11,21 @@ #include +#include "mutex.h" +#include "bitfield.h" #include "architecture.h" +#include "random.h" #include "net/unicoap.h" +#include "private/state.h" +#include "private/packet.h" +#include "private/messaging.h" + /** * @defgroup net_unicoap_private Private API * @ingroup net_unicoap_internal - * @brief Discover implementation details of `unicoap` + * @brief Implementation details of `unicoap` * @{ * * @warning Do not call any of these APIs and do not interact with any of unicoap's private types, @@ -58,9 +65,244 @@ extern "C" { # define _UNICOAP_PREFIX_DEBUG(category, ...) DEBUG(UNICOAP_DEBUG_PREFIX category ": " __VA_ARGS__) # define UNICOAP_DEBUG(...) _UNICOAP_PREFIX_DEBUG("", __VA_ARGS__) # define OPTIONS_DEBUG(...) _UNICOAP_PREFIX_DEBUG(".options", __VA_ARGS__) +# define MESSAGING_DEBUG(...) _UNICOAP_PREFIX_DEBUG(".messaging", __VA_ARGS__) +# define STATE_DEBUG(...) _UNICOAP_PREFIX_DEBUG(".state", __VA_ARGS__) +# define TRANSPORT_DEBUG(...) _UNICOAP_PREFIX_DEBUG(".transport", __VA_ARGS__) +# define SERVER_DEBUG(...) _UNICOAP_PREFIX_DEBUG(".server", __VA_ARGS__) +# define API_WARNING(message) "WARNING: " message "\n" +# define API_ERROR(message) "ERROR: " message "\n" +# define API_MISUSE(message) "API MISUSE: " message "\n" +# define INTERNAL_ERROR(message) "BUG: " message "\n" +# define FIXIT(message) "+++ FIXIT: " message "\n" +#endif + +#if ENABLE_DEBUG +/** + * @brief Debug print endpoint + */ +# define DEBUG_ENDPOINT(endpoint) unicoap_print_endpoint(endpoint) +/** + * @brief Debug print flags + */ +# define DEBUG_FLAGS(flags, role) unicoap_print_flags(flags, role) +#else +/** + * @brief Debug print endpoint + */ +# define DEBUG_ENDPOINT(endpoint) (void)endpoint +/** + * @brief Debug print flags + */ +# define DEBUG_FLAGS(flags, role) \ + (void)flags; \ + (void)role #endif + +#ifndef DOXYGEN +# if IS_ACTIVE(CONFIG_UNICOAP_ASSIST) +# include +# endif +# define unicoap_assist(...) \ + do { \ + if (IS_ACTIVE(CONFIG_UNICOAP_ASSIST)) { \ + printf(__VA_ARGS__); \ + } \ + } while (0) +#endif + +/** + * @brief Emit a diagnostic stating that a driver is missing + */ +void unicoap_assist_emit_diagnostic_missing_driver(unicoap_proto_t proto); /** @} */ +/* MARK: - Thread */ +/** + * @name Thread + * @{ + */ +/** @brief Name of background thread spawned upon calls to @ref unicoap_init */ +#define UNICOAP_THREAD_IDENTIFIER "unicoap" + +/** @brief Locks internal state lock */ +void unicoap_state_lock(void); + +/** @brief Unlocks internal state lock */ +void unicoap_state_unlock(void); +/** @} */ + +/* MARK: - State and initialization */ +/** + * @name State and initialization + * @{ + */ +/** + * @brief Container the unicoap stacks keeps state in + */ +typedef struct { + /** + * @brief Used when allocating listener, transaction, carbon copy, observer, or registration + */ + mutex_t lock; + + /** @brief Groups of resources */ + unicoap_listener_t* listeners; + + /* TODO: Client and advanced server features: Exchange-layer state objects */ +} unicoap_state_t; + +/** @brief Initializes the CoAP over UDP driver on the given @p queue */ +int unicoap_init_udp(event_queue_t* queue); + +/** @brief Deinitializes the CoAP over UDP driver on the given @p queue */ +int unicoap_deinit_udp(event_queue_t* queue); + +/** @brief Initializes the CoAP over DTLS over UDP driver on the given @p queue */ +int unicoap_init_dtls(event_queue_t* queue); + +/** @brief Deinitializes the CoAP over DTLS over UDP driver on the given @p queue */ +int unicoap_deinit_dtls(event_queue_t* queue); + +/** @brief Initializes the common RFC 7252 driver on the given @p queue */ +int unicoap_init_rfc7252_common(event_queue_t* queue); + +/** @brief DeiInitializes the common RFC 7252 driver on the given @p queue */ +int unicoap_deinit_rfc7252_common(event_queue_t* queue); + +/* MARK: unicoap_driver_extension_point */ +/** @} */ + +/* MARK: - Private Server Utils */ +/** + * @name Resource-request matching + * @{ + */ +/** + * @brief Tries to find a resource for the given packet + * + * The resource and listener variables passed by reference will be set to the respective resource + * and encompassing listener, if found. + * + * This method calls each listeners @ref unicoap_listener_t.request_matcher to check whether + * the given resource can be matched to the given packet. + * + * @note The default request matcher is @ref unicoap_resource_match_request_default + * + * @param[in] packet Packet to find resource for + * @param[in] resource_ptr Pointer to a resource variable + * @param[in] listener_ptr Pointer to a listener variable + * + * @retval `0` if found + * @retval An @ref unicoap_status_t if not found, method mismatch, or server error + */ +int unicoap_resource_find(const unicoap_packet_t* packet, const unicoap_resource_t** resource_ptr, + const unicoap_listener_t** listener_ptr); + +/** + * @brief Default request-resource matcher for listeners + * + * The resource variable passed by reference will be set to the respective resource, if found. + * + * @param[in] path Path + * @param[in] path_length Number of UTF-8 characters in @p path (excluding null-terminator) + * @param[in] listener Listener + * @param[out] resource_ptr Pointer to a resource variable + * @param[in] request Request message + * @param[in] endpoint Remote endpoint the request originates from + * + * @retval `0` if found + * @retval An @ref unicoap_status_t if not found, method mismatch, or server error + */ +int unicoap_resource_match_request_default(const char* path, size_t path_length, + const unicoap_listener_t* listener, + const unicoap_resource_t** resource_ptr, + const unicoap_message_t* request, + const unicoap_endpoint_t* endpoint); + +/** + * @brief Default resource handler for `/.well-known/core` + * + * @see @ref unicoap_request_handler_t + */ +int unicoap_resource_handle_well_known_core(unicoap_message_t* message, const unicoap_aux_t* aux, + unicoap_request_context_t* ctx, void* arg); +/** @} */ + +/* MARK: - Other Utils */ +/** + * @name Other Utils + * @{ + */ + +/** + * @brief Declares internal receiver storage buffer + * + * This buffer is used by sock network backends not emitting contiguous data + */ +#define UNICOAP_DECL_RECEIVER_STORAGE \ + uint8_t unicoap_receiver_buffer[CONFIG_UNICOAP_PDU_SIZE_MAX] + +/** + * @brief `extern` declaration of @ref UNICOAP_DECL_RECEIVER_STORAGE + */ +#define UNICOAP_DECL_RECEIVER_STORAGE_EXTERN extern UNICOAP_DECL_RECEIVER_STORAGE + +/** + * @brief Determines whether the chunk of the given size must be truncated to fit into a buffer of + * size @ref CONFIG_UNICOAP_PDU_SIZE_MAX + * + * Use this method when receiving fragmented/scattered data, e.g., when reading from a socket. + * + * This method returns `false` if a chunk of the given @p chunk_size can still be copied into a + * buffer of size @ref CONFIG_UNICOAP_PDU_SIZE_MAX when @p received bytes are already stored in + * that buffer. + * + * If said buffer cannot store a chunk of the given size while also holding @p received bytes + * already saved, the chunk needs to be truncated. In this case, `true` is returned and the chunk + * size is truncated to the largest chunk size that is still acceptable. Stop your receiver loop as + * further calls to this function will, consequently, also return `true` and yield a chunk_size of + * zero (buffer is full). + * + * @param[in,out] chunk_size Number of bytes in the currently received chunk. Written to if chunk + * does not fit into buffer. + * + * @param received Number of bytes already stored in the buffer (received byte count) + * + * @returns Boolean value indicating whether the chunk, and therefore also its size, needed to be + * truncated. + */ +static inline bool unicoap_transport_truncate_received(size_t* chunk_size, size_t received) +{ + /* cannot use sizeof() here, because we want this function to be inlinable, + * extern decl would affect transport.c, too */ + if (received + *chunk_size > CONFIG_UNICOAP_PDU_SIZE_MAX) { + /* Limit chunk size to remaining available storage capacity */ + TRANSPORT_DEBUG("truncated\n"); + TRANSPORT_DEBUG("warning: recv storage too small, need at least %" PRIdSIZE "\n", + received + *chunk_size); + *chunk_size = CONFIG_UNICOAP_PDU_SIZE_MAX - received; + return true; + } + return false; +} +/** @} */ + +#ifndef DOXYGEN +static inline void __debug_hex(const uint8_t* buffer, size_t size) +{ + for (size_t i = 0; i < size; i += 1) { + printf("%02X", buffer[i]); + } +} + +# define _UNICOAP_DEBUG_HEX(bytes, size) \ + do { \ + if (ENABLE_DEBUG) { \ + __debug_hex(bytes, size); \ + } \ + } while (0) +#endif + #ifdef __cplusplus } #endif diff --git a/sys/net/application_layer/unicoap/include/private/messaging.h b/sys/net/application_layer/unicoap/include/private/messaging.h new file mode 100644 index 000000000000..af6168d07716 --- /dev/null +++ b/sys/net/application_layer/unicoap/include/private/messaging.h @@ -0,0 +1,331 @@ +/* + * Copyright (C) 2024-2025 Carl Seifert + * Copyright (C) 2024-2025 TU Dresden + * + * This file is subject to the terms and conditions of the GNU Lesser General + * Public License v2.1. See the file LICENSE in the top level directory for + * more details. + */ + +#pragma once + +#include + +#include "net/unicoap.h" + +/** + * @addtogroup net_unicoap_private + * @{ + */ + +/** + * @file + * @brief Messaging API + * @author Carl Seifert + */ + +#ifdef __cplusplus +extern "C" { +#endif + +/* MARK: - Private Messaging API */ +/** + * @name Private Messaging API + * @{ + */ + +/** + * @brief Messaging layer result after initial treatment of an inbound message + * + * ``` + * / + * | Application + * / /|\ + * Stack < | API + * \ \|/ + * | Request/response layer ------+ + * \ ^ \ Events + * | / (truncated, expected, deferred, ...) + * / Messaging layer (driver) <---+ + * | ^ + * / | + Driver(s) < Parser (driver) + * \ ^ + * | | + * \ Network (driver) + * + * Fig. 1: Inbound message path through the stack + * ``` + * + * @remark You can always use a compile-time bitwise OR to check for multiple flags to check + * for generally acceptable or unacceptable messages. + */ + +/** + * @brief Flags for sending a message communicated by the unicoap stack to the messaging driver + * + * Each time the stack wants to send a message, @ref unicoap_messaging_send will be called, which, + * in turn, invokes the messaging driver implementation. + * + * @warning Bits other than those defined as enum cases are RFU. Do not use them or make + * assumptions on their value. + * + * These flags are intended for messaging and transmission options. Messaging options may change + * the CoAP message flow (e.g., waiting for an acknowledgement) or transmission + * behavior (e.g., using an optional reliability layer of the underlying transport protocol or + * implementation). + * + * ### Relationship with Exchange-Layer Flags + * Messaging flags are provided as part of client and resource flags. The + * (TODO: ref) unicoap_client_flags_t and @ref unicoap_resource_flags_t are separated into + * - exchange flags needed for features building on top the request/response level and + * - messaging flags. + * + * The stack communicates only the messaging flags to the driver, in the form of this bitfield. + */ +typedef enum { + + /** + * @brief Messaging flag indicating the stack wants this message to be transmitted reliably + * + * For transport protocol that already provide reliability, the messaging driver can ignore this + * flag. If the CoAP messaging layer of your driver supports optional reliability, such as the + * RFC 7252 driver, turn on this behavior if you are given this flag. + * + * ## Example + * The RFC 7252 (CoAP over UDP and DTLS) supports optional reliability in the form of + * confirmable message. In contrast to non-confirmable messages, confirmable messages + * elicit an acknowledgement message sent by the receiver. The sender retransmits the original + * confirmable message using an exponential back-of mechanism until it receives the + * acknowledgement. + */ + UNICOAP_MESSAGING_FLAG_RELIABLE = 0x01, + + /* MARK: unicoap_driver_extension_point */ +} __attribute__((__packed__)) unicoap_messaging_flags_t; + +/** + * @brief The messaging flags the stack uses when no source for messaging flags is available + * + * These flags are used if the server needs to send a 4.04 Path Not Found response, + * or the message is truncated. + */ +#define UNICOAP_MESSAGING_FLAGS_DEFAULT (0) + +/** + * @brief Argument passed to exchange-layer processing function + */ +typedef union { + /** @brief Resource */ + const unicoap_resource_t* resource; + +} unicoap_exchange_arg_t; + +/** + * @brief Result of @ref unicoap_exchange_preprocess + */ +typedef enum { + /** + * @brief The given message's code class is not valid + * + * Currently considered valid: + * - `0.xx` Requests, except `0.00` + * - `2.xx`, `4.xx`, and `5.xx` Responses + * - `7.xx` Signals + */ + UNICOAP_PREPROCESSING_ERROR_INVALID_CODE_CLASS = -0x1001, + + /** + * @brief The given message cannot be processed due to being unsupported + */ + UNICOAP_PREPROCESSING_ERROR_UNSUPPORTED = -0x1002, + + /** + * @brief The given message is unexpected and cannot be processed + * + * There's no known client exchange running. + */ + UNICOAP_PREPROCESSING_ERROR_RESPONSE_UNEXPECTED = -0x1004, + + /** + * @brief Unexpected notification, no known client exchange (observation) + * + * There's no known client exchange, i.e., observation, running. + */ + UNICOAP_PREPROCESSING_ERROR_NOTIFICATION_UNEXPECTED = -0x100c, + + /** + * @brief The given message is truncated and can thus not be handled. + */ + UNICOAP_PREPROCESSING_ERROR_TRUNCATED = -0x1010, + + /** + * @brief The given message cannot be handled, disregarded + */ + UNICOAP_PREPROCESSING_ERROR_REQUEST = -0x1020, + + /** + * @brief The given message can be processed + * + * There's an active client exchange. + */ + UNICOAP_PREPROCESSING_SUCCESS_RESPONSE = 0x1001, + + /** + * @brief The given message can be processed + * + * There's a resource that can handle this request + */ + UNICOAP_PREPROCESSING_SUCCESS_REQUEST = 0x1002, + +} unicoap_preprocessing_result_t; + +/** + * @brief Performs initial processing of a CoAP packet after it has been parsed + * + * The processing of a CoAP message is divided into two parts: preprocessing and the actual + * processing. This design was chosen to allow the messaging driver to implement networking logic + * in between. + * + * *Example:* + * The RFC 7252 messaging driver sends an ACK for every confirmable response received at the client. + * Waiting for the application to handle the response could delay the ACK, possibly lead to + * retransmissions or even open a timing-based side channel. + * + * @param[in,out] packet Packet received + * @param[out] flags Messaging flags associated with resource or client exchange + * @param[out] arg Argument to be passed to the processor + * @param truncated A boolean value indicating whether the given PDU was truncated, but could still + * be parsed (i.e., a valid message) + * + * @returns @ref unicoap_preprocessing_result_t + */ +unicoap_preprocessing_result_t unicoap_exchange_preprocess(unicoap_packet_t* packet, + unicoap_messaging_flags_t* flags, + unicoap_exchange_arg_t* arg, + bool truncated); + +/** + * @brief Processes message + * + * Call this after you have preprocessed the message using @ref unicoap_exchange_preprocess. + * + * @param[in,out] packet CoAP packet + * @param[in] arg Argument returned (via out parameter) by @ref unicoap_exchange_preprocess + * + * @retval Zero if processing succeeds + * @retval Negative error number if processing failed + */ +int unicoap_exchange_process(unicoap_packet_t* packet, unicoap_exchange_arg_t arg); + +/** + * @brief Releases all buffers and state allocated in connection with the given endpoint + * + * Call this function from your driver if you encounter a severe error with a given endpoint + * + * @param[in] endpoint Remote endpoint to release buffers for + * + * @return Negative error integer or zero on success. + * @retval `-ENOENT` if no state is currently associated with the given endpoint + */ +int unicoap_exchange_release_endpoint_state(const unicoap_endpoint_t* endpoint); + +/** + * @brief Internal RFC 7252 messaging inbound processor + * @param[in] pdu Buffer containing PDU + * @param size Size of PDU in bytes + * @param truncated A boolean value indicating whether the message has been truncated by the + * transport layer + * @param[in] packet Packet to process + * + * @returns Negative error number in case of a failure, zero otherwise. + * + * This function forwards the `truncated` characteristic to the exchange layer, which can handle + * that scenario appropriately, such as by setting a Size option. + * + * @remark While it is not advised to call private API, you might want to consider calling this + * function in a very constrained environment or when using `sock` is not an option. + */ +int unicoap_messaging_process_rfc7252(const uint8_t* pdu, size_t size, bool truncated, + unicoap_packet_t* packet); + +/* MARK: unicoap_driver_extension_point */ + +/** + * @brief Forwards packet to messaging layer and the corresponding driver for further message + * processing, encoding, and transport I/O. + * + * The driver needs to encode the given message into a serialized PDU and use its own means + * of transmitting the packet to the packet's remote endpoint. + * + * This function calls the individual driver messaging implementation. + * + * @param[in,out] packet Packet to send + * @param flags Messaging flags + * @returns Zero on success or negative error value. See @ref unicoap_messaging_send_rfc7252. + */ +int unicoap_messaging_send(unicoap_packet_t* packet, unicoap_messaging_flags_t flags); + +/** @brief Sends CoAP over UDP or DTLS packet, see @ref unicoap_messaging_send */ +int unicoap_messaging_send_rfc7252(unicoap_packet_t* packet, unicoap_messaging_flags_t flags); + +/** + * @brief Generates new token + * + * Token will be @ref CONFIG_UNICOAP_GENERATED_TOKEN_LENGTH bytes long. + * + * @param[in,out] token Buffer for generated token, must have a + * minimum capacity of @ref CONFIG_UNICOAP_GENERATED_TOKEN_LENGTH + */ +void unicoap_generate_token(uint8_t* token); + +/** + * @brief Retrieves the part of a resource flags bitfield relevant for the messaging driver. + * + * @param resource_flags Resource flags + * @return Messaging flags extracted from the given bitfield + */ +static inline unicoap_messaging_flags_t +_messaging_flags_resource(unicoap_resource_flags_t resource_flags) +{ + /* We documented other flags are RFU, hence downcasting to the messaging + flags bitfield width is fine here */ + return (unicoap_messaging_flags_t)resource_flags; +} +/** @} */ + +/* MARK: - Private Exchange-Layer Server API */ +/** + * @name Server + * @{ + */ +/** + * @brief Informs the unicoap server a new packet has been preprocessed and can be handled. + * + * This function handles block-wise transfers. + * + * @param[in,out] packet Packet that will be processed by the server + * @param[in,out] resource Resource + * + * @retval `0` on success + * @retval Negative errno on failure + */ +int unicoap_server_process_request(unicoap_packet_t* packet, const unicoap_resource_t* resource); + +/** + * @brief Sends entire response body, may be split into parts and then sent + * + * @param[in,out] packet Response packet to send + * @param[in] resource mandatory resource which is sending this response + * + * @returns Zero on success, negative integer otherwise + */ +int unicoap_server_send_response_body(unicoap_packet_t* packet, + const unicoap_resource_t* resource); +/** @} */ + +#ifdef __cplusplus +} +#endif + +/** @} */ diff --git a/sys/net/application_layer/unicoap/include/private/packet.h b/sys/net/application_layer/unicoap/include/private/packet.h new file mode 100644 index 000000000000..db42588701e6 --- /dev/null +++ b/sys/net/application_layer/unicoap/include/private/packet.h @@ -0,0 +1,133 @@ +/* + * Copyright (C) 2024-2025 Carl Seifert + * Copyright (C) 2024-2025 TU Dresden + * + * This file is subject to the terms and conditions of the GNU Lesser General + * Public License v2.1. See the file LICENSE in the top level directory for + * more details. + */ + +#pragma once + +#include +#include "net/unicoap.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @addtogroup net_unicoap_private + * @{ + */ + +/** + * @file + * @brief Packet + * @author Carl Seifert + */ + +/* We don't require a DTLS implementation to be present when the CoAP over DTLS driver is not used. + * Including DTLS headers without an implementation fails. Hence, we create a local alias + * that either maps to the system-provided DTLS session struct or to nothing. */ + +#ifndef DOXYGEN +# if IS_USED(MODULE_UNICOAP_DRIVER_DTLS) +typedef sock_dtls_session_t unicoap_sock_dtls_session_t; +# else +typedef void unicoap_sock_dtls_session_t; +# endif +#endif + +/** + * @brief A type acting as an envelope for a message and data connected, like endpoints + * + * Using this type, you can allocate everything at once without needing to pass endpoints and + * message objects as parameters. + */ +typedef struct { + /** + * @brief The remote endpoint this packet was received from or is destined for + */ + const unicoap_endpoint_t* remote; + + /** + * @brief The local endpoint this packet was received at or is to be sent from + */ + const unicoap_endpoint_t* local; + + union { +#if IS_USED(MODULE_UNICOAP_DRIVER_DTLS) || defined(DOXYGEN) + /** + * @brief DTLS session + * + * Stored here to avoid looking up sessions in digital session management (DSM) + */ + const sock_dtls_session_t* dtls_session; +#endif + + /* MARK: unicoap_driver_extension_point */ + }; + + /** + * @brief Message, public API, also used by PDU header parser + */ + unicoap_message_t* message; + + /** + * @brief Backing storage for properties, used by PDU header parser + */ + unicoap_message_properties_t properties; +} unicoap_packet_t; + +/** + * @brief Retrieves protocol number from packet + * + * @param[in] packet Packet to read protocol number from + * + * @returns Protocol number + */ +static inline unicoap_proto_t unicoap_packet_proto(const unicoap_packet_t* packet) +{ + return packet->remote->proto; +} + +/** + * @brief Retrieves transport session from packet + * + * @param[in,out] packet Packet to retrieve session from + * + * @returns Transport session + */ +static inline const void* _packet_get_dtls_session(unicoap_packet_t* packet) +{ +#if IS_USED(MODULE_UNICOAP_DRIVER_DTLS) + return unicoap_packet_proto(packet) == UNICOAP_PROTO_DTLS ? packet->dtls_session : NULL; +#else + (void)packet; + return NULL; +#endif +} + +/** + * @brief Sets DTLS transport session of packet + * + * @param[in,out] packet whose session to set + * @param dtls_session DTLS session + */ +static inline void _packet_set_dtls_session(unicoap_packet_t* packet, + const unicoap_sock_dtls_session_t* dtls_session) +{ +#if IS_USED(MODULE_UNICOAP_DRIVER_DTLS) + packet->dtls_session = dtls_session; +#else + (void)packet; + (void)dtls_session; +#endif +} + +#ifdef __cplusplus +} +#endif + +/** @} */ diff --git a/sys/net/application_layer/unicoap/include/private/state.h b/sys/net/application_layer/unicoap/include/private/state.h new file mode 100644 index 000000000000..2a8e5a232a31 --- /dev/null +++ b/sys/net/application_layer/unicoap/include/private/state.h @@ -0,0 +1,151 @@ +/* + * Copyright (C) 2024-2025 Carl Seifert + * Copyright (C) 2024-2025 TU Dresden + * + * This file is subject to the terms and conditions of the GNU Lesser General + * Public License v2.1. See the file LICENSE in the top level directory for + * more details. + */ + +#pragma once + +#include "ztimer.h" +#include "random.h" +#include "event.h" +#include "container.h" + +#include "net/unicoap/application.h" + +#include "private/packet.h" + +/** + * @defgroup net_unicoap_private_state State Management + * @ingroup net_unicoap_private + * @{ + */ + +/** + * @file + * @brief State and Memo API + * @author Carl Seifert + */ + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @brief An event scheduled for a specified point in time + * @ingroup net_unicoap_private_state + * @private + * + * @note The API for this structure is internal. + * + * In contrast to @ref event_timeout_t this structure does not store the queue and clock as they + * are known statically and remain the same. This structure also does not use an @ref event_callback_t + * to save the space needed to store an additional function pointer. + * + * # Sequence of Events + * ``` + * You schedule an event + * \/ + * event_t is initialized + * ztimer_t is set + * \/ + * .... + * Timer fires + * \/ + * Internal callback is called + * \/ + * Event is posted on queue + * \/ + * .... + * Queue calls your callback + * + * Fig. 1: Sequence of events + * ``` + */ +typedef struct { + /** + * @brief Event to be posted to the internal event queue + * @internal + * + * This event stores the callback you provided to @ref unicoap_event_schedule + */ + event_t super; + + /** + * @brief Timer used to post the event + * @internal + * + * The timer stores the internal callback that posts the @ref unicoap_scheduled_event_t.super + * event on the internal unicoap queue. + */ + ztimer_t ztimer; +} unicoap_scheduled_event_t; + +/** + * @brief Memo typealias + */ +typedef struct unicoap_memo unicoap_memo_t; + +/* MARK: - Event Scheduling */ +/** + * @name Event Scheduling + * @{ + */ +/** @brief The ztimer clock used for event scheduling */ +#define UNICOAP_CLOCK ZTIMER_MSEC + +/** + * @brief Scheduled event callback + * + * Use the @p event parameter and the @ref container_of macro to get a pointer + * to the parent structure. + * + * @param[in] event pointer to event + */ +typedef void (*unicoap_event_callback_t)(unicoap_scheduled_event_t* event); + +/** + * @brief Schedules an event on the internal unicoap queue + * + * @param[in,out] event The event to schedule. Provide a pointer to a pre-allocated event + * @param[in] callback Function pointer to be called on the internal queue after @p duration ms have elapses + * @param duration Number of milliseconds to wait + */ +void unicoap_event_schedule(unicoap_scheduled_event_t* event, unicoap_event_callback_t callback, + uint32_t duration); + +/** + * @brief Discards the currently set timeout, and reschedules the event to be posted in @p duration ms + * + * Use this method to rerun the timer with the same callbacks but a new timeout value. + * E.g., the RFC 7252 driver utilizes this method for setting the next acknowledgement timeout. + * + * @param[in] event Scheduled event you want to reschedule + * @param[in] duration Number of milliseconds the event should be posted on the queue + */ +static inline void unicoap_event_reschedule(unicoap_scheduled_event_t* event, uint32_t duration) +{ + ztimer_set(UNICOAP_CLOCK, &event->ztimer, duration); +} + +/** + * @brief Cancels the event + * + * Internally, the timer is removed from the clock. + * + * @note If the event has already been posted on the queue, this method will try to remove + * the cancel the event that has already been posted to the queue. + */ +void unicoap_event_cancel(unicoap_scheduled_event_t* event); +/** @} */ + +/* TODO: Client and advanced server features: Elaborate state management */ + +#ifdef __cplusplus +} +#endif + +/** @} */ diff --git a/sys/net/application_layer/unicoap/options.c b/sys/net/application_layer/unicoap/options.c index f9bcd6bc6971..178a676029f1 100644 --- a/sys/net/application_layer/unicoap/options.c +++ b/sys/net/application_layer/unicoap/options.c @@ -19,6 +19,7 @@ #include #include +#include "ztimer.h" /* needed for generating observe value */ #include "byteorder.h" #include "compiler_hints.h" @@ -862,20 +863,20 @@ ssize_t unicoap_options_get_next_query_by_name(unicoap_options_iterator_t* itera unicoap_option_number_t number, const char* name, const char** value) { - char* _name = NULL; - const char* component = NULL; + const uint8_t* _name = NULL; + const uint8_t* component = NULL; ssize_t res = -1; while ((res =unicoap_options_get_next_by_number(iterator, number, (const uint8_t**)&component)) >= 0) { assert(component); - _name = (char*)component; + _name = component; while (res > 0 && *component != '=') { component += 1; res -= 1; } - if (strncmp(name, _name, (uintptr_t)component - (uintptr_t)_name) != 0) { + if (strncmp(name, (char*)_name, (uintptr_t)component - (uintptr_t)_name) != 0) { continue; } @@ -883,7 +884,7 @@ ssize_t unicoap_options_get_next_query_by_name(unicoap_options_iterator_t* itera assert(*component == '='); component += 1; res -= 1; - *value = component; + *value = (const char*)component; } else { *value = NULL; @@ -973,6 +974,13 @@ int unicoap_options_add_uint(unicoap_options_t* options, unicoap_option_number_t return unicoap_options_add(options, number, (uint8_t*)&value, size); } +int unicoap_options_set_observe_generated(unicoap_options_t* options) +{ + /* generate notification value */ + return unicoap_options_set_observe( + options, (ztimer_now(ZTIMER_MSEC) >> UNICOAP_OBSERVE_TICK_EXPONENT) & 0xFFFFFF); +} + ssize_t unicoap_options_swap_storage(unicoap_options_t* options, uint8_t* destination, size_t capacity) { diff --git a/sys/net/application_layer/unicoap/server.c b/sys/net/application_layer/unicoap/server.c new file mode 100644 index 000000000000..724aadae2787 --- /dev/null +++ b/sys/net/application_layer/unicoap/server.c @@ -0,0 +1,367 @@ +/* + * Copyright (C) 2024-2025 Carl Seifert + * Copyright (C) 2024-2025 TU Dresden + * + * This file is subject to the terms and conditions of the GNU Lesser General + * Public License v2.1. See the file LICENSE in the top level directory for + * more details. + */ + +/** + * @file + * @ingroup net_unicoap_server + * @brief Server implementation + * @author Carl + */ + +#include + +#include "thread.h" +#include "compiler_hints.h" + +#include "net/unicoap/application.h" + +#define ENABLE_DEBUG CONFIG_UNICOAP_DEBUG_LOGGING +#include "debug.h" +#include "private.h" + + +/** @brief Returns new length of path excluding trailing slashes */ +static inline size_t _trim_trailing_slashes(const char* path, size_t length) { + if (length > 0) { + size_t i = length - 1; + while (i > 0) { + if (path[i] == '/') { + i -= 1; + } else { + break; + } + } + return i + 1; + } else { + return length; + } +} + +bool unicoap_resource_match_path_string(const unicoap_resource_t* resource, + const char* lhs_path, size_t _lhs_length) +{ + assert(resource); + assert(lhs_path); + + /* We are comparing the left-hand side (path from request) to the right-hand side (resource). */ + + size_t rhs_length = strlen(resource->path); + assert(rhs_length > 0); + size_t lhs_length = _trim_trailing_slashes(lhs_path, _lhs_length); + + if (resource->flags & UNICOAP_RESOURCE_FLAG_MATCH_SUBTREE) { + /* The actual path (LHS) length may be longer. If it is shorter, bail out. */ + if (lhs_length < rhs_length) { + return false; + } + + /* The actual path (LHS) is now either as long or longer. If it is longer, then we + * expect a slash to indicate a subtree. Either the RHS path already ends with a slash + * or the LHS has a slash that succeeds the last RHS character. + * + * Examples: + * + * LHS: /a -> RHS ends in slash, every path that is longer than RHS is a subpath + * RHS: / + * + * LHS: /a/a -> RHS ends in slash, every path that is longer than RHS is a subpath + * RHS: /a/ + * + * LHS: /a/a -> RHS does not end in slash, so we need to require one to indicate subpath + * RHS: /a + */ + if (lhs_length > rhs_length && !(lhs_path[rhs_length] == '/' || resource->path[rhs_length - 1] == '/')) { + return false; + } + + return strncmp(lhs_path, resource->path, rhs_length) == 0; /* RHS is null-terminated */ + } else { + /* The actual path (LHS) length must match. If it is unequal, bail out. */ + if (lhs_length != rhs_length) { + return false; + } + + return strncmp(lhs_path, resource->path, lhs_length) == 0; /* RHS is null-terminated */ + } +} + +bool unicoap_resource_match_path_options(const unicoap_resource_t* resource, + const unicoap_options_t* options) +{ + assert(resource); + assert(options); + unicoap_options_iterator_t iterator; + /* Disqualifying the const here is fine as the iterator is only used locally and options + * are not manipulated. As we only have one iterator concept for both mutable and read-only + * borrowing access patterns, this is the only way to go. */ + unicoap_options_iterator_init(&iterator, (unicoap_options_t*)options); + + char* cursor = (char*)resource->path; + const char* end = resource->path + strlen(resource->path); + + /* Skip multiple successive slashes. + * https://pubs.opengroup.org/onlinepubs/009695399/basedefs/xbd_chap03.html#tag_03_266 + */ + while (*cursor == '/') { + cursor += 1; + } + char* start = cursor; + const char* component = NULL; + + /* You might think, why not check cursor < end. To remove the need for end - 1 and bounds + * checks, we use cursor <= end and treat that case as if the NULL-terminator were a slash. + * The slash/NULL-terminator signals to the loop that it is supposed to compare the + * sequence of characters it has read since the last slash to the current Uri-Path option. */ + while (cursor <= end) { + /* Found the end of a path component. + * That may either be a slash or the end of the string. */ + if ((*cursor == '/') || ((cursor != start) && (cursor == end))) { + + int res = -1; + if ((res = unicoap_options_get_next_uri_path_component(&iterator, &component)) < 0) { + break; + } + /* Cursor points to the element with the index one greater than the last character. */ + size_t size = (uintptr_t)cursor - (uintptr_t)start; + + /* Compare Uri-Path component with string path component. */ + if (((size_t)res != size) || (strncmp(start, component, size) != 0)) { + return false; + } + + /* Skip multiple successive slashes. */ + while (*cursor == '/') { + cursor += 1; + } + start = cursor; + } + else { + /* Skip over path component characters. */ + cursor += 1; + } + } + + if (cursor != end + 1) { + /* If the resource's path is longer than the actual path, the paths definitely don't match. + * The end + 1 comparison is justified as we treat the NULL-terminator like a trailing + * slash. See above. */ + return false; + } + + if (resource->flags & UNICOAP_RESOURCE_FLAG_MATCH_SUBTREE) { + /* There may be more Uri-Path options. But this is fine as subpaths are allowed. */ + return true; + } + else { + /* Make sure we read all options, i.e., the actual path is not longer than the resource's. + */ + return unicoap_options_get_next_uri_path_component(&iterator, &component) == -1; + } +} + +int unicoap_resource_match_request_default(const char* path, size_t path_length, + const unicoap_listener_t* listener, + const unicoap_resource_t** resource, + const unicoap_message_t* request, + const unicoap_endpoint_t* endpoint) +{ + int res = UNICOAP_STATUS_PATH_NOT_FOUND; + for (unsigned int i = 0; i < listener->resource_count; i += 1) { + *resource = &listener->resources[i]; + if (!unicoap_match_proto((*resource)->protocols, endpoint->proto)) { + SERVER_DEBUG("ignoring resource %s, proto %s not in set\n", (*resource)->path, + unicoap_string_from_proto(endpoint->proto)); + continue; + } + + if (!unicoap_resource_match_path_string(*resource, path, path_length)) { + /* URI mismatch */ + continue; + } + + /* potential match, check for method */ + if (!unicoap_resource_match_method((*resource)->methods, + unicoap_request_get_method(request))) { + /* record wrong method error for next iteration, in case + * another resource with the same URI and correct method + * exists */ + res = UNICOAP_STATUS_METHOD_NOT_ALLOWED; + continue; + } + else { + return 0; + } + } + return res; +} + +ssize_t unicoap_resource_encode_link(const unicoap_resource_t* resource, char* buffer, + size_t capacity, unicoap_link_encoder_ctx_t* context) +{ + assert(buffer); + size_t path_len = strlen(resource->path); + /* count target separators and any link separator */ + size_t exp_size = path_len + 2 + (context->uninitialized ? 0 : 1); + + unsigned pos = 0; + if (exp_size > capacity) { + return -ENOBUFS; + } + + if (!context->uninitialized) { + buffer[pos++] = ','; + } + buffer[pos++] = '<'; + memcpy(&buffer[pos], resource->path, path_len); + buffer[pos + path_len] = '>'; + + return exp_size; +} + +/** + * @brief Handler for `/.well-known/core`. Lists registered handlers, except for + * `/.well-known/core` itself. + */ +int unicoap_resource_handle_well_known_core(unicoap_message_t* message, const unicoap_aux_t* aux, + unicoap_request_context_t* ctx, void* arg) +{ + (void)arg; + UNICOAP_OPTIONS_ALLOC(options, 2); + if (unicoap_options_set_content_format(&options, UNICOAP_FORMAT_LINK) < 0) { + return -1; + } + + char links[CONFIG_UNICOAP_WELL_KNOWN_CORE_SIZE_MAX]; + size_t size = unicoap_resource_core_link_format_build(links, sizeof(links), aux->remote->proto); + + unicoap_response_init_with_options(message, UNICOAP_STATUS_CONTENT, (uint8_t*)links, size, + &options); + return unicoap_send_response(message, ctx); +} + + +int unicoap_server_process_request(unicoap_packet_t* packet, const unicoap_resource_t* resource) +{ + assert(packet); + assert(packet->remote); + int res = 0; + + unicoap_request_context_t context = { + .resource = resource, ._packet = packet + }; + + unicoap_aux_t aux = { + .remote = packet->remote, + .local = packet->local, + .properties = &packet->properties, + }; + + SERVER_DEBUG("invoking handler\n"); + res = resource->handler(packet->message, &aux, &context, resource->handler_arg); + + if (res > 0) { + SERVER_DEBUG("sending response " UNICOAP_CODE_CLASS_DETAIL_FORMAT + " from return value\n", + unicoap_code_class((uint8_t)res), unicoap_code_detail((uint8_t)res)); + + if (IS_ACTIVE(CONFIG_UNICOAP_PREVENT_OPTIONAL_RESPONSES)) { + if (unicoap_response_is_optional(packet->message->options, (unicoap_status_t)res)) { + SERVER_DEBUG("response " UNICOAP_CODE_CLASS_DETAIL_FORMAT + " is optional, not responding\n", + unicoap_code_class((uint8_t)res), + unicoap_code_detail((uint8_t)res)); + return 0; + } + } + + unicoap_response_init_empty(packet->message, (unicoap_status_t)res); + return unicoap_server_send_response_body(packet, resource); + } + else if (context._packet) { + /* application didn't send a response or deferred response, + * otherwise, _packet would be NULL here */ + if (res != UNICOAP_IGNORING_REQUEST) { + /* handler does not want to send response (provided, No-Response is set at all) */ + /* the decision whether to honour No-Response must be made by the handler */ + + if (IS_ACTIVE(CONFIG_UNICOAP_ASSIST)) { + unicoap_assist(API_MISUSE("handler did not respond") + FIXIT("set USEMODULE += unicoap_deferred_response and" + "call unicoap_defer_response") + FIXIT("ignore request by returning UNICOAP_IGNORING_REQUEST")); + unicoap_response_init_string(packet->message, + UNICOAP_STATUS_INTERNAL_SERVER_ERROR, "application"); + goto error; + } + } + + /* TODO: Advanced server features: Free exchange-layer state */ + return 0; + } + return 0; + +error: + if ((res = unicoap_server_send_response_body(packet, resource)) < 0) { + return res; + } + + /* TODO: Advanced server features: Free exchange-layer state */ + return 0; +} + +/** + * @brief Common function for @ref unicoap_send_response and @ref unicoap_send_response_deferred + */ +int unicoap_server_send_response_body(unicoap_packet_t* packet, + const unicoap_resource_t* resource) +{ + int res = 0; + if ((res = unicoap_messaging_send(packet, _messaging_flags_resource(resource->flags))) < 0) { + SERVER_DEBUG("error: could not send response\n"); + goto error; + } + return 0; + +error: + SERVER_DEBUG("failed to send response, trying to send 5.05 unreliably\n"); + + /* try to send 5.05, */ + unicoap_response_init_empty(packet->message, UNICOAP_STATUS_INTERNAL_SERVER_ERROR); + unicoap_messaging_send(packet, _messaging_flags_resource(resource->flags) & + ~UNICOAP_MESSAGING_FLAG_RELIABLE); + + /* TODO: Client and advanced server features: free allocated state */ + return res; +} + +int unicoap_send_response(unicoap_message_t* response, unicoap_request_context_t* context) +{ + assert(response); + assert(context); + assert(context->resource); + + if (IS_ACTIVE(CONFIG_UNICOAP_ASSIST)) { + if (!context->_packet) { + unicoap_assist(API_MISUSE("cannot send response, already sent")); + return -ECANCELED; + } + } + + assert(context->_packet); + SERVER_DEBUG("sending immediate response\n"); + + /* reuse the packet, stack-allocated, we're still inside resource handler */ + ((unicoap_packet_t*)context->_packet)->message = response; + int res = unicoap_server_send_response_body((unicoap_packet_t*)context->_packet, + context->resource); + + /* prevent context from being used for sending a response again */ + context->_packet = NULL; + return res; +} diff --git a/sys/net/application_layer/unicoap/state.c b/sys/net/application_layer/unicoap/state.c new file mode 100644 index 000000000000..65e8f6d0d218 --- /dev/null +++ b/sys/net/application_layer/unicoap/state.c @@ -0,0 +1,572 @@ +/* + * Copyright (C) 2024-2025 Carl Seifert + * Copyright (C) 2024-2025 TU Dresden + * + * This file is subject to the terms and conditions of the GNU Lesser General + * Public License v2.1. See the file LICENSE in the top level directory for + * more details. + */ + +/** + * @file + * @ingroup net_unicoap + * @brief Core functionality + * @author Carl Seifert + */ + +#include +#include "container.h" +#include "thread.h" +#include "sched.h" +#include "random.h" +#include "event.h" + +#include "net/unicoap/config.h" + +#define ENABLE_DEBUG CONFIG_UNICOAP_DEBUG_LOGGING +#include "debug.h" +#include "private.h" + +UNICOAP_DECL_RECEIVER_STORAGE; + +/* Internal variables */ +const unicoap_resource_t _unicoap_default_resources[] = { +#if CONFIG_UNICOAP_WELL_KNOWN_CORE + /* (unicoap_resource_t) */ { .path = "/.well-known/core", + .handler = unicoap_resource_handle_well_known_core, + .methods = UNICOAP_METHODS(UNICOAP_METHOD_GET) } +#endif +}; + +static unicoap_listener_t _default_listener = { + .resources = _unicoap_default_resources, + .resource_count = ARRAY_SIZE(_unicoap_default_resources), + .request_matcher = unicoap_resource_match_request_default, + .link_encoder = unicoap_resource_encode_link, + .next = NULL, +}; + +#if IS_USED(MODULE_UNICOAP_RESOURCES_XFA) +XFA_INIT_CONST(unicoap_resource_t, unicoap_resources_xfa); +#endif + +static unicoap_state_t _state = { .listeners = &_default_listener }; + +kernel_pid_t _unicoap_pid = KERNEL_PID_UNDEF; +static event_queue_t _queue; + +static inline void _lock(void) +{ + mutex_lock(&_state.lock); +} + +static inline void _unlock(void) +{ + mutex_unlock(&_state.lock); +} + +void unicoap_state_lock(void) +{ + _lock(); +} + +void unicoap_state_unlock(void) +{ + _unlock(); +} + +/* MARK: - Event Scheduling on Internal Queue */ + +static void _scheduled_event_callback(void* scheduled_event) +{ + event_post(&_queue, (event_t*)scheduled_event); +} + +void unicoap_event_schedule(unicoap_scheduled_event_t* event, unicoap_event_callback_t callback, + uint32_t duration) +{ + event->ztimer.callback = _scheduled_event_callback; + event->ztimer.arg = (void*)event; + /* This cast is fine because (a) the return types are identical and the function argument is + * a pointer to a struct, too. */ + event->super.handler = (event_handler_t)callback; + ztimer_set(UNICOAP_CLOCK, &event->ztimer, duration); +} + +void unicoap_event_cancel(unicoap_scheduled_event_t* event) +{ + if (ztimer_is_set(UNICOAP_CLOCK, &event->ztimer)) { + /* event cancel runs in O(n), so we really only want to call + * event_cancel if the event has already been posted */ + event_cancel(&_queue, &event->super); + } + ztimer_remove(UNICOAP_CLOCK, &event->ztimer); +} + +/* MARK: - Lifecycle */ + +static inline int _init_drivers(event_queue_t* queue) +{ + (void)queue; +#if IS_USED(MODULE_UNICOAP_DRIVER_RFC7252_COMMON) + if (unicoap_init_rfc7252_common(queue) < 0) { + return -1; + } +#endif +#if IS_USED(MODULE_UNICOAP_DRIVER_UDP) + if (unicoap_init_udp(queue) < 0) { + return -1; + } +#endif +#if IS_USED(MODULE_UNICOAP_DRIVER_DTLS) + if (unicoap_init_dtls(queue) < 0) { + return -1; + } +#endif + /* MARK: unicoap_driver_extension_point */ + return 0; +} + +static inline int _deinit_drivers(event_queue_t* queue) +{ + (void)queue; + int res = 0; +#if IS_USED(MODULE_UNICOAP_DRIVER_RFC7252_COMMON) + res += unicoap_deinit_rfc7252_common(queue); +#endif +#if IS_USED(MODULE_UNICOAP_DRIVER_UDP) + res += unicoap_deinit_udp(queue); +#endif +#if IS_USED(MODULE_UNICOAP_DRIVER_DTLS) + res += unicoap_deinit_dtls(queue); +#endif + /* MARK: unicoap_driver_extension_point */ + return res; +} + +void* _unicoap_loop_run(void* arg) +{ + (void)arg; + /* Now we set the already-initialized queue's waiter thread. See unicoap_init below. */ + /* _queue.waiter == NULL is asserted by event_queue_claim */ + event_queue_claim(&_queue); + _unicoap_pid = thread_getpid(); + event_loop(&_queue); + return &_queue; +} + +int unicoap_loop_enqueue(unicoap_job_t* job) { + event_post(&_queue, &job->super); + return 0; +} + +kernel_pid_t unicoap_init(void) +{ + if (_unicoap_pid != KERNEL_PID_UNDEF) { + return -EEXIST; + } + + mutex_init(&_state.lock); + +#if IS_USED(MODULE_UNICOAP_RESOURCES_XFA) + /* add CoAP resources from XFA */ + XFA_USE_CONST(unicoap_resource_t, unicoap_resources_xfa); + static unicoap_listener_t _xfa_listener = { + .resources = unicoap_resources_xfa, + }; + + _xfa_listener.resource_count = XFA_LEN(unicoap_resource_t, unicoap_resources_xfa); + unicoap_listener_register(&_xfa_listener); + SERVER_DEBUG("registered %" PRIuSIZE " XFA resources\n", _xfa_listener.resource_count); +#endif + + event_queue_init_detached(&_queue); + + if (_init_drivers(&_queue) < 0) { + UNICOAP_DEBUG("driver initialization failed\n"); + return -1; + } + +#if IS_ACTIVE(CONFIG_UNICOAP_CREATE_THREAD) + static char _unicoap_thread_stack[UNICOAP_STACK_SIZE]; + _unicoap_pid = thread_create(_unicoap_thread_stack, sizeof(_unicoap_thread_stack), + THREAD_PRIORITY_MAIN - 1, 0, _unicoap_loop_run, + NULL, UNICOAP_THREAD_IDENTIFIER); +#endif + + return _unicoap_pid; +} + +int unicoap_deinit(void) +{ + if (_unicoap_pid == KERNEL_PID_UNDEF) { + return -ENOENT; + } + + _deinit_drivers(&_queue); + _unicoap_pid = KERNEL_PID_UNDEF; + memset(&_state, 0, sizeof(_state)); + memset(&_queue, 0, sizeof(_queue)); + + return 0; +} + +// MARK: - Listeners + +void unicoap_listener_register(unicoap_listener_t* listener) +{ + assert(listener); + /* That item will be overridden, ensure that the user expecting different + * behavior will notice this. */ + assert(listener->next == NULL); + + listener->next = _state.listeners; + _state.listeners = listener; + + if (!listener->link_encoder) { + listener->link_encoder = unicoap_resource_encode_link; + } + + if (!listener->request_matcher) { + listener->request_matcher = unicoap_resource_match_request_default; + } + + for (unsigned int i = 0; i < listener->resource_count; i += 1) { + if (IS_ACTIVE(CONFIG_UNICOAP_ASSIST)) { + if (!listener->resources[i].methods) { + unicoap_assist(API_WARNING("resource '%s' is inaccessible") + FIXIT("Set .methods"), + listener->resources[i].path); + } + + /* TODO: Advanced features: emit warnings */ + } + } +} + +int unicoap_listener_deregister(unicoap_listener_t* listener) +{ + assert(listener); + + unicoap_listener_t* l = _state.listeners; + while (l) { + if (l->next == listener) { + l->next = listener->next; + return 0; + } + l = l->next; + } + return -ENOENT; +} + +/* MARK: - Intersection with messaging */ + +void unicoap_generate_token(uint8_t* token) +{ + for (size_t i = 0; i < CONFIG_UNICOAP_GENERATED_TOKEN_LENGTH; i += 4) { + uint32_t _random = random_uint32_range(1, UINT32_MAX); + memcpy(&token[i], &_random, + (CONFIG_UNICOAP_GENERATED_TOKEN_LENGTH - i >= 4) ? + 4 : + CONFIG_UNICOAP_GENERATED_TOKEN_LENGTH - i); + } +} + +static void _debug_packet(const unicoap_packet_t* packet) +{ + DEBUG("in channel:"); + + DEBUG("\n\tremote="); + DEBUG_ENDPOINT(packet->remote); + + if (packet->local) { + DEBUG("\n\tlocal="); + DEBUG_ENDPOINT(packet->local); + } + + DEBUG("\n"); +} + +int unicoap_messaging_send(unicoap_packet_t* packet, unicoap_messaging_flags_t flags) +{ + (void)packet; + (void)flags; + + assert(packet); + assert(packet->remote); + assert(packet->message); + + MESSAGING_DEBUG("sending "); + _debug_packet(packet); + + switch (unicoap_packet_proto(packet)) { +#if IS_USED(MODULE_UNICOAP_DRIVER_RFC7252_COMMON) + case UNICOAP_PROTO_UDP: + case UNICOAP_PROTO_DTLS: + return unicoap_messaging_send_rfc7252(packet, flags); +#endif + /* MARK: unicoap_driver_extension_point */ + default: + MESSAGING_DEBUG("missing driver for proto %s\n", + unicoap_string_from_proto(unicoap_packet_proto(packet))); + unicoap_assist_emit_diagnostic_missing_driver(packet->remote->proto); + return -ENOTSUP; + } +} + +unicoap_preprocessing_result_t unicoap_exchange_preprocess(unicoap_packet_t* packet, + unicoap_messaging_flags_t* flags, + unicoap_exchange_arg_t* arg, + bool truncated) +{ + *flags = UNICOAP_MESSAGING_FLAGS_DEFAULT; + unicoap_message_t* message = packet->message; + int res = 0; + + MESSAGING_DEBUG("received "); + _debug_packet(packet); + + switch (unicoap_code_class(packet->message->code)) { + case UNICOAP_CODE_CLASS_REQUEST: { + if (truncated) { + SERVER_DEBUG("truncated, not processing, sending Size1\n"); + UNICOAP_OPTIONS_ALLOC(response_options, 5); + unicoap_options_set_size1(&response_options, CONFIG_UNICOAP_BLOCK_SIZE); + unicoap_response_init_with_options(message, UNICOAP_STATUS_REQUEST_ENTITY_TOO_LARGE, + NULL, 0, &response_options); + unicoap_messaging_send(packet, UNICOAP_MESSAGING_FLAGS_DEFAULT); + return UNICOAP_PREPROCESSING_ERROR_REQUEST; + } + + const unicoap_resource_t* resource = NULL; + const unicoap_listener_t* listener = NULL; + if ((res = unicoap_resource_find(packet, &resource, &listener)) != 0) { + unicoap_response_init_empty(message, res < 0 ? UNICOAP_STATUS_INTERNAL_SERVER_ERROR : + (unicoap_status_t)res); + unicoap_messaging_send(packet, UNICOAP_MESSAGING_FLAGS_DEFAULT); + return UNICOAP_PREPROCESSING_ERROR_REQUEST; + } + arg->resource = resource; + *flags = _messaging_flags_resource(resource->flags); + return UNICOAP_PREPROCESSING_SUCCESS_REQUEST; + } + + case UNICOAP_CODE_CLASS_RESPONSE_SUCCESS: + case UNICOAP_CODE_CLASS_RESPONSE_CLIENT_FAILURE: + case UNICOAP_CODE_CLASS_RESPONSE_SERVER_FAILURE: + /* TODO: Client: Process response */ + return UNICOAP_PREPROCESSING_ERROR_RESPONSE_UNEXPECTED; + + case UNICOAP_CODE_CLASS_SIGNAL: + /* TODO: Signaling */ + return UNICOAP_PREPROCESSING_ERROR_UNSUPPORTED; + + default: + MESSAGING_DEBUG("illegal code class %" PRIu8 "\n", unicoap_code_class(message->code)); + return UNICOAP_PREPROCESSING_ERROR_INVALID_CODE_CLASS; + } +} + +int unicoap_exchange_process(unicoap_packet_t* packet, unicoap_exchange_arg_t arg) { + switch (unicoap_code_class(packet->message->code)) { + case UNICOAP_CODE_CLASS_REQUEST: + return unicoap_server_process_request(packet, arg.resource); + + case UNICOAP_CODE_CLASS_RESPONSE_SUCCESS: + case UNICOAP_CODE_CLASS_RESPONSE_CLIENT_FAILURE: + case UNICOAP_CODE_CLASS_RESPONSE_SERVER_FAILURE: + /* TODO: Client: Process response */ + return -1; + + case UNICOAP_CODE_CLASS_SIGNAL: + /* TODO: Signaling */ + return -1; + + default: + UNREACHABLE(); + return -1; + } +} + +int unicoap_exchange_release_endpoint_state(const unicoap_endpoint_t* endpoint) +{ + (void)endpoint; + /* TODO: Client and advanced server features: Elaborate state management */ + /* TODO: Observe: Remove potential registrations */ + return -ENOENT; +} + +/* These must be in state.c as it reads the _state object. */ + +ssize_t unicoap_resource_core_link_format_build(char* buffer, size_t capacity, + unicoap_proto_t proto) +{ + unicoap_listener_t* listener = _state.listeners; + + char* out = (char*)buffer; + size_t pos = 0; + + unicoap_link_encoder_ctx_t ctx = { + .content_format = UNICOAP_FORMAT_LINK, + /* indicate initial link for the list */ + .uninitialized = true, + }; + + /* write payload */ + for (; listener != NULL; listener = listener->next) { + if (!listener->link_encoder) { + continue; + } + + if (!unicoap_match_proto(listener->protocols, proto)) { + continue; + } + ctx.link_pos = 0; + + for (; ctx.link_pos < listener->resource_count; ctx.link_pos += 1) { + if (!unicoap_match_proto(listener->resources[ctx.link_pos].protocols, proto)) { + continue; + } + ssize_t res; + if (out) { + res = listener->link_encoder(&listener->resources[ctx.link_pos], &out[pos], + capacity - pos, &ctx); + } + else { + res = listener->link_encoder(&listener->resources[ctx.link_pos], NULL, 0, &ctx); + } + + if (res > 0) { + pos += res; + ctx.uninitialized = false; + } + else { + break; + } + } + } + + return pos; +} + +int unicoap_resource_find(const unicoap_packet_t* packet, const unicoap_resource_t** resource_ptr, + const unicoap_listener_t** listener_ptr) +{ + assert(packet); + int ret = UNICOAP_STATUS_PATH_NOT_FOUND; + + char path[CONFIG_UNIOCOAP_PATH_LENGTH_MAX]; + ssize_t path_length = 0; + + /* Copying Uri-Paths once followed by calls to strcmp is cheaper than + * repeatedly parsing options and not allocating a buffer */ + if ((path_length = + unicoap_options_copy_uri_path(packet->message->options, path, sizeof(path))) <= 0) { + /* The Uri-Path options are longer than + * CONFIG_UNIOCOAP_PATH_SIZE_MAX, and thus do not match anything + * that could be found by this handler. */ + SERVER_DEBUG("could not copy Uri-Path\n"); + return UNICOAP_STATUS_PATH_NOT_FOUND; + } + + for (unicoap_listener_t* listener = _state.listeners; listener; listener = listener->next) { + const unicoap_resource_t* resource; + int res; + + if (!unicoap_match_proto(listener->protocols, unicoap_packet_proto(packet))) { + SERVER_DEBUG("ignoring listener, proto %s not in set\n", + unicoap_string_from_proto(unicoap_packet_proto(packet))); + continue; + } + + res = listener->request_matcher(path, (size_t)path_length, listener, + &resource, packet->message, packet->remote); + switch (res) { + case UNICOAP_STATUS_PATH_NOT_FOUND: + /* check next resource on mismatch */ + continue; + case UNICOAP_STATUS_METHOD_NOT_ALLOWED: + *resource_ptr = resource; + *listener_ptr = listener; + ret = res; + /* found a resource, but method/proto do not match */ + continue; + case 0: + *resource_ptr = resource; + *listener_ptr = listener; + SERVER_DEBUG("%s%s: found\n", (*resource_ptr)->path, + (*resource_ptr)->flags & UNICOAP_RESOURCE_FLAG_MATCH_SUBTREE ? "/**" : ""); + return 0; + default: + SERVER_DEBUG("error: resource matcher failed\n"); + /* res is probably UNICOAP_RESOURCE_ERROR or some other + * unhandled error */ + return res; + } + } + + if (IS_ACTIVE(ENABLE_DEBUG)) { + switch (ret) { + case UNICOAP_STATUS_METHOD_NOT_ALLOWED: + SERVER_DEBUG("%s%s: method %s not allowed\n", (*resource_ptr)->path, + (*resource_ptr)->flags & UNICOAP_RESOURCE_FLAG_MATCH_SUBTREE ? "/**" : "", + unicoap_string_from_method(unicoap_request_get_method(packet->message))); + break; + case UNICOAP_STATUS_PATH_NOT_FOUND: + SERVER_DEBUG("%.*s: resource not found\n", (int)path_length, path); + break; + default: + break; + } + } + + return ret; +} + +void unicoap_print_listeners(void) +{ + printf("\n\t- listeners:\n"); + size_t i = 0; + for (const unicoap_listener_t* listener = _state.listeners; listener; + listener = listener->next) { + printf("\t\t- listener #%" PRIuSIZE "\n", i); + + printf("\t\t\t- request_matcher="); + if (listener->request_matcher == unicoap_resource_match_request_default) { + printf("\n"); + } + else { + printf("\n", listener->request_matcher); + } + + printf("\t\t\t- link_encoder=\n", listener->link_encoder); + + printf("\t\t\t- protocols="); + unicoap_print_protocols(listener->protocols); + printf("\n"); + + printf("\t\t\t- resources (%" PRIuSIZE "):\n", listener->resource_count); + for (size_t k = 0; k < listener->resource_count; k += 1) { + const unicoap_resource_t* resource = &listener->resources[k]; + + printf("\t\t\t\t- resource #%" PRIuSIZE " %s\n", k, resource->path); + + printf("\t\t\t\t\t- flags="); + unicoap_print_resource_flags(resource->flags); + printf("\n"); + + printf("\t\t\t\t\t- methods="); + unicoap_print_methods(resource->methods); + printf("\n"); + + printf("\t\t\t\t\t- protocols="); + unicoap_print_protocols(resource->protocols); + printf("\n"); + + printf("\t\t\t\t\t- handler=\n", resource->handler); + printf("\t\t\t\t\t- argument=\n", resource->handler_arg); + } + + i += 1; + } +} diff --git a/sys/net/application_layer/unicoap/utils.c b/sys/net/application_layer/unicoap/utils.c index 9457285f46c4..bf02097ba7ec 100644 --- a/sys/net/application_layer/unicoap/utils.c +++ b/sys/net/application_layer/unicoap/utils.c @@ -207,7 +207,7 @@ ssize_t unicoap_pdu_build_options_and_payload(uint8_t* pdu, size_t capacity, } } -bool unicoap_message_is_response(uint8_t code) +bool unicoap_message_code_is_response(uint8_t code) { switch (unicoap_code_class(code)) { case UNICOAP_CODE_CLASS_RESPONSE_SUCCESS: @@ -468,3 +468,62 @@ void unicoap_options_dump_all(const unicoap_options_t* options) printf(">\n"); } } + +void unicoap_print_protocols(unicoap_proto_set_t protocols) +{ + if (likely(protocols == UNICOAP_PROTOCOLS_ALLOW_ALL)) { + printf(""); + } + else { + printf("[ "); + for (unicoap_proto_t p = 1; p < (sizeof(unicoap_proto_t) * 8); p += 1) { + if (protocols & UNICOAP_PROTOCOL_FLAG(p)) { + printf("%s ", unicoap_string_from_proto(p)); + } + } + printf("]"); + } +} + +void unicoap_print_methods(unicoap_method_set_t methods) +{ + printf("[ "); + for (unicoap_method_t m = 1; m < (sizeof(unicoap_method_t) * 8); m += 1) { + if (methods & UNICOAP_METHOD_FLAG(m)) { + printf("%s ", unicoap_string_from_method(m)); + } + } + printf("]"); +} + +void unicoap_print_resource_flags(unicoap_resource_flags_t flags) +{ + printf("[ "); + if (flags & UNICOAP_RESOURCE_FLAG_RELIABLE) { + printf("RELIABLE "); + } + if (flags & UNICOAP_RESOURCE_FLAG_MATCH_SUBTREE) { + printf("MATCH_SUBTREE "); + } + printf("]"); +} + +void unicoap_assist_emit_diagnostic_missing_driver(unicoap_proto_t proto) +{ + if (IS_ACTIVE(CONFIG_UNICOAP_ASSIST)) { + switch (proto) { + case UNICOAP_PROTO_UDP: + unicoap_assist(API_ERROR("CoAP over UDP driver missing") + FIXIT("USEMODULE += unicoap_driver_udp")); + break; + + case UNICOAP_PROTO_DTLS: + unicoap_assist(API_ERROR("CoAP over DTLS driver missing") + FIXIT("USEMODULE += unicoap_driver_dtls")); + break; + + default: + break; + } + } +} diff --git a/tests/unittests/tests-unicoap/tests-unicoap-matching.c b/tests/unittests/tests-unicoap/tests-unicoap-matching.c new file mode 100644 index 000000000000..745dc8e48d22 --- /dev/null +++ b/tests/unittests/tests-unicoap/tests-unicoap-matching.c @@ -0,0 +1,217 @@ +/* + * Copyright (C) 2024-2025 Carl Seifert + * Copyright (C) 2024-2025 TU Dresden + * + * This file is subject to the terms and conditions of the GNU Lesser General + * Public License v2.1. See the file LICENSE in the top level directory for + * more details. + */ + +/** + * @file + * @ingroup unittests + * @brief Unit tests for testing path matching functions + * @author Carl Seifert + */ + +#include +#include + +#include "tests-unicoap.h" + +#include "net/unicoap.h" + +#define _MATCH_STRING(resource, string) \ + unicoap_resource_match_path_string(resource, string, static_strlen(string)) + +static void _test_root_with_string(bool match_subtree) { + unicoap_resource_t r = { + .path = "/", + .flags = match_subtree ? UNICOAP_RESOURCE_FLAG_MATCH_SUBTREE : 0 + }; + + _TEST_ASSERT_TRUE(_MATCH_STRING(&r, "/")); + TEST_ASSERT_EQUAL_INT(_MATCH_STRING(&r, "/a"), match_subtree); +} + +static void test_root_with_string(void) { + _test_root_with_string(false); +} + +static void test_root_with_string_subtree(void) { + _test_root_with_string(true); +} + +static void _test_root_with_options(bool match_subtree) { + unicoap_resource_t r = { + .path = "/", + .flags = match_subtree ? UNICOAP_RESOURCE_FLAG_MATCH_SUBTREE : 0 + }; + + UNICOAP_OPTIONS_ALLOC(options, 2); + + _TEST_ASSERT_TRUE(unicoap_resource_match_path_options(&r, &options)); + + unicoap_options_add_uri_path_component_string(&options, "a"); + TEST_ASSERT_EQUAL_INT(unicoap_resource_match_path_options(&r, &options), match_subtree); +} + +static void test_root_with_options(void) { + _test_root_with_options(false); +} + +static void test_root_with_options_subtree(void) { + _test_root_with_options(true); +} + +static void _test_simple_with_string(bool match_subtree) { + unicoap_resource_t r = { + .path = "/a", + .flags = match_subtree ? UNICOAP_RESOURCE_FLAG_MATCH_SUBTREE : 0 + }; + + _TEST_ASSERT_FALSE(_MATCH_STRING(&r, "/")); + _TEST_ASSERT_FALSE(_MATCH_STRING(&r, "a")); + _TEST_ASSERT_TRUE(_MATCH_STRING(&r, "/a")); + _TEST_ASSERT_TRUE(_MATCH_STRING(&r, "/a/")); + _TEST_ASSERT_FALSE(_MATCH_STRING(&r, "/aa")); + TEST_ASSERT_EQUAL_INT(_MATCH_STRING(&r, "/a/a"), match_subtree); +} + +static void test_simple_with_string(void) { + _test_simple_with_string(false); +} + +static void test_simple_with_string_subtree(void) { + _test_simple_with_string(true); +} + +static void _test_simple_with_options(bool match_subtree) { + unicoap_resource_t r = { + .path = "/a", + .flags = match_subtree ? UNICOAP_RESOURCE_FLAG_MATCH_SUBTREE : 0 + }; + UNICOAP_OPTIONS_ALLOC(options, 10); + + printf("not working\n"); + _TEST_ASSERT_FALSE(unicoap_resource_match_path_options(&r, &options)); + + unicoap_options_add_uri_path_component_string(&options, "a"); + _TEST_ASSERT_TRUE(unicoap_resource_match_path_options(&r, &options)); + + unicoap_options_clear(&options); + + unicoap_options_add_uri_path_component_string(&options, "a"); + unicoap_options_add_uri_path_component_string(&options, "a"); + TEST_ASSERT_EQUAL_INT(unicoap_resource_match_path_options(&r, &options), match_subtree); +} + +static void test_simple_with_options(void) { + _test_simple_with_options(false); +} + +static void test_simple_with_options_subtree(void) { + _test_simple_with_options(true); +} + +static void _test_long_with_string(bool match_subtree) { + unicoap_resource_t r = { + .path = "/a123/b567", + .flags = match_subtree ? UNICOAP_RESOURCE_FLAG_MATCH_SUBTREE : 0 + }; + + _TEST_ASSERT_FALSE(_MATCH_STRING(&r, "/")); + _TEST_ASSERT_FALSE(_MATCH_STRING(&r, "a")); + _TEST_ASSERT_FALSE(_MATCH_STRING(&r, "/a")); + _TEST_ASSERT_FALSE(_MATCH_STRING(&r, "/a123")); + _TEST_ASSERT_FALSE(_MATCH_STRING(&r, "/a123/")); + _TEST_ASSERT_TRUE(_MATCH_STRING(&r, "/a123/b567")); + _TEST_ASSERT_TRUE(_MATCH_STRING(&r, "/a123/b567/")); + TEST_ASSERT_EQUAL_INT(_MATCH_STRING(&r, "/a123/b567/c89"), match_subtree); + TEST_ASSERT_EQUAL_INT(_MATCH_STRING(&r, "/a123/b567/c89/"), match_subtree); + TEST_ASSERT_EQUAL_INT(_MATCH_STRING(&r, "/a123/b567/c89/d00"), match_subtree); +} + +static void test_long_with_string(void) { + _test_long_with_string(false); +} + +static void test_long_with_string_subtree(void) { + _test_long_with_string(true); +} + +static void _test_long_with_options(bool match_subtree) { + unicoap_resource_t r = { + .path = "/a123/b567", + .flags = match_subtree ? UNICOAP_RESOURCE_FLAG_MATCH_SUBTREE : 0 + }; + + UNICOAP_OPTIONS_ALLOC(options, 20); + _TEST_ASSERT_FALSE(unicoap_resource_match_path_options(&r, &options)); + + unicoap_options_add_uri_path_component_string(&options, "a"); + _TEST_ASSERT_FALSE(unicoap_resource_match_path_options(&r, &options)); + + unicoap_options_clear(&options); + + unicoap_options_add_uri_path_component_string(&options, "a123"); + _TEST_ASSERT_FALSE(unicoap_resource_match_path_options(&r, &options)); + + unicoap_options_clear(&options); + + unicoap_options_add_uri_path_component_string(&options, "a123"); + unicoap_options_add_uri_path_component_string(&options, "b56"); + _TEST_ASSERT_FALSE(unicoap_resource_match_path_options(&r, &options)); + + unicoap_options_clear(&options); + + unicoap_options_add_uri_path_component_string(&options, "a123"); + unicoap_options_add_uri_path_component_string(&options, "b567"); + _TEST_ASSERT_TRUE(unicoap_resource_match_path_options(&r, &options)); + + unicoap_options_clear(&options); + + unicoap_options_add_uri_path_component_string(&options, "a123"); + unicoap_options_add_uri_path_component_string(&options, "b567"); + unicoap_options_add_uri_path_component_string(&options, "c89"); + TEST_ASSERT_EQUAL_INT(unicoap_resource_match_path_options(&r, &options), match_subtree); + + unicoap_options_clear(&options); + + unicoap_options_add_uri_path_component_string(&options, "a123"); + unicoap_options_add_uri_path_component_string(&options, "b567"); + unicoap_options_add_uri_path_component_string(&options, "c89"); + unicoap_options_add_uri_path_component_string(&options, "d00"); + TEST_ASSERT_EQUAL_INT(unicoap_resource_match_path_options(&r, &options), match_subtree); +} + +static void test_long_with_options(void) { + _test_long_with_options(false); +} + +static void test_long_with_options_subtree(void) { + _test_long_with_options(true); +} + + +Test* tests_unicoap_matching(void) +{ + EMB_UNIT_TESTFIXTURES(fixtures){ + new_TestFixture(test_root_with_string), + new_TestFixture(test_root_with_string_subtree), + new_TestFixture(test_root_with_options), + new_TestFixture(test_root_with_options_subtree), + new_TestFixture(test_simple_with_string), + new_TestFixture(test_simple_with_string_subtree), + new_TestFixture(test_simple_with_options), + new_TestFixture(test_simple_with_options_subtree), + new_TestFixture(test_long_with_string), + new_TestFixture(test_long_with_string_subtree), + new_TestFixture(test_long_with_options), + new_TestFixture(test_long_with_options_subtree), + }; + + EMB_UNIT_TESTCALLER(test_unicoap, NULL, NULL, fixtures); + + return (Test*)&test_unicoap; +} diff --git a/tests/unittests/tests-unicoap/tests-unicoap.c b/tests/unittests/tests-unicoap/tests-unicoap.c index 43fba0c2d771..5414d18f80cb 100644 --- a/tests/unittests/tests-unicoap/tests-unicoap.c +++ b/tests/unittests/tests-unicoap/tests-unicoap.c @@ -15,14 +15,17 @@ */ #include "tests-unicoap.h" +#include Test* tests_unicoap_options(void); Test* tests_unicoap_pdu(void); Test* tests_unicoap_message(void); +Test* tests_unicoap_matching(void); void tests_unicoap(void) { TESTS_RUN(tests_unicoap_pdu()); TESTS_RUN(tests_unicoap_options()); TESTS_RUN(tests_unicoap_message()); + TESTS_RUN(tests_unicoap_matching()); } diff --git a/tests/unittests/tests-unicoap/tests-unicoap.h b/tests/unittests/tests-unicoap/tests-unicoap.h index 808e47198962..8fcb1bd82e36 100644 --- a/tests/unittests/tests-unicoap/tests-unicoap.h +++ b/tests/unittests/tests-unicoap/tests-unicoap.h @@ -38,6 +38,12 @@ # define _TEST_ASSERT_EQUAL_POINTER(a, b) \ TEST_ASSERT_EQUAL_INT((uintptr_t)a, (uintptr_t)b) +# define _TEST_ASSERT_TRUE(a) \ + TEST_ASSERT_EQUAL_INT(a, true) + +# define _TEST_ASSERT_FALSE(a) \ + TEST_ASSERT_EQUAL_INT(a, false) + # define _BYTES(...) ((uint8_t[]){ __VA_ARGS__ }) # define _UINT4_MAX (12)