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