mirror of
https://github.com/google/nomulus.git
synced 2025-07-07 19:53:30 +02:00
Add full prober WebWHOIS sequence functionality (#180)
* Initial Commit. * Deleted unfinished features. Added ActionHandler and its Unit Tests. * Included prober subproject in settings.gradle * Added Protocol Class and its Basic Unit Tests * Added Changes Suggested by jianglai * Fixed Gitignore to take out AutoValue generated code * Removed AutoValue java files * Added gitignore within prober * Removed all generated java * Added Ssl and WebWhois Action Handlers and their unit tests in addition to the ProbingAction class * Fixed build.gradle changes requested * Removed Files irrelevant to current pull request * Minor fixes to ActionHandler, as responded in comments, removed package-info, and updated settings.gradle * Fully Updated ActionHandler (missing updated JavaDoc) * Added changed Protocol and both Inbound and Outbound Markers * Removed AutoVaue ignore clause from .gitignore * removed unneccessary dependencies in build.gradle * Fixed Javadoc and comments for ActionHandler * Fixed comments and JavaDoc on other files * EOL added * Removed Unnecessary Files * fixed .gradle files styles * Removed outbound message from ActionHandler's fields and renamed Marker Interfaces * Fixed javadoc for Marker Interfaced * Modified Comments on ActionHandler * Removed LocalAddress from Protocol * Fixed Travis Build Issues * Rebased to Master and added in modified Handlers and ProbingAction * Fixed changes suggested by CydeWeys * Added missing license headers and JavaDoc * Minor fix in NewChannelAction JavaDoc * Minor Style Fix * Full WebWhoIs Sequence Added * fixed build issues * Refactored by responses suggested by jianglai. * Initial Commit. * Deleted unfinished features. Added ActionHandler and its Unit Tests. * Included prober subproject in settings.gradle * Added Protocol Class and its Basic Unit Tests * Added Changes Suggested by jianglai * Fixed Gitignore to take out AutoValue generated code * Removed AutoValue java files * Added gitignore within prober * Removed all generated java * Final Changes in .gitignore * Added Ssl and WebWhois Action Handlers and their unit tests in addition to the ProbingAction class * Fixed build.gradle changes requested * Removed Files irrelevant to current pull request * Fixed changes suggested by CydeWeys * Minor fixes to ActionHandler, as responded in comments, removed package-info, and updated settings.gradle * Fully Updated ActionHandler (missing updated JavaDoc) * Added changed Protocol and both Inbound and Outbound Markers * Removed AutoVaue ignore clause from .gitignore * removed unneccessary dependencies in build.gradle * Fixed Javadoc and comments for ActionHandler * Fixed comments and JavaDoc on other files * EOL added * Removed Unnecessary Files * fixed .gradle files styles * Rebased to Master and added in modified Handlers and ProbingAction * Added missing license headers and JavaDoc * Minor fix in NewChannelAction JavaDoc * Minor Style Fix * Full WebWhoIs Sequence Added * fixed build issues * Refactored by responses suggested by jianglai. * Minor Style Fixes * Minor Style Fixes * Updated build.gradle file * Updated build.gradle file * Modified license header dates * Modified license header dates * Updated WebWhois tests. * Updated WebWhois tests. * Refactored WebWhois to accomodate jianglai's suggested changes and modified tests to reflect this refactoring * Refactored WebWhois to accomodate jianglai's suggested changes and modified tests to reflect this refactoring * SpotlessApply run to fix style issues * SpotlessApply run to fix style issues * Added license header and newline where appropriate. * Added license header and newline where appropriate. * Javadoc style fix in tests and removed unused methods * Javadoc style fix in tests and removed unused methods * Refactored ProbingAction to minimize number of unnecessary methods * Refactored ProbingAction to minimize number of unnecessary methods * Modified tests for WebWhois according to changes suggested by laijiang. * Modified tests for WebWhois according to changes suggested by laijiang. * Removed TestProvider from TestUtils. * Removed TestProvider from TestUtils. * Rebased to master * Updated issues in rebasing * Minor style change on prober/build.gradle * Fixed warnings for java compilation * Fixed files to pass all style tests * Removed outbound message from ActionHandler's fields and renamed Marker Interfaces * Initial Commit. * Deleted unfinished features. Added ActionHandler and its Unit Tests. * Included prober subproject in settings.gradle * Added Protocol Class and its Basic Unit Tests * Added Changes Suggested by jianglai * Fixed Gitignore to take out AutoValue generated code * Removed AutoValue java files * Added gitignore within prober * Removed all generated java * Final Changes in .gitignore * Added Ssl and WebWhois Action Handlers and their unit tests in addition to the ProbingAction class * Fixed build.gradle changes requested * Removed Files irrelevant to current pull request * Fixed changes suggested by CydeWeys * Fixed changes suggested by CydeWeys * Fixed changes suggested by CydeWeys * Minor fixes to ActionHandler, as responded in comments, removed package-info, and updated settings.gradle * Fully Updated ActionHandler (missing updated JavaDoc) * Added changed Protocol and both Inbound and Outbound Markers * Removed AutoVaue ignore clause from .gitignore * removed unneccessary dependencies in build.gradle * Fixed Javadoc and comments for ActionHandler * Fixed comments and JavaDoc on other files * EOL added * Removed Unnecessary Files * fixed .gradle files styles * Removed outbound message from ActionHandler's fields and renamed Marker Interfaces * Fixed javadoc for Marker Interfaced * Fixed javadoc for Marker Interfaced * Modified Comments on ActionHandler * Modified Comments on ActionHandler * Removed LocalAddress from Protocol * Removed LocalAddress from Protocol * Fixed Travis Build Issues * Fixed Travis Build Issues * Rebased to Master and added in modified Handlers and ProbingAction * Rebased to Master and added in modified Handlers and ProbingAction * Rebased to Master and added in modified Handlers and ProbingAction * Added missing license headers and JavaDoc * Added missing license headers and JavaDoc * Minor fix in NewChannelAction JavaDoc * Minor fix in NewChannelAction JavaDoc * Minor Style Fix * Minor Style Fix * Full WebWhoIs Sequence Added * Full WebWhoIs Sequence Added * fixed build issues * fixed build issues * Refactored by responses suggested by jianglai. * Refactored by responses suggested by jianglai. * Minor Style Fixes * Minor Style Fixes * Updated build.gradle file * Updated build.gradle file * Modified license header dates * Modified license header dates * Updated WebWhois tests. * Updated WebWhois tests. * Refactored WebWhois to accomodate jianglai's suggested changes and modified tests to reflect this refactoring * Refactored WebWhois to accomodate jianglai's suggested changes and modified tests to reflect this refactoring * SpotlessApply run to fix style issues * SpotlessApply run to fix style issues * Added license header and newline where appropriate. * Added license header and newline where appropriate. * Javadoc style fix in tests and removed unused methods * Javadoc style fix in tests and removed unused methods * Refactored ProbingAction to minimize number of unnecessary methods * Refactored ProbingAction to minimize number of unnecessary methods * Modified tests for WebWhois according to changes suggested by laijiang. * Modified tests for WebWhois according to changes suggested by laijiang. * Removed TestProvider from TestUtils. * Removed TestProvider from TestUtils. * Rebased to master * Updated issues in rebasing * Minor style change on prober/build.gradle * Fixed warnings for java compilation * Fixed files to pass all style tests * Fixed changes suggested by CydeWeys * Rebased to Master and added in modified Handlers and ProbingAction * Added missing license headers and JavaDoc * Added missing license headers and JavaDoc * Minor fix in NewChannelAction JavaDoc * Minor fix in NewChannelAction JavaDoc * Minor Style Fix * Minor Style Fix * Full WebWhoIs Sequence Added * Full WebWhoIs Sequence Added * fixed build issues * fixed build issues * Refactored by responses suggested by jianglai. * Refactored by responses suggested by jianglai. * Minor Style Fixes * Minor Style Fixes * Updated build.gradle file * Updated build.gradle file * Modified license header dates * Modified license header dates * Updated WebWhois tests. * Updated WebWhois tests. * Refactored WebWhois to accomodate jianglai's suggested changes and modified tests to reflect this refactoring * Refactored WebWhois to accomodate jianglai's suggested changes and modified tests to reflect this refactoring * SpotlessApply run to fix style issues * SpotlessApply run to fix style issues * Added license header and newline where appropriate. * Added license header and newline where appropriate. * Javadoc style fix in tests and removed unused methods * Javadoc style fix in tests and removed unused methods * Refactored ProbingAction to minimize number of unnecessary methods * Refactored ProbingAction to minimize number of unnecessary methods * Modified tests for WebWhois according to changes suggested by laijiang. * Modified tests for WebWhois according to changes suggested by laijiang. * Removed TestProvider from TestUtils. * Removed TestProvider from TestUtils. * Rebased to master * Updated issues in rebasing * Minor style change on prober/build.gradle * Fixed warnings for java compilation * Fixed files to pass all style tests * Minor syle fixes after succesful rebase onto master
This commit is contained in:
parent
c7478fc52b
commit
27b81ba898
35 changed files with 3545 additions and 73 deletions
1
prober/.gitignore
vendored
1
prober/.gitignore
vendored
|
@ -1 +1,2 @@
|
||||||
out/
|
out/
|
||||||
|
src/main/resources/google/registry/monitoring/blackbox/modules/secrets/
|
||||||
|
|
|
@ -12,12 +12,16 @@
|
||||||
// See the License for the specific language governing permissions and
|
// See the License for the specific language governing permissions and
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
|
apply plugin: 'java'
|
||||||
|
|
||||||
createUberJar('deployJar', 'prober', 'google.registry.monitoring.blackbox.Prober')
|
createUberJar('deployJar', 'prober', 'google.registry.monitoring.blackbox.Prober')
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
def deps = rootProject.dependencyMap
|
def deps = rootProject.dependencyMap
|
||||||
|
|
||||||
compile deps['com.google.auto.value:auto-value-annotations']
|
compile deps['com.google.auto.value:auto-value-annotations']
|
||||||
|
compile deps['com.google.code.findbugs:jsr305']
|
||||||
|
compile deps['com.google.code.gson:gson']
|
||||||
compile deps['com.google.dagger:dagger']
|
compile deps['com.google.dagger:dagger']
|
||||||
compile deps['com.google.flogger:flogger']
|
compile deps['com.google.flogger:flogger']
|
||||||
compile deps['com.google.guava:guava']
|
compile deps['com.google.guava:guava']
|
||||||
|
@ -28,6 +32,10 @@ dependencies {
|
||||||
compile deps['io.netty:netty-handler']
|
compile deps['io.netty:netty-handler']
|
||||||
compile deps['io.netty:netty-transport']
|
compile deps['io.netty:netty-transport']
|
||||||
compile deps['javax.inject:javax.inject']
|
compile deps['javax.inject:javax.inject']
|
||||||
|
compile deps['joda-time:joda-time']
|
||||||
|
compile deps['org.bouncycastle:bcpkix-jdk15on']
|
||||||
|
compile deps['org.bouncycastle:bcprov-jdk15on']
|
||||||
|
compile project(':util')
|
||||||
|
|
||||||
runtime deps['com.google.flogger:flogger-system-backend']
|
runtime deps['com.google.flogger:flogger-system-backend']
|
||||||
runtime deps['com.google.auto.value:auto-value']
|
runtime deps['com.google.auto.value:auto-value']
|
||||||
|
|
|
@ -0,0 +1,43 @@
|
||||||
|
// Copyright 2019 The Nomulus Authors. All Rights Reserved.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package google.registry.monitoring.blackbox;
|
||||||
|
|
||||||
|
import com.google.common.collect.ImmutableSet;
|
||||||
|
import google.registry.monitoring.blackbox.ProberModule.ProberComponent;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Main class of the Prober, which obtains and starts the {@link ProbingSequence}s provided by
|
||||||
|
* Dagger.
|
||||||
|
*/
|
||||||
|
public class Prober {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Main Dagger Component
|
||||||
|
*/
|
||||||
|
private static ProberComponent proberComponent = DaggerProberModule_ProberComponent.builder()
|
||||||
|
.build();
|
||||||
|
|
||||||
|
|
||||||
|
public static void main(String[] args) {
|
||||||
|
|
||||||
|
//Obtains WebWhois Sequence provided by proberComponent
|
||||||
|
ImmutableSet<ProbingSequence> sequences = ImmutableSet.copyOf(proberComponent.sequences());
|
||||||
|
|
||||||
|
//Tells Sequences to start running
|
||||||
|
for (ProbingSequence sequence : sequences) {
|
||||||
|
sequence.start();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,97 @@
|
||||||
|
// Copyright 2019 The Nomulus Authors. All Rights Reserved.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package google.registry.monitoring.blackbox;
|
||||||
|
|
||||||
|
import dagger.Component;
|
||||||
|
import dagger.Module;
|
||||||
|
import dagger.Provides;
|
||||||
|
import io.netty.channel.Channel;
|
||||||
|
import io.netty.channel.EventLoopGroup;
|
||||||
|
import io.netty.channel.nio.NioEventLoopGroup;
|
||||||
|
import io.netty.channel.socket.nio.NioSocketChannel;
|
||||||
|
import io.netty.handler.ssl.OpenSsl;
|
||||||
|
import io.netty.handler.ssl.SslProvider;
|
||||||
|
import java.util.Set;
|
||||||
|
import javax.inject.Singleton;
|
||||||
|
import org.joda.time.Duration;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dagger main module, which {@link Provides} all objects that are shared between sequences and
|
||||||
|
* stores {@link ProberComponent}, which allows main {@link Prober} class to obtain each {@link
|
||||||
|
* ProbingSequence}.
|
||||||
|
*/
|
||||||
|
@Module
|
||||||
|
public class ProberModule {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Default {@link Duration} chosen to be time between each {@link ProbingAction} call.
|
||||||
|
*/
|
||||||
|
private static final Duration DEFAULT_DURATION = Duration.standardSeconds(4);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@link Provides} the {@link SslProvider} used by instances of {@link
|
||||||
|
* google.registry.monitoring.blackbox.handlers.SslClientInitializer}
|
||||||
|
*/
|
||||||
|
@Provides
|
||||||
|
@Singleton
|
||||||
|
static SslProvider provideSslProvider() {
|
||||||
|
// Prefer OpenSSL.
|
||||||
|
return OpenSsl.isAvailable() ? SslProvider.OPENSSL : SslProvider.JDK;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@link Provides} one global {@link EventLoopGroup} shared by each {@link ProbingSequence}.
|
||||||
|
*/
|
||||||
|
@Provides
|
||||||
|
@Singleton
|
||||||
|
EventLoopGroup provideEventLoopGroup() {
|
||||||
|
return new NioEventLoopGroup();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@link Provides} one global {@link Channel} class that is used to construct a {@link
|
||||||
|
* io.netty.bootstrap.Bootstrap}.
|
||||||
|
*/
|
||||||
|
@Provides
|
||||||
|
@Singleton
|
||||||
|
Class<? extends Channel> provideChannelClazz() {
|
||||||
|
return NioSocketChannel.class;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@link Provides} above {@code DEFAULT_DURATION} for all provided {@link ProbingStep}s to use.
|
||||||
|
*/
|
||||||
|
@Provides
|
||||||
|
@Singleton
|
||||||
|
Duration provideDuration() {
|
||||||
|
return DEFAULT_DURATION;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Root level {@link Component} that provides each {@link ProbingSequence}.
|
||||||
|
*/
|
||||||
|
@Singleton
|
||||||
|
@Component(
|
||||||
|
modules = {
|
||||||
|
ProberModule.class,
|
||||||
|
WebWhoisModule.class,
|
||||||
|
})
|
||||||
|
public interface ProberComponent {
|
||||||
|
|
||||||
|
//Standard WebWhois sequence
|
||||||
|
Set<ProbingSequence> sequences();
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,316 @@
|
||||||
|
// Copyright 2019 The Nomulus Authors. All Rights Reserved.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package google.registry.monitoring.blackbox;
|
||||||
|
|
||||||
|
import static com.google.common.base.Preconditions.checkArgument;
|
||||||
|
import static com.google.common.base.Preconditions.checkNotNull;
|
||||||
|
import static com.google.common.flogger.StackSize.SMALL;
|
||||||
|
import static google.registry.monitoring.blackbox.Protocol.PROTOCOL_KEY;
|
||||||
|
|
||||||
|
import com.google.auto.value.AutoValue;
|
||||||
|
import com.google.common.collect.ImmutableList;
|
||||||
|
import com.google.common.flogger.FluentLogger;
|
||||||
|
import google.registry.monitoring.blackbox.exceptions.UndeterminedStateException;
|
||||||
|
import google.registry.monitoring.blackbox.handlers.ActionHandler;
|
||||||
|
import google.registry.monitoring.blackbox.messages.OutboundMessageType;
|
||||||
|
import io.netty.bootstrap.Bootstrap;
|
||||||
|
import io.netty.channel.Channel;
|
||||||
|
import io.netty.channel.ChannelFuture;
|
||||||
|
import io.netty.channel.ChannelHandler;
|
||||||
|
import io.netty.channel.ChannelInitializer;
|
||||||
|
import io.netty.channel.ChannelPipeline;
|
||||||
|
import io.netty.channel.ChannelPromise;
|
||||||
|
import io.netty.channel.local.LocalAddress;
|
||||||
|
import io.netty.util.AttributeKey;
|
||||||
|
import io.netty.util.HashedWheelTimer;
|
||||||
|
import io.netty.util.Timer;
|
||||||
|
import java.net.InetAddress;
|
||||||
|
import java.net.InetSocketAddress;
|
||||||
|
import java.net.SocketAddress;
|
||||||
|
import java.net.UnknownHostException;
|
||||||
|
import java.util.concurrent.Callable;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import javax.inject.Provider;
|
||||||
|
import org.joda.time.Duration;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AutoValue class that represents action generated by {@link ProbingStep}
|
||||||
|
*
|
||||||
|
* <p> Inherits from {@link Callable<ChannelFuture>}, as it has can be called
|
||||||
|
* to perform its specified task, and return the {@link ChannelFuture} that will be informed when
|
||||||
|
* the task has been completed </p>
|
||||||
|
*
|
||||||
|
* <p> Is an immutable class, as it is comprised of the tools necessary for making a specific type
|
||||||
|
* of connection. It goes hand in hand with {@link Protocol}, which specifies the kind of overall
|
||||||
|
* connection to be made. {@link Protocol} gives the outline and {@link ProbingAction} gives the
|
||||||
|
* details of that connection.</p>
|
||||||
|
*
|
||||||
|
* <p> In its build, if there is no channel supplied, it will create a channel from the attributes
|
||||||
|
* already supplied. Then, it only sends the {@link OutboundMessageType} down the pipeline when
|
||||||
|
* informed that the connection is successful. If the channel is supplied, the connection future is
|
||||||
|
* automatically set to successful.</p>
|
||||||
|
*/
|
||||||
|
|
||||||
|
@AutoValue
|
||||||
|
public abstract class ProbingAction implements Callable<ChannelFuture> {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@link AttributeKey} in channel that gives {@link ChannelFuture} that is set to success when
|
||||||
|
* channel is active.
|
||||||
|
*/
|
||||||
|
public static final AttributeKey<ChannelFuture> CONNECTION_FUTURE_KEY = AttributeKey
|
||||||
|
.valueOf("CONNECTION_FUTURE_KEY");
|
||||||
|
/**
|
||||||
|
* {@link AttributeKey} in channel that gives the information of the channel's host.
|
||||||
|
*/
|
||||||
|
public static final AttributeKey<String> REMOTE_ADDRESS_KEY = AttributeKey
|
||||||
|
.valueOf("REMOTE_ADDRESS_KEY");
|
||||||
|
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
|
||||||
|
/**
|
||||||
|
* {@link Timer} that rate limits probing
|
||||||
|
*/
|
||||||
|
private static final Timer timer = new HashedWheelTimer();
|
||||||
|
|
||||||
|
public static Builder builder() {
|
||||||
|
return new AutoValue_ProbingAction.Builder();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds provided {@link ChannelHandler}s to the {@link ChannelPipeline} specified
|
||||||
|
*
|
||||||
|
* @param channelPipeline is pipeline associated with channel that we want to add handlers to
|
||||||
|
* @param handlerProviders are a list of provider objects that give us the requisite handlers Adds
|
||||||
|
* to the pipeline, the list of handlers in the order specified
|
||||||
|
*/
|
||||||
|
private static void addHandlers(
|
||||||
|
ChannelPipeline channelPipeline,
|
||||||
|
ImmutableList<Provider<? extends ChannelHandler>> handlerProviders) {
|
||||||
|
for (Provider<? extends ChannelHandler> handlerProvider : handlerProviders) {
|
||||||
|
channelPipeline.addLast(handlerProvider.get());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Actual {@link Duration} of this delay
|
||||||
|
*/
|
||||||
|
public abstract Duration delay();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@link OutboundMessageType} instance that we write and flush down pipeline to server
|
||||||
|
*/
|
||||||
|
public abstract OutboundMessageType outboundMessage();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@link Channel} object that either created by or passed into this {@link ProbingAction}
|
||||||
|
* instance
|
||||||
|
*/
|
||||||
|
public abstract Channel channel();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The {@link Protocol} instance that specifies type of connection
|
||||||
|
*/
|
||||||
|
public abstract Protocol protocol();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The hostname of the remote host we have a connection or will make a connection to
|
||||||
|
*/
|
||||||
|
public abstract String host();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Performs the work of the actual action.
|
||||||
|
*
|
||||||
|
* <p>First, checks if channel is active by setting a listener to perform the bulk of the work
|
||||||
|
* when the connection future is successful.</p>
|
||||||
|
*
|
||||||
|
* <p>Once the connection is successful, we establish which of the handlers in the pipeline is
|
||||||
|
* the {@link ActionHandler}.From that, we can obtain a future that is marked as a success when
|
||||||
|
* we receive an expected response from the server.</p>
|
||||||
|
*
|
||||||
|
* <p>Next, we set a timer set to a specified delay. After the delay has passed, we send the
|
||||||
|
* {@code outboundMessage} down the channel pipeline, and when we observe a success or failure,
|
||||||
|
* we inform the {@link ProbingStep} of this.</p>
|
||||||
|
*
|
||||||
|
* @return {@link ChannelFuture} that denotes when the action has been successfully performed.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public ChannelFuture call() {
|
||||||
|
//ChannelPromise that we return
|
||||||
|
ChannelPromise finished = channel().newPromise();
|
||||||
|
|
||||||
|
//Ensures channel has been set up with connection future as an attribute
|
||||||
|
checkNotNull(channel().attr(CONNECTION_FUTURE_KEY).get());
|
||||||
|
|
||||||
|
//When connection is established call super.call and set returned listener to success
|
||||||
|
channel().attr(CONNECTION_FUTURE_KEY).get().addListener(
|
||||||
|
(ChannelFuture connectionFuture) -> {
|
||||||
|
if (connectionFuture.isSuccess()) {
|
||||||
|
logger.atInfo().log(String
|
||||||
|
.format("Successful connection to remote host: %s at port: %d", host(),
|
||||||
|
protocol().port()));
|
||||||
|
|
||||||
|
ActionHandler actionHandler;
|
||||||
|
try {
|
||||||
|
actionHandler = channel().pipeline().get(ActionHandler.class);
|
||||||
|
} catch (ClassCastException e) {
|
||||||
|
//If we don't actually have an ActionHandler instance, we have an issue, and throw
|
||||||
|
// an UndeterminedStateException
|
||||||
|
logger.atSevere().withStackTrace(SMALL).log("ActionHandler not in Channel Pipeline");
|
||||||
|
throw new UndeterminedStateException("No Action Handler found in pipeline");
|
||||||
|
}
|
||||||
|
ChannelFuture channelFuture = actionHandler.getFinishedFuture();
|
||||||
|
|
||||||
|
timer.newTimeout(timeout -> {
|
||||||
|
// Write appropriate outboundMessage to pipeline
|
||||||
|
ChannelFuture unusedFutureWriteAndFlush =
|
||||||
|
channel().writeAndFlush(outboundMessage());
|
||||||
|
channelFuture.addListeners(
|
||||||
|
future -> {
|
||||||
|
if (future.isSuccess()) {
|
||||||
|
ChannelFuture unusedFuture = finished.setSuccess();
|
||||||
|
} else {
|
||||||
|
ChannelFuture unusedFuture = finished.setFailure(future.cause());
|
||||||
|
}
|
||||||
|
},
|
||||||
|
//If we don't have a persistent connection, close the connection to this
|
||||||
|
// channel
|
||||||
|
future -> {
|
||||||
|
if (!protocol().persistentConnection()) {
|
||||||
|
|
||||||
|
ChannelFuture closedFuture = channel().close();
|
||||||
|
closedFuture.addListener(
|
||||||
|
f -> {
|
||||||
|
if (f.isSuccess()) {
|
||||||
|
logger.atInfo()
|
||||||
|
.log("Closed stale channel. Moving on to next ProbingStep");
|
||||||
|
} else {
|
||||||
|
logger.atWarning()
|
||||||
|
.log(
|
||||||
|
"Could not close channel. Stale connection still exists"
|
||||||
|
+ ".");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
},
|
||||||
|
delay().getStandardSeconds(),
|
||||||
|
TimeUnit.SECONDS);
|
||||||
|
} else {
|
||||||
|
//if we receive a failure, log the failure, and close the channel
|
||||||
|
logger.atSevere().withCause(connectionFuture.cause()).log(
|
||||||
|
"Cannot connect to relay channel for %s channel: %s.",
|
||||||
|
protocol().name(), this.channel());
|
||||||
|
ChannelFuture unusedFuture = channel().close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
return finished;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public final String toString() {
|
||||||
|
return String.format(
|
||||||
|
"ProbingAction with delay: %d\n"
|
||||||
|
+ "outboundMessage: %s\n"
|
||||||
|
+ "protocol: %s\n"
|
||||||
|
+ "host: %s\n",
|
||||||
|
delay().getStandardSeconds(),
|
||||||
|
outboundMessage(),
|
||||||
|
protocol(),
|
||||||
|
host()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@link AutoValue.Builder} that does work of creating connection when not already present.
|
||||||
|
*/
|
||||||
|
@AutoValue.Builder
|
||||||
|
public abstract static class Builder {
|
||||||
|
|
||||||
|
private Bootstrap bootstrap;
|
||||||
|
|
||||||
|
public Builder setBootstrap(Bootstrap bootstrap) {
|
||||||
|
this.bootstrap = bootstrap;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public abstract Builder setDelay(Duration value);
|
||||||
|
|
||||||
|
public abstract Builder setOutboundMessage(OutboundMessageType value);
|
||||||
|
|
||||||
|
public abstract Builder setProtocol(Protocol value);
|
||||||
|
|
||||||
|
public abstract Builder setHost(String value);
|
||||||
|
|
||||||
|
public abstract Builder setChannel(Channel channel);
|
||||||
|
|
||||||
|
abstract Protocol protocol();
|
||||||
|
|
||||||
|
abstract Channel channel();
|
||||||
|
|
||||||
|
abstract String host();
|
||||||
|
|
||||||
|
abstract ProbingAction autoBuild();
|
||||||
|
|
||||||
|
public ProbingAction build() {
|
||||||
|
// Sets SocketAddress to bind to.
|
||||||
|
SocketAddress address;
|
||||||
|
try {
|
||||||
|
InetAddress hostAddress = InetAddress.getByName(host());
|
||||||
|
address = new InetSocketAddress(hostAddress, protocol().port());
|
||||||
|
} catch (UnknownHostException e) {
|
||||||
|
address = new LocalAddress(host());
|
||||||
|
}
|
||||||
|
|
||||||
|
//Sets channel supplied or to be created.
|
||||||
|
Channel channel;
|
||||||
|
try {
|
||||||
|
channel = channel();
|
||||||
|
} catch (IllegalStateException e) {
|
||||||
|
channel = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
checkArgument(channel == null ^ bootstrap == null,
|
||||||
|
"One and only one of bootstrap and channel must be supplied.");
|
||||||
|
//If a channel is supplied, nothing is needed to be done
|
||||||
|
|
||||||
|
//Otherwise, a Bootstrap must be supplied and be used for creating the channel
|
||||||
|
if (channel == null) {
|
||||||
|
bootstrap.handler(
|
||||||
|
new ChannelInitializer<Channel>() {
|
||||||
|
@Override
|
||||||
|
protected void initChannel(Channel outboundChannel)
|
||||||
|
throws Exception {
|
||||||
|
//Uses Handlers from Protocol to fill pipeline
|
||||||
|
addHandlers(outboundChannel.pipeline(), protocol().handlerProviders());
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.attr(PROTOCOL_KEY, protocol())
|
||||||
|
.attr(REMOTE_ADDRESS_KEY, host());
|
||||||
|
|
||||||
|
logger.atInfo().log("Initialized bootstrap with channel Handlers");
|
||||||
|
//ChannelFuture that performs action when connection is established
|
||||||
|
ChannelFuture connectionFuture = bootstrap.connect(address);
|
||||||
|
|
||||||
|
setChannel(connectionFuture.channel());
|
||||||
|
connectionFuture.channel().attr(CONNECTION_FUTURE_KEY).set(connectionFuture);
|
||||||
|
}
|
||||||
|
|
||||||
|
//now we can actually build the ProbingAction
|
||||||
|
return autoBuild();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,108 @@
|
||||||
|
// Copyright 2019 The Nomulus Authors. All Rights Reserved.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package google.registry.monitoring.blackbox;
|
||||||
|
|
||||||
|
import google.registry.monitoring.blackbox.tokens.Token;
|
||||||
|
import io.netty.bootstrap.Bootstrap;
|
||||||
|
import io.netty.channel.AbstractChannel;
|
||||||
|
import io.netty.channel.EventLoopGroup;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents Sequence of {@link ProbingStep}s that the Prober performs in order.
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* <p>Created with {@link Builder} where we specify {@link EventLoopGroup}, {@link AbstractChannel}
|
||||||
|
* class type, then sequentially add in the {@link ProbingStep.Builder}s in order and mark which one
|
||||||
|
* is the first repeated step.</p>
|
||||||
|
*
|
||||||
|
* <p>{@link ProbingSequence} implicitly points each {@link ProbingStep} to the next one, so once
|
||||||
|
* the first one is activated with the requisite {@link Token}, the {@link ProbingStep}s do the rest
|
||||||
|
* of the work.</p>
|
||||||
|
*/
|
||||||
|
public class ProbingSequence {
|
||||||
|
|
||||||
|
private ProbingStep firstStep;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Each {@link ProbingSequence} requires a start token to begin running.
|
||||||
|
*/
|
||||||
|
private Token startToken;
|
||||||
|
|
||||||
|
private ProbingSequence(ProbingStep firstStep, Token startToken) {
|
||||||
|
this.firstStep = firstStep;
|
||||||
|
this.startToken = startToken;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void start() {
|
||||||
|
// calls the first step with startToken;
|
||||||
|
firstStep.accept(startToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Turns {@link ProbingStep.Builder}s into fully self-dependent sequence with supplied {@link
|
||||||
|
* Bootstrap}.
|
||||||
|
*/
|
||||||
|
public static class Builder {
|
||||||
|
|
||||||
|
private ProbingStep currentStep;
|
||||||
|
private ProbingStep firstStep;
|
||||||
|
private ProbingStep firstRepeatedStep;
|
||||||
|
|
||||||
|
private Token startToken;
|
||||||
|
|
||||||
|
public Builder(Token startToken) {
|
||||||
|
this.startToken = startToken;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds {@link ProbingStep}, which is supplied with {@link Bootstrap}, built, and pointed to by
|
||||||
|
* the previous {@link ProbingStep} added.
|
||||||
|
*/
|
||||||
|
public Builder addStep(ProbingStep step) {
|
||||||
|
|
||||||
|
if (currentStep == null) {
|
||||||
|
firstStep = step;
|
||||||
|
} else {
|
||||||
|
currentStep.nextStep(step);
|
||||||
|
}
|
||||||
|
|
||||||
|
currentStep = step;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* We take special note of the first repeated step.
|
||||||
|
*/
|
||||||
|
public Builder markFirstRepeated() {
|
||||||
|
firstRepeatedStep = currentStep;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Points last {@link ProbingStep} to the {@code firstRepeatedStep} and calls private
|
||||||
|
* constructor to create {@link ProbingSequence}.
|
||||||
|
*/
|
||||||
|
public ProbingSequence build() {
|
||||||
|
if (firstRepeatedStep == null) {
|
||||||
|
firstRepeatedStep = firstStep;
|
||||||
|
}
|
||||||
|
|
||||||
|
currentStep.nextStep(firstRepeatedStep);
|
||||||
|
currentStep.lastStep();
|
||||||
|
return new ProbingSequence(this.firstStep, this.startToken);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,196 @@
|
||||||
|
// Copyright 2019 The Nomulus Authors. All Rights Reserved.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package google.registry.monitoring.blackbox;
|
||||||
|
|
||||||
|
import com.google.auto.value.AutoValue;
|
||||||
|
import com.google.common.flogger.FluentLogger;
|
||||||
|
import google.registry.monitoring.blackbox.exceptions.UndeterminedStateException;
|
||||||
|
import google.registry.monitoring.blackbox.messages.OutboundMessageType;
|
||||||
|
import google.registry.monitoring.blackbox.tokens.Token;
|
||||||
|
import io.netty.bootstrap.Bootstrap;
|
||||||
|
import io.netty.channel.ChannelFuture;
|
||||||
|
import java.util.function.Consumer;
|
||||||
|
import org.joda.time.Duration;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@link AutoValue} class that represents generator of actions performed at each step in {@link
|
||||||
|
* ProbingSequence}.
|
||||||
|
*
|
||||||
|
* <p>Holds the unchanged components in a given step of the {@link ProbingSequence}, which are
|
||||||
|
* the {@link OutboundMessageType}, {@link Protocol}, {@link Duration}, and {@link Bootstrap}
|
||||||
|
* instances. It then modifies these components on each loop iteration with the consumed {@link
|
||||||
|
* Token} and from that, generates a new {@link ProbingAction} to call.</p>
|
||||||
|
*/
|
||||||
|
@AutoValue
|
||||||
|
public abstract class ProbingStep implements Consumer<Token> {
|
||||||
|
|
||||||
|
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Necessary boolean to inform when to obtain next {@link Token}
|
||||||
|
*/
|
||||||
|
protected boolean isLastStep = false;
|
||||||
|
private ProbingStep nextStep;
|
||||||
|
|
||||||
|
public static Builder builder() {
|
||||||
|
return new AutoValue_ProbingStep.Builder();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Time delay duration between actions.
|
||||||
|
*/
|
||||||
|
abstract Duration duration();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@link Protocol} type for this step.
|
||||||
|
*/
|
||||||
|
abstract Protocol protocol();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@link OutboundMessageType} instance that serves as template to be modified by {@link Token}.
|
||||||
|
*/
|
||||||
|
abstract OutboundMessageType messageTemplate();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@link Bootstrap} instance provided by parent {@link ProbingSequence} that allows for creation
|
||||||
|
* of new channels.
|
||||||
|
*/
|
||||||
|
abstract Bootstrap bootstrap();
|
||||||
|
|
||||||
|
void lastStep() {
|
||||||
|
isLastStep = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void nextStep(ProbingStep step) {
|
||||||
|
this.nextStep = step;
|
||||||
|
}
|
||||||
|
|
||||||
|
ProbingStep nextStep() {
|
||||||
|
return this.nextStep;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates a new {@link ProbingAction} from {@code token} modified {@link OutboundMessageType}
|
||||||
|
*/
|
||||||
|
private ProbingAction generateAction(Token token) throws UndeterminedStateException {
|
||||||
|
OutboundMessageType message = token.modifyMessage(messageTemplate());
|
||||||
|
ProbingAction.Builder probingActionBuilder = ProbingAction.builder()
|
||||||
|
.setDelay(duration())
|
||||||
|
.setProtocol(protocol())
|
||||||
|
.setOutboundMessage(message)
|
||||||
|
.setHost(token.host());
|
||||||
|
|
||||||
|
if (token.channel() != null) {
|
||||||
|
probingActionBuilder.setChannel(token.channel());
|
||||||
|
} else {
|
||||||
|
probingActionBuilder.setBootstrap(bootstrap());
|
||||||
|
}
|
||||||
|
|
||||||
|
return probingActionBuilder.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* On the last step, gets the next {@link Token}. Otherwise, uses the same one.
|
||||||
|
*/
|
||||||
|
private Token generateNextToken(Token token) {
|
||||||
|
return isLastStep ? token.next() : token;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates new {@link ProbingAction}, calls the action, then retrieves the result of the
|
||||||
|
* action.
|
||||||
|
*
|
||||||
|
* @param token - used to generate the {@link ProbingAction} by calling {@code generateAction}.
|
||||||
|
*
|
||||||
|
* <p>If unable to generate the action, or the calling the action results in an immediate error,
|
||||||
|
* we note an error. Otherwise, if the future marked as finished when the action is completed is
|
||||||
|
* marked as a success, we note a success. Otherwise, if the cause of failure will either be a
|
||||||
|
* failure or error. </p>
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void accept(Token token) {
|
||||||
|
ProbingAction currentAction;
|
||||||
|
//attempt to generate new action. On error, move on to next step
|
||||||
|
try {
|
||||||
|
currentAction = generateAction(token);
|
||||||
|
} catch (UndeterminedStateException e) {
|
||||||
|
logger.atWarning().withCause(e).log("Error in Action Generation");
|
||||||
|
nextStep.accept(generateNextToken(token));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ChannelFuture future;
|
||||||
|
try {
|
||||||
|
//call the generated action
|
||||||
|
future = currentAction.call();
|
||||||
|
} catch (Exception e) {
|
||||||
|
//On error in calling action, log error and note an error
|
||||||
|
logger.atWarning().withCause(e).log("Error in Action Performed");
|
||||||
|
|
||||||
|
//Move on to next step in ProbingSequence
|
||||||
|
nextStep.accept(generateNextToken(token));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
future.addListener(f -> {
|
||||||
|
if (f.isSuccess()) {
|
||||||
|
//On a successful result, we log as a successful step, and not a success
|
||||||
|
logger.atInfo().log(String.format("Successfully completed Probing Step: %s", this));
|
||||||
|
|
||||||
|
} else {
|
||||||
|
//On a failed result, we log the failure and note either a failure or error
|
||||||
|
logger.atSevere().withCause(f.cause()).log("Did not result in future success");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (protocol().persistentConnection()) {
|
||||||
|
//If the connection is persistent, we store the channel in the token
|
||||||
|
token.setChannel(currentAction.channel());
|
||||||
|
}
|
||||||
|
|
||||||
|
//Move on the the next step in the ProbingSequence
|
||||||
|
nextStep.accept(generateNextToken(token));
|
||||||
|
|
||||||
|
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public final String toString() {
|
||||||
|
return String.format("ProbingStep with Protocol: %s\n"
|
||||||
|
+ "OutboundMessage: %s\n",
|
||||||
|
protocol(),
|
||||||
|
messageTemplate().getClass().getName());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Default {@link AutoValue.Builder} for {@link ProbingStep}.
|
||||||
|
*/
|
||||||
|
@AutoValue.Builder
|
||||||
|
public abstract static class Builder {
|
||||||
|
|
||||||
|
public abstract Builder setDuration(Duration value);
|
||||||
|
|
||||||
|
public abstract Builder setProtocol(Protocol value);
|
||||||
|
|
||||||
|
public abstract Builder setMessageTemplate(OutboundMessageType value);
|
||||||
|
|
||||||
|
public abstract Builder setBootstrap(Bootstrap value);
|
||||||
|
|
||||||
|
public abstract ProbingStep build();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
|
@ -17,44 +17,64 @@ package google.registry.monitoring.blackbox;
|
||||||
import com.google.auto.value.AutoValue;
|
import com.google.auto.value.AutoValue;
|
||||||
import com.google.common.collect.ImmutableList;
|
import com.google.common.collect.ImmutableList;
|
||||||
import io.netty.channel.ChannelHandler;
|
import io.netty.channel.ChannelHandler;
|
||||||
|
import io.netty.util.AttributeKey;
|
||||||
import javax.inject.Provider;
|
import javax.inject.Provider;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* {@link AutoValue} class that stores all unchanged variables necessary for type of connection
|
* {@link AutoValue} class that stores all unchanged variables necessary for type of connection.
|
||||||
*/
|
*/
|
||||||
@AutoValue
|
@AutoValue
|
||||||
public abstract class Protocol {
|
public abstract class Protocol {
|
||||||
|
|
||||||
abstract String name();
|
/**
|
||||||
|
* {@link AttributeKey} that lets channel reference {@link Protocol} that created it.
|
||||||
public abstract int port();
|
*/
|
||||||
|
public static final AttributeKey<Protocol> PROTOCOL_KEY = AttributeKey.valueOf("PROTOCOL_KEY");
|
||||||
/** The {@link ChannelHandler} providers to use for the protocol, in order. */
|
|
||||||
abstract ImmutableList<Provider<? extends ChannelHandler>> handlerProviders();
|
|
||||||
|
|
||||||
/** Boolean that notes if connection associated with Protocol is persistent.*/
|
|
||||||
abstract boolean persistentConnection();
|
|
||||||
|
|
||||||
public abstract Builder toBuilder();
|
|
||||||
|
|
||||||
public static Builder builder() {
|
public static Builder builder() {
|
||||||
return new AutoValue_Protocol.Builder();
|
return new AutoValue_Protocol.Builder();
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Builder for {@link Protocol}. */
|
public abstract String name();
|
||||||
|
|
||||||
|
public abstract int port();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The {@link ChannelHandler} providers to use for the protocol, in order.
|
||||||
|
*/
|
||||||
|
abstract ImmutableList<Provider<? extends ChannelHandler>> handlerProviders();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Boolean that notes if connection associated with Protocol is persistent.
|
||||||
|
*/
|
||||||
|
abstract boolean persistentConnection();
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public final String toString() {
|
||||||
|
return String.format(
|
||||||
|
"Protocol with name: %s, port: %d, providers: %s, and persistent connection: %s",
|
||||||
|
name(),
|
||||||
|
port(),
|
||||||
|
handlerProviders(),
|
||||||
|
persistentConnection()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Default {@link AutoValue.Builder} for {@link Protocol}.
|
||||||
|
*/
|
||||||
@AutoValue.Builder
|
@AutoValue.Builder
|
||||||
public abstract static class Builder {
|
public abstract static class Builder {
|
||||||
|
|
||||||
public abstract Builder name(String value);
|
public abstract Builder setName(String value);
|
||||||
|
|
||||||
public abstract Builder port(int num);
|
public abstract Builder setPort(int num);
|
||||||
|
|
||||||
public abstract Builder handlerProviders(
|
public abstract Builder setHandlerProviders(
|
||||||
ImmutableList<Provider<? extends ChannelHandler>> providers);
|
ImmutableList<Provider<? extends ChannelHandler>> providers);
|
||||||
|
|
||||||
public abstract Builder persistentConnection(boolean value);
|
public abstract Builder setPersistentConnection(boolean value);
|
||||||
|
|
||||||
public abstract Protocol build();
|
public abstract Protocol build();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,248 @@
|
||||||
|
// Copyright 2019 The Nomulus Authors. All Rights Reserved.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package google.registry.monitoring.blackbox;
|
||||||
|
|
||||||
|
import com.google.common.collect.ImmutableList;
|
||||||
|
import dagger.Module;
|
||||||
|
import dagger.Provides;
|
||||||
|
import dagger.multibindings.IntoSet;
|
||||||
|
import google.registry.monitoring.blackbox.handlers.SslClientInitializer;
|
||||||
|
import google.registry.monitoring.blackbox.handlers.WebWhoisActionHandler;
|
||||||
|
import google.registry.monitoring.blackbox.handlers.WebWhoisMessageHandler;
|
||||||
|
import google.registry.monitoring.blackbox.messages.HttpRequestMessage;
|
||||||
|
import google.registry.monitoring.blackbox.tokens.WebWhoisToken;
|
||||||
|
import io.netty.bootstrap.Bootstrap;
|
||||||
|
import io.netty.channel.Channel;
|
||||||
|
import io.netty.channel.ChannelHandler;
|
||||||
|
import io.netty.channel.EventLoopGroup;
|
||||||
|
import io.netty.channel.socket.nio.NioSocketChannel;
|
||||||
|
import io.netty.handler.codec.http.HttpClientCodec;
|
||||||
|
import io.netty.handler.codec.http.HttpObjectAggregator;
|
||||||
|
import io.netty.handler.ssl.SslProvider;
|
||||||
|
import javax.inject.Provider;
|
||||||
|
import javax.inject.Qualifier;
|
||||||
|
import javax.inject.Singleton;
|
||||||
|
import org.joda.time.Duration;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A module that provides the {@link Protocol}s to send HTTP(S) web WHOIS requests.
|
||||||
|
*/
|
||||||
|
@Module
|
||||||
|
public class WebWhoisModule {
|
||||||
|
|
||||||
|
private static final String HTTP_PROTOCOL_NAME = "http";
|
||||||
|
private static final String HTTPS_PROTOCOL_NAME = "https";
|
||||||
|
/**
|
||||||
|
* Standard length of messages used by Proxy. Equates to 0.5 MB.
|
||||||
|
*/
|
||||||
|
private static final int maximumMessageLengthBytes = 512 * 1024;
|
||||||
|
private final int HTTP_WHOIS_PORT = 80;
|
||||||
|
private final int HTTPS_WHOIS_PORT = 443;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@link Provides} only step used in WebWhois sequence.
|
||||||
|
*/
|
||||||
|
@Provides
|
||||||
|
@WebWhoisProtocol
|
||||||
|
static ProbingStep provideWebWhoisStep(
|
||||||
|
@HttpWhoisProtocol Protocol httpWhoisProtocol,
|
||||||
|
@WebWhoisProtocol Bootstrap bootstrap,
|
||||||
|
HttpRequestMessage messageTemplate,
|
||||||
|
Duration duration) {
|
||||||
|
|
||||||
|
return ProbingStep.builder()
|
||||||
|
.setProtocol(httpWhoisProtocol)
|
||||||
|
.setBootstrap(bootstrap)
|
||||||
|
.setMessageTemplate(messageTemplate)
|
||||||
|
.setDuration(duration)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@link Provides} the {@link Protocol} that corresponds to http connection.
|
||||||
|
*/
|
||||||
|
@Singleton
|
||||||
|
@Provides
|
||||||
|
@HttpWhoisProtocol
|
||||||
|
static Protocol provideHttpWhoisProtocol(
|
||||||
|
@HttpWhoisProtocol int httpWhoisPort,
|
||||||
|
@HttpWhoisProtocol ImmutableList<Provider<? extends ChannelHandler>> handlerProviders) {
|
||||||
|
return Protocol.builder()
|
||||||
|
.setName(HTTP_PROTOCOL_NAME)
|
||||||
|
.setPort(httpWhoisPort)
|
||||||
|
.setHandlerProviders(handlerProviders)
|
||||||
|
.setPersistentConnection(false)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@link Provides} the {@link Protocol} that corresponds to https connection.
|
||||||
|
*/
|
||||||
|
@Singleton
|
||||||
|
@Provides
|
||||||
|
@HttpsWhoisProtocol
|
||||||
|
static Protocol provideHttpsWhoisProtocol(
|
||||||
|
@HttpsWhoisProtocol int httpsWhoisPort,
|
||||||
|
@HttpsWhoisProtocol ImmutableList<Provider<? extends ChannelHandler>> handlerProviders) {
|
||||||
|
return Protocol.builder()
|
||||||
|
.setName(HTTPS_PROTOCOL_NAME)
|
||||||
|
.setPort(httpsWhoisPort)
|
||||||
|
.setHandlerProviders(handlerProviders)
|
||||||
|
.setPersistentConnection(false)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@link Provides} the list of providers of {@link ChannelHandler}s that are used for http
|
||||||
|
* protocol.
|
||||||
|
*/
|
||||||
|
@Provides
|
||||||
|
@HttpWhoisProtocol
|
||||||
|
static ImmutableList<Provider<? extends ChannelHandler>> providerHttpWhoisHandlerProviders(
|
||||||
|
Provider<HttpClientCodec> httpClientCodecProvider,
|
||||||
|
Provider<HttpObjectAggregator> httpObjectAggregatorProvider,
|
||||||
|
Provider<WebWhoisMessageHandler> messageHandlerProvider,
|
||||||
|
Provider<WebWhoisActionHandler> webWhoisActionHandlerProvider) {
|
||||||
|
return ImmutableList.of(
|
||||||
|
httpClientCodecProvider,
|
||||||
|
httpObjectAggregatorProvider,
|
||||||
|
messageHandlerProvider,
|
||||||
|
webWhoisActionHandlerProvider);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@link Provides} the list of providers of {@link ChannelHandler}s that are used for https
|
||||||
|
* protocol.
|
||||||
|
*/
|
||||||
|
@Provides
|
||||||
|
@HttpsWhoisProtocol
|
||||||
|
static ImmutableList<Provider<? extends ChannelHandler>> providerHttpsWhoisHandlerProviders(
|
||||||
|
@HttpsWhoisProtocol
|
||||||
|
Provider<SslClientInitializer<NioSocketChannel>> sslClientInitializerProvider,
|
||||||
|
Provider<HttpClientCodec> httpClientCodecProvider,
|
||||||
|
Provider<HttpObjectAggregator> httpObjectAggregatorProvider,
|
||||||
|
Provider<WebWhoisMessageHandler> messageHandlerProvider,
|
||||||
|
Provider<WebWhoisActionHandler> webWhoisActionHandlerProvider) {
|
||||||
|
return ImmutableList.of(
|
||||||
|
sslClientInitializerProvider,
|
||||||
|
httpClientCodecProvider,
|
||||||
|
httpObjectAggregatorProvider,
|
||||||
|
messageHandlerProvider,
|
||||||
|
webWhoisActionHandlerProvider);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
static HttpClientCodec provideHttpClientCodec() {
|
||||||
|
return new HttpClientCodec();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
static HttpObjectAggregator provideHttpObjectAggregator(@WebWhoisProtocol int maxContentLength) {
|
||||||
|
return new HttpObjectAggregator(maxContentLength);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@link Provides} the {@link SslClientInitializer} used for the {@link HttpsWhoisProtocol}.
|
||||||
|
*/
|
||||||
|
@Provides
|
||||||
|
@HttpsWhoisProtocol
|
||||||
|
static SslClientInitializer<NioSocketChannel> provideSslClientInitializer(
|
||||||
|
SslProvider sslProvider) {
|
||||||
|
return new SslClientInitializer<>(sslProvider);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@link Provides} the {@link Bootstrap} used by the WebWhois sequence.
|
||||||
|
*/
|
||||||
|
@Singleton
|
||||||
|
@Provides
|
||||||
|
@WebWhoisProtocol
|
||||||
|
static Bootstrap provideBootstrap(
|
||||||
|
EventLoopGroup eventLoopGroup,
|
||||||
|
Class<? extends Channel> channelClazz) {
|
||||||
|
return new Bootstrap()
|
||||||
|
.group(eventLoopGroup)
|
||||||
|
.channel(channelClazz);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@link Provides} standard WebWhois sequence.
|
||||||
|
*/
|
||||||
|
@Provides
|
||||||
|
@Singleton
|
||||||
|
@IntoSet
|
||||||
|
ProbingSequence provideWebWhoisSequence(
|
||||||
|
@WebWhoisProtocol ProbingStep probingStep,
|
||||||
|
WebWhoisToken webWhoisToken) {
|
||||||
|
|
||||||
|
return new ProbingSequence.Builder(webWhoisToken)
|
||||||
|
.addStep(probingStep)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
@WebWhoisProtocol
|
||||||
|
int provideMaximumMessageLengthBytes() {
|
||||||
|
return maximumMessageLengthBytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@link Provides} the list of top level domains to be probed
|
||||||
|
*/
|
||||||
|
@Singleton
|
||||||
|
@Provides
|
||||||
|
@WebWhoisProtocol
|
||||||
|
ImmutableList<String> provideTopLevelDomains() {
|
||||||
|
return ImmutableList.of("how", "soy", "xn--q9jyb4c");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
@HttpWhoisProtocol
|
||||||
|
int provideHttpWhoisPort() {
|
||||||
|
return HTTP_WHOIS_PORT;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
@HttpsWhoisProtocol
|
||||||
|
int provideHttpsWhoisPort() {
|
||||||
|
return HTTPS_WHOIS_PORT;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dagger qualifier to provide HTTP whois protocol related handlers and other bindings.
|
||||||
|
*/
|
||||||
|
@Qualifier
|
||||||
|
public @interface HttpWhoisProtocol {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dagger qualifier to provide HTTPS whois protocol related handlers and other bindings.
|
||||||
|
*/
|
||||||
|
@Qualifier
|
||||||
|
public @interface HttpsWhoisProtocol {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dagger qualifier to provide any WebWhois related bindings.
|
||||||
|
*/
|
||||||
|
@Qualifier
|
||||||
|
public @interface WebWhoisProtocol {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -15,67 +15,99 @@
|
||||||
package google.registry.monitoring.blackbox.handlers;
|
package google.registry.monitoring.blackbox.handlers;
|
||||||
|
|
||||||
import com.google.common.flogger.FluentLogger;
|
import com.google.common.flogger.FluentLogger;
|
||||||
|
import google.registry.monitoring.blackbox.ProbingAction;
|
||||||
|
import google.registry.monitoring.blackbox.exceptions.FailureException;
|
||||||
|
import google.registry.monitoring.blackbox.exceptions.UndeterminedStateException;
|
||||||
import google.registry.monitoring.blackbox.messages.InboundMessageType;
|
import google.registry.monitoring.blackbox.messages.InboundMessageType;
|
||||||
import google.registry.monitoring.blackbox.messages.OutboundMessageType;
|
|
||||||
import io.netty.channel.ChannelFuture;
|
import io.netty.channel.ChannelFuture;
|
||||||
import io.netty.channel.ChannelHandlerContext;
|
import io.netty.channel.ChannelHandlerContext;
|
||||||
import io.netty.channel.ChannelPromise;
|
import io.netty.channel.ChannelPromise;
|
||||||
import io.netty.channel.SimpleChannelInboundHandler;
|
import io.netty.channel.SimpleChannelInboundHandler;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Superclass of all {@link ChannelHandler}s placed at end of channel pipeline
|
* Superclass of all {@link io.netty.channel.ChannelHandler}s placed at end of channel pipeline
|
||||||
*
|
*
|
||||||
* <p>{@code ActionHandler} inherits from {@link SimpleChannelInboundHandler< InboundMessageType >},
|
* <p> {@link ActionHandler} inherits from {@link SimpleChannelInboundHandler<InboundMessageType>},
|
||||||
* as it should only be passed in messages that implement the {@link InboundMessageType} interface.
|
* as it should only be passed in messages that implement the {@link InboundMessageType}
|
||||||
|
* interface.</p>
|
||||||
*
|
*
|
||||||
* <p>The {@code ActionHandler} skeleton exists for a few main purposes. First, it returns a {@link
|
* <p> The {@link ActionHandler} skeleton exists for a few main purposes. First, it returns a
|
||||||
* ChannelPromise}, which informs the {@link ProbingAction} in charge that a response has been read.
|
* {@link ChannelPromise}, which informs the {@link ProbingAction} in charge that a response has
|
||||||
* Second, it stores the {@link OutboundMessageType} passed down the pipeline, so that subclasses
|
* been read. Second, with any exception thrown, the connection is closed, and the ProbingAction
|
||||||
* can use that information for their own processes. Lastly, with any exception thrown, the
|
* governing this channel is informed of the error. If the error is an instance of a {@link
|
||||||
* connection is closed, and the ProbingAction governing this channel is informed of the error.
|
* FailureException} {@code finished} is marked as a failure with cause {@link FailureException}. If
|
||||||
* Subclasses specify further work to be done for specific kinds of channel pipelines.
|
* it is any other type of error, it is treated as an {@link UndeterminedStateException} and {@code
|
||||||
|
* finished} set as a failure with the same cause as what caused the exception. Lastly, if no error
|
||||||
|
* is thrown, we know the action completed as a success, and, as such, we mark {@code finished} as a
|
||||||
|
* success.</p>
|
||||||
|
*
|
||||||
|
* <p>Subclasses specify further work to be done for specific kinds of channel pipelines. </p>
|
||||||
*/
|
*/
|
||||||
public abstract class ActionHandler extends SimpleChannelInboundHandler<InboundMessageType> {
|
public abstract class ActionHandler extends SimpleChannelInboundHandler<InboundMessageType> {
|
||||||
|
|
||||||
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
|
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
|
||||||
|
|
||||||
protected ChannelPromise finished;
|
/**
|
||||||
|
* {@link ChannelPromise} that informs {@link ProbingAction} if response has been received.
|
||||||
|
*/
|
||||||
|
private ChannelPromise finished;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Takes in {@link OutboundMessageType} type and saves for subclasses. Then returns initialized
|
* Returns initialized {@link ChannelPromise} to {@link ProbingAction}.
|
||||||
* {@link ChannelPromise}
|
|
||||||
*/
|
*/
|
||||||
public ChannelFuture getFuture() {
|
public ChannelFuture getFinishedFuture() {
|
||||||
return finished;
|
return finished;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Initializes new {@link ChannelPromise} */
|
/**
|
||||||
|
* Initializes {@link ChannelPromise}
|
||||||
|
*/
|
||||||
@Override
|
@Override
|
||||||
public void handlerAdded(ChannelHandlerContext ctx) {
|
public void handlerAdded(ChannelHandlerContext ctx) {
|
||||||
//Once handler is added to channel pipeline, initialize channel and future for this handler
|
//Once handler is added to channel pipeline, initialize channel and future for this handler
|
||||||
finished = ctx.newPromise();
|
finished = ctx.newPromise();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Marks {@link ChannelPromise} as success
|
||||||
|
*/
|
||||||
@Override
|
@Override
|
||||||
public void channelRead0(ChannelHandlerContext ctx, InboundMessageType inboundMessage)
|
public void channelRead0(ChannelHandlerContext ctx, InboundMessageType inboundMessage)
|
||||||
throws Exception {
|
throws FailureException, UndeterminedStateException {
|
||||||
// simply marks finished as success
|
|
||||||
finished = finished.setSuccess();
|
ChannelFuture unusedFuture = finished.setSuccess();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Logs the channel and pipeline that caused error, closes channel, then informs {@link
|
* Logs the channel and pipeline that caused error, closes channel, then informs {@link
|
||||||
* ProbingAction} listeners of error
|
* ProbingAction} listeners of error.
|
||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
|
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
|
||||||
logger.atSevere().withCause(cause).log(
|
logger.atWarning().withCause(cause).log(String.format(
|
||||||
String.format(
|
|
||||||
"Attempted Action was unsuccessful with channel: %s, having pipeline: %s",
|
"Attempted Action was unsuccessful with channel: %s, having pipeline: %s",
|
||||||
ctx.channel().toString(), ctx.channel().pipeline().toString()));
|
ctx.channel().toString(),
|
||||||
|
ctx.channel().pipeline().toString()));
|
||||||
|
|
||||||
finished = finished.setFailure(cause);
|
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();
|
ChannelFuture closedFuture = ctx.channel().close();
|
||||||
closedFuture.addListener(f -> logger.atInfo().log("Unsuccessful channel connection closed"));
|
closedFuture.addListener(f -> logger.atInfo().log("Unsuccessful channel connection closed"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,119 @@
|
||||||
|
// Copyright 2019 The Nomulus Authors. All Rights Reserved.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package google.registry.monitoring.blackbox.handlers;
|
||||||
|
|
||||||
|
import static com.google.common.base.Preconditions.checkNotNull;
|
||||||
|
import static google.registry.monitoring.blackbox.ProbingAction.REMOTE_ADDRESS_KEY;
|
||||||
|
import static google.registry.monitoring.blackbox.Protocol.PROTOCOL_KEY;
|
||||||
|
|
||||||
|
import com.google.common.annotations.VisibleForTesting;
|
||||||
|
import com.google.common.flogger.FluentLogger;
|
||||||
|
import google.registry.monitoring.blackbox.Protocol;
|
||||||
|
import io.netty.channel.Channel;
|
||||||
|
import io.netty.channel.ChannelHandler.Sharable;
|
||||||
|
import io.netty.channel.ChannelInitializer;
|
||||||
|
import io.netty.channel.embedded.EmbeddedChannel;
|
||||||
|
import io.netty.handler.ssl.SslContextBuilder;
|
||||||
|
import io.netty.handler.ssl.SslHandler;
|
||||||
|
import io.netty.handler.ssl.SslProvider;
|
||||||
|
import java.security.PrivateKey;
|
||||||
|
import java.security.cert.X509Certificate;
|
||||||
|
import java.util.function.Supplier;
|
||||||
|
import javax.inject.Singleton;
|
||||||
|
import javax.net.ssl.SSLEngine;
|
||||||
|
import javax.net.ssl.SSLParameters;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds a client side SSL handler to the channel pipeline.
|
||||||
|
*
|
||||||
|
* <p> Code is close to unchanged from {@link SslClientInitializer}</p> in proxy, but is modified
|
||||||
|
* for revised overall structure of connections, and to accomdate EPP connections </p>
|
||||||
|
*
|
||||||
|
* <p>This <b>must</b> be the first handler provided for any handler provider list, if it is
|
||||||
|
* provided. The type parameter {@code C} is needed so that unit tests can construct this handler
|
||||||
|
* that works with {@link EmbeddedChannel};
|
||||||
|
*/
|
||||||
|
@Singleton
|
||||||
|
@Sharable
|
||||||
|
public class SslClientInitializer<C extends Channel> extends ChannelInitializer<C> {
|
||||||
|
|
||||||
|
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
|
||||||
|
|
||||||
|
private final SslProvider sslProvider;
|
||||||
|
private final X509Certificate[] trustedCertificates;
|
||||||
|
private final Supplier<PrivateKey> privateKeySupplier;
|
||||||
|
private final Supplier<X509Certificate[]> certificateSupplier;
|
||||||
|
|
||||||
|
|
||||||
|
public SslClientInitializer(SslProvider sslProvider) {
|
||||||
|
// null uses the system default trust store.
|
||||||
|
//Used for WebWhois, so we don't care about privateKey and certificates, setting them to null
|
||||||
|
this(sslProvider, null, null, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public SslClientInitializer(SslProvider sslProvider, Supplier<PrivateKey> privateKeySupplier,
|
||||||
|
Supplier<X509Certificate[]> certificateSupplier) {
|
||||||
|
//We use the default trust store here as well, setting trustCertificates to null
|
||||||
|
this(sslProvider, null, privateKeySupplier, certificateSupplier);
|
||||||
|
}
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
|
SslClientInitializer(SslProvider sslProvider, X509Certificate[] trustCertificates) {
|
||||||
|
this(sslProvider, trustCertificates, null, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private SslClientInitializer(
|
||||||
|
SslProvider sslProvider,
|
||||||
|
X509Certificate[] trustCertificates,
|
||||||
|
Supplier<PrivateKey> privateKeySupplier,
|
||||||
|
Supplier<X509Certificate[]> certificateSupplier) {
|
||||||
|
logger.atInfo().log("Client SSL Provider: %s", sslProvider);
|
||||||
|
|
||||||
|
this.sslProvider = sslProvider;
|
||||||
|
this.trustedCertificates = trustCertificates;
|
||||||
|
this.privateKeySupplier = privateKeySupplier;
|
||||||
|
this.certificateSupplier = certificateSupplier;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void initChannel(C channel) throws Exception {
|
||||||
|
Protocol protocol = channel.attr(PROTOCOL_KEY).get();
|
||||||
|
String host = channel.attr(REMOTE_ADDRESS_KEY).get();
|
||||||
|
|
||||||
|
//Builds SslHandler from Protocol, and based on if we require a privateKey and certificate
|
||||||
|
checkNotNull(protocol, "Protocol is not set for channel: %s", channel);
|
||||||
|
SslContextBuilder sslContextBuilder =
|
||||||
|
SslContextBuilder.forClient()
|
||||||
|
.sslProvider(sslProvider)
|
||||||
|
.trustManager(trustedCertificates);
|
||||||
|
if (privateKeySupplier != null && certificateSupplier != null) {
|
||||||
|
sslContextBuilder = sslContextBuilder
|
||||||
|
.keyManager(privateKeySupplier.get(), certificateSupplier.get());
|
||||||
|
}
|
||||||
|
|
||||||
|
SslHandler sslHandler = sslContextBuilder
|
||||||
|
.build()
|
||||||
|
.newHandler(channel.alloc(), host, protocol.port());
|
||||||
|
|
||||||
|
// Enable hostname verification.
|
||||||
|
SSLEngine sslEngine = sslHandler.engine();
|
||||||
|
SSLParameters sslParameters = sslEngine.getSSLParameters();
|
||||||
|
sslParameters.setEndpointIdentificationAlgorithm("HTTPS");
|
||||||
|
sslEngine.setSSLParameters(sslParameters);
|
||||||
|
|
||||||
|
channel.pipeline().addLast(sslHandler);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,189 @@
|
||||||
|
// Copyright 2019 The Nomulus Authors. All Rights Reserved.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package google.registry.monitoring.blackbox.handlers;
|
||||||
|
|
||||||
|
import com.google.common.flogger.FluentLogger;
|
||||||
|
import google.registry.monitoring.blackbox.ProbingAction;
|
||||||
|
import google.registry.monitoring.blackbox.Protocol;
|
||||||
|
import google.registry.monitoring.blackbox.WebWhoisModule.HttpWhoisProtocol;
|
||||||
|
import google.registry.monitoring.blackbox.WebWhoisModule.HttpsWhoisProtocol;
|
||||||
|
import google.registry.monitoring.blackbox.WebWhoisModule.WebWhoisProtocol;
|
||||||
|
import google.registry.monitoring.blackbox.exceptions.ConnectionException;
|
||||||
|
import google.registry.monitoring.blackbox.exceptions.FailureException;
|
||||||
|
import google.registry.monitoring.blackbox.exceptions.UndeterminedStateException;
|
||||||
|
import google.registry.monitoring.blackbox.messages.HttpRequestMessage;
|
||||||
|
import google.registry.monitoring.blackbox.messages.HttpResponseMessage;
|
||||||
|
import google.registry.monitoring.blackbox.messages.InboundMessageType;
|
||||||
|
import io.netty.bootstrap.Bootstrap;
|
||||||
|
import io.netty.channel.ChannelFuture;
|
||||||
|
import io.netty.channel.ChannelHandlerContext;
|
||||||
|
import io.netty.handler.codec.http.HttpResponseStatus;
|
||||||
|
import java.net.MalformedURLException;
|
||||||
|
import java.net.URL;
|
||||||
|
import javax.inject.Inject;
|
||||||
|
import org.joda.time.Duration;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Subclass of {@link ActionHandler} that deals with the WebWhois Sequence
|
||||||
|
*
|
||||||
|
* <p> Main purpose is to verify {@link HttpResponseMessage} received is valid. If the response
|
||||||
|
* implies a redirection it follows the redirection until either an Error Response is received, or
|
||||||
|
* {@link HttpResponseStatus.OK} is received</p>
|
||||||
|
*/
|
||||||
|
public class WebWhoisActionHandler extends ActionHandler {
|
||||||
|
|
||||||
|
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
|
||||||
|
|
||||||
|
/** Dagger injected components necessary for redirect responses: */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@link Bootstrap} necessary for remaking connection on redirect response.
|
||||||
|
*/
|
||||||
|
private final Bootstrap bootstrap;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@link Protocol} for when redirected to http endpoint.
|
||||||
|
*/
|
||||||
|
private final Protocol httpWhoisProtocol;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@link Protocol} for when redirected to https endpoint.
|
||||||
|
*/
|
||||||
|
private final Protocol httpsWhoisProtocol;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@link HttpRequestMessage} that represents default GET message to be sent on redirect.
|
||||||
|
*/
|
||||||
|
private final HttpRequestMessage requestMessage;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
public WebWhoisActionHandler(
|
||||||
|
@WebWhoisProtocol Bootstrap bootstrap,
|
||||||
|
@HttpWhoisProtocol Protocol httpWhoisProtocol,
|
||||||
|
@HttpsWhoisProtocol Protocol httpsWhoisProtocol,
|
||||||
|
HttpRequestMessage requestMessage) {
|
||||||
|
|
||||||
|
this.bootstrap = bootstrap;
|
||||||
|
this.httpWhoisProtocol = httpWhoisProtocol;
|
||||||
|
this.httpsWhoisProtocol = httpsWhoisProtocol;
|
||||||
|
this.requestMessage = requestMessage;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* After receiving {@link HttpResponseMessage}, either notes success and marks future as finished,
|
||||||
|
* notes an error in the received {@link URL} and throws a {@link ConnectionException}, received a
|
||||||
|
* response indicating a Failure, or receives a redirection response, where it follows the
|
||||||
|
* redirects until receiving one of the previous three responses.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void channelRead0(ChannelHandlerContext ctx, InboundMessageType msg)
|
||||||
|
throws FailureException, UndeterminedStateException {
|
||||||
|
|
||||||
|
HttpResponseMessage response = (HttpResponseMessage) msg;
|
||||||
|
|
||||||
|
if (response.status().equals(HttpResponseStatus.OK)) {
|
||||||
|
logger.atInfo().log("Received Successful HttpResponseStatus");
|
||||||
|
logger.atInfo().log("Response Received: " + response);
|
||||||
|
|
||||||
|
//On success, we always pass message to ActionHandler's channelRead0 method.
|
||||||
|
super.channelRead0(ctx, msg);
|
||||||
|
|
||||||
|
} else if (response.status().equals(HttpResponseStatus.MOVED_PERMANENTLY)
|
||||||
|
|| response.status().equals(HttpResponseStatus.FOUND)) {
|
||||||
|
//TODO - Fix checker to better determine when we have encountered a redirection response.
|
||||||
|
|
||||||
|
//Obtain url to be redirected to
|
||||||
|
URL url;
|
||||||
|
try {
|
||||||
|
url = new URL(response.headers().get("Location"));
|
||||||
|
} catch (MalformedURLException e) {
|
||||||
|
//in case of error, log it, and let ActionHandler's exceptionThrown method deal with it
|
||||||
|
throw new FailureException(
|
||||||
|
"Redirected Location was invalid. Given Location was: " + response.headers()
|
||||||
|
.get("Location"));
|
||||||
|
}
|
||||||
|
//From url, extract new host, port, and path
|
||||||
|
String newHost = url.getHost();
|
||||||
|
String newPath = url.getPath();
|
||||||
|
|
||||||
|
logger.atInfo().log(String
|
||||||
|
.format("Redirected to %s with host: %s, port: %d, and path: %s", url, newHost,
|
||||||
|
url.getDefaultPort(), newPath));
|
||||||
|
|
||||||
|
//Construct new Protocol to reflect redirected host, path, and port
|
||||||
|
Protocol newProtocol;
|
||||||
|
if (url.getProtocol().equals(httpWhoisProtocol.name())) {
|
||||||
|
newProtocol = httpWhoisProtocol;
|
||||||
|
} else if (url.getProtocol().equals(httpsWhoisProtocol.name())) {
|
||||||
|
newProtocol = httpsWhoisProtocol;
|
||||||
|
} else {
|
||||||
|
throw new FailureException(
|
||||||
|
"Redirection Location port was invalid. Given protocol name was: " + url.getProtocol());
|
||||||
|
}
|
||||||
|
|
||||||
|
//Obtain HttpRequestMessage with modified headers to reflect new host and path.
|
||||||
|
HttpRequestMessage httpRequest = requestMessage.modifyMessage(newHost, newPath);
|
||||||
|
|
||||||
|
//Create new probingAction that takes in the new Protocol and HttpRequestMessage with no delay
|
||||||
|
ProbingAction redirectedAction = ProbingAction.builder()
|
||||||
|
.setBootstrap(bootstrap)
|
||||||
|
.setProtocol(newProtocol)
|
||||||
|
.setOutboundMessage(httpRequest)
|
||||||
|
.setDelay(Duration.ZERO)
|
||||||
|
.setHost(newHost)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
//close this channel as we no longer need it
|
||||||
|
ChannelFuture future = ctx.close();
|
||||||
|
future.addListener(
|
||||||
|
f -> {
|
||||||
|
if (f.isSuccess()) {
|
||||||
|
logger.atInfo().log("Successfully Closed Connection.");
|
||||||
|
} else {
|
||||||
|
logger.atWarning().log("Channel was unsuccessfully closed.");
|
||||||
|
}
|
||||||
|
|
||||||
|
//Once channel is closed, establish new connection to redirected host, and repeat
|
||||||
|
// same actions
|
||||||
|
ChannelFuture secondFuture = redirectedAction.call();
|
||||||
|
|
||||||
|
//Once we have a successful call, set original ChannelPromise as success to tell
|
||||||
|
// ProbingStep we can move on
|
||||||
|
secondFuture.addListener(f2 -> {
|
||||||
|
if (f2.isSuccess()) {
|
||||||
|
super.channelRead0(ctx, msg);
|
||||||
|
} else {
|
||||||
|
if (f2 instanceof FailureException) {
|
||||||
|
throw new FailureException(f2.cause());
|
||||||
|
} else {
|
||||||
|
throw new UndeterminedStateException(f2.cause());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
//Add in metrics Handling that informs MetricsCollector the response was a FAILURE
|
||||||
|
logger.atWarning().log(String.format("Received unexpected response: %s", response.status()));
|
||||||
|
throw new FailureException("Response received from remote site was: " + response.status());
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,90 @@
|
||||||
|
// Copyright 2019 The Nomulus Authors. All Rights Reserved.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package google.registry.monitoring.blackbox.messages;
|
||||||
|
|
||||||
|
import io.netty.buffer.ByteBuf;
|
||||||
|
import io.netty.handler.codec.http.DefaultFullHttpRequest;
|
||||||
|
import io.netty.handler.codec.http.FullHttpRequest;
|
||||||
|
import io.netty.handler.codec.http.HttpMethod;
|
||||||
|
import io.netty.handler.codec.http.HttpVersion;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import javax.inject.Inject;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@link OutboundMessageType} subtype that acts identically to {@link DefaultFullHttpRequest}.
|
||||||
|
*
|
||||||
|
* <p>As it is an {@link OutboundMessageType} subtype, there is a {@code modifyMessage} method
|
||||||
|
* that modifies the request to reflect the new host and optional path. We also implement a {@code
|
||||||
|
* name} method, which returns a standard name and the current hostname.</p>
|
||||||
|
*/
|
||||||
|
public class HttpRequestMessage extends DefaultFullHttpRequest implements OutboundMessageType {
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
public HttpRequestMessage() {
|
||||||
|
this(HttpVersion.HTTP_1_1, HttpMethod.GET, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
private HttpRequestMessage(HttpVersion httpVersion, HttpMethod method, String uri) {
|
||||||
|
super(httpVersion, method, uri);
|
||||||
|
}
|
||||||
|
|
||||||
|
private HttpRequestMessage(HttpVersion httpVersion, HttpMethod method, String uri,
|
||||||
|
ByteBuf content) {
|
||||||
|
super(httpVersion, method, uri, content);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Used for conversion from {@link FullHttpRequest} to {@link HttpRequestMessage}
|
||||||
|
*/
|
||||||
|
public HttpRequestMessage(FullHttpRequest request) {
|
||||||
|
this(request.protocolVersion(), request.method(), request.uri(), request.content());
|
||||||
|
request.headers().forEach((entry) -> headers().set(entry.getKey(), entry.getValue()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public HttpRequestMessage setUri(String path) {
|
||||||
|
super.setUri(path);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Modifies headers to reflect new host and new path if applicable.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public HttpRequestMessage modifyMessage(String... args) throws IllegalArgumentException {
|
||||||
|
if (args.length == 1 || args.length == 2) {
|
||||||
|
headers().set("host", args[0]);
|
||||||
|
if (args.length == 2) {
|
||||||
|
setUri(args[1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this;
|
||||||
|
|
||||||
|
} else {
|
||||||
|
throw new IllegalArgumentException(
|
||||||
|
String.format(
|
||||||
|
"Wrong number of arguments present for modifying HttpRequestMessage."
|
||||||
|
+ " Received %d arguments instead of 2. Received arguments: "
|
||||||
|
+ Arrays.toString(args), args.length));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return String.format("Http(s) Request on: %s", headers().get("host"));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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()));
|
||||||
|
}
|
||||||
|
}
|
|
@ -18,4 +18,6 @@ package google.registry.monitoring.blackbox.messages;
|
||||||
* Marker Interface that is implemented by all classes that serve as {@code inboundMessages} in
|
* Marker Interface that is implemented by all classes that serve as {@code inboundMessages} in
|
||||||
* channel pipeline
|
* channel pipeline
|
||||||
*/
|
*/
|
||||||
public interface InboundMessageType {}
|
public interface InboundMessageType {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
|
@ -14,8 +14,24 @@
|
||||||
|
|
||||||
package google.registry.monitoring.blackbox.messages;
|
package google.registry.monitoring.blackbox.messages;
|
||||||
|
|
||||||
|
import google.registry.monitoring.blackbox.exceptions.UndeterminedStateException;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Marker Interface that is implemented by all classes that serve as {@code outboundMessages} in
|
* Marker Interface that is implemented by all classes that serve as {@code outboundMessages} in
|
||||||
* channel pipeline
|
* channel pipeline
|
||||||
*/
|
*/
|
||||||
public interface OutboundMessageType {}
|
public interface OutboundMessageType {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* All {@link OutboundMessageType} implementing classes should be able to be modified by token
|
||||||
|
* with String arguments
|
||||||
|
*/
|
||||||
|
OutboundMessageType modifyMessage(String... args) throws UndeterminedStateException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Necessary to inform metrics collector what kind of message is sent down {@link
|
||||||
|
* io.netty.channel.ChannelPipeline}
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
String toString();
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,72 @@
|
||||||
|
// Copyright 2019 The Nomulus Authors. All Rights Reserved.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package google.registry.monitoring.blackbox.tokens;
|
||||||
|
|
||||||
|
import google.registry.monitoring.blackbox.ProbingSequence;
|
||||||
|
import google.registry.monitoring.blackbox.ProbingStep;
|
||||||
|
import google.registry.monitoring.blackbox.exceptions.UndeterminedStateException;
|
||||||
|
import google.registry.monitoring.blackbox.messages.OutboundMessageType;
|
||||||
|
import io.netty.channel.Channel;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Superclass that represents information passed to each {@link ProbingStep} in a single loop of a
|
||||||
|
* {@link ProbingSequence}.
|
||||||
|
*
|
||||||
|
* <p>Modifies the message passed in to reflect information relevant to a single loop
|
||||||
|
* in a {@link ProbingSequence}. Additionally, passes on channel that remains unchanged within a
|
||||||
|
* loop of the sequence.</p>
|
||||||
|
*
|
||||||
|
* <p>Also obtains the next {@link Token} corresponding to the next iteration of a loop
|
||||||
|
* in the sequence.</p>
|
||||||
|
*/
|
||||||
|
public abstract class Token {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@link Channel} that always starts out as null. Once a persistent connection is made (such as
|
||||||
|
* EPP), that channel is stored in the token and passed on to later steps in the sequence until a
|
||||||
|
* new loop begins.
|
||||||
|
*/
|
||||||
|
protected Channel channel;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Obtains next {@link Token} for next loop in sequence.
|
||||||
|
*/
|
||||||
|
public abstract Token next();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* String corresponding to host that is relevant for loop in sequence.
|
||||||
|
*/
|
||||||
|
public abstract String host();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Modifies the {@link OutboundMessageType} in the manner necessary for each loop
|
||||||
|
*/
|
||||||
|
public abstract OutboundMessageType modifyMessage(OutboundMessageType messageType)
|
||||||
|
throws UndeterminedStateException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set method for {@code channel}
|
||||||
|
*/
|
||||||
|
public void setChannel(Channel channel) {
|
||||||
|
this.channel = channel;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get method for {@code channel}.
|
||||||
|
*/
|
||||||
|
public Channel channel() {
|
||||||
|
return this.channel;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,82 @@
|
||||||
|
// Copyright 2019 The Nomulus Authors. All Rights Reserved.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package google.registry.monitoring.blackbox.tokens;
|
||||||
|
|
||||||
|
import com.google.common.collect.ImmutableList;
|
||||||
|
import google.registry.monitoring.blackbox.WebWhoisModule.WebWhoisProtocol;
|
||||||
|
import google.registry.monitoring.blackbox.exceptions.UndeterminedStateException;
|
||||||
|
import google.registry.monitoring.blackbox.messages.OutboundMessageType;
|
||||||
|
import java.util.Iterator;
|
||||||
|
import javax.inject.Inject;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@link Token} subtype designed for WebWhois sequence.
|
||||||
|
*
|
||||||
|
* <p>Between loops of a WebWhois sequence the only thing changing is the tld we
|
||||||
|
* are probing. As a result, we maintain the list of {@code topLevelDomains} and on each call to
|
||||||
|
* next, have our index looking at the next {@code topLevelDomain}. </p>
|
||||||
|
*/
|
||||||
|
public class WebWhoisToken extends Token {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* For each top level domain (tld), we probe "prefix.tld".
|
||||||
|
*/
|
||||||
|
private static final String PREFIX = "whois.nic.";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@link ImmutableList} of all top level domains to be probed.
|
||||||
|
*/
|
||||||
|
private final Iterator<String> topLevelDomainsIterator;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Current index of {@code topLevelDomains} that represents tld we are probing.
|
||||||
|
*/
|
||||||
|
private String currentDomain;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
public WebWhoisToken(@WebWhoisProtocol ImmutableList<String> topLevelDomains) {
|
||||||
|
|
||||||
|
topLevelDomainsIterator = topLevelDomains.iterator();
|
||||||
|
currentDomain = topLevelDomainsIterator.next();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Increments {@code domainsIndex} or resets it to reflect move to next top level domain.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public WebWhoisToken next() {
|
||||||
|
currentDomain = topLevelDomainsIterator.next();
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Modifies message to reflect the new host coming from the new top level domain.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public OutboundMessageType modifyMessage(OutboundMessageType original)
|
||||||
|
throws UndeterminedStateException {
|
||||||
|
return original.modifyMessage(host());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns host as the concatenation of fixed {@code prefix} and current value of {@code
|
||||||
|
* topLevelDomains}.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public String host() {
|
||||||
|
return PREFIX + currentDomain;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,162 @@
|
||||||
|
// Copyright 2019 The Nomulus Authors. All Rights Reserved.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package google.registry.monitoring.blackbox;
|
||||||
|
|
||||||
|
import static com.google.common.truth.Truth.assertThat;
|
||||||
|
import static google.registry.monitoring.blackbox.ProbingAction.CONNECTION_FUTURE_KEY;
|
||||||
|
import static java.nio.charset.StandardCharsets.US_ASCII;
|
||||||
|
import static java.nio.charset.StandardCharsets.UTF_8;
|
||||||
|
|
||||||
|
import com.google.common.collect.ImmutableList;
|
||||||
|
import google.registry.monitoring.blackbox.handlers.ActionHandler;
|
||||||
|
import google.registry.monitoring.blackbox.handlers.ConversionHandler;
|
||||||
|
import google.registry.monitoring.blackbox.handlers.NettyRule;
|
||||||
|
import google.registry.monitoring.blackbox.handlers.TestActionHandler;
|
||||||
|
import google.registry.monitoring.blackbox.messages.TestMessage;
|
||||||
|
import io.netty.bootstrap.Bootstrap;
|
||||||
|
import io.netty.buffer.ByteBuf;
|
||||||
|
import io.netty.buffer.Unpooled;
|
||||||
|
import io.netty.channel.ChannelFuture;
|
||||||
|
import io.netty.channel.ChannelHandler;
|
||||||
|
import io.netty.channel.EventLoopGroup;
|
||||||
|
import io.netty.channel.embedded.EmbeddedChannel;
|
||||||
|
import io.netty.channel.local.LocalAddress;
|
||||||
|
import io.netty.channel.local.LocalChannel;
|
||||||
|
import io.netty.channel.nio.NioEventLoopGroup;
|
||||||
|
import org.joda.time.Duration;
|
||||||
|
import org.junit.Ignore;
|
||||||
|
import org.junit.Rule;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.junit.runner.RunWith;
|
||||||
|
import org.junit.runners.JUnit4;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unit tests for {@link ProbingAction} subtypes
|
||||||
|
*
|
||||||
|
* <p>Attempts to test how well each {@link ProbingAction} works with an {@link ActionHandler}
|
||||||
|
* subtype when receiving to all possible types of responses</p>
|
||||||
|
*/
|
||||||
|
@RunWith(JUnit4.class)
|
||||||
|
public class ProbingActionTest {
|
||||||
|
|
||||||
|
private static final String TEST_MESSAGE = "MESSAGE_TEST";
|
||||||
|
private static final String SECONDARY_TEST_MESSAGE = "SECONDARY_MESSAGE_TEST";
|
||||||
|
private static final String PROTOCOL_NAME = "TEST_PROTOCOL";
|
||||||
|
private static final String ADDRESS_NAME = "TEST_ADDRESS";
|
||||||
|
private static final int TEST_PORT = 0;
|
||||||
|
|
||||||
|
private static final EventLoopGroup eventLoopGroup = new NioEventLoopGroup(1);
|
||||||
|
/**
|
||||||
|
* Used for testing how well probing step can create connection to blackbox server
|
||||||
|
*/
|
||||||
|
@Rule
|
||||||
|
public NettyRule nettyRule = new NettyRule(eventLoopGroup);
|
||||||
|
/**
|
||||||
|
* We use custom Test {@link ActionHandler} and {@link ConversionHandler} so test depends only on
|
||||||
|
* {@link ProbingAction}
|
||||||
|
*/
|
||||||
|
private ActionHandler testHandler = new TestActionHandler();
|
||||||
|
private ChannelHandler conversionHandler = new ConversionHandler();
|
||||||
|
|
||||||
|
//TODO - Currently, this test fails to receive outbound messages from the embedded channel, which
|
||||||
|
// we will fix in a later release.
|
||||||
|
@Ignore
|
||||||
|
@Test
|
||||||
|
public void testSuccess_existingChannel() {
|
||||||
|
//setup
|
||||||
|
EmbeddedChannel channel = new EmbeddedChannel(conversionHandler, testHandler);
|
||||||
|
channel.attr(CONNECTION_FUTURE_KEY).set(channel.newSucceededFuture());
|
||||||
|
|
||||||
|
// Sets up a Protocol corresponding to when a connection exists.
|
||||||
|
Protocol protocol = Protocol.builder()
|
||||||
|
.setHandlerProviders(ImmutableList.of(() -> conversionHandler, () -> testHandler))
|
||||||
|
.setName(PROTOCOL_NAME)
|
||||||
|
.setPort(TEST_PORT)
|
||||||
|
.setPersistentConnection(true)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
// Sets up a ProbingAction that creates a channel using test specified attributes.
|
||||||
|
ProbingAction action = ProbingAction.builder()
|
||||||
|
.setChannel(channel)
|
||||||
|
.setProtocol(protocol)
|
||||||
|
.setDelay(Duration.ZERO)
|
||||||
|
.setOutboundMessage(new TestMessage(TEST_MESSAGE))
|
||||||
|
.setHost("")
|
||||||
|
.build();
|
||||||
|
|
||||||
|
//tests main function of ProbingAction
|
||||||
|
ChannelFuture future = action.call();
|
||||||
|
|
||||||
|
//Obtains the outboundMessage passed through pipeline after delay
|
||||||
|
Object msg = null;
|
||||||
|
while (msg == null) {
|
||||||
|
msg = channel.readOutbound();
|
||||||
|
}
|
||||||
|
//tests the passed message is exactly what we expect
|
||||||
|
assertThat(msg).isInstanceOf(ByteBuf.class);
|
||||||
|
String request = ((ByteBuf) msg).toString(UTF_8);
|
||||||
|
assertThat(request).isEqualTo(TEST_MESSAGE);
|
||||||
|
|
||||||
|
// Ensures that we haven't marked future as done until response is received.
|
||||||
|
assertThat(future.isDone()).isFalse();
|
||||||
|
|
||||||
|
//after writing inbound, we should have a success
|
||||||
|
channel.writeInbound(Unpooled.wrappedBuffer(SECONDARY_TEST_MESSAGE.getBytes(US_ASCII)));
|
||||||
|
assertThat(future.isSuccess()).isTrue();
|
||||||
|
|
||||||
|
assertThat(testHandler.toString()).isEqualTo(SECONDARY_TEST_MESSAGE);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSuccess_newChannel() throws Exception {
|
||||||
|
//setup
|
||||||
|
|
||||||
|
LocalAddress address = new LocalAddress(ADDRESS_NAME);
|
||||||
|
Bootstrap bootstrap = new Bootstrap()
|
||||||
|
.group(eventLoopGroup)
|
||||||
|
.channel(LocalChannel.class);
|
||||||
|
|
||||||
|
// Sets up a Protocol corresponding to when a new connection is created.
|
||||||
|
Protocol protocol = Protocol.builder()
|
||||||
|
.setHandlerProviders(ImmutableList.of(() -> conversionHandler, () -> testHandler))
|
||||||
|
.setName(PROTOCOL_NAME)
|
||||||
|
.setPort(TEST_PORT)
|
||||||
|
.setPersistentConnection(false)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
nettyRule.setUpServer(address);
|
||||||
|
|
||||||
|
// Sets up a ProbingAction with existing channel using test specified attributes.
|
||||||
|
ProbingAction action = ProbingAction.builder()
|
||||||
|
.setBootstrap(bootstrap)
|
||||||
|
.setProtocol(protocol)
|
||||||
|
.setDelay(Duration.ZERO)
|
||||||
|
.setOutboundMessage(new TestMessage(TEST_MESSAGE))
|
||||||
|
.setHost(ADDRESS_NAME)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
//tests main function of ProbingAction
|
||||||
|
ChannelFuture future = action.call();
|
||||||
|
|
||||||
|
//Tests to see if message is properly sent to remote server
|
||||||
|
nettyRule.assertReceivedMessage(TEST_MESSAGE);
|
||||||
|
|
||||||
|
future = future.syncUninterruptibly();
|
||||||
|
//Tests to see that, since server responds, we have set future to true
|
||||||
|
assertThat(future.isSuccess()).isTrue();
|
||||||
|
assertThat(((TestActionHandler) testHandler).getResponse().toString()).isEqualTo(TEST_MESSAGE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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));
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,60 @@
|
||||||
|
// Copyright 2019 The Nomulus Authors. All Rights Reserved.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package google.registry.monitoring.blackbox.handlers;
|
||||||
|
|
||||||
|
import static java.nio.charset.StandardCharsets.US_ASCII;
|
||||||
|
import static java.nio.charset.StandardCharsets.UTF_8;
|
||||||
|
|
||||||
|
import google.registry.monitoring.blackbox.messages.InboundMessageType;
|
||||||
|
import google.registry.monitoring.blackbox.messages.OutboundMessageType;
|
||||||
|
import google.registry.monitoring.blackbox.messages.TestMessage;
|
||||||
|
import io.netty.buffer.ByteBuf;
|
||||||
|
import io.netty.buffer.Unpooled;
|
||||||
|
import io.netty.channel.ChannelDuplexHandler;
|
||||||
|
import io.netty.channel.ChannelHandler;
|
||||||
|
import io.netty.channel.ChannelHandlerContext;
|
||||||
|
import io.netty.channel.ChannelPromise;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@link ChannelHandler} used in tests to convert {@link OutboundMessageType} to to {@link
|
||||||
|
* ByteBuf}s and convert {@link ByteBuf}s to {@link InboundMessageType}
|
||||||
|
*
|
||||||
|
* <p>Specific type of {@link OutboundMessageType} and {@link InboundMessageType}
|
||||||
|
* used for conversion is the {@link TestMessage} type.</p>
|
||||||
|
*/
|
||||||
|
public class ConversionHandler extends ChannelDuplexHandler {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles inbound conversion
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
|
||||||
|
ByteBuf buf = (ByteBuf) msg;
|
||||||
|
ctx.fireChannelRead(new TestMessage(buf.toString(UTF_8)));
|
||||||
|
buf.release();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles outbound conversion
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise)
|
||||||
|
throws Exception {
|
||||||
|
String message = msg.toString();
|
||||||
|
ByteBuf buf = Unpooled.wrappedBuffer(message.getBytes(US_ASCII));
|
||||||
|
super.write(ctx, buf, promise);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,244 @@
|
||||||
|
// Copyright 2019 The Nomulus Authors. All Rights Reserved.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package google.registry.monitoring.blackbox.handlers;
|
||||||
|
|
||||||
|
import static com.google.common.base.Preconditions.checkState;
|
||||||
|
import static com.google.common.truth.Truth.assertThat;
|
||||||
|
import static google.registry.monitoring.blackbox.ProbingAction.REMOTE_ADDRESS_KEY;
|
||||||
|
import static google.registry.monitoring.blackbox.Protocol.PROTOCOL_KEY;
|
||||||
|
import static google.registry.testing.JUnitBackports.assertThrows;
|
||||||
|
import static java.nio.charset.StandardCharsets.US_ASCII;
|
||||||
|
import static java.nio.charset.StandardCharsets.UTF_8;
|
||||||
|
|
||||||
|
import com.google.common.base.Throwables;
|
||||||
|
import com.google.common.collect.ImmutableList;
|
||||||
|
import com.google.common.truth.ThrowableSubject;
|
||||||
|
import google.registry.monitoring.blackbox.ProbingActionTest;
|
||||||
|
import google.registry.monitoring.blackbox.ProbingStepTest;
|
||||||
|
import google.registry.monitoring.blackbox.Protocol;
|
||||||
|
import google.registry.monitoring.blackbox.testservers.TestServer;
|
||||||
|
import io.netty.bootstrap.Bootstrap;
|
||||||
|
import io.netty.buffer.ByteBuf;
|
||||||
|
import io.netty.buffer.Unpooled;
|
||||||
|
import io.netty.channel.Channel;
|
||||||
|
import io.netty.channel.ChannelFuture;
|
||||||
|
import io.netty.channel.ChannelHandler;
|
||||||
|
import io.netty.channel.ChannelHandlerContext;
|
||||||
|
import io.netty.channel.ChannelInboundHandlerAdapter;
|
||||||
|
import io.netty.channel.ChannelInitializer;
|
||||||
|
import io.netty.channel.EventLoopGroup;
|
||||||
|
import io.netty.channel.local.LocalAddress;
|
||||||
|
import io.netty.channel.local.LocalChannel;
|
||||||
|
import io.netty.channel.nio.NioEventLoopGroup;
|
||||||
|
import io.netty.util.ReferenceCountUtil;
|
||||||
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
import java.util.concurrent.ExecutionException;
|
||||||
|
import java.util.concurrent.Future;
|
||||||
|
import org.junit.rules.ExternalResource;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper for setting up and testing client / server connection with netty.
|
||||||
|
*
|
||||||
|
* <p>Code based on and almost identical to {@code NettyRule} in the proxy.
|
||||||
|
* Used in {@link SslClientInitializerTest}, {@link ProbingActionTest}, and {@link ProbingStepTest}
|
||||||
|
* </p>
|
||||||
|
*/
|
||||||
|
public final class NettyRule extends ExternalResource {
|
||||||
|
|
||||||
|
|
||||||
|
private final EventLoopGroup eventLoopGroup;
|
||||||
|
// Handler attached to server's channel to record the request received.
|
||||||
|
private EchoHandler echoHandler;
|
||||||
|
// Handler attached to client's channel to record the response received.
|
||||||
|
private DumpHandler dumpHandler;
|
||||||
|
private Channel channel;
|
||||||
|
|
||||||
|
// All I/O operations are done inside the single thread within this event loop group, which is
|
||||||
|
// different from the main test thread. Therefore synchronizations are required to make sure that
|
||||||
|
// certain I/O activities are finished when assertions are performed.
|
||||||
|
public NettyRule() {
|
||||||
|
eventLoopGroup = new NioEventLoopGroup(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
public NettyRule(EventLoopGroup e) {
|
||||||
|
eventLoopGroup = e;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void writeToChannelAndFlush(Channel channel, String data) {
|
||||||
|
ChannelFuture unusedFuture =
|
||||||
|
channel.writeAndFlush(Unpooled.wrappedBuffer(data.getBytes(US_ASCII)));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets up a server channel bound to the given local address.
|
||||||
|
*/
|
||||||
|
public void setUpServer(LocalAddress localAddress, ChannelHandler... handlers) {
|
||||||
|
checkState(echoHandler == null, "Can't call setUpServer twice");
|
||||||
|
echoHandler = new EchoHandler();
|
||||||
|
|
||||||
|
new TestServer(eventLoopGroup, localAddress,
|
||||||
|
ImmutableList.<ChannelHandler>builder().add(handlers).add(echoHandler).build());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets up a client channel connecting to the give local address.
|
||||||
|
*/
|
||||||
|
void setUpClient(
|
||||||
|
LocalAddress localAddress,
|
||||||
|
Protocol protocol,
|
||||||
|
String host,
|
||||||
|
ChannelHandler handler) {
|
||||||
|
checkState(echoHandler != null, "Must call setUpServer before setUpClient");
|
||||||
|
checkState(dumpHandler == null, "Can't call setUpClient twice");
|
||||||
|
dumpHandler = new DumpHandler();
|
||||||
|
ChannelInitializer<LocalChannel> clientInitializer =
|
||||||
|
new ChannelInitializer<LocalChannel>() {
|
||||||
|
@Override
|
||||||
|
protected void initChannel(LocalChannel ch) throws Exception {
|
||||||
|
// Add the given handler
|
||||||
|
ch.pipeline().addLast(handler);
|
||||||
|
// Add the "dumpHandler" last to log the incoming message
|
||||||
|
ch.pipeline().addLast(dumpHandler);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
Bootstrap b =
|
||||||
|
new Bootstrap()
|
||||||
|
.group(eventLoopGroup)
|
||||||
|
.channel(LocalChannel.class)
|
||||||
|
.handler(clientInitializer)
|
||||||
|
.attr(PROTOCOL_KEY, protocol)
|
||||||
|
.attr(REMOTE_ADDRESS_KEY, host);
|
||||||
|
|
||||||
|
channel = b.connect(localAddress).syncUninterruptibly().channel();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void checkReady() {
|
||||||
|
checkState(channel != null, "Must call setUpClient to finish NettyRule setup");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test that custom setup to send message to current server sends right message
|
||||||
|
*/
|
||||||
|
public void assertReceivedMessage(String message) throws Exception {
|
||||||
|
assertThat(echoHandler.getRequestFuture().get()).isEqualTo(message);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test that a message can go through, both inbound and outbound.
|
||||||
|
*
|
||||||
|
* <p>The client writes the message to the server, which echos it back and saves the string in
|
||||||
|
* its promise. The client receives the echo and saves it in its promise. All these activities
|
||||||
|
* happens in the I/O thread, and this call itself returns immediately.
|
||||||
|
*/
|
||||||
|
void assertThatMessagesWork() throws Exception {
|
||||||
|
checkReady();
|
||||||
|
assertThat(channel.isActive()).isTrue();
|
||||||
|
|
||||||
|
writeToChannelAndFlush(channel, "Hello, world!");
|
||||||
|
assertThat(echoHandler.getRequestFuture().get()).isEqualTo("Hello, world!");
|
||||||
|
assertThat(dumpHandler.getResponseFuture().get()).isEqualTo("Hello, world!");
|
||||||
|
}
|
||||||
|
|
||||||
|
Channel getChannel() {
|
||||||
|
checkReady();
|
||||||
|
return channel;
|
||||||
|
}
|
||||||
|
|
||||||
|
ThrowableSubject assertThatServerRootCause() {
|
||||||
|
checkReady();
|
||||||
|
return assertThat(
|
||||||
|
Throwables.getRootCause(
|
||||||
|
assertThrows(ExecutionException.class, () -> echoHandler.getRequestFuture().get())));
|
||||||
|
}
|
||||||
|
|
||||||
|
ThrowableSubject assertThatClientRootCause() {
|
||||||
|
checkReady();
|
||||||
|
return assertThat(
|
||||||
|
Throwables.getRootCause(
|
||||||
|
assertThrows(ExecutionException.class, () -> dumpHandler.getResponseFuture().get())));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void after() {
|
||||||
|
Future<?> unusedFuture = eventLoopGroup.shutdownGracefully();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A handler that echoes back its inbound message. The message is also saved in a promise for
|
||||||
|
* inspection later.
|
||||||
|
*/
|
||||||
|
public static class EchoHandler extends ChannelInboundHandlerAdapter {
|
||||||
|
|
||||||
|
private final CompletableFuture<String> requestFuture = new CompletableFuture<>();
|
||||||
|
|
||||||
|
public Future<String> getRequestFuture() {
|
||||||
|
return requestFuture;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
|
||||||
|
// In the test we only send messages of type ByteBuf.
|
||||||
|
|
||||||
|
assertThat(msg).isInstanceOf(ByteBuf.class);
|
||||||
|
String request = ((ByteBuf) msg).toString(UTF_8);
|
||||||
|
// After the message is written back to the client, fulfill the promise.
|
||||||
|
ChannelFuture unusedFuture =
|
||||||
|
ctx.writeAndFlush(msg).addListener(f -> requestFuture.complete(request));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Saves any inbound error as the cause of the promise failure.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
|
||||||
|
ChannelFuture unusedFuture =
|
||||||
|
ctx.channel().closeFuture().addListener(f -> requestFuture.completeExceptionally(cause));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A handler that dumps its inbound message to a promise that can be inspected later.
|
||||||
|
*/
|
||||||
|
private static class DumpHandler extends ChannelInboundHandlerAdapter {
|
||||||
|
|
||||||
|
private final CompletableFuture<String> responseFuture = new CompletableFuture<>();
|
||||||
|
|
||||||
|
Future<String> getResponseFuture() {
|
||||||
|
return responseFuture;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
|
||||||
|
// In the test we only send messages of type ByteBuf.
|
||||||
|
assertThat(msg).isInstanceOf(ByteBuf.class);
|
||||||
|
String response = ((ByteBuf) msg).toString(UTF_8);
|
||||||
|
// There is no more use of this message, we should release its reference count so that it
|
||||||
|
// can be more effectively garbage collected by Netty.
|
||||||
|
ReferenceCountUtil.release(msg);
|
||||||
|
// Save the string in the promise and make it as complete.
|
||||||
|
responseFuture.complete(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Saves any inbound error into the failure cause of the promise.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
|
||||||
|
ctx.channel().closeFuture().addListener(f -> responseFuture.completeExceptionally(cause));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,214 @@
|
||||||
|
// Copyright 2019 The Nomulus Authors. All Rights Reserved.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package google.registry.monitoring.blackbox.handlers;
|
||||||
|
|
||||||
|
import static com.google.common.truth.Truth.assertThat;
|
||||||
|
import static google.registry.monitoring.blackbox.ProbingAction.REMOTE_ADDRESS_KEY;
|
||||||
|
import static google.registry.monitoring.blackbox.Protocol.PROTOCOL_KEY;
|
||||||
|
import static google.registry.monitoring.blackbox.handlers.SslInitializerTestUtils.getKeyPair;
|
||||||
|
import static google.registry.monitoring.blackbox.handlers.SslInitializerTestUtils.setUpSslChannel;
|
||||||
|
import static google.registry.monitoring.blackbox.handlers.SslInitializerTestUtils.signKeyPair;
|
||||||
|
|
||||||
|
import com.google.common.collect.ImmutableList;
|
||||||
|
import google.registry.monitoring.blackbox.Protocol;
|
||||||
|
import io.netty.channel.ChannelHandler;
|
||||||
|
import io.netty.channel.ChannelPipeline;
|
||||||
|
import io.netty.channel.embedded.EmbeddedChannel;
|
||||||
|
import io.netty.channel.local.LocalAddress;
|
||||||
|
import io.netty.channel.local.LocalChannel;
|
||||||
|
import io.netty.handler.ssl.OpenSsl;
|
||||||
|
import io.netty.handler.ssl.SniHandler;
|
||||||
|
import io.netty.handler.ssl.SslContext;
|
||||||
|
import io.netty.handler.ssl.SslContextBuilder;
|
||||||
|
import io.netty.handler.ssl.SslHandler;
|
||||||
|
import io.netty.handler.ssl.SslProvider;
|
||||||
|
import io.netty.handler.ssl.util.SelfSignedCertificate;
|
||||||
|
import java.security.KeyPair;
|
||||||
|
import java.security.PrivateKey;
|
||||||
|
import java.security.cert.CertPathBuilderException;
|
||||||
|
import java.security.cert.CertificateException;
|
||||||
|
import java.security.cert.X509Certificate;
|
||||||
|
import javax.net.ssl.SSLException;
|
||||||
|
import org.junit.Rule;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.junit.runner.RunWith;
|
||||||
|
import org.junit.runners.Parameterized;
|
||||||
|
import org.junit.runners.Parameterized.Parameter;
|
||||||
|
import org.junit.runners.Parameterized.Parameters;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unit tests for {@link SslClientInitializer}.
|
||||||
|
*
|
||||||
|
* <p>To validate that the handler accepts & rejects connections as expected, a test server and a
|
||||||
|
* test client are spun up, and both connect to the {@link LocalAddress} within the JVM. This avoids
|
||||||
|
* the overhead of routing traffic through the network layer, even if it were to go through
|
||||||
|
* loopback. It also alleviates the need to pick a free port to use.
|
||||||
|
*
|
||||||
|
* <p>The local addresses used in each test method must to be different, otherwise tests run in
|
||||||
|
* parallel may interfere with each other.
|
||||||
|
*/
|
||||||
|
@RunWith(Parameterized.class)
|
||||||
|
public class SslClientInitializerTest {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fake host to test if the SSL engine gets the correct peer host.
|
||||||
|
*/
|
||||||
|
private static final String SSL_HOST = "www.example.tld";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fake port to test if the SSL engine gets the correct peer port.
|
||||||
|
*/
|
||||||
|
private static final int SSL_PORT = 12345;
|
||||||
|
/**
|
||||||
|
* Fake protocol saved in channel attribute.
|
||||||
|
*/
|
||||||
|
private static final Protocol PROTOCOL = Protocol.builder()
|
||||||
|
.setName("ssl")
|
||||||
|
.setPort(SSL_PORT)
|
||||||
|
.setHandlerProviders(ImmutableList.of())
|
||||||
|
.setPersistentConnection(false)
|
||||||
|
.build();
|
||||||
|
@Rule
|
||||||
|
public NettyRule nettyRule = new NettyRule();
|
||||||
|
@Parameter(0)
|
||||||
|
public SslProvider sslProvider;
|
||||||
|
/**
|
||||||
|
* Saves the SNI hostname received by the server, if sent by the client.
|
||||||
|
*/
|
||||||
|
private String sniHostReceived;
|
||||||
|
|
||||||
|
// We do our best effort to test all available SSL providers.
|
||||||
|
@Parameters(name = "{0}")
|
||||||
|
public static SslProvider[] data() {
|
||||||
|
return OpenSsl.isAvailable()
|
||||||
|
? new SslProvider[]{SslProvider.JDK, SslProvider.OPENSSL}
|
||||||
|
: new SslProvider[]{SslProvider.JDK};
|
||||||
|
}
|
||||||
|
|
||||||
|
private ChannelHandler getServerHandler(PrivateKey privateKey, X509Certificate certificate)
|
||||||
|
throws Exception {
|
||||||
|
SslContext sslContext = SslContextBuilder.forServer(privateKey, certificate).build();
|
||||||
|
return new SniHandler(
|
||||||
|
hostname -> {
|
||||||
|
sniHostReceived = hostname;
|
||||||
|
return sslContext;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSuccess_swappedInitializerWithSslHandler() throws Exception {
|
||||||
|
SslClientInitializer<EmbeddedChannel> sslClientInitializer =
|
||||||
|
new SslClientInitializer<>(sslProvider);
|
||||||
|
EmbeddedChannel channel = new EmbeddedChannel();
|
||||||
|
channel.attr(PROTOCOL_KEY).set(PROTOCOL);
|
||||||
|
channel.attr(REMOTE_ADDRESS_KEY).set(SSL_HOST);
|
||||||
|
ChannelPipeline pipeline = channel.pipeline();
|
||||||
|
pipeline.addLast(sslClientInitializer);
|
||||||
|
ChannelHandler firstHandler = pipeline.first();
|
||||||
|
assertThat(firstHandler.getClass()).isEqualTo(SslHandler.class);
|
||||||
|
SslHandler sslHandler = (SslHandler) firstHandler;
|
||||||
|
assertThat(sslHandler.engine().getPeerHost()).isEqualTo(SSL_HOST);
|
||||||
|
assertThat(sslHandler.engine().getPeerPort()).isEqualTo(SSL_PORT);
|
||||||
|
assertThat(channel.isActive()).isTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSuccess_protocolAttributeNotSet() {
|
||||||
|
SslClientInitializer<EmbeddedChannel> sslClientInitializer =
|
||||||
|
new SslClientInitializer<>(sslProvider);
|
||||||
|
EmbeddedChannel channel = new EmbeddedChannel();
|
||||||
|
ChannelPipeline pipeline = channel.pipeline();
|
||||||
|
pipeline.addLast(sslClientInitializer);
|
||||||
|
// Channel initializer swallows error thrown, and closes the connection.
|
||||||
|
assertThat(channel.isActive()).isFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testFailure_defaultTrustManager_rejectSelfSignedCert() throws Exception {
|
||||||
|
SelfSignedCertificate ssc = new SelfSignedCertificate(SSL_HOST);
|
||||||
|
LocalAddress localAddress =
|
||||||
|
new LocalAddress("DEFAULT_TRUST_MANAGER_REJECT_SELF_SIGNED_CERT_" + sslProvider);
|
||||||
|
nettyRule.setUpServer(localAddress, getServerHandler(ssc.key(), ssc.cert()));
|
||||||
|
SslClientInitializer<LocalChannel> sslClientInitializer =
|
||||||
|
new SslClientInitializer<>(sslProvider);
|
||||||
|
|
||||||
|
nettyRule.setUpClient(localAddress, PROTOCOL, SSL_HOST, sslClientInitializer);
|
||||||
|
// The connection is now terminated, both the client side and the server side should get
|
||||||
|
// exceptions.
|
||||||
|
nettyRule.assertThatClientRootCause().isInstanceOf(CertPathBuilderException.class);
|
||||||
|
nettyRule.assertThatServerRootCause().isInstanceOf(SSLException.class);
|
||||||
|
assertThat(nettyRule.getChannel().isActive()).isFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSuccess_customTrustManager_acceptCertSignedByTrustedCa() throws Exception {
|
||||||
|
LocalAddress localAddress =
|
||||||
|
new LocalAddress("CUSTOM_TRUST_MANAGER_ACCEPT_CERT_SIGNED_BY_TRUSTED_CA_" + sslProvider);
|
||||||
|
|
||||||
|
// Generate a new key pair.
|
||||||
|
KeyPair keyPair = getKeyPair();
|
||||||
|
|
||||||
|
// Generate a self signed certificate, and use it to sign the key pair.
|
||||||
|
SelfSignedCertificate ssc = new SelfSignedCertificate();
|
||||||
|
X509Certificate cert = signKeyPair(ssc, keyPair, SSL_HOST);
|
||||||
|
|
||||||
|
// Set up the server to use the signed cert and private key to perform handshake;
|
||||||
|
PrivateKey privateKey = keyPair.getPrivate();
|
||||||
|
nettyRule.setUpServer(localAddress, getServerHandler(privateKey, cert));
|
||||||
|
|
||||||
|
// Set up the client to trust the self signed cert used to sign the cert that server provides.
|
||||||
|
SslClientInitializer<LocalChannel> sslClientInitializer =
|
||||||
|
new SslClientInitializer<>(sslProvider, new X509Certificate[]{ssc.cert()});
|
||||||
|
|
||||||
|
nettyRule.setUpClient(localAddress, PROTOCOL, SSL_HOST, sslClientInitializer);
|
||||||
|
|
||||||
|
setUpSslChannel(nettyRule.getChannel(), cert);
|
||||||
|
nettyRule.assertThatMessagesWork();
|
||||||
|
|
||||||
|
// Verify that the SNI extension is sent during handshake.
|
||||||
|
assertThat(sniHostReceived).isEqualTo(SSL_HOST);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testFailure_customTrustManager_wrongHostnameInCertificate() throws Exception {
|
||||||
|
LocalAddress localAddress =
|
||||||
|
new LocalAddress("CUSTOM_TRUST_MANAGER_WRONG_HOSTNAME_" + sslProvider);
|
||||||
|
|
||||||
|
// Generate a new key pair.
|
||||||
|
KeyPair keyPair = getKeyPair();
|
||||||
|
|
||||||
|
// Generate a self signed certificate, and use it to sign the key pair.
|
||||||
|
SelfSignedCertificate ssc = new SelfSignedCertificate();
|
||||||
|
X509Certificate cert = signKeyPair(ssc, keyPair, "wrong.com");
|
||||||
|
|
||||||
|
// Set up the server to use the signed cert and private key to perform handshake;
|
||||||
|
PrivateKey privateKey = keyPair.getPrivate();
|
||||||
|
nettyRule.setUpServer(localAddress, getServerHandler(privateKey, cert));
|
||||||
|
|
||||||
|
// Set up the client to trust the self signed cert used to sign the cert that server provides.
|
||||||
|
SslClientInitializer<LocalChannel> sslClientInitializer =
|
||||||
|
new SslClientInitializer<>(sslProvider, new X509Certificate[]{ssc.cert()});
|
||||||
|
|
||||||
|
nettyRule.setUpClient(localAddress, PROTOCOL, SSL_HOST, sslClientInitializer);
|
||||||
|
|
||||||
|
// When the client rejects the server cert due to wrong hostname, both the client and server
|
||||||
|
// should throw exceptions.
|
||||||
|
nettyRule.assertThatClientRootCause().isInstanceOf(CertificateException.class);
|
||||||
|
nettyRule.assertThatClientRootCause().hasMessageThat().contains(SSL_HOST);
|
||||||
|
nettyRule.assertThatServerRootCause().isInstanceOf(SSLException.class);
|
||||||
|
assertThat(nettyRule.getChannel().isActive()).isFalse();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,232 @@
|
||||||
|
// Copyright 2019 The Nomulus Authors. All Rights Reserved.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package google.registry.monitoring.blackbox.handlers;
|
||||||
|
|
||||||
|
import static com.google.common.truth.Truth.assertThat;
|
||||||
|
import static google.registry.monitoring.blackbox.ProbingAction.CONNECTION_FUTURE_KEY;
|
||||||
|
import static google.registry.monitoring.blackbox.Protocol.PROTOCOL_KEY;
|
||||||
|
import static google.registry.monitoring.blackbox.TestUtils.makeHttpGetRequest;
|
||||||
|
import static google.registry.monitoring.blackbox.TestUtils.makeHttpResponse;
|
||||||
|
import static google.registry.monitoring.blackbox.TestUtils.makeRedirectResponse;
|
||||||
|
|
||||||
|
import com.google.common.collect.ImmutableList;
|
||||||
|
import google.registry.monitoring.blackbox.Protocol;
|
||||||
|
import google.registry.monitoring.blackbox.exceptions.FailureException;
|
||||||
|
import google.registry.monitoring.blackbox.messages.HttpRequestMessage;
|
||||||
|
import google.registry.monitoring.blackbox.messages.HttpResponseMessage;
|
||||||
|
import google.registry.monitoring.blackbox.testservers.TestServer;
|
||||||
|
import io.netty.bootstrap.Bootstrap;
|
||||||
|
import io.netty.channel.ChannelFuture;
|
||||||
|
import io.netty.channel.ChannelHandler;
|
||||||
|
import io.netty.channel.EventLoopGroup;
|
||||||
|
import io.netty.channel.embedded.EmbeddedChannel;
|
||||||
|
import io.netty.channel.local.LocalAddress;
|
||||||
|
import io.netty.channel.local.LocalChannel;
|
||||||
|
import io.netty.channel.nio.NioEventLoopGroup;
|
||||||
|
import io.netty.handler.codec.http.FullHttpResponse;
|
||||||
|
import io.netty.handler.codec.http.HttpResponseStatus;
|
||||||
|
import javax.inject.Provider;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.junit.runner.RunWith;
|
||||||
|
import org.junit.runners.JUnit4;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unit tests for {@link WebWhoisActionHandler}.
|
||||||
|
*
|
||||||
|
* <p>Attempts to test how well {@link WebWhoisActionHandler} works
|
||||||
|
* when responding to all possible types of responses </p>
|
||||||
|
*/
|
||||||
|
@RunWith(JUnit4.class)
|
||||||
|
public class WebWhoisActionHandlerTest {
|
||||||
|
|
||||||
|
private static final int HTTP_PORT = 80;
|
||||||
|
private static final String HTTP_REDIRECT = "http://";
|
||||||
|
private static final String TARGET_HOST = "whois.nic.tld";
|
||||||
|
private static final String DUMMY_URL = "__WILL_NOT_WORK__";
|
||||||
|
private final Protocol standardProtocol = Protocol.builder()
|
||||||
|
.setHandlerProviders(ImmutableList.of(() -> new WebWhoisActionHandler(
|
||||||
|
null, null, null, null)))
|
||||||
|
.setName("http")
|
||||||
|
.setPersistentConnection(false)
|
||||||
|
.setPort(HTTP_PORT)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
|
||||||
|
private EmbeddedChannel channel;
|
||||||
|
private ActionHandler actionHandler;
|
||||||
|
private Provider<? extends ChannelHandler> actionHandlerProvider;
|
||||||
|
private Protocol initialProtocol;
|
||||||
|
private HttpRequestMessage msg;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates default protocol with empty list of handlers and specified other inputs
|
||||||
|
*/
|
||||||
|
private Protocol createProtocol(String name, int port, boolean persistentConnection) {
|
||||||
|
return Protocol.builder()
|
||||||
|
.setName(name)
|
||||||
|
.setPort(port)
|
||||||
|
.setHandlerProviders(ImmutableList.of(actionHandlerProvider))
|
||||||
|
.setPersistentConnection(persistentConnection)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes new WebWhoisActionHandler
|
||||||
|
*/
|
||||||
|
private void setupActionHandler(Bootstrap bootstrap, HttpRequestMessage messageTemplate) {
|
||||||
|
actionHandler = new WebWhoisActionHandler(
|
||||||
|
bootstrap,
|
||||||
|
standardProtocol,
|
||||||
|
standardProtocol,
|
||||||
|
messageTemplate
|
||||||
|
);
|
||||||
|
actionHandlerProvider = () -> actionHandler;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets up testing channel with requisite attributes
|
||||||
|
*/
|
||||||
|
private void setupChannel(Protocol protocol) {
|
||||||
|
channel = new EmbeddedChannel(actionHandler);
|
||||||
|
channel.attr(PROTOCOL_KEY).set(protocol);
|
||||||
|
channel.attr(CONNECTION_FUTURE_KEY).set(channel.newSucceededFuture());
|
||||||
|
}
|
||||||
|
|
||||||
|
private Bootstrap makeBootstrap(EventLoopGroup group) {
|
||||||
|
return new Bootstrap()
|
||||||
|
.group(group)
|
||||||
|
.channel(LocalChannel.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private void setup(String hostName, Bootstrap bootstrap, boolean persistentConnection) {
|
||||||
|
msg = new HttpRequestMessage(makeHttpGetRequest(hostName, ""));
|
||||||
|
setupActionHandler(bootstrap, msg);
|
||||||
|
initialProtocol = createProtocol("testProtocol", 0, persistentConnection);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testBasic_responseOk() {
|
||||||
|
//setup
|
||||||
|
setup("", null, true);
|
||||||
|
setupChannel(initialProtocol);
|
||||||
|
|
||||||
|
//stores future
|
||||||
|
ChannelFuture future = actionHandler.getFinishedFuture();
|
||||||
|
channel.writeOutbound(msg);
|
||||||
|
|
||||||
|
FullHttpResponse response = new HttpResponseMessage(makeHttpResponse(HttpResponseStatus.OK));
|
||||||
|
|
||||||
|
//assesses that future listener isn't triggered yet.
|
||||||
|
assertThat(future.isDone()).isFalse();
|
||||||
|
|
||||||
|
channel.writeInbound(response);
|
||||||
|
|
||||||
|
//assesses that we successfully received good response and protocol is unchanged
|
||||||
|
assertThat(future.isSuccess()).isTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testBasic_responseFailure_badRequest() {
|
||||||
|
//setup
|
||||||
|
setup("", null, false);
|
||||||
|
setupChannel(initialProtocol);
|
||||||
|
|
||||||
|
// Stores future that informs when action is completed.
|
||||||
|
ChannelFuture future = actionHandler.getFinishedFuture();
|
||||||
|
channel.writeOutbound(msg);
|
||||||
|
|
||||||
|
FullHttpResponse response = new HttpResponseMessage(
|
||||||
|
makeHttpResponse(HttpResponseStatus.BAD_REQUEST));
|
||||||
|
|
||||||
|
// Assesses that future listener isn't triggered yet.
|
||||||
|
assertThat(future.isDone()).isFalse();
|
||||||
|
|
||||||
|
channel.writeInbound(response);
|
||||||
|
|
||||||
|
// Assesses that listener is triggered, but event is not success
|
||||||
|
assertThat(future.isDone()).isTrue();
|
||||||
|
assertThat(future.isSuccess()).isFalse();
|
||||||
|
|
||||||
|
// Ensures that we fail as a result of a FailureException.
|
||||||
|
assertThat(future.cause() instanceof FailureException).isTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testBasic_responseFailure_badURL() {
|
||||||
|
//setup
|
||||||
|
setup("", null, false);
|
||||||
|
setupChannel(initialProtocol);
|
||||||
|
|
||||||
|
//stores future
|
||||||
|
ChannelFuture future = actionHandler.getFinishedFuture();
|
||||||
|
channel.writeOutbound(msg);
|
||||||
|
|
||||||
|
FullHttpResponse response = new HttpResponseMessage(
|
||||||
|
makeRedirectResponse(HttpResponseStatus.MOVED_PERMANENTLY, DUMMY_URL, true));
|
||||||
|
|
||||||
|
//assesses that future listener isn't triggered yet.
|
||||||
|
assertThat(future.isDone()).isFalse();
|
||||||
|
|
||||||
|
channel.writeInbound(response);
|
||||||
|
|
||||||
|
//assesses that listener is triggered, and event is success
|
||||||
|
assertThat(future.isDone()).isTrue();
|
||||||
|
assertThat(future.isSuccess()).isFalse();
|
||||||
|
|
||||||
|
// Ensures that we fail as a result of a FailureException.
|
||||||
|
assertThat(future.cause() instanceof FailureException).isTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testAdvanced_redirect() {
|
||||||
|
// Sets up EventLoopGroup with 1 thread to be blocking.
|
||||||
|
EventLoopGroup group = new NioEventLoopGroup(1);
|
||||||
|
|
||||||
|
// Sets up embedded channel.
|
||||||
|
setup("", makeBootstrap(group), false);
|
||||||
|
setupChannel(initialProtocol);
|
||||||
|
|
||||||
|
// Initializes LocalAddress with unique String.
|
||||||
|
LocalAddress address = new LocalAddress(TARGET_HOST);
|
||||||
|
|
||||||
|
//stores future
|
||||||
|
ChannelFuture future = actionHandler.getFinishedFuture();
|
||||||
|
channel.writeOutbound(msg);
|
||||||
|
|
||||||
|
// Path that we test WebWhoisActionHandler uses.
|
||||||
|
String path = "/test";
|
||||||
|
|
||||||
|
// Sets up the local server that the handler will be redirected to.
|
||||||
|
TestServer.webWhoisServer(group, address, "", TARGET_HOST, path);
|
||||||
|
|
||||||
|
FullHttpResponse response =
|
||||||
|
new HttpResponseMessage(makeRedirectResponse(HttpResponseStatus.MOVED_PERMANENTLY,
|
||||||
|
HTTP_REDIRECT + TARGET_HOST + path, true));
|
||||||
|
|
||||||
|
//checks that future has not been set to successful or a failure
|
||||||
|
assertThat(future.isDone()).isFalse();
|
||||||
|
|
||||||
|
channel.writeInbound(response);
|
||||||
|
|
||||||
|
//makes sure old channel is shut down when attempting redirection
|
||||||
|
assertThat(channel.isActive()).isFalse();
|
||||||
|
|
||||||
|
//assesses that we successfully received good response and protocol is unchanged
|
||||||
|
assertThat(future.syncUninterruptibly().isSuccess()).isTrue();
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,123 @@
|
||||||
|
// Copyright 2019 The Nomulus Authors. All Rights Reserved.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package google.registry.monitoring.blackbox.testservers;
|
||||||
|
|
||||||
|
import static google.registry.monitoring.blackbox.TestUtils.makeHttpResponse;
|
||||||
|
import static google.registry.monitoring.blackbox.TestUtils.makeRedirectResponse;
|
||||||
|
|
||||||
|
import com.google.common.collect.ImmutableList;
|
||||||
|
import google.registry.monitoring.blackbox.messages.HttpResponseMessage;
|
||||||
|
import io.netty.bootstrap.ServerBootstrap;
|
||||||
|
import io.netty.channel.ChannelFuture;
|
||||||
|
import io.netty.channel.ChannelHandler;
|
||||||
|
import io.netty.channel.ChannelHandler.Sharable;
|
||||||
|
import io.netty.channel.ChannelHandlerContext;
|
||||||
|
import io.netty.channel.ChannelInitializer;
|
||||||
|
import io.netty.channel.EventLoopGroup;
|
||||||
|
import io.netty.channel.SimpleChannelInboundHandler;
|
||||||
|
import io.netty.channel.local.LocalAddress;
|
||||||
|
import io.netty.channel.local.LocalChannel;
|
||||||
|
import io.netty.channel.local.LocalServerChannel;
|
||||||
|
import io.netty.channel.nio.NioEventLoopGroup;
|
||||||
|
import io.netty.handler.codec.http.HttpRequest;
|
||||||
|
import io.netty.handler.codec.http.HttpResponse;
|
||||||
|
import io.netty.handler.codec.http.HttpResponseStatus;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mock Server Superclass whose subclasses implement specific behaviors we expect blackbox server to
|
||||||
|
* perform
|
||||||
|
*/
|
||||||
|
public class TestServer {
|
||||||
|
|
||||||
|
public TestServer(LocalAddress localAddress, ImmutableList<? extends ChannelHandler> handlers) {
|
||||||
|
this(new NioEventLoopGroup(1), localAddress, handlers);
|
||||||
|
}
|
||||||
|
|
||||||
|
public TestServer(EventLoopGroup eventLoopGroup, LocalAddress localAddress,
|
||||||
|
ImmutableList<? extends ChannelHandler> handlers) {
|
||||||
|
//Creates ChannelInitializer with handlers specified
|
||||||
|
ChannelInitializer<LocalChannel> serverInitializer = new ChannelInitializer<LocalChannel>() {
|
||||||
|
@Override
|
||||||
|
protected void initChannel(LocalChannel ch) {
|
||||||
|
for (ChannelHandler handler : handlers) {
|
||||||
|
ch.pipeline().addLast(handler);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
//Sets up serverBootstrap with specified initializer, eventLoopGroup, and using
|
||||||
|
// LocalServerChannel class
|
||||||
|
ServerBootstrap serverBootstrap =
|
||||||
|
new ServerBootstrap()
|
||||||
|
.group(eventLoopGroup)
|
||||||
|
.channel(LocalServerChannel.class)
|
||||||
|
.childHandler(serverInitializer);
|
||||||
|
|
||||||
|
ChannelFuture unusedFuture = serverBootstrap.bind(localAddress).syncUninterruptibly();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public static TestServer webWhoisServer(EventLoopGroup eventLoopGroup,
|
||||||
|
LocalAddress localAddress, String redirectInput, String destinationInput,
|
||||||
|
String destinationPath) {
|
||||||
|
return new TestServer(
|
||||||
|
eventLoopGroup,
|
||||||
|
localAddress,
|
||||||
|
ImmutableList.of(new RedirectHandler(redirectInput, destinationInput, destinationPath))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handler that will wither redirect client, give successful response, or give error messge
|
||||||
|
*/
|
||||||
|
@Sharable
|
||||||
|
static class RedirectHandler extends SimpleChannelInboundHandler<HttpRequest> {
|
||||||
|
|
||||||
|
private String redirectInput;
|
||||||
|
private String destinationInput;
|
||||||
|
private String destinationPath;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param redirectInput - Server will send back redirect to {@code destinationInput} when
|
||||||
|
* receiving a request with this host location
|
||||||
|
* @param destinationInput - Server will send back an {@link HttpResponseStatus} OK response
|
||||||
|
* when receiving a request with this host location
|
||||||
|
*/
|
||||||
|
public RedirectHandler(String redirectInput, String destinationInput, String destinationPath) {
|
||||||
|
this.redirectInput = redirectInput;
|
||||||
|
this.destinationInput = destinationInput;
|
||||||
|
this.destinationPath = destinationPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reads input {@link HttpRequest}, and creates appropriate {@link HttpResponseMessage} based on
|
||||||
|
* what header location is
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void channelRead0(ChannelHandlerContext ctx, HttpRequest request) {
|
||||||
|
HttpResponse response;
|
||||||
|
if (request.headers().get("host").equals(redirectInput)) {
|
||||||
|
response = new HttpResponseMessage(
|
||||||
|
makeRedirectResponse(HttpResponseStatus.MOVED_PERMANENTLY, destinationInput, true));
|
||||||
|
} else if (request.headers().get("host").equals(destinationInput)
|
||||||
|
&& request.uri().equals(destinationPath)) {
|
||||||
|
response = new HttpResponseMessage(makeHttpResponse(HttpResponseStatus.OK));
|
||||||
|
} else {
|
||||||
|
response = new HttpResponseMessage(makeHttpResponse(HttpResponseStatus.BAD_REQUEST));
|
||||||
|
}
|
||||||
|
ChannelFuture unusedFuture = ctx.channel().writeAndFlush(response);
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,68 @@
|
||||||
|
// Copyright 2019 The Nomulus Authors. All Rights Reserved.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package google.registry.monitoring.blackbox.tokens;
|
||||||
|
|
||||||
|
import static com.google.common.truth.Truth.assertThat;
|
||||||
|
|
||||||
|
import com.google.common.collect.ImmutableList;
|
||||||
|
import google.registry.monitoring.blackbox.exceptions.UndeterminedStateException;
|
||||||
|
import google.registry.monitoring.blackbox.messages.HttpRequestMessage;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.junit.runner.RunWith;
|
||||||
|
import org.junit.runners.JUnit4;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unit Tests for {@link WebWhoisToken}
|
||||||
|
*/
|
||||||
|
@RunWith(JUnit4.class)
|
||||||
|
public class WebWhoisTokenTest {
|
||||||
|
|
||||||
|
private static String PREFIX = "whois.nic.";
|
||||||
|
private static String HOST = "starter";
|
||||||
|
private static String FIRST_TLD = "first_test";
|
||||||
|
private static String SECOND_TLD = "second_test";
|
||||||
|
private static String THIRD_TLD = "third_test";
|
||||||
|
private static ImmutableList<String> TEST_DOMAINS = ImmutableList.of(FIRST_TLD, SECOND_TLD,
|
||||||
|
THIRD_TLD);
|
||||||
|
|
||||||
|
public Token webToken = new WebWhoisToken(TEST_DOMAINS);
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testMessageModification() throws UndeterminedStateException {
|
||||||
|
//creates Request message with header
|
||||||
|
HttpRequestMessage message = new HttpRequestMessage();
|
||||||
|
message.headers().set("host", HOST);
|
||||||
|
|
||||||
|
//attempts to use Token's method for modifying the method based on its stored host
|
||||||
|
HttpRequestMessage secondMessage = (HttpRequestMessage) webToken.modifyMessage(message);
|
||||||
|
assertThat(secondMessage.headers().get("host")).isEqualTo(PREFIX + TEST_DOMAINS.get(0));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* As Circular Linked List has not been implemented yet, we cannot yet wrap around, so we don't
|
||||||
|
* test that in testing {@code next}.
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
public void testNextToken() {
|
||||||
|
assertThat(webToken.host()).isEqualTo(PREFIX + FIRST_TLD);
|
||||||
|
webToken = webToken.next();
|
||||||
|
|
||||||
|
assertThat(webToken.host()).isEqualTo(PREFIX + SECOND_TLD);
|
||||||
|
webToken = webToken.next();
|
||||||
|
|
||||||
|
assertThat(webToken.host()).isEqualTo(PREFIX + THIRD_TLD);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
Loading…
Add table
Add a link
Reference in a new issue