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