# 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 at6//7//      http://www.apache.org/licenses/LICENSE-2.08//9// Unless required by applicable law or agreed to in writing, software10// 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 and13// limitations under the License.1415/**16 * @fileoverview Defines a {@code webdriver.CommandExecutor} that communicates17 * with a server over HTTP.18 */1920goog.provide('webdriver.http.Client');21goog.provide('webdriver.http.Executor');22goog.provide('webdriver.http.Request');23goog.provide('webdriver.http.Response');2425goog.require('bot.ErrorCode');26goog.require('goog.array');27goog.require('goog.json');28goog.require('webdriver.CommandName');29goog.require('webdriver.promise.Deferred');30313233/**34 * Interface used for sending individual HTTP requests to the server.35 * @interface36 */37webdriver.http.Client = function() {38};394041/**42 * Sends a request to the server. If an error occurs while sending the request,43 * such as a failure to connect to the server, the provided callback will be44 * invoked with a non-null {@code Error} describing the error. Otherwise, when45 * the server's response has been received, the callback will be invoked with a46 * null Error and non-null {@code webdriver.http.Response} object.47 *48 * @param {!webdriver.http.Request} request The request to send.49 * @param {function(Error, !webdriver.http.Response=)} callback the function to50 *     invoke when the server's response is ready.51 */52webdriver.http.Client.prototype.send = function(request, callback) {53};54555657/**58 * A command executor that communicates with a server using the WebDriver59 * command protocol.60 * @param {!webdriver.http.Client} client The client to use when sending61 *     requests to the server.62 * @constructor63 * @implements {webdriver.CommandExecutor}64 */65webdriver.http.Executor = function(client) {6667  /**68   * Client used to communicate with the server.69   * @private {!webdriver.http.Client}70   */71  this.client_ = client;72};737475/** @override */76webdriver.http.Executor.prototype.execute = function(command, callback) {77  var resource = webdriver.http.Executor.COMMAND_MAP_[command.getName()];78  if (!resource) {79    throw new Error('Unrecognized command: ' + command.getName());80  }8182  var parameters = command.getParameters();83  var path = webdriver.http.Executor.buildPath_(resource.path, parameters);84  var request = new webdriver.http.Request(resource.method, path, parameters);8586  this.client_.send(request, function(e, response) {87    var responseObj;88    if (!e) {89      try {90        responseObj = webdriver.http.Executor.parseHttpResponse_(91            /** @type {!webdriver.http.Response} */ (response));92      } catch (ex) {93        e = ex;94      }95    }96    callback(e, responseObj);97  });98};99100101/**102 * Builds a fully qualified path using the given set of command parameters. Each103 * path segment prefixed with ':' will be replaced by the value of the104 * corresponding parameter. All parameters spliced into the path will be105 * removed from the parameter map.106 * @param {string} path The original resource path.107 * @param {!Object.<*>} parameters The parameters object to splice into108 *     the path.109 * @return {string} The modified path.110 * @private111 */112webdriver.http.Executor.buildPath_ = function(path, parameters) {113  var pathParameters = path.match(/\/:(\w+)\b/g);114  if (pathParameters) {115    for (var i = 0; i < pathParameters.length; ++i) {116      var key = pathParameters[i].substring(2);  // Trim the /:117      if (key in parameters) {118        var value = parameters[key];119        // TODO: move webdriver.WebElement.ELEMENT definition to a120        // common file so we can reference it here without pulling in all of121        // webdriver.WebElement's dependencies.122        if (value && value['ELEMENT']) {123          // When inserting a WebElement into the URL, only use its ID value,124          // not the full JSON.125          value = value['ELEMENT'];126        }127        path = path.replace(pathParameters[i], '/' + value);128        delete parameters[key];129      } else {130        throw new Error('Missing required parameter: ' + key);131      }132    }133  }134  return path;135};136137138/**139 * Callback used to parse {@link webdriver.http.Response} objects from a140 * {@link webdriver.http.Client}.141 * @param {!webdriver.http.Response} httpResponse The HTTP response to parse.142 * @return {!bot.response.ResponseObject} The parsed response.143 * @private144 */145webdriver.http.Executor.parseHttpResponse_ = function(httpResponse) {146  try {147    return /** @type {!bot.response.ResponseObject} */ (goog.json.parse(148        httpResponse.body));149  } catch (ex) {150    // Whoops, looks like the server sent us a malformed response. We'll need151    // to manually build a response object based on the response code.152  }153154  var response = {155    'status': bot.ErrorCode.SUCCESS,156    'value': httpResponse.body.replace(/\r\n/g, '\n')157  };158159  if (!(httpResponse.status > 199 && httpResponse.status < 300)) {160    // 404 represents an unknown command; anything else is a generic unknown161    // error.162    response['status'] = httpResponse.status == 404 ?163        bot.ErrorCode.UNKNOWN_COMMAND :164        bot.ErrorCode.UNKNOWN_ERROR;165  }166167  return response;168};169170171/**172 * Maps command names to resource locator.173 * @private {!Object.<{method:string, path:string}>}174 * @const175 */176webdriver.http.Executor.COMMAND_MAP_ = (function() {177  return new Builder().178      put(webdriver.CommandName.GET_SERVER_STATUS, get('/status')).179      put(webdriver.CommandName.NEW_SESSION, post('/session')).180      put(webdriver.CommandName.GET_SESSIONS, get('/sessions')).181      put(webdriver.CommandName.DESCRIBE_SESSION, get('/session/:sessionId')).182      put(webdriver.CommandName.QUIT, del('/session/:sessionId')).183      put(webdriver.CommandName.CLOSE, del('/session/:sessionId/window')).184      put(webdriver.CommandName.GET_CURRENT_WINDOW_HANDLE,185          get('/session/:sessionId/window_handle')).186      put(webdriver.CommandName.GET_WINDOW_HANDLES,187          get('/session/:sessionId/window_handles')).188      put(webdriver.CommandName.GET_CURRENT_URL,189          get('/session/:sessionId/url')).190      put(webdriver.CommandName.GET, post('/session/:sessionId/url')).191      put(webdriver.CommandName.GO_BACK, post('/session/:sessionId/back')).192      put(webdriver.CommandName.GO_FORWARD,193          post('/session/:sessionId/forward')).194      put(webdriver.CommandName.REFRESH,195          post('/session/:sessionId/refresh')).196      put(webdriver.CommandName.ADD_COOKIE,197          post('/session/:sessionId/cookie')).198      put(webdriver.CommandName.GET_ALL_COOKIES,199          get('/session/:sessionId/cookie')).200      put(webdriver.CommandName.DELETE_ALL_COOKIES,201          del('/session/:sessionId/cookie')).202      put(webdriver.CommandName.DELETE_COOKIE,203          del('/session/:sessionId/cookie/:name')).204      put(webdriver.CommandName.FIND_ELEMENT,205          post('/session/:sessionId/element')).206      put(webdriver.CommandName.FIND_ELEMENTS,207          post('/session/:sessionId/elements')).208      put(webdriver.CommandName.GET_ACTIVE_ELEMENT,209          post('/session/:sessionId/element/active')).210      put(webdriver.CommandName.FIND_CHILD_ELEMENT,211          post('/session/:sessionId/element/:id/element')).212      put(webdriver.CommandName.FIND_CHILD_ELEMENTS,213          post('/session/:sessionId/element/:id/elements')).214      put(webdriver.CommandName.CLEAR_ELEMENT,215          post('/session/:sessionId/element/:id/clear')).216      put(webdriver.CommandName.CLICK_ELEMENT,217          post('/session/:sessionId/element/:id/click')).218      put(webdriver.CommandName.SEND_KEYS_TO_ELEMENT,219          post('/session/:sessionId/element/:id/value')).220      put(webdriver.CommandName.SUBMIT_ELEMENT,221          post('/session/:sessionId/element/:id/submit')).222      put(webdriver.CommandName.GET_ELEMENT_TEXT,223          get('/session/:sessionId/element/:id/text')).224      put(webdriver.CommandName.GET_ELEMENT_TAG_NAME,225          get('/session/:sessionId/element/:id/name')).226      put(webdriver.CommandName.IS_ELEMENT_SELECTED,227          get('/session/:sessionId/element/:id/selected')).228      put(webdriver.CommandName.IS_ELEMENT_ENABLED,229          get('/session/:sessionId/element/:id/enabled')).230      put(webdriver.CommandName.IS_ELEMENT_DISPLAYED,231          get('/session/:sessionId/element/:id/displayed')).232      put(webdriver.CommandName.GET_ELEMENT_LOCATION,233          get('/session/:sessionId/element/:id/location')).234      put(webdriver.CommandName.GET_ELEMENT_SIZE,235          get('/session/:sessionId/element/:id/size')).236      put(webdriver.CommandName.GET_ELEMENT_ATTRIBUTE,237          get('/session/:sessionId/element/:id/attribute/:name')).238      put(webdriver.CommandName.GET_ELEMENT_VALUE_OF_CSS_PROPERTY,239          get('/session/:sessionId/element/:id/css/:propertyName')).240      put(webdriver.CommandName.ELEMENT_EQUALS,241          get('/session/:sessionId/element/:id/equals/:other')).242      put(webdriver.CommandName.SWITCH_TO_WINDOW,243          post('/session/:sessionId/window')).244      put(webdriver.CommandName.MAXIMIZE_WINDOW,245          post('/session/:sessionId/window/:windowHandle/maximize')).246      put(webdriver.CommandName.GET_WINDOW_POSITION,247          get('/session/:sessionId/window/:windowHandle/position')).248      put(webdriver.CommandName.SET_WINDOW_POSITION,249          post('/session/:sessionId/window/:windowHandle/position')).250      put(webdriver.CommandName.GET_WINDOW_SIZE,251          get('/session/:sessionId/window/:windowHandle/size')).252      put(webdriver.CommandName.SET_WINDOW_SIZE,253          post('/session/:sessionId/window/:windowHandle/size')).254      put(webdriver.CommandName.SWITCH_TO_FRAME,255          post('/session/:sessionId/frame')).256      put(webdriver.CommandName.GET_PAGE_SOURCE,257          get('/session/:sessionId/source')).258      put(webdriver.CommandName.GET_TITLE,259          get('/session/:sessionId/title')).260      put(webdriver.CommandName.EXECUTE_SCRIPT,261          post('/session/:sessionId/execute')).262      put(webdriver.CommandName.EXECUTE_ASYNC_SCRIPT,263          post('/session/:sessionId/execute_async')).264      put(webdriver.CommandName.SCREENSHOT,265          get('/session/:sessionId/screenshot')).266      put(webdriver.CommandName.SET_TIMEOUT,267          post('/session/:sessionId/timeouts')).268      put(webdriver.CommandName.SET_SCRIPT_TIMEOUT,269          post('/session/:sessionId/timeouts/async_script')).270      put(webdriver.CommandName.IMPLICITLY_WAIT,271          post('/session/:sessionId/timeouts/implicit_wait')).272      put(webdriver.CommandName.MOVE_TO, post('/session/:sessionId/moveto')).273      put(webdriver.CommandName.CLICK, post('/session/:sessionId/click')).274      put(webdriver.CommandName.DOUBLE_CLICK,275          post('/session/:sessionId/doubleclick')).276      put(webdriver.CommandName.MOUSE_DOWN,277          post('/session/:sessionId/buttondown')).278      put(webdriver.CommandName.MOUSE_UP, post('/session/:sessionId/buttonup')).279      put(webdriver.CommandName.MOVE_TO, post('/session/:sessionId/moveto')).280      put(webdriver.CommandName.SEND_KEYS_TO_ACTIVE_ELEMENT,281          post('/session/:sessionId/keys')).282      put(webdriver.CommandName.ACCEPT_ALERT,283          post('/session/:sessionId/accept_alert')).284      put(webdriver.CommandName.DISMISS_ALERT,285          post('/session/:sessionId/dismiss_alert')).286      put(webdriver.CommandName.GET_ALERT_TEXT,287          get('/session/:sessionId/alert_text')).288      put(webdriver.CommandName.SET_ALERT_TEXT,289          post('/session/:sessionId/alert_text')).290      put(webdriver.CommandName.GET_LOG, post('/session/:sessionId/log')).291      put(webdriver.CommandName.GET_AVAILABLE_LOG_TYPES,292          get('/session/:sessionId/log/types')).293      put(webdriver.CommandName.GET_SESSION_LOGS, post('/logs')).294      build();295296  /** @constructor */297  function Builder() {298    var map = {};299300    this.put = function(name, resource) {301      map[name] = resource;302      return this;303    };304305    this.build = function() {306      return map;307    };308  }309310  function post(path) { return resource('POST', path); }311  function del(path)  { return resource('DELETE', path); }312  function get(path)  { return resource('GET', path); }313  function resource(method, path) { return {method: method, path: path}; }314})();315316317/**318 * Converts a headers object to a HTTP header block string.319 * @param {!Object.<string>} headers The headers object to convert.320 * @return {string} The headers as a string.321 * @private322 */323webdriver.http.headersToString_ = function(headers) {324  var ret = [];325  for (var key in headers) {326    ret.push(key + ': ' + headers[key]);327  }328  return ret.join('\n');329};330331332333/**334 * Describes a partial HTTP request. This class is a "partial" request and only335 * defines the path on the server to send a request to. It is each336 * {@code webdriver.http.Client}'s responsibility to build the full URL for the337 * final request.338 * @param {string} method The HTTP method to use for the request.339 * @param {string} path Path on the server to send the request to.340 * @param {Object=} opt_data This request's JSON data.341 * @constructor342 */343webdriver.http.Request = function(method, path, opt_data) {344345  /**346   * The HTTP method to use for the request.347   * @type {string}348   */349  this.method = method;350351  /**352   * The path on the server to send the request to.353   * @type {string}354   */355  this.path = path;356357  /**358   * This request's body.359   * @type {!Object}360   */361  this.data = opt_data || {};362363  /**364   * The headers to send with the request.365   * @type {!Object.<(string|number)>}366   */367  this.headers = {'Accept': 'application/json; charset=utf-8'};368};369370371/** @override */372webdriver.http.Request.prototype.toString = function() {373  return [374    this.method + ' ' + this.path + ' HTTP/1.1',375    webdriver.http.headersToString_(this.headers),376    '',377    goog.json.serialize(this.data)378  ].join('\n');379};380381382383/**384 * Represents a HTTP response.385 * @param {number} status The response code.386 * @param {!Object.<string>} headers The response headers. All header387 *     names will be converted to lowercase strings for consistent lookups.388 * @param {string} body The response body.389 * @constructor390 */391webdriver.http.Response = function(status, headers, body) {392393  /**394   * The HTTP response code.395   * @type {number}396   */397  this.status = status;398399  /**400   * The response body.401   * @type {string}402   */403  this.body = body;404405  /**406   * The response body.407   * @type {!Object.<string>}408   */409  this.headers = {};410  for (var header in headers) {411    this.headers[header.toLowerCase()] = headers[header];412  }413};414415416/**417 * Builds a {@code webdriver.http.Response} from a {@code XMLHttpRequest} or418 * {@code XDomainRequest} response object.419 * @param {!(XDomainRequest|XMLHttpRequest)} xhr The request to parse.420 * @return {!webdriver.http.Response} The parsed response.421 */422webdriver.http.Response.fromXmlHttpRequest = function(xhr) {423  var headers = {};424425  // getAllResponseHeaders is only available on XMLHttpRequest objects.426  if (xhr.getAllResponseHeaders) {427    var tmp = xhr.getAllResponseHeaders();428    if (tmp) {429      tmp = tmp.replace(/\r\n/g, '\n').split('\n');430      goog.array.forEach(tmp, function(header) {431        var parts = header.split(/\s*:\s*/, 2);432        if (parts[0]) {433          headers[parts[0]] = parts[1] || '';434        }435      });436    }437  }438439  // If xhr is a XDomainRequest object, it will not have a status.440  // However, if we're parsing the response from a XDomainRequest, then441  // that request must have been a success, so we can assume status == 200.442  var status = xhr.status || 200;443  return new webdriver.http.Response(status, headers,444      xhr.responseText.replace(/\0/g, ''));445};446447448/** @override */449webdriver.http.Response.prototype.toString = function() {450  var headers = webdriver.http.headersToString_(this.headers);451  var ret = ['HTTP/1.1 ' + this.status, headers];452453  if (headers) {454    ret.push('');455  }456457  if (this.body) {458    ret.push(this.body);459  }460461  return ret.join('\n');462};