# lib/webdriver/http/http.js

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 | /**
16 | * @fileoverview Defines a {@code webdriver.CommandExecutor} that communicates
17 | * with a server over HTTP.
18 | */
19 |
20 | goog.provide('webdriver.http.Client');
21 | goog.provide('webdriver.http.Executor');
22 | goog.provide('webdriver.http.Request');
23 | goog.provide('webdriver.http.Response');
24 |
25 | goog.require('bot.ErrorCode');
26 | goog.require('goog.array');
27 | goog.require('goog.json');
28 | goog.require('webdriver.CommandExecutor');
29 | goog.require('webdriver.CommandName');
30 | goog.require('webdriver.promise.Deferred');
31 |
32 |
33 |
34 | /**
35 | * Interface used for sending individual HTTP requests to the server.
36 | * @interface
37 | */
38 | webdriver.http.Client = function() {
39 | };
40 |
41 |
42 | /**
43 | * Sends a request to the server. If an error occurs while sending the request,
44 | * such as a failure to connect to the server, the provided callback will be
45 | * invoked with a non-null {@link Error} describing the error. Otherwise, when
46 | * the server's response has been received, the callback will be invoked with a
47 | * null Error and non-null {@link webdriver.http.Response} object.
48 | *
49 | * @param {!webdriver.http.Request} request The request to send.
50 | * @param {function(Error, !webdriver.http.Response=)} callback the function to
51 | * invoke when the server's response is ready.
52 | */
53 | webdriver.http.Client.prototype.send = function(request, callback) {
54 | };
55 |
56 |
57 |
58 | /**
59 | * A command executor that communicates with a server using the WebDriver
60 | * command protocol.
61 | * @param {!webdriver.http.Client} client The client to use when sending
62 | * requests to the server.
63 | * @constructor
64 | * @implements {webdriver.CommandExecutor}
65 | */
66 | webdriver.http.Executor = function(client) {
67 |
68 | /**
69 | * Client used to communicate with the server.
70 | * @private {!webdriver.http.Client}
71 | */
72 | this.client_ = client;
73 |
74 | /**
75 | * @private {!Object<{method:string, path:string}>}
76 | */
77 | this.customCommands_ = {};
78 | };
79 |
80 |
81 | /**
82 | * Defines a new command for use with this executor. When a command is sent,
83 | * the {@code path} will be preprocessed using the command's parameters; any
84 | * path segments prefixed with ":" will be replaced by the parameter of the
85 | * same name. For example, given "/person/:name" and the parameters
86 | * "{name: 'Bob'}", the final command path will be "/person/Bob".
87 | *
88 | * @param {string} name The command name.
89 | * @param {string} method The HTTP method to use when sending this command.
90 | * @param {string} path The path to send the command to, relative to
91 | * the WebDriver server's command root and of the form
92 | * "/path/:variable/segment".
93 | */
94 | webdriver.http.Executor.prototype.defineCommand = function(
95 | name, method, path) {
96 | this.customCommands_[name] = {method: method, path: path};
97 | };
98 |
99 |
100 | /** @override */
101 | webdriver.http.Executor.prototype.execute = function(command, callback) {
102 | var resource =
103 | this.customCommands_[command.getName()] ||
104 | webdriver.http.Executor.COMMAND_MAP_[command.getName()];
105 | if (!resource) {
106 | throw new Error('Unrecognized command: ' + command.getName());
107 | }
108 |
109 | var parameters = command.getParameters();
110 | var path = webdriver.http.Executor.buildPath_(resource.path, parameters);
111 | var request = new webdriver.http.Request(resource.method, path, parameters);
112 |
113 | this.client_.send(request, function(e, response) {
114 | var responseObj;
115 | if (!e) {
116 | try {
117 | responseObj = webdriver.http.Executor.parseHttpResponse_(
118 | /** @type {!webdriver.http.Response} */ (response));
119 | } catch (ex) {
120 | e = ex;
121 | }
122 | }
123 | callback(e, responseObj);
124 | });
125 | };
126 |
127 |
128 | /**
129 | * Builds a fully qualified path using the given set of command parameters. Each
130 | * path segment prefixed with ':' will be replaced by the value of the
131 | * corresponding parameter. All parameters spliced into the path will be
132 | * removed from the parameter map.
133 | * @param {string} path The original resource path.
134 | * @param {!Object.<*>} parameters The parameters object to splice into
135 | * the path.
136 | * @return {string} The modified path.
137 | * @private
138 | */
139 | webdriver.http.Executor.buildPath_ = function(path, parameters) {
140 | var pathParameters = path.match(/\/:(\w+)\b/g);
141 | if (pathParameters) {
142 | for (var i = 0; i < pathParameters.length; ++i) {
143 | var key = pathParameters[i].substring(2); // Trim the /:
144 | if (key in parameters) {
145 | var value = parameters[key];
146 | // TODO: move webdriver.WebElement.ELEMENT definition to a
147 | // common file so we can reference it here without pulling in all of
148 | // webdriver.WebElement's dependencies.
149 | if (value && value['ELEMENT']) {
150 | // When inserting a WebElement into the URL, only use its ID value,
151 | // not the full JSON.
152 | value = value['ELEMENT'];
153 | }
154 | path = path.replace(pathParameters[i], '/' + value);
155 | delete parameters[key];
156 | } else {
157 | throw new Error('Missing required parameter: ' + key);
158 | }
159 | }
160 | }
161 | return path;
162 | };
163 |
164 |
165 | /**
166 | * Callback used to parse {@link webdriver.http.Response} objects from a
167 | * {@link webdriver.http.Client}.
168 | * @param {!webdriver.http.Response} httpResponse The HTTP response to parse.
169 | * @return {!bot.response.ResponseObject} The parsed response.
170 | * @private
171 | */
172 | webdriver.http.Executor.parseHttpResponse_ = function(httpResponse) {
173 | try {
174 | return /** @type {!bot.response.ResponseObject} */ (goog.json.parse(
175 | httpResponse.body));
176 | } catch (ex) {
177 | // Whoops, looks like the server sent us a malformed response. We'll need
178 | // to manually build a response object based on the response code.
179 | }
180 |
181 | var response = {
182 | 'status': bot.ErrorCode.SUCCESS,
183 | 'value': httpResponse.body.replace(/\r\n/g, '\n')
184 | };
185 |
186 | if (!(httpResponse.status > 199 && httpResponse.status < 300)) {
187 | // 404 represents an unknown command; anything else is a generic unknown
188 | // error.
189 | response['status'] = httpResponse.status == 404 ?
190 | bot.ErrorCode.UNKNOWN_COMMAND :
191 | bot.ErrorCode.UNKNOWN_ERROR;
192 | }
193 |
194 | return response;
195 | };
196 |
197 |
198 | /**
199 | * Maps command names to resource locator.
200 | * @private {!Object.<{method:string, path:string}>}
201 | * @const
202 | */
203 | webdriver.http.Executor.COMMAND_MAP_ = (function() {
204 | return new Builder().
205 | put(webdriver.CommandName.GET_SERVER_STATUS, get('/status')).
206 | put(webdriver.CommandName.NEW_SESSION, post('/session')).
207 | put(webdriver.CommandName.GET_SESSIONS, get('/sessions')).
208 | put(webdriver.CommandName.DESCRIBE_SESSION, get('/session/:sessionId')).
209 | put(webdriver.CommandName.QUIT, del('/session/:sessionId')).
210 | put(webdriver.CommandName.CLOSE, del('/session/:sessionId/window')).
211 | put(webdriver.CommandName.GET_CURRENT_WINDOW_HANDLE,
212 | get('/session/:sessionId/window_handle')).
213 | put(webdriver.CommandName.GET_WINDOW_HANDLES,
214 | get('/session/:sessionId/window_handles')).
215 | put(webdriver.CommandName.GET_CURRENT_URL,
216 | get('/session/:sessionId/url')).
217 | put(webdriver.CommandName.GET, post('/session/:sessionId/url')).
218 | put(webdriver.CommandName.GO_BACK, post('/session/:sessionId/back')).
219 | put(webdriver.CommandName.GO_FORWARD,
220 | post('/session/:sessionId/forward')).
221 | put(webdriver.CommandName.REFRESH,
222 | post('/session/:sessionId/refresh')).
223 | put(webdriver.CommandName.ADD_COOKIE,
224 | post('/session/:sessionId/cookie')).
225 | put(webdriver.CommandName.GET_ALL_COOKIES,
226 | get('/session/:sessionId/cookie')).
227 | put(webdriver.CommandName.DELETE_ALL_COOKIES,
228 | del('/session/:sessionId/cookie')).
229 | put(webdriver.CommandName.DELETE_COOKIE,
230 | del('/session/:sessionId/cookie/:name')).
231 | put(webdriver.CommandName.FIND_ELEMENT,
232 | post('/session/:sessionId/element')).
233 | put(webdriver.CommandName.FIND_ELEMENTS,
234 | post('/session/:sessionId/elements')).
235 | put(webdriver.CommandName.GET_ACTIVE_ELEMENT,
236 | post('/session/:sessionId/element/active')).
237 | put(webdriver.CommandName.FIND_CHILD_ELEMENT,
238 | post('/session/:sessionId/element/:id/element')).
239 | put(webdriver.CommandName.FIND_CHILD_ELEMENTS,
240 | post('/session/:sessionId/element/:id/elements')).
241 | put(webdriver.CommandName.CLEAR_ELEMENT,
242 | post('/session/:sessionId/element/:id/clear')).
243 | put(webdriver.CommandName.CLICK_ELEMENT,
244 | post('/session/:sessionId/element/:id/click')).
245 | put(webdriver.CommandName.SEND_KEYS_TO_ELEMENT,
246 | post('/session/:sessionId/element/:id/value')).
247 | put(webdriver.CommandName.SUBMIT_ELEMENT,
248 | post('/session/:sessionId/element/:id/submit')).
249 | put(webdriver.CommandName.GET_ELEMENT_TEXT,
250 | get('/session/:sessionId/element/:id/text')).
251 | put(webdriver.CommandName.GET_ELEMENT_TAG_NAME,
252 | get('/session/:sessionId/element/:id/name')).
253 | put(webdriver.CommandName.IS_ELEMENT_SELECTED,
254 | get('/session/:sessionId/element/:id/selected')).
255 | put(webdriver.CommandName.IS_ELEMENT_ENABLED,
256 | get('/session/:sessionId/element/:id/enabled')).
257 | put(webdriver.CommandName.IS_ELEMENT_DISPLAYED,
258 | get('/session/:sessionId/element/:id/displayed')).
259 | put(webdriver.CommandName.GET_ELEMENT_LOCATION,
260 | get('/session/:sessionId/element/:id/location')).
261 | put(webdriver.CommandName.GET_ELEMENT_SIZE,
262 | get('/session/:sessionId/element/:id/size')).
263 | put(webdriver.CommandName.GET_ELEMENT_ATTRIBUTE,
264 | get('/session/:sessionId/element/:id/attribute/:name')).
265 | put(webdriver.CommandName.GET_ELEMENT_VALUE_OF_CSS_PROPERTY,
266 | get('/session/:sessionId/element/:id/css/:propertyName')).
267 | put(webdriver.CommandName.ELEMENT_EQUALS,
268 | get('/session/:sessionId/element/:id/equals/:other')).
269 | put(webdriver.CommandName.SWITCH_TO_WINDOW,
270 | post('/session/:sessionId/window')).
271 | put(webdriver.CommandName.MAXIMIZE_WINDOW,
272 | post('/session/:sessionId/window/:windowHandle/maximize')).
273 | put(webdriver.CommandName.GET_WINDOW_POSITION,
274 | get('/session/:sessionId/window/:windowHandle/position')).
275 | put(webdriver.CommandName.SET_WINDOW_POSITION,
276 | post('/session/:sessionId/window/:windowHandle/position')).
277 | put(webdriver.CommandName.GET_WINDOW_SIZE,
278 | get('/session/:sessionId/window/:windowHandle/size')).
279 | put(webdriver.CommandName.SET_WINDOW_SIZE,
280 | post('/session/:sessionId/window/:windowHandle/size')).
281 | put(webdriver.CommandName.SWITCH_TO_FRAME,
282 | post('/session/:sessionId/frame')).
283 | put(webdriver.CommandName.GET_PAGE_SOURCE,
284 | get('/session/:sessionId/source')).
285 | put(webdriver.CommandName.GET_TITLE,
286 | get('/session/:sessionId/title')).
287 | put(webdriver.CommandName.EXECUTE_SCRIPT,
288 | post('/session/:sessionId/execute')).
289 | put(webdriver.CommandName.EXECUTE_ASYNC_SCRIPT,
290 | post('/session/:sessionId/execute_async')).
291 | put(webdriver.CommandName.SCREENSHOT,
292 | get('/session/:sessionId/screenshot')).
293 | put(webdriver.CommandName.SET_TIMEOUT,
294 | post('/session/:sessionId/timeouts')).
295 | put(webdriver.CommandName.SET_SCRIPT_TIMEOUT,
296 | post('/session/:sessionId/timeouts/async_script')).
297 | put(webdriver.CommandName.IMPLICITLY_WAIT,
298 | post('/session/:sessionId/timeouts/implicit_wait')).
299 | put(webdriver.CommandName.MOVE_TO, post('/session/:sessionId/moveto')).
300 | put(webdriver.CommandName.CLICK, post('/session/:sessionId/click')).
301 | put(webdriver.CommandName.DOUBLE_CLICK,
302 | post('/session/:sessionId/doubleclick')).
303 | put(webdriver.CommandName.MOUSE_DOWN,
304 | post('/session/:sessionId/buttondown')).
305 | put(webdriver.CommandName.MOUSE_UP, post('/session/:sessionId/buttonup')).
306 | put(webdriver.CommandName.MOVE_TO, post('/session/:sessionId/moveto')).
307 | put(webdriver.CommandName.SEND_KEYS_TO_ACTIVE_ELEMENT,
308 | post('/session/:sessionId/keys')).
309 | put(webdriver.CommandName.TOUCH_SINGLE_TAP,
310 | post('/session/:sessionId/touch/click')).
311 | put(webdriver.CommandName.TOUCH_DOUBLE_TAP,
312 | post('/session/:sessionId/touch/doubleclick')).
313 | put(webdriver.CommandName.TOUCH_DOWN,
314 | post('/session/:sessionId/touch/down')).
315 | put(webdriver.CommandName.TOUCH_UP,
316 | post('/session/:sessionId/touch/up')).
317 | put(webdriver.CommandName.TOUCH_MOVE,
318 | post('/session/:sessionId/touch/move')).
319 | put(webdriver.CommandName.TOUCH_SCROLL,
320 | post('/session/:sessionId/touch/scroll')).
321 | put(webdriver.CommandName.TOUCH_LONG_PRESS,
322 | post('/session/:sessionId/touch/longclick')).
323 | put(webdriver.CommandName.TOUCH_FLICK,
324 | post('/session/:sessionId/touch/flick')).
325 | put(webdriver.CommandName.ACCEPT_ALERT,
326 | post('/session/:sessionId/accept_alert')).
327 | put(webdriver.CommandName.DISMISS_ALERT,
328 | post('/session/:sessionId/dismiss_alert')).
329 | put(webdriver.CommandName.GET_ALERT_TEXT,
330 | get('/session/:sessionId/alert_text')).
331 | put(webdriver.CommandName.SET_ALERT_TEXT,
332 | post('/session/:sessionId/alert_text')).
333 | put(webdriver.CommandName.GET_LOG, post('/session/:sessionId/log')).
334 | put(webdriver.CommandName.GET_AVAILABLE_LOG_TYPES,
335 | get('/session/:sessionId/log/types')).
336 | put(webdriver.CommandName.GET_SESSION_LOGS, post('/logs')).
337 | put(webdriver.CommandName.UPLOAD_FILE, post('/session/:sessionId/file')).
338 | build();
339 |
340 | /** @constructor */
341 | function Builder() {
342 | var map = {};
343 |
344 | this.put = function(name, resource) {
345 | map[name] = resource;
346 | return this;
347 | };
348 |
349 | this.build = function() {
350 | return map;
351 | };
352 | }
353 |
354 | function post(path) { return resource('POST', path); }
355 | function del(path) { return resource('DELETE', path); }
356 | function get(path) { return resource('GET', path); }
357 | function resource(method, path) { return {method: method, path: path}; }
358 | })();
359 |
360 |
361 | /**
362 | * Converts a headers object to a HTTP header block string.
363 | * @param {!Object.<string>} headers The headers object to convert.
364 | * @return {string} The headers as a string.
365 | * @private
366 | */
367 | webdriver.http.headersToString_ = function(headers) {
368 | var ret = [];
369 | for (var key in headers) {
370 | ret.push(key + ': ' + headers[key]);
371 | }
372 | return ret.join('\n');
373 | };
374 |
375 |
376 |
377 | /**
378 | * Describes a partial HTTP request. This class is a "partial" request and only
379 | * defines the path on the server to send a request to. It is each
380 | * {@link webdriver.http.Client}'s responsibility to build the full URL for the
381 | * final request.
382 | * @param {string} method The HTTP method to use for the request.
383 | * @param {string} path Path on the server to send the request to.
384 | * @param {Object=} opt_data This request's JSON data.
385 | * @constructor
386 | */
387 | webdriver.http.Request = function(method, path, opt_data) {
388 |
389 | /**
390 | * The HTTP method to use for the request.
391 | * @type {string}
392 | */
393 | this.method = method;
394 |
395 | /**
396 | * The path on the server to send the request to.
397 | * @type {string}
398 | */
399 | this.path = path;
400 |
401 | /**
402 | * This request's body.
403 | * @type {!Object}
404 | */
405 | this.data = opt_data || {};
406 |
407 | /**
408 | * The headers to send with the request.
409 | * @type {!Object.<(string|number)>}
410 | */
411 | this.headers = {'Accept': 'application/json; charset=utf-8'};
412 | };
413 |
414 |
415 | /** @override */
416 | webdriver.http.Request.prototype.toString = function() {
417 | return [
418 | this.method + ' ' + this.path + ' HTTP/1.1',
419 | webdriver.http.headersToString_(this.headers),
420 | '',
421 | goog.json.serialize(this.data)
422 | ].join('\n');
423 | };
424 |
425 |
426 |
427 | /**
428 | * Represents a HTTP response.
429 | * @param {number} status The response code.
430 | * @param {!Object.<string>} headers The response headers. All header
431 | * names will be converted to lowercase strings for consistent lookups.
432 | * @param {string} body The response body.
433 | * @constructor
434 | */
435 | webdriver.http.Response = function(status, headers, body) {
436 |
437 | /**
438 | * The HTTP response code.
439 | * @type {number}
440 | */
441 | this.status = status;
442 |
443 | /**
444 | * The response body.
445 | * @type {string}
446 | */
447 | this.body = body;
448 |
449 | /**
450 | * The response body.
451 | * @type {!Object.<string>}
452 | */
453 | this.headers = {};
454 | for (var header in headers) {
455 | this.headers[header.toLowerCase()] = headers[header];
456 | }
457 | };
458 |
459 |
460 | /**
461 | * Builds a {@link webdriver.http.Response} from a {@link XMLHttpRequest} or
462 | * {@link XDomainRequest} response object.
463 | * @param {!(XDomainRequest|XMLHttpRequest)} xhr The request to parse.
464 | * @return {!webdriver.http.Response} The parsed response.
465 | */
466 | webdriver.http.Response.fromXmlHttpRequest = function(xhr) {
467 | var headers = {};
468 |
469 | // getAllResponseHeaders is only available on XMLHttpRequest objects.
470 | if (xhr.getAllResponseHeaders) {
471 | var tmp = xhr.getAllResponseHeaders();
472 | if (tmp) {
473 | tmp = tmp.replace(/\r\n/g, '\n').split('\n');
474 | goog.array.forEach(tmp, function(header) {
475 | var parts = header.split(/\s*:\s*/, 2);
476 | if (parts[0]) {
477 | headers[parts[0]] = parts[1] || '';
478 | }
479 | });
480 | }
481 | }
482 |
483 | // If xhr is a XDomainRequest object, it will not have a status.
484 | // However, if we're parsing the response from a XDomainRequest, then
485 | // that request must have been a success, so we can assume status == 200.
486 | var status = xhr.status || 200;
487 | return new webdriver.http.Response(status, headers,
488 | xhr.responseText.replace(/\0/g, ''));
489 | };
490 |
491 |
492 | /** @override */
493 | webdriver.http.Response.prototype.toString = function() {
494 | var headers = webdriver.http.headersToString_(this.headers);
495 | var ret = ['HTTP/1.1 ' + this.status, headers];
496 |
497 | if (headers) {
498 | ret.push('');
499 | }
500 |
501 | if (this.body) {
502 | ret.push(this.body);
503 | }
504 |
505 | return ret.join('\n');
506 | };