Welcome to mirror list, hosted at ThFree Co, Russian Federation.

github.com/mozilla/geckodriver.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAndreas Tolfsen <ato@mozilla.com>2017-06-09 17:17:59 +0300
committerAndreas Tolfsen <ato@mozilla.com>2017-06-09 17:17:59 +0300
commitfbc7c9baebde02dc332c9dbc442350e73cd7b343 (patch)
tree4a18f3b733965a385614b5fa4ed54c1a0b94b82c
parent1347bd88d81ee9f8537bc70757c399424c41e037 (diff)
import from eca8d0ea03af
-rw-r--r--.gitignore2
-rw-r--r--.travis.yml89
-rw-r--r--CHANGES.md345
-rw-r--r--CONTRIBUTING.md97
-rw-r--r--Cargo.lock770
-rw-r--r--Cargo.toml34
-rw-r--r--ISSUE_TEMPLATE.md16
-rw-r--r--LICENSE373
-rw-r--r--README.md535
-rw-r--r--build.sh169
-rw-r--r--ci.sh21
-rw-r--r--i686-trusty/Dockerfile4
-rw-r--r--moz.build8
-rw-r--r--src/capabilities.rs515
-rw-r--r--src/logging.rs158
-rw-r--r--src/main.rs177
-rw-r--r--src/marionette.rs1645
-rw-r--r--src/prefs.rs229
-rw-r--r--src/tests/profile.zipbin0 -> 444 bytes
19 files changed, 5187 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..ddfca73
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,2 @@
+/target
+.version
diff --git a/.travis.yml b/.travis.yml
new file mode 100644
index 0000000..412c553
--- /dev/null
+++ b/.travis.yml
@@ -0,0 +1,89 @@
+language: generic
+cache:
+ directories:
+ - $HOME/.cargo
+ - $TRAVIS_BUILD_DIR/target
+ - $HOME/docker/
+
+before_install:
+ - if [[ "$NAME" == "linux32" ]]; then
+ sudo dpkg --purge build-essential libtool;
+ sudo apt-get -qq update;
+ sudo apt-get -yq --force-yes install libstdc++6:i386 libbz2-dev:i386 gcc-multilib:i386 musl-tools:i386; fi
+
+matrix:
+ include:
+ - os: linux
+ env:
+ - TARGET=armv7-unknown-linux-gnueabihf
+ - NAME=arm7hf
+ - EXT=tar.gz
+ addons:
+ apt:
+ packages: &armhf
+ - gcc-arm-linux-gnueabihf
+ - libc6-armhf-cross
+ - libc6-dev-armhf-cross
+ - os: linux
+ env:
+ - TARGET=x86_64-unknown-linux-musl
+ - NAME=linux64
+ - EXT=tar.gz
+ dist: trusty
+ sudo: required
+ addons:
+ apt:
+ packages:
+ - musl-tools
+ - libbz2-dev
+ - os: linux
+ env:
+ - TARGET=i686-unknown-linux-musl
+ - NAME=linux32
+ - EXT=tar.gz
+ dist: trusty
+ sudo: required
+ - os: linux
+ env:
+ - TARGET=x86_64-pc-windows-gnu
+ - NAME=win64
+ - EXT=zip
+ dist: trusty
+ sudo: required
+ addons:
+ apt:
+ packages:
+ - gcc-mingw-w64-x86-64
+ - binutils-mingw-w64-x86-64
+ - libbz2-dev
+ - os: linux
+ env:
+ - TARGET=i686-pc-windows-gnu
+ - NAME=win32
+ - EXT=zip
+ - TOOLCHAIN=nightly
+ - DOCKER_IMAGE=i686-trusty
+ dist: trusty
+ services:
+ - docker
+ - os: osx
+ env:
+ - TARGET=x86_64-apple-darwin
+ - NAME=macos
+ - EXT=tar.gz
+
+script:
+ - bash ci.sh
+
+deploy:
+ provider: releases
+ api_key:
+ secure:
+ OY6y0xRhlGSBH+3+7+7K6s4pp0Tf3BA61NAubEK4gpO23AKfyJUK+vqqCbdo06Z0E4QO4O1ke6fot7Gq9EdxJmsl/Kk5LDd9Rv3BXSYdjPupzs7coYuD8wv10NfAX6ETd9ITyPim2Zq6bq8nx1S2ESTpos/js1HCtVW3nrlCV58=
+ skip_cleanup: true
+ file:
+ - "geckodriver-$TRAVIS_TAG-$NAME.$EXT"
+ on:
+ tags: true
+ repo: mozilla/geckodriver
+ condition: -n "$NAME"
diff --git a/CHANGES.md b/CHANGES.md
new file mode 100644
index 0000000..9e5654b
--- /dev/null
+++ b/CHANGES.md
@@ -0,0 +1,345 @@
+# Change log
+
+All notable changes to this program is documented in this file.
+
+## 0.17.0 (2017-06-09)
+
+### Added
+- Added endpoints:
+ - POST `/session/{session id}/window/fullscreen` to invoke the window manager-specific `full screen` operation
+ - POST `/session/{session id}/moz/addon/install` to install an extension [Gecko only]
+ - POST `/session/{session id}/moz/addon/uninstall` to uninstall an extension [Gecko only]
+
+### Changed
+- Increasing the length of the `network.http.phishy-userpass-length` preference will cause Firefox to not prompt when navigating to a website with a username or password in the URL
+- Library dependencies upgraded to mozrunner 0.4 and mozprofile 0.3 to allow overriding of preferences via capabilities if those have been already set in the profile
+- Library dependencies upgraded to mozversion 0.1.2 to only use the normalized path of the Firefox binary for version checks but not to actually start the browser, which broke several components in Firefox on Windows
+
+### Fixed
+- The SetWindowRect command now returns the WindowRect when it is done
+- Use ASCII versions of array symbols to properly display them in the Windows command prompt
+- Use [`SessionNotCreated`](https://docs.rs/webdriver/0.25.0/webdriver/error/enum.ErrorStatus.html#variant.SessionNotCreated) error instead of [`UnknownError`](https://docs.rs/webdriver/0.25.0/webdriver/error/enum.ErrorStatus.html#variant.UnknownError) if there is no current session
+
+## 0.16.1 (2017-04-26)
+
+### Fixed
+- Read Firefox version number from stdout when failing to look for the application .ini file (fixes [Selenium #3884](https://github.com/SeleniumHQ/selenium/issues/3884))
+- Session is now ended when closing the last Firefox window (fixes [#613](https://github.com/mozilla/geckodriver/issues/613))
+
+## 0.16.0 (2017-04-21)
+
+Note that geckodriver v0.16.0 is only compatible with Selenium 3.4 and greater.
+
+### Added
+- Support for WebDriver-conforming [New Session](https://w3c.github.io/webdriver/webdriver-spec.html#dfn-new-session) negotiation, with `desiredCapabilities`/`requiredCapabilities` negotiation as fallback
+- Added two new endpoints:
+ - GET `/session/{session id}/window/rect` for [Get Window Rect](https://w3c.github.io/webdriver/webdriver-spec.html#get-window-rect)
+ - POST `/session/{session id}/window/rect` for [Set Window Rect](https://w3c.github.io/webdriver/webdriver-spec.html#set-window-rect)
+- Align errors with the [WebDriver errors](https://w3c.github.io/webdriver/webdriver-spec.html#handling-errors):
+ - Introduces new errors [`ElementClickIntercepted`](https://docs.rs/webdriver/0.25.0/webdriver/error/enum.ErrorStatus.html#variant.ElementClickIntercepted), [`ElementNotInteractable`](https://docs.rs/webdriver/0.25.0/webdriver/error/enum.ErrorStatus.html#variant.ElementNotInteractable), [`InvalidCoordinates`](https://docs.rs/webdriver/0.25.0/webdriver/error/enum.ErrorStatus.html#variant.InvalidCoordinates), [`NoSuchCookie`](https://docs.rs/webdriver/0.25.0/webdriver/error/enum.ErrorStatus.html#variant.NoSuchCookie), [`UnableToCaptureScreen`](https://docs.rs/webdriver/0.25.0/webdriver/error/enum.ErrorStatus.html#variant.UnableToCaptureScreen), and [`UnknownCommand`](https://docs.rs/webdriver/0.25.0/webdriver/error/enum.ErrorStatus.html#variant.UnknownCommand)
+ - Removes `ElementNotVisible` and `InvalidElementCoordinates` errors
+
+### Removed
+- Removed following list of unused endpoints:
+ - GET `/session/{session id}/alert_text`
+ - POST `/session/{session id}/alert_text`
+ - POST `/session/{session id}/accept_alert`
+ - POST `/session/{session id}/dismiss_alert`
+ - GET `/session/{session id}/window_handle`
+ - DELETE `/session/{session id}/window_handle`
+ - POST `/session/{session id}/execute_async`
+ - POST `/session/{session id}/execute`
+
+### Changed
+- [`SendKeysParameters`](https://docs.rs/webdriver/0.25.0/webdriver/command/struct.SendKeysParameters.html), which is used for the [Element Send Keys](https://w3c.github.io/webdriver/webdriver-spec.html#element-send-keys) and [Send Alert Text](https://w3c.github.io/webdriver/webdriver-spec.html#send-alert-text) commands, has been updated to take a string `text` field
+- [`CookieResponse`](https://docs.rs/webdriver/0.25.0/webdriver/response/struct.CookieResponse.html) and [`CloseWindowResponse`](https://docs.rs/webdriver/0.25.0/webdriver/response/struct.CloseWindowResponse.html) fixed to be properly wrapped in a `value` field, like other responses
+- Allow negative numbers for `x` and `y` fields in `pointerMove` action
+- Disable Flash and the plugin container in Firefox by default, which should help mitigate the “Plugin Container for Firefox has stopped wroking” problems [many users were reporting](https://github.com/mozilla/geckodriver/issues/225) when deleting a session
+- Preferences passed in a profile now take precedence over set of default preferences defined by geckodriver (fixed by [@DrMarcII](https://github.com/DrMarcII))
+ - The exceptions are the `marionette.port` and `marionette.log.level` preferences and their fallbacks, which are set unconditionally and cannot be overriden
+- Remove default preference that disables unsafe CPOW checks
+- WebDriver library updated to 0.25.2
+
+### Fixed
+- Fix for the “corrupt deflate stream” exception that sometimes occured when trying to write an empty profile by [@kirhgoph](https://github.com/kirhgoph)
+- Recognise `sslProxy` and `sslProxyPort` entries in the proxy configuration object (fixed by [@juangj](https://github.com/juangj))
+- Fix “`httpProxyPort` was not an integer” error (fixed by [@juangj](https://github.com/juangj))
+- Fix broken unmarshaling of _Get Timeouts_ response format from Firefox 52 and earlier (fixed by [@juangj](https://github.com/juangj))
+- Allow preferences in `moz:firefoxOptions` to be both positive- and negative integers (fixed by [@juangj](https://github.com/juangj))
+- Allow IPv6 hostnames in the proxy configuration object
+- i686-unknown-linux-musl (Linux 32-bit) build fixed
+- Log messages from other Rust modules are now ignored
+- Improved log messages to the HTTPD
+
+## 0.15.0 (2017-03-08)
+
+### Added
+- Added routing and parsing for the [Get Timeouts](https://w3c.github.io/webdriver/webdriver-spec.html#dfn-get-timeouts) command
+
+### Changed
+- All HTTP responses are now wrapped in `{value: …}` objects per the WebDriver specification; this may likely require you to update your client library
+- Pointer move action’s `element` key changed to `origin`, which lets pointer actions originate within the context of the viewport, the pointer’s current position, or from an element
+- Now uses about:blank as the new tab document; this was previously disabled due to [bug 1333736](https://bugzilla.mozilla.org/show_bug.cgi?id=1333736) in Marionette
+- WebDriver libary updated to 0.23.0
+
+### Fixed
+- Aligned the data structure accepted by the [Set Timeouts](https://w3c.github.io/webdriver/webdriver-spec.html#set-timeouts) command with the WebDriver specification
+
+## 0.14.0 (2017-01-31)
+
+### Changed
+- Firefox process is now terminated and session ended when the last window is closed
+- WebDriver library updated to version 0.20.0
+
+### Fixed
+- Stacktraces are now included when the error originates from within the Rust stack
+- HTTPD now returns correct response headers for `Content-Type` and `Cache-Control` thanks to @jugglinmike
+
+## 0.13.0 (2017-01-06)
+
+### Changed
+- When navigating to a document with an insecure- or otherwise invalid TLS certificate, an [insecure certificate](https://w3c.github.io/webdriver/webdriver-spec.html#dfn-insecure-certificate) error will be returned
+- On macOS, deducing Firefox’ location on the system will look for _firefox-bin_ on the system path (`PATH` environmental variable) before looking in the applications folder
+- Window position coordinates are allowed to be negative numbers, to cater for maximised window positioning on Windows
+- WebDriver library updated to version 0.18.0
+
+### Fixed
+- Check for single-character key codes in action sequences now counts characters instead of bytes
+
+## 0.12.0 (2017-01-03)
+
+### Added
+- Added [_Take Element Screenshot_](https://w3c.github.io/webdriver/webdriver-spec.html#take-element-screenshot) command
+- Added new [_Status_](https://w3c.github.io/webdriver/webdriver-spec.html#status) command
+- Added routing for the [_Get Timeouts_](https://w3c.github.io/webdriver/webdriver-spec.html#get-timeouts) command, but it is not yet implemented in Marionette, and will return an _unsupported operation_ error until it is
+- Implemented routing for [new actions API](https://w3c.github.io/webdriver/webdriver-spec.html#actions), but it too is not yet fully implemented in Marionette
+
+### Changed
+- [Synced Firefox preferences](https://github.com/mozilla/geckodriver/commit/2bfdc3ec8151c427a6a75a6ba3ad203459540495) with those used in Mozilla automation
+- Default log level for debug builds of Firefox, which used to be `DEBUG`, changed to `INFO`-level
+- WebDriver library dependency upgraded to 0.17.1
+- Using _session not created_ error when failing to start session
+- geckodriver will exit with exit code 69 to indicate that the port is unavailable
+
+### Fixed
+- Improved logging when starting Firefox
+- Reverted to synchronous logging, which should address cases of inconsistent output when failing to bind to port
+- Clarified in README that geckodriver is not supported on Windows XP
+- Added documentation of supported capabilities to [README](https://github.com/mozilla/geckodriver/blob/master/README.md)
+- Included capabilities example in [README](https://github.com/mozilla/geckodriver/blob/master/README.md)
+
+## 0.11.1 (2016-10-10)
+
+### Fixed
+- Version number in binary now reflects the release version.
+
+## 0.11.0 (2016-10-10)
+
+### Added
+- Introduced continous integration builds for Linux- and Windows 32-bit binaries
+- Added commands for setting- and getting the window position
+- Added new extension commands for finding an element’s anonymous children and querying its attributes; accessible through the `/session/{sessionId}/moz/xbl/{elementId}/anonymous_children` to return all anonymous children and `/session/{sessionId}/moz/xbl/{elementId}/anonymous_by_attribute` to return an anonymous element by a name and attribute query
+- Introduced a `moz:firefoxOptions` capability to customise a Firefox session:
+ - The `binary`, `args`, and `profile` entries on this dictionary is equivalent to the old `firefox_binary`, `firefox_args`, and `firefox_profile` capabilities, which have now all been removed
+ - The `log` capability takes a dictionary such as `{log: "trace"}` to enable trace level verbosity in Gecko
+ - The `prefs` capability lets you define Firefox preferences through capabilities
+- Re-introduced the `--webdriver-port` argument as a hidden alias to `--port`
+
+### Changed
+- `firefox_binary`, `firefox_args`, and `firefox_profile` capabilities removed in favour of the `moz:firefoxOptions` dictionary detailed above and in the README
+- Removed `--no-e10s` flag, and geckodriver will from now rely on the Firefox default multiprocessing settings (override using preferences)
+- Disable pop-up blocker in the default profile by @juangj
+- Changed Rust compiler version to 1.12 (beta) temporarily because of [trouble linking Musl binaries](https://github.com/rust-lang/rust/issues/34978)
+- Replaced _env_logger_ logging facility with the _slog_ package, causing the `RUST_LOG` environment variable to no longer have any affect
+- Updated the WebDriver Rust library to version 0.15.
+
+### Fixed
+- Corrected link to repository in Cargo metadata
+- Verbosity shorthand flag `-v[v]` now works again, following the replacement of the argument parsing library in the previous release
+- When the HTTPD fails to start, errors are propagated to the user
+- Disabled the additional welcome URL (`startup.homepage_welcome_url.additional`) so that officially branded Firefox builds do not start with two open tabs in fresh profiles
+- Disabled homepage override URL redirection on milestone upgrades, which means a tab with an upgrade notice is not displayed when launching a new Firefox version
+
+## 0.10.0 (2016-08-02)
+
+### Changed
+- Use multi-process Firefox (e10s) by default, added flag `--no-e10s` to disable it and removed `--e10s` flag
+- Disable autofilling of forms by default by @mythsunwind
+- Replace _argparse_ with _clap_ for arguments parsing
+
+### Fixed
+- Attempt to deploy a single file from Travis when making a release
+- Grammar fix in README
+
+
+## 0.9.0 (2016-06-30)
+
+### Added
+- Add ability to use `firefox_binary` capability to define location of Firefox to use
+- Automatically detect the default Firefox path if one is not given
+- Cross-compile to Windows and ARMv7 (HF) in CI
+- Add Musl C library-backed static binaries in CI
+- Add `-v`, `-vv`, and `--log LEVEL` flags to increase Gecko verbosity
+- Add Get Element Property endpoint
+- Add new `--version` flag showing copying information and a link to the repository
+
+### Changed
+- Now connects to a Marionette on a random port by default
+- Update webdriver-rust library dependency
+- Migrated to use Travis to deploy new releases
+- Reduced amount of logging
+- Introduced a changelog (this)
+
+
+## 0.8.0 (2016-06-07)
+
+### Added
+- Allow specifying array of arguments to the Firefox binary through the `firefox_args` capability
+- Pass parameters with New Session command
+
+### Changed
+- Change product name to _geckodriver_
+- Make README more exhaustive
+- Quit Firefox when deleting a session
+- Update webdriver-rust library
+- Update dependencies
+
+### Fixed
+- Fix tests
+- FIx typo in error message for parsing errors
+
+
+## 0.7.1 (2016-04-27)
+
+### Added
+- Add command line flag for using e10s enabeld Firefox by @martionsideofthemoon
+- Allow providing custom profiels
+
+### Changed
+- Allow binding to an IPv6 address by @juangj
+- By default, connect to host-agnostic localhost by @juangj
+- Make `GeckoContextParameters` public
+- Update dependencies
+
+### Fixed
+- Squash rustc 1.6 warnings by using `std::thread::sleep(dur: Duration)`
+
+
+## 0.6.2 (2016-01-20)
+
+### Added
+- Add LICENSE file from @joshbruning
+- Schedule builds in CI on pushes and pull requests
+
+### Changed
+- Enable CPOWs in Marionette
+
+
+## 0.6.0 (2016-01-12)
+
+### Added
+- Add Get Page Source endpoint
+
+### Changed
+- Handle arrays being sent from Marionette
+- Correct build steps in README
+- Update what properties are read from errors sent by Marionette
+- Update dependencies
+
+
+## 0.5.0 (2015-12-10)
+
+### Changed
+- Update argparse dependency to use Cargo
+- Update to the latest version of the Marionette wire protocol
+- Update to latest webdriver-rust library
+- Update dependencies
+
+
+## 0.4.2 (2015-10-02)
+
+### Changed
+- Skip compiling optional items in hyper
+
+
+## 0.4.1 (2015-10-02)
+
+### Changed
+- Update webdriver-rust library
+- Update dependencies
+
+
+## 0.4.0 (2015-09-28)
+
+### Added
+- Add command extensions for switching between content- and chrome contexts
+- Add more documentation from @vladikoff
+
+### Changed
+- Update Cargo.lock with new dependencies for building
+- Update for protocol updates that flatten commands
+- Update to new protocol error handling
+- Update for Marionette protocol version 3 changes
+- Strip any leading and trailing `{}` from the `sessionId` Marionette returns
+- Update dependencies
+
+### Fixed
+- Fix `GetCSSValue` message to send correct key `propertyName`
+- Fix example in documentation from @vladikoff
+
+
+## 0.3.0 (2015-08-17)
+
+### Added
+- Add support for finding elements in subtrees
+
+
+## 0.2.0 (2015-05-20)
+
+### Added
+- Extra debug messages
+- Add ability to set WebDriver port
+- Add support for getting the active element
+- Add support for `GetCookies` and `DeleteCookie`/`DeleteCookies`
+- Add preferences that switch off certain features not required for WebDriver tests
+
+### Changed
+- Make failing to communicate with Firefox a fatal error that closes the session
+- Shut down session only when loosing connection
+- Better handling of missing command line flags
+- Poll for connection every 100ms rather than every 100s
+- Switch to string-based error codes
+- Switch webdriver-rust library dependency to be pulled from git
+- Update dependencies
+
+### Fixed
+- Handle null id for switching to frame more correctly
+
+
+## 0.1.0 (2015-04-09)
+
+### Added
+- Add proxy for converting WebDriver HTTP protocol to Marionette protocol
+- Add endpoints for modal dialogue support
+- Allow connecting to a running Firefox instance
+- Add explicit Cargo.lock file
+- Start Firefox when we get a New Session command
+- Add flag parsing and address parsing
+- Add basic error handling
+
+### Changed
+- Update for Rust beta
+- Switch to new IO libraries
+- Pin webdriver-rust commit so we can upgrade rustc versions independently
+- Set preferences when starting Firefox
+- Improve some error messages
+- Re-enable environment variable based logging
+
+### Fixed
+- Fix Get Element Rect command to return floats instead of integers
+- Fix passing of web elements to Switch To Frame command
+- Fix serialisation of script commands
+- Fix assorted bugs found by the Selenium test suite
+- Fix conversion of Find Element/Find Elements responses from Marionette to WebDriver
+- Fixed build by updating Cargo.lock with new dependencies for building
+- Squash compile warnings
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644
index 0000000..ca22acb
--- /dev/null
+++ b/CONTRIBUTING.md
@@ -0,0 +1,97 @@
+# Contributing to geckodriver
+
+The geckodriver project welcomes contributions from everyone. There are a
+number of ways you can help:
+
+## Issue Reports
+
+When opening new issues or commenting on existing issues please make
+sure discussions are related to concrete technical issues with the
+geckodriver or Marionette software.
+
+For issue reports to be actionable, it must be clear exactly what the
+observed and expected behaviours are, and how to set up the state required
+to observe the erroneous behaviour. The most useful thing to provide is a
+minimal HTML file which allows the problem to be reproduced, plus a
+trace-level log from geckodriver showing the wire-protocol calls used to set
+up the problem. Please provide [concise reproducible test
+cases](http://sscce.org/) and describe what results you are seeing and what
+results you expect. Because of the wide variety of client bindings for
+WebDriver, clients scripts and logs are typically not very useful if the
+verbose geckodriver logs are available. Issues relating to a specific client
+should be filed in the issue tracker of that project.
+
+## Code Contributions
+
+If you're looking for easy bugs, have a look at
+[issues labelled E-easy](https://github.com/mozilla/geckodriver/issues?utf8=%E2%9C%93&q=is%3Aopen+is%3Aissue+label%3Aeasy+)
+on Github.
+
+This document will guide you through the contribution process.
+
+### Step 1: Fork
+
+Fork the project [on Github](https://github.com/mozilla/geckodriver)
+and check out your copy locally.
+
+```text
+% git clone git@github.com:username/geckodriver.git
+% cd geckodriver
+% git remote add upstream git://github.com/mozilla/geckodriver.git
+```
+
+### Step 2: Branch
+
+Create a feature branch and start hacking:
+
+```text
+% git checkout -b my-feature-branch
+```
+
+We practice HEAD-based development, which means all changes are applied
+directly on top of master.
+
+### Step 3: Commit
+
+First make sure git knows your name and email address:
+
+```text
+% git config --global user.name 'Santa Claus'
+% git config --global user.email 'santa@example.com'
+```
+
+The first line must be meaningful as it's what people see when they
+run `git shortlog` or `git log --oneline`.
+
+### Step 4: Rebase
+
+Use `git rebase` (not `git merge`) to sync your work from time to time.
+
+```text
+% git fetch upstream
+% git rebase upstream/master
+```
+
+### Step 5: Push
+
+```text
+% git push my-feature-branch
+```
+
+Go to https://github.com/yourusername/geckodriver and press the _Pull
+Request_ and fill out the form.
+
+Pull requests are usually reviewed within a few days. Reviews will be done
+through [Reviewable](https://reviewable.io/reviews/mozilla/geckodriver)
+
+### Step 6: Integration
+
+When code review is complete, a committer will take your PR and
+integrate it on geckodriver's master branch. Because we like to keep a
+linear history on the master branch, we will normally squash and rebase
+your branch history.
+
+## Communication
+
+geckodriver contributors frequent the `#ateam` channel on
+[`irc.mozilla.org`](http://chat.mibbit.com/?server=irc.mozilla.org:#ateam).
diff --git a/Cargo.lock b/Cargo.lock
new file mode 100644
index 0000000..1fb7f41
--- /dev/null
+++ b/Cargo.lock
@@ -0,0 +1,770 @@
+[root]
+name = "geckodriver"
+version = "0.17.0"
+dependencies = [
+ "chrono 0.2.25 (registry+https://github.com/rust-lang/crates.io-index)",
+ "clap 2.24.2 (registry+https://github.com/rust-lang/crates.io-index)",
+ "hyper 0.10.10 (registry+https://github.com/rust-lang/crates.io-index)",
+ "lazy_static 0.1.16 (registry+https://github.com/rust-lang/crates.io-index)",
+ "log 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)",
+ "mozprofile 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)",
+ "mozrunner 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
+ "mozversion 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)",
+ "regex 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
+ "rustc-serialize 0.3.22 (registry+https://github.com/rust-lang/crates.io-index)",
+ "slog 1.5.2 (registry+https://github.com/rust-lang/crates.io-index)",
+ "slog-atomic 0.4.3 (registry+https://github.com/rust-lang/crates.io-index)",
+ "slog-stdlog 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
+ "slog-stream 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
+ "uuid 0.1.18 (registry+https://github.com/rust-lang/crates.io-index)",
+ "webdriver 0.26.0 (registry+https://github.com/rust-lang/crates.io-index)",
+ "zip 0.1.19 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
+name = "advapi32-sys"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)",
+ "winapi-build 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
+name = "aho-corasick"
+version = "0.6.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "memchr 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
+name = "backtrace"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "backtrace-sys 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)",
+ "cfg-if 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
+ "dbghelp-sys 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
+ "kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)",
+ "libc 0.2.20 (registry+https://github.com/rust-lang/crates.io-index)",
+ "rustc-demangle 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)",
+ "winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
+name = "backtrace-sys"
+version = "0.1.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "gcc 0.3.42 (registry+https://github.com/rust-lang/crates.io-index)",
+ "libc 0.2.20 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
+name = "base64"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "byteorder 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
+name = "bitflags"
+version = "0.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+
+[[package]]
+name = "byteorder"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+
+[[package]]
+name = "bzip2"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "bzip2-sys 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)",
+ "libc 0.2.20 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
+name = "bzip2-sys"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "gcc 0.3.42 (registry+https://github.com/rust-lang/crates.io-index)",
+ "libc 0.2.20 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
+name = "cfg-if"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+
+[[package]]
+name = "chrono"
+version = "0.2.25"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "num 0.1.37 (registry+https://github.com/rust-lang/crates.io-index)",
+ "time 0.1.36 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
+name = "clap"
+version = "2.24.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "bitflags 0.8.2 (registry+https://github.com/rust-lang/crates.io-index)",
+ "strsim 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)",
+ "term_size 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)",
+ "unicode-segmentation 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
+ "unicode-width 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)",
+ "vec_map 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
+name = "cookie"
+version = "0.6.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "time 0.1.36 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
+name = "crossbeam"
+version = "0.2.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+
+[[package]]
+name = "dbghelp-sys"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)",
+ "winapi-build 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
+name = "flate2"
+version = "0.2.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "libc 0.2.20 (registry+https://github.com/rust-lang/crates.io-index)",
+ "miniz-sys 0.1.9 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
+name = "gcc"
+version = "0.3.42"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+
+[[package]]
+name = "httparse"
+version = "1.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+
+[[package]]
+name = "hyper"
+version = "0.10.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "base64 0.5.2 (registry+https://github.com/rust-lang/crates.io-index)",
+ "httparse 1.2.2 (registry+https://github.com/rust-lang/crates.io-index)",
+ "language-tags 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)",
+ "log 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)",
+ "mime 0.2.4 (registry+https://github.com/rust-lang/crates.io-index)",
+ "num_cpus 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
+ "rustc_version 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)",
+ "time 0.1.36 (registry+https://github.com/rust-lang/crates.io-index)",
+ "traitobject 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
+ "typeable 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)",
+ "unicase 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
+ "url 1.4.1 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
+name = "idna"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "matches 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)",
+ "unicode-bidi 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)",
+ "unicode-normalization 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
+name = "isatty"
+version = "0.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)",
+ "libc 0.2.20 (registry+https://github.com/rust-lang/crates.io-index)",
+ "winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
+name = "kernel32-sys"
+version = "0.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)",
+ "winapi-build 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
+name = "kernel32-sys"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)",
+ "winapi-build 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
+name = "ktmw32-sys"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)",
+ "winapi-build 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
+name = "language-tags"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+
+[[package]]
+name = "lazy_static"
+version = "0.1.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+
+[[package]]
+name = "lazy_static"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+
+[[package]]
+name = "libc"
+version = "0.2.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+
+[[package]]
+name = "log"
+version = "0.3.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+
+[[package]]
+name = "matches"
+version = "0.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+
+[[package]]
+name = "memchr"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "libc 0.2.20 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
+name = "mime"
+version = "0.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "log 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
+name = "miniz-sys"
+version = "0.1.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "gcc 0.3.42 (registry+https://github.com/rust-lang/crates.io-index)",
+ "libc 0.2.20 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
+name = "mozprofile"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "tempdir 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
+name = "mozrunner"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "log 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)",
+ "mozprofile 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)",
+ "winreg 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
+name = "mozversion"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "regex 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
+ "rust-ini 0.10.0 (registry+https://github.com/rust-lang/crates.io-index)",
+ "semver 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
+name = "msdos_time"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)",
+ "time 0.1.36 (registry+https://github.com/rust-lang/crates.io-index)",
+ "winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
+name = "num"
+version = "0.1.37"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "num-integer 0.1.33 (registry+https://github.com/rust-lang/crates.io-index)",
+ "num-iter 0.1.33 (registry+https://github.com/rust-lang/crates.io-index)",
+ "num-traits 0.1.37 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
+name = "num-integer"
+version = "0.1.33"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "num-traits 0.1.37 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
+name = "num-iter"
+version = "0.1.33"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "num-integer 0.1.33 (registry+https://github.com/rust-lang/crates.io-index)",
+ "num-traits 0.1.37 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
+name = "num-traits"
+version = "0.1.37"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+
+[[package]]
+name = "num_cpus"
+version = "1.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "libc 0.2.20 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
+name = "podio"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+
+[[package]]
+name = "rand"
+version = "0.3.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "libc 0.2.20 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
+name = "redox_syscall"
+version = "0.1.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+
+[[package]]
+name = "regex"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "aho-corasick 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)",
+ "memchr 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)",
+ "regex-syntax 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
+ "thread_local 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)",
+ "utf8-ranges 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
+name = "regex-syntax"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+
+[[package]]
+name = "rust-ini"
+version = "0.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "log 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
+name = "rustc-demangle"
+version = "0.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+
+[[package]]
+name = "rustc-serialize"
+version = "0.3.22"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+
+[[package]]
+name = "rustc_version"
+version = "0.1.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "semver 0.1.20 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
+name = "semver"
+version = "0.1.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+
+[[package]]
+name = "semver"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "semver-parser 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
+name = "semver-parser"
+version = "0.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+
+[[package]]
+name = "slog"
+version = "1.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+
+[[package]]
+name = "slog-atomic"
+version = "0.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "crossbeam 0.2.10 (registry+https://github.com/rust-lang/crates.io-index)",
+ "slog 1.5.2 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
+name = "slog-extra"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "slog 1.5.2 (registry+https://github.com/rust-lang/crates.io-index)",
+ "thread_local 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
+name = "slog-stdlog"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "crossbeam 0.2.10 (registry+https://github.com/rust-lang/crates.io-index)",
+ "lazy_static 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)",
+ "log 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)",
+ "slog 1.5.2 (registry+https://github.com/rust-lang/crates.io-index)",
+ "slog-term 1.5.0 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
+name = "slog-stream"
+version = "1.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "slog 1.5.2 (registry+https://github.com/rust-lang/crates.io-index)",
+ "slog-extra 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)",
+ "thread_local 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
+name = "slog-term"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "chrono 0.2.25 (registry+https://github.com/rust-lang/crates.io-index)",
+ "isatty 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)",
+ "slog 1.5.2 (registry+https://github.com/rust-lang/crates.io-index)",
+ "slog-stream 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
+ "thread_local 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
+name = "strsim"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+
+[[package]]
+name = "tempdir"
+version = "0.3.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "rand 0.3.15 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
+name = "term_size"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)",
+ "libc 0.2.20 (registry+https://github.com/rust-lang/crates.io-index)",
+ "winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
+name = "thread-id"
+version = "3.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)",
+ "libc 0.2.20 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
+name = "thread_local"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "thread-id 3.0.0 (registry+https://github.com/rust-lang/crates.io-index)",
+ "unreachable 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
+name = "time"
+version = "0.1.36"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)",
+ "libc 0.2.20 (registry+https://github.com/rust-lang/crates.io-index)",
+ "redox_syscall 0.1.16 (registry+https://github.com/rust-lang/crates.io-index)",
+ "winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
+name = "traitobject"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+
+[[package]]
+name = "typeable"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+
+[[package]]
+name = "unicase"
+version = "1.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "rustc_version 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
+name = "unicode-bidi"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "matches 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
+name = "unicode-normalization"
+version = "0.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+
+[[package]]
+name = "unicode-segmentation"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+
+[[package]]
+name = "unicode-width"
+version = "0.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+
+[[package]]
+name = "unreachable"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "void 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
+name = "url"
+version = "1.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "idna 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)",
+ "matches 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
+name = "utf8-ranges"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+
+[[package]]
+name = "uuid"
+version = "0.1.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "rand 0.3.15 (registry+https://github.com/rust-lang/crates.io-index)",
+ "rustc-serialize 0.3.22 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
+name = "vec_map"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+
+[[package]]
+name = "void"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+
+[[package]]
+name = "webdriver"
+version = "0.26.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "backtrace 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)",
+ "cookie 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)",
+ "hyper 0.10.10 (registry+https://github.com/rust-lang/crates.io-index)",
+ "log 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)",
+ "regex 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
+ "rustc-serialize 0.3.22 (registry+https://github.com/rust-lang/crates.io-index)",
+ "time 0.1.36 (registry+https://github.com/rust-lang/crates.io-index)",
+ "url 1.4.1 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
+name = "winapi"
+version = "0.2.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+
+[[package]]
+name = "winapi-build"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+
+[[package]]
+name = "winreg"
+version = "0.3.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "advapi32-sys 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)",
+ "kernel32-sys 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)",
+ "ktmw32-sys 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
+ "rustc-serialize 0.3.22 (registry+https://github.com/rust-lang/crates.io-index)",
+ "winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
+name = "zip"
+version = "0.1.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "bzip2 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)",
+ "flate2 0.2.19 (registry+https://github.com/rust-lang/crates.io-index)",
+ "msdos_time 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)",
+ "podio 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)",
+ "time 0.1.36 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[metadata]
+"checksum advapi32-sys 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "307c92332867e586720c0222ee9d890bbe8431711efed8a1b06bc5b40fc66bd7"
+"checksum aho-corasick 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)" = "0638fd549427caa90c499814196d1b9e3725eb4d15d7339d6de073a680ed0ca2"
+"checksum backtrace 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)" = "72f9b4182546f4b04ebc4ab7f84948953a118bd6021a1b6a6c909e3e94f6be76"
+"checksum backtrace-sys 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)" = "d192fd129132fbc97497c1f2ec2c2c5174e376b95f535199ef4fe0a293d33842"
+"checksum base64 0.5.2 (registry+https://github.com/rust-lang/crates.io-index)" = "30e93c03064e7590d0466209155251b90c22e37fab1daf2771582598b5827557"
+"checksum bitflags 0.8.2 (registry+https://github.com/rust-lang/crates.io-index)" = "1370e9fc2a6ae53aea8b7a5110edbd08836ed87c88736dfabccade1c2b44bff4"
+"checksum byteorder 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "c40977b0ee6b9885c9013cd41d9feffdd22deb3bb4dc3a71d901cc7a77de18c8"
+"checksum bzip2 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)" = "c3eafc42c44e0d827de6b1c131175098fe7fb53b8ce8a47e65cb3ea94688be24"
+"checksum bzip2-sys 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "98ce3fff84d4e90011f464bbdf48e3428f04270439f703868fd489d2aaedfc30"
+"checksum cfg-if 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "de1e760d7b6535af4241fca8bd8adf68e2e7edacc6b29f5d399050c5e48cf88c"
+"checksum chrono 0.2.25 (registry+https://github.com/rust-lang/crates.io-index)" = "9213f7cd7c27e95c2b57c49f0e69b1ea65b27138da84a170133fd21b07659c00"
+"checksum clap 2.24.2 (registry+https://github.com/rust-lang/crates.io-index)" = "6b8f69e518f967224e628896b54e41ff6acfb4dcfefc5076325c36525dac900f"
+"checksum cookie 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)" = "30b3493e12a550c2f96be785088d1da8d93189e7237c8a8d0d871bc9070334c3"
+"checksum crossbeam 0.2.10 (registry+https://github.com/rust-lang/crates.io-index)" = "0c5ea215664ca264da8a9d9c3be80d2eaf30923c259d03e870388eb927508f97"
+"checksum dbghelp-sys 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "97590ba53bcb8ac28279161ca943a924d1fd4a8fb3fa63302591647c4fc5b850"
+"checksum flate2 0.2.19 (registry+https://github.com/rust-lang/crates.io-index)" = "36df0166e856739905cd3d7e0b210fe818592211a008862599845e012d8d304c"
+"checksum gcc 0.3.42 (registry+https://github.com/rust-lang/crates.io-index)" = "291055c78f59ca3d84c99026c9501c469413d386bb46be1e1cf1d285cd1db3b0"
+"checksum httparse 1.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "77f756bed9ee3a83ce98774f4155b42a31b787029013f3a7d83eca714e500e21"
+"checksum hyper 0.10.10 (registry+https://github.com/rust-lang/crates.io-index)" = "36e108e0b1fa2d17491cbaac4bc460dc0956029d10ccf83c913dd0e5db3e7f07"
+"checksum idna 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "2233d4940b1f19f0418c158509cd7396b8d70a5db5705ce410914dc8fa603b37"
+"checksum isatty 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)" = "fa500db770a99afe2a0f2229be2a3d09c7ed9d7e4e8440bf71253141994e240f"
+"checksum kernel32-sys 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)" = "e014dab1082fd9d80ea1fa6fcb261b47ed3eb511612a14198bb507701add083e"
+"checksum kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "7507624b29483431c0ba2d82aece8ca6cdba9382bff4ddd0f7490560c056098d"
+"checksum ktmw32-sys 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "7f9313a869ff779ae08dd990b75a92dc06aa16d771f41305f7286649cd39a0ee"
+"checksum language-tags 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "a91d884b6667cd606bb5a69aa0c99ba811a115fc68915e7056ec08a46e93199a"
+"checksum lazy_static 0.1.16 (registry+https://github.com/rust-lang/crates.io-index)" = "cf186d1a8aa5f5bee5fd662bc9c1b949e0259e1bcc379d1f006847b0080c7417"
+"checksum lazy_static 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "6abe0ee2e758cd6bc8a2cd56726359007748fbf4128da998b65d0b70f881e19b"
+"checksum libc 0.2.20 (registry+https://github.com/rust-lang/crates.io-index)" = "684f330624d8c3784fb9558ca46c4ce488073a8d22450415c5eb4f4cfb0d11b5"
+"checksum log 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)" = "ab83497bf8bf4ed2a74259c1c802351fcd67a65baa86394b6ba73c36f4838054"
+"checksum matches 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)" = "efd7622e3022e1a6eaa602c4cea8912254e5582c9c692e9167714182244801b1"
+"checksum memchr 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "1dbccc0e46f1ea47b9f17e6d67c5a96bd27030519c519c9c91327e31275a47b4"
+"checksum mime 0.2.4 (registry+https://github.com/rust-lang/crates.io-index)" = "9d69889cdc6336ed56b174514ce876c4c3dc564cc23dd872e7bca589bb2a36c8"
+"checksum miniz-sys 0.1.9 (registry+https://github.com/rust-lang/crates.io-index)" = "28eaee17666671fa872e567547e8428e83308ebe5808cdf6a0e28397dbe2c726"
+"checksum mozprofile 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "1a17b8bbde1dc0fbf1c8b073192d7c6f89baa932173ece7c1447de5e9cc7cd7e"
+"checksum mozrunner 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "a568168329fc285ad6d04dfbe058ea20ff842f4301fe9205c6cbd4ed3be85378"
+"checksum mozversion 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "9fb3a40135553611560d3eb4a49479beaf0c91c5a93f723338c5b0edddf08f26"
+"checksum msdos_time 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "65ba9d75bcea84e07812618fedf284a64776c2f2ea0cad6bca7f69739695a958"
+"checksum num 0.1.37 (registry+https://github.com/rust-lang/crates.io-index)" = "98b15ba84e910ea7a1973bccd3df7b31ae282bf9d8bd2897779950c9b8303d40"
+"checksum num-integer 0.1.33 (registry+https://github.com/rust-lang/crates.io-index)" = "21e4df1098d1d797d27ef0c69c178c3fab64941559b290fcae198e0825c9c8b5"
+"checksum num-iter 0.1.33 (registry+https://github.com/rust-lang/crates.io-index)" = "f7d1891bd7b936f12349b7d1403761c8a0b85a18b148e9da4429d5d102c1a41e"
+"checksum num-traits 0.1.37 (registry+https://github.com/rust-lang/crates.io-index)" = "e1cbfa3781f3fe73dc05321bed52a06d2d491eaa764c52335cf4399f046ece99"
+"checksum num_cpus 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "a225d1e2717567599c24f88e49f00856c6e825a12125181ee42c4257e3688d39"
+"checksum podio 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "e5422a1ee1bc57cc47ae717b0137314258138f38fd5f3cea083f43a9725383a0"
+"checksum rand 0.3.15 (registry+https://github.com/rust-lang/crates.io-index)" = "022e0636ec2519ddae48154b028864bdce4eaf7d35226ab8e65c611be97b189d"
+"checksum redox_syscall 0.1.16 (registry+https://github.com/rust-lang/crates.io-index)" = "8dd35cc9a8bdec562c757e3d43c1526b5c6d2653e23e2315065bc25556550753"
+"checksum regex 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "4278c17d0f6d62dfef0ab00028feb45bd7d2102843f80763474eeb1be8a10c01"
+"checksum regex-syntax 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "2f9191b1f57603095f105d317e375d19b1c9c5c3185ea9633a99a6dcbed04457"
+"checksum rust-ini 0.10.0 (registry+https://github.com/rust-lang/crates.io-index)" = "06d4e8b0b50e7e7f827d609fa9746e1cf6371a1fa15404a1a0a86152a801079f"
+"checksum rustc-demangle 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)" = "3058a43ada2c2d0b92b3ae38007a2d0fa5e9db971be260e0171408a4ff471c95"
+"checksum rustc-serialize 0.3.22 (registry+https://github.com/rust-lang/crates.io-index)" = "237546c689f20bb44980270c73c3b9edd0891c1be49cc1274406134a66d3957b"
+"checksum rustc_version 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)" = "c5f5376ea5e30ce23c03eb77cbe4962b988deead10910c372b226388b594c084"
+"checksum semver 0.1.20 (registry+https://github.com/rust-lang/crates.io-index)" = "d4f410fedcf71af0345d7607d246e7ad15faaadd49d240ee3b24e5dc21a820ac"
+"checksum semver 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)" = "7a3186ec9e65071a2095434b1f5bb24838d4e8e130f584c790f6033c79943537"
+"checksum semver-parser 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3"
+"checksum slog 1.5.2 (registry+https://github.com/rust-lang/crates.io-index)" = "bab9d589681f7d6b9ca4ed5cc861779a392bca7beaae2f69f2341617415a78dc"
+"checksum slog-atomic 0.4.3 (registry+https://github.com/rust-lang/crates.io-index)" = "d6f5a4e4908d6818fe553b6126ba5377801556ab885c65ebf960b722a6778864"
+"checksum slog-extra 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "511581f4dd1dc90e4eca99b60be8a692d9c975e8757558aa774f16007d27492a"
+"checksum slog-stdlog 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "56cc08f40c45e0ab41dcfde0a19a22c5b7176d3827fc7d078450ebfdc080a37c"
+"checksum slog-stream 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "3fac4af71007ddb7338f771e059a46051f18d1454d8ac556f234a0573e719daa"
+"checksum slog-term 1.5.0 (registry+https://github.com/rust-lang/crates.io-index)" = "cb53c0bae0745898fd5a7b75b1c389507333470ac4c645ae431890c0f828b6ca"
+"checksum strsim 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)" = "b4d15c810519a91cf877e7e36e63fe068815c678181439f2f29e2562147c3694"
+"checksum tempdir 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)" = "87974a6f5c1dfb344d733055601650059a3363de2a6104819293baff662132d6"
+"checksum term_size 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "e2b6b55df3198cc93372e85dd2ed817f0e38ce8cc0f22eb32391bfad9c4bf209"
+"checksum thread-id 3.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "4437c97558c70d129e40629a5b385b3fb1ffac301e63941335e4d354081ec14a"
+"checksum thread_local 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "c85048c6260d17cf486ceae3282d9fb6b90be220bf5b28c400f5485ffc29f0c7"
+"checksum time 0.1.36 (registry+https://github.com/rust-lang/crates.io-index)" = "211b63c112206356ef1ff9b19355f43740fc3f85960c598a93d3a3d3ba7beade"
+"checksum traitobject 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "efd1f82c56340fdf16f2a953d7bda4f8fdffba13d93b00844c25572110b26079"
+"checksum typeable 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "1410f6f91f21d1612654e7cc69193b0334f909dcf2c790c4826254fbb86f8887"
+"checksum unicase 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "13a5906ca2b98c799f4b1ab4557b76367ebd6ae5ef14930ec841c74aed5f3764"
+"checksum unicode-bidi 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "a6a2c4e3710edd365cd7e78383153ed739fa31af19f9172f72d3575060f5a43a"
+"checksum unicode-normalization 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)" = "e28fa37426fceeb5cf8f41ee273faa7c82c47dc8fba5853402841e665fcd86ff"
+"checksum unicode-segmentation 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "18127285758f0e2c6cf325bb3f3d138a12fee27de4f23e146cd6a179f26c2cf3"
+"checksum unicode-width 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)" = "bf3a113775714a22dcb774d8ea3655c53a32debae63a063acc00a91cc586245f"
+"checksum unreachable 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "1f2ae5ddb18e1c92664717616dd9549dde73f539f01bd7b77c2edb2446bdff91"
+"checksum url 1.4.1 (registry+https://github.com/rust-lang/crates.io-index)" = "3e2ba3456fbe5c0098cb877cf08b92b76c3e18e0be9e47c35b487220d377d24e"
+"checksum utf8-ranges 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "662fab6525a98beff2921d7f61a39e7d59e0b425ebc7d0d9e66d316e55124122"
+"checksum uuid 0.1.18 (registry+https://github.com/rust-lang/crates.io-index)" = "78c590b5bd79ed10aad8fb75f078a59d8db445af6c743e55c4a53227fc01c13f"
+"checksum vec_map 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)" = "887b5b631c2ad01628bbbaa7dd4c869f80d3186688f8d0b6f58774fbe324988c"
+"checksum void 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)" = "6a02e4885ed3bc0f2de90ea6dd45ebcbb66dacffe03547fadbb0eeae2770887d"
+"checksum webdriver 0.26.0 (registry+https://github.com/rust-lang/crates.io-index)" = "3099729d884692d690796454e8529edf3f0ebd87c87840f9c809df8eabb175ed"
+"checksum winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)" = "167dc9d6949a9b857f3451275e911c3f44255842c1f7a76f33c55103a909087a"
+"checksum winapi-build 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "2d315eee3b34aca4797b2da6b13ed88266e6d612562a0c46390af8299fc699bc"
+"checksum winreg 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)" = "e63857fb213f619b4c4fff86b158285c76766aac7e7474967e92fb6dbbfeefe9"
+"checksum zip 0.1.19 (registry+https://github.com/rust-lang/crates.io-index)" = "c0deac03fc7d43abcf19f2c2db6bd9289f9ea3d31f350e26eb0ed8b4117983c1"
diff --git a/Cargo.toml b/Cargo.toml
new file mode 100644
index 0000000..6d06fe8
--- /dev/null
+++ b/Cargo.toml
@@ -0,0 +1,34 @@
+[package]
+name = "geckodriver"
+version = "0.17.0"
+authors = [
+ "James Graham <james@hoppipolla.co.uk>",
+ "Andreas Tolfsen <ato@mozilla.com>",
+]
+description = "Proxy for using WebDriver clients to interact with Gecko-based browsers."
+keywords = ["webdriver", "w3c", "httpd", "mozilla", "firefox"]
+repository = "https://github.com/mozilla/geckodriver"
+readme = "README.md"
+license = "MPL-2.0"
+
+[dependencies]
+chrono = "^0.2"
+clap = {version = "^2.19", default-features = false, features = ["suggestions", "wrap_help"]}
+hyper = "0.10"
+lazy_static = "0.1"
+log = "0.3"
+mozprofile = "0.3"
+mozrunner = "0.4"
+mozversion = "0.1"
+regex = "0.2"
+rustc-serialize = "0.3"
+slog = "1"
+slog-atomic = "0.4"
+slog-stdlog = "1"
+slog-stream = "1"
+uuid = "0.1.18"
+webdriver = "0.26.0"
+zip = "0.1"
+
+[[bin]]
+name = "geckodriver"
diff --git a/ISSUE_TEMPLATE.md b/ISSUE_TEMPLATE.md
new file mode 100644
index 0000000..ce4a78c
--- /dev/null
+++ b/ISSUE_TEMPLATE.md
@@ -0,0 +1,16 @@
+In order to help us efficiently investigate your issue, please provide the following information:
+
+## Firefox Version
+From the "About" dialog.
+
+## Platform
+e.g. Windows/OSX/Linux
+
+## Steps to reproduce -
+Please provide a description of how to reproduce the problem, including the following essential elements:
+
+ - [ ] A *minimal* HTML file (or files) that can be used to reproduce the issue (this should not be your production site, unless you are unable to isolate the problem, but a page containing only the elements required to observe the problem).
+
+ - [ ] A [trace level log](https://github.com/mozilla/geckodriver#firefox-capabilities) from a geckodriver session that demonstrates the problem using the minimal example file. Note: this is *not* the same as a selenium log.
+
+** If any of the above are missing we will have to unfortunately close your issue. We will gladly reopen the issue once all the information requested has been added **
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..14e2f77
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,373 @@
+Mozilla Public License Version 2.0
+==================================
+
+1. Definitions
+--------------
+
+1.1. "Contributor"
+ means each individual or legal entity that creates, contributes to
+ the creation of, or owns Covered Software.
+
+1.2. "Contributor Version"
+ means the combination of the Contributions of others (if any) used
+ by a Contributor and that particular Contributor's Contribution.
+
+1.3. "Contribution"
+ means Covered Software of a particular Contributor.
+
+1.4. "Covered Software"
+ means Source Code Form to which the initial Contributor has attached
+ the notice in Exhibit A, the Executable Form of such Source Code
+ Form, and Modifications of such Source Code Form, in each case
+ including portions thereof.
+
+1.5. "Incompatible With Secondary Licenses"
+ means
+
+ (a) that the initial Contributor has attached the notice described
+ in Exhibit B to the Covered Software; or
+
+ (b) that the Covered Software was made available under the terms of
+ version 1.1 or earlier of the License, but not also under the
+ terms of a Secondary License.
+
+1.6. "Executable Form"
+ means any form of the work other than Source Code Form.
+
+1.7. "Larger Work"
+ means a work that combines Covered Software with other material, in
+ a separate file or files, that is not Covered Software.
+
+1.8. "License"
+ means this document.
+
+1.9. "Licensable"
+ means having the right to grant, to the maximum extent possible,
+ whether at the time of the initial grant or subsequently, any and
+ all of the rights conveyed by this License.
+
+1.10. "Modifications"
+ means any of the following:
+
+ (a) any file in Source Code Form that results from an addition to,
+ deletion from, or modification of the contents of Covered
+ Software; or
+
+ (b) any new file in Source Code Form that contains any Covered
+ Software.
+
+1.11. "Patent Claims" of a Contributor
+ means any patent claim(s), including without limitation, method,
+ process, and apparatus claims, in any patent Licensable by such
+ Contributor that would be infringed, but for the grant of the
+ License, by the making, using, selling, offering for sale, having
+ made, import, or transfer of either its Contributions or its
+ Contributor Version.
+
+1.12. "Secondary License"
+ means either the GNU General Public License, Version 2.0, the GNU
+ Lesser General Public License, Version 2.1, the GNU Affero General
+ Public License, Version 3.0, or any later versions of those
+ licenses.
+
+1.13. "Source Code Form"
+ means the form of the work preferred for making modifications.
+
+1.14. "You" (or "Your")
+ means an individual or a legal entity exercising rights under this
+ License. For legal entities, "You" includes any entity that
+ controls, is controlled by, or is under common control with You. For
+ purposes of this definition, "control" means (a) the power, direct
+ or indirect, to cause the direction or management of such entity,
+ whether by contract or otherwise, or (b) ownership of more than
+ fifty percent (50%) of the outstanding shares or beneficial
+ ownership of such entity.
+
+2. License Grants and Conditions
+--------------------------------
+
+2.1. Grants
+
+Each Contributor hereby grants You a world-wide, royalty-free,
+non-exclusive license:
+
+(a) under intellectual property rights (other than patent or trademark)
+ Licensable by such Contributor to use, reproduce, make available,
+ modify, display, perform, distribute, and otherwise exploit its
+ Contributions, either on an unmodified basis, with Modifications, or
+ as part of a Larger Work; and
+
+(b) under Patent Claims of such Contributor to make, use, sell, offer
+ for sale, have made, import, and otherwise transfer either its
+ Contributions or its Contributor Version.
+
+2.2. Effective Date
+
+The licenses granted in Section 2.1 with respect to any Contribution
+become effective for each Contribution on the date the Contributor first
+distributes such Contribution.
+
+2.3. Limitations on Grant Scope
+
+The licenses granted in this Section 2 are the only rights granted under
+this License. No additional rights or licenses will be implied from the
+distribution or licensing of Covered Software under this License.
+Notwithstanding Section 2.1(b) above, no patent license is granted by a
+Contributor:
+
+(a) for any code that a Contributor has removed from Covered Software;
+ or
+
+(b) for infringements caused by: (i) Your and any other third party's
+ modifications of Covered Software, or (ii) the combination of its
+ Contributions with other software (except as part of its Contributor
+ Version); or
+
+(c) under Patent Claims infringed by Covered Software in the absence of
+ its Contributions.
+
+This License does not grant any rights in the trademarks, service marks,
+or logos of any Contributor (except as may be necessary to comply with
+the notice requirements in Section 3.4).
+
+2.4. Subsequent Licenses
+
+No Contributor makes additional grants as a result of Your choice to
+distribute the Covered Software under a subsequent version of this
+License (see Section 10.2) or under the terms of a Secondary License (if
+permitted under the terms of Section 3.3).
+
+2.5. Representation
+
+Each Contributor represents that the Contributor believes its
+Contributions are its original creation(s) or it has sufficient rights
+to grant the rights to its Contributions conveyed by this License.
+
+2.6. Fair Use
+
+This License is not intended to limit any rights You have under
+applicable copyright doctrines of fair use, fair dealing, or other
+equivalents.
+
+2.7. Conditions
+
+Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted
+in Section 2.1.
+
+3. Responsibilities
+-------------------
+
+3.1. Distribution of Source Form
+
+All distribution of Covered Software in Source Code Form, including any
+Modifications that You create or to which You contribute, must be under
+the terms of this License. You must inform recipients that the Source
+Code Form of the Covered Software is governed by the terms of this
+License, and how they can obtain a copy of this License. You may not
+attempt to alter or restrict the recipients' rights in the Source Code
+Form.
+
+3.2. Distribution of Executable Form
+
+If You distribute Covered Software in Executable Form then:
+
+(a) such Covered Software must also be made available in Source Code
+ Form, as described in Section 3.1, and You must inform recipients of
+ the Executable Form how they can obtain a copy of such Source Code
+ Form by reasonable means in a timely manner, at a charge no more
+ than the cost of distribution to the recipient; and
+
+(b) You may distribute such Executable Form under the terms of this
+ License, or sublicense it under different terms, provided that the
+ license for the Executable Form does not attempt to limit or alter
+ the recipients' rights in the Source Code Form under this License.
+
+3.3. Distribution of a Larger Work
+
+You may create and distribute a Larger Work under terms of Your choice,
+provided that You also comply with the requirements of this License for
+the Covered Software. If the Larger Work is a combination of Covered
+Software with a work governed by one or more Secondary Licenses, and the
+Covered Software is not Incompatible With Secondary Licenses, this
+License permits You to additionally distribute such Covered Software
+under the terms of such Secondary License(s), so that the recipient of
+the Larger Work may, at their option, further distribute the Covered
+Software under the terms of either this License or such Secondary
+License(s).
+
+3.4. Notices
+
+You may not remove or alter the substance of any license notices
+(including copyright notices, patent notices, disclaimers of warranty,
+or limitations of liability) contained within the Source Code Form of
+the Covered Software, except that You may alter any license notices to
+the extent required to remedy known factual inaccuracies.
+
+3.5. Application of Additional Terms
+
+You may choose to offer, and to charge a fee for, warranty, support,
+indemnity or liability obligations to one or more recipients of Covered
+Software. However, You may do so only on Your own behalf, and not on
+behalf of any Contributor. You must make it absolutely clear that any
+such warranty, support, indemnity, or liability obligation is offered by
+You alone, and You hereby agree to indemnify every Contributor for any
+liability incurred by such Contributor as a result of warranty, support,
+indemnity or liability terms You offer. You may include additional
+disclaimers of warranty and limitations of liability specific to any
+jurisdiction.
+
+4. Inability to Comply Due to Statute or Regulation
+---------------------------------------------------
+
+If it is impossible for You to comply with any of the terms of this
+License with respect to some or all of the Covered Software due to
+statute, judicial order, or regulation then You must: (a) comply with
+the terms of this License to the maximum extent possible; and (b)
+describe the limitations and the code they affect. Such description must
+be placed in a text file included with all distributions of the Covered
+Software under this License. Except to the extent prohibited by statute
+or regulation, such description must be sufficiently detailed for a
+recipient of ordinary skill to be able to understand it.
+
+5. Termination
+--------------
+
+5.1. The rights granted under this License will terminate automatically
+if You fail to comply with any of its terms. However, if You become
+compliant, then the rights granted under this License from a particular
+Contributor are reinstated (a) provisionally, unless and until such
+Contributor explicitly and finally terminates Your grants, and (b) on an
+ongoing basis, if such Contributor fails to notify You of the
+non-compliance by some reasonable means prior to 60 days after You have
+come back into compliance. Moreover, Your grants from a particular
+Contributor are reinstated on an ongoing basis if such Contributor
+notifies You of the non-compliance by some reasonable means, this is the
+first time You have received notice of non-compliance with this License
+from such Contributor, and You become compliant prior to 30 days after
+Your receipt of the notice.
+
+5.2. If You initiate litigation against any entity by asserting a patent
+infringement claim (excluding declaratory judgment actions,
+counter-claims, and cross-claims) alleging that a Contributor Version
+directly or indirectly infringes any patent, then the rights granted to
+You by any and all Contributors for the Covered Software under Section
+2.1 of this License shall terminate.
+
+5.3. In the event of termination under Sections 5.1 or 5.2 above, all
+end user license agreements (excluding distributors and resellers) which
+have been validly granted by You or Your distributors under this License
+prior to termination shall survive termination.
+
+************************************************************************
+* *
+* 6. Disclaimer of Warranty *
+* ------------------------- *
+* *
+* Covered Software is provided under this License on an "as is" *
+* basis, without warranty of any kind, either expressed, implied, or *
+* statutory, including, without limitation, warranties that the *
+* Covered Software is free of defects, merchantable, fit for a *
+* particular purpose or non-infringing. The entire risk as to the *
+* quality and performance of the Covered Software is with You. *
+* Should any Covered Software prove defective in any respect, You *
+* (not any Contributor) assume the cost of any necessary servicing, *
+* repair, or correction. This disclaimer of warranty constitutes an *
+* essential part of this License. No use of any Covered Software is *
+* authorized under this License except under this disclaimer. *
+* *
+************************************************************************
+
+************************************************************************
+* *
+* 7. Limitation of Liability *
+* -------------------------- *
+* *
+* Under no circumstances and under no legal theory, whether tort *
+* (including negligence), contract, or otherwise, shall any *
+* Contributor, or anyone who distributes Covered Software as *
+* permitted above, be liable to You for any direct, indirect, *
+* special, incidental, or consequential damages of any character *
+* including, without limitation, damages for lost profits, loss of *
+* goodwill, work stoppage, computer failure or malfunction, or any *
+* and all other commercial damages or losses, even if such party *
+* shall have been informed of the possibility of such damages. This *
+* limitation of liability shall not apply to liability for death or *
+* personal injury resulting from such party's negligence to the *
+* extent applicable law prohibits such limitation. Some *
+* jurisdictions do not allow the exclusion or limitation of *
+* incidental or consequential damages, so this exclusion and *
+* limitation may not apply to You. *
+* *
+************************************************************************
+
+8. Litigation
+-------------
+
+Any litigation relating to this License may be brought only in the
+courts of a jurisdiction where the defendant maintains its principal
+place of business and such litigation shall be governed by laws of that
+jurisdiction, without reference to its conflict-of-law provisions.
+Nothing in this Section shall prevent a party's ability to bring
+cross-claims or counter-claims.
+
+9. Miscellaneous
+----------------
+
+This License represents the complete agreement concerning the subject
+matter hereof. If any provision of this License is held to be
+unenforceable, such provision shall be reformed only to the extent
+necessary to make it enforceable. Any law or regulation which provides
+that the language of a contract shall be construed against the drafter
+shall not be used to construe this License against a Contributor.
+
+10. Versions of the License
+---------------------------
+
+10.1. New Versions
+
+Mozilla Foundation is the license steward. Except as provided in Section
+10.3, no one other than the license steward has the right to modify or
+publish new versions of this License. Each version will be given a
+distinguishing version number.
+
+10.2. Effect of New Versions
+
+You may distribute the Covered Software under the terms of the version
+of the License under which You originally received the Covered Software,
+or under the terms of any subsequent version published by the license
+steward.
+
+10.3. Modified Versions
+
+If you create software not governed by this License, and you want to
+create a new license for such software, you may create and use a
+modified version of this License if you rename the license and remove
+any references to the name of the license steward (except to note that
+such modified license differs from this License).
+
+10.4. Distributing Source Code Form that is Incompatible With Secondary
+Licenses
+
+If You choose to distribute Source Code Form that is Incompatible With
+Secondary Licenses under the terms of this version of the License, the
+notice described in Exhibit B of this License must be attached.
+
+Exhibit A - Source Code Form License Notice
+-------------------------------------------
+
+ 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/.
+
+If it is not possible or desirable to put the notice in a particular
+file, then You may include the notice in a location (such as a LICENSE
+file in a relevant directory) where a recipient would be likely to look
+for such a notice.
+
+You may add additional accurate notices of copyright ownership.
+
+Exhibit B - "Incompatible With Secondary Licenses" Notice
+---------------------------------------------------------
+
+ This Source Code Form is "Incompatible With Secondary Licenses", as
+ defined by the Mozilla Public License, v. 2.0.
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..cb4977b
--- /dev/null
+++ b/README.md
@@ -0,0 +1,535 @@
+# geckodriver [![Build Status](https://travis-ci.org/mozilla/geckodriver.svg?branch=master)](https://travis-ci.org/mozilla/geckodriver)
+
+Proxy for using W3C WebDriver-compatible clients
+to interact with Gecko-based browsers.
+
+This program provides the HTTP API described by
+the [WebDriver protocol](http://w3c.github.io/webdriver/webdriver-spec.html#protocol)
+to communicate with Gecko browsers, such as Firefox.
+It translates calls into
+the [Marionette automation protocol](https://developer.mozilla.org/en-US/docs/Mozilla/QA/Marionette)
+by acting as a proxy between the local- and remote ends.
+
+You can consult the [change log](https://github.com/mozilla/geckodriver/blob/master/CHANGES.md)
+for a record of all notable changes to the program.
+[Releases](https://github.com/mozilla/geckodriver/releases)
+are made available on GitHub
+on [supported platforms](#supported-firefoxen).
+
+## Supported clients
+
+[Selenium](http://docs.seleniumhq.org/) users
+must update to [version 3.4](https://github.com/SeleniumHQ/selenium/releases/tag/selenium-3.4.0)
+or later to use geckodriver.
+Other clients that follow the [W3C WebDriver specification](https://w3c.github.io/webdriver/webdriver-spec.html) are also supported.
+
+## Supported Firefoxen
+
+Marionette and geckodriver are not yet feature complete.
+This means that they do not yet offer full conformance
+with the [WebDriver standard](https://w3c.github.io/webdriver/webdriver-spec.html)
+or complete compatibility with [Selenium](http://www.seleniumhq.org/).
+You can track the [implementation status](https://developer.mozilla.org/en-US/docs/Mozilla/QA/Marionette/WebDriver/status)
+of the latest [Firefox Nightly](http://whattrainisitnow.com/) on
+[MDN](https://developer.mozilla.org/).
+We also keep track of known
+[Marionette](https://github.com/mozilla/geckodriver/issues?q=is%3Aissue+is%3Aopen+label%3Amarionette),
+[Selenium](https://github.com/mozilla/geckodriver/issues?q=is%3Aissue+is%3Aopen+label%3Aselenium),
+and [specification](https://github.com/mozilla/geckodriver/issues?q=is%3Aissue+is%3Aopen+label%3Aspec)
+problems in our
+[issue tracker](https://github.com/mozilla/geckodriver/issues).
+
+Support is best in Firefox 52.0.3 and onwards,
+although generally the more recent the Firefox version,
+the better the experience as they have more bug fixes and features.
+Some features will only be available in the most recent Firefox versions,
+and we strongly advise using the [latest Firefox Nightly](https://nightly.mozilla.org/) with geckodriver.
+Since Windows XP support in Firefox will be dropped with Firefox 53,
+we do not support this platform.
+
+## WebDriver capabilities
+
+geckodriver supports a number of
+[WebDriver capabilities](https://w3c.github.io/webdriver/webdriver-spec.html#capabilities):
+
+<table>
+ <thead>
+ <tr>
+ <th>Name
+ <th>Type
+ <th>Description
+ </tr>
+ </thead>
+
+ <tr>
+ <td><code>proxy</code>
+ <td><a href=#proxy-object><code>proxy</code></a> object
+ <td>Sets browser proxy settings.
+ </tr>
+
+ <tr>
+ <td><code>acceptInsecureCerts</code>
+ <td>boolean
+ <td>Boolean initially set to false,
+ indicating the session will not implicitly trust untrusted
+ or self-signed TLS certificates on navigation.
+ </tr>
+</table>
+
+### `proxy` object
+
+<table>
+ <thead>
+ <tr>
+ <th>Name
+ <th>Type
+ <th>Description
+ </tr>
+ </thead>
+
+ <tr>
+ <td><code>proxyType</code>
+ <td>string
+ <td>Indicates the type of proxy configuration.
+ This value must be one of
+ <code>pac</code>,
+ <code>noproxy</code>,
+ <code>autodetect</code>,
+ <code>system</code>,
+ or <code>manual</code>.
+ </tr>
+
+ <tr>
+ <td><code>proxyAutoconfigUrl</code>
+ <td>string
+ <td>Defines the URL for a proxy auto-config file.
+ This property should only be set
+ when <code>proxyType</code> is <code>pac</code>.
+ </tr>
+
+ <tr>
+ <td><code>ftpProxy</code>
+ <td>string
+ <td>Defines the proxy hostname for FTP traffic.
+ Should only be set then the <code>proxyType</code>
+ is set to <code>manual</code>.
+ </tr>
+
+ <tr>
+ <td><code>ftpProxyPort</code>
+ <td>number
+ <td>Defines the proxy port for FTP traffic.
+ This property should only be set
+ when <code>proxyType</code> is <code>manual</code>.
+ </tr>
+
+ <tr>
+ <td><code>httpProxy</code>
+ <td>string
+ <td>Defines the hostname for HTTP traffic.
+ This property should only be set
+ when <code>proxyType</code> is <code>manual</code>.
+ </tr>
+
+ <tr>
+ <td><code>httpProxyPort</code>
+ <td>number
+ <td>Defines the proxy port for HTTP traffic.
+ This property should only be set
+ when <code>proxyType</code> is <code>manual</code>.
+ </tr>
+
+ <tr>
+ <td><code>sslProxy</code>
+ <td>string
+ <td>Defines the proxy hostname
+ for encrypted TLS traffic.
+ This property should only be set
+ when <code>proxyType</code> is <code>manual</code>.
+ </tr>
+
+ <tr>
+ <td><code>sslProxyPort</code>
+ <td>number
+ <td>Defines the proxy port for SSL traffic.
+ This property should only be set
+ when <code>proxyType</code> is <code>manual</code>.
+ </tr>
+
+ <tr>
+ <td><code>socksProxy</code>
+ <td>string
+ <td>Defines the proxy hostname for a SOCKS proxy.
+ This property should only be set
+ when <code>proxyType</code> is <code>manual</code>.
+ </tr>
+
+ <tr>
+ <td><code>socksProxyPort</code>
+ <td>number
+ <td>Defines the proxy port for a SOCKS proxy.
+ This property should only be set
+ when <code>proxyType</code> is <code>manual</code>.
+ </tr>
+
+ <tr>
+ <td><code>socksVersion</code>
+ <td>number
+ <td>Defines the SOCKS proxy version.
+ This property should only be set
+ when <code>proxyType</code> is <code>manual</code>.
+ </tr>
+
+ <tr>
+ <td><code>socksUsername</code>
+ <td>string
+ <td>Defines the username used
+ when authenticating with a SOCKS proxy.
+ This property should only be set
+ when <code>proxyType</code> is <code>manual</code>.
+ </tr>
+
+ <tr>
+ <td><code>socksPassword</code>
+ <td>string
+ <td>Defines the password used
+ when authenticating with a SOCKS proxy.
+ This property should only be set
+ when <code>proxyType</code> is <code>manual</code>.
+ </tr>
+</table>
+
+## Firefox capabilities
+
+geckodriver also supports a capability named `moz:firefoxOptions`
+which takes Firefox-specific options.
+This must be a dictionary
+and may contain any of the following fields:
+
+<table>
+ <thead>
+ <tr>
+ <th>Name
+ <th>Type
+ <th>Description
+ </tr>
+ </thead>
+
+ <tr>
+ <td><code>binary</code>
+ <td>string
+ <td>Absolute path of the Firefox binary,
+ e.g. <code>/usr/bin/firefox</code>
+ or <code>/Applications/Firefox.app/Contents/MacOS/firefox</code>,
+ to select which custom browser binary to use.
+ If left undefined geckodriver will attempt
+ to deduce the default location of Firefox
+ on the current system.
+ </tr>
+
+ <tr>
+ <td><code>args</code>
+ <td>array&nbsp;of&nbsp;strings
+ <td>Command line arguments to pass to the Firefox binary.
+ These must include the leading <code>--</code> where required
+ e.g. <code>["--devtools"]</code>.
+ </tr>
+
+ <tr>
+ <td><code>profile</code>
+ <td>string
+ <td>Base64-encoded zip of a profile directory
+ to use as the profile for the Firefox instance.
+ This may be used to e.g. install extensions
+ or custom certificates.
+ By default, a new profile will be created in the system’s temporary folder.
+ The effective profile in use by the WebDriver session
+ is returned to the user in the `moz:profile` capability.
+ </tr>
+
+ <tr>
+ <td><code>log</code>
+ <td><a href=#log-object><code>log</code></a>&nbsp;object
+ <td>Logging options for Gecko.
+ </tr>
+
+ <tr>
+ <td><code>prefs</code>
+ <td><a href=#prefs-object><code>prefs</code></a>&nbsp;object
+ <td>Map of preference name to preference value, which can be a
+ string, a boolean or an integer.
+ </tr>
+</table>
+
+### `log` object
+
+<table>
+ <thead>
+ <tr>
+ <th>Name
+ <th>Type
+ <th>Description
+ </tr>
+ </thead>
+
+ <tr>
+ <td><code>level</code>
+ <td>string
+ <td>Set the level of verbosity in Gecko.
+ Available levels are <code>trace</code>,
+ <code>debug</code>, <code>config</code>,
+ <code>info</code>, <code>warn</code>,
+ <code>error</code>, and <code>fatal</code>.
+ If left undefined the default is <code>info</code>.
+ </tr>
+</table>
+
+### `prefs` object
+
+<table>
+ <thead>
+ <tr>
+ <th>Name
+ <th>Type
+ <th>Description
+ </tr>
+ </thead>
+
+ <tr>
+ <td><var>preference name</var>
+ <td>string, number, boolean
+ <td>One entry per preference to override.
+ </tr>
+</table>
+
+## Capabilities examples
+
+To select a specific Firefox binary
+and run it with a specific command-line flag,
+set a preference,
+and enable verbose logging:
+
+```js
+{
+ "capabilities": {
+ "alwaysMatch": {
+ "moz:firefoxOptions": {
+ "binary": "/usr/local/firefox/bin/firefox",
+ "args": ["--no-remote"],
+ "prefs": {
+ "dom.ipc.processCount": 8
+ },
+ "log": {
+ "level": "trace"
+ }
+ }
+ }
+ }
+}
+```
+
+## Usage
+
+Usage steps are [documented on MDN](https://developer.mozilla.org/en-US/docs/Mozilla/QA/Marionette/WebDriver),
+but how you invoke geckodriver largely depends on your use case.
+
+### Selenium
+
+If you are using geckodriver through [Selenium](http://seleniumhq.org/),
+you must ensure that you have version 3.3.1 or greater.
+Because geckodriver implements the [W3C WebDriver standard](https://w3c.github.io/webdriver/webdriver-spec.html)
+and not the same Selenium wire protocol older drivers are using,
+you may experience incompatibilities and migration problems
+when making the switch from FirefoxDriver to geckodriver.
+
+Generally speaking, Selenium 3 enabled geckodriver
+as the default WebDriver implementation for Firefox.
+With the release of Firefox 47, FirefoxDriver had to be discontinued
+for its lack of support for the [new multi-processing architecture in Gecko](https://developer.mozilla.org/en-US/Firefox/Multiprocess_Firefox).
+
+Selenium client bindings will pick up the _geckodriver_ binary executable
+from your [system’s `PATH` environmental variable](https://en.wikipedia.org/wiki/PATH_(variable))
+unless you override it by setting the `webdriver.gecko.driver`
+[Java VM system property](http://docs.oracle.com/javase/tutorial/essential/environment/sysprop.html):
+
+```java
+System.setProperty("webdriver.gecko.driver", "/home/user/bin");
+```
+
+Or by passing it as a flag to the [java(1)](http://www.manpagez.com/man/1/java/) launcher:
+
+ % java -Dwebdriver.gecko.driver=/home/user/bin YourApplication
+
+Your milage with this approach may vary
+based on which programming language bindings you are using.
+It is in any case generally the case that geckodriver will be picked up
+if it is available on the system path.
+In a bash compatible shell,
+you can make other programs aware of its location
+by exporting or setting the `PATH` variable:
+
+ % export PATH=$PATH:/home/user/bin
+ % whereis geckodriver
+ geckodriver: /home/user/bin/geckodriver
+
+On Window systems you can change the system path
+by right-clicking **My Computer** and choosing **Properties**.
+In the dialogue that appears, navigate
+**Advanced** → **Environmental Variables** → **Path**.
+
+Or in the Windows console window:
+
+ $ set PATH=%PATH%;C:\bin\geckodriver
+
+### Standalone
+
+Since geckodriver is a separate HTTP server
+that is a complete remote end implementation
+of [WebDriver](https://w3c.github.io/webdriver/webdriver-spec.html),
+it is possible to avoid using the Selenium remote server
+if you have no requirements
+to distribute processes across a matrix of systems.
+
+Given a W3C WebDriver conforming client library (or _local end_)
+you may interact with the geckodriver HTTP server
+as if you were speaking to any Selenium server.
+
+Using [curl(1)](http://www.manpagez.com/man/1/curl/):
+
+ % geckodriver &
+ [1] 16010
+ % 1491834109194 geckodriver INFO Listening on 127.0.0.1:4444
+ % curl -d '{"capabilities": {"alwaysMatch": {"acceptInsecureCerts": true}}}' http://localhost:4444/session
+ {"sessionId":"d4605710-5a4e-4d64-a52a-778bb0c31e00","value":{"XULappId":"{ec8030f7-c20a-464f-9b0e-13a3a9e97384}","acceptSslCerts":false,"appBuildId":"20160913030425","browserName":"firefox","browserVersion":"51.0a1","command_id":1,"platform":"LINUX","platformName":"linux","platformVersion":"4.9.0-1-amd64","processId":17474,"proxy":{},"raisesAccessibilityExceptions":false,"rotatable":false,"specificationLevel":0,"takesElementScreenshot":true,"takesScreenshot":true,"version":"51.0a1"}}
+ % curl -d '{"url": "https://mozilla.org"}' http://localhost:4444/session/d4605710-5a4e-4d64-a52a-778bb0c31e00/url
+ {}
+ % curl http://localhost:4444/session/d4605710-5a4e-4d64-a52a-778bb0c31e00/url
+ {"value":"https://www.mozilla.org/en-US/"
+ % curl -X DELETE http://localhost:4444/session/d4605710-5a4e-4d64-a52a-778bb0c31e00
+ {}
+ % fg
+ geckodriver
+ ^C
+ %
+
+Using the Python [wdclient](https://github.com/w3c/wpt-tools/tree/master/webdriver) library:
+
+```py
+import webdriver
+
+with webdriver.Session("127.0.0.1", 4444) as session:
+ session.url = "https://mozilla.org"
+ print "The current URL is %s" % session.url
+```
+
+And to run:
+
+ % geckodriver &
+ [1] 16054
+ % python example.py
+ 1491835308354 geckodriver INFO Listening on 127.0.0.1:4444
+ The current URL is https://www.mozilla.org/en-US/
+ % fg
+ geckodriver
+ ^C
+ %
+
+## Flags
+
+#### <code>-b <var>BINARY</var></code>/<code>--binary <var>BINARY</var></code>
+
+Path to the Firefox binary to use.
+By default geckodriver tries to find and use
+the system installation of Firefox,
+but that behaviour can be changed by using this option.
+Note that the `binary` capability of the `moz:firefoxOptions` object
+that is passed when [creating a new session](https://w3c.github.io/webdriver/webdriver-spec.html#new-session)
+will override this option.
+
+On Linux systems it will use the first _firefox_ binary
+found by searching the `PATH` environmental variable,
+which is roughly equivalent to calling [whereis(1)](http://www.manpagez.com/man/1/whereis/)
+and extracting the second column:
+
+ % whereis firefox
+ firefox: /usr/bin/firefox /usr/local/firefox
+
+On macOS, the binary is found by looking for the first _firefox-bin_ binary
+in the same fashion as on Linux systems.
+This means it is possible to also use `PATH`
+to control where geckodriver should find Firefox on macOS.
+It will then look for _/Applications/Firefox.app_.
+
+On Windows systems, geckodriver looks for the system Firefox
+by scanning the Windows registry.
+
+#### `--connect-existing`
+
+Connecting to an existing Firefox instance.
+The instance must have Marionette enabled.
+
+To enable the Marionette remote protocol
+you can pass the `--marionette` flag to Firefox,
+or (in Firefox 54 or greater)
+flip the `marionette.enabled` preference in _about:config_ at runtime.
+
+#### <code>--host <var>HOST</var></code>
+
+Host to use for the WebDriver server.
+Defaults to 127.0.0.1.
+
+#### <code>--log <var>LEVEL</var></code>
+
+Set the Gecko and geckodriver log level.
+Possible values are `fatal`, `error`, `warn`, `info`, `config`, `debug`, and `trace`.
+
+#### <code>--marionette-port <var>PORT</var></code>
+
+Port to use for connecting to the Marionette remote protocol.
+By default it will pick a free port assigned by the system.
+
+#### <code>-p <var>PORT</var></code>/<code>--port <var>PORT</var></code>
+
+Port to use for the WebDriver server.
+Defaults to 4444.
+
+A helpful trick is that it is possible to bind to 0
+to get the system to assign a free port.
+
+#### <code>-v<var>[v]</var></code>
+
+Increases the logging verbosity by to debug level when passing a single `-v`,
+or to trace level if `-vv` is passed.
+This is analogous to passing `--log debug` and `--log trace`, respectively.
+
+## Building
+
+geckodriver is written in [Rust], a systems programming language from [Mozilla].
+Crucially, it relies on the [webdriver crate] to provide the HTTPD
+and do most of the heavy lifting of marshalling the WebDriver protocol.
+geckodriver translates WebDriver [commands], [responses], and [errors]
+to the [Marionette protocol],
+and acts as a proxy between [WebDriver] and [Marionette].
+
+In order to build this program, you will need the [Rust compiler toolchain].
+
+To build the project for release,
+ensure you compile with optimisations
+to get the best performance:
+
+ % cargo build --release
+
+Or if you want a non-optimised binary for debugging:
+
+ % cargo build
+
+[Rust]: https://www.rust-lang.org/
+[Mozilla]: https://www.mozilla.org/en-US/
+[webdriver crate]: https://github.com/mozilla/webdriver-rust
+[commands]: https://docs.rs/webdriver/0.25.0/webdriver/command/index.html
+[responses]: https://docs.rs/webdriver/0.25.0/webdriver/response/index.html
+[errors]: https://docs.rs/webdriver/0.25.0/webdriver/error/enum.ErrorStatus.html
+[Marionette protocol]: https://developer.mozilla.org/en-US/docs/Mozilla/QA/Marionette/Protocol
+[WebDriver]: https://w3c.github.io/webdriver/webdriver-spec.html
+[Marionette]: http://searchfox.org/mozilla-central/source/testing/marionette/README
+[Rust compiler toolchain]: https://rustup.rs/
diff --git a/build.sh b/build.sh
new file mode 100644
index 0000000..8bf54e9
--- /dev/null
+++ b/build.sh
@@ -0,0 +1,169 @@
+set -ex
+
+print_versions() {
+ rustc -V
+ cargo -V
+ cc --version
+}
+
+rustup_install() {
+ export PATH="$PATH:$HOME/.cargo/bin"
+ curl https://sh.rustup.rs -sSf | sh -s -- -y --default-toolchain=$1
+}
+
+# Add provided target to current Rust toolchain if it is not already
+# the default or installed.
+rustup_target_add() {
+ if ! rustup target list | grep -E "$1 \((default|installed)\)"
+ then
+ rustup target add $1
+ fi
+}
+
+setup_docker() {
+ apt-get -qq -y install zip
+ cd /mnt/host
+}
+
+mingw_i686_install() {
+ curl https://static.rust-lang.org/dist/rust-mingw-nightly-i686-pc-windows-gnu.tar.gz \
+ | tar xzvf - -C /tmp
+ /tmp/rust-mingw-nightly-i686-pc-windows-gnu/install.sh --prefix=`rustc --print sysroot`
+}
+
+# Configure rustc target for cross compilation. Provided with a build
+# target, this will determine which linker to use for cross compilation.
+cargo_config() {
+ local prefix
+
+ case "$TARGET" in
+ aarch64-unknown-linux-gnu)
+ prefix=aarch64-linux-gnu
+ ;;
+ arm*-unknown-linux-gnueabihf)
+ prefix=arm-linux-gnueabihf
+ ;;
+ arm-unknown-linux-gnueabi)
+ prefix=arm-linux-gnueabi
+ ;;
+ mipsel-unknown-linux-musl)
+ prefix=mipsel-openwrt-linux
+ ;;
+ x86_64-pc-windows-gnu)
+ prefix=x86_64-w64-mingw32
+ ;;
+ i686-pc-windows-gnu)
+ prefix=i686-w64-mingw32
+ ;;
+ *)
+ return
+ ;;
+ esac
+
+ mkdir -p ~/.cargo
+ cat >~/.cargo/config <<EOF
+[target.$TARGET]
+linker = "$prefix-gcc"
+EOF
+
+ cat ~/.cargo/config
+}
+
+# Build current crate for given target and print file type information.
+# If the second argument is set, a release build will be made.
+cargo_build() {
+ local mode
+ if [ -z "$2" ]
+ then
+ mode=debug
+ else
+ mode=release
+ fi
+
+ local modeflag
+ if [ "$mode" == "release" ]
+ then
+ modeflag=--release
+ fi
+
+ cargo build --target $1 $modeflag
+
+ file $(get_binary $1 $mode)
+}
+
+# Run current crate's tests if the current system supports it.
+cargo_test() {
+ if echo "$1" | grep -E "(i686|x86_64)-unknown-linux-(gnu|musl)|darwin"
+ then
+ cargo test --target $1
+ fi
+}
+
+# Returns relative path to binary
+# based on build target and type ("release"/"debug").
+get_binary() {
+ local ext
+ if [[ "$1" =~ "windows" ]]
+ then
+ ext=".exe"
+ fi
+ echo "target/$1/$2/geckodriver$ext"
+}
+
+# Create a compressed archive of the binary
+# for the given given git tag, build target, and build type.
+package_binary() {
+ local bin
+ bin=$(get_binary $2 $4)
+ cp $bin .
+
+ if [[ "$2" =~ "windows" ]]
+ then
+ filename="geckodriver-$1-$3.zip"
+ zip "$filename" geckodriver.exe
+ file "$filename"
+ else
+ filename="geckodriver-$1-$3.tar.gz"
+ tar zcvf "$filename" geckodriver
+ file "$filename"
+ fi
+ if [ ! -z "$DOCKER_IMAGE" ]
+ then
+ chown "$USER_ID:$GROUP_ID" "$filename"
+ fi
+}
+
+main() {
+ TOOLCHAIN=${TOOLCHAIN:=stable}
+
+ if [ ! -z "$DOCKER_IMAGE" ]
+ then
+ setup_docker
+ fi
+
+ rustup_install $TOOLCHAIN
+ print_versions
+ rustup_target_add $TARGET
+
+ # custom mingw component required
+ # when compiling on 32-bit windows
+ # see https://github.com/mozilla/geckodriver/pull/138#issuecomment-232139097
+ if [[ $TARGET == "i686-pc-windows-gnu" ]]
+ then
+ mingw_i686_install
+ fi
+
+ cargo_config $TARGET
+ cargo_build $TARGET
+ cargo_test $TARGET
+
+ # when something is tagged,
+ # also create a release build and package it
+ if [ ! -z "$TRAVIS_TAG" ]
+ then
+ cargo_build $TARGET 1
+ package_binary $TRAVIS_TAG $TARGET $NAME "release"
+ fi
+}
+
+main
diff --git a/ci.sh b/ci.sh
new file mode 100644
index 0000000..7a43e34
--- /dev/null
+++ b/ci.sh
@@ -0,0 +1,21 @@
+set -ex
+
+if [ ! -z "$DOCKER_IMAGE" ]
+then
+ tag="$DOCKER_IMAGE/latest"
+ docker build $DOCKER_IMAGE -t $tag
+ docker run \
+ -e USER="$USER" \
+ -e TARGET="$TARGET" \
+ -e TOOLCHAIN="$TOOLCHAIN" \
+ -e DOCKER_IMAGE="$DOCKER_IMAGE" \
+ -e NAME="$NAME" \
+ -e TRAVIS_TAG="$TRAVIS_TAG" \
+ -e USER_ID=$(id -u) \
+ -e GROUP_ID=$(id -g) \
+ -v $PWD:/mnt/host \
+ -i $tag \
+ bash -s -- < build.sh
+else
+ bash build.sh
+fi
diff --git a/i686-trusty/Dockerfile b/i686-trusty/Dockerfile
new file mode 100644
index 0000000..4d2480f
--- /dev/null
+++ b/i686-trusty/Dockerfile
@@ -0,0 +1,4 @@
+FROM ubuntu:15.10
+
+RUN apt-get -y update
+RUN apt-get -y install curl file gcc gcc-mingw-w64-i686 zip
diff --git a/moz.build b/moz.build
new file mode 100644
index 0000000..1873949
--- /dev/null
+++ b/moz.build
@@ -0,0 +1,8 @@
+# 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/.
+
+RUST_PROGRAMS += ["geckodriver"]
+
+with Files("**"):
+ BUG_COMPONENT = ("Testing", "Marionette")
diff --git a/src/capabilities.rs b/src/capabilities.rs
new file mode 100644
index 0000000..f7b26ca
--- /dev/null
+++ b/src/capabilities.rs
@@ -0,0 +1,515 @@
+use logging::LogLevel;
+use marionette::LogOptions;
+use mozprofile::preferences::Pref;
+use mozprofile::profile::Profile;
+use mozrunner::runner::platform::firefox_default_path;
+use mozversion::{self, firefox_version, Version};
+use regex::bytes::Regex;
+use rustc_serialize::base64::FromBase64;
+use rustc_serialize::json::Json;
+use std::collections::BTreeMap;
+use std::default::Default;
+use std::error::Error;
+use std::fs;
+use std::io::BufWriter;
+use std::io::Cursor;
+use std::io;
+use std::path::{Path, PathBuf};
+use std::process::{Command, Stdio};
+use std::str::{self, FromStr};
+use webdriver::capabilities::{BrowserCapabilities, Capabilities};
+use webdriver::error::{ErrorStatus, WebDriverError, WebDriverResult};
+use zip;
+
+/// Provides matching of `moz:firefoxOptions` and resolution of which Firefox
+/// binary to use.
+///
+/// `FirefoxCapabilities` is constructed with the fallback binary, should
+/// `moz:firefoxOptions` not contain a binary entry. This may either be the
+/// system Firefox installation or an override, for example given to the
+/// `--binary` flag of geckodriver.
+pub struct FirefoxCapabilities<'a> {
+ pub chosen_binary: Option<PathBuf>,
+ fallback_binary: Option<&'a PathBuf>,
+ version_cache: BTreeMap<PathBuf, String>,
+}
+
+
+impl<'a> FirefoxCapabilities<'a> {
+ pub fn new(fallback_binary: Option<&'a PathBuf>) -> FirefoxCapabilities<'a> {
+ FirefoxCapabilities {
+ chosen_binary: None,
+ fallback_binary: fallback_binary,
+ version_cache: BTreeMap::new(),
+ }
+ }
+
+ fn set_binary(&mut self, capabilities: &BTreeMap<String, Json>) {
+ self.chosen_binary = capabilities
+ .get("moz:firefoxOptions")
+ .and_then(|x| x.find("binary"))
+ .and_then(|x| x.as_string())
+ .map(|x| PathBuf::from(x))
+ .or_else(|| self.fallback_binary.map(|x| x.clone()))
+ .or_else(|| firefox_default_path())
+ }
+
+ fn version(&mut self) -> Option<String> {
+ if let Some(ref binary) = self.chosen_binary {
+ if let Some(value) = self.version_cache.get(binary) {
+ return Some((*value).clone());
+ }
+ debug!("Trying to read firefox version from ini files");
+ let rv = firefox_version(&*binary)
+ .ok()
+ .and_then(|x| x.version_string)
+ .or_else(|| {
+ debug!("Trying to read firefox version from binary");
+ self.version_from_binary(binary)
+ });
+ if let Some(ref version) = rv {
+ debug!("Found version {}", version);
+ self.version_cache
+ .insert(binary.clone(), version.clone());
+ } else {
+ debug!("Failed to get binary version");
+ }
+ rv
+ } else {
+ None
+ }
+ }
+
+ fn version_from_binary(&self, binary: &PathBuf) -> Option<String> {
+ let version_regexp = Regex::new(r#"\d+\.\d+(?:[a-z]\d+)?"#).expect("Error parsing version regexp");
+ let output = Command::new(binary)
+ .args(&["-version"])
+ .stdout(Stdio::piped())
+ .spawn()
+ .and_then(|child| child.wait_with_output())
+ .ok();
+
+ if let Some(x) = output {
+ version_regexp.captures(&*x.stdout)
+ .and_then(|captures| captures.get(0))
+ .and_then(|m| str::from_utf8(m.as_bytes()).ok())
+ .map(|x| x.into())
+ } else {
+ None
+ }
+ }
+}
+
+// TODO: put this in webdriver-rust
+fn convert_version_error(err: mozversion::Error) -> WebDriverError {
+ WebDriverError::new(
+ ErrorStatus::SessionNotCreated,
+ err.description().to_string())
+}
+
+impl<'a> BrowserCapabilities for FirefoxCapabilities<'a> {
+ fn init(&mut self, capabilities: &Capabilities) {
+ self.set_binary(capabilities);
+ }
+
+ fn browser_name(&mut self, _: &Capabilities) -> WebDriverResult<Option<String>> {
+ Ok(Some("firefox".into()))
+ }
+
+ fn browser_version(&mut self, _: &Capabilities) -> WebDriverResult<Option<String>> {
+ Ok(self.version())
+ }
+
+ fn platform_name(&mut self, _: &Capabilities) -> WebDriverResult<Option<String>> {
+ Ok(if cfg!(target_os = "windows") {
+ Some("windows".into())
+ } else if cfg!(target_os = "macos") {
+ Some("mac".into())
+ } else if cfg!(target_os = "linux") {
+ Some("linux".into())
+ } else {
+ None
+ })
+ }
+
+ fn accept_insecure_certs(&mut self, _: &Capabilities) -> WebDriverResult<bool> {
+ let version_str = self.version();
+ if let Some(x) = version_str {
+ Ok(try!(Version::from_str(&*x).or_else(|x| Err(convert_version_error(x)))).major >= 52)
+ } else {
+ Ok(false)
+ }
+ }
+
+ fn compare_browser_version(&mut self,
+ version: &str,
+ comparison: &str)
+ -> WebDriverResult<bool> {
+ try!(Version::from_str(version).or_else(|x| Err(convert_version_error(x))))
+ .matches(comparison)
+ .or_else(|x| Err(convert_version_error(x)))
+ }
+
+ fn accept_proxy(&mut self, _: &Capabilities, _: &Capabilities) -> WebDriverResult<bool> {
+ Ok(true)
+ }
+
+ fn validate_custom(&self, name: &str, value: &Json) -> WebDriverResult<()> {
+ if !name.starts_with("moz:") {
+ return Ok(())
+ }
+ match name {
+ "moz:firefoxOptions" => {
+ let data = try_opt!(value.as_object(),
+ ErrorStatus::InvalidArgument,
+ "moz:firefoxOptions is not an object");
+ for (key, value) in data.iter() {
+ match &**key {
+ "binary" => {
+ if !value.is_string() {
+ return Err(WebDriverError::new(
+ ErrorStatus::InvalidArgument,
+ "binary path is not a string"));
+ }
+ },
+ "args" => {
+ if !try_opt!(value.as_array(),
+ ErrorStatus::InvalidArgument,
+ "args is not an array")
+ .iter()
+ .all(|value| value.is_string()) {
+ return Err(WebDriverError::new(
+ ErrorStatus::InvalidArgument,
+ "args entry is not a string"));
+ }
+ },
+ "profile" => {
+ if !value.is_string() {
+ return Err(WebDriverError::new(
+ ErrorStatus::InvalidArgument,
+ "profile is not a string"));
+ }
+ },
+ "log" => {
+ let log_data = try_opt!(value.as_object(),
+ ErrorStatus::InvalidArgument,
+ "log value is not an object");
+ for (log_key, log_value) in log_data.iter() {
+ match &**log_key {
+ "level" => {
+ let level = try_opt!(log_value.as_string(),
+ ErrorStatus::InvalidArgument,
+ "log level is not a string");
+ if LogLevel::from_str(level).is_err() {
+ return Err(WebDriverError::new(
+ ErrorStatus::InvalidArgument,
+ format!("{} is not a valid log level",
+ level)))
+ }
+ }
+ x => return Err(WebDriverError::new(
+ ErrorStatus::InvalidArgument,
+ format!("Invalid log field {}", x)))
+ }
+ }
+ },
+ "prefs" => {
+ let prefs_data = try_opt!(value.as_object(),
+ ErrorStatus::InvalidArgument,
+ "prefs value is not an object");
+ if !prefs_data.values()
+ .all(|x| x.is_string() || x.is_i64() || x.is_u64() || x.is_boolean()) {
+ return Err(WebDriverError::new(
+ ErrorStatus::InvalidArgument,
+ "Preference values not all string or integer or boolean"));
+ }
+ }
+ x => return Err(WebDriverError::new(
+ ErrorStatus::InvalidArgument,
+ format!("Invalid moz:firefoxOptions field {}", x)))
+ }
+ }
+ }
+ _ => return Err(WebDriverError::new(ErrorStatus::InvalidArgument,
+ format!("Unrecognised option {}", name)))
+ }
+ Ok(())
+ }
+
+ fn accept_custom(&mut self, _: &str, _: &Json, _: &Capabilities) -> WebDriverResult<bool> {
+ Ok(true)
+ }
+}
+
+/// Rust representation of `moz:firefoxOptions`.
+///
+/// Calling `FirefoxOptions::from_capabilities(binary, capabilities)` causes
+/// the encoded profile, the binary arguments, log settings, and additional
+/// preferences to be checked and unmarshaled from the `moz:firefoxOptions`
+/// JSON Object into a Rust representation.
+#[derive(Default)]
+pub struct FirefoxOptions {
+ pub binary: Option<PathBuf>,
+ pub profile: Option<Profile>,
+ pub args: Option<Vec<String>>,
+ pub log: LogOptions,
+ pub prefs: Vec<(String, Pref)>,
+}
+
+impl FirefoxOptions {
+ pub fn new() -> FirefoxOptions {
+ Default::default()
+ }
+
+ pub fn from_capabilities(binary_path: Option<PathBuf>,
+ matched: &mut Capabilities)
+ -> WebDriverResult<FirefoxOptions> {
+ let mut rv = FirefoxOptions::new();
+ rv.binary = binary_path;
+
+ if let Some(json) = matched.remove("moz:firefoxOptions") {
+ let options = try!(json.as_object()
+ .ok_or(WebDriverError::new(ErrorStatus::InvalidArgument,
+ "'moz:firefoxOptions' \
+ capability is not an object")));
+
+ rv.profile = try!(FirefoxOptions::load_profile(&options));
+ rv.args = try!(FirefoxOptions::load_args(&options));
+ rv.log = try!(FirefoxOptions::load_log(&options));
+ rv.prefs = try!(FirefoxOptions::load_prefs(&options));
+ }
+
+ Ok(rv)
+ }
+
+ fn load_profile(options: &Capabilities) -> WebDriverResult<Option<Profile>> {
+ if let Some(profile_json) = options.get("profile") {
+ let profile_base64 =
+ try!(profile_json
+ .as_string()
+ .ok_or(WebDriverError::new(ErrorStatus::UnknownError,
+ "Profile is not a string")));
+ let profile_zip = &*try!(profile_base64.from_base64());
+
+ // Create an emtpy profile directory
+ let profile = try!(Profile::new(None));
+ try!(unzip_buffer(profile_zip,
+ profile
+ .temp_dir
+ .as_ref()
+ .expect("Profile doesn't have a path")
+ .path()));
+
+ Ok(Some(profile))
+ } else {
+ Ok(None)
+ }
+ }
+
+ fn load_args(options: &Capabilities) -> WebDriverResult<Option<Vec<String>>> {
+ if let Some(args_json) = options.get("args") {
+ let args_array = try!(args_json
+ .as_array()
+ .ok_or(WebDriverError::new(ErrorStatus::UnknownError,
+ "Arguments were not an \
+ array")));
+ let args = try!(args_array
+ .iter()
+ .map(|x| x.as_string().map(|x| x.to_owned()))
+ .collect::<Option<Vec<String>>>()
+ .ok_or(WebDriverError::new(ErrorStatus::UnknownError,
+ "Arguments entries were not all \
+ strings")));
+ Ok(Some(args))
+ } else {
+ Ok(None)
+ }
+ }
+
+ fn load_log(options: &Capabilities) -> WebDriverResult<LogOptions> {
+ if let Some(json) = options.get("log") {
+ let log = try!(json.as_object()
+ .ok_or(WebDriverError::new(ErrorStatus::InvalidArgument,
+ "Log section is not an object")));
+
+ let level = match log.get("level") {
+ Some(json) => {
+ let s = try!(json.as_string()
+ .ok_or(WebDriverError::new(ErrorStatus::InvalidArgument,
+ "Log level is not a string")));
+ Some(try!(LogLevel::from_str(s)
+ .ok()
+ .ok_or(WebDriverError::new(ErrorStatus::InvalidArgument,
+ "Log level is unknown"))))
+ }
+ None => None,
+ };
+
+ Ok(LogOptions { level: level })
+
+ } else {
+ Ok(Default::default())
+ }
+ }
+
+ pub fn load_prefs(options: &Capabilities) -> WebDriverResult<Vec<(String, Pref)>> {
+ if let Some(prefs_data) = options.get("prefs") {
+ let prefs = try!(prefs_data
+ .as_object()
+ .ok_or(WebDriverError::new(ErrorStatus::UnknownError,
+ "Prefs were not an object")));
+ let mut rv = Vec::with_capacity(prefs.len());
+ for (key, value) in prefs.iter() {
+ rv.push((key.clone(), try!(pref_from_json(value))));
+ }
+ Ok(rv)
+ } else {
+ Ok(vec![])
+ }
+ }
+}
+
+fn pref_from_json(value: &Json) -> WebDriverResult<Pref> {
+ match value {
+ &Json::String(ref x) => Ok(Pref::new(x.clone())),
+ &Json::I64(x) => Ok(Pref::new(x)),
+ &Json::U64(x) => Ok(Pref::new(x as i64)),
+ &Json::Boolean(x) => Ok(Pref::new(x)),
+ _ => Err(WebDriverError::new(ErrorStatus::UnknownError,
+ "Could not convert pref value to string, boolean, or integer"))
+ }
+}
+
+fn unzip_buffer(buf: &[u8], dest_dir: &Path) -> WebDriverResult<()> {
+ let reader = Cursor::new(buf);
+ let mut zip = try!(zip::ZipArchive::new(reader).map_err(|_| {
+ WebDriverError::new(ErrorStatus::UnknownError, "Failed to unzip profile")
+ }));
+
+ for i in 0..zip.len() {
+ let mut file = try!(zip.by_index(i).map_err(|_| {
+ WebDriverError::new(ErrorStatus::UnknownError, "Processing profile zip file failed")
+ }));
+ let unzip_path = {
+ let name = file.name();
+ let is_dir = name.ends_with("/");
+ let rel_path = Path::new(name);
+ let dest_path = dest_dir.join(rel_path);
+
+ {
+ let create_dir = if is_dir {
+ Some(dest_path.as_path())
+ } else {
+ dest_path.parent()
+ };
+ if let Some(dir) = create_dir {
+ if !dir.exists() {
+ debug!("Creating profile directory tree {}", dir.to_string_lossy());
+ try!(fs::create_dir_all(dir));
+ }
+ }
+ }
+
+ if is_dir {
+ None
+ } else {
+ Some(dest_path)
+ }
+ };
+
+ if let Some(unzip_path) = unzip_path {
+ debug!("Extracting profile to {}", unzip_path.to_string_lossy());
+ let dest = try!(fs::File::create(unzip_path));
+ if file.size() > 0 {
+ let mut writer = BufWriter::new(dest);
+ try!(io::copy(&mut file, &mut writer));
+ }
+ }
+ }
+
+ Ok(())
+}
+
+#[cfg(test)]
+mod tests {
+ extern crate mozprofile;
+ extern crate rustc_serialize;
+
+ use self::mozprofile::preferences::Pref;
+ use self::rustc_serialize::base64::{CharacterSet, Config, Newline, ToBase64};
+ use self::rustc_serialize::json::Json;
+ use super::FirefoxOptions;
+ use marionette::MarionetteHandler;
+ use std::collections::BTreeMap;
+ use std::default::Default;
+ use std::fs::File;
+ use std::io::Read;
+
+ use webdriver::capabilities::Capabilities;
+
+ fn example_profile() -> Json {
+ let mut profile_data = Vec::with_capacity(1024);
+ let mut profile = File::open("src/tests/profile.zip").unwrap();
+ profile.read_to_end(&mut profile_data).unwrap();
+ let base64_config = Config {
+ char_set: CharacterSet::Standard,
+ newline: Newline::LF,
+ pad: true,
+ line_length: None,
+ };
+ Json::String(profile_data.to_base64(base64_config))
+ }
+
+ fn make_options(firefox_opts: Capabilities) -> FirefoxOptions {
+ let mut caps = Capabilities::new();
+ caps.insert("moz:firefoxOptions".into(), Json::Object(firefox_opts));
+ let binary = None;
+ FirefoxOptions::from_capabilities(binary, &mut caps).unwrap()
+ }
+
+ #[test]
+ fn test_profile() {
+ let encoded_profile = example_profile();
+ let mut firefox_opts = Capabilities::new();
+ firefox_opts.insert("profile".into(), encoded_profile);
+
+ let opts = make_options(firefox_opts);
+ let mut profile = opts.profile.unwrap();
+ let prefs = profile.user_prefs().unwrap();
+
+ println!("{:#?}", prefs.prefs);
+
+ assert_eq!(prefs.get("startup.homepage_welcome_url"),
+ Some(&Pref::new("data:text/html,PASS")));
+ }
+
+ #[test]
+ fn test_prefs() {
+ let encoded_profile = example_profile();
+ let mut prefs: BTreeMap<String, Json> = BTreeMap::new();
+ prefs.insert("browser.display.background_color".into(),
+ Json::String("#00ff00".into()));
+
+ let mut firefox_opts = Capabilities::new();
+ firefox_opts.insert("profile".into(), encoded_profile);
+ firefox_opts.insert("prefs".into(), Json::Object(prefs));
+
+ let opts = make_options(firefox_opts);
+ let mut profile = opts.profile.unwrap();
+
+ let handler = MarionetteHandler::new(Default::default());
+ handler
+ .set_prefs(2828, &mut profile, true, opts.prefs)
+ .unwrap();
+
+ let prefs_set = profile.user_prefs().unwrap();
+ println!("{:#?}", prefs_set.prefs);
+
+ assert_eq!(prefs_set.get("startup.homepage_welcome_url"),
+ Some(&Pref::new("data:text/html,PASS")));
+ assert_eq!(prefs_set.get("browser.display.background_color"),
+ Some(&Pref::new("#00ff00")));
+ assert_eq!(prefs_set.get("marionette.defaultPrefs.port"),
+ Some(&Pref::new(2828)));
+ }
+}
diff --git a/src/logging.rs b/src/logging.rs
new file mode 100644
index 0000000..2cbff7e
--- /dev/null
+++ b/src/logging.rs
@@ -0,0 +1,158 @@
+use std::fmt;
+use std::io;
+use std::str::FromStr;
+use std::sync::atomic::AtomicBool;
+use std::sync::atomic::Ordering::SeqCst;
+
+use chrono::{DateTime, Local};
+use slog;
+use slog::DrainExt;
+use slog_atomic::{AtomicSwitch, AtomicSwitchCtrl};
+use slog_stream::{stream, Format, Streamer};
+use slog::Level as SlogLevel;
+use slog::{LevelFilter, Logger};
+use slog::{OwnedKeyValueList, Record};
+use slog_stdlog;
+
+lazy_static! {
+ static ref ATOMIC_DRAIN: AtomicSwitchCtrl<io::Error> = AtomicSwitch::new(
+ slog::Discard.map_err(|_| io::Error::new(io::ErrorKind::Other, "should not happen"))
+ ).ctrl();
+ static ref FIRST_RUN: AtomicBool = AtomicBool::new(true);
+}
+
+static DEFAULT_LEVEL: &'static LogLevel = &LogLevel::Info;
+
+/// Logger levels from [Log.jsm]
+/// (https://developer.mozilla.org/en/docs/Mozilla/JavaScript_code_modules/Log.jsm).
+#[derive(Debug, Clone)]
+pub enum LogLevel {
+ Fatal,
+ Error,
+ Warn,
+ Info,
+ Config,
+ Debug,
+ Trace,
+}
+
+impl fmt::Display for LogLevel {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ let s = match *self {
+ LogLevel::Fatal => "FATAL",
+ LogLevel::Error => "ERROR",
+ LogLevel::Warn => "WARN",
+ LogLevel::Info => "INFO",
+ LogLevel::Config => "CONFIG",
+ LogLevel::Debug => "DEBUG",
+ LogLevel::Trace => "TRACE",
+ };
+ write!(f, "{}", s)
+ }
+}
+
+impl FromStr for LogLevel {
+ type Err = ();
+
+ fn from_str(s: &str) -> Result<LogLevel, ()> {
+ match s {
+ "fatal" => Ok(LogLevel::Fatal),
+ "error" => Ok(LogLevel::Error),
+ "warn" => Ok(LogLevel::Warn),
+ "info" => Ok(LogLevel::Info),
+ "config" => Ok(LogLevel::Config),
+ "debug" => Ok(LogLevel::Debug),
+ "trace" => Ok(LogLevel::Trace),
+ _ => Err(()),
+ }
+ }
+}
+
+trait ToSlogLevel {
+ fn to_slog(&self) -> SlogLevel;
+}
+
+impl ToSlogLevel for LogLevel {
+ fn to_slog(&self) -> SlogLevel {
+ match *self {
+ LogLevel::Fatal => SlogLevel::Critical,
+ LogLevel::Error => SlogLevel::Error,
+ LogLevel::Warn => SlogLevel::Warning,
+ LogLevel::Info => SlogLevel::Info,
+ LogLevel::Config | LogLevel::Debug => SlogLevel::Debug,
+ LogLevel::Trace => SlogLevel::Trace,
+ }
+ }
+}
+
+trait ToGeckoLevel {
+ fn to_gecko(&self) -> LogLevel;
+}
+
+impl ToGeckoLevel for SlogLevel {
+ fn to_gecko(&self) -> LogLevel {
+ match *self {
+ SlogLevel::Critical => LogLevel::Fatal,
+ SlogLevel::Error => LogLevel::Error,
+ SlogLevel::Warning => LogLevel::Warn,
+ SlogLevel::Info => LogLevel::Info,
+ SlogLevel::Debug => LogLevel::Debug,
+ SlogLevel::Trace => LogLevel::Trace,
+ }
+ }
+}
+
+/// Initialise logger if it has not been already. The provided `level`
+/// filters out log records below this granularity.
+pub fn init(level: &Option<LogLevel>) {
+ let effective_level = level.as_ref().unwrap_or(DEFAULT_LEVEL);
+
+ let drain = filtered_gecko_log(&effective_level);
+ ATOMIC_DRAIN.set(drain);
+
+ let first_run = FIRST_RUN.load(SeqCst);
+ FIRST_RUN.store(false, SeqCst);
+ if first_run {
+ let log = Logger::root(ATOMIC_DRAIN.drain().fuse(), o!());
+ slog_stdlog::set_logger(log.clone()).unwrap();
+ }
+}
+
+fn filtered_gecko_log(level: &LogLevel) -> LevelFilter<Streamer<io::Stderr, GeckoFormat>> {
+ let io = stream(io::stderr(), GeckoFormat {});
+ slog::level_filter(level.to_slog(), io)
+}
+
+struct GeckoFormat;
+
+impl Format for GeckoFormat {
+ fn format(&self, io: &mut io::Write, record: &Record, _: &OwnedKeyValueList) -> io::Result<()> {
+ // TODO(ato): Quite sure this is the wrong way to filter records with slog,
+ // but I do not comprehend how slog works.
+ let module = record.module();
+ if module.starts_with("geckodriver") || module.starts_with("webdriver") {
+ let ts = format_ts(Local::now());
+ let level = record.level().to_gecko();
+ let _ = try!(write!(io, "{}\t{}\t{}\t{}\n", ts, module, level, record.msg()));
+ }
+ Ok(())
+ }
+}
+
+/// Produces a 13-digit Unix Epoch timestamp similar to Gecko.
+fn format_ts(ts: DateTime<Local>) -> String {
+ format!("{}{:03}", ts.timestamp(), ts.timestamp_subsec_millis())
+}
+
+#[cfg(test)]
+mod tests {
+ use chrono::Local;
+ use super::format_ts;
+
+ #[test]
+ fn test_format_ts() {
+ let ts = Local::now();
+ let s = format_ts(ts);
+ assert_eq!(s.len(), 13);
+ }
+}
diff --git a/src/main.rs b/src/main.rs
new file mode 100644
index 0000000..518b50e
--- /dev/null
+++ b/src/main.rs
@@ -0,0 +1,177 @@
+extern crate chrono;
+#[macro_use]
+extern crate clap;
+#[macro_use]
+extern crate lazy_static;
+extern crate hyper;
+extern crate mozprofile;
+extern crate mozrunner;
+extern crate mozversion;
+extern crate regex;
+extern crate rustc_serialize;
+#[macro_use]
+extern crate slog;
+extern crate slog_atomic;
+extern crate slog_stdlog;
+extern crate slog_stream;
+extern crate zip;
+extern crate webdriver;
+
+#[macro_use]
+extern crate log;
+
+use std::borrow::ToOwned;
+use std::io::Write;
+use std::net::{SocketAddr, IpAddr};
+use std::path::PathBuf;
+use std::str::FromStr;
+
+use clap::{App, Arg};
+
+macro_rules! try_opt {
+ ($expr:expr, $err_type:expr, $err_msg:expr) => ({
+ match $expr {
+ Some(x) => x,
+ None => return Err(WebDriverError::new($err_type, $err_msg))
+ }
+ })
+}
+
+mod logging;
+mod prefs;
+mod marionette;
+mod capabilities;
+
+use logging::LogLevel;
+use marionette::{MarionetteHandler, MarionetteSettings, extension_routes};
+
+type ProgramResult = std::result::Result<(), (ExitCode, String)>;
+
+enum ExitCode {
+ Ok = 0,
+ Usage = 64,
+ Unavailable = 69,
+}
+
+fn app<'a, 'b>() -> App<'a, 'b> {
+ App::new(format!("geckodriver {}", crate_version!()))
+ .about("WebDriver implementation for Firefox.")
+ .arg(Arg::with_name("webdriver_host")
+ .long("host")
+ .value_name("HOST")
+ .help("Host ip to use for WebDriver server (default: 127.0.0.1)")
+ .takes_value(true))
+ .arg(Arg::with_name("webdriver_port")
+ .short("p")
+ .long("port")
+ .value_name("PORT")
+ .help("Port to use for WebDriver server (default: 4444)")
+ .takes_value(true)
+ .alias("webdriver-port"))
+ .arg(Arg::with_name("binary")
+ .short("b")
+ .long("binary")
+ .value_name("BINARY")
+ .help("Path to the Firefox binary")
+ .takes_value(true))
+ .arg(Arg::with_name("marionette_port")
+ .long("marionette-port")
+ .value_name("PORT")
+ .help("Port to use to connect to Gecko (default: random free port)")
+ .takes_value(true))
+ .arg(Arg::with_name("connect_existing")
+ .long("connect-existing")
+ .requires("marionette_port")
+ .help("Connect to an existing Firefox instance"))
+ .arg(Arg::with_name("verbosity")
+ .short("v")
+ .multiple(true)
+ .conflicts_with("log_level")
+ .help("Log level verbosity (-v for debug and -vv for trace level)"))
+ .arg(Arg::with_name("log_level")
+ .long("log")
+ .takes_value(true)
+ .value_name("LEVEL")
+ .possible_values(&["fatal", "error", "warn", "info", "config", "debug", "trace"])
+ .help("Set Gecko log level"))
+ .arg(Arg::with_name("version")
+ .short("V")
+ .long("version")
+ .help("Prints version and copying information"))
+}
+
+fn run() -> ProgramResult {
+ let matches = app().get_matches();
+
+ if matches.is_present("version") {
+ println!("geckodriver {}\n\n{}", crate_version!(),
+"The source code of this program is available at
+https://github.com/mozilla/geckodriver.
+
+This program is subject to the terms of the Mozilla Public License 2.0.
+You can obtain a copy of the license at https://mozilla.org/MPL/2.0/.");
+ return Ok(())
+ }
+
+ let host = matches.value_of("webdriver_host").unwrap_or("127.0.0.1");
+ let port = match u16::from_str(matches.value_of("webdriver_port")
+ .or(matches.value_of("webdriver_port_alias"))
+ .unwrap_or("4444")) {
+ Ok(x) => x,
+ Err(_) => return Err((ExitCode::Usage, "invalid WebDriver port".to_owned())),
+ };
+ let addr = match IpAddr::from_str(host) {
+ Ok(addr) => SocketAddr::new(addr, port),
+ Err(_) => return Err((ExitCode::Usage, "invalid host address".to_owned())),
+ };
+
+ let binary = matches.value_of("binary").map(|x| PathBuf::from(x));
+
+ let marionette_port = match matches.value_of("marionette_port") {
+ Some(x) => match u16::from_str(x) {
+ Ok(x) => Some(x),
+ Err(_) => return Err((ExitCode::Usage, "invalid Marionette port".to_owned())),
+ },
+ None => None
+ };
+
+ // overrides defaults in Gecko
+ // which are info for optimised builds
+ // and debug for debug builds
+ let log_level = if matches.is_present("log_level") {
+ LogLevel::from_str(matches.value_of("log_level").unwrap()).ok()
+ } else {
+ match matches.occurrences_of("verbosity") {
+ 0 => Some(LogLevel::Info),
+ 1 => Some(LogLevel::Debug),
+ _ => Some(LogLevel::Trace),
+ }
+ };
+ logging::init(&log_level);
+
+ let settings = MarionetteSettings {
+ port: marionette_port,
+ binary: binary,
+ connect_existing: matches.is_present("connect_existing"),
+ log_level: log_level,
+ };
+
+ let handler = MarionetteHandler::new(settings);
+ let listening = try!(webdriver::server::start(addr, handler, &extension_routes()[..])
+ .map_err(|err| (ExitCode::Unavailable, err.to_string())));
+ info!("Listening on {}", listening.socket);
+ Ok(())
+}
+
+fn main() {
+ let exit_code = match run() {
+ Ok(_) => ExitCode::Ok,
+ Err((exit_code, reason)) => {
+ error!("{}", reason);
+ exit_code
+ },
+ };
+
+ std::io::stdout().flush().unwrap();
+ std::process::exit(exit_code as i32);
+}
diff --git a/src/marionette.rs b/src/marionette.rs
new file mode 100644
index 0000000..b5ff181
--- /dev/null
+++ b/src/marionette.rs
@@ -0,0 +1,1645 @@
+use hyper::method::Method;
+use logging;
+use logging::LogLevel;
+use mozprofile::preferences::Pref;
+use mozprofile::profile::Profile;
+use mozrunner::runner::{Runner, FirefoxRunner};
+use regex::Captures;
+use rustc_serialize::json;
+use rustc_serialize::json::{Json, ToJson};
+use std::collections::BTreeMap;
+use std::error::Error;
+use std::io::Error as IoError;
+use std::io::ErrorKind;
+use std::io::prelude::*;
+use std::path::PathBuf;
+use std::io::Result as IoResult;
+use std::net::{TcpListener, TcpStream};
+use std::sync::Mutex;
+use std::thread::sleep;
+use std::time::Duration;
+use webdriver::capabilities::CapabilitiesMatching;
+use webdriver::command::{WebDriverCommand, WebDriverMessage, Parameters,
+ WebDriverExtensionCommand};
+use webdriver::command::WebDriverCommand::{
+ NewSession, DeleteSession, Status, Get, GetCurrentUrl,
+ GoBack, GoForward, Refresh, GetTitle, GetPageSource, GetWindowHandle,
+ GetWindowHandles, CloseWindow, SetWindowRect,
+ GetWindowRect, MaximizeWindow, FullscreenWindow, SwitchToWindow, SwitchToFrame,
+ SwitchToParentFrame, FindElement, FindElements,
+ FindElementElement, FindElementElements, GetActiveElement,
+ IsDisplayed, IsSelected, GetElementAttribute, GetElementProperty, GetCSSValue,
+ GetElementText, GetElementTagName, GetElementRect, IsEnabled,
+ ElementClick, ElementTap, ElementClear, ElementSendKeys,
+ ExecuteScript, ExecuteAsyncScript, GetCookies, GetNamedCookie, AddCookie,
+ DeleteCookies, DeleteCookie, GetTimeouts, SetTimeouts, DismissAlert,
+ AcceptAlert, GetAlertText, SendAlertText, TakeScreenshot, TakeElementScreenshot,
+ Extension, PerformActions, ReleaseActions};
+use webdriver::command::{
+ NewSessionParameters, GetParameters, WindowRectParameters, SwitchToWindowParameters,
+ SwitchToFrameParameters, LocatorParameters, JavascriptCommandParameters,
+ GetNamedCookieParameters, AddCookieParameters, TimeoutsParameters,
+ ActionsParameters, TakeScreenshotParameters};
+use webdriver::response::{CloseWindowResponse, Cookie, CookieResponse, ElementRectResponse,
+ NewSessionResponse, TimeoutsResponse, ValueResponse, WebDriverResponse,
+ WindowRectResponse};
+use webdriver::common::{
+ Date, Nullable, WebElement, FrameId, ELEMENT_KEY};
+use webdriver::error::{ErrorStatus, WebDriverError, WebDriverResult};
+use webdriver::server::{WebDriverHandler, Session};
+use webdriver::httpapi::{WebDriverExtensionRoute};
+
+use capabilities::{FirefoxCapabilities, FirefoxOptions};
+use prefs;
+
+const DEFAULT_HOST: &'static str = "localhost";
+
+pub fn extension_routes() -> Vec<(Method, &'static str, GeckoExtensionRoute)> {
+ return vec![(Method::Get, "/session/{sessionId}/moz/context", GeckoExtensionRoute::GetContext),
+ (Method::Post, "/session/{sessionId}/moz/context", GeckoExtensionRoute::SetContext),
+ (Method::Post,
+ "/session/{sessionId}/moz/xbl/{elementId}/anonymous_children",
+ GeckoExtensionRoute::XblAnonymousChildren),
+ (Method::Post,
+ "/session/{sessionId}/moz/xbl/{elementId}/anonymous_by_attribute",
+ GeckoExtensionRoute::XblAnonymousByAttribute),
+ (Method::Post, "/session/{sessionId}/moz/addon/install",
+ GeckoExtensionRoute::InstallAddon),
+ (Method::Post, "/session/{sessionId}/moz/addon/uninstall",
+ GeckoExtensionRoute::UninstallAddon)];
+}
+
+#[derive(Clone, PartialEq)]
+pub enum GeckoExtensionRoute {
+ GetContext,
+ SetContext,
+ XblAnonymousChildren,
+ XblAnonymousByAttribute,
+ InstallAddon,
+ UninstallAddon,
+}
+
+impl WebDriverExtensionRoute for GeckoExtensionRoute {
+ type Command = GeckoExtensionCommand;
+
+ fn command(&self,
+ captures: &Captures,
+ body_data: &Json)
+ -> WebDriverResult<WebDriverCommand<GeckoExtensionCommand>> {
+ let command = match self {
+ &GeckoExtensionRoute::GetContext => GeckoExtensionCommand::GetContext,
+ &GeckoExtensionRoute::SetContext => {
+ let parameters: GeckoContextParameters = try!(Parameters::from_json(&body_data));
+ GeckoExtensionCommand::SetContext(parameters)
+ }
+ &GeckoExtensionRoute::XblAnonymousChildren => {
+ let element_id = try!(captures.name("elementId")
+ .ok_or(WebDriverError::new(ErrorStatus::InvalidArgument,
+ "Missing elementId parameter")));
+ GeckoExtensionCommand::XblAnonymousChildren(element_id.as_str().into())
+ }
+ &GeckoExtensionRoute::XblAnonymousByAttribute => {
+ let element_id = try!(captures.name("elementId")
+ .ok_or(WebDriverError::new(ErrorStatus::InvalidArgument,
+ "Missing elementId parameter")));
+ let parameters: AttributeParameters = try!(Parameters::from_json(&body_data));
+ GeckoExtensionCommand::XblAnonymousByAttribute(element_id.as_str().into(),
+ parameters)
+ }
+ &GeckoExtensionRoute::InstallAddon => {
+ let parameters: AddonInstallParameters = try!(Parameters::from_json(&body_data));
+ GeckoExtensionCommand::InstallAddon(parameters)
+ }
+ &GeckoExtensionRoute::UninstallAddon => {
+ let parameters: AddonUninstallParameters = try!(Parameters::from_json(&body_data));
+ GeckoExtensionCommand::UninstallAddon(parameters)
+ }
+ };
+ Ok(WebDriverCommand::Extension(command))
+ }
+}
+
+#[derive(Clone, PartialEq)]
+pub enum GeckoExtensionCommand {
+ GetContext,
+ SetContext(GeckoContextParameters),
+ XblAnonymousChildren(WebElement),
+ XblAnonymousByAttribute(WebElement, AttributeParameters),
+ InstallAddon(AddonInstallParameters),
+ UninstallAddon(AddonUninstallParameters)
+}
+
+impl WebDriverExtensionCommand for GeckoExtensionCommand {
+ fn parameters_json(&self) -> Option<Json> {
+ match self {
+ &GeckoExtensionCommand::GetContext => None,
+ &GeckoExtensionCommand::SetContext(ref x) => Some(x.to_json()),
+ &GeckoExtensionCommand::XblAnonymousChildren(_) => None,
+ &GeckoExtensionCommand::XblAnonymousByAttribute(_, ref x) => Some(x.to_json()),
+ &GeckoExtensionCommand::InstallAddon(ref x) => Some(x.to_json()),
+ &GeckoExtensionCommand::UninstallAddon(ref x) => Some(x.to_json()),
+ }
+ }
+}
+
+#[derive(Clone, Debug, PartialEq)]
+enum GeckoContext {
+ Content,
+ Chrome,
+}
+
+impl ToJson for GeckoContext {
+ fn to_json(&self) -> Json {
+ match self {
+ &GeckoContext::Content => Json::String("content".to_owned()),
+ &GeckoContext::Chrome => Json::String("chrome".to_owned()),
+ }
+ }
+}
+
+#[derive(Clone, Debug, PartialEq)]
+pub struct GeckoContextParameters {
+ context: GeckoContext
+}
+
+impl Parameters for GeckoContextParameters {
+ fn from_json(body: &Json) -> WebDriverResult<GeckoContextParameters> {
+ let data = try!(body.as_object().ok_or(
+ WebDriverError::new(ErrorStatus::InvalidArgument,
+ "Message body was not an object")));
+ let context_value = try!(data.get("context").ok_or(
+ WebDriverError::new(ErrorStatus::InvalidArgument,
+ "Missing context key")));
+ let value = try!(context_value.as_string().ok_or(
+ WebDriverError::new(
+ ErrorStatus::InvalidArgument,
+ "context was not a string")));
+ let context = try!(match value {
+ "chrome" => Ok(GeckoContext::Chrome),
+ "content" => Ok(GeckoContext::Content),
+ _ => Err(WebDriverError::new(ErrorStatus::InvalidArgument,
+ format!("{} is not a valid context",
+ value)))
+ });
+ Ok(GeckoContextParameters {
+ context: context
+ })
+ }
+}
+
+impl ToMarionette for GeckoContextParameters {
+ fn to_marionette(&self) -> WebDriverResult<BTreeMap<String, Json>> {
+ let mut data = BTreeMap::new();
+ data.insert("value".to_owned(), self.context.to_json());
+ Ok(data)
+ }
+}
+
+impl ToJson for GeckoContextParameters {
+ fn to_json(&self) -> Json {
+ let mut data = BTreeMap::new();
+ data.insert("context".to_owned(), self.context.to_json());
+ Json::Object(data)
+ }
+}
+
+
+#[derive(Clone, Debug, PartialEq)]
+pub struct AttributeParameters {
+ name: String,
+ value: String
+}
+
+impl Parameters for AttributeParameters {
+ fn from_json(body: &Json) -> WebDriverResult<AttributeParameters> {
+ let data = try!(body.as_object().ok_or(
+ WebDriverError::new(ErrorStatus::InvalidArgument,
+ "Message body was not an object")));
+ let name = try!(try!(data.get("name").ok_or(
+ WebDriverError::new(ErrorStatus::InvalidArgument,
+ "Missing 'name' parameter"))).as_string().
+ ok_or(WebDriverError::new(ErrorStatus::InvalidArgument,
+ "'name' parameter is not a string")));
+ let value = try!(try!(data.get("value").ok_or(
+ WebDriverError::new(ErrorStatus::InvalidArgument,
+ "Missing 'value' parameter"))).as_string().
+ ok_or(WebDriverError::new(ErrorStatus::InvalidArgument,
+ "'value' parameter is not a string")));
+ Ok(AttributeParameters {
+ name: name.to_owned(),
+ value: value.to_owned(),
+ })
+ }
+}
+
+impl ToJson for AttributeParameters {
+ fn to_json(&self) -> Json {
+ let mut data = BTreeMap::new();
+ data.insert("name".to_owned(), self.name.to_json());
+ data.insert("value".to_owned(), self.value.to_json());
+ Json::Object(data)
+ }
+}
+
+impl ToMarionette for AttributeParameters {
+ fn to_marionette(&self) -> WebDriverResult<BTreeMap<String, Json>> {
+ let mut data = BTreeMap::new();
+ data.insert("using".to_owned(), "anon attribute".to_json());
+ let mut value = BTreeMap::new();
+ value.insert(self.name.to_owned(), self.value.to_json());
+ data.insert("value".to_owned(), Json::Object(value));
+ Ok(data)
+ }
+}
+
+#[derive(Clone, Debug, PartialEq)]
+pub struct AddonInstallParameters {
+ pub path: String,
+ pub temporary: bool
+}
+
+impl Parameters for AddonInstallParameters {
+ fn from_json(body: &Json) -> WebDriverResult<AddonInstallParameters> {
+ let data = try!(body.as_object().ok_or(
+ WebDriverError::new(ErrorStatus::InvalidArgument,
+ "Message body was not an object")));
+
+ let path = try_opt!(
+ try_opt!(data.get("path"),
+ ErrorStatus::InvalidArgument,
+ "Missing 'path' parameter").as_string(),
+ ErrorStatus::InvalidArgument,
+ "'path' is not a string").to_string();
+
+ let temporary = match data.get("temporary") {
+ Some(x) => try_opt!(x.as_boolean(),
+ ErrorStatus::InvalidArgument,
+ "Failed to convert 'temporary' to boolean"),
+ None => false
+ };
+
+ return Ok(AddonInstallParameters {
+ path: path,
+ temporary: temporary,
+ })
+ }
+}
+
+impl ToJson for AddonInstallParameters {
+ fn to_json(&self) -> Json {
+ let mut data = BTreeMap::new();
+ data.insert("path".to_string(), self.path.to_json());
+ data.insert("temporary".to_string(), self.temporary.to_json());
+ Json::Object(data)
+ }
+}
+
+impl ToMarionette for AddonInstallParameters {
+ fn to_marionette(&self) -> WebDriverResult<BTreeMap<String, Json>> {
+ let mut data = BTreeMap::new();
+ data.insert("path".to_string(), self.path.to_json());
+ data.insert("temporary".to_string(), self.temporary.to_json());
+ Ok(data)
+ }
+}
+
+#[derive(Clone, Debug, PartialEq)]
+pub struct AddonUninstallParameters {
+ pub id: String
+}
+
+impl Parameters for AddonUninstallParameters {
+ fn from_json(body: &Json) -> WebDriverResult<AddonUninstallParameters> {
+ let data = try!(body.as_object().ok_or(
+ WebDriverError::new(ErrorStatus::InvalidArgument,
+ "Message body was not an object")));
+
+ let id = try_opt!(
+ try_opt!(data.get("id"),
+ ErrorStatus::InvalidArgument,
+ "Missing 'id' parameter").as_string(),
+ ErrorStatus::InvalidArgument,
+ "'id' is not a string").to_string();
+
+ return Ok(AddonUninstallParameters {id: id})
+ }
+}
+
+impl ToJson for AddonUninstallParameters {
+ fn to_json(&self) -> Json {
+ let mut data = BTreeMap::new();
+ data.insert("id".to_string(), self.id.to_json());
+ Json::Object(data)
+ }
+}
+
+impl ToMarionette for AddonUninstallParameters {
+ fn to_marionette(&self) -> WebDriverResult<BTreeMap<String, Json>> {
+ let mut data = BTreeMap::new();
+ data.insert("id".to_string(), self.id.to_json());
+ Ok(data)
+ }
+}
+
+#[derive(Default)]
+pub struct LogOptions {
+ pub level: Option<LogLevel>,
+}
+
+#[derive(Default)]
+pub struct MarionetteSettings {
+ pub port: Option<u16>,
+ pub binary: Option<PathBuf>,
+ pub connect_existing: bool,
+
+ /// Optionally increase Marionette's verbosity by providing a log
+ /// level. The Gecko default is LogLevel::Info for optimised
+ /// builds and LogLevel::Debug for debug builds.
+ pub log_level: Option<LogLevel>,
+}
+
+pub struct MarionetteHandler {
+ connection: Mutex<Option<MarionetteConnection>>,
+ settings: MarionetteSettings,
+ browser: Option<FirefoxRunner>,
+ current_log_level: Option<LogLevel>,
+}
+
+impl MarionetteHandler {
+ pub fn new(settings: MarionetteSettings) -> MarionetteHandler {
+ MarionetteHandler {
+ connection: Mutex::new(None),
+ settings: settings,
+ browser: None,
+ current_log_level: None,
+ }
+ }
+
+ fn create_connection(&mut self,
+ session_id: &Option<String>,
+ new_session_parameters: &NewSessionParameters)
+ -> WebDriverResult<BTreeMap<String, Json>> {
+ let (options, capabilities) = {
+ let mut fx_capabilities = FirefoxCapabilities::new(self.settings.binary.as_ref());
+ let mut capabilities = try!(
+ try!(new_session_parameters
+ .match_browser(&mut fx_capabilities))
+ .ok_or(WebDriverError::new(
+ ErrorStatus::SessionNotCreated,
+ "Unable to find a matching set of capabilities")));
+
+ let options = try!(FirefoxOptions::from_capabilities(fx_capabilities.chosen_binary,
+ &mut capabilities));
+ (options, capabilities)
+ };
+
+ self.current_log_level = options.log.level.clone().or(self.settings.log_level.clone());
+ logging::init(&self.current_log_level);
+
+ let port = self.settings.port.unwrap_or(try!(get_free_port()));
+ if !self.settings.connect_existing {
+ try!(self.start_browser(port, options));
+ }
+
+ let mut connection = MarionetteConnection::new(port, session_id.clone());
+ try!(connection.connect());
+ self.connection = Mutex::new(Some(connection));
+
+ Ok(capabilities)
+ }
+
+ fn start_browser(&mut self, port: u16, mut options: FirefoxOptions) -> WebDriverResult<()> {
+ let binary = try!(options.binary
+ .ok_or(WebDriverError::new(ErrorStatus::SessionNotCreated,
+ "Expected browser binary location, but unable to find \
+ binary in default location, no \
+ 'moz:firefoxOptions.binary' capability provided, and \
+ no binary flag set on the command line")));
+
+ let custom_profile = options.profile.is_some();
+
+ let mut runner = try!(FirefoxRunner::new(&binary, options.profile.take())
+ .map_err(|e| WebDriverError::new(ErrorStatus::SessionNotCreated,
+ e.description().to_owned())));
+
+ // double-dashed flags are not accepted on Windows systems
+ runner.args().push("-marionette".to_owned());
+
+ if let Some(args) = options.args.take() {
+ runner.args().extend(args);
+ };
+
+ try!(self.set_prefs(port, &mut runner.profile, custom_profile, options.prefs)
+ .map_err(|e| {
+ WebDriverError::new(ErrorStatus::SessionNotCreated,
+ format!("Failed to set preferences: {}", e))
+ }));
+
+ info!("Starting browser {} with args {:?}", binary.display(), runner.args());
+ try!(runner.start()
+ .map_err(|e| {
+ WebDriverError::new(ErrorStatus::SessionNotCreated,
+ format!("Failed to start browser {}: {}",
+ binary.display(), e))
+ }));
+ self.browser = Some(runner);
+
+ Ok(())
+ }
+
+ pub fn set_prefs(&self, port: u16, profile: &mut Profile, custom_profile: bool,
+ extra_prefs: Vec<(String, Pref)>)
+ -> WebDriverResult<()> {
+ let prefs = try!(profile.user_prefs()
+ .map_err(|_| WebDriverError::new(ErrorStatus::UnknownError,
+ "Unable to read profile preferences file")));
+
+ for &(ref name, ref value) in prefs::DEFAULT.iter() {
+ if !custom_profile || !prefs.contains_key(name) {
+ prefs.insert((*name).clone(), (*value).clone());
+ }
+ }
+
+ prefs.insert_slice(&extra_prefs[..]);
+
+ // fallbacks can be removed when Firefox 54 becomes stable
+ if let Some(ref level) = self.current_log_level {
+ prefs.insert("marionette.log.level", Pref::new(level.to_string()));
+ prefs.insert("marionette.logging", Pref::new(level.to_string())); // fallback
+ };
+ prefs.insert("marionette.port", Pref::new(port as i64));
+ prefs.insert("marionette.defaultPrefs.port", Pref::new(port as i64)); // fallback
+
+ prefs.write().map_err(|_| WebDriverError::new(ErrorStatus::UnknownError,
+ "Unable to write Firefox profile"))
+ }
+}
+
+impl WebDriverHandler<GeckoExtensionRoute> for MarionetteHandler {
+ fn handle_command(&mut self, _: &Option<Session>,
+ msg: WebDriverMessage<GeckoExtensionRoute>) -> WebDriverResult<WebDriverResponse> {
+ let mut resolved_capabilities = None;
+ {
+ let mut capabilities_options = None;
+ // First handle the status message which doesn't actually require a marionette
+ // connection or message
+ if msg.command == Status {
+ let (ready, message) = self.connection.lock()
+ .map(|ref connection| connection
+ .as_ref()
+ .map(|_| (false, "Session already started"))
+ .unwrap_or((true, "")))
+ .unwrap_or((false, "geckodriver internal error"));
+ let mut value = BTreeMap::new();
+ value.insert("ready".to_string(), Json::Boolean(ready));
+ value.insert("message".to_string(), Json::String(message.into()));
+ return Ok(WebDriverResponse::Generic(ValueResponse::new(Json::Object(value))));
+ }
+ match self.connection.lock() {
+ Ok(ref connection) => {
+ if connection.is_none() {
+ match msg.command {
+ NewSession(ref capabilities) => {
+ capabilities_options = Some(capabilities);
+ },
+ _ => {
+ return Err(WebDriverError::new(
+ ErrorStatus::SessionNotCreated,
+ "Tried to run command without establishing a connection"));
+ }
+ }
+ }
+ },
+ Err(_) => {
+ return Err(WebDriverError::new(
+ ErrorStatus::UnknownError,
+ "Failed to aquire Marionette connection"))
+ }
+ }
+ if let Some(capabilities) = capabilities_options {
+ resolved_capabilities = Some(try!(
+ self.create_connection(&msg.session_id, &capabilities)));
+ }
+ }
+
+ match self.connection.lock() {
+ Ok(ref mut connection) => {
+ match connection.as_mut() {
+ Some(conn) => conn.send_command(resolved_capabilities, &msg),
+ None => panic!("Connection missing")
+ }
+ },
+ Err(_) => {
+ Err(WebDriverError::new(
+ ErrorStatus::UnknownError,
+ "Failed to aquire Marionette connection"))
+ }
+ }
+ }
+
+ fn delete_session(&mut self, _: &Option<Session>) {
+ if let Ok(connection) = self.connection.lock() {
+ if let Some(ref conn) = *connection {
+ conn.close();
+ }
+ }
+ if let Some(ref mut runner) = self.browser {
+ debug!("Stopping browser process");
+ if runner.stop().is_err() {
+ error!("Failed to kill browser process");
+ };
+ }
+ self.connection = Mutex::new(None);
+ self.browser = None;
+ }
+}
+
+pub struct MarionetteSession {
+ pub session_id: String,
+ protocol: Option<String>,
+ application_type: Option<String>,
+ command_id: u64
+}
+
+impl MarionetteSession {
+ pub fn new(session_id: Option<String>) -> MarionetteSession {
+ let initital_id = session_id.unwrap_or("".to_string());
+ MarionetteSession {
+ session_id: initital_id,
+ protocol: None,
+ application_type: None,
+ command_id: 0
+ }
+ }
+
+ pub fn update(&mut self, msg: &WebDriverMessage<GeckoExtensionRoute>,
+ resp: &MarionetteResponse) -> WebDriverResult<()> {
+ match msg.command {
+ NewSession(_) => {
+ let session_id = try_opt!(
+ try_opt!(resp.result.find("sessionId"),
+ ErrorStatus::SessionNotCreated,
+ "Unable to get session id").as_string(),
+ ErrorStatus::SessionNotCreated,
+ "Unable to convert session id to string");
+ self.session_id = session_id.to_string().clone();
+ },
+ _ => {}
+ }
+ Ok(())
+ }
+
+ fn to_web_element(&self, json_data: &Json) -> WebDriverResult<WebElement> {
+ let data = try_opt!(json_data.as_object(),
+ ErrorStatus::UnknownError,
+ "Failed to convert data to an object");
+ let id = try_opt!(
+ try_opt!(
+ match data.get("ELEMENT") {
+ Some(id) => Some(id),
+ None => {
+ match data.get(ELEMENT_KEY) {
+ Some(id) => Some(id),
+ None => None
+ }
+ }
+ },
+ ErrorStatus::UnknownError,
+ "Failed to extract Web Element from response").as_string(),
+ ErrorStatus::UnknownError,
+ "Failed to convert id value to string"
+ ).to_string();
+ Ok(WebElement::new(id))
+ }
+
+ pub fn next_command_id(&mut self) -> u64 {
+ self.command_id = self.command_id + 1;
+ self.command_id
+ }
+
+ pub fn response(&mut self, msg: &WebDriverMessage<GeckoExtensionRoute>,
+ resp: MarionetteResponse) -> WebDriverResult<WebDriverResponse> {
+
+ if resp.id != self.command_id {
+ return Err(WebDriverError::new(ErrorStatus::UnknownError,
+ format!("Marionette responses arrived out of sequence, expected {}, got {}",
+ self.command_id, resp.id)));
+ }
+
+ if let Some(error) = resp.error {
+ let status = self.error_from_string(&error.status);
+
+ return Err(WebDriverError::new(status, error.message));
+ }
+
+ try!(self.update(msg, &resp));
+
+ Ok(match msg.command {
+ // Everything that doesn't have a response value
+ Get(_) | GoBack | GoForward | Refresh | SetTimeouts(_) |
+ MaximizeWindow | SwitchToWindow(_) | SwitchToFrame(_) |
+ SwitchToParentFrame | AddCookie(_) | DeleteCookies | DeleteCookie(_) |
+ DismissAlert | AcceptAlert | SendAlertText(_) | ElementClick(_) |
+ ElementTap(_) | ElementClear(_) | ElementSendKeys(_, _) |
+ PerformActions(_) | ReleaseActions => {
+ WebDriverResponse::Void
+ },
+ // Things that simply return the contents of the marionette "value" property
+ GetCurrentUrl | GetTitle | GetPageSource | GetWindowHandle | IsDisplayed(_) |
+ IsSelected(_) | GetElementAttribute(_, _) | GetElementProperty(_, _) |
+ GetCSSValue(_, _) | GetElementText(_) |
+ GetElementTagName(_) | IsEnabled(_) | ExecuteScript(_) | ExecuteAsyncScript(_) |
+ GetAlertText | TakeScreenshot | TakeElementScreenshot(_) => {
+ let value = try_opt!(resp.result.find("value"),
+ ErrorStatus::UnknownError,
+ "Failed to find value field");
+ //TODO: Convert webelement keys
+ WebDriverResponse::Generic(ValueResponse::new(value.clone()))
+ },
+ GetTimeouts => {
+ let script = try_opt!(try_opt!(resp.result
+ .find("script"),
+ ErrorStatus::UnknownError,
+ "Missing field: script")
+ .as_u64(),
+ ErrorStatus::UnknownError,
+ "Failed to interpret script timeout duration as u64");
+ // Check for the spec-compliant "pageLoad", but also for "page load",
+ // which was sent by Firefox 52 and earlier.
+ let page_load = try_opt!(try_opt!(resp.result.find("pageLoad")
+ .or(resp.result.find("page load")),
+ ErrorStatus::UnknownError,
+ "Missing field: pageLoad")
+ .as_u64(),
+ ErrorStatus::UnknownError,
+ "Failed to interpret page load duration as u64");
+ let implicit = try_opt!(try_opt!(resp.result
+ .find("implicit"),
+ ErrorStatus::UnknownError,
+ "Missing field: implicit")
+ .as_u64(),
+ ErrorStatus::UnknownError,
+ "Failed to interpret implicit search duration as u64");
+
+ WebDriverResponse::Timeouts(TimeoutsResponse {
+ script: script,
+ pageLoad: page_load,
+ implicit: implicit,
+ })
+ },
+ Status => panic!("Got status command that should already have been handled"),
+ GetWindowHandles => {
+ WebDriverResponse::Generic(ValueResponse::new(resp.result.clone()))
+ },
+ CloseWindow => {
+ let data = try_opt!(resp.result.as_array(),
+ ErrorStatus::UnknownError,
+ "Failed to interpret value as array");
+ let handles = try!(data.iter()
+ .map(|x| {
+ Ok(try_opt!(x.as_string(),
+ ErrorStatus::UnknownError,
+ "Failed to interpret window handle as string")
+ .to_owned())
+ })
+ .collect());
+ WebDriverResponse::CloseWindow(CloseWindowResponse { window_handles: handles })
+ }
+ GetWindowRect => {
+ let width = try_opt!(
+ try_opt!(resp.result.find("width"),
+ ErrorStatus::UnknownError,
+ "Failed to find width field").as_u64(),
+ ErrorStatus::UnknownError,
+ "Failed to interpret width as integer");
+
+ let height = try_opt!(
+ try_opt!(resp.result.find("height"),
+ ErrorStatus::UnknownError,
+ "Failed to find height field").as_u64(),
+ ErrorStatus::UnknownError,
+ "Failed to interpret width as integer");
+
+ let x = try_opt!(
+ try_opt!(resp.result.find("x"),
+ ErrorStatus::UnknownError,
+ "Failed to find x field").as_i64(),
+ ErrorStatus::UnknownError,
+ "Failed to interpret x as integer");
+
+ let y = try_opt!(
+ try_opt!(resp.result.find("y"),
+ ErrorStatus::UnknownError,
+ "Failed to find y field").as_i64(),
+ ErrorStatus::UnknownError,
+ "Failed to interpret y as integer");
+
+ WebDriverResponse::WindowRect(WindowRectResponse {x: x,
+ y: y,
+ width: width,
+ height: height})
+ },
+ GetElementRect(_) => {
+ let x = try_opt!(
+ try_opt!(resp.result.find("x"),
+ ErrorStatus::UnknownError,
+ "Failed to find x field").as_f64(),
+ ErrorStatus::UnknownError,
+ "Failed to interpret x as float");
+
+ let y = try_opt!(
+ try_opt!(resp.result.find("y"),
+ ErrorStatus::UnknownError,
+ "Failed to find y field").as_f64(),
+ ErrorStatus::UnknownError,
+ "Failed to interpret y as float");
+
+ let width = try_opt!(
+ try_opt!(resp.result.find("width"),
+ ErrorStatus::UnknownError,
+ "Failed to find width field").as_f64(),
+ ErrorStatus::UnknownError,
+ "Failed to interpret width as float");
+
+ let height = try_opt!(
+ try_opt!(resp.result.find("height"),
+ ErrorStatus::UnknownError,
+ "Failed to find height field").as_f64(),
+ ErrorStatus::UnknownError,
+ "Failed to interpret width as float");
+
+ WebDriverResponse::ElementRect(ElementRectResponse::new(x, y, width, height))
+ },
+ SetWindowRect(_) => {
+ let x = try_opt!(
+ try_opt!(resp.result.find("x"),
+ ErrorStatus::UnknownError,
+ "Failed to find x field").as_f64(),
+ ErrorStatus::UnknownError,
+ "Failed to interpret x as float");
+
+ let y = try_opt!(
+ try_opt!(resp.result.find("y"),
+ ErrorStatus::UnknownError,
+ "Failed to find y field").as_f64(),
+ ErrorStatus::UnknownError,
+ "Failed to interpret y as float");
+
+ let width = try_opt!(
+ try_opt!(resp.result.find("width"),
+ ErrorStatus::UnknownError,
+ "Failed to find width field").as_f64(),
+ ErrorStatus::UnknownError,
+ "Failed to interpret width as float");
+
+ let height = try_opt!(
+ try_opt!(resp.result.find("height"),
+ ErrorStatus::UnknownError,
+ "Failed to find height field").as_f64(),
+ ErrorStatus::UnknownError,
+ "Failed to interpret width as float");
+
+ WebDriverResponse::ElementRect(ElementRectResponse::new(x, y, width, height))
+ },
+ FullscreenWindow => {
+ let width = try_opt!(
+ try_opt!(resp.result.find("width"),
+ ErrorStatus::UnknownError,
+ "Failed to find width field").as_u64(),
+ ErrorStatus::UnknownError,
+ "Failed to interpret width as integer");
+
+ let height = try_opt!(
+ try_opt!(resp.result.find("height"),
+ ErrorStatus::UnknownError,
+ "Failed to find height field").as_u64(),
+ ErrorStatus::UnknownError,
+ "Failed to interpret height as integer");
+
+ let x = try_opt!(
+ try_opt!(resp.result.find("x"),
+ ErrorStatus::UnknownError,
+ "Failed to find x field").as_i64(),
+ ErrorStatus::UnknownError,
+ "Failed to interpret x as integer");
+
+ let y = try_opt!(
+ try_opt!(resp.result.find("y"),
+ ErrorStatus::UnknownError,
+ "Failed to find y field").as_i64(),
+ ErrorStatus::UnknownError,
+ "Failed to interpret y as integer");
+
+ WebDriverResponse::WindowRect(WindowRectResponse {x: x,
+ y: y,
+ width: width,
+ height: height})
+ },
+ GetCookies => {
+ let cookies = try!(self.process_cookies(&resp.result));
+ WebDriverResponse::Cookie(CookieResponse::new(cookies))
+ },
+ GetNamedCookie(ref name) => {
+ let mut cookies = try!(self.process_cookies(&resp.result));
+ cookies.retain(|x| x.name == *name);
+ WebDriverResponse::Cookie(CookieResponse::new(cookies))
+ }
+ FindElement(_) | FindElementElement(_, _) => {
+ let element = try!(self.to_web_element(
+ try_opt!(resp.result.find("value"),
+ ErrorStatus::UnknownError,
+ "Failed to find value field")));
+ WebDriverResponse::Generic(ValueResponse::new(element.to_json()))
+ },
+ FindElements(_) | FindElementElements(_, _) => {
+ let element_vec = try_opt!(resp.result.as_array(),
+ ErrorStatus::UnknownError,
+ "Failed to interpret value as array");
+ let elements = try!(element_vec.iter().map(
+ |x| {
+ self.to_web_element(x)
+ }).collect::<Result<Vec<_>, _>>());
+ WebDriverResponse::Generic(ValueResponse::new(
+ Json::Array(elements.iter().map(|x| {x.to_json()}).collect())))
+ },
+ GetActiveElement => {
+ let element = try!(self.to_web_element(
+ try_opt!(resp.result.find("value"),
+ ErrorStatus::UnknownError,
+ "Failed to find value field")));
+ WebDriverResponse::Generic(ValueResponse::new(element.to_json()))
+ },
+ NewSession(_) => {
+ let mut session_id = try_opt!(
+ try_opt!(resp.result.find("sessionId"),
+ ErrorStatus::InvalidSessionId,
+ "Failed to find sessionId field").as_string(),
+ ErrorStatus::InvalidSessionId,
+ "sessionId was not a string");
+
+ if session_id.starts_with("{") && session_id.ends_with("}") {
+ session_id = &session_id[1..session_id.len()-1];
+ }
+
+ let capabilities = try_opt!(
+ try_opt!(resp.result.find("capabilities"),
+ ErrorStatus::UnknownError,
+ "Failed to find capabilities field").as_object(),
+ ErrorStatus::UnknownError,
+ "capabiltites field was not an Object");
+
+ WebDriverResponse::NewSession(NewSessionResponse::new(
+ session_id.to_string(), Json::Object(capabilities.clone())))
+ },
+ DeleteSession => {
+ WebDriverResponse::DeleteSession
+ },
+ Extension(ref extension) => {
+ match extension {
+ &GeckoExtensionCommand::GetContext => {
+ let value = try_opt!(resp.result.find("value"),
+ ErrorStatus::UnknownError,
+ "Failed to find value field");
+ WebDriverResponse::Generic(ValueResponse::new(value.clone()))
+ },
+ &GeckoExtensionCommand::SetContext(_) => WebDriverResponse::Void,
+ &GeckoExtensionCommand::XblAnonymousChildren(_) => {
+ let els_vec = try_opt!(resp.result.as_array(),
+ ErrorStatus::UnknownError, "Failed to interpret body as array");
+ let els = try!(els_vec.iter().map(|x| self.to_web_element(x))
+ .collect::<Result<Vec<_>, _>>());
+ WebDriverResponse::Generic(ValueResponse::new(
+ Json::Array(els.iter().map(|el| el.to_json()).collect())))
+ },
+ &GeckoExtensionCommand::XblAnonymousByAttribute(_, _) => {
+ let el = try!(self.to_web_element(try_opt!(resp.result.find("value"),
+ ErrorStatus::UnknownError, "Failed to find value field")));
+ WebDriverResponse::Generic(ValueResponse::new(el.to_json()))
+ },
+ &GeckoExtensionCommand::InstallAddon(_) => {
+ let value = try_opt!(resp.result.find("value"),
+ ErrorStatus::UnknownError,
+ "Failed to find value field");
+ WebDriverResponse::Generic(ValueResponse::new(value.clone()))
+ },
+ &GeckoExtensionCommand::UninstallAddon(_) => WebDriverResponse::Void
+ }
+ }
+ })
+ }
+
+ fn process_cookies(&self, json_data: &Json) -> WebDriverResult<Vec<Cookie>> {
+ let value = try_opt!(json_data.as_array(),
+ ErrorStatus::UnknownError,
+ "Failed to interpret value as array");
+ value.iter().map(|x| {
+ let name = try_opt!(
+ try_opt!(x.find("name"),
+ ErrorStatus::UnknownError,
+ "Failed to find name field").as_string(),
+ ErrorStatus::UnknownError,
+ "Failed to interpret name as string").to_string();
+ let value = try_opt!(
+ try_opt!(x.find("value"),
+ ErrorStatus::UnknownError,
+ "Failed to find value field").as_string(),
+ ErrorStatus::UnknownError,
+ "Failed to interpret value as string").to_string();
+ let path = try!(
+ Nullable::from_json(x.find("path").unwrap_or(&Json::Null),
+ |x| {
+ Ok((try_opt!(x.as_string(),
+ ErrorStatus::UnknownError,
+ "Failed to interpret path as String")).to_string())
+ }));
+ let domain = try!(
+ Nullable::from_json(x.find("domain").unwrap_or(&Json::Null),
+ |x| {
+ Ok((try_opt!(x.as_string(),
+ ErrorStatus::UnknownError,
+ "Failed to interpret domain as String")).to_string())
+ }));
+ let expiry = try!(
+ Nullable::from_json(x.find("expiry").unwrap_or(&Json::Null),
+ |x| {
+ Ok(Date::new((try_opt!(
+ x.as_u64(),
+ ErrorStatus::UnknownError,
+ "Failed to interpret expiry as u64"))))
+ }));
+ let secure = try_opt!(
+ x.find("secure").map_or(Some(false), |x| x.as_boolean()),
+ ErrorStatus::UnknownError,
+ "Failed to interpret secure as boolean");
+ let http_only = try_opt!(
+ x.find("httpOnly").map_or(Some(false), |x| x.as_boolean()),
+ ErrorStatus::UnknownError,
+ "Failed to interpret httpOnly as boolean");
+ Ok(Cookie::new(name, value, path, domain, expiry, secure, http_only))
+ }).collect::<Result<Vec<_>, _>>()
+ }
+
+ pub fn error_from_string(&self, error_code: &str) -> ErrorStatus {
+ match error_code {
+ "element click intercepted" => ErrorStatus::ElementClickIntercepted,
+ "element not interactable" | "element not visible" => ErrorStatus::ElementNotInteractable,
+ "element not selectable" => ErrorStatus::ElementNotSelectable,
+ "insecure certificate" => ErrorStatus::InsecureCertificate,
+ "invalid argument" => ErrorStatus::InvalidArgument,
+ "invalid cookie domain" => ErrorStatus::InvalidCookieDomain,
+ "invalid coordinates" | "invalid element coordinates" => ErrorStatus::InvalidCoordinates,
+ "invalid element state" => ErrorStatus::InvalidElementState,
+ "invalid selector" => ErrorStatus::InvalidSelector,
+ "invalid session id" => ErrorStatus::InvalidSessionId,
+ "javascript error" => ErrorStatus::JavascriptError,
+ "move target out of bounds" => ErrorStatus::MoveTargetOutOfBounds,
+ "no such alert" => ErrorStatus::NoSuchAlert,
+ "no such element" => ErrorStatus::NoSuchElement,
+ "no such frame" => ErrorStatus::NoSuchFrame,
+ "no such window" => ErrorStatus::NoSuchWindow,
+ "script timeout" => ErrorStatus::ScriptTimeout,
+ "session not created" => ErrorStatus::SessionNotCreated,
+ "stale element reference" => ErrorStatus::StaleElementReference,
+ "timeout" => ErrorStatus::Timeout,
+ "unable to capture screen" => ErrorStatus::UnableToCaptureScreen,
+ "unable to set cookie" => ErrorStatus::UnableToSetCookie,
+ "unexpected alert open" => ErrorStatus::UnexpectedAlertOpen,
+ "unknown command" => ErrorStatus::UnknownCommand,
+ "unknown error" => ErrorStatus::UnknownError,
+ "unsupported operation" => ErrorStatus::UnsupportedOperation,
+ _ => ErrorStatus::UnknownError,
+ }
+ }
+}
+
+pub struct MarionetteCommand {
+ pub id: u64,
+ pub name: String,
+ pub params: BTreeMap<String, Json>
+}
+
+impl MarionetteCommand {
+ fn new(id: u64, name: String, params: BTreeMap<String, Json>) -> MarionetteCommand {
+ MarionetteCommand {
+ id: id,
+ name: name,
+ params: params,
+ }
+ }
+
+ fn from_webdriver_message(id: u64,
+ capabilities: Option<BTreeMap<String, Json>>,
+ msg: &WebDriverMessage<GeckoExtensionRoute>)
+ -> WebDriverResult<MarionetteCommand> {
+ let (opt_name, opt_parameters) = match msg.command {
+ NewSession(_) => {
+ let caps = capabilities.expect("Tried to create new session without processing capabilities");
+
+ let mut data = BTreeMap::new();
+ for (k, v) in caps.iter() {
+ data.insert(k.to_string(), v.to_json());
+ }
+
+ // duplicate in capabilities.desiredCapabilities for legacy compat
+ let mut legacy_caps = BTreeMap::new();
+ legacy_caps.insert("desiredCapabilities".to_string(), caps.to_json());
+ data.insert("capabilities".to_string(), legacy_caps.to_json());
+
+ (Some("newSession"), Some(Ok(data)))
+ },
+ DeleteSession => {
+ let mut body = BTreeMap::new();
+ body.insert("flags".to_owned(), vec!["eForceQuit".to_json()].to_json());
+ (Some("quitApplication"), Some(Ok(body)))
+ },
+ Status => panic!("Got status command that should already have been handled"),
+ Get(ref x) => (Some("get"), Some(x.to_marionette())),
+ GetCurrentUrl => (Some("getCurrentUrl"), None),
+ GoBack => (Some("goBack"), None),
+ GoForward => (Some("goForward"), None),
+ Refresh => (Some("refresh"), None),
+ GetTitle => (Some("getTitle"), None),
+ GetPageSource => (Some("getPageSource"), None),
+ GetWindowHandle => (Some("getWindowHandle"), None),
+ GetWindowHandles => (Some("getWindowHandles"), None),
+ CloseWindow => (Some("close"), None),
+ GetTimeouts => (Some("getTimeouts"), None),
+ SetTimeouts(ref x) => (Some("timeouts"), Some(x.to_marionette())),
+ SetWindowRect(ref x) => (Some("setWindowRect"), Some(x.to_marionette())),
+ GetWindowRect => (Some("getWindowRect"), None),
+ MaximizeWindow => (Some("maximizeWindow"), None),
+ FullscreenWindow => (Some("fullscreenWindow"), None),
+ SwitchToWindow(ref x) => (Some("switchToWindow"), Some(x.to_marionette())),
+ SwitchToFrame(ref x) => (Some("switchToFrame"), Some(x.to_marionette())),
+ SwitchToParentFrame => (Some("switchToParentFrame"), None),
+ FindElement(ref x) => (Some("findElement"), Some(x.to_marionette())),
+ FindElements(ref x) => (Some("findElements"), Some(x.to_marionette())),
+ FindElementElement(ref e, ref x) => {
+ let mut data = try!(x.to_marionette());
+ data.insert("element".to_string(), e.id.to_json());
+ (Some("findElement"), Some(Ok(data)))
+ },
+ FindElementElements(ref e, ref x) => {
+ let mut data = try!(x.to_marionette());
+ data.insert("element".to_string(), e.id.to_json());
+ (Some("findElements"), Some(Ok(data)))
+ },
+ GetActiveElement => (Some("getActiveElement"), None),
+ IsDisplayed(ref x) => (Some("isElementDisplayed"), Some(x.to_marionette())),
+ IsSelected(ref x) => (Some("isElementSelected"), Some(x.to_marionette())),
+ GetElementAttribute(ref e, ref x) => {
+ let mut data = BTreeMap::new();
+ data.insert("id".to_string(), e.id.to_json());
+ data.insert("name".to_string(), x.to_json());
+ (Some("getElementAttribute"), Some(Ok(data)))
+ },
+ GetElementProperty(ref e, ref x) => {
+ let mut data = BTreeMap::new();
+ data.insert("id".to_string(), e.id.to_json());
+ data.insert("name".to_string(), x.to_json());
+ (Some("getElementProperty"), Some(Ok(data)))
+ },
+ GetCSSValue(ref e, ref x) => {
+ let mut data = BTreeMap::new();
+ data.insert("id".to_string(), e.id.to_json());
+ data.insert("propertyName".to_string(), x.to_json());
+ (Some("getElementValueOfCssProperty"), Some(Ok(data)))
+ },
+ GetElementText(ref x) => (Some("getElementText"), Some(x.to_marionette())),
+ GetElementTagName(ref x) => (Some("getElementTagName"), Some(x.to_marionette())),
+ GetElementRect(ref x) => (Some("getElementRect"), Some(x.to_marionette())),
+ IsEnabled(ref x) => (Some("isElementEnabled"), Some(x.to_marionette())),
+ PerformActions(ref x) => (Some("performActions"), Some(x.to_marionette())),
+ ReleaseActions => (Some("releaseActions"), None),
+ ElementClick(ref x) => (Some("clickElement"), Some(x.to_marionette())),
+ ElementTap(ref x) => (Some("singleTap"), Some(x.to_marionette())),
+ ElementClear(ref x) => (Some("clearElement"), Some(x.to_marionette())),
+ ElementSendKeys(ref e, ref x) => {
+ let mut data = BTreeMap::new();
+ data.insert("id".to_string(), e.id.to_json());
+ data.insert("text".to_string(), x.text.to_json());
+ data.insert("value".to_string(),
+ x.text
+ .chars()
+ .map(|x| x.to_string())
+ .collect::<Vec<String>>()
+ .to_json());
+ (Some("sendKeysToElement"), Some(Ok(data)))
+ },
+ ExecuteScript(ref x) => (Some("executeScript"), Some(x.to_marionette())),
+ ExecuteAsyncScript(ref x) => (Some("executeAsyncScript"), Some(x.to_marionette())),
+ GetCookies | GetNamedCookie(_) => (Some("getCookies"), None),
+ DeleteCookies => (Some("deleteAllCookies"), None),
+ DeleteCookie(ref x) => {
+ let mut data = BTreeMap::new();
+ data.insert("name".to_string(), x.to_json());
+ (Some("deleteCookie"), Some(Ok(data)))
+ },
+ AddCookie(ref x) => (Some("addCookie"), Some(x.to_marionette())),
+ DismissAlert => (Some("dismissDialog"), None),
+ AcceptAlert => (Some("acceptDialog"), None),
+ GetAlertText => (Some("getTextFromDialog"), None),
+ SendAlertText(ref x) => {
+ let mut data = BTreeMap::new();
+ data.insert("text".to_string(), x.text.to_json());
+ data.insert("value".to_string(),
+ x.text
+ .chars()
+ .map(|x| x.to_string())
+ .collect::<Vec<String>>()
+ .to_json());
+ (Some("sendKeysToDialog"), Some(Ok(data)))
+ },
+ TakeScreenshot => {
+ let mut data = BTreeMap::new();
+ data.insert("id".to_string(), Json::Null);
+ data.insert("highlights".to_string(), Json::Array(vec![]));
+ data.insert("full".to_string(), Json::Boolean(false));
+ (Some("takeScreenshot"), Some(Ok(data)))
+ },
+ TakeElementScreenshot(ref e) => {
+ let mut data = BTreeMap::new();
+ data.insert("id".to_string(), e.id.to_json());
+ data.insert("highlights".to_string(), Json::Array(vec![]));
+ data.insert("full".to_string(), Json::Boolean(false));
+ (Some("takeScreenshot"), Some(Ok(data)))
+ },
+ Extension(ref extension) => {
+ match extension {
+ &GeckoExtensionCommand::GetContext => (Some("getContext"), None),
+ &GeckoExtensionCommand::SetContext(ref x) => {
+ (Some("setContext"), Some(x.to_marionette()))
+ },
+ &GeckoExtensionCommand::XblAnonymousChildren(ref e) => {
+ let mut data = BTreeMap::new();
+ data.insert("using".to_owned(), "anon".to_json());
+ data.insert("value".to_owned(), Json::Null);
+ data.insert("element".to_string(), e.id.to_json());
+ (Some("findElements"), Some(Ok(data)))
+ },
+ &GeckoExtensionCommand::XblAnonymousByAttribute(ref e, ref x) => {
+ let mut data = try!(x.to_marionette());
+ data.insert("element".to_string(), e.id.to_json());
+ (Some("findElement"), Some(Ok(data)))
+ },
+ &GeckoExtensionCommand::InstallAddon(ref x) => {
+ (Some("addon:install"), Some(x.to_marionette()))
+ },
+ &GeckoExtensionCommand::UninstallAddon(ref x) => {
+ (Some("addon:uninstall"), Some(x.to_marionette()))
+ }
+ }
+ }
+ };
+
+ let name = try_opt!(opt_name,
+ ErrorStatus::UnsupportedOperation,
+ "Operation not supported");
+ let parameters = try!(opt_parameters.unwrap_or(Ok(BTreeMap::new())));
+
+ Ok(MarionetteCommand::new(id, name.into(), parameters))
+ }
+}
+
+impl ToJson for MarionetteCommand {
+ fn to_json(&self) -> Json {
+ Json::Array(vec![Json::U64(0), self.id.to_json(), self.name.to_json(),
+ self.params.to_json()])
+ }
+}
+
+pub struct MarionetteResponse {
+ pub id: u64,
+ pub error: Option<MarionetteError>,
+ pub result: Json,
+}
+
+impl MarionetteResponse {
+ fn from_json(data: &Json) -> WebDriverResult<MarionetteResponse> {
+ let data_array = try_opt!(data.as_array(),
+ ErrorStatus::UnknownError,
+ "Expected a json array");
+
+ if data_array.len() != 4 {
+ return Err(WebDriverError::new(
+ ErrorStatus::UnknownError,
+ "Expected an array of length 4"));
+ }
+
+ if data_array[0].as_u64() != Some(1) {
+ return Err(WebDriverError::new(ErrorStatus::UnknownError,
+ "Expected 1 in first element of response"));
+ };
+ let id = try_opt!(data[1].as_u64(),
+ ErrorStatus::UnknownError,
+ "Expected an integer id");
+ let error = if data[2].is_object() {
+ Some(try!(MarionetteError::from_json(&data[2])))
+ } else if data[2].is_null() {
+ None
+ } else {
+ return Err(WebDriverError::new(ErrorStatus::UnknownError,
+ "Expected object or null error"));
+ };
+
+ let result = if data[3].is_null() || data[3].is_object() || data[3].is_array() {
+ data[3].clone()
+ } else {
+ return Err(WebDriverError::new(ErrorStatus::UnknownError,
+ "Expected object params"));
+ };
+
+ Ok(MarionetteResponse {id: id,
+ error: error,
+ result: result})
+ }
+}
+
+impl ToJson for MarionetteResponse {
+ fn to_json(&self) -> Json {
+ Json::Array(vec![Json::U64(1), self.id.to_json(), self.error.to_json(),
+ self.result.clone()])
+ }
+}
+
+#[derive(RustcEncodable, RustcDecodable)]
+pub struct MarionetteError {
+ pub status: String,
+ pub message: String,
+ pub stacktrace: Option<String>
+}
+
+impl MarionetteError {
+ fn new(status: String, message: String, stacktrace: Option<String>) -> MarionetteError {
+ MarionetteError {
+ status: status,
+ message: message,
+ stacktrace: stacktrace
+ }
+ }
+
+ fn from_json(data: &Json) -> WebDriverResult<MarionetteError> {
+ if !data.is_object() {
+ return Err(WebDriverError::new(ErrorStatus::UnknownError,
+ "Expected an error object"));
+ }
+ let status = try_opt!(
+ try_opt!(data.find("error"),
+ ErrorStatus::UnknownError,
+ "Error value has no status").as_string(),
+ ErrorStatus::UnknownError,
+ "Error status was not a string").into();
+
+ let message = try_opt!(
+ try_opt!(data.find("message"),
+ ErrorStatus::UnknownError,
+ "Error value has no message").as_string(),
+ ErrorStatus::UnknownError,
+ "Error message was not a string").into();
+
+ let stacktrace = match data.find("stacktrace") {
+ None | Some(&Json::Null) => None,
+ Some(x) => Some(try_opt!(x.as_string(),
+ ErrorStatus::UnknownError,
+ "Error message was not a string").into()),
+ };
+ Ok(MarionetteError::new(status, message, stacktrace))
+ }
+}
+
+impl ToJson for MarionetteError {
+ fn to_json(&self) -> Json {
+ let mut data = BTreeMap::new();
+ data.insert("status".into(), self.status.to_json());
+ data.insert("message".into(), self.message.to_json());
+ data.insert("stacktrace".into(), self.stacktrace.to_json());
+ Json::Object(data)
+ }
+}
+
+fn get_free_port() -> IoResult<u16> {
+ TcpListener::bind(&("localhost", 0))
+ .and_then(|stream| stream.local_addr())
+ .map(|x| x.port())
+}
+
+pub struct MarionetteConnection {
+ port: u16,
+ stream: Option<TcpStream>,
+ pub session: MarionetteSession
+}
+
+impl MarionetteConnection {
+ pub fn new(port: u16, session_id: Option<String>) -> MarionetteConnection {
+ MarionetteConnection {
+ port: port,
+ stream: None,
+ session: MarionetteSession::new(session_id)
+ }
+ }
+
+ pub fn connect(&mut self) -> WebDriverResult<()> {
+ let timeout = 60 * 1000; // ms
+ let poll_interval = 100; // ms
+ let poll_attempts = timeout / poll_interval;
+ let mut poll_attempt = 0;
+
+ loop {
+ match TcpStream::connect(&(DEFAULT_HOST, self.port)) {
+ Ok(stream) => {
+ self.stream = Some(stream);
+ break
+ },
+ Err(e) => {
+ trace!(" connection attempt {}/{}", poll_attempt, poll_attempts);
+ if poll_attempt <= poll_attempts {
+ poll_attempt += 1;
+ sleep(Duration::from_millis(poll_interval));
+ } else {
+ return Err(WebDriverError::new(
+ ErrorStatus::UnknownError, e.description().to_owned()));
+ }
+ }
+ }
+ };
+
+ debug!("Connected to Marionette on {}:{}", DEFAULT_HOST, self.port);
+
+ try!(self.handshake());
+ Ok(())
+ }
+
+ fn handshake(&mut self) -> WebDriverResult<()> {
+ let resp = try!(self.read_resp());
+ let handshake_data = try!(Json::from_str(&*resp));
+
+ let data = try_opt!(handshake_data.as_object(),
+ ErrorStatus::UnknownError,
+ "Expected a json object in handshake");
+
+ self.session.protocol = Some(try_opt!(data.get("marionetteProtocol"),
+ ErrorStatus::UnknownError,
+ "Missing 'marionetteProtocol' field in handshake").to_string());
+
+ self.session.application_type = Some(try_opt!(data.get("applicationType"),
+ ErrorStatus::UnknownError,
+ "Missing 'applicationType' field in handshake").to_string());
+
+ if self.session.protocol != Some("3".into()) {
+ return Err(WebDriverError::new(
+ ErrorStatus::UnknownError,
+ format!("Unsupported Marionette protocol version {}, required 3",
+ self.session.protocol.as_ref().unwrap_or(&"<unknown>".into()))));
+ }
+
+ Ok(())
+ }
+
+ pub fn close(&self) {
+ }
+
+ fn encode_msg(&self, msg:Json) -> String {
+ let data = json::encode(&msg).unwrap();
+ format!("{}:{}", data.len(), data)
+ }
+
+ pub fn send_command(&mut self,
+ capabilities: Option<BTreeMap<String, Json>>,
+ msg: &WebDriverMessage<GeckoExtensionRoute>)
+ -> WebDriverResult<WebDriverResponse> {
+ let id = self.session.next_command_id();
+ let command = try!(MarionetteCommand::from_webdriver_message(id, capabilities, msg));
+
+ let resp_data = try!(self.send(command.to_json()));
+ let json_data: Json = try!(Json::from_str(&*resp_data));
+
+ self.session.response(msg, try!(MarionetteResponse::from_json(&json_data)))
+ }
+
+ fn send(&mut self, msg: Json) -> WebDriverResult<String> {
+ let data = self.encode_msg(msg);
+ trace!("-> {}", data);
+
+ match self.stream {
+ Some(ref mut stream) => {
+ if stream.write(&*data.as_bytes()).is_err() {
+ let mut err = WebDriverError::new(ErrorStatus::UnknownError,
+ "Failed to write response to stream");
+ err.delete_session = true;
+ return Err(err);
+ }
+ }
+ None => {
+ let mut err = WebDriverError::new(ErrorStatus::UnknownError,
+ "Tried to write before opening stream");
+ err.delete_session = true;
+ return Err(err);
+ }
+ }
+ match self.read_resp() {
+ Ok(resp) => Ok(resp),
+ Err(_) => {
+ let mut err = WebDriverError::new(ErrorStatus::UnknownError,
+ "Failed to decode response from marionette");
+ err.delete_session = true;
+ Err(err)
+ }
+ }
+ }
+
+ fn read_resp(&mut self) -> IoResult<String> {
+ let mut bytes = 0usize;
+
+ // TODO(jgraham): Check before we unwrap?
+ let mut stream = self.stream.as_mut().unwrap();
+ loop {
+ let mut buf = &mut [0 as u8];
+ let num_read = try!(stream.read(buf));
+ let byte = match num_read {
+ 0 => {
+ return Err(IoError::new(ErrorKind::Other,
+ "EOF reading marionette message"))
+ },
+ 1 => buf[0] as char,
+ _ => panic!("Expected one byte got more")
+ };
+ match byte {
+ '0'...'9' => {
+ bytes = bytes * 10;
+ bytes += byte as usize - '0' as usize;
+ },
+ ':' => {
+ break
+ }
+ _ => {}
+ }
+ }
+
+ let mut buf = &mut [0 as u8; 8192];
+ let mut payload = Vec::with_capacity(bytes);
+ let mut total_read = 0;
+ while total_read < bytes {
+ let num_read = try!(stream.read(buf));
+ if num_read == 0 {
+ return Err(IoError::new(ErrorKind::Other,
+ "EOF reading marionette message"))
+ }
+ total_read += num_read;
+ for x in &buf[..num_read] {
+ payload.push(*x);
+ }
+ }
+
+ // TODO(jgraham): Need to handle the error here
+ let data = String::from_utf8(payload).unwrap();
+ trace!("<- {}", data);
+
+ Ok(data)
+ }
+}
+
+trait ToMarionette {
+ fn to_marionette(&self) -> WebDriverResult<BTreeMap<String, Json>>;
+}
+
+impl ToMarionette for GetParameters {
+ fn to_marionette(&self) -> WebDriverResult<BTreeMap<String, Json>> {
+ Ok(try_opt!(self.to_json().as_object(), ErrorStatus::UnknownError, "Expected an object").clone())
+ }
+}
+
+impl ToMarionette for TimeoutsParameters {
+ fn to_marionette(&self) -> WebDriverResult<BTreeMap<String, Json>> {
+ Ok(try_opt!(self.to_json().as_object(), ErrorStatus::UnknownError, "Expected an object").clone())
+ }
+}
+
+impl ToMarionette for WindowRectParameters {
+ fn to_marionette(&self) -> WebDriverResult<BTreeMap<String, Json>> {
+ Ok(try_opt!(self.to_json().as_object(), ErrorStatus::UnknownError, "Expected an object").clone())
+ }
+}
+
+impl ToMarionette for SwitchToWindowParameters {
+ fn to_marionette(&self) -> WebDriverResult<BTreeMap<String, Json>> {
+ let mut data = BTreeMap::new();
+ data.insert("name".to_string(), self.handle.to_json());
+ Ok(data)
+ }
+}
+
+impl ToMarionette for LocatorParameters {
+ fn to_marionette(&self) -> WebDriverResult<BTreeMap<String, Json>> {
+ Ok(try_opt!(self.to_json().as_object(),
+ ErrorStatus::UnknownError,
+ "Expected an object")
+ .clone())
+ }
+}
+
+impl ToMarionette for SwitchToFrameParameters {
+ fn to_marionette(&self) -> WebDriverResult<BTreeMap<String, Json>> {
+ let mut data = BTreeMap::new();
+ let key = match self.id {
+ FrameId::Null => None,
+ FrameId::Short(_) => Some("id"),
+ FrameId::Element(_) => Some("element"),
+ };
+ if let Some(x) = key {
+ data.insert(x.to_string(), self.id.to_json());
+ }
+ Ok(data)
+ }
+}
+
+impl ToMarionette for JavascriptCommandParameters {
+ fn to_marionette(&self) -> WebDriverResult<BTreeMap<String, Json>> {
+ let mut data = self.to_json().as_object().unwrap().clone();
+ data.insert("newSandbox".to_string(), false.to_json());
+ data.insert("specialPowers".to_string(), false.to_json());
+ data.insert("scriptTimeout".to_string(), Json::Null);
+ Ok(data)
+ }
+}
+
+impl ToMarionette for ActionsParameters {
+ fn to_marionette(&self) -> WebDriverResult<BTreeMap<String, Json>> {
+ Ok(try_opt!(self.to_json().as_object(),
+ ErrorStatus::UnknownError,
+ "Expected an object")
+ .clone())
+ }
+}
+
+impl ToMarionette for GetNamedCookieParameters {
+ fn to_marionette(&self) -> WebDriverResult<BTreeMap<String, Json>> {
+ Ok(try_opt!(self.to_json().as_object(),
+ ErrorStatus::UnknownError,
+ "Expected an object")
+ .clone())
+ }
+}
+
+impl ToMarionette for AddCookieParameters {
+ fn to_marionette(&self) -> WebDriverResult<BTreeMap<String, Json>> {
+ let mut cookie = BTreeMap::new();
+ cookie.insert("name".to_string(), self.name.to_json());
+ cookie.insert("value".to_string(), self.value.to_json());
+ if self.path.is_value() {
+ cookie.insert("path".to_string(), self.path.to_json());
+ }
+ if self.domain.is_value() {
+ cookie.insert("domain".to_string(), self.domain.to_json());
+ }
+ if self.expiry.is_value() {
+ cookie.insert("expiry".to_string(), self.expiry.to_json());
+ }
+ cookie.insert("secure".to_string(), self.secure.to_json());
+ cookie.insert("httpOnly".to_string(), self.httpOnly.to_json());
+ let mut data = BTreeMap::new();
+ data.insert("cookie".to_string(), Json::Object(cookie));
+ Ok(data)
+ }
+}
+
+impl ToMarionette for TakeScreenshotParameters {
+ fn to_marionette(&self) -> WebDriverResult<BTreeMap<String, Json>> {
+ let mut data = BTreeMap::new();
+ let element = match self.element {
+ Nullable::Null => Json::Null,
+ Nullable::Value(ref x) => Json::Object(try!(x.to_marionette()))
+ };
+ data.insert("element".to_string(), element);
+ Ok(data)
+ }
+}
+
+impl ToMarionette for WebElement {
+ fn to_marionette(&self) -> WebDriverResult<BTreeMap<String, Json>> {
+ let mut data = BTreeMap::new();
+ data.insert("id".to_string(), self.id.to_json());
+ Ok(data)
+ }
+}
+
+impl<T: ToJson> ToMarionette for Nullable<T> {
+ fn to_marionette(&self) -> WebDriverResult<BTreeMap<String, Json>> {
+ //Note this is a terrible hack. We don't want Nullable<T: ToJson+ToMarionette>
+ //so in cases where ToJson != ToMarionette you have to deal with the Nullable
+ //explicitly. This kind of suggests that the whole design is wrong.
+ Ok(try_opt!(self.to_json().as_object(), ErrorStatus::UnknownError, "Expected an object").clone())
+ }
+}
+
+impl ToMarionette for FrameId {
+ fn to_marionette(&self) -> WebDriverResult<BTreeMap<String, Json>> {
+ let mut data = BTreeMap::new();
+ match *self {
+ FrameId::Short(x) => data.insert("id".to_string(), x.to_json()),
+ FrameId::Element(ref x) => data.insert("element".to_string(),
+ Json::Object(try!(x.to_marionette()))),
+ FrameId::Null => None
+ };
+ Ok(data)
+ }
+}
diff --git a/src/prefs.rs b/src/prefs.rs
new file mode 100644
index 0000000..50ee1e1
--- /dev/null
+++ b/src/prefs.rs
@@ -0,0 +1,229 @@
+use mozprofile::preferences::Pref;
+
+lazy_static! {
+ pub static ref DEFAULT: [(&'static str, Pref); 79] = [
+ // Disable automatic downloading of new releases
+ ("app.update.auto", Pref::new(false)),
+
+ // Disable automatically upgrading Firefox
+ ("app.update.enabled", Pref::new(false)),
+
+ // Increase the APZ content response timeout in tests to 1
+ // minute. This is to accommodate the fact that test environments
+ // tends to be slower than production environments (with the
+ // b2g emulator being the slowest of them all), resulting in the
+ // production timeout value sometimes being exceeded and causing
+ // false-positive test failures.
+ //
+ // (bug 1176798, bug 1177018, bug 1210465)
+ ("apz.content_response_timeout", Pref::new(60000)),
+
+ // Enable the dump function, which sends messages to the system
+ // console
+ ("browser.dom.window.dump.enabled", Pref::new(true)),
+
+ // Indicate that the download panel has been shown once so
+ // that whichever download test runs first does not show the popup
+ // inconsistently
+ ("browser.download.panel.shown", Pref::new(true)),
+
+ // Implicitly accept license
+ ("browser.EULA.override", Pref::new(true)),
+
+ // use about:blank as new tab page
+ ("browser.newtabpage.enabled", Pref::new(false)),
+
+ // Assume the about:newtab pages intro panels have been shown
+ // to not depend on which test runs first and happens to open
+ // about:newtab
+ ("browser.newtabpage.introShown", Pref::new(true)),
+
+ // Never start the browser in offline mode
+ ("browser.offline", Pref::new(false)),
+
+ // Background thumbnails in particular cause grief, and disabling
+ // thumbnails in general cannot hurt
+ ("browser.pagethumbnails.capturing_disabled", Pref::new(true)),
+
+ // Avoid performing Reader Mode intros during tests
+ ("browser.reader.detectedFirstArticle", Pref::new(true)),
+
+ // Disable safebrowsing components
+ ("browser.safebrowsing.blockedURIs.enabled", Pref::new(false)),
+ ("browser.safebrowsing.downloads.enabled", Pref::new(false)),
+ ("browser.safebrowsing.enabled", Pref::new(false)),
+ ("browser.safebrowsing.forbiddenURIs.enabled", Pref::new(false)),
+ ("browser.safebrowsing.malware.enabled", Pref::new(false)),
+ ("browser.safebrowsing.phishing.enabled", Pref::new(false)),
+
+ // Disable updates to search engines
+ ("browser.search.update", Pref::new(false)),
+
+ // Do not restore the last open set of tabs if the browser crashed
+ ("browser.sessionstore.resume_from_crash", Pref::new(false)),
+
+ // Skip check for default browser on startup
+ ("browser.shell.checkDefaultBrowser", Pref::new(false)),
+
+ // Do not warn when quitting with multiple tabs
+ ("browser.showQuitWarning", Pref::new(false)),
+
+ // Disable Android snippets
+ ("browser.snippets.enabled", Pref::new(false)),
+ ("browser.snippets.syncPromo.enabled", Pref::new(false)),
+ ("browser.snippets.firstrunHomepage.enabled", Pref::new(false)),
+
+ // Do not redirect user when a milestone upgrade of Firefox
+ // is detected
+ ("browser.startup.homepage_override.mstone", Pref::new("ignore")),
+
+ // Start with a blank page (about:blank)
+ ("browser.startup.page", Pref::new(0)),
+
+ // Disable tab animation
+ ("browser.tabs.animate", Pref::new(false)),
+
+ // Do not warn when quitting a window with multiple tabs
+ ("browser.tabs.closeWindowWithLastTab", Pref::new(false)),
+
+ // Do not allow background tabs to be zombified, otherwise for
+ // tests that open additional tabs, the test harness tab itself
+ // might get unloaded
+ ("browser.tabs.disableBackgroundZombification", Pref::new(false)),
+
+ // Do not warn on exit when multiple tabs are open
+ ("browser.tabs.warnOnClose", Pref::new(false)),
+
+ // Do not warn when closing all other open tabs
+ ("browser.tabs.warnOnCloseOtherTabs", Pref::new(false)),
+
+ // Do not warn when multiple tabs will be opened
+ ("browser.tabs.warnOnOpen", Pref::new(false)),
+
+ // Disable first run splash page on Windows 10
+ ("browser.usedOnWindows10.introURL", Pref::new("")),
+
+ // Disable the UI tour
+ ("browser.uitour.enabled", Pref::new(false)),
+
+ // Do not warn on quitting Firefox
+ ("browser.warnOnQuit", Pref::new(false)),
+
+ // Do not show datareporting policy notifications which can
+ // interfere with tests
+ ("datareporting.healthreport.about.reportUrl", Pref::new("http://%(server)s/dummy/abouthealthreport/")),
+ ("datareporting.healthreport.documentServerURI", Pref::new("http://%(server)s/dummy/healthreport/")),
+ ("datareporting.healthreport.logging.consoleEnabled", Pref::new(false)),
+ ("datareporting.healthreport.service.enabled", Pref::new(false)),
+ ("datareporting.healthreport.service.firstRun", Pref::new(false)),
+ ("datareporting.healthreport.uploadEnabled", Pref::new(false)),
+ ("datareporting.policy.dataSubmissionEnabled", Pref::new(false)),
+ ("datareporting.policy.dataSubmissionPolicyAccepted", Pref::new(false)),
+ ("datareporting.policy.dataSubmissionPolicyBypassNotification", Pref::new(true)),
+
+ // Disable popup-blocker
+ ("dom.disable_open_during_load", Pref::new(false)),
+
+ // Disable the ProcessHangMonitor
+ ("dom.ipc.reportProcessHangs", Pref::new(false)),
+
+ // Disable slow script dialogues
+ ("dom.max_chrome_script_run_time", Pref::new(0)),
+ ("dom.max_script_run_time", Pref::new(0)),
+
+ // Only load extensions from the application and user profile
+ // AddonManager.SCOPE_PROFILE + AddonManager.SCOPE_APPLICATION
+ ("extensions.autoDisableScopes", Pref::new(0)),
+ ("extensions.enabledScopes", Pref::new(5)),
+
+ // don't block add-ons for e10s
+ ("extensions.e10sBlocksEnabling", Pref::new(false)),
+
+ // Disable metadata caching for installed add-ons by default
+ ("extensions.getAddons.cache.enabled", Pref::new(false)),
+
+ // Disable intalling any distribution extensions or add-ons
+ ("extensions.installDistroAddons", Pref::new(false)),
+ ("extensions.showMismatchUI", Pref::new(false)),
+
+ // Turn off extension updates so they do not bother tests
+ ("extensions.update.enabled", Pref::new(false)),
+ ("extensions.update.notifyUser", Pref::new(false)),
+
+ // Make sure opening about:addons will not hit the network
+ ("extensions.webservice.discoverURL", Pref::new("http://%(server)s/dummy/discoveryURL")),
+
+ // Allow the application to have focus even it runs in the
+ // background
+ ("focusmanager.testmode", Pref::new(true)),
+
+ // Disable useragent updates
+ ("general.useragent.updates.enabled", Pref::new(false)),
+
+ // Always use network provider for geolocation tests so we bypass
+ // the macOS dialog raised by the corelocation provider
+ ("geo.provider.testing", Pref::new(true)),
+
+ // Do not scan wi-fi
+ ("geo.wifi.scan", Pref::new(false)),
+
+ // No hang monitor
+ ("hangmonitor.timeout", Pref::new(0)),
+
+ // Show chrome errors and warnings in the error console
+ ("javascript.options.showInConsole", Pref::new(true)),
+
+ // Make sure the disk cache does not get auto disabled
+ ("network.http.bypass-cachelock-threshold", Pref::new(200000)),
+
+ // Do not prompt with long usernames or passwords in URLs
+ ("network.http.phishy-userpass-length", Pref::new(255)),
+
+ // Do not prompt for temporary redirects
+ ("network.http.prompt-temp-redirect", Pref::new(false)),
+
+ // Disable speculative connections so they are not reported as
+ // leaking when they are hanging around
+ ("network.http.speculative-parallel-limit", Pref::new(0)),
+
+ // Do not automatically switch between offline and online
+ ("network.manage-offline-status", Pref::new(false)),
+
+ // Make sure SNTP requests do not hit the network
+ ("network.sntp.pools", Pref::new("%(server)s")),
+
+ // Disable Flash. The plugin container it is run in is
+ // causing problems when quitting Firefox from geckodriver,
+ // c.f. https://github.com/mozilla/geckodriver/issues/225.
+ ("plugin.state.flash", Pref::new(0)),
+
+ // Local documents have access to all other local docments,
+ // including directory listings.
+ ("security.fileuri.strict_origin_policy", Pref::new(false)),
+
+ // Tests don't wait for the notification button security delay
+ ("security.notification_enable_delay", Pref::new(0)),
+
+ // Ensure blocklist updates don't hit the network
+ ("services.settings.server", Pref::new("http://%(server)s/dummy/blocklist/")),
+
+ // Do not automatically fill sign-in forms with known usernames
+ // and passwords
+ ("signon.autofillForms", Pref::new(false)),
+
+ // Disable password capture, so that tests that include forms
+ // are not influenced by the presence of the persistent doorhanger
+ // notification
+ ("signon.rememberSignons", Pref::new(false)),
+
+ // Disable first run pages
+ ("startup.homepage_welcome_url", Pref::new("about:blank")),
+ ("startup.homepage_welcome_url.additional", Pref::new("")),
+
+ // Prevent starting into safe mode after application crashes
+ ("toolkit.startup.max_resumed_crashes", Pref::new(-1)),
+
+ // We want to collect telemetry, but we don't want to send in the results
+ ("toolkit.telemetry.server", Pref::new("https://%(server)s/dummy/telemetry/")),
+ ];
+}
diff --git a/src/tests/profile.zip b/src/tests/profile.zip
new file mode 100644
index 0000000..286b118
--- /dev/null
+++ b/src/tests/profile.zip
Binary files differ