// 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.rdap; import static google.registry.model.EppResourceUtils.loadByForeignKey; import static google.registry.model.ofy.ObjectifyService.ofy; import static google.registry.rdap.RdapIcannStandardInformation.TRUNCATION_NOTICES; import static google.registry.request.Action.Method.GET; import static google.registry.request.Action.Method.HEAD; import static google.registry.util.DateTimeUtils.END_OF_TIME; import com.google.common.base.Optional; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSortedSet; import com.google.common.collect.Iterables; import com.google.common.primitives.Booleans; import google.registry.config.RegistryConfig.Config; import google.registry.model.domain.DomainResource; import google.registry.model.host.HostResource; import google.registry.rdap.RdapJsonFormatter.BoilerplateType; import google.registry.rdap.RdapJsonFormatter.OutputDataType; import google.registry.request.Action; import google.registry.request.HttpException.BadRequestException; import google.registry.request.HttpException.NotFoundException; import google.registry.request.Parameter; import google.registry.request.auth.Auth; import google.registry.util.Clock; import google.registry.util.Idn; import java.net.InetAddress; import java.util.List; import javax.inject.Inject; import org.joda.time.DateTime; /** * RDAP (new WHOIS) action for nameserver search requests. * *

All commands and responses conform to the RDAP spec as defined in RFCs 7480 through 7485. * * @see RFC 7482: Registration Data Access Protocol * (RDAP) Query Format * @see RFC 7483: JSON Responses for the Registration * Data Access Protocol (RDAP) */ @Action( path = RdapNameserverSearchAction.PATH, method = {GET, HEAD}, auth = Auth.AUTH_PUBLIC_ANONYMOUS ) public class RdapNameserverSearchAction extends RdapActionBase { public static final String PATH = "/rdap/nameservers"; @Inject Clock clock; @Inject @Parameter("name") Optional nameParam; @Inject @Parameter("ip") Optional ipParam; @Inject @Config("rdapResultSetMaxSize") int rdapResultSetMaxSize; @Inject RdapNameserverSearchAction() {} @Override public String getHumanReadableObjectTypeName() { return "nameserver search"; } @Override public String getActionPath() { return PATH; } /** * Parses the parameters and calls the appropriate search function. * *

The RDAP spec allows nameserver search by either name or IP address. */ @Override public ImmutableMap getJsonObjectForResource( String pathSearchString, boolean isHeadRequest, String linkBase) { DateTime now = clock.nowUtc(); // RDAP syntax example: /rdap/nameservers?name=ns*.example.com. // The pathSearchString is not used by search commands. if (pathSearchString.length() > 0) { throw new BadRequestException("Unexpected path"); } if (Booleans.countTrue(nameParam.isPresent(), ipParam.isPresent()) != 1) { throw new BadRequestException("You must specify either name=XXXX or ip=YYYY"); } RdapSearchResults results; if (nameParam.isPresent()) { // syntax: /rdap/nameservers?name=exam*.com if (!LDH_PATTERN.matcher(nameParam.get()).matches()) { throw new BadRequestException( "Name parameter must contain only letters, dots" + " and hyphens, and an optional single wildcard"); } results = searchByName(RdapSearchPattern.create(Idn.toASCII(nameParam.get()), true), now); } else { // syntax: /rdap/nameservers?ip=1.2.3.4 results = searchByIp(ipParam.get(), now); } if (results.jsonList().isEmpty()) { throw new NotFoundException("No nameservers found"); } ImmutableMap.Builder jsonBuilder = new ImmutableMap.Builder<>(); jsonBuilder.put("nameserverSearchResults", results.jsonList()); rdapJsonFormatter.addTopLevelEntries( jsonBuilder, BoilerplateType.NAMESERVER, results.isTruncated() ? TRUNCATION_NOTICES : ImmutableList.>of(), ImmutableList.>of(), rdapLinkBase); return jsonBuilder.build(); } /** Searches for nameservers by name, returning a JSON array of nameserver info maps. */ private RdapSearchResults searchByName( final RdapSearchPattern partialStringQuery, final DateTime now) { // Handle queries without a wildcard -- just load by foreign key. if (!partialStringQuery.getHasWildcard()) { HostResource hostResource = loadByForeignKey(HostResource.class, partialStringQuery.getInitialString(), now); if (hostResource == null) { throw new NotFoundException("No nameservers found"); } return RdapSearchResults.create( ImmutableList.of( rdapJsonFormatter.makeRdapJsonForHost( hostResource, false, rdapLinkBase, rdapWhoisServer, now, OutputDataType.FULL))); // Handle queries with a wildcard. } else { // If there is a suffix, it should be a domain. If it happens to be a domain that we manage, // we can look up the domain and look through the subordinate hosts. This is more efficient, // and lets us permit wildcard searches with no initial string. if (partialStringQuery.getSuffix() != null) { DomainResource domainResource = loadByForeignKey(DomainResource.class, partialStringQuery.getSuffix(), now); ImmutableList.Builder hostListBuilder = new ImmutableList.Builder<>(); if (domainResource != null) { for (String fqhn : ImmutableSortedSet.copyOf(domainResource.getSubordinateHosts())) { // We can't just check that the host name starts with the initial query string, because // then the query ns.exam*.example.com would match against nameserver ns.example.com. if (partialStringQuery.matches(fqhn)) { HostResource hostResource = loadByForeignKey(HostResource.class, fqhn, now); if (hostResource != null) { hostListBuilder.add(hostResource); } } } } else { // If we don't recognize the domain, call queryUndeleted and filter. // TODO(mountford): figure out how to size this correctly for (HostResource hostResource : queryUndeleted( HostResource.class, "fullyQualifiedHostName", partialStringQuery, 1000)) { if (partialStringQuery.matches(hostResource.getFullyQualifiedHostName())) { hostListBuilder.add(hostResource); } } } return makeSearchResults(hostListBuilder.build(), now); // Handle queries with a wildcard, but no suffix. There are no pending deletes for hosts, so // we can call queryUndeleted. } else { return makeSearchResults( // Add 1 so we can detect truncation. queryUndeleted( HostResource.class, "fullyQualifiedHostName", partialStringQuery, rdapResultSetMaxSize + 1) .list(), now); } } } /** Searches for nameservers by IP address, returning a JSON array of nameserver info maps. */ private RdapSearchResults searchByIp(final InetAddress inetAddress, DateTime now) { return makeSearchResults( // Add 1 so we can detect truncation. ofy().load() .type(HostResource.class) .filter("inetAddresses", inetAddress.getHostAddress()) .filter("deletionTime", END_OF_TIME) .limit(rdapResultSetMaxSize + 1) .list(), now); } /** Output JSON for a list of hosts. */ private RdapSearchResults makeSearchResults(List hosts, DateTime now) { OutputDataType outputDataType = (hosts.size() > 1) ? OutputDataType.SUMMARY : OutputDataType.FULL; ImmutableList.Builder> jsonListBuilder = new ImmutableList.Builder<>(); for (HostResource host : Iterables.limit(hosts, rdapResultSetMaxSize)) { jsonListBuilder.add( rdapJsonFormatter.makeRdapJsonForHost( host, false, rdapLinkBase, rdapWhoisServer, now, outputDataType)); } ImmutableList> jsonList = jsonListBuilder.build(); return RdapSearchResults.create(jsonList, jsonList.size() < hosts.size()); } }