google-nomulus/java/google/registry/flows/domain/DomainFlowUtils.java
cgoldfeder b0bcc1bb3d Create *InfoData objects instead of reusing *Resource objects
This is probably best from a code-cleanliness perspective anyways,
but the rationale is that tightly coupling the resources to the
info responses was a straightjacket that required all status
values and fields to be directly available on the resource. With
this change, I already was able to get rid of the preMarshal()
hackery, and I will be able to get rid of cloneWithLinkedStatus()
and most of the contents of cloneProjectedAtTime() for non-domains.

-------------
Created by MOE: https://github.com/google/moe
MOE_MIGRATED_REVID=144252924
2017-01-12 14:11:51 -05:00

1319 lines
55 KiB
Java

// Copyright 2016 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.flows.domain;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Preconditions.checkState;
import static com.google.common.base.Predicates.equalTo;
import static com.google.common.collect.Iterables.any;
import static com.google.common.collect.Sets.difference;
import static com.google.common.collect.Sets.union;
import static google.registry.flows.domain.DomainPricingLogic.getMatchingLrpToken;
import static google.registry.model.EppResourceUtils.loadByForeignKey;
import static google.registry.model.ofy.ObjectifyService.ofy;
import static google.registry.model.registry.Registries.findTldForName;
import static google.registry.model.registry.label.ReservedList.getReservation;
import static google.registry.pricing.PricingEngineProxy.isDomainPremium;
import static google.registry.tldconfig.idn.IdnLabelValidator.findValidIdnTableForTld;
import static google.registry.util.CollectionUtils.nullToEmpty;
import static google.registry.util.DateTimeUtils.END_OF_TIME;
import static google.registry.util.DateTimeUtils.isAtOrAfter;
import static google.registry.util.DomainNameUtils.ACE_PREFIX;
import com.google.common.base.CharMatcher;
import com.google.common.base.Joiner;
import com.google.common.base.Optional;
import com.google.common.base.Splitter;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Sets;
import com.google.common.net.InternetDomainName;
import com.googlecode.objectify.Key;
import com.googlecode.objectify.Work;
import google.registry.flows.EppException;
import google.registry.flows.EppException.AuthorizationErrorException;
import google.registry.flows.EppException.CommandUseErrorException;
import google.registry.flows.EppException.InvalidAuthorizationInformationErrorException;
import google.registry.flows.EppException.ObjectDoesNotExistException;
import google.registry.flows.EppException.ParameterValuePolicyErrorException;
import google.registry.flows.EppException.ParameterValueRangeErrorException;
import google.registry.flows.EppException.ParameterValueSyntaxErrorException;
import google.registry.flows.EppException.RequiredParameterMissingException;
import google.registry.flows.EppException.StatusProhibitsOperationException;
import google.registry.flows.EppException.UnimplementedOptionException;
import google.registry.flows.exceptions.ResourceAlreadyExistsException;
import google.registry.flows.exceptions.ResourceHasClientUpdateProhibitedException;
import google.registry.model.EppResource;
import google.registry.model.billing.BillingEvent;
import google.registry.model.billing.BillingEvent.Flag;
import google.registry.model.billing.BillingEvent.Reason;
import google.registry.model.contact.ContactResource;
import google.registry.model.domain.DesignatedContact;
import google.registry.model.domain.DesignatedContact.Type;
import google.registry.model.domain.DomainApplication;
import google.registry.model.domain.DomainBase;
import google.registry.model.domain.DomainCommand.Create;
import google.registry.model.domain.DomainCommand.CreateOrUpdate;
import google.registry.model.domain.DomainCommand.InvalidReferencesException;
import google.registry.model.domain.DomainCommand.Update;
import google.registry.model.domain.DomainResource;
import google.registry.model.domain.ForeignKeyedDesignatedContact;
import google.registry.model.domain.LrpTokenEntity;
import google.registry.model.domain.Period;
import google.registry.model.domain.fee.Credit;
import google.registry.model.domain.fee.Fee;
import google.registry.model.domain.fee.FeeQueryCommandExtensionItem;
import google.registry.model.domain.fee.FeeQueryResponseExtensionItem;
import google.registry.model.domain.fee.FeeTransformCommandExtension;
import google.registry.model.domain.fee.FeeTransformResponseExtension;
import google.registry.model.domain.launch.LaunchCreateExtension;
import google.registry.model.domain.launch.LaunchExtension;
import google.registry.model.domain.launch.LaunchNotice;
import google.registry.model.domain.launch.LaunchNotice.InvalidChecksumException;
import google.registry.model.domain.launch.LaunchPhase;
import google.registry.model.domain.rgp.GracePeriodStatus;
import google.registry.model.domain.secdns.DelegationSignerData;
import google.registry.model.domain.secdns.SecDnsCreateExtension;
import google.registry.model.domain.secdns.SecDnsInfoExtension;
import google.registry.model.domain.secdns.SecDnsUpdateExtension;
import google.registry.model.domain.secdns.SecDnsUpdateExtension.Add;
import google.registry.model.domain.secdns.SecDnsUpdateExtension.Remove;
import google.registry.model.eppcommon.StatusValue;
import google.registry.model.eppoutput.EppResponse.ResponseExtension;
import google.registry.model.host.HostResource;
import google.registry.model.poll.PendingActionNotificationResponse.DomainPendingActionNotificationResponse;
import google.registry.model.poll.PollMessage;
import google.registry.model.registrar.Registrar;
import google.registry.model.registry.Registry;
import google.registry.model.registry.Registry.TldState;
import google.registry.model.registry.label.ReservationType;
import google.registry.model.reporting.HistoryEntry;
import google.registry.model.tmch.ClaimsListShard;
import google.registry.model.transfer.TransferData;
import google.registry.model.transfer.TransferResponse.DomainTransferResponse;
import google.registry.util.Idn;
import java.math.BigDecimal;
import java.util.HashSet;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import javax.annotation.Nullable;
import org.joda.money.CurrencyUnit;
import org.joda.money.Money;
import org.joda.time.DateTime;
/** Static utility functions for domain flows. */
public class DomainFlowUtils {
/** Map from launch phases to the equivalent tld states. */
private static final ImmutableMap<LaunchPhase, TldState> LAUNCH_PHASE_TO_TLD_STATE =
ImmutableMap.of(
LaunchPhase.SUNRISE, TldState.SUNRISE,
LaunchPhase.SUNRUSH, TldState.SUNRUSH,
LaunchPhase.LANDRUSH, TldState.LANDRUSH,
LaunchPhase.CLAIMS, TldState.GENERAL_AVAILABILITY,
LaunchPhase.OPEN, TldState.GENERAL_AVAILABILITY);
/** Reservation types that are allowed in sunrise by policy. */
public static final ImmutableSet<ReservationType> TYPES_ALLOWED_FOR_CREATE_ONLY_IN_SUNRISE =
Sets.immutableEnumSet(
ReservationType.ALLOWED_IN_SUNRISE,
ReservationType.NAME_COLLISION,
ReservationType.MISTAKEN_PREMIUM);
/** Non-sunrise tld states. */
private static final ImmutableSet<TldState> DISALLOWED_TLD_STATES_FOR_LAUNCH_FLOWS =
Sets.immutableEnumSet(
TldState.PREDELEGATION,
TldState.QUIET_PERIOD,
TldState.GENERAL_AVAILABILITY);
/** Strict validator for ascii lowercase letters, digits, and "-", allowing "." as a separator */
private static final CharMatcher ALLOWED_CHARS =
CharMatcher.inRange('a', 'z').or(CharMatcher.inRange('0', '9').or(CharMatcher.anyOf("-.")));
/** The maximum number of DS records allowed on a domain. */
private static final int MAX_DS_RECORDS_PER_DOMAIN = 8;
/** Maximum number of nameservers allowed per domain. */
private static final int MAX_NAMESERVERS_PER_DOMAIN = 13;
/** Maximum number of characters in a domain label, from RFC 2181. */
private static final int MAX_LABEL_SIZE = 63;
/**
* Returns parsed version of {@code name} if domain name label follows our naming rules and is
* under one of the given allowed TLDs.
*
* <p><b>Note:</b> This method does not perform language validation with IDN tables.
*
* @see #validateDomainNameWithIdnTables(InternetDomainName)
*/
static InternetDomainName validateDomainName(String name)
throws EppException {
if (!ALLOWED_CHARS.matchesAllOf(name)) {
throw new BadDomainNameCharacterException();
}
List<String> parts = Splitter.on('.').splitToList(name);
if (parts.size() <= 1) {
throw new BadDomainNamePartsCountException();
}
if (any(parts, equalTo(""))) {
throw new EmptyDomainNamePartException();
}
validateFirstLabel(parts.get(0));
InternetDomainName domainName = InternetDomainName.from(name);
Optional<InternetDomainName> tldParsed = findTldForName(domainName);
if (!tldParsed.isPresent()) {
throw new TldDoesNotExistException(domainName.parent().toString());
}
if (domainName.parts().size() != tldParsed.get().parts().size() + 1) {
throw new BadDomainNamePartsCountException();
}
return domainName;
}
private static void validateFirstLabel(String firstLabel) throws EppException {
if (firstLabel.length() > MAX_LABEL_SIZE) {
throw new DomainLabelTooLongException();
}
if (firstLabel.startsWith("-")) {
throw new LeadingDashException();
}
if (firstLabel.endsWith("-")) {
throw new TrailingDashException();
}
String unicode = Idn.toUnicode(firstLabel);
if (firstLabel.startsWith(ACE_PREFIX) && firstLabel.equals(unicode)) {
throw new InvalidPunycodeException();
}
if (!firstLabel.startsWith(ACE_PREFIX)
&& firstLabel.length() >= 4
&& firstLabel.substring(2).startsWith("--")) {
throw new DashesInThirdAndFourthException();
}
}
/**
* Returns name of first matching IDN table for domain label.
*
* @throws InvalidIdnDomainLabelException if IDN table or language validation failed
* @see #validateDomainName(String)
*/
static String validateDomainNameWithIdnTables(InternetDomainName domainName)
throws InvalidIdnDomainLabelException {
Optional<String> idnTableName =
findValidIdnTableForTld(domainName.parts().get(0), domainName.parent().toString());
if (!idnTableName.isPresent()) {
throw new InvalidIdnDomainLabelException();
}
return idnTableName.get();
}
/** Check if the registrar running the flow has access to the TLD in question. */
public static void checkAllowedAccessToTld(String clientId, String tld)
throws EppException {
if (!Registrar.loadByClientId(clientId).getAllowedTlds().contains(tld)) {
throw new DomainFlowUtils.NotAuthorizedForTldException(tld);
}
}
/** Check that the DS data that will be set on a domain is valid. */
static void validateDsData(Set<DelegationSignerData> dsData) throws EppException {
if (dsData != null && dsData.size() > MAX_DS_RECORDS_PER_DOMAIN) {
throw new TooManyDsRecordsException(String.format(
"A maximum of %s DS records are allowed per domain.", MAX_DS_RECORDS_PER_DOMAIN));
}
}
/** We only allow specifying years in a period. */
static Period verifyUnitIsYears(Period period) throws EppException {
if (!checkNotNull(period).getUnit().equals(Period.Unit.YEARS)) {
throw new BadPeriodUnitException();
}
return period;
}
/** Verify that no linked resources have disallowed statuses. */
static void verifyNotInPendingDelete(
Set<DesignatedContact> contacts,
Key<ContactResource> registrant,
Set<Key<HostResource>> nameservers) throws EppException {
for (DesignatedContact contact : nullToEmpty(contacts)) {
verifyNotInPendingDelete(contact.getContactKey());
}
if (registrant != null) {
verifyNotInPendingDelete(registrant);
}
for (Key<HostResource> host : nullToEmpty(nameservers)) {
verifyNotInPendingDelete(host);
}
}
private static void verifyNotInPendingDelete(
Key<? extends EppResource> resourceKey) throws EppException {
EppResource resource = ofy().load().key(resourceKey).now();
if (resource.getStatusValues().contains(StatusValue.PENDING_DELETE)) {
throw new LinkedResourceInPendingDeleteProhibitsOperationException(resource.getForeignKey());
}
}
static void validateContactsHaveTypes(Set<DesignatedContact> contacts)
throws ParameterValuePolicyErrorException {
for (DesignatedContact contact : nullToEmpty(contacts)) {
if (contact.getType() == null) {
throw new MissingContactTypeException();
}
}
}
static void validateNameserversCountForTld(String tld, int count) throws EppException {
ImmutableSet<String> whitelist = Registry.get(tld).getAllowedFullyQualifiedHostNames();
// For TLDs with a nameserver whitelist, all domains must have at least 1 nameserver.
if (!whitelist.isEmpty() && count == 0) {
throw new NameserversNotSpecifiedException();
}
if (count > MAX_NAMESERVERS_PER_DOMAIN) {
throw new TooManyNameserversException(String.format(
"Only %d nameservers are allowed per domain", MAX_NAMESERVERS_PER_DOMAIN));
}
}
static void validateNoDuplicateContacts(Set<DesignatedContact> contacts)
throws ParameterValuePolicyErrorException {
Set<Type> roles = new HashSet<>();
for (DesignatedContact contact : nullToEmpty(contacts)) {
if (!roles.add(contact.getType())) {
throw new DuplicateContactForRoleException();
}
}
}
static void validateRequiredContactsPresent(
Key<ContactResource> registrant, Set<DesignatedContact> contacts)
throws RequiredParameterMissingException {
if (registrant == null) {
throw new MissingRegistrantException();
}
Set<Type> roles = new HashSet<>();
for (DesignatedContact contact : nullToEmpty(contacts)) {
roles.add(contact.getType());
}
if (!roles.contains(Type.ADMIN)) {
throw new MissingAdminContactException();
}
if (!roles.contains(Type.TECH)) {
throw new MissingTechnicalContactException();
}
}
static void validateRegistrantAllowedOnTld(String tld, String registrantContactId)
throws RegistrantNotAllowedException {
ImmutableSet<String> whitelist = Registry.get(tld).getAllowedRegistrantContactIds();
// Empty whitelist or null registrantContactId are ignored.
if (registrantContactId != null && !whitelist.isEmpty()
&& !whitelist.contains(registrantContactId)) {
throw new RegistrantNotAllowedException(registrantContactId);
}
}
static void validateNameserversAllowedOnTld(String tld, Set<String> fullyQualifiedHostNames)
throws EppException {
ImmutableSet<String> whitelist = Registry.get(tld).getAllowedFullyQualifiedHostNames();
Set<String> hostnames = nullToEmpty(fullyQualifiedHostNames);
if (!whitelist.isEmpty()) { // Empty whitelist is ignored.
Set<String> disallowedNameservers = difference(hostnames, whitelist);
if (!disallowedNameservers.isEmpty()) {
throw new NameserversNotAllowedException(disallowedNameservers);
}
}
}
static void verifyNotReserved(InternetDomainName domainName, boolean isSunrise)
throws EppException {
if (isReserved(domainName, isSunrise)) {
throw new DomainReservedException(domainName.toString());
}
}
private static boolean isReserved(InternetDomainName domainName, boolean isSunrise) {
ReservationType type = getReservationType(domainName);
return type == ReservationType.FULLY_BLOCKED
|| type == ReservationType.RESERVED_FOR_ANCHOR_TENANT
|| (TYPES_ALLOWED_FOR_CREATE_ONLY_IN_SUNRISE.contains(type) && !isSunrise);
}
/** Returns an enum that encodes how and when this name is reserved in the current tld. */
static ReservationType getReservationType(InternetDomainName domainName) {
// The TLD should always be the parent of the requested domain name.
return getReservation(domainName.parts().get(0), domainName.parent().toString());
}
/** Verifies that a launch extension's specified phase matches the specified registry's phase. */
static void verifyLaunchPhaseMatchesRegistryPhase(
Registry registry, LaunchExtension launchExtension, DateTime now) throws EppException {
if (!Objects.equals(
registry.getTldState(now),
LAUNCH_PHASE_TO_TLD_STATE.get(launchExtension.getPhase()))) {
// No launch operations are allowed during the quiet period or predelegation.
throw new LaunchPhaseMismatchException();
}
}
/** Verifies that an application's domain name matches the target id (from a command). */
static void verifyApplicationDomainMatchesTargetId(
DomainApplication application, String targetId) throws EppException {
if (!application.getFullyQualifiedDomainName().equals(targetId)) {
throw new ApplicationDomainNameMismatchException();
}
}
/**
* Verifies that a domain name is allowed to be delegated to the given client id. The only case
* where it would not be allowed is if domain name is premium, and premium names are blocked by
* this registrar.
*/
static void verifyPremiumNameIsNotBlocked(
String domainName, DateTime priceTime, String clientId) throws EppException {
if (isDomainPremium(domainName, priceTime)) {
// NB: The load of the Registar object is transactionless, which means that it should hit
// memcache most of the time.
if (Registrar.loadByClientId(clientId).getBlockPremiumNames()) {
throw new PremiumNameBlockedException();
}
}
}
/**
* Helper to call {@link CreateOrUpdate#cloneAndLinkReferences} and convert exceptions to
* EppExceptions, since this is needed in several places.
*/
static <T extends CreateOrUpdate<T>> T cloneAndLinkReferences(T command, DateTime now)
throws EppException {
try {
return command.cloneAndLinkReferences(now);
} catch (InvalidReferencesException e) {
throw new LinkedResourcesDoNotExistException(e.getType(), e.getForeignKeys());
}
}
/**
* Fills in a builder with the data needed for an autorenew billing event for this domain. This
* does not copy over the id of the current autorenew billing event.
*/
static BillingEvent.Recurring.Builder newAutorenewBillingEvent(DomainResource domain) {
return new BillingEvent.Recurring.Builder()
.setReason(Reason.RENEW)
.setFlags(ImmutableSet.of(Flag.AUTO_RENEW))
.setTargetId(domain.getFullyQualifiedDomainName())
.setClientId(domain.getCurrentSponsorClientId())
.setEventTime(domain.getRegistrationExpirationTime());
}
/**
* Fills in a builder with the data needed for an autorenew poll message for this domain. This
* does not copy over the id of the current autorenew poll message.
*/
static PollMessage.Autorenew.Builder newAutorenewPollMessage(DomainResource domain) {
return new PollMessage.Autorenew.Builder()
.setTargetId(domain.getFullyQualifiedDomainName())
.setClientId(domain.getCurrentSponsorClientId())
.setEventTime(domain.getRegistrationExpirationTime())
.setMsg("Domain was auto-renewed.");
}
/**
* Re-saves the current autorenew billing event and poll message with a new end time. This may end
* up deleting the poll message (if closing the message interval) or recreating it (if opening the
* message interval).
*/
@SuppressWarnings("unchecked")
static void updateAutorenewRecurrenceEndTime(DomainResource domain, DateTime newEndTime) {
Optional<PollMessage.Autorenew> autorenewPollMessage =
Optional.fromNullable(ofy().load().key(domain.getAutorenewPollMessage()).now());
// Construct an updated autorenew poll message. If the autorenew poll message no longer exists,
// create a new one at the same id. This can happen if a transfer was requested on a domain
// where all autorenew poll messages had already been delivered (this would cause the poll
// message to be deleted), and then subsequently the transfer was canceled, rejected, or deleted
// (which would cause the poll message to be recreated here).
Key<PollMessage.Autorenew> existingAutorenewKey = domain.getAutorenewPollMessage();
PollMessage.Autorenew updatedAutorenewPollMessage = autorenewPollMessage.isPresent()
? autorenewPollMessage.get().asBuilder().setAutorenewEndTime(newEndTime).build()
: newAutorenewPollMessage(domain)
.setId(existingAutorenewKey.getId())
.setAutorenewEndTime(newEndTime)
.setParentKey(existingAutorenewKey.<HistoryEntry>getParent())
.build();
// If the resultant autorenew poll message would have no poll messages to deliver, then just
// delete it. Otherwise save it with the new end time.
if (isAtOrAfter(updatedAutorenewPollMessage.getEventTime(), newEndTime)) {
if (autorenewPollMessage.isPresent()) {
ofy().delete().entity(autorenewPollMessage.get());
}
} else {
ofy().save().entity(updatedAutorenewPollMessage);
}
ofy().save().entity(ofy().load().key(domain.getAutorenewBillingEvent()).now().asBuilder()
.setRecurrenceEndTime(newEndTime)
.build());
}
/**
* Validates a {@link FeeQueryCommandExtensionItem} and sets the appropriate fields on a {@link
* FeeQueryResponseExtensionItem} builder.
*/
static void handleFeeRequest(
FeeQueryCommandExtensionItem feeRequest,
FeeQueryResponseExtensionItem.Builder<?, ?> builder,
InternetDomainName domain,
@Nullable CurrencyUnit topLevelCurrency,
DateTime currentDate,
DomainPricingLogic pricingLogic)
throws EppException {
DateTime now = currentDate;
// Use the custom effective date specified in the fee check request, if there is one.
if (feeRequest.getEffectiveDate().isPresent()) {
now = feeRequest.getEffectiveDate().get();
builder.setEffectiveDateIfSupported(now);
}
String domainNameString = domain.toString();
Registry registry = Registry.get(domain.parent().toString());
int years = verifyUnitIsYears(feeRequest.getPeriod()).getValue();
boolean isSunrise = registry.getTldState(now).equals(TldState.SUNRISE);
if (feeRequest.getPhase() != null || feeRequest.getSubphase() != null) {
throw new FeeChecksDontSupportPhasesException();
}
CurrencyUnit currency =
feeRequest.getCurrency() != null ? feeRequest.getCurrency() : topLevelCurrency;
if ((currency != null) && !currency.equals(registry.getCurrency())) {
throw new CurrencyUnitMismatchException();
}
builder
.setCommand(feeRequest.getCommandName(), feeRequest.getPhase(), feeRequest.getSubphase())
.setCurrencyIfSupported(registry.getCurrency())
.setPeriod(feeRequest.getPeriod())
.setClass(pricingLogic.getFeeClass(domainNameString, now).orNull());
ImmutableList<Fee> fees = ImmutableList.of();
switch (feeRequest.getCommandName()) {
case CREATE:
if (isReserved(domain, isSunrise)) { // Don't return a create price for reserved names.
builder.setClass("reserved"); // Override whatever class we've set above.
builder.setAvailIfSupported(false);
builder.setReasonIfSupported("reserved");
} else {
builder.setAvailIfSupported(true);
fees = pricingLogic.getCreatePrice(registry, domainNameString, now, years).getFees();
}
break;
case RENEW:
builder.setAvailIfSupported(true);
fees = pricingLogic.getRenewPrice(registry, domainNameString, now, years).getFees();
break;
case RESTORE:
if (years != 1) {
throw new RestoresAreAlwaysForOneYearException();
}
builder.setAvailIfSupported(true);
fees = pricingLogic.getRestorePrice(registry, domainNameString, now).getFees();
break;
case TRANSFER:
builder.setAvailIfSupported(true);
fees = pricingLogic.getTransferPrice(registry, domainNameString, now, years).getFees();
break;
case UPDATE:
builder.setAvailIfSupported(true);
fees = pricingLogic.getUpdatePrice(registry, domainNameString, now).getFees();
break;
default:
throw new UnknownFeeCommandException(feeRequest.getUnparsedCommandName());
}
// Set the fees, and based on the validDateRange of the fees, set the notAfterDate.
if (!fees.isEmpty()) {
builder.setFees(fees);
DateTime notAfterDate = null;
for (Fee fee : fees) {
if (fee.hasValidDateRange()) {
DateTime endDate = fee.getValidDateRange().upperEndpoint();
if (notAfterDate == null || notAfterDate.isAfter(endDate)) {
notAfterDate = endDate;
}
}
}
if (notAfterDate != null && !notAfterDate.equals(END_OF_TIME)) {
builder.setNotAfterDateIfSupported(notAfterDate);
}
}
}
public static void validateFeeChallenge(
String domainName,
String tld,
DateTime priceTime,
final FeeTransformCommandExtension feeCommand,
FeesAndCredits feesAndCredits)
throws EppException {
Registry registry = Registry.get(tld);
if (registry.getPremiumPriceAckRequired()
&& isDomainPremium(domainName, priceTime)
&& feeCommand == null) {
throw new FeesRequiredForPremiumNameException();
}
// Check for the case where a fee command extension was required but not provided.
// This only happens when the total fees are non-zero and include custom fees requiring the
// extension.
if (feeCommand == null) {
if (feesAndCredits.getTotalCost().isZero() || !feesAndCredits.isFeeExtensionRequired()) {
return;
}
throw new FeesRequiredForNonFreeOperationException(feesAndCredits.getTotalCost());
}
List<Fee> fees = feeCommand.getFees();
// The schema guarantees that at least one fee will be present.
checkState(!fees.isEmpty());
BigDecimal total = BigDecimal.ZERO;
for (Fee fee : fees) {
if (!fee.hasDefaultAttributes()) {
throw new UnsupportedFeeAttributeException();
}
total = total.add(fee.getCost());
}
for (Credit credit : feeCommand.getCredits()) {
if (!credit.hasDefaultAttributes()) {
throw new UnsupportedFeeAttributeException();
}
total = total.add(credit.getCost());
}
Money feeTotal = null;
try {
feeTotal = Money.of(feeCommand.getCurrency(), total);
} catch (ArithmeticException e) {
throw new CurrencyValueScaleException();
}
if (!feeTotal.getCurrencyUnit().equals(feesAndCredits.getCurrency())) {
throw new CurrencyUnitMismatchException();
}
if (!feeTotal.equals(feesAndCredits.getTotalCost())) {
throw new FeesMismatchException(feesAndCredits.getTotalCost());
}
}
/** Create a poll message for the gaining client in a transfer. */
static PollMessage createGainingTransferPollMessage(
String targetId,
TransferData transferData,
@Nullable DateTime extendedRegistrationExpirationTime,
HistoryEntry historyEntry) {
return new PollMessage.OneTime.Builder()
.setClientId(transferData.getGainingClientId())
.setEventTime(transferData.getPendingTransferExpirationTime())
.setMsg(transferData.getTransferStatus().getMessage())
.setResponseData(ImmutableList.of(
createTransferResponse(targetId, transferData, extendedRegistrationExpirationTime),
DomainPendingActionNotificationResponse.create(
targetId,
transferData.getTransferStatus().isApproved(),
transferData.getTransferRequestTrid(),
historyEntry.getModificationTime())))
.setParent(historyEntry)
.build();
}
/** Create a poll message for the losing client in a transfer. */
static PollMessage createLosingTransferPollMessage(
String targetId,
TransferData transferData,
@Nullable DateTime extendedRegistrationExpirationTime,
HistoryEntry historyEntry) {
return new PollMessage.OneTime.Builder()
.setClientId(transferData.getLosingClientId())
.setEventTime(transferData.getPendingTransferExpirationTime())
.setMsg(transferData.getTransferStatus().getMessage())
.setResponseData(ImmutableList.of(
createTransferResponse(targetId, transferData, extendedRegistrationExpirationTime)))
.setParent(historyEntry)
.build();
}
/** Create a {@link DomainTransferResponse} off of the info in a {@link TransferData}. */
static DomainTransferResponse createTransferResponse(
String targetId,
TransferData transferData,
@Nullable DateTime extendedRegistrationExpirationTime) {
return new DomainTransferResponse.Builder()
.setFullyQualifiedDomainNameName(targetId)
.setGainingClientId(transferData.getGainingClientId())
.setLosingClientId(transferData.getLosingClientId())
.setPendingTransferExpirationTime(transferData.getPendingTransferExpirationTime())
.setTransferRequestTime(transferData.getTransferRequestTime())
.setTransferStatus(transferData.getTransferStatus())
.setExtendedRegistrationExpirationTime(extendedRegistrationExpirationTime)
.build();
}
/**
* Adds a secDns extension to a list if the given set of dsData is non-empty.
*
* <p>According to RFC 5910 section 2, we should only return this if the client specified the
* "urn:ietf:params:xml:ns:secDNS-1.1" when logging in. However, this is a "SHOULD" not a "MUST"
* and we are going to ignore it; clients who don't care about secDNS can just ignore it.
*/
static void addSecDnsExtensionIfPresent(
ImmutableList.Builder<ResponseExtension> extensions,
ImmutableSet<DelegationSignerData> dsData) {
if (!dsData.isEmpty()) {
extensions.add(SecDnsInfoExtension.create(dsData));
}
}
/** Update {@link DelegationSignerData} based on an update extension command. */
static ImmutableSet<DelegationSignerData> updateDsData(
ImmutableSet<DelegationSignerData> oldDsData, SecDnsUpdateExtension secDnsUpdate)
throws EppException {
// We don't support 'urgent' because we do everything as fast as we can anyways.
if (Boolean.TRUE.equals(secDnsUpdate.getUrgent())) { // We allow both false and null.
throw new UrgentAttributeNotSupportedException();
}
// There must be at least one of add/rem/chg, and chg isn't actually supported.
if (secDnsUpdate.getChange() != null) {
// The only thing you can change is maxSigLife, and we don't support that at all.
throw new MaxSigLifeChangeNotSupportedException();
}
Add add = secDnsUpdate.getAdd();
Remove remove = secDnsUpdate.getRemove();
if (add == null && remove == null) {
throw new EmptySecDnsUpdateException();
}
if (remove != null && Boolean.FALSE.equals(remove.getAll())) {
throw new SecDnsAllUsageException(); // Explicit all=false is meaningless.
}
Set<DelegationSignerData> toAdd = (add == null)
? ImmutableSet.<DelegationSignerData>of()
: add.getDsData();
Set<DelegationSignerData> toRemove = (remove == null)
? ImmutableSet.<DelegationSignerData>of()
: (remove.getAll() == null) ? remove.getDsData() : oldDsData;
// RFC 5910 specifies that removes are processed before adds.
return ImmutableSet.copyOf(union(difference(oldDsData, toRemove), toAdd));
}
/** If a domain or application has "clientUpdateProhibited" set, updates must clear it or fail. */
static void verifyClientUpdateNotProhibited(Update command, DomainBase existingResource)
throws ResourceHasClientUpdateProhibitedException {
if (existingResource.getStatusValues().contains(StatusValue.CLIENT_UPDATE_PROHIBITED)
&& !command.getInnerRemove().getStatusValues()
.contains(StatusValue.CLIENT_UPDATE_PROHIBITED)) {
throw new ResourceHasClientUpdateProhibitedException();
}
}
/** Check that the registry phase is not incompatible with launch extension flows. */
static void verifyRegistryStateAllowsLaunchFlows(Registry registry, DateTime now)
throws BadCommandForRegistryPhaseException {
if (DISALLOWED_TLD_STATES_FOR_LAUNCH_FLOWS.contains(registry.getTldState(now))) {
throw new BadCommandForRegistryPhaseException();
}
}
/** Check that the registry phase is not predelegation, during which some flows are forbidden. */
static void verifyNotInPredelegation(Registry registry, DateTime now)
throws BadCommandForRegistryPhaseException {
if (registry.getTldState(now) == TldState.PREDELEGATION) {
throw new BadCommandForRegistryPhaseException();
}
}
/** Validate the contacts and nameservers specified in a domain or application create command. */
static void validateCreateCommandContactsAndNameservers(Create command, String tld)
throws EppException {
verifyNotInPendingDelete(
command.getContacts(),
command.getRegistrant(),
command.getNameservers());
validateContactsHaveTypes(command.getContacts());
validateRegistrantAllowedOnTld(tld, command.getRegistrantContactId());
validateNoDuplicateContacts(command.getContacts());
validateRequiredContactsPresent(command.getRegistrant(), command.getContacts());
Set<String> fullyQualifiedHostNames =
nullToEmpty(command.getNameserverFullyQualifiedHostNames());
validateNameserversCountForTld(tld, fullyQualifiedHostNames.size());
validateNameserversAllowedOnTld(tld, fullyQualifiedHostNames);
}
/**
* Fail a domain or application create very fast if the domain is already registered.
*
* <p>Try to load the domain non-transactionally, since this can hit memcache. If we succeed, and
* the domain is not in the add grace period (the only state that allows instantaneous transition
* to being deleted), we can assume that the domain will not be deleted (and therefore won't be
* creatable) until its deletion time. For repeated failed creates this means we can avoid the
* Datastore lookup, which is very expensive (and first-seen failed creates are no worse than they
* otherwise would be). This comes at the cost of the extra lookup for successful creates (or
* rather, those that don't fail due to the domain existing) and also for failed creates within
* the existing domain's add grace period.
*/
static void failfastForCreate(final String targetId, final DateTime now) throws EppException {
// Enter a transactionless context briefly.
DomainResource domain = ofy().doTransactionless(new Work<DomainResource>() {
@Override
public DomainResource run() {
// This is cacheable because we are outside of a transaction.
return loadByForeignKey(DomainResource.class, targetId, now);
}});
// If the domain exists already and isn't in the add grace period then there is no way it will
// be suddenly deleted and therefore the create must fail.
if (domain != null && !domain.getGracePeriodStatuses().contains(GracePeriodStatus.ADD)) {
throw new ResourceAlreadyExistsException(targetId, true);
}
}
/** Validate the secDNS extension, if present. */
static SecDnsCreateExtension validateSecDnsExtension(SecDnsCreateExtension secDnsCreate)
throws EppException {
if (secDnsCreate == null) {
return null;
}
if (secDnsCreate.getDsData() == null) {
throw new DsDataRequiredException();
}
if (secDnsCreate.getMaxSigLife() != null) {
throw new MaxSigLifeNotSupportedException();
}
validateDsData(secDnsCreate.getDsData());
return secDnsCreate;
}
/** Validate the notice from a launch create extension, allowing null as a valid notice. */
static void validateLaunchCreateNotice(
@Nullable LaunchNotice notice,
String domainLabel,
boolean isSuperuser,
DateTime now) throws EppException {
if (notice == null) {
return;
}
if (!notice.getNoticeId().getValidatorId().equals("tmch")) {
throw new InvalidTrademarkValidatorException();
}
// Superuser can force domain creations regardless of the current date.
if (!isSuperuser) {
if (notice.getExpirationTime().isBefore(now)) {
throw new ExpiredClaimException();
}
// An acceptance within the past 48 hours is mandated by the TMCH Functional Spec.
if (notice.getAcceptedTime().isBefore(now.minusHours(48))) {
throw new AcceptedTooLongAgoException();
}
}
try {
notice.validate(domainLabel);
} catch (IllegalArgumentException e) {
throw new MalformedTcnIdException();
} catch (InvalidChecksumException e) {
throw new InvalidTcnIdChecksumException();
}
}
/** Check that the claims period hasn't ended. */
static void verifyClaimsPeriodNotEnded(Registry registry, DateTime now)
throws ClaimsPeriodEndedException {
if (isAtOrAfter(now, registry.getClaimsPeriodEnd())) {
throw new ClaimsPeriodEndedException(registry.getTldStr());
}
}
/**
* Check that if there's a claims notice it's on the claims list, and that if there's not one it's
* not on the claims list and is a sunrise application.
*/
static void verifyClaimsNoticeIfAndOnlyIfNeeded(
InternetDomainName domainName,
boolean hasSignedMarks,
boolean hasClaimsNotice) throws EppException {
boolean isInClaimsList = ClaimsListShard.get().getClaimKey(domainName.parts().get(0)) != null;
if (hasClaimsNotice && !isInClaimsList) {
throw new UnexpectedClaimsNoticeException(domainName.toString());
}
if (!hasClaimsNotice && isInClaimsList && !hasSignedMarks) {
throw new MissingClaimsNoticeException(domainName.toString());
}
}
/** Create a {@link LrpTokenEntity} object that records this LRP registration. */
static LrpTokenEntity prepareMarkedLrpTokenEntity(
String lrpTokenString, InternetDomainName domainName, HistoryEntry historyEntry)
throws InvalidLrpTokenException {
Optional<LrpTokenEntity> lrpToken = getMatchingLrpToken(lrpTokenString, domainName);
if (!lrpToken.isPresent()) {
throw new InvalidLrpTokenException();
}
return lrpToken.get().asBuilder()
.setRedemptionHistoryEntry(Key.create(historyEntry))
.build();
}
/** Check that there are no code marks, which is a type of mark we don't support. */
static void verifyNoCodeMarks(LaunchCreateExtension launchCreate)
throws UnsupportedMarkTypeException {
if (launchCreate.hasCodeMarks()) {
throw new UnsupportedMarkTypeException();
}
}
/** Create a response extension listign the fees on a domain or application create. */
static FeeTransformResponseExtension createFeeCreateResponse(
FeeTransformCommandExtension feeCreate, FeesAndCredits feesAndCredits) {
return feeCreate
.createResponseBuilder()
.setCurrency(feesAndCredits.getCurrency())
.setFees(feesAndCredits.getFees())
.setCredits(feesAndCredits.getCredits())
.build();
}
/** Bulk-load all referenced resources on a domain so they are in the session cache. */
static void prefetchReferencedResources(DomainBase domain) {
// Calling values() on the result blocks until loading is done.
ofy().load().values(union(domain.getNameservers(), domain.getReferencedContacts())).values();
}
static ImmutableSet<ForeignKeyedDesignatedContact> loadForeignKeyedDesignatedContacts(
ImmutableSet<DesignatedContact> contacts) {
ImmutableSet.Builder<ForeignKeyedDesignatedContact> builder = new ImmutableSet.Builder<>();
for (DesignatedContact contact : contacts) {
builder.add(ForeignKeyedDesignatedContact.create(
contact.getType(),
ofy().load().key(contact.getContactKey()).now().getContactId()));
}
return builder.build();
}
/** Resource linked to this domain does not exist. */
static class LinkedResourcesDoNotExistException extends ObjectDoesNotExistException {
public LinkedResourcesDoNotExistException(Class<?> type, ImmutableSet<String> resourceIds) {
super(type, resourceIds);
}
}
/** Linked resource in pending delete prohibits operation. */
static class LinkedResourceInPendingDeleteProhibitsOperationException
extends StatusProhibitsOperationException {
public LinkedResourceInPendingDeleteProhibitsOperationException(String resourceId) {
super(String.format(
"Linked resource in pending delete prohibits operation: %s",
resourceId));
}
}
/** Domain names can only contain a-z, 0-9, '.' and '-'. */
static class BadDomainNameCharacterException extends ParameterValuePolicyErrorException {
public BadDomainNameCharacterException() {
super("Domain names can only contain a-z, 0-9, '.' and '-'");
}
}
/** Non-IDN domain names cannot contain hyphens in the third or fourth position. */
static class DashesInThirdAndFourthException extends ParameterValuePolicyErrorException {
public DashesInThirdAndFourthException() {
super("Non-IDN domain names cannot contain dashes in the third or fourth position");
}
}
/** Domain labels cannot begin with a dash. */
static class LeadingDashException extends ParameterValuePolicyErrorException {
public LeadingDashException() {
super("Domain labels cannot begin with a dash");
}
}
/** Domain labels cannot end with a dash. */
static class TrailingDashException extends ParameterValuePolicyErrorException {
public TrailingDashException() {
super("Domain labels cannot end with a dash");
}
}
/** Domain labels cannot be longer than 63 characters. */
static class DomainLabelTooLongException extends ParameterValuePolicyErrorException {
public DomainLabelTooLongException() {
super("Domain labels cannot be longer than 63 characters");
}
}
/** No part of a domain name can be empty. */
static class EmptyDomainNamePartException extends ParameterValuePolicyErrorException {
public EmptyDomainNamePartException() {
super("No part of a domain name can be empty");
}
}
/** Domain name starts with xn-- but is not a valid IDN. */
static class InvalidPunycodeException extends ParameterValuePolicyErrorException {
public InvalidPunycodeException() {
super("Domain name starts with xn-- but is not a valid IDN");
}
}
/** Periods for domain registrations must be specified in years. */
static class BadPeriodUnitException extends ParameterValuePolicyErrorException {
public BadPeriodUnitException() {
super("Periods for domain registrations must be specified in years");
}
}
/** Missing type attribute for contact. */
static class MissingContactTypeException extends ParameterValuePolicyErrorException {
public MissingContactTypeException() {
super("Missing type attribute for contact");
}
}
/** More than one contact for a given role is not allowed. */
static class DuplicateContactForRoleException extends ParameterValuePolicyErrorException {
public DuplicateContactForRoleException() {
super("More than one contact for a given role is not allowed");
}
}
/** Declared launch extension phase does not match the current registry phase. */
static class LaunchPhaseMismatchException extends ParameterValuePolicyErrorException {
public LaunchPhaseMismatchException() {
super("Declared launch extension phase does not match the current registry phase");
}
}
/** Application referenced does not match specified domain name. */
static class ApplicationDomainNameMismatchException extends ParameterValuePolicyErrorException {
public ApplicationDomainNameMismatchException() {
super("Application referenced does not match specified domain name");
}
}
/** Too many DS records set on a domain. */
static class TooManyDsRecordsException extends ParameterValuePolicyErrorException {
public TooManyDsRecordsException(String message) {
super(message);
}
}
/** Domain name is under tld which doesn't exist. */
static class TldDoesNotExistException extends ParameterValueRangeErrorException {
public TldDoesNotExistException(String tld) {
super(String.format("Domain name is under tld %s which doesn't exist", tld));
}
}
/** Domain label is not allowed by IDN table. */
static class InvalidIdnDomainLabelException extends ParameterValueRangeErrorException {
public InvalidIdnDomainLabelException() {
super("Domain label is not allowed by IDN table");
}
}
/** Registrant is required. */
static class MissingRegistrantException extends RequiredParameterMissingException {
public MissingRegistrantException() {
super("Registrant is required");
}
}
/** Admin contact is required. */
static class MissingAdminContactException extends RequiredParameterMissingException {
public MissingAdminContactException() {
super("Admin contact is required");
}
}
/** Technical contact is required. */
static class MissingTechnicalContactException extends RequiredParameterMissingException {
public MissingTechnicalContactException() {
super("Technical contact is required");
}
}
/** Too many nameservers set on this domain. */
static class TooManyNameserversException extends ParameterValuePolicyErrorException {
public TooManyNameserversException(String message) {
super(message);
}
}
/** Domain name must have exactly one part above the tld. */
static class BadDomainNamePartsCountException extends ParameterValueSyntaxErrorException {
public BadDomainNamePartsCountException() {
super("Domain name must have exactly one part above the tld");
}
}
/** Unknown fee command name. */
static class UnknownFeeCommandException extends ParameterValuePolicyErrorException {
UnknownFeeCommandException(String commandName) {
super("Unknown fee command: " + commandName);
}
}
/** Fee checks for command phases and subphases are not supported. */
static class FeeChecksDontSupportPhasesException extends ParameterValuePolicyErrorException {
FeeChecksDontSupportPhasesException() {
super("Fee checks for command phases and subphases are not supported");
}
}
/** The requested fees cannot be provided in the requested currency. */
static class CurrencyUnitMismatchException extends ParameterValuePolicyErrorException {
CurrencyUnitMismatchException() {
super("The requested fees cannot be provided in the requested currency");
}
}
/** The requested fee is expressed in a scale that is invalid for the given currency. */
static class CurrencyValueScaleException extends ParameterValueSyntaxErrorException {
CurrencyValueScaleException() {
super("The requested fee is expressed in a scale that is invalid for the given currency");
}
}
/** Fees must be explicitly acknowledged when performing any operations on a premium name. */
static class FeesRequiredForPremiumNameException extends RequiredParameterMissingException {
FeesRequiredForPremiumNameException() {
super("Fees must be explicitly acknowledged when performing any operations on a premium"
+ " name");
}
}
/** Fees must be explicitly acknowledged when performing an operation which is not free. */
static class FeesRequiredForNonFreeOperationException extends RequiredParameterMissingException {
FeesRequiredForNonFreeOperationException() {
super("Fees must be explicitly acknowledged when performing an operation which is not free.");
}
public FeesRequiredForNonFreeOperationException(Money expectedFee) {
super(
"Fees must be explicitly acknowledged when performing an operation which is not free."
+ " The total fee is: "
+ expectedFee);
}
}
/** The 'grace-period', 'applied' and 'refundable' fields are disallowed by server policy. */
static class UnsupportedFeeAttributeException extends UnimplementedOptionException {
UnsupportedFeeAttributeException() {
super("The 'grace-period', 'refundable' and 'applied' attributes are disallowed by server "
+ "policy");
}
}
/** Restores always renew a domain for one year. */
static class RestoresAreAlwaysForOneYearException extends ParameterValuePolicyErrorException {
RestoresAreAlwaysForOneYearException() {
super("Restores always renew a domain for one year");
}
}
/** Requested domain is reserved. */
static class DomainReservedException extends StatusProhibitsOperationException {
public DomainReservedException(String domainName) {
super(String.format("%s is a reserved domain", domainName));
}
}
/**
* The requested domain name is on the premium price list, and this registrar has blocked premium
* registrations.
*/
static class PremiumNameBlockedException extends StatusProhibitsOperationException {
public PremiumNameBlockedException() {
super("The requested domain name is on the premium price list, "
+ "and this registrar has blocked premium registrations");
}
}
/** The fees passed in the transform command do not match the fees that will be charged. */
public static class FeesMismatchException extends ParameterValueRangeErrorException {
public FeesMismatchException() {
super("The fees passed in the transform command do not match the fees that will be charged");
}
public FeesMismatchException(Money correctFee) {
super(String.format(
"The fees passed in the transform command do not match the expected total of %s",
correctFee));
}
}
/** Registrar is not authorized to access this TLD. */
public static class NotAuthorizedForTldException extends AuthorizationErrorException {
public NotAuthorizedForTldException(String tld) {
super("Registrar is not authorized to access the TLD " + tld);
}
}
/** Registrant is not whitelisted for this TLD. */
public static class RegistrantNotAllowedException extends StatusProhibitsOperationException {
public RegistrantNotAllowedException(String contactId) {
super(String.format("Registrant with id %s is not whitelisted for this TLD", contactId));
}
}
/** Nameservers are not whitelisted for this TLD. */
public static class NameserversNotAllowedException extends StatusProhibitsOperationException {
public NameserversNotAllowedException(Set<String> fullyQualifiedHostNames) {
super(String.format(
"Nameservers '%s' are not whitelisted for this TLD",
Joiner.on(',').join(fullyQualifiedHostNames)));
}
}
/** Nameservers not specified for this TLD with whitelist. */
public static class NameserversNotSpecifiedException extends StatusProhibitsOperationException {
public NameserversNotSpecifiedException() {
super("At least one nameserver must be specified for this TLD");
}
}
/** Command is not allowed in the current registry phase. */
public static class BadCommandForRegistryPhaseException extends CommandUseErrorException {
public BadCommandForRegistryPhaseException() {
super("Command is not allowed in the current registry phase");
}
}
/** The secDNS:all element must have value 'true' if present. */
static class SecDnsAllUsageException extends ParameterValuePolicyErrorException {
public SecDnsAllUsageException() {
super("The secDNS:all element must have value 'true' if present");
}
}
/** At least one of 'add' or 'rem' is required on a secDNS update. */
static class EmptySecDnsUpdateException extends RequiredParameterMissingException {
public EmptySecDnsUpdateException() {
super("At least one of 'add' or 'rem' is required on a secDNS update");
}
}
/** At least one dsData is required when using the secDNS extension. */
static class DsDataRequiredException extends ParameterValuePolicyErrorException {
public DsDataRequiredException() {
super("At least one dsData is required when using the secDNS extension");
}
}
/** The 'urgent' attribute is not supported. */
static class UrgentAttributeNotSupportedException extends UnimplementedOptionException {
public UrgentAttributeNotSupportedException() {
super("The 'urgent' attribute is not supported");
}
}
/** The 'maxSigLife' setting is not supported. */
static class MaxSigLifeNotSupportedException extends UnimplementedOptionException {
public MaxSigLifeNotSupportedException() {
super("The 'maxSigLife' setting is not supported");
}
}
/** Changing 'maxSigLife' is not supported. */
static class MaxSigLifeChangeNotSupportedException extends UnimplementedOptionException {
public MaxSigLifeChangeNotSupportedException() {
super("Changing 'maxSigLife' is not supported");
}
}
/** The specified trademark validator is not supported. */
static class InvalidTrademarkValidatorException extends ParameterValuePolicyErrorException {
public InvalidTrademarkValidatorException() {
super("The only supported validationID is 'tmch' for the ICANN Trademark Clearinghouse.");
}
}
/** The expiration time specified in the claim notice has elapsed. */
static class ExpiredClaimException extends ParameterValueRangeErrorException {
public ExpiredClaimException() {
super("The expiration time specified in the claim notice has elapsed");
}
}
/** The acceptance time specified in the claim notice is more than 48 hours in the past. */
static class AcceptedTooLongAgoException extends ParameterValueRangeErrorException {
public AcceptedTooLongAgoException() {
super("The acceptance time specified in the claim notice is more than 48 hours in the past");
}
}
/** The specified TCNID is invalid. */
static class MalformedTcnIdException extends ParameterValueSyntaxErrorException {
public MalformedTcnIdException() {
super("The specified TCNID is malformed");
}
}
/** The checksum in the specified TCNID does not validate. */
static class InvalidTcnIdChecksumException extends ParameterValueRangeErrorException {
public InvalidTcnIdChecksumException() {
super("The checksum in the specified TCNID does not validate");
}
}
/** The claims period for this TLD has ended. */
static class ClaimsPeriodEndedException extends StatusProhibitsOperationException {
public ClaimsPeriodEndedException(String tld) {
super(String.format("The claims period for %s has ended", tld));
}
}
/** Requested domain requires a claims notice. */
static class MissingClaimsNoticeException extends StatusProhibitsOperationException {
public MissingClaimsNoticeException(String domainName) {
super(String.format("%s requires a claims notice", domainName));
}
}
/** Requested domain does not require a claims notice. */
static class UnexpectedClaimsNoticeException extends StatusProhibitsOperationException {
public UnexpectedClaimsNoticeException(String domainName) {
super(String.format("%s does not require a claims notice", domainName));
}
}
/** Invalid limited registration period token. */
static class InvalidLrpTokenException
extends InvalidAuthorizationInformationErrorException {
public InvalidLrpTokenException() {
super("Invalid limited registration period token");
}
}
/** Only encoded signed marks are supported. */
static class UnsupportedMarkTypeException extends ParameterValuePolicyErrorException {
public UnsupportedMarkTypeException() {
super("Only encoded signed marks are supported");
}
}
}