diff --git a/components/api/src/main/java/com/hotels/styx/api/extension/service/TlsSettings.java b/components/api/src/main/java/com/hotels/styx/api/extension/service/TlsSettings.java index b749589dc3..7793078bbc 100644 --- a/components/api/src/main/java/com/hotels/styx/api/extension/service/TlsSettings.java +++ b/components/api/src/main/java/com/hotels/styx/api/extension/service/TlsSettings.java @@ -1,5 +1,5 @@ /* - Copyright (C) 2013-2018 Expedia Inc. + Copyright (C) 2013-2019 Expedia Inc. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -23,6 +23,7 @@ import java.util.Collections; import java.util.List; import java.util.Objects; +import java.util.Optional; import java.util.Set; import static com.google.common.base.Objects.firstNonNull; @@ -47,6 +48,8 @@ public class TlsSettings { private final char[] trustStorePassword; private final List protocols; private final List cipherSuites; + private final boolean sendSni; + private final Optional sniHost; private TlsSettings(Builder builder) { this.trustAllCerts = requireNonNull(builder.trustAllCerts); @@ -56,6 +59,8 @@ private TlsSettings(Builder builder) { this.trustStorePassword = toCharArray(builder.trustStorePassword); this.protocols = ImmutableList.copyOf(builder.protocols); this.cipherSuites = ImmutableList.copyOf(builder.cipherSuites); + this.sendSni = builder.sendSni; + this.sniHost = Optional.ofNullable(builder.sniHost); } private char[] toCharArray(String password) { @@ -94,6 +99,22 @@ public List cipherSuites() { return this.cipherSuites; } + public boolean sendSni() { + return sendSni; + } + + public Optional sniHost() { + return sniHost; + } + + /** + * This method will be invoked during the serialization process to return the SNI host name in a JSON-friendly format. + * @return configured SNI hostname or null if none + */ + public String getSniHost() { + return sniHost.orElse(null); + } + @Override public boolean equals(Object obj) { if (this == obj) { @@ -109,7 +130,9 @@ public boolean equals(Object obj) { && Objects.equals(this.trustStorePath, other.trustStorePath) && Arrays.equals(this.trustStorePassword, other.trustStorePassword) && Objects.equals(this.protocols, other.protocols) - && Objects.equals(this.cipherSuites, other.cipherSuites); + && Objects.equals(this.cipherSuites, other.cipherSuites) + && Objects.equals(this.sniHost, other.sniHost) + && Objects.equals(this.sendSni, other.sendSni); } @Override @@ -122,13 +145,15 @@ public String toString() { .add("trustStorePassword", this.trustStorePassword) .add("protocols", this.protocols) .add("cipherSuites", this.cipherSuites) + .add("sendSni", this.sendSni) + .add("sniHost", this.getSniHost()) .toString(); } @Override public int hashCode() { return Objects.hash(trustAllCerts, sslProvider, additionalCerts, - trustStorePath, Arrays.hashCode(trustStorePassword), protocols, cipherSuites); + trustStorePath, Arrays.hashCode(trustStorePassword), protocols, cipherSuites, sendSni, this.getSniHost()); } @@ -144,6 +169,8 @@ public static final class Builder { private String trustStorePassword = System.getProperty("javax.net.ssl.trustStorePassword"); private List protocols = Collections.emptyList(); private List cipherSuites = Collections.emptyList(); + private boolean sendSni = true; + private String sniHost; /** * Skips origin authentication. @@ -218,6 +245,17 @@ public Builder cipherSuites(List cipherSuites) { return this; } + public Builder sendSni(boolean sendSni) { + this.sendSni = sendSni; + return this; + } + + public Builder sniHost(String sniHost) { + this.sniHost = sniHost; + return this; + } + + public TlsSettings build() { if (!trustAllCerts && trustStorePassword == null) { throw new IllegalArgumentException("trustStorePassword must be supplied when remote peer authentication is enabled."); diff --git a/components/client/src/main/java/com/hotels/styx/client/netty/connectionpool/FlowControllingHttpContentProducer.java b/components/client/src/main/java/com/hotels/styx/client/netty/connectionpool/FlowControllingHttpContentProducer.java index 4a08c846f0..7445c71fc5 100644 --- a/components/client/src/main/java/com/hotels/styx/client/netty/connectionpool/FlowControllingHttpContentProducer.java +++ b/components/client/src/main/java/com/hotels/styx/client/netty/connectionpool/FlowControllingHttpContentProducer.java @@ -1,5 +1,5 @@ /* - Copyright (C) 2013-2018 Expedia Inc. + Copyright (C) 2013-2019 Expedia Inc. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -45,6 +45,7 @@ class FlowControllingHttpContentProducer { private static final Logger LOGGER = LoggerFactory.getLogger(FlowControllingHttpContentProducer.class); + private static final int MAX_DEPTH = 1; private final StateMachine stateMachine; private final String loggingPrefix; @@ -65,7 +66,6 @@ class FlowControllingHttpContentProducer { final AtomicLong queueDepthChunks = new AtomicLong(0); private final Origin origin; - private final int MAX_DEPTH = 1; private volatile Subscriber contentSubscriber; diff --git a/components/client/src/main/java/com/hotels/styx/client/netty/connectionpool/NettyConnection.java b/components/client/src/main/java/com/hotels/styx/client/netty/connectionpool/NettyConnection.java index 0d43825ea9..6590dcf2e3 100644 --- a/components/client/src/main/java/com/hotels/styx/client/netty/connectionpool/NettyConnection.java +++ b/components/client/src/main/java/com/hotels/styx/client/netty/connectionpool/NettyConnection.java @@ -1,5 +1,5 @@ /* - Copyright (C) 2013-2018 Expedia Inc. + Copyright (C) 2013-2019 Expedia Inc. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -27,9 +27,11 @@ import io.netty.handler.codec.http.HttpClientCodec; import io.netty.handler.codec.http.HttpContentDecompressor; import io.netty.handler.ssl.SslContext; +import io.netty.handler.ssl.SslHandler; import io.netty.util.AttributeKey; import rx.Observable; +import java.util.Optional; import java.util.concurrent.TimeUnit; import static com.google.common.base.Objects.toStringHelper; @@ -40,6 +42,7 @@ */ public class NettyConnection implements Connection, TimeToFirstByteListener { private static final AttributeKey CLOSED_BY_STYX = AttributeKey.newInstance("CLOSED_BY_STYX"); + private static final int IGNORED_PORT_NUMBER = -1; private final Origin origin; private final Channel channel; @@ -52,26 +55,33 @@ public class NettyConnection implements Connection, TimeToFirstByteListener { /** * Constructs an instance with an arbitrary UUID. * - * @param origin the origin connected to - * @param channel the netty channel used + * @param origin the origin connected to + * @param channel the netty channel used * @param requestOperationFactory used to create operation objects that send http requests via this connection + * @param httpConfig configuration settings for the origin + * @param sslContext TLS context in case of secure connections + * @param sendSni include the servername extension (server name indicator) in the TLS handshake + * @param sniHost hostname override for the server name indicator */ - public NettyConnection(Origin origin, Channel channel, HttpRequestOperationFactory requestOperationFactory, HttpConfig httpConfig, SslContext sslContext) { + public NettyConnection(Origin origin, Channel channel, HttpRequestOperationFactory requestOperationFactory, + HttpConfig httpConfig, SslContext sslContext, boolean sendSni, Optional sniHost) { this.origin = requireNonNull(origin); this.channel = requireNonNull(channel); this.requestOperationFactory = requestOperationFactory; this.channel.pipeline().addLast(new TimeToFirstByteHandler(this)); this.channel.closeFuture().addListener(future -> listeners.announce().connectionClosed(NettyConnection.this)); - - addChannelHandlers(channel, httpConfig, sslContext); + addChannelHandlers(channel, httpConfig, sslContext, sendSni, sniHost.orElse(origin.host())); } - private static void addChannelHandlers(Channel channel, HttpConfig httpConfig, SslContext sslContext) { + private static void addChannelHandlers(Channel channel, HttpConfig httpConfig, SslContext sslContext, boolean sendSni, String targetHost) { ChannelPipeline pipeline = channel.pipeline(); if (sslContext != null) { - pipeline.addLast("ssl", sslContext.newHandler(channel.alloc())); + SslHandler sslHandler = sendSni + ? sslContext.newHandler(channel.alloc(), targetHost, IGNORED_PORT_NUMBER) + : sslContext.newHandler(channel.alloc()); + pipeline.addLast("ssl", sslHandler); } pipeline.addLast("http-codec", new HttpClientCodec(httpConfig.maxInitialLineLength(), httpConfig.maxHeadersSize(), httpConfig.maxChunkSize())); diff --git a/components/client/src/main/java/com/hotels/styx/client/netty/connectionpool/NettyConnectionFactory.java b/components/client/src/main/java/com/hotels/styx/client/netty/connectionpool/NettyConnectionFactory.java index ec6e519e8d..691b7e04a9 100644 --- a/components/client/src/main/java/com/hotels/styx/client/netty/connectionpool/NettyConnectionFactory.java +++ b/components/client/src/main/java/com/hotels/styx/client/netty/connectionpool/NettyConnectionFactory.java @@ -1,5 +1,5 @@ /* - Copyright (C) 2013-2018 Expedia Inc. + Copyright (C) 2013-2019 Expedia Inc. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -35,6 +35,7 @@ import io.netty.handler.ssl.SslContext; import reactor.core.publisher.Mono; +import java.util.Optional; import java.util.concurrent.CompletableFuture; import static com.hotels.styx.client.HttpConfig.defaultHttpConfig; @@ -51,6 +52,8 @@ public class NettyConnectionFactory implements Connection.Factory { private final HttpConfig httpConfig; private final SslContext sslContext; + private final boolean sendSni; + private final Optional sniHost; private final HttpRequestOperationFactory httpRequestOperationFactory; private Bootstrap bootstrap; private EventLoopGroup eventLoopGroup; @@ -66,6 +69,8 @@ private NettyConnectionFactory(Builder builder) { this.sslContext = builder.tlsSettings == null ? null : SslContextFactory.get(builder.tlsSettings); this.clientSocketChannelClass = eventLoopGroupFactory.clientSocketChannelClass(); this.httpRequestOperationFactory = requireNonNull(builder.httpRequestOperationFactory); + this.sendSni = builder.tlsSettings != null && builder.tlsSettings.sendSni(); + this.sniHost = builder.tlsSettings != null ? builder.tlsSettings.sniHost() : Optional.empty(); } @Override @@ -79,7 +84,8 @@ public Mono createConnection(Origin origin, ConnectionSettings conne channelFuture.addListener(future -> { if (future.isSuccess()) { - sink.success(new NettyConnection(origin, channelFuture.channel(), httpRequestOperationFactory, httpConfig, sslContext)); + sink.success(new NettyConnection(origin, channelFuture.channel(), httpRequestOperationFactory, + httpConfig, sslContext, sendSni, sniHost)); } else { sink.error(new OriginUnreachableException(origin, future.cause())); } diff --git a/components/client/src/test/unit/java/com/hotels/styx/client/netty/connectionpool/NettyConnectionTest.java b/components/client/src/test/unit/java/com/hotels/styx/client/netty/connectionpool/NettyConnectionTest.java index bbc9e44fa4..4425cc7551 100644 --- a/components/client/src/test/unit/java/com/hotels/styx/client/netty/connectionpool/NettyConnectionTest.java +++ b/components/client/src/test/unit/java/com/hotels/styx/client/netty/connectionpool/NettyConnectionTest.java @@ -1,5 +1,5 @@ /* - Copyright (C) 2013-2018 Expedia Inc. + Copyright (C) 2013-2019 Expedia Inc. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -15,8 +15,8 @@ */ package com.hotels.styx.client.netty.connectionpool; -import com.hotels.styx.client.Connection; import com.hotels.styx.api.extension.Origin; +import com.hotels.styx.client.Connection; import com.hotels.styx.client.HttpConfig; import io.netty.channel.Channel; import io.netty.channel.embedded.EmbeddedChannel; @@ -66,7 +66,7 @@ public void notifiesListenersWhenConnectionIsClosed() throws Exception { } private Connection createConnection() { - return new NettyConnection(origin, channel, null, httpConfig, null); + return new NettyConnection(origin, channel, null, httpConfig, null, false, Optional.empty()); } static class EventCapturingListener implements Connection.Listener { diff --git a/components/proxy/src/main/java/com/hotels/styx/infrastructure/configuration/json/mixins/TlsSettingsMixin.java b/components/proxy/src/main/java/com/hotels/styx/infrastructure/configuration/json/mixins/TlsSettingsMixin.java index 4bbce6bdc8..ad32d27064 100644 --- a/components/proxy/src/main/java/com/hotels/styx/infrastructure/configuration/json/mixins/TlsSettingsMixin.java +++ b/components/proxy/src/main/java/com/hotels/styx/infrastructure/configuration/json/mixins/TlsSettingsMixin.java @@ -1,5 +1,5 @@ /* - Copyright (C) 2013-2018 Expedia Inc. + Copyright (C) 2013-2019 Expedia Inc. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -54,6 +54,12 @@ public interface TlsSettingsMixin { @JsonProperty("cipherSuites") List cipherSuites(); + @JsonProperty("sendSni") + boolean sendSni(); + + @JsonProperty("sniHost") + String getSniHost(); + /** * The builder for SSL settings. */ @@ -82,6 +88,12 @@ interface Builder { @JsonProperty("cipherSuites") Builder cipherSuites(List cipherSuites); + + @JsonProperty("sendSni") + Builder sendSni(boolean sendSni); + + @JsonProperty("sniHost") + Builder sniHost(String sniHost); } } diff --git a/components/proxy/src/test/java/com/hotels/styx/service/TlsSettingsTest.java b/components/proxy/src/test/java/com/hotels/styx/service/TlsSettingsTest.java index 816fc4393b..107e189e2a 100644 --- a/components/proxy/src/test/java/com/hotels/styx/service/TlsSettingsTest.java +++ b/components/proxy/src/test/java/com/hotels/styx/service/TlsSettingsTest.java @@ -1,5 +1,5 @@ /* - Copyright (C) 2013-2018 Expedia Inc. + Copyright (C) 2013-2019 Expedia Inc. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -27,6 +27,7 @@ import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.Optional; import static com.hotels.styx.api.extension.service.Certificate.certificate; import static com.hotels.styx.infrastructure.configuration.json.ObjectMappers.addStyxMixins; @@ -54,6 +55,8 @@ public void serialisesAllAttributes() throws Exception { .trustStorePassword("bar") .protocols(ImmutableList.of("TLSv1.2")) .cipherSuites(ImmutableList.of("TLS_RSA_WITH_AES_128_CBC_SHA")) + .sendSni(false) + .sniHost("some.sni.host") .build(); String result = mapper.writerWithDefaultPrettyPrinter().writeValueAsString(tlsSettings); @@ -67,6 +70,10 @@ public void serialisesAllAttributes() throws Exception { assertThat(result, containsString("\"trustStorePassword\" : \"bar")); assertThat(result, containsString("TLS_RSA_WITH_AES_128_CBC_SHA")); + + assertThat(result, containsString("\"sendSni\" : " + tlsSettings.sendSni())); + assertThat(result, containsString("\"sniHost\" : \"" + tlsSettings.sniHost().orElse("") + "\"")); + } @Test @@ -81,6 +88,8 @@ public void appliesDefaultTruststoreSettings() throws Exception { assertThat(tlsSettings.trustStorePassword(), is("".toCharArray())); assertThat(tlsSettings.protocols(), is(Collections.emptyList())); assertThat(tlsSettings.cipherSuites(), is(Collections.emptyList())); + assertThat(tlsSettings.sendSni(), is(true)); + assertThat(tlsSettings.sniHost(), is(Optional.empty())); } @Test(expectedExceptions = IllegalArgumentException.class) @@ -215,9 +224,13 @@ public void toStringPrintsAttributeNames() throws Exception { @Test public void isImmutable() throws Exception { - List protocols = new ArrayList() {{ add("TLSv1"); }}; - List cipherSuites = new ArrayList() {{ add("x"); }}; - Certificate[] certificates = new Certificate[] { certificate("x", "x") }; + List protocols = new ArrayList() {{ + add("TLSv1"); + }}; + List cipherSuites = new ArrayList() {{ + add("x"); + }}; + Certificate[] certificates = new Certificate[]{certificate("x", "x")}; TlsSettings tlsSettings = new TlsSettings.Builder() .additionalCerts(certificates) diff --git a/distribution/conf/styx-env.sh b/distribution/conf/styx-env.sh index 6cbb730803..428f8a1640 100644 --- a/distribution/conf/styx-env.sh +++ b/distribution/conf/styx-env.sh @@ -1,6 +1,6 @@ #!/usr/bin/env bash # -# Copyright (C) 2013-2018 Expedia Inc. +# Copyright (C) 2013-2019 Expedia Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/docs/user-guide/configure-tls.md b/docs/user-guide/configure-tls.md index cc0b6a2d9a..4284716f2a 100644 --- a/docs/user-guide/configure-tls.md +++ b/docs/user-guide/configure-tls.md @@ -140,7 +140,11 @@ be specified as separate backends. When absent, enables all default protocols for the `sslProvider`. Possible protocol names are: `TLS`, `TLSv1`, `TLSv1.1`, and `TLSv1.2`. -Attributes that accept lists can be defined with the following format: ['ITEM1', 'ITEM2'] + - *sendSni* - Send the Origin server hostname in the TLS handshake as per the SNI extension (https://tools.ietf.org/html/rfc6066). This feature is enabled by default. + + - *sniHost* - Override the hostname of the Origin server that will be sent in the SNI (server_name) extension. When this value is not set, the hostname will be the configured in `Origins\Host`. + +Attributes that accept lists can be defined with the following format: ['ITEM1', 'ITEM2'] ## Troubleshooting TLS Configuration