| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245 |
- using Invercargill;
- using Invercargill.DataStructures;
- using Inversion;
- using Astralis;
- namespace Statum {
- /**
- * Base class for Statum entrypoint handlers.
- *
- * After a page is served its HTML boots the Statum JS, which GETs
- * `/_statum/entrypoint?uri=…`. The framework resolves the entrypoint bound
- * to that URI (see {@link StatumConfigurator.add_page}) and calls
- * {@link handle}, whose returned {@link DirectiveBuilder} directives hydrate
- * the page.
- *
- * Subclasses start a builder via {@link directives}, chain directives onto
- * it, and return it. They are given the {@link request}, {@link state_service},
- * {@link channel_service} and {@link action_registry} (for authoring action
- * references to embed in state updates).
- */
- public abstract class StatumEntrypoint : Object {
- /** The per-request Statum context (registered scoped by the framework). */
- protected StatumRequest request = inject<StatumRequest>();
- /** The singleton state service. */
- protected StateService state_service = inject<StateService>();
- /** The singleton realtime channel service. */
- protected ChannelService channel_service = inject<ChannelService>();
- /** The singleton action registry (for authoring action references). */
- protected ActionRegistry action_registry = inject<ActionRegistry>();
- /** The singleton topic registry (for watching topics). */
- protected TopicRegistry topic_registry = inject<TopicRegistry>();
- /**
- * Returns the directive builder that hydrates the page bound to this
- * entrypoint. The framework calls {@link DirectiveBuilder.build} on it.
- */
- public abstract async DirectiveBuilder handle() throws GLib.Error;
- /** Starts a fluent {@link DirectiveBuilder} bound to the state service and held slots. */
- protected DirectiveBuilder directives() {
- return new DirectiveBuilder(state_service, request.held);
- }
- }
- /**
- * Base class for Statum action handlers.
- *
- * Actions are auto-mapped to GUID endpoints (`/_statum/action/{guid}`) at
- * registration. The framework routes a request to the matching action, this
- * base resolves the held slots, decrypts the `X-Statum-Private` header into
- * {@link StatumRequest.action_private} (under {@link private_namespace}),
- * parses the form body, builds the {@link request}, then invokes
- * {@link handle} and renders the builder's directives.
- *
- * For typed private data, extend {@link TypedStatumAction} instead.
- */
- public abstract class StatumAction : Object, Endpoint {
- /** Namespace used by the non-generic base (untyped action-private). */
- public const string ACTION_PRIVATE_NAMESPACE = "action-private";
- protected HeldSlotResolver resolver = inject<HeldSlotResolver>();
- protected Cryptography.EncryptionProvider encryption_provider = inject<Cryptography.EncryptionProvider>();
- protected StateService state_service = inject<StateService>();
- protected ChannelService channel_service = inject<ChannelService>();
- protected ActionRegistry action_registry = inject<ActionRegistry>();
- protected TopicRegistry topic_registry = inject<TopicRegistry>();
- protected Inversion.Scope scope = inject<Inversion.Scope>();
- /** The per-request Statum context (populated before {@link handle}). */
- protected StatumRequest request;
- /**
- * Namespace under which this action's private blobs are sealed/read.
- * Overridden by {@link StatumAction<TPrivate>} to the action's type name.
- */
- protected virtual string private_namespace { get { return ACTION_PRIVATE_NAMESPACE; } }
- /** Whether valid `X-Statum-Private` is required (true for typed actions). */
- protected virtual bool requires_valid_private { get { return false; } }
- /** Error message returned when private data can't be verified (overridable). */
- protected virtual string private_error_message { get { return "Action private data could not be verified"; } }
- /** Error code returned when private data can't be verified (overridable). */
- protected virtual string? private_error_code { get { return "bad_private"; } }
- /**
- * Returns the directive builder for this action's response (e.g. a
- * refreshed `set` for a mutated slot, plus any `notify`/`navigate`).
- */
- public abstract async DirectiveBuilder handle() throws GLib.Error;
- /** Starts a fluent {@link DirectiveBuilder} bound to the state service and held slots. */
- protected DirectiveBuilder directives() {
- return new DirectiveBuilder(state_service, request.held);
- }
- public async HttpResult handle_request(HttpContext http_context, RouteContext route_context) throws GLib.Error {
- var resolved = resolver.resolve(http_context);
- // If the client referenced slots the server no longer has cached, ask
- // it to re-send those frames (then retry) before processing the action.
- if (resolved.transmit_keys.to_array().length > 0) {
- return StatumDirectives.transmit_result(resolved.transmit_keys, resolved.invalid_signatures);
- }
- FormData? form = null;
- var content_length_header = http_context.request.get_header("Content-Length");
- var content_length = content_length_header != null ? int.parse((!)content_length_header) : 0;
- if (content_length > 0 && (
- http_context.request.is_form_urlencoded() || http_context.request.is_multipart())) {
- form = yield FormDataParser.parse(http_context.request.request_body, http_context.request.content_type);
- }
- bool private_failed;
- var action_private = decrypt_action_private(http_context, out private_failed);
- request = new StatumRequest() {
- uri = route_context.requested_path,
- route_params = route_context.mapped_parameters,
- held = resolved.held,
- query = http_context.request.query_params,
- action_private = action_private,
- form = form
- };
- scope.register_local_scoped<StatumRequest>(() => request);
- // For typed actions, a missing or undecryptable X-Statum-Private is a
- // hard failure: return an error directive and do not run the handler.
- if (requires_valid_private && private_failed) {
- var directives = new Series<Model.DirectiveDto>();
- directives.add(new Model.ErrorDirectiveDto() {
- message = private_error_message,
- code = private_error_code
- });
- return StatumDirectives.to_result(directives, resolved.invalid_signatures, state_service, resolved.stale_keys);
- }
- var builder = yield handle();
- var directives = yield builder.build();
- return StatumDirectives.to_result(directives, resolved.invalid_signatures, state_service, resolved.stale_keys);
- }
- private Properties decrypt_action_private(HttpContext http_context, out bool failed) {
- failed = false;
- var blob = http_context.request.get_header("X-Statum-Private");
- if (blob == null || ((!)blob).length == 0) {
- failed = true;
- return new PropertyDictionary();
- }
- try {
- return encryption_provider.read_properties(private_namespace, (!)blob);
- } catch (GLib.Error e) {
- failed = true;
- warning("[Statum] Could not decrypt the X-Statum-Private header under namespace \"%s\" (action %s): %s",
- private_namespace, this.get_type().name(), e.message);
- return new PropertyDictionary();
- }
- }
- }
- /**
- * Base class for Statum action handlers that carry strongly-typed private
- * data.
- *
- * `TPrivate` is an ordinary GObject property class (e.g.
- * `public string order_id { get; set; }`); the framework maps it to/from the
- * sealed blob via GObject introspection ({@link GObjectMapping}). The seal
- * namespace is the action's type name, so one action type cannot decrypt
- * another's private blob.
- *
- * Read the decrypted, typed private data via {@link request_private}.
- *
- * (Named `TypedStatumAction` rather than `StatumAction<TPrivate>` because
- * Vala does not permit a generic and non-generic class to share a name.)
- */
- public abstract class TypedStatumAction<TPrivate> : StatumAction {
- /** Typed actions require valid `X-Statum-Private`; bad/absent → error directive. */
- protected override bool requires_valid_private { get { return true; } }
- /**
- * The {@link Type} materialised by {@link request_private}.
- *
- * The default returns `typeof(TPrivate)`, which erodes to `void` when
- * the action is instantiated reflectively — the usual case, because
- * registered actions are resolved by GType without their generic type
- * argument. Such subclasses must override this to name their private
- * model directly:
- *
- * ```
- * protected override Type private_type { get { return typeof(MyPrivate); } }
- * ```
- */
- protected virtual Type private_type { get { return typeof(TPrivate); } }
- private Object? _private_data = null;
- private bool _private_resolved = false;
- /**
- * The decrypted, typed private data for this action, or `null` when the
- * blob is absent, undecryptable, or the action's {@link private_type}
- * is not a {@link Object} type (see the erasure note there).
- *
- * The materialised model is held in a plain `Object` field so the
- * reference survives the erased-generic return path (a container-
- * constructed action carries no `TPrivate` dup function, so a direct
- * `(TPrivate)` transfer would drop the only reference).
- */
- protected TPrivate? request_private {
- get {
- if (!_private_resolved) {
- _private_resolved = true;
- var type = private_type;
- if (!type.is_a(typeof(Object))) {
- warning("[Statum] %s: typeof(TPrivate) resolved to \"%s\" at runtime (generic type erasure); "
- + "override `private_type` to return the private model's type so request_private can materialise it.",
- this.get_type().name(), type.name());
- _private_data = null;
- } else {
- try {
- _private_data = GObjectMapping.from_properties(type, request.action_private);
- } catch (GLib.Error e) {
- warning("[Statum] %s: could not materialise typed private data from the decrypted blob: %s",
- this.get_type().name(), e.message);
- _private_data = null;
- }
- }
- }
- return (TPrivate) _private_data;
- }
- }
- protected override string private_namespace {
- get { return this.get_type().name(); }
- }
- }
- }
|