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

1
prober/.gitignore vendored
View file

@ -1 +1,2 @@
out/ out/
src/main/resources/google/registry/monitoring/blackbox/modules/secrets/

View file

@ -4,7 +4,7 @@
// you may not use this file except in compliance with the License. // you may not use this file except in compliance with the License.
// You may obtain a copy of the License at // You may obtain a copy of the License at
// //
// http://www.apache.org/licenses/LICENSE-2.0 // http://www.apache.org/licenses/LICENSE-2.0
// //
// Unless required by applicable law or agreed to in writing, software // Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS, // distributed under the License is distributed on an "AS IS" BASIS,
@ -12,36 +12,44 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
apply plugin: 'java'
createUberJar('deployJar', 'prober', 'google.registry.monitoring.blackbox.Prober') createUberJar('deployJar', 'prober', 'google.registry.monitoring.blackbox.Prober')
dependencies { dependencies {
def deps = rootProject.dependencyMap def deps = rootProject.dependencyMap
compile deps['com.google.auto.value:auto-value-annotations'] compile deps['com.google.auto.value:auto-value-annotations']
compile deps['com.google.dagger:dagger'] compile deps['com.google.code.findbugs:jsr305']
compile deps['com.google.flogger:flogger'] compile deps['com.google.code.gson:gson']
compile deps['com.google.guava:guava'] compile deps['com.google.dagger:dagger']
compile deps['io.netty:netty-buffer'] compile deps['com.google.flogger:flogger']
compile deps['io.netty:netty-codec-http'] compile deps['com.google.guava:guava']
compile deps['io.netty:netty-codec'] compile deps['io.netty:netty-buffer']
compile deps['io.netty:netty-common'] compile deps['io.netty:netty-codec-http']
compile deps['io.netty:netty-handler'] compile deps['io.netty:netty-codec']
compile deps['io.netty:netty-transport'] compile deps['io.netty:netty-common']
compile deps['javax.inject:javax.inject'] compile deps['io.netty:netty-handler']
compile deps['io.netty:netty-transport']
compile deps['javax.inject:javax.inject']
compile deps['joda-time:joda-time']
compile deps['org.bouncycastle:bcpkix-jdk15on']
compile deps['org.bouncycastle:bcprov-jdk15on']
compile project(':util')
runtime deps['com.google.flogger:flogger-system-backend'] runtime deps['com.google.flogger:flogger-system-backend']
runtime deps['com.google.auto.value:auto-value'] runtime deps['com.google.auto.value:auto-value']
runtime deps['io.netty:netty-tcnative-boringssl-static'] runtime deps['io.netty:netty-tcnative-boringssl-static']
testCompile deps['com.google.truth:truth'] testCompile deps['com.google.truth:truth']
testCompile deps['junit:junit'] testCompile deps['junit:junit']
testCompile deps['org.mockito:mockito-core'] testCompile deps['org.mockito:mockito-core']
testCompile project(':third_party') testCompile project(':third_party')
// Include auto-value in compile until nebula-lint understands // Include auto-value in compile until nebula-lint understands
// annotationProcessor // annotationProcessor
annotationProcessor deps['com.google.auto.value:auto-value'] annotationProcessor deps['com.google.auto.value:auto-value']
testAnnotationProcessor deps['com.google.auto.value:auto-value'] testAnnotationProcessor deps['com.google.auto.value:auto-value']
annotationProcessor deps['com.google.dagger:dagger-compiler'] annotationProcessor deps['com.google.dagger:dagger-compiler']
testAnnotationProcessor deps['com.google.dagger:dagger-compiler'] testAnnotationProcessor deps['com.google.dagger:dagger-compiler']
} }

View file

@ -0,0 +1,43 @@
// 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 com.google.common.collect.ImmutableSet;
import google.registry.monitoring.blackbox.ProberModule.ProberComponent;
/**
* Main class of the Prober, which obtains and starts the {@link ProbingSequence}s provided by
* Dagger.
*/
public class Prober {
/**
* Main Dagger Component
*/
private static ProberComponent proberComponent = DaggerProberModule_ProberComponent.builder()
.build();
public static void main(String[] args) {
//Obtains WebWhois Sequence provided by proberComponent
ImmutableSet<ProbingSequence> sequences = ImmutableSet.copyOf(proberComponent.sequences());
//Tells Sequences to start running
for (ProbingSequence sequence : sequences) {
sequence.start();
}
}
}

View file

@ -0,0 +1,97 @@
// 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 dagger.Component;
import dagger.Module;
import dagger.Provides;
import io.netty.channel.Channel;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.ssl.OpenSsl;
import io.netty.handler.ssl.SslProvider;
import java.util.Set;
import javax.inject.Singleton;
import org.joda.time.Duration;
/**
* Dagger main module, which {@link Provides} all objects that are shared between sequences and
* stores {@link ProberComponent}, which allows main {@link Prober} class to obtain each {@link
* ProbingSequence}.
*/
@Module
public class ProberModule {
/**
* Default {@link Duration} chosen to be time between each {@link ProbingAction} call.
*/
private static final Duration DEFAULT_DURATION = Duration.standardSeconds(4);
/**
* {@link Provides} the {@link SslProvider} used by instances of {@link
* google.registry.monitoring.blackbox.handlers.SslClientInitializer}
*/
@Provides
@Singleton
static SslProvider provideSslProvider() {
// Prefer OpenSSL.
return OpenSsl.isAvailable() ? SslProvider.OPENSSL : SslProvider.JDK;
}
/**
* {@link Provides} one global {@link EventLoopGroup} shared by each {@link ProbingSequence}.
*/
@Provides
@Singleton
EventLoopGroup provideEventLoopGroup() {
return new NioEventLoopGroup();
}
/**
* {@link Provides} one global {@link Channel} class that is used to construct a {@link
* io.netty.bootstrap.Bootstrap}.
*/
@Provides
@Singleton
Class<? extends Channel> provideChannelClazz() {
return NioSocketChannel.class;
}
/**
* {@link Provides} above {@code DEFAULT_DURATION} for all provided {@link ProbingStep}s to use.
*/
@Provides
@Singleton
Duration provideDuration() {
return DEFAULT_DURATION;
}
/**
* Root level {@link Component} that provides each {@link ProbingSequence}.
*/
@Singleton
@Component(
modules = {
ProberModule.class,
WebWhoisModule.class,
})
public interface ProberComponent {
//Standard WebWhois sequence
Set<ProbingSequence> sequences();
}
}

View file

@ -0,0 +1,316 @@
// 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.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.flogger.StackSize.SMALL;
import static google.registry.monitoring.blackbox.Protocol.PROTOCOL_KEY;
import com.google.auto.value.AutoValue;
import com.google.common.collect.ImmutableList;
import com.google.common.flogger.FluentLogger;
import google.registry.monitoring.blackbox.exceptions.UndeterminedStateException;
import google.registry.monitoring.blackbox.handlers.ActionHandler;
import google.registry.monitoring.blackbox.messages.OutboundMessageType;
import io.netty.bootstrap.Bootstrap;
import io.netty.channel.Channel;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.ChannelPromise;
import io.netty.channel.local.LocalAddress;
import io.netty.util.AttributeKey;
import io.netty.util.HashedWheelTimer;
import io.netty.util.Timer;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.net.UnknownHostException;
import java.util.concurrent.Callable;
import java.util.concurrent.TimeUnit;
import javax.inject.Provider;
import org.joda.time.Duration;
/**
* AutoValue class that represents action generated by {@link ProbingStep}
*
* <p> Inherits from {@link Callable<ChannelFuture>}, as it has can be called
* to perform its specified task, and return the {@link ChannelFuture} that will be informed when
* the task has been completed </p>
*
* <p> Is an immutable class, as it is comprised of the tools necessary for making a specific type
* of connection. It goes hand in hand with {@link Protocol}, which specifies the kind of overall
* connection to be made. {@link Protocol} gives the outline and {@link ProbingAction} gives the
* details of that connection.</p>
*
* <p> In its build, if there is no channel supplied, it will create a channel from the attributes
* already supplied. Then, it only sends the {@link OutboundMessageType} down the pipeline when
* informed that the connection is successful. If the channel is supplied, the connection future is
* automatically set to successful.</p>
*/
@AutoValue
public abstract class ProbingAction implements Callable<ChannelFuture> {
/**
* {@link AttributeKey} in channel that gives {@link ChannelFuture} that is set to success when
* channel is active.
*/
public static final AttributeKey<ChannelFuture> CONNECTION_FUTURE_KEY = AttributeKey
.valueOf("CONNECTION_FUTURE_KEY");
/**
* {@link AttributeKey} in channel that gives the information of the channel's host.
*/
public static final AttributeKey<String> REMOTE_ADDRESS_KEY = AttributeKey
.valueOf("REMOTE_ADDRESS_KEY");
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
/**
* {@link Timer} that rate limits probing
*/
private static final Timer timer = new HashedWheelTimer();
public static Builder builder() {
return new AutoValue_ProbingAction.Builder();
}
/**
* Adds provided {@link ChannelHandler}s to the {@link ChannelPipeline} specified
*
* @param channelPipeline is pipeline associated with channel that we want to add handlers to
* @param handlerProviders are a list of provider objects that give us the requisite handlers Adds
* to the pipeline, the list of handlers in the order specified
*/
private static void addHandlers(
ChannelPipeline channelPipeline,
ImmutableList<Provider<? extends ChannelHandler>> handlerProviders) {
for (Provider<? extends ChannelHandler> handlerProvider : handlerProviders) {
channelPipeline.addLast(handlerProvider.get());
}
}
/**
* Actual {@link Duration} of this delay
*/
public abstract Duration delay();
/**
* {@link OutboundMessageType} instance that we write and flush down pipeline to server
*/
public abstract OutboundMessageType outboundMessage();
/**
* {@link Channel} object that either created by or passed into this {@link ProbingAction}
* instance
*/
public abstract Channel channel();
/**
* The {@link Protocol} instance that specifies type of connection
*/
public abstract Protocol protocol();
/**
* The hostname of the remote host we have a connection or will make a connection to
*/
public abstract String host();
/**
* Performs the work of the actual action.
*
* <p>First, checks if channel is active by setting a listener to perform the bulk of the work
* when the connection future is successful.</p>
*
* <p>Once the connection is successful, we establish which of the handlers in the pipeline is
* the {@link ActionHandler}.From that, we can obtain a future that is marked as a success when
* we receive an expected response from the server.</p>
*
* <p>Next, we set a timer set to a specified delay. After the delay has passed, we send the
* {@code outboundMessage} down the channel pipeline, and when we observe a success or failure,
* we inform the {@link ProbingStep} of this.</p>
*
* @return {@link ChannelFuture} that denotes when the action has been successfully performed.
*/
@Override
public ChannelFuture call() {
//ChannelPromise that we return
ChannelPromise finished = channel().newPromise();
//Ensures channel has been set up with connection future as an attribute
checkNotNull(channel().attr(CONNECTION_FUTURE_KEY).get());
//When connection is established call super.call and set returned listener to success
channel().attr(CONNECTION_FUTURE_KEY).get().addListener(
(ChannelFuture connectionFuture) -> {
if (connectionFuture.isSuccess()) {
logger.atInfo().log(String
.format("Successful connection to remote host: %s at port: %d", host(),
protocol().port()));
ActionHandler actionHandler;
try {
actionHandler = channel().pipeline().get(ActionHandler.class);
} catch (ClassCastException e) {
//If we don't actually have an ActionHandler instance, we have an issue, and throw
// an UndeterminedStateException
logger.atSevere().withStackTrace(SMALL).log("ActionHandler not in Channel Pipeline");
throw new UndeterminedStateException("No Action Handler found in pipeline");
}
ChannelFuture channelFuture = actionHandler.getFinishedFuture();
timer.newTimeout(timeout -> {
// Write appropriate outboundMessage to pipeline
ChannelFuture unusedFutureWriteAndFlush =
channel().writeAndFlush(outboundMessage());
channelFuture.addListeners(
future -> {
if (future.isSuccess()) {
ChannelFuture unusedFuture = finished.setSuccess();
} else {
ChannelFuture unusedFuture = finished.setFailure(future.cause());
}
},
//If we don't have a persistent connection, close the connection to this
// channel
future -> {
if (!protocol().persistentConnection()) {
ChannelFuture closedFuture = channel().close();
closedFuture.addListener(
f -> {
if (f.isSuccess()) {
logger.atInfo()
.log("Closed stale channel. Moving on to next ProbingStep");
} else {
logger.atWarning()
.log(
"Could not close channel. Stale connection still exists"
+ ".");
}
}
);
}
}
);
},
delay().getStandardSeconds(),
TimeUnit.SECONDS);
} else {
//if we receive a failure, log the failure, and close the channel
logger.atSevere().withCause(connectionFuture.cause()).log(
"Cannot connect to relay channel for %s channel: %s.",
protocol().name(), this.channel());
ChannelFuture unusedFuture = channel().close();
}
}
);
return finished;
}
@Override
public final String toString() {
return String.format(
"ProbingAction with delay: %d\n"
+ "outboundMessage: %s\n"
+ "protocol: %s\n"
+ "host: %s\n",
delay().getStandardSeconds(),
outboundMessage(),
protocol(),
host()
);
}
/**
* {@link AutoValue.Builder} that does work of creating connection when not already present.
*/
@AutoValue.Builder
public abstract static class Builder {
private Bootstrap bootstrap;
public Builder setBootstrap(Bootstrap bootstrap) {
this.bootstrap = bootstrap;
return this;
}
public abstract Builder setDelay(Duration value);
public abstract Builder setOutboundMessage(OutboundMessageType value);
public abstract Builder setProtocol(Protocol value);
public abstract Builder setHost(String value);
public abstract Builder setChannel(Channel channel);
abstract Protocol protocol();
abstract Channel channel();
abstract String host();
abstract ProbingAction autoBuild();
public ProbingAction build() {
// Sets SocketAddress to bind to.
SocketAddress address;
try {
InetAddress hostAddress = InetAddress.getByName(host());
address = new InetSocketAddress(hostAddress, protocol().port());
} catch (UnknownHostException e) {
address = new LocalAddress(host());
}
//Sets channel supplied or to be created.
Channel channel;
try {
channel = channel();
} catch (IllegalStateException e) {
channel = null;
}
checkArgument(channel == null ^ bootstrap == null,
"One and only one of bootstrap and channel must be supplied.");
//If a channel is supplied, nothing is needed to be done
//Otherwise, a Bootstrap must be supplied and be used for creating the channel
if (channel == null) {
bootstrap.handler(
new ChannelInitializer<Channel>() {
@Override
protected void initChannel(Channel outboundChannel)
throws Exception {
//Uses Handlers from Protocol to fill pipeline
addHandlers(outboundChannel.pipeline(), protocol().handlerProviders());
}
})
.attr(PROTOCOL_KEY, protocol())
.attr(REMOTE_ADDRESS_KEY, host());
logger.atInfo().log("Initialized bootstrap with channel Handlers");
//ChannelFuture that performs action when connection is established
ChannelFuture connectionFuture = bootstrap.connect(address);
setChannel(connectionFuture.channel());
connectionFuture.channel().attr(CONNECTION_FUTURE_KEY).set(connectionFuture);
}
//now we can actually build the ProbingAction
return autoBuild();
}
}
}

View file

@ -0,0 +1,108 @@
// 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 google.registry.monitoring.blackbox.tokens.Token;
import io.netty.bootstrap.Bootstrap;
import io.netty.channel.AbstractChannel;
import io.netty.channel.EventLoopGroup;
/**
* Represents Sequence of {@link ProbingStep}s that the Prober performs in order.
*
*
* <p>Created with {@link Builder} where we specify {@link EventLoopGroup}, {@link AbstractChannel}
* class type, then sequentially add in the {@link ProbingStep.Builder}s in order and mark which one
* is the first repeated step.</p>
*
* <p>{@link ProbingSequence} implicitly points each {@link ProbingStep} to the next one, so once
* the first one is activated with the requisite {@link Token}, the {@link ProbingStep}s do the rest
* of the work.</p>
*/
public class ProbingSequence {
private ProbingStep firstStep;
/**
* Each {@link ProbingSequence} requires a start token to begin running.
*/
private Token startToken;
private ProbingSequence(ProbingStep firstStep, Token startToken) {
this.firstStep = firstStep;
this.startToken = startToken;
}
public void start() {
// calls the first step with startToken;
firstStep.accept(startToken);
}
/**
* Turns {@link ProbingStep.Builder}s into fully self-dependent sequence with supplied {@link
* Bootstrap}.
*/
public static class Builder {
private ProbingStep currentStep;
private ProbingStep firstStep;
private ProbingStep firstRepeatedStep;
private Token startToken;
public Builder(Token startToken) {
this.startToken = startToken;
}
/**
* Adds {@link ProbingStep}, which is supplied with {@link Bootstrap}, built, and pointed to by
* the previous {@link ProbingStep} added.
*/
public Builder addStep(ProbingStep step) {
if (currentStep == null) {
firstStep = step;
} else {
currentStep.nextStep(step);
}
currentStep = step;
return this;
}
/**
* We take special note of the first repeated step.
*/
public Builder markFirstRepeated() {
firstRepeatedStep = currentStep;
return this;
}
/**
* Points last {@link ProbingStep} to the {@code firstRepeatedStep} and calls private
* constructor to create {@link ProbingSequence}.
*/
public ProbingSequence build() {
if (firstRepeatedStep == null) {
firstRepeatedStep = firstStep;
}
currentStep.nextStep(firstRepeatedStep);
currentStep.lastStep();
return new ProbingSequence(this.firstStep, this.startToken);
}
}
}

View file

@ -0,0 +1,196 @@
// 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 com.google.auto.value.AutoValue;
import com.google.common.flogger.FluentLogger;
import google.registry.monitoring.blackbox.exceptions.UndeterminedStateException;
import google.registry.monitoring.blackbox.messages.OutboundMessageType;
import google.registry.monitoring.blackbox.tokens.Token;
import io.netty.bootstrap.Bootstrap;
import io.netty.channel.ChannelFuture;
import java.util.function.Consumer;
import org.joda.time.Duration;
/**
* {@link AutoValue} class that represents generator of actions performed at each step in {@link
* ProbingSequence}.
*
* <p>Holds the unchanged components in a given step of the {@link ProbingSequence}, which are
* the {@link OutboundMessageType}, {@link Protocol}, {@link Duration}, and {@link Bootstrap}
* instances. It then modifies these components on each loop iteration with the consumed {@link
* Token} and from that, generates a new {@link ProbingAction} to call.</p>
*/
@AutoValue
public abstract class ProbingStep implements Consumer<Token> {
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
/**
* Necessary boolean to inform when to obtain next {@link Token}
*/
protected boolean isLastStep = false;
private ProbingStep nextStep;
public static Builder builder() {
return new AutoValue_ProbingStep.Builder();
}
/**
* Time delay duration between actions.
*/
abstract Duration duration();
/**
* {@link Protocol} type for this step.
*/
abstract Protocol protocol();
/**
* {@link OutboundMessageType} instance that serves as template to be modified by {@link Token}.
*/
abstract OutboundMessageType messageTemplate();
/**
* {@link Bootstrap} instance provided by parent {@link ProbingSequence} that allows for creation
* of new channels.
*/
abstract Bootstrap bootstrap();
void lastStep() {
isLastStep = true;
}
void nextStep(ProbingStep step) {
this.nextStep = step;
}
ProbingStep nextStep() {
return this.nextStep;
}
/**
* Generates a new {@link ProbingAction} from {@code token} modified {@link OutboundMessageType}
*/
private ProbingAction generateAction(Token token) throws UndeterminedStateException {
OutboundMessageType message = token.modifyMessage(messageTemplate());
ProbingAction.Builder probingActionBuilder = ProbingAction.builder()
.setDelay(duration())
.setProtocol(protocol())
.setOutboundMessage(message)
.setHost(token.host());
if (token.channel() != null) {
probingActionBuilder.setChannel(token.channel());
} else {
probingActionBuilder.setBootstrap(bootstrap());
}
return probingActionBuilder.build();
}
/**
* On the last step, gets the next {@link Token}. Otherwise, uses the same one.
*/
private Token generateNextToken(Token token) {
return isLastStep ? token.next() : token;
}
/**
* Generates new {@link ProbingAction}, calls the action, then retrieves the result of the
* action.
*
* @param token - used to generate the {@link ProbingAction} by calling {@code generateAction}.
*
* <p>If unable to generate the action, or the calling the action results in an immediate error,
* we note an error. Otherwise, if the future marked as finished when the action is completed is
* marked as a success, we note a success. Otherwise, if the cause of failure will either be a
* failure or error. </p>
*/
@Override
public void accept(Token token) {
ProbingAction currentAction;
//attempt to generate new action. On error, move on to next step
try {
currentAction = generateAction(token);
} catch (UndeterminedStateException e) {
logger.atWarning().withCause(e).log("Error in Action Generation");
nextStep.accept(generateNextToken(token));
return;
}
ChannelFuture future;
try {
//call the generated action
future = currentAction.call();
} catch (Exception e) {
//On error in calling action, log error and note an error
logger.atWarning().withCause(e).log("Error in Action Performed");
//Move on to next step in ProbingSequence
nextStep.accept(generateNextToken(token));
return;
}
future.addListener(f -> {
if (f.isSuccess()) {
//On a successful result, we log as a successful step, and not a success
logger.atInfo().log(String.format("Successfully completed Probing Step: %s", this));
} else {
//On a failed result, we log the failure and note either a failure or error
logger.atSevere().withCause(f.cause()).log("Did not result in future success");
}
if (protocol().persistentConnection()) {
//If the connection is persistent, we store the channel in the token
token.setChannel(currentAction.channel());
}
//Move on the the next step in the ProbingSequence
nextStep.accept(generateNextToken(token));
});
}
@Override
public final String toString() {
return String.format("ProbingStep with Protocol: %s\n"
+ "OutboundMessage: %s\n",
protocol(),
messageTemplate().getClass().getName());
}
/**
* Default {@link AutoValue.Builder} for {@link ProbingStep}.
*/
@AutoValue.Builder
public abstract static class Builder {
public abstract Builder setDuration(Duration value);
public abstract Builder setProtocol(Protocol value);
public abstract Builder setMessageTemplate(OutboundMessageType value);
public abstract Builder setBootstrap(Bootstrap value);
public abstract ProbingStep build();
}
}

View file

@ -17,44 +17,64 @@ package google.registry.monitoring.blackbox;
import com.google.auto.value.AutoValue; import com.google.auto.value.AutoValue;
import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableList;
import io.netty.channel.ChannelHandler; import io.netty.channel.ChannelHandler;
import io.netty.util.AttributeKey;
import javax.inject.Provider; import javax.inject.Provider;
/** /**
* {@link AutoValue} class that stores all unchanged variables necessary for type of connection * {@link AutoValue} class that stores all unchanged variables necessary for type of connection.
*/ */
@AutoValue @AutoValue
public abstract class Protocol { public abstract class Protocol {
abstract String name(); /**
* {@link AttributeKey} that lets channel reference {@link Protocol} that created it.
public abstract int port(); */
public static final AttributeKey<Protocol> PROTOCOL_KEY = AttributeKey.valueOf("PROTOCOL_KEY");
/** The {@link ChannelHandler} providers to use for the protocol, in order. */
abstract ImmutableList<Provider<? extends ChannelHandler>> handlerProviders();
/** Boolean that notes if connection associated with Protocol is persistent.*/
abstract boolean persistentConnection();
public abstract Builder toBuilder();
public static Builder builder() { public static Builder builder() {
return new AutoValue_Protocol.Builder(); return new AutoValue_Protocol.Builder();
} }
/** Builder for {@link Protocol}. */ public abstract String name();
public abstract int port();
/**
* The {@link ChannelHandler} providers to use for the protocol, in order.
*/
abstract ImmutableList<Provider<? extends ChannelHandler>> handlerProviders();
/**
* Boolean that notes if connection associated with Protocol is persistent.
*/
abstract boolean persistentConnection();
@Override
public final String toString() {
return String.format(
"Protocol with name: %s, port: %d, providers: %s, and persistent connection: %s",
name(),
port(),
handlerProviders(),
persistentConnection()
);
}
/**
* Default {@link AutoValue.Builder} for {@link Protocol}.
*/
@AutoValue.Builder @AutoValue.Builder
public abstract static class Builder { public abstract static class Builder {
public abstract Builder name(String value); public abstract Builder setName(String value);
public abstract Builder port(int num); public abstract Builder setPort(int num);
public abstract Builder handlerProviders( public abstract Builder setHandlerProviders(
ImmutableList<Provider<? extends ChannelHandler>> providers); ImmutableList<Provider<? extends ChannelHandler>> providers);
public abstract Builder persistentConnection(boolean value); public abstract Builder setPersistentConnection(boolean value);
public abstract Protocol build(); public abstract Protocol build();
} }
} }

View file

@ -0,0 +1,248 @@
// 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 com.google.common.collect.ImmutableList;
import dagger.Module;
import dagger.Provides;
import dagger.multibindings.IntoSet;
import google.registry.monitoring.blackbox.handlers.SslClientInitializer;
import google.registry.monitoring.blackbox.handlers.WebWhoisActionHandler;
import google.registry.monitoring.blackbox.handlers.WebWhoisMessageHandler;
import google.registry.monitoring.blackbox.messages.HttpRequestMessage;
import google.registry.monitoring.blackbox.tokens.WebWhoisToken;
import io.netty.bootstrap.Bootstrap;
import io.netty.channel.Channel;
import io.netty.channel.ChannelHandler;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.http.HttpClientCodec;
import io.netty.handler.codec.http.HttpObjectAggregator;
import io.netty.handler.ssl.SslProvider;
import javax.inject.Provider;
import javax.inject.Qualifier;
import javax.inject.Singleton;
import org.joda.time.Duration;
/**
* A module that provides the {@link Protocol}s to send HTTP(S) web WHOIS requests.
*/
@Module
public class WebWhoisModule {
private static final String HTTP_PROTOCOL_NAME = "http";
private static final String HTTPS_PROTOCOL_NAME = "https";
/**
* Standard length of messages used by Proxy. Equates to 0.5 MB.
*/
private static final int maximumMessageLengthBytes = 512 * 1024;
private final int HTTP_WHOIS_PORT = 80;
private final int HTTPS_WHOIS_PORT = 443;
/**
* {@link Provides} only step used in WebWhois sequence.
*/
@Provides
@WebWhoisProtocol
static ProbingStep provideWebWhoisStep(
@HttpWhoisProtocol Protocol httpWhoisProtocol,
@WebWhoisProtocol Bootstrap bootstrap,
HttpRequestMessage messageTemplate,
Duration duration) {
return ProbingStep.builder()
.setProtocol(httpWhoisProtocol)
.setBootstrap(bootstrap)
.setMessageTemplate(messageTemplate)
.setDuration(duration)
.build();
}
/**
* {@link Provides} the {@link Protocol} that corresponds to http connection.
*/
@Singleton
@Provides
@HttpWhoisProtocol
static Protocol provideHttpWhoisProtocol(
@HttpWhoisProtocol int httpWhoisPort,
@HttpWhoisProtocol ImmutableList<Provider<? extends ChannelHandler>> handlerProviders) {
return Protocol.builder()
.setName(HTTP_PROTOCOL_NAME)
.setPort(httpWhoisPort)
.setHandlerProviders(handlerProviders)
.setPersistentConnection(false)
.build();
}
/**
* {@link Provides} the {@link Protocol} that corresponds to https connection.
*/
@Singleton
@Provides
@HttpsWhoisProtocol
static Protocol provideHttpsWhoisProtocol(
@HttpsWhoisProtocol int httpsWhoisPort,
@HttpsWhoisProtocol ImmutableList<Provider<? extends ChannelHandler>> handlerProviders) {
return Protocol.builder()
.setName(HTTPS_PROTOCOL_NAME)
.setPort(httpsWhoisPort)
.setHandlerProviders(handlerProviders)
.setPersistentConnection(false)
.build();
}
/**
* {@link Provides} the list of providers of {@link ChannelHandler}s that are used for http
* protocol.
*/
@Provides
@HttpWhoisProtocol
static ImmutableList<Provider<? extends ChannelHandler>> providerHttpWhoisHandlerProviders(
Provider<HttpClientCodec> httpClientCodecProvider,
Provider<HttpObjectAggregator> httpObjectAggregatorProvider,
Provider<WebWhoisMessageHandler> messageHandlerProvider,
Provider<WebWhoisActionHandler> webWhoisActionHandlerProvider) {
return ImmutableList.of(
httpClientCodecProvider,
httpObjectAggregatorProvider,
messageHandlerProvider,
webWhoisActionHandlerProvider);
}
/**
* {@link Provides} the list of providers of {@link ChannelHandler}s that are used for https
* protocol.
*/
@Provides
@HttpsWhoisProtocol
static ImmutableList<Provider<? extends ChannelHandler>> providerHttpsWhoisHandlerProviders(
@HttpsWhoisProtocol
Provider<SslClientInitializer<NioSocketChannel>> sslClientInitializerProvider,
Provider<HttpClientCodec> httpClientCodecProvider,
Provider<HttpObjectAggregator> httpObjectAggregatorProvider,
Provider<WebWhoisMessageHandler> messageHandlerProvider,
Provider<WebWhoisActionHandler> webWhoisActionHandlerProvider) {
return ImmutableList.of(
sslClientInitializerProvider,
httpClientCodecProvider,
httpObjectAggregatorProvider,
messageHandlerProvider,
webWhoisActionHandlerProvider);
}
@Provides
static HttpClientCodec provideHttpClientCodec() {
return new HttpClientCodec();
}
@Provides
static HttpObjectAggregator provideHttpObjectAggregator(@WebWhoisProtocol int maxContentLength) {
return new HttpObjectAggregator(maxContentLength);
}
/**
* {@link Provides} the {@link SslClientInitializer} used for the {@link HttpsWhoisProtocol}.
*/
@Provides
@HttpsWhoisProtocol
static SslClientInitializer<NioSocketChannel> provideSslClientInitializer(
SslProvider sslProvider) {
return new SslClientInitializer<>(sslProvider);
}
/**
* {@link Provides} the {@link Bootstrap} used by the WebWhois sequence.
*/
@Singleton
@Provides
@WebWhoisProtocol
static Bootstrap provideBootstrap(
EventLoopGroup eventLoopGroup,
Class<? extends Channel> channelClazz) {
return new Bootstrap()
.group(eventLoopGroup)
.channel(channelClazz);
}
/**
* {@link Provides} standard WebWhois sequence.
*/
@Provides
@Singleton
@IntoSet
ProbingSequence provideWebWhoisSequence(
@WebWhoisProtocol ProbingStep probingStep,
WebWhoisToken webWhoisToken) {
return new ProbingSequence.Builder(webWhoisToken)
.addStep(probingStep)
.build();
}
@Provides
@WebWhoisProtocol
int provideMaximumMessageLengthBytes() {
return maximumMessageLengthBytes;
}
/**
* {@link Provides} the list of top level domains to be probed
*/
@Singleton
@Provides
@WebWhoisProtocol
ImmutableList<String> provideTopLevelDomains() {
return ImmutableList.of("how", "soy", "xn--q9jyb4c");
}
@Provides
@HttpWhoisProtocol
int provideHttpWhoisPort() {
return HTTP_WHOIS_PORT;
}
@Provides
@HttpsWhoisProtocol
int provideHttpsWhoisPort() {
return HTTPS_WHOIS_PORT;
}
/**
* Dagger qualifier to provide HTTP whois protocol related handlers and other bindings.
*/
@Qualifier
public @interface HttpWhoisProtocol {
}
/**
* Dagger qualifier to provide HTTPS whois protocol related handlers and other bindings.
*/
@Qualifier
public @interface HttpsWhoisProtocol {
}
/**
* Dagger qualifier to provide any WebWhois related bindings.
*/
@Qualifier
public @interface WebWhoisProtocol {
}
}

View file

@ -0,0 +1,30 @@
// 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.exceptions;
/**
* Subclass of {@link UndeterminedStateException} that represents all instances when the action
* performed failed due to an issue in the connection with the server.
*/
public class ConnectionException extends UndeterminedStateException {
public ConnectionException(String msg) {
super(msg);
}
public ConnectionException(Throwable e) {
super(e);
}
}

View file

@ -0,0 +1,29 @@
// 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.exceptions;
/**
* Base exception class for all instances when the status of the action performed is FAILURE.
*/
public class FailureException extends Exception {
public FailureException(String msg) {
super(msg);
}
public FailureException(Throwable e) {
super(e);
}
}

View file

@ -0,0 +1,30 @@
// 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.exceptions;
/**
* Base exception class for all instances when the action performed fails before we can determine
* the state of the result, meaning the status is recorded as ERROR.
*/
public class UndeterminedStateException extends Exception {
public UndeterminedStateException(String msg) {
super(msg);
}
public UndeterminedStateException(Throwable e) {
super(e);
}
}

View file

@ -15,67 +15,99 @@
package google.registry.monitoring.blackbox.handlers; package google.registry.monitoring.blackbox.handlers;
import com.google.common.flogger.FluentLogger; import com.google.common.flogger.FluentLogger;
import google.registry.monitoring.blackbox.ProbingAction;
import google.registry.monitoring.blackbox.exceptions.FailureException;
import google.registry.monitoring.blackbox.exceptions.UndeterminedStateException;
import google.registry.monitoring.blackbox.messages.InboundMessageType; import google.registry.monitoring.blackbox.messages.InboundMessageType;
import google.registry.monitoring.blackbox.messages.OutboundMessageType;
import io.netty.channel.ChannelFuture; import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelPromise; import io.netty.channel.ChannelPromise;
import io.netty.channel.SimpleChannelInboundHandler; import io.netty.channel.SimpleChannelInboundHandler;
/** /**
* Superclass of all {@link ChannelHandler}s placed at end of channel pipeline * Superclass of all {@link io.netty.channel.ChannelHandler}s placed at end of channel pipeline
* *
* <p>{@code ActionHandler} inherits from {@link SimpleChannelInboundHandler< InboundMessageType >}, * <p> {@link ActionHandler} inherits from {@link SimpleChannelInboundHandler<InboundMessageType>},
* as it should only be passed in messages that implement the {@link InboundMessageType} interface. * as it should only be passed in messages that implement the {@link InboundMessageType}
* interface.</p>
* *
* <p>The {@code ActionHandler} skeleton exists for a few main purposes. First, it returns a {@link * <p> The {@link ActionHandler} skeleton exists for a few main purposes. First, it returns a
* ChannelPromise}, which informs the {@link ProbingAction} in charge that a response has been read. * {@link ChannelPromise}, which informs the {@link ProbingAction} in charge that a response has
* Second, it stores the {@link OutboundMessageType} passed down the pipeline, so that subclasses * been read. Second, with any exception thrown, the connection is closed, and the ProbingAction
* can use that information for their own processes. Lastly, with any exception thrown, the * governing this channel is informed of the error. If the error is an instance of a {@link
* connection is closed, and the ProbingAction governing this channel is informed of the error. * FailureException} {@code finished} is marked as a failure with cause {@link FailureException}. If
* Subclasses specify further work to be done for specific kinds of channel pipelines. * it is any other type of error, it is treated as an {@link UndeterminedStateException} and {@code
* finished} set as a failure with the same cause as what caused the exception. Lastly, if no error
* is thrown, we know the action completed as a success, and, as such, we mark {@code finished} as a
* success.</p>
*
* <p>Subclasses specify further work to be done for specific kinds of channel pipelines. </p>
*/ */
public abstract class ActionHandler extends SimpleChannelInboundHandler<InboundMessageType> { public abstract class ActionHandler extends SimpleChannelInboundHandler<InboundMessageType> {
private static final FluentLogger logger = FluentLogger.forEnclosingClass(); private static final FluentLogger logger = FluentLogger.forEnclosingClass();
protected ChannelPromise finished; /**
* {@link ChannelPromise} that informs {@link ProbingAction} if response has been received.
*/
private ChannelPromise finished;
/** /**
* Takes in {@link OutboundMessageType} type and saves for subclasses. Then returns initialized * Returns initialized {@link ChannelPromise} to {@link ProbingAction}.
* {@link ChannelPromise}
*/ */
public ChannelFuture getFuture() { public ChannelFuture getFinishedFuture() {
return finished; return finished;
} }
/** Initializes new {@link ChannelPromise} */ /**
* Initializes {@link ChannelPromise}
*/
@Override @Override
public void handlerAdded(ChannelHandlerContext ctx) { public void handlerAdded(ChannelHandlerContext ctx) {
// Once handler is added to channel pipeline, initialize channel and future for this handler //Once handler is added to channel pipeline, initialize channel and future for this handler
finished = ctx.newPromise(); finished = ctx.newPromise();
} }
/**
* Marks {@link ChannelPromise} as success
*/
@Override @Override
public void channelRead0(ChannelHandlerContext ctx, InboundMessageType inboundMessage) public void channelRead0(ChannelHandlerContext ctx, InboundMessageType inboundMessage)
throws Exception { throws FailureException, UndeterminedStateException {
// simply marks finished as success
finished = finished.setSuccess(); ChannelFuture unusedFuture = finished.setSuccess();
} }
/** /**
* Logs the channel and pipeline that caused error, closes channel, then informs {@link * Logs the channel and pipeline that caused error, closes channel, then informs {@link
* ProbingAction} listeners of error * ProbingAction} listeners of error.
*/ */
@Override @Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
logger.atSevere().withCause(cause).log( logger.atWarning().withCause(cause).log(String.format(
String.format( "Attempted Action was unsuccessful with channel: %s, having pipeline: %s",
"Attempted Action was unsuccessful with channel: %s, having pipeline: %s", ctx.channel().toString(),
ctx.channel().toString(), ctx.channel().pipeline().toString())); ctx.channel().pipeline().toString()));
finished = finished.setFailure(cause); if (cause instanceof FailureException) {
ChannelFuture closedFuture = ctx.channel().close(); //On FailureException, we know the response is a failure.
closedFuture.addListener(f -> logger.atInfo().log("Unsuccessful channel connection closed"));
//Since it wasn't a success, we still want to log to see what caused the FAILURE
logger.atInfo().log(cause.getMessage());
//As always, inform the ProbingStep that we successfully completed this action
ChannelFuture unusedFuture = finished.setFailure(cause);
} else {
//On UndeterminedStateException, we know the response type is an error.
//Since it wasn't a success, we still log what caused the ERROR
logger.atWarning().log(cause.getMessage());
ChannelFuture unusedFuture = finished.setFailure(cause);
//As this was an ERROR in performing the action, we must close the channel
ChannelFuture closedFuture = ctx.channel().close();
closedFuture.addListener(f -> logger.atInfo().log("Unsuccessful channel connection closed"));
}
} }
} }

View file

@ -0,0 +1,119 @@
// 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.checkNotNull;
import static google.registry.monitoring.blackbox.ProbingAction.REMOTE_ADDRESS_KEY;
import static google.registry.monitoring.blackbox.Protocol.PROTOCOL_KEY;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.flogger.FluentLogger;
import google.registry.monitoring.blackbox.Protocol;
import io.netty.channel.Channel;
import io.netty.channel.ChannelHandler.Sharable;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.embedded.EmbeddedChannel;
import io.netty.handler.ssl.SslContextBuilder;
import io.netty.handler.ssl.SslHandler;
import io.netty.handler.ssl.SslProvider;
import java.security.PrivateKey;
import java.security.cert.X509Certificate;
import java.util.function.Supplier;
import javax.inject.Singleton;
import javax.net.ssl.SSLEngine;
import javax.net.ssl.SSLParameters;
/**
* Adds a client side SSL handler to the channel pipeline.
*
* <p> Code is close to unchanged from {@link SslClientInitializer}</p> in proxy, but is modified
* for revised overall structure of connections, and to accomdate EPP connections </p>
*
* <p>This <b>must</b> be the first handler provided for any handler provider list, if it is
* provided. The type parameter {@code C} is needed so that unit tests can construct this handler
* that works with {@link EmbeddedChannel};
*/
@Singleton
@Sharable
public class SslClientInitializer<C extends Channel> extends ChannelInitializer<C> {
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
private final SslProvider sslProvider;
private final X509Certificate[] trustedCertificates;
private final Supplier<PrivateKey> privateKeySupplier;
private final Supplier<X509Certificate[]> certificateSupplier;
public SslClientInitializer(SslProvider sslProvider) {
// null uses the system default trust store.
//Used for WebWhois, so we don't care about privateKey and certificates, setting them to null
this(sslProvider, null, null, null);
}
public SslClientInitializer(SslProvider sslProvider, Supplier<PrivateKey> privateKeySupplier,
Supplier<X509Certificate[]> certificateSupplier) {
//We use the default trust store here as well, setting trustCertificates to null
this(sslProvider, null, privateKeySupplier, certificateSupplier);
}
@VisibleForTesting
SslClientInitializer(SslProvider sslProvider, X509Certificate[] trustCertificates) {
this(sslProvider, trustCertificates, null, null);
}
private SslClientInitializer(
SslProvider sslProvider,
X509Certificate[] trustCertificates,
Supplier<PrivateKey> privateKeySupplier,
Supplier<X509Certificate[]> certificateSupplier) {
logger.atInfo().log("Client SSL Provider: %s", sslProvider);
this.sslProvider = sslProvider;
this.trustedCertificates = trustCertificates;
this.privateKeySupplier = privateKeySupplier;
this.certificateSupplier = certificateSupplier;
}
@Override
protected void initChannel(C channel) throws Exception {
Protocol protocol = channel.attr(PROTOCOL_KEY).get();
String host = channel.attr(REMOTE_ADDRESS_KEY).get();
//Builds SslHandler from Protocol, and based on if we require a privateKey and certificate
checkNotNull(protocol, "Protocol is not set for channel: %s", channel);
SslContextBuilder sslContextBuilder =
SslContextBuilder.forClient()
.sslProvider(sslProvider)
.trustManager(trustedCertificates);
if (privateKeySupplier != null && certificateSupplier != null) {
sslContextBuilder = sslContextBuilder
.keyManager(privateKeySupplier.get(), certificateSupplier.get());
}
SslHandler sslHandler = sslContextBuilder
.build()
.newHandler(channel.alloc(), host, protocol.port());
// Enable hostname verification.
SSLEngine sslEngine = sslHandler.engine();
SSLParameters sslParameters = sslEngine.getSSLParameters();
sslParameters.setEndpointIdentificationAlgorithm("HTTPS");
sslEngine.setSSLParameters(sslParameters);
channel.pipeline().addLast(sslHandler);
}
}

View file

@ -0,0 +1,189 @@
// 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 com.google.common.flogger.FluentLogger;
import google.registry.monitoring.blackbox.ProbingAction;
import google.registry.monitoring.blackbox.Protocol;
import google.registry.monitoring.blackbox.WebWhoisModule.HttpWhoisProtocol;
import google.registry.monitoring.blackbox.WebWhoisModule.HttpsWhoisProtocol;
import google.registry.monitoring.blackbox.WebWhoisModule.WebWhoisProtocol;
import google.registry.monitoring.blackbox.exceptions.ConnectionException;
import google.registry.monitoring.blackbox.exceptions.FailureException;
import google.registry.monitoring.blackbox.exceptions.UndeterminedStateException;
import google.registry.monitoring.blackbox.messages.HttpRequestMessage;
import google.registry.monitoring.blackbox.messages.HttpResponseMessage;
import google.registry.monitoring.blackbox.messages.InboundMessageType;
import io.netty.bootstrap.Bootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.http.HttpResponseStatus;
import java.net.MalformedURLException;
import java.net.URL;
import javax.inject.Inject;
import org.joda.time.Duration;
/**
* Subclass of {@link ActionHandler} that deals with the WebWhois Sequence
*
* <p> Main purpose is to verify {@link HttpResponseMessage} received is valid. If the response
* implies a redirection it follows the redirection until either an Error Response is received, or
* {@link HttpResponseStatus.OK} is received</p>
*/
public class WebWhoisActionHandler extends ActionHandler {
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
/** Dagger injected components necessary for redirect responses: */
/**
* {@link Bootstrap} necessary for remaking connection on redirect response.
*/
private final Bootstrap bootstrap;
/**
* {@link Protocol} for when redirected to http endpoint.
*/
private final Protocol httpWhoisProtocol;
/**
* {@link Protocol} for when redirected to https endpoint.
*/
private final Protocol httpsWhoisProtocol;
/**
* {@link HttpRequestMessage} that represents default GET message to be sent on redirect.
*/
private final HttpRequestMessage requestMessage;
@Inject
public WebWhoisActionHandler(
@WebWhoisProtocol Bootstrap bootstrap,
@HttpWhoisProtocol Protocol httpWhoisProtocol,
@HttpsWhoisProtocol Protocol httpsWhoisProtocol,
HttpRequestMessage requestMessage) {
this.bootstrap = bootstrap;
this.httpWhoisProtocol = httpWhoisProtocol;
this.httpsWhoisProtocol = httpsWhoisProtocol;
this.requestMessage = requestMessage;
}
/**
* After receiving {@link HttpResponseMessage}, either notes success and marks future as finished,
* notes an error in the received {@link URL} and throws a {@link ConnectionException}, received a
* response indicating a Failure, or receives a redirection response, where it follows the
* redirects until receiving one of the previous three responses.
*/
@Override
public void channelRead0(ChannelHandlerContext ctx, InboundMessageType msg)
throws FailureException, UndeterminedStateException {
HttpResponseMessage response = (HttpResponseMessage) msg;
if (response.status().equals(HttpResponseStatus.OK)) {
logger.atInfo().log("Received Successful HttpResponseStatus");
logger.atInfo().log("Response Received: " + response);
//On success, we always pass message to ActionHandler's channelRead0 method.
super.channelRead0(ctx, msg);
} else if (response.status().equals(HttpResponseStatus.MOVED_PERMANENTLY)
|| response.status().equals(HttpResponseStatus.FOUND)) {
//TODO - Fix checker to better determine when we have encountered a redirection response.
//Obtain url to be redirected to
URL url;
try {
url = new URL(response.headers().get("Location"));
} catch (MalformedURLException e) {
//in case of error, log it, and let ActionHandler's exceptionThrown method deal with it
throw new FailureException(
"Redirected Location was invalid. Given Location was: " + response.headers()
.get("Location"));
}
//From url, extract new host, port, and path
String newHost = url.getHost();
String newPath = url.getPath();
logger.atInfo().log(String
.format("Redirected to %s with host: %s, port: %d, and path: %s", url, newHost,
url.getDefaultPort(), newPath));
//Construct new Protocol to reflect redirected host, path, and port
Protocol newProtocol;
if (url.getProtocol().equals(httpWhoisProtocol.name())) {
newProtocol = httpWhoisProtocol;
} else if (url.getProtocol().equals(httpsWhoisProtocol.name())) {
newProtocol = httpsWhoisProtocol;
} else {
throw new FailureException(
"Redirection Location port was invalid. Given protocol name was: " + url.getProtocol());
}
//Obtain HttpRequestMessage with modified headers to reflect new host and path.
HttpRequestMessage httpRequest = requestMessage.modifyMessage(newHost, newPath);
//Create new probingAction that takes in the new Protocol and HttpRequestMessage with no delay
ProbingAction redirectedAction = ProbingAction.builder()
.setBootstrap(bootstrap)
.setProtocol(newProtocol)
.setOutboundMessage(httpRequest)
.setDelay(Duration.ZERO)
.setHost(newHost)
.build();
//close this channel as we no longer need it
ChannelFuture future = ctx.close();
future.addListener(
f -> {
if (f.isSuccess()) {
logger.atInfo().log("Successfully Closed Connection.");
} else {
logger.atWarning().log("Channel was unsuccessfully closed.");
}
//Once channel is closed, establish new connection to redirected host, and repeat
// same actions
ChannelFuture secondFuture = redirectedAction.call();
//Once we have a successful call, set original ChannelPromise as success to tell
// ProbingStep we can move on
secondFuture.addListener(f2 -> {
if (f2.isSuccess()) {
super.channelRead0(ctx, msg);
} else {
if (f2 instanceof FailureException) {
throw new FailureException(f2.cause());
} else {
throw new UndeterminedStateException(f2.cause());
}
}
});
}
);
} else {
//Add in metrics Handling that informs MetricsCollector the response was a FAILURE
logger.atWarning().log(String.format("Received unexpected response: %s", response.status()));
throw new FailureException("Response received from remote site was: " + response.status());
}
}
}

View file

@ -0,0 +1,59 @@
// 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.messages.HttpRequestMessage;
import google.registry.monitoring.blackbox.messages.HttpResponseMessage;
import google.registry.monitoring.blackbox.messages.InboundMessageType;
import io.netty.channel.ChannelDuplexHandler;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelPromise;
import io.netty.handler.codec.http.FullHttpResponse;
import javax.inject.Inject;
/**
* {@link io.netty.channel.ChannelHandler} that converts inbound {@link FullHttpResponse} to custom
* type {@link HttpResponseMessage} and retains {@link HttpRequestMessage} in case of reuse for
* redirection.
*/
public class WebWhoisMessageHandler extends ChannelDuplexHandler {
@Inject
public WebWhoisMessageHandler() {
}
/**
* Retains {@link HttpRequestMessage} and calls super write method.
*/
@Override
public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise)
throws Exception {
HttpRequestMessage request = (HttpRequestMessage) msg;
request.retain();
super.write(ctx, request, promise);
}
/**
* Converts {@link FullHttpResponse} to {@link HttpResponseMessage}, so it is an {@link
* InboundMessageType} instance.
*/
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
FullHttpResponse originalResponse = (FullHttpResponse) msg;
HttpResponseMessage response = new HttpResponseMessage(originalResponse);
super.channelRead(ctx, response);
}
}

View file

@ -0,0 +1,90 @@
// 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 io.netty.buffer.ByteBuf;
import io.netty.handler.codec.http.DefaultFullHttpRequest;
import io.netty.handler.codec.http.FullHttpRequest;
import io.netty.handler.codec.http.HttpMethod;
import io.netty.handler.codec.http.HttpVersion;
import java.util.Arrays;
import javax.inject.Inject;
/**
* {@link OutboundMessageType} subtype that acts identically to {@link DefaultFullHttpRequest}.
*
* <p>As it is an {@link OutboundMessageType} subtype, there is a {@code modifyMessage} method
* that modifies the request to reflect the new host and optional path. We also implement a {@code
* name} method, which returns a standard name and the current hostname.</p>
*/
public class HttpRequestMessage extends DefaultFullHttpRequest implements OutboundMessageType {
@Inject
public HttpRequestMessage() {
this(HttpVersion.HTTP_1_1, HttpMethod.GET, "");
}
private HttpRequestMessage(HttpVersion httpVersion, HttpMethod method, String uri) {
super(httpVersion, method, uri);
}
private HttpRequestMessage(HttpVersion httpVersion, HttpMethod method, String uri,
ByteBuf content) {
super(httpVersion, method, uri, content);
}
/**
* Used for conversion from {@link FullHttpRequest} to {@link HttpRequestMessage}
*/
public HttpRequestMessage(FullHttpRequest request) {
this(request.protocolVersion(), request.method(), request.uri(), request.content());
request.headers().forEach((entry) -> headers().set(entry.getKey(), entry.getValue()));
}
@Override
public HttpRequestMessage setUri(String path) {
super.setUri(path);
return this;
}
/**
* Modifies headers to reflect new host and new path if applicable.
*/
@Override
public HttpRequestMessage modifyMessage(String... args) throws IllegalArgumentException {
if (args.length == 1 || args.length == 2) {
headers().set("host", args[0]);
if (args.length == 2) {
setUri(args[1]);
}
return this;
} else {
throw new IllegalArgumentException(
String.format(
"Wrong number of arguments present for modifying HttpRequestMessage."
+ " Received %d arguments instead of 2. Received arguments: "
+ Arrays.toString(args), args.length));
}
}
@Override
public String toString() {
return String.format("Http(s) Request on: %s", headers().get("host"));
}
}

View file

@ -0,0 +1,40 @@
// 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 io.netty.buffer.ByteBuf;
import io.netty.handler.codec.http.DefaultFullHttpResponse;
import io.netty.handler.codec.http.FullHttpResponse;
import io.netty.handler.codec.http.HttpResponseStatus;
import io.netty.handler.codec.http.HttpVersion;
/**
* {@link InboundMessageType} subtype that acts identically to {@link DefaultFullHttpResponse}
*/
public class HttpResponseMessage extends DefaultFullHttpResponse implements InboundMessageType {
private HttpResponseMessage(HttpVersion version, HttpResponseStatus status, ByteBuf content) {
super(version, status, content);
}
/**
* Used for pipeline conversion from {@link FullHttpResponse} to {@link HttpResponseMessage}
*/
public HttpResponseMessage(FullHttpResponse response) {
this(response.protocolVersion(), response.status(), response.content());
response.headers().forEach((entry) -> headers().set(entry.getKey(), entry.getValue()));
}
}

View file

@ -18,4 +18,6 @@ package google.registry.monitoring.blackbox.messages;
* Marker Interface that is implemented by all classes that serve as {@code inboundMessages} in * Marker Interface that is implemented by all classes that serve as {@code inboundMessages} in
* channel pipeline * channel pipeline
*/ */
public interface InboundMessageType {} public interface InboundMessageType {
}

View file

@ -14,8 +14,24 @@
package google.registry.monitoring.blackbox.messages; package google.registry.monitoring.blackbox.messages;
import google.registry.monitoring.blackbox.exceptions.UndeterminedStateException;
/** /**
* Marker Interface that is implemented by all classes that serve as {@code outboundMessages} in * Marker Interface that is implemented by all classes that serve as {@code outboundMessages} in
* channel pipeline * channel pipeline
*/ */
public interface OutboundMessageType {} public interface OutboundMessageType {
/**
* All {@link OutboundMessageType} implementing classes should be able to be modified by token
* with String arguments
*/
OutboundMessageType modifyMessage(String... args) throws UndeterminedStateException;
/**
* Necessary to inform metrics collector what kind of message is sent down {@link
* io.netty.channel.ChannelPipeline}
*/
@Override
String toString();
}

View file

@ -0,0 +1,72 @@
// 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 google.registry.monitoring.blackbox.ProbingSequence;
import google.registry.monitoring.blackbox.ProbingStep;
import google.registry.monitoring.blackbox.exceptions.UndeterminedStateException;
import google.registry.monitoring.blackbox.messages.OutboundMessageType;
import io.netty.channel.Channel;
/**
* Superclass that represents information passed to each {@link ProbingStep} in a single loop of a
* {@link ProbingSequence}.
*
* <p>Modifies the message passed in to reflect information relevant to a single loop
* in a {@link ProbingSequence}. Additionally, passes on channel that remains unchanged within a
* loop of the sequence.</p>
*
* <p>Also obtains the next {@link Token} corresponding to the next iteration of a loop
* in the sequence.</p>
*/
public abstract class Token {
/**
* {@link Channel} that always starts out as null. Once a persistent connection is made (such as
* EPP), that channel is stored in the token and passed on to later steps in the sequence until a
* new loop begins.
*/
protected Channel channel;
/**
* Obtains next {@link Token} for next loop in sequence.
*/
public abstract Token next();
/**
* String corresponding to host that is relevant for loop in sequence.
*/
public abstract String host();
/**
* Modifies the {@link OutboundMessageType} in the manner necessary for each loop
*/
public abstract OutboundMessageType modifyMessage(OutboundMessageType messageType)
throws UndeterminedStateException;
/**
* Set method for {@code channel}
*/
public void setChannel(Channel channel) {
this.channel = channel;
}
/**
* Get method for {@code channel}.
*/
public Channel channel() {
return this.channel;
}
}

View file

@ -0,0 +1,82 @@
// 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 com.google.common.collect.ImmutableList;
import google.registry.monitoring.blackbox.WebWhoisModule.WebWhoisProtocol;
import google.registry.monitoring.blackbox.exceptions.UndeterminedStateException;
import google.registry.monitoring.blackbox.messages.OutboundMessageType;
import java.util.Iterator;
import javax.inject.Inject;
/**
* {@link Token} subtype designed for WebWhois sequence.
*
* <p>Between loops of a WebWhois sequence the only thing changing is the tld we
* are probing. As a result, we maintain the list of {@code topLevelDomains} and on each call to
* next, have our index looking at the next {@code topLevelDomain}. </p>
*/
public class WebWhoisToken extends Token {
/**
* For each top level domain (tld), we probe "prefix.tld".
*/
private static final String PREFIX = "whois.nic.";
/**
* {@link ImmutableList} of all top level domains to be probed.
*/
private final Iterator<String> topLevelDomainsIterator;
/**
* Current index of {@code topLevelDomains} that represents tld we are probing.
*/
private String currentDomain;
@Inject
public WebWhoisToken(@WebWhoisProtocol ImmutableList<String> topLevelDomains) {
topLevelDomainsIterator = topLevelDomains.iterator();
currentDomain = topLevelDomainsIterator.next();
}
/**
* Increments {@code domainsIndex} or resets it to reflect move to next top level domain.
*/
@Override
public WebWhoisToken next() {
currentDomain = topLevelDomainsIterator.next();
return this;
}
/**
* Modifies message to reflect the new host coming from the new top level domain.
*/
@Override
public OutboundMessageType modifyMessage(OutboundMessageType original)
throws UndeterminedStateException {
return original.modifyMessage(host());
}
/**
* Returns host as the concatenation of fixed {@code prefix} and current value of {@code
* topLevelDomains}.
*/
@Override
public String host() {
return PREFIX + currentDomain;
}
}

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