puppeteer-ruby 0.0.15 → 0.0.16
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/lib/puppeteer.rb +2 -0
- data/lib/puppeteer/frame.rb +0 -22
- data/lib/puppeteer/network_manager.rb +163 -5
- data/lib/puppeteer/page.rb +100 -58
- data/lib/puppeteer/request.rb +336 -0
- data/lib/puppeteer/response.rb +113 -0
- data/lib/puppeteer/version.rb +1 -1
- metadata +4 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 38a6ae4190bd04cbacb863d04f9862ebc8a37c6811fbfd690426ba051d5ea5d4
|
4
|
+
data.tar.gz: 60b61e143e63ed5e42b0bb6f3d9e5c27262fd2f51064c185e491056a5081707d
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: fc3ab652476ee319fab2d691173a0175d4b03d72346329f2fb855a142c3090693f1fb5cfa52abb4badbfbadc7d4eec5028a295f63fb7ddd3e362321564a96b7c
|
7
|
+
data.tar.gz: 6de2484b1b939c3cab1bfa48bb2225c54686f13e7901f3df086d8e5c7b18aa9fc62afb5fd9a785a3fbbf12d2aef50cde1d085f3564ce9afab996f80a39c64936
|
data/lib/puppeteer.rb
CHANGED
@@ -36,6 +36,8 @@ require 'puppeteer/mouse'
|
|
36
36
|
require 'puppeteer/network_manager'
|
37
37
|
require 'puppeteer/page'
|
38
38
|
require 'puppeteer/remote_object'
|
39
|
+
require 'puppeteer/request'
|
40
|
+
require 'puppeteer/response'
|
39
41
|
require 'puppeteer/target'
|
40
42
|
require 'puppeteer/timeout_settings'
|
41
43
|
require 'puppeteer/touch_screen'
|
data/lib/puppeteer/frame.rb
CHANGED
@@ -208,28 +208,6 @@ class Puppeteer::Frame
|
|
208
208
|
|
209
209
|
define_async_method :async_type_text
|
210
210
|
|
211
|
-
# /**
|
212
|
-
# * @param {(string|number|Function)} selectorOrFunctionOrTimeout
|
213
|
-
# * @param {!Object=} options
|
214
|
-
# * @param {!Array<*>} args
|
215
|
-
# * @return {!Promise<?Puppeteer.JSHandle>}
|
216
|
-
# */
|
217
|
-
# waitFor(selectorOrFunctionOrTimeout, options = {}, ...args) {
|
218
|
-
# const xPathPattern = '//';
|
219
|
-
|
220
|
-
# if (helper.isString(selectorOrFunctionOrTimeout)) {
|
221
|
-
# const string = /** @type {string} */ (selectorOrFunctionOrTimeout);
|
222
|
-
# if (string.startsWith(xPathPattern))
|
223
|
-
# return this.waitForXPath(string, options);
|
224
|
-
# return this.waitForSelector(string, options);
|
225
|
-
# }
|
226
|
-
# if (helper.isNumber(selectorOrFunctionOrTimeout))
|
227
|
-
# return new Promise(fulfill => setTimeout(fulfill, /** @type {number} */ (selectorOrFunctionOrTimeout)));
|
228
|
-
# if (typeof selectorOrFunctionOrTimeout === 'function')
|
229
|
-
# return this.waitForFunction(selectorOrFunctionOrTimeout, options, ...args);
|
230
|
-
# return Promise.reject(new Error('Unsupported target type: ' + (typeof selectorOrFunctionOrTimeout)));
|
231
|
-
# }
|
232
|
-
|
233
211
|
# @param selector [String]
|
234
212
|
# @param visible [Boolean] Wait for element visible (not 'display: none' nor 'visibility: hidden') on true. default to false.
|
235
213
|
# @param hidden [Boolean] Wait for element invisible ('display: none' nor 'visibility: hidden') on true. default to false.
|
@@ -1,5 +1,7 @@
|
|
1
1
|
class Puppeteer::NetworkManager
|
2
|
+
include Puppeteer::DebugPrint
|
2
3
|
include Puppeteer::EventCallbackable
|
4
|
+
include Puppeteer::IfPresent
|
3
5
|
|
4
6
|
class Credentials
|
5
7
|
# @param username [String|NilClass]
|
@@ -23,19 +25,39 @@ class Puppeteer::NetworkManager
|
|
23
25
|
@request_id_to_request = {}
|
24
26
|
|
25
27
|
# @type {!Map<string, !Protocol.Network.requestWillBeSentPayload>}
|
26
|
-
@request_id_to_request_with_be_sent_event
|
28
|
+
@request_id_to_request_with_be_sent_event = {}
|
27
29
|
|
28
30
|
@extra_http_headers = {}
|
29
31
|
|
30
32
|
@offline = false
|
31
33
|
|
32
|
-
|
33
|
-
# this._attemptedAuthentications = new Set();
|
34
|
+
@attempted_authentications = Set.new
|
34
35
|
@user_request_interception_enabled = false
|
35
36
|
@protocol_request_interception_enabled = false
|
36
37
|
@user_cache_disabled = false
|
37
|
-
|
38
|
-
|
38
|
+
@request_id_to_interception_id = {}
|
39
|
+
|
40
|
+
@client.on_event('Fetch.requestPaused') do |event|
|
41
|
+
handle_request_paused(event)
|
42
|
+
end
|
43
|
+
@client.on_event('Fetch.authRequired') do |event|
|
44
|
+
handle_auth_required(event)
|
45
|
+
end
|
46
|
+
@client.on_event('Network.requestWillBeSent') do |event|
|
47
|
+
handle_request_will_be_sent(event)
|
48
|
+
end
|
49
|
+
@client.on_event('Network.requestServedFromCache') do |event|
|
50
|
+
handle_request_served_from_cache(event)
|
51
|
+
end
|
52
|
+
@client.on_event('Network.responseReceived') do |event|
|
53
|
+
handle_response_received(event)
|
54
|
+
end
|
55
|
+
@client.on_event('Network.loadingFinished') do |event|
|
56
|
+
handle_loading_finished(event)
|
57
|
+
end
|
58
|
+
@client.on_event('Network.loadingFailed') do |event|
|
59
|
+
handle_loading_failed(event)
|
60
|
+
end
|
39
61
|
end
|
40
62
|
|
41
63
|
def init
|
@@ -119,4 +141,140 @@ class Puppeteer::NetworkManager
|
|
119
141
|
cache_disabled = @user_cache_disabled || @protocol_request_interception_enabled
|
120
142
|
@client.send_message('Network.setCacheDisabled', cacheDisabled: cache_disabled)
|
121
143
|
end
|
144
|
+
|
145
|
+
private def handle_request_will_be_sent(event)
|
146
|
+
# Request interception doesn't happen for data URLs with Network Service.
|
147
|
+
if @protocol_request_interception_enabled && !event['request']['url'].start_with?('data:')
|
148
|
+
request_id = event['requestId']
|
149
|
+
interception_id = @request_id_to_interception_id.delete(request_id)
|
150
|
+
if interception_id
|
151
|
+
handle_request(event, interception_id)
|
152
|
+
else
|
153
|
+
@request_id_to_request_with_be_sent_event[request_id] = event
|
154
|
+
end
|
155
|
+
return
|
156
|
+
end
|
157
|
+
handle_request(event, nil)
|
158
|
+
end
|
159
|
+
|
160
|
+
private def handle_auth_required(event)
|
161
|
+
response = 'Default'
|
162
|
+
if @attempted_authentications.include?(event['requestId'])
|
163
|
+
response = 'CancelAuth'
|
164
|
+
elsif @credentials
|
165
|
+
response = 'ProvideCredentials'
|
166
|
+
@attempted_authentications << event['requestId']
|
167
|
+
end
|
168
|
+
|
169
|
+
username = @credentials&.username
|
170
|
+
password = @credentials&.password
|
171
|
+
|
172
|
+
begin
|
173
|
+
@client.send_message('Fetch.continueWithAuth',
|
174
|
+
requestId: event['requestId'],
|
175
|
+
authChallengeResponse: {
|
176
|
+
response: response,
|
177
|
+
username: username,
|
178
|
+
password: password,
|
179
|
+
},
|
180
|
+
)
|
181
|
+
rescue => err
|
182
|
+
debug_puts(err)
|
183
|
+
end
|
184
|
+
end
|
185
|
+
|
186
|
+
private def handle_request_paused(event)
|
187
|
+
if !@user_request_interception_enabled && @protocol_request_interception_enabled
|
188
|
+
begin
|
189
|
+
@client.send_message('Fetch.continueRequest', requestId: event['requestId'])
|
190
|
+
rescue => err
|
191
|
+
debug_puts(err)
|
192
|
+
end
|
193
|
+
end
|
194
|
+
|
195
|
+
request_id = event['networkId']
|
196
|
+
interception_id = event['requestId']
|
197
|
+
if request_id && (request_will_be_sent_event = @request_id_to_request_with_be_sent_event.delete(request_id))
|
198
|
+
handle_request(request_will_be_sent_event, interception_id)
|
199
|
+
else
|
200
|
+
@request_id_to_interception_id[request_id] = interception_id
|
201
|
+
end
|
202
|
+
end
|
203
|
+
|
204
|
+
private def handle_request(event, interception_id)
|
205
|
+
redirect_chain = []
|
206
|
+
if event['redirectResponse']
|
207
|
+
if_present(@request_id_to_request[event['requestId']]) do |request|
|
208
|
+
handle_request_redirect(request, event['redirectResponse'])
|
209
|
+
redirect_chain = request.internal.redirect_chain
|
210
|
+
end
|
211
|
+
end
|
212
|
+
frame = if_present(event['frameId']) { |frame_id| @frame_manager.frame(frame_id) }
|
213
|
+
request = Puppeteer::Request.new(@client, frame, interception_id, @user_request_interception_enabled, event, redirect_chain)
|
214
|
+
@request_id_to_request[event['requestId']] = request
|
215
|
+
emit_event 'Events.NetworkManager.Request', request
|
216
|
+
end
|
217
|
+
|
218
|
+
private def handle_request_served_from_cache(event)
|
219
|
+
if_present(@request_id_to_request[event['requestId']]) do |request|
|
220
|
+
request.internal.from_memory_cache = true
|
221
|
+
end
|
222
|
+
end
|
223
|
+
|
224
|
+
# @param request [Puppeteer::Request]
|
225
|
+
# @param response_payload [Hash]
|
226
|
+
private def handle_request_redirect(request, response_payload)
|
227
|
+
response = Puppeteer::Response.new(@client, request, response_payload)
|
228
|
+
request.internal.response = response
|
229
|
+
request.internal.redirect_chain << request
|
230
|
+
response.internal.body_loaded_promise.reject(Puppeteer::Response::Redirected.new)
|
231
|
+
@request_id_to_request.delete(request.internal.request_id)
|
232
|
+
@attempted_authentications.delete(request.internal.interception_id)
|
233
|
+
emit_event 'Events.NetworkManager.Response', response
|
234
|
+
emit_event 'Events.NetworkManager.RequestFinished', request
|
235
|
+
end
|
236
|
+
|
237
|
+
# @param event [Hash]
|
238
|
+
private def handle_response_received(event)
|
239
|
+
request = @request_id_to_request[event['requestId']]
|
240
|
+
# FileUpload sends a response without a matching request.
|
241
|
+
return unless request
|
242
|
+
|
243
|
+
response = Puppeteer::Response.new(@client, request, event['response'])
|
244
|
+
request.internal.response = response
|
245
|
+
emit_event 'Events.NetworkManager.Response', response
|
246
|
+
end
|
247
|
+
|
248
|
+
private def handle_loading_finished(event)
|
249
|
+
request = @request_id_to_request[event['requestId']]
|
250
|
+
# For certain requestIds we never receive requestWillBeSent event.
|
251
|
+
# @see https://crbug.com/750469
|
252
|
+
return unless request
|
253
|
+
|
254
|
+
|
255
|
+
# Under certain conditions we never get the Network.responseReceived
|
256
|
+
# event from protocol. @see https://crbug.com/883475
|
257
|
+
if_present(request.response) do |response|
|
258
|
+
response.internal.body_loaded_promise.fulfill(nil)
|
259
|
+
end
|
260
|
+
|
261
|
+
@request_id_to_request.delete(request.internal.request_id)
|
262
|
+
@attempted_authentications.delete(request.internal.interception_id)
|
263
|
+
emit_event 'Events.NetworkManager.RequestFinished', request
|
264
|
+
end
|
265
|
+
|
266
|
+
private def handle_loading_failed(event)
|
267
|
+
request = @request_id_to_request[event['requestId']]
|
268
|
+
# For certain requestIds we never receive requestWillBeSent event.
|
269
|
+
# @see https://crbug.com/750469
|
270
|
+
return unless request
|
271
|
+
|
272
|
+
request.internal.failure_text = event['errorText']
|
273
|
+
if_present(request.response) do |response|
|
274
|
+
response.internal.body_loaded_promise.fulfill(nil)
|
275
|
+
end
|
276
|
+
@request_id_to_request.delete(request.internal.request_id)
|
277
|
+
@attempted_authentications.delete(request.internal.interception_id)
|
278
|
+
emit_event 'Events.NetworkManager.RequestFailed', request
|
279
|
+
end
|
122
280
|
end
|
data/lib/puppeteer/page.rb
CHANGED
@@ -689,41 +689,94 @@ class Puppeteer::Page
|
|
689
689
|
|
690
690
|
define_async_method :async_wait_for_navigation
|
691
691
|
|
692
|
-
|
693
|
-
|
694
|
-
# * @param {!{timeout?: number}=} options
|
695
|
-
# * @return {!Promise<!Puppeteer.Request>}
|
696
|
-
# */
|
697
|
-
# async waitForRequest(urlOrPredicate, options = {}) {
|
698
|
-
# const {
|
699
|
-
# timeout = this._timeoutSettings.timeout(),
|
700
|
-
# } = options;
|
701
|
-
# return helper.waitForEvent(this._frameManager.networkManager(), Events.NetworkManager.Request, request => {
|
702
|
-
# if (helper.isString(urlOrPredicate))
|
703
|
-
# return (urlOrPredicate === request.url());
|
704
|
-
# if (typeof urlOrPredicate === 'function')
|
705
|
-
# return !!(urlOrPredicate(request));
|
706
|
-
# return false;
|
707
|
-
# }, timeout, this._sessionClosePromise());
|
708
|
-
# }
|
692
|
+
private def wait_for_network_manager_event(event_name, predicate:, timeout:)
|
693
|
+
option_timeout = timeout || @timeout_settings.timeout
|
709
694
|
|
710
|
-
|
711
|
-
|
712
|
-
|
713
|
-
|
714
|
-
|
715
|
-
|
716
|
-
|
717
|
-
|
718
|
-
|
719
|
-
|
720
|
-
|
721
|
-
|
722
|
-
|
723
|
-
|
724
|
-
|
725
|
-
|
726
|
-
|
695
|
+
@wait_for_network_manager_event_listener_ids ||= {}
|
696
|
+
if_present(@wait_for_network_manager_event_listener_ids[event_name]) do |listener_id|
|
697
|
+
@frame_manager.network_manager.remove_event_listener(listener_id)
|
698
|
+
end
|
699
|
+
|
700
|
+
promise = resolvable_future
|
701
|
+
|
702
|
+
@wait_for_network_manager_event_listener_ids[event_name] =
|
703
|
+
@frame_manager.network_manager.add_event_listener(event_name) do |event_target|
|
704
|
+
if predicate.call(event_target)
|
705
|
+
promise.fulfill(nil)
|
706
|
+
end
|
707
|
+
end
|
708
|
+
|
709
|
+
begin
|
710
|
+
Timeout.timeout(option_timeout / 1000.0) do
|
711
|
+
await_any(promise, session_close_promise)
|
712
|
+
end
|
713
|
+
rescue Timeout::Error
|
714
|
+
raise Puppeteer::TimeoutError.new("waiting for #{event_name} failed: timeout #{timeout}ms exceeded")
|
715
|
+
ensure
|
716
|
+
@frame_manager.network_manager.remove_event_listener(@wait_for_network_manager_event_listener_ids[event_name])
|
717
|
+
end
|
718
|
+
end
|
719
|
+
|
720
|
+
private def session_close_promise
|
721
|
+
@disconnect_promise ||= resolvable_future do |future|
|
722
|
+
@client.observe_first('Events.CDPSession.Disconnected') do
|
723
|
+
future.reject(Puppeteer::CDPSession::Error.new('Target Closed'))
|
724
|
+
end
|
725
|
+
end
|
726
|
+
end
|
727
|
+
|
728
|
+
# - Waits until request URL matches
|
729
|
+
# `wait_for_request(url: 'https://example.com/awesome')`
|
730
|
+
# - Waits until request matches the given predicate
|
731
|
+
# `wait_for_request(predicate: -> (req){ req.url.start_with?('https://example.com/search') })`
|
732
|
+
#
|
733
|
+
# @param url [String]
|
734
|
+
# @param predicate [Proc(Puppeteer::Request -> Boolean)]
|
735
|
+
private def wait_for_request(url: nil, predicate: nil, timeout: nil)
|
736
|
+
if !url && !predicate
|
737
|
+
raise ArgumentError.new('url or predicate must be specified')
|
738
|
+
end
|
739
|
+
if predicate && !predicate.is_a?(Proc)
|
740
|
+
raise ArgumentError.new('predicate must be a proc.')
|
741
|
+
end
|
742
|
+
request_predicate =
|
743
|
+
if url
|
744
|
+
-> (request) { request.url == url }
|
745
|
+
else
|
746
|
+
-> (request) { predicate.call(request) }
|
747
|
+
end
|
748
|
+
|
749
|
+
wait_for_network_manager_event('Events.NetworkManager.Request',
|
750
|
+
predicate: request_predicate,
|
751
|
+
timeout: timeout,
|
752
|
+
)
|
753
|
+
end
|
754
|
+
|
755
|
+
define_async_method :async_wait_for_request
|
756
|
+
|
757
|
+
# @param url [String]
|
758
|
+
# @param predicate [Proc(Puppeteer::Request -> Boolean)]
|
759
|
+
private def wait_for_response(url: nil, predicate: nil, timeout: nil)
|
760
|
+
if !url && !predicate
|
761
|
+
raise ArgumentError.new('url or predicate must be specified')
|
762
|
+
end
|
763
|
+
if predicate && !predicate.is_a?(Proc)
|
764
|
+
raise ArgumentError.new('predicate must be a proc.')
|
765
|
+
end
|
766
|
+
response_predicate =
|
767
|
+
if url
|
768
|
+
-> (response) { response.url == url }
|
769
|
+
else
|
770
|
+
-> (response) { predicate.call(response) }
|
771
|
+
end
|
772
|
+
|
773
|
+
wait_for_network_manager_event('Events.NetworkManager.Response',
|
774
|
+
predicate: response_predicate,
|
775
|
+
timeout: timeout,
|
776
|
+
)
|
777
|
+
end
|
778
|
+
|
779
|
+
define_async_method :async_wait_for_response
|
727
780
|
|
728
781
|
# @param timeout [number|nil]
|
729
782
|
# @param wait_until [string|nil] 'load' | 'domcontentloaded' | 'networkidle0' | 'networkidle2'
|
@@ -762,20 +815,19 @@ class Puppeteer::Page
|
|
762
815
|
@client.send_message('Emulation.setScriptExecutionDisabled', value: !enabled)
|
763
816
|
end
|
764
817
|
|
765
|
-
#
|
766
|
-
|
767
|
-
|
768
|
-
|
769
|
-
# await this._client.send('Page.setBypassCSP', { enabled });
|
770
|
-
# }
|
818
|
+
# @param enabled [Boolean]
|
819
|
+
def bypass_csp=(enabled)
|
820
|
+
@client.send_message('Page.setBypassCSP', enabled: enabled)
|
821
|
+
end
|
771
822
|
|
772
|
-
#
|
773
|
-
|
774
|
-
|
775
|
-
|
776
|
-
|
777
|
-
|
778
|
-
|
823
|
+
# @param media_type [String|Symbol|nil] either of (media, print, nil)
|
824
|
+
def emulate_media_type(media_type)
|
825
|
+
media_type_str = media_type.to_s
|
826
|
+
unless ['screen', 'print', ''].include?(media_type_str)
|
827
|
+
raise ArgumentError.new("Unsupported media type: #{media_type}")
|
828
|
+
end
|
829
|
+
@client.send_message('Emulation.setEmulatedMedia', media: media_type_str)
|
830
|
+
end
|
779
831
|
|
780
832
|
# /**
|
781
833
|
# * @param {?Array<MediaFeature>} features
|
@@ -795,7 +847,7 @@ class Puppeteer::Page
|
|
795
847
|
|
796
848
|
# @param timezone_id [String?]
|
797
849
|
def emulate_timezone(timezone_id)
|
798
|
-
@client.send_message('Emulation.setTimezoneOverride', timezoneId:
|
850
|
+
@client.send_message('Emulation.setTimezoneOverride', timezoneId: timezone_id || '')
|
799
851
|
rescue => err
|
800
852
|
if err.message.include?('Invalid timezone')
|
801
853
|
raise ArgumentError.new("Invalid timezone ID: #{timezone_id}")
|
@@ -1034,16 +1086,6 @@ class Puppeteer::Page
|
|
1034
1086
|
|
1035
1087
|
define_async_method :async_type_text
|
1036
1088
|
|
1037
|
-
# /**
|
1038
|
-
# * @param {(string|number|Function)} selectorOrFunctionOrTimeout
|
1039
|
-
# * @param {!{visible?: boolean, hidden?: boolean, timeout?: number, polling?: string|number}=} options
|
1040
|
-
# * @param {!Array<*>} args
|
1041
|
-
# * @return {!Promise<!Puppeteer.JSHandle>}
|
1042
|
-
# */
|
1043
|
-
# waitFor(selectorOrFunctionOrTimeout, options = {}, ...args) {
|
1044
|
-
# return this.mainFrame().waitFor(selectorOrFunctionOrTimeout, options, ...args);
|
1045
|
-
# }
|
1046
|
-
|
1047
1089
|
# @param selector [String]
|
1048
1090
|
# @param visible [Boolean] Wait for element visible (not 'display: none' nor 'visibility: hidden') on true. default to false.
|
1049
1091
|
# @param hidden [Boolean] Wait for element invisible ('display: none' nor 'visibility: hidden') on true. default to false.
|
@@ -0,0 +1,336 @@
|
|
1
|
+
class Puppeteer::Request
|
2
|
+
include Puppeteer::DebugPrint
|
3
|
+
include Puppeteer::IfPresent
|
4
|
+
|
5
|
+
# defines some methods used only in NetworkManager, Response
|
6
|
+
class InternalAccessor
|
7
|
+
def initialize(request)
|
8
|
+
@request = request
|
9
|
+
end
|
10
|
+
|
11
|
+
def request_id
|
12
|
+
@request.instance_variable_get(:@request_id)
|
13
|
+
end
|
14
|
+
|
15
|
+
def interception_id
|
16
|
+
@request.instance_variable_get(:@interception_id)
|
17
|
+
end
|
18
|
+
|
19
|
+
# @param response [Puppeteer::Response]
|
20
|
+
def response=(response)
|
21
|
+
@request.instance_variable_set(:@response, response)
|
22
|
+
end
|
23
|
+
|
24
|
+
def redirect_chain
|
25
|
+
@request.instance_variable_get(:@redirect_chain)
|
26
|
+
end
|
27
|
+
|
28
|
+
def failure_text=(failure_text)
|
29
|
+
@request.instance_variable_set(:@failure_text, failure_text)
|
30
|
+
end
|
31
|
+
|
32
|
+
def from_memory_cache=(from_memory_cache)
|
33
|
+
@request.instance_variable_set(:@from_memory_cache, from_memory_cache)
|
34
|
+
end
|
35
|
+
|
36
|
+
def from_memory_cache?
|
37
|
+
@request.instance_variable_get(:@from_memory_cache)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
# @param client [Puppeteer::CDPSession]
|
42
|
+
# @param frame [Puppeteer::Frame]
|
43
|
+
# @param interception_id [string|nil]
|
44
|
+
# @param allow_interception [boolean]
|
45
|
+
# @param event [Hash]
|
46
|
+
# @param redirect_chain Array<Request>
|
47
|
+
def initialize(client, frame, interception_id, allow_interception, event, redirect_chain)
|
48
|
+
@client = client
|
49
|
+
@request_id = event['requestId']
|
50
|
+
@is_navigation_request = event['requestId'] == event['loaderId'] && event['type'] == 'Document'
|
51
|
+
@interception_id = interception_id
|
52
|
+
@allow_interception = allow_interception
|
53
|
+
@url = event['request']['url']
|
54
|
+
@resource_type = event['type'].downcase
|
55
|
+
@method = event['request']['method']
|
56
|
+
@post_data = event['request']['postData']
|
57
|
+
@frame = frame
|
58
|
+
@redirect_chain = redirect_chain
|
59
|
+
@headers = {}
|
60
|
+
event['request']['headers'].each do |key, value|
|
61
|
+
@headers[key.downcase] = value
|
62
|
+
end
|
63
|
+
@from_memory_cache = false
|
64
|
+
|
65
|
+
@internal = InternalAccessor.new(self)
|
66
|
+
end
|
67
|
+
|
68
|
+
attr_reader :internal
|
69
|
+
attr_reader :url, :resource_type, :method, :post_data, :headers, :response, :frame
|
70
|
+
|
71
|
+
def navigation_request?
|
72
|
+
@is_navigation_request
|
73
|
+
end
|
74
|
+
|
75
|
+
def redirect_chain
|
76
|
+
@redirect_chain.dup
|
77
|
+
end
|
78
|
+
|
79
|
+
def failure
|
80
|
+
if_present(@failure_text) do |failure_text|
|
81
|
+
{ errorText: @failure_text }
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
private def headers_to_array(headers)
|
86
|
+
return nil unless headers
|
87
|
+
|
88
|
+
headers.map do |key, value|
|
89
|
+
{ name: key, value: value.to_s }
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
class InterceptionNotEnabledError < StandardError
|
94
|
+
def initialize
|
95
|
+
super('Request Interception is not enabled!')
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
class AlreadyHandledError < StandardError
|
100
|
+
def initialize
|
101
|
+
super('Request is already handled!')
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
# proceed request on request interception.
|
106
|
+
#
|
107
|
+
# Example:
|
108
|
+
#
|
109
|
+
# ````
|
110
|
+
# page.on 'request' do |req|
|
111
|
+
# # Override headers
|
112
|
+
# headers = req.headers.merge(
|
113
|
+
# foo: 'bar', # set "foo" header
|
114
|
+
# origin: nil, # remove "origin" header
|
115
|
+
# )
|
116
|
+
# req.continue(headers: headers)
|
117
|
+
# end
|
118
|
+
# ```
|
119
|
+
#`
|
120
|
+
# @param error_code [String|Symbol]
|
121
|
+
def continue(url: nil, method: nil, post_data: nil, headers: nil)
|
122
|
+
# Request interception is not supported for data: urls.
|
123
|
+
return if @url.start_with?('data:')
|
124
|
+
|
125
|
+
unless @allow_interception
|
126
|
+
raise InterceptionNotEnabledError.new
|
127
|
+
end
|
128
|
+
if @interception_handled
|
129
|
+
raise AlreadyHandledError.new
|
130
|
+
end
|
131
|
+
@interception_handled = true
|
132
|
+
|
133
|
+
overrides = {
|
134
|
+
url: url,
|
135
|
+
method: method,
|
136
|
+
post_data: post_data,
|
137
|
+
headers: headers_to_array(headers),
|
138
|
+
}.compact
|
139
|
+
begin
|
140
|
+
@client.send_message('Fetch.continueRequest',
|
141
|
+
requestId: @interception_id,
|
142
|
+
**overrides,
|
143
|
+
)
|
144
|
+
rescue => err
|
145
|
+
# In certain cases, protocol will return error if the request was already canceled
|
146
|
+
# or the page was closed. We should tolerate these errors.
|
147
|
+
debug_puts(err)
|
148
|
+
end
|
149
|
+
end
|
150
|
+
|
151
|
+
# Mocking response.
|
152
|
+
#
|
153
|
+
# Example:
|
154
|
+
#
|
155
|
+
# ```
|
156
|
+
# page.on 'request' do |req|
|
157
|
+
# req.respond(
|
158
|
+
# status: 404,
|
159
|
+
# content_type: 'text/plain',
|
160
|
+
# body: 'Not Found!'
|
161
|
+
# )
|
162
|
+
# end
|
163
|
+
# ````
|
164
|
+
#
|
165
|
+
# @param status [Integer]
|
166
|
+
# @param headers [Hash<String, String>]
|
167
|
+
# @param content_type [String]
|
168
|
+
# @param body [String]
|
169
|
+
def respond(status: nil, headers: nil, content_type: nil, body: nil)
|
170
|
+
# Mocking responses for dataURL requests is not currently supported.
|
171
|
+
return if @url.start_with?('data:')
|
172
|
+
|
173
|
+
unless @allow_interception
|
174
|
+
raise InterceptionNotEnabledError.new
|
175
|
+
end
|
176
|
+
if @interception_handled
|
177
|
+
raise AlreadyHandledError.new
|
178
|
+
end
|
179
|
+
@interception_handled = true
|
180
|
+
|
181
|
+
mock_response_headers = {}
|
182
|
+
headers&.each do |key, value|
|
183
|
+
mock_response_headers[key.downcase] = value
|
184
|
+
end
|
185
|
+
if content_type
|
186
|
+
mock_response_headers['content-type'] = content_type
|
187
|
+
end
|
188
|
+
if body
|
189
|
+
mock_response_headers['content-length'] = body.length
|
190
|
+
end
|
191
|
+
|
192
|
+
mock_response = {
|
193
|
+
responseCode: status || 200,
|
194
|
+
responsePhrase: STATUS_TEXTS[(status || 200).to_s],
|
195
|
+
responseHeaders: headers_to_array(mock_response_headers),
|
196
|
+
body: if_present(body) { |mock_body| Base64.strict_encode64(mock_body) },
|
197
|
+
}.compact
|
198
|
+
begin
|
199
|
+
@client.send_message('Fetch.fulfillRequest',
|
200
|
+
requestId: @interception_id,
|
201
|
+
**mock_response,
|
202
|
+
)
|
203
|
+
rescue => err
|
204
|
+
# In certain cases, protocol will return error if the request was already canceled
|
205
|
+
# or the page was closed. We should tolerate these errors.
|
206
|
+
debug_puts(err)
|
207
|
+
end
|
208
|
+
end
|
209
|
+
|
210
|
+
# abort request on request interception.
|
211
|
+
#
|
212
|
+
# Example:
|
213
|
+
#
|
214
|
+
# ````
|
215
|
+
# page.on 'request' do |req|
|
216
|
+
# if req.url.include?("porn")
|
217
|
+
# req.abort
|
218
|
+
# else
|
219
|
+
# req.continue
|
220
|
+
# end
|
221
|
+
# end
|
222
|
+
# ```
|
223
|
+
#`
|
224
|
+
# @param error_code [String|Symbol]
|
225
|
+
def abort(error_code: :failed)
|
226
|
+
# Request interception is not supported for data: urls.
|
227
|
+
return if @url.start_with?('data:')
|
228
|
+
|
229
|
+
error_reason = ERROR_REASONS[error_code.to_s]
|
230
|
+
unless error_reason
|
231
|
+
raise ArgumentError.new("Unknown error code: #{error_code}")
|
232
|
+
end
|
233
|
+
unless @allow_interception
|
234
|
+
raise InterceptionNotEnabledError.new
|
235
|
+
end
|
236
|
+
if @interception_handled
|
237
|
+
raise AlreadyHandledError.new
|
238
|
+
end
|
239
|
+
@interception_handled = true
|
240
|
+
|
241
|
+
begin
|
242
|
+
@client.send_message('Fetch.failRequest',
|
243
|
+
requestId: @interception_id,
|
244
|
+
errorReason: error_reason,
|
245
|
+
)
|
246
|
+
rescue => err
|
247
|
+
# In certain cases, protocol will return error if the request was already canceled
|
248
|
+
# or the page was closed. We should tolerate these errors.
|
249
|
+
debug_puts(err)
|
250
|
+
end
|
251
|
+
end
|
252
|
+
|
253
|
+
ERROR_REASONS = {
|
254
|
+
'aborted' => 'Aborted',
|
255
|
+
'accessdenied' => 'AccessDenied',
|
256
|
+
'addressunreachable' => 'AddressUnreachable',
|
257
|
+
'blockedbyclient' => 'BlockedByClient',
|
258
|
+
'blockedbyresponse' => 'BlockedByResponse',
|
259
|
+
'connectionaborted' => 'ConnectionAborted',
|
260
|
+
'connectionclosed' => 'ConnectionClosed',
|
261
|
+
'connectionfailed' => 'ConnectionFailed',
|
262
|
+
'connectionrefused' => 'ConnectionRefused',
|
263
|
+
'connectionreset' => 'ConnectionReset',
|
264
|
+
'internetdisconnected' => 'InternetDisconnected',
|
265
|
+
'namenotresolved' => 'NameNotResolved',
|
266
|
+
'timedout' => 'TimedOut',
|
267
|
+
'failed' => 'Failed',
|
268
|
+
}.freeze
|
269
|
+
|
270
|
+
# List taken from https://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml with extra 306 and 418 codes.
|
271
|
+
STATUS_TEXTS = {
|
272
|
+
'100' => 'Continue',
|
273
|
+
'101' => 'Switching Protocols',
|
274
|
+
'102' => 'Processing',
|
275
|
+
'103' => 'Early Hints',
|
276
|
+
'200' => 'OK',
|
277
|
+
'201' => 'Created',
|
278
|
+
'202' => 'Accepted',
|
279
|
+
'203' => 'Non-Authoritative Information',
|
280
|
+
'204' => 'No Content',
|
281
|
+
'205' => 'Reset Content',
|
282
|
+
'206' => 'Partial Content',
|
283
|
+
'207' => 'Multi-Status',
|
284
|
+
'208' => 'Already Reported',
|
285
|
+
'226' => 'IM Used',
|
286
|
+
'300' => 'Multiple Choices',
|
287
|
+
'301' => 'Moved Permanently',
|
288
|
+
'302' => 'Found',
|
289
|
+
'303' => 'See Other',
|
290
|
+
'304' => 'Not Modified',
|
291
|
+
'305' => 'Use Proxy',
|
292
|
+
'306' => 'Switch Proxy',
|
293
|
+
'307' => 'Temporary Redirect',
|
294
|
+
'308' => 'Permanent Redirect',
|
295
|
+
'400' => 'Bad Request',
|
296
|
+
'401' => 'Unauthorized',
|
297
|
+
'402' => 'Payment Required',
|
298
|
+
'403' => 'Forbidden',
|
299
|
+
'404' => 'Not Found',
|
300
|
+
'405' => 'Method Not Allowed',
|
301
|
+
'406' => 'Not Acceptable',
|
302
|
+
'407' => 'Proxy Authentication Required',
|
303
|
+
'408' => 'Request Timeout',
|
304
|
+
'409' => 'Conflict',
|
305
|
+
'410' => 'Gone',
|
306
|
+
'411' => 'Length Required',
|
307
|
+
'412' => 'Precondition Failed',
|
308
|
+
'413' => 'Payload Too Large',
|
309
|
+
'414' => 'URI Too Long',
|
310
|
+
'415' => 'Unsupported Media Type',
|
311
|
+
'416' => 'Range Not Satisfiable',
|
312
|
+
'417' => 'Expectation Failed',
|
313
|
+
'418' => 'I\'m a teapot',
|
314
|
+
'421' => 'Misdirected Request',
|
315
|
+
'422' => 'Unprocessable Entity',
|
316
|
+
'423' => 'Locked',
|
317
|
+
'424' => 'Failed Dependency',
|
318
|
+
'425' => 'Too Early',
|
319
|
+
'426' => 'Upgrade Required',
|
320
|
+
'428' => 'Precondition Required',
|
321
|
+
'429' => 'Too Many Requests',
|
322
|
+
'431' => 'Request Header Fields Too Large',
|
323
|
+
'451' => 'Unavailable For Legal Reasons',
|
324
|
+
'500' => 'Internal Server Error',
|
325
|
+
'501' => 'Not Implemented',
|
326
|
+
'502' => 'Bad Gateway',
|
327
|
+
'503' => 'Service Unavailable',
|
328
|
+
'504' => 'Gateway Timeout',
|
329
|
+
'505' => 'HTTP Version Not Supported',
|
330
|
+
'506' => 'Variant Also Negotiates',
|
331
|
+
'507' => 'Insufficient Storage',
|
332
|
+
'508' => 'Loop Detected',
|
333
|
+
'510' => 'Not Extended',
|
334
|
+
'511' => 'Network Authentication Required',
|
335
|
+
}.freeze
|
336
|
+
end
|
@@ -0,0 +1,113 @@
|
|
1
|
+
require 'json'
|
2
|
+
|
3
|
+
class Puppeteer::Response
|
4
|
+
include Puppeteer::IfPresent
|
5
|
+
|
6
|
+
class Redirected < StandardError
|
7
|
+
def initialize
|
8
|
+
super('Response body is unavailable for redirect responses')
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
# defines methods used only in NetworkManager
|
13
|
+
class InternalAccessor
|
14
|
+
def initialize(response)
|
15
|
+
@response = response
|
16
|
+
end
|
17
|
+
|
18
|
+
def body_loaded_promise
|
19
|
+
@response.instance_variable_get(:@body_loaded_promise)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
class RemoteAddress
|
24
|
+
def initialize(ip:, port:)
|
25
|
+
@ip = ip
|
26
|
+
@port = port
|
27
|
+
end
|
28
|
+
attr_reader :ip, :port
|
29
|
+
end
|
30
|
+
|
31
|
+
# @param client [Puppeteer::CDPSession]
|
32
|
+
# @param request [Puppeteer::Request]
|
33
|
+
# @param response_payload [Hash]
|
34
|
+
def initialize(client, request, response_payload)
|
35
|
+
@client = client
|
36
|
+
@request = request
|
37
|
+
|
38
|
+
@body_loaded_promise = resolvable_future
|
39
|
+
@remote_address = RemoteAddress.new(
|
40
|
+
ip: response_payload['remoteIPAddress'],
|
41
|
+
port: response_payload['remotePort'],
|
42
|
+
)
|
43
|
+
|
44
|
+
@status = response_payload['status']
|
45
|
+
@status_text = response_payload['statusText']
|
46
|
+
@url = request.url
|
47
|
+
@from_disk_cache = !!response_payload['fromDiskCache']
|
48
|
+
@from_service_worker = !!response_payload['fromServiceWorker']
|
49
|
+
|
50
|
+
@headers = {}
|
51
|
+
response_payload['headers'].each do |key, value|
|
52
|
+
@headers[key.downcase] = value
|
53
|
+
end
|
54
|
+
@security_details = if_present(response_payload['securityDetails']) do |security_payload|
|
55
|
+
SecurityDetails.new(security_payload)
|
56
|
+
end
|
57
|
+
|
58
|
+
@internal = InternalAccessor.new(self)
|
59
|
+
end
|
60
|
+
|
61
|
+
attr_reader :internal
|
62
|
+
|
63
|
+
attr_reader :remote_address, :url, :status, :status_text, :headers, :security_details, :request
|
64
|
+
|
65
|
+
# @return [Boolean]
|
66
|
+
def ok?
|
67
|
+
@status == 0 || (@status >= 200 && @status <= 299)
|
68
|
+
end
|
69
|
+
|
70
|
+
def buffer
|
71
|
+
await @body_loaded_promise
|
72
|
+
response = @client.send_message('Network.getResponseBody', requestId: @request.internal.request_id)
|
73
|
+
if response['base64Encoded']
|
74
|
+
Base64.decode64(response['body'])
|
75
|
+
else
|
76
|
+
response['body']
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
# @param text [String]
|
81
|
+
def text
|
82
|
+
buffer
|
83
|
+
end
|
84
|
+
|
85
|
+
# @param json [Hash]
|
86
|
+
def json
|
87
|
+
JSON.parse(text)
|
88
|
+
end
|
89
|
+
|
90
|
+
def from_cache?
|
91
|
+
@from_disk_cache || @request.internal.from_memory_cache?
|
92
|
+
end
|
93
|
+
|
94
|
+
def from_service_worker?
|
95
|
+
@from_service_worker
|
96
|
+
end
|
97
|
+
|
98
|
+
def frame
|
99
|
+
@request.frame
|
100
|
+
end
|
101
|
+
|
102
|
+
class SecurityDetails
|
103
|
+
def initialize(security_payload)
|
104
|
+
@subject_name = security_payload['subjectName']
|
105
|
+
@issuer = security_payload['issuer']
|
106
|
+
@valid_from = security_payload['validFrom']
|
107
|
+
@valid_to = security_payload['validTo']
|
108
|
+
@protocol = security_payload['protocol']
|
109
|
+
end
|
110
|
+
|
111
|
+
attr_reader :subject_name, :issuer, :valid_from, :valid_to, :protocol
|
112
|
+
end
|
113
|
+
end
|
data/lib/puppeteer/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: puppeteer-ruby
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.16
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- YusukeIwaki
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2020-
|
11
|
+
date: 2020-07-12 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: concurrent-ruby
|
@@ -227,6 +227,8 @@ files:
|
|
227
227
|
- lib/puppeteer/page/pdf_options.rb
|
228
228
|
- lib/puppeteer/page/screenshot_options.rb
|
229
229
|
- lib/puppeteer/remote_object.rb
|
230
|
+
- lib/puppeteer/request.rb
|
231
|
+
- lib/puppeteer/response.rb
|
230
232
|
- lib/puppeteer/target.rb
|
231
233
|
- lib/puppeteer/timeout_settings.rb
|
232
234
|
- lib/puppeteer/touch_screen.rb
|