ferrum 0.11 → 0.12

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.
Files changed (45) hide show
  1. checksums.yaml +4 -4
  2. data/LICENSE +1 -1
  3. data/README.md +178 -29
  4. data/lib/ferrum/browser/binary.rb +46 -0
  5. data/lib/ferrum/browser/client.rb +13 -12
  6. data/lib/ferrum/browser/command.rb +7 -8
  7. data/lib/ferrum/browser/options/base.rb +1 -7
  8. data/lib/ferrum/browser/options/chrome.rb +17 -11
  9. data/lib/ferrum/browser/options/firefox.rb +11 -4
  10. data/lib/ferrum/browser/process.rb +41 -35
  11. data/lib/ferrum/browser/subscriber.rb +1 -3
  12. data/lib/ferrum/browser/web_socket.rb +9 -12
  13. data/lib/ferrum/browser/xvfb.rb +4 -8
  14. data/lib/ferrum/browser.rb +44 -12
  15. data/lib/ferrum/context.rb +6 -2
  16. data/lib/ferrum/contexts.rb +10 -8
  17. data/lib/ferrum/cookies.rb +10 -9
  18. data/lib/ferrum/errors.rb +115 -0
  19. data/lib/ferrum/frame/runtime.rb +20 -17
  20. data/lib/ferrum/frame.rb +32 -24
  21. data/lib/ferrum/headers.rb +2 -2
  22. data/lib/ferrum/keyboard.rb +11 -11
  23. data/lib/ferrum/mouse.rb +8 -7
  24. data/lib/ferrum/network/auth_request.rb +7 -2
  25. data/lib/ferrum/network/exchange.rb +14 -10
  26. data/lib/ferrum/network/intercepted_request.rb +10 -8
  27. data/lib/ferrum/network/request.rb +5 -0
  28. data/lib/ferrum/network/response.rb +4 -4
  29. data/lib/ferrum/network.rb +124 -35
  30. data/lib/ferrum/node.rb +69 -23
  31. data/lib/ferrum/page/animation.rb +0 -1
  32. data/lib/ferrum/page/frames.rb +46 -20
  33. data/lib/ferrum/page/screenshot.rb +51 -65
  34. data/lib/ferrum/page/stream.rb +38 -0
  35. data/lib/ferrum/page/tracing.rb +71 -0
  36. data/lib/ferrum/page.rb +81 -36
  37. data/lib/ferrum/proxy.rb +58 -0
  38. data/lib/ferrum/{rbga.rb → rgba.rb} +4 -2
  39. data/lib/ferrum/target.rb +1 -0
  40. data/lib/ferrum/utils/attempt.rb +20 -0
  41. data/lib/ferrum/utils/elapsed_time.rb +27 -0
  42. data/lib/ferrum/utils/platform.rb +28 -0
  43. data/lib/ferrum/version.rb +1 -1
  44. data/lib/ferrum.rb +4 -146
  45. metadata +60 -51
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "ferrum/rbga"
3
+ require "ferrum/rgba"
4
4
 
5
5
  module Ferrum
6
6
  class Page
@@ -13,21 +13,19 @@ module Ferrum
13
13
  }.freeze
14
14
 
15
15
  PAPER_FORMATS = {
16
- letter: { width: 8.50, height: 11.00 },
17
- legal: { width: 8.50, height: 14.00 },
18
- tabloid: { width: 11.00, height: 17.00 },
19
- ledger: { width: 17.00, height: 11.00 },
20
- A0: { width: 33.10, height: 46.80 },
21
- A1: { width: 23.40, height: 33.10 },
22
- A2: { width: 16.54, height: 23.40 },
23
- A3: { width: 11.70, height: 16.54 },
24
- A4: { width: 8.27, height: 11.70 },
25
- A5: { width: 5.83, height: 8.27 },
26
- A6: { width: 4.13, height: 5.83 },
16
+ letter: { width: 8.50, height: 11.00 },
17
+ legal: { width: 8.50, height: 14.00 },
18
+ tabloid: { width: 11.00, height: 17.00 },
19
+ ledger: { width: 17.00, height: 11.00 },
20
+ A0: { width: 33.10, height: 46.80 },
21
+ A1: { width: 23.40, height: 33.10 },
22
+ A2: { width: 16.54, height: 23.40 },
23
+ A3: { width: 11.70, height: 16.54 },
24
+ A4: { width: 8.27, height: 11.70 },
25
+ A5: { width: 5.83, height: 8.27 },
26
+ A6: { width: 4.13, height: 5.83 }
27
27
  }.freeze
28
28
 
29
- STREAM_CHUNK = 128 * 1024
30
-
31
29
  def screenshot(**opts)
32
30
  path, encoding = common_options(**opts)
33
31
  options = screenshot_options(path, **opts)
@@ -42,17 +40,13 @@ module Ferrum
42
40
  path, encoding = common_options(**opts)
43
41
  options = pdf_options(**opts).merge(transferMode: "ReturnAsStream")
44
42
  handle = command("Page.printToPDF", **options).fetch("stream")
45
-
46
- if path
47
- stream_to_file(handle, path: path)
48
- else
49
- stream_to_memory(handle, encoding: encoding)
50
- end
43
+ stream_to(path: path, encoding: encoding, handle: handle)
51
44
  end
52
45
 
53
46
  def mhtml(path: nil)
54
47
  data = command("Page.captureSnapshot", format: :mhtml).fetch("data")
55
48
  return data if path.nil?
49
+
56
50
  save_file(path, data)
57
51
  end
58
52
 
@@ -73,30 +67,8 @@ module Ferrum
73
67
 
74
68
  def save_file(path, data)
75
69
  return data unless path
76
- File.open(path.to_s, "wb") { |f| f.write(data) }
77
- end
78
70
 
79
- def stream_to_file(handle, path:)
80
- File.open(path, "wb") { |f| stream_to(handle, f) }
81
- true
82
- end
83
-
84
- def stream_to_memory(handle, encoding:)
85
- data = String.new("") # Mutable string has << and compatible to File
86
- stream_to(handle, data)
87
- encoding == :base64 ? Base64.encode64(data) : data
88
- end
89
-
90
- def stream_to(handle, output)
91
- loop do
92
- result = command("IO.read", handle: handle, size: STREAM_CHUNK)
93
-
94
- data_chunk = result["data"]
95
- data_chunk = Base64.decode64(data_chunk) if result["base64Encoded"]
96
- output << data_chunk
97
-
98
- break if result["eof"]
99
- end
71
+ File.binwrite(path.to_s, data)
100
72
  end
101
73
 
102
74
  def common_options(encoding: :base64, path: nil, **_)
@@ -119,44 +91,57 @@ module Ferrum
119
91
  paper_height: dimension[:height])
120
92
  end
121
93
 
122
- options.map { |k, v| [to_camel_case(k), v] }.to_h
94
+ options.transform_keys { |k| to_camel_case(k) }
123
95
  end
124
96
 
125
- def screenshot_options(path = nil, format: nil, scale: 1.0, **opts)
126
- options = {}
97
+ def screenshot_options(path = nil, format: nil, scale: 1.0, **options)
98
+ screenshot_options = {}
99
+
100
+ format, quality = format_options(format, path, options[:quality])
101
+ screenshot_options.merge!(quality: quality) if quality
102
+ screenshot_options.merge!(format: format)
103
+
104
+ clip = area_options(options[:full], options[:selector], scale)
105
+ screenshot_options.merge!(clip: clip) if clip
106
+
107
+ screenshot_options
108
+ end
127
109
 
110
+ def format_options(format, path, quality)
128
111
  format ||= path ? File.extname(path).delete(".") : "png"
129
112
  format = "jpeg" if format == "jpg"
130
113
  raise "Not supported options `:format` #{format}. jpeg | png" if format !~ /jpeg|png/i
131
- options.merge!(format: format)
132
114
 
133
- options.merge!(quality: opts[:quality] ? opts[:quality] : 75) if format == "jpeg"
115
+ quality ||= 75 if format == "jpeg"
134
116
 
135
- if !!opts[:full] && opts[:selector]
136
- warn "Ignoring :selector in #screenshot since full: true was given at #{caller(1..1).first}"
137
- end
117
+ [format, quality]
118
+ end
138
119
 
139
- if !!opts[:full]
140
- width, height = document_size
141
- options.merge!(clip: { x: 0, y: 0, width: width, height: height, scale: scale }) if width > 0 && height > 0
142
- elsif opts[:selector]
143
- options.merge!(clip: get_bounding_rect(opts[:selector]).merge(scale: scale))
144
- end
120
+ def area_options(full, selector, scale)
121
+ message = "Ignoring :selector in #screenshot since full: true was given at #{caller(1..1).first}"
122
+ warn(message) if full && selector
145
123
 
146
- if scale != 1.0
147
- if !options[:clip]
124
+ clip = if full
125
+ width, height = document_size
126
+ { x: 0, y: 0, width: width, height: height, scale: scale } if width.positive? && height.positive?
127
+ elsif selector
128
+ bounding_rect(selector).merge(scale: scale)
129
+ end
130
+
131
+ if scale != 1
132
+ unless clip
148
133
  width, height = viewport_size
149
- options[:clip] = { x: 0, y: 0, width: width, height: height }
134
+ clip = { x: 0, y: 0, width: width, height: height }
150
135
  end
151
136
 
152
- options[:clip].merge!(scale: scale)
137
+ clip.merge!(scale: scale)
153
138
  end
154
139
 
155
- options
140
+ clip
156
141
  end
157
142
 
158
- def get_bounding_rect(selector)
159
- rect = evaluate_async(%Q(
143
+ def bounding_rect(selector)
144
+ rect = evaluate_async(%(
160
145
  const rect = document
161
146
  .querySelector('#{selector}')
162
147
  .getBoundingClientRect();
@@ -169,7 +154,8 @@ module Ferrum
169
154
 
170
155
  def to_camel_case(option)
171
156
  return :preferCSSPageSize if option == :prefer_css_page_size
172
- option.to_s.gsub(/(?:_|(\/))([a-z\d]*)/) { "#{$1}#{$2.capitalize}" }.to_sym
157
+
158
+ option.to_s.gsub(%r{(?:_|(/))([a-z\d]*)}) { "#{Regexp.last_match(1)}#{Regexp.last_match(2).capitalize}" }.to_sym
173
159
  end
174
160
 
175
161
  def capture_screenshot(options, full, background_color)
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ferrum
4
+ class Page
5
+ module Stream
6
+ STREAM_CHUNK = 128 * 1024
7
+
8
+ def stream_to(path:, encoding:, handle:)
9
+ if path.nil?
10
+ stream_to_memory(encoding: encoding, handle: handle)
11
+ else
12
+ stream_to_file(path: path, handle: handle)
13
+ end
14
+ end
15
+
16
+ def stream_to_file(path:, handle:)
17
+ File.open(path, "wb") { |f| stream(output: f, handle: handle) }
18
+ true
19
+ end
20
+
21
+ def stream_to_memory(encoding:, handle:)
22
+ data = String.new # Mutable string has << and compatible to File
23
+ stream(output: data, handle: handle)
24
+ encoding == :base64 ? Base64.encode64(data) : data
25
+ end
26
+
27
+ def stream(output:, handle:)
28
+ loop do
29
+ result = command("IO.read", handle: handle, size: STREAM_CHUNK)
30
+ chunk = result.fetch("data")
31
+ chunk = Base64.decode64(chunk) if result["base64Encoded"]
32
+ output << chunk
33
+ break if result["eof"]
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ferrum
4
+ class Page
5
+ class Tracing
6
+ EXCLUDED_CATEGORIES = %w[*].freeze
7
+ SCREENSHOT_CATEGORIES = %w[disabled-by-default-devtools.screenshot].freeze
8
+ INCLUDED_CATEGORIES = %w[devtools.timeline v8.execute disabled-by-default-devtools.timeline
9
+ disabled-by-default-devtools.timeline.frame toplevel blink.console
10
+ blink.user_timing latencyInfo disabled-by-default-devtools.timeline.stack
11
+ disabled-by-default-v8.cpu_profiler disabled-by-default-v8.cpu_profiler.hires].freeze
12
+ DEFAULT_TRACE_CONFIG = {
13
+ includedCategories: INCLUDED_CATEGORIES,
14
+ excludedCategories: EXCLUDED_CATEGORIES
15
+ }.freeze
16
+
17
+ def initialize(page)
18
+ @page = page
19
+ @subscribed_tracing_complete = false
20
+ end
21
+
22
+ def record(path: nil, encoding: :binary, timeout: nil, trace_config: nil, screenshots: false)
23
+ @path = path
24
+ @encoding = encoding
25
+ @pending = Concurrent::IVar.new
26
+ trace_config ||= DEFAULT_TRACE_CONFIG.dup
27
+
28
+ if screenshots
29
+ included = trace_config.fetch(:includedCategories, [])
30
+ trace_config.merge!(includedCategories: included | SCREENSHOT_CATEGORIES)
31
+ end
32
+
33
+ subscribe_tracing_complete
34
+
35
+ start(trace_config)
36
+ yield
37
+ stop
38
+
39
+ @pending.value!(timeout || @page.timeout)
40
+ end
41
+
42
+ private
43
+
44
+ def start(config)
45
+ @page.command("Tracing.start", transferMode: "ReturnAsStream", traceConfig: config)
46
+ end
47
+
48
+ def stop
49
+ @page.command("Tracing.end")
50
+ end
51
+
52
+ def subscribe_tracing_complete
53
+ return if @subscribed_tracing_complete
54
+
55
+ @page.on("Tracing.tracingComplete") do |event, index|
56
+ next if index.to_i != 0
57
+
58
+ @pending.set(stream_handle(event["stream"]))
59
+ rescue StandardError => e
60
+ @pending.fail(e)
61
+ end
62
+
63
+ @subscribed_tracing_complete = true
64
+ end
65
+
66
+ def stream_handle(handle)
67
+ @page.stream_to(path: @path, encoding: @encoding, handle: handle)
68
+ end
69
+ end
70
+ end
71
+ end
data/lib/ferrum/page.rb CHANGED
@@ -10,6 +10,8 @@ require "ferrum/network"
10
10
  require "ferrum/page/frames"
11
11
  require "ferrum/page/screenshot"
12
12
  require "ferrum/page/animation"
13
+ require "ferrum/page/tracing"
14
+ require "ferrum/page/stream"
13
15
  require "ferrum/browser/client"
14
16
 
15
17
  module Ferrum
@@ -32,21 +34,26 @@ module Ferrum
32
34
 
33
35
  extend Forwardable
34
36
  delegate %i[at_css at_xpath css xpath
35
- current_url current_title url title body doctype set_content
37
+ current_url current_title url title body doctype content=
36
38
  execution_id evaluate evaluate_on evaluate_async execute evaluate_func
37
39
  add_script_tag add_style_tag] => :main_frame
38
40
 
39
- include Frames, Screenshot, Animation
41
+ include Animation
42
+ include Screenshot
43
+ include Frames
44
+ include Stream
40
45
 
41
46
  attr_accessor :referrer
42
47
  attr_reader :target_id, :browser,
43
48
  :headers, :cookies, :network,
44
- :mouse, :keyboard, :event, :document_id
49
+ :mouse, :keyboard, :event,
50
+ :tracing
45
51
 
46
52
  def initialize(target_id, browser)
47
53
  @frames = {}
48
54
  @main_frame = Frame.new(nil, self)
49
- @target_id, @browser = target_id, browser
55
+ @browser = browser
56
+ @target_id = target_id
50
57
  @event = Event.new.tap(&:set)
51
58
 
52
59
  host = @browser.process.host
@@ -54,9 +61,12 @@ module Ferrum
54
61
  ws_url = "ws://#{host}:#{port}/devtools/page/#{@target_id}"
55
62
  @client = Browser::Client.new(browser, ws_url, id_starts_with: 1000)
56
63
 
57
- @mouse, @keyboard = Mouse.new(self), Keyboard.new(self)
58
- @headers, @cookies = Headers.new(self), Cookies.new(self)
64
+ @mouse = Mouse.new(self)
65
+ @keyboard = Keyboard.new(self)
66
+ @headers = Headers.new(self)
67
+ @cookies = Cookies.new(self)
59
68
  @network = Network.new(self)
69
+ @tracing = Tracing.new(self)
60
70
 
61
71
  subscribe
62
72
  prepare_page
@@ -66,6 +76,10 @@ module Ferrum
66
76
  @browser.timeout
67
77
  end
68
78
 
79
+ def context
80
+ @browser.contexts.find_by(target_id: target_id)
81
+ end
82
+
69
83
  def go_to(url = nil)
70
84
  options = { url: combine_url!(url) }
71
85
  options.merge!(referrer: referrer) if referrer
@@ -77,6 +91,7 @@ module Ferrum
77
91
  net::ERR_CONNECTION_TIMED_OUT].include?(response["errorText"])
78
92
  raise StatusError, options[:url]
79
93
  end
94
+
80
95
  response["frameId"]
81
96
  rescue TimeoutError
82
97
  if @browser.pending_connection_errors
@@ -85,6 +100,7 @@ module Ferrum
85
100
  end
86
101
  end
87
102
  alias goto go_to
103
+ alias go go_to
88
104
 
89
105
  def close
90
106
  @headers.clear
@@ -113,14 +129,16 @@ module Ferrum
113
129
  @browser.command("Browser.getWindowBounds", windowId: window_id).fetch("bounds").values_at("left", "top")
114
130
  end
115
131
 
116
- def position=(left:, top:)
117
- @browser.command("Browser.setWindowBounds", windowId: window_id, bounds: { left: left, top: top })
132
+ def position=(options)
133
+ @browser.command("Browser.setWindowBounds",
134
+ windowId: window_id,
135
+ bounds: { left: options[:left], top: options[:top] })
118
136
  end
119
137
 
120
138
  def refresh
121
139
  command("Page.reload", wait: timeout, slowmoable: true)
122
140
  end
123
- alias_method :reload, :refresh
141
+ alias reload refresh
124
142
 
125
143
  def stop
126
144
  command("Page.stopLoading", slowmoable: true)
@@ -140,8 +158,7 @@ module Ferrum
140
158
  @event.set
141
159
  end
142
160
 
143
- def bypass_csp(value = true)
144
- enabled = !!value
161
+ def bypass_csp(enabled: true)
145
162
  command("Page.setBypassCSP", enabled: enabled)
146
163
  enabled
147
164
  end
@@ -155,14 +172,15 @@ module Ferrum
155
172
  end
156
173
 
157
174
  def command(method, wait: 0, slowmoable: false, **params)
158
- iteration = @event.reset if wait > 0
159
- sleep(@browser.slowmo) if slowmoable && @browser.slowmo > 0
175
+ iteration = @event.reset if wait.positive?
176
+ sleep(@browser.slowmo) if slowmoable && @browser.slowmo.positive?
160
177
  result = @client.command(method, params)
161
178
 
162
- if wait > 0
163
- @event.wait(wait) # Wait a bit after command and check if iteration has
164
- # changed which means there was some network event for
165
- # the main frame and it started to load new content.
179
+ if wait.positive?
180
+ @event.wait(wait)
181
+ # Wait a bit after command and check if iteration has
182
+ # changed which means there was some network event for
183
+ # the main frame and it started to load new content.
166
184
  if iteration != @event.iteration
167
185
  set = @event.wait(@browser.timeout)
168
186
  raise TimeoutError unless set
@@ -214,8 +232,19 @@ module Ferrum
214
232
 
215
233
  if @browser.js_errors
216
234
  on("Runtime.exceptionThrown") do |params|
217
- # FIXME https://jvns.ca/blog/2015/11/27/why-rubys-timeout-is-dangerous-and-thread-dot-raise-is-terrifying/
218
- Thread.main.raise JavaScriptError.new(params.dig("exceptionDetails", "exception"))
235
+ # FIXME: https://jvns.ca/blog/2015/11/27/why-rubys-timeout-is-dangerous-and-thread-dot-raise-is-terrifying/
236
+ Thread.main.raise JavaScriptError.new(
237
+ params.dig("exceptionDetails", "exception"),
238
+ params.dig("exceptionDetails", "stackTrace")
239
+ )
240
+ end
241
+ end
242
+
243
+ on(:dialog) do |dialog, _index, total|
244
+ if total == 1
245
+ warn "Dialog was shown but you didn't provide `on(:dialog)` callback, accepting it by default. " \
246
+ "Please take a look at https://github.com/rubycdp/ferrum#dialog"
247
+ dialog.accept
219
248
  end
220
249
  end
221
250
  end
@@ -228,8 +257,22 @@ module Ferrum
228
257
  command("Log.enable")
229
258
  command("Network.enable")
230
259
 
260
+ if @browser.proxy_options && @browser.proxy_options[:user] && @browser.proxy_options[:password]
261
+ auth_options = @browser.proxy_options.slice(:user, :password)
262
+ network.authorize(type: :proxy, **auth_options) do |request, _index, _total|
263
+ request.continue
264
+ end
265
+ end
266
+
231
267
  if @browser.options[:save_path]
232
- command("Page.setDownloadBehavior", behavior: "allow", downloadPath: @browser.options[:save_path])
268
+ unless Pathname.new(@browser.options[:save_path]).absolute?
269
+ raise Error, "supply absolute path for `:save_path` option"
270
+ end
271
+
272
+ @browser.command("Browser.setDownloadBehavior",
273
+ browserContextId: context.id,
274
+ downloadPath: browser.options[:save_path],
275
+ behavior: "allow", eventsEnabled: true)
233
276
  end
234
277
 
235
278
  @browser.extensions.each do |extension|
@@ -242,14 +285,14 @@ module Ferrum
242
285
  resize(width: width, height: height)
243
286
 
244
287
  response = command("Page.getNavigationHistory")
245
- if response.dig("entries", 0, "transitionType") != "typed"
246
- # If we create page by clicking links, submiting forms and so on it
247
- # opens a new window for which `frameStoppedLoading` event never
248
- # occurs and thus search for nodes cannot be completed. Here we check
249
- # the history and if the transitionType for example `link` then
250
- # content is already loaded and we can try to get the document.
251
- get_document_id
252
- end
288
+ return unless response.dig("entries", 0, "transitionType") != "typed"
289
+
290
+ # If we create page by clicking links, submitting forms and so on it
291
+ # opens a new window for which `frameStoppedLoading` event never
292
+ # occurs and thus search for nodes cannot be completed. Here we check
293
+ # the history and if the transitionType for example `link` then
294
+ # content is already loaded and we can try to get the document.
295
+ document_node_id
253
296
  end
254
297
 
255
298
  def inject_extensions
@@ -268,13 +311,15 @@ module Ferrum
268
311
  def history_navigate(delta:)
269
312
  history = command("Page.getNavigationHistory")
270
313
  index, entries = history.values_at("currentIndex", "entries")
314
+ entry = entries[index + delta]
271
315
 
272
- if entry = entries[index + delta]
273
- # Potential wait because of network event
274
- command("Page.navigateToHistoryEntry", wait: Mouse::CLICK_WAIT,
275
- slowmoable: true,
276
- entryId: entry["id"])
277
- end
316
+ return unless entry
317
+
318
+ # Potential wait because of network event
319
+ command("Page.navigateToHistoryEntry",
320
+ wait: Mouse::CLICK_WAIT,
321
+ slowmoable: true,
322
+ entryId: entry["id"])
278
323
  end
279
324
 
280
325
  def combine_url!(url_or_path)
@@ -288,8 +333,8 @@ module Ferrum
288
333
  (nil_or_relative ? @browser.base_url.join(url.to_s) : url).to_s
289
334
  end
290
335
 
291
- def get_document_id
292
- @document_id = command("DOM.getDocument", depth: 0).dig("root", "nodeId")
336
+ def document_node_id
337
+ command("DOM.getDocument", depth: 0).dig("root", "nodeId")
293
338
  end
294
339
  end
295
340
  end
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "tempfile"
4
+ require "webrick"
5
+ require "webrick/httpproxy"
6
+
7
+ module Ferrum
8
+ class Proxy
9
+ def self.start(**args)
10
+ new(**args).tap(&:start)
11
+ end
12
+
13
+ attr_reader :host, :port, :user, :password
14
+
15
+ def initialize(host: "127.0.0.1", port: 0, user: nil, password: nil)
16
+ @file = nil
17
+ @host = host
18
+ @port = port
19
+ @user = user
20
+ @password = password
21
+ at_exit { stop }
22
+ end
23
+
24
+ def start
25
+ options = {
26
+ ProxyURI: nil, ServerType: Thread,
27
+ Logger: Logger.new(IO::NULL), AccessLog: [],
28
+ BindAddress: host, Port: port
29
+ }
30
+
31
+ if user && password
32
+ @file = Tempfile.new("htpasswd")
33
+ htpasswd = WEBrick::HTTPAuth::Htpasswd.new(@file.path)
34
+ htpasswd.set_passwd "Proxy Realm", user, password
35
+ htpasswd.flush
36
+ authenticator = WEBrick::HTTPAuth::ProxyBasicAuth.new(Realm: "Proxy Realm",
37
+ UserDB: htpasswd,
38
+ Logger: Logger.new(IO::NULL))
39
+ options.merge!(ProxyAuthProc: authenticator.method(:authenticate).to_proc)
40
+ end
41
+
42
+ @server = WEBrick::HTTPProxyServer.new(**options)
43
+ @server.start
44
+ @port = @server.config[:Port]
45
+ end
46
+
47
+ def rotate(host:, port:, user: nil, password: nil)
48
+ credentials = "#{user}:#{password}@" if user && password
49
+ proxy_uri = "schema://#{credentials}#{host}:#{port}"
50
+ @server.config[:ProxyURI] = URI.parse(proxy_uri)
51
+ end
52
+
53
+ def stop
54
+ @file&.unlink
55
+ @server.shutdown
56
+ end
57
+ end
58
+ end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Ferrum
2
4
  class RGBA
3
5
  def initialize(red, green, blue, alpha)
@@ -23,13 +25,13 @@ module Ferrum
23
25
  end
24
26
 
25
27
  def validate_color(value)
26
- return if value && value.is_a?(Integer) && Range.new(0, 255).include?(value)
28
+ return if value.is_a?(Integer) && Range.new(0, 255).include?(value)
27
29
 
28
30
  raise ArgumentError, "Wrong value of #{value} should be Integer from 0 to 255"
29
31
  end
30
32
 
31
33
  def validate_alpha
32
- return if alpha && alpha.is_a?(Float) && Range.new(0.0, 1.0).include?(alpha)
34
+ return if alpha.is_a?(Float) && Range.new(0.0, 1.0).include?(alpha)
33
35
 
34
36
  raise ArgumentError,
35
37
  "Wrong alpha value #{alpha} should be Float between 0.0 (fully transparent) and 1.0 (fully opaque)"
data/lib/ferrum/target.rb CHANGED
@@ -9,6 +9,7 @@ module Ferrum
9
9
  attr_writer :page
10
10
 
11
11
  def initialize(browser, params = nil)
12
+ @page = nil
12
13
  @browser = browser
13
14
  @params = params
14
15
  end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ferrum
4
+ module Utils
5
+ module Attempt
6
+ module_function
7
+
8
+ def with_retry(errors:, max:, wait:)
9
+ attempts ||= 1
10
+ yield
11
+ rescue *Array(errors)
12
+ raise if attempts >= max
13
+
14
+ attempts += 1
15
+ sleep(wait)
16
+ retry
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "concurrent-ruby"
4
+
5
+ module Ferrum
6
+ module Utils
7
+ module ElapsedTime
8
+ module_function
9
+
10
+ def start
11
+ @start ||= monotonic_time
12
+ end
13
+
14
+ def elapsed_time(start = nil)
15
+ monotonic_time - (start || @start)
16
+ end
17
+
18
+ def monotonic_time
19
+ Concurrent.monotonic_time
20
+ end
21
+
22
+ def timeout?(start, timeout)
23
+ elapsed_time(start) > timeout
24
+ end
25
+ end
26
+ end
27
+ end