// Copyright 2011 Google Inc. All Rights Reserved. package com.google.appengine.tools.mapreduce.impl.handlers; import com.google.appengine.tools.mapreduce.MapReduceServlet; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.util.logging.Level; import java.util.logging.Logger; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; /** */ public final class MapReduceServletImpl { // --------------------------- STATIC FIELDS --------------------------- public static final Logger LOG = Logger.getLogger(MapReduceServlet.class.getName()); static final String CONTROLLER_PATH = "controllerCallback"; static final String START_PATH = "start"; static final String MAPPER_WORKER_PATH = "mapperCallback"; static final String COMMAND_PATH = "command"; // --------------------------- CONSTRUCTORS --------------------------- private MapReduceServletImpl() { } // -------------------------- STATIC METHODS -------------------------- /** * Handle GET http requests. */ public static void doGet(HttpServletRequest request, HttpServletResponse response) { String handler = getHandler(request); if (handler.startsWith(COMMAND_PATH)) { if (!checkForAjax(request, response)) { return; } StatusHandler.handleCommand(handler.substring(COMMAND_PATH.length() + 1), request, response); } else { handleStaticResources(handler, response); } } /** * Handle POST http requests. */ public static void doPost(HttpServletRequest request, HttpServletResponse response) { String handler = getHandler(request); if (handler.startsWith(CONTROLLER_PATH)) { if (!checkForTaskQueue(request, response)) { return; } ControllerHandler.handleController(request); } else if (handler.startsWith(MAPPER_WORKER_PATH)) { if (!checkForTaskQueue(request, response)) { return; } WorkerHandler.handleMapperWorker(request); } else if (handler.startsWith(START_PATH)) { throw new UnsupportedOperationException(); } else if (handler.startsWith(COMMAND_PATH)) { if (!checkForAjax(request, response)) { return; } StatusHandler.handleCommand(handler.substring(COMMAND_PATH.length() + 1), request, response); } else { throw new RuntimeException( "Received an unknown MapReduce request handler. See logs for more detail."); } } /** * Checks to ensure that the current request was sent via an AJAX request. * * If the request was not sent by an AJAX request, returns false, and sets * the response status code to 403. This protects against CSRF attacks against * AJAX only handlers. * * @return true if the request is a task queue request */ private static boolean checkForAjax(HttpServletRequest request, HttpServletResponse response) { if (!"XMLHttpRequest".equals(request.getHeader("X-Requested-With"))) { LOG.log( Level.SEVERE, "Received unexpected non-XMLHttpRequest command. Possible CSRF attack."); try { response.sendError(HttpServletResponse.SC_FORBIDDEN, "Received unexpected non-XMLHttpRequest command."); } catch (IOException ioe) { throw new RuntimeException("Encountered error writing error", ioe); } return false; } return true; } /** * Checks to ensure that the current request was sent via the task queue. * * If the request is not in the task queue, returns false, and sets the * response status code to 403. This protects against CSRF attacks against * task queue-only handlers. * * @return true if the request is a task queue request */ private static boolean checkForTaskQueue(HttpServletRequest request, HttpServletResponse response) { if (request.getHeader("X-AppEngine-QueueName") == null) { LOG.log(Level.SEVERE, "Received unexpected non-task queue request. Possible CSRF attack."); try { response.sendError( HttpServletResponse.SC_FORBIDDEN, "Received unexpected non-task queue request."); } catch (IOException ioe) { throw new RuntimeException("Encountered error writing error", ioe); } return false; } return true; } /** * Returns the portion of the URL from the end of the TLD (exclusive) to the * handler portion (exclusive). * * For example, getBase(https://www.google.com/foo/bar) -> /foo/ * However, there are handler portions that take more than segment * (currently only the command handlers). So in that case, we have: * getBase(https://www.google.com/foo/command/bar) -> /foo/ */ static String getBase(HttpServletRequest request) { String fullPath = request.getRequestURI(); int baseEnd = getDividingIndex(fullPath); return fullPath.substring(0, baseEnd + 1); } /** * Finds the index of the "/" separating the base from the handler. */ private static int getDividingIndex(String fullPath) { int baseEnd = fullPath.lastIndexOf('/'); if (fullPath.substring(0, baseEnd).endsWith(COMMAND_PATH)) { baseEnd = fullPath.substring(0, baseEnd).lastIndexOf('/'); } return baseEnd; } /** * Returns the handler portion of the URL path. * * For example, getHandler(https://www.google.com/foo/bar) -> bar * Note that for command handlers, * getHandler(https://www.google.com/foo/command/bar) -> command/bar */ static String getHandler(HttpServletRequest request) { String requestURI = request.getRequestURI(); return requestURI.substring(getDividingIndex(requestURI) + 1); } /** * Handle serving of static resources (which we do dynamically so users * only have to add one entry to their web.xml). */ static void handleStaticResources(String handler, HttpServletResponse response) { String fileName; if (handler.equals("status")) { response.setContentType("text/html"); fileName = "overview.html"; } else if (handler.equals("detail")) { response.setContentType("text/html"); fileName = "detail.html"; } else if (handler.equals("base.css")) { response.setContentType("text/css"); fileName = "base.css"; } else if (handler.equals("jquery.js")) { response.setContentType("text/javascript"); fileName = "jquery-1.4.2.min.js"; } else if (handler.equals("status.js")) { response.setContentType("text/javascript"); fileName = "status.js"; } else { try { response.sendError(404); } catch (IOException e) { throw new RuntimeException("Encountered error sending 404", e); } return; } response.setHeader("Cache-Control", "public; max-age=300"); try { InputStream resourceStream = MapReduceServlet.class.getResourceAsStream( "/com/google/appengine/tools/mapreduce/" + fileName); if (resourceStream == null) { resourceStream = MapReduceServlet.class.getResourceAsStream( "/third_party/java_src/appengine_mapreduce/" + fileName); } if (resourceStream == null) { throw new RuntimeException("Couldn't find static file for MapReduce library: " + fileName); } OutputStream responseStream = response.getOutputStream(); byte[] buffer = new byte[1024]; while (true) { int bytesRead = resourceStream.read(buffer); if (bytesRead < 0) { break; } responseStream.write(buffer, 0, bytesRead); } responseStream.flush(); } catch (FileNotFoundException e) { throw new RuntimeException("Couldn't find static file for MapReduce library", e); } catch (IOException e) { throw new RuntimeException("Couldn't read static file for MapReduce library", e); } } }