rails-profiler 0.31.6 → 0.32.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +32 -1
- data/LICENSE +21 -0
- data/THIRD-PARTY-NOTICES.md +92 -0
- data/lib/profiler/configuration.rb +34 -1
- data/lib/profiler/local_request.rb +12 -11
- data/lib/profiler/mcp/http_guard.rb +84 -9
- data/lib/profiler/mcp/server.rb +10 -1
- data/lib/profiler/version.rb +1 -1
- metadata +4 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 696b15a9e7e1409bea43ef0541724ca559955c2dba15643c5743bf606c62ba60
|
|
4
|
+
data.tar.gz: 4091f3101110253c4ff00c32a0010e8b8097c329b7b2af10e91f13df82a15dbe
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 101f907c26e725cafa2d4fb70cab3ae957e0e3db9ddca412220137ae66af242f8c8397646bb8c239999e6d5b4d6121bf1c1b73169fe3c8ce982f7fbf2dcd066c
|
|
7
|
+
data.tar.gz: f966f148cd6cbb34867691d0f18549190b15e5059e21b87adbdf457b5b62e8cceeeac195f0693fe206e9117440e940d0f96aac4a25c8c411cfc84bec28ec5fb5
|
data/CHANGELOG.md
CHANGED
|
@@ -19,6 +19,37 @@ every commit of every tag interval is accounted for one way or the other.
|
|
|
19
19
|
|
|
20
20
|
## [Unreleased]
|
|
21
21
|
|
|
22
|
+
## [0.32.1] - 2026-10-13
|
|
23
|
+
|
|
24
|
+
<!-- stamped -->
|
|
25
|
+
|
|
26
|
+
### Fixed
|
|
27
|
+
|
|
28
|
+
- **Packaging:** The published gem now contains its `LICENSE` file, which the MIT license asks
|
|
29
|
+
every copy to carry and which was missing from the gem up to 0.32.0, and a new
|
|
30
|
+
`THIRD-PARTY-NOTICES.md` that reproduces the copyright and license notices of the three MIT
|
|
31
|
+
packages compiled into the dashboard and toolbar JavaScript: `preact`, `@tanstack/preact-query`
|
|
32
|
+
and `@tanstack/query-core`. The license of the gem does not change, it stays MIT.
|
|
33
|
+
|
|
34
|
+
## [0.32.0] - 2026-10-06
|
|
35
|
+
|
|
36
|
+
<!-- stamped -->
|
|
37
|
+
|
|
38
|
+
### Added
|
|
39
|
+
|
|
40
|
+
- **MCP:** The HTTP endpoint at `/_profiler/mcp` accepts the `Host` headers of a reverse proxy
|
|
41
|
+
without a monkey-patch: besides the loopback names, it now accepts the hosts the application lists
|
|
42
|
+
in `config.hosts` (when that list is not empty), so an application behind Traefik usually has
|
|
43
|
+
nothing to set, and the entries of the new `config.mcp_allowed_hosts` (default `[]`): a host name
|
|
44
|
+
or `host:port`, or a `Regexp` anchored with `\A` and `\z`, matched against the whole host name,
|
|
45
|
+
for example `["api.myapp.test", %r{\Amyapp[a-z0-9-]*\z}]`. With mcp 1.x the
|
|
46
|
+
transport only accepted the loopback names and answered `Forbidden: Invalid Host header` behind
|
|
47
|
+
a proxy. The profiler now makes the check itself, before the transport, for every kind of entry,
|
|
48
|
+
and keeps the transport's `Origin` check (same origin or `cors_allowed_origins`), whatever
|
|
49
|
+
`api_forgery_protection` says; the transport's own check, which only knows exact names, is turned
|
|
50
|
+
off. A `Regexp` that is not anchored, or that cannot match a whole host name, and a String
|
|
51
|
+
holding `*` raise an `ArgumentError` when assigned. The defaults keep the previous behaviour.
|
|
52
|
+
|
|
22
53
|
## [0.31.6] - 2026-10-05
|
|
23
54
|
|
|
24
55
|
<!-- stamped -->
|
|
@@ -388,7 +419,7 @@ every commit of every tag interval is accounted for one way or the other.
|
|
|
388
419
|
|
|
389
420
|
- **Cluster:** `config.cluster_allowed_slave_urls` accepts `Regexp` entries next to URLs, for
|
|
390
421
|
slaves whose names are not known in advance, such as one container per git worktree:
|
|
391
|
-
`[%r{\Ahttp://
|
|
422
|
+
`[%r{\Ahttp://myapp-[a-z0-9-]+:3000\z}]`. Until now such a setup had to fall back to
|
|
392
423
|
`:any`, which reopens the server-side request forgery closed in 0.30.11. A pattern must start
|
|
393
424
|
with `\A` and end with `\z`, and compile once wrapped to match the whole URL; any other `Regexp`
|
|
394
425
|
raises an `ArgumentError` when assigned. It is matched as a whole against the URL as the master
|
data/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Sébastien Duplessy
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# Third-party notices
|
|
2
|
+
|
|
3
|
+
The compiled JavaScript shipped in this gem (`app/assets/builds/profiler.js` and
|
|
4
|
+
`app/assets/builds/profiler-toolbar.js`) embeds the following third-party packages. Each one is
|
|
5
|
+
distributed under the MIT License; its copyright and permission notice is reproduced below, as that
|
|
6
|
+
license requires.
|
|
7
|
+
|
|
8
|
+
The license of this gem itself is in the `LICENSE` file.
|
|
9
|
+
|
|
10
|
+
## preact 10.29.0
|
|
11
|
+
|
|
12
|
+
- License: MIT
|
|
13
|
+
|
|
14
|
+
```text
|
|
15
|
+
The MIT License (MIT)
|
|
16
|
+
|
|
17
|
+
Copyright (c) 2015-present Jason Miller
|
|
18
|
+
|
|
19
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
20
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
21
|
+
in the Software without restriction, including without limitation the rights
|
|
22
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
23
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
24
|
+
furnished to do so, subject to the following conditions:
|
|
25
|
+
|
|
26
|
+
The above copyright notice and this permission notice shall be included in all
|
|
27
|
+
copies or substantial portions of the Software.
|
|
28
|
+
|
|
29
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
30
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
31
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
32
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
33
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
34
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
35
|
+
SOFTWARE.
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## @tanstack/preact-query 5.101.0
|
|
39
|
+
|
|
40
|
+
- License: MIT
|
|
41
|
+
|
|
42
|
+
```text
|
|
43
|
+
MIT License
|
|
44
|
+
|
|
45
|
+
Copyright (c) 2021-present Tanner Linsley
|
|
46
|
+
|
|
47
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
48
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
49
|
+
in the Software without restriction, including without limitation the rights
|
|
50
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
51
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
52
|
+
furnished to do so, subject to the following conditions:
|
|
53
|
+
|
|
54
|
+
The above copyright notice and this permission notice shall be included in all
|
|
55
|
+
copies or substantial portions of the Software.
|
|
56
|
+
|
|
57
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
58
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
59
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
60
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
61
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
62
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
63
|
+
SOFTWARE.
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## @tanstack/query-core 5.101.0
|
|
67
|
+
|
|
68
|
+
- License: MIT
|
|
69
|
+
|
|
70
|
+
```text
|
|
71
|
+
MIT License
|
|
72
|
+
|
|
73
|
+
Copyright (c) 2021-present Tanner Linsley
|
|
74
|
+
|
|
75
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
76
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
77
|
+
in the Software without restriction, including without limitation the rights
|
|
78
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
79
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
80
|
+
furnished to do so, subject to the following conditions:
|
|
81
|
+
|
|
82
|
+
The above copyright notice and this permission notice shall be included in all
|
|
83
|
+
copies or substantial portions of the Software.
|
|
84
|
+
|
|
85
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
86
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
87
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
88
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
89
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
90
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
91
|
+
SOFTWARE.
|
|
92
|
+
```
|
|
@@ -132,6 +132,7 @@ module Profiler
|
|
|
132
132
|
@cluster_secret = nil
|
|
133
133
|
@cluster_require_secret = true
|
|
134
134
|
@cluster_allowed_slave_urls = []
|
|
135
|
+
@mcp_allowed_hosts = []
|
|
135
136
|
@cluster_allow_insecure_http = false
|
|
136
137
|
# Where the profiler's own errors go (Profiler.log): Rails.logger when nil, or $stderr when
|
|
137
138
|
# there is none.
|
|
@@ -147,7 +148,16 @@ module Profiler
|
|
|
147
148
|
@tmp_path = value.nil? ? nil : Pathname.new(value)
|
|
148
149
|
end
|
|
149
150
|
|
|
150
|
-
attr_reader :cluster_allowed_slave_urls
|
|
151
|
+
attr_reader :cluster_allowed_slave_urls, :mcp_allowed_hosts
|
|
152
|
+
|
|
153
|
+
# Host names the MCP HTTP endpoint accepts beyond the loopback ones and config.hosts: a String
|
|
154
|
+
# (a host name, any port, or a host:port) or a Regexp anchored with \A and \z, matched
|
|
155
|
+
# against the host name without its port. A wildcard String is refused.
|
|
156
|
+
def mcp_allowed_hosts=(value)
|
|
157
|
+
require_relative "cluster/security"
|
|
158
|
+
Array(value).each { |entry| validate_mcp_allowed_host(entry) }
|
|
159
|
+
@mcp_allowed_hosts = Array(value)
|
|
160
|
+
end
|
|
151
161
|
|
|
152
162
|
# Deprecated: the figure it was meant for was never a byte count (see AllocationCounter).
|
|
153
163
|
# Read and written as allocated_objects_warning_threshold times the bytes per object the
|
|
@@ -300,5 +310,28 @@ module Profiler
|
|
|
300
310
|
raise Error, "Unknown storage backend: #{@storage}"
|
|
301
311
|
end
|
|
302
312
|
end
|
|
313
|
+
|
|
314
|
+
def validate_mcp_allowed_host(entry)
|
|
315
|
+
case entry
|
|
316
|
+
when String
|
|
317
|
+
return unless entry.strip.empty? || entry.include?("*")
|
|
318
|
+
|
|
319
|
+
raise ArgumentError, "config.mcp_allowed_hosts: #{entry.inspect} is not a host name; list each host, " \
|
|
320
|
+
"or use an anchored Regexp"
|
|
321
|
+
when Regexp then validate_mcp_allowed_host_pattern(entry)
|
|
322
|
+
else raise ArgumentError, "config.mcp_allowed_hosts: #{entry.inspect} must be a String or a Regexp"
|
|
323
|
+
end
|
|
324
|
+
end
|
|
325
|
+
|
|
326
|
+
def validate_mcp_allowed_host_pattern(pattern)
|
|
327
|
+
unless Profiler::Cluster::Security.anchored_pattern?(pattern)
|
|
328
|
+
raise ArgumentError, "config.mcp_allowed_hosts: #{pattern.inspect} must start with \\A and end with \\z, " \
|
|
329
|
+
"so that it matches the whole host name"
|
|
330
|
+
end
|
|
331
|
+
Profiler::Cluster::Security.whole_url_pattern(pattern)
|
|
332
|
+
rescue RegexpError => e
|
|
333
|
+
raise ArgumentError, "config.mcp_allowed_hosts: #{pattern.inspect} cannot be matched against the whole " \
|
|
334
|
+
"host name (#{e.message})"
|
|
335
|
+
end
|
|
303
336
|
end
|
|
304
337
|
end
|
|
@@ -85,6 +85,18 @@ module Profiler
|
|
|
85
85
|
false
|
|
86
86
|
end
|
|
87
87
|
|
|
88
|
+
# When the application restricts config.hosts, ActionDispatch::HostAuthorization has
|
|
89
|
+
# already refused every other host; a host it lists is one the developer trusts.
|
|
90
|
+
def permitted_by_rails_hosts?(host)
|
|
91
|
+
return false unless defined?(Rails) && Rails.respond_to?(:application) && Rails.application
|
|
92
|
+
return false unless defined?(ActionDispatch::HostAuthorization::Permissions)
|
|
93
|
+
|
|
94
|
+
hosts = Rails.application.config.hosts
|
|
95
|
+
return false if hosts.nil? || hosts.empty?
|
|
96
|
+
|
|
97
|
+
ActionDispatch::HostAuthorization::Permissions.new(hosts).allows?(host)
|
|
98
|
+
end
|
|
99
|
+
|
|
88
100
|
private
|
|
89
101
|
|
|
90
102
|
def host_denial_reason(request)
|
|
@@ -143,17 +155,6 @@ module Profiler
|
|
|
143
155
|
permitted_by_rails_hosts?(host)
|
|
144
156
|
end
|
|
145
157
|
|
|
146
|
-
# When the application restricts config.hosts, ActionDispatch::HostAuthorization has
|
|
147
|
-
# already refused every other host; a host it lists is one the developer trusts.
|
|
148
|
-
def permitted_by_rails_hosts?(host)
|
|
149
|
-
return false unless defined?(Rails) && Rails.respond_to?(:application) && Rails.application
|
|
150
|
-
return false unless defined?(ActionDispatch::HostAuthorization::Permissions)
|
|
151
|
-
|
|
152
|
-
hosts = Rails.application.config.hosts
|
|
153
|
-
return false if hosts.nil? || hosts.empty?
|
|
154
|
-
|
|
155
|
-
ActionDispatch::HostAuthorization::Permissions.new(hosts).allows?(host)
|
|
156
|
-
end
|
|
157
158
|
end
|
|
158
159
|
end
|
|
159
160
|
end
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
require "json"
|
|
4
4
|
require "uri"
|
|
5
|
+
require_relative "../cluster/security"
|
|
6
|
+
require_relative "../local_request"
|
|
5
7
|
|
|
6
8
|
module Profiler
|
|
7
9
|
module MCP
|
|
@@ -25,20 +27,86 @@ module Profiler
|
|
|
25
27
|
return deny("Profiler is disabled") unless config.enabled
|
|
26
28
|
|
|
27
29
|
request = ActionDispatch::Request.new(env)
|
|
30
|
+
if (host_denial = host_denial(request, config))
|
|
31
|
+
return deny(host_denial)
|
|
32
|
+
end
|
|
28
33
|
return deny("Not authorized to access the profiler") unless config.authorized?(request)
|
|
34
|
+
if (origin_denial = origin_denial(request, config))
|
|
35
|
+
return deny(origin_denial)
|
|
36
|
+
end
|
|
29
37
|
return nil unless config.api_forgery_protection
|
|
30
38
|
|
|
31
39
|
forgery_denial(request, config)
|
|
32
40
|
end
|
|
33
41
|
|
|
42
|
+
# The DNS rebinding check of the MCP transport, done here so that one rule covers every
|
|
43
|
+
# host: the transport itself only knows exact names, and is created without its own check
|
|
44
|
+
# (see Profiler::MCP::Server#http_transport). A Host header is accepted when it names a
|
|
45
|
+
# loopback address, a host the application lists in config.hosts (when that list is not
|
|
46
|
+
# empty, Rails' HostAuthorization already refuses every other host), or an entry of
|
|
47
|
+
# config.mcp_allowed_hosts. A request without a Host header (HTTP/1.0) is let through, as
|
|
48
|
+
# the transport did: the rebinding vector always carries one.
|
|
49
|
+
def allowed_host?(raw_host, config = Profiler.configuration)
|
|
50
|
+
return true if raw_host.nil?
|
|
51
|
+
|
|
52
|
+
full = raw_host.to_s.strip.downcase
|
|
53
|
+
name = host_name(full)
|
|
54
|
+
return false if name.empty?
|
|
55
|
+
return true if LOOPBACK_HOSTS.include?(name)
|
|
56
|
+
return true if Array(config.mcp_allowed_hosts).any? { |entry| host_entry_allows?(entry, name, full) }
|
|
57
|
+
|
|
58
|
+
# Rails matches config.hosts against the whole Host header (an entry may carry a port);
|
|
59
|
+
# the bare name covers entries written without one.
|
|
60
|
+
Profiler::LocalRequest.permitted_by_rails_hosts?(full) ||
|
|
61
|
+
Profiler::LocalRequest.permitted_by_rails_hosts?(name)
|
|
62
|
+
end
|
|
63
|
+
|
|
34
64
|
private
|
|
35
65
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
66
|
+
LOOPBACK_HOSTS = %w[localhost 127.0.0.1 ::1].freeze
|
|
67
|
+
|
|
68
|
+
def host_denial(request, config)
|
|
69
|
+
raw = request.get_header("HTTP_HOST")
|
|
70
|
+
return nil if allowed_host?(raw, config)
|
|
71
|
+
|
|
72
|
+
"Host #{raw} is not allowed for the MCP endpoint: list it in config.hosts or " \
|
|
73
|
+
"config.mcp_allowed_hosts"
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# A String matches the host name (any port) or the whole host:port, case-insensitively; a
|
|
77
|
+
# Regexp must match the whole host name, without the port.
|
|
78
|
+
def host_entry_allows?(entry, name, full)
|
|
79
|
+
case entry
|
|
80
|
+
when Regexp
|
|
81
|
+
Profiler::Cluster::Security.anchored_pattern?(entry) &&
|
|
82
|
+
Profiler::Cluster::Security.whole_url_pattern(entry).match?(name)
|
|
83
|
+
when String
|
|
84
|
+
allowed = entry.strip.downcase
|
|
85
|
+
allowed == name || allowed == full
|
|
86
|
+
else
|
|
87
|
+
false
|
|
40
88
|
end
|
|
89
|
+
rescue RegexpError
|
|
90
|
+
false
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# "[::1]:3000" gives "::1", "app.local:3000" gives "app.local". Anything else (two hosts, a
|
|
94
|
+
# port that is not a number, text after the brackets) gives "", which is refused.
|
|
95
|
+
def host_name(host)
|
|
96
|
+
match = host.match(/\A\[([^\]]+)\](?::\d+)?\z/) || host.match(/\A([^:\s,]+)(?::\d+)?\z/)
|
|
97
|
+
match ? match[1] : ""
|
|
98
|
+
end
|
|
41
99
|
|
|
100
|
+
# The Origin check of the transport, kept whatever api_forgery_protection says: a
|
|
101
|
+
# browser's Origin must be the profiler's own or one of cors_allowed_origins.
|
|
102
|
+
def origin_denial(request, config)
|
|
103
|
+
origin = request.get_header("HTTP_ORIGIN")
|
|
104
|
+
return nil if origin.nil? || origin.empty? || allowed_origin?(origin, request, config)
|
|
105
|
+
|
|
106
|
+
"Cross-origin request refused: Origin #{origin} is not allowed"
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def forgery_denial(request, _config)
|
|
42
110
|
if request.post? && media_type(request) != JSON_MEDIA_TYPE
|
|
43
111
|
return deny("A POST to the MCP endpoint needs Content-Type: #{JSON_MEDIA_TYPE}")
|
|
44
112
|
end
|
|
@@ -53,13 +121,20 @@ module Profiler
|
|
|
53
121
|
def allowed_origin?(origin, request, config)
|
|
54
122
|
return true if Array(config.cors_allowed_origins).include?(origin)
|
|
55
123
|
|
|
56
|
-
same_origin?(origin, request
|
|
124
|
+
same_origin?(origin, request)
|
|
57
125
|
end
|
|
58
126
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
127
|
+
# The Origin's host and port against the Host header, as the transport compares them: the
|
|
128
|
+
# scheme is left out, since behind a proxy that ends TLS the request says http while the
|
|
129
|
+
# browser's Origin says https, and the default port of the Origin's scheme is dropped.
|
|
130
|
+
def same_origin?(origin, request)
|
|
131
|
+
uri = URI.parse(origin)
|
|
132
|
+
host = request.get_header("HTTP_HOST").to_s.downcase
|
|
133
|
+
return false if uri.host.nil? || host.empty?
|
|
134
|
+
|
|
135
|
+
authority = uri.port == uri.default_port ? uri.host.downcase : "#{uri.host.downcase}:#{uri.port}"
|
|
136
|
+
host = host.delete_suffix(":#{uri.default_port}")
|
|
137
|
+
authority == host
|
|
63
138
|
rescue URI::InvalidURIError
|
|
64
139
|
false
|
|
65
140
|
end
|
data/lib/profiler/mcp/server.rb
CHANGED
|
@@ -46,12 +46,21 @@ module Profiler
|
|
|
46
46
|
|
|
47
47
|
def http_transport
|
|
48
48
|
@http_transport ||= begin
|
|
49
|
-
t = ::MCP::Server::Transports::StreamableHTTPTransport.new(@server)
|
|
49
|
+
t = ::MCP::Server::Transports::StreamableHTTPTransport.new(@server, **http_transport_options)
|
|
50
50
|
@server.transport = t
|
|
51
51
|
t
|
|
52
52
|
end
|
|
53
53
|
end
|
|
54
54
|
|
|
55
|
+
# HttpGuard checks the Host and Origin headers of every request before the transport sees
|
|
56
|
+
# it, with config.hosts, config.mcp_allowed_hosts and Regexp entries, which the transport's
|
|
57
|
+
# own check (exact names only) cannot express. Older mcp versions have no such check, nor
|
|
58
|
+
# the option.
|
|
59
|
+
def http_transport_options
|
|
60
|
+
params = ::MCP::Server::Transports::StreamableHTTPTransport.instance_method(:initialize).parameters
|
|
61
|
+
params.any? { |_type, name| name == :dns_rebinding_protection } ? { dns_rebinding_protection: false } : {}
|
|
62
|
+
end
|
|
63
|
+
|
|
55
64
|
def start(transport: :stdio)
|
|
56
65
|
case transport
|
|
57
66
|
when :stdio
|
data/lib/profiler/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: rails-profiler
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.32.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Sébastien Duplessy
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-10-
|
|
11
|
+
date: 2026-10-13 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: rails
|
|
@@ -104,6 +104,8 @@ extensions: []
|
|
|
104
104
|
extra_rdoc_files: []
|
|
105
105
|
files:
|
|
106
106
|
- CHANGELOG.md
|
|
107
|
+
- LICENSE
|
|
108
|
+
- THIRD-PARTY-NOTICES.md
|
|
107
109
|
- app/assets/builds/profiler-toolbar.js
|
|
108
110
|
- app/assets/builds/profiler.css
|
|
109
111
|
- app/assets/builds/profiler.js
|