1
0
mirror of https://github.com/google/nomulus synced 2026-05-17 21:31:51 +00:00

Compare commits

...

2 Commits

Author SHA1 Message Date
gbrodman
b1cd8c5a6f Add a frontend endpoint for retrieving a domain in JSON form (#1916)
We might (likely will) modify some of the fiddly bits around this (maybe
the GSON serialization, where we do the actual authorization, etc) but
this should be a decent basic shell structure for endpoints that the new
registrar console can call to retrieve JSON results.
2023-02-09 15:09:42 -05:00
gbrodman
28c7bc3085 Generate and use an IAP-enabled ID token in the proxy (#1926)
This is only generated and used if "iapClientId" is set in the proxy
config. If so, we use code similar to
https://cloud.google.com/iap/docs/authentication-howto#obtaining_an_oidc_token_for_the_default_service_account
to generate an ID token that is valid for IAP. We set the token on the
Proxy-Authorization header so that we can keep using the pre-existing
access token as well -- IAP allows for us to use either the
Authorization header or the Proxy-Authorization header.
2023-02-09 14:50:35 -05:00
26 changed files with 593 additions and 67 deletions

View File

@@ -94,6 +94,12 @@
<url-pattern>/registry-lock-verify</url-pattern>
</servlet-mapping>
<!-- Registrar console endpoints -->
<servlet-mapping>
<servlet-name>frontend-servlet</servlet-name>
<url-pattern>/console-api/*</url-pattern>
</servlet-mapping>
<!-- Security config -->
<security-constraint>
<web-resource-collection>

View File

@@ -16,6 +16,7 @@ package google.registry.model;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import com.google.gson.annotations.Expose;
import javax.annotation.Nullable;
import javax.persistence.Column;
import javax.persistence.Embeddable;
@@ -28,6 +29,7 @@ import org.joda.time.DateTime;
public class CreateAutoTimestamp extends ImmutableObject implements UnsafeSerializable {
@Column(nullable = false)
@Expose
DateTime creationTime;
@PrePersist

View File

@@ -31,6 +31,7 @@ import com.github.benmanes.caffeine.cache.LoadingCache;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.gson.annotations.Expose;
import google.registry.config.RegistryConfig;
import google.registry.dns.RefreshDnsAction;
import google.registry.model.eppcommon.StatusValue;
@@ -67,7 +68,7 @@ public abstract class EppResource extends UpdateAutoTimestampEntity implements B
*
* @see <a href="https://tools.ietf.org/html/rfc5730">RFC 5730</a>
*/
@Transient String repoId;
@Expose @Transient String repoId;
/**
* The ID of the registrar that is currently sponsoring this resource.
@@ -75,7 +76,7 @@ public abstract class EppResource extends UpdateAutoTimestampEntity implements B
* <p>This can be null in the case of pre-Registry-3.0-migration history objects with null
* resource fields.
*/
String currentSponsorRegistrarId;
@Expose String currentSponsorRegistrarId;
/**
* The ID of the registrar that created this resource.
@@ -83,7 +84,7 @@ public abstract class EppResource extends UpdateAutoTimestampEntity implements B
* <p>This can be null in the case of pre-Registry-3.0-migration history objects with null
* resource fields.
*/
String creationRegistrarId;
@Expose String creationRegistrarId;
/**
* The ID of the registrar that last updated this resource.
@@ -92,7 +93,7 @@ public abstract class EppResource extends UpdateAutoTimestampEntity implements B
* edits; it only includes EPP-visible modifications such as {@literal <update>}. Can be null if
* the resource has never been modified.
*/
String lastEppUpdateRegistrarId;
@Expose String lastEppUpdateRegistrarId;
/**
* The time when this resource was created.
@@ -106,6 +107,7 @@ public abstract class EppResource extends UpdateAutoTimestampEntity implements B
*/
// Need to override the default non-null column attribute.
@AttributeOverride(name = "creationTime", column = @Column)
@Expose
CreateAutoTimestamp creationTime = CreateAutoTimestamp.create(null);
/**
@@ -130,10 +132,10 @@ public abstract class EppResource extends UpdateAutoTimestampEntity implements B
* edits; it only includes EPP-visible modifications such as {@literal <update>}. Can be null if
* the resource has never been modified.
*/
DateTime lastEppUpdateTime;
@Expose DateTime lastEppUpdateTime;
/** Status values associated with this resource. */
Set<StatusValue> statuses;
@Expose Set<StatusValue> statuses;
/**
* When this domain/host's DNS was requested to be refreshed, or null if its DNS is up-to-date.

View File

@@ -40,6 +40,7 @@ import com.google.common.collect.ImmutableSet;
import com.google.common.collect.ImmutableSortedSet;
import com.google.common.collect.Ordering;
import com.google.common.collect.Sets;
import com.google.gson.annotations.Expose;
import google.registry.flows.ResourceFlowUtils;
import google.registry.model.EppResource;
import google.registry.model.EppResource.ResourceWithTransferData;
@@ -121,20 +122,20 @@ public class DomainBase extends EppResource
*
* @invariant domainName == domainName.toLowerCase(Locale.ENGLISH)
*/
String domainName;
@Expose String domainName;
/** The top level domain this is under, de-normalized from {@link #domainName}. */
String tld;
/** References to hosts that are the nameservers for the domain. */
@Transient Set<VKey<Host>> nsHosts;
@Expose @Transient Set<VKey<Host>> nsHosts;
/** Contacts. */
VKey<Contact> adminContact;
@Expose VKey<Contact> adminContact;
VKey<Contact> billingContact;
VKey<Contact> techContact;
VKey<Contact> registrantContact;
@Expose VKey<Contact> billingContact;
@Expose VKey<Contact> techContact;
@Expose VKey<Contact> registrantContact;
/** Authorization info (aka transfer secret) of the domain. */
@Embedded
@@ -175,10 +176,10 @@ public class DomainBase extends EppResource
String idnTableName;
/** Fully qualified host names of this domain's active subordinate hosts. */
Set<String> subordinateHosts;
@Expose Set<String> subordinateHosts;
/** When this domain's registration will expire. */
DateTime registrationExpirationTime;
@Expose DateTime registrationExpirationTime;
/**
* The poll message associated with this domain being deleted.
@@ -230,7 +231,7 @@ public class DomainBase extends EppResource
*
* <p>Can be null if the resource has never been transferred.
*/
DateTime lastTransferTime;
@Expose DateTime lastTransferTime;
/**
* When the domain's autorenewal status will expire.

View File

@@ -25,6 +25,7 @@ import google.registry.monitoring.whitebox.WhiteboxModule;
import google.registry.request.RequestComponentBuilder;
import google.registry.request.RequestModule;
import google.registry.request.RequestScope;
import google.registry.ui.server.console.ConsoleDomainGetAction;
import google.registry.ui.server.registrar.ConsoleOteSetupAction;
import google.registry.ui.server.registrar.ConsoleRegistrarCreatorAction;
import google.registry.ui.server.registrar.ConsoleUiAction;
@@ -61,6 +62,8 @@ interface FrontendRequestComponent {
RegistryLockVerifyAction registryLockVerifyAction();
ConsoleDomainGetAction consoleDomainGetAction();
@Subcomponent.Builder
abstract class Builder implements RequestComponentBuilder<FrontendRequestComponent> {
@Override public abstract Builder requestModule(RequestModule requestModule);

View File

@@ -22,6 +22,7 @@ import com.google.common.base.Joiner;
import com.google.common.base.Splitter;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.gson.annotations.Expose;
import google.registry.model.EppResource;
import google.registry.model.ImmutableObject;
import google.registry.model.contact.Contact;
@@ -52,7 +53,7 @@ public class VKey<T> extends ImmutableObject implements Serializable {
.collect(toImmutableMap(Class::getSimpleName, identity()));
// The primary key for the referenced entity.
Serializable key;
@Expose Serializable key;
Class<? extends T> kind;

View File

@@ -0,0 +1,91 @@
// Copyright 2023 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.ui.server.console;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import com.google.api.client.http.HttpStatusCodes;
import com.google.gson.Gson;
import google.registry.model.EppResourceUtils;
import google.registry.model.console.ConsolePermission;
import google.registry.model.console.User;
import google.registry.model.domain.Domain;
import google.registry.request.Action;
import google.registry.request.Parameter;
import google.registry.request.Response;
import google.registry.request.auth.Auth;
import google.registry.request.auth.AuthResult;
import google.registry.request.auth.UserAuthInfo;
import google.registry.ui.server.registrar.JsonGetAction;
import java.util.Optional;
import javax.inject.Inject;
/** Returns a JSON representation of a domain to the registrar console. */
@Action(
service = Action.Service.DEFAULT,
path = ConsoleDomainGetAction.PATH,
auth = Auth.AUTH_PUBLIC_LOGGED_IN)
public class ConsoleDomainGetAction implements JsonGetAction {
public static final String PATH = "/console-api/domain";
private final AuthResult authResult;
private final Response response;
private final Gson gson;
private final String paramDomain;
@Inject
public ConsoleDomainGetAction(
AuthResult authResult,
Response response,
Gson gson,
@Parameter("domain") String paramDomain) {
this.authResult = authResult;
this.response = response;
this.gson = gson;
this.paramDomain = paramDomain;
}
@Override
public void run() {
if (!authResult.isAuthenticated() || !authResult.userAuthInfo().isPresent()) {
response.setStatus(HttpStatusCodes.STATUS_CODE_UNAUTHORIZED);
return;
}
UserAuthInfo authInfo = authResult.userAuthInfo().get();
if (!authInfo.consoleUser().isPresent()) {
response.setStatus(HttpStatusCodes.STATUS_CODE_UNAUTHORIZED);
return;
}
User user = authInfo.consoleUser().get();
Optional<Domain> possibleDomain =
tm().transact(
() ->
EppResourceUtils.loadByForeignKeyCached(
Domain.class, paramDomain, tm().getTransactionTime()));
if (!possibleDomain.isPresent()) {
response.setStatus(HttpStatusCodes.STATUS_CODE_NOT_FOUND);
return;
}
Domain domain = possibleDomain.get();
if (!user.getUserRoles()
.hasPermission(domain.getCurrentSponsorRegistrarId(), ConsolePermission.DOWNLOAD_DOMAINS)) {
response.setStatus(HttpStatusCodes.STATUS_CODE_NOT_FOUND);
return;
}
response.setStatus(HttpStatusCodes.STATUS_CODE_OK);
response.setPayload(gson.toJson(domain));
}
}

View File

@@ -156,4 +156,10 @@ public final class RegistrarConsoleModule {
static Boolean provideIsLock(HttpServletRequest req) {
return extractBooleanParameter(req, "isLock");
}
@Provides
@Parameter("domain")
static String provideDomain(HttpServletRequest req) {
return extractRequiredParameter(req, "domain");
}
}

View File

@@ -0,0 +1,139 @@
// Copyright 2023 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.ui.server.console;
import static com.google.common.truth.Truth.assertThat;
import static google.registry.testing.DatabaseHelper.createTld;
import static org.mockito.Mockito.mock;
import com.google.api.client.http.HttpStatusCodes;
import com.google.common.collect.ImmutableMap;
import com.google.gson.Gson;
import google.registry.model.console.RegistrarRole;
import google.registry.model.console.User;
import google.registry.model.console.UserRoles;
import google.registry.persistence.transaction.JpaTestExtensions;
import google.registry.request.auth.AuthLevel;
import google.registry.request.auth.AuthResult;
import google.registry.request.auth.UserAuthInfo;
import google.registry.testing.DatabaseHelper;
import google.registry.testing.FakeResponse;
import google.registry.util.UtilsModule;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;
/** Tests for {@link google.registry.ui.server.console.ConsoleDomainGetAction}. */
public class ConsoleDomainGetActionTest {
private static final Gson GSON = UtilsModule.provideGson();
private static final FakeResponse RESPONSE = new FakeResponse();
@RegisterExtension
final JpaTestExtensions.JpaIntegrationTestExtension jpa =
new JpaTestExtensions.Builder().buildIntegrationTestExtension();
@BeforeEach
void beforeEach() {
createTld("tld");
DatabaseHelper.persistActiveDomain("exists.tld");
}
@Test
void testSuccess_fullJsonRepresentation() {
ConsoleDomainGetAction action =
createAction(
AuthResult.create(
AuthLevel.USER,
UserAuthInfo.create(
createUser(
new UserRoles.Builder()
.setRegistrarRoles(
ImmutableMap.of("TheRegistrar", RegistrarRole.ACCOUNT_MANAGER))
.build()))),
"exists.tld");
action.run();
assertThat(RESPONSE.getStatus()).isEqualTo(HttpStatusCodes.STATUS_CODE_OK);
assertThat(RESPONSE.getPayload())
.isEqualTo(
"{\"domainName\":\"exists.tld\",\"adminContact\":{\"key\":\"3-ROID\"},\"techContact\":"
+ "{\"key\":\"3-ROID\"},\"registrantContact\":{\"key\":\"3-ROID\"},\"registrationExpirationTime\":"
+ "\"294247-01-10T04:00:54.775Z\",\"lastTransferTime\":\"null\",\"repoId\":\"2-TLD\","
+ "\"currentSponsorRegistrarId\":\"TheRegistrar\",\"creationRegistrarId\":\"TheRegistrar\","
+ "\"creationTime\":{\"creationTime\":\"1970-01-01T00:00:00.000Z\"},\"lastEppUpdateTime\":\"null\","
+ "\"statuses\":[\"INACTIVE\"]}");
}
@Test
void testFailure_emptyAuth() {
ConsoleDomainGetAction action = createAction(AuthResult.NOT_AUTHENTICATED, "exists.tld");
action.run();
assertThat(RESPONSE.getStatus()).isEqualTo(HttpStatusCodes.STATUS_CODE_UNAUTHORIZED);
}
@Test
void testFailure_appAuth() {
ConsoleDomainGetAction action = createAction(AuthResult.create(AuthLevel.APP), "exists.tld");
action.run();
assertThat(RESPONSE.getStatus()).isEqualTo(HttpStatusCodes.STATUS_CODE_UNAUTHORIZED);
}
@Test
void testFailure_wrongTypeOfUser() {
ConsoleDomainGetAction action =
createAction(
AuthResult.create(
AuthLevel.USER,
UserAuthInfo.create(mock(com.google.appengine.api.users.User.class), false)),
"exists.tld");
action.run();
assertThat(RESPONSE.getStatus()).isEqualTo(HttpStatusCodes.STATUS_CODE_UNAUTHORIZED);
}
@Test
void testFailure_noAccessToRegistrar() {
ConsoleDomainGetAction action =
createAction(
AuthResult.create(
AuthLevel.USER, UserAuthInfo.create(createUser(new UserRoles.Builder().build()))),
"exists.tld");
action.run();
assertThat(RESPONSE.getStatus()).isEqualTo(HttpStatusCodes.STATUS_CODE_NOT_FOUND);
}
@Test
void testFailure_nonexistentDomain() {
ConsoleDomainGetAction action =
createAction(
AuthResult.create(
AuthLevel.USER,
UserAuthInfo.create(createUser(new UserRoles.Builder().setIsAdmin(true).build()))),
"nonexistent.tld");
action.run();
assertThat(RESPONSE.getStatus()).isEqualTo(HttpStatusCodes.STATUS_CODE_NOT_FOUND);
}
private User createUser(UserRoles userRoles) {
return new User.Builder()
.setEmailAddress("email@email.com")
.setGaiaId("gaiaId")
.setUserRoles(userRoles)
.build();
}
private ConsoleDomainGetAction createAction(AuthResult authResult, String domain) {
return new ConsoleDomainGetAction(authResult, RESPONSE, GSON, domain);
}
}

View File

@@ -1,5 +1,6 @@
PATH CLASS METHODS OK AUTH_METHODS MIN USER_POLICY
/_dr/epp EppTlsAction POST n INTERNAL,API APP PUBLIC
/console-api/domain ConsoleDomainGetAction GET n API,LEGACY USER PUBLIC
/registrar ConsoleUiAction GET n INTERNAL,API,LEGACY NONE PUBLIC
/registrar-create ConsoleRegistrarCreatorAction POST,GET n INTERNAL,API,LEGACY NONE PUBLIC
/registrar-ote-setup ConsoleOteSetupAction POST,GET n INTERNAL,API,LEGACY NONE PUBLIC

View File

@@ -16,6 +16,7 @@ package google.registry.proxy;
import static google.registry.util.ResourceUtils.readResourceBytes;
import com.google.auth.oauth2.GoogleCredentials;
import com.google.common.collect.ImmutableList;
import dagger.Module;
import dagger.Provides;
@@ -43,6 +44,7 @@ import io.netty.handler.timeout.ReadTimeoutHandler;
import java.io.IOException;
import java.security.PrivateKey;
import java.security.cert.X509Certificate;
import java.util.Optional;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.ScheduledExecutorService;
import java.util.function.Supplier;
@@ -147,12 +149,18 @@ public final class EppProtocolModule {
@Provides
static EppServiceHandler provideEppServiceHandler(
@Named("accessToken") Supplier<String> accessTokenSupplier,
Supplier<GoogleCredentials> refreshedCredentialsSupplier,
@Named("iapClientId") Optional<String> iapClientId,
@Named("hello") byte[] helloBytes,
FrontendMetrics metrics,
ProxyConfig config) {
return new EppServiceHandler(
config.epp.relayHost, config.epp.relayPath, accessTokenSupplier, helloBytes, metrics);
config.epp.relayHost,
config.epp.relayPath,
refreshedCredentialsSupplier,
iapClientId,
helloBytes,
metrics);
}
@Singleton

View File

@@ -40,6 +40,7 @@ public class ProxyConfig {
private static final String CUSTOM_CONFIG_FORMATTER = "config/proxy-config-%s.yaml";
public String projectId;
public String iapClientId;
public List<String> gcpScopes;
public int serverCertificateCacheSeconds;
public Gcs gcs;

View File

@@ -157,6 +157,13 @@ public class ProxyModule {
return this;
}
@Provides
@Named("iapClientId")
@Singleton
Optional<String> provideIapClientId(ProxyConfig config) {
return Optional.ofNullable(config.iapClientId);
}
@Provides
@WhoisProtocol
int provideWhoisPort(ProxyConfig config) {
@@ -207,7 +214,7 @@ public class ProxyModule {
@Singleton
@Provides
static GoogleCredentialsBundle provideCredential(ProxyConfig config) {
static GoogleCredentialsBundle provideCredentialsBundle(ProxyConfig config) {
try {
GoogleCredentials credentials = GoogleCredentials.getApplicationDefault();
if (credentials.createScopedRequired()) {
@@ -219,19 +226,19 @@ public class ProxyModule {
}
}
/** Access token supplier that auto refreshes 1 minute before expiry. */
/** Provides a set of credentials that auto refreshes 1 minute before expiry. */
@Singleton
@Provides
@Named("accessToken")
static Supplier<String> provideAccessTokenSupplier(GoogleCredentialsBundle credentialsBundle) {
static Supplier<GoogleCredentials> provideRefreshedCredentialsSupplier(
GoogleCredentialsBundle credentialsBundle) {
return () -> {
GoogleCredentials credentials = credentialsBundle.getGoogleCredentials();
try {
credentials.refreshIfExpired();
} catch (IOException e) {
throw new RuntimeException("Cannot refresh access token.", e);
throw new RuntimeException("Cannot refresh credentials.", e);
}
return credentials.getAccessToken().getTokenValue();
return credentials;
};
}

View File

@@ -14,6 +14,7 @@
package google.registry.proxy;
import com.google.auth.oauth2.GoogleCredentials;
import com.google.common.collect.ImmutableList;
import dagger.Module;
import dagger.Provides;
@@ -34,6 +35,7 @@ import google.registry.util.Clock;
import io.netty.channel.ChannelHandler;
import io.netty.handler.codec.LineBasedFrameDecoder;
import io.netty.handler.timeout.ReadTimeoutHandler;
import java.util.Optional;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.ScheduledExecutorService;
import java.util.function.Supplier;
@@ -91,10 +93,15 @@ public class WhoisProtocolModule {
@Provides
static WhoisServiceHandler provideWhoisServiceHandler(
ProxyConfig config,
@Named("accessToken") Supplier<String> accessTokenSupplier,
Supplier<GoogleCredentials> refreshedCredentialsSupplier,
@Named("iapClientId") Optional<String> iapClientId,
FrontendMetrics metrics) {
return new WhoisServiceHandler(
config.whois.relayHost, config.whois.relayPath, accessTokenSupplier, metrics);
config.whois.relayHost,
config.whois.relayPath,
refreshedCredentialsSupplier,
iapClientId,
metrics);
}
@Provides

View File

@@ -8,6 +8,9 @@
# GCP project ID
projectId: your-gcp-project-id
# IAP client ID, if IAP is enabled for this project
iapClientId: null
# OAuth scope that the GoogleCredential will be constructed with. This list
# should include all service scopes that the proxy depends on.
gcpScopes:

View File

@@ -20,6 +20,7 @@ import static google.registry.networking.handler.SslServerInitializer.CLIENT_CER
import static google.registry.proxy.handler.ProxyProtocolHandler.REMOTE_ADDRESS_KEY;
import static google.registry.util.X509Utils.getCertificateHash;
import com.google.auth.oauth2.GoogleCredentials;
import com.google.common.flogger.FluentLogger;
import google.registry.proxy.metric.FrontendMetrics;
import google.registry.util.ProxyHttpHeaders;
@@ -36,6 +37,7 @@ import io.netty.handler.ssl.SslHandshakeCompletionEvent;
import io.netty.util.AttributeKey;
import io.netty.util.concurrent.Promise;
import java.security.cert.X509Certificate;
import java.util.Optional;
import java.util.function.Supplier;
/** Handler that processes EPP protocol logic. */
@@ -60,10 +62,11 @@ public class EppServiceHandler extends HttpsRelayServiceHandler {
public EppServiceHandler(
String relayHost,
String relayPath,
Supplier<String> accessTokenSupplier,
Supplier<GoogleCredentials> refreshedCredentialsSupplier,
Optional<String> iapClientId,
byte[] helloBytes,
FrontendMetrics metrics) {
super(relayHost, relayPath, accessTokenSupplier, metrics);
super(relayHost, relayPath, refreshedCredentialsSupplier, iapClientId, metrics);
this.helloBytes = helloBytes;
}

View File

@@ -16,7 +16,12 @@ package google.registry.proxy.handler;
import static java.nio.charset.StandardCharsets.UTF_8;
import com.google.auth.oauth2.GoogleCredentials;
import com.google.auth.oauth2.IdToken;
import com.google.auth.oauth2.IdTokenProvider;
import com.google.auth.oauth2.IdTokenProvider.Option;
import com.google.common.base.Throwables;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.flogger.FluentLogger;
import google.registry.proxy.metric.FrontendMetrics;
@@ -37,9 +42,11 @@ 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 io.netty.handler.timeout.ReadTimeoutException;
import java.io.IOException;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.function.Supplier;
import javax.net.ssl.SSLHandshakeException;
@@ -72,18 +79,21 @@ public abstract class HttpsRelayServiceHandler extends ByteToMessageCodec<FullHt
private final Map<String, Cookie> cookieStore = new LinkedHashMap<>();
private final String relayHost;
private final String relayPath;
private final Supplier<String> accessTokenSupplier;
private final Supplier<GoogleCredentials> refreshedCredentialsSupplier;
private final Optional<String> iapClientId;
protected final FrontendMetrics metrics;
HttpsRelayServiceHandler(
String relayHost,
String relayPath,
Supplier<String> accessTokenSupplier,
Supplier<GoogleCredentials> refreshedCredentialsSupplier,
Optional<String> iapClientId,
FrontendMetrics metrics) {
this.relayHost = relayHost;
this.relayPath = relayPath;
this.accessTokenSupplier = accessTokenSupplier;
this.refreshedCredentialsSupplier = refreshedCredentialsSupplier;
this.iapClientId = iapClientId;
this.metrics = metrics;
}
@@ -91,19 +101,37 @@ public abstract class HttpsRelayServiceHandler extends ByteToMessageCodec<FullHt
* 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.
* 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);
GoogleCredentials credentials = refreshedCredentialsSupplier.get();
request
.headers()
.set(HttpHeaderNames.USER_AGENT, "Proxy")
.set(HttpHeaderNames.HOST, relayHost)
.set(HttpHeaderNames.AUTHORIZATION, "Bearer " + accessTokenSupplier.get())
.set(
HttpHeaderNames.AUTHORIZATION, "Bearer " + credentials.getAccessToken().getTokenValue())
.setInt(HttpHeaderNames.CONTENT_LENGTH, byteBuf.readableBytes());
// Set the Proxy-Authorization header if using IAP
if (iapClientId.isPresent()) {
IdTokenProvider idTokenProvider = (IdTokenProvider) credentials;
try {
// Note: we use Option.FORMAT_FULL to make sure the JWT we receive contains the email
// address (as is required by IAP)
IdToken idToken =
idTokenProvider.idTokenWithAudience(
iapClientId.get(), ImmutableList.of(Option.FORMAT_FULL));
request
.headers()
.set(HttpHeaderNames.PROXY_AUTHORIZATION, "Bearer " + idToken.getTokenValue());
} catch (IOException e) {
logger.atSevere().withCause(e).log("Error when attempting to retrieve IAP ID token");
}
}
request.content().writeBytes(byteBuf);
return request;
}

View File

@@ -16,6 +16,7 @@ package google.registry.proxy.handler;
import static com.google.common.base.Preconditions.checkArgument;
import com.google.auth.oauth2.GoogleCredentials;
import google.registry.proxy.metric.FrontendMetrics;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelFutureListener;
@@ -25,6 +26,7 @@ import io.netty.handler.codec.http.FullHttpRequest;
import io.netty.handler.codec.http.HttpHeaderNames;
import io.netty.handler.codec.http.HttpHeaderValues;
import io.netty.handler.codec.http.HttpResponse;
import java.util.Optional;
import java.util.function.Supplier;
/** Handler that processes WHOIS protocol logic. */
@@ -33,9 +35,10 @@ public final class WhoisServiceHandler extends HttpsRelayServiceHandler {
public WhoisServiceHandler(
String relayHost,
String relayPath,
Supplier<String> accessTokenSupplier,
Supplier<GoogleCredentials> refreshedCredentialsSupplier,
Optional<String> iapClientId,
FrontendMetrics metrics) {
super(relayHost, relayPath, accessTokenSupplier, metrics);
super(relayHost, relayPath, refreshedCredentialsSupplier, iapClientId, metrics);
}
@Override

View File

@@ -36,6 +36,7 @@ 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.util.concurrent.Promise;
import java.io.IOException;
import java.security.cert.X509Certificate;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
@@ -96,14 +97,15 @@ class EppProtocolModuleTest extends ProtocolModuleTest {
return buffer;
}
private FullHttpRequest makeEppHttpRequest(byte[] content, Cookie... cookies) {
private FullHttpRequest makeEppHttpRequest(byte[] content, Cookie... cookies) throws IOException {
return TestUtils.makeEppHttpRequest(
new String(content, UTF_8),
PROXY_CONFIG.epp.relayHost,
PROXY_CONFIG.epp.relayPath,
TestModule.provideFakeAccessToken().get(),
TestModule.provideFakeCredentials().get(),
getCertificateHash(certificate),
CLIENT_ADDRESS,
TestModule.provideIapClientId(),
cookies);
}

View File

@@ -17,7 +17,14 @@ package google.registry.proxy;
import static com.google.common.collect.ImmutableList.toImmutableList;
import static google.registry.proxy.ProxyConfig.Environment.LOCAL;
import static google.registry.proxy.ProxyConfig.getProxyConfig;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import com.google.auth.oauth2.AccessToken;
import com.google.auth.oauth2.ComputeEngineCredentials;
import com.google.auth.oauth2.GoogleCredentials;
import com.google.auth.oauth2.IdToken;
import com.google.auth.oauth2.IdTokenProvider.Option;
import com.google.common.base.Suppliers;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
@@ -52,7 +59,9 @@ import io.netty.channel.embedded.EmbeddedChannel;
import io.netty.handler.logging.LoggingHandler;
import io.netty.handler.ssl.SslProvider;
import io.netty.handler.timeout.ReadTimeoutHandler;
import java.io.IOException;
import java.time.Duration;
import java.util.Optional;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
@@ -223,7 +232,7 @@ public abstract class ProtocolModuleTest {
* should be provided in the respective {@code ProtocolModule} instead.
*/
@Module
static class TestModule {
public static class TestModule {
/**
* A fake clock that is explicitly provided. Users can construct a module with a controller
@@ -235,6 +244,12 @@ public abstract class ProtocolModuleTest {
this.fakeClock = fakeClock;
}
@Provides
@Named("iapClientId")
public static Optional<String> provideIapClientId() {
return Optional.of("iapClientId");
}
@Singleton
@Provides
static ProxyConfig provideProxyConfig() {
@@ -249,9 +264,19 @@ public abstract class ProtocolModuleTest {
@Singleton
@Provides
@Named("accessToken")
static Supplier<String> provideFakeAccessToken() {
return Suppliers.ofInstance("fake.test.token");
static Supplier<GoogleCredentials> provideFakeCredentials() {
ComputeEngineCredentials mockCredentials = mock(ComputeEngineCredentials.class);
when(mockCredentials.getAccessToken()).thenReturn(new AccessToken("fake.test.token", null));
IdToken mockIdToken = mock(IdToken.class);
when(mockIdToken.getTokenValue()).thenReturn("fake.test.id.token");
try {
when(mockCredentials.idTokenWithAudience(
"iapClientId", ImmutableList.of(Option.FORMAT_FULL)))
.thenReturn(mockIdToken);
} catch (IOException e) {
throw new RuntimeException(e);
}
return Suppliers.ofInstance(mockCredentials);
}
@Singleton

View File

@@ -17,6 +17,10 @@ package google.registry.proxy;
import static com.google.common.truth.Truth.assertThat;
import static java.nio.charset.StandardCharsets.US_ASCII;
import com.google.auth.oauth2.GoogleCredentials;
import com.google.auth.oauth2.IdTokenProvider;
import com.google.auth.oauth2.IdTokenProvider.Option;
import com.google.common.collect.ImmutableList;
import google.registry.util.ProxyHttpHeaders;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
@@ -34,6 +38,8 @@ 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;
import java.io.IOException;
import java.util.Optional;
/** Utility class for various helper methods used in testing. */
public class TestUtils {
@@ -71,13 +77,19 @@ public class TestUtils {
}
public static FullHttpRequest makeWhoisHttpRequest(
String content, String host, String path, String accessToken) {
String content,
String host,
String path,
GoogleCredentials credentials,
Optional<String> iapClientId)
throws IOException {
FullHttpRequest request = makeHttpPostRequest(content, host, path);
request
.headers()
.set("authorization", "Bearer " + accessToken)
.set("authorization", "Bearer " + credentials.getAccessToken().getTokenValue())
.set(HttpHeaderNames.CONTENT_TYPE, "text/plain")
.set("accept", "text/plain");
maybeSetProxyAuthForIap(request, credentials, iapClientId);
return request;
}
@@ -85,18 +97,21 @@ public class TestUtils {
String content,
String host,
String path,
String accessToken,
GoogleCredentials credentials,
String sslClientCertificateHash,
String clientAddress,
Cookie... cookies) {
Optional<String> iapClientId,
Cookie... cookies)
throws IOException {
FullHttpRequest request = makeHttpPostRequest(content, host, path);
request
.headers()
.set("authorization", "Bearer " + accessToken)
.set("authorization", "Bearer " + credentials.getAccessToken().getTokenValue())
.set(HttpHeaderNames.CONTENT_TYPE, "application/epp+xml")
.set("accept", "application/epp+xml")
.set(ProxyHttpHeaders.CERTIFICATE_HASH, sslClientCertificateHash)
.set(ProxyHttpHeaders.IP_ADDRESS, clientAddress);
maybeSetProxyAuthForIap(request, credentials, iapClientId);
if (cookies.length != 0) {
request.headers().set("cookie", ClientCookieEncoder.STRICT.encode(cookies));
}
@@ -146,4 +161,16 @@ public class TestUtils {
public static void assertHttpRequestEquivalent(HttpRequest req1, HttpRequest req2) {
assertHttpMessageEquivalent(req1, req2);
}
private static void maybeSetProxyAuthForIap(
FullHttpRequest request, GoogleCredentials credentials, Optional<String> iapClientId)
throws IOException {
if (iapClientId.isPresent()) {
String idTokenValue =
((IdTokenProvider) credentials)
.idTokenWithAudience(iapClientId.get(), ImmutableList.of(Option.FORMAT_FULL))
.getTokenValue();
request.headers().set("proxy-authorization", "Bearer " + idTokenValue);
}
}
}

View File

@@ -41,7 +41,7 @@ class WhoisProtocolModuleTest extends ProtocolModuleTest {
}
@Test
void testSuccess_singleFrameInboundMessage() {
void testSuccess_singleFrameInboundMessage() throws Exception {
String inputString = "test.tld\r\n";
// Inbound message processed and passed along.
assertThat(channel.writeInbound(Unpooled.wrappedBuffer(inputString.getBytes(US_ASCII))))
@@ -53,7 +53,8 @@ class WhoisProtocolModuleTest extends ProtocolModuleTest {
"test.tld",
PROXY_CONFIG.whois.relayHost,
PROXY_CONFIG.whois.relayPath,
TestModule.provideFakeAccessToken().get());
TestModule.provideFakeCredentials().get(),
TestModule.provideIapClientId());
assertThat(actualRequest).isEqualTo(expectedRequest);
assertThat(channel.isActive()).isTrue();
// Nothing more to read.
@@ -70,7 +71,7 @@ class WhoisProtocolModuleTest extends ProtocolModuleTest {
}
@Test
void testSuccess_multiFrameInboundMessage() {
void testSuccess_multiFrameInboundMessage() throws Exception {
String frame1 = "test";
String frame2 = "1.tld";
String frame3 = "\r\nte";
@@ -88,7 +89,8 @@ class WhoisProtocolModuleTest extends ProtocolModuleTest {
"test1.tld",
PROXY_CONFIG.whois.relayHost,
PROXY_CONFIG.whois.relayPath,
TestModule.provideFakeAccessToken().get());
TestModule.provideFakeCredentials().get(),
TestModule.provideIapClientId());
assertThat(actualRequest1).isEqualTo(expectedRequest1);
// No more message at this point.
assertThat((Object) channel.readInbound()).isNull();
@@ -102,7 +104,8 @@ class WhoisProtocolModuleTest extends ProtocolModuleTest {
"test2.tld",
PROXY_CONFIG.whois.relayHost,
PROXY_CONFIG.whois.relayPath,
TestModule.provideFakeAccessToken().get());
TestModule.provideFakeCredentials().get(),
TestModule.provideIapClientId());
assertThat(actualRequest2).isEqualTo(expectedRequest2);
// The third message is not complete yet.
assertThat(channel.isActive()).isTrue();

View File

@@ -25,8 +25,14 @@ import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.when;
import com.google.auth.oauth2.AccessToken;
import com.google.auth.oauth2.ComputeEngineCredentials;
import com.google.auth.oauth2.IdToken;
import com.google.auth.oauth2.IdTokenProvider.Option;
import com.google.common.base.Throwables;
import com.google.common.collect.ImmutableList;
import google.registry.proxy.TestUtils;
import google.registry.proxy.handler.HttpsRelayServiceHandler.NonOkHttpResponseException;
import google.registry.proxy.metric.FrontendMetrics;
@@ -44,7 +50,9 @@ 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.util.concurrent.Promise;
import java.io.IOException;
import java.security.cert.X509Certificate;
import java.util.Optional;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
@@ -59,9 +67,12 @@ class EppServiceHandlerTest {
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 CLIENT_ADDRESS = "epp.client.tld";
private static final String PROTOCOL = "epp";
private static final String IAP_CLIENT_ID = "iapClientId";
private static final ComputeEngineCredentials mockCredentials =
mock(ComputeEngineCredentials.class);
private X509Certificate clientCertificate;
@@ -69,7 +80,12 @@ class EppServiceHandlerTest {
private final EppServiceHandler eppServiceHandler =
new EppServiceHandler(
RELAY_HOST, RELAY_PATH, () -> ACCESS_TOKEN, HELLO.getBytes(UTF_8), metrics);
RELAY_HOST,
RELAY_PATH,
() -> mockCredentials,
Optional.of(IAP_CLIENT_ID),
HELLO.getBytes(UTF_8),
metrics);
private EmbeddedChannel channel;
@@ -79,7 +95,7 @@ class EppServiceHandlerTest {
channel.attr(CLIENT_CERTIFICATE_PROMISE_KEY).get().setSuccess(certificate);
}
private void setHandshakeSuccess() throws Exception {
private void setHandshakeSuccess() {
setHandshakeSuccess(channel, clientCertificate);
}
@@ -91,23 +107,29 @@ class EppServiceHandlerTest {
.setFailure(new Exception("Handshake Failure"));
}
private void setHandshakeFailure() throws Exception {
private void setHandshakeFailure() {
setHandshakeFailure(channel);
}
private FullHttpRequest makeEppHttpRequest(String content, Cookie... cookies) {
private FullHttpRequest makeEppHttpRequest(String content, Cookie... cookies) throws IOException {
return TestUtils.makeEppHttpRequest(
content,
RELAY_HOST,
RELAY_PATH,
ACCESS_TOKEN,
mockCredentials,
getCertificateHash(clientCertificate),
CLIENT_ADDRESS,
Optional.of(IAP_CLIENT_ID),
cookies);
}
@BeforeEach
void beforeEach() throws Exception {
when(mockCredentials.getAccessToken()).thenReturn(new AccessToken("this.access.token", null));
IdToken mockIdToken = mock(IdToken.class);
when(mockIdToken.getTokenValue()).thenReturn("fake.test.id.token");
when(mockCredentials.idTokenWithAudience(IAP_CLIENT_ID, ImmutableList.of(Option.FORMAT_FULL)))
.thenReturn(mockIdToken);
clientCertificate = SelfSignedCaCertificate.create().cert();
channel = setUpNewChannel(eppServiceHandler);
}
@@ -140,10 +162,15 @@ class EppServiceHandlerTest {
String certHash = getCertificateHash(clientCertificate);
assertThat(channel.isActive()).isTrue();
// Setup the second channel.
// Set up the second channel.
EppServiceHandler eppServiceHandler2 =
new EppServiceHandler(
RELAY_HOST, RELAY_PATH, () -> ACCESS_TOKEN, HELLO.getBytes(UTF_8), metrics);
RELAY_HOST,
RELAY_PATH,
() -> mockCredentials,
Optional.empty(),
HELLO.getBytes(UTF_8),
metrics);
EmbeddedChannel channel2 = setUpNewChannel(eppServiceHandler2);
setHandshakeSuccess(channel2, clientCertificate);
@@ -160,10 +187,15 @@ class EppServiceHandlerTest {
String certHash = getCertificateHash(clientCertificate);
assertThat(channel.isActive()).isTrue();
// Setup the second channel.
// Set up the second channel.
EppServiceHandler eppServiceHandler2 =
new EppServiceHandler(
RELAY_HOST, RELAY_PATH, () -> ACCESS_TOKEN, HELLO.getBytes(UTF_8), metrics);
RELAY_HOST,
RELAY_PATH,
() -> mockCredentials,
Optional.empty(),
HELLO.getBytes(UTF_8),
metrics);
EmbeddedChannel channel2 = setUpNewChannel(eppServiceHandler2);
X509Certificate clientCertificate2 = SelfSignedCaCertificate.create().cert();
setHandshakeSuccess(channel2, clientCertificate2);
@@ -326,4 +358,38 @@ class EppServiceHandlerTest {
assertThat((Object) channel.readOutbound()).isNull();
assertThat(channel.isActive()).isTrue();
}
@Test
void testSuccess_withoutIapClientId() throws Exception {
// Without an IAP client ID configured, we shouldn't include the proxy-authorization header
EppServiceHandler nonIapServiceHandler =
new EppServiceHandler(
RELAY_HOST,
RELAY_PATH,
() -> mockCredentials,
Optional.empty(),
HELLO.getBytes(UTF_8),
metrics);
channel = setUpNewChannel(nonIapServiceHandler);
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(
TestUtils.makeEppHttpRequest(
content,
RELAY_HOST,
RELAY_PATH,
mockCredentials,
getCertificateHash(clientCertificate),
CLIENT_ADDRESS,
Optional.empty()));
// Nothing further to pass to the next handler.
assertThat((Object) channel.readInbound()).isNull();
assertThat(channel.isActive()).isTrue();
}
}

View File

@@ -22,8 +22,14 @@ import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.when;
import com.google.auth.oauth2.AccessToken;
import com.google.auth.oauth2.ComputeEngineCredentials;
import com.google.auth.oauth2.IdToken;
import com.google.auth.oauth2.IdTokenProvider.Option;
import com.google.common.base.Throwables;
import com.google.common.collect.ImmutableList;
import google.registry.proxy.handler.HttpsRelayServiceHandler.NonOkHttpResponseException;
import google.registry.proxy.metric.FrontendMetrics;
import io.netty.buffer.ByteBuf;
@@ -34,6 +40,7 @@ import io.netty.handler.codec.EncoderException;
import io.netty.handler.codec.http.FullHttpRequest;
import io.netty.handler.codec.http.FullHttpResponse;
import io.netty.handler.codec.http.HttpResponseStatus;
import java.util.Optional;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
@@ -43,18 +50,26 @@ 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 static final String IAP_CLIENT_ID = "iapClientId";
private static final ComputeEngineCredentials mockCredentials =
mock(ComputeEngineCredentials.class);
private final FrontendMetrics metrics = mock(FrontendMetrics.class);
private final WhoisServiceHandler whoisServiceHandler =
new WhoisServiceHandler(RELAY_HOST, RELAY_PATH, () -> ACCESS_TOKEN, metrics);
new WhoisServiceHandler(
RELAY_HOST, RELAY_PATH, () -> mockCredentials, Optional.of(IAP_CLIENT_ID), metrics);
private EmbeddedChannel channel;
@BeforeEach
void beforeEach() {
void beforeEach() throws Exception {
when(mockCredentials.getAccessToken()).thenReturn(new AccessToken("this.access.token", null));
IdToken mockIdToken = mock(IdToken.class);
when(mockIdToken.getTokenValue()).thenReturn("fake.test.id.token");
when(mockCredentials.idTokenWithAudience(IAP_CLIENT_ID, ImmutableList.of(Option.FORMAT_FULL)))
.thenReturn(mockIdToken);
// 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);
@@ -74,7 +89,8 @@ class WhoisServiceHandlerTest {
// Setup second channel.
WhoisServiceHandler whoisServiceHandler2 =
new WhoisServiceHandler(RELAY_HOST, RELAY_PATH, () -> ACCESS_TOKEN, metrics);
new WhoisServiceHandler(
RELAY_HOST, RELAY_PATH, () -> mockCredentials, Optional.empty(), 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.
@@ -85,10 +101,11 @@ class WhoisServiceHandlerTest {
}
@Test
void testSuccess_fireInboundHttpRequest() {
void testSuccess_fireInboundHttpRequest() throws Exception {
ByteBuf inputBuffer = Unpooled.wrappedBuffer(QUERY_CONTENT.getBytes(US_ASCII));
FullHttpRequest expectedRequest =
makeWhoisHttpRequest(QUERY_CONTENT, RELAY_HOST, RELAY_PATH, ACCESS_TOKEN);
makeWhoisHttpRequest(
QUERY_CONTENT, RELAY_HOST, RELAY_PATH, mockCredentials, Optional.of(IAP_CLIENT_ID));
// Input data passed to next handler
assertThat(channel.writeInbound(inputBuffer)).isTrue();
FullHttpRequest inputRequest = channel.readInbound();
@@ -111,6 +128,27 @@ class WhoisServiceHandlerTest {
assertThat(channel.isActive()).isFalse();
}
@Test
void testSuccess_withoutIapClientId() throws Exception {
// Without an IAP client ID configured, we shouldn't include the proxy-authorization header
WhoisServiceHandler nonIapHandler =
new WhoisServiceHandler(
RELAY_HOST, RELAY_PATH, () -> mockCredentials, Optional.empty(), metrics);
channel = new EmbeddedChannel(nonIapHandler);
ByteBuf inputBuffer = Unpooled.wrappedBuffer(QUERY_CONTENT.getBytes(US_ASCII));
FullHttpRequest expectedRequest =
makeWhoisHttpRequest(
QUERY_CONTENT, RELAY_HOST, RELAY_PATH, mockCredentials, Optional.empty());
// 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 testFailure_OutboundHttpResponseNotOK() {
String outputString = "line1\r\nline2\r\n";

View File

@@ -0,0 +1,41 @@
// Copyright 2023 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.util;
import com.google.gson.TypeAdapter;
import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonWriter;
import java.io.IOException;
import java.util.Objects;
import org.joda.time.DateTime;
import org.joda.time.format.ISODateTimeFormat;
/** GSON type adapter for Joda {@link DateTime} objects. */
public class DateTimeTypeAdapter extends TypeAdapter<DateTime> {
@Override
public void write(JsonWriter out, DateTime value) throws IOException {
out.value(Objects.toString(value));
}
@Override
public DateTime read(JsonReader in) throws IOException {
String stringValue = in.nextString();
if (stringValue.equals("null")) {
return null;
}
return ISODateTimeFormat.dateTime().withZoneUTC().parseDateTime(stringValue);
}
}

View File

@@ -16,6 +16,8 @@ package google.registry.util;
import com.google.appengine.api.modules.ModulesService;
import com.google.appengine.api.modules.ModulesServiceFactory;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import dagger.Binds;
import dagger.Module;
import dagger.Provides;
@@ -25,6 +27,7 @@ import java.security.SecureRandom;
import java.util.Random;
import javax.inject.Named;
import javax.inject.Singleton;
import org.joda.time.DateTime;
/** Dagger module to provide instances of various utils classes. */
@Module
@@ -83,4 +86,13 @@ public abstract class UtilsModule {
public static StringGenerator provideDigitsOnlyStringGenerator(SecureRandom secureRandom) {
return new RandomStringGenerator(StringGenerator.Alphabets.DIGITS_ONLY, secureRandom);
}
@Singleton
@Provides
public static Gson provideGson() {
return new GsonBuilder()
.registerTypeAdapter(DateTime.class, new DateTimeTypeAdapter())
.excludeFieldsWithoutExposeAnnotation()
.create();
}
}