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..258d1c8c93f --- /dev/null +++ b/core/src/main/java/google/registry/tools/RdapQueryCommand.java @@ -0,0 +1,100 @@ +// 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.checkState; + +import com.beust.jcommander.Parameter; +import com.beust.jcommander.Parameters; +import com.google.common.collect.ImmutableMap; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonElement; +import com.google.gson.JsonParser; +import google.registry.config.RegistryConfig.Config; +import google.registry.request.Action.Service; +import jakarta.inject.Inject; +import java.io.IOException; +import java.util.Optional; +import javax.annotation.Nullable; + +/** 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 Gson GSON = new GsonBuilder().setPrettyPrinting().create(); + + enum RdapQueryType { + DOMAIN("/rdap/domain/%s"), + DOMAIN_SEARCH("/rdap/domains", "name"), + NAMESERVER("/rdap/nameserver/%s"), + NAMESERVER_SEARCH("/rdap/nameservers", "name"), + ENTITY("/rdap/entity/%s"), + ENTITY_SEARCH("/rdap/entities", "fn"); + + private final String pathFormat; + private final Optional searchParamKey; + + RdapQueryType(String pathFormat) { + this(pathFormat, null); + } + + RdapQueryType(String pathFormat, @Nullable String searchParamKey) { + this.pathFormat = pathFormat; + this.searchParamKey = Optional.ofNullable(searchParamKey); + } + + String getQueryPath(String queryTerm) { + return String.format(pathFormat, queryTerm); + } + + ImmutableMap getQueryParameters(String queryTerm) { + return searchParamKey.map(key -> ImmutableMap.of(key, queryTerm)).orElse(ImmutableMap.of()); + } + } + + @Parameter(names = "--type", description = "The type of RDAP query to perform.", required = true) + private RdapQueryType type; + + @Parameter( + description = "The main query term (e.g., a domain name or search pattern).", + required = true) + private String queryTerm; + + @Inject ServiceConnection defaultConnection; + + @Inject + @Config("useCanary") + boolean useCanary; + + @Override + public void setConnection(ServiceConnection connection) { + this.defaultConnection = connection; + } + + @Override + public void run() throws IOException { + checkState(defaultConnection != null, "ServiceConnection was not set by RegistryCli."); + + String path = type.getQueryPath(queryTerm); + ImmutableMap queryParams = type.getQueryParameters(queryTerm); + + ServiceConnection pubapiConnection = defaultConnection.withService(Service.PUBAPI, useCanary); + String rdapResponse = pubapiConnection.sendGetRequest(path, queryParams); + + JsonElement rdapJson = JsonParser.parseString(rdapResponse); + System.out.println(GSON.toJson(rdapJson)); + } +} 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) 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); diff --git a/core/src/test/java/google/registry/tools/RdapQueryCommandTest.java b/core/src/test/java/google/registry/tools/RdapQueryCommandTest.java new file mode 100644 index 00000000000..91c14d8bbc5 --- /dev/null +++ b/core/src/test/java/google/registry/tools/RdapQueryCommandTest.java @@ -0,0 +1,99 @@ +// 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 org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import com.beust.jcommander.ParameterException; +import com.google.common.collect.ImmutableMap; +import google.registry.request.Action.Service; +import java.io.IOException; +import java.util.Map; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.mockito.junit.jupiter.MockitoSettings; +import org.mockito.quality.Strictness; + +/** Unit tests for {@link RdapQueryCommand}. */ +@ExtendWith(MockitoExtension.class) +@MockitoSettings(strictness = Strictness.LENIENT) +class RdapQueryCommandTest extends CommandTestCase { + + @Mock private ServiceConnection mockDefaultConnection; + @Mock private ServiceConnection mockPubapiConnection; + + @BeforeEach + void beforeEach() throws IOException { + command.setConnection(mockDefaultConnection); + when(mockDefaultConnection.withService(Service.PUBAPI, false)).thenReturn(mockPubapiConnection); + when(mockPubapiConnection.sendGetRequest(anyString(), any(Map.class))).thenReturn(""); + } + + @Test + void testSuccess_domainLookup() throws Exception { + runCommand("--type=DOMAIN", "example.dev"); + verify(mockPubapiConnection).sendGetRequest("/rdap/domain/example.dev", ImmutableMap.of()); + } + + @Test + void testSuccess_domainSearch() throws Exception { + runCommand("--type=DOMAIN_SEARCH", "exam*.dev"); + verify(mockPubapiConnection) + .sendGetRequest("/rdap/domains", ImmutableMap.of("name", "exam*.dev")); + } + + @Test + void testSuccess_nameserverLookup() throws Exception { + runCommand("--type=NAMESERVER", "ns1.example.com"); + verify(mockPubapiConnection) + .sendGetRequest("/rdap/nameserver/ns1.example.com", ImmutableMap.of()); + } + + @Test + void testSuccess_nameserverSearch() throws Exception { + runCommand("--type=NAMESERVER_SEARCH", "ns*.example.com"); + verify(mockPubapiConnection) + .sendGetRequest("/rdap/nameservers", ImmutableMap.of("name", "ns*.example.com")); + } + + @Test + void testSuccess_entityLookup() throws Exception { + runCommand("--type=ENTITY", "123-FOO"); + verify(mockPubapiConnection).sendGetRequest("/rdap/entity/123-FOO", ImmutableMap.of()); + } + + @Test + void testSuccess_entitySearch() throws Exception { + runCommand("--type=ENTITY_SEARCH", "John*"); + verify(mockPubapiConnection).sendGetRequest("/rdap/entities", ImmutableMap.of("fn", "John*")); + } + + @Test + void testFailure_missingType() { + assertThrows(ParameterException.class, () -> runCommand("some-term")); + } + + @Test + void testFailure_missingQueryTerm() { + assertThrows(ParameterException.class, () -> runCommand("--type=DOMAIN")); + } +}