lib/webdriver/http/http.js

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