blob: ce20cd3bd8552f5fee441536efc3bf4d2665f3c7 [file] [log] [blame]
/*
* Copyright (c) 2023 Nordic Semiconductor ASA
*
* SPDX-License-Identifier: Apache-2.0
*/
#include <string.h>
#include <zephyr/logging/log.h>
LOG_MODULE_DECLARE(net_coap, CONFIG_COAP_LOG_LEVEL);
#include <zephyr/net/socket.h>
#include <zephyr/net/coap.h>
#include <zephyr/net/coap_client.h>
#define COAP_VERSION 1
#define COAP_PATH_ELEM_DELIM '/'
#define COAP_PATH_ELEM_QUERY '?'
#define COAP_PATH_ELEM_AMP '&'
#define COAP_SEPARATE_TIMEOUT 6000
#define DEFAULT_RETRY_AMOUNT 5
#define BLOCK1_OPTION_SIZE 4
#define PAYLOAD_MARKER_SIZE 1
static int coap_client_schedule_poll(struct coap_client *client, int sock,
struct coap_client_request *req)
{
client->fd = sock;
client->coap_request = req;
k_sem_give(&client->coap_client_recv_sem);
atomic_set(&client->coap_client_recv_active, 1);
return 0;
}
static int send_request(int sock, const void *buf, size_t len, int flags,
const struct sockaddr *dest_addr, socklen_t addrlen)
{
if (addrlen == 0) {
return sendto(sock, buf, len, flags, NULL, 0);
} else {
return sendto(sock, buf, len, flags, dest_addr, addrlen);
}
}
static int receive(int sock, void *buf, size_t max_len, int flags,
struct sockaddr *src_addr, socklen_t *addrlen)
{
if (*addrlen == 0) {
return recvfrom(sock, buf, max_len, flags, NULL, NULL);
} else {
return recvfrom(sock, buf, max_len, flags, src_addr, addrlen);
}
}
static void reset_block_contexts(struct coap_client *client)
{
client->recv_blk_ctx.block_size = 0;
client->recv_blk_ctx.total_size = 0;
client->recv_blk_ctx.current = 0;
client->send_blk_ctx.block_size = 0;
client->send_blk_ctx.total_size = 0;
client->send_blk_ctx.current = 0;
}
static int coap_client_init_path_options(struct coap_packet *pckt, const char *path)
{
int ret = 0;
int path_start, path_end;
int path_length;
bool contains_query = false;
int i;
path_start = 0;
path_end = 0;
path_length = strlen(path);
for (i = 0; i < path_length; i++) {
path_end = i;
if (path[i] == COAP_PATH_ELEM_DELIM) {
/* Guard for preceding delimiters */
if (path_start < path_end) {
ret = coap_packet_append_option(pckt, COAP_OPTION_URI_PATH,
path + path_start,
path_end - path_start);
if (ret < 0) {
LOG_ERR("Failed to append path to CoAP message");
goto out;
}
}
/* Check if there is a new path after delimiter,
* if not, point to the end of string to not add
* new option after this
*/
if (path_length > i + 1) {
path_start = i + 1;
} else {
path_start = path_length;
}
} else if (path[i] == COAP_PATH_ELEM_QUERY) {
/* Guard for preceding delimiters */
if (path_start < path_end) {
ret = coap_packet_append_option(pckt, COAP_OPTION_URI_PATH,
path + path_start,
path_end - path_start);
if (ret < 0) {
LOG_ERR("Failed to append path to CoAP message");
goto out;
}
}
/* Rest of the path is query */
contains_query = true;
if (path_length > i + 1) {
path_start = i + 1;
} else {
path_start = path_length;
}
break;
}
}
if (contains_query) {
for (i = path_start; i < path_length; i++) {
path_end = i;
if (path[i] == COAP_PATH_ELEM_AMP || path[i] == COAP_PATH_ELEM_QUERY) {
/* Guard for preceding delimiters */
if (path_start < path_end) {
ret = coap_packet_append_option(pckt, COAP_OPTION_URI_QUERY,
path + path_start,
path_end - path_start);
if (ret < 0) {
LOG_ERR("Failed to append path to CoAP message");
goto out;
}
}
/* Check if there is a new query option after delimiter,
* if not, point to the end of string to not add
* new option after this
*/
if (path_length > i + 1) {
path_start = i + 1;
} else {
path_start = path_length;
}
}
}
}
if (path_start < path_end) {
if (contains_query) {
ret = coap_packet_append_option(pckt, COAP_OPTION_URI_QUERY,
path + path_start,
path_end - path_start + 1);
} else {
ret = coap_packet_append_option(pckt, COAP_OPTION_URI_PATH,
path + path_start,
path_end - path_start + 1);
}
if (ret < 0) {
LOG_ERR("Failed to append path to CoAP message");
goto out;
}
}
out:
return ret;
}
static enum coap_block_size coap_client_default_block_size(void)
{
switch (CONFIG_COAP_CLIENT_BLOCK_SIZE) {
case 16:
return COAP_BLOCK_16;
case 32:
return COAP_BLOCK_32;
case 64:
return COAP_BLOCK_64;
case 128:
return COAP_BLOCK_128;
case 256:
return COAP_BLOCK_256;
case 512:
return COAP_BLOCK_512;
case 1024:
return COAP_BLOCK_1024;
}
return COAP_BLOCK_256;
}
static int coap_client_init_request(struct coap_client *client,
struct coap_client_request *req)
{
int ret = 0;
int i;
memset(client->send_buf, 0, sizeof(client->send_buf));
ret = coap_packet_init(&client->request, client->send_buf, MAX_COAP_MSG_LEN, 1,
req->confirmable ? COAP_TYPE_CON : COAP_TYPE_NON_CON,
COAP_TOKEN_MAX_LEN, coap_next_token(), req->method,
coap_next_id());
if (ret < 0) {
LOG_ERR("Failed to init CoAP message %d", ret);
goto out;
}
ret = coap_client_init_path_options(&client->request, req->path);
if (ret < 0) {
LOG_ERR("Failed to parse path to options %d", ret);
goto out;
}
ret = coap_append_option_int(&client->request, COAP_OPTION_CONTENT_FORMAT, req->fmt);
if (ret < 0) {
LOG_ERR("Failed to append content format option");
goto out;
}
/* Blockwise receive ongoing, request next block. */
if (client->recv_blk_ctx.current > 0) {
ret = coap_append_block2_option(&client->request, &client->recv_blk_ctx);
if (ret < 0) {
LOG_ERR("Failed to append block 2 option");
goto out;
}
}
/* Add extra options if any */
for (i = 0; i < req->num_options; i++) {
ret = coap_packet_append_option(&client->request, req->options[i].code,
req->options[i].value, req->options[i].len);
if (ret < 0) {
LOG_ERR("Failed to append %d option", req->options[i].code);
goto out;
}
}
if (req->payload) {
uint16_t payload_len;
uint16_t offset;
/* Blockwise send ongoing, add block1 */
if (client->send_blk_ctx.total_size > 0 ||
(req->len > CONFIG_COAP_CLIENT_MESSAGE_SIZE)) {
if (client->send_blk_ctx.total_size == 0) {
coap_block_transfer_init(&client->send_blk_ctx,
coap_client_default_block_size(),
req->len);
}
ret = coap_append_block1_option(&client->request, &client->send_blk_ctx);
if (ret < 0) {
LOG_ERR("Failed to append block1 option");
goto out;
}
}
ret = coap_packet_append_payload_marker(&client->request);
if (ret < 0) {
LOG_ERR("Failed to append payload marker to CoAP message");
goto out;
}
if (client->send_blk_ctx.total_size > 0) {
uint16_t block_in_bytes =
coap_block_size_to_bytes(client->send_blk_ctx.block_size);
payload_len = client->send_blk_ctx.total_size -
client->send_blk_ctx.current;
if (payload_len > block_in_bytes) {
payload_len = block_in_bytes;
}
offset = client->send_blk_ctx.current;
} else {
payload_len = req->len;
offset = 0;
}
ret = coap_packet_append_payload(&client->request, req->payload + offset,
payload_len);
if (ret < 0) {
LOG_ERR("Failed to append payload to CoAP message");
goto out;
}
if (client->send_blk_ctx.total_size > 0) {
coap_next_block(&client->request, &client->send_blk_ctx);
}
}
client->request_tkl = coap_header_get_token(&client->request, client->request_token);
out:
return ret;
}
int coap_client_req(struct coap_client *client, int sock, const struct sockaddr *addr,
struct coap_client_request *req, int retries)
{
int ret;
if (client->coap_client_recv_active) {
return -EAGAIN;
}
if (sock < 0 || req == NULL || req->path == NULL) {
return -EINVAL;
}
if (addr != NULL) {
memcpy(&client->address, addr, sizeof(*addr));
client->socklen = sizeof(client->address);
} else {
memset(&client->address, 0, sizeof(client->address));
client->socklen = 0;
}
if (retries == -1) {
client->retry_count = DEFAULT_RETRY_AMOUNT;
} else {
client->retry_count = retries;
}
ret = coap_client_init_request(client, req);
if (ret < 0) {
LOG_ERR("Failed to initialize coap request");
return ret;
}
ret = coap_client_schedule_poll(client, sock, req);
if (ret < 0) {
LOG_ERR("Failed to schedule polling");
goto out;
}
ret = coap_pending_init(&client->pending, &client->request, &client->address,
client->retry_count);
if (ret < 0) {
LOG_ERR("Failed to initialize pending struct");
goto out;
}
coap_pending_cycle(&client->pending);
ret = send_request(sock, client->request.data, client->request.offset, 0, &client->address,
client->socklen);
if (ret < 0) {
LOG_ERR("Transmission failed: %d", errno);
} else {
/* Do not return the number of bytes sent */
ret = 0;
}
out:
return ret;
}
static int handle_poll(struct coap_client *client)
{
int ret = 0;
while (1) {
struct pollfd fds;
fds.fd = client->fd;
fds.events = POLLIN;
fds.revents = 0;
/* rfc7252#section-5.2.2, use separate timeout value for a separate response */
if (client->pending.timeout != 0) {
ret = poll(&fds, 1, client->pending.timeout);
} else {
ret = poll(&fds, 1, COAP_SEPARATE_TIMEOUT);
}
if (ret < 0) {
LOG_ERR("Error in poll:%d", errno);
errno = 0;
return ret;
} else if (ret == 0) {
if (client->pending.timeout != 0 && coap_pending_cycle(&client->pending)) {
LOG_ERR("Timeout in poll, retrying send");
send_request(client->fd, client->request.data,
client->request.offset, 0, &client->address,
client->socklen);
} else {
/* No more retries left, don't retry */
LOG_ERR("Timeout in poll, no more retries");
ret = -EFAULT;
break;
}
} else {
if (fds.revents & POLLERR) {
LOG_ERR("Error in poll");
ret = -EIO;
break;
}
if (fds.revents & POLLHUP) {
LOG_ERR("Error in poll: POLLHUP");
ret = -ECONNRESET;
break;
}
if (fds.revents & POLLNVAL) {
LOG_ERR("Error in poll: POLLNVAL - fd not open");
ret = -EINVAL;
break;
}
if (!(fds.revents & POLLIN)) {
LOG_ERR("Unknown poll error");
ret = -EINVAL;
break;
}
ret = 0;
break;
}
}
return ret;
}
static bool token_compare(struct coap_client *client, const struct coap_packet *resp)
{
uint8_t response_token[COAP_TOKEN_MAX_LEN];
uint8_t response_tkl;
response_tkl = coap_header_get_token(resp, response_token);
if (client->request_tkl != response_tkl) {
return false;
}
return memcmp(&client->request_token, &response_token, response_tkl) == 0;
}
static int recv_response(struct coap_client *client, struct coap_packet *response)
{
int len;
int ret;
memset(client->recv_buf, 0, sizeof(client->recv_buf));
len = receive(client->fd, client->recv_buf, sizeof(client->recv_buf), MSG_DONTWAIT,
&client->address, &client->socklen);
if (len < 0) {
LOG_ERR("Error reading response: %d", errno);
return -EINVAL;
} else if (len == 0) {
LOG_ERR("Zero length recv");
return -EINVAL;
}
LOG_DBG("Received %d bytes", len);
ret = coap_packet_parse(response, client->recv_buf, len, NULL, 0);
if (ret < 0) {
LOG_ERR("Invalid data received");
return ret;
}
return ret;
}
static void report_callback_error(struct coap_client *client, int error_code)
{
if (client->coap_request->cb) {
client->coap_request->cb(error_code, 0, NULL, 0, true,
client->coap_request->user_data);
}
}
static int send_ack(struct coap_client *client, const struct coap_packet *req,
uint8_t response_code)
{
int ret;
ret = coap_ack_init(&client->request, req, client->send_buf, MAX_COAP_MSG_LEN,
response_code);
if (ret < 0) {
LOG_ERR("Failed to initialize CoAP ACK-message");
return ret;
}
ret = send_request(client->fd, client->request.data, client->request.offset, 0,
&client->address, client->socklen);
if (ret < 0) {
LOG_ERR("Error sending a CoAP ACK-message");
return ret;
}
return 0;
}
static int send_reset(struct coap_client *client, const struct coap_packet *req,
uint8_t response_code)
{
int ret;
uint16_t id;
uint8_t token[COAP_TOKEN_MAX_LEN];
uint8_t tkl;
id = coap_header_get_id(req);
tkl = response_code ? coap_header_get_token(req, token) : 0;
ret = coap_packet_init(&client->request, client->send_buf, MAX_COAP_MSG_LEN, COAP_VERSION,
COAP_TYPE_RESET, tkl, token, response_code, id);
if (ret < 0) {
LOG_ERR("Error creating CoAP reset message");
return ret;
}
ret = send_request(client->fd, client->request.data, client->request.offset, 0,
&client->address, client->socklen);
if (ret < 0) {
LOG_ERR("Error sending CoAP reset message");
return ret;
}
return 0;
}
static int handle_response(struct coap_client *client, const struct coap_packet *response)
{
int ret = 0;
int response_type;
int block_option;
int block_num;
bool blockwise_transfer = false;
bool last_block = false;
/* Handle different types, ACK might be separate or piggybacked
* CON and NCON contains a separate response, CON needs an empty response
* CON request results as ACK and possibly separate CON or NCON response
* NCON request results only as a separate CON or NCON message as there is no ACK
* With RESET, just drop gloves and call the callback.
*/
response_type = coap_header_get_type(response);
/* Reset and Ack need to match the message ID with request */
if ((response_type == COAP_TYPE_ACK || response_type == COAP_TYPE_RESET) &&
coap_header_get_id(response) != client->pending.id) {
LOG_ERR("Unexpected ACK or Reset");
return -EFAULT;
} else if (response_type == COAP_TYPE_RESET) {
coap_pending_clear(&client->pending);
}
/* CON, NON_CON and piggybacked ACK need to match the token with original request */
uint16_t payload_len;
uint8_t response_code = coap_header_get_code(response);
const uint8_t *payload = coap_packet_get_payload(response, &payload_len);
/* Separate response */
if (payload_len == 0 && response_type == COAP_TYPE_ACK &&
response_code == COAP_CODE_EMPTY) {
/* Clear the pending, poll uses now the separate timeout for the response. */
coap_pending_clear(&client->pending);
return 1;
}
/* Check for tokens */
if (!token_compare(client, response)) {
LOG_ERR("Not matching tokens, respond with reset");
ret = send_reset(client, response, COAP_RESPONSE_CODE_NOT_FOUND);
return 1;
}
/* Send ack for CON */
if (response_type == COAP_TYPE_CON) {
/* CON response is always a separate response, respond with empty ACK. */
ret = send_ack(client, response, COAP_CODE_EMPTY);
if (ret < 0) {
goto fail;
}
}
if (client->pending.timeout != 0) {
coap_pending_clear(&client->pending);
}
/* Check if block2 exists */
block_option = coap_get_option_int(response, COAP_OPTION_BLOCK2);
if (block_option > 0) {
blockwise_transfer = true;
last_block = !GET_MORE(block_option);
block_num = GET_BLOCK_NUM(block_option);
if (block_num == 0) {
coap_block_transfer_init(&client->recv_blk_ctx,
coap_client_default_block_size(),
0);
client->offset = 0;
}
ret = coap_update_from_block(response, &client->recv_blk_ctx);
if (ret < 0) {
LOG_ERR("Error updating block context");
}
coap_next_block(response, &client->recv_blk_ctx);
} else {
client->offset = 0;
last_block = true;
}
/* Check if this was a response to last blockwise send */
if (client->send_blk_ctx.total_size > 0) {
blockwise_transfer = true;
if (client->send_blk_ctx.total_size == client->send_blk_ctx.current) {
last_block = true;
} else {
last_block = false;
}
}
/* Call user callback */
if (client->coap_request->cb) {
client->coap_request->cb(response_code, client->offset, payload, payload_len,
last_block, client->coap_request->user_data);
/* Update the offset for next callback in a blockwise transfer */
if (blockwise_transfer) {
client->offset += payload_len;
}
}
/* If this wasn't last block, send the next request */
if (blockwise_transfer && !last_block) {
ret = coap_client_init_request(client, client->coap_request);
if (ret < 0) {
LOG_ERR("Error creating a CoAP request");
goto fail;
}
if (client->pending.timeout != 0) {
LOG_ERR("Previous pending hasn't arrived");
goto fail;
}
ret = coap_pending_init(&client->pending, &client->request, &client->address,
client->retry_count);
if (ret < 0) {
LOG_ERR("Error creating pending");
goto fail;
}
coap_pending_cycle(&client->pending);
ret = send_request(client->fd, client->request.data, client->request.offset, 0,
&client->address, client->socklen);
if (ret < 0) {
LOG_ERR("Error sending a CoAP request");
goto fail;
} else {
return 1;
}
}
fail:
return ret;
}
void coap_client_recv(void *coap_cl, void *a, void *b)
{
int ret;
struct coap_client *const client = coap_cl;
reset_block_contexts(client);
k_sem_take(&client->coap_client_recv_sem, K_FOREVER);
while (true) {
struct coap_packet response;
atomic_set(&client->coap_client_recv_active, 1);
ret = handle_poll(client);
if (ret < 0) {
/* Error in polling, clear pending. */
LOG_ERR("Error in poll");
coap_pending_clear(&client->pending);
report_callback_error(client, ret);
goto idle;
}
ret = recv_response(client, &response);
if (ret < 0) {
LOG_ERR("Error receiving response");
report_callback_error(client, ret);
goto idle;
}
ret = handle_response(client, &response);
if (ret < 0) {
LOG_ERR("Error handling respnse");
report_callback_error(client, ret);
goto idle;
}
/* There are more messages coming for the original request */
if (ret > 0) {
continue;
} else {
idle:
reset_block_contexts(client);
atomic_set(&client->coap_client_recv_active, 0);
k_sem_take(&client->coap_client_recv_sem, K_FOREVER);
}
}
}
int coap_client_init(struct coap_client *client, const char *info)
{
if (client == NULL) {
return -EINVAL;
}
client->fd = -1;
k_sem_init(&client->coap_client_recv_sem, 0, 1);
client->tid =
k_thread_create(&client->thread, client->coap_thread_stack,
K_THREAD_STACK_SIZEOF(client->coap_thread_stack),
coap_client_recv, client, NULL, NULL,
CONFIG_COAP_CLIENT_THREAD_PRIORITY, 0, K_NO_WAIT);
if (IS_ENABLED(CONFIG_THREAD_NAME)) {
if (info != NULL) {
k_thread_name_set(client->tid, info);
} else {
k_thread_name_set(client->tid, "coap_client");
}
}
return 0;
}