From a8ad9231ab9da5fcc0792ccc769168cba9f78271 Mon Sep 17 00:00:00 2001 From: Scott McCrory Date: Sun, 13 Nov 2005 22:29:13 +0000 Subject: [PATCH] Added initial integration of SEC-29 "Save POST request parameters before redirect" for peer review. See http://opensource2.atlassian.com/projects/spring/browse/SEC-29 for more info. --- .../web/SandboxSecurityEnforcementFilter.java | 308 ++++++++++++ ...urityContextHolderAwareRequestWrapper.java | 475 ++++++++++++++++++ .../wrapper/redirect/Enumerator.java | 143 ++++++ .../wrapper/redirect/FastHttpDateFormat.java | 226 +++++++++ .../redirect/SavedHttpServletRequest.java | 352 +++++++++++++ .../wrapper/redirect/package.html | 8 + 6 files changed, 1512 insertions(+) create mode 100644 sandbox/src/main/java/org/acegisecurity/intercept/web/SandboxSecurityEnforcementFilter.java create mode 100644 sandbox/src/main/java/org/acegisecurity/wrapper/SandboxSecurityContextHolderAwareRequestWrapper.java create mode 100644 sandbox/src/main/java/org/acegisecurity/wrapper/redirect/Enumerator.java create mode 100644 sandbox/src/main/java/org/acegisecurity/wrapper/redirect/FastHttpDateFormat.java create mode 100644 sandbox/src/main/java/org/acegisecurity/wrapper/redirect/SavedHttpServletRequest.java create mode 100644 sandbox/src/main/java/org/acegisecurity/wrapper/redirect/package.html diff --git a/sandbox/src/main/java/org/acegisecurity/intercept/web/SandboxSecurityEnforcementFilter.java b/sandbox/src/main/java/org/acegisecurity/intercept/web/SandboxSecurityEnforcementFilter.java new file mode 100644 index 0000000000..b9d44762ec --- /dev/null +++ b/sandbox/src/main/java/org/acegisecurity/intercept/web/SandboxSecurityEnforcementFilter.java @@ -0,0 +1,308 @@ +/* Copyright 2004, 2005 Acegi Technology Pty Limited + * + * 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 net.sf.acegisecurity.intercept.web; + +import java.io.IOException; + +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.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import net.sf.acegisecurity.AccessDeniedException; +import net.sf.acegisecurity.AuthenticationException; +import net.sf.acegisecurity.AuthenticationTrustResolver; +import net.sf.acegisecurity.AuthenticationTrustResolverImpl; +import net.sf.acegisecurity.InsufficientAuthenticationException; +import net.sf.acegisecurity.context.SecurityContextHolder; +import net.sf.acegisecurity.ui.AbstractProcessingFilter; +import net.sf.acegisecurity.util.PortResolver; +import net.sf.acegisecurity.util.PortResolverImpl; +import net.sf.acegisecurity.wrapper.redirect.SavedHttpServletRequest; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.util.Assert; + +/** + * Wraps requests to the {@link FilterSecurityInterceptor}. + * + *

+ * This filter is necessary because it provides the bridge between incoming + * requests and the FilterSecurityInterceptor instance. + *

+ * + *

+ * If an {@link AuthenticationException} is detected, the filter will launch + * the authenticationEntryPoint. This allows common handling of + * authentication failures originating from any subclass of {@link + * net.sf.acegisecurity.intercept.AbstractSecurityInterceptor}. + *

+ * + *

+ * If an {@link AccessDeniedException} is detected, the filter will determine + * whether or not the user is an anonymous user. If they are an anonymous + * user, the authenticationEntryPoint will be launched. If they + * are not an anonymous user, the filter will respond with a + * HttpServletResponse.SC_FORBIDDEN (403 error). In addition, + * the AccessDeniedException itself will be placed in the + * HttpSession attribute keyed against {@link + * #ACEGI_SECURITY_ACCESS_DENIED_EXCEPTION_KEY} (to allow access to the stack + * trace etc). Again, this allows common access denied handling irrespective + * of the originating security interceptor. + *

+ * + *

+ * To use this filter, it is necessary to specify the following properties: + *

+ * + * + * + *

+ * Do not use this class directly. Instead configure + * web.xml to use the {@link + * net.sf.acegisecurity.util.FilterToBeanProxy}. + *

+ * + * @author Ben Alex + * @author colin sampaleanu + * @author Andrey Grebnev <andrey.grebnev@blandware.com> + * @version $Id$ + */ +public class SandboxSecurityEnforcementFilter implements Filter, InitializingBean { + + private static final Log logger = LogFactory.getLog(SecurityEnforcementFilter.class); + + public static final String ACEGI_SECURITY_ACCESS_DENIED_EXCEPTION_KEY = "ACEGI_SECURITY_403_EXCEPTION"; + public static final String SAVED_REQUEST_SESSION_ATTRIBUTE = "net.sf.acegisecurity.intercept.web.SAVED_REQUEST_SESSION_ATTRIBUTE"; + + private AuthenticationEntryPoint authenticationEntryPoint; + private AuthenticationTrustResolver authenticationTrustResolver = new AuthenticationTrustResolverImpl(); + private FilterSecurityInterceptor filterSecurityInterceptor; + private PortResolver portResolver = new PortResolverImpl(); + private boolean createSessionAllowed = true; + + /** + * Sets the AuthenticationEntryPoint. + * @param authenticationEntryPoint The authentication entry point. + */ + public void setAuthenticationEntryPoint( + AuthenticationEntryPoint authenticationEntryPoint) { + this.authenticationEntryPoint = authenticationEntryPoint; + } + + /** + * Returns the AuthenticationEntryPoint. + * @return The authentication entry point. + */ + public AuthenticationEntryPoint getAuthenticationEntryPoint() { + return authenticationEntryPoint; + } + + /** + * Sets the AuthenticationTrustResolver. + * @param authenticationTrustResolver The AuthenticationTrustResolver. + */ + public void setAuthenticationTrustResolver( + AuthenticationTrustResolver authenticationTrustResolver) { + this.authenticationTrustResolver = authenticationTrustResolver; + } + + /** + * If true, indicates that SecurityEnforcementFilter is permitted + * to store the target URL and exception information in the HttpSession (the + * default). In situations where you do not wish to unnecessarily create HttpSessions + * - because the user agent will know the failed URL, such as with BASIC or Digest authentication + * - you may wish to set this property to false. Remember to also set the + * {@link net.sf.acegisecurity.context.HttpSessionContextIntegrationFilter#allowSessionCreation} + * to false if you set this property to false. + * + * @return true if the HttpSession will be used to store information + * about the failed request, false if the HttpSession will not be + * used + */ + public boolean isCreateSessionAllowed() { + return createSessionAllowed; + } + + public void setCreateSessionAllowed(boolean createSessionAllowed) { + this.createSessionAllowed = createSessionAllowed; + } + + public AuthenticationTrustResolver getAuthenticationTrustResolver() { + return authenticationTrustResolver; + } + + public void setFilterSecurityInterceptor( + FilterSecurityInterceptor filterSecurityInterceptor) { + this.filterSecurityInterceptor = filterSecurityInterceptor; + } + + public FilterSecurityInterceptor getFilterSecurityInterceptor() { + return filterSecurityInterceptor; + } + + public void setPortResolver(PortResolver portResolver) { + this.portResolver = portResolver; + } + + public PortResolver getPortResolver() { + return portResolver; + } + + public void afterPropertiesSet() throws Exception { + Assert.notNull(authenticationEntryPoint, + "authenticationEntryPoint must be specified"); + Assert.notNull(filterSecurityInterceptor, + "filterSecurityInterceptor must be specified"); + Assert.notNull(portResolver, "portResolver must be specified"); + Assert.notNull(authenticationTrustResolver, + "authenticationTrustResolver must be specified"); + } + + public void destroy() { + } + + public void doFilter(ServletRequest request, ServletResponse response, + FilterChain chain) throws IOException, ServletException { + if (!(request instanceof HttpServletRequest)) { + throw new ServletException("HttpServletRequest required"); + } + + if (!(response instanceof HttpServletResponse)) { + throw new ServletException("HttpServletResponse required"); + } + + FilterInvocation fi = new FilterInvocation(request, response, chain); + + try { + filterSecurityInterceptor.invoke(fi); + + if (logger.isDebugEnabled()) { + logger.debug("Chain processed normally"); + } + } catch (AuthenticationException authentication) { + if (logger.isDebugEnabled()) { + logger.debug("Authentication exception occurred; redirecting to authentication entry point", + authentication); + } + + sendStartAuthentication(fi, authentication); + } catch (AccessDeniedException accessDenied) { + if (authenticationTrustResolver.isAnonymous( + SecurityContextHolder.getContext().getAuthentication())) { + if (logger.isDebugEnabled()) { + logger.debug("Access is denied (user is anonymous); redirecting to authentication entry point", + accessDenied); + } + + sendStartAuthentication(fi, + new InsufficientAuthenticationException( + "Full authentication is required to access this resource")); + } else { + if (logger.isDebugEnabled()) { + logger.debug("Access is denied (user is not anonymous); sending back forbidden response", + accessDenied); + } + + sendAccessDeniedError(fi, accessDenied); + } + } catch (ServletException e) { + throw e; + } catch (IOException e) { + throw e; + } catch (Throwable otherException) { + throw new ServletException(otherException); + } + } + + public void init(FilterConfig filterConfig) throws ServletException { + } + + protected void sendAccessDeniedError(FilterInvocation fi, + AccessDeniedException accessDenied) + throws ServletException, IOException { + if (createSessionAllowed) { + ((HttpServletRequest) fi.getRequest()).getSession().setAttribute(ACEGI_SECURITY_ACCESS_DENIED_EXCEPTION_KEY, + accessDenied); + } + + ((HttpServletResponse) fi.getResponse()).sendError(HttpServletResponse.SC_FORBIDDEN, + accessDenied.getMessage()); // 403 + } + + protected void sendStartAuthentication(FilterInvocation fi, + AuthenticationException reason) throws ServletException, IOException { + + // Get the HttpServletRequest + HttpServletRequest request = (HttpServletRequest) fi.getRequest(); + + // Save this original request on the session in case + // SecurityContextHolderAwareRequestWrapper has to resume it after (re)authentication. + request.getSession().setAttribute(SAVED_REQUEST_SESSION_ATTRIBUTE, SavedHttpServletRequest.saveRequest(request)); + + int port = portResolver.getServerPort(request); + boolean includePort = true; + + if ("http".equals(request.getScheme().toLowerCase()) && (port == 80)) { + includePort = false; + } + + if ("https".equals(request.getScheme().toLowerCase()) && (port == 443)) { + includePort = false; + } + + // Build the target URL from the request + String targetUrl = request.getScheme() + "://" + + request.getServerName() + ((includePort) ? (":" + port) : "") + + request.getContextPath() + fi.getRequestUrl(); + + if (logger.isDebugEnabled()) { + logger.debug( + "Authentication entry point being called; target URL added to Session: " + + targetUrl); + } + + if (createSessionAllowed) { + ((HttpServletRequest) request).getSession().setAttribute(AbstractProcessingFilter.ACEGI_SECURITY_TARGET_URL_KEY, + targetUrl); + } + + authenticationEntryPoint.commence(request, + (HttpServletResponse) fi.getResponse(), reason); + } + +} diff --git a/sandbox/src/main/java/org/acegisecurity/wrapper/SandboxSecurityContextHolderAwareRequestWrapper.java b/sandbox/src/main/java/org/acegisecurity/wrapper/SandboxSecurityContextHolderAwareRequestWrapper.java new file mode 100644 index 0000000000..0e4982c0d1 --- /dev/null +++ b/sandbox/src/main/java/org/acegisecurity/wrapper/SandboxSecurityContextHolderAwareRequestWrapper.java @@ -0,0 +1,475 @@ +/* Copyright 2004, 2005 Acegi Technology Pty Limited + * + * 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 net.sf.acegisecurity.wrapper; + +import java.security.Principal; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Enumeration; +import java.util.Iterator; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.TimeZone; + +import javax.servlet.http.Cookie; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletRequestWrapper; +import javax.servlet.http.HttpSession; + +import net.sf.acegisecurity.Authentication; +import net.sf.acegisecurity.AuthenticationTrustResolver; +import net.sf.acegisecurity.AuthenticationTrustResolverImpl; +import net.sf.acegisecurity.GrantedAuthority; +import net.sf.acegisecurity.UserDetails; +import net.sf.acegisecurity.context.SecurityContextHolder; +import net.sf.acegisecurity.intercept.web.SandboxSecurityEnforcementFilter; +import net.sf.acegisecurity.wrapper.redirect.Enumerator; +import net.sf.acegisecurity.wrapper.redirect.FastHttpDateFormat; +import net.sf.acegisecurity.wrapper.redirect.SavedHttpServletRequest; + +/** + * An Acegi Security-aware HttpServletRequestWrapper, which uses + * the SecurityContext-defined Authentication + * object for + * {@link SecurityContextHolderAwareRequestWrapper#isUserInRole(java.lang.String)} + * and {@link javax.servlet.http.HttpServletRequestWrapper#getRemoteUser()} + * responses. + *

+ * Provides request parameters, headers, cookies from original requrest or saved request. + *

+ * + * @author Orlando Garcia Carmona + * @author Ben Alex + * @author Andrey Grebnev <andrey.grebnev@blandware.com> + * @version $Id$ + */ +public class SandboxSecurityContextHolderAwareRequestWrapper extends + HttpServletRequestWrapper { + + // ~ Static fields ======================================================== + + protected static final TimeZone GMT_ZONE = TimeZone.getTimeZone("GMT"); + + /** + * The default Locale if none are specified. + */ + protected static Locale defaultLocale = Locale.getDefault(); + + // ~ Instance fields + // ======================================================== + + /** + * Authentication trust resolver. + */ + private AuthenticationTrustResolver authenticationTrustResolver = new AuthenticationTrustResolverImpl(); + + /** + * The set of SimpleDateFormat formats to use in getDateHeader(). + * + * Notice that because SimpleDateFormat is not thread-safe, we can't declare + * formats[] as a static variable. + */ + protected SimpleDateFormat formats[] = new SimpleDateFormat[3]; + + /** + * Saved request (to be resumed after authentication) + */ + protected SavedHttpServletRequest savedRequest = null; + + // ~ Constructors + // =========================================================== + + /** + * The class' primary constructor. + * + * @param request HttpServletRequest + */ + public SandboxSecurityContextHolderAwareRequestWrapper(HttpServletRequest request) { + + // First do what the parent class needs to. + super(request); + + // Return if there isn't an existing HttpSession + HttpSession session = request.getSession(false); + if (session != null) { + + // We know there's an existing HttpSession, so see if it has a + // saved request (placed there by SecurityEnforcementFilter). + SavedHttpServletRequest saved = (SavedHttpServletRequest) session + .getAttribute(SandboxSecurityEnforcementFilter.SAVED_REQUEST_SESSION_ATTRIBUTE); + if (saved != null) { + + // We know there's a saved request, so see if it has a + // saved "root" request URI to forward to. + String requestURI = saved.getRequestURI(); + if (requestURI != null) { + + // We know there's a saved "root" request URI, so see if + // it's the + // same one specified by this request. + if (requestURI.equals(request.getRequestURI())) { + + // They're the same "root" request URIs, so get the + // saved request and remove it from the HttpSession + // since we only want to process it once. + savedRequest = saved; + session + .removeAttribute(SandboxSecurityEnforcementFilter.SAVED_REQUEST_SESSION_ATTRIBUTE); + + formats[0] = new SimpleDateFormat( + "EEE, dd MMM yyyy HH:mm:ss zzz", Locale.US); + formats[1] = new SimpleDateFormat( + "EEEEEE, dd-MMM-yy HH:mm:ss zzz", Locale.US); + formats[2] = new SimpleDateFormat( + "EEE MMMM d HH:mm:ss yyyy", Locale.US); + + formats[0].setTimeZone(GMT_ZONE); + formats[1].setTimeZone(GMT_ZONE); + formats[2].setTimeZone(GMT_ZONE); + } + + } + + } + + } + + return; + + } + + // ~ Methods + // ================================================================ + + /** + * Returns the principal's name, as obtained from the + * SecurityContextHolder. Properly handles both + * String-based and UserDetails-based + * principals. + * + * @return the username or null if unavailable + */ + public String getRemoteUser() { + Authentication auth = getAuthentication(); + + if ((auth == null) || (auth.getPrincipal() == null)) { + return null; + } + + if (auth.getPrincipal() instanceof UserDetails) { + return ((UserDetails) auth.getPrincipal()).getUsername(); + } + + return auth.getPrincipal().toString(); + } + + /** + * Simple searches for an exactly matching {@link + * GrantedAuthority#getAuthority()}. + * + *

+ * Will always return false if the + * SecurityContextHolder contains an + * Authentication with + * nullprincipal and/or + * GrantedAuthority[] objects. + *

+ * + * @param role the GrantedAuthorityString representation to check for. + * @return true if an exact (case sensitive) matching granted authority is located, false otherwise. + */ + public boolean isUserInRole(String role) { + return isGranted(role); + } + + /** + * Returns the Authentication (which is a subclass of + * Principal), or null if unavailable. + * + *

+ * Note: Override this method in order to workaround the problem in Sun Java + * System Application Server 8.1 PE + *

+ * + * @return the Authentication, or null + */ + public Principal getUserPrincipal() { + Authentication auth = getAuthentication(); + + if ((auth == null) || (auth.getPrincipal() == null)) { + return null; + } + + return auth; + } + + /** + * Obtain the current active Authentication + * + * @return the authentication object or null + */ + private Authentication getAuthentication() { + Authentication auth = SecurityContextHolder.getContext() + .getAuthentication(); + + if (!authenticationTrustResolver.isAnonymous(auth)) { + return auth; + } + + return null; + } + + /** + * Determines if principal has been granted a given role. + * + * @param role The role being tested. + * @return True if principal has been granted the given role. + */ + private boolean isGranted(String role) { + Authentication auth = getAuthentication(); + + if ((auth == null) || (auth.getPrincipal() == null) + || (auth.getAuthorities() == null)) { + return false; + } + + for (int i = 0; i < auth.getAuthorities().length; i++) { + if (role.equals(auth.getAuthorities()[i].getAuthority())) { + return true; + } + } + + return false; + } + + /** + * The default behavior of this method is to return getMethod() on the + * wrapped request object. + */ + public String getMethod() { + if (savedRequest == null) { + return super.getMethod(); + } else { + return savedRequest.getMethod(); + } + } + + /** + * The default behavior of this method is to return getHeader(String name) + * on the wrapped request object. + */ + public String getHeader(String name) { + if (savedRequest == null) { + return super.getHeader(name); + } else { + String header = null; + Iterator iterator = savedRequest.getHeaderValues(name); + while (iterator.hasNext()) { + header = (String) iterator.next(); + break; + } + return header; + } + } + + /** + * The default behavior of this method is to return getIntHeader(String + * name) on the wrapped request object. + */ + public int getIntHeader(String name) { + if (savedRequest == null) { + return super.getIntHeader(name); + } else { + String value = getHeader(name); + if (value == null) { + return (-1); + } else { + return (Integer.parseInt(value)); + } + } + } + + /** + * The default behavior of this method is to return getDateHeader(String + * name) on the wrapped request object. + */ + public long getDateHeader(String name) { + if (savedRequest == null) { + return super.getDateHeader(name); + } else { + String value = getHeader(name); + if (value == null) + return (-1L); + + // Attempt to convert the date header in a variety of formats + long result = FastHttpDateFormat.parseDate(value, formats); + if (result != (-1L)) { + return result; + } + throw new IllegalArgumentException(value); + } + } + + /** + * The default behavior of this method is to return getHeaderNames() on the + * wrapped request object. + */ + public Enumeration getHeaderNames() { + if (savedRequest == null) { + return super.getHeaderNames(); + } else { + return new Enumerator(savedRequest.getHeaderNames()); + } + } + + /** + * The default behavior of this method is to return getHeaders(String name) + * on the wrapped request object. + */ + public Enumeration getHeaders(String name) { + if (savedRequest == null) { + return super.getHeaders(name); + } else { + return new Enumerator(savedRequest.getHeaderValues(name)); + } + } + + /** + * The default behavior of this method is to return getCookies() on the + * wrapped request object. + */ + public Cookie[] getCookies() { + if (savedRequest == null) { + return super.getCookies(); + } else { + List cookies = savedRequest.getCookies(); + return (Cookie[]) cookies.toArray(new Cookie[cookies.size()]); + } + } + + /** + * The default behavior of this method is to return + * getParameterValues(String name) on the wrapped request object. + */ + public String[] getParameterValues(String name) { + if (savedRequest == null) { + return super.getParameterValues(name); + } else { + return savedRequest.getParameterValues(name); + } + } + + /** + * The default behavior of this method is to return getParameterNames() on + * the wrapped request object. + */ + public Enumeration getParameterNames() { + if (savedRequest == null) { + return super.getParameterNames(); + } else { + return new Enumerator(savedRequest.getParameterNames()); + } + } + + /** + * The default behavior of this method is to return getParameterMap() on the + * wrapped request object. + */ + public Map getParameterMap() { + if (savedRequest == null) { + return super.getParameterMap(); + } else { + return savedRequest.getParameterMap(); + } + } + + /** + * The default behavior of this method is to return getParameter(String + * name) on the wrapped request object. + */ + public String getParameter(String name) { + + /* + * if (savedRequest == null) { return super.getParameter(name); } else { + * String value = null; String[] values = + * savedRequest.getParameterValues(name); if (values == null) return + * null; for (int i = 0; i < values.length; i++) { value = values[i]; + * break; } return value; } + */ + + // We do not get value from super.getParameter because + // of a bug in Jetty servlet-container. + String value = null; + String[] values = null; + if (savedRequest == null) { + values = super.getParameterValues(name); + } else { + values = savedRequest.getParameterValues(name); + } + + if (values == null) + return null; + for (int i = 0; i < values.length; i++) { + value = values[i]; + break; + } + return value; + + } + + /** + * The default behavior of this method is to return getLocales() on the + * wrapped request object. + */ + public Enumeration getLocales() { + if (savedRequest == null) { + return super.getLocales(); + } else { + Iterator iterator = savedRequest.getLocales(); + if (iterator.hasNext()) { + return new Enumerator(iterator); + } else { + ArrayList results = new ArrayList(); + results.add(defaultLocale); + return new Enumerator(results.iterator()); + } + } + } + + /** + * The default behavior of this method is to return getLocale() on the + * wrapped request object. + */ + public Locale getLocale() { + if (savedRequest == null) { + return super.getLocale(); + } else { + Locale locale = null; + Iterator iterator = savedRequest.getLocales(); + while (iterator.hasNext()) { + locale = (Locale) iterator.next(); + break; + } + if (locale == null) { + return defaultLocale; + } else { + return locale; + } + } + } + +} diff --git a/sandbox/src/main/java/org/acegisecurity/wrapper/redirect/Enumerator.java b/sandbox/src/main/java/org/acegisecurity/wrapper/redirect/Enumerator.java new file mode 100644 index 0000000000..4828e7397d --- /dev/null +++ b/sandbox/src/main/java/org/acegisecurity/wrapper/redirect/Enumerator.java @@ -0,0 +1,143 @@ +/* Copyright 2004, 2005 Acegi Technology Pty Limited + * + * 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 net.sf.acegisecurity.wrapper.redirect; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Enumeration; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.NoSuchElementException; + +/** + *

Adapter class that wraps an Enumeration around a Java2 + * collection classes object Iterator so that existing APIs + * returning Enumerations can easily run on top of the new collections. + * Constructors are provided to easliy create such wrappers.

+ *

The source code is taken from Apache Tomcat

+ * + *

View Source

+ * + * @author Craig R. McClanahan + * @author Andrey Grebnev <andrey.grebnev@blandware.com> + * @version $Revision$ $Date$ + */ +public class Enumerator implements Enumeration { + + // ----------------------------------------------------------- Constructors + + /** + * The Iterator over which the Enumeration + * represented by this class actually operates. + */ + private Iterator iterator = null; + + /** + * Return an Enumeration over the values of the specified Collection. + * + * @param collection Collection whose values should be enumerated + */ + public Enumerator(Collection collection) { + this(collection.iterator()); + } + + /** + * Return an Enumeration over the values of the specified Collection. + * + * @param collection Collection whose values should be enumerated + * @param clone true to clone iterator + */ + public Enumerator(Collection collection, boolean clone) { + this(collection.iterator(), clone); + } + + /** + * Return an Enumeration over the values returned by the + * specified Iterator. + * + * @param iterator Iterator to be wrapped + */ + public Enumerator(Iterator iterator) { + super(); + this.iterator = iterator; + } + + /** + * Return an Enumeration over the values returned by the + * specified Iterator. + * + * @param iterator Iterator to be wrapped + * @param clone true to clone iterator + */ + public Enumerator(Iterator iterator, boolean clone) { + + super(); + if (!clone) { + this.iterator = iterator; + } else { + List list = new ArrayList(); + while (iterator.hasNext()) { + list.add(iterator.next()); + } + this.iterator = list.iterator(); + } + + } + + /** + * Return an Enumeration over the values of the specified Map. + * + * @param map Map whose values should be enumerated + */ + public Enumerator(Map map) { + this(map.values().iterator()); + } + + /** + * Return an Enumeration over the values of the specified Map. + * + * @param map Map whose values should be enumerated + * @param clone true to clone iterator + */ + public Enumerator(Map map, boolean clone) { + this(map.values().iterator(), clone); + } + + /** + * Tests if this enumeration contains more elements. + * + * @return true if and only if this enumeration object + * contains at least one more element to provide, false + * otherwise + */ + public boolean hasMoreElements() { + return (iterator.hasNext()); + } + + /** + * Returns the next element of this enumeration if this enumeration + * has at least one more element to provide. + * + * @return the next element of this enumeration + * + * @exception NoSuchElementException if no more elements exist + */ + public Object nextElement() throws NoSuchElementException { + return (iterator.next()); + } + +} diff --git a/sandbox/src/main/java/org/acegisecurity/wrapper/redirect/FastHttpDateFormat.java b/sandbox/src/main/java/org/acegisecurity/wrapper/redirect/FastHttpDateFormat.java new file mode 100644 index 0000000000..4f8f8d4cf1 --- /dev/null +++ b/sandbox/src/main/java/org/acegisecurity/wrapper/redirect/FastHttpDateFormat.java @@ -0,0 +1,226 @@ +/* Copyright 2004, 2005 Acegi Technology Pty Limited + * + * 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 net.sf.acegisecurity.wrapper.redirect; + +import java.text.DateFormat; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.HashMap; +import java.util.Locale; +import java.util.TimeZone; + +/** + *

Utility class to generate HTTP dates.

+ *

View Source

+ *

This source code is taken from Tomcat Apache

+ * + * @author Remy Maucherat + * @author Andrey Grebnev <andrey.grebnev@blandware.com> + * @version $Revision$ $Date$ + */ +public class FastHttpDateFormat { + + /** + * Current formatted date. + */ + protected static String currentDate = null; + + /** + * Instant on which the currentDate object was generated. + */ + protected static long currentDateGenerated = 0L; + + /** + * HTTP date format. + */ + protected static final SimpleDateFormat format = + new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss zzz", Locale.US); + + /** + * Formatter cache. + */ + protected static final HashMap formatCache = new HashMap(); + + /** + * The set of SimpleDateFormat formats to use in getDateHeader(). + */ + protected static final SimpleDateFormat formats[] = { + new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss zzz", Locale.US), + new SimpleDateFormat("EEEEEE, dd-MMM-yy HH:mm:ss zzz", Locale.US), + new SimpleDateFormat("EEE MMMM d HH:mm:ss yyyy", Locale.US) + }; + + /** + * GMT timezone - all HTTP dates are on GMT + */ + protected final static TimeZone gmtZone = TimeZone.getTimeZone("GMT"); + + /** + * Parser cache. + */ + protected static final HashMap parseCache = new HashMap(); + + static { + + format.setTimeZone(gmtZone); + + formats[0].setTimeZone(gmtZone); + formats[1].setTimeZone(gmtZone); + formats[2].setTimeZone(gmtZone); + + } + + /** + * Formats a specified date to HTTP format. If local format is not + * null, it's used instead. + * + * @param value Date value to format + * @param threadLocalformat The format to use (or null -- then + * HTTP format will be used) + * @return Formatted date + */ + public static final String formatDate(long value, + DateFormat threadLocalformat) { + + String cachedDate = null; + Long longValue = new Long(value); + try { + cachedDate = (String) formatCache.get(longValue); + } catch (Exception e) { + } + if (cachedDate != null) + return cachedDate; + + String newDate = null; + Date dateValue = new Date(value); + if (threadLocalformat != null) { + newDate = threadLocalformat.format(dateValue); + synchronized (formatCache) { + updateCache(formatCache, longValue, newDate); + } + } else { + synchronized (formatCache) { + newDate = format.format(dateValue); + updateCache(formatCache, longValue, newDate); + } + } + return newDate; + + } + + /** + * Gets the current date in HTTP format. + * + * @return Current date in HTTP format + */ + public static final String getCurrentDate() { + + long now = System.currentTimeMillis(); + if ((now - currentDateGenerated) > 1000) { + synchronized (format) { + if ((now - currentDateGenerated) > 1000) { + currentDateGenerated = now; + currentDate = format.format(new Date(now)); + } + } + } + return currentDate; + + } + + /** + * Parses date with given formatters. + * + * @param value The string to parse + * @param formats Array of formats to use + * @return Parsed date (or null if no formatter mached) + */ + private static final Long internalParseDate + (String value, DateFormat[] formats) { + Date date = null; + for (int i = 0; (date == null) && (i < formats.length); i++) { + try { + date = formats[i].parse(value); + } catch (ParseException e) { + ; + } + } + if (date == null) { + return null; + } + return new Long(date.getTime()); + } + + /** + * Tries to parse the given date as an HTTP date. If local format list is not + * null, it's used instead. + * + * @param value The string to parse + * @param threadLocalformats Array of formats to use for parsing. + * If null, HTTP formats are used. + * @return Parsed date (or -1 if error occured) + */ + public static final long parseDate(String value, + DateFormat[] threadLocalformats) { + + Long cachedDate = null; + try { + cachedDate = (Long) parseCache.get(value); + } catch (Exception e) { + } + if (cachedDate != null) + return cachedDate.longValue(); + + Long date = null; + if (threadLocalformats != null) { + date = internalParseDate(value, threadLocalformats); + synchronized (parseCache) { + updateCache(parseCache, value, date); + } + } else { + synchronized (parseCache) { + date = internalParseDate(value, formats); + updateCache(parseCache, value, date); + } + } + if (date == null) { + return (-1L); + } else { + return date.longValue(); + } + + } + + /** + * Updates cache. + * + * @param cache Cache to be updated + * @param key Key to be updated + * @param value New value + */ + private static final void updateCache(HashMap cache, Object key, + Object value) { + if (value == null) { + return; + } + if (cache.size() > 1000) { + cache.clear(); + } + cache.put(key, value); + } + +} diff --git a/sandbox/src/main/java/org/acegisecurity/wrapper/redirect/SavedHttpServletRequest.java b/sandbox/src/main/java/org/acegisecurity/wrapper/redirect/SavedHttpServletRequest.java new file mode 100644 index 0000000000..f6bffbd315 --- /dev/null +++ b/sandbox/src/main/java/org/acegisecurity/wrapper/redirect/SavedHttpServletRequest.java @@ -0,0 +1,352 @@ +/* Copyright 2004, 2005 Acegi Technology Pty Limited + * + * 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 net.sf.acegisecurity.wrapper.redirect; + +import java.util.ArrayList; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Locale; +import java.util.Map; + +import javax.servlet.http.Cookie; +import javax.servlet.http.HttpServletRequest; + +import net.sf.acegisecurity.intercept.web.SecurityEnforcementFilter; +import net.sf.acegisecurity.wrapper.SecurityContextHolderAwareRequestWrapper; + +/** + *

+ * Object that saves the critical information from a request so that + * HTTP (header and parameter)-based authentication can reproduce it + * once the user has been authenticated. + *

+ * IMPLEMENTATION NOTE - It is assumed that this object is accessed + * only from the context of a single thread, so no synchronization around + * internal collection classes is performed. + *

+ * Note that SavedHttpServletRequest doesn't save uploaded file binary data, although + * it does save request parameters so that a POST transaction can be faithfully + * duplicated. On one hand it is unfortunate to lose such type of data, but on + * the other hand we don't store it in session because the data can be very big + * and this solution can overload restricted resources. + *

+ *

The original source code from Apache Tomcat

+ * + * @see SecurityEnforcementFilter + * @see SecurityContextHolderAwareRequestWrapper + * @author Craig R. McClanahan + * @author Andrey Grebnev <andrey.grebnev@blandware.com> + * @version $Revision$ $Date$ + */ +public class SavedHttpServletRequest { + + /** + * This method provides ability to create SavedHttpServletRequest from HttpServletRequest + * @param request request to be saved + * @return saved request resulting SavedHttpServletRequest + */ + public static SavedHttpServletRequest saveRequest(HttpServletRequest request) { + if (request.getRequestURI() == null) + return null; + + // Create and populate a SavedHttpServletRequest object for this request + SavedHttpServletRequest saved = new SavedHttpServletRequest(); + Cookie cookies[] = request.getCookies(); + if (cookies != null) { + for (int i = 0; i < cookies.length; i++) + saved.addCookie(cookies[i]); + } + Enumeration names = request.getHeaderNames(); + while (names.hasMoreElements()) { + String name = (String) names.nextElement(); + Enumeration values = request.getHeaders(name); + while (values.hasMoreElements()) { + String value = (String) values.nextElement(); + saved.addHeader(name, value); + } + } + Enumeration locales = request.getLocales(); + while (locales.hasMoreElements()) { + Locale locale = (Locale) locales.nextElement(); + saved.addLocale(locale); + } + Map parameters = request.getParameterMap(); + Iterator paramNames = parameters.keySet().iterator(); + while (paramNames.hasNext()) { + String paramName = (String) paramNames.next(); + String paramValues[] = (String[]) parameters.get(paramName); + saved.addParameter(paramName, paramValues); + } + saved.setMethod(request.getMethod()); + saved.setQueryString(request.getQueryString()); + saved.setRequestURI(request.getRequestURI()); +// saved.setPathInfo(request.getPathInfo()); + + return saved; + } + + /** + * The set of Cookies associated with this Request. + */ + private ArrayList cookies = new ArrayList(); + + /** + * The set of Headers associated with this Request. Each key is a header + * name, while the value is a ArrayList containing one or more actual + * values for this header. The values are returned as an Iterator when + * you ask for them. + */ + private HashMap headers = new HashMap(); + + /** + * The set of Locales associated with this Request. + */ + private ArrayList locales = new ArrayList(); + + /** + * The request method used on this Request. + */ + private String method = null; + + /** + * The set of request parameters associated with this Request. Each + * entry is keyed by the parameter name, pointing at a String array of + * the corresponding values. + */ + private HashMap parameters = new HashMap(); + + /** + * The request pathInfo associated with this Request. + */ + private String pathInfo = null; + + /** + * The query string associated with this Request. + */ + private String queryString = null; + + /** + * The request URI associated with this Request. + */ + private String requestURI = null; + + /** + * Adds cookie to list of cookies + * + * @param cookie cookie to add + */ + public void addCookie(Cookie cookie) { + cookies.add(cookie); + } + + /** + * Adds header + * + * @param name header name + * @param value header value + */ + public void addHeader(String name, String value) { + ArrayList values = (ArrayList) headers.get(name); + if (values == null) { + values = new ArrayList(); + headers.put(name, values); + } + values.add(value); + } + + /** + * Adds locale + * + * @param locale locale to add + */ + public void addLocale(Locale locale) { + locales.add(locale); + } + + /** + * Adds parameter + * + * @param name parameter name + * @param values parameter values + */ + public void addParameter(String name, String values[]) { + parameters.put(name, values); + } + + /** + * Returns list of cookies + * + * @return list of cookies + */ + public List getCookies() { + return cookies; + } + + /** + * Returns iterator over header names + * + * @return iterator over header names + */ + public Iterator getHeaderNames() { + return (headers.keySet().iterator()); + } + + /** + * Returns iterator over header values + * + * @param name header name + * @return iterator over header values + */ + public Iterator getHeaderValues(String name) { + ArrayList values = (ArrayList) headers.get(name); + if (values == null) + return ((new ArrayList()).iterator()); + else + return (values.iterator()); + } + + /** + * Returns iterator over locales + * + * @return iterator over locales + */ + public Iterator getLocales() { + return (locales.iterator()); + } + + /** + * Returns request method + * + * @return request method + */ + public String getMethod() { + return (this.method); + } + + /** + * Returns parameters + * + * @return parameters map + */ + public Map getParameterMap() { + return parameters; + } + + /** + * Returns iterator over parameter names + * + * @return iterator over parameter names + */ + public Iterator getParameterNames() { + return (parameters.keySet().iterator()); + } + + /** + * Returns parameter values + * + * @param name parameter name + * @return parameter values + */ + public String[] getParameterValues(String name) { + return ((String[]) parameters.get(name)); + } + + /** + * Returns path info + * + * @return path info + */ + public String getPathInfo() { + return pathInfo; + } + + /** + * Returns query string + * + * @return query string + */ + public String getQueryString() { + return (this.queryString); + } + + /** + * Returns request URI + * + * @return request URI + */ + public String getRequestURI() { + return (this.requestURI); + } + + /** + * Gets uri with path info and query string + * + * @return uri with path info and query string + */ + public String getRequestURL() { + if (getRequestURI() == null) + return null; + + StringBuffer sb = new StringBuffer(getRequestURI()); +// if (getPathInfo() != null) { +// sb.append(getPathInfo()); +// } + if (getQueryString() != null) { + sb.append('?'); + sb.append(getQueryString()); + } + return sb.toString(); + } + + /** + * Sets request method + * + * @param method request method to set + */ + public void setMethod(String method) { + this.method = method; + } + + /** + * Sets path info + * + * @param pathInfo path info to set + */ + public void setPathInfo(String pathInfo) { + this.pathInfo = pathInfo; + } + + /** + * Sets query string + * + * @param queryString query string to set + */ + public void setQueryString(String queryString) { + this.queryString = queryString; + } + + /** + * Sets request URI + * + * @param requestURI request URI to set + */ + public void setRequestURI(String requestURI) { + this.requestURI = requestURI; + } + +} diff --git a/sandbox/src/main/java/org/acegisecurity/wrapper/redirect/package.html b/sandbox/src/main/java/org/acegisecurity/wrapper/redirect/package.html new file mode 100644 index 0000000000..cc8aa87f29 --- /dev/null +++ b/sandbox/src/main/java/org/acegisecurity/wrapper/redirect/package.html @@ -0,0 +1,8 @@ + + +Redirection helper code for +SecurityContextHolderAwareRequestWrapper +and +SecurityEnforcementFilter. + +