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