Skip to content

[js][bidi] Add request and response handler #15571

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 4 commits into
base: trunk
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 8 additions & 8 deletions javascript/selenium-webdriver/bidi/network.js
Original file line number Diff line number Diff line change
Expand Up @@ -154,26 +154,26 @@ class Network {

this.ws = await this.bidi.socket
this.ws.on('message', (event) => {
const { params } = JSON.parse(Buffer.from(event.toString()))
const { method, params } = JSON.parse(Buffer.from(event.toString()))
if (params) {
let response = null
if ('initiator' in params) {
response = new BeforeRequestSent(
if ('request' in params && 'response' in params) {
response = new ResponseStarted(
params.context,
params.navigation,
params.redirectCount,
params.request,
params.timestamp,
params.initiator,
params.response,
)
} else if ('response' in params) {
response = new ResponseStarted(
} else if ('initiator' in params && !('response' in params)) {
response = new BeforeRequestSent(
params.context,
params.navigation,
params.redirectCount,
params.request,
params.timestamp,
params.response,
params.initiator,
)
} else if ('errorText' in params) {
response = new FetchError(
Expand All @@ -185,7 +185,7 @@ class Network {
params.errorText,
)
}
this.invokeCallbacks(eventType, response)
this.invokeCallbacks(method, response)
}
})
return id
Expand Down
11 changes: 11 additions & 0 deletions javascript/selenium-webdriver/bidi/networkTypes.js
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,17 @@ class Header {
get value() {
return this._value
}

/**
* Converts the Header to a map.
* @returns {Map<string, string>} A map representation of the Header.
*/
asMap() {
const map = new Map()
map.set('name', this._name)
map.set('value', Object.fromEntries(this._value.asMap()))
return map
}
}

/**
Expand Down
10 changes: 8 additions & 2 deletions javascript/selenium-webdriver/lib/http.js
Original file line number Diff line number Diff line change
Expand Up @@ -88,12 +88,18 @@ class Request {
* @param {string} method The HTTP method to use for the request.
* @param {string} path The path on the server to send the request to.
* @param {Object=} opt_data This request's non-serialized JSON payload data.
* @param {Map<string, string>} [headers=new Map()] - The optional headers as a Map.
*/
constructor(method, path, opt_data) {
constructor(method, path, opt_data, headers = new Map()) {
this.method = /** string */ method
this.path = /** string */ path
this.data = /** Object */ opt_data
this.headers = /** !Map<string, string> */ new Map([['Accept', 'application/json; charset=utf-8']])

if (headers.size > 0) {
this.headers = headers
} else {
this.headers = /** !Map<string, string> */ new Map([['Accept', 'application/json; charset=utf-8']])
}
}

/** @override */
Expand Down
175 changes: 175 additions & 0 deletions javascript/selenium-webdriver/lib/network.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,18 @@
const { Network: getNetwork } = require('../bidi/network')
const { InterceptPhase } = require('../bidi/interceptPhase')
const { AddInterceptParameters } = require('../bidi/addInterceptParameters')
const { ContinueRequestParameters } = require('../bidi/continueRequestParameters')
const { ProvideResponseParameters } = require('../bidi/provideResponseParameters')
const { Request } = require('./http')
const { BytesValue, Header } = require('../bidi/networkTypes')

class Network {
#callbackId = 0
#driver
#network
#authHandlers = new Map()
#requestHandlers = new Map()
#responseHandlers = new Map()

constructor(driver) {
this.#driver = driver
Expand All @@ -43,6 +49,8 @@ class Network {

await this.#network.addIntercept(new AddInterceptParameters(InterceptPhase.AUTH_REQUIRED))

await this.#network.addIntercept(new AddInterceptParameters(InterceptPhase.BEFORE_REQUEST_SENT))

await this.#network.authRequired(async (event) => {
const requestId = event.request.request
const uri = event.request.url
Expand All @@ -54,6 +62,76 @@ class Network {

await this.#network.continueWithAuthNoCredentials(requestId)
})

await this.#network.beforeRequestSent(async (event) => {
const requestId = event.request.request
const requestData = event.request

// Build the original request from the intercepted request details.
const originalRequest = new Request(requestData.method, requestData.url, null, new Map(requestData.headers))

let requestHandler = this.getRequestHandler(originalRequest)
let responseHandler = this.getResponseHandler(originalRequest)

// Populate the headers of the original request.
// Body is not available as part of WebDriver Spec, hence we cannot add that or use that.

const continueRequestParams = new ContinueRequestParameters(requestId)

// If a response handler exists, we mock the response instead of modifying the outgoing request
if (responseHandler !== null) {
const modifiedResponse = await responseHandler()

const provideResponseParams = new ProvideResponseParameters(requestId)
provideResponseParams.statusCode(modifiedResponse.status)

// Convert headers
if (modifiedResponse.headers.size > 0) {
const headers = []

modifiedResponse.headers.forEach((value, key) => {
headers.push(new Header(key, new BytesValue('string', value)))
})
provideResponseParams.headers(headers)
}

// Convert body if available
if (modifiedResponse.body && modifiedResponse.body.length > 0) {
provideResponseParams.body(new BytesValue('string', modifiedResponse.body))
}

await this.#network.provideResponse(provideResponseParams)
return
}

// If request handler exists, modify the request
if (requestHandler !== null) {
const modifiedRequest = requestHandler(originalRequest)

continueRequestParams.method(modifiedRequest.method)

if (originalRequest.path !== modifiedRequest.path) {
continueRequestParams.url(modifiedRequest.path)
}

// Convert headers
if (modifiedRequest.headers.size > 0) {
const headers = []

modifiedRequest.headers.forEach((value, key) => {
headers.push(new Header(key, new BytesValue('string', value)))
})
continueRequestParams.headers(headers)
}

if (modifiedRequest.data && modifiedRequest.data.length > 0) {
continueRequestParams.body(new BytesValue('string', modifiedRequest.data))
}
}

// Continue with the modified or original request
await this.#network.continueRequest(continueRequestParams)
})
}

getAuthCredentials(uri) {
Expand All @@ -64,6 +142,27 @@ class Network {
}
return null
}

getRequestHandler(req) {
for (let [, value] of this.#requestHandlers) {
const filter = value.filter
if (filter(req)) {
return value.handler
}
}
return null
}

getResponseHandler(req) {
for (let [, value] of this.#responseHandlers) {
const filter = value.filter
if (filter(req)) {
return value.handler
}
}
return null
}

async addAuthenticationHandler(username, password, uri = '//') {
await this.#init()

Expand All @@ -86,6 +185,82 @@ class Network {
async clearAuthenticationHandlers() {
this.#authHandlers.clear()
}

/**
* Adds a request handler that filters requests based on a predicate function.
* @param {Function} filter - A function that takes an HTTP request and returns true or false.
* @param {Function} handler - A function that takes an HTTP request and returns a modified request.
* @returns {number} - A unique handler ID.
* @throws {Error} - If filter is not a function or handler does not return a request.
*/
async addRequestHandler(filter, handler) {
if (typeof filter !== 'function') {
throw new Error('Filter must be a function.')
}

if (typeof handler !== 'function') {
throw new Error('Handler must be a function.')
}

await this.#init()

const id = this.#callbackId++

this.#requestHandlers.set(id, { filter, handler })
return id
}

async removeRequestHandler(id) {
await this.#init()

if (this.#requestHandlers.has(id)) {
this.#requestHandlers.delete(id)
} else {
throw Error(`Callback with id ${id} not found`)
}
}

async clearRequestHandlers() {
this.#requestHandlers.clear()
}

/**
* Adds a response handler that mocks responses.
* @param {Function} filter - A function that takes an HTTP request, returning a boolean.
* @param {Function} handler - A function that returns a mocked HTTP response.
* @returns {number} - A unique handler ID.
* @throws {Error} - If filter is not a function or handler is not an async function.
*/
async addResponseHandler(filter, handler) {
if (typeof filter !== 'function') {
throw new Error('Filter must be a function.')
}

if (typeof handler !== 'function') {
throw new Error('Handler must be a function.')
}

await this.#init()

const id = this.#callbackId++

this.#responseHandlers.set(id, { filter, handler })
return id
}

async removeResponseHandler(id) {
await this.#init()

if (this.#responseHandlers.has(id)) {
this.#responseHandlers.delete(id)
} else {
throw Error(`Callback with id ${id} not found`)
}
}

async clearResponseHandlers() {
this.#responseHandlers.clear()
}
}

module.exports = Network
29 changes: 29 additions & 0 deletions javascript/selenium-webdriver/lib/test/fileserver.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ const Pages = (function () {
})
}

addPage('addRequestBody', 'addRequestBody')
addPage('ajaxyPage', 'ajaxy_page.html')
addPage('alertsPage', 'alerts.html')
addPage('basicAuth', 'basicAuth')
Expand Down Expand Up @@ -131,6 +132,7 @@ const Path = {
PAGE: WEB_ROOT + '/page',
SLEEP: WEB_ROOT + '/sleep',
UPLOAD: WEB_ROOT + '/upload',
ADD_REQUEST_BODY: WEB_ROOT + '/addRequestBody',
}

var app = express()
Expand All @@ -143,6 +145,7 @@ app
})
.use(JS_ROOT, serveIndex(jsDirectory), express.static(jsDirectory))
.post(Path.UPLOAD, handleUpload)
.post(Path.ADD_REQUEST_BODY, addRequestBody)
.use(WEB_ROOT, serveIndex(baseDirectory), express.static(baseDirectory))
.use(DATA_ROOT, serveIndex(dataDirectory), express.static(dataDirectory))
.get(Path.ECHO, sendEcho)
Expand Down Expand Up @@ -187,6 +190,32 @@ function sendInifinitePage(request, response) {
response.end(body)
}

function addRequestBody(request, response) {
let requestBody = ''

request.on('data', (chunk) => {
requestBody += chunk
})

request.on('end', () => {
let body = [
'<!DOCTYPE html>',
'<html>',
'<head><title>Page</title></head>',
'<body>',
`<p>Request Body:</p><pre>${requestBody}</pre>`,
'</body>',
'</html>',
].join('')

response.writeHead(200, {
'Content-Length': Buffer.byteLength(body, 'utf8'),
'Content-Type': 'text/html; charset=utf-8',
})
response.end(body)
})
}

function sendBasicAuth(request, response) {
const denyAccess = function () {
response.writeHead(401, { 'WWW-Authenticate': 'Basic realm="test"' })
Expand Down
Loading
Loading