From c567bb2447be662166e6a4d85386d6584ce603fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?BENEDEK=20L=C3=A1szl=C3=B3?= Date: Tue, 5 Dec 2023 18:58:39 +0100 Subject: [PATCH] URI parsing --- CMakeLists.txt | 21 ++++++++----- inc/rum/http/http.h | 5 +-- inc/rum/http/method.h | 2 +- inc/rum/http/request.h | 11 ++++--- inc/rum/http/response.h | 3 +- inc/rum/http/server.h | 10 +++--- inc/rum/http/uri.h | 34 +++++++++++++++++++++ inc/rum/tcp/server.h | 8 ++++- inc/rum/tcp/utility.h | 2 +- src/rum/http/method.cpp | 4 +-- src/rum/http/request.cpp | 20 +++++++----- src/rum/http/response.cpp | 26 ++++++++++------ src/rum/http/server.cpp | 20 +++++++----- src/rum/http/uri.cpp | 64 +++++++++++++++++++++++++++++++++++++++ src/rum/tcp/server.cpp | 12 ++++---- src/server.cpp | 29 +++++++++++++++--- 16 files changed, 210 insertions(+), 61 deletions(-) create mode 100644 inc/rum/http/uri.h create mode 100644 src/rum/http/uri.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 05acd39..db34696 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -4,16 +4,18 @@ project(Rum VERSION 1.0) # compiler setup set(CMAKE_CXX_COMPILER "clang++") -#set(CMAKE_CXX_STANDARD 23) -#set(CMAKE_CXX_STANDARD_REQUIRED true) +set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -Wextra -Werror") # warnings +set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++2b") # std c++23 -set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -Wextra -Werror") # warnings and errors -set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++2b") # std c++23 for clang +if (STATIC) + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -static") # static +endif() -# set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -static") # static compile -# set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -g") # debug symbols -set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -O2") # enable optimisations +if (DEBUG) + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -g") # debug symbols +endif() +set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -O2") # optimisations # IDE setup (clangd) set(CMAKE_EXPORT_COMPILE_COMMANDS true) @@ -28,4 +30,9 @@ add_custom_target(run COMMAND $ DEPENDS server.bin COMMENT "Running server.bin" +) + +add_custom_target(docker + COMMAND docker build -t dowerx/rum:${CMAKE_PROJECT_VERSION} .. + COMMENT "Building docker image" ) \ No newline at end of file diff --git a/inc/rum/http/http.h b/inc/rum/http/http.h index 4492d07..cbae418 100644 --- a/inc/rum/http/http.h +++ b/inc/rum/http/http.h @@ -3,7 +3,4 @@ #include "method.h" #include "request.h" #include "response.h" -#include "server.h" - -#define WORKERS 10 -#define BUFFER_LEN 8196 \ No newline at end of file +#include "server.h" \ No newline at end of file diff --git a/inc/rum/http/method.h b/inc/rum/http/method.h index 1732b95..35bad52 100644 --- a/inc/rum/http/method.h +++ b/inc/rum/http/method.h @@ -5,5 +5,5 @@ namespace Rum::HTTP { enum Method { GET, HEAD, POST, PUT, DELETE, CONNECT, OPTIONS, TRACE }; -std::string method_to_string(Method m); +std::string to_string(Method method); } // namespace Rum::HTTP \ No newline at end of file diff --git a/inc/rum/http/request.h b/inc/rum/http/request.h index 0446822..af1530c 100644 --- a/inc/rum/http/request.h +++ b/inc/rum/http/request.h @@ -3,15 +3,16 @@ #include #include #include "method.h" +#include "rum/http/uri.h" namespace Rum::HTTP { class Request { public: - Request() : method(GET), path(""), body("") {} - Request(Method method, std::string path) : method(method), path(path), body("") {} + Request() : method(GET), uri(URI()), body("") {} + Request(Method method, const URI& uri) : method(method), uri(uri), body("") {} Method get_method() const { return method; } - std::string get_path() const { return path; } + const URI& get_uri() const { return uri; } const std::map& get_headers() const { return headers; } std::string get_header(std::string name) const { return headers.at(name); } @@ -20,13 +21,13 @@ class Request { std::string get_body() const { return body; } void set_body(std::string body) { this->body = body; } - operator std::string () const; + operator std::string() const; friend std::ostream& operator<<(std::ostream& stream, const Request req); private: Method method; - std::string path; + URI uri; std::map headers; std::string body; }; diff --git a/inc/rum/http/response.h b/inc/rum/http/response.h index d84cef9..9172470 100644 --- a/inc/rum/http/response.h +++ b/inc/rum/http/response.h @@ -8,10 +8,11 @@ class Response { const int client_sock; bool sent_header; bool sent_body; + bool closed; unsigned int code; public: - Response(int client_sock) : client_sock(client_sock), sent_header(false), sent_body(false), code(200) {} + Response(int client_sock) : client_sock(client_sock), sent_header(false), sent_body(false), closed(false), code(200) {} ~Response(); void send_header(const std::string& name, const std::string& value); diff --git a/inc/rum/http/server.h b/inc/rum/http/server.h index 2b0891b..9ef53f6 100644 --- a/inc/rum/http/server.h +++ b/inc/rum/http/server.h @@ -11,8 +11,8 @@ #include "request.h" #include "response.h" -#define WORKERS 10 -#define BUFFER_LEN 8196 +#define DEFAULT_WORKER_COUNT 10 +#define DEFAULT_BUFFER_SIZE 8196 namespace Rum::HTTP { @@ -25,6 +25,8 @@ class Server : public Rum::TCP::Server { sockaddr_in sockaddr; }; + const size_t buffer_size; + std::vector workers; std::queue tasks; std::mutex mtx; @@ -36,8 +38,8 @@ class Server : public Rum::TCP::Server { void handler(int client_sock, const sockaddr_in& client_address, char* buffer); public: - Server(unsigned int port, size_t worker_count); - Server(unsigned int port) : Server(port, WORKERS) {} + Server(unsigned int port, size_t worker_count, size_t buffer_size); + Server(unsigned int port) : Server(port, DEFAULT_WORKER_COUNT, DEFAULT_BUFFER_SIZE) {} ~Server(); void listen(); diff --git a/inc/rum/http/uri.h b/inc/rum/http/uri.h new file mode 100644 index 0000000..7970710 --- /dev/null +++ b/inc/rum/http/uri.h @@ -0,0 +1,34 @@ +#pragma once + +#include +#include +#include + +namespace Rum::HTTP { +class URI { + private: + std::string scheme; + std::string user; + std::string password; + std::string host; + std::string port; + std::string path; + std::map parameters; + std::string fragment; + + public: + URI(const std::string& uri); + URI() {} + + const std::string& get_scheme() const { return scheme; } + const std::string& get_user() const { return user; } + const std::string& get_password() const { return password; } + const std::string& get_host() const { return host; } + const std::string& get_port() const { return port; } + const std::string& get_path() const { return path; } + const std::map& get_parameters() const { return parameters; } + const std::string& get_fragment() const { return fragment; } + + operator std::string() const; +}; +} // namespace Rum::HTTP \ No newline at end of file diff --git a/inc/rum/tcp/server.h b/inc/rum/tcp/server.h index 5a3820d..9b263c0 100644 --- a/inc/rum/tcp/server.h +++ b/inc/rum/tcp/server.h @@ -4,16 +4,22 @@ #include #include +#define MAX_PENDING 10 + namespace Rum::TCP { class Server { private: const int sock; + unsigned short port; bool stop; public: Server(unsigned short port); virtual ~Server(); - void listen(std::function handler) const; + + unsigned short get_port() const { return port; } + + void listen(const std::function& handler) const; virtual void end() { stop = true; close(sock); diff --git a/inc/rum/tcp/utility.h b/inc/rum/tcp/utility.h index 059d198..bc8b836 100644 --- a/inc/rum/tcp/utility.h +++ b/inc/rum/tcp/utility.h @@ -5,6 +5,6 @@ namespace Rum::TCP { -std::string address_to_string(sockaddr_in address); +std::string to_string(const sockaddr_in& address); } // namespace Rum::TCP \ No newline at end of file diff --git a/src/rum/http/method.cpp b/src/rum/http/method.cpp index 248f763..decdeed 100644 --- a/src/rum/http/method.cpp +++ b/src/rum/http/method.cpp @@ -1,8 +1,8 @@ #include namespace Rum::HTTP { -std::string method_to_string(Method m) { - switch (m) { +std::string to_string(Method method) { + switch (method) { case GET: return "GET"; case HEAD: diff --git a/src/rum/http/request.cpp b/src/rum/http/request.cpp index 794ec7a..bcead53 100644 --- a/src/rum/http/request.cpp +++ b/src/rum/http/request.cpp @@ -1,18 +1,24 @@ #include +#include #include #include +#include "rum/http/method.h" namespace Rum::HTTP { Request::operator std::string() const { - std::string result("Request{\n\tmethod: " + method_to_string(method) + "\n\tpath: \"" + path + "\"\n\theaders:\n"); - for (auto header : get_headers()) { - result += "\t\t\"" + header.first + "\": \"" + header.second + "\"\n"; + std::string headers_string; + for (auto header : headers) { + headers_string += "\t\t'" + header.first + "': '" + header.second + "'\n"; } - - result += "\tbody: \"" + body + "\"\n}"; - - return result; + return std::vformat( + "Request{{\n" + "\tmethod: {}\n" + "\t{}\n" + "\theaders: \n{}" + "\tbody: \n'{}'" + "}}", + std::make_format_args(to_string(method), (std::string)uri, headers_string, body)); } std::ostream& operator<<(std::ostream& stream, const Request req) { diff --git a/src/rum/http/response.cpp b/src/rum/http/response.cpp index 5082a2a..3e67d56 100644 --- a/src/rum/http/response.cpp +++ b/src/rum/http/response.cpp @@ -5,8 +5,8 @@ #include namespace Rum::HTTP { -std::string status_code_to_string(int code) { - static std::map codes{{100, "Continue"}, +std::string to_string(int code) { + static const std::map codes{{100, "Continue"}, {101, "Switching Protocols"}, {102, "Processing"}, {103, "Early Hints"}, @@ -77,15 +77,19 @@ std::string status_code_to_string(int code) { void Response::send_header(const std::string& name, const std::string& value) { if (!sent_header) { - std::string resp("HTTP/1.1 " + std::to_string(code) + " " + status_code_to_string(code) + "\r\n"); - if (-1 == send(client_sock, resp.c_str(), resp.size(), 0)) + std::string resp("HTTP/1.1 " + std::to_string(code) + " " + to_string(code) + "\r\n"); + if (-1 == send(client_sock, resp.c_str(), resp.size(), 0)) { + closed = true; throw TCP::Error(TCP::Error::CLOSED); + } sent_header = true; } std::string header = name + ": " + value + "\r\n"; - if (-1 == send(client_sock, header.c_str(), header.size(), 0)) + if (-1 == send(client_sock, header.c_str(), header.size(), 0)) { + closed = true; throw TCP::Error(TCP::Error::CLOSED); + } } void Response::send_body(const std::string& value) { @@ -94,18 +98,22 @@ void Response::send_body(const std::string& value) { } if (!sent_body) { - if (-1 == send(client_sock, "\r\n", 2, 0)) + if (-1 == send(client_sock, "\r\n", 2, 0)) { + closed = true; throw TCP::Error(TCP::Error::CLOSED); + } sent_body = true; } - if (-1 == send(client_sock, value.c_str(), value.size(), 0)) + if (-1 == send(client_sock, value.c_str(), value.size(), 0)) { + closed = true; throw TCP::Error(TCP::Error::CLOSED); + } } Response::~Response() { - if (!sent_header) { - std::string resp("HTTP/1.1 " + std::to_string(code) + " " + status_code_to_string(code)); + if (!sent_header && !closed) { + std::string resp("HTTP/1.1 " + std::to_string(code) + " " + to_string(code)); send(client_sock, resp.c_str(), resp.size(), 0); } } diff --git a/src/rum/http/server.cpp b/src/rum/http/server.cpp index 26ee066..6433c51 100644 --- a/src/rum/http/server.cpp +++ b/src/rum/http/server.cpp @@ -1,14 +1,16 @@ +#include "rum/tcp/server.h" #include #include #include #include #include +#include namespace Rum::HTTP { -Server::Server(unsigned int port, size_t worker_count) : Rum::TCP::Server(port), stop(false) { +Server::Server(unsigned int port, size_t worker_count, size_t buffer_size) : Rum::TCP::Server(port), buffer_size(buffer_size), stop(false) { for (size_t i = 0; i < worker_count; i++) { std::thread worker([this, i]() { - char* buffer = new char[BUFFER_LEN](); + char* buffer = new char[this->buffer_size](); while (true) { Task task; @@ -30,7 +32,7 @@ Server::Server(unsigned int port, size_t worker_count) : Rum::TCP::Server(port), handler(task.client_sock, task.sockaddr, buffer); if (int status = close(task.client_sock); status == TCP::Error::UNKNOWN) { - std::cerr << TCP::address_to_string(task.sockaddr) << ": " << TCP::Error((TCP::Error::Type)status).what() << std::endl; + std::cerr << TCP::to_string(task.sockaddr) << ": " << TCP::Error((TCP::Error::Type)status).what() << std::endl; } } }); @@ -85,7 +87,7 @@ Method string_to_method(std::string text) { } void Server::handler(int client_sock, const sockaddr_in& client_address, char* buffer) { - std::string address = TCP::address_to_string(client_address); + std::string address = TCP::to_string(client_address); std::cout << address << ": connected" << std::endl; Request request; @@ -96,7 +98,7 @@ void Server::handler(int client_sock, const sockaddr_in& client_address, char* b std::string message; while (true) { - ssize_t recieved = recv(client_sock, buffer, BUFFER_LEN, 0); + ssize_t recieved = recv(client_sock, buffer, buffer_size, 0); switch (recieved) { case TCP::Error::CLOSED: std::cout << address << ": connection closed" << std::endl; @@ -109,10 +111,11 @@ void Server::handler(int client_sock, const sockaddr_in& client_address, char* b message += std::string(buffer, buffer + recieved); if (stage == METHOD && message.contains("\r\n")) { - std::regex method_regex("(GET|HEAD|POST|PUT|DELETE|CONNECT|OPTIONS|TRACE) (\\/.*) .*"); + std::regex method_regex("(GET|HEAD|POST|PUT|DELETE|CONNECT|OPTIONS|TRACE) (\\/.*) HTTP.*"); std::smatch match; if (std::regex_match(message.cbegin(), message.cbegin() + message.find("\r\n"), match, method_regex)) { - request = Request(string_to_method(match.str(1)), match.str(2)); + // create request object + request = Request(string_to_method(match.str(1)), URI("http://0.0.0.0:" + std::to_string(TCP::Server::get_port()) + match.str(2))); message = message.substr(message.find("\r\n")); stage = HEADER; } else { @@ -148,9 +151,10 @@ void Server::handler(int client_sock, const sockaddr_in& client_address, char* b bool found = false; for (auto it = paths[request.get_method()].cbegin(); it != paths[request.get_method()].cend(); it++) { - if (std::regex_match(request.get_path(), it->first)) { + if (std::regex_match(request.get_uri().get_path(), it->first)) { it->second(request, resp); found = true; + break; } } diff --git a/src/rum/http/uri.cpp b/src/rum/http/uri.cpp new file mode 100644 index 0000000..5989ff2 --- /dev/null +++ b/src/rum/http/uri.cpp @@ -0,0 +1,64 @@ +#include +#include +#include +#include +#include + +#define MATCH(var, i) \ + if (match[i].matched) \ + var = match.str(i); + +namespace Rum::HTTP { + +URI::operator std::string() const { + std::string query; + for (auto param : parameters) { + query += "\t\t'" + param.first + "': '" + param.second + "'\n"; + } + + return std::vformat( + "URI{{" + "\n\tscheme: '{}'" + "\n\tuser: '{}'" + "\n\tpassword: '{}'" + "\n\thost: '{}'" + "\n\tport: '{}'" + "\n\tpath: '{}'" + "\n\tquery: \n{}" + "\n\tfragment: '{}'" + "\n}}", + std::make_format_args(scheme, user, password, host, port, path, query, fragment)); +} + +URI::URI(const std::string& uri) { + std::regex uri_regex(R"((([\w\d]+):\/\/)?(([\w\d]+)(:([\w\d]+)?)@)?([\w\d\.]+)(:(\d+))?(\/?[\w\d\.\/]+)?(\?([\w\d\=\&]+))?(\#([\w\d]*))?)"); + + std::smatch match; + + std::cout << uri << std::endl; + + if (std::regex_match(uri.cbegin(), uri.cend(), match, uri_regex)) { + MATCH(scheme, 2) + MATCH(user, 4) + MATCH(password, 6) + MATCH(host, 7) + MATCH(port, 9) + MATCH(path, 10) + std::string query; + MATCH(query, 12) + MATCH(fragment, 14) + + std::stringstream parameters_string(query); + std::string parameter; + while (std::getline(parameters_string, parameter, '&')) { + if (size_t pos = parameter.find('='); pos != parameter.npos) { + parameters[parameter.substr(0, pos)] = parameter.substr(pos + 1); + } else { + parameters[parameter] = ""; + } + } + } else { + throw std::exception(); + } +} +} // namespace Rum::HTTP \ No newline at end of file diff --git a/src/rum/tcp/server.cpp b/src/rum/tcp/server.cpp index 3819c89..df591a1 100644 --- a/src/rum/tcp/server.cpp +++ b/src/rum/tcp/server.cpp @@ -3,21 +3,22 @@ #include #include #include +#include #include #include -#define MAX_PENDING 10 - namespace Rum::TCP { -std::string address_to_string(sockaddr_in address) { +std::string to_string(const sockaddr_in& address) { return std::string(inet_ntoa(address.sin_addr)) + ":" + std::to_string(ntohs(address.sin_port)); } -Server::Server(unsigned short port) : sock(socket(AF_INET, SOCK_STREAM, 0)), stop(false) { +Server::Server(unsigned short port) : sock(socket(AF_INET, SOCK_STREAM, 0)), port(port), stop(false) { if (sock == -1) throw Error(Error::UNKNOWN); + std::signal(SIGPIPE, SIG_IGN); + sockaddr_in address = {.sin_family = AF_INET, .sin_port = htons(port), .sin_addr = {.s_addr = INADDR_ANY}}; if (bind(sock, (struct sockaddr*)&address, sizeof(address)) == -1) @@ -28,10 +29,9 @@ Server::~Server() { end(); } -void Server::listen(std::function handler) const { +void Server::listen(const std::function& handler) const { if (::listen(sock, MAX_PENDING) == -1) throw Error(Error::UNKNOWN); - ; while (!stop) { sockaddr_in client_address; diff --git a/src/server.cpp b/src/server.cpp index d8d01f7..36cfb2e 100644 --- a/src/server.cpp +++ b/src/server.cpp @@ -8,24 +8,43 @@ Rum::HTTP::Server* server; int main(int argc, char** argv) { Flags::Parser parser; - int* port = parser.add("port", "tcp port number", true, 8080); - parser.parse(argc, argv); + int* port = parser.add("port", "tcp port number", false, 8080); + unsigned long int* workers = parser.add("workers", "number of worker threads", false, 10); + + bool* help = parser.add("help", "print help", false, false); + + if (!parser.parse(argc, argv) || *help) { + parser.help(); + return -1; + } std::cout << "Port: " << *port << std::endl; + std::cout << "Workers: " << *workers << std::endl; try { - server = new Rum::HTTP::Server(*port); + server = new Rum::HTTP::Server(*port, *workers, DEFAULT_BUFFER_SIZE); + std::signal(SIGINT, [](int) { std::cout << "\nStopping server..." << std::endl; server->end(); }); - server->add_path("/", [](const Rum::HTTP::Request&, Rum::HTTP::Response& resp) { + + server->add_path("/asd", [](const Rum::HTTP::Request& req, Rum::HTTP::Response& resp) { std::cout << "request accepted" << std::endl; - resp.send_body("

Hello World

"); + resp.send_body("

asd

" + (std::string)req + "
"); }); + + server->add_path("/.*", [](const Rum::HTTP::Request& req, Rum::HTTP::Response& resp) { + std::cout << "request accepted" << std::endl; + resp.send_body("

Hello World

" + (std::string)req + "
"); + }); + server->listen(); + delete server; + } catch (Rum::TCP::Error&) { std::cerr << "Failed to bind port " << *port << std::endl; + return -1; } } \ No newline at end of file