commit c62c4f4ff920eb840dde804ce044543f73a00774 Author: BENEDEK László Date: Mon Dec 4 17:40:14 2023 +0100 init diff --git a/.clang-format b/.clang-format new file mode 100644 index 0000000..20677a2 --- /dev/null +++ b/.clang-format @@ -0,0 +1,9 @@ +BasedOnStyle: Chromium +IndentWidth: 2 +ColumnLimit: 160 +SpaceAfterCStyleCast: false +UseTab: Never +AllowShortIfStatementsOnASingleLine: false +AlignTrailingComments: false +SpacesBeforeTrailingComments: 1 +AlignConsecutiveMacros: Consecutive diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e0b65e9 --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +.vscode +.cache + +build + +inc/flags +src/flags \ No newline at end of file diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..05acd39 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,31 @@ +cmake_minimum_required(VERSION 3.25) + +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 and errors +set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++2b") # std c++23 for clang + +# 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 + + +# IDE setup (clangd) +set(CMAKE_EXPORT_COMPILE_COMMANDS true) + +file(GLOB_RECURSE SOURCES "${CMAKE_SOURCE_DIR}/src/*.cpp") + +add_executable(server.bin ${SOURCES}) + +target_include_directories(server.bin PRIVATE "${CMAKE_SOURCE_DIR}/inc") + +add_custom_target(run + COMMAND $ + DEPENDS server.bin + COMMENT "Running server.bin" +) \ No newline at end of file diff --git a/inc/rum/http/http.h b/inc/rum/http/http.h new file mode 100644 index 0000000..4492d07 --- /dev/null +++ b/inc/rum/http/http.h @@ -0,0 +1,9 @@ +#pragma once + +#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 diff --git a/inc/rum/http/method.h b/inc/rum/http/method.h new file mode 100644 index 0000000..1732b95 --- /dev/null +++ b/inc/rum/http/method.h @@ -0,0 +1,9 @@ +#pragma once + +#include +namespace Rum::HTTP { + +enum Method { GET, HEAD, POST, PUT, DELETE, CONNECT, OPTIONS, TRACE }; + +std::string method_to_string(Method m); +} // namespace Rum::HTTP \ No newline at end of file diff --git a/inc/rum/http/request.h b/inc/rum/http/request.h new file mode 100644 index 0000000..0446822 --- /dev/null +++ b/inc/rum/http/request.h @@ -0,0 +1,33 @@ +#pragma once + +#include +#include +#include "method.h" + +namespace Rum::HTTP { +class Request { + public: + Request() : method(GET), path(""), body("") {} + Request(Method method, std::string path) : method(method), path(path), body("") {} + + Method get_method() const { return method; } + std::string get_path() const { return path; } + + const std::map& get_headers() const { return headers; } + std::string get_header(std::string name) const { return headers.at(name); } + void set_header(std::string name, std::string value) { headers[name] = value; } + + std::string get_body() const { return body; } + void set_body(std::string body) { this->body = body; } + + operator std::string () const; + + friend std::ostream& operator<<(std::ostream& stream, const Request req); + + private: + Method method; + std::string path; + std::map headers; + std::string body; +}; +} // namespace Rum::HTTP \ No newline at end of file diff --git a/inc/rum/http/response.h b/inc/rum/http/response.h new file mode 100644 index 0000000..d84cef9 --- /dev/null +++ b/inc/rum/http/response.h @@ -0,0 +1,21 @@ +#pragma once + +#include + +namespace Rum::HTTP { +class Response { + private: + const int client_sock; + bool sent_header; + bool sent_body; + unsigned int code; + + public: + Response(int client_sock) : client_sock(client_sock), sent_header(false), sent_body(false), code(200) {} + ~Response(); + + void send_header(const std::string& name, const std::string& value); + void send_body(const std::string& value); + void set_code(unsigned int code) { this->code = code; } +}; +} // namespace Rum::HTTP \ No newline at end of file diff --git a/inc/rum/http/server.h b/inc/rum/http/server.h new file mode 100644 index 0000000..2b0891b --- /dev/null +++ b/inc/rum/http/server.h @@ -0,0 +1,52 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include "../tcp/server.h" +#include "request.h" +#include "response.h" + +#define WORKERS 10 +#define BUFFER_LEN 8196 + +namespace Rum::HTTP { + +class Server : public Rum::TCP::Server { + private: + struct Task { + Task() {} + Task(int client_sock, const sockaddr_in& sockaddr) : client_sock(client_sock), sockaddr(sockaddr) {} + int client_sock; + sockaddr_in sockaddr; + }; + + std::vector workers; + std::queue tasks; + std::mutex mtx; + std::condition_variable condition; + bool stop; + + std::map>>> paths; + + 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(); + void listen(); + + void end() override; + + template + void add_path(const std::string& path, const std::function& callback) { + paths[M].push_back(std::pair>(std::regex(path), callback)); + } +}; + +} // namespace Rum::HTTP \ No newline at end of file diff --git a/inc/rum/tcp/error.h b/inc/rum/tcp/error.h new file mode 100644 index 0000000..7954eca --- /dev/null +++ b/inc/rum/tcp/error.h @@ -0,0 +1,18 @@ +#pragma once + +#include + +namespace Rum::TCP { + +class Error : public std::exception { + public: + enum Type { CLOSED = 0, UNKNOWN = -1 }; + + Error(Type type) : type(type) {} + + const char* what() const noexcept override; + + private: + Type type; +}; +} // namespace Rum::TCP \ No newline at end of file diff --git a/inc/rum/tcp/server.h b/inc/rum/tcp/server.h new file mode 100644 index 0000000..5a3820d --- /dev/null +++ b/inc/rum/tcp/server.h @@ -0,0 +1,22 @@ +#pragma once + +#include +#include +#include + +namespace Rum::TCP { +class Server { + private: + const int sock; + bool stop; + + public: + Server(unsigned short port); + virtual ~Server(); + void listen(std::function handler) const; + virtual void end() { + stop = true; + close(sock); + } +}; +} // namespace Rum::TCP \ No newline at end of file diff --git a/inc/rum/tcp/tcp.h b/inc/rum/tcp/tcp.h new file mode 100644 index 0000000..9c0d1d6 --- /dev/null +++ b/inc/rum/tcp/tcp.h @@ -0,0 +1,5 @@ +#pragma once + +#include "error.h" +#include "server.h" +#include "utility.h" \ No newline at end of file diff --git a/inc/rum/tcp/utility.h b/inc/rum/tcp/utility.h new file mode 100644 index 0000000..059d198 --- /dev/null +++ b/inc/rum/tcp/utility.h @@ -0,0 +1,10 @@ +#pragma once + +#include +#include + +namespace Rum::TCP { + +std::string address_to_string(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 new file mode 100644 index 0000000..248f763 --- /dev/null +++ b/src/rum/http/method.cpp @@ -0,0 +1,24 @@ +#include + +namespace Rum::HTTP { +std::string method_to_string(Method m) { + switch (m) { + case GET: + return "GET"; + case HEAD: + return "HEAD"; + case POST: + return "POST"; + case PUT: + return "PUT"; + case DELETE: + return "DELETE"; + case CONNECT: + return "CONNECT"; + case OPTIONS: + return "OPTIONS"; + case TRACE: + return "TRACE"; + } +} +} // namespace Rum::HTTP diff --git a/src/rum/http/request.cpp b/src/rum/http/request.cpp new file mode 100644 index 0000000..794ec7a --- /dev/null +++ b/src/rum/http/request.cpp @@ -0,0 +1,23 @@ +#include +#include +#include + +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"; + } + + result += "\tbody: \"" + body + "\"\n}"; + + return result; +} + +std::ostream& operator<<(std::ostream& stream, const Request req) { + stream << (std::string)req; + return stream; +} + +} // namespace Rum::HTTP \ No newline at end of file diff --git a/src/rum/http/response.cpp b/src/rum/http/response.cpp new file mode 100644 index 0000000..5082a2a --- /dev/null +++ b/src/rum/http/response.cpp @@ -0,0 +1,113 @@ +#include +#include +#include +#include +#include + +namespace Rum::HTTP { +std::string status_code_to_string(int code) { + static std::map codes{{100, "Continue"}, + {101, "Switching Protocols"}, + {102, "Processing"}, + {103, "Early Hints"}, + {200, "OK"}, + {201, "Created"}, + {202, "Accepted"}, + {203, "Non-Authoritative Information"}, + {204, "No Content"}, + {205, "Reset Content"}, + {206, "Partial Content"}, + {207, "Multi-Status"}, + {208, "Already Reported"}, + {226, "IM Used"}, + {300, "Multiple Choices"}, + {301, "Moved Permanently"}, + {302, "Found"}, + {303, "See Other"}, + {304, "Not Modified"}, + {307, "Temporary Redirect"}, + {308, "Permanent Redirect"}, + {400, "Bad Request"}, + {401, "Unauthorized"}, + {402, "Payment Required"}, + {403, "Forbidden"}, + {404, "Not Found"}, + {405, "Method Not Allowed"}, + {406, "Not Acceptable"}, + {407, "Proxy Authentication Required"}, + {408, "Request Timeout"}, + {409, "Conflict"}, + {410, "Gone"}, + {411, "Length Required"}, + {412, "Precondition Failed"}, + {413, "Content Too Large"}, + {414, "URI Too Long"}, + {415, "Unsupported Media Type"}, + {416, "Range Not Satisfiable"}, + {417, "Expectation Failed"}, + {418, "I'm a teapot"}, + {421, "Misdirected Request"}, + {422, "Unprocessable Content"}, + {423, "Locked"}, + {424, "Failed Dependency"}, + {425, "Too Early"}, + {426, "Upgrade Required"}, + {428, "Precondition Required"}, + {429, "Too Many Requests"}, + {431, "Request Header Fields Too Large"}, + {451, "Unavailable For Legal Reasons"}, + {500, "Internal Server Error"}, + {501, "Not Implemented"}, + {502, "Bad Gateway"}, + {503, "Service Unavailable"}, + {504, "Gateway Timeout"}, + {505, "HTTP Version Not Supported"}, + {506, "Variant Also Negotiates"}, + {507, "Insufficient Storage"}, + {508, "Loop Detected"}, + {510, "Not Extended"}, + {511, "Network Authentication Required"}}; + + try { + return codes.at(code); + } catch (std::out_of_range) { + return codes.at((code / 100) * 100); + } +} + +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)) + 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)) + throw TCP::Error(TCP::Error::CLOSED); +} + +void Response::send_body(const std::string& value) { + if (!sent_header) { + send_header("Content-Type", "text/html"); + } + + if (!sent_body) { + if (-1 == send(client_sock, "\r\n", 2, 0)) + throw TCP::Error(TCP::Error::CLOSED); + sent_body = true; + } + + if (-1 == send(client_sock, value.c_str(), value.size(), 0)) + 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)); + send(client_sock, resp.c_str(), resp.size(), 0); + } +} + +} // namespace Rum::HTTP \ No newline at end of file diff --git a/src/rum/http/server.cpp b/src/rum/http/server.cpp new file mode 100644 index 0000000..26ee066 --- /dev/null +++ b/src/rum/http/server.cpp @@ -0,0 +1,165 @@ +#include +#include +#include +#include +#include + +namespace Rum::HTTP { +Server::Server(unsigned int port, size_t worker_count) : Rum::TCP::Server(port), stop(false) { + for (size_t i = 0; i < worker_count; i++) { + std::thread worker([this, i]() { + char* buffer = new char[BUFFER_LEN](); + + while (true) { + Task task; + + { + std::unique_lock lock(mtx); + condition.wait(lock, [this]() { return stop || !tasks.empty(); }); + + if (stop && tasks.empty()) { + delete[] buffer; + return; + } + + task = tasks.front(); + tasks.pop(); + } + + std::cout << "Worker #" << i << " accepted a connection." << std::endl; + + 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; + } + } + }); + workers.emplace_back(std::move(worker)); + std::cout << "Worker #" << i << " created" << std::endl; + } +} + +void Server::end() { + if (stop) + return; + + TCP::Server::end(); + + { + std::unique_lock lock(mtx); + stop = true; + } + condition.notify_all(); + for (auto& worker : workers) + worker.join(); +} + +Server::~Server() { + end(); +} + +void Server::listen() { + Rum::TCP::Server::listen([this](int client_sock, sockaddr_in client_address) { + tasks.emplace(client_sock, client_address); + condition.notify_one(); + }); +} + +Method string_to_method(std::string text) { + if (text.starts_with("GET")) + return GET; + if (text.starts_with("HEAD")) + return HEAD; + if (text.starts_with("POST")) + return POST; + if (text.starts_with("PUT")) + return PUT; + if (text.starts_with("DELETE")) + return DELETE; + if (text.starts_with("CONNECT")) + return CONNECT; + if (text.starts_with("OPTIONS")) + return OPTIONS; + // if (text.starts_with("TRACE")) + return TRACE; +} + +void Server::handler(int client_sock, const sockaddr_in& client_address, char* buffer) { + std::string address = TCP::address_to_string(client_address); + std::cout << address << ": connected" << std::endl; + + Request request; + + enum Stage { METHOD, HEADER, BODY }; + Stage stage = METHOD; + + std::string message; + + while (true) { + ssize_t recieved = recv(client_sock, buffer, BUFFER_LEN, 0); + switch (recieved) { + case TCP::Error::CLOSED: + std::cout << address << ": connection closed" << std::endl; + return; + case TCP::Error::UNKNOWN: + std::cout << "socket error" << std::endl; + return; + } + + 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::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)); + message = message.substr(message.find("\r\n")); + stage = HEADER; + } else { + return; + } + } + + if (stage == HEADER && message.contains("\r\n\r\n")) { + std::regex header_regex("(.*): (.*)"); + for (std::sregex_iterator it = std::sregex_iterator(message.cbegin(), message.cbegin() + message.find("\r\n\r\n"), header_regex); + it != std::sregex_iterator(); it++) { + request.set_header(it->str(1), it->str(2)); + } + message = message.substr(message.find("\r\n\r\n")); + + if (Method method = request.get_method(); method == POST || method == PUT) + stage = BODY; + else + break; + } + + if (stage == BODY) { + request.set_body(message); + break; + } + } + + std::cout << request << std::endl; + + try { + Response resp(client_sock); + + 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)) { + it->second(request, resp); + found = true; + } + } + + if (!found) { + resp.set_code(404); + resp.send_body("

404: Page not found :C

"); + } + } catch (std::out_of_range) { + } catch (TCP::Error) { + } +} +} // namespace Rum::HTTP \ No newline at end of file diff --git a/src/rum/tcp/error.cpp b/src/rum/tcp/error.cpp new file mode 100644 index 0000000..d1c6325 --- /dev/null +++ b/src/rum/tcp/error.cpp @@ -0,0 +1,13 @@ +#include + +namespace Rum::TCP { + +const char* Error::what() const noexcept { + switch (type) { + case CLOSED: + return "the connection is already closed"; + case UNKNOWN: + return "an unknown error has occured"; + } +} +} // namespace Rum::TCP \ No newline at end of file diff --git a/src/rum/tcp/server.cpp b/src/rum/tcp/server.cpp new file mode 100644 index 0000000..3819c89 --- /dev/null +++ b/src/rum/tcp/server.cpp @@ -0,0 +1,47 @@ +#include +#include +#include +#include +#include +#include +#include + +#define MAX_PENDING 10 + +namespace Rum::TCP { + +std::string address_to_string(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) { + if (sock == -1) + throw Error(Error::UNKNOWN); + + 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) + throw Error(Error::UNKNOWN); +} + +Server::~Server() { + end(); +} + +void Server::listen(std::function handler) const { + if (::listen(sock, MAX_PENDING) == -1) + throw Error(Error::UNKNOWN); + ; + + while (!stop) { + sockaddr_in client_address; + socklen_t client_address_len(sizeof(client_address)); + int client_sock = accept(sock, (struct sockaddr*)&client_address, &client_address_len); + if (client_sock == -1) + continue; + + handler(client_sock, client_address); + } +} + +} // namespace Rum::TCP \ No newline at end of file diff --git a/src/server.cpp b/src/server.cpp new file mode 100644 index 0000000..d8d01f7 --- /dev/null +++ b/src/server.cpp @@ -0,0 +1,31 @@ +#include +#include +#include +#include +#include + +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); + + std::cout << "Port: " << *port << std::endl; + + try { + server = new Rum::HTTP::Server(*port); + std::signal(SIGINT, [](int) { + std::cout << "\nStopping server..." << std::endl; + server->end(); + }); + server->add_path("/", [](const Rum::HTTP::Request&, Rum::HTTP::Response& resp) { + std::cout << "request accepted" << std::endl; + resp.send_body("

Hello World

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