/* * This file is part of Alpine. * * 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. * * SPDX-License-Identifier: Apache-2.0 * Copyright (c) Steve Springett. All Rights Reserved. */ package alpine.filters; import alpine.util.BooleanUtil; 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 java.io.IOException; /** * <p> * Implements HTTP Strict Transport Security (HSTS) (RFC 6797). * </p> * * <p> * This filter is configured via the applications web.xml. * </p> * <pre> * <init-param> * <param-name>httpsPort</param-name> * <param-value>443</param-value> * </init-param> * <init-param> * <param-name>maxAge</param-name> * <param-value>31536000</param-value> * </init-param> * <init-param> * <param-name>includeSubdomains</param-name> * <param-value>false</param-value> * </init-param> * </pre> * * An example implementation in web.xml: * * <pre> * <filter> * <filter-name>HstsFilter</filter-name> * <filter-class>alpine.filters.HstsFilter</filter-class> * <init-param> * <param-name>httpsPort</param-name> * <param-value>443</param-value> * </init-param> * <init-param> * <param-name>maxAge</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> * </pre> * * @author Steve Springett * @since 1.0.0 */ public class HstsFilter implements Filter { private static final int DEFAULT_HTTPS_PORT = 443; private static final int DEFAULT_MAX_AGE = 86400; private int httpsPort = DEFAULT_HTTPS_PORT; private long maxAge = DEFAULT_MAX_AGE; private boolean includeSubdomains; @Override public void init(final FilterConfig filterConfig) throws ServletException { final String portString = filterConfig.getInitParameter("httpsPort"); try { httpsPort = Integer.valueOf(portString); } catch (NumberFormatException e) { httpsPort = DEFAULT_HTTPS_PORT; } final String maxAgeString = filterConfig.getInitParameter("maxAge"); try { maxAge = Long.valueOf(maxAgeString); } catch (NumberFormatException e) { maxAge = DEFAULT_MAX_AGE; } includeSubdomains = BooleanUtil.valueOf(filterConfig.getInitParameter("includeSubdomains")); } @Override public void doFilter(final ServletRequest req, final ServletResponse resp, final FilterChain chain) throws ServletException, IOException { final HttpServletRequest request = (HttpServletRequest) req; final HttpServletResponse response = (HttpServletResponse) resp; if (request.isSecure()) { if (includeSubdomains) { response.setHeader("Strict-Transport-Security", "max-age=" + maxAge + "; includeSubDomains"); } else { response.setHeader("Strict-Transport-Security", "max-age=" + maxAge + ";"); } } else { response.setStatus(HttpServletResponse.SC_MOVED_PERMANENTLY); final StringBuilder sb = new StringBuilder(); sb.append("https://").append(request.getServerName()); if (httpsPort != DEFAULT_HTTPS_PORT) { sb.append(":").append(httpsPort); } if (request.getContextPath() != null) { sb.append(request.getContextPath()); } if (request.getServletPath() != null) { sb.append(request.getServletPath()); } if (request.getPathInfo() != null) { sb.append(request.getPathInfo()); } if (request.getQueryString() != null && request.getQueryString().length() > 0) { sb.append("?").append(request.getQueryString()); } response.setHeader("Location", sb.toString()); return; } chain.doFilter(request, response); } @Override public void destroy() { // Intentionally empty to satisfy interface } }