0
\$\begingroup\$

The request object is given an input stream in the constructor.

I will parse the message command and the headers.
It then creates a wrapper around the stream so the user can access the body. This stream will decode the body encoding providing the body as a normal stream (just like a file).

Note 1: This makes it very easy to test the functionality as we can use std::string stream. Note 2: The server version of the streaming acts like a blocking stream but behind the scenes allows the thread to yield.

Request.h

#ifndef THORSANVIL_NISSE_NISSEHTTP_REQUEST_H
#define THORSANVIL_NISSE_NISSEHTTP_REQUEST_H
#include "NisseHTTPConfig.h"
#include "URL.h"
#include "HeaderRequest.h"
#include "HeaderResponse.h"
#include "StreamInput.h"
#include <istream>
namespace ThorsAnvil::Nisse::NisseHTTP
{
class Request
{
 std::string messageHeader;
 Version version;
 Method method;
 URL url;
 HeaderRequest head;
 HeaderRequest tail;
 HeaderResponse failResponse;
 StreamInput input;
 std::unique_ptr<std::streambuf> streamBuf;
 public:
 Request(std::string_view proto, std::istream& stream);
 Version getVersion() const {return version;}
 Method getMethod() const {return method;}
 URL const& getUrl() const {return url;}
 std::string_view httpRawRequest()const {return messageHeader;}
 HeaderRequest const& headers() const {return head;}
 HeaderRequest const& trailers() const {return tail;}
 HeaderResponse const& failHeader() const {return failResponse;}
 bool isValidRequest()const {return failResponse.empty();}
 // Trailers will return an empty HeaderRequest() if body has not been read.
 // if (body().eof()) Then trailers have been read.
 std::istream& body(); // Can be used to read the stream body.
 // It will auto eof() when no more data is available in the body.
 // Note this stream will auto decode the incoming message body based
 // on the 'content-encoding'
 private:
 std::string_view readFirstLine(std::istream& stream);
 bool readHeaders(HeaderRequest& dst, std::istream& stream);
 Version findVersion(std::string_view pv);
 Method findMethod(std::string_view method);
 bool buildURL(std::string_view proto, std::string_view path);
 bool buildStream(std::istream& stream);
 std::string_view getValue(std::string_view input);
};
}
#endif

Request.cpp

#include "Request.h"
#include "StreamInput.h"
#include <ThorsLogging/ThorsLogging.h>
#include <map>
#include <iostream>
using namespace ThorsAnvil::Nisse::NisseHTTP;
Request::Request(std::string_view proto, std::istream& stream)
 : version{Version::Unknown}
 , method{Method::Other}
{
 std::string_view path = readFirstLine(stream);
 if (path.size() != 0)
 {
 readHeaders(head, stream) &&
 buildURL(proto, path) &&
 buildStream(stream);
 }
}
std::string_view Request::readFirstLine(std::istream& stream)
{
 // Read the first line
 std::getline(stream, messageHeader);
 if (messageHeader.size() > 0 && messageHeader[messageHeader.size() - 1] == '\r') {
 messageHeader.resize(messageHeader.size() - 1);
 }
 else
 {
 ThorsLogInfo("ThorsAnvil::Nisse::PyntHTTP::Request", "readFirstLine", ": Header not \r\n terminated");
 failResponse.add("error", "Invalid HTTP Request");
 failResponse.add("rason", "Header Not terminated with <CR><LF>");
 return "";
 }
 // Extract the Method
 auto methStart = std::min(messageHeader.size(), messageHeader.find_first_not_of(" ", 0));
 auto methEnd = std::min(messageHeader.size(), messageHeader.find_first_of(' ', methStart));
 // Path
 auto pathStart = std::min(messageHeader.size(), messageHeader.find_first_not_of(" ", methEnd));
 auto pathEnd = std::min(messageHeader.size(), messageHeader.find_first_of(" ", pathStart));
 // Proto
 auto protStart = std::min(messageHeader.size(), messageHeader.find_first_not_of(" ", pathEnd));
 auto protEnd = std::min(messageHeader.size(), messageHeader.find_first_of("/", protStart));
 // Version
 auto versStart = std::min(messageHeader.size(), protEnd + 1);
 auto versEnd = std::min(messageHeader.size(), messageHeader.find_first_of(" \r", versStart));
 std::string_view meth(messageHeader.begin() + methStart, messageHeader.begin() + methEnd);
 std::string_view path(messageHeader.begin() + pathStart, messageHeader.begin() + pathEnd);
 std::string_view prot(messageHeader.begin() + protStart, messageHeader.begin() + protEnd);
 std::string_view vers(messageHeader.begin() + versStart, messageHeader.begin() + versEnd);
 std::string_view pv (messageHeader.begin() + protStart, messageHeader.begin() + versEnd);
 version = findVersion(pv);
 method = findMethod(meth);
 if (meth.size() == 0 || path.size() == 0 || pv.size() == 0 || version == Version::Unknown || method == Method::Other)
 {
 ThorsLogInfo("ThorsAnvil::Nisse::PyntHTTP::Request", "readFirstLine", ": Bad Request: ", "Method: >", meth, "< Path: >", path, "< Proto: >", pv, "<");
 failResponse.add("error", "Invalid HTTP Request");
 failResponse.add("method", meth);
 failResponse.add("path", path);
 failResponse.add("proto", pv);
 return "";
 }
 return path;
}
bool Request::readHeaders(HeaderRequest& dst, std::istream& stream)
{
 std::string line;
 while (std::getline(stream, line))
 {
 if (line == "\r") {
 break;
 }
 auto split = line.find(':');
 if (line.size() == 0 || line[line.size() - 1] != '\r' || split == std::string::npos)
 {
 ThorsLogInfo("ThorsAnvil::Nisse::PyntHTTP::Request", "readHeaders", ": Bad Request Header: ", line);
 failResponse.add("error", "Invalid HTTP Header");
 failResponse.add("header", line);
 return false;
 }
 dst.add({&line[0], &line[0] + split}, {&line[0] + split + 1, &line[0] + line.size() - 1});
 }
 return true;
}
bool Request::buildURL(std::string_view proto, std::string_view path)
{
 using std::literals::operator""sv;
 std::vector<std::string> const& hostValues = head.getHeader("host"sv);
 if (hostValues.size() == 0)
 {
 ThorsLogInfo("ThorsAnvil::Nisse::PyntHTTP::Request", "buildURL", ": Bad Request No Host Field: ");
 failResponse.add("error", "Invalid HTTP Request- No Host header");
 return false;
 }
 std::string_view hostValue = hostValues.size() == 0 ? ""sv : hostValues[0];
 url = URL{proto, hostValue, path};
 return true;
}
Version Request::findVersion(std::string_view pv)
{
 static const std::map<std::string_view, Version> versionMap { {"HTTP/1.0", Version::HTTP1_0},
 {"HTTP/1.1", Version::HTTP1_1},
 {"HTTP/2", Version::HTTP2},
 {"HTTP/3", Version::HTTP3}
 };
 auto find = versionMap.find(pv);
 if (find != versionMap.end()) {
 return find->second;
 }
 return Version::Unknown;
}
Method Request::findMethod(std::string_view method)
{
 static const std::map<std::string_view, Method> methodMap { {"GET", Method::GET},
 {"HEAD", Method::HEAD},
 {"OPTIONS", Method::OPTIONS},
 {"TRACE", Method::TRACE},
 {"PUT", Method::PUT},
 {"DELETE", Method::DELETE},
 {"POST", Method::POST},
 {"PATCH", Method::PATCH},
 {"CONNECT", Method::CONNECT}
 };
 auto find = methodMap.find(method);
 if (find != methodMap.end()) {
 return find->second;
 }
 return Method::Other;
}
bool Request::buildStream(std::istream& stream)
{
 auto& contentLength = head.getHeader("content-length");
 auto& transferEncoding = head.getHeader("transfer-encoding");
 if (contentLength.size() != 0 && transferEncoding.size() != 0)
 {
 ThorsLogInfo("ThorsAnvil::Nisse::PyntHTTP::Request", "buildStream", ": Bad Request: Includes both 'content-length' and 'transfer-encoding'");
 failResponse.add("error", "Invalid HTTP Request- Includes both 'content-length' and 'transfer-encoding'");
 failResponse.add("value-content-length", contentLength[0]);
 for (auto const& v: transferEncoding) {
 failResponse.add("value-transfer-encoding", v);
 }
 return false;
 }
 if (transferEncoding.size() == 0)
 {
 std::streamsize bodySize = 0;
 if (contentLength.size() != 0) {
 bodySize = std::stoi(contentLength[0]);
 }
 input.addBuffer(StreamBufInput(stream, bodySize));
 return true;
 }
 if (transferEncoding.size() != 0 && transferEncoding.size() == 1 && transferEncoding[0] == "chunked")
 {
 input.addBuffer(StreamBufInput(stream,
 Encoding::Chunked,
 [&](){readHeaders(tail, stream);}));
 return true;
 }
 // TODO Handle other transfer encoding.
 // Currently what will happen is that you can not read from the input stream.
 // Which means POST/PUT etc commands can not transfer data.
 ThorsLogInfo("ThorsAnvil::Nisse::PyntHTTP::Request", "buildStream", ": Bad Request: Unsupported Transfer Encoding.");
 failResponse.add("error", "Invalid HTTP Request- Unsupported Transer Encoding");
 for (auto const& v: transferEncoding) {
 failResponse.add("value-transfer-encoding", v);
 }
 failResponse.add("supported-transfer-encoding", "chunked");
 return false;
}
std::istream& Request::body()
{
 return input;
}
asked Oct 20, 2024 at 18:11
\$\endgroup\$

0

Know someone who can answer? Share a link to this question via email, Twitter, or Facebook.

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.