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.mgt;
020
021import org.apache.shiro.config.ConfigurationException;
022import org.apache.shiro.util.CollectionUtils;
023import org.apache.shiro.util.Nameable;
024import org.apache.shiro.util.StringUtils;
025import org.apache.shiro.web.filter.PathConfigProcessor;
026import org.slf4j.Logger;
027import org.slf4j.LoggerFactory;
028
029import javax.servlet.Filter;
030import javax.servlet.FilterChain;
031import javax.servlet.FilterConfig;
032import javax.servlet.ServletException;
033import java.util.ArrayList;
034import java.util.Collections;
035import java.util.LinkedHashMap;
036import java.util.List;
037import java.util.Map;
038import java.util.Set;
039
040/**
041 * Default {@link FilterChainManager} implementation maintaining a map of {@link Filter Filter} instances
042 * (key: filter name, value: Filter) as well as a map of {@link NamedFilterList NamedFilterList}s created from these
043 * {@code Filter}s (key: filter chain name, value: NamedFilterList).  The {@code NamedFilterList} is essentially a
044 * {@link FilterChain} that also has a name property by which it can be looked up.
045 *
046 * @see NamedFilterList
047 * @since 1.0
048 */
049public class DefaultFilterChainManager implements FilterChainManager {
050
051    private static transient final Logger log = LoggerFactory.getLogger(DefaultFilterChainManager.class);
052
053    private FilterConfig filterConfig;
054
055    private Map<String, Filter> filters; //pool of filters available for creating chains
056
057    private List<String> globalFilterNames; // list of filters to prepend to every chain
058
059    private Map<String, NamedFilterList> filterChains; //key: chain name, value: chain
060
061    public DefaultFilterChainManager() {
062        this.filters = new LinkedHashMap<String, Filter>();
063        this.filterChains = new LinkedHashMap<String, NamedFilterList>();
064        this.globalFilterNames = new ArrayList<>();
065        addDefaultFilters(false);
066    }
067
068    public DefaultFilterChainManager(FilterConfig filterConfig) {
069        this.filters = new LinkedHashMap<String, Filter>();
070        this.filterChains = new LinkedHashMap<String, NamedFilterList>();
071        this.globalFilterNames = new ArrayList<>();
072        setFilterConfig(filterConfig);
073        addDefaultFilters(true);
074    }
075
076    /**
077     * Returns the {@code FilterConfig} provided by the Servlet container at webapp startup.
078     *
079     * @return the {@code FilterConfig} provided by the Servlet container at webapp startup.
080     */
081    public FilterConfig getFilterConfig() {
082        return filterConfig;
083    }
084
085    /**
086     * Sets the {@code FilterConfig} provided by the Servlet container at webapp startup.
087     *
088     * @param filterConfig the {@code FilterConfig} provided by the Servlet container at webapp startup.
089     */
090    public void setFilterConfig(FilterConfig filterConfig) {
091        this.filterConfig = filterConfig;
092    }
093
094    public Map<String, Filter> getFilters() {
095        return filters;
096    }
097
098    @SuppressWarnings({"UnusedDeclaration"})
099    public void setFilters(Map<String, Filter> filters) {
100        this.filters = filters;
101    }
102
103    public Map<String, NamedFilterList> getFilterChains() {
104        return filterChains;
105    }
106
107    @SuppressWarnings({"UnusedDeclaration"})
108    public void setFilterChains(Map<String, NamedFilterList> filterChains) {
109        this.filterChains = filterChains;
110    }
111
112    public Filter getFilter(String name) {
113        return this.filters.get(name);
114    }
115
116    public void addFilter(String name, Filter filter) {
117        addFilter(name, filter, false);
118    }
119
120    public void addFilter(String name, Filter filter, boolean init) {
121        addFilter(name, filter, init, true);
122    }
123
124    public void createDefaultChain(String chainName) {
125        // only create the defaultChain if we don't have a chain with this name already
126        // (the global filters will already be in that chain)
127        if (!getChainNames().contains(chainName) && !CollectionUtils.isEmpty(globalFilterNames)) {
128            // add each of global filters
129            globalFilterNames.stream().forEach(filterName -> addToChain(chainName, filterName));
130        }
131    }
132
133    public void createChain(String chainName, String chainDefinition) {
134        if (!StringUtils.hasText(chainName)) {
135            throw new NullPointerException("chainName cannot be null or empty.");
136        }
137        if (!StringUtils.hasText(chainDefinition)) {
138            throw new NullPointerException("chainDefinition cannot be null or empty.");
139        }
140
141        if (log.isDebugEnabled()) {
142            log.debug("Creating chain [" + chainName + "] with global filters " + globalFilterNames + " and from String definition [" + chainDefinition + "]");
143        }
144
145        // first add each of global filters
146        if (!CollectionUtils.isEmpty(globalFilterNames)) {
147            globalFilterNames.stream().forEach(filterName -> addToChain(chainName, filterName));
148        }
149
150        //parse the value by tokenizing it to get the resulting filter-specific config entries
151        //
152        //e.g. for a value of
153        //
154        //     "authc, roles[admin,user], perms[file:edit]"
155        //
156        // the resulting token array would equal
157        //
158        //     { "authc", "roles[admin,user]", "perms[file:edit]" }
159        //
160        String[] filterTokens = splitChainDefinition(chainDefinition);
161
162        //each token is specific to each filter.
163        //strip the name and extract any filter-specific config between brackets [ ]
164        for (String token : filterTokens) {
165            String[] nameConfigPair = toNameConfigPair(token);
166
167            //now we have the filter name, path and (possibly null) path-specific config.  Let's apply them:
168            addToChain(chainName, nameConfigPair[0], nameConfigPair[1]);
169        }
170    }
171
172    /**
173     * Splits the comma-delimited filter chain definition line into individual filter definition tokens.
174     * <p/>
175     * Example Input:
176     * <pre>
177     *     foo, bar[baz], blah[x, y]
178     * </pre>
179     * Resulting Output:
180     * <pre>
181     *     output[0] == foo
182     *     output[1] == bar[baz]
183     *     output[2] == blah[x, y]
184     * </pre>
185     * @param chainDefinition the comma-delimited filter chain definition.
186     * @return an array of filter definition tokens
187     * @since 1.2
188     * @see <a href="https://issues.apache.org/jira/browse/SHIRO-205">SHIRO-205</a>
189     */
190    protected String[] splitChainDefinition(String chainDefinition) {
191        return StringUtils.split(chainDefinition, StringUtils.DEFAULT_DELIMITER_CHAR, '[', ']', true, true);
192    }
193
194    /**
195     * Based on the given filter chain definition token (e.g. 'foo' or 'foo[bar, baz]'), this will return the token
196     * as a name/value pair, removing any brackets as necessary.  Examples:
197     * <table>
198     *     <tr>
199     *         <th>Input</th>
200     *         <th>Result</th>
201     *     </tr>
202     *     <tr>
203     *         <td>{@code foo}</td>
204     *         <td>returned[0] == {@code foo}<br/>returned[1] == {@code null}</td>
205     *     </tr>
206     *     <tr>
207     *         <td>{@code foo[bar, baz]}</td>
208     *         <td>returned[0] == {@code foo}<br/>returned[1] == {@code bar, baz}</td>
209     *     </tr>
210     * </table>
211     * @param token the filter chain definition token
212     * @return A name/value pair representing the filter name and a (possibly null) config value.
213     * @throws ConfigurationException if the token cannot be parsed
214     * @since 1.2
215     * @see <a href="https://issues.apache.org/jira/browse/SHIRO-205">SHIRO-205</a>
216     */
217    protected String[] toNameConfigPair(String token) throws ConfigurationException {
218
219        try {
220            String[] pair = token.split("\\[", 2);
221            String name = StringUtils.clean(pair[0]);
222
223            if (name == null) {
224                throw new IllegalArgumentException("Filter name not found for filter chain definition token: " + token);
225            }
226            String config = null;
227
228            if (pair.length == 2) {
229                config = StringUtils.clean(pair[1]);
230                //if there was an open bracket, it assumed there is a closing bracket, so strip it too:
231                config = config.substring(0, config.length() - 1);
232                config = StringUtils.clean(config);
233
234                //backwards compatibility prior to implementing SHIRO-205:
235                //prior to SHIRO-205 being implemented, it was common for end-users to quote the config inside brackets
236                //if that config required commas.  We need to strip those quotes to get to the interior quoted definition
237                //to ensure any existing quoted definitions still function for end users:
238                if (config != null && config.startsWith("\"") && config.endsWith("\"")) {
239                    String stripped = config.substring(1, config.length() - 1);
240                    stripped = StringUtils.clean(stripped);
241
242                    //if the stripped value does not have any internal quotes, we can assume that the entire config was
243                    //quoted and we can use the stripped value.
244                    if (stripped != null && stripped.indexOf('"') == -1) {
245                        config = stripped;
246                    }
247                    //else:
248                    //the remaining config does have internal quotes, so we need to assume that each comma delimited
249                    //pair might be quoted, in which case we need the leading and trailing quotes that we stripped
250                    //So we ignore the stripped value.
251                }
252            }
253            
254            return new String[]{name, config};
255
256        } catch (Exception e) {
257            String msg = "Unable to parse filter chain definition token: " + token;
258            throw new ConfigurationException(msg, e);
259        }
260    }
261
262    protected void addFilter(String name, Filter filter, boolean init, boolean overwrite) {
263        Filter existing = getFilter(name);
264        if (existing == null || overwrite) {
265            if (filter instanceof Nameable) {
266                ((Nameable) filter).setName(name);
267            }
268            if (init) {
269                initFilter(filter);
270            }
271            this.filters.put(name, filter);
272        }
273    }
274
275    public void addToChain(String chainName, String filterName) {
276        addToChain(chainName, filterName, null);
277    }
278
279    public void addToChain(String chainName, String filterName, String chainSpecificFilterConfig) {
280        if (!StringUtils.hasText(chainName)) {
281            throw new IllegalArgumentException("chainName cannot be null or empty.");
282        }
283        Filter filter = getFilter(filterName);
284        if (filter == null) {
285            throw new IllegalArgumentException("There is no filter with name '" + filterName +
286                    "' to apply to chain [" + chainName + "] in the pool of available Filters.  Ensure a " +
287                    "filter with that name/path has first been registered with the addFilter method(s).");
288        }
289
290        applyChainConfig(chainName, filter, chainSpecificFilterConfig);
291
292        NamedFilterList chain = ensureChain(chainName);
293        chain.add(filter);
294    }
295
296    public void setGlobalFilters(List<String> globalFilterNames) throws ConfigurationException {
297        // validate each filter name
298        if (!CollectionUtils.isEmpty(globalFilterNames)) {
299            for (String filterName : globalFilterNames) {
300                Filter filter = filters.get(filterName);
301                if (filter == null) {
302                    throw new ConfigurationException("There is no filter with name '" + filterName +
303                                                     "' to apply to the global filters in the pool of available Filters.  Ensure a " +
304                                                     "filter with that name/path has first been registered with the addFilter method(s).");
305                }
306                this.globalFilterNames.add(filterName);
307            }
308        }
309    }
310
311    protected void applyChainConfig(String chainName, Filter filter, String chainSpecificFilterConfig) {
312        if (log.isDebugEnabled()) {
313            log.debug("Attempting to apply path [" + chainName + "] to filter [" + filter + "] " +
314                    "with config [" + chainSpecificFilterConfig + "]");
315        }
316        if (filter instanceof PathConfigProcessor) {
317            ((PathConfigProcessor) filter).processPathConfig(chainName, chainSpecificFilterConfig);
318        } else {
319            if (StringUtils.hasText(chainSpecificFilterConfig)) {
320                //they specified a filter configuration, but the Filter doesn't implement PathConfigProcessor
321                //this is an erroneous config:
322                String msg = "chainSpecificFilterConfig was specified, but the underlying " +
323                        "Filter instance is not an 'instanceof' " +
324                        PathConfigProcessor.class.getName() + ".  This is required if the filter is to accept " +
325                        "chain-specific configuration.";
326                throw new ConfigurationException(msg);
327            }
328        }
329    }
330
331    protected NamedFilterList ensureChain(String chainName) {
332        NamedFilterList chain = getChain(chainName);
333        if (chain == null) {
334            chain = new SimpleNamedFilterList(chainName);
335            this.filterChains.put(chainName, chain);
336        }
337        return chain;
338    }
339
340    public NamedFilterList getChain(String chainName) {
341        return this.filterChains.get(chainName);
342    }
343
344    public boolean hasChains() {
345        return !CollectionUtils.isEmpty(this.filterChains);
346    }
347
348    public Set<String> getChainNames() {
349        //noinspection unchecked
350        return this.filterChains != null ? this.filterChains.keySet() : Collections.EMPTY_SET;
351    }
352
353    public FilterChain proxy(FilterChain original, String chainName) {
354        NamedFilterList configured = getChain(chainName);
355        if (configured == null) {
356            String msg = "There is no configured chain under the name/key [" + chainName + "].";
357            throw new IllegalArgumentException(msg);
358        }
359        return configured.proxy(original);
360    }
361
362    /**
363     * Initializes the filter by calling <code>filter.init( {@link #getFilterConfig() getFilterConfig()} );</code>.
364     *
365     * @param filter the filter to initialize with the {@code FilterConfig}.
366     */
367    protected void initFilter(Filter filter) {
368        FilterConfig filterConfig = getFilterConfig();
369        if (filterConfig == null) {
370            throw new IllegalStateException("FilterConfig attribute has not been set.  This must occur before filter " +
371                    "initialization can occur.");
372        }
373        try {
374            filter.init(filterConfig);
375        } catch (ServletException e) {
376            throw new ConfigurationException(e);
377        }
378    }
379
380    protected void addDefaultFilters(boolean init) {
381        for (DefaultFilter defaultFilter : DefaultFilter.values()) {
382            addFilter(defaultFilter.name(), defaultFilter.newInstance(), init, false);
383        }
384    }
385}