Skip to content

Commit 96ed95a

Browse files
committed
[js] Fix proxy configuration for geckodriver
The geckodriver follows the W3C spec for proxy capabilities (where host and port are specified separately), so we need to translate the legacy Selenium proxy config object. This logic should be reversed (defaulting to W3C spec) when more browsers support W3C over legacy. Also, geckodriver requires the proxy to be configured through requiredCapabilities, see mozilla/geckodriver#97
1 parent 91b3777 commit 96ed95a

File tree

7 files changed

+227
-21
lines changed

7 files changed

+227
-21
lines changed

javascript/node/selenium-webdriver/CHANGES.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
* Removed the mandatory use of Firefox Dev Edition, when using Marionette driver
1111
* Fixed timeouts' URL
1212
* Properly send HTTP requests when using a WebDriver server proxy
13+
* Properly configure proxies when using the geckodriver
1314

1415
### API Changes
1516

javascript/node/selenium-webdriver/firefox/index.js

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -331,6 +331,40 @@ function prepareProfile(profile, port) {
331331
}
332332

333333

334+
function normalizeProxyConfiguration(config) {
335+
if ('manual' === config.proxyType) {
336+
if (config.ftpProxy && !config.ftpProxyPort) {
337+
let hostAndPort = net.splitHostAndPort(config.ftpProxy);
338+
config.ftpProxy = hostAndPort.host;
339+
config.ftpProxyPort = hostAndPort.port;
340+
}
341+
342+
if (config.httpProxy && !config.httpProxyPort) {
343+
let hostAndPort = net.splitHostAndPort(config.httpProxy);
344+
config.httpProxy = hostAndPort.host;
345+
config.httpProxyPort = hostAndPort.port;
346+
}
347+
348+
if (config.sslProxy && !config.sslProxyPort) {
349+
let hostAndPort = net.splitHostAndPort(config.sslProxy);
350+
config.sslProxy = hostAndPort.host;
351+
config.sslProxyPort = hostAndPort.port;
352+
}
353+
354+
if (config.socksProxy && !config.socksProxyPort) {
355+
let hostAndPort = net.splitHostAndPort(config.socksProxy);
356+
config.socksProxy = hostAndPort.host;
357+
config.socksProxyPort = hostAndPort.port;
358+
}
359+
} else if ('pac' === config.proxyType) {
360+
if (config.proxyAutoconfigUrl && !config.pacUrl) {
361+
config.pacUrl = config.proxyAutoconfigUrl;
362+
}
363+
}
364+
return config;
365+
}
366+
367+
334368
/**
335369
* A WebDriver client for Firefox.
336370
*/
@@ -381,6 +415,18 @@ class Driver extends webdriver.WebDriver {
381415
caps.set(Capability.PROFILE, profile.encode());
382416
}
383417

418+
if (caps.has(capabilities.Capability.PROXY)) {
419+
let proxy = normalizeProxyConfiguration(
420+
caps.get(capabilities.Capability.PROXY));
421+
422+
// Marionette requires proxy settings to be specified as required
423+
// capabilities. See mozilla/geckodriver#97
424+
let required = new capabilities.Capabilities()
425+
.set(capabilities.Capability.PROXY, proxy);
426+
427+
caps.delete(capabilities.Capability.PROXY);
428+
caps = {required, desired: caps};
429+
}
384430
} else {
385431
profile = profile || new Profile;
386432

javascript/node/selenium-webdriver/lib/webdriver.js

Lines changed: 45 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -308,20 +308,61 @@ class WebDriver {
308308

309309
/**
310310
* Creates a new WebDriver session.
311+
*
312+
* By default, the requested session `capabilities` are merely "desired" and
313+
* the remote end will still create a new session even if it cannot satisfy
314+
* all of the requested capabilities. You can query which capabilities a
315+
* session actually has using the
316+
* {@linkplain #getCapabilities() getCapabilities()} method on the returned
317+
* WebDriver instance.
318+
*
319+
* To define _required capabilities_, provide the `capabilities` as an object
320+
* literal with `required` and `desired` keys. The `desired` key may be
321+
* omitted if all capabilities are required, and vice versa. If the server
322+
* cannot create a session with all of the required capabilities, it will
323+
* return an {@linkplain error.SessionNotCreatedError}.
324+
*
325+
* let required = new Capabilities().set('browserName', 'firefox');
326+
* let desired = new Capabilities().set('version', '45');
327+
* let driver = WebDriver.createSession(executor, {required, desired});
328+
*
329+
* This function will always return a WebDriver instance. If there is an error
330+
* creating the session, such as the aforementioned SessionNotCreatedError,
331+
* the driver will have a rejected {@linkplain #getSession session} promise.
332+
* It is recommended that this promise is left _unhandled_ so it will
333+
* propagate through the {@linkplain promise.ControlFlow control flow} and
334+
* cause subsequent commands to fail.
335+
*
336+
* let required = Capabilities.firefox();
337+
* let driver = WebDriver.createSession(executor, {required});
338+
*
339+
* // If the createSession operation failed, then this command will also
340+
* // also fail, propagating the creation failure.
341+
* driver.get('http://www.google.com').catch(e => console.log(e));
342+
*
311343
* @param {!command.Executor} executor The executor to create the new session
312344
* with.
313-
* @param {!./capabilities.Capabilities} desiredCapabilities The desired
345+
* @param {(!Capabilities|
346+
* {desired: (Capabilities|undefined),
347+
* required: (Capabilities|undefined)})} capabilities The desired
314348
* capabilities for the new session.
315349
* @param {promise.ControlFlow=} opt_flow The control flow all driver
316350
* commands should execute under, including the initial session creation.
317351
* Defaults to the {@link promise.controlFlow() currently active}
318352
* control flow.
319353
* @return {!WebDriver} The driver for the newly created session.
320354
*/
321-
static createSession(executor, desiredCapabilities, opt_flow) {
355+
static createSession(executor, capabilities, opt_flow) {
322356
let flow = opt_flow || promise.controlFlow();
323-
let cmd = new command.Command(command.Name.NEW_SESSION)
324-
.setParameter('desiredCapabilities', desiredCapabilities) ;
357+
let cmd = new command.Command(command.Name.NEW_SESSION);
358+
359+
if (capabilities && (capabilities.desired || capabilities.required)) {
360+
cmd.setParameter('desiredCapabilities', capabilities.desired);
361+
cmd.setParameter('requiredCapabilities', capabilities.required);
362+
} else {
363+
cmd.setParameter('desiredCapabilities', capabilities);
364+
}
365+
325366
let session = flow.execute(
326367
() => executeCommand(executor, cmd),
327368
'WebDriver.createSession()');

javascript/node/selenium-webdriver/net/index.js

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,3 +84,34 @@ exports.getAddress = function(opt_family) {
8484
exports.getLoopbackAddress = function(opt_family) {
8585
return getAddress(true, opt_family);
8686
};
87+
88+
89+
/**
90+
* Splits a hostport string, e.g. "www.example.com:80", into its component
91+
* parts.
92+
*
93+
* @param {string} hostport The string to split.
94+
* @return {{host: string, port: ?number}} A host and port. If no port is
95+
* present in the argument `hostport`, port is null.
96+
*/
97+
exports.splitHostAndPort = function(hostport) {
98+
let lastIndex = hostport.lastIndexOf(':');
99+
if (lastIndex < 0) {
100+
return {host: hostport, port: null};
101+
}
102+
103+
let firstIndex = hostport.indexOf(':');
104+
if (firstIndex != lastIndex && !hostport.includes('[')) {
105+
// Multiple colons but no brackets, so assume the string is an IPv6 address
106+
// with no port (e.g. "1234:5678:9:0:1234:5678:9:0").
107+
return {host: hostport, port: null};
108+
}
109+
110+
let host = hostport.slice(0, lastIndex);
111+
if (host.startsWith('[') && host.endsWith(']')) {
112+
host = host.slice(1, -1);
113+
}
114+
115+
let port = parseInt(hostport.slice(lastIndex + 1), 10);
116+
return {host, port};
117+
};

javascript/node/selenium-webdriver/test/lib/webdriver_test.js

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -345,6 +345,23 @@ describe('WebDriver', function() {
345345
return driver.getSession().then(v => assert.strictEqual(v, aSession));
346346
});
347347

348+
it('handles desired and requried capabilities', function() {
349+
let aSession = new Session(SESSION_ID, {'browserName': 'firefox'});
350+
let executor = new FakeExecutor().
351+
expect(CName.NEW_SESSION).
352+
withParameters({
353+
'desiredCapabilities': {'foo': 'bar'},
354+
'requiredCapabilities': {'bim': 'baz'}
355+
}).
356+
andReturnSuccess(aSession).
357+
end();
358+
359+
let desired = new Capabilities().set('foo', 'bar');
360+
let required = new Capabilities().set('bim', 'baz');
361+
var driver = WebDriver.createSession(executor, {desired, required});
362+
return driver.getSession().then(v => assert.strictEqual(v, aSession));
363+
});
364+
348365
it('failsToCreateSession', function() {
349366
let executor = new FakeExecutor().
350367
expect(CName.NEW_SESSION).
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
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+
'use strict';
19+
20+
var assert = require('assert');
21+
22+
var net = require('../../net');
23+
24+
describe('net.splitHostAndPort', function() {
25+
it('hostname with no port', function() {
26+
assert.deepEqual(
27+
net.splitHostAndPort('www.example.com'),
28+
{host: 'www.example.com', port: null});
29+
});
30+
31+
it('hostname with port', function() {
32+
assert.deepEqual(
33+
net.splitHostAndPort('www.example.com:80'),
34+
{host: 'www.example.com', port: 80});
35+
});
36+
37+
it('IPv4 with no port', function() {
38+
assert.deepEqual(
39+
net.splitHostAndPort('127.0.0.1'),
40+
{host: '127.0.0.1', port: null});
41+
});
42+
43+
it('IPv4 with port', function() {
44+
assert.deepEqual(
45+
net.splitHostAndPort('127.0.0.1:1234'),
46+
{host: '127.0.0.1', port: 1234});
47+
});
48+
49+
it('IPv6 with no port', function() {
50+
assert.deepEqual(
51+
net.splitHostAndPort('1234:0:1000:5768:1234:5678:90'),
52+
{host: '1234:0:1000:5768:1234:5678:90', port: null});
53+
});
54+
55+
it('IPv6 with port', function() {
56+
assert.deepEqual(
57+
net.splitHostAndPort('[1234:0:1000:5768:1234:5678:90]:1234'),
58+
{host: '1234:0:1000:5768:1234:5678:90', port: 1234});
59+
});
60+
});

javascript/node/selenium-webdriver/test/proxy_test.js

Lines changed: 27 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -22,13 +22,13 @@ var http = require('http'),
2222

2323
var Browser = require('..').Browser,
2424
promise = require('..').promise,
25+
firefox = require('../firefox'),
2526
proxy = require('../proxy'),
2627
assert = require('../testing/assert'),
2728
test = require('../lib/test'),
2829
Server = require('../lib/test/httpserver').Server,
2930
Pages = test.Pages;
3031

31-
3232
test.suite(function(env) {
3333
function writeResponse(res, body, encoding, contentType) {
3434
res.writeHead(200, {
@@ -86,7 +86,6 @@ test.suite(function(env) {
8686
};
8787
}
8888

89-
9089
test.before(mkStartFunc(proxyServer));
9190
test.before(mkStartFunc(helloServer));
9291
test.before(mkStartFunc(goodbyeServer));
@@ -99,18 +98,28 @@ test.suite(function(env) {
9998
test.beforeEach(function() { driver = null; });
10099
test.afterEach(function() { driver && driver.quit(); });
101100

101+
function createDriver(proxy) {
102+
// For Firefox we need to explicitly enable proxies for localhost by
103+
// clearing the network.proxy.no_proxies_on preference.
104+
let profile = new firefox.Profile();
105+
profile.setPreference('network.proxy.no_proxies_on', '');
106+
107+
driver = env.builder()
108+
.setFirefoxOptions(new firefox.Options().setProfile(profile))
109+
.setProxy(proxy)
110+
.build();
111+
}
112+
102113
// Proxy support not implemented.
103114
test.ignore(env.browsers(Browser.IE, Browser.OPERA, Browser.SAFARI)).
104115
describe('manual proxy settings', function() {
105116
// phantomjs 1.9.1 in webdriver mode does not appear to respect proxy
106117
// settings.
107118
test.ignore(env.browsers(Browser.PHANTOM_JS)).
108119
it('can configure HTTP proxy host', function() {
109-
driver = env.builder().
110-
setProxy(proxy.manual({
111-
http: proxyServer.host()
112-
})).
113-
build();
120+
createDriver(proxy.manual({
121+
http: proxyServer.host()
122+
}));
114123

115124
driver.get(helloServer.url());
116125
assert(driver.getTitle()).equalTo('Proxy page');
@@ -119,14 +128,17 @@ test.suite(function(env) {
119128
});
120129

121130
// PhantomJS does not support bypassing the proxy for individual hosts.
122-
test.ignore(env.browsers(Browser.PHANTOM_JS)).
131+
// geckodriver does not support the bypass option, this must be configured
132+
// through profile preferences.
133+
test.ignore(env.browsers(
134+
Browser.FIREFOX,
135+
'legacy-' + Browser.FIREFOX,
136+
Browser.PHANTOM_JS)).
123137
it('can bypass proxy for specific hosts', function() {
124-
driver = env.builder().
125-
setProxy(proxy.manual({
126-
http: proxyServer.host(),
127-
bypass: helloServer.host()
128-
})).
129-
build();
138+
createDriver(proxy.manual({
139+
http: proxyServer.host(),
140+
bypass: helloServer.host()
141+
}));
130142

131143
driver.get(helloServer.url());
132144
assert(driver.getTitle()).equalTo('Hello');
@@ -148,9 +160,7 @@ test.suite(function(env) {
148160
Browser.IE, Browser.OPERA, Browser.PHANTOM_JS, Browser.SAFARI)).
149161
describe('pac proxy settings', function() {
150162
test.it('can configure proxy through PAC file', function() {
151-
driver = env.builder().
152-
setProxy(proxy.pac(proxyServer.url('/proxy.pac'))).
153-
build();
163+
createDriver(proxy.pac(proxyServer.url('/proxy.pac')));
154164

155165
driver.get(helloServer.url());
156166
assert(driver.getTitle()).equalTo('Proxy page');

0 commit comments

Comments
 (0)