001/*
002 * Licensed to the Apache Software Foundation (ASF) under one
003 * or more contributor license agreements.  See the NOTICE file
004 * distributed with this work for additional information
005 * regarding copyright ownership.  The ASF licenses this file
006 * to you under the Apache License, Version 2.0 (the
007 * "License"); you may not use this file except in compliance
008 * with the License.  You may obtain a copy of the License at
009 *
010 *     http://www.apache.org/licenses/LICENSE-2.0
011 *
012 * Unless required by applicable law or agreed to in writing,
013 * software distributed under the License is distributed on an
014 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
015 * KIND, either express or implied.  See the License for the
016 * specific language governing permissions and limitations
017 * under the License.
018 */
019package org.apache.shiro.web.filter;
020
021import org.apache.shiro.util.AntPathMatcher;
022import org.apache.shiro.util.PatternMatcher;
023import org.apache.shiro.web.servlet.AdviceFilter;
024import org.apache.shiro.web.util.WebUtils;
025import org.owasp.encoder.Encode;
026import org.slf4j.Logger;
027import org.slf4j.LoggerFactory;
028
029import javax.servlet.Filter;
030import javax.servlet.ServletRequest;
031import javax.servlet.ServletResponse;
032import java.util.LinkedHashMap;
033import java.util.Map;
034
035import static org.apache.shiro.lang.util.StringUtils.split;
036
037/**
038 * <p>Base class for Filters that will process only specified paths and allow all others to pass through.</p>
039 *
040 * @since 0.9
041 */
042public abstract class PathMatchingFilter extends AdviceFilter implements PathConfigProcessor {
043
044    /**
045     * Log available to this class only
046     */
047    private static final Logger LOGGER = LoggerFactory.getLogger(PathMatchingFilter.class);
048
049    private static final String DEFAULT_PATH_SEPARATOR = "/";
050
051    /**
052     * PatternMatcher used in determining which paths to react to for a given request.
053     */
054    protected PatternMatcher pathMatcher = new AntPathMatcher();
055
056    /**
057     * A collection of path-to-config entries where the key is a path which this filter should process and
058     * the value is the (possibly null) configuration element specific to this Filter for that specific path.
059     * <p/>
060     * <p>To put it another way, the keys are the paths (urls) that this Filter will process.
061     * <p>The values are filter-specific data that this Filter should use when processing the corresponding
062     * key (path).  The values can be null if no Filter-specific config was specified for that url.
063     */
064    protected Map<String, Object> appliedPaths = new LinkedHashMap<String, Object>();
065
066    /**
067     * Splits any comma-delimited values that might be found in the <code>config</code> argument and sets the resulting
068     * <code>String[]</code> array on the <code>appliedPaths</code> internal Map.
069     * <p/>
070     * That is:
071     * <pre><code>
072     * String[] values = null;
073     * if (config != null) {
074     *     values = split(config);
075     * }
076     * <p/>
077     * this.{@link #appliedPaths appliedPaths}.put(path, values);
078     * </code></pre>
079     *
080     * @param path   the application context path to match for executing this filter.
081     * @param config the specified for <em>this particular filter only</em> for the given <code>path</code>
082     * @return this configured filter.
083     */
084    public Filter processPathConfig(String path, String config) {
085        String[] values = null;
086        if (config != null) {
087            values = split(config);
088        }
089
090        this.appliedPaths.put(path, values);
091        return this;
092    }
093
094    @Override
095    public void setCaseInsensitive(boolean caseInsensitive) {
096        if (pathMatcher != null) {
097            pathMatcher.setCaseInsensitive(caseInsensitive);
098        }
099    }
100
101    /**
102     * Returns the context path within the application based on the specified <code>request</code>.
103     * <p/>
104     * This implementation merely delegates to
105     * {@link WebUtils#getPathWithinApplication(javax.servlet.http.HttpServletRequest)
106     *      WebUtils.getPathWithinApplication(request)},
107     * but can be overridden by subclasses for custom logic.
108     *
109     * @param request the incoming <code>ServletRequest</code>
110     * @return the context path within the application.
111     */
112    protected String getPathWithinApplication(ServletRequest request) {
113        return WebUtils.getPathWithinApplication(WebUtils.toHttp(request));
114    }
115
116    /**
117     * Returns <code>true</code> if the incoming <code>request</code> matches the specified <code>path</code> pattern,
118     * <code>false</code> otherwise.
119     * <p/>
120     * The default implementation acquires the <code>request</code>'s path within the application and determines
121     * if that matches:
122     * <p/>
123     * <code>String requestURI = {@link #getPathWithinApplication(ServletRequest) getPathWithinApplication(request)};<br/>
124     * return {@link #pathsMatch(String, String) pathsMatch(path,requestURI)}</code>
125     *
126     * @param path    the configured url pattern to check the incoming request against.
127     * @param request the incoming ServletRequest
128     * @return <code>true</code> if the incoming <code>request</code> matches the specified <code>path</code> pattern,
129     * <code>false</code> otherwise.
130     */
131    protected boolean pathsMatch(String path, ServletRequest request) {
132        String requestURI = getPathWithinApplication(request);
133
134        LOGGER.trace("Attempting to match pattern '{}' with current requestURI '{}'...", path, Encode.forHtml(requestURI));
135        boolean match = pathsMatch(path, requestURI);
136
137        if (!match) {
138            if (requestURI != null && !DEFAULT_PATH_SEPARATOR.equals(requestURI)
139                    && requestURI.endsWith(DEFAULT_PATH_SEPARATOR)) {
140                requestURI = requestURI.substring(0, requestURI.length() - 1);
141            }
142            if (path != null && !DEFAULT_PATH_SEPARATOR.equals(path)
143                    && path.endsWith(DEFAULT_PATH_SEPARATOR)) {
144                path = path.substring(0, path.length() - 1);
145            }
146            LOGGER.trace("Attempting to match pattern '{}' with current requestURI '{}'...", path, Encode.forHtml(requestURI));
147            match = pathsMatch(path, requestURI);
148        }
149
150        return match;
151    }
152
153    /**
154     * Returns <code>true</code> if the <code>path</code> matches the specified <code>pattern</code> string,
155     * <code>false</code> otherwise.
156     * <p/>
157     * Simply delegates to
158     * <b><code>this.pathMatcher.{@link PatternMatcher#matches(String, String) matches(pattern,path)}</code></b>,
159     * but can be overridden by subclasses for custom matching behavior.
160     *
161     * @param pattern the pattern to match against
162     * @param path    the value to match with the specified <code>pattern</code>
163     * @return <code>true</code> if the <code>path</code> matches the specified <code>pattern</code> string,
164     * <code>false</code> otherwise.
165     */
166    protected boolean pathsMatch(String pattern, String path) {
167        boolean matches = pathMatcher.matches(pattern, path);
168        LOGGER.trace("Pattern [{}] matches path [{}] => [{}]", pattern, path, matches);
169        return matches;
170    }
171
172    /**
173     * Implementation that handles path-matching behavior before a request is evaluated.  If the path matches and
174     * the filter
175     * {@link #isEnabled(javax.servlet.ServletRequest, javax.servlet.ServletResponse, String, Object) isEnabled} for
176     * that path/config, the request will be allowed through via the result from
177     * {@link #onPreHandle(javax.servlet.ServletRequest, javax.servlet.ServletResponse, Object) onPreHandle}.  If the
178     * path does not match or the filter is not enabled for that path, this filter will allow passthrough immediately
179     * to allow the {@code FilterChain} to continue executing.
180     * <p/>
181     * In order to retain path-matching functionality, subclasses should not override this method if at all
182     * possible, and instead override
183     * {@link #onPreHandle(javax.servlet.ServletRequest, javax.servlet.ServletResponse, Object) onPreHandle} instead.
184     *
185     * @param request  the incoming ServletRequest
186     * @param response the outgoing ServletResponse
187     * @return {@code true} if the filter chain is allowed to continue to execute, {@code false} if a subclass has
188     * handled the request explicitly.
189     * @throws Exception if an error occurs
190     */
191    protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception {
192
193        if (this.appliedPaths == null || this.appliedPaths.isEmpty()) {
194            if (LOGGER.isTraceEnabled()) {
195                LOGGER.trace("appliedPaths property is null or empty.  This Filter will passthrough immediately.");
196            }
197            return true;
198        }
199
200        for (String path : this.appliedPaths.keySet()) {
201            // If the path does match, then pass on to the subclass implementation for specific checks
202            //(first match 'wins'):
203            if (pathsMatch(path, request)) {
204                LOGGER.trace("Current requestURI matches pattern '{}'.  Determining filter chain execution...", path);
205                Object config = this.appliedPaths.get(path);
206                return isFilterChainContinued(request, response, path, config);
207            }
208        }
209
210        //no path matched, allow the request to go through:
211        return true;
212    }
213
214    /**
215     * Simple method to abstract out logic from the preHandle implementation - it was getting a bit unruly.
216     *
217     * @since 1.2
218     */
219    @SuppressWarnings({"JavaDoc"})
220    private boolean isFilterChainContinued(ServletRequest request, ServletResponse response,
221                                           String path, Object pathConfig) throws Exception {
222
223        //isEnabled check added in 1.2
224        if (isEnabled(request, response, path, pathConfig)) {
225            if (LOGGER.isTraceEnabled()) {
226                LOGGER.trace("Filter '{}' is enabled for the current request under path '{}' with config [{}].  "
227                                + "Delegating to subclass implementation for 'onPreHandle' check.",
228                        getName(), path, pathConfig);
229            }
230            //The filter is enabled for this specific request, so delegate to subclass implementations
231            //so they can decide if the request should continue through the chain or not:
232            return onPreHandle(request, response, pathConfig);
233        }
234
235        if (LOGGER.isTraceEnabled()) {
236            LOGGER.trace("Filter '{}' is disabled for the current request under path '{}' with config [{}].  "
237                            + "The next element in the FilterChain will be called immediately.",
238                    getName(), path, pathConfig);
239        }
240        //This filter is disabled for this specific request,
241        //return 'true' immediately to indicate that the filter will not process the request
242        //and let the request/response to continue through the filter chain:
243        return true;
244    }
245
246    /**
247     * This default implementation always returns {@code true} and should be overridden by subclasses for custom
248     * logic if necessary.
249     *
250     * @param request     the incoming ServletRequest
251     * @param response    the outgoing ServletResponse
252     * @param mappedValue the filter-specific config value mapped to this filter in the URL rules mappings.
253     * @return {@code true} if the request should be able to continue, {@code false} if the filter will
254     * handle the response directly.
255     * @throws Exception if an error occurs
256     * @see #isEnabled(javax.servlet.ServletRequest, javax.servlet.ServletResponse, String, Object)
257     */
258    protected boolean onPreHandle(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception {
259        return true;
260    }
261
262    @SuppressWarnings("UnusedParameters")
263    /**
264     * Path-matching version of the parent class's
265     * {@link #isEnabled(javax.servlet.ServletRequest, javax.servlet.ServletResponse)} method, but additionally allows
266     * for inspection of any path-specific configuration values corresponding to the specified request.  Subclasses
267     * may wish to inspect this additional mapped configuration to determine if the filter is enabled or not.
268     * <p/>
269     * This method's default implementation ignores the {@code path} and {@code mappedValue} arguments and merely
270     * returns the value from a call to {@link #isEnabled(javax.servlet.ServletRequest, javax.servlet.ServletResponse)}.
271     * It is expected that subclasses override this method if they need to perform enable/disable logic for a specific
272     * request based on any path-specific config for the filter instance.
273     *
274     * @param request     the incoming servlet request
275     * @param response    the outbound servlet response
276     * @param path        the path matched for the incoming servlet request
277     *                    that has been configured with the given {@code mappedValue}.
278     * @param mappedValue the filter-specific config value mapped to
279     *                    this filter in the URL rules mappings for the given {@code path}.
280     * @return {@code true} if this filter should filter the specified request, {@code false} if it should let the
281     * request/response pass through immediately to the next element in the {@code FilterChain}.
282     * @throws Exception in the case of any error
283     * @since 1.2
284     */
285    protected boolean isEnabled(ServletRequest request, ServletResponse response, String path, Object mappedValue)
286            throws Exception {
287        return isEnabled(request, response);
288    }
289}