diff --git a/java/google/registry/model/registrar/Registrar.java b/java/google/registry/model/registrar/Registrar.java index f45e79b18..56d416856 100644 --- a/java/google/registry/model/registrar/Registrar.java +++ b/java/google/registry/model/registrar/Registrar.java @@ -242,6 +242,9 @@ public class Registrar extends ImmutableObject implements Buildable, Jsonifiable /** Host name of WHOIS server. */ String whoisServer; + /** Base URLs for the registrar's RDAP servers. */ + Set rdapBaseUrls; + /** * Whether registration of premium names should be blocked over EPP. If this is set to true, then * the only way to register premium names is with the superuser flag. @@ -526,6 +529,10 @@ public class Registrar extends ImmutableObject implements Buildable, Jsonifiable return firstNonNull(whoisServer, getDefaultRegistrarWhoisServer()); } + public ImmutableSet getRdapBaseUrls() { + return nullToEmptyImmutableSortedCopy(rdapBaseUrls); + } + public boolean getBlockPremiumNames() { return blockPremiumNames; } @@ -603,6 +610,7 @@ public class Registrar extends ImmutableObject implements Buildable, Jsonifiable .put("faxNumber", faxNumber) .put("emailAddress", emailAddress) .put("whoisServer", getWhoisServer()) + .putListOfStrings("rdapBaseUrls", getRdapBaseUrls()) .put("blockPremiumNames", blockPremiumNames) .put("url", url) .put("icannReferralEmail", getIcannReferralEmail()) @@ -837,6 +845,11 @@ public class Registrar extends ImmutableObject implements Buildable, Jsonifiable return this; } + public Builder setRdapBaseUrls(Set rdapBaseUrls) { + getInstance().rdapBaseUrls = ImmutableSet.copyOf(rdapBaseUrls); + return this; + } + public Builder setBlockPremiumNames(boolean blockPremiumNames) { getInstance().blockPremiumNames = blockPremiumNames; return this; @@ -908,9 +921,24 @@ public class Registrar extends ImmutableObject implements Buildable, Jsonifiable ofy().load().type(Registrar.class).parent(getCrossTldKey()).id(clientId).now()); } - /** Loads and returns a registrar entity by its client id using an in-memory cache. */ + /** + * Loads and returns a registrar entity by its client id using an in-memory cache. + * + *

Returns empty if the registrar isn't found. + */ public static Optional loadByClientIdCached(String clientId) { checkArgument(!Strings.isNullOrEmpty(clientId), "clientId must be specified"); return Optional.ofNullable(CACHE_BY_CLIENT_ID.get().get(clientId)); } + + /** + * Loads and returns a registrar entity by its client id using an in-memory cache. + * + *

Throws if the registrar isn't found. + */ + public static Registrar loadRequiredRegistrarCached(String clientId) { + Optional registrar = loadByClientIdCached(clientId); + checkArgument(registrar.isPresent(), "couldn't find registrar '%s'", clientId); + return registrar.get(); + } } diff --git a/java/google/registry/rdap/RdapJsonFormatter.java b/java/google/registry/rdap/RdapJsonFormatter.java index 8a8ea473c..9ff76e6d6 100644 --- a/java/google/registry/rdap/RdapJsonFormatter.java +++ b/java/google/registry/rdap/RdapJsonFormatter.java @@ -66,7 +66,6 @@ import google.registry.rdap.RdapObjectClasses.SecureDns; 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.util.Clock; import java.net.Inet4Address; import java.net.Inet6Address; @@ -301,11 +300,25 @@ public class RdapJsonFormatter { // The domain object in the RDAP response MUST contain an entity with the Registrar role. // // See {@link createRdapRegistrarEntity} for details of section 2.4 conformance - builder - .entitiesBuilder() - .add( - createRdapRegistrarEntity( - domainBase.getCurrentSponsorClientId(), OutputDataType.INTERNAL)); + Registrar registrar = + Registrar.loadRequiredRegistrarCached(domainBase.getCurrentSponsorClientId()); + builder.entitiesBuilder().add(createRdapRegistrarEntity(registrar, OutputDataType.INTERNAL)); + // RDAP Technical Implementation Guide 3.2: must have link to the registrar's RDAP URL for this + // domain, with rel=related. + for (String registrarRdapBase : registrar.getRdapBaseUrls()) { + String href = + makeServerRelativeUrl( + registrarRdapBase, "domain", domainBase.getFullyQualifiedDomainName()); + builder + .linksBuilder() + .add( + Link.builder() + .setHref(href) + .setValue(href) + .setRel("related") + .setType("application/rdap+json") + .build()); + } // RDAP Response Profile 2.6.1: must have at least one status member // makeStatusValueList should in theory always contain one of either "active" or "inactive". ImmutableSet status = @@ -443,11 +456,9 @@ public class RdapJsonFormatter { // RDAP Response Profile 4.3 - Registrar member is optional, so we only set it for FULL if (outputDataType == OutputDataType.FULL) { - builder - .entitiesBuilder() - .add( - createRdapRegistrarEntity( - hostResource.getPersistedCurrentSponsorClientId(), OutputDataType.INTERNAL)); + Registrar registrar = + Registrar.loadRequiredRegistrarCached(hostResource.getPersistedCurrentSponsorClientId()); + builder.entitiesBuilder().add(createRdapRegistrarEntity(registrar, OutputDataType.INTERNAL)); } if (outputDataType != OutputDataType.INTERNAL) { // Rdap Response Profile 4.4, must have "last update of RDAP database" response. But this is @@ -747,21 +758,6 @@ public class RdapJsonFormatter { return builder.build(); } - /** - * Creates a JSON object for the desired registrar to an existing list of JSON objects. - * - * @param clientId the registrar client ID - * @param outputDataType whether to generate FULL, SUMMARY, or INTERNAL data. - */ - RdapRegistrarEntity createRdapRegistrarEntity(String clientId, OutputDataType outputDataType) { - Optional registrar = Registrar.loadByClientIdCached(clientId); - if (!registrar.isPresent()) { - throw new InternalServerErrorException( - String.format("Couldn't find registrar '%s'", clientId)); - } - return createRdapRegistrarEntity(registrar.get(), outputDataType); - } - /** * Creates a JSON object for a {@link RegistrarContact}. * @@ -1059,11 +1055,18 @@ public class RdapJsonFormatter { * Create a link relative to the RDAP server endpoint. */ String makeRdapServletRelativeUrl(String part, String... moreParts) { + return makeServerRelativeUrl(fullServletPath, part, moreParts); + } + + /** + * Create a link relative to some base server + */ + static String makeServerRelativeUrl(String baseServer, String part, String... moreParts) { String relativePath = Paths.get(part, moreParts).toString(); - if (fullServletPath.endsWith("/")) { - return fullServletPath + relativePath; + if (baseServer.endsWith("/")) { + return baseServer + relativePath; } - return fullServletPath + "/" + relativePath; + return baseServer + "/" + relativePath; } /** diff --git a/java/google/registry/tools/CreateOrUpdateRegistrarCommand.java b/java/google/registry/tools/CreateOrUpdateRegistrarCommand.java index 0c8903fd8..7082e3d38 100644 --- a/java/google/registry/tools/CreateOrUpdateRegistrarCommand.java +++ b/java/google/registry/tools/CreateOrUpdateRegistrarCommand.java @@ -254,6 +254,11 @@ abstract class CreateOrUpdateRegistrarCommand extends MutatingCommand { description = "Hostname of registrar WHOIS server. (Default: whois.nic.google)") String whoisServer; + @Parameter( + names = "--rdap_servers", + description = "Comma-delimited list of RDAP servers. An empty argument clears the list") + List rdapServers = new ArrayList<>(); + /** Returns the existing registrar (for update) or null (for creates). */ @Nullable abstract Registrar getOldRegistrar(String clientId); @@ -389,6 +394,15 @@ abstract class CreateOrUpdateRegistrarCommand extends MutatingCommand { Optional.ofNullable(icannReferralEmail).ifPresent(builder::setIcannReferralEmail); Optional.ofNullable(whoisServer).ifPresent(builder::setWhoisServer); + if (!rdapServers.isEmpty()) { + // If we only have empty strings, then remove all the RDAP servers + // This is to differentiate between "I didn't set the rdapServers because I don't want to + // change them" and "I set the RDAP servers to an empty string because I want no RDAP + // servers". + builder.setRdapBaseUrls( + rdapServers.stream().filter(server -> !server.isEmpty()).collect(toImmutableSet())); + } + // If the registrarName is being set, verify that it is either null or it normalizes uniquely. String oldRegistrarName = (oldRegistrar == null) ? null : oldRegistrar.getRegistrarName(); if (registrarName != null && !registrarName.equals(oldRegistrarName)) { diff --git a/javatests/google/registry/model/testdata/schema.txt b/javatests/google/registry/model/testdata/schema.txt index eaf0b7e1a..a7ef1deb8 100644 --- a/javatests/google/registry/model/testdata/schema.txt +++ b/javatests/google/registry/model/testdata/schema.txt @@ -436,6 +436,7 @@ class google.registry.model.registrar.Registrar { java.util.List ipAddressWhitelist; java.util.Map billingAccountMap; java.util.Set allowedTlds; + java.util.Set rdapBaseUrls; org.joda.time.DateTime lastCertificateUpdateTime; } class google.registry.model.registrar.Registrar$BillingAccountEntry { diff --git a/javatests/google/registry/rdap/testdata/rdap_domain.json b/javatests/google/registry/rdap/testdata/rdap_domain.json index 5d4b38e22..be3b79760 100644 --- a/javatests/google/registry/rdap/testdata/rdap_domain.json +++ b/javatests/google/registry/rdap/testdata/rdap_domain.json @@ -19,6 +19,18 @@ "type": "application/rdap+json", "rel": "self", "value": "https://example.tld/rdap/domain/%DOMAIN_PUNYCODE_NAME_1%" + }, + { + "href": "https://rdap.example.com/withSlash/domain/%DOMAIN_PUNYCODE_NAME_1%", + "type": "application/rdap+json", + "rel": "related", + "value": "https://rdap.example.com/withSlash/domain/%DOMAIN_PUNYCODE_NAME_1%" + }, + { + "href": "https://rdap.example.com/withoutSlash/domain/%DOMAIN_PUNYCODE_NAME_1%", + "type": "application/rdap+json", + "rel": "related", + "value": "https://rdap.example.com/withoutSlash/domain/%DOMAIN_PUNYCODE_NAME_1%" } ], "events": [ diff --git a/javatests/google/registry/rdap/testdata/rdap_domain_cat2.json b/javatests/google/registry/rdap/testdata/rdap_domain_cat2.json index 1b69bd2d2..304916368 100644 --- a/javatests/google/registry/rdap/testdata/rdap_domain_cat2.json +++ b/javatests/google/registry/rdap/testdata/rdap_domain_cat2.json @@ -19,6 +19,18 @@ "type": "application/rdap+json", "rel": "self", "value": "https://example.tld/rdap/domain/%DOMAIN_PUNYCODE_NAME_1%" + }, + { + "href": "https://rdap.example.com/withSlash/domain/%DOMAIN_PUNYCODE_NAME_1%", + "type": "application/rdap+json", + "rel": "related", + "value": "https://rdap.example.com/withSlash/domain/%DOMAIN_PUNYCODE_NAME_1%" + }, + { + "href": "https://rdap.example.com/withoutSlash/domain/%DOMAIN_PUNYCODE_NAME_1%", + "type": "application/rdap+json", + "rel": "related", + "value": "https://rdap.example.com/withoutSlash/domain/%DOMAIN_PUNYCODE_NAME_1%" } ], "events": [ diff --git a/javatests/google/registry/rdap/testdata/rdap_domain_deleted.json b/javatests/google/registry/rdap/testdata/rdap_domain_deleted.json index 6f4dc8d1e..c76f202b1 100644 --- a/javatests/google/registry/rdap/testdata/rdap_domain_deleted.json +++ b/javatests/google/registry/rdap/testdata/rdap_domain_deleted.json @@ -20,6 +20,18 @@ "type": "application/rdap+json", "rel": "self", "value": "https://example.tld/rdap/domain/%DOMAIN_PUNYCODE_NAME_1%" + }, + { + "href": "https://rdap.example.com/withSlash/domain/%DOMAIN_PUNYCODE_NAME_1%", + "type": "application/rdap+json", + "rel": "related", + "value": "https://rdap.example.com/withSlash/domain/%DOMAIN_PUNYCODE_NAME_1%" + }, + { + "href": "https://rdap.example.com/withoutSlash/domain/%DOMAIN_PUNYCODE_NAME_1%", + "type": "application/rdap+json", + "rel": "related", + "value": "https://rdap.example.com/withoutSlash/domain/%DOMAIN_PUNYCODE_NAME_1%" } ], "events": [ diff --git a/javatests/google/registry/rdap/testdata/rdap_domain_no_contacts_with_remark.json b/javatests/google/registry/rdap/testdata/rdap_domain_no_contacts_with_remark.json index b0b0c03cb..c35367e99 100644 --- a/javatests/google/registry/rdap/testdata/rdap_domain_no_contacts_with_remark.json +++ b/javatests/google/registry/rdap/testdata/rdap_domain_no_contacts_with_remark.json @@ -19,6 +19,18 @@ "type": "application/rdap+json", "rel": "self", "value": "https://example.tld/rdap/domain/%DOMAIN_PUNYCODE_NAME_1%" + }, + { + "href": "https://rdap.example.com/withSlash/domain/%DOMAIN_PUNYCODE_NAME_1%", + "type": "application/rdap+json", + "rel": "related", + "value": "https://rdap.example.com/withSlash/domain/%DOMAIN_PUNYCODE_NAME_1%" + }, + { + "href": "https://rdap.example.com/withoutSlash/domain/%DOMAIN_PUNYCODE_NAME_1%", + "type": "application/rdap+json", + "rel": "related", + "value": "https://rdap.example.com/withoutSlash/domain/%DOMAIN_PUNYCODE_NAME_1%" } ], "events": [ diff --git a/javatests/google/registry/rdap/testdata/rdap_domain_unicode.json b/javatests/google/registry/rdap/testdata/rdap_domain_unicode.json index 4f628919a..11b134fd4 100644 --- a/javatests/google/registry/rdap/testdata/rdap_domain_unicode.json +++ b/javatests/google/registry/rdap/testdata/rdap_domain_unicode.json @@ -20,6 +20,18 @@ "type": "application/rdap+json", "rel": "self", "value": "https://example.tld/rdap/domain/%DOMAIN_PUNYCODE_NAME_1%" + }, + { + "href": "https://rdap.example.com/withSlash/domain/%DOMAIN_PUNYCODE_NAME_1%", + "type": "application/rdap+json", + "rel": "related", + "value": "https://rdap.example.com/withSlash/domain/%DOMAIN_PUNYCODE_NAME_1%" + }, + { + "href": "https://rdap.example.com/withoutSlash/domain/%DOMAIN_PUNYCODE_NAME_1%", + "type": "application/rdap+json", + "rel": "related", + "value": "https://rdap.example.com/withoutSlash/domain/%DOMAIN_PUNYCODE_NAME_1%" } ], "events": [ diff --git a/javatests/google/registry/rdap/testdata/rdap_domain_unicode_no_contacts_with_remark.json b/javatests/google/registry/rdap/testdata/rdap_domain_unicode_no_contacts_with_remark.json index 8c80fd479..7ab2040a3 100644 --- a/javatests/google/registry/rdap/testdata/rdap_domain_unicode_no_contacts_with_remark.json +++ b/javatests/google/registry/rdap/testdata/rdap_domain_unicode_no_contacts_with_remark.json @@ -20,6 +20,18 @@ "type": "application/rdap+json", "rel": "self", "value": "https://example.tld/rdap/domain/%DOMAIN_PUNYCODE_NAME_1%" + }, + { + "href": "https://rdap.example.com/withSlash/domain/%DOMAIN_PUNYCODE_NAME_1%", + "type": "application/rdap+json", + "rel": "related", + "value": "https://rdap.example.com/withSlash/domain/%DOMAIN_PUNYCODE_NAME_1%" + }, + { + "href": "https://rdap.example.com/withoutSlash/domain/%DOMAIN_PUNYCODE_NAME_1%", + "type": "application/rdap+json", + "rel": "related", + "value": "https://rdap.example.com/withoutSlash/domain/%DOMAIN_PUNYCODE_NAME_1%" } ], "events": [ diff --git a/javatests/google/registry/rdap/testdata/rdapjson_domain_full.json b/javatests/google/registry/rdap/testdata/rdapjson_domain_full.json index 5b552586d..68742a42a 100644 --- a/javatests/google/registry/rdap/testdata/rdapjson_domain_full.json +++ b/javatests/google/registry/rdap/testdata/rdapjson_domain_full.json @@ -17,6 +17,18 @@ "rel" : "self", "href" : "https://example.tld/rdap/domain/cat.xn--q9jyb4c", "type" : "application/rdap+json" + }, + { + "value" : "https://rdap.example.com/withSlash/domain/cat.xn--q9jyb4c", + "rel" : "related", + "href" : "https://rdap.example.com/withSlash/domain/cat.xn--q9jyb4c", + "type" : "application/rdap+json" + }, + { + "value" : "https://rdap.example.com/withoutSlash/domain/cat.xn--q9jyb4c", + "rel" : "related", + "href" : "https://rdap.example.com/withoutSlash/domain/cat.xn--q9jyb4c", + "type" : "application/rdap+json" } ], "events": [ diff --git a/javatests/google/registry/rdap/testdata/rdapjson_domain_logged_out.json b/javatests/google/registry/rdap/testdata/rdapjson_domain_logged_out.json index f6eb42f5e..f5defac03 100644 --- a/javatests/google/registry/rdap/testdata/rdapjson_domain_logged_out.json +++ b/javatests/google/registry/rdap/testdata/rdapjson_domain_logged_out.json @@ -17,6 +17,18 @@ "rel": "self", "href": "https://example.tld/rdap/domain/cat.xn--q9jyb4c", "type": "application/rdap+json" + }, + { + "href": "https://rdap.example.com/withSlash/domain/cat.xn--q9jyb4c", + "type": "application/rdap+json", + "rel": "related", + "value": "https://rdap.example.com/withSlash/domain/cat.xn--q9jyb4c" + }, + { + "href": "https://rdap.example.com/withoutSlash/domain/cat.xn--q9jyb4c", + "type": "application/rdap+json", + "rel": "related", + "value": "https://rdap.example.com/withoutSlash/domain/cat.xn--q9jyb4c" } ], "events": [ diff --git a/javatests/google/registry/rdap/testdata/rdapjson_domain_no_nameservers.json b/javatests/google/registry/rdap/testdata/rdapjson_domain_no_nameservers.json index a0d54a11b..528b0ea96 100644 --- a/javatests/google/registry/rdap/testdata/rdapjson_domain_no_nameservers.json +++ b/javatests/google/registry/rdap/testdata/rdapjson_domain_no_nameservers.json @@ -18,6 +18,18 @@ "rel": "self", "href": "https://example.tld/rdap/domain/fish.xn--q9jyb4c", "type": "application/rdap+json" + }, + { + "href": "https://rdap.example.com/withSlash/domain/fish.xn--q9jyb4c", + "type": "application/rdap+json", + "rel": "related", + "value": "https://rdap.example.com/withSlash/domain/fish.xn--q9jyb4c" + }, + { + "href": "https://rdap.example.com/withoutSlash/domain/fish.xn--q9jyb4c", + "type": "application/rdap+json", + "rel": "related", + "value": "https://rdap.example.com/withoutSlash/domain/fish.xn--q9jyb4c" } ], "events": [ diff --git a/javatests/google/registry/testing/FullFieldsTestEntityHelper.java b/javatests/google/registry/testing/FullFieldsTestEntityHelper.java index 71be4b380..4cb3d88dc 100644 --- a/javatests/google/registry/testing/FullFieldsTestEntityHelper.java +++ b/javatests/google/registry/testing/FullFieldsTestEntityHelper.java @@ -80,6 +80,8 @@ public final class FullFieldsTestEntityHelper { .setFaxNumber("+1.2125551213") .setEmailAddress("contact-us@example.com") .setWhoisServer("whois.example.com") + .setRdapBaseUrls(ImmutableSet.of( + "https://rdap.example.com/withSlash/", "https://rdap.example.com/withoutSlash")) .setUrl("http://my.fake.url") .build(); }