Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
SeleniumHQ
GitHub Repository: SeleniumHQ/Selenium
Path: blob/trunk/rb/lib/selenium/webdriver/devtools/network_interceptor.rb
1865 views
1
# frozen_string_literal: true
2
3
# Licensed to the Software Freedom Conservancy (SFC) under one
4
# or more contributor license agreements. See the NOTICE file
5
# distributed with this work for additional information
6
# regarding copyright ownership. The SFC licenses this file
7
# to you under the Apache License, Version 2.0 (the
8
# "License"); you may not use this file except in compliance
9
# with the License. You may obtain a copy of the License at
10
#
11
# http://www.apache.org/licenses/LICENSE-2.0
12
#
13
# Unless required by applicable law or agreed to in writing,
14
# software distributed under the License is distributed on an
15
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
16
# KIND, either express or implied. See the License for the
17
# specific language governing permissions and limitations
18
# under the License.
19
20
module Selenium
21
module WebDriver
22
class DevTools
23
#
24
# Wraps the network request/response interception, providing
25
# thread-safety guarantees and handling special cases such as browser
26
# canceling requests midst interception.
27
#
28
# You should not be using this class directly, use Driver#intercept instead.
29
# @api private
30
#
31
32
class NetworkInterceptor
33
# CDP fails to get body on certain responses (301) and raises:
34
# "Can only get response body on requests captured after headers received."
35
CANNOT_GET_BODY_ON_REDIRECT_ERROR_CODE = '-32000'
36
37
# CDP fails to operate with intercepted requests.
38
# Typical reason is browser cancelling intercepted requests/responses.
39
INVALID_INTERCEPTION_ID_ERROR_CODE = '-32602'
40
41
def initialize(devtools)
42
@devtools = devtools
43
@lock = Mutex.new
44
end
45
46
def intercept(&block)
47
devtools.network.on(:loading_failed) { |params| track_cancelled_request(params) }
48
devtools.fetch.on(:request_paused) { |params| request_paused(params, &block) }
49
50
devtools.network.set_cache_disabled(cache_disabled: true)
51
devtools.network.enable
52
devtools.fetch.enable(patterns: [{requestStage: 'Request'}, {requestStage: 'Response'}])
53
end
54
55
private
56
57
attr_accessor :devtools, :lock
58
59
# We should be thread-safe to use the hash without synchronization
60
# because its keys are interception job identifiers and they should be
61
# unique within a devtools session.
62
def pending_response_requests
63
@pending_response_requests ||= {}
64
end
65
66
# Ensure usage of cancelled_requests is thread-safe via synchronization!
67
def cancelled_requests
68
@cancelled_requests ||= []
69
end
70
71
def track_cancelled_request(data)
72
return unless data['canceled']
73
74
lock.synchronize { cancelled_requests << data['requestId'] }
75
end
76
77
def request_paused(data, &block)
78
id = data['requestId']
79
network_id = data['networkId']
80
81
with_cancellable_request(network_id) do
82
if response?(data)
83
block = pending_response_requests.delete(id)
84
intercept_response(id, data, &block)
85
else
86
intercept_request(id, data, &block)
87
end
88
end
89
end
90
91
# The presence of any of these fields indicate we're at the response stage.
92
# @see https://chromedevtools.github.io/devtools-protocol/tot/Fetch/#event-requestPaused
93
def response?(params)
94
params.key?('responseStatusCode') || params.key?('responseErrorReason')
95
end
96
97
def intercept_request(id, params, &block)
98
original = DevTools::Request.from(id, params)
99
mutable = DevTools::Request.from(id, params)
100
101
block.call(mutable) do |&continue|
102
pending_response_requests[id] = continue
103
104
if original == mutable
105
continue_request(original.id)
106
else
107
mutate_request(mutable)
108
end
109
end
110
end
111
112
def intercept_response(id, params)
113
return continue_response(id) unless block_given?
114
115
body = fetch_response_body(id)
116
original = DevTools::Response.from(id, body, params)
117
mutable = DevTools::Response.from(id, body, params)
118
yield mutable
119
120
if original == mutable
121
continue_response(id)
122
else
123
mutate_response(mutable)
124
end
125
end
126
127
def continue_request(id)
128
devtools.fetch.continue_request(request_id: id)
129
end
130
alias continue_response continue_request
131
132
def mutate_request(request)
133
devtools.fetch.continue_request(
134
request_id: request.id,
135
url: request.url,
136
method: request.method,
137
post_data: (Base64.strict_encode64(request.post_data) if request.post_data),
138
headers: request.headers.map do |k, v|
139
{name: k, value: v}
140
end
141
)
142
end
143
144
def mutate_response(response)
145
devtools.fetch.fulfill_request(
146
request_id: response.id,
147
body: (Base64.strict_encode64(response.body) if response.body),
148
response_code: response.code,
149
response_headers: response.headers.map do |k, v|
150
{name: k, value: v}
151
end
152
)
153
end
154
155
def fetch_response_body(id)
156
devtools.fetch.get_response_body(request_id: id).dig('result', 'body')
157
rescue Error::WebDriverError => e
158
raise unless e.message.start_with?(CANNOT_GET_BODY_ON_REDIRECT_ERROR_CODE)
159
end
160
161
def with_cancellable_request(network_id)
162
yield
163
rescue Error::WebDriverError => e
164
raise if e.message.start_with?(INVALID_INTERCEPTION_ID_ERROR_CODE) && !cancelled?(network_id)
165
end
166
167
def cancelled?(network_id)
168
lock.synchronize { !!cancelled_requests.delete(network_id) }
169
end
170
end # NetworkInterceptor
171
end # DevTools
172
end # WebDriver
173
end # Selenium
174
175