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     */
019    package org.apache.shiro.web.filter;
020    
021    import org.apache.shiro.util.AntPathMatcher;
022    import org.apache.shiro.util.PatternMatcher;
023    import org.apache.shiro.util.StringUtils;
024    import org.apache.shiro.web.servlet.AdviceFilter;
025    import org.apache.shiro.web.util.WebUtils;
026    import org.slf4j.Logger;
027    import org.slf4j.LoggerFactory;
028    
029    import javax.servlet.Filter;
030    import javax.servlet.ServletRequest;
031    import javax.servlet.ServletResponse;
032    import java.util.LinkedHashMap;
033    import java.util.Map;
034    
035    import static org.apache.shiro.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     */
042    public abstract class PathMatchingFilter extends AdviceFilter implements PathConfigProcessor {
043    
044        /**
045         * Log available to this class only
046         */
047        private static final Logger log = LoggerFactory.getLogger(PathMatchingFilter.class);
048    
049        /**
050         * PatternMatcher used in determining which paths to react to for a given request.
051         */
052        protected PatternMatcher pathMatcher = new AntPathMatcher();
053    
054        /**
055         * A collection of path-to-config entries where the key is a path which this filter should process and
056         * the value is the (possibly null) configuration element specific to this Filter for that specific path.
057         * <p/>
058         * <p>To put it another way, the keys are the paths (urls) that this Filter will process.
059         * <p>The values are filter-specific data that this Filter should use when processing the corresponding
060         * key (path).  The values can be null if no Filter-specific config was specified for that url.
061         */
062        protected Map<String, Object> appliedPaths = new LinkedHashMap<String, Object>();
063    
064        /**
065         * Splits any comma-delmited values that might be found in the <code>config</code> argument and sets the resulting
066         * <code>String[]</code> array on the <code>appliedPaths</code> internal Map.
067         * <p/>
068         * That is:
069         * <pre><code>
070         * String[] values = null;
071         * if (config != null) {
072         *     values = split(config);
073         * }
074         * <p/>
075         * this.{@link #appliedPaths appliedPaths}.put(path, values);
076         * </code></pre>
077         *
078         * @param path   the application context path to match for executing this filter.
079         * @param config the specified for <em>this particular filter only</em> for the given <code>path</code>
080         * @return this configured filter.
081         */
082        public Filter processPathConfig(String path, String config) {
083            String[] values = null;
084            if (config != null) {
085                values = split(config);
086            }
087    
088            this.appliedPaths.put(path, values);
089            return this;
090        }
091    
092        /**
093         * Returns the context path within the application based on the specified <code>request</code>.
094         * <p/>
095         * This implementation merely delegates to
096         * {@link WebUtils#getPathWithinApplication(javax.servlet.http.HttpServletRequest) WebUtils.getPathWithinApplication(request)},
097         * but can be overridden by subclasses for custom logic.
098         *
099         * @param request the incoming <code>ServletRequest</code>
100         * @return the context path within the application.
101         */
102        protected String getPathWithinApplication(ServletRequest request) {
103            return WebUtils.getPathWithinApplication(WebUtils.toHttp(request));
104        }
105    
106        /**
107         * Returns <code>true</code> if the incoming <code>request</code> matches the specified <code>path</code> pattern,
108         * <code>false</code> otherwise.
109         * <p/>
110         * The default implementation acquires the <code>request</code>'s path within the application and determines
111         * if that matches:
112         * <p/>
113         * <code>String requestURI = {@link #getPathWithinApplication(javax.servlet.ServletRequest) getPathWithinApplication(request)};<br/>
114         * return {@link #pathsMatch(String, String) pathsMatch(path,requestURI)}</code>
115         *
116         * @param path    the configured url pattern to check the incoming request against.
117         * @param request the incoming ServletRequest
118         * @return <code>true</code> if the incoming <code>request</code> matches the specified <code>path</code> pattern,
119         *         <code>false</code> otherwise.
120         */
121        protected boolean pathsMatch(String path, ServletRequest request) {
122            String requestURI = getPathWithinApplication(request);
123            log.trace("Attempting to match pattern '{}' with current requestURI '{}'...", path, requestURI);
124            return pathsMatch(path, requestURI);
125        }
126    
127        /**
128         * Returns <code>true</code> if the <code>path</code> matches the specified <code>pattern</code> string,
129         * <code>false</code> otherwise.
130         * <p/>
131         * Simply delegates to
132         * <b><code>this.pathMatcher.{@link PatternMatcher#matches(String, String) matches(pattern,path)}</code></b>,
133         * but can be overridden by subclasses for custom matching behavior.
134         *
135         * @param pattern the pattern to match against
136         * @param path    the value to match with the specified <code>pattern</code>
137         * @return <code>true</code> if the <code>path</code> matches the specified <code>pattern</code> string,
138         *         <code>false</code> otherwise.
139         */
140        protected boolean pathsMatch(String pattern, String path) {
141            return pathMatcher.matches(pattern, path);
142        }
143    
144        /**
145         * Implementation that handles path-matching behavior before a request is evaluated.  If the path matches and
146         * the filter
147         * {@link #isEnabled(javax.servlet.ServletRequest, javax.servlet.ServletResponse, String, Object) isEnabled} for
148         * that path/config, the request will be allowed through via the result from
149         * {@link #onPreHandle(javax.servlet.ServletRequest, javax.servlet.ServletResponse, Object) onPreHandle}.  If the
150         * path does not match or the filter is not enabled for that path, this filter will allow passthrough immediately
151         * to allow the {@code FilterChain} to continue executing.
152         * <p/>
153         * In order to retain path-matching functionality, subclasses should not override this method if at all
154         * possible, and instead override
155         * {@link #onPreHandle(javax.servlet.ServletRequest, javax.servlet.ServletResponse, Object) onPreHandle} instead.
156         *
157         * @param request  the incoming ServletRequest
158         * @param response the outgoing ServletResponse
159         * @return {@code true} if the filter chain is allowed to continue to execute, {@code false} if a subclass has
160         *         handled the request explicitly.
161         * @throws Exception if an error occurs
162         */
163        protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception {
164    
165            if (this.appliedPaths == null || this.appliedPaths.isEmpty()) {
166                if (log.isTraceEnabled()) {
167                    log.trace("appliedPaths property is null or empty.  This Filter will passthrough immediately.");
168                }
169                return true;
170            }
171    
172            for (String path : this.appliedPaths.keySet()) {
173                // If the path does match, then pass on to the subclass implementation for specific checks
174                //(first match 'wins'):
175                if (pathsMatch(path, request)) {
176                    log.trace("Current requestURI matches pattern '{}'.  Determining filter chain execution...", path);
177                    Object config = this.appliedPaths.get(path);
178                    return isFilterChainContinued(request, response, path, config);
179                }
180            }
181    
182            //no path matched, allow the request to go through:
183            return true;
184        }
185    
186        /**
187         * Simple method to abstract out logic from the preHandle implementation - it was getting a bit unruly.
188         *
189         * @since 1.2
190         */
191        @SuppressWarnings({"JavaDoc"})
192        private boolean isFilterChainContinued(ServletRequest request, ServletResponse response,
193                                               String path, Object pathConfig) throws Exception {
194    
195            if (isEnabled(request, response, path, pathConfig)) { //isEnabled check added in 1.2
196                if (log.isTraceEnabled()) {
197                    log.trace("Filter '{}' is enabled for the current request under path '{}' with config [{}].  " +
198                            "Delegating to subclass implementation for 'onPreHandle' check.",
199                            new Object[]{getName(), path, pathConfig});
200                }
201                //The filter is enabled for this specific request, so delegate to subclass implementations
202                //so they can decide if the request should continue through the chain or not:
203                return onPreHandle(request, response, pathConfig);
204            }
205    
206            if (log.isTraceEnabled()) {
207                log.trace("Filter '{}' is disabled for the current request under path '{}' with config [{}].  " +
208                        "The next element in the FilterChain will be called immediately.",
209                        new Object[]{getName(), path, pathConfig});
210            }
211            //This filter is disabled for this specific request,
212            //return 'true' immediately to indicate that the filter will not process the request
213            //and let the request/response to continue through the filter chain:
214            return true;
215        }
216    
217        /**
218         * This default implementation always returns {@code true} and should be overridden by subclasses for custom
219         * logic if necessary.
220         *
221         * @param request     the incoming ServletRequest
222         * @param response    the outgoing ServletResponse
223         * @param mappedValue the filter-specific config value mapped to this filter in the URL rules mappings.
224         * @return {@code true} if the request should be able to continue, {@code false} if the filter will
225         *         handle the response directly.
226         * @throws Exception if an error occurs
227         * @see #isEnabled(javax.servlet.ServletRequest, javax.servlet.ServletResponse, String, Object)
228         */
229        protected boolean onPreHandle(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception {
230            return true;
231        }
232    
233        /**
234         * Path-matching version of the parent class's
235         * {@link #isEnabled(javax.servlet.ServletRequest, javax.servlet.ServletResponse)} method, but additionally allows
236         * for inspection of any path-specific configuration values corresponding to the specified request.  Subclasses
237         * may wish to inspect this additional mapped configuration to determine if the filter is enabled or not.
238         * <p/>
239         * This method's default implementation ignores the {@code path} and {@code mappedValue} arguments and merely
240         * returns the value from a call to {@link #isEnabled(javax.servlet.ServletRequest, javax.servlet.ServletResponse)}.
241         * It is expected that subclasses override this method if they need to perform enable/disable logic for a specific
242         * request based on any path-specific config for the filter instance.
243         *
244         * @param request     the incoming servlet request
245         * @param response    the outbound servlet response
246         * @param path        the path matched for the incoming servlet request that has been configured with the given {@code mappedValue}.
247         * @param mappedValue the filter-specific config value mapped to this filter in the URL rules mappings for the given {@code path}.
248         * @return {@code true} if this filter should filter the specified request, {@code false} if it should let the
249         *         request/response pass through immediately to the next element in the {@code FilterChain}.
250         * @throws Exception in the case of any error
251         * @since 1.2
252         */
253        @SuppressWarnings({"UnusedParameters"})
254        protected boolean isEnabled(ServletRequest request, ServletResponse response, String path, Object mappedValue)
255                throws Exception {
256            return isEnabled(request, response);
257        }
258    }