queuez_subscription.cpp 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285
  1. #include <array>
  2. #include <limits>
  3. #include "../../../../../core/logging/log.h"
  4. #include "../../../../../middleware/secure_channel/runtime.h"
  5. #include "../../../../../state/runtime/runtime.h"
  6. #include "../../queuez/queuez_state_validation.h"
  7. #include "../snapshot/snapshot.h"
  8. #include "queuez_push_reporting.h"
  9. #include "queuez_update_frame.h"
  10. namespace sunrise::server::bap::encrypted::push {
  11. namespace {
  12. /**
  13. * Appends the unsolicited Family-4 companion of a Family-3 subscription.
  14. * @param scratch Lock-owned transform buffers.
  15. * @param before Queuez state visible to the current BAP peer.
  16. * @param familyRootSoid Root the Client subscribed for Family 3.
  17. * @param key Active AES-GCM session key.
  18. * @param nonce Push-direction nonce, advanced only by a complete frame.
  19. * @param response Caller-owned output containing prior frames.
  20. * @param written Existing byte count, updated by a complete frame.
  21. * @param after Receives the queuez state published when the companion succeeds.
  22. * @return True when the companion frame is appended.
  23. */
  24. [[nodiscard]] bool append_family4_companion(Scratch& scratch,
  25. const queuez::SessionState& before,
  26. std::uint64_t familyRootSoid,
  27. std::span<const std::byte, state::kAesKeySize> key,
  28. std::array<std::byte, state::kBapNonceSize>& nonce,
  29. std::span<std::byte> response,
  30. std::size_t& written,
  31. queuez::SessionState& after) noexcept {
  32. middleware::queuez::Subscription companion{};
  33. companion.familyType = queuez::kAccountFamilyType;
  34. companion.familyRootSoid = familyRootSoid;
  35. snapshot::Prepared prepared{};
  36. if (!snapshot::prepare_initial(scratch, companion, {}, prepared)) {
  37. core::log::write(core::log::Channel::server,
  38. core::log::Level::warn,
  39. "ev=queuez stage=companion result=fail reason=prepare");
  40. return false;
  41. }
  42. // The record is still state 1 DECLARED at this point, and only state 2 accepts a snapshot.
  43. // The first copy is expected to be rejected and the delayed copy is the one that lands,
  44. // so a refused staging still sends the frame and still owes the re-push.
  45. queuez::SessionState staged = before;
  46. const bool resident = !prepared.family.objects.empty();
  47. const bool recorded =
  48. resident && queuez::stage_family4_snapshot(before, prepared.family, staged);
  49. if (!recorded) {
  50. staged = before;
  51. }
  52. if (!queuez_frame::append(scratch,
  53. prepared.family,
  54. prepared.rawClearSize,
  55. prepared.compressedClearSize,
  56. key,
  57. nonce,
  58. response,
  59. written)) {
  60. core::log::write(core::log::Channel::server,
  61. core::log::Level::warn,
  62. "ev=queuez stage=companion result=fail reason=frame");
  63. return false;
  64. }
  65. middleware::secure_channel::advance_nonce(nonce);
  66. after = staged;
  67. return true;
  68. }
  69. } // namespace
  70. /** Appends one current full account snapshot at the peer's next Family-4 version. */
  71. bool append_account_resync_notification(
  72. Scratch& scratch,
  73. const queuez::SessionState& before,
  74. std::span<const queuez::AcquisitionPresentationRow> acquisitionPresentationRows,
  75. std::span<const std::byte, state::kAesKeySize> key,
  76. std::array<std::byte, state::kBapNonceSize>& nonce,
  77. std::span<std::byte> response,
  78. std::size_t& written,
  79. queuez::SessionState& after) noexcept {
  80. after = before;
  81. ensure_account_canonical();
  82. if (!queuez::valid(before) || !before.family4Active || before.family4RootSoid == 0
  83. || before.family4Version == (std::numeric_limits<std::int32_t>::max)()) {
  84. return false;
  85. }
  86. snapshot::Prepared prepared{};
  87. if (!snapshot::prepare_family4_refresh(scratch,
  88. before.family4RootSoid,
  89. before.family4Version + 1,
  90. acquisitionPresentationRows,
  91. prepared)
  92. || !queuez::stage_family4_refresh(before, prepared.family, after)) {
  93. return false;
  94. }
  95. if (!queuez_frame::append(scratch,
  96. prepared.family,
  97. prepared.rawClearSize,
  98. prepared.compressedClearSize,
  99. key,
  100. nonce,
  101. response,
  102. written)) {
  103. after = before;
  104. return false;
  105. }
  106. middleware::secure_channel::advance_nonce(nonce);
  107. return true;
  108. }
  109. /**
  110. * Stages the snapshots one subscription needs.
  111. * Every step reports and continues. The subscribe is answered whether or not a frame is built.
  112. * @param scratch Lock-owned transform buffers.
  113. * @param before Queuez state visible to the current BAP peer.
  114. * @param subscription Family the Client picked.
  115. * @param key Active AES-GCM session key.
  116. * @param nonce Push-direction nonce, advanced once per appended frame.
  117. * @param response Caller-owned output containing the existing response prefix.
  118. * @param written Existing byte count, updated after each complete push.
  119. * @param after Receives the queuez state published after caller output is copied.
  120. * @param armsRepush Receives whether the Family-4 companion owes its delayed second copy.
  121. * @param armsBannerRepush Receives whether a family-zero body owes its delayed second copy.
  122. */
  123. void append_queuez_notification(Scratch& scratch,
  124. const queuez::SessionState& before,
  125. const middleware::queuez::Subscription& subscription,
  126. std::span<const std::byte, state::kAesKeySize> key,
  127. std::array<std::byte, state::kBapNonceSize>& nonce,
  128. std::span<std::byte> response,
  129. std::size_t& written,
  130. queuez::SessionState& after,
  131. bool& armsRepush,
  132. bool& armsBannerRepush) noexcept {
  133. after = before;
  134. armsRepush = false;
  135. armsBannerRepush = false;
  136. // Ahead of the dispatch below, not inside one family's builder: family zero reads the account
  137. // directly and family three is built before the family-four companion, so a migration run any
  138. // later would leave the three images describing different accounts.
  139. ensure_account_canonical();
  140. if (subscription.familyType == queuez::kAccountFamilyType && before.family4Active
  141. && before.family4Version != queuez::kInitialFamilyVersion) {
  142. // Our mirror of the Client's records is an observation, not an authority on what may be
  143. // sent. It reports and the frame still goes out. The Client owns the accept decision.
  144. queuez_report::subscription_state("session");
  145. }
  146. bool publish = true;
  147. bool incremental = false;
  148. queuez::SessionState stagedAfter = before;
  149. if (subscription.familyType == queuez::kRosterFamilyType
  150. && !queuez::stage_family3_subscription(before, subscription, publish, stagedAfter)) {
  151. queuez_report::subscription_state("stage_family3");
  152. stagedAfter = before;
  153. // A failed mirror check must never send a version-zero roster into an active incremental
  154. // ladder. The correlated subscription response still goes out without a stale snapshot.
  155. publish = false;
  156. }
  157. snapshot::Prepared prepared{};
  158. if (subscription.familyType == queuez::kBannerFamilyType) {
  159. // Family zero's version and flags come from this peer's own ladder, so it is prepared
  160. // here instead of through the generic initial-snapshot path.
  161. const state::AccountState account = state::account_snapshot();
  162. // The first character stands in before any pick. The record accepts a snapshot only in the
  163. // short window the subscribe opens, so holding the answer for the pick spends that window
  164. // and the subscription times out. The pick moves the pair afterwards.
  165. const std::uint64_t selected = state::account::banner_character_soid(account);
  166. if (selected == 0) {
  167. stagedAfter.pendingBannerRoot = subscription.familyRootSoid;
  168. queuez_report::subscription_state("nocharacter");
  169. after = stagedAfter;
  170. return;
  171. }
  172. stagedAfter.pendingBannerRoot = 0;
  173. if (!queuez::stage_family0_subscription(
  174. before, selected, publish, incremental, stagedAfter)) {
  175. queuez_report::subscription_state("stage_family0");
  176. stagedAfter = before;
  177. stagedAfter.pendingBannerRoot = 0;
  178. incremental = false;
  179. }
  180. // The unsolicited pair records its own delivery, so a later explicit subscribe finds the
  181. // ladder already holding this character. The Client asked, so it is answered regardless.
  182. publish = true;
  183. if (!snapshot::prepare_banner(scratch,
  184. subscription.familyRootSoid,
  185. stagedAfter.family0Version,
  186. incremental ? before.family0Character : 0,
  187. prepared)) {
  188. queuez_report::subscription_failure("prepare_banner");
  189. return;
  190. }
  191. } else if (!snapshot::prepare_initial(scratch, subscription, {}, prepared)) {
  192. queuez_report::subscription_failure("prepare");
  193. return;
  194. }
  195. // An empty snapshot is sent without claiming a resident manifest.
  196. if (subscription.familyType == queuez::kAccountFamilyType && !prepared.family.objects.empty()
  197. && !queuez::stage_family4_snapshot(before, prepared.family, stagedAfter)) {
  198. queuez_report::subscription_state("stage_family4");
  199. stagedAfter = before;
  200. }
  201. // An empty full snapshot prunes the family to nothing. Wiping the roster closes the gate the
  202. // family-zero source list is emitted from. Every other family needs the empty snapshot: it is
  203. // what moves a record with no body to synced.
  204. if (prepared.family.objects.empty() && subscription.familyType == queuez::kRosterFamilyType) {
  205. queuez_frame::clear_object_storage(
  206. scratch, prepared.rawClearSize, prepared.compressedClearSize);
  207. queuez_report::subscription_state("empty");
  208. after = stagedAfter;
  209. return;
  210. }
  211. if (!publish) {
  212. // Response-only suppression still builds the live snapshot, which names the root.
  213. queuez_frame::clear_object_storage(
  214. scratch, prepared.rawClearSize, prepared.compressedClearSize);
  215. after = stagedAfter;
  216. return;
  217. }
  218. if (subscription.familyType == queuez::kRosterFamilyType
  219. && (!stagedAfter.family3Active || stagedAfter.family3RootSoid != subscription.familyRootSoid
  220. || stagedAfter.family3Version != queuez::kInitialFamilyVersion
  221. || prepared.family.type != queuez::kRosterFamilyType
  222. || prepared.family.rootSoid != stagedAfter.family3RootSoid
  223. || prepared.family.version != stagedAfter.family3Version
  224. || prepared.family.flags != middleware::queuez::kFullSnapshotFlag)) {
  225. queuez_report::subscription_failure("family3_ladder");
  226. return;
  227. }
  228. if (!queuez_frame::append(scratch,
  229. prepared.family,
  230. prepared.rawClearSize,
  231. prepared.compressedClearSize,
  232. key,
  233. nonce,
  234. response,
  235. written)) {
  236. queuez_report::subscription_failure("frame");
  237. return;
  238. }
  239. middleware::secure_channel::advance_nonce(nonce);
  240. after = stagedAfter;
  241. // The client sends its subscribe just before it writes the record state, so this first copy
  242. // arrives while the record still reads its previous state and is refused. Family zero has
  243. // nothing else behind it, so the delayed copy is the one that lands.
  244. armsBannerRepush = subscription.familyType == queuez::kBannerFamilyType;
  245. if (subscription.familyType == queuez::kRosterFamilyType && !stagedAfter.family4Active) {
  246. queuez::SessionState companionAfter{};
  247. if (append_family4_companion(scratch,
  248. stagedAfter,
  249. subscription.familyRootSoid,
  250. key,
  251. nonce,
  252. response,
  253. written,
  254. companionAfter)) {
  255. after = companionAfter;
  256. armsRepush = true;
  257. }
  258. // The banner pair follows family four, last in the burst, which is retail's order.
  259. const queuez::SessionState bannerBefore = after;
  260. queuez::SessionState bannerDelivered{};
  261. if (append_banner_notification(scratch,
  262. bannerBefore,
  263. subscription.familyRootSoid,
  264. key,
  265. nonce,
  266. response,
  267. written,
  268. bannerDelivered)) {
  269. after = bannerDelivered;
  270. armsBannerRepush = true;
  271. }
  272. }
  273. }
  274. } // namespace sunrise::server::bap::encrypted::push