Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for repeated HTTP header fields #2290

Merged
merged 9 commits into from
Mar 29, 2021
38 changes: 36 additions & 2 deletions Sming/Core/Network/Http/HttpHeaderFields.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,28 @@
#include <FlashString/Vector.hpp>

// Define field name strings and a lookup table
#define XX(_tag, _str, _comment) DEFINE_FSTR_LOCAL(hhfnStr_##_tag, _str);
#define XX(tag, str, flags, comment) DEFINE_FSTR_LOCAL(hhfnStr_##tag, str);
HTTP_HEADER_FIELDNAME_MAP(XX)
#undef XX

#define XX(_tag, _str, _comment) &hhfnStr_##_tag,
#define XX(tag, str, flags, comment) &hhfnStr_##tag,
DEFINE_FSTR_VECTOR_LOCAL(fieldNameStrings, FlashString, HTTP_HEADER_FIELDNAME_MAP(XX));
#undef XX

HttpHeaderFields::Flags HttpHeaderFields::getFlags(HttpHeaderFieldName name) const
{
switch(name) {
#define XX(tag, str, flags, comment) \
case HttpHeaderFieldName::tag: \
return flags;
HTTP_HEADER_FIELDNAME_MAP(XX)
#undef XX
default:
// Custom fields
return 0;
}
}

String HttpHeaderFields::toString(HttpHeaderFieldName name) const
{
if(name == HTTP_HEADER_UNKNOWN) {
Expand All @@ -35,6 +49,26 @@ String HttpHeaderFields::toString(HttpHeaderFieldName name) const
return customFieldNames[unsigned(name) - unsigned(HTTP_HEADER_CUSTOM)];
}

String HttpHeaderFields::toString(HttpHeaderFieldName name, const String& value) const
{
String tag = toString(name);
if(getFlags(name)[Flag::Multi]) {
tag += ": ";
CStringArray values(value);
String s;
s.reserve((tag.length() + 2) * values.count() + value.length());
for(auto p : values) {
s += tag;
s += p;
s += "\r\n";
}

return s;
}

return toString(tag, value);
}

String HttpHeaderFields::toString(const String& name, const String& value)
{
String s;
Expand Down
101 changes: 61 additions & 40 deletions Sming/Core/Network/Http/HttpHeaderFields.h
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@

#include "Data/CStringArray.h"
#include "WString.h"
#include <Data/BitSet.h>

/*
* Common HTTP header field names. Enumerating these simplifies matching
Expand All @@ -25,68 +26,91 @@
* A brief description of each header field is given for information purposes.
* For details see https://www.iana.org/assignments/message-headers/message-headers.xhtml
*
* Entries are formatted thus: XX(tag, str, flags, comment)
* tag: Identifier used in code (without HTTP_HEADER_ prefix)
* str: String used in HTTP protocol
* flags: Additional flags (see HttpHeaderFields::Flag)
* comment: Further details
*/
#define HTTP_HEADER_FIELDNAME_MAP(XX) \
XX(ACCEPT, "Accept", "Limit acceptable response types") \
XX(ACCESS_CONTROL_ALLOW_ORIGIN, "Access-Control-Allow-Origin", "") \
XX(AUTHORIZATION, "Authorization", "Basic user agent authentication") \
XX(CC, "Cc", "email field") \
XX(CONNECTION, "Connection", "Indicates sender's desired control options") \
XX(CONTENT_DISPOSITION, "Content-Disposition", "Additional information about how to process response payload") \
XX(CONTENT_ENCODING, "Content-Encoding", "Applied encodings in addition to content type") \
XX(CONTENT_LENGTH, "Content-Length", "Anticipated size for payload when not using transfer encoding") \
XX(CONTENT_TYPE, "Content-Type", \
XX(ACCEPT, "Accept", 0, "Limit acceptable response types") \
XX(ACCESS_CONTROL_ALLOW_ORIGIN, "Access-Control-Allow-Origin", 0, "") \
XX(AUTHORIZATION, "Authorization", 0, "Basic user agent authentication") \
XX(CC, "Cc", 0, "email field") \
XX(CONNECTION, "Connection", 0, "Indicates sender's desired control options") \
XX(CONTENT_DISPOSITION, "Content-Disposition", 0, "Additional information about how to process response payload") \
XX(CONTENT_ENCODING, "Content-Encoding", 0, "Applied encodings in addition to content type") \
XX(CONTENT_LENGTH, "Content-Length", 0, "Anticipated size for payload when not using transfer encoding") \
XX(CONTENT_TYPE, "Content-Type", 0, \
"Payload media type indicating both data format and intended manner of processing by recipient") \
XX(CONTENT_TRANSFER_ENCODING, "Content-Transfer-Encoding", "Coding method used in a MIME message body part") \
XX(CACHE_CONTROL, "Cache-Control", "Directives for caches along the request/response chain") \
XX(DATE, "Date", "Message originating date/time") \
XX(EXPECT, "Expect", "Behaviours to be supported by the server in order to properly handle this request.") \
XX(ETAG, "ETag", \
XX(CONTENT_TRANSFER_ENCODING, "Content-Transfer-Encoding", 0, "Coding method used in a MIME message body part") \
XX(CACHE_CONTROL, "Cache-Control", 0, "Directives for caches along the request/response chain") \
XX(DATE, "Date", 0, "Message originating date/time") \
XX(EXPECT, "Expect", 0, "Behaviours to be supported by the server in order to properly handle this request.") \
XX(ETAG, "ETag", 0, \
"Validates resource, such as a file, so recipient can confirm whether it has changed - generally more " \
"reliable than Date") \
XX(FROM, "From", "email address of human user who controls the requesting user agent") \
XX(HOST, "Host", \
XX(FROM, "From", 0, "email address of human user who controls the requesting user agent") \
XX(HOST, "Host", 0, \
"Request host and port information for target URI; allows server to service requests for multiple hosts on a " \
"single IP address") \
XX(IF_MATCH, "If-Match", \
XX(IF_MATCH, "If-Match", 0, \
"Precondition check using ETag to avoid accidental overwrites when servicing multiple user requests. Ensures " \
"resource entity tag matches before proceeding.") \
XX(IF_MODIFIED_SINCE, "If-Modified-Since", "Precondition check using Date") \
XX(LAST_MODIFIED, "Last-Modified", "Server timestamp indicating date and time resource was last modified") \
XX(LOCATION, "Location", "Used in redirect responses, amongst other places") \
XX(SEC_WEBSOCKET_ACCEPT, "Sec-WebSocket-Accept", "Server response to opening Websocket handshake") \
XX(SEC_WEBSOCKET_VERSION, "Sec-WebSocket-Version", \
XX(IF_MODIFIED_SINCE, "If-Modified-Since", 0, "Precondition check using Date") \
XX(LAST_MODIFIED, "Last-Modified", 0, "Server timestamp indicating date and time resource was last modified") \
XX(LOCATION, "Location", 0, "Used in redirect responses, amongst other places") \
XX(SEC_WEBSOCKET_ACCEPT, "Sec-WebSocket-Accept", 0, "Server response to opening Websocket handshake") \
XX(SEC_WEBSOCKET_VERSION, "Sec-WebSocket-Version", 0, \
"Websocket opening request indicates acceptable protocol version. Can appear more than once.") \
XX(SEC_WEBSOCKET_KEY, "Sec-WebSocket-Key", "Websocket opening request validation key") \
XX(SEC_WEBSOCKET_PROTOCOL, "Sec-WebSocket-Protocol", \
XX(SEC_WEBSOCKET_KEY, "Sec-WebSocket-Key", 0, "Websocket opening request validation key") \
XX(SEC_WEBSOCKET_PROTOCOL, "Sec-WebSocket-Protocol", 0, \
"Websocket opening request indicates supported protocol(s), response contains negotiated protocol(s)") \
XX(SERVER, "Server", "Identifies software handling requests") \
XX(SET_COOKIE, "Set-Cookie", "Server may pass name/value pairs and associated metadata to user agent (client)") \
XX(SUBJECT, "Subject", "email subject line") \
XX(TO, "To", "email intended recipient address") \
XX(TRANSFER_ENCODING, "Transfer-Encoding", "e.g. Chunked, compress, deflate, gzip") \
XX(UPGRADE, "Upgrade", \
XX(SERVER, "Server", 0, "Identifies software handling requests") \
XX(SET_COOKIE, "Set-Cookie", Flag::Multi, \
"Server may pass name/value pairs and associated metadata to user agent (client)") \
XX(SUBJECT, "Subject", 0, "email subject line") \
XX(TO, "To", 0, "email intended recipient address") \
XX(TRANSFER_ENCODING, "Transfer-Encoding", 0, "e.g. Chunked, compress, deflate, gzip") \
XX(UPGRADE, "Upgrade", 0, \
"Used to transition from HTTP to some other protocol on the same connection. e.g. Websocket") \
XX(USER_AGENT, "User-Agent", "Information about the user agent originating the request") \
XX(WWW_AUTHENTICATE, "WWW-Authenticate", "Indicates HTTP authentication scheme(s) and applicable parameters")
XX(USER_AGENT, "User-Agent", 0, "Information about the user agent originating the request") \
XX(WWW_AUTHENTICATE, "WWW-Authenticate", Flag::Multi, \
"Indicates HTTP authentication scheme(s) and applicable parameters") \
XX(PROXY_AUTHENTICATE, "Proxy-Authenticate", Flag::Multi, \
"Indicates proxy authentication scheme(s) and applicable parameters")

enum class HttpHeaderFieldName {
UNKNOWN = 0,
#define XX(tag, str, comment) tag,
#define XX(tag, str, flags, comment) tag,
HTTP_HEADER_FIELDNAME_MAP(XX)
#undef XX
CUSTOM // First custom header tag value
};

#define XX(tag, str, comment) constexpr HttpHeaderFieldName HTTP_HEADER_##tag = HttpHeaderFieldName::tag;
XX(UNKNOWN, "", "")
#define XX(tag, str, flags, comment) constexpr HttpHeaderFieldName HTTP_HEADER_##tag = HttpHeaderFieldName::tag;
XX(UNKNOWN, "", 0, "")
HTTP_HEADER_FIELDNAME_MAP(XX)
XX(CUSTOM, "", "")
XX(CUSTOM, "", 0, "")
#undef XX

class HttpHeaderFields
{
public:
/**
* @brief Flag values providing additional information about header fields
*/
enum class Flag {
Multi, ///< Field may have multiple values
};
using Flags = BitSet<uint8_t, Flag, 1>;

/**
* @brief Get flags (if any) for given header field
* @retval Flags
*/
Flags getFlags(HttpHeaderFieldName name) const;

String toString(HttpHeaderFieldName name) const;

/** @brief Produce a string for output in the HTTP header, with line ending
Expand All @@ -96,10 +120,7 @@ class HttpHeaderFields
*/
static String toString(const String& name, const String& value);

String toString(HttpHeaderFieldName name, const String& value) const
{
return toString(toString(name), value);
}
String toString(HttpHeaderFieldName name, const String& value) const;

/** @brief Find the enumerated value for the given field name string
* @param name
Expand Down
48 changes: 48 additions & 0 deletions Sming/Core/Network/Http/HttpHeaders.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
/****
* Sming Framework Project - Open Source framework for high efficiency native ESP8266 development.
* Created 2015 by Skurydin Alexey
* http://github.com/SmingHub/Sming
* All files of the Sming Core are provided under the LGPL v3 license.
*
* HttpHeaders.cpp
*
****/

#include "HttpHeaders.h"
#include <debug_progmem.h>

const String& HttpHeaders::operator[](const String& name) const
{
auto field = fromString(name);
if(field == HTTP_HEADER_UNKNOWN) {
return nil;
}
return operator[](field);
}

bool HttpHeaders::append(const HttpHeaderFieldName& name, const String& value)
{
int i = indexOf(name);
if(i < 0) {
operator[](name) = value;
return true;
}

if(!getFlags(name)[Flag::Multi]) {
debug_w("[HTTP] Append not supported for header field '%s'", toString(name).c_str());
return false;
}

valueAt(i) += '\0' + value;

return true;
}

void HttpHeaders::setMultiple(const HttpHeaders& headers)
{
for(unsigned i = 0; i < headers.count(); i++) {
HttpHeaderFieldName fieldName = headers.keyAt(i);
auto fieldNameString = headers.toString(fieldName);
operator[](fieldNameString) = headers.valueAt(i);
}
}
26 changes: 10 additions & 16 deletions Sming/Core/Network/Http/HttpHeaders.h
Original file line number Diff line number Diff line change
Expand Up @@ -48,14 +48,7 @@ class HttpHeaders : public HttpHeaderFields, private HashMap<HttpHeaderFieldName
* @retval const String& Reference to value
* @note if the field doesn't exist a null String reference is returned
*/
const String& operator[](const String& name) const
{
auto field = fromString(name);
if(field == HTTP_HEADER_UNKNOWN) {
return nil;
}
return operator[](field);
}
const String& operator[](const String& name) const;

/** @brief Fetch a reference to the header field value by name
* @param name
Expand Down Expand Up @@ -86,19 +79,20 @@ class HttpHeaders : public HttpHeaderFields, private HashMap<HttpHeaderFieldName

using HashMap::remove;

/**
* @brief Append value to multi-value field
* @param name
* @param value
* @retval bool false if value exists and field does not permit multiple values
*/
bool append(const HttpHeaderFieldName& name, const String& value);

void remove(const String& name)
{
remove(fromString(name));
}

void setMultiple(const HttpHeaders& headers)
{
for(unsigned i = 0; i < headers.count(); i++) {
HttpHeaderFieldName fieldName = headers.keyAt(i);
auto fieldNameString = headers.toString(fieldName);
operator[](fieldNameString) = headers.valueAt(i);
}
}
void setMultiple(const HttpHeaders& headers);

HttpHeaders& operator=(const HttpHeaders& headers)
{
Expand Down
9 changes: 7 additions & 2 deletions Sming/Core/Network/Http/HttpResponse.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,17 @@
#include "Data/Stream/MemoryDataStream.h"
#include "Data/Stream/FileStream.h"

HttpResponse* HttpResponse::setCookie(const String& name, const String& value)
HttpResponse* HttpResponse::setCookie(const String& name, const String& value, bool append)
{
String s = name;
s += '=';
s += value;
headers[HTTP_HEADER_SET_COOKIE] = s;
if(append) {
headers.append(HTTP_HEADER_SET_COOKIE, s);
} else {
headers[HTTP_HEADER_SET_COOKIE] = s;
}

return this;
}

Expand Down
2 changes: 1 addition & 1 deletion Sming/Core/Network/Http/HttpResponse.h
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ class HttpResponse
return setContentType(::toString(type));
}

HttpResponse* setCookie(const String& name, const String& value);
HttpResponse* setCookie(const String& name, const String& value, bool append = false);

HttpResponse* setHeader(const String& name, const String& value)
{
Expand Down
5 changes: 5 additions & 0 deletions samples/HttpServer_Bootstrap/app/application.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,11 @@ void onIndex(HttpRequest& request, HttpResponse& response)
void onHello(HttpRequest& request, HttpResponse& response)
{
response.setContentType(MIME_HTML);

// Below is an example how to send multiple cookies
response.setCookie("cookie1", "value1");
response.setCookie("cookie2", "value", true);

// Use direct strings output only for small amount of data (huge memory allocation)
response.sendString("Sming. Let's do smart things.");
}
Expand Down
25 changes: 22 additions & 3 deletions tests/HostTests/modules/Http.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -35,11 +35,12 @@ class HttpTest : public TestGroup
TEST_CASE("http lookups")
{
auto s = toString(HPE_UNKNOWN);
REQUIRE(s == F("HPE_UNKNOWN"));
REQUIRE(s == "HPE_UNKNOWN");
s = httpGetErrorDescription(HPE_INVALID_URL);
REQUIRE(s == F("invalid URL"));
REQUIRE(s == "invalid URL");
s = toString(HTTP_STATUS_TOO_MANY_REQUESTS);
REQUIRE(s.equalsIgnoreCase(F("too many requests")));
DEFINE_FSTR_LOCAL(too_many_requests, "too many requests");
REQUIRE(s.equalsIgnoreCase(too_many_requests));
}
}

Expand Down Expand Up @@ -195,6 +196,24 @@ class HttpTest : public TestGroup
REQUIRE(serialize(headers2) == FS_serialized);
printHeaders(headers2);
}

DEFINE_FSTR_LOCAL(FS_cookies, "Set-Cookie: name1=value1\r\n"
"Set-Cookie: name2=value2\r\n");

TEST_CASE("appendHeaders")
{
HttpHeaders headers2;
headers2.append(HTTP_HEADER_SET_COOKIE, "name1=value1");
headers2.append(HTTP_HEADER_SET_COOKIE, "name2=value2");
printHeaders(headers2);
REQUIRE(headers2.count() == 1);
REQUIRE(serialize(headers2) == FS_cookies);

// Append should work if field not already set
REQUIRE(headers2.append(HTTP_HEADER_CONTENT_LENGTH, "0") == true);
// But fail on actual append
REQUIRE(headers2.append(HTTP_HEADER_CONTENT_LENGTH, "1234") == false);
}
}
};

Expand Down