1 | // Copyright 2011 Software Freedom Conservancy. All Rights Reserved. |
2 | // |
3 | // Licensed under the Apache License, Version 2.0 (the "License"); |
4 | // you may not use this file except in compliance with the License. |
5 | // You may obtain a copy of the License at |
6 | // |
7 | // http://www.apache.org/licenses/LICENSE-2.0 |
8 | // |
9 | // Unless required by applicable law or agreed to in writing, software |
10 | // distributed under the License is distributed on an "AS IS" BASIS, |
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
12 | // See the License for the specific language governing permissions and |
13 | // limitations under the License. |
14 | |
15 | goog.provide('webdriver.http.CorsClient'); |
16 | |
17 | goog.require('goog.json'); |
18 | goog.require('webdriver.http.Response'); |
19 | |
20 | |
21 | |
22 | /** |
23 | * Communicates with a WebDriver server, which may be on a different domain, |
24 | * using the <a href="http://www.w3.org/TR/cors/">cross-origin resource sharing |
25 | * </a> (CORS) extension to WebDriver's JSON wire protocol. |
26 | * |
27 | * <p>Each command from the standard JSON protocol will be encoded in a |
28 | * JSON object with the following form: |
29 | * {method:string, path:string, data:!Object} |
30 | * |
31 | * <p>The encoded command is then sent as a POST request to the server's /xdrpc |
32 | * endpoint. The server will decode the command, re-route it to the appropriate |
33 | * handler, and then return the command's response as a standard JSON response |
34 | * object. The JSON responses will <em>always</em> be returned with a 200 |
35 | * response from the server; clients must rely on the response's "status" field |
36 | * to determine whether the command succeeded. |
37 | * |
38 | * <p>This client cannot be used with the standard wire protocol due to |
39 | * limitations in the various browser implementations of the CORS specification: |
40 | * <ul> |
41 | * <li>IE's <a href="http://goo.gl/6l3kA">XDomainRequest</a> object is only |
42 | * capable of generating the types of requests that may be generated through |
43 | * a standard <a href="http://goo.gl/vgzAU">HTML form</a> - it can not send |
44 | * DELETE requests, as is required in the wire protocol. |
45 | * <li>WebKit's implementation of CORS does not follow the spec and forbids |
46 | * redirects: https://bugs.webkit.org/show_bug.cgi?id=57600 |
47 | * This limitation appears to be intentional and is documented in WebKit's |
48 | * Layout tests: |
49 | * //LayoutTests/http/tests/xmlhttprequest/access-control-and-redirects.html |
50 | * <li>If the server does not return a 2xx response, IE and Opera's |
51 | * implementations will fire the XDomainRequest/XMLHttpRequest object's |
52 | * onerror handler, but without the corresponding response text returned by |
53 | * the server. This renders IE and Opera incapable of handling command |
54 | * failures in the standard JSON protocol. |
55 | * </ul> |
56 | * |
57 | * @param {string} url URL for the WebDriver server to send commands to. |
58 | * @constructor |
59 | * @implements {webdriver.http.Client} |
60 | * @see <a href="http://www.w3.org/TR/cors/">CORS Spec</a> |
61 | * @see <a href="http://code.google.com/p/selenium/wiki/JsonWireProtocol"> |
62 | * JSON wire protocol</a> |
63 | */ |
64 | webdriver.http.CorsClient = function(url) { |
65 | if (!webdriver.http.CorsClient.isAvailable()) { |
66 | throw Error('The current environment does not support cross-origin ' + |
67 | 'resource sharing'); |
68 | } |
69 | |
70 | /** @private {string} */ |
71 | this.url_ = url + webdriver.http.CorsClient.XDRPC_ENDPOINT; |
72 | }; |
73 | |
74 | |
75 | /** |
76 | * Resource URL to send commands to on the server. |
77 | * @type {string} |
78 | * @const |
79 | */ |
80 | webdriver.http.CorsClient.XDRPC_ENDPOINT = '/xdrpc'; |
81 | |
82 | |
83 | /** |
84 | * Tests whether the current environment supports cross-origin resource sharing. |
85 | * @return {boolean} Whether cross-origin resource sharing is supported. |
86 | * @see http://www.w3.org/TR/cors/ |
87 | */ |
88 | webdriver.http.CorsClient.isAvailable = function() { |
89 | return typeof XDomainRequest !== 'undefined' || |
90 | (typeof XMLHttpRequest !== 'undefined' && |
91 | goog.isBoolean(new XMLHttpRequest().withCredentials)); |
92 | }; |
93 | |
94 | |
95 | /** @override */ |
96 | webdriver.http.CorsClient.prototype.send = function(request, callback) { |
97 | try { |
98 | var xhr = new (typeof XDomainRequest !== 'undefined' ? |
99 | XDomainRequest : XMLHttpRequest); |
100 | xhr.open('POST', this.url_, true); |
101 | |
102 | xhr.onload = function() { |
103 | callback(null, webdriver.http.Response.fromXmlHttpRequest( |
104 | /** @type {!XMLHttpRequest} */ (xhr))); |
105 | }; |
106 | |
107 | var url = this.url_; |
108 | xhr.onerror = function() { |
109 | callback(Error([ |
110 | 'Unable to send request: POST ', url, |
111 | '\nPerhaps the server did not respond to the preflight request ', |
112 | 'with valid access control headers?' |
113 | ].join(''))); |
114 | }; |
115 | |
116 | // Define event handlers for all events on the XDomainRequest. Apparently, |
117 | // if we don't do this, IE9+10 will silently abort our request. Yay IE. |
118 | // Note, we're not using goog.nullFunction, because it tends to get |
119 | // optimized away by the compiler, which leaves us where we were before. |
120 | xhr.onprogress = xhr.ontimeout = function() {}; |
121 | |
122 | xhr.send(goog.json.serialize({ |
123 | 'method': request.method, |
124 | 'path': request.path, |
125 | 'data': request.data |
126 | })); |
127 | } catch (ex) { |
128 | callback(ex); |
129 | } |
130 | }; |