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