Compare commits

...
This repository has been archived on 2026-04-20. You can view files and clone it, but you cannot make any changes to it's state, such as pushing and creating new issues, pull requests or comments.

10 commits

Author SHA1 Message Date
rawalcher
b717bd96b1 modified regex
- it matched "www.google" which shouldnt be possible
2025-01-27 12:05:35 +01:00
rawalcher
611f9ecc5f . 2025-01-27 11:44:01 +01:00
rawalcher
39e1218cf8 . 2025-01-27 11:43:54 +01:00
rawalcher
5a8d51fc92 Modified dockerfile 2024-12-29 19:06:43 +01:00
rawalcher
0d89ce5e2d 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
2024-12-29 18:53:46 +01:00
rawalcher
cbfc0b563c sqlite database with methods added 2024-12-29 17:05:55 +01:00
=
897310fe28 integrate files with serving (even though ugly) 2024-12-16 20:17:09 +01:00
=
63fd2b7cca basic redirection w.o. db backend 2024-12-16 16:48:51 +01:00
Thomas Schleicher
f61be671d6 frontend draft 2024-12-16 12:46:52 +01:00
Thomas Schleicher
acb7221e34 database header 2024-12-16 11:30:41 +01:00
14 changed files with 545 additions and 67 deletions

13
.dockerignore Normal file
View file

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

View file

@ -1,5 +1,4 @@
cmake_minimum_required(VERSION 3.16) cmake_minimum_required(VERSION 3.16)
cmake_policy(SET CMP0167 OLD)
project(short-link VERSION 1.0 LANGUAGES CXX) project(short-link VERSION 1.0 LANGUAGES CXX)
@ -9,13 +8,23 @@ set(CMAKE_CXX_STANDARD_REQUIRED True)
set(BUILD_SHARED_LIBS OFF) set(BUILD_SHARED_LIBS OFF)
include_directories(${PROJECT_SOURCE_DIR}/includes) include_directories(${PROJECT_SOURCE_DIR}/includes)
add_executable(Application src/main.cpp
src/http_connection.cpp add_executable(Application
src/http_server.cpp src/main.cpp
src/request_handler.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) if(Boost_FOUND)
include_directories(${Boost_INCLUDE_DIRS}) include_directories(${Boost_INCLUDE_DIRS})
@ -23,4 +32,22 @@ if(Boost_FOUND)
target_link_libraries(Application PRIVATE Boost::filesystem Boost::system) target_link_libraries(Application PRIVATE Boost::filesystem Boost::system)
else() else()
message(FATAL_ERROR "Boost not found!") message(FATAL_ERROR "Boost not found!")
endif() 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,12 +3,24 @@ FROM ubuntu:20.04
ENV APPLICATION_BINARY="Application" ENV APPLICATION_BINARY="Application"
ENV OPEN_PORT=8080 ENV OPEN_PORT=8080
COPY ${APPLICATION_BINARY} .
RUN apt-get update && \ RUN apt-get update && \
ln -fs /usr/share/zoneinfo/America/New_York /etc/localtime && \ 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/* rm -rf /var/lib/apt/lists/*
WORKDIR /app
COPY . .
RUN mkdir build && cd build && \
cmake .. && \
make
EXPOSE ${OPEN_PORT} EXPOSE ${OPEN_PORT}
CMD [ "/${APPLICATION_BINARY}" ]
CMD ["./build/Application"]

46
frontend/index.html Normal file
View file

@ -0,0 +1,46 @@
<!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>

43
frontend/index.js Normal file
View file

@ -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 = `<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,28 +1,35 @@
#ifndef DATABASE_SERVICE_HPP #ifndef DATABASE_SERVICE_HPP
#define DATABASE_SERVICE_HPP #define DATABASE_SERVICE_HPP
#include <boost/asio/io_context.hpp> #include <string>
#include <cstddef> #include <functional>
#include <boost/redis/connection.hpp> #include <mutex>
#include <memory> #include <memory>
#include <vector>
#include <soci/soci.h>
class DatabaseService { class DatabaseService {
public: 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<void(std::error_code, std::string)> callback);
void getLongURL(const std::string& shortCode, std::function<void(std::error_code, std::string)> callback);
private: private:
std::shared_ptr<boost::redis::connection> getConnection(); //retrive a connection from the connection pool DatabaseService(const std::string& db_path, std::size_t pool_size = 4);
DatabaseService(boost::asio::io_context& io_context, std::size_t pool_size = 4);
~DatabaseService(); ~DatabaseService();
DatabaseService(DatabaseService const&); DatabaseService(DatabaseService const&);
void operator=(DatabaseService const&); void operator=(DatabaseService const&);
boost::asio::io_context& io_context_; std::shared_ptr<soci::session> getConnection();
std::string generateShortUUID();
std::string db_path_;
std::size_t pool_size_; std::size_t pool_size_;
std::vector<std::shared_ptr<boost::redis::connection>> connection_pool_; std::vector<std::shared_ptr<soci::session>> 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

View file

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

62
includes/log_utils.hpp Normal file
View file

@ -0,0 +1,62 @@
#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,14 +4,20 @@
#include <boost/beast/http.hpp> #include <boost/beast/http.hpp>
#include <boost/beast/http/message.hpp> #include <boost/beast/http/message.hpp>
#include <boost/beast/http/string_body.hpp> #include <boost/beast/http/string_body.hpp>
#include <variant>
using namespace boost::beast; using namespace boost::beast;
class RequestHandler { class RequestHandler {
public: public:
http::response<http::string_body> handle(const http::request<http::string_body>& request); std::variant<
http::response<http::string_body>,
http::response<http::file_body>
> handle(const http::request<http::string_body>& request);
private: 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 #endif

View file

@ -1,16 +1,111 @@
#include "../includes/database_service.hpp" #include "../includes/database_service.hpp"
#include <cstddef> #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>
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) {
static DatabaseService INSTANCE;
return INSTANCE; std::lock_guard<std::mutex> 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::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() { 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,9 +1,8 @@
#include "../includes/http_connection.hpp" #include "../includes/http_connection.hpp"
#include <boost/asio/io_context.hpp> #include "../includes/log_utils.hpp"
#include <boost/asio/ip/tcp.hpp> #include <boost/asio/ip/tcp.hpp>
#include <boost/beast/core/error.hpp> #include <boost/beast/core/error.hpp>
#include <iostream> #include <iostream>
#include <sys/socket.h>
HttpConnection::HttpConnection(io_context& io_context) HttpConnection::HttpConnection(io_context& io_context)
: socket_(io_context) {}; : socket_(io_context) {};
@ -22,21 +21,34 @@ void HttpConnection::process_connection() {
void HttpConnection::read() { void HttpConnection::read() {
auto self = shared_from_this(); auto self = shared_from_this();
http::async_read(socket_, buffer_, request_, http::async_read(socket_, buffer_, request_,
[self](boost::beast::error_code error_code, size_t bytes_transferred) { [self](boost::beast::error_code error_code, size_t bytes_transferred) {
if (!error_code) { 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->response_ = self->request_handler_.handle(self->request_);
self->write(); self->write();
} else { } else {
cerr << "Error reading: " << error_code.message() << endl; LOG(LogLevel::ERROR, "Error reading request: " + error_code.message());
} }
}); });
} }
void HttpConnection::write() { void HttpConnection::write() {
auto self = shared_from_this(); 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) { [self](boost::beast::error_code error_code, size_t bytes_transferred) {
if (!error_code) { if (!error_code) {
auto error_code_socket = self->socket_.shutdown(ip::tcp::socket::shutdown_send, error_code); auto error_code_socket = self->socket_.shutdown(ip::tcp::socket::shutdown_send, error_code);
@ -45,6 +57,12 @@ void HttpConnection::write() {
} }
} else { } else {
cerr << "Error writing response: " << error_code.message() << endl; 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_);
} }

View file

@ -1,8 +1,8 @@
#include "../includes/http_server.hpp" #include "../includes/http_server.hpp"
#include "../includes/log_utils.hpp"
#include <iostream>
#include "http_connection.hpp" #include "http_connection.hpp"
#include <boost/asio/io_context.hpp>
#include <boost/asio/placeholders.hpp> #include <boost/asio/placeholders.hpp>
#include <sys/socket.h>
HttpServer::HttpServer(io_context& io_context, const ip::tcp::endpoint& endpoint) HttpServer::HttpServer(io_context& io_context, const ip::tcp::endpoint& endpoint)
: io_context_(io_context), acceptor_(io_context, 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() { void HttpServer::accept_connection() {
HttpConnection::pointer new_connection = HttpConnection::create(io_context_); 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) { void HttpServer::handle_accept(HttpConnection::pointer& new_connection, const boost::system::error_code& error_code) {
if (!error_code) { if (!error_code) {
LOG(LogLevel::INFO, "Accepted new connection from: " + new_connection->socket().remote_endpoint().address().to_string());
new_connection->process_connection(); new_connection->process_connection();
} else {
LOG(LogLevel::ERROR, "Error accepting connection: " + error_code.message());
} }
accept_connection(); accept_connection();
} }

View file

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

View file

@ -1,31 +1,173 @@
#include "../includes/request_handler.hpp" #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/field.hpp>
#include <boost/beast/http/file_body.hpp> #include <boost/beast/http/file_body.hpp>
#include <boost/beast/http/message.hpp> #include <boost/beast/http/message.hpp>
#include <boost/beast/http/status.hpp> #include <boost/beast/http/status.hpp>
#include <boost/beast/http/string_body.hpp> #include <boost/beast/http/string_body.hpp>
#include <regex>
#include <future>
#include <filesystem>
http::response<http::string_body> RequestHandler::handle(const http::request<http::string_body>& request) { http::response<http::string_body> RequestHandler::BadRequest(const std::string& why) {
string_view target = request.target(); LOG(LogLevel::BADREQUEST, "BadRequest: " + 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;
}
if (target == "/") { std::variant<
//case 1: "/" -> serve angular frontend or static frontend what ever http::response<http::string_body>,
http::response<http::string_body> response; http::response<http::file_body>
response.result(http::status::ok); > RequestHandler::handle(const http::request<http::string_body>& request) {
response.version(request.version()); std::string target = request.target();
response.set(http::field::server, "Beast"); http::verb method = request.method();
response.set(http::field::content_type, "text/html");
response.keep_alive();
//todo: load angular application / plain html & js auto& dbService = DatabaseService::getInstance("urls.db", 4);
response.body() = "<html><h1>TEST</h1></html>";
response.prepare_payload(); LOG(LogLevel::INFO, "Received request:");
return response; 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<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())
));
}
});
try {
std::string shortURL = future.get();
LOG(LogLevel::INFO, "Shortened URL generated: " + shortURL);
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()));
}
}
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::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.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 return BadRequest("No rule matched.");
//case 3: neither -> redirect to 404 }
return http::response<http::string_body>{http::status::bad_request, request.version()}; 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;
} }