mirror of
https://github.com/mozilla/gecko-dev.git
synced 2024-11-24 13:21:05 +00:00
24fba2b89e
This change introduces more data validation checks on unmarshaling Marionette protocol messages. Specifically, validation of message.Command's and message.Response's constructor arguments and packet contents in their respective fromMsg functions are tested. Doing these tests ensures more safety further down the pipeline with respect to the data integrity in Marionette commands. MozReview-Commit-ID: BxYipX5zfo9 --HG-- extra : rebase_source : 5cd9edab8801323b19688f871ba78ccf70a05c5e
298 lines
7.8 KiB
JavaScript
298 lines
7.8 KiB
JavaScript
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
|
* You can obtain one at http://mozilla.org/MPL/2.0/. */
|
|
|
|
"use strict";
|
|
|
|
var {utils: Cu} = Components;
|
|
|
|
Cu.import("resource://gre/modules/Log.jsm");
|
|
Cu.import("resource://gre/modules/Task.jsm");
|
|
|
|
Cu.import("chrome://marionette/content/assert.js");
|
|
Cu.import("chrome://marionette/content/error.js");
|
|
|
|
this.EXPORTED_SYMBOLS = [
|
|
"Command",
|
|
"Message",
|
|
"MessageOrigin",
|
|
"Response",
|
|
];
|
|
|
|
const logger = Log.repository.getLogger("Marionette");
|
|
|
|
this.MessageOrigin = {
|
|
Client: 0,
|
|
Server: 1,
|
|
};
|
|
|
|
this.Message = {};
|
|
|
|
/**
|
|
* Converts a data packet into a Command or Response type.
|
|
*
|
|
* @param {Array.<number, number, ?, ?>} data
|
|
* A four element array where the elements, in sequence, signifies
|
|
* message type, message ID, method name or error, and parameters
|
|
* or result.
|
|
*
|
|
* @return {(Command,Response)}
|
|
* Based on the message type, a Command or Response instance.
|
|
*
|
|
* @throws {TypeError}
|
|
* If the message type is not recognised.
|
|
*/
|
|
Message.fromMsg = function (data) {
|
|
switch (data[0]) {
|
|
case Command.TYPE:
|
|
return Command.fromMsg(data);
|
|
|
|
case Response.TYPE:
|
|
return Response.fromMsg(data);
|
|
|
|
default:
|
|
throw new TypeError(
|
|
"Unrecognised message type in packet: " + JSON.stringify(data));
|
|
}
|
|
};
|
|
|
|
/**
|
|
* A command is a request from the client to run a series of remote end
|
|
* steps and return a fitting response.
|
|
*
|
|
* The command can be synthesised from the message passed over the
|
|
* Marionette socket using the {@code fromMsg} function. The format of
|
|
* a message is:
|
|
*
|
|
* [type, id, name, params]
|
|
*
|
|
* where
|
|
*
|
|
* type (integer)
|
|
* Must be zero (integer). Zero means that this message is a command.
|
|
*
|
|
* id (integer)
|
|
* Integer used as a sequence number. The server replies with the
|
|
* same ID for the response.
|
|
*
|
|
* name (string)
|
|
* String representing the command name with an associated set of
|
|
* remote end steps.
|
|
*
|
|
* params (JSON Object or null)
|
|
* Object of command function arguments. The keys of this object
|
|
* must be strings, but the values can be arbitrary values.
|
|
*
|
|
* A command has an associated message {@code id} that prevents the
|
|
* dispatcher from sending responses in the wrong order.
|
|
*
|
|
* The command may also have optional error- and result handlers that
|
|
* are called when the client returns with a response. These are
|
|
* {@code function onerror({Object})}, {@code function onresult({Object})},
|
|
* and {@code function onresult({Response})}.
|
|
*
|
|
* @param {number} msgId
|
|
* Message ID unique identifying this message.
|
|
* @param {string} name
|
|
* Command name.
|
|
* @param {Object<string, ?>} params
|
|
* Command parameters.
|
|
*/
|
|
this.Command = class {
|
|
constructor(msgID, name, params = {}) {
|
|
this.id = assert.integer(msgID);
|
|
this.name = assert.string(name);
|
|
this.parameters = assert.object(params);
|
|
|
|
this.onerror = null;
|
|
this.onresult = null;
|
|
|
|
this.origin = MessageOrigin.Client;
|
|
this.sent = false;
|
|
}
|
|
|
|
/**
|
|
* Calls the error- or result handler associated with this command.
|
|
* This function can be replaced with a custom response handler.
|
|
*
|
|
* @param {Response} resp
|
|
* The response to pass on to the result or error to the
|
|
* {@code onerror} or {@code onresult} handlers to.
|
|
*/
|
|
onresponse(resp) {
|
|
if (this.onerror && resp.error) {
|
|
this.onerror(resp.error);
|
|
} else if (this.onresult && resp.body) {
|
|
this.onresult(resp.body);
|
|
}
|
|
}
|
|
|
|
toMsg() {
|
|
return [Command.TYPE, this.id, this.name, this.parameters];
|
|
}
|
|
|
|
toString() {
|
|
return "Command {id: " + this.id + ", " +
|
|
"name: " + JSON.stringify(this.name) + ", " +
|
|
"parameters: " + JSON.stringify(this.parameters) + "}";
|
|
}
|
|
|
|
static fromMsg(msg) {
|
|
let [type, msgID, name, params] = msg;
|
|
assert.that(n => n === Command.TYPE)(type);
|
|
|
|
// if parameters are given but null, treat them as undefined
|
|
if (params === null) {
|
|
params = undefined;
|
|
}
|
|
|
|
return new Command(msgID, name, params);
|
|
}
|
|
};
|
|
|
|
Command.TYPE = 0;
|
|
|
|
|
|
const validator = {
|
|
exclusionary: {
|
|
"capabilities": ["error", "value"],
|
|
"error": ["value", "sessionId", "capabilities"],
|
|
"sessionId": ["error", "value"],
|
|
"value": ["error", "sessionId", "capabilities"],
|
|
},
|
|
|
|
set: function (obj, prop, val) {
|
|
let tests = this.exclusionary[prop];
|
|
if (tests) {
|
|
for (let t of tests) {
|
|
if (obj.hasOwnProperty(t)) {
|
|
throw new TypeError(`${t} set, cannot set ${prop}`);
|
|
}
|
|
}
|
|
}
|
|
|
|
obj[prop] = val;
|
|
return true;
|
|
},
|
|
};
|
|
|
|
/**
|
|
* The response body is exposed as an argument to commands.
|
|
* Commands can set fields on the body through defining properties.
|
|
*
|
|
* Setting properties invokes a validator that performs tests for
|
|
* mutually exclusionary fields on the input against the existing data
|
|
* in the body.
|
|
*
|
|
* For example setting the {@code error} property on the body when
|
|
* {@code value}, {@code sessionId}, or {@code capabilities} have been
|
|
* set previously will cause an error.
|
|
*/
|
|
this.ResponseBody = () => new Proxy({}, validator);
|
|
|
|
/**
|
|
* Represents the response returned from the remote end after execution
|
|
* of its corresponding command.
|
|
*
|
|
* The response is a mutable object passed to each command for
|
|
* modification through the available setters. To send data in a response,
|
|
* you modify the body property on the response. The body property can
|
|
* also be replaced completely.
|
|
*
|
|
* The response is sent implicitly by CommandProcessor when a command
|
|
* has finished executing, and any modifications made subsequent to that
|
|
* will have no effect.
|
|
*
|
|
* @param {number} msgID
|
|
* Message ID tied to the corresponding command request this is a
|
|
* response for.
|
|
* @param {function(Response|Message)} respHandler
|
|
* Function callback called on sending the response.
|
|
*/
|
|
this.Response = class {
|
|
constructor(msgID, respHandler = () => {}) {
|
|
this.id = assert.integer(msgID);
|
|
this.respHandler_ = assert.callable(respHandler);
|
|
|
|
this.error = null;
|
|
this.body = ResponseBody();
|
|
|
|
this.origin = MessageOrigin.Server;
|
|
this.sent = false;
|
|
}
|
|
|
|
/**
|
|
* Sends response conditionally, given a predicate.
|
|
*
|
|
* @param {function(Response): boolean} predicate
|
|
* A predicate taking a Response object and returning a boolean.
|
|
*/
|
|
sendConditionally(predicate) {
|
|
if (predicate(this)) {
|
|
this.send();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Sends response using the response handler provided on construction.
|
|
*
|
|
* @throws {RangeError}
|
|
* If the response has already been sent.
|
|
*/
|
|
send() {
|
|
if (this.sent) {
|
|
throw new RangeError("Response has already been sent: " + this);
|
|
}
|
|
this.respHandler_(this);
|
|
this.sent = true;
|
|
}
|
|
|
|
/**
|
|
* Send given Error to client.
|
|
*
|
|
* Turns the response into an error response, clears any previously
|
|
* set body data, and sends it using the response handler provided
|
|
* on construction.
|
|
*
|
|
* @param {Error} err
|
|
* The Error instance to send.
|
|
*
|
|
* @throws {Error}
|
|
* If the {@code error} is not a WebDriverError, the error is
|
|
* propagated.
|
|
*/
|
|
sendError(err) {
|
|
this.error = error.wrap(err).toJSON();
|
|
this.body = null;
|
|
this.send();
|
|
|
|
// propagate errors which are implementation problems
|
|
if (!error.isWebDriverError(err)) {
|
|
throw err;
|
|
}
|
|
}
|
|
|
|
toMsg() {
|
|
return [Response.TYPE, this.id, this.error, this.body];
|
|
}
|
|
|
|
toString() {
|
|
return "Response {id: " + this.id + ", " +
|
|
"error: " + JSON.stringify(this.error) + ", " +
|
|
"body: " + JSON.stringify(this.body) + "}";
|
|
}
|
|
|
|
static fromMsg(msg) {
|
|
let [type, msgID, err, body] = msg;
|
|
assert.that(n => n === Response.TYPE)(type);
|
|
|
|
let resp = new Response(msgID);
|
|
resp.error = assert.string(err);
|
|
|
|
resp.body = body;
|
|
return resp;
|
|
}
|
|
};
|
|
|
|
Response.TYPE = 1;
|