/*
 * Copyright (c) 2011-2012 ICM Uniwersytet Warszawski All rights reserved.
 * See LICENCE file for licensing information.
 *
 * Derived from the code copyrighted and licensed as follows:
 * 
 * Copyright (c) Members of the EGEE Collaboration. 2004.
 * See http://www.eu-egee.org/partners/ for details on the copyright
 * holders.
 * 
 * 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 eu.emi.security.authn.x509.helpers.proxy;

import java.io.IOException;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.Iterator;
import java.util.List;

import org.bouncycastle.asn1.ASN1Object;
import org.bouncycastle.asn1.ASN1EncodableVector;
import org.bouncycastle.asn1.ASN1Primitive;
import org.bouncycastle.asn1.ASN1OctetString;
import org.bouncycastle.asn1.ASN1Sequence;
import org.bouncycastle.asn1.DERSequence;
import org.bouncycastle.asn1.DERTaggedObject;
import org.bouncycastle.asn1.x509.GeneralName;
import org.bouncycastle.asn1.x509.GeneralSubtree;

import eu.emi.security.authn.x509.helpers.CertificateHelpers;

/**
 * An utility class for defining the allowed address space, used both to define
 * the source and target restrictions. The format is:
 * 
 * <pre>
 * iGTFProxyRestrictFrom ::= NameConstraints
 * iGTFProxyRestrictTarget ::= NameConstraints
 *  
 * NameConstraints::= SEQUENCE {
 *            permittedSubtrees       [0]     GeneralSubtrees OPTIONAL,
 *            excludedSubtrees        [1]     GeneralSubtrees OPTIONAL }
 * 
 * GeneralSubtrees ::= SEQUENCE SIZE (1..MAX) OF GeneralSubtree
 * 
 * GeneralSubtree ::= SEQUENCE {
 *            base                    GeneralName,
 *            minimum         [0]     BaseDistance DEFAULT 0,
 *            maximum         [1]     BaseDistance OPTIONAL }
 * 
 * BaseDistance ::= INTEGER (0..MAX)
 * 
 * GeneralName ::= CHOICE {
 *         otherName                       [0]     OtherName,
 *         rfc822Name                      [1]     IA5String,
 *         dNSName                         [2]     IA5String,
 *         x400Address                     [3]     ORAddress,
 *         directoryName                   [4]     Name,
 *         ediPartyName                    [5]     EDIPartyName,
 *         uniformResourceIdentifier       [6]     IA5String,
 *         iPAddress                       [7]     OCTET STRING,
 *         registeredID                    [8]     OBJECT IDENTIFIER }
 * 
 * OtherName ::= SEQUENCE {
 *         type-id    OBJECT IDENTIFIER,
 *         value      [0] EXPLICIT ANY DEFINED BY type-id }
 * 
 * EDIPartyName ::= SEQUENCE {
 *         nameAssigner            [0]     DirectoryString OPTIONAL,
 *         partyName               [1]     DirectoryString }
 * </pre>
 * 
 * And in this class only the IPAddress as a IP address - netmask combination is
 * supported.
 * 
 * @author joni.hahkala@cern.ch
 * @author K. Benedyczak
 */
public class ProxyAddressRestrictionData extends ASN1Object
{
	public static final String SOURCE_RESTRICTION_OID = "1.2.840.113612.5.5.1.1.2.1";
	public static final String TARGET_RESTRICTION_OID = "1.2.840.113612.5.5.1.1.2.2";

	private List<GeneralSubtree> permittedGeneralSubtrees = new ArrayList<GeneralSubtree>();
	private List<GeneralSubtree> excludedGeneralSubtrees = new ArrayList<GeneralSubtree>();


	/**
	 * Parses the restriction data from byte array.
	 * 
	 * @param bytes The byte array to parse.
	 * @throws IOException In case there is a problem parsing the structure.
	 */
	public ProxyAddressRestrictionData(byte[] bytes) throws IOException
	{
		ASN1Sequence nameSpaceRestrictionsSeq = (ASN1Sequence) ASN1Primitive.fromByteArray(bytes);
		switch (nameSpaceRestrictionsSeq.size()) 
		{
		case 0:
			return;
		case 1:
			DERTaggedObject taggedSequence = (DERTaggedObject) nameSpaceRestrictionsSeq.getObjectAt(0);
			if (taggedSequence.getTagNo() == 0)
			{
				copyCondSequenceToVector((DERSequence) taggedSequence.getObject(),
						permittedGeneralSubtrees);
			} else
			{
				if (taggedSequence.getTagNo() == 1)
				{
					copyCondSequenceToVector((DERSequence) taggedSequence.getObject(),
							excludedGeneralSubtrees);
				} else
				{
					throw new IllegalArgumentException("Illegal tag number in the proxy restriction NameConstraints data structure: "
								+ taggedSequence.getTagNo() + ", should have been 0 or 1");
				}
			}
			break;
		case 2:
			taggedSequence = (DERTaggedObject) nameSpaceRestrictionsSeq.getObjectAt(0);
			if (taggedSequence.getTagNo() == 0)
			{
				copyCondSequenceToVector((DERSequence) taggedSequence.getObject(),
						permittedGeneralSubtrees);
			} else
			{
				throw new IllegalArgumentException(
						"Illegal tag number in the proxy restriction NameConstraints data structure at the first position: "
								+ taggedSequence.getTagNo() + ", should have been 0");
			}
			taggedSequence = (DERTaggedObject) nameSpaceRestrictionsSeq.getObjectAt(1);
			if (taggedSequence.getTagNo() == 1)
			{
				copyCondSequenceToVector((DERSequence) taggedSequence.getObject(),
						excludedGeneralSubtrees);
			} else
			{
				throw new IllegalArgumentException(
						"Illegal tag number in the proxy restriction NameConstraints data structure at the second position: "
								+ taggedSequence.getTagNo() + ", should have been 1");
			}
			break;
		default:
			throw new IllegalArgumentException(
					"Illegal number of items in the proxy restriction NameConstraints data structure: "
							+ nameSpaceRestrictionsSeq.size() + ", should have been 0 to 2");
		}
	}

	/**
	 * Creates an instance of the extension of the given type from a certificate.
	 * @param certificate certificate
	 * @param source whether to create object representing the source restriction (if true) or target (if value is false).
	 * @return null if the certificate does not have the required extension, initialized object otherwise.
	 * @throws IOException IO exception
	 */
	public static ProxyAddressRestrictionData getInstance(X509Certificate certificate, boolean source) 
			throws IOException
	{
		byte []ext = CertificateHelpers.getExtensionBytes(certificate, 
				source ? SOURCE_RESTRICTION_OID : TARGET_RESTRICTION_OID);
		if (ext == null)
			return null;
		return new ProxyAddressRestrictionData(ext);
	}
	
	/**
	 * Constructor to generate an empty ProxyRestrictionData object for
	 * creating new restrictions. Notice that putting an empty proxy
	 * restriction into a certificate means that there are no permitted IP
	 * spaces, meaning the proxy should be rejected everywhere.
	 */
	public ProxyAddressRestrictionData()
	{
		// creates empty restriction data object.
	}
	
	/**
	 * This method copies the contents of a generalSubtrees sequence into
	 * the given vector. Static to protect the internal data structures from
	 * access.
	 * 
	 * @param subSeq
	 *                the subsequence to copy.
	 * @param vector
	 *                The target to copy the parsed GeneralSubtree objects.
	 */
	private static void copyCondSequenceToVector(DERSequence subSeq,
			List<GeneralSubtree> vector)
	{
		Enumeration<?> subTreeEnum = subSeq.getObjects();
		while (subTreeEnum.hasMoreElements())
		{
			ASN1Primitive object = (ASN1Primitive) subTreeEnum.nextElement();
			vector.add(GeneralSubtree.getInstance(object));
		}
	}

	/**
	 * Adds a new permitted IP addressSpace to the data structure.
	 * 
	 * @param address The address space to add to the allowed ip address
	 *                space. Example of the format: 192.168.0.0/16. Which
	 *                equals a 192.168.0.0 with a net mask 255.255.0.0. A
	 *                single IP address can be defined as
	 *                xxx.xxx.xxx.xxx/32. <br> It is also possible to provide IPv6 
	 *                addresses.
	 *                See <a href="http://www.ietf.org/rfc/rfc4632.txt"> RFC4632.</a>
	 */
	public void addPermittedIPAddressWithNetmask(String address)
	{
		permittedGeneralSubtrees.add(new GeneralSubtree(new GeneralName(
				GeneralName.iPAddress, address), null, null));
	}

	/**
	 * Adds a new excluded IP addressSpace to the data structure.
	 * 
	 * @param address The address space to add to the allowed ip address
	 *                space. Example of the format: 192.168.0.0/16. Which
	 *                equals a 192.168.0.0 with a net mask 255.255.0.0. A
	 *                single IP address can be defined as
	 *                xxx.xxx.xxx.xxx/32. <br> It is also possible to provide IPv6 
	 *                addresses. See <a href="http://www.ietf.org/rfc/rfc4632.txt"> RFC4632.</a> 
	 */
	public void addExcludedIPAddressWithNetmask(String address)
	{
		excludedGeneralSubtrees.add(new GeneralSubtree(new GeneralName(
				GeneralName.iPAddress, address), null, null));
	}

	/**
	 * Returns the NameConstraints structure of the restrictions.
	 * 
	 * @return The DERSequence containing the NameConstraints structure.
	 */
	@Override
	public ASN1Primitive toASN1Primitive()
	{
		ASN1EncodableVector nameConstraintsSequenceVector = new ASN1EncodableVector();

		addTaggedSequenceOfSubtrees(0, permittedGeneralSubtrees,
				nameConstraintsSequenceVector);
		addTaggedSequenceOfSubtrees(1, excludedGeneralSubtrees,
				nameConstraintsSequenceVector);

		return new DERSequence(nameConstraintsSequenceVector);
	}

	/**
	 * Adds, with the given tag, a DER sequence object that contains the
	 * GeneralSubtree objects into the ASN1Vector.
	 * 
	 * @param tagNo
	 *                The tag to tag the object.
	 * @param subtrees
	 *                The Vector of GeneralSubtree objects. Null will throw
	 *                NullPointerException. An empty Vector will not be
	 *                added.
	 * @param asn1Vector
	 *                The vector to add the subtrees sequence with the given
	 *                tag.
	 */
	private static void addTaggedSequenceOfSubtrees(int tagNo, List<GeneralSubtree> subtrees,
			ASN1EncodableVector asn1Vector)
	{
		if (!subtrees.isEmpty())
		{
			ASN1EncodableVector subtreesSequenceVector = new ASN1EncodableVector();

			Iterator<GeneralSubtree> generalSubtreesEnum = subtrees.iterator();
			while (generalSubtreesEnum.hasNext())
			{
				subtreesSequenceVector.add(generalSubtreesEnum.next());
			}
			asn1Vector.add(new DERTaggedObject(tagNo, new DERSequence(
					subtreesSequenceVector)));
		}
	}

	/**
	 * Returns a Vector of Vectors of IP address spaces as defined in rfc
	 * 4632.
	 * 
	 * @see #addExcludedIPAddressWithNetmask(String)
	 * @return The array of arrays of string representation of address
	 *         spaces defined in this structure. The first element in the
	 *         array lists the permitted IP address spaces and the second
	 *         the excluded IP spaces. In format ipaddress/netmask bytes.
	 *         Example {137,138,0,0,255,255,0,0}. Array always contains two
	 *         items, but they can be of length 0.
	 */
	public byte[][][] getIPSpaces()
	{
		byte allowedIPSpaces[][] = subtreesIntoArray(permittedGeneralSubtrees);
		byte excludedIPSpaces[][] = subtreesIntoArray(excludedGeneralSubtrees);

		return new byte[][][] { allowedIPSpaces, excludedIPSpaces };
	}
	
	public String[] getPermittedAddresses()
	{
		byte[][][] spaces = getIPSpaces();
		return convert2strings(spaces[0]);
	}

	public String[] getExcludedAddresses()
	{
		byte[][][] spaces = getIPSpaces();
		return convert2strings(spaces[1]);
	}

	/**
	 * Generates a string array of IP address spaces from a list of
	 * GeneralSubtrees.
	 * 
	 * @param subtrees The list of GeneralSubtrees to parse. Null as input
	 *                will return null.
	 * @return the array of IP address spaces.
	 */
	private static byte[][] subtreesIntoArray(List<GeneralSubtree> subtrees)
	{
		if (subtrees == null)
			return null;

		List<byte[]> ips = new ArrayList<byte[]>();
		Iterator<GeneralSubtree> enumGeneralNames = subtrees.iterator();
		while (enumGeneralNames.hasNext())
		{
			GeneralName item = enumGeneralNames.next().getBase();
			if (item.getTagNo() == GeneralName.iPAddress)
			{
				ASN1OctetString octets = (ASN1OctetString) item.getName();
				byte[] bytes = octets.getOctets();
				ips.add(bytes);
			}
		}
		return ips.toArray(new byte[ips.size()][]);
	}
	
	public static String convert2sr(byte[] src)
	{
		int half = src.length/2;
		StringBuilder ret = new StringBuilder(40);
		boolean ipv6 = src.length == 32;
		for (int i=0; i<half; i++)
		{
			ret.append(ipv6 ? Integer.toHexString(src[i]&255) : src[i]&255);
			if (i<half-1)
				ret.append(ipv6? ":" : ".");
		}
		ret.append("/");
		int mask = 0;
		for (int i=half; i<src.length; i++)
			mask += Integer.bitCount(src[i]&255);
		ret.append(mask);
		return ret.toString();
	}
	
	public static String[] convert2strings(byte[][] src)
	{
		String[] ret = new String[src.length];
		for (int i=0; i<src.length; i++)
			ret[i] = convert2sr(src[i]);
		return ret;
	}
}
