Java tutorial
/* * Copyright 2008, 2009 Electronic Business Systems Ltd. * * This file is part of GSS. * * GSS is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * GSS is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with GSS. If not, see <http://www.gnu.org/licenses/>. */ package org.gss_project.gss.server.rest; import static org.gss_project.gss.server.configuration.GSSConfigurationFactory.getConfiguration; import org.gss_project.gss.common.exceptions.InsufficientPermissionsException; import org.gss_project.gss.common.exceptions.ObjectNotFoundException; import org.gss_project.gss.common.exceptions.RpcException; import org.gss_project.gss.server.domain.FileHeader; import org.gss_project.gss.server.domain.User; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.OutputStreamWriter; import java.io.PrintWriter; import java.io.UnsupportedEncodingException; import java.util.Calendar; import java.util.Enumeration; import java.util.HashMap; import java.util.Map; import javax.crypto.Mac; import javax.crypto.spec.SecretKeySpec; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.apache.commons.codec.binary.Base64; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; /** * The servlet that handles requests for the REST API. * * @author past */ public class RequestHandler extends Webdav { /** * The request attribute containing the flag that will be used to indicate an * authentication bypass has occurred. We will have to check for authentication * later. This is a shortcut for dealing with publicly-readable files. */ protected static final String AUTH_DEFERRED_ATTR = "authDeferred"; /** * The path for the search subsystem. */ protected static final String PATH_SEARCH = "/search"; /** * The path for the user search subsystem. */ protected static final String PATH_USERS = "/users"; /** * The path for the resource manipulation subsystem. */ protected static final String PATH_FILES = FileHeader.PATH_FILES; /** * The path for the trash virtual folder. */ protected static final String PATH_TRASH = "/trash"; /** * The path for the subsystem that deals with the user attributes. */ protected static final String PATH_GROUPS = "/groups"; /** * The path for the shared resources virtual folder. */ protected static final String PATH_SHARED = "/shared"; /** * The path for the other users' shared resources virtual folder. */ protected static final String PATH_OTHERS = "/others"; /** * The path for tags created by the user. */ protected static final String PATH_TAGS = "/tags"; /** * The path for token renewal. */ protected static final String PATH_TOKEN = "/newtoken"; /** * The GSS-specific header for the request timestamp. */ protected static final String GSS_DATE_HEADER = "X-GSS-Date"; /** * The RFC 2616 date header. */ protected static final String DATE_HEADER = "Date"; /** * The Authorization HTTP header. */ protected static final String AUTHORIZATION_HEADER = "Authorization"; /** * The group parameter name. */ protected static final String GROUP_PARAMETER = "name"; /** * The username parameter name. */ protected static final String USERNAME_PARAMETER = "name"; /** * The "new folder name" parameter name. */ protected static final String NEW_FOLDER_PARAMETER = "new"; /** * The resource update parameter name. */ protected static final String RESOURCE_UPDATE_PARAMETER = "update"; /** * The resource trash parameter name. */ protected static final String RESOURCE_TRASH_PARAMETER = "trash"; /** * The resource restore parameter name. */ protected static final String RESOURCE_RESTORE_PARAMETER = "restore"; /** * The resource copy parameter name. */ protected static final String RESOURCE_COPY_PARAMETER = "copy"; /** * The resource move parameter name. */ protected static final String RESOURCE_MOVE_PARAMETER = "move"; /** * The HMAC-SHA1 hash name. */ private static final String HMAC_SHA1 = "HmacSHA1"; /** * The serial version UID of the class. */ private static final long serialVersionUID = 1L; /** * The logger. */ private static Log logger = LogFactory.getLog(RequestHandler.class); /** * Create a mapping between paths and allowed HTTP methods for fast lookup. */ private final Map<String, String> methodsAllowed = new HashMap<String, String>(7); @Override public void init() throws ServletException { super.init(); methodsAllowed.put(PATH_FILES, METHOD_GET + ", " + METHOD_POST + ", " + METHOD_DELETE + ", " + METHOD_PUT + ", " + METHOD_HEAD); methodsAllowed.put(PATH_GROUPS, METHOD_GET + ", " + METHOD_POST + ", " + METHOD_DELETE); methodsAllowed.put(PATH_OTHERS, METHOD_GET); methodsAllowed.put(PATH_SEARCH, METHOD_GET); methodsAllowed.put(PATH_USERS, METHOD_GET); methodsAllowed.put(PATH_SHARED, METHOD_GET); methodsAllowed.put(PATH_TAGS, METHOD_GET); methodsAllowed.put(PATH_TRASH, METHOD_GET + ", " + METHOD_DELETE); methodsAllowed.put(PATH_TOKEN, METHOD_GET); } /** * Return the root of every API request URL. */ protected String getApiRoot() { return getConfiguration().getString("restUrl", "http://localhost:8080/gss/rest/"); } @Override public void service(final HttpServletRequest request, final HttpServletResponse response) throws IOException, ServletException { String method = request.getMethod(); String path = getRelativePath(request); if (logger.isDebugEnabled()) logger.debug("[" + method + "] " + path); if (!isRequestValid(request)) { if (!method.equals(METHOD_GET) && !method.equals(METHOD_HEAD) && !method.equals(METHOD_POST)) { response.sendError(HttpServletResponse.SC_FORBIDDEN); return; } // Raise a flag to indicate we will have to check for // authentication later. This is a shortcut for dealing // with publicly-readable files. request.setAttribute(AUTH_DEFERRED_ATTR, true); } // Dispatch to the appropriate method handler. if (method.equals(METHOD_GET)) doGet(request, response); else if (method.equals(METHOD_POST)) doPost(request, response); else if (method.equals(METHOD_PUT)) doPut(request, response); else if (method.equals(METHOD_DELETE)) doDelete(request, response); else if (method.equals(METHOD_HEAD)) doHead(request, response); else response.sendError(HttpServletResponse.SC_BAD_REQUEST); } @Override protected void doHead(HttpServletRequest req, HttpServletResponse resp) throws IOException, ServletException { boolean authDeferred = getAuthDeferred(req); // Strip the username part String path; try { path = getUserPath(req); } catch (ObjectNotFoundException e) { if (authDeferred) { // We do not want to leak information if the request // was not authenticated. resp.sendError(HttpServletResponse.SC_FORBIDDEN); return; } resp.sendError(HttpServletResponse.SC_NOT_FOUND, e.getMessage()); return; } if (authDeferred && !path.startsWith(PATH_FILES)) { // Only files may be open to the public. resp.sendError(HttpServletResponse.SC_FORBIDDEN); return; } if (path.startsWith(PATH_GROUPS)) { resp.addHeader("Allow", methodsAllowed.get(PATH_GROUPS)); resp.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED); } else if (path.startsWith(PATH_OTHERS)) { resp.addHeader("Allow", methodsAllowed.get(PATH_OTHERS)); resp.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED); } else if (path.startsWith(PATH_SEARCH)) { resp.addHeader("Allow", methodsAllowed.get(PATH_SEARCH)); resp.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED); } else if (path.startsWith(PATH_TOKEN)) { resp.addHeader("Allow", methodsAllowed.get(PATH_TOKEN)); resp.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED); } else if (path.startsWith(PATH_USERS)) { resp.addHeader("Allow", methodsAllowed.get(PATH_USERS)); resp.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED); } else if (path.startsWith(PATH_SHARED)) { resp.addHeader("Allow", methodsAllowed.get(PATH_SHARED)); resp.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED); } else if (path.startsWith(PATH_TAGS)) { resp.addHeader("Allow", methodsAllowed.get(PATH_TAGS)); resp.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED); } else if (path.startsWith(PATH_TRASH)) { resp.addHeader("Allow", methodsAllowed.get(PATH_TRASH)); resp.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED); } else if (path.startsWith(PATH_FILES)) // Serve the requested resource, without the data content new FilesHandler(getServletContext()).serveResource(req, resp, false); else resp.sendError(HttpServletResponse.SC_NOT_FOUND, req.getRequestURI()); } /** * Handle storing and updating file resources. * * @param req The servlet request we are processing * @param resp The servlet response we are creating * @throws IOException if the response cannot be sent */ @Override protected void doPut(HttpServletRequest req, HttpServletResponse resp) throws IOException { // TODO: fix code duplication between doPut() and Webdav.doPut() // Strip the username part String path; try { path = getUserPath(req); } catch (ObjectNotFoundException e) { resp.sendError(HttpServletResponse.SC_NOT_FOUND, e.getMessage()); return; } if (path.startsWith(PATH_GROUPS)) { resp.addHeader("Allow", methodsAllowed.get(PATH_GROUPS)); resp.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED); } else if (path.startsWith(PATH_OTHERS)) { resp.addHeader("Allow", methodsAllowed.get(PATH_OTHERS)); resp.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED); } else if (path.startsWith(PATH_SEARCH)) { resp.addHeader("Allow", methodsAllowed.get(PATH_SEARCH)); resp.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED); } else if (path.startsWith(PATH_TOKEN)) { resp.addHeader("Allow", methodsAllowed.get(PATH_TOKEN)); resp.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED); } else if (path.startsWith(PATH_USERS)) { resp.addHeader("Allow", methodsAllowed.get(PATH_USERS)); resp.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED); } else if (path.startsWith(PATH_SHARED)) { resp.addHeader("Allow", methodsAllowed.get(PATH_SHARED)); resp.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED); } else if (path.startsWith(PATH_TAGS)) { resp.addHeader("Allow", methodsAllowed.get(PATH_TAGS)); resp.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED); } else if (path.startsWith(PATH_TRASH)) { resp.addHeader("Allow", methodsAllowed.get(PATH_TRASH)); resp.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED); } else if (path.startsWith(PATH_FILES)) new FilesHandler(getServletContext()).putResource(req, resp); else resp.sendError(HttpServletResponse.SC_NOT_FOUND, req.getRequestURI()); } @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException, ServletException { boolean authDeferred = getAuthDeferred(req); // Strip the username part String path; try { path = getUserPath(req); } catch (ObjectNotFoundException e) { if (authDeferred) { // We do not want to leak information if the request // was not authenticated. resp.sendError(HttpServletResponse.SC_FORBIDDEN); return; } resp.sendError(HttpServletResponse.SC_NOT_FOUND, e.getMessage()); return; } if (authDeferred && !path.startsWith(PATH_FILES)) { // Only files may be open to the public. resp.sendError(HttpServletResponse.SC_FORBIDDEN); return; } // Dispatch according to the specified namespace if (path.equals("") || path.equals("/")) new UserHandler().serveUser(req, resp); else if (path.startsWith(PATH_FILES)) // Serve the requested resource, including the data content new FilesHandler(getServletContext()).serveResource(req, resp, true); else if (path.startsWith(PATH_TRASH)) new TrashHandler().serveTrash(req, resp); else if (path.startsWith(PATH_SEARCH)) new SearchHandler().serveSearchResults(req, resp); else if (path.startsWith(PATH_USERS)) new UserSearchHandler().serveResults(req, resp); else if (path.startsWith(PATH_GROUPS)) new GroupsHandler().serveGroups(req, resp); else if (path.startsWith(PATH_SHARED)) new SharedHandler().serveShared(req, resp); else if (path.startsWith(PATH_OTHERS)) new OthersHandler().serveOthers(req, resp); else if (path.startsWith(PATH_TAGS)) new TagsHandler().serveTags(req, resp); else if (path.startsWith(PATH_TOKEN)) new TokenHandler().newToken(req, resp); else resp.sendError(HttpServletResponse.SC_NOT_FOUND, req.getRequestURI()); } /** * Handle a Delete request. * * @param req The servlet request we are processing * @param resp The servlet response we are processing * @throws IOException if the response cannot be sent */ @Override protected void doDelete(HttpServletRequest req, HttpServletResponse resp) throws IOException { // Strip the username part String path; try { path = getUserPath(req); } catch (ObjectNotFoundException e) { resp.sendError(HttpServletResponse.SC_NOT_FOUND, e.getMessage()); return; } if (path.startsWith(PATH_OTHERS)) { resp.addHeader("Allow", methodsAllowed.get(PATH_OTHERS)); resp.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED); } else if (path.startsWith(PATH_SEARCH)) { resp.addHeader("Allow", methodsAllowed.get(PATH_SEARCH)); resp.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED); } else if (path.startsWith(PATH_TOKEN)) { resp.addHeader("Allow", methodsAllowed.get(PATH_TOKEN)); resp.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED); } else if (path.startsWith(PATH_USERS)) { resp.addHeader("Allow", methodsAllowed.get(PATH_USERS)); resp.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED); } else if (path.startsWith(PATH_SHARED)) { resp.addHeader("Allow", methodsAllowed.get(PATH_SHARED)); resp.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED); } else if (path.startsWith(PATH_TAGS)) { resp.addHeader("Allow", methodsAllowed.get(PATH_TAGS)); resp.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED); } else if (path.startsWith(PATH_GROUPS)) new GroupsHandler().deleteGroup(req, resp); else if (path.startsWith(PATH_TRASH)) new TrashHandler().emptyTrash(req, resp); else if (path.startsWith(PATH_FILES)) new FilesHandler(getServletContext()).deleteResource(req, resp); else resp.sendError(HttpServletResponse.SC_NOT_FOUND, req.getRequestURI()); } @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException { boolean authDeferred = getAuthDeferred(req); // Strip the username part String path; try { path = getUserPath(req); } catch (ObjectNotFoundException e) { if (authDeferred) { // We do not want to leak information if the request // was not authenticated. resp.sendError(HttpServletResponse.SC_FORBIDDEN); return; } resp.sendError(HttpServletResponse.SC_NOT_FOUND, e.getMessage()); return; } if (authDeferred && !path.startsWith(PATH_FILES)) { // Only POST to files may be authenticated without an Authorization header. resp.sendError(HttpServletResponse.SC_FORBIDDEN); return; } if (path.startsWith(PATH_OTHERS)) { resp.addHeader("Allow", methodsAllowed.get(PATH_OTHERS)); resp.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED); } else if (path.startsWith(PATH_SEARCH)) { resp.addHeader("Allow", methodsAllowed.get(PATH_SEARCH)); resp.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED); } else if (path.startsWith(PATH_TOKEN)) { resp.addHeader("Allow", methodsAllowed.get(PATH_TOKEN)); resp.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED); } else if (path.startsWith(PATH_USERS)) { resp.addHeader("Allow", methodsAllowed.get(PATH_USERS)); resp.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED); } else if (path.startsWith(PATH_SHARED)) { resp.addHeader("Allow", methodsAllowed.get(PATH_SHARED)); resp.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED); } else if (path.startsWith(PATH_TAGS)) { resp.addHeader("Allow", methodsAllowed.get(PATH_TAGS)); resp.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED); } else if (path.startsWith(PATH_GROUPS)) new GroupsHandler().postGroup(req, resp); else if (path.startsWith(PATH_TRASH)) { resp.addHeader("Allow", methodsAllowed.get(PATH_TRASH)); resp.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED); } else if (path.startsWith(PATH_FILES)) new FilesHandler(getServletContext()).postResource(req, resp); else if (path.equals("/")) new UserHandler().postUser(req, resp); else resp.sendError(HttpServletResponse.SC_NOT_FOUND, req.getRequestURI()); } /** * Return the path inside the user namespace. * * @param req the HTTP request * @return the path after the username part has been removed * @throws ObjectNotFoundException if the namespace owner was not found */ private String getUserPath(HttpServletRequest req) throws ObjectNotFoundException { String path = getRelativePath(req); if (path.length() < 2) return path; int slash = path.substring(1).indexOf('/'); if (slash == -1) return path; String owner = path.substring(1, slash + 1); User o; try { o = getService().findUser(owner); } catch (RpcException e) { logger.error("", e); throw new ObjectNotFoundException("User " + owner + " not found, due to internal server error"); } if (o != null) { req.setAttribute(OWNER_ATTRIBUTE, o); return path.substring(slash + 1); } if (!path.startsWith(PATH_SEARCH) && !path.startsWith(PATH_USERS) && !path.startsWith(PATH_TOKEN)) throw new ObjectNotFoundException("User " + owner + " not found"); return path; } /** * Retrieve the request context path with or without a trailing slash * according to the provided argument. * * @param req the HTTP request * @param withTrailingSlash a flag that denotes whether the path should * end with a slash * @return the context path */ protected String getContextPath(HttpServletRequest req, boolean withTrailingSlash) { String contextPath = req.getRequestURL().toString(); if (withTrailingSlash) return contextPath.endsWith("/") ? contextPath : contextPath + '/'; return contextPath.endsWith("/") ? contextPath.substring(0, contextPath.length() - 1) : contextPath; } /** * @param req * @param resp * @param json * @throws UnsupportedEncodingException * @throws IOException */ protected void sendJson(HttpServletRequest req, HttpServletResponse resp, String json) throws UnsupportedEncodingException, IOException { ByteArrayOutputStream stream = new ByteArrayOutputStream(); OutputStreamWriter osWriter = new OutputStreamWriter(stream, "UTF8"); PrintWriter writer = new PrintWriter(osWriter); writer.write(json); writer.flush(); resp.setContentType("application/json;charset=UTF-8"); resp.setBufferSize(output); try { copy(null, new ByteArrayInputStream(stream.toByteArray()), resp.getOutputStream(), req, null); } catch (ObjectNotFoundException e) { // This should never happen with a null first parameter. logger.error("", e); resp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); return; } catch (InsufficientPermissionsException e) { // This should never happen with a null first parameter. logger.error("", e); resp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); return; } catch (RpcException e) { logger.error("", e); resp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); return; } } /** * Retrieve the path to the requested resource after removing the user namespace * part and the subsequent namespace part that differentiates resources like files, * groups, trash, etc. * * @param req the HTTP request * @param namespace the subnamespace * @return the inner path */ protected String getInnerPath(HttpServletRequest req, String namespace) { // Strip the username part String path; try { path = getUserPath(req); } catch (ObjectNotFoundException e) { throw new RuntimeException(e.getMessage()); } // Chop the resource namespace part path = path.substring(namespace.length()); return path; } /** * Confirms the validity of the request. * * @param request the incoming HTTP request * @return true if the request is valid, false otherwise */ private boolean isRequestValid(HttpServletRequest request) { if (logger.isDebugEnabled()) { Enumeration headers = request.getHeaderNames(); while (headers.hasMoreElements()) { String h = (String) headers.nextElement(); logger.debug(h + ": " + request.getHeader(h)); } } // Fetch the timestamp used to guard against replay attacks. long timestamp = 0; boolean useGssDateHeader = true; try { timestamp = request.getDateHeader(GSS_DATE_HEADER); if (timestamp == -1) { useGssDateHeader = false; timestamp = request.getDateHeader(DATE_HEADER); } } catch (IllegalArgumentException e) { return false; } // Fetch the Authorization header and find the user specified in it. String auth = request.getHeader(AUTHORIZATION_HEADER); if (auth == null) return false; String[] authParts = auth.split(" "); if (authParts.length != 2) return false; String username = authParts[0]; String signature = authParts[1]; User user = null; try { user = getService().findUser(username); } catch (RpcException e) { return false; } if (user == null) return false; request.setAttribute(USER_ATTRIBUTE, user); // Validate the signature in the Authorization header. String dateHeader = useGssDateHeader ? request.getHeader(GSS_DATE_HEADER) : request.getHeader(DATE_HEADER); String data; // Remove the servlet path from the request URI. String p = request.getRequestURI(); String servletPath = request.getContextPath() + request.getServletPath(); p = p.substring(servletPath.length()); data = request.getMethod() + dateHeader + p; return isSignatureValid(signature, user, data); } /** * Calculates the signature for the specified data String and then * compares it against the provided signature. If the signatures match, * the method returns true. Otherwise it returns false. * * @param signature the signature to compare against * @param user the current user * @param data the data to sign * @return true if the calculated signature matches the supplied one */ protected boolean isSignatureValid(String signature, User user, String data) { if (logger.isDebugEnabled()) logger.debug("server pre-signing data: " + data); String serverSignature = null; // If the authentication token is not valid, the user must get another one. if (user.getAuthToken() == null) return false; // Get an HMAC-SHA1 key from the authentication token. SecretKeySpec signingKey = new SecretKeySpec(user.getAuthToken(), HMAC_SHA1); try { // Get an HMAC-SHA1 Mac instance and initialize with the signing key. Mac mac = Mac.getInstance(HMAC_SHA1); mac.init(signingKey); // Compute the HMAC on input data bytes. byte[] rawHmac = mac.doFinal(data.getBytes()); serverSignature = new String(Base64.encodeBase64(rawHmac), "US-ASCII"); } catch (Exception e) { logger.error("Error while creating signature", e); return false; } if (logger.isDebugEnabled()) logger.debug("Signature: client=" + signature + ", server=" + serverSignature); if (!serverSignature.equals(signature)) return false; return true; } protected boolean getAuthDeferred(HttpServletRequest req) { Boolean attr = (Boolean) req.getAttribute(AUTH_DEFERRED_ATTR); return attr == null ? false : attr; } /** * Return the actual requested path in the API namespace. * * @param request the servlet request we are processing * @return the relative path */ @Override protected String getRelativePath(HttpServletRequest request) { // Remove the servlet path from the request URI. String p = request.getRequestURI(); String servletPath = request.getContextPath() + request.getServletPath(); String result = p.substring(servletPath.length()); if (result == null || result.equals("")) result = "/"; return result; } /** * Reject illegal resource names, like '.' or '..' or resource names containing '/'. */ protected boolean isValidResourceName(String name) { if (".".equals(name) || "..".equals(name) || name.contains("/")) return false; return true; } }