Add full prober WebWHOIS sequence functionality (#180)

* Initial Commit.

* Deleted unfinished features. Added ActionHandler and its Unit Tests.

* Included prober subproject in settings.gradle

* Added Protocol Class and its Basic Unit Tests

* Added Changes Suggested by jianglai

* Fixed Gitignore to take out AutoValue generated code

* Removed AutoValue java files

* Added gitignore within prober

* Removed all generated java

* Added Ssl and WebWhois Action Handlers and their unit tests in addition to the ProbingAction class

* Fixed build.gradle changes requested

* Removed Files irrelevant to current pull request

* Minor fixes to ActionHandler, as responded in comments, removed package-info, and updated settings.gradle

* Fully Updated ActionHandler (missing updated JavaDoc)

* Added changed Protocol and both Inbound and Outbound Markers

* Removed AutoVaue ignore clause from .gitignore

* removed unneccessary dependencies in build.gradle

* Fixed Javadoc and comments for ActionHandler

* Fixed comments and JavaDoc on other files

* EOL added

* Removed Unnecessary Files

* fixed .gradle files styles

* Removed outbound message from ActionHandler's fields and renamed Marker Interfaces

* Fixed javadoc for Marker Interfaced

* Modified Comments on ActionHandler

* Removed LocalAddress from Protocol

* Fixed Travis Build Issues

* Rebased to Master and added in modified Handlers and ProbingAction

* Fixed changes suggested by CydeWeys

* Added missing license headers and JavaDoc

* Minor fix in NewChannelAction JavaDoc

* Minor Style Fix

* Full WebWhoIs Sequence Added

* fixed build issues

* Refactored by responses suggested by jianglai.

* Initial Commit.

* Deleted unfinished features. Added ActionHandler and its Unit Tests.

* Included prober subproject in settings.gradle

* Added Protocol Class and its Basic Unit Tests

* Added Changes Suggested by jianglai

* Fixed Gitignore to take out AutoValue generated code

* Removed AutoValue java files

* Added gitignore within prober

* Removed all generated java

* Final Changes in .gitignore

* Added Ssl and WebWhois Action Handlers and their unit tests in addition to the ProbingAction class

* Fixed build.gradle changes requested

* Removed Files irrelevant to current pull request

* Fixed changes suggested by CydeWeys

* Minor fixes to ActionHandler, as responded in comments, removed package-info, and updated settings.gradle

* Fully Updated ActionHandler (missing updated JavaDoc)

* Added changed Protocol and both Inbound and Outbound Markers

* Removed AutoVaue ignore clause from .gitignore

* removed unneccessary dependencies in build.gradle

* Fixed Javadoc and comments for ActionHandler

* Fixed comments and JavaDoc on other files

* EOL added

* Removed Unnecessary Files

* fixed .gradle files styles

* Rebased to Master and added in modified Handlers and ProbingAction

* Added missing license headers and JavaDoc

* Minor fix in NewChannelAction JavaDoc

* Minor Style Fix

* Full WebWhoIs Sequence Added

* fixed build issues

* Refactored by responses suggested by jianglai.

* Minor Style Fixes

* Minor Style Fixes

* Updated build.gradle file

* Updated build.gradle file

* Modified license header dates

* Modified license header dates

* Updated WebWhois tests.

* Updated WebWhois tests.

* Refactored WebWhois to accomodate jianglai's suggested changes and modified tests to reflect this refactoring

* Refactored WebWhois to accomodate jianglai's suggested changes and modified tests to reflect this refactoring

* SpotlessApply run to fix style issues

* SpotlessApply run to fix style issues

* Added license header and newline where appropriate.

* Added license header and newline where appropriate.

* Javadoc style fix in tests and removed unused methods

* Javadoc style fix in tests and removed unused methods

* Refactored ProbingAction to minimize number of unnecessary methods

* Refactored ProbingAction to minimize number of unnecessary methods

* Modified tests for WebWhois according to changes suggested by laijiang.

* Modified tests for WebWhois according to changes suggested by laijiang.

* Removed TestProvider from TestUtils.

* Removed TestProvider from TestUtils.

* Rebased to master

* Updated issues in rebasing

* Minor style change on prober/build.gradle

* Fixed warnings for java compilation

* Fixed files to pass all style tests

* Removed outbound message from ActionHandler's fields and renamed Marker Interfaces

* Initial Commit.

* Deleted unfinished features. Added ActionHandler and its Unit Tests.

* Included prober subproject in settings.gradle

* Added Protocol Class and its Basic Unit Tests

* Added Changes Suggested by jianglai

* Fixed Gitignore to take out AutoValue generated code

* Removed AutoValue java files

* Added gitignore within prober

* Removed all generated java

* Final Changes in .gitignore

* Added Ssl and WebWhois Action Handlers and their unit tests in addition to the ProbingAction class

* Fixed build.gradle changes requested

* Removed Files irrelevant to current pull request

* Fixed changes suggested by CydeWeys

* Fixed changes suggested by CydeWeys

* Fixed changes suggested by CydeWeys

* Minor fixes to ActionHandler, as responded in comments, removed package-info, and updated settings.gradle

* Fully Updated ActionHandler (missing updated JavaDoc)

* Added changed Protocol and both Inbound and Outbound Markers

* Removed AutoVaue ignore clause from .gitignore

* removed unneccessary dependencies in build.gradle

* Fixed Javadoc and comments for ActionHandler

* Fixed comments and JavaDoc on other files

* EOL added

* Removed Unnecessary Files

* fixed .gradle files styles

* Removed outbound message from ActionHandler's fields and renamed Marker Interfaces

* Fixed javadoc for Marker Interfaced

* Fixed javadoc for Marker Interfaced

* Modified Comments on ActionHandler

* Modified Comments on ActionHandler

* Removed LocalAddress from Protocol

* Removed LocalAddress from Protocol

* Fixed Travis Build Issues

* Fixed Travis Build Issues

* Rebased to Master and added in modified Handlers and ProbingAction

* Rebased to Master and added in modified Handlers and ProbingAction

* Rebased to Master and added in modified Handlers and ProbingAction

* Added missing license headers and JavaDoc

* Added missing license headers and JavaDoc

* Minor fix in NewChannelAction JavaDoc

* Minor fix in NewChannelAction JavaDoc

* Minor Style Fix

* Minor Style Fix

* Full WebWhoIs Sequence Added

* Full WebWhoIs Sequence Added

* fixed build issues

* fixed build issues

* Refactored by responses suggested by jianglai.

* Refactored by responses suggested by jianglai.

* Minor Style Fixes

* Minor Style Fixes

* Updated build.gradle file

* Updated build.gradle file

* Modified license header dates

* Modified license header dates

* Updated WebWhois tests.

* Updated WebWhois tests.

* Refactored WebWhois to accomodate jianglai's suggested changes and modified tests to reflect this refactoring

* Refactored WebWhois to accomodate jianglai's suggested changes and modified tests to reflect this refactoring

* SpotlessApply run to fix style issues

* SpotlessApply run to fix style issues

* Added license header and newline where appropriate.

* Added license header and newline where appropriate.

* Javadoc style fix in tests and removed unused methods

* Javadoc style fix in tests and removed unused methods

* Refactored ProbingAction to minimize number of unnecessary methods

* Refactored ProbingAction to minimize number of unnecessary methods

* Modified tests for WebWhois according to changes suggested by laijiang.

* Modified tests for WebWhois according to changes suggested by laijiang.

* Removed TestProvider from TestUtils.

* Removed TestProvider from TestUtils.

* Rebased to master

* Updated issues in rebasing

* Minor style change on prober/build.gradle

* Fixed warnings for java compilation

* Fixed files to pass all style tests

* Fixed changes suggested by CydeWeys

* Rebased to Master and added in modified Handlers and ProbingAction

* Added missing license headers and JavaDoc

* Added missing license headers and JavaDoc

* Minor fix in NewChannelAction JavaDoc

* Minor fix in NewChannelAction JavaDoc

* Minor Style Fix

* Minor Style Fix

* Full WebWhoIs Sequence Added

* Full WebWhoIs Sequence Added

* fixed build issues

* fixed build issues

* Refactored by responses suggested by jianglai.

* Refactored by responses suggested by jianglai.

* Minor Style Fixes

* Minor Style Fixes

* Updated build.gradle file

* Updated build.gradle file

* Modified license header dates

* Modified license header dates

* Updated WebWhois tests.

* Updated WebWhois tests.

* Refactored WebWhois to accomodate jianglai's suggested changes and modified tests to reflect this refactoring

* Refactored WebWhois to accomodate jianglai's suggested changes and modified tests to reflect this refactoring

* SpotlessApply run to fix style issues

* SpotlessApply run to fix style issues

* Added license header and newline where appropriate.

* Added license header and newline where appropriate.

* Javadoc style fix in tests and removed unused methods

* Javadoc style fix in tests and removed unused methods

* Refactored ProbingAction to minimize number of unnecessary methods

* Refactored ProbingAction to minimize number of unnecessary methods

* Modified tests for WebWhois according to changes suggested by laijiang.

* Modified tests for WebWhois according to changes suggested by laijiang.

* Removed TestProvider from TestUtils.

* Removed TestProvider from TestUtils.

* Rebased to master

* Updated issues in rebasing

* Minor style change on prober/build.gradle

* Fixed warnings for java compilation

* Fixed files to pass all style tests

* Minor syle fixes after succesful rebase onto master
This commit is contained in:
Aman Sanger 2019-08-08 15:17:41 -04:00 committed by GitHub
parent c7478fc52b
commit 27b81ba898
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
35 changed files with 3545 additions and 73 deletions

View file

@ -0,0 +1,162 @@
// Copyright 2019 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.monitoring.blackbox;
import static com.google.common.truth.Truth.assertThat;
import static google.registry.monitoring.blackbox.ProbingAction.CONNECTION_FUTURE_KEY;
import static java.nio.charset.StandardCharsets.US_ASCII;
import static java.nio.charset.StandardCharsets.UTF_8;
import com.google.common.collect.ImmutableList;
import google.registry.monitoring.blackbox.handlers.ActionHandler;
import google.registry.monitoring.blackbox.handlers.ConversionHandler;
import google.registry.monitoring.blackbox.handlers.NettyRule;
import google.registry.monitoring.blackbox.handlers.TestActionHandler;
import google.registry.monitoring.blackbox.messages.TestMessage;
import io.netty.bootstrap.Bootstrap;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelHandler;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.embedded.EmbeddedChannel;
import io.netty.channel.local.LocalAddress;
import io.netty.channel.local.LocalChannel;
import io.netty.channel.nio.NioEventLoopGroup;
import org.joda.time.Duration;
import org.junit.Ignore;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
/**
* Unit tests for {@link ProbingAction} subtypes
*
* <p>Attempts to test how well each {@link ProbingAction} works with an {@link ActionHandler}
* subtype when receiving to all possible types of responses</p>
*/
@RunWith(JUnit4.class)
public class ProbingActionTest {
private static final String TEST_MESSAGE = "MESSAGE_TEST";
private static final String SECONDARY_TEST_MESSAGE = "SECONDARY_MESSAGE_TEST";
private static final String PROTOCOL_NAME = "TEST_PROTOCOL";
private static final String ADDRESS_NAME = "TEST_ADDRESS";
private static final int TEST_PORT = 0;
private static final EventLoopGroup eventLoopGroup = new NioEventLoopGroup(1);
/**
* Used for testing how well probing step can create connection to blackbox server
*/
@Rule
public NettyRule nettyRule = new NettyRule(eventLoopGroup);
/**
* We use custom Test {@link ActionHandler} and {@link ConversionHandler} so test depends only on
* {@link ProbingAction}
*/
private ActionHandler testHandler = new TestActionHandler();
private ChannelHandler conversionHandler = new ConversionHandler();
//TODO - Currently, this test fails to receive outbound messages from the embedded channel, which
// we will fix in a later release.
@Ignore
@Test
public void testSuccess_existingChannel() {
//setup
EmbeddedChannel channel = new EmbeddedChannel(conversionHandler, testHandler);
channel.attr(CONNECTION_FUTURE_KEY).set(channel.newSucceededFuture());
// Sets up a Protocol corresponding to when a connection exists.
Protocol protocol = Protocol.builder()
.setHandlerProviders(ImmutableList.of(() -> conversionHandler, () -> testHandler))
.setName(PROTOCOL_NAME)
.setPort(TEST_PORT)
.setPersistentConnection(true)
.build();
// Sets up a ProbingAction that creates a channel using test specified attributes.
ProbingAction action = ProbingAction.builder()
.setChannel(channel)
.setProtocol(protocol)
.setDelay(Duration.ZERO)
.setOutboundMessage(new TestMessage(TEST_MESSAGE))
.setHost("")
.build();
//tests main function of ProbingAction
ChannelFuture future = action.call();
//Obtains the outboundMessage passed through pipeline after delay
Object msg = null;
while (msg == null) {
msg = channel.readOutbound();
}
//tests the passed message is exactly what we expect
assertThat(msg).isInstanceOf(ByteBuf.class);
String request = ((ByteBuf) msg).toString(UTF_8);
assertThat(request).isEqualTo(TEST_MESSAGE);
// Ensures that we haven't marked future as done until response is received.
assertThat(future.isDone()).isFalse();
//after writing inbound, we should have a success
channel.writeInbound(Unpooled.wrappedBuffer(SECONDARY_TEST_MESSAGE.getBytes(US_ASCII)));
assertThat(future.isSuccess()).isTrue();
assertThat(testHandler.toString()).isEqualTo(SECONDARY_TEST_MESSAGE);
}
@Test
public void testSuccess_newChannel() throws Exception {
//setup
LocalAddress address = new LocalAddress(ADDRESS_NAME);
Bootstrap bootstrap = new Bootstrap()
.group(eventLoopGroup)
.channel(LocalChannel.class);
// Sets up a Protocol corresponding to when a new connection is created.
Protocol protocol = Protocol.builder()
.setHandlerProviders(ImmutableList.of(() -> conversionHandler, () -> testHandler))
.setName(PROTOCOL_NAME)
.setPort(TEST_PORT)
.setPersistentConnection(false)
.build();
nettyRule.setUpServer(address);
// Sets up a ProbingAction with existing channel using test specified attributes.
ProbingAction action = ProbingAction.builder()
.setBootstrap(bootstrap)
.setProtocol(protocol)
.setDelay(Duration.ZERO)
.setOutboundMessage(new TestMessage(TEST_MESSAGE))
.setHost(ADDRESS_NAME)
.build();
//tests main function of ProbingAction
ChannelFuture future = action.call();
//Tests to see if message is properly sent to remote server
nettyRule.assertReceivedMessage(TEST_MESSAGE);
future = future.syncUninterruptibly();
//Tests to see that, since server responds, we have set future to true
assertThat(future.isSuccess()).isTrue();
assertThat(((TestActionHandler) testHandler).getResponse().toString()).isEqualTo(TEST_MESSAGE);
}
}

View file

@ -0,0 +1,92 @@
// Copyright 2019 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.monitoring.blackbox;
import static com.google.common.truth.Truth.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.doCallRealMethod;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import google.registry.monitoring.blackbox.tokens.Token;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
import org.mockito.Mockito;
@RunWith(JUnit4.class)
public class ProbingSequenceTest {
private ProbingStep firstStep;
private ProbingStep secondStep;
private ProbingStep thirdStep;
private Token testToken;
private ProbingStep setupMockStep() {
ProbingStep mock = Mockito.mock(ProbingStep.class);
doCallRealMethod().when(mock).nextStep(any(ProbingStep.class));
doCallRealMethod().when(mock).nextStep();
return mock;
}
@Before
public void setup() {
firstStep = setupMockStep();
secondStep = setupMockStep();
thirdStep = setupMockStep();
testToken = Mockito.mock(Token.class);
}
@Test
public void testSequenceBasicConstruction_Success() {
ProbingSequence sequence = new ProbingSequence.Builder(testToken)
.addStep(firstStep)
.addStep(secondStep)
.addStep(thirdStep)
.build();
assertThat(firstStep.nextStep()).isEqualTo(secondStep);
assertThat(secondStep.nextStep()).isEqualTo(thirdStep);
assertThat(thirdStep.nextStep()).isEqualTo(firstStep);
sequence.start();
verify(firstStep, times(1)).accept(testToken);
}
@Test
public void testSequenceAdvancedConstruction_Success() {
ProbingSequence sequence = new ProbingSequence.Builder(testToken)
.addStep(thirdStep)
.addStep(secondStep)
.markFirstRepeated()
.addStep(firstStep)
.build();
assertThat(firstStep.nextStep()).isEqualTo(secondStep);
assertThat(secondStep.nextStep()).isEqualTo(firstStep);
assertThat(thirdStep.nextStep()).isEqualTo(secondStep);
sequence.start();
verify(thirdStep, times(1)).accept(testToken);
}
}

View file

@ -0,0 +1,202 @@
// Copyright 2019 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.monitoring.blackbox;
import static com.google.common.truth.Truth.assertThat;
import static google.registry.monitoring.blackbox.ProbingAction.CONNECTION_FUTURE_KEY;
import static java.nio.charset.StandardCharsets.US_ASCII;
import static java.nio.charset.StandardCharsets.UTF_8;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import com.google.common.collect.ImmutableList;
import google.registry.monitoring.blackbox.exceptions.UndeterminedStateException;
import google.registry.monitoring.blackbox.handlers.ActionHandler;
import google.registry.monitoring.blackbox.handlers.ConversionHandler;
import google.registry.monitoring.blackbox.handlers.NettyRule;
import google.registry.monitoring.blackbox.handlers.TestActionHandler;
import google.registry.monitoring.blackbox.messages.OutboundMessageType;
import google.registry.monitoring.blackbox.messages.TestMessage;
import google.registry.monitoring.blackbox.tokens.Token;
import io.netty.bootstrap.Bootstrap;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandler;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.embedded.EmbeddedChannel;
import io.netty.channel.local.LocalAddress;
import io.netty.channel.local.LocalChannel;
import io.netty.channel.nio.NioEventLoopGroup;
import org.joda.time.Duration;
import org.junit.Ignore;
import org.junit.Rule;
import org.junit.Test;
import org.mockito.Mockito;
/**
* Unit Tests for {@link ProbingSequence}s and {@link ProbingStep}s and their specific
* implementations
*/
public class ProbingStepTest {
/**
* Basic Constants necessary for tests
*/
private static final String ADDRESS_NAME = "TEST_ADDRESS";
private static final String PROTOCOL_NAME = "TEST_PROTOCOL";
private static final int PROTOCOL_PORT = 0;
private static final String TEST_MESSAGE = "TEST_MESSAGE";
private static final String SECONDARY_TEST_MESSAGE = "SECONDARY_TEST_MESSAGE";
private static final LocalAddress ADDRESS = new LocalAddress(ADDRESS_NAME);
private final EventLoopGroup eventLoopGroup = new NioEventLoopGroup(1);
private final Bootstrap bootstrap = new Bootstrap()
.group(eventLoopGroup)
.channel(LocalChannel.class);
/**
* Used for testing how well probing step can create connection to blackbox server
*/
@Rule
public NettyRule nettyRule = new NettyRule(eventLoopGroup);
/**
* The two main handlers we need in any test pipeline used that connects to {@link NettyRule's
* server}
**/
private ActionHandler testHandler = new TestActionHandler();
private ChannelHandler conversionHandler = new ConversionHandler();
/**
* Creates mock {@link Token} object that returns the host and returns unchanged message when
* modifying it.
*/
private Token testToken(String host) throws UndeterminedStateException {
Token token = Mockito.mock(Token.class);
doReturn(host).when(token).host();
doAnswer(answer -> answer.getArgument(0)).when(token)
.modifyMessage(any(OutboundMessageType.class));
return token;
}
@Test
public void testNewChannel() throws Exception {
// Sets up Protocol for when we create a new channel.
Protocol testProtocol = Protocol.builder()
.setHandlerProviders(ImmutableList.of(() -> conversionHandler, () -> testHandler))
.setName(PROTOCOL_NAME)
.setPort(PROTOCOL_PORT)
.setPersistentConnection(false)
.build();
// Sets up our main step (firstStep) and throwaway step (dummyStep).
ProbingStep firstStep = ProbingStep.builder()
.setBootstrap(bootstrap)
.setDuration(Duration.ZERO)
.setMessageTemplate(new TestMessage(TEST_MESSAGE))
.setProtocol(testProtocol)
.build();
//Sets up mock dummy step that returns succeeded promise when we successfully reach it.
ProbingStep dummyStep = Mockito.mock(ProbingStep.class);
firstStep.nextStep(dummyStep);
// Sets up testToken to return arbitrary values, and no channel. Used when we create a new
// channel.
Token testToken = testToken(ADDRESS_NAME);
//Set up blackbox server that receives our messages then echoes them back to us
nettyRule.setUpServer(ADDRESS);
//checks that the ProbingSteps are appropriately pointing to each other
assertThat(firstStep.nextStep()).isEqualTo(dummyStep);
//Call accept on the first step, which should send our message to the server, which will then be
//echoed back to us, causing us to move to the next step
firstStep.accept(testToken);
//checks that we have appropriately sent the write message to server
nettyRule.assertReceivedMessage(TEST_MESSAGE);
//checks that when the future is successful, we pass down the requisite token
verify(dummyStep, times(1)).accept(any(Token.class));
}
//TODO - Currently, this test fails to receive outbound messages from the embedded channel, which
// we will fix in a later release.
@Ignore
@Test
public void testWithSequence_ExistingChannel() throws Exception {
// Sets up Protocol for when a channel already exists.
Protocol testProtocol = Protocol.builder()
.setHandlerProviders(ImmutableList.of(() -> conversionHandler, () -> testHandler))
.setName(PROTOCOL_NAME)
.setPort(PROTOCOL_PORT)
.setPersistentConnection(true)
.build();
// Sets up our main step (firstStep) and throwaway step (dummyStep).
ProbingStep firstStep = ProbingStep.builder()
.setBootstrap(bootstrap)
.setDuration(Duration.ZERO)
.setMessageTemplate(new TestMessage(TEST_MESSAGE))
.setProtocol(testProtocol)
.build();
//Sets up mock dummy step that returns succeeded promise when we successfully reach it.
ProbingStep dummyStep = Mockito.mock(ProbingStep.class);
firstStep.nextStep(dummyStep);
// Sets up an embedded channel to contain the two handlers we created already.
EmbeddedChannel channel = new EmbeddedChannel(conversionHandler, testHandler);
//Assures that the channel has a succeeded connectionFuture.
channel.attr(CONNECTION_FUTURE_KEY).set(channel.newSucceededFuture());
// Sets up testToken to return arbitrary value, and the embedded channel. Used for when the
// ProbingStep generates an ExistingChannelAction.
Token testToken = testToken("");
doReturn(channel).when(testToken).channel();
//checks that the ProbingSteps are appropriately pointing to each other
assertThat(firstStep.nextStep()).isEqualTo(dummyStep);
//Call accept on the first step, which should send our message through the EmbeddedChannel
// pipeline
firstStep.accept(testToken);
Object msg = channel.readOutbound();
while (msg == null) {
msg = channel.readOutbound();
}
//Ensures the accurate message is sent down the pipeline
assertThat(((ByteBuf) channel.readOutbound()).toString(UTF_8)).isEqualTo(TEST_MESSAGE);
//Write response to our message down EmbeddedChannel pipeline
channel.writeInbound(Unpooled.wrappedBuffer(SECONDARY_TEST_MESSAGE.getBytes(US_ASCII)));
//At this point, we should have received the message, so the future obtained should be marked
// as a success
verify(dummyStep, times(1)).accept(any(Token.class));
}
}

View file

@ -0,0 +1,70 @@
// Copyright 2019 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.monitoring.blackbox;
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.FullHttpRequest;
import io.netty.handler.codec.http.FullHttpResponse;
import io.netty.handler.codec.http.HttpMethod;
import io.netty.handler.codec.http.HttpResponseStatus;
import io.netty.handler.codec.http.HttpVersion;
/**
* Utility class for various helper methods used in testing.
*/
public class TestUtils {
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;
}
/**
* Creates HttpResponse given status, redirection location, and other necessary inputs
*/
public static FullHttpResponse makeRedirectResponse(
HttpResponseStatus status, String location, boolean keepAlive) {
FullHttpResponse response = makeHttpResponse("", status);
response.headers().set("content-type", "text/plain");
if (location != null) {
response.headers().set("location", location);
}
if (keepAlive) {
response.headers().set("connection", "keep-alive");
}
return response;
}
}

View file

@ -0,0 +1,60 @@
// Copyright 2019 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.monitoring.blackbox.handlers;
import static java.nio.charset.StandardCharsets.US_ASCII;
import static java.nio.charset.StandardCharsets.UTF_8;
import google.registry.monitoring.blackbox.messages.InboundMessageType;
import google.registry.monitoring.blackbox.messages.OutboundMessageType;
import google.registry.monitoring.blackbox.messages.TestMessage;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelDuplexHandler;
import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelPromise;
/**
* {@link ChannelHandler} used in tests to convert {@link OutboundMessageType} to to {@link
* ByteBuf}s and convert {@link ByteBuf}s to {@link InboundMessageType}
*
* <p>Specific type of {@link OutboundMessageType} and {@link InboundMessageType}
* used for conversion is the {@link TestMessage} type.</p>
*/
public class ConversionHandler extends ChannelDuplexHandler {
/**
* Handles inbound conversion
*/
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
ByteBuf buf = (ByteBuf) msg;
ctx.fireChannelRead(new TestMessage(buf.toString(UTF_8)));
buf.release();
}
/**
* Handles outbound conversion
*/
@Override
public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise)
throws Exception {
String message = msg.toString();
ByteBuf buf = Unpooled.wrappedBuffer(message.getBytes(US_ASCII));
super.write(ctx, buf, promise);
}
}

View file

@ -0,0 +1,244 @@
// Copyright 2019 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.monitoring.blackbox.handlers;
import static com.google.common.base.Preconditions.checkState;
import static com.google.common.truth.Truth.assertThat;
import static google.registry.monitoring.blackbox.ProbingAction.REMOTE_ADDRESS_KEY;
import static google.registry.monitoring.blackbox.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.collect.ImmutableList;
import com.google.common.truth.ThrowableSubject;
import google.registry.monitoring.blackbox.ProbingActionTest;
import google.registry.monitoring.blackbox.ProbingStepTest;
import google.registry.monitoring.blackbox.Protocol;
import google.registry.monitoring.blackbox.testservers.TestServer;
import io.netty.bootstrap.Bootstrap;
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.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>Code based on and almost identical to {@code NettyRule} in the proxy.
* Used in {@link SslClientInitializerTest}, {@link ProbingActionTest}, and {@link ProbingStepTest}
* </p>
*/
public final class NettyRule extends ExternalResource {
private final EventLoopGroup eventLoopGroup;
// 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;
// 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.
public NettyRule() {
eventLoopGroup = new NioEventLoopGroup(1);
}
public NettyRule(EventLoopGroup e) {
eventLoopGroup = e;
}
private static void writeToChannelAndFlush(Channel channel, String data) {
ChannelFuture unusedFuture =
channel.writeAndFlush(Unpooled.wrappedBuffer(data.getBytes(US_ASCII)));
}
/**
* Sets up a server channel bound to the given local address.
*/
public void setUpServer(LocalAddress localAddress, ChannelHandler... handlers) {
checkState(echoHandler == null, "Can't call setUpServer twice");
echoHandler = new EchoHandler();
new TestServer(eventLoopGroup, localAddress,
ImmutableList.<ChannelHandler>builder().add(handlers).add(echoHandler).build());
}
/**
* Sets up a client channel connecting to the give local address.
*/
void setUpClient(
LocalAddress localAddress,
Protocol protocol,
String host,
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)
.attr(REMOTE_ADDRESS_KEY, host);
channel = b.connect(localAddress).syncUninterruptibly().channel();
}
private void checkReady() {
checkState(channel != null, "Must call setUpClient to finish NettyRule setup");
}
/**
* Test that custom setup to send message to current server sends right message
*/
public void assertReceivedMessage(String message) throws Exception {
assertThat(echoHandler.getRequestFuture().get()).isEqualTo(message);
}
/**
* 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())));
}
@Override
protected void after() {
Future<?> unusedFuture = eventLoopGroup.shutdownGracefully();
}
/**
* A handler that echoes back its inbound message. The message is also saved in a promise for
* inspection later.
*/
public static class EchoHandler extends ChannelInboundHandlerAdapter {
private final CompletableFuture<String> requestFuture = new CompletableFuture<>();
public 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));
}
}
}

View file

@ -0,0 +1,214 @@
// Copyright 2019 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.monitoring.blackbox.handlers;
import static com.google.common.truth.Truth.assertThat;
import static google.registry.monitoring.blackbox.ProbingAction.REMOTE_ADDRESS_KEY;
import static google.registry.monitoring.blackbox.Protocol.PROTOCOL_KEY;
import static google.registry.monitoring.blackbox.handlers.SslInitializerTestUtils.getKeyPair;
import static google.registry.monitoring.blackbox.handlers.SslInitializerTestUtils.setUpSslChannel;
import static google.registry.monitoring.blackbox.handlers.SslInitializerTestUtils.signKeyPair;
import com.google.common.collect.ImmutableList;
import google.registry.monitoring.blackbox.Protocol;
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;
/**
* Fake protocol saved in channel attribute.
*/
private static final Protocol PROTOCOL = Protocol.builder()
.setName("ssl")
.setPort(SSL_PORT)
.setHandlerProviders(ImmutableList.of())
.setPersistentConnection(false)
.build();
@Rule
public NettyRule nettyRule = new NettyRule();
@Parameter(0)
public SslProvider sslProvider;
/**
* Saves the SNI hostname received by the server, if sent by the client.
*/
private String sniHostReceived;
// 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};
}
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);
channel.attr(REMOTE_ADDRESS_KEY).set(SSL_HOST);
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, SSL_HOST, 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, SSL_HOST, 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, SSL_HOST, 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 2019 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.monitoring.blackbox.handlers;
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}
*/
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,42 @@
// Copyright 2019 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.monitoring.blackbox.handlers;
import google.registry.monitoring.blackbox.exceptions.FailureException;
import google.registry.monitoring.blackbox.exceptions.UndeterminedStateException;
import google.registry.monitoring.blackbox.messages.InboundMessageType;
import io.netty.channel.ChannelHandlerContext;
/**
* Concrete implementation of {@link ActionHandler} that does nothing different from parent class
* other than store and return the {@code inboundMessage}
*/
public class TestActionHandler extends ActionHandler {
private InboundMessageType receivedMessage;
@Override
public void channelRead0(ChannelHandlerContext ctx, InboundMessageType inboundMessage)
throws FailureException, UndeterminedStateException {
receivedMessage = inboundMessage;
super.channelRead0(ctx, inboundMessage);
}
public InboundMessageType getResponse() {
return receivedMessage;
}
}

View file

@ -0,0 +1,232 @@
// Copyright 2019 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.monitoring.blackbox.handlers;
import static com.google.common.truth.Truth.assertThat;
import static google.registry.monitoring.blackbox.ProbingAction.CONNECTION_FUTURE_KEY;
import static google.registry.monitoring.blackbox.Protocol.PROTOCOL_KEY;
import static google.registry.monitoring.blackbox.TestUtils.makeHttpGetRequest;
import static google.registry.monitoring.blackbox.TestUtils.makeHttpResponse;
import static google.registry.monitoring.blackbox.TestUtils.makeRedirectResponse;
import com.google.common.collect.ImmutableList;
import google.registry.monitoring.blackbox.Protocol;
import google.registry.monitoring.blackbox.exceptions.FailureException;
import google.registry.monitoring.blackbox.messages.HttpRequestMessage;
import google.registry.monitoring.blackbox.messages.HttpResponseMessage;
import google.registry.monitoring.blackbox.testservers.TestServer;
import io.netty.bootstrap.Bootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelHandler;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.embedded.EmbeddedChannel;
import io.netty.channel.local.LocalAddress;
import io.netty.channel.local.LocalChannel;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.handler.codec.http.FullHttpResponse;
import io.netty.handler.codec.http.HttpResponseStatus;
import javax.inject.Provider;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
/**
* Unit tests for {@link WebWhoisActionHandler}.
*
* <p>Attempts to test how well {@link WebWhoisActionHandler} works
* when responding to all possible types of responses </p>
*/
@RunWith(JUnit4.class)
public class WebWhoisActionHandlerTest {
private static final int HTTP_PORT = 80;
private static final String HTTP_REDIRECT = "http://";
private static final String TARGET_HOST = "whois.nic.tld";
private static final String DUMMY_URL = "__WILL_NOT_WORK__";
private final Protocol standardProtocol = Protocol.builder()
.setHandlerProviders(ImmutableList.of(() -> new WebWhoisActionHandler(
null, null, null, null)))
.setName("http")
.setPersistentConnection(false)
.setPort(HTTP_PORT)
.build();
private EmbeddedChannel channel;
private ActionHandler actionHandler;
private Provider<? extends ChannelHandler> actionHandlerProvider;
private Protocol initialProtocol;
private HttpRequestMessage msg;
/**
* Creates default protocol with empty list of handlers and specified other inputs
*/
private Protocol createProtocol(String name, int port, boolean persistentConnection) {
return Protocol.builder()
.setName(name)
.setPort(port)
.setHandlerProviders(ImmutableList.of(actionHandlerProvider))
.setPersistentConnection(persistentConnection)
.build();
}
/**
* Initializes new WebWhoisActionHandler
*/
private void setupActionHandler(Bootstrap bootstrap, HttpRequestMessage messageTemplate) {
actionHandler = new WebWhoisActionHandler(
bootstrap,
standardProtocol,
standardProtocol,
messageTemplate
);
actionHandlerProvider = () -> actionHandler;
}
/**
* Sets up testing channel with requisite attributes
*/
private void setupChannel(Protocol protocol) {
channel = new EmbeddedChannel(actionHandler);
channel.attr(PROTOCOL_KEY).set(protocol);
channel.attr(CONNECTION_FUTURE_KEY).set(channel.newSucceededFuture());
}
private Bootstrap makeBootstrap(EventLoopGroup group) {
return new Bootstrap()
.group(group)
.channel(LocalChannel.class);
}
private void setup(String hostName, Bootstrap bootstrap, boolean persistentConnection) {
msg = new HttpRequestMessage(makeHttpGetRequest(hostName, ""));
setupActionHandler(bootstrap, msg);
initialProtocol = createProtocol("testProtocol", 0, persistentConnection);
}
@Test
public void testBasic_responseOk() {
//setup
setup("", null, true);
setupChannel(initialProtocol);
//stores future
ChannelFuture future = actionHandler.getFinishedFuture();
channel.writeOutbound(msg);
FullHttpResponse response = new HttpResponseMessage(makeHttpResponse(HttpResponseStatus.OK));
//assesses that future listener isn't triggered yet.
assertThat(future.isDone()).isFalse();
channel.writeInbound(response);
//assesses that we successfully received good response and protocol is unchanged
assertThat(future.isSuccess()).isTrue();
}
@Test
public void testBasic_responseFailure_badRequest() {
//setup
setup("", null, false);
setupChannel(initialProtocol);
// Stores future that informs when action is completed.
ChannelFuture future = actionHandler.getFinishedFuture();
channel.writeOutbound(msg);
FullHttpResponse response = new HttpResponseMessage(
makeHttpResponse(HttpResponseStatus.BAD_REQUEST));
// Assesses that future listener isn't triggered yet.
assertThat(future.isDone()).isFalse();
channel.writeInbound(response);
// Assesses that listener is triggered, but event is not success
assertThat(future.isDone()).isTrue();
assertThat(future.isSuccess()).isFalse();
// Ensures that we fail as a result of a FailureException.
assertThat(future.cause() instanceof FailureException).isTrue();
}
@Test
public void testBasic_responseFailure_badURL() {
//setup
setup("", null, false);
setupChannel(initialProtocol);
//stores future
ChannelFuture future = actionHandler.getFinishedFuture();
channel.writeOutbound(msg);
FullHttpResponse response = new HttpResponseMessage(
makeRedirectResponse(HttpResponseStatus.MOVED_PERMANENTLY, DUMMY_URL, true));
//assesses that future listener isn't triggered yet.
assertThat(future.isDone()).isFalse();
channel.writeInbound(response);
//assesses that listener is triggered, and event is success
assertThat(future.isDone()).isTrue();
assertThat(future.isSuccess()).isFalse();
// Ensures that we fail as a result of a FailureException.
assertThat(future.cause() instanceof FailureException).isTrue();
}
@Test
public void testAdvanced_redirect() {
// Sets up EventLoopGroup with 1 thread to be blocking.
EventLoopGroup group = new NioEventLoopGroup(1);
// Sets up embedded channel.
setup("", makeBootstrap(group), false);
setupChannel(initialProtocol);
// Initializes LocalAddress with unique String.
LocalAddress address = new LocalAddress(TARGET_HOST);
//stores future
ChannelFuture future = actionHandler.getFinishedFuture();
channel.writeOutbound(msg);
// Path that we test WebWhoisActionHandler uses.
String path = "/test";
// Sets up the local server that the handler will be redirected to.
TestServer.webWhoisServer(group, address, "", TARGET_HOST, path);
FullHttpResponse response =
new HttpResponseMessage(makeRedirectResponse(HttpResponseStatus.MOVED_PERMANENTLY,
HTTP_REDIRECT + TARGET_HOST + path, true));
//checks that future has not been set to successful or a failure
assertThat(future.isDone()).isFalse();
channel.writeInbound(response);
//makes sure old channel is shut down when attempting redirection
assertThat(channel.isActive()).isFalse();
//assesses that we successfully received good response and protocol is unchanged
assertThat(future.syncUninterruptibly().isSuccess()).isTrue();
}
}

View file

@ -0,0 +1,41 @@
// Copyright 2019 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.monitoring.blackbox.messages;
import google.registry.monitoring.blackbox.exceptions.UndeterminedStateException;
/**
* {@link InboundMessageType} and {@link OutboundMessageType} type for the purpose of containing
* String messages to be passed down channel
*/
public class TestMessage implements OutboundMessageType, InboundMessageType {
private String message;
public TestMessage(String msg) {
message = msg;
}
@Override
public String toString() {
return message;
}
@Override
public OutboundMessageType modifyMessage(String... args) throws UndeterminedStateException {
message = args[0];
return this;
}
}

View file

@ -0,0 +1,123 @@
// Copyright 2019 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.monitoring.blackbox.testservers;
import static google.registry.monitoring.blackbox.TestUtils.makeHttpResponse;
import static google.registry.monitoring.blackbox.TestUtils.makeRedirectResponse;
import com.google.common.collect.ImmutableList;
import google.registry.monitoring.blackbox.messages.HttpResponseMessage;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelHandler.Sharable;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.channel.local.LocalAddress;
import io.netty.channel.local.LocalChannel;
import io.netty.channel.local.LocalServerChannel;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.handler.codec.http.HttpRequest;
import io.netty.handler.codec.http.HttpResponse;
import io.netty.handler.codec.http.HttpResponseStatus;
/**
* Mock Server Superclass whose subclasses implement specific behaviors we expect blackbox server to
* perform
*/
public class TestServer {
public TestServer(LocalAddress localAddress, ImmutableList<? extends ChannelHandler> handlers) {
this(new NioEventLoopGroup(1), localAddress, handlers);
}
public TestServer(EventLoopGroup eventLoopGroup, LocalAddress localAddress,
ImmutableList<? extends ChannelHandler> handlers) {
//Creates ChannelInitializer with handlers specified
ChannelInitializer<LocalChannel> serverInitializer = new ChannelInitializer<LocalChannel>() {
@Override
protected void initChannel(LocalChannel ch) {
for (ChannelHandler handler : handlers) {
ch.pipeline().addLast(handler);
}
}
};
//Sets up serverBootstrap with specified initializer, eventLoopGroup, and using
// LocalServerChannel class
ServerBootstrap serverBootstrap =
new ServerBootstrap()
.group(eventLoopGroup)
.channel(LocalServerChannel.class)
.childHandler(serverInitializer);
ChannelFuture unusedFuture = serverBootstrap.bind(localAddress).syncUninterruptibly();
}
public static TestServer webWhoisServer(EventLoopGroup eventLoopGroup,
LocalAddress localAddress, String redirectInput, String destinationInput,
String destinationPath) {
return new TestServer(
eventLoopGroup,
localAddress,
ImmutableList.of(new RedirectHandler(redirectInput, destinationInput, destinationPath))
);
}
/**
* Handler that will wither redirect client, give successful response, or give error messge
*/
@Sharable
static class RedirectHandler extends SimpleChannelInboundHandler<HttpRequest> {
private String redirectInput;
private String destinationInput;
private String destinationPath;
/**
* @param redirectInput - Server will send back redirect to {@code destinationInput} when
* receiving a request with this host location
* @param destinationInput - Server will send back an {@link HttpResponseStatus} OK response
* when receiving a request with this host location
*/
public RedirectHandler(String redirectInput, String destinationInput, String destinationPath) {
this.redirectInput = redirectInput;
this.destinationInput = destinationInput;
this.destinationPath = destinationPath;
}
/**
* Reads input {@link HttpRequest}, and creates appropriate {@link HttpResponseMessage} based on
* what header location is
*/
@Override
public void channelRead0(ChannelHandlerContext ctx, HttpRequest request) {
HttpResponse response;
if (request.headers().get("host").equals(redirectInput)) {
response = new HttpResponseMessage(
makeRedirectResponse(HttpResponseStatus.MOVED_PERMANENTLY, destinationInput, true));
} else if (request.headers().get("host").equals(destinationInput)
&& request.uri().equals(destinationPath)) {
response = new HttpResponseMessage(makeHttpResponse(HttpResponseStatus.OK));
} else {
response = new HttpResponseMessage(makeHttpResponse(HttpResponseStatus.BAD_REQUEST));
}
ChannelFuture unusedFuture = ctx.channel().writeAndFlush(response);
}
}
}

View file

@ -0,0 +1,68 @@
// Copyright 2019 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.monitoring.blackbox.tokens;
import static com.google.common.truth.Truth.assertThat;
import com.google.common.collect.ImmutableList;
import google.registry.monitoring.blackbox.exceptions.UndeterminedStateException;
import google.registry.monitoring.blackbox.messages.HttpRequestMessage;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
/**
* Unit Tests for {@link WebWhoisToken}
*/
@RunWith(JUnit4.class)
public class WebWhoisTokenTest {
private static String PREFIX = "whois.nic.";
private static String HOST = "starter";
private static String FIRST_TLD = "first_test";
private static String SECOND_TLD = "second_test";
private static String THIRD_TLD = "third_test";
private static ImmutableList<String> TEST_DOMAINS = ImmutableList.of(FIRST_TLD, SECOND_TLD,
THIRD_TLD);
public Token webToken = new WebWhoisToken(TEST_DOMAINS);
@Test
public void testMessageModification() throws UndeterminedStateException {
//creates Request message with header
HttpRequestMessage message = new HttpRequestMessage();
message.headers().set("host", HOST);
//attempts to use Token's method for modifying the method based on its stored host
HttpRequestMessage secondMessage = (HttpRequestMessage) webToken.modifyMessage(message);
assertThat(secondMessage.headers().get("host")).isEqualTo(PREFIX + TEST_DOMAINS.get(0));
}
/**
* As Circular Linked List has not been implemented yet, we cannot yet wrap around, so we don't
* test that in testing {@code next}.
*/
@Test
public void testNextToken() {
assertThat(webToken.host()).isEqualTo(PREFIX + FIRST_TLD);
webToken = webToken.next();
assertThat(webToken.host()).isEqualTo(PREFIX + SECOND_TLD);
webToken = webToken.next();
assertThat(webToken.host()).isEqualTo(PREFIX + THIRD_TLD);
}
}