mirror of
https://github.com/google/nomulus.git
synced 2025-05-19 10:49:35 +02:00
Refactor to be more in line with a standard Gradle project structure
This commit is contained in:
parent
8fa45e8c76
commit
a7a983bfed
3141 changed files with 99 additions and 100 deletions
53
proxy/src/test/java/google/registry/proxy/BUILD
Normal file
53
proxy/src/test/java/google/registry/proxy/BUILD
Normal file
|
@ -0,0 +1,53 @@
|
|||
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",
|
||||
"quota/testdata/*.yaml",
|
||||
]),
|
||||
runtime_deps = [
|
||||
"@io_netty_tcnative_boringssl_static",
|
||||
],
|
||||
deps = [
|
||||
"//java/google/registry/proxy",
|
||||
"//java/google/registry/util",
|
||||
"//javatests/google/registry/testing",
|
||||
"@com_beust_jcommander",
|
||||
"@com_google_dagger",
|
||||
"@com_google_guava",
|
||||
"@com_google_monitoring_client_contrib",
|
||||
"@com_google_monitoring_client_metrics",
|
||||
"@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",
|
||||
"@javax_inject",
|
||||
"@joda_time",
|
||||
"@junit",
|
||||
"@org_bouncycastle_bcpkix_jdk15on",
|
||||
"@org_mockito_core",
|
||||
"@org_yaml_snakeyaml",
|
||||
],
|
||||
)
|
||||
|
||||
GenTestRules(
|
||||
name = "GeneratedTestRules",
|
||||
test_files = glob(
|
||||
["**/*Test.java"],
|
||||
exclude = ["ProtocolModuleTest.java"],
|
||||
),
|
||||
deps = [":proxy"],
|
||||
)
|
|
@ -0,0 +1,161 @@
|
|||
// 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 google.registry.testing.JUnitBackports.assertThrows;
|
||||
import static java.nio.charset.StandardCharsets.UTF_8;
|
||||
|
||||
import dagger.Component;
|
||||
import dagger.Module;
|
||||
import dagger.Provides;
|
||||
import google.registry.proxy.CertificateModule.Prod;
|
||||
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[] pemBytes) {
|
||||
return DaggerCertificateModuleTest_TestComponent.builder()
|
||||
.pemBytesModule(new PemBytesModule(pemBytes))
|
||||
.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);
|
||||
IllegalStateException thrown =
|
||||
assertThrows(IllegalStateException.class, () -> component.privateKey());
|
||||
assertThat(thrown).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);
|
||||
IllegalStateException thrown =
|
||||
assertThrows(IllegalStateException.class, () -> component.privateKey());
|
||||
assertThat(thrown).hasMessageThat().contains("2 keys are found");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFailure_certificatesOutOfOrder() throws Exception {
|
||||
byte[] pemBytes = getPemBytes(ssc.cert(), cert, key);
|
||||
component = createComponent(pemBytes);
|
||||
IllegalStateException thrown =
|
||||
assertThrows(IllegalStateException.class, () -> component.certificates());
|
||||
assertThat(thrown).hasMessageThat().contains("is not signed by");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFailure_noCertificates() throws Exception {
|
||||
byte[] pemBytes = getPemBytes(key);
|
||||
component = createComponent(pemBytes);
|
||||
IllegalStateException thrown =
|
||||
assertThrows(IllegalStateException.class, () -> component.certificates());
|
||||
assertThat(thrown).hasMessageThat().contains("No certificates");
|
||||
}
|
||||
|
||||
@Module
|
||||
static class PemBytesModule {
|
||||
private final byte[] pemBytes;
|
||||
|
||||
PemBytesModule(byte[] pemBytes) {
|
||||
this.pemBytes = pemBytes;
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Named("pemBytes")
|
||||
byte[] providePemBytes() {
|
||||
return pemBytes;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test component that exposes prod certificate and key.
|
||||
*
|
||||
* <p>Local certificate and key are not tested because they are directly extracted from a
|
||||
* self-signed certificate. Here we want to test that we can correctly parse and create
|
||||
* certificate and keys from a .pem file.
|
||||
*/
|
||||
@Singleton
|
||||
@Component(modules = {CertificateModule.class, PemBytesModule.class})
|
||||
interface TestComponent {
|
||||
|
||||
@Prod
|
||||
PrivateKey privateKey();
|
||||
|
||||
@Prod
|
||||
X509Certificate[] certificates();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,280 @@
|
|||
// 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.testing.JUnitBackports.assertThrows;
|
||||
import static google.registry.util.ResourceUtils.readResourceBytes;
|
||||
import static google.registry.util.X509Utils.getCertificateHash;
|
||||
import static java.nio.charset.StandardCharsets.UTF_8;
|
||||
|
||||
import com.google.common.base.Throwables;
|
||||
import google.registry.proxy.handler.HttpsRelayServiceHandler.NonOkHttpResponseException;
|
||||
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.EncoderException;
|
||||
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),
|
||||
CLIENT_ADDRESS,
|
||||
cookies);
|
||||
}
|
||||
|
||||
private FullHttpResponse makeEppHttpResponse(byte[] content, Cookie... cookies) {
|
||||
return makeEppHttpResponse(content, HttpResponseStatus.OK, cookies);
|
||||
}
|
||||
|
||||
private FullHttpResponse makeEppHttpResponse(
|
||||
byte[] content, HttpResponseStatus status, Cookie... cookies) {
|
||||
return TestUtils.makeEppHttpResponse(new String(content, UTF_8), status, 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 testFailure_nonOkOutboundMessage() throws Exception {
|
||||
// First inbound message is hello.
|
||||
channel.readInbound();
|
||||
|
||||
byte[] outputBytes = readResourceBytes(getClass(), "testdata/login_response.xml").read();
|
||||
|
||||
// Verify outbound message is not written to the peer as the response is not OK.
|
||||
EncoderException thrown =
|
||||
assertThrows(
|
||||
EncoderException.class,
|
||||
() ->
|
||||
channel.writeOutbound(
|
||||
makeEppHttpResponse(outputBytes, HttpResponseStatus.UNAUTHORIZED)));
|
||||
assertThat(Throwables.getRootCause(thrown)).isInstanceOf(NonOkHttpResponseException.class);
|
||||
assertThat(thrown).hasMessageThat().contains("401 Unauthorized");
|
||||
assertThat((Object) channel.readOutbound()).isNull();
|
||||
|
||||
// Channel is closed.
|
||||
assertThat(channel.isActive()).isFalse();
|
||||
}
|
||||
|
||||
@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,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 com.google.common.base.Joiner;
|
||||
import java.util.logging.Level;
|
||||
import java.util.logging.LogRecord;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.junit.runners.JUnit4;
|
||||
|
||||
/** Unit tests for {@link google.registry.proxy.GcpJsonFormatter}. */
|
||||
@RunWith(JUnit4.class)
|
||||
public class GcpJsonFormatterTest {
|
||||
|
||||
private static final String LOGGER_NAME = "example.company.app.logger";
|
||||
private static final String SOURCE_CLASS_NAME = "example.company.app.component.Doer";
|
||||
private static final String SOURCE_METHOD_NAME = "doStuff";
|
||||
private static final String MESSAGE = "Something I have to say";
|
||||
|
||||
private final GcpJsonFormatter formatter = new GcpJsonFormatter();
|
||||
private final LogRecord logRecord = new LogRecord(Level.WARNING, MESSAGE);
|
||||
|
||||
private static String makeJson(String severity, String source, String message) {
|
||||
return "{"
|
||||
+ Joiner.on(",")
|
||||
.join(
|
||||
makeJsonField("severity", severity),
|
||||
makeJsonField("source", source),
|
||||
makeJsonField("message", "\\n" + message))
|
||||
+ "}\n";
|
||||
}
|
||||
|
||||
private static String makeJsonField(String name, String content) {
|
||||
return Joiner.on(":").join(addQuoteAndReplaceNewline(name), addQuoteAndReplaceNewline(content));
|
||||
}
|
||||
|
||||
private static String addQuoteAndReplaceNewline(String content) {
|
||||
// This quadruple escaping is hurting my eyes.
|
||||
return "\"" + content.replaceAll("\n", "\\\\n") + "\"";
|
||||
}
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
logRecord.setLoggerName(LOGGER_NAME);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSuccess() {
|
||||
String actual = formatter.format(logRecord);
|
||||
String expected = makeJson("WARNING", LOGGER_NAME, MESSAGE);
|
||||
assertThat(actual).isEqualTo(expected);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSuccess_sourceClassAndMethod() {
|
||||
logRecord.setSourceClassName(SOURCE_CLASS_NAME);
|
||||
logRecord.setSourceMethodName(SOURCE_METHOD_NAME);
|
||||
String actual = formatter.format(logRecord);
|
||||
String expected = makeJson("WARNING", SOURCE_CLASS_NAME + " " + SOURCE_METHOD_NAME, MESSAGE);
|
||||
assertThat(actual).isEqualTo(expected);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSuccess_multilineMessage() {
|
||||
String multilineMessage = "First line message\nSecond line message\n";
|
||||
logRecord.setMessage(multilineMessage);
|
||||
String actual = formatter.format(logRecord);
|
||||
String expected = makeJson("WARNING", LOGGER_NAME, multilineMessage);
|
||||
assertThat(actual).isEqualTo(expected);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSuccess_withCause() {
|
||||
Throwable throwable = new Throwable("Some reason");
|
||||
StackTraceElement[] stacktrace = {
|
||||
new StackTraceElement("class1", "method1", "file1", 5),
|
||||
new StackTraceElement("class2", "method2", "file2", 10),
|
||||
};
|
||||
String stacktraceString =
|
||||
"java.lang.Throwable: Some reason\\n"
|
||||
+ "\\tat class1.method1(file1:5)\\n"
|
||||
+ "\\tat class2.method2(file2:10)\\n";
|
||||
throwable.setStackTrace(stacktrace);
|
||||
logRecord.setThrown(throwable);
|
||||
String actual = formatter.format(logRecord);
|
||||
String expected = makeJson("WARNING", LOGGER_NAME, MESSAGE + "\\n" + stacktraceString);
|
||||
assertThat(actual).isEqualTo(expected);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,291 @@
|
|||
// 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.LOCAL;
|
||||
import static google.registry.proxy.ProxyConfig.getProxyConfig;
|
||||
|
||||
import com.google.common.base.Suppliers;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import com.google.common.util.concurrent.MoreExecutors;
|
||||
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.ProxyConfig.Environment;
|
||||
import google.registry.proxy.WebWhoisProtocolsModule.HttpWhoisProtocol;
|
||||
import google.registry.proxy.WhoisProtocolModule.WhoisProtocol;
|
||||
import google.registry.proxy.handler.BackendMetricsHandler;
|
||||
import google.registry.proxy.handler.ProxyProtocolHandler;
|
||||
import google.registry.proxy.handler.QuotaHandler.EppQuotaHandler;
|
||||
import google.registry.proxy.handler.QuotaHandler.WhoisQuotaHandler;
|
||||
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.proxy.handler.WebWhoisRedirectHandler;
|
||||
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.timeout.ReadTimeoutHandler;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.Supplier;
|
||||
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(LOCAL);
|
||||
|
||||
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,
|
||||
// This handler is tested in its own unit tests. It is installed in web whois redirect
|
||||
// protocols. The end-to-end tests for the rest of the handlers in its pipeline need to
|
||||
// be able to emit incoming requests out of the channel for assertions. Therefore this
|
||||
// handler is removed from the pipeline.
|
||||
WebWhoisRedirectHandler.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,
|
||||
// Quota management is tested separately.
|
||||
WhoisQuotaHandler.class,
|
||||
EppQuotaHandler.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,
|
||||
CertificateModule.class,
|
||||
WhoisProtocolModule.class,
|
||||
WebWhoisProtocolsModule.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();
|
||||
|
||||
@HttpWhoisProtocol
|
||||
ImmutableList<Provider<? extends ChannelHandler>> httpWhoisHandlers();
|
||||
}
|
||||
|
||||
/**
|
||||
* 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(LOCAL);
|
||||
}
|
||||
|
||||
@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
|
||||
Clock provideFakeClock() {
|
||||
return fakeClock;
|
||||
}
|
||||
|
||||
@Singleton
|
||||
@Provides
|
||||
static ExecutorService provideExecutorService() {
|
||||
return MoreExecutors.newDirectExecutorService();
|
||||
}
|
||||
|
||||
@Singleton
|
||||
@Provides
|
||||
static ScheduledExecutorService provideScheduledExecutorService() {
|
||||
return Executors.newSingleThreadScheduledExecutor();
|
||||
}
|
||||
|
||||
@Singleton
|
||||
@Provides
|
||||
static Environment provideEnvironment() {
|
||||
return Environment.LOCAL;
|
||||
}
|
||||
|
||||
// This method is only here to satisfy Dagger binding, but is never used. In test environment,
|
||||
// it is the self-signed certificate and its key that end up being used.
|
||||
@Singleton
|
||||
@Provides
|
||||
@Named("pemBytes")
|
||||
static byte[] providePemBytes() {
|
||||
return new byte[0];
|
||||
}
|
||||
}
|
||||
}
|
133
proxy/src/test/java/google/registry/proxy/ProxyModuleTest.java
Normal file
133
proxy/src/test/java/google/registry/proxy/ProxyModuleTest.java
Normal file
|
@ -0,0 +1,133 @@
|
|||
// 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.LOCAL;
|
||||
import static google.registry.proxy.ProxyConfig.getProxyConfig;
|
||||
import static google.registry.testing.JUnitBackports.assertThrows;
|
||||
|
||||
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(LOCAL);
|
||||
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.provideHttpWhoisProtocol(PROXY_CONFIG))
|
||||
.isEqualTo(PROXY_CONFIG.webWhois.httpPort);
|
||||
assertThat(proxyModule.provideHttpsWhoisProtocol(PROXY_CONFIG))
|
||||
.isEqualTo(PROXY_CONFIG.webWhois.httpsPort);
|
||||
assertThat(proxyModule.provideEnvironment()).isEqualTo(LOCAL);
|
||||
assertThat(proxyModule.log).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFailure_parseArgs_loggingInProduction() {
|
||||
String[] args = {"--env", "production", "--log"};
|
||||
IllegalArgumentException e =
|
||||
assertThrows(
|
||||
IllegalArgumentException.class,
|
||||
() -> {
|
||||
proxyModule.parse(args);
|
||||
});
|
||||
assertThat(e)
|
||||
.hasMessageThat()
|
||||
.isEqualTo("Logging cannot be enabled for production environment");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFailure_parseArgs_wrongArguments() {
|
||||
String[] args = {"--wrong_flag", "some_value"};
|
||||
ParameterException thrown =
|
||||
assertThrows(ParameterException.class, () -> proxyModule.parse(args));
|
||||
assertThat(thrown).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_customhttpWhoisPort() {
|
||||
String[] args = {"--http_whois", "12121"};
|
||||
proxyModule.parse(args);
|
||||
assertThat(proxyModule.provideHttpWhoisProtocol(PROXY_CONFIG)).isEqualTo(12121);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSuccess_parseArgs_customhttpsWhoisPort() {
|
||||
String[] args = {"--https_whois", "21212"};
|
||||
proxyModule.parse(args);
|
||||
assertThat(proxyModule.provideHttpsWhoisProtocol(PROXY_CONFIG)).isEqualTo(21212);
|
||||
}
|
||||
|
||||
@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 =
|
||||
assertThrows(
|
||||
ParameterException.class,
|
||||
() -> {
|
||||
String[] args = {"--env", "beta"};
|
||||
proxyModule.parse(args);
|
||||
});
|
||||
assertThat(e).hasMessageThat().contains("Invalid value for --env parameter");
|
||||
}
|
||||
}
|
147
proxy/src/test/java/google/registry/proxy/TestUtils.java
Normal file
147
proxy/src/test/java/google/registry/proxy/TestUtils.java
Normal file
|
@ -0,0 +1,147 @@
|
|||
// 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 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.HttpMessage;
|
||||
import io.netty.handler.codec.http.HttpMethod;
|
||||
import io.netty.handler.codec.http.HttpRequest;
|
||||
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("user-agent", "Proxy")
|
||||
.set("host", host)
|
||||
.setInt("content-length", buf.readableBytes());
|
||||
return request;
|
||||
}
|
||||
|
||||
public static FullHttpRequest makeHttpGetRequest(String host, String path) {
|
||||
FullHttpRequest request =
|
||||
new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.GET, path);
|
||||
request.headers().set("host", host).setInt("content-length", 0);
|
||||
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().setInt("content-length", buf.readableBytes());
|
||||
return response;
|
||||
}
|
||||
|
||||
public static FullHttpResponse makeHttpResponse(HttpResponseStatus status) {
|
||||
FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, status);
|
||||
response.headers().setInt("content-length", 0);
|
||||
return response;
|
||||
}
|
||||
|
||||
public static FullHttpRequest makeWhoisHttpRequest(
|
||||
String content, String host, String path, String accessToken) {
|
||||
FullHttpRequest request = makeHttpPostRequest(content, host, path);
|
||||
request
|
||||
.headers()
|
||||
.set("authorization", "Bearer " + accessToken)
|
||||
.set("content-type", "text/plain")
|
||||
.set("accept", "text/plain");
|
||||
return request;
|
||||
}
|
||||
|
||||
public static FullHttpRequest makeEppHttpRequest(
|
||||
String content,
|
||||
String host,
|
||||
String path,
|
||||
String accessToken,
|
||||
String sslClientCertificateHash,
|
||||
String clientAddress,
|
||||
Cookie... cookies) {
|
||||
FullHttpRequest request = makeHttpPostRequest(content, host, path);
|
||||
request
|
||||
.headers()
|
||||
.set("authorization", "Bearer " + accessToken)
|
||||
.set("content-type", "application/epp+xml")
|
||||
.set("accept", "application/epp+xml")
|
||||
.set("X-SSL-Certificate", sslClientCertificateHash)
|
||||
.set("X-Forwarded-For", clientAddress);
|
||||
if (cookies.length != 0) {
|
||||
request.headers().set("cookie", ClientCookieEncoder.STRICT.encode(cookies));
|
||||
}
|
||||
return request;
|
||||
}
|
||||
|
||||
public static FullHttpResponse makeWhoisHttpResponse(String content, HttpResponseStatus status) {
|
||||
FullHttpResponse response = makeHttpResponse(content, status);
|
||||
response.headers().set("content-type", "text/plain");
|
||||
return response;
|
||||
}
|
||||
|
||||
public static FullHttpResponse makeEppHttpResponse(
|
||||
String content, HttpResponseStatus status, Cookie... cookies) {
|
||||
FullHttpResponse response = makeHttpResponse(content, status);
|
||||
response.headers().set("content-type", "application/epp+xml");
|
||||
for (Cookie cookie : cookies) {
|
||||
response.headers().add("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(HttpMessage msg1, HttpMessage msg2) {
|
||||
assertThat(msg1.protocolVersion()).isEqualTo(msg2.protocolVersion());
|
||||
assertThat(msg1.headers()).isEqualTo(msg2.headers());
|
||||
if (msg1 instanceof FullHttpRequest && msg2 instanceof FullHttpRequest) {
|
||||
assertThat(((FullHttpRequest) msg1).content()).isEqualTo(((FullHttpRequest) msg2).content());
|
||||
}
|
||||
}
|
||||
|
||||
public static void assertHttpResponseEquivalent(FullHttpResponse res1, FullHttpResponse res2) {
|
||||
assertThat(res1.status()).isEqualTo(res2.status());
|
||||
assertHttpMessageEquivalent(res1, res2);
|
||||
}
|
||||
|
||||
public static void assertHttpRequestEquivalent(HttpRequest req1, HttpRequest req2) {
|
||||
assertHttpMessageEquivalent(req1, req2);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,109 @@
|
|||
// Copyright 2018 The Nomulus Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package google.registry.proxy;
|
||||
|
||||
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.makeHttpGetRequest;
|
||||
import static google.registry.proxy.TestUtils.makeHttpResponse;
|
||||
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import io.netty.channel.embedded.EmbeddedChannel;
|
||||
import io.netty.handler.codec.http.DefaultHttpRequest;
|
||||
import io.netty.handler.codec.http.FullHttpRequest;
|
||||
import io.netty.handler.codec.http.FullHttpResponse;
|
||||
import io.netty.handler.codec.http.HttpClientCodec;
|
||||
import io.netty.handler.codec.http.HttpObjectAggregator;
|
||||
import io.netty.handler.codec.http.HttpResponseStatus;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.junit.runners.JUnit4;
|
||||
|
||||
/**
|
||||
* End-to-end tests for {@link WebWhoisProtocolsModule}.
|
||||
*
|
||||
* <p>This protocol defines a connection in which the proxy behaves as a standard http server (sans
|
||||
* the redirect 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 a client is reconstructed faithfully by the server, and vice versa, that the response a
|
||||
* client decoded from incoming bytes is equivalent to the response sent by the server.
|
||||
*
|
||||
* <p>These tests only ensure that the server represented by this protocol is compatible with a
|
||||
* client 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.
|
||||
*
|
||||
* <p>Only the HTTP redirect protocol is tested as both protocols share the same handlers except for
|
||||
* those that are excluded ({@code SslServerInitializer}, {@code WebWhoisRedirectHandler}).
|
||||
*/
|
||||
@RunWith(JUnit4.class)
|
||||
public class WebWhoisProtocolsModuleTest extends ProtocolModuleTest {
|
||||
|
||||
private static final String HOST = "test.tld";
|
||||
private static final String PATH = "/path/to/test";
|
||||
|
||||
private final EmbeddedChannel clientChannel =
|
||||
new EmbeddedChannel(new HttpClientCodec(), new HttpObjectAggregator(512 * 1024));
|
||||
|
||||
public WebWhoisProtocolsModuleTest() {
|
||||
super(TestComponent::httpWhoisHandlers);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 = makeHttpGetRequest(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(clientChannel.writeOutbound(requestSent.copy())).isTrue();
|
||||
buffer = clientChannel.readOutbound();
|
||||
assertThat(channel.writeInbound(buffer)).isTrue();
|
||||
// We only have a DefaultHttpRequest, not a FullHttpRequest because there is no HTTP aggregator
|
||||
// in the server's pipeline. But it is fine as we are not interested in the content (payload) of
|
||||
// the request, just its headers, which are contained in the DefaultHttpRequest.
|
||||
DefaultHttpRequest requestReceived = channel.readInbound();
|
||||
// Verify that the request received is the same as the request sent.
|
||||
assertHttpRequestEquivalent(requestSent, requestReceived);
|
||||
|
||||
FullHttpResponse responseSent = makeHttpResponse(status);
|
||||
assertThat(channel.writeOutbound(responseSent.copy())).isTrue();
|
||||
buffer = channel.readOutbound();
|
||||
assertThat(clientChannel.writeInbound(buffer)).isTrue();
|
||||
FullHttpResponse responseReceived = clientChannel.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);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,164 @@
|
|||
// 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 google.registry.testing.JUnitBackports.assertThrows;
|
||||
import static java.nio.charset.StandardCharsets.US_ASCII;
|
||||
import static java.util.stream.Collectors.joining;
|
||||
|
||||
import com.google.common.base.Throwables;
|
||||
import google.registry.proxy.handler.HttpsRelayServiceHandler.NonOkHttpResponseException;
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import io.netty.buffer.Unpooled;
|
||||
import io.netty.handler.codec.EncoderException;
|
||||
import io.netty.handler.codec.http.FullHttpRequest;
|
||||
import io.netty.handler.codec.http.FullHttpResponse;
|
||||
import io.netty.handler.codec.http.HttpResponseStatus;
|
||||
import java.nio.channels.ClosedChannelException;
|
||||
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(actualRequest).isEqualTo(expectedRequest);
|
||||
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(actualRequest1).isEqualTo(expectedRequest1);
|
||||
// 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(actualRequest2).isEqualTo(expectedRequest2);
|
||||
// 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()).isFalse();
|
||||
// Nothing more to write.
|
||||
assertThat((Object) channel.readOutbound()).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFailure_parseOnlyFirstFromMultipleOutboundHttpResponse() {
|
||||
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);
|
||||
assertThrows(ClosedChannelException.class, () -> channel.writeOutbound(response1, response2));
|
||||
// First Http response parsed
|
||||
ByteBuf outputBuffer1 = channel.readOutbound();
|
||||
assertThat(outputBuffer1.toString(US_ASCII)).isEqualTo(outputString1);
|
||||
// Second Http response not parsed because the connection is closed.
|
||||
assertThat(channel.isActive()).isFalse();
|
||||
assertThat((Object) channel.readOutbound()).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFailure_outboundResponseStatusNotOK() {
|
||||
String outputString = "line1\r\nline2\r\n";
|
||||
FullHttpResponse response = makeWhoisHttpResponse(outputString, HttpResponseStatus.BAD_REQUEST);
|
||||
EncoderException thrown =
|
||||
assertThrows(EncoderException.class, () -> channel.writeOutbound(response));
|
||||
assertThat(Throwables.getRootCause(thrown)).isInstanceOf(NonOkHttpResponseException.class);
|
||||
assertThat(thrown).hasMessageThat().contains("400 Bad Request");
|
||||
assertThat((Object) channel.readOutbound()).isNull();
|
||||
assertThat(channel.isActive()).isFalse();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,233 @@
|
|||
// 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.assertThrows;
|
||||
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 =
|
||||
assertThrows(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 =
|
||||
assertThrows(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.content().readableBytes());
|
||||
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.content().readableBytes());
|
||||
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.content().readableBytes());
|
||||
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 =
|
||||
assertThrows(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 22", HOST, "/");
|
||||
FullHttpResponse response2 = makeHttpResponse("response 22", HttpResponseStatus.OK);
|
||||
FullHttpRequest request3 = makeHttpPostRequest("request 333", HOST, "/");
|
||||
FullHttpResponse response3 = makeHttpResponse("response 333", 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.content().readableBytes());
|
||||
verify(metrics)
|
||||
.requestSent(RELAYED_PROTOCOL_NAME, CLIENT_CERT_HASH, request2.content().readableBytes());
|
||||
verify(metrics)
|
||||
.requestSent(RELAYED_PROTOCOL_NAME, CLIENT_CERT_HASH, request3.content().readableBytes());
|
||||
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,174 @@
|
|||
// 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.EppServiceHandler.CLIENT_CERTIFICATE_HASH_KEY;
|
||||
import static google.registry.testing.JUnitBackports.assertThrows;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.verifyNoMoreInteractions;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import google.registry.proxy.Protocol;
|
||||
import google.registry.proxy.handler.QuotaHandler.EppQuotaHandler;
|
||||
import google.registry.proxy.handler.QuotaHandler.OverQuotaException;
|
||||
import google.registry.proxy.metric.FrontendMetrics;
|
||||
import google.registry.proxy.quota.QuotaManager;
|
||||
import google.registry.proxy.quota.QuotaManager.QuotaRebate;
|
||||
import google.registry.proxy.quota.QuotaManager.QuotaRequest;
|
||||
import google.registry.proxy.quota.QuotaManager.QuotaResponse;
|
||||
import io.netty.channel.Channel;
|
||||
import io.netty.channel.ChannelFuture;
|
||||
import io.netty.channel.embedded.EmbeddedChannel;
|
||||
import org.joda.time.DateTime;
|
||||
import org.joda.time.DateTimeZone;
|
||||
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 EppQuotaHandler} */
|
||||
@RunWith(JUnit4.class)
|
||||
public class EppQuotaHandlerTest {
|
||||
|
||||
private final QuotaManager quotaManager = mock(QuotaManager.class);
|
||||
private final FrontendMetrics metrics = mock(FrontendMetrics.class);
|
||||
private final EppQuotaHandler handler = new EppQuotaHandler(quotaManager, metrics);
|
||||
private final EmbeddedChannel channel = new EmbeddedChannel(handler);
|
||||
private final String clientCertHash = "blah/123!";
|
||||
private final DateTime now = DateTime.now(DateTimeZone.UTC);
|
||||
private final Object message = new Object();
|
||||
|
||||
private void setProtocol(Channel channel) {
|
||||
channel
|
||||
.attr(PROTOCOL_KEY)
|
||||
.set(
|
||||
Protocol.frontendBuilder()
|
||||
.name("epp")
|
||||
.port(12345)
|
||||
.handlerProviders(ImmutableList.of())
|
||||
.relayProtocol(
|
||||
Protocol.backendBuilder()
|
||||
.name("backend")
|
||||
.host("host.tld")
|
||||
.port(1234)
|
||||
.handlerProviders(ImmutableList.of())
|
||||
.build())
|
||||
.build());
|
||||
}
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
channel.attr(CLIENT_CERTIFICATE_HASH_KEY).set(clientCertHash);
|
||||
setProtocol(channel);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSuccess_quotaGrantedAndReturned() {
|
||||
when(quotaManager.acquireQuota(QuotaRequest.create(clientCertHash)))
|
||||
.thenReturn(QuotaResponse.create(true, clientCertHash, now));
|
||||
|
||||
// First read, acquire quota.
|
||||
assertThat(channel.writeInbound(message)).isTrue();
|
||||
assertThat((Object) channel.readInbound()).isEqualTo(message);
|
||||
assertThat(channel.isActive()).isTrue();
|
||||
verify(quotaManager).acquireQuota(QuotaRequest.create(clientCertHash));
|
||||
|
||||
// Second read, should not acquire quota again.
|
||||
Object newMessage = new Object();
|
||||
assertThat(channel.writeInbound(newMessage)).isTrue();
|
||||
assertThat((Object) channel.readInbound()).isEqualTo(newMessage);
|
||||
verifyNoMoreInteractions(quotaManager);
|
||||
|
||||
// Channel closed, release quota.
|
||||
ChannelFuture unusedFuture = channel.close();
|
||||
verify(quotaManager)
|
||||
.releaseQuota(QuotaRebate.create(QuotaResponse.create(true, clientCertHash, now)));
|
||||
verifyNoMoreInteractions(quotaManager);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFailure_quotaNotGranted() {
|
||||
when(quotaManager.acquireQuota(QuotaRequest.create(clientCertHash)))
|
||||
.thenReturn(QuotaResponse.create(false, clientCertHash, now));
|
||||
OverQuotaException e =
|
||||
assertThrows(OverQuotaException.class, () -> channel.writeInbound(message));
|
||||
ChannelFuture unusedFuture = channel.close();
|
||||
assertThat(e).hasMessageThat().contains(clientCertHash);
|
||||
verify(quotaManager).acquireQuota(QuotaRequest.create(clientCertHash));
|
||||
// Make sure that quotaManager.releaseQuota() is not called when the channel closes.
|
||||
verifyNoMoreInteractions(quotaManager);
|
||||
verify(metrics).registerQuotaRejection("epp", clientCertHash);
|
||||
verifyNoMoreInteractions(metrics);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSuccess_twoChannels_twoUserIds() {
|
||||
// Set up another user.
|
||||
final EppQuotaHandler otherHandler = new EppQuotaHandler(quotaManager, metrics);
|
||||
final EmbeddedChannel otherChannel = new EmbeddedChannel(otherHandler);
|
||||
final String otherClientCertHash = "hola@9x";
|
||||
otherChannel.attr(CLIENT_CERTIFICATE_HASH_KEY).set(otherClientCertHash);
|
||||
setProtocol(otherChannel);
|
||||
final DateTime later = now.plus(Duration.standardSeconds(1));
|
||||
|
||||
when(quotaManager.acquireQuota(QuotaRequest.create(clientCertHash)))
|
||||
.thenReturn(QuotaResponse.create(true, clientCertHash, now));
|
||||
when(quotaManager.acquireQuota(QuotaRequest.create(otherClientCertHash)))
|
||||
.thenReturn(QuotaResponse.create(false, otherClientCertHash, later));
|
||||
|
||||
// Allows the first user.
|
||||
assertThat(channel.writeInbound(message)).isTrue();
|
||||
assertThat((Object) channel.readInbound()).isEqualTo(message);
|
||||
assertThat(channel.isActive()).isTrue();
|
||||
|
||||
// Blocks the second user.
|
||||
OverQuotaException e =
|
||||
assertThrows(OverQuotaException.class, () -> otherChannel.writeInbound(message));
|
||||
assertThat(e).hasMessageThat().contains(otherClientCertHash);
|
||||
verify(metrics).registerQuotaRejection("epp", otherClientCertHash);
|
||||
verifyNoMoreInteractions(metrics);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSuccess_twoChannels_sameUserIds() {
|
||||
// Set up another channel for the same user.
|
||||
final EppQuotaHandler otherHandler = new EppQuotaHandler(quotaManager, metrics);
|
||||
final EmbeddedChannel otherChannel = new EmbeddedChannel(otherHandler);
|
||||
otherChannel.attr(CLIENT_CERTIFICATE_HASH_KEY).set(clientCertHash);
|
||||
setProtocol(otherChannel);
|
||||
final DateTime later = now.plus(Duration.standardSeconds(1));
|
||||
|
||||
when(quotaManager.acquireQuota(QuotaRequest.create(clientCertHash)))
|
||||
.thenReturn(QuotaResponse.create(true, clientCertHash, now))
|
||||
.thenReturn(QuotaResponse.create(false, clientCertHash, later));
|
||||
|
||||
// Allows the first channel.
|
||||
assertThat(channel.writeInbound(message)).isTrue();
|
||||
assertThat((Object) channel.readInbound()).isEqualTo(message);
|
||||
assertThat(channel.isActive()).isTrue();
|
||||
|
||||
// Blocks the second channel.
|
||||
OverQuotaException e =
|
||||
assertThrows(OverQuotaException.class, () -> otherChannel.writeInbound(message));
|
||||
assertThat(e).hasMessageThat().contains(clientCertHash);
|
||||
verify(metrics).registerQuotaRejection("epp", clientCertHash);
|
||||
verifyNoMoreInteractions(metrics);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,329 @@
|
|||
// 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.testing.JUnitBackports.assertThrows;
|
||||
import static google.registry.util.X509Utils.getCertificateHash;
|
||||
import static java.nio.charset.StandardCharsets.UTF_8;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.verifyNoMoreInteractions;
|
||||
|
||||
import com.google.common.base.Throwables;
|
||||
import google.registry.proxy.TestUtils;
|
||||
import google.registry.proxy.handler.HttpsRelayServiceHandler.NonOkHttpResponseException;
|
||||
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 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,
|
||||
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),
|
||||
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,
|
||||
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,
|
||||
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(Unpooled.wrappedBuffer(content.getBytes(UTF_8))).isEqualTo(expectedResponse);
|
||||
// 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>";
|
||||
EncoderException thrown =
|
||||
assertThrows(
|
||||
EncoderException.class,
|
||||
() ->
|
||||
channel.writeOutbound(
|
||||
makeEppHttpResponse(content, HttpResponseStatus.BAD_REQUEST)));
|
||||
assertThat(Throwables.getRootCause(thrown)).isInstanceOf(NonOkHttpResponseException.class);
|
||||
assertThat(thrown).hasMessageThat().contains(HttpResponseStatus.BAD_REQUEST.toString());
|
||||
assertThat((Object) channel.readOutbound()).isNull();
|
||||
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();
|
||||
}
|
||||
}
|
223
proxy/src/test/java/google/registry/proxy/handler/NettyRule.java
Normal file
223
proxy/src/test/java/google/registry/proxy/handler/NettyRule.java
Normal file
|
@ -0,0 +1,223 @@
|
|||
// Copyright 2018 The Nomulus Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package google.registry.proxy.handler;
|
||||
|
||||
import static com.google.common.base.Preconditions.checkState;
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
import static google.registry.proxy.Protocol.PROTOCOL_KEY;
|
||||
import static google.registry.testing.JUnitBackports.assertThrows;
|
||||
import static java.nio.charset.StandardCharsets.US_ASCII;
|
||||
import static java.nio.charset.StandardCharsets.UTF_8;
|
||||
|
||||
import com.google.common.base.Throwables;
|
||||
import com.google.common.truth.ThrowableSubject;
|
||||
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.ChannelHandler;
|
||||
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.util.ReferenceCountUtil;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.Future;
|
||||
import org.junit.rules.ExternalResource;
|
||||
|
||||
/**
|
||||
* Helper for setting up and testing client / server connection with netty.
|
||||
*
|
||||
* <p>Used in {@link SslClientInitializerTest} and {@link SslServerInitializerTest}.
|
||||
*/
|
||||
final class NettyRule extends ExternalResource {
|
||||
|
||||
// All I/O operations are done inside the single thread within this event loop group, which is
|
||||
// different from the main test thread. Therefore synchronizations are required to make sure that
|
||||
// certain I/O activities are finished when assertions are performed.
|
||||
private final EventLoopGroup eventLoopGroup = new NioEventLoopGroup(1);
|
||||
|
||||
// Handler attached to server's channel to record the request received.
|
||||
private EchoHandler echoHandler;
|
||||
|
||||
// Handler attached to client's channel to record the response received.
|
||||
private DumpHandler dumpHandler;
|
||||
|
||||
private Channel channel;
|
||||
|
||||
/** Sets up a server channel bound to the given local address. */
|
||||
void setUpServer(LocalAddress localAddress, ChannelHandler handler) {
|
||||
checkState(echoHandler == null, "Can't call setUpServer twice");
|
||||
echoHandler = new EchoHandler();
|
||||
ChannelInitializer<LocalChannel> serverInitializer =
|
||||
new ChannelInitializer<LocalChannel>() {
|
||||
@Override
|
||||
protected void initChannel(LocalChannel ch) {
|
||||
// Add the given handler
|
||||
ch.pipeline().addLast(handler);
|
||||
// Add the "echoHandler" last to log the incoming message and send it back
|
||||
ch.pipeline().addLast(echoHandler);
|
||||
}
|
||||
};
|
||||
ServerBootstrap sb =
|
||||
new ServerBootstrap()
|
||||
.group(eventLoopGroup)
|
||||
.channel(LocalServerChannel.class)
|
||||
.childHandler(serverInitializer);
|
||||
ChannelFuture unusedFuture = sb.bind(localAddress).syncUninterruptibly();
|
||||
}
|
||||
|
||||
/** Sets up a client channel connecting to the give local address. */
|
||||
void setUpClient(
|
||||
LocalAddress localAddress,
|
||||
BackendProtocol protocol,
|
||||
ChannelHandler handler) {
|
||||
checkState(echoHandler != null, "Must call setUpServer before setUpClient");
|
||||
checkState(dumpHandler == null, "Can't call setUpClient twice");
|
||||
dumpHandler = new DumpHandler();
|
||||
ChannelInitializer<LocalChannel> clientInitializer =
|
||||
new ChannelInitializer<LocalChannel>() {
|
||||
@Override
|
||||
protected void initChannel(LocalChannel ch) throws Exception {
|
||||
// Add the given handler
|
||||
ch.pipeline().addLast(handler);
|
||||
// Add the "dumpHandler" last to log the incoming message
|
||||
ch.pipeline().addLast(dumpHandler);
|
||||
}
|
||||
};
|
||||
Bootstrap b =
|
||||
new Bootstrap()
|
||||
.group(eventLoopGroup)
|
||||
.channel(LocalChannel.class)
|
||||
.handler(clientInitializer)
|
||||
.attr(PROTOCOL_KEY, protocol);
|
||||
channel = b.connect(localAddress).syncUninterruptibly().channel();
|
||||
}
|
||||
|
||||
void checkReady() {
|
||||
checkState(channel != null, "Must call setUpClient to finish NettyRule setup");
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that a message can go through, both inbound and outbound.
|
||||
*
|
||||
* <p>The client writes the message to the server, which echos it back and saves the string in its
|
||||
* promise. The client receives the echo and saves it in its promise. All these activities happens
|
||||
* in the I/O thread, and this call itself returns immediately.
|
||||
*/
|
||||
void assertThatMessagesWork() throws Exception {
|
||||
checkReady();
|
||||
assertThat(channel.isActive()).isTrue();
|
||||
|
||||
writeToChannelAndFlush(channel, "Hello, world!");
|
||||
assertThat(echoHandler.getRequestFuture().get()).isEqualTo("Hello, world!");
|
||||
assertThat(dumpHandler.getResponseFuture().get()).isEqualTo("Hello, world!");
|
||||
}
|
||||
|
||||
Channel getChannel() {
|
||||
checkReady();
|
||||
return channel;
|
||||
}
|
||||
|
||||
ThrowableSubject assertThatServerRootCause() {
|
||||
checkReady();
|
||||
return assertThat(
|
||||
Throwables.getRootCause(
|
||||
assertThrows(ExecutionException.class, () -> echoHandler.getRequestFuture().get())));
|
||||
}
|
||||
|
||||
ThrowableSubject assertThatClientRootCause() {
|
||||
checkReady();
|
||||
return assertThat(
|
||||
Throwables.getRootCause(
|
||||
assertThrows(ExecutionException.class, () -> dumpHandler.getResponseFuture().get())));
|
||||
}
|
||||
|
||||
/**
|
||||
* A handler that echoes back its inbound message. The message is also saved in a promise for
|
||||
* inspection later.
|
||||
*/
|
||||
private static class EchoHandler extends ChannelInboundHandlerAdapter {
|
||||
|
||||
private final CompletableFuture<String> requestFuture = new CompletableFuture<>();
|
||||
|
||||
Future<String> getRequestFuture() {
|
||||
return requestFuture;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
|
||||
// In the test we only send messages of type ByteBuf.
|
||||
assertThat(msg).isInstanceOf(ByteBuf.class);
|
||||
String request = ((ByteBuf) msg).toString(UTF_8);
|
||||
// After the message is written back to the client, fulfill the promise.
|
||||
ChannelFuture unusedFuture =
|
||||
ctx.writeAndFlush(msg).addListener(f -> requestFuture.complete(request));
|
||||
}
|
||||
|
||||
/** Saves any inbound error as the cause of the promise failure. */
|
||||
@Override
|
||||
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
|
||||
ChannelFuture unusedFuture =
|
||||
ctx.channel().closeFuture().addListener(f -> requestFuture.completeExceptionally(cause));
|
||||
}
|
||||
}
|
||||
|
||||
/** A handler that dumps its inbound message to a promise that can be inspected later. */
|
||||
private static class DumpHandler extends ChannelInboundHandlerAdapter {
|
||||
|
||||
private final CompletableFuture<String> responseFuture = new CompletableFuture<>();
|
||||
|
||||
Future<String> getResponseFuture() {
|
||||
return responseFuture;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
|
||||
// In the test we only send messages of type ByteBuf.
|
||||
assertThat(msg).isInstanceOf(ByteBuf.class);
|
||||
String response = ((ByteBuf) msg).toString(UTF_8);
|
||||
// There is no more use of this message, we should release its reference count so that it
|
||||
// can be more effectively garbage collected by Netty.
|
||||
ReferenceCountUtil.release(msg);
|
||||
// Save the string in the promise and make it as complete.
|
||||
responseFuture.complete(response);
|
||||
}
|
||||
|
||||
/** Saves any inbound error into the failure cause of the promise. */
|
||||
@Override
|
||||
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
|
||||
ctx.channel().closeFuture().addListener(f -> responseFuture.completeExceptionally(cause));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void after() {
|
||||
Future<?> unusedFuture = eventLoopGroup.shutdownGracefully();
|
||||
}
|
||||
|
||||
private static void writeToChannelAndFlush(Channel channel, String data) {
|
||||
ChannelFuture unusedFuture =
|
||||
channel.writeAndFlush(Unpooled.wrappedBuffer(data.getBytes(US_ASCII)));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,131 @@
|
|||
// 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_proxyHeaderUnknownSource_singleFrame() {
|
||||
header = "PROXY UNKNOWN\r\n";
|
||||
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("0.0.0.0");
|
||||
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,125 @@
|
|||
// 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.RelayHandler.RELAY_BUFFER_KEY;
|
||||
import static google.registry.proxy.handler.RelayHandler.RELAY_CHANNEL_KEY;
|
||||
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import google.registry.proxy.Protocol;
|
||||
import google.registry.proxy.Protocol.BackendProtocol;
|
||||
import google.registry.proxy.Protocol.FrontendProtocol;
|
||||
import io.netty.channel.embedded.EmbeddedChannel;
|
||||
import java.util.ArrayDeque;
|
||||
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();
|
||||
private final FrontendProtocol frontendProtocol =
|
||||
Protocol.frontendBuilder()
|
||||
.port(0)
|
||||
.name("FRONTEND")
|
||||
.handlerProviders(ImmutableList.of())
|
||||
.relayProtocol(
|
||||
Protocol.backendBuilder()
|
||||
.host("host.invalid")
|
||||
.port(0)
|
||||
.name("BACKEND")
|
||||
.handlerProviders(ImmutableList.of())
|
||||
.build())
|
||||
.build();
|
||||
private final BackendProtocol backendProtocol = frontendProtocol.relayProtocol();
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
inboundChannel.attr(RELAY_CHANNEL_KEY).set(outboundChannel);
|
||||
inboundChannel.attr(RELAY_BUFFER_KEY).set(new ArrayDeque<>());
|
||||
inboundChannel.attr(PROTOCOL_KEY).set(frontendProtocol);
|
||||
outboundChannel.attr(PROTOCOL_KEY).set(backendProtocol);
|
||||
}
|
||||
|
||||
@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_frontClosed() {
|
||||
inboundChannel.attr(RELAY_BUFFER_KEY).set(null);
|
||||
inboundChannel.attr(PROTOCOL_KEY).set(backendProtocol);
|
||||
outboundChannel.attr(PROTOCOL_KEY).set(frontendProtocol);
|
||||
ExpectedType inboundMessage = new ExpectedType();
|
||||
// Outbound channel (frontend) is closed.
|
||||
outboundChannel.finish();
|
||||
assertThat(inboundChannel.writeInbound(inboundMessage)).isFalse();
|
||||
ExpectedType relayedMessage = outboundChannel.readOutbound();
|
||||
assertThat(relayedMessage).isNull();
|
||||
// Inbound channel (backend) should stay open.
|
||||
assertThat(inboundChannel.isActive()).isTrue();
|
||||
assertThat(inboundChannel.attr(RELAY_BUFFER_KEY).get()).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSuccess_backendClosed_enqueueBuffer() {
|
||||
ExpectedType inboundMessage = new ExpectedType();
|
||||
// Outbound channel (backend) is closed.
|
||||
outboundChannel.finish();
|
||||
assertThat(inboundChannel.writeInbound(inboundMessage)).isFalse();
|
||||
ExpectedType relayedMessage = outboundChannel.readOutbound();
|
||||
assertThat(relayedMessage).isNull();
|
||||
// Inbound channel (frontend) should stay open.
|
||||
assertThat(inboundChannel.isActive()).isTrue();
|
||||
assertThat(inboundChannel.attr(RELAY_BUFFER_KEY).get()).containsExactly(inboundMessage);
|
||||
}
|
||||
|
||||
@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,206 @@
|
|||
// 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.setUpSslChannel;
|
||||
import static google.registry.proxy.handler.SslInitializerTestUtils.signKeyPair;
|
||||
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import google.registry.proxy.Protocol;
|
||||
import google.registry.proxy.Protocol.BackendProtocol;
|
||||
import io.netty.channel.ChannelHandler;
|
||||
import io.netty.channel.ChannelPipeline;
|
||||
import io.netty.channel.embedded.EmbeddedChannel;
|
||||
import io.netty.channel.local.LocalAddress;
|
||||
import io.netty.channel.local.LocalChannel;
|
||||
import io.netty.handler.ssl.OpenSsl;
|
||||
import io.netty.handler.ssl.SniHandler;
|
||||
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 java.security.KeyPair;
|
||||
import java.security.PrivateKey;
|
||||
import java.security.cert.CertPathBuilderException;
|
||||
import java.security.cert.CertificateException;
|
||||
import java.security.cert.X509Certificate;
|
||||
import javax.net.ssl.SSLException;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.junit.runners.Parameterized;
|
||||
import org.junit.runners.Parameterized.Parameter;
|
||||
import org.junit.runners.Parameterized.Parameters;
|
||||
|
||||
/**
|
||||
* 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(Parameterized.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;
|
||||
|
||||
@Rule
|
||||
public NettyRule nettyRule = new NettyRule();
|
||||
|
||||
@Parameter(0)
|
||||
public SslProvider sslProvider;
|
||||
|
||||
// We do our best effort to test all available SSL providers.
|
||||
@Parameters(name = "{0}")
|
||||
public static SslProvider[] data() {
|
||||
return OpenSsl.isAvailable()
|
||||
? new SslProvider[] {SslProvider.JDK, SslProvider.OPENSSL}
|
||||
: new SslProvider[] {SslProvider.JDK};
|
||||
}
|
||||
|
||||
/** Saves the SNI hostname received by the server, if sent by the client. */
|
||||
private String sniHostReceived;
|
||||
|
||||
/** 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 ChannelHandler getServerHandler(PrivateKey privateKey, X509Certificate certificate)
|
||||
throws Exception {
|
||||
SslContext sslContext = SslContextBuilder.forServer(privateKey, certificate).build();
|
||||
return new SniHandler(
|
||||
hostname -> {
|
||||
sniHostReceived = hostname;
|
||||
return sslContext;
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSuccess_swappedInitializerWithSslHandler() throws Exception {
|
||||
SslClientInitializer<EmbeddedChannel> sslClientInitializer =
|
||||
new SslClientInitializer<>(sslProvider);
|
||||
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);
|
||||
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_" + sslProvider);
|
||||
nettyRule.setUpServer(localAddress, getServerHandler(ssc.key(), ssc.cert()));
|
||||
SslClientInitializer<LocalChannel> sslClientInitializer =
|
||||
new SslClientInitializer<>(sslProvider);
|
||||
nettyRule.setUpClient(localAddress, PROTOCOL, sslClientInitializer);
|
||||
// The connection is now terminated, both the client side and the server side should get
|
||||
// exceptions.
|
||||
nettyRule.assertThatClientRootCause().isInstanceOf(CertPathBuilderException.class);
|
||||
nettyRule.assertThatServerRootCause().isInstanceOf(SSLException.class);
|
||||
assertThat(nettyRule.getChannel().isActive()).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSuccess_customTrustManager_acceptCertSignedByTrustedCa() throws Exception {
|
||||
LocalAddress localAddress =
|
||||
new LocalAddress("CUSTOM_TRUST_MANAGER_ACCEPT_CERT_SIGNED_BY_TRUSTED_CA_" + sslProvider);
|
||||
|
||||
// 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();
|
||||
nettyRule.setUpServer(localAddress, getServerHandler(privateKey, cert));
|
||||
|
||||
// Set up the client to trust the self signed cert used to sign the cert that server provides.
|
||||
SslClientInitializer<LocalChannel> sslClientInitializer =
|
||||
new SslClientInitializer<>(sslProvider, new X509Certificate[] {ssc.cert()});
|
||||
nettyRule.setUpClient(localAddress, PROTOCOL, sslClientInitializer);
|
||||
|
||||
setUpSslChannel(nettyRule.getChannel(), cert);
|
||||
nettyRule.assertThatMessagesWork();
|
||||
|
||||
// Verify that the SNI extension is sent during handshake.
|
||||
assertThat(sniHostReceived).isEqualTo(SSL_HOST);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFailure_customTrustManager_wrongHostnameInCertificate() throws Exception {
|
||||
LocalAddress localAddress =
|
||||
new LocalAddress("CUSTOM_TRUST_MANAGER_WRONG_HOSTNAME_" + sslProvider);
|
||||
|
||||
// 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();
|
||||
nettyRule.setUpServer(localAddress, getServerHandler(privateKey, cert));
|
||||
|
||||
// Set up the client to trust the self signed cert used to sign the cert that server provides.
|
||||
SslClientInitializer<LocalChannel> sslClientInitializer =
|
||||
new SslClientInitializer<>(sslProvider, new X509Certificate[] {ssc.cert()});
|
||||
nettyRule.setUpClient(localAddress, PROTOCOL, sslClientInitializer);
|
||||
|
||||
// When the client rejects the server cert due to wrong hostname, both the client and server
|
||||
// should throw exceptions.
|
||||
nettyRule.assertThatClientRootCause().isInstanceOf(CertificateException.class);
|
||||
nettyRule.assertThatClientRootCause().hasMessageThat().contains(SSL_HOST);
|
||||
nettyRule.assertThatServerRootCause().isInstanceOf(SSLException.class);
|
||||
assertThat(nettyRule.getChannel().isActive()).isFalse();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,95 @@
|
|||
// 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 io.netty.channel.Channel;
|
||||
import io.netty.handler.ssl.SslHandler;
|
||||
import io.netty.handler.ssl.util.SelfSignedCertificate;
|
||||
import java.math.BigInteger;
|
||||
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 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());
|
||||
}
|
||||
|
||||
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 setUpSslChannel(
|
||||
Channel channel,
|
||||
X509Certificate... certs)
|
||||
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()
|
||||
.containsExactlyElementsIn(certs);
|
||||
// Returns the SSL session for further assertion.
|
||||
return sslHandler.engine().getSession();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,270 @@
|
|||
// 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.setUpSslChannel;
|
||||
import static google.registry.proxy.handler.SslInitializerTestUtils.signKeyPair;
|
||||
|
||||
import com.google.common.base.Suppliers;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import google.registry.proxy.Protocol;
|
||||
import google.registry.proxy.Protocol.BackendProtocol;
|
||||
import io.netty.channel.ChannelHandler;
|
||||
import io.netty.channel.ChannelInitializer;
|
||||
import io.netty.channel.ChannelPipeline;
|
||||
import io.netty.channel.embedded.EmbeddedChannel;
|
||||
import io.netty.channel.local.LocalAddress;
|
||||
import io.netty.channel.local.LocalChannel;
|
||||
import io.netty.handler.ssl.OpenSsl;
|
||||
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 java.security.KeyPair;
|
||||
import java.security.PrivateKey;
|
||||
import java.security.cert.CertificateException;
|
||||
import java.security.cert.X509Certificate;
|
||||
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.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.junit.runners.Parameterized;
|
||||
import org.junit.runners.Parameterized.Parameter;
|
||||
import org.junit.runners.Parameterized.Parameters;
|
||||
|
||||
/**
|
||||
* 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(Parameterized.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();
|
||||
|
||||
@Rule
|
||||
public NettyRule nettyRule = new NettyRule();
|
||||
|
||||
@Parameter(0)
|
||||
public SslProvider sslProvider;
|
||||
|
||||
// We do our best effort to test all available SSL providers.
|
||||
@Parameters(name = "{0}")
|
||||
public static SslProvider[] data() {
|
||||
return OpenSsl.isAvailable()
|
||||
? new SslProvider[] {SslProvider.OPENSSL, SslProvider.JDK}
|
||||
: new SslProvider[] {SslProvider.JDK};
|
||||
}
|
||||
|
||||
private ChannelHandler getServerHandler(
|
||||
boolean requireClientCert, PrivateKey privateKey, X509Certificate... certificates) {
|
||||
return new SslServerInitializer<LocalChannel>(
|
||||
requireClientCert,
|
||||
sslProvider,
|
||||
Suppliers.ofInstance(privateKey),
|
||||
Suppliers.ofInstance(certificates));
|
||||
}
|
||||
|
||||
private ChannelHandler getServerHandler(PrivateKey privateKey, X509Certificate... certificates) {
|
||||
return getServerHandler(true, privateKey, certificates);
|
||||
}
|
||||
|
||||
private ChannelHandler getClientHandler(
|
||||
X509Certificate trustedCertificate,
|
||||
PrivateKey privateKey,
|
||||
X509Certificate certificate) {
|
||||
return new ChannelInitializer<LocalChannel>() {
|
||||
@Override
|
||||
protected void initChannel(LocalChannel ch) throws Exception {
|
||||
SslContextBuilder sslContextBuilder =
|
||||
SslContextBuilder.forClient().trustManager(trustedCertificate).sslProvider(sslProvider);
|
||||
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(sslHandler);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSuccess_swappedInitializerWithSslHandler() throws Exception {
|
||||
SelfSignedCertificate ssc = new SelfSignedCertificate(SSL_HOST);
|
||||
SslServerInitializer<EmbeddedChannel> sslServerInitializer =
|
||||
new SslServerInitializer<>(
|
||||
true,
|
||||
sslProvider,
|
||||
Suppliers.ofInstance(ssc.key()),
|
||||
Suppliers.ofInstance(new X509Certificate[] {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_" + sslProvider);
|
||||
|
||||
nettyRule.setUpServer(localAddress, getServerHandler(serverSsc.key(), serverSsc.cert()));
|
||||
SelfSignedCertificate clientSsc = new SelfSignedCertificate();
|
||||
nettyRule.setUpClient(
|
||||
localAddress,
|
||||
PROTOCOL,
|
||||
getClientHandler(serverSsc.cert(), clientSsc.key(), clientSsc.cert()));
|
||||
|
||||
SSLSession sslSession = setUpSslChannel(nettyRule.getChannel(), serverSsc.cert());
|
||||
nettyRule.assertThatMessagesWork();
|
||||
|
||||
// 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());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSuccess_doesNotRequireClientCert() throws Exception {
|
||||
SelfSignedCertificate serverSsc = new SelfSignedCertificate(SSL_HOST);
|
||||
LocalAddress localAddress = new LocalAddress("DOES_NOT_REQUIRE_CLIENT_CERT_" + sslProvider);
|
||||
|
||||
nettyRule.setUpServer(
|
||||
localAddress,
|
||||
getServerHandler(false, serverSsc.key(), serverSsc.cert()));
|
||||
nettyRule.setUpClient(
|
||||
localAddress, PROTOCOL, getClientHandler(serverSsc.cert(), null, null));
|
||||
|
||||
SSLSession sslSession = setUpSslChannel(nettyRule.getChannel(), serverSsc.cert());
|
||||
nettyRule.assertThatMessagesWork();
|
||||
|
||||
// Verify that the SSL session does not contain any 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()).isNull();
|
||||
assertThat(sslSession.getPeerCertificates()).asList().containsExactly(serverSsc.cert());
|
||||
}
|
||||
|
||||
@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_" + sslProvider);
|
||||
|
||||
nettyRule.setUpServer(
|
||||
localAddress,
|
||||
getServerHandler(
|
||||
keyPair.getPrivate(),
|
||||
// Serving both the server cert, and the CA cert
|
||||
serverCert,
|
||||
caSsc.cert()));
|
||||
SelfSignedCertificate clientSsc = new SelfSignedCertificate();
|
||||
nettyRule.setUpClient(
|
||||
localAddress,
|
||||
PROTOCOL,
|
||||
getClientHandler(
|
||||
// Client trusts the CA cert
|
||||
caSsc.cert(), clientSsc.key(), clientSsc.cert()));
|
||||
|
||||
SSLSession sslSession = setUpSslChannel(nettyRule.getChannel(), serverCert, caSsc.cert());
|
||||
nettyRule.assertThatMessagesWork();
|
||||
|
||||
assertThat(sslSession.getLocalCertificates()).asList().containsExactly(clientSsc.cert());
|
||||
assertThat(sslSession.getPeerCertificates())
|
||||
.asList()
|
||||
.containsExactly(serverCert, caSsc.cert())
|
||||
.inOrder();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFailure_requireClientCertificate() throws Exception {
|
||||
SelfSignedCertificate serverSsc = new SelfSignedCertificate(SSL_HOST);
|
||||
LocalAddress localAddress = new LocalAddress("REQUIRE_CLIENT_CERT_" + sslProvider);
|
||||
|
||||
nettyRule.setUpServer(localAddress, getServerHandler(serverSsc.key(), serverSsc.cert()));
|
||||
nettyRule.setUpClient(
|
||||
localAddress,
|
||||
PROTOCOL,
|
||||
getClientHandler(
|
||||
serverSsc.cert(),
|
||||
// No client cert/private key used.
|
||||
null,
|
||||
null));
|
||||
|
||||
// When the server rejects the client during handshake due to lack of client certificate, both
|
||||
// should throw exceptions.
|
||||
nettyRule.assertThatServerRootCause().isInstanceOf(SSLHandshakeException.class);
|
||||
nettyRule.assertThatClientRootCause().isInstanceOf(SSLException.class);
|
||||
assertThat(nettyRule.getChannel().isActive()).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFailure_wrongHostnameInCertificate() throws Exception {
|
||||
SelfSignedCertificate serverSsc = new SelfSignedCertificate("wrong.com");
|
||||
LocalAddress localAddress = new LocalAddress("WRONG_HOSTNAME_" + sslProvider);
|
||||
|
||||
nettyRule.setUpServer(localAddress, getServerHandler(serverSsc.key(), serverSsc.cert()));
|
||||
SelfSignedCertificate clientSsc = new SelfSignedCertificate();
|
||||
nettyRule.setUpClient(
|
||||
localAddress,
|
||||
PROTOCOL,
|
||||
getClientHandler(serverSsc.cert(), clientSsc.key(), clientSsc.cert()));
|
||||
|
||||
// When the client rejects the server cert due to wrong hostname, both the server and the client
|
||||
// throw exceptions.
|
||||
nettyRule.assertThatClientRootCause().isInstanceOf(CertificateException.class);
|
||||
nettyRule.assertThatClientRootCause().hasMessageThat().contains(SSL_HOST);
|
||||
nettyRule.assertThatServerRootCause().isInstanceOf(SSLException.class);
|
||||
assertThat(nettyRule.getChannel().isActive()).isFalse();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,233 @@
|
|||
// Copyright 2018 The Nomulus Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package google.registry.proxy.handler;
|
||||
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
import static google.registry.proxy.TestUtils.assertHttpResponseEquivalent;
|
||||
import static google.registry.proxy.TestUtils.makeHttpGetRequest;
|
||||
import static google.registry.proxy.TestUtils.makeHttpPostRequest;
|
||||
import static google.registry.proxy.TestUtils.makeHttpResponse;
|
||||
|
||||
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.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.junit.runners.JUnit4;
|
||||
|
||||
/** Unit tests for {@link WebWhoisRedirectHandler}. */
|
||||
@RunWith(JUnit4.class)
|
||||
public class WebWhoisRedirectHandlerTest {
|
||||
|
||||
private static final String REDIRECT_HOST = "www.example.com";
|
||||
private static final String TARGET_HOST = "whois.nic.tld";
|
||||
|
||||
private EmbeddedChannel channel;
|
||||
private FullHttpRequest request;
|
||||
private FullHttpResponse response;
|
||||
|
||||
private void setupChannel(boolean isHttps) {
|
||||
channel = new EmbeddedChannel(new WebWhoisRedirectHandler(isHttps, REDIRECT_HOST));
|
||||
}
|
||||
|
||||
private static FullHttpResponse makeRedirectResponse(
|
||||
HttpResponseStatus status, String location, boolean keepAlive, boolean hsts) {
|
||||
FullHttpResponse response = makeHttpResponse("", status);
|
||||
response.headers().set("content-type", "text/plain").set("content-length", "0");
|
||||
if (location != null) {
|
||||
response.headers().set("location", location);
|
||||
}
|
||||
if (keepAlive) {
|
||||
response.headers().set("connection", "keep-alive");
|
||||
}
|
||||
if (hsts) {
|
||||
response.headers().set("Strict-Transport-Security", "max-age=31536000");
|
||||
}
|
||||
return response;
|
||||
}
|
||||
|
||||
// HTTP redirect tests.
|
||||
|
||||
@Test
|
||||
public void testSuccess_http_methodNotAllowed() {
|
||||
setupChannel(false);
|
||||
request = makeHttpPostRequest("", TARGET_HOST, "/");
|
||||
// No inbound message passed to the next handler.
|
||||
assertThat(channel.writeInbound(request)).isFalse();
|
||||
response = channel.readOutbound();
|
||||
assertHttpResponseEquivalent(
|
||||
response, makeRedirectResponse(HttpResponseStatus.METHOD_NOT_ALLOWED, null, true, false));
|
||||
assertThat(channel.isActive()).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSuccess_http_badHost() {
|
||||
setupChannel(false);
|
||||
request = makeHttpGetRequest("", "/");
|
||||
// No inbound message passed to the next handler.
|
||||
assertThat(channel.writeInbound(request)).isFalse();
|
||||
response = channel.readOutbound();
|
||||
assertHttpResponseEquivalent(
|
||||
response, makeRedirectResponse(HttpResponseStatus.BAD_REQUEST, null, true, false));
|
||||
assertThat(channel.isActive()).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSuccess_http_noHost() {
|
||||
setupChannel(false);
|
||||
request = makeHttpGetRequest("", "/");
|
||||
request.headers().remove("host");
|
||||
// No inbound message passed to the next handler.
|
||||
assertThat(channel.writeInbound(request)).isFalse();
|
||||
response = channel.readOutbound();
|
||||
assertHttpResponseEquivalent(
|
||||
response, makeRedirectResponse(HttpResponseStatus.BAD_REQUEST, null, true, false));
|
||||
assertThat(channel.isActive()).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSuccess_http_healthCheck() {
|
||||
setupChannel(false);
|
||||
request = makeHttpPostRequest("", TARGET_HOST, "/");
|
||||
// No inbound message passed to the next handler.
|
||||
assertThat(channel.writeInbound(request)).isFalse();
|
||||
response = channel.readOutbound();
|
||||
assertHttpResponseEquivalent(
|
||||
response, makeRedirectResponse(HttpResponseStatus.METHOD_NOT_ALLOWED, null, true, false));
|
||||
assertThat(channel.isActive()).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSuccess_http_redirectToHttps() {
|
||||
setupChannel(false);
|
||||
request = makeHttpGetRequest(TARGET_HOST, "/");
|
||||
// No inbound message passed to the next handler.
|
||||
assertThat(channel.writeInbound(request)).isFalse();
|
||||
response = channel.readOutbound();
|
||||
assertHttpResponseEquivalent(
|
||||
response,
|
||||
makeRedirectResponse(
|
||||
HttpResponseStatus.MOVED_PERMANENTLY, "https://whois.nic.tld/", true, false));
|
||||
assertThat(channel.isActive()).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSuccess_http_redirectToHttps_hostAndPort() {
|
||||
setupChannel(false);
|
||||
request = makeHttpGetRequest(TARGET_HOST + ":80", "/");
|
||||
// No inbound message passed to the next handler.
|
||||
assertThat(channel.writeInbound(request)).isFalse();
|
||||
response = channel.readOutbound();
|
||||
assertHttpResponseEquivalent(
|
||||
response,
|
||||
makeRedirectResponse(
|
||||
HttpResponseStatus.MOVED_PERMANENTLY, "https://whois.nic.tld/", true, false));
|
||||
assertThat(channel.isActive()).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSuccess_http_redirectToHttps_noKeepAlive() {
|
||||
setupChannel(false);
|
||||
request = makeHttpGetRequest(TARGET_HOST, "/");
|
||||
request.headers().set("connection", "close");
|
||||
// No inbound message passed to the next handler.
|
||||
assertThat(channel.writeInbound(request)).isFalse();
|
||||
response = channel.readOutbound();
|
||||
assertHttpResponseEquivalent(
|
||||
response,
|
||||
makeRedirectResponse(
|
||||
HttpResponseStatus.MOVED_PERMANENTLY, "https://whois.nic.tld/", false, false));
|
||||
assertThat(channel.isActive()).isFalse();
|
||||
}
|
||||
|
||||
// HTTPS redirect tests.
|
||||
|
||||
@Test
|
||||
public void testSuccess_https_methodNotAllowed() {
|
||||
setupChannel(true);
|
||||
request = makeHttpPostRequest("", TARGET_HOST, "/");
|
||||
// No inbound message passed to the next handler.
|
||||
assertThat(channel.writeInbound(request)).isFalse();
|
||||
response = channel.readOutbound();
|
||||
assertHttpResponseEquivalent(
|
||||
response, makeRedirectResponse(HttpResponseStatus.METHOD_NOT_ALLOWED, null, true, false));
|
||||
assertThat(channel.isActive()).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSuccess_https_badHost() {
|
||||
setupChannel(true);
|
||||
request = makeHttpGetRequest("", "/");
|
||||
// No inbound message passed to the next handler.
|
||||
assertThat(channel.writeInbound(request)).isFalse();
|
||||
response = channel.readOutbound();
|
||||
assertHttpResponseEquivalent(
|
||||
response, makeRedirectResponse(HttpResponseStatus.BAD_REQUEST, null, true, false));
|
||||
assertThat(channel.isActive()).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSuccess_https_noHost() {
|
||||
setupChannel(true);
|
||||
request = makeHttpGetRequest("", "/");
|
||||
request.headers().remove("host");
|
||||
// No inbound message passed to the next handler.
|
||||
assertThat(channel.writeInbound(request)).isFalse();
|
||||
response = channel.readOutbound();
|
||||
assertHttpResponseEquivalent(
|
||||
response, makeRedirectResponse(HttpResponseStatus.BAD_REQUEST, null, true, false));
|
||||
assertThat(channel.isActive()).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSuccess_https_healthCheck() {
|
||||
setupChannel(true);
|
||||
request = makeHttpGetRequest("health-check.invalid", "/");
|
||||
// No inbound message passed to the next handler.
|
||||
assertThat(channel.writeInbound(request)).isFalse();
|
||||
response = channel.readOutbound();
|
||||
assertHttpResponseEquivalent(
|
||||
response, makeRedirectResponse(HttpResponseStatus.FORBIDDEN, null, true, false));
|
||||
assertThat(channel.isActive()).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSuccess_https_redirectToDestination() {
|
||||
setupChannel(true);
|
||||
request = makeHttpGetRequest(TARGET_HOST, "/");
|
||||
// No inbound message passed to the next handler.
|
||||
assertThat(channel.writeInbound(request)).isFalse();
|
||||
response = channel.readOutbound();
|
||||
assertHttpResponseEquivalent(
|
||||
response,
|
||||
makeRedirectResponse(HttpResponseStatus.FOUND, "https://www.example.com/", true, true));
|
||||
assertThat(channel.isActive()).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSuccess_https_redirectToDestination_noKeepAlive() {
|
||||
setupChannel(true);
|
||||
request = makeHttpGetRequest(TARGET_HOST, "/");
|
||||
request.headers().set("connection", "close");
|
||||
// No inbound message passed to the next handler.
|
||||
assertThat(channel.writeInbound(request)).isFalse();
|
||||
response = channel.readOutbound();
|
||||
assertHttpResponseEquivalent(
|
||||
response,
|
||||
makeRedirectResponse(HttpResponseStatus.FOUND, "https://www.example.com/", false, true));
|
||||
assertThat(channel.isActive()).isFalse();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,179 @@
|
|||
// 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.ProxyProtocolHandler.REMOTE_ADDRESS_KEY;
|
||||
import static google.registry.testing.JUnitBackports.assertThrows;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.verifyNoMoreInteractions;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import google.registry.proxy.Protocol;
|
||||
import google.registry.proxy.handler.QuotaHandler.OverQuotaException;
|
||||
import google.registry.proxy.handler.QuotaHandler.WhoisQuotaHandler;
|
||||
import google.registry.proxy.metric.FrontendMetrics;
|
||||
import google.registry.proxy.quota.QuotaManager;
|
||||
import google.registry.proxy.quota.QuotaManager.QuotaRequest;
|
||||
import google.registry.proxy.quota.QuotaManager.QuotaResponse;
|
||||
import io.netty.channel.Channel;
|
||||
import io.netty.channel.ChannelFuture;
|
||||
import io.netty.channel.embedded.EmbeddedChannel;
|
||||
import org.joda.time.DateTime;
|
||||
import org.joda.time.DateTimeZone;
|
||||
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 WhoisQuotaHandler} */
|
||||
@RunWith(JUnit4.class)
|
||||
public class WhoisQuotaHandlerTest {
|
||||
|
||||
private final QuotaManager quotaManager = mock(QuotaManager.class);
|
||||
private final FrontendMetrics metrics = mock(FrontendMetrics.class);
|
||||
private final WhoisQuotaHandler handler = new WhoisQuotaHandler(quotaManager, metrics);
|
||||
private final EmbeddedChannel channel = new EmbeddedChannel(handler);
|
||||
private final DateTime now = DateTime.now(DateTimeZone.UTC);
|
||||
private final String remoteAddress = "127.0.0.1";
|
||||
private final Object message = new Object();
|
||||
|
||||
private void setProtocol(Channel channel) {
|
||||
channel
|
||||
.attr(PROTOCOL_KEY)
|
||||
.set(
|
||||
Protocol.frontendBuilder()
|
||||
.name("whois")
|
||||
.port(12345)
|
||||
.handlerProviders(ImmutableList.of())
|
||||
.relayProtocol(
|
||||
Protocol.backendBuilder()
|
||||
.name("backend")
|
||||
.host("host.tld")
|
||||
.port(1234)
|
||||
.handlerProviders(ImmutableList.of())
|
||||
.build())
|
||||
.build());
|
||||
}
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
channel.attr(REMOTE_ADDRESS_KEY).set(remoteAddress);
|
||||
setProtocol(channel);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSuccess_quotaGranted() {
|
||||
when(quotaManager.acquireQuota(QuotaRequest.create(remoteAddress)))
|
||||
.thenReturn(QuotaResponse.create(true, remoteAddress, now));
|
||||
|
||||
// First read, acquire quota.
|
||||
assertThat(channel.writeInbound(message)).isTrue();
|
||||
assertThat((Object) channel.readInbound()).isEqualTo(message);
|
||||
assertThat(channel.isActive()).isTrue();
|
||||
verify(quotaManager).acquireQuota(QuotaRequest.create(remoteAddress));
|
||||
|
||||
// Second read, should not acquire quota again.
|
||||
assertThat(channel.writeInbound(message)).isTrue();
|
||||
assertThat((Object) channel.readInbound()).isEqualTo(message);
|
||||
|
||||
// Channel closed, release quota.
|
||||
ChannelFuture unusedFuture = channel.close();
|
||||
verifyNoMoreInteractions(quotaManager);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFailure_quotaNotGranted() {
|
||||
when(quotaManager.acquireQuota(QuotaRequest.create(remoteAddress)))
|
||||
.thenReturn(QuotaResponse.create(false, remoteAddress, now));
|
||||
OverQuotaException e =
|
||||
assertThrows(OverQuotaException.class, () -> channel.writeInbound(message));
|
||||
assertThat(e).hasMessageThat().contains("none");
|
||||
verify(metrics).registerQuotaRejection("whois", "none");
|
||||
verifyNoMoreInteractions(metrics);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSuccess_twoChannels_twoUserIds() {
|
||||
// Set up another user.
|
||||
final WhoisQuotaHandler otherHandler = new WhoisQuotaHandler(quotaManager, metrics);
|
||||
final EmbeddedChannel otherChannel = new EmbeddedChannel(otherHandler);
|
||||
final String otherRemoteAddress = "192.168.0.1";
|
||||
otherChannel.attr(REMOTE_ADDRESS_KEY).set(otherRemoteAddress);
|
||||
setProtocol(otherChannel);
|
||||
final DateTime later = now.plus(Duration.standardSeconds(1));
|
||||
|
||||
when(quotaManager.acquireQuota(QuotaRequest.create(remoteAddress)))
|
||||
.thenReturn(QuotaResponse.create(true, remoteAddress, now));
|
||||
when(quotaManager.acquireQuota(QuotaRequest.create(otherRemoteAddress)))
|
||||
.thenReturn(QuotaResponse.create(false, otherRemoteAddress, later));
|
||||
|
||||
// Allows the first user.
|
||||
assertThat(channel.writeInbound(message)).isTrue();
|
||||
assertThat((Object) channel.readInbound()).isEqualTo(message);
|
||||
assertThat(channel.isActive()).isTrue();
|
||||
|
||||
// Blocks the second user.
|
||||
OverQuotaException e =
|
||||
assertThrows(OverQuotaException.class, () -> otherChannel.writeInbound(message));
|
||||
assertThat(e).hasMessageThat().contains("none");
|
||||
verify(metrics).registerQuotaRejection("whois", "none");
|
||||
verifyNoMoreInteractions(metrics);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSuccess_oneUser_rateLimited() {
|
||||
// Set up another channel for the same user.
|
||||
final WhoisQuotaHandler otherHandler = new WhoisQuotaHandler(quotaManager, metrics);
|
||||
final EmbeddedChannel otherChannel = new EmbeddedChannel(otherHandler);
|
||||
otherChannel.attr(REMOTE_ADDRESS_KEY).set(remoteAddress);
|
||||
setProtocol(otherChannel);
|
||||
final DateTime later = now.plus(Duration.standardSeconds(1));
|
||||
|
||||
// Set up the third channel for the same user
|
||||
final WhoisQuotaHandler thirdHandler = new WhoisQuotaHandler(quotaManager, metrics);
|
||||
final EmbeddedChannel thirdChannel = new EmbeddedChannel(thirdHandler);
|
||||
thirdChannel.attr(REMOTE_ADDRESS_KEY).set(remoteAddress);
|
||||
final DateTime evenLater = now.plus(Duration.standardSeconds(60));
|
||||
|
||||
when(quotaManager.acquireQuota(QuotaRequest.create(remoteAddress)))
|
||||
.thenReturn(QuotaResponse.create(true, remoteAddress, now))
|
||||
// Throttles the second connection.
|
||||
.thenReturn(QuotaResponse.create(false, remoteAddress, later))
|
||||
// Allows the third connection because token refilled.
|
||||
.thenReturn(QuotaResponse.create(true, remoteAddress, evenLater));
|
||||
|
||||
// Allows the first channel.
|
||||
assertThat(channel.writeInbound(message)).isTrue();
|
||||
assertThat((Object) channel.readInbound()).isEqualTo(message);
|
||||
assertThat(channel.isActive()).isTrue();
|
||||
|
||||
// Blocks the second channel.
|
||||
OverQuotaException e =
|
||||
assertThrows(OverQuotaException.class, () -> otherChannel.writeInbound(message));
|
||||
assertThat(e).hasMessageThat().contains("none");
|
||||
verify(metrics).registerQuotaRejection("whois", "none");
|
||||
|
||||
// Allows the third channel.
|
||||
assertThat(thirdChannel.writeInbound(message)).isTrue();
|
||||
assertThat((Object) thirdChannel.readInbound()).isEqualTo(message);
|
||||
assertThat(thirdChannel.isActive()).isTrue();
|
||||
verifyNoMoreInteractions(metrics);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,129 @@
|
|||
// 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 google.registry.testing.JUnitBackports.assertThrows;
|
||||
import static java.nio.charset.StandardCharsets.US_ASCII;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.verifyNoMoreInteractions;
|
||||
|
||||
import com.google.common.base.Throwables;
|
||||
import google.registry.proxy.handler.HttpsRelayServiceHandler.NonOkHttpResponseException;
|
||||
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.EncoderException;
|
||||
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()).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFailure_OutboundHttpResponseNotOK() {
|
||||
String outputString = "line1\r\nline2\r\n";
|
||||
FullHttpResponse outputResponse =
|
||||
makeWhoisHttpResponse(outputString, HttpResponseStatus.BAD_REQUEST);
|
||||
EncoderException thrown =
|
||||
assertThrows(EncoderException.class, () -> channel.writeOutbound(outputResponse));
|
||||
assertThat(Throwables.getRootCause(thrown)).isInstanceOf(NonOkHttpResponseException.class);
|
||||
assertThat(thrown).hasMessageThat().contains("400 Bad Request");
|
||||
assertThat((Object) channel.readOutbound()).isNull();
|
||||
assertThat(channel.isActive()).isFalse();
|
||||
}
|
||||
}
|
|
@ -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 com.google.monitoring.metrics.contrib.DistributionMetricSubject.assertThat;
|
||||
import static com.google.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.content().readableBytes());
|
||||
|
||||
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.content().readableBytes());
|
||||
metrics.requestSent(protocol, certHash, request2.content().readableBytes());
|
||||
|
||||
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.content().readableBytes());
|
||||
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();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,187 @@
|
|||
// 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 com.google.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();
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
ChannelFuture unusedFuture1 = 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();
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
ChannelFuture unusedFuture2 = 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();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSuccess_registerQuotaRejections() {
|
||||
String otherCertHash = "foobar1234X";
|
||||
String remoteAddress = "127.0.0.1";
|
||||
String otherProtocol = "other protocol";
|
||||
metrics.registerQuotaRejection(PROTOCOL, CERT_HASH);
|
||||
metrics.registerQuotaRejection(PROTOCOL, otherCertHash);
|
||||
metrics.registerQuotaRejection(PROTOCOL, otherCertHash);
|
||||
metrics.registerQuotaRejection(otherProtocol, remoteAddress);
|
||||
assertThat(FrontendMetrics.quotaRejectionsCounter)
|
||||
.hasValueForLabels(1, PROTOCOL, CERT_HASH)
|
||||
.and()
|
||||
.hasValueForLabels(2, PROTOCOL, otherCertHash)
|
||||
.and()
|
||||
.hasValueForLabels(1, otherProtocol, remoteAddress)
|
||||
.and()
|
||||
.hasNoOtherValues();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,136 @@
|
|||
// 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.proxy.metric.MetricParameters.CLUSTER_NAME_PATH;
|
||||
import static google.registry.proxy.metric.MetricParameters.CONTAINER_NAME_ENV;
|
||||
import static google.registry.proxy.metric.MetricParameters.INSTANCE_ID_PATH;
|
||||
import static google.registry.proxy.metric.MetricParameters.NAMESPACE_ID_ENV;
|
||||
import static google.registry.proxy.metric.MetricParameters.POD_ID_ENV;
|
||||
import static google.registry.proxy.metric.MetricParameters.PROJECT_ID_PATH;
|
||||
import static google.registry.proxy.metric.MetricParameters.ZONE_PATH;
|
||||
import static java.nio.charset.StandardCharsets.UTF_8;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyInt;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map.Entry;
|
||||
import java.util.function.Function;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.junit.runners.JUnit4;
|
||||
|
||||
/** Unit tests for {@link MetricParameters}. */
|
||||
@RunWith(JUnit4.class)
|
||||
public class MetricParametersTest {
|
||||
|
||||
private static final HashMap<String, String> RESULTS = new HashMap<>();
|
||||
|
||||
private final HttpURLConnection projectIdConnection = mock(HttpURLConnection.class);
|
||||
private final HttpURLConnection clusterNameConnection = mock(HttpURLConnection.class);
|
||||
private final HttpURLConnection instanceIdConnection = mock(HttpURLConnection.class);
|
||||
private final HttpURLConnection zoneConnection = mock(HttpURLConnection.class);
|
||||
private final ImmutableMap<String, HttpURLConnection> mockConnections =
|
||||
ImmutableMap.of(
|
||||
PROJECT_ID_PATH,
|
||||
projectIdConnection,
|
||||
CLUSTER_NAME_PATH,
|
||||
clusterNameConnection,
|
||||
INSTANCE_ID_PATH,
|
||||
instanceIdConnection,
|
||||
ZONE_PATH,
|
||||
zoneConnection);
|
||||
private final HashMap<String, String> fakeEnvVarMap = new HashMap<>();
|
||||
private final Function<String, HttpURLConnection> fakeConnectionFactory = mockConnections::get;
|
||||
|
||||
private final MetricParameters metricParameters =
|
||||
new MetricParameters(fakeEnvVarMap, fakeConnectionFactory);
|
||||
|
||||
private static InputStream makeInputStreamFromString(String input) {
|
||||
return new ByteArrayInputStream(input.getBytes(UTF_8));
|
||||
}
|
||||
|
||||
@Before
|
||||
public void setUp() throws Exception {
|
||||
fakeEnvVarMap.put(NAMESPACE_ID_ENV, "some-namespace");
|
||||
fakeEnvVarMap.put(POD_ID_ENV, "some-pod");
|
||||
fakeEnvVarMap.put(CONTAINER_NAME_ENV, "some-container");
|
||||
when(projectIdConnection.getInputStream())
|
||||
.thenReturn(makeInputStreamFromString("some-project"));
|
||||
when(clusterNameConnection.getInputStream())
|
||||
.thenReturn(makeInputStreamFromString("some-cluster"));
|
||||
when(instanceIdConnection.getInputStream())
|
||||
.thenReturn(makeInputStreamFromString("some-instance"));
|
||||
when(zoneConnection.getInputStream())
|
||||
.thenReturn(makeInputStreamFromString("projects/some-project/zones/some-zone"));
|
||||
for (Entry<String, HttpURLConnection> entry : mockConnections.entrySet()) {
|
||||
when(entry.getValue().getResponseCode()).thenReturn(200);
|
||||
}
|
||||
RESULTS.put("project_id", "some-project");
|
||||
RESULTS.put("cluster_name", "some-cluster");
|
||||
RESULTS.put("namespace_id", "some-namespace");
|
||||
RESULTS.put("instance_id", "some-instance");
|
||||
RESULTS.put("pod_id", "some-pod");
|
||||
RESULTS.put("container_name", "some-container");
|
||||
RESULTS.put("zone", "some-zone");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSuccess() {
|
||||
assertThat(metricParameters.makeLabelsMap()).isEqualTo(ImmutableMap.copyOf(RESULTS));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSuccess_missingEnvVar() {
|
||||
fakeEnvVarMap.remove(POD_ID_ENV);
|
||||
RESULTS.put("pod_id", "");
|
||||
assertThat(metricParameters.makeLabelsMap()).isEqualTo(ImmutableMap.copyOf(RESULTS));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSuccess_malformedZone() throws Exception {
|
||||
when(zoneConnection.getInputStream()).thenReturn(makeInputStreamFromString("some-zone"));
|
||||
RESULTS.put("zone", "");
|
||||
assertThat(metricParameters.makeLabelsMap()).isEqualTo(ImmutableMap.copyOf(RESULTS));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSuccess_errorResponseCode() throws Exception {
|
||||
when(projectIdConnection.getResponseCode()).thenReturn(404);
|
||||
when(projectIdConnection.getErrorStream())
|
||||
.thenReturn(makeInputStreamFromString("some error message"));
|
||||
RESULTS.put("project_id", "");
|
||||
assertThat(metricParameters.makeLabelsMap()).isEqualTo(ImmutableMap.copyOf(RESULTS));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSuccess_connectionError() throws Exception {
|
||||
InputStream fakeInputStream = mock(InputStream.class);
|
||||
when(projectIdConnection.getInputStream()).thenReturn(fakeInputStream);
|
||||
when(fakeInputStream.read(any(byte[].class), anyInt(), anyInt()))
|
||||
.thenThrow(new IOException("some exception"));
|
||||
RESULTS.put("project_id", "");
|
||||
assertThat(metricParameters.makeLabelsMap()).isEqualTo(ImmutableMap.copyOf(RESULTS));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,92 @@
|
|||
// Copyright 2018 The Nomulus Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package google.registry.proxy.quota;
|
||||
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
import static google.registry.testing.JUnitBackports.assertThrows;
|
||||
import static google.registry.util.ResourceUtils.readResourceUtf8;
|
||||
|
||||
import google.registry.proxy.ProxyConfig.Quota;
|
||||
import org.joda.time.Duration;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.junit.runners.JUnit4;
|
||||
import org.yaml.snakeyaml.Yaml;
|
||||
|
||||
/** Unit Tests for {@link QuotaConfig} */
|
||||
@RunWith(JUnit4.class)
|
||||
public class QuotaConfigTest {
|
||||
|
||||
private QuotaConfig quotaConfig;
|
||||
|
||||
private static QuotaConfig loadQuotaConfig(String filename) {
|
||||
return new QuotaConfig(
|
||||
new Yaml()
|
||||
.loadAs(readResourceUtf8(QuotaConfigTest.class, "testdata/" + filename), Quota.class),
|
||||
"theProtocol");
|
||||
}
|
||||
|
||||
private void validateQuota(String userId, int tokenAmount, int refillSeconds) {
|
||||
assertThat(quotaConfig.hasUnlimitedTokens(userId)).isFalse();
|
||||
assertThat(quotaConfig.getTokenAmount(userId)).isEqualTo(tokenAmount);
|
||||
assertThat(quotaConfig.getRefillPeriod(userId))
|
||||
.isEqualTo(Duration.standardSeconds(refillSeconds));
|
||||
assertThat(quotaConfig.getProtocolName()).isEqualTo("theProtocol");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSuccess_regularConfig() {
|
||||
quotaConfig = loadQuotaConfig("quota_config_regular.yaml");
|
||||
assertThat(quotaConfig.getRefreshPeriod()).isEqualTo(Duration.standardHours(1));
|
||||
validateQuota("abc", 10, 60);
|
||||
validateQuota("987lol", 500, 10);
|
||||
validateQuota("no_match", 100, 60);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSuccess_onlyDefault() {
|
||||
quotaConfig = loadQuotaConfig("quota_config_default.yaml");
|
||||
assertThat(quotaConfig.getRefreshPeriod()).isEqualTo(Duration.standardHours(1));
|
||||
validateQuota("abc", 100, 60);
|
||||
validateQuota("987lol", 100, 60);
|
||||
validateQuota("no_match", 100, 60);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSuccess_noRefresh_noRefill() {
|
||||
quotaConfig = loadQuotaConfig("quota_config_no_refresh_no_refill.yaml");
|
||||
assertThat(quotaConfig.getRefreshPeriod()).isEqualTo(Duration.ZERO);
|
||||
assertThat(quotaConfig.getRefillPeriod("no_match")).isEqualTo(Duration.ZERO);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFailure_getTokenAmount_throwsOnUnlimitedTokens() {
|
||||
quotaConfig = loadQuotaConfig("quota_config_unlimited_tokens.yaml");
|
||||
assertThat(quotaConfig.hasUnlimitedTokens("some_user")).isTrue();
|
||||
IllegalStateException e =
|
||||
assertThrows(IllegalStateException.class, () -> quotaConfig.getTokenAmount("some_user"));
|
||||
assertThat(e)
|
||||
.hasMessageThat()
|
||||
.contains("User ID some_user is provisioned with unlimited tokens");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFailure_duplicateUserId() {
|
||||
IllegalArgumentException e =
|
||||
assertThrows(
|
||||
IllegalArgumentException.class, () -> loadQuotaConfig("quota_config_duplicate.yaml"));
|
||||
assertThat(e).hasMessageThat().contains("Multiple entries with same key");
|
||||
}
|
||||
}
|
|
@ -0,0 +1,82 @@
|
|||
// 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.quota;
|
||||
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.verifyNoMoreInteractions;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import com.google.common.util.concurrent.MoreExecutors;
|
||||
import google.registry.proxy.quota.QuotaManager.QuotaRebate;
|
||||
import google.registry.proxy.quota.QuotaManager.QuotaRequest;
|
||||
import google.registry.proxy.quota.QuotaManager.QuotaResponse;
|
||||
import google.registry.proxy.quota.TokenStore.TimestampedInteger;
|
||||
import google.registry.testing.FakeClock;
|
||||
import java.util.concurrent.Future;
|
||||
import org.joda.time.DateTime;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.junit.runners.JUnit4;
|
||||
|
||||
/** Unit tests for {@link QuotaManager}. */
|
||||
@RunWith(JUnit4.class)
|
||||
public class QuotaManagerTest {
|
||||
|
||||
private static final String USER_ID = "theUser";
|
||||
|
||||
private final TokenStore tokenStore = mock(TokenStore.class);
|
||||
private final FakeClock clock = new FakeClock();
|
||||
|
||||
private QuotaManager quotaManager =
|
||||
new QuotaManager(tokenStore, MoreExecutors.newDirectExecutorService());
|
||||
private QuotaRequest request;
|
||||
private QuotaResponse response;
|
||||
|
||||
@Test
|
||||
public void testSuccess_requestApproved() {
|
||||
when(tokenStore.take(anyString())).thenReturn(TimestampedInteger.create(1, clock.nowUtc()));
|
||||
|
||||
request = QuotaRequest.create(USER_ID);
|
||||
response = quotaManager.acquireQuota(request);
|
||||
assertThat(response.success()).isTrue();
|
||||
assertThat(response.userId()).isEqualTo(USER_ID);
|
||||
assertThat(response.grantedTokenRefillTime()).isEqualTo(clock.nowUtc());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSuccess_requestDenied() {
|
||||
when(tokenStore.take(anyString())).thenReturn(TimestampedInteger.create(0, clock.nowUtc()));
|
||||
|
||||
request = QuotaRequest.create(USER_ID);
|
||||
response = quotaManager.acquireQuota(request);
|
||||
assertThat(response.success()).isFalse();
|
||||
assertThat(response.userId()).isEqualTo(USER_ID);
|
||||
assertThat(response.grantedTokenRefillTime()).isEqualTo(clock.nowUtc());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSuccess_rebate() throws Exception {
|
||||
DateTime grantedTokenRefillTime = clock.nowUtc();
|
||||
response = QuotaResponse.create(true, USER_ID, grantedTokenRefillTime);
|
||||
QuotaRebate rebate = QuotaRebate.create(response);
|
||||
Future<?> unusedFuture = quotaManager.releaseQuota(rebate);
|
||||
verify(tokenStore).scheduleRefresh();
|
||||
verify(tokenStore).put(USER_ID, grantedTokenRefillTime);
|
||||
verifyNoMoreInteractions(tokenStore);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,315 @@
|
|||
// 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.quota;
|
||||
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
import static google.registry.proxy.quota.QuotaConfig.SENTINEL_UNLIMITED_TOKENS;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.spy;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import google.registry.proxy.quota.TokenStore.TimestampedInteger;
|
||||
import google.registry.testing.FakeClock;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.Future;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
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;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
|
||||
/** Unit tests for {@link TokenStore}. */
|
||||
@RunWith(JUnit4.class)
|
||||
public class TokenStoreTest {
|
||||
|
||||
private final QuotaConfig quotaConfig = mock(QuotaConfig.class);
|
||||
private final FakeClock clock = new FakeClock();
|
||||
private final ScheduledExecutorService refreshExecutor = mock(ScheduledExecutorService.class);
|
||||
private final TokenStore tokenStore = spy(new TokenStore(quotaConfig, refreshExecutor, clock));
|
||||
private final String user = "theUser";
|
||||
private final String otherUser = "theOtherUser";
|
||||
|
||||
private DateTime assertTake(int grantAmount, int amountLeft, DateTime timestamp) {
|
||||
return assertTake(user, grantAmount, amountLeft, timestamp);
|
||||
}
|
||||
|
||||
private DateTime assertTake(String user, int grantAmount, int amountLeft, DateTime timestamp) {
|
||||
TimestampedInteger grantedToken = tokenStore.take(user);
|
||||
assertThat(grantedToken).isEqualTo(TimestampedInteger.create(grantAmount, timestamp));
|
||||
assertThat(tokenStore.getTokenForTests(user))
|
||||
.isEqualTo(TimestampedInteger.create(amountLeft, timestamp));
|
||||
return grantedToken.timestamp();
|
||||
}
|
||||
|
||||
private void assertPut(
|
||||
DateTime returnedTokenRefillTime, int amountAfterReturn, DateTime refillTime) {
|
||||
assertPut(user, returnedTokenRefillTime, amountAfterReturn, refillTime);
|
||||
}
|
||||
|
||||
private void assertPut(
|
||||
String user, DateTime returnedTokenRefillTime, int amountAfterReturn, DateTime refillTime) {
|
||||
tokenStore.put(user, returnedTokenRefillTime);
|
||||
assertThat(tokenStore.getTokenForTests(user))
|
||||
.isEqualTo(TimestampedInteger.create(amountAfterReturn, refillTime));
|
||||
}
|
||||
|
||||
private void submitAndWaitForTasks(ExecutorService executor, Runnable... tasks) {
|
||||
List<Future<?>> futures = new ArrayList<>();
|
||||
for (Runnable task : tasks) {
|
||||
futures.add(executor.submit(task));
|
||||
}
|
||||
futures.forEach(
|
||||
f -> {
|
||||
try {
|
||||
f.get();
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
when(quotaConfig.getRefreshPeriod()).thenReturn(Duration.standardSeconds(60));
|
||||
when(quotaConfig.getRefillPeriod(user)).thenReturn(Duration.standardSeconds(10));
|
||||
when(quotaConfig.getTokenAmount(user)).thenReturn(3);
|
||||
when(quotaConfig.getRefillPeriod(otherUser)).thenReturn(Duration.standardSeconds(15));
|
||||
when(quotaConfig.getTokenAmount(otherUser)).thenReturn(5);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSuccess_take() {
|
||||
// Take 3 tokens one by one.
|
||||
DateTime refillTime = clock.nowUtc();
|
||||
assertTake(1, 2, refillTime);
|
||||
assertTake(1, 1, refillTime);
|
||||
clock.advanceBy(Duration.standardSeconds(2));
|
||||
assertTake(1, 0, refillTime);
|
||||
|
||||
// Take 1 token, not enough tokens left.
|
||||
clock.advanceBy(Duration.standardSeconds(3));
|
||||
assertTake(0, 0, refillTime);
|
||||
|
||||
// Refill period passed. Take 1 token - success.
|
||||
clock.advanceBy(Duration.standardSeconds(6));
|
||||
refillTime = clock.nowUtc();
|
||||
assertTake(1, 2, refillTime);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSuccess_put_entryDoesNotExist() {
|
||||
tokenStore.put(user, clock.nowUtc());
|
||||
assertThat(tokenStore.getTokenForTests(user)).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSuccess_put() {
|
||||
DateTime refillTime = clock.nowUtc();
|
||||
|
||||
// Initialize the entry.
|
||||
DateTime grantedTokenRefillTime = assertTake(1, 2, refillTime);
|
||||
|
||||
// Put into full bucket.
|
||||
assertPut(grantedTokenRefillTime, 3, refillTime);
|
||||
assertPut(grantedTokenRefillTime, 3, refillTime);
|
||||
|
||||
clock.advanceBy(Duration.standardSeconds(3));
|
||||
|
||||
// Take 1 token out, put 1 back in.
|
||||
assertTake(1, 2, refillTime);
|
||||
assertPut(refillTime, 3, refillTime);
|
||||
|
||||
// Do not put old token back.
|
||||
grantedTokenRefillTime = assertTake(1, 2, refillTime);
|
||||
clock.advanceBy(Duration.standardSeconds(11));
|
||||
refillTime = clock.nowUtc();
|
||||
assertPut(grantedTokenRefillTime, 3, refillTime);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSuccess_takeAndPut() {
|
||||
DateTime refillTime = clock.nowUtc();
|
||||
|
||||
// Take 1 token.
|
||||
DateTime grantedTokenRefillTime1 = assertTake(1, 2, refillTime);
|
||||
|
||||
// Take 1 token.
|
||||
DateTime grantedTokenRefillTime2 = assertTake(1, 1, refillTime);
|
||||
|
||||
// Return first token.
|
||||
clock.advanceBy(Duration.standardSeconds(2));
|
||||
assertPut(grantedTokenRefillTime1, 2, refillTime);
|
||||
|
||||
// Refill time passed, second returned token discarded.
|
||||
clock.advanceBy(Duration.standardSeconds(10));
|
||||
refillTime = clock.nowUtc();
|
||||
assertPut(grantedTokenRefillTime2, 3, refillTime);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSuccess_multipleUsers() {
|
||||
DateTime refillTime1 = clock.nowUtc();
|
||||
DateTime refillTime2 = clock.nowUtc();
|
||||
|
||||
// Take 1 from first user.
|
||||
DateTime grantedTokenRefillTime1 = assertTake(user, 1, 2, refillTime1);
|
||||
|
||||
// Take 1 from second user.
|
||||
DateTime grantedTokenRefillTime2 = assertTake(otherUser, 1, 4, refillTime2);
|
||||
assertTake(otherUser, 1, 3, refillTime2);
|
||||
assertTake(otherUser, 1, 2, refillTime2);
|
||||
|
||||
// first user tokens refilled.
|
||||
clock.advanceBy(Duration.standardSeconds(10));
|
||||
refillTime1 = clock.nowUtc();
|
||||
DateTime grantedTokenRefillTime3 = assertTake(user, 1, 2, refillTime1);
|
||||
DateTime grantedTokenRefillTime4 = assertTake(otherUser, 1, 1, refillTime2);
|
||||
assertPut(user, grantedTokenRefillTime1, 2, refillTime1);
|
||||
assertPut(otherUser, grantedTokenRefillTime2, 2, refillTime2);
|
||||
|
||||
// second user tokens refilled.
|
||||
clock.advanceBy(Duration.standardSeconds(5));
|
||||
refillTime2 = clock.nowUtc();
|
||||
assertPut(user, grantedTokenRefillTime3, 3, refillTime1);
|
||||
assertPut(otherUser, grantedTokenRefillTime4, 5, refillTime2);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSuccess_refresh() {
|
||||
DateTime refillTime1 = clock.nowUtc();
|
||||
assertTake(user, 1, 2, refillTime1);
|
||||
|
||||
clock.advanceBy(Duration.standardSeconds(5));
|
||||
DateTime refillTime2 = clock.nowUtc();
|
||||
assertTake(otherUser, 1, 4, refillTime2);
|
||||
|
||||
clock.advanceBy(Duration.standardSeconds(55));
|
||||
|
||||
// Entry for user is 60s old, entry for otherUser is 55s old.
|
||||
tokenStore.refresh();
|
||||
assertThat(tokenStore.getTokenForTests(user)).isNull();
|
||||
assertThat(tokenStore.getTokenForTests(otherUser))
|
||||
.isEqualTo(TimestampedInteger.create(4, refillTime2));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSuccess_unlimitedQuota() {
|
||||
when(quotaConfig.hasUnlimitedTokens(user)).thenReturn(true);
|
||||
for (int i = 0; i < 10000; ++i) {
|
||||
assertTake(1, SENTINEL_UNLIMITED_TOKENS, clock.nowUtc());
|
||||
}
|
||||
for (int i = 0; i < 10000; ++i) {
|
||||
assertPut(clock.nowUtc(), SENTINEL_UNLIMITED_TOKENS, clock.nowUtc());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSuccess_noRefill() {
|
||||
when(quotaConfig.getRefillPeriod(user)).thenReturn(Duration.ZERO);
|
||||
DateTime refillTime = clock.nowUtc();
|
||||
assertTake(1, 2, refillTime);
|
||||
assertTake(1, 1, refillTime);
|
||||
assertTake(1, 0, refillTime);
|
||||
clock.advanceBy(Duration.standardDays(365));
|
||||
assertTake(0, 0, refillTime);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSuccess_noRefresh() {
|
||||
when(quotaConfig.getRefreshPeriod()).thenReturn(Duration.ZERO);
|
||||
DateTime refillTime = clock.nowUtc();
|
||||
assertTake(1, 2, refillTime);
|
||||
clock.advanceBy(Duration.standardDays(365));
|
||||
assertThat(tokenStore.getTokenForTests(user))
|
||||
.isEqualTo(TimestampedInteger.create(2, refillTime));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSuccess_concurrency() throws Exception {
|
||||
ExecutorService executor = Executors.newWorkStealingPool();
|
||||
final DateTime time1 = clock.nowUtc();
|
||||
submitAndWaitForTasks(
|
||||
executor,
|
||||
() -> tokenStore.take(user),
|
||||
() -> tokenStore.take(otherUser),
|
||||
() -> tokenStore.take(user),
|
||||
() -> tokenStore.take(otherUser));
|
||||
assertThat(tokenStore.getTokenForTests(user)).isEqualTo(TimestampedInteger.create(1, time1));
|
||||
assertThat(tokenStore.getTokenForTests(otherUser))
|
||||
.isEqualTo(TimestampedInteger.create(3, time1));
|
||||
|
||||
// No refill.
|
||||
clock.advanceBy(Duration.standardSeconds(5));
|
||||
submitAndWaitForTasks(
|
||||
executor, () -> tokenStore.take(user), () -> tokenStore.put(otherUser, time1));
|
||||
assertThat(tokenStore.getTokenForTests(user)).isEqualTo(TimestampedInteger.create(0, time1));
|
||||
assertThat(tokenStore.getTokenForTests(otherUser))
|
||||
.isEqualTo(TimestampedInteger.create(4, time1));
|
||||
|
||||
// First user refill.
|
||||
clock.advanceBy(Duration.standardSeconds(5));
|
||||
final DateTime time2 = clock.nowUtc();
|
||||
submitAndWaitForTasks(
|
||||
executor,
|
||||
() -> {
|
||||
tokenStore.put(user, time1);
|
||||
tokenStore.take(user);
|
||||
},
|
||||
() -> tokenStore.take(otherUser));
|
||||
assertThat(tokenStore.getTokenForTests(user)).isEqualTo(TimestampedInteger.create(2, time2));
|
||||
assertThat(tokenStore.getTokenForTests(otherUser))
|
||||
.isEqualTo(TimestampedInteger.create(3, time1));
|
||||
|
||||
// Second user refill.
|
||||
clock.advanceBy(Duration.standardSeconds(5));
|
||||
final DateTime time3 = clock.nowUtc();
|
||||
submitAndWaitForTasks(
|
||||
executor,
|
||||
() -> tokenStore.take(user),
|
||||
() -> {
|
||||
tokenStore.put(otherUser, time1);
|
||||
tokenStore.take(otherUser);
|
||||
});
|
||||
assertThat(tokenStore.getTokenForTests(user)).isEqualTo(TimestampedInteger.create(1, time2));
|
||||
assertThat(tokenStore.getTokenForTests(otherUser))
|
||||
.isEqualTo(TimestampedInteger.create(4, time3));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSuccess_scheduleRefresh() throws Exception {
|
||||
when(quotaConfig.getRefreshPeriod()).thenReturn(Duration.standardSeconds(5));
|
||||
|
||||
tokenStore.scheduleRefresh();
|
||||
|
||||
// Verify that a task is scheduled.
|
||||
ArgumentCaptor<Runnable> argument = ArgumentCaptor.forClass(Runnable.class);
|
||||
verify(refreshExecutor)
|
||||
.scheduleWithFixedDelay(
|
||||
argument.capture(), eq((long) 5), eq((long) 5), eq(TimeUnit.SECONDS));
|
||||
|
||||
// Verify that the scheduled task calls TokenStore.refresh().
|
||||
argument.getValue().run();
|
||||
verify(tokenStore).refresh();
|
||||
}
|
||||
}
|
8
proxy/src/test/java/google/registry/proxy/quota/testdata/quota_config_default.yaml
vendored
Normal file
8
proxy/src/test/java/google/registry/proxy/quota/testdata/quota_config_default.yaml
vendored
Normal file
|
@ -0,0 +1,8 @@
|
|||
refreshSeconds: 3600
|
||||
|
||||
defaultQuota:
|
||||
userId: []
|
||||
tokenAmount: 100
|
||||
refillSeconds: 60
|
||||
|
||||
customQuota: []
|
14
proxy/src/test/java/google/registry/proxy/quota/testdata/quota_config_duplicate.yaml
vendored
Normal file
14
proxy/src/test/java/google/registry/proxy/quota/testdata/quota_config_duplicate.yaml
vendored
Normal file
|
@ -0,0 +1,14 @@
|
|||
refreshSeconds: 3600
|
||||
|
||||
defaultQuota:
|
||||
userId: []
|
||||
tokenAmount: 100
|
||||
refillSeconds: 60
|
||||
|
||||
customQuota:
|
||||
- userId: ["abc", "def"]
|
||||
tokenAmount: 10
|
||||
refillSeconds: 60
|
||||
- userId: ["xyz123", "def", "luckycat"]
|
||||
tokenAmount: 500
|
||||
refillSeconds: 10
|
|
@ -0,0 +1,8 @@
|
|||
refreshSeconds: 0
|
||||
|
||||
defaultQuota:
|
||||
userId: []
|
||||
tokenAmount: 100
|
||||
refillSeconds: 0
|
||||
|
||||
customQuota: []
|
14
proxy/src/test/java/google/registry/proxy/quota/testdata/quota_config_regular.yaml
vendored
Normal file
14
proxy/src/test/java/google/registry/proxy/quota/testdata/quota_config_regular.yaml
vendored
Normal file
|
@ -0,0 +1,14 @@
|
|||
refreshSeconds: 3600
|
||||
|
||||
defaultQuota:
|
||||
userId: []
|
||||
tokenAmount: 100
|
||||
refillSeconds: 60
|
||||
|
||||
customQuota:
|
||||
- userId: ["abc", "def"]
|
||||
tokenAmount: 10
|
||||
refillSeconds: 60
|
||||
- userId: ["xyz123", "987lol", "luckycat"]
|
||||
tokenAmount: 500
|
||||
refillSeconds: 10
|
8
proxy/src/test/java/google/registry/proxy/quota/testdata/quota_config_unlimited_tokens.yaml
vendored
Normal file
8
proxy/src/test/java/google/registry/proxy/quota/testdata/quota_config_unlimited_tokens.yaml
vendored
Normal file
|
@ -0,0 +1,8 @@
|
|||
refreshSeconds: 3600
|
||||
|
||||
defaultQuota:
|
||||
userId: []
|
||||
tokenAmount: -1
|
||||
refillSeconds: 60
|
||||
|
||||
customQuota: []
|
23
proxy/src/test/java/google/registry/proxy/testdata/login.xml
vendored
Normal file
23
proxy/src/test/java/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
proxy/src/test/java/google/registry/proxy/testdata/login_response.xml
vendored
Normal file
12
proxy/src/test/java/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
proxy/src/test/java/google/registry/proxy/testdata/logout.xml
vendored
Normal file
7
proxy/src/test/java/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
proxy/src/test/java/google/registry/proxy/testdata/logout_response.xml
vendored
Normal file
12
proxy/src/test/java/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