utopia 3.0.4 → 3.0.5
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.
- checksums.yaml +4 -4
- checksums.yaml.gz.sig +0 -0
- data/context/middleware.md +2 -2
- data/lib/traces/provider/utopia/content/middleware.rb +20 -0
- data/lib/traces/provider/utopia/static/middleware.rb +19 -0
- data/lib/traces/provider/utopia.rb +7 -0
- data/lib/utopia/components.rb +1 -6
- data/lib/utopia/content/builder.rb +1 -5
- data/lib/utopia/content/links.rb +5 -3
- data/lib/utopia/content/markup.rb +3 -3
- data/lib/utopia/content/middleware.rb +16 -22
- data/lib/utopia/content/node.rb +7 -9
- data/lib/utopia/controller/rewrite.rb +3 -1
- data/lib/utopia/exceptions/application_errors.rb +11 -0
- data/lib/utopia/exceptions/handler.rb +4 -3
- data/lib/utopia/exceptions/mailer.rb +81 -19
- data/lib/utopia/path.rb +11 -16
- data/lib/utopia/request.rb +5 -20
- data/lib/utopia/session/lazy_hash.rb +28 -9
- data/lib/utopia/session/middleware.rb +74 -49
- data/lib/utopia/static/local_file.rb +7 -3
- data/lib/utopia/static/middleware.rb +0 -12
- data/lib/utopia/version.rb +1 -1
- data/readme.md +9 -0
- data/releases.md +9 -0
- data.tar.gz.sig +0 -0
- metadata +7 -3
- metadata.gz.sig +0 -0
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: '0314485e770b9085839422a7484c179a39e447d81b15b211d70fbb4361fdc8b2'
|
|
4
|
+
data.tar.gz: 6e583362bc02ce48424f8e3adf7b9528bfd0990ea4ff946879d4bfbc95a52624
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 4a81a495790a61faea6d2ab3873fd4043807134a4dec40b50177318802eefc14484e8dc218373fbf32b8bfe441ff96038ccaa21093acda9610d1ebc3ad6e87bb
|
|
7
|
+
data.tar.gz: 35d548a73022d0307b8a631d3a8c755b1eef9fcb0edb3f7b224ac2ef57b2334018928f3c8a47e4fea6812882ce794973a639af563b5a941627f3cafcce8ca971
|
checksums.yaml.gz.sig
CHANGED
|
Binary file
|
data/context/middleware.md
CHANGED
|
@@ -167,7 +167,7 @@ This template would typically be designed with supporting `_page.xnode` and `_he
|
|
|
167
167
|
|
|
168
168
|
## Session
|
|
169
169
|
|
|
170
|
-
The {ruby Utopia::Session} middleware provides session storage using encrypted client-side cookies. The session management uses symmetric private key encryption to store data on the client and
|
|
170
|
+
The {ruby Utopia::Session} middleware provides session storage using authenticated, encrypted client-side cookies. The session management uses symmetric private key encryption to store data on the client and prevent tampering.
|
|
171
171
|
|
|
172
172
|
```ruby
|
|
173
173
|
use Utopia::Session,
|
|
@@ -177,7 +177,7 @@ use Utopia::Session,
|
|
|
177
177
|
secure: true
|
|
178
178
|
```
|
|
179
179
|
|
|
180
|
-
All session data is stored on the client, but it
|
|
180
|
+
All session data is stored on the client, but it is encrypted and authenticated with a key derived from the secret. The client cannot read or modify the data without the secret stored on the server.
|
|
181
181
|
|
|
182
182
|
When the middleware is installed, the session is available on the request:
|
|
183
183
|
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Released under the MIT License.
|
|
4
|
+
# Copyright, 2026, by Samuel Williams.
|
|
5
|
+
|
|
6
|
+
require_relative "../../../../utopia/content/middleware"
|
|
7
|
+
|
|
8
|
+
require "traces/provider"
|
|
9
|
+
|
|
10
|
+
Traces::Provider(Utopia::Content::Middleware) do
|
|
11
|
+
def respond(link, request, localization: request.localization)
|
|
12
|
+
attributes = {
|
|
13
|
+
"link.key" => link.key,
|
|
14
|
+
"link.href" => link.href,
|
|
15
|
+
"link.locale" => localization&.locale,
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
Traces.trace("utopia.content.respond", attributes: attributes){super}
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Released under the MIT License.
|
|
4
|
+
# Copyright, 2026, by Samuel Williams.
|
|
5
|
+
|
|
6
|
+
require_relative "../../../../utopia/static/middleware"
|
|
7
|
+
|
|
8
|
+
require "traces/provider"
|
|
9
|
+
|
|
10
|
+
Traces::Provider(Utopia::Static::Middleware) do
|
|
11
|
+
def respond(request, path, extension, content_type, localization: request.localization)
|
|
12
|
+
attributes = {
|
|
13
|
+
path: path,
|
|
14
|
+
locale: localization&.locale,
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
Traces.trace("utopia.static.respond", attributes: attributes){super}
|
|
18
|
+
end
|
|
19
|
+
end
|
data/lib/utopia/components.rb
CHANGED
|
@@ -8,7 +8,7 @@ require "json"
|
|
|
8
8
|
require "pathname"
|
|
9
9
|
|
|
10
10
|
module Utopia
|
|
11
|
-
# Installs JavaScript packages into the public components directory. Package contents are copied from `dist` when it exists, otherwise from the package root.
|
|
11
|
+
# Installs JavaScript packages from `node_modules` into the public components directory. Package contents are copied from `dist` when it exists, otherwise from the package root.
|
|
12
12
|
#
|
|
13
13
|
# By default, the complete source directory is installed. Projects can limit an individual package to a set of files using `utopia.components` in their `package.json` file.
|
|
14
14
|
class Components
|
|
@@ -19,11 +19,6 @@ module Utopia
|
|
|
19
19
|
@root = Pathname.new(root)
|
|
20
20
|
@package_root = @root + "node_modules"
|
|
21
21
|
|
|
22
|
-
# This is a legacy path:
|
|
23
|
-
unless @package_root.directory?
|
|
24
|
-
@package_root = @root + "lib/components"
|
|
25
|
-
end
|
|
26
|
-
|
|
27
22
|
@install_root = @root + "public/_components"
|
|
28
23
|
@configuration = load_configuration
|
|
29
24
|
end
|
|
@@ -82,11 +82,7 @@ module Utopia
|
|
|
82
82
|
def text(content)
|
|
83
83
|
return unless content
|
|
84
84
|
|
|
85
|
-
|
|
86
|
-
content.build_markup(self)
|
|
87
|
-
else
|
|
88
|
-
XRB::Markup.append(@output, content)
|
|
89
|
-
end
|
|
85
|
+
content.build_markup(self)
|
|
90
86
|
end
|
|
91
87
|
|
|
92
88
|
# Write a complete tag to the output.
|
data/lib/utopia/content/links.rb
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
# Released under the MIT License.
|
|
4
|
-
# Copyright, 2015-
|
|
4
|
+
# Copyright, 2015-2026, by Samuel Williams.
|
|
5
5
|
|
|
6
6
|
require_relative "link"
|
|
7
7
|
|
|
@@ -204,8 +204,10 @@ module Utopia
|
|
|
204
204
|
def each(locale)
|
|
205
205
|
return to_enum(:each, locale) unless block_given?
|
|
206
206
|
|
|
207
|
-
|
|
208
|
-
|
|
207
|
+
@named.each_key do |name|
|
|
208
|
+
if link = lookup(name, locale)
|
|
209
|
+
yield link
|
|
210
|
+
end
|
|
209
211
|
end
|
|
210
212
|
end
|
|
211
213
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
# Released under the MIT License.
|
|
4
|
-
# Copyright, 2009-
|
|
4
|
+
# Copyright, 2009-2026, by Samuel Williams.
|
|
5
5
|
|
|
6
6
|
require "xrb/parsers"
|
|
7
7
|
require "xrb/entities"
|
|
@@ -108,9 +108,9 @@ module Utopia
|
|
|
108
108
|
# @returns [String] The resulting string.
|
|
109
109
|
def to_s
|
|
110
110
|
if @closing_tag
|
|
111
|
-
"#{start_location}: #{@opening_tag} was not closed!"
|
|
112
|
-
else
|
|
113
111
|
"#{start_location}: #{@opening_tag} was closed by #{@closing_tag}!"
|
|
112
|
+
else
|
|
113
|
+
"#{start_location}: #{@opening_tag} was not closed!"
|
|
114
114
|
end
|
|
115
115
|
end
|
|
116
116
|
end
|
|
@@ -16,7 +16,6 @@ require_relative "tags"
|
|
|
16
16
|
|
|
17
17
|
require "xrb/template"
|
|
18
18
|
require "concurrent/map"
|
|
19
|
-
require "traces/provider"
|
|
20
19
|
|
|
21
20
|
module Utopia
|
|
22
21
|
module Content
|
|
@@ -66,11 +65,7 @@ module Utopia
|
|
|
66
65
|
end
|
|
67
66
|
|
|
68
67
|
attr :root
|
|
69
|
-
|
|
70
|
-
# TODO we should remove this method and expose `@links` directly.
|
|
71
|
-
def links(path, **options)
|
|
72
|
-
@links.index(path, **options)
|
|
73
|
-
end
|
|
68
|
+
attr :links
|
|
74
69
|
|
|
75
70
|
# Load and cache a content template.
|
|
76
71
|
# @parameter path [Utopia::Path | String] The path.
|
|
@@ -159,12 +154,7 @@ module Utopia
|
|
|
159
154
|
private
|
|
160
155
|
|
|
161
156
|
def lookup_content(name, parent_path)
|
|
162
|
-
if String === name && name.index("/")
|
|
163
|
-
name = Path.create(name)
|
|
164
|
-
end
|
|
165
|
-
|
|
166
157
|
if Path === name
|
|
167
|
-
name = parent_path + name
|
|
168
158
|
name_path = name.components.dup
|
|
169
159
|
name_path[-1] += XNODE_EXTENSION
|
|
170
160
|
else
|
|
@@ -195,6 +185,21 @@ module Utopia
|
|
|
195
185
|
end
|
|
196
186
|
|
|
197
187
|
def content_tag(name, node, parent_path: node.parent_path)
|
|
188
|
+
# Preserve nested names while searching the physical content hierarchy:
|
|
189
|
+
if String === name
|
|
190
|
+
if name.index("/")
|
|
191
|
+
name = Path.create(name)
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
if Path === name
|
|
196
|
+
cache_key = parent_path + name
|
|
197
|
+
|
|
198
|
+
return @node_cache.fetch_or_store(cache_key) do
|
|
199
|
+
lookup_content(name, parent_path)
|
|
200
|
+
end
|
|
201
|
+
end
|
|
202
|
+
|
|
198
203
|
full_path = parent_path + name
|
|
199
204
|
|
|
200
205
|
name = full_path.pop
|
|
@@ -216,16 +221,5 @@ module Utopia
|
|
|
216
221
|
end
|
|
217
222
|
end
|
|
218
223
|
|
|
219
|
-
Traces::Provider(Middleware) do
|
|
220
|
-
def respond(link, request, localization: request.localization)
|
|
221
|
-
attributes = {
|
|
222
|
-
"link.key" => link.key,
|
|
223
|
-
"link.href" => link.href,
|
|
224
|
-
"link.locale" => localization&.locale,
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
Traces.trace("utopia.content.middleware.respond", attributes: attributes){super}
|
|
228
|
-
end
|
|
229
|
-
end
|
|
230
224
|
end
|
|
231
225
|
end
|
data/lib/utopia/content/node.rb
CHANGED
|
@@ -51,14 +51,12 @@ module Utopia
|
|
|
51
51
|
def local_path(path = ".", base = nil)
|
|
52
52
|
path = Path[path]
|
|
53
53
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
if path.absolute?
|
|
57
|
-
return root.join(*path.components)
|
|
58
|
-
else
|
|
54
|
+
if path.relative?
|
|
59
55
|
base ||= uri_path.dirname
|
|
60
|
-
|
|
56
|
+
path = base + path
|
|
61
57
|
end
|
|
58
|
+
|
|
59
|
+
return Pathname.new(path.to_url_path.local_path(@controller.root))
|
|
62
60
|
end
|
|
63
61
|
|
|
64
62
|
# Resolve a path relative to this node's containing URI path.
|
|
@@ -84,7 +82,7 @@ module Utopia
|
|
|
84
82
|
def links(path = ".", **options, &block)
|
|
85
83
|
path = uri_path.dirname + Path[path]
|
|
86
84
|
|
|
87
|
-
links = @controller.links(path, **options)
|
|
85
|
+
links = @controller.links.index(path, **options)
|
|
88
86
|
|
|
89
87
|
if block_given?
|
|
90
88
|
links.each(&block)
|
|
@@ -96,7 +94,7 @@ module Utopia
|
|
|
96
94
|
# Return localized and indexed variants related to this node.
|
|
97
95
|
# @returns [Array(Link)] The related links.
|
|
98
96
|
def related_links
|
|
99
|
-
@controller.links(@uri_path.dirname, name: @uri_path.basename, indices: true)
|
|
97
|
+
@controller.links.index(@uri_path.dirname, name: @uri_path.basename, indices: true)
|
|
100
98
|
end
|
|
101
99
|
|
|
102
100
|
# Return the directory whose links are siblings of this node.
|
|
@@ -113,7 +111,7 @@ module Utopia
|
|
|
113
111
|
# @parameter options [Hash] The options.
|
|
114
112
|
# @returns [Array(Link)] The sibling links.
|
|
115
113
|
def sibling_links(**options)
|
|
116
|
-
return @controller.links(siblings_path, **options)
|
|
114
|
+
return @controller.links.index(siblings_path, **options)
|
|
117
115
|
end
|
|
118
116
|
|
|
119
117
|
# Lookup the given tag which is being rendered within the given node. Invoked by {Document}.
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
# Released under the MIT License.
|
|
4
|
-
# Copyright, 2015-
|
|
4
|
+
# Copyright, 2015-2026, by Samuel Williams.
|
|
5
5
|
|
|
6
6
|
require_relative "../http"
|
|
7
7
|
require_relative "../path/matcher"
|
|
@@ -48,6 +48,8 @@ module Utopia
|
|
|
48
48
|
# Freeze this object and its internal state.
|
|
49
49
|
# @returns [self] This object.
|
|
50
50
|
def freeze
|
|
51
|
+
return self if frozen?
|
|
52
|
+
|
|
51
53
|
@matcher.freeze
|
|
52
54
|
@block.freeze
|
|
53
55
|
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Released under the MIT License.
|
|
4
|
+
# Copyright, 2026, by Samuel Williams.
|
|
5
|
+
|
|
6
|
+
module Utopia
|
|
7
|
+
module Exceptions
|
|
8
|
+
# Exceptions raised by application code which can be safely handled and reported.
|
|
9
|
+
APPLICATION_ERRORS = [StandardError, ScriptError].freeze
|
|
10
|
+
end
|
|
11
|
+
end
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
# Released under the MIT License.
|
|
4
|
-
# Copyright, 2014-
|
|
4
|
+
# Copyright, 2014-2026, by Samuel Williams.
|
|
5
5
|
# Copyright, 2025, by Olle Jonsson.
|
|
6
6
|
|
|
7
7
|
require "console"
|
|
@@ -9,6 +9,7 @@ require "console"
|
|
|
9
9
|
require_relative "../middleware"
|
|
10
10
|
require_relative "../request"
|
|
11
11
|
require_relative "../response"
|
|
12
|
+
require_relative "application_errors"
|
|
12
13
|
|
|
13
14
|
module Utopia
|
|
14
15
|
module Exceptions
|
|
@@ -37,7 +38,7 @@ module Utopia
|
|
|
37
38
|
def call(request)
|
|
38
39
|
begin
|
|
39
40
|
return @delegate.call(request)
|
|
40
|
-
rescue
|
|
41
|
+
rescue *APPLICATION_ERRORS => exception
|
|
41
42
|
Console.warn(self, "An error occurred while processing the request.", error: exception)
|
|
42
43
|
|
|
43
44
|
begin
|
|
@@ -52,7 +53,7 @@ module Utopia
|
|
|
52
53
|
error_response.status = 500
|
|
53
54
|
|
|
54
55
|
return error_response
|
|
55
|
-
rescue
|
|
56
|
+
rescue *APPLICATION_ERRORS => exception
|
|
56
57
|
# If redirection fails, we also finish with a fatal error:
|
|
57
58
|
Console.error(self, "An error occurred while invoking the error handler.", error: exception)
|
|
58
59
|
return Response[500, {"content-type" => "text/plain"}, ["An error occurred while processing the request."]]
|
|
@@ -1,14 +1,17 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
# Released under the MIT License.
|
|
4
|
-
# Copyright, 2016-
|
|
4
|
+
# Copyright, 2016-2026, by Samuel Williams.
|
|
5
5
|
|
|
6
6
|
require "net/smtp"
|
|
7
7
|
require "mail"
|
|
8
|
+
require "console"
|
|
9
|
+
require "stringio"
|
|
10
|
+
require "yaml"
|
|
8
11
|
|
|
9
12
|
require_relative "../middleware"
|
|
10
13
|
require_relative "../request"
|
|
11
|
-
require_relative "
|
|
14
|
+
require_relative "application_errors"
|
|
12
15
|
|
|
13
16
|
module Utopia
|
|
14
17
|
module Exceptions
|
|
@@ -23,20 +26,33 @@ module Utopia
|
|
|
23
26
|
|
|
24
27
|
DEFAULT_FROM = (ENV["USER"] || "utopia").freeze
|
|
25
28
|
DEFAULT_SUBJECT = "%{exception} [PID %{pid} : %{cwd}]".freeze
|
|
29
|
+
ATTACHMENT_SIZE_LIMIT = 64*1024
|
|
30
|
+
SENSITIVE_FIELD = /authorization|cookie|credential|password|private[_-]?key|referer|referrer|secret|session|token|variables|api[_-]?key/i
|
|
31
|
+
REDACTED = "[REDACTED]".freeze
|
|
26
32
|
|
|
27
33
|
# @param to [String] The address to email error reports to.
|
|
28
34
|
# @param from [String] The from address for error reports.
|
|
29
35
|
# @param subject [String] The subject template which can access attributes defined by `#attributes_for`.
|
|
30
36
|
# @param delivery_method [Object] The delivery method as required by the mail gem.
|
|
31
|
-
# @param
|
|
32
|
-
|
|
37
|
+
# @param dump_body [Boolean] Attach a bounded rewindable request body to the error report.
|
|
38
|
+
# @param dump_environment [Boolean] Include application state and attach it as `state.yaml`.
|
|
39
|
+
# @param attachment_size_limit [Integer] The maximum size of each attachment.
|
|
40
|
+
# @param redact [Regexp | Nil] A pattern matching structured field names whose values should be redacted.
|
|
41
|
+
def initialize(app, to: "postmaster", from: DEFAULT_FROM, subject: DEFAULT_SUBJECT, delivery_method: LOCAL_SMTP, dump_body: false, dump_environment: false, attachment_size_limit: ATTACHMENT_SIZE_LIMIT, redact: SENSITIVE_FIELD)
|
|
33
42
|
super(app)
|
|
34
43
|
|
|
35
44
|
@to = to
|
|
36
45
|
@from = from
|
|
37
46
|
@subject = subject
|
|
38
47
|
@delivery_method = delivery_method
|
|
48
|
+
@dump_body = dump_body
|
|
39
49
|
@dump_environment = dump_environment
|
|
50
|
+
@attachment_size_limit = Integer(attachment_size_limit)
|
|
51
|
+
@redact = redact
|
|
52
|
+
|
|
53
|
+
if @attachment_size_limit < 0
|
|
54
|
+
raise ArgumentError, "attachment_size_limit must not be negative!"
|
|
55
|
+
end
|
|
40
56
|
end
|
|
41
57
|
|
|
42
58
|
# Freeze this object and its internal state.
|
|
@@ -48,7 +64,10 @@ module Utopia
|
|
|
48
64
|
@from.freeze
|
|
49
65
|
@subject.freeze
|
|
50
66
|
@delivery_method.freeze
|
|
67
|
+
@dump_body.freeze
|
|
51
68
|
@dump_environment.freeze
|
|
69
|
+
@attachment_size_limit.freeze
|
|
70
|
+
@redact.freeze
|
|
52
71
|
|
|
53
72
|
super
|
|
54
73
|
end
|
|
@@ -59,7 +78,7 @@ module Utopia
|
|
|
59
78
|
def call(request)
|
|
60
79
|
begin
|
|
61
80
|
return @delegate.call(request)
|
|
62
|
-
rescue => exception
|
|
81
|
+
rescue *APPLICATION_ERRORS => exception
|
|
63
82
|
request.exception = exception
|
|
64
83
|
send_notification exception, request
|
|
65
84
|
|
|
@@ -99,30 +118,32 @@ module Utopia
|
|
|
99
118
|
def generate_body(exception, request)
|
|
100
119
|
io = StringIO.new
|
|
101
120
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
# TODO embed the request body if it's textual?
|
|
105
|
-
# TODO dump and embed `utopia.variables`?
|
|
121
|
+
# Do not include the raw query string, as it may contain sensitive values:
|
|
122
|
+
io.puts "#{request.method} #{request.url.path.encoded}"
|
|
106
123
|
|
|
107
124
|
io.puts
|
|
108
125
|
|
|
109
126
|
REQUEST_ATTRIBUTES.each do |key|
|
|
110
|
-
value = request.send(key)
|
|
127
|
+
value = redact(key, request.send(key))
|
|
111
128
|
io.puts "request.#{key}: #{value.inspect}"
|
|
112
129
|
end
|
|
113
130
|
|
|
114
131
|
request.query_parameters.each do |key, value|
|
|
132
|
+
value = redact(key, value)
|
|
115
133
|
io.puts "request.query_parameters.#{key}: #{value.inspect}"
|
|
116
134
|
end
|
|
117
135
|
|
|
118
136
|
io.puts
|
|
119
137
|
|
|
120
138
|
request.headers.each do |key, value|
|
|
139
|
+
value = redact(key, value)
|
|
121
140
|
io.puts "header[#{key.inspect}]: #{value.inspect}"
|
|
122
141
|
end
|
|
123
142
|
|
|
124
|
-
|
|
125
|
-
|
|
143
|
+
if @dump_environment
|
|
144
|
+
filtered_state(request).each do |key, value|
|
|
145
|
+
io.puts "state.#{key}: #{value.inspect}"
|
|
146
|
+
end
|
|
126
147
|
end
|
|
127
148
|
|
|
128
149
|
io.puts
|
|
@@ -150,12 +171,14 @@ module Utopia
|
|
|
150
171
|
mail.text_part = Mail::Part.new
|
|
151
172
|
mail.text_part.body = generate_body(exception, request)
|
|
152
173
|
|
|
153
|
-
if
|
|
154
|
-
|
|
174
|
+
if @dump_body
|
|
175
|
+
if body = extract_body(request, @attachment_size_limit)
|
|
176
|
+
mail.attachments["body.bin"] = body
|
|
177
|
+
end
|
|
155
178
|
end
|
|
156
179
|
|
|
157
180
|
if @dump_environment
|
|
158
|
-
mail
|
|
181
|
+
attach(mail, "state.yaml", YAML.dump(filtered_state(request)))
|
|
159
182
|
end
|
|
160
183
|
|
|
161
184
|
return mail
|
|
@@ -168,8 +191,7 @@ module Utopia
|
|
|
168
191
|
|
|
169
192
|
mail.deliver
|
|
170
193
|
rescue => mail_exception
|
|
171
|
-
|
|
172
|
-
$stderr.puts mail_exception.backtrace
|
|
194
|
+
Console.warn(self, "Failed to deliver exception notification.", error: mail_exception)
|
|
173
195
|
end
|
|
174
196
|
|
|
175
197
|
def current_state(request)
|
|
@@ -181,11 +203,51 @@ module Utopia
|
|
|
181
203
|
}
|
|
182
204
|
end
|
|
183
205
|
|
|
184
|
-
def
|
|
206
|
+
def filtered_state(request)
|
|
207
|
+
redact(nil, current_state(request))
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
def redact(name, value)
|
|
211
|
+
if @redact && name
|
|
212
|
+
if @redact.match?(name.to_s)
|
|
213
|
+
return REDACTED
|
|
214
|
+
end
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
case value
|
|
218
|
+
when Hash
|
|
219
|
+
return value.to_h do |key, item|
|
|
220
|
+
[key, redact(key, item)]
|
|
221
|
+
end
|
|
222
|
+
when Array
|
|
223
|
+
return value.map{|item| redact(nil, item)}
|
|
224
|
+
else
|
|
225
|
+
return value
|
|
226
|
+
end
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
def attach(mail, name, content)
|
|
230
|
+
if content.bytesize <= @attachment_size_limit
|
|
231
|
+
mail.attachments[name] = content
|
|
232
|
+
end
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
def extract_body(request, size_limit)
|
|
185
236
|
body = request.body
|
|
186
237
|
|
|
187
238
|
if body&.rewindable? && body.rewind
|
|
188
|
-
|
|
239
|
+
buffer = String.new.b
|
|
240
|
+
|
|
241
|
+
body.each do |chunk|
|
|
242
|
+
# Do not retain a partial body when the complete attachment would exceed the limit:
|
|
243
|
+
if chunk.bytesize > size_limit - buffer.bytesize
|
|
244
|
+
return nil
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
buffer << chunk
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
return buffer unless buffer.empty?
|
|
189
251
|
end
|
|
190
252
|
end
|
|
191
253
|
end
|
data/lib/utopia/path.rb
CHANGED
|
@@ -1,12 +1,20 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
# Released under the MIT License.
|
|
4
|
-
# Copyright, 2009-
|
|
4
|
+
# Copyright, 2009-2026, by Samuel Williams.
|
|
5
5
|
|
|
6
6
|
require "protocol/url/path"
|
|
7
7
|
|
|
8
8
|
module Utopia
|
|
9
|
-
# Represents
|
|
9
|
+
# Represents an application path as a traversal through a tree.
|
|
10
|
+
#
|
|
11
|
+
# Each component names a node and `/` represents the edge between adjacent nodes. A leading empty component anchors the traversal at the root, while a trailing empty component preserves an explicit final edge and denotes a directory:
|
|
12
|
+
#
|
|
13
|
+
# - `["foo", "bar"]` represents `foo/bar`.
|
|
14
|
+
# - `["", "foo", "bar"]` represents `/foo/bar`.
|
|
15
|
+
# - `["", "foo", "bar", ""]` represents `/foo/bar/`.
|
|
16
|
+
#
|
|
17
|
+
# The structural root is represented by `[""]` and contains no traversed edge. It is intentionally distinct from parsing `/`, which preserves the explicit edge as `["", ""]`. Both serialize as `/`, but they retain different structural representations. In particular, the structural root maps to an empty local path so it can be resolved relative to an application root.
|
|
10
18
|
class Path
|
|
11
19
|
include Comparable
|
|
12
20
|
|
|
@@ -36,7 +44,7 @@ module Utopia
|
|
|
36
44
|
@components.empty?
|
|
37
45
|
end
|
|
38
46
|
|
|
39
|
-
# Construct the root path.
|
|
47
|
+
# Construct the structural root path without an explicit trailing separator.
|
|
40
48
|
# @returns [Path] The root path.
|
|
41
49
|
def self.root
|
|
42
50
|
self.new([""])
|
|
@@ -203,12 +211,6 @@ module Utopia
|
|
|
203
211
|
end
|
|
204
212
|
end
|
|
205
213
|
|
|
206
|
-
# Remove the first component when this path is relative.
|
|
207
|
-
# @returns [String | Nil] The removed component, or `nil` when the path is absolute.
|
|
208
|
-
def to_relative!
|
|
209
|
-
@components.shift if relative?
|
|
210
|
-
end
|
|
211
|
-
|
|
212
214
|
# Convert this object to a string.
|
|
213
215
|
# @returns [String] The resulting string.
|
|
214
216
|
def to_str
|
|
@@ -532,11 +534,4 @@ module Utopia
|
|
|
532
534
|
return index
|
|
533
535
|
end
|
|
534
536
|
end
|
|
535
|
-
|
|
536
|
-
# Coerce a value into a {Path}.
|
|
537
|
-
# @parameter path [Utopia::Path | String] The path.
|
|
538
|
-
# @returns [Path | Nil] The coerced path.
|
|
539
|
-
def self.Path(path)
|
|
540
|
-
Path.create(path)
|
|
541
|
-
end
|
|
542
537
|
end
|
data/lib/utopia/request.rb
CHANGED
|
@@ -117,28 +117,13 @@ module Utopia
|
|
|
117
117
|
@query_parameters ||= parse_query_parameters(self.url.query)
|
|
118
118
|
end
|
|
119
119
|
|
|
120
|
-
private def parse_cookies(cookie_header)
|
|
121
|
-
cookies = {}
|
|
122
|
-
|
|
123
|
-
return cookies unless cookie_header
|
|
124
|
-
|
|
125
|
-
if cookie_header.respond_to?(:to_str)
|
|
126
|
-
cookie_header = cookie_header.to_str
|
|
127
|
-
else
|
|
128
|
-
cookie_header = cookie_header.to_s
|
|
129
|
-
end
|
|
130
|
-
|
|
131
|
-
cookie_header.split(/;\s*/).each do |pair|
|
|
132
|
-
key, value = pair.split("=", 2)
|
|
133
|
-
cookies[key] = value || ""
|
|
134
|
-
end
|
|
135
|
-
|
|
136
|
-
return cookies
|
|
137
|
-
end
|
|
138
|
-
|
|
139
120
|
# Decoded request cookies.
|
|
140
121
|
def cookies
|
|
141
|
-
@cookies ||=
|
|
122
|
+
@cookies ||= if cookie_header = self.headers["cookie"]
|
|
123
|
+
cookie_header.to_h.transform_values(&:value)
|
|
124
|
+
else
|
|
125
|
+
{}
|
|
126
|
+
end
|
|
142
127
|
end
|
|
143
128
|
|
|
144
129
|
# The request user agent.
|
|
@@ -16,10 +16,6 @@ module Utopia
|
|
|
16
16
|
@loader = block
|
|
17
17
|
end
|
|
18
18
|
|
|
19
|
-
# The loaded session values, if already loaded.
|
|
20
|
-
# @returns [Hash | Nil] The loaded values.
|
|
21
|
-
attr :values
|
|
22
|
-
|
|
23
19
|
# Fetch a value by key, loading the hash if necessary.
|
|
24
20
|
# @parameter key [Object] The key.
|
|
25
21
|
# @returns [Object | Nil] The value.
|
|
@@ -66,10 +62,26 @@ module Utopia
|
|
|
66
62
|
@changed
|
|
67
63
|
end
|
|
68
64
|
|
|
69
|
-
#
|
|
70
|
-
# @returns [
|
|
71
|
-
def
|
|
72
|
-
|
|
65
|
+
# The current time for session expiry and persistence.
|
|
66
|
+
# @returns [Time] The current time in UTC.
|
|
67
|
+
def now
|
|
68
|
+
Time.now.utc
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Persist the session values if they have changed or require updating.
|
|
72
|
+
# @parameter timeout [Numeric | Nil] The maximum age before an update is required.
|
|
73
|
+
# @yields {|values, updated_at| ...} The loaded values and their update time.
|
|
74
|
+
# @returns [Object | Nil] The result of the block if persistence was required.
|
|
75
|
+
def persist(timeout = nil)
|
|
76
|
+
return unless needs_update?(timeout)
|
|
77
|
+
|
|
78
|
+
values = load!
|
|
79
|
+
updated_at = values[:updated_at] = now
|
|
80
|
+
|
|
81
|
+
result = yield(values, updated_at)
|
|
82
|
+
@changed = false
|
|
83
|
+
|
|
84
|
+
return result
|
|
73
85
|
end
|
|
74
86
|
|
|
75
87
|
# Check whether the underlying values have been loaded.
|
|
@@ -88,11 +100,18 @@ module Utopia
|
|
|
88
100
|
# We want to be careful here and not call load! which isn't cheap operation.
|
|
89
101
|
if timeout and @values and updated_at = @values[:updated_at]
|
|
90
102
|
# If the last update was too long ago, we need update:
|
|
91
|
-
return true if updated_at < (
|
|
103
|
+
return true if updated_at < (now - timeout)
|
|
92
104
|
end
|
|
93
105
|
|
|
94
106
|
return false
|
|
95
107
|
end
|
|
108
|
+
|
|
109
|
+
private
|
|
110
|
+
|
|
111
|
+
# Load and return the underlying values.
|
|
112
|
+
def load!
|
|
113
|
+
@values ||= @loader.call(now)
|
|
114
|
+
end
|
|
96
115
|
end
|
|
97
116
|
end
|
|
98
117
|
end
|
|
@@ -24,12 +24,14 @@ module Utopia
|
|
|
24
24
|
class PayloadError < StandardError
|
|
25
25
|
end
|
|
26
26
|
|
|
27
|
-
|
|
27
|
+
SIZE_LIMIT = 1024*32
|
|
28
28
|
|
|
29
29
|
SECRET_KEY = "UTOPIA_SESSION_SECRET".freeze
|
|
30
30
|
|
|
31
31
|
SESSION_KEY = "utopia.session".freeze
|
|
32
|
-
CIPHER_ALGORITHM = "aes-256-
|
|
32
|
+
CIPHER_ALGORITHM = "aes-256-gcm"
|
|
33
|
+
PAYLOAD_VERSION = "v1".freeze
|
|
34
|
+
AUTHENTICATION_TAG_SIZE = 16
|
|
33
35
|
|
|
34
36
|
# The session will expire if no requests were made within 24 hours:
|
|
35
37
|
DEFAULT_EXPIRES_AFTER = 3600*24
|
|
@@ -48,8 +50,8 @@ module Utopia
|
|
|
48
50
|
# @param http_only [Boolean] Whether client-side scripts may access the cookie.
|
|
49
51
|
# @param same_site [Symbol | String | Boolean | Nil] Controls whether the cookie is sent with cross-site requests.
|
|
50
52
|
# @param partitioned [Boolean] Whether the cookie uses partitioned storage.
|
|
51
|
-
# @param
|
|
52
|
-
def initialize(app, session_name: SESSION_KEY, secret: nil, expires_after: DEFAULT_EXPIRES_AFTER, update_timeout: DEFAULT_UPDATE_TIMEOUT, domain: nil, path: "/", max_age: nil, secure: false, http_only: true, same_site: :lax, partitioned: false,
|
|
53
|
+
# @param size_limit [Integer | Nil] The encoded session payload size limit.
|
|
54
|
+
def initialize(app, session_name: SESSION_KEY, secret: nil, expires_after: DEFAULT_EXPIRES_AFTER, update_timeout: DEFAULT_UPDATE_TIMEOUT, domain: nil, path: "/", max_age: nil, secure: false, http_only: true, same_site: :lax, partitioned: false, size_limit: SIZE_LIMIT)
|
|
53
55
|
super(app)
|
|
54
56
|
|
|
55
57
|
@session_name = session_name
|
|
@@ -61,6 +63,7 @@ module Utopia
|
|
|
61
63
|
|
|
62
64
|
# This generates a 32-byte key suitable for aes.
|
|
63
65
|
@key = Digest::SHA2.digest(secret)
|
|
66
|
+
@authentication_context = "#{@cookie_name}\0#{PAYLOAD_VERSION}".b.freeze
|
|
64
67
|
|
|
65
68
|
@expires_after = expires_after
|
|
66
69
|
@update_timeout = update_timeout
|
|
@@ -82,7 +85,7 @@ module Utopia
|
|
|
82
85
|
}
|
|
83
86
|
|
|
84
87
|
@serialization = Serialization.new
|
|
85
|
-
@
|
|
88
|
+
@size_limit = size_limit
|
|
86
89
|
end
|
|
87
90
|
|
|
88
91
|
attr :cookie_name
|
|
@@ -100,6 +103,7 @@ module Utopia
|
|
|
100
103
|
|
|
101
104
|
@cookie_name.freeze
|
|
102
105
|
@key.freeze
|
|
106
|
+
@authentication_context.freeze
|
|
103
107
|
@expires_after.freeze
|
|
104
108
|
@update_timeout.freeze
|
|
105
109
|
@cookie_defaults.freeze
|
|
@@ -139,59 +143,53 @@ module Utopia
|
|
|
139
143
|
end
|
|
140
144
|
|
|
141
145
|
def prepare_session(request)
|
|
142
|
-
LazyHash.new do
|
|
143
|
-
self.load_session_values(request)
|
|
146
|
+
LazyHash.new do |now|
|
|
147
|
+
self.load_session_values(request, now)
|
|
144
148
|
end
|
|
145
149
|
end
|
|
146
150
|
|
|
147
151
|
def update_session(session_hash, headers)
|
|
148
|
-
|
|
149
|
-
values
|
|
150
|
-
|
|
151
|
-
values[:updated_at] = Time.now.utc
|
|
152
|
-
|
|
153
|
-
data = encrypt(session_hash.values)
|
|
154
|
-
|
|
155
|
-
commit(data, values[:updated_at], headers)
|
|
152
|
+
session_hash.persist(@update_timeout) do |values, updated_at|
|
|
153
|
+
commit(encrypt(values), updated_at, headers)
|
|
156
154
|
end
|
|
157
155
|
end
|
|
158
156
|
|
|
159
157
|
# Constructs a valid session for the given request. These fields must match as per the checks performed in `valid_session?`:
|
|
160
|
-
def build_initial_session(request)
|
|
158
|
+
def build_initial_session(request, now)
|
|
161
159
|
{
|
|
162
160
|
user_agent: request.user_agent,
|
|
163
|
-
created_at:
|
|
164
|
-
updated_at:
|
|
161
|
+
created_at: now,
|
|
162
|
+
updated_at: now,
|
|
165
163
|
}
|
|
166
164
|
end
|
|
167
165
|
|
|
168
166
|
# Load session from user supplied cookie. If the data is invalid or otherwise fails validation, `build_iniital_session` is invoked.
|
|
169
167
|
# @return hash of values.
|
|
170
|
-
def load_session_values(request)
|
|
168
|
+
def load_session_values(request, now)
|
|
171
169
|
# Decrypt the data from the user if possible:
|
|
172
170
|
if data = request.cookies[@cookie_name]
|
|
173
171
|
begin
|
|
174
172
|
if values = decrypt(data)
|
|
175
|
-
validate_session!(request, values)
|
|
173
|
+
validate_session!(request, values, now)
|
|
176
174
|
|
|
177
175
|
return values
|
|
178
176
|
end
|
|
179
|
-
rescue => error
|
|
180
|
-
Console.
|
|
177
|
+
rescue PayloadError => error
|
|
178
|
+
Console.debug(self, "Discarding invalid session cookie!", exception: error)
|
|
181
179
|
end
|
|
182
180
|
end
|
|
183
181
|
|
|
184
182
|
# If we couldn't create a session
|
|
185
|
-
return build_initial_session(request)
|
|
183
|
+
return build_initial_session(request, now)
|
|
186
184
|
end
|
|
187
185
|
|
|
188
|
-
def validate_session!(request, values)
|
|
186
|
+
def validate_session!(request, values, now)
|
|
189
187
|
if values[:user_agent] != request.user_agent
|
|
190
188
|
raise PayloadError, "Invalid session because supplied user agent #{request.user_agent.inspect} does not match session user agent #{values[:user_agent].inspect}!"
|
|
191
189
|
end
|
|
192
190
|
|
|
193
191
|
if expires_at = expires(values[:updated_at])
|
|
194
|
-
if expires_at <
|
|
192
|
+
if expires_at < now
|
|
195
193
|
raise PayloadError, "Expired session cookie, user agent submitted a cookie that should have expired at #{expires_at}."
|
|
196
194
|
end
|
|
197
195
|
end
|
|
@@ -199,7 +197,7 @@ module Utopia
|
|
|
199
197
|
return true
|
|
200
198
|
end
|
|
201
199
|
|
|
202
|
-
def expires(updated_at
|
|
200
|
+
def expires(updated_at)
|
|
203
201
|
if @expires_after
|
|
204
202
|
return updated_at + @expires_after
|
|
205
203
|
end
|
|
@@ -253,38 +251,65 @@ module Utopia
|
|
|
253
251
|
end
|
|
254
252
|
|
|
255
253
|
def encrypt(hash)
|
|
256
|
-
|
|
257
|
-
|
|
254
|
+
cipher = OpenSSL::Cipher.new(CIPHER_ALGORITHM)
|
|
255
|
+
cipher.encrypt
|
|
256
|
+
|
|
257
|
+
cipher.key = @key
|
|
258
|
+
cipher.iv = initialization_vector = cipher.random_iv
|
|
259
|
+
cipher.auth_data = @authentication_context
|
|
260
|
+
|
|
261
|
+
encrypted_data = cipher.update(@serialization.dump(hash))
|
|
262
|
+
encrypted_data << cipher.final
|
|
258
263
|
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
c.iv = iv = c.random_iv
|
|
264
|
+
payload = initialization_vector + encrypted_data + cipher.auth_tag(AUTHENTICATION_TAG_SIZE)
|
|
265
|
+
data = "#{PAYLOAD_VERSION}.#{[payload].pack("m0")}"
|
|
262
266
|
|
|
263
|
-
|
|
264
|
-
e << c.final
|
|
267
|
+
validate_size!(data)
|
|
265
268
|
|
|
266
|
-
return
|
|
269
|
+
return data
|
|
267
270
|
end
|
|
268
271
|
|
|
269
272
|
def decrypt(data)
|
|
270
|
-
|
|
271
|
-
raise PayloadError, "Session payload size #{data.bytesize}bytes exceeds maximum allowed size #{@maximum_size}bytes!"
|
|
272
|
-
end
|
|
273
|
-
|
|
274
|
-
payload = data.unpack1("m0")
|
|
275
|
-
iv = payload.byteslice(0, 16)
|
|
276
|
-
e = payload.byteslice(16..)
|
|
277
|
-
|
|
278
|
-
c = OpenSSL::Cipher.new(CIPHER_ALGORITHM)
|
|
279
|
-
c.decrypt
|
|
273
|
+
validate_size!(data)
|
|
280
274
|
|
|
281
|
-
|
|
282
|
-
c.iv = iv
|
|
275
|
+
version, encoded_payload = data.split(".", 2)
|
|
283
276
|
|
|
284
|
-
|
|
285
|
-
|
|
277
|
+
if version != PAYLOAD_VERSION or encoded_payload.nil?
|
|
278
|
+
raise PayloadError, "Unsupported session payload format!"
|
|
279
|
+
end
|
|
286
280
|
|
|
287
|
-
|
|
281
|
+
begin
|
|
282
|
+
cipher = OpenSSL::Cipher.new(CIPHER_ALGORITHM)
|
|
283
|
+
payload = encoded_payload.unpack1("m0")
|
|
284
|
+
minimum_size = cipher.iv_len + AUTHENTICATION_TAG_SIZE
|
|
285
|
+
|
|
286
|
+
if payload.bytesize < minimum_size
|
|
287
|
+
raise PayloadError, "Invalid session payload!"
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
initialization_vector = payload.byteslice(0, cipher.iv_len)
|
|
291
|
+
encrypted_data = payload.byteslice(cipher.iv_len, payload.bytesize - minimum_size)
|
|
292
|
+
authentication_tag = payload.byteslice(-AUTHENTICATION_TAG_SIZE, AUTHENTICATION_TAG_SIZE)
|
|
293
|
+
|
|
294
|
+
cipher.decrypt
|
|
295
|
+
cipher.key = @key
|
|
296
|
+
cipher.iv = initialization_vector
|
|
297
|
+
cipher.auth_tag = authentication_tag
|
|
298
|
+
cipher.auth_data = @authentication_context
|
|
299
|
+
|
|
300
|
+
decrypted_data = cipher.update(encrypted_data)
|
|
301
|
+
decrypted_data << cipher.final
|
|
302
|
+
|
|
303
|
+
return @serialization.load(decrypted_data)
|
|
304
|
+
rescue ArgumentError, OpenSSL::Cipher::CipherError
|
|
305
|
+
raise PayloadError, "Invalid session payload!"
|
|
306
|
+
end
|
|
307
|
+
end
|
|
308
|
+
|
|
309
|
+
def validate_size!(data)
|
|
310
|
+
if @size_limit and data.bytesize > @size_limit
|
|
311
|
+
raise PayloadError, "Session payload size #{data.bytesize}bytes exceeds size limit #{@size_limit}bytes!"
|
|
312
|
+
end
|
|
288
313
|
end
|
|
289
314
|
end
|
|
290
315
|
end
|
|
@@ -69,9 +69,13 @@ module Utopia
|
|
|
69
69
|
ranges = byte_ranges(request)
|
|
70
70
|
size = bytesize
|
|
71
71
|
|
|
72
|
-
#
|
|
73
|
-
|
|
74
|
-
|
|
72
|
+
# A valid byte-range request with no satisfiable ranges cannot be fulfilled:
|
|
73
|
+
if ranges && ranges.empty?
|
|
74
|
+
response_headers[CONTENT_LENGTH] = "0"
|
|
75
|
+
response_headers[CONTENT_RANGE] = "bytes */#{size}"
|
|
76
|
+
|
|
77
|
+
return Response[416, response_headers, []]
|
|
78
|
+
elsif ranges == nil or ranges.size != 1
|
|
75
79
|
# No ranges, or multiple ranges (which we don't support).
|
|
76
80
|
# TODO: Support multiple byte-ranges, for now just send entire file:
|
|
77
81
|
status = 200
|
|
@@ -11,8 +11,6 @@ require_relative "local_file"
|
|
|
11
11
|
require_relative "mime_types"
|
|
12
12
|
require_relative "../localization/resolver"
|
|
13
13
|
|
|
14
|
-
require "traces/provider"
|
|
15
|
-
|
|
16
14
|
module Utopia
|
|
17
15
|
module Static
|
|
18
16
|
DEFAULT_CACHE_CONTROL = "public, max-age=3600".freeze
|
|
@@ -138,15 +136,5 @@ module Utopia
|
|
|
138
136
|
end
|
|
139
137
|
end
|
|
140
138
|
|
|
141
|
-
Traces::Provider(Static) do
|
|
142
|
-
def respond(request, path, extension, content_type, localization: request.localization)
|
|
143
|
-
attributes = {
|
|
144
|
-
path: path,
|
|
145
|
-
locale: localization&.locale,
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
Traces.trace("utopia.static.respond", attributes: attributes){super}
|
|
149
|
-
end
|
|
150
|
-
end
|
|
151
139
|
end
|
|
152
140
|
end
|
data/lib/utopia/version.rb
CHANGED
data/readme.md
CHANGED
|
@@ -31,6 +31,15 @@ Please see the [project documentation](https://socketry.github.io/utopia/) for m
|
|
|
31
31
|
|
|
32
32
|
Please see the [project releases](https://socketry.github.io/utopia/releases/index) for all releases.
|
|
33
33
|
|
|
34
|
+
### v3.0.5
|
|
35
|
+
|
|
36
|
+
- **Breaking** Remove support for JavaScript packages installed in `lib/components`; use `node_modules` instead.
|
|
37
|
+
- **Breaking** Expose <code class="language-ruby">Utopia::Content::Middleware\#links</code> as the content link resolver rather than an indexed lookup method.
|
|
38
|
+
- **Security** Authenticate encrypted session cookies using AES-256-GCM. Existing session cookies are invalidated.
|
|
39
|
+
- Constrain content node local paths to the configured content root.
|
|
40
|
+
- **Security** Redact sensitive exception report fields and make bounded request body attachments opt-in.
|
|
41
|
+
- Return `416 Range Not Satisfiable` for unsatisfiable static file byte ranges.
|
|
42
|
+
|
|
34
43
|
### v3.0.0
|
|
35
44
|
|
|
36
45
|
The 3.0.x series is considered a development release while the protocol HTTP application and controller interfaces stabilize.
|
data/releases.md
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
# Releases
|
|
2
2
|
|
|
3
|
+
## v3.0.5
|
|
4
|
+
|
|
5
|
+
- **Breaking** Remove support for JavaScript packages installed in `lib/components`; use `node_modules` instead.
|
|
6
|
+
- **Breaking** Expose {ruby Utopia::Content::Middleware\#links} as the content link resolver rather than an indexed lookup method.
|
|
7
|
+
- **Security** Authenticate encrypted session cookies using AES-256-GCM. Existing session cookies are invalidated.
|
|
8
|
+
- Constrain content node local paths to the configured content root.
|
|
9
|
+
- **Security** Redact sensitive exception report fields and make bounded request body attachments opt-in.
|
|
10
|
+
- Return `416 Range Not Satisfiable` for unsatisfiable static file byte ranges.
|
|
11
|
+
|
|
3
12
|
## v3.0.0
|
|
4
13
|
|
|
5
14
|
The 3.0.x series is considered a development release while the protocol HTTP application and controller interfaces stabilize.
|
data.tar.gz.sig
CHANGED
|
Binary file
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: utopia
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 3.0.
|
|
4
|
+
version: 3.0.5
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Samuel Williams
|
|
@@ -161,14 +161,14 @@ dependencies:
|
|
|
161
161
|
requirements:
|
|
162
162
|
- - "~>"
|
|
163
163
|
- !ruby/object:Gem::Version
|
|
164
|
-
version: '0.
|
|
164
|
+
version: '0.71'
|
|
165
165
|
type: :runtime
|
|
166
166
|
prerelease: false
|
|
167
167
|
version_requirements: !ruby/object:Gem::Requirement
|
|
168
168
|
requirements:
|
|
169
169
|
- - "~>"
|
|
170
170
|
- !ruby/object:Gem::Version
|
|
171
|
-
version: '0.
|
|
171
|
+
version: '0.71'
|
|
172
172
|
- !ruby/object:Gem::Dependency
|
|
173
173
|
name: protocol-media
|
|
174
174
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -285,6 +285,9 @@ files:
|
|
|
285
285
|
- context/server-setup.md
|
|
286
286
|
- context/updating-utopia.md
|
|
287
287
|
- context/what-is-xnode.md
|
|
288
|
+
- lib/traces/provider/utopia.rb
|
|
289
|
+
- lib/traces/provider/utopia/content/middleware.rb
|
|
290
|
+
- lib/traces/provider/utopia/static/middleware.rb
|
|
288
291
|
- lib/utopia.rb
|
|
289
292
|
- lib/utopia/application.rb
|
|
290
293
|
- lib/utopia/components.rb
|
|
@@ -311,6 +314,7 @@ files:
|
|
|
311
314
|
- lib/utopia/controller/rewrite.rb
|
|
312
315
|
- lib/utopia/controller/variables.rb
|
|
313
316
|
- lib/utopia/exceptions.rb
|
|
317
|
+
- lib/utopia/exceptions/application_errors.rb
|
|
314
318
|
- lib/utopia/exceptions/handler.rb
|
|
315
319
|
- lib/utopia/exceptions/mailer.rb
|
|
316
320
|
- lib/utopia/extensions/array_split.rb
|
metadata.gz.sig
CHANGED
|
Binary file
|