I am using the Philhower RP2040 core, version 4.4.3, and a WIZNet W5500 Ethernet chip. Rather than creating a global reference to Wiznet5500lwIP, I am using a pointer that I allocate and free as the system starts and stops. (In case you are wondering why, it's because I also plan to send commands to the WIZNet W5500 Ethernet chip to turn it off/put it in low power mode and then back on). I have run into a situation where tcp_bind() fails and I can't properly control the cleanup of the internal data structures that cause the problem.
Attached is sample code that should demonstrate the issue. I am using an HTTP server based on HTTP by Giuseppe Masino. I have included an earlier version of that library because I couldn't get the most recent (7 years old) to compile. So, don't install the HTTP library into Arduino IDE, just use the included code.
MAC address and IP settings are near the beginning of the .ino file - adjust for your network. I hard coded the IP address settings because the network connection starts faster.
To run the example code you'll need a Raspberry Pi Pico, a WIZnet W5500 board, and a pushbutton. The WIZnet is connected to CS 17 and SPI0. The button can be connected to any available pin - I used pin 28. To debug, I use a PicoProbe.
The project setup is mostly defaults. Optimize is turned off to allow set of breakpoint at tcp_bind() result:
- Board: Raspberry Pi Pico
- Debug Level: "None"
- Debug Port: "Disabled"
- C++ Exceptions: "Disabled"
- Flash Size: "2MB (no FS)"
- CPU Speed: "133 MHz"
- IP/Bluetooth Stack: "IPv4 Only"
- Optimize: Disabled (-O0) - Profiling: "Disabled"
- RTTI: "Disabled"
- Stack Protector: "Disabled"
- Upload Method: "Picoprobe/Debugprobe (CMSIS-DAP)"
- USB Stack: "Pico SDK"
After the code is built and running, press the button to start the Ethernet chip and servers. Notice that ping of DemoOfFailedTCPBind.local works and a browse to http://DemoOfFailedTCPBind.local returns the demo web page.
If the button is pressed again, the servers are stopped and the Ethernet object is closed and deleted. Now ping and the demo web page are no longer available.
Here is the problem: If the button is pressed again, which starts Ethernet and the webserver again, the tcp_bind() that is called by the webserver begin() will fail. To see this, set a breakpoint in arduino15/packages/rp2040/hardware/rp2040/4.4.3/libraries/WiFi/src/WiFiServer.cpp, line 68.
Note these observations: tcp_bind() will always succeed if (a) I never refresh the web page http://DemoOfFailedTCPBind.local, (b) I restart the debugger, or (c) if I wait EXACTLY 2 minutes or more after the web client closes.
Can anyone help me solve this issue? Am I using the client code correctly? Is there a re-init function for the W5500lwIP code?
I also don't see how the translation layer lwip_wrap.cpp in the Philhower core is built - the functions with names like __real_lwip_init() and __real_tcp_bind().
Any feedback would be appreciated.
(I will attach the code when Arduino.cc doesn't consider me a new user. In the meantime I'll have to include it as part of this message. What I find odd is that I originally created my Arduino.cc account on 6 December 2022 and even received an updated Privacy Policy/Terms of Service on 3 December 2024. When I tried to log in today, 10 February 2025, the account had been deleted)
Directory structure:
DemonstrateFailedTCPbind/
|
+- src/
| |
| +-HTTP/
| |
| +- httpHeader.cpp
| +- httpMessage.hpp
| +- httpServer.hpp
| +- httpHeader.hpp
| +- httpRequestHandler.hpp
+- DemonstrateFailedTCPbind.ino
+- HTTPInterface.cpp
+- HTTPInterface.h
DemonstrateFailedTCPbind.ino:
#include <W5500lwIP.h> // Or W5100lwIP.h or ENC28J60.h
#include <LEAmDNS.h>
#include "src/HTTP/httpServer.hpp"
#include "HTTPInterface.h"
#define FRONT_PANEL_SWn 28
Wiznet5500lwIP *pEth = NULL;
byte macAddress[6] { 0xDE, 0xAD, 0xBE, 0xEF, 0xFE, 0xED } ;
IPAddress myIP(192,168,199,215);
IPAddress myGW(192,168,199,1);
IPAddress myNetMask(255,255,255,0);
IPAddress myDNS1(192,168,199,1);
IPAddress myDNS2(8,8,8,8);
static char *MDNSname = "DemoOfFailedTCPBind";
void setup() {
pinMode(FRONT_PANEL_SWn, INPUT_PULLDOWN);
}
enum Test_State
{
ETHERNET_START = 1,
ETHERNET_STARTING,
ETHERNET_RUN,
ETHERNET_STOP,
};
Test_State currentState = ETHERNET_STOP;
EthernetLinkStatus linkStatus = Unknown;
wl_status_t connectionStatus = WL_IDLE_STATUS;
// Create an HTTP reference pointer
http::Server *pHttpServer = NULL;
// Create a HTTP Server reference pointer
HTTP_NETWORK_SERVER_CLASS * _server = NULL;
#define HTTP_PORT 80
void http_server_init(void)
{
// Create the HTTP server
_server = new HTTP_NETWORK_SERVER_CLASS(HTTP_PORT);
if (NULL != _server)
{
// start the HTTP server
_server->begin();
// Initialize the HTTP server library
pHttpServer = new http::Server();
if (NULL != pHttpServer)
{
// Use the default http request handler
// (although returnTestPage is the default handler, the assignment here is just for clarity)
//pHttpServer->GET_requestHandler = http::requestHandler::returnTestPage;
}
}
}
void http_server_loop(void)
{
HTTP_YIELD();
if ( (NULL != pHttpServer) && (NULL != _server) )
{
#if(HTTP_NETWORK_TYPE == NETWORK_ESP8266) || (HTTP_NETWORK_TYPE == NETWORK_ESP32) || (HTTP_NETWORK_TYPE == NETWORK_RP2040)
while(_server->hasClient())
{
#endif
// store new connection
#if(HTTP_NETWORK_TYPE == NETWORK_WIFI_NINA)
HTTP_NETWORK_CLASS * tcpClient = new HTTP_NETWORK_CLASS(_server->available());
#else
HTTP_NETWORK_CLASS * tcpClient = new HTTP_NETWORK_CLASS(_server->accept());
#endif
if(!tcpClient)
{
DEBUG_HTTP("[HTTP-Client] creating Network class failed!");
return;
}
handleNewClient(tcpClient);
// Then pass the object to the server
pHttpServer->replyTo(*tcpClient);
#if(HTTP_NETWORK_TYPE == NETWORK_ESP8266) || (HTTP_NETWORK_TYPE == NETWORK_ESP32) || (HTTP_NETWORK_TYPE == NETWORK_RP2040)
}
#endif
}
}
void http_server_terminate(void)
{
if (NULL != pHttpServer)
{
httpServerdisconnect(); // pHttpServer->disconnect();
delete pHttpServer;
pHttpServer = NULL;
}
if (NULL != _server)
{
#if(HTTP_NETWORK_TYPE == NETWORK_ESP8266) || (HTTP_NETWORK_TYPE == NETWORK_RP2040)
_server->close();
#elif(HTTP_NETWORK_TYPE == NETWORK_ESP32)
_server->end();
#else
// TODO how to close server?
#endif
delete _server;
_server = NULL;
}
}
uint32_t previousMillis = 0;
void loop()
{
uint32_t currentMillis = millis();
uint32_t timeDifference = 0;
if (currentMillis < previousMillis)
{
// Overflow has occurred
timeDifference = UINT32_MAX - previousMillis + currentMillis;
}
else
{
// No overflow
timeDifference = currentMillis - previousMillis;
}
if (timeDifference >= 10)
{
previousMillis = currentMillis;
static uint32_t delayms = 0;
// This routine is called every AMP_POWER_CALL_PERIOD ms.
if (delayms > 0)
delayms--;
switch (currentState)
{
case ETHERNET_START:
pEth = new Wiznet5500lwIP(SS); // Parameter is the Chip Select pin
pEth->config(myIP, myGW, myNetMask, myDNS1, myDNS2);
pEth->begin(macAddress);
currentState = ETHERNET_STARTING;
break;
case ETHERNET_STARTING:
linkStatus = pEth->linkStatus();
connectionStatus = pEth->status();
if ( (LinkON == linkStatus) && (WL_CONNECTED == connectionStatus) )
{
if (MDNS.begin(MDNSname))
{
Serial.println("MDNS responder started");
}
http_server_init();
delayms = 50; // 50*10ms debounce
currentState = ETHERNET_RUN;
}
break;
case ETHERNET_RUN:
connectionStatus = pEth->status();
if (WL_CONNECTED == connectionStatus)
{
http_server_loop();
// This runs the Bonjour module. CALL THIS PERIODICALLY, preferably, call it once per loop().
MDNS.update();
}
if (0 == delayms)
{
// Check for Front Panel or Trigger initiated power off
bool FP_instantaneous_value = digitalRead(FRONT_PANEL_SWn);
if (FP_instantaneous_value == 0)
{
delayms = 50; // 5*10ms debounce
currentState = ETHERNET_STOP;
}
}
break;
case ETHERNET_STOP:
if (NULL != pEth)
{
MDNS.end();
http_server_terminate();
pEth->end();
// Delete the Ethernet class
delete pEth;
pEth = NULL;
}
if (0 == delayms)
{
// Check for Front Panel or Trigger initiated power off
bool FP_instantaneous_value = digitalRead(FRONT_PANEL_SWn);
if (FP_instantaneous_value == 0)
{
currentState = ETHERNET_START;
}
}
break;
}
}
}
HTTPInterface.h:
#include <WebSockets.h>
#define HTTP_NETWORK_CLASS WEBSOCKETS_NETWORK_CLASS
#define HTTP_NETWORK_SERVER_CLASS WEBSOCKETS_NETWORK_SERVER_CLASS
#define HTTP_NETWORK_TYPE WEBSOCKETS_NETWORK_TYPE
#define HTTP_YIELD WEBSOCKETS_YIELD
#define DEBUG_HTTP DEBUG_WEBSOCKETS
#define HTTP_TCP_TIMEOUT WEBSOCKETS_TCP_TIMEOUT
#define HTTP_SERVER_CLIENT_MAX (2)
typedef enum {
HTTP_NOT_CONNECTED,
HTTP_HEADER,
HTTP_BODY,
HTTP_CONNECTED
} HTTPclientsStatus_t;
typedef struct
{
HTTPclientsStatus_t status = HTTP_NOT_CONNECTED;
HTTP_NETWORK_CLASS * tcp = nullptr;
} HTTPclient_t;
HTTPclient_t* newClient(HTTP_NETWORK_CLASS * TCPclient);
void handleNewClient(HTTP_NETWORK_CLASS * tcpClient);
bool clientIsConnected(HTTPclient_t * client);
void dropNativeClient(HTTPclient_t * client);
void clientDisconnect(HTTPclient_t * client);
void httpServerdisconnect(void);
HTTPInterface.cpp:
#include "HTTPInterface.h"
HTTPclient_t _clients[HTTP_SERVER_CLIENT_MAX];
HTTPclient_t* newClient(HTTP_NETWORK_CLASS * TCPclient)
{
HTTPclient_t * client;
// search free list entry for client
for(uint8_t i = 0; i < HTTP_SERVER_CLIENT_MAX; i++)
{
client = &_clients[i];
// look for match to existing socket before creating a new one
if(clientIsConnected(client))
{
#if(HTTP_NETWORK_TYPE == NETWORK_W5100)
// Check to see if it is the same socket - if so, return it
if(client->tcp->getSocketNumber() == TCPclient->getSocketNumber())
{
return client;
}
#endif
}
else
{
// state is not connected or tcp connection is lost
client->tcp = TCPclient;
#if(HTTP_NETWORK_TYPE == NETWORK_ESP8266) || (HTTP_NETWORK_TYPE == NETWORK_ESP32)
client->isSSL = false;
client->tcp->setNoDelay(true);
#endif
// set Timeout for readBytesUntil and readStringUntil
client->tcp->setTimeout(HTTP_TCP_TIMEOUT);
client->status = HTTP_HEADER;
#if(HTTP_NETWORK_TYPE == NETWORK_ESP8266) || (HTTP_NETWORK_TYPE == NETWORK_ESP32) || (HTTP_NETWORK_TYPE == NETWORK_RP2040)
#ifndef NODEBUG_HTTP
IPAddress ip = client->tcp->remoteIP();
#endif
DEBUG_HTTP("[HTTP-Server] new client from %d.%d.%d.%d\n", ip[0], ip[1], ip[2], ip[3]);
#else
DEBUG_HTTP("[HTTP-Server] new client\n");
#endif
return client;
break;
}
}
return nullptr;
}
/**
* Handle incoming Connection Request
*/
void handleNewClient(HTTP_NETWORK_CLASS * tcpClient)
{
HTTPclient_t * client = newClient(tcpClient);
if(!client)
{
// no free space to handle client
#if(HTTP_NETWORK_TYPE == NETWORK_ESP8266) || (HTTP_NETWORK_TYPE == NETWORK_ESP32) || (HTTP_NETWORK_TYPE == NETWORK_RP2040)
#ifndef NODEBUG_HTTP
IPAddress ip = tcpClient->remoteIP();
#endif
DEBUG_HTTP("[HTTP-Server] no free space new client from %d.%d.%d.%d\n", ip[0], ip[1], ip[2], ip[3]);
#else
DEBUG_HTTP("[HTTP-Server] no free space new client\n");
#endif
// no client! => create dummy!
HTTPclient_t dummy = HTTPclient_t();
client = &dummy;
client->tcp = tcpClient;
dropNativeClient(client);
}
HTTP_YIELD();
}
/**
* get client state
* @param client HTTPclient_t * ptr to the client struct
* @return true = connected
*/
bool clientIsConnected(HTTPclient_t * client)
{
if(!client->tcp)
{
return false;
}
if(client->tcp->connected())
{
if(client->status != HTTP_NOT_CONNECTED)
{
return true;
}
}
else
{
// client lost
if(client->status != HTTP_NOT_CONNECTED)
{
DEBUG_HTTP("[HTTP-Server] client connection lost.\n");
// do cleanup
clientDisconnect(client);
}
}
if(client->tcp)
{
// do cleanup
DEBUG_HTTP("[HTTP-Server] client list cleanup.\n");
clientDisconnect(client);
}
return false;
}
/**
* Discard a native client
* @param client HTTPclient_t * ptr to the client struct contaning the native client "->tcp"
*/
void dropNativeClient(HTTPclient_t * client)
{
if(!client)
{
return;
}
if(client->tcp)
{
if(client->tcp->connected())
{
#if (HTTP_NETWORK_TYPE != NETWORK_ESP32) && (HTTP_NETWORK_TYPE != NETWORK_RP2040)
client->tcp->flush();
#endif
client->tcp->stop();
}
#if(HTTP_NETWORK_TYPE == NETWORK_WIFI_NINA) || (HTTP_NETWORK_TYPE == NETWORK_SAMD_SEED)
// does not support delete (no destructor)
#else
delete client->tcp;
#endif
client->tcp = NULL;
}
}
/**
* Disconnect an client
* @param client HTTPclient_t * ptr to the client struct
*/
void clientDisconnect(HTTPclient_t * client)
{
dropNativeClient(client);
client->status = HTTP_NOT_CONNECTED;
DEBUG_HTTP("[HTTP-Server] client disconnected.\n");
}
/**
* disconnect all clients
*/
void httpServerdisconnect(void)
{
HTTPclient_t * client;
for(uint8_t i = 0; i < HTTP_SERVER_CLIENT_MAX; i++)
{
client = &_clients[i];
if(clientIsConnected(client))
{
clientDisconnect(client);
}
}
}
httpHeader.cpp:
#include "httpHeader.hpp"
#include <vector>
const String requestMethodString[]
{
"INVALID", // Internally used value
"OPTIONS",
"GET",
"HEAD",
"POST",
"PUT",
"DELETE",
"TRACE",
"CONNECT"
};
const String responseString[]
{
"100 CONTINUE" ,
"101 SWITCHING PROTOCOLS ",
"102 PROCESSING ",
"200 OK" ,
"201 CREATED ",
"202 ACCEPTED ",
"203 UNOFFICIAL INFO ",
"204 NO CONTENT ",
"205 RESET CONTENT ",
"206 PARTIAL CONTENT ",
"207 MULTI STATUS ",
"300 MULTIPLE CHOICES " ,
"301 MOVED PERMANENTLY ",
"302 FOUND ",
"303 SEE OTHER ",
"304 NOT MODIFIED ",
"305 USE PROXY ",
"306 TEMPORARY REDIRECT ",
"307 PERMANENT REDIRECT ",
"400 BAD REQUEST " ,
"401 UNAUTHORIZED " ,
"402 PAYMENT REQUIRED " ,
"403 FORBIDDEN " ,
"404 NOT FOUND " ,
"405 METHOD NOT ALLOWED " ,
"406 NOT ACCEPTABLE ",
"407 PROXY AUTH REQUIRED ",
"408 REQUEST TIMEOUT ",
"409 CONFLICT ",
"410 GONE ",
"411 LENGHT REQUIRED ",
"412 PRECONDITION FAILED ",
"413 ENTITY TOO LARGE ",
"414 URI TOO LONG ",
"415 UNSUPPORTED MEDIA TYPE ",
"416 REQUESTED RANGE NOT SATISFIABLE ",
"417 EXPECTATION FAILED ",
"418 I AM A TEAPOT ",
"420 ENHACE YOUR CALM" ,
"422 UNPROCESSABLE ENTITY" ,
"426 UPGRADE REQUIRED" ,
"429 RETRY WITH" ,
"451 UNAVAILABLE FOR LEGAL REASONS"
};
http::Request_t::operator String() const
{
return requestMethodString[ (uint8_t) requestId ];
}
http::Request_t::operator Request() const
{
return requestId;
}
http::Request http::Request_t::operator = ( const Request & newRequest )
{
requestId = newRequest;
return * this;
}
http::Response_t::operator String() const
{
return responseString[ ( uint8_t ) responseId ];
}
http::Response_t::operator Response() const
{
return responseId;
}
/**
* Main serialization function for HTTP header classes
* Here is where much of the job is done
*
* @param fieldN numeber of fields available in the HTTP header
* @param fieldArray pointer to the fieldArray member of the HTTP header object
*/
String headerToString( std::vector<http::Field> & vec )
{
// String that holds the serialized header
String header;
/* NOTE:
* pragma is a special field that is used to store all the actual pragma paramaters
* It is known to be the first element of fieldArray
* This reference is made for convenience
*
* As it needs to be processed in a different way than other fields,
* all loops that iterate over fields start at index 1 to not touch it
*/
const http::Field pragma = vec[0];
// Local function that serializes the header field at index n of fieldArray
auto serializeField = [ & ]( const uint8_t n )
{
const http::Field field = vec[n];
return field.value.length() > 0 ? field.tag + " " + field.value + "\n" : "";
};
// Begin header serialization
/* NOTE:
* The first row serialization method changes if this
* is for a response or a request, so delegate this
* to the proper function overloads.
*
* Instead, the other fields will be serialized here
*
* NOTE2:
* The serializeField function is used only in the following loop
* but in this way the whole thing is more expressive
*/
for( auto n = 1; n < vec.size(); ++ n )
{
header += serializeField(n);
}
// Serialize and append pragma parameters to the header
{
uint8_t subStrCount = 0;
uint8_t newStrCount = 0;
/* NOTE: ( pragma field structure )
* The pragma field value is a concatenation of the values of
* all the actual pragma fields, each terminated with a \n
*/
// Count the \n terminated substrings and create an array that can hold them
for( auto ptr = pragma.value.begin(); ptr < pragma.value.end(); ++ptr )
if( * ptr == '\n' ) ++subStrCount;
String pragmaArray[ subStrCount ];
// Extract the individual pragma field values
for( auto ptr = pragma.value.begin(); ptr < pragma.value.end(); ++ptr )
{
pragmaArray[ newStrCount ] += *ptr;
if( *ptr == '\n' ) ++newStrCount;
}
// Once extraction is complete, serialize them all
for( newStrCount = 0; newStrCount < subStrCount; ++newStrCount )
header += pragma.tag + pragmaArray[ newStrCount ];
}
// Field serialization complete
// Append header termination sequence
header += "\r\n";
return header;
}
/// Serialization method for HTTP response header classes
http::ResponseHeader::operator String() const
{
std::vector<Field> fieldVector = getFieldVector();
String partialHeader = headerToString( fieldVector );
String response = "HTTP/" + version + " " + ( String ) responseCode + "\n";
return response + partialHeader;
}
/// Serialization method for HTTP request header classes
http::RequestHeader::operator String() const
{
std::vector<Field> fieldVector = getFieldVector();
String partialHeader = headerToString( fieldVector );
String request = ( String ) requestMethod + " " + requestTarget + " " + version + "\n";
return request + partialHeader;
}
#if 0
http::ResponseHeader & http::ResponseHeader::operator = ( const ResponseHeader & other )
{
for( auto n = 0; n < fieldN; ++ n )
{
//// fieldArray[ n ]->value = other.fieldArray[ n ]->value;
if ("" == other.fieldArray[ n ]->value)
return * NULL;
}
return * this;
}
http::RequestHeader & http::RequestHeader::operator = ( const RequestHeader & other )
{
for( auto n = 0; n < fieldN; ++ n )
fieldArray[ n ]->value = other.fieldArray[ n ]->value;
return * this;
}
#endif
httpHeader.hpp:
/**
* @file
* @brief Header file containing HTTP header related structures and functions
*/
#pragma once
#include <Arduino.h>
namespace http
{
enum class Request;
enum class Response;
class Request_t;
class Response_t;
struct Field;
struct ResponseHeader;
struct RequestHeader;
}
/**
* Enum class representing a HTTP request method
* It is intended to be used only as an assignment parameter for http::Request_t
*/
enum class http::Request
{
INVALID = 0, // Internally used value
OPTIONS,
GET,
HEAD,
POST,
PUT,
DELETE,
TRACE,
CONNECT
};
/**
* Enum class representing a HTTP response code
* It is intended to be used only as an assignment parameter for http::Request_t
*/
enum class http::Response
{
// Informational
CONTINUE = 0 ,
SWITCHING_PROTOCOLS ,
PROCESSING ,
// Successful
OK ,
CREATED ,
ACCEPTED ,
UNOFFICIAL_INFO ,
NO_CONTENT ,
RESET_CONTENT ,
PARTIAL_CONTENT ,
MULTI_STATUS ,
// Redirection
MULTIPLE_CHOICES ,
MOVED_PERMANENTLY ,
FOUND ,
SEE_OTHER ,
NOT_MODIFIED ,
USE_PROXY ,
TEMPORARY_REDIRECT ,
PERMANENT_REDIRECT ,
// Client error
BAD_REQUEST ,
UNAUTHORIZED ,
PAYMENT_REQUIRED ,
FORBIDDEN ,
NOT_FOUND ,
METHOD_NOT_ALLOWED ,
NOT_ACCEPTABLE ,
PROXY_AUTH_REQUIRED ,
REQUEST_TIMEOUT ,
CONFLICT ,
GONE ,
LENGHT_REQUIRED ,
PRECONDITION_FAILED ,
ENTITY_TOO_LARGE ,
URI_TOO_LONG ,
UNSUPPORTED_MEDIA_TYPE ,
REQUESTED_RANGE_NOT_SATISFIABLE ,
EXPECTATION_FAILED ,
I_AM_A_TEAPOT ,
// Server Error
ENHACE_YOUR_CALM ,
UNPROCESSABLE_ENTITY ,
UPGRADE_REQUIRED ,
RETRY_WITH ,
UNAVAILABLE_FOR_LEGAL_REASONS
};
/**
* Wrapper class for http::Request
* it provides a strong type for representing a HTTP request method
* in addition to conversion operators to String and to enum type
*/
class http::Request_t
{
public:
Request_t( const Request & request ) : requestId( request ) {};
operator String() const;
operator Request() const;
Request operator = ( const Request & );
private:
Request requestId = Request::INVALID;
};
/**
* Wrapper class for http::Response
* it provides a strong type for representing a HTTP response code
* in addition to conversion operators to String and to enum type
*/
class http::Response_t
{
public:
Response_t( const Response & response ) : responseId( response ) {};
Response_t operator = ( const Response & newResponse )
{
responseId = newResponse;
return * this;
}
operator String() const;
operator Response() const;
private:
Response responseId;
};
/**
* A http header field
* This struct is intended to use only in http header classes
*
* The tag can only be defined at the time of the instantiation,
* while the value can be assigned at any time by the assignment operator
*/
struct http::Field
{
explicit Field( const String & tag_ ) : tag( tag_ ) {}
Field( const String & tag_, const String & value_ ) : tag( tag_ ), value( value_ ) {}
Field operator = ( const String & newValue )
{
value = newValue;
return * this;
}
const String tag;
String value;
};
/**
* A struct representing a HTTP response header
* It can be serialized by conversion to String
*/
struct http::ResponseHeader
{
ResponseHeader & operator = ( const ResponseHeader & );
String version;
Response_t responseCode = Response::OK;
Field pragma { "Pragma:" };
// Control data fields
Field age { "Age:" };
Field cacheControl { "Cache-Control:" };
Field expires { "Expires:" };
Field date { "Date:" };
Field location { "Location:" };
Field retryAfter { "Retry-After:" };
Field vary { "Vary:" };
Field warning { "Warning:" };
// Validator fields
Field ETag { "ETag:" };
Field lastModified { "Last-Modified:" };
// Authentication Challenges fields
Field WWWAuthenticate { "WWW-Authenticate:" };
Field proxyAuthenticate { "Proxy-Authenticate:" };
// Response Context fields
Field acceptRanges { "Accept-Ranges:" };
Field allow { "Allow:" };
Field server { "Server:" };
Field connection { "Connection:" };
Field encoding { "Content-Encoding:" };
Field type { "Content-Type:" };
Field length { "Content-length:" };
operator String() const;
std::vector<Field> getFieldVector() const
{
std::vector<Field> fieldVector = {pragma, age, cacheControl, expires, date, location, retryAfter,
vary, warning, ETag, lastModified, WWWAuthenticate, proxyAuthenticate,
acceptRanges, allow, server, connection, encoding, type, length};
return fieldVector;
};
};
/**
* A struct representing a HTTP request header
* It can be serialized by conversion to String
*/
struct http::RequestHeader
{
RequestHeader & operator = ( const RequestHeader & );
Request_t requestMethod = Request::INVALID;
String requestTarget;
String version;
// Control fields
Field connection { "Connection:" };
Field cacheControl { "Cache-Control:" };
Field expect { "Expect:" };
Field host { "Host:" };
Field maxForwards { "Max-Forwards:" };
Field pragma { "Pragma:" };
Field range { "Range:" };
Field textEncodings { "TE:" };
// Conditional fields
Field ifMatch { "If-Match:" };
Field ifNoneMatch { "If-None-Match:" };
Field ifModifiedSince { "If-Modified-Since:" };
Field ifUnmodifiedSince { "If-Unmodified-Since:" };
Field ifRange { "If-Range:" };
// Content negotiation fields
Field acceptedTypes { "Accept:" };
Field acceptedCharset { "Accept-Charset:" };
Field acceptedEncoding { "Accept-Encoding:" };
Field acceptedLanguage { "Accept-Language:" };
// Authentication credentials fields
Field authorization { "Authorization:" };
Field proxyAuth { "Proxy-Authorization:" };
// Context request fields
Field from { "From:" };
Field referrer { "Referer:" };
Field userAgent { "User-Agent:" };
operator String() const;
std::vector<Field> getFieldVector() const
{
std::vector<Field> fieldVector = {pragma, connection, cacheControl, expect, host, maxForwards, range,
textEncodings, ifMatch, ifNoneMatch, ifModifiedSince, ifUnmodifiedSince,
ifRange, acceptedTypes, acceptedCharset, acceptedEncoding, acceptedLanguage,
authorization, proxyAuth, from, referrer, userAgent};
return fieldVector;
};
};
httpMessage.hpp:
/**
* @file
* @brief Header file containing HTTP message related structures and functions
*/
#pragma once
#include "httpHeader.hpp"
namespace http
{
struct RequestMessage;
struct ResponseMessage;
}
/// Struct representing a whole HTTP request message
struct http::RequestMessage
{
RequestHeader header;
String payload;
bool parsingFailed = false;
};
/// Struct representing a whole HTTP response message
struct http::ResponseMessage
{
ResponseMessage(const ResponseHeader & header_ ,
const String & payload_ = "" ,
const bool & parsingFailed_ = false,
const bool & skipWrite_ = false) : header( header_ ), payload( payload_ ), parsingFailed( parsingFailed_ ), skipWrite( skipWrite_ )
{};
ResponseHeader header;
String payload;
bool parsingFailed;
bool skipWrite;
operator String() const
{
return ( String ) header + payload + "\n";
}
};
httpRequestHandler.hpp:
/**
* @file
* @brief Header file containing default HTTP request handlers
*/
#pragma once
#include "httpMessage.hpp"
/**
* Helper macro that simplifies the creation of a request handler
*
* @param name The name of the handler with eventual namespace scoping
*/
#define IMPLEMENT_HTTP_REQUEST_HANDLER(name) \
void name ( \
const http::RequestMessage & requestMessage , \
http::ResponseMessage & responseMessage , \
WiFiClient & client \
)
namespace http
{
namespace requestHandler
{
IMPLEMENT_HTTP_REQUEST_HANDLER( returnDefaultHeader ); ///< Default HEAD handler
IMPLEMENT_HTTP_REQUEST_HANDLER( returnTestPage ); ///< Default GET handler
}
}
// ACTUAL IMPLEMENTATION BELOW
IMPLEMENT_HTTP_REQUEST_HANDLER( http::requestHandler::returnDefaultHeader )
{
// Do nothing, as the default response message already has
// the default header and empty payload
}
IMPLEMENT_HTTP_REQUEST_HANDLER( http::requestHandler::returnTestPage )
{
const auto & target = requestMessage.header.requestTarget;
responseMessage.payload =
" Hello from Arduino ! \n"
"\n"
" The request target was " + target + "\n"
"\n"
" This test page is offered by the returnTestPage Handler\n"
" part of the Arduino_HTTP/1.1 library by qub1750ul\n"
"\n"
" https://github.com/qub1750ul/Arduino_HTTP\n\n";
}
httpServer.hpp:
/**
* @file
* @brief Header file containing the HTTP Server class and the HTTP parser
*/
#pragma once
#include "httpRequestHandler.hpp"
namespace http
{
class Server;
RequestMessage parseRawMessageFrom( WiFiClient );
}
/// The HTTP Server Class, managing the requests from remote clients
class http::Server
{
public:
Server();
void replyTo( WiFiClient );
ResponseHeader defaultResponseHeader;
// Callback functions for HTTP requests
#define DECLARE_REQUEST_HANDLER_PTR(name) \
IMPLEMENT_HTTP_REQUEST_HANDLER( ( * name ) ) = nullptr
DECLARE_REQUEST_HANDLER_PTR( OPTIONS_requestHandler );
DECLARE_REQUEST_HANDLER_PTR( GET_requestHandler );
DECLARE_REQUEST_HANDLER_PTR( HEAD_requestHandler );
DECLARE_REQUEST_HANDLER_PTR( POST_requestHandler );
DECLARE_REQUEST_HANDLER_PTR( PUT_requestHandler );
DECLARE_REQUEST_HANDLER_PTR( DELETE_requestHandler );
DECLARE_REQUEST_HANDLER_PTR( TRACE_requestHandler );
DECLARE_REQUEST_HANDLER_PTR( CONNECT_requestHandler );
#undef DECLARE_REQUEST_HANDLER_PTR
};
// ******************
// * IMPLEMENTATION *
// ******************
http::Server::Server()
{
// Set default header
defaultResponseHeader.version = "1.1";
defaultResponseHeader.connection = "close";
defaultResponseHeader.server = "Arduino_HTTP/1.0.0 Arduino";
// Set default request handlers
GET_requestHandler = requestHandler::returnTestPage;
HEAD_requestHandler = requestHandler::returnDefaultHeader;
}
/**
* Method used to reply to remote client requests
* The request type and the appropriate handler are automatically deduced from
* the request header
*
* @param Client_t The class representing the socket of the transport layer
* @param client A RemoteClient object representing the remote client
*/
void http::Server::replyTo( WiFiClient client )
{
if( ! client.available() ) return;
RequestMessage inboundMessage = parseRawMessageFrom( client );
ResponseMessage responseMessage( defaultResponseHeader );
if( inboundMessage.parsingFailed )
responseMessage.header.responseCode = Response::BAD_REQUEST;
else
{
const Request & requestMethod = inboundMessage.header.requestMethod;
// Select the appropriate function to handle the request
auto requestHandler =
requestMethod == Request::OPTIONS ? OPTIONS_requestHandler :
requestMethod == Request::GET ? GET_requestHandler :
requestMethod == Request::HEAD ? HEAD_requestHandler :
requestMethod == Request::POST ? POST_requestHandler :
requestMethod == Request::PUT ? PUT_requestHandler :
requestMethod == Request::DELETE ? DELETE_requestHandler :
requestMethod == Request::TRACE ? TRACE_requestHandler :
requestMethod == Request::CONNECT ? CONNECT_requestHandler : nullptr;
if( requestHandler == nullptr )
responseMessage.header.responseCode = Response::BAD_REQUEST;
else
requestHandler( inboundMessage, responseMessage, client );
}
if (responseMessage.skipWrite)
{
// Reset the value
responseMessage.skipWrite = false;
}
else
{
client.write( String(responseMessage).c_str() );
}
client.stop();
}
/**
* The parsing function used to deserialize incoming HTTP messages
*
* @param Client_t The class representing the socket of the transport layer
* @param client A RemoteClient object representing the remote client
*/
http::RequestMessage http::parseRawMessageFrom( WiFiClient client )
{
RequestMessage requestMessage;
// Manually read the first byte to let getNextWord and accessNextField
// work as expected
char charBuffer = client.read();
auto getNextWord = [ & ]() -> String
{
String word;
// Read from the buffer until a word-terminating char or field end is encoutered
while( charBuffer != ' ' && charBuffer != '\n' )
{
word += charBuffer;
charBuffer = client.read();
}
// This is necessary to not get stuck on next call
if( charBuffer == ' ' )
charBuffer = client.read();
return word;
};
auto accessNextField = [ & ]() -> void
{
// Discard all chars until the next field
while( charBuffer != '\n' )
charBuffer = client.read();
// Begin reading the new field, so getNextWord doesn't get stuck
charBuffer = client.read();
};
// Parse first line
// Parse request method
{
String requestMethod = getNextWord();
#define ifMatchesThenAssign(x) requestMethod == ( Request_t ) x ? x
requestMessage.header.requestMethod =
ifMatchesThenAssign( Request::CONNECT ) :
ifMatchesThenAssign( Request::DELETE ) :
ifMatchesThenAssign( Request::GET ) :
ifMatchesThenAssign( Request::HEAD ) :
ifMatchesThenAssign( Request::OPTIONS ) :
ifMatchesThenAssign( Request::POST ) :
ifMatchesThenAssign( Request::PUT ) :
ifMatchesThenAssign( Request::TRACE ) : Request::INVALID;
#undef ifMatchesThenAssign
}
requestMessage.header.requestTarget = getNextWord();
requestMessage.header.version = getNextWord();
if ( requestMessage.header.requestMethod == Request::INVALID ||
requestMessage.header.requestTarget == "" )
{
requestMessage.parsingFailed = true;
return requestMessage;
}
// Parse header fields until the header-payload separator or end-of-message is encoutered
while( charBuffer != '\r' && client.available() )
{
accessNextField();
// Get the field name
const String parsedTag = getNextWord();
/* As HTTP/1.1 specification states, if a field starts with a space ignore it
*
* IMPLEMENTATION NOTE:
* Since getNextWord() stops extracting the word on every whitespace
* if the field begins with a whitespace the first word extracted
* for that field will be empty
*/
if( parsedTag == "" ) continue;
std::vector<Field> fieldVector = requestMessage.header.getFieldVector();
// Go through all the header fields to find one with a matching name
// if no one is found the current field is ignored
for( uint8_t n = 0; n < fieldVector.size(); ++ n )
{
Field & field = fieldVector[n];
if( field.tag != parsedTag ) continue;
// Read the field value, appending eventual spaces discarded by getNextWord()
for( String parsedValue = getNextWord(); parsedValue != ""; parsedValue = getNextWord() )
field.value += parsedValue + ( charBuffer == ' ' ? " " : "" );
break;
}
}
// Read the payload
accessNextField();
/* IMPLEMENTATION NOTE :
* This method allows to correctly read the payload even when using
* keep-alive connections
*/
while( charBuffer != '\r' && client.available() )
{
const String nextWord = getNextWord();
if( nextWord == "" ) accessNextField();
else requestMessage.payload += nextWord + ( charBuffer == ' ' ? " " : "" );
}
return requestMessage;
}