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
23var http = require('http'),
24 url = require('url');
25
26var 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 */
41var 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 */
67HttpClient.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 */
108var 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.} */
189exports.Executor = base.require('webdriver.http.Executor');
190
191/** @type {webdriver.http.Request.} */
192exports.Request = base.require('webdriver.http.Request');
193
194/** @type {webdriver.http.Response.} */
195exports.Response = base.require('webdriver.http.Response');
196
197exports.HttpClient = HttpClient;