From a9ba770bfadc78cf0696dad19f17ec29d1a94d6d Mon Sep 17 00:00:00 2001 From: Lai Jiang Date: Tue, 22 Oct 2024 13:12:00 -0400 Subject: [PATCH] Add canary service to GKE (#2594) --- .../google/registry/tools/CurlCommand.java | 6 +- .../registry/tools/ServiceConnection.java | 32 +++++---- .../registry/tools/ServiceConnectionTest.java | 46 +++++++++++- jetty/deploy-nomulus-for-env.sh | 5 ++ .../gateway/nomulus-route-backend.yaml | 51 +++++++++++++ .../gateway/nomulus-route-console.yaml | 33 +++++++++ .../gateway/nomulus-route-frontend.yaml | 27 +++++++ .../gateway/nomulus-route-pubapi.yaml | 45 ++++++++++++ .../registry/proxy/EppProtocolModule.java | 2 + .../google/registry/proxy/ProxyConfig.java | 1 + .../google/registry/proxy/ProxyModule.java | 7 ++ .../registry/proxy/WhoisProtocolModule.java | 2 + .../registry/proxy/config/default-config.yaml | 3 + .../proxy/handler/EppServiceHandler.java | 3 +- .../handler/HttpsRelayServiceHandler.java | 7 ++ .../proxy/handler/WhoisServiceHandler.java | 3 +- .../registry/proxy/ProtocolModuleTest.java | 7 ++ .../java/google/registry/proxy/TestUtils.java | 72 ++++++++++++++----- .../proxy/handler/EppServiceHandlerTest.java | 44 +++++++++--- .../handler/WhoisServiceHandlerTest.java | 21 +++++- 20 files changed, 368 insertions(+), 49 deletions(-) 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 oldServiceClazz = this.service.getClass(); + Class 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";