rewrite HTTP interface

This commit is contained in:
Martin Michelsen
2025-11-16 13:12:33 -08:00
parent 11cc19fe3e
commit 62c4c82fcc
9 changed files with 895 additions and 831 deletions
+6 -7
View File
@@ -163,7 +163,7 @@ asio::awaitable<HTTPRequest> HTTPClient::recv_http_request(size_t max_line_size,
} else if (method_token == "TRACE") { } else if (method_token == "TRACE") {
req.method = HTTPRequest::Method::TRACE; req.method = HTTPRequest::Method::TRACE;
} else { } else {
throw HTTPError(400, "unknown request method"); throw HTTPError(400, "Unknown request method");
} }
req.http_version = std::move(line_tokens[2]); req.http_version = std::move(line_tokens[2]);
@@ -237,26 +237,26 @@ asio::awaitable<HTTPRequest> HTTPClient::recv_http_request(size_t max_line_size,
size_t parse_offset = 0; size_t parse_offset = 0;
size_t chunk_size = stoull(line, &parse_offset, 16); size_t chunk_size = stoull(line, &parse_offset, 16);
if (parse_offset != line.size()) { if (parse_offset != line.size()) {
throw HTTPError(400, "invalid chunk header during chunked encoding"); throw HTTPError(400, "Invalid chunk header during chunked encoding");
} }
if (chunk_size == 0) { if (chunk_size == 0) {
break; break;
} }
total_data_bytes += chunk_size; total_data_bytes += chunk_size;
if (total_data_bytes > max_body_size) { if (total_data_bytes > max_body_size) {
throw HTTPError(400, "request data size too large"); throw HTTPError(400, "Request data size too large");
} }
chunks.emplace_back(co_await this->r.read_data(chunk_size)); chunks.emplace_back(co_await this->r.read_data(chunk_size));
auto after_chunk_data = co_await this->r.read_line("\r\n", 0x20); auto after_chunk_data = co_await this->r.read_line("\r\n", 0x20);
if (!after_chunk_data.empty()) { if (!after_chunk_data.empty()) {
throw HTTPError(400, "incorrect trailing sequence after chunk data"); throw HTTPError(400, "Incorrect trailing sequence after chunk data");
} }
} }
} else { } else {
auto content_length_header = req.get_header("content-length"); auto content_length_header = req.get_header("content-length");
size_t content_length = content_length_header ? stoull(*content_length_header) : 0; size_t content_length = content_length_header ? stoull(*content_length_header) : 0;
if (content_length > max_body_size) { if (content_length > max_body_size) {
throw HTTPError(400, "request data size too large"); throw HTTPError(400, "Request data size too large");
} else if (content_length > 0) { } else if (content_length > 0) {
req.data = co_await this->r.read_data(content_length); req.data = co_await this->r.read_data(content_length);
} }
@@ -289,8 +289,7 @@ asio::awaitable<WebSocketMessage> HTTPClient::recv_websocket_message(size_t max_
while (this->r.get_socket().is_open()) { while (this->r.get_socket().is_open()) {
WebSocketMessage msg; WebSocketMessage msg;
// We need at most 10 bytes to determine if there's a valid frame, or as // We need at most 10 bytes to determine if there's a valid frame, or as little as 2
// little as 2
co_await this->r.read_data_into(msg.header, 2); co_await this->r.read_data_into(msg.header, 2);
// Get the payload size // Get the payload size
+124
View File
@@ -9,6 +9,7 @@
#include <functional> #include <functional>
#include <memory> #include <memory>
#include <optional> #include <optional>
#include <phosg/Encoding.hh>
#include <phosg/Hash.hh> #include <phosg/Hash.hh>
#include <phosg/Time.hh> #include <phosg/Time.hh>
#include <string> #include <string>
@@ -82,6 +83,106 @@ struct HTTPClient {
asio::awaitable<void> send_websocket_message(const std::string& data, uint8_t opcode = 0x01); asio::awaitable<void> send_websocket_message(const std::string& data, uint8_t opcode = 0x01);
}; };
template <typename RetT>
class HTTPRouter {
public:
struct Args {
std::shared_ptr<HTTPClient> client;
const HTTPRequest& req;
std::unordered_map<std::string, std::string> params;
phosg::JSON post_data;
template <typename T>
requires(std::is_integral_v<T>)
T get_param(const char* name, bool hex = false) const {
const auto& value_str = this->params.at(name);
size_t conversion_end;
int64_t v = std::stoull(value_str, &conversion_end, hex ? 16 : 0);
if (conversion_end != value_str.size()) {
throw HTTPError(400, "Invalid integer value");
}
uint64_t uv = static_cast<uint64_t>(v);
if constexpr (std::is_unsigned_v<T>) {
if (uv & (~phosg::mask_for_type<T>)) {
throw HTTPError(400, "Unsigned value out of range");
}
return uv;
} else {
if (((uv & (~(phosg::mask_for_type<T> >> 1))) != 0) && ((uv & (~(phosg::mask_for_type<T> >> 1))) != (~(phosg::mask_for_type<T> >> 1)))) {
throw HTTPError(400, "Signed value out of range");
}
return v;
}
}
};
using Handler = std::function<asio::awaitable<RetT>(Args&&)>;
static std::vector<std::string> split_and_normalize_path(const std::string& path) {
auto path_tokens = phosg::split(path, '/');
while (!path_tokens.empty() && path_tokens.back().empty()) {
path_tokens.pop_back();
}
return path_tokens;
}
void add(HTTPRequest::Method method, const std::string& path_pattern, Handler handler) {
this->routes.emplace_back(Route{
.method = method, .path_tokens = this->split_and_normalize_path(path_pattern), .handler = handler});
}
asio::awaitable<RetT> call_handler(std::shared_ptr<HTTPClient> c, const HTTPRequest& req) {
Args args = {.client = c, .req = req, .params = {}, .post_data = phosg::JSON()};
auto tokens = this->split_and_normalize_path(req.path);
for (const auto& route : this->routes) {
if (route.path_tokens.size() != tokens.size()) {
continue;
}
bool matched = true;
args.params.clear();
for (size_t z = 0; z < tokens.size(); z++) {
if (route.path_tokens[z].starts_with(':')) {
args.params.emplace(route.path_tokens[z].substr(1), tokens[z]);
} else if (route.path_tokens[z] != tokens[z]) {
matched = false;
break;
}
}
if (matched) {
if (req.method != route.method) {
throw HTTPError(405, "Incorrect HTTP method");
}
if (req.method == HTTPRequest::Method::POST) {
auto* content_type = req.get_header("content-type");
if (!content_type || (*content_type != "application/json")) {
throw HTTPError(400, "POST requests must use the application/json content type");
}
try {
args.post_data = phosg::JSON::parse(req.data);
} catch (const std::exception& e) {
throw HTTPError(400, std::format("Invalid JSON: {}", e.what()));
}
}
co_return co_await route.handler(std::move(args));
}
}
throw HTTPError(404, "Request path did not match any route");
}
private:
struct Route {
HTTPRequest::Method method;
std::vector<std::string> path_tokens;
Handler handler;
};
std::vector<Route> routes;
};
struct HTTPServerLimits { struct HTTPServerLimits {
size_t max_http_request_line_size = 0x1000; // 4KB size_t max_http_request_line_size = 0x1000; // 4KB
size_t max_http_data_size = 0x200000; // 2MB size_t max_http_data_size = 0x200000; // 2MB
@@ -120,6 +221,29 @@ public:
protected: protected:
HTTPServerLimits limits; HTTPServerLimits limits;
void require_GET(const HTTPRequest& req) {
if (req.method != HTTPRequest::Method::GET) {
throw HTTPError(405, "GET method required for this endpoint");
}
}
phosg::JSON require_JSON_POST(const HTTPRequest& req) {
if (req.method != HTTPRequest::Method::POST) {
throw HTTPError(405, "POST method required for this endpoint");
}
auto* content_type = req.get_header("content-type");
if (!content_type || (*content_type != "application/json")) {
throw HTTPError(400, "POST requests must use the application/json content type");
}
try {
return phosg::JSON::parse(req.data);
} catch (const std::exception& e) {
throw HTTPError(400, std::format("Invalid JSON: {}", e.what()));
}
}
// Attempts to switch the client to WebSockets. Returns true if this is done // Attempts to switch the client to WebSockets. Returns true if this is done
// successfully (and the caller should then receive/send WebSocket messages), // successfully (and the caller should then receive/send WebSocket messages),
// or false if this failed (and the caller should send an HTTP response). // or false if this failed (and the caller should send an HTTP response).
+4
View File
@@ -265,7 +265,11 @@ asio::awaitable<std::invoke_result_t<FnT, ArgTs...>> call_on_thread_pool(asio::t
// call_on_thread_pool coroutine has been destroyed) // call_on_thread_pool coroutine has been destroyed)
auto promise = std::make_shared<AsyncPromise<ReturnT>>(); auto promise = std::make_shared<AsyncPromise<ReturnT>>();
asio::post(pool, [bound = std::move(bound), promise]() mutable { asio::post(pool, [bound = std::move(bound), promise]() mutable {
try {
promise->set_value(bound()); promise->set_value(bound());
} catch (...) {
promise->set_exception(std::current_exception());
}
}); });
co_return co_await promise->get(); co_return co_await promise->get();
} }
+310 -366
View File
@@ -17,99 +17,47 @@
using namespace std; using namespace std;
HTTPServer::HTTPServer(shared_ptr<ServerState> state) HTTPServer::HTTPServer(shared_ptr<ServerState> state)
: AsyncHTTPServer(state->io_context, "[HTTPServer] "), state(state) {} : AsyncHTTPServer(state->io_context, "[HTTPServer] "), state(state) {
using RouterRetT = std::variant<RawResponse, std::shared_ptr<const phosg::JSON>>;
using RetT = asio::awaitable<RouterRetT>;
using ArgsT = HTTPRouter<RouterRetT>::Args;
asio::awaitable<void> HTTPServer::send_rare_drop_notification(shared_ptr<const phosg::JSON> message) { auto generate_server_version_json = []() -> phosg::JSON {
if (!this->rare_drop_subscribers.empty()) { return phosg::JSON::dict({
string data = message->serialize();
// Make a copy of the rare drop subscribers set, so we can guarantee that
// the client objects are all valid until this coroutine returns
unordered_set<shared_ptr<HTTPClient>> subscribers = this->rare_drop_subscribers;
size_t expected_results = subscribers.size();
AsyncPromise<void> complete_promise;
auto fn = [this, &data, &expected_results, &complete_promise](shared_ptr<HTTPClient> c) -> asio::awaitable<void> {
try {
co_await c->send_websocket_message(data);
} catch (const std::exception& e) {
auto remote_s = str_for_endpoint(c->r.get_socket().remote_endpoint());
this->log.info_f("Failed to send WebSocket message to {}: {}", remote_s, e.what());
}
if (--expected_results == 0) {
complete_promise.set_value();
}
co_return;
};
for (const auto& c : subscribers) {
asio::co_spawn(co_await asio::this_coro::executor, fn(c), asio::detached);
}
co_await complete_promise.get();
}
co_return;
}
std::shared_ptr<phosg::JSON> HTTPServer::generate_server_version() const {
return make_shared<phosg::JSON>(phosg::JSON::dict({
{"ServerType", "newserv"}, {"ServerType", "newserv"},
{"BuildTime", BUILD_TIMESTAMP}, {"BuildTime", BUILD_TIMESTAMP},
{"BuildTimeStr", phosg::format_time(BUILD_TIMESTAMP)}, {"BuildTimeStr", phosg::format_time(BUILD_TIMESTAMP)},
{"Revision", GIT_REVISION_HASH}, {"Revision", GIT_REVISION_HASH},
})); });
} };
std::shared_ptr<phosg::JSON> HTTPServer::generate_account_json(shared_ptr<const Account> a) const { this->router.add(HTTPRequest::Method::GET, "/", [generate_server_version_json](ArgsT&&) -> RetT {
auto dc_nte_licenses_json = phosg::JSON::list(); co_return make_shared<phosg::JSON>(generate_server_version_json());
for (const auto& it : a->dc_nte_licenses) { });
dc_nte_licenses_json.emplace_back(it.first);
}
auto dc_licenses_json = phosg::JSON::list();
for (const auto& it : a->dc_licenses) {
dc_licenses_json.emplace_back(it.first);
}
auto pc_licenses_json = phosg::JSON::list();
for (const auto& it : a->pc_licenses) {
pc_licenses_json.emplace_back(it.first);
}
auto gc_licenses_json = phosg::JSON::list();
for (const auto& it : a->gc_licenses) {
gc_licenses_json.emplace_back(it.first);
}
auto xb_licenses_json = phosg::JSON::list();
for (const auto& it : a->xb_licenses) {
xb_licenses_json.emplace_back(it.first);
}
auto bb_licenses_json = phosg::JSON::list();
for (const auto& it : a->bb_licenses) {
bb_licenses_json.emplace_back(it.first);
}
auto auto_patches_json = phosg::JSON::list();
for (const auto& it : a->auto_patches_enabled) {
auto_patches_json.emplace_back(it);
}
return make_shared<phosg::JSON>(phosg::JSON::dict({
{"AccountID", a->account_id},
{"Flags", a->flags},
{"BanEndTime", a->ban_end_time ? a->ban_end_time : phosg::JSON(nullptr)},
{"Ep3CurrentMeseta", a->ep3_current_meseta},
{"Ep3TotalMesetaEarned", a->ep3_total_meseta_earned},
{"BBTeamID", a->bb_team_id},
{"LastPlayerName", a->last_player_name},
{"AutoReplyMessage", a->auto_reply_message},
{"IsTemporary", a->is_temporary},
{"DCNTELicenses", std::move(dc_nte_licenses_json)},
{"DCLicenses", std::move(dc_licenses_json)},
{"PCLicenses", std::move(pc_licenses_json)},
{"GCLicenses", std::move(gc_licenses_json)},
{"XBLicenses", std::move(xb_licenses_json)},
{"BBLicenses", std::move(bb_licenses_json)},
{"AutoPatchesEnabled", std::move(auto_patches_json)},
}));
};
std::shared_ptr<phosg::JSON> HTTPServer::generate_client_json( this->router.add(HTTPRequest::Method::POST, "/y/shell-exec", [this](ArgsT&& args) -> RetT {
shared_ptr<const Client> c, shared_ptr<const ItemNameIndex> item_name_index) const { auto command = args.post_data.get_string("command");
auto s = c->require_server_state(); try {
auto dispatch_res = co_await ShellCommand::dispatch_str(this->state, command);
co_return make_shared<phosg::JSON>(phosg::JSON::dict({{"result", phosg::join(dispatch_res, "\n")}}));
} catch (const exception& e) {
throw HTTPError(400, e.what());
}
});
this->router.add(HTTPRequest::Method::GET, "/y/rare-drops/stream", [this, generate_server_version_json](ArgsT&& args) -> RetT {
if (!(co_await this->enable_websockets(args.client, args.req))) {
throw HTTPError(400, "this path requires a websocket connection");
}
this->rare_drop_subscribers.emplace(args.client);
co_await args.client->send_websocket_message(generate_server_version_json().serialize());
co_return nullptr;
});
this->router.add(HTTPRequest::Method::GET, "/y/clients", [this](ArgsT&&) -> RetT {
auto res = make_shared<phosg::JSON>(phosg::JSON::list());
for (const auto& c : this->state->game_server->all_clients()) {
auto item_name_index = this->state->item_name_index_opt(c->version());
const char* drop_notifications_mode = "unknown"; const char* drop_notifications_mode = "unknown";
switch (c->get_drop_notification_mode()) { switch (c->get_drop_notification_mode()) {
@@ -126,8 +74,7 @@ std::shared_ptr<phosg::JSON> HTTPServer::generate_client_json(
drop_notifications_mode = "every"; drop_notifications_mode = "every";
break; break;
} }
auto client_json = phosg::JSON::dict({
auto ret = make_shared<phosg::JSON>(phosg::JSON::dict({
{"ID", c->id}, {"ID", c->id},
{"RemoteAddress", c->channel->default_name()}, {"RemoteAddress", c->channel->default_name()},
{"Version", phosg::name_for_enum(c->version())}, {"Version", phosg::name_for_enum(c->version())},
@@ -154,34 +101,33 @@ std::shared_ptr<phosg::JSON> HTTPServer::generate_client_json(
{"OverrideSectionID", ((c->override_section_id != 0xFF) ? c->override_section_id : phosg::JSON(nullptr))}, {"OverrideSectionID", ((c->override_section_id != 0xFF) ? c->override_section_id : phosg::JSON(nullptr))},
{"OverrideLobbyEvent", ((c->override_lobby_event != 0xFF) ? c->override_lobby_event : phosg::JSON(nullptr))}, {"OverrideLobbyEvent", ((c->override_lobby_event != 0xFF) ? c->override_lobby_event : phosg::JSON(nullptr))},
{"OverrideLobbyNumber", ((c->override_lobby_number != 0x80) ? c->override_lobby_number : phosg::JSON(nullptr))}, {"OverrideLobbyNumber", ((c->override_lobby_number != 0x80) ? c->override_lobby_number : phosg::JSON(nullptr))},
})); });
if (c->login) { if (c->login) {
auto acc_json = HTTPServer::generate_account_json(c->login->account); client_json.emplace("Account", c->login->account->json());
ret->emplace("Account", std::move(*acc_json));
} else { } else {
ret->emplace("Account", phosg::JSON()); client_json.emplace("Account", phosg::JSON());
} }
auto l = c->lobby.lock(); auto l = c->lobby.lock();
if (l) { if (l) {
ret->emplace("LobbyID", l->lobby_id); client_json.emplace("LobbyID", l->lobby_id);
ret->emplace("LobbyClientID", c->lobby_client_id); client_json.emplace("LobbyClientID", c->lobby_client_id);
} }
if (c->version() == Version::BB_V4) { if (c->version() == Version::BB_V4) {
ret->emplace("BBCharacterIndex", c->bb_character_index); client_json.emplace("BBCharacterIndex", c->bb_character_index);
} }
auto p = c->character_file(false, false); auto p = c->character_file(false, false);
if (p) { if (p) {
if (!is_ep3(c->version())) { if (!is_ep3(c->version())) {
if (c->version() != Version::DC_NTE) { if (c->version() != Version::DC_NTE) {
ret->emplace("InventoryLanguage", name_for_language(p->inventory.language)); client_json.emplace("InventoryLanguage", name_for_language(p->inventory.language));
ret->emplace("NumHPMaterialsUsed", p->get_material_usage(PSOBBCharacterFile::MaterialType::HP)); client_json.emplace("NumHPMaterialsUsed", p->get_material_usage(PSOBBCharacterFile::MaterialType::HP));
ret->emplace("NumTPMaterialsUsed", p->get_material_usage(PSOBBCharacterFile::MaterialType::TP)); client_json.emplace("NumTPMaterialsUsed", p->get_material_usage(PSOBBCharacterFile::MaterialType::TP));
if (!is_v1_or_v2(c->version())) { if (!is_v1_or_v2(c->version())) {
ret->emplace("NumPowerMaterialsUsed", p->get_material_usage(PSOBBCharacterFile::MaterialType::POWER)); client_json.emplace("NumPowerMaterialsUsed", p->get_material_usage(PSOBBCharacterFile::MaterialType::POWER));
ret->emplace("NumDefMaterialsUsed", p->get_material_usage(PSOBBCharacterFile::MaterialType::DEF)); client_json.emplace("NumDefMaterialsUsed", p->get_material_usage(PSOBBCharacterFile::MaterialType::DEF));
ret->emplace("NumMindMaterialsUsed", p->get_material_usage(PSOBBCharacterFile::MaterialType::MIND)); client_json.emplace("NumMindMaterialsUsed", p->get_material_usage(PSOBBCharacterFile::MaterialType::MIND));
ret->emplace("NumEvadeMaterialsUsed", p->get_material_usage(PSOBBCharacterFile::MaterialType::EVADE)); client_json.emplace("NumEvadeMaterialsUsed", p->get_material_usage(PSOBBCharacterFile::MaterialType::EVADE));
ret->emplace("NumLuckMaterialsUsed", p->get_material_usage(PSOBBCharacterFile::MaterialType::LUCK)); client_json.emplace("NumLuckMaterialsUsed", p->get_material_usage(PSOBBCharacterFile::MaterialType::LUCK));
} }
} }
phosg::JSON items_json = phosg::JSON::list(); phosg::JSON items_json = phosg::JSON::list();
@@ -197,53 +143,53 @@ std::shared_ptr<phosg::JSON> HTTPServer::generate_client_json(
} }
items_json.emplace_back(std::move(item_dict)); items_json.emplace_back(std::move(item_dict));
} }
ret->emplace("InventoryItems", std::move(items_json)); client_json.emplace("InventoryItems", std::move(items_json));
ret->emplace("ATP", p->disp.stats.char_stats.atp.load()); client_json.emplace("ATP", p->disp.stats.char_stats.atp.load());
ret->emplace("MST", p->disp.stats.char_stats.mst.load()); client_json.emplace("MST", p->disp.stats.char_stats.mst.load());
ret->emplace("EVP", p->disp.stats.char_stats.evp.load()); client_json.emplace("EVP", p->disp.stats.char_stats.evp.load());
ret->emplace("HP", p->disp.stats.char_stats.hp.load()); client_json.emplace("HP", p->disp.stats.char_stats.hp.load());
ret->emplace("DFP", p->disp.stats.char_stats.dfp.load()); client_json.emplace("DFP", p->disp.stats.char_stats.dfp.load());
ret->emplace("ATA", p->disp.stats.char_stats.ata.load()); client_json.emplace("ATA", p->disp.stats.char_stats.ata.load());
ret->emplace("LCK", p->disp.stats.char_stats.lck.load()); client_json.emplace("LCK", p->disp.stats.char_stats.lck.load());
ret->emplace("EXP", p->disp.stats.experience.load()); client_json.emplace("EXP", p->disp.stats.experience.load());
ret->emplace("Meseta", p->disp.stats.meseta.load()); client_json.emplace("Meseta", p->disp.stats.meseta.load());
auto tech_levels_json = phosg::JSON::dict(); auto tech_levels_json = phosg::JSON::dict();
for (size_t z = 0; z < 0x13; z++) { for (size_t z = 0; z < 0x13; z++) {
auto level = p->get_technique_level(z); auto level = p->get_technique_level(z);
tech_levels_json.emplace(name_for_technique(z), (level != 0xFF) ? (level + 1) : phosg::JSON(nullptr)); tech_levels_json.emplace(name_for_technique(z), (level != 0xFF) ? (level + 1) : phosg::JSON(nullptr));
} }
ret->emplace("TechniqueLevels", std::move(tech_levels_json)); client_json.emplace("TechniqueLevels", std::move(tech_levels_json));
} }
ret->emplace("Height", p->disp.stats.height.load()); client_json.emplace("Height", p->disp.stats.height.load());
ret->emplace("Level", p->disp.stats.level.load() + 1); client_json.emplace("Level", p->disp.stats.level.load() + 1);
ret->emplace("NameColor", p->disp.visual.name_color.load()); client_json.emplace("NameColor", p->disp.visual.name_color.load());
ret->emplace("ExtraModel", (p->disp.visual.validation_flags & 2) ? p->disp.visual.extra_model : phosg::JSON(nullptr)); client_json.emplace("ExtraModel", (p->disp.visual.validation_flags & 2) ? p->disp.visual.extra_model : phosg::JSON(nullptr));
ret->emplace("SectionID", name_for_section_id(p->disp.visual.section_id)); client_json.emplace("SectionID", name_for_section_id(p->disp.visual.section_id));
ret->emplace("CharClass", name_for_char_class(p->disp.visual.char_class)); client_json.emplace("CharClass", name_for_char_class(p->disp.visual.char_class));
ret->emplace("Costume", p->disp.visual.costume.load()); client_json.emplace("Costume", p->disp.visual.costume.load());
ret->emplace("Skin", p->disp.visual.skin.load()); client_json.emplace("Skin", p->disp.visual.skin.load());
ret->emplace("Face", p->disp.visual.face.load()); client_json.emplace("Face", p->disp.visual.face.load());
ret->emplace("Head", p->disp.visual.head.load()); client_json.emplace("Head", p->disp.visual.head.load());
ret->emplace("Hair", p->disp.visual.hair.load()); client_json.emplace("Hair", p->disp.visual.hair.load());
ret->emplace("HairR", p->disp.visual.hair_r.load()); client_json.emplace("HairR", p->disp.visual.hair_r.load());
ret->emplace("HairG", p->disp.visual.hair_g.load()); client_json.emplace("HairG", p->disp.visual.hair_g.load());
ret->emplace("HairB", p->disp.visual.hair_b.load()); client_json.emplace("HairB", p->disp.visual.hair_b.load());
ret->emplace("ProportionX", p->disp.visual.proportion_x.load()); client_json.emplace("ProportionX", p->disp.visual.proportion_x.load());
ret->emplace("ProportionY", p->disp.visual.proportion_y.load()); client_json.emplace("ProportionY", p->disp.visual.proportion_y.load());
ret->emplace("Name", p->disp.name.decode(c->language())); client_json.emplace("Name", p->disp.name.decode(c->language()));
ret->emplace("PlayTimeSeconds", p->play_time_seconds.load()); client_json.emplace("PlayTimeSeconds", p->play_time_seconds.load());
ret->emplace("AutoReply", p->auto_reply.decode(c->language())); client_json.emplace("AutoReply", p->auto_reply.decode(c->language()));
ret->emplace("InfoBoard", p->info_board.decode(c->language())); client_json.emplace("InfoBoard", p->info_board.decode(c->language()));
auto battle_place_counts = phosg::JSON::list({ auto battle_place_counts = phosg::JSON::list({
p->battle_records.place_counts[0].load(), p->battle_records.place_counts[0].load(),
p->battle_records.place_counts[1].load(), p->battle_records.place_counts[1].load(),
p->battle_records.place_counts[2].load(), p->battle_records.place_counts[2].load(),
p->battle_records.place_counts[3].load(), p->battle_records.place_counts[3].load(),
}); });
ret->emplace("BattlePlaceCounts", std::move(battle_place_counts)); client_json.emplace("BattlePlaceCounts", std::move(battle_place_counts));
ret->emplace("BattleDisconnectCount", p->battle_records.disconnect_count.load()); client_json.emplace("BattleDisconnectCount", p->battle_records.disconnect_count.load());
if (!is_ep3(c->version())) { if (!is_ep3(c->version())) {
auto json_for_challenge_times = []<size_t Count>(const parray<ChallengeTime, Count>& times) -> phosg::JSON { auto json_for_challenge_times = []<size_t Count>(const parray<ChallengeTime, Count>& times) -> phosg::JSON {
@@ -253,21 +199,21 @@ std::shared_ptr<phosg::JSON> HTTPServer::generate_client_json(
} }
return times_json; return times_json;
}; };
ret->emplace("ChallengeTitleColorXRGB1555", p->challenge_records.title_color.load()); client_json.emplace("ChallengeTitleColorXRGB1555", p->challenge_records.title_color.load());
ret->emplace("ChallengeTimesEp1Online", json_for_challenge_times(p->challenge_records.times_ep1_online)); client_json.emplace("ChallengeTimesEp1Online", json_for_challenge_times(p->challenge_records.times_ep1_online));
ret->emplace("ChallengeTimesEp2Online", json_for_challenge_times(p->challenge_records.times_ep2_online)); client_json.emplace("ChallengeTimesEp2Online", json_for_challenge_times(p->challenge_records.times_ep2_online));
ret->emplace("ChallengeTimesEp1Offline", json_for_challenge_times(p->challenge_records.times_ep1_offline)); client_json.emplace("ChallengeTimesEp1Offline", json_for_challenge_times(p->challenge_records.times_ep1_offline));
ret->emplace("ChallengeGraveIsEp2", p->challenge_records.grave_is_ep2 ? true : false); client_json.emplace("ChallengeGraveIsEp2", p->challenge_records.grave_is_ep2 ? true : false);
ret->emplace("ChallengeGraveStageNum", p->challenge_records.grave_stage_num); client_json.emplace("ChallengeGraveStageNum", p->challenge_records.grave_stage_num);
ret->emplace("ChallengeGraveFloor", p->challenge_records.grave_floor); client_json.emplace("ChallengeGraveFloor", p->challenge_records.grave_floor);
ret->emplace("ChallengeGraveDeaths", p->challenge_records.grave_deaths.load()); client_json.emplace("ChallengeGraveDeaths", p->challenge_records.grave_deaths.load());
{ {
uint16_t year = 2000 + ((p->challenge_records.grave_time >> 28) & 0x0F); uint16_t year = 2000 + ((p->challenge_records.grave_time >> 28) & 0x0F);
uint8_t month = (p->challenge_records.grave_time >> 24) & 0x0F; uint8_t month = (p->challenge_records.grave_time >> 24) & 0x0F;
uint8_t day = (p->challenge_records.grave_time >> 16) & 0xFF; uint8_t day = (p->challenge_records.grave_time >> 16) & 0xFF;
uint8_t hour = (p->challenge_records.grave_time >> 8) & 0xFF; uint8_t hour = (p->challenge_records.grave_time >> 8) & 0xFF;
uint8_t minute = p->challenge_records.grave_time & 0xFF; uint8_t minute = p->challenge_records.grave_time & 0xFF;
ret->emplace("ChallengeGraveTime", std::format("{:04}-{:02}-{:02} {:02}:{:02}:00", year, month, day, hour, minute)); client_json.emplace("ChallengeGraveTime", std::format("{:04}-{:02}-{:02} {:02}:{:02}:00", year, month, day, hour, minute));
} }
string grave_enemy_types; string grave_enemy_types;
if (p->challenge_records.grave_defeated_by_enemy_rt_index) { if (p->challenge_records.grave_defeated_by_enemy_rt_index) {
@@ -278,19 +224,19 @@ std::shared_ptr<phosg::JSON> HTTPServer::generate_client_json(
grave_enemy_types += phosg::name_for_enum(type); grave_enemy_types += phosg::name_for_enum(type);
} }
} }
ret->emplace("ChallengeGraveDefeatedByEnemy", std::move(grave_enemy_types)); client_json.emplace("ChallengeGraveDefeatedByEnemy", std::move(grave_enemy_types));
ret->emplace("ChallengeGraveX", p->challenge_records.grave_x.load()); client_json.emplace("ChallengeGraveX", p->challenge_records.grave_x.load());
ret->emplace("ChallengeGraveY", p->challenge_records.grave_y.load()); client_json.emplace("ChallengeGraveY", p->challenge_records.grave_y.load());
ret->emplace("ChallengeGraveZ", p->challenge_records.grave_z.load()); client_json.emplace("ChallengeGraveZ", p->challenge_records.grave_z.load());
ret->emplace("ChallengeGraveTeam", p->challenge_records.grave_team.decode()); client_json.emplace("ChallengeGraveTeam", p->challenge_records.grave_team.decode());
ret->emplace("ChallengeGraveMessage", p->challenge_records.grave_message.decode()); client_json.emplace("ChallengeGraveMessage", p->challenge_records.grave_message.decode());
ret->emplace("ChallengeAwardStateEp1OnlineFlags", p->challenge_records.ep1_online_award_state.rank_award_flags.load()); client_json.emplace("ChallengeAwardStateEp1OnlineFlags", p->challenge_records.ep1_online_award_state.rank_award_flags.load());
ret->emplace("ChallengeAwardStateEp1OnlineMaxRank", p->challenge_records.ep1_online_award_state.maximum_rank.decode()); client_json.emplace("ChallengeAwardStateEp1OnlineMaxRank", p->challenge_records.ep1_online_award_state.maximum_rank.decode());
ret->emplace("ChallengeAwardStateEp2OnlineFlags", p->challenge_records.ep2_online_award_state.rank_award_flags.load()); client_json.emplace("ChallengeAwardStateEp2OnlineFlags", p->challenge_records.ep2_online_award_state.rank_award_flags.load());
ret->emplace("ChallengeAwardStateEp2OnlineMaxRank", p->challenge_records.ep2_online_award_state.maximum_rank.decode()); client_json.emplace("ChallengeAwardStateEp2OnlineMaxRank", p->challenge_records.ep2_online_award_state.maximum_rank.decode());
ret->emplace("ChallengeAwardStateEp1OfflineFlags", p->challenge_records.ep1_offline_award_state.rank_award_flags.load()); client_json.emplace("ChallengeAwardStateEp1OfflineFlags", p->challenge_records.ep1_offline_award_state.rank_award_flags.load());
ret->emplace("ChallengeAwardStateEp1OfflineMaxRank", p->challenge_records.ep1_offline_award_state.maximum_rank.decode()); client_json.emplace("ChallengeAwardStateEp1OfflineMaxRank", p->challenge_records.ep1_offline_award_state.maximum_rank.decode());
ret->emplace("ChallengeRankTitle", p->challenge_records.rank_title.decode()); client_json.emplace("ChallengeRankTitle", p->challenge_records.rank_title.decode());
} }
} }
auto ses = c->proxy_session; auto ses = c->proxy_session;
@@ -338,23 +284,29 @@ std::shared_ptr<phosg::JSON> HTTPServer::generate_client_json(
ses_json.emplace("DropMode", "proxy"); ses_json.emplace("DropMode", "proxy");
break; break;
} }
ret->emplace("ProxySession", std::move(ses_json)); client_json.emplace("ProxySession", std::move(ses_json));
} else { } else {
ret->emplace("ProxySession", phosg::JSON()); client_json.emplace("ProxySession", phosg::JSON());
} }
return ret;
}
std::shared_ptr<phosg::JSON> HTTPServer::generate_lobby_json( res->emplace_back(std::move(client_json));
shared_ptr<const Lobby> l, shared_ptr<const ItemNameIndex> item_name_index) const { }
std::array<std::shared_ptr<Client>, 12> clients; co_return res;
});
this->router.add(HTTPRequest::Method::GET, "/y/lobbies", [this](ArgsT&&) -> RetT {
auto res = make_shared<phosg::JSON>(phosg::JSON::list());
for (const auto& [_, l] : this->state->id_to_lobby) {
auto leader = l->clients[l->leader_id];
Version v = leader ? leader->version() : Version::BB_V4;
auto item_name_index = this->state->item_name_index_opt(v);
auto client_ids_json = phosg::JSON::list(); auto client_ids_json = phosg::JSON::list();
for (size_t z = 0; z < l->max_clients; z++) { for (size_t z = 0; z < l->max_clients; z++) {
client_ids_json.emplace_back(l->clients[z] ? l->clients[z]->id : phosg::JSON(nullptr)); client_ids_json.emplace_back(l->clients[z] ? l->clients[z]->id : phosg::JSON(nullptr));
} }
auto ret = make_shared<phosg::JSON>(phosg::JSON::dict({ auto lobby_json = phosg::JSON::dict({
{"ID", l->lobby_id}, {"ID", l->lobby_id},
{"AllowedVersions", l->allowed_versions}, {"AllowedVersions", l->allowed_versions},
{"Event", l->event}, {"Event", l->event},
@@ -364,61 +316,61 @@ std::shared_ptr<phosg::JSON> HTTPServer::generate_lobby_json(
{"ClientIDs", std::move(client_ids_json)}, {"ClientIDs", std::move(client_ids_json)},
{"IsGame", l->is_game()}, {"IsGame", l->is_game()},
{"IsPersistent", l->check_flag(Lobby::Flag::PERSISTENT)}, {"IsPersistent", l->check_flag(Lobby::Flag::PERSISTENT)},
})); });
if (l->is_game()) { if (l->is_game()) {
ret->emplace("CheatsEnabled", l->check_flag(Lobby::Flag::CHEATS_ENABLED)); lobby_json.emplace("CheatsEnabled", l->check_flag(Lobby::Flag::CHEATS_ENABLED));
ret->emplace("MinLevel", l->min_level + 1); lobby_json.emplace("MinLevel", l->min_level + 1);
ret->emplace("MaxLevel", l->max_level + 1); lobby_json.emplace("MaxLevel", l->max_level + 1);
ret->emplace("Episode", name_for_episode(l->episode)); lobby_json.emplace("Episode", name_for_episode(l->episode));
ret->emplace("HasPassword", !l->password.empty()); lobby_json.emplace("HasPassword", !l->password.empty());
ret->emplace("Name", l->name); lobby_json.emplace("Name", l->name);
ret->emplace("RandomSeed", l->random_seed); lobby_json.emplace("RandomSeed", l->random_seed);
if (l->episode != Episode::EP3) { if (l->episode != Episode::EP3) {
ret->emplace("QuestSelectionInProgress", l->check_flag(Lobby::Flag::QUEST_SELECTION_IN_PROGRESS)); lobby_json.emplace("QuestSelectionInProgress", l->check_flag(Lobby::Flag::QUEST_SELECTION_IN_PROGRESS));
ret->emplace("QuestInProgress", l->check_flag(Lobby::Flag::QUEST_IN_PROGRESS)); lobby_json.emplace("QuestInProgress", l->check_flag(Lobby::Flag::QUEST_IN_PROGRESS));
ret->emplace("JoinableQuestInProgress", l->check_flag(Lobby::Flag::JOINABLE_QUEST_IN_PROGRESS)); lobby_json.emplace("JoinableQuestInProgress", l->check_flag(Lobby::Flag::JOINABLE_QUEST_IN_PROGRESS));
ret->emplace("Variations", l->variations.json()); lobby_json.emplace("Variations", l->variations.json());
uint8_t effective_section_id = l->effective_section_id(); uint8_t effective_section_id = l->effective_section_id();
if (effective_section_id < 10) { if (effective_section_id < 10) {
ret->emplace("SectionID", name_for_section_id(effective_section_id)); lobby_json.emplace("SectionID", name_for_section_id(effective_section_id));
} else { } else {
ret->emplace("SectionID", nullptr); lobby_json.emplace("SectionID", nullptr);
} }
ret->emplace("Mode", name_for_mode(l->mode)); lobby_json.emplace("Mode", name_for_mode(l->mode));
ret->emplace("Difficulty", name_for_difficulty(l->difficulty)); lobby_json.emplace("Difficulty", name_for_difficulty(l->difficulty));
ret->emplace("BaseEXPMultiplier", l->base_exp_multiplier); lobby_json.emplace("BaseEXPMultiplier", l->base_exp_multiplier);
ret->emplace("EXPShareMultiplier", l->exp_share_multiplier); lobby_json.emplace("EXPShareMultiplier", l->exp_share_multiplier);
ret->emplace("AllowedDropModes", l->allowed_drop_modes); lobby_json.emplace("AllowedDropModes", l->allowed_drop_modes);
switch (l->drop_mode) { switch (l->drop_mode) {
case ServerDropMode::DISABLED: case ServerDropMode::DISABLED:
ret->emplace("DropMode", "none"); lobby_json.emplace("DropMode", "none");
break; break;
case ServerDropMode::CLIENT: case ServerDropMode::CLIENT:
ret->emplace("DropMode", "client"); lobby_json.emplace("DropMode", "client");
break; break;
case ServerDropMode::SERVER_SHARED: case ServerDropMode::SERVER_SHARED:
ret->emplace("DropMode", "shared"); lobby_json.emplace("DropMode", "shared");
break; break;
case ServerDropMode::SERVER_PRIVATE: case ServerDropMode::SERVER_PRIVATE:
ret->emplace("DropMode", "private"); lobby_json.emplace("DropMode", "private");
break; break;
case ServerDropMode::SERVER_DUPLICATE: case ServerDropMode::SERVER_DUPLICATE:
ret->emplace("DropMode", "duplicate"); lobby_json.emplace("DropMode", "duplicate");
break; break;
} }
if (l->mode == GameMode::CHALLENGE) { if (l->mode == GameMode::CHALLENGE) {
ret->emplace("ChallengeEXPMultiplier", l->challenge_exp_multiplier); lobby_json.emplace("ChallengeEXPMultiplier", l->challenge_exp_multiplier);
if (l->challenge_params) { if (l->challenge_params) {
ret->emplace("ChallengeStageNumber", l->challenge_params->stage_number); lobby_json.emplace("ChallengeStageNumber", l->challenge_params->stage_number);
ret->emplace("ChallengeRankColor", l->challenge_params->rank_color); lobby_json.emplace("ChallengeRankColor", l->challenge_params->rank_color);
ret->emplace("ChallengeRankText", l->challenge_params->rank_text); lobby_json.emplace("ChallengeRankText", l->challenge_params->rank_text);
ret->emplace("ChallengeRank0ThresholdBitmask", l->challenge_params->rank_thresholds[0].bitmask); lobby_json.emplace("ChallengeRank0ThresholdBitmask", l->challenge_params->rank_thresholds[0].bitmask);
ret->emplace("ChallengeRank0ThresholdSeconds", l->challenge_params->rank_thresholds[0].seconds); lobby_json.emplace("ChallengeRank0ThresholdSeconds", l->challenge_params->rank_thresholds[0].seconds);
ret->emplace("ChallengeRank1ThresholdBitmask", l->challenge_params->rank_thresholds[1].bitmask); lobby_json.emplace("ChallengeRank1ThresholdBitmask", l->challenge_params->rank_thresholds[1].bitmask);
ret->emplace("ChallengeRank1ThresholdSeconds", l->challenge_params->rank_thresholds[1].seconds); lobby_json.emplace("ChallengeRank1ThresholdSeconds", l->challenge_params->rank_thresholds[1].seconds);
ret->emplace("ChallengeRank2ThresholdBitmask", l->challenge_params->rank_thresholds[2].bitmask); lobby_json.emplace("ChallengeRank2ThresholdBitmask", l->challenge_params->rank_thresholds[2].bitmask);
ret->emplace("ChallengeRank2ThresholdSeconds", l->challenge_params->rank_thresholds[2].seconds); lobby_json.emplace("ChallengeRank2ThresholdSeconds", l->challenge_params->rank_thresholds[2].seconds);
} }
} }
@@ -441,13 +393,13 @@ std::shared_ptr<phosg::JSON> HTTPServer::generate_lobby_json(
floor_items_json.emplace_back(std::move(item_dict)); floor_items_json.emplace_back(std::move(item_dict));
} }
} }
ret->emplace("FloorItems", std::move(floor_items_json)); lobby_json.emplace("FloorItems", std::move(floor_items_json));
ret->emplace("Quest", l->quest ? l->quest->json() : phosg::JSON(nullptr)); lobby_json.emplace("Quest", l->quest ? l->quest->json() : phosg::JSON(nullptr));
} else { } else {
ret->emplace("BattleInProgress", l->check_flag(Lobby::Flag::BATTLE_IN_PROGRESS)); lobby_json.emplace("BattleInProgress", l->check_flag(Lobby::Flag::BATTLE_IN_PROGRESS));
ret->emplace("IsSpectatorTeam", l->check_flag(Lobby::Flag::IS_SPECTATOR_TEAM)); lobby_json.emplace("IsSpectatorTeam", l->check_flag(Lobby::Flag::IS_SPECTATOR_TEAM));
ret->emplace("SpectatorsForbidden", l->check_flag(Lobby::Flag::SPECTATORS_FORBIDDEN)); lobby_json.emplace("SpectatorsForbidden", l->check_flag(Lobby::Flag::SPECTATORS_FORBIDDEN));
auto ep3s = l->ep3_server; auto ep3s = l->ep3_server;
if (ep3s) { if (ep3s) {
@@ -521,49 +473,82 @@ std::shared_ptr<phosg::JSON> HTTPServer::generate_lobby_json(
}); });
// std::shared_ptr<StateFlags> state_flags; // std::shared_ptr<StateFlags> state_flags;
// std::array<std::shared_ptr<PlayerState>, 4> player_states; // std::array<std::shared_ptr<PlayerState>, 4> player_states;
ret->emplace("Episode3BattleState", std::move(battle_state_json)); lobby_json.emplace("Episode3BattleState", std::move(battle_state_json));
} else { } else {
ret->emplace("Episode3BattleState", nullptr); lobby_json.emplace("Episode3BattleState", nullptr);
} }
auto watched_lobby = l->watched_lobby.lock(); auto watched_lobby = l->watched_lobby.lock();
if (watched_lobby) { if (watched_lobby) {
ret->emplace("WatchedLobbyID", watched_lobby->lobby_id); lobby_json.emplace("WatchedLobbyID", watched_lobby->lobby_id);
} }
auto watcher_lobby_ids_json = phosg::JSON::list(); auto watcher_lobby_ids_json = phosg::JSON::list();
for (const auto& watcher_lobby : l->watcher_lobbies) { for (const auto& watcher_lobby : l->watcher_lobbies) {
watcher_lobby_ids_json.emplace_back(watcher_lobby->lobby_id); watcher_lobby_ids_json.emplace_back(watcher_lobby->lobby_id);
} }
ret->emplace("WatcherLobbyIDs", std::move(watcher_lobby_ids_json)); lobby_json.emplace("WatcherLobbyIDs", std::move(watcher_lobby_ids_json));
ret->emplace("IsReplayLobby", !!l->battle_player); lobby_json.emplace("IsReplayLobby", !!l->battle_player);
} }
} else { // Not game } else { // Not game
ret->emplace("IsPublic", l->check_flag(Lobby::Flag::PUBLIC)); lobby_json.emplace("IsPublic", l->check_flag(Lobby::Flag::PUBLIC));
ret->emplace("IsDefault", l->check_flag(Lobby::Flag::DEFAULT)); lobby_json.emplace("IsDefault", l->check_flag(Lobby::Flag::DEFAULT));
ret->emplace("IsOverflow", l->check_flag(Lobby::Flag::IS_OVERFLOW)); lobby_json.emplace("IsOverflow", l->check_flag(Lobby::Flag::IS_OVERFLOW));
ret->emplace("Block", l->block); lobby_json.emplace("Block", l->block);
} }
return ret;
}
std::shared_ptr<phosg::JSON> HTTPServer::generate_accounts_json() const { res->emplace_back(std::move(lobby_json));
}
co_return res;
});
this->router.add(HTTPRequest::Method::GET, "/y/accounts", [this](ArgsT&&) -> RetT {
auto res = make_shared<phosg::JSON>(phosg::JSON::list()); auto res = make_shared<phosg::JSON>(phosg::JSON::list());
for (const auto& it : this->state->account_index->all()) { for (const auto& it : this->state->account_index->all()) {
res->emplace_back(it->json()); res->emplace_back(it->json());
} }
return res; co_return res;
} });
std::shared_ptr<phosg::JSON> HTTPServer::generate_clients_json() const { this->router.add(HTTPRequest::Method::GET, "/y/account/:account_id", [this](ArgsT&& args) -> RetT {
auto res = make_shared<phosg::JSON>(phosg::JSON::list()); uint32_t account_id = args.get_param<uint32_t>("account_id");
for (const auto& it : this->state->game_server->all_clients()) { try {
auto client_json = this->generate_client_json(it, this->state->item_name_index_opt(it->version())); co_return make_shared<phosg::JSON>(this->state->account_index->from_account_id(account_id)->json());
res->emplace_back(std::move(*client_json)); } catch (const AccountIndex::missing_account&) {
throw HTTPError(404, "Account does not exist");
} }
return res; });
}
std::shared_ptr<phosg::JSON> HTTPServer::generate_server_info_json() const { this->router.add(HTTPRequest::Method::GET, "/y/teams", [this](ArgsT&& params) -> RetT {
auto res = make_shared<phosg::JSON>(phosg::JSON::dict());
for (const auto& it : this->state->team_index->all()) {
res->emplace(std::format("{}", it->team_id), it->json());
}
co_return res;
});
this->router.add(HTTPRequest::Method::GET, "/y/team/:team_id", [this](ArgsT&& args) -> RetT {
uint32_t team_id = args.get_param<uint32_t>("team_id");
auto team = this->state->team_index->get_by_id(team_id);
if (!team) {
throw HTTPError(404, "Team does not exist");
}
co_return make_shared<phosg::JSON>(team->json());
});
this->router.add(HTTPRequest::Method::GET, "/y/team/:team_id/flag", [this](ArgsT&& args) -> RetT {
uint32_t team_id = args.get_param<uint32_t>("team_id");
auto team = this->state->team_index->get_by_id(team_id);
if (!team) {
throw HTTPError(404, "Team does not exist");
}
if (!team->flag_data) {
throw HTTPError(404, "Team does not have a flag");
}
auto img = team->decode_flag_data();
co_return RawResponse{.content_type = "image/png", .data = img.serialize(phosg::ImageFormat::PNG)};
});
std::function<phosg::JSON()> generate_server_info_json = [this]() -> phosg::JSON {
size_t game_count = 0; size_t game_count = 0;
size_t lobby_count = 0; size_t lobby_count = 0;
for (const auto& it : this->state->id_to_lobby) { for (const auto& it : this->state->id_to_lobby) {
@@ -574,7 +559,7 @@ std::shared_ptr<phosg::JSON> HTTPServer::generate_server_info_json() const {
} }
} }
uint64_t uptime_usecs = phosg::now() - this->state->creation_time; uint64_t uptime_usecs = phosg::now() - this->state->creation_time;
return make_shared<phosg::JSON>(phosg::JSON::dict({ return phosg::JSON::dict({
{"StartTimeUsecs", this->state->creation_time}, {"StartTimeUsecs", this->state->creation_time},
{"StartTime", phosg::format_time(this->state->creation_time)}, {"StartTime", phosg::format_time(this->state->creation_time)},
{"UptimeUsecs", uptime_usecs}, {"UptimeUsecs", uptime_usecs},
@@ -584,21 +569,18 @@ std::shared_ptr<phosg::JSON> HTTPServer::generate_server_info_json() const {
{"ClientCount", this->state->game_server->all_clients().size() - ProxySession::num_proxy_sessions}, {"ClientCount", this->state->game_server->all_clients().size() - ProxySession::num_proxy_sessions},
{"ProxySessionCount", ProxySession::num_proxy_sessions}, {"ProxySessionCount", ProxySession::num_proxy_sessions},
{"ServerName", this->state->name}, {"ServerName", this->state->name},
})); });
} };
std::shared_ptr<phosg::JSON> HTTPServer::generate_lobbies_json() const { this->router.add(HTTPRequest::Method::GET, "/y/server", [generate_server_info_json](ArgsT&&) -> RetT {
auto res = make_shared<phosg::JSON>(phosg::JSON::list()); co_return make_shared<phosg::JSON>(generate_server_info_json());
for (const auto& it : this->state->id_to_lobby) { });
auto leader = it.second->clients[it.second->leader_id];
Version v = leader ? leader->version() : Version::BB_V4;
auto lobby_json = this->generate_lobby_json(it.second, this->state->item_name_index_opt(v));
res->emplace_back(std::move(*lobby_json));
}
return res;
}
std::shared_ptr<phosg::JSON> HTTPServer::generate_summary_json() const { this->router.add(HTTPRequest::Method::GET, "/y/config", [this](ArgsT&&) -> RetT {
co_return this->state->config_json;
});
this->router.add(HTTPRequest::Method::GET, "/y/summary", [this, generate_server_info_json](ArgsT&& args) -> RetT {
auto clients_json = phosg::JSON::list(); auto clients_json = phosg::JSON::list();
for (const auto& c : this->state->game_server->all_clients()) { for (const auto& c : this->state->game_server->all_clients()) {
auto p = c->character_file(false, false); auto p = c->character_file(false, false);
@@ -653,61 +635,62 @@ std::shared_ptr<phosg::JSON> HTTPServer::generate_summary_json() const {
} }
} }
auto server_json = this->generate_server_info_json(); co_return make_shared<phosg::JSON>(phosg::JSON::dict({
return make_shared<phosg::JSON>(phosg::JSON::dict({
{"Clients", std::move(clients_json)}, {"Clients", std::move(clients_json)},
{"Games", std::move(games_json)}, {"Games", std::move(games_json)},
{"Server", std::move(*server_json)}, {"Server", generate_server_info_json()},
})); }));
} });
std::shared_ptr<phosg::JSON> HTTPServer::generate_all_json() const { this->router.add(HTTPRequest::Method::GET, "/y/data/ep3/cards", [this](ArgsT&& args) -> RetT {
auto clients_json = this->generate_clients_json(); auto& index = args.req.query_params.count("trial") ? this->state->ep3_card_index_trial : this->state->ep3_card_index;
auto lobbies_json = this->generate_lobbies_json();
auto server_json = this->generate_server_info_json();
return make_shared<phosg::JSON>(phosg::JSON::dict({
{"Clients", std::move(*clients_json)},
{"Lobbies", std::move(*lobbies_json)},
{"Server", std::move(*server_json)},
}));
}
asio::awaitable<std::shared_ptr<phosg::JSON>> HTTPServer::generate_ep3_cards_json(bool trial) const {
auto& index = trial ? this->state->ep3_card_index_trial : this->state->ep3_card_index;
co_return co_await call_on_thread_pool(*this->state->thread_pool, [&]() -> shared_ptr<phosg::JSON> { co_return co_await call_on_thread_pool(*this->state->thread_pool, [&]() -> shared_ptr<phosg::JSON> {
return make_shared<phosg::JSON>(index->definitions_json()); return make_shared<phosg::JSON>(index->definitions_json());
}); });
} });
std::shared_ptr<phosg::JSON> HTTPServer::generate_common_table_list_json() const { this->router.add(HTTPRequest::Method::GET, "/y/data/ep3/card/:card_id", [this](ArgsT&& args) -> RetT {
auto& index = args.req.query_params.count("trial") ? this->state->ep3_card_index_trial : this->state->ep3_card_index;
uint32_t card_id = args.get_param<uint32_t>("card_id");
try {
co_return make_shared<phosg::JSON>(index->definition_for_id(card_id)->def.json());
} catch (const std::out_of_range&) {
throw HTTPError(404, "Card definition does not exist");
}
});
// TODO: Add /y/data/ep3/maps, /y/data/ep3/map/:map_number and /y/data/ep3/map/:map_number/raw
this->router.add(HTTPRequest::Method::GET, "/y/data/common-tables", [this](ArgsT&&) -> RetT {
auto ret = make_shared<phosg::JSON>(phosg::JSON::list()); auto ret = make_shared<phosg::JSON>(phosg::JSON::list());
for (const auto& it : this->state->common_item_sets) { for (const auto& it : this->state->common_item_sets) {
ret->emplace_back(it.first); ret->emplace_back(it.first);
} }
return ret; co_return ret;
} });
asio::awaitable<std::shared_ptr<phosg::JSON>> HTTPServer::generate_common_table_json(const std::string& table_name) const { this->router.add(HTTPRequest::Method::GET, "/y/data/common-table/:table_name", [this](ArgsT&& args) -> RetT {
try { try {
const auto& table = this->state->common_item_sets.at(table_name); const auto& table = this->state->common_item_sets.at(args.params.at("table_name"));
co_return co_await call_on_thread_pool(*this->state->thread_pool, [&]() -> shared_ptr<phosg::JSON> { co_return co_await call_on_thread_pool(*this->state->thread_pool, [&]() -> shared_ptr<phosg::JSON> {
return make_shared<phosg::JSON>(table->json()); return make_shared<phosg::JSON>(table->json());
}); });
} catch (const out_of_range&) { } catch (const out_of_range&) {
throw HTTPError(404, "Table does not exist"); throw HTTPError(404, "Table does not exist");
} }
} });
std::shared_ptr<phosg::JSON> HTTPServer::generate_rare_table_list_json() const { this->router.add(HTTPRequest::Method::GET, "/y/data/rare-tables", [this](ArgsT&&) -> RetT {
auto ret = make_shared<phosg::JSON>(phosg::JSON::list()); auto ret = make_shared<phosg::JSON>(phosg::JSON::list());
for (const auto& it : this->state->rare_item_sets) { for (const auto& it : this->state->rare_item_sets) {
ret->emplace_back(it.first); ret->emplace_back(it.first);
} }
return ret; co_return ret;
} });
asio::awaitable<std::shared_ptr<phosg::JSON>> HTTPServer::generate_rare_table_json(const std::string& table_name) const { this->router.add(HTTPRequest::Method::GET, "/y/data/rare-table/:table_name", [this](ArgsT&& args) -> RetT {
try { try {
const auto& table_name = args.params.at("table_name");
const auto& table = this->state->rare_item_sets.at(table_name); const auto& table = this->state->rare_item_sets.at(table_name);
shared_ptr<const ItemNameIndex> name_index; shared_ptr<const ItemNameIndex> name_index;
if (table_name.ends_with("-v1")) { if (table_name.ends_with("-v1")) {
@@ -725,39 +708,56 @@ asio::awaitable<std::shared_ptr<phosg::JSON>> HTTPServer::generate_rare_table_js
} catch (const out_of_range&) { } catch (const out_of_range&) {
throw HTTPError(404, "Table does not exist"); throw HTTPError(404, "Table does not exist");
} }
} });
asio::awaitable<std::shared_ptr<phosg::JSON>> HTTPServer::generate_quest_list_json() { this->router.add(HTTPRequest::Method::GET, "/y/data/quests", [this](ArgsT&& args) -> RetT {
co_return co_await call_on_thread_pool(*this->state->thread_pool, [&]() -> shared_ptr<phosg::JSON> { co_return co_await call_on_thread_pool(*this->state->thread_pool, [&]() -> shared_ptr<phosg::JSON> {
return make_shared<phosg::JSON>(this->state->quest_index->json()); return make_shared<phosg::JSON>(this->state->quest_index->json());
}); });
});
this->router.add(HTTPRequest::Method::GET, "/y/data/quest/:quest_num", [this](ArgsT&& args) -> RetT {
uint32_t quest_num = args.get_param<uint32_t>("quest_num");
auto q = this->state->quest_index->get(quest_num);
if (!q) {
throw HTTPError(404, "Quest does not exist");
}
co_return make_shared<phosg::JSON>(q->json());
});
} }
void HTTPServer::require_GET(const HTTPRequest& req) { asio::awaitable<void> HTTPServer::send_rare_drop_notification(shared_ptr<const phosg::JSON> message) {
if (req.method != HTTPRequest::Method::GET) { if (!this->rare_drop_subscribers.empty()) {
throw HTTPError(405, "GET method required for this endpoint"); string data = message->serialize();
}
}
phosg::JSON HTTPServer::require_POST(const HTTPRequest& req) { // Make a copy of the rare drop subscribers set, so we can guarantee that
if (req.method != HTTPRequest::Method::POST) { // the client objects are all valid until this coroutine returns
throw HTTPError(405, "POST method required for this endpoint"); unordered_set<shared_ptr<HTTPClient>> subscribers = this->rare_drop_subscribers;
}
auto* content_type = req.get_header("content-type");
if (!content_type || (*content_type != "application/json")) {
throw HTTPError(400, "POST requests must use the application/json content type");
}
size_t expected_results = subscribers.size();
AsyncPromise<void> complete_promise;
auto fn = [this, &data, &expected_results, &complete_promise](shared_ptr<HTTPClient> c) -> asio::awaitable<void> {
try { try {
return phosg::JSON::parse(req.data); co_await c->send_websocket_message(data);
} catch (const exception& e) { } catch (const std::exception& e) {
throw HTTPError(400, string("Invalid JSON: ") + e.what()); auto remote_s = str_for_endpoint(c->r.get_socket().remote_endpoint());
this->log.info_f("Failed to send WebSocket message to {}: {}", remote_s, e.what());
} }
if (--expected_results == 0) {
complete_promise.set_value();
}
co_return;
};
for (const auto& c : subscribers) {
asio::co_spawn(co_await asio::this_coro::executor, fn(c), asio::detached);
}
co_await complete_promise.get();
}
co_return;
} }
asio::awaitable<std::unique_ptr<HTTPResponse>> HTTPServer::handle_request(shared_ptr<HTTPClient> c, HTTPRequest&& req) { asio::awaitable<std::unique_ptr<HTTPResponse>> HTTPServer::handle_request(shared_ptr<HTTPClient> c, HTTPRequest&& req) {
shared_ptr<const phosg::JSON> ret; variant<RawResponse, shared_ptr<const phosg::JSON>> ret;
uint32_t serialize_options = phosg::JSON::SerializeOption::ESCAPE_CONTROLS_ONLY; uint32_t serialize_options = phosg::JSON::SerializeOption::ESCAPE_CONTROLS_ONLY;
uint64_t start_time = phosg::now(); uint64_t start_time = phosg::now();
@@ -766,7 +766,6 @@ asio::awaitable<std::unique_ptr<HTTPResponse>> HTTPServer::handle_request(shared
auto resp = make_unique<HTTPResponse>(); auto resp = make_unique<HTTPResponse>();
resp->http_version = req.http_version; resp->http_version = req.http_version;
resp->response_code = 200; resp->response_code = 200;
resp->headers.emplace("Content-Type", "application/json");
resp->headers.emplace("Server", "newserv"); resp->headers.emplace("Server", "newserv");
resp->headers.emplace("X-Newserv-Revision", GIT_REVISION_HASH); resp->headers.emplace("X-Newserv-Revision", GIT_REVISION_HASH);
resp->headers.emplace("X-Newserv-Build-Timestamp", phosg::format_time(BUILD_TIMESTAMP)); resp->headers.emplace("X-Newserv-Build-Timestamp", phosg::format_time(BUILD_TIMESTAMP));
@@ -781,74 +780,7 @@ asio::awaitable<std::unique_ptr<HTTPResponse>> HTTPServer::handle_request(shared
serialize_options |= phosg::JSON::SerializeOption::HEX_INTEGERS; serialize_options |= phosg::JSON::SerializeOption::HEX_INTEGERS;
} }
if (req.path == "/") { ret = co_await this->router.call_handler(c, req);
this->require_GET(req);
ret = this->generate_server_version();
} else if (req.path == "/y/shell-exec") {
auto json = this->require_POST(req);
auto command = json.get_string("command");
try {
auto dispatch_res = co_await ShellCommand::dispatch_str(this->state, command);
ret = make_shared<phosg::JSON>(phosg::JSON::dict({{"result", phosg::join(dispatch_res, "\n")}}));
} catch (const exception& e) {
throw HTTPError(400, e.what());
}
} else if (req.path == "/y/rare-drops/stream") {
this->require_GET(req);
if (!(co_await this->enable_websockets(c, req))) {
throw HTTPError(400, "this path requires a websocket connection");
} else {
this->rare_drop_subscribers.emplace(c);
auto version_message = this->generate_server_version();
co_await c->send_websocket_message(version_message->serialize());
co_return nullptr;
}
} else if (req.path == "/y/data/ep3-cards") {
this->require_GET(req);
ret = co_await this->generate_ep3_cards_json(false);
} else if (req.path == "/y/data/ep3-cards-trial") {
this->require_GET(req);
ret = co_await this->generate_ep3_cards_json(true);
} else if (req.path == "/y/data/common-tables") {
this->require_GET(req);
ret = this->generate_common_table_list_json();
} else if (req.path.starts_with("/y/data/common-tables/")) {
this->require_GET(req);
ret = co_await this->generate_common_table_json(req.path.substr(22));
} else if (req.path == "/y/data/rare-tables") {
this->require_GET(req);
ret = this->generate_rare_table_list_json();
} else if (req.path.starts_with("/y/data/rare-tables/")) {
this->require_GET(req);
ret = co_await this->generate_rare_table_json(req.path.substr(20));
} else if (req.path == "/y/data/quests") {
this->require_GET(req);
ret = co_await this->generate_quest_list_json();
} else if (req.path == "/y/data/config") {
this->require_GET(req);
ret = this->state->config_json;
} else if (req.path == "/y/accounts") {
this->require_GET(req);
ret = this->generate_accounts_json();
} else if (req.path == "/y/clients") {
this->require_GET(req);
ret = this->generate_clients_json();
} else if (req.path == "/y/lobbies") {
this->require_GET(req);
ret = this->generate_lobbies_json();
} else if (req.path == "/y/server") {
this->require_GET(req);
ret = this->generate_server_info_json();
} else if (req.path == "/y/summary") {
this->require_GET(req);
ret = this->generate_summary_json();
} else {
throw HTTPError(404, "unknown action");
}
} catch (const HTTPError& e) { } catch (const HTTPError& e) {
ret = make_shared<phosg::JSON>(phosg::JSON::dict({{"Error", true}, {"Message", e.what()}})); ret = make_shared<phosg::JSON>(phosg::JSON::dict({{"Error", true}, {"Message", e.what()}}));
@@ -857,22 +789,34 @@ asio::awaitable<std::unique_ptr<HTTPResponse>> HTTPServer::handle_request(shared
ret = make_shared<phosg::JSON>(phosg::JSON::dict({{"Error", true}, {"Message", e.what()}})); ret = make_shared<phosg::JSON>(phosg::JSON::dict({{"Error", true}, {"Message", e.what()}}));
resp->response_code = 500; resp->response_code = 500;
} }
if (!ret) {
throw logic_error("ret was not set after HTTP handler completed");
}
uint64_t handler_end = phosg::now(); uint64_t handler_end = phosg::now();
if (holds_alternative<shared_ptr<const phosg::JSON>>(ret)) {
// If the handler returns nullptr (not JSON null), assume it called enable_websockets and send no response
auto& json = get<shared_ptr<const phosg::JSON>>(ret);
if (!json) {
co_return nullptr;
}
resp->headers.emplace("Content-Type", "application/json");
resp->data = co_await call_on_thread_pool(*this->state->thread_pool, [&]() -> string { resp->data = co_await call_on_thread_pool(*this->state->thread_pool, [&]() -> string {
return ret->serialize(serialize_options, 0); return json->serialize(serialize_options, 0);
}); });
uint64_t serialize_end = phosg::now(); uint64_t serialize_end = phosg::now();
string handler_time = phosg::format_duration(handler_end - start_time); string handler_time = phosg::format_duration(handler_end - start_time);
string serialize_time = phosg::format_duration(serialize_end - handler_end); string serialize_time = phosg::format_duration(serialize_end - handler_end);
string size_str = phosg::format_size(resp->data.size()); string size_str = phosg::format_size(resp->data.size());
this->log.info_f("{} in [handler: {}, serialize: {}, size: {}]", this->log.info_f("{} in [handler: {}, serialize: {}, size: {}]", req.path, handler_time, serialize_time, size_str);
req.path, handler_time, serialize_time, size_str);
} else {
auto& raw_resp = get<RawResponse>(ret);
resp->headers.emplace("Content-Type", std::move(raw_resp.content_type));
resp->data = std::move(raw_resp.data);
string handler_time = phosg::format_duration(handler_end - start_time);
string size_str = phosg::format_size(resp->data.size());
this->log.info_f("{} in [handler: {}, size: {}]", req.path, handler_time, size_str);
}
co_return resp; co_return resp;
} }
+6 -20
View File
@@ -4,6 +4,7 @@
#include <memory> #include <memory>
#include <string> #include <string>
#include <variant>
#include "AsyncHTTPServer.hh" #include "AsyncHTTPServer.hh"
#include "ServerState.hh" #include "ServerState.hh"
@@ -20,28 +21,13 @@ public:
asio::awaitable<void> send_rare_drop_notification(std::shared_ptr<const phosg::JSON> message); asio::awaitable<void> send_rare_drop_notification(std::shared_ptr<const phosg::JSON> message);
protected: protected:
struct RawResponse {
std::string content_type;
std::string data;
};
std::shared_ptr<ServerState> state; std::shared_ptr<ServerState> state;
std::unordered_set<std::shared_ptr<HTTPClient>> rare_drop_subscribers; std::unordered_set<std::shared_ptr<HTTPClient>> rare_drop_subscribers;
HTTPRouter<std::variant<RawResponse, std::shared_ptr<const phosg::JSON>>> router;
std::shared_ptr<phosg::JSON> generate_server_version() const;
std::shared_ptr<phosg::JSON> generate_account_json(std::shared_ptr<const Account> a) const;
std::shared_ptr<phosg::JSON> generate_client_json(
std::shared_ptr<const Client> c, std::shared_ptr<const ItemNameIndex> item_name_index) const;
std::shared_ptr<phosg::JSON> generate_lobby_json(
std::shared_ptr<const Lobby> l, std::shared_ptr<const ItemNameIndex> item_name_index) const;
std::shared_ptr<phosg::JSON> generate_accounts_json() const;
std::shared_ptr<phosg::JSON> generate_clients_json() const;
std::shared_ptr<phosg::JSON> generate_server_info_json() const;
std::shared_ptr<phosg::JSON> generate_lobbies_json() const;
std::shared_ptr<phosg::JSON> generate_summary_json() const;
std::shared_ptr<phosg::JSON> generate_all_json() const;
asio::awaitable<std::shared_ptr<phosg::JSON>> generate_ep3_cards_json(bool trial) const;
std::shared_ptr<phosg::JSON> generate_common_table_list_json() const;
std::shared_ptr<phosg::JSON> generate_rare_table_list_json() const;
asio::awaitable<std::shared_ptr<phosg::JSON>> generate_common_table_json(const std::string& table_name) const;
asio::awaitable<std::shared_ptr<phosg::JSON>> generate_rare_table_json(const std::string& table_name) const;
asio::awaitable<std::shared_ptr<phosg::JSON>> generate_quest_list_json();
void require_GET(const HTTPRequest& req); void require_GET(const HTTPRequest& req);
phosg::JSON require_POST(const HTTPRequest& req); phosg::JSON require_POST(const HTTPRequest& req);
-3
View File
@@ -845,9 +845,6 @@ phosg::JSON QuestIndex::json() const {
{"Categories", std::move(categories_json)}, {"Categories", std::move(categories_json)},
{"Quests", std::move(quests_json)}, {"Quests", std::move(quests_json)},
}); });
// std::map<uint32_t, std::shared_ptr<Quest>> quests_by_number;
// std::map<std::string, std::shared_ptr<Quest>> quests_by_name;
// std::map<uint32_t, std::map<uint32_t, std::shared_ptr<Quest>>> quests_by_category_id_and_number;
} }
shared_ptr<const Quest> QuestIndex::get(uint32_t quest_number) const { shared_ptr<const Quest> QuestIndex::get(uint32_t quest_number) const {
+1 -1
View File
@@ -171,7 +171,7 @@ phosg::JSON QuestMetadata::json() const {
enemy_exp_overrides_json.emplace(key_str, exp_override); enemy_exp_overrides_json.emplace(key_str, exp_override);
} }
auto create_item_mask_entries_json = phosg::JSON::dict(); auto create_item_mask_entries_json = phosg::JSON::list();
for (const auto& item : this->create_item_mask_entries) { for (const auto& item : this->create_item_mask_entries) {
create_item_mask_entries_json.emplace_back(item.str()); create_item_mask_entries_json.emplace_back(item.str());
} }
+16 -7
View File
@@ -20,6 +20,7 @@ TeamIndex::Team::Member::Member(const phosg::JSON& json)
try { try {
this->account_id = json.get_int("AccountID"); this->account_id = json.get_int("AccountID");
} catch (const out_of_range&) { } catch (const out_of_range&) {
// Old format
this->account_id = json.get_int("SerialNumber"); this->account_id = json.get_int("SerialNumber");
} }
} }
@@ -78,7 +79,7 @@ void TeamIndex::Team::load_config() {
this->reward_flags = json.get_int("RewardFlags"); this->reward_flags = json.get_int("RewardFlags");
} }
void TeamIndex::Team::save_config() const { phosg::JSON TeamIndex::Team::json() const {
phosg::JSON members_json = phosg::JSON::list(); phosg::JSON members_json = phosg::JSON::list();
for (const auto& it : this->members) { for (const auto& it : this->members) {
members_json.emplace_back(it.second.json()); members_json.emplace_back(it.second.json());
@@ -87,14 +88,17 @@ void TeamIndex::Team::save_config() const {
for (const auto& it : this->reward_keys) { for (const auto& it : this->reward_keys) {
reward_keys_json.emplace_back(it); reward_keys_json.emplace_back(it);
} }
phosg::JSON root = phosg::JSON::dict({ return phosg::JSON::dict({
{"Name", this->name}, {"Name", this->name},
{"SpentPoints", this->spent_points}, {"SpentPoints", this->spent_points},
{"Members", std::move(members_json)}, {"Members", std::move(members_json)},
{"RewardKeys", std::move(reward_keys_json)}, {"RewardKeys", std::move(reward_keys_json)},
{"RewardFlags", this->reward_flags}, {"RewardFlags", this->reward_flags},
}); });
phosg::save_file(this->json_filename(), root.serialize(phosg::JSON::SerializeOption::FORMAT | phosg::JSON::SerializeOption::HEX_INTEGERS | phosg::JSON::SerializeOption::ESCAPE_CONTROLS_ONLY)); }
void TeamIndex::Team::save_config() const {
phosg::save_file(this->json_filename(), this->json().serialize(phosg::JSON::SerializeOption::FORMAT | phosg::JSON::SerializeOption::HEX_INTEGERS | phosg::JSON::SerializeOption::ESCAPE_CONTROLS_ONLY));
} }
void TeamIndex::Team::load_flag() { void TeamIndex::Team::load_flag() {
@@ -110,16 +114,21 @@ void TeamIndex::Team::load_flag() {
} }
} }
void TeamIndex::Team::save_flag() const { phosg::ImageRGBA8888N TeamIndex::Team::decode_flag_data() const {
if (!this->flag_data) {
return;
}
phosg::ImageRGBA8888N img(32, 32); phosg::ImageRGBA8888N img(32, 32);
for (size_t y = 0; y < 32; y++) { for (size_t y = 0; y < 32; y++) {
for (size_t x = 0; x < 32; x++) { for (size_t x = 0; x < 32; x++) {
img.write(x, y, phosg::rgba8888_for_argb1555(this->flag_data->at(y * 0x20 + x))); img.write(x, y, phosg::rgba8888_for_argb1555(this->flag_data->at(y * 0x20 + x)));
} }
} }
return img;
}
void TeamIndex::Team::save_flag() const {
if (!this->flag_data) {
return;
}
auto img = this->decode_flag_data();
phosg::save_file(this->flag_filename(), img.serialize(phosg::ImageFormat::WINDOWS_BITMAP)); phosg::save_file(this->flag_filename(), img.serialize(phosg::ImageFormat::WINDOWS_BITMAP));
} }
+1
View File
@@ -76,6 +76,7 @@ public:
void load_config(); void load_config();
void save_config() const; void save_config() const;
void load_flag(); void load_flag();
phosg::ImageRGBA8888N decode_flag_data() const;
void save_flag() const; void save_flag() const;
void delete_files() const; void delete_files() const;