Skip to content

Commit

Permalink
[js] Fix proxy configuration for geckodriver
Browse files Browse the repository at this point in the history
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
  • Loading branch information
jleyba committed Jun 30, 2016
1 parent 91b3777 commit 96ed95a
Show file tree
Hide file tree
Showing 7 changed files with 227 additions and 21 deletions.
1 change: 1 addition & 0 deletions javascript/node/selenium-webdriver/CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
* Removed the mandatory use of Firefox Dev Edition, when using Marionette driver
* Fixed timeouts' URL
* Properly send HTTP requests when using a WebDriver server proxy
* Properly configure proxies when using the geckodriver

### API Changes

Expand Down
46 changes: 46 additions & 0 deletions javascript/node/selenium-webdriver/firefox/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -331,6 +331,40 @@ function prepareProfile(profile, port) {
}


function normalizeProxyConfiguration(config) {
if ('manual' === config.proxyType) {
if (config.ftpProxy && !config.ftpProxyPort) {
let hostAndPort = net.splitHostAndPort(config.ftpProxy);
config.ftpProxy = hostAndPort.host;
config.ftpProxyPort = hostAndPort.port;
}

if (config.httpProxy && !config.httpProxyPort) {
let hostAndPort = net.splitHostAndPort(config.httpProxy);
config.httpProxy = hostAndPort.host;
config.httpProxyPort = hostAndPort.port;
}

if (config.sslProxy && !config.sslProxyPort) {
let hostAndPort = net.splitHostAndPort(config.sslProxy);
config.sslProxy = hostAndPort.host;
config.sslProxyPort = hostAndPort.port;
}

if (config.socksProxy && !config.socksProxyPort) {
let hostAndPort = net.splitHostAndPort(config.socksProxy);
config.socksProxy = hostAndPort.host;
config.socksProxyPort = hostAndPort.port;
}
} else if ('pac' === config.proxyType) {
if (config.proxyAutoconfigUrl && !config.pacUrl) {
config.pacUrl = config.proxyAutoconfigUrl;
}
}
return config;
}


/**
* A WebDriver client for Firefox.
*/
Expand Down Expand Up @@ -381,6 +415,18 @@ class Driver extends webdriver.WebDriver {
caps.set(Capability.PROFILE, profile.encode());
}

if (caps.has(capabilities.Capability.PROXY)) {
let proxy = normalizeProxyConfiguration(
caps.get(capabilities.Capability.PROXY));

// Marionette requires proxy settings to be specified as required
// capabilities. See mozilla/geckodriver#97
let required = new capabilities.Capabilities()
.set(capabilities.Capability.PROXY, proxy);

caps.delete(capabilities.Capability.PROXY);
caps = {required, desired: caps};
}
} else {
profile = profile || new Profile;

Expand Down
49 changes: 45 additions & 4 deletions javascript/node/selenium-webdriver/lib/webdriver.js
Original file line number Diff line number Diff line change
Expand Up @@ -308,20 +308,61 @@ class WebDriver {

/**
* Creates a new WebDriver session.
*
* By default, the requested session `capabilities` are merely "desired" and
* the remote end will still create a new session even if it cannot satisfy
* all of the requested capabilities. You can query which capabilities a
* session actually has using the
* {@linkplain #getCapabilities() getCapabilities()} method on the returned
* WebDriver instance.
*
* To define _required capabilities_, provide the `capabilities` as an object
* literal with `required` and `desired` keys. The `desired` key may be
* omitted if all capabilities are required, and vice versa. If the server
* cannot create a session with all of the required capabilities, it will
* return an {@linkplain error.SessionNotCreatedError}.
*
* let required = new Capabilities().set('browserName', 'firefox');
* let desired = new Capabilities().set('version', '45');
* let driver = WebDriver.createSession(executor, {required, desired});
*
* This function will always return a WebDriver instance. If there is an error
* creating the session, such as the aforementioned SessionNotCreatedError,
* the driver will have a rejected {@linkplain #getSession session} promise.
* It is recommended that this promise is left _unhandled_ so it will
* propagate through the {@linkplain promise.ControlFlow control flow} and
* cause subsequent commands to fail.
*
* let required = Capabilities.firefox();
* let driver = WebDriver.createSession(executor, {required});
*
* // If the createSession operation failed, then this command will also
* // also fail, propagating the creation failure.
* driver.get('http://www.google.com').catch(e => console.log(e));
*
* @param {!command.Executor} executor The executor to create the new session
* with.
* @param {!./capabilities.Capabilities} desiredCapabilities The desired
* @param {(!Capabilities|
* {desired: (Capabilities|undefined),
* required: (Capabilities|undefined)})} capabilities The desired
* capabilities for the new session.
* @param {promise.ControlFlow=} opt_flow The control flow all driver
* commands should execute under, including the initial session creation.
* Defaults to the {@link promise.controlFlow() currently active}
* control flow.
* @return {!WebDriver} The driver for the newly created session.
*/
static createSession(executor, desiredCapabilities, opt_flow) {
static createSession(executor, capabilities, opt_flow) {
let flow = opt_flow || promise.controlFlow();
let cmd = new command.Command(command.Name.NEW_SESSION)
.setParameter('desiredCapabilities', desiredCapabilities) ;
let cmd = new command.Command(command.Name.NEW_SESSION);

if (capabilities && (capabilities.desired || capabilities.required)) {
cmd.setParameter('desiredCapabilities', capabilities.desired);
cmd.setParameter('requiredCapabilities', capabilities.required);
} else {
cmd.setParameter('desiredCapabilities', capabilities);
}

let session = flow.execute(
() => executeCommand(executor, cmd),
'WebDriver.createSession()');
Expand Down
31 changes: 31 additions & 0 deletions javascript/node/selenium-webdriver/net/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -84,3 +84,34 @@ exports.getAddress = function(opt_family) {
exports.getLoopbackAddress = function(opt_family) {
return getAddress(true, opt_family);
};


/**
* Splits a hostport string, e.g. "www.example.com:80", into its component
* parts.
*
* @param {string} hostport The string to split.
* @return {{host: string, port: ?number}} A host and port. If no port is
* present in the argument `hostport`, port is null.
*/
exports.splitHostAndPort = function(hostport) {
let lastIndex = hostport.lastIndexOf(':');
if (lastIndex < 0) {
return {host: hostport, port: null};
}

let firstIndex = hostport.indexOf(':');
if (firstIndex != lastIndex && !hostport.includes('[')) {
// Multiple colons but no brackets, so assume the string is an IPv6 address
// with no port (e.g. "1234:5678:9:0:1234:5678:9:0").
return {host: hostport, port: null};
}

let host = hostport.slice(0, lastIndex);
if (host.startsWith('[') && host.endsWith(']')) {
host = host.slice(1, -1);
}

let port = parseInt(hostport.slice(lastIndex + 1), 10);
return {host, port};
};
17 changes: 17 additions & 0 deletions javascript/node/selenium-webdriver/test/lib/webdriver_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -345,6 +345,23 @@ describe('WebDriver', function() {
return driver.getSession().then(v => assert.strictEqual(v, aSession));
});

it('handles desired and requried capabilities', function() {
let aSession = new Session(SESSION_ID, {'browserName': 'firefox'});
let executor = new FakeExecutor().
expect(CName.NEW_SESSION).
withParameters({
'desiredCapabilities': {'foo': 'bar'},
'requiredCapabilities': {'bim': 'baz'}
}).
andReturnSuccess(aSession).
end();

let desired = new Capabilities().set('foo', 'bar');
let required = new Capabilities().set('bim', 'baz');
var driver = WebDriver.createSession(executor, {desired, required});
return driver.getSession().then(v => assert.strictEqual(v, aSession));
});

it('failsToCreateSession', function() {
let executor = new FakeExecutor().
expect(CName.NEW_SESSION).
Expand Down
60 changes: 60 additions & 0 deletions javascript/node/selenium-webdriver/test/net/index_test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
// Licensed to the Software Freedom Conservancy (SFC) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The SFC licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.

'use strict';

var assert = require('assert');

var net = require('../../net');

describe('net.splitHostAndPort', function() {
it('hostname with no port', function() {
assert.deepEqual(
net.splitHostAndPort('www.example.com'),
{host: 'www.example.com', port: null});
});

it('hostname with port', function() {
assert.deepEqual(
net.splitHostAndPort('www.example.com:80'),
{host: 'www.example.com', port: 80});
});

it('IPv4 with no port', function() {
assert.deepEqual(
net.splitHostAndPort('127.0.0.1'),
{host: '127.0.0.1', port: null});
});

it('IPv4 with port', function() {
assert.deepEqual(
net.splitHostAndPort('127.0.0.1:1234'),
{host: '127.0.0.1', port: 1234});
});

it('IPv6 with no port', function() {
assert.deepEqual(
net.splitHostAndPort('1234:0:1000:5768:1234:5678:90'),
{host: '1234:0:1000:5768:1234:5678:90', port: null});
});

it('IPv6 with port', function() {
assert.deepEqual(
net.splitHostAndPort('[1234:0:1000:5768:1234:5678:90]:1234'),
{host: '1234:0:1000:5768:1234:5678:90', port: 1234});
});
});
44 changes: 27 additions & 17 deletions javascript/node/selenium-webdriver/test/proxy_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,13 @@ var http = require('http'),

var Browser = require('..').Browser,
promise = require('..').promise,
firefox = require('../firefox'),
proxy = require('../proxy'),
assert = require('../testing/assert'),
test = require('../lib/test'),
Server = require('../lib/test/httpserver').Server,
Pages = test.Pages;


test.suite(function(env) {
function writeResponse(res, body, encoding, contentType) {
res.writeHead(200, {
Expand Down Expand Up @@ -86,7 +86,6 @@ test.suite(function(env) {
};
}


test.before(mkStartFunc(proxyServer));
test.before(mkStartFunc(helloServer));
test.before(mkStartFunc(goodbyeServer));
Expand All @@ -99,18 +98,28 @@ test.suite(function(env) {
test.beforeEach(function() { driver = null; });
test.afterEach(function() { driver && driver.quit(); });

function createDriver(proxy) {
// For Firefox we need to explicitly enable proxies for localhost by
// clearing the network.proxy.no_proxies_on preference.
let profile = new firefox.Profile();
profile.setPreference('network.proxy.no_proxies_on', '');

driver = env.builder()
.setFirefoxOptions(new firefox.Options().setProfile(profile))
.setProxy(proxy)
.build();
}

// Proxy support not implemented.
test.ignore(env.browsers(Browser.IE, Browser.OPERA, Browser.SAFARI)).
describe('manual proxy settings', function() {
// phantomjs 1.9.1 in webdriver mode does not appear to respect proxy
// settings.
test.ignore(env.browsers(Browser.PHANTOM_JS)).
it('can configure HTTP proxy host', function() {
driver = env.builder().
setProxy(proxy.manual({
http: proxyServer.host()
})).
build();
createDriver(proxy.manual({
http: proxyServer.host()
}));

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

// PhantomJS does not support bypassing the proxy for individual hosts.
test.ignore(env.browsers(Browser.PHANTOM_JS)).
// geckodriver does not support the bypass option, this must be configured
// through profile preferences.
test.ignore(env.browsers(
Browser.FIREFOX,
'legacy-' + Browser.FIREFOX,
Browser.PHANTOM_JS)).
it('can bypass proxy for specific hosts', function() {
driver = env.builder().
setProxy(proxy.manual({
http: proxyServer.host(),
bypass: helloServer.host()
})).
build();
createDriver(proxy.manual({
http: proxyServer.host(),
bypass: helloServer.host()
}));

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

driver.get(helloServer.url());
assert(driver.getTitle()).equalTo('Proxy page');
Expand Down

0 comments on commit 96ed95a

Please sign in to comment.