forest_admin_agent 1.33.1 → 1.34.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:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 5eac03c350cacd4d12aa3408eb1bc3d0546f7d5999974335b870343609ac3375
|
|
4
|
+
data.tar.gz: 6d21b84fda83ad2f90bf157c8fc8a0b7f65710d1159cf771645a11f0349c8037
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 3c5b3a78c4ac0fa919dfdbddf7518eac9520b36e47a5c258c3f97651fd12989f2fad2ef65a669c4ee6bf3862921ba91b505eb6c4d70a615001e32da927b7c7e1
|
|
7
|
+
data.tar.gz: 424a07e610f49273e9d12751369ade15ead966d6876f0bf70af4acb5b68444ab508f2b51ef31e3522c2c3b51cfb6fb7e3649d2993c18b194212ef270dfe8669e
|
|
@@ -1,45 +1,45 @@
|
|
|
1
1
|
require 'faraday'
|
|
2
|
+
require 'uri'
|
|
2
3
|
|
|
3
4
|
module ForestAdminAgent
|
|
4
5
|
module Routes
|
|
5
6
|
module Workflow
|
|
6
|
-
#
|
|
7
|
-
# Mounted only when
|
|
7
|
+
# Generic proxy: forwards any sub-path/verb under AGENT_PREFIX to the executor verbatim, so a
|
|
8
|
+
# new executor route needs no change here. Mounted only when `workflow_executor_url` is set.
|
|
8
9
|
class WorkflowExecutorProxy < AbstractAuthenticatedRoute
|
|
9
|
-
AGENT_PREFIX = '/_internal/
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
10
|
+
AGENT_PREFIX = '/_internal/executor'.freeze
|
|
11
|
+
# Never forwarded (request or response): hop-by-hop, Host, and body-framing headers.
|
|
12
|
+
# Faraday and `render json:` set their own length and de/recompress the body, so relaying
|
|
13
|
+
# the upstream length/encoding would mismatch the bytes we actually send — and forwarding
|
|
14
|
+
# accept-encoding would disable Faraday's transparent gzip decompression.
|
|
15
|
+
SKIPPED_HEADERS = %w[
|
|
16
|
+
connection keep-alive transfer-encoding upgrade te trailer
|
|
17
|
+
proxy-authenticate proxy-authorization host
|
|
18
|
+
content-length content-encoding accept-encoding
|
|
19
|
+
].freeze
|
|
20
|
+
# Fragments that could let the wildcard leave the executor origin once decoded.
|
|
21
|
+
UNSAFE_PATH_FRAGMENTS = ['..', '%2e', '%2E', '\\', "\0"].freeze
|
|
13
22
|
OPEN_TIMEOUT = 2
|
|
14
|
-
|
|
15
|
-
TRIGGER_TIMEOUT = 120
|
|
23
|
+
REQUEST_TIMEOUT = 120
|
|
16
24
|
|
|
17
25
|
def setup_routes
|
|
18
26
|
return self unless executor_configured?
|
|
19
27
|
|
|
20
28
|
add_route(
|
|
21
|
-
'
|
|
22
|
-
|
|
23
|
-
"#{AGENT_PREFIX}
|
|
24
|
-
->(args) { handle_request(
|
|
25
|
-
)
|
|
26
|
-
add_route(
|
|
27
|
-
'forest_workflow_run_trigger',
|
|
28
|
-
'post',
|
|
29
|
-
"#{AGENT_PREFIX}/:run_id/trigger",
|
|
30
|
-
->(args) { handle_request(:post, args) }
|
|
29
|
+
'forest_workflow_executor_proxy',
|
|
30
|
+
:all,
|
|
31
|
+
"#{AGENT_PREFIX}/*path",
|
|
32
|
+
->(args) { handle_request(args) }
|
|
31
33
|
)
|
|
32
34
|
|
|
33
35
|
self
|
|
34
36
|
end
|
|
35
37
|
|
|
36
|
-
def handle_request(
|
|
38
|
+
def handle_request(args = {})
|
|
37
39
|
build(args)
|
|
38
40
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
path = build_path(run_id, method)
|
|
42
|
-
response = forward(method, base_url, path, args)
|
|
41
|
+
method = (args[:method] || 'get').to_s.downcase.to_sym
|
|
42
|
+
response = forward(method, build_target_url(args), args)
|
|
43
43
|
|
|
44
44
|
{
|
|
45
45
|
content: response.body,
|
|
@@ -67,76 +67,98 @@ module ForestAdminAgent
|
|
|
67
67
|
url.to_s.sub(%r{/+\z}, '')
|
|
68
68
|
end
|
|
69
69
|
|
|
70
|
-
def
|
|
71
|
-
|
|
72
|
-
|
|
70
|
+
def build_target_url(args)
|
|
71
|
+
base = configured_executor_url
|
|
72
|
+
path = build_executor_path(args.dig(:params, 'path') || args.dig(:params, :path))
|
|
73
|
+
reject_off_origin!(base, path)
|
|
74
|
+
query = args[:query_string].to_s
|
|
75
|
+
url = "#{base}#{path}"
|
|
76
|
+
|
|
77
|
+
query.empty? ? url : "#{url}?#{query}"
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# First-pass rejection of escape attempts; the authoritative origin check is reject_off_origin!.
|
|
81
|
+
def build_executor_path(raw_path)
|
|
82
|
+
path = raw_path.to_s
|
|
83
|
+
raise Http::Exceptions::NotFoundError, 'Invalid workflow executor path' if unsafe_path?(path)
|
|
84
|
+
|
|
85
|
+
"/#{path}"
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Authoritative SSRF check: forwarding must never leave the executor origin.
|
|
89
|
+
def reject_off_origin!(base, path)
|
|
90
|
+
base_uri = URI.parse(base)
|
|
91
|
+
target = URI.parse("#{base}#{path}")
|
|
92
|
+
same = target.scheme == base_uri.scheme && target.host == base_uri.host &&
|
|
93
|
+
target.port == base_uri.port
|
|
94
|
+
raise Http::Exceptions::NotFoundError, 'Invalid workflow executor path' unless same
|
|
95
|
+
rescue URI::InvalidURIError
|
|
96
|
+
raise Http::Exceptions::NotFoundError, 'Invalid workflow executor path'
|
|
73
97
|
end
|
|
74
98
|
|
|
75
|
-
def
|
|
76
|
-
|
|
99
|
+
def unsafe_path?(path)
|
|
100
|
+
return true if path.empty? || path.start_with?('/')
|
|
101
|
+
|
|
102
|
+
UNSAFE_PATH_FRAGMENTS.any? { |fragment| path.include?(fragment) }
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def forward(method, target_url, args)
|
|
77
106
|
headers = forwarded_request_headers(args[:headers])
|
|
78
|
-
body =
|
|
79
|
-
target_url = "#{base_url}#{path}"
|
|
107
|
+
body = method == :get ? nil : args[:body]
|
|
80
108
|
|
|
81
|
-
|
|
82
|
-
client.run_request(method, target_url, body, headers) do |req|
|
|
83
|
-
req.params.update(query) unless query.empty?
|
|
84
|
-
end
|
|
109
|
+
build_client.run_request(method, target_url, body, headers)
|
|
85
110
|
rescue Faraday::TimeoutError => e
|
|
86
111
|
raise Http::Exceptions::ServiceUnavailableError.new('Workflow executor timed out', cause: e)
|
|
87
112
|
rescue Faraday::ConnectionFailed => e
|
|
88
113
|
raise Http::Exceptions::ServiceUnavailableError.new('Workflow executor unreachable', cause: e)
|
|
114
|
+
# Any other transport-level Faraday failure (SSL, etc.) is an executor-reachability problem,
|
|
115
|
+
# not a 500 — Faraday never raises on executor 4xx/5xx (no raise_error middleware).
|
|
116
|
+
rescue Faraday::Error => e
|
|
117
|
+
raise Http::Exceptions::ServiceUnavailableError.new('Workflow executor request failed', cause: e)
|
|
89
118
|
end
|
|
90
119
|
|
|
91
|
-
def
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
def build_client(request_timeout)
|
|
96
|
-
Faraday.new(request: { open_timeout: OPEN_TIMEOUT, timeout: request_timeout }) do |f|
|
|
97
|
-
f.request :json
|
|
120
|
+
def build_client
|
|
121
|
+
Faraday.new(request: { open_timeout: OPEN_TIMEOUT, timeout: REQUEST_TIMEOUT }) do |f|
|
|
122
|
+
# No request :json middleware: body is forwarded raw (not reshaped).
|
|
98
123
|
f.response :json, content_type: /\bjson$/
|
|
99
124
|
f.adapter Faraday.default_adapter
|
|
100
125
|
end
|
|
101
126
|
end
|
|
102
127
|
|
|
103
|
-
#
|
|
104
|
-
|
|
105
|
-
|
|
128
|
+
# `env` is the Rack env: real HTTP headers are the HTTP_* keys (+ CONTENT_TYPE/CONTENT_LENGTH);
|
|
129
|
+
# rack.*/action_dispatch.*/server vars are not headers and are dropped.
|
|
130
|
+
def forwarded_request_headers(env)
|
|
131
|
+
return {} unless env.is_a?(Hash)
|
|
106
132
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
next
|
|
133
|
+
env.each_with_object({}) do |(key, value), acc|
|
|
134
|
+
name = http_header_name(key.to_s)
|
|
135
|
+
next unless name
|
|
136
|
+
next if SKIPPED_HEADERS.include?(name.downcase)
|
|
137
|
+
next if value.nil? || value.to_s.empty?
|
|
110
138
|
|
|
111
|
-
acc[
|
|
139
|
+
acc[name] = value.to_s
|
|
112
140
|
end
|
|
113
141
|
end
|
|
114
142
|
|
|
115
|
-
def
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
acc[name] = value if value && !value.to_s.empty?
|
|
143
|
+
def http_header_name(env_key)
|
|
144
|
+
if env_key.start_with?('HTTP_')
|
|
145
|
+
titleize_header(env_key.delete_prefix('HTTP_'))
|
|
146
|
+
elsif %w[CONTENT_TYPE CONTENT_LENGTH].include?(env_key)
|
|
147
|
+
titleize_header(env_key)
|
|
121
148
|
end
|
|
122
149
|
end
|
|
123
150
|
|
|
124
|
-
def
|
|
125
|
-
|
|
126
|
-
return nil unless params.is_a?(Hash)
|
|
127
|
-
|
|
128
|
-
# JSON request bodies arrive parsed under :data when sent as JSON:API,
|
|
129
|
-
# or as the raw top-level params hash otherwise. Prefer :data when
|
|
130
|
-
# present; fall back to a sanitized copy of params.
|
|
131
|
-
body = params['data'] || params[:data]
|
|
132
|
-
return body if body
|
|
133
|
-
|
|
134
|
-
params.reject { |key, _| ROUTING_KEYS.include?(key.to_s) }
|
|
151
|
+
def titleize_header(rack_name)
|
|
152
|
+
rack_name.split('_').map(&:capitalize).join('-')
|
|
135
153
|
end
|
|
136
154
|
|
|
137
155
|
def forwarded_response_headers(response)
|
|
138
|
-
|
|
139
|
-
|
|
156
|
+
response.headers.each_with_object({}) do |(name, value), acc|
|
|
157
|
+
next if name.nil? || SKIPPED_HEADERS.include?(name.to_s.downcase)
|
|
158
|
+
next if value.nil? || value.to_s.empty?
|
|
159
|
+
|
|
160
|
+
acc[name.to_s] = value.to_s
|
|
161
|
+
end
|
|
140
162
|
end
|
|
141
163
|
end
|
|
142
164
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: forest_admin_agent
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.
|
|
4
|
+
version: 1.34.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Matthieu
|
|
@@ -9,7 +9,7 @@ authors:
|
|
|
9
9
|
autorequire:
|
|
10
10
|
bindir: exe
|
|
11
11
|
cert_chain: []
|
|
12
|
-
date: 2026-06-
|
|
12
|
+
date: 2026-06-26 00:00:00.000000000 Z
|
|
13
13
|
dependencies:
|
|
14
14
|
- !ruby/object:Gem::Dependency
|
|
15
15
|
name: activesupport
|