--- java/org/apache/catalina/filters/HstsFilter.java (revision 0) +++ java/org/apache/catalina/filters/HstsFilter.java (working copy) @@ -0,0 +1,123 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.catalina.filters; + +import java.io.IOException; + +import javax.servlet.FilterChain; +import javax.servlet.FilterConfig; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletResponse; + +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; + +/** + * Implements HTTP Strict Transport Security (HSTS) according to RFC 6797. + *
+ * The filter assumes that: + *
+ * <web-app ...>
+ * ...
+ * <filter>
+ * <filter-name>HstsFilter</filter-name>
+ * <filter-class>org.apache.catalina.filters.HstsFilter</filter-class>
+ * <init-param>
+ * <param-name>maxAgeSeconds</param-name>
+ * <param-value>31536000</param-value>
+ * </init-param>
+ * <init-param>
+ * <param-name>includeSubDomains</param-name>
+ * <param-value>true</param-value>
+ * </init-param>
+ * </filter>
+ * ...
+ * <filter-mapping>
+ * <filter-name>HstsFilter</filter-name>
+ * <url-pattern>/*</url-pattern>
+ * </filter-mapping>
+ * ...
+ * </web-app>
+ *
+ * @author Jens Borgland
+ * @see RFC 6797
+ */
+public class HstsFilter extends FilterBase {
+
+ private static final String HEADER_NAME = "Strict-Transport-Security";
+ private static final String MAX_AGE_DIRECTIVE = "max-age=%s";
+ private static final String INCLUDE_SUB_DOMAINS_DIRECTIVE = "includeSubDomains";
+
+ private static final Log log = LogFactory.getLog(HstsFilter.class);
+
+ // The default is "0" like recommended in section 11.2 of RFC 6797
+ private int maxAgeSeconds = 0;
+ private boolean includeSubDomains = false;
+
+ private String directives;
+
+ public void setMaxAgeSeconds(int maxAgeSeconds) {
+ this.maxAgeSeconds = maxAgeSeconds;
+ }
+
+ public void setIncludeSubDomains(boolean includeSubDomains) {
+ this.includeSubDomains = includeSubDomains;
+ }
+
+ @Override
+ public void doFilter(ServletRequest request, ServletResponse response,
+ FilterChain chain) throws IOException, ServletException {
+ chain.doFilter(request, response);
+
+ // Note that the HSTS header must not be included in HTTP responses
+ // conveyed over non-secure transport
+ if (request.isSecure() && response instanceof HttpServletResponse) {
+ HttpServletResponse res = (HttpServletResponse) response;
+ res.addHeader(HEADER_NAME, this.directives);
+ }
+ }
+
+ @SuppressWarnings("boxing")
+ @Override
+ public void init(FilterConfig filterConfig) throws ServletException {
+ super.init(filterConfig);
+ if (this.maxAgeSeconds < 0) {
+ throw new ServletException(sm.getString(
+ "hsts.invalidParameterValue", this.maxAgeSeconds,
+ "maxAgeSeconds"));
+ }
+ this.directives = String.format(MAX_AGE_DIRECTIVE, this.maxAgeSeconds);
+ if (this.includeSubDomains) {
+ this.directives += (" ; " + INCLUDE_SUB_DOMAINS_DIRECTIVE);
+ }
+ }
+
+ @Override
+ protected Log getLogger() {
+ return log;
+ }
+
+}
--- java/org/apache/catalina/filters/LocalStrings.properties (revision 1451166)
+++ java/org/apache/catalina/filters/LocalStrings.properties (working copy)
@@ -17,6 +17,7 @@
csrfPrevention.invalidRandomClass=Unable to create Random source using class [{0}]
filterbase.noSuchProperty=The property "{0}" is not defined for filters of type "{1}"
+hsts.invalidParameterValue="Invalid value "{0}" for parameter "{1}"
http.403=Access to the specified resource ({0}) has been forbidden.
expiresFilter.noExpirationConfigured=Request "{0}" with response status "{1}" content-type "{2}", no expiration configured
--- test/org/apache/catalina/filters/TestHstsFilter.java (revision 0)
+++ test/org/apache/catalina/filters/TestHstsFilter.java (working copy)
@@ -0,0 +1,136 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.catalina.filters;
+
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+import org.apache.catalina.Context;
+import org.apache.catalina.deploy.FilterDef;
+import org.apache.catalina.deploy.FilterMap;
+import org.apache.catalina.startup.Tomcat;
+import org.apache.catalina.startup.TomcatBaseTest;
+import org.apache.tomcat.util.buf.ByteChunk;
+
+public class TestHstsFilter extends TomcatBaseTest {
+
+ @Test
+ public void testIncludeDomainsTrue() throws Exception {
+ doTest("0", "true", true, true, "max-age=0 ; includeSubDomains");
+ }
+
+ @Test
+ public void testIncludeDomainsFalse() throws Exception {
+ doTest("0", "false", true, true, "max-age=0");
+ }
+
+ @Test
+ public void testNonZeroMaxAge() throws Exception {
+ doTest("31536000", "true", true, true,
+ "max-age=31536000 ; includeSubDomains");
+ }
+
+ @Test
+ public void testNoParameters() throws Exception {
+ doTest(null, null, true, true, "max-age=0");
+ }
+
+ @Test
+ public void testNoMaxAgeParameter() throws Exception {
+ doTest(null, "true", true, true, "max-age=0 ; includeSubDomains");
+ }
+
+ @Test
+ public void testNoIncludeSubDomainsParameter() throws Exception {
+ doTest("0", null, true, true, "max-age=0");
+ }
+
+ @Test
+ public void testNonSecure() throws Exception {
+ doTest("0", "false", false, false, null);
+ }
+
+ public void doTest(String maxAge, String includeSubDomains, boolean secure,
+ boolean expectHeader, String expectedDirectives) throws Exception {
+ Tomcat tomcat = getTomcatInstance();
+ tomcat.getConnector().setSecure(secure);
+
+ Context ctx = tomcat.addContext("",
+ System.getProperty("java.io.tmpdir"));
+
+ HttpServlet servlet = new HttpServlet() {
+ private static final long serialVersionUID = 1L;
+
+ @Override
+ protected void service(HttpServletRequest request,
+ HttpServletResponse response) throws ServletException,
+ IOException {
+ response.setContentType("text/plain");
+ }
+ };
+ Tomcat.addServlet(ctx, "servlet", servlet);
+ ctx.addServletMapping("/", "servlet");
+
+ FilterDef filterDef = new FilterDef();
+ filterDef.setFilterClass(HstsFilter.class.getName());
+ filterDef.setFilterName("filter");
+ if (maxAge != null) {
+ filterDef.addInitParameter("maxAgeSeconds", maxAge);
+ }
+ if (includeSubDomains != null) {
+ filterDef.addInitParameter("includeSubDomains", includeSubDomains);
+ }
+ ctx.addFilterDef(filterDef);
+ FilterMap filterMap = new FilterMap();
+ filterMap.setFilterName("filter");
+ filterMap.addServletName("servlet");
+ ctx.addFilterMap(filterMap);
+
+ tomcat.start();
+
+ Map