This class keep tracks of the HTTP headers that are being read from the incoming request.
Few special cases.
As per RFC 9110 Section 5.3
some headers can only appear once and are automatically de-dupped (removing all but the first one of these headers).
Most (but not all) headers allow multiple values. The multiple values can be either in the same header (comma separated) or provided by multiple header lines. We combine all the values as appropriate. So for each header we map it to a std::vector<std::string>
to store each of the values.
Also headers are supposed to be case insensitive. So to make things simpler on the server all headers are lowercased before use.
HeaderRequest.h
#ifndef THORSANVIL_NISSE_NISSEHTTP_HEADER_REQUEST_H
#define THORSANVIL_NISSE_NISSEHTTP_HEADER_REQUEST_H
#include "NisseHTTPConfig.h"
#include <map>
#include <vector>
#include <string>
#include <string_view>
namespace ThorsAnvil::Nisse::NisseHTTP
{
class HeaderRequest
{
using HeaderMap = std::map<std::string, std::vector<std::string>>;
using CIterator = HeaderMap::const_iterator;
HeaderMap headers;
public:
CIterator begin() const {return std::begin(headers);}
CIterator end() const {return std::end(headers);}
bool hasHeader(std::string_view header) const {return hasHeader(std::string(header));}
bool hasHeader(std::string const& header) const {return headers.find(header) != headers.end();}
std::vector<std::string> const& getHeader(char const* header) const {return getHeader(std::string(header));}
std::vector<std::string> const& getHeader(std::string_view header) const {return getHeader(std::string(header));}
std::vector<std::string> const& getHeader(std::string const& header) const;
bool operator==(HeaderRequest const& rhs) const {return headers == rhs.headers;}
bool operator!=(HeaderRequest const& rhs) const {return !(*this == rhs);}
void add(std::string_view header, std::string_view value);
private:
bool dedupHeader(std::string_view header);
bool splitOnComma(std::string_view header);
std::string_view getValue(std::string_view input);
};
}
#endif
HeaderRequest.cpp
#include "HeaderRequest.h"
#include <set>
#include <sstream>
using namespace ThorsAnvil::Nisse::NisseHTTP;
std::vector<std::string> const& HeaderRequest::getHeader(std::string const& header) const
{
static const std::vector<std::string> empty;
auto find = headers.find(header);
if (find == headers.end()) {
return empty;
}
return find->second;
}
void HeaderRequest::add(std::string_view header, std::string_view value)
{
header = getValue(header);
std::string key(header.size(), ' ');
std::transform(std::begin(header), std::end(header), std::begin(key), [](unsigned char c){ return std::tolower(c);});
if (key == "set-cookie")
{
// SPECIAL HANDLING for Cookies.
// TODO
return;
}
auto& headerValue = headers[key];
if (dedupHeader(key))
{
if (headerValue.size() == 1) {
return;
}
headerValue.emplace_back(getValue(value));
return;
}
if (splitOnComma(key))
{
std::stringstream ss{std::string{value}};
std::string splitValue;
while (std::getline(ss, splitValue, ','))
{
headerValue.emplace_back(getValue(splitValue));
}
}
else
{
headerValue.emplace_back(getValue(value));
}
}
std::string_view HeaderRequest::getValue(std::string_view input)
{
// Remove leading and trailing white space.
input.remove_prefix(std::min(input.size(), input.find_first_not_of(" \r\t\v")));
input.remove_suffix(input.size() - std::min(input.size(), input.find_last_not_of(" \r\t\v")) - 1);
return input;
}
bool HeaderRequest::dedupHeader(std::string_view header)
{
// See RFC 9110 Section 5.3 for more information.
static const std::set<std::string_view> singleValueHeaders = {"age", "authorization", "content-length", "content-type", "etag", "expires", "from", "host", "if-modified-since", "if-unmodified-since", "last-modified", "location", "max-forwards", "proxy-authorization", "referer", "retry-after", "server", "user-agent"};
return singleValueHeaders.find(header) != singleValueHeaders.end();
}
bool HeaderRequest::splitOnComma(std::string_view header)
{
static const std::set<std::string_view> nosplit = {"accept-datetime", "access-control-request-method", "access-control-request-header", "content-md5", "cookie", "date", "expect", "if-match", "if-none-match", "if-range"};
return nosplit.find(header) == nosplit.end();
}