diff --git a/java/google/registry/tools/AppEngineConnection.java b/java/google/registry/tools/AppEngineConnection.java index 97210e9d1..d15be6f21 100644 --- a/java/google/registry/tools/AppEngineConnection.java +++ b/java/google/registry/tools/AppEngineConnection.java @@ -14,6 +14,7 @@ package google.registry.tools; +import static com.google.common.base.Preconditions.checkNotNull; import static com.google.common.base.Suppliers.memoize; import static com.google.common.net.HttpHeaders.X_REQUESTED_WITH; import static com.google.common.net.MediaType.JSON_UTF_8; @@ -40,6 +41,7 @@ import google.registry.tools.ServerSideCommand.Connection; import java.io.IOException; import java.io.InputStreamReader; import java.util.Map; +import javax.annotation.Nullable; import javax.inject.Inject; import org.json.simple.JSONValue; @@ -81,14 +83,16 @@ class AppEngineConnection implements Connection { return CharStreams.toString(new InputStreamReader(response.getContent(), UTF_8)); } - @Override - public String send( - String endpoint, Map params, MediaType contentType, byte[] payload) - throws IOException { + private String internalSend( + String endpoint, Map params, MediaType contentType, @Nullable byte[] payload) + throws IOException { GenericUrl url = new GenericUrl(String.format("%s%s", getServerUrl(), endpoint)); url.putAll(params); HttpRequest request = - requestFactory.buildPostRequest(url, new ByteArrayContent(contentType.toString(), payload)); + (payload != null) + ? requestFactory.buildPostRequest( + url, new ByteArrayContent(contentType.toString(), payload)) + : requestFactory.buildGetRequest(url); HttpHeaders headers = request.getHeaders(); headers.setCacheControl("no-cache"); headers.put(X_CSRF_TOKEN, ImmutableList.of(xsrfToken.get())); @@ -118,14 +122,27 @@ class AppEngineConnection implements Connection { } } + // TODO(b/111123862): Rename this to sendPostRequest() + @Override + public String send(String endpoint, Map params, MediaType contentType, byte[] payload) + throws IOException { + return internalSend(endpoint, params, contentType, checkNotNull(payload, "payload")); + } + + @Override + public String sendGetRequest(String endpoint, Map params) throws IOException { + return internalSend(endpoint, params, MediaType.PLAIN_TEXT_UTF_8, null); + } + @Override @SuppressWarnings("unchecked") public Map sendJson(String endpoint, Map object) throws IOException { - String response = send( - endpoint, - ImmutableMap.of(), - JSON_UTF_8, - JSONValue.toJSONString(object).getBytes(UTF_8)); + String response = + send( + endpoint, + ImmutableMap.of(), + JSON_UTF_8, + JSONValue.toJSONString(object).getBytes(UTF_8)); return (Map) JSONValue.parse(response.substring(JSON_SAFETY_PREFIX.length())); } diff --git a/java/google/registry/tools/CurlCommand.java b/java/google/registry/tools/CurlCommand.java new file mode 100644 index 000000000..090f13fcc --- /dev/null +++ b/java/google/registry/tools/CurlCommand.java @@ -0,0 +1,89 @@ +// Copyright 2018 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 java.nio.charset.StandardCharsets.UTF_8; + +import com.beust.jcommander.Parameter; +import com.beust.jcommander.Parameters; +import com.google.common.base.Joiner; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.net.MediaType; +import java.util.List; + +@Parameters(separators = " =", commandDescription = "Send an HTTP command to the nomulus server.") +class CurlCommand implements ServerSideCommand { + private Connection connection; + + // HTTP Methods that are acceptable for use as values for --method. + public enum Method { + GET, + POST + } + + @Parameter( + names = {"-X", "--request"}, + description = "HTTP method. Must be either \"GET\" or \"POST\".") + private Method method; + + @Parameter( + names = {"-u", "--path"}, + description = + "URL path to send the request to. (e.g. \"/_dr/foo?parm=val\"). Be careful " + + "with the shell quoting.", + required = true) + private String path; + + @Parameter( + names = {"-t", "--content-type"}, + description = + "Media type of the request body (for a POST request. Must be combined with --body)") + private MediaType mimeType = MediaType.PLAIN_TEXT_UTF_8; + + // TODO(b/112314048): Make this data flag friendlier (support escaping, convert to query args for + // GET...) + @Parameter( + names = {"-d", "--data"}, + description = + "Body for a post request. If specified, a POST request is sent. If " + + "absent, a GET request is sent.") + private List data; + + @Override + public void setConnection(Connection connection) { + this.connection = connection; + } + + @Override + public void run() throws Exception { + if (method == null) { + method = (data == null) ? Method.GET : Method.POST; + } else if (method == Method.POST && data == null) { + data = ImmutableList.of(""); + } else if (method == Method.GET && data != null) { + throw new IllegalArgumentException("You may not specify a body for a get method."); + } + + // TODO(b/112315418): Make it possible to address any backend. + String response = + (method == Method.GET) + ? connection.sendGetRequest(path, ImmutableMap.of()) + : connection.send( + path, ImmutableMap.of(), mimeType, + Joiner.on("&").join(data).getBytes(UTF_8)); + System.out.println(response); + } +} diff --git a/java/google/registry/tools/DeleteEntityCommand.java b/java/google/registry/tools/DeleteEntityCommand.java deleted file mode 100644 index a84b901c0..000000000 --- a/java/google/registry/tools/DeleteEntityCommand.java +++ /dev/null @@ -1,67 +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.tools; - -import com.beust.jcommander.Parameter; -import com.beust.jcommander.Parameters; -import com.google.common.base.Joiner; -import com.google.common.collect.ImmutableMap; -import com.google.common.net.MediaType; -import google.registry.tools.server.DeleteEntityAction; -import java.util.List; - -/** - * Command to delete an entity (or entities) in Datastore specified by raw key ids, which can be - * found in Datastore Viewer in the AppEngine console -- it's the really long alphanumeric key that - * is labeled "Entity key" on the page for an individual entity. - * - *

WARNING: This command can be dangerous if used incorrectly as it can bypass checks on - * deletion (including whether the entity is referenced by other entities) and it does not write - * commit log entries for non-registered types. It should mainly be used for deleting testing or - * malformed data that cannot be properly deleted using existing tools. Generally, if there already - * exists an entity-specific deletion command, then use that one instead. - */ -@Parameters(separators = " =", commandDescription = "Delete entities from Datastore by raw key.") -public class DeleteEntityCommand extends ConfirmingCommand implements ServerSideCommand { - - @Parameter(description = "One or more raw keys of entities to delete.", required = true) - private List rawKeyStrings; - - private Connection connection; - - @Override - public void setConnection(Connection connection) { - this.connection = connection; - } - - @Override - protected String prompt() { - if (rawKeyStrings.size() == 1) { - return "You are about to delete the entity: \n" + rawKeyStrings.get(0); - } else { - return "You are about to delete the entities: \n" + rawKeyStrings; - } - } - - @Override - protected String execute() throws Exception { - String rawKeysJoined = Joiner.on(",").join(rawKeyStrings); - return connection.send( - DeleteEntityAction.PATH, - ImmutableMap.of(DeleteEntityAction.PARAM_RAW_KEYS, rawKeysJoined), - MediaType.PLAIN_TEXT_UTF_8, - new byte[0]); - } -} diff --git a/java/google/registry/tools/RegistryTool.java b/java/google/registry/tools/RegistryTool.java index e46b5980d..ebefa483d 100644 --- a/java/google/registry/tools/RegistryTool.java +++ b/java/google/registry/tools/RegistryTool.java @@ -47,8 +47,8 @@ public final class RegistryTool { .put("create_registrar_groups", CreateRegistrarGroupsCommand.class) .put("create_reserved_list", CreateReservedListCommand.class) .put("create_tld", CreateTldCommand.class) + .put("curl", CurlCommand.class) .put("delete_domain", DeleteDomainCommand.class) - .put("delete_entity", DeleteEntityCommand.class) .put("delete_host", DeleteHostCommand.class) .put("delete_premium_list", DeletePremiumListCommand.class) .put("delete_reserved_list", DeleteReservedListCommand.class) @@ -103,7 +103,6 @@ public final class RegistryTool { .put("resave_entities", ResaveEntitiesCommand.class) .put("resave_environment_entities", ResaveEnvironmentEntitiesCommand.class) .put("resave_epp_resource", ResaveEppResourceCommand.class) - .put("restore_commit_logs", RestoreCommitLogsCommand.class) .put("send_escrow_report_to_icann", SendEscrowReportToIcannCommand.class) .put("setup_ote", SetupOteCommand.class) .put("uniform_rapid_suspension", UniformRapidSuspensionCommand.class) diff --git a/java/google/registry/tools/RestoreCommitLogsCommand.java b/java/google/registry/tools/RestoreCommitLogsCommand.java deleted file mode 100644 index 31b9f7c39..000000000 --- a/java/google/registry/tools/RestoreCommitLogsCommand.java +++ /dev/null @@ -1,65 +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.tools; - -import com.beust.jcommander.Parameter; -import com.beust.jcommander.Parameters; -import com.google.common.collect.ImmutableMap; -import com.google.common.net.MediaType; -import google.registry.backup.RestoreCommitLogsAction; -import org.joda.time.DateTime; - -@Parameters(separators = " =", commandDescription = "Restore the commit logs.") -class RestoreCommitLogsCommand implements ServerSideCommand { - private Connection connection; - - @Parameter( - names = {"-d", "--dry_run"}, - description = "Don't actually make any changes, just show what you would do." - ) - private boolean dryRun = false; - - @Parameter( - names = {"-f", "--from_time"}, - description = "Time to start restoring from.", - required = true - ) - private DateTime fromTime; - - @Parameter( - names = {"-t", "--to_time"}, - description = "Last commit diff timestamp to use when restoring." - ) - private DateTime toTime; - - @Override - public void setConnection(Connection connection) { - this.connection = connection; - } - - @Override - public void run() throws Exception { - ImmutableMap.Builder params = new ImmutableMap.Builder<>(); - params.put("dryRun", dryRun); - params.put("fromTime", fromTime); - if (toTime != null) { - params.put("toTime", toTime); - } - String response = - connection.send( - RestoreCommitLogsAction.PATH, params.build(), MediaType.PLAIN_TEXT_UTF_8, new byte[0]); - System.out.println(response); - } -} diff --git a/java/google/registry/tools/ServerSideCommand.java b/java/google/registry/tools/ServerSideCommand.java index 32b21eb4d..1e514bf43 100644 --- a/java/google/registry/tools/ServerSideCommand.java +++ b/java/google/registry/tools/ServerSideCommand.java @@ -18,6 +18,7 @@ import com.google.common.net.MediaType; import google.registry.tools.Command.RemoteApiCommand; import java.io.IOException; import java.util.Map; +import javax.annotation.Nullable; /** A command that executes on the server. */ interface ServerSideCommand extends RemoteApiCommand { @@ -27,9 +28,13 @@ interface ServerSideCommand extends RemoteApiCommand { void prefetchXsrfToken(); - String send(String endpoint, Map params, MediaType contentType, byte[] payload) + /** Send a POST request. TODO(mmuller): change to sendPostRequest() */ + String send( + String endpoint, Map params, MediaType contentType, @Nullable byte[] payload) throws IOException; + String sendGetRequest(String endpoint, Map params) throws IOException; + Map sendJson(String endpoint, Map object) throws IOException; String getServerUrl(); diff --git a/javatests/google/registry/tools/CurlCommandTest.java b/javatests/google/registry/tools/CurlCommandTest.java new file mode 100644 index 000000000..0685c031f --- /dev/null +++ b/javatests/google/registry/tools/CurlCommandTest.java @@ -0,0 +1,100 @@ +// 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.tools; + +import static com.google.common.truth.Truth.assertThat; +import static google.registry.testing.JUnitBackports.assertThrows; +import static java.nio.charset.StandardCharsets.UTF_8; +import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.verify; + +import com.google.common.collect.ImmutableMap; +import com.google.common.net.MediaType; +import google.registry.tools.ServerSideCommand.Connection; +import org.junit.Before; +import org.junit.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.Mock; + +/** Unit tests for {@link RefreshDnsForAllDomainsCommand}. */ +public class CurlCommandTest extends CommandTestCase { + @Mock private Connection connection; + + @Before + public void init() { + command.setConnection(connection); + } + + @Captor ArgumentCaptor> urlParamCaptor; + + @Test + public void testGetInvocation() throws Exception { + runCommand("--path=/foo/bar?a=1&b=2"); + verify(connection) + .sendGetRequest(eq("/foo/bar?a=1&b=2"), eq(ImmutableMap.of())); + } + + @Test + public void testExplicitGetInvocation() throws Exception { + runCommand("--path=/foo/bar?a=1&b=2", "--request=GET"); + verify(connection) + .sendGetRequest(eq("/foo/bar?a=1&b=2"), eq(ImmutableMap.of())); + } + + @Test + public void testPostInvocation() throws Exception { + runCommand("--path=/foo/bar?a=1&b=2", "--data=some data"); + verify(connection) + .send( + eq("/foo/bar?a=1&b=2"), + eq(ImmutableMap.of()), + eq(MediaType.PLAIN_TEXT_UTF_8), + eq("some data".getBytes(UTF_8))); + } + + @Test + public void testMultiDataPost() throws Exception { + runCommand("--path=/foo/bar?a=1&b=2", "--data=first=100", "-d", "second=200"); + verify(connection) + .send( + eq("/foo/bar?a=1&b=2"), + eq(ImmutableMap.of()), + eq(MediaType.PLAIN_TEXT_UTF_8), + eq("first=100&second=200".getBytes(UTF_8))); + } + + @Test + public void testExplicitPostInvocation() throws Exception { + runCommand("--path=/foo/bar?a=1&b=2", "--request=POST"); + verify(connection) + .send( + eq("/foo/bar?a=1&b=2"), + eq(ImmutableMap.of()), + eq(MediaType.PLAIN_TEXT_UTF_8), + eq("".getBytes(UTF_8))); + } + + @Test + public void testGetWithBody() throws Exception { + IllegalArgumentException thrown = + assertThrows( + IllegalArgumentException.class, + () -> + runCommand( + "--path=/foo/bar?a=1&b=2", "--request=GET", "--data=inappropriate data")); + assertThat(thrown).hasMessageThat().contains("You may not specify a body for a get method."); + } +} diff --git a/javatests/google/registry/tools/DeleteEntityCommandTest.java b/javatests/google/registry/tools/DeleteEntityCommandTest.java deleted file mode 100644 index c08eba008..000000000 --- a/javatests/google/registry/tools/DeleteEntityCommandTest.java +++ /dev/null @@ -1,58 +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.tools; - -import static org.mockito.Matchers.any; -import static org.mockito.Matchers.anyMap; -import static org.mockito.Matchers.anyString; -import static org.mockito.Matchers.eq; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -import com.google.common.collect.ImmutableMap; -import com.google.common.net.MediaType; -import google.registry.tools.ServerSideCommand.Connection; -import org.junit.Before; -import org.junit.Test; -import org.mockito.Mock; - -/** Unit tests for {@link DeleteEntityCommand}. */ -public class DeleteEntityCommandTest extends CommandTestCase { - - @Mock - private Connection connection; - - @Before - public void init() { - command.setConnection(connection); - } - - @SuppressWarnings("unchecked") - @Test - public void test_deleteTwoEntities() throws Exception { - String firstKey = "alphaNumericKey1"; - String secondKey = "alphaNumericKey2"; - String rawKeys = String.format("%s,%s", firstKey, secondKey); - when(connection.send(anyString(), anyMap(), any(MediaType.class), any(byte[].class))) - .thenReturn("Deleted 1 raw entities and 1 registered entities."); - runCommandForced(firstKey, secondKey); - verify(connection).send( - eq("/_dr/admin/deleteEntity"), - eq(ImmutableMap.of("rawKeys", rawKeys)), - eq(MediaType.PLAIN_TEXT_UTF_8), - eq(new byte[0])); - assertInStdout("Deleted 1 raw entities and 1 registered entities."); - } -} diff --git a/javatests/google/registry/tools/RestoreCommitLogsCommandTest.java b/javatests/google/registry/tools/RestoreCommitLogsCommandTest.java deleted file mode 100644 index dac5a8ffd..000000000 --- a/javatests/google/registry/tools/RestoreCommitLogsCommandTest.java +++ /dev/null @@ -1,81 +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.tools; - -import static com.google.common.truth.Truth.assertThat; -import static org.mockito.Matchers.eq; -import static org.mockito.Mockito.verify; - -import com.google.common.collect.ImmutableMap; -import com.google.common.net.MediaType; -import google.registry.backup.RestoreCommitLogsAction; -import google.registry.tools.ServerSideCommand.Connection; -import org.joda.time.DateTime; -import org.junit.Before; -import org.junit.Test; -import org.mockito.ArgumentCaptor; -import org.mockito.Captor; -import org.mockito.Mock; - -/** Unit tests for {@link CreateRegistrarCommand}. */ -public class RestoreCommitLogsCommandTest extends CommandTestCase { - @Mock private Connection connection; - - @Before - public void init() { - command.setConnection(connection); - } - - @Captor ArgumentCaptor> urlParamCaptor; - - @Test - public void testNormalForm() throws Exception { - runCommand("--from_time=2017-05-19T20:30:00Z"); - verifySend( - ImmutableMap.of("dryRun", false, "fromTime", DateTime.parse("2017-05-19T20:30:00.000Z"))); - } - - @Test - public void testToTime() throws Exception { - runCommand("--from_time=2017-05-19T20:30:00Z", "--to_time=2017-05-19T20:40:00Z"); - verifySend( - ImmutableMap.of( - "dryRun", false, - "fromTime", DateTime.parse("2017-05-19T20:30:00.000Z"), - "toTime", DateTime.parse("2017-05-19T20:40:00.000Z"))); - } - - @Test - public void testDryRun() throws Exception { - runCommand("--dry_run", "--from_time=2017-05-19T20:30:00Z", "--to_time=2017-05-19T20:40:00Z"); - verifySend( - ImmutableMap.of( - "dryRun", true, - "fromTime", DateTime.parse("2017-05-19T20:30:00.000Z"), - "toTime", DateTime.parse("2017-05-19T20:40:00.000Z"))); - } - - // Note that this is very similar to the one in CreateOrUpdatePremiumListCommandTestCase.java but - // not identical. - void verifySend(ImmutableMap parameters) throws Exception { - verify(connection) - .send( - eq(RestoreCommitLogsAction.PATH), - urlParamCaptor.capture(), - eq(MediaType.PLAIN_TEXT_UTF_8), - eq(new byte[0])); - assertThat(urlParamCaptor.getValue()).containsExactlyEntriesIn(parameters); - } -}