Path: blob/trunk/rb/lib/selenium/webdriver/devtools/network_interceptor.rb
1865 views
# frozen_string_literal: true12# Licensed to the Software Freedom Conservancy (SFC) under one3# or more contributor license agreements. See the NOTICE file4# distributed with this work for additional information5# regarding copyright ownership. The SFC licenses this file6# to you under the Apache License, Version 2.0 (the7# "License"); you may not use this file except in compliance8# with the License. You may obtain a copy of the License at9#10# http://www.apache.org/licenses/LICENSE-2.011#12# Unless required by applicable law or agreed to in writing,13# software distributed under the License is distributed on an14# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY15# KIND, either express or implied. See the License for the16# specific language governing permissions and limitations17# under the License.1819module Selenium20module WebDriver21class DevTools22#23# Wraps the network request/response interception, providing24# thread-safety guarantees and handling special cases such as browser25# canceling requests midst interception.26#27# You should not be using this class directly, use Driver#intercept instead.28# @api private29#3031class NetworkInterceptor32# CDP fails to get body on certain responses (301) and raises:33# "Can only get response body on requests captured after headers received."34CANNOT_GET_BODY_ON_REDIRECT_ERROR_CODE = '-32000'3536# CDP fails to operate with intercepted requests.37# Typical reason is browser cancelling intercepted requests/responses.38INVALID_INTERCEPTION_ID_ERROR_CODE = '-32602'3940def initialize(devtools)41@devtools = devtools42@lock = Mutex.new43end4445def intercept(&block)46devtools.network.on(:loading_failed) { |params| track_cancelled_request(params) }47devtools.fetch.on(:request_paused) { |params| request_paused(params, &block) }4849devtools.network.set_cache_disabled(cache_disabled: true)50devtools.network.enable51devtools.fetch.enable(patterns: [{requestStage: 'Request'}, {requestStage: 'Response'}])52end5354private5556attr_accessor :devtools, :lock5758# We should be thread-safe to use the hash without synchronization59# because its keys are interception job identifiers and they should be60# unique within a devtools session.61def pending_response_requests62@pending_response_requests ||= {}63end6465# Ensure usage of cancelled_requests is thread-safe via synchronization!66def cancelled_requests67@cancelled_requests ||= []68end6970def track_cancelled_request(data)71return unless data['canceled']7273lock.synchronize { cancelled_requests << data['requestId'] }74end7576def request_paused(data, &block)77id = data['requestId']78network_id = data['networkId']7980with_cancellable_request(network_id) do81if response?(data)82block = pending_response_requests.delete(id)83intercept_response(id, data, &block)84else85intercept_request(id, data, &block)86end87end88end8990# The presence of any of these fields indicate we're at the response stage.91# @see https://chromedevtools.github.io/devtools-protocol/tot/Fetch/#event-requestPaused92def response?(params)93params.key?('responseStatusCode') || params.key?('responseErrorReason')94end9596def intercept_request(id, params, &block)97original = DevTools::Request.from(id, params)98mutable = DevTools::Request.from(id, params)99100block.call(mutable) do |&continue|101pending_response_requests[id] = continue102103if original == mutable104continue_request(original.id)105else106mutate_request(mutable)107end108end109end110111def intercept_response(id, params)112return continue_response(id) unless block_given?113114body = fetch_response_body(id)115original = DevTools::Response.from(id, body, params)116mutable = DevTools::Response.from(id, body, params)117yield mutable118119if original == mutable120continue_response(id)121else122mutate_response(mutable)123end124end125126def continue_request(id)127devtools.fetch.continue_request(request_id: id)128end129alias continue_response continue_request130131def mutate_request(request)132devtools.fetch.continue_request(133request_id: request.id,134url: request.url,135method: request.method,136post_data: (Base64.strict_encode64(request.post_data) if request.post_data),137headers: request.headers.map do |k, v|138{name: k, value: v}139end140)141end142143def mutate_response(response)144devtools.fetch.fulfill_request(145request_id: response.id,146body: (Base64.strict_encode64(response.body) if response.body),147response_code: response.code,148response_headers: response.headers.map do |k, v|149{name: k, value: v}150end151)152end153154def fetch_response_body(id)155devtools.fetch.get_response_body(request_id: id).dig('result', 'body')156rescue Error::WebDriverError => e157raise unless e.message.start_with?(CANNOT_GET_BODY_ON_REDIRECT_ERROR_CODE)158end159160def with_cancellable_request(network_id)161yield162rescue Error::WebDriverError => e163raise if e.message.start_with?(INVALID_INTERCEPTION_ID_ERROR_CODE) && !cancelled?(network_id)164end165166def cancelled?(network_id)167lock.synchronize { !!cancelled_requests.delete(network_id) }168end169end # NetworkInterceptor170end # DevTools171end # WebDriver172end # Selenium173174175