/*
 * Portions Copyright 2000-2006 Sun Microsystems, Inc.  All Rights Reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 * This code is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 2 only, as
 * published by the Free Software Foundation.  Sun designates this
 * particular file as subject to the "Classpath" exception as provided
 * by Sun in the LICENSE file that accompanied this code.
 *
 * This code is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 * version 2 for more details (a copy is included in the LICENSE file that
 * accompanied this code).
 *
 * You should have received a copy of the GNU General Public License version
 * 2 along with this work; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * Please contact Sun Microsystems, Inc., 4150 Network Circle, Santa Clara,
 * CA 95054 USA or visit www.sun.com if you need additional information or
 * have any questions.
 */

/*
 *  (C) Copyright IBM Corp. 1999 All Rights Reserved.
 *  Copyright 1997 The Open Group Research Institute.  All rights reserved.
 */

package sun.security.krb5;

import sun.security.krb5.internal.Krb5;
import sun.security.krb5.internal.UDPClient;
import sun.security.krb5.internal.TCPClient;
import java.io.IOException;
import java.io.InterruptedIOException;
import java.net.SocketTimeoutException;
import java.net.UnknownHostException;
import java.util.StringTokenizer;
import java.security.AccessController;
import java.security.PrivilegedExceptionAction;
import java.security.PrivilegedActionException;

public abstract class KrbKdcReq {

    /**
     * Default port for a KDC.
     */
    private static final int DEFAULT_KDC_PORT = Krb5.KDC_INET_DEFAULT_PORT;

    // Currently there is no option to specify retries
    // in the kerberos configuration file

    private static final int DEFAULT_KDC_RETRY_LIMIT = Krb5.KDC_RETRY_LIMIT;

    /**
     * Default timeout period when requesting a ticket from a KDC.
     * If not specified in the configuration file,
     * a value of 30 seconds is used.
     */
    public static final int DEFAULT_KDC_TIMEOUT; // milliseconds

    private static final boolean DEBUG = Krb5.DEBUG;

    private static int udpPrefLimit = -1;

    static {

        /*
         * Get default timeout.
         */

        int timeout = -1;
        try {
            Config cfg = Config.getInstance();
            String temp = cfg.getDefault("kdc_timeout", "libdefaults");
            timeout = parsePositiveIntString(temp);
            temp = cfg.getDefault("udp_preference_limit", "libdefaults");
            udpPrefLimit = parsePositiveIntString(temp);
        } catch (Exception exc) {
           // ignore any exceptions; use the default time out values
           if (DEBUG) {
                System.out.println ("Exception in getting kdc_timeout value, " +
                                    "using default value " +
                                    exc.getMessage());
           }
        }

        if (timeout > 0)
            DEFAULT_KDC_TIMEOUT = timeout;
        else
            DEFAULT_KDC_TIMEOUT = 30*1000; // 30 seconds
    }

    protected byte[] obuf;
    protected byte[] ibuf;

    /**
     * Sends the provided data to the KDC of the specified realm.
     * Returns the response from the KDC.
     * Default realm/KDC is used if realm is null.
     * @param realm the realm of the KDC where data is to be sent.
     * @returns the kdc to which the AS request was sent to
     * @exception InterruptedIOException if timeout expires
     * @exception KrbException
     */

    public String send(String realm)
        throws IOException, KrbException {
        boolean useTCP = (udpPrefLimit > 0 &&
             (obuf != null && obuf.length > udpPrefLimit));

        return (send(realm, useTCP));
    }

    public String send(String realm, boolean useTCP)
        throws IOException, KrbException {

        if (obuf == null)
            return null;
        Exception savedException = null;
        Config cfg = Config.getInstance();

        if (realm == null) {
            realm = cfg.getDefaultRealm();
            if (realm == null) {
                throw new KrbException(Krb5.KRB_ERR_GENERIC,
                                       "Cannot find default realm");
            }
        }

        /*
         * Get timeout.
         */

        int timeout = getKdcTimeout(realm);

        String kdcList = cfg.getKDCList(realm);
        if (kdcList == null) {
            throw new KrbException("Cannot get kdc for realm " + realm);
        }
        String tempKdc = null; // may include the port number also
        StringTokenizer st = new StringTokenizer(kdcList);
        while (st.hasMoreTokens()) {
            tempKdc = st.nextToken();
            try {
                send(realm,tempKdc,useTCP);
                break;
            } catch (Exception e) {
                savedException = e;
            }
        }
        if (ibuf == null && savedException != null) {
            if (savedException instanceof IOException) {
                throw (IOException) savedException;
            } else {
                throw (KrbException) savedException;
            }
        }
        return tempKdc;
    }

    // send the AS Request to the specified KDC

    public void send(String realm, String tempKdc, boolean useTCP)
        throws IOException, KrbException {

        if (obuf == null)
            return;
        PrivilegedActionException savedException = null;
        int port = Krb5.KDC_INET_DEFAULT_PORT;

        /*
         * Get timeout.
         */
        int timeout = getKdcTimeout(realm);
        /*
         * Get port number for this KDC.
         */
        StringTokenizer strTok = new StringTokenizer(tempKdc, ":");
        String kdc = strTok.nextToken();
        if (strTok.hasMoreTokens()) {
            String portStr = strTok.nextToken();
            int tempPort = parsePositiveIntString(portStr);
            if (tempPort > 0)
                port = tempPort;
        }

        if (DEBUG) {
            System.out.println(">>> KrbKdcReq send: kdc=" + kdc
                               + (useTCP ? " TCP:":" UDP:")
                               +  port +  ", timeout="
                               + timeout
                               + ", number of retries ="
                               + DEFAULT_KDC_RETRY_LIMIT
                               + ", #bytes=" + obuf.length);
        }

        KdcCommunication kdcCommunication =
            new KdcCommunication(kdc, port, useTCP, timeout, obuf);
        try {
            ibuf = AccessController.doPrivileged(kdcCommunication);
            if (DEBUG) {
                System.out.println(">>> KrbKdcReq send: #bytes read="
                        + (ibuf != null ? ibuf.length : 0));
            }
        } catch (PrivilegedActionException e) {
            Exception wrappedException = e.getException();
            if (wrappedException instanceof IOException) {
                throw (IOException) wrappedException;
            } else {
                throw (KrbException) wrappedException;
            }
        }
        if (DEBUG) {
            System.out.println(">>> KrbKdcReq send: #bytes read="
                               + (ibuf != null ? ibuf.length : 0));
        }
    }

    private static class KdcCommunication
        implements PrivilegedExceptionAction<byte[]> {

        private String kdc;
        private int port;
        private boolean useTCP;
        private int timeout;
        private byte[] obuf;

        public KdcCommunication(String kdc, int port, boolean useTCP,
                                int timeout, byte[] obuf) {
            this.kdc = kdc;
            this.port = port;
            this.useTCP = useTCP;
            this.timeout = timeout;
            this.obuf = obuf;
        }

        // The caller only casts IOException and KrbException so don't
        // add any new ones!

        public byte[] run() throws IOException, KrbException {

            byte[] ibuf = null;

            if (useTCP) {
                TCPClient kdcClient = new TCPClient(kdc, port);
                try {
                    /*
                     * Send the data to the kdc.
                     */
                    kdcClient.send(obuf);
                    /*
                     * And get a response.
                     */
                    ibuf = kdcClient.receive();
                } finally {
                    kdcClient.close();
                }

            } else {
                // For each KDC we try DEFAULT_KDC_RETRY_LIMIT (3) times to
                // get the response
                for (int i=1; i <= DEFAULT_KDC_RETRY_LIMIT; i++) {
                    UDPClient kdcClient = new UDPClient(kdc, port, timeout);

                    if (DEBUG) {
                        System.out.println(">>> KDCCommunication: kdc=" + kdc
                               + (useTCP ? " TCP:":" UDP:")
                               +  port +  ", timeout="
                               + timeout
                               + ",Attempt =" + i
                               + ", #bytes=" + obuf.length);
                    }
                    try {
                        /*
                         * Send the data to the kdc.
                         */

                        kdcClient.send(obuf);

                        /*
                         * And get a response.
                         */
                        try {
                            ibuf = kdcClient.receive();
                            break;
                        } catch (SocketTimeoutException se) {
                            if (DEBUG) {
                                System.out.println ("SocketTimeOutException with " +
                                                    "attempt: " + i);
                            }
                            if (i == DEFAULT_KDC_RETRY_LIMIT) {
                                ibuf = null;
                                throw se;
                            }
                        }
                    } finally {
                        kdcClient.close();
                    }
                }
            }
            return ibuf;
        }
    }

    /**
     * Returns a timeout value for the KDC of the given realm.
     * A KDC-specific timeout, if specified in the config file,
     * overrides the default timeout (which may also be specified
     * in the config file). Default timeout is returned if null
     * is specified for realm.
     * @param realm the realm which kdc's timeout is requested
     * @return KDC timeout
     */
    private int getKdcTimeout(String realm)
    {
        int timeout = DEFAULT_KDC_TIMEOUT;

        if (realm == null)
            return timeout;

        int tempTimeout = -1;
        try {
            String temp =
               Config.getInstance().getDefault("kdc_timeout", realm);
            tempTimeout = parsePositiveIntString(temp);
        } catch (Exception exc) {
        }

        if (tempTimeout > 0)
            timeout = tempTimeout;

        return timeout;
    }

    private static int parsePositiveIntString(String intString)
    {
        if (intString == null)
            return -1;

        int ret = -1;

        try {
            ret = Integer.parseInt(intString);
        } catch (Exception exc) {
            return -1;
        }

        if (ret >= 0)
            return ret;

        return -1;
    }
}
