/*
 * Copyright 1999-2010 University of Chicago
 *
 * Licensed 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.globus.gsi;

import org.globus.gsi.util.CertificateUtil;

import org.apache.commons.logging.LogFactory;

import org.apache.commons.logging.Log;


import java.io.BufferedReader;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
import java.io.Reader;
import java.util.HashMap;
import java.util.Map;
import java.util.StringTokenizer;
import java.util.Vector;


import java.util.regex.Pattern;

import javax.security.auth.x500.X500Principal;


/**
 * Signing policy BCNF grammar as implemented here: (based on C implementation)
 * <p/>
 * eacl ::=  {eacl_entry} eacl_entry ::= {access_identity} pos_rights
 * {restriction} {pos_rights {restriction}} | {access_identity} neg_rights
 * access_identity ::= access_identity_type  def_authority  value  \n
 * access_identity_type ::= "access_id_HOST"  | "access_id_USER"  |
 * "access_id_GROUP" | "access_id_CA"    | "access_id_APPLICATION" |
 * "access_id_ANYBODY" pos_rights ::=  "pos_rights" def_authority value
 * {"pos_rights" def_authority value} neg_rights ::= "neg_rights" def_authority
 * value {"neg_rights" def_authority value} restriction ::= condition_type
 * def_authority  value  \n condition_type ::= alphanumeric_string def_authority
 * ::= alphanumeric_string value ::= alphanumeric_string
 * <p/>
 * This class take a signing policy file as input and parses it to extract the
 * policy that is enforced. Only the following policy is enforced: access_id_CA
 * with defining authority as X509 with CA DN as value. Any positive rights
 * following it with globus as defining authority and value CA:sign. Lastly,
 * restriction "cond_subjects" with globus as defining authority and the DNs the
 * CA is authorized to sign. restrictions are assumed to start with cond_. Order
 * of rights matter, so the first occurance of CA:Sign with allowedDNs is used
 * and rest of the policy is ignored.
 * <p/>
 * For a given signing policy file, only policy with the particular CA's DN is
 * parsed.
 * <p/>
 * subject names may include the following wildcard characters: *    Matches
 * zero or any number of characters. ?    Matches any single character.
 * <p/>
 * All subject names should be in Globus format, with slashes and should NOT be
 * revered.
 * <p/>
 * The allowed DN patterns are returned as a vector of java.util.regexp.Pattern.
 * The BCNF grammar that uses wildcard (*) and single character (?) are replaced
 * with the regexp grammar needed by the Pattern class.
 */
// COMMENT: BCB: moved over from crux-security-core: different parse-function name, stricter check for parameters
public class SigningPolicyParser {

    public static final String ACCESS_ID_PREFIX = "access_id_";
    public static final String ACCESS_ID_CA = "access_id_CA";

    public static final String DEF_AUTH_X509 = "X509";
    public static final String DEF_AUTH_GLOBUS = "globus";

    public static final String POS_RIGHTS = "pos_rights";
    public static final String NEG_RIGHTS = "neg_rights";

    public static final String CONDITION_PREFIX = "cond_";
    public static final String CONDITION_SUBJECT = "cond_subjects";

    public static final String VALUE_CA_SIGN = "CA:sign";

    public static final String SINGLE_CHAR = "?";
    public static final String WILDCARD = "*";

    public static final String SINGLE_PATTERN = "[\\p{Print}\\p{Blank}]";
    public static final String WILDCARD_PATTERN = SINGLE_PATTERN + "*";
    private static final int MIN_TOKENS_PER_LINE = 3;

    static final String[] ALLOWED_LINE_START = new String[]{ACCESS_ID_PREFIX, POS_RIGHTS, NEG_RIGHTS, CONDITION_PREFIX};

    private Log logger = LogFactory.getLog(SigningPolicyParser.class.getName());

    /**
     * Parses the file to extract signing policy defined for CA with the
     * specified DN. If the policy file does not exist, a SigningPolicy object
     * with only CA DN is created. If policy path exists, but no relevant policy
     * exisit, SigningPolicy object with CA DN and file path is created.
     *
     * @param fileName Name of the signing policy file
     * @return SigningPolicy object that contains the information. If no policy
     *         is found, SigningPolicy object with only the CA DN is returned.
     * @throws org.globus.gsi.SigningPolicyException
     *                               Any errors with parsing the signing policy file.
     * @throws FileNotFoundException If the signing policy file does not exist.
     */
    public Map<X500Principal, SigningPolicy> parse(String fileName)
            throws FileNotFoundException, SigningPolicyException {

        if ((fileName == null) || (fileName.trim().isEmpty())) {
            throw new IllegalArgumentException();
        }

        logger.debug("Signing policy file name " + fileName);

        FileReader fileReader = null;

        try {
            fileReader = new FileReader(fileName);
            return parse(fileReader);
        } catch (Exception e) {
            throw new SigningPolicyException(e);
        } finally {
            if (fileReader != null) {
                try {
                    fileReader.close();
                } catch (Exception exp) {
                    logger.debug("Error closing file reader", exp);
                }
            }
        }


    }

    /**
     * Parses input stream to extract signing policy defined for CA with the
     * specified DN.
     *
     * @param reader Reader to any input stream to get the signing policy
     *               information.
     * @return signing policy map defined by the signing policy file
     * @throws org.globus.gsi.SigningPolicyException
     *          Any errors with parsing the signing policy.
     */
    public Map<X500Principal, SigningPolicy> parse(Reader reader)
            throws SigningPolicyException {

        Map<X500Principal, SigningPolicy> policies = new HashMap<X500Principal, SigningPolicy>();

        BufferedReader bufferedReader = new BufferedReader(reader);
        try {
            String line;

            while ((line = bufferedReader.readLine()) != null) {

                line = line.trim();

                // read line until some line that needs to be parsed.
                if (!isValidLine(line)) {
                    continue;
                }

                logger.debug("Line to parse: " + line);

                String caDN = null;
                if (line.startsWith(ACCESS_ID_PREFIX)) {

                    logger.debug("Check if it is CA and get the DN " + line);
                    caDN = getCaDN(line, caDN);
                    boolean usefulEntry = true;
                    Boolean posNegRights = null;
                    // check for neg or pos rights with restrictions
                    checkRights(policies, bufferedReader, caDN, usefulEntry, posNegRights);
                }
                // JGLOBUS-94
            }
        } catch (IOException exp) {
            throw new SigningPolicyException("", exp);
        } finally {
            cleanupReaders(reader, bufferedReader);
        }
        return policies;
    }

    private void checkRights(Map<X500Principal, SigningPolicy> policies, BufferedReader bufferedReader, String caDN,
                             boolean usefulEntry, Boolean posNegRights) throws IOException, SigningPolicyException {
        boolean tmpUsefulEntry = usefulEntry;
        Boolean tmpPosNegRights = posNegRights;
        String line = bufferedReader.readLine();
        while (line != null) {

            if (!isValidLine(line)) {
                line = bufferedReader.readLine();
                continue;
            }

            line = line.trim();
            logger.debug("Line is " + line);

            if (line.startsWith(POS_RIGHTS)) {
                validatePositiveRights(tmpPosNegRights);
                if (tmpUsefulEntry) {
                    tmpUsefulEntry = isUsefulEntry(line);
                }
                tmpPosNegRights = Boolean.TRUE;
            } else if (line.startsWith(NEG_RIGHTS)) {
                tmpPosNegRights = handleNegativeRights(tmpPosNegRights);

            } else if (line.startsWith(CONDITION_PREFIX)) {
                if (handleConditionalLine(policies, line, caDN, tmpUsefulEntry, tmpPosNegRights)) {
                    break;
                }
            } else {
                String err = "invalidLine";
                // no valid start with
                // String err = i18n.getMessage("invalidLine", line);
                throw new SigningPolicyException(err + line);
            }
            line = bufferedReader.readLine();
        }
    }

    private boolean handleConditionalLine(Map<X500Principal, SigningPolicy> policies, String line, String caDN, boolean usefulEntry, Boolean posNegRights) throws SigningPolicyException {
        if (!Boolean.TRUE.equals(posNegRights)) {
            String err = "invalidRestrictions";
            //   i18n.getMessage("invalidRestrictions", line);
            throw new SigningPolicyException(err);
        }

        if (usefulEntry && line.startsWith(CONDITION_SUBJECT)) {
            logger.debug("Read in subject condition.");
            int startIndex = CONDITION_SUBJECT.length();
            int endIndex = line.length();
            Vector<Pattern> allowedDNs = getAllowedDNs(line.substring(startIndex, endIndex));
            // Some IGTF CA signing policies include all the various versions of having the emailAddress
            // in the DN.  The "E=" variant causes an exception to be thrown in modern JVMs.
            // Hence, we ignore invalid DNs.  Luckily, the signing policies contain all variants so
            // it is safe to ignore.
            try {
                X500Principal caPrincipal = CertificateUtil.toPrincipal(caDN);
                SigningPolicy policy = new SigningPolicy(caPrincipal, allowedDNs);
                policies.put(caPrincipal, policy);
            } catch (java.lang.IllegalArgumentException e) {
                if (caDN == null) {
                    throw e;
                }
                String [] components = caDN.split("/");
                boolean hasE = false;
                for (int i=0; i<components.length; i++) {
                    String attribute = components[i].split("=")[0];
                    if (attribute.equals("E")) {
                        hasE = true;
                        break;
                    }
                }
                if (hasE) {
                    logger.warn("Invalid DN (" + caDN + ") in the CA policy");
                    logger.debug("Invalid DN in the CA policy", e);
                    return true;
                } else {
                    throw e;
                }
            }
            return true;
        }
        return false;
    }

    private String getCaDN(String line, String caDN) throws SigningPolicyException {
        String outCaDN = caDN;
        if (line.startsWith(ACCESS_ID_CA)) {
            outCaDN = getCA(line.substring(ACCESS_ID_CA.length(),
                    line.length()));
            logger.debug("CA DN is " + outCaDN);
        }
        return outCaDN;
    }

    private void validatePositiveRights(Boolean posNegRights) throws SigningPolicyException {
        if (Boolean.FALSE.equals(posNegRights)) {
            String err = "invalidPosRights";
            //  i18n.getMessage("invalidPosRights", line);
            throw new SigningPolicyException(err);
        }
    }

    private boolean isUsefulEntry(String line) throws SigningPolicyException {
        boolean usefulEntry;
        logger.debug("Parse pos_rights here");
        int startIndex = POS_RIGHTS.length();
        int endIndex = line.length();
        // if it is not CASignRight, then
        // usefulentry will be false. Otherwise
        // other restrictions will be useful.
        usefulEntry = isCASignRight(line.substring(startIndex, endIndex));
        return usefulEntry;
    }

    private Boolean handleNegativeRights(Boolean posNegRights) throws SigningPolicyException {
        if (Boolean.TRUE.equals(posNegRights)) {
            String err = "invalidNegRights";
            //  i18n.getMessage("invalidNegRights", line);
            throw new SigningPolicyException(err);
        }
        logger.debug("Ignore neg_rights");
        return Boolean.FALSE;
    }

    private void cleanupReaders(Reader reader, BufferedReader bufferedReader) {
        if (bufferedReader != null) {
            try {
                bufferedReader.close();
            } catch (Exception exp) {
                //Nothing we can do
                logger.debug("Unable to close bufferedReader", exp);
            }
        }
        if (reader != null) {
            try {
                reader.close();
            } catch (Exception e) {
                //Nothing we can do
                logger.debug("Unable to close reader", e);
            }
        }
    }

    private boolean isValidLine(String line)
            throws SigningPolicyException {

        String trimmedLine = line.trim();

        // if line is empty or comment character, skip it.
        if (trimmedLine.isEmpty() || trimmedLine.startsWith("#")) {
            return false;
        }

        // Validate that there are atleast three tokens on the line
        StringTokenizer tokenizer = new StringTokenizer(trimmedLine);
        if (tokenizer.countTokens() < MIN_TOKENS_PER_LINE) {
            // String err = i18n.getMessage("invalidTokens", line);
            String err = "invalidTokens";
            throw new SigningPolicyException(err + " on line \"" + trimmedLine + "\"");
        }

        for (String allowedLineStart : ALLOWED_LINE_START) {
            if (trimmedLine.startsWith(allowedLineStart)) {
                return true;
            }
        }
        throw new SigningPolicyException("Line starts incorrectly");

    }

    private Vector<Pattern> getAllowedDNs(String line)
            throws SigningPolicyException {

        String trimmedLine = line.trim();

        int index = findIndex(trimmedLine);

        if (index == -1) {
            String err = "invalid tokens";
            //  i18n.getMessage("invalidTokens", line);
            throw new SigningPolicyException(err);
        }

        String defAuth = trimmedLine.substring(0, index);

        if (DEF_AUTH_GLOBUS.equals(defAuth)) {

            String value = trimmedLine.substring(index + 1, trimmedLine.length());
            value = value.trim();

            int startIndex = 0;
            int endIndex = value.length();
            if (value.charAt(startIndex) == '\'') {
                startIndex++;
                int endOfDNIndex = value.indexOf('\'', startIndex);
                if (endOfDNIndex == -1) {
                    String err = "invlaid subjects";
                    //i18n.getMessage("invalidSubjects",
                    //                       lineForErr);
                    throw new SigningPolicyException(err);
                }
                endIndex = endOfDNIndex;
            }

            value = value.substring(startIndex, endIndex);
            value = value.trim();

            if (value.isEmpty()) {
                String err = "empty subjects";
                //i18n.getMessage("emptySubjects", lineForErr);
                throw new SigningPolicyException(err);
            }

            Vector<Pattern> vector = new Vector<Pattern>();

            startIndex = 0;
            endIndex = value.length();
            if (value.indexOf("\"") == -1) {
                vector.add(getPattern(value));
            } else {
                while (startIndex < endIndex) {

                    int quot1 = value.indexOf("\"", startIndex);
                    int quot2 = value.indexOf("\"", quot1 + 1);
                    if (quot2 == -1) {
                        String err = "unmatched quotes";
                        //i18n.getMessage("unmatchedQuotes",
                        //                      lineForErr);
                        throw new SigningPolicyException(err);
                    }
                    String token = value.substring(quot1 + 1, quot2);
                    vector.add(getPattern(token));
                    startIndex = quot2 + 1;
                }
            }

            return vector;
        }
        return null;
    }

    private boolean isCASignRight(String line)
            throws SigningPolicyException {

        String trimmedLine = line.trim();

        int index = findIndex(trimmedLine);

        if (index == -1) {
            String err = "invalid tokens";
            //    i18n.getMessage("invalidTokens", line);
            throw new SigningPolicyException(err);
        }

        String defAuth = trimmedLine.substring(0, index);
        if (DEF_AUTH_GLOBUS.equals(defAuth)) {
            trimmedLine = trimmedLine.substring(index + 1, trimmedLine.length());
            trimmedLine = trimmedLine.trim();
            // check if it is CA:Sign
            String value = trimmedLine.substring(0, trimmedLine.length());
            if (VALUE_CA_SIGN.equals(value)) {
                return true;
            }
        }

        return false;
    }

    private String getCA(String inputLine)
            throws SigningPolicyException {

        String line = inputLine.trim();

        int index = findIndex(line);

        if (index == -1) {
            String err = "invalid tokens";
            //  i18n.getMessage("invalidTokens", line);
            throw new SigningPolicyException(err);
        }

        String defAuth = line.substring(0, index);

        if (DEF_AUTH_X509.equals(defAuth)) {

            line = line.substring(index + 1, line.length());
            line = line.trim();

//            String dnString = line.substring(0, line.length());

            String caDN;
            // find CA DN
            int caDNLocation = 0;
            if (line.charAt(caDNLocation) == '\'') {
                caDNLocation++;
                int endofDNIndex = line.indexOf('\'', caDNLocation + 1);
                if (endofDNIndex == -1) {
                    //  String err = i18n.getMessage("invalidCaDN", inputLine);
                    String err = "invalid ca dn";
                    throw new SigningPolicyException(err);
                }
                caDN = line.substring(caDNLocation, endofDNIndex);
            } else {
                caDN = line.substring(caDNLocation, line.length() - 1);
            }
            caDN = caDN.trim();
            return caDN;
        }
        return null;
    }

    /**
     * Method that takes a pattern string as described in the signing policy
     * file with * for zero or many characters and ? for single character, and
     * converts it into java.util.regexp.Pattern object. This requires replacing
     * the wildcard characters with equivalent expression in regexp grammar.
     *
     * @param patternStr Pattern string as described in the signing policy file
     *                   with for zero or many characters and ? for single
     *                   character
     * @return Pattern object with the expression equivalent to patternStr.
     */
    public static Pattern getPattern(String patternStr) {

        if (patternStr == null) {
            throw new IllegalArgumentException();
        }

        int startIndex = 0;
        int endIndex = patternStr.length();
        StringBuffer buffer = new StringBuffer("");
        while (startIndex < endIndex) {
            int star = patternStr.indexOf(WILDCARD, startIndex);
            if (star == -1) {
                star = endIndex;
                String preStr = patternStr.substring(startIndex, star);
                buffer = buffer.append(preStr);
            } else {
                String preStr = patternStr.substring(startIndex, star);
                buffer = buffer.append(preStr).append(WILDCARD_PATTERN);
            }
            startIndex = star + 1;
        }

        String tmpPatternStr = buffer.toString();

        startIndex = 0;
        endIndex = tmpPatternStr.length();
        buffer = new StringBuffer("");
        while (startIndex < endIndex) {
            int qMark = tmpPatternStr.indexOf(SINGLE_CHAR, startIndex);
            if (qMark == -1) {
                qMark = endIndex;
                String preStr = tmpPatternStr.substring(startIndex, qMark);
                buffer = buffer.append(preStr);
            } else {
                String preStr = tmpPatternStr.substring(startIndex, qMark);
                buffer = buffer.append(preStr).append(SINGLE_PATTERN);
            }
            startIndex = qMark + 1;
        }
        tmpPatternStr = buffer.toString();

        LogFactory.getLog(SigningPolicyParser.class.getCanonicalName()).debug("String with replaced pattern is " + tmpPatternStr);

        return Pattern.compile(tmpPatternStr, Pattern.CASE_INSENSITIVE);
    }

    // find first space or tab as separator.

    private int findIndex(String line) {

        int index = -1;

        if (line == null) {
            return index;
        }

        String trimmedLine = line.trim();
        int spaceIndex = trimmedLine.indexOf(" ");
        int tabIndex = trimmedLine.indexOf("\t");

        if (spaceIndex != -1) {
            if (tabIndex != -1) {
                if (spaceIndex < tabIndex) {
                    index = spaceIndex;
                } else {
                    index = tabIndex;
                }
            } else {
                index = spaceIndex;
            }
        } else {
            index = tabIndex;
        }
        return index;
    }
}
