* Copyright © 2013-2020, The SeedStack authors <http://seedstack.org>
 * This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/.
package org.seedstack.seed.web.security.internal;

import com.google.common.base.Strings;
import com.google.inject.Key;
import com.google.inject.Scopes;
import com.google.inject.name.Names;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.servlet.Filter;
import javax.servlet.ServletContext;
import org.apache.shiro.config.ConfigurationException;
import org.apache.shiro.guice.web.GuiceShiroFilter;
import org.apache.shiro.guice.web.ShiroWebModule;
import org.apache.shiro.util.StringUtils;
import org.apache.shiro.web.filter.PathMatchingFilter;
import org.apache.shiro.web.filter.authc.LogoutFilter;
import org.apache.shiro.web.filter.authc.SeedBasicHttpAuthenticationFilter;
import org.apache.shiro.web.filter.authc.SeedFormAuthenticationFilter;
import org.seedstack.seed.security.internal.SecurityGuiceConfigurer;
import org.seedstack.seed.web.SecurityFilter;
import org.seedstack.seed.web.security.WebSecurityConfig;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

class WebSecurityModule extends ShiroWebModule {
    private static final Logger LOGGER = LoggerFactory.getLogger(ShiroWebModule.class);
    private static final Map<String, Key<? extends Filter>> DEFAULT_FILTERS = new HashMap<>();

    static {
        // Shiro filters
        DEFAULT_FILTERS.put("anon", ANON);
        DEFAULT_FILTERS.put("logout", LOGOUT);
        DEFAULT_FILTERS.put("noSessionCreation", NO_SESSION_CREATION);
        DEFAULT_FILTERS.put("perms", PERMS);
        DEFAULT_FILTERS.put("port", PORT);
        DEFAULT_FILTERS.put("rest", REST);
        DEFAULT_FILTERS.put("roles", ROLES);
        DEFAULT_FILTERS.put("ssl", SSL);
        DEFAULT_FILTERS.put("user", USER);

        // SeedStack filters (authc and authcBasic prevent potential session fixation vulnerability)
        DEFAULT_FILTERS.put("authc", Key.get(SeedFormAuthenticationFilter.class));
        DEFAULT_FILTERS.put("authcBasic", Key.get(SeedBasicHttpAuthenticationFilter.class));
        DEFAULT_FILTERS.put("cert", Key.get(X509CertificateFilter.class));
        DEFAULT_FILTERS.put("xsrf", Key.get(AntiXsrfFilter.class));

    private final String applicationName;
    private final WebSecurityConfig securityConfig;
    private final Collection<Class<? extends Filter>> customFilters;
    private final SecurityGuiceConfigurer securityGuiceConfigurer;

    WebSecurityModule(ServletContext servletContext, WebSecurityConfig securityConfig,
            Collection<Class<? extends Filter>> customFilters, String applicationName,
            SecurityGuiceConfigurer securityGuiceConfigurer) {
        this.securityConfig = securityConfig;
        this.customFilters = customFilters;
        this.applicationName = applicationName;
        this.securityGuiceConfigurer = securityGuiceConfigurer;

    protected void configureShiroWeb() {
        for (WebSecurityConfig.UrlConfig urlConfig : securityConfig.getUrls()) {
            String pattern = urlConfig.getPattern();
            List<String> filters = urlConfig.getFilters();
            LOGGER.debug("URL pattern {} is secured with filter chain {}", pattern, filters);
            addFilterChain(pattern, getFilterKeys(filters));

        // Bind filters which are not PatchMatchingFilters

        // Bind custom filters not extending PathMatchingFilter as Shiro doesn't do it
        for (Class<? extends Filter> customFilter : customFilters) {
            if (!PathMatchingFilter.class.isAssignableFrom(customFilter)) {

        // General configuration attributes

        // Form configuration attributes
        WebSecurityConfig.FormConfig formConfig = securityConfig.form();

        // Shiro global configuration

        // Shiro filter

    private FilterConfig<?>[] getFilterKeys(List<String> filters) {
        FilterConfig<?>[] filterConfig = new FilterConfig<?>[filters.size()];
        int index = 0;
        for (String filter : filters) {
            String[] nameConfig = toNameConfigPair(filter);
            Key<? extends Filter> currentKey = findKey(nameConfig[0]);
            if (currentKey != null) {
                if (!Strings.isNullOrEmpty(nameConfig[1])) {
                    filterConfig[index] = filterConfig(currentKey, nameConfig[1]);
                } else {
                    filterConfig[index] = filterConfig(currentKey);
            } else {
                        "The filter [" + nameConfig[0] + "] could not be found as a default filter or as a class "
                                + "annotated with SecurityFilter");
        return filterConfig;

    private Key<? extends Filter> findKey(String filterName) {
        Key<? extends Filter> currentKey = null;
        if (DEFAULT_FILTERS.containsKey(filterName)) {
            currentKey = DEFAULT_FILTERS.get(filterName);
        } else {
            for (Class<? extends Filter> filterClass : customFilters) {
                String name = filterClass.getAnnotation(SecurityFilter.class).value();
                if (filterName.equals(name)) {
                    currentKey = Key.get(filterClass);
        return currentKey;

     * This method is copied from the same method in Shiro in class DefaultFilterChainManager.
    private String[] toNameConfigPair(String token) throws ConfigurationException {
        String[] pair = token.split("\\[", 2);
        String name = StringUtils.clean(pair[0]);

        if (name == null) {
            throw new IllegalArgumentException("Filter name not found for filter chain definition token: " + token);
        String config = null;

        if (pair.length == 2) {
            config = StringUtils.clean(pair[1]);
            //if there was an open bracket, it assumed there is a closing bracket, so strip it too:
            config = config.substring(0, config.length() - 1);
            config = StringUtils.clean(config);

            //backwards compatibility prior to implementing SHIRO-205:
            //prior to SHIRO-205 being implemented, it was common for end-users to quote the config inside brackets
            //if that config required commas.  We need to strip those quotes to get to the interior quoted definition
            //to ensure any existing quoted definitions still function for end users:
            if (config != null && config.startsWith("\"") && config.endsWith("\"")) {
                String stripped = config.substring(1, config.length() - 1);
                stripped = StringUtils.clean(stripped);

                //if the stripped value does not have any internal quotes, we can assume that the entire config was
                //quoted and we can use the stripped value.
                if (stripped != null && stripped.indexOf('"') == -1) {
                    config = stripped;
                //the remaining config does have internal quotes, so we need to assume that each comma delimited
                //pair might be quoted, in which case we need the leading and trailing quotes that we stripped
                //So we ignore the stripped value.

        return new String[]{name, config};