From b532f578462b7dd89fb332e741613e49651e0a26 Mon Sep 17 00:00:00 2001 From: Frank Lee Date: Wed, 24 Apr 2019 14:14:04 +0800 Subject: [PATCH] net: http: implement high level API of http client - support GET and POST request - based BSD socket Signed-off-by: Frank Lee --- include/net/http_client.h | 290 +++++++++++++++ subsys/net/lib/http/CMakeLists.txt | 4 + subsys/net/lib/http/Kconfig | 20 + subsys/net/lib/http/README_http_client | 32 ++ subsys/net/lib/http/http_client.c | 487 +++++++++++++++++++++++++ 5 files changed, 833 insertions(+) create mode 100644 include/net/http_client.h create mode 100644 subsys/net/lib/http/README_http_client create mode 100644 subsys/net/lib/http/http_client.c diff --git a/include/net/http_client.h b/include/net/http_client.h new file mode 100644 index 0000000000000..928fb7cd23a40 --- /dev/null +++ b/include/net/http_client.h @@ -0,0 +1,290 @@ +/** @file + * @brief HTTP client high-level api implementation for Zephyr. + */ + +/* + * Copyright (c) 2019 Unisoc Technologies INC. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#ifndef ZEPHYR_INCLUDE_HTTP_CLIENT_H_ +#define ZEPHYR_INCLUDE_HTTP_CLIENT_H_ + +#include + +#ifdef CONFIG_HTTPS +#define CONFIG_NET_SOCKETS_SOCKOPT_TLS +#endif + +#if !defined(ZEPHYR_USER_AGENT) +#define ZEPHYR_USER_AGENT "Zephyr" +#endif + +#if !defined(HTTP_PROTOCOL) +#define HTTP_PROTOCOL "HTTP/1.1" +#endif + +#define HTTP_CRLF "\r\n" +#define HTTP_STATUS_OK "OK" +#define HTTP_PARTIAL_CONTENT "Partial Content" + +#define HOST_NAME_MAX_SIZE 32 +#define HTTP_REQUEST_MAX_SIZE 1024 +#define HTTP_RESPONSE_MAX_SIZE 4096 +#define HTTP_STATUS_STR_SIZE 16 + +#if defined(CONFIG_NET_SOCKETS_SOCKOPT_TLS) +#define HTTP_PORT "443" +#else +#define HTTP_PORT "80" +#endif + +struct http_ctx; + +/* HTTP header fields struct */ +struct http_field_value { + /** Field name, this variable will point to the beginning of the string + * containing the HTTP field name + */ + const char *key; + + /** Value, this variable will point to the beginning of the string + * containing the field value + */ + const char *value; + + /** Length of the field name */ + u16_t key_len; + + /** Length of the field value */ + u16_t value_len; +}; + +/* Is there more data to come */ +enum http_final_call { + HTTP_DATA_MORE = 0, + HTTP_DATA_FINAL = 1, +}; + +/* http header filed and value */ +enum http_header_field_val { + HTTP_HEADER_FIELD = 0, + HTTP_HEADER_VALUE = 1, +}; + +/** + * @typedef http_response_cb_t + * @brief Callback used when a response has been received from peer. + * + * @param ctx HTTP context. + * @param body data Received body point + * @param body_len received body length + * @param final_data Does this data buffer contain all the data or + * is there still more data to come. + */ +typedef void (*http_response_cb_t)(struct http_ctx *ctx, + const char *body, + int body_len, + enum http_final_call final_data); + +/** + * @typedef http_get_filed_value_cb_t + * @brief Callback used when got http header filed and value. + * + * @param ctx HTTP context. + * @param at data of header + * @param at_len header field or value length + * @param f_v http_header_field_val indicate it's filed or value + */ +typedef void (*http_get_filed_value_cb_t)(struct http_ctx *ctx, + const char *at, + int at_len, + enum http_header_field_val f_v); + +/** + * HTTP client request. This contains all the data that is needed when doing + * a HTTP request. + */ +struct http_request { + /** The HTTP method: GET, HEAD, OPTIONS, POST, ... */ + const char *method_str; + + /** The URL for this request, for example: /index.html */ + const char *path; + + /** The HTTP header for user added (like Range/Connect-Type...)*/ + const char *header_fields; + + /** The size of HTTP header fields */ + const u16_t header_fields_size; + + /** http header keep-alive*/ + const bool keep_alive; + +}; + +struct http_resp { + /** HTTP Content-Length field value */ + size_t content_length; + + /** Content length parsed. This should be the same as + * the content_length field if parsing was ok. + */ + size_t processed; + + /* https://tools.ietf.org/html/rfc7230#section-3.1.2 + * The status-code element is a 3-digit integer code + * + * The reason-phrase element exists for the sole + * purpose of providing a textual description + * associated with the numeric status code. A client + * SHOULD ignore the reason-phrase content. + */ + char http_status[HTTP_STATUS_STR_SIZE]; + + u8_t cl_present; + u8_t body_found; + u8_t message_complete; +}; + +/** + * Http context information. This contains all the data that is + * needed when working with http API. + */ +struct http_ctx { + /** Server socket addr */ + struct addrinfo *addr_info; + char host[HOST_NAME_MAX_SIZE]; + + /** HTTP request information */ + struct http_request req; + char *req_buf; + + /** HTTP response information */ + struct http_resp rsp; + char *rsp_buf; + + /** HTTP parser for parsing the initial request */ + struct http_parser parser; + + /** HTTP parser settings */ + struct http_parser_settings parser_settings; + + /** socket id with http conenct*/ + u16_t sock_id; + + /** User provided HTTP response callback which is + * called when a response is received to a sent HTTP + * request. + */ + http_response_cb_t rsp_cb; + + /** User provided to parse field-value what user care about + * it, such as Content-Range to get the resource file size + */ + http_get_filed_value_cb_t fv_cb; +}; + +/** + * @brief Initialize user-supplied HTTP context. + * + * @param ctx HTTP context. + * @param host HTTP request host name. + * @param port http server port number. + * @param tls http connect is https or not + * + * @return Return 0 if ok, and <0 if error. + */ +int http_client_init(struct http_ctx *ctx, + char *host, + char *port, + bool tls); + +/** + *@brief Generic function to get a HTTP request to the network. + * + * @param ctx HTTP context. + * @param path http request path, like /index.html. + * @param keep_alive http connect is alive or not + * @param user_data User data related to this request. This is passed + * to send callback if user has specified that. + * + * @return Return 0 if ok, and <0 if error. + */ +int http_client_get(struct http_ctx *ctx, + char *path, + bool keep_alive, + void *user_data); + +/** + * @brief Generic function to post a HTTP request to the network. + * + * @param ctx HTTP context. + * @param path http request path, like /index.html. + * @param keep_alive http connect is alive or not + * @param user_data User data related to this request. This is passed + * to send callback if user has specified that. + * + * @return Return 0 if ok, and <0 if error. + */ +int http_client_post(struct http_ctx *ctx, + char *path, + bool keep_alive, + void *user_data); + +/** + * @brief Close a network connection to peer. + * + * @param ctx Http context. + * + * @return 0 if ok, <0 if error. + */ +int http_client_close(struct http_ctx *ctx); + +/** + * @brief Add HTTP header field to the message. + * + * @details This can be called multiple times to add pieces of HTTP header + * fields into the message. Caller must put the "\\r\\n" characters to the + * input header_fields variable. + * + * @param ctx Http context. + * @param header_fields All or part of HTTP header to be added. + * + * @return void + */ +void http_add_header_field(struct http_ctx *ctx, + char *header_fields); + +/** + * @brief set http response callback. + * + * @details user can call this function to set http response callback, + * then it will got the response body data. + * + * @param ctx Http context. + * @param resp_cb function of response callback. + * + * @return void + */ +void http_set_resp_callback(struct http_ctx *ctx, + http_response_cb_t resp_cb); + +/** + * @brief set parse http response header field and value. + * + * @details user can call this function to set parse filed and value of + * response header, such as Content-Range to get resource size, + * and ETag to get the resource brief of resource... + * + * @param ctx Http context. + * @param fv_cb function of parse filed and value + * callback. + * + * @return void + */ +void http_set_fv_callback(struct http_ctx *ctx, + http_get_filed_value_cb_t fv_cb); + +#endif /* ZEPHYR_INCLUDE_HTTP_CLIENT_H_ */ diff --git a/subsys/net/lib/http/CMakeLists.txt b/subsys/net/lib/http/CMakeLists.txt index e178d62f4faf2..aef10bbeb42fa 100644 --- a/subsys/net/lib/http/CMakeLists.txt +++ b/subsys/net/lib/http/CMakeLists.txt @@ -8,3 +8,7 @@ endif() zephyr_library_sources_if_kconfig(http_parser.c) zephyr_library_sources_if_kconfig(http_parser_url.c) + +zephyr_library_sources_ifdef(CONFIG_HTTP_CLIENT + http_client.c + ) diff --git a/subsys/net/lib/http/Kconfig b/subsys/net/lib/http/Kconfig index 10d3d5c9264b0..5867fd32a4336 100644 --- a/subsys/net/lib/http/Kconfig +++ b/subsys/net/lib/http/Kconfig @@ -1,4 +1,5 @@ # Copyright (c) 2016 Intel Corporation +# Copyright (c) 2019 Unisoc Technologies INC. # # SPDX-License-Identifier: Apache-2.0 # @@ -23,3 +24,22 @@ config HTTP_PARSER_STRICT depends on (HTTP_PARSER || HTTP_PARSER_URL) help This option enables the strict parsing option + +config HTTP_CLIENT + bool "HTTP CLIENT support" + select HTTP_PARSER + help + This option enables the http client module. + +config HTTPS + bool "HTTPS support" + help + Theis option enables the https connection. + +if HTTP_CLIENT + module=HTTP_CLIENT + module-dep=NET_LOG + module-str=Log level for HTTP_CLIENT + module-help=Enables http client debug messages. + source "subsys/net/Kconfig.template.log_config.net" +endif #HTTP_CLIENT diff --git a/subsys/net/lib/http/README_http_client b/subsys/net/lib/http/README_http_client new file mode 100644 index 0000000000000..7219384dffdb3 --- /dev/null +++ b/subsys/net/lib/http/README_http_client @@ -0,0 +1,32 @@ +HTTP Client Support in Zephyr +----------------------------- + +The HTTP client is based BSD socket API, it implement the request method of +GET and POST, and processed with synchronization only. +Besides, the api of http client is simple and easy use. + +1. Usage + <1> At first, call http_client_init function to get host ip and create socket + connection, the http_ctx will save the socket id. + + <2> The sencond, call http_client_get or http_client_post to request. User + can set the input parameter keep_alive when need keep the connection alive. + Otherwise, user can call the http_add_header_field function to set callback + when it need processing special feature, such as add Range/Content-Type + to request header, or process the response body would call the + http_set_resp_callback function. + + if user need repeat to request, then just call http_client_get or + http_client_post again. + + <3> Finally, close the http connection when call http_client_close. + + + 2. trouble shoot + <1> call http_client_init return error. + error code [-101]: it means system intenal error, maybe DHCP can not + successful, user need check local networking was normal or not. + + <2> call http_client_get or http_client_post return error. + error code [-1]: it means connection is cutoff, user need excute + http_client_close to close http connect and reconnect by http_client_init. diff --git a/subsys/net/lib/http/http_client.c b/subsys/net/lib/http/http_client.c new file mode 100644 index 0000000000000..8a17f628b2845 --- /dev/null +++ b/subsys/net/lib/http/http_client.c @@ -0,0 +1,487 @@ +/* + * Copyright (c) 2019 Unisoc Technologies INC. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +LOG_MODULE_REGISTER(net_http_client, CONFIG_HTTP_CLIENT_LOG_LEVEL); + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +#if defined(CONFIG_NET_SOCKETS_SOCKOPT_TLS) +#include +#include "ca_certificate.h" +#endif + +#include "../../ip/net_private.h" +#include + +#define HTTP_HOST "Host" +#define HTTP_CONTENT_TYPE "Content-Type" +#define HTTP_CONTENT_LEN "Content-Length" + +#define MAX_NUM_DIGITS 16 + +static int on_status(struct http_parser *parser, + const char *at, size_t length) +{ + struct http_ctx *ctx = CONTAINER_OF(parser, + struct http_ctx, + parser); + + if (length > (HTTP_STATUS_STR_SIZE - 1)) { + NET_ERR("HTTP response status error, status=%s", at); + return -EINVAL; + } + + memcpy(ctx->rsp.http_status, at, length); + ctx->rsp.http_status[length] = 0; + + NET_DBG("HTTP response status %s", + log_strdup(ctx->rsp.http_status)); + + return 0; +} + +static int on_body(struct http_parser *parser, + const char *at, size_t length) +{ + struct http_ctx *ctx = CONTAINER_OF(parser, + struct http_ctx, + parser); + + ctx->rsp.body_found = 1; + ctx->rsp.processed += length; + + NET_DBG("%s: Processed %zd length %zd", + __func__, ctx->rsp.processed, length); + + if (ctx->rsp_cb) { + ctx->rsp_cb(ctx, at, length, HTTP_DATA_MORE); + } + + return 0; +} + +static int on_header_field(struct http_parser *parser, const char *at, + size_t length) +{ + const char *content_len = HTTP_CONTENT_LEN; + struct http_ctx *ctx = CONTAINER_OF(parser, + struct http_ctx, + parser); + u16_t len; + + len = strlen(content_len); + if (length >= len && memcmp(at, content_len, len) == 0) { + ctx->rsp.cl_present = true; + } + + if (ctx->fv_cb) { + ctx->fv_cb(ctx, at, length, HTTP_HEADER_FIELD); + } + + return 0; +} + + +static int on_header_value(struct http_parser *parser, const char *at, + size_t length) +{ + char str[MAX_NUM_DIGITS]; + struct http_ctx *ctx = CONTAINER_OF(parser, + struct http_ctx, + parser); + + if (ctx->rsp.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; + } + + ctx->rsp.content_length = num; + NET_DBG("http response content_len=%d\n", + ctx->rsp.content_length); + } + + ctx->rsp.cl_present = false; + } + + if (ctx->fv_cb) { + ctx->fv_cb(ctx, at, length, HTTP_HEADER_VALUE); + } + + return 0; +} + +static int on_message_complete(struct http_parser *parser) +{ + struct http_ctx *ctx = CONTAINER_OF(parser, + struct http_ctx, + parser); + + ctx->rsp.message_complete = 1; + + if (ctx->rsp_cb) { + ctx->rsp_cb(ctx, NULL, 0, HTTP_DATA_FINAL); + } + + return 0; +} + +static void dump_addrinfo(const struct addrinfo *ai) +{ + NET_DBG("addrinfo @%p: ai_family=%d, ai_socktype=%d, ai_protocol=%d, " + "sa_family=%d, sin_port=%x\n", + ai, ai->ai_family, ai->ai_socktype, ai->ai_protocol, + ai->ai_addr->sa_family, + ((struct sockaddr_in *)ai->ai_addr)->sin_port); +} + + +static int http_client_request(struct http_ctx *ctx) +{ + int ret; + int size = strlen(ctx->req_buf); + int sent, recved; + int parsered; + + if (ctx->sock_id <= 0) { + NET_ERR("http request, socket error, sock_id=%d", ctx->sock_id); + return ctx->sock_id; + } + + sent = 0; + NET_DBG("http client request start\n"); + while (sent < size) { + ret = send(ctx->sock_id, ctx->req_buf+sent, size-sent, 0); + if (ret < 0) { + NET_ERR("http send error, ret=%d", ret); + return ret; + } + + sent += ret; + } + + /** init the http parse*/ + http_parser_init(&ctx->parser, HTTP_RESPONSE); + memset(&ctx->parser_settings, 0, sizeof(struct http_parser_settings)); + ctx->parser_settings.on_body = on_body; + ctx->parser_settings.on_header_field = on_header_field; + ctx->parser_settings.on_header_value = on_header_value; + ctx->parser_settings.on_message_complete = on_message_complete; + ctx->parser_settings.on_status = on_status; + + ctx->rsp_buf = malloc(HTTP_RESPONSE_MAX_SIZE); + if (ctx->rsp_buf < 0) { + NET_DBG("Cannot malloc http request buf[ctx->rsp_buf]\n"); + return -ENOMEM; + } + + memset(ctx->rsp_buf, 0, HTTP_RESPONSE_MAX_SIZE); + memset(&ctx->rsp, 0, sizeof(struct http_resp)); + + do { + memset(ctx->rsp_buf, 0, HTTP_RESPONSE_MAX_SIZE); + recved = recv(ctx->sock_id, ctx->rsp_buf, + HTTP_RESPONSE_MAX_SIZE-1, 0); + if (recved < 0) { + NET_ERR("http recv error, recved=%d", recved); + ret = recved; + goto end; + } + + NET_DBG("http client recv response, recved=%d\n", recved); + + parsered = http_parser_execute(&ctx->parser, + &ctx->parser_settings, + ctx->rsp_buf, + recved); + + if (!strcmp(ctx->rsp.http_status, HTTP_STATUS_OK) || + !strcmp(ctx->rsp.http_status, HTTP_PARTIAL_CONTENT)) { + if (ctx->rsp.message_complete != 1) { + NET_DBG("http recv response continue\n"); + continue; + } else { + NET_DBG("http recv response complete\n"); + recved = 0; + } + } else { + NET_ERR("http resp err, http_status=%s", + ctx->rsp.http_status); + ret = -1; + goto end; + } + + } while (recved); + +end: + free(ctx->rsp_buf); + + return ret; +} + +int http_client_init(struct http_ctx *ctx, + char *host, + char *port, + bool tls) +{ + static struct addrinfo hints; + struct addrinfo *res, *cur; + int ret; + char m_ipaddr[16]; + struct sockaddr_in *addr; + + if (tls) { +#if defined(CONFIG_NET_SOCKETS_SOCKOPT_TLS) + tls_credential_add(CA_CERTIFICATE_TAG, + TLS_CREDENTIAL_CA_CERTIFICATE, + ca_certificate, sizeof(ca_certificate)); +#else + NET_ERR("Please Check CONFIG_HTTPS was Enable or Not!\n"); +#endif + } + + hints.ai_family = AF_INET; + hints.ai_socktype = SOCK_STREAM; + ret = getaddrinfo(host, port, &hints, &res); + NET_DBG("getaddrinfo status: %d\n", ret); + + dump_addrinfo(res); + if (ret < 0) { + return ret; + } + + for (cur = res; cur != NULL; cur = cur->ai_next) { + addr = (struct sockaddr_in *)cur->ai_addr; + sprintf(m_ipaddr, "%d.%d.%d.%d", + (*addr).sin_addr.s4_addr[0], + (*addr).sin_addr.s4_addr[1], + (*addr).sin_addr.s4_addr[2], + (*addr).sin_addr.s4_addr[3]); + NET_DBG("Get Ip Addr:%s\n", m_ipaddr); + } + +#if defined(CONFIG_NET_SOCKETS_SOCKOPT_TLS) + if (tls) { + ctx->sock_id = socket(res->ai_family, + res->ai_socktype, + IPPROTO_TLS_1_2); + sec_tag_t sec_tag_opt[] = { + CA_CERTIFICATE_TAG, + }; + setsockopt(ctx->sock_id, SOL_TLS, TLS_SEC_TAG_LIST, + sec_tag_opt, sizeof(sec_tag_opt)); + + setsockopt(ctx->sock_id, SOL_TLS, TLS_HOSTNAME, + ctx->host, strlen(ctx->host)); + } else { + ctx->sock_id = socket(res->ai_family, + res->ai_socktype, + res->ai_protocol); + } +#else + { + ctx->sock_id = socket(res->ai_family, + res->ai_socktype, + res->ai_protocol); + } +#endif + + NET_DBG("http client do connect: ctx->sock_id = %d\n", ctx->sock_id); + ret = connect(ctx->sock_id, res->ai_addr, res->ai_addrlen); + NET_DBG("http connect status: %d\n", ret); + if (ret < 0) { + ctx->sock_id = 0; + return ret; + } + + freeaddrinfo(res); + strcpy(ctx->host, host); + return 0; +} + +int http_client_get(struct http_ctx *ctx, + char *path, + bool keep_alive, + void *user_data) +{ + int ret; + char temp[128]; + + if (ctx == NULL) { + return -EINVAL; + } + + ctx->req_buf = malloc(HTTP_REQUEST_MAX_SIZE); + if (ctx->req_buf < 0) { + NET_DBG("Cannot malloc http request buff [ctx->req_buf]"); + return -ENOMEM; + } + memset(ctx->req_buf, 0, HTTP_REQUEST_MAX_SIZE); + memset(temp, 0, sizeof(temp)); + + ctx->req.method_str = http_method_str(HTTP_GET); + if (path != NULL) { + if (user_data != NULL) { + sprintf(temp, "%s?%s", path, (char *)user_data); + } else { + sprintf(temp, "%s", path); + } + } else { + if (user_data != NULL) { + sprintf(temp, "/?%s", (char *)user_data); + } else { + temp[0] = '/'; + } + } + + sprintf(ctx->req_buf, "GET %s HTTP/1.1\r\nHOST: %s\r\n", + temp, ctx->host); + memset(temp, 0, sizeof(temp)); + if (keep_alive) { + sprintf(temp, "Connection: Keep-alive\r\n"); + } else { + sprintf(temp, "Connection: close\r\n"); + } + + strcat(ctx->req_buf, temp); + + /** add header fields*/ + if (ctx->req.header_fields) { + strcat(ctx->req_buf, ctx->req.header_fields); + } + + /**add http end */ + strcat(ctx->req_buf, "\r\n"); + printk("http request size=%d\n", strlen(ctx->req_buf)); + NET_DBG("http client send request:%s\n", ctx->req_buf); + + /**send http request*/ + ret = http_client_request(ctx); + + free(ctx->req_buf); + return ret; +} + +int http_client_post(struct http_ctx *ctx, + char *path, + bool keep_alive, + void *user_data) +{ + int ret; + char temp[128]; + + if (ctx == NULL) { + return -EINVAL; + } + + ctx->req_buf = malloc(HTTP_REQUEST_MAX_SIZE); + if (ctx->req_buf < 0) { + NET_DBG("Cannot malloc http request buff [ctx->req_buf]"); + return -ENOMEM; + } + + memset(ctx->req_buf, 0, HTTP_REQUEST_MAX_SIZE); + memset(temp, 0, sizeof(temp)); + + ctx->req.method_str = http_method_str(HTTP_POST); + if (path != NULL) { + sprintf(temp, "%s", path); + } else { + temp[0] = '/'; + } + + sprintf(ctx->req_buf, "POST %s HTTP/1.1\r\nHOST: %s\r\n", + temp, ctx->host); + memset(temp, 0, sizeof(temp)); + if (keep_alive) { + sprintf(temp, "Connection: Keep-alive\r\n"); + } else { + sprintf(temp, "Connection: close\r\n"); + } + + strcat(ctx->req_buf, temp); + + /** add header fields*/ + if (ctx->req.header_fields) { + strcat(ctx->req_buf, ctx->req.header_fields); + } + + /**add http end */ + strcat(ctx->req_buf, "\r\n"); + + /**add post user data*/ + if (user_data != NULL) { + strcat(ctx->req_buf, (char *)user_data); + } + + NET_DBG("http client send request:%s\n", ctx->req_buf); + /**send http request*/ + ret = http_client_request(ctx); + + free(ctx->req_buf); + return ret; +} + +int http_client_close(struct http_ctx *ctx) +{ + if (ctx == NULL) { + return -EINVAL; + } + + close(ctx->sock_id); + + return 0; +} + +void http_add_header_field(struct http_ctx *ctx, + char *header_fields) +{ + if (ctx == NULL) { + return; + } + + ctx->req.header_fields = header_fields; +} + +void http_set_resp_callback(struct http_ctx *ctx, + http_response_cb_t rsp_cb) +{ + if (ctx == NULL) { + return; + } + + ctx->rsp_cb = rsp_cb; +} + +void http_set_fv_callback(struct http_ctx *ctx, + http_get_filed_value_cb_t fv_cb) +{ + if (ctx == NULL) { + return; + } + + ctx->fv_cb = fv_cb; +}