diff --git a/java/google/registry/proxy/BUILD b/java/google/registry/proxy/BUILD
index 79719ae3e..8c9415278 100644
--- a/java/google/registry/proxy/BUILD
+++ b/java/google/registry/proxy/BUILD
@@ -70,6 +70,8 @@ container_image(
"30000",
"30001",
"30002",
+ "30010",
+ "30011",
],
)
diff --git a/java/google/registry/proxy/CertificateModule.java b/java/google/registry/proxy/CertificateModule.java
index d5f712818..abfdbe42a 100644
--- a/java/google/registry/proxy/CertificateModule.java
+++ b/java/google/registry/proxy/CertificateModule.java
@@ -44,7 +44,7 @@ import org.bouncycastle.openssl.PEMParser;
import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter;
/**
- * Dagger module that provides bindings needed to inject EPP SSL certificate chain and private key.
+ * Dagger module that provides bindings needed to inject server certificate chain and private key.
*
*
The production certificates and private key are stored in a .pem file that is encrypted by
* Cloud KMS. The .pem file can be generated by concatenating the .crt certificate files on the
@@ -60,17 +60,22 @@ import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter;
@Module
public class CertificateModule {
- /** Dagger qualifier to provide bindings related to EPP certificates */
+ /** Dagger qualifier to provide bindings related to the certificates that the server provides. */
@Qualifier
- public @interface EppCertificates {}
+ @interface ServerCertificates {}
/** Dagger qualifier to provide bindings when running locally. */
@Qualifier
- public @interface Local {}
+ @interface Local {}
- /** Dagger qualifier to provide bindings when running in production. */
+ /**
+ * Dagger qualifier to provide bindings when running in production.
+ *
+ *
The "production" here means that the proxy runs on GKE, as apposed to on a local machine. It
+ * does not necessary mean the production environment.
+ */
@Qualifier
- public @interface Prod {}
+ @interface Prod {}
static {
Security.addProvider(new BouncyCastleProvider());
@@ -95,7 +100,7 @@ public class CertificateModule {
@Singleton
@Provides
- @EppCertificates
+ @ServerCertificates
static X509Certificate[] provideCertificates(
Environment env,
@Local Lazy localCertificates,
@@ -105,7 +110,7 @@ public class CertificateModule {
@Singleton
@Provides
- @EppCertificates
+ @ServerCertificates
static PrivateKey providePrivateKey(
Environment env,
@Local Lazy localPrivateKey,
diff --git a/java/google/registry/proxy/EppProtocolModule.java b/java/google/registry/proxy/EppProtocolModule.java
index fa82703dc..e5d3f8d06 100644
--- a/java/google/registry/proxy/EppProtocolModule.java
+++ b/java/google/registry/proxy/EppProtocolModule.java
@@ -20,6 +20,7 @@ import com.google.common.collect.ImmutableList;
import dagger.Module;
import dagger.Provides;
import dagger.multibindings.IntoSet;
+import google.registry.proxy.CertificateModule.ServerCertificates;
import google.registry.proxy.HttpsRelayProtocolModule.HttpsRelayProtocol;
import google.registry.proxy.Protocol.BackendProtocol;
import google.registry.proxy.Protocol.FrontendProtocol;
@@ -37,8 +38,11 @@ import io.netty.channel.ChannelHandler;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.LengthFieldBasedFrameDecoder;
import io.netty.handler.codec.LengthFieldPrepender;
+import io.netty.handler.ssl.SslProvider;
import io.netty.handler.timeout.ReadTimeoutHandler;
import java.io.IOException;
+import java.security.PrivateKey;
+import java.security.cert.X509Certificate;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.ScheduledExecutorService;
import java.util.function.Supplier;
@@ -76,8 +80,8 @@ public class EppProtocolModule {
@Provides
@EppProtocol
static ImmutableList> provideHandlerProviders(
- Provider> sslServerInitializerProvider,
Provider proxyProtocolHandlerProvider,
+ @EppProtocol Provider> sslServerInitializerProvider,
@EppProtocol Provider readTimeoutHandlerProvider,
Provider lengthFieldBasedFrameDecoderProvider,
Provider lengthFieldPrependerProvider,
@@ -152,6 +156,16 @@ public class EppProtocolModule {
metrics);
}
+ @Singleton
+ @Provides
+ @EppProtocol
+ static SslServerInitializer provideSslServerInitializer(
+ SslProvider sslProvider,
+ @ServerCertificates PrivateKey privateKey,
+ @ServerCertificates X509Certificate... certificates) {
+ return new SslServerInitializer<>(true, sslProvider, privateKey, certificates);
+ }
+
@Provides
@EppProtocol
static TokenStore provideTokenStore(
diff --git a/java/google/registry/proxy/HealthCheckProtocolModule.java b/java/google/registry/proxy/HealthCheckProtocolModule.java
index 32fd37236..b40da6f1b 100644
--- a/java/google/registry/proxy/HealthCheckProtocolModule.java
+++ b/java/google/registry/proxy/HealthCheckProtocolModule.java
@@ -50,7 +50,7 @@ public class HealthCheckProtocolModule {
return Protocol.frontendBuilder()
.name(PROTOCOL_NAME)
.port(healthCheckPort)
- .isHealthCheck(true)
+ .hasBackend(false)
.handlerProviders(handlerProviders)
.build();
}
diff --git a/java/google/registry/proxy/Protocol.java b/java/google/registry/proxy/Protocol.java
index 9ffe68027..2f00a20d2 100644
--- a/java/google/registry/proxy/Protocol.java
+++ b/java/google/registry/proxy/Protocol.java
@@ -42,13 +42,9 @@ public interface Protocol {
/** The {@link ChannelHandler} providers to use for the protocol, in order. */
ImmutableList> handlerProviders();
- /**
- * A builder for {@link FrontendProtocol}, default is non-health-checking.
- *
- * @see HealthCheckProtocolModule
- */
+ /** A builder for {@link FrontendProtocol}, by default there is a backend associated with it. */
static FrontendProtocol.Builder frontendBuilder() {
- return new AutoValue_Protocol_FrontendProtocol.Builder().isHealthCheck(false);
+ return new AutoValue_Protocol_FrontendProtocol.Builder().hasBackend(true);
}
static BackendProtocol.Builder backendBuilder() {
@@ -83,18 +79,22 @@ public interface Protocol {
/**
* The {@link BackendProtocol} used to establish a relay channel and relay the traffic to. Not
- * required for health check protocol.
+ * required for health check protocol or HTTP(S) redirect.
*/
@Nullable
public abstract BackendProtocol relayProtocol();
- public abstract boolean isHealthCheck();
+ /**
+ * Whether this {@code FrontendProtocol} relays to a {@code BackendProtocol}. All proxied
+ * traffic must be represented by a protocol that has a backend.
+ */
+ public abstract boolean hasBackend();
@AutoValue.Builder
public abstract static class Builder extends Protocol.Builder {
public abstract Builder relayProtocol(BackendProtocol value);
- public abstract Builder isHealthCheck(boolean value);
+ public abstract Builder hasBackend(boolean value);
abstract FrontendProtocol autoBuild();
@@ -102,7 +102,7 @@ public interface Protocol {
public FrontendProtocol build() {
FrontendProtocol frontendProtocol = autoBuild();
Preconditions.checkState(
- frontendProtocol.isHealthCheck() || frontendProtocol.relayProtocol() != null,
+ !frontendProtocol.hasBackend() || frontendProtocol.relayProtocol() != null,
"Frontend protocol %s must define a relay protocol.",
frontendProtocol.name());
return frontendProtocol;
diff --git a/java/google/registry/proxy/ProxyConfig.java b/java/google/registry/proxy/ProxyConfig.java
index ceecad627..16a14bab2 100644
--- a/java/google/registry/proxy/ProxyConfig.java
+++ b/java/google/registry/proxy/ProxyConfig.java
@@ -44,6 +44,7 @@ public class ProxyConfig {
public Epp epp;
public Whois whois;
public HealthCheck healthCheck;
+ public WebWhois webWhois;
public HttpsRelay httpsRelay;
public Metrics metrics;
@@ -89,6 +90,13 @@ public class ProxyConfig {
public String checkResponse;
}
+ /** Configuration options that apply to web WHOIS redirects. */
+ public static class WebWhois {
+ public int httpPort;
+ public int httpsPort;
+ public String redirectHost;
+ }
+
/** Configuration options that apply to HTTPS relay protocol. */
public static class HttpsRelay {
public int port;
diff --git a/java/google/registry/proxy/ProxyModule.java b/java/google/registry/proxy/ProxyModule.java
index c4f1c8aae..1c3bf9109 100644
--- a/java/google/registry/proxy/ProxyModule.java
+++ b/java/google/registry/proxy/ProxyModule.java
@@ -38,6 +38,8 @@ import google.registry.proxy.EppProtocolModule.EppProtocol;
import google.registry.proxy.HealthCheckProtocolModule.HealthCheckProtocol;
import google.registry.proxy.Protocol.FrontendProtocol;
import google.registry.proxy.ProxyConfig.Environment;
+import google.registry.proxy.WebWhoisProtocolsModule.HttpWhoisProtocol;
+import google.registry.proxy.WebWhoisProtocolsModule.HttpsWhoisProtocol;
import google.registry.proxy.WhoisProtocolModule.WhoisProtocol;
import google.registry.proxy.handler.ProxyProtocolHandler;
import google.registry.util.Clock;
@@ -75,9 +77,15 @@ public class ProxyModule {
@Parameter(names = "--epp", description = "Port for EPP")
private Integer eppPort;
- @Parameter(names = "--health_check", description = "Port for health check protocol")
+ @Parameter(names = "--health_check", description = "Port for health check")
private Integer healthCheckPort;
+ @Parameter(names = "--http_whois", description = "Port for HTTP WHOIS")
+ private Integer httpWhoisPort;
+
+ @Parameter(names = "--https_whois", description = "Port for HTTPS WHOIS")
+ private Integer httpsWhoisPort;
+
@Parameter(names = "--env", description = "Environment to run the proxy in")
private Environment env = Environment.LOCAL;
@@ -165,6 +173,18 @@ public class ProxyModule {
return Optional.ofNullable(healthCheckPort).orElse(config.healthCheck.port);
}
+ @Provides
+ @HttpWhoisProtocol
+ int provideHttpWhoisProtocol(ProxyConfig config) {
+ return Optional.ofNullable(httpWhoisPort).orElse(config.webWhois.httpPort);
+ }
+
+ @Provides
+ @HttpsWhoisProtocol
+ int provideHttpsWhoisProtocol(ProxyConfig config) {
+ return Optional.ofNullable(httpsWhoisPort).orElse(config.webWhois.httpsPort);
+ }
+
@Provides
ImmutableMap providePortToProtocolMap(
Set protocolSet) {
@@ -316,16 +336,16 @@ public class ProxyModule {
/** Root level component that exposes the port-to-protocol map. */
@Singleton
@Component(
- modules = {
- ProxyModule.class,
- CertificateModule.class,
- HttpsRelayProtocolModule.class,
- WhoisProtocolModule.class,
- EppProtocolModule.class,
- HealthCheckProtocolModule.class,
- MetricsModule.class
- }
- )
+ modules = {
+ ProxyModule.class,
+ CertificateModule.class,
+ HttpsRelayProtocolModule.class,
+ WhoisProtocolModule.class,
+ WebWhoisProtocolsModule.class,
+ EppProtocolModule.class,
+ HealthCheckProtocolModule.class,
+ MetricsModule.class
+ })
interface ProxyComponent {
ImmutableMap portToProtocolMap();
diff --git a/java/google/registry/proxy/ProxyServer.java b/java/google/registry/proxy/ProxyServer.java
index 4a223173f..a02a4c94e 100644
--- a/java/google/registry/proxy/ProxyServer.java
+++ b/java/google/registry/proxy/ProxyServer.java
@@ -23,6 +23,7 @@ import com.google.common.flogger.FluentLogger;
import com.google.monitoring.metrics.MetricReporter;
import google.registry.proxy.Protocol.BackendProtocol;
import google.registry.proxy.Protocol.FrontendProtocol;
+import google.registry.proxy.ProxyConfig.Environment;
import google.registry.proxy.ProxyModule.ProxyComponent;
import io.netty.bootstrap.Bootstrap;
import io.netty.bootstrap.ServerBootstrap;
@@ -87,9 +88,9 @@ public class ProxyServer implements Runnable {
inboundChannel.attr(PROTOCOL_KEY).set(inboundProtocol);
addHandlers(inboundChannel.pipeline(), inboundProtocol.handlerProviders());
- if (inboundProtocol.isHealthCheck()) {
- // A health check server protocol has no relay channel. It simply replies to incoming
- // request with a preset response.
+ if (!inboundProtocol.hasBackend()) {
+ // If the frontend has no backend to relay to (health check, web WHOIS redirect, etc), start
+ // reading immediately.
inboundChannel.config().setAutoRead(true);
} else {
// Connect to the relay (outbound) channel specified by the BackendProtocol.
@@ -208,33 +209,35 @@ public class ProxyServer implements Runnable {
// Configure the components, this needs to run first so that the logging format is properly
// configured for each environment.
+ ProxyModule proxyModule = new ProxyModule().parse(args);
ProxyComponent proxyComponent =
- DaggerProxyModule_ProxyComponent.builder()
- .proxyModule(new ProxyModule().parse(args))
- .build();
+ DaggerProxyModule_ProxyComponent.builder().proxyModule(proxyModule).build();
- MetricReporter metricReporter = proxyComponent.metricReporter();
- try {
- metricReporter.startAsync().awaitRunning(10, TimeUnit.SECONDS);
- logger.atInfo().log("Started up MetricReporter");
- } catch (TimeoutException timeoutException) {
- logger.atSevere().withCause(timeoutException).log(
- "Failed to initialize MetricReporter: %s", timeoutException);
+ // Do not write metrics when running locally.
+ if (proxyModule.provideEnvironment() != Environment.LOCAL) {
+ MetricReporter metricReporter = proxyComponent.metricReporter();
+ try {
+ metricReporter.startAsync().awaitRunning(10, TimeUnit.SECONDS);
+ logger.atInfo().log("Started up MetricReporter");
+ } catch (TimeoutException timeoutException) {
+ logger.atSevere().withCause(timeoutException).log(
+ "Failed to initialize MetricReporter: %s", timeoutException);
+ }
+ Runtime.getRuntime()
+ .addShutdownHook(
+ new Thread(
+ () -> {
+ try {
+ metricReporter.stopAsync().awaitTerminated(10, TimeUnit.SECONDS);
+ logger.atInfo().log("Shut down MetricReporter");
+ } catch (TimeoutException timeoutException) {
+ logger.atWarning().withCause(timeoutException).log(
+ "Failed to stop MetricReporter: %s", timeoutException);
+ }
+ }));
}
- Runtime.getRuntime()
- .addShutdownHook(
- new Thread(
- () -> {
- try {
- metricReporter.stopAsync().awaitTerminated(10, TimeUnit.SECONDS);
- logger.atInfo().log("Shut down MetricReporter");
- } catch (TimeoutException timeoutException) {
- logger.atWarning().withCause(timeoutException).log(
- "Failed to stop MetricReporter: %s", timeoutException);
- }
- }));
-
+ // Start the proxy.
new ProxyServer(proxyComponent).run();
}
}
diff --git a/java/google/registry/proxy/WebWhoisProtocolsModule.java b/java/google/registry/proxy/WebWhoisProtocolsModule.java
new file mode 100644
index 000000000..eb612b53b
--- /dev/null
+++ b/java/google/registry/proxy/WebWhoisProtocolsModule.java
@@ -0,0 +1,139 @@
+// Copyright 2018 The Nomulus Authors. All Rights Reserved.
+//
+// Licensed 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.
+
+package google.registry.proxy;
+
+import com.google.common.collect.ImmutableList;
+import dagger.Module;
+import dagger.Provides;
+import dagger.multibindings.IntoSet;
+import google.registry.proxy.CertificateModule.ServerCertificates;
+import google.registry.proxy.Protocol.FrontendProtocol;
+import google.registry.proxy.handler.SslServerInitializer;
+import google.registry.proxy.handler.WebWhoisRedirectHandler;
+import io.netty.channel.ChannelHandler;
+import io.netty.channel.socket.nio.NioSocketChannel;
+import io.netty.handler.codec.http.HttpServerCodec;
+import io.netty.handler.codec.http.HttpServerExpectContinueHandler;
+import io.netty.handler.ssl.SslProvider;
+import java.security.PrivateKey;
+import java.security.cert.X509Certificate;
+import javax.inject.Provider;
+import javax.inject.Qualifier;
+import javax.inject.Singleton;
+
+/** A module that provides the {@link FrontendProtocol}s to redirect HTTP(S) web WHOIS requests. */
+@Module
+public class WebWhoisProtocolsModule {
+
+ /** Dagger qualifier to provide HTTP whois protocol related handlers and other bindings. */
+ @Qualifier
+ @interface HttpWhoisProtocol {}
+
+ /** Dagger qualifier to provide HTTPS whois protocol related handlers and other bindings. */
+ @Qualifier
+ @interface HttpsWhoisProtocol {}
+
+ private static final String HTTP_PROTOCOL_NAME = "whois_http";
+ private static final String HTTPS_PROTOCOL_NAME = "whois_https";
+
+ @Singleton
+ @Provides
+ @IntoSet
+ static FrontendProtocol provideHttpWhoisProtocol(
+ @HttpWhoisProtocol int httpWhoisPort,
+ @HttpWhoisProtocol ImmutableList> handlerProviders) {
+ return google.registry.proxy.Protocol.frontendBuilder()
+ .name(HTTP_PROTOCOL_NAME)
+ .port(httpWhoisPort)
+ .hasBackend(false)
+ .handlerProviders(handlerProviders)
+ .build();
+ }
+
+ @Singleton
+ @Provides
+ @IntoSet
+ static FrontendProtocol provideHttpsWhoisProtocol(
+ @HttpsWhoisProtocol int httpsWhoisPort,
+ @HttpsWhoisProtocol ImmutableList> handlerProviders) {
+ return google.registry.proxy.Protocol.frontendBuilder()
+ .name(HTTPS_PROTOCOL_NAME)
+ .port(httpsWhoisPort)
+ .hasBackend(false)
+ .handlerProviders(handlerProviders)
+ .build();
+ }
+
+ @Provides
+ @HttpWhoisProtocol
+ static ImmutableList> providerHttpWhoisHandlerProviders(
+ Provider httpServerCodecProvider,
+ Provider httpServerExpectContinueHandlerProvider,
+ @HttpWhoisProtocol Provider webWhoisRedirectHandlerProvides) {
+ return ImmutableList.of(
+ httpServerCodecProvider,
+ httpServerExpectContinueHandlerProvider,
+ webWhoisRedirectHandlerProvides);
+ };
+
+ @Provides
+ @HttpsWhoisProtocol
+ static ImmutableList> providerHttpsWhoisHandlerProviders(
+ @HttpsWhoisProtocol
+ Provider> sslServerInitializerProvider,
+ Provider httpServerCodecProvider,
+ Provider httpServerExpectContinueHandlerProvider,
+ @HttpsWhoisProtocol Provider webWhoisRedirectHandlerProvides) {
+ return ImmutableList.of(
+ sslServerInitializerProvider,
+ httpServerCodecProvider,
+ httpServerExpectContinueHandlerProvider,
+ webWhoisRedirectHandlerProvides);
+ };
+
+ @Provides
+ static HttpServerCodec provideHttpServerCodec() {
+ return new HttpServerCodec();
+ }
+
+ @Provides
+ @HttpWhoisProtocol
+ static WebWhoisRedirectHandler provideHttpRedirectHandler(
+ google.registry.proxy.ProxyConfig config) {
+ return new WebWhoisRedirectHandler(false, config.webWhois.redirectHost);
+ }
+
+ @Provides
+ @HttpsWhoisProtocol
+ static WebWhoisRedirectHandler provideHttpsRedirectHandler(
+ google.registry.proxy.ProxyConfig config) {
+ return new WebWhoisRedirectHandler(true, config.webWhois.redirectHost);
+ }
+
+ @Provides
+ static HttpServerExpectContinueHandler provideHttpServerExpectContinueHandler() {
+ return new HttpServerExpectContinueHandler();
+ }
+
+ @Singleton
+ @Provides
+ @HttpsWhoisProtocol
+ static SslServerInitializer provideSslServerInitializer(
+ SslProvider sslProvider,
+ @ServerCertificates PrivateKey privateKey,
+ @ServerCertificates X509Certificate... certificates) {
+ return new SslServerInitializer<>(false, sslProvider, privateKey, certificates);
+ }
+}
diff --git a/java/google/registry/proxy/config/default-config.yaml b/java/google/registry/proxy/config/default-config.yaml
index aaf5e0145..5bf54e835 100644
--- a/java/google/registry/proxy/config/default-config.yaml
+++ b/java/google/registry/proxy/config/default-config.yaml
@@ -187,6 +187,14 @@ httpsRelay:
# Maximum size of an HTTP message in bytes.
maxMessageLengthBytes: 524288
+webWhois:
+ httpPort: 30010
+ httpsPort: 30011
+
+ # The 302 redirect destination of HTTPS web WHOIS GET requests.
+ # HTTP web WHOIS GET requests will be 301 redirected to HTTPS first.
+ redirectHost: whois.yourdomain.tld
+
metrics:
# Max queries per second for the Google Cloud Monitoring V3 (aka Stackdriver)
# API. The limit can be adjusted by contacting Cloud Support.
diff --git a/java/google/registry/proxy/handler/HttpsRelayServiceHandler.java b/java/google/registry/proxy/handler/HttpsRelayServiceHandler.java
index 4dfe8dc0a..4e6974aff 100644
--- a/java/google/registry/proxy/handler/HttpsRelayServiceHandler.java
+++ b/java/google/registry/proxy/handler/HttpsRelayServiceHandler.java
@@ -93,7 +93,7 @@ abstract class HttpsRelayServiceHandler extends ByteToMessageCodec extends ChannelInitializer {
@@ -59,16 +55,18 @@ public class SslServerInitializer extends ChannelInitializer<
AttributeKey.valueOf("CLIENT_CERTIFICATE_PROMISE_KEY");
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+ private final boolean requireClientCert;
private final SslProvider sslProvider;
private final PrivateKey privateKey;
private final X509Certificate[] certificates;
- @Inject
- SslServerInitializer(
+ public SslServerInitializer(
+ boolean requireClientCert,
SslProvider sslProvider,
- @EppCertificates PrivateKey privateKey,
- @EppCertificates X509Certificate... certificates) {
+ PrivateKey privateKey,
+ X509Certificate... certificates) {
logger.atInfo().log("Server SSL Provider: %s", sslProvider);
+ this.requireClientCert = requireClientCert;
this.sslProvider = sslProvider;
this.privateKey = privateKey;
this.certificates = certificates;
@@ -80,26 +78,28 @@ public class SslServerInitializer extends ChannelInitializer<
SslContextBuilder.forServer(privateKey, certificates)
.sslProvider(sslProvider)
.trustManager(InsecureTrustManagerFactory.INSTANCE)
- .clientAuth(ClientAuth.REQUIRE)
+ .clientAuth(requireClientCert ? ClientAuth.REQUIRE : ClientAuth.NONE)
.build()
.newHandler(channel.alloc());
- Promise clientCertificatePromise = channel.eventLoop().newPromise();
- Future unusedFuture =
- sslHandler
- .handshakeFuture()
- .addListener(
- future -> {
- if (future.isSuccess()) {
- Promise unusedPromise =
- clientCertificatePromise.setSuccess(
- (X509Certificate)
- sslHandler.engine().getSession().getPeerCertificates()[0]);
- } else {
- Promise unusedPromise =
- clientCertificatePromise.setFailure(future.cause());
- }
- });
- channel.attr(CLIENT_CERTIFICATE_PROMISE_KEY).set(clientCertificatePromise);
+ if (requireClientCert) {
+ Promise clientCertificatePromise = channel.eventLoop().newPromise();
+ Future unusedFuture =
+ sslHandler
+ .handshakeFuture()
+ .addListener(
+ future -> {
+ if (future.isSuccess()) {
+ Promise unusedPromise =
+ clientCertificatePromise.setSuccess(
+ (X509Certificate)
+ sslHandler.engine().getSession().getPeerCertificates()[0]);
+ } else {
+ Promise unusedPromise =
+ clientCertificatePromise.setFailure(future.cause());
+ }
+ });
+ channel.attr(CLIENT_CERTIFICATE_PROMISE_KEY).set(clientCertificatePromise);
+ }
channel.pipeline().addLast(sslHandler);
}
}
diff --git a/java/google/registry/proxy/handler/WebWhoisRedirectHandler.java b/java/google/registry/proxy/handler/WebWhoisRedirectHandler.java
new file mode 100644
index 000000000..b0b77062a
--- /dev/null
+++ b/java/google/registry/proxy/handler/WebWhoisRedirectHandler.java
@@ -0,0 +1,137 @@
+// Copyright 2018 The Nomulus Authors. All Rights Reserved.
+//
+// Licensed 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.
+
+package google.registry.proxy.handler;
+
+import static io.netty.handler.codec.http.HttpHeaderNames.CONNECTION;
+import static io.netty.handler.codec.http.HttpHeaderNames.CONTENT_LENGTH;
+import static io.netty.handler.codec.http.HttpHeaderNames.CONTENT_TYPE;
+import static io.netty.handler.codec.http.HttpHeaderNames.HOST;
+import static io.netty.handler.codec.http.HttpHeaderNames.LOCATION;
+import static io.netty.handler.codec.http.HttpHeaderValues.KEEP_ALIVE;
+import static io.netty.handler.codec.http.HttpHeaderValues.TEXT_PLAIN;
+import static io.netty.handler.codec.http.HttpMethod.GET;
+import static io.netty.handler.codec.http.HttpResponseStatus.FORBIDDEN;
+import static io.netty.handler.codec.http.HttpResponseStatus.FOUND;
+import static io.netty.handler.codec.http.HttpResponseStatus.METHOD_NOT_ALLOWED;
+import static io.netty.handler.codec.http.HttpResponseStatus.MOVED_PERMANENTLY;
+import static io.netty.handler.codec.http.HttpResponseStatus.OK;
+import static io.netty.handler.codec.http.HttpVersion.HTTP_1_1;
+
+import com.google.common.base.Splitter;
+import com.google.common.flogger.FluentLogger;
+import io.netty.channel.ChannelFuture;
+import io.netty.channel.ChannelFutureListener;
+import io.netty.channel.ChannelHandlerContext;
+import io.netty.channel.SimpleChannelInboundHandler;
+import io.netty.handler.codec.http.DefaultFullHttpResponse;
+import io.netty.handler.codec.http.FullHttpResponse;
+import io.netty.handler.codec.http.HttpRequest;
+import io.netty.handler.codec.http.HttpUtil;
+import java.time.Duration;
+
+/**
+ * Handler that redirects web WHOIS requests to a canonical website.
+ *
+ * ICANN requires that port 43 and web-based WHOIS are both available on whois.nic.TLD. Since we
+ * expose a single IPv4/IPv6 anycast external IP address for the proxy, we need the load balancer to
+ * router port 80/443 traffic to the proxy to support web WHOIS.
+ *
+ *
HTTP (port 80) traffic is simply upgraded to HTTPS (port 443) on the same host, while HTTPS
+ * requests are redirected to the {@code redirectHost}, which is the canonical website that provide
+ * the web WHOIS service.
+ *
+ * @see
+ * REGISTRY AGREEMENT
+ */
+public class WebWhoisRedirectHandler extends SimpleChannelInboundHandler {
+
+ private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
+ /**
+ * HTTP health check sent by GCP HTTP load balancer is set to use this host name.
+ *
+ * Status 200 must be returned in order for a health check to be considered successful.
+ *
+ * @see
+ * HTTP, HTTPS, and HTTP/2 health checks
+ */
+ private static final String HEALTH_CHECK_HOST = "health-check.invalid";
+
+ private static final String HSTS_HEADER_NAME = "Strict-Transport-Security";
+ private static final Duration HSTS_MAX_AGE = Duration.ofDays(365);
+
+ private final boolean isHttps;
+ private final String redirectHost;
+
+ public WebWhoisRedirectHandler(boolean isHttps, String redirectHost) {
+ this.isHttps = isHttps;
+ this.redirectHost = redirectHost;
+ }
+
+ @Override
+ protected void channelRead0(ChannelHandlerContext ctx, HttpRequest msg) {
+ FullHttpResponse response;
+ // We only support GET, any other HTTP method should result in 405 error.
+ if (!msg.method().equals(GET)) {
+ response = new DefaultFullHttpResponse(HTTP_1_1, METHOD_NOT_ALLOWED);
+ } else {
+ // All HTTP/1.1 request must contain a Host header with the format "host:[port]".
+ // See https://tools.ietf.org/html/rfc2616#section-14.23
+ String host = Splitter.on(':').split(msg.headers().get(HOST)).iterator().next();
+ if (host.equals(HEALTH_CHECK_HOST)) {
+ // The health check request should always be sent to the HTTP port.
+ response =
+ isHttps
+ ? new DefaultFullHttpResponse(HTTP_1_1, FORBIDDEN)
+ : new DefaultFullHttpResponse(HTTP_1_1, OK);
+ ;
+ } else {
+ // HTTP -> HTTPS is a 301 redirect, whereas HTTPS -> web WHOIS site is 302 redirect.
+ response = new DefaultFullHttpResponse(HTTP_1_1, isHttps ? FOUND : MOVED_PERMANENTLY);
+ String redirectUrl = String.format("https://%s/", isHttps ? redirectHost : host);
+ response.headers().set(LOCATION, redirectUrl);
+ }
+ }
+ // Add HSTS header to HTTPS response.
+ if (isHttps) {
+ response
+ .headers()
+ .set(HSTS_HEADER_NAME, String.format("max-age=%d", HSTS_MAX_AGE.getSeconds()));
+ }
+ response
+ .headers()
+ .set(CONTENT_TYPE, TEXT_PLAIN)
+ .setInt(CONTENT_LENGTH, response.content().readableBytes());
+
+ // Close the connection if keep-alive is not set in the request.
+ if (!HttpUtil.isKeepAlive(msg)) {
+ ChannelFuture unusedFuture =
+ ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE);
+ } else {
+ response.headers().set(CONNECTION, KEEP_ALIVE);
+ ChannelFuture unusedFuture = ctx.writeAndFlush(response);
+ }
+ }
+
+ @Override
+ public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
+ logger.atSevere().withCause(cause).log(
+ (isHttps ? "HTTPS" : "HTTP") + " WHOIS inbound exception caught for channel %s",
+ ctx.channel());
+ ChannelFuture unusedFuture = ctx.close();
+ }
+}
diff --git a/javatests/google/registry/proxy/ProtocolModuleTest.java b/javatests/google/registry/proxy/ProtocolModuleTest.java
index 161687bdd..03a40ec62 100644
--- a/javatests/google/registry/proxy/ProtocolModuleTest.java
+++ b/javatests/google/registry/proxy/ProtocolModuleTest.java
@@ -175,15 +175,14 @@ public abstract class ProtocolModuleTest {
*/
@Singleton
@Component(
- modules = {
- TestModule.class,
- CertificateModule.class,
- WhoisProtocolModule.class,
- EppProtocolModule.class,
- HealthCheckProtocolModule.class,
- HttpsRelayProtocolModule.class
- }
- )
+ modules = {
+ TestModule.class,
+ CertificateModule.class,
+ WhoisProtocolModule.class,
+ EppProtocolModule.class,
+ HealthCheckProtocolModule.class,
+ HttpsRelayProtocolModule.class
+ })
interface TestComponent {
@WhoisProtocol
ImmutableList> whoisHandlers();
diff --git a/javatests/google/registry/proxy/ProxyModuleTest.java b/javatests/google/registry/proxy/ProxyModuleTest.java
index 3536e2747..40916f455 100644
--- a/javatests/google/registry/proxy/ProxyModuleTest.java
+++ b/javatests/google/registry/proxy/ProxyModuleTest.java
@@ -41,6 +41,10 @@ public class ProxyModuleTest {
assertThat(proxyModule.provideEppPort(PROXY_CONFIG)).isEqualTo(PROXY_CONFIG.epp.port);
assertThat(proxyModule.provideHealthCheckPort(PROXY_CONFIG))
.isEqualTo(PROXY_CONFIG.healthCheck.port);
+ assertThat(proxyModule.provideHttpWhoisProtocol(PROXY_CONFIG))
+ .isEqualTo(PROXY_CONFIG.webWhois.httpPort);
+ assertThat(proxyModule.provideHttpsWhoisProtocol(PROXY_CONFIG))
+ .isEqualTo(PROXY_CONFIG.webWhois.httpsPort);
assertThat(proxyModule.provideEnvironment()).isEqualTo(LOCAL);
assertThat(proxyModule.log).isFalse();
}
@@ -98,6 +102,20 @@ public class ProxyModuleTest {
assertThat(proxyModule.provideHealthCheckPort(PROXY_CONFIG)).isEqualTo(23456);
}
+ @Test
+ public void testSuccess_parseArgs_customhttpWhoisPort() {
+ String[] args = {"--http_whois", "12121"};
+ proxyModule.parse(args);
+ assertThat(proxyModule.provideHttpWhoisProtocol(PROXY_CONFIG)).isEqualTo(12121);
+ }
+
+ @Test
+ public void testSuccess_parseArgs_customhttpsWhoisPort() {
+ String[] args = {"--https_whois", "21212"};
+ proxyModule.parse(args);
+ assertThat(proxyModule.provideHttpsWhoisProtocol(PROXY_CONFIG)).isEqualTo(21212);
+ }
+
@Test
public void testSuccess_parseArgs_customEnvironment() {
String[] args = {"--env", "ALpHa"};
diff --git a/javatests/google/registry/proxy/TestUtils.java b/javatests/google/registry/proxy/TestUtils.java
index de4ed05fe..31d44d0ad 100644
--- a/javatests/google/registry/proxy/TestUtils.java
+++ b/javatests/google/registry/proxy/TestUtils.java
@@ -48,14 +48,21 @@ public class TestUtils {
.headers()
.set(HttpHeaderNames.USER_AGENT, "Proxy")
.set(HttpHeaderNames.HOST, host)
- .set(HttpHeaderNames.CONTENT_LENGTH, buf.readableBytes());
+ .setInt(HttpHeaderNames.CONTENT_LENGTH, buf.readableBytes());
+ return request;
+ }
+
+ public static FullHttpRequest makeHttpGetRequest(String host, String path) {
+ FullHttpRequest request =
+ new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.GET, path);
+ request.headers().set(HttpHeaderNames.HOST, host).setInt(HttpHeaderNames.CONTENT_LENGTH, 0);
return request;
}
public static FullHttpResponse makeHttpResponse(String content, HttpResponseStatus status) {
ByteBuf buf = Unpooled.wrappedBuffer(content.getBytes(US_ASCII));
FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, status, buf);
- response.headers().set(HttpHeaderNames.CONTENT_LENGTH, buf.readableBytes());
+ response.headers().setInt(HttpHeaderNames.CONTENT_LENGTH, buf.readableBytes());
return response;
}
diff --git a/javatests/google/registry/proxy/handler/SslServerInitializerTest.java b/javatests/google/registry/proxy/handler/SslServerInitializerTest.java
index 718dbe3aa..d18ca44aa 100644
--- a/javatests/google/registry/proxy/handler/SslServerInitializerTest.java
+++ b/javatests/google/registry/proxy/handler/SslServerInitializerTest.java
@@ -87,6 +87,7 @@ public class SslServerInitializerTest {
.build();
private ChannelInitializer getServerInitializer(
+ boolean requireClientCert,
Lock serverLock,
Exception serverException,
PrivateKey privateKey,
@@ -97,12 +98,22 @@ public class SslServerInitializerTest {
protected void initChannel(LocalChannel ch) throws Exception {
ch.pipeline()
.addLast(
- new SslServerInitializer(SslProvider.JDK, privateKey, certificates),
+ new SslServerInitializer(
+ requireClientCert, SslProvider.JDK, privateKey, certificates),
new EchoHandler(serverLock, serverException));
}
};
}
+ private ChannelInitializer getServerInitializer(
+ Lock serverLock,
+ Exception serverException,
+ PrivateKey privateKey,
+ X509Certificate... certificates)
+ throws Exception {
+ return getServerInitializer(true, serverLock, serverException, privateKey, certificates);
+ }
+
private ChannelInitializer getClientInitializer(
X509Certificate trustedCertificate,
PrivateKey privateKey,
@@ -137,7 +148,7 @@ public class SslServerInitializerTest {
public void testSuccess_swappedInitializerWithSslHandler() throws Exception {
SelfSignedCertificate ssc = new SelfSignedCertificate(SSL_HOST);
SslServerInitializer sslServerInitializer =
- new SslServerInitializer<>(SslProvider.JDK, ssc.key(), ssc.cert());
+ new SslServerInitializer<>(true, SslProvider.JDK, ssc.key(), ssc.cert());
EmbeddedChannel channel = new EmbeddedChannel();
ChannelPipeline pipeline = channel.pipeline();
pipeline.addLast(sslServerInitializer);
@@ -187,6 +198,39 @@ public class SslServerInitializerTest {
Future> unusedFuture = eventLoopGroup.shutdownGracefully().syncUninterruptibly();
}
+ @Test
+ public void testSuccess_doesNotRequireClientCert() throws Exception {
+ SelfSignedCertificate serverSsc = new SelfSignedCertificate(SSL_HOST);
+ LocalAddress localAddress = new LocalAddress("DOES_NOT_REQUIRE_CLIENT_CERT");
+ Lock clientLock = new ReentrantLock();
+ Lock serverLock = new ReentrantLock();
+ ByteBuf buffer = Unpooled.buffer();
+ Exception clientException = new Exception();
+ Exception serverException = new Exception();
+ EventLoopGroup eventLoopGroup =
+ setUpServer(
+ getServerInitializer(
+ false, serverLock, serverException, serverSsc.key(), serverSsc.cert()),
+ localAddress);
+ Channel channel =
+ setUpClient(
+ eventLoopGroup,
+ getClientInitializer(serverSsc.cert(), null, null, clientLock, buffer, clientException),
+ localAddress,
+ PROTOCOL);
+
+ SSLSession sslSession =
+ verifySslChannel(
+ channel, ImmutableList.of(serverSsc.cert()), clientLock, serverLock, buffer, SSL_HOST);
+ // Verify that the SSL session does not contain any client cert. Note that this SslSession is
+ // for the client channel, therefore its local certificates are the remote certificates of the
+ // SslSession for the server channel, and vice versa.
+ assertThat(sslSession.getLocalCertificates()).isNull();
+ assertThat(sslSession.getPeerCertificates()).asList().containsExactly(serverSsc.cert());
+
+ Future> unusedFuture = eventLoopGroup.shutdownGracefully().syncUninterruptibly();
+ }
+
@Test
public void testSuccess_CertSignedByOtherCA() throws Exception {
// The self-signed cert of the CA.
diff --git a/javatests/google/registry/proxy/handler/WebWhoisRedirectHandlerTest.java b/javatests/google/registry/proxy/handler/WebWhoisRedirectHandlerTest.java
new file mode 100644
index 000000000..f7ad2efe7
--- /dev/null
+++ b/javatests/google/registry/proxy/handler/WebWhoisRedirectHandlerTest.java
@@ -0,0 +1,185 @@
+// Copyright 2018 The Nomulus Authors. All Rights Reserved.
+//
+// Licensed 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.
+
+package google.registry.proxy.handler;
+
+import static com.google.common.truth.Truth.assertThat;
+import static google.registry.proxy.TestUtils.assertHttpResponseEquivalent;
+import static google.registry.proxy.TestUtils.makeHttpGetRequest;
+import static google.registry.proxy.TestUtils.makeHttpPostRequest;
+import static google.registry.proxy.TestUtils.makeHttpResponse;
+
+import io.netty.channel.embedded.EmbeddedChannel;
+import io.netty.handler.codec.http.FullHttpRequest;
+import io.netty.handler.codec.http.FullHttpResponse;
+import io.netty.handler.codec.http.HttpResponseStatus;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Unit tests for {@link WebWhoisRedirectHandler}. */
+@RunWith(JUnit4.class)
+public class WebWhoisRedirectHandlerTest {
+
+ private static final String REDIRECT_HOST = "www.example.com";
+ private static final String TARGET_HOST = "whois.nic.tld";
+
+ private WebWhoisRedirectHandler redirectHandler;
+ private EmbeddedChannel channel;
+ private FullHttpRequest request;
+ private FullHttpResponse response;
+
+ private static FullHttpResponse makeRedirectResponse(
+ HttpResponseStatus status, String location, boolean keepAlive, boolean isHttps) {
+ FullHttpResponse response = makeHttpResponse("", status);
+ response.headers().set("content-type", "text/plain").set("content-length", "0");
+ if (location != null) {
+ response.headers().set("location", location);
+ }
+ if (keepAlive) {
+ response.headers().set("connection", "keep-alive");
+ }
+ if (isHttps) {
+ response.headers().set("Strict-Transport-Security", "max-age=31536000");
+ }
+ return response;
+ }
+
+ @Test
+ public void testSuccess_http_redirectToHttps() {
+ redirectHandler = new WebWhoisRedirectHandler(false, REDIRECT_HOST);
+ channel = new EmbeddedChannel(redirectHandler);
+ request = makeHttpGetRequest(TARGET_HOST, "/");
+ // No inbound message passed to the next handler.
+ assertThat(channel.writeInbound(request)).isFalse();
+ response = channel.readOutbound();
+ assertHttpResponseEquivalent(
+ response,
+ makeRedirectResponse(
+ HttpResponseStatus.MOVED_PERMANENTLY, "https://whois.nic.tld/", true, false));
+ assertThat(channel.isActive()).isTrue();
+ }
+
+ @Test
+ public void testSuccess_http_redirectToHttps_hostAndPort() {
+ redirectHandler = new WebWhoisRedirectHandler(false, REDIRECT_HOST);
+ channel = new EmbeddedChannel(redirectHandler);
+ request = makeHttpGetRequest(TARGET_HOST + ":80", "/");
+ // No inbound message passed to the next handler.
+ assertThat(channel.writeInbound(request)).isFalse();
+ response = channel.readOutbound();
+ assertHttpResponseEquivalent(
+ response,
+ makeRedirectResponse(
+ HttpResponseStatus.MOVED_PERMANENTLY, "https://whois.nic.tld/", true, false));
+ assertThat(channel.isActive()).isTrue();
+ }
+
+ @Test
+ public void testSuccess_http_redirectToHttps_noKeepAlive() {
+ redirectHandler = new WebWhoisRedirectHandler(false, REDIRECT_HOST);
+ channel = new EmbeddedChannel(redirectHandler);
+ request = makeHttpGetRequest(TARGET_HOST, "/");
+ request.headers().set("connection", "close");
+ // No inbound message passed to the next handler.
+ assertThat(channel.writeInbound(request)).isFalse();
+ response = channel.readOutbound();
+ assertHttpResponseEquivalent(
+ response,
+ makeRedirectResponse(
+ HttpResponseStatus.MOVED_PERMANENTLY, "https://whois.nic.tld/", false, false));
+ assertThat(channel.isActive()).isFalse();
+ }
+
+ @Test
+ public void testSuccess_http_notGet() {
+ redirectHandler = new WebWhoisRedirectHandler(false, REDIRECT_HOST);
+ channel = new EmbeddedChannel(redirectHandler);
+ request = makeHttpPostRequest("", TARGET_HOST, "/");
+ // No inbound message passed to the next handler.
+ assertThat(channel.writeInbound(request)).isFalse();
+ response = channel.readOutbound();
+ assertHttpResponseEquivalent(
+ response, makeRedirectResponse(HttpResponseStatus.METHOD_NOT_ALLOWED, null, true, false));
+ assertThat(channel.isActive()).isTrue();
+ }
+
+ @Test
+ public void testSuccess_http_healthCheck() {
+ redirectHandler = new WebWhoisRedirectHandler(false, REDIRECT_HOST);
+ channel = new EmbeddedChannel(redirectHandler);
+ request = makeHttpPostRequest("", TARGET_HOST, "/");
+ // No inbound message passed to the next handler.
+ assertThat(channel.writeInbound(request)).isFalse();
+ response = channel.readOutbound();
+ assertHttpResponseEquivalent(
+ response, makeRedirectResponse(HttpResponseStatus.METHOD_NOT_ALLOWED, null, true, false));
+ assertThat(channel.isActive()).isTrue();
+ }
+
+ @Test
+ public void testSuccess_https_redirectToDestination() {
+ redirectHandler = new WebWhoisRedirectHandler(true, REDIRECT_HOST);
+ channel = new EmbeddedChannel(redirectHandler);
+ request = makeHttpGetRequest(TARGET_HOST, "/");
+ // No inbound message passed to the next handler.
+ assertThat(channel.writeInbound(request)).isFalse();
+ response = channel.readOutbound();
+ assertHttpResponseEquivalent(
+ response,
+ makeRedirectResponse(HttpResponseStatus.FOUND, "https://www.example.com/", true, true));
+ assertThat(channel.isActive()).isTrue();
+ }
+
+ @Test
+ public void testSuccess_https_redirectToDestination_noKeepAlive() {
+ redirectHandler = new WebWhoisRedirectHandler(true, REDIRECT_HOST);
+ channel = new EmbeddedChannel(redirectHandler);
+ request = makeHttpGetRequest(TARGET_HOST, "/");
+ request.headers().set("connection", "close");
+ // No inbound message passed to the next handler.
+ assertThat(channel.writeInbound(request)).isFalse();
+ response = channel.readOutbound();
+ assertHttpResponseEquivalent(
+ response,
+ makeRedirectResponse(HttpResponseStatus.FOUND, "https://www.example.com/", false, true));
+ assertThat(channel.isActive()).isFalse();
+ }
+
+ @Test
+ public void testSuccess_https_notGet() {
+ redirectHandler = new WebWhoisRedirectHandler(true, REDIRECT_HOST);
+ channel = new EmbeddedChannel(redirectHandler);
+ request = makeHttpPostRequest("", TARGET_HOST, "/");
+ // No inbound message passed to the next handler.
+ assertThat(channel.writeInbound(request)).isFalse();
+ response = channel.readOutbound();
+ assertHttpResponseEquivalent(
+ response, makeRedirectResponse(HttpResponseStatus.METHOD_NOT_ALLOWED, null, true, true));
+ assertThat(channel.isActive()).isTrue();
+ }
+
+ @Test
+ public void testSuccess_https_healthCheck() {
+ redirectHandler = new WebWhoisRedirectHandler(true, REDIRECT_HOST);
+ channel = new EmbeddedChannel(redirectHandler);
+ request = makeHttpGetRequest("health-check.invalid", "/");
+ // No inbound message passed to the next handler.
+ assertThat(channel.writeInbound(request)).isFalse();
+ response = channel.readOutbound();
+ assertHttpResponseEquivalent(
+ response, makeRedirectResponse(HttpResponseStatus.FORBIDDEN, null, true, true));
+ assertThat(channel.isActive()).isTrue();
+ }
+}