Skip to content

Commit

Permalink
https between components (SeleniumHQ#7767)
Browse files Browse the repository at this point in the history
Merging in support to run the standalone server and/or component parts of the server as https. Default is still http due to configuration pain. Note: none of the client bindings have been updated to communicate to a https server
  • Loading branch information
adamgoucher authored and nsatragno committed Dec 5, 2019
1 parent 3936d4d commit 0da3d6d
Show file tree
Hide file tree
Showing 9 changed files with 187 additions and 29 deletions.
Expand Up @@ -76,6 +76,11 @@ public Executable configure(String... args) {
toDisplay = "tracing.txt";
break;

case "security":
title = "About Security";
toDisplay = "security.txt";
break;

case "help":
default:
title = "Help";
Expand Down
1 change: 1 addition & 0 deletions java/server/src/org/openqa/selenium/grid/commands/help.txt
Expand Up @@ -2,6 +2,7 @@ You can find out more about topics concerning Selenium Grid by running
the "help" command followed by:

* tracing - Help on how distributed tracing works with selenium
* security - Help on how how to secure communications to and within selenium

Each topic will give you enough information to help you get started, and
contains some pointers on our site to provide more complete information.
88 changes: 88 additions & 0 deletions java/server/src/org/openqa/selenium/grid/commands/security.txt
@@ -0,0 +1,88 @@
Selenium Grid by default communicates over HTTP. This is fine for a lot
of use cases, especially if everything is contained within the firewall
and against test sites with testing data. However, if your server is
exposed to the Internet or is being used in environments with production
data (or that which has PII) then you should secure it.

Standalone

In order to run the server using HTTPS instead of HTTP you need to start
it with the `--https-private-key` and `--https-certificate` flags to
provide it the certificate and private key (as a PKCS8 file).

```
java -jar selenium.jar \
hub \
--https-private-key /path/to/key.pkcs8 \
--https-certificate /path/to/cert.pem
```

Distributed

Alternatively, if you are starting things individually you would also
specify https when telling where to find things.

```
java -jar selenium.jar \
sessions \
--https-private-key /path/to/key.pkcs8 \
--https-certificate /path/to/cert.pem
```

```
java -jar selenium.jar \
distributor \
--https-private-key /path/to/key.pkcs8 \
--https-certificate /path/to/cert.pem \
-s https://sessions.grid.com:5556
```

```
java -jar selenium.jar \
router \
--https-private-key /path/to/key.pkcs8 \
--https-certificate /path/to/cert.pem \
-s https://sessions.grid.com:5556 \
-d https://distributor.grid.com:5553 \
```

Certificates

The Selenium Grid will not operate with self-signed certificates, as a
result you will need to have some provisioned to you from a Certificate
Authority of some sort. For experimentation purposes you can use MiniCA to
create and sign your certificates.

```
minica --domains sessions.grid.com,distributor.grid.com,router.grid.com
```

This will create minica.pem and minica.key in the current directory as well as
cert.pem and key.pem in a directory `sessions.grid.com` which will have both
distributor.grid.com and router.grid.com as alternative names. Because Selenium
Grid requires the key to be in PKCS8, you have to convert it.

```
openssl pkcs8 \
-in sessions.grid.com/key.pem \
-topk8 \
-out sessions.grid.com/key.pkcs8 \
-nocrypt
```

And since we are using a non-standard CA, we have to teach Java about it. To do that
you add it to the cacert truststore which is by default, $JAVA_HOME/jre/lib/security/cacerts

```
sudo keytool \
-import \
-file /path/to/minica.pem \
-alias minica \
-keystore $JAVA_HOME/jre/lib/security/cacerts \
-storepass changeit \
-cacerts
```

More information can be found at:

* MiniCA: https://github.com/jsha/minica
Expand Up @@ -53,6 +53,14 @@ public class BaseServerFlags {
@ConfigValue(section = "server", name = "allow-cors")
private boolean allowCORS = false;

@Parameter(description = "Private key for https", names = "--https-private-key")
@ConfigValue(section = "server", name = "https-private-key")
private String httpsPrivateKey;

@Parameter(description = "Server certificate for https", names = "--https-certificate")
@ConfigValue(section = "server", name = "https-certificate")
private String httpsCertificate;

public BaseServerFlags(int defaultPort) {
this.port = defaultPort;
}
Expand Down
Expand Up @@ -24,6 +24,7 @@
import org.openqa.selenium.net.NetworkUtils;
import org.openqa.selenium.net.PortProber;

import java.io.File;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.Optional;
Expand Down Expand Up @@ -87,7 +88,7 @@ public URI getExternalUri() {
int port = getPort();

try {
return new URI("http", null, host, port, null, null, null);
return new URI(isSecure() ? "https" : "http", null, host, port, null, null, null);
} catch (URISyntaxException e) {
throw new ConfigException("Cannot determine external URI: " + e.getMessage());
}
Expand All @@ -96,4 +97,24 @@ public URI getExternalUri() {
public boolean getAllowCORS() {
return config.getBool("server", "allow-cors").orElse(false);
}

public boolean isSecure() {
return config.get("server", "https-private-key").isPresent() && config.get("server", "https-certificate").isPresent();
}

public File getPrivateKey() {
String privateKey = config.get("server", "https-private-key").orElse(null);
if (privateKey != null) {
return new File(privateKey);
}
throw new ConfigException("you must provide a private key via --https-private-key when using --https");
}

public File getCertificate() {
String certificatePath = config.get("server", "https-certificate").orElse(null);
if (certificatePath != null) {
return new File(certificatePath);
}
throw new ConfigException("you must provide a certificate via --https-certificate when using --https");
}
}
23 changes: 20 additions & 3 deletions java/server/src/org/openqa/selenium/netty/server/NettyServer.java
Expand Up @@ -25,17 +25,18 @@
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.logging.LogLevel;
import io.netty.handler.logging.LoggingHandler;
import io.netty.handler.ssl.SslContext;
import io.netty.handler.ssl.SslContextBuilder;
import org.openqa.selenium.grid.server.AddWebDriverSpecHeaders;
import org.openqa.selenium.grid.server.BaseServerOptions;
import org.openqa.selenium.grid.server.Server;
import org.openqa.selenium.grid.server.WrapExceptions;
import org.openqa.selenium.grid.server.BaseServerOptions;
import org.openqa.selenium.grid.server.Server;
import org.openqa.selenium.remote.http.HttpHandler;

import java.io.IOException;
import java.io.UncheckedIOException;
import java.net.MalformedURLException;
import javax.net.ssl.SSLException;
import java.net.URL;
import java.util.Objects;

Expand All @@ -46,19 +47,34 @@ public class NettyServer implements Server<NettyServer> {
private final int port;
private final URL externalUrl;
private final HttpHandler handler;
private final SslContext sslCtx;

private Channel channel;

public NettyServer(BaseServerOptions options, HttpHandler handler) {
Objects.requireNonNull(options, "Server options must be set.");
Objects.requireNonNull(handler, "Handler to use must be set.");

Boolean secure = options.isSecure();
if (secure) {
try {
sslCtx = SslContextBuilder.forServer(options.getCertificate(), options.getPrivateKey())
.build();
} catch (SSLException e) {
throw new UncheckedIOException(new IOException("Certificate problem.", e));
}

} else {
sslCtx = null;
}

this.handler = handler.with(new WrapExceptions().andThen(new AddWebDriverSpecHeaders()));

bossGroup = new NioEventLoopGroup(1);
workerGroup = new NioEventLoopGroup();

port = options.getPort();

try {
externalUrl = options.getExternalUri().toURL();
} catch (MalformedURLException e) {
Expand Down Expand Up @@ -92,10 +108,11 @@ public void stop() {

public NettyServer start() {
ServerBootstrap b = new ServerBootstrap();

b.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.handler(new LoggingHandler(LogLevel.INFO))
.childHandler(new SeleniumHttpInitializer(handler));
.childHandler(new SeleniumHttpInitializer(handler, sslCtx));

try {
channel = b.bind(port).sync().channel();
Expand Down
Expand Up @@ -22,6 +22,7 @@
import io.netty.handler.codec.http.HttpObjectAggregator;
import io.netty.handler.codec.http.HttpServerCodec;
import io.netty.handler.codec.http.HttpServerKeepAliveHandler;
import io.netty.handler.ssl.SslContext;
import io.netty.handler.stream.ChunkedWriteHandler;
import org.openqa.selenium.remote.http.HttpHandler;

Expand All @@ -30,13 +31,18 @@
class SeleniumHttpInitializer extends ChannelInitializer<SocketChannel> {

private HttpHandler seleniumHandler;
private SslContext sslCtx;

SeleniumHttpInitializer(HttpHandler seleniumHandler) {
SeleniumHttpInitializer(HttpHandler seleniumHandler, SslContext sslCtx) {
this.seleniumHandler = Objects.requireNonNull(seleniumHandler);
this.sslCtx = sslCtx;
}

@Override
protected void initChannel(SocketChannel ch) {
if (sslCtx != null) {
ch.pipeline().addLast("ssl", sslCtx.newHandler(ch.alloc()));
}
ch.pipeline().addLast("codec", new HttpServerCodec());
ch.pipeline().addLast("keep-alive", new HttpServerKeepAliveHandler());
ch.pipeline().addLast("chunked-write", new ChunkedWriteHandler());
Expand Down
20 changes: 12 additions & 8 deletions java/server/test/org/openqa/selenium/grid/router/EndToEndTest.java
Expand Up @@ -65,6 +65,7 @@
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.security.cert.CertificateException;
import java.util.Collection;
import java.util.Map;
import java.util.UUID;
Expand All @@ -82,6 +83,8 @@
import static org.openqa.selenium.remote.http.HttpMethod.GET;
import static org.openqa.selenium.remote.http.HttpMethod.POST;

import javax.net.ssl.SSLException;

@RunWith(Parameterized.class)
public class EndToEndTest {

Expand All @@ -94,7 +97,7 @@ public static Collection<Supplier<Object[]>> buildGrids() {
() -> {
try {
return createRemotes();
} catch (URISyntaxException e) {
} catch (CertificateException | SSLException | URISyntaxException e) {
throw new RuntimeException(e);
}
},
Expand All @@ -121,7 +124,7 @@ public void setFields() {
this.clientFactory = (HttpClient.Factory) raw[1];
}

private static Object[] createInMemory() throws URISyntaxException, MalformedURLException {
private static Object[] createInMemory() throws CertificateException, MalformedURLException, SSLException, URISyntaxException {
Tracer tracer = NoopTracerFactory.create();
EventBus bus = ZeroMqEventBus.create(
new ZContext(),
Expand Down Expand Up @@ -156,7 +159,7 @@ private static Object[] createInMemory() throws URISyntaxException, MalformedURL
return new Object[] { server, clientFactory };
}

private static Object[] createRemotes() throws URISyntaxException {
private static Object[] createRemotes() throws URISyntaxException, SSLException, CertificateException {
Tracer tracer = NoopTracerFactory.create();
EventBus bus = ZeroMqEventBus.create(
new ZContext(),
Expand Down Expand Up @@ -192,10 +195,11 @@ private static Object[] createRemotes() throws URISyntaxException {
LocalNode localNode = LocalNode.builder(tracer, bus, clientFactory, nodeUri)
.add(CAPS, createFactory(nodeUri))
.build();

Server<?> nodeServer = new NettyServer(
new BaseServerOptions(
new MapConfig(ImmutableMap.of("server", ImmutableMap.of("port", port)))),
localNode);
localNode);
nodeServer.start();

distributor.add(localNode);
Expand All @@ -207,12 +211,12 @@ private static Object[] createRemotes() throws URISyntaxException {
return new Object[] { routerServer, clientFactory };
}

private static Server<?> createServer(HttpHandler handler) {
private static Server<?> createServer(HttpHandler handler) throws CertificateException, SSLException{
int port = PortProber.findFreePort();
return new NettyServer(
new BaseServerOptions(
new MapConfig(ImmutableMap.of("server", ImmutableMap.of("port", port)))),
handler);
new BaseServerOptions(
new MapConfig(ImmutableMap.of("server", ImmutableMap.of("port", port)))),
handler);
}

private static SessionFactory createFactory(URI serverUri) {
Expand Down

0 comments on commit 0da3d6d

Please sign in to comment.