diff --git a/prober/.gitignore b/prober/.gitignore
index 89f9ac04a..c86568e76 100644
--- a/prober/.gitignore
+++ b/prober/.gitignore
@@ -1 +1,2 @@
out/
+src/main/resources/google/registry/monitoring/blackbox/modules/secrets/
diff --git a/prober/build.gradle b/prober/build.gradle
index 8551d11f7..3652390d0 100644
--- a/prober/build.gradle
+++ b/prober/build.gradle
@@ -4,7 +4,7 @@
// 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
+// 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,
@@ -12,36 +12,44 @@
// See the License for the specific language governing permissions and
// limitations under the License.
+apply plugin: 'java'
+
createUberJar('deployJar', 'prober', 'google.registry.monitoring.blackbox.Prober')
dependencies {
- def deps = rootProject.dependencyMap
+ def deps = rootProject.dependencyMap
- compile deps['com.google.auto.value:auto-value-annotations']
- compile deps['com.google.dagger:dagger']
- compile deps['com.google.flogger:flogger']
- compile deps['com.google.guava:guava']
- compile deps['io.netty:netty-buffer']
- compile deps['io.netty:netty-codec-http']
- compile deps['io.netty:netty-codec']
- compile deps['io.netty:netty-common']
- compile deps['io.netty:netty-handler']
- compile deps['io.netty:netty-transport']
- compile deps['javax.inject:javax.inject']
+ compile deps['com.google.auto.value:auto-value-annotations']
+ compile deps['com.google.code.findbugs:jsr305']
+ compile deps['com.google.code.gson:gson']
+ compile deps['com.google.dagger:dagger']
+ compile deps['com.google.flogger:flogger']
+ compile deps['com.google.guava:guava']
+ compile deps['io.netty:netty-buffer']
+ compile deps['io.netty:netty-codec-http']
+ compile deps['io.netty:netty-codec']
+ compile deps['io.netty:netty-common']
+ 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.auto.value:auto-value']
- runtime deps['io.netty:netty-tcnative-boringssl-static']
+ runtime deps['com.google.flogger:flogger-system-backend']
+ runtime deps['com.google.auto.value:auto-value']
+ runtime deps['io.netty:netty-tcnative-boringssl-static']
- testCompile deps['com.google.truth:truth']
- testCompile deps['junit:junit']
- testCompile deps['org.mockito:mockito-core']
- testCompile project(':third_party')
+ testCompile deps['com.google.truth:truth']
+ testCompile deps['junit:junit']
+ testCompile deps['org.mockito:mockito-core']
+ testCompile project(':third_party')
- // Include auto-value in compile until nebula-lint understands
- // annotationProcessor
- annotationProcessor deps['com.google.auto.value:auto-value']
- testAnnotationProcessor deps['com.google.auto.value:auto-value']
- annotationProcessor deps['com.google.dagger:dagger-compiler']
- testAnnotationProcessor deps['com.google.dagger:dagger-compiler']
+ // Include auto-value in compile until nebula-lint understands
+ // annotationProcessor
+ annotationProcessor deps['com.google.auto.value:auto-value']
+ testAnnotationProcessor deps['com.google.auto.value:auto-value']
+ annotationProcessor deps['com.google.dagger:dagger-compiler']
+ testAnnotationProcessor deps['com.google.dagger:dagger-compiler']
}
diff --git a/prober/src/main/java/google/registry/monitoring/blackbox/Prober.java b/prober/src/main/java/google/registry/monitoring/blackbox/Prober.java
new file mode 100644
index 000000000..433d7361d
--- /dev/null
+++ b/prober/src/main/java/google/registry/monitoring/blackbox/Prober.java
@@ -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 sequences = ImmutableSet.copyOf(proberComponent.sequences());
+
+ //Tells Sequences to start running
+ for (ProbingSequence sequence : sequences) {
+ sequence.start();
+ }
+ }
+}
diff --git a/prober/src/main/java/google/registry/monitoring/blackbox/ProberModule.java b/prober/src/main/java/google/registry/monitoring/blackbox/ProberModule.java
new file mode 100644
index 000000000..e5a716361
--- /dev/null
+++ b/prober/src/main/java/google/registry/monitoring/blackbox/ProberModule.java
@@ -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 sequences();
+
+ }
+}
diff --git a/prober/src/main/java/google/registry/monitoring/blackbox/ProbingAction.java b/prober/src/main/java/google/registry/monitoring/blackbox/ProbingAction.java
new file mode 100644
index 000000000..8173ba6ba
--- /dev/null
+++ b/prober/src/main/java/google/registry/monitoring/blackbox/ProbingAction.java
@@ -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}
+ *
+ * Inherits from {@link Callable}, 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
+ *
+ * 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.
+ *
+ * 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.
+ */
+
+@AutoValue
+public abstract class ProbingAction implements Callable {
+
+ /**
+ * {@link AttributeKey} in channel that gives {@link ChannelFuture} that is set to success when
+ * channel is active.
+ */
+ public static final AttributeKey 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 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> 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.
+ *
+ * First, checks if channel is active by setting a listener to perform the bulk of the work
+ * when the connection future is successful.
+ *
+ * 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.
+ *
+ * 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.
+ *
+ * @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() {
+ @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();
+ }
+ }
+}
diff --git a/prober/src/main/java/google/registry/monitoring/blackbox/ProbingSequence.java b/prober/src/main/java/google/registry/monitoring/blackbox/ProbingSequence.java
new file mode 100644
index 000000000..7f97f8bd5
--- /dev/null
+++ b/prober/src/main/java/google/registry/monitoring/blackbox/ProbingSequence.java
@@ -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.
+ *
+ *
+ * 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.
+ *
+ * {@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.
+ */
+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);
+ }
+ }
+}
+
diff --git a/prober/src/main/java/google/registry/monitoring/blackbox/ProbingStep.java b/prober/src/main/java/google/registry/monitoring/blackbox/ProbingStep.java
new file mode 100644
index 000000000..35bb14124
--- /dev/null
+++ b/prober/src/main/java/google/registry/monitoring/blackbox/ProbingStep.java
@@ -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}.
+ *
+ * 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.
+ */
+@AutoValue
+public abstract class ProbingStep implements Consumer {
+
+ 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}.
+ *
+ * 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.
+ */
+ @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();
+ }
+
+}
+
+
diff --git a/prober/src/main/java/google/registry/monitoring/blackbox/Protocol.java b/prober/src/main/java/google/registry/monitoring/blackbox/Protocol.java
index 271af3c67..ea2a61303 100644
--- a/prober/src/main/java/google/registry/monitoring/blackbox/Protocol.java
+++ b/prober/src/main/java/google/registry/monitoring/blackbox/Protocol.java
@@ -17,44 +17,64 @@ package google.registry.monitoring.blackbox;
import com.google.auto.value.AutoValue;
import com.google.common.collect.ImmutableList;
import io.netty.channel.ChannelHandler;
+import io.netty.util.AttributeKey;
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
public abstract class Protocol {
- abstract String name();
-
- public abstract int port();
-
- /** The {@link ChannelHandler} providers to use for the protocol, in order. */
- abstract ImmutableList> handlerProviders();
-
- /** Boolean that notes if connection associated with Protocol is persistent.*/
- abstract boolean persistentConnection();
-
- public abstract Builder toBuilder();
+ /**
+ * {@link AttributeKey} that lets channel reference {@link Protocol} that created it.
+ */
+ public static final AttributeKey PROTOCOL_KEY = AttributeKey.valueOf("PROTOCOL_KEY");
public static Builder 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> 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
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> providers);
- public abstract Builder persistentConnection(boolean value);
+ public abstract Builder setPersistentConnection(boolean value);
public abstract Protocol build();
}
}
-
diff --git a/prober/src/main/java/google/registry/monitoring/blackbox/WebWhoisModule.java b/prober/src/main/java/google/registry/monitoring/blackbox/WebWhoisModule.java
new file mode 100644
index 000000000..8365607da
--- /dev/null
+++ b/prober/src/main/java/google/registry/monitoring/blackbox/WebWhoisModule.java
@@ -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> 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> 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> providerHttpWhoisHandlerProviders(
+ Provider httpClientCodecProvider,
+ Provider httpObjectAggregatorProvider,
+ Provider messageHandlerProvider,
+ Provider 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> providerHttpsWhoisHandlerProviders(
+ @HttpsWhoisProtocol
+ Provider> sslClientInitializerProvider,
+ Provider httpClientCodecProvider,
+ Provider httpObjectAggregatorProvider,
+ Provider messageHandlerProvider,
+ Provider 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 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 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 {
+
+ }
+
+
+}
diff --git a/prober/src/main/java/google/registry/monitoring/blackbox/exceptions/ConnectionException.java b/prober/src/main/java/google/registry/monitoring/blackbox/exceptions/ConnectionException.java
new file mode 100644
index 000000000..776a231d6
--- /dev/null
+++ b/prober/src/main/java/google/registry/monitoring/blackbox/exceptions/ConnectionException.java
@@ -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);
+ }
+}
diff --git a/prober/src/main/java/google/registry/monitoring/blackbox/exceptions/FailureException.java b/prober/src/main/java/google/registry/monitoring/blackbox/exceptions/FailureException.java
new file mode 100644
index 000000000..36687e7ac
--- /dev/null
+++ b/prober/src/main/java/google/registry/monitoring/blackbox/exceptions/FailureException.java
@@ -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);
+ }
+}
diff --git a/prober/src/main/java/google/registry/monitoring/blackbox/exceptions/UndeterminedStateException.java b/prober/src/main/java/google/registry/monitoring/blackbox/exceptions/UndeterminedStateException.java
new file mode 100644
index 000000000..d7127717d
--- /dev/null
+++ b/prober/src/main/java/google/registry/monitoring/blackbox/exceptions/UndeterminedStateException.java
@@ -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);
+ }
+}
diff --git a/prober/src/main/java/google/registry/monitoring/blackbox/handlers/ActionHandler.java b/prober/src/main/java/google/registry/monitoring/blackbox/handlers/ActionHandler.java
index f0d9ac023..1e1a83423 100644
--- a/prober/src/main/java/google/registry/monitoring/blackbox/handlers/ActionHandler.java
+++ b/prober/src/main/java/google/registry/monitoring/blackbox/handlers/ActionHandler.java
@@ -15,67 +15,99 @@
package google.registry.monitoring.blackbox.handlers;
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.OutboundMessageType;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelPromise;
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
*
- * {@code ActionHandler} inherits from {@link SimpleChannelInboundHandler< InboundMessageType >},
- * as it should only be passed in messages that implement the {@link InboundMessageType} interface.
+ *
{@link ActionHandler} inherits from {@link SimpleChannelInboundHandler},
+ * as it should only be passed in messages that implement the {@link InboundMessageType}
+ * interface.
*
- * The {@code ActionHandler} skeleton exists for a few main purposes. First, it returns a {@link
- * ChannelPromise}, which informs the {@link ProbingAction} in charge that a response has been read.
- * Second, it stores the {@link OutboundMessageType} passed down the pipeline, so that subclasses
- * can use that information for their own processes. Lastly, with any exception thrown, the
- * connection is closed, and the ProbingAction governing this channel is informed of the error.
- * Subclasses specify further work to be done for specific kinds of channel pipelines.
+ *
The {@link ActionHandler} skeleton exists for a few main purposes. First, it returns a
+ * {@link ChannelPromise}, which informs the {@link ProbingAction} in charge that a response has
+ * been read. Second, with any exception thrown, the connection is closed, and the ProbingAction
+ * governing this channel is informed of the error. If the error is an instance of a {@link
+ * FailureException} {@code finished} is marked as a failure with cause {@link FailureException}. If
+ * 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.
+ *
+ * Subclasses specify further work to be done for specific kinds of channel pipelines.
*/
public abstract class ActionHandler extends SimpleChannelInboundHandler {
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
- * {@link ChannelPromise}
+ * Returns initialized {@link ChannelPromise} to {@link ProbingAction}.
*/
- public ChannelFuture getFuture() {
+ public ChannelFuture getFinishedFuture() {
return finished;
}
- /** Initializes new {@link ChannelPromise} */
+ /**
+ * Initializes {@link ChannelPromise}
+ */
@Override
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();
}
+ /**
+ * Marks {@link ChannelPromise} as success
+ */
@Override
public void channelRead0(ChannelHandlerContext ctx, InboundMessageType inboundMessage)
- throws Exception {
- // simply marks finished as success
- finished = finished.setSuccess();
+ throws FailureException, UndeterminedStateException {
+
+ ChannelFuture unusedFuture = finished.setSuccess();
}
/**
* Logs the channel and pipeline that caused error, closes channel, then informs {@link
- * ProbingAction} listeners of error
+ * ProbingAction} listeners of error.
*/
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
- logger.atSevere().withCause(cause).log(
- String.format(
- "Attempted Action was unsuccessful with channel: %s, having pipeline: %s",
- ctx.channel().toString(), ctx.channel().pipeline().toString()));
+ logger.atWarning().withCause(cause).log(String.format(
+ "Attempted Action was unsuccessful with channel: %s, having pipeline: %s",
+ ctx.channel().toString(),
+ ctx.channel().pipeline().toString()));
- finished = finished.setFailure(cause);
- ChannelFuture closedFuture = ctx.channel().close();
- closedFuture.addListener(f -> logger.atInfo().log("Unsuccessful channel connection closed"));
+ if (cause instanceof FailureException) {
+ //On FailureException, we know the response is a failure.
+
+ //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"));
+ }
}
}
diff --git a/prober/src/main/java/google/registry/monitoring/blackbox/handlers/SslClientInitializer.java b/prober/src/main/java/google/registry/monitoring/blackbox/handlers/SslClientInitializer.java
new file mode 100644
index 000000000..c62668b19
--- /dev/null
+++ b/prober/src/main/java/google/registry/monitoring/blackbox/handlers/SslClientInitializer.java
@@ -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.
+ *
+ * Code is close to unchanged from {@link SslClientInitializer}
in proxy, but is modified
+ * for revised overall structure of connections, and to accomdate EPP connections
+ *
+ * This must 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 extends ChannelInitializer {
+
+ private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
+ private final SslProvider sslProvider;
+ private final X509Certificate[] trustedCertificates;
+ private final Supplier privateKeySupplier;
+ private final Supplier 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 privateKeySupplier,
+ Supplier 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 privateKeySupplier,
+ Supplier 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);
+ }
+}
+
diff --git a/prober/src/main/java/google/registry/monitoring/blackbox/handlers/WebWhoisActionHandler.java b/prober/src/main/java/google/registry/monitoring/blackbox/handlers/WebWhoisActionHandler.java
new file mode 100644
index 000000000..c56c8c7d9
--- /dev/null
+++ b/prober/src/main/java/google/registry/monitoring/blackbox/handlers/WebWhoisActionHandler.java
@@ -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
+ *
+ * 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
+ */
+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());
+
+ }
+ }
+
+
+}
+
diff --git a/prober/src/main/java/google/registry/monitoring/blackbox/handlers/WebWhoisMessageHandler.java b/prober/src/main/java/google/registry/monitoring/blackbox/handlers/WebWhoisMessageHandler.java
new file mode 100644
index 000000000..576d6a04d
--- /dev/null
+++ b/prober/src/main/java/google/registry/monitoring/blackbox/handlers/WebWhoisMessageHandler.java
@@ -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);
+ }
+}
diff --git a/prober/src/main/java/google/registry/monitoring/blackbox/messages/HttpRequestMessage.java b/prober/src/main/java/google/registry/monitoring/blackbox/messages/HttpRequestMessage.java
new file mode 100644
index 000000000..b850cfb3b
--- /dev/null
+++ b/prober/src/main/java/google/registry/monitoring/blackbox/messages/HttpRequestMessage.java
@@ -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}.
+ *
+ * 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.
+ */
+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"));
+ }
+
+}
diff --git a/prober/src/main/java/google/registry/monitoring/blackbox/messages/HttpResponseMessage.java b/prober/src/main/java/google/registry/monitoring/blackbox/messages/HttpResponseMessage.java
new file mode 100644
index 000000000..7d2ff7220
--- /dev/null
+++ b/prober/src/main/java/google/registry/monitoring/blackbox/messages/HttpResponseMessage.java
@@ -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()));
+ }
+}
diff --git a/prober/src/main/java/google/registry/monitoring/blackbox/messages/InboundMessageType.java b/prober/src/main/java/google/registry/monitoring/blackbox/messages/InboundMessageType.java
index 84b77f89d..0a584dfa2 100644
--- a/prober/src/main/java/google/registry/monitoring/blackbox/messages/InboundMessageType.java
+++ b/prober/src/main/java/google/registry/monitoring/blackbox/messages/InboundMessageType.java
@@ -18,4 +18,6 @@ package google.registry.monitoring.blackbox.messages;
* Marker Interface that is implemented by all classes that serve as {@code inboundMessages} in
* channel pipeline
*/
-public interface InboundMessageType {}
+public interface InboundMessageType {
+
+}
diff --git a/prober/src/main/java/google/registry/monitoring/blackbox/messages/OutboundMessageType.java b/prober/src/main/java/google/registry/monitoring/blackbox/messages/OutboundMessageType.java
index e393e47ff..4a7c16243 100644
--- a/prober/src/main/java/google/registry/monitoring/blackbox/messages/OutboundMessageType.java
+++ b/prober/src/main/java/google/registry/monitoring/blackbox/messages/OutboundMessageType.java
@@ -14,8 +14,24 @@
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
* 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();
+}
diff --git a/prober/src/main/java/google/registry/monitoring/blackbox/tokens/Token.java b/prober/src/main/java/google/registry/monitoring/blackbox/tokens/Token.java
new file mode 100644
index 000000000..b10c45dae
--- /dev/null
+++ b/prober/src/main/java/google/registry/monitoring/blackbox/tokens/Token.java
@@ -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}.
+ *
+ * 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.
+ *
+ * Also obtains the next {@link Token} corresponding to the next iteration of a loop
+ * in the sequence.
+ */
+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;
+ }
+}
diff --git a/prober/src/main/java/google/registry/monitoring/blackbox/tokens/WebWhoisToken.java b/prober/src/main/java/google/registry/monitoring/blackbox/tokens/WebWhoisToken.java
new file mode 100644
index 000000000..3ff852f35
--- /dev/null
+++ b/prober/src/main/java/google/registry/monitoring/blackbox/tokens/WebWhoisToken.java
@@ -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.
+ *
+ * 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}.
+ */
+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 topLevelDomainsIterator;
+
+ /**
+ * Current index of {@code topLevelDomains} that represents tld we are probing.
+ */
+ private String currentDomain;
+
+ @Inject
+ public WebWhoisToken(@WebWhoisProtocol ImmutableList 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;
+ }
+}
+
diff --git a/prober/src/test/java/google/registry/monitoring/blackbox/ProbingActionTest.java b/prober/src/test/java/google/registry/monitoring/blackbox/ProbingActionTest.java
new file mode 100644
index 000000000..df3296438
--- /dev/null
+++ b/prober/src/test/java/google/registry/monitoring/blackbox/ProbingActionTest.java
@@ -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
+ *
+ * Attempts to test how well each {@link ProbingAction} works with an {@link ActionHandler}
+ * subtype when receiving to all possible types of responses
+ */
+@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);
+ }
+}
+
diff --git a/prober/src/test/java/google/registry/monitoring/blackbox/ProbingSequenceTest.java b/prober/src/test/java/google/registry/monitoring/blackbox/ProbingSequenceTest.java
new file mode 100644
index 000000000..02599bcb5
--- /dev/null
+++ b/prober/src/test/java/google/registry/monitoring/blackbox/ProbingSequenceTest.java
@@ -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);
+ }
+
+}
diff --git a/prober/src/test/java/google/registry/monitoring/blackbox/ProbingStepTest.java b/prober/src/test/java/google/registry/monitoring/blackbox/ProbingStepTest.java
new file mode 100644
index 000000000..db7ec2719
--- /dev/null
+++ b/prober/src/test/java/google/registry/monitoring/blackbox/ProbingStepTest.java
@@ -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));
+ }
+}
diff --git a/prober/src/test/java/google/registry/monitoring/blackbox/TestUtils.java b/prober/src/test/java/google/registry/monitoring/blackbox/TestUtils.java
new file mode 100644
index 000000000..66e2928f4
--- /dev/null
+++ b/prober/src/test/java/google/registry/monitoring/blackbox/TestUtils.java
@@ -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;
+ }
+}
+
diff --git a/prober/src/test/java/google/registry/monitoring/blackbox/handlers/ConversionHandler.java b/prober/src/test/java/google/registry/monitoring/blackbox/handlers/ConversionHandler.java
new file mode 100644
index 000000000..987abbfd7
--- /dev/null
+++ b/prober/src/test/java/google/registry/monitoring/blackbox/handlers/ConversionHandler.java
@@ -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}
+ *
+ * Specific type of {@link OutboundMessageType} and {@link InboundMessageType}
+ * used for conversion is the {@link TestMessage} type.
+ */
+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);
+ }
+}
+
diff --git a/prober/src/test/java/google/registry/monitoring/blackbox/handlers/NettyRule.java b/prober/src/test/java/google/registry/monitoring/blackbox/handlers/NettyRule.java
new file mode 100644
index 000000000..4b6eed9c1
--- /dev/null
+++ b/prober/src/test/java/google/registry/monitoring/blackbox/handlers/NettyRule.java
@@ -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.
+ *
+ * Code based on and almost identical to {@code NettyRule} in the proxy.
+ * Used in {@link SslClientInitializerTest}, {@link ProbingActionTest}, and {@link ProbingStepTest}
+ *
+ */
+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.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 clientInitializer =
+ new ChannelInitializer() {
+ @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.
+ *
+ * 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 requestFuture = new CompletableFuture<>();
+
+ public Future 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 responseFuture = new CompletableFuture<>();
+
+ Future 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));
+ }
+ }
+}
+
diff --git a/prober/src/test/java/google/registry/monitoring/blackbox/handlers/SslClientInitializerTest.java b/prober/src/test/java/google/registry/monitoring/blackbox/handlers/SslClientInitializerTest.java
new file mode 100644
index 000000000..2a9f9758e
--- /dev/null
+++ b/prober/src/test/java/google/registry/monitoring/blackbox/handlers/SslClientInitializerTest.java
@@ -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}.
+ *
+ * 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.
+ *
+ *
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 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 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 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 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 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();
+ }
+}
+
diff --git a/prober/src/test/java/google/registry/monitoring/blackbox/handlers/SslInitializerTestUtils.java b/prober/src/test/java/google/registry/monitoring/blackbox/handlers/SslInitializerTestUtils.java
new file mode 100644
index 000000000..68b291914
--- /dev/null
+++ b/prober/src/test/java/google/registry/monitoring/blackbox/handlers/SslInitializerTestUtils.java
@@ -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();
+ }
+}
+
diff --git a/prober/src/test/java/google/registry/monitoring/blackbox/handlers/TestActionHandler.java b/prober/src/test/java/google/registry/monitoring/blackbox/handlers/TestActionHandler.java
new file mode 100644
index 000000000..19e608c97
--- /dev/null
+++ b/prober/src/test/java/google/registry/monitoring/blackbox/handlers/TestActionHandler.java
@@ -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;
+ }
+
+}
+
diff --git a/prober/src/test/java/google/registry/monitoring/blackbox/handlers/WebWhoisActionHandlerTest.java b/prober/src/test/java/google/registry/monitoring/blackbox/handlers/WebWhoisActionHandlerTest.java
new file mode 100644
index 000000000..a4c2c57dc
--- /dev/null
+++ b/prober/src/test/java/google/registry/monitoring/blackbox/handlers/WebWhoisActionHandlerTest.java
@@ -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}.
+ *
+ * Attempts to test how well {@link WebWhoisActionHandler} works
+ * when responding to all possible types of responses
+ */
+@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();
+ }
+}
diff --git a/prober/src/test/java/google/registry/monitoring/blackbox/messages/TestMessage.java b/prober/src/test/java/google/registry/monitoring/blackbox/messages/TestMessage.java
new file mode 100644
index 000000000..922c3052b
--- /dev/null
+++ b/prober/src/test/java/google/registry/monitoring/blackbox/messages/TestMessage.java
@@ -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;
+ }
+}
diff --git a/prober/src/test/java/google/registry/monitoring/blackbox/testservers/TestServer.java b/prober/src/test/java/google/registry/monitoring/blackbox/testservers/TestServer.java
new file mode 100644
index 000000000..c39fe5543
--- /dev/null
+++ b/prober/src/test/java/google/registry/monitoring/blackbox/testservers/TestServer.java
@@ -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 serverInitializer = new ChannelInitializer() {
+ @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 {
+
+ 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);
+
+ }
+ }
+}
diff --git a/prober/src/test/java/google/registry/monitoring/blackbox/tokens/WebWhoisTokenTest.java b/prober/src/test/java/google/registry/monitoring/blackbox/tokens/WebWhoisTokenTest.java
new file mode 100644
index 000000000..9a3e75536
--- /dev/null
+++ b/prober/src/test/java/google/registry/monitoring/blackbox/tokens/WebWhoisTokenTest.java
@@ -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 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);
+ }
+
+}