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