/* * Copyright (C) 2016-18 Kaspar Schleiser * 2018 Inria * 2018 Freie Universität Berlin * * 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. */ /** * @ingroup net_nanocoap * @{ * * @file * @brief Nanocoap sock helpers * * @author Kaspar Schleiser * @author Benjamin Valentin * * @} */ #include #include #include #include "atomic_utils.h" #include "net/credman.h" #include "net/nanocoap_sock.h" #include "net/sock/util.h" #include "net/sock/udp.h" #include "net/iana/portrange.h" #include "random.h" #include "sys/uio.h" #include "timex.h" #include "ztimer.h" #define ENABLE_DEBUG 0 #include "debug.h" /** * @brief Size of the buffer used for the DTLS handshake * * This size was found suitable for DTLS using a simple PSK in mode AES_128_CCM_8. * DTLS places no restriction on its handshake package size therefore this might need change, * if mode or key-size change especially if certificates instead of PSK are used. */ #ifndef CONFIG_NANOCOAP_DTLS_HANDSHAKE_BUF_SIZE #define CONFIG_NANOCOAP_DTLS_HANDSHAKE_BUF_SIZE (160) #endif enum { STATE_REQUEST_SEND, /**< request was just sent or will be sent again */ STATE_RESPONSE_RCVD, /**< response received but might be invalid */ STATE_RESPONSE_OK, /**< valid response was received */ }; typedef struct { coap_blockwise_cb_t callback; void *arg; bool more; } _block_ctx_t; static uint16_t _get_id(void) { __attribute__((section(".noinit"))) static uint16_t id; return atomic_fetch_add_u16(&id, 1); } #if IS_USED(MODULE_NANOCOAP_DTLS) int nanocoap_sock_dtls_connect(nanocoap_sock_t *sock, sock_udp_ep_t *local, const sock_udp_ep_t *remote, credman_tag_t tag) { int res; uint32_t timeout_ms = CONFIG_NANOCOAP_SOCK_DTLS_TIMEOUT_MS; uint8_t retries = CONFIG_NANOCOAP_SOCK_DTLS_RETRIES; bool auto_port = local->port == 0; do { if (auto_port) { /* choose random ephemeral port, since DTLS requires a local port */ local->port = random_uint32_range(IANA_DYNAMIC_PORTRANGE_MIN, IANA_DYNAMIC_PORTRANGE_MAX); } /* connect UDP socket */ res = nanocoap_sock_connect(sock, local, remote); } while (auto_port && (res == -EADDRINUSE)); if (res < 0) { return res; } /* create DTLS socket on to of UDP socket */ res = sock_dtls_create(&sock->dtls, &sock->udp, tag, SOCK_DTLS_1_2, SOCK_DTLS_CLIENT); if (res < 0) { DEBUG("Unable to create DTLS sock: %s\n", strerror(-res)); nanocoap_sock_close(sock); return res; } sock->type = COAP_SOCKET_TYPE_DTLS; while (1) { uint8_t buf[CONFIG_NANOCOAP_DTLS_HANDSHAKE_BUF_SIZE]; mutex_t lock = MUTEX_INIT_LOCKED; ztimer_t timeout; /* unlock lock after timeout */ ztimer_mutex_unlock(ZTIMER_MSEC, &timeout, timeout_ms, &lock); /* create DTLS session */ res = sock_dtls_session_init(&sock->dtls, remote, &sock->dtls_session); if (res >= 0) { /* handle handshake */ res = sock_dtls_recv(&sock->dtls, &sock->dtls_session, buf, sizeof(buf), timeout_ms * US_PER_MS); if (res == -SOCK_DTLS_HANDSHAKE) { DEBUG("DTLS handshake successful\n"); ztimer_remove(ZTIMER_MSEC, &timeout); return 0; } DEBUG("Unable to establish DTLS handshake: %s\n", strerror(-res)); } else { DEBUG("Unable to initialize DTLS session: %s\n", strerror(-res)); } sock_dtls_session_destroy(&sock->dtls, &sock->dtls_session); if (retries--) { /* wait for timeout to expire */ mutex_lock(&lock); } else { ztimer_remove(ZTIMER_MSEC, &timeout); break; } /* see https://datatracker.ietf.org/doc/html/rfc6347#section-4.2.4.1 */ timeout_ms *= 2U; } nanocoap_sock_close(sock); return res; } #else int nanocoap_sock_dtls_connect(nanocoap_sock_t *sock, const sock_udp_ep_t *local, const sock_udp_ep_t *remote, credman_tag_t tag) { (void)sock; (void)local; (void)remote; (void)tag; return -ENOTSUP; } #endif static int _get_error(const coap_pkt_t *pkt) { switch (coap_get_code_class(pkt)) { case COAP_CLASS_CLIENT_FAILURE: return -ENXIO; case COAP_CLASS_SERVER_FAILURE: return -ENETRESET; default: return 0; } } static inline nanocoap_socket_type_t _get_type(nanocoap_sock_t *sock) { #if IS_USED(MODULE_NANOCOAP_DTLS) return sock->type; #else (void)sock; return COAP_SOCKET_TYPE_UDP; #endif } static int _sock_sendv(nanocoap_sock_t *sock, const iolist_t *snips) { switch (_get_type(sock)) { case COAP_SOCKET_TYPE_UDP: return sock_udp_sendv(&sock->udp, snips, NULL); #if IS_USED(MODULE_NANOCOAP_DTLS) case COAP_SOCKET_TYPE_DTLS: return sock_dtls_sendv(&sock->dtls, &sock->dtls_session, snips, CONFIG_NANOCOAP_SOCK_DTLS_TIMEOUT_MS); #endif default: assert(0); return -EINVAL; } } static int _sock_recv_buf(nanocoap_sock_t *sock, void **data, void **ctx, uint32_t timeout) { switch (_get_type(sock)) { case COAP_SOCKET_TYPE_UDP: return sock_udp_recv_buf(&sock->udp, data, ctx, timeout, NULL); #if IS_USED(MODULE_NANOCOAP_DTLS) case COAP_SOCKET_TYPE_DTLS: return sock_dtls_recv_buf(&sock->dtls, &sock->dtls_session, data, ctx, timeout); #endif default: assert(0); return -EINVAL; } } static int _send_ack(nanocoap_sock_t *sock, coap_pkt_t *pkt) { coap_hdr_t ack; unsigned tkl = coap_get_token_len(pkt); const iolist_t snip = { .iol_base = &ack, .iol_len = sizeof(ack), }; coap_build_hdr(&ack, COAP_TYPE_ACK, coap_get_token(pkt), tkl, COAP_CODE_EMPTY, ntohs(pkt->hdr->id)); return _sock_sendv(sock, &snip); } static bool _id_or_token_missmatch(const coap_pkt_t *pkt, unsigned id, const void *token, size_t token_len) { switch (coap_get_type(pkt)) { case COAP_TYPE_RST: case COAP_TYPE_ACK: return coap_get_id(pkt) != id; default: if (coap_get_token_len(pkt) != token_len) { return true; } return memcmp(coap_get_token(pkt), token, token_len); } } static uint32_t _deadline_from_interval(uint32_t interval) { return US_PER_MS * ztimer_now(ZTIMER_MSEC) + interval; } static uint32_t _deadline_left_us(uint32_t deadline) { uint32_t now = ztimer_now(ZTIMER_MSEC) * US_PER_MS; if (now > deadline) { return 0; } return deadline - now; } ssize_t nanocoap_sock_request_cb(nanocoap_sock_t *sock, coap_pkt_t *pkt, coap_request_cb_t cb, void *arg) { ssize_t tmp, res = 0; const unsigned id = coap_get_id(pkt); void *payload, *ctx = NULL; const uint8_t *token = coap_get_token(pkt); uint8_t token_len = coap_get_token_len(pkt); uint8_t state = STATE_REQUEST_SEND; /* random timeout, deadline for receive retries */ uint32_t timeout = random_uint32_range(CONFIG_COAP_ACK_TIMEOUT_MS * US_PER_MS, CONFIG_COAP_ACK_TIMEOUT_MS * CONFIG_COAP_RANDOM_FACTOR_1000); uint32_t deadline = _deadline_from_interval(timeout); /* check if we expect a reply */ const bool confirmable = coap_get_type(pkt) == COAP_TYPE_CON; /* add 1 for initial transmit, retry only when CONfirmable */ unsigned tries_left = confirmable * CONFIG_COAP_MAX_RETRANSMIT + 1; /* Create the first payload snip from the request buffer */ iolist_t head = { .iol_next = pkt->snips, .iol_base = pkt->hdr, .iol_len = coap_get_total_len(pkt), }; while (1) { switch (state) { case STATE_REQUEST_SEND: if (tries_left == 0) { DEBUG("nanocoap: maximum retries reached\n"); return -ETIMEDOUT; } --tries_left; DEBUG("nanocoap: send %u bytes (%u tries left)\n", (unsigned)iolist_size(&head), tries_left); res = _sock_sendv(sock, &head); if (res <= 0) { DEBUG("nanocoap: error sending coap request, %d\n", (int)res); return res; } /* no response needed and no response handler given */ if (!confirmable && !cb) { return 0; } /* ctx must have been released at this point */ assert(ctx == NULL); /* fall-through */ case STATE_RESPONSE_RCVD: case STATE_RESPONSE_OK: if (ctx == NULL) { DEBUG("nanocoap: waiting for response (timeout: %"PRIu32" µs)\n", _deadline_left_us(deadline)); } const void *old_ctx = ctx; tmp = _sock_recv_buf(sock, &payload, &ctx, _deadline_left_us(deadline)); /* sock_udp_recv_buf() is supposed to return multiple packet fragments * when called multiple times with the same context. * In practise, this is not implemented and it will always return a pointer * to the whole packet on the first call and NULL on the second call, which * releases the packet. * This assertion will trigger should the behavior change in the future. */ if (old_ctx) { assert(tmp == 0 && ctx == NULL); } if (tmp == 0) { /* no more data */ /* sock_udp_recv_buf() needs to be called in a loop until ctx is NULL again * to release the buffer */ if (state == STATE_RESPONSE_RCVD) { continue; } return res; } res = tmp; if (res == -ETIMEDOUT) { DEBUG("nanocoap: timeout waiting for response\n"); timeout *= 2; deadline = _deadline_from_interval(timeout); state = STATE_REQUEST_SEND; continue; } if (res < 0) { DEBUG("nanocoap: error receiving coap response, %d\n", (int)res); return res; } /* parse response */ state = STATE_RESPONSE_RCVD; if (coap_parse(pkt, payload, res) < 0) { DEBUG("nanocoap: error parsing packet\n"); continue; } else if (_id_or_token_missmatch(pkt, id, token, token_len)) { DEBUG("nanocoap: ID mismatch %u != %u\n", coap_get_id(pkt), id); continue; } state = STATE_RESPONSE_OK; DEBUG("nanocoap: response code=%i\n", coap_get_code(pkt)); switch (coap_get_type(pkt)) { case COAP_TYPE_RST: /* TODO: handle different? */ res = -EBADMSG; break; case COAP_TYPE_CON: _send_ack(sock, pkt); /* fall-through */ case COAP_TYPE_ACK: if (cb && coap_get_code(pkt) == COAP_CODE_EMPTY) { /* empty ACK, wait for separate response */ state = STATE_RESPONSE_RCVD; deadline = _deadline_from_interval(CONFIG_COAP_SEPARATE_RESPONSE_TIMEOUT_MS * US_PER_MS); DEBUG("nanocoap: wait for separate response\n"); continue; } /* fall-through */ case COAP_TYPE_NON: /* call user callback */ if (cb) { res = cb(arg, pkt); } else { res = _get_error(pkt); } break; } } } return res; } static int _request_cb(void *arg, coap_pkt_t *pkt) { struct iovec *buf = arg; size_t pkt_len = coap_get_total_len(pkt); int res = _get_error(pkt); if (res) { return res; } if (pkt_len > buf->iov_len) { return -ENOBUFS; } memcpy(buf->iov_base, pkt->hdr, pkt_len); pkt->hdr = buf->iov_base; pkt->payload = (uint8_t*)pkt->hdr + (pkt_len - pkt->payload_len); return pkt_len; } ssize_t nanocoap_sock_request(nanocoap_sock_t *sock, coap_pkt_t *pkt, size_t len) { struct iovec buf = { .iov_base = pkt->hdr, .iov_len = len, }; return nanocoap_sock_request_cb(sock, pkt, _request_cb, &buf); } static int _get_put_cb(void *arg, coap_pkt_t *pkt) { struct iovec *buf = arg; int res = _get_error(pkt); if (res) { return res; } if (pkt->payload_len > buf->iov_len) { return -ENOBUFS; } memcpy(buf->iov_base, pkt->payload, pkt->payload_len); return pkt->payload_len; } ssize_t nanocoap_sock_get(nanocoap_sock_t *sock, const char *path, void *buf, size_t len) { uint8_t *pktpos = buf; coap_pkt_t pkt = { .hdr = buf, }; struct iovec ctx = { .iov_base = buf, .iov_len = len, }; pktpos += coap_build_hdr(pkt.hdr, COAP_TYPE_CON, NULL, 0, COAP_METHOD_GET, _get_id()); pktpos += coap_opt_put_uri_path(pktpos, 0, path); pkt.payload = pktpos; pkt.payload_len = 0; return nanocoap_sock_request_cb(sock, &pkt, _get_put_cb, &ctx); } ssize_t _sock_put_post(nanocoap_sock_t *sock, const char *path, unsigned code, uint8_t type, const void *request, size_t len, void *response, size_t max_len) { /* buffer for CoAP header */ uint8_t buffer[CONFIG_NANOCOAP_BLOCK_HEADER_MAX]; uint8_t *pktpos = buffer; iolist_t payload = { .iol_base = (void *)request, .iol_len = len, }; coap_pkt_t pkt = { .hdr = (void *)buffer, .snips = &payload, }; struct iovec ctx = { .iov_base = response, .iov_len = max_len, }; pktpos += coap_build_hdr(pkt.hdr, type, NULL, 0, code, _get_id()); pktpos += coap_opt_put_uri_path(pktpos, 0, path); if (response == NULL && type == COAP_TYPE_NON) { /* all responses (2.xx, 4.xx and 5.xx) are ignored */ pktpos += coap_opt_put_uint(pktpos, COAP_OPT_URI_PATH, COAP_OPT_NO_RESPONSE, 26); } if (len) { /* set payload marker */ *pktpos++ = 0xFF; } pkt.payload = pktpos; pkt.payload_len = 0; return nanocoap_sock_request_cb(sock, &pkt, response ? _get_put_cb : NULL, &ctx); } ssize_t nanocoap_sock_put(nanocoap_sock_t *sock, const char *path, const void *request, size_t len, void *response, size_t len_max) { return _sock_put_post(sock, path, COAP_METHOD_PUT, COAP_TYPE_CON, request, len, response, len_max); } ssize_t nanocoap_sock_post(nanocoap_sock_t *sock, const char *path, const void *request, size_t len, void *response, size_t len_max) { return _sock_put_post(sock, path, COAP_METHOD_POST, COAP_TYPE_CON, request, len, response, len_max); } ssize_t nanocoap_sock_put_non(nanocoap_sock_t *sock, const char *path, const void *request, size_t len, void *response, size_t len_max) { return _sock_put_post(sock, path, COAP_METHOD_PUT, COAP_TYPE_NON, request, len, response, len_max); } ssize_t nanocoap_sock_post_non(nanocoap_sock_t *sock, const char *path, const void *request, size_t len, void *response, size_t len_max) { return _sock_put_post(sock, path, COAP_METHOD_POST, COAP_TYPE_NON, request, len, response, len_max); } static ssize_t _sock_put_post_url(const char *url, unsigned code, const void *request, size_t len, void *response, size_t len_max) { nanocoap_sock_t sock; int res = nanocoap_sock_url_connect(url, &sock); if (res) { return res; } res = _sock_put_post(&sock, sock_urlpath(url), code, COAP_TYPE_CON, request, len, response, len_max); nanocoap_sock_close(&sock); return res; } ssize_t nanocoap_sock_put_url(const char *url, const void *request, size_t len, void *response, size_t len_max) { return _sock_put_post_url(url, COAP_METHOD_PUT, request, len, response, len_max); } ssize_t nanocoap_sock_post_url(const char *url, const void *request, size_t len, void *response, size_t len_max) { return _sock_put_post_url(url, COAP_METHOD_POST, request, len, response, len_max); } ssize_t nanocoap_sock_delete(nanocoap_sock_t *sock, const char *path) { /* buffer for CoAP header */ uint8_t buffer[CONFIG_NANOCOAP_BLOCK_HEADER_MAX]; uint8_t *pktpos = buffer; coap_pkt_t pkt = { .hdr = (void *)pktpos, }; pktpos += coap_build_hdr(pkt.hdr, COAP_TYPE_CON, NULL, 0, COAP_METHOD_DELETE, _get_id()); pktpos += coap_opt_put_uri_path(pktpos, 0, path); pkt.payload = pktpos; return nanocoap_sock_request_cb(sock, &pkt, NULL, NULL); } ssize_t nanocoap_sock_delete_url(const char *url) { nanocoap_sock_t sock; int res = nanocoap_sock_url_connect(url, &sock); if (res) { return res; } res = nanocoap_sock_delete(&sock, sock_urlpath(url)); nanocoap_sock_close(&sock); return res; } ssize_t nanocoap_request(coap_pkt_t *pkt, const sock_udp_ep_t *local, const sock_udp_ep_t *remote, size_t len) { int res; nanocoap_sock_t sock; res = nanocoap_sock_connect(&sock, local, remote); if (res) { return res; } res = nanocoap_sock_request(&sock, pkt, len); nanocoap_sock_close(&sock); return res; } ssize_t nanocoap_get(const sock_udp_ep_t *remote, const char *path, void *buf, size_t len) { int res; nanocoap_sock_t sock; res = nanocoap_sock_connect(&sock, NULL, remote); if (res) { return res; } res = nanocoap_sock_get(&sock, path, buf, len); nanocoap_sock_close(&sock); return res; } static int _block_cb(void *arg, coap_pkt_t *pkt) { _block_ctx_t *ctx = arg; coap_block1_t block2; int res = _get_error(pkt); if (res) { return res; } /* response was not block-wise */ if (!coap_get_block2(pkt, &block2)) { block2.offset = 0; block2.more = false; } ctx->more = block2.more; return ctx->callback(ctx->arg, block2.offset, pkt->payload, pkt->payload_len, block2.more); } static int _fetch_block(nanocoap_sock_t *sock, uint8_t *buf, size_t len, const char *path, coap_blksize_t blksize, unsigned num, _block_ctx_t *ctx) { coap_pkt_t pkt = { .hdr = (void *)buf, }; uint16_t lastonum = 0; buf += coap_build_hdr(pkt.hdr, COAP_TYPE_CON, NULL, 0, COAP_METHOD_GET, _get_id()); buf += coap_opt_put_uri_pathquery(buf, &lastonum, path); buf += coap_opt_put_uint(buf, lastonum, COAP_OPT_BLOCK2, (num << 4) | blksize); (void)len; assert((uintptr_t)buf - (uintptr_t)pkt.hdr < len); pkt.payload = buf; pkt.payload_len = 0; return nanocoap_sock_request_cb(sock, &pkt, _block_cb, ctx); } int nanocoap_sock_block_request(coap_block_request_t *req, const void *data, size_t len, bool more, coap_request_cb_t callback, void *arg) { /* clip the payload at the block size */ if (len > coap_szx2size(req->blksize)) { len = coap_szx2size(req->blksize); more = true; } int res; uint8_t buf[CONFIG_NANOCOAP_BLOCK_HEADER_MAX]; iolist_t snip = { .iol_base = (void *)data, .iol_len = len, }; coap_pkt_t pkt = { .hdr = (void *)buf, .snips = &snip, }; uint8_t *pktpos = (void *)pkt.hdr; uint16_t lastonum = 0; pktpos += coap_build_hdr(pkt.hdr, COAP_TYPE_CON, NULL, 0, req->method, _get_id()); pktpos += coap_opt_put_uri_pathquery(pktpos, &lastonum, req->path); pktpos += coap_opt_put_uint(pktpos, lastonum, COAP_OPT_BLOCK1, (req->blknum << 4) | req->blksize | (more ? 0x8 : 0)); if (len) { /* set payload marker */ *pktpos++ = 0xFF; } pkt.payload = pktpos; pkt.payload_len = 0; res = nanocoap_sock_request_cb(req->sock, &pkt, callback, arg); if (res < 0) { return res; } ++req->blknum; return len; } int nanocoap_sock_get_blockwise(nanocoap_sock_t *sock, const char *path, coap_blksize_t blksize, coap_blockwise_cb_t callback, void *arg) { uint8_t buf[CONFIG_NANOCOAP_BLOCK_HEADER_MAX]; _block_ctx_t ctx = { .callback = callback, .arg = arg, .more = true, }; unsigned num = 0; while (ctx.more) { DEBUG("fetching block %u\n", num); int res = _fetch_block(sock, buf, sizeof(buf), path, blksize, num, &ctx); if (res < 0) { DEBUG("error fetching block %u: %d\n", num, res); return res; } num += 1; } return 0; } int nanocoap_sock_url_connect(const char *url, nanocoap_sock_t *sock) { char hostport[CONFIG_SOCK_HOSTPORT_MAXLEN]; sock_udp_ep_t remote; bool is_coaps = false; if (IS_USED(MODULE_NANOCOAP_DTLS) && !strncmp(url, "coaps://", 8)) { DEBUG("nanocoap: CoAPS URL detected\n"); is_coaps = true; } if (!is_coaps && strncmp(url, "coap://", 7)) { DEBUG("nanocoap: URL doesn't start with \"coap://\"\n"); return -EINVAL; } if (sock_urlsplit(url, hostport, NULL) < 0) { DEBUG("nanocoap: invalid URL\n"); return -EINVAL; } if (sock_udp_name2ep(&remote, hostport) < 0) { DEBUG("nanocoap: invalid URL\n"); return -EINVAL; } if (!remote.port) { remote.port = is_coaps ? COAPS_PORT : COAP_PORT; } if (is_coaps) { /* tinydtls wants the interface to match */ if (!remote.netif && ipv6_addr_is_link_local((ipv6_addr_t *)remote.addr.ipv6)) { netif_t *iface = netif_iter(NULL); if (iface == NULL) { return -ENODEV; } remote.netif = netif_get_id(iface); } sock_udp_ep_t local = SOCK_IPV6_EP_ANY; return nanocoap_sock_dtls_connect(sock, &local, &remote, CONFIG_NANOCOAP_SOCK_DTLS_TAG); } else { return nanocoap_sock_connect(sock, NULL, &remote); } } int nanocoap_get_blockwise_url(const char *url, coap_blksize_t blksize, coap_blockwise_cb_t callback, void *arg) { nanocoap_sock_t sock; int res = nanocoap_sock_url_connect(url, &sock); if (res) { return res; } res = nanocoap_sock_get_blockwise(&sock, sock_urlpath(url), blksize, callback, arg); nanocoap_sock_close(&sock); return res; } typedef struct { uint8_t *ptr; size_t len; } _buf_t; static int _2buf(void *arg, size_t offset, uint8_t *buf, size_t len, int more) { _buf_t *dst = arg; if (offset + len > dst->len) { return -ENOBUFS; } memcpy(dst->ptr + offset, buf, len); if (!more) { dst->len = offset + len; } return 0; } ssize_t nanocoap_get_blockwise_url_to_buf(const char *url, coap_blksize_t blksize, void *buf, size_t len) { _buf_t _buf = { .ptr = buf, .len = len }; int res = nanocoap_get_blockwise_url(url, blksize, _2buf, &_buf); return (res < 0) ? (ssize_t)res : (ssize_t)_buf.len; } int nanocoap_server(sock_udp_ep_t *local, uint8_t *buf, size_t bufsize) { nanocoap_sock_t sock; sock_udp_ep_t remote; coap_request_ctx_t ctx = { .remote = &remote, }; if (!local->port) { local->port = COAP_PORT; } ssize_t res = sock_udp_create(&sock.udp, local, NULL, 0); if (res != 0) { return -1; } while (1) { res = sock_udp_recv(&sock.udp, buf, bufsize, -1, &remote); if (res < 0) { DEBUG("error receiving UDP packet %d\n", (int)res); } else if (res > 0) { coap_pkt_t pkt; if (coap_parse(&pkt, (uint8_t *)buf, res) < 0) { DEBUG("error parsing packet\n"); continue; } if ((res = coap_handle_req(&pkt, buf, bufsize, &ctx)) > 0) { sock_udp_send(&sock.udp, buf, res, &remote); } else { DEBUG("error handling request %d\n", (int)res); } } } return 0; }