Compare commits

..

1 commit

Author SHA1 Message Date
=
48d1851541 basic redirection w.o. db backend 2024-12-16 16:41:01 +01:00
14 changed files with 106 additions and 528 deletions

View file

@ -1,13 +0,0 @@
# Ignore build files
build/
cmake-build-*
# Ignore editor-specific files
*.swp
*.log
.idea/
.vscode/
# Ignore version control directories
.git/
.gitignore

View file

@ -1,4 +1,5 @@
cmake_minimum_required(VERSION 3.16)
cmake_policy(SET CMP0167 OLD)
project(short-link VERSION 1.0 LANGUAGES CXX)
@ -8,23 +9,13 @@ set(CMAKE_CXX_STANDARD_REQUIRED True)
set(BUILD_SHARED_LIBS OFF)
include_directories(${PROJECT_SOURCE_DIR}/includes)
add_executable(Application
src/main.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
)
add_dependencies(Application copy_frontend)
find_package(Boost REQUIRED COMPONENTS filesystem system)
find_package(Boost REQUIRED filesystem system)
if(Boost_FOUND)
include_directories(${Boost_INCLUDE_DIRS})
@ -33,21 +24,3 @@ if(Boost_FOUND)
else()
message(FATAL_ERROR "Boost not found!")
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()

View file

@ -3,24 +3,12 @@ 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 \
build-essential \
cmake \
libboost-all-dev \
git \
libsqlite3-dev && \
DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends libboost-all-dev && \
rm -rf /var/lib/apt/lists/*
WORKDIR /app
COPY . .
RUN mkdir build && cd build && \
cmake .. && \
make
EXPOSE ${OPEN_PORT}
CMD ["./build/Application"]
CMD [ "/${APPLICATION_BINARY}" ]

View file

@ -1,46 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Short-Link</title>
<style>
html, body {
margin: 0;
height: 100%;
}
#background {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: -1;
object-fit: cover;
}
#container {
height: 100%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
background-color: transparent;
}
</style>
</head>
<body>
<svg id="background" xmlns="http://www.w3.org/2000/svg" version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:svgjs="http://svgjs.dev/svgjs" viewBox="0 0 800 800"><defs><linearGradient x1="50%" y1="0%" x2="50%" y2="100%" id="nnneon-grad"><stop stop-color="hsl(157, 100%, 54%)" stop-opacity="1" offset="0%"></stop><stop stop-color="hsl(331, 87%, 61%)" stop-opacity="1" offset="100%"></stop></linearGradient><filter id="nnneon-filter" x="-100%" y="-100%" width="400%" height="400%" filterUnits="objectBoundingBox" primitiveUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feGaussianBlur stdDeviation="17 8" x="0%" y="0%" width="100%" height="100%" in="SourceGraphic" edgeMode="none" result="blur"></feGaussianBlur></filter><filter id="nnneon-filter2" x="-100%" y="-100%" width="400%" height="400%" filterUnits="objectBoundingBox" primitiveUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feGaussianBlur stdDeviation="10 17" x="0%" y="0%" width="100%" height="100%" in="SourceGraphic" edgeMode="none" result="blur"></feGaussianBlur></filter></defs><g stroke-width="16" stroke="url(#nnneon-grad)" fill="none"><path d="M388.4530107167675 233.46125231926777C395.59828768523175 229.33666774729716 404.40171231476825 229.33666774729716 411.5469892832325 233.46240701819613L538.4530203016707 306.7315181170911C545.5982972701349 310.8561026890618 550.0000095849032 318.48057971278024 550.0000095849032 326.7309035556499V473.2691257534399C550.0000095849032 481.5194495963096 545.5982972701349 489.14392662002797 538.4530203016707 493.26966589092694L411.5469892832325 566.538776989822C404.40171231476825 570.6633615617925 395.59828768523175 570.6633615617925 388.4530107167675 566.5376222908938L261.54697969832927 493.26851119199864C254.401702729865 489.14392662002797 249.99999041509682 481.5194495963096 249.99999041509682 473.2691257534399V326.7309035556499C249.99999041509682 318.48057971278024 254.401702729865 310.8561026890618 261.54697969832927 306.7303634181629L388.4530107167675 233.46125231926777Z " filter="url(#nnneon-filter)"></path><path d="M400.4530107167675 233.46125231926777C407.59828768523175 229.33666774729716 416.40171231476825 229.33666774729716 423.5469892832325 233.46240701819613L550.4530203016707 306.7315181170911C557.5982972701349 310.8561026890618 562.0000095849032 318.48057971278024 562.0000095849032 326.7309035556499V473.2691257534399C562.0000095849032 481.5194495963096 557.5982972701349 489.14392662002797 550.4530203016707 493.26966589092694L423.5469892832325 566.538776989822C416.40171231476825 570.6633615617925 407.59828768523175 570.6633615617925 400.4530107167675 566.5376222908938L273.54697969832927 493.26851119199864C266.401702729865 489.14392662002797 261.9999904150968 481.5194495963096 261.9999904150968 473.2691257534399V326.7309035556499C261.9999904150968 318.48057971278024 266.401702729865 310.8561026890618 273.54697969832927 306.7303634181629L400.4530107167675 233.46125231926777Z " filter="url(#nnneon-filter2)" opacity="0.25"></path><path d="M376.4530107167675 233.46125231926777C383.59828768523175 229.33666774729716 392.40171231476825 229.33666774729716 399.5469892832325 233.46240701819613L526.4530203016707 306.7315181170911C533.5982972701349 310.8561026890618 538.0000095849032 318.48057971278024 538.0000095849032 326.7309035556499V473.2691257534399C538.0000095849032 481.5194495963096 533.5982972701349 489.14392662002797 526.4530203016707 493.26966589092694L399.5469892832325 566.538776989822C392.40171231476825 570.6633615617925 383.59828768523175 570.6633615617925 376.4530107167675 566.5376222908938L249.54697969832927 493.26851119199864C242.401702729865 489.14392662002797 237.99999041509682 481.5194495963096 237.99999041509682 473.2691257534399V326.7309035556499C237.99999041509682 318.48057971278024 242.401702729865 310.8561026890618 249.54697969832927 306.7303634181629L376.4530107167675 233.46125231926777Z " filter="url(#nnneon-filter2)" opacity="0.25"></path><path d="M388.4530107167675 233.46125231926777C395.59828768523175 229.33666774729716 404.40171231476825 229.33666774729716 411.5469892832325 233.46240701819613L538.4530203016707 306.7315181170911C545.5982972701349 310.8561026890618 550.0000095849032 318.48057971278024 550.0000095849032 326.7309035556499V473.2691257534399C550.0000095849032 481.5194495963096 545.5982972701349 489.14392662002797 538.4530203016707 493.26966589092694L411.5469892832325 566.538776989822C404.40171231476825 570.6633615617925 395.59828768523175 570.6633615617925 388.4530107167675 566.5376222908938L261.54697969832927 493.26851119199864C254.401702729865 489.14392662002797 249.99999041509682 481.5194495963096 249.99999041509682 473.2691257534399V326.7309035556499C249.99999041509682 318.48057971278024 254.401702729865 310.8561026890618 261.54697969832927 306.7303634181629L388.4530107167675 233.46125231926777Z "></path></g></svg>
<div id="container">
<h1>Short-Link</h1>
<div id="input-container">
<input type="url" id="urlInput" placeholder="Enter URL here...">
<input type="button" id="shortenButton" value="Submit">
</div>
<div id="output-container"></div>
<div id="error-container"></div>
</div>
<script src="index.js"></script>
</body>
</html>

View file

@ -1,43 +0,0 @@
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 = `<p>Shortened URL: <a href="${shortenedUrl}" target="_blank">${shortenedUrl}</a></p>`;
errorContainer.innerHTML = "";
}else{
const error = await response.text();
errorContainer.innerHTML = `<p>Something went wrong: ${error}</p>`;
outputContainer.innerHTML = "";
}
}
});

View file

@ -1,35 +1,28 @@
#ifndef DATABASE_SERVICE_HPP
#define DATABASE_SERVICE_HPP
#include <string>
#include <functional>
#include <mutex>
#include <boost/asio/io_context.hpp>
#include <cstddef>
#include <boost/redis/connection.hpp>
#include <memory>
#include <vector>
#include <soci/soci.h>
class DatabaseService {
public:
static DatabaseService& getInstance(const std::string& db_path, std::size_t pool_size);
void shortenURL(const std::string& longURL, std::function<void(std::error_code, std::string)> callback);
void getLongURL(const std::string& shortCode, std::function<void(std::error_code, std::string)> callback);
static DatabaseService& getInstance(boost::asio::io_context& io_context, std::size_t pool_size);
private:
DatabaseService(const std::string& db_path, std::size_t pool_size = 4);
std::shared_ptr<boost::redis::connection> getConnection(); //retrive a connection from the connection pool
DatabaseService(boost::asio::io_context& io_context, std::size_t pool_size = 4);
~DatabaseService();
DatabaseService(DatabaseService const&);
void operator=(DatabaseService const&);
std::shared_ptr<soci::session> getConnection();
std::string generateShortUUID();
std::string db_path_;
boost::asio::io_context& io_context_;
std::size_t pool_size_;
std::vector<std::shared_ptr<soci::session>> connection_pool_;
std::size_t current_index_ = 0;
static DatabaseService* INSTANCE;
static std::mutex singleton_mutex;
std::vector<std::shared_ptr<boost::redis::connection>> connection_pool_;
std::size_t current_index_ = 0; //index used for round robin selection
};
#endif

View file

@ -8,7 +8,6 @@
#include <boost/beast/http/string_body.hpp>
#include <boost/beast/http.hpp>
#include <boost/beast/core.hpp>
#include <variant>
using namespace std;
using namespace boost::asio;
@ -26,7 +25,7 @@ private:
ip::tcp::socket socket_;
boost::beast::flat_buffer buffer_;
http::request<http::string_body> request_;
variant<http::response<http::string_body>, http::response<http::file_body>> response_;
http::response<http::string_body> response_;
RequestHandler request_handler_;
void read();

View file

@ -1,62 +0,0 @@
#ifndef LOG_UTILS_HPP
#define LOG_UTILS_HPP
#include <iostream>
#include <string>
#include <chrono>
#include <iomanip>
#include <filesystem>
#include <unordered_map>
// 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<std::string> 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

View file

@ -4,20 +4,14 @@
#include <boost/beast/http.hpp>
#include <boost/beast/http/message.hpp>
#include <boost/beast/http/string_body.hpp>
#include <variant>
using namespace boost::beast;
class RequestHandler {
public:
std::variant<
http::response<http::string_body>,
http::response<http::file_body>
> handle(const http::request<http::string_body>& request);
http::response<http::string_body> handle(const http::request<http::string_body>& request);
private:
http::response<http::string_body> BadRequest(const std::string& why);
http::response<http::file_body> handle_file_request(const std::string& filename, boost::system::error_code& ec);
};
#endif

View file

@ -1,111 +1,16 @@
#include "../includes/database_service.hpp"
#include "../includes/log_utils.hpp"
#include <boost/uuid/uuid.hpp>
#include <boost/uuid/uuid_generators.hpp>
#include <boost/uuid/uuid_io.hpp>
#include <stdexcept>
#include <mutex>
#include <soci/soci.h>
#include <soci/sqlite3/soci-sqlite3.h>
#include <cstddef>
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<std::mutex> lock(singleton_mutex);
if (!INSTANCE) {
INSTANCE = new DatabaseService(db_path, pool_size);
}
return *INSTANCE;
DatabaseService& DatabaseService::getInstance(boost::asio::io_context &io_context, std::size_t pool_size) {
static DatabaseService INSTANCE;
return INSTANCE;
}
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");
DatabaseService::DatabaseService(boost::asio::io_context& io_context, std::size_t pool_size) : io_context_(io_context), pool_size_(pool_size) {
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::session>(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<soci::session> 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<void(std::error_code, std::string)> 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<void(std::error_code, std::string)> 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), "");
}
}

View file

@ -1,8 +1,9 @@
#include "../includes/http_connection.hpp"
#include "../includes/log_utils.hpp"
#include <boost/asio/io_context.hpp>
#include <boost/asio/ip/tcp.hpp>
#include <boost/beast/core/error.hpp>
#include <iostream>
#include <sys/socket.h>
HttpConnection::HttpConnection(io_context& io_context)
: socket_(io_context) {};
@ -21,35 +22,23 @@ 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) {
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...");
cout << "Request received:\n" << self->request_ << endl;
self->response_ = self->request_handler_.handle(self->request_);
self->write();
} else {
LOG(LogLevel::ERROR, "Error reading request: " + error_code.message());
cerr << "Error reading: " << error_code.message() << endl;
}
});
}
void HttpConnection::write() {
auto self = shared_from_this();
std::visit(
[this, self](auto& response) {
http::async_write(socket_, 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) {
@ -63,6 +52,4 @@ void HttpConnection::write() {
}
}
});
}, response_);
}

View file

@ -1,8 +1,8 @@
#include "../includes/http_server.hpp"
#include "../includes/log_utils.hpp"
#include <iostream>
#include "http_connection.hpp"
#include <boost/asio/io_context.hpp>
#include <boost/asio/placeholders.hpp>
#include <sys/socket.h>
HttpServer::HttpServer(io_context& io_context, const ip::tcp::endpoint& endpoint)
: io_context_(io_context), acceptor_(io_context, endpoint) {
@ -11,18 +11,14 @@ HttpServer::HttpServer(io_context& io_context, const ip::tcp::endpoint& endpoint
void HttpServer::accept_connection() {
HttpConnection::pointer new_connection = HttpConnection::create(io_context_);
LOG(LogLevel::INFO, "Waiting for new connection");
acceptor_.async_accept(new_connection->socket(),
bind(&HttpServer::handle_accept, this, new_connection, boost::asio::placeholders::error));
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();
}

View file

@ -1,25 +1,23 @@
#include "../includes/http_server.hpp"
#include "../includes/log_utils.hpp"
#include <iostream>
#include <boost/asio.hpp>
#include "../includes/http_server.hpp"
#include <iostream>
using namespace boost::asio;
int main(int argc, char* argv[]) {
int main(int argc, char *argv[]) {
const int port = 8080;
LOG(LogLevel::INFO, "Starting Server on 127.0.0.1:" + std::to_string(port));
try {
std::cout << "Starting server!" << std::endl;
try
{
io_context io_context;
ip::tcp::endpoint endpoint(ip::tcp::v4(), port);
HttpServer server(io_context, endpoint);
io_context.run();
} catch (const std::exception& e) {
LOG(LogLevel::ERROR, "Exception occurred: " + std::string(e.what()));
}
catch (std::exception& e)
{
std::cerr << e.what() << std::endl;
}
return 0;

View file

@ -1,173 +1,82 @@
#include "../includes/request_handler.hpp"
#include "../includes/database_service.hpp"
#include "../includes/log_utils.hpp"
#include <boost/beast/http/field.hpp>
#include <boost/beast/http/file_body.hpp>
#include <boost/beast/http/message.hpp>
#include <boost/beast/http/status.hpp>
#include <boost/beast/http/string_body.hpp>
#include <regex>
#include <future>
#include <filesystem>
http::response<http::string_body> RequestHandler::BadRequest(const std::string& why) {
LOG(LogLevel::BADREQUEST, "BadRequest: " + why);
http::response<http::string_body> BadRequest(const std::string& why) {
http::response<http::string_body> 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;
}
std::variant<
http::response<http::string_body>,
http::response<http::file_body>
> RequestHandler::handle(const http::request<http::string_body>& request) {
std::string target = request.target();
http::response<http::string_body> RequestHandler::handle(const http::request<http::string_body>& request) {
string_view 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<http::string_body> response;
response.result(http::status::ok);
response.version(request.version());
response.set(http::field::server, "Beast");
response.set(http::field::content_type, "text/html");
auto& dbService = DatabaseService::getInstance("urls.db", 4);
//todo: load angular application / plain html & js
response.body() = "<html><h1>TEST</h1></html>";
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 /");
response.prepare_payload();
return response;
}
if (request.find(http::field::content_type) == request.end()) {
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") {
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)) {
if(!std::regex_match(url, url_match, url_regex)) {
return BadRequest("Invalid URL");
}
if (!url_match[1].matched) {
if(!url_match[1].matched){
url = "https://" + url;
}
LOG(LogLevel::INFO, "Valid URL: " + url);
//todo: save url to database and return short url
std::promise<std::string> promise;
std::future<std::string> 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())
));
return BadRequest("Request is actually not bad. processing " + url);
}
});
try {
std::string shortURL = future.get();
LOG(LogLevel::INFO, "Shortened URL generated: " + shortURL);
}else {
if(method == http::verb::get){
http::response<http::string_body> 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()));
}
std::string short_url = target.substr(1);
}
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<http::file_body> 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::string expanded_url = "https://google.com"; //todo: get expanded url from database
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<std::string> promise;
std::future<std::string> 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<http::string_body> response;
response.result(http::status::moved_permanently);
response.set(http::field::location, expandedURL);
response.set(http::field::location, expanded_url);
response.version(request.version());
response.set(http::field::server, "Beast");
response.body() = "Redirecting to " + expandedURL;
response.keep_alive(false);
response.body() = "Redirecting to " + expanded_url;
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()));
} else {
return BadRequest("Method not allowed");
}
}
//case 2: "/url" -> redirect to expanded url
//case 3: neither -> redirect to 404
return BadRequest("No rule matched.");
}
http::response<http::file_body> 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<http::file_body> 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;
}