google-nomulus/java/google/registry/proxy/handler/HttpsRelayServiceHandler.java
jianglai 4a5b317016 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
2018-08-10 13:46:48 -04:00

184 lines
7.2 KiB
Java

// Copyright 2017 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.base.Preconditions.checkArgument;
import static java.nio.charset.StandardCharsets.UTF_8;
import com.google.common.flogger.FluentLogger;
import google.registry.proxy.metric.FrontendMetrics;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelPromise;
import io.netty.handler.codec.ByteToMessageCodec;
import io.netty.handler.codec.http.DefaultFullHttpRequest;
import io.netty.handler.codec.http.FullHttpRequest;
import io.netty.handler.codec.http.FullHttpResponse;
import io.netty.handler.codec.http.HttpHeaderNames;
import io.netty.handler.codec.http.HttpMethod;
import io.netty.handler.codec.http.HttpResponseStatus;
import io.netty.handler.codec.http.HttpVersion;
import io.netty.handler.codec.http.cookie.ClientCookieDecoder;
import io.netty.handler.codec.http.cookie.ClientCookieEncoder;
import io.netty.handler.codec.http.cookie.Cookie;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Supplier;
/**
* Handler that relays a single (framed) ByteBuf message to an HTTPS server.
*
* <p>This handler reads in a {@link ByteBuf}, converts it to an {@link FullHttpRequest}, and passes
* it to the {@code channelRead} method of the next inbound handler the channel pipeline, which is
* usually a {@link RelayHandler<FullHttpRequest>}. The relay handler writes the request to the
* relay channel, which is connected to an HTTPS endpoint. After the relay channel receives a {@link
* FullHttpResponse} back, its own relay handler writes the response back to this channel, which is
* the relay channel of the relay channel. This handler then handles write request by encoding the
* {@link FullHttpResponse} to a plain {@link ByteBuf}, and pass it down to the {@code write} method
* of the next outbound handler in the channel pipeline, which eventually writes the response bytes
* to the remote peer of this channel.
*
* <p>This handler is session aware and will store all the session cookies that the are contained in
* the HTTP response headers, which are added back to headers of subsequent HTTP requests.
*/
abstract class HttpsRelayServiceHandler extends ByteToMessageCodec<FullHttpResponse> {
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
private final Map<String, Cookie> cookieStore = new LinkedHashMap<>();
private final String relayHost;
private final String relayPath;
private final Supplier<String> accessTokenSupplier;
protected final FrontendMetrics metrics;
HttpsRelayServiceHandler(
String relayHost,
String relayPath,
Supplier<String> accessTokenSupplier,
FrontendMetrics metrics) {
this.relayHost = relayHost;
this.relayPath = relayPath;
this.accessTokenSupplier = accessTokenSupplier;
this.metrics = metrics;
}
/**
* Construct the {@link FullHttpRequest}.
*
* <p>This default method creates a bare-bone {@link FullHttpRequest} that may need to be
* modified, e. g. adding headers specific for each protocol.
*
* @param byteBuf inbound message.
*/
protected FullHttpRequest decodeFullHttpRequest(ByteBuf byteBuf) {
FullHttpRequest request =
new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.POST, relayPath);
request
.headers()
.set(HttpHeaderNames.USER_AGENT, "Proxy")
.set(HttpHeaderNames.HOST, relayHost)
.set(HttpHeaderNames.AUTHORIZATION, "Bearer " + accessTokenSupplier.get())
.setInt(HttpHeaderNames.CONTENT_LENGTH, byteBuf.readableBytes());
request.content().writeBytes(byteBuf);
return request;
}
/**
* Load session cookies in the cookie store and write them in to the HTTP request.
*
* <p>Multiple cookies are folded into one {@code Cookie} header per RFC 6265.
*
* @see <a href="https://tools.ietf.org/html/rfc6265#section-5.4">RFC 6265 5.4.The Cookie
* Header</a>
*/
private void loadCookies(FullHttpRequest request) {
if (!cookieStore.isEmpty()) {
request
.headers()
.set(HttpHeaderNames.COOKIE, ClientCookieEncoder.STRICT.encode(cookieStore.values()));
}
}
@Override
protected void decode(ChannelHandlerContext ctx, ByteBuf byteBuf, List<Object> out)
throws Exception {
FullHttpRequest request = decodeFullHttpRequest(byteBuf);
loadCookies(request);
out.add(request);
}
/**
* Construct the {@link ByteBuf}
*
* <p>This default method puts all the response payload into the {@link ByteBuf}.
*
* @param fullHttpResponse outbound http response.
*/
ByteBuf encodeFullHttpResponse(FullHttpResponse fullHttpResponse) {
return fullHttpResponse.content();
}
/**
* Save session cookies from the HTTP response header to the cookie store.
*
* <p>Multiple cookies are </b>not</b> folded in to one {@code Set-Cookie} header per RFC 6265.
*
* @see <a href="https://tools.ietf.org/html/rfc6265#section-3">RFC 6265 3.Overview</a>
*/
private void saveCookies(FullHttpResponse response) {
for (String cookieString : response.headers().getAll(HttpHeaderNames.SET_COOKIE)) {
Cookie cookie = ClientCookieDecoder.STRICT.decode(cookieString);
cookieStore.put(cookie.name(), cookie);
}
}
@Override
protected void encode(ChannelHandlerContext ctx, FullHttpResponse response, ByteBuf byteBuf)
throws Exception {
checkArgument(
response.status().equals(HttpResponseStatus.OK),
"Cannot relay HTTP response status \"%s\"\n%s",
response.status(),
response.content().toString(UTF_8));
saveCookies(response);
byteBuf.writeBytes(encodeFullHttpResponse(response));
}
/** Terminates connection upon inbound exception. */
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
logger.atSevere().withCause(cause).log(
"Inbound exception caught for channel %s", ctx.channel());
ChannelFuture unusedFuture = ctx.close();
}
/** Terminates connection upon outbound exception. */
@Override
public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise)
throws Exception {
promise.addListener(
(ChannelFuture channelFuture) -> {
if (!channelFuture.isSuccess()) {
logger.atSevere().withCause(channelFuture.cause()).log(
"Outbound exception caught for channel %s", channelFuture.channel());
ChannelFuture unusedFuture = channelFuture.channel().close();
}
});
super.write(ctx, msg, promise);
}
}