/*
 * Copyright 1999-2006 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.myproxy;

import java.nio.charset.Charset;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.Writer;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.IOException;
import java.io.EOFException;
import java.io.ByteArrayOutputStream;
import java.io.ByteArrayInputStream;
import java.io.DataOutputStream;
import java.util.Map;
import java.util.HashMap;
import java.util.Iterator;
import java.net.Socket;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.UnknownHostException;

import java.security.KeyPair;
import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;
import java.security.cert.X509Certificate;
import java.security.MessageDigest;
import java.security.GeneralSecurityException;

import java.security.Signature;

import javax.security.auth.x500.X500Principal;

import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSocket;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;

import org.bouncycastle.asn1.x509.X509Name;

import org.globus.common.CoGProperties;

import org.bouncycastle.util.encoders.Base64;

import org.globus.gsi.X509Credential;
import org.globus.gsi.GSIConstants;
import org.globus.gsi.util.CertificateIOUtil;
import org.globus.gsi.util.CertificateUtil;
import org.globus.gsi.OpenSSLKey;
import org.globus.gsi.gssapi.net.GssSocket;
import org.globus.gsi.gssapi.net.GssSocketFactory;
import org.globus.gsi.gssapi.auth.Authorization;
import org.globus.gsi.gssapi.auth.IdentityAuthorization;
import org.globus.gsi.gssapi.GlobusGSSCredentialImpl;
import org.globus.gsi.gssapi.GSSConstants;
import org.globus.gsi.bc.BouncyCastleCertProcessingFactory;

import org.gridforum.jgss.ExtendedGSSManager;

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

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


/**
 * This class provides an API for communicating with MyProxy servers.
 * It provides main functions for retrieving, removing and
 * storing credentials on MyProxy server. It also provides functions
 * for getting credential information and changing passwords.
 * <P>
 * More information about MyProxy is available on the
 * <a href="http://myproxy.ncsa.uiuc.edu/">MyProxy Home Page</a>.
 * <P>
 *
 * @version 2.0
 */
public class MyProxy  {

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

    public final static String version = "2.0";

    public static final int MIN_PASSWORD_LENGTH =
        MyProxyConstants.MIN_PASSWORD_LENGTH;

    public static final String MYPROXY_PROTOCOL_VERSION =
        MyProxyConstants.MYPROXY_PROTOCOL_VERSION;

    private static final String RESPONSE   = "RESPONSE=";
    private static final String ERROR      = "ERROR=";
    private static final String AUTHZ_DATA = "AUTHORIZATION_DATA=";

    private static final String CRED            = "CRED_";
    private static final String OWNER           = "OWNER=";
    private static final String START_TIME      = "START_TIME=";
    private static final String END_TIME        = "END_TIME=";
    private static final String DESC            = "DESC=";
    private static final String RETRIEVER       = "RETRIEVER=";
    private static final String RENEWER         = "RENEWER=";
    private static final String TRUSTROOTS      = "TRUSTED_CERTS=";

    private static final String CRED_START_TIME = CRED + START_TIME;
    private static final String CRED_END_TIME   = CRED + END_TIME;
    private static final String CRED_OWNER      = CRED + OWNER;
    private static final String CRED_DESC       = CRED + DESC;
    private static final String CRED_RETRIEVER  = CRED + RETRIEVER;
    private static final String CRED_RENEWER    = CRED + RENEWER;
    private static final String CRED_NAME       = CRED + "NAME=";

    /** The default MyProxy server port (7512). */
    public static final int DEFAULT_PORT = 7512;

    /** The default key size (2048 bits). */
    public static final int DEFAULT_KEYBITS = 2048;

    /** The integer command number for the MyProxy 'Get' command (0). */
    public static final int GET_PROXY       = 0;
    /** The integer command number for the MyProxy 'Put' command (1). */
    public static final int PUT_PROXY       = 1;
    /** The integer command number for the MyProxy 'Info' command (2). */
    public static final int INFO_PROXY      = 2;
    /** The integer command number for the MyProxy 'Destroy' command (3). */
    public static final int DESTROY_PROXY   = 3;
    /** The integer command number for the MyProxy Password Change
     * command (4). */
    public static final int CHANGE_PASSWORD = 4;
    /** The integer command number for the MyProxy 'Store' command (5). */
    public static final int STORE_CREDENTIAL = 5;
    /** The integer command number for the MyProxy 'Retrieve' command (6). */
    public static final int RETRIEVE_CREDENTIAL = 6;
    /** The integer command number for the MyProxy 'Get Trustroots' command (7). */
    public static final int GET_TRUSTROOTS = 7;

    /** The hostname(s) of the target MyProxy server(s). Multiple host names
        can be specified comma delimited with each hostname optionally followed
        by a ':' and port number. The client will communicate with the first
        server it has a successful network connection with. */
    protected String host;
    /** The port of the target MyProxy server (default 7512). */
    protected int port = DEFAULT_PORT;
    /** The authorization policy in effect for the target MyProxy server. */
    protected Authorization authorization;
    /** The GSSContext for communication with the MyProxy server. */
    protected GSSContext context;
    /** Trustroot information and path constant. */
    protected String[] trustrootFilenames;
    protected String[] trustrootData;
    private final static String TRUSTED_CERT_PATH = "/.globus/certificates";

    /**
     * Initialize the MyProxy client object with the default
     * authorization policy.
     */
    public MyProxy() {
        setAuthorization(new MyProxyServerAuthorization());
    }

    /**
     * Prepare to connect to the MyProxy server at the specified
     * host and port using the default authorization policy.
     *
     * @param host
     *        The hostname(s) of the MyProxy server(s) with optional port
     *        info. Multiple hostnames can be specified in a comma separated
     *        list with each hostname optionally followed by a  ':' and port
     *        number. The client will communicate with the first server it has
     *        a successful network connection with.
     * @param port
     *        The port number of the MyProxy server to use if one is not
     *        specified as part of the host string.
     */
    public MyProxy(String host, int port) {
        setHost(host);
        setPort(port);
        setAuthorization(new MyProxyServerAuthorization());
    }

    /**
     * Set MyProxy server hostname.
     * @param host
     *        The hostname(s) of the MyProxy server(s). Multiple host names
     *        are comma delimited with each hostname optionally followed by a
     *       ':' and port number. The client will communicate with the first
     *       server it has a successful network connection with.
     */
    public void setHost(String host) {
        this.host = host;
    }

    /**
     * Get MyProxy server hostname.
     * @return The hostname of the MyProxy server.
     */
    public String getHost() {
        return host;
    }

    /**
     * Set MyProxy server port.
     * @param port
     *        The port number of the MyProxy server to use if one is not
     *        specified as part of the host string. Defaults to
     *        MyProxy.DEFAULT_PORT.
     */
    public void setPort(int port) {
        this.port = port;
    }

    /**
     * Get MyProxy server port.
     * @return The port number of the MyProxy server.
     */
    public int getPort() {
        return port;
    }

    /**
     * Set MyProxy server authorization mechanism.
     * @param authorization
     *        The authorization mechanism for the MyProxy server.
     */
    public void setAuthorization(Authorization authorization) {
        this.authorization = authorization;
    }

    /**
     * Get MyProxy server authorization mechanism.
     * @return The authorization mechanism for the MyProxy server.
     */
    public Authorization getAuthorization() {
        return this.authorization;
    }

    private GssSocket getSocket(GSSCredential credential)
        throws IOException, GSSException {
        GSSManager manager = ExtendedGSSManager.getInstance();

        this.context = manager.createContext(null,
                                             GSSConstants.MECH_OID,
                                             credential,
                                             GSSContext.DEFAULT_LIFETIME);

        // no delegation
        this.context.requestCredDeleg(false);

        // Request confidentiality
        this.context.requestConf(true);

        IOException exception = null;
        Socket socket = null;
        String goodAddr = "";
        int hostIdx = 0;
        String hosts[] = host.split(",");
        int socketTimeout = CoGProperties.getDefault().getSocketTimeout();

            int currentPort = port;
        search:
            while (hostIdx < hosts.length) {
                String hostPort[] = hosts[hostIdx].split(":");
                hosts[hostIdx] = hostPort[0];
                if (hostPort.length > 1 && hostPort[1] != null)
                    // port number specified
                    port = Integer.parseInt(hostPort[1].trim());
                else
                    port = currentPort;

                InetAddress addrs[] = null;
                try {
                    addrs = InetAddress.getAllByName(hosts[hostIdx]);
                } catch (UnknownHostException e) {
                    if (logger.isDebugEnabled()) {
                        logger.debug("getSocket(): Skipping unknown host " +
                                hosts[hostIdx]);
                    }
                    exception = e;
                }
                for (int addrIdx = 0; addrs != null && addrIdx < addrs.length;
                                                                  addrIdx++) {
                    exception = null;
                    try {
                        if (logger.isDebugEnabled()) {
                            logger.debug("getSocket(): Trying " +
                                addrs[addrIdx].toString());
                        }
                        socket = new Socket();
                        socket.connect(
                            new InetSocketAddress(addrs[addrIdx],port),
                            socketTimeout);

                        goodAddr = addrs[addrIdx].toString();
                        if (logger.isDebugEnabled()) {
                            logger.debug("             Succeeded.");
                        }
                        break search;
                    } catch (IOException e) {
                        exception = e;
                        if (logger.isDebugEnabled()) {
                            logger.debug("             Failed.");
                        }
                    }
                }
                hostIdx += 1;
            }

        if (exception != null) {
            if (logger.isDebugEnabled()) {
                logger.debug("getSocket(): " +
                    "Unable to connect to a MyProxy host");
            }
            throw exception;
        }

        setHost(hosts[hostIdx]); // host we have successfully connected to

        GssSocketFactory gssFactory = GssSocketFactory.getDefault();

        GssSocket gssSocket =
            (GssSocket)gssFactory.createSocket(socket,
                                               hosts[hostIdx],
                                               port,
                                               this.context);

        if (logger.isDebugEnabled()) {
            logger.debug("getSocket(): Connected to " + goodAddr);
        }

        gssSocket.setAuthorization(this.authorization);

        return gssSocket;
    }

    /**
     * Delegate credentials to a MyProxy server.
     *
     * @param  credential
     *         The GSI credentials to use.
     * @param  username
     *         The username to store the credentials under.
     * @param  passphrase
     *         The passphrase to use to encrypt the stored
     *         credentials.
     * @param  lifetime
     *         The maximum lifetime of credentials delegated by the server
     *         (in seconds).
     * @exception MyProxyException
     *            If an error occurred during the operation.
     */
    public void put(GSSCredential credential,
                    String username,
                    String passphrase,
                    int lifetime)
        throws MyProxyException {
        InitParams request = new InitParams();
        request.setUserName(username);
        request.setPassphrase(passphrase);
        request.setLifetime(lifetime);
        put(credential, request);
    }

    /**
     * Delegate credentials to a MyProxy server.
     *
     * @param  credential
     *         The GSI credentials to use.
     * @param  params
     *         The parameters for the put operation.
     * @exception MyProxyException
     *            If an error occurred during the operation.
     */
    public void put(GSSCredential credential,
                    InitParams params)
        throws MyProxyException {

        if (credential == null) {
            throw new IllegalArgumentException("credential == null");
        }

        if (params == null) {
            throw new IllegalArgumentException("params == null");
        }

        if (!(credential instanceof GlobusGSSCredentialImpl)) {
            throw new IllegalArgumentException("wrong type of credentials");
        }

        String msg = params.makeRequest();

        Socket gsiSocket = null;
        OutputStream out = null;
        InputStream  in  = null;

        try {
            gsiSocket = getSocket(credential);

            out = gsiSocket.getOutputStream();
            in  = gsiSocket.getInputStream();

            if (!((GssSocket)gsiSocket).getContext().getConfState())
                throw new Exception("Confidentiality requested but not available");

            // send message
            out.write(msg.getBytes());
            out.flush();

            if (logger.isDebugEnabled()) {
                logger.debug("Req sent:" + params);
            }

            handleReply(in);

            BouncyCastleCertProcessingFactory certFactory =
                BouncyCastleCertProcessingFactory.getDefault();

            GlobusGSSCredentialImpl pkiCred =
                (GlobusGSSCredentialImpl)credential;

            X509Certificate [] certs = pkiCred.getCertificateChain();

            // read in the cert request from socket and
            // generate a certificate to be sent back to the server
            X509Certificate cert =
                certFactory.createCertificate(in,
                                              certs[0],
                                              pkiCred.getPrivateKey(),
                                              -1,
                     BouncyCastleCertProcessingFactory.decideProxyType(certs[0],
                                             GSIConstants.DelegationType.FULL));

            // write the new cert we've generated to the socket to send it back
            // to the server

            // must put everything into one message
            ByteArrayOutputStream buffer = new ByteArrayOutputStream(2048);

            buffer.write( (byte)(certs.length+1) );

            // write signed ceritifcate
            buffer.write(cert.getEncoded());

            for (int i=0;i<certs.length;i++) {
                buffer.write( certs[i].getEncoded() );

                // DEBUG: print out subject name of sent certificate
                if (logger.isDebugEnabled()) {
                    logger.debug("Sent cert: " + certs[i].getSubjectDN());
                }

            }

            out.write(buffer.toByteArray());
            out.flush();

            handleReply(in);

        } catch(Exception e) {
            throw new MyProxyException("MyProxy put failed.", e);
        } finally {
            // close socket
            close(out, in, gsiSocket);
        }
    }

    /**
     * Store credentials on a MyProxy server.
     * Copies certificate(s) and private key directly to the server rather
     * than delegating an X.509 proxy credential.
     *
     * @param  credential
     *         The local GSI credentials to use for authentication.
     * @param  certs
     *         The certificate(s) to store.
     * @param  key
     *         The private key to store (typically encrypted).
     * @param  params
     *         The parameters for the store operation.
     * @exception MyProxyException
     *            If an error occurred during the operation.
     */
    public void store(GSSCredential credential,
                      X509Certificate [] certs,
                      OpenSSLKey key,
                      StoreParams params)
        throws MyProxyException {

        if (credential == null) {
            throw new IllegalArgumentException("credential == null");
        }

        if (certs == null) {
            throw new IllegalArgumentException("certs == null");
        }

        if (key == null) {
            throw new IllegalArgumentException("key == null");
        }

        if (params == null) {
            throw new IllegalArgumentException("params == null");
        }

        if (!(credential instanceof GlobusGSSCredentialImpl)) {
            throw new IllegalArgumentException("wrong type of credentials");
        }

        String msg = params.makeRequest();

        Socket gsiSocket = null;
        OutputStream out = null;
        InputStream  in  = null;

        try {
            gsiSocket = getSocket(credential);

            out = gsiSocket.getOutputStream();
            in  = gsiSocket.getInputStream();

            if (!((GssSocket)gsiSocket).getContext().getConfState())
                throw new Exception("Confidentiality requested but not available");

            // send message
            out.write(msg.getBytes());
            out.flush();

            if (logger.isDebugEnabled()) {
                logger.debug("Req sent:" + params);
            }

            handleReply(in);

            CertificateIOUtil.writeCertificate(out, certs[0]);
            key.writeTo(out);
            for (int i=1;i<certs.length;i++) {
                // skip the self-signed certificates
                if (certs[i].getSubjectDN().equals(certs[i].getIssuerDN()))
                    continue;
                CertificateIOUtil.writeCertificate(out, certs[i]);
            }
            out.write('\0');
            out.flush();

            handleReply(in);

        } catch(Exception e) {
            throw new MyProxyException("MyProxy store failed.", e);
        } finally {
            // close socket
            close(out, in, gsiSocket);
        }
    }

    /**
     * Removes delegated credentials from the MyProxy server.
     *
     * @param  credential
     *         The local GSI credentials to use for authentication.
     * @param  username
     *         The username of the credentials to remove.
     * @param  passphrase
     *         The passphrase of the credentials to remove.
     * @exception MyProxyException
     *         If an error occurred during the operation.
     */
    public void destroy(GSSCredential credential,
                        String username,
                        String passphrase)
        throws MyProxyException {
        DestroyParams request = new DestroyParams();
        request.setUserName(username);
        request.setPassphrase(passphrase);
        destroy(credential, request);
    }

    /**
     * Removes delegated credentials from the MyProxy server.
     *
     * @param  credential
     *         The local GSI credentials to use for authentication.
     * @param  params
     *         The parameters for the destroy operation.
     * @exception MyProxyException
     *         If an error occurred during the operation.
     */
    public void destroy(GSSCredential credential,
                        DestroyParams params)
        throws MyProxyException {

        if (credential == null) {
            throw new IllegalArgumentException("credential == null");
        }

        if (params == null) {
            throw new IllegalArgumentException("params == null");
        }

        String msg = params.makeRequest();

        Socket gsiSocket = null;
        OutputStream out = null;
        InputStream  in  = null;

        try {
            gsiSocket = getSocket(credential);

            out = gsiSocket.getOutputStream();
            in  = gsiSocket.getInputStream();

            if (!((GssSocket)gsiSocket).getContext().getConfState())
                throw new Exception("Confidentiality requested but not available");

            // send message
            out.write(msg.getBytes());
            out.flush();

            if (logger.isDebugEnabled()) {
                logger.debug("Req sent:" + params);
            }

            handleReply(in);

        } catch(Exception e) {
            throw new MyProxyException("MyProxy destroy failed.", e);
        } finally {
            // close socket
            close(out, in, gsiSocket);
        }
    }

    /**
     * Changes the password of the credential on the
     * MyProxy server.
     *
     * @param  credential
     *         The local GSI credentials to use for authentication.
     * @param  params
     *         The parameters for the change password operation.
     * @exception MyProxyException
     *         If an error occurred during the operation.
     */
    public void changePassword(GSSCredential credential,
                               ChangePasswordParams params)
        throws MyProxyException {

        if (credential == null) {
            throw new IllegalArgumentException("credential == null");
        }

        if (params == null) {
            throw new IllegalArgumentException("params == null");
        }

        Socket gsiSocket = null;
        OutputStream out = null;
        InputStream  in  = null;

        String msg = params.makeRequest();

        try {
            gsiSocket = getSocket(credential);

            out = gsiSocket.getOutputStream();
            in  = gsiSocket.getInputStream();

            if (!((GssSocket)gsiSocket).getContext().getConfState())
                throw new Exception("Confidentiality requested but not available");

            // send message
            out.write(msg.getBytes());
            out.flush();

            if (logger.isDebugEnabled()) {
                logger.debug("Req sent:" + params);
            }

            handleReply(in);

        } catch(Exception e) {
            throw new MyProxyException("MyProxy change password failed.", e);
        } finally {
            // close socket
            close(out, in, gsiSocket);
        }
    }


    /**
     * Retrieves credential information from MyProxy server.
     * Only the information of the default credential is returned
     * by this operation.
     *
     * @param  credential
     *         The local GSI credentials to use for authentication.
     * @param  username
     *         The username of the credentials to remove.
     * @param  passphrase
     *         The passphrase of the credentials to remove.
     * @exception MyProxyException
     *         If an error occurred during the operation.
     * @return The credential information of the default
     *         credential.
     */
    public CredentialInfo info(GSSCredential credential,
                               String username,
                               String passphrase)
        throws MyProxyException {
        InfoParams request = new InfoParams();
        request.setUserName(username);
        request.setPassphrase(passphrase);
        return info(credential, request)[0];
    }

    /**
     * Retrieves credential information from MyProxy server.
     *
     * @param  credential
     *         The local GSI credentials to use for authentication.
     * @param  params
     *         The parameters for the info operation.
     * @exception MyProxyException
     *         If an error occurred during the operation.
     * @return The array of credential information of all
     *         the user's credentials.
     */
    public CredentialInfo[] info(GSSCredential credential,
                                 InfoParams params)
        throws MyProxyException {

        if (credential == null) {
            throw new IllegalArgumentException("credential == null");
        }

        if (params == null) {
            throw new IllegalArgumentException("params == null");
        }

        String msg = params.makeRequest();

        CredentialInfo [] creds = null;
        Socket gsiSocket = null;
        OutputStream out = null;
        InputStream  in  = null;

        try {
            gsiSocket = getSocket(credential);

            out = gsiSocket.getOutputStream();
            in  = gsiSocket.getInputStream();

            if (!((GssSocket)gsiSocket).getContext().getConfState())
                throw new Exception("Confidentiality requested but not available");

            // send message
            out.write(msg.getBytes());
            out.flush();

            if (logger.isDebugEnabled()) {
                logger.debug("Req sent:" + params);
            }

            InputStream reply = handleReply(in);
            String line = null;
            String value = null;
            Map credMap = new HashMap();
            CredentialInfo info = new CredentialInfo();
            while( (line = readLine(reply)) != null ) {
                if (line.startsWith(CRED_START_TIME)) {
                    value = line.substring(CRED_START_TIME.length());
                    info.setStartTime(Long.parseLong(value) * 1000);
                } else if (line.startsWith(CRED_END_TIME)) {
                    value = line.substring(CRED_END_TIME.length());
                    info.setEndTime(Long.parseLong(value) * 1000);
                } else if (line.startsWith(CRED_OWNER)) {
                    info.setOwner(line.substring(CRED_OWNER.length()));
                } else if (line.startsWith(CRED_NAME)) {
                    info.setName(line.substring(CRED_NAME.length()));
                } else if (line.startsWith(CRED_DESC)) {
                    info.setDescription(line.substring(CRED_DESC.length()));
                } else if (line.startsWith(CRED_RENEWER)) {
                    info.setRenewers(line.substring(CRED_RENEWER.length()));
                } else if (line.startsWith(CRED_RETRIEVER)) {
                    info.setRetrievers(line.substring(CRED_RETRIEVER.length()));
                } else if (line.startsWith(CRED)) {
                    int pos = line.indexOf('=', CRED.length());
                    if (pos == -1) {
                        continue;
                    }
                    value = line.substring(pos+1);

                    if (matches(line, pos+1, OWNER)) {
                        String name = getCredName(line, pos, OWNER);
                        getCredentialInfo(credMap, name).setOwner(value);
                    } else if (matches(line, pos+1, START_TIME)) {
                        String name = getCredName(line, pos, START_TIME);
                        getCredentialInfo(credMap, name).setStartTime(Long.parseLong(value) * 1000);
                    } else if (matches(line, pos+1, END_TIME)) {
                        String name = getCredName(line, pos, END_TIME);
                        getCredentialInfo(credMap, name).setEndTime(Long.parseLong(value) * 1000);
                    } else if (matches(line, pos+1, DESC)) {
                        String name = getCredName(line, pos, DESC);
                        getCredentialInfo(credMap, name).setDescription(value);
                    } else if (matches(line, pos+1, RENEWER)) {
                        String name = getCredName(line, pos, RENEWER);
                        getCredentialInfo(credMap, name).setRenewers(value);
                    } else if (matches(line, pos+1, RETRIEVER)) {
                        String name = getCredName(line, pos, RETRIEVER);
                        getCredentialInfo(credMap, name).setRetrievers(value);
                    }
                }
            }

            creds = new CredentialInfo[1 + credMap.size()];
            creds[0] = info; // defailt creds at position 0

            if (credMap.size() > 0) {
                int i = 1;
                Iterator iter = credMap.entrySet().iterator();
                while(iter.hasNext()) {
                    Map.Entry entry = (Map.Entry)iter.next();
                    creds[i++] = (CredentialInfo)entry.getValue();
                }
            }

            return creds;
        } catch(Exception e) {
            throw new MyProxyException("MyProxy info failed.", e);
        } finally {
            // close socket
            close(out, in, gsiSocket);
        }
    }

    private boolean matches(String line, int pos, String arg) {
        return line.regionMatches(true,
                                  pos - arg.length(),
                                  arg,
                                  0,
                                  arg.length());
    }

    private String getCredName(String line, int pos, String arg) {
        return line.substring(CRED.length(), pos-arg.length());
    }

    private CredentialInfo getCredentialInfo(Map map, String name) {
        CredentialInfo info = (CredentialInfo)map.get(name);
        if (info == null) {
            info = new CredentialInfo();
            info.setName(name);
            map.put(name, info);
        }
        return info;
    }

    /**
     * Retrieves delegated credentials from MyProxy server Anonymously
     * (without local credentials)
     *
     * Notes: Performs simple verification of private/public keys of
     *        the delegated credential. Should be improved later.
     *        And only checks for RSA keys.
     *
     * @param  username
     *         The username of the credentials to retrieve.
     * @param  passphrase
     *         The passphrase of the credentials to retrieve.
     * @param  lifetime
     *         The requested lifetime of the retrieved credential (in seconds).
     * @return GSSCredential
     *         The retrieved delegated credentials.
     * @exception MyProxyException
     *         If an error occurred during the operation.
     */
    public GSSCredential get(String username,
                             String passphrase,
                             int lifetime)
        throws MyProxyException {
        return get(null, username, passphrase, lifetime);
    }

    /**
     * Retrieves delegated credentials from the MyProxy server.
     *
     * Notes: Performs simple verification of private/public keys of
     *        the delegated credential. Should be improved later.
     *        And only checks for RSA keys.
     *
     * @param  credential
     *         The local GSI credentials to use for authentication.
     *         Can be set to null if no local credentials.
     * @param  username
     *         The username of the credentials to retrieve.
     * @param  passphrase
     *         The passphrase of the credentials to retrieve.
     * @param  lifetime
     *         The requested lifetime of the retrieved credential (in seconds).
     * @return GSSCredential
     *         The retrieved delegated credentials.
     * @exception MyProxyException
     *         If an error occurred during the operation.
     */
    public GSSCredential get(GSSCredential credential,
                             String username,
                             String passphrase,
                             int lifetime)
        throws MyProxyException {
        GetParams request = new GetParams();
        request.setUserName(username);
        request.setPassphrase(passphrase);
        request.setLifetime(lifetime);
        return get(credential, request);
    }

    /**
     * Retrieves delegated credentials from the MyProxy server.
     *
     * @param  credential
     *         The local GSI credentials to use for authentication.
     *         Can be set to null if no local credentials.
     * @param  params
     *         The parameters for the get operation.
     * @return GSSCredential
     *         The retrieved delegated credentials.
     * @exception MyProxyException
     *         If an error occurred during the operation.
     */
    public GSSCredential get(GSSCredential credential,
                             GetParams params)
        throws MyProxyException {

        if (params == null) {
            throw new IllegalArgumentException("params == null");
        }

        if (credential == null) {
            try {
                credential = getAnonymousCredential();
            } catch (GSSException e) {
                throw new MyProxyException("Failed to create anonymous credentials", e);
            }
        }

        String msg = params.makeRequest();

        Socket gsiSocket = null;
        OutputStream out = null;
        InputStream in   = null;

        try {
            gsiSocket = getSocket(credential);

            if (credential.getName().isAnonymous()) {
                this.context.requestAnonymity(true);
            }

            out = gsiSocket.getOutputStream();
            in  = gsiSocket.getInputStream();

            if (!((GssSocket)gsiSocket).getContext().getConfState())
                throw new Exception("Confidentiality requested but not available");

            // send message
            out.write(msg.getBytes());
            out.flush();

            if (logger.isDebugEnabled()) {
                logger.debug("Req sent:" + params);
            }

            // may require authz handshake
            handleReply(in, out, params.getAuthzCreds(),
                        params.getWantTrustroots());

            // start delegation - generate key pair
            KeyPair keyPair = CertificateUtil.generateKeyPair("RSA",
                    DEFAULT_KEYBITS);

            // According to the MyProxy protocol, the MyProxy server
            // will ignore the subject in the client's certificate
            // signing request (CSR). However, in some cases it is
            // helpful to control the CSR subject (for example, when
            // the MyProxy server is using a CA back-end that can only
            // issue certificates with subjects matching the request).
            // So we construct the CSR subject using the given MyProxy
            // username (if possible).
            String CSRsubjectString = params.getUserName();
            CSRsubjectString = CSRsubjectString.trim();
            if (CSRsubjectString.contains("CN=") ||
                CSRsubjectString.contains("cn=")) {
                // If the MyProxy username is a DN, use it.
                if (CSRsubjectString.charAt(0) == '/') {
                    // "good enough" conversion of OpenSSL DN strings
                    CSRsubjectString = CSRsubjectString.substring(1);
                    CSRsubjectString = CSRsubjectString.replace('/', ',');
                }
            } else {
                CSRsubjectString = "CN="+CSRsubjectString;
            }

            X509Name CSRsubjectName;
            try {
                CSRsubjectName = new X509Name(CSRsubjectString);
            } catch (Exception e) {
                // If our X509Name construction fails for any reason,
                // just use a default value (as in the past).
                CSRsubjectName = new X509Name("CN=ignore");
            }

            if (logger.isDebugEnabled()) {
                logger.debug("CSR subject: " + CSRsubjectName.toString());
            }

            BouncyCastleCertProcessingFactory certFactory =
                BouncyCastleCertProcessingFactory.getDefault();

            byte [] req = null;
            req = certFactory.createCertificateRequest(CSRsubjectName,
                                                       "SHA1WithRSAEncryption",
                                                       keyPair);

            // send the request to server
            out.write(req);
            out.flush();

            // read the number of certificates
            int size = in.read();

            if (logger.isDebugEnabled()) {
                logger.debug("Reading " + size + " certs");
            }

            X509Certificate [] chain
                = new X509Certificate[size];

            for (int i=0;i<size;i++) {
                chain[i] = certFactory.loadCertificate(in);
                // DEBUG: display the cert names
                if (logger.isDebugEnabled()) {
                    logger.debug("Received cert: " + chain[i].getSubjectDN());
                }
            }

            // get the response
            handleReply(in);

            // make sure the private key belongs to the right public key
            // currently only works with RSA keys
            RSAPublicKey pkey   = (RSAPublicKey)chain[0].getPublicKey();
            RSAPrivateKey prkey = (RSAPrivateKey)keyPair.getPrivate();

            if (!pkey.getModulus().equals(prkey.getModulus())) {
                throw new MyProxyException("Private/Public key mismatch!");
            }

            X509Credential newCredential = null;

            newCredential = new X509Credential(keyPair.getPrivate(),
                                                 chain);

            return new GlobusGSSCredentialImpl(newCredential,
                                               GSSCredential.INITIATE_AND_ACCEPT);

        } catch(Exception e) {
            throw new MyProxyException("MyProxy get failed.", e);
        } finally {
            // close socket
            close(out, in, gsiSocket);
        }
    }

    /**
     * Retrieves trustroot information from the MyProxy server.
     *
     * @param  credential
     *         The local GSI credentials to use for authentication.
     *         Can be set to null if no local credentials.
     * @param  params
     *         The parameters for the get-trustroots operation.
     * @exception MyProxyException
     *         If an error occurred during the operation.
     */
    public void getTrustroots(GSSCredential credential,
                              GetTrustrootsParams params)
        throws MyProxyException {

        if (params == null) {
            throw new IllegalArgumentException("params == null");
        }

        if (credential == null) {
            try {
                credential = getAnonymousCredential();
            } catch (GSSException e) {
                throw new MyProxyException("Failed to create anonymous credentials", e);
            }
        }

        Socket gsiSocket = null;
        OutputStream out = null;
        InputStream in   = null;

        String msg = params.makeRequest();

        try {
            gsiSocket = getSocket(credential);

            if (credential.getName().isAnonymous()) {
                this.context.requestAnonymity(true);
            }

            out = gsiSocket.getOutputStream();
            in  = gsiSocket.getInputStream();

            if (!((GssSocket)gsiSocket).getContext().getConfState())
                throw new Exception("Confidentiality requested but not available");

            // send message
            out.write(msg.getBytes());
            out.flush();

            if (logger.isDebugEnabled()) {
                logger.debug("Req sent:" + params);
            }

            // get the response
            handleReply(in, out, null, true);

        } catch(Exception e) {
            throw new MyProxyException("MyProxy get-trustroots failed.", e);
        } finally {
            // close socket
            close(out, in, gsiSocket);
        }
    }

    /**
     * Bootstraps trustroot information from the MyProxy server.
     *
     * @exception MyProxyException
     *         If an error occurred during the operation.
     */
    public void bootstrapTrust() throws MyProxyException {
        try {
            SSLContext sc = SSLContext.getInstance("SSL");
            MyTrustManager myTrustManager = new MyTrustManager();
            TrustManager[] trustAllCerts = new TrustManager[] { myTrustManager };
            sc.init(null, trustAllCerts, new java.security.SecureRandom());
            SSLSocketFactory sf = sc.getSocketFactory();
            SSLSocket socket = (SSLSocket)sf.createSocket(this.host, this.port);
            socket.setEnabledProtocols(new String[] { "SSLv3" });
            socket.startHandshake();
            socket.close();

            X509Certificate[] acceptedIssuers = myTrustManager.getAcceptedIssuers();
            if (acceptedIssuers == null) {
                throw new MyProxyException("Failed to determine MyProxy server trust roots in bootstrapTrust.");
            }
            for (int idx = 0; idx < acceptedIssuers.length; idx++)
            {
                File x509Dir = new File(org.globus.myproxy.MyProxy.getTrustRootPath());
                if (!x509Dir.exists())
                {
                    StringBuffer newSubject = new StringBuffer();
                    String[] subjArr = acceptedIssuers[idx].getSubjectDN().getName().split(", ");
                    for(int i = (subjArr.length - 1); i > -1; i--)
                    {
                        newSubject.append("/");
                        newSubject.append(subjArr[i]);
                    }
                    String subject = newSubject.toString();

                    File tmpDir = new File(getTrustRootPath() + "-" +
                                           System.currentTimeMillis());
                    if (tmpDir.mkdir() == true)
                    {
                        String hash = opensslHash(acceptedIssuers[idx]);
                        String filename = tmpDir.getPath() + tmpDir.separator + hash + ".0";

                        FileOutputStream os = new FileOutputStream(new File(filename));
                        CertificateIOUtil.writeCertificate(os, acceptedIssuers[idx]);

                        os.close();
                        if (logger.isDebugEnabled()) {
                            logger.debug("wrote trusted certificate to " + filename);
                        }

                        filename = tmpDir.getPath() + tmpDir.separator + hash + ".signing_policy";

                        os = new FileOutputStream(new File(filename));
                        Writer wr = new OutputStreamWriter(os, Charset.forName("UTF-8"));
                        wr.write("access_id_CA X509 '");
                        wr.write(subject);
                        wr.write("'\npos_rights globus CA:sign\ncond_subjects globus \"*\"\n");
                        wr.flush();
                        wr.close();
                        os.close();

                        if (logger.isDebugEnabled()) {
                            logger.debug("wrote trusted certificate policy to " + filename);
                        }

                        // success.  commit the bootstrapped directory.
                        if (tmpDir.renameTo(x509Dir) == true)
                        {
                            if (logger.isDebugEnabled()) {
                                logger.debug("renamed " + tmpDir.getPath() + " to " +
                                             x509Dir.getPath());
                            }
                        }
                        else
                        {
                            throw new MyProxyException("Unable to rename " + tmpDir.getPath() + " to " +
                                                x509Dir.getPath());
                        }
                    }
                    else
                    {
                        throw new MyProxyException("Cannot create temporary directory: " + tmpDir.getName());
                    }
                }
            }
        } catch(Exception e) {
            throw new MyProxyException("MyProxy bootstrapTrust failed.", e);
        }
    }

    private static String readLine(InputStream is) throws IOException {
        StringBuffer sb = new StringBuffer();
        for (int c = is.read(); c > 0 && c != '\n'; c = is.read()) {
            sb.append((char) c);
        }
        if (sb.length() > 0) {
            if (logger.isDebugEnabled()) {
                logger.debug("Received line: " + sb);
            }
            return new String(sb);
        }
        return null;
    }

    private InputStream handleReply(InputStream in)
        throws IOException, MyProxyException {
        return handleReply(in, null, null, false);
    }


    private InputStream handleReply(InputStream in,
                                    OutputStream out,
                                    GSSCredential authzcreds,
                                    boolean wantTrustroots)
        throws IOException, MyProxyException {
        String tmp = null;

        /* there was something weird here with the
           received protocol version sometimes. it
           contains an extra <32 byte. fixed it by
           using endsWith. now i read extra byte at the
           end of each message.
        */

        // protocol version
        tmp = readLine(in);
        if (tmp == null) {
            throw new EOFException();
        }
        if (!tmp.endsWith(MyProxyConstants.VERSION)) {
            throw new MyProxyException("Protocol version mismatch: " + tmp);
        }

        // response
        tmp = readLine(in);
        if (tmp == null) {
            throw new EOFException();
        }

        if (!tmp.startsWith(RESPONSE)) {
            throw new MyProxyException("Invalid reply: no response message");
        }

        boolean error = tmp.charAt(RESPONSE.length()) == '1';
        boolean authzchallenge = tmp.charAt(RESPONSE.length()) == '2';

        if (error) {
            StringBuffer errorStr = new StringBuffer();
            while( (tmp = readLine(in)) != null ) {
                if (tmp.startsWith(ERROR)) {
                    if (errorStr.length() > 0) errorStr.append(' ');
                    errorStr.append(tmp.substring(ERROR.length()));
                }
            }
            if (errorStr.length() == 0) {
                errorStr.append("unspecified server error");
            }
            throw new MyProxyException(errorStr.toString());
        }
        if (authzchallenge) {
            if (authzcreds == null) {
                throw new MyProxyException("Unable to respond to server's authentication challenge. No credentials for renewal.");
            }
            if (out == null) {
                throw new MyProxyException("Internal error. Authz challenge but no OutputStream.");
            }
            String[] authzdata = null;
            while( (tmp = readLine(in)) != null ) {
                if (tmp.startsWith(AUTHZ_DATA)) {
                    int pos = tmp.indexOf(':', AUTHZ_DATA.length()+1);
                    if (pos != -1) {
                        authzdata = new String[2];
                        authzdata[0] =
                            tmp.substring(AUTHZ_DATA.length(), pos).trim();
                        authzdata[1] =
                            tmp.substring(pos+1).trim();
                    }
                    if (authzdata == null) {
                        throw new MyProxyException("Unable to parse authorization challenge from server.");
                    }
                    if (authzdata[0].equals("X509_certificate")) {
                        GlobusGSSCredentialImpl pkiCred =
                            (GlobusGSSCredentialImpl)authzcreds;
                        try {
                            Signature sig = Signature.getInstance("SHA1withRSA");
                            sig.initSign(pkiCred.getPrivateKey());
                            sig.update(authzdata[1].getBytes());
                            byte[] sigbytes = sig.sign();
                            X509Certificate [] certs =
                                pkiCred.getCertificateChain();
                            ByteArrayOutputStream buffer =
                                new ByteArrayOutputStream(2048);
                            buffer.write(2); // AUTHORIZETYPE_CERT
                            buffer.write(0); buffer.write(0); buffer.write(0); // pad
                            DataOutputStream dos = new DataOutputStream(buffer);
                            dos.writeInt(sigbytes.length);
                            dos.flush();
                            buffer.write(sigbytes);
                            buffer.write((byte)certs.length);
                            for (int i=0; i<certs.length; i++) {
                                buffer.write(certs[i].getEncoded());
                            }
                            out.write(buffer.toByteArray());
                            out.flush();
                        } catch(Exception e) {
                            throw new MyProxyException("Authz response failed.", e);
                        }
                    } else {
                        authzdata = null;
                        continue;
                    }
                }
            }

            return handleReply(in, out, authzcreds, wantTrustroots);

        }

        if (wantTrustroots == true) {
            while ((tmp = readLine(in)) != null) {
                if (tmp.startsWith(TRUSTROOTS)) {
                    String filenameList = tmp.substring(TRUSTROOTS.length());
                    this.trustrootFilenames = filenameList.split(",");
                    this.trustrootData = new String[this.trustrootFilenames.length];
                    for(int i = 0; i < this.trustrootFilenames.length; i++) {
                    String lineStart = "FILEDATA_" + this.trustrootFilenames[i]
                        + "=";
                    tmp = readLine(in);
                    if (tmp == null) {
                        throw new EOFException();
                    }
                    if (!tmp.startsWith(lineStart)) {
                        throw new MyProxyException(
                            "bad MyProxy protocol RESPONSE: expecting "
                            + lineStart + " but received " + tmp);
                    }
                    this.trustrootData[i] = new String(
                        Base64.decode(tmp.substring(lineStart.length()).getBytes()));
                    }
                }
            }
        }

        /* always consume the entire message */
        int avail = in.available();
        byte [] b = new byte[avail];
        if (avail > 0) in.read(b);

        ByteArrayInputStream inn = new ByteArrayInputStream(b);

        return inn;
    }

    private static void close(OutputStream out,
                              InputStream in,
                              Socket sock) {
        try {
            if (out != null) out.close();
            if (in  != null) in.close();
            if (sock != null) sock.close();
        } catch(IOException ee) {}
    }

    private static Authorization getAuthorization(String subjectDN) {
        if (subjectDN == null) {
            return new MyProxyServerAuthorization();
        } else {
            return new IdentityAuthorization(subjectDN);
        }
    }

    private GSSCredential getAnonymousCredential()
        throws GSSException {
        GSSManager manager = ExtendedGSSManager.getInstance();
        GSSName anonName = manager.createName((String)null, null);
        return manager.createCredential(anonName,
                                        GSSCredential.INDEFINITE_LIFETIME,
                                        (Oid)null,
                                        GSSCredential.INITIATE_AND_ACCEPT);
    }

    /**
     * Returns the trusted certificates directory location where
     * writeTrustRoots() will store certificates.
     * It first checks the X509_CERT_DIR system property.
     * If that property is not set, it uses
     * ${user.home}/.globus/certificates.
     * Note that, unlike CoGProperties.getCaCertLocations(),
     * it does not return /etc/grid-security/certificates or
     * ${GLOBUS_LOCATION}/share/certificates.
     */
    public static String getTrustRootPath() {
        String path = System.getProperty("X509_CERT_DIR");
        if (path == null) {
            path = System.getProperty("user.home") + TRUSTED_CERT_PATH;
        }
        return path;
    }

    /**
     * Writes the retrieved trust roots to the Globus trusted certificates
     * directory.
     * @return true if trust roots are written successfully, false if no
     *         trust roots are available to be written
     */
    public boolean writeTrustRoots() throws IOException {
        return writeTrustRoots(getTrustRootPath());
    }

    /**
     * Writes the retrieved trust roots to a trusted certificates directory.
     * @param directory
     *        path where the trust roots should be written
     * @return true if trust roots are written successfully, false if no
     *         trust roots are available to be written
     */
    public boolean writeTrustRoots(String directory) throws IOException {
        if (this.trustrootFilenames == null || this.trustrootData == null) {
            return false;
        }
        File rootDir = new File(directory);
        if (!rootDir.exists()) {
            rootDir.mkdirs();
        }
        for (int i = 0; i < trustrootFilenames.length; i++) {
            FileOutputStream out = new FileOutputStream(
                directory + File.separator + this.trustrootFilenames[i]);
            out.write(this.trustrootData[i].getBytes());
            out.close();
        }
        return true;
    }

    /*
      the following methods are based off code to compute the subject
      name hash from:
      http://blog.piefox.com/2008/10/javaopenssl-ca-generation.html
    */
    private static String opensslHash(X509Certificate cert) {
        try {
            return openssl_X509_NAME_hash(cert.getSubjectX500Principal());
        }
        catch (Exception e) {
            throw new Error("MD5 isn't available!", e);
        }
    }

    /**
     * Generates a hex X509_NAME hash (like openssl x509 -hash -in cert.pem)
     * Based on openssl's crypto/x509/x509_cmp.c line 321
     */
    private static String openssl_X509_NAME_hash(X500Principal p) throws Exception {
        // This code replicates OpenSSL's hashing function
        // DER-encode the Principal, MD5 hash it, then extract the first 4 bytes and reverse their positions
        byte[] derEncodedSubject = p.getEncoded();
        byte[] md5 = MessageDigest.getInstance("MD5").digest(derEncodedSubject);

        // Reduce the MD5 hash to a single unsigned long
        byte[] result = new byte[] { md5[3], md5[2], md5[1], md5[0] };
        return toHex(result);
    }

    // encode binary to hex
    private static String toHex(final byte[] bin) {
        if (bin == null || bin.length == 0)
            return "";

        char[] buffer = new char[bin.length * 2];

        final char[] hex = "0123456789abcdef".toCharArray();

        // i tracks input position, j tracks output position
        for (int i = 0, j = 0; i < bin.length; i++)
        {
            final byte b = bin[i];
            buffer[j++] = hex[(b >> 4) & 0x0F];
            buffer[j++] = hex[b & 0x0F];
        }
        return new String(buffer);
    }
}
