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 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 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 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 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 handlers) { + this(new NioEventLoopGroup(1), localAddress, handlers); + } + + public TestServer(EventLoopGroup eventLoopGroup, LocalAddress localAddress, + ImmutableList 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); + } + +}