html2rss 0.29.1 → 0.30.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.
@@ -1,233 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'brotli'
4
- require 'faraday'
5
- require 'faraday/follow_redirects'
6
- require 'faraday/gzip'
7
-
8
- module Html2rss
9
- class RequestService
10
- ##
11
- # Strategy to use Faraday for the request.
12
- # @see https://rubygems.org/gems/faraday
13
- # rubocop:disable-next Metrics/ClassLength -- terminal redirect retry colocated with Faraday transport
14
- class FaradayStrategy < Strategy
15
- ##
16
- # Restores buffered streamed bytes so response middleware can process them.
17
- class StreamingBodyMiddleware < Faraday::Middleware
18
- # Request-context key used to store streamed chunks before middleware completion.
19
- STREAM_BUFFER_KEY = :html2rss_stream_buffer
20
-
21
- # @param env [Faraday::Env] completed response environment
22
- # @return [void]
23
- def on_complete(env)
24
- buffer = env.request.context&.delete(STREAM_BUFFER_KEY)
25
- return if buffer.nil? || buffer.empty?
26
-
27
- env.body = buffer
28
- end
29
- end
30
-
31
- ##
32
- # @return [ResponseGuard]
33
- attr_reader :response_guard
34
-
35
- # Executes the request with runtime policy enforcement, returning the normalized response.
36
- #
37
- # @return [Response] normalized response
38
- def perform_execute
39
- deadline = request_deadline
40
- @response_guard = ResponseGuard.new(policy: ctx.policy)
41
- reset_redirect_tracking!
42
- raw_response = request_with_terminal_redirect_retry(response_guard, deadline:)
43
- raw_response = retry_without_streaming(response_guard, deadline:) if retry_without_streaming?(raw_response)
44
- build_response(raw_response)
45
- end
46
-
47
- private
48
-
49
- def request_deadline
50
- monotonic_now + ctx.budget.effective_timeout_seconds(fallback: ctx.policy.total_timeout_seconds)
51
- end
52
-
53
- def reset_redirect_tracking!
54
- @last_redirect_to = nil
55
- @terminal_redirect_retried = false
56
- @request_url_override = nil
57
- end
58
-
59
- def request_with_terminal_redirect_retry(response_guard, deadline:)
60
- faraday_request(response_guard, deadline:, streaming_buffer: true)
61
- rescue Faraday::FollowRedirects::RedirectLimitReached => error
62
- raise RedirectLimitReached, error.message unless terminal_redirect_retryable?
63
-
64
- begin
65
- retry_from_terminal_redirect!(response_guard, deadline:)
66
- rescue Faraday::FollowRedirects::RedirectLimitReached => retry_error
67
- raise RedirectLimitReached, retry_error.message
68
- end
69
- end
70
-
71
- def terminal_redirect_retryable?
72
- return false if @terminal_redirect_retried || @last_redirect_to.nil?
73
-
74
- @last_redirect_to.to_s != request_url.to_s
75
- end
76
-
77
- def retry_from_terminal_redirect!(response_guard, deadline:)
78
- terminal_url = @last_redirect_to
79
- @terminal_redirect_retried = true
80
- Log.debug("#{self.class}: redirect limit reached; retrying once from #{terminal_url}")
81
- begin_terminal_url_request!(terminal_url)
82
- faraday_request(response_guard, deadline:, streaming_buffer: true, consume_budget: false)
83
- end
84
-
85
- def begin_terminal_url_request!(terminal_url)
86
- ctx.policy.validate_request!(url: terminal_url, origin_url: ctx.origin_url, relation: ctx.relation)
87
- @request_url_override = terminal_url
88
- @client = nil
89
- @last_redirect_to = nil
90
- end
91
-
92
- def request_url
93
- @request_url_override || ctx.url
94
- end
95
-
96
- def build_response(response)
97
- Response.new(body: CompressedBody.decode(response.body, headers: response.headers),
98
- headers: response.headers, url: response_url(response),
99
- status: response.status)
100
- end
101
-
102
- def faraday_request(response_guard, deadline:, streaming_buffer:, consume_budget: true)
103
- preflight!(consume_budget:)
104
-
105
- client.get do |req|
106
- apply_timeouts(req, deadline:)
107
- next unless streaming_buffer
108
-
109
- buffer = prepare_stream_buffer(req)
110
- req.options.on_data = on_data_callback(response_guard, buffer)
111
- end
112
- end
113
-
114
- def retry_without_streaming(response_guard, deadline:)
115
- faraday_request(response_guard, deadline:, streaming_buffer: false, consume_budget: false)
116
- end
117
-
118
- ##
119
- # Validates the remote socket peer IP during Net::HTTP start.
120
- module PeerIpValidator
121
- module_function
122
-
123
- # @param http [Net::HTTP] connection to configure
124
- # @param policy [Policy] request policy
125
- # @return [void]
126
- def install!(http, policy:)
127
- orig_start = http.method(:start)
128
- http.define_singleton_method(:start) do |&block|
129
- orig_start.call do |opened_http|
130
- PeerIpValidator.validate!(opened_http, policy:)
131
- block.call(opened_http)
132
- end
133
- end
134
- end
135
-
136
- # @param opened_http [Net::HTTP] active connection
137
- # @param policy [Policy] request policy
138
- # @return [void]
139
- def validate!(opened_http, policy:)
140
- scheme = opened_http.use_ssl? ? 'https' : 'http'
141
- url = Html2rss::Url.from_absolute("#{scheme}://#{opened_http.address}:#{opened_http.port}")
142
- policy.validate_remote_ip!(ip: peer_ip_for(opened_http), url:)
143
- end
144
-
145
- # @param opened_http [Net::HTTP]
146
- # @return [String, nil]
147
- def peer_ip_for(opened_http)
148
- sock = opened_http.instance_variable_get(:@socket)
149
- io = sock.respond_to?(:io) ? sock.io : sock
150
- peeraddr = io.respond_to?(:peeraddr) ? io.peeraddr : nil
151
- peeraddr&.[](3) || peeraddr&.[](2)
152
- end
153
- end
154
-
155
- # rubocop:disable-next Metrics/AbcSize
156
- def client
157
- @client ||= Faraday.new(url: request_url.to_s, headers: ctx.headers) do |faraday|
158
- faraday.use Faraday::FollowRedirects::Middleware, limit: ctx.policy.max_redirects, callback: redirect_callback
159
- faraday.request :gzip
160
- faraday.use StreamingBodyMiddleware
161
- faraday.adapter Faraday.default_adapter do |http|
162
- PeerIpValidator.install!(http, policy: ctx.policy)
163
- end
164
- end
165
- end
166
-
167
- def apply_timeouts(request, deadline:)
168
- remaining_timeout = remaining_timeout_seconds(deadline)
169
- request.options.timeout = remaining_timeout
170
- request.options.open_timeout = [ctx.policy.connect_timeout_seconds, remaining_timeout].min
171
- request.options.read_timeout = [ctx.policy.read_timeout_seconds, remaining_timeout].min
172
- end
173
-
174
- def prepare_stream_buffer(request)
175
- request.options.context ||= {}
176
- request.options.context[StreamingBodyMiddleware::STREAM_BUFFER_KEY] = +''
177
- end
178
-
179
- def on_data_callback(response_guard, buffer)
180
- proc do |chunk, total_bytes, env|
181
- response_guard.inspect_chunk!(total_bytes:, headers: env&.response_headers)
182
- buffer&.<< chunk
183
- end
184
- end
185
-
186
- def remaining_timeout_seconds(deadline)
187
- remaining = deadline - monotonic_now
188
- raise RequestTimedOut, 'Request timed out' if remaining <= 0
189
-
190
- remaining
191
- end
192
-
193
- def retry_without_streaming?(response)
194
- return false if response.body.to_s.empty? == false
195
- return false unless response_success?(response)
196
-
197
- final_url = response.env&.url
198
- return false unless final_url
199
-
200
- final_url.to_s != ctx.url.to_s
201
- end
202
-
203
- def response_success?(response)
204
- return true if response.status.nil?
205
-
206
- response.status >= 200 && response.status < 300
207
- end
208
-
209
- def response_url(response)
210
- return ctx.url unless (url = response.env&.url)
211
-
212
- Html2rss::Url.from_absolute(url.to_s)
213
- end
214
-
215
- def redirect_callback
216
- lambda do |old_env, new_env|
217
- from_url = normalize_url(old_env[:url])
218
- to_url = normalize_url(new_env[:url])
219
- @last_redirect_to = to_url
220
- ctx.policy.validate_redirect!(from_url:, to_url:, origin_url: ctx.origin_url, relation: ctx.relation)
221
- end
222
- end
223
-
224
- def normalize_url(url)
225
- Html2rss::Url.from_absolute(url.to_s)
226
- end
227
-
228
- def monotonic_now
229
- Process.clock_gettime(Process::CLOCK_MONOTONIC)
230
- end
231
- end
232
- end
233
- end