diff --git a/build.xml b/build.xml
index 65e61e8c69..c8361478e3 100644
--- a/build.xml
+++ b/build.xml
@@ -183,6 +183,7 @@
+
@@ -204,6 +205,7 @@
+
@@ -224,6 +226,7 @@
+
@@ -244,6 +247,7 @@
+
diff --git a/core/src/main/java/org/acegisecurity/securechannel/ChannelDecisionManager.java b/core/src/main/java/org/acegisecurity/securechannel/ChannelDecisionManager.java
new file mode 100644
index 0000000000..2b01df8f68
--- /dev/null
+++ b/core/src/main/java/org/acegisecurity/securechannel/ChannelDecisionManager.java
@@ -0,0 +1,38 @@
+/* Copyright 2004 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.securechannel;
+
+import net.sf.acegisecurity.ConfigAttributeDefinition;
+import net.sf.acegisecurity.intercept.web.FilterInvocation;
+
+
+/**
+ * Decides whether a web channel provides sufficient security.
+ *
+ * @author Ben Alex
+ * @version $Id$
+ */
+public interface ChannelDecisionManager {
+ //~ Methods ================================================================
+
+ /**
+ * Decided whether the presented {@link FilterInvocation} provides
+ * sufficient security based on the requested {@link
+ * ConfigAttributeDefinition}.
+ */
+ public void decide(FilterInvocation invocation,
+ ConfigAttributeDefinition config) throws SecureChannelRequiredException;
+}
diff --git a/core/src/main/java/org/acegisecurity/securechannel/ChannelDecisionManagerImpl.java b/core/src/main/java/org/acegisecurity/securechannel/ChannelDecisionManagerImpl.java
new file mode 100644
index 0000000000..749ddcc50c
--- /dev/null
+++ b/core/src/main/java/org/acegisecurity/securechannel/ChannelDecisionManagerImpl.java
@@ -0,0 +1,83 @@
+/* Copyright 2004 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.securechannel;
+
+import net.sf.acegisecurity.ConfigAttribute;
+import net.sf.acegisecurity.ConfigAttributeDefinition;
+import net.sf.acegisecurity.intercept.web.FilterInvocation;
+
+import org.springframework.beans.factory.InitializingBean;
+
+import java.util.Iterator;
+
+
+/**
+ *
+ * Requires a secure channel for a web request if a {@link
+ * ConfigAttribute#getAttribute()} keyword is detected.
+ *
+ *
+ *
+ * The default keyword string is REQUIRES_SECURE_CHANNEL, but this
+ * may be overriden to any value. The ConfigAttribute must
+ * exactly match the case of the keyword string.
+ *
+ *
+ * @author Ben Alex
+ * @version $Id$
+ */
+public class ChannelDecisionManagerImpl implements InitializingBean,
+ ChannelDecisionManager {
+ //~ Instance fields ========================================================
+
+ private String keyword = "REQUIRES_SECURE_CHANNEL";
+
+ //~ Methods ================================================================
+
+ public void setKeyword(String keyword) {
+ this.keyword = keyword;
+ }
+
+ public String getKeyword() {
+ return keyword;
+ }
+
+ public void afterPropertiesSet() throws Exception {
+ if ((keyword == null) || "".equals(keyword)) {
+ throw new IllegalArgumentException("keyword required");
+ }
+ }
+
+ public void decide(FilterInvocation invocation,
+ ConfigAttributeDefinition config) throws SecureChannelRequiredException {
+ if ((invocation == null) || (config == null)) {
+ throw new IllegalArgumentException("Nulls cannot be provided");
+ }
+
+ Iterator iter = config.getConfigAttributes();
+
+ while (iter.hasNext()) {
+ ConfigAttribute attribute = (ConfigAttribute) iter.next();
+
+ if (attribute.equals(keyword)) {
+ if (!invocation.getHttpRequest().isSecure()) {
+ throw new SecureChannelRequiredException(
+ "Request is not being made over a secure channel");
+ }
+ }
+ }
+ }
+}
diff --git a/core/src/main/java/org/acegisecurity/securechannel/ChannelEntryPoint.java b/core/src/main/java/org/acegisecurity/securechannel/ChannelEntryPoint.java
new file mode 100644
index 0000000000..0187b2e822
--- /dev/null
+++ b/core/src/main/java/org/acegisecurity/securechannel/ChannelEntryPoint.java
@@ -0,0 +1,49 @@
+/* Copyright 2004 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.securechannel;
+
+import java.io.IOException;
+
+import javax.servlet.ServletException;
+import javax.servlet.ServletRequest;
+import javax.servlet.ServletResponse;
+
+
+/**
+ * Used by {@link ChannelProcessingFilter} to launch a secure web channel.
+ *
+ * @author Ben Alex
+ * @version $Id$
+ */
+public interface ChannelEntryPoint {
+ //~ Methods ================================================================
+
+ /**
+ * Commences a secure channel.
+ *
+ *
+ * Implementations should modify the headers on the
+ * ServletResponse as necessary to commence the user agent
+ * using the secure channel.
+ *
+ *
+ * @param request that resulted in a
+ * SecureChannelRequiredException
+ * @param response so that the user agent can begin using a secure channel
+ */
+ public void commence(ServletRequest request, ServletResponse response)
+ throws IOException, ServletException;
+}
diff --git a/core/src/main/java/org/acegisecurity/securechannel/ChannelProcessingFilter.java b/core/src/main/java/org/acegisecurity/securechannel/ChannelProcessingFilter.java
new file mode 100644
index 0000000000..3e33b9e80f
--- /dev/null
+++ b/core/src/main/java/org/acegisecurity/securechannel/ChannelProcessingFilter.java
@@ -0,0 +1,152 @@
+/* Copyright 2004 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.securechannel;
+
+import net.sf.acegisecurity.ConfigAttributeDefinition;
+import net.sf.acegisecurity.intercept.web.FilterInvocation;
+import net.sf.acegisecurity.intercept.web.FilterInvocationDefinitionSource;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+import org.springframework.beans.factory.InitializingBean;
+
+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;
+
+
+/**
+ * Ensures a request is delivered over a secure channel.
+ *
+ *
+ * Internally uses a {@link FilterInvocation} to represent the request, so that
+ * the FilterInvocation-related property editors and lookup
+ * classes can be used.
+ *
+ *
+ *
+ * Do not use this class directly. Instead configure
+ * web.xml to use the {@link
+ * net.sf.acegisecurity.util.FilterToBeanProxy}.
+ *
+ *
+ * @author Ben Alex
+ * @version $Id$
+ */
+public class ChannelProcessingFilter implements InitializingBean, Filter {
+ //~ Static fields/initializers =============================================
+
+ private static final Log logger = LogFactory.getLog(ChannelProcessingFilter.class);
+
+ //~ Instance fields ========================================================
+
+ private ChannelDecisionManager channelDecisionManager;
+ private ChannelEntryPoint channelEntryPoint;
+ private FilterInvocationDefinitionSource filterInvocationDefinitionSource;
+
+ //~ Methods ================================================================
+
+ public void setChannelDecisionManager(
+ ChannelDecisionManager channelDecisionManager) {
+ this.channelDecisionManager = channelDecisionManager;
+ }
+
+ public ChannelDecisionManager getChannelDecisionManager() {
+ return channelDecisionManager;
+ }
+
+ public void setChannelEntryPoint(ChannelEntryPoint channelEntryPoint) {
+ this.channelEntryPoint = channelEntryPoint;
+ }
+
+ public ChannelEntryPoint getChannelEntryPoint() {
+ return channelEntryPoint;
+ }
+
+ public void setFilterInvocationDefinitionSource(
+ FilterInvocationDefinitionSource filterInvocationDefinitionSource) {
+ this.filterInvocationDefinitionSource = filterInvocationDefinitionSource;
+ }
+
+ public FilterInvocationDefinitionSource getFilterInvocationDefinitionSource() {
+ return filterInvocationDefinitionSource;
+ }
+
+ public void afterPropertiesSet() throws Exception {
+ if (filterInvocationDefinitionSource == null) {
+ throw new IllegalArgumentException(
+ "filterInvocationDefinitionSource must be specified");
+ }
+
+ if (channelDecisionManager == null) {
+ throw new IllegalArgumentException(
+ "channelDecisionManager must be specified");
+ }
+
+ if (channelEntryPoint == null) {
+ throw new IllegalArgumentException(
+ "channelEntryPoint 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);
+ ConfigAttributeDefinition attr = this.filterInvocationDefinitionSource
+ .getAttributes(fi);
+
+ if (attr != null) {
+ if (logger.isDebugEnabled()) {
+ logger.debug("Request : " + request.toString()
+ + "; ConfigAttributes: " + attr.toString());
+ }
+
+ try {
+ channelDecisionManager.decide(fi, attr);
+ } catch (SecureChannelRequiredException channelException) {
+ if (logger.isDebugEnabled()) {
+ logger.debug("Channel insufficient ("
+ + channelException.getMessage()
+ + "); delegating to channelEntryPoint");
+ }
+
+ channelEntryPoint.commence(request, response);
+ }
+ }
+
+ chain.doFilter(request, response);
+ }
+
+ public void init(FilterConfig filterConfig) throws ServletException {}
+}
diff --git a/core/src/main/java/org/acegisecurity/securechannel/RetryWithHttpsEntryPoint.java b/core/src/main/java/org/acegisecurity/securechannel/RetryWithHttpsEntryPoint.java
new file mode 100644
index 0000000000..27c5bb5fa1
--- /dev/null
+++ b/core/src/main/java/org/acegisecurity/securechannel/RetryWithHttpsEntryPoint.java
@@ -0,0 +1,158 @@
+/* Copyright 2004 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.securechannel;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+import org.springframework.beans.factory.InitializingBean;
+
+import java.io.IOException;
+
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Map;
+
+import javax.servlet.ServletException;
+import javax.servlet.ServletRequest;
+import javax.servlet.ServletResponse;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+
+/**
+ * Commences a secure channel by retrying the original request using HTTPS.
+ *
+ *
+ * This entry point should suffice in most circumstances. However, it is not
+ * intended to properly handle HTTP POSTs or other usage where a standard
+ * redirect would cause an issue.
+ *
+ *
+ * @author Ben Alex
+ * @version $Id$
+ */
+public class RetryWithHttpsEntryPoint implements InitializingBean,
+ ChannelEntryPoint {
+ //~ Static fields/initializers =============================================
+
+ private static final Log logger = LogFactory.getLog(RetryWithHttpsEntryPoint.class);
+
+ //~ Instance fields ========================================================
+
+ private Map httpsPortMappings;
+
+ //~ Constructors ===========================================================
+
+ public RetryWithHttpsEntryPoint() {
+ httpsPortMappings = new HashMap();
+ httpsPortMappings.put(new Integer(80), new Integer(443));
+ httpsPortMappings.put(new Integer(8080), new Integer(8443));
+ }
+
+ //~ Methods ================================================================
+
+ /**
+ *
+ * Set to override the default http port to https port mappings of 80:443,
+ * and 8080:8443.
+ *
+ * In a Spring XML ApplicationContext, a definition would look something
+ * like this:
+ *
+ * <property name="httpsPortMapping">
+ * <map>
+ * <entry key="80"><value>443</value></entry>
+ * <entry key="8080"><value>8443</value></entry>
+ * </map>
+ * </property>
+ *
+ *
+ * @param newMappings A Map consisting of String keys and String values,
+ * where for each entry the key is the string representation of an
+ * integer http port number, and the value is the string
+ * representation of the corresponding integer https port number.
+ *
+ * @throws IllegalArgumentException if input map does not consist of String
+ * keys and values, each representing an integer port number in
+ * the range 1-65535 for that mapping.
+ */
+ public void setHttpsPortMappings(HashMap newMappings) {
+ httpsPortMappings.clear();
+
+ Iterator it = newMappings.entrySet().iterator();
+
+ while (it.hasNext()) {
+ Map.Entry entry = (Map.Entry) it.next();
+ Integer httpPort = new Integer((String) entry.getKey());
+ Integer httpsPort = new Integer((String) entry.getValue());
+
+ if ((httpPort.intValue() < 1) || (httpPort.intValue() > 65535)
+ || (httpsPort.intValue() < 1) || (httpsPort.intValue() > 65535)) {
+ throw new IllegalArgumentException(
+ "one or both ports out of legal range: " + httpPort + ", "
+ + httpsPort);
+ }
+
+ httpsPortMappings.put(httpPort, httpsPort);
+
+ if (httpsPortMappings.size() < 1) {
+ throw new IllegalArgumentException("must map at least one port");
+ }
+ }
+ }
+
+ public void afterPropertiesSet() throws Exception {
+ if (httpsPortMappings == null) {
+ throw new IllegalArgumentException("httpsPortMappings required");
+ }
+ }
+
+ public void commence(ServletRequest request, ServletResponse response)
+ throws IOException, ServletException {
+ HttpServletRequest req = (HttpServletRequest) request;
+
+ String pathInfo = req.getPathInfo();
+ String queryString = req.getQueryString();
+ String contextPath = req.getContextPath();
+ String destination = req.getServletPath()
+ + ((pathInfo == null) ? "" : pathInfo)
+ + ((queryString == null) ? "" : ("?" + queryString));
+
+ String redirectUrl = contextPath;
+
+ Integer httpPort = new Integer(req.getServerPort());
+ Integer httpsPort = (Integer) httpsPortMappings.get(httpPort);
+
+ if (httpsPort != null) {
+ String serverName = req.getServerName();
+ redirectUrl = "https://" + serverName + ":" + httpsPort
+ + contextPath + destination;
+ }
+
+ ((HttpServletResponse) response).sendRedirect(redirectUrl);
+ }
+
+ /**
+ * Returns the translated (Integer -> Integer) version of the original port
+ * mapping specified via setHttpsPortMapping()
+ *
+ * @return DOCUMENT ME!
+ */
+ protected Map getTranslatedHttpsPortMappings() {
+ return httpsPortMappings;
+ }
+}
diff --git a/core/src/main/java/org/acegisecurity/securechannel/SecureChannelRequiredException.java b/core/src/main/java/org/acegisecurity/securechannel/SecureChannelRequiredException.java
new file mode 100644
index 0000000000..62ed5410b6
--- /dev/null
+++ b/core/src/main/java/org/acegisecurity/securechannel/SecureChannelRequiredException.java
@@ -0,0 +1,50 @@
+/* Copyright 2004 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.securechannel;
+
+import net.sf.acegisecurity.AccessDeniedException;
+
+
+/**
+ * Thrown if a secure web channel is required, but is not present.
+ *
+ * @author Ben Alex
+ * @version $Id$
+ */
+public class SecureChannelRequiredException extends AccessDeniedException {
+ //~ Constructors ===========================================================
+
+ /**
+ * Constructs a SecureChannelRequiredException with the
+ * specified message.
+ *
+ * @param msg the detail message.
+ */
+ public SecureChannelRequiredException(String msg) {
+ super(msg);
+ }
+
+ /**
+ * Constructs a SecureChannelRequiredException with the
+ * specified message and root cause.
+ *
+ * @param msg the detail message.
+ * @param t root cause
+ */
+ public SecureChannelRequiredException(String msg, Throwable t) {
+ super(msg, t);
+ }
+}
diff --git a/core/src/main/java/org/acegisecurity/securechannel/package.html b/core/src/main/java/org/acegisecurity/securechannel/package.html
new file mode 100644
index 0000000000..257fc29113
--- /dev/null
+++ b/core/src/main/java/org/acegisecurity/securechannel/package.html
@@ -0,0 +1,6 @@
+
+
+Classes that ensure web requests are received over required
+transport channels.
+
+
diff --git a/samples/contacts/etc/filter/applicationContext.xml b/samples/contacts/etc/filter/applicationContext.xml
index ecc2831420..a89199cdc8 100644
--- a/samples/contacts/etc/filter/applicationContext.xml
+++ b/samples/contacts/etc/filter/applicationContext.xml
@@ -133,6 +133,25 @@
+
+
+
+
+
+
+
+ CONVERT_URL_TO_LOWERCASE_BEFORE_COMPARISON
+ \A/secure/.*\Z=REQUIRES_SECURE_CHANNEL
+ \A/info/.*\Z=REQUIRES_SECURE_CHANNEL
+ \A/acegilogin.jsp.*\Z=REQUIRES_SECURE_CHANNEL
+
+
+
+
+
+
+
+
diff --git a/samples/contacts/etc/filter/web.xml b/samples/contacts/etc/filter/web.xml
index df4ef191cc..86ae3db0f4 100644
--- a/samples/contacts/etc/filter/web.xml
+++ b/samples/contacts/etc/filter/web.xml
@@ -24,6 +24,15 @@
/WEB-INF/applicationContext.xml
+
+ Acegi Channel Processing Filter
+ net.sf.acegisecurity.util.FilterToBeanProxy
+
+ targetClass
+ net.sf.acegisecurity.securechannel.ChannelProcessingFilter
+
+
+
Acegi Authentication Processing Filter
net.sf.acegisecurity.util.FilterToBeanProxy
@@ -56,6 +65,11 @@
+
+ Acegi Channel Processing Filter
+ /*
+
+
Acegi Authentication Processing Filter
/*