Merge branch 'feature/http_server_finalize' into 'master'

Feature add HTTP Web Server component

See merge request idf/esp-idf!2606
This commit is contained in:
Angus Gratton 2018-07-23 13:11:04 +08:00
commit 5f2f84c220
41 changed files with 7089 additions and 2 deletions

View file

@ -37,6 +37,9 @@
#if __has_include("esp_wps.h")
#include "esp_wps.h"
#endif
#if __has_include("http_server.h")
#include "http_server.h"
#endif
#if __has_include("nvs.h")
#include "nvs.h"
#endif
@ -420,6 +423,37 @@ static const esp_err_msg_t esp_err_msg_table[] = {
# ifdef ESP_ERR_HTTP_INVALID_TRANSPORT
ERR_TBL_IT(ESP_ERR_HTTP_INVALID_TRANSPORT), /* 28677 0x7005 There are no transport support for the input
scheme */
# endif
// components/http_server/include/http_server.h
# ifdef ESP_ERR_HTTPD_BASE
ERR_TBL_IT(ESP_ERR_HTTPD_BASE), /* 32768 0x8000 Starting number of HTTPD error codes */
# endif
# ifdef ESP_ERR_HTTPD_HANDLERS_FULL
ERR_TBL_IT(ESP_ERR_HTTPD_HANDLERS_FULL), /* 32769 0x8001 All slots for registering URI handlers have
been consumed */
# endif
# ifdef ESP_ERR_HTTPD_HANDLER_EXISTS
ERR_TBL_IT(ESP_ERR_HTTPD_HANDLER_EXISTS), /* 32770 0x8002 URI handler with same method and target URI
already registered */
# endif
# ifdef ESP_ERR_HTTPD_INVALID_REQ
ERR_TBL_IT(ESP_ERR_HTTPD_INVALID_REQ), /* 32771 0x8003 Invalid request pointer */
# endif
# ifdef ESP_ERR_HTTPD_RESULT_TRUNC
ERR_TBL_IT(ESP_ERR_HTTPD_RESULT_TRUNC), /* 32772 0x8004 Result string truncated */
# endif
# ifdef ESP_ERR_HTTPD_RESP_HDR
ERR_TBL_IT(ESP_ERR_HTTPD_RESP_HDR), /* 32773 0x8005 Response header field larger than supported */
# endif
# ifdef ESP_ERR_HTTPD_RESP_SEND
ERR_TBL_IT(ESP_ERR_HTTPD_RESP_SEND), /* 32774 0x8006 Error occured while sending response packet */
# endif
# ifdef ESP_ERR_HTTPD_ALLOC_MEM
ERR_TBL_IT(ESP_ERR_HTTPD_ALLOC_MEM), /* 32775 0x8007 Failed to dynamically allocate memory for
resource */
# endif
# ifdef ESP_ERR_HTTPD_TASK
ERR_TBL_IT(ESP_ERR_HTTPD_TASK), /* 32776 0x8008 Failed to launch server task/thread */
# endif
// components/spi_flash/include/esp_spi_flash.h
# ifdef ESP_ERR_FLASH_BASE

View file

@ -0,0 +1,15 @@
menu "HTTP Server"
config HTTPD_MAX_REQ_HDR_LEN
int "Max HTTP Request Header Length"
default 512
help
This sets the maximum supported size of headers section in HTTP request packet to be processed by the server
config HTTPD_MAX_URI_LEN
int "Max HTTP URI Length"
default 512
help
This sets the maximum supported size of HTTP request URI to be processed by the server
endmenu

View file

@ -0,0 +1,4 @@
COMPONENT_SRCDIRS := src src/util
COMPONENT_ADD_INCLUDEDIRS := include
COMPONENT_PRIV_INCLUDEDIRS := src/port/esp32 src/util

View file

@ -0,0 +1,924 @@
// Copyright 2018 Espressif Systems (Shanghai) PTE LTD
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#ifndef _HTTP_SERVER_H_
#define _HTTP_SERVER_H_
#include <stdio.h>
#include <string.h>
#include <freertos/FreeRTOS.h>
#include <freertos/task.h>
#include <http_parser.h>
#include <sdkconfig.h>
#include <esp_err.h>
#ifdef __cplusplus
extern "C" {
#endif
#define HTTPD_DEFAULT_CONFIG() { \
.task_priority = tskIDLE_PRIORITY+5, \
.stack_size = 4096, \
.server_port = 80, \
.ctrl_port = 32768, \
.max_open_sockets = 7, \
.max_uri_handlers = 8, \
.max_resp_headers = 8, \
.backlog_conn = 5, \
.lru_purge_enable = false, \
.recv_wait_timeout = 5, \
.send_wait_timeout = 5, \
};
#define ESP_ERR_HTTPD_BASE (0x8000) /*!< Starting number of HTTPD error codes */
#define ESP_ERR_HTTPD_HANDLERS_FULL (ESP_ERR_HTTPD_BASE + 1) /*!< All slots for registering URI handlers have been consumed */
#define ESP_ERR_HTTPD_HANDLER_EXISTS (ESP_ERR_HTTPD_BASE + 2) /*!< URI handler with same method and target URI already registered */
#define ESP_ERR_HTTPD_INVALID_REQ (ESP_ERR_HTTPD_BASE + 3) /*!< Invalid request pointer */
#define ESP_ERR_HTTPD_RESULT_TRUNC (ESP_ERR_HTTPD_BASE + 4) /*!< Result string truncated */
#define ESP_ERR_HTTPD_RESP_HDR (ESP_ERR_HTTPD_BASE + 5) /*!< Response header field larger than supported */
#define ESP_ERR_HTTPD_RESP_SEND (ESP_ERR_HTTPD_BASE + 6) /*!< Error occured while sending response packet */
#define ESP_ERR_HTTPD_ALLOC_MEM (ESP_ERR_HTTPD_BASE + 7) /*!< Failed to dynamically allocate memory for resource */
#define ESP_ERR_HTTPD_TASK (ESP_ERR_HTTPD_BASE + 8) /*!< Failed to launch server task/thread */
/* ************** Group: Initialization ************** */
/** @name Initialization
* APIs related to the Initialization of the web server
* @{
*/
/**
* @brief HTTP Server Instance Handle
*
* Every instance of the server will have a unique handle.
*/
typedef void* httpd_handle_t;
/**
* @brief HTTP Method Type wrapper over "enum http_method"
* available in "http_parser" library
*/
typedef enum http_method httpd_method_t;
/**
* @brief HTTP Server Configuration Structure
*
* @note Use HTTPD_DEFAULT_CONFIG() to initialize the configuration
* to a default value and then modify only those fields that are
* specifically determined by the use case.
*/
typedef struct httpd_config {
unsigned task_priority; /*!< Priority of FreeRTOS task which runs the server */
size_t stack_size; /*!< The maximum stack size allowed for the server task */
/**
* TCP Port number for receiving and transmitting HTTP traffic
*/
uint16_t server_port;
/**
* UDP Port number for asynchronously exchanging control signals
* between various components of the server
*/
uint16_t ctrl_port;
uint16_t max_open_sockets; /*!< Max number of sockets/clients connected at any time*/
uint16_t max_uri_handlers; /*!< Maximum allowed uri handlers */
uint16_t max_resp_headers; /*!< Maximum allowed additional headers in HTTP response */
uint16_t backlog_conn; /*!< Number of backlog connections */
bool lru_purge_enable; /*!< Purge "Least Recently Used" connection */
uint16_t recv_wait_timeout; /*!< Timeout for recv function (in seconds)*/
uint16_t send_wait_timeout; /*!< Timeout for send function (in seconds)*/
} httpd_config_t;
/**
* @brief Starts the web server
*
* Create an instance of HTTP server and allocate memory/resources for it
* depending upon the specified configuration.
*
* Example usage:
* @code{c}
*
* //Function for starting the webserver
* httpd_handle_t start_webserver(void)
* {
* // Generate default configuration
* httpd_config_t config = HTTPD_DEFAULT_CONFIG();
*
* // Empty handle to http_server
* httpd_handle_t server = NULL;
*
* // Start the httpd server
* if (httpd_start(&server, &config) == ESP_OK) {
* // Register URI handlers
* httpd_register_uri_handler(server, &uri_get);
* httpd_register_uri_handler(server, &uri_post);
* }
* // If server failed to start, handle will be NULL
* return server;
* }
*
* @endcode
*
* @param[in] config : Configuration for new instance of the server
* @param[out] handle : Handle to newly created instance of the server. NULL on error
* @return
* - ESP_OK : Instance created successfully
* - ESP_ERR_INVALID_ARG : Null argument(s)
* - ESP_ERR_HTTPD_ALLOC_MEM : Failed to allocate memory for instance
* - ESP_ERR_HTTPD_TASK : Failed to launch server task
*/
esp_err_t httpd_start(httpd_handle_t *handle, const httpd_config_t *config);
/**
* @brief Stops the web server
*
* Deallocates memory/resources used by an HTTP server instance and
* deletes it. Once deleted the handle can no longer be used for accessing
* the instance.
*
* Example usage:
* @code{c}
*
* // Function for stopping the webserver
* void stop_webserver(httpd_handle_t server)
* {
* // Ensure handle is non NULL
* if (server != NULL) {
* // Stop the httpd server
* httpd_stop(server);
* }
* }
*
* @endcode
*
* @param[in] handle Handle to server returned by httpd_start
* @return
* - ESP_OK : Server stopped successfully
* - ESP_ERR_INVALID_ARG : Handle argument is Null
*/
esp_err_t httpd_stop(httpd_handle_t handle);
/** End of Group Initialization
* @}
*/
/* ************** Group: URI Handlers ************** */
/** @name URI Handlers
* APIs related to the URI handlers
* @{
*/
/**
* @brief Function type for freeing context data (if any)
*/
typedef void (*httpd_free_sess_ctx_fn_t)(void *sess_ctx);
/* Max supported HTTP request header length */
#define HTTPD_MAX_REQ_HDR_LEN CONFIG_HTTPD_MAX_REQ_HDR_LEN
/* Max supported HTTP request URI length */
#define HTTPD_MAX_URI_LEN CONFIG_HTTPD_MAX_URI_LEN
/**
* @brief HTTP Request Data Structure
*/
typedef struct httpd_req {
httpd_handle_t handle; /*!< Handle to server instance */
int method; /*!< The type of HTTP request, -1 if unsupported method */
const char uri[HTTPD_MAX_URI_LEN + 1]; /*!< The URI of this request (1 byte extra for null termination) */
size_t content_len; /*!< Length of the request body */
void *aux; /*!< Internally used members */
/**
* User context pointer passed during URI registration.
*/
void *user_ctx;
/**
* Session Context Pointer
*
* A session context. Contexts are maintained across 'sessions' for a
* given open TCP connection. One session could have multiple request
* responses. The web server will ensure that the context persists
* across all these request and responses.
*
* By default, this is NULL. URI Handlers can set this to any meaningful
* value.
*
* If the underlying socket gets closed, and this pointer is non-NULL,
* the web server will free up the context by calling free(), unless
* free_ctx function is set.
*/
void *sess_ctx;
/**
* Pointer to free context hook
*
* Function to free session context
*
* If the web server's socket closes, it frees up the session context by
* calling free() on the sess_ctx member. If you wish to use a custom
* function for freeing the session context, please specify that here.
*/
httpd_free_sess_ctx_fn_t free_ctx;
} httpd_req_t;
/**
* @brief Structure for URI handler
*/
typedef struct httpd_uri {
const char *uri; /*!< The URI to handle */
httpd_method_t method; /*!< Method supported by the URI */
/**
* Handler to call for supported request method. This must
* return ESP_OK, or else the underlying socket will be closed.
*/
esp_err_t (*handler)(httpd_req_t *r);
/**
* Pointer to user context data which will be available to handler
*/
void *user_ctx;
} httpd_uri_t;
/**
* @brief Registers a URI handler
*
* @note URI handlers can be registered in real time as long as the
* server handle is valid.
*
* Example usage:
* @code{c}
*
* esp_err_t my_uri_handler(httpd_req_t* req)
* {
* // Recv , Process and Send
* ....
* ....
* ....
*
* // Fail condition
* if (....) {
* // Return fail to close session //
* return ESP_FAIL;
* }
*
* // On success
* return ESP_OK;
* }
*
* // URI handler structure
* httpd_uri_t my_uri {
* .uri = "/my_uri/path/xyz",
* .method = HTTPD_GET,
* .handler = my_uri_handler,
* .user_ctx = NULL
* };
*
* // Register handler
* if (httpd_register_uri_handler(server_handle, &my_uri) != ESP_OK) {
* // If failed to register handler
* ....
* }
*
* @endcode
*
* @param[in] handle handle to HTTPD server instance
* @param[in] uri_handler pointer to handler that needs to be registered
*
* @return
* - ESP_OK : On successfully registering the handler
* - ESP_ERR_INVALID_ARG : Null arguments
* - ESP_ERR_HTTPD_HANDLERS_FULL : If no slots left for new handler
* - ESP_ERR_HTTPD_HANDLER_EXISTS : If handler with same URI and
* method is already registered
*/
esp_err_t httpd_register_uri_handler(httpd_handle_t handle,
const httpd_uri_t *uri_handler);
/**
* @brief Unregister a URI handler
*
* @param[in] handle handle to HTTPD server instance
* @param[in] uri URI string
* @param[in] method HTTP method
*
* @return
* - ESP_OK : On successfully deregistering the handler
* - ESP_ERR_INVALID_ARG : Null arguments
* - ESP_ERR_NOT_FOUND : Handler with specified URI and method not found
*/
esp_err_t httpd_unregister_uri_handler(httpd_handle_t handle,
const char *uri, httpd_method_t method);
/**
* @brief Unregister all URI handlers with the specified uri string
*
* @param[in] handle handle to HTTPD server instance
* @param[in] uri uri string specifying all handlers that need
* to be deregisterd
*
* @return
* - ESP_OK : On successfully deregistering all such handlers
* - ESP_ERR_INVALID_ARG : Null arguments
* - ESP_ERR_NOT_FOUND : No handler registered with specified uri string
*/
esp_err_t httpd_unregister_uri(httpd_handle_t handle, const char* uri);
/** End of URI Handlers
* @}
*/
/* ************** Group: TX/RX ************** */
/** @name TX / RX
* Prototype for HTTPDs low-level send/recv functions
* @{
*/
/**
* @brief Prototype for HTTPDs low-level send function
* @return
* - Bytes : The number of bytes sent successfully
* - -VE : In case of error
*/
typedef int (*httpd_send_func_t)(int sockfd, const char *buf, size_t buf_len, int flags);
/**
* @brief Prototype for HTTPDs low-level recv function
* @return
* - Bytes : The number of bytes received successfully
* - -VE : In case of error
*/
typedef int (*httpd_recv_func_t)(int sockfd, char *buf, size_t buf_len, int flags);
/** End of TX / RX
* @}
*/
/* ************** Group: Request/Response ************** */
/** @name Request / Response
* APIs related to the data send/receive by URI handlers.
* These APIs are supposed to be called only from the context of
* a URI handler where httpd_req_t* request pointer is valid.
* @{
*/
/**
* @brief Override web server's receive function
*
* This function overrides the web server's receive function. This same function is
* used to read and parse HTTP headers as well as body.
*
* @note This API is supposed to be called only from the context of
* a URI handler where httpd_req_t* request pointer is valid.
*
* @param[in] r The request being responded to
* @param[in] recv_func The receive function to be set for this request
*
* @return
* - ESP_OK : On successfully registering override
* - ESP_ERR_INVALID_ARG : Null arguments
* - ESP_ERR_HTTPD_INVALID_REQ : Invalid request pointer
*/
esp_err_t httpd_set_recv_override(httpd_req_t *r, httpd_recv_func_t recv_func);
/**
* @brief Override web server's send function
*
* This function overrides the web server's send function. This same function is
* used to send out any response to any HTTP request.
*
* @note This API is supposed to be called only from the context of
* a URI handler where httpd_req_t* request pointer is valid.
*
* @param[in] r The request being responded to
* @param[in] send_func The send function to be set for this request
*
* @return
* - ESP_OK : On successfully registering override
* - ESP_ERR_INVALID_ARG : Null arguments
* - ESP_ERR_HTTPD_INVALID_REQ : Invalid request pointer
*/
esp_err_t httpd_set_send_override(httpd_req_t *r, httpd_send_func_t send_func);
/**
* @brief Get the Socket Descriptor from the HTTP request
*
* This API will return the socket descriptor of the session for
* which URI handler was executed on reception of HTTP request.
* This is useful when user wants to call functions that require
* session socket fd, from within a URI handler, ie. :
* httpd_sess_get_ctx(),
* httpd_trigger_sess_close(),
* httpd_sess_update_timestamp().
*
* @note This API is supposed to be called only from the context of
* a URI handler where httpd_req_t* request pointer is valid.
*
* @param[in] r The request whose socket descriptor should be found
*
* @return
* - Socket descriptor : The socket descriptor for this request
* - -1 : Invalid/NULL request pointer
*/
int httpd_req_to_sockfd(httpd_req_t *r);
/**
* @brief API to read content data from the HTTP request
*
* This API will read HTTP content data from the HTTP request into
* provided buffer. Use content_len provided in httpd_req_t structure
* to know the length of data to be fetched. If content_len is too
* large for the buffer then user may have to make multiple calls to
* this function, each time fetching 'buf_len' number of bytes,
* while the pointer to content data is incremented internally by
* the same number.
*
* @note
* - This API is supposed to be called only from the context of
* a URI handler where httpd_req_t* request pointer is valid.
* - If an error is returned, the URI handler must further return an error.
* This will ensure that the erroneous socket is closed and cleaned up by
* the web server.
* - Presently Chunked Encoding is not supported
*
* @param[in] r The request being responded to
* @param[in] buf Pointer to a buffer that the data will be read into
* @param[in] buf_len Length of the buffer
*
* @return
* - Bytes : Number of bytes read into the buffer successfully
* - Zero : When no more data is left for read
* - -1 : On raw recv error / Null arguments / Request pointer is invalid
*/
int httpd_req_recv(httpd_req_t *r, char *buf, size_t buf_len);
/**
* @brief Search for a field in request headers and
* return the string length of it's value
*
* @note
* - This API is supposed to be called only from the context of
* a URI handler where httpd_req_t* request pointer is valid.
* - Once httpd_resp_send() API is called all request headers
* are purged, so request headers need be copied into separate
* buffers if they are required later.
*
* @param[in] r The request being responded to
* @param[in] field The header field to be searched in the request
*
* @return
* - Length : If field is found in the request URL
* - Zero : Field not found / Invalid request / Null arguments
*/
size_t httpd_req_get_hdr_value_len(httpd_req_t *r, const char *field);
/**
* @brief Get the value string of a field from the request headers
*
* @note
* - This API is supposed to be called only from the context of
* a URI handler where httpd_req_t* request pointer is valid.
* - Once httpd_resp_send() API is called all request headers
* are purged, so request headers need be copied into separate
* buffers if they are required later.
* - If output size is greater than input, then the value is truncated,
* accompanied by truncation error as return value.
* - Use httpd_req_get_hdr_value_len() to know the right buffer length
*
* @param[in] r The request being responded to
* @param[in] field The field to be searched in the header
* @param[out] val Pointer to the buffer into which the value will be copied if the field is found
* @param[in] val_size Size of the user buffer "val"
*
* @return
* - ESP_OK : Field found in the request header and value string copied
* - ESP_ERR_NOT_FOUND : Key not found
* - ESP_ERR_INVALID_ARG : Null arguments
* - ESP_ERR_HTTPD_INVALID_REQ : Invalid HTTP request pointer
* - ESP_ERR_HTTPD_RESULT_TRUNC : Value string truncated
*/
esp_err_t httpd_req_get_hdr_value_str(httpd_req_t *r, const char *field, char *val, size_t val_size);
/**
* @brief Get Query string length from the request URL
*
* @note This API is supposed to be called only from the context of
* a URI handler where httpd_req_t* request pointer is valid
*
* @param[in] r The request being responded to
*
* @return
* - Length : Query is found in the request URL
* - Zero : Query not found / Null arguments / Invalid request
*/
size_t httpd_req_get_url_query_len(httpd_req_t *r);
/**
* @brief Get Query string from the request URL
*
* @note
* - Presently, the user can fetch the full URL query string, but decoding
* will have to be performed by the user. Request headers can be read using
* httpd_req_get_hdr_value_str() to know the 'Content-Type' (eg. Content-Type:
* application/x-www-form-urlencoded) and then the appropriate decoding
* algorithm needs to be applied.
* - This API is supposed to be called only from the context of
* a URI handler where httpd_req_t* request pointer is valid
* - If output size is greater than input, then the value is truncated,
* accompanied by truncation error as return value
* - Use httpd_req_get_url_query_len() to know the right buffer length
*
* @param[in] r The request being responded to
* @param[out] buf Pointer to the buffer into which the query string will be copied (if found)
* @param[in] buf_len Length of output buffer
*
* @return
* - ESP_OK : Query is found in the request URL and copied to buffer
* - ESP_ERR_NOT_FOUND : Query not found
* - ESP_ERR_INVALID_ARG : Null arguments
* - ESP_ERR_HTTPD_INVALID_REQ : Invalid HTTP request pointer
* - ESP_ERR_HTTPD_RESULT_TRUNC : Query string truncated
*/
esp_err_t httpd_req_get_url_query_str(httpd_req_t *r, char *buf, size_t buf_len);
/**
* @brief Helper function to get a URL query tag from a query
* string of the type param1=val1&param2=val2
*
* @note
* - The components of URL query string (keys and values) are not URLdecoded.
* The user must check for 'Content-Type' field in the request headers and
* then depending upon the specified encoding (URLencoded or otherwise) apply
* the appropriate decoding algorithm.
* - If actual value size is greater than val_size, then the value is truncated,
* accompanied by truncation error as return value.
*
* @param[in] qry Pointer to query string
* @param[in] key The key to be searched in the query string
* @param[out] val Pointer to the buffer into which the value will be copied if the key is found
* @param[in] val_size Size of the user buffer "val"
*
* @return
* - ESP_OK : Key is found in the URL query string and copied to buffer
* - ESP_ERR_NOT_FOUND : Key not found
* - ESP_ERR_INVALID_ARG : Null arguments
* - ESP_ERR_HTTPD_RESULT_TRUNC : Value string truncated
*/
esp_err_t httpd_query_key_value(const char *qry, const char *key, char *val, size_t val_size);
/**
* @brief API to send a complete HTTP response.
*
* This API will send the data as an HTTP response to the request.
* This assumes that you have the entire response ready in a single
* buffer. If you wish to send response in incremental chunks use
* httpd_resp_send_chunk() instead.
*
* If no status code and content-type were set, by default this
* will send 200 OK status code and content type as text/html.
* You may call the following functions before this API to configure
* the response headers :
* httpd_resp_set_status() - for setting the HTTP status string,
* httpd_resp_set_type() - for setting the Content Type,
* httpd_resp_set_hdr() - for appending any additional field
* value entries in the response header
*
* @note
* - This API is supposed to be called only from the context of
* a URI handler where httpd_req_t* request pointer is valid.
* - Once this API is called, the request has been responded to.
* - No additional data can then be sent for the request.
* - Once this API is called, all request headers are purged, so
* request headers need be copied into separate buffers if
* they are required later.
*
* @param[in] r The request being responded to
* @param[in] buf Buffer from where the content is to be fetched
* @param[in] buf_len Length of the buffer
*
* @return
* - ESP_OK : On successfully sending the response packet
* - ESP_ERR_INVALID_ARG : Null request pointer
* - ESP_ERR_HTTPD_RESP_HDR : Essential headers are too large for internal buffer
* - ESP_ERR_HTTPD_RESP_SEND : Error in raw send
* - ESP_ERR_HTTPD_INVALID_REQ : Invalid request
*/
esp_err_t httpd_resp_send(httpd_req_t *r, const char *buf, size_t buf_len);
/**
* @brief API to send one HTTP chunk
*
* This API will send the data as an HTTP response to the
* request. This API will use chunked-encoding and send the response
* in the form of chunks. If you have the entire response contained in
* a single buffer, please use httpd_resp_send() instead.
*
* If no status code and content-type were set, by default this will
* send 200 OK status code and content type as text/html. You may
* call the following functions before this API to configure the
* response headers
* httpd_resp_set_status() - for setting the HTTP status string,
* httpd_resp_set_type() - for setting the Content Type,
* httpd_resp_set_hdr() - for appending any additional field
* value entries in the response header
*
* @note
* - This API is supposed to be called only from the context of
* a URI handler where httpd_req_t* request pointer is valid.
* - When you are finished sending all your chunks, you must call
* this function with buf_len as 0.
* - Once this API is called, all request headers are purged, so
* request headers need be copied into separate buffers if they
* are required later.
*
* @param[in] r The request being responded to
* @param[in] buf Pointer to a buffer that stores the data
* @param[in] buf_len Length of the data from the buffer that should be sent out
*
* @return
* - ESP_OK : On successfully sending the response packet chunk
* - ESP_ERR_INVALID_ARG : Null request pointer
* - ESP_ERR_HTTPD_RESP_HDR : Essential headers are too large for internal buffer
* - ESP_ERR_HTTPD_RESP_SEND : Error in raw send
* - ESP_ERR_HTTPD_INVALID_REQ : Invalid request pointer
*/
esp_err_t httpd_resp_send_chunk(httpd_req_t *r, const char *buf, size_t buf_len);
/* Some commonly used status codes */
#define HTTPD_200 "200 OK" /*!< HTTP Response 200 */
#define HTTPD_204 "204 No Content" /*!< HTTP Response 204 */
#define HTTPD_207 "207 Multi-Status" /*!< HTTP Response 207 */
#define HTTPD_400 "400 Bad Request" /*!< HTTP Response 400 */
#define HTTPD_404 "404 Not Found" /*!< HTTP Response 404 */
#define HTTPD_500 "500 Internal Server Error" /*!< HTTP Response 500 */
/**
* @brief API to set the HTTP status code
*
* This API sets the status of the HTTP response to the value specified.
* By default, the '200 OK' response is sent as the response.
*
* @note
* - This API is supposed to be called only from the context of
* a URI handler where httpd_req_t* request pointer is valid.
* - This API only sets the status to this value. The status isn't
* sent out until any of the send APIs is executed.
* - Make sure that the lifetime of the status string is valid till
* send function is called.
*
* @param[in] r The request being responded to
* @param[in] status The HTTP status code of this response
*
* @return
* - ESP_OK : On success
* - ESP_ERR_INVALID_ARG : Null arguments
* - ESP_ERR_HTTPD_INVALID_REQ : Invalid request pointer
*/
esp_err_t httpd_resp_set_status(httpd_req_t *r, const char *status);
/* Some commonly used content types */
#define HTTPD_TYPE_JSON "application/json" /*!< HTTP Content type JSON */
#define HTTPD_TYPE_TEXT "text/html" /*!< HTTP Content type text/HTML */
#define HTTPD_TYPE_OCTET "application/octet-stream" /*!< HTTP Content type octext-stream */
/**
* @brief API to set the HTTP content type
*
* This API sets the 'Content Type' field of the response.
* The default content type is 'text/html'.
*
* @note
* - This API is supposed to be called only from the context of
* a URI handler where httpd_req_t* request pointer is valid.
* - This API only sets the content type to this value. The type
* isn't sent out until any of the send APIs is executed.
* - Make sure that the lifetime of the type string is valid till
* send function is called.
*
* @param[in] r The request being responded to
* @param[in] type The Content Type of the response
*
* @return
* - ESP_OK : On success
* - ESP_ERR_INVALID_ARG : Null arguments
* - ESP_ERR_HTTPD_INVALID_REQ : Invalid request pointer
*/
esp_err_t httpd_resp_set_type(httpd_req_t *r, const char *type);
/**
* @brief API to append any additional headers
*
* This API sets any additional header fields that need to be sent in the response.
*
* @note
* - This API is supposed to be called only from the context of
* a URI handler where httpd_req_t* request pointer is valid.
* - The header isn't sent out until any of the send APIs is executed.
* - The maximum allowed number of additional headers is limited to
* value of max_resp_headers in config structure.
* - Make sure that the lifetime of the field value strings are valid till
* send function is called.
*
* @param[in] r The request being responded to
* @param[in] field The field name of the HTTP header
* @param[in] value The value of this HTTP header
*
* @return
* - ESP_OK : On successfully appending new header
* - ESP_ERR_INVALID_ARG : Null arguments
* - ESP_ERR_HTTPD_RESP_HDR : Total additional headers exceed max allowed
* - ESP_ERR_HTTPD_INVALID_REQ : Invalid request pointer
*/
esp_err_t httpd_resp_set_hdr(httpd_req_t *r, const char *field, const char *value);
/**
* @brief Helper function for HTTP 404
*
* Send HTTP 404 message. If you wish to send additional data in the body of the
* response, please use the lower-level functions directly.
*
* @note
* - This API is supposed to be called only from the context of
* a URI handler where httpd_req_t* request pointer is valid.
* - Once this API is called, all request headers are purged, so
* request headers need be copied into separate buffers if
* they are required later.
*
* @param[in] r The request being responded to
*
* @return
* - ESP_OK : On successfully sending the response packet
* - ESP_ERR_INVALID_ARG : Null arguments
* - ESP_ERR_HTTPD_RESP_SEND : Error in raw send
* - ESP_ERR_HTTPD_INVALID_REQ : Invalid request pointer
*/
esp_err_t httpd_resp_send_404(httpd_req_t *r);
/**
* @brief Raw HTTP send
*
* Call this API if you wish to construct your custom response packet.
* When using this, all essential header, eg. HTTP version, Status Code,
* Content Type and Length, Encoding, etc. will have to be constructed
* manually, and HTTP delimeters (CRLF) will need to be placed correctly
* for separating sub-sections of the HTTP response packet.
*
* If the send override function is set, this API will end up
* calling that function eventually to send data out.
*
* @note
* - This API is supposed to be called only from the context of
* a URI handler where httpd_req_t* request pointer is valid.
* - Unless the response has the correct HTTP structure (which the
* user must now ensure) it is not guaranteed that it will be
* recognized by the client. For most cases, you wouldn't have
* to call this API, but you would rather use either of :
* httpd_resp_send(),
* httpd_resp_send_chunk()
*
* @param[in] r The request being responded to
* @param[in] buf Buffer from where the fully constructed packet is to be read
* @param[in] buf_len Length of the buffer
*
* @return
* - Bytes : Number of bytes that were sent successfully
* - -1 : Error in raw send / Invalid request / Null arguments
*/
int httpd_send(httpd_req_t *r, const char *buf, size_t buf_len);
/** End of Request / Response
* @}
*/
/* ************** Group: Session ************** */
/** @name Session
* Functions for controlling sessions and accessing context data
* @{
*/
/**
* @brief Get session context from socket descriptor
*
* Typically if a session context is created, it is available to URI handlers
* through the httpd_req_t structure. But, there are cases where the web
* server's send/receive functions may require the context (for example, for
* accessing keying information etc). Since the send/receive function only have
* the socket descriptor at their disposal, this API provides them with a way to
* retrieve the session context.
*
* @param[in] handle Handle to server returned by httpd_start
* @param[in] sockfd The socket descriptor for which the context should be extracted.
*
* @return
* - void* : Pointer to the context associated with this session
* - NULL : Empty context / Invalid handle / Invalid socket fd
*/
void *httpd_sess_get_ctx(httpd_handle_t handle, int sockfd);
/**
* @brief Trigger an httpd session close externally
*
* @note Calling this API is only required in special circumstances wherein
* some application requires to close an httpd client session asynchronously.
*
* @param[in] handle Handle to server returned by httpd_start
* @param[in] sockfd The socket descriptor of the session to be closed
*
* @return
* - ESP_OK : On successfully initiating closure
* - ESP_FAIL : Failure to queue work
* - ESP_ERR_NOT_FOUND : Socket fd not found
* - ESP_ERR_INVALID_ARG : Null arguments
*/
esp_err_t httpd_trigger_sess_close(httpd_handle_t handle, int sockfd);
/**
* @brief Update timestamp for a given socket
*
* Timestamps are internally associated with each session to monitor
* how recently a session exchanged traffic. When LRU purge is enabled,
* if a client is requesting for connection but maximum number of
* sockets/sessions is reached, then the session having the earliest
* timestamp is closed automatically.
*
* Updating the timestamp manually prevents the socket from being purged
* due to the Least Recently Used (LRU) logic, even though it might not
* have received traffic for some time. This is useful when all open
* sockets/session are frequently exchanging traffic but the user specifically
* wants one of the sessions to be kept open, irrespective of when it last
* exchanged a packet.
*
* @note Calling this API is only necessary if the LRU Purge Enable option
* is enabled.
*
* @param[in] handle Handle to server returned by httpd_start
* @param[in] sockfd The socket descriptor of the session for which timestamp
* is to be updated
*
* @return
* - ESP_OK : Socket found and timestamp updated
* - ESP_ERR_NOT_FOUND : Socket not found
* - ESP_ERR_INVALID_ARG : Null arguments
*/
esp_err_t httpd_sess_update_timestamp(httpd_handle_t handle, int sockfd);
/** End of Session
* @}
*/
/* ************** Group: Work Queue ************** */
/** @name Work Queue
* APIs related to the HTTPD Work Queue
* @{
*/
/**
* @brief Prototype of the HTTPD work function
* Please refer to httpd_queue_work() for more details.
* @param[in] arg The arguments for this work function
*/
typedef void (*httpd_work_fn_t)(void *arg);
/**
* @brief Queue execution of a function in HTTPD's context
*
* This API queues a work function for asynchronous execution
*
* @note Some protocols require that the web server generate some asynchronous data
* and send it to the persistently opened connection. This facility is for use
* by such protocols.
*
* @param[in] handle Handle to server returned by httpd_start
* @param[in] work Pointer to the function to be executed in the HTTPD's context
* @param[in] arg Pointer to the arguments that should be passed to this function
*
* @return
* - ESP_OK : On successfully queueing the work
* - ESP_FAIL : Failure in ctrl socket
* - ESP_ERR_INVALID_ARG : Null arguments
*/
esp_err_t httpd_queue_work(httpd_handle_t handle, httpd_work_fn_t work, void *arg);
/** End of Group Work Queue
* @}
*/
#ifdef __cplusplus
}
#endif
#endif /* ! _HTTP_SERVER_H_ */

View file

@ -0,0 +1,376 @@
// Copyright 2018 Espressif Systems (Shanghai) PTE LTD
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#include <string.h>
#include <sys/socket.h>
#include <sys/param.h>
#include <errno.h>
#include <esp_log.h>
#include <esp_err.h>
#include <assert.h>
#include <http_server.h>
#include "httpd_priv.h"
#include "ctrl_sock.h"
static const char *TAG = "httpd";
static esp_err_t httpd_accept_conn(struct httpd_data *hd, int listen_fd)
{
/* If no space is available for new session, close the least recently used one */
if (hd->config.lru_purge_enable == true) {
if (!httpd_is_sess_available(hd)) {
/* Queue asynchronous closure of the least recently used session */
return httpd_sess_close_lru(hd);
/* Returning from this allowes the main server thread to process
* the queued asynchronous control message for closing LRU session.
* Since connection request hasn't been addressed yet using accept()
* therefore httpd_accept_conn() will be called again, but this time
* with space available for one session
*/
}
}
struct sockaddr_in addr_from;
socklen_t addr_from_len = sizeof(addr_from);
int new_fd = accept(listen_fd, (struct sockaddr *)&addr_from, &addr_from_len);
if (new_fd < 0) {
ESP_LOGW(TAG, LOG_FMT("error in accept (%d)"), errno);
return ESP_FAIL;
}
ESP_LOGD(TAG, LOG_FMT("newfd = %d"), new_fd);
struct timeval tv;
/* Set recv timeout of this fd as per config */
tv.tv_sec = hd->config.recv_wait_timeout;
tv.tv_usec = 0;
setsockopt(new_fd, SOL_SOCKET, SO_RCVTIMEO, (const char*)&tv, sizeof(tv));
/* Set send timeout of this fd as per config */
tv.tv_sec = hd->config.send_wait_timeout;
tv.tv_usec = 0;
setsockopt(new_fd, SOL_SOCKET, SO_SNDTIMEO, (const char*)&tv, sizeof(tv));
if (httpd_sess_new(hd, new_fd)) {
ESP_LOGW(TAG, LOG_FMT("no slots left for launching new session"));
close(new_fd);
return ESP_FAIL;
}
ESP_LOGD(TAG, LOG_FMT("complete"));
return ESP_OK;
}
struct httpd_ctrl_data {
enum httpd_ctrl_msg {
HTTPD_CTRL_SHUTDOWN,
HTTPD_CTRL_WORK,
} hc_msg;
httpd_work_fn_t hc_work;
void *hc_work_arg;
};
esp_err_t httpd_queue_work(httpd_handle_t handle, httpd_work_fn_t work, void *arg)
{
if (handle == NULL || work == NULL) {
return ESP_ERR_INVALID_ARG;
}
struct httpd_data *hd = (struct httpd_data *) handle;
struct httpd_ctrl_data msg = {
.hc_msg = HTTPD_CTRL_WORK,
.hc_work = work,
.hc_work_arg = arg,
};
int ret = cs_send_to_ctrl_sock(hd->msg_fd, hd->config.ctrl_port, &msg, sizeof(msg));
if (ret < 0) {
ESP_LOGW(TAG, LOG_FMT("failed to queue work"));
return ESP_FAIL;
}
return ESP_OK;
}
static void httpd_close_all_sessions(struct httpd_data *hd)
{
int fd = -1;
while ((fd = httpd_sess_iterate(hd, fd)) != -1) {
ESP_LOGD(TAG, LOG_FMT("cleaning up socket %d"), fd);
httpd_sess_delete(hd, fd);
close(fd);
}
}
static void httpd_process_ctrl_msg(struct httpd_data *hd)
{
struct httpd_ctrl_data msg;
int ret = recv(hd->ctrl_fd, &msg, sizeof(msg), 0);
if (ret <= 0) {
ESP_LOGW(TAG, LOG_FMT("error in recv (%d)"), errno);
return;
}
if (ret != sizeof(msg)) {
ESP_LOGW(TAG, LOG_FMT("incomplete msg"));
return;
}
switch (msg.hc_msg) {
case HTTPD_CTRL_WORK:
if (msg.hc_work) {
ESP_LOGD(TAG, LOG_FMT("work"));
(*msg.hc_work)(msg.hc_work_arg);
}
break;
case HTTPD_CTRL_SHUTDOWN:
ESP_LOGD(TAG, LOG_FMT("shutdown"));
hd->hd_td.status = THREAD_STOPPING;
break;
default:
break;
}
}
/* Manage in-coming connection or data requests */
static esp_err_t httpd_server(struct httpd_data *hd)
{
fd_set read_set;
FD_ZERO(&read_set);
FD_SET(hd->listen_fd, &read_set);
FD_SET(hd->ctrl_fd, &read_set);
int tmp_max_fd;
httpd_sess_set_descriptors(hd, &read_set, &tmp_max_fd);
int maxfd = MAX(hd->listen_fd, tmp_max_fd);
tmp_max_fd = maxfd;
maxfd = MAX(hd->ctrl_fd, tmp_max_fd);
ESP_LOGD(TAG, LOG_FMT("doing select maxfd+1 = %d"), maxfd + 1);
int active_cnt = select(maxfd + 1, &read_set, NULL, NULL, NULL);
if (active_cnt < 0) {
ESP_LOGE(TAG, LOG_FMT("error in select (%d)"), errno);
/* Assert, as it's not possible to recover from this point onwards,
* and there is no way to notify the main thread that server handle
* has become invalid */
assert(false);
return ESP_FAIL;
}
/* Case0: Do we have a control message? */
if (FD_ISSET(hd->ctrl_fd, &read_set)) {
ESP_LOGD(TAG, LOG_FMT("processing ctrl message"));
httpd_process_ctrl_msg(hd);
if (hd->hd_td.status == THREAD_STOPPING) {
ESP_LOGD(TAG, LOG_FMT("stopping thread"));
return ESP_FAIL;
}
}
/* Case1: Do we have any activity on the current data
* sessions? */
int fd = -1;
while ((fd = httpd_sess_iterate(hd, fd)) != -1) {
if (FD_ISSET(fd, &read_set) || (httpd_sess_pending(hd, fd))) {
ESP_LOGD(TAG, LOG_FMT("processing socket %d"), fd);
if (httpd_sess_process(hd, fd) != ESP_OK) {
ESP_LOGD(TAG, LOG_FMT("closing socket %d"), fd);
close(fd);
/* Delete session and update fd to that
* preceding the one being deleted */
fd = httpd_sess_delete(hd, fd);
}
}
}
/* Case2: Do we have any incoming connection requests to
* process? */
if (FD_ISSET(hd->listen_fd, &read_set)) {
ESP_LOGD(TAG, LOG_FMT("processing listen socket %d"), hd->listen_fd);
if (httpd_accept_conn(hd, hd->listen_fd) != ESP_OK) {
ESP_LOGW(TAG, LOG_FMT("error accepting new connection"));
}
}
return ESP_OK;
}
/* The main HTTPD thread */
static void httpd_thread(void *arg)
{
int ret;
struct httpd_data *hd = (struct httpd_data *) arg;
hd->hd_td.status = THREAD_RUNNING;
ESP_LOGD(TAG, LOG_FMT("web server started"));
while (1) {
ret = httpd_server(hd);
if (ret != ESP_OK) {
break;
}
}
ESP_LOGD(TAG, LOG_FMT("web server exiting"));
close(hd->msg_fd);
cs_free_ctrl_sock(hd->ctrl_fd);
httpd_close_all_sessions(hd);
close(hd->listen_fd);
hd->hd_td.status = THREAD_STOPPED;
httpd_os_thread_delete();
}
static esp_err_t httpd_server_init(struct httpd_data *hd)
{
int fd = socket(PF_INET6, SOCK_STREAM, 0);
if (fd < 0) {
ESP_LOGE(TAG, LOG_FMT("error in socket (%d)"), errno);
return ESP_FAIL;
}
struct in6_addr inaddr_any = IN6ADDR_ANY_INIT;
struct sockaddr_in6 serv_addr = {
.sin6_family = PF_INET6,
.sin6_addr = inaddr_any,
.sin6_port = htons(hd->config.server_port)
};
int ret = bind(fd, (struct sockaddr *)&serv_addr, sizeof(serv_addr));
if (ret < 0) {
ESP_LOGE(TAG, LOG_FMT("error in bind (%d)"), errno);
close(fd);
return ESP_FAIL;
}
ret = listen(fd, hd->config.backlog_conn);
if (ret < 0) {
ESP_LOGE(TAG, LOG_FMT("error in listen (%d)"), errno);
close(fd);
return ESP_FAIL;
}
int ctrl_fd = cs_create_ctrl_sock(hd->config.ctrl_port);
if (ctrl_fd < 0) {
ESP_LOGE(TAG, LOG_FMT("error in creating ctrl socket (%d)"), errno);
close(fd);
return ESP_FAIL;
}
int msg_fd = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
if (msg_fd < 0) {
ESP_LOGE(TAG, LOG_FMT("error in creating msg socket (%d)"), errno);
close(fd);
close(ctrl_fd);
return ESP_FAIL;
}
hd->listen_fd = fd;
hd->ctrl_fd = ctrl_fd;
hd->msg_fd = msg_fd;
return ESP_OK;
}
static struct httpd_data *httpd_create(const httpd_config_t *config)
{
/* Allocate memory for httpd instance data */
struct httpd_data *hd = calloc(1, sizeof(struct httpd_data));
if (hd != NULL) {
hd->hd_calls = calloc(config->max_uri_handlers, sizeof(httpd_uri_t *));
if (hd->hd_calls == NULL) {
free(hd);
return NULL;
}
hd->hd_sd = calloc(config->max_open_sockets, sizeof(struct sock_db));
if (hd->hd_sd == NULL) {
free(hd->hd_calls);
free(hd);
return NULL;
}
struct httpd_req_aux *ra = &hd->hd_req_aux;
ra->resp_hdrs = calloc(config->max_resp_headers, sizeof(struct resp_hdr));
if (ra->resp_hdrs == NULL) {
free(hd->hd_sd);
free(hd->hd_calls);
free(hd);
return NULL;
}
/* Save the configuration for this instance */
hd->config = *config;
} else {
ESP_LOGE(TAG, "mem alloc failed");
}
return hd;
}
static void httpd_delete(struct httpd_data *hd)
{
struct httpd_req_aux *ra = &hd->hd_req_aux;
/* Free memory of httpd instance data */
free(ra->resp_hdrs);
free(hd->hd_sd);
/* Free registered URI handlers */
httpd_unregister_all_uri_handlers(hd);
free(hd->hd_calls);
free(hd);
}
esp_err_t httpd_start(httpd_handle_t *handle, const httpd_config_t *config)
{
if (handle == NULL || config == NULL) {
return ESP_ERR_INVALID_ARG;
}
struct httpd_data *hd = httpd_create(config);
if (hd == NULL) {
/* Failed to allocate memory */
return ESP_ERR_HTTPD_ALLOC_MEM;
}
if (httpd_server_init(hd) != ESP_OK) {
httpd_delete(hd);
return ESP_FAIL;
}
httpd_sess_init(hd);
if (httpd_os_thread_create(&hd->hd_td.handle, "httpd",
hd->config.stack_size,
hd->config.task_priority,
httpd_thread, hd) != ESP_OK) {
/* Failed to launch task */
httpd_delete(hd);
return ESP_ERR_HTTPD_TASK;
}
*handle = (httpd_handle_t *)hd;
return ESP_OK;
}
esp_err_t httpd_stop(httpd_handle_t handle)
{
struct httpd_data *hd = (struct httpd_data *) handle;
if (hd == NULL) {
return ESP_ERR_INVALID_ARG;
}
struct httpd_ctrl_data msg;
memset(&msg, 0, sizeof(msg));
msg.hc_msg = HTTPD_CTRL_SHUTDOWN;
cs_send_to_ctrl_sock(hd->msg_fd, hd->config.ctrl_port, &msg, sizeof(msg));
ESP_LOGD(TAG, LOG_FMT("sent control msg to stop server"));
while (hd->hd_td.status != THREAD_STOPPED) {
httpd_os_thread_sleep(1000);
}
ESP_LOGD(TAG, LOG_FMT("server stopped"));
httpd_delete(hd);
return ESP_OK;
}

View file

@ -0,0 +1,839 @@
// Copyright 2018 Espressif Systems (Shanghai) PTE LTD
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#include <stdlib.h>
#include <sys/param.h>
#include <esp_log.h>
#include <esp_err.h>
#include <http_parser.h>
#include <http_server.h>
#include "httpd_priv.h"
#include "osal.h"
static const char *TAG = "httpd_parse";
typedef struct {
/* Parser settings for http_parser_execute() */
http_parser_settings settings;
/* Request being parsed */
struct httpd_req *req;
/* Status of the parser describes the part of the
* HTTP request packet being processed at any moment.
*/
enum {
PARSING_IDLE = 0,
PARSING_URL,
PARSING_HDR_FIELD,
PARSING_HDR_VALUE,
PARSING_BODY,
PARSING_COMPLETE,
PARSING_FAILED
} status;
/* Response error code in case of PARSING_FAILED */
httpd_err_resp_t error;
/* For storing last callback parameters */
struct {
const char *at;
size_t length;
} last;
/* State variables */
bool paused; /*!< Parser is paused */
size_t pre_parsed; /*!< Length of data to be skipped while parsing */
size_t raw_datalen; /*!< Full length of the raw data in scratch buffer */
} parser_data_t;
static esp_err_t verify_url (http_parser *parser)
{
parser_data_t *parser_data = (parser_data_t *) parser->data;
struct httpd_req *r = parser_data->req;
struct httpd_req_aux *ra = r->aux;
struct http_parser_url *res = &ra->url_parse_res;
/* Get previous values of the parser callback arguments */
const char *at = parser_data->last.at;
size_t length = parser_data->last.length;
if ((r->method = parser->method) < 0) {
ESP_LOGW(TAG, LOG_FMT("HTTP Operation not supported"));
parser_data->error = HTTPD_501_METHOD_NOT_IMPLEMENTED;
return ESP_FAIL;
}
if (sizeof(r->uri) < (length + 1)) {
ESP_LOGW(TAG, LOG_FMT("URI length (%d) greater than supported (%d)"),
length, sizeof(r->uri));
parser_data->error = HTTPD_414_URI_TOO_LONG;
parser_data->status = PARSING_FAILED;
return ESP_FAIL;
}
/* Keep URI with terminating null character. Note URI string pointed
* by 'at' is not NULL terminated, therfore use length provided by
* parser while copying the URI to buffer */
strlcpy((char *)r->uri, at, (length + 1));
ESP_LOGD(TAG, LOG_FMT("received URI = %s"), r->uri);
/* Make sure version is HTTP/1.1 */
if ((parser->http_major != 1) && (parser->http_minor != 1)) {
ESP_LOGW(TAG, LOG_FMT("unsupported HTTP version = %d.%d"),
parser->http_major, parser->http_minor);
parser_data->error = HTTPD_505_VERSION_NOT_SUPPORTED;
return ESP_FAIL;
}
/* Parse URL and keep result for later */
http_parser_url_init(res);
if (http_parser_parse_url(r->uri, strlen(r->uri),
r->method == HTTP_CONNECT, res)) {
ESP_LOGW(TAG, LOG_FMT("http_parser_parse_url failed with errno = %d"),
parser->http_errno);
parser_data->error = HTTPD_400_BAD_REQUEST;
return ESP_FAIL;
}
return ESP_OK;
}
/* http_parser callback on finding url in HTTP request
* Will be invoked ATLEAST once every packet
*/
static esp_err_t cb_url(http_parser *parser,
const char *at, size_t length)
{
parser_data_t *parser_data = (parser_data_t *) parser->data;
if (parser_data->status == PARSING_IDLE) {
ESP_LOGD(TAG, LOG_FMT("message begin"));
/* Store current values of the parser callback arguments */
parser_data->last.at = at;
parser_data->last.length = 0;
parser_data->status = PARSING_URL;
} else if (parser_data->status != PARSING_URL) {
ESP_LOGE(TAG, LOG_FMT("unexpected state transition"));
parser_data->status = PARSING_FAILED;
return ESP_FAIL;
}
ESP_LOGD(TAG, LOG_FMT("processing url = %.*s"), length, at);
/* Update length of URL string */
if ((parser_data->last.length += length) > HTTPD_MAX_URI_LEN) {
ESP_LOGW(TAG, LOG_FMT("URI length (%d) greater than supported (%d)"),
parser_data->last.length, HTTPD_MAX_URI_LEN);
parser_data->error = HTTPD_414_URI_TOO_LONG;
parser_data->status = PARSING_FAILED;
return ESP_FAIL;
}
return ESP_OK;
}
static esp_err_t pause_parsing(http_parser *parser, const char* at)
{
parser_data_t *parser_data = (parser_data_t *) parser->data;
struct httpd_req *r = parser_data->req;
struct httpd_req_aux *ra = r->aux;
parser_data->pre_parsed = parser_data->raw_datalen
- (at - ra->scratch);
if (parser_data->pre_parsed != httpd_unrecv(r, at, parser_data->pre_parsed)) {
ESP_LOGE(TAG, LOG_FMT("data too large for un-recv = %d"),
parser_data->pre_parsed);
return ESP_FAIL;
}
http_parser_pause(parser, 1);
parser_data->paused = true;
ESP_LOGD(TAG, LOG_FMT("paused"));
return ESP_OK;
}
static size_t continue_parsing(http_parser *parser, size_t length)
{
parser_data_t *data = (parser_data_t *) parser->data;
/* Part of the blk may have been parsed before
* so we must skip that */
length = MIN(length, data->pre_parsed);
data->pre_parsed -= length;
ESP_LOGD(TAG, LOG_FMT("skip pre-parsed data of size = %d"), length);
http_parser_pause(parser, 0);
data->paused = false;
ESP_LOGD(TAG, LOG_FMT("un-paused"));
return length;
}
/* http_parser callback on header field in HTTP request
* May be invoked ATLEAST once every header field
*/
static esp_err_t cb_header_field(http_parser *parser, const char *at, size_t length)
{
parser_data_t *parser_data = (parser_data_t *) parser->data;
struct httpd_req *r = parser_data->req;
struct httpd_req_aux *ra = r->aux;
/* Check previous status */
if (parser_data->status == PARSING_URL) {
if (verify_url(parser) != ESP_OK) {
parser_data->status = PARSING_FAILED;
return ESP_FAIL;
}
ESP_LOGD(TAG, LOG_FMT("headers begin"));
/* Last at is set to start of scratch where headers
* will be received next */
parser_data->last.at = ra->scratch;
parser_data->last.length = 0;
parser_data->status = PARSING_HDR_FIELD;
/* Stop parsing for now and give control to process */
if (pause_parsing(parser, at) != ESP_OK) {
parser_data->status = PARSING_FAILED;
return ESP_FAIL;
}
} else if (parser_data->status == PARSING_HDR_VALUE) {
/* NULL terminate last header (key: value) pair */
size_t offset = parser_data->last.at - ra->scratch;
ra->scratch[offset + parser_data->last.length] = '\0';
/* Store current values of the parser callback arguments */
parser_data->last.at = at;
parser_data->last.length = 0;
parser_data->status = PARSING_HDR_FIELD;
} else if (parser_data->status != PARSING_HDR_FIELD) {
ESP_LOGE(TAG, LOG_FMT("unexpected state transition"));
parser_data->status = PARSING_FAILED;
return ESP_FAIL;
}
ESP_LOGD(TAG, LOG_FMT("processing field = %.*s"), length, at);
/* Update length of header string */
parser_data->last.length += length;
return ESP_OK;
}
/* http_parser callback on header value in HTTP request.
* May be invoked ATLEAST once every header value
*/
static esp_err_t cb_header_value(http_parser *parser, const char *at, size_t length)
{
parser_data_t *parser_data = (parser_data_t *) parser->data;
struct httpd_req *r = parser_data->req;
struct httpd_req_aux *ra = r->aux;
/* Check previous status */
if (parser_data->status == PARSING_HDR_FIELD) {
/* Store current values of the parser callback arguments */
parser_data->last.at = at;
parser_data->last.length = 0;
parser_data->status = PARSING_HDR_VALUE;
/* Increment header count */
ra->req_hdrs_count++;
} else if (parser_data->status != PARSING_HDR_VALUE) {
ESP_LOGE(TAG, LOG_FMT("unexpected state transition"));
parser_data->status = PARSING_FAILED;
return ESP_FAIL;
}
ESP_LOGD(TAG, LOG_FMT("processing value = %.*s"), length, at);
/* Update length of header string */
parser_data->last.length += length;
return ESP_OK;
}
/* http_parser callback on completing headers in HTTP request.
* Will be invoked ONLY once every packet
*/
static esp_err_t cb_headers_complete(http_parser *parser)
{
parser_data_t *parser_data = (parser_data_t *) parser->data;
struct httpd_req *r = parser_data->req;
struct httpd_req_aux *ra = r->aux;
/* Check previous status */
if (parser_data->status == PARSING_URL) {
ESP_LOGD(TAG, LOG_FMT("no headers"));
if (verify_url(parser) != ESP_OK) {
parser_data->status = PARSING_FAILED;
return ESP_FAIL;
}
} else if (parser_data->status == PARSING_HDR_VALUE) {
/* NULL terminate last header (key: value) pair */
size_t offset = parser_data->last.at - ra->scratch;
ra->scratch[offset + parser_data->last.length] = '\0';
/* Reach end of last header */
parser_data->last.at += parser_data->last.length;
} else {
ESP_LOGE(TAG, LOG_FMT("unexpected state transition"));
parser_data->status = PARSING_FAILED;
return ESP_FAIL;
}
/* In absence of body/chunked enoding, http_parser sets content_len to -1 */
r->content_len = ((int)parser->content_length != -1 ?
parser->content_length : 0);
ESP_LOGD(TAG, LOG_FMT("bytes read = %d"), parser->nread);
ESP_LOGD(TAG, LOG_FMT("content length = %zu"), r->content_len);
if (parser->upgrade) {
ESP_LOGW(TAG, LOG_FMT("upgrade from HTTP not supported"));
parser_data->error = HTTPD_XXX_UPGRADE_NOT_SUPPORTED;
parser_data->status = PARSING_FAILED;
return ESP_FAIL;
}
parser_data->status = PARSING_BODY;
ra->remaining_len = r->content_len;
return ESP_OK;
}
/* Last http_parser callback if body present in HTTP request.
* Will be invoked ONLY once every packet
*/
static esp_err_t cb_on_body(http_parser *parser, const char *at, size_t length)
{
parser_data_t *parser_data = (parser_data_t *) parser->data;
/* Check previous status */
if (parser_data->status != PARSING_BODY) {
ESP_LOGE(TAG, LOG_FMT("unexpected state transition"));
parser_data->status = PARSING_FAILED;
return ESP_FAIL;
}
/* Pause parsing so that if part of another packet
* is in queue then it doesn't get parsed, which
* may reset the parser state and cause current
* request packet to be lost */
if (pause_parsing(parser, at) != ESP_OK) {
parser_data->status = PARSING_FAILED;
return ESP_FAIL;
}
parser_data->last.at = 0;
parser_data->last.length = 0;
parser_data->status = PARSING_COMPLETE;
ESP_LOGD(TAG, LOG_FMT("body begins"));
return ESP_OK;
}
/* Last http_parser callback if body absent in HTTP request.
* Will be invoked ONLY once every packet
*/
static esp_err_t cb_no_body(http_parser *parser)
{
parser_data_t *parser_data = (parser_data_t *) parser->data;
const char* at = parser_data->last.at;
/* Check previous status */
if (parser_data->status == PARSING_URL) {
ESP_LOGD(TAG, LOG_FMT("no headers"));
if (verify_url(parser) != ESP_OK) {
parser_data->status = PARSING_FAILED;
return ESP_FAIL;
}
} else if (parser_data->status != PARSING_BODY) {
ESP_LOGE(TAG, LOG_FMT("unexpected state transition"));
parser_data->status = PARSING_FAILED;
return ESP_FAIL;
}
/* Get end of packet */
at += strlen("\r\n\r\n");
/* Pause parsing so that if part of another packet
* is in queue then it doesn't get parsed, which
* may reset the parser state and cause current
* request packet to be lost */
if (pause_parsing(parser, at) != ESP_OK) {
parser_data->status = PARSING_FAILED;
return ESP_FAIL;
}
parser_data->last.at = 0;
parser_data->last.length = 0;
parser_data->status = PARSING_COMPLETE;
ESP_LOGD(TAG, LOG_FMT("message complete"));
return ESP_OK;
}
static int read_block(httpd_req_t *req, size_t offset, size_t length)
{
struct httpd_req_aux *raux = req->aux;
/* Limits the read to scratch buffer size */
size_t buf_len = MIN(length, (sizeof(raux->scratch) - offset));
if (buf_len == 0) {
return 0;
}
/* Receive data into buffer. If data is pending (from unrecv) then return
* immediatly after receiving pending data, as pending data may just complete
* this request packet. */
int nbytes = httpd_recv_with_opt(req, raux->scratch + offset, buf_len, true);
if (nbytes < 0) {
ESP_LOGD(TAG, LOG_FMT("error in httpd_recv"));
/* Connection error. Notify Timeout in all cases.
* Need some way to check errno for ETIMEDOUT. */
httpd_resp_send_err(req, HTTPD_408_REQ_TIMEOUT);
return -1;
} else if (nbytes == 0) {
ESP_LOGD(TAG, LOG_FMT("connection closed"));
return -1;
}
ESP_LOGD(TAG, LOG_FMT("received HTTP request block size = %d"), nbytes);
return nbytes;
}
static int parse_block(http_parser *parser, size_t offset, size_t length)
{
parser_data_t *data = (parser_data_t *)(parser->data);
httpd_req_t *req = data->req;
struct httpd_req_aux *raux = req->aux;
size_t nparsed = 0;
if (!length) {
ESP_LOGW(TAG, LOG_FMT("response uri/header too big"));
switch (data->status) {
case PARSING_URL:
data->error = HTTPD_414_URI_TOO_LONG;
break;
case PARSING_HDR_FIELD:
case PARSING_HDR_VALUE:
data->error = HTTPD_431_REQ_HDR_FIELDS_TOO_LARGE;
default:
break;
}
data->status = PARSING_FAILED;
return -1;
}
/* Unpause the parsing if paused */
if (data->paused) {
nparsed = continue_parsing(parser, length);
length -= nparsed;
offset += nparsed;
if (!length) {
return nparsed;
}
}
/* Execute http_parser */
nparsed = http_parser_execute(parser, &data->settings,
raux->scratch + offset, length);
/* Check state */
if (data->status == PARSING_FAILED) {
ESP_LOGW(TAG, LOG_FMT("parsing failed"));
return -1;
} else if (data->paused) {
/* Keep track of parsed data to be skipped
* during next parsing cycle */
data->pre_parsed -= (length - nparsed);
return 0;
} else if (nparsed != length) {
/* http_parser error */
data->status = PARSING_FAILED;
data->error = HTTPD_400_BAD_REQUEST;
ESP_LOGW(TAG, LOG_FMT("incomplete (%d/%d) with parser error = %d"),
nparsed, length, parser->http_errno);
return -1;
}
/* Continue parsing this section of HTTP request packet */
ESP_LOGD(TAG, LOG_FMT("parsed block size = %d"), offset + nparsed);
return offset + nparsed;
}
static void parse_init(httpd_req_t *r, http_parser *parser, parser_data_t *data)
{
/* Initialize parser data */
memset(data, 0, sizeof(parser_data_t));
data->req = r;
/* Initialize parser */
http_parser_init(parser, HTTP_REQUEST);
parser->data = (void *)data;
/* Initialize parser settings */
http_parser_settings_init(&data->settings);
/* Set parser callbacks */
data->settings.on_url = cb_url;
data->settings.on_header_field = cb_header_field;
data->settings.on_header_value = cb_header_value;
data->settings.on_headers_complete = cb_headers_complete;
data->settings.on_body = cb_on_body;
data->settings.on_message_complete = cb_no_body;
}
/* Function that receives TCP data and runs parser on it
*/
static esp_err_t httpd_parse_req(struct httpd_data *hd)
{
httpd_req_t *r = &hd->hd_req;
int blk_len, offset;
http_parser parser;
parser_data_t parser_data;
/* Initilaize parser */
parse_init(r, &parser, &parser_data);
/* Set offset to start of scratch buffer */
offset = 0;
do {
/* Read block into scratch buffer */
if ((blk_len = read_block(r, offset, PARSER_BLOCK_SIZE)) < 0) {
/* Return error to close socket */
return ESP_FAIL;
}
/* This is used by the callbacks to track
* data usage of the buffer */
parser_data.raw_datalen = blk_len + offset;
/* Parse data block from buffer */
if ((offset = parse_block(&parser, offset, blk_len)) < 0) {
/* Server/Client error. Send error code as response status */
return httpd_resp_send_err(r, parser_data.error);
}
} while (parser_data.status != PARSING_COMPLETE);
ESP_LOGD(TAG, LOG_FMT("parsing complete"));
return httpd_uri(hd);
}
static void init_req(httpd_req_t *r, httpd_config_t *config)
{
r->handle = 0;
r->method = 0;
memset((char*)r->uri, 0, sizeof(r->uri));
r->content_len = 0;
r->aux = 0;
r->user_ctx = 0;
r->sess_ctx = 0;
r->free_ctx = 0;
}
static void init_req_aux(struct httpd_req_aux *ra, httpd_config_t *config)
{
ra->sd = 0;
memset(ra->scratch, 0, sizeof(ra->scratch));
ra->remaining_len = 0;
ra->status = 0;
ra->content_type = 0;
ra->first_chunk_sent = 0;
ra->req_hdrs_count = 0;
ra->resp_hdrs_count = 0;
memset(ra->resp_hdrs, 0, config->max_resp_headers * sizeof(struct resp_hdr));
}
/* Function that processes incoming TCP data and
* updates the http request data httpd_req_t
*/
esp_err_t httpd_req_new(struct httpd_data *hd, struct sock_db *sd)
{
httpd_req_t *r = &hd->hd_req;
init_req(r, &hd->config);
init_req_aux(&hd->hd_req_aux, &hd->config);
r->handle = hd;
r->aux = &hd->hd_req_aux;
/* Associate the request to the socket */
struct httpd_req_aux *ra = r->aux;
ra->sd = sd;
/* Set defaults */
ra->status = (char *)HTTPD_200;
ra->content_type = (char *)HTTPD_TYPE_JSON;
ra->first_chunk_sent = false;
/* Copy session info to the request */
r->sess_ctx = sd->ctx;
r->free_ctx = sd->free_ctx;
/* Parse request */
return httpd_parse_req(hd);
}
/* Function that resets the http request data
*/
esp_err_t httpd_req_delete(struct httpd_data *hd)
{
httpd_req_t *r = &hd->hd_req;
struct httpd_req_aux *ra = r->aux;
/* Finish off reading any pending/leftover data */
while (ra->remaining_len) {
/* Any length small enough not to overload the stack, but large
* enough to finish off the buffers fast
*/
char dummy[32];
int recv_len = MIN(sizeof(dummy) - 1, ra->remaining_len);
int ret = httpd_req_recv(r, dummy, recv_len);
if (ret < 0) {
return ESP_FAIL;
}
dummy[ret] = '\0';
ESP_LOGD(TAG, LOG_FMT("purging data : %s"), dummy);
}
/* Retrieve session info from the request into the socket database */
ra->sd->ctx = r->sess_ctx;
ra->sd->free_ctx = r->free_ctx;
/* Clear out the request and request_aux structures */
ra->sd = NULL;
r->aux = NULL;
return ESP_OK;
}
/* Validates the request to prevent users from calling APIs, that are to
* be called only inside URI handler, outside the handler context
*/
bool httpd_valid_req(httpd_req_t *r)
{
if (r) {
struct httpd_data *hd = (struct httpd_data *) r->handle;
if (hd) {
/* Check if this function is running in the context of
* the correct httpd server thread */
if (httpd_os_thread_handle() == hd->hd_td.handle) {
return true;
}
}
}
return false;
}
/* Helper function to get a URL query tag from a query string of the type param1=val1&param2=val2 */
esp_err_t httpd_query_key_value(const char *qry_str, const char *key, char *val, size_t val_size)
{
if (qry_str == NULL || key == NULL || val == NULL) {
return ESP_ERR_INVALID_ARG;
}
const char *qry_ptr = qry_str;
const size_t buf_len = val_size;
while (strlen(qry_ptr)) {
/* Search for the '=' character. Else, it would mean
* that the parameter is invalid */
const char *val_ptr = strchr(qry_ptr, '=');
if (!val_ptr) {
break;
}
size_t offset = val_ptr - qry_ptr;
/* If the key, does not match, continue searching.
* Compare lengths first as key from url is not
* null terminated (has '=' in the end) */
if ((offset != strlen(key)) ||
(strncmp(qry_ptr, key, offset))) {
/* Get the name=val string. Multiple name=value pairs
* are separated by '&' */
qry_ptr = strchr(val_ptr, '&');
if (!qry_ptr) {
break;
}
qry_ptr++;
continue;
}
/* Locate start of next query */
qry_ptr = strchr(++val_ptr, '&');
/* Or this could be the last query, in which
* case get to the end of query string */
if (!qry_ptr) {
qry_ptr = val_ptr + strlen(val_ptr);
}
/* Update value length, including one byte for null */
val_size = qry_ptr - val_ptr + 1;
/* Copy value to the caller's buffer. */
strlcpy(val, val_ptr, MIN(val_size, buf_len));
/* If buffer length is smaller than needed, return truncation error */
if (buf_len < val_size) {
return ESP_ERR_HTTPD_RESULT_TRUNC;
}
return ESP_OK;
}
ESP_LOGD(TAG, LOG_FMT("key %s not found"), key);
return ESP_ERR_NOT_FOUND;
}
size_t httpd_req_get_url_query_len(httpd_req_t *r)
{
if (r == NULL) {
return 0;
}
if (!httpd_valid_req(r)) {
return 0;
}
struct httpd_req_aux *ra = r->aux;
struct http_parser_url *res = &ra->url_parse_res;
/* Check if query field is present in the URL */
if (res->field_set & (1 << UF_QUERY)) {
return res->field_data[UF_QUERY].len;
}
return 0;
}
esp_err_t httpd_req_get_url_query_str(httpd_req_t *r, char *buf, size_t buf_len)
{
if (r == NULL || buf == NULL) {
return ESP_ERR_INVALID_ARG;
}
if (!httpd_valid_req(r)) {
return ESP_ERR_HTTPD_INVALID_REQ;
}
struct httpd_req_aux *ra = r->aux;
struct http_parser_url *res = &ra->url_parse_res;
/* Check if query field is present in the URL */
if (res->field_set & (1 << UF_QUERY)) {
const char *qry = r->uri + res->field_data[UF_QUERY].off;
/* Minimum required buffer len for keeping
* null terminated query string */
size_t min_buf_len = res->field_data[UF_QUERY].len + 1;
strlcpy(buf, qry, MIN(buf_len, min_buf_len));
if (buf_len < min_buf_len) {
return ESP_ERR_HTTPD_RESULT_TRUNC;
}
return ESP_OK;
}
return ESP_ERR_NOT_FOUND;
}
/* Get the length of the value string of a header request field */
size_t httpd_req_get_hdr_value_len(httpd_req_t *r, const char *field)
{
if (r == NULL || field == NULL) {
return 0;
}
if (!httpd_valid_req(r)) {
return 0;
}
struct httpd_req_aux *ra = r->aux;
const char *hdr_ptr = ra->scratch; /*!< Request headers are kept in scratch buffer */
unsigned count = ra->req_hdrs_count; /*!< Count set during parsing */
while (count--) {
/* Search for the ':' character. Else, it would mean
* that the field is invalid
*/
const char *val_ptr = strchr(hdr_ptr, ':');
if (!val_ptr) {
break;
}
/* If the field, does not match, continue searching.
* Compare lengths first as field from header is not
* null terminated (has ':' in the end).
*/
if ((val_ptr - hdr_ptr != strlen(field)) ||
(strncmp(hdr_ptr, field, strlen(field)))) {
hdr_ptr += strlen(hdr_ptr) + strlen("\r\n");
continue;
}
/* Skip ':' */
val_ptr++;
/* Skip preceding space */
while ((*val_ptr != '\0') && (*val_ptr == ' ')) {
val_ptr++;
}
return strlen(val_ptr);
}
return 0;
}
/* Get the value of a field from the request headers */
esp_err_t httpd_req_get_hdr_value_str(httpd_req_t *r, const char *field, char *val, size_t val_size)
{
if (r == NULL || field == NULL) {
return ESP_ERR_INVALID_ARG;
}
if (!httpd_valid_req(r)) {
return ESP_ERR_HTTPD_INVALID_REQ;
}
struct httpd_req_aux *ra = r->aux;
const char *hdr_ptr = ra->scratch; /*!< Request headers are kept in scratch buffer */
unsigned count = ra->req_hdrs_count; /*!< Count set during parsing */
const size_t buf_len = val_size;
while (count--) {
/* Search for the ':' character. Else, it would mean
* that the field is invalid
*/
const char *val_ptr = strchr(hdr_ptr, ':');
if (!val_ptr) {
break;
}
/* If the field, does not match, continue searching.
* Compare lengths first as field from header is not
* null terminated (has ':' in the end).
*/
if ((val_ptr - hdr_ptr != strlen(field)) ||
(strncmp(hdr_ptr, field, strlen(field)))) {
hdr_ptr += strlen(hdr_ptr) + strlen("\r\n");
continue;
}
/* Skip ':' */
val_ptr++;
/* Skip preceding space */
while ((*val_ptr != '\0') && (*val_ptr == ' ')) {
val_ptr++;
}
/* Get the NULL terminated value and copy it to the caller's buffer. */
strlcpy(val, val_ptr, buf_len);
/* Update value length, including one byte for null */
val_size = strlen(val_ptr) + 1;
/* If buffer length is smaller than needed, return truncation error */
if (buf_len < val_size) {
return ESP_ERR_HTTPD_RESULT_TRUNC;
}
return ESP_OK;
}
return ESP_ERR_NOT_FOUND;
}

View file

@ -0,0 +1,492 @@
// Copyright 2018 Espressif Systems (Shanghai) PTE LTD
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#ifndef _HTTPD_PRIV_H_
#define _HTTPD_PRIV_H_
#include <stdbool.h>
#include <sys/socket.h>
#include <sys/param.h>
#include <netinet/in.h>
#include <esp_log.h>
#include <esp_err.h>
#include <http_server.h>
#include "osal.h"
#ifdef __cplusplus
extern "C" {
#endif
/* Size of request data block/chunk (not to be confused with chunked encoded data)
* that is received and parsed in one turn of the parsing process. This should not
* exceed the scratch buffer size and should atleast be 8 bytes */
#define PARSER_BLOCK_SIZE 128
/* Calculate the maximum size needed for the scratch buffer */
#define HTTPD_SCRATCH_BUF MAX(HTTPD_MAX_REQ_HDR_LEN, HTTPD_MAX_URI_LEN)
/* Formats a log string to prepend context function name */
#define LOG_FMT(x) "%s: " x, __func__
/**
* @brief Thread related data for internal use
*/
struct thread_data {
othread_t handle; /*!< Handle to thread/task */
enum {
THREAD_IDLE = 0,
THREAD_RUNNING,
THREAD_STOPPING,
THREAD_STOPPED,
} status; /*!< State of the thread */
};
/**
* @brief Error codes sent by server in case of errors
* encountered during processing of an HTTP request
*/
typedef enum {
/* For any unexpected errors during parsing, like unexpected
* state transitions, or unhandled errors.
*/
HTTPD_500_SERVER_ERROR = 0,
/* For methods not supported by http_parser. Presently
* http_parser halts parsing when such methods are
* encountered and so the server responds with 400 Bad
* Request error instead.
*/
HTTPD_501_METHOD_NOT_IMPLEMENTED,
/* When HTTP version is not 1.1 */
HTTPD_505_VERSION_NOT_SUPPORTED,
/* Returned when http_parser halts parsing due to incorrect
* syntax of request, unsupported method in request URI or
* due to chunked encoding option present in headers
*/
HTTPD_400_BAD_REQUEST,
/* When requested URI is not found */
HTTPD_404_NOT_FOUND,
/* When URI found, but method has no handler registered */
HTTPD_405_METHOD_NOT_ALLOWED,
/* Intended for recv timeout. Presently it's being sent
* for other recv errors as well. Client should expect the
* server to immediatly close the connection after
* responding with this.
*/
HTTPD_408_REQ_TIMEOUT,
/* Intended for responding to chunked encoding, which is
* not supported currently. Though unhandled http_parser
* callback for chunked request returns "400 Bad Request"
*/
HTTPD_411_LENGTH_REQUIRED,
/* URI length greater than HTTPD_MAX_URI_LEN */
HTTPD_414_URI_TOO_LONG,
/* Headers section larger thn HTTPD_MAX_REQ_HDR_LEN */
HTTPD_431_REQ_HDR_FIELDS_TOO_LARGE,
/* There is no particular HTTP error code for not supporting
* upgrade. For this respond with 200 OK. Client expects status
* code 101 if upgrade were supported, so 200 should be fine.
*/
HTTPD_XXX_UPGRADE_NOT_SUPPORTED
} httpd_err_resp_t;
/**
* @brief A database of all the open sockets in the system.
*/
struct sock_db {
int fd; /*!< The file descriptor for this socket */
void *ctx; /*!< A custom context for this socket */
httpd_handle_t handle; /*!< Server handle */
httpd_free_sess_ctx_fn_t free_ctx; /*!< Function for freeing the context */
httpd_send_func_t send_fn; /*!< Send function for this socket */
httpd_recv_func_t recv_fn; /*!< Send function for this socket */
int64_t timestamp; /*!< Timestamp indicating when the socket was last used */
char pending_data[PARSER_BLOCK_SIZE]; /*!< Buffer for pending data to be received */
size_t pending_len; /*!< Length of pending data to be received */
};
/**
* @brief Auxilary data structure for use during reception and processing
* of requests and temporarily keeping responses
*/
struct httpd_req_aux {
struct sock_db *sd; /*!< Pointer to socket database */
char scratch[HTTPD_SCRATCH_BUF + 1]; /*!< Temporary buffer for our operations (1 byte extra for null termination) */
size_t remaining_len; /*!< Amount of data remaining to be fetched */
char *status; /*!< HTTP response's status code */
char *content_type; /*!< HTTP response's content type */
bool first_chunk_sent; /*!< Used to indicate if first chunk sent */
unsigned req_hdrs_count; /*!< Count of total headers in request packet */
unsigned resp_hdrs_count; /*!< Count of additional headers in response packet */
struct resp_hdr {
const char *field;
const char *value;
} *resp_hdrs; /*!< Additional headers in response packet */
struct http_parser_url url_parse_res; /*!< URL parsing result, used for retrieving URL elements */
};
/**
* @brief Server data for each instance. This is exposed publicaly as
* httpd_handle_t but internal structure/members are kept private.
*/
struct httpd_data {
httpd_config_t config; /*!< HTTPD server configuration */
int listen_fd; /*!< Server listener FD */
int ctrl_fd; /*!< Ctrl message receiver FD */
int msg_fd; /*!< Ctrl message sender FD */
struct thread_data hd_td; /*!< Information for the HTTPd thread */
struct sock_db *hd_sd; /*!< The socket database */
httpd_uri_t **hd_calls; /*!< Registered URI handlers */
struct httpd_req hd_req; /*!< The current HTTPD request */
struct httpd_req_aux hd_req_aux; /*!< Additional data about the HTTPD request kept unexposed */
};
/******************* Group : Session Management ********************/
/** @name Session Management
* Functions related to HTTP session management
* @{
*/
/**
* @brief Initializes an http session by resetting the sockets database.
*
* @param[in] hd Server instance data
*/
void httpd_sess_init(struct httpd_data *hd);
/**
* @brief Starts a new session for client requesting connection and adds
* it's descriptor to the socket database.
*
* @param[in] hd Server instance data
* @param[in] newfd Descriptor of the new client to be added to the session.
*
* @return
* - ESP_OK : on successfully queueing the work
* - ESP_FAIL : in case of control socket error while sending
*/
esp_err_t httpd_sess_new(struct httpd_data *hd, int newfd);
/**
* @brief Processes incoming HTTP requests
*
* @param[in] hd Server instance data
* @param[in] clifd Descriptor of the client from which data is to be received
*
* @return
* - ESP_OK : on successfully receiving, parsing and responding to a request
* - ESP_FAIL : in case of failure in any of the stages of processing
*/
esp_err_t httpd_sess_process(struct httpd_data *hd, int clifd);
/**
* @brief Remove client descriptor from the session / socket database
* and close the connection for this client.
*
* @note The returned descriptor should be used by httpd_sess_iterate()
* to continue the iteration correctly. This ensurs that the
* iteration is not restarted abruptly which may cause reading from
* a socket which has been already processed and thus blocking
* the server loop until data appears on that socket.
*
* @param[in] hd Server instance data
* @param[in] clifd Descriptor of the client to be removed from the session.
*
* @return
* - +VE : Client descriptor preceding the one being deleted
* - -1 : No descriptor preceding the one being deleted
*/
int httpd_sess_delete(struct httpd_data *hd, int clifd);
/**
* @brief Add descriptors present in the socket database to an fd_set and
* update the value of maxfd which are needed by the select function
* for looking through all available sockets for incoming data.
*
* @param[in] hd Server instance data
* @param[out] fdset File descriptor set to be updated.
* @param[out] maxfd Maximum value among all file descriptors.
*/
void httpd_sess_set_descriptors(struct httpd_data *hd, fd_set *fdset, int *maxfd);
/**
* @brief Iterates through the list of client fds in the session /socket database.
* Passing the value of a client fd returns the fd for the next client
* in the database. In order to iterate from the beginning pass -1 as fd.
*
* @param[in] hd Server instance data
* @param[in] fd Last accessed client descriptor.
* -1 to reset iterator to start of database.
*
* @return
* - +VE : Client descriptor next in the database
* - -1 : End of iteration
*/
int httpd_sess_iterate(struct httpd_data *hd, int fd);
/**
* @brief Checks if session can accept another connection from new client.
* If sockets database is full then this returns false.
*
* @param[in] hd Server instance data
*
* @return True if session can accept new clients
*/
bool httpd_is_sess_available(struct httpd_data *hd);
/**
* @brief Checks if session has any pending data/packets
* for processing
*
* This is needed as httpd_unrecv may unreceive next
* packet in the stream. If only partial packet was
* received then select() would mark the fd for processing
* as remaining part of the packet would still be in socket
* recv queue. But if a complete packet got unreceived
* then it would not be processed until furtur data is
* received on the socket. This is when this function
* comes in use, as it checks the socket's pending data
* buffer.
*
* @param[in] hd Server instance data
* @param[in] fd Client descriptor
*
* @return True if there is any pending data
*/
bool httpd_sess_pending(struct httpd_data *hd, int fd);
/**
* @brief Removes the least recently used client from the session
*
* This may be useful if new clients are requesting for connection but
* max number of connections is reached, in which case the client which
* is inactive for the longest will be removed from the session.
*
* @param[in] hd Server instance data
*
* @return
* - ESP_OK : if session closure initiated successfully
* - ESP_FAIL : if failed
*/
esp_err_t httpd_sess_close_lru(struct httpd_data *hd);
/** End of Group : Session Management
* @}
*/
/****************** Group : URI Handling ********************/
/** @name URI Handling
* Methods for accessing URI handlers
* @{
*/
/**
* @brief For an HTTP request, searches through all the registered URI handlers
* and invokes the appropriate one if found
*
* @param[in] hd Server instance data for which handler needs to be invoked
*
* @return
* - ESP_OK : if handler found and executed successfully
* - ESP_FAIL : otherwise
*/
esp_err_t httpd_uri(struct httpd_data *hd);
/**
* @brief Deregister all URI handlers
*
* @param[in] hd Server instance data
*/
void httpd_unregister_all_uri_handlers(struct httpd_data *hd);
/**
* @brief Validates the request to prevent users from calling APIs, that are to
* be called only inside a URI handler, outside the handler context
*
* @param[in] req Pointer to HTTP request that neds to be validated
*
* @return
* - true : if valid request
* - false : otherwise
*/
bool httpd_valid_req(httpd_req_t *r);
/** End of Group : URI Handling
* @}
*/
/****************** Group : Processing ********************/
/** @name Processing
* Methods for processing HTTP requests
* @{
*/
/**
* @brief Initiates the processing of HTTP request
*
* Receives incoming TCP packet on a socket, then parses the packet as
* HTTP request and fills httpd_req_t data structure with the extracted
* URI, headers are ready to be fetched from scratch buffer and calling
* http_recv() after this reads the body of the request.
*
* @param[in] hd Server instance data
* @param[in] sd Pointer to socket which is needed for receiving TCP packets.
*
* @return
* - ESP_OK : if request packet is valid
* - ESP_FAIL : otherwise
*/
esp_err_t httpd_req_new(struct httpd_data *hd, struct sock_db *sd);
/**
* @brief For an HTTP request, resets the resources allocated for it and
* purges any data left to be received
*
* @param[in] hd Server instance data
*
* @return
* - ESP_OK : if request packet deleted and resources cleaned.
* - ESP_FAIL : otherwise.
*/
esp_err_t httpd_req_delete(struct httpd_data *hd);
/** End of Group : Parsing
* @}
*/
/****************** Group : Send/Receive ********************/
/** @name Send and Receive
* Methods for transmitting and receiving HTTP requests and responses
* @{
*/
/**
* @brief For sending out error code in response to HTTP request.
*
* @param[in] req Pointer to the HTTP request for which the resonse needs to be sent
* @param[in] error Error type to send
*
* @return
* - ESP_OK : if successful
* - ESP_FAIL : if failed
*/
esp_err_t httpd_resp_send_err(httpd_req_t *req, httpd_err_resp_t error);
/**
* @brief For sending out data in response to an HTTP request.
*
* @param[in] req Pointer to the HTTP request for which the resonse needs to be sent
* @param[in] buf Pointer to the buffer from where the body of the response is taken
* @param[in] buf_len Length of the buffer
*
* @return
* - Length of data : if successful
* - ESP_FAIL : if failed
*/
int httpd_send(httpd_req_t *req, const char *buf, size_t buf_len);
/**
* @brief For receiving HTTP request data
*
* @note The exposed API httpd_recv() is simply this function with last parameter
* set as false. This function is used internally during reception and
* processing of a new request. The option to halt after receiving pending
* data prevents the server from requesting more data than is needed for
* completing a packet in case when all the remaining part of the packet is
* in the pending buffer.
*
* @param[in] req Pointer to new HTTP request which only has the socket descriptor
* @param[out] buf Pointer to the buffer which will be filled with the received data
* @param[in] buf_len Length of the buffer
* @param[in] halt_after_pending When set true, halts immediatly after receiving from
* pending buffer
*
* @return
* - Length of data : if successful
* - ESP_FAIL : if failed
*/
int httpd_recv_with_opt(httpd_req_t *r, char *buf, size_t buf_len, bool halt_after_pending);
/**
* @brief For un-receiving HTTP request data
*
* This function copies data into internal buffer pending_data so that
* when httpd_recv is called, it first fetches this pending data and
* then only starts receiving from the socket
*
* @note If data is too large for the internal buffer then only
* part of the data is unreceived, reflected in the returned
* length. Make sure that such truncation is checked for and
* handled properly.
*
* @param[in] req Pointer to new HTTP request which only has the socket descriptor
* @param[in] buf Pointer to the buffer from where data needs to be un-received
* @param[in] buf_len Length of the buffer
*
* @return Length of data copied into pending buffer
*/
size_t httpd_unrecv(struct httpd_req *r, const char *buf, size_t buf_len);
/**
* @brief This is the low level default send function of the HTTPD. This should
* NEVER be called directly. The semantics of this is exactly similar to
* send() of the BSD socket API.
*
* @param[in] sockfd Socket descriptor for sending data
* @param[in] buf Pointer to the buffer from where the body of the response is taken
* @param[in] buf_len Length of the buffer
* @param[in] flags Flags for mode selection
*
* @return
* - Length of data : if successful
* - -1 : if failed (appropriate errno is set)
*/
int httpd_default_send(int sockfd, const char *buf, size_t buf_len, int flags);
/**
* @brief This is the low level default recv function of the HTTPD. This should
* NEVER be called directly. The semantics of this is exactly similar to
* recv() of the BSD socket API.
*
* @param[in] sockfd Socket descriptor for sending data
* @param[out] buf Pointer to the buffer which will be filled with the received data
* @param[in] buf_len Length of the buffer
* @param[in] flags Flags for mode selection
*
* @return
* - Length of data : if successful
* - -1 : if failed (appropriate errno is set)
*/
int httpd_default_recv(int sockfd, char *buf, size_t buf_len, int flags);
/** End of Group : Send and Receive
* @}
*/
#ifdef __cplusplus
}
#endif
#endif /* ! _HTTPD_PRIV_H_ */

View file

@ -0,0 +1,259 @@
// Copyright 2018 Espressif Systems (Shanghai) PTE LTD
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#include <stdlib.h>
#include <esp_log.h>
#include <esp_err.h>
#include <http_server.h>
#include "httpd_priv.h"
static const char *TAG = "httpd_sess";
bool httpd_is_sess_available(struct httpd_data *hd)
{
int i;
for (i = 0; i < hd->config.max_open_sockets; i++) {
if (hd->hd_sd[i].fd == -1) {
return true;
}
}
return false;
}
static struct sock_db *httpd_sess_get(struct httpd_data *hd, int newfd)
{
int i;
for (i = 0; i < hd->config.max_open_sockets; i++) {
if (hd->hd_sd[i].fd == newfd) {
return &hd->hd_sd[i];
}
}
return NULL;
}
esp_err_t httpd_sess_new(struct httpd_data *hd, int newfd)
{
ESP_LOGD(TAG, LOG_FMT("fd = %d"), newfd);
if (httpd_sess_get(hd, newfd)) {
ESP_LOGE(TAG, LOG_FMT("session already exists with fd = %d"), newfd);
return ESP_FAIL;
}
int i;
for (i = 0; i < hd->config.max_open_sockets; i++) {
if (hd->hd_sd[i].fd == -1) {
memset(&hd->hd_sd[i], 0, sizeof(hd->hd_sd[i]));
hd->hd_sd[i].fd = newfd;
hd->hd_sd[i].handle = (httpd_handle_t) hd;
hd->hd_sd[i].send_fn = httpd_default_send;
hd->hd_sd[i].recv_fn = httpd_default_recv;
return ESP_OK;
}
}
ESP_LOGD(TAG, LOG_FMT("unable to launch session for fd = %d"), newfd);
return ESP_FAIL;
}
void *httpd_sess_get_ctx(httpd_handle_t handle, int sockfd)
{
if (handle == NULL) {
return NULL;
}
struct httpd_data *hd = (struct httpd_data *) handle;
struct sock_db *sd = httpd_sess_get(hd, sockfd);
if (sd == NULL) {
return NULL;
}
return sd->ctx;
}
void httpd_sess_set_descriptors(struct httpd_data *hd,
fd_set *fdset, int *maxfd)
{
int i;
*maxfd = -1;
for (i = 0; i < hd->config.max_open_sockets; i++) {
if (hd->hd_sd[i].fd != -1) {
FD_SET(hd->hd_sd[i].fd, fdset);
if (hd->hd_sd[i].fd > *maxfd) {
*maxfd = hd->hd_sd[i].fd;
}
}
}
}
int httpd_sess_delete(struct httpd_data *hd, int fd)
{
ESP_LOGD(TAG, LOG_FMT("fd = %d"), fd);
int i;
int pre_sess_fd = -1;
for (i = 0; i < hd->config.max_open_sockets; i++) {
if (hd->hd_sd[i].fd == fd) {
hd->hd_sd[i].fd = -1;
if (hd->hd_sd[i].ctx) {
if (hd->hd_sd[i].free_ctx) {
hd->hd_sd[i].free_ctx(hd->hd_sd[i].ctx);
} else {
free(hd->hd_sd[i].ctx);
}
hd->hd_sd[i].ctx = NULL;
hd->hd_sd[i].free_ctx = NULL;
}
break;
} else if (hd->hd_sd[i].fd != -1) {
/* Return the fd just preceding the one being
* deleted so that iterator can continue from
* the correct fd */
pre_sess_fd = hd->hd_sd[i].fd;
}
}
return pre_sess_fd;
}
void httpd_sess_init(struct httpd_data *hd)
{
int i;
for (i = 0; i < hd->config.max_open_sockets; i++) {
hd->hd_sd[i].fd = -1;
hd->hd_sd[i].ctx = NULL;
}
}
bool httpd_sess_pending(struct httpd_data *hd, int fd)
{
struct sock_db *sd = httpd_sess_get(hd, fd);
if (! sd) {
return ESP_FAIL;
}
return (sd->pending_len != 0);
}
/* This MUST return ESP_OK on successful execution. If any other
* value is returned, everything related to this socket will be
* cleaned up and the socket will be closed.
*/
esp_err_t httpd_sess_process(struct httpd_data *hd, int newfd)
{
struct sock_db *sd = httpd_sess_get(hd, newfd);
if (! sd) {
return ESP_FAIL;
}
ESP_LOGD(TAG, LOG_FMT("httpd_req_new"));
if (httpd_req_new(hd, sd) != ESP_OK) {
return ESP_FAIL;
}
ESP_LOGD(TAG, LOG_FMT("httpd_req_delete"));
if (httpd_req_delete(hd) != ESP_OK) {
return ESP_FAIL;
}
ESP_LOGD(TAG, LOG_FMT("success"));
sd->timestamp = httpd_os_get_timestamp();
return ESP_OK;
}
esp_err_t httpd_sess_update_timestamp(httpd_handle_t handle, int sockfd)
{
if (handle == NULL) {
return ESP_ERR_INVALID_ARG;
}
/* Search for the socket database entry */
struct httpd_data *hd = (struct httpd_data *) handle;
int i;
for (i = 0; i < hd->config.max_open_sockets; i++) {
if (hd->hd_sd[i].fd == sockfd) {
hd->hd_sd[i].timestamp = httpd_os_get_timestamp();
return ESP_OK;
}
}
return ESP_ERR_NOT_FOUND;
}
esp_err_t httpd_sess_close_lru(struct httpd_data *hd)
{
int64_t timestamp = INT64_MAX;
int lru_fd = -1;
int i;
for (i = 0; i < hd->config.max_open_sockets; i++) {
/* If a descriptor is -1, there is no need to close any session.
* So, we can return from here, without finding the Least Recently Used
* session
*/
if (hd->hd_sd[i].fd == -1) {
return ESP_OK;
}
if (hd->hd_sd[i].timestamp < timestamp) {
timestamp = hd->hd_sd[i].timestamp;
lru_fd = hd->hd_sd[i].fd;
}
}
ESP_LOGD(TAG, LOG_FMT("fd = %d"), lru_fd);
return httpd_trigger_sess_close(hd, lru_fd);
}
int httpd_sess_iterate(struct httpd_data *hd, int start_fd)
{
int start_index = 0;
int i;
if (start_fd != -1) {
/* Take our index to where this fd is stored */
for (i = 0; i < hd->config.max_open_sockets; i++) {
if (hd->hd_sd[i].fd == start_fd) {
start_index = i + 1;
break;
}
}
}
for (i = start_index; i < hd->config.max_open_sockets; i++) {
if (hd->hd_sd[i].fd != -1) {
return hd->hd_sd[i].fd;
}
}
return -1;
}
static void httpd_sess_close(void *arg)
{
struct sock_db *sock_db = (struct sock_db *)arg;
if (sock_db) {
int fd = sock_db->fd;
struct httpd_data *hd = (struct httpd_data *) sock_db->handle;
httpd_sess_delete(hd, fd);
close(fd);
}
}
esp_err_t httpd_trigger_sess_close(httpd_handle_t handle, int sockfd)
{
if (handle == NULL) {
return ESP_ERR_INVALID_ARG;
}
struct httpd_data *hd = (struct httpd_data *) handle;
struct sock_db *sock_db = httpd_sess_get(hd, sockfd);
if (sock_db) {
return httpd_queue_work(handle, httpd_sess_close, sock_db);
}
return ESP_ERR_NOT_FOUND;
}

View file

@ -0,0 +1,500 @@
// Copyright 2018 Espressif Systems (Shanghai) PTE LTD
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#include <errno.h>
#include <esp_log.h>
#include <esp_err.h>
#include <http_server.h>
#include "httpd_priv.h"
static const char *TAG = "httpd_txrx";
esp_err_t httpd_set_send_override(httpd_req_t *r, httpd_send_func_t send_func)
{
if (r == NULL || send_func == NULL) {
return ESP_ERR_INVALID_ARG;
}
if (!httpd_valid_req(r)) {
return ESP_ERR_HTTPD_INVALID_REQ;
}
struct httpd_req_aux *ra = r->aux;
ra->sd->send_fn = send_func;
return ESP_OK;
}
esp_err_t httpd_set_recv_override(httpd_req_t *r, httpd_recv_func_t recv_func)
{
if (r == NULL || recv_func == NULL) {
return ESP_ERR_INVALID_ARG;
}
if (!httpd_valid_req(r)) {
return ESP_ERR_HTTPD_INVALID_REQ;
}
struct httpd_req_aux *ra = r->aux;
ra->sd->recv_fn = recv_func;
return ESP_OK;
}
int httpd_send(httpd_req_t *r, const char *buf, size_t buf_len)
{
if (r == NULL || buf == NULL) {
return -1;
}
if (!httpd_valid_req(r)) {
return -1;
}
struct httpd_req_aux *ra = r->aux;
int ret = ra->sd->send_fn(ra->sd->fd, buf, buf_len, 0);
if (ret < 0) {
ESP_LOGD(TAG, LOG_FMT("error in send_fn"));
return -1;
}
return ret;
}
static esp_err_t httpd_send_all(httpd_req_t *r, const char *buf, size_t buf_len)
{
struct httpd_req_aux *ra = r->aux;
int ret;
while (buf_len > 0) {
ret = ra->sd->send_fn(ra->sd->fd, buf, buf_len, 0);
if (ret < 0) {
ESP_LOGD(TAG, LOG_FMT("error in send_fn"));
return ESP_FAIL;
}
ESP_LOGD(TAG, LOG_FMT("sent = %d"), ret);
buf += ret;
buf_len -= ret;
}
return ESP_OK;
}
static size_t httpd_recv_pending(httpd_req_t *r, char *buf, size_t buf_len)
{
struct httpd_req_aux *ra = r->aux;
size_t offset = sizeof(ra->sd->pending_data) - ra->sd->pending_len;
/* buf_len must not be greater than remaining_len */
buf_len = MIN(ra->sd->pending_len, buf_len);
memcpy(buf, ra->sd->pending_data + offset, buf_len);
ra->sd->pending_len -= buf_len;
return buf_len;
}
int httpd_recv_with_opt(httpd_req_t *r, char *buf, size_t buf_len, bool halt_after_pending)
{
ESP_LOGD(TAG, LOG_FMT("requested length = %d"), buf_len);
size_t pending_len = 0;
struct httpd_req_aux *ra = r->aux;
/* First fetch pending data from local buffer */
if (ra->sd->pending_len > 0) {
ESP_LOGD(TAG, LOG_FMT("pending length = %d"), ra->sd->pending_len);
pending_len = httpd_recv_pending(r, buf, buf_len);
buf += pending_len;
buf_len -= pending_len;
/* If buffer filled then no need to recv.
* If asked to halt after receiving pending data then
* return with received length */
if (!buf_len || halt_after_pending) {
return pending_len;
}
}
/* Receive data of remaining length */
int ret = ra->sd->recv_fn(ra->sd->fd, buf, buf_len, 0);
if (ret < 0) {
ESP_LOGD(TAG, LOG_FMT("error in recv_fn"));
return -1;
}
ESP_LOGD(TAG, LOG_FMT("received length = %d"), ret + pending_len);
return ret + pending_len;
}
int httpd_recv(httpd_req_t *r, char *buf, size_t buf_len)
{
return httpd_recv_with_opt(r, buf, buf_len, false);
}
size_t httpd_unrecv(struct httpd_req *r, const char *buf, size_t buf_len)
{
struct httpd_req_aux *ra = r->aux;
/* Truncate if external buf_len is greater than pending_data buffer size */
ra->sd->pending_len = MIN(sizeof(ra->sd->pending_data), buf_len);
/* Copy data into internal pending_data buffer */
size_t offset = sizeof(ra->sd->pending_data) - ra->sd->pending_len;
memcpy(ra->sd->pending_data + offset, buf, buf_len);
ESP_LOGD(TAG, LOG_FMT("length = %d"), ra->sd->pending_len);
return ra->sd->pending_len;
}
/**
* This API appends an additional header field-value pair in the HTTP response.
* But the header isn't sent out until any of the send APIs is executed.
*/
esp_err_t httpd_resp_set_hdr(httpd_req_t *r, const char *field, const char *value)
{
if (r == NULL || field == NULL || value == NULL) {
return ESP_ERR_INVALID_ARG;
}
if (!httpd_valid_req(r)) {
return ESP_ERR_HTTPD_INVALID_REQ;
}
struct httpd_req_aux *ra = r->aux;
struct httpd_data *hd = (struct httpd_data *) r->handle;
/* Number of additional headers is limited */
if (ra->resp_hdrs_count >= hd->config.max_resp_headers) {
return ESP_ERR_HTTPD_RESP_HDR;
}
/* Assign header field-value pair */
ra->resp_hdrs[ra->resp_hdrs_count].field = field;
ra->resp_hdrs[ra->resp_hdrs_count].value = value;
ra->resp_hdrs_count++;
ESP_LOGD(TAG, LOG_FMT("new header = %s: %s"), field, value);
return ESP_OK;
}
/**
* This API sets the status of the HTTP response to the value specified.
* But the status isn't sent out until any of the send APIs is executed.
*/
esp_err_t httpd_resp_set_status(httpd_req_t *r, const char *status)
{
if (r == NULL || status == NULL) {
return ESP_ERR_INVALID_ARG;
}
if (!httpd_valid_req(r)) {
return ESP_ERR_HTTPD_INVALID_REQ;
}
struct httpd_req_aux *ra = r->aux;
ra->status = (char *)status;
return ESP_OK;
}
/**
* This API sets the method/type of the HTTP response to the value specified.
* But the method isn't sent out until any of the send APIs is executed.
*/
esp_err_t httpd_resp_set_type(httpd_req_t *r, const char *type)
{
if (r == NULL || type == NULL) {
return ESP_ERR_INVALID_ARG;
}
if (!httpd_valid_req(r)) {
return ESP_ERR_HTTPD_INVALID_REQ;
}
struct httpd_req_aux *ra = r->aux;
ra->content_type = (char *)type;
return ESP_OK;
}
esp_err_t httpd_resp_send(httpd_req_t *r, const char *buf, size_t buf_len)
{
if (r == NULL) {
return ESP_ERR_INVALID_ARG;
}
if (!httpd_valid_req(r)) {
return ESP_ERR_HTTPD_INVALID_REQ;
}
struct httpd_req_aux *ra = r->aux;
const char *httpd_hdr_str = "HTTP/1.1 %s\r\nContent-Type: %s\r\nContent-Length: %d\r\n";
const char *colon_separator = ": ";
const char *cr_lf_seperator = "\r\n";
/* Request headers are no longer available */
ra->req_hdrs_count = 0;
/* Size of essential headers is limited by scratch buffer size */
if (snprintf(ra->scratch, sizeof(ra->scratch), httpd_hdr_str,
ra->status, ra->content_type, buf_len) >= sizeof(ra->scratch)) {
return ESP_ERR_HTTPD_RESP_HDR;
}
/* Sending essential headers */
if (httpd_send_all(r, ra->scratch, strlen(ra->scratch)) != ESP_OK) {
return ESP_ERR_HTTPD_RESP_SEND;
}
/* Sending additional headers based on set_header */
for (unsigned i = 0; i < ra->resp_hdrs_count; i++) {
/* Send header field */
if (httpd_send_all(r, ra->resp_hdrs[i].field, strlen(ra->resp_hdrs[i].field)) != ESP_OK) {
return ESP_ERR_HTTPD_RESP_SEND;
}
/* Send ': ' */
if (httpd_send_all(r, colon_separator, strlen(colon_separator)) != ESP_OK) {
return ESP_ERR_HTTPD_RESP_SEND;
}
/* Send header value */
if (httpd_send_all(r, ra->resp_hdrs[i].value, strlen(ra->resp_hdrs[i].value)) != ESP_OK) {
return ESP_ERR_HTTPD_RESP_SEND;
}
/* Send CR + LF */
if (httpd_send_all(r, cr_lf_seperator, strlen(cr_lf_seperator)) != ESP_OK) {
return ESP_ERR_HTTPD_RESP_SEND;
}
}
/* End header section */
if (httpd_send_all(r, cr_lf_seperator, strlen(cr_lf_seperator)) != ESP_OK) {
return ESP_ERR_HTTPD_RESP_SEND;
}
/* Sending content */
if (buf && buf_len) {
if (httpd_send_all(r, buf, buf_len) != ESP_OK) {
return ESP_ERR_HTTPD_RESP_SEND;
}
}
return ESP_OK;
}
esp_err_t httpd_resp_send_chunk(httpd_req_t *r, const char *buf, size_t buf_len)
{
if (r == NULL) {
return ESP_ERR_INVALID_ARG;
}
if (!httpd_valid_req(r)) {
return ESP_ERR_HTTPD_INVALID_REQ;
}
struct httpd_req_aux *ra = r->aux;
const char *httpd_chunked_hdr_str = "HTTP/1.1 %s\r\nContent-Type: %s\r\nContent-Length: 0\r\nTransfer-Encoding: chunked\r\n";
const char *colon_separator = ": ";
const char *cr_lf_seperator = "\r\n";
/* Request headers are no longer available */
ra->req_hdrs_count = 0;
if (!ra->first_chunk_sent) {
/* Size of essential headers is limited by scratch buffer size */
if (snprintf(ra->scratch, sizeof(ra->scratch), httpd_chunked_hdr_str,
ra->status, ra->content_type) >= sizeof(ra->scratch)) {
return ESP_ERR_HTTPD_RESP_HDR;
}
/* Sending essential headers */
if (httpd_send_all(r, ra->scratch, strlen(ra->scratch)) != ESP_OK) {
return ESP_ERR_HTTPD_RESP_SEND;
}
/* Sending additional headers based on set_header */
for (unsigned i = 0; i < ra->resp_hdrs_count; i++) {
/* Send header field */
if (httpd_send_all(r, ra->resp_hdrs[i].field, strlen(ra->resp_hdrs[i].field)) != ESP_OK) {
return ESP_ERR_HTTPD_RESP_SEND;
}
/* Send ': ' */
if (httpd_send_all(r, colon_separator, strlen(colon_separator)) != ESP_OK) {
return ESP_ERR_HTTPD_RESP_SEND;
}
/* Send header value */
if (httpd_send_all(r, ra->resp_hdrs[i].value, strlen(ra->resp_hdrs[i].value)) != ESP_OK) {
return ESP_ERR_HTTPD_RESP_SEND;
}
/* Send CR + LF */
if (httpd_send_all(r, cr_lf_seperator, strlen(cr_lf_seperator)) != ESP_OK) {
return ESP_ERR_HTTPD_RESP_SEND;
}
}
/* End header section */
if (httpd_send_all(r, cr_lf_seperator, strlen(cr_lf_seperator)) != ESP_OK) {
return ESP_ERR_HTTPD_RESP_SEND;
}
ra->first_chunk_sent = true;
}
/* Sending chunked content */
char len_str[10];
snprintf(len_str, sizeof(len_str), "%x\r\n", buf_len);
if (httpd_send_all(r, len_str, strlen(len_str)) != ESP_OK) {
return ESP_ERR_HTTPD_RESP_SEND;
}
if (buf) {
if (httpd_send_all(r, buf, buf_len) != ESP_OK) {
return ESP_ERR_HTTPD_RESP_SEND;
}
}
/* Indicate end of chunk */
if (httpd_send_all(r, "\r\n", strlen("\r\n")) != ESP_OK) {
return ESP_ERR_HTTPD_RESP_SEND;
}
return ESP_OK;
}
esp_err_t httpd_resp_send_404(httpd_req_t *r)
{
return httpd_resp_send_err(r, HTTPD_404_NOT_FOUND);
}
esp_err_t httpd_resp_send_err(httpd_req_t *req, httpd_err_resp_t error)
{
const char *msg;
const char *status;
switch (error) {
case HTTPD_501_METHOD_NOT_IMPLEMENTED:
status = "501 Method Not Implemented";
msg = "Request method is not supported by server";
break;
case HTTPD_505_VERSION_NOT_SUPPORTED:
status = "505 Version Not Supported";
msg = "HTTP version not supported by server";
break;
case HTTPD_400_BAD_REQUEST:
status = "400 Bad Request";
msg = "Server unable to understand request due to invalid syntax";
break;
case HTTPD_404_NOT_FOUND:
status = "404 Not Found";
msg = "This URI doesn't exist";
break;
case HTTPD_405_METHOD_NOT_ALLOWED:
status = "405 Method Not Allowed";
msg = "Request method for this URI is not handled by server";
break;
case HTTPD_408_REQ_TIMEOUT:
status = "408 Request Timeout";
msg = "Server closed this connection";
break;
case HTTPD_414_URI_TOO_LONG:
status = "414 URI Too Long";
msg = "URI is too long for server to interpret";
break;
case HTTPD_411_LENGTH_REQUIRED:
status = "411 Length Required";
msg = "Chunked encoding not supported by server";
break;
case HTTPD_431_REQ_HDR_FIELDS_TOO_LARGE:
status = "431 Request Header Fields Too Large";
msg = "Header fields are too long for server to interpret";
break;
case HTTPD_XXX_UPGRADE_NOT_SUPPORTED:
/* If the server does not support upgrade, or is unable to upgrade
* it responds with a standard HTTP/1.1 response */
status = "200 OK";
msg = "Upgrade not supported by server";
break;
case HTTPD_500_SERVER_ERROR:
default:
status = "500 Server Error";
msg = "Server has encountered an unexpected error";
}
ESP_LOGW(TAG, LOG_FMT("%s - %s"), status, msg);
httpd_resp_set_status (req, status);
httpd_resp_set_type (req, HTTPD_TYPE_TEXT);
return httpd_resp_send (req, msg, strlen(msg));
}
int httpd_req_recv(httpd_req_t *r, char *buf, size_t buf_len)
{
if (r == NULL || buf == NULL) {
return -1;
}
if (!httpd_valid_req(r)) {
ESP_LOGW(TAG, LOG_FMT("invalid request"));
return -1;
}
struct httpd_req_aux *ra = r->aux;
ESP_LOGD(TAG, LOG_FMT("remaining length = %d"), ra->remaining_len);
if (buf_len > ra->remaining_len) {
buf_len = ra->remaining_len;
}
if (buf_len == 0) {
return buf_len;
}
int ret = httpd_recv(r, buf, buf_len);
if (ret < 0) {
ESP_LOGD(TAG, LOG_FMT("error in httpd_recv"));
ra->remaining_len = 0;
return -1;
}
ra->remaining_len -= ret;
ESP_LOGD(TAG, LOG_FMT("received length = %d"), ret);
return ret;
}
int httpd_req_to_sockfd(httpd_req_t *r)
{
if (r == NULL) {
return -1;
}
if (!httpd_valid_req(r)) {
ESP_LOGW(TAG, LOG_FMT("invalid request"));
return -1;
}
struct httpd_req_aux *ra = r->aux;
return ra->sd->fd;
}
int httpd_default_send(int sockfd, const char *buf, size_t buf_len, int flags)
{
if (buf == NULL) {
return ESP_ERR_INVALID_ARG;
}
int ret = send(sockfd, buf, buf_len, flags);
if (ret < 0) {
ESP_LOGW(TAG, LOG_FMT("error in send = %d"), errno);
}
return ret;
}
int httpd_default_recv(int sockfd, char *buf, size_t buf_len, int flags)
{
if (buf == NULL) {
return ESP_ERR_INVALID_ARG;
}
int ret = recv(sockfd, buf, buf_len, flags);
if (ret < 0) {
ESP_LOGW(TAG, LOG_FMT("error in recv = %d"), errno);
}
return ret;
}

View file

@ -0,0 +1,223 @@
// Copyright 2018 Espressif Systems (Shanghai) PTE LTD
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#include <errno.h>
#include <esp_log.h>
#include <esp_err.h>
#include <http_parser.h>
#include <http_server.h>
#include "httpd_priv.h"
static const char *TAG = "httpd_uri";
static int httpd_find_uri_handler(struct httpd_data *hd,
const char* uri,
httpd_method_t method)
{
for (int i = 0; i < hd->config.max_uri_handlers; i++) {
if (hd->hd_calls[i]) {
ESP_LOGD(TAG, LOG_FMT("[%d] = %s"), i, hd->hd_calls[i]->uri);
if ((hd->hd_calls[i]->method == method) && // First match methods
(strcmp(hd->hd_calls[i]->uri, uri) == 0)) { // Then match uri strings
return i;
}
}
}
return -1;
}
esp_err_t httpd_register_uri_handler(httpd_handle_t handle,
const httpd_uri_t *uri_handler)
{
if (handle == NULL || uri_handler == NULL) {
return ESP_ERR_INVALID_ARG;
}
struct httpd_data *hd = (struct httpd_data *) handle;
/* Make sure another handler with same URI and method
* is not already registered
*/
if (httpd_find_uri_handler(handle, uri_handler->uri,
uri_handler->method) != -1) {
ESP_LOGW(TAG, LOG_FMT("handler %s with method %d already registered"),
uri_handler->uri, uri_handler->method);
return ESP_ERR_HTTPD_HANDLER_EXISTS;
}
for (int i = 0; i < hd->config.max_uri_handlers; i++) {
if (hd->hd_calls[i] == NULL) {
hd->hd_calls[i] = malloc(sizeof(httpd_uri_t));
if (hd->hd_calls[i] == NULL) {
/* Failed to allocate memory */
return ESP_ERR_HTTPD_ALLOC_MEM;
}
/* Copy URI string */
hd->hd_calls[i]->uri = strdup(uri_handler->uri);
if (hd->hd_calls[i]->uri == NULL) {
/* Failed to allocate memory */
free(hd->hd_calls[i]);
return ESP_ERR_HTTPD_ALLOC_MEM;
}
/* Copy remaining members */
hd->hd_calls[i]->method = uri_handler->method;
hd->hd_calls[i]->handler = uri_handler->handler;
hd->hd_calls[i]->user_ctx = uri_handler->user_ctx;
ESP_LOGD(TAG, LOG_FMT("[%d] installed %s"), i, uri_handler->uri);
return ESP_OK;
}
ESP_LOGD(TAG, LOG_FMT("[%d] exists %s"), i, hd->hd_calls[i]->uri);
}
ESP_LOGW(TAG, LOG_FMT("no slots left for registering handler"));
return ESP_ERR_HTTPD_HANDLERS_FULL;
}
esp_err_t httpd_unregister_uri_handler(httpd_handle_t handle,
const char *uri, httpd_method_t method)
{
if (handle == NULL || uri == NULL) {
return ESP_ERR_INVALID_ARG;
}
struct httpd_data *hd = (struct httpd_data *) handle;
int i = httpd_find_uri_handler(hd, uri, method);
if (i != -1) {
ESP_LOGD(TAG, LOG_FMT("[%d] removing %s"), i, hd->hd_calls[i]->uri);
free((char*)hd->hd_calls[i]->uri);
free(hd->hd_calls[i]);
hd->hd_calls[i] = NULL;
return ESP_OK;
}
ESP_LOGW(TAG, LOG_FMT("handler %s with method %d not found"), uri, method);
return ESP_ERR_NOT_FOUND;
}
esp_err_t httpd_unregister_uri(httpd_handle_t handle, const char *uri)
{
if (handle == NULL || uri == NULL) {
return ESP_ERR_INVALID_ARG;
}
struct httpd_data *hd = (struct httpd_data *) handle;
bool found = false;
for (int i = 0; i < hd->config.max_uri_handlers; i++) {
if ((hd->hd_calls[i] != NULL) &&
(strcmp(hd->hd_calls[i]->uri, uri) == 0)) {
ESP_LOGD(TAG, LOG_FMT("[%d] removing %s"), i, uri);
free((char*)hd->hd_calls[i]->uri);
free(hd->hd_calls[i]);
hd->hd_calls[i] = NULL;
found = true;
}
}
if (!found) {
ESP_LOGW(TAG, LOG_FMT("no handler found for URI %s"), uri);
}
return (found ? ESP_OK : ESP_ERR_NOT_FOUND);
}
void httpd_unregister_all_uri_handlers(struct httpd_data *hd)
{
for (unsigned i = 0; i < hd->config.max_uri_handlers; i++) {
if (hd->hd_calls[i]) {
ESP_LOGD(TAG, LOG_FMT("[%d] removing %s"), i, hd->hd_calls[i]->uri);
free((char*)hd->hd_calls[i]->uri);
free(hd->hd_calls[i]);
}
}
}
/* Alternate implmentation of httpd_find_uri_handler()
* which takes a uri_len field. This is useful when the URI
* string contains extra parameters that are not to be included
* while matching with the registered URI_handler strings
*/
static httpd_uri_t* httpd_find_uri_handler2(httpd_err_resp_t *err,
struct httpd_data *hd,
const char *uri, size_t uri_len,
httpd_method_t method)
{
*err = 0;
for (int i = 0; i < hd->config.max_uri_handlers; i++) {
if (hd->hd_calls[i]) {
ESP_LOGD(TAG, LOG_FMT("[%d] = %s"), i, hd->hd_calls[i]->uri);
if ((strlen(hd->hd_calls[i]->uri) == uri_len) && // First match uri length
(strncmp(hd->hd_calls[i]->uri, uri, uri_len) == 0)) { // Then match uri strings
if (hd->hd_calls[i]->method == method) { // Finally match methods
return hd->hd_calls[i];
}
/* URI found but method not allowed.
* If URI IS found later then this
* error is to be neglected */
*err = HTTPD_405_METHOD_NOT_ALLOWED;
}
}
}
if (*err == 0) {
*err = HTTPD_404_NOT_FOUND;
}
return NULL;
}
esp_err_t httpd_uri(struct httpd_data *hd)
{
httpd_uri_t *uri = NULL;
httpd_req_t *req = &hd->hd_req;
struct http_parser_url *res = &hd->hd_req_aux.url_parse_res;
/* For conveying URI not found/method not allowed */
httpd_err_resp_t err = 0;
ESP_LOGD(TAG, LOG_FMT("request for %s with type %d"), req->uri, req->method);
/* URL parser result contains offset and length of path string */
if (res->field_set & (1 << UF_PATH)) {
uri = httpd_find_uri_handler2(&err, hd,
req->uri + res->field_data[UF_PATH].off,
res->field_data[UF_PATH].len,
req->method);
}
/* If URI with method not found, respond with error code */
if (uri == NULL) {
switch (err) {
case HTTPD_404_NOT_FOUND:
return httpd_resp_send_err(req, HTTPD_404_NOT_FOUND);
case HTTPD_405_METHOD_NOT_ALLOWED:
return httpd_resp_send_err(req, HTTPD_405_METHOD_NOT_ALLOWED);
default:
return ESP_FAIL;
}
}
/* Attach user context data (passed during URI registration) into request */
req->user_ctx = uri->user_ctx;
/* Invoke handler */
if (uri->handler(req) != ESP_OK) {
/* Handler returns error, this socket should be closed */
ESP_LOGW(TAG, LOG_FMT("uri handler execution failed"));
httpd_resp_send_err(req, HTTPD_408_REQ_TIMEOUT);
return ESP_FAIL;
}
return ESP_OK;
}

View file

@ -0,0 +1,69 @@
// Copyright 2018 Espressif Systems (Shanghai) PTE LTD
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#ifndef _OSAL_H_
#define _OSAL_H_
#include <freertos/FreeRTOS.h>
#include <freertos/task.h>
#include <unistd.h>
#include <stdint.h>
#include <esp_timer.h>
#ifdef __cplusplus
extern "C" {
#endif
#define OS_SUCCESS ESP_OK
#define OS_FAIL ESP_FAIL
typedef TaskHandle_t othread_t;
static inline int httpd_os_thread_create(othread_t *thread,
const char *name, uint16_t stacksize, int prio,
void (*thread_routine)(void *arg), void *arg)
{
int ret = xTaskCreate(thread_routine, name, stacksize, arg, prio, thread);
if (ret == pdPASS) {
return OS_SUCCESS;
}
return OS_FAIL;
}
/* Only self delete is supported */
static inline void httpd_os_thread_delete()
{
vTaskDelete(xTaskGetCurrentTaskHandle());
}
static inline void httpd_os_thread_sleep(int msecs)
{
vTaskDelay(msecs / portTICK_RATE_MS);
}
static inline int64_t httpd_os_get_timestamp()
{
return esp_timer_get_time();
}
static inline othread_t httpd_os_thread_handle()
{
return xTaskGetCurrentTaskHandle();
}
#ifdef __cplusplus
}
#endif
#endif /* ! _OSAL_H_ */

View file

@ -0,0 +1,77 @@
// Copyright 2018 Espressif Systems (Shanghai) PTE LTD
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "ctrl_sock.h"
/* Control socket, because in some network stacks select can't be woken up any
* other way
*/
int cs_create_ctrl_sock(int port)
{
int fd = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
if (fd < 0) {
return -1;
}
int ret;
struct sockaddr_in addr;
memset(&addr, 0, sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_port = htons(port);
inet_aton("127.0.0.1", &addr.sin_addr);
ret = bind(fd, (struct sockaddr *)&addr, sizeof(addr));
if (ret < 0) {
close(fd);
return -1;
}
return fd;
}
void cs_free_ctrl_sock(int fd)
{
close(fd);
}
int cs_send_to_ctrl_sock(int send_fd, int port, void *data, unsigned int data_len)
{
int ret;
struct sockaddr_in to_addr;
to_addr.sin_family = AF_INET;
to_addr.sin_port = htons(port);
inet_aton("127.0.0.1", &to_addr.sin_addr);
ret = sendto(send_fd, data, data_len, 0, (struct sockaddr *)&to_addr, sizeof(to_addr));
if (ret < 0) {
return -1;
}
return ret;
}
int cs_recv_from_ctrl_sock(int fd, void *data, unsigned int data_len)
{
int ret;
ret = recvfrom(fd, data, data_len, 0, NULL, NULL);
if (ret < 0) {
return -1;
}
return ret;
}

View file

@ -0,0 +1,99 @@
// Copyright 2018 Espressif Systems (Shanghai) PTE LTD
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
/**
* \file ctrl_sock.h
* \brief Control Socket for select() wakeup
*
* LWIP doesn't allow an easy mechanism to on-demand wakeup a thread
* sleeping on select. This is a common requirement for sending
* control commands to a network server. This control socket API
* facilitates the same.
*/
#ifndef _CTRL_SOCK_H_
#define _CTRL_SOCK_H_
#ifdef __cplusplus
extern "C" {
#endif
/**
* @brief Create a control socket
*
* LWIP doesn't allow an easy mechanism to on-demand wakeup a thread
* sleeping on select. This is a common requirement for sending
* control commands to a network server. This control socket API
* facilitates the same.
*
* This API will create a UDP control socket on the specified port. It
* will return a socket descriptor that can then be added to your
* fd_set in select()
*
* @param[in] port the local port on which the control socket will listen
*
* @return - the socket descriptor that can be added to the fd_set in select.
* - an error code if less than zero
*/
int cs_create_ctrl_sock(int port);
/**
* @brief Free the control socket
*
* This frees up the control socket that was earlier created using
* cs_create_ctrl_sock()
*
* @param[in] fd the socket descriptor associated with this control socket
*/
void cs_free_ctrl_sock(int fd);
/**
* @brief Send data to control socket
*
* This API sends data to the control socket. If a server is blocked
* on select() with the control socket, this call will wake up that
* server.
*
* @param[in] send_fd the socket for sending ctrl messages
* @param[in] port the port on which the control socket was created
* @param[in] data pointer to a buffer that contains data to send on the socket
* @param[in] data_len the length of the data contained in the buffer pointed to be data
*
* @return - the number of bytes sent to the control socket
* - an error code if less than zero
*/
int cs_send_to_ctrl_sock(int send_fd, int port, void *data, unsigned int data_len);
/**
* @brief Receive data from control socket
*
* This API receives any data that was sent to the control
* socket. This will be typically called from the server thread to
* process any commands on this socket.
*
* @param[in] fd the socket descriptor of the control socket
* @param[in] data pointer to a buffer that will be used to store
* received from the control socket
* @param[in] data_len the maximum length of the data that can be
* stored in the buffer pointed by data
*
* @return - the number of bytes received from the control socket
* - an error code if less than zero
*/
int cs_recv_from_ctrl_sock(int fd, void *data, unsigned int data_len);
#ifdef __cplusplus
}
#endif
#endif /* ! _CTRL_SOCK_H_ */

View file

@ -0,0 +1 @@
COMPONENT_ADD_LDFLAGS = -Wl,--whole-archive -l$(COMPONENT_NAME) -Wl,--no-whole-archive

View file

@ -0,0 +1,150 @@
// Copyright 2018 Espressif Systems (Shanghai) PTE LTD
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#include <stdlib.h>
#include <stdbool.h>
#include <esp_system.h>
#include <http_server.h>
#include "unity.h"
int pre_start_mem, post_stop_mem, post_stop_min_mem;
bool basic_sanity = true;
esp_err_t null_func(httpd_req_t *req)
{
return ESP_OK;
}
httpd_uri_t handler_limit_uri (char* path)
{
httpd_uri_t uri = {
.uri = path,
.method = HTTP_GET,
.handler = null_func,
.user_ctx = NULL,
};
return uri;
};
static inline unsigned num_digits(unsigned x)
{
unsigned digits = 1;
while ((x = x/10) != 0) {
digits++;
}
return digits;
}
#define HTTPD_TEST_MAX_URI_HANDLERS 8
void test_handler_limit(httpd_handle_t hd)
{
int i;
char x[HTTPD_TEST_MAX_URI_HANDLERS+1][num_digits(HTTPD_TEST_MAX_URI_HANDLERS)+1];
httpd_uri_t uris[HTTPD_TEST_MAX_URI_HANDLERS+1];
for (i = 0; i < HTTPD_TEST_MAX_URI_HANDLERS + 1; i++) {
sprintf(x[i],"%d",i);
uris[i] = handler_limit_uri(x[i]);
}
/* Register multiple instances of the same handler for MAX URI Handlers */
for (i = 0; i < HTTPD_TEST_MAX_URI_HANDLERS; i++) {
TEST_ASSERT(httpd_register_uri_handler(hd, &uris[i]) == ESP_OK);
}
/* Register the MAX URI + 1 Handlers should fail */
TEST_ASSERT(httpd_register_uri_handler(hd, &uris[HTTPD_TEST_MAX_URI_HANDLERS]) != ESP_OK);
/* Unregister the one of the Handler should pass */
TEST_ASSERT(httpd_unregister_uri_handler(hd, uris[0].uri, uris[0].method) == ESP_OK);
/* Unregister non added Handler should fail */
TEST_ASSERT(httpd_unregister_uri_handler(hd, uris[0].uri, uris[0].method) != ESP_OK);
/* Register the MAX URI Handler should pass */
TEST_ASSERT(httpd_register_uri_handler(hd, &uris[0]) == ESP_OK);
/* Reregister same instance of handler should fail */
TEST_ASSERT(httpd_register_uri_handler(hd, &uris[0]) != ESP_OK);
/* Register the MAX URI + 1 Handlers should fail */
TEST_ASSERT(httpd_register_uri_handler(hd, &uris[HTTPD_TEST_MAX_URI_HANDLERS]) != ESP_OK);
/* Unregister the same handler for MAX URI Handlers */
for (i = 0; i < HTTPD_TEST_MAX_URI_HANDLERS; i++) {
TEST_ASSERT(httpd_unregister_uri_handler(hd, uris[i].uri, uris[i].method) == ESP_OK);
}
basic_sanity = false;
}
/********************* Test Handler Limit End *******************/
httpd_handle_t test_httpd_start(uint16_t id)
{
httpd_handle_t hd;
httpd_config_t config = HTTPD_DEFAULT_CONFIG();
config.max_uri_handlers = HTTPD_TEST_MAX_URI_HANDLERS;
config.server_port += id;
config.ctrl_port += id;
TEST_ASSERT(httpd_start(&hd, &config) == ESP_OK)
return hd;
}
#define SERVER_INSTANCES 2
/* Currently this only tests for the number of tasks.
* Heap leakage is not tested as LWIP allocates memory
* which may not be freed immedietly causing erroneous
* evaluation. Another test to implement would be the
* monitoring of open sockets, but LWIP presently provides
* no such API for getting the number of open sockets.
*/
TEST_CASE("Leak Test", "[HTTP SERVER]")
{
httpd_handle_t hd[SERVER_INSTANCES];
unsigned task_count = uxTaskGetNumberOfTasks();
pre_start_mem = esp_get_free_heap_size();
bool res = true;
for (int i = 0; i < SERVER_INSTANCES; i++) {
hd[i] = test_httpd_start(i);
vTaskDelay(10);
if (uxTaskGetNumberOfTasks() != ++task_count) {
res = false;
}
}
for (int i = 0; i < SERVER_INSTANCES; i++) {
if (httpd_stop(hd[i]) != ESP_OK) {
res = false;
}
vTaskDelay(10);
if (uxTaskGetNumberOfTasks() != --task_count) {
res = false;
}
}
post_stop_mem = esp_get_free_heap_size();
TEST_ASSERT(res == true);
}
TEST_CASE("Basic Functionality Tests", "[HTTP SERVER]")
{
httpd_handle_t hd;
httpd_config_t config = HTTPD_DEFAULT_CONFIG();
TEST_ASSERT(httpd_start(&hd, &config) == ESP_OK);
test_handler_limit(hd);
TEST_ASSERT(httpd_stop(hd) == ESP_OK);
}

View file

@ -92,9 +92,10 @@ INPUT = \
##
## ESP-TLS
../../components/esp-tls/esp_tls.h \
## mDNS
## mDNS
../../components/mdns/include/mdns.h \
../../components/esp_http_client/include/esp_http_client.h \
../../components/http_server/include/http_server.h \
##
## Storage - API Reference
##
@ -109,7 +110,7 @@ INPUT = \
../../components/driver/include/driver/sdmmc_host.h \
../../components/driver/include/driver/sdmmc_types.h \
../../components/driver/include/driver/sdspi_host.h \
## SDIO slave
## SDIO slave
../../components/driver/include/driver/sdio_slave.h \
## Non-Volatile Storage
../../components/nvs_flash/include/nvs.h \

View file

@ -0,0 +1,146 @@
HTTP Server
===========
Overview
--------
The HTTP Server component provides an ability for running a lightweight web server on ESP32. Following are detailed steps to use the API exposed by HTTP Server:
* :cpp:func:`httpd_start`: Creates an instance of HTTP server, allocate memory/resources for it depending upon the specified configuration and outputs a handle to the server instance. The server has both, a listening socket (TCP) for HTTP traffic, and a control socket (UDP) for control signals, which are selected in a round robin fashion in the server task loop. The task priority and stack size are configurable during server instance creation by passing httpd_config_t structure to httpd_start(). TCP traffic is parsed as HTTP requests and, depending on the requested URI, user registered handlers are invoked which are supposed to send back HTTP response packets.
* :cpp:func:`httpd_stop`: This stops the server with the provided handle and frees up any associated memory/resources. This is a blocking function that first signals a halt to the server task and then waits for the task to terminate. While stopping, the task will close all open connections, remove registered URI handlers and reset all session context data to empty.
* :cpp:func:`httpd_register_uri_handler`: A URI handler is registered by passing object of type ``httpd_uri_t`` structure which has members including ``uri`` name, ``method`` type (eg. ``HTTPD_GET/HTTPD_POST/HTTPD_PUT`` etc.), function pointer of type ``esp_err_t *handler (httpd_req_t *req)`` and ``user_ctx`` pointer to user context data.
Application Example
-------------------
.. highlight:: c
::
/* Our URI handler function to be called during GET /uri request */
esp_err_t get_handler(httpd_req_t *req)
{
/* Send a simple response */
const char[] resp = "URI GET Response";
httpd_resp_send(req, resp, strlen(resp));
return ESP_OK;
}
/* Our URI handler function to be called during POST /uri request */
esp_err_t post_handler(httpd_req_t *req)
{
/* Read request content */
char[100] content;
/* Truncate if content length larger than the buffer */
size_t recv_size = MIN(req->content_len, sizeof(content));
int ret = httpd_req_recv(req, content, recv_size);
if (ret < 0) {
/* In case of recv error, returning ESP_FAIL will
* ensure that the underlying socket is closed */
return ESP_FAIL;
}
/* Send a simple response */
const char[] resp = "URI POST Response";
httpd_resp_send(req, resp, strlen(resp));
return ESP_OK;
}
/* URI handler structure for GET /uri */
httpd_uri_t uri_get = {
.uri = "/uri",
.method = HTTP_GET,
.handler = get_handler,
.user_ctx = NULL
};
/* URI handler structure for POST /uri */
httpd_uri_t uri_post = {
.uri = "/uri",
.method = HTTP_POST,
.handler = post_handler,
.user_ctx = NULL
};
/* Function for starting the webserver */
httpd_handle_t start_webserver(void)
{
/* Generate default configuration */
httpd_config_t config = HTTPD_DEFAULT_CONFIG();
/* Empty handle to http_server */
httpd_handle_t server = NULL;
/* Start the httpd server */
if (httpd_start(&server, &config) == ESP_OK) {
/* Register URI handlers */
httpd_register_uri_handler(server, &uri_get);
httpd_register_uri_handler(server, &uri_post);
}
/* If server failed to start, handle will be NULL */
return server;
}
/* Function for stopping the webserver */
void stop_webserver(httpd_handle_t server)
{
if (server) {
/* Stop the httpd server */
httpd_stop(server);
}
}
Simple HTTP server example
^^^^^^^^^^^^^^^^^^^^^^^^^^
Check HTTP server example under :example:`protocols/http_server/simple` where handling of arbitrary content lengths, reading request headers and URL query parameters, and setting response headers is demonstrated.
Persistent Connections
----------------------
HTTP server features persistent connections, allowing for the re-use of the same connection (session) for several transfers, all the while maintaining context specific data for the session. Context data may be allocated dynamically by the handler in which case a custom function may need to be specified for freeing this data when the connection/session is closed.
Persistent Connections Example
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.. highlight:: c
::
/* Custom function to free context */
void free_ctx_func(void *ctx)
{
/* Could be something other than free */
free(ctx);
}
esp_err_t adder_post_handler(httpd_req_t *req)
{
/* Create session's context if not already available */
if (! req->sess_ctx) {
req->sess_ctx = malloc(sizeof(ANY_DATA_TYPE)); /*!< Pointer to context data */
req->free_ctx = free_ctx_func; /*!< Function to free context data */
}
/* Access context data */
ANY_DATA_TYPE *ctx_data = (ANY_DATA_TYPE *)req->sess_ctx;
/* Respond */
...............
...............
...............
return ESP_OK;
}
Check the example under :example:`protocols/http_server/persistent_sockets`.
API Reference
-------------
.. include:: /_build/inc/http_server.inc

View file

@ -7,5 +7,6 @@ Protocols API
mDNS <mdns>
ESP-TLS <esp_tls>
HTTP Client <esp_http_client>
HTTP Server <http_server>
Example code for this API section is provided in :example:`protocols` directory of ESP-IDF examples.

View file

@ -0,0 +1 @@
.. include:: ../../../en/api-reference/protocols/http_server.rst

View file

@ -0,0 +1,9 @@
#
# This is a project Makefile. It is assumed the directory this Makefile resides in is a
# project subdirectory.
#
PROJECT_NAME := tests
include $(IDF_PATH)/make/project.mk

View file

@ -0,0 +1,183 @@
#!/usr/bin/env python
#
# Copyright 2018 Espressif Systems (Shanghai) PTE LTD
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import imp
import re
import os
import sys
import string
import random
import socket
# This environment variable is expected on the host machine
test_fw_path = os.getenv("TEST_FW_PATH")
if test_fw_path and test_fw_path not in sys.path:
sys.path.insert(0, test_fw_path)
# When running on local machine execute the following before running this script
# > export TEST_FW_PATH='~/esp/esp-idf/tools/tiny-test-fw'
# > make print_flash_cmd | tail -n 1 > build/download.config
# > make app bootloader
import TinyFW
import IDF
# Import client module
expath = os.path.dirname(os.path.realpath(__file__))
client = imp.load_source("client", expath + "/scripts/test.py")
@IDF.idf_example_test(env_tag="Example_WIFI")
def test_examples_protocol_http_server_advanced(env, extra_data):
# Acquire DUT
dut1 = env.get_dut("http_server", "examples/protocols/http_server/advanced_tests")
# Get binary file
binary_file = os.path.join(dut1.app.binary_path, "tests.bin")
bin_size = os.path.getsize(binary_file)
IDF.log_performance("http_server_bin_size", "{}KB".format(bin_size//1024))
IDF.check_performance("http_server_bin_size", bin_size//1024)
# Upload binary and start testing
dut1.start_app()
# Parse IP address of STA
got_ip = dut1.expect(re.compile(r"(?:[\s\S]*)Got IP: (\d+.\d+.\d+.\d+)"), timeout=30)[0]
print "Leak Tests..."
# Expected Leak test Logs
dut1.expect("Leak Test Started...");
dut1.expect("Leak Test Passed");
got_port = dut1.expect(re.compile(r"(?:[\s\S]*)Started HTTP server on port: (\d+)"))[0]
result = dut1.expect(re.compile(r"(?:[\s\S]*)Max URI handlers: (\d+)(?:[\s\S]*)Max Open Sessions: (\d+)(?:[\s\S]*)Max Header Length: (\d+)(?:[\s\S]*)Max URI Length: (\d+)(?:[\s\S]*)Max Stack Size: (\d+)"))
max_uri_handlers = int(result[0])
max_sessions = int(result[1])
max_hdr_len = int(result[2])
max_uri_len = int(result[3])
max_stack_size = int(result[4])
print "Got IP : " + got_ip
print "Got Port : " + got_port
print "Handler Tests..."
# Expected Handler Test Logs
dut1.expect("Test: Register Max URI handlers")
dut1.expect("Success")
dut1.expect("Test: Register Max URI + 1 handlers")
dut1.expect("no slots left for registering handler")
dut1.expect("Success")
dut1.expect("Test: Unregister 0th handler")
dut1.expect("Success")
dut1.expect("Test: Again unregister 0th handler not registered")
dut1.expect("handler 0 with method 1 not found")
dut1.expect("Success")
dut1.expect("Test: Register back 0th handler")
dut1.expect("Success")
dut1.expect("Test: Register 0th handler again after registering")
dut1.expect("handler 0 with method 1 already registered")
dut1.expect("Success")
dut1.expect("Test: Register 1 more handler")
dut1.expect("no slots left for registering handler")
dut1.expect("Success")
dut1.expect("Test: Unregister all handlers")
dut1.expect("Success")
dut1.expect("Registering basic handlers")
dut1.expect("Success")
# Run test script
# If failed raise appropriate exception
print "Basic HTTP Client Tests..."
if not client.get_hello(got_ip, got_port):
raise RuntimeError
inital_stack = int(dut1.expect(re.compile(r"(?:[\s\S]*)Free Stack for server task: (\d+)"))[0])
if inital_stack < 0.1*max_stack_size:
print "More than 90% of stack being used on server start"
raise RuntimeError
if not client.post_hello(got_ip, got_port):
raise RuntimeError
if not client.put_hello(got_ip, got_port):
raise RuntimeError
if not client.post_echo(got_ip, got_port):
raise RuntimeError
if not client.get_echo(got_ip, got_port):
raise RuntimeError
if not client.put_echo(got_ip, got_port):
raise RuntimeError
if not client.get_hello_type(got_ip, got_port):
raise RuntimeError
if not client.get_hello_status(got_ip, got_port):
raise RuntimeError
if not client.get_false_uri(got_ip, got_port):
raise RuntimeError
print "Error code tests..."
if not client.code_500_server_error_test(got_ip, got_port):
raise RuntimeError
if not client.code_501_method_not_impl(got_ip, got_port):
raise RuntimeError
if not client.code_505_version_not_supported(got_ip, got_port):
raise RuntimeError
if not client.code_400_bad_request(got_ip, got_port):
raise RuntimeError
if not client.code_404_not_found(got_ip, got_port):
raise RuntimeError
if not client.code_405_method_not_allowed(got_ip, got_port):
raise RuntimeError
if not client.code_408_req_timeout(got_ip, got_port):
raise RuntimeError
if not client.code_414_uri_too_long(got_ip, got_port, max_uri_len):
raise RuntimeError
if not client.code_431_hdr_too_long(got_ip, got_port, max_hdr_len):
raise RuntimeError
if not client.test_upgrade_not_supported(got_ip, got_port):
raise RuntimeError
print "Sessions and Context Tests..."
if not client.parallel_sessions_adder(got_ip, got_port, max_sessions):
raise RuntimeError
if not client.leftover_data_test(got_ip, got_port):
raise RuntimeError
if not client.async_response_test(got_ip, got_port):
raise RuntimeError
if not client.spillover_session(got_ip, got_port, max_sessions):
raise RuntimeError
if not client.recv_timeout_test(got_ip, got_port):
raise RuntimeError
# May timeout in case requests are sent slower than responses are read.
# Instead use httperf stress test
#if not client.pipeline_test(got_ip, got_port, max_sessions):
# raise RuntimeError
test_size = 50*1024 # 50KB
if not client.packet_size_limit_test(got_ip, got_port, test_size):
raise RuntimeError
if not client.get_hello(got_ip, got_port):
raise RuntimeError
final_stack = int(dut1.expect(re.compile(r"(?:[\s\S]*)Free Stack for server task: (\d+)"))[0])
if final_stack < 0.05*max_stack_size:
print "More than 95% of stack got used during tests"
raise RuntimeError
if __name__ == '__main__':
test_examples_protocol_http_server_advanced()

View file

@ -0,0 +1,16 @@
menu "Example Configuration"
config WIFI_SSID
string "WiFi SSID"
default "myssid"
help
SSID (network name) for the example to connect to.
config WIFI_PASSWORD
string "WiFi Password"
default "mypasswd"
help
WiFi password (WPA or WPA2) for the example to use.
Can be left blank if the network has no security set.
endmenu

View file

@ -0,0 +1,5 @@
#
# "main" pseudo-component makefile.
#
# (Uses default behaviour of compiling all source files in directory, adding 'include' to include path.)

View file

@ -0,0 +1,9 @@
#ifndef __HTTPD_TESTS_H__
#define __HTTPD_TESTS_H__
#include <http_server.h>
extern httpd_handle_t start_tests(void);
extern void stop_tests(httpd_handle_t hd);
#endif // __HTTPD_TESTS_H__

View file

@ -0,0 +1,81 @@
#include "esp_wifi.h"
#include "esp_event_loop.h"
#include "esp_log.h"
#include "esp_system.h"
#include "nvs_flash.h"
#include "tests.h"
/* The examples use simple WiFi configuration that you can set via
'make menuconfig'.
If you'd rather not, just change the below entries to strings with
the config you want - ie #define EXAMPLE_WIFI_SSID "mywifissid"
*/
#define EXAMPLE_WIFI_SSID CONFIG_WIFI_SSID
#define EXAMPLE_WIFI_PASS CONFIG_WIFI_PASSWORD
static const char *TAG="TEST_WIFI";
static esp_err_t event_handler(void *ctx, system_event_t *event)
{
httpd_handle_t *hd = (httpd_handle_t *) ctx;
switch(event->event_id) {
case SYSTEM_EVENT_STA_START:
ESP_LOGI(TAG, "SYSTEM_EVENT_STA_START");
ESP_ERROR_CHECK(esp_wifi_connect());
break;
case SYSTEM_EVENT_STA_GOT_IP:
ESP_LOGI(TAG, "SYSTEM_EVENT_STA_GOT_IP");
ESP_LOGI(TAG, "Got IP: %s",
ip4addr_ntoa(&event->event_info.got_ip.ip_info.ip));
// Start webserver tests
if (*hd == NULL) {
*hd = start_tests();
}
break;
case SYSTEM_EVENT_STA_DISCONNECTED:
ESP_LOGI(TAG, "SYSTEM_EVENT_STA_DISCONNECTED");
ESP_ERROR_CHECK(esp_wifi_connect());
// Stop webserver tests
if (*hd) {
stop_tests(*hd);
*hd = NULL;
}
break;
default:
break;
}
return ESP_OK;
}
static void initialise_wifi(void)
{
tcpip_adapter_init();
static httpd_handle_t hd = NULL;
ESP_ERROR_CHECK(esp_event_loop_init(event_handler, &hd));
wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
ESP_ERROR_CHECK(esp_wifi_init(&cfg));
ESP_ERROR_CHECK(esp_wifi_set_storage(WIFI_STORAGE_RAM));
wifi_config_t wifi_config = {
.sta = {
.ssid = EXAMPLE_WIFI_SSID,
.password = EXAMPLE_WIFI_PASS,
},
};
ESP_LOGI(TAG, "Setting WiFi configuration SSID %s...", wifi_config.sta.ssid);
ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA));
ESP_ERROR_CHECK(esp_wifi_set_config(ESP_IF_WIFI_STA, &wifi_config));
ESP_ERROR_CHECK(esp_wifi_start());
}
void app_main()
{
ESP_ERROR_CHECK(nvs_flash_init());
initialise_wifi();
}

View file

@ -0,0 +1,511 @@
#include <stdlib.h>
#include <stdbool.h>
#include <esp_log.h>
#include <esp_system.h>
#include <http_server.h>
#include "tests.h"
static const char *TAG="TESTS";
int pre_start_mem, post_stop_mem, post_stop_min_mem;
bool basic_sanity = true;
/********************* Basic Handlers Start *******************/
esp_err_t hello_get_handler(httpd_req_t *req)
{
#define STR "Hello World!"
ESP_LOGI(TAG, "Free Stack for server task: %d", uxTaskGetStackHighWaterMark(NULL));
httpd_resp_send(req, STR, strlen(STR));
return ESP_OK;
#undef STR
}
esp_err_t hello_type_get_handler(httpd_req_t *req)
{
#define STR "Hello World!"
httpd_resp_set_type(req, HTTPD_TYPE_TEXT);
httpd_resp_send(req, STR, strlen(STR));
return ESP_OK;
#undef STR
}
esp_err_t hello_status_get_handler(httpd_req_t *req)
{
#define STR "Hello World!"
httpd_resp_set_status(req, HTTPD_500);
httpd_resp_send(req, STR, strlen(STR));
return ESP_OK;
#undef STR
}
esp_err_t echo_post_handler(httpd_req_t *req)
{
ESP_LOGI(TAG, "/echo handler read content length %d", req->content_len);
char* buf = malloc(req->content_len + 1);
size_t off = 0;
int ret;
if (!buf) {
return ESP_FAIL;
}
while (off < req->content_len) {
/* Read data received in the request */
ret = httpd_req_recv(req, buf + off, req->content_len - off);
if (ret < 0) {
free (buf);
return ESP_FAIL;
}
off += ret;
ESP_LOGI(TAG, "/echo handler recv length %d", ret);
}
buf[off] = '\0';
if (req->content_len < 128) {
ESP_LOGI(TAG, "/echo handler read %s", buf);
}
/* Search for Custom header field */
char* req_hdr = 0;
size_t hdr_len = httpd_req_get_hdr_value_len(req, "Custom");
if (hdr_len) {
/* Read Custom header value */
req_hdr = malloc(hdr_len + 1);
if (req_hdr) {
httpd_req_get_hdr_value_str(req, "Custom", req_hdr, hdr_len + 1);
/* Set as additional header for response packet */
httpd_resp_set_hdr(req, "Custom", req_hdr);
}
}
httpd_resp_send(req, buf, req->content_len);
free (req_hdr);
free (buf);
return ESP_OK;
}
void adder_free_func(void *ctx)
{
ESP_LOGI(TAG, "Custom Free Context function called");
free(ctx);
}
/* Create a context, keep incrementing value in the context, by whatever was
* received. Return the result
*/
esp_err_t adder_post_handler(httpd_req_t *req)
{
char buf[10];
char outbuf[50];
int ret;
/* Read data received in the request */
ret = httpd_req_recv(req, buf, sizeof(buf));
if (ret < 0) {
return ESP_FAIL;
}
buf[ret] = '\0';
int val = atoi(buf);
ESP_LOGI(TAG, "/adder handler read %d", val);
if (! req->sess_ctx) {
ESP_LOGI(TAG, "/adder allocating new session");
req->sess_ctx = malloc(sizeof(int));
req->free_ctx = adder_free_func;
*(int *)req->sess_ctx = 0;
}
int *adder = (int *)req->sess_ctx;
*adder += val;
snprintf(outbuf, sizeof(outbuf),"%d", *adder);
httpd_resp_send(req, outbuf, strlen(outbuf));
return ESP_OK;
}
esp_err_t leftover_data_post_handler(httpd_req_t *req)
{
/* Only echo the first 10 bytes of the request, leaving the rest of the
* request data as is.
*/
char buf[11];
int ret;
/* Read data received in the request */
ret = httpd_req_recv(req, buf, sizeof(buf) - 1);
if (ret < 0) {
return ESP_FAIL;
}
buf[ret] = '\0';
ESP_LOGI(TAG, "leftover data handler read %s", buf);
httpd_resp_send(req, buf, strlen(buf));
return ESP_OK;
}
int httpd_default_send(int sockfd, const char *buf, unsigned buf_len, int flags);
void generate_async_resp(void *arg)
{
char buf[250];
int fd = (int )arg;
#define HTTPD_HDR_STR "HTTP/1.1 200 OK\r\n" \
"Content-Type: text/html\r\n" \
"Content-Length: %d\r\n"
#define STR "Hello Double World!"
ESP_LOGI(TAG, "Executing queued work fd : %d", fd);
snprintf(buf, sizeof(buf), HTTPD_HDR_STR,
strlen(STR));
httpd_default_send(fd, buf, strlen(buf), 0);
/* Space for sending additional headers based on set_header */
httpd_default_send(fd, "\r\n", strlen("\r\n"), 0);
httpd_default_send(fd, STR, strlen(STR), 0);
#undef STR
}
esp_err_t async_get_handler(httpd_req_t *req)
{
#define STR "Hello World!"
httpd_resp_send(req, STR, strlen(STR));
/* Also register a HTTPD Work which sends the same data on the same
* socket again
*/
int fd = httpd_req_to_sockfd(req);
if (fd < 0) {
return ESP_FAIL;
}
ESP_LOGI(TAG, "Queuing work fd : %d", fd);
httpd_queue_work(req->handle, generate_async_resp, (void *)fd);
return ESP_OK;
#undef STR
}
httpd_uri_t basic_handlers[] = {
{ .uri = "/hello/type_html",
.method = HTTP_GET,
.handler = hello_type_get_handler,
.user_ctx = NULL,
},
{ .uri = "/hello",
.method = HTTP_GET,
.handler = hello_get_handler,
.user_ctx = NULL,
},
{ .uri = "/hello/status_500",
.method = HTTP_GET,
.handler = hello_status_get_handler,
.user_ctx = NULL,
},
{ .uri = "/echo",
.method = HTTP_POST,
.handler = echo_post_handler,
.user_ctx = NULL,
},
{ .uri = "/echo",
.method = HTTP_PUT,
.handler = echo_post_handler,
.user_ctx = NULL,
},
{ .uri = "/leftover_data",
.method = HTTP_POST,
.handler = leftover_data_post_handler,
.user_ctx = NULL,
},
{ .uri = "/adder",
.method = HTTP_POST,
.handler = adder_post_handler,
.user_ctx = NULL,
},
{ .uri = "/async_data",
.method = HTTP_GET,
.handler = async_get_handler,
.user_ctx = NULL,
}
};
int basic_handlers_no = sizeof(basic_handlers)/sizeof(httpd_uri_t);
void register_basic_handlers(httpd_handle_t hd)
{
int i;
ESP_LOGI(TAG, "Registering basic handlers");
ESP_LOGI(TAG, "No of handlers = %d", basic_handlers_no);
for (i = 0; i < basic_handlers_no; i++) {
if (httpd_register_uri_handler(hd, &basic_handlers[i]) != ESP_OK) {
ESP_LOGW(TAG, "register uri failed for %d", i);
return;
}
}
ESP_LOGI(TAG, "Success");
}
/********************* Basic Handlers End *******************/
esp_err_t my_hello_post_handler(httpd_req_t *req)
{
char buf[10];
char outbuf[50];
int ret;
ret = httpd_req_recv(req, buf, sizeof(buf));
if (ret < 0) {
return ESP_FAIL;
}
httpd_resp_set_status(req, HTTPD_404);
httpd_resp_set_type(req, HTTPD_TYPE_TEXT);
ESP_LOGI(TAG, "Read %d bytes as:%s:", ret, buf);
buf[ret] = '\0';
#define STR "my_hello_handler"
snprintf(outbuf, sizeof(outbuf), STR" %s", buf);
httpd_resp_send(req, outbuf, strlen(outbuf));
return ESP_OK;
#undef STR
}
/********************* Test Handler Limit Start *******************/
esp_err_t null_func(httpd_req_t *req)
{
return ESP_OK;
}
httpd_uri_t handler_limit_uri (char* path)
{
httpd_uri_t uri = {
.uri = path,
.method = HTTP_GET,
.handler = null_func,
.user_ctx = NULL,
};
return uri;
};
static inline unsigned num_digits(unsigned x)
{
unsigned digits = 1;
while ((x = x/10) != 0) {
digits++;
}
return digits;
}
#define HTTPD_TEST_MAX_URI_HANDLERS 8
void test_handler_limit(httpd_handle_t hd)
{
int i, ret;
char x[HTTPD_TEST_MAX_URI_HANDLERS+1][num_digits(HTTPD_TEST_MAX_URI_HANDLERS)+1];
httpd_uri_t uris[HTTPD_TEST_MAX_URI_HANDLERS+1];
for (i = 0; i < HTTPD_TEST_MAX_URI_HANDLERS + 1; i++) {
sprintf(x[i],"%d",i);
uris[i] = handler_limit_uri(x[i]);
}
/* Register multiple instances of the same handler for MAX URI Handlers */
ESP_LOGI(TAG, "Test: Register Max URI handlers: %d...", HTTPD_TEST_MAX_URI_HANDLERS);
for (i = 0; i < HTTPD_TEST_MAX_URI_HANDLERS; i++) {
ret = httpd_register_uri_handler(hd, &uris[i]);
if (ret != ESP_OK) {
ESP_LOGI(TAG, "Fail");
goto error_ret;
}
}
ESP_LOGI(TAG, "Success");
/* Register the MAX URI + 1 Handlers should fail */
ESP_LOGI(TAG, "Test: Register Max URI + 1 handlers: %d th...", HTTPD_TEST_MAX_URI_HANDLERS +1 );
ret = httpd_register_uri_handler(hd, &uris[HTTPD_TEST_MAX_URI_HANDLERS]);
if (ret == ESP_OK) {
ESP_LOGI(TAG, "Fail");
goto error_ret;
}
ESP_LOGI(TAG, "Success");
/* Unregister the one of the Handler should pass */
ESP_LOGI(TAG, "Test: Unregister 0th handler...");
ret = httpd_unregister_uri_handler(hd, uris[0].uri, uris[0].method);
if (ret != ESP_OK) {
ESP_LOGI(TAG, "Fail");
goto error_ret;
}
ESP_LOGI(TAG, "Success");
/* Unregister non added Handler should fail */
ESP_LOGI(TAG, "Test: Again unregister 0th handler not registered...");
ret = httpd_unregister_uri_handler(hd, uris[0].uri, uris[0].method);
if (ret == ESP_OK) {
ESP_LOGI(TAG, "Fail");
goto error_ret;
}
ESP_LOGI(TAG, "Success");
/* Register the MAX URI Handler should pass */
ESP_LOGI(TAG, "Test: Register back 0th handler...");
ret = httpd_register_uri_handler(hd, &uris[0]);
if (ret != ESP_OK) {
ESP_LOGI(TAG, "Fail");
goto error_ret;
}
ESP_LOGI(TAG, "Success");
/* Reregister same instance of handler should fail */
ESP_LOGI(TAG, "Test: Register 0th handler again after registering...");
ret = httpd_register_uri_handler(hd, &uris[0]);
if (ret == ESP_OK) {
ESP_LOGI(TAG, "Fail");
goto error_ret;
}
ESP_LOGI(TAG, "Success");
/* Register the MAX URI + 1 Handlers should fail */
ESP_LOGI(TAG, "Test: Register 1 more handler...");
ret = httpd_register_uri_handler(hd, &uris[HTTPD_TEST_MAX_URI_HANDLERS]);
if (ret == ESP_OK) {
ESP_LOGI(TAG, "Fail");
goto error_ret;
}
ESP_LOGI(TAG, "Success");
/* Unregister the same handler for MAX URI Handlers */
ESP_LOGI(TAG, "Test: Unregister all handlers:");
for (i = 0; i < HTTPD_TEST_MAX_URI_HANDLERS; i++) {
ret = httpd_unregister_uri_handler(hd, uris[i].uri, uris[i].method);
if (ret != 0) {
ESP_LOGI(TAG, "Fail");
goto error_ret;
}
}
ESP_LOGI(TAG, "Success");
error_ret:
for (; i < HTTPD_TEST_MAX_URI_HANDLERS; i++) {
httpd_unregister_uri_handler(hd, uris[i].uri, uris[i].method);
}
basic_sanity = false;
}
/********************* Test Handler Limit End *******************/
httpd_handle_t test_httpd_start()
{
pre_start_mem = esp_get_free_heap_size();
httpd_handle_t hd;
httpd_config_t config = HTTPD_DEFAULT_CONFIG();
if (httpd_start(&hd, &config) == ESP_OK) {
ESP_LOGI(TAG, "Started HTTP server on port: %d", config.server_port);
ESP_LOGI(TAG, "Max URI handlers: %d", config.max_uri_handlers);
ESP_LOGI(TAG, "Max Open Sessions: %d", config.max_open_sockets);
ESP_LOGI(TAG, "Max Header Length: %d", HTTPD_MAX_REQ_HDR_LEN);
ESP_LOGI(TAG, "Max URI Length: %d", HTTPD_MAX_URI_LEN);
ESP_LOGI(TAG, "Max Stack Size: %d", config.stack_size);
return hd;
}
return NULL;
}
httpd_handle_t test_httpd_start_dummy(uint16_t id)
{
pre_start_mem = esp_get_free_heap_size();
ESP_LOGI(TAG, "HTTPD Start: Current free memory: %d", pre_start_mem);
httpd_handle_t hd;
httpd_config_t config = HTTPD_DEFAULT_CONFIG();
config.max_uri_handlers = HTTPD_TEST_MAX_URI_HANDLERS;
config.server_port += id;
config.ctrl_port += id;
if (httpd_start(&hd, &config) == ESP_OK) {
return hd;
}
return NULL;
}
void test_httpd_stop(httpd_handle_t hd)
{
httpd_stop(hd);
post_stop_mem = esp_get_free_heap_size();
ESP_LOGI(TAG, "HTTPD Stop: Current free memory: %d", post_stop_mem);
// Below function is not available but would be desirable to have
// post_stop_min_mem = os_get_minimum_free_mem();
// ESP_LOGI(TAG, "HTTPD Stop: Minimum free memory: %d", post_stop_min_mem);
}
#define SERVER_INSTANCES 10
/* Currently this only tests for the number of tasks.
* Heap leakage is not tested as LWIP allocates memory
* which may not be freed immedietly causing erroneous
* evaluation. Another test to implement would be the
* monitoring of open sockets, but LWIP presently provides
* no such API for getting the number of open sockets.
*/
bool leak_test(void)
{
httpd_handle_t hd[SERVER_INSTANCES];
bool success = true;
unsigned task_count = uxTaskGetNumberOfTasks();
ESP_LOGI(TAG, "Leak Test Started...");
ESP_LOGI(TAG, "Current free heap : %d", xPortGetFreeHeapSize());
ESP_LOGI(TAG, "Total tasks running : %d", task_count);
for (int i = 0; i < SERVER_INSTANCES; i++) {
ESP_LOGI(TAG, "Starting Server Instance [%d]", i);
hd[i] = test_httpd_start_dummy(i);
if (hd[i]) {
register_basic_handlers(hd[i]);
task_count++;
}
ESP_LOGI(TAG, "Current free heap : %d", xPortGetFreeHeapSize());
ESP_LOGI(TAG, "Total tasks running : %d", uxTaskGetNumberOfTasks());
if (uxTaskGetNumberOfTasks() != task_count) {
ESP_LOGE(TAG, "Task count mismatch");
success = false;
break;
}
}
for (int i = 0; i < SERVER_INSTANCES; i++) {
ESP_LOGI(TAG, "Stopping Server Instance [%d]", i);
if (hd[i]) {
httpd_stop(hd[i]);
task_count--;
}
ESP_LOGI(TAG, "Current free heap : %d", xPortGetFreeHeapSize());
ESP_LOGI(TAG, "Total tasks running : %d", uxTaskGetNumberOfTasks());
if (uxTaskGetNumberOfTasks() != task_count) {
ESP_LOGE(TAG, "Task count mismatch");
success = false;
}
}
if (success) {
ESP_LOGI(TAG, "Leak Test Passed");
}
else {
ESP_LOGI(TAG, "Leak Test Failed");
}
return success;
}
httpd_handle_t start_tests()
{
leak_test();
httpd_handle_t hd = test_httpd_start();
if (hd) {
test_handler_limit(hd);
register_basic_handlers(hd);
}
return hd;
}
void stop_tests(httpd_handle_t hd)
{
ESP_LOGI(TAG, "Stopping httpd");
test_httpd_stop(hd);
}

View file

@ -0,0 +1,841 @@
#!/usr/bin/env python
#
# Copyright 2018 Espressif Systems (Shanghai) PTE LTD
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# Utility for testing the web server. Test cases:
# Assume the device supports 'n' simultaneous open sockets
#
# HTTP Server Tests
#
# 0. Firmware Settings:
# - Create a dormant thread whose sole job is to call httpd_stop() when instructed
# - Measure the following before httpd_start() is called:
# - current free memory
# - current free sockets
# - Measure the same whenever httpd_stop is called
# - Register maximum possible URI handlers: should be successful
# - Register one more URI handler: should fail
# - Deregister on URI handler: should be successful
# - Register on more URI handler: should succeed
# - Register separate handlers for /hello, /hello/type_html. Also
# ensure that /hello/type_html is registered BEFORE /hello. (tests
# that largest matching URI is picked properly)
# - Create URI handler /adder. Make sure it uses a custom free_ctx
# structure to free it up
# 1. Using Standard Python HTTP Client
# - simple GET on /hello (returns Hello World. Ensures that basic
# firmware tests are complete, or returns error)
# - POST on /hello (should fail)
# - PUT on /hello (should fail)
# - simple POST on /echo (returns whatever the POST data)
# - simple PUT on /echo (returns whatever the PUT data)
# - GET on /echo (should fail)
# - simple GET on /hello/type_html (returns Content type as text/html)
# - simple GET on /hello/status_500 (returns HTTP status 500)
# - simple GET on /false_uri (returns HTTP status 404)
# - largest matching URI handler is picked is already verified because
# of /hello and /hello/type_html tests
#
#
# 2. Session Tests
# - Sessions + Pipelining basics:
# - Create max supported sessions
# - On session i,
# - send 3 back-to-back POST requests with data i on /adder
# - read back 3 responses. They should be i, 2i and 3i
# - Tests that
# - pipelining works
# - per-session context is maintained for all supported
# sessions
# - Close all sessions
#
# - Cleanup leftover data: Tests that the web server properly cleans
# up leftover data
# - Create a session
# - POST on /leftover_data with 52 bytes of data (data includes
# \r\n)(the handler only
# reads first 10 bytes and returns them, leaving the rest of the
# bytes unread)
# - GET on /hello (should return 'Hello World')
# - POST on /false_uri with 52 bytes of data (data includes \r\n)
# (should return HTTP 404)
# - GET on /hello (should return 'Hello World')
#
# - Test HTTPd Asynchronous response
# - Create a session
# - GET on /async_data
# - returns 'Hello World!' as a response
# - the handler schedules an async response, which generates a second
# response 'Hello Double World!'
#
# - Spillover test
# - Create max supported sessions with the web server
# - GET /hello on all the sessions (should return Hello World)
# - Create one more session, this should fail
# - GET /hello on all the sessions (should return Hello World)
#
# - Timeout test
# - Create a session and only Send 'GE' on the same (simulates a
# client that left the network halfway through a request)
# - Wait for recv-wait-timeout
# - Server should automatically close the socket
############# TODO TESTS #############
# 3. Stress Tests
#
# - httperf
# - Run the following httperf command:
# httperf --server=10.31.130.126 --wsess=8,50,0.5 --rate 8 --burst-length 2
#
# - The above implies that the test suite will open
# - 8 simultaneous connections with the server
# - the rate of opening the sessions will be 8 per sec. So in our
# case, a new connection will be opened every 0.2 seconds for 1 second
# - The burst length 2 indicates that 2 requests will be sent
# simultaneously on the same connection in a single go
# - 0.5 seconds is the time between sending out 2 bursts
# - 50 is the total number of requests that will be sent out
#
# - So in the above example, the test suite will open 8
# connections, each separated by 0.2 seconds. On each connection
# it will send 2 requests in a single burst. The bursts on a
# single connection will be separated by 0.5 seconds. A total of
# 25 bursts (25 x 2 = 50) will be sent out
# 4. Leak Tests
# - Simple Leak test
# - Simple GET on /hello/restart (returns success, stop web server, measures leaks, restarts webserver)
# - Simple GET on /hello/restart_results (returns the leak results)
# - Leak test with open sockets
# - Open 8 sessions
# - Simple GET on /hello/restart (returns success, stop web server,
# measures leaks, restarts webserver)
# - All sockets should get closed
# - Simple GET on /hello/restart_results (returns the leak results)
import threading
import socket
import time
import argparse
import requests
import signal
import sys
import string
import random
_verbose_ = False
class Session:
def __init__(self, addr, port):
self.client = socket.create_connection((addr, int(port)))
self.client.settimeout(30)
self.target = addr
def send_get(self, path, headers=None):
request = "GET " + path + " HTTP/1.1\r\nHost: " + self.target
if headers:
for field, value in headers.iteritems():
request += "\r\n"+field+": "+value
request += "\r\n\r\n"
self.client.send(request);
def send_put(self, path, data, headers=None):
request = "PUT " + path + " HTTP/1.1\r\nHost: " + self.target
if headers:
for field, value in headers.iteritems():
request += "\r\n"+field+": "+value
request += "\r\nContent-Length: " + str(len(data)) +"\r\n\r\n"
self.client.send(request)
self.client.send(data)
def send_post(self, path, data, headers=None):
request = "POST " + path + " HTTP/1.1\r\nHost: " + self.target
if headers:
for field, value in headers.iteritems():
request += "\r\n"+field+": "+value
request += "\r\nContent-Length: " + str(len(data)) +"\r\n\r\n"
self.client.send(request)
self.client.send(data)
def read_resp_hdrs(self):
state = 'nothing'
resp_read = ''
while True:
char = self.client.recv(1)
if char == '\r' and state == 'nothing':
state = 'first_cr'
elif char == '\n' and state == 'first_cr':
state = 'first_lf'
elif char == '\r' and state == 'first_lf':
state = 'second_cr'
elif char == '\n' and state == 'second_cr':
state = 'second_lf'
else:
state = 'nothing'
resp_read += char
if state == 'second_lf':
break;
# Handle first line
line_hdrs = resp_read.splitlines()
line_comp = line_hdrs[0].split()
self.status = line_comp[1]
del line_hdrs[0]
self.encoding = ''
self.content_type = ''
headers = dict()
# Process other headers
for h in range(len(line_hdrs)):
line_comp = line_hdrs[h].split(':')
if line_comp[0] == 'Content-Length':
self.content_len = int(line_comp[1])
if line_comp[0] == 'Content-Type':
self.content_type = line_comp[1].lstrip()
if line_comp[0] == 'Transfer-Encoding':
self.encoding = line_comp[1].lstrip()
if len(line_comp) == 2:
headers[line_comp[0]] = line_comp[1].lstrip()
return headers
def read_resp_data(self):
read_data = ''
if self.encoding != 'chunked':
while len(read_data) != self.content_len:
read_data += self.client.recv(self.content_len)
self.content_len = 0
else:
chunk_data_buf = ''
while (True):
# Read one character into temp buffer
read_ch = self.client.recv(1)
# Check CRLF
if (read_ch == '\r'):
read_ch = self.client.recv(1)
if (read_ch == '\n'):
# If CRLF decode length of chunk
chunk_len = int(chunk_data_buf, 16)
# Keep adding to contents
self.content_len += chunk_len
read_data += self.client.recv(chunk_len)
chunk_data_buf = ''
# Fetch remaining CRLF
if self.client.recv(2) != "\r\n":
# Error in packet
return None
if not chunk_len:
# If last chunk
break
continue
chunk_data_buf += '\r'
# If not CRLF continue appending
# character to chunked data buffer
chunk_data_buf += read_ch
return read_data
def close(self):
self.client.close()
def test_val(text, expected, received):
if expected != received:
print " Fail!"
print " [reason] " + text + ":"
print " expected: " + str(expected)
print " received: " + str(received)
return False
return True
class myThread (threading.Thread):
def __init__(self, id, dut, port):
threading.Thread.__init__(self)
self.id = id
self.dut = dut
self.session = Session(dut, port)
def run(self):
self.response = []
# Pipeline 3 requests
if (_verbose_):
print " Thread: Using adder start " + str(self.id)
self.session.send_post('/adder', str(self.id));
time.sleep(1)
self.session.send_post('/adder', str(self.id));
time.sleep(1)
self.session.send_post('/adder', str(self.id));
time.sleep(1)
self.session.read_resp_hdrs()
self.response.append(self.session.read_resp_data())
self.session.read_resp_hdrs()
self.response.append(self.session.read_resp_data())
self.session.read_resp_hdrs()
self.response.append(self.session.read_resp_data())
def adder_result(self):
for i in range(len(self.response)):
# print self.response[i]
if not test_val("thread" + str(self.id) + ": response[" + str(i) + "]",
str(self.id * (i + 1)), str(self.response[i])):
return False
return True
def close(self):
self.session.close()
def get_hello(dut, port):
# GET /hello should return 'Hello World!'
print "[test] GET /hello returns 'Hello World!' =>",
r = requests.get("http://" + dut + ":" + port + "/hello")
if not test_val("status_code", 200, r.status_code):
return False
if not test_val("data", "Hello World!", r.text):
return False
if not test_val("data", "application/json", r.headers['Content-Type']):
return False
print "Success"
return True
def post_hello(dut, port):
# PUT /hello returns 405'
print "[test] PUT /hello returns 405' =>",
r = requests.put("http://" + dut + ":" + port + "/hello", data="Hello")
if not test_val("status_code", 405, r.status_code):
return False
print "Success"
return True
def put_hello(dut, port):
# POST /hello returns 405'
print "[test] POST /hello returns 404' =>",
r = requests.post("http://" + dut + ":" + port + "/hello", data="Hello")
if not test_val("status_code", 405, r.status_code):
return False
print "Success"
return True
def post_echo(dut, port):
# POST /echo echoes data'
print "[test] POST /echo echoes data' =>",
r = requests.post("http://" + dut + ":" + port + "/echo", data="Hello")
if not test_val("status_code", 200, r.status_code):
return False
if not test_val("data", "Hello", r.text):
return False
print "Success"
return True
def put_echo(dut, port):
# POST /echo echoes data'
print "[test] PUT /echo echoes data' =>",
r = requests.put("http://" + dut + ":" + port + "/echo", data="Hello")
if not test_val("status_code", 200, r.status_code):
return False
if not test_val("data", "Hello", r.text):
return False
print "Success"
return True
def get_echo(dut, port):
# GET /echo returns 404'
print "[test] GET /echo returns 405' =>",
r = requests.get("http://" + dut + ":" + port + "/echo")
if not test_val("status_code", 405, r.status_code):
return False
print "Success"
return True
def get_hello_type(dut, port):
# GET /hello/type_html returns text/html as Content-Type'
print "[test] GET /hello/type_html has Content-Type of text/html =>",
r = requests.get("http://" + dut + ":" + port + "/hello/type_html")
if not test_val("status_code", 200, r.status_code):
return False
if not test_val("data", "Hello World!", r.text):
return False
if not test_val("data", "text/html", r.headers['Content-Type']):
return False
print "Success"
return True
def get_hello_status(dut, port):
# GET /hello/status_500 returns status 500'
print "[test] GET /hello/status_500 returns status 500 =>",
r = requests.get("http://" + dut + ":" + port + "/hello/status_500")
if not test_val("status_code", 500, r.status_code):
return False
print "Success"
return True
def get_false_uri(dut, port):
# GET /false_uri returns status 404'
print "[test] GET /false_uri returns status 404 =>",
r = requests.get("http://" + dut + ":" + port + "/false_uri")
if not test_val("status_code", 404, r.status_code):
return False
print "Success"
return True
def parallel_sessions_adder(dut, port, max_sessions):
# POSTs on /adder in parallel sessions
print "[test] POST {pipelined} on /adder in " + str(max_sessions) + " sessions =>",
t = []
# Create all sessions
for i in xrange(max_sessions):
t.append(myThread(i * 2, dut, port))
for i in xrange(len(t)):
t[i].start()
for i in xrange(len(t)):
t[i].join()
res = True
for i in xrange(len(t)):
if not t[i].adder_result():
if not test_val("Thread" + str(i) + "Failed", "True", "False"):
res = False
t[i].close()
if (res):
print "Success"
return res
def async_response_test(dut, port):
# Test that an asynchronous work is executed in the HTTPD's context
# This is tested by reading two responses over the same session
print "[test] Test HTTPD Work Queue (Async response) =>",
s = Session(dut, port)
s.send_get('/async_data')
s.read_resp_hdrs()
if not test_val("First Response", "Hello World!", s.read_resp_data()):
s.close()
return False
s.read_resp_hdrs()
if not test_val("Second Response", "Hello Double World!", s.read_resp_data()):
s.close()
return False
s.close()
print "Success"
return True
def leftover_data_test(dut, port):
# Leftover data in POST is purged (valid and invalid URIs)
print "[test] Leftover data in POST is purged (valid and invalid URIs) =>",
s = Session(dut, port)
s.send_post('/leftover_data',
"abcdefghijklmnopqrstuvwxyz\r\nabcdefghijklmnopqrstuvwxyz")
s.read_resp_hdrs()
if not test_val("Partial data", "abcdefghij", s.read_resp_data()):
s.close()
return False
s.send_get('/hello')
s.read_resp_hdrs()
if not test_val("Hello World Data", "Hello World!", s.read_resp_data()):
s.close()
return False
s.send_post('/false_uri',
"abcdefghijklmnopqrstuvwxyz\r\nabcdefghijklmnopqrstuvwxyz")
s.read_resp_hdrs()
if not test_val("False URI Status", str(404), str(s.status)):
s.close()
return False
s.read_resp_data()
s.send_get('/hello')
s.read_resp_hdrs()
if not test_val("Hello World Data", "Hello World!", s.read_resp_data()):
s.close()
return False
s.close()
print "Success"
return True
def timeout_handler(signum, frame):
raise Exception("Timeout")
def spillover_session(dut, port, max):
# Session max_sessions + 1 is rejected
print "[test] Session max_sessions (" + str(max) + ") + 1 is rejected =>",
s = []
# Register a timeout callback
signal.signal(signal.SIGALRM, timeout_handler)
for i in xrange(max + 1):
if (_verbose_):
print "Executing " + str(i)
a = Session(dut, port)
a.send_get('/hello')
try:
# Check for response timeout
signal.alarm(5)
a.read_resp_hdrs()
a.read_resp_data()
signal.alarm(0)
# Control reaches here only if connection was established
s.append(a)
except Exception, msg:
if (_verbose_):
print str(msg) + ": Connection " + str(i) + " rejected"
break
# Close open connections
for a in s:
a.close()
# Check if number of connections is equal to max
print ["Fail","Success"][len(s) == max]
return (len(s) == max)
def recv_timeout_test(dut, port):
print "[test] Timeout occurs if partial packet sent =>",
signal.signal(signal.SIGALRM, timeout_handler)
s = Session(dut, port)
s.client.send("GE")
try:
signal.alarm(15)
s.read_resp_hdrs()
resp = s.read_resp_data()
signal.alarm(0)
if not test_val("Request Timeout", "Server closed this connection", resp):
s.close()
return False
except:
s.close()
return False
s.close()
print "Success"
return True
def pipeline_test(dut, port, max_sess):
print "[test] Pipelining test =>",
s = [Session(dut, port) for _ in range(max_sess)]
path = "/echo"
pipeline_depth = 10
header = "POST " + path + " HTTP/1.1\r\nHost: " + dut + "\r\nContent-Length: "
s[0].client.send(header)
for i in range(1, max_sess):
for j in range(pipeline_depth):
data = str(i) + ":" + str(j)
request = header + str(len(data)) + "\r\n\r\n" + data
s[i].client.send(request)
s[0].client.send(str(len("0:0")) + "\r\n\r\n" + "0:0")
for j in range(1, pipeline_depth):
data = "0:" + str(j)
request = header + str(len(data)) + "\r\n\r\n" + data
s[0].client.send(request)
res = True
for i in range(max_sess):
#time.sleep(1)
for j in range(pipeline_depth):
s[i].read_resp_hdrs()
echo_data = s[i].read_resp_data()
if (_verbose_):
print "[" + str(i) + "][" + str(j) + "] = " + echo_data
if not test_val("Echo Data", str(i) + ":" + str(j), echo_data):
res = False
s[i].close()
#for i in range(max_sess):
#s[i].close()
if (res):
print "Success"
return res
def packet_size_limit_test(dut, port, test_size):
print "[test] LWIP send size limit test =>",
s = Session(dut, port)
random_data = ''.join(string.printable[random.randint(0,len(string.printable))-1] for _ in range(test_size))
path = "/echo"
s.send_post(path, random_data)
s.read_resp_hdrs()
resp = s.read_resp_data()
result = (resp == random_data)
if not result:
test_val("Data size", str(len(random_data)), str(len(resp)))
s.close()
return False
print "Success"
s.close()
return True
def code_500_server_error_test(dut, port):
print "[test] 500 Server Error test =>",
s = Session(dut, port)
s.client.send("abcdefgh\0")
s.read_resp_hdrs()
resp = s.read_resp_data()
# Presently server sends back 400 Bad Request
#if not test_val("Server Error", "500", s.status):
#s.close()
#return False
if not test_val("Server Error", "400", s.status):
s.close()
return False
s.close()
print "Success"
return True
def code_501_method_not_impl(dut, port):
print "[test] 501 Method Not Implemented =>",
s = Session(dut, port)
path = "/hello"
s.client.send("ABC " + path + " HTTP/1.1\r\nHost: " + dut + "\r\n\r\n")
s.read_resp_hdrs()
resp = s.read_resp_data()
# Presently server sends back 400 Bad Request
#if not test_val("Server Error", "501", s.status):
#s.close()
#return False
if not test_val("Server Error", "400", s.status):
s.close()
return False
s.close()
print "Success"
return True
def code_505_version_not_supported(dut, port):
print "[test] 505 Version Not Supported =>",
s = Session(dut, port)
path = "/hello"
s.client.send("GET " + path + " HTTP/2.0\r\nHost: " + dut + "\r\n\r\n")
s.read_resp_hdrs()
resp = s.read_resp_data()
if not test_val("Server Error", "505", s.status):
s.close()
return False
s.close()
print "Success"
return True
def code_400_bad_request(dut, port):
print "[test] 400 Bad Request =>",
s = Session(dut, port)
path = "/hello"
s.client.send("XYZ " + path + " HTTP/1.1\r\nHost: " + dut + "\r\n\r\n")
s.read_resp_hdrs()
resp = s.read_resp_data()
if not test_val("Client Error", "400", s.status):
s.close()
return False
s.close()
print "Success"
return True
def code_404_not_found(dut, port):
print "[test] 404 Not Found =>",
s = Session(dut, port)
path = "/dummy"
s.client.send("GET " + path + " HTTP/1.1\r\nHost: " + dut + "\r\n\r\n")
s.read_resp_hdrs()
resp = s.read_resp_data()
if not test_val("Client Error", "404", s.status):
s.close()
return False
s.close()
print "Success"
return True
def code_405_method_not_allowed(dut, port):
print "[test] 405 Method Not Allowed =>",
s = Session(dut, port)
path = "/hello"
s.client.send("POST " + path + " HTTP/1.1\r\nHost: " + dut + "\r\n\r\n")
s.read_resp_hdrs()
resp = s.read_resp_data()
if not test_val("Client Error", "405", s.status):
s.close()
return False
s.close()
print "Success"
return True
def code_408_req_timeout(dut, port):
print "[test] 408 Request Timeout =>",
signal.signal(signal.SIGALRM, timeout_handler)
s = Session(dut, port)
s.client.send("POST /echo HTTP/1.1\r\nHost: " + dut + "\r\nContent-Length: 10\r\n\r\nABCD")
try:
signal.alarm(15)
s.read_resp_hdrs()
resp = s.read_resp_data()
signal.alarm(0)
if not test_val("Client Error", "408", s.status):
s.close()
return False
except:
s.close()
return False
s.close()
print "Success"
return True
def code_411_length_required(dut, port):
print "[test] 411 Length Required =>",
s = Session(dut, port)
path = "/echo"
s.client.send("POST " + path + " HTTP/1.1\r\nHost: " + dut + "\r\nContent-Type: text/plain\r\nTransfer-Encoding: chunked\r\n\r\n")
s.read_resp_hdrs()
resp = s.read_resp_data()
# Presently server sends back 400 Bad Request
#if not test_val("Client Error", "411", s.status):
#s.close()
#return False
if not test_val("Client Error", "400", s.status):
s.close()
return False
s.close()
print "Success"
return True
def send_getx_uri_len(dut, port, length):
s = Session(dut, port)
method = "GET "
version = " HTTP/1.1\r\n"
path = "/"+"x"*(length - len(method) - len(version) - len("/"))
s.client.send(method)
time.sleep(1)
s.client.send(path)
time.sleep(1)
s.client.send(version + "Host: " + dut + "\r\n\r\n")
s.read_resp_hdrs()
resp = s.read_resp_data()
s.close()
return s.status
def code_414_uri_too_long(dut, port, max_uri_len):
print "[test] 414 URI Too Long =>",
status = send_getx_uri_len(dut, port, max_uri_len)
if not test_val("Client Error", "404", status):
return False
status = send_getx_uri_len(dut, port, max_uri_len + 1)
if not test_val("Client Error", "414", status):
return False
print "Success"
return True
def send_postx_hdr_len(dut, port, length):
s = Session(dut, port)
path = "/echo"
host = "Host: " + dut
custom_hdr_field = "\r\nCustom: "
custom_hdr_val = "x"*(length - len(host) - len(custom_hdr_field) - len("\r\n\r\n") + len("0"))
request = "POST " + path + " HTTP/1.1\r\n" + host + custom_hdr_field + custom_hdr_val + "\r\n\r\n"
s.client.send(request[:length/2])
time.sleep(1)
s.client.send(request[length/2:])
hdr = s.read_resp_hdrs()
resp = s.read_resp_data()
s.close()
if "Custom" in hdr:
return (hdr["Custom"] == custom_hdr_val), resp
return False, s.status
def code_431_hdr_too_long(dut, port, max_hdr_len):
print "[test] 431 Header Too Long =>",
res, status = send_postx_hdr_len(dut, port, max_hdr_len)
if not res:
return False
res, status = send_postx_hdr_len(dut, port, max_hdr_len + 1)
if not test_val("Client Error", "431", status):
return False
print "Success"
return True
def test_upgrade_not_supported(dut, port):
print "[test] Upgrade Not Supported =>",
s = Session(dut, port)
path = "/hello"
s.client.send("OPTIONS * HTTP/1.1\r\nHost:" + dut + "\r\nUpgrade: TLS/1.0\r\nConnection: Upgrade\r\n\r\n");
s.read_resp_hdrs()
resp = s.read_resp_data()
if not test_val("Client Error", "200", s.status):
s.close()
return False
s.close()
print "Success"
return True
if __name__ == '__main__':
########### Execution begins here...
# Configuration
# Max number of threads/sessions
max_sessions = 7
max_uri_len = 512
max_hdr_len = 512
parser = argparse.ArgumentParser(description='Run HTTPD Test')
parser.add_argument('-4','--ipv4', help='IPv4 address')
parser.add_argument('-6','--ipv6', help='IPv6 address')
parser.add_argument('-p','--port', help='Port')
args = vars(parser.parse_args())
dut4 = args['ipv4']
dut6 = args['ipv6']
port = args['port']
dut = dut4
_verbose_ = True
print "### Basic HTTP Client Tests"
get_hello(dut, port)
post_hello(dut, port)
put_hello(dut, port)
post_echo(dut, port)
get_echo(dut, port)
put_echo(dut, port)
get_hello_type(dut, port)
get_hello_status(dut, port)
get_false_uri(dut, port)
print "### Error code tests"
code_500_server_error_test(dut, port)
code_501_method_not_impl(dut, port)
code_505_version_not_supported(dut, port)
code_400_bad_request(dut, port)
code_404_not_found(dut, port)
code_405_method_not_allowed(dut, port)
code_408_req_timeout(dut, port)
code_414_uri_too_long(dut, port, max_uri_len)
code_431_hdr_too_long(dut, port, max_hdr_len)
test_upgrade_not_supported(dut, port)
# Not supported yet (Error on chunked request)
###code_411_length_required(dut, port)
print "### Sessions and Context Tests"
parallel_sessions_adder(dut, port, max_sessions)
leftover_data_test(dut, port)
async_response_test(dut, port)
spillover_session(dut, port, max_sessions)
recv_timeout_test(dut, port)
# May timeout in case requests are sent slower than responses are read.
# Instead use httperf stress test
pipeline_test(dut, port, max_sessions)
packet_size_limit_test(dut, port, 50*1024)
get_hello(dut, port)
sys.exit()

View file

@ -0,0 +1,9 @@
#
# This is a project Makefile. It is assumed the directory this Makefile resides in is a
# project subdirectory.
#
PROJECT_NAME := persistent_sockets
include $(IDF_PATH)/make/project.mk

View file

@ -0,0 +1,18 @@
# HTTPD Server Persistant Sockets Example
The Example consists of HTTPD server persistent sockets demo.
This sort of persistancy enables the server to have independent sessions/contexts per client.
* Configure the project using "make menuconfig" and goto :
* Example Configuration ->
1. WIFI SSID: WIFI network to which your PC is also connected to.
2. WIFI Password: WIFI password
* In order to test the HTTPD server persistent sockets demo :
1. compile and burn the firmware "make flash"
2. run "make monitor" and note down the IP assigned to your ESP module. The default port is 80
3. run the test script "python2 scripts/adder.py \<IP\> \<port\> \<N\>"
* the provided test script sends (POST) numbers from 1 to N to the server which has a URI POST handler for adding these numbers into an accumulator that is valid throughout the lifetime of the connection socket, hence persistent
* the script does a GET before closing and displays the final value of the accumulator
See the README.md file in the upper level 'examples' directory for more information about examples.

View file

@ -0,0 +1,132 @@
#!/usr/bin/env python
#
# Copyright 2018 Espressif Systems (Shanghai) PTE LTD
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import imp
import re
import os
import sys
import string
import random
import socket
# This environment variable is expected on the host machine
test_fw_path = os.getenv("TEST_FW_PATH")
if test_fw_path and test_fw_path not in sys.path:
sys.path.insert(0, test_fw_path)
# When running on local machine execute the following before running this script
# > export TEST_FW_PATH='~/esp/esp-idf/tools/tiny-test-fw'
# > make print_flash_cmd | tail -n 1 > build/download.config
# > make app bootloader
import TinyFW
import IDF
# Import client module
expath = os.path.dirname(os.path.realpath(__file__))
client = imp.load_source("client", expath + "/scripts/adder.py")
@IDF.idf_example_test(env_tag="Example_WIFI")
def test_examples_protocol_http_server_persistence(env, extra_data):
# Acquire DUT
dut1 = env.get_dut("http_server", "examples/protocols/http_server/persistent_sockets")
# Get binary file
binary_file = os.path.join(dut1.app.binary_path, "persistent_sockets.bin")
bin_size = os.path.getsize(binary_file)
IDF.log_performance("http_server_bin_size", "{}KB".format(bin_size//1024))
IDF.check_performance("http_server_bin_size", bin_size//1024)
# Upload binary and start testing
dut1.start_app()
# Parse IP address of STA
got_ip = dut1.expect(re.compile(r"(?:[\s\S]*)Got IP: (\d+.\d+.\d+.\d+)"), timeout=30)[0]
got_port = dut1.expect(re.compile(r"(?:[\s\S]*)Starting server on port: (\d+)"))[0]
print "Got IP : " + got_ip
print "Got Port : " + got_port
# Expected Logs
dut1.expect("Registering URI handlers")
# Run test script
conn = client.start_session(got_ip, got_port)
visitor = 0
adder = 0
# Test PUT request and initialize session context
num = random.randint(0,100)
client.putreq(conn, "/adder", str(num))
visitor += 1
dut1.expect("/adder visitor count = " + str(visitor))
dut1.expect("/adder PUT handler read " + str(num))
dut1.expect("PUT allocating new session")
# Retest PUT request and change session context value
num = random.randint(0,100)
print "Adding :", num
client.putreq(conn, "/adder", str(num))
visitor += 1
adder += num
dut1.expect("/adder visitor count = " + str(visitor))
dut1.expect("/adder PUT handler read " + str(num))
try:
# Re allocation shouldn't happen
dut1.expect("PUT allocating new session")
# Not expected
raise RuntimeError
except:
# As expected
pass
# Test POST request and session persistence
random_nums = [random.randint(0,100) for _ in range(100)]
for num in random_nums:
print "Adding :", num
client.postreq(conn, "/adder", str(num))
visitor += 1
adder += num
dut1.expect("/adder visitor count = " + str(visitor))
dut1.expect("/adder handler read " + str(num))
# Test GET request and session persistence
print "Matching final sum :", adder
if client.getreq(conn, "/adder") != str(adder):
raise RuntimeError
visitor += 1
dut1.expect("/adder visitor count = " + str(visitor))
dut1.expect("/adder GET handler send " + str(adder))
print "Ending session"
# Close connection and check for invocation of context "Free" function
client.end_session(conn)
dut1.expect("/adder Free Context function called")
print "Validating user context data"
# Start another session to check user context data
conn2 = client.start_session(got_ip, got_port)
num = random.randint(0,100)
client.putreq(conn, "/adder", str(num))
visitor += 1
dut1.expect("/adder visitor count = " + str(visitor))
dut1.expect("/adder PUT handler read " + str(num))
dut1.expect("PUT allocating new session")
client.end_session(conn)
dut1.expect("/adder Free Context function called")
if __name__ == '__main__':
test_examples_protocol_http_server_persistence()

View file

@ -0,0 +1,16 @@
menu "Example Configuration"
config WIFI_SSID
string "WiFi SSID"
default "myssid"
help
SSID (network name) for the example to connect to.
config WIFI_PASSWORD
string "WiFi Password"
default "myssid"
help
WiFi password (WPA or WPA2) for the example to use.
Can be left blank if the network has no security set.
endmenu

View file

@ -0,0 +1,5 @@
#
# "main" pseudo-component makefile.
#
# (Uses default behaviour of compiling all source files in directory, adding 'include' to include path.)

View file

@ -0,0 +1,246 @@
/* Persistent Sockets Example
This example code is in the Public Domain (or CC0 licensed, at your option.)
Unless required by applicable law or agreed to in writing, this
software is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
CONDITIONS OF ANY KIND, either express or implied.
*/
#include <esp_wifi.h>
#include <esp_event_loop.h>
#include <esp_log.h>
#include <esp_system.h>
#include <nvs_flash.h>
#include <http_server.h>
/* An example to demonstrate persistent sockets, with context maintained across
* multiple requests on that socket.
* The examples use simple WiFi configuration that you can set via 'make menuconfig'.
* If you'd rather not, just change the below entries to strings with
* the config you want - ie #define EXAMPLE_WIFI_SSID "mywifissid"
*/
#define EXAMPLE_WIFI_SSID CONFIG_WIFI_SSID
#define EXAMPLE_WIFI_PASS CONFIG_WIFI_PASSWORD
static const char *TAG="APP";
/* Function to free context */
void adder_free_func(void *ctx)
{
ESP_LOGI(TAG, "/adder Free Context function called");
free(ctx);
}
/* This handler keeps accumulating data that is posted to it into a per
* socket/session context. And returns the result.
*/
esp_err_t adder_post_handler(httpd_req_t *req)
{
/* Log total visitors */
unsigned *visitors = (unsigned *)req->user_ctx;
ESP_LOGI(TAG, "/adder visitor count = %d", ++(*visitors));
char buf[10];
char outbuf[50];
int ret;
/* Read data received in the request */
ret = httpd_req_recv(req, buf, sizeof(buf));
if (ret < 0) {
return ESP_FAIL;
}
buf[ret] = '\0';
int val = atoi(buf);
ESP_LOGI(TAG, "/adder handler read %d", val);
/* Create session's context if not already available */
if (! req->sess_ctx) {
ESP_LOGI(TAG, "/adder allocating new session");
req->sess_ctx = malloc(sizeof(int));
req->free_ctx = adder_free_func;
*(int *)req->sess_ctx = 0;
}
/* Add the received data to the context */
int *adder = (int *)req->sess_ctx;
*adder += val;
/* Respond with the accumulated value */
snprintf(outbuf, sizeof(outbuf),"%d", *adder);
httpd_resp_send(req, outbuf, strlen(outbuf));
return ESP_OK;
}
/* This handler gets the present value of the accumulator */
esp_err_t adder_get_handler(httpd_req_t *req)
{
/* Log total visitors */
unsigned *visitors = (unsigned *)req->user_ctx;
ESP_LOGI(TAG, "/adder visitor count = %d", ++(*visitors));
char outbuf[50];
/* Create session's context if not already available */
if (! req->sess_ctx) {
ESP_LOGI(TAG, "/adder GET allocating new session");
req->sess_ctx = malloc(sizeof(int));
req->free_ctx = adder_free_func;
*(int *)req->sess_ctx = 0;
}
ESP_LOGI(TAG, "/adder GET handler send %d", *(int *)req->sess_ctx);
/* Respond with the accumulated value */
snprintf(outbuf, sizeof(outbuf),"%d", *((int *)req->sess_ctx));
httpd_resp_send(req, outbuf, strlen(outbuf));
return ESP_OK;
}
/* This handler resets the value of the accumulator */
esp_err_t adder_put_handler(httpd_req_t *req)
{
/* Log total visitors */
unsigned *visitors = (unsigned *)req->user_ctx;
ESP_LOGI(TAG, "/adder visitor count = %d", ++(*visitors));
char buf[10];
char outbuf[50];
int ret;
/* Read data received in the request */
ret = httpd_req_recv(req, buf, sizeof(buf));
if (ret < 0) {
return ESP_FAIL;
}
buf[ret] = '\0';
int val = atoi(buf);
ESP_LOGI(TAG, "/adder PUT handler read %d", val);
/* Create session's context if not already available */
if (! req->sess_ctx) {
ESP_LOGI(TAG, "/adder PUT allocating new session");
req->sess_ctx = malloc(sizeof(int));
req->free_ctx = adder_free_func;
}
*(int *)req->sess_ctx = val;
/* Respond with the reset value */
snprintf(outbuf, sizeof(outbuf),"%d", *((int *)req->sess_ctx));
httpd_resp_send(req, outbuf, strlen(outbuf));
return ESP_OK;
}
/* Maintain a variable which stores the number of times
* the "/adder" URI has been visited */
static unsigned visitors = 0;
httpd_uri_t adder_post = {
.uri = "/adder",
.method = HTTP_POST,
.handler = adder_post_handler,
.user_ctx = &visitors
};
httpd_uri_t adder_get = {
.uri = "/adder",
.method = HTTP_GET,
.handler = adder_get_handler,
.user_ctx = &visitors
};
httpd_uri_t adder_put = {
.uri = "/adder",
.method = HTTP_PUT,
.handler = adder_put_handler,
.user_ctx = &visitors
};
httpd_handle_t start_webserver(void)
{
httpd_config_t config = HTTPD_DEFAULT_CONFIG();
// Start the httpd server
ESP_LOGI(TAG, "Starting server on port: %d", config.server_port);
httpd_handle_t server;
if (httpd_start(&server, &config) == ESP_OK) {
// Set URI handlers
ESP_LOGI(TAG, "Registering URI handlers");
httpd_register_uri_handler(server, &adder_get);
httpd_register_uri_handler(server, &adder_put);
httpd_register_uri_handler(server, &adder_post);
return server;
}
ESP_LOGI(TAG, "Error starting server!");
return NULL;
}
void stop_webserver(httpd_handle_t server)
{
// Stop the httpd server
httpd_stop(server);
}
static esp_err_t event_handler(void *ctx, system_event_t *event)
{
httpd_handle_t *server = (httpd_handle_t *) ctx;
switch(event->event_id) {
case SYSTEM_EVENT_STA_START:
ESP_LOGI(TAG, "SYSTEM_EVENT_STA_START");
ESP_ERROR_CHECK(esp_wifi_connect());
break;
case SYSTEM_EVENT_STA_GOT_IP:
ESP_LOGI(TAG, "SYSTEM_EVENT_STA_GOT_IP");
ESP_LOGI(TAG, "Got IP: %s",
ip4addr_ntoa(&event->event_info.got_ip.ip_info.ip));
/* Start the web server */
if (*server == NULL) {
*server = start_webserver();
}
break;
case SYSTEM_EVENT_STA_DISCONNECTED:
ESP_LOGI(TAG, "SYSTEM_EVENT_STA_DISCONNECTED");
ESP_ERROR_CHECK(esp_wifi_connect());
/* Stop the webserver */
if (*server) {
stop_webserver(*server);
*server = NULL;
}
break;
default:
break;
}
return ESP_OK;
}
static void initialise_wifi(void *arg)
{
tcpip_adapter_init();
ESP_ERROR_CHECK(esp_event_loop_init(event_handler, arg));
wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
ESP_ERROR_CHECK(esp_wifi_init(&cfg));
ESP_ERROR_CHECK(esp_wifi_set_storage(WIFI_STORAGE_RAM));
wifi_config_t wifi_config = {
.sta = {
.ssid = EXAMPLE_WIFI_SSID,
.password = EXAMPLE_WIFI_PASS,
},
};
ESP_LOGI(TAG, "Setting WiFi configuration SSID %s...", wifi_config.sta.ssid);
ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA));
ESP_ERROR_CHECK(esp_wifi_set_config(ESP_IF_WIFI_STA, &wifi_config));
ESP_ERROR_CHECK(esp_wifi_start());
}
void app_main()
{
static httpd_handle_t server = NULL;
ESP_ERROR_CHECK(nvs_flash_init());
initialise_wifi(&server);
}

View file

@ -0,0 +1,94 @@
#!/usr/bin/env python
#
# Copyright 2018 Espressif Systems (Shanghai) PTE LTD
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import httplib
import argparse
def start_session (ip, port):
return httplib.HTTPConnection(ip, int(port))
def end_session (conn):
conn.close()
def getreq (conn, path, verbose = False):
conn.request("GET", path)
resp = conn.getresponse()
data = resp.read()
if verbose:
print "GET : ", path
print "Status : ", resp.status
print "Reason : ", resp.reason
print "Data length : ", len(data)
print "Data content : ", data
return data
def postreq (conn, path, data, verbose = False):
conn.request("POST", path, data)
resp = conn.getresponse()
data = resp.read()
if verbose:
print "POST : ", data
print "Status : ", resp.status
print "Reason : ", resp.reason
print "Data length : ", len(data)
print "Data content : ", data
return data
def putreq (conn, path, body, verbose = False):
conn.request("PUT", path, body)
resp = conn.getresponse()
data = resp.read()
if verbose:
print "PUT : ", path, body
print "Status : ", resp.status
print "Reason : ", resp.reason
print "Data length : ", len(data)
print "Data content : ", data
return data
if __name__ == '__main__':
# Configure argument parser
parser = argparse.ArgumentParser(description='Run HTTPd Test')
parser.add_argument('IP' , metavar='IP' , type=str, help='Server IP')
parser.add_argument('port', metavar='port', type=str, help='Server port')
parser.add_argument('N' , metavar='integer', type=int, help='Integer to sum upto')
args = vars(parser.parse_args())
# Get arguments
ip = args['IP']
port = args['port']
N = args['N']
# Establish HTTP connection
print "Connecting to => " + ip + ":" + port
conn = start_session (ip, port)
# Reset adder context to specified value(0)
# -- Not needed as new connection will always
# -- have zero value of the accumulator
print "Reset the accumulator to 0"
putreq (conn, "/adder", str(0))
# Sum numbers from 1 to specified value(N)
print "Summing numbers from 1 to " + str(N)
for i in xrange(1, N+1):
postreq (conn, "/adder", str(i))
# Fetch the result
print "Result :", getreq (conn, "/adder")
# Close HTTP connection
end_session (conn)

View file

@ -0,0 +1,9 @@
#
# This is a project Makefile. It is assumed the directory this Makefile resides in is a
# project subdirectory.
#
PROJECT_NAME := simple
include $(IDF_PATH)/make/project.mk

View file

@ -0,0 +1,27 @@
# Simple HTTPD Server Example
The Example consists of HTTPD server demo with demostration of URI handling :
1. URI \hello for GET command returns "Hello World!" message
2. URI \echo for POST command echoes back the POSTed message
* Configure the project using "make menuconfig" and goto :
* Example Configuration ->
1. WIFI SSID: WIFI network to which your PC is also connected to.
2. WIFI Password: WIFI password
* In order to test the HTTPD server persistent sockets demo :
1. compile and burn the firmware "make flash"
2. run "make monitor" and note down the IP assigned to your ESP module. The default port is 80
3. test the example :
* run the test script : "python2 scripts/client.py \<IP\> \<port\> \<MSG\>"
* the provided test script first does a GET \hello and displays the response
* the script does a POST to \echo with the user input \<MSG\> and displays the response
* or use curl (asssuming IP is 192.168.43.130):
1. "curl 192.168.43.130:80/hello" - tests the GET "\hello" handler
2. "curl -X POST --data-binary @anyfile 192.168.43.130:80/echo > tmpfile"
* "anyfile" is the file being sent as request body and "tmpfile" is where the body of the response is saved
* since the server echoes back the request body, the two files should be same, as can be confirmed using : "cmp anyfile tmpfile"
3. "curl -X PUT -d "0" 192.168.43.130:80/ctrl" - disable /hello and /echo handlers
4. "curl -X PUT -d "1" 192.168.43.130:80/ctrl" - enable /hello and /echo handlers
See the README.md file in the upper level 'examples' directory for more information about examples.

View file

@ -0,0 +1,114 @@
#!/usr/bin/env python
#
# Copyright 2018 Espressif Systems (Shanghai) PTE LTD
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import imp
import re
import os
import sys
import string
import random
import socket
# This environment variable is expected on the host machine
test_fw_path = os.getenv("TEST_FW_PATH")
if test_fw_path and test_fw_path not in sys.path:
sys.path.insert(0, test_fw_path)
# When running on local machine execute the following before running this script
# > export TEST_FW_PATH='~/esp/esp-idf/tools/tiny-test-fw'
# > make print_flash_cmd | tail -n 1 > build/download.config
# > make app bootloader
import TinyFW
import IDF
# Import client module
expath = os.path.dirname(os.path.realpath(__file__))
client = imp.load_source("client", expath + "/scripts/client.py")
@IDF.idf_example_test(env_tag="Example_WIFI")
def test_examples_protocol_http_server_simple(env, extra_data):
# Acquire DUT
dut1 = env.get_dut("http_server", "examples/protocols/http_server/simple")
# Get binary file
binary_file = os.path.join(dut1.app.binary_path, "simple.bin")
bin_size = os.path.getsize(binary_file)
IDF.log_performance("http_server_bin_size", "{}KB".format(bin_size//1024))
IDF.check_performance("http_server_bin_size", bin_size//1024)
# Upload binary and start testing
dut1.start_app()
# Parse IP address of STA
got_ip = dut1.expect(re.compile(r"(?:[\s\S]*)Got IP: (\d+.\d+.\d+.\d+)"), timeout=30)[0]
got_port = dut1.expect(re.compile(r"(?:[\s\S]*)Starting server on port: (\d+)"))[0]
print "Got IP : " + got_ip
print "Got Port : " + got_port
# Expected Logs
dut1.expect("Registering URI handlers")
# Run test script
# If failed raise appropriate exception
print "Test /hello GET handler"
if not client.test_get_handler(got_ip, got_port):
raise RuntimeError
# Acquire host IP. Need a way to check it
host_ip = dut1.expect(re.compile(r"(?:[\s\S]*)Found header => Host: (\d+.\d+.\d+.\d+)"))[0]
# Match additional headers sent in the request
dut1.expect("Found header => Test-Header-2: Test-Value-2")
dut1.expect("Found header => Test-Header-1: Test-Value-1")
dut1.expect("Found URL query parameter => query1=value1")
dut1.expect("Found URL query parameter => query3=value3")
dut1.expect("Found URL query parameter => query2=value2")
dut1.expect("Request headers lost")
print "Test /ctrl PUT handler and realtime handler de/registration"
if not client.test_put_handler(got_ip, got_port):
raise RuntimeError
dut1.expect("Unregistering /hello and /echo URIs")
dut1.expect("Registering /hello and /echo URIs")
# Generate random data of 10KB
random_data = ''.join(string.printable[random.randint(0,len(string.printable))-1] for _ in range(1024*10))
print "Test /echo POST handler with random data"
if not client.test_post_handler(got_ip, got_port, random_data):
raise RuntimeError
query = "http://foobar"
print "Test /hello with custom query : " + query
if not client.test_custom_uri_query(got_ip, got_port, query):
raise RuntimeError
dut1.expect("Found URL query => " + query)
query = "abcd+1234%20xyz"
print "Test /hello with custom query : " + query
if not client.test_custom_uri_query(got_ip, got_port, query):
raise RuntimeError
dut1.expect("Found URL query => " + query)
query = "abcd\nyz"
print "Test /hello with invalid query"
if client.test_custom_uri_query(got_ip, got_port, query):
raise RuntimeError
dut1.expect("400 Bad Request - Server unable to understand request due to invalid syntax")
if __name__ == '__main__':
test_examples_protocol_http_server_simple()

View file

@ -0,0 +1,16 @@
menu "Example Configuration"
config WIFI_SSID
string "WiFi SSID"
default "myssid"
help
SSID (network name) for the example to connect to.
config WIFI_PASSWORD
string "WiFi Password"
default "myssid"
help
WiFi password (WPA or WPA2) for the example to use.
Can be left blank if the network has no security set.
endmenu

View file

@ -0,0 +1,5 @@
#
# "main" pseudo-component makefile.
#
# (Uses default behaviour of compiling all source files in directory, adding 'include' to include path.)

View file

@ -0,0 +1,272 @@
/* Simple HTTP Server Example
This example code is in the Public Domain (or CC0 licensed, at your option.)
Unless required by applicable law or agreed to in writing, this
software is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
CONDITIONS OF ANY KIND, either express or implied.
*/
#include <esp_wifi.h>
#include <esp_event_loop.h>
#include <esp_log.h>
#include <esp_system.h>
#include <nvs_flash.h>
#include <sys/param.h>
#include <http_server.h>
/* A simple example that demonstrates how to create GET and POST
* handlers for the web server.
* The examples use simple WiFi configuration that you can set via
* 'make menuconfig'.
* If you'd rather not, just change the below entries to strings
* with the config you want -
* ie. #define EXAMPLE_WIFI_SSID "mywifissid"
*/
#define EXAMPLE_WIFI_SSID CONFIG_WIFI_SSID
#define EXAMPLE_WIFI_PASS CONFIG_WIFI_PASSWORD
static const char *TAG="APP";
/* An HTTP GET handler */
esp_err_t hello_get_handler(httpd_req_t *req)
{
char* buf;
size_t buf_len;
/* Get header value string length and allocate memory for length + 1,
* extra byte for null termination */
buf_len = httpd_req_get_hdr_value_len(req, "Host") + 1;
if (buf_len > 1) {
buf = malloc(buf_len);
/* Copy null terminated value string into buffer */
if (httpd_req_get_hdr_value_str(req, "Host", buf, buf_len) == ESP_OK) {
ESP_LOGI(TAG, "Found header => Host: %s", buf);
}
free(buf);
}
buf_len = httpd_req_get_hdr_value_len(req, "Test-Header-2") + 1;
if (buf_len > 1) {
buf = malloc(buf_len);
if (httpd_req_get_hdr_value_str(req, "Test-Header-2", buf, buf_len) == ESP_OK) {
ESP_LOGI(TAG, "Found header => Test-Header-2: %s", buf);
}
free(buf);
}
buf_len = httpd_req_get_hdr_value_len(req, "Test-Header-1") + 1;
if (buf_len > 1) {
buf = malloc(buf_len);
if (httpd_req_get_hdr_value_str(req, "Test-Header-1", buf, buf_len) == ESP_OK) {
ESP_LOGI(TAG, "Found header => Test-Header-1: %s", buf);
}
free(buf);
}
/* Read URL query string length and allocate memory for length + 1,
* extra byte for null termination */
buf_len = httpd_req_get_url_query_len(req) + 1;
if (buf_len > 1) {
buf = malloc(buf_len);
if (httpd_req_get_url_query_str(req, buf, buf_len) == ESP_OK) {
ESP_LOGI(TAG, "Found URL query => %s", buf);
char param[32];
/* Get value of expected key from query string */
if (httpd_query_key_value(buf, "query1", param, sizeof(param)) == ESP_OK) {
ESP_LOGI(TAG, "Found URL query parameter => query1=%s", param);
}
if (httpd_query_key_value(buf, "query3", param, sizeof(param)) == ESP_OK) {
ESP_LOGI(TAG, "Found URL query parameter => query3=%s", param);
}
if (httpd_query_key_value(buf, "query2", param, sizeof(param)) == ESP_OK) {
ESP_LOGI(TAG, "Found URL query parameter => query2=%s", param);
}
}
free(buf);
}
/* Set some custom headers */
httpd_resp_set_hdr(req, "Custom-Header-1", "Custom-Value-1");
httpd_resp_set_hdr(req, "Custom-Header-2", "Custom-Value-2");
/* Send response with custom headers and body set as the
* string passed in user context*/
const char* resp_str = (const char*) req->user_ctx;
httpd_resp_send(req, resp_str, strlen(resp_str));
/* After sending the HTTP response the old HTTP request
* headers are lost. Check if HTTP request headers can be read now. */
if (httpd_req_get_hdr_value_len(req, "Host") == 0) {
ESP_LOGI(TAG, "Request headers lost");
}
return ESP_OK;
}
httpd_uri_t hello = {
.uri = "/hello",
.method = HTTP_GET,
.handler = hello_get_handler,
/* Let's pass response string in user
* context to demonstrate it's usage */
.user_ctx = "Hello World!"
};
/* An HTTP POST handler */
esp_err_t echo_post_handler(httpd_req_t *req)
{
char buf[100];
int ret, remaining = req->content_len;
while (remaining > 0) {
/* Read the data for the request */
if ((ret = httpd_req_recv(req, buf,
MIN(remaining, sizeof(buf)))) < 0) {
return ESP_FAIL;
}
/* Send back the same data */
httpd_resp_send_chunk(req, buf, ret);
remaining -= ret;
/* Log data received */
ESP_LOGI(TAG, "=========== RECEIVED DATA ==========");
ESP_LOGI(TAG, "%.*s", ret, buf);
ESP_LOGI(TAG, "====================================");
}
// End response
httpd_resp_send_chunk(req, NULL, 0);
return ESP_OK;
}
httpd_uri_t echo = {
.uri = "/echo",
.method = HTTP_POST,
.handler = echo_post_handler,
.user_ctx = NULL
};
/* An HTTP PUT handler. This demonstrates realtime
* registration and deregistration of URI handlers
*/
esp_err_t ctrl_put_handler(httpd_req_t *req)
{
char buf;
int ret;
if ((ret = httpd_req_recv(req, &buf, 1)) < 0) {
return ESP_FAIL;
}
if (buf == '0') {
/* Handler can be unregistered using the uri string */
ESP_LOGI(TAG, "Unregistering /hello and /echo URIs");
httpd_unregister_uri(req->handle, "/hello");
httpd_unregister_uri(req->handle, "/echo");
}
else {
ESP_LOGI(TAG, "Registering /hello and /echo URIs");
httpd_register_uri_handler(req->handle, &hello);
httpd_register_uri_handler(req->handle, &echo);
}
/* Respond with empty body */
httpd_resp_send(req, NULL, 0);
return ESP_OK;
}
httpd_uri_t ctrl = {
.uri = "/ctrl",
.method = HTTP_PUT,
.handler = ctrl_put_handler,
.user_ctx = NULL
};
httpd_handle_t start_webserver(void)
{
httpd_handle_t server = NULL;
httpd_config_t config = HTTPD_DEFAULT_CONFIG();
// Start the httpd server
ESP_LOGI(TAG, "Starting server on port: %d", config.server_port);
if (httpd_start(&server, &config) == ESP_OK) {
// Set URI handlers
ESP_LOGI(TAG, "Registering URI handlers");
httpd_register_uri_handler(server, &hello);
httpd_register_uri_handler(server, &echo);
httpd_register_uri_handler(server, &ctrl);
return server;
}
ESP_LOGI(TAG, "Error starting server!");
return NULL;
}
void stop_webserver(httpd_handle_t server)
{
// Stop the httpd server
httpd_stop(server);
}
static esp_err_t event_handler(void *ctx, system_event_t *event)
{
httpd_handle_t *server = (httpd_handle_t *) ctx;
switch(event->event_id) {
case SYSTEM_EVENT_STA_START:
ESP_LOGI(TAG, "SYSTEM_EVENT_STA_START");
ESP_ERROR_CHECK(esp_wifi_connect());
break;
case SYSTEM_EVENT_STA_GOT_IP:
ESP_LOGI(TAG, "SYSTEM_EVENT_STA_GOT_IP");
ESP_LOGI(TAG, "Got IP: %s",
ip4addr_ntoa(&event->event_info.got_ip.ip_info.ip));
/* Start the web server */
if (*server == NULL) {
*server = start_webserver();
}
break;
case SYSTEM_EVENT_STA_DISCONNECTED:
ESP_LOGI(TAG, "SYSTEM_EVENT_STA_DISCONNECTED");
ESP_ERROR_CHECK(esp_wifi_connect());
/* Stop the web server */
if (*server) {
stop_webserver(*server);
*server = NULL;
}
break;
default:
break;
}
return ESP_OK;
}
static void initialise_wifi(void *arg)
{
tcpip_adapter_init();
ESP_ERROR_CHECK(esp_event_loop_init(event_handler, arg));
wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
ESP_ERROR_CHECK(esp_wifi_init(&cfg));
ESP_ERROR_CHECK(esp_wifi_set_storage(WIFI_STORAGE_RAM));
wifi_config_t wifi_config = {
.sta = {
.ssid = EXAMPLE_WIFI_SSID,
.password = EXAMPLE_WIFI_PASS,
},
};
ESP_LOGI(TAG, "Setting WiFi configuration SSID %s...", wifi_config.sta.ssid);
ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA));
ESP_ERROR_CHECK(esp_wifi_set_config(ESP_IF_WIFI_STA, &wifi_config));
ESP_ERROR_CHECK(esp_wifi_start());
}
void app_main()
{
static httpd_handle_t server = NULL;
ESP_ERROR_CHECK(nvs_flash_init());
initialise_wifi(&server);
}

View file

@ -0,0 +1,258 @@
#!/usr/bin/env python
#
# Copyright 2018 Espressif Systems (Shanghai) PTE LTD
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import socket
import argparse
class Session:
def __init__(self, addr, port):
self.client = socket.create_connection((addr, int(port)))
self.target = addr
def send_get(self, path, headers=None):
request = "GET " + path + " HTTP/1.1\r\nHost: " + self.target
if headers:
for field, value in headers.iteritems():
request += "\r\n"+field+": "+value
request += "\r\n\r\n"
self.client.send(request);
def send_put(self, path, data, headers=None):
request = "PUT " + path + " HTTP/1.1\r\nHost: " + self.target
if headers:
for field, value in headers.iteritems():
request += "\r\n"+field+": "+value
request += "\r\nContent-Length: " + str(len(data)) +"\r\n\r\n"
self.client.send(request)
self.client.send(data)
def send_post(self, path, data, headers=None):
request = "POST " + path + " HTTP/1.1\r\nHost: " + self.target
if headers:
for field, value in headers.iteritems():
request += "\r\n"+field+": "+value
request += "\r\nContent-Length: " + str(len(data)) +"\r\n\r\n"
self.client.send(request)
self.client.send(data)
def read_resp_hdrs(self):
state = 'nothing'
resp_read = ''
while True:
char = self.client.recv(1)
if char == '\r' and state == 'nothing':
state = 'first_cr'
elif char == '\n' and state == 'first_cr':
state = 'first_lf'
elif char == '\r' and state == 'first_lf':
state = 'second_cr'
elif char == '\n' and state == 'second_cr':
state = 'second_lf'
else:
state = 'nothing'
resp_read += char
if state == 'second_lf':
break;
# Handle first line
line_hdrs = resp_read.splitlines()
line_comp = line_hdrs[0].split()
self.status = line_comp[1]
del line_hdrs[0]
self.encoding = ''
self.content_type = ''
headers = dict()
# Process other headers
for h in range(len(line_hdrs)):
line_comp = line_hdrs[h].split(':')
if line_comp[0] == 'Content-Length':
self.content_len = int(line_comp[1])
if line_comp[0] == 'Content-Type':
self.content_type = line_comp[1].lstrip()
if line_comp[0] == 'Transfer-Encoding':
self.encoding = line_comp[1].lstrip()
if len(line_comp) == 2:
headers[line_comp[0]] = line_comp[1].lstrip()
return headers
def read_resp_data(self):
read_data = ''
if self.encoding != 'chunked':
while len(read_data) != self.content_len:
read_data += self.client.recv(self.content_len)
self.content_len = 0
else:
chunk_data_buf = ''
while (True):
# Read one character into temp buffer
read_ch = self.client.recv(1)
# Check CRLF
if (read_ch == '\r'):
read_ch = self.client.recv(1)
if (read_ch == '\n'):
# If CRLF decode length of chunk
chunk_len = int(chunk_data_buf, 16)
# Keep adding to contents
self.content_len += chunk_len
read_data += self.client.recv(chunk_len)
chunk_data_buf = ''
# Fetch remaining CRLF
if self.client.recv(2) != "\r\n":
# Error in packet
return None
if not chunk_len:
# If last chunk
break
continue
chunk_data_buf += '\r'
# If not CRLF continue appending
# character to chunked data buffer
chunk_data_buf += read_ch
return read_data
def close(self):
self.client.close()
def verbose_print(verbosity, *args):
if (verbosity):
print ''.join(str(elems) for elems in args)
def test_get_handler(ip, port, verbosity = False):
verbose_print(verbosity, "======== GET HANDLER TEST =============")
# Establish HTTP connection
verbose_print(verbosity, "Connecting to => " + ip + ":" + port)
sess = Session(ip, port)
uri = "/hello?query1=value1&query2=value2&query3=value3"
# GET hello response
test_headers = {"Test-Header-1":"Test-Value-1", "Test-Header-2":"Test-Value-2"}
verbose_print(verbosity, "Sending GET to URI : ", uri)
verbose_print(verbosity, "Sending additional headers : ")
for k, v in test_headers.iteritems():
verbose_print(verbosity, "\t", k, ": ", v)
sess.send_get(uri, test_headers)
resp_hdrs = sess.read_resp_hdrs()
resp_data = sess.read_resp_data()
try:
if resp_hdrs["Custom-Header-1"] != "Custom-Value-1":
return False
if resp_hdrs["Custom-Header-2"] != "Custom-Value-2":
return False
except:
return False
verbose_print(verbosity, "vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv")
verbose_print(verbosity, "Server response to GET /hello")
verbose_print(verbosity, "Response Headers : ")
for k, v in resp_hdrs.iteritems():
verbose_print(verbosity, "\t", k, ": ", v)
verbose_print(verbosity, "Response Data : " + resp_data)
verbose_print(verbosity, "========================================\n")
# Close HTTP connection
sess.close()
return (resp_data == "Hello World!")
def test_post_handler(ip, port, msg, verbosity = False):
verbose_print(verbosity, "======== POST HANDLER TEST ============")
# Establish HTTP connection
verbose_print(verbosity, "Connecting to => " + ip + ":" + port)
sess = Session(ip, port)
# POST message to /echo and get back response
sess.send_post("/echo", msg)
resp_hdrs = sess.read_resp_hdrs()
resp_data = sess.read_resp_data()
verbose_print(verbosity, "Server response to POST /echo (" + msg + ")")
verbose_print(verbosity, "vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv")
verbose_print(verbosity, resp_data)
verbose_print(verbosity, "========================================\n")
# Close HTTP connection
sess.close()
return (resp_data == msg)
def test_put_handler(ip, port, verbosity = False):
verbose_print(verbosity, "======== PUT HANDLER TEST =============")
# Establish HTTP connection
verbose_print(verbosity, "Connecting to => " + ip + ":" + port)
sess = Session(ip, port)
# PUT message to /ctrl to disable /hello URI handler
verbose_print(verbosity, "Disabling /hello handler")
sess.send_put("/ctrl", "0")
sess.read_resp_hdrs()
if sess.content_len:
sess.read_resp_data()
sess.send_get("/hello")
sess.read_resp_hdrs()
resp_data1 = sess.read_resp_data()
verbose_print(verbosity, "Response on GET /hello : " + resp_data1)
# PUT message to /ctrl to enable /hello URI handler
verbose_print(verbosity, "Enabling /hello handler")
sess.send_put("/ctrl", "1")
sess.read_resp_hdrs()
if sess.content_len:
sess.read_resp_data()
sess.send_get("/hello")
sess.read_resp_hdrs()
resp_data2 = sess.read_resp_data()
verbose_print(verbosity, "Response on GET /hello : " + resp_data2)
# Close HTTP connection
sess.close()
return ((resp_data2 == "Hello World!") and (resp_data1 == "This URI doesn't exist"))
def test_custom_uri_query(ip, port, query, verbosity = False):
verbose_print(verbosity, "======== GET HANDLER TEST =============")
# Establish HTTP connection
verbose_print(verbosity, "Connecting to => " + ip + ":" + port)
sess = Session(ip, port)
uri = "/hello?" + query
# GET hello response
verbose_print(verbosity, "Sending GET to URI : ", uri)
sess.send_get(uri, {})
resp_hdrs = sess.read_resp_hdrs()
resp_data = sess.read_resp_data()
verbose_print(verbosity, "vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv")
verbose_print(verbosity, "Server response to GET /hello")
verbose_print(verbosity, "Response Data : " + resp_data)
verbose_print(verbosity, "========================================\n")
# Close HTTP connection
sess.close()
return (resp_data == "Hello World!")
if __name__ == '__main__':
# Configure argument parser
parser = argparse.ArgumentParser(description='Run HTTPd Test')
parser.add_argument('IP' , metavar='IP' , type=str, help='Server IP')
parser.add_argument('port', metavar='port', type=str, help='Server port')
parser.add_argument('msg', metavar='message', type=str, help='Message to be sent to server')
args = vars(parser.parse_args())
# Get arguments
ip = args['IP']
port = args['port']
msg = args['msg']
test_get_handler (ip, port, True)
test_post_handler(ip, port, msg, True)
test_put_handler (ip, port, True)