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

import org.globus.gsi.gssapi.jaas.JaasSubject;

import java.io.InputStream;
import java.io.FileInputStream;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.security.Provider;
import java.util.Set;
import java.util.Iterator;

import javax.security.auth.Subject;

import org.ietf.jgss.GSSName;
import org.ietf.jgss.GSSCredential;
import org.ietf.jgss.GSSException;
import org.ietf.jgss.GSSContext;
import org.ietf.jgss.Oid;

import org.gridforum.jgss.ExtendedGSSManager;
import org.gridforum.jgss.ExtendedGSSCredential;

import org.globus.gsi.X509Credential;
import org.globus.gsi.CredentialException;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

/**
 * An implementation of <code>GlobusGSSManager</code>.
 */
public class GlobusGSSManagerImpl extends ExtendedGSSManager {

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

    static final Oid[] MECHS;

    static {
	try {
	    MECHS = new Oid[] {GSSConstants.MECH_OID};
	} catch(Exception e) {
	    throw new RuntimeException(e.getMessage());
	}
    }

    private GlobusGSSCredentialImpl defaultCred;

    /**
     * Acquires GSI GSS credentials.
     *
     * @see #createCredential(GSSName, int, Oid, int)
     */
    public GSSCredential createCredential (int usage)
	throws GSSException {
	return createCredential(null,
				GSSCredential.DEFAULT_LIFETIME,
				(Oid)null,
				usage);
    }

    /** Acquires GSI GSS credentials. First, it tries to find the credentials
     * in the private credential set of the current JAAS Subject. If the
     * Subject is not set or credentials are not found in the Subject, it
     * tries to get a default user credential (usually an user proxy file)
     *
     * @param lifetime Only lifetime set to
     *        {@link GSSCredential#DEFAULT_LIFETIME
     *        GSSCredential.DEFAULT_LIFETIME} is allowed.
     * @see org.globus.gsi.X509Credential#getDefaultCredential()
     */
    public GSSCredential createCredential (GSSName name,
					   int lifetime,
					   Oid mech,
					   int usage)
	throws GSSException {
	checkMechanism(mech);

	if (name != null) {
	    if (name.isAnonymous()) {
		return new GlobusGSSCredentialImpl();
	    } else {
		throw new GSSException(GSSException.UNAVAILABLE);
	    }
	}

	X509Credential cred = null;

	Subject subject = JaasSubject.getCurrentSubject();
	if (subject != null) {
	    logger.debug("Getting credential from context");
	    Set gssCreds =
		subject.getPrivateCredentials(GlobusGSSCredentialImpl.class);
	    if (gssCreds != null) {
		Iterator iter = gssCreds.iterator();
		if (iter.hasNext()) {
		    GlobusGSSCredentialImpl credImpl =
			(GlobusGSSCredentialImpl)iter.next();
		    cred = credImpl.getX509Credential();
		}
	    }
	}

	if (lifetime == GSSCredential.INDEFINITE_LIFETIME ||
            lifetime > 0) {
            // lifetime not supported
            throw new GlobusGSSException(GSSException.FAILURE,
                                         GlobusGSSException.BAD_ARGUMENT,
                                         "badLifetime01");
	}

	if (cred == null) {
	    logger.debug("Getting default credential");
	    try {
		cred = X509Credential.getDefaultCredential();
	    } catch(CredentialException e) {
		throw new GlobusGSSException(GSSException.DEFECTIVE_CREDENTIAL,
					     e);
	    } catch(Exception e) {
		throw new GlobusGSSException(GSSException.DEFECTIVE_CREDENTIAL,
					     e);
	    }

            return getDefaultCredential(cred, usage);
	} else {
            return new GlobusGSSCredentialImpl(cred, usage);
        }
    }

    private synchronized GSSCredential getDefaultCredential(
                                                 X509Credential cred,
                                                 int usage)
        throws GSSException {
        if (this.defaultCred != null &&
            this.defaultCred.getUsage() == usage &&
            this.defaultCred.getX509Credential() == cred) {
            return this.defaultCred;
        } else {
            this.defaultCred = new GlobusGSSCredentialImpl(cred, usage);
            return this.defaultCred;
        }
    }

    /**
     * Acquires GSI GSS credentials.
     *
     * @see #createCredential(GSSName, int, Oid, int)
     */
    public GSSCredential createCredential(GSSName name,
					  int lifetime,
					  Oid mechs[],
					  int usage)
	throws GSSException {
	if (mechs == null || mechs.length == 0) {
	    return createCredential(name, lifetime, (Oid)null, usage);
	} else {
	    // XXX: not sure this is correct
	    GSSCredential cred =
		createCredential(name, lifetime, mechs[0], usage);
	    for (int i = 1; i < mechs.length; i++) {
		cred.add(name, lifetime, lifetime, mechs[i], usage);
	    }
	    return cred;
	}
    }

    /**
     * Imports a credential.
     *
     * @param lifetime Only lifetime set to
     *        {@link GSSCredential#DEFAULT_LIFETIME
     *        GSSCredential.DEFAULT_LIFETIME} is allowed.
     */
    public GSSCredential createCredential (byte[] buff,
					   int option,
					   int lifetime,
					   Oid mech,
					   int usage)
	throws GSSException {
	checkMechanism(mech);

	if (buff == null || buff.length < 1) {
	    throw new GlobusGSSException(GSSException.FAILURE,
					 GlobusGSSException.BAD_ARGUMENT,
					 "invalidBuf");
	}

	if (lifetime == GSSCredential.INDEFINITE_LIFETIME ||
            lifetime > 0) {
            // lifetime not supported
            throw new GlobusGSSException(GSSException.FAILURE,
                                         GlobusGSSException.BAD_ARGUMENT,
                                         "badLifetime01");
	}

	InputStream input = null;

	switch (option) {
	case ExtendedGSSCredential.IMPEXP_OPAQUE:
	    input = new ByteArrayInputStream(buff);
	    break;
	case ExtendedGSSCredential.IMPEXP_MECH_SPECIFIC:
	    String s = new String(buff);
	    int pos = s.indexOf('=');
	    if (pos == -1) {
		throw new GSSException(GSSException.FAILURE);
	    }
	    String filename = s.substring(pos+1).trim();
	    try {
		input = new FileInputStream(filename);
	    } catch (IOException e) {
		throw new GlobusGSSException(GSSException.FAILURE, e);
	    }
	    break;
	default:
	    throw new GlobusGSSException(GSSException.FAILURE,
					 GlobusGSSException.BAD_ARGUMENT,
					 "unknownOption",
					 new Object[] {new Integer(option)});
	}

	X509Credential cred = null;
	try {
	    cred = new X509Credential(input);
	} catch(CredentialException e) {
	    throw new GlobusGSSException(GSSException.DEFECTIVE_CREDENTIAL, e);
	} catch (Exception e) {
	    throw new GlobusGSSException(GSSException.DEFECTIVE_CREDENTIAL, e);
	}

	return new GlobusGSSCredentialImpl(cred, usage);
    }

    // for initiators
    public GSSContext createContext(GSSName peer,
				    Oid mech,
				    GSSCredential cred,
				    int lifetime)
	throws GSSException {
	checkMechanism(mech);


	GlobusGSSCredentialImpl globusCred = null;
	if (cred == null) {
	    globusCred = (GlobusGSSCredentialImpl)createCredential(GSSCredential.INITIATE_ONLY);
	} else if (cred instanceof GlobusGSSCredentialImpl) {
	    globusCred = (GlobusGSSCredentialImpl)cred;
	} else {
	    throw new GSSException(GSSException.NO_CRED);
	}

	GSSContext ctx = new GlobusGSSContextImpl(peer, globusCred);
	ctx.requestLifetime(lifetime);

	return ctx;
    }

    // for acceptors
    public GSSContext createContext(GSSCredential cred)
	throws GSSException {

	GlobusGSSCredentialImpl globusCred = null;
	if (cred == null) {
	    globusCred = (GlobusGSSCredentialImpl)createCredential(GSSCredential.ACCEPT_ONLY);
	} else if (cred instanceof GlobusGSSCredentialImpl) {
	    globusCred = (GlobusGSSCredentialImpl)cred;
	} else {
	    throw new GSSException(GSSException.NO_CRED);
	}
	// XXX: don't know about the first argument
	GSSContext ctx = new GlobusGSSContextImpl(null,  globusCred);
	return ctx;
    }

    public Oid[] getMechs() {
	return MECHS;
    }

    public GSSName createName(String nameStr, Oid nameType)
	throws GSSException {
	return new GlobusGSSName(nameStr, nameType);
    }

    /**
     * Checks if the specified mechanism matches
     * the mechanism supported by this implementation.
     *
     * @param mech mechanism to check
     * @exception GSSException if mechanism not supported.
     */
    public static void checkMechanism(Oid mech)
	throws GSSException {
	if (mech != null && !mech.equals(GSSConstants.MECH_OID)) {
	    throw new GSSException(GSSException.BAD_MECH);
	}
    }

    // ==================================================================
    // Not implemented below
    // ==================================================================

    /**
     * Currently not implemented.
     */
    public GSSContext createContext(byte [] interProcessToken)
	throws GSSException {
	throw new GSSException(GSSException.UNAVAILABLE);
    }

    /**
     * Currently not implemented.
     */
    public Oid[] getNamesForMech(Oid mech)
	throws GSSException {
	throw new GSSException(GSSException.UNAVAILABLE);
    }

    /**
     * Currently not implemented.
     */
    public Oid[] getMechsForName(Oid nameType) {
	// not implemented, not needed by Globus
	return null;
    }

    /**
     * Currently not implemented.
     */
    public GSSName createName(String nameStr,
			      Oid nameType,
			      Oid mech)
	throws GSSException {
	throw new GSSException(GSSException.UNAVAILABLE);
    }

    /**
     * Currently not implemented.
     */
    public GSSName createName(byte name[], Oid nameType)
	throws GSSException {
	throw new GSSException(GSSException.UNAVAILABLE);
    }

    /**
     * Currently not implemented.
     */
    public GSSName createName(byte name[], Oid nameType, Oid mech)
	throws GSSException {
	throw new GSSException(GSSException.UNAVAILABLE);
    }

    /**
     * Currently not implemented.
     */
    public void addProviderAtFront(Provider p, Oid mech)
	throws GSSException {
	// this GSSManager implementation does not support an SPI
	// with a pluggable provider architecture
	throw new GSSException(GSSException.UNAVAILABLE);
    }

    /**
     * Currently not implemented.
     */
    public void addProviderAtEnd(Provider p, Oid mech)
	throws GSSException {
	// this GSSManager implementation does not support an SPI
	// with a pluggable provider architecture
	throw new GSSException(GSSException.UNAVAILABLE);
    }


}
