session.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458
  1. /* This Source Code Form is subject to the terms of the Mozilla Public
  2. * License, v. 2.0. If a copy of the MPL was not distributed with this file,
  3. * You can obtain one at http://mozilla.org/MPL/2.0/. */
  4. "use strict";
  5. const {interfaces: Ci, utils: Cu} = Components;
  6. Cu.import("resource://gre/modules/Log.jsm");
  7. Cu.import("resource://gre/modules/Preferences.jsm");
  8. Cu.import("resource://gre/modules/Services.jsm");
  9. Cu.import("chrome://marionette/content/assert.js");
  10. Cu.import("chrome://marionette/content/error.js");
  11. this.EXPORTED_SYMBOLS = ["session"];
  12. const logger = Log.repository.getLogger("Marionette");
  13. const {pprint} = error;
  14. // Enable testing this module, as Services.appinfo.* is not available
  15. // in xpcshell tests.
  16. const appinfo = {name: "<missing>", version: "<missing>"};
  17. try { appinfo.name = Services.appinfo.name.toLowerCase(); } catch (e) {}
  18. try { appinfo.version = Services.appinfo.version; } catch (e) {}
  19. /** State associated with a WebDriver session. */
  20. this.session = {};
  21. /** Representation of WebDriver session timeouts. */
  22. session.Timeouts = class {
  23. constructor () {
  24. // disabled
  25. this.implicit = 0;
  26. // five mintues
  27. this.pageLoad = 300000;
  28. // 30 seconds
  29. this.script = 30000;
  30. }
  31. toString () { return "[object session.Timeouts]"; }
  32. toJSON () {
  33. return {
  34. "implicit": this.implicit,
  35. "page load": this.pageLoad,
  36. "script": this.script,
  37. };
  38. }
  39. static fromJSON (json) {
  40. assert.object(json);
  41. let t = new session.Timeouts();
  42. for (let [typ, ms] of Object.entries(json)) {
  43. assert.positiveInteger(ms);
  44. switch (typ) {
  45. case "implicit":
  46. t.implicit = ms;
  47. break;
  48. case "script":
  49. t.script = ms;
  50. break;
  51. case "page load":
  52. t.pageLoad = ms;
  53. break;
  54. default:
  55. throw new InvalidArgumentError();
  56. }
  57. }
  58. return t;
  59. }
  60. };
  61. /** Enum of page loading strategies. */
  62. session.PageLoadStrategy = {
  63. None: "none",
  64. Eager: "eager",
  65. Normal: "normal",
  66. };
  67. /** Proxy configuration object representation. */
  68. session.Proxy = class {
  69. constructor() {
  70. this.proxyType = null;
  71. this.httpProxy = null;
  72. this.httpProxyPort = null;
  73. this.sslProxy = null;
  74. this.sslProxyPort = null;
  75. this.ftpProxy = null;
  76. this.ftpProxyPort = null;
  77. this.socksProxy = null;
  78. this.socksProxyPort = null;
  79. this.socksVersion = null;
  80. this.proxyAutoconfigUrl = null;
  81. }
  82. /**
  83. * Sets Firefox proxy settings.
  84. *
  85. * @return {boolean}
  86. * True if proxy settings were updated as a result of calling this
  87. * function, or false indicating that this function acted as
  88. * a no-op.
  89. */
  90. init() {
  91. switch (this.proxyType) {
  92. case "manual":
  93. Preferences.set("network.proxy.type", 1);
  94. if (this.httpProxy && this.httpProxyPort) {
  95. Preferences.set("network.proxy.http", this.httpProxy);
  96. Preferences.set("network.proxy.http_port", this.httpProxyPort);
  97. }
  98. if (this.sslProxy && this.sslProxyPort) {
  99. Preferences.set("network.proxy.ssl", this.sslProxy);
  100. Preferences.set("network.proxy.ssl_port", this.sslProxyPort);
  101. }
  102. if (this.ftpProxy && this.ftpProxyPort) {
  103. Preferences.set("network.proxy.ftp", this.ftpProxy);
  104. Preferences.set("network.proxy.ftp_port", this.ftpProxyPort);
  105. }
  106. if (this.socksProxy) {
  107. Preferences.set("network.proxy.socks", this.socksProxy);
  108. Preferences.set("network.proxy.socks_port", this.socksProxyPort);
  109. if (this.socksVersion) {
  110. Preferences.set("network.proxy.socks_version", this.socksVersion);
  111. }
  112. }
  113. return true;
  114. case "pac":
  115. Preferences.set("network.proxy.type", 2);
  116. Preferences.set("network.proxy.autoconfig_url", this.proxyAutoconfigUrl);
  117. return true;
  118. case "autodetect":
  119. Preferences.set("network.proxy.type", 4);
  120. return true;
  121. case "system":
  122. Preferences.set("network.proxy.type", 5);
  123. return true;
  124. case "noproxy":
  125. Preferences.set("network.proxy.type", 0);
  126. return true;
  127. default:
  128. return false;
  129. }
  130. }
  131. toString () { return "[object session.Proxy]"; }
  132. toJSON () {
  133. return marshal({
  134. proxyType: this.proxyType,
  135. httpProxy: this.httpProxy,
  136. httpProxyPort: this.httpProxyPort ,
  137. sslProxy: this.sslProxy,
  138. sslProxyPort: this.sslProxyPort,
  139. ftpProxy: this.ftpProxy,
  140. ftpProxyPort: this.ftpProxyPort,
  141. socksProxy: this.socksProxy,
  142. socksProxyPort: this.socksProxyPort,
  143. socksProxyVersion: this.socksProxyVersion,
  144. proxyAutoconfigUrl: this.proxyAutoconfigUrl,
  145. });
  146. }
  147. static fromJSON (json) {
  148. let p = new session.Proxy();
  149. if (typeof json == "undefined" || json === null) {
  150. return p;
  151. }
  152. assert.object(json);
  153. assert.in("proxyType", json);
  154. p.proxyType = json.proxyType;
  155. if (json.proxyType == "manual") {
  156. if (typeof json.httpProxy != "undefined") {
  157. p.httpProxy = assert.string(json.httpProxy);
  158. p.httpProxyPort = assert.positiveInteger(json.httpProxyPort);
  159. }
  160. if (typeof json.sslProxy != "undefined") {
  161. p.sslProxy = assert.string(json.sslProxy);
  162. p.sslProxyPort = assert.positiveInteger(json.sslProxyPort);
  163. }
  164. if (typeof json.ftpProxy != "undefined") {
  165. p.ftpProxy = assert.string(json.ftpProxy);
  166. p.ftpProxyPort = assert.positiveInteger(json.ftpProxyPort);
  167. }
  168. if (typeof json.socksProxy != "undefined") {
  169. p.socksProxy = assert.string(json.socksProxy);
  170. p.socksProxyPort = assert.positiveInteger(json.socksProxyPort);
  171. p.socksProxyVersion = assert.positiveInteger(json.socksProxyVersion);
  172. }
  173. }
  174. if (typeof json.proxyAutoconfigUrl != "undefined") {
  175. p.proxyAutoconfigUrl = assert.string(json.proxyAutoconfigUrl);
  176. }
  177. return p;
  178. }
  179. };
  180. /** WebDriver session capabilities representation. */
  181. session.Capabilities = class extends Map {
  182. constructor () {
  183. super([
  184. // webdriver
  185. ["browserName", appinfo.name],
  186. ["browserVersion", appinfo.version],
  187. ["platformName", Services.sysinfo.getProperty("name").toLowerCase()],
  188. ["platformVersion", Services.sysinfo.getProperty("version")],
  189. ["pageLoadStrategy", session.PageLoadStrategy.Normal],
  190. ["acceptInsecureCerts", false],
  191. ["timeouts", new session.Timeouts()],
  192. ["proxy", new session.Proxy()],
  193. // features
  194. ["rotatable", appinfo.name == "B2G"],
  195. // proprietary
  196. ["specificationLevel", 0],
  197. ["moz:processID", Services.appinfo.processID],
  198. ["moz:profile", maybeProfile()],
  199. ["moz:accessibilityChecks", false],
  200. ]);
  201. }
  202. set (key, value) {
  203. if (key === "timeouts" && !(value instanceof session.Timeouts)) {
  204. throw new TypeError();
  205. } else if (key === "proxy" && !(value instanceof session.Proxy)) {
  206. throw new TypeError();
  207. }
  208. return super.set(key, value);
  209. }
  210. toString() { return "[object session.Capabilities]"; }
  211. toJSON() {
  212. return marshal(this);
  213. }
  214. /**
  215. * Unmarshal a JSON object representation of WebDriver capabilities.
  216. *
  217. * @param {Object.<string, ?>=} json
  218. * WebDriver capabilities.
  219. * @param {boolean=} merge
  220. * If providing |json| with |desiredCapabilities| or
  221. * |requiredCapabilities| fields, or both, it should be set to
  222. * true to merge these before parsing. This indicates
  223. * that the input provided is from a client and not from
  224. * |session.Capabilities#toJSON|.
  225. *
  226. * @return {session.Capabilities}
  227. * Internal representation of WebDriver capabilities.
  228. */
  229. static fromJSON (json, {merge = false} = {}) {
  230. if (typeof json == "undefined" || json === null) {
  231. json = {};
  232. }
  233. assert.object(json);
  234. if (merge) {
  235. json = session.Capabilities.merge_(json);
  236. }
  237. return session.Capabilities.match_(json);
  238. }
  239. // Processes capabilities as described by WebDriver.
  240. static merge_ (json) {
  241. for (let entry of [json.desiredCapabilities, json.requiredCapabilities]) {
  242. if (typeof entry == "undefined" || entry === null) {
  243. continue;
  244. }
  245. assert.object(entry, error.pprint`Expected ${entry} to be a capabilities object`);
  246. }
  247. let desired = json.desiredCapabilities || {};
  248. let required = json.requiredCapabilities || {};
  249. // One level deep union merge of desired- and required capabilities
  250. // with preference on required
  251. return Object.assign({}, desired, required);
  252. }
  253. // Matches capabilities as described by WebDriver.
  254. static match_ (caps = {}) {
  255. let matched = new session.Capabilities();
  256. const defined = v => typeof v != "undefined" && v !== null;
  257. const wildcard = v => v === "*";
  258. // Iff |actual| provides some value, or is a wildcard or an exact
  259. // match of |expected|. This means it can be null or undefined,
  260. // or "*", or "firefox".
  261. function stringMatch (actual, expected) {
  262. return !defined(actual) || (wildcard(actual) || actual === expected);
  263. }
  264. for (let [k,v] of Object.entries(caps)) {
  265. switch (k) {
  266. case "browserName":
  267. let bname = matched.get("browserName");
  268. if (!stringMatch(v, bname)) {
  269. throw new TypeError(
  270. pprint`Given browserName ${v}, but my name is ${bname}`);
  271. }
  272. break;
  273. // TODO(ato): bug 1326397
  274. case "browserVersion":
  275. let bversion = matched.get("browserVersion");
  276. if (!stringMatch(v, bversion)) {
  277. throw new TypeError(
  278. pprint`Given browserVersion ${v}, ` +
  279. pprint`but current version is ${bversion}`);
  280. }
  281. break;
  282. case "platformName":
  283. let pname = matched.get("platformName");
  284. if (!stringMatch(v, pname)) {
  285. throw new TypeError(
  286. pprint`Given platformName ${v}, ` +
  287. pprint`but current platform is ${pname}`);
  288. }
  289. break;
  290. // TODO(ato): bug 1326397
  291. case "platformVersion":
  292. let pversion = matched.get("platformVersion");
  293. if (!stringMatch(v, pversion)) {
  294. throw new TypeError(
  295. pprint`Given platformVersion ${v}, ` +
  296. pprint`but current platform version is ${pversion}`);
  297. }
  298. break;
  299. case "acceptInsecureCerts":
  300. assert.boolean(v);
  301. matched.set("acceptInsecureCerts", v);
  302. break;
  303. case "pageLoadStrategy":
  304. if (Object.values(session.PageLoadStrategy).includes(v)) {
  305. matched.set("pageLoadStrategy", v);
  306. } else {
  307. throw new TypeError("Unknown page load strategy: " + v);
  308. }
  309. break;
  310. case "proxy":
  311. let proxy = session.Proxy.fromJSON(v);
  312. matched.set("proxy", proxy);
  313. break;
  314. case "timeouts":
  315. let timeouts = session.Timeouts.fromJSON(v);
  316. matched.set("timeouts", timeouts);
  317. break;
  318. case "specificationLevel":
  319. assert.positiveInteger(v);
  320. matched.set("specificationLevel", v);
  321. break;
  322. case "moz:accessibilityChecks":
  323. assert.boolean(v);
  324. matched.set("moz:accessibilityChecks", v);
  325. break;
  326. }
  327. }
  328. return matched;
  329. }
  330. };
  331. // Specialisation of |JSON.stringify| that produces JSON-safe object
  332. // literals, dropping empty objects and entries which values are undefined
  333. // or null. Objects are allowed to produce their own JSON representations
  334. // by implementing a |toJSON| function.
  335. function marshal(obj) {
  336. let rv = Object.create(null);
  337. function* iter(mapOrObject) {
  338. if (mapOrObject instanceof Map) {
  339. for (const [k,v] of mapOrObject) {
  340. yield [k,v];
  341. }
  342. } else {
  343. for (const k of Object.keys(mapOrObject)) {
  344. yield [k, mapOrObject[k]];
  345. }
  346. }
  347. }
  348. for (let [k,v] of iter(obj)) {
  349. // Skip empty values when serialising to JSON.
  350. if (typeof v == "undefined" || v === null) {
  351. continue;
  352. }
  353. // Recursively marshal objects that are able to produce their own
  354. // JSON representation.
  355. if (typeof v.toJSON == "function") {
  356. v = marshal(v.toJSON());
  357. }
  358. // Or do the same for object literals.
  359. else if (isObject(v)) {
  360. v = marshal(v);
  361. }
  362. // And finally drop (possibly marshaled) objects which have no
  363. // entries.
  364. if (!isObjectEmpty(v)) {
  365. rv[k] = v;
  366. }
  367. }
  368. return rv;
  369. }
  370. function isObject(obj) {
  371. return Object.prototype.toString.call(obj) == "[object Object]";
  372. }
  373. function isObjectEmpty(obj) {
  374. return isObject(obj) && Object.keys(obj).length === 0;
  375. }
  376. // Services.dirsvc is not accessible from content frame scripts,
  377. // but we should not panic about that.
  378. function maybeProfile() {
  379. try {
  380. return Services.dirsvc.get("ProfD", Ci.nsIFile).path;
  381. } catch (e) {
  382. return "<protected>";
  383. }
  384. }