eq2go/old/common/web_server.hpp
2025-08-06 19:00:30 -05:00

447 lines
15 KiB
C++

// Copyright (C) EQ2EMU Team, GPL v3 License
#ifndef __WEBSERVER_HPP__
#define __WEBSERVER_HPP__
#include <boost/beast/core.hpp>
#include <boost/beast/http.hpp>
#include <boost/beast/ssl.hpp>
#include <boost/beast/version.hpp>
#include <boost/asio/ip/tcp.hpp>
#include <boost/asio/strand.hpp>
#include <boost/config.hpp>
#include <boost/beast/core/detail/base64.hpp>
#include <boost/algorithm/string.hpp>
#include <boost/property_tree/ptree.hpp>
#include <boost/property_tree/json_parser.hpp>
#include <iostream>
#include <sstream>
#include <random>
#include <chrono>
#include <string>
#include <string_view>
#include <thread>
#include <functional>
#include <unordered_map>
#include <optional>
#include <algorithm>
#include <pthread.h>
#include "types.hpp"
#include "version.hpp"
#ifdef WORLD
#include "../WorldServer/WorldDatabase.h"
extern WorldDatabase database;
#endif
#ifdef LOGIN
#include "../LoginServer/login_database.hpp"
extern LoginDatabase database;
#endif
namespace beast = boost::beast;
namespace http = beast::http;
namespace boost_net = boost::asio;
namespace ssl = boost::asio::ssl;
using tcp = boost_net::ip::tcp;
// Forward declaration for thread function
ThreadReturnType RunWebServer(void* tmp);
// Global variable for SSL key password
static std::string keypasswd;
// Constants for session management
constexpr size_t SESSION_ID_LENGTH = 32;
constexpr std::string_view HEX_CHARS = "0123456789abcdef";
constexpr std::string_view BASIC_AUTH_PREFIX = "Basic ";
constexpr std::string_view SESSION_COOKIE_NAME = "session_id";
// Handles version endpoint requests - returns application version information in JSON format
inline void web_handle_version(const http::request<http::string_body>& req, http::response<http::string_body>& res)
{
res.set(http::field::content_type, "application/json");
boost::property_tree::ptree pt;
// Add key-value pairs to the property tree
pt.put("eq2emu_process", EQ2EMU_MODULE);
pt.put("version", CURRENT_VERSION);
pt.put("compile_date", COMPILE_DATE);
pt.put("compile_time", COMPILE_TIME);
std::ostringstream oss;
boost::property_tree::write_json(oss, pt);
res.body() = oss.str();
res.prepare_payload();
}
// Handles root endpoint requests - returns simple greeting message
inline void web_handle_root(const http::request<http::string_body>& req, http::response<http::string_body>& res)
{
res.set(http::field::content_type, "text/html");
res.body() = "Hello!";
res.prepare_payload();
}
class WebServer
{
public:
// Constructor - initializes web server with SSL support and authentication
WebServer(std::string_view address, unsigned short port, std::string_view cert_file, std::string_view key_file, std::string_view key_password, std::string_view hardcode_user, std::string_view hardcode_password);
// Destructor - stops IO context
~WebServer();
// Starts the web server in a separate thread
void run();
// Starts the web server in current thread
void start();
// Registers a new route with optional authentication requirement
void register_route(std::string_view uri, std::function<void(const http::request<http::string_body>&, http::response<http::string_body>&)> handler, bool auth_required = true);
private:
bool is_ssl = false; // Flag indicating if SSL is enabled
// SSL password callback function for encrypted private keys
static std::string my_password_callback(std::size_t max_length, ssl::context::password_purpose purpose);
// Accepts incoming connections asynchronously
void do_accept();
// Handles accepted connections and determines SSL vs non-SSL session
void on_accept(beast::error_code ec, tcp::socket socket);
// Handles SSL encrypted sessions
void do_session_ssl(tcp::socket socket);
// Handles non-SSL sessions
void do_session(tcp::socket socket);
// Main request handler template - processes HTTP requests and routes them appropriately
template <class Body, class Allocator>
void handle_request(http::request<Body, http::basic_fields<Allocator>>&& req, std::function<void(http::response<http::string_body>&&)> send);
// Authenticates user credentials and manages sessions
std::optional<std::string> authenticate(const http::request<http::string_body>& req, int32* user_status = nullptr);
// Generates random session IDs for authenticated users
std::string generate_session_id();
// Parses session cookie from request
std::optional<std::string> parse_session_cookie(std::string_view cookie_header);
// Parses basic auth credentials from header
std::optional<std::pair<std::string, std::string>> parse_basic_auth(std::string_view auth_header);
boost_net::io_context ioc_{1}; // IO context for async operations
ssl::context ssl_ctx_{ssl::context::tlsv13_server}; // SSL context for encrypted connections
tcp::acceptor acceptor_{ioc_}; // TCP acceptor for incoming connections
std::unordered_map<std::string, std::string> sessions_; // Maps session_id to username
std::unordered_map<std::string, int32> sessions_status_; // Maps session_id to user status level
std::unordered_map<std::string, std::string> credentials_; // Maps username to password for hardcoded users
std::unordered_map<std::string, int32> route_required_status_; // Maps route to required status level
std::unordered_map<std::string, std::function<void(const http::request<http::string_body>&, http::response<http::string_body>&)>> routes_; // Authenticated routes
std::unordered_map<std::string, std::function<void(const http::request<http::string_body>&, http::response<http::string_body>&)>> noauth_routes_; // Non-authenticated routes
std::mt19937 rng_{std::random_device{}()}; // Random number generator for session IDs
std::uniform_int_distribution<> hex_dist_{0, 15}; // Distribution for hex characters
};
// SSL password callback implementation - returns the stored key password
std::string WebServer::my_password_callback(std::size_t max_length, ssl::context::password_purpose purpose)
{
return keypasswd;
}
// WebServer constructor - sets up SSL context, acceptor, and default routes
WebServer::WebServer(std::string_view address, unsigned short port, std::string_view cert_file, std::string_view key_file, std::string_view key_password, std::string_view hardcode_user, std::string_view hardcode_password)
: acceptor_(ioc_, {boost_net::ip::make_address(address), port})
{
keypasswd = key_password;
// Initialize SSL context if certificates provided
if (!cert_file.empty() && !key_file.empty()) {
ssl_ctx_.set_password_callback(my_password_callback);
ssl_ctx_.use_certificate_chain_file(std::string(cert_file));
ssl_ctx_.use_private_key_file(std::string(key_file), ssl::context::file_format::pem);
is_ssl = true;
}
keypasswd.clear(); // reset no longer needed
// Initialize hardcoded credentials if provided
if (!hardcode_user.empty() && !hardcode_password.empty()) {
credentials_[std::string(hardcode_user)] = hardcode_password;
}
register_route("/", web_handle_root);
register_route("/version", web_handle_version);
}
// WebServer destructor - stops the IO context
WebServer::~WebServer()
{
ioc_.stop();
}
// Thread function for running web server in separate thread
ThreadReturnType RunWebServer(void* tmp)
{
if (!tmp) THREAD_RETURN(NULL);
auto* ws = static_cast<WebServer*>(tmp);
ws->start();
THREAD_RETURN(NULL);
}
// Starts the web server by accepting connections and running IO context
void WebServer::start()
{
do_accept();
ioc_.run();
}
// Runs the web server in a detached thread
void WebServer::run()
{
pthread_t thread;
pthread_create(&thread, nullptr, RunWebServer, this);
pthread_detach(thread);
}
// Registers a route handler with optional authentication and database override
void WebServer::register_route(std::string_view uri, std::function<void(const http::request<http::string_body>&, http::response<http::string_body>&)> handler, bool auth_req)
{
const std::string uri_str{uri};
const int32 status = database.NoAuthRoute(const_cast<char*>(uri_str.c_str())); // overrides the default hardcode settings via DB
if (status == 0) auth_req = false;
if (auth_req) {
routes_[uri_str] = std::move(handler);
} else {
noauth_routes_[uri_str] = std::move(handler);
}
route_required_status_[uri_str] = status;
}
// Initiates asynchronous accept operation for incoming connections
void WebServer::do_accept()
{
acceptor_.async_accept([this](beast::error_code ec, tcp::socket socket) {
this->on_accept(ec, std::move(socket));
});
}
// Handles accepted connections and starts appropriate session type
void WebServer::on_accept(beast::error_code ec, tcp::socket socket)
{
if (!ec) {
if (is_ssl) {
std::thread(&WebServer::do_session_ssl, this, std::move(socket)).detach();
} else {
std::thread(&WebServer::do_session, this, std::move(socket)).detach();
}
}
do_accept();
}
// Handles SSL encrypted sessions with handshake and request processing
void WebServer::do_session_ssl(tcp::socket socket)
{
try {
ssl::stream<tcp::socket> stream(std::move(socket), ssl_ctx_);
stream.handshake(ssl::stream_base::server);
beast::flat_buffer buffer;
bool close = false;
while (!close) {
http::request<http::string_body> req;
http::read(stream, buffer, req);
handle_request(std::move(req), [&](auto&& response) {
if (response.need_eof()) close = true;
http::write(stream, response);
});
}
beast::error_code ec;
socket.shutdown(tcp::socket::shutdown_send, ec);
}
catch (const std::exception&) {
// Connection errors are expected and logged elsewhere if needed
}
}
// Handles non-SSL sessions with request processing loop
void WebServer::do_session(tcp::socket socket)
{
try {
beast::flat_buffer buffer;
bool close = false;
while (!close) {
http::request<http::string_body> req;
http::read(socket, buffer, req);
handle_request(std::move(req), [&](auto&& response) {
if (response.need_eof()) close = true;
http::write(socket, response);
});
}
beast::error_code ec;
socket.shutdown(tcp::socket::shutdown_send, ec);
}
catch (const std::exception&) {
// Connection errors are expected and logged elsewhere if needed
}
}
// Main request handler - routes requests based on authentication and authorization
template <class Body, class Allocator>
void WebServer::handle_request(http::request<Body, http::basic_fields<Allocator>>&& req, std::function<void(http::response<http::string_body>&&)> send)
{
const std::string target = req.target().to_string();
// Check for non-authenticated routes first
if (const auto it = noauth_routes_.find(target); it != noauth_routes_.end()) {
http::response<http::string_body> res{http::status::ok, req.version()};
res.set(http::field::server, BOOST_BEAST_VERSION_STRING);
it->second(req, res);
return send(std::move(res));
}
// Authenticate user
int32 user_status = 0;
const auto session_id = authenticate(req, &user_status);
if (!session_id) {
http::response<http::string_body> res{http::status::unauthorized, req.version()};
res.set(http::field::server, BOOST_BEAST_VERSION_STRING);
res.set(http::field::www_authenticate, "Basic realm=\"example\"");
res.body() = "Unauthorized";
res.prepare_payload();
return send(std::move(res));
}
// Check authorization level
if (const auto status_it = route_required_status_.find(target); status_it != route_required_status_.end()) {
const auto required_status = status_it->second;
if (required_status > 0 && required_status != 0xFFFFFFFF && required_status > user_status) {
http::response<http::string_body> res{http::status::unauthorized, req.version()};
res.set(http::field::server, BOOST_BEAST_VERSION_STRING);
res.body() = "Unauthorized status";
res.prepare_payload();
return send(std::move(res));
}
}
// Handle authenticated routes
if (const auto it = routes_.find(target); it != routes_.end()) {
http::response<http::string_body> res{http::status::ok, req.version()};
res.set(http::field::set_cookie, std::string(SESSION_COOKIE_NAME) + "=" + *session_id);
res.set(http::field::server, BOOST_BEAST_VERSION_STRING);
it->second(req, res);
return send(std::move(res));
}
return send(http::response<http::string_body>{http::status::bad_request, req.version()});
}
// Parses session cookie from cookie header
std::optional<std::string> WebServer::parse_session_cookie(std::string_view cookie_header)
{
const auto pos = cookie_header.find(SESSION_COOKIE_NAME);
if (pos == std::string_view::npos) return std::nullopt;
const auto value_start = pos + SESSION_COOKIE_NAME.length() + 1; // +1 for '='
if (value_start >= cookie_header.length()) return std::nullopt;
const auto value_end = cookie_header.find(';', value_start);
const auto session_id = cookie_header.substr(value_start,
value_end == std::string_view::npos ? std::string_view::npos : value_end - value_start);
return std::string(session_id);
}
// Parses basic auth credentials from authorization header
std::optional<std::pair<std::string, std::string>> WebServer::parse_basic_auth(std::string_view auth_header)
{
if (!auth_header.starts_with(BASIC_AUTH_PREFIX)) return std::nullopt;
const auto encoded_credentials = auth_header.substr(BASIC_AUTH_PREFIX.length());
std::string decoded_credentials;
decoded_credentials.resize(boost::beast::detail::base64::decoded_size(encoded_credentials.size()));
const auto result = boost::beast::detail::base64::decode(
decoded_credentials.data(),
encoded_credentials.data(),
encoded_credentials.size()
);
decoded_credentials.resize(result.first);
const auto colon_pos = decoded_credentials.find(':');
if (colon_pos == std::string::npos) return std::nullopt;
return std::make_pair(
decoded_credentials.substr(0, colon_pos),
decoded_credentials.substr(colon_pos + 1)
);
}
// Authenticates users via session cookies or Basic Auth and returns session ID
std::optional<std::string> WebServer::authenticate(const http::request<http::string_body>& req, int32* user_status)
{
// Try session cookie first
if (const auto cookie_it = req.find(http::field::cookie); cookie_it != req.end()) {
if (const auto session_id = parse_session_cookie(cookie_it->value().to_string())) {
if (const auto session_it = sessions_.find(*session_id); session_it != sessions_.end()) {
if (user_status) {
if (const auto status_it = sessions_status_.find(*session_id); status_it != sessions_status_.end()) {
*user_status = status_it->second;
}
}
return *session_id;
}
}
}
// Try basic authentication
if (const auto auth_it = req.find(http::field::authorization); auth_it != req.end()) {
if (const auto credentials = parse_basic_auth(auth_it->value().to_string())) {
const auto& [username, password] = *credentials;
int32 out_status = 0;
// Check hardcoded credentials or database
const bool auth_success = (credentials_.contains(username) && credentials_[username] == password) ||
(database.AuthenticateWebUser(const_cast<char*>(username.c_str()), const_cast<char*>(password.c_str()), &out_status) > 0);
if (auth_success) {
const auto session_id = generate_session_id();
sessions_[session_id] = username;
sessions_status_[session_id] = out_status;
if (user_status) *user_status = out_status;
return session_id;
}
}
}
return std::nullopt;
}
// Generates a random 32-character hexadecimal session ID
std::string WebServer::generate_session_id()
{
std::string session_id;
session_id.reserve(SESSION_ID_LENGTH);
for (size_t i = 0; i < SESSION_ID_LENGTH; ++i) {
session_id += HEX_CHARS[hex_dist_(rng_)];
}
return session_id;
}
#endif // __WEBSERVER_HPP__