mirror of
https://github.com/google/nomulus.git
synced 2025-08-04 08:52:12 +02:00
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:
parent
f614044681
commit
4a5b317016
18 changed files with 686 additions and 97 deletions
|
@ -93,7 +93,7 @@ abstract class HttpsRelayServiceHandler extends ByteToMessageCodec<FullHttpRespo
|
|||
.set(HttpHeaderNames.USER_AGENT, "Proxy")
|
||||
.set(HttpHeaderNames.HOST, relayHost)
|
||||
.set(HttpHeaderNames.AUTHORIZATION, "Bearer " + accessTokenSupplier.get())
|
||||
.set(HttpHeaderNames.CONTENT_LENGTH, byteBuf.readableBytes());
|
||||
.setInt(HttpHeaderNames.CONTENT_LENGTH, byteBuf.readableBytes());
|
||||
request.content().writeBytes(byteBuf);
|
||||
return request;
|
||||
}
|
||||
|
|
|
@ -15,7 +15,6 @@
|
|||
package google.registry.proxy.handler;
|
||||
|
||||
import com.google.common.flogger.FluentLogger;
|
||||
import google.registry.proxy.CertificateModule.EppCertificates;
|
||||
import io.netty.channel.Channel;
|
||||
import io.netty.channel.ChannelHandler.Sharable;
|
||||
import io.netty.channel.ChannelInitializer;
|
||||
|
@ -30,8 +29,6 @@ import io.netty.util.concurrent.Future;
|
|||
import io.netty.util.concurrent.Promise;
|
||||
import java.security.PrivateKey;
|
||||
import java.security.cert.X509Certificate;
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Singleton;
|
||||
|
||||
/**
|
||||
* 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
|
||||
* this handler).
|
||||
*/
|
||||
@Singleton
|
||||
@Sharable
|
||||
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");
|
||||
|
||||
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<C extends Channel> 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<X509Certificate> clientCertificatePromise = channel.eventLoop().newPromise();
|
||||
Future<Channel> unusedFuture =
|
||||
sslHandler
|
||||
.handshakeFuture()
|
||||
.addListener(
|
||||
future -> {
|
||||
if (future.isSuccess()) {
|
||||
Promise<X509Certificate> unusedPromise =
|
||||
clientCertificatePromise.setSuccess(
|
||||
(X509Certificate)
|
||||
sslHandler.engine().getSession().getPeerCertificates()[0]);
|
||||
} else {
|
||||
Promise<X509Certificate> unusedPromise =
|
||||
clientCertificatePromise.setFailure(future.cause());
|
||||
}
|
||||
});
|
||||
channel.attr(CLIENT_CERTIFICATE_PROMISE_KEY).set(clientCertificatePromise);
|
||||
if (requireClientCert) {
|
||||
Promise<X509Certificate> clientCertificatePromise = channel.eventLoop().newPromise();
|
||||
Future<Channel> unusedFuture =
|
||||
sslHandler
|
||||
.handshakeFuture()
|
||||
.addListener(
|
||||
future -> {
|
||||
if (future.isSuccess()) {
|
||||
Promise<X509Certificate> unusedPromise =
|
||||
clientCertificatePromise.setSuccess(
|
||||
(X509Certificate)
|
||||
sslHandler.engine().getSession().getPeerCertificates()[0]);
|
||||
} else {
|
||||
Promise<X509Certificate> unusedPromise =
|
||||
clientCertificatePromise.setFailure(future.cause());
|
||||
}
|
||||
});
|
||||
channel.attr(CLIENT_CERTIFICATE_PROMISE_KEY).set(clientCertificatePromise);
|
||||
}
|
||||
channel.pipeline().addLast(sslHandler);
|
||||
}
|
||||
}
|
||||
|
|
137
java/google/registry/proxy/handler/WebWhoisRedirectHandler.java
Normal file
137
java/google/registry/proxy/handler/WebWhoisRedirectHandler.java
Normal 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();
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue