diff --git a/Makefile b/Makefile index a7a4fb9..f094c19 100644 --- a/Makefile +++ b/Makefile @@ -55,9 +55,8 @@ CLANG_STDLIB ?= -stdlib=libc++ SRCS := src/Log.cpp src/main.cpp src/Server.cpp \ src/config/Config.cpp src/config/ConfigurationParser.cpp src/config/LocationConfig.cpp src/config/ServerConfig.cpp \ src/event/Epoll.cpp src/event/EventToken.cpp \ - src/net/ByteBuffer.cpp src/net/Connection.cpp src/net/FileDescriptor.cpp src/net/Listener.cpp src/net/ListenerPlan.cpp src/net/PauseMask.cpp src/net/SlotPool.cpp - -# src/http/HttpStatus.cpp \ + src/net/ByteBuffer.cpp src/net/Connection.cpp src/net/FileDescriptor.cpp src/net/Listener.cpp src/net/ListenerPlan.cpp src/net/PauseMask.cpp src/net/SlotPool.cpp \ + src/http/Request.cpp src/http/RequestParser.cpp OBJS := $(SRCS:$(SRC_DIR)/%.cpp=$(OBJ_DIR)/%.o) @@ -67,7 +66,7 @@ SRCS := src/Log.cpp src/main.cpp src/Server.cpp \ DEPS := $(OBJS:.o=.d) HDRS := include/config/Config.hpp include/config/ConfigurationParser.hpp include/config/LocationConfig.hpp include/config/ServerConfig.hpp \ include/event/Epoll.hpp include/event/EventToken.hpp \ - include/http/HttpStatus.hpp \ + include/http/HttpStatus.hpp include/http/Request.hpp include/http/RequestParser.hpp \ include/net/ByteBuffer.hpp include/net/Connection.hpp include/net/FileDescriptor.hpp include/net/Listener.hpp include/net/ListenerPlan.hpp include/net/PauseMask.hpp include/net/SlotPool.hpp \ include/Log.hpp include/Result.hpp include/Server.hpp \ diff --git a/include/http/Request.hpp b/include/http/Request.hpp new file mode 100644 index 0000000..76dcac9 --- /dev/null +++ b/include/http/Request.hpp @@ -0,0 +1,40 @@ +/*********************************/ +/* */ +/* o.riabenkyi@gmail.com */ +/* */ +/*********************************/ + +#ifndef REQUEST_HPP +#define REQUEST_HPP + +#include +#include +#include +#include +#include + +namespace webserv { + +enum class Method { Get, Post, Delete }; + + const char* toString(Method method) noexcept; + + bool headerNameEquals(std::string_view a, std::string_view b) noexcept; + + enum class HttpVersion { Http10, Http11 }; + + struct Request { + Method method; + std::string target; + HttpVersion version; + + std::vector> headers; + std::vector body; + + std::optional header(std::string_view name) const noexcept; + }; + +} + +#endif + diff --git a/include/http/RequestParser.hpp b/include/http/RequestParser.hpp new file mode 100644 index 0000000..cf0d82b --- /dev/null +++ b/include/http/RequestParser.hpp @@ -0,0 +1,29 @@ +/*********************************/ +/* */ +/* o.riabenkyi@gmail.com */ +/* */ +/*********************************/ + +#ifndef REQUESTPARSER_HPP +#define REQUESTPARSER_HPP + +#include +#include +#include + +#include "Result.hpp" +#include "http/Request.hpp" + +namespace webserv { + + struct ParsedRequest { + Request request; + std::size_t consumed; + }; + + [[nodiscard]] std::optional> parseRequest( + std::span data, std::size_t limit); + +} // namespace webserv + +#endif diff --git a/src/http/Request.cpp b/src/http/Request.cpp new file mode 100644 index 0000000..04b572d --- /dev/null +++ b/src/http/Request.cpp @@ -0,0 +1,44 @@ +/* ************************************************************************** */ +/* */ +/* ::: :::::::: */ +/* Request.cpp :+: :+: :+: */ +/* +:+ +:+ +:+ */ +/* +#+#+#+#+#+ +#+ */ +/* */ +/* ************************************************************************** */ + +#include "http/Request.hpp" + +#include +#include + +namespace webserv { + +const char* toString(Method method) noexcept { + switch (method) { + case Method::Get: + return "GET"; + case Method::Post: + return "POST"; + case Method::Delete: + return "DELETE"; + } + return "GET"; +} + +bool headerNameEquals(std::string_view a, std::string_view b) noexcept { + return std::ranges::equal(a, b, [](unsigned char x, unsigned char y) { + return std::tolower(x) == std::tolower(y); + }); +} + +std::optional Request::header(std::string_view name) const noexcept { + for (const auto& [fieldName, value] : headers) { + if (headerNameEquals(fieldName, name)) { + return std::string_view(value); + } + } + return std::nullopt; +} + +} // namespace webserv diff --git a/src/http/RequestParser.cpp b/src/http/RequestParser.cpp new file mode 100644 index 0000000..4afd39b --- /dev/null +++ b/src/http/RequestParser.cpp @@ -0,0 +1,171 @@ +/* ************************************************************************** */ +/* */ +/* ::: :::::::: */ +/* RequestParser.cpp :+: :+: :+: */ +/* +:+ +:+ +:+ */ +/* +#+#+#+#+#+ +#+ */ +/* */ +/* ************************************************************************** */ + +#include "http/RequestParser.hpp" + +#include +#include +#include +#include +#include + +#include "http/HttpStatus.hpp" + +namespace webserv { + +namespace { + +std::optional parseMethod(std::string_view token) { + if (token == "GET") return Method::Get; + if (token == "POST") return Method::Post; + if (token == "DELETE") return Method::Delete; + return std::nullopt; +} + +std::optional parseVersion(std::string_view token) { + if (token == "HTTP/1.1") return HttpVersion::Http11; + if (token == "HTTP/1.0") return HttpVersion::Http10; + return std::nullopt; +} + +std::string_view trim(std::string_view value) { + while (!value.empty() && (value.front() == ' ' || value.front() == '\t')) + value.remove_prefix(1); + while (!value.empty() && (value.back() == ' ' || value.back() == '\t')) + value.remove_suffix(1); + return value; +} + +std::optional> splitRequestLine(std::string_view line) { + const std::size_t firstSpace = line.find(' '); + if (firstSpace == std::string_view::npos) return std::nullopt; + const std::size_t secondSpace = line.find(' ', firstSpace + 1); + if (secondSpace == std::string_view::npos) return std::nullopt; + if (line.find(' ', secondSpace + 1) != std::string_view::npos) return std::nullopt; + + return std::array{ + line.substr(0, firstSpace), + line.substr(firstSpace + 1, secondSpace - firstSpace - 1), + line.substr(secondSpace + 1)}; +} + +std::optional> parseHeaderLine(std::string_view line) { + const std::size_t colon = line.find(':'); + if (colon == std::string_view::npos || colon == 0) return std::nullopt; + + const std::string_view name = line.substr(0, colon); + if (name.find(' ') != std::string_view::npos || name.find('\t') != std::string_view::npos) + return std::nullopt; + + const std::string_view value = trim(line.substr(colon + 1)); + return std::pair(std::string(name), std::string(value)); +} + +std::optional parseContentLength(std::string_view value) { + if (value.empty()) return std::nullopt; + std::size_t result = 0; + const auto parsed = std::from_chars(value.data(), value.data() + value.size(), result); + if (parsed.ec != std::errc{} || parsed.ptr != value.data() + value.size()) + return std::nullopt; + return result; +} + +} // namespace + +std::optional> parseRequest(std::span data, std::size_t limit) { + const std::string_view view(data.data(), data.size()); + + const std::size_t headEnd = view.find("\r\n\r\n"); + if (headEnd == std::string_view::npos) { + if (data.size() >= limit) + return Result(std::unexpected(HttpStatus::RequestHeaderFieldsTooLarge)); + return std::nullopt; + } + const std::size_t headBytes = headEnd + 4; + + if (headBytes > limit) + return Result(std::unexpected(HttpStatus::RequestHeaderFieldsTooLarge)); + + std::string_view remaining = view.substr(0, headEnd); + + const std::size_t lineEnd = remaining.find("\r\n"); + const std::string_view requestLine = remaining.substr(0, lineEnd); + remaining = (lineEnd == std::string_view::npos) ? std::string_view() + : remaining.substr(lineEnd + 2); + + const auto fields = splitRequestLine(requestLine); + if (!fields.has_value()) + return Result(std::unexpected(HttpStatus::BadRequest)); + + const auto method = parseMethod((*fields)[0]); + if (!method.has_value()) + return Result(std::unexpected(HttpStatus::NotImplemented)); + + const std::string_view target = (*fields)[1]; + if (target.empty() || target.front() != '/') + return Result(std::unexpected(HttpStatus::BadRequest)); + + const auto version = parseVersion((*fields)[2]); + if (!version.has_value()) + return Result(std::unexpected(HttpStatus::HttpVersionNotSupported)); + + Request request; + request.method = *method; + request.target = std::string(target); + request.version = *version; + + while (!remaining.empty()) { + const std::size_t next = remaining.find("\r\n"); + const std::string_view headerLine = remaining.substr(0, next); + remaining = (next == std::string_view::npos) ? std::string_view() + : remaining.substr(next + 2); + + auto field = parseHeaderLine(headerLine); + if (!field.has_value()) + return Result(std::unexpected(HttpStatus::BadRequest)); + request.headers.push_back(std::move(*field)); + } + + if (request.version == HttpVersion::Http11 && !request.header("Host").has_value()) + return Result(std::unexpected(HttpStatus::BadRequest)); + + bool hasTransferEncoding = false; + std::optional contentLength; + for (const auto& [name, value] : request.headers) { + if (headerNameEquals(name, "transfer-encoding")) { + hasTransferEncoding = true; + } else if (headerNameEquals(name, "content-length")) { + const auto parsed = parseContentLength(value); + if (!parsed.has_value()) + return Result(std::unexpected(HttpStatus::BadRequest)); + if (contentLength.has_value() && *contentLength != *parsed) + return Result(std::unexpected(HttpStatus::BadRequest)); + contentLength = parsed; + } + } + + if (hasTransferEncoding && contentLength.has_value()) + return Result(std::unexpected(HttpStatus::BadRequest)); + if (hasTransferEncoding) { + return Result(std::unexpected(HttpStatus::NotImplemented)); + } + + const std::size_t bodyLength = contentLength.value_or(0); + if (bodyLength > limit - headBytes) + return Result(std::unexpected(HttpStatus::ContentTooLarge)); + + if (data.size() < headBytes + bodyLength) return std::nullopt; + + request.body.assign(data.begin() + static_cast(headBytes), + data.begin() + static_cast(headBytes + bodyLength)); + + return Result(ParsedRequest{std::move(request), headBytes + bodyLength}); +} + +} // namespace webserv