From b602aac09a8e4935a2801edcb9a736d827dcedf9 Mon Sep 17 00:00:00 2001 From: Ben McIlwain Date: Wed, 3 Jul 2024 17:42:20 -0400 Subject: [PATCH] Make all contacts nullable in the data model (#2490) This doesn't yet allow them to be absent in EPP flows, but it should make the code not break if they happen to be null in the database. This is a follow-up to PR #2477, which ends up being a bit easier because whereas the registrant is used in more parts of the codebase, the other contact types (admin, technical, billing) are really only used in RDE, WHOIS, and RDAP, and because they were already being used as a collection anyway, the handling for if that collection contains fewer elements or is empty happened to already be mostly correct. --- .../google/registry/beam/rde/RdePipeline.java | 11 +- .../registry/model/domain/DomainBase.java | 61 ++-- .../registry/rdap/RdapJsonFormatter.java | 8 +- .../flows/domain/DomainInfoFlowTest.java | 13 + .../registry/model/domain/DomainTest.java | 24 +- .../registry/rdap/RdapDomainActionTest.java | 15 + .../whois/DomainWhoisResponseTest.java | 19 ++ .../domain_info_response_no_contacts.xml | 35 +++ ..._domain_no_contacts_exist_with_remark.json | 263 ++++++++++++++++++ .../whois/whois_domain_no_contacts.txt | 28 ++ 10 files changed, 430 insertions(+), 47 deletions(-) create mode 100644 core/src/test/resources/google/registry/flows/domain/domain_info_response_no_contacts.xml create mode 100644 core/src/test/resources/google/registry/rdap/rdap_domain_no_contacts_exist_with_remark.json create mode 100644 core/src/test/resources/google/registry/whois/whois_domain_no_contacts.txt diff --git a/core/src/main/java/google/registry/beam/rde/RdePipeline.java b/core/src/main/java/google/registry/beam/rde/RdePipeline.java index a44d030ea..8c32b8623 100644 --- a/core/src/main/java/google/registry/beam/rde/RdePipeline.java +++ b/core/src/main/java/google/registry/beam/rde/RdePipeline.java @@ -466,13 +466,10 @@ public class RdePipeline implements Serializable { // Contacts and hosts are only deposited in RDE, not BRDA. if (pendingDeposit.mode() == RdeMode.FULL) { HashSet contacts = new HashSet<>(); - contacts.add(domain.getAdminContact().getKey()); - contacts.add(domain.getTechContact().getKey()); - domain.getRegistrant().ifPresent(r -> contacts.add(r.getKey())); - // Billing contact is not mandatory. - if (domain.getBillingContact() != null) { - contacts.add(domain.getBillingContact().getKey()); - } + domain.getAdminContact().ifPresent(c -> contacts.add(c.getKey())); + domain.getTechContact().ifPresent(c -> contacts.add(c.getKey())); + domain.getRegistrant().ifPresent(c -> contacts.add(c.getKey())); + domain.getBillingContact().ifPresent(c -> contacts.add(c.getKey())); referencedContactCounter.inc(contacts.size()); contacts.forEach( contactRepoId -> diff --git a/core/src/main/java/google/registry/model/domain/DomainBase.java b/core/src/main/java/google/registry/model/domain/DomainBase.java index 721463b66..c853a8208 100644 --- a/core/src/main/java/google/registry/model/domain/DomainBase.java +++ b/core/src/main/java/google/registry/model/domain/DomainBase.java @@ -46,6 +46,7 @@ import google.registry.model.EppResource; import google.registry.model.EppResource.ResourceWithTransferData; import google.registry.model.billing.BillingRecurrence; import google.registry.model.contact.Contact; +import google.registry.model.domain.DesignatedContact.Type; import google.registry.model.domain.launch.LaunchNotice; import google.registry.model.domain.rgp.GracePeriodStatus; import google.registry.model.domain.secdns.DomainDsData; @@ -131,10 +132,10 @@ public class DomainBase extends EppResource @Expose @Transient Set> nsHosts; /** Contacts. */ - @Expose VKey adminContact; + @Expose @Nullable VKey adminContact; - @Expose VKey billingContact; - @Expose VKey techContact; + @Expose @Nullable VKey billingContact; + @Expose @Nullable VKey techContact; @Expose @Nullable VKey registrantContact; /** Authorization info (aka transfer secret) of the domain. */ @@ -589,24 +590,32 @@ public class DomainBase extends EppResource return Optional.ofNullable(registrantContact); } - public VKey getAdminContact() { - return adminContact; + public Optional> getAdminContact() { + return Optional.ofNullable(adminContact); } - public VKey getBillingContact() { - return billingContact; + public Optional> getBillingContact() { + return Optional.ofNullable(billingContact); } - public VKey getTechContact() { - return techContact; + public Optional> getTechContact() { + return Optional.ofNullable(techContact); } - /** Associated contacts for the domain (other than registrant). */ + /** + * Associated contacts for the domain (other than registrant). + * + *

Note: This can be an empty set if no contacts are present for the domain. + */ public ImmutableSet getContacts() { return getAllContacts(false); } - /** Gets all associated contacts for the domain, including the registrant. */ + /** + * Gets all associated contacts for the domain, including the registrant. + * + *

Note: This can be an empty set if no contacts are present for the domain. + */ public ImmutableSet getAllContacts() { return getAllContacts(true); } @@ -615,7 +624,11 @@ public class DomainBase extends EppResource return authInfo; } - /** Returns all referenced contacts from this domain. */ + /** + * Returns all referenced contacts from this domain. + * + *

Note: This can be an empty set if no contacts are present for the domain. + */ public ImmutableSet> getReferencedContacts() { return nullToEmptyImmutableCopy(getAllContacts(true)).stream() .map(DesignatedContact::getContactKey) @@ -625,18 +638,12 @@ public class DomainBase extends EppResource private ImmutableSet getAllContacts(boolean includeRegistrant) { ImmutableSet.Builder builder = new ImmutableSet.Builder<>(); - if (includeRegistrant && registrantContact != null) { - builder.add(DesignatedContact.create(DesignatedContact.Type.REGISTRANT, registrantContact)); - } - if (adminContact != null) { - builder.add(DesignatedContact.create(DesignatedContact.Type.ADMIN, adminContact)); - } - if (billingContact != null) { - builder.add(DesignatedContact.create(DesignatedContact.Type.BILLING, billingContact)); - } - if (techContact != null) { - builder.add(DesignatedContact.create(DesignatedContact.Type.TECH, techContact)); + if (includeRegistrant) { + getRegistrant().ifPresent(c -> builder.add(DesignatedContact.create(Type.REGISTRANT, c))); } + getAdminContact().ifPresent(c -> builder.add(DesignatedContact.create(Type.ADMIN, c))); + getBillingContact().ifPresent(c -> builder.add(DesignatedContact.create(Type.BILLING, c))); + getTechContact().ifPresent(c -> builder.add(DesignatedContact.create(Type.TECH, c))); return builder.build(); } @@ -652,11 +659,13 @@ public class DomainBase extends EppResource */ void setContactFields(Set contacts, boolean includeRegistrant) { // Set the individual contact fields. - billingContact = techContact = adminContact = null; + billingContact = null; + techContact = null; + adminContact = null; if (includeRegistrant) { registrantContact = null; } - HashSet contactsDiscovered = new HashSet<>(); + HashSet contactsDiscovered = new HashSet<>(); for (DesignatedContact contact : contacts) { checkArgument( !contactsDiscovered.contains(contact.getType()), @@ -687,7 +696,7 @@ public class DomainBase extends EppResource /** Predicate to determine if a given {@link DesignatedContact} is the registrant. */ static final Predicate IS_REGISTRANT = - (DesignatedContact contact) -> DesignatedContact.Type.REGISTRANT.equals(contact.type); + (DesignatedContact contact) -> Type.REGISTRANT.equals(contact.type); /** An override of {@link EppResource#asBuilder} with tighter typing. */ @Override diff --git a/core/src/main/java/google/registry/rdap/RdapJsonFormatter.java b/core/src/main/java/google/registry/rdap/RdapJsonFormatter.java index ef7fcbcf6..29ebca3ea 100644 --- a/core/src/main/java/google/registry/rdap/RdapJsonFormatter.java +++ b/core/src/main/java/google/registry/rdap/RdapJsonFormatter.java @@ -381,8 +381,11 @@ public class RdapJsonFormatter { () -> ImmutableSet.copyOf(replicaTm().loadByKeys(domain.getNameservers()).values())); // Load the registrant and other contacts and add them to the data. + ImmutableSet> contacts = domain.getReferencedContacts(); ImmutableMap, Contact> loadedContacts = - replicaTm().transact(() -> replicaTm().loadByKeysIfPresent(domain.getReferencedContacts())); + contacts.isEmpty() + ? ImmutableMap.of() + : replicaTm().transact(() -> replicaTm().loadByKeysIfPresent(contacts)); // RDAP Response Profile 2.7.1, 2.7.3 - we MUST have the contacts. 2.7.4 discusses redaction of // fields we don't want to show (as opposed to not having contacts at all) because of GDPR etc. @@ -544,7 +547,8 @@ public class RdapJsonFormatter { // TODO(mcilwain): Once the RDAP profile is fully updated for minimum registration data set, // we will want to not include non-existent contacts at all, rather than // pretending they exist and just showing REDACTED info. This is especially - // important for authorized flows, where you wouldn't expect to see redaction. + // important for authorized flows, where you wouldn't expect to see redaction + // (although no one actually has access to authorized flows yet). boolean isAuthorized = contact.isPresent() && rdapAuthorization.isAuthorizedForRegistrar( diff --git a/core/src/test/java/google/registry/flows/domain/DomainInfoFlowTest.java b/core/src/test/java/google/registry/flows/domain/DomainInfoFlowTest.java index 5137c2378..a8efbf582 100644 --- a/core/src/test/java/google/registry/flows/domain/DomainInfoFlowTest.java +++ b/core/src/test/java/google/registry/flows/domain/DomainInfoFlowTest.java @@ -235,6 +235,19 @@ class DomainInfoFlowTest extends ResourceFlowTestCase { doSuccessfulTest("domain_info_response_no_registrant.xml", false); } + @Test + void testSuccess_noContacts() throws Exception { + persistTestEntities(false); + domain = + persistResource( + domain + .asBuilder() + .setRegistrant(Optional.empty()) + .setContacts(ImmutableSet.of()) + .build()); + doSuccessfulTest("domain_info_response_no_contacts.xml", false); + } + @Test void testSuccess_clTridNotSpecified() throws Exception { setEppInput("domain_info_no_cltrid.xml"); diff --git a/core/src/test/java/google/registry/model/domain/DomainTest.java b/core/src/test/java/google/registry/model/domain/DomainTest.java index 1307bffb1..a6a22f0d7 100644 --- a/core/src/test/java/google/registry/model/domain/DomainTest.java +++ b/core/src/test/java/google/registry/model/domain/DomainTest.java @@ -1023,16 +1023,16 @@ public class DomainTest { DesignatedContact.create(Type.TECH, contact4Key)), true); assertThat(domain.getRegistrant()).hasValue(contact1Key); - assertThat(domain.getAdminContact()).isEqualTo(contact2Key); - assertThat(domain.getBillingContact()).isEqualTo(contact3Key); - assertThat(domain.getTechContact()).isEqualTo(contact4Key); + assertThat(domain.getAdminContact()).hasValue(contact2Key); + assertThat(domain.getBillingContact()).hasValue(contact3Key); + assertThat(domain.getTechContact()).hasValue(contact4Key); // Make sure everything gets nulled out. domain.setContactFields(ImmutableSet.of(), true); assertThat(domain.getRegistrant()).isEmpty(); - assertThat(domain.getAdminContact()).isNull(); - assertThat(domain.getBillingContact()).isNull(); - assertThat(domain.getTechContact()).isNull(); + assertThat(domain.getAdminContact()).isEmpty(); + assertThat(domain.getBillingContact()).isEmpty(); + assertThat(domain.getTechContact()).isEmpty(); // Make sure that changes don't affect the registrant unless requested. domain.setContactFields( @@ -1043,15 +1043,15 @@ public class DomainTest { DesignatedContact.create(Type.TECH, contact4Key)), false); assertThat(domain.getRegistrant()).isEmpty(); - assertThat(domain.getAdminContact()).isEqualTo(contact2Key); - assertThat(domain.getBillingContact()).isEqualTo(contact3Key); - assertThat(domain.getTechContact()).isEqualTo(contact4Key); + assertThat(domain.getAdminContact()).hasValue(contact2Key); + assertThat(domain.getBillingContact()).hasValue(contact3Key); + assertThat(domain.getTechContact()).hasValue(contact4Key); domain = domain.asBuilder().setRegistrant(Optional.of(contact1Key)).build(); domain.setContactFields(ImmutableSet.of(), false); assertThat(domain.getRegistrant()).hasValue(contact1Key); - assertThat(domain.getAdminContact()).isNull(); - assertThat(domain.getBillingContact()).isNull(); - assertThat(domain.getTechContact()).isNull(); + assertThat(domain.getAdminContact()).isEmpty(); + assertThat(domain.getBillingContact()).isEmpty(); + assertThat(domain.getTechContact()).isEmpty(); } @Test diff --git a/core/src/test/java/google/registry/rdap/RdapDomainActionTest.java b/core/src/test/java/google/registry/rdap/RdapDomainActionTest.java index b834fb0a4..5ad7b262e 100644 --- a/core/src/test/java/google/registry/rdap/RdapDomainActionTest.java +++ b/core/src/test/java/google/registry/rdap/RdapDomainActionTest.java @@ -27,6 +27,7 @@ import static google.registry.testing.FullFieldsTestEntityHelper.makeRegistrarPo import static google.registry.testing.GsonSubject.assertAboutJson; import static org.mockito.Mockito.verify; +import com.google.common.collect.ImmutableSet; import com.google.gson.JsonObject; import google.registry.model.contact.Contact; import google.registry.model.domain.Domain; @@ -292,6 +293,20 @@ class RdapDomainActionTest extends RdapActionBaseTestCase { assertProperResponseForCatLol("cat.lol", "rdap_domain_no_contacts_with_remark.json"); } + @Test + void testValidDomain_notLoggedIn_contactsShowRedacted_whenNoContactsExist() { + // Even though the domain has no contacts, it still shows a full set of REDACTED fields through + // RDAP. + persistResource( + loadByForeignKey(Domain.class, "cat.lol", clock.nowUtc()) + .get() + .asBuilder() + .setRegistrant(Optional.empty()) + .setContacts(ImmutableSet.of()) + .build()); + assertProperResponseForCatLol("cat.lol", "rdap_domain_no_contacts_exist_with_remark.json"); + } + @Test void testValidDomain_loggedInAsOtherRegistrar_noContacts() { login("idnregistrar"); diff --git a/core/src/test/java/google/registry/whois/DomainWhoisResponseTest.java b/core/src/test/java/google/registry/whois/DomainWhoisResponseTest.java index 54f334fe1..9bf33cec5 100644 --- a/core/src/test/java/google/registry/whois/DomainWhoisResponseTest.java +++ b/core/src/test/java/google/registry/whois/DomainWhoisResponseTest.java @@ -307,6 +307,25 @@ class DomainWhoisResponseTest { .isEqualTo(WhoisResponseResults.create(loadFile("whois_domain_no_registrant.txt"), 1)); } + @Test + void getPlainTextOutputTest_noContacts() { + DomainWhoisResponse domainWhoisResponse = + new DomainWhoisResponse( + domain + .asBuilder() + .setRegistrant(Optional.empty()) + .setContacts(ImmutableSet.of()) + .build(), + false, + "Please contact registrar", + clock.nowUtc()); + assertThat( + domainWhoisResponse.getResponse( + false, + "Doodle Disclaimer\nI exist so that carriage return\nin disclaimer can be tested.")) + .isEqualTo(WhoisResponseResults.create(loadFile("whois_domain_no_contacts.txt"), 1)); + } + @Test void getPlainTextOutputTest_registrarAbuseInfoMissing() { persistResource(abuseContact.asBuilder().setVisibleInDomainWhoisAsAbuse(false).build()); diff --git a/core/src/test/resources/google/registry/flows/domain/domain_info_response_no_contacts.xml b/core/src/test/resources/google/registry/flows/domain/domain_info_response_no_contacts.xml new file mode 100644 index 000000000..5b64419b8 --- /dev/null +++ b/core/src/test/resources/google/registry/flows/domain/domain_info_response_no_contacts.xml @@ -0,0 +1,35 @@ + + + + Command completed successfully + + + + example.tld + %ROID% + + + ns1.example.tld + ns1.example.net + + ns1.example.tld + ns2.example.tld + NewRegistrar + TheRegistrar + 1999-04-03T22:00:00.0Z + NewRegistrar + 1999-12-03T09:00:00.0Z + 2005-04-03T22:00:00.0Z + 2000-04-08T09:00:00.0Z + + 2fooBAR + + + + + ABC-12345 + server-trid + + + diff --git a/core/src/test/resources/google/registry/rdap/rdap_domain_no_contacts_exist_with_remark.json b/core/src/test/resources/google/registry/rdap/rdap_domain_no_contacts_exist_with_remark.json new file mode 100644 index 000000000..dac53076c --- /dev/null +++ b/core/src/test/resources/google/registry/rdap/rdap_domain_no_contacts_exist_with_remark.json @@ -0,0 +1,263 @@ +{ + "rdapConformance": [ + "rdap_level_0", + "icann_rdap_response_profile_0", + "icann_rdap_technical_implementation_guide_0" + ], + "objectClassName": "domain", + "handle": "%DOMAIN_HANDLE_1%", + "ldhName": "%DOMAIN_PUNYCODE_NAME_1%", + "status": [ + "client delete prohibited", + "client renew prohibited", + "client transfer prohibited", + "server update prohibited" + ], + "links": [ + { + "href": "https://example.tld/rdap/domain/%DOMAIN_PUNYCODE_NAME_1%", + "type": "application/rdap+json", + "rel": "self" + }, + { + "href": "https://rdap.example.com/withSlash/domain/%DOMAIN_PUNYCODE_NAME_1%", + "type": "application/rdap+json", + "rel": "related" + }, + { + "href": "https://rdap.example.com/withoutSlash/domain/%DOMAIN_PUNYCODE_NAME_1%", + "type": "application/rdap+json", + "rel": "related" + } + ], + "events": [ + { + "eventAction": "registration", + "eventActor": "TheRegistrar", + "eventDate": "1997-01-01T00:00:00.000Z" + }, + { + "eventAction": "expiration", + "eventDate": "2110-10-08T00:44:59.000Z" + }, + { + "eventAction": "last update of RDAP database", + "eventDate": "2000-01-01T00:00:00.000Z" + }, + { + "eventAction": "last changed", + "eventDate": "2009-05-29T20:13:00.000Z" + } + ], + "nameservers": [ + { + "objectClassName": "nameserver", + "handle": "%NAMESERVER_HANDLE_1%", + "ldhName": "%NAMESERVER_NAME_1%", + "links": [ + { + "href": "https://example.tld/rdap/nameserver/%NAMESERVER_NAME_1%", + "type": "application/rdap+json", + "rel": "self" + } + ], + "remarks": [ + { + "title": "Incomplete Data", + "type": "object truncated due to unexplainable reasons", + "description": ["Summary data only. For complete data, send a specific query for the object."] + } + ] + }, + { + "objectClassName": "nameserver", + "handle": "%NAMESERVER_HANDLE_2%", + "ldhName": "%NAMESERVER_NAME_2%", + "links": [ + { + "href": "https://example.tld/rdap/nameserver/%NAMESERVER_NAME_2%", + "type": "application/rdap+json", + "rel": "self" + } + ], + "remarks": [ + { + "title": "Incomplete Data", + "type": "object truncated due to unexplainable reasons", + "description": ["Summary data only. For complete data, send a specific query for the object."] + } + ] + } + ], + "secureDNS" : { + "delegationSigned": true, + "zoneSigned":true, + "dsData":[ + {"algorithm":2,"digest":"DEADFACE","digestType":3,"keyTag":1} + ] + }, + "entities": [ + { + "objectClassName" : "entity", + "handle" : "1", + "roles" : ["registrar"], + "links" : [ + { + "rel" : "self", + "href" : "https://example.tld/rdap/entity/1", + "type" : "application/rdap+json" + } + ], + "publicIds" : [ + { + "type" : "IANA Registrar ID", + "identifier" : "1" + } + ], + "vcardArray" : [ + "vcard", + [ + ["version", {}, "text", "4.0"], + ["fn", {}, "text", "%REGISTRAR_FULL_NAME_1%"] + ] + ], + "entities" : [ + { + "objectClassName":"entity", + "roles":["abuse"], + "status":["active"], + "vcardArray": [ + "vcard", + [ + ["version",{},"text","4.0"], + ["fn",{},"text","Jake Doe"], + ["tel",{"type":["voice"]},"uri","tel:+1.2125551216"], + ["tel",{"type":["fax"]},"uri","tel:+1.2125551216"], + ["email",{},"text","jakedoe@example.com"] + ] + ] + } + ], + "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":"entity", + "handle":"", + "remarks":[ + { + "title":"REDACTED FOR PRIVACY", + "type":"object redacted due to authorization", + "description":[ + "Some of the data in this object has been removed.", + "Contact personal data is visible only to the owning registrar." + ], + "links":[ + { + "href":"https://github.com/google/nomulus/blob/master/docs/rdap.md#authentication", + "rel":"alternate", + "type":"text/html" + } + ] + }, + { + "title":"EMAIL REDACTED FOR PRIVACY", + "type":"object redacted due to authorization", + "description":[ + "Please query the RDDS service of the Registrar of Record identifies in this output for information on how to contact the Registrant of the queried domain name." + ] + } + ], + "roles":["registrant"], + "vcardArray":[ + "vcard", + [ + ["version", {}, "text", "4.0"], + ["fn", {}, "text", ""] + ] + ] + }, + + { + "objectClassName": "entity", + "handle": "", + "roles":["administrative"], + "remarks": [ + { + "title":"REDACTED FOR PRIVACY", + "type":"object redacted due to authorization", + "description": [ + "Some of the data in this object has been removed.", + "Contact personal data is visible only to the owning registrar." + ], + "links":[ + { + "href":"https://github.com/google/nomulus/blob/master/docs/rdap.md#authentication", + "rel":"alternate", + "type":"text/html" + } + ] + }, + { + "title":"EMAIL REDACTED FOR PRIVACY", + "type":"object redacted due to authorization", + "description": [ + "Please query the RDDS service of the Registrar of Record identifies in this output for information on how to contact the Registrant of the queried domain name." + ] + } + ], + "vcardArray":[ + "vcard", + [ + ["version", {}, "text", "4.0"], + ["fn", {}, "text", ""] + ] + ] + }, + + { + "objectClassName":"entity", + "handle":"", + "remarks":[ + { + "title":"REDACTED FOR PRIVACY", + "type":"object redacted due to authorization", + "description":[ + "Some of the data in this object has been removed.", + "Contact personal data is visible only to the owning registrar." + ], + "links":[ + { + "href":"https://github.com/google/nomulus/blob/master/docs/rdap.md#authentication", + "rel":"alternate", + "type":"text/html" + } + ] + }, + { + "description":[ + "Please query the RDDS service of the Registrar of Record identifies in this output for information on how to contact the Registrant of the queried domain name." + ], + "title":"EMAIL REDACTED FOR PRIVACY", + "type":"object redacted due to authorization" + } + ], + "roles": ["technical"], + "vcardArray": [ + "vcard", + [ + ["version", {}, "text", "4.0"], + ["fn", {}, "text", ""] + ] + ] + } + ] +} diff --git a/core/src/test/resources/google/registry/whois/whois_domain_no_contacts.txt b/core/src/test/resources/google/registry/whois/whois_domain_no_contacts.txt new file mode 100644 index 000000000..b839efcfe --- /dev/null +++ b/core/src/test/resources/google/registry/whois/whois_domain_no_contacts.txt @@ -0,0 +1,28 @@ +Domain Name: example.tld +Registry Domain ID: 3-TLD +Registrar WHOIS Server: whois.nic.fakewhois.example +Registrar URL: http://my.fake.url +Updated Date: 2009-05-29T20:13:00Z +Creation Date: 2000-10-08T00:45:00Z +Registry Expiry Date: 2010-10-08T00:44:59Z +Registrar: New Registrar +Registrar IANA ID: 5555555 +Registrar Abuse Contact Email: jakedoe@theregistrar.com +Registrar Abuse Contact Phone: +1.2125551216 +Domain Status: addPeriod https://icann.org/epp#addPeriod +Domain Status: clientDeleteProhibited https://icann.org/epp#clientDeleteProhibited +Domain Status: clientRenewProhibited https://icann.org/epp#clientRenewProhibited +Domain Status: clientTransferProhibited https://icann.org/epp#clientTransferProhibited +Domain Status: serverUpdateProhibited https://icann.org/epp#serverUpdateProhibited +Domain Status: transferPeriod https://icann.org/epp#transferPeriod +Name Server: ns01.exampleregistrar.tld +Name Server: ns02.exampleregistrar.tld +DNSSEC: signedDelegation +URL of the ICANN Whois Inaccuracy Complaint Form: https://www.icann.org/wicf/ +>>> Last update of WHOIS database: 2009-05-29T20:15:00Z <<< + +For more information on Whois status codes, please visit https://icann.org/epp + +Doodle Disclaimer +I exist so that carriage return +in disclaimer can be tested.