mirror of
https://github.com/google/nomulus.git
synced 2025-05-13 16:07:15 +02:00
Open source GCP proxy
Dagger updated to 2.13, along with all its dependencies. Also allows us to have multiple config files for different environment (prod, sandbox, alpha, local, etc) and specify which one to use on the command line with a --env flag. Therefore the same binary can be used in all environments. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=176551289
This commit is contained in:
parent
c7484b25e0
commit
7e42ee48a4
54 changed files with 6648 additions and 15 deletions
50
java/google/registry/proxy/BUILD
Normal file
50
java/google/registry/proxy/BUILD
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
# Description:
|
||||||
|
# This package contains the code for the binary that proxies TCP traffic from
|
||||||
|
# the GCE/GKE to AppEngine.
|
||||||
|
|
||||||
|
package(
|
||||||
|
default_visibility = ["//java/google/registry:registry_project"],
|
||||||
|
)
|
||||||
|
|
||||||
|
licenses(["notice"]) # Apache 2.0
|
||||||
|
|
||||||
|
java_library(
|
||||||
|
name = "proxy",
|
||||||
|
srcs = glob(["**/*.java"]),
|
||||||
|
resources = glob([
|
||||||
|
"resources/*",
|
||||||
|
"config/*.yaml",
|
||||||
|
]),
|
||||||
|
deps = [
|
||||||
|
"//java/google/registry/config",
|
||||||
|
"//java/google/registry/monitoring/metrics",
|
||||||
|
"//java/google/registry/monitoring/metrics/stackdriver",
|
||||||
|
"//java/google/registry/util",
|
||||||
|
"@com_beust_jcommander",
|
||||||
|
"@com_google_api_client",
|
||||||
|
"@com_google_apis_google_api_services_cloudkms",
|
||||||
|
"@com_google_apis_google_api_services_monitoring",
|
||||||
|
"@com_google_auto_value",
|
||||||
|
"@com_google_code_findbugs_jsr305",
|
||||||
|
"@com_google_dagger",
|
||||||
|
"@com_google_guava",
|
||||||
|
"@io_netty_buffer",
|
||||||
|
"@io_netty_codec",
|
||||||
|
"@io_netty_codec_http",
|
||||||
|
"@io_netty_common",
|
||||||
|
"@io_netty_handler",
|
||||||
|
"@io_netty_transport",
|
||||||
|
"@javax_inject",
|
||||||
|
"@joda_time",
|
||||||
|
"@org_bouncycastle_bcpkix_jdk15on",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
java_binary(
|
||||||
|
name = "proxy_server",
|
||||||
|
main_class = "google.registry.proxy.ProxyServer",
|
||||||
|
runtime_deps = [
|
||||||
|
":proxy",
|
||||||
|
"@io_netty_tcnative",
|
||||||
|
],
|
||||||
|
)
|
163
java/google/registry/proxy/CertificateModule.java
Normal file
163
java/google/registry/proxy/CertificateModule.java
Normal file
|
@ -0,0 +1,163 @@
|
||||||
|
// Copyright 2017 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.proxy;
|
||||||
|
|
||||||
|
import static com.google.common.base.Preconditions.checkState;
|
||||||
|
import static com.google.common.collect.ImmutableList.toImmutableList;
|
||||||
|
import static java.nio.charset.StandardCharsets.UTF_8;
|
||||||
|
|
||||||
|
import com.google.common.collect.ImmutableList;
|
||||||
|
import dagger.Module;
|
||||||
|
import dagger.Provides;
|
||||||
|
import google.registry.proxy.ProxyModule.PemBytes;
|
||||||
|
import google.registry.util.FormattingLogger;
|
||||||
|
import java.io.ByteArrayInputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStreamReader;
|
||||||
|
import java.security.PrivateKey;
|
||||||
|
import java.security.Security;
|
||||||
|
import java.security.cert.CertificateException;
|
||||||
|
import java.security.cert.X509Certificate;
|
||||||
|
import java.util.function.Function;
|
||||||
|
import javax.inject.Named;
|
||||||
|
import javax.inject.Singleton;
|
||||||
|
import org.bouncycastle.cert.X509CertificateHolder;
|
||||||
|
import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter;
|
||||||
|
import org.bouncycastle.jce.provider.BouncyCastleProvider;
|
||||||
|
import org.bouncycastle.openssl.PEMException;
|
||||||
|
import org.bouncycastle.openssl.PEMKeyPair;
|
||||||
|
import org.bouncycastle.openssl.PEMParser;
|
||||||
|
import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dagger module that provides bindings needed to inject EPP SSL certificate chain and private key.
|
||||||
|
*
|
||||||
|
* <p>The certificates and private key are stored in a .pem file that is encrypted by Cloud KMS. The
|
||||||
|
* .pem file can be generated by concatenating the .crt certificate files on the chain and the .key
|
||||||
|
* private file.
|
||||||
|
*
|
||||||
|
* <p>The certificates in the .pem file must be stored in order, where the next certificate's
|
||||||
|
* subject is the previous certificate's issuer.
|
||||||
|
*
|
||||||
|
* @see <a href="https://cloud.google.com/kms/">Cloud Key Management Service</a>
|
||||||
|
*/
|
||||||
|
@Module
|
||||||
|
public class CertificateModule {
|
||||||
|
|
||||||
|
private static final FormattingLogger logger = FormattingLogger.getLoggerForCallerClass();
|
||||||
|
|
||||||
|
static {
|
||||||
|
Security.addProvider(new BouncyCastleProvider());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Select specific type from a given {@link ImmutableList} and convert them using the converter.
|
||||||
|
*
|
||||||
|
* @param objects the {@link ImmutableList} to filter from.
|
||||||
|
* @param clazz the class to filter.
|
||||||
|
* @param converter the converter function to act on the items in the filtered list.
|
||||||
|
*/
|
||||||
|
private static <T, E> ImmutableList<E> filterAndConvert(
|
||||||
|
ImmutableList<Object> objects, Class<T> clazz, Function<T, E> converter) {
|
||||||
|
return objects
|
||||||
|
.stream()
|
||||||
|
.filter(obj -> clazz.isInstance(obj))
|
||||||
|
.map(obj -> clazz.cast(obj))
|
||||||
|
.map(converter)
|
||||||
|
.collect(toImmutableList());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Singleton
|
||||||
|
@Provides
|
||||||
|
@Named("pemObjects")
|
||||||
|
static ImmutableList<Object> providePemObjects(PemBytes pemBytes) {
|
||||||
|
PEMParser pemParser =
|
||||||
|
new PEMParser(new InputStreamReader(new ByteArrayInputStream(pemBytes.getBytes()), UTF_8));
|
||||||
|
ImmutableList.Builder<Object> listBuilder = new ImmutableList.Builder<>();
|
||||||
|
Object obj;
|
||||||
|
// PEMParser returns an object (private key, certificate, etc) each time readObject() is called,
|
||||||
|
// until no more object is to be read from the file.
|
||||||
|
while (true) {
|
||||||
|
try {
|
||||||
|
obj = pemParser.readObject();
|
||||||
|
if (obj == null) {
|
||||||
|
break;
|
||||||
|
} else {
|
||||||
|
listBuilder.add(obj);
|
||||||
|
}
|
||||||
|
} catch (IOException e) {
|
||||||
|
logger.severe(e, "Cannot parse PEM file correctly.");
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return listBuilder.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Singleton
|
||||||
|
@Provides
|
||||||
|
static PrivateKey providePrivateKey(@Named("pemObjects") ImmutableList<Object> pemObjects) {
|
||||||
|
JcaPEMKeyConverter converter = new JcaPEMKeyConverter().setProvider("BC");
|
||||||
|
Function<PEMKeyPair, PrivateKey> privateKeyConverter =
|
||||||
|
pemKeyPair -> {
|
||||||
|
try {
|
||||||
|
return converter.getKeyPair(pemKeyPair).getPrivate();
|
||||||
|
} catch (PEMException e) {
|
||||||
|
logger.severefmt(e, "Error converting private key: %s", pemKeyPair);
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
ImmutableList<PrivateKey> privateKeys =
|
||||||
|
filterAndConvert(pemObjects, PEMKeyPair.class, privateKeyConverter);
|
||||||
|
checkState(
|
||||||
|
privateKeys.size() == 1,
|
||||||
|
"The pem file must contain exactly one private key, but %s keys are found",
|
||||||
|
privateKeys.size());
|
||||||
|
return privateKeys.get(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Singleton
|
||||||
|
@Provides
|
||||||
|
@Named("eppServerCertificates")
|
||||||
|
static X509Certificate[] provideCertificates(
|
||||||
|
@Named("pemObjects") ImmutableList<Object> pemObject) {
|
||||||
|
JcaX509CertificateConverter converter = new JcaX509CertificateConverter().setProvider("BC");
|
||||||
|
Function<X509CertificateHolder, X509Certificate> certificateConverter =
|
||||||
|
certificateHolder -> {
|
||||||
|
try {
|
||||||
|
return converter.getCertificate(certificateHolder);
|
||||||
|
} catch (CertificateException e) {
|
||||||
|
logger.severefmt(e, "Error converting certificate: %s", certificateHolder);
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
ImmutableList<X509Certificate> certificates =
|
||||||
|
filterAndConvert(pemObject, X509CertificateHolder.class, certificateConverter);
|
||||||
|
checkState(certificates.size() != 0, "No certificates found in the pem file");
|
||||||
|
X509Certificate lastCert = null;
|
||||||
|
for (X509Certificate cert : certificates) {
|
||||||
|
if (lastCert != null) {
|
||||||
|
checkState(
|
||||||
|
lastCert.getIssuerX500Principal().equals(cert.getSubjectX500Principal()),
|
||||||
|
"Certificate chain error:\n%s\nis not signed by\n%s",
|
||||||
|
lastCert,
|
||||||
|
cert);
|
||||||
|
}
|
||||||
|
lastCert = cert;
|
||||||
|
}
|
||||||
|
X509Certificate[] certificateArray = new X509Certificate[certificates.size()];
|
||||||
|
certificates.toArray(certificateArray);
|
||||||
|
return certificateArray;
|
||||||
|
}
|
||||||
|
}
|
152
java/google/registry/proxy/EppProtocolModule.java
Normal file
152
java/google/registry/proxy/EppProtocolModule.java
Normal file
|
@ -0,0 +1,152 @@
|
||||||
|
// Copyright 2017 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.proxy;
|
||||||
|
|
||||||
|
import static google.registry.util.ResourceUtils.readResourceBytes;
|
||||||
|
|
||||||
|
import com.google.common.base.Supplier;
|
||||||
|
import com.google.common.collect.ImmutableList;
|
||||||
|
import dagger.Module;
|
||||||
|
import dagger.Provides;
|
||||||
|
import dagger.multibindings.IntoSet;
|
||||||
|
import google.registry.proxy.HttpsRelayProtocolModule.HttpsRelayProtocol;
|
||||||
|
import google.registry.proxy.Protocol.BackendProtocol;
|
||||||
|
import google.registry.proxy.Protocol.FrontendProtocol;
|
||||||
|
import google.registry.proxy.handler.EppServiceHandler;
|
||||||
|
import google.registry.proxy.handler.ProxyProtocolHandler;
|
||||||
|
import google.registry.proxy.handler.RelayHandler.FullHttpRequestRelayHandler;
|
||||||
|
import google.registry.proxy.handler.SslServerInitializer;
|
||||||
|
import google.registry.proxy.metric.FrontendMetrics;
|
||||||
|
import google.registry.util.FormattingLogger;
|
||||||
|
import io.netty.channel.ChannelHandler;
|
||||||
|
import io.netty.channel.socket.nio.NioSocketChannel;
|
||||||
|
import io.netty.handler.codec.LengthFieldBasedFrameDecoder;
|
||||||
|
import io.netty.handler.codec.LengthFieldPrepender;
|
||||||
|
import io.netty.handler.logging.LoggingHandler;
|
||||||
|
import io.netty.handler.timeout.ReadTimeoutHandler;
|
||||||
|
import java.io.IOException;
|
||||||
|
import javax.inject.Named;
|
||||||
|
import javax.inject.Provider;
|
||||||
|
import javax.inject.Qualifier;
|
||||||
|
import javax.inject.Singleton;
|
||||||
|
|
||||||
|
/** A module that provides the {@link FrontendProtocol} used for epp protocol. */
|
||||||
|
@Module
|
||||||
|
class EppProtocolModule {
|
||||||
|
|
||||||
|
private static final FormattingLogger logger = FormattingLogger.getLoggerForCallerClass();
|
||||||
|
|
||||||
|
/** Dagger qualifier to provide epp protocol related handlers and other bindings. */
|
||||||
|
@Qualifier
|
||||||
|
@interface EppProtocol {};
|
||||||
|
|
||||||
|
private static final String PROTOCOL_NAME = "epp";
|
||||||
|
|
||||||
|
@Singleton
|
||||||
|
@Provides
|
||||||
|
@IntoSet
|
||||||
|
static FrontendProtocol provideProtocol(
|
||||||
|
ProxyConfig config,
|
||||||
|
@EppProtocol int eppPort,
|
||||||
|
@EppProtocol ImmutableList<Provider<? extends ChannelHandler>> handlerProviders,
|
||||||
|
@HttpsRelayProtocol BackendProtocol.Builder backendProtocolBuilder) {
|
||||||
|
return Protocol.frontendBuilder()
|
||||||
|
.name(PROTOCOL_NAME)
|
||||||
|
.port(eppPort)
|
||||||
|
.handlerProviders(handlerProviders)
|
||||||
|
.relayProtocol(backendProtocolBuilder.host(config.epp.relayHost).build())
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
@EppProtocol
|
||||||
|
static ImmutableList<Provider<? extends ChannelHandler>> provideHandlerProviders(
|
||||||
|
Provider<SslServerInitializer<NioSocketChannel>> sslServerInitializerProvider,
|
||||||
|
Provider<ProxyProtocolHandler> proxyProtocolHandlerProvider,
|
||||||
|
@EppProtocol Provider<ReadTimeoutHandler> readTimeoutHandlerProvider,
|
||||||
|
Provider<LengthFieldBasedFrameDecoder> lengthFieldBasedFrameDecoderProvider,
|
||||||
|
Provider<LengthFieldPrepender> lengthFieldPrependerProvider,
|
||||||
|
Provider<EppServiceHandler> eppServiceHandlerProvider,
|
||||||
|
Provider<LoggingHandler> loggingHandlerProvider,
|
||||||
|
Provider<FullHttpRequestRelayHandler> relayHandlerProvider) {
|
||||||
|
return ImmutableList.of(
|
||||||
|
proxyProtocolHandlerProvider,
|
||||||
|
sslServerInitializerProvider,
|
||||||
|
readTimeoutHandlerProvider,
|
||||||
|
lengthFieldBasedFrameDecoderProvider,
|
||||||
|
lengthFieldPrependerProvider,
|
||||||
|
eppServiceHandlerProvider,
|
||||||
|
loggingHandlerProvider,
|
||||||
|
relayHandlerProvider);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
static LengthFieldBasedFrameDecoder provideLengthFieldBasedFrameDecoder(ProxyConfig config) {
|
||||||
|
return new LengthFieldBasedFrameDecoder(
|
||||||
|
// Max message length.
|
||||||
|
config.epp.maxMessageLengthBytes,
|
||||||
|
// Header field location offset.
|
||||||
|
0,
|
||||||
|
// Header field length.
|
||||||
|
config.epp.headerLengthBytes,
|
||||||
|
// Adjustment applied to the header field value in order to obtain message length.
|
||||||
|
-config.epp.headerLengthBytes,
|
||||||
|
// Initial bytes to strip (i. e. strip the length header).
|
||||||
|
config.epp.headerLengthBytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Singleton
|
||||||
|
@Provides
|
||||||
|
static LengthFieldPrepender provideLengthFieldPrepender(ProxyConfig config) {
|
||||||
|
return new LengthFieldPrepender(
|
||||||
|
// Header field length.
|
||||||
|
config.epp.headerLengthBytes,
|
||||||
|
// Length includes header field length.
|
||||||
|
true);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
@EppProtocol
|
||||||
|
static ReadTimeoutHandler provideReadTimeoutHandler(ProxyConfig config) {
|
||||||
|
return new ReadTimeoutHandler(config.epp.readTimeoutSeconds);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Singleton
|
||||||
|
@Provides
|
||||||
|
@Named("hello")
|
||||||
|
static byte[] provideHelloBytes() {
|
||||||
|
try {
|
||||||
|
return readResourceBytes(EppProtocolModule.class, "resources/hello.xml").read();
|
||||||
|
} catch (IOException e) {
|
||||||
|
logger.severe(e, "Cannot read EPP <hello> message file.");
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
static EppServiceHandler provideEppServiceHandler(
|
||||||
|
@Named("accessToken") Supplier<String> accessTokenSupplier,
|
||||||
|
@Named("hello") byte[] helloBytes,
|
||||||
|
FrontendMetrics metrics,
|
||||||
|
ProxyConfig config) {
|
||||||
|
return new EppServiceHandler(
|
||||||
|
config.epp.relayHost,
|
||||||
|
config.epp.relayPath,
|
||||||
|
accessTokenSupplier,
|
||||||
|
config.epp.serverHostname,
|
||||||
|
helloBytes,
|
||||||
|
metrics);
|
||||||
|
}
|
||||||
|
}
|
76
java/google/registry/proxy/HealthCheckProtocolModule.java
Normal file
76
java/google/registry/proxy/HealthCheckProtocolModule.java
Normal file
|
@ -0,0 +1,76 @@
|
||||||
|
// Copyright 2017 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.proxy;
|
||||||
|
|
||||||
|
import com.google.common.collect.ImmutableList;
|
||||||
|
import dagger.Module;
|
||||||
|
import dagger.Provides;
|
||||||
|
import dagger.multibindings.IntoSet;
|
||||||
|
import google.registry.proxy.Protocol.FrontendProtocol;
|
||||||
|
import google.registry.proxy.handler.HealthCheckHandler;
|
||||||
|
import io.netty.channel.ChannelHandler;
|
||||||
|
import io.netty.handler.codec.FixedLengthFrameDecoder;
|
||||||
|
import javax.inject.Provider;
|
||||||
|
import javax.inject.Qualifier;
|
||||||
|
import javax.inject.Singleton;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Module that provides a {@link FrontendProtocol} used for GCP load balancer health checking.
|
||||||
|
*
|
||||||
|
* <p>The load balancer sends health checking messages to the GCE instances to assess whether they
|
||||||
|
* are ready to receive traffic. No relay channel needs to be established for this protocol.
|
||||||
|
*/
|
||||||
|
@Module
|
||||||
|
public class HealthCheckProtocolModule {
|
||||||
|
|
||||||
|
/** Dagger qualifier to provide health check protocol related handlers and other bindings. */
|
||||||
|
@Qualifier
|
||||||
|
@interface HealthCheckProtocol {}
|
||||||
|
|
||||||
|
private static final String PROTOCOL_NAME = "health_check";
|
||||||
|
|
||||||
|
@Singleton
|
||||||
|
@Provides
|
||||||
|
@IntoSet
|
||||||
|
static FrontendProtocol provideProtocol(
|
||||||
|
@HealthCheckProtocol int healthCheckPort,
|
||||||
|
@HealthCheckProtocol ImmutableList<Provider<? extends ChannelHandler>> handlerProviders) {
|
||||||
|
return Protocol.frontendBuilder()
|
||||||
|
.name(PROTOCOL_NAME)
|
||||||
|
.port(healthCheckPort)
|
||||||
|
.isHealthCheck(true)
|
||||||
|
.handlerProviders(handlerProviders)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
@HealthCheckProtocol
|
||||||
|
static ImmutableList<Provider<? extends ChannelHandler>> provideHandlerProviders(
|
||||||
|
Provider<FixedLengthFrameDecoder> fixedLengthFrameDecoderProvider,
|
||||||
|
Provider<HealthCheckHandler> healthCheckHandlerProvider) {
|
||||||
|
return ImmutableList.of(fixedLengthFrameDecoderProvider, healthCheckHandlerProvider);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
static FixedLengthFrameDecoder provideFixedLengthFrameDecoder(ProxyConfig config) {
|
||||||
|
return new FixedLengthFrameDecoder(config.healthCheck.checkRequest.length());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
static HealthCheckHandler provideHealthCheckHandler(ProxyConfig config) {
|
||||||
|
return new HealthCheckHandler(
|
||||||
|
config.healthCheck.checkRequest, config.healthCheck.checkResponse);
|
||||||
|
}
|
||||||
|
}
|
97
java/google/registry/proxy/HttpsRelayProtocolModule.java
Normal file
97
java/google/registry/proxy/HttpsRelayProtocolModule.java
Normal file
|
@ -0,0 +1,97 @@
|
||||||
|
// Copyright 2017 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.proxy;
|
||||||
|
|
||||||
|
import com.google.common.collect.ImmutableList;
|
||||||
|
import dagger.Module;
|
||||||
|
import dagger.Provides;
|
||||||
|
import google.registry.proxy.Protocol.BackendProtocol;
|
||||||
|
import google.registry.proxy.handler.BackendMetricsHandler;
|
||||||
|
import google.registry.proxy.handler.RelayHandler.FullHttpResponseRelayHandler;
|
||||||
|
import google.registry.proxy.handler.SslClientInitializer;
|
||||||
|
import io.netty.channel.ChannelHandler;
|
||||||
|
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.logging.LoggingHandler;
|
||||||
|
import java.security.cert.X509Certificate;
|
||||||
|
import javax.annotation.Nullable;
|
||||||
|
import javax.inject.Named;
|
||||||
|
import javax.inject.Provider;
|
||||||
|
import javax.inject.Qualifier;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Module that provides a {@link BackendProtocol.Builder} for HTTPS protocol.
|
||||||
|
*
|
||||||
|
* <p>Only a builder is provided because the client protocol itself depends on the remote host
|
||||||
|
* address, which is provided in the server protocol module that relays to this client protocol
|
||||||
|
* module, e. g. {@link WhoisProtocolModule}.
|
||||||
|
*/
|
||||||
|
@Module
|
||||||
|
public class HttpsRelayProtocolModule {
|
||||||
|
|
||||||
|
/** Dagger qualifier to provide https relay protocol related handlers and other bindings. */
|
||||||
|
@Qualifier
|
||||||
|
@interface HttpsRelayProtocol {}
|
||||||
|
|
||||||
|
private static final String PROTOCOL_NAME = "https_relay";
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
@HttpsRelayProtocol
|
||||||
|
static BackendProtocol.Builder provideProtocolBuilder(
|
||||||
|
ProxyConfig config,
|
||||||
|
@HttpsRelayProtocol ImmutableList<Provider<? extends ChannelHandler>> handlerProviders) {
|
||||||
|
return Protocol.backendBuilder()
|
||||||
|
.name(PROTOCOL_NAME)
|
||||||
|
.port(config.httpsRelay.port)
|
||||||
|
.handlerProviders(handlerProviders);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
@HttpsRelayProtocol
|
||||||
|
static ImmutableList<Provider<? extends ChannelHandler>> provideHandlerProviders(
|
||||||
|
Provider<SslClientInitializer<NioSocketChannel>> sslClientInitializerProvider,
|
||||||
|
Provider<HttpClientCodec> httpClientCodecProvider,
|
||||||
|
Provider<HttpObjectAggregator> httpObjectAggregatorProvider,
|
||||||
|
Provider<BackendMetricsHandler> backendMetricsHandlerProvider,
|
||||||
|
Provider<LoggingHandler> loggingHandlerProvider,
|
||||||
|
Provider<FullHttpResponseRelayHandler> relayHandlerProvider) {
|
||||||
|
return ImmutableList.of(
|
||||||
|
sslClientInitializerProvider,
|
||||||
|
httpClientCodecProvider,
|
||||||
|
httpObjectAggregatorProvider,
|
||||||
|
backendMetricsHandlerProvider,
|
||||||
|
loggingHandlerProvider,
|
||||||
|
relayHandlerProvider);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
static HttpClientCodec provideHttpClientCodec() {
|
||||||
|
return new HttpClientCodec();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
static HttpObjectAggregator provideHttpObjectAggregator(ProxyConfig config) {
|
||||||
|
return new HttpObjectAggregator(config.httpsRelay.maxMessageLengthBytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
@Provides
|
||||||
|
@Named("relayTrustedCertificates")
|
||||||
|
public static X509Certificate[] provideTrustedCertificates() {
|
||||||
|
// null uses the system default trust store.
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
78
java/google/registry/proxy/MetricsModule.java
Normal file
78
java/google/registry/proxy/MetricsModule.java
Normal file
|
@ -0,0 +1,78 @@
|
||||||
|
// Copyright 2017 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.proxy;
|
||||||
|
|
||||||
|
import com.google.api.client.googleapis.auth.oauth2.GoogleCredential;
|
||||||
|
import com.google.api.client.googleapis.util.Utils;
|
||||||
|
import com.google.api.services.monitoring.v3.Monitoring;
|
||||||
|
import com.google.api.services.monitoring.v3.model.MonitoredResource;
|
||||||
|
import com.google.common.collect.ImmutableMap;
|
||||||
|
import com.google.common.util.concurrent.ThreadFactoryBuilder;
|
||||||
|
import dagger.Component;
|
||||||
|
import dagger.Module;
|
||||||
|
import dagger.Provides;
|
||||||
|
import google.registry.monitoring.metrics.MetricReporter;
|
||||||
|
import google.registry.monitoring.metrics.MetricWriter;
|
||||||
|
import google.registry.monitoring.metrics.stackdriver.StackdriverWriter;
|
||||||
|
import javax.inject.Singleton;
|
||||||
|
|
||||||
|
/** Module that provides necessary bindings to instantiate a {@link MetricReporter} */
|
||||||
|
@Module
|
||||||
|
public class MetricsModule {
|
||||||
|
|
||||||
|
// TODO (b/64765479): change to GKE cluster and config in YAML file.
|
||||||
|
private static final String MONITORED_RESOURCE_TYPE = "gce_instance";
|
||||||
|
private static final String GCE_INSTANCE_ZONE = "us-east4-c";
|
||||||
|
private static final String GCE_INSTANCE_ID = "5401454098973297721";
|
||||||
|
|
||||||
|
@Singleton
|
||||||
|
@Provides
|
||||||
|
static Monitoring provideMonitoring(GoogleCredential credential, ProxyConfig config) {
|
||||||
|
return new Monitoring.Builder(
|
||||||
|
Utils.getDefaultTransport(), Utils.getDefaultJsonFactory(), credential)
|
||||||
|
.setApplicationName(config.projectId)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Singleton
|
||||||
|
@Provides
|
||||||
|
static MetricWriter provideMetricWriter(Monitoring monitoringClient, ProxyConfig config) {
|
||||||
|
// The MonitoredResource for GAE apps is not writable (and missing fields anyway) so we just
|
||||||
|
// use the gce_instance resource type instead.
|
||||||
|
return new StackdriverWriter(
|
||||||
|
monitoringClient,
|
||||||
|
config.projectId,
|
||||||
|
new MonitoredResource()
|
||||||
|
.setType(MONITORED_RESOURCE_TYPE)
|
||||||
|
.setLabels(ImmutableMap.of("zone", GCE_INSTANCE_ZONE, "instance_id", GCE_INSTANCE_ID)),
|
||||||
|
config.metrics.stackdriverMaxQps,
|
||||||
|
config.metrics.stackdriverMaxPointsPerRequest);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Singleton
|
||||||
|
@Provides
|
||||||
|
static MetricReporter provideMetricReporter(MetricWriter metricWriter, ProxyConfig config) {
|
||||||
|
return new MetricReporter(
|
||||||
|
metricWriter,
|
||||||
|
config.metrics.writeIntervalSeconds,
|
||||||
|
new ThreadFactoryBuilder().setDaemon(true).build());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Singleton
|
||||||
|
@Component(modules = {MetricsModule.class, ProxyModule.class})
|
||||||
|
interface MetricsComponent {
|
||||||
|
MetricReporter metricReporter();
|
||||||
|
}
|
||||||
|
}
|
130
java/google/registry/proxy/Protocol.java
Normal file
130
java/google/registry/proxy/Protocol.java
Normal file
|
@ -0,0 +1,130 @@
|
||||||
|
// Copyright 2017 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.proxy;
|
||||||
|
|
||||||
|
import com.google.auto.value.AutoValue;
|
||||||
|
import com.google.common.base.Preconditions;
|
||||||
|
import com.google.common.collect.ImmutableList;
|
||||||
|
import io.netty.channel.Channel;
|
||||||
|
import io.netty.channel.ChannelHandler;
|
||||||
|
import io.netty.channel.socket.nio.NioSocketChannel;
|
||||||
|
import io.netty.util.Attribute;
|
||||||
|
import io.netty.util.AttributeKey;
|
||||||
|
import javax.annotation.Nullable;
|
||||||
|
import javax.inject.Provider;
|
||||||
|
|
||||||
|
/** Value class that encapsulates parameters of a specific connection. */
|
||||||
|
public interface Protocol {
|
||||||
|
|
||||||
|
/** Key used to retrieve the {@link Protocol} from a {@link Channel}'s {@link Attribute}. */
|
||||||
|
AttributeKey<Protocol> PROTOCOL_KEY = AttributeKey.valueOf("PROTOCOL_KEY");
|
||||||
|
|
||||||
|
/** Protocol name. */
|
||||||
|
String name();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Port to bind to (for {@link FrontendProtocol}) or to connect to (for {@link BackendProtocol}).
|
||||||
|
*/
|
||||||
|
int port();
|
||||||
|
|
||||||
|
/** The {@link ChannelHandler} providers to use for the protocol, in order. */
|
||||||
|
ImmutableList<Provider<? extends ChannelHandler>> handlerProviders();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A builder for {@link FrontendProtocol}, default is non-health-checking.
|
||||||
|
*
|
||||||
|
* @see HealthCheckProtocolModule
|
||||||
|
*/
|
||||||
|
static FrontendProtocol.Builder frontendBuilder() {
|
||||||
|
return new AutoValue_Protocol_FrontendProtocol.Builder().isHealthCheck(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
static BackendProtocol.Builder backendBuilder() {
|
||||||
|
return new AutoValue_Protocol_BackendProtocol.Builder();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generic builder enabling chaining for concrete implementations.
|
||||||
|
*
|
||||||
|
* @param <B> builder of the concrete subtype of {@link Protocol}.
|
||||||
|
* @param <P> type of the concrete subtype of {@link Protocol}.
|
||||||
|
*/
|
||||||
|
abstract class Builder<B extends Builder<B, P>, P extends Protocol> {
|
||||||
|
|
||||||
|
public abstract B name(String value);
|
||||||
|
|
||||||
|
public abstract B port(int port);
|
||||||
|
|
||||||
|
public abstract B handlerProviders(ImmutableList<Provider<? extends ChannelHandler>> value);
|
||||||
|
|
||||||
|
public abstract P build();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Connection parameters for a connection from the client to the proxy.
|
||||||
|
*
|
||||||
|
* <p>This protocol is associated to a {@link NioSocketChannel} established by remote peer
|
||||||
|
* connecting to the given {@code port} that the proxy is listening on.
|
||||||
|
*/
|
||||||
|
@AutoValue
|
||||||
|
abstract class FrontendProtocol implements Protocol {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The {@link BackendProtocol} used to establish a relay channel and relay the traffic to. Not
|
||||||
|
* required for health check protocol.
|
||||||
|
*/
|
||||||
|
@Nullable
|
||||||
|
public abstract BackendProtocol relayProtocol();
|
||||||
|
|
||||||
|
public abstract boolean isHealthCheck();
|
||||||
|
|
||||||
|
@AutoValue.Builder
|
||||||
|
public abstract static class Builder extends Protocol.Builder<Builder, FrontendProtocol> {
|
||||||
|
public abstract Builder relayProtocol(BackendProtocol value);
|
||||||
|
|
||||||
|
public abstract Builder isHealthCheck(boolean value);
|
||||||
|
|
||||||
|
abstract FrontendProtocol autoBuild();
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public FrontendProtocol build() {
|
||||||
|
FrontendProtocol frontendProtocol = autoBuild();
|
||||||
|
Preconditions.checkState(
|
||||||
|
frontendProtocol.isHealthCheck() || frontendProtocol.relayProtocol() != null,
|
||||||
|
"Frontend protocol %s must define a relay protocol.",
|
||||||
|
frontendProtocol.name());
|
||||||
|
return frontendProtocol;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Connection parameters for a connection from the proxy to the GAE app.
|
||||||
|
*
|
||||||
|
* <p>This protocol is associated to a {@link NioSocketChannel} established by the proxy
|
||||||
|
* connecting to a remote peer.
|
||||||
|
*/
|
||||||
|
@AutoValue
|
||||||
|
abstract class BackendProtocol implements Protocol {
|
||||||
|
/** The hostname that the proxy connects to. */
|
||||||
|
public abstract String host();
|
||||||
|
|
||||||
|
/** Builder of {@link BackendProtocol}. */
|
||||||
|
@AutoValue.Builder
|
||||||
|
public abstract static class Builder extends Protocol.Builder<Builder, BackendProtocol> {
|
||||||
|
public abstract Builder host(String value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
102
java/google/registry/proxy/ProxyConfig.java
Normal file
102
java/google/registry/proxy/ProxyConfig.java
Normal file
|
@ -0,0 +1,102 @@
|
||||||
|
// Copyright 2017 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.proxy;
|
||||||
|
|
||||||
|
import static google.registry.config.YamlUtils.getConfigSettings;
|
||||||
|
import static google.registry.util.ResourceUtils.readResourceUtf8;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/** The POJO that YAML config files are deserialized into. */
|
||||||
|
public class ProxyConfig {
|
||||||
|
|
||||||
|
enum Environment {
|
||||||
|
PRODUCTION,
|
||||||
|
SANDBOX,
|
||||||
|
ALPHA,
|
||||||
|
LOCAL,
|
||||||
|
TEST,
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final String DEFAULT_CONFIG = "config/default-config.yaml";
|
||||||
|
private static final String CUSTOM_CONFIG_FORMATTER = "config/proxy-config-%s.yaml";
|
||||||
|
|
||||||
|
public String projectId;
|
||||||
|
public List<String> gcpScopes;
|
||||||
|
public int accessTokenValidPeriodSeconds;
|
||||||
|
public int accessTokenRefreshBeforeExpirySeconds;
|
||||||
|
public String sslPemFilename;
|
||||||
|
public Kms kms;
|
||||||
|
public Epp epp;
|
||||||
|
public Whois whois;
|
||||||
|
public HealthCheck healthCheck;
|
||||||
|
public HttpsRelay httpsRelay;
|
||||||
|
public Metrics metrics;
|
||||||
|
|
||||||
|
/** Configuration options that apply to Cloud KMS. */
|
||||||
|
public static class Kms {
|
||||||
|
public String location;
|
||||||
|
public String keyRing;
|
||||||
|
public String cryptoKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Configuration options that apply to EPP protocol. */
|
||||||
|
public static class Epp {
|
||||||
|
public int port;
|
||||||
|
public String relayHost;
|
||||||
|
public String relayPath;
|
||||||
|
public int maxMessageLengthBytes;
|
||||||
|
public int headerLengthBytes;
|
||||||
|
public int readTimeoutSeconds;
|
||||||
|
public String serverHostname;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Configuration options that apply to WHOIS protocol. */
|
||||||
|
public static class Whois {
|
||||||
|
public int port;
|
||||||
|
public String relayHost;
|
||||||
|
public String relayPath;
|
||||||
|
public int maxMessageLengthBytes;
|
||||||
|
public int readTimeoutSeconds;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Configuration options that apply to GCP load balancer health check protocol. */
|
||||||
|
public static class HealthCheck {
|
||||||
|
public int port;
|
||||||
|
public String checkRequest;
|
||||||
|
public String checkResponse;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Configuration options that apply to HTTPS relay protocol. */
|
||||||
|
public static class HttpsRelay {
|
||||||
|
public int port;
|
||||||
|
public int maxMessageLengthBytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Configuration options that apply to Stackdriver monitoring metrics. */
|
||||||
|
public static class Metrics {
|
||||||
|
public int stackdriverMaxQps;
|
||||||
|
public int stackdriverMaxPointsPerRequest;
|
||||||
|
public int writeIntervalSeconds;
|
||||||
|
}
|
||||||
|
|
||||||
|
static ProxyConfig getProxyConfig(Environment env) {
|
||||||
|
String defaultYaml = readResourceUtf8(ProxyConfig.class, DEFAULT_CONFIG);
|
||||||
|
String customYaml =
|
||||||
|
readResourceUtf8(
|
||||||
|
ProxyConfig.class, String.format(CUSTOM_CONFIG_FORMATTER, env.name().toLowerCase()));
|
||||||
|
return getConfigSettings(defaultYaml, customYaml, ProxyConfig.class);
|
||||||
|
}
|
||||||
|
}
|
304
java/google/registry/proxy/ProxyModule.java
Normal file
304
java/google/registry/proxy/ProxyModule.java
Normal file
|
@ -0,0 +1,304 @@
|
||||||
|
// Copyright 2017 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.proxy;
|
||||||
|
|
||||||
|
import static com.google.common.base.Suppliers.memoizeWithExpiration;
|
||||||
|
import static google.registry.proxy.ProxyConfig.getProxyConfig;
|
||||||
|
import static google.registry.util.ResourceUtils.readResourceBytes;
|
||||||
|
import static java.util.concurrent.TimeUnit.SECONDS;
|
||||||
|
|
||||||
|
import com.beust.jcommander.JCommander;
|
||||||
|
import com.beust.jcommander.Parameter;
|
||||||
|
import com.beust.jcommander.ParameterException;
|
||||||
|
import com.google.api.client.googleapis.auth.oauth2.GoogleCredential;
|
||||||
|
import com.google.api.client.googleapis.util.Utils;
|
||||||
|
import com.google.api.services.cloudkms.v1.CloudKMS;
|
||||||
|
import com.google.api.services.cloudkms.v1.model.DecryptRequest;
|
||||||
|
import com.google.common.base.Optional;
|
||||||
|
import com.google.common.base.Supplier;
|
||||||
|
import com.google.common.collect.ImmutableList;
|
||||||
|
import com.google.common.collect.ImmutableMap;
|
||||||
|
import com.google.common.collect.Maps;
|
||||||
|
import dagger.Component;
|
||||||
|
import dagger.Module;
|
||||||
|
import dagger.Provides;
|
||||||
|
import google.registry.proxy.EppProtocolModule.EppProtocol;
|
||||||
|
import google.registry.proxy.HealthCheckProtocolModule.HealthCheckProtocol;
|
||||||
|
import google.registry.proxy.Protocol.FrontendProtocol;
|
||||||
|
import google.registry.proxy.ProxyConfig.Environment;
|
||||||
|
import google.registry.proxy.WhoisProtocolModule.WhoisProtocol;
|
||||||
|
import google.registry.util.Clock;
|
||||||
|
import google.registry.util.FormattingLogger;
|
||||||
|
import google.registry.util.SystemClock;
|
||||||
|
import io.netty.handler.logging.LoggingHandler;
|
||||||
|
import io.netty.handler.ssl.OpenSsl;
|
||||||
|
import io.netty.handler.ssl.SslProvider;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.logging.ConsoleHandler;
|
||||||
|
import java.util.logging.Handler;
|
||||||
|
import java.util.logging.Level;
|
||||||
|
import java.util.logging.Logger;
|
||||||
|
import javax.inject.Named;
|
||||||
|
import javax.inject.Singleton;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A module that provides the port-to-protocol map and other configs that are used to bootstrap the
|
||||||
|
* server.
|
||||||
|
*/
|
||||||
|
@Module
|
||||||
|
public class ProxyModule {
|
||||||
|
|
||||||
|
private static final FormattingLogger logger = FormattingLogger.getLoggerForCallerClass();
|
||||||
|
|
||||||
|
@Parameter(names = "--whois", description = "Port for WHOIS")
|
||||||
|
private Integer whoisPort;
|
||||||
|
|
||||||
|
@Parameter(names = "--epp", description = "Port for EPP")
|
||||||
|
private Integer eppPort;
|
||||||
|
|
||||||
|
@Parameter(names = "--health_check", description = "Port for health check protocol")
|
||||||
|
private Integer healthCheckPort;
|
||||||
|
|
||||||
|
@Parameter(names = "--env", description = "Environment to run the proxy in")
|
||||||
|
private Environment env = Environment.LOCAL;
|
||||||
|
|
||||||
|
@Parameter(names = "--log", description = "Whether to log activities for debugging")
|
||||||
|
boolean log;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enable FINE level logging.
|
||||||
|
*
|
||||||
|
* <p>Set the loggers log level to {@code FINE}, and also add a console handler that actually
|
||||||
|
* output {@code FINE} level messages to stdout. The handler filters out all non FINE level log
|
||||||
|
* record to avoid double logging. Log records at level higher than FINE (e. g. INFO) will be
|
||||||
|
* handled at parent loggers. Note that {@code FINE} level corresponds to {@code DEBUG} level in
|
||||||
|
* Netty.
|
||||||
|
*/
|
||||||
|
private static void enableDebugLevelLogging() {
|
||||||
|
ImmutableList<Logger> parentLoggers =
|
||||||
|
ImmutableList.of(
|
||||||
|
Logger.getLogger("io.netty.handler.logging.LoggingHandler"),
|
||||||
|
// Parent of all FormattingLoggers, so that we do not have to configure each
|
||||||
|
// FormattingLogger individually.
|
||||||
|
Logger.getLogger("google.registry.proxy"));
|
||||||
|
for (Logger logger : parentLoggers) {
|
||||||
|
logger.setLevel(Level.FINE);
|
||||||
|
Handler handler = new ConsoleHandler();
|
||||||
|
handler.setFilter(record -> Objects.equals(record.getLevel(), Level.FINE));
|
||||||
|
handler.setLevel(Level.FINE);
|
||||||
|
logger.addHandler(handler);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses command line arguments. Show usage if wrong arguments are given.
|
||||||
|
*
|
||||||
|
* @param args list of {@code String} arguments
|
||||||
|
* @return this {@code ProxyModule} object
|
||||||
|
*/
|
||||||
|
ProxyModule parse(String[] args) {
|
||||||
|
JCommander jCommander = new JCommander(this);
|
||||||
|
jCommander.setProgramName("proxy_server");
|
||||||
|
try {
|
||||||
|
jCommander.parse(args);
|
||||||
|
} catch (ParameterException e) {
|
||||||
|
jCommander.usage();
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
if (log) {
|
||||||
|
logger.info("DEBUG LOGGING: ENABLED");
|
||||||
|
enableDebugLevelLogging();
|
||||||
|
}
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
@WhoisProtocol
|
||||||
|
int provideWhoisPort(ProxyConfig config) {
|
||||||
|
return Optional.fromNullable(whoisPort).or(config.whois.port);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
@EppProtocol
|
||||||
|
int provideEppPort(ProxyConfig config) {
|
||||||
|
return Optional.fromNullable(eppPort).or(config.epp.port);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
@HealthCheckProtocol
|
||||||
|
int provideHealthCheckPort(ProxyConfig config) {
|
||||||
|
return Optional.fromNullable(healthCheckPort).or(config.healthCheck.port);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
ImmutableMap<Integer, FrontendProtocol> providePortToProtocolMap(
|
||||||
|
Set<FrontendProtocol> protocolSet) {
|
||||||
|
return Maps.uniqueIndex(protocolSet, Protocol::port);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
Environment provideEnvironment() {
|
||||||
|
return env;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Shared logging handler, set to default DEBUG(FINE) level. */
|
||||||
|
@Singleton
|
||||||
|
@Provides
|
||||||
|
static LoggingHandler provideLoggingHandler() {
|
||||||
|
return new LoggingHandler();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Singleton
|
||||||
|
@Provides
|
||||||
|
static GoogleCredential provideCredential(ProxyConfig config) {
|
||||||
|
try {
|
||||||
|
GoogleCredential credential = GoogleCredential.getApplicationDefault();
|
||||||
|
if (credential.createScopedRequired()) {
|
||||||
|
credential = credential.createScoped(config.gcpScopes);
|
||||||
|
}
|
||||||
|
return credential;
|
||||||
|
} catch (IOException e) {
|
||||||
|
logger.severe(e, "Unable to obtain OAuth2 credential.");
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Access token supplier that auto refreshes 1 minute before expiry. */
|
||||||
|
@Singleton
|
||||||
|
@Provides
|
||||||
|
@Named("accessToken")
|
||||||
|
static Supplier<String> provideAccessTokenSupplier(
|
||||||
|
GoogleCredential credential, ProxyConfig config) {
|
||||||
|
return memoizeWithExpiration(
|
||||||
|
() -> {
|
||||||
|
try {
|
||||||
|
credential.refreshToken();
|
||||||
|
} catch (IOException e) {
|
||||||
|
logger.severe(e, "Cannot refresh access token.");
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
return credential.getAccessToken();
|
||||||
|
},
|
||||||
|
config.accessTokenValidPeriodSeconds - config.accessTokenRefreshBeforeExpirySeconds,
|
||||||
|
SECONDS);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Singleton
|
||||||
|
@Provides
|
||||||
|
static CloudKMS provideCloudKms(GoogleCredential credential, ProxyConfig config) {
|
||||||
|
return new CloudKMS.Builder(
|
||||||
|
Utils.getDefaultTransport(), Utils.getDefaultJsonFactory(), credential)
|
||||||
|
.setApplicationName(config.projectId)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Singleton
|
||||||
|
@Provides
|
||||||
|
@Named("encryptedPemBytes")
|
||||||
|
static byte[] provideEncryptedPemBytes(ProxyConfig config) {
|
||||||
|
try {
|
||||||
|
return readResourceBytes(ProxyModule.class, "resources/" + config.sslPemFilename).read();
|
||||||
|
} catch (IOException e) {
|
||||||
|
logger.severefmt(e, "Error reading encrypted PEM file: %s", config.sslPemFilename);
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Singleton
|
||||||
|
@Provides
|
||||||
|
static PemBytes providePemBytes(
|
||||||
|
CloudKMS cloudKms, @Named("encryptedPemBytes") byte[] encryptedPemBytes, ProxyConfig config) {
|
||||||
|
String cryptoKeyUrl =
|
||||||
|
String.format(
|
||||||
|
"projects/%s/locations/%s/keyRings/%s/cryptoKeys/%s",
|
||||||
|
config.projectId, config.kms.location, config.kms.keyRing, config.kms.cryptoKey);
|
||||||
|
try {
|
||||||
|
DecryptRequest decryptRequest = new DecryptRequest().encodeCiphertext(encryptedPemBytes);
|
||||||
|
return PemBytes.create(
|
||||||
|
cloudKms
|
||||||
|
.projects()
|
||||||
|
.locations()
|
||||||
|
.keyRings()
|
||||||
|
.cryptoKeys()
|
||||||
|
.decrypt(cryptoKeyUrl, decryptRequest)
|
||||||
|
.execute()
|
||||||
|
.decodePlaintext());
|
||||||
|
} catch (IOException e) {
|
||||||
|
logger.severefmt(e, "PEM file decryption failed using CryptoKey: %s", cryptoKeyUrl);
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
static SslProvider provideSslProvider() {
|
||||||
|
// Prefer OpenSSL.
|
||||||
|
return OpenSsl.isAvailable() ? SslProvider.OPENSSL : SslProvider.JDK;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
@Singleton
|
||||||
|
static Clock provideClock() {
|
||||||
|
return new SystemClock();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Singleton
|
||||||
|
@Provides
|
||||||
|
ProxyConfig provideProxyConfig(Environment env) {
|
||||||
|
return getProxyConfig(env);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A wrapper class for decrypted bytes of the PEM file.
|
||||||
|
*
|
||||||
|
* <p>Note that this should not be an @AutoValue class because we need a clone of the bytes to be
|
||||||
|
* returned, otherwise the wrapper class becomes mutable.
|
||||||
|
*/
|
||||||
|
// TODO: remove this class once FOSS build can use @BindsInstance to bind a byte[]
|
||||||
|
// (https://github.com/bazelbuild/bazel/issues/4138)
|
||||||
|
static class PemBytes {
|
||||||
|
|
||||||
|
private final byte[] bytes;
|
||||||
|
|
||||||
|
static final PemBytes create(byte[] bytes) {
|
||||||
|
return new PemBytes(bytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
private PemBytes(byte[] bytes) {
|
||||||
|
this.bytes = bytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
byte[] getBytes() {
|
||||||
|
return bytes.clone();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Root level component that exposes the port-to-protocol map. */
|
||||||
|
@Singleton
|
||||||
|
@Component(
|
||||||
|
modules = {
|
||||||
|
ProxyModule.class,
|
||||||
|
CertificateModule.class,
|
||||||
|
HttpsRelayProtocolModule.class,
|
||||||
|
WhoisProtocolModule.class,
|
||||||
|
EppProtocolModule.class,
|
||||||
|
HealthCheckProtocolModule.class
|
||||||
|
}
|
||||||
|
)
|
||||||
|
interface ProxyComponent {
|
||||||
|
ImmutableMap<Integer, FrontendProtocol> portToProtocolMap();
|
||||||
|
}
|
||||||
|
}
|
239
java/google/registry/proxy/ProxyServer.java
Normal file
239
java/google/registry/proxy/ProxyServer.java
Normal file
|
@ -0,0 +1,239 @@
|
||||||
|
// Copyright 2017 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.proxy;
|
||||||
|
|
||||||
|
import static google.registry.proxy.Protocol.PROTOCOL_KEY;
|
||||||
|
import static google.registry.proxy.handler.RelayHandler.RELAY_CHANNEL_KEY;
|
||||||
|
|
||||||
|
import com.google.common.collect.ImmutableList;
|
||||||
|
import com.google.common.collect.ImmutableMap;
|
||||||
|
import google.registry.monitoring.metrics.MetricReporter;
|
||||||
|
import google.registry.proxy.Protocol.BackendProtocol;
|
||||||
|
import google.registry.proxy.Protocol.FrontendProtocol;
|
||||||
|
import google.registry.proxy.ProxyModule.ProxyComponent;
|
||||||
|
import google.registry.util.FormattingLogger;
|
||||||
|
import io.netty.bootstrap.Bootstrap;
|
||||||
|
import io.netty.bootstrap.ServerBootstrap;
|
||||||
|
import io.netty.channel.Channel;
|
||||||
|
import io.netty.channel.ChannelFuture;
|
||||||
|
import io.netty.channel.ChannelHandler;
|
||||||
|
import io.netty.channel.ChannelInitializer;
|
||||||
|
import io.netty.channel.ChannelOption;
|
||||||
|
import io.netty.channel.ChannelPipeline;
|
||||||
|
import io.netty.channel.EventLoopGroup;
|
||||||
|
import io.netty.channel.nio.NioEventLoopGroup;
|
||||||
|
import io.netty.channel.socket.nio.NioServerSocketChannel;
|
||||||
|
import io.netty.channel.socket.nio.NioSocketChannel;
|
||||||
|
import io.netty.util.concurrent.Future;
|
||||||
|
import io.netty.util.internal.logging.InternalLoggerFactory;
|
||||||
|
import io.netty.util.internal.logging.JdkLoggerFactory;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import java.util.concurrent.TimeoutException;
|
||||||
|
import javax.inject.Provider;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A multi-protocol proxy server that listens on port(s) specified in {@link
|
||||||
|
* ProxyModule.ProxyComponent#portToProtocolMap()} }.
|
||||||
|
*/
|
||||||
|
public class ProxyServer implements Runnable {
|
||||||
|
|
||||||
|
private static final FormattingLogger logger = FormattingLogger.getLoggerForCallerClass();
|
||||||
|
private static final MetricReporter metricReporter =
|
||||||
|
DaggerMetricsModule_MetricsComponent.create().metricReporter();
|
||||||
|
/** Maximum length of the queue of incoming connections. */
|
||||||
|
private static final int MAX_SOCKET_BACKLOG = 128;
|
||||||
|
|
||||||
|
private final ImmutableMap<Integer, FrontendProtocol> portToProtocolMap;
|
||||||
|
private final HashMap<Integer, Channel> portToChannelMap = new HashMap<>();
|
||||||
|
private final EventLoopGroup eventGroup = new NioEventLoopGroup();
|
||||||
|
|
||||||
|
ProxyServer(ProxyComponent proxyComponent) {
|
||||||
|
this.portToProtocolMap = proxyComponent.portToProtocolMap();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A {@link ChannelInitializer} for connections from a client of a certain protocol.
|
||||||
|
*
|
||||||
|
* <p>The {@link #initChannel} method does the following:
|
||||||
|
*
|
||||||
|
* <ol>
|
||||||
|
* <li>Determine the {@link FrontendProtocol} of the inbound {@link Channel} from its parent
|
||||||
|
* {@link Channel}, i. e. the {@link Channel} that binds to local port and listens.
|
||||||
|
* <li>Add handlers for the {@link FrontendProtocol} to the inbound {@link Channel}.
|
||||||
|
* <li>Establish an outbound {@link Channel} that serves as the relay channel of the inbound
|
||||||
|
* {@link Channel}, as specified by {@link FrontendProtocol#relayProtocol}.
|
||||||
|
* <li>After the outbound {@link Channel} connects successfully, enable {@link
|
||||||
|
* ChannelOption#AUTO_READ} on the inbound {@link Channel} to start reading.
|
||||||
|
* </ol>
|
||||||
|
*/
|
||||||
|
private static class ServerChannelInitializer extends ChannelInitializer<NioSocketChannel> {
|
||||||
|
@Override
|
||||||
|
protected void initChannel(NioSocketChannel inboundChannel) throws Exception {
|
||||||
|
// Add inbound channel handlers.
|
||||||
|
FrontendProtocol inboundProtocol =
|
||||||
|
(FrontendProtocol) inboundChannel.parent().attr(PROTOCOL_KEY).get();
|
||||||
|
inboundChannel.attr(PROTOCOL_KEY).set(inboundProtocol);
|
||||||
|
addHandlers(inboundChannel.pipeline(), inboundProtocol.handlerProviders());
|
||||||
|
|
||||||
|
if (inboundProtocol.isHealthCheck()) {
|
||||||
|
// A health check server protocol has no relay channel. It simply replies to incoming
|
||||||
|
// request with a preset response.
|
||||||
|
inboundChannel.config().setAutoRead(true);
|
||||||
|
} else {
|
||||||
|
// Connect to the relay (outbound) channel specified by the BackendProtocol.
|
||||||
|
BackendProtocol outboundProtocol = inboundProtocol.relayProtocol();
|
||||||
|
Bootstrap bootstrap =
|
||||||
|
new Bootstrap()
|
||||||
|
// Use the same thread to connect to the relay channel, therefore avoiding
|
||||||
|
// synchronization handling due to interactions between the two channels
|
||||||
|
.group(inboundChannel.eventLoop())
|
||||||
|
.channel(NioSocketChannel.class)
|
||||||
|
.handler(
|
||||||
|
new ChannelInitializer<NioSocketChannel>() {
|
||||||
|
@Override
|
||||||
|
protected void initChannel(NioSocketChannel outboundChannel)
|
||||||
|
throws Exception {
|
||||||
|
addHandlers(
|
||||||
|
outboundChannel.pipeline(), outboundProtocol.handlerProviders());
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.option(ChannelOption.SO_KEEPALIVE, true)
|
||||||
|
// Outbound channel relays to inbound channel.
|
||||||
|
.attr(RELAY_CHANNEL_KEY, inboundChannel)
|
||||||
|
.attr(PROTOCOL_KEY, outboundProtocol);
|
||||||
|
ChannelFuture outboundChannelFuture =
|
||||||
|
bootstrap.connect(outboundProtocol.host(), outboundProtocol.port());
|
||||||
|
outboundChannelFuture.addListener(
|
||||||
|
(ChannelFuture future) -> {
|
||||||
|
if (future.isSuccess()) {
|
||||||
|
Channel outboundChannel = future.channel();
|
||||||
|
// Inbound channel relays to outbound channel.
|
||||||
|
inboundChannel.attr(RELAY_CHANNEL_KEY).set(outboundChannel);
|
||||||
|
// Outbound channel established successfully, inbound channel can start reading.
|
||||||
|
// This setter also calls channel.read() to request read operation.
|
||||||
|
inboundChannel.config().setAutoRead(true);
|
||||||
|
logger.infofmt(
|
||||||
|
"Relay established: %s <-> %s\nSERVER: %s\nCLIENT: %s",
|
||||||
|
inboundProtocol.name(),
|
||||||
|
outboundProtocol.name(),
|
||||||
|
inboundChannel,
|
||||||
|
outboundChannel);
|
||||||
|
} else {
|
||||||
|
logger.severefmt(
|
||||||
|
future.cause(),
|
||||||
|
"Cannot connect to relay channel for %s protocol connection from %s.",
|
||||||
|
inboundProtocol.name(),
|
||||||
|
inboundChannel.remoteAddress().getHostName());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void addHandlers(
|
||||||
|
ChannelPipeline channelPipeline,
|
||||||
|
ImmutableList<Provider<? extends ChannelHandler>> handlerProviders) {
|
||||||
|
for (Provider<? extends ChannelHandler> handlerProvider : handlerProviders) {
|
||||||
|
channelPipeline.addLast(handlerProvider.get());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
try {
|
||||||
|
ServerBootstrap serverBootstrap =
|
||||||
|
new ServerBootstrap()
|
||||||
|
.group(eventGroup)
|
||||||
|
.channel(NioServerSocketChannel.class)
|
||||||
|
.childHandler(new ServerChannelInitializer())
|
||||||
|
.option(ChannelOption.SO_BACKLOG, MAX_SOCKET_BACKLOG)
|
||||||
|
.childOption(ChannelOption.SO_KEEPALIVE, true)
|
||||||
|
// Do not read before relay channel is established.
|
||||||
|
.childOption(ChannelOption.AUTO_READ, false);
|
||||||
|
|
||||||
|
// Bind to each port specified in portToHandlersMap.
|
||||||
|
portToProtocolMap.forEach(
|
||||||
|
(port, protocol) -> {
|
||||||
|
try {
|
||||||
|
// Wait for binding to be established for each listening port.
|
||||||
|
ChannelFuture serverChannelFuture = serverBootstrap.bind(port).sync();
|
||||||
|
if (serverChannelFuture.isSuccess()) {
|
||||||
|
logger.infofmt(
|
||||||
|
"Start listening on port %s for %s protocol.", port, protocol.name());
|
||||||
|
Channel serverChannel = serverChannelFuture.channel();
|
||||||
|
serverChannel.attr(PROTOCOL_KEY).set(protocol);
|
||||||
|
portToChannelMap.put(port, serverChannel);
|
||||||
|
}
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
logger.severefmt(
|
||||||
|
e, "Cannot listen on port %s for %s protocol.", port, protocol.name());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Wait for all listening ports to close.
|
||||||
|
portToChannelMap.forEach(
|
||||||
|
(port, channel) -> {
|
||||||
|
try {
|
||||||
|
// Block until all server channels are closed.
|
||||||
|
ChannelFuture unusedFuture = channel.closeFuture().sync();
|
||||||
|
logger.infofmt(
|
||||||
|
"Stop listening on port %s for %s protocol.",
|
||||||
|
port, channel.attr(PROTOCOL_KEY).get().name());
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
logger.severefmt(
|
||||||
|
e,
|
||||||
|
"Listening on port %s for %s protocol interrupted.",
|
||||||
|
port,
|
||||||
|
channel.attr(PROTOCOL_KEY).get().name());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
logger.info("Shutting down server...");
|
||||||
|
Future<?> unusedFuture = eventGroup.shutdownGracefully();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void main(String[] args) throws Exception {
|
||||||
|
// Use JDK logger for Netty's LoggingHandler,
|
||||||
|
// which is what google.registry.util.FormattingLog uses under the hood.
|
||||||
|
InternalLoggerFactory.setDefaultFactory(JdkLoggerFactory.INSTANCE);
|
||||||
|
|
||||||
|
try {
|
||||||
|
metricReporter.startAsync().awaitRunning(10, TimeUnit.SECONDS);
|
||||||
|
logger.info("Started up MetricReporter");
|
||||||
|
} catch (TimeoutException timeoutException) {
|
||||||
|
logger.severefmt("Failed to initialize MetricReporter: %s", timeoutException);
|
||||||
|
}
|
||||||
|
|
||||||
|
Runtime.getRuntime()
|
||||||
|
.addShutdownHook(
|
||||||
|
new Thread(
|
||||||
|
() -> {
|
||||||
|
try {
|
||||||
|
metricReporter.stopAsync().awaitTerminated(10, TimeUnit.SECONDS);
|
||||||
|
logger.info("Shut down MetricReporter");
|
||||||
|
} catch (TimeoutException timeoutException) {
|
||||||
|
logger.severefmt("Failed to stop MetricReporter: %s", timeoutException);
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
ProxyComponent proxyComponent =
|
||||||
|
DaggerProxyModule_ProxyComponent.builder()
|
||||||
|
.proxyModule(new ProxyModule().parse(args))
|
||||||
|
.build();
|
||||||
|
new ProxyServer(proxyComponent).run();
|
||||||
|
}
|
||||||
|
}
|
98
java/google/registry/proxy/WhoisProtocolModule.java
Normal file
98
java/google/registry/proxy/WhoisProtocolModule.java
Normal file
|
@ -0,0 +1,98 @@
|
||||||
|
// Copyright 2017 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.proxy;
|
||||||
|
|
||||||
|
import com.google.common.base.Supplier;
|
||||||
|
import com.google.common.collect.ImmutableList;
|
||||||
|
import dagger.Module;
|
||||||
|
import dagger.Provides;
|
||||||
|
import dagger.multibindings.IntoSet;
|
||||||
|
import google.registry.proxy.HttpsRelayProtocolModule.HttpsRelayProtocol;
|
||||||
|
import google.registry.proxy.Protocol.BackendProtocol;
|
||||||
|
import google.registry.proxy.Protocol.FrontendProtocol;
|
||||||
|
import google.registry.proxy.handler.RelayHandler.FullHttpRequestRelayHandler;
|
||||||
|
import google.registry.proxy.handler.WhoisServiceHandler;
|
||||||
|
import google.registry.proxy.metric.FrontendMetrics;
|
||||||
|
import io.netty.channel.ChannelHandler;
|
||||||
|
import io.netty.handler.codec.LineBasedFrameDecoder;
|
||||||
|
import io.netty.handler.logging.LoggingHandler;
|
||||||
|
import io.netty.handler.timeout.ReadTimeoutHandler;
|
||||||
|
import javax.inject.Named;
|
||||||
|
import javax.inject.Provider;
|
||||||
|
import javax.inject.Qualifier;
|
||||||
|
import javax.inject.Singleton;
|
||||||
|
|
||||||
|
/** A module that provides the {@link FrontendProtocol} used for whois protocol. */
|
||||||
|
@Module
|
||||||
|
class WhoisProtocolModule {
|
||||||
|
|
||||||
|
/** Dagger qualifier to provide whois protocol related handlers and other bindings. */
|
||||||
|
@Qualifier
|
||||||
|
@interface WhoisProtocol {};
|
||||||
|
|
||||||
|
private static final String PROTOCOL_NAME = "whois";
|
||||||
|
|
||||||
|
@Singleton
|
||||||
|
@Provides
|
||||||
|
@IntoSet
|
||||||
|
static FrontendProtocol provideProtocol(
|
||||||
|
ProxyConfig config,
|
||||||
|
@WhoisProtocol int whoisPort,
|
||||||
|
@WhoisProtocol ImmutableList<Provider<? extends ChannelHandler>> handlerProviders,
|
||||||
|
@HttpsRelayProtocol BackendProtocol.Builder backendProtocolBuilder) {
|
||||||
|
return Protocol.frontendBuilder()
|
||||||
|
.name(PROTOCOL_NAME)
|
||||||
|
.port(whoisPort)
|
||||||
|
.handlerProviders(handlerProviders)
|
||||||
|
.relayProtocol(backendProtocolBuilder.host(config.whois.relayHost).build())
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
@WhoisProtocol
|
||||||
|
static ImmutableList<Provider<? extends ChannelHandler>> provideHandlerProviders(
|
||||||
|
@WhoisProtocol Provider<ReadTimeoutHandler> readTimeoutHandlerProvider,
|
||||||
|
Provider<LineBasedFrameDecoder> lineBasedFrameDecoderProvider,
|
||||||
|
Provider<WhoisServiceHandler> whoisServiceHandlerProvider,
|
||||||
|
Provider<LoggingHandler> loggingHandlerProvider,
|
||||||
|
Provider<FullHttpRequestRelayHandler> relayHandlerProvider) {
|
||||||
|
return ImmutableList.of(
|
||||||
|
readTimeoutHandlerProvider,
|
||||||
|
lineBasedFrameDecoderProvider,
|
||||||
|
whoisServiceHandlerProvider,
|
||||||
|
loggingHandlerProvider,
|
||||||
|
relayHandlerProvider);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
static WhoisServiceHandler provideWhoisServiceHandler(
|
||||||
|
ProxyConfig config,
|
||||||
|
@Named("accessToken") Supplier<String> accessTokenSupplier,
|
||||||
|
FrontendMetrics metrics) {
|
||||||
|
return new WhoisServiceHandler(
|
||||||
|
config.whois.relayHost, config.whois.relayPath, accessTokenSupplier, metrics);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
static LineBasedFrameDecoder provideLineBasedFrameDecoder(ProxyConfig config) {
|
||||||
|
return new LineBasedFrameDecoder(config.whois.maxMessageLengthBytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
@WhoisProtocol
|
||||||
|
static ReadTimeoutHandler provideReadTimeoutHandler(ProxyConfig config) {
|
||||||
|
return new ReadTimeoutHandler(config.whois.readTimeoutSeconds);
|
||||||
|
}
|
||||||
|
}
|
128
java/google/registry/proxy/config/default-config.yaml
Normal file
128
java/google/registry/proxy/config/default-config.yaml
Normal file
|
@ -0,0 +1,128 @@
|
||||||
|
# This is the default configuration file for the proxy. Do not make changes to
|
||||||
|
# it unless you are writing new features that requires you to. To customize an
|
||||||
|
# individual deployment or environment, create a proxy-config.yaml file in the
|
||||||
|
# same directory overriding only the values you wish to change. You may need
|
||||||
|
# to override some of these values to configure and enable some services used in
|
||||||
|
# production environments.
|
||||||
|
|
||||||
|
# GCP project ID
|
||||||
|
projectId: your-gcp-project-id
|
||||||
|
|
||||||
|
# OAuth scope that the GoogleCredential will be constructed with. This list
|
||||||
|
# should include all service scopes that the proxy depends on.
|
||||||
|
gcpScopes:
|
||||||
|
# The default OAuth scope granted to GCE instances. Local development instance
|
||||||
|
# needs this scope to mimic running on GCE. Currently it is used to access
|
||||||
|
# Cloud KMS and Stackdriver Monitoring APIs.
|
||||||
|
- https://www.googleapis.com/auth/cloud-platform
|
||||||
|
|
||||||
|
# The OAuth scope required to be included in the access token for the GAE app
|
||||||
|
# to authenticate.
|
||||||
|
- https://www.googleapis.com/auth/userinfo.email
|
||||||
|
|
||||||
|
# Access token is valid for 60 minutes.
|
||||||
|
#
|
||||||
|
# See also: Data store
|
||||||
|
# (https://developers.google.com/api-client-library/java/google-api-java-client/oauth2#data_store).
|
||||||
|
accessTokenValidPeriodSeconds: 3600
|
||||||
|
|
||||||
|
# Access token is refreshed 1 minutes before expiry.
|
||||||
|
#
|
||||||
|
# This is the default refresh time used by
|
||||||
|
# com.google.api.client.auth.oauth2.Credential#intercept.
|
||||||
|
accessTokenRefreshBeforeExpirySeconds: 60
|
||||||
|
|
||||||
|
# Name of the encrypted PEM file.
|
||||||
|
sslPemFilename: your-ssl.pem
|
||||||
|
|
||||||
|
# Strings used to construct the KMS crypto key URL.
|
||||||
|
# See: https://cloud.google.com/kms/docs/reference/rest/v1/projects.locations.keyRings.cryptoKeys
|
||||||
|
kms:
|
||||||
|
# Location where your key ring is stored (global, us-east1, etc).
|
||||||
|
location: your-kms-location
|
||||||
|
|
||||||
|
# Name of the KeyRing that contains the CryptoKey file.
|
||||||
|
keyRing: your-kms-keyRing
|
||||||
|
|
||||||
|
# Name of the CryptoKey used to encrypt the PEM file.
|
||||||
|
cryptoKey: your-kms-cryptoKey
|
||||||
|
|
||||||
|
epp:
|
||||||
|
port: 700
|
||||||
|
relayHost: registry-project-id.appspot.com
|
||||||
|
relayPath: /_dr/epp
|
||||||
|
|
||||||
|
# Maximum input message length in bytes.
|
||||||
|
#
|
||||||
|
# The first 4 bytes in a message is the total length of message, in bytes.
|
||||||
|
#
|
||||||
|
# We accept a message up to 1 GB, which should be plentiful, if not over the
|
||||||
|
# top. In fact we should probably limit this to a more reasonable number, as a
|
||||||
|
# 1 GB message will likely cause the proxy to go out of memory.
|
||||||
|
#
|
||||||
|
# See also: RFC 5734 4 Data Unit Format
|
||||||
|
# (https://tools.ietf.org/html/rfc5734#section-4).
|
||||||
|
maxMessageLengthBytes: 1073741824
|
||||||
|
|
||||||
|
# Length of the header field in bytes.
|
||||||
|
#
|
||||||
|
# Note that value of the header field is the total length (in bytes) of the
|
||||||
|
# message, including the header itself, the length of the epp xml instance is
|
||||||
|
# therefore 4 bytes shorter than this value.
|
||||||
|
headerLengthBytes: 4
|
||||||
|
|
||||||
|
# Time after which an idle connection will be closed.
|
||||||
|
#
|
||||||
|
# The RFC gives registry discretionary power to set a timeout period. 1 hr
|
||||||
|
# should be reasonable enough for any registrar to login and submit their
|
||||||
|
# request.
|
||||||
|
readTimeoutSeconds: 3600
|
||||||
|
|
||||||
|
# Hostname of the EPP server.
|
||||||
|
# TODO(b/64510444) Remove this after nomulus no longer check sni header.
|
||||||
|
serverHostname: epp.yourdomain.tld
|
||||||
|
|
||||||
|
whois:
|
||||||
|
port: 43
|
||||||
|
relayHost: registry-project-id.appspot.com
|
||||||
|
relayPath: /_dr/whois
|
||||||
|
|
||||||
|
# Maximum input message length in bytes.
|
||||||
|
#
|
||||||
|
# Domain name cannot be longer than 256 characters. 512-character message
|
||||||
|
# length should be safe for most cases, including registrar queries.
|
||||||
|
#
|
||||||
|
# See also: RFC 1035 2.3.4 Size limits
|
||||||
|
# (http://www.freesoft.org/CIE/RFC/1035/9.htm).
|
||||||
|
maxMessageLengthBytes: 512
|
||||||
|
|
||||||
|
# Whois protocol is transient, the client should not establish a long lasting
|
||||||
|
# idle connection.
|
||||||
|
readTimeoutSeconds: 60
|
||||||
|
|
||||||
|
healthCheck:
|
||||||
|
port: 11111
|
||||||
|
|
||||||
|
# Health checker request message, defined in GCP load balancer backend.
|
||||||
|
checkRequest: HEALTH_CHECK_REQUEST
|
||||||
|
|
||||||
|
# Health checker response message, defined in GCP load balancer backend.
|
||||||
|
checkResponse: HEALTH_CHECK_RESPONSE
|
||||||
|
|
||||||
|
httpsRelay:
|
||||||
|
port: 443
|
||||||
|
|
||||||
|
# Maximum size of an HTTP message in bytes.
|
||||||
|
maxMessageLengthBytes: 524288
|
||||||
|
|
||||||
|
metrics:
|
||||||
|
# Max queries per second for the Google Cloud Monitoring V3 (aka Stackdriver)
|
||||||
|
# API. The limit can be adjusted by contacting Cloud Support.
|
||||||
|
stackdriverMaxQps: 30
|
||||||
|
|
||||||
|
# Max number of points that can be sent to Stackdriver in a single
|
||||||
|
# TimeSeries.Create API call.
|
||||||
|
stackdriverMaxPointsPerRequest: 200
|
||||||
|
|
||||||
|
# How often metrics are written.
|
||||||
|
writeIntervalSeconds: 60
|
|
@ -0,0 +1 @@
|
||||||
|
# Add environment-specific proxy configuration here.
|
|
@ -0,0 +1 @@
|
||||||
|
# Add environment-specific proxy configuration here.
|
|
@ -0,0 +1 @@
|
||||||
|
# Add environment-specific proxy configuration here.
|
|
@ -0,0 +1 @@
|
||||||
|
# Add environment-specific proxy configuration here.
|
1
java/google/registry/proxy/config/proxy-config-test.yaml
Normal file
1
java/google/registry/proxy/config/proxy-config-test.yaml
Normal file
|
@ -0,0 +1 @@
|
||||||
|
# This file is for test only. Leave it blank.
|
129
java/google/registry/proxy/handler/BackendMetricsHandler.java
Normal file
129
java/google/registry/proxy/handler/BackendMetricsHandler.java
Normal file
|
@ -0,0 +1,129 @@
|
||||||
|
// Copyright 2017 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.proxy.handler;
|
||||||
|
|
||||||
|
import static com.google.common.base.Preconditions.checkArgument;
|
||||||
|
import static com.google.common.base.Preconditions.checkNotNull;
|
||||||
|
import static com.google.common.base.Preconditions.checkState;
|
||||||
|
import static google.registry.proxy.Protocol.PROTOCOL_KEY;
|
||||||
|
import static google.registry.proxy.handler.EppServiceHandler.CLIENT_CERTIFICATE_HASH_KEY;
|
||||||
|
import static google.registry.proxy.handler.RelayHandler.RELAY_CHANNEL_KEY;
|
||||||
|
|
||||||
|
import google.registry.proxy.handler.RelayHandler.FullHttpResponseRelayHandler;
|
||||||
|
import google.registry.proxy.metric.BackendMetrics;
|
||||||
|
import google.registry.util.Clock;
|
||||||
|
import io.netty.channel.Channel;
|
||||||
|
import io.netty.channel.ChannelDuplexHandler;
|
||||||
|
import io.netty.channel.ChannelFuture;
|
||||||
|
import io.netty.channel.ChannelHandlerContext;
|
||||||
|
import io.netty.channel.ChannelPromise;
|
||||||
|
import io.netty.handler.codec.http.FullHttpRequest;
|
||||||
|
import io.netty.handler.codec.http.FullHttpResponse;
|
||||||
|
import java.util.ArrayDeque;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.Queue;
|
||||||
|
import javax.inject.Inject;
|
||||||
|
import org.joda.time.DateTime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handler that records metrics a backend channel.
|
||||||
|
*
|
||||||
|
* <p>This handler is added right before {@link FullHttpResponseRelayHandler} in the backend
|
||||||
|
* protocol handler provider method. {@link FullHttpRequest} outbound messages encounter this first
|
||||||
|
* before being handed over to HTTP related handler. {@link FullHttpResponse} inbound messages are
|
||||||
|
* first constructed (from plain bytes) by preceding handlers and then logged in this handler.
|
||||||
|
*/
|
||||||
|
public class BackendMetricsHandler extends ChannelDuplexHandler {
|
||||||
|
|
||||||
|
private final Clock clock;
|
||||||
|
private final BackendMetrics metrics;
|
||||||
|
|
||||||
|
private String relayedProtocolName;
|
||||||
|
private String clientCertHash;
|
||||||
|
private Channel relayedChannel;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A queue that saves the time at which a request is sent to the GAE app.
|
||||||
|
*
|
||||||
|
* <p>This queue is used to calculate HTTP request-response latency. HTTP 1.1 specification allows
|
||||||
|
* for pipelining, in which a client can sent multiple requests without waiting for each
|
||||||
|
* responses. Therefore a queue is needed to record all the requests that are sent but have not
|
||||||
|
* yet received a response.
|
||||||
|
*
|
||||||
|
* <p>A server must send its response in the same order it receives requests. This invariance
|
||||||
|
* guarantees that the request time at the head of the queue always corresponds to the response
|
||||||
|
* received in {@link #channelRead}.
|
||||||
|
*
|
||||||
|
* @see <a href="https://www.w3.org/Protocols/rfc2616/rfc2616-sec8.html">RFC 2616 8.1.2.2
|
||||||
|
* Pipelining</a>
|
||||||
|
*/
|
||||||
|
private final Queue<DateTime> requestSentTimeQueue = new ArrayDeque<>();
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
BackendMetricsHandler(Clock clock, BackendMetrics metrics) {
|
||||||
|
this.clock = clock;
|
||||||
|
this.metrics = metrics;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void channelActive(ChannelHandlerContext ctx) throws Exception {
|
||||||
|
// Backend channel is always established after a frontend channel is connected, so this
|
||||||
|
relayedChannel = ctx.channel().attr(RELAY_CHANNEL_KEY).get();
|
||||||
|
checkNotNull(relayedChannel, "No frontend channel found.");
|
||||||
|
relayedProtocolName = relayedChannel.attr(PROTOCOL_KEY).get().name();
|
||||||
|
super.channelActive(ctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
|
||||||
|
checkArgument(msg instanceof FullHttpResponse, "Incoming response must be FullHttpResponse.");
|
||||||
|
checkState(!requestSentTimeQueue.isEmpty(), "Response received before request is sent.");
|
||||||
|
metrics.responseReceived(
|
||||||
|
relayedProtocolName,
|
||||||
|
clientCertHash,
|
||||||
|
(FullHttpResponse) msg,
|
||||||
|
clock.nowUtc().getMillis() - requestSentTimeQueue.remove().getMillis());
|
||||||
|
super.channelRead(ctx, msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise)
|
||||||
|
throws Exception {
|
||||||
|
checkArgument(msg instanceof FullHttpRequest, "Outgoing request must be FullHttpRequest.");
|
||||||
|
// For WHOIS, client certificate hash is always set to "none".
|
||||||
|
// For EPP, the client hash attribute is set upon handshake completion, before the first HELLO
|
||||||
|
// is sent to the server. Therefore the first call to write() with HELLO payload has access to
|
||||||
|
// the hash in its channel attribute.
|
||||||
|
if (clientCertHash == null) {
|
||||||
|
clientCertHash =
|
||||||
|
Optional.ofNullable(relayedChannel.attr(CLIENT_CERTIFICATE_HASH_KEY).get())
|
||||||
|
.orElse("none");
|
||||||
|
}
|
||||||
|
FullHttpRequest request = (FullHttpRequest) msg;
|
||||||
|
|
||||||
|
// Record sent time before write finishes allows us to take network latency into account.
|
||||||
|
DateTime sentTime = clock.nowUtc();
|
||||||
|
ChannelFuture unusedFuture =
|
||||||
|
ctx.write(msg, promise)
|
||||||
|
.addListener(
|
||||||
|
future -> {
|
||||||
|
if (future.isSuccess()) {
|
||||||
|
// Only instrument request metrics when the request is actually sent to GAE.
|
||||||
|
metrics.requestSent(relayedProtocolName, clientCertHash, request);
|
||||||
|
requestSentTimeQueue.add(sentTime);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
154
java/google/registry/proxy/handler/EppServiceHandler.java
Normal file
154
java/google/registry/proxy/handler/EppServiceHandler.java
Normal file
|
@ -0,0 +1,154 @@
|
||||||
|
// Copyright 2017 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.proxy.handler;
|
||||||
|
|
||||||
|
import static com.google.common.base.Preconditions.checkArgument;
|
||||||
|
import static com.google.common.base.Preconditions.checkNotNull;
|
||||||
|
import static google.registry.proxy.handler.ProxyProtocolHandler.REMOTE_ADDRESS_KEY;
|
||||||
|
import static google.registry.proxy.handler.SslServerInitializer.CLIENT_CERTIFICATE_PROMISE_KEY;
|
||||||
|
import static google.registry.util.X509Utils.getCertificateHash;
|
||||||
|
|
||||||
|
import com.google.common.base.Supplier;
|
||||||
|
import google.registry.proxy.metric.FrontendMetrics;
|
||||||
|
import google.registry.util.FormattingLogger;
|
||||||
|
import io.netty.buffer.ByteBuf;
|
||||||
|
import io.netty.buffer.Unpooled;
|
||||||
|
import io.netty.channel.ChannelFuture;
|
||||||
|
import io.netty.channel.ChannelFutureListener;
|
||||||
|
import io.netty.channel.ChannelHandlerContext;
|
||||||
|
import io.netty.channel.ChannelPromise;
|
||||||
|
import io.netty.handler.codec.http.FullHttpRequest;
|
||||||
|
import io.netty.handler.codec.http.HttpHeaderNames;
|
||||||
|
import io.netty.handler.codec.http.HttpResponse;
|
||||||
|
import io.netty.handler.ssl.SslHandshakeCompletionEvent;
|
||||||
|
import io.netty.util.AttributeKey;
|
||||||
|
import io.netty.util.concurrent.Promise;
|
||||||
|
import java.security.cert.X509Certificate;
|
||||||
|
|
||||||
|
/** Handler that processes EPP protocol logic. */
|
||||||
|
public class EppServiceHandler extends HttpsRelayServiceHandler {
|
||||||
|
|
||||||
|
private static final FormattingLogger logger = FormattingLogger.getLoggerForCallerClass();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attribute key to the client certificate hash whose value is set when the certificate promise is
|
||||||
|
* fulfilled.
|
||||||
|
*/
|
||||||
|
public static final AttributeKey<String> CLIENT_CERTIFICATE_HASH_KEY =
|
||||||
|
AttributeKey.valueOf("CLIENT_CERTIFICATE_HASH_KEY");
|
||||||
|
|
||||||
|
/** Name of the HTTP header that stores the client certificate hash. */
|
||||||
|
public static final String SSL_CLIENT_CERTIFICATE_HASH_FIELD = "X-SSL-Certificate";
|
||||||
|
|
||||||
|
/** Name of the HTTP header that stores the epp server name requested by the client using SNI. */
|
||||||
|
// TODO(b/64510444): remove this header entirely when borg proxy is retired.
|
||||||
|
public static final String REQUESTED_SERVERNAME_VIA_SNI_FIELD = "X-Requested-Servername-SNI";
|
||||||
|
|
||||||
|
/** Name of the HTTP header that stores the client IP address. */
|
||||||
|
public static final String FORWARDED_FOR_FIELD = "X-Forwarded-For";
|
||||||
|
|
||||||
|
/** Name of the HTTP header that indicates if the EPP session should be closed. */
|
||||||
|
public static final String EPP_SESSION_FIELD = "Epp-Session";
|
||||||
|
|
||||||
|
public static final String EPP_CONTENT_TYPE = "application/epp+xml";
|
||||||
|
|
||||||
|
private final String serverHostname;
|
||||||
|
private final byte[] helloBytes;
|
||||||
|
|
||||||
|
private String sslClientCertificateHash;
|
||||||
|
private String clientAddress;
|
||||||
|
|
||||||
|
public EppServiceHandler(
|
||||||
|
String relayHost,
|
||||||
|
String relayPath,
|
||||||
|
Supplier<String> accessTokenSupplier,
|
||||||
|
String serverHostname,
|
||||||
|
byte[] helloBytes,
|
||||||
|
FrontendMetrics metrics) {
|
||||||
|
super(relayHost, relayPath, accessTokenSupplier, metrics);
|
||||||
|
this.serverHostname = serverHostname;
|
||||||
|
this.helloBytes = helloBytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Write <hello> to the server after SSL handshake completion to request <greeting>
|
||||||
|
*
|
||||||
|
* <p>When handling EPP over TCP, the server should issue a <greeting> to the client when a
|
||||||
|
* connection is established. Nomulus app however does not automatically sends the <greeting> upon
|
||||||
|
* connection. The proxy therefore first sends a <hello> to registry to request a <greeting>
|
||||||
|
* response.
|
||||||
|
*
|
||||||
|
* <p>The <hello> request is only sent after SSL handshake is completed between the client and the
|
||||||
|
* proxy so that the client certificate hash is available, which is needed to communicate with the
|
||||||
|
* server. Because {@link SslHandshakeCompletionEvent} is triggered before any calls to {@link
|
||||||
|
* #channelRead} are scheduled by the event loop executor, the <hello> request is guaranteed to be
|
||||||
|
* the first message sent to the server.
|
||||||
|
*
|
||||||
|
* @see <a href="https://tools.ietf.org/html/rfc5734">RFC 5732 EPP Transport over TCP</a>
|
||||||
|
* @see <a href="https://www.haproxy.org/download/1.8/doc/proxy-protocol.txt">The Proxy
|
||||||
|
* Protocol</a>
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void channelActive(ChannelHandlerContext ctx) throws Exception {
|
||||||
|
Promise<X509Certificate> unusedPromise =
|
||||||
|
ctx.channel()
|
||||||
|
.attr(CLIENT_CERTIFICATE_PROMISE_KEY)
|
||||||
|
.get()
|
||||||
|
.addListener(
|
||||||
|
(Promise<X509Certificate> promise) -> {
|
||||||
|
if (promise.isSuccess()) {
|
||||||
|
sslClientCertificateHash = getCertificateHash(promise.get());
|
||||||
|
// Set the client cert hash key attribute for both this channel,
|
||||||
|
// used for collecting metrics on specific clients.
|
||||||
|
ctx.channel().attr(CLIENT_CERTIFICATE_HASH_KEY).set(sslClientCertificateHash);
|
||||||
|
clientAddress = ctx.channel().attr(REMOTE_ADDRESS_KEY).get();
|
||||||
|
metrics.registerActiveConnection(
|
||||||
|
"epp", sslClientCertificateHash, ctx.channel());
|
||||||
|
channelRead(ctx, Unpooled.wrappedBuffer(helloBytes));
|
||||||
|
} else {
|
||||||
|
logger.severefmt(promise.cause(), "Cannot finish handshake.");
|
||||||
|
ChannelFuture unusedFuture = ctx.close();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
super.channelActive(ctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected FullHttpRequest decodeFullHttpRequest(ByteBuf byteBuf) {
|
||||||
|
checkNotNull(clientAddress, "Cannot obtain client address.");
|
||||||
|
checkNotNull(sslClientCertificateHash, "Cannot obtain client certificate hash.");
|
||||||
|
FullHttpRequest request = super.decodeFullHttpRequest(byteBuf);
|
||||||
|
request
|
||||||
|
.headers()
|
||||||
|
.set(SSL_CLIENT_CERTIFICATE_HASH_FIELD, sslClientCertificateHash)
|
||||||
|
.set(REQUESTED_SERVERNAME_VIA_SNI_FIELD, serverHostname)
|
||||||
|
.set(FORWARDED_FOR_FIELD, clientAddress)
|
||||||
|
.set(HttpHeaderNames.CONTENT_TYPE, EPP_CONTENT_TYPE)
|
||||||
|
.set(HttpHeaderNames.ACCEPT, EPP_CONTENT_TYPE);
|
||||||
|
return request;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise)
|
||||||
|
throws Exception {
|
||||||
|
checkArgument(msg instanceof HttpResponse);
|
||||||
|
HttpResponse response = (HttpResponse) msg;
|
||||||
|
String sessionAliveValue = response.headers().get(EPP_SESSION_FIELD);
|
||||||
|
if (sessionAliveValue != null && sessionAliveValue.equals("close")) {
|
||||||
|
promise.addListener(ChannelFutureListener.CLOSE);
|
||||||
|
}
|
||||||
|
super.write(ctx, msg, promise);
|
||||||
|
}
|
||||||
|
}
|
42
java/google/registry/proxy/handler/HealthCheckHandler.java
Normal file
42
java/google/registry/proxy/handler/HealthCheckHandler.java
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
// Copyright 2017 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.proxy.handler;
|
||||||
|
|
||||||
|
import io.netty.buffer.ByteBuf;
|
||||||
|
import io.netty.buffer.Unpooled;
|
||||||
|
import io.netty.channel.ChannelHandlerContext;
|
||||||
|
import io.netty.channel.ChannelInboundHandlerAdapter;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
|
||||||
|
/** A handler that responds to GCP load balancer health check message */
|
||||||
|
public class HealthCheckHandler extends ChannelInboundHandlerAdapter {
|
||||||
|
|
||||||
|
private final ByteBuf checkRequest;
|
||||||
|
private final ByteBuf checkResponse;
|
||||||
|
|
||||||
|
public HealthCheckHandler(String checkRequest, String checkResponse) {
|
||||||
|
this.checkRequest = Unpooled.wrappedBuffer(checkRequest.getBytes(StandardCharsets.US_ASCII));
|
||||||
|
this.checkResponse = Unpooled.wrappedBuffer(checkResponse.getBytes(StandardCharsets.US_ASCII));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
|
||||||
|
ByteBuf buf = (ByteBuf) msg;
|
||||||
|
if (buf.equals(checkRequest)) {
|
||||||
|
ctx.writeAndFlush(checkResponse);
|
||||||
|
}
|
||||||
|
buf.release();
|
||||||
|
}
|
||||||
|
}
|
185
java/google/registry/proxy/handler/HttpsRelayServiceHandler.java
Normal file
185
java/google/registry/proxy/handler/HttpsRelayServiceHandler.java
Normal file
|
@ -0,0 +1,185 @@
|
||||||
|
// Copyright 2017 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.proxy.handler;
|
||||||
|
|
||||||
|
import static com.google.common.base.Preconditions.checkArgument;
|
||||||
|
import static java.nio.charset.StandardCharsets.UTF_8;
|
||||||
|
|
||||||
|
import com.google.common.base.Supplier;
|
||||||
|
import google.registry.proxy.metric.FrontendMetrics;
|
||||||
|
import google.registry.util.FormattingLogger;
|
||||||
|
import io.netty.buffer.ByteBuf;
|
||||||
|
import io.netty.channel.ChannelFuture;
|
||||||
|
import io.netty.channel.ChannelHandlerContext;
|
||||||
|
import io.netty.channel.ChannelPromise;
|
||||||
|
import io.netty.handler.codec.ByteToMessageCodec;
|
||||||
|
import io.netty.handler.codec.http.DefaultFullHttpRequest;
|
||||||
|
import io.netty.handler.codec.http.FullHttpRequest;
|
||||||
|
import io.netty.handler.codec.http.FullHttpResponse;
|
||||||
|
import io.netty.handler.codec.http.HttpHeaderNames;
|
||||||
|
import io.netty.handler.codec.http.HttpMethod;
|
||||||
|
import io.netty.handler.codec.http.HttpResponseStatus;
|
||||||
|
import io.netty.handler.codec.http.HttpVersion;
|
||||||
|
import io.netty.handler.codec.http.cookie.ClientCookieDecoder;
|
||||||
|
import io.netty.handler.codec.http.cookie.ClientCookieEncoder;
|
||||||
|
import io.netty.handler.codec.http.cookie.Cookie;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handler that relays a single (framed) ByteBuf message to an HTTPS server.
|
||||||
|
*
|
||||||
|
* <p>This handler reads in a {@link ByteBuf}, converts it to an {@link FullHttpRequest}, and passes
|
||||||
|
* it to the {@code channelRead} method of the next inbound handler the channel pipeline, which is
|
||||||
|
* usually a {@link RelayHandler<FullHttpRequest>}. The relay handler writes the request to the
|
||||||
|
* relay channel, which is connected to an HTTPS endpoint. After the relay channel receives a {@link
|
||||||
|
* FullHttpResponse} back, its own relay handler writes the response back to this channel, which is
|
||||||
|
* the relay channel of the relay channel. This handler then handles write request by encoding the
|
||||||
|
* {@link FullHttpResponse} to a plain {@link ByteBuf}, and pass it down to the {@code write} method
|
||||||
|
* of the next outbound handler in the channel pipeline, which eventually writes the response bytes
|
||||||
|
* to the remote peer of this channel.
|
||||||
|
*
|
||||||
|
* <p>This handler is session aware and will store all the session cookies that the are contained in
|
||||||
|
* the HTTP response headers, which are added back to headers of subsequent HTTP requests.
|
||||||
|
*/
|
||||||
|
abstract class HttpsRelayServiceHandler extends ByteToMessageCodec<FullHttpResponse> {
|
||||||
|
|
||||||
|
private static final FormattingLogger logger = FormattingLogger.getLoggerForCallerClass();
|
||||||
|
|
||||||
|
private final Map<String, Cookie> cookieStore = new LinkedHashMap<>();
|
||||||
|
private final String relayHost;
|
||||||
|
private final String relayPath;
|
||||||
|
private final Supplier<String> accessTokenSupplier;
|
||||||
|
|
||||||
|
protected final FrontendMetrics metrics;
|
||||||
|
|
||||||
|
HttpsRelayServiceHandler(
|
||||||
|
String relayHost,
|
||||||
|
String relayPath,
|
||||||
|
Supplier<String> accessTokenSupplier,
|
||||||
|
FrontendMetrics metrics) {
|
||||||
|
this.relayHost = relayHost;
|
||||||
|
this.relayPath = relayPath;
|
||||||
|
this.accessTokenSupplier = accessTokenSupplier;
|
||||||
|
this.metrics = metrics;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Construct the {@link FullHttpRequest}.
|
||||||
|
*
|
||||||
|
* <p>This default method creates a bare-bone {@link FullHttpRequest} that may need to be
|
||||||
|
* modified, e. g. adding headers specific for each protocol.
|
||||||
|
*
|
||||||
|
* @param byteBuf inbound message.
|
||||||
|
*/
|
||||||
|
protected FullHttpRequest decodeFullHttpRequest(ByteBuf byteBuf) {
|
||||||
|
FullHttpRequest request =
|
||||||
|
new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.POST, relayPath);
|
||||||
|
request
|
||||||
|
.headers()
|
||||||
|
.set(HttpHeaderNames.USER_AGENT, "Proxy")
|
||||||
|
.set(HttpHeaderNames.HOST, relayHost)
|
||||||
|
.set(HttpHeaderNames.AUTHORIZATION, "Bearer " + accessTokenSupplier.get())
|
||||||
|
.set(HttpHeaderNames.CONTENT_LENGTH, byteBuf.readableBytes());
|
||||||
|
request.content().writeBytes(byteBuf);
|
||||||
|
return request;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load session cookies in the cookie store and write them in to the HTTP request.
|
||||||
|
*
|
||||||
|
* <p>Multiple cookies are folded into one {@code Cookie} header per RFC 6265.
|
||||||
|
*
|
||||||
|
* @see <a href="https://tools.ietf.org/html/rfc6265#section-5.4">RFC 6265 5.4.The Cookie
|
||||||
|
* Header</a>
|
||||||
|
*/
|
||||||
|
private void loadCookies(FullHttpRequest request) {
|
||||||
|
if (!cookieStore.isEmpty()) {
|
||||||
|
request
|
||||||
|
.headers()
|
||||||
|
.set(HttpHeaderNames.COOKIE, ClientCookieEncoder.STRICT.encode(cookieStore.values()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void decode(ChannelHandlerContext ctx, ByteBuf byteBuf, List<Object> out)
|
||||||
|
throws Exception {
|
||||||
|
FullHttpRequest request = decodeFullHttpRequest(byteBuf);
|
||||||
|
loadCookies(request);
|
||||||
|
out.add(request);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Construct the {@link ByteBuf}
|
||||||
|
*
|
||||||
|
* <p>This default method puts all the response payload into the {@link ByteBuf}.
|
||||||
|
*
|
||||||
|
* @param fullHttpResponse outbound http response.
|
||||||
|
*/
|
||||||
|
ByteBuf encodeFullHttpResponse(FullHttpResponse fullHttpResponse) {
|
||||||
|
return fullHttpResponse.content();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save session cookies from the HTTP response header to the cookie store.
|
||||||
|
*
|
||||||
|
* <p>Multiple cookies are </b>not</b> folded in to one {@code Set-Cookie} header per RFC 6265.
|
||||||
|
*
|
||||||
|
* @see <a href="https://tools.ietf.org/html/rfc6265#section-3">RFC 6265 3.Overview</a>
|
||||||
|
*/
|
||||||
|
private void saveCookies(FullHttpResponse response) {
|
||||||
|
for (String cookieString : response.headers().getAll(HttpHeaderNames.SET_COOKIE)) {
|
||||||
|
Cookie cookie = ClientCookieDecoder.STRICT.decode(cookieString);
|
||||||
|
cookieStore.put(cookie.name(), cookie);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void encode(ChannelHandlerContext ctx, FullHttpResponse response, ByteBuf byteBuf)
|
||||||
|
throws Exception {
|
||||||
|
checkArgument(
|
||||||
|
response.status().equals(HttpResponseStatus.OK),
|
||||||
|
"Cannot relay HTTP response status \"%s\"\n%s",
|
||||||
|
response.status(),
|
||||||
|
response.content().toString(UTF_8));
|
||||||
|
saveCookies(response);
|
||||||
|
byteBuf.writeBytes(encodeFullHttpResponse(response));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Terminates connection upon inbound exception. */
|
||||||
|
@Override
|
||||||
|
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
|
||||||
|
logger.severefmt(cause, "Inbound exception caught for channel %s", ctx.channel());
|
||||||
|
ChannelFuture unusedFuture = ctx.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Terminates connection upon outbound exception. */
|
||||||
|
@Override
|
||||||
|
public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise)
|
||||||
|
throws Exception {
|
||||||
|
promise.addListener(
|
||||||
|
(ChannelFuture channelFuture) -> {
|
||||||
|
if (!channelFuture.isSuccess()) {
|
||||||
|
logger.severefmt(
|
||||||
|
channelFuture.cause(),
|
||||||
|
"Outbound exception caught for channel %s",
|
||||||
|
channelFuture.channel());
|
||||||
|
ChannelFuture unusedFuture = channelFuture.channel().close();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
super.write(ctx, msg, promise);
|
||||||
|
}
|
||||||
|
}
|
167
java/google/registry/proxy/handler/ProxyProtocolHandler.java
Normal file
167
java/google/registry/proxy/handler/ProxyProtocolHandler.java
Normal file
|
@ -0,0 +1,167 @@
|
||||||
|
// Copyright 2017 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.proxy.handler;
|
||||||
|
|
||||||
|
import static com.google.common.base.Preconditions.checkState;
|
||||||
|
import static java.nio.charset.StandardCharsets.US_ASCII;
|
||||||
|
|
||||||
|
import google.registry.util.FormattingLogger;
|
||||||
|
import io.netty.buffer.ByteBuf;
|
||||||
|
import io.netty.channel.ChannelHandlerContext;
|
||||||
|
import io.netty.handler.codec.ByteToMessageDecoder;
|
||||||
|
import io.netty.util.AttributeKey;
|
||||||
|
import java.net.InetSocketAddress;
|
||||||
|
import java.net.SocketAddress;
|
||||||
|
import java.util.List;
|
||||||
|
import javax.inject.Inject;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handler that processes possible existence of a PROXY protocol v1 header.
|
||||||
|
*
|
||||||
|
* <p>When an EPP client connects to the registry (through the proxy), the registry performs two
|
||||||
|
* validations to ensure that only known registrars are allowed. First it checks the sha265 hash of
|
||||||
|
* the client SSL certificate and match it to the hash stored in datastore for the registrar. It
|
||||||
|
* then checks if the connection is from an whitelisted IP address that belongs to that registrar.
|
||||||
|
*
|
||||||
|
* <p>The proxy receives client connects via the GCP load balancer, which results in the loss of
|
||||||
|
* original client IP from the channel. Luckily, the load balancer supports the PROXY protocol v1,
|
||||||
|
* which adds a header with source IP information, among other things, to the TCP request at the
|
||||||
|
* start of the connection.
|
||||||
|
*
|
||||||
|
* <p>This handler determines if a connection is proxied (PROXY protocol v1 header present) and
|
||||||
|
* correctly sets the source IP address to the channel's attribute regardless of whether it is
|
||||||
|
* proxied. After that it removes itself from the channel pipeline because the proxy header is only
|
||||||
|
* present at the beginning of the connection.
|
||||||
|
*
|
||||||
|
* <p>This handler must be the very first handler in a protocol, even before SSL handlers, because
|
||||||
|
* PROXY protocol header comes as the very first thing, even before SSL handshake request.
|
||||||
|
*
|
||||||
|
* @see <a href="https://www.haproxy.org/download/1.8/doc/proxy-protocol.txt">The PROXY protocol</a>
|
||||||
|
*/
|
||||||
|
public class ProxyProtocolHandler extends ByteToMessageDecoder {
|
||||||
|
|
||||||
|
/** Key used to retrieve origin IP address from a channel's attribute. */
|
||||||
|
public static final AttributeKey<String> REMOTE_ADDRESS_KEY =
|
||||||
|
AttributeKey.valueOf("REMOTE_ADDRESS_KEY");
|
||||||
|
|
||||||
|
private static final FormattingLogger logger = FormattingLogger.getLoggerForCallerClass();
|
||||||
|
|
||||||
|
// The proxy header must start with this prefix.
|
||||||
|
// Sample header: "PROXY TCP4 255.255.255.255 255.255.255.255 65535 65535\r\n".
|
||||||
|
private static final byte[] HEADER_PREFIX = "PROXY".getBytes(US_ASCII);
|
||||||
|
|
||||||
|
private boolean finished = false;
|
||||||
|
private String proxyHeader = null;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
ProxyProtocolHandler() {}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
|
||||||
|
super.channelRead(ctx, msg);
|
||||||
|
if (finished) {
|
||||||
|
if (proxyHeader != null) {
|
||||||
|
logger.finefmt("PROXIED CONNECTION: %s", ctx.channel());
|
||||||
|
logger.finefmt("PROXY HEADER: %s", proxyHeader);
|
||||||
|
ctx.channel().attr(REMOTE_ADDRESS_KEY).set(proxyHeader.split(" ")[2]);
|
||||||
|
} else {
|
||||||
|
SocketAddress remoteAddress = ctx.channel().remoteAddress();
|
||||||
|
if (remoteAddress instanceof InetSocketAddress) {
|
||||||
|
ctx.channel()
|
||||||
|
.attr(REMOTE_ADDRESS_KEY)
|
||||||
|
.set(((InetSocketAddress) remoteAddress).getAddress().getHostAddress());
|
||||||
|
logger.finefmt("REMOTE IP ADDRESS: %s", ctx.channel().attr(REMOTE_ADDRESS_KEY).get());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ctx.pipeline().remove(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attempts to decode an internally accumulated buffer and find the proxy protocol header.
|
||||||
|
*
|
||||||
|
* <p>When the connection is not proxied (i. e. the initial bytes are not "PROXY"), simply set
|
||||||
|
* {@link #finished} to true and allow the handler to be removed. Otherwise the handler waits
|
||||||
|
* until there's enough bytes to parse the header, save the parsed header to {@link #proxyHeader},
|
||||||
|
* and then mark {@link #finished}.
|
||||||
|
*
|
||||||
|
* @param in internally accumulated buffer, newly arrived bytes are appended to it.
|
||||||
|
* @param out objects passed to the next handler, in this case nothing is ever passed because the
|
||||||
|
* header itself is processed and written to the attribute of the proxy, and the handler is
|
||||||
|
* then removed from the pipeline.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
|
||||||
|
// Wait until there are more bytes available than the header's length before processing.
|
||||||
|
if (in.readableBytes() >= HEADER_PREFIX.length) {
|
||||||
|
if (containsHeader(in)) {
|
||||||
|
// The inbound message contains the header, it must be a proxied connection. Note that
|
||||||
|
// currently proxied connection is only used for EPP protocol, which requires the connection
|
||||||
|
// to be SSL enabled. So the beginning of the inbound message upon connection can only be
|
||||||
|
// either the proxy header (when proxied), or SSL handshake request (when not proxied),
|
||||||
|
// which does not start with "PROXY". Therefore it is safe to assume that if the beginning
|
||||||
|
// of the message contains "PROXY", it must be proxied, and must contain \r\n.
|
||||||
|
int eol = findEndOfLine(in);
|
||||||
|
// If eol is not found, that is because that we do not yet have enough inbound message, do
|
||||||
|
// nothing and wait for more bytes to be readable. eol will eventually be positive because
|
||||||
|
// of the reasoning above: The connection starts with "PROXY", so it must be a proxied
|
||||||
|
// connection and contain \r\n.
|
||||||
|
if (eol >= 0) {
|
||||||
|
// ByteBuf.readBytes is called so that the header is processed and not passed to handlers
|
||||||
|
// further in the pipeline.
|
||||||
|
proxyHeader = in.readBytes(eol).toString(US_ASCII);
|
||||||
|
// Skip \r\n.
|
||||||
|
in.skipBytes(2);
|
||||||
|
// Proxy header processed, mark finished so that this handler is removed.
|
||||||
|
finished = true;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// The inbound message does not contain a proxy header, mark finished so that this handler
|
||||||
|
// is removed. Note that no inbound bytes are actually processed by this handler because we
|
||||||
|
// did not call ByteBuf.readBytes(), but ByteBuf.getByte(), which does not change reader
|
||||||
|
// index of the ByteBuf. So any inbound byte is then passed to the next handler to process.
|
||||||
|
finished = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the index in the buffer of the end of line found. Returns -1 if no end of line was
|
||||||
|
* found in the buffer.
|
||||||
|
*/
|
||||||
|
private static int findEndOfLine(final ByteBuf buffer) {
|
||||||
|
final int n = buffer.writerIndex();
|
||||||
|
for (int i = buffer.readerIndex(); i < n; i++) {
|
||||||
|
final byte b = buffer.getByte(i);
|
||||||
|
if (b == '\r' && i < n - 1 && buffer.getByte(i + 1) == '\n') {
|
||||||
|
return i; // \r\n
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return -1; // Not found.
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Checks if the given buffer contains the proxy header prefix. */
|
||||||
|
private boolean containsHeader(ByteBuf buffer) {
|
||||||
|
// The readable bytes is always more or equal to the size of the header prefix because this
|
||||||
|
// method is only called when this condition is true.
|
||||||
|
checkState(buffer.readableBytes() >= HEADER_PREFIX.length);
|
||||||
|
for (int i = 0; i < HEADER_PREFIX.length; ++i) {
|
||||||
|
if (buffer.getByte(buffer.readerIndex() + i) != HEADER_PREFIX[i]) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
99
java/google/registry/proxy/handler/RelayHandler.java
Normal file
99
java/google/registry/proxy/handler/RelayHandler.java
Normal file
|
@ -0,0 +1,99 @@
|
||||||
|
// Copyright 2017 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.proxy.handler;
|
||||||
|
|
||||||
|
import static com.google.common.base.Preconditions.checkNotNull;
|
||||||
|
|
||||||
|
import google.registry.util.FormattingLogger;
|
||||||
|
import io.netty.channel.Channel;
|
||||||
|
import io.netty.channel.ChannelFuture;
|
||||||
|
import io.netty.channel.ChannelHandlerContext;
|
||||||
|
import io.netty.channel.SimpleChannelInboundHandler;
|
||||||
|
import io.netty.handler.codec.http.FullHttpRequest;
|
||||||
|
import io.netty.handler.codec.http.FullHttpResponse;
|
||||||
|
import io.netty.util.Attribute;
|
||||||
|
import io.netty.util.AttributeKey;
|
||||||
|
import javax.inject.Inject;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Receives inbound massage of type {@code I}, and writes it to the {@code relayChannel} stored in
|
||||||
|
* the inbound channel's attribute.
|
||||||
|
*/
|
||||||
|
public class RelayHandler<I> extends SimpleChannelInboundHandler<I> {
|
||||||
|
|
||||||
|
private static final FormattingLogger logger = FormattingLogger.getLoggerForCallerClass();
|
||||||
|
|
||||||
|
/** Key used to retrieve the relay channel from a {@link Channel}'s {@link Attribute}. */
|
||||||
|
public static final AttributeKey<Channel> RELAY_CHANNEL_KEY =
|
||||||
|
AttributeKey.valueOf("RELAY_CHANNEL");
|
||||||
|
|
||||||
|
public RelayHandler(Class<? extends I> clazz) {
|
||||||
|
super(clazz, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Terminate connection when an exception is caught during inbound IO. */
|
||||||
|
@Override
|
||||||
|
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
|
||||||
|
logger.severefmt(cause, "Inbound exception caught for channel %s", ctx.channel());
|
||||||
|
ctx.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Close relay channel if this channel is closed. */
|
||||||
|
@Override
|
||||||
|
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
|
||||||
|
Channel relayChannel = ctx.channel().attr(RELAY_CHANNEL_KEY).get();
|
||||||
|
if (relayChannel != null) {
|
||||||
|
relayChannel.close();
|
||||||
|
}
|
||||||
|
ctx.fireChannelInactive();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Read message of type {@code I}, write it as-is into the relay channel. */
|
||||||
|
@Override
|
||||||
|
protected void channelRead0(ChannelHandlerContext ctx, I msg) throws Exception {
|
||||||
|
Channel relayChannel = ctx.channel().attr(RELAY_CHANNEL_KEY).get();
|
||||||
|
checkNotNull(relayChannel, "Relay channel not specified for channel: %s", ctx.channel());
|
||||||
|
if (relayChannel.isActive()) {
|
||||||
|
// Relay channel is open, write to it.
|
||||||
|
ChannelFuture channelFuture = relayChannel.writeAndFlush(msg);
|
||||||
|
channelFuture.addListener(
|
||||||
|
future -> {
|
||||||
|
// Cannot write into relay channel, close this channel.
|
||||||
|
if (!future.isSuccess()) {
|
||||||
|
ctx.close();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// close this channel if the relay channel is closed.
|
||||||
|
ctx.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Specialized {@link RelayHandler} that takes a {@link FullHttpRequest} as inbound payload. */
|
||||||
|
public static class FullHttpRequestRelayHandler extends RelayHandler<FullHttpRequest> {
|
||||||
|
@Inject
|
||||||
|
public FullHttpRequestRelayHandler() {
|
||||||
|
super(FullHttpRequest.class);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Specialized {@link RelayHandler} that takes a {@link FullHttpResponse} as inbound payload. */
|
||||||
|
public static class FullHttpResponseRelayHandler extends RelayHandler<FullHttpResponse> {
|
||||||
|
@Inject
|
||||||
|
public FullHttpResponseRelayHandler() {
|
||||||
|
super(FullHttpResponse.class);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
80
java/google/registry/proxy/handler/SslClientInitializer.java
Normal file
80
java/google/registry/proxy/handler/SslClientInitializer.java
Normal file
|
@ -0,0 +1,80 @@
|
||||||
|
// Copyright 2017 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.proxy.handler;
|
||||||
|
|
||||||
|
import static com.google.common.base.Preconditions.checkNotNull;
|
||||||
|
import static google.registry.proxy.Protocol.PROTOCOL_KEY;
|
||||||
|
|
||||||
|
import google.registry.proxy.Protocol.BackendProtocol;
|
||||||
|
import google.registry.util.FormattingLogger;
|
||||||
|
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.cert.X509Certificate;
|
||||||
|
import javax.annotation.Nullable;
|
||||||
|
import javax.inject.Inject;
|
||||||
|
import javax.inject.Named;
|
||||||
|
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>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 FormattingLogger logger = FormattingLogger.getLoggerForCallerClass();
|
||||||
|
private final SslProvider sslProvider;
|
||||||
|
private final X509Certificate[] trustedCertificates;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
SslClientInitializer(
|
||||||
|
SslProvider sslProvider,
|
||||||
|
@Nullable @Named("relayTrustedCertificates") X509Certificate... trustCertificates) {
|
||||||
|
logger.finefmt("Client SSL Provider: %s", sslProvider);
|
||||||
|
this.sslProvider = sslProvider;
|
||||||
|
this.trustedCertificates = trustCertificates;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void initChannel(C channel) throws Exception {
|
||||||
|
BackendProtocol protocol = (BackendProtocol) channel.attr(PROTOCOL_KEY).get();
|
||||||
|
checkNotNull(protocol, "Protocol is not set for channel: %s", channel);
|
||||||
|
SslHandler sslHandler =
|
||||||
|
SslContextBuilder.forClient()
|
||||||
|
.sslProvider(sslProvider)
|
||||||
|
.trustManager(trustedCertificates)
|
||||||
|
.build()
|
||||||
|
.newHandler(channel.alloc(), protocol.host(), protocol.port());
|
||||||
|
|
||||||
|
// Enable hostname verification.
|
||||||
|
SSLEngine sslEngine = sslHandler.engine();
|
||||||
|
SSLParameters sslParameters = sslEngine.getSSLParameters();
|
||||||
|
sslParameters.setEndpointIdentificationAlgorithm("HTTPS");
|
||||||
|
sslEngine.setSSLParameters(sslParameters);
|
||||||
|
|
||||||
|
channel.pipeline().addLast(sslHandler);
|
||||||
|
}
|
||||||
|
}
|
105
java/google/registry/proxy/handler/SslServerInitializer.java
Normal file
105
java/google/registry/proxy/handler/SslServerInitializer.java
Normal file
|
@ -0,0 +1,105 @@
|
||||||
|
// Copyright 2017 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.proxy.handler;
|
||||||
|
|
||||||
|
import google.registry.util.FormattingLogger;
|
||||||
|
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.ClientAuth;
|
||||||
|
import io.netty.handler.ssl.SslContextBuilder;
|
||||||
|
import io.netty.handler.ssl.SslHandler;
|
||||||
|
import io.netty.handler.ssl.SslProvider;
|
||||||
|
import io.netty.handler.ssl.util.InsecureTrustManagerFactory;
|
||||||
|
import io.netty.util.AttributeKey;
|
||||||
|
import io.netty.util.concurrent.Future;
|
||||||
|
import io.netty.util.concurrent.Promise;
|
||||||
|
import java.security.PrivateKey;
|
||||||
|
import java.security.cert.X509Certificate;
|
||||||
|
import javax.inject.Inject;
|
||||||
|
import javax.inject.Named;
|
||||||
|
import javax.inject.Singleton;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds a server side SSL handler to the channel pipeline.
|
||||||
|
*
|
||||||
|
* <p>This <b>should</b> be the first handler provided for any handler provider list, if it is
|
||||||
|
* provided. Unless you wish to first process the PROXY header with {@link ProxyProtocolHandler},
|
||||||
|
* which should come before this handler. The type parameter {@code C} is needed so that unit tests
|
||||||
|
* can construct this handler that works with {@link EmbeddedChannel};
|
||||||
|
*
|
||||||
|
* <p>The ssl handler added requires client authentication, but it uses an {@link
|
||||||
|
* InsecureTrustManagerFactory}, which accepts any ssl certificate presented by the client, as long
|
||||||
|
* as the client uses the corresponding private key to establish SSL handshake. The client
|
||||||
|
* certificate hash will be passed along to GAE as an HTTP header for verification (not handled by
|
||||||
|
* this handler).
|
||||||
|
*/
|
||||||
|
@Singleton
|
||||||
|
@Sharable
|
||||||
|
public class SslServerInitializer<C extends Channel> extends ChannelInitializer<C> {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attribute key to the client certificate promise whose value is set when SSL handshake completes
|
||||||
|
* successfully.
|
||||||
|
*/
|
||||||
|
public static final AttributeKey<Promise<X509Certificate>> CLIENT_CERTIFICATE_PROMISE_KEY =
|
||||||
|
AttributeKey.valueOf("CLIENT_CERTIFICATE_PROMISE_KEY");
|
||||||
|
|
||||||
|
private static final FormattingLogger logger = FormattingLogger.getLoggerForCallerClass();
|
||||||
|
private final SslProvider sslProvider;
|
||||||
|
private final PrivateKey privateKey;
|
||||||
|
private final X509Certificate[] certificates;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
SslServerInitializer(
|
||||||
|
SslProvider sslProvider,
|
||||||
|
PrivateKey privateKey,
|
||||||
|
@Named("eppServerCertificates") X509Certificate... certificates) {
|
||||||
|
logger.finefmt("Server SSL Provider: %s", sslProvider);
|
||||||
|
this.sslProvider = sslProvider;
|
||||||
|
this.privateKey = privateKey;
|
||||||
|
this.certificates = certificates;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void initChannel(C channel) throws Exception {
|
||||||
|
SslHandler sslHandler =
|
||||||
|
SslContextBuilder.forServer(privateKey, certificates)
|
||||||
|
.sslProvider(sslProvider)
|
||||||
|
.trustManager(InsecureTrustManagerFactory.INSTANCE)
|
||||||
|
.clientAuth(ClientAuth.REQUIRE)
|
||||||
|
.build()
|
||||||
|
.newHandler(channel.alloc());
|
||||||
|
Promise<X509Certificate> clientCertificatePromise = channel.eventLoop().newPromise();
|
||||||
|
Future<Channel> unusedFuture =
|
||||||
|
sslHandler
|
||||||
|
.handshakeFuture()
|
||||||
|
.addListener(
|
||||||
|
future -> {
|
||||||
|
if (future.isSuccess()) {
|
||||||
|
Promise<X509Certificate> unusedPromise =
|
||||||
|
clientCertificatePromise.setSuccess(
|
||||||
|
(X509Certificate)
|
||||||
|
sslHandler.engine().getSession().getPeerCertificates()[0]);
|
||||||
|
} else {
|
||||||
|
Promise<X509Certificate> unusedPromise =
|
||||||
|
clientCertificatePromise.setFailure(future.cause());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
channel.attr(CLIENT_CERTIFICATE_PROMISE_KEY).set(clientCertificatePromise);
|
||||||
|
channel.pipeline().addLast(sslHandler);
|
||||||
|
}
|
||||||
|
}
|
54
java/google/registry/proxy/handler/WhoisServiceHandler.java
Normal file
54
java/google/registry/proxy/handler/WhoisServiceHandler.java
Normal file
|
@ -0,0 +1,54 @@
|
||||||
|
// Copyright 2017 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.proxy.handler;
|
||||||
|
|
||||||
|
import com.google.common.base.Supplier;
|
||||||
|
import google.registry.proxy.metric.FrontendMetrics;
|
||||||
|
import io.netty.buffer.ByteBuf;
|
||||||
|
import io.netty.channel.ChannelHandlerContext;
|
||||||
|
import io.netty.handler.codec.http.FullHttpRequest;
|
||||||
|
import io.netty.handler.codec.http.HttpHeaderNames;
|
||||||
|
import io.netty.handler.codec.http.HttpHeaderValues;
|
||||||
|
|
||||||
|
/** Handler that processes WHOIS protocol logic. */
|
||||||
|
public final class WhoisServiceHandler extends HttpsRelayServiceHandler {
|
||||||
|
|
||||||
|
public WhoisServiceHandler(
|
||||||
|
String relayHost,
|
||||||
|
String relayPath,
|
||||||
|
Supplier<String> accessTokenSupplier,
|
||||||
|
FrontendMetrics metrics) {
|
||||||
|
super(relayHost, relayPath, accessTokenSupplier, metrics);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void channelActive(ChannelHandlerContext ctx) throws Exception {
|
||||||
|
metrics.registerActiveConnection("whois", "none", ctx.channel());
|
||||||
|
super.channelActive(ctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected FullHttpRequest decodeFullHttpRequest(ByteBuf byteBuf) {
|
||||||
|
FullHttpRequest request = super.decodeFullHttpRequest(byteBuf);
|
||||||
|
request
|
||||||
|
.headers()
|
||||||
|
// Close connection after a response is received, per RFC-3912
|
||||||
|
// https://tools.ietf.org/html/rfc3912
|
||||||
|
.set(HttpHeaderNames.CONNECTION, HttpHeaderValues.CLOSE)
|
||||||
|
.set(HttpHeaderNames.CONTENT_TYPE, HttpHeaderValues.TEXT_PLAIN)
|
||||||
|
.set(HttpHeaderNames.ACCEPT, HttpHeaderValues.TEXT_PLAIN);
|
||||||
|
return request;
|
||||||
|
}
|
||||||
|
}
|
125
java/google/registry/proxy/metric/BackendMetrics.java
Normal file
125
java/google/registry/proxy/metric/BackendMetrics.java
Normal file
|
@ -0,0 +1,125 @@
|
||||||
|
// Copyright 2017 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.proxy.metric;
|
||||||
|
|
||||||
|
import com.google.common.collect.ImmutableSet;
|
||||||
|
import google.registry.monitoring.metrics.CustomFitter;
|
||||||
|
import google.registry.monitoring.metrics.EventMetric;
|
||||||
|
import google.registry.monitoring.metrics.ExponentialFitter;
|
||||||
|
import google.registry.monitoring.metrics.FibonacciFitter;
|
||||||
|
import google.registry.monitoring.metrics.IncrementableMetric;
|
||||||
|
import google.registry.monitoring.metrics.LabelDescriptor;
|
||||||
|
import google.registry.monitoring.metrics.MetricRegistryImpl;
|
||||||
|
import google.registry.util.NonFinalForTesting;
|
||||||
|
import io.netty.handler.codec.http.FullHttpRequest;
|
||||||
|
import io.netty.handler.codec.http.FullHttpResponse;
|
||||||
|
import javax.inject.Inject;
|
||||||
|
|
||||||
|
/** Backend metrics instrumentation. */
|
||||||
|
public class BackendMetrics {
|
||||||
|
|
||||||
|
// Maximum request size is defined in the config file, this is not realistic and we'd be out of
|
||||||
|
// memory when the size approach 1 GB.
|
||||||
|
private static final CustomFitter DEFAULT_SIZE_FITTER = FibonacciFitter.create(1073741824);
|
||||||
|
|
||||||
|
// Maximum 1 hour latency, this is not specified by the spec, but given we have a one hour idle
|
||||||
|
// timeout, it seems reasonable that maximum latency is set to 1 hour as well. If we are
|
||||||
|
// approaching anywhere near 1 hour latency, we'd be way out of SLO anyway.
|
||||||
|
private static final ExponentialFitter DEFAULT_LATENCY_FITTER =
|
||||||
|
ExponentialFitter.create(22, 2, 1.0);
|
||||||
|
|
||||||
|
private static final ImmutableSet<LabelDescriptor> LABELS =
|
||||||
|
ImmutableSet.of(
|
||||||
|
LabelDescriptor.create("protocol", "Name of the protocol."),
|
||||||
|
LabelDescriptor.create(
|
||||||
|
"client_cert_hash", "SHA256 hash of the client certificate, if available."));
|
||||||
|
|
||||||
|
static final IncrementableMetric requestsCounter =
|
||||||
|
MetricRegistryImpl.getDefault()
|
||||||
|
.newIncrementableMetric(
|
||||||
|
"/proxy/backend/requests",
|
||||||
|
"Total number of requests send to the backend.",
|
||||||
|
"Requests",
|
||||||
|
LABELS);
|
||||||
|
|
||||||
|
static final IncrementableMetric responsesCounter =
|
||||||
|
MetricRegistryImpl.getDefault()
|
||||||
|
.newIncrementableMetric(
|
||||||
|
"/proxy/backend/responses",
|
||||||
|
"Total number of responses received by the backend.",
|
||||||
|
"Responses",
|
||||||
|
ImmutableSet.<LabelDescriptor>builder()
|
||||||
|
.addAll(LABELS)
|
||||||
|
.add(LabelDescriptor.create("status", "HTTP status code."))
|
||||||
|
.build());
|
||||||
|
|
||||||
|
static final EventMetric requestBytes =
|
||||||
|
MetricRegistryImpl.getDefault()
|
||||||
|
.newEventMetric(
|
||||||
|
"/proxy/backend/request_bytes",
|
||||||
|
"Size of the backend requests sent.",
|
||||||
|
"Bytes",
|
||||||
|
LABELS,
|
||||||
|
DEFAULT_SIZE_FITTER);
|
||||||
|
|
||||||
|
static final EventMetric responseBytes =
|
||||||
|
MetricRegistryImpl.getDefault()
|
||||||
|
.newEventMetric(
|
||||||
|
"/proxy/backend/response_bytes",
|
||||||
|
"Size of the backend responses received.",
|
||||||
|
"Bytes",
|
||||||
|
LABELS,
|
||||||
|
DEFAULT_SIZE_FITTER);
|
||||||
|
|
||||||
|
static final EventMetric latencyMs =
|
||||||
|
MetricRegistryImpl.getDefault()
|
||||||
|
.newEventMetric(
|
||||||
|
"/proxy/backend/latency_ms",
|
||||||
|
"Round-trip time between a request sent and its corresponding response received.",
|
||||||
|
"Milliseconds",
|
||||||
|
LABELS,
|
||||||
|
DEFAULT_LATENCY_FITTER);
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
BackendMetrics() {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resets all backend metrics.
|
||||||
|
*
|
||||||
|
* <p>This should only used in tests to clear out states. No production code should call this
|
||||||
|
* function.
|
||||||
|
*/
|
||||||
|
void resetMetric() {
|
||||||
|
requestBytes.reset();
|
||||||
|
requestsCounter.reset();
|
||||||
|
responseBytes.reset();
|
||||||
|
responsesCounter.reset();
|
||||||
|
latencyMs.reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonFinalForTesting
|
||||||
|
public void requestSent(String protocol, String certHash, FullHttpRequest request) {
|
||||||
|
requestsCounter.increment(protocol, certHash);
|
||||||
|
requestBytes.record(request.content().readableBytes(), protocol, certHash);
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonFinalForTesting
|
||||||
|
public void responseReceived(
|
||||||
|
String protocol, String certHash, FullHttpResponse response, long latency) {
|
||||||
|
latencyMs.record(latency, protocol, certHash);
|
||||||
|
responseBytes.record(response.content().readableBytes(), protocol, certHash);
|
||||||
|
responsesCounter.increment(protocol, certHash, response.status().toString());
|
||||||
|
}
|
||||||
|
}
|
99
java/google/registry/proxy/metric/FrontendMetrics.java
Normal file
99
java/google/registry/proxy/metric/FrontendMetrics.java
Normal file
|
@ -0,0 +1,99 @@
|
||||||
|
// Copyright 2017 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.proxy.metric;
|
||||||
|
|
||||||
|
import com.google.common.annotations.VisibleForTesting;
|
||||||
|
import com.google.common.collect.ImmutableList;
|
||||||
|
import com.google.common.collect.ImmutableMap;
|
||||||
|
import com.google.common.collect.ImmutableSet;
|
||||||
|
import google.registry.monitoring.metrics.IncrementableMetric;
|
||||||
|
import google.registry.monitoring.metrics.LabelDescriptor;
|
||||||
|
import google.registry.monitoring.metrics.Metric;
|
||||||
|
import google.registry.monitoring.metrics.MetricRegistryImpl;
|
||||||
|
import google.registry.util.NonFinalForTesting;
|
||||||
|
import io.netty.channel.Channel;
|
||||||
|
import io.netty.channel.group.ChannelGroup;
|
||||||
|
import io.netty.channel.group.DefaultChannelGroup;
|
||||||
|
import io.netty.util.concurrent.GlobalEventExecutor;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
import java.util.concurrent.ConcurrentMap;
|
||||||
|
import javax.inject.Inject;
|
||||||
|
|
||||||
|
/** Frontend metrics instrumentation. */
|
||||||
|
public class FrontendMetrics {
|
||||||
|
|
||||||
|
private static final ImmutableSet<LabelDescriptor> LABELS =
|
||||||
|
ImmutableSet.of(
|
||||||
|
LabelDescriptor.create("protocol", "Name of the protocol."),
|
||||||
|
LabelDescriptor.create(
|
||||||
|
"client_cert_hash", "SHA256 hash of the client certificate, if available."));
|
||||||
|
|
||||||
|
private static final ConcurrentMap<ImmutableList<String>, ChannelGroup> activeConnections =
|
||||||
|
new ConcurrentHashMap<>();
|
||||||
|
|
||||||
|
static final Metric<Long> activeConnectionsGauge =
|
||||||
|
MetricRegistryImpl.getDefault()
|
||||||
|
.newGauge(
|
||||||
|
"/proxy/frontend/active_connections",
|
||||||
|
"Number of active connections from clients to the proxy.",
|
||||||
|
"Connections",
|
||||||
|
LABELS,
|
||||||
|
() ->
|
||||||
|
activeConnections
|
||||||
|
.entrySet()
|
||||||
|
.stream()
|
||||||
|
.collect(
|
||||||
|
ImmutableMap.toImmutableMap(
|
||||||
|
Map.Entry::getKey, entry -> (long) entry.getValue().size())),
|
||||||
|
Long.class);
|
||||||
|
|
||||||
|
static final IncrementableMetric totalConnectionsCounter =
|
||||||
|
MetricRegistryImpl.getDefault()
|
||||||
|
.newIncrementableMetric(
|
||||||
|
"/proxy/frontend/total_connections",
|
||||||
|
"Total number connections ever made from clients to the proxy.",
|
||||||
|
"Connections",
|
||||||
|
LABELS);
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
public FrontendMetrics() {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resets all frontend metrics.
|
||||||
|
*
|
||||||
|
* <p>This should only be used in tests to reset states. Production code should not call this
|
||||||
|
* method.
|
||||||
|
*/
|
||||||
|
@VisibleForTesting
|
||||||
|
void resetMetrics() {
|
||||||
|
totalConnectionsCounter.reset();
|
||||||
|
activeConnections.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonFinalForTesting
|
||||||
|
public void registerActiveConnection(String protocol, String certHash, Channel channel) {
|
||||||
|
totalConnectionsCounter.increment(protocol, certHash);
|
||||||
|
ImmutableList<String> labels = ImmutableList.of(protocol, certHash);
|
||||||
|
ChannelGroup channelGroup;
|
||||||
|
if (activeConnections.containsKey(labels)) {
|
||||||
|
channelGroup = activeConnections.get(labels);
|
||||||
|
} else {
|
||||||
|
channelGroup = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);
|
||||||
|
activeConnections.put(labels, channelGroup);
|
||||||
|
}
|
||||||
|
channelGroup.add(channel);
|
||||||
|
}
|
||||||
|
}
|
4
java/google/registry/proxy/resources/hello.xml
Normal file
4
java/google/registry/proxy/resources/hello.xml
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<epp xmlns="urn:ietf:params:xml:ns:epp-1.0">
|
||||||
|
<hello/>
|
||||||
|
</epp>
|
|
@ -66,7 +66,9 @@ def domain_registry_repositories(
|
||||||
omit_com_google_dagger_compiler=False,
|
omit_com_google_dagger_compiler=False,
|
||||||
omit_com_google_dagger_producers=False,
|
omit_com_google_dagger_producers=False,
|
||||||
omit_com_google_errorprone_error_prone_annotations=False,
|
omit_com_google_errorprone_error_prone_annotations=False,
|
||||||
|
omit_com_google_errorprone_javac_shaded=False,
|
||||||
omit_com_google_gdata_core=False,
|
omit_com_google_gdata_core=False,
|
||||||
|
omit_com_google_googlejavaformat_google_java_format=False,
|
||||||
omit_com_google_guava=False,
|
omit_com_google_guava=False,
|
||||||
omit_com_google_guava_testlib=False,
|
omit_com_google_guava_testlib=False,
|
||||||
omit_com_google_http_client=False,
|
omit_com_google_http_client=False,
|
||||||
|
@ -86,6 +88,7 @@ def domain_registry_repositories(
|
||||||
omit_com_googlecode_json_simple=False,
|
omit_com_googlecode_json_simple=False,
|
||||||
omit_com_ibm_icu_icu4j=False,
|
omit_com_ibm_icu_icu4j=False,
|
||||||
omit_com_jcraft_jzlib=False,
|
omit_com_jcraft_jzlib=False,
|
||||||
|
omit_com_squareup_javapoet=False,
|
||||||
omit_com_squareup_javawriter=False,
|
omit_com_squareup_javawriter=False,
|
||||||
omit_com_sun_xml_bind_jaxb_core=False,
|
omit_com_sun_xml_bind_jaxb_core=False,
|
||||||
omit_com_sun_xml_bind_jaxb_impl=False,
|
omit_com_sun_xml_bind_jaxb_impl=False,
|
||||||
|
@ -94,8 +97,17 @@ def domain_registry_repositories(
|
||||||
omit_commons_codec=False,
|
omit_commons_codec=False,
|
||||||
omit_commons_logging=False,
|
omit_commons_logging=False,
|
||||||
omit_dnsjava=False,
|
omit_dnsjava=False,
|
||||||
|
omit_io_netty_buffer=False,
|
||||||
|
omit_io_netty_codec=False,
|
||||||
|
omit_io_netty_codec_http=False,
|
||||||
|
omit_io_netty_common=False,
|
||||||
|
omit_io_netty_handler=False,
|
||||||
|
omit_io_netty_resolver=False,
|
||||||
|
omit_io_netty_tcnative=False,
|
||||||
|
omit_io_netty_transport=False,
|
||||||
omit_it_unimi_dsi_fastutil=False,
|
omit_it_unimi_dsi_fastutil=False,
|
||||||
omit_javax_activation=False,
|
omit_javax_activation=False,
|
||||||
|
omit_javax_annotation_jsr250_api=False,
|
||||||
omit_javax_inject=False,
|
omit_javax_inject=False,
|
||||||
omit_javax_mail=False,
|
omit_javax_mail=False,
|
||||||
omit_javax_servlet_api=False,
|
omit_javax_servlet_api=False,
|
||||||
|
@ -227,8 +239,12 @@ def domain_registry_repositories(
|
||||||
com_google_dagger_producers()
|
com_google_dagger_producers()
|
||||||
if not omit_com_google_errorprone_error_prone_annotations:
|
if not omit_com_google_errorprone_error_prone_annotations:
|
||||||
com_google_errorprone_error_prone_annotations()
|
com_google_errorprone_error_prone_annotations()
|
||||||
|
if not omit_com_google_errorprone_javac_shaded:
|
||||||
|
com_google_errorprone_javac_shaded()
|
||||||
if not omit_com_google_gdata_core:
|
if not omit_com_google_gdata_core:
|
||||||
com_google_gdata_core()
|
com_google_gdata_core()
|
||||||
|
if not omit_com_google_googlejavaformat_google_java_format:
|
||||||
|
com_google_googlejavaformat_google_java_format()
|
||||||
if not omit_com_google_guava:
|
if not omit_com_google_guava:
|
||||||
com_google_guava()
|
com_google_guava()
|
||||||
if not omit_com_google_guava_testlib:
|
if not omit_com_google_guava_testlib:
|
||||||
|
@ -267,6 +283,8 @@ def domain_registry_repositories(
|
||||||
com_ibm_icu_icu4j()
|
com_ibm_icu_icu4j()
|
||||||
if not omit_com_jcraft_jzlib:
|
if not omit_com_jcraft_jzlib:
|
||||||
com_jcraft_jzlib()
|
com_jcraft_jzlib()
|
||||||
|
if not omit_com_squareup_javapoet:
|
||||||
|
com_squareup_javapoet()
|
||||||
if not omit_com_squareup_javawriter:
|
if not omit_com_squareup_javawriter:
|
||||||
com_squareup_javawriter()
|
com_squareup_javawriter()
|
||||||
if not omit_com_sun_xml_bind_jaxb_core:
|
if not omit_com_sun_xml_bind_jaxb_core:
|
||||||
|
@ -283,10 +301,28 @@ def domain_registry_repositories(
|
||||||
commons_logging()
|
commons_logging()
|
||||||
if not omit_dnsjava:
|
if not omit_dnsjava:
|
||||||
dnsjava()
|
dnsjava()
|
||||||
|
if not omit_io_netty_buffer:
|
||||||
|
io_netty_buffer()
|
||||||
|
if not omit_io_netty_codec:
|
||||||
|
io_netty_codec()
|
||||||
|
if not omit_io_netty_codec_http:
|
||||||
|
io_netty_codec_http()
|
||||||
|
if not omit_io_netty_common:
|
||||||
|
io_netty_common()
|
||||||
|
if not omit_io_netty_handler:
|
||||||
|
io_netty_handler()
|
||||||
|
if not omit_io_netty_resolver:
|
||||||
|
io_netty_resolver()
|
||||||
|
if not omit_io_netty_tcnative:
|
||||||
|
io_netty_tcnative()
|
||||||
|
if not omit_io_netty_transport:
|
||||||
|
io_netty_transport()
|
||||||
if not omit_it_unimi_dsi_fastutil:
|
if not omit_it_unimi_dsi_fastutil:
|
||||||
it_unimi_dsi_fastutil()
|
it_unimi_dsi_fastutil()
|
||||||
if not omit_javax_activation:
|
if not omit_javax_activation:
|
||||||
javax_activation()
|
javax_activation()
|
||||||
|
if not omit_javax_annotation_jsr250_api:
|
||||||
|
javax_annotation_jsr250_api()
|
||||||
if not omit_javax_inject:
|
if not omit_javax_inject:
|
||||||
javax_inject()
|
javax_inject()
|
||||||
if not omit_javax_mail:
|
if not omit_javax_mail:
|
||||||
|
@ -888,21 +924,21 @@ def com_google_auto_value():
|
||||||
def com_google_code_findbugs_jsr305():
|
def com_google_code_findbugs_jsr305():
|
||||||
java_import_external(
|
java_import_external(
|
||||||
name = "com_google_code_findbugs_jsr305",
|
name = "com_google_code_findbugs_jsr305",
|
||||||
jar_sha256 = "905721a0eea90a81534abb7ee6ef4ea2e5e645fa1def0a5cd88402df1b46c9ed",
|
|
||||||
jar_urls = [
|
|
||||||
"http://domain-registry-maven.storage.googleapis.com/repo1.maven.org/maven2/com/google/code/findbugs/jsr305/1.3.9/jsr305-1.3.9.jar",
|
|
||||||
"http://repo1.maven.org/maven2/com/google/code/findbugs/jsr305/1.3.9/jsr305-1.3.9.jar",
|
|
||||||
],
|
|
||||||
licenses = ["notice"], # The Apache Software License, Version 2.0
|
licenses = ["notice"], # The Apache Software License, Version 2.0
|
||||||
|
jar_sha256 = "c885ce34249682bc0236b4a7d56efcc12048e6135a5baf7a9cde8ad8cda13fcd",
|
||||||
|
jar_urls = [
|
||||||
|
"http://repo1.maven.org/maven2/com/google/code/findbugs/jsr305/3.0.1/jsr305-3.0.1.jar",
|
||||||
|
"http://maven.ibiblio.org/maven2/com/google/code/findbugs/jsr305/3.0.1/jsr305-3.0.1.jar",
|
||||||
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
def com_google_dagger():
|
def com_google_dagger():
|
||||||
java_import_external(
|
java_import_external(
|
||||||
name = "com_google_dagger",
|
name = "com_google_dagger",
|
||||||
jar_sha256 = "5070e1dff5c551a4908ba7b93125c0243de2a688aed3d2f475357d86d9d7c0ad",
|
jar_sha256 = "b2142693bc7413f0b74330f0a92ca44ea95a12a22b659972ed6aa9832e8352e4",
|
||||||
jar_urls = [
|
jar_urls = [
|
||||||
"http://domain-registry-maven.storage.googleapis.com/repo1.maven.org/maven2/com/google/dagger/dagger/2.8/dagger-2.8.jar",
|
"http://repo1.maven.org/maven2/com/google/dagger/dagger/2.13/dagger-2.13.jar",
|
||||||
"http://repo1.maven.org/maven2/com/google/dagger/dagger/2.8/dagger-2.8.jar",
|
"http://maven.ibiblio.org/maven2/com/google/dagger/dagger/2.13/dagger-2.13.jar",
|
||||||
],
|
],
|
||||||
licenses = ["notice"], # Apache 2.0
|
licenses = ["notice"], # Apache 2.0
|
||||||
deps = ["@javax_inject"],
|
deps = ["@javax_inject"],
|
||||||
|
@ -922,17 +958,21 @@ def com_google_dagger():
|
||||||
def com_google_dagger_compiler():
|
def com_google_dagger_compiler():
|
||||||
java_import_external(
|
java_import_external(
|
||||||
name = "com_google_dagger_compiler",
|
name = "com_google_dagger_compiler",
|
||||||
jar_sha256 = "7b2686f94907868c5364e9965601ffe2f020ba4af1849ad9b57dad5fe3fa6242",
|
jar_sha256 = "8b711253c9cbb58bd2c019cb38afb32ee79f283e1bb3030c8c85b645c7a6d25f",
|
||||||
jar_urls = [
|
jar_urls = [
|
||||||
"http://domain-registry-maven.storage.googleapis.com/repo1.maven.org/maven2/com/google/dagger/dagger-compiler/2.8/dagger-compiler-2.8.jar",
|
"http://maven.ibiblio.org/maven2/com/google/dagger/dagger-compiler/2.13/dagger-compiler-2.13.jar",
|
||||||
"http://repo1.maven.org/maven2/com/google/dagger/dagger-compiler/2.8/dagger-compiler-2.8.jar",
|
"http://repo1.maven.org/maven2/com/google/dagger/dagger-compiler/2.13/dagger-compiler-2.13.jar",
|
||||||
],
|
],
|
||||||
licenses = ["notice"], # Apache 2.0
|
licenses = ["notice"], # Apache 2.0
|
||||||
deps = [
|
deps = [
|
||||||
"@com_google_code_findbugs_jsr305",
|
|
||||||
"@com_google_dagger//:runtime",
|
"@com_google_dagger//:runtime",
|
||||||
"@com_google_dagger_producers//:runtime",
|
"@com_google_dagger_producers//:runtime",
|
||||||
|
"@com_google_code_findbugs_jsr305",
|
||||||
|
"@com_google_googlejavaformat_google_java_format",
|
||||||
"@com_google_guava",
|
"@com_google_guava",
|
||||||
|
"@com_squareup_javapoet",
|
||||||
|
"@javax_annotation_jsr250_api",
|
||||||
|
"@javax_inject",
|
||||||
],
|
],
|
||||||
extra_build_file_content = "\n".join([
|
extra_build_file_content = "\n".join([
|
||||||
"java_plugin(",
|
"java_plugin(",
|
||||||
|
@ -952,15 +992,17 @@ def com_google_dagger_compiler():
|
||||||
def com_google_dagger_producers():
|
def com_google_dagger_producers():
|
||||||
java_import_external(
|
java_import_external(
|
||||||
name = "com_google_dagger_producers",
|
name = "com_google_dagger_producers",
|
||||||
jar_sha256 = "1e4043e85f67de381d19e22c7932aaf7ff1611091be7e1aaae93f2c37f331cf2",
|
jar_sha256 = "cf35b21c634939917eee9ffcd72a9f5f6e261ad57a4c0f0d15cf6f1430262bb0",
|
||||||
jar_urls = [
|
jar_urls = [
|
||||||
"http://domain-registry-maven.storage.googleapis.com/repo1.maven.org/maven2/com/google/dagger/dagger-producers/2.8/dagger-producers-2.8.jar",
|
"http://repo1.maven.org/maven2/com/google/dagger/dagger-producers/2.13/dagger-producers-2.13.jar",
|
||||||
"http://repo1.maven.org/maven2/com/google/dagger/dagger-producers/2.8/dagger-producers-2.8.jar",
|
"http://maven.ibiblio.org/maven2/com/google/dagger/dagger-producers/2.13/dagger-producers-2.13.jar",
|
||||||
],
|
],
|
||||||
licenses = ["notice"], # Apache 2.0
|
licenses = ["notice"], # Apache 2.0
|
||||||
deps = [
|
deps = [
|
||||||
"@com_google_dagger//:runtime",
|
"@com_google_dagger//:runtime",
|
||||||
|
"@com_google_code_findbugs_jsr305",
|
||||||
"@com_google_guava",
|
"@com_google_guava",
|
||||||
|
"@javax_inject",
|
||||||
],
|
],
|
||||||
generated_rule_name = "runtime",
|
generated_rule_name = "runtime",
|
||||||
extra_build_file_content = "\n".join([
|
extra_build_file_content = "\n".join([
|
||||||
|
@ -986,6 +1028,56 @@ def com_google_errorprone_error_prone_annotations():
|
||||||
licenses = ["notice"], # Apache 2.0
|
licenses = ["notice"], # Apache 2.0
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def com_google_errorprone_javac_shaded():
|
||||||
|
java_import_external(
|
||||||
|
name = "com_google_errorprone_javac_shaded",
|
||||||
|
# GNU General Public License, version 2, with the Classpath Exception
|
||||||
|
# http://openjdk.java.net/legal/gplv2+ce.html
|
||||||
|
licenses = ["TODO"],
|
||||||
|
jar_sha256 = "65bfccf60986c47fbc17c9ebab0be626afc41741e0a6ec7109e0768817a36f30",
|
||||||
|
jar_urls = [
|
||||||
|
"http://repo1.maven.org/maven2/com/google/errorprone/javac-shaded/9-dev-r4023-3/javac-shaded-9-dev-r4023-3.jar",
|
||||||
|
"http://maven.ibiblio.org/maven2/com/google/errorprone/javac-shaded/9-dev-r4023-3/javac-shaded-9-dev-r4023-3.jar",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
def com_google_googlejavaformat_google_java_format():
|
||||||
|
java_import_external(
|
||||||
|
name = "com_google_googlejavaformat_google_java_format",
|
||||||
|
licenses = ["notice"], # The Apache Software License, Version 2.0
|
||||||
|
jar_sha256 = "39d18ec9ab610097074bf49e971285488eaf5d0bc2369df0a0d5a3f9f9de2faa",
|
||||||
|
jar_urls = [
|
||||||
|
"http://maven.ibiblio.org/maven2/com/google/googlejavaformat/google-java-format/1.4/google-java-format-1.4.jar",
|
||||||
|
"http://repo1.maven.org/maven2/com/google/googlejavaformat/google-java-format/1.4/google-java-format-1.4.jar",
|
||||||
|
],
|
||||||
|
deps = [
|
||||||
|
"@com_google_guava",
|
||||||
|
"@com_google_errorprone_javac_shaded",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
def com_squareup_javapoet():
|
||||||
|
java_import_external(
|
||||||
|
name = "com_squareup_javapoet",
|
||||||
|
licenses = ["notice"], # Apache 2.0
|
||||||
|
jar_sha256 = "8e108c92027bb428196f10fa11cffbe589f7648a6af2016d652279385fdfd789",
|
||||||
|
jar_urls = [
|
||||||
|
"http://maven.ibiblio.org/maven2/com/squareup/javapoet/1.8.0/javapoet-1.8.0.jar",
|
||||||
|
"http://repo1.maven.org/maven2/com/squareup/javapoet/1.8.0/javapoet-1.8.0.jar",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
def javax_annotation_jsr250_api():
|
||||||
|
java_import_external(
|
||||||
|
name = "javax_annotation_jsr250_api",
|
||||||
|
licenses = ["reciprocal"], # COMMON DEVELOPMENT AND DISTRIBUTION LICENSE (CDDL) Version 1.0
|
||||||
|
jar_sha256 = "a1a922d0d9b6d183ed3800dfac01d1e1eb159f0e8c6f94736931c1def54a941f",
|
||||||
|
jar_urls = [
|
||||||
|
"http://repo1.maven.org/maven2/javax/annotation/jsr250-api/1.0/jsr250-api-1.0.jar",
|
||||||
|
"http://maven.ibiblio.org/maven2/javax/annotation/jsr250-api/1.0/jsr250-api-1.0.jar",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
def com_google_gdata_core():
|
def com_google_gdata_core():
|
||||||
java_import_external(
|
java_import_external(
|
||||||
name = "com_google_gdata_core",
|
name = "com_google_gdata_core",
|
||||||
|
@ -2147,6 +2239,102 @@ def xpp3():
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def io_netty_buffer():
|
||||||
|
java_import_external(
|
||||||
|
name = "io_netty_buffer",
|
||||||
|
licenses = ["notice"], # Apache License, Version 2.0
|
||||||
|
jar_sha256 = "b24a28e2129fc11e1f6124ebf93725d1f9c0904ea679d261da7b2e21d4c8615e",
|
||||||
|
jar_urls = [
|
||||||
|
"http://maven.ibiblio.org/maven2/io/netty/netty-buffer/4.1.17.Final/netty-buffer-4.1.17.Final.jar",
|
||||||
|
"http://repo1.maven.org/maven2/io/netty/netty-buffer/4.1.17.Final/netty-buffer-4.1.17.Final.jar",
|
||||||
|
],
|
||||||
|
deps = ["@io_netty_common"],
|
||||||
|
)
|
||||||
|
|
||||||
|
def io_netty_codec():
|
||||||
|
java_import_external(
|
||||||
|
name = "io_netty_codec",
|
||||||
|
licenses = ["notice"], # Apache License, Version 2.0
|
||||||
|
jar_sha256 = "790ce1b7694fc41663131579d776a370e332e3b3fe2fe6543662fd5a40a948e1",
|
||||||
|
jar_urls = [
|
||||||
|
"http://repo1.maven.org/maven2/io/netty/netty-codec/4.1.17.Final/netty-codec-4.1.17.Final.jar",
|
||||||
|
"http://maven.ibiblio.org/maven2/io/netty/netty-codec/4.1.17.Final/netty-codec-4.1.17.Final.jar",
|
||||||
|
],
|
||||||
|
deps = ["@io_netty_transport"],
|
||||||
|
)
|
||||||
|
|
||||||
|
def io_netty_codec_http():
|
||||||
|
java_import_external(
|
||||||
|
name = "io_netty_codec_http",
|
||||||
|
licenses = ["notice"], # Apache License, Version 2.0
|
||||||
|
jar_sha256 = "fc05d02755c5d204ccc848be8399ef5d48d5a80da9b93f075287c57eb9381e5b",
|
||||||
|
jar_urls = [
|
||||||
|
"http://repo1.maven.org/maven2/io/netty/netty-codec-http/4.1.17.Final/netty-codec-http-4.1.17.Final.jar",
|
||||||
|
"http://maven.ibiblio.org/maven2/io/netty/netty-codec-http/4.1.17.Final/netty-codec-http-4.1.17.Final.jar",
|
||||||
|
],
|
||||||
|
deps = ["@io_netty_codec"],
|
||||||
|
)
|
||||||
|
|
||||||
|
def io_netty_common():
|
||||||
|
java_import_external(
|
||||||
|
name = "io_netty_common",
|
||||||
|
licenses = ["notice"], # Apache License, Version 2.0
|
||||||
|
jar_sha256 = "dddabdec01959180da44129d130301b84c23b473411288f143d5e29e0b098d26",
|
||||||
|
jar_urls = [
|
||||||
|
"http://repo1.maven.org/maven2/io/netty/netty-common/4.1.17.Final/netty-common-4.1.17.Final.jar",
|
||||||
|
"http://maven.ibiblio.org/maven2/io/netty/netty-common/4.1.17.Final/netty-common-4.1.17.Final.jar",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
def io_netty_handler():
|
||||||
|
java_import_external(
|
||||||
|
name = "io_netty_handler",
|
||||||
|
licenses = ["notice"], # Apache License, Version 2.0
|
||||||
|
jar_sha256 = "85bada604fe14bc358da7b140583264a88d7a45ca12daba1216c4225aadb0c7b",
|
||||||
|
jar_urls = [
|
||||||
|
"http://repo1.maven.org/maven2/io/netty/netty-handler/4.1.17.Final/netty-handler-4.1.17.Final.jar",
|
||||||
|
"http://maven.ibiblio.org/maven2/io/netty/netty-handler/4.1.17.Final/netty-handler-4.1.17.Final.jar",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
def io_netty_resolver():
|
||||||
|
java_import_external(
|
||||||
|
name = "io_netty_resolver",
|
||||||
|
licenses = ["notice"], # Apache License, Version 2.0
|
||||||
|
jar_sha256 = "082ac49149cb72c675c7ed1615ba35923d3167e65bfb37c4a1422ec499137cb1",
|
||||||
|
jar_urls = [
|
||||||
|
"http://maven.ibiblio.org/maven2/io/netty/netty-resolver/4.1.17.Final/netty-resolver-4.1.17.Final.jar",
|
||||||
|
"http://repo1.maven.org/maven2/io/netty/netty-resolver/4.1.17.Final/netty-resolver-4.1.17.Final.jar",
|
||||||
|
],
|
||||||
|
deps = ["@io_netty_common"],
|
||||||
|
)
|
||||||
|
|
||||||
|
def io_netty_tcnative():
|
||||||
|
java_import_external(
|
||||||
|
name = "io_netty_tcnative",
|
||||||
|
licenses = ["notice"], # Apache License, Version 2.0
|
||||||
|
jar_sha256 = "cd49317267a8f2fd617075d22e25ceb3aef98e6b64bd6f66cca95f8825cdc1f3",
|
||||||
|
jar_urls = [
|
||||||
|
"http://repo1.maven.org/maven2/io/netty/netty-tcnative/2.0.7.Final/netty-tcnative-2.0.7.Final.jar",
|
||||||
|
"http://maven.ibiblio.org/maven2/io/netty/netty-tcnative/2.0.7.Final/netty-tcnative-2.0.7.Final.jar",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
def io_netty_transport():
|
||||||
|
java_import_external(
|
||||||
|
name = "io_netty_transport",
|
||||||
|
licenses = ["notice"], # Apache License, Version 2.0
|
||||||
|
jar_sha256 = "60763426c79dd930c70d0da95e474f662bd17a58d3d57b332696d089cf208089",
|
||||||
|
jar_urls = [
|
||||||
|
"http://maven.ibiblio.org/maven2/io/netty/netty-transport/4.1.17.Final/netty-transport-4.1.17.Final.jar",
|
||||||
|
"http://repo1.maven.org/maven2/io/netty/netty-transport/4.1.17.Final/netty-transport-4.1.17.Final.jar",
|
||||||
|
],
|
||||||
|
deps = [
|
||||||
|
"@io_netty_buffer",
|
||||||
|
"@io_netty_resolver",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
def _check_bazel_version(project, bazel_version):
|
def _check_bazel_version(project, bazel_version):
|
||||||
if "bazel_version" not in dir(native):
|
if "bazel_version" not in dir(native):
|
||||||
fail("%s requires Bazel >=%s but was <0.2.1" % (project, bazel_version))
|
fail("%s requires Bazel >=%s but was <0.2.1" % (project, bazel_version))
|
||||||
|
|
45
javatests/google/registry/proxy/BUILD
Normal file
45
javatests/google/registry/proxy/BUILD
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
package(
|
||||||
|
default_testonly = 1,
|
||||||
|
default_visibility = ["//java/google/registry:registry_project"],
|
||||||
|
)
|
||||||
|
|
||||||
|
licenses(["notice"]) # Apache 2.0
|
||||||
|
|
||||||
|
load("//java/com/google/testing/builddefs:GenTestRules.bzl", "GenTestRules")
|
||||||
|
|
||||||
|
java_library(
|
||||||
|
name = "proxy",
|
||||||
|
srcs = glob(["**/*.java"]),
|
||||||
|
resources = glob(["testdata/*.xml"]),
|
||||||
|
deps = [
|
||||||
|
"//java/google/registry/monitoring/metrics",
|
||||||
|
"//java/google/registry/monitoring/metrics/contrib",
|
||||||
|
"//java/google/registry/proxy",
|
||||||
|
"//java/google/registry/util",
|
||||||
|
"//javatests/google/registry/testing",
|
||||||
|
"@com_beust_jcommander",
|
||||||
|
"@com_google_dagger",
|
||||||
|
"@com_google_guava",
|
||||||
|
"@com_google_truth",
|
||||||
|
"@com_google_truth_extensions_truth_java8_extension",
|
||||||
|
"@io_netty_buffer",
|
||||||
|
"@io_netty_codec",
|
||||||
|
"@io_netty_codec_http",
|
||||||
|
"@io_netty_common",
|
||||||
|
"@io_netty_handler",
|
||||||
|
"@io_netty_transport",
|
||||||
|
"@joda_time",
|
||||||
|
"@junit",
|
||||||
|
"@org_bouncycastle_bcpkix_jdk15on",
|
||||||
|
"@org_mockito_all",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
GenTestRules(
|
||||||
|
name = "GeneratedTestRules",
|
||||||
|
test_files = glob(
|
||||||
|
["**/*Test.java"],
|
||||||
|
exclude = ["ProtocolModuleTest.java"],
|
||||||
|
),
|
||||||
|
deps = [":proxy"],
|
||||||
|
)
|
158
javatests/google/registry/proxy/CertificateModuleTest.java
Normal file
158
javatests/google/registry/proxy/CertificateModuleTest.java
Normal file
|
@ -0,0 +1,158 @@
|
||||||
|
// Copyright 2017 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.proxy;
|
||||||
|
|
||||||
|
import static com.google.common.truth.Truth.assertThat;
|
||||||
|
import static google.registry.proxy.handler.SslInitializerTestUtils.getKeyPair;
|
||||||
|
import static google.registry.proxy.handler.SslInitializerTestUtils.signKeyPair;
|
||||||
|
import static java.nio.charset.StandardCharsets.UTF_8;
|
||||||
|
import static org.junit.Assert.fail;
|
||||||
|
|
||||||
|
import dagger.BindsInstance;
|
||||||
|
import dagger.Component;
|
||||||
|
import google.registry.proxy.ProxyModule.PemBytes;
|
||||||
|
import io.netty.handler.ssl.util.SelfSignedCertificate;
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
|
import java.io.OutputStreamWriter;
|
||||||
|
import java.security.KeyPair;
|
||||||
|
import java.security.PrivateKey;
|
||||||
|
import java.security.cert.Certificate;
|
||||||
|
import java.security.cert.X509Certificate;
|
||||||
|
import javax.inject.Named;
|
||||||
|
import javax.inject.Singleton;
|
||||||
|
import org.bouncycastle.openssl.PEMWriter;
|
||||||
|
import org.junit.Before;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.junit.runner.RunWith;
|
||||||
|
import org.junit.runners.JUnit4;
|
||||||
|
|
||||||
|
/** Unit tests for {@link CertificateModule}. */
|
||||||
|
@RunWith(JUnit4.class)
|
||||||
|
public class CertificateModuleTest {
|
||||||
|
|
||||||
|
private SelfSignedCertificate ssc;
|
||||||
|
private PrivateKey key;
|
||||||
|
private Certificate cert;
|
||||||
|
private TestComponent component;
|
||||||
|
|
||||||
|
private static byte[] getPemBytes(Object... objects) throws Exception {
|
||||||
|
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
|
||||||
|
try (PEMWriter pemWriter =
|
||||||
|
new PEMWriter(new OutputStreamWriter(byteArrayOutputStream, UTF_8))) {
|
||||||
|
for (Object object : objects) {
|
||||||
|
pemWriter.writeObject(object);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return byteArrayOutputStream.toByteArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Create a component with bindings to the given bytes[] as the contents from a PEM file. */
|
||||||
|
private TestComponent createComponent(byte[] bytes) {
|
||||||
|
return DaggerCertificateModuleTest_TestComponent.builder()
|
||||||
|
.pemBytes(PemBytes.create(bytes))
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Before
|
||||||
|
public void setUp() throws Exception {
|
||||||
|
ssc = new SelfSignedCertificate();
|
||||||
|
KeyPair keyPair = getKeyPair();
|
||||||
|
key = keyPair.getPrivate();
|
||||||
|
cert = signKeyPair(ssc, keyPair, "example.tld");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSuccess() throws Exception {
|
||||||
|
byte[] pemBytes = getPemBytes(cert, ssc.cert(), key);
|
||||||
|
component = createComponent(pemBytes);
|
||||||
|
assertThat(component.privateKey()).isEqualTo(key);
|
||||||
|
assertThat(component.certificates()).asList().containsExactly(cert, ssc.cert()).inOrder();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSuccess_certificateChainNotContinuous() throws Exception {
|
||||||
|
byte[] pemBytes = getPemBytes(cert, key, ssc.cert());
|
||||||
|
component = createComponent(pemBytes);
|
||||||
|
assertThat(component.privateKey()).isEqualTo(key);
|
||||||
|
assertThat(component.certificates()).asList().containsExactly(cert, ssc.cert()).inOrder();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testFailure_noPrivateKey() throws Exception {
|
||||||
|
byte[] pemBytes = getPemBytes(cert, ssc.cert());
|
||||||
|
component = createComponent(pemBytes);
|
||||||
|
try {
|
||||||
|
component.privateKey();
|
||||||
|
fail("Expect IllegalStateException.");
|
||||||
|
} catch (IllegalStateException e) {
|
||||||
|
assertThat(e).hasMessageThat().contains("0 keys are found");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testFailure_twoPrivateKeys() throws Exception {
|
||||||
|
byte[] pemBytes = getPemBytes(cert, ssc.cert(), key, ssc.key());
|
||||||
|
component = createComponent(pemBytes);
|
||||||
|
try {
|
||||||
|
component.privateKey();
|
||||||
|
fail("Expect IllegalStateException.");
|
||||||
|
} catch (IllegalStateException e) {
|
||||||
|
assertThat(e).hasMessageThat().contains("2 keys are found");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testFailure_certificatesOutOfOrder() throws Exception {
|
||||||
|
byte[] pemBytes = getPemBytes(ssc.cert(), cert, key);
|
||||||
|
component = createComponent(pemBytes);
|
||||||
|
try {
|
||||||
|
component.certificates();
|
||||||
|
fail("Expect IllegalStateException.");
|
||||||
|
} catch (IllegalStateException e) {
|
||||||
|
assertThat(e).hasMessageThat().contains("is not signed by");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testFailure_noCertificates() throws Exception {
|
||||||
|
byte[] pemBytes = getPemBytes(key);
|
||||||
|
component = createComponent(pemBytes);
|
||||||
|
try {
|
||||||
|
component.certificates();
|
||||||
|
fail("Expect IllegalStateException.");
|
||||||
|
} catch (IllegalStateException e) {
|
||||||
|
assertThat(e).hasMessageThat().contains("No certificates");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Singleton
|
||||||
|
@Component(modules = {CertificateModule.class})
|
||||||
|
interface TestComponent {
|
||||||
|
|
||||||
|
PrivateKey privateKey();
|
||||||
|
|
||||||
|
@Named("eppServerCertificates")
|
||||||
|
X509Certificate[] certificates();
|
||||||
|
|
||||||
|
@Component.Builder
|
||||||
|
interface Builder {
|
||||||
|
|
||||||
|
@BindsInstance
|
||||||
|
Builder pemBytes(PemBytes pemBytes);
|
||||||
|
|
||||||
|
TestComponent build();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
251
javatests/google/registry/proxy/EppProtocolModuleTest.java
Normal file
251
javatests/google/registry/proxy/EppProtocolModuleTest.java
Normal file
|
@ -0,0 +1,251 @@
|
||||||
|
// Copyright 2017 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.proxy;
|
||||||
|
|
||||||
|
import static com.google.common.truth.Truth.assertThat;
|
||||||
|
import static google.registry.proxy.handler.ProxyProtocolHandler.REMOTE_ADDRESS_KEY;
|
||||||
|
import static google.registry.proxy.handler.SslServerInitializer.CLIENT_CERTIFICATE_PROMISE_KEY;
|
||||||
|
import static google.registry.util.ResourceUtils.readResourceBytes;
|
||||||
|
import static google.registry.util.X509Utils.getCertificateHash;
|
||||||
|
import static java.nio.charset.StandardCharsets.UTF_8;
|
||||||
|
|
||||||
|
import google.registry.testing.FakeClock;
|
||||||
|
import io.netty.buffer.ByteBuf;
|
||||||
|
import io.netty.buffer.Unpooled;
|
||||||
|
import io.netty.channel.embedded.EmbeddedChannel;
|
||||||
|
import io.netty.handler.codec.http.FullHttpRequest;
|
||||||
|
import io.netty.handler.codec.http.FullHttpResponse;
|
||||||
|
import io.netty.handler.codec.http.HttpResponseStatus;
|
||||||
|
import io.netty.handler.codec.http.cookie.Cookie;
|
||||||
|
import io.netty.handler.codec.http.cookie.DefaultCookie;
|
||||||
|
import io.netty.handler.ssl.util.SelfSignedCertificate;
|
||||||
|
import io.netty.util.concurrent.Promise;
|
||||||
|
import java.security.cert.X509Certificate;
|
||||||
|
import org.junit.Before;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.junit.runner.RunWith;
|
||||||
|
import org.junit.runners.JUnit4;
|
||||||
|
|
||||||
|
/** End-to-end tests for {@link EppProtocolModule}. */
|
||||||
|
@RunWith(JUnit4.class)
|
||||||
|
public class EppProtocolModuleTest extends ProtocolModuleTest {
|
||||||
|
|
||||||
|
private static final int HEADER_LENGTH = 4;
|
||||||
|
|
||||||
|
private static final String CLIENT_ADDRESS = "epp.client.tld";
|
||||||
|
|
||||||
|
private static final byte[] HELLO_BYTES =
|
||||||
|
("<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>\n"
|
||||||
|
+ "<epp xmlns=\"urn:ietf:params:xml:ns:epp-1.0\">\n"
|
||||||
|
+ " <hello/>\n"
|
||||||
|
+ "</epp>\n")
|
||||||
|
.getBytes(UTF_8);
|
||||||
|
|
||||||
|
private X509Certificate certificate;
|
||||||
|
|
||||||
|
public EppProtocolModuleTest() {
|
||||||
|
super(TestComponent::eppHandlers);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Verifies that the epp message content is represented by the buffers. */
|
||||||
|
private static void assertBufferRepresentsContent(ByteBuf buffer, byte[] expectedContents) {
|
||||||
|
// First make sure that buffer length is expected content length plus header length.
|
||||||
|
assertThat(buffer.readableBytes()).isEqualTo(expectedContents.length + HEADER_LENGTH);
|
||||||
|
// Then check if the header value is indeed expected content length plus header length.
|
||||||
|
assertThat(buffer.readInt()).isEqualTo(expectedContents.length + HEADER_LENGTH);
|
||||||
|
// Finally check the buffer contains the expected contents.
|
||||||
|
byte[] actualContents = new byte[expectedContents.length];
|
||||||
|
buffer.readBytes(actualContents);
|
||||||
|
assertThat(actualContents).isEqualTo(expectedContents);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Read all available outbound frames and make a composite {@link ByteBuf} consisting all of them.
|
||||||
|
*
|
||||||
|
* <p>This is needed because {@link io.netty.handler.codec.LengthFieldPrepender} does not
|
||||||
|
* necessary output only one {@link ByteBuf} from one input message. We need to reassemble the
|
||||||
|
* frames together in order to obtain the processed message (prepended with length header).
|
||||||
|
*/
|
||||||
|
private static ByteBuf getAllOutboundFrames(EmbeddedChannel channel) {
|
||||||
|
ByteBuf combinedBuffer = Unpooled.buffer();
|
||||||
|
ByteBuf buffer;
|
||||||
|
while ((buffer = channel.readOutbound()) != null) {
|
||||||
|
combinedBuffer.writeBytes(buffer);
|
||||||
|
}
|
||||||
|
return combinedBuffer;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Get a {@link ByteBuf} that represents the raw epp request with the given content. */
|
||||||
|
private ByteBuf getByteBufFromContent(byte[] content) {
|
||||||
|
ByteBuf buffer = Unpooled.buffer();
|
||||||
|
buffer.writeInt(content.length + HEADER_LENGTH);
|
||||||
|
buffer.writeBytes(content);
|
||||||
|
return buffer;
|
||||||
|
}
|
||||||
|
|
||||||
|
private FullHttpRequest makeEppHttpRequest(byte[] content, Cookie... cookies) {
|
||||||
|
return TestUtils.makeEppHttpRequest(
|
||||||
|
new String(content, UTF_8),
|
||||||
|
PROXY_CONFIG.epp.relayHost,
|
||||||
|
PROXY_CONFIG.epp.relayPath,
|
||||||
|
TestModule.provideFakeAccessToken().get(),
|
||||||
|
getCertificateHash(certificate),
|
||||||
|
PROXY_CONFIG.epp.serverHostname,
|
||||||
|
CLIENT_ADDRESS,
|
||||||
|
cookies);
|
||||||
|
}
|
||||||
|
|
||||||
|
private FullHttpResponse makeEppHttpResponse(byte[] content, Cookie... cookies) {
|
||||||
|
return TestUtils.makeEppHttpResponse(
|
||||||
|
new String(content, UTF_8), HttpResponseStatus.OK, cookies);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Before
|
||||||
|
public void setUp() throws Exception {
|
||||||
|
testComponent = makeTestComponent(new FakeClock());
|
||||||
|
certificate = new SelfSignedCertificate().cert();
|
||||||
|
initializeChannel(
|
||||||
|
ch -> {
|
||||||
|
ch.attr(REMOTE_ADDRESS_KEY).set(CLIENT_ADDRESS);
|
||||||
|
ch.attr(CLIENT_CERTIFICATE_PROMISE_KEY).set(ch.eventLoop().newPromise());
|
||||||
|
addAllTestableHandlers(ch);
|
||||||
|
});
|
||||||
|
Promise<X509Certificate> unusedPromise =
|
||||||
|
channel.attr(CLIENT_CERTIFICATE_PROMISE_KEY).get().setSuccess(certificate);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSuccess_singleFrameInboundMessage() throws Exception {
|
||||||
|
// First inbound message is hello.
|
||||||
|
assertThat((FullHttpRequest) channel.readInbound()).isEqualTo(makeEppHttpRequest(HELLO_BYTES));
|
||||||
|
|
||||||
|
byte[] inputBytes = readResourceBytes(getClass(), "testdata/login.xml").read();
|
||||||
|
|
||||||
|
// Verify inbound message is as expected.
|
||||||
|
assertThat(channel.writeInbound(getByteBufFromContent(inputBytes))).isTrue();
|
||||||
|
assertThat((FullHttpRequest) channel.readInbound()).isEqualTo(makeEppHttpRequest(inputBytes));
|
||||||
|
|
||||||
|
// Nothing more to read.
|
||||||
|
assertThat((Object) channel.readInbound()).isNull();
|
||||||
|
assertThat(channel.isActive()).isTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSuccess_SingleFrame_MultipleInboundMessages() throws Exception {
|
||||||
|
// First inbound message is hello.
|
||||||
|
channel.readInbound();
|
||||||
|
|
||||||
|
byte[] inputBytes1 = readResourceBytes(getClass(), "testdata/login.xml").read();
|
||||||
|
byte[] inputBytes2 = readResourceBytes(getClass(), "testdata/logout.xml").read();
|
||||||
|
|
||||||
|
// Verify inbound messages are as expected.
|
||||||
|
assertThat(
|
||||||
|
channel.writeInbound(
|
||||||
|
Unpooled.wrappedBuffer(
|
||||||
|
getByteBufFromContent(inputBytes1), getByteBufFromContent(inputBytes2))))
|
||||||
|
.isTrue();
|
||||||
|
assertThat((FullHttpRequest) channel.readInbound()).isEqualTo(makeEppHttpRequest(inputBytes1));
|
||||||
|
assertThat((FullHttpRequest) channel.readInbound()).isEqualTo(makeEppHttpRequest(inputBytes2));
|
||||||
|
|
||||||
|
// Nothing more to read.
|
||||||
|
assertThat((Object) channel.readInbound()).isNull();
|
||||||
|
assertThat(channel.isActive()).isTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSuccess_MultipleFrames_MultipleInboundMessages() throws Exception {
|
||||||
|
// First inbound message is hello.
|
||||||
|
channel.readInbound();
|
||||||
|
|
||||||
|
byte[] inputBytes1 = readResourceBytes(getClass(), "testdata/login.xml").read();
|
||||||
|
byte[] inputBytes2 = readResourceBytes(getClass(), "testdata/logout.xml").read();
|
||||||
|
ByteBuf inputBuffer =
|
||||||
|
Unpooled.wrappedBuffer(
|
||||||
|
getByteBufFromContent(inputBytes1), getByteBufFromContent(inputBytes2));
|
||||||
|
|
||||||
|
// The first frame does not contain the entire first message because it is missing 4 byte of
|
||||||
|
// header length.
|
||||||
|
assertThat(channel.writeInbound(inputBuffer.readBytes(inputBytes1.length))).isFalse();
|
||||||
|
|
||||||
|
// The second frame contains the first message, and part of the second message.
|
||||||
|
assertThat(channel.writeInbound(inputBuffer.readBytes(inputBytes2.length))).isTrue();
|
||||||
|
assertThat((FullHttpRequest) channel.readInbound()).isEqualTo(makeEppHttpRequest(inputBytes1));
|
||||||
|
|
||||||
|
// The third frame contains the rest of the second message.
|
||||||
|
assertThat(channel.writeInbound(inputBuffer)).isTrue();
|
||||||
|
assertThat((FullHttpRequest) channel.readInbound()).isEqualTo(makeEppHttpRequest(inputBytes2));
|
||||||
|
|
||||||
|
// Nothing more to read.
|
||||||
|
assertThat((Object) channel.readInbound()).isNull();
|
||||||
|
assertThat(channel.isActive()).isTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSuccess_simpleOutboundMessage() throws Exception {
|
||||||
|
// First inbound message is hello.
|
||||||
|
channel.readInbound();
|
||||||
|
|
||||||
|
byte[] outputBytes = readResourceBytes(getClass(), "testdata/login_response.xml").read();
|
||||||
|
|
||||||
|
// Verify outbound message is as expected.
|
||||||
|
assertThat(channel.writeOutbound(makeEppHttpResponse(outputBytes))).isTrue();
|
||||||
|
assertBufferRepresentsContent(getAllOutboundFrames(channel), outputBytes);
|
||||||
|
|
||||||
|
// Nothing more to write.
|
||||||
|
assertThat((Object) channel.readOutbound()).isNull();
|
||||||
|
assertThat(channel.isActive()).isTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSuccess_setAndReadCookies() throws Exception {
|
||||||
|
// First inbound message is hello.
|
||||||
|
channel.readInbound();
|
||||||
|
|
||||||
|
byte[] outputBytes1 = readResourceBytes(getClass(), "testdata/login_response.xml").read();
|
||||||
|
Cookie cookie1 = new DefaultCookie("name1", "value1");
|
||||||
|
Cookie cookie2 = new DefaultCookie("name2", "value2");
|
||||||
|
|
||||||
|
// Verify outbound message is as expected.
|
||||||
|
assertThat(channel.writeOutbound(makeEppHttpResponse(outputBytes1, cookie1, cookie2))).isTrue();
|
||||||
|
assertBufferRepresentsContent(getAllOutboundFrames(channel), outputBytes1);
|
||||||
|
|
||||||
|
// Verify inbound message contains cookies.
|
||||||
|
byte[] inputBytes1 = readResourceBytes(getClass(), "testdata/logout.xml").read();
|
||||||
|
assertThat(channel.writeInbound(getByteBufFromContent(inputBytes1))).isTrue();
|
||||||
|
assertThat((FullHttpRequest) channel.readInbound())
|
||||||
|
.isEqualTo(makeEppHttpRequest(inputBytes1, cookie1, cookie2));
|
||||||
|
|
||||||
|
// Second outbound message change cookies.
|
||||||
|
byte[] outputBytes2 = readResourceBytes(getClass(), "testdata/logout_response.xml").read();
|
||||||
|
Cookie cookie3 = new DefaultCookie("name3", "value3");
|
||||||
|
cookie2 = new DefaultCookie("name2", "newValue2");
|
||||||
|
|
||||||
|
// Verify outbound message is as expected.
|
||||||
|
assertThat(channel.writeOutbound(makeEppHttpResponse(outputBytes2, cookie2, cookie3))).isTrue();
|
||||||
|
assertBufferRepresentsContent(getAllOutboundFrames(channel), outputBytes2);
|
||||||
|
|
||||||
|
// Verify inbound message contains updated cookies.
|
||||||
|
byte[] inputBytes2 = readResourceBytes(getClass(), "testdata/login.xml").read();
|
||||||
|
assertThat(channel.writeInbound(getByteBufFromContent(inputBytes2))).isTrue();
|
||||||
|
assertThat((FullHttpRequest) channel.readInbound())
|
||||||
|
.isEqualTo(makeEppHttpRequest(inputBytes2, cookie1, cookie2, cookie3));
|
||||||
|
|
||||||
|
// Nothing more to write or read.
|
||||||
|
assertThat((Object) channel.readOutbound()).isNull();
|
||||||
|
assertThat((Object) channel.readInbound()).isNull();
|
||||||
|
assertThat(channel.isActive()).isTrue();
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,88 @@
|
||||||
|
// Copyright 2017 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.proxy;
|
||||||
|
|
||||||
|
import static com.google.common.truth.Truth.assertThat;
|
||||||
|
import static java.nio.charset.StandardCharsets.US_ASCII;
|
||||||
|
|
||||||
|
import io.netty.buffer.ByteBuf;
|
||||||
|
import io.netty.buffer.Unpooled;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.junit.runner.RunWith;
|
||||||
|
import org.junit.runners.JUnit4;
|
||||||
|
|
||||||
|
/** End-to-end tests for {@link HealthCheckProtocolModule}. */
|
||||||
|
@RunWith(JUnit4.class)
|
||||||
|
public class HealthCheckProtocolModuleTest extends ProtocolModuleTest {
|
||||||
|
|
||||||
|
public HealthCheckProtocolModuleTest() {
|
||||||
|
super(TestComponent::healthCheckHandlers);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSuccess_expectedInboundMessage() {
|
||||||
|
// no inbound message passed along.
|
||||||
|
assertThat(
|
||||||
|
channel.writeInbound(
|
||||||
|
Unpooled.wrappedBuffer(PROXY_CONFIG.healthCheck.checkRequest.getBytes(US_ASCII))))
|
||||||
|
.isFalse();
|
||||||
|
ByteBuf outputBuffer = channel.readOutbound();
|
||||||
|
// response written to channel.
|
||||||
|
assertThat(outputBuffer.toString(US_ASCII)).isEqualTo(PROXY_CONFIG.healthCheck.checkResponse);
|
||||||
|
assertThat(channel.isActive()).isTrue();
|
||||||
|
// nothing more to write.
|
||||||
|
assertThat((Object) channel.readOutbound()).isNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSuccess_InboundMessageTooShort() {
|
||||||
|
String shortRequest = "HEALTH_CHECK";
|
||||||
|
// no inbound message passed along.
|
||||||
|
assertThat(channel.writeInbound(Unpooled.wrappedBuffer(shortRequest.getBytes(US_ASCII))))
|
||||||
|
.isFalse();
|
||||||
|
// nothing to write.
|
||||||
|
assertThat(channel.isActive()).isTrue();
|
||||||
|
assertThat((Object) channel.readOutbound()).isNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSuccess_InboundMessageTooLong() {
|
||||||
|
String longRequest = "HEALTH_CHECK_REQUEST HELLO";
|
||||||
|
// no inbound message passed along.
|
||||||
|
assertThat(channel.writeInbound(Unpooled.wrappedBuffer(longRequest.getBytes(US_ASCII))))
|
||||||
|
.isFalse();
|
||||||
|
ByteBuf outputBuffer = channel.readOutbound();
|
||||||
|
// The fixed length frame decoder will decode the first inbound message as "HEALTH_CHECK_
|
||||||
|
// REQUEST", which is what this handler expects. So it will respond with the pre-defined
|
||||||
|
// response message. This is an acceptable false-positive because the GCP health checker will
|
||||||
|
// only send the pre-defined request message. As long as the health check can receive the
|
||||||
|
// request it expects, we do not care if the protocol also respond to other requests.
|
||||||
|
assertThat(outputBuffer.toString(US_ASCII)).isEqualTo(PROXY_CONFIG.healthCheck.checkResponse);
|
||||||
|
assertThat(channel.isActive()).isTrue();
|
||||||
|
// nothing more to write.
|
||||||
|
assertThat((Object) channel.readOutbound()).isNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSuccess_InboundMessageNotMatch() {
|
||||||
|
String invalidRequest = "HEALTH_CHECK_REQUESX";
|
||||||
|
// no inbound message passed along.
|
||||||
|
assertThat(channel.writeInbound(Unpooled.wrappedBuffer(invalidRequest.getBytes(US_ASCII))))
|
||||||
|
.isFalse();
|
||||||
|
// nothing to write.
|
||||||
|
assertThat(channel.isActive()).isTrue();
|
||||||
|
assertThat((Object) channel.readOutbound()).isNull();
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,103 @@
|
||||||
|
// Copyright 2017 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.proxy;
|
||||||
|
|
||||||
|
import static com.google.common.truth.Truth.assertThat;
|
||||||
|
import static google.registry.proxy.TestUtils.assertHttpRequestEquivalent;
|
||||||
|
import static google.registry.proxy.TestUtils.assertHttpResponseEquivalent;
|
||||||
|
import static google.registry.proxy.TestUtils.makeHttpPostRequest;
|
||||||
|
import static google.registry.proxy.TestUtils.makeHttpResponse;
|
||||||
|
|
||||||
|
import io.netty.buffer.ByteBuf;
|
||||||
|
import io.netty.channel.embedded.EmbeddedChannel;
|
||||||
|
import io.netty.handler.codec.http.FullHttpRequest;
|
||||||
|
import io.netty.handler.codec.http.FullHttpResponse;
|
||||||
|
import io.netty.handler.codec.http.HttpObjectAggregator;
|
||||||
|
import io.netty.handler.codec.http.HttpResponseStatus;
|
||||||
|
import io.netty.handler.codec.http.HttpServerCodec;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.junit.runner.RunWith;
|
||||||
|
import org.junit.runners.JUnit4;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* End-to-end tests for {@link HttpsRelayProtocolModule}.
|
||||||
|
*
|
||||||
|
* <p>This protocol defines a connection in which the proxy behaves as a standard http client (sans
|
||||||
|
* the relay operation which is excluded in end-to-end testing). Because non user-defined handlers
|
||||||
|
* are used, the tests here focus on verifying that the request written to the network socket by the
|
||||||
|
* client is reconstructed faithfully by a server, and vice versa, that the response the client
|
||||||
|
* decoded from incoming bytes is equivalent to the response sent by the server.
|
||||||
|
*
|
||||||
|
* <p>These tests only ensure that the client represented by this protocol is compatible with a
|
||||||
|
* server implementation provided by Netty itself. They test the self-consistency of various Netty
|
||||||
|
* handlers that deal with HTTP protocol, but not whether the handlers converts between bytes and
|
||||||
|
* HTTP messages correctly, which is presumed correct.
|
||||||
|
*/
|
||||||
|
@RunWith(JUnit4.class)
|
||||||
|
public class HttpsRelayProtocolModuleTest extends ProtocolModuleTest {
|
||||||
|
|
||||||
|
private static final String HOST = "test.tld";
|
||||||
|
private static final String PATH = "/path/to/test";
|
||||||
|
private static final String CONTENT = "content to test\nnext line\n";
|
||||||
|
|
||||||
|
private final EmbeddedChannel serverChannel =
|
||||||
|
new EmbeddedChannel(new HttpServerCodec(), new HttpObjectAggregator(512 * 1024));
|
||||||
|
|
||||||
|
public HttpsRelayProtocolModuleTest() {
|
||||||
|
super(TestComponent::httpsRelayHandlers);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests that the client converts given {@link FullHttpRequest} to bytes, which is sent to the
|
||||||
|
* server and reconstructed to a {@link FullHttpRequest} that is equivalent to the original. Then
|
||||||
|
* test that the server converts given {@link FullHttpResponse} to bytes, which is sent to the
|
||||||
|
* client and reconstructed to a {@link FullHttpResponse} that is equivalent to the original.
|
||||||
|
*
|
||||||
|
* <p>The request and response equivalences are tested in the same method because the client codec
|
||||||
|
* tries to pair the response it receives with the request it sends. Receiving a response without
|
||||||
|
* sending a request first will cause the {@link HttpObjectAggregator} to fail to aggregate
|
||||||
|
* properly.
|
||||||
|
*/
|
||||||
|
private void requestAndRespondWithStatus(HttpResponseStatus status) {
|
||||||
|
ByteBuf buffer;
|
||||||
|
FullHttpRequest requestSent = makeHttpPostRequest(CONTENT, HOST, PATH);
|
||||||
|
// Need to send a copy as the content read index will advance after the request is written to
|
||||||
|
// the outbound of client channel, making comparison with requestReceived fail.
|
||||||
|
assertThat(channel.writeOutbound(requestSent.copy())).isTrue();
|
||||||
|
buffer = channel.readOutbound();
|
||||||
|
assertThat(serverChannel.writeInbound(buffer)).isTrue();
|
||||||
|
FullHttpRequest requestReceived = serverChannel.readInbound();
|
||||||
|
// Verify that the request received is the same as the request sent.
|
||||||
|
assertHttpRequestEquivalent(requestSent, requestReceived);
|
||||||
|
|
||||||
|
FullHttpResponse responseSent = makeHttpResponse(CONTENT, status);
|
||||||
|
assertThat(serverChannel.writeOutbound(responseSent.copy())).isTrue();
|
||||||
|
buffer = serverChannel.readOutbound();
|
||||||
|
assertThat(channel.writeInbound(buffer)).isTrue();
|
||||||
|
FullHttpResponse responseReceived = channel.readInbound();
|
||||||
|
// Verify that the request received is the same as the request sent.
|
||||||
|
assertHttpResponseEquivalent(responseSent, responseReceived);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSuccess_OkResponse() {
|
||||||
|
requestAndRespondWithStatus(HttpResponseStatus.OK);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSuccess_NonOkResponse() {
|
||||||
|
requestAndRespondWithStatus(HttpResponseStatus.BAD_REQUEST);
|
||||||
|
}
|
||||||
|
}
|
269
javatests/google/registry/proxy/ProtocolModuleTest.java
Normal file
269
javatests/google/registry/proxy/ProtocolModuleTest.java
Normal file
|
@ -0,0 +1,269 @@
|
||||||
|
// Copyright 2017 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.proxy;
|
||||||
|
|
||||||
|
import static com.google.common.collect.ImmutableList.toImmutableList;
|
||||||
|
import static google.registry.proxy.ProxyConfig.Environment.TEST;
|
||||||
|
import static google.registry.proxy.ProxyConfig.getProxyConfig;
|
||||||
|
|
||||||
|
import com.google.common.base.Supplier;
|
||||||
|
import com.google.common.base.Suppliers;
|
||||||
|
import com.google.common.collect.ImmutableList;
|
||||||
|
import com.google.common.collect.ImmutableSet;
|
||||||
|
import dagger.Component;
|
||||||
|
import dagger.Module;
|
||||||
|
import dagger.Provides;
|
||||||
|
import google.registry.proxy.EppProtocolModule.EppProtocol;
|
||||||
|
import google.registry.proxy.HealthCheckProtocolModule.HealthCheckProtocol;
|
||||||
|
import google.registry.proxy.HttpsRelayProtocolModule.HttpsRelayProtocol;
|
||||||
|
import google.registry.proxy.WhoisProtocolModule.WhoisProtocol;
|
||||||
|
import google.registry.proxy.handler.BackendMetricsHandler;
|
||||||
|
import google.registry.proxy.handler.ProxyProtocolHandler;
|
||||||
|
import google.registry.proxy.handler.RelayHandler.FullHttpRequestRelayHandler;
|
||||||
|
import google.registry.proxy.handler.RelayHandler.FullHttpResponseRelayHandler;
|
||||||
|
import google.registry.proxy.handler.SslClientInitializer;
|
||||||
|
import google.registry.proxy.handler.SslServerInitializer;
|
||||||
|
import google.registry.testing.FakeClock;
|
||||||
|
import google.registry.util.Clock;
|
||||||
|
import io.netty.channel.Channel;
|
||||||
|
import io.netty.channel.ChannelHandler;
|
||||||
|
import io.netty.channel.ChannelInitializer;
|
||||||
|
import io.netty.channel.embedded.EmbeddedChannel;
|
||||||
|
import io.netty.handler.logging.LoggingHandler;
|
||||||
|
import io.netty.handler.ssl.SslProvider;
|
||||||
|
import io.netty.handler.ssl.util.SelfSignedCertificate;
|
||||||
|
import io.netty.handler.timeout.ReadTimeoutHandler;
|
||||||
|
import java.security.PrivateKey;
|
||||||
|
import java.security.cert.X509Certificate;
|
||||||
|
import java.util.function.Consumer;
|
||||||
|
import java.util.function.Function;
|
||||||
|
import javax.inject.Named;
|
||||||
|
import javax.inject.Provider;
|
||||||
|
import javax.inject.Singleton;
|
||||||
|
import org.junit.Before;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Base class for end-to-end tests of a {@link Protocol}.
|
||||||
|
*
|
||||||
|
* <p>The end-to-end tests ensures that the business logic that a {@link Protocol} defines are
|
||||||
|
* correctly performed by various handlers attached to its pipeline. Non-business essential handlers
|
||||||
|
* should be excluded.
|
||||||
|
*
|
||||||
|
* <p>Subclass should implement an no-arg constructor that calls constructors of this class,
|
||||||
|
* providing the method reference of the {@link TestComponent} method to call to obtain the list of
|
||||||
|
* {@link ChannelHandler} providers for the {@link Protocol} to test, and optionally a set of {@link
|
||||||
|
* ChannelHandler} classes to exclude from testing.
|
||||||
|
*/
|
||||||
|
public abstract class ProtocolModuleTest {
|
||||||
|
|
||||||
|
protected static final ProxyConfig PROXY_CONFIG = getProxyConfig(TEST);
|
||||||
|
|
||||||
|
protected TestComponent testComponent;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Default list of handler classes that are not of interest in end-to-end testing of the {@link
|
||||||
|
* Protocol}.
|
||||||
|
*/
|
||||||
|
private static final ImmutableSet<Class<? extends ChannelHandler>> DEFAULT_EXCLUDED_HANDLERS =
|
||||||
|
ImmutableSet.of(
|
||||||
|
// The PROXY protocol is only used when the proxy is behind the GCP load balancer. It is
|
||||||
|
// not part of any business logic.
|
||||||
|
ProxyProtocolHandler.class,
|
||||||
|
// SSL is part of the business logic for some protocol (EPP for example), but its
|
||||||
|
// impact is isolated. Including it makes tests much more complicated. It should be tested
|
||||||
|
// separately in its own unit tests.
|
||||||
|
SslClientInitializer.class,
|
||||||
|
SslServerInitializer.class,
|
||||||
|
// These two handlers provide essential functionalities for the proxy to operate, but they
|
||||||
|
// do not directly implement the business logic of a well-defined protocol. They should be
|
||||||
|
// tested separately in their respective unit tests.
|
||||||
|
FullHttpRequestRelayHandler.class,
|
||||||
|
FullHttpResponseRelayHandler.class,
|
||||||
|
// The rest are not part of business logic and do not need to be tested, obviously.
|
||||||
|
LoggingHandler.class,
|
||||||
|
// Metrics instrumentation is tested separately.
|
||||||
|
BackendMetricsHandler.class,
|
||||||
|
ReadTimeoutHandler.class);
|
||||||
|
|
||||||
|
protected EmbeddedChannel channel;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Method reference to the component method that exposes the list of handler providers for the
|
||||||
|
* specific {@link Protocol} in interest.
|
||||||
|
*/
|
||||||
|
protected final Function<TestComponent, ImmutableList<Provider<? extends ChannelHandler>>>
|
||||||
|
handlerProvidersMethod;
|
||||||
|
|
||||||
|
protected final ImmutableSet<Class<? extends ChannelHandler>> excludedHandlers;
|
||||||
|
|
||||||
|
protected ProtocolModuleTest(
|
||||||
|
Function<TestComponent, ImmutableList<Provider<? extends ChannelHandler>>>
|
||||||
|
handlerProvidersMethod,
|
||||||
|
ImmutableSet<Class<? extends ChannelHandler>> excludedHandlers) {
|
||||||
|
this.handlerProvidersMethod = handlerProvidersMethod;
|
||||||
|
this.excludedHandlers = excludedHandlers;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected ProtocolModuleTest(
|
||||||
|
Function<TestComponent, ImmutableList<Provider<? extends ChannelHandler>>>
|
||||||
|
handlerProvidersMethod) {
|
||||||
|
this(handlerProvidersMethod, DEFAULT_EXCLUDED_HANDLERS);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Excludes handler providers that are not of interested for testing. */
|
||||||
|
private ImmutableList<Provider<? extends ChannelHandler>> excludeHandlerProvidersForTesting(
|
||||||
|
ImmutableList<Provider<? extends ChannelHandler>> handlerProviders) {
|
||||||
|
return handlerProviders
|
||||||
|
.stream()
|
||||||
|
.filter(handlerProvider -> !excludedHandlers.contains(handlerProvider.get().getClass()))
|
||||||
|
.collect(toImmutableList());
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void initializeChannel(Consumer<Channel> initializer) {
|
||||||
|
channel =
|
||||||
|
new EmbeddedChannel(
|
||||||
|
new ChannelInitializer<Channel>() {
|
||||||
|
@Override
|
||||||
|
protected void initChannel(Channel ch) throws Exception {
|
||||||
|
initializer.accept(ch);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Adds handlers to the channel pipeline, excluding any one in {@link #excludedHandlers}. */
|
||||||
|
void addAllTestableHandlers(Channel ch) {
|
||||||
|
for (Provider<? extends ChannelHandler> handlerProvider :
|
||||||
|
excludeHandlerProvidersForTesting(handlerProvidersMethod.apply(testComponent))) {
|
||||||
|
ch.pipeline().addLast(handlerProvider.get());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static TestComponent makeTestComponent(FakeClock fakeClock) {
|
||||||
|
return DaggerProtocolModuleTest_TestComponent.builder()
|
||||||
|
.testModule(new TestModule(new FakeClock()))
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Before
|
||||||
|
public void setUp() throws Exception {
|
||||||
|
testComponent = makeTestComponent(new FakeClock());
|
||||||
|
initializeChannel(this::addAllTestableHandlers);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Component used to obtain the list of {@link ChannelHandler} providers for each {@link
|
||||||
|
* Protocol}.
|
||||||
|
*/
|
||||||
|
@Singleton
|
||||||
|
@Component(
|
||||||
|
modules = {
|
||||||
|
TestModule.class,
|
||||||
|
WhoisProtocolModule.class,
|
||||||
|
EppProtocolModule.class,
|
||||||
|
HealthCheckProtocolModule.class,
|
||||||
|
HttpsRelayProtocolModule.class
|
||||||
|
}
|
||||||
|
)
|
||||||
|
interface TestComponent {
|
||||||
|
@WhoisProtocol
|
||||||
|
ImmutableList<Provider<? extends ChannelHandler>> whoisHandlers();
|
||||||
|
|
||||||
|
@EppProtocol
|
||||||
|
ImmutableList<Provider<? extends ChannelHandler>> eppHandlers();
|
||||||
|
|
||||||
|
@HealthCheckProtocol
|
||||||
|
ImmutableList<Provider<? extends ChannelHandler>> healthCheckHandlers();
|
||||||
|
|
||||||
|
@HttpsRelayProtocol
|
||||||
|
ImmutableList<Provider<? extends ChannelHandler>> httpsRelayHandlers();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Module that provides bindings used in tests.
|
||||||
|
*
|
||||||
|
* <p>Most of the binding provided in this module should be either a fake, or a {@link
|
||||||
|
* ChannelHandler} that is excluded, and annotated with {@code @Singleton}. This module acts as a
|
||||||
|
* replacement for {@link ProxyModule} used in production component. Providing a handler that is
|
||||||
|
* part of the business logic of a {@link Protocol} from this module is a sign that the binding
|
||||||
|
* should be provided in the respective {@code ProtocolModule} instead.
|
||||||
|
*/
|
||||||
|
@Module
|
||||||
|
static class TestModule {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A fake clock that is explicitly provided. Users can construct a module with a controller
|
||||||
|
* clock.
|
||||||
|
*/
|
||||||
|
private final FakeClock fakeClock;
|
||||||
|
|
||||||
|
TestModule(FakeClock fakeClock) {
|
||||||
|
this.fakeClock = fakeClock;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Singleton
|
||||||
|
@Provides
|
||||||
|
static ProxyConfig provideProxyConfig() {
|
||||||
|
return getProxyConfig(TEST);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Singleton
|
||||||
|
@Provides
|
||||||
|
static SslProvider provideSslProvider() {
|
||||||
|
return SslProvider.JDK;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Singleton
|
||||||
|
@Provides
|
||||||
|
@Named("accessToken")
|
||||||
|
static Supplier<String> provideFakeAccessToken() {
|
||||||
|
return Suppliers.ofInstance("fake.test.token");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Singleton
|
||||||
|
@Provides
|
||||||
|
static LoggingHandler provideLoggingHandler() {
|
||||||
|
return new LoggingHandler();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Singleton
|
||||||
|
@Provides
|
||||||
|
static SelfSignedCertificate provideSelfSignedCertificate() {
|
||||||
|
try {
|
||||||
|
return new SelfSignedCertificate();
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Singleton
|
||||||
|
@Provides
|
||||||
|
@Named("eppServerCertificates")
|
||||||
|
static X509Certificate[] provideCertificate(SelfSignedCertificate ssc) {
|
||||||
|
return new X509Certificate[] {ssc.cert()};
|
||||||
|
}
|
||||||
|
|
||||||
|
@Singleton
|
||||||
|
@Provides
|
||||||
|
static PrivateKey providePrivateKey(SelfSignedCertificate ssc) {
|
||||||
|
return ssc.key();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Singleton
|
||||||
|
@Provides
|
||||||
|
Clock provideFakeClock() {
|
||||||
|
return fakeClock;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
105
javatests/google/registry/proxy/ProxyModuleTest.java
Normal file
105
javatests/google/registry/proxy/ProxyModuleTest.java
Normal file
|
@ -0,0 +1,105 @@
|
||||||
|
// Copyright 2017 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.proxy;
|
||||||
|
|
||||||
|
import static com.google.common.truth.Truth.assertThat;
|
||||||
|
import static google.registry.proxy.ProxyConfig.Environment.TEST;
|
||||||
|
import static google.registry.proxy.ProxyConfig.getProxyConfig;
|
||||||
|
import static google.registry.testing.JUnitBackports.expectThrows;
|
||||||
|
import static org.junit.Assert.fail;
|
||||||
|
|
||||||
|
import com.beust.jcommander.ParameterException;
|
||||||
|
import google.registry.proxy.ProxyConfig.Environment;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.junit.runner.RunWith;
|
||||||
|
import org.junit.runners.JUnit4;
|
||||||
|
|
||||||
|
/** Unit tests for {@link ProxyModule}. */
|
||||||
|
@RunWith(JUnit4.class)
|
||||||
|
public class ProxyModuleTest {
|
||||||
|
|
||||||
|
private static final ProxyConfig PROXY_CONFIG = getProxyConfig(TEST);
|
||||||
|
private final ProxyModule proxyModule = new ProxyModule();
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSuccess_parseArgs_defaultArgs() {
|
||||||
|
String[] args = {};
|
||||||
|
proxyModule.parse(args);
|
||||||
|
assertThat(proxyModule.provideWhoisPort(PROXY_CONFIG)).isEqualTo(PROXY_CONFIG.whois.port);
|
||||||
|
assertThat(proxyModule.provideEppPort(PROXY_CONFIG)).isEqualTo(PROXY_CONFIG.epp.port);
|
||||||
|
assertThat(proxyModule.provideHealthCheckPort(PROXY_CONFIG))
|
||||||
|
.isEqualTo(PROXY_CONFIG.healthCheck.port);
|
||||||
|
assertThat(proxyModule.provideEnvironment()).isEqualTo(Environment.LOCAL);
|
||||||
|
assertThat(proxyModule.log).isFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testFailure_parseArgs_wrongArguments() {
|
||||||
|
String[] args = {"--wrong_flag", "some_value"};
|
||||||
|
try {
|
||||||
|
proxyModule.parse(args);
|
||||||
|
fail("Expected ParameterException.");
|
||||||
|
} catch (ParameterException e) {
|
||||||
|
assertThat(e).hasMessageThat().contains("--wrong_flag");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSuccess_parseArgs_log() {
|
||||||
|
String[] args = {"--log"};
|
||||||
|
proxyModule.parse(args);
|
||||||
|
assertThat(proxyModule.log).isTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSuccess_parseArgs_customWhoisPort() {
|
||||||
|
String[] args = {"--whois", "12345"};
|
||||||
|
proxyModule.parse(args);
|
||||||
|
assertThat(proxyModule.provideWhoisPort(PROXY_CONFIG)).isEqualTo(12345);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSuccess_parseArgs_customEppPort() {
|
||||||
|
String[] args = {"--epp", "22222"};
|
||||||
|
proxyModule.parse(args);
|
||||||
|
assertThat(proxyModule.provideEppPort(PROXY_CONFIG)).isEqualTo(22222);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSuccess_parseArgs_customHealthCheckPort() {
|
||||||
|
String[] args = {"--health_check", "23456"};
|
||||||
|
proxyModule.parse(args);
|
||||||
|
assertThat(proxyModule.provideHealthCheckPort(PROXY_CONFIG)).isEqualTo(23456);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSuccess_parseArgs_customEnvironment() {
|
||||||
|
String[] args = {"--env", "ALpHa"};
|
||||||
|
proxyModule.parse(args);
|
||||||
|
assertThat(proxyModule.provideEnvironment()).isEqualTo(Environment.ALPHA);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testFailure_parseArgs_wrongEnvironment() {
|
||||||
|
ParameterException e =
|
||||||
|
expectThrows(
|
||||||
|
ParameterException.class,
|
||||||
|
() -> {
|
||||||
|
String[] args = {"--env", "beta"};
|
||||||
|
proxyModule.parse(args);
|
||||||
|
});
|
||||||
|
assertThat(e).hasMessageThat().contains("Invalid value for --env parameter");
|
||||||
|
}
|
||||||
|
}
|
139
javatests/google/registry/proxy/TestUtils.java
Normal file
139
javatests/google/registry/proxy/TestUtils.java
Normal file
|
@ -0,0 +1,139 @@
|
||||||
|
// Copyright 2017 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.proxy;
|
||||||
|
|
||||||
|
import static com.google.common.truth.Truth.assertThat;
|
||||||
|
import static google.registry.proxy.handler.EppServiceHandler.EPP_CONTENT_TYPE;
|
||||||
|
import static google.registry.proxy.handler.EppServiceHandler.FORWARDED_FOR_FIELD;
|
||||||
|
import static google.registry.proxy.handler.EppServiceHandler.REQUESTED_SERVERNAME_VIA_SNI_FIELD;
|
||||||
|
import static google.registry.proxy.handler.EppServiceHandler.SSL_CLIENT_CERTIFICATE_HASH_FIELD;
|
||||||
|
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.FullHttpMessage;
|
||||||
|
import io.netty.handler.codec.http.FullHttpRequest;
|
||||||
|
import io.netty.handler.codec.http.FullHttpResponse;
|
||||||
|
import io.netty.handler.codec.http.HttpHeaderNames;
|
||||||
|
import io.netty.handler.codec.http.HttpHeaderValues;
|
||||||
|
import io.netty.handler.codec.http.HttpMethod;
|
||||||
|
import io.netty.handler.codec.http.HttpResponseStatus;
|
||||||
|
import io.netty.handler.codec.http.HttpVersion;
|
||||||
|
import io.netty.handler.codec.http.cookie.ClientCookieEncoder;
|
||||||
|
import io.netty.handler.codec.http.cookie.Cookie;
|
||||||
|
import io.netty.handler.codec.http.cookie.ServerCookieEncoder;
|
||||||
|
|
||||||
|
/** Utility class for various helper methods used in testing. */
|
||||||
|
public class TestUtils {
|
||||||
|
|
||||||
|
public static FullHttpRequest makeHttpPostRequest(String content, String host, String path) {
|
||||||
|
ByteBuf buf = Unpooled.wrappedBuffer(content.getBytes(US_ASCII));
|
||||||
|
FullHttpRequest request =
|
||||||
|
new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.POST, path, buf);
|
||||||
|
request
|
||||||
|
.headers()
|
||||||
|
.set(HttpHeaderNames.USER_AGENT, "Proxy")
|
||||||
|
.set(HttpHeaderNames.HOST, host)
|
||||||
|
.set(HttpHeaderNames.CONTENT_LENGTH, buf.readableBytes());
|
||||||
|
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().set(HttpHeaderNames.CONTENT_LENGTH, buf.readableBytes());
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static FullHttpRequest makeWhoisHttpRequest(
|
||||||
|
String content, String host, String path, String accessToken) {
|
||||||
|
FullHttpRequest request = makeHttpPostRequest(content, host, path);
|
||||||
|
request
|
||||||
|
.headers()
|
||||||
|
.set(HttpHeaderNames.CONNECTION, HttpHeaderValues.CLOSE)
|
||||||
|
.set(HttpHeaderNames.AUTHORIZATION, "Bearer " + accessToken)
|
||||||
|
.set(HttpHeaderNames.CONTENT_TYPE, HttpHeaderValues.TEXT_PLAIN)
|
||||||
|
.set(HttpHeaderNames.ACCEPT, HttpHeaderValues.TEXT_PLAIN);
|
||||||
|
return request;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static FullHttpRequest makeEppHttpRequest(
|
||||||
|
String content,
|
||||||
|
String host,
|
||||||
|
String path,
|
||||||
|
String accessToken,
|
||||||
|
String sslClientCertificateHash,
|
||||||
|
String serverHostname,
|
||||||
|
String clientAddress,
|
||||||
|
Cookie... cookies) {
|
||||||
|
FullHttpRequest request = makeHttpPostRequest(content, host, path);
|
||||||
|
request
|
||||||
|
.headers()
|
||||||
|
.set(HttpHeaderNames.AUTHORIZATION, "Bearer " + accessToken)
|
||||||
|
.set(HttpHeaderNames.CONTENT_TYPE, EPP_CONTENT_TYPE)
|
||||||
|
.set(HttpHeaderNames.ACCEPT, EPP_CONTENT_TYPE)
|
||||||
|
.set(SSL_CLIENT_CERTIFICATE_HASH_FIELD, sslClientCertificateHash)
|
||||||
|
.set(REQUESTED_SERVERNAME_VIA_SNI_FIELD, serverHostname)
|
||||||
|
.set(FORWARDED_FOR_FIELD, clientAddress);
|
||||||
|
if (cookies.length != 0) {
|
||||||
|
request.headers().set(HttpHeaderNames.COOKIE, ClientCookieEncoder.STRICT.encode(cookies));
|
||||||
|
}
|
||||||
|
return request;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static FullHttpResponse makeWhoisHttpResponse(String content, HttpResponseStatus status) {
|
||||||
|
FullHttpResponse response = makeHttpResponse(content, status);
|
||||||
|
response.headers().set(HttpHeaderNames.CONTENT_TYPE, HttpHeaderValues.TEXT_PLAIN);
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static FullHttpResponse makeEppHttpResponse(
|
||||||
|
String content, HttpResponseStatus status, Cookie... cookies) {
|
||||||
|
FullHttpResponse response = makeHttpResponse(content, status);
|
||||||
|
response.headers().set(HttpHeaderNames.CONTENT_TYPE, EPP_CONTENT_TYPE);
|
||||||
|
for (Cookie cookie : cookies) {
|
||||||
|
response.headers().add(HttpHeaderNames.SET_COOKIE, ServerCookieEncoder.STRICT.encode(cookie));
|
||||||
|
}
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compares two {@link FullHttpMessage} for equivalency.
|
||||||
|
*
|
||||||
|
* <p>This method is needed because an HTTP message decoded and aggregated from inbound {@link
|
||||||
|
* ByteBuf} is of a different class than the one written to the outbound {@link ByteBuf}, and The
|
||||||
|
* {@link ByteBuf} implementations that hold the content of the HTTP messages are different, even
|
||||||
|
* though the actual content, headers, etc are the same.
|
||||||
|
*
|
||||||
|
* <p>This method is not type-safe, msg1 & msg2 can be a request and a response, respectively. Do
|
||||||
|
* not use this method directly.
|
||||||
|
*/
|
||||||
|
private static void assertHttpMessageEquivalent(FullHttpMessage msg1, FullHttpMessage msg2) {
|
||||||
|
assertThat(msg1.protocolVersion()).isEqualTo(msg2.protocolVersion());
|
||||||
|
assertThat(msg1.content()).isEqualTo(msg2.content());
|
||||||
|
assertThat(msg1.headers()).isEqualTo(msg2.headers());
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void assertHttpResponseEquivalent(FullHttpResponse res1, FullHttpResponse res2) {
|
||||||
|
assertThat(res1.status()).isEqualTo(res2.status());
|
||||||
|
assertHttpMessageEquivalent(res1, res2);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void assertHttpRequestEquivalent(FullHttpRequest req1, FullHttpRequest req2) {
|
||||||
|
assertHttpMessageEquivalent(req1, req2);
|
||||||
|
}
|
||||||
|
}
|
165
javatests/google/registry/proxy/WhoisProtocolModuleTest.java
Normal file
165
javatests/google/registry/proxy/WhoisProtocolModuleTest.java
Normal file
|
@ -0,0 +1,165 @@
|
||||||
|
// Copyright 2017 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.proxy;
|
||||||
|
|
||||||
|
import static com.google.common.truth.Truth.assertThat;
|
||||||
|
import static google.registry.proxy.TestUtils.makeWhoisHttpRequest;
|
||||||
|
import static google.registry.proxy.TestUtils.makeWhoisHttpResponse;
|
||||||
|
import static java.nio.charset.StandardCharsets.US_ASCII;
|
||||||
|
import static java.util.stream.Collectors.joining;
|
||||||
|
import static org.junit.Assert.fail;
|
||||||
|
|
||||||
|
import io.netty.buffer.ByteBuf;
|
||||||
|
import io.netty.buffer.Unpooled;
|
||||||
|
import io.netty.handler.codec.http.FullHttpRequest;
|
||||||
|
import io.netty.handler.codec.http.FullHttpResponse;
|
||||||
|
import io.netty.handler.codec.http.HttpResponseStatus;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.junit.runner.RunWith;
|
||||||
|
import org.junit.runners.JUnit4;
|
||||||
|
|
||||||
|
/** End-to-end tests for {@link WhoisProtocolModule}. */
|
||||||
|
@RunWith(JUnit4.class)
|
||||||
|
public class WhoisProtocolModuleTest extends ProtocolModuleTest {
|
||||||
|
|
||||||
|
public WhoisProtocolModuleTest() {
|
||||||
|
super(TestComponent::whoisHandlers);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSuccess_singleFrameInboundMessage() {
|
||||||
|
String inputString = "test.tld\r\n";
|
||||||
|
// Inbound message processed and passed along.
|
||||||
|
assertThat(channel.writeInbound(Unpooled.wrappedBuffer(inputString.getBytes(US_ASCII))))
|
||||||
|
.isTrue();
|
||||||
|
|
||||||
|
FullHttpRequest actualRequest = channel.readInbound();
|
||||||
|
FullHttpRequest expectedRequest =
|
||||||
|
makeWhoisHttpRequest(
|
||||||
|
"test.tld",
|
||||||
|
PROXY_CONFIG.whois.relayHost,
|
||||||
|
PROXY_CONFIG.whois.relayPath,
|
||||||
|
TestModule.provideFakeAccessToken().get());
|
||||||
|
assertThat(expectedRequest).isEqualTo(actualRequest);
|
||||||
|
assertThat(channel.isActive()).isTrue();
|
||||||
|
// Nothing more to read.
|
||||||
|
assertThat((Object) channel.readInbound()).isNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSuccess_noNewlineInboundMessage() {
|
||||||
|
String inputString = "test.tld";
|
||||||
|
// No newline encountered, no message formed.
|
||||||
|
assertThat(channel.writeInbound(Unpooled.wrappedBuffer(inputString.getBytes(US_ASCII))))
|
||||||
|
.isFalse();
|
||||||
|
assertThat(channel.isActive()).isTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSuccess_multiFrameInboundMessage() {
|
||||||
|
String frame1 = "test";
|
||||||
|
String frame2 = "1.tld";
|
||||||
|
String frame3 = "\r\nte";
|
||||||
|
String frame4 = "st2.tld\r";
|
||||||
|
String frame5 = "\ntest3.tld";
|
||||||
|
// No newline yet.
|
||||||
|
assertThat(channel.writeInbound(Unpooled.wrappedBuffer(frame1.getBytes(US_ASCII)))).isFalse();
|
||||||
|
// Still no newline yet.
|
||||||
|
assertThat(channel.writeInbound(Unpooled.wrappedBuffer(frame2.getBytes(US_ASCII)))).isFalse();
|
||||||
|
// First newline encountered.
|
||||||
|
assertThat(channel.writeInbound(Unpooled.wrappedBuffer(frame3.getBytes(US_ASCII)))).isTrue();
|
||||||
|
FullHttpRequest actualRequest1 = channel.readInbound();
|
||||||
|
FullHttpRequest expectedRequest1 =
|
||||||
|
makeWhoisHttpRequest(
|
||||||
|
"test1.tld",
|
||||||
|
PROXY_CONFIG.whois.relayHost,
|
||||||
|
PROXY_CONFIG.whois.relayPath,
|
||||||
|
TestModule.provideFakeAccessToken().get());
|
||||||
|
assertThat(expectedRequest1).isEqualTo(actualRequest1);
|
||||||
|
// No more message at this point.
|
||||||
|
assertThat((Object) channel.readInbound()).isNull();
|
||||||
|
// More inbound bytes, but no newline.
|
||||||
|
assertThat(channel.writeInbound(Unpooled.wrappedBuffer(frame4.getBytes(US_ASCII)))).isFalse();
|
||||||
|
// Second message read.
|
||||||
|
assertThat(channel.writeInbound(Unpooled.wrappedBuffer(frame5.getBytes(US_ASCII)))).isTrue();
|
||||||
|
FullHttpRequest actualRequest2 = channel.readInbound();
|
||||||
|
FullHttpRequest expectedRequest2 =
|
||||||
|
makeWhoisHttpRequest(
|
||||||
|
"test2.tld",
|
||||||
|
PROXY_CONFIG.whois.relayHost,
|
||||||
|
PROXY_CONFIG.whois.relayPath,
|
||||||
|
TestModule.provideFakeAccessToken().get());
|
||||||
|
assertThat(expectedRequest2).isEqualTo(actualRequest2);
|
||||||
|
// The third message is not complete yet.
|
||||||
|
assertThat(channel.isActive()).isTrue();
|
||||||
|
assertThat((Object) channel.readInbound()).isNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSuccess_inboundMessageTooLong() {
|
||||||
|
String inputString = Stream.generate(() -> "x").limit(513).collect(joining()) + "\r\n";
|
||||||
|
// Nothing gets propagated further.
|
||||||
|
assertThat(channel.writeInbound(Unpooled.wrappedBuffer(inputString.getBytes(US_ASCII))))
|
||||||
|
.isFalse();
|
||||||
|
// Connection is closed due to inbound message overflow.
|
||||||
|
assertThat(channel.isActive()).isFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSuccess_parseSingleOutboundHttpResponse() {
|
||||||
|
String outputString = "line1\r\nline2\r\n";
|
||||||
|
FullHttpResponse response = makeWhoisHttpResponse(outputString, HttpResponseStatus.OK);
|
||||||
|
// Http response parsed and passed along.
|
||||||
|
assertThat(channel.writeOutbound(response)).isTrue();
|
||||||
|
ByteBuf outputBuffer = channel.readOutbound();
|
||||||
|
assertThat(outputBuffer.toString(US_ASCII)).isEqualTo(outputString);
|
||||||
|
assertThat(channel.isActive()).isTrue();
|
||||||
|
// Nothing more to write.
|
||||||
|
assertThat((Object) channel.readOutbound()).isNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSuccess_parseMultipleOutboundHttpResponse() {
|
||||||
|
String outputString1 = "line1\r\nline2\r\n";
|
||||||
|
String outputString2 = "line3\r\nline4\r\nline5\r\n";
|
||||||
|
FullHttpResponse response1 = makeWhoisHttpResponse(outputString1, HttpResponseStatus.OK);
|
||||||
|
FullHttpResponse response2 = makeWhoisHttpResponse(outputString2, HttpResponseStatus.OK);
|
||||||
|
assertThat(channel.writeOutbound(response1, response2)).isTrue();
|
||||||
|
// First Http response parsed
|
||||||
|
ByteBuf outputBuffer1 = channel.readOutbound();
|
||||||
|
assertThat(outputBuffer1.toString(US_ASCII)).isEqualTo(outputString1);
|
||||||
|
// Second Http response parsed
|
||||||
|
ByteBuf outputBuffer2 = channel.readOutbound();
|
||||||
|
assertThat(outputBuffer2.toString(US_ASCII)).isEqualTo(outputString2);
|
||||||
|
assertThat(channel.isActive()).isTrue();
|
||||||
|
// Nothing more to write.
|
||||||
|
assertThat((Object) channel.readOutbound()).isNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testFailure_outboundResponseStatusNotOK() {
|
||||||
|
String outputString = "line1\r\nline2\r\n";
|
||||||
|
FullHttpResponse response = makeWhoisHttpResponse(outputString, HttpResponseStatus.BAD_REQUEST);
|
||||||
|
try {
|
||||||
|
channel.writeOutbound(response);
|
||||||
|
fail("Expected failure due to non-OK HTTP response status");
|
||||||
|
} catch (Exception e) {
|
||||||
|
assertThat(e).hasCauseThat().isInstanceOf(IllegalArgumentException.class);
|
||||||
|
assertThat(e).hasMessageThat().contains("400 Bad Request");
|
||||||
|
}
|
||||||
|
assertThat(channel.isActive()).isFalse();
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,227 @@
|
||||||
|
// Copyright 2017 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.proxy.handler;
|
||||||
|
|
||||||
|
import static com.google.common.truth.Truth.assertThat;
|
||||||
|
import static google.registry.proxy.Protocol.PROTOCOL_KEY;
|
||||||
|
import static google.registry.proxy.TestUtils.assertHttpRequestEquivalent;
|
||||||
|
import static google.registry.proxy.TestUtils.assertHttpResponseEquivalent;
|
||||||
|
import static google.registry.proxy.TestUtils.makeHttpPostRequest;
|
||||||
|
import static google.registry.proxy.TestUtils.makeHttpResponse;
|
||||||
|
import static google.registry.proxy.handler.EppServiceHandler.CLIENT_CERTIFICATE_HASH_KEY;
|
||||||
|
import static google.registry.proxy.handler.RelayHandler.RELAY_CHANNEL_KEY;
|
||||||
|
import static google.registry.testing.JUnitBackports.expectThrows;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
import static org.mockito.Mockito.verify;
|
||||||
|
import static org.mockito.Mockito.verifyNoMoreInteractions;
|
||||||
|
|
||||||
|
import com.google.common.collect.ImmutableList;
|
||||||
|
import google.registry.proxy.Protocol;
|
||||||
|
import google.registry.proxy.Protocol.BackendProtocol;
|
||||||
|
import google.registry.proxy.Protocol.FrontendProtocol;
|
||||||
|
import google.registry.proxy.metric.BackendMetrics;
|
||||||
|
import google.registry.testing.FakeClock;
|
||||||
|
import io.netty.channel.ChannelInitializer;
|
||||||
|
import io.netty.channel.embedded.EmbeddedChannel;
|
||||||
|
import io.netty.handler.codec.http.FullHttpRequest;
|
||||||
|
import io.netty.handler.codec.http.FullHttpResponse;
|
||||||
|
import io.netty.handler.codec.http.HttpResponseStatus;
|
||||||
|
import org.joda.time.DateTime;
|
||||||
|
import org.joda.time.Duration;
|
||||||
|
import org.junit.Before;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.junit.runner.RunWith;
|
||||||
|
import org.junit.runners.JUnit4;
|
||||||
|
|
||||||
|
/** Unit tests for {@link BackendMetricsHandler}. */
|
||||||
|
@RunWith(JUnit4.class)
|
||||||
|
public class BackendMetricsHandlerTest {
|
||||||
|
|
||||||
|
private static final String HOST = "host.tld";
|
||||||
|
private static final String CLIENT_CERT_HASH = "blah12345";
|
||||||
|
private static final String RELAYED_PROTOCOL_NAME = "frontend protocol";
|
||||||
|
|
||||||
|
private final FakeClock fakeClock = new FakeClock();
|
||||||
|
private final BackendMetrics metrics = mock(BackendMetrics.class);
|
||||||
|
private final BackendMetricsHandler handler = new BackendMetricsHandler(fakeClock, metrics);
|
||||||
|
|
||||||
|
private final BackendProtocol backendProtocol =
|
||||||
|
Protocol.backendBuilder()
|
||||||
|
.name("backend protocol")
|
||||||
|
.host(HOST)
|
||||||
|
.port(1)
|
||||||
|
.handlerProviders(ImmutableList.of())
|
||||||
|
.build();
|
||||||
|
|
||||||
|
private final FrontendProtocol frontendProtocol =
|
||||||
|
Protocol.frontendBuilder()
|
||||||
|
.name(RELAYED_PROTOCOL_NAME)
|
||||||
|
.port(2)
|
||||||
|
.relayProtocol(backendProtocol)
|
||||||
|
.handlerProviders(ImmutableList.of())
|
||||||
|
.build();
|
||||||
|
|
||||||
|
private EmbeddedChannel channel;
|
||||||
|
|
||||||
|
@Before
|
||||||
|
public void setUp() {
|
||||||
|
EmbeddedChannel frontendChannel = new EmbeddedChannel();
|
||||||
|
frontendChannel.attr(PROTOCOL_KEY).set(frontendProtocol);
|
||||||
|
frontendChannel.attr(CLIENT_CERTIFICATE_HASH_KEY).set(CLIENT_CERT_HASH);
|
||||||
|
channel =
|
||||||
|
new EmbeddedChannel(
|
||||||
|
new ChannelInitializer<EmbeddedChannel>() {
|
||||||
|
@Override
|
||||||
|
protected void initChannel(EmbeddedChannel ch) throws Exception {
|
||||||
|
ch.attr(PROTOCOL_KEY).set(backendProtocol);
|
||||||
|
ch.attr(RELAY_CHANNEL_KEY).set(frontendChannel);
|
||||||
|
ch.pipeline().addLast(handler);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testFailure_outbound_wrongType() {
|
||||||
|
Object request = new Object();
|
||||||
|
IllegalArgumentException e =
|
||||||
|
expectThrows(IllegalArgumentException.class, () -> channel.writeOutbound(request));
|
||||||
|
assertThat(e).hasMessageThat().isEqualTo("Outgoing request must be FullHttpRequest.");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testFailure_inbound_wrongType() {
|
||||||
|
Object response = new Object();
|
||||||
|
IllegalArgumentException e =
|
||||||
|
expectThrows(IllegalArgumentException.class, () -> channel.writeInbound(response));
|
||||||
|
assertThat(e).hasMessageThat().isEqualTo("Incoming response must be FullHttpResponse.");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSuccess_oneRequest() {
|
||||||
|
FullHttpRequest request = makeHttpPostRequest("some content", HOST, "/");
|
||||||
|
// outbound message passed to the next handler.
|
||||||
|
assertThat(channel.writeOutbound(request)).isTrue();
|
||||||
|
assertHttpRequestEquivalent(request, channel.readOutbound());
|
||||||
|
verify(metrics).requestSent(RELAYED_PROTOCOL_NAME, CLIENT_CERT_HASH, request);
|
||||||
|
verifyNoMoreInteractions(metrics);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSuccess_oneRequest_oneResponse() {
|
||||||
|
FullHttpRequest request = makeHttpPostRequest("some request", HOST, "/");
|
||||||
|
FullHttpResponse response = makeHttpResponse("some response", HttpResponseStatus.OK);
|
||||||
|
// outbound message passed to the next handler.
|
||||||
|
assertThat(channel.writeOutbound(request)).isTrue();
|
||||||
|
assertHttpRequestEquivalent(request, channel.readOutbound());
|
||||||
|
fakeClock.advanceOneMilli();
|
||||||
|
// inbound message passed to the next handler.
|
||||||
|
assertThat(channel.writeInbound(response)).isTrue();
|
||||||
|
assertHttpResponseEquivalent(response, channel.readInbound());
|
||||||
|
|
||||||
|
verify(metrics).requestSent(RELAYED_PROTOCOL_NAME, CLIENT_CERT_HASH, request);
|
||||||
|
verify(metrics).responseReceived(RELAYED_PROTOCOL_NAME, CLIENT_CERT_HASH, response, 1);
|
||||||
|
verifyNoMoreInteractions(metrics);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSuccess_badResponse() {
|
||||||
|
FullHttpRequest request = makeHttpPostRequest("some request", HOST, "/");
|
||||||
|
FullHttpResponse response =
|
||||||
|
makeHttpResponse("some bad response", HttpResponseStatus.BAD_REQUEST);
|
||||||
|
// outbound message passed to the next handler.
|
||||||
|
assertThat(channel.writeOutbound(request)).isTrue();
|
||||||
|
assertHttpRequestEquivalent(request, channel.readOutbound());
|
||||||
|
fakeClock.advanceOneMilli();
|
||||||
|
// inbound message passed to the next handler.
|
||||||
|
// Even though the response status is not OK, the metrics handler only logs it and pass it
|
||||||
|
// along to the next handler, which handles it.
|
||||||
|
assertThat(channel.writeInbound(response)).isTrue();
|
||||||
|
assertHttpResponseEquivalent(response, channel.readInbound());
|
||||||
|
|
||||||
|
verify(metrics).requestSent(RELAYED_PROTOCOL_NAME, CLIENT_CERT_HASH, request);
|
||||||
|
verify(metrics).responseReceived(RELAYED_PROTOCOL_NAME, CLIENT_CERT_HASH, response, 1);
|
||||||
|
verifyNoMoreInteractions(metrics);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testFailure_responseBeforeRequest() {
|
||||||
|
FullHttpResponse response = makeHttpResponse("phantom response", HttpResponseStatus.OK);
|
||||||
|
IllegalStateException e =
|
||||||
|
expectThrows(IllegalStateException.class, () -> channel.writeInbound(response));
|
||||||
|
assertThat(e).hasMessageThat().isEqualTo("Response received before request is sent.");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSuccess_pipelinedResponses() {
|
||||||
|
FullHttpRequest request1 = makeHttpPostRequest("request 1", HOST, "/");
|
||||||
|
FullHttpResponse response1 = makeHttpResponse("response 1", HttpResponseStatus.OK);
|
||||||
|
FullHttpRequest request2 = makeHttpPostRequest("request 2", HOST, "/");
|
||||||
|
FullHttpResponse response2 = makeHttpResponse("response 2", HttpResponseStatus.OK);
|
||||||
|
FullHttpRequest request3 = makeHttpPostRequest("request 3", HOST, "/");
|
||||||
|
FullHttpResponse response3 = makeHttpResponse("response 3", HttpResponseStatus.OK);
|
||||||
|
|
||||||
|
// First request, time = 0
|
||||||
|
assertThat(channel.writeOutbound(request1)).isTrue();
|
||||||
|
assertHttpRequestEquivalent(request1, channel.readOutbound());
|
||||||
|
DateTime sentTime1 = fakeClock.nowUtc();
|
||||||
|
|
||||||
|
fakeClock.advanceBy(Duration.millis(5));
|
||||||
|
|
||||||
|
// Second request, time = 5
|
||||||
|
assertThat(channel.writeOutbound(request2)).isTrue();
|
||||||
|
assertHttpRequestEquivalent(request2, channel.readOutbound());
|
||||||
|
DateTime sentTime2 = fakeClock.nowUtc();
|
||||||
|
|
||||||
|
fakeClock.advanceBy(Duration.millis(7));
|
||||||
|
|
||||||
|
// First response, time = 12, latency = 12 - 0 = 12
|
||||||
|
assertThat(channel.writeInbound(response1)).isTrue();
|
||||||
|
assertHttpResponseEquivalent(response1, channel.readInbound());
|
||||||
|
DateTime receivedTime1 = fakeClock.nowUtc();
|
||||||
|
|
||||||
|
fakeClock.advanceBy(Duration.millis(11));
|
||||||
|
|
||||||
|
// Third request, time = 23
|
||||||
|
assertThat(channel.writeOutbound(request3)).isTrue();
|
||||||
|
assertHttpRequestEquivalent(request3, channel.readOutbound());
|
||||||
|
DateTime sentTime3 = fakeClock.nowUtc();
|
||||||
|
|
||||||
|
fakeClock.advanceBy(Duration.millis(2));
|
||||||
|
|
||||||
|
// Second response, time = 25, latency = 25 - 5 = 20
|
||||||
|
assertThat(channel.writeInbound(response2)).isTrue();
|
||||||
|
assertHttpResponseEquivalent(response2, channel.readInbound());
|
||||||
|
DateTime receivedTime2 = fakeClock.nowUtc();
|
||||||
|
|
||||||
|
fakeClock.advanceBy(Duration.millis(4));
|
||||||
|
|
||||||
|
// Third response, time = 29, latency = 29 - 23 = 6
|
||||||
|
assertThat(channel.writeInbound(response3)).isTrue();
|
||||||
|
assertHttpResponseEquivalent(response3, channel.readInbound());
|
||||||
|
DateTime receivedTime3 = fakeClock.nowUtc();
|
||||||
|
|
||||||
|
long latency1 = new Duration(sentTime1, receivedTime1).getMillis();
|
||||||
|
long latency2 = new Duration(sentTime2, receivedTime2).getMillis();
|
||||||
|
long latency3 = new Duration(sentTime3, receivedTime3).getMillis();
|
||||||
|
|
||||||
|
verify(metrics).requestSent(RELAYED_PROTOCOL_NAME, CLIENT_CERT_HASH, request1);
|
||||||
|
verify(metrics).requestSent(RELAYED_PROTOCOL_NAME, CLIENT_CERT_HASH, request2);
|
||||||
|
verify(metrics).requestSent(RELAYED_PROTOCOL_NAME, CLIENT_CERT_HASH, request3);
|
||||||
|
verify(metrics).responseReceived(RELAYED_PROTOCOL_NAME, CLIENT_CERT_HASH, response1, latency1);
|
||||||
|
verify(metrics).responseReceived(RELAYED_PROTOCOL_NAME, CLIENT_CERT_HASH, response2, latency2);
|
||||||
|
verify(metrics).responseReceived(RELAYED_PROTOCOL_NAME, CLIENT_CERT_HASH, response3, latency3);
|
||||||
|
verifyNoMoreInteractions(metrics);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,330 @@
|
||||||
|
// Copyright 2017 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.proxy.handler;
|
||||||
|
|
||||||
|
import static com.google.common.truth.Truth.assertThat;
|
||||||
|
import static google.registry.proxy.TestUtils.assertHttpRequestEquivalent;
|
||||||
|
import static google.registry.proxy.TestUtils.makeEppHttpResponse;
|
||||||
|
import static google.registry.proxy.handler.ProxyProtocolHandler.REMOTE_ADDRESS_KEY;
|
||||||
|
import static google.registry.proxy.handler.SslServerInitializer.CLIENT_CERTIFICATE_PROMISE_KEY;
|
||||||
|
import static google.registry.util.X509Utils.getCertificateHash;
|
||||||
|
import static java.nio.charset.StandardCharsets.UTF_8;
|
||||||
|
import static org.junit.Assert.fail;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
import static org.mockito.Mockito.verify;
|
||||||
|
import static org.mockito.Mockito.verifyNoMoreInteractions;
|
||||||
|
|
||||||
|
import google.registry.proxy.TestUtils;
|
||||||
|
import google.registry.proxy.metric.FrontendMetrics;
|
||||||
|
import io.netty.buffer.ByteBuf;
|
||||||
|
import io.netty.buffer.Unpooled;
|
||||||
|
import io.netty.channel.ChannelInitializer;
|
||||||
|
import io.netty.channel.DefaultChannelId;
|
||||||
|
import io.netty.channel.embedded.EmbeddedChannel;
|
||||||
|
import io.netty.handler.codec.EncoderException;
|
||||||
|
import io.netty.handler.codec.http.FullHttpRequest;
|
||||||
|
import io.netty.handler.codec.http.HttpResponse;
|
||||||
|
import io.netty.handler.codec.http.HttpResponseStatus;
|
||||||
|
import io.netty.handler.codec.http.cookie.Cookie;
|
||||||
|
import io.netty.handler.codec.http.cookie.DefaultCookie;
|
||||||
|
import io.netty.handler.ssl.util.SelfSignedCertificate;
|
||||||
|
import io.netty.util.concurrent.Promise;
|
||||||
|
import java.security.cert.X509Certificate;
|
||||||
|
import org.junit.Before;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.junit.runner.RunWith;
|
||||||
|
import org.junit.runners.JUnit4;
|
||||||
|
|
||||||
|
/** Unit tests for {@link EppServiceHandler}. */
|
||||||
|
@RunWith(JUnit4.class)
|
||||||
|
public class EppServiceHandlerTest {
|
||||||
|
|
||||||
|
private static final String HELLO =
|
||||||
|
"<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>\n"
|
||||||
|
+ "<epp xmlns=\"urn:ietf:params:xml:ns:epp-1.0\">\n"
|
||||||
|
+ " <hello/>\n"
|
||||||
|
+ "</epp>\n";
|
||||||
|
|
||||||
|
private static final String RELAY_HOST = "registry.example.tld";
|
||||||
|
private static final String RELAY_PATH = "/epp";
|
||||||
|
private static final String ACCESS_TOKEN = "this.access.token";
|
||||||
|
private static final String SERVER_HOSTNAME = "epp.example.tld";
|
||||||
|
private static final String CLIENT_ADDRESS = "epp.client.tld";
|
||||||
|
private static final String PROTOCOL = "epp";
|
||||||
|
|
||||||
|
private X509Certificate clientCertificate;
|
||||||
|
|
||||||
|
private final FrontendMetrics metrics = mock(FrontendMetrics.class);
|
||||||
|
|
||||||
|
private final EppServiceHandler eppServiceHandler =
|
||||||
|
new EppServiceHandler(
|
||||||
|
RELAY_HOST,
|
||||||
|
RELAY_PATH,
|
||||||
|
() -> ACCESS_TOKEN,
|
||||||
|
SERVER_HOSTNAME,
|
||||||
|
HELLO.getBytes(UTF_8),
|
||||||
|
metrics);
|
||||||
|
|
||||||
|
private EmbeddedChannel channel;
|
||||||
|
|
||||||
|
private void setHandshakeSuccess(EmbeddedChannel channel, X509Certificate certificate)
|
||||||
|
throws Exception {
|
||||||
|
Promise<X509Certificate> unusedPromise =
|
||||||
|
channel.attr(CLIENT_CERTIFICATE_PROMISE_KEY).get().setSuccess(certificate);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setHandshakeSuccess() throws Exception {
|
||||||
|
setHandshakeSuccess(channel, clientCertificate);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setHandshakeFailure(EmbeddedChannel channel) throws Exception {
|
||||||
|
Promise<X509Certificate> unusedPromise =
|
||||||
|
channel
|
||||||
|
.attr(CLIENT_CERTIFICATE_PROMISE_KEY)
|
||||||
|
.get()
|
||||||
|
.setFailure(new Exception("Handshake Failure"));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setHandshakeFailure() throws Exception {
|
||||||
|
setHandshakeFailure(channel);
|
||||||
|
}
|
||||||
|
|
||||||
|
private FullHttpRequest makeEppHttpRequest(String content, Cookie... cookies) {
|
||||||
|
return TestUtils.makeEppHttpRequest(
|
||||||
|
content,
|
||||||
|
RELAY_HOST,
|
||||||
|
RELAY_PATH,
|
||||||
|
ACCESS_TOKEN,
|
||||||
|
getCertificateHash(clientCertificate),
|
||||||
|
SERVER_HOSTNAME,
|
||||||
|
CLIENT_ADDRESS,
|
||||||
|
cookies);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Before
|
||||||
|
public void setUp() throws Exception {
|
||||||
|
clientCertificate = new SelfSignedCertificate().cert();
|
||||||
|
channel = setUpNewChannel(eppServiceHandler);
|
||||||
|
}
|
||||||
|
|
||||||
|
private EmbeddedChannel setUpNewChannel(EppServiceHandler handler) throws Exception {
|
||||||
|
return new EmbeddedChannel(
|
||||||
|
DefaultChannelId.newInstance(),
|
||||||
|
new ChannelInitializer<EmbeddedChannel>() {
|
||||||
|
@Override
|
||||||
|
protected void initChannel(EmbeddedChannel ch) throws Exception {
|
||||||
|
ch.attr(REMOTE_ADDRESS_KEY).set(CLIENT_ADDRESS);
|
||||||
|
ch.attr(CLIENT_CERTIFICATE_PROMISE_KEY).set(ch.eventLoop().newPromise());
|
||||||
|
ch.pipeline().addLast(handler);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSuccess_connectionMetrics_oneConnection() throws Exception {
|
||||||
|
setHandshakeSuccess();
|
||||||
|
String certHash = getCertificateHash(clientCertificate);
|
||||||
|
assertThat(channel.isActive()).isTrue();
|
||||||
|
verify(metrics).registerActiveConnection(PROTOCOL, certHash, channel);
|
||||||
|
verifyNoMoreInteractions(metrics);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSuccess_connectionMetrics_twoConnections_sameClient() throws Exception {
|
||||||
|
setHandshakeSuccess();
|
||||||
|
String certHash = getCertificateHash(clientCertificate);
|
||||||
|
assertThat(channel.isActive()).isTrue();
|
||||||
|
|
||||||
|
// Setup the second channel.
|
||||||
|
EppServiceHandler eppServiceHandler2 =
|
||||||
|
new EppServiceHandler(
|
||||||
|
RELAY_HOST,
|
||||||
|
RELAY_PATH,
|
||||||
|
() -> ACCESS_TOKEN,
|
||||||
|
SERVER_HOSTNAME,
|
||||||
|
HELLO.getBytes(UTF_8),
|
||||||
|
metrics);
|
||||||
|
EmbeddedChannel channel2 = setUpNewChannel(eppServiceHandler2);
|
||||||
|
setHandshakeSuccess(channel2, clientCertificate);
|
||||||
|
|
||||||
|
assertThat(channel2.isActive()).isTrue();
|
||||||
|
|
||||||
|
verify(metrics).registerActiveConnection(PROTOCOL, certHash, channel);
|
||||||
|
verify(metrics).registerActiveConnection(PROTOCOL, certHash, channel2);
|
||||||
|
verifyNoMoreInteractions(metrics);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSuccess_connectionMetrics_twoConnections_differentClients() throws Exception {
|
||||||
|
setHandshakeSuccess();
|
||||||
|
String certHash = getCertificateHash(clientCertificate);
|
||||||
|
assertThat(channel.isActive()).isTrue();
|
||||||
|
|
||||||
|
// Setup the second channel.
|
||||||
|
EppServiceHandler eppServiceHandler2 =
|
||||||
|
new EppServiceHandler(
|
||||||
|
RELAY_HOST,
|
||||||
|
RELAY_PATH,
|
||||||
|
() -> ACCESS_TOKEN,
|
||||||
|
SERVER_HOSTNAME,
|
||||||
|
HELLO.getBytes(UTF_8),
|
||||||
|
metrics);
|
||||||
|
EmbeddedChannel channel2 = setUpNewChannel(eppServiceHandler2);
|
||||||
|
X509Certificate clientCertificate2 = new SelfSignedCertificate().cert();
|
||||||
|
setHandshakeSuccess(channel2, clientCertificate2);
|
||||||
|
String certHash2 = getCertificateHash(clientCertificate2);
|
||||||
|
|
||||||
|
assertThat(channel2.isActive()).isTrue();
|
||||||
|
|
||||||
|
verify(metrics).registerActiveConnection(PROTOCOL, certHash, channel);
|
||||||
|
verify(metrics).registerActiveConnection(PROTOCOL, certHash2, channel2);
|
||||||
|
verifyNoMoreInteractions(metrics);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSuccess_sendHelloUponHandshakeSuccess() throws Exception {
|
||||||
|
// Nothing to pass to the next handler.
|
||||||
|
assertThat((Object) channel.readInbound()).isNull();
|
||||||
|
setHandshakeSuccess();
|
||||||
|
// hello bytes should be passed to the next handler.
|
||||||
|
FullHttpRequest helloRequest = channel.readInbound();
|
||||||
|
assertThat(helloRequest).isEqualTo(makeEppHttpRequest(HELLO));
|
||||||
|
// Nothing further to pass to the next handler.
|
||||||
|
assertThat((Object) channel.readInbound()).isNull();
|
||||||
|
assertThat(channel.isActive()).isTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSuccess_disconnectUponHandshakeFailure() throws Exception {
|
||||||
|
// Nothing to pass to the next handler.
|
||||||
|
assertThat((Object) channel.readInbound()).isNull();
|
||||||
|
setHandshakeFailure();
|
||||||
|
assertThat(channel.isActive()).isFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSuccess_sendRequestToNextHandler() throws Exception {
|
||||||
|
setHandshakeSuccess();
|
||||||
|
// First inbound message is hello.
|
||||||
|
channel.readInbound();
|
||||||
|
String content = "<epp>stuff</epp>";
|
||||||
|
channel.writeInbound(Unpooled.wrappedBuffer(content.getBytes(UTF_8)));
|
||||||
|
FullHttpRequest request = channel.readInbound();
|
||||||
|
assertThat(request).isEqualTo(makeEppHttpRequest(content));
|
||||||
|
// Nothing further to pass to the next handler.
|
||||||
|
assertThat((Object) channel.readInbound()).isNull();
|
||||||
|
assertThat(channel.isActive()).isTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSuccess_sendResponseToNextHandler() throws Exception {
|
||||||
|
setHandshakeSuccess();
|
||||||
|
String content = "<epp>stuff</epp>";
|
||||||
|
channel.writeOutbound(makeEppHttpResponse(content, HttpResponseStatus.OK));
|
||||||
|
ByteBuf response = channel.readOutbound();
|
||||||
|
assertThat(response).isEqualTo(Unpooled.wrappedBuffer(content.getBytes(UTF_8)));
|
||||||
|
// Nothing further to pass to the next handler.
|
||||||
|
assertThat((Object) channel.readOutbound()).isNull();
|
||||||
|
assertThat(channel.isActive()).isTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSuccess_sendResponseToNextHandler_andDisconnect() throws Exception {
|
||||||
|
setHandshakeSuccess();
|
||||||
|
String content = "<epp>stuff</epp>";
|
||||||
|
HttpResponse response = makeEppHttpResponse(content, HttpResponseStatus.OK);
|
||||||
|
response.headers().set("Epp-Session", "close");
|
||||||
|
channel.writeOutbound(response);
|
||||||
|
ByteBuf expectedResponse = channel.readOutbound();
|
||||||
|
assertThat(expectedResponse).isEqualTo(Unpooled.wrappedBuffer(content.getBytes(UTF_8)));
|
||||||
|
// Nothing further to pass to the next handler.
|
||||||
|
assertThat((Object) channel.readOutbound()).isNull();
|
||||||
|
// Channel is disconnected.
|
||||||
|
assertThat(channel.isActive()).isFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testFailure_disconnectOnNonOKResponseStatus() throws Exception {
|
||||||
|
setHandshakeSuccess();
|
||||||
|
String content = "<epp>stuff</epp>";
|
||||||
|
try {
|
||||||
|
channel.writeOutbound(makeEppHttpResponse(content, HttpResponseStatus.BAD_REQUEST));
|
||||||
|
fail("Expected EncoderException");
|
||||||
|
} catch (EncoderException e) {
|
||||||
|
assertThat(e).hasCauseThat().isInstanceOf(IllegalArgumentException.class);
|
||||||
|
assertThat(e).hasMessageThat().contains(HttpResponseStatus.BAD_REQUEST.toString());
|
||||||
|
assertThat(channel.isActive()).isFalse();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSuccess_setCookies() throws Exception {
|
||||||
|
setHandshakeSuccess();
|
||||||
|
// First inbound message is hello.
|
||||||
|
channel.readInbound();
|
||||||
|
String responseContent = "<epp>response</epp>";
|
||||||
|
Cookie cookie1 = new DefaultCookie("name1", "value1");
|
||||||
|
Cookie cookie2 = new DefaultCookie("name2", "value2");
|
||||||
|
channel.writeOutbound(
|
||||||
|
makeEppHttpResponse(responseContent, HttpResponseStatus.OK, cookie1, cookie2));
|
||||||
|
ByteBuf response = channel.readOutbound();
|
||||||
|
assertThat(response).isEqualTo(Unpooled.wrappedBuffer(responseContent.getBytes(UTF_8)));
|
||||||
|
String requestContent = "<epp>request</epp>";
|
||||||
|
channel.writeInbound(Unpooled.wrappedBuffer(requestContent.getBytes(UTF_8)));
|
||||||
|
FullHttpRequest request = channel.readInbound();
|
||||||
|
assertHttpRequestEquivalent(request, makeEppHttpRequest(requestContent, cookie1, cookie2));
|
||||||
|
// Nothing further to pass to the next handler.
|
||||||
|
assertThat((Object) channel.readInbound()).isNull();
|
||||||
|
assertThat((Object) channel.readOutbound()).isNull();
|
||||||
|
assertThat(channel.isActive()).isTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSuccess_updateCookies() throws Exception {
|
||||||
|
setHandshakeSuccess();
|
||||||
|
// First inbound message is hello.
|
||||||
|
channel.readInbound();
|
||||||
|
String responseContent1 = "<epp>response1</epp>";
|
||||||
|
Cookie cookie1 = new DefaultCookie("name1", "value1");
|
||||||
|
Cookie cookie2 = new DefaultCookie("name2", "value2");
|
||||||
|
// First response written.
|
||||||
|
channel.writeOutbound(
|
||||||
|
makeEppHttpResponse(responseContent1, HttpResponseStatus.OK, cookie1, cookie2));
|
||||||
|
channel.readOutbound();
|
||||||
|
String requestContent1 = "<epp>request1</epp>";
|
||||||
|
// First request written.
|
||||||
|
channel.writeInbound(Unpooled.wrappedBuffer(requestContent1.getBytes(UTF_8)));
|
||||||
|
FullHttpRequest request1 = channel.readInbound();
|
||||||
|
assertHttpRequestEquivalent(request1, makeEppHttpRequest(requestContent1, cookie1, cookie2));
|
||||||
|
String responseContent2 = "<epp>response2</epp>";
|
||||||
|
Cookie cookie3 = new DefaultCookie("name3", "value3");
|
||||||
|
Cookie newCookie2 = new DefaultCookie("name2", "newValue");
|
||||||
|
// Second response written.
|
||||||
|
channel.writeOutbound(
|
||||||
|
makeEppHttpResponse(responseContent2, HttpResponseStatus.OK, cookie3, newCookie2));
|
||||||
|
channel.readOutbound();
|
||||||
|
String requestContent2 = "<epp>request2</epp>";
|
||||||
|
// Second request written.
|
||||||
|
channel.writeInbound(Unpooled.wrappedBuffer(requestContent2.getBytes(UTF_8)));
|
||||||
|
FullHttpRequest request2 = channel.readInbound();
|
||||||
|
// Cookies in second request should be updated.
|
||||||
|
assertHttpRequestEquivalent(
|
||||||
|
request2, makeEppHttpRequest(requestContent2, cookie1, newCookie2, cookie3));
|
||||||
|
// Nothing further to pass to the next handler.
|
||||||
|
assertThat((Object) channel.readInbound()).isNull();
|
||||||
|
assertThat((Object) channel.readOutbound()).isNull();
|
||||||
|
assertThat(channel.isActive()).isTrue();
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,58 @@
|
||||||
|
// Copyright 2017 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.proxy.handler;
|
||||||
|
|
||||||
|
import static com.google.common.truth.Truth.assertThat;
|
||||||
|
import static java.nio.charset.StandardCharsets.US_ASCII;
|
||||||
|
|
||||||
|
import io.netty.buffer.ByteBuf;
|
||||||
|
import io.netty.buffer.Unpooled;
|
||||||
|
import io.netty.channel.embedded.EmbeddedChannel;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.junit.runner.RunWith;
|
||||||
|
import org.junit.runners.JUnit4;
|
||||||
|
|
||||||
|
/** Unit tests for {@link HealthCheckHandler}. */
|
||||||
|
@RunWith(JUnit4.class)
|
||||||
|
public class HealthCheckHandlerTest {
|
||||||
|
|
||||||
|
private static final String CHECK_REQ = "REQUEST";
|
||||||
|
private static final String CHECK_RES = "RESPONSE";
|
||||||
|
|
||||||
|
private final HealthCheckHandler healthCheckHandler =
|
||||||
|
new HealthCheckHandler(CHECK_REQ, CHECK_RES);
|
||||||
|
private final EmbeddedChannel channel = new EmbeddedChannel(healthCheckHandler);
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSuccess_ResponseSent() {
|
||||||
|
ByteBuf input = Unpooled.wrappedBuffer(CHECK_REQ.getBytes(US_ASCII));
|
||||||
|
// No inbound message passed to the next handler.
|
||||||
|
assertThat(channel.writeInbound(input)).isFalse();
|
||||||
|
ByteBuf output = channel.readOutbound();
|
||||||
|
assertThat(channel.isActive()).isTrue();
|
||||||
|
assertThat(output.toString(US_ASCII)).isEqualTo(CHECK_RES);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSuccess_IgnoreUnrecognizedRequest() {
|
||||||
|
String unrecognizedInput = "1234567";
|
||||||
|
ByteBuf input = Unpooled.wrappedBuffer(unrecognizedInput.getBytes(US_ASCII));
|
||||||
|
// No inbound message passed to the next handler.
|
||||||
|
assertThat(channel.writeInbound(input)).isFalse();
|
||||||
|
// No response is sent.
|
||||||
|
assertThat(channel.isActive()).isTrue();
|
||||||
|
assertThat((Object) channel.readOutbound()).isNull();
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,118 @@
|
||||||
|
// Copyright 2017 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.proxy.handler;
|
||||||
|
|
||||||
|
import static com.google.common.truth.Truth.assertThat;
|
||||||
|
import static google.registry.proxy.handler.ProxyProtocolHandler.REMOTE_ADDRESS_KEY;
|
||||||
|
import static java.nio.charset.StandardCharsets.UTF_8;
|
||||||
|
|
||||||
|
import io.netty.buffer.ByteBuf;
|
||||||
|
import io.netty.buffer.Unpooled;
|
||||||
|
import io.netty.channel.embedded.EmbeddedChannel;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.junit.runner.RunWith;
|
||||||
|
import org.junit.runners.JUnit4;
|
||||||
|
|
||||||
|
/** Unit tests for {@link ProxyProtocolHandler}. */
|
||||||
|
@RunWith(JUnit4.class)
|
||||||
|
public class ProxyProtocolHandlerTest {
|
||||||
|
|
||||||
|
private static final String HEADER_TEMPLATE = "PROXY TCP%d %s %s %s %s\r\n";
|
||||||
|
|
||||||
|
private final ProxyProtocolHandler handler = new ProxyProtocolHandler();
|
||||||
|
private final EmbeddedChannel channel = new EmbeddedChannel(handler);
|
||||||
|
|
||||||
|
private String header;
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSuccess_proxyHeaderPresent_singleFrame() {
|
||||||
|
header = String.format(HEADER_TEMPLATE, 4, "172.0.0.1", "255.255.255.255", "234", "123");
|
||||||
|
String message = "some message";
|
||||||
|
// Header processed, rest of the message passed along.
|
||||||
|
assertThat(channel.writeInbound(Unpooled.wrappedBuffer((header + message).getBytes(UTF_8))))
|
||||||
|
.isTrue();
|
||||||
|
assertThat(((ByteBuf) channel.readInbound()).toString(UTF_8)).isEqualTo(message);
|
||||||
|
assertThat(channel.attr(REMOTE_ADDRESS_KEY).get()).isEqualTo("172.0.0.1");
|
||||||
|
assertThat(channel.pipeline().get(ProxyProtocolHandler.class)).isNull();
|
||||||
|
assertThat(channel.isActive()).isTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSuccess_proxyHeaderPresent_multipleFrames() {
|
||||||
|
header = String.format(HEADER_TEMPLATE, 4, "172.0.0.1", "255.255.255.255", "234", "123");
|
||||||
|
String frame1 = header.substring(0, 4);
|
||||||
|
String frame2 = header.substring(4, 7);
|
||||||
|
String frame3 = header.substring(7, 15);
|
||||||
|
String frame4 = header.substring(15, header.length() - 1);
|
||||||
|
String frame5 = header.substring(header.length() - 1) + "some message";
|
||||||
|
// Have not had enough bytes to determine the presence of a header, no message passed along.
|
||||||
|
assertThat(channel.writeInbound(Unpooled.wrappedBuffer(frame1.getBytes(UTF_8)))).isFalse();
|
||||||
|
// Have not had enough bytes to determine the end a header, no message passed along.
|
||||||
|
assertThat(channel.writeInbound(Unpooled.wrappedBuffer(frame2.getBytes(UTF_8)))).isFalse();
|
||||||
|
assertThat(channel.writeInbound(Unpooled.wrappedBuffer(frame3.getBytes(UTF_8)))).isFalse();
|
||||||
|
assertThat(channel.writeInbound(Unpooled.wrappedBuffer(frame4.getBytes(UTF_8)))).isFalse();
|
||||||
|
// Now there are enough bytes to construct a header.
|
||||||
|
assertThat(channel.writeInbound(Unpooled.wrappedBuffer(frame5.getBytes(UTF_8)))).isTrue();
|
||||||
|
assertThat(((ByteBuf) channel.readInbound()).toString(UTF_8)).isEqualTo("some message");
|
||||||
|
assertThat(channel.attr(REMOTE_ADDRESS_KEY).get()).isEqualTo("172.0.0.1");
|
||||||
|
assertThat(channel.pipeline().get(ProxyProtocolHandler.class)).isNull();
|
||||||
|
assertThat(channel.isActive()).isTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSuccess_proxyHeaderPresent_singleFrame_ipv6() {
|
||||||
|
header =
|
||||||
|
String.format(HEADER_TEMPLATE, 6, "2001:db8:0:1:1:1:1:1", "0:0:0:0:0:0:0:1", "234", "123");
|
||||||
|
String message = "some message";
|
||||||
|
// Header processed, rest of the message passed along.
|
||||||
|
assertThat(channel.writeInbound(Unpooled.wrappedBuffer((header + message).getBytes(UTF_8))))
|
||||||
|
.isTrue();
|
||||||
|
assertThat(((ByteBuf) channel.readInbound()).toString(UTF_8)).isEqualTo(message);
|
||||||
|
assertThat(channel.attr(REMOTE_ADDRESS_KEY).get()).isEqualTo("2001:db8:0:1:1:1:1:1");
|
||||||
|
assertThat(channel.pipeline().get(ProxyProtocolHandler.class)).isNull();
|
||||||
|
assertThat(channel.isActive()).isTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSuccess_proxyHeaderNotPresent_singleFrame() {
|
||||||
|
String message = "some message";
|
||||||
|
// No header present, rest of the message passed along.
|
||||||
|
assertThat(channel.writeInbound(Unpooled.wrappedBuffer(message.getBytes(UTF_8)))).isTrue();
|
||||||
|
assertThat(((ByteBuf) channel.readInbound()).toString(UTF_8)).isEqualTo(message);
|
||||||
|
assertThat(channel.attr(REMOTE_ADDRESS_KEY).get()).isNull();
|
||||||
|
assertThat(channel.pipeline().get(ProxyProtocolHandler.class)).isNull();
|
||||||
|
assertThat(channel.isActive()).isTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSuccess_proxyHeaderNotPresent_multipleFrames() {
|
||||||
|
String frame1 = "som";
|
||||||
|
String frame2 = "e mess";
|
||||||
|
String frame3 = "age\nis not";
|
||||||
|
String frame4 = "meant to be good.\n";
|
||||||
|
// Have not had enough bytes to determine the presence of a header, no message passed along.
|
||||||
|
assertThat(channel.writeInbound(Unpooled.wrappedBuffer(frame1.getBytes(UTF_8)))).isFalse();
|
||||||
|
// Now we have more than five bytes to determine if it starts with "PROXY"
|
||||||
|
assertThat(channel.writeInbound(Unpooled.wrappedBuffer(frame2.getBytes(UTF_8)))).isTrue();
|
||||||
|
assertThat(((ByteBuf) channel.readInbound()).toString(UTF_8)).isEqualTo(frame1 + frame2);
|
||||||
|
assertThat(channel.writeInbound(Unpooled.wrappedBuffer(frame3.getBytes(UTF_8)))).isTrue();
|
||||||
|
assertThat(((ByteBuf) channel.readInbound()).toString(UTF_8)).isEqualTo(frame3);
|
||||||
|
assertThat(channel.writeInbound(Unpooled.wrappedBuffer(frame4.getBytes(UTF_8)))).isTrue();
|
||||||
|
assertThat(((ByteBuf) channel.readInbound()).toString(UTF_8)).isEqualTo(frame4);
|
||||||
|
assertThat(channel.attr(REMOTE_ADDRESS_KEY).get()).isNull();
|
||||||
|
assertThat(channel.pipeline().get(ProxyProtocolHandler.class)).isNull();
|
||||||
|
assertThat(channel.isActive()).isTrue();
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,91 @@
|
||||||
|
// Copyright 2017 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.proxy.handler;
|
||||||
|
|
||||||
|
import static com.google.common.truth.Truth.assertThat;
|
||||||
|
import static google.registry.proxy.handler.RelayHandler.RELAY_CHANNEL_KEY;
|
||||||
|
|
||||||
|
import io.netty.channel.ChannelFuture;
|
||||||
|
import io.netty.channel.embedded.EmbeddedChannel;
|
||||||
|
import org.junit.Before;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.junit.runner.RunWith;
|
||||||
|
import org.junit.runners.JUnit4;
|
||||||
|
|
||||||
|
/** Unit tests for {@link RelayHandler}. */
|
||||||
|
@RunWith(JUnit4.class)
|
||||||
|
public class RelayHandlerTest {
|
||||||
|
|
||||||
|
private static final class ExpectedType {}
|
||||||
|
|
||||||
|
private static final class OtherType {}
|
||||||
|
|
||||||
|
private final RelayHandler<ExpectedType> relayHandler = new RelayHandler<>(ExpectedType.class);
|
||||||
|
private final EmbeddedChannel inboundChannel = new EmbeddedChannel(relayHandler);
|
||||||
|
private final EmbeddedChannel outboundChannel = new EmbeddedChannel();
|
||||||
|
|
||||||
|
@Before
|
||||||
|
public void setUp() {
|
||||||
|
inboundChannel.attr(RELAY_CHANNEL_KEY).set(outboundChannel);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSuccess_relayInboundMessageOfExpectedType() {
|
||||||
|
ExpectedType inboundMessage = new ExpectedType();
|
||||||
|
// Relay handler intercepted the message, no further inbound message.
|
||||||
|
assertThat(inboundChannel.writeInbound(inboundMessage)).isFalse();
|
||||||
|
// Message wrote to outbound channel as-is.
|
||||||
|
ExpectedType relayedMessage = outboundChannel.readOutbound();
|
||||||
|
assertThat(relayedMessage).isEqualTo(inboundMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSuccess_ignoreInboundMessageOfOtherType() {
|
||||||
|
OtherType inboundMessage = new OtherType();
|
||||||
|
// Relay handler ignores inbound message of other types, the inbound message is passed along.
|
||||||
|
assertThat(inboundChannel.writeInbound(inboundMessage)).isTrue();
|
||||||
|
// Nothing is written into the outbound channel.
|
||||||
|
ExpectedType relayedMessage = outboundChannel.readOutbound();
|
||||||
|
assertThat(relayedMessage).isNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSuccess_disconnectIfRelayIsUnsuccessful() {
|
||||||
|
ExpectedType inboundMessage = new ExpectedType();
|
||||||
|
// Outbound channel is closed.
|
||||||
|
outboundChannel.finish();
|
||||||
|
assertThat(inboundChannel.writeInbound(inboundMessage)).isFalse();
|
||||||
|
ExpectedType relayedMessage = outboundChannel.readOutbound();
|
||||||
|
assertThat(relayedMessage).isNull();
|
||||||
|
// Inbound channel is closed as well.
|
||||||
|
assertThat(inboundChannel.isActive()).isFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSuccess_disconnectRelayChannelIfInactive() {
|
||||||
|
ChannelFuture unusedFuture = inboundChannel.close();
|
||||||
|
assertThat(outboundChannel.isActive()).isFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSuccess_channelRead_relayNotSet() {
|
||||||
|
ExpectedType inboundMessage = new ExpectedType();
|
||||||
|
inboundChannel.attr(RELAY_CHANNEL_KEY).set(null);
|
||||||
|
// Nothing to read.
|
||||||
|
assertThat(inboundChannel.writeInbound(inboundMessage)).isFalse();
|
||||||
|
// Inbound channel is closed.
|
||||||
|
assertThat(inboundChannel.isActive()).isFalse();
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,288 @@
|
||||||
|
// Copyright 2017 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.proxy.handler;
|
||||||
|
|
||||||
|
import static com.google.common.truth.Truth.assertThat;
|
||||||
|
import static google.registry.proxy.Protocol.PROTOCOL_KEY;
|
||||||
|
import static google.registry.proxy.handler.SslInitializerTestUtils.getKeyPair;
|
||||||
|
import static google.registry.proxy.handler.SslInitializerTestUtils.setUpClient;
|
||||||
|
import static google.registry.proxy.handler.SslInitializerTestUtils.setUpServer;
|
||||||
|
import static google.registry.proxy.handler.SslInitializerTestUtils.signKeyPair;
|
||||||
|
import static google.registry.proxy.handler.SslInitializerTestUtils.verifySslChannel;
|
||||||
|
|
||||||
|
import com.google.common.collect.ImmutableList;
|
||||||
|
import google.registry.proxy.Protocol;
|
||||||
|
import google.registry.proxy.Protocol.BackendProtocol;
|
||||||
|
import google.registry.proxy.handler.SslInitializerTestUtils.DumpHandler;
|
||||||
|
import google.registry.proxy.handler.SslInitializerTestUtils.EchoHandler;
|
||||||
|
import io.netty.buffer.ByteBuf;
|
||||||
|
import io.netty.buffer.Unpooled;
|
||||||
|
import io.netty.channel.Channel;
|
||||||
|
import io.netty.channel.ChannelHandler;
|
||||||
|
import io.netty.channel.ChannelInitializer;
|
||||||
|
import io.netty.channel.ChannelPipeline;
|
||||||
|
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.handler.codec.DecoderException;
|
||||||
|
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 io.netty.util.concurrent.Future;
|
||||||
|
import java.security.KeyPair;
|
||||||
|
import java.security.PrivateKey;
|
||||||
|
import java.security.cert.CertificateException;
|
||||||
|
import java.security.cert.X509Certificate;
|
||||||
|
import java.util.concurrent.locks.Lock;
|
||||||
|
import java.util.concurrent.locks.ReentrantLock;
|
||||||
|
import javax.net.ssl.SSLException;
|
||||||
|
import javax.net.ssl.SSLHandshakeException;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.junit.runner.RunWith;
|
||||||
|
import org.junit.runners.JUnit4;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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(JUnit4.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 BackendProtocol PROTOCOL =
|
||||||
|
Protocol.backendBuilder()
|
||||||
|
.name("ssl")
|
||||||
|
.host(SSL_HOST)
|
||||||
|
.port(SSL_PORT)
|
||||||
|
.handlerProviders(ImmutableList.of())
|
||||||
|
.build();
|
||||||
|
|
||||||
|
private ChannelInitializer<LocalChannel> getServerInitializer(
|
||||||
|
PrivateKey privateKey,
|
||||||
|
X509Certificate certificate,
|
||||||
|
Lock serverLock,
|
||||||
|
Exception serverException)
|
||||||
|
throws Exception {
|
||||||
|
SslContext sslContext = SslContextBuilder.forServer(privateKey, certificate).build();
|
||||||
|
return new ChannelInitializer<LocalChannel>() {
|
||||||
|
@Override
|
||||||
|
protected void initChannel(LocalChannel ch) throws Exception {
|
||||||
|
ch.pipeline()
|
||||||
|
.addLast(
|
||||||
|
sslContext.newHandler(ch.alloc()), new EchoHandler(serverLock, serverException));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private ChannelInitializer<LocalChannel> getClientInitializer(
|
||||||
|
SslClientInitializer<LocalChannel> sslClientInitializer,
|
||||||
|
Lock clientLock,
|
||||||
|
ByteBuf buffer,
|
||||||
|
Exception clientException) {
|
||||||
|
return new ChannelInitializer<LocalChannel>() {
|
||||||
|
@Override
|
||||||
|
protected void initChannel(LocalChannel ch) throws Exception {
|
||||||
|
ch.pipeline()
|
||||||
|
.addLast(sslClientInitializer, new DumpHandler(clientLock, buffer, clientException));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSuccess_swappedInitializerWithSslHandler() throws Exception {
|
||||||
|
SslClientInitializer<EmbeddedChannel> sslClientInitializer =
|
||||||
|
new SslClientInitializer<>(SslProvider.JDK, (X509Certificate[]) null);
|
||||||
|
EmbeddedChannel channel = new EmbeddedChannel();
|
||||||
|
channel.attr(PROTOCOL_KEY).set(PROTOCOL);
|
||||||
|
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.JDK, (X509Certificate[]) null);
|
||||||
|
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");
|
||||||
|
Lock clientLock = new ReentrantLock();
|
||||||
|
Lock serverLock = new ReentrantLock();
|
||||||
|
ByteBuf buffer = Unpooled.buffer();
|
||||||
|
Exception clientException = new Exception();
|
||||||
|
Exception serverException = new Exception();
|
||||||
|
EventLoopGroup eventLoopGroup =
|
||||||
|
setUpServer(
|
||||||
|
getServerInitializer(ssc.key(), ssc.cert(), serverLock, serverException), localAddress);
|
||||||
|
SslClientInitializer<LocalChannel> sslClientInitializer =
|
||||||
|
new SslClientInitializer<>(SslProvider.JDK, (X509Certificate[]) null);
|
||||||
|
Channel channel =
|
||||||
|
setUpClient(
|
||||||
|
eventLoopGroup,
|
||||||
|
getClientInitializer(sslClientInitializer, clientLock, buffer, clientException),
|
||||||
|
localAddress,
|
||||||
|
PROTOCOL);
|
||||||
|
// Wait for handshake exception to throw.
|
||||||
|
clientLock.lock();
|
||||||
|
serverLock.lock();
|
||||||
|
// The connection is now terminated, both the client side and the server side should get
|
||||||
|
// exceptions (caught in the caughtException method in EchoHandler and DumpHandler,
|
||||||
|
// respectively).
|
||||||
|
assertThat(clientException).hasCauseThat().isInstanceOf(DecoderException.class);
|
||||||
|
assertThat(clientException)
|
||||||
|
.hasCauseThat()
|
||||||
|
.hasCauseThat()
|
||||||
|
.isInstanceOf(SSLHandshakeException.class);
|
||||||
|
assertThat(serverException).hasCauseThat().isInstanceOf(DecoderException.class);
|
||||||
|
assertThat(serverException).hasCauseThat().hasCauseThat().isInstanceOf(SSLException.class);
|
||||||
|
assertThat(channel.isActive()).isFalse();
|
||||||
|
|
||||||
|
Future<?> unusedFuture = eventLoopGroup.shutdownGracefully().syncUninterruptibly();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSuccess_customTrustManager_acceptCertSignedByTrustedCa() throws Exception {
|
||||||
|
LocalAddress localAddress =
|
||||||
|
new LocalAddress("CUSTOM_TRUST_MANAGER_ACCEPT_CERT_SIGNED_BY_TRUSTED_CA");
|
||||||
|
Lock clientLock = new ReentrantLock();
|
||||||
|
Lock serverLock = new ReentrantLock();
|
||||||
|
ByteBuf buffer = Unpooled.buffer();
|
||||||
|
Exception clientException = new Exception();
|
||||||
|
Exception serverException = new Exception();
|
||||||
|
|
||||||
|
// 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();
|
||||||
|
EventLoopGroup eventLoopGroup =
|
||||||
|
setUpServer(
|
||||||
|
getServerInitializer(privateKey, cert, serverLock, serverException), localAddress);
|
||||||
|
|
||||||
|
// Set up the client to trust the self signed cert used to sign the cert that server provides.
|
||||||
|
SslClientInitializer<LocalChannel> sslClientInitializer =
|
||||||
|
new SslClientInitializer<>(SslProvider.JDK, ssc.cert());
|
||||||
|
Channel channel =
|
||||||
|
setUpClient(
|
||||||
|
eventLoopGroup,
|
||||||
|
getClientInitializer(sslClientInitializer, clientLock, buffer, clientException),
|
||||||
|
localAddress,
|
||||||
|
PROTOCOL);
|
||||||
|
|
||||||
|
verifySslChannel(channel, ImmutableList.of(cert), clientLock, serverLock, buffer, SSL_HOST);
|
||||||
|
|
||||||
|
Future<?> unusedFuture = eventLoopGroup.shutdownGracefully().syncUninterruptibly();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testFailure_customTrustManager_wrongHostnameInCertificate() throws Exception {
|
||||||
|
LocalAddress localAddress = new LocalAddress("CUSTOM_TRUST_MANAGER_WRONG_HOSTNAME");
|
||||||
|
Lock clientLock = new ReentrantLock();
|
||||||
|
Lock serverLock = new ReentrantLock();
|
||||||
|
ByteBuf buffer = Unpooled.buffer();
|
||||||
|
Exception clientException = new Exception();
|
||||||
|
Exception serverException = new Exception();
|
||||||
|
|
||||||
|
// 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();
|
||||||
|
EventLoopGroup eventLoopGroup =
|
||||||
|
setUpServer(
|
||||||
|
getServerInitializer(privateKey, cert, serverLock, serverException), localAddress);
|
||||||
|
|
||||||
|
// Set up the client to trust the self signed cert used to sign the cert that server provides.
|
||||||
|
SslClientInitializer<LocalChannel> sslClientInitializer =
|
||||||
|
new SslClientInitializer<>(SslProvider.JDK, ssc.cert());
|
||||||
|
Channel channel =
|
||||||
|
setUpClient(
|
||||||
|
eventLoopGroup,
|
||||||
|
getClientInitializer(sslClientInitializer, clientLock, buffer, clientException),
|
||||||
|
localAddress,
|
||||||
|
PROTOCOL);
|
||||||
|
|
||||||
|
serverLock.lock();
|
||||||
|
clientLock.lock();
|
||||||
|
|
||||||
|
// When the client rejects the server cert due to wrong hostname, the client error is wrapped
|
||||||
|
// several layers in the exception. The server also throws an exception.
|
||||||
|
assertThat(clientException).hasCauseThat().isInstanceOf(DecoderException.class);
|
||||||
|
assertThat(clientException)
|
||||||
|
.hasCauseThat()
|
||||||
|
.hasCauseThat()
|
||||||
|
.isInstanceOf(SSLHandshakeException.class);
|
||||||
|
assertThat(clientException)
|
||||||
|
.hasCauseThat()
|
||||||
|
.hasCauseThat()
|
||||||
|
.hasCauseThat()
|
||||||
|
.isInstanceOf(SSLHandshakeException.class);
|
||||||
|
assertThat(clientException)
|
||||||
|
.hasCauseThat()
|
||||||
|
.hasCauseThat()
|
||||||
|
.hasCauseThat()
|
||||||
|
.hasCauseThat()
|
||||||
|
.isInstanceOf(CertificateException.class);
|
||||||
|
assertThat(clientException)
|
||||||
|
.hasCauseThat()
|
||||||
|
.hasCauseThat()
|
||||||
|
.hasCauseThat()
|
||||||
|
.hasCauseThat()
|
||||||
|
.hasMessageThat()
|
||||||
|
.contains(SSL_HOST);
|
||||||
|
assertThat(serverException).hasCauseThat().isInstanceOf(DecoderException.class);
|
||||||
|
assertThat(serverException).hasCauseThat().hasCauseThat().isInstanceOf(SSLException.class);
|
||||||
|
assertThat(channel.isActive()).isFalse();
|
||||||
|
|
||||||
|
Future<?> unusedFuture = eventLoopGroup.shutdownGracefully().syncUninterruptibly();
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,277 @@
|
||||||
|
// Copyright 2017 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.proxy.handler;
|
||||||
|
|
||||||
|
import static com.google.common.truth.Truth.assertThat;
|
||||||
|
import static google.registry.proxy.Protocol.PROTOCOL_KEY;
|
||||||
|
import static java.nio.charset.StandardCharsets.UTF_8;
|
||||||
|
|
||||||
|
import com.google.common.collect.ImmutableList;
|
||||||
|
import google.registry.proxy.Protocol.BackendProtocol;
|
||||||
|
import io.netty.bootstrap.Bootstrap;
|
||||||
|
import io.netty.bootstrap.ServerBootstrap;
|
||||||
|
import io.netty.buffer.ByteBuf;
|
||||||
|
import io.netty.buffer.Unpooled;
|
||||||
|
import io.netty.channel.Channel;
|
||||||
|
import io.netty.channel.ChannelFuture;
|
||||||
|
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.local.LocalServerChannel;
|
||||||
|
import io.netty.channel.nio.NioEventLoopGroup;
|
||||||
|
import io.netty.handler.ssl.SslHandler;
|
||||||
|
import io.netty.handler.ssl.util.SelfSignedCertificate;
|
||||||
|
import java.math.BigInteger;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
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 java.util.concurrent.locks.Lock;
|
||||||
|
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} and {@link
|
||||||
|
* SslServerInitializerTest}.
|
||||||
|
*/
|
||||||
|
public class SslInitializerTestUtils {
|
||||||
|
|
||||||
|
static {
|
||||||
|
Security.addProvider(new BouncyCastleProvider());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets up a server channel bound to the given local address.
|
||||||
|
*
|
||||||
|
* @return the event loop group used to process incoming connections.
|
||||||
|
*/
|
||||||
|
static EventLoopGroup setUpServer(
|
||||||
|
ChannelInitializer<LocalChannel> serverInitializer, LocalAddress localAddress)
|
||||||
|
throws Exception {
|
||||||
|
// Only use one thread in the event loop group. The same event loop group will be used to
|
||||||
|
// register client channels during setUpClient as well. This ensures that all I/O activities
|
||||||
|
// in both channels happen in the same thread, making debugging easier (i. e. no need to jump
|
||||||
|
// between threads when debugging, everything happens synchronously within the only I/O
|
||||||
|
// effectively). Note that the main thread is still separate from the I/O thread and
|
||||||
|
// synchronization (using the lock field) is still needed when the main thread needs to verify
|
||||||
|
// properties calculated by the I/O thread.
|
||||||
|
EventLoopGroup eventLoopGroup = new NioEventLoopGroup(1);
|
||||||
|
ServerBootstrap sb =
|
||||||
|
new ServerBootstrap()
|
||||||
|
.group(eventLoopGroup)
|
||||||
|
.channel(LocalServerChannel.class)
|
||||||
|
.childHandler(serverInitializer);
|
||||||
|
ChannelFuture unusedFuture = sb.bind(localAddress).syncUninterruptibly();
|
||||||
|
return eventLoopGroup;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets up a client channel connecting to the give local address.
|
||||||
|
*
|
||||||
|
* @param eventLoopGroup the same {@link EventLoopGroup} that is used to bootstrap server.
|
||||||
|
* @return the connected client channel.
|
||||||
|
*/
|
||||||
|
static Channel setUpClient(
|
||||||
|
EventLoopGroup eventLoopGroup,
|
||||||
|
ChannelInitializer<LocalChannel> clientInitializer,
|
||||||
|
LocalAddress localAddress,
|
||||||
|
BackendProtocol protocol)
|
||||||
|
throws Exception {
|
||||||
|
Bootstrap b =
|
||||||
|
new Bootstrap()
|
||||||
|
.group(eventLoopGroup)
|
||||||
|
.channel(LocalChannel.class)
|
||||||
|
.handler(clientInitializer)
|
||||||
|
.attr(PROTOCOL_KEY, protocol);
|
||||||
|
return b.connect(localAddress).syncUninterruptibly().channel();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** A handler that echoes back its inbound message. Used in test server. */
|
||||||
|
static class EchoHandler extends ChannelInboundHandlerAdapter {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A lock that synchronizes server I/O activity with the main thread. Acquired by the server I/O
|
||||||
|
* thread when the handler is constructed, released when the server echoes back, or when an
|
||||||
|
* exception is caught (during SSH handshake for example).
|
||||||
|
*/
|
||||||
|
private final Lock lock;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Exception that would be initialized with the exception caught during SSL handshake. This
|
||||||
|
* field is constructed in the main thread and passed in the constructor. After a failure the
|
||||||
|
* main thread can inspect this object to assert the cause of the failure.
|
||||||
|
*/
|
||||||
|
private final Exception serverException;
|
||||||
|
|
||||||
|
EchoHandler(Lock lock, Exception serverException) {
|
||||||
|
// This handler is constructed within getClientInitializer, which is called in the I/O thread.
|
||||||
|
// The server lock is therefore locked by the I/O thread.
|
||||||
|
lock.lock();
|
||||||
|
this.lock = lock;
|
||||||
|
this.serverException = serverException;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
|
||||||
|
// Always unlock regardless of whether the write is successful.
|
||||||
|
ctx.writeAndFlush(msg).addListener(future -> lock.unlock());
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Saves any inbound error into the server exception field. */
|
||||||
|
@Override
|
||||||
|
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
|
||||||
|
serverException.initCause(cause);
|
||||||
|
// If an exception is caught, we should also release the lock so that the main thread knows
|
||||||
|
// there is an exception to inspect now.
|
||||||
|
lock.unlock();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** A handler that dumps its inbound message in to {@link ByteBuf}. */
|
||||||
|
static class DumpHandler extends ChannelInboundHandlerAdapter {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A lock that synchronizes server I/O activity with the main thread. Acquired by the server I/O
|
||||||
|
* thread when the handler is constructed, released when the server echoes back, or when an
|
||||||
|
* exception is caught (during SSH handshake for example).
|
||||||
|
*/
|
||||||
|
private final Lock lock;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A Buffer that is used to store incoming message. Constructed in the main thread and passed in
|
||||||
|
* the constructor. The main thread can inspect this object to assert that the incoming message
|
||||||
|
* is as expected.
|
||||||
|
*/
|
||||||
|
private final ByteBuf buffer;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Exception that would be initialized with the exception caught during SSL handshake. This
|
||||||
|
* field is constructed in the main thread and passed in the constructor. After a failure the
|
||||||
|
* main thread can inspect this object to assert the cause of the failure.
|
||||||
|
*/
|
||||||
|
private final Exception clientException;
|
||||||
|
|
||||||
|
DumpHandler(Lock lock, ByteBuf buffer, Exception clientException) {
|
||||||
|
super();
|
||||||
|
// This handler is constructed within getClientInitializer, which is called in the I/O thread.
|
||||||
|
// The client lock is therefore locked by the I/O thread.
|
||||||
|
lock.lock();
|
||||||
|
this.lock = lock;
|
||||||
|
this.buffer = buffer;
|
||||||
|
this.clientException = clientException;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
|
||||||
|
buffer.writeBytes((ByteBuf) msg);
|
||||||
|
// If a message is received here, the main thread must be waiting to acquire the lock from
|
||||||
|
// the I/O thread in order to verify it. Releasing the lock to notify the main thread it can
|
||||||
|
// continue now that the message has been written.
|
||||||
|
lock.unlock();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Saves any inbound error into clientException. */
|
||||||
|
@Override
|
||||||
|
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
|
||||||
|
clientException.initCause(cause);
|
||||||
|
// If an exception is caught here, the main thread must be waiting to acquire the lock from
|
||||||
|
// the I/O thread in order to verify it. Releasing the lock to notify the main thread it can
|
||||||
|
// continue now that the exception has been written.
|
||||||
|
lock.unlock();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 verifySslChannel(
|
||||||
|
Channel channel,
|
||||||
|
ImmutableList<X509Certificate> certs,
|
||||||
|
Lock clientLock,
|
||||||
|
Lock serverLock,
|
||||||
|
ByteBuf buffer,
|
||||||
|
String sniHostname)
|
||||||
|
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()
|
||||||
|
.containsExactly(certs.toArray());
|
||||||
|
// Verify that the client sent expected SNI name during handshake.
|
||||||
|
assertThat(sslHandler.engine().getSSLParameters().getServerNames()).hasSize(1);
|
||||||
|
assertThat(sslHandler.engine().getSSLParameters().getServerNames().get(0).getEncoded())
|
||||||
|
.isEqualTo(sniHostname.getBytes(UTF_8));
|
||||||
|
|
||||||
|
// Test that message can go through, bound inbound and outbound.
|
||||||
|
String inputString = "Hello, world!";
|
||||||
|
// The client writes the message to the server, which echos it back. The client receives the
|
||||||
|
// echo and writes to BUFFER. All these activities happens in the I/O thread, and this call
|
||||||
|
// returns immediately.
|
||||||
|
ChannelFuture unusedFuture =
|
||||||
|
channel.writeAndFlush(
|
||||||
|
Unpooled.wrappedBuffer(inputString.getBytes(StandardCharsets.US_ASCII)));
|
||||||
|
// The lock is acquired by the I/O thread when the client's DumpHandler is constructed.
|
||||||
|
// Attempting to acquire it here blocks the main thread, until the I/O thread releases the lock
|
||||||
|
// after the DumpHandler writes the echo back to the buffer.
|
||||||
|
clientLock.lock();
|
||||||
|
serverLock.lock();
|
||||||
|
assertThat(buffer.toString(StandardCharsets.US_ASCII)).isEqualTo(inputString);
|
||||||
|
return sslHandler.engine().getSession();
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,347 @@
|
||||||
|
// Copyright 2017 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.proxy.handler;
|
||||||
|
|
||||||
|
import static com.google.common.truth.Truth.assertThat;
|
||||||
|
import static google.registry.proxy.handler.SslInitializerTestUtils.getKeyPair;
|
||||||
|
import static google.registry.proxy.handler.SslInitializerTestUtils.setUpClient;
|
||||||
|
import static google.registry.proxy.handler.SslInitializerTestUtils.setUpServer;
|
||||||
|
import static google.registry.proxy.handler.SslInitializerTestUtils.signKeyPair;
|
||||||
|
import static google.registry.proxy.handler.SslInitializerTestUtils.verifySslChannel;
|
||||||
|
|
||||||
|
import com.google.common.collect.ImmutableList;
|
||||||
|
import google.registry.proxy.Protocol;
|
||||||
|
import google.registry.proxy.Protocol.BackendProtocol;
|
||||||
|
import google.registry.proxy.handler.SslInitializerTestUtils.DumpHandler;
|
||||||
|
import google.registry.proxy.handler.SslInitializerTestUtils.EchoHandler;
|
||||||
|
import io.netty.buffer.ByteBuf;
|
||||||
|
import io.netty.buffer.Unpooled;
|
||||||
|
import io.netty.channel.Channel;
|
||||||
|
import io.netty.channel.ChannelHandler;
|
||||||
|
import io.netty.channel.ChannelInitializer;
|
||||||
|
import io.netty.channel.ChannelPipeline;
|
||||||
|
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.handler.codec.DecoderException;
|
||||||
|
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 io.netty.util.concurrent.Future;
|
||||||
|
import java.security.KeyPair;
|
||||||
|
import java.security.PrivateKey;
|
||||||
|
import java.security.cert.CertificateException;
|
||||||
|
import java.security.cert.X509Certificate;
|
||||||
|
import java.util.concurrent.locks.Lock;
|
||||||
|
import java.util.concurrent.locks.ReentrantLock;
|
||||||
|
import javax.net.ssl.SSLEngine;
|
||||||
|
import javax.net.ssl.SSLException;
|
||||||
|
import javax.net.ssl.SSLHandshakeException;
|
||||||
|
import javax.net.ssl.SSLParameters;
|
||||||
|
import javax.net.ssl.SSLSession;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.junit.runner.RunWith;
|
||||||
|
import org.junit.runners.JUnit4;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unit tests for {@link SslServerInitializer}.
|
||||||
|
*
|
||||||
|
* <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(JUnit4.class)
|
||||||
|
public class SslServerInitializerTest {
|
||||||
|
|
||||||
|
/** 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 BackendProtocol PROTOCOL =
|
||||||
|
Protocol.backendBuilder()
|
||||||
|
.name("ssl")
|
||||||
|
.host(SSL_HOST)
|
||||||
|
.port(SSL_PORT)
|
||||||
|
.handlerProviders(ImmutableList.of())
|
||||||
|
.build();
|
||||||
|
|
||||||
|
private ChannelInitializer<LocalChannel> getServerInitializer(
|
||||||
|
Lock serverLock,
|
||||||
|
Exception serverException,
|
||||||
|
PrivateKey privateKey,
|
||||||
|
X509Certificate... certificates)
|
||||||
|
throws Exception {
|
||||||
|
return new ChannelInitializer<LocalChannel>() {
|
||||||
|
@Override
|
||||||
|
protected void initChannel(LocalChannel ch) throws Exception {
|
||||||
|
ch.pipeline()
|
||||||
|
.addLast(
|
||||||
|
new SslServerInitializer<LocalChannel>(SslProvider.JDK, privateKey, certificates),
|
||||||
|
new EchoHandler(serverLock, serverException));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private ChannelInitializer<LocalChannel> getClientInitializer(
|
||||||
|
X509Certificate trustedCertificate,
|
||||||
|
PrivateKey privateKey,
|
||||||
|
X509Certificate certificate,
|
||||||
|
Lock clientLock,
|
||||||
|
ByteBuf buffer,
|
||||||
|
Exception clientException) {
|
||||||
|
return new ChannelInitializer<LocalChannel>() {
|
||||||
|
@Override
|
||||||
|
protected void initChannel(LocalChannel ch) throws Exception {
|
||||||
|
SslContextBuilder sslContextBuilder =
|
||||||
|
SslContextBuilder.forClient().trustManager(trustedCertificate);
|
||||||
|
if (privateKey != null && certificate != null) {
|
||||||
|
sslContextBuilder.keyManager(privateKey, certificate);
|
||||||
|
}
|
||||||
|
SslHandler sslHandler =
|
||||||
|
sslContextBuilder.build().newHandler(ch.alloc(), SSL_HOST, SSL_PORT);
|
||||||
|
|
||||||
|
// Enable hostname verification.
|
||||||
|
SSLEngine sslEngine = sslHandler.engine();
|
||||||
|
SSLParameters sslParameters = sslEngine.getSSLParameters();
|
||||||
|
sslParameters.setEndpointIdentificationAlgorithm("HTTPS");
|
||||||
|
sslEngine.setSSLParameters(sslParameters);
|
||||||
|
|
||||||
|
ch.pipeline().addLast("Client SSL Handler", sslHandler);
|
||||||
|
ch.pipeline().addLast(new DumpHandler(clientLock, buffer, clientException));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSuccess_swappedInitializerWithSslHandler() throws Exception {
|
||||||
|
SelfSignedCertificate ssc = new SelfSignedCertificate(SSL_HOST);
|
||||||
|
SslServerInitializer<EmbeddedChannel> sslServerInitializer =
|
||||||
|
new SslServerInitializer<>(SslProvider.JDK, ssc.key(), ssc.cert());
|
||||||
|
EmbeddedChannel channel = new EmbeddedChannel();
|
||||||
|
ChannelPipeline pipeline = channel.pipeline();
|
||||||
|
pipeline.addLast(sslServerInitializer);
|
||||||
|
ChannelHandler firstHandler = pipeline.first();
|
||||||
|
assertThat(firstHandler.getClass()).isEqualTo(SslHandler.class);
|
||||||
|
SslHandler sslHandler = (SslHandler) firstHandler;
|
||||||
|
assertThat(sslHandler.engine().getNeedClientAuth()).isTrue();
|
||||||
|
assertThat(channel.isActive()).isTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSuccess_trustAnyClientCert() throws Exception {
|
||||||
|
SelfSignedCertificate serverSsc = new SelfSignedCertificate(SSL_HOST);
|
||||||
|
LocalAddress localAddress = new LocalAddress("TRUST_ANY_CLIENT_CERT");
|
||||||
|
Lock clientLock = new ReentrantLock();
|
||||||
|
Lock serverLock = new ReentrantLock();
|
||||||
|
ByteBuf buffer = Unpooled.buffer();
|
||||||
|
Exception clientException = new Exception();
|
||||||
|
Exception serverException = new Exception();
|
||||||
|
EventLoopGroup eventLoopGroup =
|
||||||
|
setUpServer(
|
||||||
|
getServerInitializer(serverLock, serverException, serverSsc.key(), serverSsc.cert()),
|
||||||
|
localAddress);
|
||||||
|
SelfSignedCertificate clientSsc = new SelfSignedCertificate();
|
||||||
|
Channel channel =
|
||||||
|
setUpClient(
|
||||||
|
eventLoopGroup,
|
||||||
|
getClientInitializer(
|
||||||
|
serverSsc.cert(),
|
||||||
|
clientSsc.key(),
|
||||||
|
clientSsc.cert(),
|
||||||
|
clientLock,
|
||||||
|
buffer,
|
||||||
|
clientException),
|
||||||
|
localAddress,
|
||||||
|
PROTOCOL);
|
||||||
|
|
||||||
|
SSLSession sslSession =
|
||||||
|
verifySslChannel(
|
||||||
|
channel, ImmutableList.of(serverSsc.cert()), clientLock, serverLock, buffer, SSL_HOST);
|
||||||
|
// Verify that the SSL session gets the client cert. Note that this SslSession is for the client
|
||||||
|
// channel, therefore its local certificates are the remote certificates of the SslSession for
|
||||||
|
// the server channel, and vice versa.
|
||||||
|
assertThat(sslSession.getLocalCertificates()).asList().containsExactly(clientSsc.cert());
|
||||||
|
assertThat(sslSession.getPeerCertificates()).asList().containsExactly(serverSsc.cert());
|
||||||
|
|
||||||
|
Future<?> unusedFuture = eventLoopGroup.shutdownGracefully().syncUninterruptibly();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSuccess_CertSignedByOtherCA() throws Exception {
|
||||||
|
// The self-signed cert of the CA.
|
||||||
|
SelfSignedCertificate caSsc = new SelfSignedCertificate();
|
||||||
|
KeyPair keyPair = getKeyPair();
|
||||||
|
X509Certificate serverCert = signKeyPair(caSsc, keyPair, SSL_HOST);
|
||||||
|
LocalAddress localAddress = new LocalAddress("CERT_SIGNED_BY_OTHER_CA");
|
||||||
|
Lock clientLock = new ReentrantLock();
|
||||||
|
Lock serverLock = new ReentrantLock();
|
||||||
|
ByteBuf buffer = Unpooled.buffer();
|
||||||
|
Exception clientException = new Exception();
|
||||||
|
Exception serverException = new Exception();
|
||||||
|
EventLoopGroup eventLoopGroup =
|
||||||
|
setUpServer(
|
||||||
|
getServerInitializer(
|
||||||
|
serverLock,
|
||||||
|
serverException,
|
||||||
|
keyPair.getPrivate(),
|
||||||
|
// Serving both the server cert, and the CA cert
|
||||||
|
serverCert,
|
||||||
|
caSsc.cert()),
|
||||||
|
localAddress);
|
||||||
|
SelfSignedCertificate clientSsc = new SelfSignedCertificate();
|
||||||
|
Channel channel =
|
||||||
|
setUpClient(
|
||||||
|
eventLoopGroup,
|
||||||
|
getClientInitializer(
|
||||||
|
// Client trusts the CA cert
|
||||||
|
caSsc.cert(),
|
||||||
|
clientSsc.key(),
|
||||||
|
clientSsc.cert(),
|
||||||
|
clientLock,
|
||||||
|
buffer,
|
||||||
|
clientException),
|
||||||
|
localAddress,
|
||||||
|
PROTOCOL);
|
||||||
|
|
||||||
|
SSLSession sslSession =
|
||||||
|
verifySslChannel(
|
||||||
|
channel,
|
||||||
|
ImmutableList.of(serverCert, caSsc.cert()),
|
||||||
|
clientLock,
|
||||||
|
serverLock,
|
||||||
|
buffer,
|
||||||
|
SSL_HOST);
|
||||||
|
|
||||||
|
assertThat(sslSession.getLocalCertificates()).asList().containsExactly(clientSsc.cert());
|
||||||
|
assertThat(sslSession.getPeerCertificates())
|
||||||
|
.asList()
|
||||||
|
.containsExactly(serverCert, caSsc.cert())
|
||||||
|
.inOrder();
|
||||||
|
|
||||||
|
Future<?> unusedFuture = eventLoopGroup.shutdownGracefully().syncUninterruptibly();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testFailure_requireClientCertificate() throws Exception {
|
||||||
|
SelfSignedCertificate serverSsc = new SelfSignedCertificate(SSL_HOST);
|
||||||
|
LocalAddress localAddress = new LocalAddress("REQUIRE_CLIENT_CERT");
|
||||||
|
Lock clientLock = new ReentrantLock();
|
||||||
|
Lock serverLock = new ReentrantLock();
|
||||||
|
ByteBuf buffer = Unpooled.buffer();
|
||||||
|
Exception clientException = new Exception();
|
||||||
|
Exception serverException = new Exception();
|
||||||
|
EventLoopGroup eventLoopGroup =
|
||||||
|
setUpServer(
|
||||||
|
getServerInitializer(serverLock, serverException, serverSsc.key(), serverSsc.cert()),
|
||||||
|
localAddress);
|
||||||
|
Channel channel =
|
||||||
|
setUpClient(
|
||||||
|
eventLoopGroup,
|
||||||
|
getClientInitializer(
|
||||||
|
serverSsc.cert(),
|
||||||
|
// No client cert/private key used.
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
clientLock,
|
||||||
|
buffer,
|
||||||
|
clientException),
|
||||||
|
localAddress,
|
||||||
|
PROTOCOL);
|
||||||
|
|
||||||
|
serverLock.lock();
|
||||||
|
|
||||||
|
// When the server rejects the client during handshake due to lack of client certificate, only
|
||||||
|
// the server throws an exception.
|
||||||
|
assertThat(serverException).hasCauseThat().isInstanceOf(DecoderException.class);
|
||||||
|
assertThat(serverException)
|
||||||
|
.hasCauseThat()
|
||||||
|
.hasCauseThat()
|
||||||
|
.isInstanceOf(SSLHandshakeException.class);
|
||||||
|
assertThat(channel.isActive()).isFalse();
|
||||||
|
|
||||||
|
Future<?> unusedFuture = eventLoopGroup.shutdownGracefully().syncUninterruptibly();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testFailure_wrongHostnameInCertificate() throws Exception {
|
||||||
|
SelfSignedCertificate serverSsc = new SelfSignedCertificate("wrong.com");
|
||||||
|
LocalAddress localAddress = new LocalAddress("REQUIRE_CLIENT_CERT");
|
||||||
|
Lock clientLock = new ReentrantLock();
|
||||||
|
Lock serverLock = new ReentrantLock();
|
||||||
|
ByteBuf buffer = Unpooled.buffer();
|
||||||
|
Exception clientException = new Exception();
|
||||||
|
Exception serverException = new Exception();
|
||||||
|
EventLoopGroup eventLoopGroup =
|
||||||
|
setUpServer(
|
||||||
|
getServerInitializer(serverLock, serverException, serverSsc.key(), serverSsc.cert()),
|
||||||
|
localAddress);
|
||||||
|
SelfSignedCertificate clientSsc = new SelfSignedCertificate();
|
||||||
|
Channel channel =
|
||||||
|
setUpClient(
|
||||||
|
eventLoopGroup,
|
||||||
|
getClientInitializer(
|
||||||
|
serverSsc.cert(),
|
||||||
|
clientSsc.key(),
|
||||||
|
clientSsc.cert(),
|
||||||
|
clientLock,
|
||||||
|
buffer,
|
||||||
|
clientException),
|
||||||
|
localAddress,
|
||||||
|
PROTOCOL);
|
||||||
|
|
||||||
|
serverLock.lock();
|
||||||
|
clientLock.lock();
|
||||||
|
|
||||||
|
// When the client rejects the server cert due to wrong hostname, the client error is wrapped
|
||||||
|
// several layers in the exception. The server also throws an exception.
|
||||||
|
assertThat(clientException).hasCauseThat().isInstanceOf(DecoderException.class);
|
||||||
|
assertThat(clientException)
|
||||||
|
.hasCauseThat()
|
||||||
|
.hasCauseThat()
|
||||||
|
.isInstanceOf(SSLHandshakeException.class);
|
||||||
|
assertThat(clientException)
|
||||||
|
.hasCauseThat()
|
||||||
|
.hasCauseThat()
|
||||||
|
.hasCauseThat()
|
||||||
|
.isInstanceOf(SSLHandshakeException.class);
|
||||||
|
assertThat(clientException)
|
||||||
|
.hasCauseThat()
|
||||||
|
.hasCauseThat()
|
||||||
|
.hasCauseThat()
|
||||||
|
.hasCauseThat()
|
||||||
|
.isInstanceOf(CertificateException.class);
|
||||||
|
assertThat(clientException)
|
||||||
|
.hasCauseThat()
|
||||||
|
.hasCauseThat()
|
||||||
|
.hasCauseThat()
|
||||||
|
.hasCauseThat()
|
||||||
|
.hasMessageThat()
|
||||||
|
.contains(SSL_HOST);
|
||||||
|
assertThat(serverException).hasCauseThat().isInstanceOf(DecoderException.class);
|
||||||
|
assertThat(serverException).hasCauseThat().hasCauseThat().isInstanceOf(SSLException.class);
|
||||||
|
assertThat(channel.isActive()).isFalse();
|
||||||
|
|
||||||
|
Future<?> unusedFuture = eventLoopGroup.shutdownGracefully().syncUninterruptibly();
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,128 @@
|
||||||
|
// Copyright 2017 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.proxy.handler;
|
||||||
|
|
||||||
|
import static com.google.common.truth.Truth.assertThat;
|
||||||
|
import static google.registry.proxy.TestUtils.makeWhoisHttpRequest;
|
||||||
|
import static google.registry.proxy.TestUtils.makeWhoisHttpResponse;
|
||||||
|
import static java.nio.charset.StandardCharsets.US_ASCII;
|
||||||
|
import static org.junit.Assert.fail;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
import static org.mockito.Mockito.verify;
|
||||||
|
import static org.mockito.Mockito.verifyNoMoreInteractions;
|
||||||
|
|
||||||
|
import google.registry.proxy.metric.FrontendMetrics;
|
||||||
|
import io.netty.buffer.ByteBuf;
|
||||||
|
import io.netty.buffer.Unpooled;
|
||||||
|
import io.netty.channel.DefaultChannelId;
|
||||||
|
import io.netty.channel.embedded.EmbeddedChannel;
|
||||||
|
import io.netty.handler.codec.http.FullHttpRequest;
|
||||||
|
import io.netty.handler.codec.http.FullHttpResponse;
|
||||||
|
import io.netty.handler.codec.http.HttpResponseStatus;
|
||||||
|
import org.junit.Before;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.junit.runner.RunWith;
|
||||||
|
import org.junit.runners.JUnit4;
|
||||||
|
|
||||||
|
/** Unit tests for {@link WhoisServiceHandler}. */
|
||||||
|
@RunWith(JUnit4.class)
|
||||||
|
public class WhoisServiceHandlerTest {
|
||||||
|
|
||||||
|
private static final String RELAY_HOST = "www.example.tld";
|
||||||
|
private static final String RELAY_PATH = "/test";
|
||||||
|
private static final String QUERY_CONTENT = "test.tld";
|
||||||
|
private static final String ACCESS_TOKEN = "this.access.token";
|
||||||
|
private static final String PROTOCOL = "whois";
|
||||||
|
private static final String CLIENT_HASH = "none";
|
||||||
|
|
||||||
|
private final FrontendMetrics metrics = mock(FrontendMetrics.class);
|
||||||
|
|
||||||
|
private final WhoisServiceHandler whoisServiceHandler =
|
||||||
|
new WhoisServiceHandler(RELAY_HOST, RELAY_PATH, () -> ACCESS_TOKEN, metrics);
|
||||||
|
private EmbeddedChannel channel;
|
||||||
|
|
||||||
|
@Before
|
||||||
|
public void setUp() {
|
||||||
|
// Need to reset metrics for each test method, since they are static fields on the class and
|
||||||
|
// shared between each run.
|
||||||
|
channel = new EmbeddedChannel(whoisServiceHandler);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSuccess_connectionMetrics_oneChannel() {
|
||||||
|
assertThat(channel.isActive()).isTrue();
|
||||||
|
verify(metrics).registerActiveConnection(PROTOCOL, CLIENT_HASH, channel);
|
||||||
|
verifyNoMoreInteractions(metrics);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSuccess_ConnectionMetrics_twoConnections() {
|
||||||
|
assertThat(channel.isActive()).isTrue();
|
||||||
|
verify(metrics).registerActiveConnection(PROTOCOL, CLIENT_HASH, channel);
|
||||||
|
|
||||||
|
// Setup second channel.
|
||||||
|
WhoisServiceHandler whoisServiceHandler2 =
|
||||||
|
new WhoisServiceHandler(RELAY_HOST, RELAY_PATH, () -> ACCESS_TOKEN, metrics);
|
||||||
|
EmbeddedChannel channel2 =
|
||||||
|
// We need a new channel id so that it has a different hash code.
|
||||||
|
// This only is needed for EmbeddedChannel because it has a dummy hash code implementation.
|
||||||
|
new EmbeddedChannel(DefaultChannelId.newInstance(), whoisServiceHandler2);
|
||||||
|
assertThat(channel2.isActive()).isTrue();
|
||||||
|
verify(metrics).registerActiveConnection(PROTOCOL, CLIENT_HASH, channel2);
|
||||||
|
verifyNoMoreInteractions(metrics);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSuccess_fireInboundHttpRequest() {
|
||||||
|
ByteBuf inputBuffer = Unpooled.wrappedBuffer(QUERY_CONTENT.getBytes(US_ASCII));
|
||||||
|
FullHttpRequest expectedRequest =
|
||||||
|
makeWhoisHttpRequest(QUERY_CONTENT, RELAY_HOST, RELAY_PATH, ACCESS_TOKEN);
|
||||||
|
// Input data passed to next handler
|
||||||
|
assertThat(channel.writeInbound(inputBuffer)).isTrue();
|
||||||
|
FullHttpRequest inputRequest = channel.readInbound();
|
||||||
|
assertThat(inputRequest).isEqualTo(expectedRequest);
|
||||||
|
// The channel is still open, and nothing else is to be read from it.
|
||||||
|
assertThat((Object) channel.readInbound()).isNull();
|
||||||
|
assertThat(channel.isActive()).isTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSuccess_parseOutboundHttpResponse() {
|
||||||
|
String outputString = "line1\r\nline2\r\n";
|
||||||
|
FullHttpResponse outputResponse = makeWhoisHttpResponse(outputString, HttpResponseStatus.OK);
|
||||||
|
// output data passed to next handler
|
||||||
|
assertThat(channel.writeOutbound(outputResponse)).isTrue();
|
||||||
|
ByteBuf parsedBuffer = channel.readOutbound();
|
||||||
|
assertThat(parsedBuffer.toString(US_ASCII)).isEqualTo(outputString);
|
||||||
|
// The channel is still open, and nothing else is to be written to it.
|
||||||
|
assertThat((Object) channel.readOutbound()).isNull();
|
||||||
|
assertThat(channel.isActive()).isTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testFailure_OutboundHttpResponseNotOK() {
|
||||||
|
String outputString = "line1\r\nline2\r\n";
|
||||||
|
FullHttpResponse outputResponse =
|
||||||
|
makeWhoisHttpResponse(outputString, HttpResponseStatus.BAD_REQUEST);
|
||||||
|
try {
|
||||||
|
channel.writeOutbound(outputResponse);
|
||||||
|
fail("Expected failure due to non-OK HTTP response status.");
|
||||||
|
} catch (Exception e) {
|
||||||
|
assertThat(e).hasCauseThat().isInstanceOf(IllegalArgumentException.class);
|
||||||
|
assertThat(e).hasMessageThat().contains("400 Bad Request");
|
||||||
|
}
|
||||||
|
assertThat(channel.isActive()).isFalse();
|
||||||
|
}
|
||||||
|
}
|
173
javatests/google/registry/proxy/metric/BackendMetricsTest.java
Normal file
173
javatests/google/registry/proxy/metric/BackendMetricsTest.java
Normal file
|
@ -0,0 +1,173 @@
|
||||||
|
// Copyright 2017 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.proxy.metric;
|
||||||
|
|
||||||
|
import static google.registry.monitoring.metrics.contrib.DistributionMetricSubject.assertThat;
|
||||||
|
import static google.registry.monitoring.metrics.contrib.LongMetricSubject.assertThat;
|
||||||
|
import static google.registry.proxy.TestUtils.makeHttpPostRequest;
|
||||||
|
import static google.registry.proxy.TestUtils.makeHttpResponse;
|
||||||
|
|
||||||
|
import com.google.common.collect.ImmutableSet;
|
||||||
|
import io.netty.handler.codec.http.FullHttpRequest;
|
||||||
|
import io.netty.handler.codec.http.FullHttpResponse;
|
||||||
|
import io.netty.handler.codec.http.HttpResponseStatus;
|
||||||
|
import org.junit.Before;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.junit.runner.RunWith;
|
||||||
|
import org.junit.runners.JUnit4;
|
||||||
|
|
||||||
|
/** Unit tests for {@link BackendMetrics}. */
|
||||||
|
@RunWith(JUnit4.class)
|
||||||
|
public class BackendMetricsTest {
|
||||||
|
|
||||||
|
private final String host = "host.tld";
|
||||||
|
private final String certHash = "blah12345";
|
||||||
|
private final String protocol = "frontend protocol";
|
||||||
|
|
||||||
|
private final BackendMetrics metrics = new BackendMetrics();
|
||||||
|
|
||||||
|
@Before
|
||||||
|
public void setUp() {
|
||||||
|
metrics.resetMetric();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSuccess_oneRequest() {
|
||||||
|
String content = "some content";
|
||||||
|
FullHttpRequest request = makeHttpPostRequest(content, host, "/");
|
||||||
|
metrics.requestSent(protocol, certHash, request);
|
||||||
|
|
||||||
|
assertThat(BackendMetrics.requestsCounter)
|
||||||
|
.hasValueForLabels(1, protocol, certHash)
|
||||||
|
.and()
|
||||||
|
.hasNoOtherValues();
|
||||||
|
assertThat(BackendMetrics.requestBytes)
|
||||||
|
.hasDataSetForLabels(ImmutableSet.of(content.length()), protocol, certHash)
|
||||||
|
.and()
|
||||||
|
.hasNoOtherValues();
|
||||||
|
assertThat(BackendMetrics.responsesCounter).hasNoOtherValues();
|
||||||
|
assertThat(BackendMetrics.responseBytes).hasNoOtherValues();
|
||||||
|
assertThat(BackendMetrics.latencyMs).hasNoOtherValues();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSuccess_multipleRequests() {
|
||||||
|
String content1 = "some content";
|
||||||
|
String content2 = "some other content";
|
||||||
|
FullHttpRequest request1 = makeHttpPostRequest(content1, host, "/");
|
||||||
|
FullHttpRequest request2 = makeHttpPostRequest(content2, host, "/");
|
||||||
|
metrics.requestSent(protocol, certHash, request1);
|
||||||
|
metrics.requestSent(protocol, certHash, request2);
|
||||||
|
|
||||||
|
assertThat(BackendMetrics.requestsCounter)
|
||||||
|
.hasValueForLabels(2, protocol, certHash)
|
||||||
|
.and()
|
||||||
|
.hasNoOtherValues();
|
||||||
|
assertThat(BackendMetrics.requestBytes)
|
||||||
|
.hasDataSetForLabels(
|
||||||
|
ImmutableSet.of(content1.length(), content2.length()), protocol, certHash)
|
||||||
|
.and()
|
||||||
|
.hasNoOtherValues();
|
||||||
|
assertThat(BackendMetrics.responsesCounter).hasNoOtherValues();
|
||||||
|
assertThat(BackendMetrics.responseBytes).hasNoOtherValues();
|
||||||
|
assertThat(BackendMetrics.latencyMs).hasNoOtherValues();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSuccess_oneResponse() {
|
||||||
|
String content = "some response";
|
||||||
|
FullHttpResponse response = makeHttpResponse(content, HttpResponseStatus.OK);
|
||||||
|
metrics.responseReceived(protocol, certHash, response, 5);
|
||||||
|
|
||||||
|
assertThat(BackendMetrics.requestsCounter).hasNoOtherValues();
|
||||||
|
assertThat(BackendMetrics.requestBytes).hasNoOtherValues();
|
||||||
|
assertThat(BackendMetrics.responsesCounter)
|
||||||
|
.hasValueForLabels(1, protocol, certHash, "200 OK")
|
||||||
|
.and()
|
||||||
|
.hasNoOtherValues();
|
||||||
|
assertThat(BackendMetrics.responseBytes)
|
||||||
|
.hasDataSetForLabels(ImmutableSet.of(content.length()), protocol, certHash)
|
||||||
|
.and()
|
||||||
|
.hasNoOtherValues();
|
||||||
|
assertThat(BackendMetrics.latencyMs)
|
||||||
|
.hasDataSetForLabels(ImmutableSet.of(5), protocol, certHash)
|
||||||
|
.and()
|
||||||
|
.hasNoOtherValues();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSuccess_multipleResponses() {
|
||||||
|
String content1 = "some response";
|
||||||
|
String content2 = "other response";
|
||||||
|
String content3 = "a very bad response";
|
||||||
|
FullHttpResponse response1 = makeHttpResponse(content1, HttpResponseStatus.OK);
|
||||||
|
FullHttpResponse response2 = makeHttpResponse(content2, HttpResponseStatus.OK);
|
||||||
|
FullHttpResponse response3 = makeHttpResponse(content3, HttpResponseStatus.BAD_REQUEST);
|
||||||
|
metrics.responseReceived(protocol, certHash, response1, 5);
|
||||||
|
metrics.responseReceived(protocol, certHash, response2, 8);
|
||||||
|
metrics.responseReceived(protocol, certHash, response3, 2);
|
||||||
|
|
||||||
|
assertThat(BackendMetrics.requestsCounter).hasNoOtherValues();
|
||||||
|
assertThat(BackendMetrics.requestBytes).hasNoOtherValues();
|
||||||
|
assertThat(BackendMetrics.responsesCounter)
|
||||||
|
.hasValueForLabels(2, protocol, certHash, "200 OK")
|
||||||
|
.and()
|
||||||
|
.hasValueForLabels(1, protocol, certHash, "400 Bad Request")
|
||||||
|
.and()
|
||||||
|
.hasNoOtherValues();
|
||||||
|
assertThat(BackendMetrics.responseBytes)
|
||||||
|
.hasDataSetForLabels(
|
||||||
|
ImmutableSet.of(content1.length(), content2.length(), content3.length()),
|
||||||
|
protocol,
|
||||||
|
certHash)
|
||||||
|
.and()
|
||||||
|
.hasNoOtherValues();
|
||||||
|
assertThat(BackendMetrics.latencyMs)
|
||||||
|
.hasDataSetForLabels(ImmutableSet.of(5, 8, 2), protocol, certHash)
|
||||||
|
.and()
|
||||||
|
.hasNoOtherValues();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSuccess_oneRequest_oneResponse() {
|
||||||
|
String requestContent = "some request";
|
||||||
|
String responseContent = "the only response";
|
||||||
|
FullHttpRequest request = makeHttpPostRequest(requestContent, host, "/");
|
||||||
|
FullHttpResponse response = makeHttpResponse(responseContent, HttpResponseStatus.OK);
|
||||||
|
metrics.requestSent(protocol, certHash, request);
|
||||||
|
metrics.responseReceived(protocol, certHash, response, 10);
|
||||||
|
|
||||||
|
assertThat(BackendMetrics.requestsCounter)
|
||||||
|
.hasValueForLabels(1, protocol, certHash)
|
||||||
|
.and()
|
||||||
|
.hasNoOtherValues();
|
||||||
|
assertThat(BackendMetrics.responsesCounter)
|
||||||
|
.hasValueForLabels(1, protocol, certHash, "200 OK")
|
||||||
|
.and()
|
||||||
|
.hasNoOtherValues();
|
||||||
|
assertThat(BackendMetrics.requestBytes)
|
||||||
|
.hasDataSetForLabels(ImmutableSet.of(requestContent.length()), protocol, certHash)
|
||||||
|
.and()
|
||||||
|
.hasNoOtherValues();
|
||||||
|
assertThat(BackendMetrics.responseBytes)
|
||||||
|
.hasDataSetForLabels(ImmutableSet.of(responseContent.length()), protocol, certHash)
|
||||||
|
.and()
|
||||||
|
.hasNoOtherValues();
|
||||||
|
assertThat(BackendMetrics.latencyMs)
|
||||||
|
.hasDataSetForLabels(ImmutableSet.of(10), protocol, certHash)
|
||||||
|
.and()
|
||||||
|
.hasNoOtherValues();
|
||||||
|
}
|
||||||
|
}
|
166
javatests/google/registry/proxy/metric/FrontendMetricsTest.java
Normal file
166
javatests/google/registry/proxy/metric/FrontendMetricsTest.java
Normal file
|
@ -0,0 +1,166 @@
|
||||||
|
// Copyright 2017 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.proxy.metric;
|
||||||
|
|
||||||
|
import static com.google.common.truth.Truth.assertThat;
|
||||||
|
import static google.registry.monitoring.metrics.contrib.LongMetricSubject.assertThat;
|
||||||
|
|
||||||
|
import io.netty.channel.ChannelFuture;
|
||||||
|
import io.netty.channel.DefaultChannelId;
|
||||||
|
import io.netty.channel.embedded.EmbeddedChannel;
|
||||||
|
import org.junit.Before;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.junit.runner.RunWith;
|
||||||
|
import org.junit.runners.JUnit4;
|
||||||
|
|
||||||
|
/** Unit tests for {@link FrontendMetrics}. */
|
||||||
|
@RunWith(JUnit4.class)
|
||||||
|
public class FrontendMetricsTest {
|
||||||
|
|
||||||
|
private static final String PROTOCOL = "some protocol";
|
||||||
|
private static final String CERT_HASH = "abc_blah_1134zdf";
|
||||||
|
private final FrontendMetrics metrics = new FrontendMetrics();
|
||||||
|
|
||||||
|
@Before
|
||||||
|
public void setUp() {
|
||||||
|
metrics.resetMetrics();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSuccess_oneConnection() {
|
||||||
|
EmbeddedChannel channel = new EmbeddedChannel();
|
||||||
|
metrics.registerActiveConnection(PROTOCOL, CERT_HASH, channel);
|
||||||
|
assertThat(channel.isActive()).isTrue();
|
||||||
|
assertThat(FrontendMetrics.activeConnectionsGauge)
|
||||||
|
.hasValueForLabels(1, PROTOCOL, CERT_HASH)
|
||||||
|
.and()
|
||||||
|
.hasNoOtherValues();
|
||||||
|
assertThat(FrontendMetrics.totalConnectionsCounter)
|
||||||
|
.hasValueForLabels(1, PROTOCOL, CERT_HASH)
|
||||||
|
.and()
|
||||||
|
.hasNoOtherValues();
|
||||||
|
|
||||||
|
ChannelFuture unusedFuture = channel.close();
|
||||||
|
assertThat(channel.isActive()).isFalse();
|
||||||
|
assertThat(FrontendMetrics.activeConnectionsGauge).hasNoOtherValues();
|
||||||
|
assertThat(FrontendMetrics.totalConnectionsCounter)
|
||||||
|
.hasValueForLabels(1, PROTOCOL, CERT_HASH)
|
||||||
|
.and()
|
||||||
|
.hasNoOtherValues();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSuccess_twoConnections_sameClient() {
|
||||||
|
EmbeddedChannel channel1 = new EmbeddedChannel();
|
||||||
|
EmbeddedChannel channel2 = new EmbeddedChannel(DefaultChannelId.newInstance());
|
||||||
|
|
||||||
|
metrics.registerActiveConnection(PROTOCOL, CERT_HASH, channel1);
|
||||||
|
assertThat(channel1.isActive()).isTrue();
|
||||||
|
assertThat(FrontendMetrics.activeConnectionsGauge)
|
||||||
|
.hasValueForLabels(1, PROTOCOL, CERT_HASH)
|
||||||
|
.and()
|
||||||
|
.hasNoOtherValues();
|
||||||
|
assertThat(FrontendMetrics.totalConnectionsCounter)
|
||||||
|
.hasValueForLabels(1, PROTOCOL, CERT_HASH)
|
||||||
|
.and()
|
||||||
|
.hasNoOtherValues();
|
||||||
|
|
||||||
|
metrics.registerActiveConnection(PROTOCOL, CERT_HASH, channel2);
|
||||||
|
assertThat(channel2.isActive()).isTrue();
|
||||||
|
assertThat(FrontendMetrics.activeConnectionsGauge)
|
||||||
|
.hasValueForLabels(2, PROTOCOL, CERT_HASH)
|
||||||
|
.and()
|
||||||
|
.hasNoOtherValues();
|
||||||
|
assertThat(FrontendMetrics.totalConnectionsCounter)
|
||||||
|
.hasValueForLabels(2, PROTOCOL, CERT_HASH)
|
||||||
|
.and()
|
||||||
|
.hasNoOtherValues();
|
||||||
|
|
||||||
|
ChannelFuture unusedFuture = channel1.close();
|
||||||
|
assertThat(channel1.isActive()).isFalse();
|
||||||
|
assertThat(FrontendMetrics.activeConnectionsGauge)
|
||||||
|
.hasValueForLabels(1, PROTOCOL, CERT_HASH)
|
||||||
|
.and()
|
||||||
|
.hasNoOtherValues();
|
||||||
|
assertThat(FrontendMetrics.totalConnectionsCounter)
|
||||||
|
.hasValueForLabels(2, PROTOCOL, CERT_HASH)
|
||||||
|
.and()
|
||||||
|
.hasNoOtherValues();
|
||||||
|
|
||||||
|
unusedFuture = channel2.close();
|
||||||
|
assertThat(channel2.isActive()).isFalse();
|
||||||
|
assertThat(FrontendMetrics.activeConnectionsGauge).hasNoOtherValues();
|
||||||
|
assertThat(FrontendMetrics.totalConnectionsCounter)
|
||||||
|
.hasValueForLabels(2, PROTOCOL, CERT_HASH)
|
||||||
|
.and()
|
||||||
|
.hasNoOtherValues();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSuccess_twoConnections_differentClients() {
|
||||||
|
EmbeddedChannel channel1 = new EmbeddedChannel();
|
||||||
|
EmbeddedChannel channel2 = new EmbeddedChannel(DefaultChannelId.newInstance());
|
||||||
|
String certHash2 = "blahblah_lol_234";
|
||||||
|
|
||||||
|
metrics.registerActiveConnection(PROTOCOL, CERT_HASH, channel1);
|
||||||
|
assertThat(channel1.isActive()).isTrue();
|
||||||
|
assertThat(FrontendMetrics.activeConnectionsGauge)
|
||||||
|
.hasValueForLabels(1, PROTOCOL, CERT_HASH)
|
||||||
|
.and()
|
||||||
|
.hasNoOtherValues();
|
||||||
|
assertThat(FrontendMetrics.totalConnectionsCounter)
|
||||||
|
.hasValueForLabels(1, PROTOCOL, CERT_HASH)
|
||||||
|
.and()
|
||||||
|
.hasNoOtherValues();
|
||||||
|
|
||||||
|
metrics.registerActiveConnection(PROTOCOL, certHash2, channel2);
|
||||||
|
assertThat(channel2.isActive()).isTrue();
|
||||||
|
assertThat(FrontendMetrics.activeConnectionsGauge)
|
||||||
|
.hasValueForLabels(1, PROTOCOL, CERT_HASH)
|
||||||
|
.and()
|
||||||
|
.hasValueForLabels(1, PROTOCOL, certHash2)
|
||||||
|
.and()
|
||||||
|
.hasNoOtherValues();
|
||||||
|
assertThat(FrontendMetrics.totalConnectionsCounter)
|
||||||
|
.hasValueForLabels(1, PROTOCOL, CERT_HASH)
|
||||||
|
.and()
|
||||||
|
.hasValueForLabels(1, PROTOCOL, certHash2)
|
||||||
|
.and()
|
||||||
|
.hasNoOtherValues();
|
||||||
|
|
||||||
|
ChannelFuture unusedFuture = channel1.close();
|
||||||
|
assertThat(channel1.isActive()).isFalse();
|
||||||
|
assertThat(FrontendMetrics.activeConnectionsGauge)
|
||||||
|
.hasValueForLabels(1, PROTOCOL, certHash2)
|
||||||
|
.and()
|
||||||
|
.hasNoOtherValues();
|
||||||
|
assertThat(FrontendMetrics.totalConnectionsCounter)
|
||||||
|
.hasValueForLabels(1, PROTOCOL, CERT_HASH)
|
||||||
|
.and()
|
||||||
|
.hasValueForLabels(1, PROTOCOL, certHash2)
|
||||||
|
.and()
|
||||||
|
.hasNoOtherValues();
|
||||||
|
|
||||||
|
unusedFuture = channel2.close();
|
||||||
|
assertThat(channel2.isActive()).isFalse();
|
||||||
|
assertThat(FrontendMetrics.activeConnectionsGauge).hasNoOtherValues();
|
||||||
|
assertThat(FrontendMetrics.totalConnectionsCounter)
|
||||||
|
.hasValueForLabels(1, PROTOCOL, CERT_HASH)
|
||||||
|
.and()
|
||||||
|
.hasValueForLabels(1, PROTOCOL, certHash2)
|
||||||
|
.and()
|
||||||
|
.hasNoOtherValues();
|
||||||
|
}
|
||||||
|
}
|
23
javatests/google/registry/proxy/testdata/login.xml
vendored
Normal file
23
javatests/google/registry/proxy/testdata/login.xml
vendored
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<epp xmlns="urn:ietf:params:xml:ns:epp-1.0">
|
||||||
|
<command>
|
||||||
|
<login>
|
||||||
|
<clID>ClientX</clID>
|
||||||
|
<pw>foo-BAR2</pw>
|
||||||
|
<newPW>bar-FOO2</newPW>
|
||||||
|
<options>
|
||||||
|
<version>1.0</version>
|
||||||
|
<lang>en</lang>
|
||||||
|
</options>
|
||||||
|
<svcs>
|
||||||
|
<objURI>urn:ietf:params:xml:ns:obj1</objURI>
|
||||||
|
<objURI>urn:ietf:params:xml:ns:obj2</objURI>
|
||||||
|
<objURI>urn:ietf:params:xml:ns:obj3</objURI>
|
||||||
|
<svcExtension>
|
||||||
|
<extURI>http://custom/obj1ext-1.0</extURI>
|
||||||
|
</svcExtension>
|
||||||
|
</svcs>
|
||||||
|
</login>
|
||||||
|
<clTRID>ABC-12345</clTRID>
|
||||||
|
</command>
|
||||||
|
</epp>
|
12
javatests/google/registry/proxy/testdata/login_response.xml
vendored
Normal file
12
javatests/google/registry/proxy/testdata/login_response.xml
vendored
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||||
|
<epp xmlns:domain="urn:ietf:params:xml:ns:domain-1.0" xmlns:contact="urn:ietf:params:xml:ns:contact-1.0" xmlns:host="urn:ietf:params:xml:ns:host-1.0" xmlns:launch="urn:ietf:params:xml:ns:launch-1.0" xmlns:rgp="urn:ietf:params:xml:ns:rgp-1.0" xmlns="urn:ietf:params:xml:ns:epp-1.0" xmlns:secDNS="urn:ietf:params:xml:ns:secDNS-1.1" xmlns:fee12="urn:ietf:params:xml:ns:fee-0.12" xmlns:fee11="urn:ietf:params:xml:ns:fee-0.11" xmlns:mark="urn:ietf:params:xml:ns:mark-1.0" xmlns:fee="urn:ietf:params:xml:ns:fee-0.6">
|
||||||
|
<response>
|
||||||
|
<result code="1000">
|
||||||
|
<msg>Command completed successfully</msg>
|
||||||
|
</result>
|
||||||
|
<trID>
|
||||||
|
<clTRID>proxy-login</clTRID>
|
||||||
|
<svTRID>inlxipwsQKaXS3VmbKOmBA==-a</svTRID>
|
||||||
|
</trID>
|
||||||
|
</response>
|
||||||
|
</epp>
|
7
javatests/google/registry/proxy/testdata/logout.xml
vendored
Normal file
7
javatests/google/registry/proxy/testdata/logout.xml
vendored
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<epp xmlns="urn:ietf:params:xml:ns:epp-1.0">
|
||||||
|
<command>
|
||||||
|
<logout/>
|
||||||
|
<clTRID>ABC-12345</clTRID>
|
||||||
|
</command>
|
||||||
|
</epp>
|
12
javatests/google/registry/proxy/testdata/logout_response.xml
vendored
Normal file
12
javatests/google/registry/proxy/testdata/logout_response.xml
vendored
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||||
|
<epp xmlns:domain="urn:ietf:params:xml:ns:domain-1.0" xmlns:contact="urn:ietf:params:xml:ns:contact-1.0" xmlns:host="urn:ietf:params:xml:ns:host-1.0" xmlns:launch="urn:ietf:params:xml:ns:launch-1.0" xmlns:rgp="urn:ietf:params:xml:ns:rgp-1.0" xmlns="urn:ietf:params:xml:ns:epp-1.0" xmlns:secDNS="urn:ietf:params:xml:ns:secDNS-1.1" xmlns:fee12="urn:ietf:params:xml:ns:fee-0.12" xmlns:fee11="urn:ietf:params:xml:ns:fee-0.11" xmlns:mark="urn:ietf:params:xml:ns:mark-1.0" xmlns:fee="urn:ietf:params:xml:ns:fee-0.6">
|
||||||
|
<response>
|
||||||
|
<result code="1500">
|
||||||
|
<msg>Command completed successfully; ending session</msg>
|
||||||
|
</result>
|
||||||
|
<trID>
|
||||||
|
<clTRID>proxy-logout</clTRID>
|
||||||
|
<svTRID>inlxipwsQKaXS3VmbKOmBA==-c</svTRID>
|
||||||
|
</trID>
|
||||||
|
</response>
|
||||||
|
</epp>
|
Loading…
Add table
Add a link
Reference in a new issue