432 lines
14 KiB
JavaScript
432 lines
14 KiB
JavaScript
// Licensed to the Software Freedom Conservancy (SFC) under one
|
|
// or more contributor license agreements. See the NOTICE file
|
|
// distributed with this work for additional information
|
|
// regarding copyright ownership. The SFC licenses this file
|
|
// to you under the Apache License, Version 2.0 (the
|
|
// "License"); you may not use this file except in compliance
|
|
// with the License. You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing,
|
|
// software distributed under the License is distributed on an
|
|
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
|
// KIND, either express or implied. See the License for the
|
|
// specific language governing permissions and limitations
|
|
// under the License.
|
|
|
|
const { BeforeRequestSent, ResponseStarted, FetchError } = require('./networkTypes')
|
|
const { AddInterceptParameters } = require('./addInterceptParameters')
|
|
const { ContinueResponseParameters } = require('./continueResponseParameters')
|
|
const { ContinueRequestParameters } = require('./continueRequestParameters')
|
|
const { ProvideResponseParameters } = require('./provideResponseParameters')
|
|
|
|
const NetworkEvent = {
|
|
BEFORE_REQUEST_SENT: 'network.beforeRequestSent',
|
|
RESPONSE_STARTED: 'network.responseStarted',
|
|
RESPONSE_COMPLETED: 'network.responseCompleted',
|
|
AUTH_REQUIRED: 'network.authRequired',
|
|
FETCH_ERROR: 'network.fetchError',
|
|
}
|
|
|
|
const CacheBehavior = Object.freeze({
|
|
DEFAULT: 'default',
|
|
BYPASS: 'bypass',
|
|
})
|
|
|
|
/**
|
|
* Represents all commands and events of Network module.
|
|
* Described in https://w3c.github.io/webdriver-bidi/#module-network.
|
|
*/
|
|
class Network {
|
|
#callbackId = 0
|
|
#listener
|
|
|
|
/**
|
|
* Represents a Network object.
|
|
* @constructor
|
|
* @param {Driver} driver - The driver to fetch the BiDi connection.
|
|
* @param {Array} browsingContextIds - An array of browsing context IDs that the network events will be subscribed to.
|
|
*/
|
|
constructor(driver, browsingContextIds) {
|
|
this._driver = driver
|
|
this._browsingContextIds = browsingContextIds
|
|
this.#listener = new Map()
|
|
this.#listener.set(NetworkEvent.AUTH_REQUIRED, new Map())
|
|
this.#listener.set(NetworkEvent.BEFORE_REQUEST_SENT, new Map())
|
|
this.#listener.set(NetworkEvent.FETCH_ERROR, new Map())
|
|
this.#listener.set(NetworkEvent.RESPONSE_STARTED, new Map())
|
|
this.#listener.set(NetworkEvent.RESPONSE_COMPLETED, new Map())
|
|
}
|
|
|
|
addCallback(eventType, callback) {
|
|
const id = ++this.#callbackId
|
|
|
|
const eventCallbackMap = this.#listener.get(eventType)
|
|
eventCallbackMap.set(id, callback)
|
|
return id
|
|
}
|
|
|
|
removeCallback(id) {
|
|
let hasId = false
|
|
for (const [, callbacks] of this.#listener) {
|
|
if (callbacks.has(id)) {
|
|
callbacks.delete(id)
|
|
hasId = true
|
|
}
|
|
}
|
|
|
|
if (!hasId) {
|
|
throw Error(`Callback with id ${id} not found`)
|
|
}
|
|
}
|
|
|
|
invokeCallbacks(eventType, data) {
|
|
const callbacks = this.#listener.get(eventType)
|
|
if (callbacks) {
|
|
for (const [, callback] of callbacks) {
|
|
callback(data)
|
|
}
|
|
}
|
|
}
|
|
|
|
async init() {
|
|
this.bidi = await this._driver.getBidi()
|
|
}
|
|
|
|
/**
|
|
* Subscribes to the 'network.beforeRequestSent' event and handles it with the provided callback.
|
|
*
|
|
* @param {Function} callback - The callback function to handle the event.
|
|
* @returns {Promise<void>} - A promise that resolves when the subscription is successful.
|
|
*/
|
|
async beforeRequestSent(callback) {
|
|
await this.subscribeAndHandleEvent('network.beforeRequestSent', callback)
|
|
}
|
|
|
|
/**
|
|
* Subscribes to the 'network.responseStarted' event and handles it with the provided callback.
|
|
*
|
|
* @param {Function} callback - The callback function to handle the event.
|
|
* @returns {Promise<void>} - A promise that resolves when the subscription is successful.
|
|
*/
|
|
async responseStarted(callback) {
|
|
await this.subscribeAndHandleEvent('network.responseStarted', callback)
|
|
}
|
|
|
|
/**
|
|
* Subscribes to the 'network.responseCompleted' event and handles it with the provided callback.
|
|
*
|
|
* @param {Function} callback - The callback function to handle the event.
|
|
* @returns {Promise<void>} - A promise that resolves when the subscription is successful.
|
|
*/
|
|
async responseCompleted(callback) {
|
|
await this.subscribeAndHandleEvent('network.responseCompleted', callback)
|
|
}
|
|
|
|
/**
|
|
* Subscribes to the 'network.authRequired' event and handles it with the provided callback.
|
|
*
|
|
* @param {Function} callback - The callback function to handle the event.
|
|
* @returns {Promise<number>} - A promise that resolves when the subscription is successful.
|
|
*/
|
|
async authRequired(callback) {
|
|
return await this.subscribeAndHandleEvent('network.authRequired', callback)
|
|
}
|
|
|
|
/**
|
|
* Subscribes to the 'network.fetchError' event and handles it with the provided callback.
|
|
*
|
|
* @param {Function} callback - The callback function to handle the event.
|
|
* @returns {Promise<void>} - A promise that resolves when the subscription is successful.
|
|
*/
|
|
async fetchError(callback) {
|
|
await this.subscribeAndHandleEvent('network.fetchError', callback)
|
|
}
|
|
|
|
async subscribeAndHandleEvent(eventType, callback) {
|
|
if (this._browsingContextIds != null) {
|
|
await this.bidi.subscribe(eventType, this._browsingContextIds)
|
|
} else {
|
|
await this.bidi.subscribe(eventType)
|
|
}
|
|
let id = this.addCallback(eventType, callback)
|
|
|
|
this.ws = await this.bidi.socket
|
|
this.ws.on('message', (event) => {
|
|
const { params } = JSON.parse(Buffer.from(event.toString()))
|
|
if (params) {
|
|
let response = null
|
|
if ('initiator' in params) {
|
|
response = new BeforeRequestSent(
|
|
params.context,
|
|
params.navigation,
|
|
params.redirectCount,
|
|
params.request,
|
|
params.timestamp,
|
|
params.initiator,
|
|
)
|
|
} else if ('response' in params) {
|
|
response = new ResponseStarted(
|
|
params.context,
|
|
params.navigation,
|
|
params.redirectCount,
|
|
params.request,
|
|
params.timestamp,
|
|
params.response,
|
|
)
|
|
} else if ('errorText' in params) {
|
|
response = new FetchError(
|
|
params.context,
|
|
params.navigation,
|
|
params.redirectCount,
|
|
params.request,
|
|
params.timestamp,
|
|
params.errorText,
|
|
)
|
|
}
|
|
this.invokeCallbacks(eventType, response)
|
|
}
|
|
})
|
|
return id
|
|
}
|
|
|
|
/**
|
|
* Adds a network intercept.
|
|
*
|
|
* @param {AddInterceptParameters} params - The parameters for the network intercept.
|
|
* @returns {Promise<string>} - A promise that resolves to the added intercept's id.
|
|
* @throws {Error} - If params is not an instance of AddInterceptParameters.
|
|
*/
|
|
async addIntercept(params) {
|
|
if (!(params instanceof AddInterceptParameters)) {
|
|
throw new Error(`Params must be an instance of AddInterceptParameters. Received:'${params}'`)
|
|
}
|
|
|
|
const command = {
|
|
method: 'network.addIntercept',
|
|
params: Object.fromEntries(params.asMap()),
|
|
}
|
|
|
|
let response = await this.bidi.send(command)
|
|
|
|
return response.result.intercept
|
|
}
|
|
|
|
/**
|
|
* Removes an intercept.
|
|
*
|
|
* @param {string} interceptId - The ID of the intercept to be removed.
|
|
* @returns {Promise<void>} - A promise that resolves when the intercept is successfully removed.
|
|
*/
|
|
async removeIntercept(interceptId) {
|
|
const command = {
|
|
method: 'network.removeIntercept',
|
|
params: { intercept: interceptId },
|
|
}
|
|
|
|
await this.bidi.send(command)
|
|
}
|
|
|
|
/**
|
|
* Continues the network request with authentication credentials.
|
|
* @param {string} requestId - The ID of the request to continue.
|
|
* @param {string} username - The username for authentication.
|
|
* @param {string} password - The password for authentication.
|
|
* @returns {Promise<void>} - A promise that resolves when the command is sent.
|
|
*/
|
|
async continueWithAuth(requestId, username, password) {
|
|
const command = {
|
|
method: 'network.continueWithAuth',
|
|
params: {
|
|
request: requestId.toString(),
|
|
action: 'provideCredentials',
|
|
credentials: {
|
|
type: 'password',
|
|
username: username,
|
|
password: password,
|
|
},
|
|
},
|
|
}
|
|
await this.bidi.send(command)
|
|
}
|
|
|
|
/**
|
|
* Fails a network request.
|
|
*
|
|
* @param {number} requestId - The ID of the request to fail.
|
|
* @returns {Promise<void>} - A promise that resolves when the command is sent.
|
|
*/
|
|
async failRequest(requestId) {
|
|
const command = {
|
|
method: 'network.failRequest',
|
|
params: {
|
|
request: requestId.toString(),
|
|
},
|
|
}
|
|
await this.bidi.send(command)
|
|
}
|
|
|
|
/**
|
|
* Continues the network request with authentication but without providing credentials.
|
|
* @param {string} requestId - The ID of the request to continue with authentication.
|
|
* @returns {Promise<void>} - A promise that resolves when the command is sent.
|
|
*/
|
|
async continueWithAuthNoCredentials(requestId) {
|
|
const command = {
|
|
method: 'network.continueWithAuth',
|
|
params: {
|
|
request: requestId.toString(),
|
|
action: 'default',
|
|
},
|
|
}
|
|
await this.bidi.send(command)
|
|
}
|
|
|
|
/**
|
|
* Cancels the authentication for a specific request.
|
|
*
|
|
* @param {string} requestId - The ID of the request to cancel authentication for.
|
|
* @returns {Promise<void>} - A promise that resolves when the command is sent.
|
|
*/
|
|
async cancelAuth(requestId) {
|
|
const command = {
|
|
method: 'network.continueWithAuth',
|
|
params: {
|
|
request: requestId.toString(),
|
|
action: 'cancel',
|
|
},
|
|
}
|
|
await this.bidi.send(command)
|
|
}
|
|
|
|
/**
|
|
* Continues the network request with the provided parameters.
|
|
*
|
|
* @param {ContinueRequestParameters} params - The parameters for continuing the request.
|
|
* @throws {Error} If params is not an instance of ContinueRequestParameters.
|
|
* @returns {Promise<void>} A promise that resolves when the command is sent.
|
|
*/
|
|
async continueRequest(params) {
|
|
if (!(params instanceof ContinueRequestParameters)) {
|
|
throw new Error(`Params must be an instance of ContinueRequestParameters. Received:'${params}'`)
|
|
}
|
|
|
|
const command = {
|
|
method: 'network.continueRequest',
|
|
params: Object.fromEntries(params.asMap()),
|
|
}
|
|
|
|
await this.bidi.send(command)
|
|
}
|
|
|
|
/**
|
|
* Continues the network response with the given parameters.
|
|
*
|
|
* @param {ContinueResponseParameters} params - The parameters for continuing the response.
|
|
* @throws {Error} If params is not an instance of ContinueResponseParameters.
|
|
* @returns {Promise<void>} A promise that resolves when the command is sent.
|
|
*/
|
|
async continueResponse(params) {
|
|
if (!(params instanceof ContinueResponseParameters)) {
|
|
throw new Error(`Params must be an instance of ContinueResponseParameters. Received:'${params}'`)
|
|
}
|
|
|
|
const command = {
|
|
method: 'network.continueResponse',
|
|
params: Object.fromEntries(params.asMap()),
|
|
}
|
|
|
|
await this.bidi.send(command)
|
|
}
|
|
|
|
/**
|
|
* Provides a response for the network.
|
|
*
|
|
* @param {ProvideResponseParameters} params - The parameters for providing the response.
|
|
* @throws {Error} If params is not an instance of ProvideResponseParameters.
|
|
* @returns {Promise<void>} A promise that resolves when the command is sent.
|
|
*/
|
|
async provideResponse(params) {
|
|
if (!(params instanceof ProvideResponseParameters)) {
|
|
throw new Error(`Params must be an instance of ProvideResponseParameters. Received:'${params}'`)
|
|
}
|
|
|
|
const command = {
|
|
method: 'network.provideResponse',
|
|
params: Object.fromEntries(params.asMap()),
|
|
}
|
|
|
|
await this.bidi.send(command)
|
|
}
|
|
|
|
/**
|
|
* Sets the cache behavior for network requests.
|
|
*
|
|
* @param {string} behavior - The cache behavior ("default" or "bypass")
|
|
* @param {Array<string>} [contexts] - Optional array of browsing context IDs
|
|
* @returns {Promise<void>} A promise that resolves when the cache behavior is set
|
|
* @throws {Error} If behavior is invalid or context IDs are invalid
|
|
*/
|
|
async setCacheBehavior(behavior, contexts = null) {
|
|
if (!Object.values(CacheBehavior).includes(behavior)) {
|
|
throw new Error(`Cache behavior must be either "${CacheBehavior.DEFAULT}" or "${CacheBehavior.BYPASS}"`)
|
|
}
|
|
|
|
const command = {
|
|
method: 'network.setCacheBehavior',
|
|
params: {
|
|
cacheBehavior: behavior,
|
|
},
|
|
}
|
|
|
|
if (contexts !== null) {
|
|
if (
|
|
!Array.isArray(contexts) ||
|
|
contexts.length === 0 ||
|
|
contexts.some((c) => typeof c !== 'string' || c.trim() === '')
|
|
) {
|
|
throw new Error('Contexts must be an array of non-empty strings')
|
|
}
|
|
command.params.contexts = contexts
|
|
}
|
|
|
|
await this.bidi.send(command)
|
|
}
|
|
|
|
/**
|
|
* Unsubscribes from network events for all browsing contexts.
|
|
* @returns {Promise<void>} A promise that resolves when the network connection is closed.
|
|
*/
|
|
async close() {
|
|
if (
|
|
this._browsingContextIds !== null &&
|
|
this._browsingContextIds !== undefined &&
|
|
this._browsingContextIds.length > 0
|
|
) {
|
|
await this.bidi.unsubscribe(
|
|
'network.beforeRequestSent',
|
|
'network.responseStarted',
|
|
'network.responseCompleted',
|
|
'network.authRequired',
|
|
this._browsingContextIds,
|
|
)
|
|
} else {
|
|
await this.bidi.unsubscribe(
|
|
'network.beforeRequestSent',
|
|
'network.responseStarted',
|
|
'network.responseCompleted',
|
|
'network.authRequired',
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
async function getNetworkInstance(driver, browsingContextIds = null) {
|
|
let instance = new Network(driver, browsingContextIds)
|
|
await instance.init()
|
|
return instance
|
|
}
|
|
|
|
module.exports = { Network: getNetworkInstance, CacheBehavior }
|