StatumPage.vala 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160
  1. using Invercargill;
  2. using Invercargill.DataStructures;
  3. using Invercargill.Expressions;
  4. using Inversion;
  5. using Astralis;
  6. namespace Statum {
  7. /**
  8. * Outcome of {@link StatumPage.select_variant}: either the index of the
  9. * pre-rendered variant to serve, or a guard failure (optional redirect or an
  10. * error page at a status).
  11. */
  12. public class StatumVariantSelection : Object {
  13. public bool guard_failed { get; private set; }
  14. public string? redirect { get; private set; }
  15. public StatusCode status { get; private set; default = StatusCode.FORBIDDEN; }
  16. public int variant_index { get; private set; }
  17. /** Selects the variant at `index`. */
  18. public static StatumVariantSelection variant(int index) {
  19. var selection = new StatumVariantSelection();
  20. selection.variant_index = index;
  21. return selection;
  22. }
  23. /** Signals a guard failure that should redirect to `redirect` (HTTP 302). */
  24. public static StatumVariantSelection redirect_to(string redirect) {
  25. var selection = new StatumVariantSelection();
  26. selection.guard_failed = true;
  27. selection.redirect = redirect;
  28. return selection;
  29. }
  30. /** Signals a guard failure that should serve an error page at `status`. */
  31. public static StatumVariantSelection failure(StatusCode status = StatusCode.FORBIDDEN) {
  32. var selection = new StatumVariantSelection();
  33. selection.guard_failed = true;
  34. selection.status = status;
  35. return selection;
  36. }
  37. }
  38. /**
  39. * Base class for pre-rendered Statum pages.
  40. *
  41. * A generated subclass (emitted by `statum-mkpstm`) extends this, providing
  42. * the precompressed variant byte arrays and a compiled {@link select_variant}
  43. * that evaluates `pstm-guard`s and the `pstm-if` matrix against the request's
  44. * held slots. This base supplies header parsing → held slots, encoding
  45. * negotiation, ETag handling and guard redirect/error-page serving.
  46. */
  47. public abstract class StatumPage : Object, Endpoint {
  48. protected HeldSlotResolver resolver = inject<HeldSlotResolver>();
  49. /** Content type of the served page (normally `text/html`). */
  50. public abstract string content_type { get; }
  51. /**
  52. * Evaluates the page's guards and `pstm-if` matrix against the held
  53. * slots, returning the variant to serve or a guard failure.
  54. */
  55. public abstract StatumVariantSelection select_variant(ReadOnlyAssociative<string, HeldSlot> held) throws GLib.Error;
  56. /** Best supported encoding for the chosen variant. */
  57. public abstract string get_best_encoding(int variant, Set<string> supported);
  58. /** The bytes for `variant` under `encoding`. */
  59. public abstract unowned uint8[] get_encoding(int variant, string encoding);
  60. /** The ETag for `variant` under `encoding`. */
  61. public abstract string get_etag_for(int variant, string encoding);
  62. public async HttpResult handle_request(HttpContext http_context, RouteContext route_context) throws GLib.Error {
  63. var held = resolver.resolve(http_context).held;
  64. StatumVariantSelection selection;
  65. try {
  66. selection = select_variant(held);
  67. } catch (GLib.Error e) {
  68. return new HttpStringResult(@"Internal error: $(e.message)", StatusCode.INTERNAL_SERVER_ERROR);
  69. }
  70. if (selection.guard_failed) {
  71. return serve_guard_failure(selection);
  72. }
  73. var accepts = parse_accept_encoding(http_context);
  74. var best = get_best_encoding(selection.variant_index, accepts);
  75. var etag = get_etag_for(selection.variant_index, best);
  76. if (etag_matches(http_context, etag)) {
  77. return new HttpEmptyResult(StatusCode.NOT_MODIFIED);
  78. }
  79. var bytes = get_encoding(selection.variant_index, best);
  80. var result = new HttpDataResult(Wrap.byte_array(bytes));
  81. result.set_header("Content-Type", content_type);
  82. result.set_header("Content-Encoding", best);
  83. result.set_header("ETag", etag);
  84. return result;
  85. }
  86. private static HttpResult serve_guard_failure(StatumVariantSelection selection) {
  87. if (selection.redirect != null) {
  88. var result = new HttpEmptyResult((StatusCode) 302);
  89. result.set_header("Location", (!)selection.redirect);
  90. return result;
  91. }
  92. return new HttpStringResult("Forbidden", selection.status);
  93. }
  94. private static Set<string> parse_accept_encoding(HttpContext http_context) {
  95. var supported = new HashSet<string>();
  96. foreach (var header in http_context.request.headers.get_or_empty("Accept-Encoding")) {
  97. foreach (var token in header.split(",")) {
  98. var encoding = token.strip();
  99. if (encoding.length > 0) {
  100. supported.add(encoding);
  101. }
  102. }
  103. }
  104. return supported;
  105. }
  106. private static bool etag_matches(HttpContext http_context, string etag) {
  107. foreach (var header in http_context.request.headers.get_or_empty("If-None-Match")) {
  108. if (header.contains(etag)) {
  109. return true;
  110. }
  111. }
  112. return false;
  113. }
  114. }
  115. /**
  116. * Registers a {@link PropertiesPropertyAccessor} for any {@link Element}
  117. * wrapping a {@link Properties} value, so that `pstm-if`/`pstm-guard`
  118. * predicate evaluation (the runtime-evaluator fallback) can read members off
  119. * a slot's public/private data — e.g. `auth.role` or `auth.private.score`.
  120. *
  121. * Registered once by {@link StatumModule}.
  122. */
  123. public class StatumPropertiesAccessorFactory : Object, PropertyAccessorFactory {
  124. public PropertyAccessor? create(Element element) {
  125. Properties properties;
  126. if (element.try_get_as<Properties>(out properties)) {
  127. return new PropertiesPropertyAccessor(properties);
  128. }
  129. return null;
  130. }
  131. }
  132. }