/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.apache.catalina.realm;

import java.beans.PropertyChangeListener;
import java.beans.PropertyChangeSupport;
import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.UnsupportedEncodingException;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.security.NoSuchAlgorithmException;
import java.security.Principal;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Locale;

import jakarta.servlet.annotation.ServletSecurity.TransportGuarantee;
import jakarta.servlet.http.HttpServletResponse;

import org.apache.catalina.Container;
import org.apache.catalina.Context;
import org.apache.catalina.CredentialHandler;
import org.apache.catalina.Engine;
import org.apache.catalina.Host;
import org.apache.catalina.LifecycleException;
import org.apache.catalina.LifecycleState;
import org.apache.catalina.Realm;
import org.apache.catalina.Server;
import org.apache.catalina.Service;
import org.apache.catalina.Wrapper;
import org.apache.catalina.connector.Request;
import org.apache.catalina.connector.Response;
import org.apache.catalina.util.LifecycleMBeanBase;
import org.apache.catalina.util.SessionConfig;
import org.apache.catalina.util.ToStringUtil;
import org.apache.juli.logging.Log;
import org.apache.juli.logging.LogFactory;
import org.apache.tomcat.util.IntrospectionUtils;
import org.apache.tomcat.util.buf.B2CConverter;
import org.apache.tomcat.util.buf.HexUtils;
import org.apache.tomcat.util.descriptor.web.SecurityCollection;
import org.apache.tomcat.util.descriptor.web.SecurityConstraint;
import org.apache.tomcat.util.res.StringManager;
import org.apache.tomcat.util.security.ConcurrentMessageDigest;
import org.apache.tomcat.util.security.ConstantTime;
import org.ietf.jgss.GSSContext;
import org.ietf.jgss.GSSCredential;
import org.ietf.jgss.GSSException;
import org.ietf.jgss.GSSName;

/**
 * Simple implementation of <b>Realm</b> that reads an XML file to configure the valid users, passwords, and roles. The
 * file format (and default file location) are identical to those currently supported by Tomcat 3.X.
 */
public abstract class RealmBase extends LifecycleMBeanBase implements Realm {

    private static final Log log = LogFactory.getLog(RealmBase.class);

    /**
     * The character used for delimiting user attribute names.
     * <p>
     * Applies to some of the Realm implementations only.
     */
    protected static final String USER_ATTRIBUTES_DELIMITER = ",";

    /**
     * The character used as wildcard in user attribute lists. Using it means <i>query all available user
     * attributes</i>.
     * <p>
     * Applies to some of the Realm implementations only.
     */
    protected static final String USER_ATTRIBUTES_WILDCARD = "*";

    private static final List<Class<? extends DigestCredentialHandlerBase>> credentialHandlerClasses =
            new ArrayList<>();

    static {
        // Order is important since it determines the search order for a
        // matching handler if only an algorithm is specified when calling
        // main()
        credentialHandlerClasses.add(MessageDigestCredentialHandler.class);
        credentialHandlerClasses.add(SecretKeyCredentialHandler.class);
    }

    // ----------------------------------------------------- Instance Variables


    /**
     * The Container with which this Realm is associated.
     */
    protected Container container = null;


    /**
     * Container log
     */
    protected Log containerLog = null;


    private CredentialHandler credentialHandler;


    /**
     * The string manager for this package.
     */
    protected static final StringManager sm = StringManager.getManager(RealmBase.class);


    /**
     * The property change support for this component.
     */
    protected final PropertyChangeSupport support = new PropertyChangeSupport(this);


    /**
     * Should we validate client certificate chains when they are presented?
     */
    protected boolean validate = true;

    /**
     * The name of the class to use for retrieving usernames from X509 certificates.
     */
    protected String x509UsernameRetrieverClassName;

    /**
     * The object that will extract usernames from X509 client certificates.
     */
    protected X509UsernameRetriever x509UsernameRetriever;

    /**
     * The all role mode.
     */
    protected AllRolesMode allRolesMode = AllRolesMode.STRICT_MODE;


    /**
     * When processing users authenticated via the GSS-API, should any &quot;@...&quot; be stripped from the end of the
     * username?
     */
    protected boolean stripRealmForGss = true;


    private int transportGuaranteeRedirectStatus = HttpServletResponse.SC_FOUND;


    /**
     * The comma separated names of user attributes to additionally query from the realm. These will be provided to the
     * user through the created Principal's <i>attributes</i> map. Support for this feature is optional.
     */
    protected String userAttributes = null;


    /**
     * The list of user attributes to additionally query from the realm. These will be provided to the user through the
     * created Principal's <i>attributes</i> map. Support for this feature is optional.
     */
    protected List<String> userAttributesList = null;


    // ------------------------------------------------------------- Properties

    /**
     * @return The HTTP status code used when the container needs to issue an HTTP redirect to meet the requirements of
     *             a configured transport guarantee.
     */
    public int getTransportGuaranteeRedirectStatus() {
        return transportGuaranteeRedirectStatus;
    }


    /**
     * Set the HTTP status code used when the container needs to issue an HTTP redirect to meet the requirements of a
     * configured transport guarantee.
     *
     * @param transportGuaranteeRedirectStatus The status to use. This value is not validated
     */
    public void setTransportGuaranteeRedirectStatus(int transportGuaranteeRedirectStatus) {
        this.transportGuaranteeRedirectStatus = transportGuaranteeRedirectStatus;
    }


    @Override
    public CredentialHandler getCredentialHandler() {
        return credentialHandler;
    }


    @Override
    public void setCredentialHandler(CredentialHandler credentialHandler) {
        this.credentialHandler = credentialHandler;
    }


    @Override
    public Container getContainer() {
        return container;
    }


    @Override
    public void setContainer(Container container) {

        Container oldContainer = this.container;
        this.container = container;
        support.firePropertyChange("container", oldContainer, this.container);

    }

    /**
     * Return the all roles mode.
     *
     * @return A string representation of the current all roles mode
     */
    public String getAllRolesMode() {
        return allRolesMode.toString();
    }


    /**
     * Set the all roles mode.
     *
     * @param allRolesMode A string representation of the new all roles mode
     */
    public void setAllRolesMode(String allRolesMode) {
        this.allRolesMode = AllRolesMode.toMode(allRolesMode);
    }


    /**
     * Return the "validate certificate chains" flag.
     *
     * @return The value of the validate certificate chains flag
     */
    public boolean getValidate() {
        return validate;
    }


    /**
     * Set the "validate certificate chains" flag.
     *
     * @param validate The new validate certificate chains flag
     */
    public void setValidate(boolean validate) {

        this.validate = validate;

    }

    /**
     * Gets the name of the class that will be used to extract usernames from X509 client certificates.
     *
     * @return The name of the class that will be used to extract usernames from X509 client certificates.
     */
    public String getX509UsernameRetrieverClassName() {
        return x509UsernameRetrieverClassName;
    }

    /**
     * Sets the name of the class that will be used to extract usernames from X509 client certificates. The class must
     * implement X509UsernameRetriever.
     *
     * @param className The name of the class that will be used to extract usernames from X509 client certificates.
     *
     * @see X509UsernameRetriever
     */
    public void setX509UsernameRetrieverClassName(String className) {
        this.x509UsernameRetrieverClassName = className;
    }

    public boolean isStripRealmForGss() {
        return stripRealmForGss;
    }


    public void setStripRealmForGss(boolean stripRealmForGss) {
        this.stripRealmForGss = stripRealmForGss;
    }


    /**
     * @return the comma separated names of user attributes to additionally query from realm
     */
    public String getUserAttributes() {
        return userAttributes;
    }

    /**
     * Set the comma separated names of user attributes to additionally query from the realm. These will be provided to
     * the user through the created Principal's <i>attributes</i> map. In this map, each field value is bound to the
     * field's name, that is, the name of the field serves as the key of the mapping.
     * <p>
     * If set to the wildcard character, or, if the wildcard character is part of the comma separated list, all
     * available attributes - except the <i>password</i> attribute (as specified by <code>userCredCol</code>) - are
     * queried. The wildcard character is defined by constant {@link RealmBase#USER_ATTRIBUTES_WILDCARD}. It defaults to
     * the asterisk (*) character.
     *
     * @param userAttributes the comma separated names of user attributes
     */
    public void setUserAttributes(String userAttributes) {
        this.userAttributes = userAttributes;
    }

    // --------------------------------------------------------- Public Methods

    @Override
    public void addPropertyChangeListener(PropertyChangeListener listener) {
        support.addPropertyChangeListener(listener);
    }


    @Override
    public Principal authenticate(String username) {

        if (username == null) {
            return null;
        }

        if (containerLog.isTraceEnabled()) {
            containerLog.trace(sm.getString("realmBase.authenticateSuccess", username));
        }

        return getPrincipal(username);
    }


    @Override
    public Principal authenticate(String username, String credentials) {
        // No user or no credentials
        // Can't possibly authenticate, don't bother doing anything.
        if (username == null || credentials == null) {
            if (containerLog.isTraceEnabled()) {
                containerLog.trace(sm.getString("realmBase.authenticateFailure", username));
            }
            return null;
        }

        // Look up the user's credentials
        String serverCredentials = getPassword(username);

        if (serverCredentials == null) {
            // User was not found
            // Waste a bit of time as not to reveal that the user does not exist.
            getCredentialHandler().mutate(credentials);

            if (containerLog.isTraceEnabled()) {
                containerLog.trace(sm.getString("realmBase.authenticateFailure", username));
            }
            return null;
        }

        boolean validated = getCredentialHandler().matches(credentials, serverCredentials);

        if (validated) {
            if (containerLog.isTraceEnabled()) {
                containerLog.trace(sm.getString("realmBase.authenticateSuccess", username));
            }
            return getPrincipal(username);
        } else {
            if (containerLog.isTraceEnabled()) {
                containerLog.trace(sm.getString("realmBase.authenticateFailure", username));
            }
            return null;
        }
    }


    @Override
    public Principal authenticate(String username, String clientDigest, String nonce, String nc, String cnonce,
            String qop, String realm, String digestA2, String algorithm) {

        // In digest auth, digests are always lower case
        String digestA1 = getDigest(username, realm, algorithm);
        if (digestA1 == null) {
            return null;
        }
        digestA1 = digestA1.toLowerCase(Locale.ENGLISH);
        String serverDigestValue;
        if (qop == null) {
            serverDigestValue = digestA1 + ":" + nonce + ":" + digestA2;
        } else {
            serverDigestValue = digestA1 + ":" + nonce + ":" + nc + ":" + cnonce + ":" + qop + ":" + digestA2;
        }

        byte[] valueBytes;
        try {
            valueBytes = serverDigestValue.getBytes(getDigestCharset());
        } catch (UnsupportedEncodingException uee) {
            throw new IllegalArgumentException(sm.getString("realmBase.invalidDigestEncoding", getDigestEncoding()),
                    uee);
        }

        String serverDigest = HexUtils.toHexString(ConcurrentMessageDigest.digest(algorithm, valueBytes));

        if (log.isTraceEnabled()) {
            log.trace("Digest : " + clientDigest + " Username:" + username + " ClientDigest:" + clientDigest +
                    " nonce:" + nonce + " nc:" + nc + " cnonce:" + cnonce + " qop:" + qop + " realm:" + realm +
                    "digestA2:" + digestA2 + " Server digest:" + serverDigest);
        }

        if (ConstantTime.equals(serverDigest, clientDigest, true)) {
            return getPrincipal(username);
        }

        return null;
    }


    @Override
    public Principal authenticate(X509Certificate[] certs) {

        if ((certs == null) || (certs.length < 1)) {
            return null;
        }

        // Check the validity of each certificate in the chain
        if (log.isTraceEnabled()) {
            log.trace("Authenticating client certificate chain");
        }
        if (validate) {
            for (X509Certificate cert : certs) {
                if (log.isTraceEnabled()) {
                    log.trace(" Checking validity for '" + cert.getSubjectX500Principal().toString() + "'");
                }
                try {
                    cert.checkValidity();
                } catch (Exception e) {
                    if (log.isDebugEnabled()) {
                        log.debug(sm.getString("realmBase.validity"), e);
                    }
                    return null;
                }
            }
        }

        // Check the existence of the client Principal in our database
        return getPrincipal(certs[0]);
    }


    @Override
    public Principal authenticate(GSSContext gssContext, boolean storeCred) {
        if (gssContext.isEstablished()) {
            GSSName gssName = null;
            try {
                gssName = gssContext.getSrcName();
            } catch (GSSException e) {
                log.warn(sm.getString("realmBase.gssNameFail"), e);
            }

            if (gssName != null) {
                GSSCredential gssCredential = null;
                if (storeCred) {
                    if (gssContext.getCredDelegState()) {
                        try {
                            gssCredential = gssContext.getDelegCred();
                        } catch (GSSException e) {
                            log.warn(sm.getString("realmBase.delegatedCredentialFail", gssName), e);
                        }
                    } else {
                        if (log.isTraceEnabled()) {
                            log.trace(sm.getString("realmBase.credentialNotDelegated", gssName));
                        }
                    }
                }

                return getPrincipal(gssName, gssCredential, gssContext);
            }
        } else {
            log.error(sm.getString("realmBase.gssContextNotEstablished"));
        }

        // Fail in all other cases
        return null;
    }


    @Override
    public Principal authenticate(GSSName gssName, GSSCredential gssCredential) {
        if (gssName == null) {
            return null;
        }

        return getPrincipal(gssName, gssCredential);
    }


    /**
     * {@inheritDoc}
     * <p>
     * The default implementation is NO-OP.
     */
    @Override
    public void backgroundProcess() {
        // NOOP in base class
    }


    @Override
    public SecurityConstraint[] findSecurityConstraints(Request request, Context context) {

        ArrayList<SecurityConstraint> results = null;
        // Are there any defined security constraints?
        SecurityConstraint[] constraints = context.findConstraints();
        if (constraints == null || constraints.length == 0) {
            if (log.isTraceEnabled()) {
                log.trace("  No applicable constraints defined");
            }
            return null;
        }

        // Check each defined security constraint
        String uri = request.getRequestPathMB().toString();
        // Bug47080 - in rare cases this may be null or ""
        // Mapper treats as '/' do the same to prevent NPE
        if (uri == null || uri.isEmpty()) {
            uri = "/";
        }

        String method = request.getMethod();
        int i;
        boolean found = false;
        for (i = 0; i < constraints.length; i++) {
            SecurityCollection[] collections = constraints[i].findCollections();

            // If collection is null, continue to avoid an NPE
            // See Bugzilla 30624
            if (collections == null) {
                continue;
            }

            if (log.isTraceEnabled()) {
                log.trace("  Checking constraint '" + constraints[i] + "' against " + method + " " + uri + " --> " +
                        constraints[i].included(uri, method));
            }

            for (SecurityCollection securityCollection : collections) {
                String[] patterns = securityCollection.findPatterns();

                // If patterns is null, continue to avoid an NPE
                // See Bugzilla 30624
                if (patterns == null) {
                    continue;
                }

                for (String pattern : patterns) {
                    // Exact match including special case for the context root.
                    if (uri.equals(pattern) || pattern.isEmpty() && uri.equals("/")) {
                        found = true;
                        if (securityCollection.findMethod(method)) {
                            if (results == null) {
                                results = new ArrayList<>();
                            }
                            results.add(constraints[i]);
                        }
                    }
                }
            }
        }

        if (found) {
            return resultsToArray(results);
        }

        int longest = -1;

        for (i = 0; i < constraints.length; i++) {
            SecurityCollection[] collection = constraints[i].findCollections();

            // If collection is null, continue to avoid an NPE
            // See Bugzilla 30624
            if (collection == null) {
                continue;
            }

            if (log.isTraceEnabled()) {
                log.trace("  Checking constraint '" + constraints[i] + "' against " + method + " " + uri + " --> " +
                        constraints[i].included(uri, method));
            }

            for (SecurityCollection securityCollection : collection) {
                String[] patterns = securityCollection.findPatterns();

                // If patterns is null, continue to avoid an NPE
                // See Bugzilla 30624
                if (patterns == null) {
                    continue;
                }

                boolean matched = false;
                int length = -1;
                for (String pattern : patterns) {
                    if (pattern.startsWith("/") && pattern.endsWith("/*") && pattern.length() >= longest) {

                        if (pattern.length() == 2) {
                            matched = true;
                            length = pattern.length();
                        } else if (pattern.regionMatches(0, uri, 0, pattern.length() - 1) ||
                                (pattern.length() - 2 == uri.length() &&
                                        pattern.regionMatches(0, uri, 0, pattern.length() - 2))) {
                            matched = true;
                            length = pattern.length();
                        }
                    }
                }
                if (matched) {
                    if (length > longest) {
                        found = false;
                        if (results != null) {
                            results.clear();
                        }
                        longest = length;
                    }
                    if (securityCollection.findMethod(method)) {
                        found = true;
                        if (results == null) {
                            results = new ArrayList<>();
                        }
                        results.add(constraints[i]);
                    }
                }
            }
        }

        if (found) {
            return resultsToArray(results);
        }

        for (i = 0; i < constraints.length; i++) {
            SecurityCollection[] collection = constraints[i].findCollections();

            // If collection is null, continue to avoid an NPE
            // See Bugzilla 30624
            if (collection == null) {
                continue;
            }

            if (log.isTraceEnabled()) {
                log.trace("  Checking constraint '" + constraints[i] + "' against " + method + " " + uri + " --> " +
                        constraints[i].included(uri, method));
            }

            for (int j = 0; j < collection.length; j++) {
                String[] patterns = collection[j].findPatterns();

                // If patterns is null, continue to avoid an NPE
                // See Bugzilla 30624
                if (patterns == null) {
                    continue;
                }

                boolean matched = false;
                for (int k = 0; k < patterns.length && !matched; k++) {
                    String pattern = patterns[k];
                    if (pattern.startsWith("*.")) {
                        int slash = uri.lastIndexOf('/');
                        int dot = uri.lastIndexOf('.');
                        if (slash >= 0 && dot > slash && dot != uri.length() - 1 &&
                                uri.length() - dot == pattern.length() - 1) {
                            if (pattern.regionMatches(1, uri, dot, uri.length() - dot)) {
                                matched = true;
                            }
                        }
                    }
                }
                if (matched) {
                    found = true;
                    if (collection[j].findMethod(method)) {
                        if (results == null) {
                            results = new ArrayList<>();
                        }
                        results.add(constraints[i]);
                    }
                }
            }
        }

        if (found) {
            return resultsToArray(results);
        }

        for (i = 0; i < constraints.length; i++) {
            SecurityCollection[] collection = constraints[i].findCollections();

            // If collection is null, continue to avoid an NPE
            // See Bugzilla 30624
            if (collection == null) {
                continue;
            }

            if (log.isTraceEnabled()) {
                log.trace("  Checking constraint '" + constraints[i] + "' against " + method + " " + uri + " --> " +
                        constraints[i].included(uri, method));
            }

            for (SecurityCollection securityCollection : collection) {
                String[] patterns = securityCollection.findPatterns();

                // If patterns is null, continue to avoid an NPE
                // See Bugzilla 30624
                if (patterns == null) {
                    continue;
                }

                boolean matched = false;
                for (String pattern : patterns) {
                    if (pattern.equals("/")) {
                        matched = true;
                        break;
                    }
                }
                if (matched) {
                    if (results == null) {
                        results = new ArrayList<>();
                    }
                    results.add(constraints[i]);
                }
            }
        }

        if (results == null) {
            // No applicable security constraint was found
            if (log.isTraceEnabled()) {
                log.trace("  No applicable constraint located");
            }
        }
        return resultsToArray(results);
    }

    /**
     * Convert an ArrayList to a SecurityConstraint [].
     */
    private SecurityConstraint[] resultsToArray(ArrayList<SecurityConstraint> results) {
        if (results == null || results.isEmpty()) {
            return null;
        }
        return results.toArray(new SecurityConstraint[0]);
    }


    @Override
    public boolean hasResourcePermission(Request request, Response response, SecurityConstraint[] constraints,
            Context context) throws IOException {

        if (constraints == null || constraints.length == 0) {
            return true;
        }

        // Which user principal have we already authenticated?
        Principal principal = request.getPrincipal();
        boolean status = false;
        boolean denyfromall = false;
        for (SecurityConstraint constraint : constraints) {
            String[] roles;
            if (constraint.getAllRoles()) {
                // * means all roles defined in web.xml
                roles = request.getContext().findSecurityRoles();
            } else {
                roles = constraint.findAuthRoles();
            }

            if (roles == null) {
                roles = new String[0];
            }

            if (log.isTraceEnabled()) {
                log.trace("  Checking roles " + principal);
            }

            if (constraint.getAuthenticatedUsers() && principal != null) {
                if (log.isTraceEnabled()) {
                    log.trace("Passing all authenticated users");
                }
                status = true;
            } else if (roles.length == 0 && !constraint.getAllRoles() && !constraint.getAuthenticatedUsers()) {
                if (constraint.getAuthConstraint()) {
                    if (log.isTraceEnabled()) {
                        log.trace("No roles");
                    }
                    status = false; // No listed roles means no access at all
                    denyfromall = true;
                    break;
                }

                if (log.isTraceEnabled()) {
                    log.trace("Passing all access");
                }
                status = true;
            } else if (principal == null) {
                if (log.isTraceEnabled()) {
                    log.trace("  No user authenticated, cannot grant access");
                }
            } else {
                for (String role : roles) {
                    if (hasRole(request.getWrapper(), principal, role)) {
                        status = true;
                        if (log.isTraceEnabled()) {
                            log.trace("Role found:  " + role);
                        }
                    } else if (log.isTraceEnabled()) {
                        log.trace("No role found:  " + role);
                    }
                }
            }
        }

        if (!denyfromall && allRolesMode != AllRolesMode.STRICT_MODE && !status && principal != null) {
            if (log.isTraceEnabled()) {
                log.trace("Checking for all roles mode: " + allRolesMode);
            }
            // Check for an all roles(role-name="*")
            for (SecurityConstraint constraint : constraints) {
                // If the all roles mode exists, sets
                if (constraint.getAllRoles()) {
                    if (allRolesMode == AllRolesMode.AUTH_ONLY_MODE) {
                        if (log.isTraceEnabled()) {
                            log.trace("Granting access for role-name=*, auth-only");
                        }
                        status = true;
                        break;
                    }

                    // For AllRolesMode.STRICT_AUTH_ONLY_MODE there must be zero roles
                    String[] roles = request.getContext().findSecurityRoles();
                    if (roles == null) {
                        roles = new String[0];
                    }
                    if (roles.length == 0 && allRolesMode == AllRolesMode.STRICT_AUTH_ONLY_MODE) {
                        if (log.isTraceEnabled()) {
                            log.trace("Granting access for role-name=*, strict auth-only");
                        }
                        status = true;
                        break;
                    }
                }
            }
        }

        // Return a "Forbidden" message denying access to this resource
        if (!status) {
            response.sendError(HttpServletResponse.SC_FORBIDDEN, sm.getString("realmBase.forbidden"));
        }
        return status;

    }


    /**
     * {@inheritDoc}
     * <p>
     * This method or {@link #hasRoleInternal(Principal, String)} can be overridden by Realm implementations, but the
     * default is adequate when an instance of <code>GenericPrincipal</code> is used to represent authenticated
     * Principals from this Realm.
     */
    @Override
    public boolean hasRole(Wrapper wrapper, Principal principal, String role) {
        // Check for a role alias
        if (wrapper != null) {
            String realRole = wrapper.findSecurityReference(role);
            if (realRole != null) {
                role = realRole;
            }
        }

        // Should be overridden in JAASRealm - to avoid pretty inefficient conversions
        if (principal == null || role == null) {
            return false;
        }

        boolean result = hasRoleInternal(principal, role);

        if (log.isTraceEnabled()) {
            String name = principal.getName();
            if (result) {
                log.trace(sm.getString("realmBase.hasRoleSuccess", name, role));
            } else {
                log.trace(sm.getString("realmBase.hasRoleFailure", name, role));
            }
        }

        return result;
    }


    /**
     * Parse the specified delimiter separated attribute names and return a list of that names or <code>null</code>, if
     * no attributes have been specified.
     * <p>
     * If a wildcard character is found, return a list consisting of a single wildcard character only.
     *
     * @param userAttributes comma separated names of attributes to parse
     *
     * @return a list containing the parsed attribute names or <code>null</code>, if no attributes have been specified
     */
    protected List<String> parseUserAttributes(String userAttributes) {
        if (userAttributes == null) {
            return null;
        }
        List<String> attrs = new ArrayList<>();
        for (String name : userAttributes.split(USER_ATTRIBUTES_DELIMITER)) {
            name = name.trim();
            if (name.isEmpty()) {
                continue;
            }
            if (name.equals(USER_ATTRIBUTES_WILDCARD)) {
                return Collections.singletonList(USER_ATTRIBUTES_WILDCARD);
            }
            if (attrs.contains(name)) {
                // skip duplicates
                continue;
            }
            attrs.add(name);
        }
        return !attrs.isEmpty() ? attrs : null;
    }


    /**
     * Check if the specified Principal has the specified security role, within the context of this Realm. This method
     * or {@link #hasRoleInternal(Principal, String)} can be overridden by Realm implementations, but the default is
     * adequate when an instance of <code>GenericPrincipal</code> is used to represent authenticated Principals from
     * this Realm.
     *
     * @param principal Principal for whom the role is to be checked
     * @param role      Security role to be checked
     *
     * @return <code>true</code> if the specified Principal has the specified security role, within the context of this
     *             Realm; otherwise return <code>false</code>.
     */
    protected boolean hasRoleInternal(Principal principal, String role) {
        // Should be overridden in JAASRealm - to avoid pretty inefficient conversions
        if (!(principal instanceof GenericPrincipal gp)) {
            return false;
        }

        return gp.hasRole(role);
    }


    @Override
    public boolean hasUserDataPermission(Request request, Response response, SecurityConstraint[] constraints)
            throws IOException {

        // Is there a relevant user data constraint?
        if (constraints == null || constraints.length == 0) {
            if (log.isTraceEnabled()) {
                log.trace("  No applicable security constraint defined");
            }
            return true;
        }
        for (SecurityConstraint constraint : constraints) {
            String userConstraint = constraint.getUserConstraint();
            if (userConstraint == null) {
                if (log.isTraceEnabled()) {
                    log.trace("  No applicable user data constraint defined");
                }
                return true;
            }
            if (userConstraint.equals(TransportGuarantee.NONE.name())) {
                if (log.isTraceEnabled()) {
                    log.trace("  User data constraint has no restrictions");
                }
                return true;
            }

        }
        // Validate the request against the user data constraint
        if (request.getRequest().isSecure()) {
            if (log.isTraceEnabled()) {
                log.trace("  User data constraint already satisfied");
            }
            return true;
        }
        // Initialize variables we need to determine the appropriate action
        int redirectPort = request.getConnector().getRedirectPortWithOffset();

        // Is redirecting disabled?
        if (redirectPort <= 0) {
            if (log.isTraceEnabled()) {
                log.trace("  SSL redirect is disabled");
            }
            response.sendError(HttpServletResponse.SC_FORBIDDEN, request.getRequestURI());
            return false;
        }

        // Redirect to the corresponding SSL port
        StringBuilder file = new StringBuilder();
        String protocol = "https";
        String host = request.getServerName();
        // Protocol
        file.append(protocol).append("://").append(host);
        // Host with port
        if (redirectPort != 443) {
            file.append(':').append(redirectPort);
        }
        // URI
        file.append(request.getRequestURI());
        String requestedSessionId = request.getRequestedSessionId();
        if ((requestedSessionId != null) && request.isRequestedSessionIdFromURL()) {
            file.append(';');
            file.append(SessionConfig.getSessionUriParamName(request.getContext()));
            file.append('=');
            file.append(requestedSessionId);
        }
        String queryString = request.getQueryString();
        if (queryString != null) {
            file.append('?');
            file.append(queryString);
        }
        if (log.isTraceEnabled()) {
            log.trace("  Redirecting to " + file.toString());
        }
        response.sendRedirect(file.toString(), transportGuaranteeRedirectStatus);
        return false;

    }


    @Override
    public void removePropertyChangeListener(PropertyChangeListener listener) {
        support.removePropertyChangeListener(listener);
    }


    @Override
    protected void initInternal() throws LifecycleException {

        super.initInternal();

        // We want logger as soon as possible
        if (container != null) {
            this.containerLog = container.getLogger();
        }

        x509UsernameRetriever = createUsernameRetriever(x509UsernameRetrieverClassName);
    }


    /**
     * Prepare for the beginning of active use of the public methods of this component and implement the requirements of
     * {@link org.apache.catalina.util.LifecycleBase#startInternal()}.
     *
     * @exception LifecycleException if this component detects a fatal error that prevents this component from being
     *                                   used
     */
    @Override
    protected void startInternal() throws LifecycleException {
        if (credentialHandler == null) {
            credentialHandler = new MessageDigestCredentialHandler();
        }
        if (userAttributes != null) {
            userAttributesList = parseUserAttributes(userAttributes);
        }
        setState(LifecycleState.STARTING);
    }


    /**
     * Gracefully terminate the active use of the public methods of this component and implement the requirements of
     * {@link org.apache.catalina.util.LifecycleBase#stopInternal()}.
     *
     * @exception LifecycleException if this component detects a fatal error that needs to be reported
     */
    @Override
    protected void stopInternal() throws LifecycleException {
        setState(LifecycleState.STOPPING);
    }


    @Override
    public String toString() {
        return ToStringUtil.toString(this);
    }


    // ------------------------------------------------------ Protected Methods

    protected boolean hasMessageDigest(String algorithm) {
        CredentialHandler ch = credentialHandler;
        if (ch instanceof MessageDigestCredentialHandler) {
            String realmAlgorithm = ((MessageDigestCredentialHandler) ch).getAlgorithm();
            if (realmAlgorithm != null) {
                if (realmAlgorithm.equals(algorithm)) {
                    return true;
                } else {
                    log.debug(sm.getString("relamBase.digestMismatch", algorithm, realmAlgorithm));
                }
            }
        }
        return false;
    }


    /**
     * Return the digest associated with given principal's username.
     *
     * @param username  The username
     * @param realmName The realm name
     * @param algorithm The name of the message digest algorithm to use
     *
     * @return the digest for the specified user
     */
    protected String getDigest(String username, String realmName, String algorithm) {
        String password = getPassword(username);

        // Short-cut null password case
        if (password == null) {
            return null;
        }

        if (hasMessageDigest(algorithm)) {
            // Use pre-generated digest
            return password;
        }

        String digestValue = username + ":" + realmName + ":" + password;

        byte[] valueBytes;
        try {
            valueBytes = digestValue.getBytes(getDigestCharset());
        } catch (UnsupportedEncodingException uee) {
            throw new IllegalArgumentException(sm.getString("realmBase.invalidDigestEncoding", getDigestEncoding()),
                    uee);
        }

        return HexUtils.toHexString(ConcurrentMessageDigest.digest(algorithm, valueBytes));
    }


    private String getDigestEncoding() {
        CredentialHandler ch = credentialHandler;
        if (ch instanceof MessageDigestCredentialHandler) {
            return ((MessageDigestCredentialHandler) ch).getEncoding();
        }
        return null;
    }


    private Charset getDigestCharset() throws UnsupportedEncodingException {
        String charset = getDigestEncoding();
        if (charset == null) {
            return StandardCharsets.ISO_8859_1;
        } else {
            return B2CConverter.getCharset(charset);
        }
    }


    /**
     * Get the password for the specified user.
     *
     * @param username The username
     *
     * @return the password associated with the given principal's username.
     */
    protected abstract String getPassword(String username);


    /**
     * Get the principal associated with the specified certificate.
     *
     * @param usercert The user certificate
     *
     * @return the Principal associated with the given certificate.
     */
    protected Principal getPrincipal(X509Certificate usercert) {
        String username = x509UsernameRetriever.getUsername(usercert);

        if (log.isTraceEnabled()) {
            log.trace(sm.getString("realmBase.gotX509Username", username));
        }

        return getPrincipal(username);
    }


    /**
     * Get the principal associated with the specified user.
     *
     * @param username The username
     *
     * @return the Principal associated with the given username.
     */
    protected abstract Principal getPrincipal(String username);


    /**
     * Get the principal associated with the specified {@link GSSName}.
     *
     * @param gssName       The GSS name
     * @param gssCredential the GSS credential of the principal
     * @param gssContext    the established GSS context
     *
     * @return the principal associated with the given username.
     */
    protected Principal getPrincipal(GSSName gssName, GSSCredential gssCredential, GSSContext gssContext) {
        return getPrincipal(gssName, gssCredential);
    }


    /**
     * Get the principal associated with the specified {@link GSSName}.
     *
     * @param gssName       The GSS name
     * @param gssCredential the GSS credential of the principal
     *
     * @return the principal associated with the given username.
     */
    protected Principal getPrincipal(GSSName gssName, GSSCredential gssCredential) {
        String name = gssName.toString();

        if (isStripRealmForGss()) {
            int i = name.indexOf('@');
            if (i > 0) {
                // Zero so we don't leave a zero length name
                name = name.substring(0, i);
            }
        }

        Principal p = getPrincipal(name);

        if (p instanceof GenericPrincipal) {
            ((GenericPrincipal) p).setGssCredential(gssCredential);
        }

        return p;
    }


    /**
     * Return the Server object that is the ultimate parent for the container with which this Realm is associated. If
     * the server cannot be found (e.g. because the container hierarchy is not complete), <code>null</code> is returned.
     *
     * @return the Server associated with the realm
     */
    protected Server getServer() {
        Container c = container;
        if (c instanceof Context) {
            c = c.getParent();
        }
        if (c instanceof Host) {
            c = c.getParent();
        }
        if (c instanceof Engine) {
            Service s = ((Engine) c).getService();
            if (s != null) {
                return s.getServer();
            }
        }
        return null;
    }


    // --------------------------------------------------------- Static Methods

    /**
     * Generate a stored credential string for the given password and associated parameters.
     * <p>
     * The following parameters are supported:
     * </p>
     * <ul>
     * <li><b>-a</b> - The algorithm to use to generate the stored credential. If not specified a default of SHA-512
     * will be used.</li>
     * <li><b>-e</b> - The encoding to use for any byte to/from character conversion that may be necessary. If not
     * specified, the system encoding ({@link Charset#defaultCharset()}) will be used.</li>
     * <li><b>-i</b> - The number of iterations to use when generating the stored credential. If not specified, the
     * default for the CredentialHandler will be used.</li>
     * <li><b>-s</b> - The length (in bytes) of salt to generate and store as part of the credential. If not specified,
     * the default for the CredentialHandler will be used.</li>
     * <li><b>-k</b> - The length (in bits) of the key(s), if any, created while generating the credential. If not
     * specified, the default for the CredentialHandler will be used.</li>
     * <li><b>-h</b> - The fully qualified class name of the CredentialHandler to use. If not specified, the built-in
     * handlers will be tested in turn and the first one to accept the specified algorithm will be used.</li>
     * <li><b>-f</b> - The name of the file that contains passwords to encode. Each line in the file should contain only
     * one password. Using this option ignores other password input.</li>
     * </ul>
     * <p>
     * This generation process currently supports the following CredentialHandlers, the correct one being selected based
     * on the algorithm specified:
     * </p>
     * <ul>
     * <li>{@link MessageDigestCredentialHandler}</li>
     * <li>{@link SecretKeyCredentialHandler}</li>
     * </ul>
     *
     * @param args The parameters passed on the command line
     *
     * @throws IOException If an error occurs reading the password file
     */
    public static void main(String[] args) throws IOException {

        // Use negative values since null is not an option to indicate 'not set'
        int saltLength = -1;
        int iterations = -1;
        int keyLength = -1;
        // Default
        String encoding = Charset.defaultCharset().name();
        // Default values for these depend on whether either of them are set on
        // the command line
        String algorithm = null;
        String handlerClassName = null;
        // File name to read password(s) from
        String passwordFile = null;

        if (args.length == 0) {
            usage();
            return;
        }

        int argIndex = 0;
        // Boolean to check and see if we've reached the -- option
        boolean endOfList = false;

        // Note: Reducing args.length requirement to argIndex+1 so that -f works and ignores
        // trailing words
        while (args.length > argIndex + 1 && args[argIndex].length() == 2 && args[argIndex].charAt(0) == '-' &&
                !endOfList) {
            switch (args[argIndex].charAt(1)) {
                case 'a': {
                    algorithm = args[argIndex + 1];
                    break;
                }
                case 'e': {
                    encoding = args[argIndex + 1];
                    break;
                }
                case 'i': {
                    iterations = Integer.parseInt(args[argIndex + 1]);
                    break;
                }
                case 's': {
                    saltLength = Integer.parseInt(args[argIndex + 1]);
                    break;
                }
                case 'k': {
                    keyLength = Integer.parseInt(args[argIndex + 1]);
                    break;
                }
                case 'h': {
                    handlerClassName = args[argIndex + 1];
                    break;
                }
                case 'f': {
                    passwordFile = args[argIndex + 1];
                    break;
                }
                case '-': {
                    // When encountering -- option don't parse anything else as an option
                    endOfList = true;
                    // The -- opt doesn't take an argument, decrement the argIndex so that it parses
                    // all remaining args
                    argIndex--;
                    break;
                }
                default: {
                    usage();
                    return;
                }
            }
            argIndex += 2;
        }

        // Determine defaults for -a and -h. The rules are more complex to
        // express than the implementation:
        // - if neither -a nor -h is set, use SHA-512 and
        // MessageDigestCredentialHandler
        // - if only -a is set the built-in handlers will be searched in order
        // (MessageDigestCredentialHandler, SecretKeyCredentialHandler) and
        // the first handler that supports the algorithm will be used
        // - if only -h is set no default will be used for -a. The handler may
        // or may nor support -a and may or may not supply a sensible default
        if (algorithm == null && handlerClassName == null) {
            algorithm = "SHA-512";
        }

        CredentialHandler handler = null;

        if (handlerClassName == null) {
            for (Class<? extends DigestCredentialHandlerBase> clazz : credentialHandlerClasses) {
                try {
                    handler = clazz.getConstructor().newInstance();
                    if (IntrospectionUtils.setProperty(handler, "algorithm", algorithm)) {
                        break;
                    }
                } catch (ReflectiveOperationException e) {
                    // This isn't good.
                    throw new RuntimeException(e);
                }
            }
        } else {
            try {
                Class<?> clazz = Class.forName(handlerClassName);
                handler = (DigestCredentialHandlerBase) clazz.getConstructor().newInstance();
                IntrospectionUtils.setProperty(handler, "algorithm", algorithm);
            } catch (ReflectiveOperationException e) {
                throw new RuntimeException(e);
            }
        }

        if (handler == null) {
            throw new RuntimeException(new NoSuchAlgorithmException(algorithm));
        }

        IntrospectionUtils.setProperty(handler, "encoding", encoding);
        if (iterations > 0) {
            IntrospectionUtils.setProperty(handler, "iterations", Integer.toString(iterations));
        }
        if (saltLength > -1) {
            IntrospectionUtils.setProperty(handler, "saltLength", Integer.toString(saltLength));
        }
        if (keyLength > 0) {
            IntrospectionUtils.setProperty(handler, "keyLength", Integer.toString(keyLength));
        }
        if (passwordFile != null) {
            // If the file name is used, then don't parse the trailing arguments
            argIndex = args.length;

            // Special case, allow for - filename to refer to stdin
            try (BufferedReader br = passwordFile.equals("-") ? new BufferedReader(new InputStreamReader(System.in)) :
                    new BufferedReader(new FileReader(passwordFile))) {
                String line;
                while ((line = br.readLine()) != null) {
                    // Mutate each line in the file, or stdin
                    mutateCredential(line, handler);
                }
            } catch (Exception e) {
                // A FileNotFound is the likely exception here and self-explanatory. Softly
                // reporting it and exit 1 so that you can tell it failed from the command line.
                if (e instanceof java.io.FileNotFoundException) {
                    System.err.println("cannot stat '" + passwordFile + "': No such file or directory");
                    // Not sure if using an exit here is OK, but I wanted to return a code that
                    // showed failure.
                    System.exit(1);
                } else {
                    throw e;
                }
            }
        }
        for (; argIndex < args.length; argIndex++) {
            mutateCredential(args[argIndex], handler);
        }
    }

    private static void mutateCredential(String credential, CredentialHandler handler) {
        System.out.print(credential + ":");
        System.out.println(handler.mutate(credential));
    }

    private static void usage() {
        System.out.println("Usage: RealmBase [-a <algorithm>] [-e <encoding>]" +
                " [-i <iterations>] [-s <salt-length>] [-k <key-length>]" +
                " [-h <handler-class-name>] | <XX credentials>");
    }


    // -------------------- JMX and Registration --------------------

    @Override
    public String getObjectNameKeyProperties() {
        return "type=Realm" + getRealmSuffix() + container.getMBeanKeyProperties();
    }

    @Override
    public String getDomainInternal() {
        return container.getDomain();
    }

    protected String realmPath = "/realm0";

    public String getRealmPath() {
        return realmPath;
    }

    public void setRealmPath(String theRealmPath) {
        realmPath = theRealmPath;
    }

    protected String getRealmSuffix() {
        return ",realmPath=" + getRealmPath();
    }


    protected static class AllRolesMode {

        private final String name;
        /**
         * Use the strict servlet spec interpretation which requires that the user have one of the
         * web-app/security-role/role-name
         */
        public static final AllRolesMode STRICT_MODE = new AllRolesMode("strict");
        /** Allow any authenticated user */
        public static final AllRolesMode AUTH_ONLY_MODE = new AllRolesMode("authOnly");
        /**
         * Allow any authenticated user only if there are no web-app/security-roles
         */
        public static final AllRolesMode STRICT_AUTH_ONLY_MODE = new AllRolesMode("strictAuthOnly");

        static AllRolesMode toMode(String name) {
            AllRolesMode mode;
            if (name.equalsIgnoreCase(STRICT_MODE.name)) {
                mode = STRICT_MODE;
            } else if (name.equalsIgnoreCase(AUTH_ONLY_MODE.name)) {
                mode = AUTH_ONLY_MODE;
            } else if (name.equalsIgnoreCase(STRICT_AUTH_ONLY_MODE.name)) {
                mode = STRICT_AUTH_ONLY_MODE;
            } else {
                throw new IllegalStateException(sm.getString("realmBase.unknownAllRolesMode", name));
            }
            return mode;
        }

        private AllRolesMode(String name) {
            this.name = name;
        }

        @Override
        public boolean equals(Object o) {
            boolean equals = false;
            if (o instanceof AllRolesMode mode) {
                equals = name.equals(mode.name);
            }
            return equals;
        }

        @Override
        public int hashCode() {
            return name.hashCode();
        }

        @Override
        public String toString() {
            return name;
        }
    }

    private static X509UsernameRetriever createUsernameRetriever(String className) throws LifecycleException {
        if (null == className || className.trim().isEmpty()) {
            return new X509SubjectDnRetriever();
        }

        try {
            @SuppressWarnings("unchecked")
            Class<? extends X509UsernameRetriever> clazz =
                    (Class<? extends X509UsernameRetriever>) Class.forName(className);
            return clazz.getConstructor().newInstance();
        } catch (ReflectiveOperationException e) {
            throw new LifecycleException(sm.getString("realmBase.createUsernameRetriever.newInstance", className), e);
        } catch (ClassCastException e) {
            throw new LifecycleException(
                    sm.getString("realmBase.createUsernameRetriever.ClassCastException", className), e);
        }
    }
}
