ferrum 0.17.1 → 0.18.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/LICENSE +1 -1
- data/README.md +9 -1369
- data/lib/ferrum/accessibility/ax_node.rb +108 -0
- data/lib/ferrum/accessibility.rb +106 -0
- data/lib/ferrum/browser/binary.rb +41 -0
- data/lib/ferrum/browser/command.rb +20 -0
- data/lib/ferrum/browser/options/base.rb +69 -2
- data/lib/ferrum/browser/options/chrome.rb +88 -26
- data/lib/ferrum/browser/options/firefox.rb +32 -0
- data/lib/ferrum/browser/options.rb +43 -3
- data/lib/ferrum/browser/process.rb +56 -4
- data/lib/ferrum/browser/xvfb.rb +24 -0
- data/lib/ferrum/browser.rb +35 -9
- data/lib/ferrum/client/subscriber.rb +58 -0
- data/lib/ferrum/client/web_socket.rb +63 -12
- data/lib/ferrum/client.rb +206 -14
- data/lib/ferrum/context.rb +99 -4
- data/lib/ferrum/contexts.rb +136 -22
- data/lib/ferrum/cookies/cookie.rb +7 -1
- data/lib/ferrum/cookies.rb +5 -0
- data/lib/ferrum/dialog.rb +18 -2
- data/lib/ferrum/downloads.rb +52 -0
- data/lib/ferrum/errors.rb +62 -6
- data/lib/ferrum/frame/dom.rb +17 -0
- data/lib/ferrum/frame/runtime.rb +49 -8
- data/lib/ferrum/frame.rb +58 -1
- data/lib/ferrum/headers.rb +6 -0
- data/lib/ferrum/interceptable.rb +62 -0
- data/lib/ferrum/keyboard.rb +25 -0
- data/lib/ferrum/mouse.rb +6 -0
- data/lib/ferrum/network/auth_request.rb +81 -2
- data/lib/ferrum/network/error.rb +15 -0
- data/lib/ferrum/network/exchange.rb +10 -0
- data/lib/ferrum/network/intercepted_request.rb +94 -2
- data/lib/ferrum/network/request.rb +1 -1
- data/lib/ferrum/network/response.rb +16 -1
- data/lib/ferrum/network.rb +122 -10
- data/lib/ferrum/node.rb +305 -15
- data/lib/ferrum/page/animation.rb +7 -2
- data/lib/ferrum/page/frames.rb +69 -7
- data/lib/ferrum/page/screencast.rb +5 -0
- data/lib/ferrum/page/screenshot.rb +52 -20
- data/lib/ferrum/page/stream.rb +56 -0
- data/lib/ferrum/page/tracing.rb +6 -0
- data/lib/ferrum/page.rb +141 -46
- data/lib/ferrum/proxy.rb +52 -2
- data/lib/ferrum/rgba.rb +10 -0
- data/lib/ferrum/target.rb +124 -1
- data/lib/ferrum/utils/attempt.rb +20 -0
- data/lib/ferrum/utils/elapsed_time.rb +38 -0
- data/lib/ferrum/utils/event.rb +14 -0
- data/lib/ferrum/utils/platform.rb +22 -1
- data/lib/ferrum/utils/thread.rb +12 -0
- data/lib/ferrum/version.rb +1 -1
- data/lib/ferrum/worker.rb +125 -0
- data/lib/ferrum.rb +7 -0
- metadata +8 -21
|
@@ -124,7 +124,9 @@ module Ferrum
|
|
|
124
124
|
#
|
|
125
125
|
# @return [String]
|
|
126
126
|
#
|
|
127
|
-
|
|
127
|
+
# @raise [Ferrum::BrowserError]
|
|
128
|
+
#
|
|
129
|
+
def body!
|
|
128
130
|
@body ||= begin
|
|
129
131
|
body, encoded = @page.command("Network.getResponseBody", requestId: id)
|
|
130
132
|
.values_at("body", "base64Encoded")
|
|
@@ -132,6 +134,19 @@ module Ferrum
|
|
|
132
134
|
end
|
|
133
135
|
end
|
|
134
136
|
|
|
137
|
+
#
|
|
138
|
+
# The response body.
|
|
139
|
+
#
|
|
140
|
+
# @return [String, nil]
|
|
141
|
+
#
|
|
142
|
+
def body
|
|
143
|
+
body!
|
|
144
|
+
rescue Ferrum::BrowserError
|
|
145
|
+
# nop
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
#
|
|
149
|
+
# Whether this response is the main frame's current response.
|
|
135
150
|
#
|
|
136
151
|
# @return [Boolean]
|
|
137
152
|
#
|
data/lib/ferrum/network.rb
CHANGED
|
@@ -8,6 +8,13 @@ require "ferrum/network/request"
|
|
|
8
8
|
require "ferrum/network/response"
|
|
9
9
|
|
|
10
10
|
module Ferrum
|
|
11
|
+
#
|
|
12
|
+
# Tracks a page's network activity, exposing it as a list of {#traffic}
|
|
13
|
+
# {Network::Exchange}s built from the underlying CDP `Network.*` events.
|
|
14
|
+
# Also provides request interception/authorization (`intercept`,
|
|
15
|
+
# `authorize`, `blacklist=`/`whitelist=`) and network condition emulation
|
|
16
|
+
# (`emulate_network_conditions`, `offline_mode`).
|
|
17
|
+
#
|
|
11
18
|
class Network
|
|
12
19
|
CLEAR_TYPE = %i[traffic cache].freeze
|
|
13
20
|
AUTHORIZE_TYPE = %i[server proxy].freeze
|
|
@@ -17,7 +24,7 @@ module Ferrum
|
|
|
17
24
|
SignedExchange Ping CSPViolationReport Preflight Other].freeze
|
|
18
25
|
AUTHORIZE_BLOCK_MISSING = "Block is missing, call `authorize(...) { |r| r.continue } " \
|
|
19
26
|
"or subscribe to `on(:request)` events before calling it"
|
|
20
|
-
AUTHORIZE_TYPE_WRONG = ":type should be in #{AUTHORIZE_TYPE}"
|
|
27
|
+
AUTHORIZE_TYPE_WRONG = ":type should be in #{AUTHORIZE_TYPE}".freeze
|
|
21
28
|
ALLOWED_CONNECTION_TYPE = %w[none cellular2g cellular3g cellular4g bluetooth ethernet wifi wimax other].freeze
|
|
22
29
|
|
|
23
30
|
# Network traffic.
|
|
@@ -38,6 +45,7 @@ module Ferrum
|
|
|
38
45
|
@exchange = nil
|
|
39
46
|
@blacklist = nil
|
|
40
47
|
@whitelist = nil
|
|
48
|
+
@mutex = Mutex.new
|
|
41
49
|
end
|
|
42
50
|
|
|
43
51
|
#
|
|
@@ -81,18 +89,45 @@ module Ferrum
|
|
|
81
89
|
raise TimeoutError unless result
|
|
82
90
|
end
|
|
83
91
|
|
|
92
|
+
#
|
|
93
|
+
# Whether the network is idle, i.e. no more than `connections`
|
|
94
|
+
# connections are still pending.
|
|
95
|
+
#
|
|
96
|
+
# @param [Integer] connections
|
|
97
|
+
# How many connections are allowed for network to be idling.
|
|
98
|
+
#
|
|
99
|
+
# @return [Boolean]
|
|
100
|
+
#
|
|
84
101
|
def idle?(connections = 0)
|
|
85
102
|
pending_connections <= connections
|
|
86
103
|
end
|
|
87
104
|
|
|
105
|
+
#
|
|
106
|
+
# Total number of network connections seen since the traffic was last
|
|
107
|
+
# cleared.
|
|
108
|
+
#
|
|
109
|
+
# @return [Integer]
|
|
110
|
+
#
|
|
88
111
|
def total_connections
|
|
89
112
|
@traffic.size
|
|
90
113
|
end
|
|
91
114
|
|
|
115
|
+
#
|
|
116
|
+
# Number of network connections that have finished, i.e. were blocked, got
|
|
117
|
+
# a loaded response, errored, or are otherwise no longer pending.
|
|
118
|
+
#
|
|
119
|
+
# @return [Integer]
|
|
120
|
+
#
|
|
92
121
|
def finished_connections
|
|
93
122
|
@traffic.count(&:finished?)
|
|
94
123
|
end
|
|
95
124
|
|
|
125
|
+
#
|
|
126
|
+
# Number of network connections that are still pending, i.e. haven't
|
|
127
|
+
# finished yet.
|
|
128
|
+
#
|
|
129
|
+
# @return [Integer]
|
|
130
|
+
#
|
|
96
131
|
def pending_connections
|
|
97
132
|
total_connections - finished_connections
|
|
98
133
|
end
|
|
@@ -164,12 +199,38 @@ module Ferrum
|
|
|
164
199
|
true
|
|
165
200
|
end
|
|
166
201
|
|
|
202
|
+
#
|
|
203
|
+
# Sets a list of patterns for URLs that should be blocked from loading.
|
|
204
|
+
# Aborts any request whose URL matches one of the given patterns, and
|
|
205
|
+
# continues all others. Can't be used together with `whitelist=`.
|
|
206
|
+
#
|
|
207
|
+
# @param [String, Regexp, Array<String, Regexp>] patterns
|
|
208
|
+
# One or more patterns matched against the request's URL, see
|
|
209
|
+
# {InterceptedRequest#match?}.
|
|
210
|
+
#
|
|
211
|
+
# @example
|
|
212
|
+
# browser.network.blacklist = /jquery/
|
|
213
|
+
# browser.go_to("https://example.com/")
|
|
214
|
+
#
|
|
167
215
|
def blacklist=(patterns)
|
|
168
216
|
@blacklist = Array(patterns)
|
|
169
217
|
blacklist_subscribe
|
|
170
218
|
end
|
|
171
219
|
alias blocklist= blacklist=
|
|
172
220
|
|
|
221
|
+
#
|
|
222
|
+
# Sets a list of patterns for URLs that are the only ones allowed to load.
|
|
223
|
+
# Continues any request whose URL matches one of the given patterns, and
|
|
224
|
+
# aborts all others. Can't be used together with `blacklist=`.
|
|
225
|
+
#
|
|
226
|
+
# @param [String, Regexp, Array<String, Regexp>] patterns
|
|
227
|
+
# One or more patterns matched against the request's URL, see
|
|
228
|
+
# {InterceptedRequest#match?}.
|
|
229
|
+
#
|
|
230
|
+
# @example
|
|
231
|
+
# browser.network.whitelist = /example/
|
|
232
|
+
# browser.go_to("https://example.com/")
|
|
233
|
+
#
|
|
173
234
|
def whitelist=(patterns)
|
|
174
235
|
@whitelist = Array(patterns)
|
|
175
236
|
whitelist_subscribe
|
|
@@ -268,6 +329,11 @@ module Ferrum
|
|
|
268
329
|
end
|
|
269
330
|
end
|
|
270
331
|
|
|
332
|
+
#
|
|
333
|
+
# Subscribes to the CDP events needed to keep track of `traffic`. Called
|
|
334
|
+
# once when the page is initialized.
|
|
335
|
+
#
|
|
336
|
+
# @api private
|
|
271
337
|
def subscribe
|
|
272
338
|
subscribe_request_will_be_sent
|
|
273
339
|
subscribe_response_received
|
|
@@ -276,6 +342,23 @@ module Ferrum
|
|
|
276
342
|
subscribe_log_entry_added
|
|
277
343
|
end
|
|
278
344
|
|
|
345
|
+
#
|
|
346
|
+
# Builds the `authChallengeResponse` sent back to Chrome for an
|
|
347
|
+
# authenticated request, used by `authorize`.
|
|
348
|
+
#
|
|
349
|
+
# @param [Array<String>] ids
|
|
350
|
+
# Request ids that were already given credentials, to avoid an infinite
|
|
351
|
+
# retry loop if the credentials are rejected.
|
|
352
|
+
#
|
|
353
|
+
# @param [String] request_id
|
|
354
|
+
#
|
|
355
|
+
# @param [String, nil] username
|
|
356
|
+
#
|
|
357
|
+
# @param [String, nil] password
|
|
358
|
+
#
|
|
359
|
+
# @return [Hash, nil]
|
|
360
|
+
#
|
|
361
|
+
# @api private
|
|
279
362
|
def authorized_response(ids, request_id, username, password)
|
|
280
363
|
if ids.include?(request_id)
|
|
281
364
|
{ response: "CancelAuth" }
|
|
@@ -286,14 +369,42 @@ module Ferrum
|
|
|
286
369
|
end
|
|
287
370
|
end
|
|
288
371
|
|
|
372
|
+
#
|
|
373
|
+
# Finds the exchanges in `traffic` with the given request id.
|
|
374
|
+
#
|
|
375
|
+
# @param [String] request_id
|
|
376
|
+
#
|
|
377
|
+
# @return [Array<Exchange>]
|
|
378
|
+
#
|
|
379
|
+
# @api private
|
|
289
380
|
def select(request_id)
|
|
290
381
|
@traffic.select { |e| e.id == request_id }
|
|
291
382
|
end
|
|
292
383
|
|
|
384
|
+
#
|
|
385
|
+
# Builds a new {Exchange} for the given request id and appends it to
|
|
386
|
+
# `traffic`.
|
|
387
|
+
#
|
|
388
|
+
# @param [String] id
|
|
389
|
+
#
|
|
390
|
+
# @return [Exchange]
|
|
391
|
+
#
|
|
392
|
+
# @api private
|
|
293
393
|
def build_exchange(id)
|
|
294
394
|
Network::Exchange.new(@page, id).tap { |e| @traffic << e }
|
|
295
395
|
end
|
|
296
396
|
|
|
397
|
+
# `Network.requestWillBeSent` and `Fetch.requestPaused` are handled on
|
|
398
|
+
# separate threads (see `Client::Subscriber`), so the "find the existing
|
|
399
|
+
# exchange for this id or build a new one" check has to be atomic,
|
|
400
|
+
# otherwise both threads can race past the `select` before either has
|
|
401
|
+
# appended, and end up building two exchanges for the same request.
|
|
402
|
+
#
|
|
403
|
+
# @api private
|
|
404
|
+
def find_or_build_exchange(id)
|
|
405
|
+
@mutex.synchronize { select(id).last || build_exchange(id) }
|
|
406
|
+
end
|
|
407
|
+
|
|
297
408
|
#
|
|
298
409
|
# Activates emulation of network conditions.
|
|
299
410
|
#
|
|
@@ -380,24 +491,25 @@ module Ferrum
|
|
|
380
491
|
|
|
381
492
|
# We can build exchange in two places, here on the event or when request
|
|
382
493
|
# is interrupted. So we have to be careful when to create new one. We
|
|
383
|
-
# create new exchange only if there's no with such id or there's, but
|
|
384
|
-
# it's filled with request which means this one is new but has response
|
|
385
|
-
# for a redirect. So we assign response from the params to previous
|
|
386
|
-
# exchange and build new exchange to assign this request to it.
|
|
387
|
-
exchange =
|
|
388
|
-
|
|
494
|
+
# create a new exchange only if there's no with such an id or there's, but
|
|
495
|
+
# it's filled with request which means this one is new but has a response
|
|
496
|
+
# for a redirect. So we assign a response from the params to the previous
|
|
497
|
+
# exchange and build a new exchange to assign this request to it.
|
|
498
|
+
exchange = @mutex.synchronize do
|
|
499
|
+
ex = select(request.id).last
|
|
500
|
+
ex.nil? || !ex.blank? ? build_exchange(request.id) : ex
|
|
501
|
+
end
|
|
389
502
|
request.headers.merge!(Hash(exchange.request_extra_info&.dig("headers")))
|
|
390
503
|
exchange.request = request
|
|
391
504
|
|
|
392
|
-
if exchange.navigation_request?(@page.main_frame
|
|
505
|
+
if exchange.navigation_request?(@page.main_frame&.id)
|
|
393
506
|
@exchange = exchange
|
|
394
507
|
classify_pending_exchanges(exchange.loader_id)
|
|
395
508
|
end
|
|
396
509
|
end
|
|
397
510
|
|
|
398
511
|
@page.on("Network.requestWillBeSentExtraInfo") do |params|
|
|
399
|
-
exchange =
|
|
400
|
-
exchange ||= build_exchange(params["requestId"])
|
|
512
|
+
exchange = find_or_build_exchange(params["requestId"])
|
|
401
513
|
exchange.request_extra_info = params
|
|
402
514
|
exchange.request&.headers&.merge!(params["headers"])
|
|
403
515
|
end
|