package io.lumify.web; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import com.google.common.base.Preconditions; import io.lumify.core.config.Configuration; import io.lumify.core.exception.LumifyAccessDeniedException; import io.lumify.core.exception.LumifyException; import io.lumify.core.model.user.UserRepository; import io.lumify.core.model.workspace.WorkspaceRepository; import io.lumify.core.user.ProxyUser; import io.lumify.core.user.User; import io.lumify.miniweb.Handler; import io.lumify.miniweb.HandlerChain; import io.lumify.web.clientapi.model.ClientApiObject; import io.lumify.web.clientapi.model.Privilege; import io.lumify.web.clientapi.model.util.ObjectMapperFactory; import org.apache.commons.codec.binary.Hex; import org.apache.commons.io.IOUtils; import org.json.JSONArray; import org.json.JSONObject; import org.securegraph.Authorizations; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.Part; import java.io.*; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.util.ArrayList; import java.util.List; import java.util.Set; import java.util.TimeZone; /** * Represents the base behavior that a {@link Handler} must support * and provides common methods for handler usage */ public abstract class BaseRequestHandler extends MinimalRequestHandler { protected static final int EXPIRES_1_HOUR = 60 * 60; private static final String LUMIFY_WORKSPACE_ID_HEADER_NAME = "Lumify-Workspace-Id"; private static final String LUMIFY_TIME_ZONE_HEADER_NAME = "Lumify-TimeZone"; private static final String TIME_ZONE_ATTRIBUTE_NAME = "timeZone"; private static final String TIME_ZONE_PARAMETER_NAME = "timeZone"; private final UserRepository userRepository; private final WorkspaceRepository workspaceRepository; private final ObjectMapper objectMapper = ObjectMapperFactory.getInstance(); protected BaseRequestHandler(UserRepository userRepository, WorkspaceRepository workspaceRepository, Configuration configuration) { super(configuration); this.userRepository = userRepository; this.workspaceRepository = workspaceRepository; } @Override public abstract void handle(HttpServletRequest request, HttpServletResponse response, HandlerChain chain) throws Exception; protected String getBaseUrl(HttpServletRequest request) { String configuredBaseUrl = getConfiguration().get(Configuration.BASE_URL, null); if (configuredBaseUrl != null && configuredBaseUrl.trim().length() > 0) { return configuredBaseUrl; } String scheme = request.getScheme(); String serverName = request.getServerName(); int port = request.getServerPort(); String contextPath = request.getContextPath(); StringBuilder sb = new StringBuilder(); sb.append(scheme).append("://").append(serverName); if (!(scheme.equals("http") && port == 80 || scheme.equals("https") && port == 443)) { sb.append(":").append(port); } sb.append(contextPath); return sb.toString(); } protected String getActiveWorkspaceId(final HttpServletRequest request) { String workspaceId = getWorkspaceIdOrDefault(request); if (workspaceId == null || workspaceId.trim().length() == 0) { throw new LumifyException(LUMIFY_WORKSPACE_ID_HEADER_NAME + " is a required header."); } return workspaceId; } protected String getWorkspaceIdOrDefault(final HttpServletRequest request) { String workspaceId = (String) request.getAttribute("workspaceId"); if (workspaceId == null || workspaceId.trim().length() == 0) { workspaceId = request.getHeader(LUMIFY_WORKSPACE_ID_HEADER_NAME); if (workspaceId == null || workspaceId.trim().length() == 0) { workspaceId = getOptionalParameter(request, "workspaceId"); if (workspaceId == null || workspaceId.trim().length() == 0) { return null; } } } return workspaceId; } protected String getTimeZone(final HttpServletRequest request) { String timeZone = (String) request.getAttribute(TIME_ZONE_ATTRIBUTE_NAME); if (timeZone == null || timeZone.trim().length() == 0) { timeZone = request.getHeader(LUMIFY_TIME_ZONE_HEADER_NAME); if (timeZone == null || timeZone.trim().length() == 0) { timeZone = getOptionalParameter(request, TIME_ZONE_PARAMETER_NAME); if (timeZone == null || timeZone.trim().length() == 0) { timeZone = getConfiguration().get(Configuration.DEFAULT_TIME_ZONE, TimeZone.getDefault().getDisplayName()); } } } return timeZone; } protected Authorizations getAuthorizations(final HttpServletRequest request, final User user) { String workspaceId = getWorkspaceIdOrDefault(request); if (workspaceId != null) { if (!this.workspaceRepository.hasReadPermissions(workspaceId, user)) { throw new LumifyAccessDeniedException("You do not have access to workspace: " + workspaceId, user, workspaceId); } return getUserRepository().getAuthorizations(user, workspaceId); } return getUserRepository().getAuthorizations(user); } protected Set<Privilege> getPrivileges(User user) { return getUserRepository().getPrivileges(user); } public static void setMaxAge(final HttpServletResponse response, int numberOfSeconds) { response.setHeader("Cache-Control", "max-age=" + numberOfSeconds); } public static String generateETag(byte[] data) { try { MessageDigest digest = MessageDigest.getInstance("MD5"); byte[] md5 = digest.digest(data); return Hex.encodeHexString(md5); } catch (NoSuchAlgorithmException e) { throw new LumifyException("Could not find MD5", e); } } public static void addETagHeader(final HttpServletResponse response, String eTag) { response.setHeader("ETag", "\"" + eTag + "\""); } public boolean testEtagHeaders(HttpServletRequest request, HttpServletResponse response, String eTag) throws IOException { String ifNoneMatch = request.getHeader("If-None-Match"); if (ifNoneMatch != null) { if (ifNoneMatch.startsWith("\"") && ifNoneMatch.length() > 2) { ifNoneMatch = ifNoneMatch.substring(1, ifNoneMatch.length() - 1); } if (eTag.equalsIgnoreCase(ifNoneMatch)) { addETagHeader(response, eTag); respondWithNotModified(response); return true; } } return false; } protected void respondWithNotFound(final HttpServletResponse response) throws IOException { response.sendError(HttpServletResponse.SC_NOT_FOUND); } protected void respondWithNotModified(final HttpServletResponse response) throws IOException { response.sendError(HttpServletResponse.SC_NOT_MODIFIED); } protected void respondWithNotFound(final HttpServletResponse response, String message) throws IOException { response.sendError(HttpServletResponse.SC_NOT_FOUND, message); } protected void respondWithAccessDenied(final HttpServletResponse response, String message) throws IOException { response.sendError(HttpServletResponse.SC_FORBIDDEN, message); } /** * Send a Bad Request response with JSON object mapping field error messages */ protected void respondWithBadRequest(final HttpServletResponse response, final String parameterName, final String errorMessage, final String invalidValue) throws IOException { List<String> values = null; if (invalidValue != null) { values = new ArrayList<String>(); values.add(invalidValue); } respondWithBadRequest(response, parameterName, errorMessage, values); } /** * Send a Bad Request response with JSON object mapping field error messages */ protected void respondWithBadRequest(final HttpServletResponse response, final String parameterName, final String errorMessage, final List<String> invalidValues) throws IOException { JSONObject error = new JSONObject(); error.put(parameterName, errorMessage); if (invalidValues != null) { JSONArray values = new JSONArray(); for (String v : invalidValues) { values.put(v); } error.put("invalidValues", values); } response.sendError(HttpServletResponse.SC_BAD_REQUEST, error.toString()); } /** * Send a Bad Request response with JSON object mapping field error messages */ protected void respondWithBadRequest(final HttpServletResponse response, final String parameterName, final String errorMessage) throws IOException { respondWithBadRequest(response, parameterName, errorMessage, new ArrayList<String>()); } /** * Configures the content type for the provided response to contain {@link JSONObject} data * * @param response The response instance to modify * @param jsonObject The JSON data to include in the response */ protected void respondWithJson(final HttpServletResponse response, final JSONObject jsonObject) { configureResponse(ResponseTypes.JSON_OBJECT, response, jsonObject); } protected void respondWithSuccessJson(HttpServletResponse response) { JSONObject result = new JSONObject(); result.put("success", true); respondWithJson(response, result); } protected void respondWithClientApiObject(HttpServletResponse response, ClientApiObject obj) throws IOException { if (obj == null) { respondWithNotFound(response); return; } try { String jsonObject = objectMapper.writeValueAsString(obj); configureResponse(ResponseTypes.JSON_OBJECT, response, jsonObject); } catch (JsonProcessingException e) { throw new LumifyException("Could not write json", e); } } /** * Configures the content type for the provided response to contain {@link JSONArray} data * * @param response The response instance to modify * @param jsonArray The JSON data to include in the response */ protected void respondWithJson(final HttpServletResponse response, final JSONArray jsonArray) { configureResponse(ResponseTypes.JSON_ARRAY, response, jsonArray); } protected void respondWithPlaintext(final HttpServletResponse response, final String plaintext) { configureResponse(ResponseTypes.PLAINTEXT, response, plaintext); } protected void respondWithHtml(final HttpServletResponse response, final String html) { configureResponse(ResponseTypes.HTML, response, html); } protected User getUser(HttpServletRequest request) { return new ProxyUser(CurrentUser.get(request), this.userRepository); } private void configureResponse(final ResponseTypes type, final HttpServletResponse response, final Object responseData) { Preconditions.checkNotNull(response, "The provided response was invalid"); Preconditions.checkNotNull(responseData, "The provided data was invalid"); try { switch (type) { case JSON_OBJECT: response.setContentType("application/json"); response.setCharacterEncoding("UTF-8"); response.getWriter().write(responseData.toString()); break; case JSON_ARRAY: response.setContentType("application/json"); response.setCharacterEncoding("UTF-8"); response.getWriter().write(responseData.toString()); break; case PLAINTEXT: response.setContentType("text/plain"); response.setCharacterEncoding("UTF-8"); response.getWriter().write(responseData.toString()); break; case HTML: response.setContentType("text/html"); response.setCharacterEncoding("UTF-8"); response.getWriter().write(responseData.toString()); break; default: throw new RuntimeException("Unsupported response type encountered"); } if (response.getWriter().checkError()) { throw new ConnectionClosedException(); } } catch (IOException e) { throw new RuntimeException("Error occurred while writing response", e); } } protected void copyPartToOutputStream(Part part, OutputStream out) throws IOException { InputStream in = part.getInputStream(); try { IOUtils.copy(in, out); } finally { out.close(); in.close(); } } protected void copyPartToFile(Part part, File outFile) throws IOException { FileOutputStream out = new FileOutputStream(outFile); copyPartToOutputStream(part, out); } public UserRepository getUserRepository() { return userRepository; } public WorkspaceRepository getWorkspaceRepository() { return workspaceRepository; } public ObjectMapper getObjectMapper() { return objectMapper; } }