/* * 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.wicket.protocol.http; import java.io.IOException; import java.util.HashSet; import java.util.Set; 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.annotation.WebFilter; import javax.servlet.annotation.WebServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.apache.wicket.ThreadContext; import org.apache.wicket.WicketRuntimeException; import org.apache.wicket.request.cycle.RequestCycle; import org.apache.wicket.request.http.WebRequest; import org.apache.wicket.request.http.WebResponse; import org.apache.wicket.util.file.WebXmlFile; import org.apache.wicket.util.lang.Args; import org.apache.wicket.util.string.Strings; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * Filter for initiating handling of Wicket requests. * <p> * The advantage of a filter is that, unlike a servlet, it can choose not to process the request and * let whatever is next in chain try. So when using a Wicket filter and a request comes in for * foo.gif the filter can choose not to process it because it knows it is not a wicket-related * request. Since the filter didn't process it, it falls on to the application server to try, and * then it works." * * @see WicketServlet for documentation * * @author Jonathan Locke * @author Timur Mehrvarz * @author Juergen Donnerstag * @author Igor Vaynberg (ivaynberg) * @author Al Maw * @author jcompagner * @author Matej Knopp */ public class WicketFilter implements Filter { private static final Logger log = LoggerFactory.getLogger(WicketFilter.class); /** The name of the root path parameter that specifies the root dir of the app. */ public static final String FILTER_MAPPING_PARAM = "filterMappingUrlPattern"; /** The name of the context parameter that specifies application factory class */ public static final String APP_FACT_PARAM = "applicationFactoryClassName"; /** * Name of parameter used to express a comma separated list of paths that should be ignored */ public static final String IGNORE_PATHS_PARAM = "ignorePaths"; // Wicket's Application object private WebApplication application; /** the factory used to create the web aplication instance */ private IWebApplicationFactory applicationFactory; private FilterConfig filterConfig; private String filterPath; // filterPath length without trailing "/" private int filterPathLength = -1; /** set of paths that should be ignored by the wicket filter */ private final Set<String> ignorePaths = new HashSet<String>(); /** * A flag indicating whether WicketFilter is used directly or through WicketServlet */ private boolean isServlet = false; /** * default constructor, usually invoked through the servlet container by the web.xml * configuration */ public WicketFilter() { } /** * constructor supporting programmatic setup of the filter * <p/> * this can be useful for programmatically creating and appending the wicket filter to the * servlet context using servlet 3 features. * * @param application * web application */ public WicketFilter(WebApplication application) { this.application = Args.notNull(application, "application"); } /** * @return The class loader */ protected ClassLoader getClassLoader() { return Thread.currentThread().getContextClassLoader(); } /** * This is Wicket's main method to execute a request * * @param request * @param response * @param chain * @return false, if the request could not be processed * @throws IOException * @throws ServletException */ boolean processRequest(ServletRequest request, final ServletResponse response, final FilterChain chain) throws IOException, ServletException { final ThreadContext previousThreadContext = ThreadContext.detach(); // Assume we are able to handle the request boolean res = true; final ClassLoader previousClassLoader = Thread.currentThread().getContextClassLoader(); final ClassLoader newClassLoader = getClassLoader(); HttpServletRequest httpServletRequest = (HttpServletRequest)request; HttpServletResponse httpServletResponse = (HttpServletResponse)response; try { if (previousClassLoader != newClassLoader) { Thread.currentThread().setContextClassLoader(newClassLoader); } // Make sure getFilterPath() gets called before checkIfRedirectRequired() String filterPath = getFilterPath(httpServletRequest); if (filterPath == null) { throw new IllegalStateException("filter path was not configured"); } if (shouldIgnorePath(httpServletRequest)) { log.debug("Ignoring request {}", httpServletRequest.getRequestURL()); if (chain != null) { // invoke next filter from within Wicket context chain.doFilter(request, response); } return false; } if ("OPTIONS".equalsIgnoreCase(httpServletRequest.getMethod())) { // handle the OPTIONS request outside of normal request processing. // wicket pages normally only support GET and POST methods, but resources and // special pages acting like REST clients can also support other methods, so // we include them all. httpServletResponse.setStatus(HttpServletResponse.SC_OK); httpServletResponse.setHeader("Allow", "GET,POST,OPTIONS,PUT,HEAD,PATCH,DELETE,TRACE"); httpServletResponse.setHeader("Content-Length", "0"); return true; } String redirectURL = checkIfRedirectRequired(httpServletRequest); if (redirectURL == null) { // No redirect; process the request ThreadContext.setApplication(application); WebRequest webRequest = application.createWebRequest(httpServletRequest, filterPath); WebResponse webResponse = application.createWebResponse(webRequest, httpServletResponse); RequestCycle requestCycle = application.createRequestCycle(webRequest, webResponse); res = processRequestCycle(requestCycle, webResponse, httpServletRequest, httpServletResponse, chain); } else { if (Strings.isEmpty(httpServletRequest.getQueryString()) == false) { redirectURL += "?" + httpServletRequest.getQueryString(); } try { // send redirect - this will discard POST parameters if the request is POST // - still better than getting an error because of lacking trailing slash httpServletResponse.sendRedirect(httpServletResponse.encodeRedirectURL(redirectURL)); } catch (IOException e) { throw new RuntimeException(e); } } } finally { ThreadContext.restore(previousThreadContext); if (newClassLoader != previousClassLoader) { Thread.currentThread().setContextClassLoader(previousClassLoader); } if (response.isCommitted() && httpServletRequest.isAsyncStarted() == false) { response.flushBuffer(); } } return res; } /** * Process the request cycle * * @param requestCycle * @param webResponse * @param httpServletRequest * @param httpServletResponse * @param chain * @return false, if the request could not be processed * @throws IOException * @throws ServletException */ protected boolean processRequestCycle(RequestCycle requestCycle, WebResponse webResponse, HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, final FilterChain chain) throws IOException, ServletException { // Assume we are able to handle the request boolean res = true; if (requestCycle.processRequestAndDetach()) { webResponse.flush(); } else { if (chain != null) { // invoke next filter from within Wicket context chain.doFilter(httpServletRequest, httpServletResponse); } res = false; } return res; } /** * @see javax.servlet.Filter#doFilter(javax.servlet.ServletRequest, * javax.servlet.ServletResponse, javax.servlet.FilterChain) */ @Override public void doFilter(final ServletRequest request, final ServletResponse response, final FilterChain chain) throws IOException, ServletException { processRequest(request, response, chain); } /** * Creates the web application factory instance. * * If no APP_FACT_PARAM is specified in web.xml ContextParamWebApplicationFactory will be used * by default. * * @see ContextParamWebApplicationFactory * * @return application factory instance */ protected IWebApplicationFactory getApplicationFactory() { final String appFactoryClassName = filterConfig.getInitParameter(APP_FACT_PARAM); if (appFactoryClassName == null) { // If no context param was specified we return the default factory return new ContextParamWebApplicationFactory(); } else { try { // Try to find the specified factory class // see http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=6500212 // final Class<?> factoryClass = Thread.currentThread() // .getContextClassLoader() // .loadClass(appFactoryClassName); final Class<?> factoryClass = Class.forName(appFactoryClassName, false, Thread.currentThread().getContextClassLoader()); // Instantiate the factory return (IWebApplicationFactory)factoryClass.newInstance(); } catch (ClassCastException e) { throw new WicketRuntimeException("Application factory class " + appFactoryClassName + " must implement IWebApplicationFactory"); } catch (ClassNotFoundException e) { throw new WebApplicationFactoryCreationException(appFactoryClassName, e); } catch (InstantiationException e) { throw new WebApplicationFactoryCreationException(appFactoryClassName, e); } catch (IllegalAccessException e) { throw new WebApplicationFactoryCreationException(appFactoryClassName, e); } catch (SecurityException e) { throw new WebApplicationFactoryCreationException(appFactoryClassName, e); } } } /** * If you do have a need to subclass, you may subclass {@link #init(boolean, FilterConfig)} * * @see javax.servlet.Filter#init(javax.servlet.FilterConfig) */ @Override public final void init(final FilterConfig filterConfig) throws ServletException { init(false, filterConfig); } /** * Servlets and Filters are treated essentially the same with Wicket. This is the entry point * for both of them. * * @see #init(FilterConfig) * * @param isServlet * True if Servlet, false if Filter * @param filterConfig * @throws ServletException */ public void init(final boolean isServlet, final FilterConfig filterConfig) throws ServletException { this.filterConfig = filterConfig; this.isServlet = isServlet; initIgnorePaths(filterConfig); final ClassLoader previousClassLoader = Thread.currentThread().getContextClassLoader(); final ClassLoader newClassLoader = getClassLoader(); try { if (previousClassLoader != newClassLoader) { Thread.currentThread().setContextClassLoader(newClassLoader); } // locate application instance unless it was already specified during construction if (application == null) { applicationFactory = getApplicationFactory(); application = applicationFactory.createApplication(this); } if (application.getName() == null) { application.setName(filterConfig.getFilterName()); } application.setWicketFilter(this); // Allow the filterPath to be preset via setFilterPath() String configureFilterPath = getFilterPath(); if (configureFilterPath == null) { configureFilterPath = getFilterPathFromConfig(filterConfig); if (configureFilterPath == null) { configureFilterPath = getFilterPathFromWebXml(isServlet, filterConfig); if (configureFilterPath == null) { configureFilterPath = getFilterPathFromAnnotation(isServlet); } } if (configureFilterPath != null) { setFilterPath(configureFilterPath); } } if (getFilterPath() == null) { log.warn("Unable to determine filter path from filter init-param, web.xml, " + "or servlet 3.0 annotations. Assuming user will set filter path " + "manually by calling setFilterPath(String)"); } ThreadContext.setApplication(application); try { application.initApplication(); // Give the application the option to log that it is started application.logStarted(); } finally { ThreadContext.detach(); } } catch (Exception e) { // #destroy() might not be called by the web container when #init() fails, // so destroy now log.error(String.format("The initialization of an application with name '%s' has failed.", filterConfig.getFilterName()), e); try { destroy(); } catch (Exception destroyException) { log.error("Unable to destroy after initialization failure", destroyException); } throw new ServletException(e); } finally { if (newClassLoader != previousClassLoader) { Thread.currentThread().setContextClassLoader(previousClassLoader); } } } /** * Stub method that lets subclasses configure filter path from annotations. * * @param isServlet * @return Filter path from annotation */ protected String getFilterPathFromAnnotation(boolean isServlet) { String[] patterns = null; if (isServlet) { WebServlet servlet = getClass().getAnnotation(WebServlet.class); if (servlet != null) { if (servlet.urlPatterns().length > 0) { patterns = servlet.urlPatterns(); } else { patterns = servlet.value(); } } } else { WebFilter filter = getClass().getAnnotation(WebFilter.class); if (filter != null) { if (filter.urlPatterns().length > 0) { patterns = filter.urlPatterns(); } else { patterns = filter.value(); } } } if (patterns != null && patterns.length > 0) { String pattern = patterns[0]; if (patterns.length > 1) { log.warn( "Multiple url patterns defined for Wicket filter/servlet, using the first: {}", pattern); } if ("/*".equals(pattern)) { pattern = ""; } if (pattern.endsWith("*")) { pattern = pattern.substring(0, pattern.length() - 1); } return pattern; } return null; } /** * * @param isServlet * @param filterConfig * @return filter path from web.xml */ protected String getFilterPathFromWebXml(final boolean isServlet, final FilterConfig filterConfig) { return new WebXmlFile().getUniqueFilterPath(isServlet, filterConfig); } /** * @return filter config */ public FilterConfig getFilterConfig() { return filterConfig; } /** * Either get the filterPath retrieved from web.xml, or if not found the old (1.3) way via a * filter mapping param. * * @param request * @return filterPath */ protected String getFilterPath(final HttpServletRequest request) { return filterPath; } /** * Provide a standard getter for filterPath. * * @return The configured filterPath. */ public String getFilterPath() { return filterPath; } /** * * @param filterConfig * @return filter path */ protected String getFilterPathFromConfig(FilterConfig filterConfig) { String result = filterConfig.getInitParameter(FILTER_MAPPING_PARAM); if (result != null) { if (result.equals("/*")) { result = ""; } else if (!result.startsWith("/") || !result.endsWith("/*")) { throw new WicketRuntimeException("Your " + FILTER_MAPPING_PARAM + " must start with \"/\" and end with \"/*\". It is: " + result); } else { // remove leading "/" and trailing "*" result = result.substring(1, result.length() - 1); } } return result; } /** * @see javax.servlet.Filter#destroy() */ @Override public void destroy() { if (application != null) { try { ThreadContext.setApplication(application); application.internalDestroy(); } finally { ThreadContext.detach(); application = null; } } if (applicationFactory != null) { try { applicationFactory.destroy(this); } finally { applicationFactory = null; } } } /** * Try to determine as fast as possible if a redirect is necessary * * @param request * @return null, if no redirect is necessary. Else the redirect URL */ private String checkIfRedirectRequired(final HttpServletRequest request) { return checkIfRedirectRequired(request.getRequestURI(), request.getContextPath()); } /** * Try to determine as fast as possible if a redirect is necessary * * @param requestURI * @param contextPath * @return null, if no redirect is necessary. Else the redirect URL */ protected final String checkIfRedirectRequired(final String requestURI, final String contextPath) { // length without jsessionid (http://.../abc;jsessionid=...?param) int uriLength = requestURI.indexOf(';'); if (uriLength == -1) { uriLength = requestURI.length(); } // request.getContextPath() + "/" + filterPath. But without any trailing "/". int homePathLength = contextPath.length() + (filterPathLength > 0 ? 1 + filterPathLength : 0); if (uriLength != homePathLength) { // requestURI and homePath are different (in length) // => continue with standard request processing. No redirect. return null; } // Fail fast failed. Revert to "slow" but exact check String uri = Strings.stripJSessionId(requestURI); // home page without trailing slash URI String homePageUri = contextPath + '/' + getFilterPath(); if (homePageUri.endsWith("/")) { homePageUri = homePageUri.substring(0, homePageUri.length() - 1); } // If both are equal => redirect if (uri.equals(homePageUri)) { uri += "/"; return uri; } // no match => standard request processing; no redirect return null; } /** * Sets the filter path instead of reading it from web.xml. * * Please note that you must subclass WicketFilter.init(FilterConfig) and set your filter path * before you call super.init(filterConfig). * * @param filterPath */ public final void setFilterPath(String filterPath) { // see https://issues.apache.org/jira/browse/WICKET-701 if (this.filterPath != null) { throw new IllegalStateException( "Filter path is write-once. You can not change it. Current value='" + filterPath + '\''); } if (filterPath != null) { filterPath = canonicaliseFilterPath(filterPath); // We only need to determine it once. It'll not change. if (filterPath.endsWith("/")) { filterPathLength = filterPath.length() - 1; } else { filterPathLength = filterPath.length(); } } this.filterPath = filterPath; } /** * Returns a relative path to the filter path and context root from an HttpServletRequest - use * this to resolve a Wicket request. * * @param request * @return Path requested, minus query string, context path, and filterPath. Relative, no * leading '/'. */ public String getRelativePath(HttpServletRequest request) { String path = Strings.stripJSessionId(request.getRequestURI()); String contextPath = request.getContextPath(); path = path.substring(contextPath.length()); if (isServlet) { String servletPath = request.getServletPath(); path = path.substring(servletPath.length()); } if (path.length() > 0) { path = path.substring(1); } // We should always be under the rootPath, except // for the special case of someone landing on the // home page without a trailing slash. String filterPath = getFilterPath(); if (!path.startsWith(filterPath)) { if (filterPath.equals(path + "/")) { path += "/"; } } if (path.startsWith(filterPath)) { path = path.substring(filterPath.length()); } return path; } protected WebApplication getApplication() { return application; } /** * Checks whether this is a request to an ignored path * * @param request * the current http request * @return {@code true} when the request should be ignored, {@code false} - otherwise */ private boolean shouldIgnorePath(final HttpServletRequest request) { boolean ignore = false; if (ignorePaths.size() > 0) { String relativePath = getRelativePath(request); if (Strings.isEmpty(relativePath) == false) { for (String path : ignorePaths) { if (relativePath.startsWith(path)) { ignore = true; break; } } } } return ignore; } /** * initializes the ignore paths parameter * * @param filterConfig */ private void initIgnorePaths(final FilterConfig filterConfig) { String paths = filterConfig.getInitParameter(IGNORE_PATHS_PARAM); if (Strings.isEmpty(paths) == false) { String[] parts = Strings.split(paths, ','); for (String path : parts) { path = path.trim(); if (path.startsWith("/")) { path = path.substring(1); } ignorePaths.add(path); } } } /** * A filterPath should have all leading slashes removed and exactly one trailing slash. A * wildcard asterisk character has no special meaning. If your intention is to mean the top * level "/" then an empty string should be used instead. * * @param filterPath * @return canonic filter path */ static String canonicaliseFilterPath(String filterPath) { if (Strings.isEmpty(filterPath)) { return filterPath; } int beginIndex = 0; int endIndex = filterPath.length(); while (beginIndex < endIndex) { char c = filterPath.charAt(beginIndex); if (c != '/') { break; } beginIndex++; } int o; int i = o = beginIndex; while (i < endIndex) { char c = filterPath.charAt(i); i++; if (c != '/') { o = i; } } if (o < endIndex) { o++; // include exactly one trailing slash filterPath = filterPath.substring(beginIndex, o); } else { // ensure to append trailing slash filterPath = filterPath.substring(beginIndex) + '/'; } if (filterPath.equals("/")) { return ""; } return filterPath; } }