diff --git a/core/src/main/java/google/registry/tools/CurlCommand.java b/core/src/main/java/google/registry/tools/CurlCommand.java
index 1b3c234fa..df3be3d13 100644
--- a/core/src/main/java/google/registry/tools/CurlCommand.java
+++ b/core/src/main/java/google/registry/tools/CurlCommand.java
@@ -21,6 +21,7 @@ import com.beust.jcommander.IStringConverter;
import com.beust.jcommander.Parameter;
import com.beust.jcommander.Parameters;
import com.beust.jcommander.converters.IParameterSplitter;
+import com.google.common.base.Ascii;
import com.google.common.base.Joiner;
import com.google.common.base.Splitter;
import com.google.common.collect.ImmutableList;
@@ -103,7 +104,10 @@ class CurlCommand implements CommandWithConnection {
throw new IllegalArgumentException("You may not specify a body for a get method.");
}
- Service service = useGke ? GkeService.valueOf(serviceName) : GaeService.valueOf(serviceName);
+ Service service =
+ useGke
+ ? GkeService.valueOf(Ascii.toUpperCase(serviceName))
+ : GaeService.valueOf(Ascii.toUpperCase(serviceName));
ServiceConnection connectionToService = connection.withService(service, canary);
String response =
diff --git a/core/src/main/java/google/registry/tools/ServiceConnection.java b/core/src/main/java/google/registry/tools/ServiceConnection.java
index 29f725870..961ccd94d 100644
--- a/core/src/main/java/google/registry/tools/ServiceConnection.java
+++ b/core/src/main/java/google/registry/tools/ServiceConnection.java
@@ -14,7 +14,6 @@
package google.registry.tools;
-import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Strings.isNullOrEmpty;
import static com.google.common.base.Verify.verify;
@@ -35,7 +34,6 @@ import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.io.CharStreams;
import com.google.common.net.MediaType;
-import com.google.re2j.Matcher;
import com.google.re2j.Pattern;
import google.registry.config.RegistryConfig.Config;
import google.registry.request.Action.GaeService;
@@ -60,6 +58,8 @@ public class ServiceConnection {
/** Pattern to heuristically extract title tag contents in HTML responses. */
protected static final Pattern HTML_TITLE_TAG_PATTERN = Pattern.compile("
(.*?)");
+ private static final String CANARY_HEADER = "canary";
+
private final Service service;
private final boolean useCanary;
private final HttpRequestFactory requestFactory;
@@ -70,9 +70,6 @@ public class ServiceConnection {
}
private ServiceConnection(Service service, HttpRequestFactory requestFactory, boolean useCanary) {
- // Currently, only GAE supports connecting to canary.
- // TODO (jianglai): decide how to implement canary for GKE.
- checkArgument(useCanary == false || service instanceof GaeService, "Canary is only for GAE");
this.service = service;
this.requestFactory = requestFactory;
this.useCanary = useCanary;
@@ -80,15 +77,17 @@ public class ServiceConnection {
/** Returns a copy of this connection that talks to a different service endpoint. */
public ServiceConnection withService(Service service, boolean useCanary) {
+ Class extends Service> oldServiceClazz = this.service.getClass();
+ Class extends Service> newServiceClazz = service.getClass();
+ if (oldServiceClazz != newServiceClazz) {
+ throw new IllegalArgumentException(
+ String.format(
+ "Cannot switch from %s to %s",
+ oldServiceClazz.getSimpleName(), newServiceClazz.getSimpleName()));
+ }
return new ServiceConnection(service, requestFactory, useCanary);
}
- /** Returns the contents of the title tag in the given HTML, or null if not found. */
- private static String extractHtmlTitle(String html) {
- Matcher matcher = HTML_TITLE_TAG_PATTERN.matcher(html);
- return (matcher.find() ? matcher.group(1) : null);
- }
-
/** Returns the HTML from the connection error stream, if any, otherwise the empty string. */
private static String getErrorHtmlAsString(HttpResponse response) throws IOException {
return CharStreams.toString(new InputStreamReader(response.getContent(), UTF_8));
@@ -107,19 +106,22 @@ public class ServiceConnection {
HttpHeaders headers = request.getHeaders();
headers.setCacheControl("no-cache");
headers.put(X_REQUESTED_WITH, ImmutableList.of("RegistryTool"));
+ if (useCanary) {
+ headers.set(CANARY_HEADER, "true");
+ }
request.setHeaders(headers);
request.setFollowRedirects(false);
request.setThrowExceptionOnExecuteError(false);
request.setUnsuccessfulResponseHandler(
(request1, response, supportsRetry) -> {
- String errorTitle = extractHtmlTitle(getErrorHtmlAsString(response));
+ String error = getErrorHtmlAsString(response);
throw new IOException(
String.format(
"Error from %s: %d %s%s",
request1.getUrl().toString(),
response.getStatusCode(),
response.getStatusMessage(),
- (errorTitle == null ? "" : ": " + errorTitle)));
+ error));
});
HttpResponse response = null;
try {
@@ -135,8 +137,8 @@ public class ServiceConnection {
@VisibleForTesting
URL getServer() {
URL url = service.getServiceUrl();
- if (useCanary) {
- verify(!isNullOrEmpty(url.getHost()), "Null host in url");
+ verify(!isNullOrEmpty(url.getHost()), "Null host in url");
+ if (useCanary && service instanceof GaeService) {
url =
makeUrl(
String.format(
diff --git a/core/src/test/java/google/registry/tools/ServiceConnectionTest.java b/core/src/test/java/google/registry/tools/ServiceConnectionTest.java
index d04f2a7c0..87e65be38 100644
--- a/core/src/test/java/google/registry/tools/ServiceConnectionTest.java
+++ b/core/src/test/java/google/registry/tools/ServiceConnectionTest.java
@@ -16,23 +16,65 @@ package google.registry.tools;
import static com.google.common.truth.Truth.assertThat;
import static google.registry.request.Action.GaeService.DEFAULT;
+import static org.junit.Assert.assertThrows;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+import com.google.api.client.http.GenericUrl;
+import com.google.api.client.http.HttpHeaders;
+import com.google.api.client.http.HttpRequest;
+import com.google.api.client.http.HttpRequestFactory;
+import com.google.api.client.http.HttpResponse;
+import com.google.common.collect.ImmutableMap;
+import google.registry.request.Action.GkeService;
+import java.io.ByteArrayInputStream;
import org.junit.jupiter.api.Test;
/** Unit tests for {@link google.registry.tools.ServiceConnection}. */
public class ServiceConnectionTest {
@Test
- void testServerUrl_notCanary() {
+ void testSuccess_serverUrl_notCanary() {
ServiceConnection connection = new ServiceConnection(false, null).withService(DEFAULT, false);
String serverUrl = connection.getServer().toString();
assertThat(serverUrl).isEqualTo("https://default.example.com"); // See default-config.yaml
}
@Test
- void testServerUrl_canary() {
+ void testFailure_mixedService() throws Exception {
+ IllegalArgumentException thrown =
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> {
+ new ServiceConnection(true, null).withService(DEFAULT, true);
+ });
+ assertThat(thrown).hasMessageThat().contains("Cannot switch from GkeService to GaeService");
+ }
+
+ @Test
+ void testSuccess_serverUrl_gae_canary() {
ServiceConnection connection = new ServiceConnection(false, null).withService(DEFAULT, true);
String serverUrl = connection.getServer().toString();
assertThat(serverUrl).isEqualTo("https://nomulus-dot-default.example.com");
}
+
+ @Test
+ void testSuccess_serverUrl_gke_canary() throws Exception {
+ HttpRequestFactory factory = mock(HttpRequestFactory.class);
+ HttpRequest request = mock(HttpRequest.class);
+ HttpHeaders headers = mock(HttpHeaders.class);
+ HttpResponse response = mock(HttpResponse.class);
+ when(request.getHeaders()).thenReturn(headers);
+ when(factory.buildGetRequest(any(GenericUrl.class))).thenReturn(request);
+ when(request.execute()).thenReturn(response);
+ when(response.getContent()).thenReturn(ByteArrayInputStream.nullInputStream());
+ ServiceConnection connection =
+ new ServiceConnection(true, factory).withService(GkeService.PUBAPI, true);
+ String serverUrl = connection.getServer().toString();
+ assertThat(serverUrl).isEqualTo("https://pubapi.registry.test");
+ connection.sendGetRequest("/path", ImmutableMap.of());
+ verify(headers).set("canary", "true");
+ }
}
diff --git a/jetty/deploy-nomulus-for-env.sh b/jetty/deploy-nomulus-for-env.sh
index aa832983c..22154657e 100755
--- a/jetty/deploy-nomulus-for-env.sh
+++ b/jetty/deploy-nomulus-for-env.sh
@@ -37,6 +37,11 @@ do
sed s/GCP_PROJECT/"${project}"/g "./kubernetes/nomulus-${service}.yaml" | \
sed s/ENVIRONMENT/"${environment}"/g | \
kubectl apply -f -
+ # canary
+ sed s/GCP_PROJECT/"${project}"/g "./kubernetes/nomulus-${service}.yaml" | \
+ sed s/ENVIRONMENT/"${environment}"/g | \
+ sed s/"${service}"/"${service}-canary"/g | \
+ kubectl apply -f -
done
# Kills all running pods, new pods created will be pulling the new image.
kubectl delete pods --all
diff --git a/jetty/kubernetes/gateway/nomulus-route-backend.yaml b/jetty/kubernetes/gateway/nomulus-route-backend.yaml
index e77a041ed..c632a8cec 100644
--- a/jetty/kubernetes/gateway/nomulus-route-backend.yaml
+++ b/jetty/kubernetes/gateway/nomulus-route-backend.yaml
@@ -30,6 +30,42 @@ spec:
kind: ServiceImport
name: backend
port: 80
+ - matches:
+ - path:
+ type: PathPrefix
+ value: /_dr/task
+ headers:
+ - name: "canary"
+ value: "true"
+ - path:
+ type: PathPrefix
+ value: /_dr/cron
+ headers:
+ - name: "canary"
+ value: "true"
+ - path:
+ type: PathPrefix
+ value: /_dr/admin
+ headers:
+ - name: "canary"
+ value: "true"
+ - path:
+ type: PathPrefix
+ value: /_dr/epptool
+ headers:
+ - name: "canary"
+ value: "true"
+ - path:
+ type: PathPrefix
+ value: /loadtest
+ headers:
+ - name: "canary"
+ value: "true"
+ backendRefs:
+ - group: net.gke.io
+ kind: ServiceImport
+ name: backend-canary
+ port: 80
---
apiVersion: networking.gke.io/v1
kind: HealthCheckPolicy
@@ -45,3 +81,18 @@ spec:
group: net.gke.io
kind: ServiceImport
name: backend
+---
+apiVersion: networking.gke.io/v1
+kind: HealthCheckPolicy
+metadata:
+ name: backend-canary
+spec:
+ default:
+ config:
+ type: HTTP
+ httpHealthCheck:
+ requestPath: /healthz/
+ targetRef:
+ group: net.gke.io
+ kind: ServiceImport
+ name: backend-canary
diff --git a/jetty/kubernetes/gateway/nomulus-route-console.yaml b/jetty/kubernetes/gateway/nomulus-route-console.yaml
index 87a2722d9..e855f942d 100644
--- a/jetty/kubernetes/gateway/nomulus-route-console.yaml
+++ b/jetty/kubernetes/gateway/nomulus-route-console.yaml
@@ -21,6 +21,24 @@ spec:
kind: ServiceImport
name: console
port: 80
+ - matches:
+ - path:
+ type: PathPrefix
+ value: /console-api
+ headers:
+ - name: "canary"
+ value: "true"
+ - path:
+ type: PathPrefix
+ value: /console
+ headers:
+ - name: "canary"
+ value: "true"
+ backendRefs:
+ - group: net.gke.io
+ kind: ServiceImport
+ name: console-canary
+ port: 80
---
apiVersion: networking.gke.io/v1
kind: HealthCheckPolicy
@@ -36,3 +54,18 @@ spec:
group: net.gke.io
kind: ServiceImport
name: console
+---
+apiVersion: networking.gke.io/v1
+kind: HealthCheckPolicy
+metadata:
+ name: console-canary
+spec:
+ default:
+ config:
+ type: HTTP
+ httpHealthCheck:
+ requestPath: /healthz/
+ targetRef:
+ group: net.gke.io
+ kind: ServiceImport
+ name: console-canary
diff --git a/jetty/kubernetes/gateway/nomulus-route-frontend.yaml b/jetty/kubernetes/gateway/nomulus-route-frontend.yaml
index e5edd08f8..af40395f5 100644
--- a/jetty/kubernetes/gateway/nomulus-route-frontend.yaml
+++ b/jetty/kubernetes/gateway/nomulus-route-frontend.yaml
@@ -18,6 +18,18 @@ spec:
kind: ServiceImport
name: frontend
port: 80
+ - matches:
+ - path:
+ type: PathPrefix
+ value: /_dr/epp
+ headers:
+ - name: "canary"
+ value: "true"
+ backendRefs:
+ - group: net.gke.io
+ kind: ServiceImport
+ name: frontend-canary
+ port: 80
---
apiVersion: networking.gke.io/v1
kind: HealthCheckPolicy
@@ -33,3 +45,18 @@ spec:
group: net.gke.io
kind: ServiceImport
name: frontend
+---
+apiVersion: networking.gke.io/v1
+kind: HealthCheckPolicy
+metadata:
+ name: frontend-canary
+spec:
+ default:
+ config:
+ type: HTTP
+ httpHealthCheck:
+ requestPath: /healthz/
+ targetRef:
+ group: net.gke.io
+ kind: ServiceImport
+ name: frontend-canary
diff --git a/jetty/kubernetes/gateway/nomulus-route-pubapi.yaml b/jetty/kubernetes/gateway/nomulus-route-pubapi.yaml
index c38e62f31..e0acfd283 100644
--- a/jetty/kubernetes/gateway/nomulus-route-pubapi.yaml
+++ b/jetty/kubernetes/gateway/nomulus-route-pubapi.yaml
@@ -27,6 +27,36 @@ spec:
kind: ServiceImport
name: pubapi
port: 80
+ - matches:
+ - path:
+ type: PathPrefix
+ value: /_dr/whois
+ headers:
+ - name: "canary"
+ value: "true"
+ - path:
+ type: PathPrefix
+ value: /check
+ headers:
+ - name: "canary"
+ value: "true"
+ - path:
+ type: PathPrefix
+ value: /whois
+ headers:
+ - name: "canary"
+ value: "true"
+ - path:
+ type: PathPrefix
+ value: /rdap
+ headers:
+ - name: "canary"
+ value: "true"
+ backendRefs:
+ - group: net.gke.io
+ kind: ServiceImport
+ name: pubapi-canary
+ port: 80
---
apiVersion: networking.gke.io/v1
kind: HealthCheckPolicy
@@ -42,3 +72,18 @@ spec:
group: net.gke.io
kind: ServiceImport
name: pubapi
+---
+apiVersion: networking.gke.io/v1
+kind: HealthCheckPolicy
+metadata:
+ name: pubapi-canary
+spec:
+ default:
+ config:
+ type: HTTP
+ httpHealthCheck:
+ requestPath: /healthz/
+ targetRef:
+ group: net.gke.io
+ kind: ServiceImport
+ name: pubapi-canary
diff --git a/proxy/src/main/java/google/registry/proxy/EppProtocolModule.java b/proxy/src/main/java/google/registry/proxy/EppProtocolModule.java
index 3982eac2c..908cbd0f4 100644
--- a/proxy/src/main/java/google/registry/proxy/EppProtocolModule.java
+++ b/proxy/src/main/java/google/registry/proxy/EppProtocolModule.java
@@ -151,12 +151,14 @@ public final class EppProtocolModule {
static EppServiceHandler provideEppServiceHandler(
@Named("idToken") Supplier idTokenSupplier,
@Named("hello") byte[] helloBytes,
+ @Named("canary") boolean canary,
FrontendMetrics metrics,
ProxyConfig config,
@HttpsRelayProtocol boolean localRelay) {
return new EppServiceHandler(
localRelay ? "localhost" : config.epp.relayHost,
config.epp.relayPath,
+ canary,
idTokenSupplier,
helloBytes,
metrics);
diff --git a/proxy/src/main/java/google/registry/proxy/ProxyConfig.java b/proxy/src/main/java/google/registry/proxy/ProxyConfig.java
index 0fd492356..cd88c9a90 100644
--- a/proxy/src/main/java/google/registry/proxy/ProxyConfig.java
+++ b/proxy/src/main/java/google/registry/proxy/ProxyConfig.java
@@ -41,6 +41,7 @@ public class ProxyConfig {
public String projectId;
public String oauthClientId;
+ public boolean canary;
public List gcpScopes;
public int serverCertificateCacheSeconds;
public Gcs gcs;
diff --git a/proxy/src/main/java/google/registry/proxy/ProxyModule.java b/proxy/src/main/java/google/registry/proxy/ProxyModule.java
index b1353018b..ab2a26605 100644
--- a/proxy/src/main/java/google/registry/proxy/ProxyModule.java
+++ b/proxy/src/main/java/google/registry/proxy/ProxyModule.java
@@ -278,6 +278,13 @@ public class ProxyModule {
() -> OidcTokenUtils.createOidcToken(credentialsBundle, clientId), 1, TimeUnit.HOURS);
}
+ @Singleton
+ @Provides
+ @Named("canary")
+ static boolean provideIsCanary(ProxyConfig config) {
+ return config.canary;
+ }
+
@Singleton
@Provides
static CloudKMS provideCloudKms(GoogleCredentialsBundle credentialsBundle, ProxyConfig config) {
diff --git a/proxy/src/main/java/google/registry/proxy/WhoisProtocolModule.java b/proxy/src/main/java/google/registry/proxy/WhoisProtocolModule.java
index dcf79902a..e5d9d7ee9 100644
--- a/proxy/src/main/java/google/registry/proxy/WhoisProtocolModule.java
+++ b/proxy/src/main/java/google/registry/proxy/WhoisProtocolModule.java
@@ -96,11 +96,13 @@ public final class WhoisProtocolModule {
static WhoisServiceHandler provideWhoisServiceHandler(
ProxyConfig config,
@Named("idToken") Supplier idTokenSupplier,
+ @Named("canary") boolean canary,
FrontendMetrics metrics,
@HttpsRelayProtocol boolean localRelay) {
return new WhoisServiceHandler(
localRelay ? "localhost" : config.whois.relayHost,
config.whois.relayPath,
+ canary,
idTokenSupplier,
metrics);
}
diff --git a/proxy/src/main/java/google/registry/proxy/config/default-config.yaml b/proxy/src/main/java/google/registry/proxy/config/default-config.yaml
index ed6fd0eab..0e29e9a28 100644
--- a/proxy/src/main/java/google/registry/proxy/config/default-config.yaml
+++ b/proxy/src/main/java/google/registry/proxy/config/default-config.yaml
@@ -8,6 +8,9 @@
# GCP project ID
projectId: your-gcp-project-id
+# Whether to connect to the canary (instead of regular) service.
+canary: false
+
# OAuth client ID set as the audience of the OIDC token. This value must be the
# same as the auth.oauthClientId value in Nomulus config file, which usually is
# the IAP client ID, to allow the request to access IAP protected endpoints.
diff --git a/proxy/src/main/java/google/registry/proxy/handler/EppServiceHandler.java b/proxy/src/main/java/google/registry/proxy/handler/EppServiceHandler.java
index 42946c5a1..4ef72dd19 100644
--- a/proxy/src/main/java/google/registry/proxy/handler/EppServiceHandler.java
+++ b/proxy/src/main/java/google/registry/proxy/handler/EppServiceHandler.java
@@ -60,10 +60,11 @@ public class EppServiceHandler extends HttpsRelayServiceHandler {
public EppServiceHandler(
String relayHost,
String relayPath,
+ boolean canary,
Supplier idTokenSupplier,
byte[] helloBytes,
FrontendMetrics metrics) {
- super(relayHost, relayPath, idTokenSupplier, metrics);
+ super(relayHost, relayPath, canary, idTokenSupplier, metrics);
this.helloBytes = helloBytes.clone();
}
diff --git a/proxy/src/main/java/google/registry/proxy/handler/HttpsRelayServiceHandler.java b/proxy/src/main/java/google/registry/proxy/handler/HttpsRelayServiceHandler.java
index d64af4121..5f5f84a26 100644
--- a/proxy/src/main/java/google/registry/proxy/handler/HttpsRelayServiceHandler.java
+++ b/proxy/src/main/java/google/registry/proxy/handler/HttpsRelayServiceHandler.java
@@ -62,6 +62,7 @@ import javax.net.ssl.SSLHandshakeException;
public abstract class HttpsRelayServiceHandler extends ByteToMessageCodec {
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+ private static final String CANARY_HEADER = "canary";
protected static final ImmutableSet> NON_FATAL_INBOUND_EXCEPTIONS =
ImmutableSet.of(ReadTimeoutException.class, SSLHandshakeException.class);
@@ -72,6 +73,7 @@ public abstract class HttpsRelayServiceHandler extends ByteToMessageCodec cookieStore = new LinkedHashMap<>();
private final String relayHost;
private final String relayPath;
+ private final boolean canary;
private final Supplier idTokenSupplier;
protected final FrontendMetrics metrics;
@@ -79,10 +81,12 @@ public abstract class HttpsRelayServiceHandler extends ByteToMessageCodec idTokenSupplier,
FrontendMetrics metrics) {
this.relayHost = relayHost;
this.relayPath = relayPath;
+ this.canary = canary;
this.idTokenSupplier = idTokenSupplier;
this.metrics = metrics;
}
@@ -104,6 +108,9 @@ public abstract class HttpsRelayServiceHandler extends ByteToMessageCodec idTokenSupplier,
FrontendMetrics metrics) {
- super(relayHost, relayPath, idTokenSupplier, metrics);
+ super(relayHost, relayPath, canary, idTokenSupplier, metrics);
}
@Override
diff --git a/proxy/src/test/java/google/registry/proxy/ProtocolModuleTest.java b/proxy/src/test/java/google/registry/proxy/ProtocolModuleTest.java
index 3a6c45b5f..ddfc6e93b 100644
--- a/proxy/src/test/java/google/registry/proxy/ProtocolModuleTest.java
+++ b/proxy/src/test/java/google/registry/proxy/ProtocolModuleTest.java
@@ -261,6 +261,13 @@ public abstract class ProtocolModuleTest {
return Suppliers.ofInstance("fake.test.id.token");
}
+ @Singleton
+ @Provides
+ @Named("canary")
+ static boolean provideIsCanary() {
+ return false;
+ }
+
@Singleton
@Provides
static LoggingHandler provideLoggingHandler() {
diff --git a/proxy/src/test/java/google/registry/proxy/TestUtils.java b/proxy/src/test/java/google/registry/proxy/TestUtils.java
index b5677c058..9876040d1 100644
--- a/proxy/src/test/java/google/registry/proxy/TestUtils.java
+++ b/proxy/src/test/java/google/registry/proxy/TestUtils.java
@@ -25,7 +25,6 @@ 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.HttpMessage;
import io.netty.handler.codec.http.HttpMethod;
import io.netty.handler.codec.http.HttpRequest;
@@ -53,6 +52,22 @@ public final class TestUtils {
return request;
}
+ public static FullHttpRequest makeHttpPostRequest(
+ String content, String host, String path, boolean canary) {
+ ByteBuf buf = Unpooled.wrappedBuffer(content.getBytes(US_ASCII));
+ FullHttpRequest request =
+ new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.POST, path, buf);
+ request
+ .headers()
+ .set("user-agent", "Proxy")
+ .set("host", host)
+ .setInt("content-length", buf.readableBytes());
+ if (canary) {
+ request.headers().set("canary", "true");
+ }
+ return request;
+ }
+
public static FullHttpRequest makeHttpGetRequest(String host, String path) {
FullHttpRequest request =
new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.GET, path);
@@ -74,16 +89,46 @@ public final class TestUtils {
}
public static FullHttpRequest makeWhoisHttpRequest(
- String content, String host, String path, String idToken) throws IOException {
- FullHttpRequest request = makeHttpPostRequest(content, host, path);
+ String content, String host, String path, boolean canary, String idToken) throws IOException {
+ FullHttpRequest request = makeHttpPostRequest(content, host, path, canary);
request
.headers()
.set("authorization", "Bearer " + idToken)
- .set(HttpHeaderNames.CONTENT_TYPE, "text/plain")
+ .set("content-type", "text/plain")
.set("accept", "text/plain");
return request;
}
+ public static FullHttpRequest makeWhoisHttpRequest(
+ String content, String host, String path, String idToken) throws IOException {
+ return makeWhoisHttpRequest(content, host, path, false, idToken);
+ }
+
+ public static FullHttpRequest makeEppHttpRequest(
+ String content,
+ String host,
+ String path,
+ boolean canary,
+ String idToken,
+ String sslClientCertificateHash,
+ String clientAddress,
+ Cookie... cookies)
+ throws IOException {
+ FullHttpRequest request = makeHttpPostRequest(content, host, path, canary);
+ request
+ .headers()
+ .set("authorization", "Bearer " + idToken)
+ .set("content-type", "application/epp+xml")
+ .set("accept", "application/epp+xml")
+ .set(ProxyHttpHeaders.CERTIFICATE_HASH, sslClientCertificateHash)
+ .set(ProxyHttpHeaders.IP_ADDRESS, clientAddress)
+ .set(ProxyHttpHeaders.FALLBACK_IP_ADDRESS, clientAddress);
+ if (cookies.length != 0) {
+ request.headers().set("cookie", ClientCookieEncoder.STRICT.encode(cookies));
+ }
+ return request;
+ }
+
public static FullHttpRequest makeEppHttpRequest(
String content,
String host,
@@ -93,31 +138,20 @@ public final class TestUtils {
String clientAddress,
Cookie... cookies)
throws IOException {
- FullHttpRequest request = makeHttpPostRequest(content, host, path);
- request
- .headers()
- .set("authorization", "Bearer " + idToken)
- .set(HttpHeaderNames.CONTENT_TYPE, "application/epp+xml")
- .set("accept", "application/epp+xml")
- .set(ProxyHttpHeaders.CERTIFICATE_HASH, sslClientCertificateHash)
- .set(ProxyHttpHeaders.IP_ADDRESS, clientAddress)
- .set(ProxyHttpHeaders.FALLBACK_IP_ADDRESS, clientAddress);
- if (cookies.length != 0) {
- request.headers().set("cookie", ClientCookieEncoder.STRICT.encode(cookies));
- }
- return request;
+ return makeEppHttpRequest(
+ content, host, path, false, idToken, sslClientCertificateHash, clientAddress, cookies);
}
public static FullHttpResponse makeWhoisHttpResponse(String content, HttpResponseStatus status) {
FullHttpResponse response = makeHttpResponse(content, status);
- response.headers().set(HttpHeaderNames.CONTENT_TYPE, "text/plain");
+ response.headers().set("content-type", "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, "application/epp+xml");
+ response.headers().set("content-type", "application/epp+xml");
for (Cookie cookie : cookies) {
response.headers().add("set-cookie", ServerCookieEncoder.STRICT.encode(cookie));
}
diff --git a/proxy/src/test/java/google/registry/proxy/handler/EppServiceHandlerTest.java b/proxy/src/test/java/google/registry/proxy/handler/EppServiceHandlerTest.java
index c83b8b098..650bdc960 100644
--- a/proxy/src/test/java/google/registry/proxy/handler/EppServiceHandlerTest.java
+++ b/proxy/src/test/java/google/registry/proxy/handler/EppServiceHandlerTest.java
@@ -54,11 +54,11 @@ class EppServiceHandlerTest {
private static final String HELLO =
"""
-
-
-
-
- """;
+
+
+
+
+ """;
private static final String RELAY_HOST = "registry.example.tld";
private static final String RELAY_PATH = "/epp";
@@ -71,7 +71,8 @@ class EppServiceHandlerTest {
private final FrontendMetrics metrics = mock(FrontendMetrics.class);
private final EppServiceHandler eppServiceHandler =
- new EppServiceHandler(RELAY_HOST, RELAY_PATH, () -> ID_TOKEN, HELLO.getBytes(UTF_8), metrics);
+ new EppServiceHandler(
+ RELAY_HOST, RELAY_PATH, false, () -> ID_TOKEN, HELLO.getBytes(UTF_8), metrics);
private EmbeddedChannel channel;
@@ -146,7 +147,7 @@ class EppServiceHandlerTest {
// Set up the second channel.
EppServiceHandler eppServiceHandler2 =
new EppServiceHandler(
- RELAY_HOST, RELAY_PATH, () -> ID_TOKEN, HELLO.getBytes(UTF_8), metrics);
+ RELAY_HOST, RELAY_PATH, false, () -> ID_TOKEN, HELLO.getBytes(UTF_8), metrics);
EmbeddedChannel channel2 = setUpNewChannel(eppServiceHandler2);
setHandshakeSuccess(channel2, clientCertificate);
@@ -166,7 +167,7 @@ class EppServiceHandlerTest {
// Set up the second channel.
EppServiceHandler eppServiceHandler2 =
new EppServiceHandler(
- RELAY_HOST, RELAY_PATH, () -> ID_TOKEN, HELLO.getBytes(UTF_8), metrics);
+ RELAY_HOST, RELAY_PATH, false, () -> ID_TOKEN, HELLO.getBytes(UTF_8), metrics);
EmbeddedChannel channel2 = setUpNewChannel(eppServiceHandler2);
X509Certificate clientCertificate2 = SelfSignedCaCertificate.create().cert();
setHandshakeSuccess(channel2, clientCertificate2);
@@ -214,6 +215,33 @@ class EppServiceHandlerTest {
assertThat(channel.isActive()).isTrue();
}
+ @Test
+ void testSuccess_sendRequestToNextHandler_canary() throws Exception {
+ EppServiceHandler eppServiceHandler2 =
+ new EppServiceHandler(
+ RELAY_HOST, RELAY_PATH, true, () -> ID_TOKEN, HELLO.getBytes(UTF_8), metrics);
+ channel = setUpNewChannel(eppServiceHandler2);
+ setHandshakeSuccess();
+ // First inbound message is hello.
+ channel.readInbound();
+ String content = "stuff";
+ channel.writeInbound(Unpooled.wrappedBuffer(content.getBytes(UTF_8)));
+ FullHttpRequest request = channel.readInbound();
+ assertThat(request)
+ .isEqualTo(
+ TestUtils.makeEppHttpRequest(
+ content,
+ RELAY_HOST,
+ RELAY_PATH,
+ true,
+ ID_TOKEN,
+ getCertificateHash(clientCertificate),
+ CLIENT_ADDRESS));
+ // Nothing further to pass to the next handler.
+ assertThat((Object) channel.readInbound()).isNull();
+ assertThat(channel.isActive()).isTrue();
+ }
+
@Test
void testSuccess_sendResponseToNextHandler() throws Exception {
setHandshakeSuccess();
diff --git a/proxy/src/test/java/google/registry/proxy/handler/WhoisServiceHandlerTest.java b/proxy/src/test/java/google/registry/proxy/handler/WhoisServiceHandlerTest.java
index bd8d9200d..991f573da 100644
--- a/proxy/src/test/java/google/registry/proxy/handler/WhoisServiceHandlerTest.java
+++ b/proxy/src/test/java/google/registry/proxy/handler/WhoisServiceHandlerTest.java
@@ -50,7 +50,7 @@ class WhoisServiceHandlerTest {
private final FrontendMetrics metrics = mock(FrontendMetrics.class);
private final WhoisServiceHandler whoisServiceHandler =
- new WhoisServiceHandler(RELAY_HOST, RELAY_PATH, () -> ID_TOKEN, metrics);
+ new WhoisServiceHandler(RELAY_HOST, RELAY_PATH, false, () -> ID_TOKEN, metrics);
private EmbeddedChannel channel;
@BeforeEach
@@ -74,7 +74,7 @@ class WhoisServiceHandlerTest {
// Setup second channel.
WhoisServiceHandler whoisServiceHandler2 =
- new WhoisServiceHandler(RELAY_HOST, RELAY_PATH, () -> ID_TOKEN, metrics);
+ new WhoisServiceHandler(RELAY_HOST, RELAY_PATH, false, () -> ID_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.
@@ -98,6 +98,23 @@ class WhoisServiceHandlerTest {
assertThat(channel.isActive()).isTrue();
}
+ @Test
+ void testSuccess_fireInboundHttpRequest_canary() throws Exception {
+ WhoisServiceHandler whoisServiceHandler2 =
+ new WhoisServiceHandler(RELAY_HOST, RELAY_PATH, true, () -> ID_TOKEN, metrics);
+ channel = new EmbeddedChannel(whoisServiceHandler2);
+ ByteBuf inputBuffer = Unpooled.wrappedBuffer(QUERY_CONTENT.getBytes(US_ASCII));
+ FullHttpRequest expectedRequest =
+ makeWhoisHttpRequest(QUERY_CONTENT, RELAY_HOST, RELAY_PATH, true, ID_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
void testSuccess_parseOutboundHttpResponse() {
String outputString = "line1\r\nline2\r\n";