queuez_banner_push.cpp 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478
  1. #include <array>
  2. #include <cstdio>
  3. #include <span>
  4. #include "../../../../../core/logging/log.h"
  5. #include "../../../../../middleware/datagen/definitions.h"
  6. #include "../../../../../middleware/secure_channel/runtime.h"
  7. #include "../../../../../state/account/account_state.h"
  8. #include "../../../../../state/build_data/runtime.h"
  9. #include "../../../../../state/runtime/runtime.h"
  10. #include "../../queuez/queuez_state_validation.h"
  11. #include "../snapshot/snapshot.h"
  12. #include "queuez_push_reporting.h"
  13. #include "queuez_update_frame.h"
  14. namespace sunrise::server::bap::encrypted::push {
  15. namespace {
  16. /** One line carries the refusal key and nothing else. */
  17. constexpr std::size_t kSkipLineCapacity = 96;
  18. /**
  19. * Reports the banner move declining to build a frame.
  20. * The move has 4 separate refusals and all leave the emblem where it is, so the key is the only
  21. * way to tell a pick owing nothing from a ladder that was never seeded.
  22. * @param reason Key naming the refusal.
  23. */
  24. void report_skip(const char* reason) noexcept {
  25. std::array<char, kSkipLineCapacity> line{};
  26. const int written = std::snprintf(
  27. line.data(), line.size(), "ev=queuez stage=banner_move result=skip reason=%s", reason);
  28. if (written > 0) {
  29. core::log::write(core::log::Channel::server,
  30. core::log::Level::warn,
  31. {line.data(), static_cast<std::size_t>(written)});
  32. }
  33. }
  34. /** Validates and appends one same-character Family-0 appearance refresh. */
  35. [[nodiscard]] bool append_appearance_frame(Scratch& scratch,
  36. const queuez::CharacterAppearanceRefresh& refresh,
  37. snapshot::Prepared& prepared,
  38. const char* stage,
  39. std::span<const std::byte, state::kAesKeySize> key,
  40. std::array<std::byte, state::kBapNonceSize>& nonce,
  41. std::span<std::byte> response,
  42. std::size_t& written) noexcept {
  43. const std::size_t objectCount = prepared.family.objects.size();
  44. const std::size_t beforeBytes = written;
  45. const bool replacement =
  46. objectCount >= 2
  47. && prepared.family.objects.front().id == middleware::datagen::kBannerCharacterObjectId
  48. && prepared.family.objects.front().version == refresh.characterSoid
  49. && prepared.family.objects.front().payload.empty();
  50. const std::size_t characterIndex = replacement ? 1U : 0U;
  51. const bool hasAnchor = objectCount == characterIndex + 2U;
  52. if ((objectCount != characterIndex + 1U && !hasAnchor)
  53. || prepared.family.type != queuez::kBannerFamilyType
  54. || prepared.family.rootSoid != refresh.after.family4RootSoid
  55. || prepared.family.version != refresh.after.family0Version || prepared.family.flags != 0
  56. || (replacement
  57. && prepared.family.objects.front().encoding != middleware::queuez::Encoding::raw)
  58. || prepared.family.objects[characterIndex].id
  59. != middleware::datagen::kBannerCharacterObjectId
  60. || prepared.family.objects[characterIndex].version != refresh.characterSoid
  61. || prepared.family.objects[characterIndex].encoding != middleware::queuez::Encoding::oodle
  62. || prepared.family.objects[characterIndex].payload.empty()
  63. || (hasAnchor
  64. && (prepared.family.objects.back().id != middleware::datagen::kBannerAnchorObjectId
  65. || prepared.family.objects.back().version != refresh.after.family4RootSoid
  66. || prepared.family.objects.back().encoding != middleware::queuez::Encoding::oodle
  67. || prepared.family.objects.back().payload.empty()))
  68. || !queuez_frame::append(scratch,
  69. prepared.family,
  70. prepared.rawClearSize,
  71. prepared.compressedClearSize,
  72. key,
  73. nonce,
  74. response,
  75. written)) {
  76. return false;
  77. }
  78. middleware::secure_channel::advance_nonce(nonce);
  79. queuez_report::push(stage, queuez::kBannerFamilyType, objectCount, written - beforeBytes, 1);
  80. return true;
  81. }
  82. /** Validates and appends one incremental Family-3 appearance frame. */
  83. [[nodiscard]] bool
  84. append_roster_appearance_frame(Scratch& scratch,
  85. const queuez::RosterAppearanceRefresh& refresh,
  86. snapshot::Prepared& prepared,
  87. const char* stage,
  88. std::span<const std::byte, state::kAesKeySize> key,
  89. std::array<std::byte, state::kBapNonceSize>& nonce,
  90. std::span<std::byte> response,
  91. std::size_t& written) noexcept {
  92. const std::size_t expectedObjects = refresh.includeRoster ? 2U : 1U;
  93. const std::size_t objectCount = prepared.family.objects.size();
  94. const std::size_t beforeBytes = written;
  95. if (objectCount != expectedObjects || prepared.family.type != queuez::kRosterFamilyType
  96. || prepared.family.rootSoid != refresh.after.family3RootSoid
  97. || prepared.family.version != refresh.after.family3Version || prepared.family.flags != 0
  98. || prepared.family.objects.front().id != middleware::datagen::kRosterCharacterObjectId
  99. || prepared.family.objects.front().version != refresh.characterSoid
  100. || prepared.family.objects.front().encoding != middleware::queuez::Encoding::oodle
  101. || prepared.family.objects.front().payload.empty()
  102. || (refresh.includeRoster
  103. && (prepared.family.objects.back().id != middleware::datagen::kRosterObjectId
  104. || prepared.family.objects.back().version != refresh.after.family3RootSoid
  105. || prepared.family.objects.back().encoding != middleware::queuez::Encoding::oodle
  106. || prepared.family.objects.back().payload.empty()))
  107. || !queuez_frame::append(scratch,
  108. prepared.family,
  109. prepared.rawClearSize,
  110. prepared.compressedClearSize,
  111. key,
  112. nonce,
  113. response,
  114. written)) {
  115. return false;
  116. }
  117. middleware::secure_channel::advance_nonce(nonce);
  118. queuez_report::push(stage, queuez::kRosterFamilyType, objectCount, written - beforeBytes, 1);
  119. return true;
  120. }
  121. } // namespace
  122. /**
  123. * Appends the unsolicited family-zero banner pair that follows a family-three subscription.
  124. * Sent twice per boot at the same version: family zero hits the state-1 race with no svc-12
  125. * re-push behind it, so a rejected first pair would blank the banner for the run.
  126. * @param scratch Lock-owned transform buffers.
  127. * @param familyRootSoid Root the Client subscribed for Family 3.
  128. * @param key Active AES-GCM session key.
  129. * @param nonce Push-direction nonce, advanced only by a complete frame.
  130. * @param response Caller-owned output containing prior frames.
  131. * @param written Existing byte count, updated by a complete frame.
  132. * @return True when the banner frame is appended.
  133. */
  134. bool append_banner_notification(Scratch& scratch,
  135. const queuez::SessionState& before,
  136. std::uint64_t familyRootSoid,
  137. std::span<const std::byte, state::kAesKeySize> key,
  138. std::array<std::byte, state::kBapNonceSize>& nonce,
  139. std::span<std::byte> response,
  140. std::size_t& written,
  141. queuez::SessionState& after) noexcept {
  142. after = before;
  143. // The pair names the first character when none is picked yet. The client's family-zero record
  144. // accepts a snapshot for about ten seconds, then clears the family and refuses every later
  145. // one, so holding the pair for the pick spends that window and the subscription times out.
  146. if (state::account::banner_character_soid(state::account_snapshot()) == 0) {
  147. core::log::write(core::log::Channel::server,
  148. core::log::Level::info,
  149. "ev=queuez stage=banner result=skip reason=nocharacter");
  150. return false;
  151. }
  152. snapshot::Prepared prepared{};
  153. // The unsolicited pair is the family's first delivery, so it carries the full-snapshot flag.
  154. if (!snapshot::prepare_banner(
  155. scratch, familyRootSoid, queuez::kInitialFamilyVersion, 0, prepared)) {
  156. core::log::write(core::log::Channel::server,
  157. core::log::Level::warn,
  158. "ev=queuez stage=banner result=fail reason=prepare");
  159. return false;
  160. }
  161. const std::size_t objectCount = prepared.family.objects.size();
  162. const std::size_t beforeBytes = written;
  163. if (!queuez_frame::append(scratch,
  164. prepared.family,
  165. prepared.rawClearSize,
  166. prepared.compressedClearSize,
  167. key,
  168. nonce,
  169. response,
  170. written)) {
  171. core::log::write(core::log::Channel::server,
  172. core::log::Level::warn,
  173. "ev=queuez stage=banner result=fail reason=frame");
  174. return false;
  175. }
  176. middleware::secure_channel::advance_nonce(nonce);
  177. // The Client now holds this pair, so the ladder owns it. Without this an unsubscribe leaves
  178. // family zero unrecorded and the next pick has no previous record to release.
  179. const std::uint64_t delivered =
  180. state::account::banner_character_soid(state::account_snapshot());
  181. if (!after.family0Active && delivered != 0) {
  182. after.family0Active = true;
  183. after.family0Character = delivered;
  184. after.family0Version = queuez::kInitialFamilyVersion;
  185. }
  186. queuez_report::push("banner",
  187. prepared.family.type,
  188. objectCount,
  189. written - beforeBytes,
  190. queuez_report::kNoRecordOutcome);
  191. return true;
  192. }
  193. /**
  194. * Appends the family-zero move that follows an opcode-504 pick.
  195. * The Client holds the objIdx-1 buffer for one character at a time, allocated from the character
  196. * the anchor names, so the pair moves with the pick or the banner keeps the old emblem.
  197. * @param scratch Lock-owned transform buffers.
  198. * @param before Queuez state after the family-four move.
  199. * @param selectedCharacter Character the pick named.
  200. * @param key Active AES-GCM session key.
  201. * @param nonce Push-direction nonce, advanced only by a complete frame.
  202. * @param response Caller-owned output containing prior frames.
  203. * @param written Existing byte count, updated by a complete frame.
  204. * @param after Receives the state published once the frame is copied.
  205. * @return True when a frame went out and `after` carries the advanced ladder.
  206. */
  207. bool append_banner_move_notification(Scratch& scratch,
  208. const queuez::SessionState& before,
  209. std::uint64_t selectedCharacter,
  210. std::span<const std::byte, state::kAesKeySize> key,
  211. std::array<std::byte, state::kBapNonceSize>& nonce,
  212. std::span<std::byte> response,
  213. std::size_t& written,
  214. queuez::SessionState& after) noexcept {
  215. bool publish = false;
  216. bool incremental = false;
  217. after = before;
  218. // A pick that names the character the pair already holds owes nothing, and so does a family
  219. // zero that has not had its first delivery yet.
  220. const char* reason = nullptr;
  221. if (!queuez::stage_family0_subscription(
  222. before, selectedCharacter, publish, incremental, after)) {
  223. reason = "stage";
  224. } else if (!publish) {
  225. reason = "unchanged";
  226. } else if (before.family4RootSoid == 0) {
  227. reason = "no_root";
  228. }
  229. if (reason != nullptr) {
  230. report_skip(reason);
  231. after = before;
  232. return false;
  233. }
  234. snapshot::Prepared prepared{};
  235. // A first delivery releases nothing: the Client holds no record for this family yet, so the
  236. // pair goes out as its own full snapshot instead of as a move off a previous character.
  237. if (!snapshot::prepare_banner(scratch,
  238. before.family4RootSoid,
  239. after.family0Version,
  240. incremental ? before.family0Character : 0,
  241. prepared)) {
  242. core::log::write(core::log::Channel::server,
  243. core::log::Level::warn,
  244. "ev=queuez stage=banner_move result=fail reason=prepare");
  245. after = before;
  246. return false;
  247. }
  248. const std::size_t objectCount = prepared.family.objects.size();
  249. const std::size_t beforeBytes = written;
  250. if (!queuez_frame::append(scratch,
  251. prepared.family,
  252. prepared.rawClearSize,
  253. prepared.compressedClearSize,
  254. key,
  255. nonce,
  256. response,
  257. written)) {
  258. core::log::write(core::log::Channel::server,
  259. core::log::Level::warn,
  260. "ev=queuez stage=banner_move result=fail reason=frame");
  261. after = before;
  262. return false;
  263. }
  264. middleware::secure_channel::advance_nonce(nonce);
  265. queuez_report::push("banner_move",
  266. prepared.family.type,
  267. objectCount,
  268. written - beforeBytes,
  269. queuez_report::kNoRecordOutcome);
  270. return true;
  271. }
  272. /** Appends one same-character Family-0 appearance-record upsert after an equipment swap. */
  273. bool append_equipment_appearance_refresh_notification(
  274. Scratch& scratch,
  275. const queuez::CharacterAppearanceRefresh& refresh,
  276. const state::PendingEquipmentSwap& mutation,
  277. std::span<const std::byte, state::kAesKeySize> key,
  278. std::array<std::byte, state::kBapNonceSize>& nonce,
  279. std::span<std::byte> response,
  280. std::size_t& written) noexcept {
  281. if (!mutation.prepared || mutation.characterSoid != refresh.characterSoid) {
  282. return false;
  283. }
  284. snapshot::Prepared prepared{};
  285. if (!snapshot::prepare_character_appearance_refresh(scratch,
  286. refresh,
  287. mutation.afterCharacter,
  288. mutation.characterIndex,
  289. mutation.nativeEquipmentSlot,
  290. false,
  291. prepared)) {
  292. return false;
  293. }
  294. return append_appearance_frame(
  295. scratch, refresh, prepared, "equip_appearance", key, nonce, response, written);
  296. }
  297. /** Appends one Family-0 record upsert after a socket change on an equipped item. */
  298. bool append_socket_appearance_refresh_notification(
  299. Scratch& scratch,
  300. const queuez::CharacterAppearanceRefresh& refresh,
  301. const state::PendingSocketPlug& mutation,
  302. std::span<const std::byte, state::kAesKeySize> key,
  303. std::array<std::byte, state::kBapNonceSize>& nonce,
  304. std::span<std::byte> response,
  305. std::size_t& written) noexcept {
  306. if (!mutation.prepared || !mutation.targetEquipped
  307. || mutation.characterSoid != refresh.characterSoid
  308. || mutation.itemIndex >= mutation.afterCharacter.equipment.slots.size()
  309. || !mutation.afterCharacter.equipment.slots[mutation.itemIndex].has_value()) {
  310. return false;
  311. }
  312. const state::account::inventory::Item& target =
  313. *mutation.afterCharacter.equipment.slots[mutation.itemIndex];
  314. if (target.instanceSoid != mutation.targetInstanceSoid) {
  315. return false;
  316. }
  317. state::build_data::items::details::Definition detail{};
  318. if (!state::build_data::find_configured_item_detail(mutation.targetDefinitionIndex, detail)
  319. || detail.definitionIndex != mutation.targetDefinitionIndex
  320. || detail.definitionHash != mutation.targetDefinitionHash
  321. || detail.bucketId != mutation.targetBucketId || !detail.equipmentSlot.has_value()
  322. || *detail.equipmentSlot < 0
  323. || static_cast<std::size_t>(*detail.equipmentSlot)
  324. >= state::build_data::items::details::kEquipmentSlotCount) {
  325. return false;
  326. }
  327. snapshot::Prepared prepared{};
  328. if (!snapshot::prepare_character_appearance_refresh(
  329. scratch,
  330. refresh,
  331. mutation.afterCharacter,
  332. mutation.characterIndex,
  333. static_cast<std::uint8_t>(*detail.equipmentSlot),
  334. true,
  335. prepared)) {
  336. return false;
  337. }
  338. return append_appearance_frame(
  339. scratch, refresh, prepared, "socket_appearance", key, nonce, response, written);
  340. }
  341. /** Appends the Family-3 character-then-roster refresh owed by one equipment mutation. */
  342. bool append_equipment_roster_refresh_notification(
  343. Scratch& scratch,
  344. const queuez::RosterAppearanceRefresh& refresh,
  345. const state::PendingEquipmentSwap& mutation,
  346. std::span<const std::byte, state::kAesKeySize> key,
  347. std::array<std::byte, state::kBapNonceSize>& nonce,
  348. std::span<std::byte> response,
  349. std::size_t& written) noexcept {
  350. if (!mutation.prepared || !refresh.includeRoster
  351. || mutation.characterSoid != refresh.characterSoid) {
  352. return false;
  353. }
  354. snapshot::Prepared prepared{};
  355. if (!snapshot::prepare_roster_appearance_refresh(
  356. scratch, refresh, mutation.afterCharacter, mutation.characterIndex, prepared)) {
  357. return false;
  358. }
  359. return append_roster_appearance_frame(
  360. scratch, refresh, prepared, "equip_roster", key, nonce, response, written);
  361. }
  362. /** Appends the Family-3 character-only refresh owed by a socket change on equipped gear. */
  363. bool append_socket_roster_refresh_notification(Scratch& scratch,
  364. const queuez::RosterAppearanceRefresh& refresh,
  365. const state::PendingSocketPlug& mutation,
  366. std::span<const std::byte, state::kAesKeySize> key,
  367. std::array<std::byte, state::kBapNonceSize>& nonce,
  368. std::span<std::byte> response,
  369. std::size_t& written) noexcept {
  370. if (!mutation.prepared || !mutation.targetEquipped || refresh.includeRoster
  371. || mutation.characterSoid != refresh.characterSoid
  372. || mutation.itemIndex >= mutation.afterCharacter.equipment.slots.size()
  373. || !mutation.afterCharacter.equipment.slots[mutation.itemIndex].has_value()
  374. || mutation.afterCharacter.equipment.slots[mutation.itemIndex]->instanceSoid
  375. != mutation.targetInstanceSoid) {
  376. return false;
  377. }
  378. snapshot::Prepared prepared{};
  379. if (!snapshot::prepare_roster_appearance_refresh(
  380. scratch, refresh, mutation.afterCharacter, mutation.characterIndex, prepared)) {
  381. return false;
  382. }
  383. return append_roster_appearance_frame(
  384. scratch, refresh, prepared, "socket_roster", key, nonce, response, written);
  385. }
  386. /** Refreshes the selected character's complete Family-0 appearance from committed State. */
  387. bool append_account_resync_appearance_notification(
  388. Scratch& scratch,
  389. const queuez::SessionState& before,
  390. std::span<const std::byte, state::kAesKeySize> key,
  391. std::array<std::byte, state::kBapNonceSize>& nonce,
  392. std::span<std::byte> response,
  393. std::size_t& written,
  394. queuez::SessionState& after) noexcept {
  395. after = before;
  396. if (!before.family0Active) {
  397. return true;
  398. }
  399. const state::AccountState account = state::account_snapshot();
  400. const std::uint64_t selected = state::account::selected_character_soid(account);
  401. if (selected == 0) {
  402. return false;
  403. }
  404. if (before.family0Character != selected) {
  405. return append_banner_move_notification(
  406. scratch, before, selected, key, nonce, response, written, after);
  407. }
  408. std::size_t characterIndex = account.characterCount;
  409. for (std::size_t index = 0; index < account.characterCount; ++index) {
  410. if (account.characters[index].soid == selected) {
  411. characterIndex = index;
  412. break;
  413. }
  414. }
  415. queuez::CharacterAppearanceRefresh refresh{};
  416. snapshot::Prepared prepared{};
  417. if (characterIndex >= account.characterCount
  418. || !queuez::stage_character_appearance_refresh(before, selected, refresh)
  419. || !snapshot::prepare_character_appearance_refresh(
  420. scratch, refresh, account.characters[characterIndex], characterIndex, 0, true, prepared)
  421. || !append_appearance_frame(
  422. scratch, refresh, prepared, "peer_resync_appearance", key, nonce, response, written)) {
  423. return false;
  424. }
  425. after = refresh.after;
  426. return true;
  427. }
  428. /** Refreshes the selected character and its account roster from committed State. */
  429. bool append_account_resync_roster_notification(Scratch& scratch,
  430. const queuez::SessionState& before,
  431. std::span<const std::byte, state::kAesKeySize> key,
  432. std::array<std::byte, state::kBapNonceSize>& nonce,
  433. std::span<std::byte> response,
  434. std::size_t& written,
  435. queuez::SessionState& after) noexcept {
  436. after = before;
  437. if (!before.family3Active) {
  438. return true;
  439. }
  440. const state::AccountState account = state::account_snapshot();
  441. const std::uint64_t selected = state::account::selected_character_soid(account);
  442. std::size_t characterIndex = account.characterCount;
  443. for (std::size_t index = 0; index < account.characterCount; ++index) {
  444. if (account.characters[index].soid == selected) {
  445. characterIndex = index;
  446. break;
  447. }
  448. }
  449. queuez::RosterAppearanceRefresh refresh{};
  450. snapshot::Prepared prepared{};
  451. if (selected == 0 || characterIndex >= account.characterCount
  452. || !queuez::stage_roster_appearance_refresh(before, selected, true, refresh)
  453. || !snapshot::prepare_roster_appearance_refresh(
  454. scratch, refresh, account.characters[characterIndex], characterIndex, prepared)
  455. || !append_roster_appearance_frame(
  456. scratch, refresh, prepared, "peer_resync_roster", key, nonce, response, written)) {
  457. return false;
  458. }
  459. after = refresh.after;
  460. return true;
  461. }
  462. } // namespace sunrise::server::bap::encrypted::push