mirror of
https://github.com/google/nomulus.git
synced 2025-08-02 07:52:11 +02:00
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:
parent
c7484b25e0
commit
7e42ee48a4
54 changed files with 6648 additions and 15 deletions
45
javatests/google/registry/proxy/BUILD
Normal file
45
javatests/google/registry/proxy/BUILD
Normal file
|
@ -0,0 +1,45 @@
|
|||
package(
|
||||
default_testonly = 1,
|
||||
default_visibility = ["//java/google/registry:registry_project"],
|
||||
)
|
||||
|
||||
licenses(["notice"]) # Apache 2.0
|
||||
|
||||
load("//java/com/google/testing/builddefs:GenTestRules.bzl", "GenTestRules")
|
||||
|
||||
java_library(
|
||||
name = "proxy",
|
||||
srcs = glob(["**/*.java"]),
|
||||
resources = glob(["testdata/*.xml"]),
|
||||
deps = [
|
||||
"//java/google/registry/monitoring/metrics",
|
||||
"//java/google/registry/monitoring/metrics/contrib",
|
||||
"//java/google/registry/proxy",
|
||||
"//java/google/registry/util",
|
||||
"//javatests/google/registry/testing",
|
||||
"@com_beust_jcommander",
|
||||
"@com_google_dagger",
|
||||
"@com_google_guava",
|
||||
"@com_google_truth",
|
||||
"@com_google_truth_extensions_truth_java8_extension",
|
||||
"@io_netty_buffer",
|
||||
"@io_netty_codec",
|
||||
"@io_netty_codec_http",
|
||||
"@io_netty_common",
|
||||
"@io_netty_handler",
|
||||
"@io_netty_transport",
|
||||
"@joda_time",
|
||||
"@junit",
|
||||
"@org_bouncycastle_bcpkix_jdk15on",
|
||||
"@org_mockito_all",
|
||||
],
|
||||
)
|
||||
|
||||
GenTestRules(
|
||||
name = "GeneratedTestRules",
|
||||
test_files = glob(
|
||||
["**/*Test.java"],
|
||||
exclude = ["ProtocolModuleTest.java"],
|
||||
),
|
||||
deps = [":proxy"],
|
||||
)
|
158
javatests/google/registry/proxy/CertificateModuleTest.java
Normal file
158
javatests/google/registry/proxy/CertificateModuleTest.java
Normal file
|
@ -0,0 +1,158 @@
|
|||
// 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;
|
||||
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
import static google.registry.proxy.handler.SslInitializerTestUtils.getKeyPair;
|
||||
import static google.registry.proxy.handler.SslInitializerTestUtils.signKeyPair;
|
||||
import static java.nio.charset.StandardCharsets.UTF_8;
|
||||
import static org.junit.Assert.fail;
|
||||
|
||||
import dagger.BindsInstance;
|
||||
import dagger.Component;
|
||||
import google.registry.proxy.ProxyModule.PemBytes;
|
||||
import io.netty.handler.ssl.util.SelfSignedCertificate;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.OutputStreamWriter;
|
||||
import java.security.KeyPair;
|
||||
import java.security.PrivateKey;
|
||||
import java.security.cert.Certificate;
|
||||
import java.security.cert.X509Certificate;
|
||||
import javax.inject.Named;
|
||||
import javax.inject.Singleton;
|
||||
import org.bouncycastle.openssl.PEMWriter;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.junit.runners.JUnit4;
|
||||
|
||||
/** Unit tests for {@link CertificateModule}. */
|
||||
@RunWith(JUnit4.class)
|
||||
public class CertificateModuleTest {
|
||||
|
||||
private SelfSignedCertificate ssc;
|
||||
private PrivateKey key;
|
||||
private Certificate cert;
|
||||
private TestComponent component;
|
||||
|
||||
private static byte[] getPemBytes(Object... objects) throws Exception {
|
||||
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
|
||||
try (PEMWriter pemWriter =
|
||||
new PEMWriter(new OutputStreamWriter(byteArrayOutputStream, UTF_8))) {
|
||||
for (Object object : objects) {
|
||||
pemWriter.writeObject(object);
|
||||
}
|
||||
}
|
||||
return byteArrayOutputStream.toByteArray();
|
||||
}
|
||||
|
||||
/** Create a component with bindings to the given bytes[] as the contents from a PEM file. */
|
||||
private TestComponent createComponent(byte[] bytes) {
|
||||
return DaggerCertificateModuleTest_TestComponent.builder()
|
||||
.pemBytes(PemBytes.create(bytes))
|
||||
.build();
|
||||
}
|
||||
|
||||
@Before
|
||||
public void setUp() throws Exception {
|
||||
ssc = new SelfSignedCertificate();
|
||||
KeyPair keyPair = getKeyPair();
|
||||
key = keyPair.getPrivate();
|
||||
cert = signKeyPair(ssc, keyPair, "example.tld");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSuccess() throws Exception {
|
||||
byte[] pemBytes = getPemBytes(cert, ssc.cert(), key);
|
||||
component = createComponent(pemBytes);
|
||||
assertThat(component.privateKey()).isEqualTo(key);
|
||||
assertThat(component.certificates()).asList().containsExactly(cert, ssc.cert()).inOrder();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSuccess_certificateChainNotContinuous() throws Exception {
|
||||
byte[] pemBytes = getPemBytes(cert, key, ssc.cert());
|
||||
component = createComponent(pemBytes);
|
||||
assertThat(component.privateKey()).isEqualTo(key);
|
||||
assertThat(component.certificates()).asList().containsExactly(cert, ssc.cert()).inOrder();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFailure_noPrivateKey() throws Exception {
|
||||
byte[] pemBytes = getPemBytes(cert, ssc.cert());
|
||||
component = createComponent(pemBytes);
|
||||
try {
|
||||
component.privateKey();
|
||||
fail("Expect IllegalStateException.");
|
||||
} catch (IllegalStateException e) {
|
||||
assertThat(e).hasMessageThat().contains("0 keys are found");
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFailure_twoPrivateKeys() throws Exception {
|
||||
byte[] pemBytes = getPemBytes(cert, ssc.cert(), key, ssc.key());
|
||||
component = createComponent(pemBytes);
|
||||
try {
|
||||
component.privateKey();
|
||||
fail("Expect IllegalStateException.");
|
||||
} catch (IllegalStateException e) {
|
||||
assertThat(e).hasMessageThat().contains("2 keys are found");
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFailure_certificatesOutOfOrder() throws Exception {
|
||||
byte[] pemBytes = getPemBytes(ssc.cert(), cert, key);
|
||||
component = createComponent(pemBytes);
|
||||
try {
|
||||
component.certificates();
|
||||
fail("Expect IllegalStateException.");
|
||||
} catch (IllegalStateException e) {
|
||||
assertThat(e).hasMessageThat().contains("is not signed by");
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFailure_noCertificates() throws Exception {
|
||||
byte[] pemBytes = getPemBytes(key);
|
||||
component = createComponent(pemBytes);
|
||||
try {
|
||||
component.certificates();
|
||||
fail("Expect IllegalStateException.");
|
||||
} catch (IllegalStateException e) {
|
||||
assertThat(e).hasMessageThat().contains("No certificates");
|
||||
}
|
||||
}
|
||||
|
||||
@Singleton
|
||||
@Component(modules = {CertificateModule.class})
|
||||
interface TestComponent {
|
||||
|
||||
PrivateKey privateKey();
|
||||
|
||||
@Named("eppServerCertificates")
|
||||
X509Certificate[] certificates();
|
||||
|
||||
@Component.Builder
|
||||
interface Builder {
|
||||
|
||||
@BindsInstance
|
||||
Builder pemBytes(PemBytes pemBytes);
|
||||
|
||||
TestComponent build();
|
||||
}
|
||||
}
|
||||
}
|
251
javatests/google/registry/proxy/EppProtocolModuleTest.java
Normal file
251
javatests/google/registry/proxy/EppProtocolModuleTest.java
Normal file
|
@ -0,0 +1,251 @@
|
|||
// 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;
|
||||
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
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.ResourceUtils.readResourceBytes;
|
||||
import static google.registry.util.X509Utils.getCertificateHash;
|
||||
import static java.nio.charset.StandardCharsets.UTF_8;
|
||||
|
||||
import google.registry.testing.FakeClock;
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import io.netty.buffer.Unpooled;
|
||||
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 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;
|
||||
|
||||
/** End-to-end tests for {@link EppProtocolModule}. */
|
||||
@RunWith(JUnit4.class)
|
||||
public class EppProtocolModuleTest extends ProtocolModuleTest {
|
||||
|
||||
private static final int HEADER_LENGTH = 4;
|
||||
|
||||
private static final String CLIENT_ADDRESS = "epp.client.tld";
|
||||
|
||||
private static final byte[] HELLO_BYTES =
|
||||
("<?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")
|
||||
.getBytes(UTF_8);
|
||||
|
||||
private X509Certificate certificate;
|
||||
|
||||
public EppProtocolModuleTest() {
|
||||
super(TestComponent::eppHandlers);
|
||||
}
|
||||
|
||||
/** Verifies that the epp message content is represented by the buffers. */
|
||||
private static void assertBufferRepresentsContent(ByteBuf buffer, byte[] expectedContents) {
|
||||
// First make sure that buffer length is expected content length plus header length.
|
||||
assertThat(buffer.readableBytes()).isEqualTo(expectedContents.length + HEADER_LENGTH);
|
||||
// Then check if the header value is indeed expected content length plus header length.
|
||||
assertThat(buffer.readInt()).isEqualTo(expectedContents.length + HEADER_LENGTH);
|
||||
// Finally check the buffer contains the expected contents.
|
||||
byte[] actualContents = new byte[expectedContents.length];
|
||||
buffer.readBytes(actualContents);
|
||||
assertThat(actualContents).isEqualTo(expectedContents);
|
||||
}
|
||||
|
||||
/**
|
||||
* Read all available outbound frames and make a composite {@link ByteBuf} consisting all of them.
|
||||
*
|
||||
* <p>This is needed because {@link io.netty.handler.codec.LengthFieldPrepender} does not
|
||||
* necessary output only one {@link ByteBuf} from one input message. We need to reassemble the
|
||||
* frames together in order to obtain the processed message (prepended with length header).
|
||||
*/
|
||||
private static ByteBuf getAllOutboundFrames(EmbeddedChannel channel) {
|
||||
ByteBuf combinedBuffer = Unpooled.buffer();
|
||||
ByteBuf buffer;
|
||||
while ((buffer = channel.readOutbound()) != null) {
|
||||
combinedBuffer.writeBytes(buffer);
|
||||
}
|
||||
return combinedBuffer;
|
||||
}
|
||||
|
||||
/** Get a {@link ByteBuf} that represents the raw epp request with the given content. */
|
||||
private ByteBuf getByteBufFromContent(byte[] content) {
|
||||
ByteBuf buffer = Unpooled.buffer();
|
||||
buffer.writeInt(content.length + HEADER_LENGTH);
|
||||
buffer.writeBytes(content);
|
||||
return buffer;
|
||||
}
|
||||
|
||||
private FullHttpRequest makeEppHttpRequest(byte[] content, Cookie... cookies) {
|
||||
return TestUtils.makeEppHttpRequest(
|
||||
new String(content, UTF_8),
|
||||
PROXY_CONFIG.epp.relayHost,
|
||||
PROXY_CONFIG.epp.relayPath,
|
||||
TestModule.provideFakeAccessToken().get(),
|
||||
getCertificateHash(certificate),
|
||||
PROXY_CONFIG.epp.serverHostname,
|
||||
CLIENT_ADDRESS,
|
||||
cookies);
|
||||
}
|
||||
|
||||
private FullHttpResponse makeEppHttpResponse(byte[] content, Cookie... cookies) {
|
||||
return TestUtils.makeEppHttpResponse(
|
||||
new String(content, UTF_8), HttpResponseStatus.OK, cookies);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Before
|
||||
public void setUp() throws Exception {
|
||||
testComponent = makeTestComponent(new FakeClock());
|
||||
certificate = new SelfSignedCertificate().cert();
|
||||
initializeChannel(
|
||||
ch -> {
|
||||
ch.attr(REMOTE_ADDRESS_KEY).set(CLIENT_ADDRESS);
|
||||
ch.attr(CLIENT_CERTIFICATE_PROMISE_KEY).set(ch.eventLoop().newPromise());
|
||||
addAllTestableHandlers(ch);
|
||||
});
|
||||
Promise<X509Certificate> unusedPromise =
|
||||
channel.attr(CLIENT_CERTIFICATE_PROMISE_KEY).get().setSuccess(certificate);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSuccess_singleFrameInboundMessage() throws Exception {
|
||||
// First inbound message is hello.
|
||||
assertThat((FullHttpRequest) channel.readInbound()).isEqualTo(makeEppHttpRequest(HELLO_BYTES));
|
||||
|
||||
byte[] inputBytes = readResourceBytes(getClass(), "testdata/login.xml").read();
|
||||
|
||||
// Verify inbound message is as expected.
|
||||
assertThat(channel.writeInbound(getByteBufFromContent(inputBytes))).isTrue();
|
||||
assertThat((FullHttpRequest) channel.readInbound()).isEqualTo(makeEppHttpRequest(inputBytes));
|
||||
|
||||
// Nothing more to read.
|
||||
assertThat((Object) channel.readInbound()).isNull();
|
||||
assertThat(channel.isActive()).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSuccess_SingleFrame_MultipleInboundMessages() throws Exception {
|
||||
// First inbound message is hello.
|
||||
channel.readInbound();
|
||||
|
||||
byte[] inputBytes1 = readResourceBytes(getClass(), "testdata/login.xml").read();
|
||||
byte[] inputBytes2 = readResourceBytes(getClass(), "testdata/logout.xml").read();
|
||||
|
||||
// Verify inbound messages are as expected.
|
||||
assertThat(
|
||||
channel.writeInbound(
|
||||
Unpooled.wrappedBuffer(
|
||||
getByteBufFromContent(inputBytes1), getByteBufFromContent(inputBytes2))))
|
||||
.isTrue();
|
||||
assertThat((FullHttpRequest) channel.readInbound()).isEqualTo(makeEppHttpRequest(inputBytes1));
|
||||
assertThat((FullHttpRequest) channel.readInbound()).isEqualTo(makeEppHttpRequest(inputBytes2));
|
||||
|
||||
// Nothing more to read.
|
||||
assertThat((Object) channel.readInbound()).isNull();
|
||||
assertThat(channel.isActive()).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSuccess_MultipleFrames_MultipleInboundMessages() throws Exception {
|
||||
// First inbound message is hello.
|
||||
channel.readInbound();
|
||||
|
||||
byte[] inputBytes1 = readResourceBytes(getClass(), "testdata/login.xml").read();
|
||||
byte[] inputBytes2 = readResourceBytes(getClass(), "testdata/logout.xml").read();
|
||||
ByteBuf inputBuffer =
|
||||
Unpooled.wrappedBuffer(
|
||||
getByteBufFromContent(inputBytes1), getByteBufFromContent(inputBytes2));
|
||||
|
||||
// The first frame does not contain the entire first message because it is missing 4 byte of
|
||||
// header length.
|
||||
assertThat(channel.writeInbound(inputBuffer.readBytes(inputBytes1.length))).isFalse();
|
||||
|
||||
// The second frame contains the first message, and part of the second message.
|
||||
assertThat(channel.writeInbound(inputBuffer.readBytes(inputBytes2.length))).isTrue();
|
||||
assertThat((FullHttpRequest) channel.readInbound()).isEqualTo(makeEppHttpRequest(inputBytes1));
|
||||
|
||||
// The third frame contains the rest of the second message.
|
||||
assertThat(channel.writeInbound(inputBuffer)).isTrue();
|
||||
assertThat((FullHttpRequest) channel.readInbound()).isEqualTo(makeEppHttpRequest(inputBytes2));
|
||||
|
||||
// Nothing more to read.
|
||||
assertThat((Object) channel.readInbound()).isNull();
|
||||
assertThat(channel.isActive()).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSuccess_simpleOutboundMessage() throws Exception {
|
||||
// First inbound message is hello.
|
||||
channel.readInbound();
|
||||
|
||||
byte[] outputBytes = readResourceBytes(getClass(), "testdata/login_response.xml").read();
|
||||
|
||||
// Verify outbound message is as expected.
|
||||
assertThat(channel.writeOutbound(makeEppHttpResponse(outputBytes))).isTrue();
|
||||
assertBufferRepresentsContent(getAllOutboundFrames(channel), outputBytes);
|
||||
|
||||
// Nothing more to write.
|
||||
assertThat((Object) channel.readOutbound()).isNull();
|
||||
assertThat(channel.isActive()).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSuccess_setAndReadCookies() throws Exception {
|
||||
// First inbound message is hello.
|
||||
channel.readInbound();
|
||||
|
||||
byte[] outputBytes1 = readResourceBytes(getClass(), "testdata/login_response.xml").read();
|
||||
Cookie cookie1 = new DefaultCookie("name1", "value1");
|
||||
Cookie cookie2 = new DefaultCookie("name2", "value2");
|
||||
|
||||
// Verify outbound message is as expected.
|
||||
assertThat(channel.writeOutbound(makeEppHttpResponse(outputBytes1, cookie1, cookie2))).isTrue();
|
||||
assertBufferRepresentsContent(getAllOutboundFrames(channel), outputBytes1);
|
||||
|
||||
// Verify inbound message contains cookies.
|
||||
byte[] inputBytes1 = readResourceBytes(getClass(), "testdata/logout.xml").read();
|
||||
assertThat(channel.writeInbound(getByteBufFromContent(inputBytes1))).isTrue();
|
||||
assertThat((FullHttpRequest) channel.readInbound())
|
||||
.isEqualTo(makeEppHttpRequest(inputBytes1, cookie1, cookie2));
|
||||
|
||||
// Second outbound message change cookies.
|
||||
byte[] outputBytes2 = readResourceBytes(getClass(), "testdata/logout_response.xml").read();
|
||||
Cookie cookie3 = new DefaultCookie("name3", "value3");
|
||||
cookie2 = new DefaultCookie("name2", "newValue2");
|
||||
|
||||
// Verify outbound message is as expected.
|
||||
assertThat(channel.writeOutbound(makeEppHttpResponse(outputBytes2, cookie2, cookie3))).isTrue();
|
||||
assertBufferRepresentsContent(getAllOutboundFrames(channel), outputBytes2);
|
||||
|
||||
// Verify inbound message contains updated cookies.
|
||||
byte[] inputBytes2 = readResourceBytes(getClass(), "testdata/login.xml").read();
|
||||
assertThat(channel.writeInbound(getByteBufFromContent(inputBytes2))).isTrue();
|
||||
assertThat((FullHttpRequest) channel.readInbound())
|
||||
.isEqualTo(makeEppHttpRequest(inputBytes2, cookie1, cookie2, cookie3));
|
||||
|
||||
// Nothing more to write or read.
|
||||
assertThat((Object) channel.readOutbound()).isNull();
|
||||
assertThat((Object) channel.readInbound()).isNull();
|
||||
assertThat(channel.isActive()).isTrue();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,88 @@
|
|||
// 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;
|
||||
|
||||
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 org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.junit.runners.JUnit4;
|
||||
|
||||
/** End-to-end tests for {@link HealthCheckProtocolModule}. */
|
||||
@RunWith(JUnit4.class)
|
||||
public class HealthCheckProtocolModuleTest extends ProtocolModuleTest {
|
||||
|
||||
public HealthCheckProtocolModuleTest() {
|
||||
super(TestComponent::healthCheckHandlers);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSuccess_expectedInboundMessage() {
|
||||
// no inbound message passed along.
|
||||
assertThat(
|
||||
channel.writeInbound(
|
||||
Unpooled.wrappedBuffer(PROXY_CONFIG.healthCheck.checkRequest.getBytes(US_ASCII))))
|
||||
.isFalse();
|
||||
ByteBuf outputBuffer = channel.readOutbound();
|
||||
// response written to channel.
|
||||
assertThat(outputBuffer.toString(US_ASCII)).isEqualTo(PROXY_CONFIG.healthCheck.checkResponse);
|
||||
assertThat(channel.isActive()).isTrue();
|
||||
// nothing more to write.
|
||||
assertThat((Object) channel.readOutbound()).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSuccess_InboundMessageTooShort() {
|
||||
String shortRequest = "HEALTH_CHECK";
|
||||
// no inbound message passed along.
|
||||
assertThat(channel.writeInbound(Unpooled.wrappedBuffer(shortRequest.getBytes(US_ASCII))))
|
||||
.isFalse();
|
||||
// nothing to write.
|
||||
assertThat(channel.isActive()).isTrue();
|
||||
assertThat((Object) channel.readOutbound()).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSuccess_InboundMessageTooLong() {
|
||||
String longRequest = "HEALTH_CHECK_REQUEST HELLO";
|
||||
// no inbound message passed along.
|
||||
assertThat(channel.writeInbound(Unpooled.wrappedBuffer(longRequest.getBytes(US_ASCII))))
|
||||
.isFalse();
|
||||
ByteBuf outputBuffer = channel.readOutbound();
|
||||
// The fixed length frame decoder will decode the first inbound message as "HEALTH_CHECK_
|
||||
// REQUEST", which is what this handler expects. So it will respond with the pre-defined
|
||||
// response message. This is an acceptable false-positive because the GCP health checker will
|
||||
// only send the pre-defined request message. As long as the health check can receive the
|
||||
// request it expects, we do not care if the protocol also respond to other requests.
|
||||
assertThat(outputBuffer.toString(US_ASCII)).isEqualTo(PROXY_CONFIG.healthCheck.checkResponse);
|
||||
assertThat(channel.isActive()).isTrue();
|
||||
// nothing more to write.
|
||||
assertThat((Object) channel.readOutbound()).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSuccess_InboundMessageNotMatch() {
|
||||
String invalidRequest = "HEALTH_CHECK_REQUESX";
|
||||
// no inbound message passed along.
|
||||
assertThat(channel.writeInbound(Unpooled.wrappedBuffer(invalidRequest.getBytes(US_ASCII))))
|
||||
.isFalse();
|
||||
// nothing to write.
|
||||
assertThat(channel.isActive()).isTrue();
|
||||
assertThat((Object) channel.readOutbound()).isNull();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,103 @@
|
|||
// 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;
|
||||
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
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 io.netty.buffer.ByteBuf;
|
||||
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.HttpObjectAggregator;
|
||||
import io.netty.handler.codec.http.HttpResponseStatus;
|
||||
import io.netty.handler.codec.http.HttpServerCodec;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.junit.runners.JUnit4;
|
||||
|
||||
/**
|
||||
* End-to-end tests for {@link HttpsRelayProtocolModule}.
|
||||
*
|
||||
* <p>This protocol defines a connection in which the proxy behaves as a standard http client (sans
|
||||
* the relay operation which is excluded in end-to-end testing). Because non user-defined handlers
|
||||
* are used, the tests here focus on verifying that the request written to the network socket by the
|
||||
* client is reconstructed faithfully by a server, and vice versa, that the response the client
|
||||
* decoded from incoming bytes is equivalent to the response sent by the server.
|
||||
*
|
||||
* <p>These tests only ensure that the client represented by this protocol is compatible with a
|
||||
* server implementation provided by Netty itself. They test the self-consistency of various Netty
|
||||
* handlers that deal with HTTP protocol, but not whether the handlers converts between bytes and
|
||||
* HTTP messages correctly, which is presumed correct.
|
||||
*/
|
||||
@RunWith(JUnit4.class)
|
||||
public class HttpsRelayProtocolModuleTest extends ProtocolModuleTest {
|
||||
|
||||
private static final String HOST = "test.tld";
|
||||
private static final String PATH = "/path/to/test";
|
||||
private static final String CONTENT = "content to test\nnext line\n";
|
||||
|
||||
private final EmbeddedChannel serverChannel =
|
||||
new EmbeddedChannel(new HttpServerCodec(), new HttpObjectAggregator(512 * 1024));
|
||||
|
||||
public HttpsRelayProtocolModuleTest() {
|
||||
super(TestComponent::httpsRelayHandlers);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests that the client converts given {@link FullHttpRequest} to bytes, which is sent to the
|
||||
* server and reconstructed to a {@link FullHttpRequest} that is equivalent to the original. Then
|
||||
* test that the server converts given {@link FullHttpResponse} to bytes, which is sent to the
|
||||
* client and reconstructed to a {@link FullHttpResponse} that is equivalent to the original.
|
||||
*
|
||||
* <p>The request and response equivalences are tested in the same method because the client codec
|
||||
* tries to pair the response it receives with the request it sends. Receiving a response without
|
||||
* sending a request first will cause the {@link HttpObjectAggregator} to fail to aggregate
|
||||
* properly.
|
||||
*/
|
||||
private void requestAndRespondWithStatus(HttpResponseStatus status) {
|
||||
ByteBuf buffer;
|
||||
FullHttpRequest requestSent = makeHttpPostRequest(CONTENT, HOST, PATH);
|
||||
// Need to send a copy as the content read index will advance after the request is written to
|
||||
// the outbound of client channel, making comparison with requestReceived fail.
|
||||
assertThat(channel.writeOutbound(requestSent.copy())).isTrue();
|
||||
buffer = channel.readOutbound();
|
||||
assertThat(serverChannel.writeInbound(buffer)).isTrue();
|
||||
FullHttpRequest requestReceived = serverChannel.readInbound();
|
||||
// Verify that the request received is the same as the request sent.
|
||||
assertHttpRequestEquivalent(requestSent, requestReceived);
|
||||
|
||||
FullHttpResponse responseSent = makeHttpResponse(CONTENT, status);
|
||||
assertThat(serverChannel.writeOutbound(responseSent.copy())).isTrue();
|
||||
buffer = serverChannel.readOutbound();
|
||||
assertThat(channel.writeInbound(buffer)).isTrue();
|
||||
FullHttpResponse responseReceived = channel.readInbound();
|
||||
// Verify that the request received is the same as the request sent.
|
||||
assertHttpResponseEquivalent(responseSent, responseReceived);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSuccess_OkResponse() {
|
||||
requestAndRespondWithStatus(HttpResponseStatus.OK);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSuccess_NonOkResponse() {
|
||||
requestAndRespondWithStatus(HttpResponseStatus.BAD_REQUEST);
|
||||
}
|
||||
}
|
269
javatests/google/registry/proxy/ProtocolModuleTest.java
Normal file
269
javatests/google/registry/proxy/ProtocolModuleTest.java
Normal file
|
@ -0,0 +1,269 @@
|
|||
// 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;
|
||||
|
||||
import static com.google.common.collect.ImmutableList.toImmutableList;
|
||||
import static google.registry.proxy.ProxyConfig.Environment.TEST;
|
||||
import static google.registry.proxy.ProxyConfig.getProxyConfig;
|
||||
|
||||
import com.google.common.base.Supplier;
|
||||
import com.google.common.base.Suppliers;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import dagger.Component;
|
||||
import dagger.Module;
|
||||
import dagger.Provides;
|
||||
import google.registry.proxy.EppProtocolModule.EppProtocol;
|
||||
import google.registry.proxy.HealthCheckProtocolModule.HealthCheckProtocol;
|
||||
import google.registry.proxy.HttpsRelayProtocolModule.HttpsRelayProtocol;
|
||||
import google.registry.proxy.WhoisProtocolModule.WhoisProtocol;
|
||||
import google.registry.proxy.handler.BackendMetricsHandler;
|
||||
import google.registry.proxy.handler.ProxyProtocolHandler;
|
||||
import google.registry.proxy.handler.RelayHandler.FullHttpRequestRelayHandler;
|
||||
import google.registry.proxy.handler.RelayHandler.FullHttpResponseRelayHandler;
|
||||
import google.registry.proxy.handler.SslClientInitializer;
|
||||
import google.registry.proxy.handler.SslServerInitializer;
|
||||
import google.registry.testing.FakeClock;
|
||||
import google.registry.util.Clock;
|
||||
import io.netty.channel.Channel;
|
||||
import io.netty.channel.ChannelHandler;
|
||||
import io.netty.channel.ChannelInitializer;
|
||||
import io.netty.channel.embedded.EmbeddedChannel;
|
||||
import io.netty.handler.logging.LoggingHandler;
|
||||
import io.netty.handler.ssl.SslProvider;
|
||||
import io.netty.handler.ssl.util.SelfSignedCertificate;
|
||||
import io.netty.handler.timeout.ReadTimeoutHandler;
|
||||
import java.security.PrivateKey;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Function;
|
||||
import javax.inject.Named;
|
||||
import javax.inject.Provider;
|
||||
import javax.inject.Singleton;
|
||||
import org.junit.Before;
|
||||
|
||||
/**
|
||||
* Base class for end-to-end tests of a {@link Protocol}.
|
||||
*
|
||||
* <p>The end-to-end tests ensures that the business logic that a {@link Protocol} defines are
|
||||
* correctly performed by various handlers attached to its pipeline. Non-business essential handlers
|
||||
* should be excluded.
|
||||
*
|
||||
* <p>Subclass should implement an no-arg constructor that calls constructors of this class,
|
||||
* providing the method reference of the {@link TestComponent} method to call to obtain the list of
|
||||
* {@link ChannelHandler} providers for the {@link Protocol} to test, and optionally a set of {@link
|
||||
* ChannelHandler} classes to exclude from testing.
|
||||
*/
|
||||
public abstract class ProtocolModuleTest {
|
||||
|
||||
protected static final ProxyConfig PROXY_CONFIG = getProxyConfig(TEST);
|
||||
|
||||
protected TestComponent testComponent;
|
||||
|
||||
/**
|
||||
* Default list of handler classes that are not of interest in end-to-end testing of the {@link
|
||||
* Protocol}.
|
||||
*/
|
||||
private static final ImmutableSet<Class<? extends ChannelHandler>> DEFAULT_EXCLUDED_HANDLERS =
|
||||
ImmutableSet.of(
|
||||
// The PROXY protocol is only used when the proxy is behind the GCP load balancer. It is
|
||||
// not part of any business logic.
|
||||
ProxyProtocolHandler.class,
|
||||
// SSL is part of the business logic for some protocol (EPP for example), but its
|
||||
// impact is isolated. Including it makes tests much more complicated. It should be tested
|
||||
// separately in its own unit tests.
|
||||
SslClientInitializer.class,
|
||||
SslServerInitializer.class,
|
||||
// These two handlers provide essential functionalities for the proxy to operate, but they
|
||||
// do not directly implement the business logic of a well-defined protocol. They should be
|
||||
// tested separately in their respective unit tests.
|
||||
FullHttpRequestRelayHandler.class,
|
||||
FullHttpResponseRelayHandler.class,
|
||||
// The rest are not part of business logic and do not need to be tested, obviously.
|
||||
LoggingHandler.class,
|
||||
// Metrics instrumentation is tested separately.
|
||||
BackendMetricsHandler.class,
|
||||
ReadTimeoutHandler.class);
|
||||
|
||||
protected EmbeddedChannel channel;
|
||||
|
||||
/**
|
||||
* Method reference to the component method that exposes the list of handler providers for the
|
||||
* specific {@link Protocol} in interest.
|
||||
*/
|
||||
protected final Function<TestComponent, ImmutableList<Provider<? extends ChannelHandler>>>
|
||||
handlerProvidersMethod;
|
||||
|
||||
protected final ImmutableSet<Class<? extends ChannelHandler>> excludedHandlers;
|
||||
|
||||
protected ProtocolModuleTest(
|
||||
Function<TestComponent, ImmutableList<Provider<? extends ChannelHandler>>>
|
||||
handlerProvidersMethod,
|
||||
ImmutableSet<Class<? extends ChannelHandler>> excludedHandlers) {
|
||||
this.handlerProvidersMethod = handlerProvidersMethod;
|
||||
this.excludedHandlers = excludedHandlers;
|
||||
}
|
||||
|
||||
protected ProtocolModuleTest(
|
||||
Function<TestComponent, ImmutableList<Provider<? extends ChannelHandler>>>
|
||||
handlerProvidersMethod) {
|
||||
this(handlerProvidersMethod, DEFAULT_EXCLUDED_HANDLERS);
|
||||
}
|
||||
|
||||
/** Excludes handler providers that are not of interested for testing. */
|
||||
private ImmutableList<Provider<? extends ChannelHandler>> excludeHandlerProvidersForTesting(
|
||||
ImmutableList<Provider<? extends ChannelHandler>> handlerProviders) {
|
||||
return handlerProviders
|
||||
.stream()
|
||||
.filter(handlerProvider -> !excludedHandlers.contains(handlerProvider.get().getClass()))
|
||||
.collect(toImmutableList());
|
||||
}
|
||||
|
||||
protected void initializeChannel(Consumer<Channel> initializer) {
|
||||
channel =
|
||||
new EmbeddedChannel(
|
||||
new ChannelInitializer<Channel>() {
|
||||
@Override
|
||||
protected void initChannel(Channel ch) throws Exception {
|
||||
initializer.accept(ch);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/** Adds handlers to the channel pipeline, excluding any one in {@link #excludedHandlers}. */
|
||||
void addAllTestableHandlers(Channel ch) {
|
||||
for (Provider<? extends ChannelHandler> handlerProvider :
|
||||
excludeHandlerProvidersForTesting(handlerProvidersMethod.apply(testComponent))) {
|
||||
ch.pipeline().addLast(handlerProvider.get());
|
||||
}
|
||||
}
|
||||
|
||||
static TestComponent makeTestComponent(FakeClock fakeClock) {
|
||||
return DaggerProtocolModuleTest_TestComponent.builder()
|
||||
.testModule(new TestModule(new FakeClock()))
|
||||
.build();
|
||||
}
|
||||
|
||||
@Before
|
||||
public void setUp() throws Exception {
|
||||
testComponent = makeTestComponent(new FakeClock());
|
||||
initializeChannel(this::addAllTestableHandlers);
|
||||
}
|
||||
|
||||
/**
|
||||
* Component used to obtain the list of {@link ChannelHandler} providers for each {@link
|
||||
* Protocol}.
|
||||
*/
|
||||
@Singleton
|
||||
@Component(
|
||||
modules = {
|
||||
TestModule.class,
|
||||
WhoisProtocolModule.class,
|
||||
EppProtocolModule.class,
|
||||
HealthCheckProtocolModule.class,
|
||||
HttpsRelayProtocolModule.class
|
||||
}
|
||||
)
|
||||
interface TestComponent {
|
||||
@WhoisProtocol
|
||||
ImmutableList<Provider<? extends ChannelHandler>> whoisHandlers();
|
||||
|
||||
@EppProtocol
|
||||
ImmutableList<Provider<? extends ChannelHandler>> eppHandlers();
|
||||
|
||||
@HealthCheckProtocol
|
||||
ImmutableList<Provider<? extends ChannelHandler>> healthCheckHandlers();
|
||||
|
||||
@HttpsRelayProtocol
|
||||
ImmutableList<Provider<? extends ChannelHandler>> httpsRelayHandlers();
|
||||
}
|
||||
|
||||
/**
|
||||
* Module that provides bindings used in tests.
|
||||
*
|
||||
* <p>Most of the binding provided in this module should be either a fake, or a {@link
|
||||
* ChannelHandler} that is excluded, and annotated with {@code @Singleton}. This module acts as a
|
||||
* replacement for {@link ProxyModule} used in production component. Providing a handler that is
|
||||
* part of the business logic of a {@link Protocol} from this module is a sign that the binding
|
||||
* should be provided in the respective {@code ProtocolModule} instead.
|
||||
*/
|
||||
@Module
|
||||
static class TestModule {
|
||||
|
||||
/**
|
||||
* A fake clock that is explicitly provided. Users can construct a module with a controller
|
||||
* clock.
|
||||
*/
|
||||
private final FakeClock fakeClock;
|
||||
|
||||
TestModule(FakeClock fakeClock) {
|
||||
this.fakeClock = fakeClock;
|
||||
}
|
||||
|
||||
@Singleton
|
||||
@Provides
|
||||
static ProxyConfig provideProxyConfig() {
|
||||
return getProxyConfig(TEST);
|
||||
}
|
||||
|
||||
@Singleton
|
||||
@Provides
|
||||
static SslProvider provideSslProvider() {
|
||||
return SslProvider.JDK;
|
||||
}
|
||||
|
||||
@Singleton
|
||||
@Provides
|
||||
@Named("accessToken")
|
||||
static Supplier<String> provideFakeAccessToken() {
|
||||
return Suppliers.ofInstance("fake.test.token");
|
||||
}
|
||||
|
||||
@Singleton
|
||||
@Provides
|
||||
static LoggingHandler provideLoggingHandler() {
|
||||
return new LoggingHandler();
|
||||
}
|
||||
|
||||
@Singleton
|
||||
@Provides
|
||||
static SelfSignedCertificate provideSelfSignedCertificate() {
|
||||
try {
|
||||
return new SelfSignedCertificate();
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Singleton
|
||||
@Provides
|
||||
@Named("eppServerCertificates")
|
||||
static X509Certificate[] provideCertificate(SelfSignedCertificate ssc) {
|
||||
return new X509Certificate[] {ssc.cert()};
|
||||
}
|
||||
|
||||
@Singleton
|
||||
@Provides
|
||||
static PrivateKey providePrivateKey(SelfSignedCertificate ssc) {
|
||||
return ssc.key();
|
||||
}
|
||||
|
||||
@Singleton
|
||||
@Provides
|
||||
Clock provideFakeClock() {
|
||||
return fakeClock;
|
||||
}
|
||||
}
|
||||
}
|
105
javatests/google/registry/proxy/ProxyModuleTest.java
Normal file
105
javatests/google/registry/proxy/ProxyModuleTest.java
Normal file
|
@ -0,0 +1,105 @@
|
|||
// 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;
|
||||
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
import static google.registry.proxy.ProxyConfig.Environment.TEST;
|
||||
import static google.registry.proxy.ProxyConfig.getProxyConfig;
|
||||
import static google.registry.testing.JUnitBackports.expectThrows;
|
||||
import static org.junit.Assert.fail;
|
||||
|
||||
import com.beust.jcommander.ParameterException;
|
||||
import google.registry.proxy.ProxyConfig.Environment;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.junit.runners.JUnit4;
|
||||
|
||||
/** Unit tests for {@link ProxyModule}. */
|
||||
@RunWith(JUnit4.class)
|
||||
public class ProxyModuleTest {
|
||||
|
||||
private static final ProxyConfig PROXY_CONFIG = getProxyConfig(TEST);
|
||||
private final ProxyModule proxyModule = new ProxyModule();
|
||||
|
||||
@Test
|
||||
public void testSuccess_parseArgs_defaultArgs() {
|
||||
String[] args = {};
|
||||
proxyModule.parse(args);
|
||||
assertThat(proxyModule.provideWhoisPort(PROXY_CONFIG)).isEqualTo(PROXY_CONFIG.whois.port);
|
||||
assertThat(proxyModule.provideEppPort(PROXY_CONFIG)).isEqualTo(PROXY_CONFIG.epp.port);
|
||||
assertThat(proxyModule.provideHealthCheckPort(PROXY_CONFIG))
|
||||
.isEqualTo(PROXY_CONFIG.healthCheck.port);
|
||||
assertThat(proxyModule.provideEnvironment()).isEqualTo(Environment.LOCAL);
|
||||
assertThat(proxyModule.log).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFailure_parseArgs_wrongArguments() {
|
||||
String[] args = {"--wrong_flag", "some_value"};
|
||||
try {
|
||||
proxyModule.parse(args);
|
||||
fail("Expected ParameterException.");
|
||||
} catch (ParameterException e) {
|
||||
assertThat(e).hasMessageThat().contains("--wrong_flag");
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSuccess_parseArgs_log() {
|
||||
String[] args = {"--log"};
|
||||
proxyModule.parse(args);
|
||||
assertThat(proxyModule.log).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSuccess_parseArgs_customWhoisPort() {
|
||||
String[] args = {"--whois", "12345"};
|
||||
proxyModule.parse(args);
|
||||
assertThat(proxyModule.provideWhoisPort(PROXY_CONFIG)).isEqualTo(12345);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSuccess_parseArgs_customEppPort() {
|
||||
String[] args = {"--epp", "22222"};
|
||||
proxyModule.parse(args);
|
||||
assertThat(proxyModule.provideEppPort(PROXY_CONFIG)).isEqualTo(22222);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSuccess_parseArgs_customHealthCheckPort() {
|
||||
String[] args = {"--health_check", "23456"};
|
||||
proxyModule.parse(args);
|
||||
assertThat(proxyModule.provideHealthCheckPort(PROXY_CONFIG)).isEqualTo(23456);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSuccess_parseArgs_customEnvironment() {
|
||||
String[] args = {"--env", "ALpHa"};
|
||||
proxyModule.parse(args);
|
||||
assertThat(proxyModule.provideEnvironment()).isEqualTo(Environment.ALPHA);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFailure_parseArgs_wrongEnvironment() {
|
||||
ParameterException e =
|
||||
expectThrows(
|
||||
ParameterException.class,
|
||||
() -> {
|
||||
String[] args = {"--env", "beta"};
|
||||
proxyModule.parse(args);
|
||||
});
|
||||
assertThat(e).hasMessageThat().contains("Invalid value for --env parameter");
|
||||
}
|
||||
}
|
139
javatests/google/registry/proxy/TestUtils.java
Normal file
139
javatests/google/registry/proxy/TestUtils.java
Normal file
|
@ -0,0 +1,139 @@
|
|||
// 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;
|
||||
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
import static google.registry.proxy.handler.EppServiceHandler.EPP_CONTENT_TYPE;
|
||||
import static google.registry.proxy.handler.EppServiceHandler.FORWARDED_FOR_FIELD;
|
||||
import static google.registry.proxy.handler.EppServiceHandler.REQUESTED_SERVERNAME_VIA_SNI_FIELD;
|
||||
import static google.registry.proxy.handler.EppServiceHandler.SSL_CLIENT_CERTIFICATE_HASH_FIELD;
|
||||
import static java.nio.charset.StandardCharsets.US_ASCII;
|
||||
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import io.netty.buffer.Unpooled;
|
||||
import io.netty.handler.codec.http.DefaultFullHttpRequest;
|
||||
import io.netty.handler.codec.http.DefaultFullHttpResponse;
|
||||
import io.netty.handler.codec.http.FullHttpMessage;
|
||||
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.HttpHeaderValues;
|
||||
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.ClientCookieEncoder;
|
||||
import io.netty.handler.codec.http.cookie.Cookie;
|
||||
import io.netty.handler.codec.http.cookie.ServerCookieEncoder;
|
||||
|
||||
/** Utility class for various helper methods used in testing. */
|
||||
public class TestUtils {
|
||||
|
||||
public static FullHttpRequest makeHttpPostRequest(String content, String host, String path) {
|
||||
ByteBuf buf = Unpooled.wrappedBuffer(content.getBytes(US_ASCII));
|
||||
FullHttpRequest request =
|
||||
new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.POST, path, buf);
|
||||
request
|
||||
.headers()
|
||||
.set(HttpHeaderNames.USER_AGENT, "Proxy")
|
||||
.set(HttpHeaderNames.HOST, host)
|
||||
.set(HttpHeaderNames.CONTENT_LENGTH, buf.readableBytes());
|
||||
return request;
|
||||
}
|
||||
|
||||
public static FullHttpResponse makeHttpResponse(String content, HttpResponseStatus status) {
|
||||
ByteBuf buf = Unpooled.wrappedBuffer(content.getBytes(US_ASCII));
|
||||
FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, status, buf);
|
||||
response.headers().set(HttpHeaderNames.CONTENT_LENGTH, buf.readableBytes());
|
||||
return response;
|
||||
}
|
||||
|
||||
public static FullHttpRequest makeWhoisHttpRequest(
|
||||
String content, String host, String path, String accessToken) {
|
||||
FullHttpRequest request = makeHttpPostRequest(content, host, path);
|
||||
request
|
||||
.headers()
|
||||
.set(HttpHeaderNames.CONNECTION, HttpHeaderValues.CLOSE)
|
||||
.set(HttpHeaderNames.AUTHORIZATION, "Bearer " + accessToken)
|
||||
.set(HttpHeaderNames.CONTENT_TYPE, HttpHeaderValues.TEXT_PLAIN)
|
||||
.set(HttpHeaderNames.ACCEPT, HttpHeaderValues.TEXT_PLAIN);
|
||||
return request;
|
||||
}
|
||||
|
||||
public static FullHttpRequest makeEppHttpRequest(
|
||||
String content,
|
||||
String host,
|
||||
String path,
|
||||
String accessToken,
|
||||
String sslClientCertificateHash,
|
||||
String serverHostname,
|
||||
String clientAddress,
|
||||
Cookie... cookies) {
|
||||
FullHttpRequest request = makeHttpPostRequest(content, host, path);
|
||||
request
|
||||
.headers()
|
||||
.set(HttpHeaderNames.AUTHORIZATION, "Bearer " + accessToken)
|
||||
.set(HttpHeaderNames.CONTENT_TYPE, EPP_CONTENT_TYPE)
|
||||
.set(HttpHeaderNames.ACCEPT, EPP_CONTENT_TYPE)
|
||||
.set(SSL_CLIENT_CERTIFICATE_HASH_FIELD, sslClientCertificateHash)
|
||||
.set(REQUESTED_SERVERNAME_VIA_SNI_FIELD, serverHostname)
|
||||
.set(FORWARDED_FOR_FIELD, clientAddress);
|
||||
if (cookies.length != 0) {
|
||||
request.headers().set(HttpHeaderNames.COOKIE, ClientCookieEncoder.STRICT.encode(cookies));
|
||||
}
|
||||
return request;
|
||||
}
|
||||
|
||||
public static FullHttpResponse makeWhoisHttpResponse(String content, HttpResponseStatus status) {
|
||||
FullHttpResponse response = makeHttpResponse(content, status);
|
||||
response.headers().set(HttpHeaderNames.CONTENT_TYPE, HttpHeaderValues.TEXT_PLAIN);
|
||||
return response;
|
||||
}
|
||||
|
||||
public static FullHttpResponse makeEppHttpResponse(
|
||||
String content, HttpResponseStatus status, Cookie... cookies) {
|
||||
FullHttpResponse response = makeHttpResponse(content, status);
|
||||
response.headers().set(HttpHeaderNames.CONTENT_TYPE, EPP_CONTENT_TYPE);
|
||||
for (Cookie cookie : cookies) {
|
||||
response.headers().add(HttpHeaderNames.SET_COOKIE, ServerCookieEncoder.STRICT.encode(cookie));
|
||||
}
|
||||
return response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compares two {@link FullHttpMessage} for equivalency.
|
||||
*
|
||||
* <p>This method is needed because an HTTP message decoded and aggregated from inbound {@link
|
||||
* ByteBuf} is of a different class than the one written to the outbound {@link ByteBuf}, and The
|
||||
* {@link ByteBuf} implementations that hold the content of the HTTP messages are different, even
|
||||
* though the actual content, headers, etc are the same.
|
||||
*
|
||||
* <p>This method is not type-safe, msg1 & msg2 can be a request and a response, respectively. Do
|
||||
* not use this method directly.
|
||||
*/
|
||||
private static void assertHttpMessageEquivalent(FullHttpMessage msg1, FullHttpMessage msg2) {
|
||||
assertThat(msg1.protocolVersion()).isEqualTo(msg2.protocolVersion());
|
||||
assertThat(msg1.content()).isEqualTo(msg2.content());
|
||||
assertThat(msg1.headers()).isEqualTo(msg2.headers());
|
||||
}
|
||||
|
||||
public static void assertHttpResponseEquivalent(FullHttpResponse res1, FullHttpResponse res2) {
|
||||
assertThat(res1.status()).isEqualTo(res2.status());
|
||||
assertHttpMessageEquivalent(res1, res2);
|
||||
}
|
||||
|
||||
public static void assertHttpRequestEquivalent(FullHttpRequest req1, FullHttpRequest req2) {
|
||||
assertHttpMessageEquivalent(req1, req2);
|
||||
}
|
||||
}
|
165
javatests/google/registry/proxy/WhoisProtocolModuleTest.java
Normal file
165
javatests/google/registry/proxy/WhoisProtocolModuleTest.java
Normal file
|
@ -0,0 +1,165 @@
|
|||
// 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;
|
||||
|
||||
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 java.util.stream.Collectors.joining;
|
||||
import static org.junit.Assert.fail;
|
||||
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import io.netty.buffer.Unpooled;
|
||||
import io.netty.handler.codec.http.FullHttpRequest;
|
||||
import io.netty.handler.codec.http.FullHttpResponse;
|
||||
import io.netty.handler.codec.http.HttpResponseStatus;
|
||||
import java.util.stream.Stream;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.junit.runners.JUnit4;
|
||||
|
||||
/** End-to-end tests for {@link WhoisProtocolModule}. */
|
||||
@RunWith(JUnit4.class)
|
||||
public class WhoisProtocolModuleTest extends ProtocolModuleTest {
|
||||
|
||||
public WhoisProtocolModuleTest() {
|
||||
super(TestComponent::whoisHandlers);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSuccess_singleFrameInboundMessage() {
|
||||
String inputString = "test.tld\r\n";
|
||||
// Inbound message processed and passed along.
|
||||
assertThat(channel.writeInbound(Unpooled.wrappedBuffer(inputString.getBytes(US_ASCII))))
|
||||
.isTrue();
|
||||
|
||||
FullHttpRequest actualRequest = channel.readInbound();
|
||||
FullHttpRequest expectedRequest =
|
||||
makeWhoisHttpRequest(
|
||||
"test.tld",
|
||||
PROXY_CONFIG.whois.relayHost,
|
||||
PROXY_CONFIG.whois.relayPath,
|
||||
TestModule.provideFakeAccessToken().get());
|
||||
assertThat(expectedRequest).isEqualTo(actualRequest);
|
||||
assertThat(channel.isActive()).isTrue();
|
||||
// Nothing more to read.
|
||||
assertThat((Object) channel.readInbound()).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSuccess_noNewlineInboundMessage() {
|
||||
String inputString = "test.tld";
|
||||
// No newline encountered, no message formed.
|
||||
assertThat(channel.writeInbound(Unpooled.wrappedBuffer(inputString.getBytes(US_ASCII))))
|
||||
.isFalse();
|
||||
assertThat(channel.isActive()).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSuccess_multiFrameInboundMessage() {
|
||||
String frame1 = "test";
|
||||
String frame2 = "1.tld";
|
||||
String frame3 = "\r\nte";
|
||||
String frame4 = "st2.tld\r";
|
||||
String frame5 = "\ntest3.tld";
|
||||
// No newline yet.
|
||||
assertThat(channel.writeInbound(Unpooled.wrappedBuffer(frame1.getBytes(US_ASCII)))).isFalse();
|
||||
// Still no newline yet.
|
||||
assertThat(channel.writeInbound(Unpooled.wrappedBuffer(frame2.getBytes(US_ASCII)))).isFalse();
|
||||
// First newline encountered.
|
||||
assertThat(channel.writeInbound(Unpooled.wrappedBuffer(frame3.getBytes(US_ASCII)))).isTrue();
|
||||
FullHttpRequest actualRequest1 = channel.readInbound();
|
||||
FullHttpRequest expectedRequest1 =
|
||||
makeWhoisHttpRequest(
|
||||
"test1.tld",
|
||||
PROXY_CONFIG.whois.relayHost,
|
||||
PROXY_CONFIG.whois.relayPath,
|
||||
TestModule.provideFakeAccessToken().get());
|
||||
assertThat(expectedRequest1).isEqualTo(actualRequest1);
|
||||
// No more message at this point.
|
||||
assertThat((Object) channel.readInbound()).isNull();
|
||||
// More inbound bytes, but no newline.
|
||||
assertThat(channel.writeInbound(Unpooled.wrappedBuffer(frame4.getBytes(US_ASCII)))).isFalse();
|
||||
// Second message read.
|
||||
assertThat(channel.writeInbound(Unpooled.wrappedBuffer(frame5.getBytes(US_ASCII)))).isTrue();
|
||||
FullHttpRequest actualRequest2 = channel.readInbound();
|
||||
FullHttpRequest expectedRequest2 =
|
||||
makeWhoisHttpRequest(
|
||||
"test2.tld",
|
||||
PROXY_CONFIG.whois.relayHost,
|
||||
PROXY_CONFIG.whois.relayPath,
|
||||
TestModule.provideFakeAccessToken().get());
|
||||
assertThat(expectedRequest2).isEqualTo(actualRequest2);
|
||||
// The third message is not complete yet.
|
||||
assertThat(channel.isActive()).isTrue();
|
||||
assertThat((Object) channel.readInbound()).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSuccess_inboundMessageTooLong() {
|
||||
String inputString = Stream.generate(() -> "x").limit(513).collect(joining()) + "\r\n";
|
||||
// Nothing gets propagated further.
|
||||
assertThat(channel.writeInbound(Unpooled.wrappedBuffer(inputString.getBytes(US_ASCII))))
|
||||
.isFalse();
|
||||
// Connection is closed due to inbound message overflow.
|
||||
assertThat(channel.isActive()).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSuccess_parseSingleOutboundHttpResponse() {
|
||||
String outputString = "line1\r\nline2\r\n";
|
||||
FullHttpResponse response = makeWhoisHttpResponse(outputString, HttpResponseStatus.OK);
|
||||
// Http response parsed and passed along.
|
||||
assertThat(channel.writeOutbound(response)).isTrue();
|
||||
ByteBuf outputBuffer = channel.readOutbound();
|
||||
assertThat(outputBuffer.toString(US_ASCII)).isEqualTo(outputString);
|
||||
assertThat(channel.isActive()).isTrue();
|
||||
// Nothing more to write.
|
||||
assertThat((Object) channel.readOutbound()).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSuccess_parseMultipleOutboundHttpResponse() {
|
||||
String outputString1 = "line1\r\nline2\r\n";
|
||||
String outputString2 = "line3\r\nline4\r\nline5\r\n";
|
||||
FullHttpResponse response1 = makeWhoisHttpResponse(outputString1, HttpResponseStatus.OK);
|
||||
FullHttpResponse response2 = makeWhoisHttpResponse(outputString2, HttpResponseStatus.OK);
|
||||
assertThat(channel.writeOutbound(response1, response2)).isTrue();
|
||||
// First Http response parsed
|
||||
ByteBuf outputBuffer1 = channel.readOutbound();
|
||||
assertThat(outputBuffer1.toString(US_ASCII)).isEqualTo(outputString1);
|
||||
// Second Http response parsed
|
||||
ByteBuf outputBuffer2 = channel.readOutbound();
|
||||
assertThat(outputBuffer2.toString(US_ASCII)).isEqualTo(outputString2);
|
||||
assertThat(channel.isActive()).isTrue();
|
||||
// Nothing more to write.
|
||||
assertThat((Object) channel.readOutbound()).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFailure_outboundResponseStatusNotOK() {
|
||||
String outputString = "line1\r\nline2\r\n";
|
||||
FullHttpResponse response = makeWhoisHttpResponse(outputString, HttpResponseStatus.BAD_REQUEST);
|
||||
try {
|
||||
channel.writeOutbound(response);
|
||||
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();
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
173
javatests/google/registry/proxy/metric/BackendMetricsTest.java
Normal file
173
javatests/google/registry/proxy/metric/BackendMetricsTest.java
Normal file
|
@ -0,0 +1,173 @@
|
|||
// 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.metric;
|
||||
|
||||
import static google.registry.monitoring.metrics.contrib.DistributionMetricSubject.assertThat;
|
||||
import static google.registry.monitoring.metrics.contrib.LongMetricSubject.assertThat;
|
||||
import static google.registry.proxy.TestUtils.makeHttpPostRequest;
|
||||
import static google.registry.proxy.TestUtils.makeHttpResponse;
|
||||
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
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 BackendMetrics}. */
|
||||
@RunWith(JUnit4.class)
|
||||
public class BackendMetricsTest {
|
||||
|
||||
private final String host = "host.tld";
|
||||
private final String certHash = "blah12345";
|
||||
private final String protocol = "frontend protocol";
|
||||
|
||||
private final BackendMetrics metrics = new BackendMetrics();
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
metrics.resetMetric();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSuccess_oneRequest() {
|
||||
String content = "some content";
|
||||
FullHttpRequest request = makeHttpPostRequest(content, host, "/");
|
||||
metrics.requestSent(protocol, certHash, request);
|
||||
|
||||
assertThat(BackendMetrics.requestsCounter)
|
||||
.hasValueForLabels(1, protocol, certHash)
|
||||
.and()
|
||||
.hasNoOtherValues();
|
||||
assertThat(BackendMetrics.requestBytes)
|
||||
.hasDataSetForLabels(ImmutableSet.of(content.length()), protocol, certHash)
|
||||
.and()
|
||||
.hasNoOtherValues();
|
||||
assertThat(BackendMetrics.responsesCounter).hasNoOtherValues();
|
||||
assertThat(BackendMetrics.responseBytes).hasNoOtherValues();
|
||||
assertThat(BackendMetrics.latencyMs).hasNoOtherValues();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSuccess_multipleRequests() {
|
||||
String content1 = "some content";
|
||||
String content2 = "some other content";
|
||||
FullHttpRequest request1 = makeHttpPostRequest(content1, host, "/");
|
||||
FullHttpRequest request2 = makeHttpPostRequest(content2, host, "/");
|
||||
metrics.requestSent(protocol, certHash, request1);
|
||||
metrics.requestSent(protocol, certHash, request2);
|
||||
|
||||
assertThat(BackendMetrics.requestsCounter)
|
||||
.hasValueForLabels(2, protocol, certHash)
|
||||
.and()
|
||||
.hasNoOtherValues();
|
||||
assertThat(BackendMetrics.requestBytes)
|
||||
.hasDataSetForLabels(
|
||||
ImmutableSet.of(content1.length(), content2.length()), protocol, certHash)
|
||||
.and()
|
||||
.hasNoOtherValues();
|
||||
assertThat(BackendMetrics.responsesCounter).hasNoOtherValues();
|
||||
assertThat(BackendMetrics.responseBytes).hasNoOtherValues();
|
||||
assertThat(BackendMetrics.latencyMs).hasNoOtherValues();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSuccess_oneResponse() {
|
||||
String content = "some response";
|
||||
FullHttpResponse response = makeHttpResponse(content, HttpResponseStatus.OK);
|
||||
metrics.responseReceived(protocol, certHash, response, 5);
|
||||
|
||||
assertThat(BackendMetrics.requestsCounter).hasNoOtherValues();
|
||||
assertThat(BackendMetrics.requestBytes).hasNoOtherValues();
|
||||
assertThat(BackendMetrics.responsesCounter)
|
||||
.hasValueForLabels(1, protocol, certHash, "200 OK")
|
||||
.and()
|
||||
.hasNoOtherValues();
|
||||
assertThat(BackendMetrics.responseBytes)
|
||||
.hasDataSetForLabels(ImmutableSet.of(content.length()), protocol, certHash)
|
||||
.and()
|
||||
.hasNoOtherValues();
|
||||
assertThat(BackendMetrics.latencyMs)
|
||||
.hasDataSetForLabels(ImmutableSet.of(5), protocol, certHash)
|
||||
.and()
|
||||
.hasNoOtherValues();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSuccess_multipleResponses() {
|
||||
String content1 = "some response";
|
||||
String content2 = "other response";
|
||||
String content3 = "a very bad response";
|
||||
FullHttpResponse response1 = makeHttpResponse(content1, HttpResponseStatus.OK);
|
||||
FullHttpResponse response2 = makeHttpResponse(content2, HttpResponseStatus.OK);
|
||||
FullHttpResponse response3 = makeHttpResponse(content3, HttpResponseStatus.BAD_REQUEST);
|
||||
metrics.responseReceived(protocol, certHash, response1, 5);
|
||||
metrics.responseReceived(protocol, certHash, response2, 8);
|
||||
metrics.responseReceived(protocol, certHash, response3, 2);
|
||||
|
||||
assertThat(BackendMetrics.requestsCounter).hasNoOtherValues();
|
||||
assertThat(BackendMetrics.requestBytes).hasNoOtherValues();
|
||||
assertThat(BackendMetrics.responsesCounter)
|
||||
.hasValueForLabels(2, protocol, certHash, "200 OK")
|
||||
.and()
|
||||
.hasValueForLabels(1, protocol, certHash, "400 Bad Request")
|
||||
.and()
|
||||
.hasNoOtherValues();
|
||||
assertThat(BackendMetrics.responseBytes)
|
||||
.hasDataSetForLabels(
|
||||
ImmutableSet.of(content1.length(), content2.length(), content3.length()),
|
||||
protocol,
|
||||
certHash)
|
||||
.and()
|
||||
.hasNoOtherValues();
|
||||
assertThat(BackendMetrics.latencyMs)
|
||||
.hasDataSetForLabels(ImmutableSet.of(5, 8, 2), protocol, certHash)
|
||||
.and()
|
||||
.hasNoOtherValues();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSuccess_oneRequest_oneResponse() {
|
||||
String requestContent = "some request";
|
||||
String responseContent = "the only response";
|
||||
FullHttpRequest request = makeHttpPostRequest(requestContent, host, "/");
|
||||
FullHttpResponse response = makeHttpResponse(responseContent, HttpResponseStatus.OK);
|
||||
metrics.requestSent(protocol, certHash, request);
|
||||
metrics.responseReceived(protocol, certHash, response, 10);
|
||||
|
||||
assertThat(BackendMetrics.requestsCounter)
|
||||
.hasValueForLabels(1, protocol, certHash)
|
||||
.and()
|
||||
.hasNoOtherValues();
|
||||
assertThat(BackendMetrics.responsesCounter)
|
||||
.hasValueForLabels(1, protocol, certHash, "200 OK")
|
||||
.and()
|
||||
.hasNoOtherValues();
|
||||
assertThat(BackendMetrics.requestBytes)
|
||||
.hasDataSetForLabels(ImmutableSet.of(requestContent.length()), protocol, certHash)
|
||||
.and()
|
||||
.hasNoOtherValues();
|
||||
assertThat(BackendMetrics.responseBytes)
|
||||
.hasDataSetForLabels(ImmutableSet.of(responseContent.length()), protocol, certHash)
|
||||
.and()
|
||||
.hasNoOtherValues();
|
||||
assertThat(BackendMetrics.latencyMs)
|
||||
.hasDataSetForLabels(ImmutableSet.of(10), protocol, certHash)
|
||||
.and()
|
||||
.hasNoOtherValues();
|
||||
}
|
||||
}
|
166
javatests/google/registry/proxy/metric/FrontendMetricsTest.java
Normal file
166
javatests/google/registry/proxy/metric/FrontendMetricsTest.java
Normal file
|
@ -0,0 +1,166 @@
|
|||
// 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.metric;
|
||||
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
import static google.registry.monitoring.metrics.contrib.LongMetricSubject.assertThat;
|
||||
|
||||
import io.netty.channel.ChannelFuture;
|
||||
import io.netty.channel.DefaultChannelId;
|
||||
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 FrontendMetrics}. */
|
||||
@RunWith(JUnit4.class)
|
||||
public class FrontendMetricsTest {
|
||||
|
||||
private static final String PROTOCOL = "some protocol";
|
||||
private static final String CERT_HASH = "abc_blah_1134zdf";
|
||||
private final FrontendMetrics metrics = new FrontendMetrics();
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
metrics.resetMetrics();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSuccess_oneConnection() {
|
||||
EmbeddedChannel channel = new EmbeddedChannel();
|
||||
metrics.registerActiveConnection(PROTOCOL, CERT_HASH, channel);
|
||||
assertThat(channel.isActive()).isTrue();
|
||||
assertThat(FrontendMetrics.activeConnectionsGauge)
|
||||
.hasValueForLabels(1, PROTOCOL, CERT_HASH)
|
||||
.and()
|
||||
.hasNoOtherValues();
|
||||
assertThat(FrontendMetrics.totalConnectionsCounter)
|
||||
.hasValueForLabels(1, PROTOCOL, CERT_HASH)
|
||||
.and()
|
||||
.hasNoOtherValues();
|
||||
|
||||
ChannelFuture unusedFuture = channel.close();
|
||||
assertThat(channel.isActive()).isFalse();
|
||||
assertThat(FrontendMetrics.activeConnectionsGauge).hasNoOtherValues();
|
||||
assertThat(FrontendMetrics.totalConnectionsCounter)
|
||||
.hasValueForLabels(1, PROTOCOL, CERT_HASH)
|
||||
.and()
|
||||
.hasNoOtherValues();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSuccess_twoConnections_sameClient() {
|
||||
EmbeddedChannel channel1 = new EmbeddedChannel();
|
||||
EmbeddedChannel channel2 = new EmbeddedChannel(DefaultChannelId.newInstance());
|
||||
|
||||
metrics.registerActiveConnection(PROTOCOL, CERT_HASH, channel1);
|
||||
assertThat(channel1.isActive()).isTrue();
|
||||
assertThat(FrontendMetrics.activeConnectionsGauge)
|
||||
.hasValueForLabels(1, PROTOCOL, CERT_HASH)
|
||||
.and()
|
||||
.hasNoOtherValues();
|
||||
assertThat(FrontendMetrics.totalConnectionsCounter)
|
||||
.hasValueForLabels(1, PROTOCOL, CERT_HASH)
|
||||
.and()
|
||||
.hasNoOtherValues();
|
||||
|
||||
metrics.registerActiveConnection(PROTOCOL, CERT_HASH, channel2);
|
||||
assertThat(channel2.isActive()).isTrue();
|
||||
assertThat(FrontendMetrics.activeConnectionsGauge)
|
||||
.hasValueForLabels(2, PROTOCOL, CERT_HASH)
|
||||
.and()
|
||||
.hasNoOtherValues();
|
||||
assertThat(FrontendMetrics.totalConnectionsCounter)
|
||||
.hasValueForLabels(2, PROTOCOL, CERT_HASH)
|
||||
.and()
|
||||
.hasNoOtherValues();
|
||||
|
||||
ChannelFuture unusedFuture = channel1.close();
|
||||
assertThat(channel1.isActive()).isFalse();
|
||||
assertThat(FrontendMetrics.activeConnectionsGauge)
|
||||
.hasValueForLabels(1, PROTOCOL, CERT_HASH)
|
||||
.and()
|
||||
.hasNoOtherValues();
|
||||
assertThat(FrontendMetrics.totalConnectionsCounter)
|
||||
.hasValueForLabels(2, PROTOCOL, CERT_HASH)
|
||||
.and()
|
||||
.hasNoOtherValues();
|
||||
|
||||
unusedFuture = channel2.close();
|
||||
assertThat(channel2.isActive()).isFalse();
|
||||
assertThat(FrontendMetrics.activeConnectionsGauge).hasNoOtherValues();
|
||||
assertThat(FrontendMetrics.totalConnectionsCounter)
|
||||
.hasValueForLabels(2, PROTOCOL, CERT_HASH)
|
||||
.and()
|
||||
.hasNoOtherValues();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSuccess_twoConnections_differentClients() {
|
||||
EmbeddedChannel channel1 = new EmbeddedChannel();
|
||||
EmbeddedChannel channel2 = new EmbeddedChannel(DefaultChannelId.newInstance());
|
||||
String certHash2 = "blahblah_lol_234";
|
||||
|
||||
metrics.registerActiveConnection(PROTOCOL, CERT_HASH, channel1);
|
||||
assertThat(channel1.isActive()).isTrue();
|
||||
assertThat(FrontendMetrics.activeConnectionsGauge)
|
||||
.hasValueForLabels(1, PROTOCOL, CERT_HASH)
|
||||
.and()
|
||||
.hasNoOtherValues();
|
||||
assertThat(FrontendMetrics.totalConnectionsCounter)
|
||||
.hasValueForLabels(1, PROTOCOL, CERT_HASH)
|
||||
.and()
|
||||
.hasNoOtherValues();
|
||||
|
||||
metrics.registerActiveConnection(PROTOCOL, certHash2, channel2);
|
||||
assertThat(channel2.isActive()).isTrue();
|
||||
assertThat(FrontendMetrics.activeConnectionsGauge)
|
||||
.hasValueForLabels(1, PROTOCOL, CERT_HASH)
|
||||
.and()
|
||||
.hasValueForLabels(1, PROTOCOL, certHash2)
|
||||
.and()
|
||||
.hasNoOtherValues();
|
||||
assertThat(FrontendMetrics.totalConnectionsCounter)
|
||||
.hasValueForLabels(1, PROTOCOL, CERT_HASH)
|
||||
.and()
|
||||
.hasValueForLabels(1, PROTOCOL, certHash2)
|
||||
.and()
|
||||
.hasNoOtherValues();
|
||||
|
||||
ChannelFuture unusedFuture = channel1.close();
|
||||
assertThat(channel1.isActive()).isFalse();
|
||||
assertThat(FrontendMetrics.activeConnectionsGauge)
|
||||
.hasValueForLabels(1, PROTOCOL, certHash2)
|
||||
.and()
|
||||
.hasNoOtherValues();
|
||||
assertThat(FrontendMetrics.totalConnectionsCounter)
|
||||
.hasValueForLabels(1, PROTOCOL, CERT_HASH)
|
||||
.and()
|
||||
.hasValueForLabels(1, PROTOCOL, certHash2)
|
||||
.and()
|
||||
.hasNoOtherValues();
|
||||
|
||||
unusedFuture = channel2.close();
|
||||
assertThat(channel2.isActive()).isFalse();
|
||||
assertThat(FrontendMetrics.activeConnectionsGauge).hasNoOtherValues();
|
||||
assertThat(FrontendMetrics.totalConnectionsCounter)
|
||||
.hasValueForLabels(1, PROTOCOL, CERT_HASH)
|
||||
.and()
|
||||
.hasValueForLabels(1, PROTOCOL, certHash2)
|
||||
.and()
|
||||
.hasNoOtherValues();
|
||||
}
|
||||
}
|
23
javatests/google/registry/proxy/testdata/login.xml
vendored
Normal file
23
javatests/google/registry/proxy/testdata/login.xml
vendored
Normal file
|
@ -0,0 +1,23 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<epp xmlns="urn:ietf:params:xml:ns:epp-1.0">
|
||||
<command>
|
||||
<login>
|
||||
<clID>ClientX</clID>
|
||||
<pw>foo-BAR2</pw>
|
||||
<newPW>bar-FOO2</newPW>
|
||||
<options>
|
||||
<version>1.0</version>
|
||||
<lang>en</lang>
|
||||
</options>
|
||||
<svcs>
|
||||
<objURI>urn:ietf:params:xml:ns:obj1</objURI>
|
||||
<objURI>urn:ietf:params:xml:ns:obj2</objURI>
|
||||
<objURI>urn:ietf:params:xml:ns:obj3</objURI>
|
||||
<svcExtension>
|
||||
<extURI>http://custom/obj1ext-1.0</extURI>
|
||||
</svcExtension>
|
||||
</svcs>
|
||||
</login>
|
||||
<clTRID>ABC-12345</clTRID>
|
||||
</command>
|
||||
</epp>
|
12
javatests/google/registry/proxy/testdata/login_response.xml
vendored
Normal file
12
javatests/google/registry/proxy/testdata/login_response.xml
vendored
Normal file
|
@ -0,0 +1,12 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<epp xmlns:domain="urn:ietf:params:xml:ns:domain-1.0" xmlns:contact="urn:ietf:params:xml:ns:contact-1.0" xmlns:host="urn:ietf:params:xml:ns:host-1.0" xmlns:launch="urn:ietf:params:xml:ns:launch-1.0" xmlns:rgp="urn:ietf:params:xml:ns:rgp-1.0" xmlns="urn:ietf:params:xml:ns:epp-1.0" xmlns:secDNS="urn:ietf:params:xml:ns:secDNS-1.1" xmlns:fee12="urn:ietf:params:xml:ns:fee-0.12" xmlns:fee11="urn:ietf:params:xml:ns:fee-0.11" xmlns:mark="urn:ietf:params:xml:ns:mark-1.0" xmlns:fee="urn:ietf:params:xml:ns:fee-0.6">
|
||||
<response>
|
||||
<result code="1000">
|
||||
<msg>Command completed successfully</msg>
|
||||
</result>
|
||||
<trID>
|
||||
<clTRID>proxy-login</clTRID>
|
||||
<svTRID>inlxipwsQKaXS3VmbKOmBA==-a</svTRID>
|
||||
</trID>
|
||||
</response>
|
||||
</epp>
|
7
javatests/google/registry/proxy/testdata/logout.xml
vendored
Normal file
7
javatests/google/registry/proxy/testdata/logout.xml
vendored
Normal file
|
@ -0,0 +1,7 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<epp xmlns="urn:ietf:params:xml:ns:epp-1.0">
|
||||
<command>
|
||||
<logout/>
|
||||
<clTRID>ABC-12345</clTRID>
|
||||
</command>
|
||||
</epp>
|
12
javatests/google/registry/proxy/testdata/logout_response.xml
vendored
Normal file
12
javatests/google/registry/proxy/testdata/logout_response.xml
vendored
Normal file
|
@ -0,0 +1,12 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<epp xmlns:domain="urn:ietf:params:xml:ns:domain-1.0" xmlns:contact="urn:ietf:params:xml:ns:contact-1.0" xmlns:host="urn:ietf:params:xml:ns:host-1.0" xmlns:launch="urn:ietf:params:xml:ns:launch-1.0" xmlns:rgp="urn:ietf:params:xml:ns:rgp-1.0" xmlns="urn:ietf:params:xml:ns:epp-1.0" xmlns:secDNS="urn:ietf:params:xml:ns:secDNS-1.1" xmlns:fee12="urn:ietf:params:xml:ns:fee-0.12" xmlns:fee11="urn:ietf:params:xml:ns:fee-0.11" xmlns:mark="urn:ietf:params:xml:ns:mark-1.0" xmlns:fee="urn:ietf:params:xml:ns:fee-0.6">
|
||||
<response>
|
||||
<result code="1500">
|
||||
<msg>Command completed successfully; ending session</msg>
|
||||
</result>
|
||||
<trID>
|
||||
<clTRID>proxy-logout</clTRID>
|
||||
<svTRID>inlxipwsQKaXS3VmbKOmBA==-c</svTRID>
|
||||
</trID>
|
||||
</response>
|
||||
</epp>
|
Loading…
Add table
Add a link
Reference in a new issue