ultra-lite-hub 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (162) hide show
  1. checksums.yaml +7 -0
  2. data/roda-3.105.0/MIT-LICENSE +21 -0
  3. data/roda-3.105.0/lib/roda/cache.rb +42 -0
  4. data/roda-3.105.0/lib/roda/plugins/Integer_matcher_max.rb +55 -0
  5. data/roda-3.105.0/lib/roda/plugins/_after_hook.rb +11 -0
  6. data/roda-3.105.0/lib/roda/plugins/_base64.rb +34 -0
  7. data/roda-3.105.0/lib/roda/plugins/_before_hook.rb +41 -0
  8. data/roda-3.105.0/lib/roda/plugins/_optimized_matching.rb +227 -0
  9. data/roda-3.105.0/lib/roda/plugins/_symbol_class_matchers.rb +108 -0
  10. data/roda-3.105.0/lib/roda/plugins/_symbol_regexp_matchers.rb +24 -0
  11. data/roda-3.105.0/lib/roda/plugins/additional_render_engines.rb +61 -0
  12. data/roda-3.105.0/lib/roda/plugins/additional_view_directories.rb +66 -0
  13. data/roda-3.105.0/lib/roda/plugins/all_verbs.rb +50 -0
  14. data/roda-3.105.0/lib/roda/plugins/assets.rb +902 -0
  15. data/roda-3.105.0/lib/roda/plugins/assets_preloading.rb +89 -0
  16. data/roda-3.105.0/lib/roda/plugins/assume_ssl.rb +28 -0
  17. data/roda-3.105.0/lib/roda/plugins/autoload_hash_branches.rb +79 -0
  18. data/roda-3.105.0/lib/roda/plugins/autoload_named_routes.rb +65 -0
  19. data/roda-3.105.0/lib/roda/plugins/backtracking_array.rb +88 -0
  20. data/roda-3.105.0/lib/roda/plugins/bearer_token.rb +28 -0
  21. data/roda-3.105.0/lib/roda/plugins/branch_locals.rb +74 -0
  22. data/roda-3.105.0/lib/roda/plugins/break.rb +43 -0
  23. data/roda-3.105.0/lib/roda/plugins/caching.rb +206 -0
  24. data/roda-3.105.0/lib/roda/plugins/capture_erb.rb +94 -0
  25. data/roda-3.105.0/lib/roda/plugins/chunked.rb +331 -0
  26. data/roda-3.105.0/lib/roda/plugins/class_level_routing.rb +111 -0
  27. data/roda-3.105.0/lib/roda/plugins/class_matchers.rb +155 -0
  28. data/roda-3.105.0/lib/roda/plugins/common_logger.rb +80 -0
  29. data/roda-3.105.0/lib/roda/plugins/conditional_sessions.rb +67 -0
  30. data/roda-3.105.0/lib/roda/plugins/content_for.rb +103 -0
  31. data/roda-3.105.0/lib/roda/plugins/content_security_policy.rb +329 -0
  32. data/roda-3.105.0/lib/roda/plugins/cookie_flags.rb +157 -0
  33. data/roda-3.105.0/lib/roda/plugins/cookies.rb +51 -0
  34. data/roda-3.105.0/lib/roda/plugins/csrf.rb +81 -0
  35. data/roda-3.105.0/lib/roda/plugins/custom_block_results.rb +85 -0
  36. data/roda-3.105.0/lib/roda/plugins/custom_matchers.rb +89 -0
  37. data/roda-3.105.0/lib/roda/plugins/default_headers.rb +64 -0
  38. data/roda-3.105.0/lib/roda/plugins/default_status.rb +35 -0
  39. data/roda-3.105.0/lib/roda/plugins/delay_build.rb +18 -0
  40. data/roda-3.105.0/lib/roda/plugins/delegate.rb +77 -0
  41. data/roda-3.105.0/lib/roda/plugins/delete_empty_headers.rb +36 -0
  42. data/roda-3.105.0/lib/roda/plugins/direct_call.rb +38 -0
  43. data/roda-3.105.0/lib/roda/plugins/disallow_file_uploads.rb +38 -0
  44. data/roda-3.105.0/lib/roda/plugins/drop_body.rb +43 -0
  45. data/roda-3.105.0/lib/roda/plugins/each_part.rb +87 -0
  46. data/roda-3.105.0/lib/roda/plugins/early_hints.rb +25 -0
  47. data/roda-3.105.0/lib/roda/plugins/empty_root.rb +49 -0
  48. data/roda-3.105.0/lib/roda/plugins/environments.rb +79 -0
  49. data/roda-3.105.0/lib/roda/plugins/erb_h.rb +43 -0
  50. data/roda-3.105.0/lib/roda/plugins/error_email.rb +152 -0
  51. data/roda-3.105.0/lib/roda/plugins/error_handler.rb +133 -0
  52. data/roda-3.105.0/lib/roda/plugins/error_mail.rb +149 -0
  53. data/roda-3.105.0/lib/roda/plugins/exception_page.rb +445 -0
  54. data/roda-3.105.0/lib/roda/plugins/filter_common_logger.rb +46 -0
  55. data/roda-3.105.0/lib/roda/plugins/flash.rb +117 -0
  56. data/roda-3.105.0/lib/roda/plugins/h.rb +59 -0
  57. data/roda-3.105.0/lib/roda/plugins/halt.rb +108 -0
  58. data/roda-3.105.0/lib/roda/plugins/hash_branch_view_subdir.rb +76 -0
  59. data/roda-3.105.0/lib/roda/plugins/hash_branches.rb +145 -0
  60. data/roda-3.105.0/lib/roda/plugins/hash_matcher.rb +39 -0
  61. data/roda-3.105.0/lib/roda/plugins/hash_paths.rb +128 -0
  62. data/roda-3.105.0/lib/roda/plugins/hash_public.rb +125 -0
  63. data/roda-3.105.0/lib/roda/plugins/hash_public_cache.rb +91 -0
  64. data/roda-3.105.0/lib/roda/plugins/hash_routes.rb +304 -0
  65. data/roda-3.105.0/lib/roda/plugins/head.rb +99 -0
  66. data/roda-3.105.0/lib/roda/plugins/header_matchers.rb +88 -0
  67. data/roda-3.105.0/lib/roda/plugins/heartbeat.rb +41 -0
  68. data/roda-3.105.0/lib/roda/plugins/hmac_paths.rb +413 -0
  69. data/roda-3.105.0/lib/roda/plugins/hooks.rb +92 -0
  70. data/roda-3.105.0/lib/roda/plugins/host_authorization.rb +155 -0
  71. data/roda-3.105.0/lib/roda/plugins/host_routing.rb +190 -0
  72. data/roda-3.105.0/lib/roda/plugins/hsts.rb +35 -0
  73. data/roda-3.105.0/lib/roda/plugins/indifferent_params.rb +124 -0
  74. data/roda-3.105.0/lib/roda/plugins/inject_erb.rb +33 -0
  75. data/roda-3.105.0/lib/roda/plugins/invalid_request_body.rb +107 -0
  76. data/roda-3.105.0/lib/roda/plugins/ip_from_header.rb +34 -0
  77. data/roda-3.105.0/lib/roda/plugins/json.rb +119 -0
  78. data/roda-3.105.0/lib/roda/plugins/json_parser.rb +107 -0
  79. data/roda-3.105.0/lib/roda/plugins/link_to.rb +83 -0
  80. data/roda-3.105.0/lib/roda/plugins/mail_processor.rb +629 -0
  81. data/roda-3.105.0/lib/roda/plugins/mailer.rb +283 -0
  82. data/roda-3.105.0/lib/roda/plugins/map_matcher.rb +48 -0
  83. data/roda-3.105.0/lib/roda/plugins/match_affix.rb +64 -0
  84. data/roda-3.105.0/lib/roda/plugins/match_hook.rb +34 -0
  85. data/roda-3.105.0/lib/roda/plugins/match_hook_args.rb +93 -0
  86. data/roda-3.105.0/lib/roda/plugins/middleware.rb +225 -0
  87. data/roda-3.105.0/lib/roda/plugins/middleware_stack.rb +101 -0
  88. data/roda-3.105.0/lib/roda/plugins/module_include.rb +96 -0
  89. data/roda-3.105.0/lib/roda/plugins/multi_public.rb +95 -0
  90. data/roda-3.105.0/lib/roda/plugins/multi_route.rb +156 -0
  91. data/roda-3.105.0/lib/roda/plugins/multi_run.rb +144 -0
  92. data/roda-3.105.0/lib/roda/plugins/multi_view.rb +83 -0
  93. data/roda-3.105.0/lib/roda/plugins/multibyte_string_matcher.rb +57 -0
  94. data/roda-3.105.0/lib/roda/plugins/named_routes.rb +167 -0
  95. data/roda-3.105.0/lib/roda/plugins/named_templates.rb +102 -0
  96. data/roda-3.105.0/lib/roda/plugins/not_allowed.rb +149 -0
  97. data/roda-3.105.0/lib/roda/plugins/not_found.rb +55 -0
  98. data/roda-3.105.0/lib/roda/plugins/optimized_segment_matchers.rb +53 -0
  99. data/roda-3.105.0/lib/roda/plugins/optimized_string_matchers.rb +50 -0
  100. data/roda-3.105.0/lib/roda/plugins/padrino_render.rb +42 -0
  101. data/roda-3.105.0/lib/roda/plugins/param_matchers.rb +83 -0
  102. data/roda-3.105.0/lib/roda/plugins/params_capturing.rb +116 -0
  103. data/roda-3.105.0/lib/roda/plugins/part.rb +76 -0
  104. data/roda-3.105.0/lib/roda/plugins/partials.rb +74 -0
  105. data/roda-3.105.0/lib/roda/plugins/pass.rb +47 -0
  106. data/roda-3.105.0/lib/roda/plugins/path.rb +255 -0
  107. data/roda-3.105.0/lib/roda/plugins/path_matchers.rb +54 -0
  108. data/roda-3.105.0/lib/roda/plugins/path_rewriter.rb +111 -0
  109. data/roda-3.105.0/lib/roda/plugins/permissions_policy.rb +336 -0
  110. data/roda-3.105.0/lib/roda/plugins/placeholder_string_matchers.rb +48 -0
  111. data/roda-3.105.0/lib/roda/plugins/plain_hash_response_headers.rb +32 -0
  112. data/roda-3.105.0/lib/roda/plugins/precompile_templates.rb +156 -0
  113. data/roda-3.105.0/lib/roda/plugins/public.rb +182 -0
  114. data/roda-3.105.0/lib/roda/plugins/r.rb +35 -0
  115. data/roda-3.105.0/lib/roda/plugins/recheck_precompiled_assets.rb +107 -0
  116. data/roda-3.105.0/lib/roda/plugins/redirect_http_to_https.rb +99 -0
  117. data/roda-3.105.0/lib/roda/plugins/redirect_path.rb +59 -0
  118. data/roda-3.105.0/lib/roda/plugins/relative_path.rb +74 -0
  119. data/roda-3.105.0/lib/roda/plugins/render.rb +998 -0
  120. data/roda-3.105.0/lib/roda/plugins/render_coverage.rb +105 -0
  121. data/roda-3.105.0/lib/roda/plugins/render_each.rb +224 -0
  122. data/roda-3.105.0/lib/roda/plugins/render_locals.rb +102 -0
  123. data/roda-3.105.0/lib/roda/plugins/request_aref.rb +77 -0
  124. data/roda-3.105.0/lib/roda/plugins/request_headers.rb +81 -0
  125. data/roda-3.105.0/lib/roda/plugins/response_attachment.rb +94 -0
  126. data/roda-3.105.0/lib/roda/plugins/response_content_type.rb +79 -0
  127. data/roda-3.105.0/lib/roda/plugins/response_request.rb +29 -0
  128. data/roda-3.105.0/lib/roda/plugins/route_block_args.rb +50 -0
  129. data/roda-3.105.0/lib/roda/plugins/route_csrf.rb +388 -0
  130. data/roda-3.105.0/lib/roda/plugins/run_append_slash.rb +51 -0
  131. data/roda-3.105.0/lib/roda/plugins/run_handler.rb +51 -0
  132. data/roda-3.105.0/lib/roda/plugins/run_require_slash.rb +46 -0
  133. data/roda-3.105.0/lib/roda/plugins/sec_fetch_site_csrf.rb +131 -0
  134. data/roda-3.105.0/lib/roda/plugins/send_file.rb +127 -0
  135. data/roda-3.105.0/lib/roda/plugins/sessions.rb +537 -0
  136. data/roda-3.105.0/lib/roda/plugins/shared_vars.rb +86 -0
  137. data/roda-3.105.0/lib/roda/plugins/sinatra_helpers.rb +443 -0
  138. data/roda-3.105.0/lib/roda/plugins/slash_path_empty.rb +26 -0
  139. data/roda-3.105.0/lib/roda/plugins/static.rb +42 -0
  140. data/roda-3.105.0/lib/roda/plugins/static_routing.rb +89 -0
  141. data/roda-3.105.0/lib/roda/plugins/status_303.rb +34 -0
  142. data/roda-3.105.0/lib/roda/plugins/status_handler.rb +86 -0
  143. data/roda-3.105.0/lib/roda/plugins/streaming.rb +170 -0
  144. data/roda-3.105.0/lib/roda/plugins/strip_path_prefix.rb +33 -0
  145. data/roda-3.105.0/lib/roda/plugins/symbol_matchers.rb +188 -0
  146. data/roda-3.105.0/lib/roda/plugins/symbol_status.rb +32 -0
  147. data/roda-3.105.0/lib/roda/plugins/symbol_views.rb +37 -0
  148. data/roda-3.105.0/lib/roda/plugins/timestamp_public.rb +78 -0
  149. data/roda-3.105.0/lib/roda/plugins/type_routing.rb +215 -0
  150. data/roda-3.105.0/lib/roda/plugins/typecast_params.rb +1183 -0
  151. data/roda-3.105.0/lib/roda/plugins/typecast_params_sized_integers.rb +108 -0
  152. data/roda-3.105.0/lib/roda/plugins/unescape_path.rb +41 -0
  153. data/roda-3.105.0/lib/roda/plugins/view_options.rb +201 -0
  154. data/roda-3.105.0/lib/roda/plugins/view_subdir_leading_slash.rb +49 -0
  155. data/roda-3.105.0/lib/roda/plugins.rb +68 -0
  156. data/roda-3.105.0/lib/roda/request.rb +722 -0
  157. data/roda-3.105.0/lib/roda/response.rb +221 -0
  158. data/roda-3.105.0/lib/roda/session_middleware.rb +183 -0
  159. data/roda-3.105.0/lib/roda/version.rb +18 -0
  160. data/roda-3.105.0/lib/roda.rb +586 -0
  161. data/ultra-lite-hub.gemspec +12 -0
  162. metadata +201 -0
@@ -0,0 +1,537 @@
1
+ # frozen-string-literal: true
2
+
3
+ require 'openssl'
4
+
5
+ begin
6
+ OpenSSL::Cipher.new("aes-256-ctr")
7
+ rescue OpenSSL::Cipher::CipherError
8
+ # :nocov:
9
+ raise LoadError, "Roda sessions plugin requires the aes-256-ctr cipher"
10
+ # :nocov:
11
+ end
12
+
13
+ require 'json'
14
+ require 'securerandom'
15
+ require 'zlib'
16
+ require 'rack/utils'
17
+
18
+ class Roda
19
+ module RodaPlugins
20
+ # The sessions plugin adds support for sessions using cookies. It is the recommended
21
+ # way to support sessions in Roda applications.
22
+ #
23
+ # The session cookies are encrypted with AES-256-CTR using a separate encryption key per cookie,
24
+ # and then signed with HMAC-SHA-256. By default, session data is padded to reduce information
25
+ # leaked based on the session size.
26
+ #
27
+ # Sessions are serialized via JSON, so session information should only store data that
28
+ # allows roundtrips via JSON (String, Integer, Float, Array, Hash, true, false, and nil).
29
+ # In particular, note that Symbol does not round trip via JSON, so symbols should not be
30
+ # used in sessions when this plugin is used. This plugin sets the
31
+ # +:sessions_convert_symbols+ application option to +true+ if it hasn't been set yet,
32
+ # for better integration with plugins that can use either symbol or string session or
33
+ # flash keys. Unlike Rack::Session::Cookie, the session is stored as a plain ruby hash,
34
+ # and does not convert all keys to strings.
35
+ #
36
+ # All sessions are timestamped and session expiration is enabled by default, with sessions
37
+ # being valid for 30 days maximum and 7 days since last use by default. Session creation time is
38
+ # reset whenever the session is empty when serialized and also whenever +clear_session+
39
+ # is called while processing the request.
40
+ #
41
+ # Session secrets can be rotated. See options below.
42
+ #
43
+ # The sessions plugin can transparently upgrade sessions from versions of Rack::Session::Cookie
44
+ # shipped with Rack before Rack 3,
45
+ # if the default Rack::Session::Cookie coder and HMAC are used, see options below.
46
+ # It is recommended to only enable transparent upgrades for a brief transition period,
47
+ # and remove support for them once old sessions have converted or timed out.
48
+ #
49
+ # If the final cookie is too large (>=4096 bytes), a Roda::RodaPlugins::Sessions::CookieTooLarge
50
+ # exception will be raised.
51
+ #
52
+ # = Required Options
53
+ #
54
+ # The session cookies this plugin uses are both encrypted and signed, so two separate
55
+ # secrets are used internally. However, for ease of use, these secrets are combined into
56
+ # a single +:secret+ option. The +:secret+ option must be a string of at least 64 bytes
57
+ # and should be randomly generated. The first 32 bytes are used as the secret for the
58
+ # cipher, any remaining bytes are used for the secret for the HMAC.
59
+ #
60
+ # = Other Options
61
+ #
62
+ # :cookie_options :: Any cookie options to set on the session cookie. By default, uses
63
+ # <tt>httponly: true, path: '/', same_site: :lax</tt> so that the cookie is not accessible
64
+ # to javascript, allowed for all paths, and will not be used for cross-site non-GET requests
65
+ # that. If the +:secure+ option is not present in the hash, then
66
+ # <tt>secure: true</tt> is also set if the request is made over HTTPS. If this option is
67
+ # given, it will be merged into the default cookie options.
68
+ # :env_key :: The key in `env` where the session should be located. Defaults to <tt>"rack.session"</tt>, the
69
+ # default key for sessions in Rack.
70
+ # :gzip_over :: For session data over this many bytes, compress it with the deflate algorithm (default: nil,
71
+ # so never compress). Note that compression should not be enabled if you are storing data in
72
+ # the session derived from user input and also storing sensitive data in the session.
73
+ # :key :: The cookie name to use (default: <tt>'roda.session'</tt>)
74
+ # :max_seconds :: The maximum number of seconds to allow for total session lifetime, starting with when
75
+ # the session was originally created. Default is <tt>86400*30</tt> (30 days). Can be set to
76
+ # +nil+ to disable session lifetime checks.
77
+ # :max_idle_seconds :: The maximum number of seconds to allow since the session was last updated.
78
+ # Default is <tt>86400*7</tt> (7 days). Can be set to nil to disable session idleness
79
+ # checks.
80
+ # :old_secret :: The previous secret to use, allowing for secret rotation. Must be a string of at least 64
81
+ # bytes if given.
82
+ # :pad_size :: Pad session data (after possible compression, before encryption), to a multiple of this
83
+ # many bytes (default: 32). This can be between 2-4096 bytes, or +nil+ to disable padding.
84
+ # :per_cookie_cipher_secret :: Uses a separate cipher key for every cookie, with the key used generated using
85
+ # HMAC-SHA-256 of 32 bytes of random data with the default cipher secret. This
86
+ # offers additional protection in case the random initialization vector used when
87
+ # encrypting the session data has been reused. Odds of that are 1 in 2**64 if
88
+ # initialization vector is truly random, but weaknesses in the random number
89
+ # generator could make the odds much higher. Default is +true+.
90
+ # :parser :: The parser for the serialized session data (default: <tt>JSON.method(:parse)</tt>).
91
+ # :serializer :: The serializer for the session data (default +:to_json.to_proc+).
92
+ # :skip_within :: If the last update time for the session cookie is less than this number of seconds from the
93
+ # current time, and the session has not been modified, do not set a new session cookie
94
+ # (default: 3600).
95
+ # :upgrade_from_rack_session_cookie_key :: The cookie name to use for transparently upgrading from
96
+ # Rack::Session:Cookie (defaults to <tt>'rack.session'</tt>).
97
+ # :upgrade_from_rack_session_cookie_secret :: The secret for the HMAC-SHA1 signature when allowing
98
+ # transparent upgrades from Rack::Session::Cookie. Using this
99
+ # option is only recommended during a short transition period,
100
+ # and is not enabled by default as it lowers security.
101
+ # :upgrade_from_rack_session_cookie_options :: Options to pass when deleting the cookie used by
102
+ # Rack::Session::Cookie after converting it to use the session
103
+ # cookies used by this plugin.
104
+ #
105
+ # = Not a Rack Middleware
106
+ #
107
+ # Unlike some other approaches to sessions, the sessions plugin does not use
108
+ # a rack middleware, so session information is not available to other rack middleware,
109
+ # only to the application itself, with the session not being loaded from the cookie
110
+ # until the +session+ method is called.
111
+ #
112
+ # If you need rack middleware to access the session information, then
113
+ # <tt>require 'roda/session_middleware'</tt> and <tt>use RodaSessionMiddleware</tt>.
114
+ # <tt>RodaSessionMiddleware</tt> passes the options given to this plugin.
115
+ #
116
+ # = Session Cookie Cryptography/Format
117
+ #
118
+ # Session cookies created by this plugin by default use the following format:
119
+ #
120
+ # urlsafe_base64("\1" + random_data + IV + encrypted session data + HMAC)
121
+ #
122
+ # If +:per_cookie_cipher_secret+ option is set to +false+, an older format is used:
123
+ #
124
+ # urlsafe_base64("\0" + IV + encrypted session data + HMAC)
125
+ #
126
+ # where:
127
+ #
128
+ # version :: 1 byte, currently must be 1 or 0, other values reserved for future expansion.
129
+ # random_data :: 32 bytes, used for generating the per-cookie secret
130
+ # IV :: 16 bytes, initialization vector for AES-256-CTR cipher.
131
+ # encrypted session data :: >=12 bytes of data encrypted with AES-256-CTR cipher, see below.
132
+ # HMAC :: 32 bytes, HMAC-SHA-256 of all preceding data plus cookie key (so that a cookie value
133
+ # for a different key cannot be used even if the secret is the same).
134
+ #
135
+ # The encrypted session data uses the following format:
136
+ #
137
+ # bitmap + creation time + update time + padding + serialized data
138
+ #
139
+ # where:
140
+ #
141
+ # bitmap :: 2 bytes in little endian format, lower 12 bits storing number of padding
142
+ # bytes, 13th bit storing whether serialized data is compressed with deflate.
143
+ # Bits 14-16 reserved for future expansion.
144
+ # creation time :: 4 byte integer in unsigned little endian format, storing unix timestamp
145
+ # since session initially created.
146
+ # update time :: 4 byte integer in unsigned little endian format, storing unix timestamp
147
+ # since session last updated.
148
+ # padding :: >=0 padding bytes specified in bitmap, filled with random data, can be ignored.
149
+ # serialized data :: >=2 bytes of serialized data in JSON format. If the bitmap indicates
150
+ # deflate compression, this contains the deflate compressed data.
151
+ module Sessions
152
+ DEFAULT_COOKIE_OPTIONS = {:httponly=>true, :path=>'/'.freeze, :same_site=>:lax}.freeze
153
+ DEFAULT_OPTIONS = {:key => 'roda.session'.freeze, :max_seconds=>86400*30, :max_idle_seconds=>86400*7, :pad_size=>32, :gzip_over=>nil, :skip_within=>3600, :env_key=>'rack.session'}.freeze
154
+ DEFLATE_BIT = 0x1000
155
+ PADDING_MASK = 0x0fff
156
+ SESSION_CREATED_AT = 'roda.session.created_at'.freeze
157
+ SESSION_UPDATED_AT = 'roda.session.updated_at'.freeze
158
+ SESSION_SERIALIZED = 'roda.session.serialized'.freeze
159
+ SESSION_VERSION_NUM = 'roda.session.version'.freeze
160
+ SESSION_DELETE_RACK_COOKIE = 'roda.session.delete_rack_session_cookie'.freeze
161
+
162
+ # Exception class used when creating a session cookie that would exceed the
163
+ # allowable cookie size limit.
164
+ class CookieTooLarge < RodaError
165
+ end
166
+
167
+ # Split given secret into a cipher secret and an hmac secret.
168
+ def self.split_secret(name, secret)
169
+ raise RodaError, "sessions plugin :#{name} option must be a String" unless secret.is_a?(String)
170
+ raise RodaError, "invalid sessions plugin :#{name} option length: #{secret.bytesize}, must be >=64" unless secret.bytesize >= 64
171
+ hmac_secret = secret = secret.dup.force_encoding('BINARY')
172
+ cipher_secret = secret.slice!(0, 32)
173
+ [cipher_secret.freeze, hmac_secret.freeze]
174
+ end
175
+
176
+ def self.load_dependencies(app, opts=OPTS)
177
+ app.plugin :_base64
178
+ end
179
+
180
+ # Configure the plugin, see Sessions for details on options.
181
+ def self.configure(app, opts=OPTS)
182
+ opts = (app.opts[:sessions] || DEFAULT_OPTIONS).merge(opts)
183
+ co = opts[:cookie_options] = DEFAULT_COOKIE_OPTIONS.merge(opts[:cookie_options] || OPTS).freeze
184
+ opts[:remove_cookie_options] = co.merge(:max_age=>'0', :expires=>Time.at(0))
185
+ opts[:parser] ||= app.opts[:json_parser] || JSON.method(:parse)
186
+ opts[:serializer] ||= app.opts[:json_serializer] || :to_json.to_proc
187
+
188
+ opts[:per_cookie_cipher_secret] = true unless opts.has_key?(:per_cookie_cipher_secret)
189
+ opts[:session_version_num] = opts[:per_cookie_cipher_secret] ? 1 : 0
190
+
191
+ if opts[:upgrade_from_rack_session_cookie_secret]
192
+ opts[:upgrade_from_rack_session_cookie_key] ||= 'rack.session'
193
+ rsco = opts[:upgrade_from_rack_session_cookie_options] = Hash[opts[:upgrade_from_rack_session_cookie_options] || OPTS]
194
+ rsco[:path] ||= co[:path]
195
+ rsco[:domain] ||= co[:domain]
196
+ end
197
+
198
+ opts[:cipher_secret], opts[:hmac_secret] = split_secret(:secret, opts[:secret])
199
+ opts[:old_cipher_secret], opts[:old_hmac_secret] = (split_secret(:old_secret, opts[:old_secret]) if opts[:old_secret])
200
+
201
+ case opts[:pad_size]
202
+ when nil
203
+ # no changes
204
+ when Integer
205
+ raise RodaError, "invalid :pad_size: #{opts[:pad_size]}, must be >=2, < 4096" unless opts[:pad_size] >= 2 && opts[:pad_size] < 4096
206
+ else
207
+ raise RodaError, "invalid :pad_size option: #{opts[:pad_size].inspect}, must be Integer or nil"
208
+ end
209
+
210
+ app.opts[:sessions] = opts.freeze
211
+ app.opts[:sessions_convert_symbols] = true unless app.opts.has_key?(:sessions_convert_symbols)
212
+ end
213
+
214
+ module InstanceMethods
215
+ # Clear data from the session, and update the request environment
216
+ # so that the session cookie will use a new creation timestamp
217
+ # instead of the previous creation timestamp.
218
+ def clear_session
219
+ session.clear
220
+ env.delete(SESSION_CREATED_AT)
221
+ env.delete(SESSION_UPDATED_AT)
222
+ nil
223
+ end
224
+
225
+ private
226
+
227
+ # If session information has been set in the request environment,
228
+ # update the rack response headers to set the session cookie in
229
+ # the response.
230
+ def _roda_after_50__sessions(res)
231
+ if res && (session = env[self.class.opts[:sessions][:env_key]])
232
+ @_request.persist_session(res[1], session)
233
+ end
234
+ end
235
+ end
236
+
237
+ module RequestMethods
238
+ # Load the session information from the cookie. With the sessions
239
+ # plugin, you must call this method to get the session, instead of
240
+ # trying to access the session directly through the request environment.
241
+ # For maximum compatibility with other software that uses rack sessions,
242
+ # this method stores the session in 'rack.session' in the request environment,
243
+ # but that does not happen until this method is called.
244
+ def session
245
+ @env[roda_class.opts[:sessions][:env_key]] ||= _load_session
246
+ end
247
+
248
+ # The time the session was originally created. nil if there is no active session.
249
+ def session_created_at
250
+ session
251
+ Time.at(@env[SESSION_CREATED_AT]) if @env[SESSION_SERIALIZED]
252
+ end
253
+
254
+ # The time the session was last updated. nil if there is no active session.
255
+ def session_updated_at
256
+ session
257
+ Time.at(@env[SESSION_UPDATED_AT]) if @env[SESSION_SERIALIZED]
258
+ end
259
+
260
+ # Persist the session data as a cookie. If transparently upgrading from
261
+ # Rack::Session::Cookie, mark the related cookie for expiration so it isn't
262
+ # sent in the future.
263
+ def persist_session(headers, session)
264
+ opts = roda_class.opts[:sessions]
265
+
266
+ if session.empty?
267
+ if env[SESSION_SERIALIZED]
268
+ # If session was submitted and is now empty, remove the cookie
269
+ Rack::Utils.delete_cookie_header!(headers, opts[:key], opts[:remove_cookie_options])
270
+ # else
271
+ # If no session was submitted, and the session is empty
272
+ # then there is no need to do anything
273
+ end
274
+ elsif cookie_value = _serialize_session(session)
275
+ cookie = Hash[opts[:cookie_options]]
276
+ cookie[:value] = cookie_value
277
+ cookie[:secure] = true if !cookie.has_key?(:secure) && ssl?
278
+
279
+ before_size = if (set_cookie_before = headers[RodaResponseHeaders::SET_COOKIE]).is_a?(String)
280
+ set_cookie_before.bytesize
281
+ else
282
+ 0
283
+ end
284
+
285
+ Rack::Utils.set_cookie_header!(headers, opts[:key], cookie)
286
+
287
+ cookie_size = case set_cookie_after = headers[RodaResponseHeaders::SET_COOKIE]
288
+ when String
289
+ # Rack < 3 always takes this branch, combines cookies into string, subtract previous size
290
+ # Rack 3+ takes this branch if this is the first cookie set, in which case before size is 0
291
+ set_cookie_after.bytesize - before_size
292
+ else # when Array
293
+ # Rack 3+ takes branch if this is not the first cookie set, and last element of the array
294
+ # is most recently added cookie
295
+ set_cookie_after.last.bytesize
296
+ end
297
+
298
+ if cookie_size >= 4096
299
+ raise CookieTooLarge, "attempted to create cookie larger than 4096 bytes (bytes: #{cookie_size})"
300
+ end
301
+ end
302
+
303
+ if env[SESSION_DELETE_RACK_COOKIE]
304
+ Rack::Utils.delete_cookie_header!(headers, opts[:upgrade_from_rack_session_cookie_key], opts[:upgrade_from_rack_session_cookie_options])
305
+ end
306
+
307
+ nil
308
+ end
309
+
310
+ private
311
+
312
+ # Load the session by looking for the appropriate cookie, or falling
313
+ # back to the rack session cookie if configured.
314
+ def _load_session
315
+ opts = roda_class.opts[:sessions]
316
+ cs = cookies
317
+
318
+ if data = cs[opts[:key]]
319
+ _deserialize_session(data)
320
+ elsif (key = opts[:upgrade_from_rack_session_cookie_key]) && (data = cs[key])
321
+ _deserialize_rack_session(data)
322
+ end || {}
323
+ end
324
+
325
+ # If 'rack.errors' is set, write the error message to it.
326
+ # This is used for errors that shouldn't be raised as exceptions,
327
+ # such as improper session cookies.
328
+ def _session_serialization_error(msg)
329
+ return unless error_stream = @env['rack.errors']
330
+ error_stream.puts(msg)
331
+ nil
332
+ end
333
+
334
+ # Interpret given cookie data as a Rack::Session::Cookie
335
+ # serialized session using the default Rack::Session::Cookie
336
+ # hmac and coder.
337
+ def _deserialize_rack_session(data)
338
+ opts = roda_class.opts[:sessions]
339
+ data, digest = data.split("--", 2)
340
+ unless digest
341
+ return _session_serialization_error("Not decoding Rack::Session::Cookie session: invalid format")
342
+ end
343
+ unless Rack::Utils.secure_compare(digest, OpenSSL::HMAC.hexdigest(OpenSSL::Digest::SHA1.new, opts[:upgrade_from_rack_session_cookie_secret], data))
344
+ return _session_serialization_error("Not decoding Rack::Session::Cookie session: HMAC invalid")
345
+ end
346
+
347
+ begin
348
+ session = Marshal.load(data.unpack('m').first)
349
+ rescue
350
+ return _session_serialization_error("Error decoding Rack::Session::Cookie session: not base64 encoded marshal dump")
351
+ end
352
+
353
+ # Mark rack session cookie for deletion on success
354
+ env[SESSION_DELETE_RACK_COOKIE] = true
355
+
356
+ # Delete the session id before serializing it. Starting in rack 2.0.8,
357
+ # this is an object and not just a string, and calling to_s on it raises
358
+ # a RuntimeError.
359
+ session.delete("session_id")
360
+
361
+ # Convert the rack session by roundtripping it through
362
+ # the parser and serializer, so that you would get the
363
+ # same result as you would if the session was handled
364
+ # by this plugin.
365
+ env[SESSION_SERIALIZED] = data = opts[:serializer].call(session)
366
+ env[SESSION_CREATED_AT] = Time.now.to_i
367
+ opts[:parser].call(data)
368
+ end
369
+
370
+ # Interpret given cookie data as a Rack::Session::Cookie
371
+ def _deserialize_session(data)
372
+ opts = roda_class.opts[:sessions]
373
+
374
+ begin
375
+ data = Base64_.urlsafe_decode64(data)
376
+ rescue ArgumentError
377
+ return _session_serialization_error("Unable to decode session: invalid base64")
378
+ end
379
+
380
+ case version = data.getbyte(0)
381
+ when 1
382
+ per_cookie_secret = true
383
+ # minimum length (1+32+16+12+32) (version+random_data+cipher_iv+minimum session+hmac)
384
+ # 1 : version
385
+ # 32 : random_data (if per_cookie_cipher_secret)
386
+ # 16 : cipher_iv
387
+ # 12 : minimum_session
388
+ # 2 : bitmap for gzip + padding info
389
+ # 4 : creation time
390
+ # 4 : update time
391
+ # 2 : data
392
+ # 32 : HMAC-SHA-256
393
+ min_data_length = 93
394
+ when 0
395
+ per_cookie_secret = false
396
+ # minimum length (1+16+12+32) (version+cipher_iv+minimum session+hmac)
397
+ min_data_length = 61
398
+ when nil
399
+ return _session_serialization_error("Unable to decode session: no data")
400
+ else
401
+ return _session_serialization_error("Unable to decode session: version marker unsupported")
402
+ end
403
+
404
+ length = data.bytesize
405
+ if data.length < min_data_length
406
+ return _session_serialization_error("Unable to decode session: data too short")
407
+ end
408
+
409
+ encrypted_data = data.slice!(0, length-32)
410
+ unless Rack::Utils.secure_compare(data, OpenSSL::HMAC.digest(OpenSSL::Digest::SHA256.new, opts[:hmac_secret], encrypted_data+opts[:key]))
411
+ if opts[:old_hmac_secret] && Rack::Utils.secure_compare(data, OpenSSL::HMAC.digest(OpenSSL::Digest::SHA256.new, opts[:old_hmac_secret], encrypted_data+opts[:key]))
412
+ use_old_cipher_secret = true
413
+ else
414
+ return _session_serialization_error("Not decoding session: HMAC invalid")
415
+ end
416
+ end
417
+
418
+ # Remove version
419
+ encrypted_data.slice!(0)
420
+
421
+ cipher_secret = opts[use_old_cipher_secret ? :old_cipher_secret : :cipher_secret]
422
+ if per_cookie_secret
423
+ cipher_secret = OpenSSL::HMAC.digest(OpenSSL::Digest::SHA256.new, cipher_secret, encrypted_data.slice!(0, 32))
424
+ end
425
+ cipher_iv = encrypted_data.slice!(0, 16)
426
+
427
+ cipher = OpenSSL::Cipher.new("aes-256-ctr")
428
+
429
+ # Not rescuing cipher errors. If there is an error in the decryption, that's
430
+ # either a bug in the plugin that needs to be fixed, or an attacker is already
431
+ # able to forge a valid HMAC, in which case the error should be raised to
432
+ # alert the application owner about the problem.
433
+ cipher.decrypt
434
+ cipher.key = cipher_secret
435
+ cipher.iv = cipher_iv
436
+ data = cipher.update(encrypted_data) << cipher.final
437
+
438
+ bitmap, created_at, updated_at = data.unpack('vVV')
439
+ padding_bytes = bitmap & PADDING_MASK
440
+ now = Time.now.to_i
441
+ if (max = opts[:max_seconds]) && now > created_at + max
442
+ return _session_serialization_error("Not returning session: maximum session time expired")
443
+ end
444
+ if (max = opts[:max_idle_seconds]) && now > updated_at + max
445
+ return _session_serialization_error("Not returning session: maximum session idle time expired")
446
+ end
447
+
448
+ data = data.slice(10+padding_bytes, data.bytesize)
449
+
450
+ if bitmap & DEFLATE_BIT > 0
451
+ data = Zlib::Inflate.inflate(data)
452
+ end
453
+
454
+ env = @env
455
+ env[SESSION_CREATED_AT] = created_at
456
+ env[SESSION_UPDATED_AT] = updated_at
457
+ env[SESSION_SERIALIZED] = data
458
+ env[SESSION_VERSION_NUM] = version
459
+
460
+ opts[:parser].call(data)
461
+ end
462
+
463
+ def _serialize_session(session)
464
+ opts = roda_class.opts[:sessions]
465
+ env = @env
466
+ now = Time.now.to_i
467
+ json_data = opts[:serializer].call(session).force_encoding('BINARY')
468
+
469
+ if (serialized_session = env[SESSION_SERIALIZED]) &&
470
+ (opts[:session_version_num] == env[SESSION_VERSION_NUM]) &&
471
+ (updated_at = env[SESSION_UPDATED_AT]) &&
472
+ (now - updated_at < opts[:skip_within]) &&
473
+ (serialized_session == json_data)
474
+ return
475
+ end
476
+
477
+ bitmap = 0
478
+ json_length = json_data.bytesize
479
+ gzip_over = opts[:gzip_over]
480
+
481
+ if gzip_over && json_length > gzip_over
482
+ json_data = Zlib.deflate(json_data)
483
+ json_length = json_data.bytesize
484
+ bitmap |= DEFLATE_BIT
485
+ end
486
+
487
+ # When calculating padding bytes to use, include 10 bytes for bitmap and
488
+ # session create/update times, so total size of encrypted data is a
489
+ # multiple of pad_size.
490
+ if (pad_size = opts[:pad_size]) && (padding_bytes = (json_length+10) % pad_size) != 0
491
+ padding_bytes = pad_size - padding_bytes
492
+ bitmap |= padding_bytes
493
+ padding_data = SecureRandom.random_bytes(padding_bytes)
494
+ end
495
+
496
+ session_create_time = env[SESSION_CREATED_AT]
497
+ serialized_data = [bitmap, session_create_time||now, now].pack('vVV')
498
+
499
+ serialized_data << padding_data if padding_data
500
+ serialized_data << json_data
501
+
502
+ cipher_secret = opts[:cipher_secret]
503
+ if opts[:per_cookie_cipher_secret]
504
+ version = "\1"
505
+ per_cookie_secret_base = SecureRandom.random_bytes(32)
506
+ cipher_secret = OpenSSL::HMAC.digest(OpenSSL::Digest::SHA256.new, cipher_secret, per_cookie_secret_base)
507
+ else
508
+ version = "\0"
509
+ end
510
+
511
+ cipher = OpenSSL::Cipher.new("aes-256-ctr")
512
+ cipher.encrypt
513
+ cipher.key = cipher_secret
514
+ cipher_iv = cipher.random_iv
515
+ encrypted_data = cipher.update(serialized_data) << cipher.final
516
+
517
+ data = String.new
518
+ data << version
519
+ data << per_cookie_secret_base if per_cookie_secret_base
520
+ data << cipher_iv
521
+ data << encrypted_data
522
+ data << OpenSSL::HMAC.digest(OpenSSL::Digest::SHA256.new, opts[:hmac_secret], data+opts[:key])
523
+
524
+ data = Base64_.urlsafe_encode64(data)
525
+
526
+ if data.bytesize >= 4096
527
+ raise CookieTooLarge, "attempted to create cookie larger than 4096 bytes (bytes: #{data.bytesize})"
528
+ end
529
+
530
+ data
531
+ end
532
+ end
533
+ end
534
+
535
+ register_plugin(:sessions, Sessions)
536
+ end
537
+ end
@@ -0,0 +1,86 @@
1
+ # frozen-string-literal: true
2
+
3
+ #
4
+ class Roda
5
+ module RodaPlugins
6
+ # The shared_vars plugin adds a +shared+ method for storing
7
+ # shared variables across nested Roda apps.
8
+ #
9
+ # class API < Roda
10
+ # plugin :shared_vars
11
+ # route do |r|
12
+ # user = shared[:user]
13
+ # # ...
14
+ # end
15
+ # end
16
+ #
17
+ # class App < Roda
18
+ # plugin :shared_vars
19
+ #
20
+ # route do |r|
21
+ # r.on Integer do |user_id|
22
+ # shared[:user] = User[user_id]
23
+ # r.run API
24
+ # end
25
+ # end
26
+ # end
27
+ #
28
+ # If you pass a hash to shared, it will update the shared
29
+ # vars with the content of the hash:
30
+ #
31
+ # route do |r|
32
+ # r.on Integer do |user_id|
33
+ # shared(user: User[user_id])
34
+ # r.run API
35
+ # end
36
+ # end
37
+ #
38
+ # You can also pass a block to shared, which will set the
39
+ # shared variables only for the given block, restoring the
40
+ # previous shared variables afterward:
41
+ #
42
+ # route do |r|
43
+ # r.on Integer do |user_id|
44
+ # shared(user: User[user_id]) do
45
+ # r.run API
46
+ # end
47
+ # end
48
+ # end
49
+ module SharedVars
50
+ module InstanceMethods
51
+ # Returns the current shared vars for the request. These are
52
+ # stored in the request's environment, so they will be implicitly
53
+ # shared with other apps using this plugin.
54
+ #
55
+ # If the +vars+ argument is given, it should be a hash that will be
56
+ # merged into the current shared vars.
57
+ #
58
+ # If a block is given, a +vars+ argument must be provided, and it will
59
+ # only make the changes to the shared vars for the duration of the
60
+ # block, restoring the previous shared vars before the block returns.
61
+ def shared(vars=nil)
62
+ h = env['roda.shared'] ||= {}
63
+
64
+ if defined?(yield)
65
+ if vars
66
+ begin
67
+ env['roda.shared'] = h.merge(vars)
68
+ yield
69
+ ensure
70
+ env['roda.shared'] = h
71
+ end
72
+ else
73
+ raise RodaError, "must pass a vars hash when calling shared with a block"
74
+ end
75
+ elsif vars
76
+ h.merge!(vars)
77
+ end
78
+
79
+ h
80
+ end
81
+ end
82
+ end
83
+
84
+ register_plugin(:shared_vars, SharedVars)
85
+ end
86
+ end