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.mgt;
020    
021    import org.apache.shiro.config.ConfigurationException;
022    import org.apache.shiro.util.CollectionUtils;
023    import org.apache.shiro.util.Nameable;
024    import org.apache.shiro.util.StringUtils;
025    import org.apache.shiro.web.filter.PathConfigProcessor;
026    import org.slf4j.Logger;
027    import org.slf4j.LoggerFactory;
028    
029    import javax.servlet.Filter;
030    import javax.servlet.FilterChain;
031    import javax.servlet.FilterConfig;
032    import javax.servlet.ServletException;
033    import java.util.Collections;
034    import java.util.LinkedHashMap;
035    import java.util.Map;
036    import 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     */
047    public 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    }