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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2acc29c9f259551473810464771039fd61748d4387892bb42eda70cbe78648de
4
- data.tar.gz: d47575f34e2dd8ad24e6290c2633a2a5d57e6591439eb954ce3d74f60ea17749
3
+ metadata.gz: 1c74753b10258047c15aafb6de0c2b5900bc16f01f4b499405b5b72d0d6d3b9e
4
+ data.tar.gz: 55ad17c8d51cf4eb8e2ed9ec3fd7158a15e23becba666b204b11d7b7c81f0e47
5
5
  SHA512:
6
- metadata.gz: 784d7a77e084ffc3ffb1c4664f3e6519323146a1a40a1cd783b2144950ede1129702b1e32e4b5583449bc8786d438fcdf2b943730aecdc2d5c95bbc4944309f2
7
- data.tar.gz: 0d9d5aa7631bfc6459a5f66a7f9f181093a4365c4459ac27423d378532fad2b6858586a3b565bf985719f60aedb3a75aab5a47b86bd28ff3bbf36ce5c1280366
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
- return nil if Array(allowed).any? { |entry| covers?(normalize(entry), target) }
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 are refused
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.include?("/") }
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
- :cluster_allowed_slave_urls, :cluster_allow_insecure_http
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Profiler
4
- VERSION = "0.30.15"
4
+ VERSION = "0.31.0"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rails-profiler
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.30.15
4
+ version: 0.31.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sébastien Duplessy