diff --git a/java/google/registry/flows/CheckApi2Action.java b/java/google/registry/flows/CheckApi2Action.java
new file mode 100644
index 000000000..60bf5d95f
--- /dev/null
+++ b/java/google/registry/flows/CheckApi2Action.java
@@ -0,0 +1,148 @@
+// 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.flows;
+
+import static com.google.common.base.Strings.nullToEmpty;
+import static com.google.common.net.HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN;
+import static google.registry.flows.domain.DomainFlowUtils.validateDomainName;
+import static google.registry.flows.domain.DomainFlowUtils.validateDomainNameWithIdnTables;
+import static google.registry.flows.domain.DomainFlowUtils.verifyNotInPredelegation;
+import static google.registry.model.registry.label.ReservationType.getTypeOfHighestSeverity;
+import static google.registry.model.registry.label.ReservedList.getReservationTypes;
+import static google.registry.pricing.PricingEngineProxy.isDomainPremium;
+import static google.registry.util.DomainNameUtils.canonicalizeDomainName;
+import static org.json.simple.JSONValue.toJSONString;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.flogger.FluentLogger;
+import com.google.common.net.InternetDomainName;
+import com.google.common.net.MediaType;
+import dagger.Module;
+import google.registry.flows.domain.DomainFlowUtils.BadCommandForRegistryPhaseException;
+import google.registry.model.domain.DomainResource;
+import google.registry.model.index.ForeignKeyIndex;
+import google.registry.model.registry.Registry;
+import google.registry.model.registry.label.ReservationType;
+import google.registry.request.Action;
+import google.registry.request.Parameter;
+import google.registry.request.Response;
+import google.registry.request.auth.Auth;
+import google.registry.util.Clock;
+import java.util.Map;
+import java.util.Optional;
+import javax.inject.Inject;
+import org.joda.time.DateTime;
+
+/**
+ * An action that returns availability and premium checks as JSON.
+ *
+ * This action returns plain JSON without a safety prefix, so it's vital that the output not be
+ * user controlled, lest it open an XSS vector. Do not modify this to return the domain name in the
+ * response.
+ */
+@Action(path = "/check2", auth = Auth.AUTH_PUBLIC_ANONYMOUS)
+// TODO(b/80417678): rename this class to CheckApiAction and change path to "/check".
+public class CheckApi2Action implements Runnable {
+
+ private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
+ @Inject
+ @Parameter("domain")
+ String domain;
+
+ @Inject Response response;
+ @Inject Clock clock;
+
+ @Inject
+ CheckApi2Action() {}
+
+ @Override
+ public void run() {
+ response.setHeader("Content-Disposition", "attachment");
+ response.setHeader("X-Content-Type-Options", "nosniff");
+ response.setHeader(ACCESS_CONTROL_ALLOW_ORIGIN, "*");
+ response.setContentType(MediaType.JSON_UTF_8);
+ response.setPayload(toJSONString(doCheck()));
+ }
+
+ private Map doCheck() {
+ String domainString;
+ InternetDomainName domainName;
+ try {
+ domainString = canonicalizeDomainName(nullToEmpty(domain));
+ domainName = validateDomainName(domainString);
+ } catch (IllegalArgumentException | EppException e) {
+ return fail("Must supply a valid domain name on an authoritative TLD");
+ }
+ try {
+ // Throws an EppException with a reasonable error message which will be sent back to caller.
+ validateDomainNameWithIdnTables(domainName);
+
+ DateTime now = clock.nowUtc();
+ Registry registry = Registry.get(domainName.parent().toString());
+ try {
+ verifyNotInPredelegation(registry, now);
+ } catch (BadCommandForRegistryPhaseException e) {
+ return fail("Check in this TLD is not allowed in the current registry phase");
+ }
+
+ String errorMsg =
+ checkExists(domainString, now)
+ ? "In use"
+ : checkReserved(domainName).orElse(null);
+
+ boolean available = (errorMsg == null);
+ ImmutableMap.Builder responseBuilder = new ImmutableMap.Builder<>();
+ responseBuilder.put("status", "success").put("available", available);
+ if (available) {
+ responseBuilder.put("tier", isDomainPremium(domainString, now) ? "premium" : "standard");
+ } else {
+ responseBuilder.put("reason", errorMsg);
+ }
+ return responseBuilder.build();
+ } catch (EppException e) {
+ return fail(e.getResult().getMsg());
+ } catch (Exception e) {
+ logger.atWarning().withCause(e).log("Unknown error");
+ return fail("Invalid request");
+ }
+ }
+
+ private boolean checkExists(String domainString, DateTime now) {
+ return !ForeignKeyIndex.loadCached(DomainResource.class, ImmutableList.of(domainString), now)
+ .isEmpty();
+ }
+
+ private Optional checkReserved(InternetDomainName domainName) {
+ ImmutableSet reservationTypes =
+ getReservationTypes(domainName.parts().get(0), domainName.parent().toString());
+ if (!reservationTypes.isEmpty()) {
+ return Optional.of(getTypeOfHighestSeverity(reservationTypes).getMessageForCheck());
+ }
+ return Optional.empty();
+ }
+
+ private Map fail(String reason) {
+ return ImmutableMap.of("status", "error", "reason", reason);
+ }
+
+ /** Dagger module for the check api endpoint. */
+ @Module
+ public static final class CheckApi2Module {
+ // TODO(b/80417678): provide Parameter("domain") once CheckApiAction is replaced by this class.
+ }
+}
diff --git a/java/google/registry/flows/domain/DomainFlowUtils.java b/java/google/registry/flows/domain/DomainFlowUtils.java
index bedab7f9f..0bddd38a9 100644
--- a/java/google/registry/flows/domain/DomainFlowUtils.java
+++ b/java/google/registry/flows/domain/DomainFlowUtils.java
@@ -231,7 +231,7 @@ public class DomainFlowUtils {
* @throws InvalidIdnDomainLabelException if IDN table or language validation failed
* @see #validateDomainName(String)
*/
- static String validateDomainNameWithIdnTables(InternetDomainName domainName)
+ public static String validateDomainNameWithIdnTables(InternetDomainName domainName)
throws InvalidIdnDomainLabelException {
Optional idnTableName =
findValidIdnTableForTld(domainName.parts().get(0), domainName.parent().toString());
@@ -856,7 +856,7 @@ public class DomainFlowUtils {
}
/** Check that the registry phase is not predelegation, during which some flows are forbidden. */
- static void verifyNotInPredelegation(Registry registry, DateTime now)
+ public static void verifyNotInPredelegation(Registry registry, DateTime now)
throws BadCommandForRegistryPhaseException {
if (registry.getTldState(now) == TldState.PREDELEGATION) {
throw new BadCommandForRegistryPhaseException();
diff --git a/java/google/registry/module/frontend/FrontendRequestComponent.java b/java/google/registry/module/frontend/FrontendRequestComponent.java
index 118343789..fe86a0ece 100644
--- a/java/google/registry/module/frontend/FrontendRequestComponent.java
+++ b/java/google/registry/module/frontend/FrontendRequestComponent.java
@@ -17,6 +17,8 @@ package google.registry.module.frontend;
import dagger.Module;
import dagger.Subcomponent;
import google.registry.dns.DnsModule;
+import google.registry.flows.CheckApi2Action;
+import google.registry.flows.CheckApi2Action.CheckApi2Module;
import google.registry.flows.CheckApiAction;
import google.registry.flows.CheckApiAction.CheckApiModule;
import google.registry.flows.EppConsoleAction;
@@ -49,16 +51,18 @@ import google.registry.whois.WhoisModule;
@RequestScope
@Subcomponent(
modules = {
- CheckApiModule.class,
- DnsModule.class,
- EppTlsModule.class,
- RdapModule.class,
- RequestModule.class,
- WhiteboxModule.class,
- WhoisModule.class,
+ CheckApiModule.class,
+ CheckApi2Module.class,
+ DnsModule.class,
+ EppTlsModule.class,
+ RdapModule.class,
+ RequestModule.class,
+ WhiteboxModule.class,
+ WhoisModule.class,
})
interface FrontendRequestComponent {
CheckApiAction checkApiAction();
+ CheckApi2Action checkApi2Action();
ConsoleUiAction consoleUiAction();
EppConsoleAction eppConsoleAction();
EppTlsAction eppTlsAction();
diff --git a/java/google/registry/module/pubapi/PubApiRequestComponent.java b/java/google/registry/module/pubapi/PubApiRequestComponent.java
index 2e3612517..95dd56826 100644
--- a/java/google/registry/module/pubapi/PubApiRequestComponent.java
+++ b/java/google/registry/module/pubapi/PubApiRequestComponent.java
@@ -17,6 +17,8 @@ package google.registry.module.pubapi;
import dagger.Module;
import dagger.Subcomponent;
import google.registry.dns.DnsModule;
+import google.registry.flows.CheckApi2Action;
+import google.registry.flows.CheckApi2Action.CheckApi2Module;
import google.registry.flows.CheckApiAction;
import google.registry.flows.CheckApiAction.CheckApiModule;
import google.registry.flows.FlowComponent;
@@ -43,16 +45,18 @@ import google.registry.whois.WhoisModule;
@RequestScope
@Subcomponent(
modules = {
- CheckApiModule.class,
- DnsModule.class,
- EppTlsModule.class,
- RdapModule.class,
- RequestModule.class,
- WhiteboxModule.class,
- WhoisModule.class,
+ CheckApiModule.class,
+ CheckApi2Module.class,
+ DnsModule.class,
+ EppTlsModule.class,
+ RdapModule.class,
+ RequestModule.class,
+ WhiteboxModule.class,
+ WhoisModule.class,
})
interface PubApiRequestComponent {
CheckApiAction checkApiAction();
+ CheckApi2Action checkApi2Action();
// TODO(b/79692981): Remove flow-related includes once check API is rewritten to not wrap flow.
FlowComponent.Builder flowComponentBuilder();
RdapAutnumAction rdapAutnumAction();
diff --git a/javatests/google/registry/flows/CheckApi2ActionTest.java b/javatests/google/registry/flows/CheckApi2ActionTest.java
new file mode 100644
index 000000000..ce24d9c0a
--- /dev/null
+++ b/javatests/google/registry/flows/CheckApi2ActionTest.java
@@ -0,0 +1,182 @@
+// 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.flows;
+
+import static com.google.common.truth.Truth.assertThat;
+import static google.registry.testing.DatastoreHelper.createTld;
+import static google.registry.testing.DatastoreHelper.persistActiveDomain;
+import static google.registry.testing.DatastoreHelper.persistReservedList;
+import static google.registry.testing.DatastoreHelper.persistResource;
+
+import google.registry.model.registry.Registry;
+import google.registry.model.registry.Registry.TldState;
+import google.registry.testing.AppEngineRule;
+import google.registry.testing.FakeClock;
+import google.registry.testing.FakeResponse;
+import java.util.Map;
+import org.json.simple.JSONValue;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Tests for {@link CheckApi2Action}. */
+@RunWith(JUnit4.class)
+public class CheckApi2ActionTest {
+
+ @Rule public final AppEngineRule appEngine = AppEngineRule.builder().withDatastore().build();
+
+ @Before
+ public void init() throws Exception {
+ createTld("example");
+ persistResource(
+ Registry.get("example")
+ .asBuilder()
+ .setReservedLists(persistReservedList("example-reserved", "foo,FULLY_BLOCKED"))
+ .build());
+ }
+
+ @SuppressWarnings("unchecked")
+ private Map getCheckResponse(String domain) {
+ CheckApi2Action action = new CheckApi2Action();
+ action.domain = domain;
+ action.response = new FakeResponse();
+ action.clock = new FakeClock();
+ action.run();
+ return (Map) JSONValue.parse(((FakeResponse) action.response).getPayload());
+ }
+
+ @Test
+ public void testFailure_nullDomain() throws Exception {
+ assertThat(getCheckResponse(null))
+ .containsExactly(
+ "status", "error",
+ "reason", "Must supply a valid domain name on an authoritative TLD");
+ }
+
+ @Test
+ public void testFailure_emptyDomain() throws Exception {
+ assertThat(getCheckResponse(""))
+ .containsExactly(
+ "status", "error",
+ "reason", "Must supply a valid domain name on an authoritative TLD");
+ }
+
+ @Test
+ public void testFailure_invalidDomain() throws Exception {
+ assertThat(getCheckResponse("@#$%^"))
+ .containsExactly(
+ "status", "error",
+ "reason", "Must supply a valid domain name on an authoritative TLD");
+ }
+
+ @Test
+ public void testFailure_singlePartDomain() throws Exception {
+ assertThat(getCheckResponse("foo"))
+ .containsExactly(
+ "status", "error",
+ "reason", "Must supply a valid domain name on an authoritative TLD");
+ }
+
+ @Test
+ public void testFailure_nonExistentTld() throws Exception {
+ assertThat(getCheckResponse("foo.bar"))
+ .containsExactly(
+ "status", "error",
+ "reason", "Must supply a valid domain name on an authoritative TLD");
+ }
+
+ @Test
+ public void testFailure_invalidIdnTable() throws Exception {
+ assertThat(getCheckResponse("ΑΒΓ.example"))
+ .containsExactly(
+ "status", "error",
+ "reason", "Domain label is not allowed by IDN table");
+ }
+
+ @Test
+ public void testFailure_tldInPredelegation() throws Exception {
+ createTld("predelegated", TldState.PREDELEGATION);
+ assertThat(getCheckResponse("foo.predelegated"))
+ .containsExactly(
+ "status", "error",
+ "reason", "Check in this TLD is not allowed in the current registry phase");
+ }
+
+ @Test
+ public void testSuccess_availableStandard() throws Exception {
+ assertThat(getCheckResponse("somedomain.example"))
+ .containsExactly(
+ "status", "success",
+ "available", true,
+ "tier", "standard");
+ }
+
+ @Test
+ public void testSuccess_availableCapital() throws Exception {
+ assertThat(getCheckResponse("SOMEDOMAIN.EXAMPLE"))
+ .containsExactly(
+ "status", "success",
+ "available", true,
+ "tier", "standard");
+ }
+
+ @Test
+ public void testSuccess_availableUnicode() throws Exception {
+ assertThat(getCheckResponse("ééé.example"))
+ .containsExactly(
+ "status", "success",
+ "available", true,
+ "tier", "standard");
+ }
+
+ @Test
+ public void testSuccess_availablePunycode() throws Exception {
+ assertThat(getCheckResponse("xn--9caaa.example"))
+ .containsExactly(
+ "status", "success",
+ "available", true,
+ "tier", "standard");
+ }
+
+ @Test
+ public void testSuccess_availablePremium() throws Exception {
+ assertThat(getCheckResponse("rich.example"))
+ .containsExactly(
+ "status", "success",
+ "available", true,
+ "tier", "premium");
+ }
+
+ @Test
+ public void testSuccess_alreadyRegistered() throws Exception {
+ persistActiveDomain("somedomain.example");
+ assertThat(getCheckResponse("somedomain.example"))
+ .containsExactly(
+ "status", "success",
+ "available", false,
+ "reason", "In use");
+ }
+
+ @Test
+ public void testSuccess_reserved() throws Exception {
+ assertThat(getCheckResponse("foo.example"))
+ .containsExactly(
+ "status", "success",
+ "available", false,
+ "reason", "Reserved");
+ }
+}
diff --git a/javatests/google/registry/module/frontend/testdata/frontend_routing.txt b/javatests/google/registry/module/frontend/testdata/frontend_routing.txt
index 1b771c337..9cb3fa248 100644
--- a/javatests/google/registry/module/frontend/testdata/frontend_routing.txt
+++ b/javatests/google/registry/module/frontend/testdata/frontend_routing.txt
@@ -2,6 +2,7 @@ PATH CLASS METHODS OK AUTH_METHODS
/_dr/epp EppTlsAction POST n INTERNAL,API APP PUBLIC
/_dr/whois WhoisAction POST n INTERNAL,API APP PUBLIC
/check CheckApiAction GET n INTERNAL NONE PUBLIC
+/check2 CheckApi2Action GET n INTERNAL NONE PUBLIC
/rdap/autnum/(*) RdapAutnumAction GET,HEAD n INTERNAL NONE PUBLIC
/rdap/domain/(*) RdapDomainAction GET,HEAD n INTERNAL,API,LEGACY NONE PUBLIC
/rdap/domains RdapDomainSearchAction GET,HEAD n INTERNAL,API,LEGACY NONE PUBLIC
diff --git a/javatests/google/registry/module/pubapi/testdata/pubapi_routing.txt b/javatests/google/registry/module/pubapi/testdata/pubapi_routing.txt
index 089d67c16..89cf2d5b0 100644
--- a/javatests/google/registry/module/pubapi/testdata/pubapi_routing.txt
+++ b/javatests/google/registry/module/pubapi/testdata/pubapi_routing.txt
@@ -1,6 +1,7 @@
PATH CLASS METHODS OK AUTH_METHODS MIN USER_POLICY
/_dr/whois WhoisAction POST n INTERNAL,API APP PUBLIC
/check CheckApiAction GET n INTERNAL NONE PUBLIC
+/check2 CheckApi2Action GET n INTERNAL NONE PUBLIC
/rdap/autnum/(*) RdapAutnumAction GET,HEAD n INTERNAL NONE PUBLIC
/rdap/domain/(*) RdapDomainAction GET,HEAD n INTERNAL,API,LEGACY NONE PUBLIC
/rdap/domains RdapDomainSearchAction GET,HEAD n INTERNAL,API,LEGACY NONE PUBLIC