/**
 * JOnAS: Java(TM) Open Application Server
 * Copyright (C) 2006-2007 Bull S.A.S.
 * Contact: jonas-team@ow2.org
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or any later version.
 *
 * This library 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
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307
 * USA
 *
 * --------------------------------------------------------------------------
 * $Id$
 * --------------------------------------------------------------------------
 */

package org.ow2.jonas.security.auth.spi;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Hashtable;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.NamingException;
import javax.rmi.PortableRemoteObject;
import javax.security.auth.Subject;
import javax.security.auth.callback.Callback;
import javax.security.auth.callback.CallbackHandler;
import javax.security.auth.callback.NameCallback;
import javax.security.auth.callback.PasswordCallback;
import javax.security.auth.login.LoginException;
import javax.security.auth.spi.LoginModule;

import org.ow2.jonas.security.internal.JonasSecurityServiceImpl;
import org.ow2.jonas.security.internal.realm.factory.JResourceRemote;


/**
 * This Remote login module is used to run a JAAS authentication on the remote
 * side. This allows users to provide a simple LoginModule which run on the
 * server side and to share this login module from the client side (in this case
 * it use remotely the user login module)
 * @author Florent Benoit
 */
public class RemoteLoginModule implements LoginModule {

    /**
     * Default JOnAS server name (when asking remote server).
     */
    private static final String DEFAULT_SERVER_NAME = "jonas";

    /**
     * Subject used.
     */
    private Subject subject = null;

    /**
     * Remote subject returned for authentication.
     */
    private Subject remoteSubject = null;

    /**
     * The callbackhandler user for identification.
     */
    private CallbackHandler callbackHandler = null;

    /**
     * Options for this login module.
     */
    private Map options = null;

    /**
     * Password of the principal.
     */
    private String password = null;

    /**
     * Indicates if the login was successfull or not.
     */
    private boolean loginWasDoneWithSuccess = false;

    /**
     * Initialize this LoginModule. This method is called by the LoginContext
     * after this LoginModule has been instantiated. The purpose of this method
     * is to initialize this LoginModule with the relevant information. If this
     * LoginModule does not understand any of the data stored in sharedState or
     * options parameters, they can be ignored.
     * @param subject the Subject to be authenticated.
     * @param callbackHandler a CallbackHandler for communicating with the end
     *        user (prompting for usernames and passwords, for example).
     * @param sharedState state shared with other configured LoginModules.
     * @param options options specified in the login Configuration for this
     *        particular LoginModule.
     */
    public void initialize(Subject subject, CallbackHandler callbackHandler, Map sharedState, Map options) {
        this.subject = subject;
        this.callbackHandler = callbackHandler;
        this.options = options;
    }

    /**
     * Method to authenticate a Subject (phase 1). The implementation of this
     * method authenticates a Subject. For example, it may prompt for Subject
     * information such as a username and password and then attempt to verify
     * the password. This method saves the result of the authentication attempt
     * as private state within the LoginModule.
     * @return true if the authentication succeeded, or false if this
     *         LoginModule should be ignored.
     * @throws LoginException if the authentication fails
     */
    public boolean login() throws LoginException {

        // No handler
        if (callbackHandler == null) {
            throw new LoginException("No handler has been defined.");
        }

        // Name of the jaas entry to use on the server side
        String entryName = (String) options.get("entryName");

        // Get PROVIDER_URL property used to access to remote servers.
        // This property can be a comma separated list
        // If this is null, it will use the default setting
        String providerURLsProp = (String) options.get("providerURLs");
        List providerURLs = null;
        if (providerURLsProp != null) {
            String[] providerURLsArray = providerURLsProp.split(",");
            providerURLs = Arrays.asList(providerURLsArray);
        }


        // Name of the servers for retrieving the security service
        String serverNameProp = (String) options.get("serverName");
        String serverNamesProp = (String) options.get("serverNames");
        List<String> serverNames = new ArrayList<String>();

        // Build list of the servers
        if (serverNameProp == null && serverNamesProp == null) {
            serverNames.add(DEFAULT_SERVER_NAME);
        } else {
            // Append servers found
            if (serverNameProp != null) {
                serverNames.add(serverNameProp);
            }
            if (serverNamesProp != null) {
                String[] servNames = serverNamesProp.split(",");
                for (int i = 0; i < servNames.length; i++) {
                    serverNames.add(servNames[i]);
                }
            }
        }

        // No entry has been is specified -> fails
        if (entryName == null) {
            throw new LoginException(
                    "The 'entryName' argument is a required argument of this login module.");
        }


        // Build a list of resources object to be searched in the available registry
        List<String> resourceNames = new ArrayList<String>();
        Iterator<String> it = serverNames.iterator();
        while (it.hasNext()) {
            String sName= it.next();
            // Remove extra spaces
            sName = sName.trim();
            // add a suffix to each server name
            resourceNames.add(sName + JonasSecurityServiceImpl.REMOTE_RESOUCE);
        }

        // Get the resource and perform authentication
        try {

            JResourceRemote jResourceRemote = findRemoteResource(resourceNames, providerURLs);

            // Handle callbacks
            NameCallback nameCallback = new NameCallback("User :");
            // False to hide password when it is entered
            PasswordCallback passwordCallback = new PasswordCallback("Password :", false);
            Callback[] callbacks = new Callback[] {nameCallback, passwordCallback};
            callbackHandler.handle(callbacks);

            // Get values
            String principalName = nameCallback.getName();
            if (principalName == null) {
                throw new LoginException("A null username is not a valid username");
            }

            char[] arrayPass = passwordCallback.getPassword();
            if (arrayPass == null) {
                throw new LoginException("A null password is not a valid password");
            }

            password = new String(arrayPass);
            try {
                remoteSubject = jResourceRemote.authenticateJAAS(principalName, arrayPass, entryName);
            } catch (Exception e) {
                throw new LoginException("Cannot authenticate with principal name = '" + principalName + "' : " + e.getMessage());
            }
        } catch (Exception e) {
            throw new LoginException("Error during the login phase : " + e.getMessage());
        }
        loginWasDoneWithSuccess = true;
        return true;
    }


    /**
     * Gets a remote resource in order to ask a remote authentication.
     * @param resourceNames the names of the resources that should be in the registry.
     * @param userProviderURLs the list of PROVIDER_URL to use (empty/null = default)
     * @return the first resource found, else throw an exception
     * @throws LoginException if no resources are available.
     */
    protected JResourceRemote findRemoteResource(final List<String> resourceNames, final List<String> userProviderURLs)
            throws LoginException {

        // Search on each initial context, the given set of resources
        // If no resources are available, throw an exception.

        // Add a null providerURL if it is empty
        List<String> providerURLs = null;
        if (userProviderURLs == null || userProviderURLs.size() == 0) {
            providerURLs = new ArrayList<String>();
            providerURLs.add(null);
        } else {
            providerURLs = userProviderURLs;
        }

        //Get strategy from configuration file
        String strategy = (String) this.options.get("strategy");
        List<String> myproviderURLs = new ArrayList<String>(providerURLs);
        int index = 0;

        // Iterate on each context
        for (int i = 0; i < providerURLs.size(); i++) {
            //if strategy="random" is set, select a providerURL randomly
            if (strategy != null && strategy.equals("random")) {
                index = (int) (System.currentTimeMillis() % myproviderURLs.size());
            }
            String providerURL = myproviderURLs.get(index);
            myproviderURLs.remove(index);
            // Remove extra spaces.
            providerURL = providerURL.trim();

            // First, get a context
            Context ictx;
            try {
                ictx = getInitialContext(providerURL);
            } catch (NamingException e) {
                // try the next one
                continue;
            }

            // Loop on the given resource set.
            Iterator<String> itResourceNames = resourceNames.iterator();
            while (itResourceNames.hasNext()) {
                String remoteResourceName = itResourceNames.next();

                // Then search the given resources in this context
                JResourceRemote jResourceRemote = null;
                try {
                    Object o = ictx.lookup(remoteResourceName);
                    jResourceRemote = (JResourceRemote) PortableRemoteObject.narrow(o, JResourceRemote.class);
                } catch (NamingException ne) {
                    // name is not found, this may be ok.
                }

                // found a resource, return it
                if (jResourceRemote != null) {
                    return jResourceRemote;
                }
            }
        }

        // If we go here, it means that no resources have been found
        // --> it means error !
        StringBuffer sb = new StringBuffer();
        sb.append("The resources named '");
        Iterator<String> itResourceNames = resourceNames.iterator();
        while (itResourceNames.hasNext()) {
            sb.append(itResourceNames.next());
            if (itResourceNames.hasNext()) {
                sb.append(",");
            }
        }
        sb.append("' have not been found in the ");
        if (userProviderURLs == null) {
            sb.append("default InitialContext");
        } else {
            Iterator<String> itProv = userProviderURLs.iterator();
            sb.append("Inital Context with PROVIDER_URL = '");
            // Iterate on each context
            while (itProv.hasNext()) {
                sb.append(itProv.next());
                if (itProv.hasNext()) {
                    sb.append(",");
                }
            }
            sb.append("'");
        }
        sb.append(". Check that the server names are correct and that resources are bound.");
        throw new LoginException(sb.toString());
    }



    /**
     * Builds an Initial Context for the given providerURL.
     * @param providerURL the given URL to use. If null, use the default setting
     * @return an instance of Initial Context
     * @throws NamingException if no context can be built
     */
    protected Context getInitialContext(final String providerURL) throws NamingException {
        if (providerURL == null) {
            return new InitialContext();
        }
        // else use the given URL
        Hashtable<String, String> env = new Hashtable<String, String>();
        env.put(Context.PROVIDER_URL, providerURL);
        return new InitialContext(env);
    }

    /**
     * Method to commit the authentication process (phase 2). This method is
     * called if the LoginContext's overall authentication succeeded (the
     * relevant REQUIRED, REQUISITE, SUFFICIENT and OPTIONAL LoginModules
     * succeeded). If this LoginModule's own authentication attempt succeeded
     * (checked by retrieving the private state saved by the login method), then
     * this method associates relevant Principals and Credentials with the
     * Subject located in the LoginModule. If this LoginModule's own
     * authentication attempted failed, then this method removes/destroys any
     * state that was originally saved.
     * @return true if this method succeeded, or false if this LoginModule
     *         should be ignored.
     * @throws LoginException if the commit fails
     */
    public boolean commit() throws LoginException {

            //overall authentication succeeded
        if (loginWasDoneWithSuccess && remoteSubject != null) {
            // Add principal to the current subject
            subject.getPrincipals().addAll(remoteSubject.getPrincipals());
            subject.getPrivateCredentials().add(password);
        }
        return loginWasDoneWithSuccess;
    }

    /**
     * Method to abort the authentication process (phase 2). This method is
     * called if the LoginContext's overall authentication failed. (the relevant
     * REQUIRED, REQUISITE, SUFFICIENT and OPTIONAL LoginModules did not
     * succeed). If this LoginModule's own authentication attempt succeeded
     * (checked by retrieving the private state saved by the login method), then
     * this method cleans up any state that was originally saved.
     * @return true if this method succeeded, or false if this LoginModule
     *         should be ignored.
     * @throws LoginException if the abort fails
     */
    public boolean abort() throws LoginException {
        //overall authentication succeeded
        if (loginWasDoneWithSuccess && remoteSubject != null) {
            // Reset temp values
            remoteSubject = null;
        }
        return loginWasDoneWithSuccess;
    }

    /**
     * Method which logs out a Subject. An implementation of this method might
     * remove/destroy a Subject's Principals and Credentials.
     * @return true if this method succeeded, or false if this LoginModule
     *         should be ignored.
     * @throws LoginException if the logout fails
     */
    public boolean logout() throws LoginException {

        //overall authentication succeeded
        if (loginWasDoneWithSuccess && remoteSubject != null) {
            // Remove principal name
            subject.getPrincipals().remove(remoteSubject.getPrincipals());
        }

        return loginWasDoneWithSuccess;
    }

}