/*
 * 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.util;

import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.security.SecureRandom;
import java.security.Security;
import java.util.Queue;
import java.util.Set;
import java.util.concurrent.ConcurrentLinkedQueue;

import org.apache.catalina.LifecycleException;
import org.apache.catalina.LifecycleState;
import org.apache.catalina.SessionIdGenerator;
import org.apache.juli.logging.Log;
import org.apache.juli.logging.LogFactory;
import org.apache.tomcat.util.res.StringManager;

/**
 * Base implementation of {@link SessionIdGenerator} that manages a pool of
 * {@link SecureRandom} instances for generating session identifiers.
 */
public abstract class SessionIdGeneratorBase extends LifecycleBase implements SessionIdGenerator {

    private final Log log = LogFactory.getLog(SessionIdGeneratorBase.class); // must not be static

    private static final StringManager sm = StringManager.getManager("org.apache.catalina.util");

    /**
     * The default SecureRandom algorithm. SHA1PRNG if available, otherwise the
     * platform default.
     */
    public static final String DEFAULT_SECURE_RANDOM_ALGORITHM;

    static {
        /*
         * The default is normally SHA1PRNG. This was chosen because a) it is quick and b) it available by default in
         * all JREs. However, it may not be available in some configurations such as those that use a FIPS certified
         * provider. In those cases, use the platform default.
         */
        Set<String> algorithmNames = Security.getAlgorithms("SecureRandom");
        if (algorithmNames.contains("SHA1PRNG")) {
            DEFAULT_SECURE_RANDOM_ALGORITHM = "SHA1PRNG";
        } else {
            // Empty string - This will trigger the use of the platform default.
            DEFAULT_SECURE_RANDOM_ALGORITHM = "";
            Log log = LogFactory.getLog(SessionIdGeneratorBase.class);
            log.warn(sm.getString("sessionIdGeneratorBase.noSHA1PRNG"));
        }
    }

    /**
     * Queue of random number generator objects to be used when creating session identifiers. If the queue is empty when
     * a random number generator is required, a new random number generator object is created. This is designed this way
     * since random number generators use a sync to make them thread-safe and the sync makes using a single object
     * slow(er).
     */
    private final Queue<SecureRandom> randoms = new ConcurrentLinkedQueue<>();

    private String secureRandomClass = null;

    private String secureRandomAlgorithm = DEFAULT_SECURE_RANDOM_ALGORITHM;

    private String secureRandomProvider = null;

    /**
     * Construct a new SessionIdGeneratorBase instance.
     */
    protected SessionIdGeneratorBase() {
    }

    /** Node identifier when in a cluster. Defaults to the empty string. */
    private String jvmRoute = "";


    /** Number of bytes in a session ID. Defaults to 16. */
    private int sessionIdLength = 16;


    /**
     * Get the class name of the {@link SecureRandom} implementation used to generate session IDs.
     *
     * @return The fully qualified class name. {@code null} indicates that the JRE provided {@link SecureRandom}
     *             implementation will be used
     */
    public String getSecureRandomClass() {
        return secureRandomClass;
    }


    /**
     * Specify a non-default {@link SecureRandom} implementation to use. The implementation must be self-seeding and
     * have a zero-argument constructor. If not specified, an instance of {@link SecureRandom} will be generated.
     *
     * @param secureRandomClass The fully-qualified class name
     */
    public void setSecureRandomClass(String secureRandomClass) {
        this.secureRandomClass = secureRandomClass;
    }


    /**
     * Get the name of the algorithm used to create the {@link SecureRandom} instances which generate new session IDs.
     *
     * @return The name of the algorithm. {@code null} or the empty string means that platform default will be used
     */
    public String getSecureRandomAlgorithm() {
        return secureRandomAlgorithm;
    }


    /**
     * Specify a non-default algorithm to use to create instances of {@link SecureRandom} which are used to generate
     * session IDs. If no algorithm is specified, SHA1PRNG will be used. If SHA1PRNG is not available, the platform
     * default will be used. To use the platform default (which may be SHA1PRNG), specify {@code null} or the empty
     * string. If an invalid algorithm and/or provider is specified the {@link SecureRandom} instances will be created
     * using the defaults for this {@link SessionIdGenerator} implementation. If that fails, the {@link SecureRandom}
     * instances will be created using platform defaults.
     *
     * @param secureRandomAlgorithm The name of the algorithm
     */
    public void setSecureRandomAlgorithm(String secureRandomAlgorithm) {
        this.secureRandomAlgorithm = secureRandomAlgorithm;
    }


    /**
     * Get the name of the provider used to create the {@link SecureRandom} instances which generate new session IDs.
     *
     * @return The name of the provider. {@code null} or the empty string means that platform default will be used
     */
    public String getSecureRandomProvider() {
        return secureRandomProvider;
    }


    /**
     * Specify a non-default provider to use to create instances of {@link SecureRandom} which are used to generate
     * session IDs. If no provider is specified, the platform default is used. To use the platform default specify
     * {@code null} or the empty string. If an invalid algorithm and/or provider is specified the {@link SecureRandom}
     * instances will be created using the defaults for this {@link SessionIdGenerator} implementation. If that fails,
     * the {@link SecureRandom} instances will be created using platform defaults.
     *
     * @param secureRandomProvider The name of the provider
     */
    public void setSecureRandomProvider(String secureRandomProvider) {
        this.secureRandomProvider = secureRandomProvider;
    }


    /**
     * @return the node identifier used when generating session IDs in a clustered environment
     */
    @Override
    public String getJvmRoute() {
        return jvmRoute;
    }


    /**
     * Set the node identifier used when generating session IDs in a clustered environment.
     *
     * @param jvmRoute the node identifier
     */
    @Override
    public void setJvmRoute(String jvmRoute) {
        this.jvmRoute = jvmRoute;
    }


    /**
     * @return the number of bytes used for a session ID
     */
    @Override
    public int getSessionIdLength() {
        return sessionIdLength;
    }


    /**
     * Set the number of bytes used for a session ID.
     *
     * @param sessionIdLength the number of bytes
     */
    @Override
    public void setSessionIdLength(int sessionIdLength) {
        if (sessionIdLength < 2) {
            log.warn(sm.getString("sessionIdGeneratorBase.idLengthTooShort", Integer.toString(sessionIdLength),
                    Integer.toString(this.sessionIdLength)));
            return;
        }
        this.sessionIdLength = sessionIdLength;
    }

    /**
     * Generate a new session ID using the configured jvmRoute.
     *
     * @return the newly generated session ID
     */
    @Override
    public String generateSessionId() {
        return generateSessionId(jvmRoute);
    }


    /**
     * Fill the given byte array with random bytes using a pooled {@link SecureRandom} instance.
     *
     * @param bytes the byte array to fill with random data
     */
    protected void getRandomBytes(byte[] bytes) {

        SecureRandom random = randoms.poll();
        if (random == null) {
            random = createSecureRandom();
        }
        random.nextBytes(bytes);
        randoms.add(random);
    }


    /**
     * Create a new random number generator instance we should use for generating session identifiers.
     */
    private SecureRandom createSecureRandom() {

        SecureRandom result = null;

        long t1 = System.currentTimeMillis();
        if (secureRandomClass != null) {
            try {
                // Construct and seed a new random number generator
                Class<?> clazz = Class.forName(secureRandomClass);
                result = (SecureRandom) clazz.getConstructor().newInstance();
            } catch (Exception e) {
                log.error(sm.getString("sessionIdGeneratorBase.random", secureRandomClass), e);
            }
        }

        boolean error = false;
        if (result == null) {
            // No secureRandomClass or creation failed. Use SecureRandom.
            try {
                if (secureRandomProvider != null && !secureRandomProvider.isEmpty()) {
                    result = SecureRandom.getInstance(secureRandomAlgorithm, secureRandomProvider);
                } else if (secureRandomAlgorithm != null && !secureRandomAlgorithm.isEmpty()) {
                    result = SecureRandom.getInstance(secureRandomAlgorithm);
                }
            } catch (NoSuchAlgorithmException e) {
                error = true;
                log.error(sm.getString("sessionIdGeneratorBase.randomAlgorithm", secureRandomAlgorithm), e);
            } catch (NoSuchProviderException e) {
                error = true;
                log.error(sm.getString("sessionIdGeneratorBase.randomProvider", secureRandomProvider), e);
            }
        }

        // In theory, DEFAULT_SECURE_RANDOM_ALGORITHM should always work but
        // with custom providers that might not be the case.
        if (result == null && error && !DEFAULT_SECURE_RANDOM_ALGORITHM.equals(secureRandomAlgorithm)) {
            // Invalid provider / algorithm - use the default
            try {
                result = SecureRandom.getInstance(DEFAULT_SECURE_RANDOM_ALGORITHM);
            } catch (NoSuchAlgorithmException e) {
                log.error(sm.getString("sessionIdGeneratorBase.randomAlgorithm", DEFAULT_SECURE_RANDOM_ALGORITHM), e);
            }
        }

        if (result == null) {
            // Nothing works - use platform default
            result = new SecureRandom();
        }

        // Force seeding to take place
        result.nextInt();

        long t2 = System.currentTimeMillis();
        if ((t2 - t1) > 100) {
            log.warn(sm.getString("sessionIdGeneratorBase.createRandom", result.getAlgorithm(), Long.valueOf(t2 - t1)));
        }
        return result;
    }


    @Override
    protected void initInternal() throws LifecycleException {
        // NO-OP
    }


    @Override
    protected void startInternal() throws LifecycleException {
        // Ensure SecureRandom has been initialised
        generateSessionId();

        setState(LifecycleState.STARTING);
    }


    @Override
    protected void stopInternal() throws LifecycleException {
        setState(LifecycleState.STOPPING);
        randoms.clear();
    }


    @Override
    protected void destroyInternal() throws LifecycleException {
        // NO-OP
    }
}
