// Copyright 2017 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.picker; import static google.registry.model.domain.launch.LaunchCreateExtension.CreateType.APPLICATION; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import com.google.common.collect.ImmutableTable; import com.google.common.collect.Table; import google.registry.flows.EppException; import google.registry.flows.EppException.SyntaxErrorException; import google.registry.flows.EppException.UnimplementedCommandException; import google.registry.flows.Flow; import google.registry.flows.contact.ContactCheckFlow; import google.registry.flows.contact.ContactCreateFlow; import google.registry.flows.contact.ContactDeleteFlow; import google.registry.flows.contact.ContactInfoFlow; import google.registry.flows.contact.ContactTransferApproveFlow; import google.registry.flows.contact.ContactTransferCancelFlow; import google.registry.flows.contact.ContactTransferQueryFlow; import google.registry.flows.contact.ContactTransferRejectFlow; import google.registry.flows.contact.ContactTransferRequestFlow; import google.registry.flows.contact.ContactUpdateFlow; import google.registry.flows.domain.DomainAllocateFlow; import google.registry.flows.domain.DomainApplicationCreateFlow; import google.registry.flows.domain.DomainApplicationDeleteFlow; import google.registry.flows.domain.DomainApplicationInfoFlow; import google.registry.flows.domain.DomainApplicationUpdateFlow; import google.registry.flows.domain.DomainCheckFlow; import google.registry.flows.domain.DomainClaimsCheckFlow; import google.registry.flows.domain.DomainCreateFlow; import google.registry.flows.domain.DomainDeleteFlow; import google.registry.flows.domain.DomainInfoFlow; import google.registry.flows.domain.DomainRenewFlow; import google.registry.flows.domain.DomainRestoreRequestFlow; import google.registry.flows.domain.DomainTransferApproveFlow; import google.registry.flows.domain.DomainTransferCancelFlow; import google.registry.flows.domain.DomainTransferQueryFlow; import google.registry.flows.domain.DomainTransferRejectFlow; import google.registry.flows.domain.DomainTransferRequestFlow; import google.registry.flows.domain.DomainUpdateFlow; import google.registry.flows.host.HostCheckFlow; import google.registry.flows.host.HostCreateFlow; import google.registry.flows.host.HostDeleteFlow; import google.registry.flows.host.HostInfoFlow; import google.registry.flows.host.HostUpdateFlow; import google.registry.flows.poll.PollAckFlow; import google.registry.flows.poll.PollRequestFlow; import google.registry.flows.session.HelloFlow; import google.registry.flows.session.LoginFlow; import google.registry.flows.session.LogoutFlow; import google.registry.model.contact.ContactCommand; import google.registry.model.domain.DomainCommand; import google.registry.model.domain.allocate.AllocateCreateExtension; import google.registry.model.domain.launch.ApplicationIdTargetExtension; import google.registry.model.domain.launch.LaunchCheckExtension; import google.registry.model.domain.launch.LaunchCheckExtension.CheckType; import google.registry.model.domain.launch.LaunchCreateExtension; import google.registry.model.domain.launch.LaunchPhase; import google.registry.model.domain.rgp.RestoreCommand.RestoreOp; import google.registry.model.domain.rgp.RgpUpdateExtension; import google.registry.model.eppinput.EppInput; import google.registry.model.eppinput.EppInput.Hello; import google.registry.model.eppinput.EppInput.InnerCommand; import google.registry.model.eppinput.EppInput.Login; import google.registry.model.eppinput.EppInput.Logout; import google.registry.model.eppinput.EppInput.Poll; import google.registry.model.eppinput.EppInput.ResourceCommandWrapper; import google.registry.model.eppinput.EppInput.Transfer; import google.registry.model.eppinput.EppInput.Transfer.TransferOp; import google.registry.model.eppinput.ResourceCommand; import google.registry.model.host.HostCommand; import java.util.Map; import java.util.Optional; /** Class that picks a flow to handle a given EPP command. */ public class FlowPicker { /** Marker class for unimplemented flows. */ private abstract static class UnimplementedFlow implements Flow {} /** A function type that takes an {@link EppInput} and returns a {@link Flow} class. */ private abstract static class FlowProvider { /** Get the flow associated with this {@link EppInput} or return null to signal no match. */ Class get(EppInput eppInput) { InnerCommand innerCommand = eppInput.getCommandWrapper().getCommand(); return get(eppInput, innerCommand, (innerCommand instanceof ResourceCommandWrapper) ? ((ResourceCommandWrapper) innerCommand).getResourceCommand() : null); } /** * Subclasses need to implement this to examine the parameters and choose a flow (or null if * the subclass doesn't know of an appropriate flow. */ abstract Class get( EppInput eppInput, InnerCommand innerCommand, ResourceCommand resourceCommand); } /** The hello flow is keyed on a special {@code CommandWrapper} type. */ private static final FlowProvider HELLO_FLOW_PROVIDER = new FlowProvider() { @Override Class get( EppInput eppInput, InnerCommand innerCommand, ResourceCommand resourceCommand) { return eppInput.getCommandWrapper() instanceof Hello ? HelloFlow.class : null; }}; /** Session flows like login and logout are keyed only on the {@link InnerCommand} type. */ private static final FlowProvider SESSION_FLOW_PROVIDER = new FlowProvider() { private final Map, Class> commandFlows = ImmutableMap.of( Login.class, LoginFlow.class, Logout.class, LogoutFlow.class); @Override Class get( EppInput eppInput, InnerCommand innerCommand, ResourceCommand resourceCommand) { return innerCommand == null ? null : commandFlows.get(innerCommand.getClass()); }}; /** Poll flows have an {@link InnerCommand} of type {@link Poll}. */ private static final FlowProvider POLL_FLOW_PROVIDER = new FlowProvider() { @Override Class get( EppInput eppInput, InnerCommand innerCommand, ResourceCommand resourceCommand) { if (!(innerCommand instanceof Poll)) { return null; } switch (((Poll) innerCommand).getPollOp()) { case ACK: return PollAckFlow.class; case REQUEST: return PollRequestFlow.class; default: return UnimplementedFlow.class; } }}; /** * The domain restore command is technically a domain {@literal }, but logically a totally * separate flow. * *

This provider must be tried before {@link #RESOURCE_CRUD_FLOW_PROVIDER}. Otherwise, the * regular domain update flow will match first. */ private static final FlowProvider DOMAIN_RESTORE_FLOW_PROVIDER = new FlowProvider() { @Override Class get( EppInput eppInput, InnerCommand innerCommand, ResourceCommand resourceCommand) { if (!(resourceCommand instanceof DomainCommand.Update)) { return null; } Optional rgpUpdateExtension = eppInput.getSingleExtension(RgpUpdateExtension.class); if (!rgpUpdateExtension.isPresent()) { return null; } // Restore command with an op of "report" is not currently supported. return (rgpUpdateExtension.get().getRestoreCommand().getRestoreOp() == RestoreOp.REQUEST) ? DomainRestoreRequestFlow.class : UnimplementedFlow.class; }}; /** * The claims check flow is keyed on the type of the {@link ResourceCommand} and on having the * correct extension with a specific phase value. */ private static final FlowProvider DOMAIN_CHECK_FLOW_PROVIDER = new FlowProvider() { @Override Class get( EppInput eppInput, InnerCommand innerCommand, ResourceCommand resourceCommand) { if (!(resourceCommand instanceof DomainCommand.Check)) { return null; } Optional launchCheck = eppInput.getSingleExtension(LaunchCheckExtension.class); if (!launchCheck.isPresent() || CheckType.AVAILABILITY.equals(launchCheck.get().getCheckType())) { // We don't distinguish between registry phases for "avail", so don't bother checking // phase. return DomainCheckFlow.class; } if (CheckType.CLAIMS.equals(launchCheck.get().getCheckType()) && LaunchPhase.CLAIMS.equals(launchCheck.get().getPhase())) { return DomainClaimsCheckFlow.class; } return null; } }; /** General resource CRUD flows are keyed on the type of their {@link ResourceCommand}. */ private static final FlowProvider RESOURCE_CRUD_FLOW_PROVIDER = new FlowProvider() { private final Map, Class> resourceCrudFlows = new ImmutableMap.Builder, Class>() .put(ContactCommand.Check.class, ContactCheckFlow.class) .put(ContactCommand.Create.class, ContactCreateFlow.class) .put(ContactCommand.Delete.class, ContactDeleteFlow.class) .put(ContactCommand.Info.class, ContactInfoFlow.class) .put(ContactCommand.Update.class, ContactUpdateFlow.class) .put(DomainCommand.Create.class, DomainCreateFlow.class) .put(DomainCommand.Delete.class, DomainDeleteFlow.class) .put(DomainCommand.Info.class, DomainInfoFlow.class) .put(DomainCommand.Renew.class, DomainRenewFlow.class) .put(DomainCommand.Update.class, DomainUpdateFlow.class) .put(HostCommand.Check.class, HostCheckFlow.class) .put(HostCommand.Create.class, HostCreateFlow.class) .put(HostCommand.Delete.class, HostDeleteFlow.class) .put(HostCommand.Info.class, HostInfoFlow.class) .put(HostCommand.Update.class, HostUpdateFlow.class) .build(); @Override Class get( EppInput eppInput, InnerCommand innerCommand, ResourceCommand resourceCommand) { return resourceCommand == null ? null : resourceCrudFlows.get(resourceCommand.getClass()); }}; /** The domain allocate flow has a specific extension. */ private static final FlowProvider ALLOCATE_FLOW_PROVIDER = new FlowProvider() { @Override Class get( EppInput eppInput, InnerCommand innerCommand, ResourceCommand resourceCommand) { return (resourceCommand instanceof DomainCommand.Create && eppInput.getSingleExtension(AllocateCreateExtension.class).isPresent()) ? DomainAllocateFlow.class : null; } }; private static final ImmutableSet LAUNCH_PHASES_DEFAULTING_TO_APPLICATION = ImmutableSet.of(LaunchPhase.SUNRUSH, LaunchPhase.LANDRUSH); /** * Application CRUD flows have an extension and are keyed on the type of their {@link * ResourceCommand}. */ private static final FlowProvider APPLICATION_CRUD_FLOW_PROVIDER = new FlowProvider() { private final Map, Class> applicationFlows = ImmutableMap.of( DomainCommand.Create.class, DomainApplicationCreateFlow.class, DomainCommand.Delete.class, DomainApplicationDeleteFlow.class, DomainCommand.Info.class, DomainApplicationInfoFlow.class, DomainCommand.Update.class, DomainApplicationUpdateFlow.class); @Override Class get( EppInput eppInput, InnerCommand innerCommand, ResourceCommand resourceCommand) { if (eppInput.getSingleExtension(ApplicationIdTargetExtension.class).isPresent()) { return applicationFlows.get(resourceCommand.getClass()); } Optional createExtension = eppInput.getSingleExtension(LaunchCreateExtension.class); // Return a flow if the type is APPLICATION. If the type is REGISTRATION, return null. if (createExtension.isPresent()) { LaunchPhase launchPhase = createExtension.get().getPhase(); // has an optional type argument, that can take either "application" or // "registration". // https://tools.ietf.org/html/rfc8334#section-3.3.1 // We get that type via createExtension.get().getCreateType() // If it isn't given, the function returns null. // In that case, we need to decide based on the TLD. For now we can't do that - so we // TEMPORARILY decide as follows: // landrush and sunrush phases will default to APPLICATION, because there's no possible // registration for it. // sunrise defaults to REGISTRATION because we're currenly launching start-date sunrise // that uses direct registration. // // TODO(b/76095570): if createExtension.get().getCreateType() isn't explicitly given, // we need to set it according to the TldState (which means we need to know the TLD and // load the Registry - which will probably result in a big refactoring since we can use // TldState information to pick the flow) if (APPLICATION.equals(createExtension.get().getCreateType()) || (createExtension.get().getCreateType() == null && LAUNCH_PHASES_DEFAULTING_TO_APPLICATION.contains(launchPhase))) { return applicationFlows.get(resourceCommand.getClass()); } } return null; } }; /** Transfer flows have an {@link InnerCommand} of type {@link Transfer}. */ private static final FlowProvider TRANSFER_FLOW_PROVIDER = new FlowProvider() { private final Table, TransferOp, Class> transferFlows = ImmutableTable ., TransferOp, Class>builder() .put(ContactCommand.Transfer.class, TransferOp.APPROVE, ContactTransferApproveFlow.class) .put(ContactCommand.Transfer.class, TransferOp.CANCEL, ContactTransferCancelFlow.class) .put(ContactCommand.Transfer.class, TransferOp.QUERY, ContactTransferQueryFlow.class) .put(ContactCommand.Transfer.class, TransferOp.REJECT, ContactTransferRejectFlow.class) .put(ContactCommand.Transfer.class, TransferOp.REQUEST, ContactTransferRequestFlow.class) .put(DomainCommand.Transfer.class, TransferOp.APPROVE, DomainTransferApproveFlow.class) .put(DomainCommand.Transfer.class, TransferOp.CANCEL, DomainTransferCancelFlow.class) .put(DomainCommand.Transfer.class, TransferOp.QUERY, DomainTransferQueryFlow.class) .put(DomainCommand.Transfer.class, TransferOp.REJECT, DomainTransferRejectFlow.class) .put(DomainCommand.Transfer.class, TransferOp.REQUEST, DomainTransferRequestFlow.class) .build(); @Override Class get( EppInput eppInput, InnerCommand innerCommand, ResourceCommand resourceCommand) { return resourceCommand != null && innerCommand instanceof Transfer ? transferFlows.get(resourceCommand.getClass(), ((Transfer) innerCommand).getTransferOp()) : null; }}; private static final ImmutableList FLOW_PROVIDERS = ImmutableList.of( HELLO_FLOW_PROVIDER, SESSION_FLOW_PROVIDER, POLL_FLOW_PROVIDER, DOMAIN_RESTORE_FLOW_PROVIDER, ALLOCATE_FLOW_PROVIDER, APPLICATION_CRUD_FLOW_PROVIDER, DOMAIN_CHECK_FLOW_PROVIDER, RESOURCE_CRUD_FLOW_PROVIDER, TRANSFER_FLOW_PROVIDER); /** Return the appropriate flow to handle this EPP command. */ public static Class getFlowClass(EppInput eppInput) throws EppException { // Do some sanity checking on the input; anything but Hello must have a command type. InnerCommand innerCommand = eppInput.getCommandWrapper().getCommand(); if (innerCommand == null && !(eppInput.getCommandWrapper() instanceof Hello)) { throw new MissingCommandException(); } // Try the FlowProviders until we find a match. The order matters because it's possible to // match multiple FlowProviders and so more specific matches are tried first. for (FlowProvider flowProvider : FLOW_PROVIDERS) { Class flowClass = flowProvider.get(eppInput); if (flowClass == UnimplementedFlow.class) { break; // We found it, but it's marked as not implemented. } if (flowClass != null) { return flowClass; // We found it! } } // Nothing usable was found, so throw an exception. throw new UnimplementedCommandException(innerCommand); } /** Command missing. */ static class MissingCommandException extends SyntaxErrorException { public MissingCommandException() { super("Command missing"); } } }