rewrite HTTP interface
This commit is contained in:
@@ -163,7 +163,7 @@ asio::awaitable<HTTPRequest> HTTPClient::recv_http_request(size_t max_line_size,
|
||||
} else if (method_token == "TRACE") {
|
||||
req.method = HTTPRequest::Method::TRACE;
|
||||
} else {
|
||||
throw HTTPError(400, "unknown request method");
|
||||
throw HTTPError(400, "Unknown request method");
|
||||
}
|
||||
|
||||
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 chunk_size = stoull(line, &parse_offset, 16);
|
||||
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) {
|
||||
break;
|
||||
}
|
||||
total_data_bytes += chunk_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));
|
||||
auto after_chunk_data = co_await this->r.read_line("\r\n", 0x20);
|
||||
if (!after_chunk_data.empty()) {
|
||||
throw HTTPError(400, "incorrect trailing sequence after chunk data");
|
||||
throw HTTPError(400, "Incorrect trailing sequence after chunk data");
|
||||
}
|
||||
}
|
||||
} else {
|
||||
auto content_length_header = req.get_header("content-length");
|
||||
size_t content_length = content_length_header ? stoull(*content_length_header) : 0;
|
||||
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) {
|
||||
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()) {
|
||||
WebSocketMessage msg;
|
||||
|
||||
// We need at most 10 bytes to determine if there's a valid frame, or as
|
||||
// little as 2
|
||||
// We need at most 10 bytes to determine if there's a valid frame, or as little as 2
|
||||
co_await this->r.read_data_into(msg.header, 2);
|
||||
|
||||
// Get the payload size
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <phosg/Encoding.hh>
|
||||
#include <phosg/Hash.hh>
|
||||
#include <phosg/Time.hh>
|
||||
#include <string>
|
||||
@@ -82,6 +83,106 @@ struct HTTPClient {
|
||||
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 {
|
||||
size_t max_http_request_line_size = 0x1000; // 4KB
|
||||
size_t max_http_data_size = 0x200000; // 2MB
|
||||
@@ -120,6 +221,29 @@ public:
|
||||
protected:
|
||||
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
|
||||
// successfully (and the caller should then receive/send WebSocket messages),
|
||||
// or false if this failed (and the caller should send an HTTP response).
|
||||
|
||||
+5
-1
@@ -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)
|
||||
auto promise = std::make_shared<AsyncPromise<ReturnT>>();
|
||||
asio::post(pool, [bound = std::move(bound), promise]() mutable {
|
||||
promise->set_value(bound());
|
||||
try {
|
||||
promise->set_value(bound());
|
||||
} catch (...) {
|
||||
promise->set_exception(std::current_exception());
|
||||
}
|
||||
});
|
||||
co_return co_await promise->get();
|
||||
}
|
||||
|
||||
+736
-792
File diff suppressed because it is too large
Load Diff
+6
-20
@@ -4,6 +4,7 @@
|
||||
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <variant>
|
||||
|
||||
#include "AsyncHTTPServer.hh"
|
||||
#include "ServerState.hh"
|
||||
@@ -20,28 +21,13 @@ public:
|
||||
asio::awaitable<void> send_rare_drop_notification(std::shared_ptr<const phosg::JSON> message);
|
||||
|
||||
protected:
|
||||
struct RawResponse {
|
||||
std::string content_type;
|
||||
std::string data;
|
||||
};
|
||||
std::shared_ptr<ServerState> state;
|
||||
std::unordered_set<std::shared_ptr<HTTPClient>> rare_drop_subscribers;
|
||||
|
||||
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();
|
||||
HTTPRouter<std::variant<RawResponse, std::shared_ptr<const phosg::JSON>>> router;
|
||||
|
||||
void require_GET(const HTTPRequest& req);
|
||||
phosg::JSON require_POST(const HTTPRequest& req);
|
||||
|
||||
@@ -845,9 +845,6 @@ phosg::JSON QuestIndex::json() const {
|
||||
{"Categories", std::move(categories_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 {
|
||||
|
||||
@@ -171,7 +171,7 @@ phosg::JSON QuestMetadata::json() const {
|
||||
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) {
|
||||
create_item_mask_entries_json.emplace_back(item.str());
|
||||
}
|
||||
|
||||
+16
-7
@@ -20,6 +20,7 @@ TeamIndex::Team::Member::Member(const phosg::JSON& json)
|
||||
try {
|
||||
this->account_id = json.get_int("AccountID");
|
||||
} catch (const out_of_range&) {
|
||||
// Old format
|
||||
this->account_id = json.get_int("SerialNumber");
|
||||
}
|
||||
}
|
||||
@@ -78,7 +79,7 @@ void TeamIndex::Team::load_config() {
|
||||
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();
|
||||
for (const auto& it : this->members) {
|
||||
members_json.emplace_back(it.second.json());
|
||||
@@ -87,14 +88,17 @@ void TeamIndex::Team::save_config() const {
|
||||
for (const auto& it : this->reward_keys) {
|
||||
reward_keys_json.emplace_back(it);
|
||||
}
|
||||
phosg::JSON root = phosg::JSON::dict({
|
||||
return phosg::JSON::dict({
|
||||
{"Name", this->name},
|
||||
{"SpentPoints", this->spent_points},
|
||||
{"Members", std::move(members_json)},
|
||||
{"RewardKeys", std::move(reward_keys_json)},
|
||||
{"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() {
|
||||
@@ -110,16 +114,21 @@ void TeamIndex::Team::load_flag() {
|
||||
}
|
||||
}
|
||||
|
||||
void TeamIndex::Team::save_flag() const {
|
||||
if (!this->flag_data) {
|
||||
return;
|
||||
}
|
||||
phosg::ImageRGBA8888N TeamIndex::Team::decode_flag_data() const {
|
||||
phosg::ImageRGBA8888N img(32, 32);
|
||||
for (size_t y = 0; y < 32; y++) {
|
||||
for (size_t x = 0; x < 32; 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));
|
||||
}
|
||||
|
||||
|
||||
@@ -76,6 +76,7 @@ public:
|
||||
void load_config();
|
||||
void save_config() const;
|
||||
void load_flag();
|
||||
phosg::ImageRGBA8888N decode_flag_data() const;
|
||||
void save_flag() const;
|
||||
void delete_files() const;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user