From 9aa2f3b96e36fdce53f779c9e4455836fd45ea93 Mon Sep 17 00:00:00 2001 From: mcilwain Date: Mon, 7 Nov 2016 14:34:30 -0800 Subject: [PATCH] Make host flows only accept canonicalized host names as input This now throws errors when a non-lower-cased, non-puny-coded, or non-canonicalized host name is passed in as an input parameter. The approach we'll take is to first notify registrars which hosts we'll be renaming, then issue EPP host update commands to effect those renames as superuser, then push this code live to production. This fixes #38 on GitHub. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=138441130 --- docs/flows.md | 14 ++++ .../registry/flows/host/HostCreateFlow.java | 3 + .../registry/flows/host/HostDeleteFlow.java | 5 ++ .../registry/flows/host/HostFlowUtils.java | 40 +++++++++++- .../registry/flows/host/HostInfoFlow.java | 7 +- .../registry/flows/host/HostUpdateFlow.java | 8 +++ .../flows/host/HostCreateFlowTest.java | 24 +++++++ .../flows/host/HostDeleteFlowTest.java | 27 +++++++- .../registry/flows/host/HostInfoFlowTest.java | 27 +++++++- .../flows/host/HostUpdateFlowTest.java | 64 +++++++++++++++++++ .../flows/host/testdata/host_delete.xml | 2 +- .../flows/host/testdata/host_info.xml | 2 +- 12 files changed, 215 insertions(+), 8 deletions(-) diff --git a/docs/flows.md b/docs/flows.md index 01234be34..08f648b1f 100644 --- a/docs/flows.md +++ b/docs/flows.md @@ -967,6 +967,9 @@ are enqueued to update DNS accordingly. addresses. * 2005 * Invalid host name. + * Host names must be in lower-case. + * Host names must be in normalized format. + * Host names must be puny-coded. * 2201 * The specified resource belongs to another client. * 2302 @@ -996,6 +999,10 @@ see the information for any host. ### Errors +* 2005 + * Host names must be in lower-case. + * Host names must be in normalized format. + * Host names must be puny-coded. * 2303 * Resource with this id does not exist. @@ -1015,6 +1022,10 @@ or failure message when the process is complete. ### Errors +* 2005 + * Host names must be in lower-case. + * Host names must be in normalized format. + * Host names must be puny-coded. * 2201 * The specified resource belongs to another client. * 2303 @@ -1047,6 +1058,9 @@ allows creating a host name, and if necessary enqueues tasks to update DNS. * External hosts must not have ip addresses. * 2005 * Invalid host name. + * Host names must be in lower-case. + * Host names must be in normalized format. + * Host names must be puny-coded. * 2302 * Resource with this id already exists. * 2303 diff --git a/java/google/registry/flows/host/HostCreateFlow.java b/java/google/registry/flows/host/HostCreateFlow.java index 4736c4149..e9a8a26f0 100644 --- a/java/google/registry/flows/host/HostCreateFlow.java +++ b/java/google/registry/flows/host/HostCreateFlow.java @@ -65,6 +65,9 @@ import org.joda.time.DateTime; * @error {@link HostFlowUtils.HostNameTooLongException} * @error {@link HostFlowUtils.HostNameTooShallowException} * @error {@link HostFlowUtils.InvalidHostNameException} + * @error {@link HostFlowUtils.HostNameNotLowerCaseException} + * @error {@link HostFlowUtils.HostNameNotNormalizedException} + * @error {@link HostFlowUtils.HostNameNotPunyCodedException} * @error {@link HostFlowUtils.SuperordinateDomainDoesNotExistException} * @error {@link SubordinateHostMustHaveIpException} * @error {@link UnexpectedExternalHostIpException} diff --git a/java/google/registry/flows/host/HostDeleteFlow.java b/java/google/registry/flows/host/HostDeleteFlow.java index bb3a24f13..e490b956b 100644 --- a/java/google/registry/flows/host/HostDeleteFlow.java +++ b/java/google/registry/flows/host/HostDeleteFlow.java @@ -19,6 +19,7 @@ import static google.registry.flows.ResourceFlowUtils.failfastForAsyncDelete; import static google.registry.flows.ResourceFlowUtils.loadAndVerifyExistence; import static google.registry.flows.ResourceFlowUtils.verifyNoDisallowedStatuses; import static google.registry.flows.ResourceFlowUtils.verifyResourceOwnership; +import static google.registry.flows.host.HostFlowUtils.validateHostName; import static google.registry.model.eppoutput.Result.Code.SUCCESS_WITH_ACTION_PENDING; import static google.registry.model.ofy.ObjectifyService.ofy; @@ -54,6 +55,9 @@ import org.joda.time.DateTime; * @error {@link google.registry.flows.ResourceFlowUtils.ResourceNotOwnedException} * @error {@link google.registry.flows.exceptions.ResourceStatusProhibitsOperationException} * @error {@link google.registry.flows.exceptions.ResourceToDeleteIsReferencedException} + * @error {@link HostFlowUtils.HostNameNotLowerCaseException} + * @error {@link HostFlowUtils.HostNameNotNormalizedException} + * @error {@link HostFlowUtils.HostNameNotPunyCodedException} */ public final class HostDeleteFlow implements TransactionalFlow { @@ -85,6 +89,7 @@ public final class HostDeleteFlow implements TransactionalFlow { extensionManager.validate(); validateClientIsLoggedIn(clientId); DateTime now = ofy().getTransactionTime(); + validateHostName(targetId); failfastForAsyncDelete(targetId, now, HostResource.class, GET_NAMESERVERS); HostResource existingHost = loadAndVerifyExistence(HostResource.class, targetId, now); verifyNoDisallowedStatuses(existingHost, DISALLOWED_STATUSES); diff --git a/java/google/registry/flows/host/HostFlowUtils.java b/java/google/registry/flows/host/HostFlowUtils.java index af2d6da6b..805f89689 100644 --- a/java/google/registry/flows/host/HostFlowUtils.java +++ b/java/google/registry/flows/host/HostFlowUtils.java @@ -17,7 +17,9 @@ package google.registry.flows.host; import static google.registry.model.EppResourceUtils.isActive; import static google.registry.model.EppResourceUtils.loadByForeignKey; import static google.registry.model.registry.Registries.findTldForName; +import static google.registry.util.PreconditionsUtils.checkArgumentNotNull; +import com.google.common.base.Ascii; import com.google.common.base.Joiner; import com.google.common.base.Optional; import com.google.common.collect.Iterables; @@ -29,6 +31,7 @@ import google.registry.flows.EppException.ParameterValuePolicyErrorException; import google.registry.flows.EppException.ParameterValueRangeErrorException; import google.registry.flows.EppException.ParameterValueSyntaxErrorException; import google.registry.model.domain.DomainResource; +import google.registry.util.Idn; import org.joda.time.DateTime; /** Static utility functions for host flows. */ @@ -36,14 +39,23 @@ public class HostFlowUtils { /** Checks that a host name is valid. */ static InternetDomainName validateHostName(String name) throws EppException { - if (name == null) { - return null; - } + checkArgumentNotNull(name, "Must specify host name to validate"); if (name.length() > 253) { throw new HostNameTooLongException(); } + String hostNameLowerCase = Ascii.toLowerCase(name); + if (!name.equals(hostNameLowerCase)) { + throw new HostNameNotLowerCaseException(hostNameLowerCase); + } + String hostNamePunyCoded = Idn.toASCII(name); + if (!name.equals(hostNamePunyCoded)) { + throw new HostNameNotPunyCodedException(hostNamePunyCoded); + } try { InternetDomainName hostName = InternetDomainName.from(name); + if (!name.equals(hostName.toString())) { + throw new HostNameNotNormalizedException(hostName.toString()); + } // Checks whether a hostname is deep enough. Technically a host can be just one under a // public suffix (e.g. example.com) but we require by policy that it has to be at least one // part beyond that (e.g. ns1.example.com). The public suffix list includes all current @@ -135,4 +147,26 @@ public class HostFlowUtils { super("Invalid host name"); } } + + /** Host names must be in lower-case. */ + static class HostNameNotLowerCaseException extends ParameterValueSyntaxErrorException { + public HostNameNotLowerCaseException(String expectedHostName) { + super(String.format("Host names must be in lower-case; expected %s", expectedHostName)); + } + } + + /** Host names must be puny-coded. */ + static class HostNameNotPunyCodedException extends ParameterValueSyntaxErrorException { + public HostNameNotPunyCodedException(String expectedHostName) { + super(String.format("Host names must be puny-coded; expected %s", expectedHostName)); + } + } + + /** Host names must be in normalized format. */ + static class HostNameNotNormalizedException extends ParameterValueSyntaxErrorException { + public HostNameNotNormalizedException(String expectedHostName) { + super( + String.format("Host names must be in normalized format; expected %s", expectedHostName)); + } + } } diff --git a/java/google/registry/flows/host/HostInfoFlow.java b/java/google/registry/flows/host/HostInfoFlow.java index f03cea67e..94a3c5a1a 100644 --- a/java/google/registry/flows/host/HostInfoFlow.java +++ b/java/google/registry/flows/host/HostInfoFlow.java @@ -16,6 +16,7 @@ package google.registry.flows.host; import static google.registry.flows.FlowUtils.validateClientIsLoggedIn; import static google.registry.flows.ResourceFlowUtils.loadAndVerifyExistence; +import static google.registry.flows.host.HostFlowUtils.validateHostName; import static google.registry.model.EppResourceUtils.cloneResourceWithLinkedStatus; import google.registry.flows.EppException; @@ -36,6 +37,9 @@ import org.joda.time.DateTime; * transfer if it has ever been transferred. Any registrar can see the information for any host. * * @error {@link google.registry.flows.ResourceFlowUtils.ResourceDoesNotExistException} + * @error {@link HostFlowUtils.HostNameNotLowerCaseException} + * @error {@link HostFlowUtils.HostNameNotNormalizedException} + * @error {@link HostFlowUtils.HostNameNotPunyCodedException} */ public final class HostInfoFlow implements Flow { @@ -49,7 +53,8 @@ public final class HostInfoFlow implements Flow { @Override public EppResponse run() throws EppException { extensionManager.validate(); // There are no legal extensions for this flow. - validateClientIsLoggedIn(clientId); + validateClientIsLoggedIn(clientId); + validateHostName(targetId); DateTime now = clock.nowUtc(); HostResource host = loadAndVerifyExistence(HostResource.class, targetId, now); return responseBuilder.setResData(cloneResourceWithLinkedStatus(host, now)).build(); diff --git a/java/google/registry/flows/host/HostUpdateFlow.java b/java/google/registry/flows/host/HostUpdateFlow.java index d48af8ef7..39563160e 100644 --- a/java/google/registry/flows/host/HostUpdateFlow.java +++ b/java/google/registry/flows/host/HostUpdateFlow.java @@ -82,6 +82,9 @@ import org.joda.time.DateTime; * @error {@link google.registry.flows.exceptions.ResourceStatusProhibitsOperationException} * @error {@link HostFlowUtils.HostNameTooShallowException} * @error {@link HostFlowUtils.InvalidHostNameException} + * @error {@link HostFlowUtils.HostNameNotLowerCaseException} + * @error {@link HostFlowUtils.HostNameNotNormalizedException} + * @error {@link HostFlowUtils.HostNameNotPunyCodedException} * @error {@link HostFlowUtils.SuperordinateDomainDoesNotExistException} * @error {@link CannotAddIpToExternalHostException} * @error {@link CannotRemoveSubordinateHostLastIpException} @@ -120,6 +123,11 @@ public final class HostUpdateFlow implements TransactionalFlow { Change change = command.getInnerChange(); String suppliedNewHostName = change.getFullyQualifiedHostName(); DateTime now = ofy().getTransactionTime(); + // Validation is disabled for superusers to allow renaming of existing invalid hostnames. + // TODO(b/32328995): Remove superuser override once all bad data in prod has been fixed. + if (!isSuperuser) { + validateHostName(targetId); + } HostResource existingHost = loadAndVerifyExistence(HostResource.class, targetId, now); boolean isHostRename = suppliedNewHostName != null; String oldHostName = targetId; diff --git a/javatests/google/registry/flows/host/HostCreateFlowTest.java b/javatests/google/registry/flows/host/HostCreateFlowTest.java index 51daec368..26d0a6953 100644 --- a/javatests/google/registry/flows/host/HostCreateFlowTest.java +++ b/javatests/google/registry/flows/host/HostCreateFlowTest.java @@ -32,6 +32,9 @@ import google.registry.flows.ResourceFlowTestCase; import google.registry.flows.exceptions.ResourceAlreadyExistsException; import google.registry.flows.host.HostCreateFlow.SubordinateHostMustHaveIpException; import google.registry.flows.host.HostCreateFlow.UnexpectedExternalHostIpException; +import google.registry.flows.host.HostFlowUtils.HostNameNotLowerCaseException; +import google.registry.flows.host.HostFlowUtils.HostNameNotNormalizedException; +import google.registry.flows.host.HostFlowUtils.HostNameNotPunyCodedException; import google.registry.flows.host.HostFlowUtils.HostNameTooLongException; import google.registry.flows.host.HostFlowUtils.HostNameTooShallowException; import google.registry.flows.host.HostFlowUtils.InvalidHostNameException; @@ -169,6 +172,27 @@ public class HostCreateFlowTest extends ResourceFlowTestCase exception) @@ -158,4 +162,25 @@ public class HostDeleteFlowTest extends ResourceFlowTestCase { public HostInfoFlowTest() { - setEppInput("host_info.xml"); + setEppInput("host_info.xml", ImmutableMap.of("HOSTNAME", "ns1.example.tld")); } @Before @@ -158,4 +162,25 @@ public class HostInfoFlowTest extends ResourceFlowTestCase - ns1.example.tld + %HOSTNAME% ABC-12345 diff --git a/javatests/google/registry/flows/host/testdata/host_info.xml b/javatests/google/registry/flows/host/testdata/host_info.xml index cf923d541..431bf3680 100644 --- a/javatests/google/registry/flows/host/testdata/host_info.xml +++ b/javatests/google/registry/flows/host/testdata/host_info.xml @@ -3,7 +3,7 @@ - ns1.example.tld + %HOSTNAME% ABC-12345