Add web WHOIS redirect support

Opened two ports (30010 and 30011 by default) that handles HTTP(S) GET requests. the HTTP request is redirected to the corresponding HTTPS site, whereas the HTTPS request is redirected to a site that supports web WHOIS.

The GCLB currently exposes port 80, but not port 443 on its TCP proxy load balancer (see https://cloud.google.com/load-balancing/docs/choosing-load-balancer). As a result, the HTTP traffic has to be routed by the HTTP load balancer, which requires a separate HTTP health check (as opposed to the TCP health check that the TCP proxy LB uses). This CL also added support for HTTP health check.

There is not a strong case for adding an end-to-end test for WebWhoisProtocolsModule (like those for EppProtocolModule, etc) as it just assembles standard HTTP codecs used for an HTTP server, plus the WebWhoisRedirectHandler, which is tested. The end-to-end test would just be testing if the Netty provided HTTP handlers correctly parse raw HTTP messages.

Sever other small improvement is also included:

[1] Use setInt other than set when setting content length in HTTP headers. I don't think it is necessary, but it is nevertheless a better practice to use a more specialized setter.
[2] Do not write metrics when running locally.
[3] Rename the qualifier @EppCertificates to @ServerSertificate as it now provides the certificate used in HTTPS traffic as well.

-------------
Created by MOE: https://github.com/google/moe
MOE_MIGRATED_REVID=206944843
This commit is contained in:
jianglai 2018-08-01 09:57:25 -07:00
parent f614044681
commit 4a5b317016
18 changed files with 686 additions and 97 deletions

View file

@ -70,6 +70,8 @@ container_image(
"30000", "30000",
"30001", "30001",
"30002", "30002",
"30010",
"30011",
], ],
) )

View file

@ -44,7 +44,7 @@ import org.bouncycastle.openssl.PEMParser;
import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter; 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.
* *
* <p>The production certificates and private key are stored in a .pem file that is encrypted by * <p>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 * 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 @Module
public class CertificateModule { 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 @Qualifier
public @interface EppCertificates {} @interface ServerCertificates {}
/** Dagger qualifier to provide bindings when running locally. */ /** Dagger qualifier to provide bindings when running locally. */
@Qualifier @Qualifier
public @interface Local {} @interface Local {}
/** Dagger qualifier to provide bindings when running in production. */ /**
* Dagger qualifier to provide bindings when running in production.
*
* <p>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 @Qualifier
public @interface Prod {} @interface Prod {}
static { static {
Security.addProvider(new BouncyCastleProvider()); Security.addProvider(new BouncyCastleProvider());
@ -95,7 +100,7 @@ public class CertificateModule {
@Singleton @Singleton
@Provides @Provides
@EppCertificates @ServerCertificates
static X509Certificate[] provideCertificates( static X509Certificate[] provideCertificates(
Environment env, Environment env,
@Local Lazy<X509Certificate[]> localCertificates, @Local Lazy<X509Certificate[]> localCertificates,
@ -105,7 +110,7 @@ public class CertificateModule {
@Singleton @Singleton
@Provides @Provides
@EppCertificates @ServerCertificates
static PrivateKey providePrivateKey( static PrivateKey providePrivateKey(
Environment env, Environment env,
@Local Lazy<PrivateKey> localPrivateKey, @Local Lazy<PrivateKey> localPrivateKey,

View file

@ -20,6 +20,7 @@ import com.google.common.collect.ImmutableList;
import dagger.Module; import dagger.Module;
import dagger.Provides; import dagger.Provides;
import dagger.multibindings.IntoSet; import dagger.multibindings.IntoSet;
import google.registry.proxy.CertificateModule.ServerCertificates;
import google.registry.proxy.HttpsRelayProtocolModule.HttpsRelayProtocol; import google.registry.proxy.HttpsRelayProtocolModule.HttpsRelayProtocol;
import google.registry.proxy.Protocol.BackendProtocol; import google.registry.proxy.Protocol.BackendProtocol;
import google.registry.proxy.Protocol.FrontendProtocol; 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.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.LengthFieldBasedFrameDecoder; import io.netty.handler.codec.LengthFieldBasedFrameDecoder;
import io.netty.handler.codec.LengthFieldPrepender; import io.netty.handler.codec.LengthFieldPrepender;
import io.netty.handler.ssl.SslProvider;
import io.netty.handler.timeout.ReadTimeoutHandler; import io.netty.handler.timeout.ReadTimeoutHandler;
import java.io.IOException; import java.io.IOException;
import java.security.PrivateKey;
import java.security.cert.X509Certificate;
import java.util.concurrent.ExecutorService; import java.util.concurrent.ExecutorService;
import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledExecutorService;
import java.util.function.Supplier; import java.util.function.Supplier;
@ -76,8 +80,8 @@ public class EppProtocolModule {
@Provides @Provides
@EppProtocol @EppProtocol
static ImmutableList<Provider<? extends ChannelHandler>> provideHandlerProviders( static ImmutableList<Provider<? extends ChannelHandler>> provideHandlerProviders(
Provider<SslServerInitializer<NioSocketChannel>> sslServerInitializerProvider,
Provider<ProxyProtocolHandler> proxyProtocolHandlerProvider, Provider<ProxyProtocolHandler> proxyProtocolHandlerProvider,
@EppProtocol Provider<SslServerInitializer<NioSocketChannel>> sslServerInitializerProvider,
@EppProtocol Provider<ReadTimeoutHandler> readTimeoutHandlerProvider, @EppProtocol Provider<ReadTimeoutHandler> readTimeoutHandlerProvider,
Provider<LengthFieldBasedFrameDecoder> lengthFieldBasedFrameDecoderProvider, Provider<LengthFieldBasedFrameDecoder> lengthFieldBasedFrameDecoderProvider,
Provider<LengthFieldPrepender> lengthFieldPrependerProvider, Provider<LengthFieldPrepender> lengthFieldPrependerProvider,
@ -152,6 +156,16 @@ public class EppProtocolModule {
metrics); metrics);
} }
@Singleton
@Provides
@EppProtocol
static SslServerInitializer<NioSocketChannel> provideSslServerInitializer(
SslProvider sslProvider,
@ServerCertificates PrivateKey privateKey,
@ServerCertificates X509Certificate... certificates) {
return new SslServerInitializer<>(true, sslProvider, privateKey, certificates);
}
@Provides @Provides
@EppProtocol @EppProtocol
static TokenStore provideTokenStore( static TokenStore provideTokenStore(

View file

@ -50,7 +50,7 @@ public class HealthCheckProtocolModule {
return Protocol.frontendBuilder() return Protocol.frontendBuilder()
.name(PROTOCOL_NAME) .name(PROTOCOL_NAME)
.port(healthCheckPort) .port(healthCheckPort)
.isHealthCheck(true) .hasBackend(false)
.handlerProviders(handlerProviders) .handlerProviders(handlerProviders)
.build(); .build();
} }

View file

@ -42,13 +42,9 @@ public interface Protocol {
/** The {@link ChannelHandler} providers to use for the protocol, in order. */ /** The {@link ChannelHandler} providers to use for the protocol, in order. */
ImmutableList<Provider<? extends ChannelHandler>> handlerProviders(); ImmutableList<Provider<? extends ChannelHandler>> handlerProviders();
/** /** A builder for {@link FrontendProtocol}, by default there is a backend associated with it. */
* A builder for {@link FrontendProtocol}, default is non-health-checking.
*
* @see HealthCheckProtocolModule
*/
static FrontendProtocol.Builder frontendBuilder() { static FrontendProtocol.Builder frontendBuilder() {
return new AutoValue_Protocol_FrontendProtocol.Builder().isHealthCheck(false); return new AutoValue_Protocol_FrontendProtocol.Builder().hasBackend(true);
} }
static BackendProtocol.Builder backendBuilder() { 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 * 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 @Nullable
public abstract BackendProtocol relayProtocol(); 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 @AutoValue.Builder
public abstract static class Builder extends Protocol.Builder<Builder, FrontendProtocol> { public abstract static class Builder extends Protocol.Builder<Builder, FrontendProtocol> {
public abstract Builder relayProtocol(BackendProtocol value); public abstract Builder relayProtocol(BackendProtocol value);
public abstract Builder isHealthCheck(boolean value); public abstract Builder hasBackend(boolean value);
abstract FrontendProtocol autoBuild(); abstract FrontendProtocol autoBuild();
@ -102,7 +102,7 @@ public interface Protocol {
public FrontendProtocol build() { public FrontendProtocol build() {
FrontendProtocol frontendProtocol = autoBuild(); FrontendProtocol frontendProtocol = autoBuild();
Preconditions.checkState( Preconditions.checkState(
frontendProtocol.isHealthCheck() || frontendProtocol.relayProtocol() != null, !frontendProtocol.hasBackend() || frontendProtocol.relayProtocol() != null,
"Frontend protocol %s must define a relay protocol.", "Frontend protocol %s must define a relay protocol.",
frontendProtocol.name()); frontendProtocol.name());
return frontendProtocol; return frontendProtocol;

View file

@ -44,6 +44,7 @@ public class ProxyConfig {
public Epp epp; public Epp epp;
public Whois whois; public Whois whois;
public HealthCheck healthCheck; public HealthCheck healthCheck;
public WebWhois webWhois;
public HttpsRelay httpsRelay; public HttpsRelay httpsRelay;
public Metrics metrics; public Metrics metrics;
@ -89,6 +90,13 @@ public class ProxyConfig {
public String checkResponse; 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. */ /** Configuration options that apply to HTTPS relay protocol. */
public static class HttpsRelay { public static class HttpsRelay {
public int port; public int port;

View file

@ -38,6 +38,8 @@ import google.registry.proxy.EppProtocolModule.EppProtocol;
import google.registry.proxy.HealthCheckProtocolModule.HealthCheckProtocol; import google.registry.proxy.HealthCheckProtocolModule.HealthCheckProtocol;
import google.registry.proxy.Protocol.FrontendProtocol; import google.registry.proxy.Protocol.FrontendProtocol;
import google.registry.proxy.ProxyConfig.Environment; 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.WhoisProtocolModule.WhoisProtocol;
import google.registry.proxy.handler.ProxyProtocolHandler; import google.registry.proxy.handler.ProxyProtocolHandler;
import google.registry.util.Clock; import google.registry.util.Clock;
@ -75,9 +77,15 @@ public class ProxyModule {
@Parameter(names = "--epp", description = "Port for EPP") @Parameter(names = "--epp", description = "Port for EPP")
private Integer eppPort; 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; 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") @Parameter(names = "--env", description = "Environment to run the proxy in")
private Environment env = Environment.LOCAL; private Environment env = Environment.LOCAL;
@ -165,6 +173,18 @@ public class ProxyModule {
return Optional.ofNullable(healthCheckPort).orElse(config.healthCheck.port); 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 @Provides
ImmutableMap<Integer, FrontendProtocol> providePortToProtocolMap( ImmutableMap<Integer, FrontendProtocol> providePortToProtocolMap(
Set<FrontendProtocol> protocolSet) { Set<FrontendProtocol> protocolSet) {
@ -316,16 +336,16 @@ public class ProxyModule {
/** Root level component that exposes the port-to-protocol map. */ /** Root level component that exposes the port-to-protocol map. */
@Singleton @Singleton
@Component( @Component(
modules = { modules = {
ProxyModule.class, ProxyModule.class,
CertificateModule.class, CertificateModule.class,
HttpsRelayProtocolModule.class, HttpsRelayProtocolModule.class,
WhoisProtocolModule.class, WhoisProtocolModule.class,
EppProtocolModule.class, WebWhoisProtocolsModule.class,
HealthCheckProtocolModule.class, EppProtocolModule.class,
MetricsModule.class HealthCheckProtocolModule.class,
} MetricsModule.class
) })
interface ProxyComponent { interface ProxyComponent {
ImmutableMap<Integer, FrontendProtocol> portToProtocolMap(); ImmutableMap<Integer, FrontendProtocol> portToProtocolMap();

View file

@ -23,6 +23,7 @@ import com.google.common.flogger.FluentLogger;
import com.google.monitoring.metrics.MetricReporter; import com.google.monitoring.metrics.MetricReporter;
import google.registry.proxy.Protocol.BackendProtocol; import google.registry.proxy.Protocol.BackendProtocol;
import google.registry.proxy.Protocol.FrontendProtocol; import google.registry.proxy.Protocol.FrontendProtocol;
import google.registry.proxy.ProxyConfig.Environment;
import google.registry.proxy.ProxyModule.ProxyComponent; import google.registry.proxy.ProxyModule.ProxyComponent;
import io.netty.bootstrap.Bootstrap; import io.netty.bootstrap.Bootstrap;
import io.netty.bootstrap.ServerBootstrap; import io.netty.bootstrap.ServerBootstrap;
@ -87,9 +88,9 @@ public class ProxyServer implements Runnable {
inboundChannel.attr(PROTOCOL_KEY).set(inboundProtocol); inboundChannel.attr(PROTOCOL_KEY).set(inboundProtocol);
addHandlers(inboundChannel.pipeline(), inboundProtocol.handlerProviders()); addHandlers(inboundChannel.pipeline(), inboundProtocol.handlerProviders());
if (inboundProtocol.isHealthCheck()) { if (!inboundProtocol.hasBackend()) {
// A health check server protocol has no relay channel. It simply replies to incoming // If the frontend has no backend to relay to (health check, web WHOIS redirect, etc), start
// request with a preset response. // reading immediately.
inboundChannel.config().setAutoRead(true); inboundChannel.config().setAutoRead(true);
} else { } else {
// Connect to the relay (outbound) channel specified by the BackendProtocol. // 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 // Configure the components, this needs to run first so that the logging format is properly
// configured for each environment. // configured for each environment.
ProxyModule proxyModule = new ProxyModule().parse(args);
ProxyComponent proxyComponent = ProxyComponent proxyComponent =
DaggerProxyModule_ProxyComponent.builder() DaggerProxyModule_ProxyComponent.builder().proxyModule(proxyModule).build();
.proxyModule(new ProxyModule().parse(args))
.build();
MetricReporter metricReporter = proxyComponent.metricReporter(); // Do not write metrics when running locally.
try { if (proxyModule.provideEnvironment() != Environment.LOCAL) {
metricReporter.startAsync().awaitRunning(10, TimeUnit.SECONDS); MetricReporter metricReporter = proxyComponent.metricReporter();
logger.atInfo().log("Started up MetricReporter"); try {
} catch (TimeoutException timeoutException) { metricReporter.startAsync().awaitRunning(10, TimeUnit.SECONDS);
logger.atSevere().withCause(timeoutException).log( logger.atInfo().log("Started up MetricReporter");
"Failed to initialize MetricReporter: %s", timeoutException); } 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() // Start the proxy.
.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);
}
}));
new ProxyServer(proxyComponent).run(); new ProxyServer(proxyComponent).run();
} }
} }

View file

@ -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<Provider<? extends ChannelHandler>> 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<Provider<? extends ChannelHandler>> handlerProviders) {
return google.registry.proxy.Protocol.frontendBuilder()
.name(HTTPS_PROTOCOL_NAME)
.port(httpsWhoisPort)
.hasBackend(false)
.handlerProviders(handlerProviders)
.build();
}
@Provides
@HttpWhoisProtocol
static ImmutableList<Provider<? extends ChannelHandler>> providerHttpWhoisHandlerProviders(
Provider<HttpServerCodec> httpServerCodecProvider,
Provider<HttpServerExpectContinueHandler> httpServerExpectContinueHandlerProvider,
@HttpWhoisProtocol Provider<WebWhoisRedirectHandler> webWhoisRedirectHandlerProvides) {
return ImmutableList.of(
httpServerCodecProvider,
httpServerExpectContinueHandlerProvider,
webWhoisRedirectHandlerProvides);
};
@Provides
@HttpsWhoisProtocol
static ImmutableList<Provider<? extends ChannelHandler>> providerHttpsWhoisHandlerProviders(
@HttpsWhoisProtocol
Provider<SslServerInitializer<NioSocketChannel>> sslServerInitializerProvider,
Provider<HttpServerCodec> httpServerCodecProvider,
Provider<HttpServerExpectContinueHandler> httpServerExpectContinueHandlerProvider,
@HttpsWhoisProtocol Provider<WebWhoisRedirectHandler> 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<NioSocketChannel> provideSslServerInitializer(
SslProvider sslProvider,
@ServerCertificates PrivateKey privateKey,
@ServerCertificates X509Certificate... certificates) {
return new SslServerInitializer<>(false, sslProvider, privateKey, certificates);
}
}

View file

@ -187,6 +187,14 @@ httpsRelay:
# Maximum size of an HTTP message in bytes. # Maximum size of an HTTP message in bytes.
maxMessageLengthBytes: 524288 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: metrics:
# Max queries per second for the Google Cloud Monitoring V3 (aka Stackdriver) # Max queries per second for the Google Cloud Monitoring V3 (aka Stackdriver)
# API. The limit can be adjusted by contacting Cloud Support. # API. The limit can be adjusted by contacting Cloud Support.

View file

@ -93,7 +93,7 @@ abstract class HttpsRelayServiceHandler extends ByteToMessageCodec<FullHttpRespo
.set(HttpHeaderNames.USER_AGENT, "Proxy") .set(HttpHeaderNames.USER_AGENT, "Proxy")
.set(HttpHeaderNames.HOST, relayHost) .set(HttpHeaderNames.HOST, relayHost)
.set(HttpHeaderNames.AUTHORIZATION, "Bearer " + accessTokenSupplier.get()) .set(HttpHeaderNames.AUTHORIZATION, "Bearer " + accessTokenSupplier.get())
.set(HttpHeaderNames.CONTENT_LENGTH, byteBuf.readableBytes()); .setInt(HttpHeaderNames.CONTENT_LENGTH, byteBuf.readableBytes());
request.content().writeBytes(byteBuf); request.content().writeBytes(byteBuf);
return request; return request;
} }

View file

@ -15,7 +15,6 @@
package google.registry.proxy.handler; package google.registry.proxy.handler;
import com.google.common.flogger.FluentLogger; import com.google.common.flogger.FluentLogger;
import google.registry.proxy.CertificateModule.EppCertificates;
import io.netty.channel.Channel; import io.netty.channel.Channel;
import io.netty.channel.ChannelHandler.Sharable; import io.netty.channel.ChannelHandler.Sharable;
import io.netty.channel.ChannelInitializer; import io.netty.channel.ChannelInitializer;
@ -30,8 +29,6 @@ import io.netty.util.concurrent.Future;
import io.netty.util.concurrent.Promise; import io.netty.util.concurrent.Promise;
import java.security.PrivateKey; import java.security.PrivateKey;
import java.security.cert.X509Certificate; import java.security.cert.X509Certificate;
import javax.inject.Inject;
import javax.inject.Singleton;
/** /**
* Adds a server side SSL handler to the channel pipeline. * Adds a server side SSL handler to the channel pipeline.
@ -47,7 +44,6 @@ import javax.inject.Singleton;
* certificate hash will be passed along to GAE as an HTTP header for verification (not handled by * certificate hash will be passed along to GAE as an HTTP header for verification (not handled by
* this handler). * this handler).
*/ */
@Singleton
@Sharable @Sharable
public class SslServerInitializer<C extends Channel> extends ChannelInitializer<C> { public class SslServerInitializer<C extends Channel> extends ChannelInitializer<C> {
@ -59,16 +55,18 @@ public class SslServerInitializer<C extends Channel> extends ChannelInitializer<
AttributeKey.valueOf("CLIENT_CERTIFICATE_PROMISE_KEY"); AttributeKey.valueOf("CLIENT_CERTIFICATE_PROMISE_KEY");
private static final FluentLogger logger = FluentLogger.forEnclosingClass(); private static final FluentLogger logger = FluentLogger.forEnclosingClass();
private final boolean requireClientCert;
private final SslProvider sslProvider; private final SslProvider sslProvider;
private final PrivateKey privateKey; private final PrivateKey privateKey;
private final X509Certificate[] certificates; private final X509Certificate[] certificates;
@Inject public SslServerInitializer(
SslServerInitializer( boolean requireClientCert,
SslProvider sslProvider, SslProvider sslProvider,
@EppCertificates PrivateKey privateKey, PrivateKey privateKey,
@EppCertificates X509Certificate... certificates) { X509Certificate... certificates) {
logger.atInfo().log("Server SSL Provider: %s", sslProvider); logger.atInfo().log("Server SSL Provider: %s", sslProvider);
this.requireClientCert = requireClientCert;
this.sslProvider = sslProvider; this.sslProvider = sslProvider;
this.privateKey = privateKey; this.privateKey = privateKey;
this.certificates = certificates; this.certificates = certificates;
@ -80,26 +78,28 @@ public class SslServerInitializer<C extends Channel> extends ChannelInitializer<
SslContextBuilder.forServer(privateKey, certificates) SslContextBuilder.forServer(privateKey, certificates)
.sslProvider(sslProvider) .sslProvider(sslProvider)
.trustManager(InsecureTrustManagerFactory.INSTANCE) .trustManager(InsecureTrustManagerFactory.INSTANCE)
.clientAuth(ClientAuth.REQUIRE) .clientAuth(requireClientCert ? ClientAuth.REQUIRE : ClientAuth.NONE)
.build() .build()
.newHandler(channel.alloc()); .newHandler(channel.alloc());
Promise<X509Certificate> clientCertificatePromise = channel.eventLoop().newPromise(); if (requireClientCert) {
Future<Channel> unusedFuture = Promise<X509Certificate> clientCertificatePromise = channel.eventLoop().newPromise();
sslHandler Future<Channel> unusedFuture =
.handshakeFuture() sslHandler
.addListener( .handshakeFuture()
future -> { .addListener(
if (future.isSuccess()) { future -> {
Promise<X509Certificate> unusedPromise = if (future.isSuccess()) {
clientCertificatePromise.setSuccess( Promise<X509Certificate> unusedPromise =
(X509Certificate) clientCertificatePromise.setSuccess(
sslHandler.engine().getSession().getPeerCertificates()[0]); (X509Certificate)
} else { sslHandler.engine().getSession().getPeerCertificates()[0]);
Promise<X509Certificate> unusedPromise = } else {
clientCertificatePromise.setFailure(future.cause()); Promise<X509Certificate> unusedPromise =
} clientCertificatePromise.setFailure(future.cause());
}); }
channel.attr(CLIENT_CERTIFICATE_PROMISE_KEY).set(clientCertificatePromise); });
channel.attr(CLIENT_CERTIFICATE_PROMISE_KEY).set(clientCertificatePromise);
}
channel.pipeline().addLast(sslHandler); channel.pipeline().addLast(sslHandler);
} }
} }

View file

@ -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.
*
* <p>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.
*
* <p>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 <a
* href="https://newgtlds.icann.org/sites/default/files/agreements/agreement-approved-31jul17-en.html">
* REGISTRY AGREEMENT</a>
*/
public class WebWhoisRedirectHandler extends SimpleChannelInboundHandler<HttpRequest> {
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
/**
* HTTP health check sent by GCP HTTP load balancer is set to use this host name.
*
* <p>Status 200 must be returned in order for a health check to be considered successful.
*
* @see <a
* href="https://cloud.google.com/load-balancing/docs/health-check-concepts#http_https_and_http2_health_checks">
* HTTP, HTTPS, and HTTP/2 health checks</a>
*/
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();
}
}

View file

@ -175,15 +175,14 @@ public abstract class ProtocolModuleTest {
*/ */
@Singleton @Singleton
@Component( @Component(
modules = { modules = {
TestModule.class, TestModule.class,
CertificateModule.class, CertificateModule.class,
WhoisProtocolModule.class, WhoisProtocolModule.class,
EppProtocolModule.class, EppProtocolModule.class,
HealthCheckProtocolModule.class, HealthCheckProtocolModule.class,
HttpsRelayProtocolModule.class HttpsRelayProtocolModule.class
} })
)
interface TestComponent { interface TestComponent {
@WhoisProtocol @WhoisProtocol
ImmutableList<Provider<? extends ChannelHandler>> whoisHandlers(); ImmutableList<Provider<? extends ChannelHandler>> whoisHandlers();

View file

@ -41,6 +41,10 @@ public class ProxyModuleTest {
assertThat(proxyModule.provideEppPort(PROXY_CONFIG)).isEqualTo(PROXY_CONFIG.epp.port); assertThat(proxyModule.provideEppPort(PROXY_CONFIG)).isEqualTo(PROXY_CONFIG.epp.port);
assertThat(proxyModule.provideHealthCheckPort(PROXY_CONFIG)) assertThat(proxyModule.provideHealthCheckPort(PROXY_CONFIG))
.isEqualTo(PROXY_CONFIG.healthCheck.port); .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.provideEnvironment()).isEqualTo(LOCAL);
assertThat(proxyModule.log).isFalse(); assertThat(proxyModule.log).isFalse();
} }
@ -98,6 +102,20 @@ public class ProxyModuleTest {
assertThat(proxyModule.provideHealthCheckPort(PROXY_CONFIG)).isEqualTo(23456); 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 @Test
public void testSuccess_parseArgs_customEnvironment() { public void testSuccess_parseArgs_customEnvironment() {
String[] args = {"--env", "ALpHa"}; String[] args = {"--env", "ALpHa"};

View file

@ -48,14 +48,21 @@ public class TestUtils {
.headers() .headers()
.set(HttpHeaderNames.USER_AGENT, "Proxy") .set(HttpHeaderNames.USER_AGENT, "Proxy")
.set(HttpHeaderNames.HOST, host) .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; return request;
} }
public static FullHttpResponse makeHttpResponse(String content, HttpResponseStatus status) { public static FullHttpResponse makeHttpResponse(String content, HttpResponseStatus status) {
ByteBuf buf = Unpooled.wrappedBuffer(content.getBytes(US_ASCII)); ByteBuf buf = Unpooled.wrappedBuffer(content.getBytes(US_ASCII));
FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, status, buf); 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; return response;
} }

View file

@ -87,6 +87,7 @@ public class SslServerInitializerTest {
.build(); .build();
private ChannelInitializer<LocalChannel> getServerInitializer( private ChannelInitializer<LocalChannel> getServerInitializer(
boolean requireClientCert,
Lock serverLock, Lock serverLock,
Exception serverException, Exception serverException,
PrivateKey privateKey, PrivateKey privateKey,
@ -97,12 +98,22 @@ public class SslServerInitializerTest {
protected void initChannel(LocalChannel ch) throws Exception { protected void initChannel(LocalChannel ch) throws Exception {
ch.pipeline() ch.pipeline()
.addLast( .addLast(
new SslServerInitializer<LocalChannel>(SslProvider.JDK, privateKey, certificates), new SslServerInitializer<LocalChannel>(
requireClientCert, SslProvider.JDK, privateKey, certificates),
new EchoHandler(serverLock, serverException)); new EchoHandler(serverLock, serverException));
} }
}; };
} }
private ChannelInitializer<LocalChannel> getServerInitializer(
Lock serverLock,
Exception serverException,
PrivateKey privateKey,
X509Certificate... certificates)
throws Exception {
return getServerInitializer(true, serverLock, serverException, privateKey, certificates);
}
private ChannelInitializer<LocalChannel> getClientInitializer( private ChannelInitializer<LocalChannel> getClientInitializer(
X509Certificate trustedCertificate, X509Certificate trustedCertificate,
PrivateKey privateKey, PrivateKey privateKey,
@ -137,7 +148,7 @@ public class SslServerInitializerTest {
public void testSuccess_swappedInitializerWithSslHandler() throws Exception { public void testSuccess_swappedInitializerWithSslHandler() throws Exception {
SelfSignedCertificate ssc = new SelfSignedCertificate(SSL_HOST); SelfSignedCertificate ssc = new SelfSignedCertificate(SSL_HOST);
SslServerInitializer<EmbeddedChannel> sslServerInitializer = SslServerInitializer<EmbeddedChannel> sslServerInitializer =
new SslServerInitializer<>(SslProvider.JDK, ssc.key(), ssc.cert()); new SslServerInitializer<>(true, SslProvider.JDK, ssc.key(), ssc.cert());
EmbeddedChannel channel = new EmbeddedChannel(); EmbeddedChannel channel = new EmbeddedChannel();
ChannelPipeline pipeline = channel.pipeline(); ChannelPipeline pipeline = channel.pipeline();
pipeline.addLast(sslServerInitializer); pipeline.addLast(sslServerInitializer);
@ -187,6 +198,39 @@ public class SslServerInitializerTest {
Future<?> unusedFuture = eventLoopGroup.shutdownGracefully().syncUninterruptibly(); 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 @Test
public void testSuccess_CertSignedByOtherCA() throws Exception { public void testSuccess_CertSignedByOtherCA() throws Exception {
// The self-signed cert of the CA. // The self-signed cert of the CA.

View file

@ -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();
}
}