///////////////////////////////////////////////////////////////////////////// // // Project ProjectForge Community Edition // www.projectforge.org // // Copyright (C) 2001-2014 Kai Reinhard (k.reinhard@micromata.de) // // ProjectForge is dual-licensed. // // This community edition 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; version 3 of the License. // // This community edition 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 this program; if not, see http://www.gnu.org/licenses/. // ///////////////////////////////////////////////////////////////////////////// package org.projectforge.web; import java.io.IOException; import java.util.Collections; import java.util.Enumeration; import java.util.Locale; import javax.servlet.Filter; import javax.servlet.FilterChain; import javax.servlet.FilterConfig; import javax.servlet.ServletException; import javax.servlet.ServletRequest; import javax.servlet.ServletResponse; import javax.servlet.http.Cookie; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequestWrapper; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; import org.apache.commons.lang.StringUtils; import org.apache.log4j.MDC; import org.projectforge.common.NumberHelper; import org.projectforge.common.StringHelper; import org.projectforge.registry.Registry; import org.projectforge.user.Login; import org.projectforge.user.PFUserContext; import org.projectforge.user.PFUserDO; import org.projectforge.user.UserDao; import org.projectforge.web.core.LogoServlet; import org.projectforge.web.meb.SMSReceiverServlet; import org.projectforge.web.wicket.WicketUtils; /** * Ensures that an user is logged in and put the user id, locale and ip to the logging mdc.<br/> * Ignores login for: /ProjectForge/wa/resources/* with the suffixes: *.js, *.css, *.gif, *.png. <br/> * Don't forget to call setServletContext on applications start-up! */ public class UserFilter implements Filter { /** * Set after stay-logged-in functionality (used by MenuMobilePage). */ public static final String USER_ATTR_STAY_LOGGED_IN = "stayLoggedIn"; private static final org.apache.log4j.Logger log = org.apache.log4j.Logger.getLogger(UserFilter.class); private final static String SESSION_KEY_USER = "UserFilter.user"; private static final String COOKIE_NAME_FOR_STAY_LOGGED_IN = "stayLoggedIn"; private static final int COOKIE_MAX_AGE = 30 * 24 * 3600; // 30 days. private static String IGNORE_PREFIX_WICKET; private static String IGNORE_PREFIX_DOC; private static String IGNORE_PREFIX_SITE_DOC; private static String IGNORE_PREFIX_LOGO; private static String IGNORE_PREFIX_SMS_REVEIVE_SERVLET; private static String WICKET_PAGES_PREFIX; public static String CONTEXT_PATH; private static UserDao userDao; private static boolean updateRequiredFirst = false; public static void initialize(final UserDao userDao, final String contextPath) { UserFilter.userDao = userDao; CONTEXT_PATH = contextPath; WICKET_PAGES_PREFIX = CONTEXT_PATH + "/" + WicketUtils.WICKET_APPLICATION_PATH; IGNORE_PREFIX_WICKET = WICKET_PAGES_PREFIX + "resources"; IGNORE_PREFIX_DOC = contextPath + "/secure/doc"; IGNORE_PREFIX_SITE_DOC = contextPath + "/secure/site"; IGNORE_PREFIX_LOGO = contextPath + "/" + LogoServlet.BASE_URL; IGNORE_PREFIX_SMS_REVEIVE_SERVLET = contextPath + "/" + SMSReceiverServlet.URL; } public static void setUpdateRequiredFirst(final boolean value) { updateRequiredFirst = value; } public static boolean isUpdateRequiredFirst() { return updateRequiredFirst; } public static Cookie getStayLoggedInCookie(final HttpServletRequest request) { return getCookie(request, COOKIE_NAME_FOR_STAY_LOGGED_IN); } public static Cookie getCookie(final HttpServletRequest request, final String name) { final Cookie[] cookies = request.getCookies(); if (cookies != null) { for (final Cookie cookie : cookies) { if (name.equals(cookie.getName()) == true) { return cookie; } } } return null; } /** * Adds or refresh the given cookie. * @param request * @param response * @param stayLoggedInCookie */ public static void addStayLoggedInCookie(final HttpServletRequest request, final HttpServletResponse response, final Cookie stayLoggedInCookie) { stayLoggedInCookie.setMaxAge(COOKIE_MAX_AGE); stayLoggedInCookie.setPath("/"); if (request.isSecure() == true) { log.debug("Set secure cookie"); stayLoggedInCookie.setSecure(true); } else { log.debug("Set unsecure cookie"); } response.addCookie(stayLoggedInCookie); // Refresh cookie. } public static void login(final HttpServletRequest request, final PFUserDO user) { final HttpSession session = request.getSession(); final PFUserDO storedUser = new PFUserDO(); copyUser(user, storedUser); session.setAttribute(SESSION_KEY_USER, storedUser); } public static void updateUser(final HttpServletRequest request, final PFUserDO user) { final PFUserDO origUser = getUser(request); if (origUser.getId().equals(user.getId()) == false) { log.error("**** Intruser? User id of the session user is different to the id of the given user!"); return; } copyUser(user, origUser); } private static void copyUser(final PFUserDO srcUser, final PFUserDO destUser) { destUser.copyValuesFrom(srcUser, "password", "stayLoggedInKey"); } public static PFUserDO getUser(final HttpServletRequest request) { final HttpSession session = request.getSession(); if (session == null) { return null; } return (PFUserDO) session.getAttribute(SESSION_KEY_USER); } public void destroy() { // do nothing } public void init(final FilterConfig filterConfig) throws ServletException { // do nothing } public void doFilter(final ServletRequest req, final ServletResponse resp, final FilterChain chain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest) req; if (log.isDebugEnabled() == true) { log.debug("doFilter " + request.getRequestURI() + ": " + request.getSession().getId()); final Cookie[] cookies = request.getCookies(); if (cookies != null) { for (final Cookie cookie : cookies) { log.debug("Cookie " + cookie.getName() + ", path=" + cookie.getPath() + ", value=" + cookie.getValue() + ", secure=" + cookie.getVersion() + ", maxAge=" + cookie.getMaxAge() + ", domain=" + cookie.getDomain()); } } } final HttpServletResponse response = (HttpServletResponse) resp; PFUserDO user = null; try { MDC.put("ip", request.getRemoteAddr()); MDC.put("session", request.getSession().getId()); if (ignoreFilterFor(request) == true) { // Ignore the filter for this request: if (log.isDebugEnabled() == true) { log.debug("Ignore: " + request.getRequestURI()); } chain.doFilter(request, response); } else { // final boolean sessionTimeout = request.isRequestedSessionIdValid() == false; user = (PFUserDO) request.getSession().getAttribute(SESSION_KEY_USER); if (user != null) { if (updateRequiredFirst == false) { // Get the fresh user from the user cache (not in maintenance mode because user group cache is perhaps not initialized correctly // if updates of e. g. the user table are necessary. user = Registry.instance().getUserGroupCache().getUser(user.getId()); } if (log.isDebugEnabled() == true) { log.debug("User found in session: " + request.getRequestURI()); } } else if (updateRequiredFirst == false) { // Ignore stay-logged-in if redirect to update page is required. user = checkStayLoggedIn(request, response); if (user != null) { if (log.isDebugEnabled() == true) { log.debug("User's stay logged-in cookie found: " + request.getRequestURI()); } user.setAttribute(USER_ATTR_STAY_LOGGED_IN, true); // Used by MenuMobilePage. UserFilter.login(request, user); } } if (user != null) { MDC.put("user", user.getUsername()); PFUserContext.setUser(user); request = decorateWithLocale(request, user); chain.doFilter(request, response); } else { if (((HttpServletRequest) req).getRequestURI().startsWith(WICKET_PAGES_PREFIX) == true) { // Access-checking is done by Wicket, not by this filter: request = decorateWithLocale(request, user); chain.doFilter(request, response); } else { response.getWriter().append("No access."); } } } } finally { PFUserContext.setUser(null); MDC.remove("ip"); MDC.remove("session"); if (user != null) { MDC.remove("user"); } if (log.isDebugEnabled() == true) { log.debug("doFilter finished for " + request.getRequestURI() + ": " + request.getSession().getId()); } } } /** * User is not logged. Checks a stay-logged-in-cookie. * @return user if valid cookie found, otherwise null. */ private PFUserDO checkStayLoggedIn(final HttpServletRequest request, final HttpServletResponse response) { final Cookie sessionIdCookie = getCookie(request, "JSESSIONID"); if (sessionIdCookie != null && sessionIdCookie.getSecure() == false && request.isSecure() == true) { // Hack for developers: Safari (may-be also other browsers) don't update unsecure cookies for secure connections. This seems to be // occurring // if you use ProjectForge on localhost with http and https (e. g. for testing). You have to delete this cookie normally in your // browser. final Cookie cookie = new Cookie("JSESSIONID", "to be deleted"); cookie.setMaxAge(0); cookie.setPath(sessionIdCookie.getPath()); // Doesn't work for Safari: getPath() returns always null! response.addCookie(cookie); } final Cookie stayLoggedInCookie = getStayLoggedInCookie(request); if (stayLoggedInCookie != null) { final String value = stayLoggedInCookie.getValue(); if (StringUtils.isBlank(value) == true) { return null; } final String[] values = value.split(":"); if (values == null || values.length != 3) { log.warn("Invalid cookie found: " + value); return null; } final Integer userId = NumberHelper.parseInteger(values[0]); final PFUserDO user = userDao.internalGetById(userId); if (user == null) { log.warn("Invalid cookie found (user not found): " + value); return null; } if (user.getUsername().equals(values[1]) == false) { log.warn("Invalid cookie found (user name wrong, maybe changed): " + value); return null; } if (values[2] == null || values[2].equals(user.getStayLoggedInKey()) == false) { log.warn("Invalid cookie found (stay-logged-in key, maybe renewed and/or user password changed): " + value); return null; } if (Login.getInstance().checkStayLoggedIn(user) == false) { log.warn("Stay-logged-in wasn't accepted by the login handler: " + user.getUserDisplayname()); return null; } addStayLoggedInCookie(request, response, stayLoggedInCookie); log.info("User successfully logged in using stay-logged-in method: " + user.getUserDisplayname()); return user; } return null; } /** * @param request * @param user * @return */ protected HttpServletRequest decorateWithLocale(HttpServletRequest request, final PFUserDO user) { final Locale locale = PFUserContext.getLocale(request.getLocale()); request = new HttpServletRequestWrapper(request) { @Override public Locale getLocale() { return locale; } @Override public Enumeration< ? > getLocales() { return Collections.enumeration(Collections.singleton(locale)); } }; return request; } /** * Will be called by doFilter. * @param req from do Filter. * @return true, if the filter should ignore this request, otherwise false. */ protected boolean ignoreFilterFor(final ServletRequest req) { final HttpServletRequest hreq = (HttpServletRequest) req; final String uri = hreq.getRequestURI(); // If you have an NPE you have probably forgotten to call setServletContext on applications start-up. // Paranoia setting. May-be there could be a vulnerability with request parameters: if (uri.contains("?") == false) { // if (uri.startsWith(IGNORE_PREFIX_WICKET) && StringHelper.endsWith(uri, ".js", ".css", ".gif", ".png") == true) { // No access checking for Wicket resources. // return true; // } else if (StringHelper.startsWith(uri, IGNORE_PREFIX_DOC, IGNORE_PREFIX_SITE_DOC) == true // && StringHelper.endsWith(uri, ".html", ".pdf", ".js", ".css", ".gif", ".png") == true) { // No access checking for documentation (including site doc). // return true; // } else if (StringHelper.startsWith(uri, IGNORE_PREFIX_LOGO, IGNORE_PREFIX_SMS_REVEIVE_SERVLET) == true) { // No access checking for logo and sms receiver servlet. // The sms receiver servlet has its own authentification (key). return true; } } return false; } }