Open source GCP proxy

Dagger updated to 2.13, along with all its dependencies.

Also allows us to have multiple config files for different environment (prod, sandbox, alpha, local, etc) and specify which one to use on the command line with a --env flag. Therefore the same binary can be used in all environments.

-------------
Created by MOE: https://github.com/google/moe
MOE_MIGRATED_REVID=176551289
This commit is contained in:
jianglai 2017-11-21 13:17:10 -08:00
parent c7484b25e0
commit 7e42ee48a4
54 changed files with 6648 additions and 15 deletions

View file

@ -0,0 +1,227 @@
// 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.Protocol.PROTOCOL_KEY;
import static google.registry.proxy.TestUtils.assertHttpRequestEquivalent;
import static google.registry.proxy.TestUtils.assertHttpResponseEquivalent;
import static google.registry.proxy.TestUtils.makeHttpPostRequest;
import static google.registry.proxy.TestUtils.makeHttpResponse;
import static google.registry.proxy.handler.EppServiceHandler.CLIENT_CERTIFICATE_HASH_KEY;
import static google.registry.proxy.handler.RelayHandler.RELAY_CHANNEL_KEY;
import static google.registry.testing.JUnitBackports.expectThrows;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import com.google.common.collect.ImmutableList;
import google.registry.proxy.Protocol;
import google.registry.proxy.Protocol.BackendProtocol;
import google.registry.proxy.Protocol.FrontendProtocol;
import google.registry.proxy.metric.BackendMetrics;
import google.registry.testing.FakeClock;
import io.netty.channel.ChannelInitializer;
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.joda.time.DateTime;
import org.joda.time.Duration;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
/** Unit tests for {@link BackendMetricsHandler}. */
@RunWith(JUnit4.class)
public class BackendMetricsHandlerTest {
private static final String HOST = "host.tld";
private static final String CLIENT_CERT_HASH = "blah12345";
private static final String RELAYED_PROTOCOL_NAME = "frontend protocol";
private final FakeClock fakeClock = new FakeClock();
private final BackendMetrics metrics = mock(BackendMetrics.class);
private final BackendMetricsHandler handler = new BackendMetricsHandler(fakeClock, metrics);
private final BackendProtocol backendProtocol =
Protocol.backendBuilder()
.name("backend protocol")
.host(HOST)
.port(1)
.handlerProviders(ImmutableList.of())
.build();
private final FrontendProtocol frontendProtocol =
Protocol.frontendBuilder()
.name(RELAYED_PROTOCOL_NAME)
.port(2)
.relayProtocol(backendProtocol)
.handlerProviders(ImmutableList.of())
.build();
private EmbeddedChannel channel;
@Before
public void setUp() {
EmbeddedChannel frontendChannel = new EmbeddedChannel();
frontendChannel.attr(PROTOCOL_KEY).set(frontendProtocol);
frontendChannel.attr(CLIENT_CERTIFICATE_HASH_KEY).set(CLIENT_CERT_HASH);
channel =
new EmbeddedChannel(
new ChannelInitializer<EmbeddedChannel>() {
@Override
protected void initChannel(EmbeddedChannel ch) throws Exception {
ch.attr(PROTOCOL_KEY).set(backendProtocol);
ch.attr(RELAY_CHANNEL_KEY).set(frontendChannel);
ch.pipeline().addLast(handler);
}
});
}
@Test
public void testFailure_outbound_wrongType() {
Object request = new Object();
IllegalArgumentException e =
expectThrows(IllegalArgumentException.class, () -> channel.writeOutbound(request));
assertThat(e).hasMessageThat().isEqualTo("Outgoing request must be FullHttpRequest.");
}
@Test
public void testFailure_inbound_wrongType() {
Object response = new Object();
IllegalArgumentException e =
expectThrows(IllegalArgumentException.class, () -> channel.writeInbound(response));
assertThat(e).hasMessageThat().isEqualTo("Incoming response must be FullHttpResponse.");
}
@Test
public void testSuccess_oneRequest() {
FullHttpRequest request = makeHttpPostRequest("some content", HOST, "/");
// outbound message passed to the next handler.
assertThat(channel.writeOutbound(request)).isTrue();
assertHttpRequestEquivalent(request, channel.readOutbound());
verify(metrics).requestSent(RELAYED_PROTOCOL_NAME, CLIENT_CERT_HASH, request);
verifyNoMoreInteractions(metrics);
}
@Test
public void testSuccess_oneRequest_oneResponse() {
FullHttpRequest request = makeHttpPostRequest("some request", HOST, "/");
FullHttpResponse response = makeHttpResponse("some response", HttpResponseStatus.OK);
// outbound message passed to the next handler.
assertThat(channel.writeOutbound(request)).isTrue();
assertHttpRequestEquivalent(request, channel.readOutbound());
fakeClock.advanceOneMilli();
// inbound message passed to the next handler.
assertThat(channel.writeInbound(response)).isTrue();
assertHttpResponseEquivalent(response, channel.readInbound());
verify(metrics).requestSent(RELAYED_PROTOCOL_NAME, CLIENT_CERT_HASH, request);
verify(metrics).responseReceived(RELAYED_PROTOCOL_NAME, CLIENT_CERT_HASH, response, 1);
verifyNoMoreInteractions(metrics);
}
@Test
public void testSuccess_badResponse() {
FullHttpRequest request = makeHttpPostRequest("some request", HOST, "/");
FullHttpResponse response =
makeHttpResponse("some bad response", HttpResponseStatus.BAD_REQUEST);
// outbound message passed to the next handler.
assertThat(channel.writeOutbound(request)).isTrue();
assertHttpRequestEquivalent(request, channel.readOutbound());
fakeClock.advanceOneMilli();
// inbound message passed to the next handler.
// Even though the response status is not OK, the metrics handler only logs it and pass it
// along to the next handler, which handles it.
assertThat(channel.writeInbound(response)).isTrue();
assertHttpResponseEquivalent(response, channel.readInbound());
verify(metrics).requestSent(RELAYED_PROTOCOL_NAME, CLIENT_CERT_HASH, request);
verify(metrics).responseReceived(RELAYED_PROTOCOL_NAME, CLIENT_CERT_HASH, response, 1);
verifyNoMoreInteractions(metrics);
}
@Test
public void testFailure_responseBeforeRequest() {
FullHttpResponse response = makeHttpResponse("phantom response", HttpResponseStatus.OK);
IllegalStateException e =
expectThrows(IllegalStateException.class, () -> channel.writeInbound(response));
assertThat(e).hasMessageThat().isEqualTo("Response received before request is sent.");
}
@Test
public void testSuccess_pipelinedResponses() {
FullHttpRequest request1 = makeHttpPostRequest("request 1", HOST, "/");
FullHttpResponse response1 = makeHttpResponse("response 1", HttpResponseStatus.OK);
FullHttpRequest request2 = makeHttpPostRequest("request 2", HOST, "/");
FullHttpResponse response2 = makeHttpResponse("response 2", HttpResponseStatus.OK);
FullHttpRequest request3 = makeHttpPostRequest("request 3", HOST, "/");
FullHttpResponse response3 = makeHttpResponse("response 3", HttpResponseStatus.OK);
// First request, time = 0
assertThat(channel.writeOutbound(request1)).isTrue();
assertHttpRequestEquivalent(request1, channel.readOutbound());
DateTime sentTime1 = fakeClock.nowUtc();
fakeClock.advanceBy(Duration.millis(5));
// Second request, time = 5
assertThat(channel.writeOutbound(request2)).isTrue();
assertHttpRequestEquivalent(request2, channel.readOutbound());
DateTime sentTime2 = fakeClock.nowUtc();
fakeClock.advanceBy(Duration.millis(7));
// First response, time = 12, latency = 12 - 0 = 12
assertThat(channel.writeInbound(response1)).isTrue();
assertHttpResponseEquivalent(response1, channel.readInbound());
DateTime receivedTime1 = fakeClock.nowUtc();
fakeClock.advanceBy(Duration.millis(11));
// Third request, time = 23
assertThat(channel.writeOutbound(request3)).isTrue();
assertHttpRequestEquivalent(request3, channel.readOutbound());
DateTime sentTime3 = fakeClock.nowUtc();
fakeClock.advanceBy(Duration.millis(2));
// Second response, time = 25, latency = 25 - 5 = 20
assertThat(channel.writeInbound(response2)).isTrue();
assertHttpResponseEquivalent(response2, channel.readInbound());
DateTime receivedTime2 = fakeClock.nowUtc();
fakeClock.advanceBy(Duration.millis(4));
// Third response, time = 29, latency = 29 - 23 = 6
assertThat(channel.writeInbound(response3)).isTrue();
assertHttpResponseEquivalent(response3, channel.readInbound());
DateTime receivedTime3 = fakeClock.nowUtc();
long latency1 = new Duration(sentTime1, receivedTime1).getMillis();
long latency2 = new Duration(sentTime2, receivedTime2).getMillis();
long latency3 = new Duration(sentTime3, receivedTime3).getMillis();
verify(metrics).requestSent(RELAYED_PROTOCOL_NAME, CLIENT_CERT_HASH, request1);
verify(metrics).requestSent(RELAYED_PROTOCOL_NAME, CLIENT_CERT_HASH, request2);
verify(metrics).requestSent(RELAYED_PROTOCOL_NAME, CLIENT_CERT_HASH, request3);
verify(metrics).responseReceived(RELAYED_PROTOCOL_NAME, CLIENT_CERT_HASH, response1, latency1);
verify(metrics).responseReceived(RELAYED_PROTOCOL_NAME, CLIENT_CERT_HASH, response2, latency2);
verify(metrics).responseReceived(RELAYED_PROTOCOL_NAME, CLIENT_CERT_HASH, response3, latency3);
verifyNoMoreInteractions(metrics);
}
}

View file

@ -0,0 +1,330 @@
// 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 =
"<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>\n"
+ "<epp xmlns=\"urn:ietf:params:xml:ns:epp-1.0\">\n"
+ " <hello/>\n"
+ "</epp>\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<X509Certificate> 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<X509Certificate> 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<EmbeddedChannel>() {
@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 = "<epp>stuff</epp>";
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 = "<epp>stuff</epp>";
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 = "<epp>stuff</epp>";
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 = "<epp>stuff</epp>";
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 = "<epp>response</epp>";
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 = "<epp>request</epp>";
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 = "<epp>response1</epp>";
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 = "<epp>request1</epp>";
// First request written.
channel.writeInbound(Unpooled.wrappedBuffer(requestContent1.getBytes(UTF_8)));
FullHttpRequest request1 = channel.readInbound();
assertHttpRequestEquivalent(request1, makeEppHttpRequest(requestContent1, cookie1, cookie2));
String responseContent2 = "<epp>response2</epp>";
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 = "<epp>request2</epp>";
// 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();
}
}

View file

@ -0,0 +1,58 @@
// 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 java.nio.charset.StandardCharsets.US_ASCII;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.embedded.EmbeddedChannel;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
/** Unit tests for {@link HealthCheckHandler}. */
@RunWith(JUnit4.class)
public class HealthCheckHandlerTest {
private static final String CHECK_REQ = "REQUEST";
private static final String CHECK_RES = "RESPONSE";
private final HealthCheckHandler healthCheckHandler =
new HealthCheckHandler(CHECK_REQ, CHECK_RES);
private final EmbeddedChannel channel = new EmbeddedChannel(healthCheckHandler);
@Test
public void testSuccess_ResponseSent() {
ByteBuf input = Unpooled.wrappedBuffer(CHECK_REQ.getBytes(US_ASCII));
// No inbound message passed to the next handler.
assertThat(channel.writeInbound(input)).isFalse();
ByteBuf output = channel.readOutbound();
assertThat(channel.isActive()).isTrue();
assertThat(output.toString(US_ASCII)).isEqualTo(CHECK_RES);
}
@Test
public void testSuccess_IgnoreUnrecognizedRequest() {
String unrecognizedInput = "1234567";
ByteBuf input = Unpooled.wrappedBuffer(unrecognizedInput.getBytes(US_ASCII));
// No inbound message passed to the next handler.
assertThat(channel.writeInbound(input)).isFalse();
// No response is sent.
assertThat(channel.isActive()).isTrue();
assertThat((Object) channel.readOutbound()).isNull();
}
}

View file

@ -0,0 +1,118 @@
// 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.handler.ProxyProtocolHandler.REMOTE_ADDRESS_KEY;
import static java.nio.charset.StandardCharsets.UTF_8;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.embedded.EmbeddedChannel;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
/** Unit tests for {@link ProxyProtocolHandler}. */
@RunWith(JUnit4.class)
public class ProxyProtocolHandlerTest {
private static final String HEADER_TEMPLATE = "PROXY TCP%d %s %s %s %s\r\n";
private final ProxyProtocolHandler handler = new ProxyProtocolHandler();
private final EmbeddedChannel channel = new EmbeddedChannel(handler);
private String header;
@Test
public void testSuccess_proxyHeaderPresent_singleFrame() {
header = String.format(HEADER_TEMPLATE, 4, "172.0.0.1", "255.255.255.255", "234", "123");
String message = "some message";
// Header processed, rest of the message passed along.
assertThat(channel.writeInbound(Unpooled.wrappedBuffer((header + message).getBytes(UTF_8))))
.isTrue();
assertThat(((ByteBuf) channel.readInbound()).toString(UTF_8)).isEqualTo(message);
assertThat(channel.attr(REMOTE_ADDRESS_KEY).get()).isEqualTo("172.0.0.1");
assertThat(channel.pipeline().get(ProxyProtocolHandler.class)).isNull();
assertThat(channel.isActive()).isTrue();
}
@Test
public void testSuccess_proxyHeaderPresent_multipleFrames() {
header = String.format(HEADER_TEMPLATE, 4, "172.0.0.1", "255.255.255.255", "234", "123");
String frame1 = header.substring(0, 4);
String frame2 = header.substring(4, 7);
String frame3 = header.substring(7, 15);
String frame4 = header.substring(15, header.length() - 1);
String frame5 = header.substring(header.length() - 1) + "some message";
// Have not had enough bytes to determine the presence of a header, no message passed along.
assertThat(channel.writeInbound(Unpooled.wrappedBuffer(frame1.getBytes(UTF_8)))).isFalse();
// Have not had enough bytes to determine the end a header, no message passed along.
assertThat(channel.writeInbound(Unpooled.wrappedBuffer(frame2.getBytes(UTF_8)))).isFalse();
assertThat(channel.writeInbound(Unpooled.wrappedBuffer(frame3.getBytes(UTF_8)))).isFalse();
assertThat(channel.writeInbound(Unpooled.wrappedBuffer(frame4.getBytes(UTF_8)))).isFalse();
// Now there are enough bytes to construct a header.
assertThat(channel.writeInbound(Unpooled.wrappedBuffer(frame5.getBytes(UTF_8)))).isTrue();
assertThat(((ByteBuf) channel.readInbound()).toString(UTF_8)).isEqualTo("some message");
assertThat(channel.attr(REMOTE_ADDRESS_KEY).get()).isEqualTo("172.0.0.1");
assertThat(channel.pipeline().get(ProxyProtocolHandler.class)).isNull();
assertThat(channel.isActive()).isTrue();
}
@Test
public void testSuccess_proxyHeaderPresent_singleFrame_ipv6() {
header =
String.format(HEADER_TEMPLATE, 6, "2001:db8:0:1:1:1:1:1", "0:0:0:0:0:0:0:1", "234", "123");
String message = "some message";
// Header processed, rest of the message passed along.
assertThat(channel.writeInbound(Unpooled.wrappedBuffer((header + message).getBytes(UTF_8))))
.isTrue();
assertThat(((ByteBuf) channel.readInbound()).toString(UTF_8)).isEqualTo(message);
assertThat(channel.attr(REMOTE_ADDRESS_KEY).get()).isEqualTo("2001:db8:0:1:1:1:1:1");
assertThat(channel.pipeline().get(ProxyProtocolHandler.class)).isNull();
assertThat(channel.isActive()).isTrue();
}
@Test
public void testSuccess_proxyHeaderNotPresent_singleFrame() {
String message = "some message";
// No header present, rest of the message passed along.
assertThat(channel.writeInbound(Unpooled.wrappedBuffer(message.getBytes(UTF_8)))).isTrue();
assertThat(((ByteBuf) channel.readInbound()).toString(UTF_8)).isEqualTo(message);
assertThat(channel.attr(REMOTE_ADDRESS_KEY).get()).isNull();
assertThat(channel.pipeline().get(ProxyProtocolHandler.class)).isNull();
assertThat(channel.isActive()).isTrue();
}
@Test
public void testSuccess_proxyHeaderNotPresent_multipleFrames() {
String frame1 = "som";
String frame2 = "e mess";
String frame3 = "age\nis not";
String frame4 = "meant to be good.\n";
// Have not had enough bytes to determine the presence of a header, no message passed along.
assertThat(channel.writeInbound(Unpooled.wrappedBuffer(frame1.getBytes(UTF_8)))).isFalse();
// Now we have more than five bytes to determine if it starts with "PROXY"
assertThat(channel.writeInbound(Unpooled.wrappedBuffer(frame2.getBytes(UTF_8)))).isTrue();
assertThat(((ByteBuf) channel.readInbound()).toString(UTF_8)).isEqualTo(frame1 + frame2);
assertThat(channel.writeInbound(Unpooled.wrappedBuffer(frame3.getBytes(UTF_8)))).isTrue();
assertThat(((ByteBuf) channel.readInbound()).toString(UTF_8)).isEqualTo(frame3);
assertThat(channel.writeInbound(Unpooled.wrappedBuffer(frame4.getBytes(UTF_8)))).isTrue();
assertThat(((ByteBuf) channel.readInbound()).toString(UTF_8)).isEqualTo(frame4);
assertThat(channel.attr(REMOTE_ADDRESS_KEY).get()).isNull();
assertThat(channel.pipeline().get(ProxyProtocolHandler.class)).isNull();
assertThat(channel.isActive()).isTrue();
}
}

View file

@ -0,0 +1,91 @@
// 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.handler.RelayHandler.RELAY_CHANNEL_KEY;
import io.netty.channel.ChannelFuture;
import io.netty.channel.embedded.EmbeddedChannel;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
/** Unit tests for {@link RelayHandler}. */
@RunWith(JUnit4.class)
public class RelayHandlerTest {
private static final class ExpectedType {}
private static final class OtherType {}
private final RelayHandler<ExpectedType> relayHandler = new RelayHandler<>(ExpectedType.class);
private final EmbeddedChannel inboundChannel = new EmbeddedChannel(relayHandler);
private final EmbeddedChannel outboundChannel = new EmbeddedChannel();
@Before
public void setUp() {
inboundChannel.attr(RELAY_CHANNEL_KEY).set(outboundChannel);
}
@Test
public void testSuccess_relayInboundMessageOfExpectedType() {
ExpectedType inboundMessage = new ExpectedType();
// Relay handler intercepted the message, no further inbound message.
assertThat(inboundChannel.writeInbound(inboundMessage)).isFalse();
// Message wrote to outbound channel as-is.
ExpectedType relayedMessage = outboundChannel.readOutbound();
assertThat(relayedMessage).isEqualTo(inboundMessage);
}
@Test
public void testSuccess_ignoreInboundMessageOfOtherType() {
OtherType inboundMessage = new OtherType();
// Relay handler ignores inbound message of other types, the inbound message is passed along.
assertThat(inboundChannel.writeInbound(inboundMessage)).isTrue();
// Nothing is written into the outbound channel.
ExpectedType relayedMessage = outboundChannel.readOutbound();
assertThat(relayedMessage).isNull();
}
@Test
public void testSuccess_disconnectIfRelayIsUnsuccessful() {
ExpectedType inboundMessage = new ExpectedType();
// Outbound channel is closed.
outboundChannel.finish();
assertThat(inboundChannel.writeInbound(inboundMessage)).isFalse();
ExpectedType relayedMessage = outboundChannel.readOutbound();
assertThat(relayedMessage).isNull();
// Inbound channel is closed as well.
assertThat(inboundChannel.isActive()).isFalse();
}
@Test
public void testSuccess_disconnectRelayChannelIfInactive() {
ChannelFuture unusedFuture = inboundChannel.close();
assertThat(outboundChannel.isActive()).isFalse();
}
@Test
public void testSuccess_channelRead_relayNotSet() {
ExpectedType inboundMessage = new ExpectedType();
inboundChannel.attr(RELAY_CHANNEL_KEY).set(null);
// Nothing to read.
assertThat(inboundChannel.writeInbound(inboundMessage)).isFalse();
// Inbound channel is closed.
assertThat(inboundChannel.isActive()).isFalse();
}
}

View file

@ -0,0 +1,288 @@
// 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.Protocol.PROTOCOL_KEY;
import static google.registry.proxy.handler.SslInitializerTestUtils.getKeyPair;
import static google.registry.proxy.handler.SslInitializerTestUtils.setUpClient;
import static google.registry.proxy.handler.SslInitializerTestUtils.setUpServer;
import static google.registry.proxy.handler.SslInitializerTestUtils.signKeyPair;
import static google.registry.proxy.handler.SslInitializerTestUtils.verifySslChannel;
import com.google.common.collect.ImmutableList;
import google.registry.proxy.Protocol;
import google.registry.proxy.Protocol.BackendProtocol;
import google.registry.proxy.handler.SslInitializerTestUtils.DumpHandler;
import google.registry.proxy.handler.SslInitializerTestUtils.EchoHandler;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.Channel;
import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.embedded.EmbeddedChannel;
import io.netty.channel.local.LocalAddress;
import io.netty.channel.local.LocalChannel;
import io.netty.handler.codec.DecoderException;
import io.netty.handler.ssl.SslContext;
import io.netty.handler.ssl.SslContextBuilder;
import io.netty.handler.ssl.SslHandler;
import io.netty.handler.ssl.SslProvider;
import io.netty.handler.ssl.util.SelfSignedCertificate;
import io.netty.util.concurrent.Future;
import java.security.KeyPair;
import java.security.PrivateKey;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import javax.net.ssl.SSLException;
import javax.net.ssl.SSLHandshakeException;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
/**
* Unit tests for {@link SslClientInitializer}.
*
* <p>To validate that the handler accepts & rejects connections as expected, a test server and a
* test client are spun up, and both connect to the {@link LocalAddress} within the JVM. This avoids
* the overhead of routing traffic through the network layer, even if it were to go through
* loopback. It also alleviates the need to pick a free port to use.
*
* <p>The local addresses used in each test method must to be different, otherwise tests run in
* parallel may interfere with each other.
*/
@RunWith(JUnit4.class)
public class SslClientInitializerTest {
/** Fake host to test if the SSL engine gets the correct peer host. */
private static final String SSL_HOST = "www.example.tld";
/** Fake port to test if the SSL engine gets the correct peer port. */
private static final int SSL_PORT = 12345;
/** Fake protocol saved in channel attribute. */
private static final BackendProtocol PROTOCOL =
Protocol.backendBuilder()
.name("ssl")
.host(SSL_HOST)
.port(SSL_PORT)
.handlerProviders(ImmutableList.of())
.build();
private ChannelInitializer<LocalChannel> getServerInitializer(
PrivateKey privateKey,
X509Certificate certificate,
Lock serverLock,
Exception serverException)
throws Exception {
SslContext sslContext = SslContextBuilder.forServer(privateKey, certificate).build();
return new ChannelInitializer<LocalChannel>() {
@Override
protected void initChannel(LocalChannel ch) throws Exception {
ch.pipeline()
.addLast(
sslContext.newHandler(ch.alloc()), new EchoHandler(serverLock, serverException));
}
};
}
private ChannelInitializer<LocalChannel> getClientInitializer(
SslClientInitializer<LocalChannel> sslClientInitializer,
Lock clientLock,
ByteBuf buffer,
Exception clientException) {
return new ChannelInitializer<LocalChannel>() {
@Override
protected void initChannel(LocalChannel ch) throws Exception {
ch.pipeline()
.addLast(sslClientInitializer, new DumpHandler(clientLock, buffer, clientException));
}
};
}
@Test
public void testSuccess_swappedInitializerWithSslHandler() throws Exception {
SslClientInitializer<EmbeddedChannel> sslClientInitializer =
new SslClientInitializer<>(SslProvider.JDK, (X509Certificate[]) null);
EmbeddedChannel channel = new EmbeddedChannel();
channel.attr(PROTOCOL_KEY).set(PROTOCOL);
ChannelPipeline pipeline = channel.pipeline();
pipeline.addLast(sslClientInitializer);
ChannelHandler firstHandler = pipeline.first();
assertThat(firstHandler.getClass()).isEqualTo(SslHandler.class);
SslHandler sslHandler = (SslHandler) firstHandler;
assertThat(sslHandler.engine().getPeerHost()).isEqualTo(SSL_HOST);
assertThat(sslHandler.engine().getPeerPort()).isEqualTo(SSL_PORT);
assertThat(channel.isActive()).isTrue();
}
@Test
public void testSuccess_protocolAttributeNotSet() {
SslClientInitializer<EmbeddedChannel> sslClientInitializer =
new SslClientInitializer<>(SslProvider.JDK, (X509Certificate[]) null);
EmbeddedChannel channel = new EmbeddedChannel();
ChannelPipeline pipeline = channel.pipeline();
pipeline.addLast(sslClientInitializer);
// Channel initializer swallows error thrown, and closes the connection.
assertThat(channel.isActive()).isFalse();
}
@Test
public void testFailure_defaultTrustManager_rejectSelfSignedCert() throws Exception {
SelfSignedCertificate ssc = new SelfSignedCertificate(SSL_HOST);
LocalAddress localAddress = new LocalAddress("DEFAULT_TRUST_MANAGER_REJECT_SELF_SIGNED_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(ssc.key(), ssc.cert(), serverLock, serverException), localAddress);
SslClientInitializer<LocalChannel> sslClientInitializer =
new SslClientInitializer<>(SslProvider.JDK, (X509Certificate[]) null);
Channel channel =
setUpClient(
eventLoopGroup,
getClientInitializer(sslClientInitializer, clientLock, buffer, clientException),
localAddress,
PROTOCOL);
// Wait for handshake exception to throw.
clientLock.lock();
serverLock.lock();
// The connection is now terminated, both the client side and the server side should get
// exceptions (caught in the caughtException method in EchoHandler and DumpHandler,
// respectively).
assertThat(clientException).hasCauseThat().isInstanceOf(DecoderException.class);
assertThat(clientException)
.hasCauseThat()
.hasCauseThat()
.isInstanceOf(SSLHandshakeException.class);
assertThat(serverException).hasCauseThat().isInstanceOf(DecoderException.class);
assertThat(serverException).hasCauseThat().hasCauseThat().isInstanceOf(SSLException.class);
assertThat(channel.isActive()).isFalse();
Future<?> unusedFuture = eventLoopGroup.shutdownGracefully().syncUninterruptibly();
}
@Test
public void testSuccess_customTrustManager_acceptCertSignedByTrustedCa() throws Exception {
LocalAddress localAddress =
new LocalAddress("CUSTOM_TRUST_MANAGER_ACCEPT_CERT_SIGNED_BY_TRUSTED_CA");
Lock clientLock = new ReentrantLock();
Lock serverLock = new ReentrantLock();
ByteBuf buffer = Unpooled.buffer();
Exception clientException = new Exception();
Exception serverException = new Exception();
// Generate a new key pair.
KeyPair keyPair = getKeyPair();
// Generate a self signed certificate, and use it to sign the key pair.
SelfSignedCertificate ssc = new SelfSignedCertificate();
X509Certificate cert = signKeyPair(ssc, keyPair, SSL_HOST);
// Set up the server to use the signed cert and private key to perform handshake;
PrivateKey privateKey = keyPair.getPrivate();
EventLoopGroup eventLoopGroup =
setUpServer(
getServerInitializer(privateKey, cert, serverLock, serverException), localAddress);
// Set up the client to trust the self signed cert used to sign the cert that server provides.
SslClientInitializer<LocalChannel> sslClientInitializer =
new SslClientInitializer<>(SslProvider.JDK, ssc.cert());
Channel channel =
setUpClient(
eventLoopGroup,
getClientInitializer(sslClientInitializer, clientLock, buffer, clientException),
localAddress,
PROTOCOL);
verifySslChannel(channel, ImmutableList.of(cert), clientLock, serverLock, buffer, SSL_HOST);
Future<?> unusedFuture = eventLoopGroup.shutdownGracefully().syncUninterruptibly();
}
@Test
public void testFailure_customTrustManager_wrongHostnameInCertificate() throws Exception {
LocalAddress localAddress = new LocalAddress("CUSTOM_TRUST_MANAGER_WRONG_HOSTNAME");
Lock clientLock = new ReentrantLock();
Lock serverLock = new ReentrantLock();
ByteBuf buffer = Unpooled.buffer();
Exception clientException = new Exception();
Exception serverException = new Exception();
// Generate a new key pair.
KeyPair keyPair = getKeyPair();
// Generate a self signed certificate, and use it to sign the key pair.
SelfSignedCertificate ssc = new SelfSignedCertificate();
X509Certificate cert = signKeyPair(ssc, keyPair, "wrong.com");
// Set up the server to use the signed cert and private key to perform handshake;
PrivateKey privateKey = keyPair.getPrivate();
EventLoopGroup eventLoopGroup =
setUpServer(
getServerInitializer(privateKey, cert, serverLock, serverException), localAddress);
// Set up the client to trust the self signed cert used to sign the cert that server provides.
SslClientInitializer<LocalChannel> sslClientInitializer =
new SslClientInitializer<>(SslProvider.JDK, ssc.cert());
Channel channel =
setUpClient(
eventLoopGroup,
getClientInitializer(sslClientInitializer, clientLock, buffer, clientException),
localAddress,
PROTOCOL);
serverLock.lock();
clientLock.lock();
// When the client rejects the server cert due to wrong hostname, the client error is wrapped
// several layers in the exception. The server also throws an exception.
assertThat(clientException).hasCauseThat().isInstanceOf(DecoderException.class);
assertThat(clientException)
.hasCauseThat()
.hasCauseThat()
.isInstanceOf(SSLHandshakeException.class);
assertThat(clientException)
.hasCauseThat()
.hasCauseThat()
.hasCauseThat()
.isInstanceOf(SSLHandshakeException.class);
assertThat(clientException)
.hasCauseThat()
.hasCauseThat()
.hasCauseThat()
.hasCauseThat()
.isInstanceOf(CertificateException.class);
assertThat(clientException)
.hasCauseThat()
.hasCauseThat()
.hasCauseThat()
.hasCauseThat()
.hasMessageThat()
.contains(SSL_HOST);
assertThat(serverException).hasCauseThat().isInstanceOf(DecoderException.class);
assertThat(serverException).hasCauseThat().hasCauseThat().isInstanceOf(SSLException.class);
assertThat(channel.isActive()).isFalse();
Future<?> unusedFuture = eventLoopGroup.shutdownGracefully().syncUninterruptibly();
}
}

View file

@ -0,0 +1,277 @@
// 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.Protocol.PROTOCOL_KEY;
import static java.nio.charset.StandardCharsets.UTF_8;
import com.google.common.collect.ImmutableList;
import google.registry.proxy.Protocol.BackendProtocol;
import io.netty.bootstrap.Bootstrap;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.Channel;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.local.LocalAddress;
import io.netty.channel.local.LocalChannel;
import io.netty.channel.local.LocalServerChannel;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.handler.ssl.SslHandler;
import io.netty.handler.ssl.util.SelfSignedCertificate;
import java.math.BigInteger;
import java.nio.charset.StandardCharsets;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.SecureRandom;
import java.security.Security;
import java.security.cert.X509Certificate;
import java.time.Duration;
import java.time.Instant;
import java.util.Date;
import java.util.concurrent.locks.Lock;
import javax.net.ssl.SSLSession;
import javax.security.auth.x500.X500Principal;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.x509.X509V3CertificateGenerator;
/**
* Utility class that provides methods used by {@link SslClientInitializerTest} and {@link
* SslServerInitializerTest}.
*/
public class SslInitializerTestUtils {
static {
Security.addProvider(new BouncyCastleProvider());
}
/**
* Sets up a server channel bound to the given local address.
*
* @return the event loop group used to process incoming connections.
*/
static EventLoopGroup setUpServer(
ChannelInitializer<LocalChannel> serverInitializer, LocalAddress localAddress)
throws Exception {
// Only use one thread in the event loop group. The same event loop group will be used to
// register client channels during setUpClient as well. This ensures that all I/O activities
// in both channels happen in the same thread, making debugging easier (i. e. no need to jump
// between threads when debugging, everything happens synchronously within the only I/O
// effectively). Note that the main thread is still separate from the I/O thread and
// synchronization (using the lock field) is still needed when the main thread needs to verify
// properties calculated by the I/O thread.
EventLoopGroup eventLoopGroup = new NioEventLoopGroup(1);
ServerBootstrap sb =
new ServerBootstrap()
.group(eventLoopGroup)
.channel(LocalServerChannel.class)
.childHandler(serverInitializer);
ChannelFuture unusedFuture = sb.bind(localAddress).syncUninterruptibly();
return eventLoopGroup;
}
/**
* Sets up a client channel connecting to the give local address.
*
* @param eventLoopGroup the same {@link EventLoopGroup} that is used to bootstrap server.
* @return the connected client channel.
*/
static Channel setUpClient(
EventLoopGroup eventLoopGroup,
ChannelInitializer<LocalChannel> clientInitializer,
LocalAddress localAddress,
BackendProtocol protocol)
throws Exception {
Bootstrap b =
new Bootstrap()
.group(eventLoopGroup)
.channel(LocalChannel.class)
.handler(clientInitializer)
.attr(PROTOCOL_KEY, protocol);
return b.connect(localAddress).syncUninterruptibly().channel();
}
/** A handler that echoes back its inbound message. Used in test server. */
static class EchoHandler extends ChannelInboundHandlerAdapter {
/**
* A lock that synchronizes server I/O activity with the main thread. Acquired by the server I/O
* thread when the handler is constructed, released when the server echoes back, or when an
* exception is caught (during SSH handshake for example).
*/
private final Lock lock;
/**
* Exception that would be initialized with the exception caught during SSL handshake. This
* field is constructed in the main thread and passed in the constructor. After a failure the
* main thread can inspect this object to assert the cause of the failure.
*/
private final Exception serverException;
EchoHandler(Lock lock, Exception serverException) {
// This handler is constructed within getClientInitializer, which is called in the I/O thread.
// The server lock is therefore locked by the I/O thread.
lock.lock();
this.lock = lock;
this.serverException = serverException;
}
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
// Always unlock regardless of whether the write is successful.
ctx.writeAndFlush(msg).addListener(future -> lock.unlock());
}
/** Saves any inbound error into the server exception field. */
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
serverException.initCause(cause);
// If an exception is caught, we should also release the lock so that the main thread knows
// there is an exception to inspect now.
lock.unlock();
}
}
/** A handler that dumps its inbound message in to {@link ByteBuf}. */
static class DumpHandler extends ChannelInboundHandlerAdapter {
/**
* A lock that synchronizes server I/O activity with the main thread. Acquired by the server I/O
* thread when the handler is constructed, released when the server echoes back, or when an
* exception is caught (during SSH handshake for example).
*/
private final Lock lock;
/**
* A Buffer that is used to store incoming message. Constructed in the main thread and passed in
* the constructor. The main thread can inspect this object to assert that the incoming message
* is as expected.
*/
private final ByteBuf buffer;
/**
* Exception that would be initialized with the exception caught during SSL handshake. This
* field is constructed in the main thread and passed in the constructor. After a failure the
* main thread can inspect this object to assert the cause of the failure.
*/
private final Exception clientException;
DumpHandler(Lock lock, ByteBuf buffer, Exception clientException) {
super();
// This handler is constructed within getClientInitializer, which is called in the I/O thread.
// The client lock is therefore locked by the I/O thread.
lock.lock();
this.lock = lock;
this.buffer = buffer;
this.clientException = clientException;
}
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
buffer.writeBytes((ByteBuf) msg);
// If a message is received here, the main thread must be waiting to acquire the lock from
// the I/O thread in order to verify it. Releasing the lock to notify the main thread it can
// continue now that the message has been written.
lock.unlock();
}
/** Saves any inbound error into clientException. */
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
clientException.initCause(cause);
// If an exception is caught here, the main thread must be waiting to acquire the lock from
// the I/O thread in order to verify it. Releasing the lock to notify the main thread it can
// continue now that the exception has been written.
lock.unlock();
}
}
public static KeyPair getKeyPair() throws Exception {
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA", "BC");
keyPairGenerator.initialize(2048, new SecureRandom());
return keyPairGenerator.generateKeyPair();
}
/**
* Signs the given key pair with the given self signed certificate.
*
* @return signed public key (of the key pair) certificate
*/
public static X509Certificate signKeyPair(
SelfSignedCertificate ssc, KeyPair keyPair, String hostname) throws Exception {
X509V3CertificateGenerator certGen = new X509V3CertificateGenerator();
X500Principal dnName = new X500Principal("CN=" + hostname);
certGen.setSerialNumber(BigInteger.valueOf(System.currentTimeMillis()));
certGen.setSubjectDN(dnName);
certGen.setIssuerDN(ssc.cert().getSubjectX500Principal());
certGen.setNotBefore(Date.from(Instant.now().minus(Duration.ofDays(1))));
certGen.setNotAfter(Date.from(Instant.now().plus(Duration.ofDays(1))));
certGen.setPublicKey(keyPair.getPublic());
certGen.setSignatureAlgorithm("SHA256WithRSAEncryption");
return certGen.generate(ssc.key(), "BC");
}
/**
* Verifies tha the SSL channel is established as expected, and also sends a message to the server
* and verifies if it is echoed back correctly.
*
* @param certs The certificate that the server should provide.
* @return The SSL session in current channel, can be used for further validation.
*/
static SSLSession verifySslChannel(
Channel channel,
ImmutableList<X509Certificate> certs,
Lock clientLock,
Lock serverLock,
ByteBuf buffer,
String sniHostname)
throws Exception {
SslHandler sslHandler = channel.pipeline().get(SslHandler.class);
// Wait till the handshake is complete.
sslHandler.handshakeFuture().get();
assertThat(channel.isActive()).isTrue();
assertThat(sslHandler.handshakeFuture().isSuccess()).isTrue();
assertThat(sslHandler.engine().getSession().isValid()).isTrue();
assertThat(sslHandler.engine().getSession().getPeerCertificates())
.asList()
.containsExactly(certs.toArray());
// Verify that the client sent expected SNI name during handshake.
assertThat(sslHandler.engine().getSSLParameters().getServerNames()).hasSize(1);
assertThat(sslHandler.engine().getSSLParameters().getServerNames().get(0).getEncoded())
.isEqualTo(sniHostname.getBytes(UTF_8));
// Test that message can go through, bound inbound and outbound.
String inputString = "Hello, world!";
// The client writes the message to the server, which echos it back. The client receives the
// echo and writes to BUFFER. All these activities happens in the I/O thread, and this call
// returns immediately.
ChannelFuture unusedFuture =
channel.writeAndFlush(
Unpooled.wrappedBuffer(inputString.getBytes(StandardCharsets.US_ASCII)));
// The lock is acquired by the I/O thread when the client's DumpHandler is constructed.
// Attempting to acquire it here blocks the main thread, until the I/O thread releases the lock
// after the DumpHandler writes the echo back to the buffer.
clientLock.lock();
serverLock.lock();
assertThat(buffer.toString(StandardCharsets.US_ASCII)).isEqualTo(inputString);
return sslHandler.engine().getSession();
}
}

View file

@ -0,0 +1,347 @@
// 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.handler.SslInitializerTestUtils.getKeyPair;
import static google.registry.proxy.handler.SslInitializerTestUtils.setUpClient;
import static google.registry.proxy.handler.SslInitializerTestUtils.setUpServer;
import static google.registry.proxy.handler.SslInitializerTestUtils.signKeyPair;
import static google.registry.proxy.handler.SslInitializerTestUtils.verifySslChannel;
import com.google.common.collect.ImmutableList;
import google.registry.proxy.Protocol;
import google.registry.proxy.Protocol.BackendProtocol;
import google.registry.proxy.handler.SslInitializerTestUtils.DumpHandler;
import google.registry.proxy.handler.SslInitializerTestUtils.EchoHandler;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.Channel;
import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.embedded.EmbeddedChannel;
import io.netty.channel.local.LocalAddress;
import io.netty.channel.local.LocalChannel;
import io.netty.handler.codec.DecoderException;
import io.netty.handler.ssl.SslContextBuilder;
import io.netty.handler.ssl.SslHandler;
import io.netty.handler.ssl.SslProvider;
import io.netty.handler.ssl.util.SelfSignedCertificate;
import io.netty.util.concurrent.Future;
import java.security.KeyPair;
import java.security.PrivateKey;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import javax.net.ssl.SSLEngine;
import javax.net.ssl.SSLException;
import javax.net.ssl.SSLHandshakeException;
import javax.net.ssl.SSLParameters;
import javax.net.ssl.SSLSession;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
/**
* Unit tests for {@link SslServerInitializer}.
*
* <p>To validate that the handler accepts & rejects connections as expected, a test server and a
* test client are spun up, and both connect to the {@link LocalAddress} within the JVM. This avoids
* the overhead of routing traffic through the network layer, even if it were to go through
* loopback. It also alleviates the need to pick a free port to use.
*
* <p>The local addresses used in each test method must to be different, otherwise tests run in
* parallel may interfere with each other.
*/
@RunWith(JUnit4.class)
public class SslServerInitializerTest {
/** Fake host to test if the SSL engine gets the correct peer host. */
private static final String SSL_HOST = "www.example.tld";
/** Fake port to test if the SSL engine gets the correct peer port. */
private static final int SSL_PORT = 12345;
/** Fake protocol saved in channel attribute. */
private static final BackendProtocol PROTOCOL =
Protocol.backendBuilder()
.name("ssl")
.host(SSL_HOST)
.port(SSL_PORT)
.handlerProviders(ImmutableList.of())
.build();
private ChannelInitializer<LocalChannel> getServerInitializer(
Lock serverLock,
Exception serverException,
PrivateKey privateKey,
X509Certificate... certificates)
throws Exception {
return new ChannelInitializer<LocalChannel>() {
@Override
protected void initChannel(LocalChannel ch) throws Exception {
ch.pipeline()
.addLast(
new SslServerInitializer<LocalChannel>(SslProvider.JDK, privateKey, certificates),
new EchoHandler(serverLock, serverException));
}
};
}
private ChannelInitializer<LocalChannel> getClientInitializer(
X509Certificate trustedCertificate,
PrivateKey privateKey,
X509Certificate certificate,
Lock clientLock,
ByteBuf buffer,
Exception clientException) {
return new ChannelInitializer<LocalChannel>() {
@Override
protected void initChannel(LocalChannel ch) throws Exception {
SslContextBuilder sslContextBuilder =
SslContextBuilder.forClient().trustManager(trustedCertificate);
if (privateKey != null && certificate != null) {
sslContextBuilder.keyManager(privateKey, certificate);
}
SslHandler sslHandler =
sslContextBuilder.build().newHandler(ch.alloc(), SSL_HOST, SSL_PORT);
// Enable hostname verification.
SSLEngine sslEngine = sslHandler.engine();
SSLParameters sslParameters = sslEngine.getSSLParameters();
sslParameters.setEndpointIdentificationAlgorithm("HTTPS");
sslEngine.setSSLParameters(sslParameters);
ch.pipeline().addLast("Client SSL Handler", sslHandler);
ch.pipeline().addLast(new DumpHandler(clientLock, buffer, clientException));
}
};
}
@Test
public void testSuccess_swappedInitializerWithSslHandler() throws Exception {
SelfSignedCertificate ssc = new SelfSignedCertificate(SSL_HOST);
SslServerInitializer<EmbeddedChannel> sslServerInitializer =
new SslServerInitializer<>(SslProvider.JDK, ssc.key(), ssc.cert());
EmbeddedChannel channel = new EmbeddedChannel();
ChannelPipeline pipeline = channel.pipeline();
pipeline.addLast(sslServerInitializer);
ChannelHandler firstHandler = pipeline.first();
assertThat(firstHandler.getClass()).isEqualTo(SslHandler.class);
SslHandler sslHandler = (SslHandler) firstHandler;
assertThat(sslHandler.engine().getNeedClientAuth()).isTrue();
assertThat(channel.isActive()).isTrue();
}
@Test
public void testSuccess_trustAnyClientCert() throws Exception {
SelfSignedCertificate serverSsc = new SelfSignedCertificate(SSL_HOST);
LocalAddress localAddress = new LocalAddress("TRUST_ANY_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(serverLock, serverException, serverSsc.key(), serverSsc.cert()),
localAddress);
SelfSignedCertificate clientSsc = new SelfSignedCertificate();
Channel channel =
setUpClient(
eventLoopGroup,
getClientInitializer(
serverSsc.cert(),
clientSsc.key(),
clientSsc.cert(),
clientLock,
buffer,
clientException),
localAddress,
PROTOCOL);
SSLSession sslSession =
verifySslChannel(
channel, ImmutableList.of(serverSsc.cert()), clientLock, serverLock, buffer, SSL_HOST);
// Verify that the SSL session gets the 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()).asList().containsExactly(clientSsc.cert());
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.
SelfSignedCertificate caSsc = new SelfSignedCertificate();
KeyPair keyPair = getKeyPair();
X509Certificate serverCert = signKeyPair(caSsc, keyPair, SSL_HOST);
LocalAddress localAddress = new LocalAddress("CERT_SIGNED_BY_OTHER_CA");
Lock clientLock = new ReentrantLock();
Lock serverLock = new ReentrantLock();
ByteBuf buffer = Unpooled.buffer();
Exception clientException = new Exception();
Exception serverException = new Exception();
EventLoopGroup eventLoopGroup =
setUpServer(
getServerInitializer(
serverLock,
serverException,
keyPair.getPrivate(),
// Serving both the server cert, and the CA cert
serverCert,
caSsc.cert()),
localAddress);
SelfSignedCertificate clientSsc = new SelfSignedCertificate();
Channel channel =
setUpClient(
eventLoopGroup,
getClientInitializer(
// Client trusts the CA cert
caSsc.cert(),
clientSsc.key(),
clientSsc.cert(),
clientLock,
buffer,
clientException),
localAddress,
PROTOCOL);
SSLSession sslSession =
verifySslChannel(
channel,
ImmutableList.of(serverCert, caSsc.cert()),
clientLock,
serverLock,
buffer,
SSL_HOST);
assertThat(sslSession.getLocalCertificates()).asList().containsExactly(clientSsc.cert());
assertThat(sslSession.getPeerCertificates())
.asList()
.containsExactly(serverCert, caSsc.cert())
.inOrder();
Future<?> unusedFuture = eventLoopGroup.shutdownGracefully().syncUninterruptibly();
}
@Test
public void testFailure_requireClientCertificate() throws Exception {
SelfSignedCertificate serverSsc = new SelfSignedCertificate(SSL_HOST);
LocalAddress localAddress = new LocalAddress("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(serverLock, serverException, serverSsc.key(), serverSsc.cert()),
localAddress);
Channel channel =
setUpClient(
eventLoopGroup,
getClientInitializer(
serverSsc.cert(),
// No client cert/private key used.
null,
null,
clientLock,
buffer,
clientException),
localAddress,
PROTOCOL);
serverLock.lock();
// When the server rejects the client during handshake due to lack of client certificate, only
// the server throws an exception.
assertThat(serverException).hasCauseThat().isInstanceOf(DecoderException.class);
assertThat(serverException)
.hasCauseThat()
.hasCauseThat()
.isInstanceOf(SSLHandshakeException.class);
assertThat(channel.isActive()).isFalse();
Future<?> unusedFuture = eventLoopGroup.shutdownGracefully().syncUninterruptibly();
}
@Test
public void testFailure_wrongHostnameInCertificate() throws Exception {
SelfSignedCertificate serverSsc = new SelfSignedCertificate("wrong.com");
LocalAddress localAddress = new LocalAddress("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(serverLock, serverException, serverSsc.key(), serverSsc.cert()),
localAddress);
SelfSignedCertificate clientSsc = new SelfSignedCertificate();
Channel channel =
setUpClient(
eventLoopGroup,
getClientInitializer(
serverSsc.cert(),
clientSsc.key(),
clientSsc.cert(),
clientLock,
buffer,
clientException),
localAddress,
PROTOCOL);
serverLock.lock();
clientLock.lock();
// When the client rejects the server cert due to wrong hostname, the client error is wrapped
// several layers in the exception. The server also throws an exception.
assertThat(clientException).hasCauseThat().isInstanceOf(DecoderException.class);
assertThat(clientException)
.hasCauseThat()
.hasCauseThat()
.isInstanceOf(SSLHandshakeException.class);
assertThat(clientException)
.hasCauseThat()
.hasCauseThat()
.hasCauseThat()
.isInstanceOf(SSLHandshakeException.class);
assertThat(clientException)
.hasCauseThat()
.hasCauseThat()
.hasCauseThat()
.hasCauseThat()
.isInstanceOf(CertificateException.class);
assertThat(clientException)
.hasCauseThat()
.hasCauseThat()
.hasCauseThat()
.hasCauseThat()
.hasMessageThat()
.contains(SSL_HOST);
assertThat(serverException).hasCauseThat().isInstanceOf(DecoderException.class);
assertThat(serverException).hasCauseThat().hasCauseThat().isInstanceOf(SSLException.class);
assertThat(channel.isActive()).isFalse();
Future<?> unusedFuture = eventLoopGroup.shutdownGracefully().syncUninterruptibly();
}
}

View file

@ -0,0 +1,128 @@
// 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.makeWhoisHttpRequest;
import static google.registry.proxy.TestUtils.makeWhoisHttpResponse;
import static java.nio.charset.StandardCharsets.US_ASCII;
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.metric.FrontendMetrics;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.DefaultChannelId;
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.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
/** Unit tests for {@link WhoisServiceHandler}. */
@RunWith(JUnit4.class)
public class WhoisServiceHandlerTest {
private static final String RELAY_HOST = "www.example.tld";
private static final String RELAY_PATH = "/test";
private static final String QUERY_CONTENT = "test.tld";
private static final String ACCESS_TOKEN = "this.access.token";
private static final String PROTOCOL = "whois";
private static final String CLIENT_HASH = "none";
private final FrontendMetrics metrics = mock(FrontendMetrics.class);
private final WhoisServiceHandler whoisServiceHandler =
new WhoisServiceHandler(RELAY_HOST, RELAY_PATH, () -> ACCESS_TOKEN, metrics);
private EmbeddedChannel channel;
@Before
public void setUp() {
// Need to reset metrics for each test method, since they are static fields on the class and
// shared between each run.
channel = new EmbeddedChannel(whoisServiceHandler);
}
@Test
public void testSuccess_connectionMetrics_oneChannel() {
assertThat(channel.isActive()).isTrue();
verify(metrics).registerActiveConnection(PROTOCOL, CLIENT_HASH, channel);
verifyNoMoreInteractions(metrics);
}
@Test
public void testSuccess_ConnectionMetrics_twoConnections() {
assertThat(channel.isActive()).isTrue();
verify(metrics).registerActiveConnection(PROTOCOL, CLIENT_HASH, channel);
// Setup second channel.
WhoisServiceHandler whoisServiceHandler2 =
new WhoisServiceHandler(RELAY_HOST, RELAY_PATH, () -> ACCESS_TOKEN, metrics);
EmbeddedChannel channel2 =
// We need a new channel id so that it has a different hash code.
// This only is needed for EmbeddedChannel because it has a dummy hash code implementation.
new EmbeddedChannel(DefaultChannelId.newInstance(), whoisServiceHandler2);
assertThat(channel2.isActive()).isTrue();
verify(metrics).registerActiveConnection(PROTOCOL, CLIENT_HASH, channel2);
verifyNoMoreInteractions(metrics);
}
@Test
public void testSuccess_fireInboundHttpRequest() {
ByteBuf inputBuffer = Unpooled.wrappedBuffer(QUERY_CONTENT.getBytes(US_ASCII));
FullHttpRequest expectedRequest =
makeWhoisHttpRequest(QUERY_CONTENT, RELAY_HOST, RELAY_PATH, ACCESS_TOKEN);
// Input data passed to next handler
assertThat(channel.writeInbound(inputBuffer)).isTrue();
FullHttpRequest inputRequest = channel.readInbound();
assertThat(inputRequest).isEqualTo(expectedRequest);
// The channel is still open, and nothing else is to be read from it.
assertThat((Object) channel.readInbound()).isNull();
assertThat(channel.isActive()).isTrue();
}
@Test
public void testSuccess_parseOutboundHttpResponse() {
String outputString = "line1\r\nline2\r\n";
FullHttpResponse outputResponse = makeWhoisHttpResponse(outputString, HttpResponseStatus.OK);
// output data passed to next handler
assertThat(channel.writeOutbound(outputResponse)).isTrue();
ByteBuf parsedBuffer = channel.readOutbound();
assertThat(parsedBuffer.toString(US_ASCII)).isEqualTo(outputString);
// The channel is still open, and nothing else is to be written to it.
assertThat((Object) channel.readOutbound()).isNull();
assertThat(channel.isActive()).isTrue();
}
@Test
public void testFailure_OutboundHttpResponseNotOK() {
String outputString = "line1\r\nline2\r\n";
FullHttpResponse outputResponse =
makeWhoisHttpResponse(outputString, HttpResponseStatus.BAD_REQUEST);
try {
channel.writeOutbound(outputResponse);
fail("Expected failure due to non-OK HTTP response status.");
} catch (Exception e) {
assertThat(e).hasCauseThat().isInstanceOf(IllegalArgumentException.class);
assertThat(e).hasMessageThat().contains("400 Bad Request");
}
assertThat(channel.isActive()).isFalse();
}
}