Refactor to be more in line with a standard Gradle project structure

This commit is contained in:
Gus Brodman 2019-05-21 14:12:47 -04:00
parent 8fa45e8c76
commit a7a983bfed
3141 changed files with 99 additions and 100 deletions

View 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"],
)

View file

@ -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();
}
}

View file

@ -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();
}
}

View file

@ -0,0 +1,105 @@
// Copyright 2017 The Nomulus Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package google.registry.proxy;
import static com.google.common.truth.Truth.assertThat;
import 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);
}
}

View file

@ -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();
}
}

View file

@ -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);
}
}

View file

@ -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];
}
}
}

View 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");
}
}

View 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);
}
}

View file

@ -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);
}
}

View file

@ -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();
}
}

View file

@ -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);
}
}

View file

@ -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);
}
}

View file

@ -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();
}
}

View file

@ -0,0 +1,58 @@
// Copyright 2017 The Nomulus Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package google.registry.proxy.handler;
import static com.google.common.truth.Truth.assertThat;
import static java.nio.charset.StandardCharsets.US_ASCII;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.embedded.EmbeddedChannel;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
/** Unit tests for {@link HealthCheckHandler}. */
@RunWith(JUnit4.class)
public class HealthCheckHandlerTest {
private static final String CHECK_REQ = "REQUEST";
private static final String CHECK_RES = "RESPONSE";
private final HealthCheckHandler healthCheckHandler =
new HealthCheckHandler(CHECK_REQ, CHECK_RES);
private final EmbeddedChannel channel = new EmbeddedChannel(healthCheckHandler);
@Test
public void testSuccess_ResponseSent() {
ByteBuf input = Unpooled.wrappedBuffer(CHECK_REQ.getBytes(US_ASCII));
// No inbound message passed to the next handler.
assertThat(channel.writeInbound(input)).isFalse();
ByteBuf output = channel.readOutbound();
assertThat(channel.isActive()).isTrue();
assertThat(output.toString(US_ASCII)).isEqualTo(CHECK_RES);
}
@Test
public void testSuccess_IgnoreUnrecognizedRequest() {
String unrecognizedInput = "1234567";
ByteBuf input = Unpooled.wrappedBuffer(unrecognizedInput.getBytes(US_ASCII));
// No inbound message passed to the next handler.
assertThat(channel.writeInbound(input)).isFalse();
// No response is sent.
assertThat(channel.isActive()).isTrue();
assertThat((Object) channel.readOutbound()).isNull();
}
}

View file

@ -0,0 +1,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)));
}
}

View file

@ -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();
}
}

View file

@ -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();
}
}

View file

@ -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();
}
}

View file

@ -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();
}
}

View file

@ -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();
}
}

View file

@ -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();
}
}

View file

@ -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);
}
}

View file

@ -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();
}
}

View file

@ -0,0 +1,173 @@
// Copyright 2017 The Nomulus Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package google.registry.proxy.metric;
import static 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();
}
}

View file

@ -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();
}
}

View file

@ -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));
}
}

View file

@ -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");
}
}

View file

@ -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);
}
}

View file

@ -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();
}
}

View file

@ -0,0 +1,8 @@
refreshSeconds: 3600
defaultQuota:
userId: []
tokenAmount: 100
refillSeconds: 60
customQuota: []

View 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

View file

@ -0,0 +1,8 @@
refreshSeconds: 0
defaultQuota:
userId: []
tokenAmount: 100
refillSeconds: 0
customQuota: []

View 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

View file

@ -0,0 +1,8 @@
refreshSeconds: 3600
defaultQuota:
userId: []
tokenAmount: -1
refillSeconds: 60
customQuota: []

View 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>

View 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>

View 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>

View 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>