diff --git a/java/google/registry/env/common/default/WEB-INF/web.xml b/java/google/registry/env/common/default/WEB-INF/web.xml index 395f7874b..de84e0888 100644 --- a/java/google/registry/env/common/default/WEB-INF/web.xml +++ b/java/google/registry/env/common/default/WEB-INF/web.xml @@ -4,32 +4,24 @@ xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"> + + - - This is the primary EPP endpoint for the Registry. It accepts - EPP XHRs from our TLS proxy. - - EPP - epp - google.registry.flows.EppTlsServlet + FrontendServlet + frontend-servlet + google.registry.module.frontend.FrontendServlet 1 + + - epp + frontend-servlet /_dr/epp - - - Registrar Console XHR servlet. Accepts EPP XHRs from GAE GAIA-authenticated frontend sessions. - - Registrar Console XHR - registrar-xhr - google.registry.flows.EppConsoleServlet - 1 - + - registrar-xhr + frontend-servlet /registrar-xhr @@ -44,14 +36,6 @@ /registrar-settings - - - FrontendServlet - frontend-servlet - google.registry.module.frontend.FrontendServlet - 1 - - frontend-servlet @@ -89,17 +73,8 @@ - - - Availability Check API. - - Availability Check - check - google.registry.ui.server.api.CheckApiServlet - 1 - - check + frontend-servlet /check diff --git a/java/google/registry/env/common/tools/WEB-INF/web.xml b/java/google/registry/env/common/tools/WEB-INF/web.xml index a69522423..15be36f89 100644 --- a/java/google/registry/env/common/tools/WEB-INF/web.xml +++ b/java/google/registry/env/common/tools/WEB-INF/web.xml @@ -61,17 +61,8 @@ - - - Execute epp from the registry tool. - - Registry tool EPP endpoint - epptool - google.registry.flows.EppToolServlet - 1 - - epptool + tools-servlet /_dr/epptool diff --git a/java/google/registry/flows/EppConsoleAction.java b/java/google/registry/flows/EppConsoleAction.java new file mode 100644 index 000000000..3411892a3 --- /dev/null +++ b/java/google/registry/flows/EppConsoleAction.java @@ -0,0 +1,47 @@ +// Copyright 2016 The Domain Registry 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.appengine.api.users.UserServiceFactory.getUserService; + +import google.registry.request.Action; +import google.registry.request.Action.Method; +import google.registry.request.Payload; + +import javax.inject.Inject; +import javax.servlet.http.HttpSession; + +/** Runs EPP from the console and requires GAE user authentication. */ +@Action( + path = "/registrar-xhr", + xsrfProtection = true, + xsrfScope = EppConsoleAction.XSRF_SCOPE, + method = Method.POST) +public class EppConsoleAction implements Runnable { + + public static final String XSRF_SCOPE = "console"; + + @Inject @Payload byte[] inputXmlBytes; + @Inject HttpSession session; + @Inject EppRequestHandler eppRequestHandler; + @Inject EppConsoleAction() {} + + @Override + public void run() { + eppRequestHandler.executeEpp( + new HttpSessionMetadata(new GaeUserCredentials(getUserService().getCurrentUser()), session), + inputXmlBytes); + } +} diff --git a/java/google/registry/flows/EppConsoleServlet.java b/java/google/registry/flows/EppConsoleServlet.java deleted file mode 100644 index 7956e3bfe..000000000 --- a/java/google/registry/flows/EppConsoleServlet.java +++ /dev/null @@ -1,120 +0,0 @@ -// Copyright 2016 The Domain Registry 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.appengine.api.users.UserServiceFactory.getUserService; -import static com.google.common.base.Strings.nullToEmpty; -import static google.registry.flows.EppServletUtils.handleEppCommandAndWriteResponse; -import static java.lang.System.identityHashCode; - -import com.google.appengine.api.users.User; -import com.google.common.annotations.VisibleForTesting; -import com.google.common.io.ByteStreams; - -import google.registry.flows.EppException.AuthenticationErrorException; -import google.registry.model.registrar.Registrar; -import google.registry.model.registrar.RegistrarContact; -import google.registry.security.XsrfProtectedServlet; - -import org.joda.time.Duration; - -import java.io.IOException; - -import javax.annotation.Nullable; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; - -/** The {@link EppConsoleServlet} runs EPP from the console. It requires GAE user authentication. */ -public class EppConsoleServlet extends XsrfProtectedServlet { - - /** - * Credentials provided by the GAE User service. - * - * @see com.google.appengine.api.users.UserService - */ - public static final class GaeUserCredentials implements TransportCredentials { - - /** User is not logged in as a GAE user. */ - public static class UserNotLoggedInException extends AuthenticationErrorException { - public UserNotLoggedInException() { - super("User is not logged in"); - } - } - - /** GAE user id is not allowed to login as requested registrar. */ - public static class BadGaeUserIdException extends AuthenticationErrorException { - public BadGaeUserIdException(User user) { - super( - "User id is not allowed to login as requested registrar: " - + (nullToEmpty(user.getEmail()))); - } - } - - final User gaeUser; - - @VisibleForTesting - public GaeUserCredentials(@Nullable User gaeUser) { - this.gaeUser = gaeUser; - } - - @Override - public boolean performsLoginCheck() { - return true; - } - - @Override - public void validate(Registrar r) throws AuthenticationErrorException { - if (gaeUser == null) { - throw new UserNotLoggedInException(); - } - // Allow admins to act as any registrar. - if (getUserService().isUserAdmin()) { - return; - } - // Check Registrar's contacts to see if any are associated with this gaeUserId. - final String gaeUserId = gaeUser.getUserId(); - for (RegistrarContact rc : r.getContacts()) { - if (gaeUserId.equals(rc.getGaeUserId())) { - return; - } - } - throw new BadGaeUserIdException(gaeUser); - } - - @Override - public String toString() { - return String.format("GaeUserCredentials@%s{gaeUser: %s}", identityHashCode(this), gaeUser); - } - } - - /** Used by related UI servlets to generate matching XSRF tokens. */ - public static final String XSRF_SCOPE = "console"; - - /** How long generated XSRF tokens for this scope remain valid. */ - public static final Duration XSRF_LIFETIME = Duration.standardDays(1); - - public EppConsoleServlet() { - super(XSRF_SCOPE, false); - } - - @Override - protected void doPost(HttpServletRequest req, HttpServletResponse rsp) throws IOException { - handleEppCommandAndWriteResponse( - ByteStreams.toByteArray(req.getInputStream()), - rsp, - new HttpSessionMetadata( - new GaeUserCredentials(getUserService().getCurrentUser()), req.getSession(true))); - } -} diff --git a/java/google/registry/flows/EppController.java b/java/google/registry/flows/EppController.java index 0d840932e..f5b7ef2c0 100644 --- a/java/google/registry/flows/EppController.java +++ b/java/google/registry/flows/EppController.java @@ -18,7 +18,6 @@ import static google.registry.flows.EppXmlTransformer.marshalWithLenientRetry; import static google.registry.flows.EppXmlTransformer.unmarshal; import static google.registry.flows.picker.FlowPicker.getFlowClass; -import com.google.apphosting.api.ApiProxy; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Joiner; import com.google.common.collect.ImmutableList; @@ -34,11 +33,11 @@ import google.registry.model.eppoutput.Result.Code; import google.registry.monitoring.whitebox.EppMetrics; import google.registry.util.Clock; import google.registry.util.FormattingLogger; -import google.registry.util.SystemClock; + +import javax.inject.Inject; /** - * The EppController class, which implements the state machine for the EPP command/response - * protocol. + * An implementation of the EPP command/response protocol. * * @see "http://tools.ietf.org/html/rfc5730" */ @@ -46,18 +45,16 @@ public final class EppController { private static final FormattingLogger logger = FormattingLogger.getLoggerForCallerClass(); - private static final Clock clock = new SystemClock(); + @Inject Clock clock; + @Inject EppMetrics metrics; + @Inject EppController() {} /** * Read an EPP envelope from the client, find the matching flow, execute it, and return * the response marshalled to a byte array. */ - public static byte[] handleEppCommand(byte[] inputXmlBytes, SessionMetadata sessionMetadata) { + public byte[] handleEppCommand(SessionMetadata sessionMetadata, byte[] inputXmlBytes) { Trid trid = null; - EppMetrics metrics = new EppMetrics(); - metrics.setRequestId( - ApiProxy.getCurrentEnvironment().getAttributes().get( - "com.google.appengine.runtime.request_log_id").toString()); try { EppInput eppInput = unmarshal(inputXmlBytes); trid = Trid.create(eppInput.getCommandWrapper().getClTrid()); @@ -71,14 +68,14 @@ public final class EppController { if (!targetIds.isEmpty()) { metrics.setEppTarget(Joiner.on(",").join(targetIds)); } - FlowRunner flowRunner = new FlowRunner( getFlowClass(eppInput), eppInput, trid, sessionMetadata, inputXmlBytes, - metrics); + metrics, + clock); EppOutput eppOutput = flowRunner.run( sessionMetadata.isDryRun() ? CommitMode.DRY_RUN : CommitMode.LIVE, sessionMetadata.isSuperuser() ? UserPrivileges.SUPERUSER : UserPrivileges.NORMAL); @@ -89,12 +86,13 @@ public final class EppController { } catch (EppException e) { // The command failed. Send the client an error message. metrics.setEppStatus(e.getResult().getCode()); - return marshalWithLenientRetry(getErrorResponse(e.getResult(), trid)); + return marshalWithLenientRetry(getErrorResponse(clock, e.getResult(), trid)); } catch (Throwable e) { // Something bad and unexpected happened. Send the client a generic error, and log it. logger.severe(e, "Unexpected failure"); metrics.setEppStatus(Code.CommandFailed); - return marshalWithLenientRetry(getErrorResponse(Result.create(Code.CommandFailed), trid)); + return marshalWithLenientRetry( + getErrorResponse(clock, Result.create(Code.CommandFailed), trid)); } finally { metrics.export(); } @@ -102,7 +100,7 @@ public final class EppController { /** Create a response indicating an Epp failure. */ @VisibleForTesting - static EppOutput getErrorResponse(Result result, Trid trid) { + static EppOutput getErrorResponse(Clock clock, Result result, Trid trid) { // Create TRID (without a clTRID) if one hasn't been created yet, as it's necessary to construct // a valid response. This can happen if the error occurred before we could even parse out the // clTRID (e.g. if a syntax error occurred parsing the supplied XML). diff --git a/java/google/registry/flows/EppRequestHandler.java b/java/google/registry/flows/EppRequestHandler.java new file mode 100644 index 000000000..95ea8bf71 --- /dev/null +++ b/java/google/registry/flows/EppRequestHandler.java @@ -0,0 +1,58 @@ +// Copyright 2016 The Domain Registry 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 java.nio.charset.StandardCharsets.UTF_8; +import static javax.servlet.http.HttpServletResponse.SC_BAD_REQUEST; +import static javax.servlet.http.HttpServletResponse.SC_OK; + +import com.google.common.net.MediaType; + +import google.registry.request.Response; +import google.registry.util.FormattingLogger; + +import javax.inject.Inject; + +/** Handle an EPP request and response. */ +public class EppRequestHandler { + + private static final MediaType APPLICATION_EPP_XML = + MediaType.create("application", "epp+xml").withCharset(UTF_8); + + private static final FormattingLogger logger = FormattingLogger.getLoggerForCallerClass(); + + @Inject EppController eppController; + @Inject Response response; + @Inject EppRequestHandler() {} + + /** Handle an EPP request and write out a servlet response. */ + public void executeEpp(SessionMetadata sessionMetadata, byte[] inputXmlBytes) { + try { + response.setPayload(new String( + eppController.handleEppCommand(sessionMetadata, inputXmlBytes), UTF_8)); + response.setContentType(APPLICATION_EPP_XML); + // Note that we always return 200 (OK) even if the EppController returns an error response. + // This is because returning an non-OK HTTP status code will cause the proxy server to + // silently close the connection without returning any data. The only time we will ever return + // a non-OK status (400) is if we fail to muster even an EPP error response message. In that + // case it's better to close the connection than to return garbage. + response.setStatus(SC_OK); + } catch (Exception e) { + logger.warning(e, "handleEppCommand general exception"); + response.setStatus(SC_BAD_REQUEST); + } + } +} diff --git a/java/google/registry/flows/EppServletUtils.java b/java/google/registry/flows/EppServletUtils.java deleted file mode 100644 index 05a1a7029..000000000 --- a/java/google/registry/flows/EppServletUtils.java +++ /dev/null @@ -1,68 +0,0 @@ -// Copyright 2016 The Domain Registry 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 google.registry.flows.EppController.handleEppCommand; -import static java.nio.charset.StandardCharsets.UTF_8; -import static javax.servlet.http.HttpServletResponse.SC_BAD_REQUEST; -import static javax.servlet.http.HttpServletResponse.SC_OK; - -import com.google.common.net.MediaType; - -import google.registry.util.FormattingLogger; - -import java.io.IOException; -import java.io.OutputStream; - -import javax.servlet.http.HttpServletResponse; - -/** Utility methods for Epp servlet classes. */ -public final class EppServletUtils { - - public static final MediaType APPLICATION_EPP_XML_UTF8 = - MediaType.create("application", "epp+xml").withCharset(UTF_8); - - private static final FormattingLogger logger = FormattingLogger.getLoggerForCallerClass(); - - /** - * Handle an EPP request and write out a servlet response. - * - * @throws IOException upon failure writing to {@code rsp} - */ - static void handleEppCommandAndWriteResponse( - byte[] inputXmlBytes, HttpServletResponse rsp, SessionMetadata sessionMetadata) - throws IOException { - byte[] response; - try { - response = handleEppCommand(inputXmlBytes, sessionMetadata); - } catch (Exception e) { - logger.warning(e, "handleEppCommand general exception"); - rsp.setStatus(SC_BAD_REQUEST); - return; - } - // Note that we always return 200 (OK) even if the EppController returns an error response. - // This is because returning an non-OK HTTP status code will cause the proxy server to - // silently close the connection without returning any data. The only time we will ever return - // a non-OK status (400) is if we fail to muster even an EPP error response message. In that - // case it's better to close the connection than to return garbage. - rsp.setStatus(SC_OK); - rsp.setContentType(APPLICATION_EPP_XML_UTF8.toString()); - try (OutputStream output = rsp.getOutputStream()) { - output.write(response); - } - } - - private EppServletUtils() {} -} diff --git a/java/google/registry/flows/EppTlsAction.java b/java/google/registry/flows/EppTlsAction.java new file mode 100644 index 000000000..233f66257 --- /dev/null +++ b/java/google/registry/flows/EppTlsAction.java @@ -0,0 +1,52 @@ +// Copyright 2016 The Domain Registry 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 google.registry.request.Action; +import google.registry.request.Action.Method; +import google.registry.request.Payload; +import google.registry.util.FormattingLogger; + +import javax.inject.Inject; +import javax.servlet.http.HttpSession; + +/** + * Establishes a transport for EPP+TLS over HTTP. All commands and responses are EPP XML according + * to RFC 5730. Commands must be requested via POST. + */ +@Action( + path = "/_dr/epp", + method = Method.POST) +public class EppTlsAction implements Runnable { + + private static final FormattingLogger logger = FormattingLogger.getLoggerForCallerClass(); + + @Inject @Payload byte[] inputXmlBytes; + @Inject TlsCredentials tlsCredentials; + @Inject HttpSession session; + @Inject EppRequestHandler eppRequestHandler; + @Inject EppTlsAction() {} + + @Override + public void run() { + // Check that SNI header is present. This is a signal that we're receiving traffic proxied by a + // GFE, which is the expectation of this servlet. The value is unused. + if (!tlsCredentials.hasSni()) { + logger.warning("Request did not include required SNI header."); + } + eppRequestHandler.executeEpp(new HttpSessionMetadata(tlsCredentials, session), inputXmlBytes); + } +} + diff --git a/java/google/registry/flows/EppTlsServlet.java b/java/google/registry/flows/EppTlsServlet.java deleted file mode 100644 index 565eea836..000000000 --- a/java/google/registry/flows/EppTlsServlet.java +++ /dev/null @@ -1,72 +0,0 @@ -// Copyright 2016 The Domain Registry 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.io.ByteStreams.toByteArray; -import static google.registry.flows.EppServletUtils.handleEppCommandAndWriteResponse; - -import google.registry.util.FormattingLogger; - -import java.io.IOException; - -import javax.servlet.http.HttpServlet; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; - -/** - * The {@link EppTlsServlet} class establishes a transport for EPP+TLS over* HTTP. All commands and - * responses are EPP XML according to RFC 5730. Commands must must requested via POST. - * - *

There are a number of expected headers to this endpoint: - *

- *
{@value #SSL_CLIENT_CERTIFICATE_HASH_FIELD} - *
- * This field should contain a base64 encoded digest of the client's TLS certificate. It is - * validated during an EPP login command against a known good value that is transmitted out of - * band. - *
{@value #FORWARDED_FOR_FIELD} - *
- * This field should contain the host and port of the connecting client. It is validated during - * an EPP login command against an IP whitelist that is transmitted out of band. - *
{@value #REQUESTED_SERVERNAME_VIA_SNI_FIELD} - *
- * This field should contain the servername that the client requested during the TLS handshake. - * It is unused, but expected to be present in the GFE-proxied configuration. - *
- */ -public class EppTlsServlet extends HttpServlet { - - private static final FormattingLogger logger = FormattingLogger.getLoggerForCallerClass(); - - static final String REQUESTED_SERVERNAME_VIA_SNI_FIELD = "X-GFE-Requested-Servername-SNI"; - static final String FORWARDED_FOR_FIELD = "X-Forwarded-For"; - static final String SSL_CLIENT_CERTIFICATE_HASH_FIELD = "X-GFE-SSL-Certificate"; - - @Override - public void doPost(HttpServletRequest req, HttpServletResponse rsp) throws IOException { - // Check that SNI header is present. This is a signal that we're receiving traffic proxied by a - // GFE, which is the expectation of this servlet. The value is unused. - TlsCredentials tlsCredentials = new TlsCredentials(req); - if (!tlsCredentials.hasSni()) { - logger.warning("Request did not include required SNI header."); - } - SessionMetadata sessionMetadata = new HttpSessionMetadata(tlsCredentials, req.getSession(true)); - // Note that we are using the raw input stream rather than the reader, which implies that we are - // ignoring the HTTP-specified charset (if any) in favor of whatever charset the XML declares. - // This is ok because this code is only called from the proxy, which can't specify a charset - // (it blindly copies bytes off a socket). - handleEppCommandAndWriteResponse(toByteArray(req.getInputStream()), rsp, sessionMetadata); - } -} diff --git a/java/google/registry/flows/EppToolAction.java b/java/google/registry/flows/EppToolAction.java new file mode 100644 index 000000000..52fc4a701 --- /dev/null +++ b/java/google/registry/flows/EppToolAction.java @@ -0,0 +1,85 @@ +// Copyright 2016 The Domain Registry 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 google.registry.request.RequestParameters.extractBooleanParameter; +import static google.registry.request.RequestParameters.extractRequiredParameter; +import static java.nio.charset.StandardCharsets.UTF_8; + +import dagger.Module; +import dagger.Provides; + +import google.registry.flows.SessionMetadata.SessionSource; +import google.registry.model.eppcommon.ProtocolDefinition; +import google.registry.request.Action; +import google.registry.request.Action.Method; +import google.registry.request.Parameter; + +import javax.inject.Inject; +import javax.servlet.http.HttpServletRequest; + +/** Runs EPP commands directly without logging in, verifying an XSRF token from the tool. */ +@Action( + path = "/_dr/epptool", + xsrfProtection = true, + xsrfScope = "admin", + method = Method.POST) +public class EppToolAction implements Runnable { + + @Inject @Parameter("clientIdentifier") String clientIdentifier; + @Inject @Parameter("superuser") boolean superuser; + @Inject @Parameter("dryRun") boolean dryRun; + @Inject @Parameter("xml") String xml; + @Inject EppRequestHandler eppRequestHandler; + @Inject EppToolAction() {} + + @Override + public void run() { + eppRequestHandler.executeEpp( + new StatelessRequestSessionMetadata( + clientIdentifier, + superuser, + dryRun, + ProtocolDefinition.getVisibleServiceExtensionUris(), + SessionSource.TOOL), + xml.getBytes(UTF_8)); + } + + /** Dagger module for the epp tool endpoint. */ + @Module + public static final class EppToolModule { + + // TODO(b/29139545): Make parameters consistent across the graph. @Parameter("dryRun") is + // already provided elsewhere in the graph and happens to work for us but that's just luck. + + @Provides + @Parameter("xml") + static String provideXml(HttpServletRequest req) { + return extractRequiredParameter(req, "xml"); + } + + @Provides + @Parameter("superuser") + static boolean provideIsSuperuser(HttpServletRequest req) { + return extractBooleanParameter(req, "superuser"); + } + + @Provides + @Parameter("clientIdentifier") + static String provideClientIdentifier(HttpServletRequest req) { + return extractRequiredParameter(req, "clientIdentifier"); + } + } +} diff --git a/java/google/registry/flows/EppToolServlet.java b/java/google/registry/flows/EppToolServlet.java deleted file mode 100644 index 9acbe1894..000000000 --- a/java/google/registry/flows/EppToolServlet.java +++ /dev/null @@ -1,52 +0,0 @@ -// Copyright 2016 The Domain Registry 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 google.registry.flows.EppServletUtils.handleEppCommandAndWriteResponse; -import static java.nio.charset.StandardCharsets.UTF_8; - -import google.registry.flows.SessionMetadata.SessionSource; -import google.registry.model.eppcommon.ProtocolDefinition; -import google.registry.security.XsrfProtectedServlet; - -import java.io.IOException; - -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; - -/** - * This servlet runs EPP commands directly without logging in. It verifies an XSRF token that could - * only come from the tool. - */ -public class EppToolServlet extends XsrfProtectedServlet { - - /** Used to verify XSRF tokens. */ - public static final String XSRF_SCOPE = "admin"; - - public EppToolServlet() { - super(XSRF_SCOPE, true); - } - - @Override - protected void doPost(HttpServletRequest req, HttpServletResponse rsp) throws IOException { - handleEppCommandAndWriteResponse( - req.getParameter("xml").getBytes(UTF_8), rsp, new StatelessRequestSessionMetadata( - req.getParameter("clientIdentifier"), - Boolean.parseBoolean(req.getParameter("superuser")), - Boolean.parseBoolean(req.getParameter("dryRun")), - ProtocolDefinition.getVisibleServiceExtensionUris(), - SessionSource.TOOL)); - } -} diff --git a/java/google/registry/flows/FlowRunner.java b/java/google/registry/flows/FlowRunner.java index 0bc44fe86..10254cb9f 100644 --- a/java/google/registry/flows/FlowRunner.java +++ b/java/google/registry/flows/FlowRunner.java @@ -29,8 +29,6 @@ import google.registry.model.eppoutput.EppOutput; import google.registry.monitoring.whitebox.EppMetrics; import google.registry.util.Clock; import google.registry.util.FormattingLogger; -import google.registry.util.NonFinalForTesting; -import google.registry.util.SystemClock; import google.registry.util.TypeUtils; import org.joda.time.DateTime; @@ -48,15 +46,13 @@ public class FlowRunner { private static final FormattingLogger logger = FormattingLogger.getLoggerForCallerClass(); - @NonFinalForTesting - private static Clock clock = new SystemClock(); - private final Class flowClass; private final EppInput eppInput; private final Trid trid; private final SessionMetadata sessionMetadata; private final byte[] inputXmlBytes; private final EppMetrics metrics; + private final Clock clock; public FlowRunner( Class flowClass, @@ -64,13 +60,15 @@ public class FlowRunner { Trid trid, SessionMetadata sessionMetadata, byte[] inputXmlBytes, - final EppMetrics metrics) { + final EppMetrics metrics, + Clock clock) { this.flowClass = flowClass; this.eppInput = eppInput; this.trid = trid; this.sessionMetadata = sessionMetadata; this.inputXmlBytes = inputXmlBytes; this.metrics = metrics; + this.clock = clock; } public EppOutput run( diff --git a/java/google/registry/flows/GaeUserCredentials.java b/java/google/registry/flows/GaeUserCredentials.java new file mode 100644 index 000000000..1bc25191d --- /dev/null +++ b/java/google/registry/flows/GaeUserCredentials.java @@ -0,0 +1,84 @@ +// Copyright 2016 The Domain Registry 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.appengine.api.users.UserServiceFactory.getUserService; +import static com.google.common.base.Strings.nullToEmpty; +import static java.lang.System.identityHashCode; + +import com.google.appengine.api.users.User; +import com.google.common.annotations.VisibleForTesting; + +import google.registry.flows.EppException.AuthenticationErrorException; +import google.registry.model.registrar.Registrar; +import google.registry.model.registrar.RegistrarContact; + +import javax.annotation.Nullable; + +/** Credentials provided by {@link com.google.appengine.api.users.UserService}. */ +public class GaeUserCredentials implements TransportCredentials { + + final User gaeUser; + + @VisibleForTesting + public GaeUserCredentials(@Nullable User gaeUser) { + this.gaeUser = gaeUser; + } + + @Override + public boolean performsLoginCheck() { + return true; + } + + @Override + public void validate(Registrar r) throws AuthenticationErrorException { + if (gaeUser == null) { + throw new UserNotLoggedInException(); + } + // Allow admins to act as any registrar. + if (getUserService().isUserAdmin()) { + return; + } + // Check Registrar's contacts to see if any are associated with this gaeUserId. + final String gaeUserId = gaeUser.getUserId(); + for (RegistrarContact rc : r.getContacts()) { + if (gaeUserId.equals(rc.getGaeUserId())) { + return; + } + } + throw new BadGaeUserIdException(gaeUser); + } + + @Override + public String toString() { + return String.format("GaeUserCredentials@%s{gaeUser: %s}", identityHashCode(this), gaeUser); + } + + /** User is not logged in as a GAE user. */ + public static class UserNotLoggedInException extends AuthenticationErrorException { + public UserNotLoggedInException() { + super("User is not logged in"); + } + } + + /** GAE user id is not allowed to login as requested registrar. */ + public static class BadGaeUserIdException extends AuthenticationErrorException { + public BadGaeUserIdException(User user) { + super( + "User id is not allowed to login as requested registrar: " + + (nullToEmpty(user.getEmail()))); + } + } +} diff --git a/java/google/registry/flows/TlsCredentials.java b/java/google/registry/flows/TlsCredentials.java index be31889b3..0f5d4e3ba 100644 --- a/java/google/registry/flows/TlsCredentials.java +++ b/java/google/registry/flows/TlsCredentials.java @@ -16,77 +16,68 @@ package google.registry.flows; import static com.google.common.base.MoreObjects.toStringHelper; import static com.google.common.base.Strings.isNullOrEmpty; +import static google.registry.request.RequestParameters.extractOptionalHeader; +import static google.registry.request.RequestParameters.extractRequiredHeader; import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Optional; import com.google.common.collect.ImmutableList; import com.google.common.net.HostAndPort; import com.google.common.net.InetAddresses; +import dagger.Module; +import dagger.Provides; + import google.registry.flows.EppException.AuthenticationErrorException; import google.registry.model.registrar.Registrar; +import google.registry.request.Header; import google.registry.util.CidrAddressBlock; import google.registry.util.FormattingLogger; import java.net.InetAddress; +import javax.inject.Inject; import javax.servlet.http.HttpServletRequest; /** * Container and validation for TLS certificate and ip-whitelisting. + * + *

Credentials are based on the following headers: + *

+ *
X-GFE-Requested-Servername-SNI + *
+ * This field should contain a base64 encoded digest of the client's TLS certificate. It is + * validated during an EPP login command against a known good value that is transmitted out of + * band. + *
X-Forwarded-For + *
+ * This field should contain the host and port of the connecting client. It is validated during + * an EPP login command against an IP whitelist that is transmitted out of band. + *
X-GFE-Requested-Servername-SNI + *
+ * This field should contain the servername that the client requested during the TLS handshake. + * It is unused, but expected to be present in the GFE-proxied configuration. + *
*/ -public final class TlsCredentials implements TransportCredentials { - - /** Registrar certificate does not match stored certificate. */ - public static class BadRegistrarCertificateException extends AuthenticationErrorException { - public BadRegistrarCertificateException() { - super("Registrar certificate does not match stored certificate"); - } - } - - /** Registrar certificate not present. */ - public static class MissingRegistrarCertificateException extends AuthenticationErrorException { - public MissingRegistrarCertificateException() { - super("Registrar certificate not present"); - } - } - - /** SNI header is required. */ - public static class NoSniException extends AuthenticationErrorException { - public NoSniException() { - super("SNI header is required"); - } - } - - /** Registrar IP address is not in stored whitelist. */ - public static class BadRegistrarIpAddressException extends AuthenticationErrorException { - public BadRegistrarIpAddressException() { - super("Registrar IP address is not in stored whitelist"); - } - } +public class TlsCredentials implements TransportCredentials { private static final FormattingLogger logger = FormattingLogger.getLoggerForCallerClass(); private final String clientCertificateHash; - private final InetAddress clientInetAddr; private final String sni; + private final InetAddress clientInetAddr; + @Inject @VisibleForTesting - public TlsCredentials(String clientCertificateHash, InetAddress clientInetAddr, String sni) { + public TlsCredentials( + @Header("X-GFE-SSL-Certificate") String clientCertificateHash, + @Header("X-Forwarded-For") Optional clientAddress, + @Header("X-GFE-Requested-Servername-SNI") String sni) { this.clientCertificateHash = clientCertificateHash; - this.clientInetAddr = clientInetAddr; + this.clientInetAddr = clientAddress.isPresent() ? parseInetAddress(clientAddress.get()) : null; this.sni = sni; } - /** - * Extracts the client TLS certificate and source internet address - * from the given HTTP request. - */ - TlsCredentials(HttpServletRequest req) { - this(req.getHeader(EppTlsServlet.SSL_CLIENT_CERTIFICATE_HASH_FIELD), - parseInetAddress(req.getHeader(EppTlsServlet.FORWARDED_FOR_FIELD)), - req.getHeader(EppTlsServlet.REQUESTED_SERVERNAME_VIA_SNI_FIELD)); - } - static InetAddress parseInetAddress(String asciiAddr) { try { return InetAddresses.forString(HostAndPort.fromString(asciiAddr).getHostText()); @@ -155,7 +146,7 @@ public final class TlsCredentials implements TransportCredentials { if (!hasSni()) { throw new NoSniException(); } - logger.infofmt("Request did not include %s", EppTlsServlet.SSL_CLIENT_CERTIFICATE_HASH_FIELD); + logger.infofmt("Request did not include %s", "X-GFE-SSL-Certificate"); throw new MissingRegistrarCertificateException(); } if (!clientCertificateHash.equals(registrar.getClientCertificateHash()) @@ -174,8 +165,58 @@ public final class TlsCredentials implements TransportCredentials { return toStringHelper(getClass()) .add("system hash code", System.identityHashCode(this)) .add("clientCertificateHash", clientCertificateHash) - .add("clientInetAddress", clientInetAddr) + .add("clientAddress", clientInetAddr) .add("sni", sni) .toString(); } + + /** Registrar certificate does not match stored certificate. */ + public static class BadRegistrarCertificateException extends AuthenticationErrorException { + public BadRegistrarCertificateException() { + super("Registrar certificate does not match stored certificate"); + } + } + + /** Registrar certificate not present. */ + public static class MissingRegistrarCertificateException extends AuthenticationErrorException { + public MissingRegistrarCertificateException() { + super("Registrar certificate not present"); + } + } + + /** SNI header is required. */ + public static class NoSniException extends AuthenticationErrorException { + public NoSniException() { + super("SNI header is required"); + } + } + + /** Registrar IP address is not in stored whitelist. */ + public static class BadRegistrarIpAddressException extends AuthenticationErrorException { + public BadRegistrarIpAddressException() { + super("Registrar IP address is not in stored whitelist"); + } + } + + /** Dagger module for the EPP TLS endpoint. */ + @Module + public static final class EppTlsModule { + @Provides + @Header("X-GFE-SSL-Certificate") + static String provideClientCertificateHash(HttpServletRequest req) { + return extractRequiredHeader(req, "X-GFE-SSL-Certificate"); + } + + @Provides + @Header("X-Forwarded-For") + static Optional provideForwardedFor(HttpServletRequest req) { + return extractOptionalHeader(req, "X-Forwarded-For"); + } + + @Provides + @Header("X-GFE-Requested-Servername-SNI") + static String provideRequestedServername(HttpServletRequest req) { + return extractRequiredHeader(req, "X-GFE-Requested-Servername-SNI"); + } + } } diff --git a/java/google/registry/flows/session/LoginFlow.java b/java/google/registry/flows/session/LoginFlow.java index 0d5cd4396..5dd7c78f3 100644 --- a/java/google/registry/flows/session/LoginFlow.java +++ b/java/google/registry/flows/session/LoginFlow.java @@ -46,11 +46,11 @@ import java.util.Set; /** * An EPP flow for login. * - * @error {@link google.registry.flows.EppConsoleServlet.GaeUserCredentials.BadGaeUserIdException} - * @error {@link google.registry.flows.EppConsoleServlet.GaeUserCredentials.UserNotLoggedInException} * @error {@link google.registry.flows.EppException.UnimplementedExtensionException} * @error {@link google.registry.flows.EppException.UnimplementedObjectServiceException} * @error {@link google.registry.flows.EppException.UnimplementedProtocolVersionException} + * @error {@link google.registry.flows.GaeUserCredentials.BadGaeUserIdException} + * @error {@link google.registry.flows.GaeUserCredentials.UserNotLoggedInException} * @error {@link google.registry.flows.TlsCredentials.BadRegistrarCertificateException} * @error {@link google.registry.flows.TlsCredentials.BadRegistrarIpAddressException} * @error {@link google.registry.flows.TlsCredentials.MissingRegistrarCertificateException} diff --git a/java/google/registry/module/frontend/BUILD b/java/google/registry/module/frontend/BUILD index 93fe2cc72..d074baeb5 100644 --- a/java/google/registry/module/frontend/BUILD +++ b/java/google/registry/module/frontend/BUILD @@ -18,11 +18,13 @@ java_library( "//third_party/java/servlet/servlet_api", "//java/google/registry/braintree", "//java/google/registry/config", + "//java/google/registry/flows", "//java/google/registry/keyring/api", "//java/google/registry/rdap", "//java/google/registry/request", "//java/google/registry/request:modules", "//java/google/registry/ui", + "//java/google/registry/ui/server/api", "//java/google/registry/ui/server/registrar", "//java/google/registry/util", "//java/google/registry/whois", diff --git a/java/google/registry/module/frontend/FrontendRequestComponent.java b/java/google/registry/module/frontend/FrontendRequestComponent.java index c471faff4..3977f63d4 100644 --- a/java/google/registry/module/frontend/FrontendRequestComponent.java +++ b/java/google/registry/module/frontend/FrontendRequestComponent.java @@ -16,6 +16,9 @@ package google.registry.module.frontend; import dagger.Subcomponent; +import google.registry.flows.EppConsoleAction; +import google.registry.flows.EppTlsAction; +import google.registry.flows.TlsCredentials.EppTlsModule; import google.registry.rdap.RdapAutnumAction; import google.registry.rdap.RdapDomainAction; import google.registry.rdap.RdapDomainSearchAction; @@ -28,6 +31,8 @@ import google.registry.rdap.RdapNameserverAction; import google.registry.rdap.RdapNameserverSearchAction; import google.registry.request.RequestModule; import google.registry.request.RequestScope; +import google.registry.ui.server.api.CheckApiAction; +import google.registry.ui.server.api.CheckApiAction.CheckApiModule; import google.registry.ui.server.registrar.ConsoleUiAction; import google.registry.ui.server.registrar.RegistrarPaymentAction; import google.registry.ui.server.registrar.RegistrarPaymentSetupAction; @@ -40,13 +45,18 @@ import google.registry.whois.WhoisServer; @RequestScope @Subcomponent( modules = { + CheckApiModule.class, + EppTlsModule.class, RdapModule.class, RegistrarUserModule.class, RequestModule.class, WhoisModule.class, }) interface FrontendRequestComponent { + CheckApiAction checkApiAction(); ConsoleUiAction consoleUiAction(); + EppConsoleAction eppConsoleAction(); + EppTlsAction eppTlsAction(); RdapAutnumAction rdapAutnumAction(); RegistrarPaymentAction registrarPaymentAction(); RegistrarPaymentSetupAction registrarPaymentSetupAction(); diff --git a/java/google/registry/module/tools/BUILD b/java/google/registry/module/tools/BUILD index 8b07acdd3..fc57ff329 100644 --- a/java/google/registry/module/tools/BUILD +++ b/java/google/registry/module/tools/BUILD @@ -18,6 +18,7 @@ java_library( "//third_party/java/servlet/servlet_api", "//java/google/registry/config", "//java/google/registry/export", + "//java/google/registry/flows", "//java/google/registry/gcs", "//java/google/registry/groups", "//java/google/registry/keyring/api", diff --git a/java/google/registry/module/tools/ToolsRequestComponent.java b/java/google/registry/module/tools/ToolsRequestComponent.java index 90c4264ef..4664a55c7 100644 --- a/java/google/registry/module/tools/ToolsRequestComponent.java +++ b/java/google/registry/module/tools/ToolsRequestComponent.java @@ -17,6 +17,8 @@ package google.registry.module.tools; import dagger.Subcomponent; import google.registry.export.PublishDetailReportAction; +import google.registry.flows.EppToolAction; +import google.registry.flows.EppToolAction.EppToolModule; import google.registry.loadtest.LoadTestAction; import google.registry.loadtest.LoadTestModule; import google.registry.mapreduce.MapreduceModule; @@ -46,6 +48,7 @@ import google.registry.tools.server.javascrap.CountRecurringBillingEventsAction; @RequestScope @Subcomponent( modules = { + EppToolModule.class, LoadTestModule.class, MapreduceModule.class, RequestModule.class, @@ -58,6 +61,7 @@ interface ToolsRequestComponent { CreatePremiumListAction createPremiumListAction(); DeleteEntityAction deleteEntityAction(); DeleteProberDataAction deleteProberDataAction(); + EppToolAction eppToolAction(); GenerateZoneFilesAction generateZoneFilesAction(); KillAllCommitLogsAction killAllCommitLogsAction(); KillAllEppResourcesAction killAllEppResourcesAction(); diff --git a/java/google/registry/monitoring/whitebox/EppMetrics.java b/java/google/registry/monitoring/whitebox/EppMetrics.java index db6d0e8d4..a823bf628 100644 --- a/java/google/registry/monitoring/whitebox/EppMetrics.java +++ b/java/google/registry/monitoring/whitebox/EppMetrics.java @@ -15,12 +15,17 @@ package google.registry.monitoring.whitebox; import com.google.api.services.bigquery.model.TableFieldSchema; +import com.google.apphosting.api.ApiProxy; import com.google.common.collect.ImmutableList; import google.registry.bigquery.BigqueryUtils.FieldType; import google.registry.model.eppoutput.Result.Code; +import google.registry.request.RequestScope; + +import javax.inject.Inject; /** The EPP Metrics collector. See {@link Metrics}. */ +@RequestScope public class EppMetrics extends Metrics { static final String EPPMETRICS_TABLE_ID = "eppMetrics"; @@ -37,9 +42,14 @@ public class EppMetrics extends Metrics { new TableFieldSchema().setName("eppStatus").setType(FieldType.INTEGER.name()), new TableFieldSchema().setName("attempts").setType(FieldType.INTEGER.name())); + @Inject public EppMetrics() { setTableId(EPPMETRICS_TABLE_ID); fields.put("attempts", 0); + fields.put( + "requestId", + ApiProxy.getCurrentEnvironment().getAttributes() + .get("com.google.appengine.runtime.request_log_id").toString()); } public void setCommandName(String name) { @@ -58,10 +68,6 @@ public class EppMetrics extends Metrics { fields.put("eppTarget", eppTarget); } - public void setRequestId(String requestId) { - fields.put("requestId", requestId); - } - public void setEppStatus(Code status) { fields.put("eppStatus", String.valueOf(status.code)); } diff --git a/java/google/registry/request/RequestModule.java b/java/google/registry/request/RequestModule.java index 30baf4bc6..40d75167b 100644 --- a/java/google/registry/request/RequestModule.java +++ b/java/google/registry/request/RequestModule.java @@ -37,6 +37,7 @@ import java.util.Map; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.HttpSession; /** Dagger module for servlets. */ @Module @@ -55,6 +56,11 @@ public final class RequestModule { return response; } + @Provides + HttpSession provideHttpSession() { + return req.getSession(); + } + @Provides HttpServletRequest provideHttpServletRequest() { return req; diff --git a/java/google/registry/request/RequestParameters.java b/java/google/registry/request/RequestParameters.java index 7fb594f18..c9320e3ca 100644 --- a/java/google/registry/request/RequestParameters.java +++ b/java/google/registry/request/RequestParameters.java @@ -198,5 +198,14 @@ public final class RequestParameters { return result; } + /** + * Returns an {@link Optional} of the first HTTP header associated with {@code name}, or empty. + * + * @param name case insensitive header name + */ + public static Optional extractOptionalHeader(HttpServletRequest req, String name) { + return Optional.fromNullable(req.getHeader(name)); + } + private RequestParameters() {} } diff --git a/java/google/registry/tools/ValidateLoginCredentialsCommand.java b/java/google/registry/tools/ValidateLoginCredentialsCommand.java index 46c2a247d..b06ee826b 100644 --- a/java/google/registry/tools/ValidateLoginCredentialsCommand.java +++ b/java/google/registry/tools/ValidateLoginCredentialsCommand.java @@ -17,38 +17,37 @@ package google.registry.tools; import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Strings.isNullOrEmpty; import static com.google.common.io.Resources.getResource; -import static google.registry.flows.EppXmlTransformer.unmarshal; import static google.registry.tools.CommandUtilities.runFlow; import static google.registry.util.X509Utils.getCertificateHash; import static google.registry.util.X509Utils.loadCertificate; import static java.nio.charset.StandardCharsets.US_ASCII; import static java.nio.charset.StandardCharsets.UTF_8; -import com.google.common.net.InetAddresses; +import com.google.common.base.Optional; import com.google.template.soy.SoyFileSet; import com.google.template.soy.data.SoyMapData; import com.beust.jcommander.Parameter; import com.beust.jcommander.Parameters; -import google.registry.flows.Flow; +import google.registry.flows.EppXmlTransformer; import google.registry.flows.FlowRunner; import google.registry.flows.FlowRunner.CommitMode; import google.registry.flows.FlowRunner.UserPrivileges; -import google.registry.flows.SessionMetadata; +import google.registry.flows.HttpSessionMetadata; import google.registry.flows.TlsCredentials; -import google.registry.flows.picker.FlowPicker; +import google.registry.flows.session.LoginFlow; import google.registry.model.eppcommon.Trid; import google.registry.model.eppinput.EppInput; import google.registry.tools.Command.GtechCommand; import google.registry.tools.Command.RemoteApiCommand; import google.registry.tools.params.PathParameter; import google.registry.tools.soy.LoginSoyInfo; +import google.registry.util.BasicHttpSession; +import google.registry.util.SystemClock; import java.nio.file.Files; import java.nio.file.Path; -import java.util.HashMap; -import java.util.Map; import javax.annotation.Nullable; @@ -102,44 +101,20 @@ final class ValidateLoginCredentialsCommand implements RemoteApiCommand, GtechCo .setData(new SoyMapData("clientIdentifier", clientIdentifier, "password", password)) .render() .getBytes(UTF_8); - EppInput eppInput = unmarshal(inputXmlBytes); - Class flowClass = FlowPicker.getFlowClass(eppInput); System.out.println(runFlow( new FlowRunner( - flowClass, - eppInput, - Trid.create(eppInput.getCommandWrapper().getClTrid()), - new SessionMetadata() { - - private final Map properties = new HashMap<>(); - - { - setTransportCredentials(new TlsCredentials( + LoginFlow.class, + EppXmlTransformer.unmarshal(inputXmlBytes), + Trid.create(null), + new HttpSessionMetadata( + new TlsCredentials( clientCertificateHash, - InetAddresses.forString(clientIpAddress), - "placeholder")); // behave as if we have SNI on, since we're validating a cert - } - - @Override - protected void setProperty(String key, Object value) { - properties.put(key, value); - } - - @Override - protected Object getProperty(String key) { - return properties.get(key); - } - - @Override - public SessionSource getSessionSource() { - return SessionSource.TOOL; - } - - @Override - public void invalidate() {} - }, + Optional.of(clientIpAddress), + "placeholder"), // behave as if we have SNI on, since we're validating a cert + new BasicHttpSession()), inputXmlBytes, - null), + null, + new SystemClock()), CommitMode.DRY_RUN, UserPrivileges.NORMAL)); } diff --git a/java/google/registry/ui/server/api/BUILD b/java/google/registry/ui/server/api/BUILD index 7736648e8..e2a5c7210 100644 --- a/java/google/registry/ui/server/api/BUILD +++ b/java/google/registry/ui/server/api/BUILD @@ -11,9 +11,12 @@ java_library( "//java/com/google/common/base", "//java/com/google/common/collect", "//java/com/google/common/net", + "//third_party/java/dagger", + "//third_party/java/jsr330_inject", "//java/google/registry/config", "//java/google/registry/flows", "//java/google/registry/model", + "//java/google/registry/request", "//java/google/registry/ui/server", "//java/google/registry/ui/soy/api:soy_java_wrappers", "//java/google/registry/util", diff --git a/java/google/registry/ui/server/api/CheckApiServlet.java b/java/google/registry/ui/server/api/CheckApiAction.java similarity index 77% rename from java/google/registry/ui/server/api/CheckApiServlet.java rename to java/google/registry/ui/server/api/CheckApiAction.java index 893b6c415..46bdca8cb 100644 --- a/java/google/registry/ui/server/api/CheckApiServlet.java +++ b/java/google/registry/ui/server/api/CheckApiAction.java @@ -31,6 +31,9 @@ import com.google.common.net.InternetDomainName; import com.google.common.net.MediaType; import com.google.template.soy.tofu.SoyTofu; +import dagger.Module; +import dagger.Provides; + import google.registry.config.RegistryEnvironment; import google.registry.flows.EppException; import google.registry.flows.EppXmlTransformer; @@ -47,37 +50,33 @@ import google.registry.model.eppinput.EppInput; import google.registry.model.eppoutput.CheckData.DomainCheck; import google.registry.model.eppoutput.CheckData.DomainCheckData; import google.registry.model.eppoutput.Response; +import google.registry.request.Action; +import google.registry.request.Parameter; +import google.registry.request.RequestParameters; import google.registry.ui.soy.api.DomainCheckFeeEppSoyInfo; +import google.registry.util.Clock; +import google.registry.util.FormattingLogger; -import java.io.IOException; import java.util.Map; -import javax.servlet.http.HttpServlet; +import javax.inject.Inject; import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; /** * A servlet that returns availability and premium checks as json. * - *

This servlet returns plain JSON without a safety prefix, so it's vital that the output not be + *

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. */ -public class CheckApiServlet extends HttpServlet { +@Action(path = "/check") +public class CheckApiAction implements Runnable { + + private static final FormattingLogger logger = FormattingLogger.getLoggerForCallerClass(); private static final Supplier TOFU_SUPPLIER = createTofuSupplier(DomainCheckFeeEppSoyInfo.getInstance()); - @Override - public void doGet(HttpServletRequest req, HttpServletResponse rsp) throws IOException { - Map response = doCheck(req.getParameter("domain")); - rsp.setHeader("Content-Disposition", "attachment"); - rsp.setHeader("X-Content-Type-Options", "nosniff"); - rsp.setHeader(ACCESS_CONTROL_ALLOW_ORIGIN, "*"); - rsp.setContentType(MediaType.JSON_UTF_8.toString()); - rsp.getWriter().write(toJSONString(response)); - } - private StatelessRequestSessionMetadata sessionMetadata = new StatelessRequestSessionMetadata( RegistryEnvironment.get().config().getCheckApiServletRegistrarClientId(), false, @@ -85,6 +84,21 @@ public class CheckApiServlet extends HttpServlet { ImmutableSet.of(FEE_0_6.getUri()), SessionSource.HTTP); + @Inject @Parameter("domain") String domain; + @Inject google.registry.request.Response response; + @Inject Clock clock; + @Inject CheckApiAction() {} + + @Override + public void run() { + Map checkResponse = doCheck(domain); + 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(checkResponse)); + } + // TODO(rgr): add whitebox instrumentation for this? private Map doCheck(String domainString) { try { @@ -103,10 +117,11 @@ public class CheckApiServlet extends HttpServlet { Response response = new FlowRunner( DomainCheckFlow.class, EppXmlTransformer.unmarshal(inputXmlBytes), - Trid.create(CheckApiServlet.class.getSimpleName()), + Trid.create(getClass().getSimpleName()), sessionMetadata, inputXmlBytes, - null) + null, + clock) .run(CommitMode.LIVE, UserPrivileges.NORMAL) .getResponse(); DomainCheckData checkData = (DomainCheckData) response.getResponseData().get(0); @@ -127,7 +142,7 @@ public class CheckApiServlet extends HttpServlet { } catch (EppException e) { return fail(e.getMessage()); } catch (Exception e) { - e.printStackTrace(); + logger.warning(e, "Unknown error"); return fail("Invalid request"); } } @@ -137,4 +152,14 @@ public class CheckApiServlet extends HttpServlet { "status", "error", "reason", reason); } + + /** Dagger module for the check api endpoint. */ + @Module + public static final class CheckApiModule { + @Provides + @Parameter("domain") + static String provideDomain(HttpServletRequest req) { + return RequestParameters.extractRequiredParameter(req, "domain"); + } + } } diff --git a/java/google/registry/ui/server/registrar/ConsoleUiAction.java b/java/google/registry/ui/server/registrar/ConsoleUiAction.java index c8acb4c37..018492b14 100644 --- a/java/google/registry/ui/server/registrar/ConsoleUiAction.java +++ b/java/google/registry/ui/server/registrar/ConsoleUiAction.java @@ -28,7 +28,7 @@ import com.google.template.soy.shared.SoyCssRenamingMap; import com.google.template.soy.tofu.SoyTofu; import google.registry.config.ConfigModule.Config; -import google.registry.flows.EppConsoleServlet; +import google.registry.flows.EppConsoleAction; import google.registry.model.registrar.Registrar; import google.registry.request.Action; import google.registry.request.Response; @@ -92,7 +92,7 @@ public final class ConsoleUiAction implements Runnable { } Registrar registrar = Registrar.loadByClientId(sessionUtils.getRegistrarClientId(req)); SoyMapData data = new SoyMapData(); - data.put("xsrfToken", XsrfTokenManager.generateToken(EppConsoleServlet.XSRF_SCOPE)); + data.put("xsrfToken", XsrfTokenManager.generateToken(EppConsoleAction.XSRF_SCOPE)); data.put("clientId", registrar.getClientIdentifier()); data.put("username", userService.getCurrentUser().getNickname()); data.put("isAdmin", userService.isUserAdmin()); diff --git a/java/google/registry/ui/server/registrar/ResourceServlet.java b/java/google/registry/ui/server/registrar/ResourceServlet.java index 1a101c4bd..1922d398e 100644 --- a/java/google/registry/ui/server/registrar/ResourceServlet.java +++ b/java/google/registry/ui/server/registrar/ResourceServlet.java @@ -15,7 +15,7 @@ package google.registry.ui.server.registrar; import static com.google.appengine.api.users.UserServiceFactory.getUserService; -import static google.registry.flows.EppConsoleServlet.XSRF_SCOPE; +import static google.registry.flows.EppConsoleAction.XSRF_SCOPE; import static google.registry.security.JsonResponseHelper.Status.ERROR; import com.google.common.base.Optional; diff --git a/javatests/google/registry/flows/EppConsoleActionTest.java b/javatests/google/registry/flows/EppConsoleActionTest.java new file mode 100644 index 000000000..2c666838a --- /dev/null +++ b/javatests/google/registry/flows/EppConsoleActionTest.java @@ -0,0 +1,73 @@ +// Copyright 2016 The Domain Registry 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 java.nio.charset.StandardCharsets.UTF_8; +import static org.mockito.Mockito.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +import google.registry.testing.AppEngineRule; +import google.registry.testing.ShardableTestCase; +import google.registry.testing.UserInfo; +import google.registry.util.BasicHttpSession; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; +import org.mockito.ArgumentCaptor; + +/** Tests for {@link EppConsoleAction}. */ +@RunWith(JUnit4.class) +public class EppConsoleActionTest extends ShardableTestCase { + + private static final byte[] INPUT_XML_BYTES = "".getBytes(UTF_8); + + @Rule + public final AppEngineRule appEngine = AppEngineRule.builder() + .withUserService(UserInfo.create("person@example.com", "12345")) + .build(); + + private void doTest(boolean superuser) { + EppConsoleAction action = new EppConsoleAction(); + action.inputXmlBytes = INPUT_XML_BYTES; + action.session = new BasicHttpSession(); + action.session.setAttribute("CLIENT_ID", "ClientIdentifier"); + action.session.setAttribute("SUPERUSER", superuser); + action.eppRequestHandler = mock(EppRequestHandler.class); + action.run(); + ArgumentCaptor captor = ArgumentCaptor.forClass(SessionMetadata.class); + verify(action.eppRequestHandler).executeEpp(captor.capture(), eq(INPUT_XML_BYTES)); + SessionMetadata sessionMetadata = captor.getValue(); + assertThat(((GaeUserCredentials) sessionMetadata.getTransportCredentials()).gaeUser.getEmail()) + .isEqualTo("person@example.com"); + assertThat(sessionMetadata.getClientId()).isEqualTo("ClientIdentifier"); + assertThat(sessionMetadata.isDryRun()).isFalse(); // Should always be false for console. + assertThat(sessionMetadata.isSuperuser()).isEqualTo(superuser); + } + + @Test + public void testSuperuser() throws Exception { + doTest(true); + } + + @Test + public void testNotSuperuser() throws Exception { + doTest(false); + } +} diff --git a/javatests/google/registry/flows/EppControllerTest.java b/javatests/google/registry/flows/EppControllerTest.java index a5abca449..ef75953bf 100644 --- a/javatests/google/registry/flows/EppControllerTest.java +++ b/javatests/google/registry/flows/EppControllerTest.java @@ -14,13 +14,14 @@ package google.registry.flows; -import static google.registry.flows.EppController.getErrorResponse; import static google.registry.flows.EppXmlTransformer.marshal; import google.registry.model.eppcommon.Trid; import google.registry.model.eppoutput.Result; import google.registry.model.eppoutput.Result.Code; import google.registry.testing.AppEngineRule; +import google.registry.testing.ShardableTestCase; +import google.registry.util.SystemClock; import google.registry.xml.ValidationMode; import org.junit.Rule; @@ -30,7 +31,7 @@ import org.junit.runners.JUnit4; /** Unit tests for {@link EppController}. */ @RunWith(JUnit4.class) -public class EppControllerTest { +public class EppControllerTest extends ShardableTestCase { @Rule public AppEngineRule appEngineRule = new AppEngineRule.Builder().build(); @@ -38,13 +39,8 @@ public class EppControllerTest { @Test public void testMarshallingUnknownError() throws Exception { marshal( - getErrorResponse(Result.create(Code.CommandFailed), Trid.create(null)), + EppController.getErrorResponse( + new SystemClock(), Result.create(Code.CommandFailed), Trid.create(null)), ValidationMode.STRICT); } - - // Extra methods so the test runner doesn't produce empty shards. - - @Test public void testNothing1() {} - @Test public void testNothing2() {} - @Test public void testNothing3() {} } diff --git a/javatests/google/registry/flows/EppLifecycleContactTest.java b/javatests/google/registry/flows/EppLifecycleContactTest.java new file mode 100644 index 000000000..1583ff578 --- /dev/null +++ b/javatests/google/registry/flows/EppLifecycleContactTest.java @@ -0,0 +1,87 @@ +// Copyright 2016 The Domain Registry 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 com.google.common.collect.ImmutableMap; + +import google.registry.testing.AppEngineRule; + +import org.joda.time.DateTime; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +/** Tests for contact lifecycle. */ +@RunWith(JUnit4.class) +public class EppLifecycleContactTest extends EppTestCase { + + @Rule + public final AppEngineRule appEngine = AppEngineRule.builder() + .withDatastore() + .withTaskQueue() + .build(); + + @Test + public void testContactLifecycle() throws Exception { + assertCommandAndResponse("login_valid.xml", "login_response.xml"); + assertCommandAndResponse( + "contact_create_sh8013.xml", + null, + "contact_create_response_sh8013.xml", + ImmutableMap.of("CRDATE", "2000-06-01T00:00:00Z"), + DateTime.parse("2000-06-01T00:00:00Z")); + assertCommandAndResponse( + "contact_info.xml", + "contact_info_from_create_response.xml", + DateTime.parse("2000-06-01T00:01:00Z")); + assertCommandAndResponse("contact_delete_sh8013.xml", "contact_delete_response_sh8013.xml"); + assertCommandAndResponse("logout.xml", "logout_response.xml"); + } + + @Test + public void testContactTransferPollMessage() throws Exception { + assertCommandAndResponse("login_valid.xml", "login_response.xml"); + assertCommandAndResponse( + "contact_create_sh8013.xml", + ImmutableMap.of(), + "contact_create_response_sh8013.xml", + ImmutableMap.of("CRDATE", "2000-06-01T00:00:00Z"), + DateTime.parse("2000-06-01T00:00:00Z")); + assertCommandAndResponse("logout.xml", "logout_response.xml"); + + // Initiate a transfer of the newly created contact. + assertCommandAndResponse("login2_valid.xml", "login_response.xml"); + assertCommandAndResponse( + "contact_transfer_request.xml", + "contact_transfer_request_response_alternate.xml", + DateTime.parse("2000-06-08T22:00:00Z")); + assertCommandAndResponse("logout.xml", "logout_response.xml"); + + // Log back in with the losing registrar, read the poll message, and then ack it. + assertCommandAndResponse("login_valid.xml", "login_response.xml"); + assertCommandAndResponse( + "poll.xml", + "poll_response_contact_transfer.xml", + DateTime.parse("2000-06-08T22:01:00Z")); + assertCommandAndResponse( + "poll_ack.xml", + ImmutableMap.of("ID", "2-1-ROID-3-4"), + "poll_ack_response_empty.xml", + null, + DateTime.parse("2000-06-08T22:02:00Z")); + assertCommandAndResponse("logout.xml", "logout_response.xml"); + } +} diff --git a/javatests/google/registry/flows/EppLifecycleDomainApplicationTest.java b/javatests/google/registry/flows/EppLifecycleDomainApplicationTest.java new file mode 100644 index 000000000..1a920081b --- /dev/null +++ b/javatests/google/registry/flows/EppLifecycleDomainApplicationTest.java @@ -0,0 +1,118 @@ +// Copyright 2016 The Domain Registry 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 google.registry.testing.DatastoreHelper.createTld; + +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSortedMap; + +import google.registry.model.registry.Registry.TldState; +import google.registry.testing.AppEngineRule; +import google.registry.util.DateTimeUtils; + +import org.joda.time.DateTime; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +/** Tests for domain application lifecycle. */ +@RunWith(JUnit4.class) +public class EppLifecycleDomainApplicationTest extends EppTestCase { + + private static final DateTime START_OF_GA = DateTime.parse("2014-03-01T00:00:00Z"); + + @Rule + public final AppEngineRule appEngine = AppEngineRule.builder() + .withDatastore() + .withTaskQueue() + .build(); + + @Before + public void initTld() { + createTld("example", ImmutableSortedMap.of( + DateTimeUtils.START_OF_TIME, TldState.SUNRISE, + START_OF_GA, TldState.GENERAL_AVAILABILITY)); + } + + /** Create the two administrative contacts and two hosts. */ + void createContactsAndHosts() throws Exception { + DateTime startTime = DateTime.parse("2000-06-01T00:00:00Z"); + assertCommandAndResponse( + "contact_create_sh8013.xml", + ImmutableMap.of(), + "contact_create_response_sh8013.xml", + ImmutableMap.of("CRDATE", "2000-06-01T00:00:00Z"), + startTime); + assertCommandAndResponse( + "contact_create_jd1234.xml", + "contact_create_response_jd1234.xml", + startTime.plusMinutes(1)); + assertCommandAndResponse( + "host_create.xml", + "host_create_response.xml", + startTime.plusMinutes(2)); + assertCommandAndResponse( + "host_create2.xml", + "host_create2_response.xml", + startTime.plusMinutes(3)); + } + + @Test + public void testApplicationDuringSunrise_doesntCreateDomainWithoutAllocation() throws Exception { + assertCommandAndResponse("login_valid.xml", "login_response.xml"); + createContactsAndHosts(); + // Note that the trademark is valid from 2013-08-09 to 2017-07-23, hence the creation in 2014. + assertCommandAndResponse( + "domain_create_sunrise_encoded_mark.xml", + "domain_create_sunrise_encoded_signed_mark_response.xml", + DateTime.parse("2014-01-01T00:00:00Z")); + assertCommandAndResponse( + "domain_info_testvalidate.xml", + "domain_info_response_testvalidate_doesnt_exist.xml", + DateTime.parse("2014-01-01T00:01:00Z")); + assertCommandAndResponse("logout.xml", "logout_response.xml"); + } + + @Test + public void testDomainAllocation_succeedsOnlyAsSuperuser() throws Exception { + assertCommandAndResponse("login_valid.xml", "login_response.xml"); + createContactsAndHosts(); + assertCommandAndResponse( + "domain_create_sunrise_encoded_mark.xml", + "domain_create_sunrise_encoded_signed_mark_response.xml", + DateTime.parse("2014-01-01T00:00:00Z")); + assertCommandAndResponse( + "domain_info_testvalidate.xml", + "domain_info_response_testvalidate_doesnt_exist.xml", + DateTime.parse("2014-01-01T00:01:00Z")); + assertCommandAndResponse( + "domain_allocate_testvalidate.xml", + "domain_allocate_response_testvalidate_only_superuser.xml", + START_OF_GA.plusDays(1)); + setSuperuser(true); + assertCommandAndResponse( + "domain_allocate_testvalidate.xml", + "domain_allocate_response_testvalidate.xml", + START_OF_GA.plusDays(1).plusMinutes(1)); + setSuperuser(false); + assertCommandAndResponse( + "domain_info_testvalidate.xml", + "domain_info_response_testvalidate_ok.xml", + START_OF_GA.plusDays(1).plusMinutes(2)); + } +} diff --git a/javatests/google/registry/flows/EppTlsServletTest.java b/javatests/google/registry/flows/EppLifecycleDomainTest.java similarity index 56% rename from javatests/google/registry/flows/EppTlsServletTest.java rename to javatests/google/registry/flows/EppLifecycleDomainTest.java index c80ce1aa9..3903ec728 100644 --- a/javatests/google/registry/flows/EppTlsServletTest.java +++ b/javatests/google/registry/flows/EppLifecycleDomainTest.java @@ -14,28 +14,27 @@ package google.registry.flows; -import static google.registry.testing.DatastoreHelper.persistResource; -import static java.nio.charset.StandardCharsets.UTF_8; -import static org.mockito.Mockito.when; +import static google.registry.testing.DatastoreHelper.createTld; +import static google.registry.util.DateTimeUtils.START_OF_TIME; import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSortedMap; import com.google.re2j.Matcher; import com.google.re2j.Pattern; -import google.registry.model.registrar.Registrar; +import google.registry.model.registry.Registry.TldState; import google.registry.testing.AppEngineRule; -import google.registry.testing.CertificateSamples; -import google.registry.testing.FakeServletInputStream; +import org.joda.time.DateTime; import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; -import org.mockito.runners.MockitoJUnitRunner; +import org.junit.runners.JUnit4; -/** Test setup for EppServletTest subclasses. */ -@RunWith(MockitoJUnitRunner.class) -public class EppTlsServletTest extends EppServletXmlLoginTestCase { +/** Tests for domain lifecycle. */ +@RunWith(JUnit4.class) +public class EppLifecycleDomainTest extends EppTestCase { @Rule public final AppEngineRule appEngine = AppEngineRule.builder() @@ -43,79 +42,276 @@ public class EppTlsServletTest extends EppServletXmlLoginTestCase .withTaskQueue() .build(); - String ipAddressAndPort = "192.168.1.100:54321"; - String clientCert = CertificateSamples.SAMPLE_CERT_HASH; - String clientCert2 = CertificateSamples.SAMPLE_CERT2_HASH; - String requestedServername = "test.example"; - - private String gfeRequestClientCertificateHashField; - @Before - public void initTest() throws Exception { - persistResource(Registrar.loadByClientId("NewRegistrar") - .asBuilder() - .setClientCertificateHash(clientCert) - .build()); - - persistResource(Registrar.loadByClientId("TheRegistrar") - .asBuilder() - .setClientCertificateHash(clientCert2) - .build()); + public void initTld() { + createTld("example"); } - @Test - public void testSetTldViaSni() throws Exception { - requestedServername = "epp.nic.xn--q9jyb4c"; - assertCommandAndResponse("login2_valid.xml", "login_response.xml"); + /** Create the two administrative contacts and two hosts. */ + void createContactsAndHosts() throws Exception { + DateTime startTime = DateTime.parse("2000-06-01T00:00:00Z"); assertCommandAndResponse( "contact_create_sh8013.xml", ImmutableMap.of(), "contact_create_response_sh8013.xml", ImmutableMap.of("CRDATE", "2000-06-01T00:00:00Z"), - "2000-06-01T00:00:00Z"); + startTime); assertCommandAndResponse( - "domain_create_minna.xml", - "domain_create_response_minna.xml", - "2000-06-01T01:02:00Z"); - assertCommandAndResponse("logout.xml", "logout_response.xml"); + "contact_create_jd1234.xml", + "contact_create_response_jd1234.xml", + startTime.plusMinutes(1)); + assertCommandAndResponse( + "host_create.xml", + "host_create_response.xml", + startTime.plusMinutes(2)); + assertCommandAndResponse( + "host_create2.xml", + "host_create2_response.xml", + startTime.plusMinutes(3)); + } + + /** Creates the domain fakesite.example with two nameservers on it. */ + void createFakesite() throws Exception { + createContactsAndHosts(); + assertCommandAndResponse( + "domain_create_fakesite.xml", + "domain_create_response_fakesite.xml", + DateTime.parse("2000-06-01T00:04:00Z")); + assertCommandAndResponse( + "domain_info_fakesite.xml", + "domain_info_response_fakesite_ok.xml", + DateTime.parse("2000-06-06T00:00:00Z")); + } + + /** Creates ns3.fakesite.example as a host, then adds it to fakesite. */ + void createSubordinateHost() throws Exception { + // Add the fakesite nameserver (requires that domain is already created). + assertCommandAndResponse( + "host_create_fakesite.xml", + "host_create_response_fakesite.xml", + DateTime.parse("2000-06-06T00:01:00Z")); + // Add new nameserver to domain. + assertCommandAndResponse( + "domain_update_add_nameserver_fakesite.xml", + "domain_update_add_nameserver_response_fakesite.xml", + DateTime.parse("2000-06-08T00:00:00Z")); + // Verify new nameserver was added. + assertCommandAndResponse( + "domain_info_fakesite.xml", + "domain_info_response_fakesite_3_nameservers.xml", + DateTime.parse("2000-06-08T00:01:00Z")); + // Verify that nameserver's data was set correctly. + assertCommandAndResponse( + "host_info_fakesite.xml", + "host_info_response_fakesite.xml", + DateTime.parse("2000-06-08T00:02:00Z")); } - /** This test requires multiple registrars, which EppConsoleServlet doesn't allow. */ @Test - public void testContactTransferPollMessage() throws Exception { + public void testDomainDeleteRestore() throws Exception { assertCommandAndResponse("login_valid.xml", "login_response.xml"); + + // Create contacts sh8013 and jd1234. assertCommandAndResponse( "contact_create_sh8013.xml", - ImmutableMap.of(), - "contact_create_response_sh8013.xml", - ImmutableMap.of("CRDATE", "2000-06-01T00:00:00Z"), - "2000-06-01T00:00:00Z"); - assertCommandAndResponse("logout.xml", "logout_response.xml"); - - // Initiate a transfer of the newly created contact. - assertCommandAndResponse("login2_valid.xml", "login_response.xml"); - assertCommandAndResponse( - "contact_transfer_request.xml", - "contact_transfer_request_response_alternate.xml", - "2000-06-08T22:00:00Z"); - assertCommandAndResponse("logout.xml", "logout_response.xml"); - - // Log back in with the losing registrar, read the poll message, and then ack it. - assertCommandAndResponse("login_valid.xml", "login_response.xml"); - assertCommandAndResponse( - "poll.xml", - "poll_response_contact_transfer.xml", - "2000-06-08T22:01:00Z"); - assertCommandAndResponse( - "poll_ack.xml", - ImmutableMap.of("ID", "2-4-ROID-6-7"), - "poll_ack_response_empty.xml", null, - "2000-06-08T22:02:00Z"); + "contact_create_response_sh8013.xml", + ImmutableMap.of("CRDATE", "2000-06-01T00:00:00Z"), + DateTime.parse("2000-06-01T00:00:00Z")); + assertCommandAndResponse( + "contact_create_jd1234.xml", + "contact_create_response_jd1234.xml", + DateTime.parse("2000-06-01T00:01:00Z")); + + // Create domain example.tld. + assertCommandAndResponse( + "domain_create_no_hosts_or_dsdata.xml", + "domain_create_response.xml", + DateTime.parse("2000-06-01T00:02:00Z")); + + // Delete domain example.com after its add grace period has expired. + assertCommandAndResponse( + "domain_delete.xml", + "generic_success_action_pending_response.xml", + DateTime.parse("2000-07-01T00:02:00Z")); + + // Restore the domain. + assertCommandAndResponse( + "domain_update_restore_request.xml", + "domain_update_restore_request_response.xml", + DateTime.parse("2000-07-01T00:03:00Z")); + + assertCommandAndResponse("logout.xml", "logout_response.xml"); + } + + @Test + public void testDomainDeletion_withinAddGracePeriod() throws Exception { + assertCommandAndResponse("login_valid.xml", "login_response.xml"); + + // Create contacts sh8013 and jd1234. + assertCommandAndResponse( + "contact_create_sh8013.xml", + null, + "contact_create_response_sh8013.xml", + ImmutableMap.of("CRDATE", "2000-06-01T00:00:00Z"), + DateTime.parse("2000-06-01T00:00:00Z")); + assertCommandAndResponse( + "contact_create_jd1234.xml", + "contact_create_response_jd1234.xml", + DateTime.parse("2000-06-01T00:01:00Z")); + + // Create domain example.tld. + assertCommandAndResponse( + "domain_create_no_hosts_or_dsdata.xml", + "domain_create_response.xml", + DateTime.parse("2000-06-01T00:02:00Z")); + + // Delete domain example.tld after its add grace period has expired. + assertCommandAndResponse( + "domain_delete.xml", + "generic_success_action_pending_response.xml", + DateTime.parse("2000-07-01T00:02:00Z")); + + // Poke the domain a little at various times to see its status + assertCommandAndResponse( + "domain_info.xml", + "domain_info_response_pendingdelete.xml", + DateTime.parse("2000-08-01T00:02:00Z")); // 1 day out. + + assertCommandAndResponse("logout.xml", "logout_response.xml"); + } + + @Test + public void testDomainDeletionWithSubordinateHost_fails() throws Exception { + assertCommandAndResponse("login_valid.xml", "login_response.xml"); + createFakesite(); + createSubordinateHost(); + assertCommandAndResponse( + "domain_delete_fakesite.xml", + "domain_delete_response_prohibited.xml", + DateTime.parse("2002-05-30T01:01:00Z")); + assertCommandAndResponse("logout.xml", "logout_response.xml"); + } + + @Test + public void testDeletionOfDomain_afterRenameOfSubordinateHost_succeeds() throws Exception { + assertCommandAndResponse("login_valid.xml", "login_response.xml"); + createFakesite(); + createSubordinateHost(); + // Update the ns3 host to no longer be on fakesite.example domain. + assertCommandAndResponse( + "host_update_fakesite.xml", + "generic_success_response.xml", + DateTime.parse("2002-05-30T01:01:00Z")); + // Delete the fakesite.example domain (which should succeed since it no longer has subords). + assertCommandAndResponse( + "domain_delete_fakesite.xml", + "generic_success_action_pending_response.xml", + DateTime.parse("2002-05-30T01:02:00Z")); + // Check info on the renamed host and verify that it's still around and wasn't deleted. + assertCommandAndResponse( + "host_info_ns9000_example.xml", + "host_info_response_ns9000_example.xml", + DateTime.parse("2002-06-30T01:03:00Z")); + assertCommandAndResponse("logout.xml", "logout_response.xml"); + } + + @Test + public void testDeletionOfDomain_afterUpdateThatCreatesSubordinateHost_fails() throws Exception { + assertCommandAndResponse("login_valid.xml", "login_response.xml"); + createFakesite(); + // Update the ns1 host to be on the fakesite.example domain. + assertCommandAndResponse( + "host_update_ns1_to_fakesite.xml", + "generic_success_response.xml", + DateTime.parse("2002-05-30T01:01:00Z")); + // Attempt to delete the fakesite.example domain (which should fail since it now has a + // subordinate host). + assertCommandAndResponse( + "domain_delete_fakesite.xml", + "domain_delete_response_prohibited.xml", + DateTime.parse("2002-05-30T01:02:00Z")); + // Check info on the renamed host and verify that it's still around and wasn't deleted. + assertCommandAndResponse( + "host_info_fakesite.xml", + "host_info_response_fakesite_post_update.xml", + DateTime.parse("2002-06-30T01:03:00Z")); + // Verify that fakesite.example domain is still around and wasn't deleted. + assertCommandAndResponse( + "domain_info_fakesite.xml", + "domain_info_response_fakesite_ok_post_host_update.xml", + DateTime.parse("2002-05-30T01:00:00Z")); + assertCommandAndResponse("logout.xml", "logout_response.xml"); + } + + @Test + public void testRenamingHostToExistingHost_fails() throws Exception { + assertCommandAndResponse("login_valid.xml", "login_response.xml"); + // Create the two hosts. + assertCommandAndResponse( + "host_create.xml", "host_create_response.xml", DateTime.parse("2000-06-01T00:02:00Z")); + assertCommandAndResponse( + "host_create2.xml", "host_create2_response.xml", DateTime.parse("2000-06-01T00:03:00Z")); + // Verify that host1 and host2 were created as we expect them. + assertCommandAndResponse( + "host_info_ns1.xml", "host_info_response_ns1.xml", DateTime.parse("2000-06-01T00:04:00Z")); + assertCommandAndResponse( + "host_info_ns2.xml", "host_info_response_ns2.xml", DateTime.parse("2000-06-01T00:05:00Z")); + // Attempt overwriting of host1 on top of host2 (and verify that it fails). + assertCommandAndResponse( + "host_update_ns1_to_ns2.xml", + "host_update_failed_response.xml", + DateTime.parse("2000-06-01T00:06:00Z")); + // Verify that host1 and host2 still exist in their unmodified states. + assertCommandAndResponse( + "host_info_ns1.xml", "host_info_response_ns1.xml", DateTime.parse("2000-06-01T00:07:00Z")); + assertCommandAndResponse( + "host_info_ns2.xml", "host_info_response_ns2.xml", DateTime.parse("2000-06-01T00:08:00Z")); + assertCommandAndResponse("logout.xml", "logout_response.xml"); + } + + @Test + public void testDomainCreation_failsBeforeSunrise() throws Exception { + DateTime sunriseDate = DateTime.parse("2000-05-30T00:00:00Z"); + createTld("example", ImmutableSortedMap.of( + START_OF_TIME, TldState.PREDELEGATION, + sunriseDate, TldState.SUNRISE, + sunriseDate.plusMonths(2), TldState.GENERAL_AVAILABILITY)); + + assertCommandAndResponse("login_valid.xml", "login_response.xml"); + + createContactsAndHosts(); + + assertCommandAndResponse( + "domain_create_sunrise_encoded_mark.xml", + "domain_create_testvalidate_invalid_phase.xml", + sunriseDate.minusDays(1)); + + assertCommandAndResponse( + "domain_info_testvalidate.xml", + "domain_info_response_testvalidate_doesnt_exist.xml", + sunriseDate.plusDays(1)); + + assertCommandAndResponse("logout.xml", "logout_response.xml"); + } + + @Test + public void testDomainCheckFee_succeeds() throws Exception { + DateTime gaDate = DateTime.parse("2000-05-30T00:00:00Z"); + createTld("example", ImmutableSortedMap.of( + START_OF_TIME, TldState.PREDELEGATION, + gaDate, TldState.GENERAL_AVAILABILITY)); + + assertCommandAndResponse("login_valid_fee_extension.xml", "login_response.xml"); + + assertCommandAndResponse( + "domain_check_fee_premium.xml", + "domain_check_fee_premium_response.xml", + gaDate.plusDays(1)); + assertCommandAndResponse("logout.xml", "logout_response.xml"); } - /** This test requires multiple registrars, which EppConsoleServlet doesn't allow. */ @Test public void testDomainTransferPollMessage_serverApproved() throws Exception { // As the losing registrar, create the domain. @@ -128,7 +324,7 @@ public class EppTlsServletTest extends EppServletXmlLoginTestCase String response = assertCommandAndResponse( "domain_transfer_request_1_year.xml", "domain_transfer_response_1_year.xml", - "2001-01-01T00:00:00Z"); + DateTime.parse("2001-01-01T00:00:00Z")); Matcher matcher = Pattern.compile("(.*)").matcher(response); matcher.find(); String transferRequestTrid = matcher.group(1); @@ -139,25 +335,25 @@ public class EppTlsServletTest extends EppServletXmlLoginTestCase assertCommandAndResponse( "poll.xml", "poll_response_domain_transfer_request.xml", - "2001-01-01T00:01:00Z"); + DateTime.parse("2001-01-01T00:01:00Z")); assertCommandAndResponse( "poll_ack.xml", - ImmutableMap.of("ID", "1-C-EXAMPLE-18-22"), + ImmutableMap.of("ID", "1-A-EXAMPLE-16-20"), "poll_ack_response_empty.xml", null, - "2001-01-01T00:01:00Z"); + DateTime.parse("2001-01-01T00:01:00Z")); // Five days in the future, expect a server approval poll message to the loser, and ack it. assertCommandAndResponse( "poll.xml", "poll_response_domain_transfer_server_approve_loser.xml", - "2001-01-06T00:01:00Z"); + DateTime.parse("2001-01-06T00:01:00Z")); assertCommandAndResponse( "poll_ack.xml", - ImmutableMap.of("ID", "1-C-EXAMPLE-18-24"), + ImmutableMap.of("ID", "1-A-EXAMPLE-16-22"), "poll_ack_response_empty.xml", null, - "2001-01-06T00:01:00Z"); + DateTime.parse("2001-01-06T00:01:00Z")); assertCommandAndResponse("logout.xml", "logout_response.xml"); // Also expect a server approval poll message to the winner, with the transfer request trid. @@ -167,32 +363,16 @@ public class EppTlsServletTest extends EppServletXmlLoginTestCase null, "poll_response_domain_transfer_server_approve_winner.xml", ImmutableMap.of("SERVER_TRID", transferRequestTrid), - "2001-01-06T00:02:00Z"); + DateTime.parse("2001-01-06T00:02:00Z")); assertCommandAndResponse( "poll_ack.xml", - ImmutableMap.of("ID", "1-C-EXAMPLE-18-23"), + ImmutableMap.of("ID", "1-A-EXAMPLE-16-21"), "poll_ack_response_empty.xml", null, - "2001-01-06T00:02:00Z"); + DateTime.parse("2001-01-06T00:02:00Z")); assertCommandAndResponse("logout.xml", "logout_response.xml"); } - @Override - protected void extendedSessionConfig(String inputFile) throws Exception { - when(req.getHeader(EppTlsServlet.REQUESTED_SERVERNAME_VIA_SNI_FIELD)) - .thenReturn(requestedServername); - when(req.getHeader(EppTlsServlet.FORWARDED_FOR_FIELD)) - .thenReturn(ipAddressAndPort); - if (gfeRequestClientCertificateHashField != null) { - when(req.getHeader(EppTlsServlet.SSL_CLIENT_CERTIFICATE_HASH_FIELD)) - .thenReturn(gfeRequestClientCertificateHashField); - } else { - when(req.getHeader(EppTlsServlet.SSL_CLIENT_CERTIFICATE_HASH_FIELD)) - .thenReturn(inputFile.contains("TheRegistrar") ? clientCert2 : clientCert); - } - when(req.getInputStream()).thenReturn(new FakeServletInputStream(inputFile.getBytes(UTF_8))); - } - @Test public void testIgnoredTransferDuringAutoRenewPeriod_succeeds() throws Exception { // Register the domain as the first registrar. @@ -205,19 +385,20 @@ public class EppTlsServletTest extends EppServletXmlLoginTestCase assertCommandAndResponse( "domain_transfer_request_2_years.xml", "domain_transfer_response_2_years.xml", - "2002-05-30T00:00:00Z"); + DateTime.parse("2002-05-30T00:00:00Z")); assertCommandAndResponse("logout.xml", "logout_response.xml"); // Log back in as the first registrar and verify things. - assertCommandAndResponse("login_valid.xml", "login_response.xml"); + assertCommandAndResponse( + "login_valid.xml", "login_response.xml"); assertCommandAndResponse( "domain_info_fakesite.xml", "domain_info_response_fakesite_pending_transfer.xml", - "2002-05-30T01:00:00Z"); + DateTime.parse("2002-05-30T01:00:00Z")); assertCommandAndResponse( "domain_info_fakesite.xml", "domain_info_response_fakesite_pending_transfer_autorenew.xml", - "2002-06-02T00:00:00Z"); + DateTime.parse("2002-06-02T00:00:00Z")); assertCommandAndResponse("logout.xml", "logout_response.xml"); // Log back in as the second registrar and verify transfer details. @@ -226,66 +407,14 @@ public class EppTlsServletTest extends EppServletXmlLoginTestCase assertCommandAndResponse( "domain_info_fakesite.xml", "domain_info_response_fakesite_transfer_period.xml", - "2002-06-06T00:00:00Z"); + DateTime.parse("2002-06-06T00:00:00Z")); assertCommandAndResponse( "domain_info_fakesite.xml", "domain_info_response_fakesite_transfer_complete.xml", - "2002-06-12T00:00:00Z"); + DateTime.parse("2002-06-12T00:00:00Z")); assertCommandAndResponse("logout.xml", "logout_response.xml"); } - @Test - public void testBadCertificate_failsBadCertificate2200() throws Exception { - gfeRequestClientCertificateHashField = "laffo"; - assertCommandAndResponse("login_valid.xml", "login_response_bad_certificate.xml"); - } - - @Test - public void testGfeDidntProvideClientCertificate_failsMissingCertificate2200() throws Exception { - gfeRequestClientCertificateHashField = ""; - assertCommandAndResponse("login_valid.xml", "login_response_missing_certificate.xml"); - } - - @Test - public void testGoodPrimaryCertificate() throws Exception { - gfeRequestClientCertificateHashField = CertificateSamples.SAMPLE_CERT_HASH; - persistResource(Registrar.loadByClientId("NewRegistrar").asBuilder() - .setClientCertificate(CertificateSamples.SAMPLE_CERT, clock.nowUtc()) - .setFailoverClientCertificate(CertificateSamples.SAMPLE_CERT2, clock.nowUtc()) - .build()); - assertCommandAndResponse("login_valid.xml", "login_response.xml"); - } - - @Test - public void testGoodFailoverCertificate() throws Exception { - gfeRequestClientCertificateHashField = CertificateSamples.SAMPLE_CERT2_HASH; - persistResource(Registrar.loadByClientId("NewRegistrar").asBuilder() - .setClientCertificate(CertificateSamples.SAMPLE_CERT, clock.nowUtc()) - .setFailoverClientCertificate(CertificateSamples.SAMPLE_CERT2, clock.nowUtc()) - .build()); - assertCommandAndResponse("login_valid.xml", "login_response.xml"); - } - - @Test - public void testMissingPrimaryCertificateButHasFailover_usesFailover() throws Exception { - gfeRequestClientCertificateHashField = CertificateSamples.SAMPLE_CERT2_HASH; - persistResource(Registrar.loadByClientId("NewRegistrar").asBuilder() - .setClientCertificate(null, clock.nowUtc()) - .setFailoverClientCertificate(CertificateSamples.SAMPLE_CERT2, clock.nowUtc()) - .build()); - assertCommandAndResponse("login_valid.xml", "login_response.xml"); - } - - @Test - public void testRegistrarHasNoCertificatesOnFile_disablesCertChecking() throws Exception { - gfeRequestClientCertificateHashField = "laffo"; - persistResource(Registrar.loadByClientId("NewRegistrar").asBuilder() - .setClientCertificate(null, clock.nowUtc()) - .setFailoverClientCertificate(null, clock.nowUtc()) - .build()); - assertCommandAndResponse("login_valid.xml", "login_response.xml"); - } - @Test public void testNameserversTransferWithDomain_successfully() throws Exception { // Log in as the first registrar and set up domains with hosts. @@ -299,15 +428,14 @@ public class EppTlsServletTest extends EppServletXmlLoginTestCase assertCommandAndResponse( "domain_transfer_request_2_years.xml", "domain_transfer_response_2_years.xml", - "2002-05-30T00:00:00Z"); + DateTime.parse("2002-05-30T00:00:00Z")); assertCommandAndResponse("logout.xml", "logout_response.xml"); // Log back in as the first registrar and verify domain is pending transfer. assertCommandAndResponse("login_valid.xml", "login_response.xml"); - assertCommandAndResponse( - "domain_info_fakesite.xml", + assertCommandAndResponse("domain_info_fakesite.xml", "domain_info_response_fakesite_3_nameservers_pending_transfer.xml", - "2002-05-30T01:00:00Z"); + DateTime.parse("2002-05-30T01:00:00Z")); assertCommandAndResponse("logout.xml", "logout_response.xml"); // Log back in as second registrar and verify transfer was successful. @@ -316,14 +444,14 @@ public class EppTlsServletTest extends EppServletXmlLoginTestCase assertCommandAndResponse( "domain_info_fakesite.xml", "domain_info_response_fakesite_3_nameservers_transfer_successful.xml", - "2002-06-09T00:00:00Z"); + DateTime.parse("2002-06-09T00:00:00Z")); // Verify that host's client ID was set to the new registrar and has the transfer date set. assertCommandAndResponse( "host_info_fakesite.xml", null, "host_info_response_fakesite_post_transfer.xml", ImmutableMap.of("trDate", "2002-06-04T00:00:00Z"), - "2002-06-09T00:01:00Z"); + DateTime.parse("2002-06-09T00:01:00Z")); assertCommandAndResponse("logout.xml", "logout_response.xml"); } @@ -339,7 +467,7 @@ public class EppTlsServletTest extends EppServletXmlLoginTestCase assertCommandAndResponse( "domain_transfer_request_2_years.xml", "domain_transfer_response_2_years.xml", - "2002-05-30T00:00:00Z"); + DateTime.parse("2002-05-30T00:00:00Z")); assertCommandAndResponse("logout.xml", "logout_response.xml"); // Log back in as the first registrar and delete then restore the domain while the transfer @@ -348,26 +476,26 @@ public class EppTlsServletTest extends EppServletXmlLoginTestCase assertCommandAndResponse( "domain_info_fakesite.xml", "domain_info_response_fakesite_pending_transfer.xml", - "2002-05-30T01:00:00Z"); + DateTime.parse("2002-05-30T01:00:00Z")); assertCommandAndResponse( "domain_delete_fakesite.xml", "generic_success_action_pending_response.xml", - "2002-05-30T01:01:00Z"); + DateTime.parse("2002-05-30T01:01:00Z")); assertCommandAndResponse( "domain_info_fakesite.xml", "domain_info_response_fakesite_pending_delete.xml", - "2002-05-30T01:02:00Z"); + DateTime.parse("2002-05-30T01:02:00Z")); assertCommandAndResponse( "domain_update_restore_fakesite.xml", "domain_update_restore_request_response.xml", - "2002-05-30T01:03:00Z"); + DateTime.parse("2002-05-30T01:03:00Z")); // Expect domain is ok now, not pending delete or transfer, and has been extended by a year from // the date of the restore. (Not from the original expiration date.) assertCommandAndResponse( "domain_info_fakesite.xml", "domain_info_response_fakesite_restored_ok.xml", - "2002-05-30T01:04:00Z"); + DateTime.parse("2002-05-30T01:04:00Z")); assertCommandAndResponse("logout.xml", "logout_response.xml"); } @@ -379,15 +507,14 @@ public class EppTlsServletTest extends EppServletXmlLoginTestCase assertCommandAndResponse( "domain_transfer_query_fakesite.xml", "domain_transfer_query_response_no_transfer_history.xml", - "2000-09-02T00:00:00Z"); + DateTime.parse("2000-09-02T00:00:00Z")); assertCommandAndResponse("logout.xml", "logout_response.xml"); // Request a transfer of the domain to the second registrar. assertCommandAndResponse("login2_valid.xml", "login_response.xml"); assertCommandAndResponse( "domain_transfer_request_1_year.xml", - "domain_transfer_response_1_year.xml", - "2001-01-01T00:00:00Z"); + "domain_transfer_response_1_year.xml", DateTime.parse("2001-01-01T00:00:00Z")); assertCommandAndResponse("logout.xml", "logout_response.xml"); assertCommandAndResponse("login_valid.xml", "login_response.xml"); @@ -395,12 +522,12 @@ public class EppTlsServletTest extends EppServletXmlLoginTestCase assertCommandAndResponse( "domain_transfer_query_fakesite.xml", "domain_transfer_query_response_fakesite.xml", - "2001-01-02T00:00:00Z"); + DateTime.parse("2001-01-02T00:00:00Z")); // Verify that status went from 'pending' to 'serverApproved'. assertCommandAndResponse( "domain_transfer_query_fakesite.xml", "domain_transfer_query_response_completed_fakesite.xml", - "2001-01-08T00:00:00Z"); + DateTime.parse("2001-01-08T00:00:00Z")); assertCommandAndResponse("logout.xml", "logout_response.xml"); } @@ -419,25 +546,25 @@ public class EppTlsServletTest extends EppServletXmlLoginTestCase assertCommandAndResponse( "domain_transfer_query_fakesite.xml", "domain_transfer_query_response_no_transfer_history.xml", - "2000-09-02T00:00:00Z"); + DateTime.parse("2000-09-02T00:00:00Z")); assertCommandAndResponse("logout.xml", "logout_response.xml"); // Request a transfer of the domain to the second registrar. assertCommandAndResponse("login2_valid.xml", "login_response.xml"); assertCommandAndResponse( "domain_transfer_request_1_year.xml", "domain_transfer_response_1_year.xml", - "2001-01-01T00:00:00Z"); + DateTime.parse("2001-01-01T00:00:00Z")); // Verify that the lastTransferTime now reflects the superordinate domain's transfer. assertCommandAndResponse( "host_info.xml", ImmutableMap.of("hostname", "ns3.fakesite.example"), "host_info_response_fakesite_post_transfer.xml", ImmutableMap.of("trDate", "2001-01-06T00:00:00.000Z"), - "2001-01-07T00:00:00Z"); + DateTime.parse("2001-01-07T00:00:00Z")); assertCommandAndResponse( "domain_create_secondsite.xml", "domain_create_response_secondsite.xml", - "2001-01-08T00:00:00Z"); + DateTime.parse("2001-01-08T00:00:00Z")); // Update the host to be subordinate to a different domain by renaming it to // ns3.secondsite.example assertCommandAndResponse( @@ -445,7 +572,7 @@ public class EppTlsServletTest extends EppServletXmlLoginTestCase ImmutableMap.of("oldName", "ns3.fakesite.example", "newName", "ns3.secondsite.example"), "generic_success_response.xml", null, - "2002-05-30T01:01:00Z"); + DateTime.parse("2002-05-30T01:01:00Z")); // The last transfer time on the host should still be what it was from the transfer. assertCommandAndResponse( "host_info.xml", @@ -454,7 +581,7 @@ public class EppTlsServletTest extends EppServletXmlLoginTestCase ImmutableMap.of( "hostname", "ns3.secondsite.example", "trDate", "2001-01-06T00:00:00.000Z"), - "2003-01-07T00:00:00Z"); + DateTime.parse("2003-01-07T00:00:00Z")); assertCommandAndResponse("logout.xml", "logout_response.xml"); } @@ -472,21 +599,21 @@ public class EppTlsServletTest extends EppServletXmlLoginTestCase assertCommandAndResponse( "domain_transfer_query_fakesite.xml", "domain_transfer_query_response_no_transfer_history.xml", - "2000-09-02T00:00:00Z"); + DateTime.parse("2000-09-02T00:00:00Z")); assertCommandAndResponse("logout.xml", "logout_response.xml"); // Request a transfer of the domain to the second registrar. assertCommandAndResponse("login2_valid.xml", "login_response.xml"); assertCommandAndResponse( "domain_transfer_request_1_year.xml", "domain_transfer_response_1_year.xml", - "2001-01-01T00:00:00Z"); + DateTime.parse("2001-01-01T00:00:00Z")); // Verify that the lastTransferTime now reflects the superordinate domain's transfer. assertCommandAndResponse( "host_info_fakesite.xml", null, "host_info_response_fakesite_post_transfer.xml", ImmutableMap.of("trDate", "2001-01-06T00:00:00.000Z"), - "2001-01-07T00:00:00Z"); + DateTime.parse("2001-01-07T00:00:00Z")); // Update the host to be external by renaming it to ns3.notarealsite.external assertCommandAndResponse( "host_update_rename_and_remove_addresses.xml", @@ -495,7 +622,7 @@ public class EppTlsServletTest extends EppServletXmlLoginTestCase "newName", "ns3.notarealsite.external"), "generic_success_response.xml", null, - "2002-05-30T01:01:00Z"); + DateTime.parse("2002-05-30T01:01:00Z")); // The last transfer time on the host should still be what it was from the transfer. assertCommandAndResponse( "host_info.xml", @@ -504,7 +631,7 @@ public class EppTlsServletTest extends EppServletXmlLoginTestCase ImmutableMap.of( "hostname", "ns3.notarealsite.external", "trDate", "2001-01-06T00:00:00.000Z"), - "2001-01-07T00:00:00Z"); + DateTime.parse("2001-01-07T00:00:00Z")); assertCommandAndResponse("logout.xml", "logout_response.xml"); } } diff --git a/javatests/google/registry/flows/EppLifecycleHostTest.java b/javatests/google/registry/flows/EppLifecycleHostTest.java new file mode 100644 index 000000000..daa1b9f8c --- /dev/null +++ b/javatests/google/registry/flows/EppLifecycleHostTest.java @@ -0,0 +1,71 @@ +// Copyright 2016 The Domain Registry 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 google.registry.testing.AppEngineRule; + +import org.joda.time.DateTime; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +/** Tests for host lifecycle. */ +@RunWith(JUnit4.class) +public class EppLifecycleHostTest extends EppTestCase { + + @Rule + public final AppEngineRule appEngine = AppEngineRule.builder() + .withDatastore() + .build(); + + @Test + public void testRenamingHostToExistingHost_fails() throws Exception { + assertCommandAndResponse("login_valid.xml", "login_response.xml"); + // Create the two hosts. + assertCommandAndResponse( + "host_create.xml", + "host_create_response.xml", + DateTime.parse("2000-06-01T00:02:00Z")); + assertCommandAndResponse( + "host_create2.xml", + "host_create2_response.xml", + DateTime.parse("2000-06-01T00:03:00Z")); + // Verify that host1 and host2 were created as we expect them. + assertCommandAndResponse( + "host_info_ns1.xml", + "host_info_response_ns1.xml", + DateTime.parse("2000-06-01T00:04:00Z")); + assertCommandAndResponse( + "host_info_ns2.xml", + "host_info_response_ns2.xml", + DateTime.parse("2000-06-01T00:05:00Z")); + // Attempt overwriting of host1 on top of host2 (and verify that it fails). + assertCommandAndResponse( + "host_update_ns1_to_ns2.xml", + "host_update_failed_response.xml", + DateTime.parse("2000-06-01T00:06:00Z")); + // Verify that host1 and host2 still exist in their unmodified states. + assertCommandAndResponse( + "host_info_ns1.xml", + "host_info_response_ns1.xml", + DateTime.parse("2000-06-01T00:07:00Z")); + assertCommandAndResponse( + "host_info_ns2.xml", + "host_info_response_ns2.xml", + DateTime.parse("2000-06-01T00:08:00Z")); + assertCommandAndResponse("logout.xml", "logout_response.xml"); + } +} diff --git a/javatests/google/registry/flows/EppLoggedOutTest.java b/javatests/google/registry/flows/EppLoggedOutTest.java new file mode 100644 index 000000000..f8a9f0689 --- /dev/null +++ b/javatests/google/registry/flows/EppLoggedOutTest.java @@ -0,0 +1,54 @@ +// Copyright 2016 The Domain Registry 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 org.joda.time.DateTimeZone.UTC; +import static org.joda.time.format.ISODateTimeFormat.dateTimeNoMillis; + +import com.google.common.collect.ImmutableMap; + +import google.registry.testing.AppEngineRule; + +import org.joda.time.DateTime; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +/** Test flows without login. */ +@RunWith(JUnit4.class) +public class EppLoggedOutTest extends EppTestCase { + + @Rule + public final AppEngineRule appEngine = AppEngineRule.builder() + .withDatastore() + .build(); + + @Test + public void testHello() throws Exception { + DateTime now = DateTime.now(UTC); + assertCommandAndResponse( + "hello.xml", + null, + "greeting_crr.xml", + ImmutableMap.of("DATE", now.toString(dateTimeNoMillis())), + now); + } + + @Test + public void testSyntaxError() throws Exception { + assertCommandAndResponse("syntax_error.xml", "syntax_error_response.xml"); + } +} diff --git a/javatests/google/registry/flows/EppConsoleAsAdminServletTest.java b/javatests/google/registry/flows/EppLoginAdminUserTest.java similarity index 57% rename from javatests/google/registry/flows/EppConsoleAsAdminServletTest.java rename to javatests/google/registry/flows/EppLoginAdminUserTest.java index 55cf18af6..95c824749 100644 --- a/javatests/google/registry/flows/EppConsoleAsAdminServletTest.java +++ b/javatests/google/registry/flows/EppLoginAdminUserTest.java @@ -14,10 +14,8 @@ package google.registry.flows; +import static com.google.appengine.api.users.UserServiceFactory.getUserService; -import static google.registry.testing.DatastoreHelper.persistResource; - -import google.registry.model.registrar.Registrar; import google.registry.testing.AppEngineRule; import google.registry.testing.UserInfo; @@ -25,37 +23,39 @@ import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; -import org.mockito.runners.MockitoJUnitRunner; +import org.junit.runners.JUnit4; -/** Tests for {@link EppConsoleServlet} running in admin mode. */ -@RunWith(MockitoJUnitRunner.class) -public class EppConsoleAsAdminServletTest extends EppServletXmlLoginTestCase { +/** Test logging in with appengine admin user credentials. */ +@RunWith(JUnit4.class) +public class EppLoginAdminUserTest extends EppTestCase { @Rule public final AppEngineRule appEngine = AppEngineRule.builder() .withDatastore() - .withTaskQueue() - .withUserService(UserInfo.createAdmin(GAE_USER_EMAIL, GAE_USER_ID)) + .withUserService(UserInfo.createAdmin("someone@example.com", "12345")) .build(); - private static final String GAE_USER_ID = "12345"; - private static final String GAE_USER_EMAIL = "someone@example.com"; - - // Note that the setup done in EppConsoleServletTest, of allowing - // the test user to login as the Registrar, is not done here. @Before - public void initTest() throws Exception { - persistResource( - Registrar.loadByClientId("NewRegistrar").asBuilder().setPassword("PwAdminDNKnow").build()); + public void initTransportCredentials() { + setTransportCredentials(new GaeUserCredentials(getUserService().getCurrentUser())); } @Test - public void testNonAuthedLogin() throws Exception { + public void testNonAuthedLogin_succeedsAsAdmin() throws Exception { + // Login succeeds even though this user isn't listed on the registrar. assertCommandAndResponse("login2_valid.xml", "login_response.xml"); } @Test - public void testMultiLogin() throws Exception { + public void testLoginLogout_wrongPasswordStillWorks() throws Exception { + // For user-based logins the password in the epp xml is ignored. + assertCommandAndResponse("login_invalid_wrong_password.xml", "login_response.xml"); + assertCommandAndResponse("logout.xml", "logout_response.xml"); + } + + @Test + public void testNonAuthedMultiLogin_succeedsAsAdmin() throws Exception { + // The admin can log in as different registrars. assertCommandAndResponse("login_valid.xml", "login_response.xml"); assertCommandAndResponse("logout.xml", "logout_response.xml"); assertCommandAndResponse("login_valid.xml", "login_response.xml"); diff --git a/javatests/google/registry/flows/EppLoginTlsTest.java b/javatests/google/registry/flows/EppLoginTlsTest.java new file mode 100644 index 000000000..c936d04a6 --- /dev/null +++ b/javatests/google/registry/flows/EppLoginTlsTest.java @@ -0,0 +1,146 @@ +// Copyright 2016 The Domain Registry 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 google.registry.testing.DatastoreHelper.persistResource; +import static org.joda.time.DateTimeZone.UTC; + +import com.google.common.base.Optional; + +import google.registry.model.registrar.Registrar; +import google.registry.testing.AppEngineRule; +import google.registry.testing.CertificateSamples; + +import org.joda.time.DateTime; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +/** Test logging in with TLS credentials. */ +@RunWith(JUnit4.class) +public class EppLoginTlsTest extends EppTestCase { + + @Rule + public final AppEngineRule appEngine = AppEngineRule.builder() + .withDatastore() + .build(); + + + void setClientCertificateHash(String clientCertificateHash) { + setTransportCredentials(new TlsCredentials( + clientCertificateHash, Optional.of("192.168.1.100:54321"), "test.example")); + } + + @Before + public void initTest() throws Exception { + persistResource(Registrar.loadByClientId("NewRegistrar").asBuilder() + .setClientCertificateHash(CertificateSamples.SAMPLE_CERT_HASH) + .build()); + // Set a cert for the second registrar, or else any cert will be allowed for login. + persistResource(Registrar.loadByClientId("TheRegistrar").asBuilder() + .setClientCertificateHash(CertificateSamples.SAMPLE_CERT2_HASH) + .build()); + } + + @Test + public void testLoginLogout() throws Exception { + setClientCertificateHash(CertificateSamples.SAMPLE_CERT_HASH); + assertCommandAndResponse("login_valid.xml", "login_response.xml"); + assertCommandAndResponse("logout.xml", "logout_response.xml"); + } + + @Test + public void testLogin_wrongPasswordFails() throws Exception { + setClientCertificateHash(CertificateSamples.SAMPLE_CERT_HASH); + // For TLS login, we also check the epp xml password. + assertCommandAndResponse( + "login_invalid_wrong_password.xml", "login_response_wrong_password.xml"); + } + + @Test + public void testMultiLogin() throws Exception { + setClientCertificateHash(CertificateSamples.SAMPLE_CERT_HASH); + assertCommandAndResponse("login_valid.xml", "login_response.xml"); + assertCommandAndResponse("logout.xml", "logout_response.xml"); + assertCommandAndResponse("login_valid.xml", "login_response.xml"); + assertCommandAndResponse("logout.xml", "logout_response.xml"); + assertCommandAndResponse("login2_valid.xml", "login_response_bad_certificate.xml"); + } + + @Test + public void testNonAuthedLogin_fails() throws Exception { + setClientCertificateHash(CertificateSamples.SAMPLE_CERT_HASH); + assertCommandAndResponse("login2_valid.xml", "login_response_bad_certificate.xml"); + } + + + @Test + public void testBadCertificate_failsBadCertificate2200() throws Exception { + setClientCertificateHash("laffo"); + assertCommandAndResponse("login_valid.xml", "login_response_bad_certificate.xml"); + } + + @Test + public void testGfeDidntProvideClientCertificate_failsMissingCertificate2200() throws Exception { + setClientCertificateHash(""); + assertCommandAndResponse("login_valid.xml", "login_response_missing_certificate.xml"); + } + + @Test + public void testGoodPrimaryCertificate() throws Exception { + setClientCertificateHash(CertificateSamples.SAMPLE_CERT_HASH); + DateTime now = DateTime.now(UTC); + persistResource(Registrar.loadByClientId("NewRegistrar").asBuilder() + .setClientCertificate(CertificateSamples.SAMPLE_CERT, now) + .setFailoverClientCertificate(CertificateSamples.SAMPLE_CERT2, now) + .build()); + assertCommandAndResponse("login_valid.xml", "login_response.xml"); + } + + @Test + public void testGoodFailoverCertificate() throws Exception { + setClientCertificateHash(CertificateSamples.SAMPLE_CERT2_HASH); + DateTime now = DateTime.now(UTC); + persistResource(Registrar.loadByClientId("NewRegistrar").asBuilder() + .setClientCertificate(CertificateSamples.SAMPLE_CERT, now) + .setFailoverClientCertificate(CertificateSamples.SAMPLE_CERT2, now) + .build()); + assertCommandAndResponse("login_valid.xml", "login_response.xml"); + } + + @Test + public void testMissingPrimaryCertificateButHasFailover_usesFailover() throws Exception { + setClientCertificateHash(CertificateSamples.SAMPLE_CERT2_HASH); + DateTime now = DateTime.now(UTC); + persistResource(Registrar.loadByClientId("NewRegistrar").asBuilder() + .setClientCertificate(null, now) + .setFailoverClientCertificate(CertificateSamples.SAMPLE_CERT2, now) + .build()); + assertCommandAndResponse("login_valid.xml", "login_response.xml"); + } + + @Test + public void testRegistrarHasNoCertificatesOnFile_disablesCertChecking() throws Exception { + setClientCertificateHash("laffo"); + DateTime now = DateTime.now(UTC); + persistResource(Registrar.loadByClientId("NewRegistrar").asBuilder() + .setClientCertificate(null, now) + .setFailoverClientCertificate(null, now) + .build()); + assertCommandAndResponse("login_valid.xml", "login_response.xml"); + } +} diff --git a/javatests/google/registry/flows/EppConsoleServletTest.java b/javatests/google/registry/flows/EppLoginUserTest.java similarity index 59% rename from javatests/google/registry/flows/EppConsoleServletTest.java rename to javatests/google/registry/flows/EppLoginUserTest.java index 317278ab7..031d7ea4d 100644 --- a/javatests/google/registry/flows/EppConsoleServletTest.java +++ b/javatests/google/registry/flows/EppLoginUserTest.java @@ -14,9 +14,10 @@ package google.registry.flows; - +import static com.google.appengine.api.users.UserServiceFactory.getUserService; import static google.registry.testing.DatastoreHelper.persistResource; +import com.google.appengine.api.users.User; import com.google.common.collect.ImmutableSet; import google.registry.model.registrar.Registrar; @@ -28,36 +29,38 @@ import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; -import org.mockito.runners.MockitoJUnitRunner; +import org.junit.runners.JUnit4; -/** Tests for {@link EppConsoleServlet}. */ -@RunWith(MockitoJUnitRunner.class) -public class EppConsoleServletTest extends EppServletXmlLoginTestCase { +/** Test logging in with appengine user credentials, such as via the console. */ +@RunWith(JUnit4.class) +public class EppLoginUserTest extends EppTestCase { @Rule public final AppEngineRule appEngine = AppEngineRule.builder() .withDatastore() - .withTaskQueue() - .withUserService(UserInfo.create(GAE_USER_EMAIL, GAE_USER_ID)) + .withUserService(UserInfo.create("person@example.com", "12345")) .build(); - private static final String GAE_USER_ID = "12345"; - private static final String GAE_USER_EMAIL = "person@example.com"; - @Before public void initTest() throws Exception { - Registrar registrar = Registrar.loadByClientId("NewRegistrar"); - RegistrarContact contact = new RegistrarContact.Builder() - .setParent(registrar) - .setEmailAddress(GAE_USER_EMAIL) + User user = getUserService().getCurrentUser(); + persistResource(new RegistrarContact.Builder() + .setParent(Registrar.loadByClientId("NewRegistrar")) + .setEmailAddress(user.getEmail()) + .setGaeUserId(user.getUserId()) .setTypes(ImmutableSet.of(RegistrarContact.Type.ADMIN)) - .setGaeUserId(GAE_USER_ID) - .build(); - persistResource(contact); + .build()); + setTransportCredentials(new GaeUserCredentials(user)); } @Test - public void testNonAuthedLogin() throws Exception { + public void testLoginLogout() throws Exception { + assertCommandAndResponse("login_valid.xml", "login_response.xml"); + assertCommandAndResponse("logout.xml", "logout_response.xml"); + } + + @Test + public void testNonAuthedLogin_fails() throws Exception { assertCommandAndResponse("login2_valid.xml", "login_response_unauthorized_role.xml"); } @@ -69,4 +72,11 @@ public class EppConsoleServletTest extends EppServletXmlLoginTestCase The EppXXXServlet class to test. - */ -public abstract class EppServletTestCase { - - @Rule - public final InjectRule inject = new InjectRule(); - - @Mock - HttpServletRequest req; - - @Mock - HttpServletResponse rsp; - - @Mock - ModulesService modulesService; - - HttpSession session; - - FakeClock clock = new FakeClock(); - - private String currentTld = null; - private Optional isSuperuser = Optional. absent(); - private Optional clientIdentifier = Optional. absent(); - - void setSuperuser(boolean isSuperuser) { - this.isSuperuser = Optional.of(isSuperuser); - } - - void setClientIdentifier(String clientIdentifier) { - this.clientIdentifier = Optional.of(clientIdentifier); - } - - static final DateTime START_OF_GA = DateTime.parse("2014-03-01T00:00:00Z"); - - @Before - public final void init() throws Exception { - inject.setStaticField(Ofy.class, "clock", clock); // For transactional flows. - inject.setStaticField(FlowRunner.class, "clock", clock); // For non-transactional flows. - inject.setStaticField(Metrics.class, "modulesService", modulesService); - when(modulesService.getVersionHostname("backend", null)).thenReturn("backend.hostname"); - - // Create RegistryData for all TLDs used in these tests. - // We want to create all of these even for tests that don't use them to make sure that - // tld-selection works correctly. - createTlds("net", "xn--q9jyb4c", "example"); - ofy().saveWithoutBackup().entity(new ClaimsListSingleton()).now(); - - session = new BasicHttpSession(); - persistResource( - Registrar.loadByClientId("NewRegistrar") - .asBuilder() - .setAllowedTlds(ImmutableSet.of("net", "example", "xn--q9jyb4c")) - .build()); - - persistResource( - Registrar.loadByClientId("TheRegistrar") - .asBuilder() - .setAllowedTlds(ImmutableSet.of("net", "example", "xn--q9jyb4c")) - .build()); - } - - void assertCommandAndResponse( - String inputFilename, - Map inputSubstitutions, - String outputFilename, - Map outputSubstitutions) throws Exception { - assertCommandAndResponse( - inputFilename, - inputSubstitutions, - outputFilename, - outputSubstitutions, - DateTime.now(UTC)); - } - - String assertCommandAndResponse(String inputFilename, String outputFilename) throws Exception { - return assertCommandAndResponse(inputFilename, outputFilename, DateTime.now(UTC)); - } - - String assertCommandAndResponse( - String inputFilename, - Map inputSubstitutions, - String outputFilename, - Map outputSubstitutions, - String nowString) throws Exception { - return assertCommandAndResponse( - inputFilename, - inputSubstitutions, - outputFilename, - outputSubstitutions, - DateTime.parse(nowString)); - } - - String assertCommandAndResponse(String inputFilename, String outputFilename, String nowString) - throws Exception { - return assertCommandAndResponse(inputFilename, outputFilename, DateTime.parse(nowString)); - } - - String assertCommandAndResponse(String inputFilename, String outputFilename, DateTime now) - throws Exception { - return assertCommandAndResponse(inputFilename, null, outputFilename, null, now); - } - - String assertCommandAndResponse( - String inputFilename, - Map inputSubstitutions, - String outputFilename, - Map outputSubstitutions, - DateTime now) throws Exception { - String outputFile = - loadFileWithSubstitutions(EppServletTestCase.class, outputFilename, outputSubstitutions); - String actualOutput = expectXmlCommand(loadFileWithSubstitutions( - EppServletTestCase.class, inputFilename, inputSubstitutions), now); - assertXmlEqualsWithMessage( - outputFile, - actualOutput, - "Running " + inputFilename + " => " + outputFilename, - "epp.response.resData.infData.roid", - "epp.response.trID.svTRID"); - ofy().clearSessionCache(); // Clear the cache like OfyFilter would. - return actualOutput; - } - - HttpSession getOrRenewSession() { - // Try an idempotent op on the session to see if it's valid. - try { - session.getAttribute(null); - return session; - } catch (IllegalStateException e) { - // Session is invalid. - session = new BasicHttpSession(); - return session; - } - } - - @SuppressWarnings("resource") - String expectXmlCommand(String inputFile, DateTime now) throws Exception { - clock.setTo(now); // Makes Ofy use 'now' as its time - reset(req, rsp); - HttpServlet servlet = new TypeInstantiator(getClass()){}.instantiate(); - if (servlet instanceof XsrfProtectedServlet) { - when(req.getHeader(X_CSRF_TOKEN)) - .thenReturn(generateToken(((XsrfProtectedServlet) servlet).getScope())); - } - when(req.getInputStream()).thenReturn(new FakeServletInputStream(inputFile.getBytes(UTF_8))); - when(req.getParameter("xml")).thenReturn(inputFile); - if (isSuperuser.isPresent()) { - when(req.getParameter("superuser")).thenReturn(isSuperuser.get().toString()); - } - if (clientIdentifier.isPresent()) { - when(req.getParameter("clientIdentifier")).thenReturn(clientIdentifier.get()); - } - when(req.getParameter("tld")).thenReturn(currentTld); - when(req.getServletPath()).thenReturn(""); - when(req.getMethod()).thenReturn("POST"); - when(req.getHeader("X-Requested-With")).thenReturn("XMLHttpRequest"); - when(req.getSession(true)).thenAnswer(new Answer() { - @Override - public HttpSession answer(InvocationOnMock invocation) { - return getOrRenewSession(); - }}); - final ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); - when(rsp.getOutputStream()).thenReturn(new ServletOutputStream() { - @Override - public void write(int b) { - byteArrayOutputStream.write(b); - }}); - extendedSessionConfig(inputFile); - - servlet.init(mock(ServletConfig.class)); - servlet.service(req, rsp); - verify(rsp).setStatus(HttpServletResponse.SC_OK); - String result = new String(byteArrayOutputStream.toByteArray(), UTF_8); - // Run the resulting xml through the unmarshaller to verify that it was valid. - EppXmlTransformer.validateOutput(result); - return result; - } - - /** Create the two administrative contacts and two hosts that are used by a lot of our tests. */ - protected void createContactsAndHosts() throws Exception { - DateTime startTime = DateTime.parse("2000-06-01T00:00:00Z"); - assertCommandAndResponse( - "contact_create_sh8013.xml", - ImmutableMap.of(), - "contact_create_response_sh8013.xml", - ImmutableMap.of("CRDATE", "2000-06-01T00:00:00Z"), - startTime); - assertCommandAndResponse( - "contact_create_jd1234.xml", - "contact_create_response_jd1234.xml", - startTime.plusMinutes(1)); - assertCommandAndResponse( - "host_create.xml", - "host_create_response.xml", - startTime.plusMinutes(2)); - assertCommandAndResponse( - "host_create2.xml", - "host_create2_response.xml", - startTime.plusMinutes(3)); - } - - /** - * Creates the domain fakesite.example with two nameservers on it. - */ - protected void createFakesite() throws Exception { - createContactsAndHosts(); - assertCommandAndResponse( - "domain_create_fakesite.xml", - "domain_create_response_fakesite.xml", - "2000-06-01T00:04:00Z"); - assertCommandAndResponse( - "domain_info_fakesite.xml", - "domain_info_response_fakesite_ok.xml", - "2000-06-06T00:00:00Z"); - } - - // Adds ns3.fakesite.example as a host, then adds it to fakesite. - protected void createSubordinateHost() throws Exception { - // Add the fakesite nameserver (requires that domain is already created). - assertCommandAndResponse( - "host_create_fakesite.xml", - "host_create_response_fakesite.xml", - "2000-06-06T00:01:00Z"); - // Add new nameserver to domain. - assertCommandAndResponse( - "domain_update_add_nameserver_fakesite.xml", - "domain_update_add_nameserver_response_fakesite.xml", - "2000-06-08T00:00:00Z"); - // Verify new nameserver was added. - assertCommandAndResponse( - "domain_info_fakesite.xml", - "domain_info_response_fakesite_3_nameservers.xml", - "2000-06-08T00:01:00Z"); - // Verify that nameserver's data was set correctly. - assertCommandAndResponse( - "host_info_fakesite.xml", - "host_info_response_fakesite.xml", - "2000-06-08T00:02:00Z"); - } - - /** For subclasses to further setup the session. */ - protected void extendedSessionConfig( - @SuppressWarnings("unused") String inputFile) throws Exception {} -} diff --git a/javatests/google/registry/flows/EppServletXmlLoginTestCase.java b/javatests/google/registry/flows/EppServletXmlLoginTestCase.java deleted file mode 100644 index 65dc8960f..000000000 --- a/javatests/google/registry/flows/EppServletXmlLoginTestCase.java +++ /dev/null @@ -1,358 +0,0 @@ -// Copyright 2016 The Domain Registry 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 google.registry.testing.DatastoreHelper.createTld; -import static google.registry.util.DateTimeUtils.START_OF_TIME; -import static org.joda.time.DateTimeZone.UTC; - -import com.google.common.collect.ImmutableMap; -import com.google.common.collect.ImmutableSortedMap; - -import google.registry.model.registry.Registry.TldState; -import google.registry.util.DateTimeUtils; - -import org.joda.time.DateTime; -import org.joda.time.format.ISODateTimeFormat; -import org.junit.Test; - -import javax.servlet.http.HttpServlet; - -/** - * Test setup for EppServletTest subclasses which use XML-based authentication. - * - * @param The EppXXXServlet class to test. - */ -public abstract class EppServletXmlLoginTestCase extends - EppServletTestCase { - - @Test - public void testHello() throws Exception { - DateTime now = DateTime.now(UTC); - assertCommandAndResponse( - "hello.xml", - null, - "greeting_crr.xml", - ImmutableMap.of("DATE", now.toString(ISODateTimeFormat.dateTimeNoMillis())), - now); - } - - @Test - public void testLoginLogout() throws Exception { - assertCommandAndResponse("login_valid.xml", "login_response.xml"); - assertCommandAndResponse("logout.xml", "logout_response.xml"); - } - - @Test - public void testPdtLogin() throws Exception { - assertCommandAndResponse("pdt_login.xml", "login_response.xml"); - assertCommandAndResponse("logout.xml", "logout_response.xml"); - } - - @Test - public void testSyntaxError() throws Exception { - assertCommandAndResponse("syntax_error.xml", "syntax_error_response.xml"); - } - - @Test - public void testContactLifecycle() throws Exception { - assertCommandAndResponse("login_valid.xml", "login_response.xml"); - assertCommandAndResponse( - "contact_create_sh8013.xml", - ImmutableMap.of(), - "contact_create_response_sh8013.xml", - ImmutableMap.of("CRDATE", "2000-06-01T00:00:00Z"), - "2000-06-01T00:00:00Z"); - assertCommandAndResponse( - "contact_info.xml", - "contact_info_from_create_response.xml", - "2000-06-01T00:01:00Z"); - assertCommandAndResponse("contact_delete_sh8013.xml", "contact_delete_response_sh8013.xml"); - assertCommandAndResponse("logout.xml", "logout_response.xml"); - } - - @Test - public void testDomainDeleteRestore() throws Exception { - assertCommandAndResponse("login_valid.xml", "login_response.xml"); - // Create contacts sh8013 and jd1234. - assertCommandAndResponse( - "contact_create_sh8013.xml", - ImmutableMap.of(), - "contact_create_response_sh8013.xml", - ImmutableMap.of("CRDATE", "2000-06-01T00:00:00Z"), - "2000-06-01T00:00:00Z"); - - assertCommandAndResponse( - "contact_create_jd1234.xml", - "contact_create_response_jd1234.xml", - "2000-06-01T00:01:00Z"); - - // Create domain example.tld. - assertCommandAndResponse( - "domain_create_no_hosts_or_dsdata.xml", - "domain_create_response.xml", - "2000-06-01T00:02:00Z"); - - // Delete domain example.com after its add grace period has expired. - assertCommandAndResponse( - "domain_delete.xml", - "generic_success_action_pending_response.xml", - "2000-07-01T00:02:00Z"); - - // Restore the domain. - assertCommandAndResponse( - "domain_update_restore_request.xml", - "domain_update_restore_request_response.xml", - "2000-07-01T00:03:00Z"); - - assertCommandAndResponse("logout.xml", "logout_response.xml"); - } - - @Test - public void testDomainDeletion_withinAddGracePeriod() throws Exception { - assertCommandAndResponse("login_valid.xml", "login_response.xml"); - - // Create contacts sh8013 and jd1234. - assertCommandAndResponse( - "contact_create_sh8013.xml", - ImmutableMap.of(), - "contact_create_response_sh8013.xml", - ImmutableMap.of("CRDATE", "2000-06-01T00:00:00Z"), - "2000-06-01T00:00:00Z"); - - assertCommandAndResponse( - "contact_create_jd1234.xml", - "contact_create_response_jd1234.xml", - "2000-06-01T00:01:00Z"); - - // Create domain example.tld. - assertCommandAndResponse( - "domain_create_no_hosts_or_dsdata.xml", - "domain_create_response.xml", - "2000-06-01T00:02:00Z"); - - // Delete domain example.tld after its add grace period has expired. - assertCommandAndResponse( - "domain_delete.xml", - "generic_success_action_pending_response.xml", - "2000-07-01T00:02:00Z"); - - // Poke the domain a little at various times to see its status - assertCommandAndResponse( - "domain_info.xml", - "domain_info_response_pendingdelete.xml", - "2000-08-01T00:02:00Z"); // 1 day out. - - assertCommandAndResponse("logout.xml", "logout_response.xml"); - } - - @Test - public void testDomainDeletionWithSubordinateHost_fails() throws Exception { - assertCommandAndResponse("login_valid.xml", "login_response.xml"); - createFakesite(); - createSubordinateHost(); - assertCommandAndResponse( - "domain_delete_fakesite.xml", - "domain_delete_response_prohibited.xml", - "2002-05-30T01:01:00Z"); - assertCommandAndResponse("logout.xml", "logout_response.xml"); - } - - @Test - public void testDeletionOfDomain_afterRenameOfSubordinateHost_succeeds() throws Exception { - assertCommandAndResponse("login_valid.xml", "login_response.xml"); - createFakesite(); - createSubordinateHost(); - // Update the ns3 host to no longer be on fakesite.example domain. - assertCommandAndResponse( - "host_update_fakesite.xml", - "generic_success_response.xml", - "2002-05-30T01:01:00Z"); - // Delete the fakesite.example domain (which should succeed since it no longer has subords). - assertCommandAndResponse( - "domain_delete_fakesite.xml", - "generic_success_action_pending_response.xml", - "2002-05-30T01:02:00Z"); - // Check info on the renamed host and verify that it's still around and wasn't deleted. - assertCommandAndResponse( - "host_info_ns9000_example.xml", - "host_info_response_ns9000_example.xml", - "2002-06-30T01:03:00Z"); - assertCommandAndResponse("logout.xml", "logout_response.xml"); - } - - @Test - public void testDeletionOfDomain_afterUpdateThatCreatesSubordinateHost_fails() throws Exception { - assertCommandAndResponse("login_valid.xml", "login_response.xml"); - createFakesite(); - // Update the ns1 host to be on the fakesite.example domain. - assertCommandAndResponse( - "host_update_ns1_to_fakesite.xml", - "generic_success_response.xml", - "2002-05-30T01:01:00Z"); - // Attempt to delete the fakesite.example domain (which should fail since it now has a - // subordinate host). - assertCommandAndResponse( - "domain_delete_fakesite.xml", - "domain_delete_response_prohibited.xml", - "2002-05-30T01:02:00Z"); - // Check info on the renamed host and verify that it's still around and wasn't deleted. - assertCommandAndResponse( - "host_info_fakesite.xml", - "host_info_response_fakesite_post_update.xml", - "2002-06-30T01:03:00Z"); - // Verify that fakesite.example domain is still around and wasn't deleted. - assertCommandAndResponse( - "domain_info_fakesite.xml", - "domain_info_response_fakesite_ok_post_host_update.xml", - "2002-05-30T01:00:00Z"); - assertCommandAndResponse("logout.xml", "logout_response.xml"); - } - - @Test - public void testRenamingHostToExistingHost_fails() throws Exception { - assertCommandAndResponse("login_valid.xml", "login_response.xml"); - // Create the two hosts. - assertCommandAndResponse( - "host_create.xml", - "host_create_response.xml", - "2000-06-01T00:02:00Z"); - assertCommandAndResponse( - "host_create2.xml", - "host_create2_response.xml", - "2000-06-01T00:03:00Z"); - // Verify that host1 and host2 were created as we expect them. - assertCommandAndResponse( - "host_info_ns1.xml", - "host_info_response_ns1.xml", - "2000-06-01T00:04:00Z"); - assertCommandAndResponse( - "host_info_ns2.xml", - "host_info_response_ns2.xml", - "2000-06-01T00:05:00Z"); - // Attempt overwriting of host1 on top of host2 (and verify that it fails). - assertCommandAndResponse( - "host_update_ns1_to_ns2.xml", - "host_update_failed_response.xml", - "2000-06-01T00:06:00Z"); - // Verify that host1 and host2 still exist in their unmodified states. - assertCommandAndResponse( - "host_info_ns1.xml", - "host_info_response_ns1.xml", - "2000-06-01T00:07:00Z"); - assertCommandAndResponse( - "host_info_ns2.xml", - "host_info_response_ns2.xml", - "2000-06-01T00:08:00Z"); - assertCommandAndResponse("logout.xml", "logout_response.xml"); - } - - @Test - public void testApplicationDuringSunrise_doesntCreateDomainWithoutAllocation() throws Exception { - ImmutableSortedMap transitions = ImmutableSortedMap.of( - DateTimeUtils.START_OF_TIME, TldState.SUNRISE, - START_OF_GA, TldState.GENERAL_AVAILABILITY); - createTld("example", transitions); - - assertCommandAndResponse("login_valid.xml", "login_response.xml"); - createContactsAndHosts(); - // Note that the trademark is valid from 2013-08-09 to 2017-07-23, hence the domain creation - // in 2014. - assertCommandAndResponse( - "domain_create_sunrise_encoded_mark.xml", - "domain_create_sunrise_encoded_signed_mark_response.xml", - "2014-01-01T00:00:00Z"); - assertCommandAndResponse( - "domain_info_testvalidate.xml", - "domain_info_response_testvalidate_doesnt_exist.xml", - "2014-01-01T00:01:00Z"); - - assertCommandAndResponse("logout.xml", "logout_response.xml"); - } - - @Test - public void testDomainCreation_failsBeforeSunrise() throws Exception { - assertCommandAndResponse("login_valid.xml", "login_response.xml"); - - DateTime sunriseDate = DateTime.parse("2000-05-30T00:00:00Z"); - ImmutableSortedMap transitions = ImmutableSortedMap.of( - START_OF_TIME, TldState.PREDELEGATION, - sunriseDate, TldState.SUNRISE, - sunriseDate.plusMonths(2), TldState.GENERAL_AVAILABILITY); - createTld("example", transitions); - - setClientIdentifier("NewRegistrar"); - createContactsAndHosts(); - assertCommandAndResponse( - "domain_create_sunrise_encoded_mark.xml", - "domain_create_testvalidate_invalid_phase.xml", - sunriseDate.minusDays(1)); - - assertCommandAndResponse( - "domain_info_testvalidate.xml", - "domain_info_response_testvalidate_doesnt_exist.xml", - sunriseDate.plusDays(1)); - - assertCommandAndResponse("logout.xml", "logout_response.xml"); - } - - @Test - public void testDomainCheckFee_succeeds() throws Exception { - assertCommandAndResponse("login_valid_fee_extension.xml", "login_response.xml"); - - DateTime gaDate = DateTime.parse("2000-05-30T00:00:00Z"); - ImmutableSortedMap transitions = ImmutableSortedMap.of( - START_OF_TIME, TldState.PREDELEGATION, - gaDate, TldState.GENERAL_AVAILABILITY); - createTld("example", transitions); - - assertCommandAndResponse( - "domain_check_fee_premium.xml", - "domain_check_fee_premium_response.xml", - gaDate.plusDays(1)); - - assertCommandAndResponse("logout.xml", "logout_response.xml"); - } - - @Test - public void testRemoteXmlExternalEntity() throws Exception { - // Check go/XXE - assertCommandAndResponse("login_valid.xml", "login_response.xml"); - assertCommandAndResponse( - "contact_create_remote_xxe.xml", - "contact_create_remote_response_xxe.xml"); - assertCommandAndResponse("logout.xml", "logout_response.xml"); - } - - @Test - public void testLocalXmlExternalEntity() throws Exception { - // Check go/XXE - assertCommandAndResponse("login_valid.xml", "login_response.xml"); - assertCommandAndResponse( - "contact_create_local_xxe.xml", - "contact_create_local_response_xxe.xml"); - assertCommandAndResponse("logout.xml", "logout_response.xml"); - } - - @Test - public void testBillionLaughsAttack() throws Exception { - // Check go/XXE - assertCommandAndResponse("login_valid.xml", "login_response.xml"); - assertCommandAndResponse( - "contact_create_billion_laughs.xml", - "contact_create_response_billion_laughs.xml"); - assertCommandAndResponse("logout.xml", "logout_response.xml"); - } -} diff --git a/javatests/google/registry/flows/EppTestCase.java b/javatests/google/registry/flows/EppTestCase.java new file mode 100644 index 000000000..c329bcb18 --- /dev/null +++ b/javatests/google/registry/flows/EppTestCase.java @@ -0,0 +1,130 @@ +// Copyright 2016 The Domain Registry 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.model.ofy.ObjectifyService.ofy; +import static google.registry.testing.TestDataHelper.loadFileWithSubstitutions; +import static google.registry.xml.XmlTestUtils.assertXmlEqualsWithMessage; +import static java.nio.charset.StandardCharsets.UTF_8; +import static javax.servlet.http.HttpServletResponse.SC_OK; +import static org.joda.time.DateTimeZone.UTC; +import static org.mockito.Mockito.mock; + +import com.google.common.net.MediaType; + +import google.registry.model.ofy.Ofy; +import google.registry.monitoring.whitebox.EppMetrics; +import google.registry.testing.FakeClock; +import google.registry.testing.FakeResponse; +import google.registry.testing.InjectRule; +import google.registry.testing.ShardableTestCase; +import google.registry.testing.TestSessionMetadata; + +import org.joda.time.DateTime; +import org.junit.Before; +import org.junit.Rule; + +import java.util.Map; + +public class EppTestCase extends ShardableTestCase { + + private static final MediaType APPLICATION_EPP_XML_UTF8 = + MediaType.create("application", "epp+xml").withCharset(UTF_8); + + @Rule + public final InjectRule inject = new InjectRule(); + + private final FakeClock clock = new FakeClock(); + + private TestSessionMetadata sessionMetadata; + private TransportCredentials credentials; + private boolean superuser; + + @Before + public void initTestCase() { + inject.setStaticField(Ofy.class, "clock", clock); // For transactional flows. + } + + /** + * Set the transport credentials. + * + *

When the credentials are null, the login flow still checks the EPP password from the xml, + * which is sufficient for all tests that aren't explicitly testing a form of login credentials + * such as {@link EppLoginUserTest}, {@link EppLoginAdminUserTest} and {@link EppLoginTlsTest}. + * Therefore, only those tests should call this method. + */ + protected void setTransportCredentials(TransportCredentials credentials) { + this.credentials = credentials; + } + + protected void setSuperuser(boolean superuser) { + this.superuser = superuser; + } + + String assertCommandAndResponse(String inputFilename, String outputFilename) throws Exception { + return assertCommandAndResponse(inputFilename, outputFilename, DateTime.now(UTC)); + } + + String assertCommandAndResponse(String inputFilename, String outputFilename, DateTime now) + throws Exception { + return assertCommandAndResponse(inputFilename, null, outputFilename, null, now); + } + + String assertCommandAndResponse( + String inputFilename, + Map inputSubstitutions, + String outputFilename, + Map outputSubstitutions, + DateTime now) throws Exception { + clock.setTo(now); + String input = loadFileWithSubstitutions(getClass(), inputFilename, inputSubstitutions); + String expectedOutput = + loadFileWithSubstitutions(getClass(), outputFilename, outputSubstitutions); + if (sessionMetadata == null) { + sessionMetadata = new TestSessionMetadata(); + sessionMetadata.setTransportCredentials(credentials); + } + sessionMetadata.setSuperuser(superuser); + String actualOutput = executeXmlCommand(input); + if (!sessionMetadata.isValid()) { + sessionMetadata = null; + } + assertXmlEqualsWithMessage( + expectedOutput, + actualOutput, + "Running " + inputFilename + " => " + outputFilename, + "epp.response.resData.infData.roid", + "epp.response.trID.svTRID"); + ofy().clearSessionCache(); // Clear the cache like OfyFilter would. + return actualOutput; + } + + private String executeXmlCommand(String inputXml) throws Exception { + EppRequestHandler handler = new EppRequestHandler(); + FakeResponse response = new FakeResponse(); + handler.response = response; + handler.eppController = new EppController(); + handler.eppController.clock = clock; + handler.eppController.metrics = mock(EppMetrics.class); + handler.executeEpp(sessionMetadata, inputXml.getBytes(UTF_8)); + assertThat(response.getStatus()).isEqualTo(SC_OK); + assertThat(response.getContentType()).isEqualTo(APPLICATION_EPP_XML_UTF8); + String result = response.getPayload(); + // Run the resulting xml through the unmarshaller to verify that it was valid. + EppXmlTransformer.validateOutput(result); + return result; + } +} diff --git a/javatests/google/registry/flows/EppTlsActionTest.java b/javatests/google/registry/flows/EppTlsActionTest.java new file mode 100644 index 000000000..2eb1f10ed --- /dev/null +++ b/javatests/google/registry/flows/EppTlsActionTest.java @@ -0,0 +1,67 @@ +// Copyright 2016 The Domain Registry 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 java.nio.charset.StandardCharsets.UTF_8; +import static org.mockito.Mockito.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import google.registry.testing.ShardableTestCase; +import google.registry.util.BasicHttpSession; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; +import org.mockito.ArgumentCaptor; + +/** Tests for {@link EppTlsAction}. */ +@RunWith(JUnit4.class) +public class EppTlsActionTest extends ShardableTestCase { + + private static final byte[] INPUT_XML_BYTES = "".getBytes(UTF_8); + + private void doTest(boolean superuser) { + EppTlsAction action = new EppTlsAction(); + action.inputXmlBytes = INPUT_XML_BYTES; + action.tlsCredentials = mock(TlsCredentials.class); + when(action.tlsCredentials.hasSni()).thenReturn(true); + action.session = new BasicHttpSession(); + action.session.setAttribute("CLIENT_ID", "ClientIdentifier"); + action.session.setAttribute("SUPERUSER", superuser); + action.eppRequestHandler = mock(EppRequestHandler.class); + action.run(); + ArgumentCaptor captor = ArgumentCaptor.forClass(SessionMetadata.class); + verify(action.eppRequestHandler).executeEpp(captor.capture(), eq(INPUT_XML_BYTES)); + SessionMetadata sessionMetadata = captor.getValue(); + assertThat(sessionMetadata.getClientId()).isEqualTo("ClientIdentifier"); + assertThat(sessionMetadata.isDryRun()).isFalse(); // Should always be false for TLS. + assertThat(sessionMetadata.isSuperuser()).isEqualTo(superuser); + assertThat(sessionMetadata.getTransportCredentials()).isSameAs(action.tlsCredentials); + } + + @Test + public void testSuperuser() throws Exception { + doTest(true); + } + + @Test + public void testNotSuperuser() throws Exception { + doTest(false); + } +} diff --git a/javatests/google/registry/flows/EppToolActionTest.java b/javatests/google/registry/flows/EppToolActionTest.java new file mode 100644 index 000000000..347042af3 --- /dev/null +++ b/javatests/google/registry/flows/EppToolActionTest.java @@ -0,0 +1,68 @@ +// Copyright 2016 The Domain Registry 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 java.nio.charset.StandardCharsets.UTF_8; +import static org.mockito.Mockito.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; +import org.mockito.ArgumentCaptor; + +/** Tests for {@link EppToolAction}. */ +@RunWith(JUnit4.class) +public class EppToolActionTest { + + private void doTest(boolean dryRun, boolean superuser) { + EppToolAction action = new EppToolAction(); + action.clientIdentifier = "ClientIdentifier"; + action.dryRun = dryRun; + action.superuser = superuser; + action.eppRequestHandler = mock(EppRequestHandler.class); + action.xml = ""; + action.run(); + ArgumentCaptor captor = ArgumentCaptor.forClass(SessionMetadata.class); + verify(action.eppRequestHandler).executeEpp(captor.capture(), eq(action.xml.getBytes(UTF_8))); + SessionMetadata sessionMetadata = captor.getValue(); + assertThat(sessionMetadata.getClientId()).isEqualTo("ClientIdentifier"); + assertThat(sessionMetadata.isDryRun()).isEqualTo(dryRun); + assertThat(sessionMetadata.isSuperuser()).isEqualTo(superuser); + } + + @Test + public void testDryRunAndSuperuser() throws Exception { + doTest(true, true); + } + + @Test + public void testDryRun() throws Exception { + doTest(true, false); + } + + @Test + public void testSuperuser() throws Exception { + doTest(false, true); + } + + @Test + public void testNeitherDryRunNorSuperuser() throws Exception { + doTest(false, false); + } +} diff --git a/javatests/google/registry/flows/EppToolServletTest.java b/javatests/google/registry/flows/EppToolServletTest.java deleted file mode 100644 index 4937d596a..000000000 --- a/javatests/google/registry/flows/EppToolServletTest.java +++ /dev/null @@ -1,137 +0,0 @@ -// Copyright 2016 The Domain Registry 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 google.registry.testing.DatastoreHelper.createTld; -import static google.registry.testing.DatastoreHelper.persistResource; -import static google.registry.util.DateTimeUtils.START_OF_TIME; - -import com.google.common.collect.ImmutableSet; -import com.google.common.collect.ImmutableSortedMap; - -import google.registry.model.registrar.Registrar; -import google.registry.model.registrar.RegistrarContact; -import google.registry.model.registry.Registry.TldState; -import google.registry.testing.AppEngineRule; -import google.registry.testing.UserInfo; - -import org.joda.time.DateTime; -import org.junit.Before; -import org.junit.Rule; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.runners.MockitoJUnitRunner; - -/** Tests for {@link EppToolServlet}. */ -@RunWith(MockitoJUnitRunner.class) -public class EppToolServletTest extends EppServletTestCase { - - @Rule - public final AppEngineRule appEngine = AppEngineRule.builder() - .withDatastore() - .withTaskQueue() - .withUserService(UserInfo.createAdmin(GAE_USER_EMAIL, GAE_USER_ID)) - .build(); - - private static final String GAE_USER_ID = "12345"; - private static final String GAE_USER_EMAIL = "person@example.com"; - - @Before - public void initTest() throws Exception { - Registrar registrar = Registrar.loadByClientId("NewRegistrar"); - persistResource( - new RegistrarContact.Builder() - .setParent(registrar) - .setEmailAddress(GAE_USER_EMAIL) - .setTypes(ImmutableSet.of(RegistrarContact.Type.ADMIN)) - .setGaeUserId(GAE_USER_ID) - .build()); - } - - @Test - public void testDomainAllocation_succeedsOnlyAsSuperuser() throws Exception { - ImmutableSortedMap transitions = ImmutableSortedMap.of( - START_OF_TIME, TldState.SUNRISE, - START_OF_GA, TldState.GENERAL_AVAILABILITY); - createTld("example", transitions); - - setClientIdentifier("NewRegistrar"); - setSuperuser(false); - createContactsAndHosts(); - // Note that the trademark is valid from 20130809 to 20170723, hence the domain creation - // in 2014. - assertCommandAndResponse( - "domain_create_sunrise_encoded_mark.xml", - "domain_create_sunrise_encoded_signed_mark_response.xml", - "2014-01-01T00:00:00Z"); - assertCommandAndResponse( - "domain_info_testvalidate.xml", - "domain_info_response_testvalidate_doesnt_exist.xml", - "2014-01-01T00:01:00Z"); - assertCommandAndResponse( - "domain_allocate_testvalidate.xml", - "domain_allocate_response_testvalidate_only_superuser.xml", - START_OF_GA.plusDays(1)); - setSuperuser(true); - assertCommandAndResponse( - "domain_allocate_testvalidate.xml", - "domain_allocate_response_testvalidate.xml", - START_OF_GA.plusDays(1).plusMinutes(1)); - setSuperuser(false); - assertCommandAndResponse( - "domain_info_testvalidate.xml", - "domain_info_response_testvalidate_ok.xml", - START_OF_GA.plusDays(1).plusMinutes(2)); - } - - @Test - public void testDomainCreation_failsBeforeSunrise() throws Exception { - DateTime sunriseDate = DateTime.parse("2000-05-30T00:00:00Z"); - ImmutableSortedMap transitions = ImmutableSortedMap.of( - START_OF_TIME, TldState.PREDELEGATION, - sunriseDate, TldState.SUNRISE, - sunriseDate.plusMonths(2), TldState.GENERAL_AVAILABILITY); - createTld("example", transitions); - - setClientIdentifier("NewRegistrar"); - createContactsAndHosts(); - assertCommandAndResponse( - "domain_create_sunrise_encoded_mark.xml", - "domain_create_testvalidate_invalid_phase.xml", - sunriseDate.minusDays(1)); - assertCommandAndResponse( - "domain_info_testvalidate.xml", - "domain_info_response_testvalidate_doesnt_exist.xml", - sunriseDate.plusDays(1)); - } - - @Test - public void testDomainCheckFee_succeeds() throws Exception { - DateTime gaDate = DateTime.parse("2000-05-30T00:00:00Z"); - ImmutableSortedMap transitions = ImmutableSortedMap.of( - START_OF_TIME, TldState.PREDELEGATION, - gaDate, TldState.GENERAL_AVAILABILITY); - createTld("example", transitions); - - setClientIdentifier("NewRegistrar"); - assertCommandAndResponse( - "domain_check_fee_premium.xml", - "domain_check_fee_premium_response.xml", - gaDate.plusDays(1)); - } - - // Extra method so the test runner doesn't produce empty shards. - @Test public void testNothing1() {} -} diff --git a/javatests/google/registry/flows/EppXxeAttackTest.java b/javatests/google/registry/flows/EppXxeAttackTest.java new file mode 100644 index 000000000..8bb95ac90 --- /dev/null +++ b/javatests/google/registry/flows/EppXxeAttackTest.java @@ -0,0 +1,58 @@ +// Copyright 2016 The Domain Registry 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 google.registry.testing.AppEngineRule; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public class EppXxeAttackTest extends EppTestCase { + + @Rule + public final AppEngineRule appEngine = AppEngineRule.builder() + .withDatastore() + .build(); + + @Test + public void testRemoteXmlExternalEntity() throws Exception { + assertCommandAndResponse("login_valid.xml", "login_response.xml"); + assertCommandAndResponse( + "contact_create_remote_xxe.xml", + "contact_create_remote_response_xxe.xml"); + assertCommandAndResponse("logout.xml", "logout_response.xml"); + } + + @Test + public void testLocalXmlExtrernalEntity() throws Exception { + assertCommandAndResponse("login_valid.xml", "login_response.xml"); + assertCommandAndResponse( + "contact_create_local_xxe.xml", + "contact_create_local_response_xxe.xml"); + assertCommandAndResponse("logout.xml", "logout_response.xml"); + } + + @Test + public void testBillionLaughsAttack() throws Exception { + assertCommandAndResponse("login_valid.xml", "login_response.xml"); + assertCommandAndResponse( + "contact_create_billion_laughs.xml", + "contact_create_response_billion_laughs.xml"); + assertCommandAndResponse("logout.xml", "logout_response.xml"); + } +} diff --git a/javatests/google/registry/flows/FlowTestCase.java b/javatests/google/registry/flows/FlowTestCase.java index 83735693f..01443afd3 100644 --- a/javatests/google/registry/flows/FlowTestCase.java +++ b/javatests/google/registry/flows/FlowTestCase.java @@ -99,7 +99,6 @@ public abstract class FlowTestCase { sessionMetadata.setSessionSource(SessionSource.NONE); ofy().saveWithoutBackup().entity(new ClaimsListSingleton()).now(); inject.setStaticField(Ofy.class, "clock", clock); // For transactional flows. - inject.setStaticField(FlowRunner.class, "clock", clock); // For non-transactional flows. } protected void removeServiceExtensionUri(String uri) { @@ -139,7 +138,8 @@ public abstract class FlowTestCase { getTrid(), sessionMetadata, "".getBytes(), - null); + null, + clock); } protected Trid getTrid() throws Exception { diff --git a/javatests/google/registry/flows/session/LoginFlowViaConsoleTest.java b/javatests/google/registry/flows/session/LoginFlowViaConsoleTest.java index 1ec8056bf..3f47df188 100644 --- a/javatests/google/registry/flows/session/LoginFlowViaConsoleTest.java +++ b/javatests/google/registry/flows/session/LoginFlowViaConsoleTest.java @@ -22,9 +22,9 @@ import com.google.apphosting.api.ApiProxy; import com.google.apphosting.api.ApiProxy.Environment; import com.google.common.collect.ImmutableSet; -import google.registry.flows.EppConsoleServlet.GaeUserCredentials; -import google.registry.flows.EppConsoleServlet.GaeUserCredentials.BadGaeUserIdException; -import google.registry.flows.EppConsoleServlet.GaeUserCredentials.UserNotLoggedInException; +import google.registry.flows.GaeUserCredentials; +import google.registry.flows.GaeUserCredentials.BadGaeUserIdException; +import google.registry.flows.GaeUserCredentials.UserNotLoggedInException; import google.registry.model.registrar.Registrar; import google.registry.model.registrar.RegistrarContact; diff --git a/javatests/google/registry/flows/session/LoginFlowViaTlsTest.java b/javatests/google/registry/flows/session/LoginFlowViaTlsTest.java index 310f10407..2ab04238b 100644 --- a/javatests/google/registry/flows/session/LoginFlowViaTlsTest.java +++ b/javatests/google/registry/flows/session/LoginFlowViaTlsTest.java @@ -16,6 +16,7 @@ package google.registry.flows.session; import static google.registry.testing.DatastoreHelper.persistResource; +import com.google.common.base.Optional; import com.google.common.collect.ImmutableList; import com.google.common.net.InetAddresses; @@ -30,21 +31,22 @@ import google.registry.util.CidrAddressBlock; import org.junit.Test; -import java.net.InetAddress; - /** Unit tests for {@link LoginFlow} when accessed via a TLS transport. */ public class LoginFlowViaTlsTest extends LoginFlowTestCase { + private static final String GOOD_CERT = CertificateSamples.SAMPLE_CERT_HASH; private static final String BAD_CERT = CertificateSamples.SAMPLE_CERT2_HASH; - private static final InetAddress GOOD_IP = InetAddresses.forString("192.168.1.1"); - private static final InetAddress BAD_IP = InetAddresses.forString("1.1.1.1"); + private static final Optional GOOD_IP = Optional.of("192.168.1.1"); + private static final Optional BAD_IP = Optional.of("1.1.1.1"); + private static final Optional GOOD_IPV6 = Optional.of("2001:db8::1"); + private static final Optional BAD_IPV6 = Optional.of("2001:db8::2"); @Override protected Registrar.Builder getRegistrarBuilder() { return super.getRegistrarBuilder() .setClientCertificateHash(GOOD_CERT) .setIpAddressWhitelist(ImmutableList.of( - CidrAddressBlock.create(GOOD_IP, 32))); + CidrAddressBlock.create(InetAddresses.forString(GOOD_IP.get()), 32))); } @Test @@ -62,7 +64,7 @@ public class LoginFlowViaTlsTest extends LoginFlowTestCase { CidrAddressBlock.create("2001:db8:0:0:0:0:1:1/32"))) .build()); sessionMetadata.setTransportCredentials( - new TlsCredentials(GOOD_CERT, InetAddresses.forString("2001:db8::1"), "goo.example")); + new TlsCredentials(GOOD_CERT, GOOD_IPV6, "goo.example")); doSuccessfulTest("login_valid.xml"); } @@ -74,7 +76,7 @@ public class LoginFlowViaTlsTest extends LoginFlowTestCase { CidrAddressBlock.create("2001:db8:0:0:0:0:1:1/32"))) .build()); sessionMetadata.setTransportCredentials( - new TlsCredentials(GOOD_CERT, InetAddresses.forString("2001:db8::1"), "goo.example")); + new TlsCredentials(GOOD_CERT, GOOD_IPV6, "goo.example")); doSuccessfulTest("login_valid.xml"); } @@ -85,8 +87,7 @@ public class LoginFlowViaTlsTest extends LoginFlowTestCase { .setIpAddressWhitelist(ImmutableList.of( CidrAddressBlock.create("192.168.1.255/24"))) .build()); - sessionMetadata.setTransportCredentials( - new TlsCredentials(GOOD_CERT, InetAddresses.forString("192.168.1.1"), "goo.example")); + sessionMetadata.setTransportCredentials(new TlsCredentials(GOOD_CERT, GOOD_IP, "goo.example")); doSuccessfulTest("login_valid.xml"); } @@ -119,7 +120,8 @@ public class LoginFlowViaTlsTest extends LoginFlowTestCase { CidrAddressBlock.create(InetAddresses.forString("192.168.1.1"), 32), CidrAddressBlock.create(InetAddresses.forString("2001:db8::1"), 128))) .build()); - sessionMetadata.setTransportCredentials(new TlsCredentials(GOOD_CERT, null, "goo.example")); + sessionMetadata.setTransportCredentials( + new TlsCredentials(GOOD_CERT, Optional.empty(), "goo.example")); doFailingTest("login_valid.xml", BadRegistrarIpAddressException.class); } @@ -143,8 +145,7 @@ public class LoginFlowViaTlsTest extends LoginFlowTestCase { CidrAddressBlock.create(InetAddresses.forString("192.168.1.1"), 32), CidrAddressBlock.create(InetAddresses.forString("2001:db8::1"), 128))) .build()); - sessionMetadata.setTransportCredentials( - new TlsCredentials(GOOD_CERT, InetAddresses.forString("2001:db8::2"), "goo.example")); + sessionMetadata.setTransportCredentials(new TlsCredentials(GOOD_CERT, BAD_IPV6, "goo.example")); doFailingTest("login_valid.xml", BadRegistrarIpAddressException.class); } } diff --git a/javatests/google/registry/flows/testdata/domain_allocate_testvalidate.xml b/javatests/google/registry/flows/testdata/domain_allocate_testvalidate.xml index 36da6e718..ed5298e92 100644 --- a/javatests/google/registry/flows/testdata/domain_allocate_testvalidate.xml +++ b/javatests/google/registry/flows/testdata/domain_allocate_testvalidate.xml @@ -19,7 +19,7 @@ - D-EXAMPLE + A-EXAMPLE 2014-01-01T00:00:00Z diff --git a/javatests/google/registry/flows/testdata/domain_create_sunrise_encoded_signed_mark_response.xml b/javatests/google/registry/flows/testdata/domain_create_sunrise_encoded_signed_mark_response.xml index c1bbcb13d..79311e553 100644 --- a/javatests/google/registry/flows/testdata/domain_create_sunrise_encoded_signed_mark_response.xml +++ b/javatests/google/registry/flows/testdata/domain_create_sunrise_encoded_signed_mark_response.xml @@ -14,7 +14,7 @@ sunrise - D-EXAMPLE + A-EXAMPLE diff --git a/javatests/google/registry/flows/testdata/pdt_login.xml b/javatests/google/registry/flows/testdata/login_invalid_wrong_password.xml similarity index 60% rename from javatests/google/registry/flows/testdata/pdt_login.xml rename to javatests/google/registry/flows/testdata/login_invalid_wrong_password.xml index 9de8a8037..bb95c0274 100644 --- a/javatests/google/registry/flows/testdata/pdt_login.xml +++ b/javatests/google/registry/flows/testdata/login_invalid_wrong_password.xml @@ -1,21 +1,19 @@ - + NewRegistrar - foo-BAR2 + incorrect 1.0 en + urn:ietf:params:xml:ns:host-1.0 urn:ietf:params:xml:ns:domain-1.0 urn:ietf:params:xml:ns:contact-1.0 - urn:ietf:params:xml:ns:host-1.0 - urn:ietf:params:xml:ns:secDNS-1.1 + urn:ietf:params:xml:ns:launch-1.0 + urn:ietf:params:xml:ns:rgp-1.0 diff --git a/javatests/google/registry/flows/testdata/login_response_unauthorized_tld.xml b/javatests/google/registry/flows/testdata/login_response_unauthorized_tld.xml deleted file mode 100644 index ae24dbf30..000000000 --- a/javatests/google/registry/flows/testdata/login_response_unauthorized_tld.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - - Registrar is not authorized to access this TLD - - - ABC-12345 - Mt2PuhrJTKO1nM/MbHxT4g==-2a - - - diff --git a/javatests/google/registry/flows/testdata/login_response_wrong_password.xml b/javatests/google/registry/flows/testdata/login_response_wrong_password.xml new file mode 100644 index 000000000..d71584c3c --- /dev/null +++ b/javatests/google/registry/flows/testdata/login_response_wrong_password.xml @@ -0,0 +1,11 @@ + + + + Registrar password is incorrect + + + ABC-12345 + server-trid + + + diff --git a/javatests/google/registry/flows/testdata/poll_response_contact_transfer.xml b/javatests/google/registry/flows/testdata/poll_response_contact_transfer.xml index ef148c442..5c38f5b85 100644 --- a/javatests/google/registry/flows/testdata/poll_response_contact_transfer.xml +++ b/javatests/google/registry/flows/testdata/poll_response_contact_transfer.xml @@ -3,7 +3,7 @@ Command completed successfully; ack to dequeue - + 2000-06-08T22:00:00Z Transfer requested. diff --git a/javatests/google/registry/flows/testdata/poll_response_domain_transfer_request.xml b/javatests/google/registry/flows/testdata/poll_response_domain_transfer_request.xml index e5fc66cb3..472dd18b7 100644 --- a/javatests/google/registry/flows/testdata/poll_response_domain_transfer_request.xml +++ b/javatests/google/registry/flows/testdata/poll_response_domain_transfer_request.xml @@ -3,7 +3,7 @@ Command completed successfully; ack to dequeue - + 2001-01-01T00:00:00Z Transfer requested. diff --git a/javatests/google/registry/flows/testdata/poll_response_domain_transfer_server_approve_loser.xml b/javatests/google/registry/flows/testdata/poll_response_domain_transfer_server_approve_loser.xml index 6661620c9..640babb06 100644 --- a/javatests/google/registry/flows/testdata/poll_response_domain_transfer_server_approve_loser.xml +++ b/javatests/google/registry/flows/testdata/poll_response_domain_transfer_server_approve_loser.xml @@ -3,7 +3,7 @@ Command completed successfully; ack to dequeue - + 2001-01-06T00:00:00Z Transfer approved. diff --git a/javatests/google/registry/flows/testdata/poll_response_domain_transfer_server_approve_winner.xml b/javatests/google/registry/flows/testdata/poll_response_domain_transfer_server_approve_winner.xml index ec3000b21..ed2c57b8e 100644 --- a/javatests/google/registry/flows/testdata/poll_response_domain_transfer_server_approve_winner.xml +++ b/javatests/google/registry/flows/testdata/poll_response_domain_transfer_server_approve_winner.xml @@ -4,7 +4,7 @@ Command completed successfully; ack to dequeue - + 2001-01-06T00:00:00Z Transfer approved. diff --git a/javatests/google/registry/model/EppResourceUtilsTest.java b/javatests/google/registry/model/EppResourceUtilsTest.java index 67e3b6ac7..0b7dbaee4 100644 --- a/javatests/google/registry/model/EppResourceUtilsTest.java +++ b/javatests/google/registry/model/EppResourceUtilsTest.java @@ -87,7 +87,8 @@ public class EppResourceUtilsTest { Trid.create(null, "server-trid"), sessionMetadata, "".getBytes(), - null) + null, + clock) .run(CommitMode.LIVE, UserPrivileges.NORMAL); } diff --git a/javatests/google/registry/server/RegistryTestServer.java b/javatests/google/registry/server/RegistryTestServer.java index 3dcd2b36c..bdcec0cc5 100644 --- a/javatests/google/registry/server/RegistryTestServer.java +++ b/javatests/google/registry/server/RegistryTestServer.java @@ -29,11 +29,16 @@ public final class RegistryTestServer { public static final ImmutableMap RUNFILES = new ImmutableMap.Builder() - .put("/index.html", Paths.get("java/google/registry/ui/html/index.html")) - .put("/error.html", Paths.get("java/google/registry/ui/html/error.html")) + .put( + "/index.html", + Paths.get("java/google/registry/ui/html/index.html")) + .put( + "/error.html", + Paths.get("java/google/registry/ui/html/error.html")) .put("/assets/js/*", Paths.get("java/google/registry/ui")) .put("/assets/css/*", Paths.get("java/google/registry/ui/css")) - .put("/assets/sources/deps.js", + .put( + "/assets/sources/deps.js", Paths.get("java/google/registry/ui/deps.js")) .put("/assets/sources/*", Paths.get("")) .put("/assets/*", Paths.get("java/google/registry/ui/assets")) @@ -43,11 +48,11 @@ public final class RegistryTestServer { // Frontend Services route("/whois/*", google.registry.module.frontend.FrontendServlet.class), route("/rdap/*", google.registry.module.frontend.FrontendServlet.class), - route("/registrar-xhr", google.registry.flows.EppConsoleServlet.class), - route("/check", google.registry.ui.server.api.CheckApiServlet.class), + route("/registrar-xhr", google.registry.module.frontend.FrontendServlet.class), + route("/check", google.registry.module.frontend.FrontendServlet.class), // Proxy Services - route("/_dr/epp", google.registry.flows.EppTlsServlet.class), + route("/_dr/epp", google.registry.module.frontend.FrontendServlet.class), route("/_dr/whois", google.registry.module.frontend.FrontendServlet.class), // Registry Data Escrow (RDE) diff --git a/javatests/google/registry/testing/ShardableTestCase.java b/javatests/google/registry/testing/ShardableTestCase.java new file mode 100644 index 000000000..af3d5d81c --- /dev/null +++ b/javatests/google/registry/testing/ShardableTestCase.java @@ -0,0 +1,34 @@ +// Copyright 2016 The Domain Registry 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.testing; + +import org.junit.Test; + +/** + * Test case with 3 empty methods. + * + * The sharding test runner fails if it produces an empty shard, and we shard 4 ways. This makes + * sure that we never produces empty shards. + */ +public abstract class ShardableTestCase { + @Test + public void testNothing1() {} + + @Test + public void testNothing2() {} + + @Test + public void testNothing3() {} +} diff --git a/javatests/google/registry/testing/TestDataHelper.java b/javatests/google/registry/testing/TestDataHelper.java index ed666a8bd..67cfca694 100644 --- a/javatests/google/registry/testing/TestDataHelper.java +++ b/javatests/google/registry/testing/TestDataHelper.java @@ -14,7 +14,7 @@ package google.registry.testing; -import static google.registry.util.CollectionUtils.isNullOrEmpty; +import static google.registry.util.CollectionUtils.nullToEmpty; import static google.registry.util.ResourceUtils.readResourceUtf8; import java.util.Map; @@ -32,10 +32,8 @@ public final class TestDataHelper { public static String loadFileWithSubstitutions( Class context, String filename, Map substitutions) { String fileContents = readResourceUtf8(context, "testdata/" + filename); - if (!isNullOrEmpty(substitutions)) { - for (Entry entry : substitutions.entrySet()) { - fileContents = fileContents.replaceAll("%" + entry.getKey() + "%", entry.getValue()); - } + for (Entry entry : nullToEmpty(substitutions).entrySet()) { + fileContents = fileContents.replaceAll("%" + entry.getKey() + "%", entry.getValue()); } return fileContents; } diff --git a/javatests/google/registry/testing/TestSessionMetadata.java b/javatests/google/registry/testing/TestSessionMetadata.java index a965d5999..5537a71ce 100644 --- a/javatests/google/registry/testing/TestSessionMetadata.java +++ b/javatests/google/registry/testing/TestSessionMetadata.java @@ -56,4 +56,8 @@ public class TestSessionMetadata extends SessionMetadata { public void setSessionSource(SessionSource source) { sessionSource = source; } + + public boolean isValid() { + return isValid; + } } diff --git a/javatests/google/registry/ui/server/api/BUILD b/javatests/google/registry/ui/server/api/BUILD index 80eddd528..b9b6c4f2f 100644 --- a/javatests/google/registry/ui/server/api/BUILD +++ b/javatests/google/registry/ui/server/api/BUILD @@ -21,6 +21,7 @@ java_library( "//third_party/java/truth", "//java/google/registry/model", "//java/google/registry/ui/server/api", + "//java/google/registry/util", "//javatests/google/registry/testing", ], ) diff --git a/javatests/google/registry/ui/server/api/CheckApiServletTest.java b/javatests/google/registry/ui/server/api/CheckApiActionTest.java similarity index 63% rename from javatests/google/registry/ui/server/api/CheckApiServletTest.java rename to javatests/google/registry/ui/server/api/CheckApiActionTest.java index 5f2775939..c15084ace 100644 --- a/javatests/google/registry/ui/server/api/CheckApiServletTest.java +++ b/javatests/google/registry/ui/server/api/CheckApiActionTest.java @@ -19,45 +19,34 @@ 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 static org.mockito.Mockito.when; -import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import google.registry.model.registrar.Registrar; import google.registry.model.registry.Registry; import google.registry.testing.AppEngineRule; +import google.registry.testing.FakeResponse; +import google.registry.util.SystemClock; import org.json.simple.JSONValue; import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; -import org.mockito.Mock; -import org.mockito.runners.MockitoJUnitRunner; +import org.junit.runners.JUnit4; -import java.io.PrintWriter; -import java.io.StringWriter; import java.util.Map; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; - -/** Tests for {@link CheckApiServlet}. */ -@RunWith(MockitoJUnitRunner.class) -public class CheckApiServletTest { +/** Tests for {@link CheckApiAction}. */ +@RunWith(JUnit4.class) +public class CheckApiActionTest { @Rule public final AppEngineRule appEngine = AppEngineRule.builder() .withDatastore() .build(); - @Mock HttpServletRequest req; - @Mock HttpServletResponse rsp; - - private final StringWriter writer = new StringWriter(); - - private final CheckApiServlet servlet = new CheckApiServlet(); + final CheckApiAction action = new CheckApiAction(); @Before public void init() throws Exception { @@ -67,51 +56,50 @@ public class CheckApiServletTest { .asBuilder() .setReservedLists(persistReservedList("example-reserved", "foo,FULLY_BLOCKED")) .build()); - when(rsp.getWriter()).thenReturn(new PrintWriter(writer)); } - private void doTest(Map expected) throws Exception { - servlet.doGet(req, rsp); - assertThat(JSONValue.parse(writer.toString())).isEqualTo(expected); + @SuppressWarnings("unchecked") + private Map getCheckResponse(String domain) { + action.domain = domain; + action.response = new FakeResponse(); + action.clock = new SystemClock(); + action.run(); + return (Map) JSONValue.parse(((FakeResponse) action.response).getPayload()); } @Test public void testFailure_nullDomain() throws Exception { - doTest(ImmutableMap.of( + assertThat(getCheckResponse(null)).containsExactly( "status", "error", - "reason", "Must supply a valid domain name on an authoritative TLD")); + "reason", "Must supply a valid domain name on an authoritative TLD"); } @Test public void testFailure_emptyDomain() throws Exception { - when(req.getParameter("domain")).thenReturn(""); - doTest(ImmutableMap.of( + assertThat(getCheckResponse("")).containsExactly( "status", "error", - "reason", "Must supply a valid domain name on an authoritative TLD")); + "reason", "Must supply a valid domain name on an authoritative TLD"); } @Test public void testFailure_invalidDomain() throws Exception { - when(req.getParameter("domain")).thenReturn("@#$%^"); - doTest(ImmutableMap.of( + assertThat(getCheckResponse("@#$%^")).containsExactly( "status", "error", - "reason", "Must supply a valid domain name on an authoritative TLD")); + "reason", "Must supply a valid domain name on an authoritative TLD"); } @Test public void testFailure_singlePartDomain() throws Exception { - when(req.getParameter("domain")).thenReturn("foo"); - doTest(ImmutableMap.of( + assertThat(getCheckResponse("foo")).containsExactly( "status", "error", - "reason", "Must supply a valid domain name on an authoritative TLD")); + "reason", "Must supply a valid domain name on an authoritative TLD"); } @Test public void testFailure_nonExistentTld() throws Exception { - when(req.getParameter("domain")).thenReturn("foo.bar"); - doTest(ImmutableMap.of( + assertThat(getCheckResponse("foo.bar")).containsExactly( "status", "error", - "reason", "Must supply a valid domain name on an authoritative TLD")); + "reason", "Must supply a valid domain name on an authoritative TLD"); } @Test @@ -122,73 +110,65 @@ public class CheckApiServletTest { .asBuilder() .setAllowedTlds(ImmutableSet.of("foo")) .build()); - when(req.getParameter("domain")).thenReturn("timmy.example"); - doTest(ImmutableMap.of( + assertThat(getCheckResponse("timmy.example")).containsExactly( "status", "error", - "reason", "Registrar is not authorized to access the TLD example")); + "reason", "Registrar is not authorized to access the TLD example"); } @Test public void testSuccess_availableStandard() throws Exception { - when(req.getParameter("domain")).thenReturn("somedomain.example"); - doTest(ImmutableMap.of( + assertThat(getCheckResponse("somedomain.example")).containsExactly( "status", "success", "available", true, - "tier", "standard")); + "tier", "standard"); } @Test public void testSuccess_availableCapital() throws Exception { - when(req.getParameter("domain")).thenReturn("SOMEDOMAIN.EXAMPLE"); - doTest(ImmutableMap.of( + assertThat(getCheckResponse("SOMEDOMAIN.EXAMPLE")).containsExactly( "status", "success", "available", true, - "tier", "standard")); + "tier", "standard"); } @Test public void testSuccess_availableUnicode() throws Exception { - when(req.getParameter("domain")).thenReturn("ééé.example"); - doTest(ImmutableMap.of( + assertThat(getCheckResponse("ééé.example")).containsExactly( "status", "success", "available", true, - "tier", "standard")); + "tier", "standard"); } @Test public void testSuccess_availablePunycode() throws Exception { - when(req.getParameter("domain")).thenReturn("xn--9caaa.example"); - doTest(ImmutableMap.of( + assertThat(getCheckResponse("xn--9caaa.example")).containsExactly( "status", "success", "available", true, - "tier", "standard")); + "tier", "standard"); } @Test public void testSuccess_availablePremium() throws Exception { - when(req.getParameter("domain")).thenReturn("rich.example"); - doTest(ImmutableMap.of( + assertThat(getCheckResponse("rich.example")).containsExactly( "status", "success", "available", true, - "tier", "premium")); + "tier", "premium"); } @Test public void testSuccess_alreadyRegistered() throws Exception { persistActiveDomain("somedomain.example"); - when(req.getParameter("domain")).thenReturn("somedomain.example"); - doTest(ImmutableMap.of( + assertThat(getCheckResponse("somedomain.example")).containsExactly( "status", "success", "available", false, - "reason", "In use")); + "reason", "In use"); } @Test public void testSuccess_reserved() throws Exception { - when(req.getParameter("domain")).thenReturn("foo.example"); - doTest(ImmutableMap.of( + assertThat(getCheckResponse("foo.example")).containsExactly( "status", "success", "available", false, - "reason", "Reserved")); + "reason", "Reserved"); } }