| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348 |
- #include "web_service_runtime.h"
- #include <algorithm>
- #include <array>
- #include <atomic>
- #include <cstdio>
- #include <cstring>
- #include <string_view>
- #include "../../core/logging/log.h"
- #include "../../core/runtime/server_clock.h"
- #include "../../middleware/encoding/bit_reader.h"
- #include "../../middleware/encoding/byte_order.h"
- #include "../../middleware/web_service/messages/opcode1820.h"
- #include "../../middleware/web_service/messages/opcode1901.h"
- #include "../../middleware/web_service/messages/opcode205.h"
- #include "../../middleware/web_service/messages/opcode206.h"
- #include "../../middleware/web_service/messages/opcode402.h"
- #include "../../middleware/web_service/messages/opcode403.h"
- #include "../../middleware/web_service/messages/opcode406.h"
- #include "../../middleware/web_service/messages/opcode501_codec.h"
- #include "../../middleware/web_service/messages/opcode503.h"
- #include "../../middleware/web_service/messages/opcode504.h"
- #include "../../middleware/web_service/messages/opcode601/opcode601_codec.h"
- #include "../../middleware/web_service/messages/opcode701/opcode701_codec.h"
- #include "../../middleware/web_service/messages/opcode702.h"
- #include "../../middleware/web_service/messages/opcode801.h"
- #include "../../middleware/web_service/messages/opcode901/opcode901_codec.h"
- #include "../../middleware/web_service/messages/opcode903.h"
- #include "../../middleware/web_service/web_service_envelope.h"
- #include "../../state/account/account_state.h"
- #include "../../state/activity/membership/activity_membership_query.h"
- #include "../../state/build_data/runtime.h"
- #include "../../state/runtime/runtime.h"
- #include "internal.h"
- #include "opcode_routes.h"
- #include "web_service_actions.h"
- namespace sunrise::server::web_service {
- namespace {
- namespace messages = middleware::web_service::messages;
- /** One ordinary event line carries an opcode and its fixed prefix. */
- constexpr std::size_t kOpcodeLineCapacity = 64;
- /** One refusal line carries both request indices, the clock presence, and the clock verdict. */
- constexpr std::size_t kPurchaseLineCapacity = 128;
- /** A request trace keeps enough payload to identify an item-action descriptor. */
- constexpr std::size_t kRequestPayloadTraceBytes = 192;
- /** Marks a trace that stopped at the cap, so a short hex string is not read as a short payload. */
- constexpr std::string_view kTruncated = " truncated=1";
- /** The mutation variant's first alternative is the empty one, so index zero prepared nothing. */
- constexpr std::size_t kNoMutation = 0;
- /**
- * Logs the Web Service opcode and a bounded payload trace.
- * One svc-10 frame looks like any other, and the opcode drives the client's queuez state machine.
- * @param message Parsed request envelope and borrowed payload.
- */
- void report_request(const middleware::web_service::Message& message) noexcept {
- std::array<char, core::log::kLineCapacity> line{};
- const int prefix =
- std::snprintf(line.data(),
- line.size(),
- "ev=ws stage=request opcode=%u transaction=%u payload_bytes=%zu payload_hex=",
- static_cast<unsigned>(message.opcode),
- static_cast<unsigned>(message.transactionId),
- message.payload.size());
- if (prefix <= 0 || static_cast<std::size_t>(prefix) >= line.size()) {
- return;
- }
- std::size_t length = static_cast<std::size_t>(prefix);
- const std::size_t traced =
- (std::min)(message.payload.size(), static_cast<std::size_t>(kRequestPayloadTraceBytes));
- (void)core::log::append_hex(line, length, message.payload.first(traced));
- if (traced != message.payload.size() && length + kTruncated.size() < line.size()) {
- std::memcpy(line.data() + length, kTruncated.data(), kTruncated.size());
- length += kTruncated.size();
- }
- core::log::write(core::log::Channel::server, core::log::Level::info, {line.data(), length});
- }
- /**
- * Refuses one vendor purchase and answers it.
- * No award, cost or stock rule exists yet, so no purchase can succeed. The refusal must still be
- * answered, because no answer holds the head of the client's pending queue.
- * @param message Parsed purchase request.
- * @param response Response-body storage owned by the caller.
- * @param written Receives the encoded response size.
- * @return True when the refusal was encoded.
- */
- [[nodiscard]] bool refuse_purchase(const middleware::web_service::Message& message,
- std::span<std::byte> response,
- std::size_t& written) noexcept {
- messages::opcode901::Request purchase;
- const bool parsed = messages::opcode901::parse_request(message, purchase);
- // The clock verdict is logged, never acted on. Nothing can pass while the route refuses.
- const auto policy =
- messages::opcode901::check_clock(purchase, core::runtime::server_clock_seconds());
- std::array<char, kPurchaseLineCapacity> line{};
- const int length =
- parsed ? std::snprintf(
- line.data(),
- line.size(),
- "ev=ws901 stage=purchase result=refuse vendor=%d sale=%d present=%u policy=%s",
- static_cast<int>(purchase.vendorIndex),
- static_cast<int>(purchase.saleIndex),
- purchase.hasClock ? 1U : 0U,
- messages::opcode901::clock_policy_name(policy))
- : std::snprintf(line.data(),
- line.size(),
- "ev=ws901 stage=purchase result=refuse reason=parse");
- report_line(core::log::Level::error, line, length);
- middleware::web_service::StatusResponse status{};
- status.code = middleware::web_service::kRefusedStatusCode;
- // A refused purchase grants nothing, so no Family-4 revision carries its result.
- status.value = middleware::web_service::kNoFamily4Publication;
- // The trailing bool drives a local action effect on the client, so it stays clear.
- status.trailingBool = false;
- return middleware::web_service::encode_response(
- message,
- middleware::web_service::ResponseShape::statusPairWithBool,
- status,
- response,
- written);
- }
- /**
- * Answers a request whose own codec refused with the bare correlated echo.
- * The Client matches on the echoed transaction id. A missing body under-runs its decoder and
- * takes the BAP connection down, so a thin body is always sent.
- * @param message Parsed request whose correlation fields are echoed.
- * @param response Svc-11 response-body storage owned by the caller.
- * @param written Gets the encoded response-body size in bytes.
- * @return True when the echo fits.
- */
- bool encode_echo(const middleware::web_service::Message& message,
- std::span<std::byte> response,
- std::size_t& written) noexcept {
- std::array<char, kOpcodeLineCapacity> line{};
- const int count = std::snprintf(
- line.data(), line.size(), "ev=ws stage=body result=echo opcode=%u", message.opcode);
- report_line(core::log::Level::warn, line, count);
- namespace ws = middleware::web_service;
- return ws::encode_response(
- message, ws::ResponseShape::generic, ws::StatusResponse{}, response, written);
- }
- /**
- * Issues the family-5 server clock the Client extrapolates its family-5 time from.
- * The wire field counts whole seconds. A repeated value reads as no change and stalls the
- * Client's family-5 boot task, so the issued count must strictly increase.
- * @return Unix seconds, always greater than the previous call's result.
- */
- [[nodiscard]] std::uint64_t next_family5_clock() noexcept {
- static std::atomic<std::uint64_t> issued{0};
- const auto wall = static_cast<std::uint64_t>(core::runtime::server_clock_seconds());
- std::uint64_t previous = issued.load(std::memory_order_relaxed);
- std::uint64_t next = 0;
- do {
- next = wall > previous ? wall : previous + 1;
- } while (!issued.compare_exchange_weak(previous, next, std::memory_order_relaxed));
- return next;
- }
- /**
- * Records the world state the character write-back reports.
- * The body is client-owned state; the world-state field is the one value the host acts on.
- * @param message Parsed ws-702 envelope.
- */
- void note_character_writeback(const middleware::web_service::Message& message) noexcept {
- messages::opcode702::Request request;
- const bool parsed = messages::opcode702::parse_request(message, request);
- std::array<char, core::log::kLineCapacity> line{};
- const int written = std::snprintf(line.data(),
- line.size(),
- "ev=activity stage=writeback result=%s world_state=%u",
- parsed ? "ok" : "unparsed",
- static_cast<unsigned>(request.worldState));
- if (written > 0) {
- core::log::write(core::log::Channel::server,
- core::log::Level::info,
- {line.data(), static_cast<std::size_t>(written)});
- }
- if (parsed) {
- state::activity::membership::note_client_writeback(request.worldState
- == messages::opcode702::kInWorld);
- }
- }
- /**
- * Applies the report that carries no answer of its own beyond the shared status pair.
- * @param message Parsed request envelope and borrowed payload.
- */
- void note_reports(const middleware::web_service::Message& message) noexcept {
- if (message.opcode == messages::opcode702::kOpcode) {
- note_character_writeback(message);
- }
- }
- } // namespace
- /** Re-encodes a prepared reply as a refusal after its Queuez staging failed. */
- bool encode_staging_refusal(const middleware::web_service::Message& message,
- std::span<std::byte> response,
- std::size_t& written) noexcept {
- middleware::web_service::ResponseShape shape{};
- resolve_response_shape(message.opcode, shape);
- middleware::web_service::StatusResponse status{};
- status.code = middleware::web_service::kRefusedStatusCode;
- status.value = middleware::web_service::kNoFamily4Publication;
- return middleware::web_service::encode_response(message, shape, status, response, written);
- }
- /** Answers one Web Service request when its caller has no action to publish. */
- bool consume(std::span<const std::byte> request,
- std::span<std::byte> response,
- std::size_t& written) noexcept {
- Outcome outcome;
- return consume(request, response, written, outcome);
- }
- /** Parses one request, prepares any action it names, and encodes the reply that reports it. */
- bool consume(std::span<const std::byte> request,
- std::span<std::byte> response,
- std::size_t& written,
- Outcome& outcome) noexcept {
- written = 0;
- outcome = {};
- middleware::web_service::Message message;
- if (!middleware::web_service::parse_request(request, message)) {
- core::log::write(
- core::log::Channel::server, core::log::Level::warn, "ev=ws stage=parse result=fail");
- return false;
- }
- report_request(message);
- note_reports(message);
- if (message.opcode == messages::opcode205::kOpcode) {
- state::InvestmentState investment{};
- return (state::investment_snapshot(investment)
- && messages::opcode205::encode_response(
- message, investment, next_family5_clock(), response, written))
- || encode_echo(message, response, written);
- }
- if (message.opcode == messages::opcode503::kOpcode) {
- messages::opcode503::Request bootstrap;
- const bool parsed = messages::opcode503::parse_request(message, bootstrap);
- // The request's own key is echoed and adopted. An authored id here costs the ship and the
- // banner.
- if (!bootstrap.hasPrimarySoid) {
- bootstrap.primarySoid = state::account_snapshot().primarySoid;
- }
- state::InvestmentState investment{};
- if (!parsed || !state::investment_snapshot(investment)
- || !messages::opcode503::encode_response(
- message, bootstrap, investment, next_family5_clock(), response, written)) {
- return encode_echo(message, response, written);
- }
- if (bootstrap.hasPrimarySoid && !state::set_primary_soid(bootstrap.primarySoid)) {
- core::log::write(core::log::Channel::server,
- core::log::Level::warn,
- "ev=ws503 stage=adopt result=fail");
- }
- return true;
- }
- if (message.opcode == messages::opcode501::kOpcode) {
- // Returns a SOID family three already publishes. The request body is not parsed.
- const std::uint64_t characterSoid =
- state::account::selected_character_soid(state::account_snapshot());
- return messages::opcode501::encode_response(message, characterSoid, response, written)
- || encode_echo(message, response, written);
- }
- // Runs before the shared response-shape path, which would answer the success status.
- if (message.opcode == messages::opcode901::kOpcode) {
- return refuse_purchase(message, response, written)
- || encode_echo(message, response, written);
- }
- if (message.opcode == messages::opcode601::kOpcode) {
- return messages::opcode601::encode_response(message, response, written)
- || encode_echo(message, response, written);
- }
- // A subscribe whose body does not parse is still answered; only the subscription is dropped.
- middleware::queuez::Subscription subscription;
- const bool subscribes = message.opcode == messages::opcode206::kOpcode
- && messages::opcode206::parse_request(message, subscription);
- // The action runs before its reply is encoded, because the reply reports whether it worked.
- // Most actions fill the outcome only after preparing a whole transition. WS-701 also accepts
- // a valid no-op heartbeat, so that one success is tracked separately from mutation presence.
- bool dispatched = true;
- bool acceptedWithoutMutation = false;
- if (message.opcode == messages::opcode504::kOpcode) {
- select_character(message, outcome);
- } else if (message.opcode == messages::opcode402::kOpcode) {
- dismantle_item(message, outcome);
- } else if (message.opcode == messages::opcode403::kOpcode) {
- mutate_equipment(message, false, outcome);
- } else if (message.opcode == messages::opcode403::kUnequipOpcode) {
- mutate_equipment(message, true, outcome);
- } else if (message.opcode == messages::opcode801::kOpcode) {
- mutate_subclass_selection(message, outcome);
- } else if (message.opcode == messages::opcode903::kOpcode) {
- mutate_socket_plug(message, outcome);
- } else if (message.opcode == messages::opcode1901::kOpcode) {
- mutate_equipped_socket_plug(message, outcome);
- } else if (message.opcode == messages::opcode406::kOpcode) {
- mutate_item_state(message, outcome);
- } else if (message.opcode == messages::opcode701::kOpcode) {
- const state::SettingsUpdateDisposition disposition = mutate_settings(message, outcome);
- acceptedWithoutMutation = disposition == state::SettingsUpdateDisposition::acceptedNoChange;
- } else if (message.opcode == messages::opcode1820::kOpcode) {
- acquire_item(message, outcome);
- } else {
- dispatched = false;
- }
- const bool prepared = outcome.hasSelectedCharacter || outcome.mutation.index() != kNoMutation;
- middleware::web_service::ResponseShape shape{};
- resolve_response_shape(message.opcode, shape);
- middleware::web_service::StatusResponse status{};
- if (awaits_family4_version(message.opcode)) {
- // Nothing is published from here. A staged mutation re-encodes this with its own revision.
- status.value = middleware::web_service::kNoFamily4Publication;
- }
- if (dispatched && !prepared && !acceptedWithoutMutation) {
- status.code = middleware::web_service::kRefusedStatusCode;
- }
- if (!middleware::web_service::encode_response(message, shape, status, response, written)) {
- // The echo carries no status, so nothing may be published against it.
- outcome = {};
- return encode_echo(message, response, written);
- }
- if (subscribes) {
- // Publish the subscription only after its correlated response is complete.
- outcome.hasSubscription = true;
- outcome.subscription = subscription;
- }
- return true;
- }
- } // namespace sunrise::server::web_service
|