From acb7221e34dc0a9a34a959df09fc52c9223435c4 Mon Sep 17 00:00:00 2001 From: Thomas Schleicher Date: Mon, 16 Dec 2024 11:30:41 +0100 Subject: [PATCH 01/10] database header --- includes/database_service.hpp | 15 +++++++++++---- src/database_connection.cpp | 33 ++++++++++++++++++++++++++++++--- 2 files changed, 41 insertions(+), 7 deletions(-) diff --git a/includes/database_service.hpp b/includes/database_service.hpp index 4d997e3..a374260 100644 --- a/includes/database_service.hpp +++ b/includes/database_service.hpp @@ -2,27 +2,34 @@ #define DATABASE_SERVICE_HPP #include +#include #include #include +#include #include +#include class DatabaseService { public: static DatabaseService& getInstance(boost::asio::io_context& io_context, std::size_t pool_size); + void asyncSetKey(const std::string& key, const std::string& value, std::function callback); + void asyncGetKey(const std::string& key, std::function callback); //we could use a varient here for the result - -private: - std::shared_ptr getConnection(); //retrive a connection from the connection pool - +private: DatabaseService(boost::asio::io_context& io_context, std::size_t pool_size = 4); ~DatabaseService(); DatabaseService(DatabaseService const&); void operator=(DatabaseService const&); + std::shared_ptr getConnection(); + boost::asio::io_context& io_context_; std::size_t pool_size_; std::vector> connection_pool_; std::size_t current_index_ = 0; //index used for round robin selection + + static DatabaseService* INSTANCE; + static std::mutex singleton_mutex; }; #endif \ No newline at end of file diff --git a/src/database_connection.cpp b/src/database_connection.cpp index 194bf2b..6ff47b7 100644 --- a/src/database_connection.cpp +++ b/src/database_connection.cpp @@ -1,16 +1,43 @@ #include "../includes/database_service.hpp" +#include #include +#include +#include +#include +std::mutex DatabaseService::singleton_mutex; +DatabaseService* DatabaseService::INSTANCE = nullptr; DatabaseService& DatabaseService::getInstance(boost::asio::io_context &io_context, std::size_t pool_size) { - static DatabaseService INSTANCE; - return INSTANCE; + std::lock_guard lock(singleton_mutex); + if (!INSTANCE) { + INSTANCE = new DatabaseService(io_context, pool_size); + } + return *INSTANCE; } -DatabaseService::DatabaseService(boost::asio::io_context& io_context, std::size_t pool_size) : io_context_(io_context), pool_size_(pool_size) { +DatabaseService::DatabaseService(boost::asio::io_context& io_context, std::size_t pool_size) : io_context_(io_context), pool_size_(pool_size), current_index_(0) { + if (pool_size_ <= 0) { + throw std::invalid_argument("Connection pool size must be greater than zero."); + } + for (std::size_t i = 0; i < pool_size_; ++i) { + auto conn = std::make_shared(io_context_); + connection_pool_.emplace_back(conn); + } } DatabaseService::~DatabaseService() { + connection_pool_.clear(); +} +std::shared_ptr DatabaseService::getConnection() { + std::lock_guard lock(singleton_mutex); + if (connection_pool_.empty()) { + throw std::runtime_error("Connection pool is empty."); + } + + auto conn = connection_pool_[current_index_]; + current_index_ = (current_index_ + 1) % pool_size_; + return conn; } \ No newline at end of file From f61be671d6fbc653a05233f196a303978a22bfb4 Mon Sep 17 00:00:00 2001 From: Thomas Schleicher Date: Mon, 16 Dec 2024 12:46:52 +0100 Subject: [PATCH 02/10] frontend draft --- frontend/index.html | 44 ++++++++++++++++++++++++++++++++++++++++++++ frontend/index.js | 34 ++++++++++++++++++++++++++++++++++ 2 files changed, 78 insertions(+) create mode 100644 frontend/index.html create mode 100644 frontend/index.js diff --git a/frontend/index.html b/frontend/index.html new file mode 100644 index 0000000..d4e3922 --- /dev/null +++ b/frontend/index.html @@ -0,0 +1,44 @@ + + + + + + Short-Link + + + + + + +
+

Short-Link

+
+ + +
+
+
+ + + \ No newline at end of file diff --git a/frontend/index.js b/frontend/index.js new file mode 100644 index 0000000..d3a85fd --- /dev/null +++ b/frontend/index.js @@ -0,0 +1,34 @@ +document.getElementById("urlInput").addEventListener("input", function() { + const urlInput = document.getElementById("urlInput"); + const shortenButton = document.getElementById("shortenButton"); + + const urlPattern = /^(https?:\/\/)?([\w\d-]+\.)+[a-z]{2,6}(\/[\w\d-]*)*\/?$/i; + const isValid = urlPattern.test(urlInput.value); + + if (isValid) { + urlInput.style.borderColor = "green"; + shortenButton.disabled = false; + } else { + urlInput.style.borderColor = "red"; + shortenButton.disabled = true; + } +}); + +document.getElementById("shortenButton").addEventListener("click", async function() { + const urlInput = document.getElementById("urlInput").value; + const outputContainer = document.getElementById("output-container"); + + if (urlInput) { + // const shortenedUrl = urlInput + "-short"; // only for testing + + const shortenedUrl = await fetch("0.0.0.0:8080/", { + method: "POST", + headers: { + "Content-Type": "text/plain", + }, + body: urlInput, + }); + + outputContainer.innerHTML = `

Shortened URL: ${shortenedUrl}

`; + } +}); From 63fd2b7ccab0f78d0e8a996062d4655c61a81498 Mon Sep 17 00:00:00 2001 From: = Date: Mon, 16 Dec 2024 16:41:01 +0100 Subject: [PATCH 03/10] basic redirection w.o. db backend --- src/http_connection.cpp | 5 +++ src/request_handler.cpp | 79 +++++++++++++++++++++++++++++++++-------- 2 files changed, 70 insertions(+), 14 deletions(-) diff --git a/src/http_connection.cpp b/src/http_connection.cpp index ed30d2f..7741622 100644 --- a/src/http_connection.cpp +++ b/src/http_connection.cpp @@ -38,6 +38,7 @@ void HttpConnection::write() { auto self = shared_from_this(); http::async_write(socket_, response_, [self](boost::beast::error_code error_code, size_t bytes_transferred) { + cout << "Sending response:\n" << self->response_ << endl; if (!error_code) { auto error_code_socket = self->socket_.shutdown(ip::tcp::socket::shutdown_send, error_code); if (error_code_socket) { @@ -45,6 +46,10 @@ void HttpConnection::write() { } } else { cerr << "Error writing response: " << error_code.message() << endl; + auto error_code_socket = self->socket_.shutdown(ip::tcp::socket::shutdown_both, error_code); + if (error_code_socket) { + cerr << "Error shuting down socket: " << error_code_socket.message() << endl; + } } }); } \ No newline at end of file diff --git a/src/request_handler.cpp b/src/request_handler.cpp index 3bedadf..f137959 100644 --- a/src/request_handler.cpp +++ b/src/request_handler.cpp @@ -4,28 +4,79 @@ #include #include #include +#include + +http::response BadRequest(const std::string& why) { + http::response response; + response.result(http::status::bad_request); + response.set(http::field::server, "Beast"); + response.set(http::field::content_type, "text/html"); + response.body() = why; + response.prepare_payload(); + return response; +} http::response RequestHandler::handle(const http::request& request) { string_view target = request.target(); - + http::verb method = request.method(); if (target == "/") { - //case 1: "/" -> serve angular frontend or static frontend what ever - http::response response; - response.result(http::status::ok); - response.version(request.version()); - response.set(http::field::server, "Beast"); - response.set(http::field::content_type, "text/html"); - response.keep_alive(); + if(method == http::verb::get) { + //case 1: "/" -> serve angular frontend or static frontend what ever + http::response response; + response.result(http::status::ok); + response.version(request.version()); + response.set(http::field::server, "Beast"); + response.set(http::field::content_type, "text/html"); - //todo: load angular application / plain html & js - response.body() = "

TEST

"; + //todo: load angular application / plain html & js + response.body() = "

TEST

"; - response.prepare_payload(); - return response; + response.prepare_payload(); + return response; + } + else if (method == http::verb::post) { + if(request.find(http::field::content_type) == request.end()) { + return BadRequest("Content-Type header is required for POST requests"); + } + auto content_type = request[http::field::content_type]; + if(content_type != "text/plain") { + return BadRequest("Content-Type must be text/plain"); + } + std::string url = request.body(); + std::regex url_regex("^(https?://)?(?:www\\.)?[-a-zA-Z0-9@%._\\+~#=]{1,256}\\.[a-zA-Z0-9()]{1,6}\\b(?:[-a-zA-Z0-9()@:%_\\+.~#?&/=]*)$"); + std::smatch url_match; + if(!std::regex_match(url, url_match, url_regex)) { + return BadRequest("Invalid URL"); + } + if(!url_match[1].matched){ + url = "https://" + url; + } + + //todo: save url to database and return short url + + return BadRequest("Request is actually not bad. processing " + url); + } + }else { + if(method == http::verb::get){ + http::response response; + std::string short_url = target.substr(1); + + std::string expanded_url = "https://google.com"; //todo: get expanded url from database + + response.result(http::status::moved_permanently); + response.set(http::field::location, expanded_url); + response.version(request.version()); + response.set(http::field::server, "Beast"); + response.body() = "Redirecting to " + expanded_url; + response.prepare_payload(); + return response; + } else { + return BadRequest("Method not allowed"); + } } - + //case 2: "/url" -> redirect to expanded url //case 3: neither -> redirect to 404 - return http::response{http::status::bad_request, request.version()}; + return BadRequest("No rule matched."); } \ No newline at end of file From 897310fe28b27e89d2742aeeb35f0c93d1eb8cd0 Mon Sep 17 00:00:00 2001 From: = Date: Mon, 16 Dec 2024 20:17:09 +0100 Subject: [PATCH 04/10] integrate files with serving (even though ugly) --- CMakeLists.txt | 4 ++ frontend/index.html | 2 + frontend/index.js | 14 +++++-- includes/http_connection.hpp | 3 +- includes/request_handler.hpp | 8 +++- src/http_connection.cpp | 7 +++- src/request_handler.cpp | 81 +++++++++++++++++++++++------------- 7 files changed, 83 insertions(+), 36 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 04337c1..88b430b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -15,6 +15,10 @@ add_executable(Application src/main.cpp src/request_handler.cpp ) +add_custom_target(copy_frontend ALL + COMMAND ${CMAKE_COMMAND} -E copy_directory ${PROJECT_SOURCE_DIR}/frontend ${CMAKE_BINARY_DIR}/frontend +) + find_package(Boost REQUIRED filesystem system) if(Boost_FOUND) diff --git a/frontend/index.html b/frontend/index.html index d4e3922..673756a 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -24,6 +24,7 @@ flex-direction: column; align-items: center; justify-content: center; + background-color: transparent; } @@ -38,6 +39,7 @@
+
diff --git a/frontend/index.js b/frontend/index.js index d3a85fd..3c312be 100644 --- a/frontend/index.js +++ b/frontend/index.js @@ -17,18 +17,26 @@ document.getElementById("urlInput").addEventListener("input", function() { document.getElementById("shortenButton").addEventListener("click", async function() { const urlInput = document.getElementById("urlInput").value; const outputContainer = document.getElementById("output-container"); + const errorContainer = document.getElementById("error-container"); if (urlInput) { // const shortenedUrl = urlInput + "-short"; // only for testing - const shortenedUrl = await fetch("0.0.0.0:8080/", { + const response = await fetch("/", { method: "POST", headers: { "Content-Type": "text/plain", }, body: urlInput, }); - - outputContainer.innerHTML = `

Shortened URL: ${shortenedUrl}

`; + if (response.ok){ + const shortenedUrl = await response.text(); + outputContainer.innerHTML = `

Shortened URL: ${shortenedUrl}

`; + errorContainer.innerHTML = ""; + }else{ + const error = await response.text(); + errorContainer.innerHTML = `

Something went wrong: ${error}

`; + outputContainer.innerHTML = ""; + } } }); diff --git a/includes/http_connection.hpp b/includes/http_connection.hpp index ab53345..493c11c 100644 --- a/includes/http_connection.hpp +++ b/includes/http_connection.hpp @@ -8,6 +8,7 @@ #include #include #include +#include using namespace std; using namespace boost::asio; @@ -25,7 +26,7 @@ private: ip::tcp::socket socket_; boost::beast::flat_buffer buffer_; http::request request_; - http::response response_; + variant, http::response> response_; RequestHandler request_handler_; void read(); diff --git a/includes/request_handler.hpp b/includes/request_handler.hpp index 2d14176..5382f6f 100644 --- a/includes/request_handler.hpp +++ b/includes/request_handler.hpp @@ -4,14 +4,20 @@ #include #include #include +#include using namespace boost::beast; class RequestHandler { public: - http::response handle(const http::request& request); +std::variant< + http::response, + http::response + > handle(const http::request& request); private: + http::response BadRequest(const std::string& why); + http::response handle_file_request(const std::string& filename, boost::system::error_code& ec); }; #endif \ No newline at end of file diff --git a/src/http_connection.cpp b/src/http_connection.cpp index 7741622..6f1a2fe 100644 --- a/src/http_connection.cpp +++ b/src/http_connection.cpp @@ -36,9 +36,10 @@ void HttpConnection::read() { void HttpConnection::write() { auto self = shared_from_this(); - http::async_write(socket_, response_, + std::visit( + [this, self](auto& response) { + http::async_write(socket_, response, [self](boost::beast::error_code error_code, size_t bytes_transferred) { - cout << "Sending response:\n" << self->response_ << endl; if (!error_code) { auto error_code_socket = self->socket_.shutdown(ip::tcp::socket::shutdown_send, error_code); if (error_code_socket) { @@ -52,4 +53,6 @@ void HttpConnection::write() { } } }); + + }, response_); } \ No newline at end of file diff --git a/src/request_handler.cpp b/src/request_handler.cpp index f137959..f4b47a4 100644 --- a/src/request_handler.cpp +++ b/src/request_handler.cpp @@ -5,37 +5,32 @@ #include #include #include +#include "request_handler.hpp" -http::response BadRequest(const std::string& why) { +http::response RequestHandler::BadRequest(const std::string& why) { http::response response; response.result(http::status::bad_request); response.set(http::field::server, "Beast"); response.set(http::field::content_type, "text/html"); response.body() = why; + response.keep_alive(false); response.prepare_payload(); return response; } -http::response RequestHandler::handle(const http::request& request) { - string_view target = request.target(); +std::variant< + http::response, + http::response + > + RequestHandler::handle(const http::request& request) { + std::string target = request.target(); http::verb method = request.method(); - if (target == "/") { - if(method == http::verb::get) { - //case 1: "/" -> serve angular frontend or static frontend what ever - http::response response; - response.result(http::status::ok); - response.version(request.version()); - response.set(http::field::server, "Beast"); - response.set(http::field::content_type, "text/html"); - //todo: load angular application / plain html & js - response.body() = "

TEST

"; - - response.prepare_payload(); - return response; + if(method == http::verb::post){ + if(target != "/") { + return BadRequest("Cannot post to anything other than /"); } - else if (method == http::verb::post) { - if(request.find(http::field::content_type) == request.end()) { + if(request.find(http::field::content_type) == request.end()) { return BadRequest("Content-Type header is required for POST requests"); } auto content_type = request[http::field::content_type]; @@ -53,11 +48,27 @@ http::response RequestHandler::handle(const http::request response; + response.result(http::status::created); + response.set(http::field::server, "Beast"); + response.set(http::field::content_type, "text/plain"); + response.body() = "127.0.0.1:8080/asdf"; + response.keep_alive(false); + response.prepare_payload(); + return response; + } else if (method == http::verb::get) { + if(target == "/"){ + target = "/index.html"; } - }else { - if(method == http::verb::get){ + if(target == "/index.html" || target == "/index.js") { + error_code ec; + http::response response = handle_file_request("frontend" + target, ec); + if(ec) { + return BadRequest("Error reading file"); + }else { + return response; + } + } else { http::response response; std::string short_url = target.substr(1); @@ -68,15 +79,27 @@ http::response RequestHandler::handle(const http::request redirect to expanded url - //case 3: neither -> redirect to 404 + } + return BadRequest("No rule matched."); +} +http::response RequestHandler::handle_file_request(const std::string& path, error_code &ec) { + http::file_body::value_type file; + file.open(path.c_str(), file_mode::read, ec); + http::response response; + if(!ec){ + response.result(http::status::ok); + response.set(http::field::server, "Beast"); + response.set(http::field::content_type, "text/html"); + response.body() = std::move(file); + response.keep_alive(false); + response.prepare_payload(); + }; + return response; + } \ No newline at end of file From cbfc0b563c66e6ba0bbf7e5cea722027b9ff49b0 Mon Sep 17 00:00:00 2001 From: rawalcher Date: Sun, 29 Dec 2024 17:05:55 +0100 Subject: [PATCH 05/10] sqlite database with methods added --- CMakeLists.txt | 36 ++++++++++++++---- includes/database_service.hpp | 31 +++++++-------- src/database_connection.cpp | 71 ++++++++++++++++++++++++++++++----- 3 files changed, 105 insertions(+), 33 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 88b430b..c4a8093 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,5 +1,4 @@ cmake_minimum_required(VERSION 3.16) -cmake_policy(SET CMP0167 OLD) project(short-link VERSION 1.0 LANGUAGES CXX) @@ -9,17 +8,20 @@ set(CMAKE_CXX_STANDARD_REQUIRED True) set(BUILD_SHARED_LIBS OFF) include_directories(${PROJECT_SOURCE_DIR}/includes) -add_executable(Application src/main.cpp - src/http_connection.cpp - src/http_server.cpp - src/request_handler.cpp + +add_executable(Application + src/main.cpp + src/http_connection.cpp + src/http_server.cpp + src/request_handler.cpp + src/database_connection.cpp ) add_custom_target(copy_frontend ALL - COMMAND ${CMAKE_COMMAND} -E copy_directory ${PROJECT_SOURCE_DIR}/frontend ${CMAKE_BINARY_DIR}/frontend + COMMAND ${CMAKE_COMMAND} -E copy_directory ${PROJECT_SOURCE_DIR}/frontend ${CMAKE_BINARY_DIR}/frontend ) -find_package(Boost REQUIRED filesystem system) +find_package(Boost REQUIRED COMPONENTS filesystem system) if(Boost_FOUND) include_directories(${Boost_INCLUDE_DIRS}) @@ -27,4 +29,22 @@ if(Boost_FOUND) target_link_libraries(Application PRIVATE Boost::filesystem Boost::system) else() message(FATAL_ERROR "Boost not found!") -endif() \ No newline at end of file +endif() + +find_package(SOCI REQUIRED sqlite3) + +if(SOCI_FOUND) + include_directories(${SOCI_INCLUDE_DIRS}) + target_link_libraries(Application PRIVATE soci_core soci_sqlite3) +else() + message(FATAL_ERROR "SOCI not found!") +endif() + +find_package(SQLite3 REQUIRED) + +if(SQLite3_FOUND) + include_directories(${SQLite3_INCLUDE_DIRS}) + target_link_libraries(Application PRIVATE SQLite::SQLite3) +else() + message(FATAL_ERROR "SQLite3 not found!") +endif() diff --git a/includes/database_service.hpp b/includes/database_service.hpp index a374260..77845a9 100644 --- a/includes/database_service.hpp +++ b/includes/database_service.hpp @@ -1,35 +1,36 @@ #ifndef DATABASE_SERVICE_HPP #define DATABASE_SERVICE_HPP -#include -#include -#include -#include +#include #include -#include #include +#include +#include +#include +#include class DatabaseService { public: - static DatabaseService& getInstance(boost::asio::io_context& io_context, std::size_t pool_size); - void asyncSetKey(const std::string& key, const std::string& value, std::function callback); - void asyncGetKey(const std::string& key, std::function callback); //we could use a varient here for the result + static DatabaseService& getInstance(const std::string& db_path, std::size_t pool_size); + void shortenURL(const std::string& longURL, std::function callback); + void getLongURL(const std::string& shortCode, std::function callback); -private: - DatabaseService(boost::asio::io_context& io_context, std::size_t pool_size = 4); +private: + DatabaseService(const std::string& db_path, std::size_t pool_size = 4); ~DatabaseService(); DatabaseService(DatabaseService const&); void operator=(DatabaseService const&); - std::shared_ptr getConnection(); + std::shared_ptr getConnection(); + std::string generateShortUUID(); - boost::asio::io_context& io_context_; + std::string db_path_; std::size_t pool_size_; - std::vector> connection_pool_; - std::size_t current_index_ = 0; //index used for round robin selection + std::vector> connection_pool_; + std::size_t current_index_ = 0; // Index used for round-robin selection static DatabaseService* INSTANCE; static std::mutex singleton_mutex; }; -#endif \ No newline at end of file +#endif diff --git a/src/database_connection.cpp b/src/database_connection.cpp index 6ff47b7..0cdd717 100644 --- a/src/database_connection.cpp +++ b/src/database_connection.cpp @@ -1,37 +1,46 @@ #include "../includes/database_service.hpp" -#include -#include -#include +#include +#include +#include +#include #include #include std::mutex DatabaseService::singleton_mutex; DatabaseService* DatabaseService::INSTANCE = nullptr; -DatabaseService& DatabaseService::getInstance(boost::asio::io_context &io_context, std::size_t pool_size) { +DatabaseService& DatabaseService::getInstance(const std::string& db_path, std::size_t pool_size) { std::lock_guard lock(singleton_mutex); if (!INSTANCE) { - INSTANCE = new DatabaseService(io_context, pool_size); + INSTANCE = new DatabaseService(db_path, pool_size); } return *INSTANCE; } -DatabaseService::DatabaseService(boost::asio::io_context& io_context, std::size_t pool_size) : io_context_(io_context), pool_size_(pool_size), current_index_(0) { +DatabaseService::DatabaseService(const std::string& db_path, std::size_t pool_size) + : db_path_(db_path), pool_size_(pool_size), current_index_(0) { if (pool_size_ <= 0) { throw std::invalid_argument("Connection pool size must be greater than zero."); } for (std::size_t i = 0; i < pool_size_; ++i) { - auto conn = std::make_shared(io_context_); - connection_pool_.emplace_back(conn); + auto session = std::make_shared(soci::sqlite3, db_path_); + connection_pool_.emplace_back(session); } + + // Ensure the table exists + auto conn = getConnection(); + *conn << "CREATE TABLE IF NOT EXISTS urls (" + "short_code CHAR(8) PRIMARY KEY, " + "original_url TEXT NOT NULL UNIQUE, " + "created_at INTEGER DEFAULT CURRENT_TIMESTAMP);"; } DatabaseService::~DatabaseService() { connection_pool_.clear(); } -std::shared_ptr DatabaseService::getConnection() { +std::shared_ptr DatabaseService::getConnection() { std::lock_guard lock(singleton_mutex); if (connection_pool_.empty()) { throw std::runtime_error("Connection pool is empty."); @@ -39,5 +48,47 @@ std::shared_ptr DatabaseService::getConnection() { auto conn = connection_pool_[current_index_]; current_index_ = (current_index_ + 1) % pool_size_; - return conn; + return conn; +} + +std::string DatabaseService::generateShortUUID() { + boost::uuids::uuid uuid = boost::uuids::random_generator()(); + std::string uuidStr = to_string(uuid); + return uuidStr.substr(0, 8); +} + +void DatabaseService::shortenURL(const std::string& longURL, std::function callback) { + try { + auto conn = getConnection(); + std::string shortCode; + *conn << "SELECT short_code FROM urls WHERE original_url = :long_url", soci::use(longURL), soci::into(shortCode); + + if (!shortCode.empty()) { + callback({}, shortCode); + return; + } + + shortCode = generateShortUUID(); + *conn << "INSERT INTO urls (short_code, original_url) VALUES (:short_code, :long_url)", + soci::use(shortCode), soci::use(longURL); + callback({}, shortCode); + } catch (const std::exception& e) { + callback(std::make_error_code(std::errc::io_error), ""); + } +} + +void DatabaseService::getLongURL(const std::string& shortCode, std::function callback) { + try { + auto conn = getConnection(); + std::string longURL; + *conn << "SELECT original_url FROM urls WHERE short_code = :short_code", soci::use(shortCode), soci::into(longURL); + + if (!longURL.empty()) { + callback({}, longURL); + } else { + callback(std::make_error_code(std::errc::no_such_file_or_directory), ""); + } + } catch (const std::exception& e) { + callback(std::make_error_code(std::errc::io_error), ""); + } } \ No newline at end of file From 0d89ce5e2dc45caca237df08f926c3ad016a8e80 Mon Sep 17 00:00:00 2001 From: rawalcher Date: Sun, 29 Dec 2024 18:53:46 +0100 Subject: [PATCH 06/10] CMakeLists.txt - added copy_frontend as dependency (else it wont automatically execute request_handler.cpp - implemented db connection - fixed routing of short code All Files - Improved logging --- CMakeLists.txt | 3 + includes/database_service.hpp | 5 +- includes/log_utils.hpp | 62 ++++++++++++ src/database_connection.cpp | 35 +++++-- src/http_connection.cpp | 18 +++- src/http_server.cpp | 16 +-- src/main.cpp | 21 ++-- src/request_handler.cpp | 180 +++++++++++++++++++++++----------- 8 files changed, 251 insertions(+), 89 deletions(-) create mode 100644 includes/log_utils.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt index c4a8093..91d3764 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -21,6 +21,9 @@ add_custom_target(copy_frontend ALL COMMAND ${CMAKE_COMMAND} -E copy_directory ${PROJECT_SOURCE_DIR}/frontend ${CMAKE_BINARY_DIR}/frontend ) +add_dependencies(Application copy_frontend) + + find_package(Boost REQUIRED COMPONENTS filesystem system) if(Boost_FOUND) diff --git a/includes/database_service.hpp b/includes/database_service.hpp index 77845a9..bb5cb52 100644 --- a/includes/database_service.hpp +++ b/includes/database_service.hpp @@ -7,7 +7,6 @@ #include #include #include -#include class DatabaseService { public: @@ -27,10 +26,10 @@ private: std::string db_path_; std::size_t pool_size_; std::vector> connection_pool_; - std::size_t current_index_ = 0; // Index used for round-robin selection + std::size_t current_index_ = 0; static DatabaseService* INSTANCE; static std::mutex singleton_mutex; }; -#endif +#endif \ No newline at end of file diff --git a/includes/log_utils.hpp b/includes/log_utils.hpp new file mode 100644 index 0000000..8d44567 --- /dev/null +++ b/includes/log_utils.hpp @@ -0,0 +1,62 @@ +#ifndef LOG_UTILS_HPP +#define LOG_UTILS_HPP + +#include +#include +#include +#include +#include +#include + +// Define log levels +enum class LogLevel { + INFO, + ERROR, + BADREQUEST +}; + +// Function to get a color based on file name +inline const char* getColorForFile(const std::string& file_name) { + // Define color codes + const char* colors[] = { + "\033[34m", // Blue + "\033[32m", // Green + "\033[36m", // Cyan + "\033[35m", // Magenta + "\033[33m" // Yellow + }; + + // Hash the file name to determine the color + std::hash hasher; + size_t hash = hasher(file_name); + return colors[hash % (sizeof(colors) / sizeof(colors[0]))]; +} + +// Logging function +inline void log(const LogLevel level, const std::string& message, const std::string& file_path) { + const auto now = std::chrono::system_clock::now(); + const auto time = std::chrono::system_clock::to_time_t(now); + const std::tm tm = *std::localtime(&time); + + const auto color_reset = "\033[0m"; + const auto color_error = "\033[31m"; + + // Extract file name from full path + std::string file_name = std::filesystem::path(file_path).filename().string(); + + // Determine color based on log level + const char* color = level == LogLevel::ERROR || level == LogLevel::BADREQUEST ? color_error : getColorForFile(file_name); + + // Print log + std::cout << color + << "[" << (level == LogLevel::INFO ? "INFO" : "ERROR") << "] " + << std::put_time(&tm, "%H:%M:%S") // Time only + << " [" << file_name << "] " + << message + << color_reset << std::endl; +} + +// Helper macro to automatically pass the file name +#define LOG(level, message) log(level, message, __FILE__) + +#endif // LOG_UTILS_HPP diff --git a/src/database_connection.cpp b/src/database_connection.cpp index 0cdd717..eb9e60b 100644 --- a/src/database_connection.cpp +++ b/src/database_connection.cpp @@ -1,15 +1,16 @@ #include "../includes/database_service.hpp" +#include "../includes/log_utils.hpp" #include #include #include -#include -#include #include +#include std::mutex DatabaseService::singleton_mutex; DatabaseService* DatabaseService::INSTANCE = nullptr; DatabaseService& DatabaseService::getInstance(const std::string& db_path, std::size_t pool_size) { + std::lock_guard lock(singleton_mutex); if (!INSTANCE) { INSTANCE = new DatabaseService(db_path, pool_size); @@ -19,7 +20,10 @@ DatabaseService& DatabaseService::getInstance(const std::string& db_path, std::s DatabaseService::DatabaseService(const std::string& db_path, std::size_t pool_size) : db_path_(db_path), pool_size_(pool_size), current_index_(0) { + LOG(LogLevel::INFO, "Initializing DatabaseService"); + if (pool_size_ <= 0) { + LOG(LogLevel::ERROR, "Invalid pool size"); throw std::invalid_argument("Connection pool size must be greater than zero."); } @@ -27,13 +31,19 @@ DatabaseService::DatabaseService(const std::string& db_path, std::size_t pool_si auto session = std::make_shared(soci::sqlite3, db_path_); connection_pool_.emplace_back(session); } + LOG(LogLevel::INFO, "Connection Pool filled"); + try { + auto conn = getConnection(); + *conn << "CREATE TABLE IF NOT EXISTS urls (" + "short_code CHAR(8) PRIMARY KEY, " + "original_url TEXT NOT NULL UNIQUE, " + "created_at INTEGER DEFAULT CURRENT_TIMESTAMP);"; - // Ensure the table exists - auto conn = getConnection(); - *conn << "CREATE TABLE IF NOT EXISTS urls (" - "short_code CHAR(8) PRIMARY KEY, " - "original_url TEXT NOT NULL UNIQUE, " - "created_at INTEGER DEFAULT CURRENT_TIMESTAMP);"; + LOG(LogLevel::INFO, "Database schema initialized successfully"); + } catch (const std::exception& e) { + LOG(LogLevel::ERROR, "Exception during schema creation: " + std::string(e.what())); + throw; + } } DatabaseService::~DatabaseService() { @@ -41,13 +51,13 @@ DatabaseService::~DatabaseService() { } std::shared_ptr DatabaseService::getConnection() { - std::lock_guard lock(singleton_mutex); if (connection_pool_.empty()) { throw std::runtime_error("Connection pool is empty."); } + LOG(LogLevel::INFO, "Returning connection at index: " + std::to_string(current_index_)); auto conn = connection_pool_[current_index_]; - current_index_ = (current_index_ + 1) % pool_size_; + current_index_ = (current_index_ + 1) % connection_pool_.size(); return conn; } @@ -58,6 +68,7 @@ std::string DatabaseService::generateShortUUID() { } void DatabaseService::shortenURL(const std::string& longURL, std::function callback) { + LOG(LogLevel::INFO, "Shortening URL: " + longURL); try { auto conn = getConnection(); std::string shortCode; @@ -73,11 +84,13 @@ void DatabaseService::shortenURL(const std::string& longURL, std::function callback) { + LOG(LogLevel::INFO, "Getting long-URL from short-code: " + shortCode); try { auto conn = getConnection(); std::string longURL; @@ -86,9 +99,11 @@ void DatabaseService::getLongURL(const std::string& shortCode, std::function +#include "../includes/log_utils.hpp" #include #include #include -#include HttpConnection::HttpConnection(io_context& io_context) : socket_(io_context) {}; @@ -22,18 +21,29 @@ void HttpConnection::process_connection() { void HttpConnection::read() { auto self = shared_from_this(); + http::async_read(socket_, buffer_, request_, [self](boost::beast::error_code error_code, size_t bytes_transferred) { if (!error_code) { - cout << "Request received:\n" << self->request_ << endl; + LOG(LogLevel::INFO, "Request details:"); + LOG(LogLevel::INFO, "\tMethod: " + std::string(self->request_.method_string())); + LOG(LogLevel::INFO, "\tTarget: " + std::string(self->request_.target())); + LOG(LogLevel::INFO, "\tBody: " + std::string(self->request_.body())); + + // Log before forwarding to RequestHandler + LOG(LogLevel::INFO, "Forwarding request to RequestHandler..."); self->response_ = self->request_handler_.handle(self->request_); + self->write(); } else { - cerr << "Error reading: " << error_code.message() << endl; + LOG(LogLevel::ERROR, "Error reading request: " + error_code.message()); } }); } + + + void HttpConnection::write() { auto self = shared_from_this(); std::visit( diff --git a/src/http_server.cpp b/src/http_server.cpp index 60b18c0..8f5c1d5 100644 --- a/src/http_server.cpp +++ b/src/http_server.cpp @@ -1,8 +1,8 @@ #include "../includes/http_server.hpp" +#include "../includes/log_utils.hpp" +#include #include "http_connection.hpp" -#include #include -#include HttpServer::HttpServer(io_context& io_context, const ip::tcp::endpoint& endpoint) : io_context_(io_context), acceptor_(io_context, endpoint) { @@ -11,14 +11,18 @@ HttpServer::HttpServer(io_context& io_context, const ip::tcp::endpoint& endpoint void HttpServer::accept_connection() { HttpConnection::pointer new_connection = HttpConnection::create(io_context_); - acceptor_.async_accept(new_connection->socket(), - bind(&HttpServer::handle_accept, this, new_connection, boost::asio::placeholders::error) - ); + + LOG(LogLevel::INFO, "Waiting for new connection"); + acceptor_.async_accept(new_connection->socket(), + bind(&HttpServer::handle_accept, this, new_connection, boost::asio::placeholders::error)); } void HttpServer::handle_accept(HttpConnection::pointer& new_connection, const boost::system::error_code& error_code) { if (!error_code) { + LOG(LogLevel::INFO, "Accepted new connection from: " + new_connection->socket().remote_endpoint().address().to_string()); new_connection->process_connection(); + } else { + LOG(LogLevel::ERROR, "Error accepting connection: " + error_code.message()); } accept_connection(); -} \ No newline at end of file +} diff --git a/src/main.cpp b/src/main.cpp index f7624fa..19c2fa5 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,24 +1,25 @@ - #include "../includes/http_server.hpp" +#include "../includes/log_utils.hpp" #include +#include using namespace boost::asio; -int main(int argc, char *argv[]) { +int main(int argc, char* argv[]) { const int port = 8080; - std::cout << "Starting server!" << std::endl; - try - { + LOG(LogLevel::INFO, "Starting Server on 127.0.0.1:" + std::to_string(port)); + + try { io_context io_context; ip::tcp::endpoint endpoint(ip::tcp::v4(), port); + HttpServer server(io_context, endpoint); + io_context.run(); - } - catch (std::exception& e) - { - std::cerr << e.what() << std::endl; + } catch (const std::exception& e) { + LOG(LogLevel::ERROR, "Exception occurred: " + std::string(e.what())); } return 0; -} \ No newline at end of file +} diff --git a/src/request_handler.cpp b/src/request_handler.cpp index f4b47a4..2116be2 100644 --- a/src/request_handler.cpp +++ b/src/request_handler.cpp @@ -1,13 +1,17 @@ #include "../includes/request_handler.hpp" +#include "../includes/database_service.hpp" +#include "../includes/log_utils.hpp" #include #include #include #include #include #include -#include "request_handler.hpp" +#include +#include http::response RequestHandler::BadRequest(const std::string& why) { + LOG(LogLevel::BADREQUEST, "BadRequest: " + why); http::response response; response.result(http::status::bad_request); response.set(http::field::server, "Beast"); @@ -19,87 +23,151 @@ http::response RequestHandler::BadRequest(const std::string& } std::variant< - http::response, - http::response - > - RequestHandler::handle(const http::request& request) { + http::response, + http::response +> RequestHandler::handle(const http::request& request) { std::string target = request.target(); http::verb method = request.method(); - if(method == http::verb::post){ - if(target != "/") { + auto& dbService = DatabaseService::getInstance("urls.db", 4); + + LOG(LogLevel::INFO, "Received request:"); + LOG(LogLevel::INFO, "\tMethod: " + std::string(request.method_string())); + LOG(LogLevel::INFO, "\tTarget: " + target); + LOG(LogLevel::INFO, "\tBody: " + request.body()); + + if (method == http::verb::post) { + if (target != "/") { return BadRequest("Cannot post to anything other than /"); } - if(request.find(http::field::content_type) == request.end()) { - return BadRequest("Content-Type header is required for POST requests"); - } - auto content_type = request[http::field::content_type]; - if(content_type != "text/plain") { - return BadRequest("Content-Type must be text/plain"); - } - std::string url = request.body(); - std::regex url_regex("^(https?://)?(?:www\\.)?[-a-zA-Z0-9@%._\\+~#=]{1,256}\\.[a-zA-Z0-9()]{1,6}\\b(?:[-a-zA-Z0-9()@:%_\\+.~#?&/=]*)$"); - std::smatch url_match; - if(!std::regex_match(url, url_match, url_regex)) { - return BadRequest("Invalid URL"); - } - if(!url_match[1].matched){ - url = "https://" + url; - } + if (request.find(http::field::content_type) == request.end()) { + return BadRequest("Content-Type header is required for POST requests"); + } + auto content_type = request[http::field::content_type]; + if (content_type != "text/plain") { + return BadRequest("Content-Type must be text/plain"); + } - //todo: save url to database and return short url + std::string url = request.body(); + std::regex url_regex("^(https?://)?(?:www\\.)?[-a-zA-Z0-9@%._\\+~#=]{1,256}\\.[a-zA-Z0-9()]{1,6}\\b(?:[-a-zA-Z0-9()@:%_\\+.~#?&/=]*)$"); + std::smatch url_match; + if (!std::regex_match(url, url_match, url_regex)) { + return BadRequest("Invalid URL"); + } + if (!url_match[1].matched) { + url = "https://" + url; + } + + LOG(LogLevel::INFO, "Valid URL: " + url); + + std::promise promise; + std::future future = promise.get_future(); + dbService.shortenURL(url, [&promise](std::error_code ec, const std::string& shortURL) { + if (!ec) { + promise.set_value(shortURL); + } else { + promise.set_exception(std::make_exception_ptr( + std::runtime_error(ec.message()) + )); + } + }); + + try { + std::string shortURL = future.get(); + LOG(LogLevel::INFO, "Shortened URL generated: " + shortURL); http::response response; response.result(http::status::created); response.set(http::field::server, "Beast"); response.set(http::field::content_type, "text/plain"); - response.body() = "127.0.0.1:8080/asdf"; - response.keep_alive(false); - response.prepare_payload(); - return response; - } else if (method == http::verb::get) { - if(target == "/"){ - target = "/index.html"; - } - if(target == "/index.html" || target == "/index.js") { - error_code ec; - http::response response = handle_file_request("frontend" + target, ec); - if(ec) { - return BadRequest("Error reading file"); - }else { - return response; - } - } else { - http::response response; - std::string short_url = target.substr(1); - - std::string expanded_url = "https://google.com"; //todo: get expanded url from database - - response.result(http::status::moved_permanently); - response.set(http::field::location, expanded_url); - response.version(request.version()); - response.set(http::field::server, "Beast"); - response.body() = "Redirecting to " + expanded_url; + response.body() = "127.0.0.1:8080/" + shortURL; response.keep_alive(false); response.prepare_payload(); return response; + } catch (const std::exception& e) { + return BadRequest("Error generating short URL: " + std::string(e.what())); } } - + if (method == http::verb::get) { + if (target == "/") { + LOG(LogLevel::INFO, "Serving the index.html file for target /"); + target = "/index.html"; + } + if (target == "/index.html" || target == "/index.js") { + error_code ec; + http::response response = handle_file_request("frontend" + target, ec); + if (ec) { + LOG(LogLevel::ERROR, "Failed to read file: frontend" + target); + return BadRequest("Error reading file"); + } else { + LOG(LogLevel::INFO, "Served file: frontend" + target); + return response; + } + } + + std::regex regex_pattern(".*/([^/]+)$"); + std::smatch match; + std::string shortCode; + + if (std::regex_match(target, match, regex_pattern)) { + shortCode = match[1]; + LOG(LogLevel::INFO, "Extracted short code using regex: " + shortCode); + } else { + LOG(LogLevel::ERROR, "Failed to extract short code from target: " + target); + return BadRequest("Invalid short URL format"); + } + + std::promise promise; + std::future future = promise.get_future(); + + dbService.getLongURL(shortCode, [&promise](std::error_code ec, const std::string& longURL) { + if (!ec) { + promise.set_value(longURL); + } else { + promise.set_exception(std::make_exception_ptr( + std::runtime_error(ec.message()) + )); + } + }); + + try { + std::string expandedURL = future.get(); + LOG(LogLevel::INFO, "Expanded URL: " + expandedURL); + http::response response; + response.result(http::status::moved_permanently); + response.set(http::field::location, expandedURL); + response.version(request.version()); + response.set(http::field::server, "Beast"); + response.body() = "Redirecting to " + expandedURL; + response.keep_alive(false); + response.prepare_payload(); + return response; + } catch (const std::exception& e) { + LOG(LogLevel::ERROR, "Failed to expand short URL: " + shortCode + ". Error: " + std::string(e.what())); + return BadRequest("Short URL not found or error: " + std::string(e.what())); + } + } + return BadRequest("No rule matched."); } -http::response RequestHandler::handle_file_request(const std::string& path, error_code &ec) { + +http::response RequestHandler::handle_file_request(const std::string& path, error_code& ec) { http::file_body::value_type file; file.open(path.c_str(), file_mode::read, ec); + if (!ec) { + LOG(LogLevel::INFO, "Successfully opened file: " + path); + } else { + LOG(LogLevel::ERROR, "Failed to open file: " + path + ". Error: " + ec.message()); + } + http::response response; - if(!ec){ + if (!ec) { response.result(http::status::ok); response.set(http::field::server, "Beast"); response.set(http::field::content_type, "text/html"); response.body() = std::move(file); response.keep_alive(false); response.prepare_payload(); - }; + } return response; - } \ No newline at end of file From 5a8d51fc924081bf167a52308ba46f8cafb13220 Mon Sep 17 00:00:00 2001 From: rawalcher Date: Sun, 29 Dec 2024 19:06:43 +0100 Subject: [PATCH 07/10] Modified dockerfile --- .dockerignore | 13 +++++++++++++ dockerfile | 20 ++++++++++++++++---- 2 files changed, 29 insertions(+), 4 deletions(-) create mode 100644 .dockerignore diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..6a84d21 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,13 @@ +# Ignore build files +build/ +cmake-build-* + +# Ignore editor-specific files +*.swp +*.log +.idea/ +.vscode/ + +# Ignore version control directories +.git/ +.gitignore \ No newline at end of file diff --git a/dockerfile b/dockerfile index ffc27dd..327ebbc 100644 --- a/dockerfile +++ b/dockerfile @@ -3,12 +3,24 @@ FROM ubuntu:20.04 ENV APPLICATION_BINARY="Application" ENV OPEN_PORT=8080 -COPY ${APPLICATION_BINARY} . - RUN apt-get update && \ ln -fs /usr/share/zoneinfo/America/New_York /etc/localtime && \ - DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends libboost-all-dev && \ + DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \ + build-essential \ + cmake \ + libboost-all-dev \ + git \ + libsqlite3-dev && \ rm -rf /var/lib/apt/lists/* +WORKDIR /app + +COPY . . + +RUN mkdir build && cd build && \ + cmake .. && \ + make + EXPOSE ${OPEN_PORT} -CMD [ "/${APPLICATION_BINARY}" ] \ No newline at end of file + +CMD ["./build/Application"] From 39e1218cf86c1294e03b235485592e71610e7fde Mon Sep 17 00:00:00 2001 From: rawalcher Date: Mon, 27 Jan 2025 11:43:54 +0100 Subject: [PATCH 08/10] . --- src/database_connection.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/database_connection.cpp b/src/database_connection.cpp index eb9e60b..98f2db5 100644 --- a/src/database_connection.cpp +++ b/src/database_connection.cpp @@ -5,6 +5,8 @@ #include #include #include +#include +#include std::mutex DatabaseService::singleton_mutex; DatabaseService* DatabaseService::INSTANCE = nullptr; From 611f9ecc5fd6e6149c499e22d47020a51058af56 Mon Sep 17 00:00:00 2001 From: rawalcher Date: Mon, 27 Jan 2025 11:44:01 +0100 Subject: [PATCH 09/10] . --- src/main.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main.cpp b/src/main.cpp index 19c2fa5..8b57e85 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -3,6 +3,7 @@ #include #include + using namespace boost::asio; int main(int argc, char* argv[]) { From b717bd96b10082654c25e3cc250193977184cb28 Mon Sep 17 00:00:00 2001 From: rawalcher Date: Mon, 27 Jan 2025 12:05:35 +0100 Subject: [PATCH 10/10] modified regex - it matched "www.google" which shouldnt be possible --- frontend/index.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/frontend/index.js b/frontend/index.js index 3c312be..da7e0ed 100644 --- a/frontend/index.js +++ b/frontend/index.js @@ -2,7 +2,8 @@ document.getElementById("urlInput").addEventListener("input", function() { const urlInput = document.getElementById("urlInput"); const shortenButton = document.getElementById("shortenButton"); - const urlPattern = /^(https?:\/\/)?([\w\d-]+\.)+[a-z]{2,6}(\/[\w\d-]*)*\/?$/i; + const urlPattern = /(https?:\/\/(?:www\.|(?!www))[a-zA-Z0-9][a-zA-Z0-9-]+[a-zA-Z0-9]\.[^\s]{2,}|www\.[a-zA-Z0-9][a-zA-Z0-9-]+[a-zA-Z0-9]\.[^\s]{2,}|https?:\/\/(?:www\.|(?!www))[a-zA-Z0-9]+\.[^\s]{2,}|www\.[a-zA-Z0-9]+\.[^\s]{2,})/i + const isValid = urlPattern.test(urlInput.value); if (isValid) {