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/CMakeLists.txt b/CMakeLists.txt index 04337c1..91d3764 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,13 +8,23 @@ 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 ) -find_package(Boost REQUIRED filesystem system) +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) include_directories(${Boost_INCLUDE_DIRS}) @@ -23,4 +32,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/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"] diff --git a/frontend/index.html b/frontend/index.html new file mode 100644 index 0000000..673756a --- /dev/null +++ b/frontend/index.html @@ -0,0 +1,46 @@ + + + + + + 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..da7e0ed --- /dev/null +++ b/frontend/index.js @@ -0,0 +1,43 @@ +document.getElementById("urlInput").addEventListener("input", function() { + const urlInput = document.getElementById("urlInput"); + const shortenButton = document.getElementById("shortenButton"); + + 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) { + 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"); + const errorContainer = document.getElementById("error-container"); + + if (urlInput) { + // const shortenedUrl = urlInput + "-short"; // only for testing + + const response = await fetch("/", { + method: "POST", + headers: { + "Content-Type": "text/plain", + }, + body: urlInput, + }); + 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/database_service.hpp b/includes/database_service.hpp index 4d997e3..bb5cb52 100644 --- a/includes/database_service.hpp +++ b/includes/database_service.hpp @@ -1,28 +1,35 @@ #ifndef DATABASE_SERVICE_HPP #define DATABASE_SERVICE_HPP -#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); + 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: - std::shared_ptr getConnection(); //retrive a connection from the connection pool - - DatabaseService(boost::asio::io_context& io_context, std::size_t pool_size = 4); + DatabaseService(const std::string& db_path, std::size_t pool_size = 4); ~DatabaseService(); DatabaseService(DatabaseService const&); void operator=(DatabaseService const&); - boost::asio::io_context& io_context_; + std::shared_ptr getConnection(); + std::string generateShortUUID(); + + 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; + + static DatabaseService* INSTANCE; + static std::mutex singleton_mutex; }; #endif \ No newline at end of file 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/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/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/database_connection.cpp b/src/database_connection.cpp index 194bf2b..98f2db5 100644 --- a/src/database_connection.cpp +++ b/src/database_connection.cpp @@ -1,16 +1,111 @@ #include "../includes/database_service.hpp" -#include +#include "../includes/log_utils.hpp" +#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) { - static DatabaseService INSTANCE; - return INSTANCE; +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); + } + 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(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."); + } + + for (std::size_t i = 0; i < pool_size_; ++i) { + 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);"; + + 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() { + connection_pool_.clear(); +} +std::shared_ptr DatabaseService::getConnection() { + 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) % connection_pool_.size(); + 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) { + LOG(LogLevel::INFO, "Shortening URL: " + longURL); + 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) { + LOG(LogLevel::ERROR, "Failed to shorten URL: " + std::string(e.what())); + callback(std::make_error_code(std::errc::io_error), ""); + } +} + +void DatabaseService::getLongURL(const std::string& shortCode, std::function callback) { + LOG(LogLevel::INFO, "Getting long-URL from short-code: " + shortCode); + 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 { + LOG(LogLevel::ERROR, "Short code not found: " + shortCode); + callback(std::make_error_code(std::errc::no_such_file_or_directory), ""); + } + } catch (const std::exception& e) { + LOG(LogLevel::ERROR, "Failed to retrieve long URL: " + std::string(e.what())); + callback(std::make_error_code(std::errc::io_error), ""); + } } \ No newline at end of file diff --git a/src/http_connection.cpp b/src/http_connection.cpp index ed30d2f..949cd52 100644 --- a/src/http_connection.cpp +++ b/src/http_connection.cpp @@ -1,9 +1,8 @@ #include "../includes/http_connection.hpp" -#include +#include "../includes/log_utils.hpp" #include #include #include -#include HttpConnection::HttpConnection(io_context& io_context) : socket_(io_context) {}; @@ -22,21 +21,34 @@ 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(); - 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) { if (!error_code) { auto error_code_socket = self->socket_.shutdown(ip::tcp::socket::shutdown_send, error_code); @@ -45,6 +57,12 @@ 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; + } } }); + + }, response_); } \ No newline at end of file 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..8b57e85 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,24 +1,26 @@ - #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 3bedadf..2116be2 100644 --- a/src/request_handler.cpp +++ b/src/request_handler.cpp @@ -1,31 +1,173 @@ #include "../includes/request_handler.hpp" +#include "../includes/database_service.hpp" +#include "../includes/log_utils.hpp" #include #include #include #include #include +#include +#include +#include -http::response RequestHandler::handle(const http::request& request) { - string_view target = request.target(); +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"); + response.set(http::field::content_type, "text/html"); + response.body() = why; + response.keep_alive(false); + response.prepare_payload(); + return response; +} - 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(); +std::variant< + http::response, + http::response +> RequestHandler::handle(const http::request& request) { + std::string target = request.target(); + http::verb method = request.method(); - //todo: load angular application / plain html & js - response.body() = "

TEST

"; + auto& dbService = DatabaseService::getInstance("urls.db", 4); - response.prepare_payload(); - return response; + 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; + } + + 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/" + 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())); + } } - //case 2: "/url" -> redirect to expanded url - //case 3: neither -> redirect to 404 + return BadRequest("No rule matched."); +} - return http::response{http::status::bad_request, request.version()}; +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) { + 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