// 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 com.google.domain.registry.request; import static com.google.common.base.Preconditions.checkNotNull; import static com.google.common.base.Strings.nullToEmpty; import static com.google.common.net.HttpHeaders.LOCATION; import static com.google.common.net.MediaType.PLAIN_TEXT_UTF_8; import static com.google.domain.registry.security.XsrfTokenManager.X_CSRF_TOKEN; import static com.google.domain.registry.security.XsrfTokenManager.validateToken; import static javax.servlet.http.HttpServletResponse.SC_FORBIDDEN; import static javax.servlet.http.HttpServletResponse.SC_METHOD_NOT_ALLOWED; import static javax.servlet.http.HttpServletResponse.SC_MOVED_TEMPORARILY; import static javax.servlet.http.HttpServletResponse.SC_NOT_FOUND; import com.google.appengine.api.users.UserService; import com.google.appengine.api.users.UserServiceFactory; import com.google.common.base.Optional; import com.google.domain.registry.util.FormattingLogger; import com.google.domain.registry.util.NonFinalForTesting; import org.joda.time.Duration; import java.io.IOException; import java.lang.reflect.Method; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; /** * Dagger request processor for Domain Registry. * *

This class creates an HTTP request processor from a Dagger component. It routes requests from * your servlet to an {@link Action @Action} annotated handler class. * *

Component Definition

* *

Action instances are supplied on a per-request basis by invoking the methods on {@code C}. * For example: *

 * {@literal @Component}
 * interface ServerComponent {
 *   HelloAction helloAction();
 * }
* *

The rules for component methods are as follows: *

    *
  1. Methods whose raw return type does not implement {@code Runnable} will be ignored *
  2. Methods whose raw return type does not have an {@code @Action} annotation are ignored *
* *

Security Features

* *

XSRF protection is built into this class. It can be enabled or disabled on individual actions * using {@link Action#xsrfProtection() xsrfProtection} setting. * *

This class also enforces the {@link Action#requireLogin() requireLogin} setting. * * @param component type */ public final class RequestHandler { private static final FormattingLogger logger = FormattingLogger.getLoggerForCallerClass(); private static final Duration XSRF_VALIDITY = Duration.standardDays(1); @NonFinalForTesting private static UserService userService = UserServiceFactory.getUserService(); /** * Creates a new request processor based off your component methods. * *

Warning: When using the App Engine platform, you must call * {@link Method#setAccessible(boolean) setAccessible(true)} on all your component {@link Method} * instances, from within the same package as the component. This is due to cross-package * reflection restrictions. * * @param methods is the result of calling {@link Class#getMethods()} on {@code component}, which * are filtered to only include those with no arguments returning a {@link Runnable} with an * {@link Action} annotation */ public static RequestHandler create(Class component, Iterable methods) { return new RequestHandler<>(component, Router.create(methods)); } private final Router router; private RequestHandler(Class component, Router router) { checkNotNull(component); this.router = router; } /** * Runs the appropriate action for a servlet request. * * @param component is an instance of the component type whose methods were passed to * {@link #create(Class, Iterable)} */ public void handleRequest(HttpServletRequest req, HttpServletResponse rsp, C component) throws IOException { checkNotNull(component); checkNotNull(rsp); Action.Method method; try { method = Action.Method.valueOf(req.getMethod()); } catch (IllegalArgumentException e) { logger.infofmt("Unsupported method: %s", req.getMethod()); rsp.sendError(SC_METHOD_NOT_ALLOWED); return; } String path = req.getRequestURI(); Optional route = router.route(path); if (!route.isPresent()) { logger.infofmt("No action found for: %s", path); rsp.sendError(SC_NOT_FOUND); return; } if (!route.get().isMethodAllowed(method)) { logger.infofmt("Method %s not allowed for: %s", method, path); rsp.sendError(SC_METHOD_NOT_ALLOWED); return; } if (route.get().action().requireLogin() && !userService.isUserLoggedIn()) { logger.info("not logged in"); rsp.setStatus(SC_MOVED_TEMPORARILY); rsp.setHeader(LOCATION, userService.createLoginURL(req.getRequestURI())); return; } if (route.get().shouldXsrfProtect(method) && !validateToken( nullToEmpty(req.getHeader(X_CSRF_TOKEN)), route.get().action().xsrfScope(), XSRF_VALIDITY)) { rsp.sendError(SC_FORBIDDEN, "Invalid " + X_CSRF_TOKEN); return; } try { route.get().instantiator().apply(component).run(); if (route.get().action().automaticallyPrintOk()) { rsp.setContentType(PLAIN_TEXT_UTF_8.toString()); rsp.getWriter().write("OK\n"); } } catch (HttpException e) { e.send(rsp); } } }