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