google-nomulus/java/google/registry/flows/domain/DomainFlowUtils.java
jianglai b783acfcc6 Support date and notAfter in fee extension response v12
When custom effective date is passed in the check command, the response should
contain that date as an acknowledgemant that the check is performed at a time
different from now.

Also when the fee(s) returned contains a validDateRange (i. e. EAP fees that
are only valid during a certain period), the response will contain a notAfter
field which is the date after which the quoted fee(s) are no longer valid. (i.
e. the earliest of the end dates of all fees that would expire.

-------------
Created by MOE: https://github.com/google/moe
MOE_MIGRATED_REVID=133989775
2016-09-26 13:24:57 -04:00

1159 lines
47 KiB
Java

// Copyright 2016 The Domain Registry 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.Iterables.concat;
import static com.google.common.collect.Sets.difference;
import static google.registry.flows.EppXmlTransformer.unmarshal;
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.getPricesForDomainName;
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 google.registry.flows.EppException;
import google.registry.flows.EppException.AuthorizationErrorException;
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.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.CreateOrUpdate;
import google.registry.model.domain.DomainCommand.InvalidReferencesException;
import google.registry.model.domain.DomainResource;
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.launch.LaunchExtension;
import google.registry.model.domain.launch.LaunchPhase;
import google.registry.model.domain.secdns.DelegationSignerData;
import google.registry.model.domain.secdns.SecDnsInfoExtension;
import google.registry.model.eppcommon.StatusValue;
import google.registry.model.eppinput.EppInput;
import google.registry.model.eppinput.ResourceCommand.SingleResourceCommand;
import google.registry.model.eppoutput.EppResponse.ResponseExtension;
import google.registry.model.host.HostResource;
import google.registry.model.mark.Mark;
import google.registry.model.mark.ProtectedMark;
import google.registry.model.mark.Trademark;
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.smd.AbstractSignedMark;
import google.registry.model.smd.EncodedSignedMark;
import google.registry.model.smd.SignedMark;
import google.registry.model.smd.SignedMarkRevocationList;
import google.registry.model.transfer.TransferData;
import google.registry.model.transfer.TransferResponse.DomainTransferResponse;
import google.registry.tmch.TmchXmlSignature;
import google.registry.tmch.TmchXmlSignature.CertificateSignatureException;
import google.registry.util.Idn;
import java.io.IOException;
import java.math.BigDecimal;
import java.security.GeneralSecurityException;
import java.security.SignatureException;
import java.security.cert.CertificateExpiredException;
import java.security.cert.CertificateNotYetValidException;
import java.security.cert.CertificateRevokedException;
import java.util.HashSet;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import javax.annotation.Nullable;
import javax.xml.crypto.MarshalException;
import javax.xml.crypto.dsig.XMLSignatureException;
import javax.xml.parsers.ParserConfigurationException;
import org.joda.money.CurrencyUnit;
import org.joda.money.Money;
import org.joda.time.DateTime;
import org.xml.sax.SAXException;
/** 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);
/** Non-sunrise tld states. */
public static final ImmutableSet<TldState> DISALLOWED_TLD_STATES_FOR_LAUNCH_FLOWS =
Sets.immutableEnumSet(
TldState.PREDELEGATION,
TldState.QUIET_PERIOD,
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);
/** 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(Set<String> allowedTlds, String tld)
throws EppException {
if (!allowedTlds.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 isSunriseApplication) throws EppException {
if (isReserved(domainName, isSunriseApplication)) {
throw new DomainReservedException(domainName.toString());
}
}
private static boolean isReserved(InternetDomainName domainName, boolean inSunrise) {
ReservationType type = getReservationType(domainName);
return type == ReservationType.FULLY_BLOCKED
|| type == ReservationType.RESERVED_FOR_ANCHOR_TENANT
|| (TYPES_ALLOWED_FOR_CREATE_ONLY_IN_SUNRISE.contains(type) && !inSunrise);
}
/** 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 verifyLaunchPhase(
String tld, LaunchExtension launchExtension, DateTime now) throws EppException {
if (!Objects.equals(
Registry.get(tld).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 a launch extension's application id refers to an application with the same
* domain name as the one specified in the launch command.
*/
static void verifyLaunchApplicationIdMatchesDomain(
SingleResourceCommand command, DomainBase existingResource) throws EppException {
if (!Objects.equals(command.getTargetId(), existingResource.getFullyQualifiedDomainName())) {
throw new ApplicationDomainNameMismatchException();
}
}
/** 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 (getPricesForDomainName(domainName, priceTime).isPremium()) {
// 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());
}
public static SignedMark verifySignedMarks(
ImmutableList<AbstractSignedMark> signedMarks, String domainLabel, DateTime now)
throws EppException {
if (signedMarks.size() > 1) {
throw new TooManySignedMarksException();
}
if (!(signedMarks.get(0) instanceof EncodedSignedMark)) {
throw new SignedMarksMustBeEncodedException();
}
return verifyEncodedSignedMark((EncodedSignedMark) signedMarks.get(0), domainLabel, now);
}
public static SignedMark verifyEncodedSignedMark(
EncodedSignedMark encodedSignedMark, String domainLabel, DateTime now) throws EppException {
if (!encodedSignedMark.getEncoding().equals("base64")) {
throw new Base64RequiredForEncodedSignedMarksException();
}
byte[] signedMarkData;
try {
signedMarkData = encodedSignedMark.getBytes();
} catch (IllegalStateException e) {
throw new SignedMarkEncodingErrorException();
}
SignedMark signedMark;
try {
signedMark = unmarshal(SignedMark.class, signedMarkData);
} catch (EppException e) {
throw new SignedMarkParsingErrorException();
}
if (SignedMarkRevocationList.get().isSmdRevoked(signedMark.getId(), now)) {
throw new SignedMarkRevokedErrorException();
}
try {
TmchXmlSignature.verify(signedMarkData);
} catch (CertificateExpiredException e) {
throw new SignedMarkCertificateExpiredException();
} catch (CertificateNotYetValidException e) {
throw new SignedMarkCertificateNotYetValidException();
} catch (CertificateRevokedException e) {
throw new SignedMarkCertificateRevokedException();
} catch (CertificateSignatureException e) {
throw new SignedMarkCertificateSignatureException();
} catch (SignatureException | XMLSignatureException e) {
throw new SignedMarkSignatureException();
} catch (GeneralSecurityException e) {
throw new SignedMarkCertificateInvalidException();
} catch (IOException
| MarshalException
| SAXException
| ParserConfigurationException e) {
throw new SignedMarkParsingErrorException();
}
if (!(isAtOrAfter(now, signedMark.getCreationTime())
&& now.isBefore(signedMark.getExpirationTime())
&& containsMatchingLabel(signedMark.getMark(), domainLabel))) {
throw new NoMarksFoundMatchingDomainException();
}
return signedMark;
}
/** Returns true if the mark contains a valid claim that matches the label. */
static boolean containsMatchingLabel(Mark mark, String label) {
for (Trademark trademark : mark.getTrademarks()) {
if (trademark.getLabels().contains(label)) {
return true;
}
}
for (ProtectedMark protectedMark
: concat(mark.getTreatyOrStatuteMarks(), mark.getCourtMarks())) {
if (protectedMark.getLabels().contains(label)) {
return true;
}
}
return false;
}
/**
* Validates a {@link FeeQueryCommandExtensionItem} and sets the appropriate fields on a {@link
* FeeQueryResponseExtensionItem} builder.
*/
static void handleFeeRequest(
FeeQueryCommandExtensionItem feeRequest,
FeeQueryResponseExtensionItem.Builder builder,
InternetDomainName domain,
String clientId,
@Nullable CurrencyUnit topLevelCurrency,
DateTime currentDate,
EppInput eppInput) 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(TldSpecificLogicProxy.getFeeClass(domainNameString, now).orNull());
List<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 = TldSpecificLogicProxy.getCreatePrice(
registry, domainNameString, clientId, now, years, eppInput).getFees();
}
break;
case RENEW:
builder.setAvailIfSupported(true);
fees = TldSpecificLogicProxy.getRenewPrice(
registry, domainNameString, clientId, now, years, eppInput).getFees();
break;
case RESTORE:
if (years != 1) {
throw new RestoresAreAlwaysForOneYearException();
}
builder.setAvailIfSupported(true);
fees = TldSpecificLogicProxy.getRestorePrice(
registry, domainNameString, clientId, now, eppInput).getFees();
break;
case TRANSFER:
builder.setAvailIfSupported(true);
fees = TldSpecificLogicProxy.getTransferPrice(
registry, domainNameString, clientId, now, years, eppInput).getFees();
break;
case UPDATE:
builder.setAvailIfSupported(true);
fees = TldSpecificLogicProxy.getUpdatePrice(
registry, domainNameString, clientId, now, eppInput).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,
Money cost,
Money... otherCosts)
throws EppException {
Registry registry = Registry.get(tld);
if (registry.getPremiumPriceAckRequired()
&& getPricesForDomainName(domainName, priceTime).isPremium()
&& feeCommand == null) {
throw new FeesRequiredForPremiumNameException();
}
if (feeCommand == null) {
return;
}
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();
}
Money costTotal = cost;
for (Money otherCost : otherCosts) {
costTotal = costTotal.plus(otherCost);
}
if (!feeTotal.getCurrencyUnit().equals(costTotal.getCurrencyUnit())) {
throw new CurrencyUnitMismatchException();
}
if (!feeTotal.equals(costTotal)) {
throw new FeesMismatchException(costTotal);
}
}
/** 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));
}
}
/** Encoded signed marks must use base64 encoding. */
static class Base64RequiredForEncodedSignedMarksException
extends ParameterValuePolicyErrorException {
public Base64RequiredForEncodedSignedMarksException() {
super("Encoded signed marks must use base64 encoding");
}
}
/** 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");
}
}
/** Signed mark data is improperly encoded. */
static class SignedMarkEncodingErrorException extends ParameterValueSyntaxErrorException {
public SignedMarkEncodingErrorException() {
super("Signed mark data is improperly encoded");
}
}
/** Error while parsing encoded signed mark data. */
static class SignedMarkParsingErrorException extends ParameterValueSyntaxErrorException {
public SignedMarkParsingErrorException() {
super("Error while parsing encoded signed mark data");
}
}
/** Invalid signature on a signed mark. */
static class SignedMarkSignatureException extends ParameterValuePolicyErrorException {
public SignedMarkSignatureException() {
super("Signed mark signature is invalid");
}
}
/** Invalid signature on a signed mark. */
static class SignedMarkCertificateSignatureException extends ParameterValuePolicyErrorException {
public SignedMarkCertificateSignatureException() {
super("Signed mark certificate not signed by ICANN");
}
}
/** Certificate used in signed mark signature was revoked by ICANN. */
static class SignedMarkCertificateRevokedException extends ParameterValuePolicyErrorException {
public SignedMarkCertificateRevokedException() {
super("Signed mark certificate was revoked");
}
}
/** Certificate used in signed mark signature has expired. */
static class SignedMarkCertificateExpiredException extends ParameterValuePolicyErrorException {
public SignedMarkCertificateExpiredException() {
super("Signed mark certificate has expired");
}
}
/** Certificate used in signed mark signature has expired. */
static class SignedMarkCertificateNotYetValidException
extends ParameterValuePolicyErrorException {
public SignedMarkCertificateNotYetValidException() {
super("Signed mark certificate not yet valid");
}
}
/** Certificate parsing error, or possibly a bad provider or algorithm. */
static class SignedMarkCertificateInvalidException extends ParameterValuePolicyErrorException {
public SignedMarkCertificateInvalidException() {
super("Signed mark certificate is invalid");
}
}
/** Signed mark data is revoked. */
static class SignedMarkRevokedErrorException extends ParameterValuePolicyErrorException {
public SignedMarkRevokedErrorException() {
super("SMD has been revoked");
}
}
/** Only one signed mark is allowed per application. */
static class TooManySignedMarksException extends ParameterValuePolicyErrorException {
public TooManySignedMarksException() {
super("Only one signed mark is allowed per application");
}
}
/** Signed marks must be encoded. */
static class SignedMarksMustBeEncodedException extends ParameterValuePolicyErrorException {
public SignedMarksMustBeEncodedException() {
super("Signed marks must be encoded");
}
}
/** The provided mark does not match the desired domain label. */
static class NoMarksFoundMatchingDomainException extends RequiredParameterMissingException {
public NoMarksFoundMatchingDomainException() {
super("The provided mark does not match the desired domain label");
}
}
/** 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 update which is not free. */
static class FeesRequiredForNonFreeUpdateException extends RequiredParameterMissingException {
FeesRequiredForNonFreeUpdateException() {
super("Fees must be explicitly acknowledged when performing an update which is not free.");
}
}
/** 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");
}
}
}