// Copyright (C) EQ2EMU Team, GPL v3 License #ifndef __WEBSERVER_HPP__ #define __WEBSERVER_HPP__ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #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& req, http::response& 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& req, http::response& 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&, http::response&)> 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 void handle_request(http::request>&& req, std::function&&)> send); // Authenticates user credentials and manages sessions std::optional authenticate(const http::request& req, int32* user_status = nullptr); // Generates random session IDs for authenticated users std::string generate_session_id(); // Parses session cookie from request std::optional parse_session_cookie(std::string_view cookie_header); // Parses basic auth credentials from header std::optional> 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 sessions_; // Maps session_id to username std::unordered_map sessions_status_; // Maps session_id to user status level std::unordered_map credentials_; // Maps username to password for hardcoded users std::unordered_map route_required_status_; // Maps route to required status level std::unordered_map&, http::response&)>> routes_; // Authenticated routes std::unordered_map&, http::response&)>> 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(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&, http::response&)> handler, bool auth_req) { const std::string uri_str{uri}; const int32 status = database.NoAuthRoute(const_cast(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 stream(std::move(socket), ssl_ctx_); stream.handshake(ssl::stream_base::server); beast::flat_buffer buffer; bool close = false; while (!close) { http::request 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 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 void WebServer::handle_request(http::request>&& req, std::function&&)> 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 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 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 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 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::status::bad_request, req.version()}); } // Parses session cookie from cookie header std::optional 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> 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 WebServer::authenticate(const http::request& 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(username.c_str()), const_cast(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__