Compare commits

..
Author SHA1 Message Date
Ben McIlwainandGitHub a26905d39a Note that immediate deletion is for exceptional circumstances only (#962)
* Note that immediate deletion is for exceptional circumstances only
2021-02-17 13:10:12 -05:00
Ben McIlwainandGitHub 4ce790d29e Fix obscure bug when checking restore prices of duplicate domain names (#968)
* Fix obscure bug when checking restore prices of duplicate domain names

There were instances of "java.lang.IllegalArgumentException: Multiple entries
with same key" in the logs, caused by attempting to construct an ImmutableMap
containing duplicate keys. It turns out this was happening in the domain check
flow when the following conditions were all simultaneously met:

1. The older v06 fee extension is used
2. The same domain name is being queried multiple times in a single check
   command (which is valid per the spec but doesn't actually make any sense)
3. Said domain exists
4. The cost of a restore (an uncommon operation) is being checked

When all of those conditions were met, an error was being thrown when the
dupe-containing list of domain names was used as the keys of a new Map. This
fixes that bug by calling .distinct() first.

Give enough registrars enough typewriters ...

BUG=179052195
2021-02-17 12:09:19 -05:00
sarahcaseybotandGitHub bcc1924b24 Refactor SignedMarkRevocationListDao for easy primary database cutover (#943)
* Refactor SignedMarkRevocationListDao for easy primary database cutover

* Fix javadoc comments

* Use PrimaryDatabase enum

* format fix

* fix up tests

* Fix punctuation

* Remove unnecessary else ifs

* Fix error messages

* spell out class name
2021-02-14 09:58:08 -05:00
Weimin YuandGitHub f86936788e Revert BEAM pipeline back to SQL credential file (#961)
* Revert BEAM pipeline back to SQL credential file

Stop using the SecretManager for SQL credential in BEAM for now. The
SecretManager cannot be injected into the code on pipeline workers
because RegistryEnvironment is not set.

See b/179839014 for details.
2021-02-11 14:06:13 -05:00
sarahcaseybotandGitHub 13f61dd7b9 Add string constants for HTTP header names (#956)
* Add string constants for HTTP header names

* revert package-lock changes

* Clarify names

* add CONTENT_TYPE

* Fix formatting

* Move X-FORWARDED-FOR to ProxyHttpHeaders
2021-02-11 12:02:51 -05:00
Michael MullerandGitHub 17cd9ba4f1 Add db-compare tests to three more flows (#963)
* Add db-compare tests to three more flows

Add database comparison to the replay tests for DomainDeleteFlowTest,
DomainRenewFlowTest and DomainUpdateFlowTest.
2021-02-11 11:35:13 -05:00
29 changed files with 986 additions and 238 deletions
@@ -25,6 +25,7 @@ import com.google.common.flogger.FluentLogger;
import com.google.common.net.MediaType;
import google.registry.model.eppoutput.EppOutput;
import google.registry.request.Response;
import google.registry.util.ProxyHttpHeaders;
import javax.inject.Inject;
/** Handle an EPP request and response. */
@@ -72,7 +73,7 @@ public class EppRequestHandler {
// See: https://tools.ietf.org/html/rfc5734#section-2
if (eppOutput.isResponse()
&& eppOutput.getResponse().getResult().getCode() == SUCCESS_AND_CLOSE) {
response.setHeader("Epp-Session", "close");
response.setHeader(ProxyHttpHeaders.EPP_SESSION, "close");
}
// If a login request returns a success, a logged-in header is added to the response to inform
// the proxy that it is no longer necessary to send the full client certificate to the backend
@@ -80,7 +81,7 @@ public class EppRequestHandler {
if (eppOutput.isResponse()
&& eppOutput.getResponse().isLoginResponse()
&& eppOutput.isSuccess()) {
response.setHeader("Logged-In", "true");
response.setHeader(ProxyHttpHeaders.LOGGED_IN, "true");
}
} catch (Exception e) {
logger.atWarning().withCause(e).log("handleEppCommand general exception");
@@ -34,6 +34,7 @@ import google.registry.model.registrar.Registrar;
import google.registry.request.Header;
import google.registry.util.CidrAddressBlock;
import google.registry.util.Clock;
import google.registry.util.ProxyHttpHeaders;
import java.io.ByteArrayInputStream;
import java.net.InetAddress;
import java.security.cert.CertificateException;
@@ -78,9 +79,9 @@ public class TlsCredentials implements TransportCredentials {
@Inject
public TlsCredentials(
@Config("requireSslCertificates") boolean requireSslCertificates,
@Header("X-SSL-Certificate") Optional<String> clientCertificateHash,
@Header("X-SSL-Full-Certificate") Optional<String> clientCertificate,
@Header("X-Forwarded-For") Optional<String> clientAddress,
@Header(ProxyHttpHeaders.CERTIFICATE_HASH) Optional<String> clientCertificateHash,
@Header(ProxyHttpHeaders.FULL_CERTIFICATE) Optional<String> clientCertificate,
@Header(ProxyHttpHeaders.IP_ADDRESS) Optional<String> clientAddress,
CertificateChecker certificateChecker,
Clock clock) {
this.requireSslCertificates = requireSslCertificates;
@@ -328,25 +329,25 @@ public class TlsCredentials implements TransportCredentials {
public static final class EppTlsModule {
@Provides
@Header("X-SSL-Certificate")
@Header(ProxyHttpHeaders.CERTIFICATE_HASH)
static Optional<String> provideClientCertificateHash(HttpServletRequest req) {
// Note: This header is actually required, we just want to handle its absence explicitly
// by throwing an EPP exception rather than a generic Bad Request exception.
return extractOptionalHeader(req, "X-SSL-Certificate");
return extractOptionalHeader(req, ProxyHttpHeaders.CERTIFICATE_HASH);
}
@Provides
@Header("X-SSL-Full-Certificate")
@Header(ProxyHttpHeaders.FULL_CERTIFICATE)
static Optional<String> provideClientCertificate(HttpServletRequest req) {
// Note: This header is actually required, we just want to handle its absence explicitly
// by throwing an EPP exception rather than a generic Bad Request exception.
return extractOptionalHeader(req, "X-SSL-Full-Certificate");
return extractOptionalHeader(req, ProxyHttpHeaders.FULL_CERTIFICATE);
}
@Provides
@Header("X-Forwarded-For")
static Optional<String> provideForwardedFor(HttpServletRequest req) {
return extractOptionalHeader(req, "X-Forwarded-For");
@Header(ProxyHttpHeaders.IP_ADDRESS)
static Optional<String> provideIpAddress(HttpServletRequest req) {
return extractOptionalHeader(req, ProxyHttpHeaders.IP_ADDRESS);
}
}
}
@@ -301,6 +301,7 @@ public final class DomainCheckFlow implements Flow {
feeCheck.getItems().stream()
.filter(fc -> fc.getCommandName() == CommandName.RESTORE)
.map(FeeCheckCommandExtensionItem::getDomainName)
.distinct()
.collect(toImmutableList());
} else if (feeCheck.getItems().stream()
.anyMatch(fc -> fc.getCommandName() == CommandName.RESTORE)) {
@@ -16,6 +16,9 @@ package google.registry.model;
import com.google.common.flogger.FluentLogger;
import google.registry.config.RegistryEnvironment;
import google.registry.model.common.DatabaseTransitionSchedule;
import google.registry.model.common.DatabaseTransitionSchedule.PrimaryDatabase;
import google.registry.model.common.DatabaseTransitionSchedule.TransitionId;
/** Utility methods related to migrating dual-read/dual-write entities. */
public class DatabaseMigrationUtils {
@@ -34,5 +37,12 @@ public class DatabaseMigrationUtils {
}
}
/** Gets the value for the database currently considered primary. */
public static PrimaryDatabase getPrimaryDatabase(TransitionId transitionId) {
return DatabaseTransitionSchedule.getCached(transitionId)
.map(DatabaseTransitionSchedule::getPrimaryDatabase)
.orElse(PrimaryDatabase.DATASTORE);
}
private DatabaseMigrationUtils() {}
}
@@ -96,15 +96,15 @@ public class DatabaseTransitionSchedule extends ImmutableObject implements Datas
TimedTransitionProperty.forMapify(PrimaryDatabase.DATASTORE, PrimaryDatabaseTransition.class);
/** A cache that loads the {@link DatabaseTransitionSchedule} for a given id. */
private static final LoadingCache<String, Optional<DatabaseTransitionSchedule>> CACHE =
private static final LoadingCache<TransitionId, Optional<DatabaseTransitionSchedule>> CACHE =
CacheBuilder.newBuilder()
.expireAfterWrite(
java.time.Duration.ofMillis(getSingletonCacheRefreshDuration().getMillis()))
.build(
new CacheLoader<String, Optional<DatabaseTransitionSchedule>>() {
new CacheLoader<TransitionId, Optional<DatabaseTransitionSchedule>>() {
@Override
public Optional<DatabaseTransitionSchedule> load(String transitionId) {
return DatabaseTransitionSchedule.get(TransitionId.valueOf(transitionId));
public Optional<DatabaseTransitionSchedule> load(TransitionId transitionId) {
return DatabaseTransitionSchedule.get(transitionId);
}
});
@@ -136,7 +136,7 @@ public class DatabaseTransitionSchedule extends ImmutableObject implements Datas
* <p>WARNING: The schedule returned by this method could be up to 10 minutes out of date.
*/
public static Optional<DatabaseTransitionSchedule> getCached(TransitionId id) {
return CACHE.getUnchecked(id.name());
return CACHE.getUnchecked(id);
}
/** Returns the schedule for a given id. */
@@ -337,6 +337,10 @@ public class DomainContent extends EppResource
nullToEmptyImmutableCopy(dsData).stream()
.map(dsData -> dsData.cloneWithDomainRepoId(getRepoId()))
.collect(toImmutableSet());
if (transferData != null) {
transferData.convertVKeys();
}
}
@PostLoad
@@ -295,7 +295,7 @@ public abstract class PollMessage extends ImmutableObject
@Transient @ImmutableObject.DoNotCompare
List<DomainPendingActionNotificationResponse> domainPendingActionNotificationResponses;
@Transient List<DomainTransferResponse> domainTransferResponses;
@Transient @ImmutableObject.DoNotCompare List<DomainTransferResponse> domainTransferResponses;
@Transient List<HostPendingActionNotificationResponse> hostPendingActionNotificationResponses;
@@ -15,24 +15,13 @@
package google.registry.model.smd;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Preconditions.checkState;
import static com.google.common.collect.ImmutableList.toImmutableList;
import static com.google.common.collect.Iterables.isEmpty;
import static google.registry.model.CacheUtils.memoizeWithShortExpiration;
import static google.registry.model.DatabaseMigrationUtils.suppressExceptionUnlessInTest;
import static google.registry.model.common.EntityGroupRoot.getCrossTldKey;
import static google.registry.model.ofy.ObjectifyService.allocateId;
import static google.registry.model.ofy.ObjectifyService.ofy;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import static google.registry.util.DateTimeUtils.START_OF_TIME;
import static google.registry.util.DateTimeUtils.isBeforeOrAt;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Supplier;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Iterables;
import com.google.common.collect.MapDifference;
import com.google.common.collect.Maps;
import com.googlecode.objectify.Key;
import com.googlecode.objectify.annotation.EmbedMap;
import com.googlecode.objectify.annotation.Entity;
@@ -45,9 +34,7 @@ import google.registry.model.annotations.NotBackedUp;
import google.registry.model.annotations.NotBackedUp.Reason;
import google.registry.model.common.EntityGroupRoot;
import google.registry.schema.replay.NonReplicatedEntity;
import google.registry.util.CollectionUtils;
import java.util.Map;
import java.util.Optional;
import javax.persistence.CollectionTable;
import javax.persistence.Column;
import javax.persistence.ElementCollection;
@@ -116,16 +103,7 @@ public class SignedMarkRevocationList extends ImmutableObject implements NonRepl
* single {@link SignedMarkRevocationList} object.
*/
private static final Supplier<SignedMarkRevocationList> CACHE =
memoizeWithShortExpiration(
() -> {
SignedMarkRevocationList datastoreList = loadFromDatastore();
suppressExceptionUnlessInTest(
() -> {
loadAndCompareCloudSqlList(datastoreList);
},
"Error comparing signed mark revocation lists.");
return datastoreList;
});
memoizeWithShortExpiration(SignedMarkRevocationListDao::load);
/** Return a single logical instance that combines all Datastore shards. */
public static SignedMarkRevocationList get() {
@@ -159,98 +137,10 @@ public class SignedMarkRevocationList extends ImmutableObject implements NonRepl
/** Save this list to Datastore in sharded form and to Cloud SQL. Returns {@code this}. */
public SignedMarkRevocationList save() {
saveToDatastore();
SignedMarkRevocationListDao.trySave(this);
SignedMarkRevocationListDao.save(this);
return this;
}
/** Loads the shards from Datastore and combines them into one list. */
private static SignedMarkRevocationList loadFromDatastore() {
return tm().transactNewReadOnly(
() -> {
Iterable<SignedMarkRevocationList> shards =
ofy().load().type(SignedMarkRevocationList.class).ancestor(getCrossTldKey());
DateTime creationTime =
isEmpty(shards)
? START_OF_TIME
: checkNotNull(Iterables.get(shards, 0).creationTime, "creationTime");
ImmutableMap.Builder<String, DateTime> revokes = new ImmutableMap.Builder<>();
for (SignedMarkRevocationList shard : shards) {
revokes.putAll(shard.revokes);
checkState(
creationTime.equals(shard.creationTime),
"Inconsistent creation times: %s vs. %s",
creationTime,
shard.creationTime);
}
return create(creationTime, revokes.build());
});
}
/** Save this list to Datastore in sharded form. */
private SignedMarkRevocationList saveToDatastore() {
tm().transact(
() -> {
ofy()
.deleteWithoutBackup()
.keys(
ofy()
.load()
.type(SignedMarkRevocationList.class)
.ancestor(getCrossTldKey())
.keys());
ofy()
.saveWithoutBackup()
.entities(
CollectionUtils.partitionMap(revokes, SHARD_SIZE).stream()
.map(
shardRevokes -> {
SignedMarkRevocationList shard = create(creationTime, shardRevokes);
shard.id = allocateId();
shard.isShard =
true; // Avoid the exception in disallowUnshardedSaves().
return shard;
})
.collect(toImmutableList()));
});
return this;
}
private static void loadAndCompareCloudSqlList(SignedMarkRevocationList datastoreList) {
// Lifted with some modifications from ClaimsListShard
Optional<SignedMarkRevocationList> maybeCloudSqlList =
SignedMarkRevocationListDao.getLatestRevision();
if (maybeCloudSqlList.isPresent()) {
SignedMarkRevocationList cloudSqlList = maybeCloudSqlList.get();
MapDifference<String, DateTime> diff =
Maps.difference(datastoreList.revokes, cloudSqlList.revokes);
if (!diff.areEqual()) {
if (diff.entriesDiffering().size() > 10) {
String message =
String.format(
"Unequal SM revocation lists detected, Cloud SQL list with revision id %d has %d"
+ " different records than the current Datastore list.",
cloudSqlList.revisionId, diff.entriesDiffering().size());
throw new RuntimeException(message);
} else {
StringBuilder diffMessage = new StringBuilder("Unequal SM revocation lists detected:\n");
diff.entriesDiffering()
.forEach(
(label, valueDiff) ->
diffMessage.append(
String.format(
"SMD %s has key %s in Datastore and key %s in Cloud SQL.\n",
label, valueDiff.leftValue(), valueDiff.rightValue())));
throw new RuntimeException(diffMessage.toString());
}
}
} else {
if (datastoreList.size() != 0) {
throw new RuntimeException("Signed mark revocation list in Cloud SQL is empty.");
}
}
}
/** As a safety mechanism, fail if someone tries to save this class directly. */
@OnSave
void disallowUnshardedSaves() {
@@ -14,28 +14,142 @@
package google.registry.model.smd;
import static google.registry.model.CacheUtils.memoizeWithShortExpiration;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Preconditions.checkState;
import static com.google.common.collect.ImmutableList.toImmutableList;
import static com.google.common.collect.Iterables.isEmpty;
import static google.registry.model.DatabaseMigrationUtils.suppressExceptionUnlessInTest;
import static google.registry.model.common.DatabaseTransitionSchedule.PrimaryDatabase.DATASTORE;
import static google.registry.model.common.EntityGroupRoot.getCrossTldKey;
import static google.registry.model.ofy.ObjectifyService.allocateId;
import static google.registry.model.ofy.ObjectifyService.ofy;
import static google.registry.model.smd.SignedMarkRevocationList.SHARD_SIZE;
import static google.registry.persistence.transaction.TransactionManagerFactory.jpaTm;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import static google.registry.util.CollectionUtils.isNullOrEmpty;
import static google.registry.util.DateTimeUtils.START_OF_TIME;
import com.google.common.base.Supplier;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Iterables;
import com.google.common.collect.MapDifference;
import com.google.common.collect.Maps;
import com.google.common.flogger.FluentLogger;
import google.registry.model.DatabaseMigrationUtils;
import google.registry.model.common.DatabaseTransitionSchedule.PrimaryDatabase;
import google.registry.model.common.DatabaseTransitionSchedule.TransitionId;
import google.registry.util.CollectionUtils;
import java.util.Map;
import java.util.Optional;
import javax.persistence.EntityManager;
import org.joda.time.DateTime;
public class SignedMarkRevocationListDao {
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
private static final Supplier<Optional<SignedMarkRevocationList>> CACHE =
memoizeWithShortExpiration(SignedMarkRevocationListDao::getLatestRevision);
/** Returns the most recent revision of the {@link SignedMarkRevocationList}, from cache. */
public static Optional<SignedMarkRevocationList> getLatestRevisionCached() {
return CACHE.get();
/**
* Loads the {@link SignedMarkRevocationList}.
*
* <p>Loads the list from the specified primary database, and attempts to load from the secondary
* database. If the load the secondary database fails, or the list from the secondary database
* does not match the list from the primary database, the error will be logged but no exception
* will be thrown.
*/
static SignedMarkRevocationList load() {
PrimaryDatabase primaryDatabase =
tm().transactNew(
() ->
DatabaseMigrationUtils.getPrimaryDatabase(
TransitionId.SIGNED_MARK_REVOCATION_LIST));
Optional<SignedMarkRevocationList> primaryList =
primaryDatabase.equals(DATASTORE) ? loadFromDatastore() : loadFromCloudSql();
if (!primaryList.isPresent()) {
throw new IllegalStateException(
String.format(
"SignedMarkRevocationList not found in the primary database (%s).",
primaryDatabase.name()));
}
suppressExceptionUnlessInTest(
() -> loadAndCompare(primaryDatabase, primaryList.get()),
String.format(
"Error loading and comparing the SignedMarkRevocationList from the secondary database"
+ " (%s).",
primaryDatabase.equals(DATASTORE) ? "Cloud SQL" : "Datastore"));
return primaryList.get();
}
public static Optional<SignedMarkRevocationList> getLatestRevision() {
/**
* Loads the list from the secondary database and compares it to the list from the primary
* database.
*/
private static void loadAndCompare(
PrimaryDatabase primaryDatabase, SignedMarkRevocationList primaryList) {
Optional<SignedMarkRevocationList> secondaryList =
primaryDatabase.equals(DATASTORE) ? loadFromCloudSql() : loadFromDatastore();
if (secondaryList.isPresent() && !isNullOrEmpty(secondaryList.get().revokes)) {
MapDifference<String, DateTime> diff =
Maps.difference(primaryList.revokes, secondaryList.get().revokes);
if (!diff.areEqual()) {
if (diff.entriesDiffering().size() > 10) {
String message =
String.format(
"Unequal SignedMarkRevocationList detected, %s list with revision id"
+ " %d has %d different records than the current primary database list.",
primaryDatabase.equals(DATASTORE) ? "Cloud SQL" : "Datastore",
secondaryList.get().revisionId,
diff.entriesDiffering().size());
throw new IllegalStateException(message);
} else {
StringBuilder diffMessage =
new StringBuilder("Unequal SignedMarkRevocationList detected:\n");
diff.entriesDiffering()
.forEach(
(label, valueDiff) ->
diffMessage.append(
String.format(
"SMD %s has key %s in %s and key %s in secondary database.\n",
label,
valueDiff.leftValue(),
primaryDatabase.name(),
valueDiff.rightValue())));
throw new IllegalStateException(diffMessage.toString());
}
}
} else {
if (primaryList.size() != 0) {
throw new IllegalStateException(
String.format(
"SignedMarkRevocationList in %s is empty while it is not empty in the primary"
+ " database.",
primaryDatabase.equals(DATASTORE) ? "Cloud SQL" : "Datastore"));
}
}
}
/** Loads the shards from Datastore and combines them into one list. */
private static Optional<SignedMarkRevocationList> loadFromDatastore() {
return tm().transactNewReadOnly(
() -> {
Iterable<SignedMarkRevocationList> shards =
ofy().load().type(SignedMarkRevocationList.class).ancestor(getCrossTldKey());
DateTime creationTime =
isEmpty(shards)
? START_OF_TIME
: checkNotNull(Iterables.get(shards, 0).creationTime, "creationTime");
ImmutableMap.Builder<String, DateTime> revokes = new ImmutableMap.Builder<>();
for (SignedMarkRevocationList shard : shards) {
revokes.putAll(shard.revokes);
checkState(
creationTime.equals(shard.creationTime),
"Inconsistent creation times in Datastore shard: %s vs. %s",
creationTime,
shard.creationTime);
}
return Optional.of(SignedMarkRevocationList.create(creationTime, revokes.build()));
});
}
private static Optional<SignedMarkRevocationList> loadFromCloudSql() {
return jpaTm()
.transact(
() -> {
@@ -54,24 +168,65 @@ public class SignedMarkRevocationListDao {
}
/**
* Try to save the given {@link SignedMarkRevocationList} into Cloud SQL. If the save fails, the
* error will be logged but no exception will be thrown.
* Save the given {@link SignedMarkRevocationList}
*
* <p>This method is used during the dual-write phase of database migration as Datastore is still
* the authoritative database.
* <p>Saves the list to the specified primary database, and attempts to save to the secondary
* database. If the save to the secondary database fails, the error will be logged but no
* exception will be thrown.
*/
static void trySave(SignedMarkRevocationList signedMarkRevocationList) {
suppressExceptionUnlessInTest(
() -> {
SignedMarkRevocationListDao.save(signedMarkRevocationList);
logger.atInfo().log(
"Inserted %,d signed mark revocations into Cloud SQL.",
signedMarkRevocationList.revokes.size());
},
"Error inserting signed mark revocations into Cloud SQL.");
static void save(SignedMarkRevocationList signedMarkRevocationList) {
PrimaryDatabase primaryDatabase =
tm().transactNew(
() ->
DatabaseMigrationUtils.getPrimaryDatabase(
TransitionId.SIGNED_MARK_REVOCATION_LIST));
if (primaryDatabase.equals(DATASTORE)) {
saveToDatastore(signedMarkRevocationList.revokes, signedMarkRevocationList.creationTime);
suppressExceptionUnlessInTest(
() -> SignedMarkRevocationListDao.saveToCloudSql(signedMarkRevocationList),
"Error inserting signed mark revocations into secondary database (Cloud SQL).");
} else {
SignedMarkRevocationListDao.saveToCloudSql(signedMarkRevocationList);
suppressExceptionUnlessInTest(
() ->
saveToDatastore(
signedMarkRevocationList.revokes, signedMarkRevocationList.creationTime),
"Error inserting signed mark revocations into secondary database (Datastore).");
}
}
private static void save(SignedMarkRevocationList signedMarkRevocationList) {
private static void saveToCloudSql(SignedMarkRevocationList signedMarkRevocationList) {
jpaTm().transact(() -> jpaTm().getEntityManager().persist(signedMarkRevocationList));
logger.atInfo().log(
"Inserted %,d signed mark revocations into Cloud SQL.",
signedMarkRevocationList.revokes.size());
}
private static void saveToDatastore(Map<String, DateTime> revokes, DateTime creationTime) {
tm().transact(
() -> {
ofy()
.deleteWithoutBackup()
.keys(
ofy()
.load()
.type(SignedMarkRevocationList.class)
.ancestor(getCrossTldKey())
.keys());
ofy()
.saveWithoutBackup()
.entities(
CollectionUtils.partitionMap(revokes, SHARD_SIZE).stream()
.map(
shardRevokes -> {
SignedMarkRevocationList shard =
SignedMarkRevocationList.create(creationTime, shardRevokes);
shard.id = allocateId();
shard.isShard =
true; // Avoid the exception in disallowUnshardedSaves().
return shard;
})
.collect(toImmutableList()));
});
}
}
@@ -144,6 +144,16 @@ public class DomainTransferData extends TransferData<DomainTransferData.Builder>
rootKey, serverApproveAutorenewPollMessage, serverApproveAutorenewPollMessageHistoryId);
}
/**
* Fix the VKey "kind" for the PollMessage keys.
*
* <p>For use by DomainBase/DomainHistory OnLoad methods ONLY.
*/
public void convertVKeys() {
serverApproveAutorenewPollMessage =
PollMessage.Autorenew.convertVKey(serverApproveAutorenewPollMessage);
}
@SuppressWarnings("unused") // For Hibernate.
private void loadServerApproveBillingEventHistoryId(
@AlsoLoad("serverApproveBillingEvent") VKey<BillingEvent.OneTime> val) {
@@ -15,6 +15,7 @@
package google.registry.model.transfer;
import static com.google.common.collect.ImmutableList.toImmutableList;
import static google.registry.model.ImmutableObject.DoNotCompare;
import static google.registry.util.CollectionUtils.isNullOrEmpty;
import static google.registry.util.CollectionUtils.nullToEmpty;
import static google.registry.util.CollectionUtils.nullToEmptyImmutableCopy;
@@ -76,6 +77,7 @@ public abstract class TransferData<
* be deleted.
*/
@Transient
@DoNotCompare
@IgnoreSave(IfNull.class)
Set<VKey<? extends TransferServerApproveEntity>> serverApproveEntities;
@@ -46,6 +46,7 @@ import java.lang.annotation.Documented;
import java.sql.Connection;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import javax.annotation.Nullable;
import javax.inject.Provider;
@@ -204,9 +205,24 @@ public abstract class PersistenceModule {
Clock clock) {
HashMap<String, String> overrides = Maps.newHashMap(cloudSqlConfigs);
overrides.put(HIKARI_MAXIMUM_POOL_SIZE, String.valueOf(hikariMaximumPoolSize));
overrides.put(Environment.USER, username);
overrides.put(Environment.PASS, password);
// TODO(b/175700623): consider assigning different logins to pipelines
validateAndSetCredential(
credentialStore, new RobotUser(RobotId.NOMULUS), overrides, username, password);
// TODO(b/179839014): Make SqlCredentialStore injectable in BEAM
// Note: the logs below appear in the pipeline's Worker logs, not the Job log.
try {
SqlCredential credential = credentialStore.getCredential(new RobotUser(RobotId.NOMULUS));
if (!Objects.equals(username, credential.login())) {
logger.atWarning().log(
"Wrong username for nomulus. Expecting %s, found %s.", username, credential.login());
} else if (!Objects.equals(password, credential.password())) {
logger.atWarning().log("Wrong password for nomulus.");
} else {
logger.atWarning().log("Credentials in the kerying and the secret manager match.");
}
} catch (Exception e) {
logger.atWarning().withCause(e).log("Failed to get SQL credential from Secret Manager.");
}
return new JpaTransactionManagerImpl(create(overrides), clock);
}
@@ -280,7 +296,7 @@ public abstract class PersistenceModule {
overrides.put(Environment.PASS, credential.password());
logger.atWarning().log("Credentials in the kerying and the secret manager match.");
} catch (Throwable e) {
logger.atWarning().log(e.getMessage());
logger.atSevere().withCause(e).log("Failed to get SQL credential from Secret Manager");
}
}
@@ -37,7 +37,10 @@ final class DeleteDomainCommand extends MutatingEppToolCommand {
@Parameter(
names = {"--immediately"},
description = "Whether to bypass grace periods and delete the domain immediately.")
description =
"Whether to bypass grace periods and delete the domain immediately. This should only be"
+ " used in exceptional circumstances as it violates the normal expected domain"
+ " lifecycle.")
private boolean immediately = false;
@Parameter(
@@ -46,6 +46,7 @@ import google.registry.testing.FakeClock;
import google.registry.testing.FakeHttpSession;
import google.registry.testing.FakeResponse;
import google.registry.testing.InjectExtension;
import google.registry.util.ProxyHttpHeaders;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
@@ -162,7 +163,8 @@ public class EppTestCase {
FakeResponse response = executeXmlCommand(input);
// Check that the logged-in header was added to the response
assertThat(response.getHeaders()).isEqualTo(ImmutableMap.of("Logged-In", "true"));
assertThat(response.getHeaders())
.isEqualTo(ImmutableMap.of(ProxyHttpHeaders.LOGGED_IN, "true"));
return verifyAndReturnOutput(
response.getPayload(), expectedOutput, inputFilename, outputFilename);
@@ -183,7 +185,7 @@ public class EppTestCase {
// Checks that the Logged-In header is not in the response. If testing the login command, use
// assertLoginCommandAndResponse instead of this method.
assertThat(response.getHeaders()).doesNotContainEntry("Logged-In", "true");
assertThat(response.getHeaders()).doesNotContainEntry(ProxyHttpHeaders.LOGGED_IN, "true");
return verifyAndReturnOutput(
response.getPayload(), expectedOutput, inputFilename, outputFilename);
@@ -33,6 +33,7 @@ import google.registry.model.registrar.Registrar;
import google.registry.testing.AppEngineExtension;
import google.registry.testing.FakeClock;
import google.registry.util.CidrAddressBlock;
import google.registry.util.ProxyHttpHeaders;
import java.util.Optional;
import javax.servlet.http.HttpServletRequest;
import org.joda.time.DateTime;
@@ -59,7 +60,7 @@ final class TlsCredentialsTest {
@Test
void testProvideClientCertificateHash() {
HttpServletRequest req = mock(HttpServletRequest.class);
when(req.getHeader("X-SSL-Certificate")).thenReturn("data");
when(req.getHeader(ProxyHttpHeaders.CERTIFICATE_HASH)).thenReturn("data");
assertThat(TlsCredentials.EppTlsModule.provideClientCertificateHash(req)).hasValue("data");
}
@@ -128,7 +129,7 @@ final class TlsCredentialsTest {
@Test
void testProvideClientCertificate() {
HttpServletRequest req = mock(HttpServletRequest.class);
when(req.getHeader("X-SSL-Full-Certificate")).thenReturn("data");
when(req.getHeader(ProxyHttpHeaders.FULL_CERTIFICATE)).thenReturn("data");
assertThat(TlsCredentials.EppTlsModule.provideClientCertificate(req)).hasValue("data");
}
@@ -887,6 +887,20 @@ class DomainCheckFlowTest extends ResourceCheckFlowTestCase<DomainCheckFlow, Dom
runFlowAssertResponse(loadFile("domain_check_fee_reserved_response_v06.xml"));
}
@Test
void testFeeExtension_reservedName_restoreFeeWithDupes_v06() throws Exception {
persistResource(
Registry.get("tld")
.asBuilder()
.setReservedLists(createReservedList())
.setPremiumList(persistPremiumList("tld", "premiumcollision,USD 70"))
.build());
// The domain needs to exist in order for it to be loaded to check for restore fee.
persistActiveDomain("allowedinsunrise.tld");
setEppInput("domain_check_fee_reserved_dupes_v06.xml");
runFlowAssertResponse(loadFile("domain_check_fee_reserved_response_dupes_v06.xml"));
}
/** The tests must be split up for version 11, which allows only one command at a time. */
@Test
void testFeeExtension_reservedName_v11_create() throws Exception {
@@ -965,6 +979,20 @@ class DomainCheckFlowTest extends ResourceCheckFlowTestCase<DomainCheckFlow, Dom
runFlowAssertResponse(loadFile("domain_check_fee_reserved_response_v12.xml"));
}
@Test
void testFeeExtension_reservedName_restoreFeeWithDupes_v12() throws Exception {
persistResource(
Registry.get("tld")
.asBuilder()
.setReservedLists(createReservedList())
.setPremiumList(persistPremiumList("tld", "premiumcollision,USD 70"))
.build());
// The domain needs to exist in order for it to be loaded to check for restore fee.
persistActiveDomain("allowedinsunrise.tld");
setEppInput("domain_check_fee_reserved_dupes_v12.xml");
runFlowAssertResponse(loadFile("domain_check_fee_reserved_dupes_response_v12.xml"));
}
@Test
void testFeeExtension_feesNotOmittedOnReservedNamesInSunrise_v06() throws Exception {
createTld("tld", START_DATE_SUNRISE);
@@ -112,7 +112,7 @@ class DomainDeleteFlowTest extends ResourceFlowTestCase<DomainDeleteFlow, Domain
@Order(value = Order.DEFAULT - 2)
@RegisterExtension
final ReplayExtension replayExtension = ReplayExtension.createWithoutCompare(clock);
final ReplayExtension replayExtension = ReplayExtension.createWithCompare(clock);
private DomainBase domain;
private HistoryEntry earlierHistoryEntry;
@@ -108,7 +108,7 @@ class DomainRenewFlowTest extends ResourceFlowTestCase<DomainRenewFlow, DomainBa
@Order(value = Order.DEFAULT - 2)
@RegisterExtension
final ReplayExtension replayExtension = ReplayExtension.createWithoutCompare(clock);
final ReplayExtension replayExtension = ReplayExtension.createWithCompare(clock);
@BeforeEach
void initDomainTest() {
@@ -117,7 +117,7 @@ class DomainUpdateFlowTest extends ResourceFlowTestCase<DomainUpdateFlow, Domain
@Order(value = Order.DEFAULT - 2)
@RegisterExtension
final ReplayExtension replayExtension = ReplayExtension.createWithoutCompare(clock);
final ReplayExtension replayExtension = ReplayExtension.createWithCompare(clock);
@BeforeEach
void initDomainTest() {
@@ -16,21 +16,35 @@ package google.registry.model.smd;
import static com.google.common.truth.Truth.assertThat;
import static google.registry.model.ImmutableObjectSubject.assertAboutImmutableObjects;
import static google.registry.model.ofy.ObjectifyService.ofy;
import static google.registry.persistence.transaction.TransactionManagerFactory.jpaTm;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import static google.registry.util.DateTimeUtils.START_OF_TIME;
import static org.junit.jupiter.api.Assertions.assertThrows;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSortedMap;
import google.registry.config.RegistryEnvironment;
import google.registry.model.EntityTestCase;
import google.registry.model.common.DatabaseTransitionSchedule;
import google.registry.model.common.DatabaseTransitionSchedule.PrimaryDatabase;
import google.registry.model.common.DatabaseTransitionSchedule.PrimaryDatabaseTransition;
import google.registry.model.common.DatabaseTransitionSchedule.TransitionId;
import google.registry.model.common.TimedTransitionProperty;
import google.registry.persistence.transaction.JpaTestRules;
import google.registry.persistence.transaction.JpaTestRules.JpaIntegrationWithCoverageExtension;
import google.registry.testing.DatastoreEntityExtension;
import google.registry.testing.DualDatabaseTest;
import google.registry.testing.FakeClock;
import google.registry.testing.SystemPropertyExtension;
import google.registry.testing.TestOfyAndSql;
import org.joda.time.DateTime;
import org.joda.time.Duration;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Order;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;
@DualDatabaseTest
public class SignedMarkRevocationListDaoTest {
private final FakeClock fakeClock = new FakeClock();
public class SignedMarkRevocationListDaoTest extends EntityTestCase {
@RegisterExtension
final JpaIntegrationWithCoverageExtension jpa =
@@ -40,50 +54,186 @@ public class SignedMarkRevocationListDaoTest {
@Order(value = 1)
final DatastoreEntityExtension datastoreEntityExtension = new DatastoreEntityExtension();
@Test
void testSave_success() {
@RegisterExtension
@Order(value = Integer.MAX_VALUE)
final SystemPropertyExtension systemPropertyExtension = new SystemPropertyExtension();
@BeforeEach
void setup() {
fakeClock.setTo(DateTime.parse("1984-12-21T00:00:00.000Z"));
DatabaseTransitionSchedule schedule =
DatabaseTransitionSchedule.create(
TransitionId.SIGNED_MARK_REVOCATION_LIST,
TimedTransitionProperty.fromValueMap(
ImmutableSortedMap.of(
START_OF_TIME,
PrimaryDatabase.DATASTORE,
fakeClock.nowUtc().plusDays(1),
PrimaryDatabase.CLOUD_SQL),
PrimaryDatabaseTransition.class));
tm().transactNew(() -> ofy().saveWithoutBackup().entity(schedule).now());
}
@TestOfyAndSql
void testSave_datastorePrimary_success() {
SignedMarkRevocationList list =
SignedMarkRevocationList.create(
fakeClock.nowUtc(), ImmutableMap.of("mark", fakeClock.nowUtc().minusHours(1)));
SignedMarkRevocationListDao.trySave(list);
SignedMarkRevocationList fromDb = SignedMarkRevocationListDao.getLatestRevision().get();
SignedMarkRevocationListDao.save(list);
SignedMarkRevocationList fromDb = SignedMarkRevocationListDao.load();
assertAboutImmutableObjects().that(fromDb).isEqualExceptFields(list, "revisionId");
}
@TestOfyAndSql
void testSave_cloudSqlPrimary_success() {
fakeClock.advanceBy(Duration.standardDays(5));
SignedMarkRevocationList list =
SignedMarkRevocationList.create(
fakeClock.nowUtc(), ImmutableMap.of("mark", fakeClock.nowUtc().minusHours(1)));
SignedMarkRevocationListDao.save(list);
SignedMarkRevocationList fromDb = SignedMarkRevocationListDao.load();
assertAboutImmutableObjects().that(fromDb).isEqualExceptFields(list);
}
@Test
void testRetrieval_notPresent() {
assertThat(SignedMarkRevocationListDao.getLatestRevision().isPresent()).isFalse();
@TestOfyAndSql
void testSaveAndLoad_datastorePrimary_emptyList() {
SignedMarkRevocationList list =
SignedMarkRevocationList.create(START_OF_TIME, ImmutableMap.of());
SignedMarkRevocationListDao.save(list);
SignedMarkRevocationList fromDb = SignedMarkRevocationListDao.load();
assertAboutImmutableObjects().that(fromDb).isEqualExceptFields(list, "revisionId");
}
@Test
void testSaveAndRetrieval_emptyList() {
@TestOfyAndSql
void testSaveAndLoad_cloudSqlPrimary_emptyList() {
fakeClock.advanceBy(Duration.standardDays(5));
SignedMarkRevocationList list =
SignedMarkRevocationList.create(fakeClock.nowUtc(), ImmutableMap.of());
SignedMarkRevocationListDao.trySave(list);
SignedMarkRevocationList fromDb = SignedMarkRevocationListDao.getLatestRevision().get();
assertAboutImmutableObjects().that(fromDb).isEqualExceptFields(list);
SignedMarkRevocationListDao.save(list);
SignedMarkRevocationList fromDb = SignedMarkRevocationListDao.load();
assertAboutImmutableObjects().that(fromDb).isEqualExceptFields(list, "revisionId");
}
@Test
void testSave_multipleVersions() {
@TestOfyAndSql
void testSave_datastorePrimary_multipleVersions() {
SignedMarkRevocationList list =
SignedMarkRevocationList.create(
fakeClock.nowUtc(), ImmutableMap.of("mark", fakeClock.nowUtc().minusHours(1)));
SignedMarkRevocationListDao.trySave(list);
assertThat(
SignedMarkRevocationListDao.getLatestRevision()
.get()
.isSmdRevoked("mark", fakeClock.nowUtc()))
SignedMarkRevocationListDao.save(list);
assertThat(SignedMarkRevocationListDao.load().isSmdRevoked("mark", fakeClock.nowUtc()))
.isTrue();
// Now remove the revocation
SignedMarkRevocationList secondList =
SignedMarkRevocationList.create(fakeClock.nowUtc(), ImmutableMap.of());
SignedMarkRevocationListDao.trySave(secondList);
assertThat(
SignedMarkRevocationListDao.getLatestRevision()
.get()
.isSmdRevoked("mark", fakeClock.nowUtc()))
SignedMarkRevocationListDao.save(secondList);
assertThat(SignedMarkRevocationListDao.load().isSmdRevoked("mark", fakeClock.nowUtc()))
.isFalse();
}
@TestOfyAndSql
void testSave_cloudSqlPrimary_multipleVersions() {
fakeClock.advanceBy(Duration.standardDays(5));
SignedMarkRevocationList list =
SignedMarkRevocationList.create(
fakeClock.nowUtc(), ImmutableMap.of("mark", fakeClock.nowUtc().minusHours(1)));
SignedMarkRevocationListDao.save(list);
assertThat(SignedMarkRevocationListDao.load().isSmdRevoked("mark", fakeClock.nowUtc()))
.isTrue();
// Now remove the revocation
SignedMarkRevocationList secondList =
SignedMarkRevocationList.create(fakeClock.nowUtc(), ImmutableMap.of());
SignedMarkRevocationListDao.save(secondList);
assertThat(SignedMarkRevocationListDao.load().isSmdRevoked("mark", fakeClock.nowUtc()))
.isFalse();
}
@TestOfyAndSql
void testLoad_datastorePrimary_unequalLists() {
SignedMarkRevocationList list =
SignedMarkRevocationList.create(
fakeClock.nowUtc(), ImmutableMap.of("mark", fakeClock.nowUtc().minusHours(1)));
SignedMarkRevocationListDao.save(list);
SignedMarkRevocationList list2 =
SignedMarkRevocationList.create(
fakeClock.nowUtc(), ImmutableMap.of("mark", fakeClock.nowUtc().minusHours(3)));
jpaTm().transact(() -> jpaTm().put(list2));
RuntimeException thrown =
assertThrows(RuntimeException.class, SignedMarkRevocationListDao::load);
assertThat(thrown)
.hasMessageThat()
.contains(
"SMD mark has key 1984-12-20T23:00:00.000Z in DATASTORE and key"
+ " 1984-12-20T21:00:00.000Z in secondary database.");
}
@TestOfyAndSql
void testLoad_cloudSqlPrimary_unequalLists() {
fakeClock.advanceBy(Duration.standardDays(5));
SignedMarkRevocationList list =
SignedMarkRevocationList.create(
fakeClock.nowUtc(), ImmutableMap.of("mark", fakeClock.nowUtc().minusHours(1)));
SignedMarkRevocationListDao.save(list);
SignedMarkRevocationList list2 =
SignedMarkRevocationList.create(
fakeClock.nowUtc(), ImmutableMap.of("mark", fakeClock.nowUtc().minusHours(3)));
jpaTm().transact(() -> jpaTm().put(list2));
RuntimeException thrown =
assertThrows(RuntimeException.class, SignedMarkRevocationListDao::load);
assertThat(thrown)
.hasMessageThat()
.contains(
"SMD mark has key 1984-12-25T21:00:00.000Z in CLOUD_SQL and key"
+ " 1984-12-25T23:00:00.000Z in secondary database.");
}
@TestOfyAndSql
void testLoad_cloudSqlPrimary_unequalLists_succeedsInProduction() {
RegistryEnvironment.PRODUCTION.setup(systemPropertyExtension);
fakeClock.advanceBy(Duration.standardDays(5));
SignedMarkRevocationList list =
SignedMarkRevocationList.create(
fakeClock.nowUtc(), ImmutableMap.of("mark", fakeClock.nowUtc().minusHours(1)));
SignedMarkRevocationListDao.save(list);
SignedMarkRevocationList list2 =
SignedMarkRevocationList.create(
fakeClock.nowUtc(), ImmutableMap.of("mark", fakeClock.nowUtc().minusHours(3)));
jpaTm().transact(() -> jpaTm().put(list2));
SignedMarkRevocationList fromDb = SignedMarkRevocationListDao.load();
assertAboutImmutableObjects().that(fromDb).isEqualExceptFields(list2, "revisionId");
}
@TestOfyAndSql
void testLoad_datastorePrimary_noListInCloudSql() {
SignedMarkRevocationList list =
SignedMarkRevocationList.create(
fakeClock.nowUtc(), ImmutableMap.of("mark", fakeClock.nowUtc().minusHours(1)));
SignedMarkRevocationListDao.save(list);
jpaTm().transact(() -> jpaTm().delete(list));
RuntimeException thrown =
assertThrows(RuntimeException.class, SignedMarkRevocationListDao::load);
assertThat(thrown)
.hasMessageThat()
.contains(
"SignedMarkRevocationList in Cloud SQL is empty while it is not empty in the"
+ " primary database.");
}
@TestOfyAndSql
void testLoad_cloudSqlPrimary_noListInDatastore() {
fakeClock.advanceBy(Duration.standardDays(5));
SignedMarkRevocationList list =
SignedMarkRevocationList.create(
fakeClock.nowUtc(), ImmutableMap.of("mark", fakeClock.nowUtc().minusHours(1)));
jpaTm().transact(() -> jpaTm().put(list));
RuntimeException thrown =
assertThrows(RuntimeException.class, SignedMarkRevocationListDao::load);
assertThat(thrown)
.hasMessageThat()
.contains(
"SignedMarkRevocationList in Datastore is empty while it is not empty in the"
+ " primary database.");
}
}
@@ -139,18 +139,6 @@ public class SignedMarkRevocationListTest {
.isEqualTo(DateTime.parse("2000-01-01T00:00:00Z"));
}
@Test
void test_getCreationTime_missingInCloudSQL() {
clock.setTo(DateTime.parse("2000-01-01T00:00:00Z"));
createSaveGetHelper(1);
jpaTm().transact(() -> jpaTm().delete(SignedMarkRevocationListDao.getLatestRevision().get()));
RuntimeException thrown =
assertThrows(RuntimeException.class, () -> SignedMarkRevocationList.get());
assertThat(thrown)
.hasMessageThat()
.isEqualTo("Signed mark revocation list in Cloud SQL is empty.");
}
@Test
void test_getCreationTime_unequalListsInDatabases() {
clock.setTo(DateTime.parse("2000-01-01T00:00:00Z"));
@@ -159,11 +147,15 @@ public class SignedMarkRevocationListTest {
for (int i = 0; i < 3; i++) {
revokes.put(Integer.toString(i), clock.nowUtc());
}
SignedMarkRevocationListDao.trySave(
SignedMarkRevocationList.create(clock.nowUtc(), revokes.build()));
jpaTm()
.transact(
() ->
jpaTm()
.getEntityManager()
.persist(SignedMarkRevocationList.create(clock.nowUtc(), revokes.build())));
RuntimeException thrown =
assertThrows(RuntimeException.class, () -> SignedMarkRevocationList.get());
assertThat(thrown).hasMessageThat().contains("Unequal SM revocation lists detected:");
assertThat(thrown).hasMessageThat().contains("Unequal SignedMarkRevocationList detected:");
}
@Test
@@ -0,0 +1,147 @@
<epp xmlns="urn:ietf:params:xml:ns:epp-1.0">
<response>
<result code="1000">
<msg>Command completed successfully</msg>
</result>
<resData>
<domain:chkData xmlns:domain="urn:ietf:params:xml:ns:domain-1.0">
<domain:cd>
<domain:name avail="0">reserved.tld</domain:name>
<domain:reason>Reserved</domain:reason>
</domain:cd>
<domain:cd>
<domain:name avail="0">allowedinsunrise.tld</domain:name>
<domain:reason>In use</domain:reason>
</domain:cd>
<domain:cd>
<domain:name avail="0">allowedinsunrise.tld</domain:name>
<domain:reason>In use</domain:reason>
</domain:cd>
<domain:cd>
<domain:name avail="0">premiumcollision.tld</domain:name>
<domain:reason>Cannot be delegated</domain:reason>
</domain:cd>
</domain:chkData>
</resData>
<extension>
<fee:chkData xmlns:fee="urn:ietf:params:xml:ns:fee-0.12"
xmlns:domain="urn:ietf:params:xml:ns:domain-1.0">
<fee:currency>USD</fee:currency>
<fee:cd>
<fee:object>
<domain:name>reserved.tld</domain:name>
</fee:object>
<fee:command name="create">
<fee:period unit="y">1</fee:period>
<fee:class>reserved</fee:class>
</fee:command>
</fee:cd>
<fee:cd>
<fee:object>
<domain:name>reserved.tld</domain:name>
</fee:object>
<fee:command name="renew">
<fee:period unit="y">1</fee:period>
<fee:fee description="renew">11.00</fee:fee>
</fee:command>
</fee:cd>
<fee:cd>
<fee:object>
<domain:name>reserved.tld</domain:name>
</fee:object>
<fee:command name="transfer">
<fee:period unit="y">1</fee:period>
<fee:fee description="renew">11.00</fee:fee>
</fee:command>
</fee:cd>
<fee:cd>
<fee:object>
<domain:name>reserved.tld</domain:name>
</fee:object>
<fee:command name="restore">
<fee:period unit="y">1</fee:period>
<fee:fee description="restore">17.00</fee:fee>
</fee:command>
</fee:cd>
<fee:cd>
<fee:object>
<domain:name>allowedinsunrise.tld</domain:name>
</fee:object>
<fee:command name="create">
<fee:period unit="y">1</fee:period>
<fee:class>reserved</fee:class>
</fee:command>
</fee:cd>
<fee:cd>
<fee:object>
<domain:name>allowedinsunrise.tld</domain:name>
</fee:object>
<fee:command name="renew">
<fee:period unit="y">1</fee:period>
<fee:fee description="renew">11.00</fee:fee>
</fee:command>
</fee:cd>
<fee:cd>
<fee:object>
<domain:name>allowedinsunrise.tld</domain:name>
</fee:object>
<fee:command name="transfer">
<fee:period unit="y">1</fee:period>
<fee:fee description="renew">11.00</fee:fee>
</fee:command>
</fee:cd>
<fee:cd>
<fee:object>
<domain:name>allowedinsunrise.tld</domain:name>
</fee:object>
<fee:command name="restore">
<fee:period unit="y">1</fee:period>
<fee:fee description="restore">17.00</fee:fee>
</fee:command>
</fee:cd>
<fee:cd>
<fee:object>
<domain:name>premiumcollision.tld</domain:name>
</fee:object>
<fee:command name="create">
<fee:period unit="y">1</fee:period>
<fee:class>reserved</fee:class>
</fee:command>
</fee:cd>
<fee:cd>
<fee:object>
<domain:name>premiumcollision.tld</domain:name>
</fee:object>
<fee:command name="renew">
<fee:period unit="y">1</fee:period>
<fee:fee description="renew">70.00</fee:fee>
<fee:class>premium</fee:class>
</fee:command>
</fee:cd>
<fee:cd>
<fee:object>
<domain:name>premiumcollision.tld</domain:name>
</fee:object>
<fee:command name="transfer">
<fee:period unit="y">1</fee:period>
<fee:fee description="renew">70.00</fee:fee>
<fee:class>premium</fee:class>
</fee:command>
</fee:cd>
<fee:cd>
<fee:object>
<domain:name>premiumcollision.tld</domain:name>
</fee:object>
<fee:command name="restore">
<fee:period unit="y">1</fee:period>
<fee:fee description="restore">17.00</fee:fee>
</fee:command>
</fee:cd>
</fee:chkData>
</extension>
<trID>
<clTRID>ABC-12345</clTRID>
<svTRID>server-trid</svTRID>
</trID>
</response>
</epp>
@@ -0,0 +1,116 @@
<epp xmlns="urn:ietf:params:xml:ns:epp-1.0">
<command>
<check>
<domain:check xmlns:domain="urn:ietf:params:xml:ns:domain-1.0">
<domain:name>reserved.tld</domain:name>
<domain:name>allowedinsunrise.tld</domain:name>
<domain:name>allowedinsunrise.tld</domain:name>
<domain:name>premiumcollision.tld</domain:name>
</domain:check>
</check>
<extension>
<launch:check xmlns:launch="urn:ietf:params:xml:ns:launch-1.0" type="avail">
<launch:phase name="foo">custom</launch:phase>
</launch:check>
<fee:check xmlns:fee="urn:ietf:params:xml:ns:fee-0.6">
<fee:domain>
<fee:name>reserved.tld</fee:name>
<fee:currency>USD</fee:currency>
<fee:command>create</fee:command>
<fee:period unit="y">1</fee:period>
</fee:domain>
<fee:domain>
<fee:name>reserved.tld</fee:name>
<fee:currency>USD</fee:currency>
<fee:command>renew</fee:command>
<fee:period unit="y">1</fee:period>
</fee:domain>
<fee:domain>
<fee:name>reserved.tld</fee:name>
<fee:currency>USD</fee:currency>
<fee:command>transfer</fee:command>
<fee:period unit="y">1</fee:period>
</fee:domain>
<fee:domain>
<fee:name>reserved.tld</fee:name>
<fee:currency>USD</fee:currency>
<fee:command>restore</fee:command>
<fee:period unit="y">1</fee:period>
</fee:domain>
<fee:domain>
<fee:name>allowedinsunrise.tld</fee:name>
<fee:currency>USD</fee:currency>
<fee:command>create</fee:command>
<fee:period unit="y">1</fee:period>
</fee:domain>
<fee:domain>
<fee:name>allowedinsunrise.tld</fee:name>
<fee:currency>USD</fee:currency>
<fee:command>renew</fee:command>
<fee:period unit="y">1</fee:period>
</fee:domain>
<fee:domain>
<fee:name>allowedinsunrise.tld</fee:name>
<fee:currency>USD</fee:currency>
<fee:command>transfer</fee:command>
<fee:period unit="y">1</fee:period>
</fee:domain>
<fee:domain>
<fee:name>allowedinsunrise.tld</fee:name>
<fee:currency>USD</fee:currency>
<fee:command>restore</fee:command>
<fee:period unit="y">1</fee:period>
</fee:domain>
<fee:domain>
<fee:name>allowedinsunrise.tld</fee:name>
<fee:currency>USD</fee:currency>
<fee:command>create</fee:command>
<fee:period unit="y">1</fee:period>
</fee:domain>
<fee:domain>
<fee:name>allowedinsunrise.tld</fee:name>
<fee:currency>USD</fee:currency>
<fee:command>renew</fee:command>
<fee:period unit="y">1</fee:period>
</fee:domain>
<fee:domain>
<fee:name>allowedinsunrise.tld</fee:name>
<fee:currency>USD</fee:currency>
<fee:command>transfer</fee:command>
<fee:period unit="y">1</fee:period>
</fee:domain>
<fee:domain>
<fee:name>allowedinsunrise.tld</fee:name>
<fee:currency>USD</fee:currency>
<fee:command>restore</fee:command>
<fee:period unit="y">1</fee:period>
</fee:domain>
<fee:domain>
<fee:name>premiumcollision.tld</fee:name>
<fee:currency>USD</fee:currency>
<fee:command>create</fee:command>
<fee:period unit="y">1</fee:period>
</fee:domain>
<fee:domain>
<fee:name>premiumcollision.tld</fee:name>
<fee:currency>USD</fee:currency>
<fee:command>renew</fee:command>
<fee:period unit="y">1</fee:period>
</fee:domain>
<fee:domain>
<fee:name>premiumcollision.tld</fee:name>
<fee:currency>USD</fee:currency>
<fee:command>transfer</fee:command>
<fee:period unit="y">1</fee:period>
</fee:domain>
<fee:domain>
<fee:name>premiumcollision.tld</fee:name>
<fee:currency>USD</fee:currency>
<fee:command>restore</fee:command>
<fee:period unit="y">1</fee:period>
</fee:domain>
</fee:check>
</extension>
<clTRID>ABC-12345</clTRID>
</command>
</epp>
@@ -0,0 +1,33 @@
<epp xmlns="urn:ietf:params:xml:ns:epp-1.0">
<command>
<check>
<domain:check xmlns:domain="urn:ietf:params:xml:ns:domain-1.0">
<domain:name>reserved.tld</domain:name>
<domain:name>allowedinsunrise.tld</domain:name>
<domain:name>allowedinsunrise.tld</domain:name>
<domain:name>premiumcollision.tld</domain:name>
</domain:check>
</check>
<extension>
<launch:check xmlns:launch="urn:ietf:params:xml:ns:launch-1.0" type="avail">
<launch:phase name="foo">custom</launch:phase>
</launch:check>
<fee:check xmlns:fee="urn:ietf:params:xml:ns:fee-0.12">
<fee:currency>USD</fee:currency>
<fee:command name="create">
<fee:period unit="y">1</fee:period>
</fee:command>
<fee:command name="renew">
<fee:period unit="y">1</fee:period>
</fee:command>
<fee:command name="transfer">
<fee:period unit="y">1</fee:period>
</fee:command>
<fee:command name="restore">
<fee:period unit="y">1</fee:period>
</fee:command>
</fee:check>
</extension>
<clTRID>ABC-12345</clTRID>
</command>
</epp>
@@ -0,0 +1,149 @@
<epp xmlns="urn:ietf:params:xml:ns:epp-1.0">
<response>
<result code="1000">
<msg>Command completed successfully</msg>
</result>
<resData>
<domain:chkData xmlns:domain="urn:ietf:params:xml:ns:domain-1.0">
<domain:cd>
<domain:name avail="0">reserved.tld</domain:name>
<domain:reason>Reserved</domain:reason>
</domain:cd>
<domain:cd>
<domain:name avail="0">allowedinsunrise.tld</domain:name>
<domain:reason>In use</domain:reason>
</domain:cd>
<domain:cd>
<domain:name avail="0">allowedinsunrise.tld</domain:name>
<domain:reason>In use</domain:reason>
</domain:cd>
<domain:cd>
<domain:name avail="0">premiumcollision.tld</domain:name>
<domain:reason>Cannot be delegated</domain:reason>
</domain:cd>
</domain:chkData>
</resData>
<extension>
<fee:chkData xmlns:fee="urn:ietf:params:xml:ns:fee-0.6">
<fee:cd xmlns:fee="urn:ietf:params:xml:ns:fee-0.6">
<fee:name>reserved.tld</fee:name>
<fee:currency>USD</fee:currency>
<fee:command>create</fee:command>
<fee:period unit="y">1</fee:period>
<fee:class>reserved</fee:class>
</fee:cd>
<fee:cd xmlns:fee="urn:ietf:params:xml:ns:fee-0.6">
<fee:name>reserved.tld</fee:name>
<fee:currency>USD</fee:currency>
<fee:command>renew</fee:command>
<fee:period unit="y">1</fee:period>
<fee:fee description="renew">11.00</fee:fee>
</fee:cd>
<fee:cd xmlns:fee="urn:ietf:params:xml:ns:fee-0.6">
<fee:name>reserved.tld</fee:name>
<fee:currency>USD</fee:currency>
<fee:command>transfer</fee:command>
<fee:period unit="y">1</fee:period>
<fee:fee description="renew">11.00</fee:fee>
</fee:cd>
<fee:cd xmlns:fee="urn:ietf:params:xml:ns:fee-0.6">
<fee:name>reserved.tld</fee:name>
<fee:currency>USD</fee:currency>
<fee:command>restore</fee:command>
<fee:period unit="y">1</fee:period>
<fee:fee description="restore">17.00</fee:fee>
</fee:cd>
<fee:cd xmlns:fee="urn:ietf:params:xml:ns:fee-0.6">
<fee:name>allowedinsunrise.tld</fee:name>
<fee:currency>USD</fee:currency>
<fee:command>create</fee:command>
<fee:period unit="y">1</fee:period>
<fee:class>reserved</fee:class>
</fee:cd>
<fee:cd xmlns:fee="urn:ietf:params:xml:ns:fee-0.6">
<fee:name>allowedinsunrise.tld</fee:name>
<fee:currency>USD</fee:currency>
<fee:command>renew</fee:command>
<fee:period unit="y">1</fee:period>
<fee:fee description="renew">11.00</fee:fee>
</fee:cd>
<fee:cd xmlns:fee="urn:ietf:params:xml:ns:fee-0.6">
<fee:name>allowedinsunrise.tld</fee:name>
<fee:currency>USD</fee:currency>
<fee:command>transfer</fee:command>
<fee:period unit="y">1</fee:period>
<fee:fee description="renew">11.00</fee:fee>
</fee:cd>
<fee:cd xmlns:fee="urn:ietf:params:xml:ns:fee-0.6">
<fee:name>allowedinsunrise.tld</fee:name>
<fee:currency>USD</fee:currency>
<fee:command>restore</fee:command>
<fee:period unit="y">1</fee:period>
<fee:fee description="restore">17.00</fee:fee>
</fee:cd>
<fee:cd xmlns:fee="urn:ietf:params:xml:ns:fee-0.6">
<fee:name>allowedinsunrise.tld</fee:name>
<fee:currency>USD</fee:currency>
<fee:command>create</fee:command>
<fee:period unit="y">1</fee:period>
<fee:class>reserved</fee:class>
</fee:cd>
<fee:cd xmlns:fee="urn:ietf:params:xml:ns:fee-0.6">
<fee:name>allowedinsunrise.tld</fee:name>
<fee:currency>USD</fee:currency>
<fee:command>renew</fee:command>
<fee:period unit="y">1</fee:period>
<fee:fee description="renew">11.00</fee:fee>
</fee:cd>
<fee:cd xmlns:fee="urn:ietf:params:xml:ns:fee-0.6">
<fee:name>allowedinsunrise.tld</fee:name>
<fee:currency>USD</fee:currency>
<fee:command>transfer</fee:command>
<fee:period unit="y">1</fee:period>
<fee:fee description="renew">11.00</fee:fee>
</fee:cd>
<fee:cd xmlns:fee="urn:ietf:params:xml:ns:fee-0.6">
<fee:name>allowedinsunrise.tld</fee:name>
<fee:currency>USD</fee:currency>
<fee:command>restore</fee:command>
<fee:period unit="y">1</fee:period>
<fee:fee description="restore">17.00</fee:fee>
</fee:cd>
<fee:cd xmlns:fee="urn:ietf:params:xml:ns:fee-0.6">
<fee:name>premiumcollision.tld</fee:name>
<fee:currency>USD</fee:currency>
<fee:command>create</fee:command>
<fee:period unit="y">1</fee:period>
<fee:class>reserved</fee:class>
</fee:cd>
<fee:cd xmlns:fee="urn:ietf:params:xml:ns:fee-0.6">
<fee:name>premiumcollision.tld</fee:name>
<fee:currency>USD</fee:currency>
<fee:command>renew</fee:command>
<fee:period unit="y">1</fee:period>
<fee:fee description="renew">70.00</fee:fee>
<fee:class>premium</fee:class>
</fee:cd>
<fee:cd xmlns:fee="urn:ietf:params:xml:ns:fee-0.6">
<fee:name>premiumcollision.tld</fee:name>
<fee:currency>USD</fee:currency>
<fee:command>transfer</fee:command>
<fee:period unit="y">1</fee:period>
<fee:fee description="renew">70.00</fee:fee>
<fee:class>premium</fee:class>
</fee:cd>
<fee:cd xmlns:fee="urn:ietf:params:xml:ns:fee-0.6">
<fee:name>premiumcollision.tld</fee:name>
<fee:currency>USD</fee:currency>
<fee:command>restore</fee:command>
<fee:period unit="y">1</fee:period>
<fee:fee description="restore">17.00</fee:fee>
</fee:cd>
</fee:chkData>
</extension>
<trID>
<clTRID>ABC-12345</clTRID>
<svTRID>server-trid</svTRID>
</trID>
</response>
</epp>
@@ -22,6 +22,7 @@ import static google.registry.util.X509Utils.getCertificateHash;
import com.google.common.flogger.FluentLogger;
import google.registry.proxy.metric.FrontendMetrics;
import google.registry.util.ProxyHttpHeaders;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelFuture;
@@ -51,21 +52,6 @@ public class EppServiceHandler extends HttpsRelayServiceHandler {
public static final AttributeKey<String> CLIENT_CERTIFICATE_HASH_KEY =
AttributeKey.valueOf("CLIENT_CERTIFICATE_HASH_KEY");
/** Name of the HTTP header that stores the client certificate hash. */
public static final String SSL_CLIENT_CERTIFICATE_HASH_FIELD = "X-SSL-Certificate";
/** Name of the HTTP header that stores the full client certificate. */
public static final String SSL_CLIENT_FULL_CERTIFICATE_FIELD = "X-SSL-Full-Certificate";
/** Name of the HTTP header that stores the client IP address. */
public static final String FORWARDED_FOR_FIELD = "X-Forwarded-For";
/** Name of the HTTP header that indicates if the EPP session should be closed. */
public static final String EPP_SESSION_FIELD = "Epp-Session";
/** Name of the HTTP header that indicates a successful login has occurred. */
public static final String EPP_LOGGED_IN_FIELD = "Logged-In";
public static final String EPP_CONTENT_TYPE = "application/epp+xml";
private final byte[] helloBytes;
@@ -139,8 +125,8 @@ public class EppServiceHandler extends HttpsRelayServiceHandler {
FullHttpRequest request = super.decodeFullHttpRequest(byteBuf);
request
.headers()
.set(SSL_CLIENT_CERTIFICATE_HASH_FIELD, sslClientCertificateHash)
.set(FORWARDED_FOR_FIELD, clientAddress)
.set(ProxyHttpHeaders.CERTIFICATE_HASH, sslClientCertificateHash)
.set(ProxyHttpHeaders.IP_ADDRESS, clientAddress)
.set(HttpHeaderNames.CONTENT_TYPE, EPP_CONTENT_TYPE)
.set(HttpHeaderNames.ACCEPT, EPP_CONTENT_TYPE);
if (!isLoggedIn) {
@@ -148,7 +134,7 @@ public class EppServiceHandler extends HttpsRelayServiceHandler {
request
.headers()
.set(
SSL_CLIENT_FULL_CERTIFICATE_FIELD,
ProxyHttpHeaders.FULL_CERTIFICATE,
Base64.getEncoder().encodeToString(sslClientCertificate.getEncoded()));
} catch (CertificateEncodingException e) {
throw new RuntimeException("Cannot encode client certificate", e);
@@ -162,8 +148,8 @@ public class EppServiceHandler extends HttpsRelayServiceHandler {
throws Exception {
checkArgument(msg instanceof HttpResponse);
HttpResponse response = (HttpResponse) msg;
String sessionAliveValue = response.headers().get(EPP_SESSION_FIELD);
String loginValue = response.headers().get(EPP_LOGGED_IN_FIELD);
String sessionAliveValue = response.headers().get(ProxyHttpHeaders.EPP_SESSION);
String loginValue = response.headers().get(ProxyHttpHeaders.LOGGED_IN);
if (sessionAliveValue != null && sessionAliveValue.equals("close")) {
promise.addListener(ChannelFutureListener.CLOSE);
}
@@ -17,6 +17,7 @@ package google.registry.proxy;
import static com.google.common.truth.Truth.assertThat;
import static java.nio.charset.StandardCharsets.US_ASCII;
import google.registry.util.ProxyHttpHeaders;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.handler.codec.http.DefaultFullHttpRequest;
@@ -24,6 +25,7 @@ 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;
@@ -122,7 +124,7 @@ public class TestUtils {
request
.headers()
.set("authorization", "Bearer " + accessToken)
.set("content-type", "text/plain")
.set(HttpHeaderNames.CONTENT_TYPE, "text/plain")
.set("accept", "text/plain");
return request;
}
@@ -139,10 +141,10 @@ public class TestUtils {
request
.headers()
.set("authorization", "Bearer " + accessToken)
.set("content-type", "application/epp+xml")
.set(HttpHeaderNames.CONTENT_TYPE, "application/epp+xml")
.set("accept", "application/epp+xml")
.set("X-SSL-Certificate", sslClientCertificateHash)
.set("X-Forwarded-For", clientAddress);
.set(ProxyHttpHeaders.CERTIFICATE_HASH, sslClientCertificateHash)
.set(ProxyHttpHeaders.IP_ADDRESS, clientAddress);
if (cookies.length != 0) {
request.headers().set("cookie", ClientCookieEncoder.STRICT.encode(cookies));
}
@@ -166,14 +168,14 @@ public class TestUtils {
public static FullHttpResponse makeWhoisHttpResponse(String content, HttpResponseStatus status) {
FullHttpResponse response = makeHttpResponse(content, status);
response.headers().set("content-type", "text/plain");
response.headers().set(HttpHeaderNames.CONTENT_TYPE, "text/plain");
return response;
}
public static FullHttpResponse makeEppHttpResponse(
String content, HttpResponseStatus status, Cookie... cookies) {
FullHttpResponse response = makeHttpResponse(content, status);
response.headers().set("content-type", "application/epp+xml");
response.headers().set(HttpHeaderNames.CONTENT_TYPE, "application/epp+xml");
for (Cookie cookie : cookies) {
response.headers().add("set-cookie", ServerCookieEncoder.STRICT.encode(cookie));
}
@@ -32,6 +32,7 @@ import com.google.common.base.Throwables;
import google.registry.proxy.TestUtils;
import google.registry.proxy.handler.HttpsRelayServiceHandler.NonOkHttpResponseException;
import google.registry.proxy.metric.FrontendMetrics;
import google.registry.util.ProxyHttpHeaders;
import google.registry.util.SelfSignedCaCertificate;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
@@ -250,7 +251,7 @@ class EppServiceHandlerTest {
channel.writeInbound(Unpooled.wrappedBuffer(content.getBytes(UTF_8)));
FullHttpRequest request = channel.readInbound();
assertThat(request).isEqualTo(makeEppHttpRequestWithCertificate(content));
String encodedCert = request.headers().get("X-SSL-Full-Certificate");
String encodedCert = request.headers().get(ProxyHttpHeaders.FULL_CERTIFICATE);
assertThat(encodedCert).isNotEqualTo(SAMPLE_CERT);
X509Certificate decodedCert =
loadCertificate(new ByteArrayInputStream(Base64.getDecoder().decode(encodedCert)));
@@ -269,7 +270,7 @@ class EppServiceHandlerTest {
assertThat(request).isEqualTo(makeEppHttpRequestWithCertificate(content));
// Receive response indicating session is logged in
HttpResponse response = makeEppHttpResponse(content, HttpResponseStatus.OK);
response.headers().set("Logged-In", "true");
response.headers().set(ProxyHttpHeaders.LOGGED_IN, "true");
// Send another inbound message after login
channel.writeOutbound(response);
channel.writeInbound(Unpooled.wrappedBuffer(content.getBytes(UTF_8)));
@@ -297,7 +298,7 @@ class EppServiceHandlerTest {
setHandshakeSuccess();
String content = "<epp>stuff</epp>";
HttpResponse response = makeEppHttpResponse(content, HttpResponseStatus.OK);
response.headers().set("Epp-Session", "close");
response.headers().set(ProxyHttpHeaders.EPP_SESSION, "close");
channel.writeOutbound(response);
ByteBuf expectedResponse = channel.readOutbound();
assertThat(Unpooled.wrappedBuffer(content.getBytes(UTF_8))).isEqualTo(expectedResponse);
@@ -384,7 +385,7 @@ class EppServiceHandlerTest {
// Second response written.
HttpResponse response =
makeEppHttpResponse(responseContent2, HttpResponseStatus.OK, cookie3, newCookie2);
response.headers().set("Logged-In", "true");
response.headers().set(ProxyHttpHeaders.LOGGED_IN, "true");
channel.writeOutbound(response);
channel.readOutbound();
String requestContent2 = "<epp>request2</epp>";
@@ -0,0 +1,48 @@
// Copyright 2021 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.common.net.HttpHeaders;
/** Utility class of HTTP header names used for HTTP calls between Nomulus and the proxy. */
public final class ProxyHttpHeaders {
/**
* HTTP header name used to pass a full SSL certificate from the proxy to Nomulus.
*
* <p>This header contains the SSL certificate encoded to a string. It is used to pass the client
* certificate used for login to Nomulus for validation.
*/
public static final String FULL_CERTIFICATE = "X-SSL-Full-Certificate";
/** HTTP header name used to pass the certificate hash from the proxy to Nomulus. */
public static final String CERTIFICATE_HASH = "X-SSL-Certificate";
/**
* HTTP header name passed from Nomulus to proxy to indicate that a client has successfully logged
* in.
*/
public static final String LOGGED_IN = "Logged-In";
/**
* HTTP header name passed from Nomulus to proxy to indicate that an EPP session should be closed.
*/
public static final String EPP_SESSION = "Epp-Session";
/** HTTP header name used to pass the client IP address from the proxy to Nomulus. */
public static final String IP_ADDRESS = HttpHeaders.X_FORWARDED_FOR;
private ProxyHttpHeaders() {}
}