ferrum 0.10.1 → 0.12

Sign up to get free protection for your applications and to get access to all the features.
Files changed (45) hide show
  1. checksums.yaml +4 -4
  2. data/LICENSE +1 -1
  3. data/README.md +261 -28
  4. data/lib/ferrum/browser/binary.rb +46 -0
  5. data/lib/ferrum/browser/client.rb +15 -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 -10
  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 +49 -12
  15. data/lib/ferrum/context.rb +12 -4
  16. data/lib/ferrum/contexts.rb +13 -9
  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 +98 -40
  31. data/lib/ferrum/page/animation.rb +15 -0
  32. data/lib/ferrum/page/frames.rb +46 -15
  33. data/lib/ferrum/page/screenshot.rb +53 -67
  34. data/lib/ferrum/page/stream.rb +38 -0
  35. data/lib/ferrum/page/tracing.rb +71 -0
  36. data/lib/ferrum/page.rb +88 -34
  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 -140
  45. metadata +65 -50
@@ -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
@@ -12,22 +12,20 @@ module Ferrum
12
12
  scale: 1.0
13
13
  }.freeze
14
14
 
15
- PAPEP_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 },
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 }
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, **_)
@@ -114,49 +86,62 @@ module Ferrum
114
86
  raise ArgumentError, "Specify :format or :paper_width, :paper_height"
115
87
  end
116
88
 
117
- dimension = PAPEP_FORMATS.fetch(format)
89
+ dimension = PAPER_FORMATS.fetch(format)
118
90
  options.merge!(paper_width: dimension[:width],
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
@@ -9,6 +9,9 @@ require "ferrum/dialog"
9
9
  require "ferrum/network"
10
10
  require "ferrum/page/frames"
11
11
  require "ferrum/page/screenshot"
12
+ require "ferrum/page/animation"
13
+ require "ferrum/page/tracing"
14
+ require "ferrum/page/stream"
12
15
  require "ferrum/browser/client"
13
16
 
14
17
  module Ferrum
@@ -31,21 +34,26 @@ module Ferrum
31
34
 
32
35
  extend Forwardable
33
36
  delegate %i[at_css at_xpath css xpath
34
- current_url current_title url title body doctype set_content
37
+ current_url current_title url title body doctype content=
35
38
  execution_id evaluate evaluate_on evaluate_async execute evaluate_func
36
39
  add_script_tag add_style_tag] => :main_frame
37
40
 
38
- include Frames, Screenshot
41
+ include Animation
42
+ include Screenshot
43
+ include Frames
44
+ include Stream
39
45
 
40
46
  attr_accessor :referrer
41
47
  attr_reader :target_id, :browser,
42
48
  :headers, :cookies, :network,
43
- :mouse, :keyboard, :event, :document_id
49
+ :mouse, :keyboard, :event,
50
+ :tracing
44
51
 
45
52
  def initialize(target_id, browser)
46
53
  @frames = {}
47
54
  @main_frame = Frame.new(nil, self)
48
- @target_id, @browser = target_id, browser
55
+ @browser = browser
56
+ @target_id = target_id
49
57
  @event = Event.new.tap(&:set)
50
58
 
51
59
  host = @browser.process.host
@@ -53,9 +61,12 @@ module Ferrum
53
61
  ws_url = "ws://#{host}:#{port}/devtools/page/#{@target_id}"
54
62
  @client = Browser::Client.new(browser, ws_url, id_starts_with: 1000)
55
63
 
56
- @mouse, @keyboard = Mouse.new(self), Keyboard.new(self)
57
- @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)
58
68
  @network = Network.new(self)
69
+ @tracing = Tracing.new(self)
59
70
 
60
71
  subscribe
61
72
  prepare_page
@@ -65,6 +76,10 @@ module Ferrum
65
76
  @browser.timeout
66
77
  end
67
78
 
79
+ def context
80
+ @browser.contexts.find_by(target_id: target_id)
81
+ end
82
+
68
83
  def go_to(url = nil)
69
84
  options = { url: combine_url!(url) }
70
85
  options.merge!(referrer: referrer) if referrer
@@ -76,6 +91,7 @@ module Ferrum
76
91
  net::ERR_CONNECTION_TIMED_OUT].include?(response["errorText"])
77
92
  raise StatusError, options[:url]
78
93
  end
94
+
79
95
  response["frameId"]
80
96
  rescue TimeoutError
81
97
  if @browser.pending_connection_errors
@@ -84,6 +100,7 @@ module Ferrum
84
100
  end
85
101
  end
86
102
  alias goto go_to
103
+ alias go go_to
87
104
 
88
105
  def close
89
106
  @headers.clear
@@ -108,10 +125,20 @@ module Ferrum
108
125
  fitWindow: false)
109
126
  end
110
127
 
128
+ def position
129
+ @browser.command("Browser.getWindowBounds", windowId: window_id).fetch("bounds").values_at("left", "top")
130
+ end
131
+
132
+ def position=(options)
133
+ @browser.command("Browser.setWindowBounds",
134
+ windowId: window_id,
135
+ bounds: { left: options[:left], top: options[:top] })
136
+ end
137
+
111
138
  def refresh
112
139
  command("Page.reload", wait: timeout, slowmoable: true)
113
140
  end
114
- alias_method :reload, :refresh
141
+ alias reload refresh
115
142
 
116
143
  def stop
117
144
  command("Page.stopLoading", slowmoable: true)
@@ -131,8 +158,7 @@ module Ferrum
131
158
  @event.set
132
159
  end
133
160
 
134
- def bypass_csp(value = true)
135
- enabled = !!value
161
+ def bypass_csp(enabled: true)
136
162
  command("Page.setBypassCSP", enabled: enabled)
137
163
  enabled
138
164
  end
@@ -146,14 +172,15 @@ module Ferrum
146
172
  end
147
173
 
148
174
  def command(method, wait: 0, slowmoable: false, **params)
149
- iteration = @event.reset if wait > 0
150
- sleep(@browser.slowmo) if slowmoable && @browser.slowmo > 0
175
+ iteration = @event.reset if wait.positive?
176
+ sleep(@browser.slowmo) if slowmoable && @browser.slowmo.positive?
151
177
  result = @client.command(method, params)
152
178
 
153
- if wait > 0
154
- @event.wait(wait) # Wait a bit after command and check if iteration has
155
- # changed which means there was some network event for
156
- # 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.
157
184
  if iteration != @event.iteration
158
185
  set = @event.wait(@browser.timeout)
159
186
  raise TimeoutError unless set
@@ -205,8 +232,19 @@ module Ferrum
205
232
 
206
233
  if @browser.js_errors
207
234
  on("Runtime.exceptionThrown") do |params|
208
- # FIXME https://jvns.ca/blog/2015/11/27/why-rubys-timeout-is-dangerous-and-thread-dot-raise-is-terrifying/
209
- 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
210
248
  end
211
249
  end
212
250
  end
@@ -219,8 +257,22 @@ module Ferrum
219
257
  command("Log.enable")
220
258
  command("Network.enable")
221
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
+
222
267
  if @browser.options[:save_path]
223
- 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)
224
276
  end
225
277
 
226
278
  @browser.extensions.each do |extension|
@@ -233,14 +285,14 @@ module Ferrum
233
285
  resize(width: width, height: height)
234
286
 
235
287
  response = command("Page.getNavigationHistory")
236
- if response.dig("entries", 0, "transitionType") != "typed"
237
- # If we create page by clicking links, submiting forms and so on it
238
- # opens a new window for which `frameStoppedLoading` event never
239
- # occurs and thus search for nodes cannot be completed. Here we check
240
- # the history and if the transitionType for example `link` then
241
- # content is already loaded and we can try to get the document.
242
- get_document_id
243
- 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
244
296
  end
245
297
 
246
298
  def inject_extensions
@@ -259,13 +311,15 @@ module Ferrum
259
311
  def history_navigate(delta:)
260
312
  history = command("Page.getNavigationHistory")
261
313
  index, entries = history.values_at("currentIndex", "entries")
314
+ entry = entries[index + delta]
262
315
 
263
- if entry = entries[index + delta]
264
- # Potential wait because of network event
265
- command("Page.navigateToHistoryEntry", wait: Mouse::CLICK_WAIT,
266
- slowmoable: true,
267
- entryId: entry["id"])
268
- 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"])
269
323
  end
270
324
 
271
325
  def combine_url!(url_or_path)
@@ -279,8 +333,8 @@ module Ferrum
279
333
  (nil_or_relative ? @browser.base_url.join(url.to_s) : url).to_s
280
334
  end
281
335
 
282
- def get_document_id
283
- @document_id = command("DOM.getDocument", depth: 0).dig("root", "nodeId")
336
+ def document_node_id
337
+ command("DOM.getDocument", depth: 0).dig("root", "nodeId")
284
338
  end
285
339
  end
286
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