From 55174938c90cd126c6454a7a1ed9ee46b0f5a4d6 Mon Sep 17 00:00:00 2001 From: Harshita Sharma Date: Mon, 10 Nov 2025 08:49:07 +0000 Subject: [PATCH 1/4] feat: Implement rdap_query command --- .../registry/tools/RdapQueryCommand.java | 104 ++++++++++++++++++ .../google/registry/tools/RegistryTool.java | 1 + 2 files changed, 105 insertions(+) create mode 100644 core/src/main/java/google/registry/tools/RdapQueryCommand.java diff --git a/core/src/main/java/google/registry/tools/RdapQueryCommand.java b/core/src/main/java/google/registry/tools/RdapQueryCommand.java new file mode 100644 index 00000000000..eadee58bb78 --- /dev/null +++ b/core/src/main/java/google/registry/tools/RdapQueryCommand.java @@ -0,0 +1,104 @@ +// Copyright 2025 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.tools; + +import com.beust.jcommander.Parameter; +import com.beust.jcommander.Parameters; +import com.google.common.base.Ascii; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; +import com.google.gson.Gson; +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import java.io.IOException; +import java.util.List; +import java.util.Map; + +/** Command to execute an authenticated RDAP query. */ +@Parameters(separators = " =", commandDescription = "Manually perform an authenticated RDAP query") +public final class RdapQueryCommand implements CommandWithConnection { + + private static final ImmutableSet VALID_TYPES = + ImmutableSet.of("domain", "nameserver", "entity"); + + @Parameter(description = "RDAP query string, in the format ", required = true) + private List mainParameters; + + private ServiceConnection connection; + + @Override + public void setConnection(ServiceConnection connection) { + this.connection = connection; + } + + @Override + public void run() { + if (mainParameters == null || mainParameters.size() != 2) { + throw new IllegalArgumentException( + "Usage: nomulus rdap_query \n" + + " must be one of " + + VALID_TYPES); + } + + String type = Ascii.toLowerCase(mainParameters.get(0)); + if (!VALID_TYPES.contains(type)) { + throw new IllegalArgumentException( + String.format("Invalid object type '%s'. Must be one of %s", type, VALID_TYPES)); + } + String name = mainParameters.get(1); + String path = String.format("/rdap/%s/%s", type, name); + + try { + String rdapResponse = connection.sendGetRequest(path, ImmutableMap.of()); + JsonElement rdapJson = JsonParser.parseString(rdapResponse); + System.out.println(formatJsonElement(rdapJson, "")); + } catch (IOException e) { + System.err.println("Request failed for " + path + ": " + e.getMessage()); + } + } + + /** Recursively formats a JSON element into indented key-value pairs. */ + private String formatJsonElement(JsonElement element, String indent) { + StringBuilder sb = new StringBuilder(); + if (element == null || element.isJsonNull()) { + // Omit nulls for cleaner output + } else if (element.isJsonObject()) { + JsonObject obj = element.getAsJsonObject(); + obj.entrySet().stream() + .sorted(Map.Entry.comparingByKey()) // Sort keys for consistent output + .forEach( + entry -> { + if (!entry.getValue().isJsonNull()) { + sb.append(indent).append(entry.getKey()).append(":\n"); + sb.append(formatJsonElement(entry.getValue(), indent + " ")); + } + }); + } else if (element.isJsonArray()) { + JsonArray array = element.getAsJsonArray(); + for (JsonElement arrayElement : array) { + if (arrayElement.isJsonPrimitive()) { + sb.append(indent).append("- ").append(arrayElement.getAsString()).append("\n"); + } else { + sb.append(formatJsonElement(arrayElement, indent + " ")); + } + } + } else if (element.isJsonPrimitive()) { + sb.append(indent).append(element.getAsString()).append("\n"); + } + return sb.toString(); + } +} diff --git a/core/src/main/java/google/registry/tools/RegistryTool.java b/core/src/main/java/google/registry/tools/RegistryTool.java index c08c5cc490c..869e9a927ca 100644 --- a/core/src/main/java/google/registry/tools/RegistryTool.java +++ b/core/src/main/java/google/registry/tools/RegistryTool.java @@ -98,6 +98,7 @@ public final class RegistryTool { .put("login", LoginCommand.class) .put("logout", LogoutCommand.class) .put("pending_escrow", PendingEscrowCommand.class) + .put("rdap_query", RdapQueryCommand.class) .put("recreate_billing_recurrences", RecreateBillingRecurrencesCommand.class) .put("registrar_poc", RegistrarPocCommand.class) .put("renew_domain", RenewDomainCommand.class) From aec2072f10b25cd231a2f0e2f58caa5df233619d Mon Sep 17 00:00:00 2001 From: Harshita Sharma Date: Mon, 10 Nov 2025 15:32:09 +0000 Subject: [PATCH 2/4] feat: Implement rdap_query command and add server-side logging --- .../java/google/registry/rdap/RdapDomainAction.java | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/core/src/main/java/google/registry/rdap/RdapDomainAction.java b/core/src/main/java/google/registry/rdap/RdapDomainAction.java index 6ab9e5a37b9..93cfc4dba5a 100644 --- a/core/src/main/java/google/registry/rdap/RdapDomainAction.java +++ b/core/src/main/java/google/registry/rdap/RdapDomainAction.java @@ -19,6 +19,7 @@ import static google.registry.request.Action.Method.HEAD; import static google.registry.util.DateTimeUtils.START_OF_TIME; +import com.google.common.flogger.FluentLogger; import com.google.common.net.InternetDomainName; import google.registry.flows.EppException; import google.registry.flows.domain.DomainFlowUtils; @@ -45,12 +46,16 @@ auth = Auth.AUTH_PUBLIC) public class RdapDomainAction extends RdapActionBase { + private static final FluentLogger logger = FluentLogger.forEnclosingClass(); + @Inject public RdapDomainAction() { super("domain name", EndpointType.DOMAIN); } @Override public RdapDomain getJsonObjectForResource(String pathSearchString, boolean isHeadRequest) { + logger.atInfo().log("RDAP domain lookup for: %s", pathSearchString); + // RDAP Technical Implementation Guide 2.1.1 - we must support A-label (Punycode) and U-label // (Unicode) formats. canonicalizeName will transform Unicode to Punycode so we support both. pathSearchString = canonicalizeName(pathSearchString); @@ -72,7 +77,13 @@ public RdapDomain getJsonObjectForResource(String pathSearchString, boolean isHe Domain.class, pathSearchString, shouldIncludeDeleted() ? START_OF_TIME : rdapJsonFormatter.getRequestTime()); + + logger.atInfo().log("Domain lookup result: %s", domain.isPresent() ? "FOUND" : "NOT FOUND"); + if (domain.isEmpty() || !isAuthorized(domain.get())) { + if (domain.isPresent()) { + logger.atInfo().log("Authorization result for domain: %s", isAuthorized(domain.get())); + } handlePossibleBsaBlock(domainName); // RFC7480 5.3 - if the server wishes to respond that it doesn't have data satisfying the // query, it MUST reply with 404 response code. From 53d60ed3b0061039e04f4314a9cd47a5e0f07e49 Mon Sep 17 00:00:00 2001 From: Harshita Sharma Date: Wed, 12 Nov 2025 15:22:10 +0000 Subject: [PATCH 3/4] feat: Implement rdap_query command --- .../registry/tools/RdapQueryCommand.java | 114 ++++++++---------- .../registry/tools/RegistryToolComponent.java | 2 + 2 files changed, 51 insertions(+), 65 deletions(-) diff --git a/core/src/main/java/google/registry/tools/RdapQueryCommand.java b/core/src/main/java/google/registry/tools/RdapQueryCommand.java index eadee58bb78..b0963cfb4a8 100644 --- a/core/src/main/java/google/registry/tools/RdapQueryCommand.java +++ b/core/src/main/java/google/registry/tools/RdapQueryCommand.java @@ -1,104 +1,88 @@ -// Copyright 2025 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.tools; +import static com.google.common.base.Preconditions.checkArgument; + import com.beust.jcommander.Parameter; import com.beust.jcommander.Parameters; import com.google.common.base.Ascii; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; +import com.google.common.flogger.GoogleLogger; import com.google.gson.Gson; -import com.google.gson.JsonArray; +import com.google.gson.GsonBuilder; import com.google.gson.JsonElement; -import com.google.gson.JsonObject; import com.google.gson.JsonParser; +import google.registry.config.RegistryConfig.Config; +import google.registry.request.Action.GkeService; import java.io.IOException; import java.util.List; -import java.util.Map; +import javax.inject.Inject; -/** Command to execute an authenticated RDAP query. */ +/** Command to manually perform an authenticated RDAP query. */ @Parameters(separators = " =", commandDescription = "Manually perform an authenticated RDAP query") public final class RdapQueryCommand implements CommandWithConnection { + private static final GoogleLogger logger = GoogleLogger.forEnclosingClass(); + private static final ImmutableSet VALID_TYPES = - ImmutableSet.of("domain", "nameserver", "entity"); + ImmutableSet.of("domain", "registrar", "contact", "nameserver"); - @Parameter(description = "RDAP query string, in the format ", required = true) + @Parameter(description = "The object type and query term.", required = true) private List mainParameters; - private ServiceConnection connection; + private ServiceConnection defaultConnection; + + @Inject + @Config("useCanary") + boolean useCanary; @Override public void setConnection(ServiceConnection connection) { - this.connection = connection; + this.defaultConnection = connection; } @Override public void run() { - if (mainParameters == null || mainParameters.size() != 2) { - throw new IllegalArgumentException( - "Usage: nomulus rdap_query \n" - + " must be one of " - + VALID_TYPES); - } + checkArgument( + mainParameters != null && mainParameters.size() == 2, + "Usage: nomulus rdap_query \n" + + " must be one of " + + VALID_TYPES); String type = Ascii.toLowerCase(mainParameters.get(0)); - if (!VALID_TYPES.contains(type)) { - throw new IllegalArgumentException( - String.format("Invalid object type '%s'. Must be one of %s", type, VALID_TYPES)); - } + checkArgument( + VALID_TYPES.contains(type), + "Invalid object type '%s'. Must be one of %s", + type, + VALID_TYPES); + String name = mainParameters.get(1); String path = String.format("/rdap/%s/%s", type, name); + logger.atInfo().log("Starting RDAP query for path: %s", path); + try { - String rdapResponse = connection.sendGetRequest(path, ImmutableMap.of()); + if (defaultConnection == null) { + throw new IllegalStateException("ServiceConnection was not set by RegistryCli."); + } + // Create a new ServiceConnection instance targeting GkeService.PUBAPI. + // This is necessary because the default connection provided by RegistryCli + // targets GkeService.BACKEND, but RDAP queries need to be routed to the + // public-facing API. + ServiceConnection pubapiConnection = + defaultConnection.withService(GkeService.PUBAPI, useCanary); + + String rdapResponse = pubapiConnection.sendGetRequest(path, ImmutableMap.of()); JsonElement rdapJson = JsonParser.parseString(rdapResponse); - System.out.println(formatJsonElement(rdapJson, "")); + + // Pretty-print the JSON response. + Gson gson = new GsonBuilder().setPrettyPrinting().create(); + System.out.println(gson.toJson(rdapJson)); + + logger.atInfo().log("Successfully completed RDAP query for path: %s", path); } catch (IOException e) { + logger.atSevere().withCause(e).log("Request failed for path: %s", path); System.err.println("Request failed for " + path + ": " + e.getMessage()); } } - - /** Recursively formats a JSON element into indented key-value pairs. */ - private String formatJsonElement(JsonElement element, String indent) { - StringBuilder sb = new StringBuilder(); - if (element == null || element.isJsonNull()) { - // Omit nulls for cleaner output - } else if (element.isJsonObject()) { - JsonObject obj = element.getAsJsonObject(); - obj.entrySet().stream() - .sorted(Map.Entry.comparingByKey()) // Sort keys for consistent output - .forEach( - entry -> { - if (!entry.getValue().isJsonNull()) { - sb.append(indent).append(entry.getKey()).append(":\n"); - sb.append(formatJsonElement(entry.getValue(), indent + " ")); - } - }); - } else if (element.isJsonArray()) { - JsonArray array = element.getAsJsonArray(); - for (JsonElement arrayElement : array) { - if (arrayElement.isJsonPrimitive()) { - sb.append(indent).append("- ").append(arrayElement.getAsString()).append("\n"); - } else { - sb.append(formatJsonElement(arrayElement, indent + " ")); - } - } - } else if (element.isJsonPrimitive()) { - sb.append(indent).append(element.getAsString()).append("\n"); - } - return sb.toString(); - } } diff --git a/core/src/main/java/google/registry/tools/RegistryToolComponent.java b/core/src/main/java/google/registry/tools/RegistryToolComponent.java index 4846e77b384..386ec2a5749 100644 --- a/core/src/main/java/google/registry/tools/RegistryToolComponent.java +++ b/core/src/main/java/google/registry/tools/RegistryToolComponent.java @@ -135,6 +135,8 @@ interface RegistryToolComponent { void inject(PendingEscrowCommand command); + void inject(RdapQueryCommand command); + void inject(RenewDomainCommand command); void inject(SaveSqlCredentialCommand command); From 59ea436eab8a827e7410c3abbddf60d91904ba66 Mon Sep 17 00:00:00 2001 From: Harshita Sharma Date: Wed, 12 Nov 2025 15:39:26 +0000 Subject: [PATCH 4/4] feat: Implement rdap_query command and add server-side logging --- core/build.gradle | 1 + 1 file changed, 1 insertion(+) diff --git a/core/build.gradle b/core/build.gradle index c40058ec8db..4d404694ceb 100644 --- a/core/build.gradle +++ b/core/build.gradle @@ -68,6 +68,7 @@ sourceSets { srcDirs += generatedDir } resources { + srcDirs = ['src/main/resources'] exclude '**/*.xjb' } }