rails-profiler 0.30.15 → 0.31.0
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
- data/CHANGELOG.md +19 -0
- data/lib/profiler/cluster/security.rb +46 -4
- data/lib/profiler/configuration.rb +25 -1
- data/lib/profiler/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 1c74753b10258047c15aafb6de0c2b5900bc16f01f4b499405b5b72d0d6d3b9e
|
|
4
|
+
data.tar.gz: 55ad17c8d51cf4eb8e2ed9ec3fd7158a15e23becba666b204b11d7b7c81f0e47
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: f1984f7c3ef795d9f318ddc33b64e964a8ed0171878fd913a927ad7c7d4479ba4523159598e75ddd3f52a8985bdaca9cb24bd49a8e0a0f6e1fbc025b904254ba
|
|
7
|
+
data.tar.gz: 855f08439f3cd174f3686150eed1f813b349e7591dd731b554941be172b9557ff835229268a3dce9605825e161b353a4456e444f1680ed239f40ebf1a6c37abc
|
data/CHANGELOG.md
CHANGED
|
@@ -19,6 +19,25 @@ every commit of every tag interval is accounted for one way or the other.
|
|
|
19
19
|
|
|
20
20
|
## [Unreleased]
|
|
21
21
|
|
|
22
|
+
## [0.31.0] - 2026-10-05
|
|
23
|
+
|
|
24
|
+
<!-- stamped -->
|
|
25
|
+
|
|
26
|
+
### Added
|
|
27
|
+
|
|
28
|
+
- **Cluster:** `config.cluster_allowed_slave_urls` accepts `Regexp` entries next to URLs, for
|
|
29
|
+
slaves whose names are not known in advance, such as one container per git worktree:
|
|
30
|
+
`[%r{\Ahttp://travel-api-[a-z0-9-]+:3000\z}]`. Until now such a setup had to fall back to
|
|
31
|
+
`:any`, which reopens the server-side request forgery closed in 0.30.11. A pattern must start
|
|
32
|
+
with `\A` and end with `\z`, and compile once wrapped to match the whole URL; any other `Regexp`
|
|
33
|
+
raises an `ArgumentError` when assigned. It is matched as a whole against the URL as the master
|
|
34
|
+
keeps it (lower-case scheme and host, no default port, no trailing slash), after the existing
|
|
35
|
+
checks, which it cannot lift: user info, a query, a fragment or a `.` or `..` segment are still
|
|
36
|
+
refused, and plain HTTP to a host that is not a loopback address still needs
|
|
37
|
+
`cluster_allow_insecure_http`. A `String` entry is always a URL, never a pattern. The default,
|
|
38
|
+
`[]`, still refuses every URL. A slave URL whose path segment decodes to a `\` is now refused
|
|
39
|
+
like one that decodes to a `/`.
|
|
40
|
+
|
|
22
41
|
## [0.30.15] - 2026-10-05
|
|
23
42
|
|
|
24
43
|
<!-- stamped -->
|
|
@@ -82,11 +82,27 @@ module Profiler
|
|
|
82
82
|
|
|
83
83
|
allowed = config.cluster_allowed_slave_urls
|
|
84
84
|
return nil if allowed == :any
|
|
85
|
-
|
|
85
|
+
|
|
86
|
+
registered = normalized_url(url)
|
|
87
|
+
return nil if Array(allowed).any? { |entry| entry_allows?(entry, target, registered) }
|
|
86
88
|
|
|
87
89
|
"#{url} is not allowed by config.cluster_allowed_slave_urls"
|
|
88
90
|
end
|
|
89
91
|
|
|
92
|
+
# Whether a Regexp entry of cluster_allowed_slave_urls is anchored at both ends: its source
|
|
93
|
+
# starts with \A and ends with an unescaped \z. \Z, ^ and $ do not count, since they let a
|
|
94
|
+
# trailing newline or another line through. A check of the written form, so that a pattern
|
|
95
|
+
# reads as what it does; the match itself is made whole by whole_url_pattern.
|
|
96
|
+
def anchored_pattern?(pattern)
|
|
97
|
+
return false unless pattern.is_a?(Regexp)
|
|
98
|
+
|
|
99
|
+
source = pattern.source
|
|
100
|
+
return false unless source.start_with?("\\A")
|
|
101
|
+
|
|
102
|
+
ending = source.match(/(\\+)z\z/)
|
|
103
|
+
!ending.nil? && ending[1].length.odd? && source.length > 4
|
|
104
|
+
end
|
|
105
|
+
|
|
90
106
|
# Same transport rule for the master URL a slave sends its secret to.
|
|
91
107
|
def master_url_denial(url)
|
|
92
108
|
target = normalize(url)
|
|
@@ -139,6 +155,32 @@ module Profiler
|
|
|
139
155
|
"#{path.inspect} is not a valid path"
|
|
140
156
|
end
|
|
141
157
|
|
|
158
|
+
# One entry of cluster_allowed_slave_urls against a URL already through normalize and the
|
|
159
|
+
# transport rule. A Regexp is matched, as a whole, against the URL as the registry keeps it
|
|
160
|
+
# (normalized_url), and only when anchored; anything else is an URL compared by covers?,
|
|
161
|
+
# so a String is never read as a pattern.
|
|
162
|
+
def entry_allows?(entry, target, registered)
|
|
163
|
+
if entry.is_a?(Regexp)
|
|
164
|
+
return false unless registered && anchored_pattern?(entry)
|
|
165
|
+
|
|
166
|
+
whole_url_pattern(entry).match?(registered)
|
|
167
|
+
else
|
|
168
|
+
covers?(normalize(entry), target)
|
|
169
|
+
end
|
|
170
|
+
rescue RegexpError
|
|
171
|
+
false
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
# The pattern wrapped again in \A(?:...)\z, so that a top-level alternation (\Aa|b\z)
|
|
175
|
+
# cannot leave one branch anchored at a single end: this wrapping, not anchored_pattern?,
|
|
176
|
+
# is what makes the match cover the whole URL. Compiled once per pattern. Raises
|
|
177
|
+
# RegexpError when the wrapping does not compile, for instance when an x-mode comment
|
|
178
|
+
# swallows the closing \z.
|
|
179
|
+
def whole_url_pattern(pattern)
|
|
180
|
+
@whole_url_patterns ||= {}
|
|
181
|
+
@whole_url_patterns[pattern] ||= Regexp.new("\\A(?:#{pattern.source})\\z", pattern.options)
|
|
182
|
+
end
|
|
183
|
+
|
|
142
184
|
# Scheme, host and port compared after normalization (case, default port, IPv6 brackets),
|
|
143
185
|
# and a path prefix compared segment by segment.
|
|
144
186
|
def covers?(entry, target)
|
|
@@ -149,8 +191,8 @@ module Profiler
|
|
|
149
191
|
end
|
|
150
192
|
|
|
151
193
|
# Returns { scheme:, host:, port:, segments: }, or nil for anything that is not a plain
|
|
152
|
-
# http(s) URL: user info, a query, a fragment, or a "." or ".." path segment
|
|
153
|
-
# rather than interpreted.
|
|
194
|
+
# http(s) URL: user info, a query, a fragment, or a "." or ".." path segment, or one that
|
|
195
|
+
# decodes to a "/" or a "\\", are refused rather than interpreted.
|
|
154
196
|
def normalize(url)
|
|
155
197
|
uri = URI.parse(url.to_s.strip)
|
|
156
198
|
return nil unless uri.is_a?(URI::HTTP) && uri.host && !uri.host.empty?
|
|
@@ -158,7 +200,7 @@ module Profiler
|
|
|
158
200
|
|
|
159
201
|
segments = uri.path.to_s.split("/").reject(&:empty?)
|
|
160
202
|
decoded = segments.map { |segment| URI.decode_www_form_component(segment) }
|
|
161
|
-
return nil if decoded.any? { |segment| segment == "." || segment == ".." || segment.
|
|
203
|
+
return nil if decoded.any? { |segment| segment == "." || segment == ".." || segment.match?(%r{[/\\]}) }
|
|
162
204
|
|
|
163
205
|
{ scheme: uri.scheme.downcase, host: uri.hostname.downcase, port: uri.port, segments: decoded }
|
|
164
206
|
rescue URI::Error, ArgumentError
|
|
@@ -47,7 +47,7 @@ module Profiler
|
|
|
47
47
|
:name, :master_url, :self_url,
|
|
48
48
|
:cluster_heartbeat_interval, :cluster_offline_threshold,
|
|
49
49
|
:cluster_master, :cluster_secret, :cluster_require_secret,
|
|
50
|
-
:
|
|
50
|
+
:cluster_allow_insecure_http
|
|
51
51
|
|
|
52
52
|
attr_writer :tmp_path
|
|
53
53
|
|
|
@@ -117,6 +117,30 @@ module Profiler
|
|
|
117
117
|
@tmp_path || default_tmp_path
|
|
118
118
|
end
|
|
119
119
|
|
|
120
|
+
attr_reader :cluster_allowed_slave_urls
|
|
121
|
+
|
|
122
|
+
# :any, or a list of slave URLs and anchored Regexp patterns (see Profiler::Cluster::Security).
|
|
123
|
+
# A Regexp that is not anchored with \A and \z is refused here, at boot, rather than ignored.
|
|
124
|
+
def cluster_allowed_slave_urls=(value)
|
|
125
|
+
unless value == :any
|
|
126
|
+
require_relative "cluster/security"
|
|
127
|
+
Array(value).grep(Regexp).each do |pattern|
|
|
128
|
+
unless Profiler::Cluster::Security.anchored_pattern?(pattern)
|
|
129
|
+
raise ArgumentError, "config.cluster_allowed_slave_urls: #{pattern.inspect} must start with \\A and " \
|
|
130
|
+
"end with \\z, so that it matches the whole slave URL"
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
begin
|
|
134
|
+
Profiler::Cluster::Security.whole_url_pattern(pattern)
|
|
135
|
+
rescue RegexpError => e
|
|
136
|
+
raise ArgumentError, "config.cluster_allowed_slave_urls: #{pattern.inspect} cannot be matched against " \
|
|
137
|
+
"the whole slave URL (#{e.message}); an x-mode comment must not hide the \\z"
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
@cluster_allowed_slave_urls = value
|
|
142
|
+
end
|
|
143
|
+
|
|
120
144
|
def enabled=(value)
|
|
121
145
|
@assigned |= [:enabled]
|
|
122
146
|
@enabled = value
|
data/lib/profiler/version.rb
CHANGED