Java tutorial
// 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"); } } }