SpriteHTTPD - embedded HTTP server with read-only filesystem and templating, originally developed for ESP8266, now stand-alone and POSIX compatible.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
spritehttpd/lib/todo/esphttpclient/httpclient.c

590 lines
14 KiB

/*
* ----------------------------------------------------------------------------
* "THE BEER-WARE LICENSE" (Revision 42):
* Martin d'Allens <martin.dallens@gmail.com> wrote this file. As long as you retain
* this notice you can do whatever you want with this stuff. If we meet some day,
* and you think this stuff is worth it, you can buy me a beer in return.
* ----------------------------------------------------------------------------
*/
// FIXME: sprintf->snprintf everywhere.
#include <esp8266.h>
#include <httpd.h>
#include <httpclient.h>
#include "httpclient.h"
#include "esp_utils.h"
// Internal state.
typedef struct {
char *path;
int port;
char *post_data;
char *headers;
char *hostname;
char *buffer;
int buffer_size;
int max_buffer_size;
bool secure;
httpclient_cb user_callback;
int timeout;
ETSTimer timeout_timer;
httpd_method method;
void *userData;
} request_args;
static int ICACHE_FLASH_ATTR
chunked_decode(char *chunked, int size)
{
char *src = chunked;
char *end = chunked + size;
int i, dst = 0;
do {
char *endstr = NULL;
//[chunk-size]
i = (int) esp_strtol(src, &endstr, 16);
httpc_dbg("Chunk Size:%d\r\n", i);
if (i <= 0)
break;
//[chunk-size-end-ptr]
src = strstr(src, "\r\n") + 2;
//[chunk-data]
memmove(&chunked[dst], src, (size_t) i);
src += i + 2; /* CRLF */
dst += i;
} while (src < end);
//
//footer CRLF
//
/* decoded size */
return dst;
}
static void ICACHE_FLASH_ATTR
receive_callback(void *arg, char *buf, unsigned short len)
{
struct espconn *conn = (struct espconn *) arg;
request_args *req = (request_args *) conn->reserve;
if (req->buffer == NULL) {
return;
}
// Let's do the equivalent of a realloc().
int new_size = req->buffer_size + len;
if (new_size > req->max_buffer_size) {
system_soft_wdt_feed();
httpc_warn("Long resp, truncate (got %d, %d more rx, max %d)",
req->buffer_size, len, req->max_buffer_size);
int nlen = (req->max_buffer_size - req->buffer_size);
if (nlen <= 0) {
req->buffer[req->buffer_size - 1] = '\0';
if (req->secure) {
#ifdef USE_SECURE
espconn_secure_disconnect(conn);
#endif
}
else {
espconn_disconnect(conn);
}
return;
}
len = (unsigned short) nlen;
new_size = req->buffer_size + len;
}
char *new_buffer;
if (NULL == (new_buffer = (char *) malloc(new_size))) {
httpc_error("Failed to alloc more bytes (%d)", new_size);
req->buffer[0] = '\0'; // Discard the buffer to avoid using an incomplete response.
if (req->secure) {
#ifdef USE_SECURE
espconn_secure_disconnect(conn);
#endif
}
else {
espconn_disconnect(conn);
}
return; // The disconnect callback will be called.
}
memcpy(new_buffer, req->buffer, req->buffer_size);
memcpy(new_buffer + req->buffer_size - 1 /*overwrite the null character*/, buf, len); // Append new data.
new_buffer[new_size - 1] = '\0'; // Make sure there is an end of string.
free(req->buffer);
req->buffer = new_buffer;
req->buffer_size = new_size;
}
static void ICACHE_FLASH_ATTR
sent_callback(void *arg)
{
struct espconn *conn = (struct espconn *) arg;
request_args *req = (request_args *) conn->reserve;
if (req->post_data == NULL) {
httpc_dbg("All sent");
}
else {
// The headers were sent, now send the contents.
httpc_dbg("Sending request body");
if (req->secure) {
#ifdef USE_SECURE
espconn_secure_sent(conn, (uint8_t *)req->post_data, strlen(req->post_data));
#endif
}
else {
espconn_sent(conn, (uint8_t *) req->post_data, (uint16) strlen(req->post_data));
}
free(req->post_data);
req->post_data = NULL;
}
}
static void ICACHE_FLASH_ATTR
connect_callback(void *arg)
{
httpc_info("Connected!");
struct espconn *conn = (struct espconn *) arg;
request_args *req = (request_args *) conn->reserve;
espconn_regist_recvcb(conn, receive_callback);
espconn_regist_sentcb(conn, sent_callback);
char post_headers[32] = "";
if (req->post_data != NULL) { // If there is data this is a POST request.
sprintf(post_headers, "Content-Length: %d\r\n", strlen(req->post_data));
if (req->method == HTTPD_METHOD_GET) {
req->method = HTTPD_METHOD_POST;
}
}
const char *method = httpdMethodName(req->method);
if (req->headers == NULL) { /* Avoid NULL pointer, it may cause exception */
req->headers = (char *) malloc(sizeof(char));
req->headers[0] = '\0';
}
const size_t buflen = 80 // for literals in tpl string + some margin
+ strlen(method)
+ strlen(req->path)
+ strlen(req->hostname)
+ strlen(req->headers)
+ strlen(post_headers);
char buf[buflen];
int len = sprintf(buf,
"%s %s HTTP/1.1\r\n"
"Host: %s:%d\r\n"
"Connection: close\r\n"
"%s"
"%s"
"\r\n",
method, req->path, req->hostname, req->port, req->headers, post_headers);
httpc_dbg("Sending request");
if (req->secure) {
#ifdef USE_SECURE
espconn_secure_sent(conn, (uint8_t *)buf, len);
#endif
}
else {
espconn_sent(conn, (uint8_t *) buf, (uint16) len);
}
if (req->headers != NULL) {
free(req->headers);
req->headers = NULL;
}
}
/**
* @brief Free all that could be allocated in a request, including the struct itself.
* @param req : req to free
*/
static void
free_req(request_args *req)
{
if (!req) return;
if (req->buffer) free(req->buffer);
if (req->hostname) free(req->hostname);
if (req->path) free(req->path);
if (req->post_data) free(req->post_data);
if (req->headers) free(req->headers);
free(req);
}
static void ICACHE_FLASH_ATTR
disconnect_callback(void *arg)
{
httpc_dbg("Disconnected");
struct espconn *conn = (struct espconn *) arg;
if (conn == NULL) {
httpc_dbg("conn is null!");
return;
}
if (conn->reserve != NULL) {
httpc_dbg("Processing response");
request_args *req = (request_args *) conn->reserve;
int http_status = HTTP_STATUS_GENERIC_ERROR;
int body_size = 0;
char *body = "";
/* Turn off timeout timer */
os_timer_disarm(&(req->timeout_timer));
if (req->buffer == NULL) {
httpc_error("Buffer shouldn't be NULL");
}
else if (req->buffer[0] != '\0') {
// FIXME: make sure this is not a partial response, using the Content-Length header.
const char *version10 = "HTTP/1.0 ";
const char *version11 = "HTTP/1.1 ";
if (!strstarts(req->buffer, version10) && !strstarts(req->buffer, version11)) {
httpc_error("Invalid version in %s", req->buffer);
}
else {
http_status = atoi(req->buffer + strlen(version10));
/* find body and zero terminate headers */
char * headend = strstr(req->buffer, "\r\n\r\n");
if (headend != NULL) {
body = headend + 2;
*body++ = '\0';
*body++ = '\0';
body_size = (int) (req->buffer_size - (body - req->buffer));
if (strstr(req->buffer, "Transfer-Encoding: chunked")) {
body_size = chunked_decode(body, body_size);
}
}
}
}
httpc_info("Request completed.");
if (req->user_callback != NULL) { // Callback is optional.
req->user_callback(http_status, req->buffer, body, body_size, req->userData);
}
free_req(req);
} else {
httpc_error("Reserve is NULL!");
}
espconn_delete(conn);
if (conn->proto.tcp != NULL) {
free(conn->proto.tcp);
}
free(conn);
}
static void ICACHE_FLASH_ATTR
error_callback(void *arg, sint8 errType)
{
(void) errType;
httpc_error("Disconnected with error, type %d", errType);
disconnect_callback(arg);
}
static void ICACHE_FLASH_ATTR
http_timeout_callback(void *arg)
{
httpc_error("Connection timeout\n");
struct espconn *conn = (struct espconn *) arg;
if (conn == NULL) {
return;
}
request_args *req = (request_args *) conn->reserve;
if (req) {
/* Call disconnect */
if (req->secure) {
#ifdef USE_SECURE
espconn_secure_disconnect(conn);
#endif
}
else {
espconn_disconnect(conn);
}
if (req->user_callback != NULL) {
// fire callback, so user can free the userData
req->user_callback(HTTP_STATUS_TIMEOUT, req->buffer, "", 0, req->userData);
}
free_req(req);
}
// experimental - better cleanup
if (conn->proto.tcp != NULL) {
free(conn->proto.tcp);
}
free(conn);
}
static void ICACHE_FLASH_ATTR
dns_callback(const char *hostname, ip_addr_t *addr, void *arg)
{
(void)hostname;
request_args *req = (request_args *) arg;
if (addr == NULL) {
httpc_error("DNS failed for %s", hostname);
if (req->user_callback != NULL) {
req->user_callback(HTTP_STATUS_GENERIC_ERROR, "", "", 0, req->userData);
}
free(req);
}
else {
httpc_info("DNS found %s "IPSTR, hostname, IP2STR(addr));
struct espconn *conn = (struct espconn *) malloc(sizeof(struct espconn));
conn->type = ESPCONN_TCP;
conn->state = ESPCONN_NONE;
conn->proto.tcp = (esp_tcp *) malloc(sizeof(esp_tcp));
conn->proto.tcp->local_port = espconn_port();
conn->proto.tcp->remote_port = req->port;
conn->reserve = req;
memcpy(conn->proto.tcp->remote_ip, addr, 4);
espconn_regist_connectcb(conn, connect_callback);
espconn_regist_disconcb(conn, disconnect_callback);
espconn_regist_reconcb(conn, error_callback);
/* Set connection timeout timer */
os_timer_disarm(&(req->timeout_timer));
os_timer_setfn(&(req->timeout_timer), (os_timer_func_t *) http_timeout_callback, conn);
os_timer_arm(&(req->timeout_timer), req->timeout, false);
if (req->secure) {
#ifdef USE_SECURE
espconn_secure_set_size(ESPCONN_CLIENT, 5120); // set SSL buffer size
espconn_secure_connect(conn);
#endif
}
else {
espconn_connect(conn);
}
}
}
bool ICACHE_FLASH_ATTR
http_request(const httpclient_args *args, httpclient_cb user_callback)
{
// --- prepare port, secure... ---
// FIXME: handle HTTP auth with http://user:pass@host/
const char *url = args->url;
char hostname[128] = "";
int port = 80;
bool secure = false;
if (strstarts(url, "http://"))
url += strlen("http://"); // Get rid of the protocol.
else if (strstarts(url, "https://")) {
port = 443;
secure = true;
url += strlen("https://"); // Get rid of the protocol.
}
else {
httpc_error("Invalid URL protocol: %s", url);
return false;
}
char *path = strchr(url, '/');
if (path == NULL) {
path = strchr(url, '\0'); // Pointer to end of string.
}
char *colon = strchr(url, ':');
if (colon > path) {
colon = NULL; // Limit the search to characters before the path.
}
if (colon == NULL) { // The port is not present.
memcpy(hostname, url, (size_t) (path - url));
hostname[path - url] = '\0';
}
else {
port = atoi(colon + 1);
if (port == 0) {
httpc_error("Port error %s\n", url);
return false;
}
memcpy(hostname, url, (size_t) (colon - url));
hostname[colon - url] = '\0';
}
if (path[0] == '\0') { // Empty path is not allowed.
path = "/";
}
// ---
httpc_info("HTTP request: %s:%d%s", hostname, port, path);
request_args *req = (request_args *) malloc(sizeof(request_args));
req->hostname = esp_strdup(hostname);
req->path = esp_strdup(path);
// remove #anchor
char *hash = strchr(req->path, '#');
if (hash != NULL) *hash = '\0'; // remove the hash part
req->port = port;
req->secure = secure;
req->headers = esp_strdup(args->headers);
req->post_data = esp_strdup(args->body);
req->buffer_size = 1;
req->buffer = (char *) malloc(1);
req->buffer[0] = '\0'; // Empty string.
req->user_callback = user_callback;
req->timeout = HTTPCLIENT_DEF_TIMEOUT_MS;
req->method = args->method;
req->userData = args->userData;
req->max_buffer_size = (int) args->max_response_len;
ip_addr_t addr;
err_t error = espconn_gethostbyname((struct espconn *) req, // It seems we don't need a real espconn pointer here.
hostname, &addr, dns_callback);
if (error == ESPCONN_INPROGRESS) {
httpc_dbg("DNS pending");
}
else if (error == ESPCONN_OK) {
// Already in the local names table (or hostname was an IP address), execute the callback ourselves.
dns_callback(hostname, &addr, req);
}
else {
if (error == ESPCONN_ARG) {
httpc_error("DNS arg error %s", hostname);
}
else {
httpc_error("DNS error code %d", error);
}
dns_callback(hostname, NULL, req); // Handle all DNS errors the same way.
}
return true;
}
void httpclient_args_init(httpclient_args *args)
{
args->url = NULL;
args->body = NULL;
args->method = HTTPD_METHOD_GET;
args->headers = NULL;
args->max_response_len = HTTPCLIENT_DEF_MAX_LEN;
args->timeout = HTTPCLIENT_DEF_TIMEOUT_MS;
args->userData = NULL;
}
bool ICACHE_FLASH_ATTR
http_post(const char *url, const char *body, void *userData, httpclient_cb user_callback)
{
httpclient_args args;
httpclient_args_init(&args);
args.url = url;
args.body = body;
args.method = HTTPD_METHOD_POST;
args.userData = userData;
return http_request(&args, user_callback);
}
bool ICACHE_FLASH_ATTR
http_get(const char *url, void *userData, httpclient_cb user_callback)
{
httpclient_args args;
httpclient_args_init(&args);
args.url = url;
args.method = HTTPD_METHOD_GET;
args.userData = userData;
return http_request(&args, user_callback);
}
bool ICACHE_FLASH_ATTR
http_put(const char *url, const char *body, void *userData, httpclient_cb user_callback)
{
httpclient_args args;
httpclient_args_init(&args);
args.url = url;
args.body = body;
args.method = HTTPD_METHOD_PUT;
args.userData = userData;
return http_request(&args, user_callback);
}
void ICACHE_FLASH_ATTR
http_callback_example(int http_status,
char *response_headers,
char *response_body,
size_t body_size,
void *userData)
{
(void) userData;
dbg("Response: code %d", http_status);
if (http_status != HTTP_STATUS_GENERIC_ERROR) {
dbg("len(headers) = %d, len(body) = %d", (int) strlen(response_headers), body_size);
dbg("body: %s<EOF>", response_body); // FIXME: this does not handle binary data.
}
}
void ICACHE_FLASH_ATTR
http_callback_showstatus(int code,
char *response_headers,
char *response_body,
size_t body_size,
void *userData)
{
(void) response_body;
(void) response_headers;
(void) body_size;
(void) userData;
if (code == 200) {
info("Response OK (200)");
}
else if (code >= 400) {
error("Response ERROR (%d)", code);
dbg("Body: %s<EOF>", response_body);
}
else {
// ???
warn("Response %d", code);
dbg("Body: %s<EOF>", response_body);
}
}