447 lines
15 KiB
C++
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__
|