// 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.truth.Truth.assertThat;
import static google.registry.proxy.TestUtils.assertHttpRequestEquivalent;
import static google.registry.proxy.TestUtils.makeEppHttpResponse;
import static google.registry.proxy.handler.ProxyProtocolHandler.REMOTE_ADDRESS_KEY;
import static google.registry.proxy.handler.SslServerInitializer.CLIENT_CERTIFICATE_PROMISE_KEY;
import static google.registry.util.X509Utils.getCertificateHash;
import static java.nio.charset.StandardCharsets.UTF_8;
import static org.junit.Assert.fail;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import google.registry.proxy.TestUtils;
import google.registry.proxy.metric.FrontendMetrics;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.DefaultChannelId;
import io.netty.channel.embedded.EmbeddedChannel;
import io.netty.handler.codec.EncoderException;
import io.netty.handler.codec.http.FullHttpRequest;
import io.netty.handler.codec.http.HttpResponse;
import io.netty.handler.codec.http.HttpResponseStatus;
import io.netty.handler.codec.http.cookie.Cookie;
import io.netty.handler.codec.http.cookie.DefaultCookie;
import io.netty.handler.ssl.util.SelfSignedCertificate;
import io.netty.util.concurrent.Promise;
import java.security.cert.X509Certificate;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
/** Unit tests for {@link EppServiceHandler}. */
@RunWith(JUnit4.class)
public class EppServiceHandlerTest {
private static final String HELLO =
"\n"
+ "\n"
+ " \n"
+ "\n";
private static final String RELAY_HOST = "registry.example.tld";
private static final String RELAY_PATH = "/epp";
private static final String ACCESS_TOKEN = "this.access.token";
private static final String SERVER_HOSTNAME = "epp.example.tld";
private static final String CLIENT_ADDRESS = "epp.client.tld";
private static final String PROTOCOL = "epp";
private X509Certificate clientCertificate;
private final FrontendMetrics metrics = mock(FrontendMetrics.class);
private final EppServiceHandler eppServiceHandler =
new EppServiceHandler(
RELAY_HOST,
RELAY_PATH,
() -> ACCESS_TOKEN,
SERVER_HOSTNAME,
HELLO.getBytes(UTF_8),
metrics);
private EmbeddedChannel channel;
private void setHandshakeSuccess(EmbeddedChannel channel, X509Certificate certificate)
throws Exception {
Promise unusedPromise =
channel.attr(CLIENT_CERTIFICATE_PROMISE_KEY).get().setSuccess(certificate);
}
private void setHandshakeSuccess() throws Exception {
setHandshakeSuccess(channel, clientCertificate);
}
private void setHandshakeFailure(EmbeddedChannel channel) throws Exception {
Promise unusedPromise =
channel
.attr(CLIENT_CERTIFICATE_PROMISE_KEY)
.get()
.setFailure(new Exception("Handshake Failure"));
}
private void setHandshakeFailure() throws Exception {
setHandshakeFailure(channel);
}
private FullHttpRequest makeEppHttpRequest(String content, Cookie... cookies) {
return TestUtils.makeEppHttpRequest(
content,
RELAY_HOST,
RELAY_PATH,
ACCESS_TOKEN,
getCertificateHash(clientCertificate),
SERVER_HOSTNAME,
CLIENT_ADDRESS,
cookies);
}
@Before
public void setUp() throws Exception {
clientCertificate = new SelfSignedCertificate().cert();
channel = setUpNewChannel(eppServiceHandler);
}
private EmbeddedChannel setUpNewChannel(EppServiceHandler handler) throws Exception {
return new EmbeddedChannel(
DefaultChannelId.newInstance(),
new ChannelInitializer() {
@Override
protected void initChannel(EmbeddedChannel ch) throws Exception {
ch.attr(REMOTE_ADDRESS_KEY).set(CLIENT_ADDRESS);
ch.attr(CLIENT_CERTIFICATE_PROMISE_KEY).set(ch.eventLoop().newPromise());
ch.pipeline().addLast(handler);
}
});
}
@Test
public void testSuccess_connectionMetrics_oneConnection() throws Exception {
setHandshakeSuccess();
String certHash = getCertificateHash(clientCertificate);
assertThat(channel.isActive()).isTrue();
verify(metrics).registerActiveConnection(PROTOCOL, certHash, channel);
verifyNoMoreInteractions(metrics);
}
@Test
public void testSuccess_connectionMetrics_twoConnections_sameClient() throws Exception {
setHandshakeSuccess();
String certHash = getCertificateHash(clientCertificate);
assertThat(channel.isActive()).isTrue();
// Setup the second channel.
EppServiceHandler eppServiceHandler2 =
new EppServiceHandler(
RELAY_HOST,
RELAY_PATH,
() -> ACCESS_TOKEN,
SERVER_HOSTNAME,
HELLO.getBytes(UTF_8),
metrics);
EmbeddedChannel channel2 = setUpNewChannel(eppServiceHandler2);
setHandshakeSuccess(channel2, clientCertificate);
assertThat(channel2.isActive()).isTrue();
verify(metrics).registerActiveConnection(PROTOCOL, certHash, channel);
verify(metrics).registerActiveConnection(PROTOCOL, certHash, channel2);
verifyNoMoreInteractions(metrics);
}
@Test
public void testSuccess_connectionMetrics_twoConnections_differentClients() throws Exception {
setHandshakeSuccess();
String certHash = getCertificateHash(clientCertificate);
assertThat(channel.isActive()).isTrue();
// Setup the second channel.
EppServiceHandler eppServiceHandler2 =
new EppServiceHandler(
RELAY_HOST,
RELAY_PATH,
() -> ACCESS_TOKEN,
SERVER_HOSTNAME,
HELLO.getBytes(UTF_8),
metrics);
EmbeddedChannel channel2 = setUpNewChannel(eppServiceHandler2);
X509Certificate clientCertificate2 = new SelfSignedCertificate().cert();
setHandshakeSuccess(channel2, clientCertificate2);
String certHash2 = getCertificateHash(clientCertificate2);
assertThat(channel2.isActive()).isTrue();
verify(metrics).registerActiveConnection(PROTOCOL, certHash, channel);
verify(metrics).registerActiveConnection(PROTOCOL, certHash2, channel2);
verifyNoMoreInteractions(metrics);
}
@Test
public void testSuccess_sendHelloUponHandshakeSuccess() throws Exception {
// Nothing to pass to the next handler.
assertThat((Object) channel.readInbound()).isNull();
setHandshakeSuccess();
// hello bytes should be passed to the next handler.
FullHttpRequest helloRequest = channel.readInbound();
assertThat(helloRequest).isEqualTo(makeEppHttpRequest(HELLO));
// Nothing further to pass to the next handler.
assertThat((Object) channel.readInbound()).isNull();
assertThat(channel.isActive()).isTrue();
}
@Test
public void testSuccess_disconnectUponHandshakeFailure() throws Exception {
// Nothing to pass to the next handler.
assertThat((Object) channel.readInbound()).isNull();
setHandshakeFailure();
assertThat(channel.isActive()).isFalse();
}
@Test
public void testSuccess_sendRequestToNextHandler() throws Exception {
setHandshakeSuccess();
// First inbound message is hello.
channel.readInbound();
String content = "stuff";
channel.writeInbound(Unpooled.wrappedBuffer(content.getBytes(UTF_8)));
FullHttpRequest request = channel.readInbound();
assertThat(request).isEqualTo(makeEppHttpRequest(content));
// Nothing further to pass to the next handler.
assertThat((Object) channel.readInbound()).isNull();
assertThat(channel.isActive()).isTrue();
}
@Test
public void testSuccess_sendResponseToNextHandler() throws Exception {
setHandshakeSuccess();
String content = "stuff";
channel.writeOutbound(makeEppHttpResponse(content, HttpResponseStatus.OK));
ByteBuf response = channel.readOutbound();
assertThat(response).isEqualTo(Unpooled.wrappedBuffer(content.getBytes(UTF_8)));
// Nothing further to pass to the next handler.
assertThat((Object) channel.readOutbound()).isNull();
assertThat(channel.isActive()).isTrue();
}
@Test
public void testSuccess_sendResponseToNextHandler_andDisconnect() throws Exception {
setHandshakeSuccess();
String content = "stuff";
HttpResponse response = makeEppHttpResponse(content, HttpResponseStatus.OK);
response.headers().set("Epp-Session", "close");
channel.writeOutbound(response);
ByteBuf expectedResponse = channel.readOutbound();
assertThat(expectedResponse).isEqualTo(Unpooled.wrappedBuffer(content.getBytes(UTF_8)));
// Nothing further to pass to the next handler.
assertThat((Object) channel.readOutbound()).isNull();
// Channel is disconnected.
assertThat(channel.isActive()).isFalse();
}
@Test
public void testFailure_disconnectOnNonOKResponseStatus() throws Exception {
setHandshakeSuccess();
String content = "stuff";
try {
channel.writeOutbound(makeEppHttpResponse(content, HttpResponseStatus.BAD_REQUEST));
fail("Expected EncoderException");
} catch (EncoderException e) {
assertThat(e).hasCauseThat().isInstanceOf(IllegalArgumentException.class);
assertThat(e).hasMessageThat().contains(HttpResponseStatus.BAD_REQUEST.toString());
assertThat(channel.isActive()).isFalse();
}
}
@Test
public void testSuccess_setCookies() throws Exception {
setHandshakeSuccess();
// First inbound message is hello.
channel.readInbound();
String responseContent = "response";
Cookie cookie1 = new DefaultCookie("name1", "value1");
Cookie cookie2 = new DefaultCookie("name2", "value2");
channel.writeOutbound(
makeEppHttpResponse(responseContent, HttpResponseStatus.OK, cookie1, cookie2));
ByteBuf response = channel.readOutbound();
assertThat(response).isEqualTo(Unpooled.wrappedBuffer(responseContent.getBytes(UTF_8)));
String requestContent = "request";
channel.writeInbound(Unpooled.wrappedBuffer(requestContent.getBytes(UTF_8)));
FullHttpRequest request = channel.readInbound();
assertHttpRequestEquivalent(request, makeEppHttpRequest(requestContent, cookie1, cookie2));
// Nothing further to pass to the next handler.
assertThat((Object) channel.readInbound()).isNull();
assertThat((Object) channel.readOutbound()).isNull();
assertThat(channel.isActive()).isTrue();
}
@Test
public void testSuccess_updateCookies() throws Exception {
setHandshakeSuccess();
// First inbound message is hello.
channel.readInbound();
String responseContent1 = "response1";
Cookie cookie1 = new DefaultCookie("name1", "value1");
Cookie cookie2 = new DefaultCookie("name2", "value2");
// First response written.
channel.writeOutbound(
makeEppHttpResponse(responseContent1, HttpResponseStatus.OK, cookie1, cookie2));
channel.readOutbound();
String requestContent1 = "request1";
// First request written.
channel.writeInbound(Unpooled.wrappedBuffer(requestContent1.getBytes(UTF_8)));
FullHttpRequest request1 = channel.readInbound();
assertHttpRequestEquivalent(request1, makeEppHttpRequest(requestContent1, cookie1, cookie2));
String responseContent2 = "response2";
Cookie cookie3 = new DefaultCookie("name3", "value3");
Cookie newCookie2 = new DefaultCookie("name2", "newValue");
// Second response written.
channel.writeOutbound(
makeEppHttpResponse(responseContent2, HttpResponseStatus.OK, cookie3, newCookie2));
channel.readOutbound();
String requestContent2 = "request2";
// Second request written.
channel.writeInbound(Unpooled.wrappedBuffer(requestContent2.getBytes(UTF_8)));
FullHttpRequest request2 = channel.readInbound();
// Cookies in second request should be updated.
assertHttpRequestEquivalent(
request2, makeEppHttpRequest(requestContent2, cookie1, newCookie2, cookie3));
// Nothing further to pass to the next handler.
assertThat((Object) channel.readInbound()).isNull();
assertThat((Object) channel.readOutbound()).isNull();
assertThat(channel.isActive()).isTrue();
}
}