From bdc41edd34b807df1930b942e2cddea4edd5f5a2 Mon Sep 17 00:00:00 2001 From: guyben Date: Mon, 6 May 2019 20:25:56 -0700 Subject: [PATCH] Reimplement the RDAP Json creation using Jsonables Currently we try to reimplemnet the same behavior of the existing code as much as possible. We only fix issues that go against the RFC7483, but we don't yet update the code to follow the latest (15feb19) RDAP Response Profile. That will require a much bigger change especially for the test files, so it'll wait for a followup CL. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=246948018 --- .../registry/config/RdapNoticeDescriptor.java | 52 - .../registry/config/RegistryConfig.java | 59 - .../registry/rdap/AbstractJsonableObject.java | 2 - java/google/registry/rdap/RdapActionBase.java | 45 +- .../registry/rdap/RdapAutnumAction.java | 5 +- .../registry/rdap/RdapDataStructures.java | 487 ++++++++ .../registry/rdap/RdapDomainAction.java | 7 +- .../registry/rdap/RdapDomainSearchAction.java | 97 +- .../registry/rdap/RdapEntityAction.java | 8 +- .../registry/rdap/RdapEntitySearchAction.java | 75 +- java/google/registry/rdap/RdapHelpAction.java | 61 +- .../rdap/RdapIcannStandardInformation.java | 186 ++- java/google/registry/rdap/RdapIpAction.java | 5 +- .../registry/rdap/RdapJsonFormatter.java | 1011 +++++------------ .../registry/rdap/RdapNameserverAction.java | 7 +- .../rdap/RdapNameserverSearchAction.java | 64 +- .../registry/rdap/RdapObjectClasses.java | 442 +++++++ .../registry/rdap/RdapSearchActionBase.java | 19 +- .../registry/rdap/RdapSearchResults.java | 163 ++- .../registry/request/RequestModule.java | 6 + .../registry/rdap/RdapActionBaseTest.java | 53 +- .../registry/rdap/RdapActionBaseTestCase.java | 74 +- .../registry/rdap/RdapDataStructuresTest.java | 177 +++ .../registry/rdap/RdapDomainActionTest.java | 73 +- .../rdap/RdapDomainSearchActionTest.java | 147 ++- .../registry/rdap/RdapEntityActionTest.java | 81 +- .../rdap/RdapEntitySearchActionTest.java | 99 +- .../registry/rdap/RdapHelpActionTest.java | 21 +- .../registry/rdap/RdapJsonFormatterTest.java | 479 ++++---- .../rdap/RdapNameserverActionTest.java | 18 +- .../rdap/RdapNameserverSearchActionTest.java | 141 +-- .../google/registry/rdap/RdapTestHelper.java | 143 +-- .../testdata/rdap_associated_contact.json | 3 + ...p_associated_contact_no_personal_data.json | 21 + .../registry/rdap/testdata/rdap_contact.json | 3 + .../rdap/testdata/rdap_contact_deleted.json | 3 + ..._contact_no_personal_data_with_remark.json | 7 +- .../registry/rdap/testdata/rdap_domain.json | 3 + .../rdap/testdata/rdap_domain_cat2.json | 3 + .../rdap/testdata/rdap_domain_deleted.json | 3 + .../testdata/rdap_domain_no_contacts.json | 262 ----- .../rdap_domain_no_contacts_with_remark.json | 3 + .../rdap/testdata/rdap_domain_unicode.json | 3 + ...omain_unicode_no_contacts_with_remark.json | 3 + .../registry/rdap/testdata/rdap_error.json | 36 + .../rdap/testdata/rdap_error_400.json | 11 - .../rdap/testdata/rdap_error_404.json | 11 - .../rdap/testdata/rdap_error_422.json | 11 - .../rdap/testdata/rdap_error_501.json | 11 - .../rdap/testdata/rdap_formatted_output.json | 14 - .../rdap/testdata/rdap_help_index.json | 2 +- .../registry/rdap/testdata/rdap_host.json | 3 + .../rdap/testdata/rdap_host_external.json | 3 + .../rdap/testdata/rdap_host_linked.json | 3 + .../rdap/testdata/rdap_host_unicode.json | 3 + .../rdap/testdata/rdap_registrar.json | 3 + .../rdap/testdata/rdap_registrar_test.json | 3 + .../rdap/testdata/rdap_truncated_hosts.json | 2 +- .../testdata/rdap_unformatted_output.json | 2 +- .../rdap/testdata/rdapjson_admincontact.json | 4 +- .../rdap/testdata/rdapjson_domain_full.json | 38 +- .../testdata/rdapjson_domain_logged_out.json | 24 +- .../rdapjson_domain_no_nameservers.json | 20 +- .../testdata/rdapjson_domain_summary.json | 4 +- .../rdap/testdata/rdapjson_error.json | 6 +- .../rdap/testdata/rdapjson_host_both.json | 8 +- .../testdata/rdapjson_host_both_summary.json | 8 +- .../rdap/testdata/rdapjson_host_ipv4.json | 8 +- .../rdap/testdata/rdapjson_host_ipv6.json | 8 +- .../testdata/rdapjson_host_no_addresses.json | 8 +- .../testdata/rdapjson_host_not_linked.json | 8 +- .../rdapjson_host_pending_transfer.json | 8 +- .../rdapjson_notice_alternate_link.json | 4 +- .../testdata/rdapjson_notice_self_link.json | 4 +- .../rdap/testdata/rdapjson_registrant.json | 4 +- .../rdapjson_registrant_logged_out.json | 8 +- .../testdata/rdapjson_registrant_summary.json | 4 +- ...dapjson_registrant_summary_logged_out.json | 8 +- .../rdap/testdata/rdapjson_registrar.json | 10 +- .../testdata/rdapjson_registrar_summary.json | 4 +- .../testdata/rdapjson_rolelesscontact.json | 4 +- .../rdap/testdata/rdapjson_techcontact.json | 4 +- .../rdap/testdata/rdapjson_toplevel.json | 2 +- .../testdata/rdapjson_toplevel_domain.json | 2 +- .../testdata/rdapjson_unlinkedcontact.json | 4 +- 85 files changed, 2589 insertions(+), 2367 deletions(-) delete mode 100644 java/google/registry/config/RdapNoticeDescriptor.java create mode 100644 java/google/registry/rdap/RdapDataStructures.java create mode 100644 java/google/registry/rdap/RdapObjectClasses.java create mode 100644 javatests/google/registry/rdap/RdapDataStructuresTest.java delete mode 100644 javatests/google/registry/rdap/testdata/rdap_domain_no_contacts.json create mode 100644 javatests/google/registry/rdap/testdata/rdap_error.json delete mode 100644 javatests/google/registry/rdap/testdata/rdap_error_400.json delete mode 100644 javatests/google/registry/rdap/testdata/rdap_error_404.json delete mode 100644 javatests/google/registry/rdap/testdata/rdap_error_422.json delete mode 100644 javatests/google/registry/rdap/testdata/rdap_error_501.json delete mode 100644 javatests/google/registry/rdap/testdata/rdap_formatted_output.json diff --git a/java/google/registry/config/RdapNoticeDescriptor.java b/java/google/registry/config/RdapNoticeDescriptor.java deleted file mode 100644 index 1686d0f7e..000000000 --- a/java/google/registry/config/RdapNoticeDescriptor.java +++ /dev/null @@ -1,52 +0,0 @@ -// 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.config; - -import com.google.auto.value.AutoValue; -import com.google.common.collect.ImmutableList; -import javax.annotation.Nullable; - -/** - * AutoValue class describing an RDAP Notice object. - * - *

This is used for injecting RDAP help pages. - */ -@AutoValue -public abstract class RdapNoticeDescriptor { - - public static Builder builder() { - return new AutoValue_RdapNoticeDescriptor.Builder(); - } - - @Nullable public abstract String getTitle(); - public abstract ImmutableList getDescription(); - @Nullable public abstract String getTypeString(); - @Nullable public abstract String getLinkValueSuffix(); - @Nullable public abstract String getLinkHrefUrlString(); - - /** Builder class for {@link RdapNoticeDescriptor}. */ - @AutoValue.Builder - public abstract static class Builder { - - public abstract Builder setTitle(@Nullable String title); - public abstract Builder setDescription(Iterable description); - public abstract Builder setTypeString(@Nullable String typeString); - public abstract Builder setLinkValueSuffix(@Nullable String linkValueSuffix); - public abstract Builder setLinkHrefUrlString(@Nullable String linkHrefUrlString); - - public abstract RdapNoticeDescriptor build(); - } -} - diff --git a/java/google/registry/config/RegistryConfig.java b/java/google/registry/config/RegistryConfig.java index 641f1dc5d..edac2b415 100644 --- a/java/google/registry/config/RegistryConfig.java +++ b/java/google/registry/config/RegistryConfig.java @@ -26,7 +26,6 @@ import com.google.common.base.Splitter; import com.google.common.base.Strings; import com.google.common.base.Supplier; import com.google.common.collect.ImmutableList; -import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import dagger.Module; import dagger.Provides; @@ -1267,19 +1266,6 @@ public final class RegistryConfig { return ImmutableList.copyOf(config.credentialOAuth.localCredentialOauthScopes); } - /** - * Returns the help path for the RDAP terms of service. - * - *

Make sure that this path is equal to the key of the entry in the RDAP help map containing - * the terms of service. The ICANN operational profile requires that the TOS be included in all - * responses, and this string is used to find the TOS in the help map. - */ - @Provides - @Config("rdapTosPath") - public static String provideRdapTosPath() { - return "/tos"; - } - /** OAuth client ID used by the nomulus tool. */ @Provides @Config("toolsClientId") @@ -1311,51 +1297,6 @@ public final class RegistryConfig { public static String provideRdapTosStaticUrl(RegistryConfigSettings config) { return config.registryPolicy.rdapTosStaticUrl; } - - /** - * Returns the help text to be used by RDAP. - * - *

Make sure that the map entry for the terms of service use the same key as specified in - * rdapTosPath above. - */ - @Singleton - @Provides - @Config("rdapHelpMap") - public static ImmutableMap provideRdapHelpMap( - @Config("rdapTos") ImmutableList rdapTos, - @Config("rdapTosStaticUrl") @Nullable String rdapTosStaticUrl) { - return new ImmutableMap.Builder() - .put( - "/", - RdapNoticeDescriptor.builder() - .setTitle("RDAP Help") - .setDescription( - ImmutableList.of( - "domain/XXXX", - "nameserver/XXXX", - "entity/XXXX", - "domains?name=XXXX", - "domains?nsLdhName=XXXX", - "domains?nsIp=XXXX", - "nameservers?name=XXXX", - "nameservers?ip=XXXX", - "entities?fn=XXXX", - "entities?handle=XXXX", - "help/XXXX")) - .setLinkValueSuffix("help/") - .setLinkHrefUrlString( - "https://github.com/google/nomulus/blob/master/docs/rdap.md") - .build()) - .put( - "/tos", - RdapNoticeDescriptor.builder() - .setTitle("RDAP Terms of Service") - .setDescription(rdapTos) - .setLinkValueSuffix("help/tos") - .setLinkHrefUrlString(rdapTosStaticUrl) - .build()) - .build(); - } } /** diff --git a/java/google/registry/rdap/AbstractJsonableObject.java b/java/google/registry/rdap/AbstractJsonableObject.java index b1bf3722a..44bbb769e 100644 --- a/java/google/registry/rdap/AbstractJsonableObject.java +++ b/java/google/registry/rdap/AbstractJsonableObject.java @@ -360,8 +360,6 @@ abstract class AbstractJsonableObject implements Jsonable { * *

If not empty - the resulting list is the allowed names. If the name ends with [], it means * the class is an element in a array with this name. - * - *

A name of "*" means this is allowed to merge. */ static Optional> getNameRestriction(Class clazz) { // Find the first superclass that has an RestrictJsonNames annotation. diff --git a/java/google/registry/rdap/RdapActionBase.java b/java/google/registry/rdap/RdapActionBase.java index cccc5a654..8028543b0 100644 --- a/java/google/registry/rdap/RdapActionBase.java +++ b/java/google/registry/rdap/RdapActionBase.java @@ -25,11 +25,11 @@ import static javax.servlet.http.HttpServletResponse.SC_BAD_REQUEST; import static javax.servlet.http.HttpServletResponse.SC_INTERNAL_SERVER_ERROR; import static javax.servlet.http.HttpServletResponse.SC_OK; -import com.google.api.client.json.jackson2.JacksonFactory; -import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import com.google.common.flogger.FluentLogger; import com.google.common.net.MediaType; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; import com.google.re2j.Pattern; import com.googlecode.objectify.Key; import com.googlecode.objectify.cmd.Query; @@ -38,9 +38,12 @@ import google.registry.model.EppResource; import google.registry.model.registrar.Registrar; import google.registry.rdap.RdapMetrics.EndpointType; import google.registry.rdap.RdapMetrics.WildcardType; +import google.registry.rdap.RdapObjectClasses.ErrorResponse; +import google.registry.rdap.RdapObjectClasses.ReplyPayloadBase; +import google.registry.rdap.RdapObjectClasses.TopLevelReplyObject; +import google.registry.rdap.RdapSearchResults.BaseSearchResponse; import google.registry.rdap.RdapSearchResults.IncompletenessWarningType; import google.registry.request.Action; -import google.registry.request.FullServletPath; import google.registry.request.HttpException; import google.registry.request.HttpException.UnprocessableEntityException; import google.registry.request.Parameter; @@ -51,7 +54,6 @@ import google.registry.request.auth.AuthResult; import google.registry.request.auth.AuthenticatedRegistrarAccessor; import google.registry.request.auth.UserAuthInfo; import google.registry.util.Clock; -import java.io.IOException; import java.net.URI; import java.net.URISyntaxException; import java.util.ArrayList; @@ -60,7 +62,6 @@ import java.util.Optional; import javax.annotation.Nullable; import javax.inject.Inject; import org.joda.time.DateTime; -import org.json.simple.JSONValue; /** * Base RDAP (new WHOIS) action for all requests. @@ -92,7 +93,6 @@ public abstract class RdapActionBase implements Runnable { @Inject Clock clock; @Inject @RequestMethod Action.Method requestMethod; @Inject @RequestPath String requestPath; - @Inject @FullServletPath String fullServletPath; @Inject AuthResult authResult; @Inject AuthenticatedRegistrarAccessor registrarAccessor; @Inject RdapJsonFormatter rdapJsonFormatter; @@ -138,7 +138,7 @@ public abstract class RdapActionBase implements Runnable { * expensive task required to create the map which will never result in a request failure. * @return A map (probably containing nested maps and lists) with the final JSON response data. */ - abstract ImmutableMap getJsonObjectForResource( + abstract ReplyPayloadBase getJsonObjectForResource( String pathSearchString, boolean isHeadRequest); @Override @@ -159,12 +159,16 @@ public abstract class RdapActionBase implements Runnable { checkArgument( pathProper.startsWith(getActionPath()), "%s doesn't start with %s", pathProper, getActionPath()); - ImmutableMap rdapJson = + ReplyPayloadBase replyObject = getJsonObjectForResource( pathProper.substring(getActionPath().length()), requestMethod == Action.Method.HEAD); + if (replyObject instanceof BaseSearchResponse) { + metricInformationBuilder.setIncompletenessWarningType( + ((BaseSearchResponse) replyObject).incompletenessWarningType()); + } response.setStatus(SC_OK); response.setContentType(RESPONSE_MEDIA_TYPE); - setPayload(rdapJson); + setPayload(replyObject); metricInformationBuilder.setStatusCode(SC_OK); } catch (HttpException e) { setError(e.getResponseCode(), e.getResponseCodeString(), e.getMessage()); @@ -182,26 +186,29 @@ public abstract class RdapActionBase implements Runnable { response.setStatus(status); response.setContentType(RESPONSE_MEDIA_TYPE); try { - setPayload(rdapJsonFormatter.makeError(status, title, description)); + setPayload(ErrorResponse.create(status, title, description)); } catch (Exception ex) { + logger.atSevere().withCause(ex).log("Failed to create an error response."); response.setPayload(""); } } - void setPayload(ImmutableMap rdapJson) { + void setPayload(ReplyPayloadBase replyObject) { if (requestMethod == Action.Method.HEAD) { return; } + + GsonBuilder gsonBuilder = new GsonBuilder(); + gsonBuilder.disableHtmlEscaping(); if (formatOutputParam.orElse(false)) { - try { - response.setPayload(new JacksonFactory().toPrettyString(rdapJson)); - return; - } catch (IOException e) { - logger.atWarning().withCause(e).log( - "Unable to pretty-print RDAP JSON response; falling back to unformatted output."); - } + gsonBuilder.setPrettyPrinting(); } - response.setPayload(JSONValue.toJSONString(rdapJson)); + Gson gson = gsonBuilder.create(); + + TopLevelReplyObject topLevelObject = + TopLevelReplyObject.create(replyObject, rdapJsonFormatter.createTosNotice()); + + response.setPayload(gson.toJson(topLevelObject.toJson())); } RdapAuthorization getAuthorization() { diff --git a/java/google/registry/rdap/RdapAutnumAction.java b/java/google/registry/rdap/RdapAutnumAction.java index c56c6615f..e55b6b730 100644 --- a/java/google/registry/rdap/RdapAutnumAction.java +++ b/java/google/registry/rdap/RdapAutnumAction.java @@ -17,8 +17,8 @@ package google.registry.rdap; import static google.registry.request.Action.Method.GET; import static google.registry.request.Action.Method.HEAD; -import com.google.common.collect.ImmutableMap; import google.registry.rdap.RdapMetrics.EndpointType; +import google.registry.rdap.RdapObjectClasses.ReplyPayloadBase; import google.registry.request.Action; import google.registry.request.HttpException.NotImplementedException; import google.registry.request.auth.Auth; @@ -43,8 +43,7 @@ public class RdapAutnumAction extends RdapActionBase { } @Override - public ImmutableMap getJsonObjectForResource( - String pathSearchString, boolean isHeadRequest) { + public ReplyPayloadBase getJsonObjectForResource(String pathSearchString, boolean isHeadRequest) { throw new NotImplementedException("Domain Name Registry information only"); } } diff --git a/java/google/registry/rdap/RdapDataStructures.java b/java/google/registry/rdap/RdapDataStructures.java new file mode 100644 index 000000000..e0eada897 --- /dev/null +++ b/java/google/registry/rdap/RdapDataStructures.java @@ -0,0 +1,487 @@ +// Copyright 2019 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 com.google.auto.value.AutoValue; +import com.google.common.collect.ImmutableList; +import com.google.gson.JsonArray; +import com.google.gson.JsonPrimitive; +import google.registry.rdap.AbstractJsonableObject.RestrictJsonNames; +import java.util.Optional; +import org.joda.time.DateTime; + +/** + * Data Structures defined in RFC7483 section 4. + */ +final class RdapDataStructures { + + private RdapDataStructures() {} + + /** + * RDAP conformance defined in 4.1 of RFC7483. + */ + @RestrictJsonNames("rdapConformance") + static final class RdapConformance implements Jsonable { + + static final RdapConformance INSTANCE = new RdapConformance(); + + private RdapConformance() {} + + @Override + public JsonArray toJson() { + JsonArray jsonArray = new JsonArray(); + // Conformance to RFC7483 + // TODO(b/127490882) check if we need to Add back the rdap_level_0 string, as I think that + // just means we conform to the RFC, which we do + // jsonArray.add("rdap_level_0"); + + // Conformance to the RDAP Response Profile V2.1 + // (see section 1.3) + jsonArray.add("icann_rdap_response_profile_0"); + return jsonArray; + } + } + + /** + * Links defined in 4.2 of RFC7483. + */ + @RestrictJsonNames("links[]") + @AutoValue + abstract static class Link extends AbstractJsonableObject { + @JsonableElement abstract String href(); + + @JsonableElement abstract Optional rel(); + @JsonableElement abstract Optional hreflang(); + @JsonableElement abstract Optional title(); + @JsonableElement abstract Optional media(); + @JsonableElement abstract Optional type(); + @JsonableElement abstract Optional value(); + + static Builder builder() { + return new AutoValue_RdapDataStructures_Link.Builder(); + } + + @AutoValue.Builder + abstract static class Builder { + abstract Builder setHref(String href); + abstract Builder setRel(String rel); + abstract Builder setHreflang(String hrefLang); + abstract Builder setTitle(String title); + abstract Builder setMedia(String media); + abstract Builder setType(String type); + abstract Builder setValue(String value); + + abstract Link build(); + } + } + + /** + * Notices and Remarks defined in 4.3 of RFC7483. + * + *

Each has an optional "type" denoting a registered type string defined in 10.2.1. The type is + * defined as common to both Notices and Remarks, but each item is only appropriate to one of + * them. So we will divide all the "types" from the RFC to two enums - one for Notices and one for + * Remarks. + */ + private abstract static class NoticeOrRemark extends AbstractJsonableObject { + @JsonableElement abstract Optional title(); + @JsonableElement abstract ImmutableList description(); + @JsonableElement abstract ImmutableList links(); + + abstract static class Builder> { + abstract B setTitle(String title); + abstract B setDescription(ImmutableList description); + abstract B setDescription(String... description); + abstract ImmutableList.Builder linksBuilder(); + + @SuppressWarnings("unchecked") + B addLink(Link link) { + linksBuilder().add(link); + return (B) this; + } + } + } + + /** + * Notices defined in 4.3 of RFC7483. + * + *

A notice denotes information about the service itself or the entire response, and hence will + * only be in the top-most object. + */ + @AutoValue + @RestrictJsonNames("notices[]") + abstract static class Notice extends NoticeOrRemark { + + /** + * Notice and Remark Type are defined in 10.2.1 of RFC7483. + * + *

We only keep the "service or entire response" values for Notice.Type. + */ + @RestrictJsonNames("type") + enum Type implements Jsonable { + RESULT_TRUNCATED_AUTHORIZATION("result set truncated due to authorization"), + RESULT_TRUNCATED_LOAD("result set truncated due to excessive load"), + RESULT_TRUNCATED_UNEXPLAINABLE("result set truncated due to unexplainable reasons"); + + + private final String rfc7483String; + + Type(String rfc7483String) { + this.rfc7483String = rfc7483String; + } + + @Override + public JsonPrimitive toJson() { + return new JsonPrimitive(rfc7483String); + } + } + + @JsonableElement + abstract Optional type(); + + static Builder builder() { + return new AutoValue_RdapDataStructures_Notice.Builder(); + } + + @AutoValue.Builder + abstract static class Builder extends NoticeOrRemark.Builder { + abstract Builder setType(Notice.Type type); + + abstract Notice build(); + } + } + + /** + * Remarks defined in 4.3 of RFC7483. + * + *

A remark denotes information about the specific object, and hence each object has its own + * "remarks" array. + */ + @AutoValue + @RestrictJsonNames("remarks[]") + abstract static class Remark extends NoticeOrRemark { + + /** + * Notice and Remark Type are defined in 10.2.1 of RFC7483. + * + *

We only keep the "specific object" values for Remark.Type. + */ + @RestrictJsonNames("type") + enum Type implements Jsonable { + OBJECT_TRUNCATED_AUTHORIZATION("object truncated due to authorization"), + OBJECT_TRUNCATED_LOAD("object truncated due to excessive load"), + OBJECT_TRUNCATED_UNEXPLAINABLE("object truncated due to unexplainable reasons"), + // This one isn't in the "RDAP JSON Values registry", but it's in the RDAP Response Profile, + // so I'm adding it here, but we have to ask them about it... + OBJECT_REDACTED_AUTHORIZATION("object redacted due to authorization"); + + private final String rfc7483String; + + Type(String rfc7483String) { + this.rfc7483String = rfc7483String; + } + + @Override + public JsonPrimitive toJson() { + return new JsonPrimitive(rfc7483String); + } + } + + @JsonableElement + abstract Optional type(); + + static Builder builder() { + return new AutoValue_RdapDataStructures_Remark.Builder(); + } + + @AutoValue.Builder + abstract static class Builder extends NoticeOrRemark.Builder { + abstract Builder setType(Remark.Type type); + + abstract Remark build(); + } + } + + /** + * Language Identifier defined in 4.4 of RFC7483. + * + * The allowed values are described in RFC5646. + */ + @RestrictJsonNames("lang") + enum LanguageIdentifier implements Jsonable { + EN("en"); + + private final String languageIdentifier; + + LanguageIdentifier(String languageIdentifier) { + this.languageIdentifier = languageIdentifier; + } + + @Override + public JsonPrimitive toJson() { + return new JsonPrimitive(languageIdentifier); + } + } + + /** + * Events defined in 4.5 of RFC7483. + * + *

There's a type of Event that must not have the "eventActor" (see 5.1), so we create 2 + * versions - one with and one without. + */ + private abstract static class EventBase extends AbstractJsonableObject { + @JsonableElement abstract EventAction eventAction(); + @JsonableElement abstract DateTime eventDate(); + @JsonableElement abstract ImmutableList links(); + + + abstract static class Builder> { + abstract B setEventAction(EventAction eventAction); + abstract B setEventDate(DateTime eventDate); + abstract ImmutableList.Builder linksBuilder(); + + @SuppressWarnings("unchecked") + B addLink(Link link) { + linksBuilder().add(link); + return (B) this; + } + } + } + + /** Status values for events specified in RFC 7483 § 10.2.3. */ + enum EventAction implements Jsonable { + REGISTRATION("registration"), + REREGISTRATION("reregistration"), + LAST_CHANGED("last changed"), + EXPIRATION("expiration"), + DELETION("deletion"), + REINSTANTIATION("reinstantiation"), + TRANSFER("transfer"), + LOCKED("locked"), + UNLOCKED("unlocked"), + LAST_UPDATE_OF_RDAP_DATABASE("last update of RDAP database"); + + /** Value as it appears in RDAP messages. */ + private final String rfc7483String; + + EventAction(String rfc7483String) { + this.rfc7483String = rfc7483String; + } + + String getDisplayName() { + return rfc7483String; + } + + @Override + public JsonPrimitive toJson() { + return new JsonPrimitive(rfc7483String); + } + } + + + /** + * Events defined in 4.5 of RFC7483. + * + *

There's a type of Event that MUST NOT have the "eventActor" (see 5.1), so we have this + * object to enforce that. + */ + @RestrictJsonNames("asEventActor[]") + @AutoValue + abstract static class EventWithoutActor extends EventBase { + + static Builder builder() { + return new AutoValue_RdapDataStructures_EventWithoutActor.Builder(); + } + + + @AutoValue.Builder + abstract static class Builder extends EventBase.Builder { + abstract EventWithoutActor build(); + } + } + + /** + * Events defined in 4.5 of RFC7483. + */ + @RestrictJsonNames("events[]") + @AutoValue + abstract static class Event extends EventBase { + @JsonableElement abstract Optional eventActor(); + + static Builder builder() { + return new AutoValue_RdapDataStructures_Event.Builder(); + } + + + @AutoValue.Builder + abstract static class Builder extends EventBase.Builder { + abstract Builder setEventActor(String eventActor); + abstract Event build(); + } + } + + /** + * Status defined in 4.6 of RFC7483. + * + *

This indicates the state of the registered object. + * + *

The allowed values are in section 10.2.2. + */ + @RestrictJsonNames("status[]") + enum RdapStatus implements Jsonable { + + // Status values specified in RFC 7483 § 10.2.2. + VALIDATED("validated"), + RENEW_PROHIBITED("renew prohibited"), + UPDATE_PROHIBITED("update prohibited"), + TRANSFER_PROHIBITED("transfer prohibited"), + DELETE_PROHIBITED("delete prohibited"), + PROXY("proxy"), + PRIVATE("private"), + REMOVED("removed"), + OBSCURED("obscured"), + ASSOCIATED("associated"), + ACTIVE("active"), + INACTIVE("inactive"), + LOCKED("locked"), + PENDING_CREATE("pending create"), + PENDING_RENEW("pending renew"), + PENDING_TRANSFER("pending transfer"), + PENDING_UPDATE("pending update"), + PENDING_DELETE("pending delete"), + + // Additional status values defined in + // https://tools.ietf.org/html/draft-ietf-regext-epp-rdap-status-mapping-01. + ADD_PERIOD("add period"), + AUTO_RENEW_PERIOD("auto renew period"), + CLIENT_DELETE_PROHIBITED("client delete prohibited"), + CLIENT_HOLD("client hold"), + CLIENT_RENEW_PROHIBITED("client renew prohibited"), + CLIENT_TRANSFER_PROHIBITED("client transfer prohibited"), + CLIENT_UPDATE_PROHIBITED("client update prohibited"), + PENDING_RESTORE("pending restore"), + REDEMPTION_PERIOD("redemption period"), + RENEW_PERIOD("renew period"), + SERVER_DELETE_PROHIBITED("server deleted prohibited"), + SERVER_RENEW_PROHIBITED("server renew prohibited"), + SERVER_TRANSFER_PROHIBITED("server transfer prohibited"), + SERVER_UPDATE_PROHIBITED("server update prohibited"), + SERVER_HOLD("server hold"), + TRANSFER_PERIOD("transfer period"); + + /** Value as it appears in RDAP messages. */ + private final String rfc7483String; + + RdapStatus(String rfc7483String) { + this.rfc7483String = rfc7483String; + } + + String getDisplayName() { + return rfc7483String; + } + + @Override + public JsonPrimitive toJson() { + return new JsonPrimitive(rfc7483String); + } + } + + /** + * Port 43 WHOIS Server defined in 4.7 of RFC7483. + * + *

This contains the fully qualifies host name of IP address of the WHOIS RFC3912 server where + * the containing object instance may be found. + */ + @RestrictJsonNames("port43") + @AutoValue + abstract static class Port43WhoisServer implements Jsonable { + abstract String port43(); + + @Override + public JsonPrimitive toJson() { + return new JsonPrimitive(port43()); + } + + static Port43WhoisServer create(String port43) { + return new AutoValue_RdapDataStructures_Port43WhoisServer(port43); + } + } + + /** + * Public IDs defined in 4.8 of RFC7483. + * + *

Maps a public identifier to an object class. + */ + @RestrictJsonNames("publicIds[]") + @AutoValue + abstract static class PublicId extends AbstractJsonableObject { + @RestrictJsonNames("type") + enum Type implements Jsonable { + IANA_REGISTRAR_ID("IANA Registrar ID"); + + private final String rfc7483String; + + Type(String rfc7483String) { + this.rfc7483String = rfc7483String; + } + + @Override + public JsonPrimitive toJson() { + return new JsonPrimitive(rfc7483String); + } + } + + @JsonableElement + abstract PublicId.Type type(); + + @JsonableElement abstract String identifier(); + + static PublicId create(PublicId.Type type, String identifier) { + return new AutoValue_RdapDataStructures_PublicId(type, identifier); + } + } + + /** + * Object Class Name defined in 4.7 of RFC7483. + * + *

Identifies the type of the object being processed. Is REQUIRED in all RDAP response objects, + * but not so for internal objects whose type can be inferred by their key name in the enclosing + * object. + */ + @RestrictJsonNames("objectClassName") + enum ObjectClassName implements Jsonable { + /** Defined in 5.1 of RFC7483. */ + ENTITY("entity"), + /** Defined in 5.2 of RFC7483. */ + NAMESERVER("nameserver"), + /** Defined in 5.3 of RFC7483. */ + DOMAIN("domain"), + /** Defined in 5.4 of RFC7483. Only relevant for Registrars, so isn't implemented here. */ + IP_NETWORK("ip network"), + /** Defined in 5.5 of RFC7483. Only relevant for Registrars, so isn't implemented here. */ + AUTONOMUS_SYSTEM("autnum"); + + private final String className; + + ObjectClassName(String className) { + this.className = className; + } + + @Override + public JsonPrimitive toJson() { + return new JsonPrimitive(className); + } + } +} diff --git a/java/google/registry/rdap/RdapDomainAction.java b/java/google/registry/rdap/RdapDomainAction.java index f5d9f1ff7..81297858f 100644 --- a/java/google/registry/rdap/RdapDomainAction.java +++ b/java/google/registry/rdap/RdapDomainAction.java @@ -20,11 +20,11 @@ import static google.registry.request.Action.Method.GET; import static google.registry.request.Action.Method.HEAD; import static google.registry.util.DateTimeUtils.START_OF_TIME; -import com.google.common.collect.ImmutableMap; import google.registry.flows.EppException; import google.registry.model.domain.DomainBase; import google.registry.rdap.RdapJsonFormatter.OutputDataType; import google.registry.rdap.RdapMetrics.EndpointType; +import google.registry.rdap.RdapObjectClasses.RdapDomain; import google.registry.request.Action; import google.registry.request.HttpException.BadRequestException; import google.registry.request.HttpException.NotFoundException; @@ -47,8 +47,7 @@ public class RdapDomainAction extends RdapActionBase { } @Override - public ImmutableMap getJsonObjectForResource( - String pathSearchString, boolean isHeadRequest) { + public RdapDomain getJsonObjectForResource(String pathSearchString, boolean isHeadRequest) { DateTime now = clock.nowUtc(); pathSearchString = canonicalizeName(pathSearchString); try { @@ -68,8 +67,6 @@ public class RdapDomainAction extends RdapActionBase { } return rdapJsonFormatter.makeRdapJsonForDomain( domainBase.get(), - true, - fullServletPath, rdapWhoisServer, now, OutputDataType.FULL, diff --git a/java/google/registry/rdap/RdapDomainSearchAction.java b/java/google/registry/rdap/RdapDomainSearchAction.java index 1a74a7899..9fc5ccea9 100644 --- a/java/google/registry/rdap/RdapDomainSearchAction.java +++ b/java/google/registry/rdap/RdapDomainSearchAction.java @@ -22,7 +22,6 @@ import static google.registry.request.Action.Method.HEAD; import static google.registry.util.DateTimeUtils.START_OF_TIME; 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.collect.Streams; @@ -33,11 +32,11 @@ import com.googlecode.objectify.Key; import com.googlecode.objectify.cmd.Query; import google.registry.model.domain.DomainBase; import google.registry.model.host.HostResource; -import google.registry.rdap.RdapJsonFormatter.BoilerplateType; import google.registry.rdap.RdapJsonFormatter.OutputDataType; import google.registry.rdap.RdapMetrics.EndpointType; import google.registry.rdap.RdapMetrics.SearchType; import google.registry.rdap.RdapMetrics.WildcardType; +import google.registry.rdap.RdapSearchResults.DomainSearchResponse; import google.registry.rdap.RdapSearchResults.IncompletenessWarningType; import google.registry.request.Action; import google.registry.request.HttpException.BadRequestException; @@ -48,7 +47,6 @@ import google.registry.request.auth.Auth; import google.registry.util.Idn; import google.registry.util.NonFinalForTesting; import java.net.InetAddress; -import java.util.ArrayList; import java.util.Comparator; import java.util.List; import java.util.Optional; @@ -93,7 +91,7 @@ public class RdapDomainSearchAction extends RdapSearchActionBase { *

The RDAP spec allows for domain search by domain name, nameserver name or nameserver IP. */ @Override - public ImmutableMap getJsonObjectForResource( + public DomainSearchResponse getJsonObjectForResource( String pathSearchString, boolean isHeadRequest) { DateTime now = clock.nowUtc(); // RDAP syntax example: /rdap/domains?name=exam*.com. @@ -107,7 +105,7 @@ public class RdapDomainSearchAction extends RdapSearchActionBase { "You must specify either name=XXXX, nsLdhName=YYYY or nsIp=ZZZZ"); } decodeCursorToken(); - RdapSearchResults results; + DomainSearchResponse results; if (nameParam.isPresent()) { metricInformationBuilder.setSearchType(SearchType.BY_DOMAIN_NAME); // syntax: /rdap/domains?name=exam*.com @@ -142,18 +140,10 @@ public class RdapDomainSearchAction extends RdapSearchActionBase { } results = searchByNameserverIp(inetAddress, now); } - if (results.jsonList().isEmpty()) { + if (results.domainSearchResults().isEmpty()) { throw new NotFoundException("No domains found"); } - ImmutableMap.Builder builder = new ImmutableMap.Builder<>(); - builder.put("domainSearchResults", results.jsonList()); - rdapJsonFormatter.addTopLevelEntries( - builder, - BoilerplateType.DOMAIN, - getNotices(results), - ImmutableList.of(), - fullServletPath); - return builder.build(); + return results; } /** @@ -168,7 +158,7 @@ public class RdapDomainSearchAction extends RdapSearchActionBase { *

Searches which include deleted entries are effectively treated as if they have a wildcard, * since the same name can return multiple results. */ - private RdapSearchResults searchByDomainName( + private DomainSearchResponse searchByDomainName( final RdapSearchPattern partialStringQuery, final DateTime now) { // Handle queries without a wildcard -- just load by foreign key. We can't do this if deleted // entries are included, because there may be multiple nameservers with the same name. @@ -199,7 +189,7 @@ public class RdapDomainSearchAction extends RdapSearchActionBase { /** * Searches for domains by domain name without a wildcard or interest in deleted entries. */ - private RdapSearchResults searchByDomainNameWithoutWildcard( + private DomainSearchResponse searchByDomainNameWithoutWildcard( final RdapSearchPattern partialStringQuery, final DateTime now) { Optional domainBase = loadByForeignKey(DomainBase.class, partialStringQuery.getInitialString(), now); @@ -211,7 +201,7 @@ public class RdapDomainSearchAction extends RdapSearchActionBase { } /** Searches for domains by domain name with an initial string, wildcard and possible suffix. */ - private RdapSearchResults searchByDomainNameWithInitialString( + private DomainSearchResponse searchByDomainNameWithInitialString( final RdapSearchPattern partialStringQuery, final DateTime now) { // We can't query for undeleted domains as part of the query itself; that would require an // inequality query on deletion time, and we are already using inequality queries on @@ -241,7 +231,7 @@ public class RdapDomainSearchAction extends RdapSearchActionBase { } /** Searches for domains by domain name with a TLD suffix. */ - private RdapSearchResults searchByDomainNameByTld(String tld, DateTime now) { + private DomainSearchResponse searchByDomainNameByTld(String tld, DateTime now) { // Even though we are not searching on fullyQualifiedDomainName, we want the results to come // back ordered by name, so we are still in the same boat as // searchByDomainNameWithInitialString, unable to perform an inequality query on deletion time. @@ -268,7 +258,7 @@ public class RdapDomainSearchAction extends RdapSearchActionBase { *

The includeDeleted parameter does NOT cause deleted nameservers to be searched, only deleted * domains which used to be connected to an undeleted nameserver. */ - private RdapSearchResults searchByNameserverLdhName( + private DomainSearchResponse searchByNameserverLdhName( final RdapSearchPattern partialStringQuery, final DateTime now) { Iterable> hostKeys = getNameserverRefsByLdhName(partialStringQuery, now); if (Iterables.isEmpty(hostKeys)) { @@ -407,7 +397,7 @@ public class RdapDomainSearchAction extends RdapSearchActionBase { *

The includeDeleted parameter does NOT cause deleted nameservers to be searched, only deleted * domains which used to be connected to an undeleted nameserver. */ - private RdapSearchResults searchByNameserverIp( + private DomainSearchResponse searchByNameserverIp( final InetAddress inetAddress, final DateTime now) { Query query = queryItems( @@ -431,7 +421,7 @@ public class RdapDomainSearchAction extends RdapSearchActionBase { *

This method is called by {@link #searchByNameserverLdhName} and {@link * #searchByNameserverIp} after they assemble the relevant host keys. */ - private RdapSearchResults searchByNameserverRefs( + private DomainSearchResponse searchByNameserverRefs( final Iterable> hostKeys, final DateTime now) { // We must break the query up into chunks, because the in operator is limited to 30 subqueries. // Since it is possible for the same domain to show up more than once in our result list (if @@ -466,34 +456,26 @@ public class RdapDomainSearchAction extends RdapSearchActionBase { } List domains = domainSetBuilder.build().asList(); metricInformationBuilder.setNumHostsRetrieved(numHostKeysSearched); - if (domains.size() > rdapResultSetMaxSize) { - return makeSearchResults( - domains.subList(0, rdapResultSetMaxSize), - IncompletenessWarningType.TRUNCATED, - Optional.of((long) domains.size()), - now); - } else { - // If everything that we found will fit in the result, check whether there might have been - // more results that got dropped because the first stage limit on number of nameservers. If - // so, indicate the result might be incomplete. - return makeSearchResults( - domains, - (numHostKeysSearched >= maxNameserversInFirstStage) - ? IncompletenessWarningType.MIGHT_BE_INCOMPLETE - : IncompletenessWarningType.COMPLETE, - (numHostKeysSearched > 0) ? Optional.of((long) domains.size()) : Optional.empty(), - now); - } + // If everything that we found will fit in the result, check whether there might have been + // more results that got dropped because the first stage limit on number of nameservers. If + // so, indicate the result might be incomplete. + return makeSearchResults( + domains, + (numHostKeysSearched >= maxNameserversInFirstStage) + ? IncompletenessWarningType.MIGHT_BE_INCOMPLETE + : IncompletenessWarningType.COMPLETE, + (numHostKeysSearched > 0) ? Optional.of((long) domains.size()) : Optional.empty(), + now); } /** Output JSON for a list of domains, with no incompleteness warnings. */ - private RdapSearchResults makeSearchResults(List domains, DateTime now) { + private DomainSearchResponse makeSearchResults(List domains, DateTime now) { return makeSearchResults( domains, IncompletenessWarningType.COMPLETE, Optional.of((long) domains.size()), now); } /** Output JSON from data in an {@link RdapResultSet} object. */ - private RdapSearchResults makeSearchResults( + private DomainSearchResponse makeSearchResults( RdapResultSet resultSet, DateTime now) { return makeSearchResults( resultSet.resources(), @@ -509,7 +491,7 @@ public class RdapDomainSearchAction extends RdapSearchActionBase { * than are in the list, or MIGHT_BE_INCOMPLETE if a search for domains by nameserver returned the * maximum number of nameservers in the first stage query. */ - private RdapSearchResults makeSearchResults( + private DomainSearchResponse makeSearchResults( List domains, IncompletenessWarningType incompletenessWarningType, Optional numDomainsRetrieved, @@ -517,28 +499,21 @@ public class RdapDomainSearchAction extends RdapSearchActionBase { numDomainsRetrieved.ifPresent(metricInformationBuilder::setNumDomainsRetrieved); OutputDataType outputDataType = (domains.size() > 1) ? OutputDataType.SUMMARY : OutputDataType.FULL; + DomainSearchResponse.Builder builder = + DomainSearchResponse.builder() + .setIncompletenessWarningType(incompletenessWarningType); RdapAuthorization authorization = getAuthorization(); - List> jsonList = new ArrayList<>(); Optional newCursor = Optional.empty(); - for (DomainBase domain : domains) { + for (DomainBase domain : Iterables.limit(domains, rdapResultSetMaxSize)) { newCursor = Optional.of(domain.getFullyQualifiedDomainName()); - jsonList.add( + builder.domainSearchResultsBuilder().add( rdapJsonFormatter.makeRdapJsonForDomain( - domain, false, fullServletPath, rdapWhoisServer, now, outputDataType, authorization)); - if (jsonList.size() >= rdapResultSetMaxSize) { - break; - } + domain, rdapWhoisServer, now, outputDataType, authorization)); } - IncompletenessWarningType finalIncompletenessWarningType = - (jsonList.size() < domains.size()) - ? IncompletenessWarningType.TRUNCATED - : incompletenessWarningType; - metricInformationBuilder.setIncompletenessWarningType(finalIncompletenessWarningType); - return RdapSearchResults.create( - ImmutableList.copyOf(jsonList), - finalIncompletenessWarningType, - (finalIncompletenessWarningType == IncompletenessWarningType.TRUNCATED) - ? newCursor - : Optional.empty()); + if (rdapResultSetMaxSize < domains.size()) { + builder.setNextPageUri(createNavigationUri(newCursor.get())); + builder.setIncompletenessWarningType(IncompletenessWarningType.TRUNCATED); + } + return builder.build(); } } diff --git a/java/google/registry/rdap/RdapEntityAction.java b/java/google/registry/rdap/RdapEntityAction.java index 05aae8474..b0606afeb 100644 --- a/java/google/registry/rdap/RdapEntityAction.java +++ b/java/google/registry/rdap/RdapEntityAction.java @@ -19,7 +19,6 @@ import static google.registry.rdap.RdapUtils.getRegistrarByIanaIdentifier; import static google.registry.request.Action.Method.GET; import static google.registry.request.Action.Method.HEAD; -import com.google.common.collect.ImmutableMap; import com.google.common.primitives.Longs; import com.google.re2j.Pattern; import com.googlecode.objectify.Key; @@ -27,6 +26,7 @@ import google.registry.model.contact.ContactResource; import google.registry.model.registrar.Registrar; import google.registry.rdap.RdapJsonFormatter.OutputDataType; import google.registry.rdap.RdapMetrics.EndpointType; +import google.registry.rdap.RdapObjectClasses.RdapEntity; import google.registry.request.Action; import google.registry.request.HttpException.BadRequestException; import google.registry.request.HttpException.NotFoundException; @@ -60,7 +60,7 @@ public class RdapEntityAction extends RdapActionBase { } @Override - public ImmutableMap getJsonObjectForResource( + public RdapEntity getJsonObjectForResource( String pathSearchString, boolean isHeadRequest) { DateTime now = clock.nowUtc(); // The query string is not used; the RDAP syntax is /rdap/entity/handle (the handle is the roid @@ -76,9 +76,7 @@ public class RdapEntityAction extends RdapActionBase { if ((contactResource != null) && shouldBeVisible(contactResource, now)) { return rdapJsonFormatter.makeRdapJsonForContact( contactResource, - true, Optional.empty(), - fullServletPath, rdapWhoisServer, now, OutputDataType.FULL, @@ -91,7 +89,7 @@ public class RdapEntityAction extends RdapActionBase { Optional registrar = getRegistrarByIanaIdentifier(ianaIdentifier); if (registrar.isPresent() && shouldBeVisible(registrar.get())) { return rdapJsonFormatter.makeRdapJsonForRegistrar( - registrar.get(), true, fullServletPath, rdapWhoisServer, now, OutputDataType.FULL); + registrar.get(), rdapWhoisServer, now, OutputDataType.FULL); } } // At this point, we have failed to find either a contact or a registrar. diff --git a/java/google/registry/rdap/RdapEntitySearchAction.java b/java/google/registry/rdap/RdapEntitySearchAction.java index 3fe0e1daa..387eeb8c7 100644 --- a/java/google/registry/rdap/RdapEntitySearchAction.java +++ b/java/google/registry/rdap/RdapEntitySearchAction.java @@ -21,17 +21,17 @@ import static google.registry.request.Action.Method.GET; import static google.registry.request.Action.Method.HEAD; import com.google.common.collect.ImmutableList; -import com.google.common.collect.ImmutableMap; +import com.google.common.collect.Iterables; import com.google.common.collect.Streams; import com.google.common.primitives.Booleans; import com.google.common.primitives.Longs; import com.googlecode.objectify.cmd.Query; import google.registry.model.contact.ContactResource; import google.registry.model.registrar.Registrar; -import google.registry.rdap.RdapJsonFormatter.BoilerplateType; import google.registry.rdap.RdapJsonFormatter.OutputDataType; import google.registry.rdap.RdapMetrics.EndpointType; import google.registry.rdap.RdapMetrics.SearchType; +import google.registry.rdap.RdapSearchResults.EntitySearchResponse; import google.registry.rdap.RdapSearchResults.IncompletenessWarningType; import google.registry.request.Action; import google.registry.request.HttpException.BadRequestException; @@ -39,7 +39,6 @@ import google.registry.request.HttpException.NotFoundException; import google.registry.request.HttpException.UnprocessableEntityException; import google.registry.request.Parameter; import google.registry.request.auth.Auth; -import java.util.ArrayList; import java.util.Comparator; import java.util.List; import java.util.Optional; @@ -110,7 +109,7 @@ public class RdapEntitySearchAction extends RdapSearchActionBase { /** Parses the parameters and calls the appropriate search function. */ @Override - public ImmutableMap getJsonObjectForResource( + public EntitySearchResponse getJsonObjectForResource( String pathSearchString, boolean isHeadRequest) { DateTime now = clock.nowUtc(); @@ -157,7 +156,7 @@ public class RdapEntitySearchAction extends RdapSearchActionBase { } // Search by name. - RdapSearchResults results; + EntitySearchResponse results; if (fnParam.isPresent()) { metricInformationBuilder.setSearchType(SearchType.BY_FULL_NAME); // syntax: /rdap/entities?fn=Bobby%20Joe* @@ -185,18 +184,10 @@ public class RdapEntitySearchAction extends RdapSearchActionBase { } // Build the result object and return it. - if (results.jsonList().isEmpty()) { + if (results.entitySearchResults().isEmpty()) { throw new NotFoundException("No entities found"); } - ImmutableMap.Builder jsonBuilder = new ImmutableMap.Builder<>(); - jsonBuilder.put("entitySearchResults", results.jsonList()); - rdapJsonFormatter.addTopLevelEntries( - jsonBuilder, - BoilerplateType.ENTITY, - getNotices(results), - ImmutableList.of(), - fullServletPath); - return jsonBuilder.build(); + return results; } /** @@ -223,7 +214,7 @@ public class RdapEntitySearchAction extends RdapSearchActionBase { * href="https://newgtlds.icann.org/sites/default/files/agreements/agreement-approved-09jan14-en.htm">1.6 * of Section 4 of the Base Registry Agreement */ - private RdapSearchResults searchByName( + private EntitySearchResponse searchByName( final RdapSearchPattern partialStringQuery, CursorType cursorType, Optional cursorQueryString, @@ -308,7 +299,7 @@ public class RdapEntitySearchAction extends RdapSearchActionBase { * In both cases, the suffix can be turned into an additional query filter field. For contacts, * there is no equivalent string suffix that can be used as a query filter, so we disallow use. */ - private RdapSearchResults searchByHandle( + private EntitySearchResponse searchByHandle( final RdapSearchPattern partialStringQuery, CursorType cursorType, Optional cursorQueryString, @@ -424,7 +415,7 @@ public class RdapEntitySearchAction extends RdapSearchActionBase { *

This is a convenience wrapper for the four-argument makeSearchResults; it unpacks the * properties of the {@link RdapResultSet} structure and passes them as separate arguments. */ - private RdapSearchResults makeSearchResults( + private EntitySearchResponse makeSearchResults( RdapResultSet resultSet, List registrars, QueryType queryType, @@ -454,7 +445,7 @@ public class RdapEntitySearchAction extends RdapSearchActionBase { * @param now the current date and time * @return an {@link RdapSearchResults} object */ - private RdapSearchResults makeSearchResults( + private EntitySearchResponse makeSearchResults( List contacts, IncompletenessWarningType incompletenessWarningType, int numContactsRetrieved, @@ -473,25 +464,19 @@ public class RdapEntitySearchAction extends RdapSearchActionBase { // (contacts and registrars), and partially because we try to fetch one more than the max size, // so we can tell whether to display the truncation notification. RdapAuthorization authorization = getAuthorization(); - List> jsonOutputList = new ArrayList<>(); // Each time we add a contact or registrar to the output data set, remember what the appropriate // cursor would be if it were the last item returned. When we stop adding items, the last cursor // value we remembered will be the right one to pass back. + EntitySearchResponse.Builder builder = + EntitySearchResponse.builder() + .setIncompletenessWarningType(incompletenessWarningType); Optional newCursor = Optional.empty(); - for (ContactResource contact : contacts) { - if (jsonOutputList.size() >= rdapResultSetMaxSize) { - return RdapSearchResults.create( - ImmutableList.copyOf(jsonOutputList), - IncompletenessWarningType.TRUNCATED, - newCursor); - } + for (ContactResource contact : Iterables.limit(contacts, rdapResultSetMaxSize)) { // As per Andy Newton on the regext mailing list, contacts by themselves have no role, since // they are global, and might have different roles for different domains. - jsonOutputList.add(rdapJsonFormatter.makeRdapJsonForContact( + builder.entitySearchResultsBuilder().add(rdapJsonFormatter.makeRdapJsonForContact( contact, - false, Optional.empty(), - fullServletPath, rdapWhoisServer, now, outputDataType, @@ -503,22 +488,22 @@ public class RdapEntitySearchAction extends RdapSearchActionBase { ? contact.getSearchName() : contact.getRepoId())); } - for (Registrar registrar : registrars) { - if (jsonOutputList.size() >= rdapResultSetMaxSize) { - return RdapSearchResults.create( - ImmutableList.copyOf(jsonOutputList), - IncompletenessWarningType.TRUNCATED, - newCursor); + if (rdapResultSetMaxSize > contacts.size()) { + for (Registrar registrar : + Iterables.limit(registrars, rdapResultSetMaxSize - contacts.size())) { + builder + .entitySearchResultsBuilder() + .add( + rdapJsonFormatter.makeRdapJsonForRegistrar( + registrar, rdapWhoisServer, now, outputDataType)); + newCursor = Optional.of(REGISTRAR_CURSOR_PREFIX + registrar.getRegistrarName()); } - jsonOutputList.add(rdapJsonFormatter.makeRdapJsonForRegistrar( - registrar, false, fullServletPath, rdapWhoisServer, now, outputDataType)); - newCursor = Optional.of(REGISTRAR_CURSOR_PREFIX + registrar.getRegistrarName()); } - return RdapSearchResults.create( - ImmutableList.copyOf(jsonOutputList), - (jsonOutputList.size() < rdapResultSetMaxSize) - ? incompletenessWarningType - : IncompletenessWarningType.COMPLETE, - Optional.empty()); + if (rdapResultSetMaxSize < contacts.size() + registrars.size()) { + builder.setNextPageUri(createNavigationUri(newCursor.get())); + builder.setIncompletenessWarningType(IncompletenessWarningType.TRUNCATED); + return builder.build(); + } + return builder.build(); } } diff --git a/java/google/registry/rdap/RdapHelpAction.java b/java/google/registry/rdap/RdapHelpAction.java index 215dca192..1d813c5d9 100644 --- a/java/google/registry/rdap/RdapHelpAction.java +++ b/java/google/registry/rdap/RdapHelpAction.java @@ -17,12 +17,14 @@ package google.registry.rdap; import static google.registry.request.Action.Method.GET; import static google.registry.request.Action.Method.HEAD; -import com.google.common.collect.ImmutableList; -import com.google.common.collect.ImmutableMap; -import google.registry.rdap.RdapJsonFormatter.BoilerplateType; +import google.registry.rdap.RdapDataStructures.Link; +import google.registry.rdap.RdapDataStructures.Notice; import google.registry.rdap.RdapMetrics.EndpointType; +import google.registry.rdap.RdapObjectClasses.HelpResponse; import google.registry.request.Action; +import google.registry.request.HttpException.NotFoundException; import google.registry.request.auth.Auth; +import java.util.Optional; import javax.inject.Inject; /** RDAP (new WHOIS) action for help requests. */ @@ -34,22 +36,53 @@ import javax.inject.Inject; auth = Auth.AUTH_PUBLIC_ANONYMOUS) public class RdapHelpAction extends RdapActionBase { + /** The help path for the RDAP terms of service. */ + public static final String TOS_PATH = "/tos"; + + private static final String RDAP_HELP_LINK = + "https://github.com/google/nomulus/blob/master/docs/rdap.md"; + @Inject public RdapHelpAction() { super("help", EndpointType.HELP); } + private Notice createHelpNotice() { + String linkValue = rdapJsonFormatter.makeRdapServletRelativeUrl("help"); + Link.Builder linkBuilder = + Link.builder() + .setValue(linkValue) + .setRel("alternate") + .setHref(RDAP_HELP_LINK) + .setType("text/html"); + return Notice.builder() + .setTitle("RDAP Help") + .setDescription( + "domain/XXXX", + "nameserver/XXXX", + "entity/XXXX", + "domains?name=XXXX", + "domains?nsLdhName=XXXX", + "domains?nsIp=XXXX", + "nameservers?name=XXXX", + "nameservers?ip=XXXX", + "entities?fn=XXXX", + "entities?handle=XXXX", + "help/XXXX") + .addLink(linkBuilder.build()) + .build(); + } + @Override - public ImmutableMap getJsonObjectForResource( + public HelpResponse getJsonObjectForResource( String pathSearchString, boolean isHeadRequest) { - // We rely on addTopLevelEntries to notice if we are sending the TOS notice, and not add a - // duplicate boilerplate entry. - ImmutableMap.Builder builder = new ImmutableMap.Builder<>(); - rdapJsonFormatter.addTopLevelEntries( - builder, - BoilerplateType.OTHER, - ImmutableList.of(rdapJsonFormatter.getJsonHelpNotice(pathSearchString, fullServletPath)), - ImmutableList.of(), - fullServletPath); - return builder.build(); + if (pathSearchString.isEmpty() || pathSearchString.equals("/")) { + return HelpResponse.create(Optional.of(createHelpNotice())); + } + if (pathSearchString.equals(TOS_PATH)) { + // A TOS notice is added to every reply automatically, so we don't want to add another one + // here + return HelpResponse.create(Optional.empty()); + } + throw new NotFoundException("no help found for " + pathSearchString); } } diff --git a/java/google/registry/rdap/RdapIcannStandardInformation.java b/java/google/registry/rdap/RdapIcannStandardInformation.java index d0fc7d873..6d59586e8 100644 --- a/java/google/registry/rdap/RdapIcannStandardInformation.java +++ b/java/google/registry/rdap/RdapIcannStandardInformation.java @@ -15,145 +15,143 @@ package google.registry.rdap; import com.google.common.collect.ImmutableList; -import com.google.common.collect.ImmutableMap; +import google.registry.rdap.RdapDataStructures.Link; +import google.registry.rdap.RdapDataStructures.Notice; +import google.registry.rdap.RdapDataStructures.Remark; /** * This file contains boilerplate required by the ICANN RDAP Profile. * - * @see RDAP Operational Profile for gTLD Registries and Registrars + * @see RDAP + * Operational Profile for gTLD Registries and Registrars */ - public class RdapIcannStandardInformation { /** Required by ICANN RDAP Profile section 1.4.10. */ - private static final ImmutableMap CONFORMANCE_NOTICE = - ImmutableMap.of( - "description", - ImmutableList.of( + private static final Notice CONFORMANCE_NOTICE = + Notice.builder() + .setDescription( "This response conforms to the RDAP Operational Profile for gTLD Registries and" - + " Registrars version 1.0")); + + " Registrars version 1.0") + .build(); /** Required by ICANN RDAP Profile section 1.5.18. */ - private static final ImmutableMap DOMAIN_STATUS_CODES_NOTICE = - ImmutableMap.of( - "title", - "Status Codes", - "description", - ImmutableList.of( - "For more information on domain status codes, please visit https://icann.org/epp"), - "links", - ImmutableList.of( - ImmutableMap.of( - "value", "https://icann.org/epp", - "rel", "alternate", - "href", "https://icann.org/epp", - "type", "text/html"))); + private static final Notice DOMAIN_STATUS_CODES_NOTICE = + Notice.builder() + .setTitle("Status Codes") + .setDescription( + "For more information on domain status codes, please visit" + + " https://icann.org/epp") + .addLink( + Link.builder() + .setValue("https://icann.org/epp") + .setRel("alternate") + .setHref("https://icann.org/epp") + .setType("text/html") + .build()) + .build(); /** Required by ICANN RDAP Profile section 1.5.20. */ - private static final ImmutableMap INACCURACY_COMPLAINT_FORM_NOTICE = - ImmutableMap.of( - "description", - ImmutableList.of( - "URL of the ICANN Whois Inaccuracy Complaint Form: https://www.icann.org/wicf"), - "links", - ImmutableList.of( - ImmutableMap.of( - "value", "https://www.icann.org/wicf", - "rel", "alternate", - "href", "https://www.icann.org/wicf", - "type", "text/html"))); + private static final Notice INACCURACY_COMPLAINT_FORM_NOTICE = + Notice.builder() + .setDescription( + "URL of the ICANN Whois Inaccuracy Complaint Form: https://www.icann.org/wicf") + .addLink( + Link.builder() + .setValue("https://www.icann.org/wicf") + .setRel("alternate") + .setHref("https://www.icann.org/wicf") + .setType("text/html") + .build()) + .build(); /** Boilerplate notices required by domain responses. */ - static final ImmutableList> domainBoilerplateNotices = + static final ImmutableList domainBoilerplateNotices = ImmutableList.of( - CONFORMANCE_NOTICE, DOMAIN_STATUS_CODES_NOTICE, INACCURACY_COMPLAINT_FORM_NOTICE); + CONFORMANCE_NOTICE, + // RDAP Response Profile 2.6.3 + DOMAIN_STATUS_CODES_NOTICE, + INACCURACY_COMPLAINT_FORM_NOTICE); /** Boilerplate remarks required by nameserver and entity responses. */ - static final ImmutableList> nameserverAndEntityBoilerplateNotices = + static final ImmutableList nameserverAndEntityBoilerplateNotices = ImmutableList.of(CONFORMANCE_NOTICE); /** * Required by ICANN RDAP Profile section 1.4.9, as corrected by Gustavo Lozano of ICANN. * - * @see Questions about the ICANN RDAP Profile + * @see Questions about + * the ICANN RDAP Profile */ - static final ImmutableMap SUMMARY_DATA_REMARK = - ImmutableMap.of( - "title", - "Incomplete Data", - "description", - ImmutableList.of( - "Summary data only. For complete data, send a specific query for the object."), - "type", - "object truncated due to unexplainable reasons"); + static final Remark SUMMARY_DATA_REMARK = + Remark.builder() + .setTitle("Incomplete Data") + .setDescription( + "Summary data only. For complete data, send a specific query for the object.") + .setType(Remark.Type.OBJECT_TRUNCATED_UNEXPLAINABLE) + .build(); /** * Required by ICANN RDAP Profile section 1.4.8, as corrected by Gustavo Lozano of ICANN. * - * @see Questions about the ICANN RDAP Profile + * @see Questions about + * the ICANN RDAP Profile */ - static final ImmutableMap TRUNCATED_RESULT_SET_NOTICE = - ImmutableMap.of( - "title", - "Search Policy", - "description", - ImmutableList.of("Search results per query are limited."), - "type", - "result set truncated due to unexplainable reasons"); + static final Notice TRUNCATED_RESULT_SET_NOTICE = + Notice.builder() + .setTitle("Search Policy") + .setDescription("Search results per query are limited.") + .setType(Notice.Type.RESULT_TRUNCATED_UNEXPLAINABLE) + .build(); /** Truncation notice as a singleton list, for easy use. */ - static final ImmutableList> TRUNCATION_NOTICES = + static final ImmutableList TRUNCATION_NOTICES = ImmutableList.of(TRUNCATED_RESULT_SET_NOTICE); /** * Used when a search for domains by nameserver may have returned incomplete information because * there were too many nameservers in the first stage results. */ - static final ImmutableMap POSSIBLY_INCOMPLETE_RESULT_SET_NOTICE = - ImmutableMap.of( - "title", - "Search Policy", - "description", - ImmutableList.of( - "Search results may contain incomplete information due to first-stage query limits."), - "type", - "result set truncated due to unexplainable reasons"); + static final Notice POSSIBLY_INCOMPLETE_RESULT_SET_NOTICE = + Notice.builder() + .setTitle("Search Policy") + .setDescription( + "Search results may contain incomplete information due to first-stage query" + + " limits.") + .setType(Notice.Type.RESULT_TRUNCATED_UNEXPLAINABLE) + .build(); /** Possibly incomplete notice as a singleton list, for easy use. */ - static final ImmutableList> POSSIBLY_INCOMPLETE_NOTICES = + static final ImmutableList POSSIBLY_INCOMPLETE_NOTICES = ImmutableList.of(POSSIBLY_INCOMPLETE_RESULT_SET_NOTICE); /** Included when the requester is not logged in as the owner of the domain being returned. */ - static final ImmutableMap DOMAIN_CONTACTS_HIDDEN_DATA_REMARK = - ImmutableMap.of( - "title", - "Contacts Hidden", - "description", - ImmutableList.of("Domain contacts are visible only to the owning registrar."), - "type", - "object truncated due to unexplainable reasons"); + static final Remark DOMAIN_CONTACTS_HIDDEN_DATA_REMARK = + Remark.builder() + .setTitle("Contacts Hidden") + .setDescription("Domain contacts are visible only to the owning registrar.") + .setType(Remark.Type.OBJECT_TRUNCATED_UNEXPLAINABLE) + .build(); /** * Included when requester is not logged in as the owner of the contact being returned. Format - * required by ICANN RDAP Pilot Profile draft section 1.4.11. + * required by ICANN RDAP Response Profile 15feb19 section 2.7.4.3. */ - static final ImmutableMap CONTACT_PERSONAL_DATA_HIDDEN_DATA_REMARK = - ImmutableMap.of( - "title", - "Data Policy", - "description", - ImmutableList.of( + static final Remark CONTACT_PERSONAL_DATA_HIDDEN_DATA_REMARK = + Remark.builder() + .setTitle("Redacted for Privacy") + .setDescription( "Some of the data in this object has been removed.", - "Contact personal data is visible only to the owning registrar."), - "type", - "object truncated due to authorization", - "links", - ImmutableList.of( - ImmutableMap.of( - "value", - "https://github.com/google/nomulus/blob/master/docs/rdap.md#authentication", - "rel", "alternate", - "href", - "https://github.com/google/nomulus/blob/master/docs/rdap.md#authentication", - "type", "text/html"))); + "Contact personal data is visible only to the owning registrar.") + .setType(Remark.Type.OBJECT_REDACTED_AUTHORIZATION) + .addLink( + Link.builder() + .setValue( + "https://github.com/google/nomulus/blob/master/docs/rdap.md#authentication") + .setRel("alternate") + .setHref( + "https://github.com/google/nomulus/blob/master/docs/rdap.md#authentication") + .setType("text/html") + .build()) + .build(); } diff --git a/java/google/registry/rdap/RdapIpAction.java b/java/google/registry/rdap/RdapIpAction.java index 15ce2b3b0..590fedd76 100644 --- a/java/google/registry/rdap/RdapIpAction.java +++ b/java/google/registry/rdap/RdapIpAction.java @@ -17,8 +17,8 @@ package google.registry.rdap; import static google.registry.request.Action.Method.GET; import static google.registry.request.Action.Method.HEAD; -import com.google.common.collect.ImmutableMap; import google.registry.rdap.RdapMetrics.EndpointType; +import google.registry.rdap.RdapObjectClasses.ReplyPayloadBase; import google.registry.request.Action; import google.registry.request.HttpException.NotImplementedException; import google.registry.request.auth.Auth; @@ -43,8 +43,7 @@ public class RdapIpAction extends RdapActionBase { } @Override - public ImmutableMap getJsonObjectForResource( - String pathSearchString, boolean isHeadRequest) { + public ReplyPayloadBase getJsonObjectForResource(String pathSearchString, boolean isHeadRequest) { throw new NotImplementedException("Domain Name Registry information only"); } } diff --git a/java/google/registry/rdap/RdapJsonFormatter.java b/java/google/registry/rdap/RdapJsonFormatter.java index 921876f90..a59b1b649 100644 --- a/java/google/registry/rdap/RdapJsonFormatter.java +++ b/java/google/registry/rdap/RdapJsonFormatter.java @@ -17,11 +17,9 @@ package google.registry.rdap; import static com.google.common.base.Predicates.not; import static com.google.common.base.Strings.nullToEmpty; import static com.google.common.collect.ImmutableList.toImmutableList; -import static com.google.common.collect.ImmutableSortedSet.toImmutableSortedSet; import static google.registry.model.EppResourceUtils.isLinked; import static google.registry.model.ofy.ObjectifyService.ofy; import static google.registry.util.CollectionUtils.union; -import static google.registry.util.DomainNameUtils.ACE_PREFIX; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; @@ -30,8 +28,8 @@ import com.google.common.collect.Maps; import com.google.common.collect.Ordering; import com.google.common.collect.Streams; import com.google.common.net.InetAddresses; +import com.google.gson.JsonArray; import com.googlecode.objectify.Key; -import google.registry.config.RdapNoticeDescriptor; import google.registry.config.RegistryConfig.Config; import google.registry.model.EppResource; import google.registry.model.contact.ContactPhoneNumber; @@ -47,22 +45,32 @@ import google.registry.model.registrar.Registrar; import google.registry.model.registrar.RegistrarAddress; import google.registry.model.registrar.RegistrarContact; import google.registry.model.reporting.HistoryEntry; +import google.registry.rdap.RdapDataStructures.Event; +import google.registry.rdap.RdapDataStructures.EventAction; +import google.registry.rdap.RdapDataStructures.Link; +import google.registry.rdap.RdapDataStructures.Notice; +import google.registry.rdap.RdapDataStructures.Port43WhoisServer; +import google.registry.rdap.RdapDataStructures.PublicId; +import google.registry.rdap.RdapDataStructures.RdapStatus; +import google.registry.rdap.RdapObjectClasses.RdapDomain; +import google.registry.rdap.RdapObjectClasses.RdapEntity; +import google.registry.rdap.RdapObjectClasses.RdapNameserver; +import google.registry.rdap.RdapObjectClasses.Vcard; +import google.registry.rdap.RdapObjectClasses.VcardArray; +import google.registry.request.FullServletPath; import google.registry.request.HttpException.InternalServerErrorException; -import google.registry.request.HttpException.NotFoundException; -import google.registry.util.Idn; import java.net.Inet4Address; import java.net.Inet6Address; import java.net.InetAddress; import java.net.URI; +import java.nio.file.Paths; import java.util.HashMap; -import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Optional; import java.util.stream.Stream; import javax.annotation.Nullable; import javax.inject.Inject; -import javax.inject.Singleton; import org.joda.time.DateTime; import org.joda.time.DateTimeComparator; @@ -77,11 +85,12 @@ import org.joda.time.DateTimeComparator; * @see * RFC 7483: JSON Responses for the Registration Data Access Protocol (RDAP) */ -@Singleton public class RdapJsonFormatter { - @Inject @Config("rdapTosPath") String rdapTosPath; - @Inject @Config("rdapHelpMap") ImmutableMap rdapHelpMap; + + @Inject @Config("rdapTos") ImmutableList rdapTos; + @Inject @Config("rdapTosStaticUrl") @Nullable String rdapTosStaticUrl; + @Inject @FullServletPath String fullServletPath; @Inject RdapJsonFormatter() {} /** @@ -100,78 +109,6 @@ public class RdapJsonFormatter { SUMMARY } - /** - * Indication of what type of boilerplate notices are required for the RDAP JSON messages. The - * ICANN RDAP Profile specifies that, for instance, domain name responses should include a remark - * about domain status codes. So we need to know when to include such boilerplate. On the other - * hand, remarks are not allowed except in domain, nameserver and entity objects, so we need to - * suppress them for other types of responses (e.g. help). - */ - public enum BoilerplateType { - DOMAIN, - NAMESERVER, - ENTITY, - OTHER - } - - private static final String RDAP_CONFORMANCE_LEVEL = "icann_rdap_response_profile_0"; - private static final String VCARD_VERSION_NUMBER = "4.0"; - static final String NOTICES = "notices"; - private static final String REMARKS = "remarks"; - - private enum RdapStatus { - - // Status values specified in RFC 7483 § 10.2.2. - VALIDATED("validated"), - RENEW_PROHIBITED("renew prohibited"), - UPDATE_PROHIBITED("update prohibited"), - TRANSFER_PROHIBITED("transfer prohibited"), - DELETE_PROHIBITED("delete prohibited"), - PROXY("proxy"), - PRIVATE("private"), - REMOVED("removed"), - OBSCURED("obscured"), - ASSOCIATED("associated"), - ACTIVE("active"), - INACTIVE("inactive"), - LOCKED("locked"), - PENDING_CREATE("pending create"), - PENDING_RENEW("pending renew"), - PENDING_TRANSFER("pending transfer"), - PENDING_UPDATE("pending update"), - PENDING_DELETE("pending delete"), - - // Additional status values defined in - // https://tools.ietf.org/html/draft-ietf-regext-epp-rdap-status-mapping-01. - ADD_PERIOD("add period"), - AUTO_RENEW_PERIOD("auto renew period"), - CLIENT_DELETE_PROHIBITED("client delete prohibited"), - CLIENT_HOLD("client hold"), - CLIENT_RENEW_PROHIBITED("client renew prohibited"), - CLIENT_TRANSFER_PROHIBITED("client transfer prohibited"), - CLIENT_UPDATE_PROHIBITED("client update prohibited"), - PENDING_RESTORE("pending restore"), - REDEMPTION_PERIOD("redemption period"), - RENEW_PERIOD("renew period"), - SERVER_DELETE_PROHIBITED("server deleted prohibited"), - SERVER_RENEW_PROHIBITED("server renew prohibited"), - SERVER_TRANSFER_PROHIBITED("server transfer prohibited"), - SERVER_UPDATE_PROHIBITED("server update prohibited"), - SERVER_HOLD("server hold"), - TRANSFER_PERIOD("transfer period"); - - /** Value as it appears in RDAP messages. */ - private final String rfc7483String; - - RdapStatus(String rfc7483String) { - this.rfc7483String = rfc7483String; - } - - public String getDisplayName() { - return rfc7483String; - } - } - /** Map of EPP status values to the RDAP equivalents. */ private static final ImmutableMap STATUS_TO_RDAP_STATUS_MAP = new ImmutableMap.Builder() @@ -201,83 +138,48 @@ public class RdapJsonFormatter { // RdapStatus.TRANSFER_PERIOD not defined in our system .build(); - /** Role values specified in RFC 7483 § 10.2.4. */ - private enum RdapEntityRole { - REGISTRANT("registrant"), - TECH("technical"), - ADMIN("administrative"), - ABUSE("abuse"), - BILLING("billing"), - REGISTRAR("registrar"), - RESELLER("reseller"), - SPONSOR("sponsor"), - PROXY("proxy"), - NOTIFICATIONS("notifications"), - NOC("noc"); - - /** Value as it appears in RDAP messages. */ - final String rfc7483String; - - RdapEntityRole(String rfc7483String) { - this.rfc7483String = rfc7483String; - } - } - - /** Status values specified in RFC 7483 § 10.2.2. */ - private enum RdapEventAction { - REGISTRATION("registration"), - REREGISTRATION("reregistration"), - LAST_CHANGED("last changed"), - EXPIRATION("expiration"), - DELETION("deletion"), - REINSTANTIATION("reinstantiation"), - TRANSFER("transfer"), - LOCKED("locked"), - UNLOCKED("unlocked"), - LAST_UPDATE_OF_RDAP_DATABASE("last update of RDAP database"); - - /** Value as it appears in RDAP messages. */ - private final String rfc7483String; - - RdapEventAction(String rfc7483String) { - this.rfc7483String = rfc7483String; - } - - public String getDisplayName() { - return rfc7483String; - } - } - - /** Map of EPP event values to the RDAP equivalents. */ - private static final ImmutableMap + /** + * Map of EPP event values to the RDAP equivalents. + * + *

Only has entries for the events we care about, according to the RDAP Response Profile + * 15feb19. + * + * There are additional events that don't have HistoryEntry equivalent and are created + * differently. They will be in different locations in the code. These values are: EXPIRATION, + * LAST_CHANGED, LAST_UPDATE_OF_RDAP_DATABASE. + */ + private static final ImmutableMap HISTORY_ENTRY_TYPE_TO_RDAP_EVENT_ACTION_MAP = - new ImmutableMap.Builder() - .put(HistoryEntry.Type.CONTACT_CREATE, RdapEventAction.REGISTRATION) - .put(HistoryEntry.Type.CONTACT_DELETE, RdapEventAction.DELETION) - .put(HistoryEntry.Type.CONTACT_TRANSFER_APPROVE, RdapEventAction.TRANSFER) - .put(HistoryEntry.Type.DOMAIN_AUTORENEW, RdapEventAction.REREGISTRATION) - .put(HistoryEntry.Type.DOMAIN_CREATE, RdapEventAction.REGISTRATION) - .put(HistoryEntry.Type.DOMAIN_DELETE, RdapEventAction.DELETION) - .put(HistoryEntry.Type.DOMAIN_RENEW, RdapEventAction.REREGISTRATION) - .put(HistoryEntry.Type.DOMAIN_RESTORE, RdapEventAction.REINSTANTIATION) - .put(HistoryEntry.Type.DOMAIN_TRANSFER_APPROVE, RdapEventAction.TRANSFER) - .put(HistoryEntry.Type.HOST_CREATE, RdapEventAction.REGISTRATION) - .put(HistoryEntry.Type.HOST_DELETE, RdapEventAction.DELETION) + new ImmutableMap.Builder() + .put(HistoryEntry.Type.CONTACT_CREATE, EventAction.REGISTRATION) + .put(HistoryEntry.Type.CONTACT_DELETE, EventAction.DELETION) + .put(HistoryEntry.Type.CONTACT_TRANSFER_APPROVE, EventAction.TRANSFER) + + /** Not in the Response Profile. */ + .put(HistoryEntry.Type.DOMAIN_AUTORENEW, EventAction.REREGISTRATION) + /** Section 2.3.1.1, obligatory. */ + .put(HistoryEntry.Type.DOMAIN_CREATE, EventAction.REGISTRATION) + /** Not in the Response Profile. */ + .put(HistoryEntry.Type.DOMAIN_DELETE, EventAction.DELETION) + /** Not in the Response Profile. */ + .put(HistoryEntry.Type.DOMAIN_RENEW, EventAction.REREGISTRATION) + /** Not in the Response Profile. */ + .put(HistoryEntry.Type.DOMAIN_RESTORE, EventAction.REINSTANTIATION) + /** Section 2.3.2.3, optional. */ + .put(HistoryEntry.Type.DOMAIN_TRANSFER_APPROVE, EventAction.TRANSFER) + + .put(HistoryEntry.Type.HOST_CREATE, EventAction.REGISTRATION) + .put(HistoryEntry.Type.HOST_DELETE, EventAction.DELETION) .build(); - private static final ImmutableList CONFORMANCE_LIST = - ImmutableList.of(RDAP_CONFORMANCE_LEVEL); - - private static final ImmutableList STATUS_LIST_ACTIVE = - ImmutableList.of(RdapStatus.ACTIVE.rfc7483String); - private static final ImmutableList STATUS_LIST_INACTIVE = - ImmutableList.of(RdapStatus.INACTIVE.rfc7483String); + private static final ImmutableList STATUS_LIST_ACTIVE = + ImmutableList.of(RdapStatus.ACTIVE); + private static final ImmutableList STATUS_LIST_INACTIVE = + ImmutableList.of(RdapStatus.INACTIVE); private static final ImmutableMap> PHONE_TYPE_VOICE = ImmutableMap.of("type", ImmutableList.of("voice")); private static final ImmutableMap> PHONE_TYPE_FAX = ImmutableMap.of("type", ImmutableList.of("fax")); - private static final ImmutableList VCARD_ENTRY_VERSION = - ImmutableList.of("version", ImmutableMap.of(), "text", VCARD_VERSION_NUMBER); /** Sets the ordering for hosts; just use the fully qualified host name. */ private static final Ordering HOST_RESOURCE_ORDERING = @@ -287,182 +189,29 @@ public class RdapJsonFormatter { private static final Ordering DESIGNATED_CONTACT_ORDERING = Ordering.natural().onResultOf(DesignatedContact::getType); - ImmutableMap getJsonTosNotice(String rdapLinkBase) { - return getJsonHelpNotice(rdapTosPath, rdapLinkBase); - } - - ImmutableMap getJsonHelpNotice( - String pathSearchString, String rdapLinkBase) { - if (pathSearchString.isEmpty()) { - pathSearchString = "/"; - } - if (!rdapHelpMap.containsKey(pathSearchString)) { - throw new NotFoundException("no help found for " + pathSearchString); - } - try { - return RdapJsonFormatter.makeRdapJsonNotice(rdapHelpMap.get(pathSearchString), rdapLinkBase); - } catch (Exception e) { - throw new InternalServerErrorException( - String.format("Error reading RDAP help file: %s", pathSearchString), e); - } - } - - /** - * Adds the required top-level boilerplate. RFC 7483 specifies that the top-level object should - * include an entry indicating the conformance level. The ICANN RDAP Profile document (dated 3 - * December 2015) mandates several additional entries, in sections 1.4.4, 1.4.10, 1.5.18 and - * 1.5.20. Note that this method will only work if there are no object-specific remarks already in - * the JSON object being built. If there are, the boilerplate must be merged in. - * - * @param jsonBuilder a builder for a JSON map object - * @param boilerplateType type of boilerplate to be added; the ICANN RDAP Profile document - * mandates extra boilerplate for domain objects - * @param notices a list of notices to be inserted before the boilerplate notices. If the TOS - * notice is in this list, the method avoids adding a second copy. - * @param remarks a list of remarks to be inserted. - * @param rdapLinkBase the base for link URLs - */ - void addTopLevelEntries( - ImmutableMap.Builder jsonBuilder, - BoilerplateType boilerplateType, - List> notices, - List> remarks, - String rdapLinkBase) { - jsonBuilder.put("rdapConformance", CONFORMANCE_LIST); - ImmutableList.Builder> noticesBuilder = - new ImmutableList.Builder<>(); - ImmutableMap tosNotice = getJsonTosNotice(rdapLinkBase); - boolean tosNoticeFound = false; - if (!notices.isEmpty()) { - noticesBuilder.addAll(notices); - for (ImmutableMap notice : notices) { - if (notice.equals(tosNotice)) { - tosNoticeFound = true; - break; - } - } - } - if (!tosNoticeFound) { - noticesBuilder.add(tosNotice); - } - switch (boilerplateType) { - case DOMAIN: - noticesBuilder.addAll(RdapIcannStandardInformation.domainBoilerplateNotices); - break; - case NAMESERVER: - case ENTITY: - noticesBuilder.addAll(RdapIcannStandardInformation.nameserverAndEntityBoilerplateNotices); - break; - default: // things other than domains, nameservers and entities do not yet have boilerplate - break; - } - jsonBuilder.put(NOTICES, noticesBuilder.build()); - if (!remarks.isEmpty()) { - jsonBuilder.put(REMARKS, remarks); - } - } - - /** - * Creates a JSON object containing a notice or remark object, as defined by RFC 7483 § 4.3. - * The object should then be inserted into a notices or remarks array. The builder fields are: - * - *

title: the title of the notice; if null, the notice will have no title - * - *

description: objects which will be converted to strings to form the description of the - * notice (this is the only required field; all others are optional) - * - *

typeString: the notice or remark type as defined in § 10.2.1; if null, no type - * - *

linkValueSuffix: the path at the end of the URL used in the value field of the link, - * without any initial slash (e.g. a suffix of help/toc equates to a URL of - * http://example.net/help/toc); if null, no link is created; if it is not null, a single link is - * created; this method never creates more than one link) - * - *

htmlUrlString: the path, if any, to be used in the href value of the link; if the URL is - * absolute, it is used as is; if it is relative, starting with a slash, it is appended to the - * protocol and host of the link base; if it is relative, not starting with a slash, it is - * appended to the complete link base; if null, a self link is generated instead, using the link - * link value - * - *

linkBase: the base for the link value and href; if null, it is assumed to be the empty - * string - * - * @see - * RFC 7483: JSON Responses for the Registration Data Access Protocol (RDAP) - */ - static ImmutableMap makeRdapJsonNotice( - RdapNoticeDescriptor parameters, @Nullable String linkBase) { - ImmutableMap.Builder jsonBuilder = new ImmutableMap.Builder<>(); - if (parameters.getTitle() != null) { - jsonBuilder.put("title", parameters.getTitle()); - } - ImmutableList.Builder descriptionBuilder = new ImmutableList.Builder<>(); - for (String line : parameters.getDescription()) { - descriptionBuilder.add(nullToEmpty(line)); - } - jsonBuilder.put("description", descriptionBuilder.build()); - if (parameters.getTypeString() != null) { - jsonBuilder.put("typeString", parameters.getTypeString()); - } - String linkBaseNotNull = nullToEmpty(linkBase); - String linkValueSuffixNotNull = nullToEmpty(parameters.getLinkValueSuffix()); - String linkValueString = - String.format( - "%s%s%s", - linkBaseNotNull, - (linkBaseNotNull.endsWith("/") || linkValueSuffixNotNull.startsWith("/")) ? "" : "/", - linkValueSuffixNotNull); - if (parameters.getLinkHrefUrlString() == null) { - jsonBuilder.put("links", ImmutableList.of(ImmutableMap.of( - "value", linkValueString, - "rel", "self", - "href", linkValueString, - "type", "application/rdap+json"))); + /** Creates the TOS notice that is added to every reply. */ + Notice createTosNotice() { + String linkValue = makeRdapServletRelativeUrl("help", RdapHelpAction.TOS_PATH); + Link.Builder linkBuilder = Link.builder() + .setValue(linkValue); + if (rdapTosStaticUrl == null) { + linkBuilder.setRel("self").setHref(linkValue).setType("application/rdap+json"); } else { - URI htmlBaseURI = URI.create(nullToEmpty(linkBase)); - URI htmlUri = htmlBaseURI.resolve(parameters.getLinkHrefUrlString()); - jsonBuilder.put("links", ImmutableList.of(ImmutableMap.of( - "value", linkValueString, - "rel", "alternate", - "href", htmlUri.toString(), - "type", "text/html"))); + URI htmlBaseURI = URI.create(fullServletPath); + URI htmlUri = htmlBaseURI.resolve(rdapTosStaticUrl); + linkBuilder.setRel("alternate").setHref(htmlUri.toString()).setType("text/html"); } - return jsonBuilder.build(); - } - - /** - * Creates a JSON object containing a notice with a next page navigation link, which can then be - * inserted into a notices array. - * - *

At the moment, only a next page link is supported. Other types of links (e.g. previous page) - * could be added in the future, but it's not clear how to generate such links, given the way we - * are querying the database. - * - * @param nextPageUrl URL string used to navigate to next page, or empty if there is no next - */ - static ImmutableMap makeRdapJsonNavigationLinkNotice( - Optional nextPageUrl) { - ImmutableMap.Builder jsonBuilder = new ImmutableMap.Builder<>(); - jsonBuilder.put("title", "Navigation Links"); - jsonBuilder.put("description", ImmutableList.of("Links to related pages.")); - if (nextPageUrl.isPresent()) { - jsonBuilder.put( - "links", - ImmutableList.of( - ImmutableMap.of( - "rel", "next", - "href", nextPageUrl.get(), - "type", "application/rdap+json"))); - } - return jsonBuilder.build(); + return Notice.builder() + .setTitle("RDAP Terms of Service") + .setDescription(rdapTos) + .addLink(linkBuilder.build()) + .build(); } /** * Creates a JSON object for a {@link DomainBase}. * * @param domainBase the domain resource object from which the JSON object should be created - * @param isTopLevel if true, the top-level boilerplate will be added - * @param linkBase the URL base to be used when creating links * @param whoisServer the fully-qualified domain name of the WHOIS server to be listed in the * port43 field; if null, port43 is not added to the object * @param now the as-date @@ -470,174 +219,123 @@ public class RdapJsonFormatter { * @param authorization the authorization level of the request; if not authorized for the * registrar owning the domain, no contact information is included */ - ImmutableMap makeRdapJsonForDomain( + RdapDomain makeRdapJsonForDomain( DomainBase domainBase, - boolean isTopLevel, - @Nullable String linkBase, @Nullable String whoisServer, DateTime now, OutputDataType outputDataType, RdapAuthorization authorization) { - // Start with the domain-level information. - ImmutableMap.Builder jsonBuilder = new ImmutableMap.Builder<>(); - jsonBuilder.put("objectClassName", "domain"); - jsonBuilder.put("handle", domainBase.getRepoId()); - jsonBuilder.put("ldhName", domainBase.getFullyQualifiedDomainName()); - // Only include the unicodeName field if there are unicode characters. - if (hasUnicodeComponents(domainBase.getFullyQualifiedDomainName())) { - jsonBuilder.put("unicodeName", Idn.toUnicode(domainBase.getFullyQualifiedDomainName())); - } - jsonBuilder.put( - "status", + RdapDomain.Builder builder = RdapDomain.builder(); + // RDAP Response Profile 15feb19 section 2.2: + // The domain handle MUST be the ROID + builder.setHandle(domainBase.getRepoId()); + builder.setLdhName(domainBase.getFullyQualifiedDomainName()); + builder.statusBuilder().addAll( makeStatusValueList( domainBase.getStatusValues(), false, // isRedacted domainBase.getDeletionTime().isBefore(now))); - jsonBuilder.put("links", ImmutableList.of( - makeLink("domain", domainBase.getFullyQualifiedDomainName(), linkBase))); + builder.linksBuilder().add( + makeSelfLink("domain", domainBase.getFullyQualifiedDomainName())); boolean displayContacts = authorization.isAuthorizedForClientId(domainBase.getCurrentSponsorClientId()); // If we are outputting all data (not just summary data), also add information about hosts, // contacts and events (history entries). If we are outputting summary data, instead add a // remark indicating that fact. - List> remarks; if (outputDataType == OutputDataType.SUMMARY) { - remarks = ImmutableList.of(RdapIcannStandardInformation.SUMMARY_DATA_REMARK); + builder.remarksBuilder().add(RdapIcannStandardInformation.SUMMARY_DATA_REMARK); } else { - remarks = displayContacts - ? ImmutableList.of() - : ImmutableList.of(RdapIcannStandardInformation.DOMAIN_CONTACTS_HIDDEN_DATA_REMARK); - ImmutableList events = makeEvents(domainBase, now); - if (!events.isEmpty()) { - jsonBuilder.put("events", events); - } + ImmutableList events = makeEvents(domainBase, now); + builder.eventsBuilder().addAll(events); // Kick off the database loads of the nameservers that we will need, so it can load // asynchronously while we load and process the contacts. Map, HostResource> loadedHosts = ofy().load().keys(domainBase.getNameservers()); // Load the registrant and other contacts and add them to the data. - ImmutableList> entities; if (!displayContacts) { - entities = ImmutableList.of(); + builder + .remarksBuilder() + .add(RdapIcannStandardInformation.DOMAIN_CONTACTS_HIDDEN_DATA_REMARK); } else { Map, ContactResource> loadedContacts = ofy().load().keys(domainBase.getReferencedContacts()); - entities = - Streams.concat( - domainBase.getContacts().stream(), - Stream.of( - DesignatedContact.create(Type.REGISTRANT, domainBase.getRegistrant()))) - .sorted(DESIGNATED_CONTACT_ORDERING) - .map( - designatedContact -> - makeRdapJsonForContact( - loadedContacts.get(designatedContact.getContactKey()), - false, - Optional.of(designatedContact.getType()), - linkBase, - null, - now, - outputDataType, - authorization)) - .collect(toImmutableList()); - } - entities = - addRegistrarEntity( - entities, domainBase.getCurrentSponsorClientId(), linkBase, whoisServer, now); - if (!entities.isEmpty()) { - jsonBuilder.put("entities", entities); + Streams.concat( + domainBase.getContacts().stream(), + Stream.of( + DesignatedContact.create(Type.REGISTRANT, domainBase.getRegistrant()))) + .sorted(DESIGNATED_CONTACT_ORDERING) + .map( + designatedContact -> + makeRdapJsonForContact( + loadedContacts.get(designatedContact.getContactKey()), + Optional.of(designatedContact.getType()), + null, + now, + outputDataType, + authorization)) + .forEach(builder.entitiesBuilder()::add); } + builder + .entitiesBuilder() + .add( + createInternalRegistrarEntity( + domainBase.getCurrentSponsorClientId(), whoisServer, now)); // Add the nameservers to the data; the load was kicked off above for efficiency. - ImmutableList.Builder nsBuilder = new ImmutableList.Builder<>(); for (HostResource hostResource : HOST_RESOURCE_ORDERING.immutableSortedCopy(loadedHosts.values())) { - nsBuilder.add(makeRdapJsonForHost( - hostResource, false, linkBase, null, now, outputDataType)); - } - ImmutableList ns = nsBuilder.build(); - if (!ns.isEmpty()) { - jsonBuilder.put("nameservers", ns); + builder.nameserversBuilder().add(makeRdapJsonForHost( + hostResource, null, now, outputDataType)); } } if (whoisServer != null) { - jsonBuilder.put("port43", whoisServer); + builder.setPort43(Port43WhoisServer.create(whoisServer)); } - if (isTopLevel) { - addTopLevelEntries( - jsonBuilder, - BoilerplateType.DOMAIN, - remarks, - ImmutableList.of(), linkBase); - } else if (!remarks.isEmpty()) { - jsonBuilder.put(REMARKS, remarks); - } - return jsonBuilder.build(); + return builder.build(); } /** - * Adds a JSON object for the desired registrar to an existing list of JSON objects. + * Creates a JSON object for the desired registrar to an existing list of JSON objects. * - * @param entities list of entities to which the desired registrar should be added * @param clientId the registrar client ID - * @param linkBase the URL base to be used when creating links * @param whoisServer the fully-qualified domain name of the WHOIS server to be listed in the * port43 field; if null, port43 is not added to the object * @param now the as-date */ - ImmutableList> addRegistrarEntity( - ImmutableList> entities, - @Nullable String clientId, - @Nullable String linkBase, + RdapEntity createInternalRegistrarEntity( + String clientId, @Nullable String whoisServer, DateTime now) { - if (clientId == null) { - return entities; - } Optional registrar = Registrar.loadByClientIdCached(clientId); if (!registrar.isPresent()) { - return entities; + throw new InternalServerErrorException( + String.format("Coudn't find registrar '%s'", clientId)); } - ImmutableList.Builder> builder = new ImmutableList.Builder<>(); - builder.addAll(entities); // TODO(b/130150723): we need to display the ABUSE contact for registrar object inside of Domain - // responses. Currently, we use summary for any "internal" registrar. - builder.add( - makeRdapJsonForRegistrar( - registrar.get(), - false /* isTopLevel */, - linkBase, - whoisServer, - now, - OutputDataType.SUMMARY)); - return builder.build(); + // responses. Currently, we use summary for any "internal" registrar. + return makeRdapJsonForRegistrar( + registrar.get(), + whoisServer, + now, + OutputDataType.SUMMARY); } /** * Creates a JSON object for a {@link HostResource}. * * @param hostResource the host resource object from which the JSON object should be created - * @param isTopLevel if true, the top-level boilerplate will be added - * @param linkBase the URL base to be used when creating links * @param whoisServer the fully-qualified domain name of the WHOIS server to be listed in the * port43 field; if null, port43 is not added to the object * @param now the as-date * @param outputDataType whether to generate full or summary data */ - ImmutableMap makeRdapJsonForHost( + RdapNameserver makeRdapJsonForHost( HostResource hostResource, - boolean isTopLevel, - @Nullable String linkBase, @Nullable String whoisServer, DateTime now, OutputDataType outputDataType) { - ImmutableMap.Builder jsonBuilder = new ImmutableMap.Builder<>(); - jsonBuilder.put("objectClassName", "nameserver"); - jsonBuilder.put("handle", hostResource.getRepoId()); - jsonBuilder.put("ldhName", hostResource.getFullyQualifiedHostName()); - // Only include the unicodeName field if there are unicode characters. - if (hasUnicodeComponents(hostResource.getFullyQualifiedHostName())) { - jsonBuilder.put("unicodeName", Idn.toUnicode(hostResource.getFullyQualifiedHostName())); - } + RdapNameserver.Builder builder = RdapNameserver.builder() + .setHandle(hostResource.getRepoId()) + .setLdhName(hostResource.getFullyQualifiedHostName()); ImmutableSet.Builder statuses = new ImmutableSet.Builder<>(); statuses.addAll(hostResource.getStatusValues()); @@ -650,84 +348,48 @@ public class RdapJsonFormatter { .contains(StatusValue.PENDING_TRANSFER)) { statuses.add(StatusValue.PENDING_TRANSFER); } - jsonBuilder.put( - "status", - makeStatusValueList( - statuses.build(), - false, // isRedacted - hostResource.getDeletionTime().isBefore(now))); - jsonBuilder.put("links", ImmutableList.of( - makeLink("nameserver", hostResource.getFullyQualifiedHostName(), linkBase))); - List> remarks; + builder + .statusBuilder() + .addAll( + makeStatusValueList( + statuses.build(), + false, // isRedacted + hostResource.getDeletionTime().isBefore(now))); + builder + .linksBuilder() + .add(makeSelfLink("nameserver", hostResource.getFullyQualifiedHostName())); + // If we are outputting all data (not just summary data), also add events taken from the history // entries. If we are outputting summary data, instead add a remark indicating that fact. if (outputDataType == OutputDataType.SUMMARY) { - remarks = ImmutableList.of(RdapIcannStandardInformation.SUMMARY_DATA_REMARK); + builder.remarksBuilder().add(RdapIcannStandardInformation.SUMMARY_DATA_REMARK); } else { - remarks = ImmutableList.of(); - ImmutableList events = makeEvents(hostResource, now); - if (!events.isEmpty()) { - jsonBuilder.put("events", events); + builder.eventsBuilder().addAll(makeEvents(hostResource, now)); + } + + // We MUST have the ip addresses: RDAP Response Profile 4.2. + for (InetAddress inetAddress : hostResource.getInetAddresses()) { + if (inetAddress instanceof Inet4Address) { + builder.ipv4Builder().add(InetAddresses.toAddrString(inetAddress)); + } else if (inetAddress instanceof Inet6Address) { + builder.ipv6Builder().add(InetAddresses.toAddrString(inetAddress)); } } - ImmutableSet inetAddresses = hostResource.getInetAddresses(); - if (!inetAddresses.isEmpty()) { - ImmutableList.Builder v4AddressesBuilder = new ImmutableList.Builder<>(); - ImmutableList.Builder v6AddressesBuilder = new ImmutableList.Builder<>(); - for (InetAddress inetAddress : inetAddresses) { - if (inetAddress instanceof Inet4Address) { - v4AddressesBuilder.add(InetAddresses.toAddrString(inetAddress)); - } else if (inetAddress instanceof Inet6Address) { - v6AddressesBuilder.add(InetAddresses.toAddrString(inetAddress)); - } - } - ImmutableMap.Builder> ipAddressesBuilder = - new ImmutableMap.Builder<>(); - ImmutableList v4Addresses = v4AddressesBuilder.build(); - if (!v4Addresses.isEmpty()) { - ipAddressesBuilder.put("v4", Ordering.natural().immutableSortedCopy(v4Addresses)); - } - ImmutableList v6Addresses = v6AddressesBuilder.build(); - if (!v6Addresses.isEmpty()) { - ipAddressesBuilder.put("v6", Ordering.natural().immutableSortedCopy(v6Addresses)); - } - ImmutableMap> ipAddresses = ipAddressesBuilder.build(); - if (!ipAddresses.isEmpty()) { - jsonBuilder.put("ipAddresses", ipAddressesBuilder.build()); - } - } - ImmutableList> entities = - addRegistrarEntity( - ImmutableList.of(), - hostResource.getPersistedCurrentSponsorClientId(), - linkBase, - whoisServer, - now); - if (!entities.isEmpty()) { - jsonBuilder.put("entities", entities); - } + builder.entitiesBuilder().add(createInternalRegistrarEntity( + hostResource.getPersistedCurrentSponsorClientId(), + whoisServer, + now)); if (whoisServer != null) { - jsonBuilder.put("port43", whoisServer); + builder.setPort43(Port43WhoisServer.create(whoisServer)); } - if (isTopLevel) { - addTopLevelEntries( - jsonBuilder, - BoilerplateType.NAMESERVER, - remarks, - ImmutableList.of(), linkBase); - } else if (!remarks.isEmpty()) { - jsonBuilder.put(REMARKS, remarks); - } - return jsonBuilder.build(); + return builder.build(); } /** * Creates a JSON object for a {@link ContactResource} and associated contact type. * * @param contactResource the contact resource object from which the JSON object should be created - * @param isTopLevel if true, the top-level boilerplate will be added * @param contactType the contact type to map to an RDAP role; if absent, no role is listed - * @param linkBase the URL base to be used when creating links * @param whoisServer the fully-qualified domain name of the WHOIS server to be listed in the * port43 field; if null, port43 is not added to the object * @param now the as-date @@ -735,53 +397,47 @@ public class RdapJsonFormatter { * @param authorization the authorization level of the request; personal contact data is only * shown if the contact is owned by a registrar for which the request is authorized */ - ImmutableMap makeRdapJsonForContact( + RdapEntity makeRdapJsonForContact( ContactResource contactResource, - boolean isTopLevel, Optional contactType, - @Nullable String linkBase, @Nullable String whoisServer, DateTime now, OutputDataType outputDataType, RdapAuthorization authorization) { boolean isAuthorized = authorization.isAuthorizedForClientId(contactResource.getCurrentSponsorClientId()); - ImmutableMap.Builder jsonBuilder = new ImmutableMap.Builder<>(); - ImmutableList.Builder> remarksBuilder - = new ImmutableList.Builder<>(); - jsonBuilder.put("objectClassName", "entity"); - jsonBuilder.put("handle", contactResource.getRepoId()); - jsonBuilder.put( - "status", - makeStatusValueList( - isLinked(Key.create(contactResource), now) - ? union(contactResource.getStatusValues(), StatusValue.LINKED) - : contactResource.getStatusValues(), - !isAuthorized, - contactResource.getDeletionTime().isBefore(now))); + + RdapEntity.Builder entityBuilder = + RdapEntity.builder() + .setHandle(contactResource.getRepoId()); + entityBuilder + .statusBuilder() + .addAll( + makeStatusValueList( + isLinked(Key.create(contactResource), now) + ? union(contactResource.getStatusValues(), StatusValue.LINKED) + : contactResource.getStatusValues(), + !isAuthorized, + contactResource.getDeletionTime().isBefore(now))); + contactType.ifPresent( - type -> jsonBuilder.put("roles", ImmutableList.of(convertContactTypeToRdapRole(type)))); - jsonBuilder.put("links", - ImmutableList.of(makeLink("entity", contactResource.getRepoId(), linkBase))); + type -> entityBuilder.rolesBuilder().add(convertContactTypeToRdapRole(type))); + entityBuilder.linksBuilder().add(makeSelfLink("entity", contactResource.getRepoId())); // If we are logged in as the owner of this contact, create the vCard. if (isAuthorized) { - ImmutableList.Builder vcardBuilder = new ImmutableList.Builder<>(); - vcardBuilder.add(VCARD_ENTRY_VERSION); + VcardArray.Builder vcardBuilder = VcardArray.builder(); PostalInfo postalInfo = contactResource.getInternationalizedPostalInfo(); if (postalInfo == null) { postalInfo = contactResource.getLocalizedPostalInfo(); } if (postalInfo != null) { if (postalInfo.getName() != null) { - vcardBuilder.add(ImmutableList.of("fn", ImmutableMap.of(), "text", postalInfo.getName())); + vcardBuilder.add(Vcard.create("fn", "text", postalInfo.getName())); } if (postalInfo.getOrg() != null) { - vcardBuilder.add(ImmutableList.of("org", ImmutableMap.of(), "text", postalInfo.getOrg())); - } - ImmutableList addressEntry = makeVCardAddressEntry(postalInfo.getAddress()); - if (addressEntry != null) { - vcardBuilder.add(addressEntry); + vcardBuilder.add(Vcard.create("org", "text", postalInfo.getOrg())); } + addVCardAddressEntry(vcardBuilder, postalInfo.getAddress()); } ContactPhoneNumber voicePhoneNumber = contactResource.getVoiceNumber(); if (voicePhoneNumber != null) { @@ -793,91 +449,66 @@ public class RdapJsonFormatter { } String emailAddress = contactResource.getEmailAddress(); if (emailAddress != null) { - vcardBuilder.add(ImmutableList.of("email", ImmutableMap.of(), "text", emailAddress)); + vcardBuilder.add(Vcard.create("email", "text", emailAddress)); } - jsonBuilder.put("vcardArray", ImmutableList.of("vcard", vcardBuilder.build())); + entityBuilder.setVcardArray(vcardBuilder.build()); } else { - remarksBuilder.add(RdapIcannStandardInformation.CONTACT_PERSONAL_DATA_HIDDEN_DATA_REMARK); + entityBuilder + .remarksBuilder() + .add(RdapIcannStandardInformation.CONTACT_PERSONAL_DATA_HIDDEN_DATA_REMARK); } // If we are outputting all data (not just summary data), also add events taken from the history // entries. If we are outputting summary data, instead add a remark indicating that fact. if (outputDataType == OutputDataType.SUMMARY) { - remarksBuilder.add(RdapIcannStandardInformation.SUMMARY_DATA_REMARK); + entityBuilder.remarksBuilder().add(RdapIcannStandardInformation.SUMMARY_DATA_REMARK); } else { - ImmutableList events = makeEvents(contactResource, now); - if (!events.isEmpty()) { - jsonBuilder.put("events", events); - } + entityBuilder.eventsBuilder().addAll(makeEvents(contactResource, now)); } if (whoisServer != null) { - jsonBuilder.put("port43", whoisServer); + entityBuilder.setPort43(Port43WhoisServer.create(whoisServer)); } - if (isTopLevel) { - addTopLevelEntries( - jsonBuilder, - BoilerplateType.ENTITY, - remarksBuilder.build(), - ImmutableList.of(), - linkBase); - } else { - ImmutableList> remarks = remarksBuilder.build(); - if (!remarks.isEmpty()) { - jsonBuilder.put(REMARKS, remarks); - } - } - return jsonBuilder.build(); + return entityBuilder.build(); } /** * Creates a JSON object for a {@link Registrar}. * * @param registrar the registrar object from which the JSON object should be created - * @param isTopLevel if true, the top-level boilerplate will be added - * @param linkBase the URL base to be used when creating links * @param whoisServer the fully-qualified domain name of the WHOIS server to be listed in the * port43 field; if null, port43 is not added to the object * @param now the as-date * @param outputDataType whether to generate full or summary data */ - ImmutableMap makeRdapJsonForRegistrar( + RdapEntity makeRdapJsonForRegistrar( Registrar registrar, - boolean isTopLevel, - @Nullable String linkBase, @Nullable String whoisServer, DateTime now, OutputDataType outputDataType) { - ImmutableMap.Builder jsonBuilder = new ImmutableMap.Builder<>(); - jsonBuilder.put("objectClassName", "entity"); + RdapEntity.Builder builder = RdapEntity.builder(); Long ianaIdentifier = registrar.getIanaIdentifier(); - jsonBuilder.put("handle", (ianaIdentifier == null) ? "(none)" : ianaIdentifier.toString()); - jsonBuilder.put("status", registrar.isLive() ? STATUS_LIST_ACTIVE : STATUS_LIST_INACTIVE); - jsonBuilder.put("roles", ImmutableList.of(RdapEntityRole.REGISTRAR.rfc7483String)); + // the handle MUST be the ianaIdentifier, RDAP Response Profile 2.4.2. + builder.setHandle((ianaIdentifier == null) ? "(none)" : ianaIdentifier.toString()); + builder.statusBuilder().addAll(registrar.isLive() ? STATUS_LIST_ACTIVE : STATUS_LIST_INACTIVE); + builder.rolesBuilder().add(RdapEntity.Role.REGISTRAR); if (ianaIdentifier != null) { - jsonBuilder.put("links", - ImmutableList.of(makeLink("entity", ianaIdentifier.toString(), linkBase))); - jsonBuilder.put( - "publicIds", - ImmutableList.of( - ImmutableMap.of( - "type", "IANA Registrar ID", "identifier", ianaIdentifier.toString()))); + builder.linksBuilder().add(makeSelfLink("entity", ianaIdentifier.toString())); + // We MUST have a publicId with the ianaIdentifier, RDAP Response Profile 2.4.3, 4.3 + builder + .publicIdsBuilder() + .add(PublicId.create(PublicId.Type.IANA_REGISTRAR_ID, ianaIdentifier.toString())); } // Create the vCard. - ImmutableList.Builder vcardBuilder = new ImmutableList.Builder<>(); - vcardBuilder.add(VCARD_ENTRY_VERSION); + VcardArray.Builder vcardBuilder = VcardArray.builder(); String registrarName = registrar.getRegistrarName(); if (registrarName != null) { - vcardBuilder.add(ImmutableList.of("fn", ImmutableMap.of(), "text", registrarName)); + // A valid fn member MUST be present: RDAP Response Profile 2.4.1. + vcardBuilder.add(Vcard.create("fn", "text", registrarName)); } RegistrarAddress address = registrar.getInternationalizedAddress(); if (address == null) { address = registrar.getLocalizedAddress(); } - if (address != null) { - ImmutableList addressEntry = makeVCardAddressEntry(address); - if (addressEntry != null) { - vcardBuilder.add(addressEntry); - } - } + addVCardAddressEntry(vcardBuilder, address); String voicePhoneNumber = registrar.getPhoneNumber(); if (voicePhoneNumber != null) { vcardBuilder.add(makePhoneEntry(PHONE_TYPE_VOICE, "tel:" + voicePhoneNumber)); @@ -888,77 +519,55 @@ public class RdapJsonFormatter { } String emailAddress = registrar.getEmailAddress(); if (emailAddress != null) { - vcardBuilder.add(ImmutableList.of("email", ImmutableMap.of(), "text", emailAddress)); + vcardBuilder.add(Vcard.create("email", "text", emailAddress)); } - jsonBuilder.put("vcardArray", ImmutableList.of("vcard", vcardBuilder.build())); + builder.setVcardArray(vcardBuilder.build()); // If we are outputting all data (not just summary data), also add registrar contacts. If we are // outputting summary data, instead add a remark indicating that fact. - List> remarks; if (outputDataType == OutputDataType.SUMMARY) { - remarks = ImmutableList.of(RdapIcannStandardInformation.SUMMARY_DATA_REMARK); + builder.remarksBuilder().add(RdapIcannStandardInformation.SUMMARY_DATA_REMARK); } else { - remarks = ImmutableList.of(); - ImmutableList events = makeEvents(registrar, now); - if (!events.isEmpty()) { - jsonBuilder.put("events", events); - } + builder.eventsBuilder().addAll(makeEvents(registrar, now)); // include the registrar contacts as subentities - ImmutableList> registrarContacts = + ImmutableList registrarContacts = registrar.getContacts().stream() - .map(registrarContact -> makeRdapJsonForRegistrarContact(registrarContact, null)) - .filter(entity -> !entity.isEmpty()) - .collect(toImmutableList()); + .map(registrarContact -> makeRdapJsonForRegistrarContact(registrarContact, null)) + .filter(optional -> optional.isPresent()) + .map(optional -> optional.get()) + .collect(toImmutableList()); // TODO(b/117242274): add a warning (severe?) log if registrar has no ABUSE contact, as having // one is required by the RDAP response profile - if (!registrarContacts.isEmpty()) { - jsonBuilder.put("entities", registrarContacts); - } + builder.entitiesBuilder().addAll(registrarContacts); } if (whoisServer != null) { - jsonBuilder.put("port43", whoisServer); + builder.setPort43(Port43WhoisServer.create(whoisServer)); } - if (isTopLevel) { - addTopLevelEntries( - jsonBuilder, - BoilerplateType.ENTITY, - remarks, - ImmutableList.of(), - linkBase); - } else if (!remarks.isEmpty()) { - jsonBuilder.put(REMARKS, remarks); - } - return jsonBuilder.build(); + return builder.build(); } /** * Creates a JSON object for a {@link RegistrarContact}. * - *

Returns an empty object if this contact shouldn't be visible (doesn't have a role). + *

Returns empty if this contact shouldn't be visible (doesn't have a role). * * @param registrarContact the registrar contact for which the JSON object should be created * @param whoisServer the fully-qualified domain name of the WHOIS server to be listed in the * port43 field; if null, port43 is not added to the object */ - static ImmutableMap makeRdapJsonForRegistrarContact( + static Optional makeRdapJsonForRegistrarContact( RegistrarContact registrarContact, @Nullable String whoisServer) { - ImmutableList roles = makeRdapRoleList(registrarContact); + ImmutableList roles = makeRdapRoleList(registrarContact); if (roles.isEmpty()) { - return ImmutableMap.of(); + return Optional.empty(); } - ImmutableMap.Builder jsonBuilder = new ImmutableMap.Builder<>(); - jsonBuilder.put("objectClassName", "entity"); - String gaeUserId = registrarContact.getGaeUserId(); - if (gaeUserId != null) { - jsonBuilder.put("handle", registrarContact.getGaeUserId()); - } - jsonBuilder.put("status", STATUS_LIST_ACTIVE); - jsonBuilder.put("roles", roles); + RdapEntity.Builder builder = RdapEntity.builder(); + builder.statusBuilder().addAll(STATUS_LIST_ACTIVE); + builder.rolesBuilder().addAll(roles); // Create the vCard. - ImmutableList.Builder vcardBuilder = new ImmutableList.Builder<>(); - vcardBuilder.add(VCARD_ENTRY_VERSION); + VcardArray.Builder vcardBuilder = VcardArray.builder(); String name = registrarContact.getName(); if (name != null) { - vcardBuilder.add(ImmutableList.of("fn", ImmutableMap.of(), "text", name)); + vcardBuilder.add(Vcard.create("fn", "text", name)); } String voicePhoneNumber = registrarContact.getPhoneNumber(); if (voicePhoneNumber != null) { @@ -970,26 +579,26 @@ public class RdapJsonFormatter { } String emailAddress = registrarContact.getEmailAddress(); if (emailAddress != null) { - vcardBuilder.add(ImmutableList.of("email", ImmutableMap.of(), "text", emailAddress)); + vcardBuilder.add(Vcard.create("email", "text", emailAddress)); } - jsonBuilder.put("vcardArray", ImmutableList.of("vcard", vcardBuilder.build())); + builder.setVcardArray(vcardBuilder.build()); if (whoisServer != null) { - jsonBuilder.put("port43", whoisServer); + builder.setPort43(Port43WhoisServer.create(whoisServer)); } - return jsonBuilder.build(); + return Optional.of(builder.build()); } /** Converts a domain registry contact type into a role as defined by RFC 7483. */ - private static String convertContactTypeToRdapRole(DesignatedContact.Type contactType) { + private static RdapEntity.Role convertContactTypeToRdapRole(DesignatedContact.Type contactType) { switch (contactType) { case REGISTRANT: - return RdapEntityRole.REGISTRANT.rfc7483String; + return RdapEntity.Role.REGISTRANT; case TECH: - return RdapEntityRole.TECH.rfc7483String; + return RdapEntity.Role.TECH; case BILLING: - return RdapEntityRole.BILLING.rfc7483String; + return RdapEntity.Role.BILLING; case ADMIN: - return RdapEntityRole.ADMIN.rfc7483String; + return RdapEntity.Role.ADMIN; default: throw new AssertionError(); } @@ -1006,16 +615,17 @@ public class RdapJsonFormatter { * *abuse* role within the registrar *entity* which MUST include *tel* and *email*, and MAY * include other members */ - private static ImmutableList makeRdapRoleList(RegistrarContact registrarContact) { - ImmutableList.Builder rolesBuilder = new ImmutableList.Builder<>(); + private static ImmutableList makeRdapRoleList( + RegistrarContact registrarContact) { + ImmutableList.Builder rolesBuilder = new ImmutableList.Builder<>(); if (registrarContact.getVisibleInWhoisAsAdmin()) { - rolesBuilder.add(RdapEntityRole.ADMIN.rfc7483String); + rolesBuilder.add(RdapEntity.Role.ADMIN); } if (registrarContact.getVisibleInWhoisAsTech()) { - rolesBuilder.add(RdapEntityRole.TECH.rfc7483String); + rolesBuilder.add(RdapEntity.Role.TECH); } if (registrarContact.getVisibleInDomainWhoisAsAbuse()) { - rolesBuilder.add(RdapEntityRole.ABUSE.rfc7483String); + rolesBuilder.add(RdapEntity.Role.ABUSE); } return rolesBuilder.build(); } @@ -1023,8 +633,8 @@ public class RdapJsonFormatter { /** * Creates an event list for a domain, host or contact resource. */ - private static ImmutableList makeEvents(EppResource resource, DateTime now) { - HashMap lastEntryOfType = Maps.newHashMap(); + private static ImmutableList makeEvents(EppResource resource, DateTime now) { + HashMap lastEntryOfType = Maps.newHashMap(); // Events (such as transfer, but also create) can appear multiple times. We only want the last // time they appeared. // @@ -1038,7 +648,7 @@ public class RdapJsonFormatter { // domain name has not been transferred since it was created. for (HistoryEntry historyEntry : ofy().load().type(HistoryEntry.class).ancestor(resource).order("modificationTime")) { - RdapEventAction rdapEventAction = + EventAction rdapEventAction = HISTORY_ENTRY_TYPE_TO_RDAP_EVENT_ACTION_MAP.get(historyEntry.getType()); // Only save the historyEntries if this is a type we care about. if (rdapEventAction == null) { @@ -1046,7 +656,7 @@ public class RdapJsonFormatter { } lastEntryOfType.put(rdapEventAction, historyEntry); } - ImmutableList.Builder eventsBuilder = new ImmutableList.Builder<>(); + ImmutableList.Builder eventsBuilder = new ImmutableList.Builder<>(); // There are 2 possibly conflicting values for the creation time - either the // resource.getCreationTime, or the REGISTRATION event created from a HistoryEntry // @@ -1056,15 +666,15 @@ public class RdapJsonFormatter { // This is mostly an issue in the tests as in "reality" these two values should be the same. // DateTime creationTime = - Optional.ofNullable(lastEntryOfType.get(RdapEventAction.REGISTRATION)) + Optional.ofNullable(lastEntryOfType.get(EventAction.REGISTRATION)) .map(historyEntry -> historyEntry.getModificationTime()) .orElse(resource.getCreationTime()); // TODO(b/129849684) remove this and use the events List defined above once we have Event // objects ImmutableList.Builder changeTimesBuilder = new ImmutableList.Builder<>(); // The order of the elements is stable - it's the order in which the enum elements are defined - // in RdapEventAction - for (RdapEventAction rdapEventAction : RdapEventAction.values()) { + // in EventAction + for (EventAction rdapEventAction : EventAction.values()) { HistoryEntry historyEntry = lastEntryOfType.get(rdapEventAction); // Check if there was any entry of this type if (historyEntry == null) { @@ -1077,13 +687,22 @@ public class RdapJsonFormatter { if (modificationTime.isBefore(creationTime)) { continue; } - eventsBuilder.add(makeEvent(rdapEventAction, historyEntry.getClientId(), modificationTime)); + eventsBuilder.add( + Event.builder() + .setEventAction(rdapEventAction) + .setEventActor(historyEntry.getClientId()) + .setEventDate(modificationTime) + .build()); changeTimesBuilder.add(modificationTime); } if (resource instanceof DomainBase) { DateTime expirationTime = ((DomainBase) resource).getRegistrationExpirationTime(); if (expirationTime != null) { - eventsBuilder.add(makeEvent(RdapEventAction.EXPIRATION, null, expirationTime)); + eventsBuilder.add( + Event.builder() + .setEventAction(EventAction.EXPIRATION) + .setEventDate(expirationTime) + .build()); changeTimesBuilder.add(expirationTime); } } @@ -1098,9 +717,9 @@ public class RdapJsonFormatter { .max(DateTimeComparator.getInstance()) .orElse(null); if (lastChangeTime != null && lastChangeTime.isAfter(creationTime)) { - eventsBuilder.add(makeEvent(RdapEventAction.LAST_CHANGED, null, lastChangeTime)); + eventsBuilder.add(makeEvent(EventAction.LAST_CHANGED, null, lastChangeTime)); } - eventsBuilder.add(makeEvent(RdapEventAction.LAST_UPDATE_OF_RDAP_DATABASE, null, now)); + eventsBuilder.add(makeEvent(EventAction.LAST_UPDATE_OF_RDAP_DATABASE, null, now)); // TODO(b/129849684): sort events by their time once we return a list of Events instead of JSON // objects. return eventsBuilder.build(); @@ -1109,34 +728,36 @@ public class RdapJsonFormatter { /** * Creates an event list for a {@link Registrar}. */ - private static ImmutableList makeEvents(Registrar registrar, DateTime now) { - ImmutableList.Builder eventsBuilder = new ImmutableList.Builder<>(); + private static ImmutableList makeEvents(Registrar registrar, DateTime now) { + ImmutableList.Builder eventsBuilder = new ImmutableList.Builder<>(); Long ianaIdentifier = registrar.getIanaIdentifier(); eventsBuilder.add(makeEvent( - RdapEventAction.REGISTRATION, + EventAction.REGISTRATION, (ianaIdentifier == null) ? "(none)" : ianaIdentifier.toString(), registrar.getCreationTime())); if ((registrar.getLastUpdateTime() != null) && registrar.getLastUpdateTime().isAfter(registrar.getCreationTime())) { eventsBuilder.add(makeEvent( - RdapEventAction.LAST_CHANGED, null, registrar.getLastUpdateTime())); + EventAction.LAST_CHANGED, null, registrar.getLastUpdateTime())); } - eventsBuilder.add(makeEvent(RdapEventAction.LAST_UPDATE_OF_RDAP_DATABASE, null, now)); + eventsBuilder.add(makeEvent(EventAction.LAST_UPDATE_OF_RDAP_DATABASE, null, now)); return eventsBuilder.build(); } /** * Creates an RDAP event object as defined by RFC 7483. */ - private static ImmutableMap makeEvent( - RdapEventAction eventAction, @Nullable String eventActor, DateTime eventDate) { - ImmutableMap.Builder jsonBuilder = new ImmutableMap.Builder<>(); - jsonBuilder.put("eventAction", eventAction.getDisplayName()); + private static Event makeEvent( + EventAction eventAction, + @Nullable String eventActor, + DateTime eventDate) { + Event.Builder builder = Event.builder() + .setEventAction(eventAction) + .setEventDate(eventDate); if (eventActor != null) { - jsonBuilder.put("eventActor", eventActor); + builder.setEventActor(eventActor); } - jsonBuilder.put("eventDate", eventDate.toString()); - return jsonBuilder.build(); + return builder.build(); } /** @@ -1145,13 +766,13 @@ public class RdapJsonFormatter { * @see * RFC 7095: jCard: The JSON Format for vCard */ - private static ImmutableList makeVCardAddressEntry(Address address) { + private static void addVCardAddressEntry(VcardArray.Builder vcardArrayBuilder, Address address) { if (address == null) { - return null; + return; } - ImmutableList.Builder jsonBuilder = new ImmutableList.Builder<>(); - jsonBuilder.add(""); // PO box - jsonBuilder.add(""); // extended address + JsonArray addressArray = new JsonArray(); + addressArray.add(""); // PO box + addressArray.add(""); // extended address // The vCard spec allows several different ways to handle multiline street addresses. Per // Gustavo Lozano of ICANN, the one we should use is an embedded array of street address lines @@ -1181,27 +802,30 @@ public class RdapJsonFormatter { // according to RFC 7095. ImmutableList street = address.getStreet(); if (street.isEmpty()) { - jsonBuilder.add(""); + addressArray.add(""); } else if (street.size() == 1) { - jsonBuilder.add(street.get(0)); + addressArray.add(street.get(0)); } else { - jsonBuilder.add(street); + JsonArray streetArray = new JsonArray(); + street.forEach(streetArray::add); + addressArray.add(streetArray); } - jsonBuilder.add(nullToEmpty(address.getCity())); - jsonBuilder.add(nullToEmpty(address.getState())); - jsonBuilder.add(nullToEmpty(address.getZip())); - jsonBuilder.add(new Locale("en", address.getCountryCode()).getDisplayCountry(new Locale("en"))); - return ImmutableList.of( + addressArray.add(nullToEmpty(address.getCity())); + addressArray.add(nullToEmpty(address.getState())); + addressArray.add(nullToEmpty(address.getZip())); + addressArray.add( + new Locale("en", address.getCountryCode()).getDisplayCountry(new Locale("en"))); + vcardArrayBuilder.add(Vcard.create( "adr", - ImmutableMap.of(), "text", - jsonBuilder.build()); + addressArray)); } /** Creates a vCard phone number entry. */ - private static ImmutableList makePhoneEntry( + private static Vcard makePhoneEntry( ImmutableMap> type, String phoneNumber) { - return ImmutableList.of("tel", type, "uri", phoneNumber); + + return Vcard.create("tel", type, "uri", phoneNumber); } /** Creates a phone string in URI format, as per the vCard spec. */ @@ -1220,7 +844,7 @@ public class RdapJsonFormatter { * indicate deleted objects, and as directed by the profile, the "removed" status to indicate * redacted objects. */ - private static ImmutableList makeStatusValueList( + private static ImmutableList makeStatusValueList( ImmutableSet statusValues, boolean isRedacted, boolean isDeleted) { Stream stream = statusValues @@ -1236,51 +860,34 @@ public class RdapJsonFormatter { Stream.of(RdapStatus.INACTIVE)); } return stream - .map(RdapStatus::getDisplayName) - .collect(toImmutableSortedSet(Ordering.natural())) - .asList(); + .sorted(Ordering.natural().onResultOf(RdapStatus::getDisplayName)) + .collect(toImmutableList()); + } + + /** + * Create a link relative to the RDAP server endpoint. + */ + String makeRdapServletRelativeUrl(String part, String... moreParts) { + String relativePath = Paths.get(part, moreParts).toString(); + if (fullServletPath.endsWith("/")) { + return fullServletPath + relativePath; + } + return fullServletPath + "/" + relativePath; } /** * Creates a self link as directed by the spec. * - * @see - * RFC 7483: JSON Responses for the Registration Data Access Protocol (RDAP) + * @see RFC 7483: JSON Responses for the + * Registration Data Access Protocol (RDAP) */ - private static ImmutableMap makeLink( - String type, String name, @Nullable String linkBase) { - String url; - if (linkBase == null) { - url = type + '/' + name; - } else if (linkBase.endsWith("/")) { - url = linkBase + type + '/' + name; - } else { - url = linkBase + '/' + type + '/' + name; - } - return ImmutableMap.of( - "value", url, - "rel", "self", - "href", url, - "type", "application/rdap+json"); - } - - /** - * Creates a JSON error indication. - * - * @see - * RFC 7483: JSON Responses for the Registration Data Access Protocol (RDAP) - */ - ImmutableMap makeError(int status, String title, String description) { - return ImmutableMap.of( - "rdapConformance", CONFORMANCE_LIST, - "lang", "en", - "errorCode", (long) status, - "title", title, - "description", ImmutableList.of(description)); - } - - private static boolean hasUnicodeComponents(String fullyQualifiedName) { - return fullyQualifiedName.startsWith(ACE_PREFIX) - || fullyQualifiedName.contains("." + ACE_PREFIX); + private Link makeSelfLink(String type, String name) { + String url = makeRdapServletRelativeUrl(type, name); + return Link.builder() + .setValue(url) + .setRel("self") + .setHref(url) + .setType("application/rdap+json") + .build(); } } diff --git a/java/google/registry/rdap/RdapNameserverAction.java b/java/google/registry/rdap/RdapNameserverAction.java index 148945cd6..95592e133 100644 --- a/java/google/registry/rdap/RdapNameserverAction.java +++ b/java/google/registry/rdap/RdapNameserverAction.java @@ -20,11 +20,11 @@ import static google.registry.request.Action.Method.GET; import static google.registry.request.Action.Method.HEAD; import static google.registry.util.DateTimeUtils.START_OF_TIME; -import com.google.common.collect.ImmutableMap; import google.registry.flows.EppException; import google.registry.model.host.HostResource; import google.registry.rdap.RdapJsonFormatter.OutputDataType; import google.registry.rdap.RdapMetrics.EndpointType; +import google.registry.rdap.RdapObjectClasses.RdapNameserver; import google.registry.request.Action; import google.registry.request.HttpException.BadRequestException; import google.registry.request.HttpException.NotFoundException; @@ -47,8 +47,7 @@ public class RdapNameserverAction extends RdapActionBase { } @Override - public ImmutableMap getJsonObjectForResource( - String pathSearchString, boolean isHeadRequest) { + public RdapNameserver getJsonObjectForResource(String pathSearchString, boolean isHeadRequest) { DateTime now = clock.nowUtc(); pathSearchString = canonicalizeName(pathSearchString); // The RDAP syntax is /rdap/nameserver/ns1.mydomain.com. @@ -69,6 +68,6 @@ public class RdapNameserverAction extends RdapActionBase { throw new NotFoundException(pathSearchString + " not found"); } return rdapJsonFormatter.makeRdapJsonForHost( - hostResource.get(), true, fullServletPath, rdapWhoisServer, now, OutputDataType.FULL); + hostResource.get(), rdapWhoisServer, now, OutputDataType.FULL); } } diff --git a/java/google/registry/rdap/RdapNameserverSearchAction.java b/java/google/registry/rdap/RdapNameserverSearchAction.java index 5f4f47602..96116fc08 100644 --- a/java/google/registry/rdap/RdapNameserverSearchAction.java +++ b/java/google/registry/rdap/RdapNameserverSearchAction.java @@ -18,8 +18,6 @@ import static google.registry.model.EppResourceUtils.loadByForeignKey; import static google.registry.request.Action.Method.GET; import static google.registry.request.Action.Method.HEAD; -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.net.InetAddresses; @@ -27,11 +25,11 @@ import com.google.common.primitives.Booleans; import com.googlecode.objectify.cmd.Query; import google.registry.model.domain.DomainBase; import google.registry.model.host.HostResource; -import google.registry.rdap.RdapJsonFormatter.BoilerplateType; import google.registry.rdap.RdapJsonFormatter.OutputDataType; import google.registry.rdap.RdapMetrics.EndpointType; import google.registry.rdap.RdapMetrics.SearchType; import google.registry.rdap.RdapSearchResults.IncompletenessWarningType; +import google.registry.rdap.RdapSearchResults.NameserverSearchResponse; import google.registry.request.Action; import google.registry.request.HttpException.BadRequestException; import google.registry.request.HttpException.NotFoundException; @@ -82,7 +80,7 @@ public class RdapNameserverSearchAction extends RdapSearchActionBase { *

The RDAP spec allows nameserver search by either name or IP address. */ @Override - public ImmutableMap getJsonObjectForResource( + public NameserverSearchResponse getJsonObjectForResource( String pathSearchString, boolean isHeadRequest) { DateTime now = clock.nowUtc(); // RDAP syntax example: /rdap/nameservers?name=ns*.example.com. @@ -94,7 +92,7 @@ public class RdapNameserverSearchAction extends RdapSearchActionBase { throw new BadRequestException("You must specify either name=XXXX or ip=YYYY"); } decodeCursorToken(); - RdapSearchResults results; + NameserverSearchResponse results; if (nameParam.isPresent()) { // syntax: /rdap/nameservers?name=exam*.com metricInformationBuilder.setSearchType(SearchType.BY_NAMESERVER_NAME); @@ -118,19 +116,10 @@ public class RdapNameserverSearchAction extends RdapSearchActionBase { } results = searchByIp(inetAddress, now); } - if (results.jsonList().isEmpty()) { + if (results.nameserverSearchResults().isEmpty()) { throw new NotFoundException("No nameservers found"); } - ImmutableMap.Builder jsonBuilder = new ImmutableMap.Builder<>(); - jsonBuilder.put("nameserverSearchResults", results.jsonList()); - - rdapJsonFormatter.addTopLevelEntries( - jsonBuilder, - BoilerplateType.NAMESERVER, - getNotices(results), - ImmutableList.of(), - fullServletPath); - return jsonBuilder.build(); + return results; } /** @@ -139,7 +128,7 @@ public class RdapNameserverSearchAction extends RdapSearchActionBase { *

When deleted nameservers are included in the search, the search is treated as if it has a * wildcard, because multiple results can be returned. */ - private RdapSearchResults searchByName( + private NameserverSearchResponse searchByName( final RdapSearchPattern partialStringQuery, final DateTime now) { // Handle queries without a wildcard -- just load by foreign key. We can't do this if deleted // nameservers are desired, because there may be multiple nameservers with the same name. @@ -168,7 +157,7 @@ public class RdapNameserverSearchAction extends RdapSearchActionBase { * *

In this case, we can load by foreign key. */ - private RdapSearchResults searchByNameUsingForeignKey( + private NameserverSearchResponse searchByNameUsingForeignKey( final RdapSearchPattern partialStringQuery, final DateTime now) { Optional hostResource = loadByForeignKey(HostResource.class, partialStringQuery.getInitialString(), now); @@ -177,19 +166,21 @@ public class RdapNameserverSearchAction extends RdapSearchActionBase { throw new NotFoundException("No nameservers found"); } metricInformationBuilder.setNumHostsRetrieved(1); - return RdapSearchResults.create( - ImmutableList.of( + + NameserverSearchResponse.Builder builder = + NameserverSearchResponse.builder() + .setIncompletenessWarningType(IncompletenessWarningType.COMPLETE); + builder.nameserverSearchResultsBuilder().add( rdapJsonFormatter.makeRdapJsonForHost( hostResource.get(), - false, - fullServletPath, rdapWhoisServer, now, - OutputDataType.FULL))); + OutputDataType.FULL)); + return builder.build(); } /** Searches for nameservers by name using the superordinate domain as a suffix. */ - private RdapSearchResults searchByNameUsingSuperordinateDomain( + private NameserverSearchResponse searchByNameUsingSuperordinateDomain( final RdapSearchPattern partialStringQuery, final DateTime now) { Optional domainBase = loadByForeignKey(DomainBase.class, partialStringQuery.getSuffix(), now); @@ -232,7 +223,7 @@ public class RdapNameserverSearchAction extends RdapSearchActionBase { * *

There are no pending deletes for hosts, so we can call {@link RdapActionBase#queryItems}. */ - private RdapSearchResults searchByNameUsingPrefix( + private NameserverSearchResponse searchByNameUsingPrefix( final RdapSearchPattern partialStringQuery, final DateTime now) { // Add 1 so we can detect truncation. int querySizeLimit = getStandardQuerySizeLimit(); @@ -251,7 +242,7 @@ public class RdapNameserverSearchAction extends RdapSearchActionBase { } /** Searches for nameservers by IP address, returning a JSON array of nameserver info maps. */ - private RdapSearchResults searchByIp(final InetAddress inetAddress, DateTime now) { + private NameserverSearchResponse searchByIp(final InetAddress inetAddress, DateTime now) { // Add 1 so we can detect truncation. int querySizeLimit = getStandardQuerySizeLimit(); Query query = @@ -270,7 +261,7 @@ public class RdapNameserverSearchAction extends RdapSearchActionBase { } /** Output JSON for a lists of hosts contained in an {@link RdapResultSet}. */ - private RdapSearchResults makeSearchResults( + private NameserverSearchResponse makeSearchResults( RdapResultSet resultSet, CursorType cursorType, DateTime now) { return makeSearchResults( resultSet.resources(), @@ -281,7 +272,7 @@ public class RdapNameserverSearchAction extends RdapSearchActionBase { } /** Output JSON for a list of hosts. */ - private RdapSearchResults makeSearchResults( + private NameserverSearchResponse makeSearchResults( List hosts, IncompletenessWarningType incompletenessWarningType, int numHostsRetrieved, @@ -290,8 +281,8 @@ public class RdapNameserverSearchAction extends RdapSearchActionBase { metricInformationBuilder.setNumHostsRetrieved(numHostsRetrieved); OutputDataType outputDataType = (hosts.size() > 1) ? OutputDataType.SUMMARY : OutputDataType.FULL; - ImmutableList.Builder> jsonListBuilder = - new ImmutableList.Builder<>(); + NameserverSearchResponse.Builder builder = + NameserverSearchResponse.builder().setIncompletenessWarningType(incompletenessWarningType); Optional newCursor = Optional.empty(); for (HostResource host : Iterables.limit(hosts, rdapResultSetMaxSize)) { newCursor = @@ -299,15 +290,14 @@ public class RdapNameserverSearchAction extends RdapSearchActionBase { (cursorType == CursorType.NAME) ? host.getFullyQualifiedHostName() : host.getRepoId()); - jsonListBuilder.add( + builder.nameserverSearchResultsBuilder().add( rdapJsonFormatter.makeRdapJsonForHost( - host, false, fullServletPath, rdapWhoisServer, now, outputDataType)); + host, rdapWhoisServer, now, outputDataType)); } - ImmutableList> jsonList = jsonListBuilder.build(); - if (jsonList.size() < hosts.size()) { - return RdapSearchResults.create(jsonList, IncompletenessWarningType.TRUNCATED, newCursor); - } else { - return RdapSearchResults.create(jsonList, incompletenessWarningType, Optional.empty()); + if (rdapResultSetMaxSize < hosts.size()) { + builder.setNextPageUri(createNavigationUri(newCursor.get())); + builder.setIncompletenessWarningType(IncompletenessWarningType.TRUNCATED); } + return builder.build(); } } diff --git a/java/google/registry/rdap/RdapObjectClasses.java b/java/google/registry/rdap/RdapObjectClasses.java new file mode 100644 index 000000000..fa10a5416 --- /dev/null +++ b/java/google/registry/rdap/RdapObjectClasses.java @@ -0,0 +1,442 @@ +// Copyright 2019 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.util.DomainNameUtils.ACE_PREFIX; + +import com.google.auto.value.AutoValue; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Ordering; +import com.google.gson.Gson; +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonPrimitive; +import google.registry.rdap.AbstractJsonableObject.RestrictJsonNames; +import google.registry.rdap.RdapDataStructures.Event; +import google.registry.rdap.RdapDataStructures.EventWithoutActor; +import google.registry.rdap.RdapDataStructures.LanguageIdentifier; +import google.registry.rdap.RdapDataStructures.Link; +import google.registry.rdap.RdapDataStructures.Notice; +import google.registry.rdap.RdapDataStructures.ObjectClassName; +import google.registry.rdap.RdapDataStructures.Port43WhoisServer; +import google.registry.rdap.RdapDataStructures.PublicId; +import google.registry.rdap.RdapDataStructures.RdapConformance; +import google.registry.rdap.RdapDataStructures.RdapStatus; +import google.registry.rdap.RdapDataStructures.Remark; +import google.registry.util.Idn; +import java.util.Optional; + +/** + * Object Classes defined in RFC7483 section 5. + */ +final class RdapObjectClasses { + + /** + * Temporary implementation of VCards. + * + * Will create a better implementation soon. + */ + @RestrictJsonNames({}) + @AutoValue + abstract static class Vcard implements Jsonable { + abstract String property(); + abstract ImmutableMap> parameters(); + abstract String valueType(); + abstract JsonElement value(); + + static Vcard create( + String property, + ImmutableMap> parameters, + String valueType, + JsonElement value) { + return new AutoValue_RdapObjectClasses_Vcard(property, parameters, valueType, value); + } + + static Vcard create( + String property, + ImmutableMap> parameters, + String valueType, + String value) { + return create(property, parameters, valueType, new JsonPrimitive(value)); + } + + static Vcard create(String property, String valueType, JsonElement value) { + return create(property, ImmutableMap.of(), valueType, value); + } + + static Vcard create(String property, String valueType, String value) { + return create(property, valueType, new JsonPrimitive(value)); + } + + @Override + public JsonArray toJson() { + JsonArray jsonArray = new JsonArray(); + jsonArray.add(property()); + jsonArray.add(new Gson().toJsonTree(parameters())); + jsonArray.add(valueType()); + jsonArray.add(value()); + return jsonArray; + } + } + + @RestrictJsonNames("vcardArray") + @AutoValue + abstract static class VcardArray implements Jsonable { + + private static final String VCARD_VERSION_NUMBER = "4.0"; + private static final Vcard VCARD_ENTRY_VERSION = + Vcard.create("version", "text", VCARD_VERSION_NUMBER); + + abstract ImmutableList vcards(); + + @Override + public JsonArray toJson() { + JsonArray jsonArray = new JsonArray(); + jsonArray.add("vcard"); + JsonArray jsonVcardsArray = new JsonArray(); + jsonVcardsArray.add(VCARD_ENTRY_VERSION.toJson()); + vcards().forEach(vcard -> jsonVcardsArray.add(vcard.toJson())); + jsonArray.add(jsonVcardsArray); + return jsonArray; + } + + static Builder builder() { + return new AutoValue_RdapObjectClasses_VcardArray.Builder(); + } + + @AutoValue.Builder + abstract static class Builder { + abstract ImmutableList.Builder vcardsBuilder(); + Builder add(Vcard vcard) { + vcardsBuilder().add(vcard); + return this; + } + + abstract VcardArray build(); + } + } + + /** + * Indication of what type of boilerplate notices are required for the RDAP JSON messages. The + * ICANN RDAP Profile specifies that, for instance, domain name responses should include a remark + * about domain status codes. So we need to know when to include such boilerplate. On the other + * hand, remarks are not allowed except in domain, nameserver and entity objects, so we need to + * suppress them for other types of responses (e.g. help). + */ + public enum BoilerplateType { + DOMAIN, + NAMESERVER, + ENTITY, + OTHER + } + + /** + * An object that can be used to create a TopLevelReply. + * + * All Actions need to return an object of this type. + */ + @RestrictJsonNames("*") + abstract static class ReplyPayloadBase extends AbstractJsonableObject { + final BoilerplateType boilerplateType; + + ReplyPayloadBase(BoilerplateType boilerplateType) { + this.boilerplateType = boilerplateType; + } + } + + /** + * The Top Level JSON reply, Adds the required top-level boilerplate to a ReplyPayloadBase. + * + *

RFC 7483 specifies that the top-level object should include an entry indicating the + * conformance level. ICANN RDAP spec for 15feb19 mandates several additional entries, in sections + * 2.6.3, 2.11 of the Response Profile and 3.3.2, 3.5, of the Technical Implementation Guide. + */ + @AutoValue + @RestrictJsonNames({}) + abstract static class TopLevelReplyObject extends AbstractJsonableObject { + @JsonableElement("rdapConformance") + static final RdapConformance RDAP_CONFORMANCE = RdapConformance.INSTANCE; + + @JsonableElement("*") abstract ReplyPayloadBase aAreplyObject(); + @JsonableElement("notices[]") abstract Notice aTosNotice(); + + @JsonableElement("notices") ImmutableList boilerplateNotices() { + switch (aAreplyObject().boilerplateType) { + case DOMAIN: + return RdapIcannStandardInformation.domainBoilerplateNotices; + case NAMESERVER: + case ENTITY: + return RdapIcannStandardInformation.nameserverAndEntityBoilerplateNotices; + default: // things other than domains, nameservers and entities do not yet have boilerplate + return ImmutableList.of(); + } + } + + static TopLevelReplyObject create(ReplyPayloadBase replyObject, Notice tosNotice) { + return new AutoValue_RdapObjectClasses_TopLevelReplyObject(replyObject, tosNotice); + } + } + + /** + * A base object shared by Entity, Nameserver, and Domain. + * + *

Not part of the spec, but seems convenient. + */ + private abstract static class RdapObjectBase extends ReplyPayloadBase { + @JsonableElement final ObjectClassName objectClassName; + + @JsonableElement abstract Optional handle(); + @JsonableElement abstract ImmutableList publicIds(); + @JsonableElement abstract ImmutableList entities(); + @JsonableElement abstract ImmutableList status(); + @JsonableElement abstract ImmutableList remarks(); + @JsonableElement abstract ImmutableList links(); + @JsonableElement abstract Optional port43(); + @JsonableElement abstract ImmutableList events(); + + RdapObjectBase(BoilerplateType boilerplateType, ObjectClassName objectClassName) { + super(boilerplateType); + this.objectClassName = objectClassName; + } + + + abstract static class Builder> { + abstract B setHandle(String handle); + abstract ImmutableList.Builder publicIdsBuilder(); + abstract ImmutableList.Builder entitiesBuilder(); + abstract ImmutableList.Builder statusBuilder(); + abstract ImmutableList.Builder remarksBuilder(); + abstract ImmutableList.Builder linksBuilder(); + abstract B setPort43(Port43WhoisServer port43); + abstract ImmutableList.Builder eventsBuilder(); + } + } + + /** + * The Entity Object Class defined in 5.1 of RFC7483. + * + *

We're missing the "autnums" and "networks" fields + */ + @RestrictJsonNames({"entities[]", "entitySearchResults[]"}) + @AutoValue + abstract static class RdapEntity extends RdapObjectBase { + + /** Role values specified in RFC 7483 § 10.2.4. */ + @RestrictJsonNames("roles[]") + enum Role implements Jsonable { + REGISTRANT("registrant"), + TECH("technical"), + ADMIN("administrative"), + ABUSE("abuse"), + BILLING("billing"), + REGISTRAR("registrar"), + RESELLER("reseller"), + SPONSOR("sponsor"), + PROXY("proxy"), + NOTIFICATIONS("notifications"), + NOC("noc"); + + /** Value as it appears in RDAP messages. */ + final String rfc7483String; + + Role(String rfc7483String) { + this.rfc7483String = rfc7483String; + } + + @Override + public JsonPrimitive toJson() { + return new JsonPrimitive(rfc7483String); + } + } + + RdapEntity() { + super(BoilerplateType.ENTITY, ObjectClassName.ENTITY); + } + + @JsonableElement abstract Optional vcardArray(); + @JsonableElement abstract ImmutableSet roles(); + @JsonableElement abstract ImmutableList asEventActor(); + + static Builder builder() { + return new AutoValue_RdapObjectClasses_RdapEntity.Builder(); + } + + @AutoValue.Builder + abstract static class Builder extends RdapObjectBase.Builder { + abstract Builder setVcardArray(VcardArray vcardArray); + abstract ImmutableSet.Builder rolesBuilder(); + abstract ImmutableList.Builder asEventActorBuilder(); + + abstract RdapEntity build(); + } + } + + /** + * A base object shared by Nameserver, and Domain. + * + *

Takes care of the name and unicode field. + * + *

See RDAP Response Profile 15feb19 sections 2.1 and 4.1. + * + *

Not part of the spec, but seems convenient. + */ + private abstract static class RdapNamedObjectBase extends RdapObjectBase { + + @JsonableElement abstract String ldhName(); + + @JsonableElement final Optional unicodeName() { + // Only include the unicodeName field if there are unicode characters. + // + // TODO(b/127490882) Consider removing the condition (i.e. always having the unicodeName + // field) + if (!hasUnicodeComponents(ldhName())) { + return Optional.empty(); + } + return Optional.of(Idn.toUnicode(ldhName())); + } + + private static boolean hasUnicodeComponents(String fullyQualifiedName) { + return fullyQualifiedName.startsWith(ACE_PREFIX) + || fullyQualifiedName.contains("." + ACE_PREFIX); + } + + abstract static class Builder> extends RdapObjectBase.Builder { + abstract B setLdhName(String ldhName); + } + + RdapNamedObjectBase(BoilerplateType boilerplateType, ObjectClassName objectClassName) { + super(boilerplateType, objectClassName); + } + } + + /** + * The Nameserver Object Class defined in 5.2 of RFC7483. + */ + @RestrictJsonNames({"nameservers[]", "nameserverSearchResults[]"}) + @AutoValue + abstract static class RdapNameserver extends RdapNamedObjectBase { + + @JsonableElement Optional ipAddresses() { + if (ipv6().isEmpty() && ipv4().isEmpty()) { + return Optional.empty(); + } + return Optional.of(new IpAddresses()); + } + + abstract ImmutableList ipv6(); + abstract ImmutableList ipv4(); + + class IpAddresses extends AbstractJsonableObject { + @JsonableElement ImmutableList v6() { + return Ordering.natural().immutableSortedCopy(ipv6()); + } + + @JsonableElement ImmutableList v4() { + return Ordering.natural().immutableSortedCopy(ipv4()); + } + } + + RdapNameserver() { + super(BoilerplateType.NAMESERVER, ObjectClassName.NAMESERVER); + } + + static Builder builder() { + return new AutoValue_RdapObjectClasses_RdapNameserver.Builder(); + } + + @AutoValue.Builder + abstract static class Builder extends RdapNamedObjectBase.Builder { + abstract ImmutableList.Builder ipv6Builder(); + abstract ImmutableList.Builder ipv4Builder(); + + abstract RdapNameserver build(); + } + + } + + /** + * The Domain Object Class defined in 5.3 of RFC7483. + * + * We're missing the "variants", "secureDNS", "network" fields + */ + @RestrictJsonNames("domainSearchResults[]") + @AutoValue + abstract static class RdapDomain extends RdapNamedObjectBase { + + @JsonableElement abstract ImmutableList nameservers(); + + RdapDomain() { + super(BoilerplateType.DOMAIN, ObjectClassName.DOMAIN); + } + + static Builder builder() { + return new AutoValue_RdapObjectClasses_RdapDomain.Builder(); + } + + @AutoValue.Builder + abstract static class Builder extends RdapNamedObjectBase.Builder { + abstract ImmutableList.Builder nameserversBuilder(); + + abstract RdapDomain build(); + } + } + + /** + * Error Response Body defined in 6 of RFC7483. + */ + @RestrictJsonNames({}) + @AutoValue + abstract static class ErrorResponse extends ReplyPayloadBase { + + @JsonableElement final LanguageIdentifier lang = LanguageIdentifier.EN; + + @JsonableElement abstract int errorCode(); + @JsonableElement abstract String title(); + @JsonableElement abstract ImmutableList description(); + + ErrorResponse() { + super(BoilerplateType.OTHER); + } + + static ErrorResponse create(int status, String title, String description) { + return new AutoValue_RdapObjectClasses_ErrorResponse( + status, title, ImmutableList.of(description)); + } + } + + /** + * Help Response defined in 7 of RFC7483. + * + *

The helpNotice field is optional, because if the user requests the TOS - that's already + * given by the boilerplate of TopLevelReplyObject so we don't want to give it again. + */ + @RestrictJsonNames({}) + @AutoValue + abstract static class HelpResponse extends ReplyPayloadBase { + @JsonableElement("notices[]") abstract Optional helpNotice(); + + HelpResponse() { + super(BoilerplateType.OTHER); + } + + static HelpResponse create(Optional helpNotice) { + return new AutoValue_RdapObjectClasses_HelpResponse(helpNotice); + } + } + + private RdapObjectClasses() {} +} diff --git a/java/google/registry/rdap/RdapSearchActionBase.java b/java/google/registry/rdap/RdapSearchActionBase.java index 4b8d46547..aa5d2e4d3 100644 --- a/java/google/registry/rdap/RdapSearchActionBase.java +++ b/java/google/registry/rdap/RdapSearchActionBase.java @@ -18,12 +18,12 @@ import static java.nio.charset.StandardCharsets.UTF_8; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableListMultimap; -import com.google.common.collect.ImmutableMap; import google.registry.rdap.RdapMetrics.EndpointType; import google.registry.request.Parameter; import google.registry.request.ParameterMap; import google.registry.request.RequestUrl; import java.io.UnsupportedEncodingException; +import java.net.URI; import java.net.URLEncoder; import java.util.Base64; import java.util.List; @@ -119,19 +119,8 @@ public abstract class RdapSearchActionBase extends RdapActionBase { } } - ImmutableList> getNotices(RdapSearchResults results) { - ImmutableList> notices = results.getIncompletenessWarnings(); - if (results.nextCursor().isPresent()) { - ImmutableList.Builder> noticesBuilder = - new ImmutableList.Builder<>(); - noticesBuilder.addAll(notices); - noticesBuilder.add( - RdapJsonFormatter.makeRdapJsonNavigationLinkNotice( - Optional.of( - getRequestUrlWithExtraParameter( - "cursor", encodeCursorToken(results.nextCursor().get()))))); - notices = noticesBuilder.build(); - } - return notices; + /** Creates the URL for this same search with a different starting point cursor. */ + URI createNavigationUri(String cursor) { + return URI.create(getRequestUrlWithExtraParameter("cursor", encodeCursorToken(cursor))); } } diff --git a/java/google/registry/rdap/RdapSearchResults.java b/java/google/registry/rdap/RdapSearchResults.java index b2c4cac05..5d55adb87 100644 --- a/java/google/registry/rdap/RdapSearchResults.java +++ b/java/google/registry/rdap/RdapSearchResults.java @@ -20,6 +20,14 @@ import static google.registry.rdap.RdapIcannStandardInformation.TRUNCATION_NOTIC import com.google.auto.value.AutoValue; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; +import google.registry.rdap.RdapDataStructures.Link; +import google.registry.rdap.RdapDataStructures.Notice; +import google.registry.rdap.RdapObjectClasses.BoilerplateType; +import google.registry.rdap.RdapObjectClasses.RdapDomain; +import google.registry.rdap.RdapObjectClasses.RdapEntity; +import google.registry.rdap.RdapObjectClasses.RdapNameserver; +import google.registry.rdap.RdapObjectClasses.ReplyPayloadBase; +import java.net.URI; import java.util.Optional; /** @@ -31,6 +39,130 @@ import java.util.Optional; @AutoValue abstract class RdapSearchResults { + /** + * Responding To Searches defined in 8 of RFC7483. + */ + abstract static class BaseSearchResponse extends ReplyPayloadBase { + abstract IncompletenessWarningType incompletenessWarningType(); + abstract ImmutableMap navigationLinks(); + + @JsonableElement("notices") ImmutableList getIncompletenessWarnings() { + switch (incompletenessWarningType()) { + case TRUNCATED: + return TRUNCATION_NOTICES; + case MIGHT_BE_INCOMPLETE: + return POSSIBLY_INCOMPLETE_NOTICES; + case COMPLETE: + break; + } + return ImmutableList.of(); + } + + /** + * Creates a JSON object containing a notice with page navigation links. + * + *

At the moment, only a next page link is supported. Other types of links (e.g. previous + * page) could be added in the future, but it's not clear how to generate such links, given the + * way we are querying the database. + * + *

This isn't part of the spec. + */ + @JsonableElement("notices[]") + Optional getNavigationNotice() { + if (navigationLinks().isEmpty()) { + return Optional.empty(); + } + Notice.Builder builder = + Notice.builder().setTitle("Navigation Links").setDescription("Links to related pages."); + navigationLinks().forEach((name, uri) -> + builder.linksBuilder() + .add(Link.builder() + .setRel(name) + .setHref(uri.toString()) + .setType("application/rdap+json") + .build())); + return Optional.of(builder.build()); + } + + BaseSearchResponse(BoilerplateType boilerplateType) { + super(boilerplateType); + } + + abstract static class Builder> { + abstract ImmutableMap.Builder navigationLinksBuilder(); + abstract B setIncompletenessWarningType(IncompletenessWarningType type); + + @SuppressWarnings("unchecked") + B setNextPageUri(URI uri) { + navigationLinksBuilder().put("next", uri); + return (B) this; + } + } + } + + @AutoValue + abstract static class DomainSearchResponse extends BaseSearchResponse { + + @JsonableElement abstract ImmutableList domainSearchResults(); + + DomainSearchResponse() { + super(BoilerplateType.DOMAIN); + } + + static Builder builder() { + return new AutoValue_RdapSearchResults_DomainSearchResponse.Builder(); + } + + @AutoValue.Builder + abstract static class Builder extends BaseSearchResponse.Builder { + abstract ImmutableList.Builder domainSearchResultsBuilder(); + + abstract DomainSearchResponse build(); + } + } + + @AutoValue + abstract static class EntitySearchResponse extends BaseSearchResponse { + + @JsonableElement public abstract ImmutableList entitySearchResults(); + + EntitySearchResponse() { + super(BoilerplateType.ENTITY); + } + + static Builder builder() { + return new AutoValue_RdapSearchResults_EntitySearchResponse.Builder(); + } + + @AutoValue.Builder + abstract static class Builder extends BaseSearchResponse.Builder { + abstract ImmutableList.Builder entitySearchResultsBuilder(); + + abstract EntitySearchResponse build(); + } + } + + @AutoValue + abstract static class NameserverSearchResponse extends BaseSearchResponse { + + @JsonableElement public abstract ImmutableList nameserverSearchResults(); + + NameserverSearchResponse() { + super(BoilerplateType.NAMESERVER); + } + + static Builder builder() { + return new AutoValue_RdapSearchResults_NameserverSearchResponse.Builder(); + } + + @AutoValue.Builder + abstract static class Builder extends BaseSearchResponse.Builder { + abstract ImmutableList.Builder nameserverSearchResultsBuilder(); + + abstract NameserverSearchResponse build(); + } + } + enum IncompletenessWarningType { /** Result set is complete. */ @@ -45,35 +177,4 @@ abstract class RdapSearchResults { */ MIGHT_BE_INCOMPLETE } - - static RdapSearchResults create(ImmutableList> jsonList) { - return create(jsonList, IncompletenessWarningType.COMPLETE, Optional.empty()); - } - - static RdapSearchResults create( - ImmutableList> jsonList, - IncompletenessWarningType incompletenessWarningType, - Optional nextCursor) { - return new AutoValue_RdapSearchResults(jsonList, incompletenessWarningType, nextCursor); - } - - /** List of JSON result object representations. */ - abstract ImmutableList> jsonList(); - - /** Type of warning to display regarding possible incomplete data. */ - abstract IncompletenessWarningType incompletenessWarningType(); - - /** Cursor for fetching the next page of results, or empty() if there are no more. */ - abstract Optional nextCursor(); - - /** Convenience method to get the appropriate warnings for the incompleteness warning type. */ - ImmutableList> getIncompletenessWarnings() { - if (incompletenessWarningType() == IncompletenessWarningType.TRUNCATED) { - return TRUNCATION_NOTICES; - } - if (incompletenessWarningType() == IncompletenessWarningType.MIGHT_BE_INCOMPLETE) { - return POSSIBLY_INCOMPLETE_NOTICES; - } - return ImmutableList.of(); - } } diff --git a/java/google/registry/request/RequestModule.java b/java/google/registry/request/RequestModule.java index dcc632f7a..f2b380d41 100644 --- a/java/google/registry/request/RequestModule.java +++ b/java/google/registry/request/RequestModule.java @@ -96,6 +96,12 @@ public final class RequestModule { return req.getRequestURI(); } + /** + * Returns the part of this request's URL that calls the servlet. + * + *

This includes the path to the servlet, but does not include any extra path information or a + * query string. + */ @Provides @FullServletPath static String provideFullServletPath(HttpServletRequest req) { diff --git a/javatests/google/registry/rdap/RdapActionBaseTest.java b/javatests/google/registry/rdap/RdapActionBaseTest.java index 13efb4951..878124718 100644 --- a/javatests/google/registry/rdap/RdapActionBaseTest.java +++ b/javatests/google/registry/rdap/RdapActionBaseTest.java @@ -19,15 +19,13 @@ import static com.google.common.truth.Truth.assertThat; import static google.registry.request.Action.Method.GET; import static google.registry.request.Action.Method.HEAD; import static google.registry.testing.DatastoreHelper.createTld; -import static google.registry.testing.TestDataHelper.loadFile; import static org.mockito.Mockito.verify; -import com.google.common.collect.ImmutableList; -import com.google.common.collect.ImmutableMap; -import google.registry.rdap.RdapJsonFormatter.BoilerplateType; import google.registry.rdap.RdapMetrics.EndpointType; import google.registry.rdap.RdapMetrics.SearchType; import google.registry.rdap.RdapMetrics.WildcardType; +import google.registry.rdap.RdapObjectClasses.BoilerplateType; +import google.registry.rdap.RdapObjectClasses.ReplyPayloadBase; import google.registry.rdap.RdapSearchResults.IncompletenessWarningType; import google.registry.request.Action; import google.registry.request.auth.Auth; @@ -59,7 +57,7 @@ public class RdapActionBaseTest extends RdapActionBaseTestCase getJsonObjectForResource( + public ReplyPayloadBase getJsonObjectForResource( String pathSearchString, boolean isHeadRequest) { if (pathSearchString.equals("IllegalArgumentException")) { throw new IllegalArgumentException(); @@ -67,46 +65,36 @@ public class RdapActionBaseTest extends RdapActionBaseTestCase builder = new ImmutableMap.Builder<>(); - builder.put("key", "value"); - rdapJsonFormatter.addTopLevelEntries( - builder, - BoilerplateType.OTHER, - ImmutableList.of(), - ImmutableList.of(), - "http://myserver.example.com/"); - return builder.build(); + return new ReplyPayloadBase(BoilerplateType.OTHER) { + @JsonableElement public String key = "value"; + }; } } @Before public void setUp() { createTld("thing"); - action.fullServletPath = "http://myserver.example.com" + actionPath; + action.rdapJsonFormatter = RdapTestHelper.getTestRdapJsonFormatter(); } @Test public void testIllegalValue_showsReadableTypeName() { - assertThat(generateActualJson("IllegalArgumentException")).isEqualTo(JSONValue.parse( - "{\"lang\":\"en\", \"errorCode\":400, \"title\":\"Bad Request\"," - + "\"rdapConformance\":[\"icann_rdap_response_profile_0\"]," - + "\"description\":[\"Not a valid human-readable string\"]}")); + assertThat(generateActualJson("IllegalArgumentException")).isEqualTo(generateExpectedJsonError( + "Not a valid human-readable string", + 400)); assertThat(response.getStatus()).isEqualTo(400); } @Test public void testRuntimeException_returns500Error() { - assertThat(generateActualJson("RuntimeException")).isEqualTo(JSONValue.parse( - "{\"lang\":\"en\", \"errorCode\":500, \"title\":\"Internal Server Error\"," - + "\"rdapConformance\":[\"icann_rdap_response_profile_0\"]," - + "\"description\":[\"An error was encountered\"]}")); + assertThat(generateActualJson("RuntimeException")) + .isEqualTo(generateExpectedJsonError("An error was encountered", 500)); assertThat(response.getStatus()).isEqualTo(500); } @Test public void testValidName_works() { - assertThat(generateActualJson("no.thing")).isEqualTo(JSONValue.parse( - loadFile(this.getClass(), "rdapjson_toplevel.json"))); + assertThat(generateActualJson("no.thing")).isEqualTo(loadJsonFile("rdapjson_toplevel.json")); assertThat(response.getStatus()).isEqualTo(200); } @@ -173,18 +161,14 @@ public class RdapActionBaseTest extends RdapActionBaseTestCase { action.rdapJsonFormatter = RdapTestHelper.getTestRdapJsonFormatter(); action.rdapMetrics = rdapMetrics; action.requestMethod = Action.Method.GET; - action.fullServletPath = "https://example.tld/rdap"; action.rdapWhoisServer = null; logout(); } @@ -126,18 +130,80 @@ public class RdapActionBaseTestCase { metricRole = ADMINISTRATOR; } - protected Object generateActualJson(String domainName) { + protected JSONObject generateActualJson(String domainName) { action.requestPath = actionPath + domainName; action.requestMethod = GET; action.run(); - return JSONValue.parse(response.getPayload()); + return (JSONObject) JSONValue.parse(response.getPayload()); } protected String generateHeadPayload(String domainName) { action.requestPath = actionPath + domainName; - action.fullServletPath = "http://myserver.example.com" + actionPath; action.requestMethod = HEAD; action.run(); return response.getPayload(); } + + /** + * Loads a resource testdata JSON file, and applies substitutions. + * + *

{@code loadJsonFile("filename.json", "NANE", "something", "ID", "other")} is the same as + * {@code loadJsonFile("filename.json", ImmutableMap.of("NANE", "something", "ID", "other"))}. + * + * @param filename the name of the file from the testdata directory + * @param keysAndValues alternating substitution key and value. The substitutions are applied to + * the file before parsing it to JSON. + */ + protected JSONObject loadJsonFile(String filename, String... keysAndValues) { + checkArgument(keysAndValues.length % 2 == 0); + ImmutableMap.Builder builder = new ImmutableMap.Builder<>(); + for (int i = 0; i < keysAndValues.length; i += 2) { + if (keysAndValues[i + 1] != null) { + builder.put(keysAndValues[i], keysAndValues[i + 1]); + } + } + return loadJsonFile(filename, builder.build()); + } + + /** + * Loads a resource testdata JSON file, and applies substitutions. + * + * @param filename the name of the file from the testdata directory + * @param substitutions map of substitutions to apply to the file. The substitutions are applied + * to the file before parsing it to JSON. + */ + protected JSONObject loadJsonFile(String filename, Map substitutions) { + return (JSONObject) JSONValue.parse(loadFile(this.getClass(), filename, substitutions)); + } + + protected JSONObject generateExpectedJsonError( + String description, + int code) { + String title; + switch (code) { + case 404: + title = "Not Found"; + break; + case 500: + title = "Internal Server Error"; + break; + case 501: + title = "Not Implemented"; + break; + case 400: + title = "Bad Request"; + break; + case 422: + title = "Unprocessable Entity"; + break; + default: + title = "ERR"; + break; + } + return loadJsonFile( + "rdap_error.json", + "DESCRIPTION", description, + "TITLE", title, + "CODE", String.valueOf(code)); + } } diff --git a/javatests/google/registry/rdap/RdapDataStructuresTest.java b/javatests/google/registry/rdap/RdapDataStructuresTest.java new file mode 100644 index 000000000..4bc8ffff5 --- /dev/null +++ b/javatests/google/registry/rdap/RdapDataStructuresTest.java @@ -0,0 +1,177 @@ +// Copyright 2019 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 com.google.common.truth.Truth.assertThat; +import static google.registry.rdap.RdapTestHelper.createJson; + +import com.google.common.collect.ImmutableSet; +import google.registry.rdap.RdapDataStructures.Event; +import google.registry.rdap.RdapDataStructures.EventAction; +import google.registry.rdap.RdapDataStructures.EventWithoutActor; +import google.registry.rdap.RdapDataStructures.LanguageIdentifier; +import google.registry.rdap.RdapDataStructures.Link; +import google.registry.rdap.RdapDataStructures.Notice; +import google.registry.rdap.RdapDataStructures.ObjectClassName; +import google.registry.rdap.RdapDataStructures.Port43WhoisServer; +import google.registry.rdap.RdapDataStructures.PublicId; +import google.registry.rdap.RdapDataStructures.RdapConformance; +import google.registry.rdap.RdapDataStructures.RdapStatus; +import google.registry.rdap.RdapDataStructures.Remark; +import org.joda.time.DateTime; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public final class RdapDataStructuresTest { + + private void assertRestrictedNames(Object object, String... names) { + assertThat(AbstractJsonableObject.getNameRestriction(object.getClass()).get()) + .containsExactlyElementsIn(ImmutableSet.copyOf(names)); + } + + @Test + public void testRdapConformance() { + assertThat(RdapConformance.INSTANCE.toJson()) + .isEqualTo(createJson("['icann_rdap_response_profile_0']")); + } + + @Test + public void testLink() { + Link link = + Link.builder() + .setHref("myHref") + .setRel("myRel") + .setTitle("myTitle") + .setType("myType") + .build(); + assertThat(link.toJson()) + .isEqualTo(createJson("{'href':'myHref','rel':'myRel','title':'myTitle','type':'myType'}")); + assertRestrictedNames(link, "links[]"); + } + + @Test + public void testNotice() { + Notice notice = Notice.builder() + .setDescription("AAA", "BBB") + .setTitle("myTitle") + .addLink(Link.builder().setHref("myHref").setTitle("myLink").build()) + .setType(Notice.Type.RESULT_TRUNCATED_AUTHORIZATION) + .build(); + assertThat(notice.toJson()) + .isEqualTo( + createJson( + "{", + " 'title':'myTitle',", + " 'type':'result set truncated due to authorization',", + " 'description':['AAA','BBB'],", + " 'links':[{'href':'myHref','title':'myLink'}]", + "}")); + assertRestrictedNames(notice, "notices[]"); + } + + @Test + public void testRemark() { + Remark remark = Remark.builder() + .setDescription("AAA", "BBB") + .setTitle("myTitle") + .addLink(Link.builder().setHref("myHref").setTitle("myLink").build()) + .setType(Remark.Type.OBJECT_TRUNCATED_AUTHORIZATION) + .build(); + assertThat(remark.toJson()) + .isEqualTo( + createJson( + "{", + " 'title':'myTitle',", + " 'type':'object truncated due to authorization',", + " 'description':['AAA','BBB'],", + " 'links':[{'href':'myHref','title':'myLink'}]", + "}")); + assertRestrictedNames(remark, "remarks[]"); + } + + @Test + public void testLanguage() { + assertThat(LanguageIdentifier.EN.toJson()).isEqualTo(createJson("'en'")); + assertRestrictedNames(LanguageIdentifier.EN, "lang"); + } + + @Test + public void testEvent() { + Event event = + Event.builder() + .setEventAction(EventAction.REGISTRATION) + .setEventActor("Event Actor") + .setEventDate(DateTime.parse("2012-04-03T14:54Z")) + .addLink(Link.builder().setHref("myHref").build()) + .build(); + assertThat(event.toJson()) + .isEqualTo( + createJson( + "{", + " 'eventAction':'registration',", + " 'eventActor':'Event Actor',", + " 'eventDate':'2012-04-03T14:54:00.000Z',", + " 'links':[{'href':'myHref'}]", + "}")); + assertRestrictedNames(event, "events[]"); + } + + @Test + public void testEventWithoutActor() { + EventWithoutActor event = + EventWithoutActor.builder() + .setEventAction(EventAction.REGISTRATION) + .setEventDate(DateTime.parse("2012-04-03T14:54Z")) + .addLink(Link.builder().setHref("myHref").build()) + .build(); + assertThat(event.toJson()) + .isEqualTo( + createJson( + "{", + " 'eventAction':'registration',", + " 'eventDate':'2012-04-03T14:54:00.000Z',", + " 'links':[{'href':'myHref'}]", + "}")); + assertRestrictedNames(event, "asEventActor[]"); + } + + @Test + public void testRdapStatus() { + assertThat(RdapStatus.ACTIVE.toJson()).isEqualTo(createJson("'active'")); + assertRestrictedNames(RdapStatus.ACTIVE, "status[]"); + } + + @Test + public void testPort43() { + Port43WhoisServer port43 = Port43WhoisServer.create("myServer"); + assertThat(port43.toJson()).isEqualTo(createJson("'myServer'")); + assertRestrictedNames(port43, "port43"); + } + + @Test + public void testPublicId() { + PublicId publicId = PublicId.create(PublicId.Type.IANA_REGISTRAR_ID, "myId"); + assertThat(publicId.toJson()) + .isEqualTo(createJson("{'identifier':'myId','type':'IANA Registrar ID'}")); + assertRestrictedNames(publicId, "publicIds[]"); + } + + @Test + public void testObjectClassName() { + assertThat(ObjectClassName.DOMAIN.toJson()).isEqualTo(createJson("'domain'")); + assertRestrictedNames(ObjectClassName.DOMAIN, "objectClassName"); + } +} diff --git a/javatests/google/registry/rdap/RdapDomainActionTest.java b/javatests/google/registry/rdap/RdapDomainActionTest.java index 90ee442e7..d2e98d0d9 100644 --- a/javatests/google/registry/rdap/RdapDomainActionTest.java +++ b/javatests/google/registry/rdap/RdapDomainActionTest.java @@ -24,7 +24,6 @@ import static google.registry.testing.FullFieldsTestEntityHelper.makeDomainBase; import static google.registry.testing.FullFieldsTestEntityHelper.makeHistoryEntry; import static google.registry.testing.FullFieldsTestEntityHelper.makeRegistrar; import static google.registry.testing.FullFieldsTestEntityHelper.makeRegistrarContacts; -import static google.registry.testing.TestDataHelper.loadFile; import static org.mockito.Mockito.verify; import com.google.common.collect.ImmutableList; @@ -47,7 +46,6 @@ import java.util.Map; import java.util.Optional; import javax.annotation.Nullable; import org.json.simple.JSONObject; -import org.json.simple.JSONValue; import org.junit.Before; import org.junit.Ignore; import org.junit.Test; @@ -230,24 +228,15 @@ public class RdapDomainActionTest extends RdapActionBaseTestCase contactRoids, @Nullable List nameserverRoids, @Nullable List nameserverNames, - @Nullable String registrarName, - String expectedOutputFile) { + @Nullable String registrarName) { ImmutableMap.Builder substitutionsBuilder = new ImmutableMap.Builder<>(); substitutionsBuilder.put("NAME", name); substitutionsBuilder.put("PUNYCODENAME", (punycodeName == null) ? name : punycodeName); @@ -282,11 +271,10 @@ public class RdapDomainActionTest extends RdapActionBaseTestCase nameserverNames, @Nullable String registrarName, String expectedOutputFile) { - Object obj = + JSONObject obj = generateExpectedJson( + expectedOutputFile, name, punycodeName, handle, contactRoids, nameserverRoids, nameserverNames, - registrarName, - expectedOutputFile); - if (obj instanceof Map) { + registrarName); @SuppressWarnings("unchecked") Map map = (Map) obj; ImmutableMap.Builder builder = - RdapTestHelper.getBuilderExcluding( - map, ImmutableSet.of("rdapConformance", "notices", "remarks")); - builder.put("rdapConformance", ImmutableList.of("icann_rdap_response_profile_0")); + RdapTestHelper.getBuilderExcluding(map, ImmutableSet.of("notices")); RdapTestHelper.addDomainBoilerplateNotices( - builder, - false, - RdapTestHelper.createNotices( - "https://example.tld/rdap/", - (contactRoids == null) - ? RdapTestHelper.ContactNoticeType.DOMAIN - : RdapTestHelper.ContactNoticeType.NONE, - map.get("notices"))); + builder, RdapTestHelper.createNotices("https://example.tld/rdap/", map.get("notices"))); obj = new JSONObject(builder.build()); - } return obj; } @@ -361,9 +338,7 @@ public class RdapDomainActionTest extends RdapActionBaseTestCase", expectedOutputFile)); @@ -374,12 +349,10 @@ public class RdapDomainActionTest extends RdapActionBaseTestCase() - .put("TYPE", "domain name") - .put("DOMAINPUNYCODENAME1", domain1Name) - .put("DOMAINNAME1", IDN.toUnicode(domain1Name)) - .put("DOMAINHANDLE1", domain1Handle) - .put("DOMAINPUNYCODENAME2", domain2Name) - .put("DOMAINNAME2", IDN.toUnicode(domain2Name)) - .put("DOMAINHANDLE2", domain2Handle) - .put("DOMAINPUNYCODENAME3", domain3Name) - .put("DOMAINNAME3", IDN.toUnicode(domain3Name)) - .put("DOMAINHANDLE3", domain3Handle) - .put("DOMAINPUNYCODENAME4", domain4Name) - .put("DOMAINNAME4", IDN.toUnicode(domain4Name)) - .put("DOMAINHANDLE4", domain4Handle) - .put("NEXT_QUERY", nextQuery) - .build())); + return loadJsonFile( + expectedOutputFile, + "TYPE", "domain name", + "DOMAINPUNYCODENAME1", domain1Name, + "DOMAINNAME1", IDN.toUnicode(domain1Name), + "DOMAINHANDLE1", domain1Handle, + "DOMAINPUNYCODENAME2", domain2Name, + "DOMAINNAME2", IDN.toUnicode(domain2Name), + "DOMAINHANDLE2", domain2Handle, + "DOMAINPUNYCODENAME3", domain3Name, + "DOMAINNAME3", IDN.toUnicode(domain3Name), + "DOMAINHANDLE3", domain3Handle, + "DOMAINPUNYCODENAME4", domain4Name, + "DOMAINNAME4", IDN.toUnicode(domain4Name), + "DOMAINHANDLE4", domain4Handle, + "NEXT_QUERY", nextQuery); } - private Object generateExpectedJson(String name, String expectedOutputFile) { - return generateExpectedJson(name, null, null, null, null, null, expectedOutputFile); - } - - private Object generateExpectedJson( + private JSONObject generateExpectedJson( String name, String punycodeName, String handle, @@ -476,11 +465,10 @@ public class RdapDomainSearchActionTest extends RdapSearchActionTestCase nameservers, @Nullable String registrarName, String expectedOutputFile) { - Object obj = + JSONObject obj = generateExpectedJson( name, punycodeName, @@ -497,6 +485,7 @@ public class RdapDomainSearchActionTest extends RdapSearchActionTestCase builder = new ImmutableMap.Builder<>(); builder.put("domainSearchResults", ImmutableList.of(obj)); builder.put("rdapConformance", ImmutableList.of("icann_rdap_response_profile_0")); @@ -560,7 +549,7 @@ public class RdapDomainSearchActionTest extends RdapSearchActionTestCase() - .put("DOMAINNAME1", domainName1) - .put("DOMAINHANDLE1", domainHandle1) - .put("DOMAINNAME2", domainName2) - .put("DOMAINHANDLE2", domainHandle2) - .put("DOMAINNAME3", domainName3) - .put("DOMAINHANDLE3", domainHandle3) - .put("DOMAINNAME4", domainName4) - .put("DOMAINHANDLE4", domainHandle4) - .put("NEXT_QUERY", nextQuery) - .build())); + "DOMAINNAME1", domainName1, + "DOMAINHANDLE1", domainHandle1, + "DOMAINNAME2", domainName2, + "DOMAINHANDLE2", domainHandle2, + "DOMAINNAME3", domainName3, + "DOMAINHANDLE3", domainHandle3, + "DOMAINNAME4", domainName4, + "DOMAINHANDLE4", domainHandle4, + "NEXT_QUERY", nextQuery); } - private void checkNumberOfDomainsInResult(Object obj, int expected) { + private void checkNumberOfDomainsInResult(JSONObject obj, int expected) { assertThat(obj).isInstanceOf(Map.class); @SuppressWarnings("unchecked") - Map map = (Map) obj; - - @SuppressWarnings("unchecked") - List domains = (List) map.get("domainSearchResults"); + List domains = (List) obj.get("domainSearchResults"); assertThat(domains).hasSize(expected); } @@ -717,7 +700,7 @@ public class RdapDomainSearchActionTest extends RdapSearchActionTestCase() - .put("NAME", handle) - .put("FULLNAME", fullName) - .put("ADDRESS", (address == null) ? "\"1 Smiley Row\", \"Suite みんな\"" : address) - .put("EMAIL", "lol@cat.みんな") - .put("TYPE", "entity") - .put("STATUS", status) - .build())); + return loadJsonFile( + expectedOutputFile, + "NAME", handle, + "FULLNAME", fullName, + "ADDRESS", (address == null) ? "\"1 Smiley Row\", \"Suite みんな\"" : address, + "EMAIL", "lol@cat.みんな", + "TYPE", "entity", + "STATUS", status); } private Object generateExpectedJsonWithTopLevelEntries( String handle, String expectedOutputFile) { return generateExpectedJsonWithTopLevelEntries( - handle, "(◕‿◕)", "active", null, false, expectedOutputFile); + handle, "(◕‿◕)", "active", null, expectedOutputFile); } private Object generateExpectedJsonWithTopLevelEntries( @@ -184,35 +159,26 @@ public class RdapEntityActionTest extends RdapActionBaseTestCase map = (Map) obj; ImmutableMap.Builder builder = - RdapTestHelper.getBuilderExcluding( - map, ImmutableSet.of("rdapConformance", "notices", "remarks")); - builder.put("rdapConformance", ImmutableList.of("icann_rdap_response_profile_0")); + RdapTestHelper.getBuilderExcluding(map, ImmutableSet.of("notices")); RdapTestHelper.addNonDomainBoilerplateNotices( - builder, - RdapTestHelper.createNotices( - "https://example.tld/rdap/", - addNoPersonalDataRemark - ? RdapTestHelper.ContactNoticeType.CONTACT - : RdapTestHelper.ContactNoticeType.NONE, - map.get("notices"))); - obj = builder.build(); + builder, RdapTestHelper.createNotices("https://example.tld/rdap/", map.get("notices"))); + obj = new JSONObject(builder.build()); } return obj; } private void runSuccessfulTest(String queryString, String fileName) { - runSuccessfulTest(queryString, "(◕‿◕)", "active", null, false, fileName); + runSuccessfulTest(queryString, "(◕‿◕)", "active", null, fileName); } private void runSuccessfulTest(String queryString, String fullName, String fileName) { - runSuccessfulTest(queryString, fullName, "active", null, false, fileName); + runSuccessfulTest(queryString, fullName, "active", null, fileName); } private void runSuccessfulTest( @@ -220,27 +186,26 @@ public class RdapEntityActionTest extends RdapActionBaseTestCase", "inactive", null, false, "rdap_registrar.json"); + "104", "Yes Virginia