/**
 * $RCSfile$
 * $Revision: 1765 $
 * $Date: 2005-08-10 22:37:59 -0700 (Wed, 10 Aug 2005) $
 *
 * Copyright (C) 2005-2008 Jive Software. All rights reserved.
 *
 * 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.jivesoftware.openfire.auth;

import java.io.File;
import java.lang.reflect.Field;
import java.net.URL;

import org.jivesoftware.openfire.XMPPServer;
import org.jivesoftware.openfire.user.NativeUserProvider;
import org.jivesoftware.openfire.user.UserAlreadyExistsException;
import org.jivesoftware.openfire.user.UserManager;
import org.jivesoftware.openfire.user.UserNotFoundException;
import org.jivesoftware.openfire.user.UserProvider;
import org.jivesoftware.util.JiveGlobals;
import org.jivesoftware.util.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.cenqua.shaj.Shaj;

/**
 * Authenticates using the native operating system authentication method. On Windows,
 * this means Win32 authentication; on Unix/Linux, PAM authentication. New user accounts
 * will be created automatically as needed.<p>
 *
 * Authentication is handled using the <a href="http://opensource.cenqua.com/shaj">Shaj</a>
 * library. In order for this provider to work, the appropriate native library must be loaded.
 * The appropriate native library must be manually moved from the resources/nativeAuth
 * directory to the lib directory.<p>
 *
 * To enable this provider, set the following in the system properties:
 *
 * <ul>
 * <li><tt>provider.auth.className = org.jivesoftware.openfire.auth.NativeAuthProvider</tt></li>
 * <li><tt>provider.user.className = org.jivesoftware.openfire.user.NativeUserProvider</tt></li>
 * </ul>
 *
 * The properties to configure the provider are as follows:
 *
 * <ul>
 *      <li>nativeAuth.domain -- <i> on Windows, the domain to use for authentication.
 *      If the value is not set, the machine's default domain will be used or standard OS
 *      auth will be used if the machine is not part of a domain. On Unix/Linux, this
 *      value specifies the PAM module to use for authentication. If the value is not set,
 *      the PAM module "other" will be used.</i></li>
 * </ul>
 *
 * For more information about configuring the domain value and other aspects of Shaj,
 * please see: <a href="http://opensource.cenqua.com/shaj/doc.html">
 * http://opensource.cenqua.com/shaj/doc.html</a>.
 *
 * @author Matt Tucker
 */
public class NativeAuthProvider implements AuthProvider {

	private static final Logger Log = LoggerFactory.getLogger(NativeAuthProvider.class);

    private String domain;

    public NativeAuthProvider() {
        // Convert XML based provider setup to Database based
        JiveGlobals.migrateProperty("nativeAuth.domain");

        this.domain = JiveGlobals.getProperty("nativeAuth.domain");

        // Configure the library path so that we can load the shaj native library
        // from the Openfire lib directory.
        // Find the root path of this class.
        try {
            String binaryPath = (new URL(Shaj.class.getProtectionDomain()
                    .getCodeSource().getLocation(), ".")).openConnection()
                    .getPermission().getName();
            binaryPath = (new File(binaryPath)).getCanonicalPath();

            // Add the binary path to "java.library.path".
            String newLibPath = binaryPath + File.pathSeparator +
                    System.getProperty("java.library.path");
            System.setProperty("java.library.path", newLibPath);
            Field fieldSysPath = ClassLoader.class.getDeclaredField("sys_paths");
            fieldSysPath.setAccessible(true);
            fieldSysPath.set(System.class.getClassLoader(), null);
        }
        catch (Exception e) {
            Log.error(e.getMessage(), e);
        }

        // Configure Shaj to log output to the Openfire logger.
        com.cenqua.shaj.log.Log.Factory.setInstance(new com.cenqua.shaj.log.Log() {
            @Override
            public boolean isDebug() {
                return Log.isDebugEnabled();
            }

            @Override
            public void error(String string) {
                Log.error(string);
            }

            @Override
            public void error(String string, Throwable throwable) {
                Log.error(string, throwable);
            }

            @Override
            public void debug(String string) {
                Log.debug("NativeAuthProvider: "+string);
            }
        });
    }

    @Override
    public void authenticate(String username, String password) throws UnauthorizedException {
        if (username.contains("@")) {
            // Check that the specified domain matches the server's domain
            int index = username.indexOf("@");
            String domain = username.substring(index + 1);
            if (domain.equals(XMPPServer.getInstance().getServerInfo().getXMPPDomain())) {
                username = username.substring(0, index);
            } else {
                // Unknown domain. Return authentication failed.
                throw new UnauthorizedException();
            }
        }
        try {
            // Some native authentication mechanisms appear to not handle high load
            // very well. Therefore, synchronize access to Shaj to throttle auth checks.
            synchronized (this) {
                if (!Shaj.checkPassword(domain, username, password)) {
                    throw new UnauthorizedException();
                }
            }
        }
        catch (UnauthorizedException ue) {
            throw ue;
        }
        catch (Exception e) {
            throw new UnauthorizedException(e);
        }

        // See if the user exists in the database. If not, automatically create them.
        UserManager userManager = UserManager.getInstance();
        try {
            userManager.getUser(username);
        }
        catch (UserNotFoundException unfe) {
            try {
                Log.debug("Automatically creating new user account for " + username);
                // Create user; use a random password for better safety in the future.
                // Note that we have to go to the user provider directly -- because the
                // provider is read-only, UserManager will usually deny access to createUser.
                UserProvider provider = UserManager.getUserProvider();
                if (!(provider instanceof NativeUserProvider)) {
                    Log.error("Error: not using NativeUserProvider so authentication with " +
                            "NativeAuthProvider will likely fail. Using: " +
                            provider.getClass().getName());
                }
                UserManager.getUserProvider().createUser(username, StringUtils.randomString(8),
                        null, null);
            }
            catch (UserAlreadyExistsException uaee) {
                // Ignore.
            }
        }
    }

    @Override
    public void authenticate(String username, String token, String digest)
            throws UnauthorizedException
    {
        throw new UnsupportedOperationException();
    }

    @Override
    public boolean isPlainSupported() {
        return true;
    }

    @Override
    public boolean isDigestSupported() {
        return false;
    }

    @Override
    public String getPassword(String username)
            throws UserNotFoundException, UnsupportedOperationException
    {
        throw new UnsupportedOperationException();    
    }

    @Override
    public void setPassword(String username, String password) throws UserNotFoundException {
        throw new UnsupportedOperationException();
    }

    @Override
    public boolean supportsPasswordRetrieval() {
        return false;
    }

    @Override
    public boolean isScramSupported() {
        // TODO Auto-generated method stub
        return false;
    }
}
