Skip to content

Commit

Permalink
Bug 1850680 - [bidi] Add support for headers, cookies, method and bod…
Browse files Browse the repository at this point in the history
…y parameters in continueRequest r=webdriver-reviewers,whimboo

Depends on D209538

Differential Revision: https://phabricator.services.mozilla.com/D209539
  • Loading branch information
juliandescottes committed May 28, 2024
1 parent 9d043c2 commit 6ec011d
Show file tree
Hide file tree
Showing 3 changed files with 225 additions and 37 deletions.
61 changes: 61 additions & 0 deletions remote/shared/NetworkRequest.sys.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,67 @@ export class NetworkRequest {
this.#postData = postData;
}

/**
* Set the request post body
*
* @param {string} body
* The body to set.
*/
setRequestBody(body) {
// Update the requestObserversCalled flag to allow modifying the request,
// and reset once done.
this.#channel.requestObserversCalled = false;

try {
this.#channel.QueryInterface(Ci.nsIUploadChannel2);
const bodyStream = Cc[
"@mozilla.org/io/string-input-stream;1"
].createInstance(Ci.nsIStringInputStream);
bodyStream.setData(body, body.length);
this.#channel.explicitSetUploadStream(
bodyStream,
null,
-1,
this.#channel.requestMethod,
false
);
} finally {
// Make sure to reset the flag once the modification was attempted.
this.#channel.requestObserversCalled = true;
}
}

/**
* Set a request header
*
* @param {string} name
* The header's name.
* @param {string} value
* The header's value.
*/
setRequestHeader(name, value) {
this.#channel.setRequestHeader(name, value, false);
}

/**
* Update the request's method.
*
* @param {string} method
* The method to set.
*/
setRequestMethod(method) {
// Update the requestObserversCalled flag to allow modifying the request,
// and reset once done.
this.#channel.requestObserversCalled = false;

try {
this.#channel.requestMethod = method;
} finally {
// Make sure to reset the flag once the modification was attempted.
this.#channel.requestObserversCalled = true;
}
}

/**
* Convert the provided request timing to a timing relative to the beginning
* of the request. All timings are numbers representing high definition
Expand Down
179 changes: 159 additions & 20 deletions remote/webdriver-bidi/modules/root/network.sys.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -433,19 +433,20 @@ class NetworkModule extends Module {
* @param {object=} options
* @param {string} options.request
* The id of the blocked request that should be continued.
* @param {BytesValue=} options.body [unsupported]
* @param {BytesValue=} options.body
* Optional BytesValue to replace the body of the request.
* @param {Array<CookieHeader>=} options.cookies [unsupported]
* @param {Array<CookieHeader>=} options.cookies
* Optional array of cookie header values to replace the cookie header of
* the request.
* @param {Array<Header>=} options.headers [unsupported]
* @param {Array<Header>=} options.headers
* Optional array of headers to replace the headers of the request.
* request.
* @param {string=} options.method [unsupported]
* @param {string=} options.method
* Optional string to replace the method of the request.
* @param {string=} options.url [unsupported]
* Optional string to replace the url of the request. If the provided url
* is not a valid URL, an InvalidArgumentError will be thrown.
* Support will be added in https://bugzilla.mozilla.org/show_bug.cgi?id=1898158
*
* @throws {InvalidArgumentError}
* Raised if an argument is of an invalid type or value.
Expand Down Expand Up @@ -473,10 +474,6 @@ class NetworkModule extends Module {
body,
`Expected "body" to be a network.BytesValue, got ${body}`
);

throw new lazy.error.UnsupportedOperationError(
`"body" not supported yet in network.continueRequest`
);
}

if (cookies !== null) {
Expand All @@ -491,12 +488,9 @@ class NetworkModule extends Module {
`Expected values in "cookies" to be network.CookieHeader, got ${cookie}`
);
}

throw new lazy.error.UnsupportedOperationError(
`"cookies" not supported yet in network.continueRequest`
);
}

const deserializedHeaders = [];
if (headers !== null) {
lazy.assert.array(
headers,
Expand All @@ -508,22 +502,30 @@ class NetworkModule extends Module {
header,
`Expected values in "headers" to be network.Header, got ${header}`
);
}

throw new lazy.error.UnsupportedOperationError(
`"headers" not supported yet in network.continueRequest`
);
// Deserialize headers immediately to validate the value
const deserializedHeader = this.#deserializeHeader(header);
lazy.assert.that(
value => this.#isValidHttpToken(value),
`Expected "header" name to be a valid HTTP token, got ${deserializedHeader[0]}`
)(deserializedHeader[0]);
lazy.assert.that(
value => this.#isValidHeaderValue(value),
`Expected header value to be a valid header value, got ${deserializedHeader[1]}`
)(deserializedHeader[1]);
deserializedHeaders.push(deserializedHeader);
}
}

if (method !== null) {
lazy.assert.string(
method,
`Expected "method" to be a string, got ${method}`
);

throw new lazy.error.UnsupportedOperationError(
`"method" not supported yet in network.continueRequest`
);
lazy.assert.that(
value => this.#isValidHttpToken(value),
`Expected "method" to be a valid HTTP token, got ${method}`
)(method);
}

if (url !== null) {
Expand All @@ -549,6 +551,53 @@ class NetworkModule extends Module {
);
}

if (method !== null) {
request.setRequestMethod(method);
}

if (headers !== null) {
// Delete all existing request headers not found in the headers parameter.
request.getHeadersList().forEach(([name]) => {
if (!headers.some(header => header.name == name)) {
request.setRequestHeader(name, "");
}
});

// Set all headers specified in the headers parameter.
for (const [name, value] of deserializedHeaders) {
request.setRequestHeader(name, value);
}
}

if (cookies !== null) {
let cookieHeader = "";
for (const cookie of cookies) {
if (cookieHeader != "") {
cookieHeader += ";";
}
cookieHeader += this.#serializeCookieHeader(cookie);
}

let foundCookieHeader = false;
const requestHeaders = request.getHeadersList();
for (const [name] of requestHeaders) {
if (name.toLowerCase() == "cookie") {
request.setRequestHeader(name, cookieHeader);
foundCookieHeader = true;
break;
}
}

if (!foundCookieHeader) {
request.setRequestHeader("Cookie", cookieHeader);
}
}

if (body !== null) {
const value = deserializeBytesValue(body);
request.setRequestBody(value);
}

request.wrappedChannel.resume();

resolveBlockedEvent();
Expand Down Expand Up @@ -1109,6 +1158,12 @@ class NetworkModule extends Module {
}
}

#deserializeHeader(protocolHeader) {
const name = protocolHeader.name;
const value = deserializeBytesValue(protocolHeader.value);
return [name, value];
}

#extractChallenges(response) {
let headerName;

Expand Down Expand Up @@ -1310,6 +1365,65 @@ class NetworkModule extends Module {
return `Request (id: ${requestData.request}) suspended by WebDriver BiDi in ${phase} phase`;
}

#isValidHeaderValue(value) {
if (!value.length) {
return true;
}

// For non-empty strings check against:
// - leading or trailing tabs & spaces
// - new lines and null bytes
const chars = value.split("");
const tabOrSpace = [" ", "\t"];
const forbiddenChars = ["\r", "\n", "\0"];
return (
!tabOrSpace.includes(chars.at(0)) &&
!tabOrSpace.includes(chars.at(-1)) &&
forbiddenChars.every(c => !chars.includes(c))
);
}

/**
* This helper is adapted from a C++ validation helper in nsHttp.cpp.
*
* @see https://searchfox.org/mozilla-central/rev/445a6e86233c733c5557ef44e1d33444adaddefc/netwerk/protocol/http/nsHttp.cpp#169
*/
#isValidHttpToken(token) {
// prettier-ignore
// This array corresponds to all char codes between 0 and 127, which is the
// range of supported char codes for HTTP tokens. Within this range,
// accepted char codes are marked with a 1, forbidden char codes with a 0.
const validTokenMap = [
0, 0, 0, 0, 0, 0, 0, 0, // 0
0, 0, 0, 0, 0, 0, 0, 0, // 8
0, 0, 0, 0, 0, 0, 0, 0, // 16
0, 0, 0, 0, 0, 0, 0, 0, // 24

0, 1, 0, 1, 1, 1, 1, 1, // 32
0, 0, 1, 1, 0, 1, 1, 0, // 40
1, 1, 1, 1, 1, 1, 1, 1, // 48
1, 1, 0, 0, 0, 0, 0, 0, // 56

0, 1, 1, 1, 1, 1, 1, 1, // 64
1, 1, 1, 1, 1, 1, 1, 1, // 72
1, 1, 1, 1, 1, 1, 1, 1, // 80
1, 1, 1, 0, 0, 0, 1, 1, // 88

1, 1, 1, 1, 1, 1, 1, 1, // 96
1, 1, 1, 1, 1, 1, 1, 1, // 104
1, 1, 1, 1, 1, 1, 1, 1, // 112
1, 1, 1, 0, 1, 0, 1, 0 // 120
];

if (!token.length) {
return false;
}
return token
.split("")
.map(s => s.charCodeAt(0))
.every(c => validTokenMap[c]);
}

#onAuthRequired = (name, data) => {
const { authCallbacks, request, response } = data;

Expand Down Expand Up @@ -1625,6 +1739,12 @@ class NetworkModule extends Module {
return params;
}

#serializeCookieHeader(cookieHeader) {
const name = cookieHeader.name;
const value = deserializeBytesValue(cookieHeader.value);
return `${name}=${value}`;
}

#serializeHeader(name, value) {
return {
name,
Expand Down Expand Up @@ -1719,4 +1839,23 @@ class NetworkModule extends Module {
}
}

/**
* Deserialize a network BytesValue.
*
* @param {BytesValue} bytesValue
* The BytesValue to deserialize.
* @returns {string}
* The deserialized value.
*/
export function deserializeBytesValue(bytesValue) {
const { type, value } = bytesValue;

if (type === BytesValueType.String) {
return value;
}

// For type === BytesValueType.Base64.
return atob(value);
}

export const network = NetworkModule;
22 changes: 5 additions & 17 deletions remote/webdriver-bidi/modules/root/storage.sys.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ ChromeUtils.defineESModuleGetters(lazy, {
assert: "chrome://remote/content/shared/webdriver/Assert.sys.mjs",
BytesValueType:
"chrome://remote/content/webdriver-bidi/modules/root/network.sys.mjs",
deserializeBytesValue:
"chrome://remote/content/webdriver-bidi/modules/root/network.sys.mjs",
error: "chrome://remote/content/shared/webdriver/Errors.sys.mjs",
TabManager: "chrome://remote/content/shared/TabManager.sys.mjs",
UserContextManager:
Expand Down Expand Up @@ -283,7 +285,8 @@ class StorageModule extends Module {
// The cookie store is defined by originAttributes.
const originAttributes = this.#getOriginAttributes(partitionKey);

const deserializedValue = this.#deserializeProtocolBytes(value);
// The cookie value is a network.BytesValue.
const deserializedValue = lazy.deserializeBytesValue(value);

// The XPCOM interface requires to be specified if a cookie is session.
const isSession = expiry === null;
Expand Down Expand Up @@ -571,7 +574,7 @@ class StorageModule extends Module {
break;

case "value":
deserializedValue = this.#deserializeProtocolBytes(value);
deserializedValue = lazy.deserializeBytesValue(value);
break;

default:
Expand All @@ -584,21 +587,6 @@ class StorageModule extends Module {
return deserializedFilter;
}

/**
* Deserialize the value to string, since platform API
* returns cookie's value as a string.
*/
#deserializeProtocolBytes(cookieValue) {
const { type, value } = cookieValue;

if (type === lazy.BytesValueType.String) {
return value;
}

// For type === BytesValueType.Base64.
return atob(value);
}

/**
* Build a partition key.
*
Expand Down

0 comments on commit 6ec011d

Please sign in to comment.