| /** @file |
| * @brief HTTP client API |
| * |
| * An API for applications to send HTTP requests |
| */ |
| |
| /* |
| * Copyright (c) 2019 Intel Corporation |
| * |
| * SPDX-License-Identifier: Apache-2.0 |
| */ |
| |
| #include <logging/log.h> |
| LOG_MODULE_REGISTER(net_http, CONFIG_NET_HTTP_LOG_LEVEL); |
| |
| #include <kernel.h> |
| #include <string.h> |
| #include <strings.h> |
| #include <errno.h> |
| #include <stdbool.h> |
| #include <stdlib.h> |
| |
| #include <net/net_ip.h> |
| #include <net/socket.h> |
| #include <net/http_client.h> |
| |
| #include "net_private.h" |
| |
| #define HTTP_CONTENT_LEN_SIZE 6 |
| #define MAX_SEND_BUF_LEN 192 |
| |
| static ssize_t sendall(int sock, const void *buf, size_t len) |
| { |
| while (len) { |
| ssize_t out_len = zsock_send(sock, buf, len, 0); |
| |
| if (out_len < 0) { |
| return -errno; |
| } |
| |
| buf = (const char *)buf + out_len; |
| len -= out_len; |
| } |
| |
| return 0; |
| } |
| |
| static int http_send_data(int sock, char *send_buf, |
| size_t send_buf_max_len, size_t *send_buf_pos, |
| ...) |
| { |
| const char *data; |
| va_list va; |
| int ret, end_of_send = *send_buf_pos; |
| int end_of_data, remaining_len; |
| |
| va_start(va, send_buf_pos); |
| |
| data = va_arg(va, const char *); |
| |
| while (data) { |
| end_of_data = 0; |
| |
| do { |
| int to_be_copied; |
| |
| remaining_len = strlen(data + end_of_data); |
| to_be_copied = send_buf_max_len - end_of_send; |
| |
| if (remaining_len > to_be_copied) { |
| strncpy(send_buf + end_of_send, |
| data + end_of_data, |
| to_be_copied); |
| |
| end_of_send += to_be_copied; |
| end_of_data += to_be_copied; |
| remaining_len -= to_be_copied; |
| |
| LOG_HEXDUMP_DBG(send_buf, end_of_send, |
| "Data to send"); |
| |
| ret = sendall(sock, send_buf, end_of_send); |
| if (ret < 0) { |
| NET_DBG("Cannot send %d bytes (%d)", |
| end_of_send, ret); |
| goto err; |
| } |
| |
| end_of_send = 0; |
| continue; |
| } else { |
| strncpy(send_buf + end_of_send, |
| data + end_of_data, |
| remaining_len); |
| end_of_send += remaining_len; |
| remaining_len = 0; |
| } |
| } while (remaining_len > 0); |
| |
| data = va_arg(va, const char *); |
| } |
| |
| va_end(va); |
| |
| if (end_of_send > (int)send_buf_max_len) { |
| NET_ERR("Sending overflow (%d > %zd)", end_of_send, |
| send_buf_max_len); |
| return -EMSGSIZE; |
| } |
| |
| *send_buf_pos = end_of_send; |
| |
| return end_of_send; |
| |
| err: |
| va_end(va); |
| |
| return ret; |
| } |
| |
| static int http_flush_data(int sock, const char *send_buf, size_t send_buf_len) |
| { |
| LOG_HEXDUMP_DBG(send_buf, send_buf_len, "Data to send"); |
| |
| return sendall(sock, send_buf, send_buf_len); |
| } |
| |
| static void print_header_field(size_t len, const char *str) |
| { |
| if (IS_ENABLED(CONFIG_NET_HTTP_LOG_LEVEL_DBG)) { |
| #define MAX_OUTPUT_LEN 128 |
| char output[MAX_OUTPUT_LEN]; |
| |
| /* The value of len does not count \0 so we need to increase it |
| * by one. |
| */ |
| if ((len + 1) > sizeof(output)) { |
| len = sizeof(output) - 1; |
| } |
| |
| snprintk(output, len + 1, "%s", str); |
| |
| NET_DBG("[%zd] %s", len, log_strdup(output)); |
| } |
| } |
| |
| static int on_url(struct http_parser *parser, const char *at, size_t length) |
| { |
| struct http_request *req = CONTAINER_OF(parser, |
| struct http_request, |
| internal.parser); |
| print_header_field(length, at); |
| |
| if (req->internal.response.http_cb && |
| req->internal.response.http_cb->on_url) { |
| req->internal.response.http_cb->on_url(parser, at, length); |
| } |
| |
| return 0; |
| } |
| |
| static int on_status(struct http_parser *parser, const char *at, size_t length) |
| { |
| struct http_request *req = CONTAINER_OF(parser, |
| struct http_request, |
| internal.parser); |
| uint16_t len; |
| |
| len = MIN(length, sizeof(req->internal.response.http_status) - 1); |
| memcpy(req->internal.response.http_status, at, len); |
| req->internal.response.http_status[len] = 0; |
| req->internal.response.http_status_code = |
| (uint16_t)parser->status_code; |
| |
| NET_DBG("HTTP response status %d %s", parser->status_code, |
| log_strdup(req->internal.response.http_status)); |
| |
| if (req->internal.response.http_cb && |
| req->internal.response.http_cb->on_status) { |
| req->internal.response.http_cb->on_status(parser, at, length); |
| } |
| |
| return 0; |
| } |
| |
| static int on_header_field(struct http_parser *parser, const char *at, |
| size_t length) |
| { |
| struct http_request *req = CONTAINER_OF(parser, |
| struct http_request, |
| internal.parser); |
| const char *content_len = "Content-Length"; |
| uint16_t len; |
| |
| len = strlen(content_len); |
| if (length >= len && strncasecmp(at, content_len, len) == 0) { |
| req->internal.response.cl_present = true; |
| } |
| |
| print_header_field(length, at); |
| |
| if (req->internal.response.http_cb && |
| req->internal.response.http_cb->on_header_field) { |
| req->internal.response.http_cb->on_header_field(parser, at, |
| length); |
| } |
| |
| return 0; |
| } |
| |
| #define MAX_NUM_DIGITS 16 |
| |
| static int on_header_value(struct http_parser *parser, const char *at, |
| size_t length) |
| { |
| struct http_request *req = CONTAINER_OF(parser, |
| struct http_request, |
| internal.parser); |
| char str[MAX_NUM_DIGITS]; |
| |
| if (req->internal.response.cl_present) { |
| if (length <= MAX_NUM_DIGITS - 1) { |
| long int num; |
| |
| memcpy(str, at, length); |
| str[length] = 0; |
| |
| num = strtol(str, NULL, 10); |
| if (num == LONG_MIN || num == LONG_MAX) { |
| return -EINVAL; |
| } |
| |
| req->internal.response.content_length = num; |
| } |
| |
| req->internal.response.cl_present = false; |
| } |
| |
| if (req->internal.response.http_cb && |
| req->internal.response.http_cb->on_header_value) { |
| req->internal.response.http_cb->on_header_value(parser, at, |
| length); |
| } |
| |
| print_header_field(length, at); |
| |
| return 0; |
| } |
| |
| static int on_body(struct http_parser *parser, const char *at, size_t length) |
| { |
| struct http_request *req = CONTAINER_OF(parser, |
| struct http_request, |
| internal.parser); |
| |
| req->internal.response.body_found = 1; |
| req->internal.response.processed += length; |
| |
| NET_DBG("Processed %zd length %zd", req->internal.response.processed, |
| length); |
| |
| if (req->internal.response.http_cb && |
| req->internal.response.http_cb->on_body) { |
| req->internal.response.http_cb->on_body(parser, at, length); |
| } |
| |
| /* Reset the body_start pointer for each fragment. */ |
| if (!req->internal.response.body_start) { |
| req->internal.response.body_start = (uint8_t *)at; |
| } |
| |
| return 0; |
| } |
| |
| static int on_headers_complete(struct http_parser *parser) |
| { |
| struct http_request *req = CONTAINER_OF(parser, |
| struct http_request, |
| internal.parser); |
| |
| if (req->internal.response.http_cb && |
| req->internal.response.http_cb->on_headers_complete) { |
| req->internal.response.http_cb->on_headers_complete(parser); |
| } |
| |
| if (parser->status_code >= 500 && parser->status_code < 600) { |
| NET_DBG("Status %d, skipping body", parser->status_code); |
| return 1; |
| } |
| |
| if ((req->method == HTTP_HEAD || req->method == HTTP_OPTIONS) && |
| req->internal.response.content_length > 0) { |
| NET_DBG("No body expected"); |
| return 1; |
| } |
| |
| NET_DBG("Headers complete"); |
| |
| return 0; |
| } |
| |
| static int on_message_begin(struct http_parser *parser) |
| { |
| struct http_request *req = CONTAINER_OF(parser, |
| struct http_request, |
| internal.parser); |
| |
| if (req->internal.response.http_cb && |
| req->internal.response.http_cb->on_message_begin) { |
| req->internal.response.http_cb->on_message_begin(parser); |
| } |
| |
| NET_DBG("-- HTTP %s response (headers) --", |
| http_method_str(req->method)); |
| |
| return 0; |
| } |
| |
| static int on_message_complete(struct http_parser *parser) |
| { |
| struct http_request *req = CONTAINER_OF(parser, |
| struct http_request, |
| internal.parser); |
| |
| if (req->internal.response.http_cb && |
| req->internal.response.http_cb->on_message_complete) { |
| req->internal.response.http_cb->on_message_complete(parser); |
| } |
| |
| NET_DBG("-- HTTP %s response (complete) --", |
| http_method_str(req->method)); |
| |
| req->internal.response.message_complete = 1; |
| |
| return 0; |
| } |
| |
| static int on_chunk_header(struct http_parser *parser) |
| { |
| struct http_request *req = CONTAINER_OF(parser, |
| struct http_request, |
| internal.parser); |
| |
| if (req->internal.response.http_cb && |
| req->internal.response.http_cb->on_chunk_header) { |
| req->internal.response.http_cb->on_chunk_header(parser); |
| } |
| |
| return 0; |
| } |
| |
| static int on_chunk_complete(struct http_parser *parser) |
| { |
| struct http_request *req = CONTAINER_OF(parser, |
| struct http_request, |
| internal.parser); |
| |
| if (req->internal.response.http_cb && |
| req->internal.response.http_cb->on_chunk_complete) { |
| req->internal.response.http_cb->on_chunk_complete(parser); |
| } |
| |
| return 0; |
| } |
| |
| static void http_client_init_parser(struct http_parser *parser, |
| struct http_parser_settings *settings) |
| { |
| http_parser_init(parser, HTTP_RESPONSE); |
| |
| settings->on_body = on_body; |
| settings->on_chunk_complete = on_chunk_complete; |
| settings->on_chunk_header = on_chunk_header; |
| settings->on_headers_complete = on_headers_complete; |
| settings->on_header_field = on_header_field; |
| settings->on_header_value = on_header_value; |
| settings->on_message_begin = on_message_begin; |
| settings->on_message_complete = on_message_complete; |
| settings->on_status = on_status; |
| settings->on_url = on_url; |
| } |
| |
| static int http_wait_data(int sock, struct http_request *req) |
| { |
| int total_received = 0; |
| size_t offset = 0; |
| int received, ret; |
| |
| do { |
| received = zsock_recv(sock, req->internal.response.recv_buf + offset, |
| req->internal.response.recv_buf_len - offset, |
| 0); |
| if (received == 0) { |
| /* Connection closed */ |
| LOG_DBG("Connection closed"); |
| ret = total_received; |
| |
| if (req->internal.response.cb) { |
| NET_DBG("Calling callback for closed connection"); |
| |
| req->internal.response.cb(&req->internal.response, |
| HTTP_DATA_FINAL, |
| req->internal.user_data); |
| } |
| |
| break; |
| } else if (received < 0) { |
| /* Socket error */ |
| LOG_DBG("Connection error (%d)", errno); |
| ret = -errno; |
| break; |
| } else { |
| req->internal.response.data_len += received; |
| |
| (void)http_parser_execute( |
| &req->internal.parser, |
| &req->internal.parser_settings, |
| req->internal.response.recv_buf + offset, |
| received); |
| } |
| |
| total_received += received; |
| offset += received; |
| |
| if (offset >= req->internal.response.recv_buf_len) { |
| offset = 0; |
| } |
| |
| if (req->internal.response.cb) { |
| bool notify = false; |
| enum http_final_call event; |
| |
| if (req->internal.response.message_complete) { |
| NET_DBG("Calling callback for %zd len data", |
| req->internal.response.data_len); |
| |
| notify = true; |
| event = HTTP_DATA_FINAL; |
| } else if (offset == 0) { |
| NET_DBG("Calling callback for partitioned %zd len data", |
| req->internal.response.data_len); |
| |
| notify = true; |
| event = HTTP_DATA_MORE; |
| } |
| |
| if (notify) { |
| req->internal.response.cb(&req->internal.response, |
| event, |
| req->internal.user_data); |
| |
| /* Re-use the result buffer and start to fill it again */ |
| req->internal.response.data_len = 0; |
| req->internal.response.body_start = NULL; |
| } |
| } |
| |
| if (req->internal.response.message_complete) { |
| ret = total_received; |
| break; |
| } |
| |
| } while (true); |
| |
| return ret; |
| } |
| |
| static void http_timeout(struct k_work *work) |
| { |
| struct k_work_delayable *dwork = k_work_delayable_from_work(work); |
| struct http_client_internal_data *data = |
| CONTAINER_OF(dwork, struct http_client_internal_data, work); |
| |
| (void)zsock_close(data->sock); |
| } |
| |
| int http_client_req(int sock, struct http_request *req, |
| int32_t timeout, void *user_data) |
| { |
| /* Utilize the network usage by sending data in bigger blocks */ |
| char send_buf[MAX_SEND_BUF_LEN]; |
| const size_t send_buf_max_len = sizeof(send_buf); |
| size_t send_buf_pos = 0; |
| int total_sent = 0; |
| int ret, total_recv, i; |
| const char *method; |
| |
| if (sock < 0 || req == NULL || req->response == NULL || |
| req->recv_buf == NULL || req->recv_buf_len == 0) { |
| return -EINVAL; |
| } |
| |
| memset(&req->internal.response, 0, sizeof(req->internal.response)); |
| |
| req->internal.response.http_cb = req->http_cb; |
| req->internal.response.cb = req->response; |
| req->internal.response.recv_buf = req->recv_buf; |
| req->internal.response.recv_buf_len = req->recv_buf_len; |
| req->internal.user_data = user_data; |
| req->internal.sock = sock; |
| req->internal.timeout = SYS_TIMEOUT_MS(timeout); |
| |
| method = http_method_str(req->method); |
| |
| ret = http_send_data(sock, send_buf, send_buf_max_len, &send_buf_pos, |
| method, " ", req->url, " ", req->protocol, |
| HTTP_CRLF, NULL); |
| if (ret < 0) { |
| goto out; |
| } |
| |
| total_sent += ret; |
| |
| if (req->port) { |
| ret = http_send_data(sock, send_buf, send_buf_max_len, |
| &send_buf_pos, "Host", ": ", req->host, |
| ":", req->port, HTTP_CRLF, NULL); |
| |
| if (ret < 0) { |
| goto out; |
| } |
| |
| total_sent += ret; |
| } else { |
| ret = http_send_data(sock, send_buf, send_buf_max_len, |
| &send_buf_pos, "Host", ": ", req->host, |
| HTTP_CRLF, NULL); |
| |
| if (ret < 0) { |
| goto out; |
| } |
| |
| total_sent += ret; |
| } |
| |
| if (req->optional_headers_cb) { |
| ret = http_flush_data(sock, send_buf, send_buf_pos); |
| if (ret < 0) { |
| goto out; |
| } |
| |
| send_buf_pos = 0; |
| total_sent += ret; |
| |
| ret = req->optional_headers_cb(sock, req, user_data); |
| if (ret < 0) { |
| goto out; |
| } |
| |
| total_sent += ret; |
| } else { |
| for (i = 0; req->optional_headers && req->optional_headers[i]; |
| i++) { |
| ret = http_send_data(sock, send_buf, send_buf_max_len, |
| &send_buf_pos, |
| req->optional_headers[i], NULL); |
| if (ret < 0) { |
| goto out; |
| } |
| |
| total_sent += ret; |
| } |
| } |
| |
| for (i = 0; req->header_fields && req->header_fields[i]; i++) { |
| ret = http_send_data(sock, send_buf, send_buf_max_len, |
| &send_buf_pos, req->header_fields[i], |
| NULL); |
| if (ret < 0) { |
| goto out; |
| } |
| |
| total_sent += ret; |
| } |
| |
| if (req->content_type_value) { |
| ret = http_send_data(sock, send_buf, send_buf_max_len, |
| &send_buf_pos, "Content-Type", ": ", |
| req->content_type_value, HTTP_CRLF, NULL); |
| if (ret < 0) { |
| goto out; |
| } |
| |
| total_sent += ret; |
| } |
| |
| if (req->payload || req->payload_cb) { |
| if (req->payload_len) { |
| char content_len_str[HTTP_CONTENT_LEN_SIZE]; |
| |
| ret = snprintk(content_len_str, HTTP_CONTENT_LEN_SIZE, |
| "%zd", req->payload_len); |
| if (ret <= 0 || ret >= HTTP_CONTENT_LEN_SIZE) { |
| ret = -ENOMEM; |
| goto out; |
| } |
| |
| ret = http_send_data(sock, send_buf, send_buf_max_len, |
| &send_buf_pos, "Content-Length", ": ", |
| content_len_str, HTTP_CRLF, |
| HTTP_CRLF, NULL); |
| } else { |
| ret = http_send_data(sock, send_buf, send_buf_max_len, |
| &send_buf_pos, HTTP_CRLF, NULL); |
| } |
| |
| if (ret < 0) { |
| goto out; |
| } |
| |
| total_sent += ret; |
| |
| ret = http_flush_data(sock, send_buf, send_buf_pos); |
| if (ret < 0) { |
| goto out; |
| } |
| |
| send_buf_pos = 0; |
| total_sent += ret; |
| |
| if (req->payload_cb) { |
| ret = req->payload_cb(sock, req, user_data); |
| if (ret < 0) { |
| goto out; |
| } |
| |
| total_sent += ret; |
| } else { |
| uint32_t length; |
| |
| if (req->payload_len == 0) { |
| length = strlen(req->payload); |
| } else { |
| length = req->payload_len; |
| } |
| |
| ret = sendall(sock, req->payload, length); |
| if (ret < 0) { |
| goto out; |
| } |
| |
| total_sent += length; |
| } |
| } else { |
| ret = http_send_data(sock, send_buf, send_buf_max_len, |
| &send_buf_pos, HTTP_CRLF, NULL); |
| if (ret < 0) { |
| goto out; |
| } |
| } |
| |
| if (send_buf_pos > 0) { |
| ret = http_flush_data(sock, send_buf, send_buf_pos); |
| if (ret < 0) { |
| goto out; |
| } |
| |
| total_sent += ret; |
| } |
| |
| NET_DBG("Sent %d bytes", total_sent); |
| |
| http_client_init_parser(&req->internal.parser, |
| &req->internal.parser_settings); |
| |
| if (!K_TIMEOUT_EQ(req->internal.timeout, K_FOREVER) && |
| !K_TIMEOUT_EQ(req->internal.timeout, K_NO_WAIT)) { |
| k_work_init_delayable(&req->internal.work, http_timeout); |
| (void)k_work_reschedule(&req->internal.work, |
| req->internal.timeout); |
| } |
| |
| /* Request is sent, now wait data to be received */ |
| total_recv = http_wait_data(sock, req); |
| if (total_recv < 0) { |
| NET_DBG("Wait data failure (%d)", total_recv); |
| } else { |
| NET_DBG("Received %d bytes", total_recv); |
| } |
| |
| if (!K_TIMEOUT_EQ(req->internal.timeout, K_FOREVER) && |
| !K_TIMEOUT_EQ(req->internal.timeout, K_NO_WAIT)) { |
| (void)k_work_cancel_delayable(&req->internal.work); |
| } |
| |
| return total_sent; |
| |
| out: |
| return ret; |
| } |