diff --git a/java/google/registry/rdap/RdapJsonFormatter.java b/java/google/registry/rdap/RdapJsonFormatter.java index aa1133141..85488ac2f 100644 --- a/java/google/registry/rdap/RdapJsonFormatter.java +++ b/java/google/registry/rdap/RdapJsonFormatter.java @@ -573,11 +573,11 @@ public class RdapJsonFormatter { new ImmutableMap.Builder<>(); ImmutableList v4Addresses = v4AddressesBuilder.build(); if (!v4Addresses.isEmpty()) { - ipAddressesBuilder.put("v4", v4Addresses); + ipAddressesBuilder.put("v4", Ordering.natural().immutableSortedCopy(v4Addresses)); } ImmutableList v6Addresses = v6AddressesBuilder.build(); if (!v6Addresses.isEmpty()) { - ipAddressesBuilder.put("v6", v6Addresses); + ipAddressesBuilder.put("v6", Ordering.natural().immutableSortedCopy(v6Addresses)); } ImmutableMap> ipAddresses = ipAddressesBuilder.build(); if (!ipAddresses.isEmpty()) { diff --git a/java/google/registry/rdap/RdapNameserverSearchAction.java b/java/google/registry/rdap/RdapNameserverSearchAction.java index f3da540f7..5838b76d1 100644 --- a/java/google/registry/rdap/RdapNameserverSearchAction.java +++ b/java/google/registry/rdap/RdapNameserverSearchAction.java @@ -16,6 +16,7 @@ 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; @@ -24,6 +25,7 @@ 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.ConfigModule.Config; import google.registry.model.domain.DomainResource; @@ -85,7 +87,7 @@ public class RdapNameserverSearchAction extends RdapActionBase { if (Booleans.countTrue(nameParam.isPresent(), ipParam.isPresent()) != 1) { throw new BadRequestException("You must specify either name=XXXX or ip=YYYY"); } - ImmutableList> results; + RdapSearchResults results; if (nameParam.isPresent()) { // syntax: /rdap/nameservers?name=exam*.com if (!LDH_PATTERN.matcher(nameParam.get()).matches()) { @@ -98,23 +100,24 @@ public class RdapNameserverSearchAction extends RdapActionBase { // syntax: /rdap/nameservers?ip=1.2.3.4 results = searchByIp(ipParam.get(), now); } - if (results.isEmpty()) { + if (results.jsonList().isEmpty()) { throw new NotFoundException("No nameservers found"); } ImmutableMap.Builder jsonBuilder = new ImmutableMap.Builder<>(); - jsonBuilder.put("nameserverSearchResults", results); + jsonBuilder.put("nameserverSearchResults", results.jsonList()); RdapJsonFormatter.addTopLevelEntries( jsonBuilder, BoilerplateType.NAMESERVER, - ImmutableList.>of(), + 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 ImmutableList> - searchByName(final RdapSearchPattern partialStringQuery, final DateTime now) { + private RdapSearchResults searchByName( + final RdapSearchPattern partialStringQuery, final DateTime now) { // Handle queries without a wildcard -- just load by foreign key. if (!partialStringQuery.getHasWildcard()) { HostResource hostResource = @@ -122,18 +125,21 @@ public class RdapNameserverSearchAction extends RdapActionBase { if (hostResource == null) { throw new NotFoundException("No nameservers found"); } - return ImmutableList.of( - RdapJsonFormatter.makeRdapJsonForHost( - hostResource, false, rdapLinkBase, rdapWhoisServer, now, OutputDataType.FULL)); + return RdapSearchResults.create( + ImmutableList.of( + RdapJsonFormatter.makeRdapJsonForHost( + hostResource, false, rdapLinkBase, rdapWhoisServer, now, OutputDataType.FULL)), + false); // Handle queries with a wildcard, but no suffix. There are no pending deletes for hosts, so we // can call queryUndeleted. } else if (partialStringQuery.getSuffix() == null) { return makeSearchResults( + // Add 1 so we can detect truncation. queryUndeleted( HostResource.class, "fullyQualifiedHostName", partialStringQuery, - rdapResultSetMaxSize) + rdapResultSetMaxSize + 1) .list(), now); // Handle queries with a wildcard and a suffix. In this case, it is more efficient to do things @@ -161,29 +167,30 @@ public class RdapNameserverSearchAction extends RdapActionBase { } /** Searches for nameservers by IP address, returning a JSON array of nameserver info maps. */ - private ImmutableList> - searchByIp(final InetAddress inetAddress, DateTime now) { + 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) + .limit(rdapResultSetMaxSize + 1) .list(), now); } /** Output JSON for a list of hosts. */ - private ImmutableList> makeSearchResults( - List hosts, DateTime now) { + private RdapSearchResults makeSearchResults(List hosts, DateTime now) { OutputDataType outputDataType = (hosts.size() > 1) ? OutputDataType.SUMMARY : OutputDataType.FULL; - ImmutableList.Builder> jsonBuilder = new ImmutableList.Builder<>(); - for (HostResource host : hosts) { - jsonBuilder.add( + ImmutableList.Builder> jsonListBuilder = + new ImmutableList.Builder<>(); + for (HostResource host : Iterables.limit(hosts, rdapResultSetMaxSize)) { + jsonListBuilder.add( RdapJsonFormatter.makeRdapJsonForHost( host, false, rdapLinkBase, rdapWhoisServer, now, outputDataType)); } - return jsonBuilder.build(); + ImmutableList> jsonList = jsonListBuilder.build(); + return RdapSearchResults.create(jsonList, jsonList.size() < hosts.size()); } } diff --git a/javatests/google/registry/rdap/RdapNameserverSearchActionTest.java b/javatests/google/registry/rdap/RdapNameserverSearchActionTest.java index 50a362acd..852d4ac76 100644 --- a/javatests/google/registry/rdap/RdapNameserverSearchActionTest.java +++ b/javatests/google/registry/rdap/RdapNameserverSearchActionTest.java @@ -17,10 +17,12 @@ package google.registry.rdap; import static com.google.common.truth.Truth.assertThat; import static google.registry.testing.DatastoreHelper.createTld; import static google.registry.testing.DatastoreHelper.persistResource; +import static google.registry.testing.DatastoreHelper.persistResources; import static google.registry.testing.DatastoreHelper.persistSimpleResources; import static google.registry.testing.FullFieldsTestEntityHelper.makeAndPersistHostResource; import static google.registry.testing.FullFieldsTestEntityHelper.makeContactResource; import static google.registry.testing.FullFieldsTestEntityHelper.makeDomainResource; +import static google.registry.testing.FullFieldsTestEntityHelper.makeHostResource; import static google.registry.testing.FullFieldsTestEntityHelper.makeRegistrar; import static google.registry.testing.FullFieldsTestEntityHelper.makeRegistrarContacts; import static google.registry.testing.TestDataHelper.loadFileWithSubstitutions; @@ -39,6 +41,7 @@ import google.registry.testing.AppEngineRule; import google.registry.testing.FakeClock; import google.registry.testing.FakeResponse; import google.registry.testing.InjectRule; +import javax.annotation.Nullable; import org.joda.time.DateTime; import org.json.simple.JSONValue; import org.junit.Before; @@ -60,6 +63,7 @@ public class RdapNameserverSearchActionTest { private final RdapNameserverSearchAction action = new RdapNameserverSearchAction(); + private DomainResource domainCatLol; private HostResource hostNs1CatLol; private HostResource hostNs2CatLol; private HostResource hostNs1Cat2Lol; @@ -105,21 +109,20 @@ public class RdapNameserverSearchActionTest { makeAndPersistHostResource("ns1.cat.1.test", "1.2.3.6", clock.nowUtc().minusYears(1)); // create a domain so that we can use it as a test nameserver search string suffix - DomainResource domainCatLol = - persistResource( - makeDomainResource( - "cat.lol", - persistResource( - makeContactResource("5372808-ERL", "Goblin Market", "lol@cat.lol")), - persistResource( - makeContactResource("5372808-IRL", "Santa Claus", "BOFH@cat.lol")), - persistResource(makeContactResource("5372808-TRL", "The Raven", "bog@cat.lol")), - hostNs1CatLol, - hostNs2CatLol, - registrar) - .asBuilder() - .setSubordinateHosts(ImmutableSet.of("ns1.cat.lol", "ns2.cat.lol")) - .build()); + domainCatLol = persistResource( + makeDomainResource( + "cat.lol", + persistResource( + makeContactResource("5372808-ERL", "Goblin Market", "lol@cat.lol")), + persistResource( + makeContactResource("5372808-IRL", "Santa Claus", "BOFH@cat.lol")), + persistResource(makeContactResource("5372808-TRL", "The Raven", "bog@cat.lol")), + hostNs1CatLol, + hostNs2CatLol, + registrar) + .asBuilder() + .setSubordinateHosts(ImmutableSet.of("ns1.cat.lol", "ns2.cat.lol")) + .build()); persistResource( hostNs1CatLol.asBuilder().setSuperordinateDomain(Key.create(domainCatLol)).build()); persistResource( @@ -129,27 +132,35 @@ public class RdapNameserverSearchActionTest { action.clock = clock; action.requestPath = RdapNameserverSearchAction.PATH; action.response = response; - action.rdapResultSetMaxSize = 100; + action.rdapResultSetMaxSize = 4; action.rdapLinkBase = "https://example.tld/rdap/"; action.rdapWhoisServer = null; action.ipParam = Optional.absent(); action.nameParam = Optional.absent(); } + private Object generateExpectedJson(String expectedOutputFile) { + return generateExpectedJson(null, null, null, null, null, expectedOutputFile); + } + private Object generateExpectedJson(String name, String expectedOutputFile) { return generateExpectedJson(name, null, null, null, null, expectedOutputFile); } private Object generateExpectedJson( - String name, - String punycodeName, - String handle, - String ipAddressType, - String ipAddress, + @Nullable String name, + @Nullable String punycodeName, + @Nullable String handle, + @Nullable String ipAddressType, + @Nullable String ipAddress, String expectedOutputFile) { ImmutableMap.Builder builder = new ImmutableMap.Builder<>(); - builder.put("NAME", name); - builder.put("PUNYCODENAME", (punycodeName == null) ? name : punycodeName); + if (name != null) { + builder.put("NAME", name); + } + if ((name != null) || (punycodeName != null)) { + builder.put("PUNYCODENAME", (punycodeName == null) ? name : punycodeName); + } if (handle != null) { builder.put("HANDLE", handle); } @@ -182,6 +193,21 @@ public class RdapNameserverSearchActionTest { return builder.build(); } + private void createManyHosts(int numHosts) { + ImmutableList.Builder hostsBuilder = new ImmutableList.Builder<>(); + ImmutableSet.Builder subordinateHostsBuilder = new ImmutableSet.Builder<>(); + for (int i = 1; i <= numHosts; i++) { + String hostName = String.format("ns%d.cat.lol", i); + subordinateHostsBuilder.add(hostName); + hostsBuilder.add(makeHostResource(hostName, "5.5.5.1", "5.5.5.2")); + } + persistResources(hostsBuilder.build()); + domainCatLol = persistResource( + domainCatLol.asBuilder() + .setSubordinateHosts(subordinateHostsBuilder.build()) + .build()); + } + @Test public void testInvalidPath_rejected() throws Exception { action.requestPath = RdapDomainSearchAction.PATH + "/path"; @@ -328,6 +354,46 @@ public class RdapNameserverSearchActionTest { generateActualJsonWithName("dog*"); assertThat(response.getStatus()).isEqualTo(404); } + + @Test + public void testNameMatch_nontruncatedResultSet() throws Exception { + createManyHosts(4); + assertThat(generateActualJsonWithName("ns*.cat.lol")) + .isEqualTo(generateExpectedJson("rdap_nontruncated_hosts.json")); + assertThat(response.getStatus()).isEqualTo(200); + } + + @Test + public void testNameMatch_truncatedResultSet() throws Exception { + createManyHosts(5); + assertThat(generateActualJsonWithName("ns*.cat.lol")) + .isEqualTo(generateExpectedJson("rdap_truncated_hosts.json")); + assertThat(response.getStatus()).isEqualTo(200); + } + + @Test + public void testNameMatch_reallyTruncatedResultSet() throws Exception { + createManyHosts(9); + assertThat(generateActualJsonWithName("ns*.cat.lol")) + .isEqualTo(generateExpectedJson("rdap_truncated_hosts.json")); + assertThat(response.getStatus()).isEqualTo(200); + } + + @Test + public void testNameMatchDeletedHost_foundTheOtherHost() throws Exception { + persistResource( + hostNs1Cat2Lol.asBuilder().setDeletionTime(clock.nowUtc().minusDays(1)).build()); + assertThat(generateActualJsonWithIp("bad:f00d:cafe::15:beef")) + .isEqualTo( + generateExpectedJsonForNameserver( + "ns2.cat.lol", + null, + "4-ROID", + "v6", + "bad:f00d:cafe::15:beef", + "rdap_host.json")); + assertThat(response.getStatus()).isEqualTo(200); + } @Test public void testAddressMatchV4Address_found() throws Exception { @@ -341,7 +407,7 @@ public class RdapNameserverSearchActionTest { @Test public void testAddressMatchV6Address_foundMultiple() throws Exception { assertThat(generateActualJsonWithIp("bad:f00d:cafe::15:beef")) - .isEqualTo(generateExpectedJson("ns1.cat.external", "rdap_multiple_hosts.json")); + .isEqualTo(generateExpectedJson("rdap_multiple_hosts.json")); assertThat(response.getStatus()).isEqualTo(200); } @@ -376,18 +442,26 @@ public class RdapNameserverSearchActionTest { } @Test - public void testNameMatchDeletedHost_foundTheOtherHost() throws Exception { - persistResource( - hostNs1Cat2Lol.asBuilder().setDeletionTime(clock.nowUtc().minusDays(1)).build()); - assertThat(generateActualJsonWithIp("bad:f00d:cafe::15:beef")) - .isEqualTo( - generateExpectedJsonForNameserver( - "ns2.cat.lol", - null, - "4-ROID", - "v6", - "bad:f00d:cafe::15:beef", - "rdap_host.json")); + public void testAddressMatch_nontruncatedResultSet() throws Exception { + createManyHosts(4); + assertThat(generateActualJsonWithIp("5.5.5.1")) + .isEqualTo(generateExpectedJson("rdap_nontruncated_hosts.json")); + assertThat(response.getStatus()).isEqualTo(200); + } + + @Test + public void testAddressMatch_truncatedResultSet() throws Exception { + createManyHosts(5); + assertThat(generateActualJsonWithIp("5.5.5.1")) + .isEqualTo(generateExpectedJson("rdap_truncated_hosts.json")); + assertThat(response.getStatus()).isEqualTo(200); + } + + @Test + public void testAddressMatch_reallyTruncatedResultSet() throws Exception { + createManyHosts(9); + assertThat(generateActualJsonWithIp("5.5.5.1")) + .isEqualTo(generateExpectedJson("rdap_truncated_hosts.json")); assertThat(response.getStatus()).isEqualTo(200); } } diff --git a/javatests/google/registry/rdap/testdata/rdap_nontruncated_hosts.json b/javatests/google/registry/rdap/testdata/rdap_nontruncated_hosts.json new file mode 100644 index 000000000..98496105b --- /dev/null +++ b/javatests/google/registry/rdap/testdata/rdap_nontruncated_hosts.json @@ -0,0 +1,155 @@ +{ + "nameserverSearchResults" : + [ + { + "objectClassName" : "nameserver", + "handle" : "14-ROID", + "status" : ["active"], + "ldhName" : "ns1.cat.lol", + "links" : + [ + { + "value" : "https://example.tld/rdap/nameserver/ns1.cat.lol", + "rel" : "self", + "type" : "application/rdap+json", + "href" : "https://example.tld/rdap/nameserver/ns1.cat.lol" + } + ], + "ipAddresses" : + { + "v4" : ["5.5.5.1", "5.5.5.2"] + }, + "remarks": [ + { + "title": "Incomplete Data", + "description": [ + "Summary data only. For complete data, send a specific query for the object." + ], + "type": "object truncated due to unexplainable reasons" + } + ] + }, + { + "objectClassName" : "nameserver", + "handle" : "15-ROID", + "status" : ["active"], + "ldhName" : "ns2.cat.lol", + "links" : + [ + { + "value" : "https://example.tld/rdap/nameserver/ns2.cat.lol", + "rel" : "self", + "type" : "application/rdap+json", + "href" : "https://example.tld/rdap/nameserver/ns2.cat.lol" + } + ], + "ipAddresses" : + { + "v4" : ["5.5.5.1", "5.5.5.2"] + }, + "remarks": [ + { + "title": "Incomplete Data", + "description": [ + "Summary data only. For complete data, send a specific query for the object." + ], + "type": "object truncated due to unexplainable reasons" + } + ] + }, + { + "objectClassName" : "nameserver", + "handle" : "16-ROID", + "status" : ["active"], + "ldhName" : "ns3.cat.lol", + "links" : + [ + { + "value" : "https://example.tld/rdap/nameserver/ns3.cat.lol", + "rel" : "self", + "type" : "application/rdap+json", + "href" : "https://example.tld/rdap/nameserver/ns3.cat.lol" + } + ], + "ipAddresses" : + { + "v4" : ["5.5.5.1", "5.5.5.2"] + }, + "remarks": [ + { + "title": "Incomplete Data", + "description": [ + "Summary data only. For complete data, send a specific query for the object." + ], + "type": "object truncated due to unexplainable reasons" + } + ] + }, + { + "objectClassName" : "nameserver", + "handle" : "17-ROID", + "status" : ["active"], + "ldhName" : "ns4.cat.lol", + "links" : + [ + { + "value" : "https://example.tld/rdap/nameserver/ns4.cat.lol", + "rel" : "self", + "type" : "application/rdap+json", + "href" : "https://example.tld/rdap/nameserver/ns4.cat.lol" + } + ], + "ipAddresses" : + { + "v4" : ["5.5.5.1", "5.5.5.2"] + }, + "remarks": [ + { + "title": "Incomplete Data", + "description": [ + "Summary data only. For complete data, send a specific query for the object." + ], + "type": "object truncated due to unexplainable reasons" + } + ] + } + ], + "rdapConformance" : ["rdap_level_0"], + "notices" : + [ + { + "title" : "RDAP Terms of Service", + "description" : + [ + "By querying our Domain Database, you are agreeing to comply with these terms so please read them carefully.", + "Any information provided is 'as is' without any guarantee of accuracy.", + "Please do not misuse the Domain Database. It is intended solely for query-based access.", + "Don't use the Domain Database to allow, enable, or otherwise support the transmission of mass unsolicited, commercial advertising or solicitations.", + "Don't access our Domain Database through the use of high volume, automated electronic processes that send queries or data to the systems of Charleston Road Registry or any ICANN-accredited registrar.", + "You may only use the information contained in the Domain Database for lawful purposes.", + "Do not compile, repackage, disseminate, or otherwise use the information contained in the Domain Database in its entirety, or in any substantial portion, without our prior written permission.", + "We may retain certain details about queries to our Domain Database for the purposes of detecting and preventing misuse.", + "We reserve the right to restrict or deny your access to the database if we suspect that you have failed to comply with these terms.", + "We reserve the right to modify this agreement at any time." + ], + "links" : + [ + { + "value" : "https://example.tld/rdap/help/tos", + "rel" : "alternate", + "href" : "https://www.registry.google/about/rdap/tos.html", + "type" : "text/html" + } + ] + } + ], + "remarks" : + [ + { + "description" : + [ + "This response conforms to the RDAP Operational Profile for gTLD Registries and Registrars version 1.0" + ] + } + ] +} diff --git a/javatests/google/registry/rdap/testdata/rdap_truncated_hosts.json b/javatests/google/registry/rdap/testdata/rdap_truncated_hosts.json new file mode 100644 index 000000000..a0c6388aa --- /dev/null +++ b/javatests/google/registry/rdap/testdata/rdap_truncated_hosts.json @@ -0,0 +1,163 @@ +{ + "nameserverSearchResults" : + [ + { + "objectClassName" : "nameserver", + "handle" : "14-ROID", + "status" : ["active"], + "ldhName" : "ns1.cat.lol", + "links" : + [ + { + "value" : "https://example.tld/rdap/nameserver/ns1.cat.lol", + "rel" : "self", + "type" : "application/rdap+json", + "href" : "https://example.tld/rdap/nameserver/ns1.cat.lol" + } + ], + "ipAddresses" : + { + "v4" : ["5.5.5.1", "5.5.5.2"] + }, + "remarks": [ + { + "title": "Incomplete Data", + "description": [ + "Summary data only. For complete data, send a specific query for the object." + ], + "type": "object truncated due to unexplainable reasons" + } + ] + }, + { + "objectClassName" : "nameserver", + "handle" : "15-ROID", + "status" : ["active"], + "ldhName" : "ns2.cat.lol", + "links" : + [ + { + "value" : "https://example.tld/rdap/nameserver/ns2.cat.lol", + "rel" : "self", + "type" : "application/rdap+json", + "href" : "https://example.tld/rdap/nameserver/ns2.cat.lol" + } + ], + "ipAddresses" : + { + "v4" : ["5.5.5.1", "5.5.5.2"] + }, + "remarks": [ + { + "title": "Incomplete Data", + "description": [ + "Summary data only. For complete data, send a specific query for the object." + ], + "type": "object truncated due to unexplainable reasons" + } + ] + }, + { + "objectClassName" : "nameserver", + "handle" : "16-ROID", + "status" : ["active"], + "ldhName" : "ns3.cat.lol", + "links" : + [ + { + "value" : "https://example.tld/rdap/nameserver/ns3.cat.lol", + "rel" : "self", + "type" : "application/rdap+json", + "href" : "https://example.tld/rdap/nameserver/ns3.cat.lol" + } + ], + "ipAddresses" : + { + "v4" : ["5.5.5.1", "5.5.5.2"] + }, + "remarks": [ + { + "title": "Incomplete Data", + "description": [ + "Summary data only. For complete data, send a specific query for the object." + ], + "type": "object truncated due to unexplainable reasons" + } + ] + }, + { + "objectClassName" : "nameserver", + "handle" : "17-ROID", + "status" : ["active"], + "ldhName" : "ns4.cat.lol", + "links" : + [ + { + "value" : "https://example.tld/rdap/nameserver/ns4.cat.lol", + "rel" : "self", + "type" : "application/rdap+json", + "href" : "https://example.tld/rdap/nameserver/ns4.cat.lol" + } + ], + "ipAddresses" : + { + "v4" : ["5.5.5.1", "5.5.5.2"] + }, + "remarks": [ + { + "title": "Incomplete Data", + "description": [ + "Summary data only. For complete data, send a specific query for the object." + ], + "type": "object truncated due to unexplainable reasons" + } + ] + } + ], + "rdapConformance" : ["rdap_level_0"], + "notices" : + [ + { + "title" : "Search Policy", + "type" : "result set truncated due to unexplainable reasons", + "description" : + [ + "Search results per query are limited." + ] + }, + { + "title" : "RDAP Terms of Service", + "description" : + [ + "By querying our Domain Database, you are agreeing to comply with these terms so please read them carefully.", + "Any information provided is 'as is' without any guarantee of accuracy.", + "Please do not misuse the Domain Database. It is intended solely for query-based access.", + "Don't use the Domain Database to allow, enable, or otherwise support the transmission of mass unsolicited, commercial advertising or solicitations.", + "Don't access our Domain Database through the use of high volume, automated electronic processes that send queries or data to the systems of Charleston Road Registry or any ICANN-accredited registrar.", + "You may only use the information contained in the Domain Database for lawful purposes.", + "Do not compile, repackage, disseminate, or otherwise use the information contained in the Domain Database in its entirety, or in any substantial portion, without our prior written permission.", + "We may retain certain details about queries to our Domain Database for the purposes of detecting and preventing misuse.", + "We reserve the right to restrict or deny your access to the database if we suspect that you have failed to comply with these terms.", + "We reserve the right to modify this agreement at any time." + ], + "links" : + [ + { + "value" : "https://example.tld/rdap/help/tos", + "rel" : "alternate", + "href" : "https://www.registry.google/about/rdap/tos.html", + "type" : "text/html" + } + ] + } + ], + "remarks" : + [ + { + "description" : + [ + "This response conforms to the RDAP Operational Profile for gTLD Registries and Registrars version 1.0" + ] + } + ] +}