From: LeonardoBizzoni Date: Thu, 15 Aug 2024 16:42:57 +0000 (+0200) Subject: Request parsing and dispatch X-Git-Url: http://git.leonardobizzoni.com/?a=commitdiff_plain;h=e69de25100ee7dfdefe5250f0222839b9075eb42;p=http-lib Request parsing and dispatch Missing sending the `Response` to the client. There is also an error when its given a query string alongside a path eg. `example.com/eg?meaning_of_life=42`. This puts inside `Request.query` `/eg?meaning_of_life=42` instead of just `/eg`. --- diff --git a/src/const_definitions.h b/src/const_definitions.h new file mode 100644 index 0000000..507854e --- /dev/null +++ b/src/const_definitions.h @@ -0,0 +1,145 @@ +#pragma once + +#include +#include +#include + +#define Err(error) std::unexpected(error) + +namespace http { + constexpr std::string_view NEW_LINE = "\r\n"; + + enum class Status { + CONTINUE = 100, + SWITCHING_PROTOCOLS = 101, + PROCESSING = 102, + + OK = 200, + CREATED, + ACCEPTED, + NON_AUTHORITATIVE_INFORMATION, + NO_CONTENT, + PARTIAL_CONTENT, + ALREADY_REPORTED, + IM_USED, + + MULTIPLE_CHOICES = 300, + MOVED_PERMANENTLY, + FOUND, + SEE_OTHER, + NOT_MODIFIED, + USE_PROXY, + UNUSED, + TEMPORARY_REDIRECT, + PERMANENT_REDIRECT, + + BAD_REQUEST = 400, + UNAUTHORIZED, + PAYMENT_REQUIRED, + FORBIDDEN, + NOT_FOUND, + METHOD_NOT_ALLOWED, + NOT_ACCEPTABLE, + PROXY_AUTHENTICATION_REQUIRED, + REQUEST_TIMEOUT, + CONFLICT, + GONE, + LENGTH_REQUIRED, + PRECONDITION_FAILED, + PAYLOAD_TOO_LARGE, + URI_TOO_LONG, + UNSUPPORTED_MEDIA_TYPE, + RANGE_NOT_SATISFIABLE, + EXPECTATION_FAILED, + IM_A_TEAPOT, + MISDIRECTED_REQUEST = 421, + UNPROCESSABLE_CONTENT, + LOCKED, + FAILED_DEPENDENCY, + TOO_EARLY, + UPGRADE_REQUIRED, + PRECONDITION_REQUIRED = 428, + TOO_MANY_REQUESTS, + REQUEST_HEADER_FIELDS_TOO_LARGE = 431, + UNAVAILABLE_FOR_LEGAL_REASONS = 451, + + INTERNAL_SERVER_ERROR = 500, + NOT_IMPLEMENTED, + BAD_GATEWAY, + SERVICE_UNAVAILABLE, + GATEWAY_TIMEOUT, + HTTP_VERSION_NOT_SUPPORTED, + VARIANT_ALSO_NEGOTIATES, + INSUFFICIENT_STORAGE, + LOOP_DETECTED, + NOT_EXTENDED, + NETWORK_AUTHENTICATION_REQUIRED, + }; + + const std::unordered_map status_map = { + {100, Status::CONTINUE}, + {101, Status::SWITCHING_PROTOCOLS}, + {102, Status::PROCESSING}, + + {200, Status::OK}, + {201, Status::CREATED}, + {202, Status::ACCEPTED}, + {203, Status::NON_AUTHORITATIVE_INFORMATION}, + {204, Status::NO_CONTENT}, + {205, Status::PARTIAL_CONTENT}, + {208, Status::ALREADY_REPORTED}, + {226, Status::IM_USED}, + + {300, Status::MULTIPLE_CHOICES}, + {301, Status::MOVED_PERMANENTLY}, + {302, Status::FOUND}, + {303, Status::SEE_OTHER}, + {304, Status::NOT_MODIFIED}, + {305, Status::USE_PROXY}, + {306, Status::UNUSED}, + {307, Status::TEMPORARY_REDIRECT}, + {308, Status::PERMANENT_REDIRECT}, + + {400, Status::BAD_REQUEST}, + {401, Status::UNAUTHORIZED}, + {402, Status::PAYMENT_REQUIRED}, + {403, Status::FORBIDDEN}, + {404, Status::NOT_FOUND}, + {405, Status::METHOD_NOT_ALLOWED}, + {406, Status::NOT_ACCEPTABLE}, + {407, Status::PROXY_AUTHENTICATION_REQUIRED}, + {408, Status::REQUEST_TIMEOUT}, + {409, Status::CONFLICT}, + {410, Status::GONE}, + {411, Status::LENGTH_REQUIRED}, + {412, Status::PRECONDITION_FAILED}, + {413, Status::PAYLOAD_TOO_LARGE}, + {414, Status::URI_TOO_LONG}, + {415, Status::UNSUPPORTED_MEDIA_TYPE}, + {416, Status::RANGE_NOT_SATISFIABLE}, + {417, Status::EXPECTATION_FAILED}, + {418, Status::IM_A_TEAPOT}, + {421, Status::MISDIRECTED_REQUEST}, + {422, Status::UNPROCESSABLE_CONTENT}, + {423, Status::LOCKED}, + {424, Status::FAILED_DEPENDENCY}, + {425, Status::TOO_EARLY}, + {426, Status::UPGRADE_REQUIRED}, + {428, Status::PRECONDITION_REQUIRED}, + {429, Status::TOO_MANY_REQUESTS}, + {431, Status::REQUEST_HEADER_FIELDS_TOO_LARGE}, + {451, Status::UNAVAILABLE_FOR_LEGAL_REASONS}, + + {500, Status::INTERNAL_SERVER_ERROR}, + {501, Status::NOT_IMPLEMENTED}, + {502, Status::BAD_GATEWAY}, + {503, Status::SERVICE_UNAVAILABLE}, + {504, Status::GATEWAY_TIMEOUT}, + {505, Status::HTTP_VERSION_NOT_SUPPORTED}, + {506, Status::VARIANT_ALSO_NEGOTIATES}, + {507, Status::INSUFFICIENT_STORAGE}, + {508, Status::LOOP_DETECTED}, + {510, Status::NOT_EXTENDED}, + {511, Status::NETWORK_AUTHENTICATION_REQUIRED}, + }; +} diff --git a/src/error.h b/src/error.h index f67be24..0d52dec 100644 --- a/src/error.h +++ b/src/error.h @@ -8,9 +8,12 @@ namespace http { SocketConnection, DNSResolution, ServerNotFound, - InvalidRead, InvalidResponse, - InvalidResponseHTTPVersion, + InvalidRequest, + InvalidRequestMethod, + InvalidRequestPath, + InvalidQueryRequest, + InvalidHTTPVersion, InvalidResponseStatusCode, }; } diff --git a/src/http.h b/src/http.h index 87d16f3..efef966 100644 --- a/src/http.h +++ b/src/http.h @@ -2,17 +2,14 @@ #include +#include "const_definitions.h" #include "error.h" #include "method.h" #include "request.h" #include "response.h" #include "listener.h" -#define Err(error) std::unexpected(error) - namespace http { - constexpr std::string_view NEW_LINE = "\r\n"; - std::expected send(Method, const Request& req); std::expected connect(const std::string_view& domain_name, const uint16_t port = 80); diff --git a/src/http_version.h b/src/http_version.h new file mode 100644 index 0000000..7bc19ba --- /dev/null +++ b/src/http_version.h @@ -0,0 +1,10 @@ +#pragma once + +#include + +namespace http { + struct http_version { + uint8_t major = 1; + uint8_t minor = 1; + }; +} // namespace http diff --git a/src/listener.cpp b/src/listener.cpp index 31bb3fe..8c801b7 100644 --- a/src/listener.cpp +++ b/src/listener.cpp @@ -5,8 +5,8 @@ #include #include -#include +#include "const_definitions.h" #include "error.h" #include "http.h" @@ -39,18 +39,32 @@ namespace http { std::expected Listener::serve() { socklen_t addrsize = sizeof(this->addr); - int8_t clientfd = ::accept(this->socketfd, (struct sockaddr *)&this->addr, &addrsize); - if (clientfd < 0) { - return Err(Error::SocketConnection); - } - auto raw_req = read_raw_message(clientfd); - if (raw_req.has_value()) { - std::cout << raw_req.value() << std::endl; + while (true) { + int8_t clientfd = ::accept(this->socketfd, (struct sockaddr *)&this->addr, &addrsize); + if (clientfd < 0) { + continue; + } + + Response resp; + auto raw_req = read_raw_message(clientfd); + if (raw_req.has_value()) { + auto maybe_req = Request::build(raw_req.value()); + + if (maybe_req.has_value()) { + try { + resp = this->routes.at(maybe_req.value().query)(maybe_req.value()); + } catch (...) { + } + } + } + + // Send `resp` to client + + ::shutdown(clientfd, SHUT_RDWR); + ::close(clientfd); } - ::shutdown(clientfd, SHUT_RDWR); - ::close(clientfd); return {}; } } // namespace http diff --git a/src/listener.h b/src/listener.h index 00bc6d2..5fa0846 100644 --- a/src/listener.h +++ b/src/listener.h @@ -10,6 +10,7 @@ #include "error.h" #include "request.h" +#include "response.h" namespace http { struct Listener { @@ -34,7 +35,7 @@ namespace http { uint16_t backlog = 4096); public: - std::unordered_map> routes; + std::unordered_map> routes; private: int8_t socketfd; diff --git a/src/main.cpp b/src/main.cpp index 5fdd54f..ace77e4 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,13 +1,19 @@ #include #include "http.h" +#include "request.h" +#include "response.h" int main() { - auto server = http::Listener::create_on_local(8800); + auto server = http::Listener::on_local(8800); + server.value().routes["/"] = [](const http::Request &req) -> http::Response { + std::cout << req << std::endl; + return http::Response(); + }; + server.value().routes["/prova"] = [](const http::Request &req) -> http::Response { + std::cout << "Hello, World!" << std::endl; + auto _ = req.body; + return http::Response(); + }; server.value().serve(); - - server.value().routes["prova"] = [](const http::Request &req) { std::cout << req << std::endl; }; - - http::Request request; - server.value().routes["prova"](request); } diff --git a/src/method.h b/src/method.h index 3b787c1..fd980fa 100644 --- a/src/method.h +++ b/src/method.h @@ -1,6 +1,7 @@ #pragma once #include +#include namespace http { enum class Method { GET, @@ -14,6 +15,13 @@ namespace http { PATCH, UPDATE, }; + + const std::unordered_map method_map = { + {"get", Method::GET}, {"head", Method::HEAD}, {"post", Method::POST}, + {"put", Method::PUT}, {"delete", Method::DELETE}, {"connect", Method::CONNECT}, + {"options", Method::OPTIONS}, {"trace", Method::TRACE}, {"patch", Method::PATCH}, + {"update", Method::UPDATE}, + }; } // namespace http std::ostream &operator<<(std::ostream &os, const http::Method &method); diff --git a/src/request.cpp b/src/request.cpp index 0b19434..4758d51 100644 --- a/src/request.cpp +++ b/src/request.cpp @@ -1,8 +1,96 @@ #include "request.h" +#include +#include #include +#include +#include +#include + +#include "const_definitions.h" +#include "method.h" std::ostream &operator<<(std::ostream &os, const http::Request &req) { - return os << "Request { domain: " << std::quoted(req.domain_name) << ", port: " << req.port - << ", query: " << std::quoted(req.query) << ", body: " << std::quoted(req.body) << " }"; + os << "Request { domain: " << std::quoted(req.domain_name) << ", port: " << req.port + << ", query: " << std::quoted(req.query) << ", method: " << req.method + << ", version: " << (int)req.version.major << "." << (int)req.version.minor << ", headers: [ "; + + for (const auto &header : req.optheaders) { + os << header.first << " = " << header.second << ","; + } + + os << " ] }"; + return os; } + +namespace http { + std::expected Request::build(std::string_view raw_request) noexcept { + if (raw_request.empty()) { + return Err(Error::InvalidRequest); + } + + Request req; + + auto lines_view = raw_request | std::views::split(NEW_LINE); + auto lines_view_iter = lines_view.begin(); + + auto words = *lines_view_iter++ | std::views::split(' '); + auto word_iter = words.begin(); + + std::string_view method_view(*word_iter++); + std::string method(method_view); + std::transform(method_view.cbegin(), method_view.cend(), method.begin(), + [](unsigned char ch) { return std::tolower(ch); }); + + try { + req.method = method_map.at(method); + } catch (...) { + return Err(Error::InvalidRequestMethod); + } + + std::string_view query(*word_iter++); + std::cout << "\t" << query << std::endl; + if (query.empty() || !query.starts_with('/')) { + return Err(Error::InvalidQueryRequest); + } else { + req.query = query; + } + + std::string_view version(*word_iter); + auto number = version.substr(version.find_first_of('/') + 1); + auto major = number.substr(0, number.find_first_of('.')); + auto minor = number.substr(number.find_first_of('.') + 1); + + try { + req.version = {.major = (uint8_t)std::stoi(major.data()), + .minor = (uint8_t)std::stoi(minor.data())}; + } catch (...) { + return Err(Error::InvalidHTTPVersion); + } + + for (std::string_view line; lines_view_iter != lines_view.end() && + !(line = std::string_view(*lines_view_iter)).empty(); + ++lines_view_iter) { + auto words = line | std::views::split(std::string_view{":"}); + auto it = words.begin(); + std::string field_name((*it).begin(), (*it).end()); + ++it; + + if ((*it)[0] == ' ') { + req.optheaders[field_name] = std::string((*it).begin() + 1, (*it).end()); + } else { + req.optheaders[field_name] = std::string((*it).begin(), (*it).end()); + } + } + + while (++lines_view_iter != lines_view.end()) { + req.body += std::string((*lines_view_iter).begin(), (*lines_view_iter).end()); + + if (std::next(lines_view_iter) != lines_view.end()) { + req.body += "\n"; + } + } + + return req; + } +} // namespace http diff --git a/src/request.h b/src/request.h index 2705113..913abc8 100644 --- a/src/request.h +++ b/src/request.h @@ -1,22 +1,27 @@ #pragma once #include -#include +#include #include +#include #include -namespace http { - struct http_version { - uint8_t major = 1; - uint8_t minor = 1; - }; +#include "error.h" +#include "http_version.h" +#include "method.h" +namespace http { struct Request { + public: + static std::expected build(std::string_view raw_response) noexcept; + + public: uint16_t port = 80; std::string_view domain_name; - std::string_view query = "/"; - std::string_view body = ""; + std::string query = "/"; + std::string body = ""; + Method method; http_version version = {.major = 1, .minor = 1}; std::unordered_map optheaders = {}; diff --git a/src/response.cpp b/src/response.cpp index 4b02413..d3be232 100644 --- a/src/response.cpp +++ b/src/response.cpp @@ -1,10 +1,9 @@ +#include "response.h" +#include #include #include -#include #include -#include "http.h" - namespace http { std::expected Response::build(std::string_view raw_response) noexcept { if (raw_response.empty()) { @@ -27,12 +26,12 @@ namespace http { resp.version = {.major = (uint8_t)std::stoi(major.data()), .minor = (uint8_t)std::stoi(minor.data())}; } catch (...) { - return Err(Error::InvalidResponseHTTPVersion); + return Err(Error::InvalidHTTPVersion); } try { std::string_view status(*++word_iter); - resp.status = status_map[std::stoi(status.data())]; + resp.status = status_map.at(std::stoi(status.data())); } catch (...) { return Err(Error::InvalidResponseStatusCode); } diff --git a/src/response.h b/src/response.h index 2c54b62..11d64ee 100644 --- a/src/response.h +++ b/src/response.h @@ -1,149 +1,13 @@ #pragma once -#include #include #include -#include -#include +#include "const_definitions.h" #include "error.h" -#include "request.h" +#include "http_version.h" namespace http { - enum class Status { - CONTINUE = 100, - SWITCHING_PROTOCOLS = 101, - PROCESSING = 102, - - OK = 200, - CREATED, - ACCEPTED, - NON_AUTHORITATIVE_INFORMATION, - NO_CONTENT, - PARTIAL_CONTENT, - ALREADY_REPORTED, - IM_USED, - - MULTIPLE_CHOICES = 300, - MOVED_PERMANENTLY, - FOUND, - SEE_OTHER, - NOT_MODIFIED, - USE_PROXY, - UNUSED, - TEMPORARY_REDIRECT, - PERMANENT_REDIRECT, - - BAD_REQUEST = 400, - UNAUTHORIZED, - PAYMENT_REQUIRED, - FORBIDDEN, - NOT_FOUND, - METHOD_NOT_ALLOWED, - NOT_ACCEPTABLE, - PROXY_AUTHENTICATION_REQUIRED, - REQUEST_TIMEOUT, - CONFLICT, - GONE, - LENGTH_REQUIRED, - PRECONDITION_FAILED, - PAYLOAD_TOO_LARGE, - URI_TOO_LONG, - UNSUPPORTED_MEDIA_TYPE, - RANGE_NOT_SATISFIABLE, - EXPECTATION_FAILED, - IM_A_TEAPOT, - MISDIRECTED_REQUEST = 421, - UNPROCESSABLE_CONTENT, - LOCKED, - FAILED_DEPENDENCY, - TOO_EARLY, - UPGRADE_REQUIRED, - PRECONDITION_REQUIRED = 428, - TOO_MANY_REQUESTS, - REQUEST_HEADER_FIELDS_TOO_LARGE = 431, - UNAVAILABLE_FOR_LEGAL_REASONS = 451, - - INTERNAL_SERVER_ERROR = 500, - NOT_IMPLEMENTED, - BAD_GATEWAY, - SERVICE_UNAVAILABLE, - GATEWAY_TIMEOUT, - HTTP_VERSION_NOT_SUPPORTED, - VARIANT_ALSO_NEGOTIATES, - INSUFFICIENT_STORAGE, - LOOP_DETECTED, - NOT_EXTENDED, - NETWORK_AUTHENTICATION_REQUIRED, - }; - - static std::unordered_map status_map = { - {100, Status::CONTINUE}, - {101, Status::SWITCHING_PROTOCOLS}, - {102, Status::PROCESSING}, - - {200, Status::OK}, - {201, Status::CREATED}, - {202, Status::ACCEPTED}, - {203, Status::NON_AUTHORITATIVE_INFORMATION}, - {204, Status::NO_CONTENT}, - {205, Status::PARTIAL_CONTENT}, - {208, Status::ALREADY_REPORTED}, - {226, Status::IM_USED}, - - {300, Status::MULTIPLE_CHOICES}, - {301, Status::MOVED_PERMANENTLY}, - {302, Status::FOUND}, - {303, Status::SEE_OTHER}, - {304, Status::NOT_MODIFIED}, - {305, Status::USE_PROXY}, - {306, Status::UNUSED}, - {307, Status::TEMPORARY_REDIRECT}, - {308, Status::PERMANENT_REDIRECT}, - - {400, Status::BAD_REQUEST}, - {401, Status::UNAUTHORIZED}, - {402, Status::PAYMENT_REQUIRED}, - {403, Status::FORBIDDEN}, - {404, Status::NOT_FOUND}, - {405, Status::METHOD_NOT_ALLOWED}, - {406, Status::NOT_ACCEPTABLE}, - {407, Status::PROXY_AUTHENTICATION_REQUIRED}, - {408, Status::REQUEST_TIMEOUT}, - {409, Status::CONFLICT}, - {410, Status::GONE}, - {411, Status::LENGTH_REQUIRED}, - {412, Status::PRECONDITION_FAILED}, - {413, Status::PAYLOAD_TOO_LARGE}, - {414, Status::URI_TOO_LONG}, - {415, Status::UNSUPPORTED_MEDIA_TYPE}, - {416, Status::RANGE_NOT_SATISFIABLE}, - {417, Status::EXPECTATION_FAILED}, - {418, Status::IM_A_TEAPOT}, - {421, Status::MISDIRECTED_REQUEST}, - {422, Status::UNPROCESSABLE_CONTENT}, - {423, Status::LOCKED}, - {424, Status::FAILED_DEPENDENCY}, - {425, Status::TOO_EARLY}, - {426, Status::UPGRADE_REQUIRED}, - {428, Status::PRECONDITION_REQUIRED}, - {429, Status::TOO_MANY_REQUESTS}, - {431, Status::REQUEST_HEADER_FIELDS_TOO_LARGE}, - {451, Status::UNAVAILABLE_FOR_LEGAL_REASONS}, - - {500, Status::INTERNAL_SERVER_ERROR}, - {501, Status::NOT_IMPLEMENTED}, - {502, Status::BAD_GATEWAY}, - {503, Status::SERVICE_UNAVAILABLE}, - {504, Status::GATEWAY_TIMEOUT}, - {505, Status::HTTP_VERSION_NOT_SUPPORTED}, - {506, Status::VARIANT_ALSO_NEGOTIATES}, - {507, Status::INSUFFICIENT_STORAGE}, - {508, Status::LOOP_DETECTED}, - {510, Status::NOT_EXTENDED}, - {511, Status::NETWORK_AUTHENTICATION_REQUIRED}, - }; - struct Response { http_version version; Status status; @@ -151,11 +15,10 @@ namespace http { std::unordered_map fields; public: - static std::expected build(std::string_view raw_response) noexcept; + Response() : version({1,1}), status(Status::NOT_ACCEPTABLE) {} ~Response() {} - private: - Response() {} + static std::expected build(std::string_view raw_response) noexcept; }; } // namespace http