# http/index.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 the {@code webdriver.http.Client} for use with
17 | * NodeJS.
18 | */
19 |
20 | var http = require('http'),
21 | url = require('url');
22 |
23 | var base = require('../_base'),
24 | HttpResponse = base.require('webdriver.http.Response');
25 |
26 |
27 | /**
28 | * A {@link webdriver.http.Client} implementation using Node's built-in http
29 | * module.
30 | * @param {string} serverUrl URL for the WebDriver server to send commands to.
31 | * @param {http.Agent=} opt_agent The agent to use for each request.
32 | * Defaults to {@code http.globalAgent}.
33 | * @constructor
34 | * @implements {webdriver.http.Client}
35 | */
36 | var HttpClient = function(serverUrl, opt_agent) {
37 | var parsedUrl = url.parse(serverUrl);
38 | if (!parsedUrl.hostname) {
39 | throw new Error('Invalid server URL: ' + serverUrl);
40 | }
41 |
42 | /** @private {http.Agent} */
43 | this.agent_ = opt_agent;
44 |
45 | /**
46 | * Base options for each request.
47 | * @private {!Object}
48 | */
49 | this.options_ = {
50 | host: parsedUrl.hostname,
51 | path: parsedUrl.pathname,
52 | port: parsedUrl.port
53 | };
54 | };
55 |
56 |
57 | /** @override */
58 | HttpClient.prototype.send = function(httpRequest, callback) {
59 | var data;
60 | httpRequest.headers['Content-Length'] = 0;
61 | if (httpRequest.method == 'POST' || httpRequest.method == 'PUT') {
62 | data = JSON.stringify(httpRequest.data);
63 | httpRequest.headers['Content-Length'] = Buffer.byteLength(data, 'utf8');
64 | httpRequest.headers['Content-Type'] = 'application/json;charset=UTF-8';
65 | }
66 |
67 | var path = this.options_.path;
68 | if (path[path.length - 1] === '/' && httpRequest.path[0] === '/') {
69 | path += httpRequest.path.substring(1);
70 | } else {
71 | path += httpRequest.path;
72 | }
73 |
74 | var options = {
75 | method: httpRequest.method,
76 | host: this.options_.host,
77 | port: this.options_.port,
78 | path: path,
79 | headers: httpRequest.headers
80 | };
81 | if (this.agent_) {
82 | options.agent = this.agent_;
83 | }
84 | sendRequest(options, callback, data);
85 | };
86 |
87 |
88 | /**
89 | * Sends a single HTTP request.
90 | * @param {!Object} options The request options.
91 | * @param {function(Error, !webdriver.http.Response=)} callback The function to
92 | * invoke with the server's response.
93 | * @param {string=} opt_data The data to send with the request.
94 | */
95 | var sendRequest = function(options, callback, opt_data) {
96 | var request = http.request(options, function(response) {
97 | if (response.statusCode == 302 || response.statusCode == 303) {
98 | try {
99 | var location = url.parse(response.headers['location']);
100 | } catch (ex) {
101 | callback(Error(
102 | 'Failed to parse "Location" header for server redirect: ' +
103 | ex.message + '\nResponse was: \n' +
104 | new HttpResponse(response.statusCode, response.headers, '')));
105 | return;
106 | }
107 |
108 | if (!location.hostname) {
109 | location.hostname = options.host;
110 | location.port = options.port;
111 | }
112 |
113 | request.abort();
114 | sendRequest({
115 | method: 'GET',
116 | host: location.hostname,
117 | path: location.pathname + (location.search || ''),
118 | port: location.port,
119 | headers: {
120 | 'Accept': 'application/json; charset=utf-8'
121 | }
122 | }, callback);
123 | return;
124 | }
125 |
126 | var body = [];
127 | response.on('data', body.push.bind(body));
128 | response.on('end', function() {
129 | var resp = new HttpResponse(response.statusCode,
130 | response.headers, body.join('').replace(/\0/g, ''));
131 | callback(null, resp);
132 | });
133 | });
134 |
135 | request.on('error', function(e) {
136 | if (e.code === 'ECONNRESET') {
137 | setTimeout(function() {
138 | sendRequest(options, callback, opt_data);
139 | }, 15);
140 | } else {
141 | var message = e.message;
142 | if (e.code) {
143 | message = e.code + ' ' + message;
144 | }
145 | callback(new Error(message));
146 | }
147 | });
148 |
149 | if (opt_data) {
150 | request.write(opt_data);
151 | }
152 |
153 | request.end();
154 | };
155 |
156 |
157 | // PUBLIC API
158 |
159 | /** @type {webdriver.http.Executor.} */
160 | exports.Executor = base.require('webdriver.http.Executor');
161 |
162 | /** @type {webdriver.http.Request.} */
163 | exports.Request = base.require('webdriver.http.Request');
164 |
165 | /** @type {webdriver.http.Response.} */
166 | exports.Response = base.require('webdriver.http.Response');
167 |
168 | exports.HttpClient = HttpClient;