From aeae442e578c69a0b603df24b7036584547600c5 Mon Sep 17 00:00:00 2001 From: Peter Harper Date: Mon, 22 May 2023 18:42:05 +0100 Subject: [PATCH] Add a http client utility. Implemented using the lwip http client. Fixes #1386 --- src/rp2_common/pico_lwip/CMakeLists.txt | 10 ++ src/rp2_common/pico_lwip/http_client_util.c | 141 ++++++++++++++++++ .../pico_lwip/include/pico/http_client_util.h | 126 ++++++++++++++++ 3 files changed, 277 insertions(+) create mode 100644 src/rp2_common/pico_lwip/http_client_util.c create mode 100644 src/rp2_common/pico_lwip/include/pico/http_client_util.h diff --git a/src/rp2_common/pico_lwip/CMakeLists.txt b/src/rp2_common/pico_lwip/CMakeLists.txt index 59d33c295..9f1594778 100644 --- a/src/rp2_common/pico_lwip/CMakeLists.txt +++ b/src/rp2_common/pico_lwip/CMakeLists.txt @@ -302,5 +302,15 @@ if (EXISTS ${PICO_LWIP_PATH}/${LWIP_TEST_PATH}) pico_lwip_contrib_freertos pico_rand) + pico_add_library(pico_lwip_http_util NOFLAG) + target_sources(pico_lwip_http_util INTERFACE + ${CMAKE_CURRENT_LIST_DIR}/http_client_util.c + ) + pico_mirrored_target_link_libraries(pico_lwip_http_util INTERFACE + pico_lwip_http + pico_lwip_mbedtls + pico_mbedtls + ) + pico_promote_common_scope_vars() endif() diff --git a/src/rp2_common/pico_lwip/http_client_util.c b/src/rp2_common/pico_lwip/http_client_util.c new file mode 100644 index 000000000..939e280ce --- /dev/null +++ b/src/rp2_common/pico_lwip/http_client_util.c @@ -0,0 +1,141 @@ +/** + * Copyright (c) 2023 Raspberry Pi (Trading) Ltd. + * + * SPDX-License-Identifier: BSD-3-Clause + */ + +#include +#include +#include "pico/http_client_util.h" +#include "pico/async_context.h" +#include "lwip/altcp.h" +#include "lwip/altcp_tls.h" + +#ifndef HTTP_INFO +#define HTTP_INFO printf +#endif + +#ifndef HTTP_INFOC +#define HTTP_INFOC putchar +#endif + +#ifndef HTTP_INFOC +#define HTTP_INFOC putchar +#endif + +#ifndef HTTP_DEBUG +#ifdef NDEBUG +#define HTTP_DEBUG +#else +#define HTTP_DEBUG printf +#endif +#endif + +#ifndef HTTP_ERROR +#define HTTP_ERROR printf +#endif + +// Print headers to stdout +err_t http_client_header_print_fn(__unused httpc_state_t *connection, __unused void *arg, struct pbuf *hdr, u16_t hdr_len, __unused u32_t content_len) { + HTTP_INFO("\nheaders %u\n", hdr_len); + u16_t offset = 0; + while (offset < hdr->tot_len && offset < hdr_len) { + char c = (char)pbuf_get_at(hdr, offset++); + HTTP_INFOC(c); + } + return ERR_OK; +} + +// Print body to stdout +err_t http_client_receive_print_fn(__unused void *arg, __unused struct altcp_pcb *conn, struct pbuf *p, err_t err) { + HTTP_INFO("\ncontent err %d\n", err); + u16_t offset = 0; + while (offset < p->tot_len) { + char c = (char)pbuf_get_at(p, offset++); + HTTP_INFOC(c); + } + return ERR_OK; +} + + +static err_t internal_header_fn(httpc_state_t *connection, void *arg, struct pbuf *hdr, u16_t hdr_len, u32_t content_len) { + assert(arg); + PICO_HTTP_REQUEST_T *req = (PICO_HTTP_REQUEST_T*)arg; + if (req->headers_fn) { + return req->headers_fn(connection, req->callback_arg, hdr, hdr_len, content_len); + } + return ERR_OK; +} + +static err_t internal_recv_fn(void *arg, struct altcp_pcb *conn, struct pbuf *p, err_t err) { + assert(arg); + PICO_HTTP_REQUEST_T *req = (PICO_HTTP_REQUEST_T*)arg; + if (req->recv_fn) { + return req->recv_fn(req->callback_arg, conn, p, err); + } + return ERR_OK; +} + +static void internal_result_fn(void *arg, httpc_result_t httpc_result, u32_t rx_content_len, u32_t srv_res, err_t err) { + assert(arg); + PICO_HTTP_REQUEST_T *req = (PICO_HTTP_REQUEST_T*)arg; + HTTP_DEBUG("result %d len %u server_response %u err %d\n", httpc_result, rx_content_len, srv_res, err); + req->complete = true; + req->result = httpc_result; + if (req->result_fn) { + req->result_fn(req->callback_arg, httpc_result, rx_content_len, srv_res, err); + } +} + +// Override altcp_tls_alloc to set sni +static struct altcp_pcb *altcp_tls_alloc_sni(void *arg, u8_t ip_type) { + assert(arg); + PICO_HTTP_REQUEST_T *req = (PICO_HTTP_REQUEST_T*)arg; + struct altcp_pcb *pcb = altcp_tls_alloc(req->tls_config, ip_type); + if (!pcb) { + HTTP_ERROR("Failed to allocate PCB\n"); + return NULL; + } + mbedtls_ssl_set_hostname(altcp_tls_context(pcb), req->hostname); + return pcb; +} + +// Make a http request, complete when req->complete returns true +int http_client_request_async(async_context_t *context, PICO_HTTP_REQUEST_T *req) { +#if LWIP_ALTCP + const uint16_t default_port = req->tls_config ? 443 : 80; + if (req->tls_config) { + if (!req->tls_allocator.alloc) { + req->tls_allocator.alloc = altcp_tls_alloc_sni; + req->tls_allocator.arg = req; + } + req->settings.altcp_allocator = &req->tls_allocator; + } +#else + const uint16_t default_port = 80; +#endif + req->complete = false; + req->settings.headers_done_fn = req->headers_fn ? internal_header_fn : NULL; + req->settings.result_fn = internal_result_fn; + async_context_acquire_lock_blocking(context); + err_t ret = httpc_get_file_dns(req->hostname, req->port ? req->port : default_port, req->url, &req->settings, internal_recv_fn, req, NULL); + async_context_release_lock(context); + if (ret != ERR_OK) { + HTTP_ERROR("http request failed: %d", ret); + } + return ret; +} + +// Make a http request and only return when it has completed. Returns true on success +int http_client_request_sync(async_context_t *context, PICO_HTTP_REQUEST_T *req) { + assert(req); + int ret = http_client_request_async(context, req); + if (ret != 0) { + return ret; + } + while(!req->complete) { + async_context_poll(context); + async_context_wait_for_work_ms(context, 1000); + } + return req->result; +} diff --git a/src/rp2_common/pico_lwip/include/pico/http_client_util.h b/src/rp2_common/pico_lwip/include/pico/http_client_util.h new file mode 100644 index 000000000..5692c1ada --- /dev/null +++ b/src/rp2_common/pico_lwip/include/pico/http_client_util.h @@ -0,0 +1,126 @@ +/** + * Copyright (c) 2024 Raspberry Pi (Trading) Ltd. + * + * SPDX-License-Identifier: BSD-3-Clause + */ + +#ifndef PICO_HTTP_CLIENT_UTIL_H +#define PICO_HTTP_CLIENT_UTIL_H + +#include "lwip/apps/http_client.h" + +/*! \brief Parameters used to make HTTP request + * \ingroup pico_lwip + */ +typedef struct PICO_HTTP_REQUEST { + /*! + * The name of the host, e.g. www.raspberrypi.com + */ + const char *hostname; + /*! + * The url to request, e.g. /favicon.ico + */ + const char *url; + /*! + * Function to callback with headers, can be null + * @see httpc_headers_done_fn + */ + httpc_headers_done_fn headers_fn; + /*! + * Function to callback with results from the server, can be null + * @see altcp_recv_fn + */ + altcp_recv_fn recv_fn; + /*! + * Function to callback with final results of the request, can be null + * @see httpc_result_fn + */ + httpc_result_fn result_fn; + /*! + * Callback to pass to calback functions + */ + void *callback_arg; + /*! + * The port to use. A default port is chosen if this is set to zero + */ + uint16_t port; +#if LWIP_ALTCP && LWIP_ALTCP_TLS + /*! + * TLS configuration, can be null or set to a correctly configured tls configuration. + * e.g altcp_tls_create_config_client(NULL, 0) would use https without a certificate + */ + struct altcp_tls_config *tls_config; + /*! + * TLS allocator, used internall for setting TLS server name indication + */ + altcp_allocator_t tls_allocator; +#endif + /*! + * LwIP HTTP client settings + */ + httpc_connection_t settings; + /*! + * Flag to indicate when the request is complete + */ + int complete; + /*! + * Overall result of http request, only valid when complete is set + */ + httpc_result_t result; + +} PICO_HTTP_REQUEST_T; + +struct async_context; + +/*! \brief Perform a http request asynchronously + * \ingroup pico_lwip + * + * Perform the http request asynchronously + * + * @param context async context + * @param req HTTP request parameters. As a minimum this should be initialised to zero with hostname and url set to valid values + * @return If zero is returned the request has been made and is complete when \em req->complete is true or the result callback has been called. + * A non-zero return value indicates an error. + * + * @see async_context + */ +int http_client_request_async(struct async_context *context, PICO_HTTP_REQUEST_T *req); + +/*! \brief Perform a http request synchronously + * \ingroup pico_lwip + * + * Perform the http request synchronously + * + * @param context async context + * @param req HTTP request parameters. As a minimum this should be initialised to zero with hostname and url set to valid values + * @param result Returns the overall result of the http request when complete. Zero indicates success. + */ +int http_client_request_sync(struct async_context *context, PICO_HTTP_REQUEST_T *req); + +/*! \brief A http header callback that can be passed to \em http_client_init or \em http_client_init_secure + * \ingroup pico_http_client + * + * An implementation of the http header callback which just prints headers to stdout + * + * @param arg argument specified on initialisation + * @param hdr header pbuf(s) (may contain data also) + * @param hdr_len length of the headers in 'hdr' + * @param content_len content length as received in the headers (-1 if not received) + * @return if != zero is returned, the connection is aborted + */ +err_t http_client_header_print_fn(httpc_state_t *connection, void *arg, struct pbuf *hdr, u16_t hdr_len, u32_t content_len); + +/*! \brief A http recv callback that can be passed to http_client_init or http_client_init_secure + * \ingroup pico_http_client + * + * An implementation of the http recv callback which just prints the http body to stdout + * + * @param arg argument specified on initialisation + * @param conn http client connection + * @param p body pbuf(s) + * @param err Error code in the case of an error + * @return if != zero is returned, the connection is aborted + */ +err_t http_client_receive_print_fn(void *arg, struct altcp_pcb *conn, struct pbuf *p, err_t err); + +#endif