| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267 |
- using Invercargill;
- using Invercargill.DataStructures;
- using InvercargillJson;
- using Inversion;
- using Astralis;
- namespace Statum {
- /**
- * Resolves the slots the client currently holds from the request's
- * `X-Statum-Slot` headers into a type-name → {@link HeldSlot} map.
- *
- * Two header forms are supported (see `model.md`):
- *
- * - **Reference form** (`key=…; as_at=…`): the snapshot is still within its
- * `transmit_after` window, so the client sends only a reference. The
- * resolver re-uses the server-side cache via {@link StateService.get_slot}
- * and skips the slot if it is absent.
- *
- * - **Base64 frame form**: the snapshot's `transmit_after == as_at`
- * (always-send), so the client sends the full frame. The resolver
- * base64-decodes it, verifies the signature, parses the snapshot and
- * decrypts the private payload.
- *
- * Only slots whose data the client actually sent are surfaced (scoped to the
- * `X-Statum-Slot` headers), and each is indexed by its slot type name.
- */
- public class HeldSlotResolver : Object {
- private StateService state_service = inject<StateService>();
- private Cryptography.SigningProvider signing_provider = inject<Cryptography.SigningProvider>();
- private Cryptography.EncryptionProvider encryption_provider = inject<Cryptography.EncryptionProvider>();
- private const string SLOT_HEADER = "X-Statum-Slot";
- /**
- * Resolves every `X-Statum-Slot` header on `http_context`.
- *
- * - **Reference form** (`key=…; as_at=…`) for a slot still cached
- * server-side → resolved into {@link ResolvedSlots.held}.
- * - **Reference form** for a slot no longer in memory → its key is added
- * to {@link ResolvedSlots.transmit_keys}; the caller emits a
- * `transmit(retry)` so the client re-sends the frame (the slot post
- * endpoint re-verifies and re-caches it).
- * - **Base64 frame form** that verifies → resolved into
- * {@link ResolvedSlots.held}.
- * - **Base64 frame form** that fails verification (or is expired) → its
- * frame `signature` (read off the envelope, without trusting
- * `content`) is added to {@link ResolvedSlots.invalid_signatures}; the
- * caller emits an `invalidate`.
- *
- * Browsers coalesce multiple `X-Statum-Slot` headers into a single
- * comma-separated header value; neither form contains a comma, so each
- * header value is additionally split on `,`.
- */
- public ResolvedSlots resolve(HttpContext http_context) {
- var result = new ResolvedSlots();
- foreach (var header_value in http_context.request.headers.get_or_empty(SLOT_HEADER)) {
- foreach (var part in header_value.split(",")) {
- var slot_value = part.strip();
- if (slot_value.length == 0) {
- continue;
- }
- HeldSlot? slot = null;
- if (slot_value.contains("key=")) {
- slot = resolve_reference(slot_value, result);
- if (slot == null || ((!)slot).type_name == null) {
- var key = parse_reference_key(slot_value);
- if (key != null) {
- result.transmit_keys.add((!)key);
- }
- continue;
- }
- } else {
- slot = resolve_frame(slot_value);
- if (slot == null || ((!)slot).type_name == null) {
- var signature = parse_frame_signature(slot_value);
- if (signature != null) {
- result.invalid_signatures.add((!)signature);
- }
- continue;
- }
- }
- result.held.set(((!)slot).type_name, (!)slot);
- }
- }
- return result;
- }
- /** Extracts the `key` from a `key=…; as_at=…` reference header value. */
- private static string? parse_reference_key(string slot_value) {
- foreach (var part in slot_value.split(";")) {
- var trimmed = part.strip();
- if (trimmed.has_prefix("key=")) {
- var key = trimmed.substring(4).strip();
- return key.length > 0 ? key : null;
- }
- }
- return null;
- }
- /**
- * Reads the `signature` off a base64 frame envelope WITHOUT trusting or
- * parsing its `content`, so an untrustworthy/corrupt frame can still be
- * reported for invalidation. Returns `null` if the envelope itself
- * cannot be parsed.
- */
- private static string? parse_frame_signature(string slot_value) {
- try {
- var bytes = Base64.decode(slot_value);
- var frame_json = to_null_terminated_string(bytes);
- var signature = new JsonElement.from_string(frame_json).as<JsonObject>().get_string("signature");
- return (signature != null && ((!)signature).length > 0) ? signature : null;
- } catch (GLib.Error e) {
- return null;
- }
- }
- private HeldSlot? resolve_reference(string header_value, ResolvedSlots result) {
- string? key = null;
- DateTime? as_at = null;
- foreach (var part in header_value.split(";")) {
- var trimmed = part.strip();
- if (trimmed.has_prefix("key=")) {
- key = trimmed.substring(4).strip();
- } else if (trimmed.has_prefix("as_at=")) {
- var raw = trimmed.substring(6).strip();
- as_at = new DateTime.from_iso8601(raw, new TimeZone.utc());
- }
- }
- if (key == null) {
- return null;
- }
- var slot = state_service.get_slot((!)key);
- if (slot == null || slot.current_state == null) {
- return null;
- }
- var state = slot.current_state;
- DateTime as_at_value = as_at;
- if (as_at_value == null) {
- if (state.last_touched != null) {
- as_at_value = state.last_touched;
- } else {
- as_at_value = new DateTime.now_utc();
- }
- }
- // Detect staleness: if the client's as_at predates the slot's
- // last_touched, the client holds an outdated snapshot.
- if (as_at != null && state.last_touched != null
- && ((!)as_at).compare(state.last_touched) < 0) {
- result.stale_keys.add(slot.id);
- }
- return new HeldSlot() {
- key = slot.id,
- type_name = slot.type_name,
- scope = slot.scope,
- @public = state.public_data ?? new PropertyDictionary(),
- @private = state.private_data ?? new PropertyDictionary(),
- as_at = as_at_value
- };
- }
- private HeldSlot? resolve_frame(string header_value) {
- string frame_json;
- try {
- var bytes = Base64.decode(header_value.strip());
- frame_json = to_null_terminated_string(bytes);
- } catch (GLib.Error e) {
- return null;
- }
- Model.FrameDto frame;
- Model.SnapshotDto snapshot;
- try {
- var frame_element = new JsonElement.from_string(frame_json);
- frame = Model.FrameDto.get_mapper().materialise(frame_element.as<JsonObject>());
- signing_provider.verify_frame(frame);
- var snapshot_element = new JsonElement.from_string(frame.content);
- snapshot = Model.SnapshotDto.get_mapper().materialise(snapshot_element.as<JsonObject>());
- } catch (GLib.Error e) {
- return null;
- }
- // Drop snapshots that have passed their invalid_after time.
- if (snapshot.invalid_after != null) {
- var now = new DateTime.now_utc();
- if (((!)snapshot.invalid_after).compare(now) < 0) {
- return null;
- }
- }
- var private_data = decrypt_private(snapshot);
- return new HeldSlot() {
- key = snapshot.slot.key,
- type_name = snapshot.slot.slot_type,
- scope = snapshot.slot.scope,
- @public = snapshot.@public ?? new PropertyDictionary(),
- @private = private_data,
- as_at = snapshot.as_at,
- transmit_after = snapshot.transmit_after,
- invalid_after = snapshot.invalid_after
- };
- }
- private Properties decrypt_private(Model.SnapshotDto snapshot) {
- if (snapshot.@private == null) {
- return new PropertyDictionary();
- }
- try {
- return encryption_provider.read_properties(snapshot.slot.key, (!)snapshot.@private);
- } catch (GLib.Error e) {
- return new PropertyDictionary();
- }
- }
- private static string to_null_terminated_string(uint8[] bytes) {
- var copy = new uint8[bytes.length + 1];
- Memory.copy(copy, bytes, bytes.length);
- return (string) copy;
- }
- }
- /**
- * The outcome of resolving a request's `X-Statum-Slot` headers.
- */
- public class ResolvedSlots : Object {
- /** Successfully resolved held slots, indexed by type name. */
- public Dictionary<string, HeldSlot> held { get; private set; default = new Dictionary<string, HeldSlot>(); }
- /**
- * Keys of reference-form slots no longer in the server's memory; the
- * caller emits a `transmit(retry)` for each so the client re-sends the
- * frame for the server to re-cache.
- */
- public Series<string> transmit_keys { get; private set; default = new Series<string>(); }
- /**
- * Signatures of frame-form slots that failed verification (or expired);
- * the caller emits an `invalidate` for each so the client drops the
- * matching held slot.
- */
- public Series<string> invalid_signatures { get; private set; default = new Series<string>(); }
- /**
- * Keys of cached slots whose client `as_at` predates the slot's
- * `last_touched`; the caller auto-signs and appends a `set` so the
- * client refreshes its stale snapshot.
- */
- public Series<string> stale_keys { get; private set; default = new Series<string>(); }
- }
- }
|