/*
 * @(#)CodeSource.java	1.31 01/12/03
 *
 * Copyright 2002 Sun Microsystems, Inc. All rights reserved.
 * SUN PROPRIETARY/CONFIDENTIAL. Use is subject to license terms.
 */
 
package java.security;


import java.net.URL;
import java.net.SocketPermission;
import java.util.Hashtable;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.security.cert.*;

/**
 *
 * <p>This class extends the concept of a codebase to
 * encapsulate not only the location (URL) but also the certificate(s)
 * that were used to verify signed code originating from that
 * location.
 *
 * @version 	1.31, 12/03/01
 * @author Li Gong
 * @author Roland Schemers
 */

public class CodeSource implements java.io.Serializable {

    /**
     * The code location.
     *
     * @serial
     */
    private URL location;

    // certificates
    private transient java.security.cert.Certificate certs[];

    // cached SocketPermission used for matchLocation
    private transient SocketPermission sp;

    /**
     * Constructs a CodeSource and associates it with the specified 
     * location and set of certificates.
     * 
     * @param url the location (URL).
     * 
     * @param certs the certificate(s).
     */
    public CodeSource(URL url, java.security.cert.Certificate certs[]) {
	this.location = url;
	if (certs != null) 
	    this.certs = (java.security.cert.Certificate[]) certs.clone();
    }

    /**
     * Returns the hash code value for this object.
     *
     * @return a hash code value for this object.
     */

    public int hashCode() {
	if (location != null)
	    return location.hashCode();
	else 
	    return 0;
    }

    /**
     * Tests for equality between the specified object and this
     * object. Two CodeSource objects are considered equal if their 
     * locations are of identical value and if the two sets of 
     * certificates are of identical values. It is not required that
     * the certificates be in the same order.
     * 
     * @param obj the object to test for equality with this object.
     * 
     * @return true if the objects are considered equal, false otherwise.
     */
    public boolean equals(Object obj) {
	if (obj == this) 
	    return true;

	// objects types must be equal
	if (!(obj instanceof CodeSource))
	    return false;

	CodeSource cs = (CodeSource) obj;

	// URLs must match
	if (location == null) {
	    // if location is null, then cs.location must be null as well
	    if (cs.location != null) return false;
	} else {
	    // if location is not null, then it must equal cs.location
	    if (!location.equals(cs.location)) return false;
	}

	// certs must match
	if (certs == null) {
	    // if certs is null, then cs.certs must be null as well
	    if (cs.certs != null) return false;
	} else {
	    // if certs is not null, then it must equal cs.certs
	    // equality means that both arrays of certs are the same set
	    // step 1 -- every cert in certs[] must match one in cs.certs[]
	    if (cs.certs == null) 
		return false;

	    boolean match;
	    for (int i = 0; i < certs.length; i++) {
		match = false;
		for (int j = 0; j < cs.certs.length; j++) {
		    if (certs[i].equals(cs.certs[j])) {
			match = true;
			break;
		    }
		}
		if (!match) return false;
	    }
	    // step 2 -- every key in cs.certs[] must match one in certs[]
	    for (int i = 0; i < cs.certs.length; i++) {
		match = false;
		for (int j = 0; j < certs.length; j++) {
		    if (cs.certs[i].equals(certs[j])) {
			match = true;
			break;
		    }
		}
		if (!match) return false;
	    }
	}
       
	// they must be equal if we got here...
	return true;
    }

    /**
     * Returns the location associated with this CodeSource.
     * 
     * @return the location (URL).
     */
    public final URL getLocation() {
	/* since URL is practically immutable, returning itself is not
           a security problem */
	return this.location;
    }

    /**
     * Returns the certificates associated with this CodeSource.
     * 
     * @return the certificates
     */
    public final java.security.cert.Certificate[] getCertificates() {
	/* return a clone copy, to avoid malicious modification to the
	   original object */
	if (this.certs != null) {
	    return (java.security.cert.Certificate[])this.certs.clone();
	} else {
	    return null;
	}
    }

    /**
     * Returns true if this CodeSource object "implies" the specified CodeSource.
     * <P>
     * More specifically, this method makes the following checks, in order. 
     * If any fail, it returns false. If they all succeed, it returns true.<p>
     * <ol>
     * <li> <i>codesource</i> must not be null.
     * <li> If this object's certificates are not null, then all
     * of this object's certificates must be present in <i>codesource</i>'s 
     * certificates.
     * <li> If this object's location (getLocation()) is not null, then the 
     * following checks are made against this object's location and 
     * <i>codesource</i>'s:<p>
     *   <ol>
     *     <li>  <i>codesource</i>'s location must not be null.
     *
     *     <li>  If this object's location 
     *           equals <i>codesource</i>'s location, then return true.
     *
     *     <li>  This object's protocol (getLocation().getProtocol()) must be
     *           equal to <i>codesource</i>'s protocol.
     *
     *     <li>  If this object's host (getLocation().getHost()) is not null,  
     *           then the SocketPermission
     *           constructed with this object's host must imply the
     *           SocketPermission constructed with <i>codesource</i>'s host.
     *
     *     <li>  If this object's port (getLocation().getPort()) is not 
     *           equal to -1 (that is, if a port is specified), it must equal 
     *           <i>codesource</i>'s port.
     *
     *     <li>  If this object's file (getLocation().getFile()) doesn't equal
     *           <i>codesource</i>'s file, then the following checks are made:
     *           If this object's file ends with "/-",
     *           then <i>codesource</i>'s file must start with this object's
     *           file (exclusive the trailing "-").
     *           If this object's file ends with a "/*",
     *           then <i>codesource</i>'s file must start with this object's
     *           file and must not have any further "/" separators.
     *           If this object's file doesn't end with a "/", 
     *           then <i>codesource</i>'s file must match this object's 
     *           file with a '/' appended.
     *
     *     <li>  If this object's reference (getLocation().getRef()) is 
     *           not null, it must equal <i>codesource</i>'s reference.
     *
     *   </ol>
     * </ol>
     * <p>
     * For example, the codesource objects with the following locations
     * and null certificates all imply
     * the codesource with the location "http://java.sun.com/classes/foo.jar"
     * and null certificates:
     * <pre>
     *     http:
     *     http://*.sun.com/classes/*
     *     http://java.sun.com/classes/-
     *     http://java.sun.com/classes/foo.jar
     * </pre>
     * 
     * Note that if this CodeSource has a null location and a null
     * certificate chain, then it implies every other CodeSource.
     *
     * @param codesource CodeSource to compare against.
     *
     * @return true if the specified codesource is implied by this codesource,
     * false if not.  
     */
 
    public boolean implies(CodeSource codesource)
    {
	if (codesource == null)
	    return false;

	return matchCerts(codesource) && matchLocation(codesource);
    }

   
    /**
     * Returns true if all the certs in this
     * CodeSource are also in <i>that</i>.
     * 
     * @param that the CodeSource to check against.
     */
    private boolean matchCerts(CodeSource that)
    {
	// match any key
	if (this.certs == null) 
	    return true;

	// if certs are null, and this.certs is not null, return false
	if (that.certs == null)
	    return false;

	boolean match;
	for (int i=0; i < this.certs.length; i++) {
	    match = false;
	    for (int j=0; j < that.certs.length; j++) {
		if (this.certs[i].equals(that.certs[j])) {
		    match = true;
		    break;
		}
	    }
	    if (!match) return false;
	}
	return true;
    }

    /**
     * Returns true if two CodeSource's have the "same" location.
     * 
     * @param that CodeSource to compare against
     */
    private boolean matchLocation(CodeSource that)
	{
	    if (location == null) {
		return true;
	    }

	    if ((that == null) || (that.location == null))
		return false;

	    if (location.equals(that.location))
		return true;

	    if (!location.getProtocol().equals(that.location.getProtocol()))
		return false;

	    if ((location.getHost() != null)) {
		if ((location.getHost().equals("") || 
		    location.getHost().equals("localhost")) &&
		    (that.location.getHost().equals("") || 
		     that.location.getHost().equals("localhost"))) {
		    // ok
		} else if (!location.getHost().equals(
					     that.location.getHost())) {
		    if (this.sp == null) {
			this.sp = 
			    new SocketPermission(location.getHost(),"resolve");
		    }
		    if (that.sp == null) {
			if (that.location.getHost() == null ||
				that.location.getHost().equals(""))
			    return false;
			that.sp = 
		       new SocketPermission(that.location.getHost(),"resolve");
		    }

		    boolean ok = this.sp.implies(that.sp);
		    if (!ok)
			return false;
		}
	    }

	    if (location.getPort() != -1) {
		if (location.getPort() != that.location.getPort())
		    return false;
	    }

	    if (location.getFile().endsWith("/-")) {
		// Matches the directory and (recursively) all files
		// and subdirectories contained in that directory.
		// For example, "/a/b/-" implies anything that starts with
		// "/a/b/"
		String thisPath = location.getFile().substring(0,
                                                location.getFile().length()-1);
		if (!that.location.getFile().startsWith(thisPath))
		    return false;
	    } else if (location.getFile().endsWith("/*")) {
		// Matches the directory and all the files contained in that
		// directory.
		// For example, "/a/b/*" implies anything that starts with
		// "/a/b/" but has no further slashes
		int last = that.location.getFile().lastIndexOf('/');
		if (last == -1) 
		    return false;
		String thisPath = location.getFile().substring(0,
                                                location.getFile().length()-1);
		String thatPath = that.location.getFile().substring(0, last+1);
		if (!thatPath.equals(thisPath))
		    return false;
	    } else {
		// Exact matches only.
		// For example, "/a/b" and "/a/b/" both imply "/a/b/" 
		if ((!that.location.getFile().equals(location.getFile()))
		&& (!that.location.getFile().equals(location.getFile()+"/"))) {
		    return false;
		}
	    }

	    if (location.getRef() == null)
		return true;
	    else 
		return location.getRef().equals(that.location.getRef());
	}

    /**
     * Returns a string describing this CodeSource, telling its
     * URL and certificates.
     * 
     * @return information about this CodeSource.
     */
    public String toString() {
	StringBuffer sb = new StringBuffer();
	sb.append("(");
	sb.append(this.location);
	if (this.certs != null && this.certs.length > 0) {
	    for (int i=0; i < this.certs.length; i++) {
		sb.append( " "+this.certs[i]);
	    }
	} else {
	    sb.append(" <no certificates>");
	}
	sb.append(")");
	return sb.toString();
    }

    /**
     * Writes this object out to a stream (i.e., serializes it).
     *
     * @serialData An initial <code>URL</code> is followed by an
     * <code>int</code> indicating the number of certificates to follow 
     * (a value of "zero" denotes that there are no certificates associated
     * with this object).
     * Each certificate is written out starting with a <code>String</code>
     * denoting the certificate type, followed by an
     * <code>int</code> specifying the length of the certificate encoding,
     * followed by the certificate encoding itself which is written out as an
     * array of bytes.
     */
    private synchronized void writeObject(java.io.ObjectOutputStream oos)
        throws IOException
    {
	oos.defaultWriteObject();

	if (certs==null || certs.length==0) {
	    oos.writeInt(0);
	} else {
	    // write out the total number of certs
	    oos.writeInt(certs.length);
	    // write out each cert, including its type
	    for (int i=0; i < certs.length; i++) {
		java.security.cert.Certificate cert = certs[i];
		try {
		    oos.writeUTF(cert.getType());
		    byte[] encoded = cert.getEncoded();
		    oos.writeInt(encoded.length);
		    oos.write(encoded);
		} catch (CertificateEncodingException cee) {
		    throw new IOException(cee.getMessage());
		}
	    }
	}
    }

    /**
     * Restores this object from a stream (i.e., deserializes it).
     */
    private synchronized void readObject(java.io.ObjectInputStream ois)
	throws IOException, ClassNotFoundException
    {
	CertificateFactory cf;
	Hashtable cfs=null;

	ois.defaultReadObject();

	// process any new-style certs in the stream (if present)
	int size = ois.readInt();
	if (size > 0) {
	    // we know of 3 different cert types: X.509, PGP, SDSI, which
	    // could all be present in the stream at the same time
	    cfs = new Hashtable(3);
	    this.certs = new java.security.cert.Certificate[size];
	}

	for (int i=0; i<size; i++) {
	    // read the certificate type, and instantiate a certificate
	    // factory of that type (reuse existing factory if possible)
	    String certType = ois.readUTF();
	    if (cfs.containsKey(certType)) {
		// reuse certificate factory
		cf = (CertificateFactory)cfs.get(certType);
	    } else {
		// create new certificate factory
		try {
		    cf = CertificateFactory.getInstance(certType);
		} catch (CertificateException ce) {
		    throw new ClassNotFoundException
			("Certificate factory for "+certType+" not found");
		}
		// store the certificate factory so we can reuse it later
		cfs.put(certType, cf);
	    }
	    // parse the certificate
	    byte[] encoded=null;
	    try {
		encoded = new byte[ois.readInt()];
	    } catch (OutOfMemoryError oome) {
		throw new IOException("Certificate too big");
	    }
	    ois.readFully(encoded);
	    ByteArrayInputStream bais = new ByteArrayInputStream(encoded);
	    try {
		this.certs[i] = cf.generateCertificate(bais);
	    } catch (CertificateException ce) {
		throw new IOException(ce.getMessage());
	    }
	    bais.close();
	}
    }
}

