/*
 * Copyright (c) 2004 N2 Broadband, Inc.  All Rights Reserved.
 *
 * This module contains unpublished, confidential, proprietary
 * material.  The use and dissemination of this material are
 * governed by a license.  The above copyright notice does not
 * evidence any actual or intended publication of this material.
 *
 * Created: Apr 15, 2004
 */
package com.n2bb.realm;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.catalina.realm.GenericPrincipal;
import org.apache.catalina.realm.DataSourceRealm;
import org.apache.catalina.core.StandardServer;
import org.apache.catalina.ServerFactory;

import java.sql.*;
import java.security.Principal;
import java.util.Hashtable;
import java.util.Calendar;
import java.util.Arrays;
import javax.naming.Context;
import javax.naming.InitialContext;
import javax.sql.DataSource;
import javax.swing.JButton;

import com.n2bb.util.DigestUtil;
import com.n2bb.util.PropertyManager;

/**
 * Custom Tomcat security realm that adds user lockout after multiple
 * unsuccessful login attempts, and password expiration.
 * 
 * @author dhenderson
 * @author kmatsuoka
 * @version $Id: N2bbDataSourceRealm.java,v 1.2 2004/04/28 16:44:27 kmatsuoka
 *          Exp $
 */
public class N2bbDataSourceRealm extends DataSourceRealm {

	private static Log authLog = LogFactory.getLog("authLog");

	private static Hashtable lockHash = new Hashtable();

	class LoginBean {

		private long times[] = new long[4];

		private int attempts = 0;

		public LoginBean(long attemptTime) {
			addAttempt(attemptTime);
		}

		public synchronized void addAttempt(long attemptTime) {
			times[attempts++] = attemptTime;
		}

		public synchronized int getAttemptsWithinTimePeriod(long timePeriod) {
			authLog.debug("enter total attempts... " + attempts);
			authLog.debug("enter timePeriod... " + timePeriod);
			for (int i = 0; i < times.length; i++) {
				authLog.debug("enter times " + times[i]);
			}

			int attemptsInPeriod = 0;
			for (int i = attempts - 1; i >= 0; i--) {
				if (times[i] >= timePeriod) {
					authLog.debug("attempt found within time period at pos... "
							+ attemptsInPeriod);
					attemptsInPeriod++;
				} else {
					// these attempts were before the time period; remove them
					// and shift the other back
					int inPeriodAttempts = i + 1;
					int k = 0;
					for (int j = inPeriodAttempts; j < times.length; j++) {
						times[k++] = times[j];
					}
					attempts = attemptsInPeriod;
					break; // times are sorted
				}
			}

			for (int i = 0; i < times.length; i++) {
				authLog.debug("leave times " + times[i]);
			}
			authLog.debug("attemptsInPeriod... " + attemptsInPeriod);
			return attemptsInPeriod;
		}

	}

	/*
	 * override to handle user status
	 */
	public Principal authenticate(String username, String credentials) {
		authLog.debug("enter");

		Connection dbConnection = open();
		try {
						
			// Acquire a Principal object for this user
			Principal principal = super.authenticate(username, credentials);
			// N2BB start
			if (principal == null) {
				handleInvalidLogin(username, dbConnection);
			} else {
				authLog.info("Got valid password for username... " + username);
				lockHash.remove(username);
				if (isInactive(username, dbConnection)) {
					authLog.warn("Unfortunately, username " + username
							+ " is inactive.  " + "Login denied.");
					return null;
				}
				if (isPasswordExpired(dbConnection, username)) {
					principal = new GenericPrincipal(this,
							"chpass_" + username, "", Arrays
									.asList(new String[] { "chpass" }));
				}
			}
			// N2BB end
			// Return the Principal (if any)
			return principal;
		} catch (SQLException e) {
			// Log the problem for posterity
			// log(sm.getString("jdbcRealm.exception"), e);
			// Return "not authenticated" for this request
			return null;
		} finally {
			close(dbConnection);
		}

	}

	/**
	 * Handles invalid login. After enough invalid passwords for a given
	 * username, that username will be locked (unless it is admin).
	 * 
	 * @param username
	 * @param dbConnection
	 * @throws SQLException
	 */
	private void handleInvalidLogin(String username, Connection dbConnection)
			throws SQLException {
		authLog.warn("login FAILED for user name... " + username);
		if (!"admin".equals(username)) {

			LoginBean lb = (LoginBean) lockHash.get(username);
			// first bad attempt
			if (lb == null) {
				authLog.debug("adding first attempt for user name... "
						+ username);
				lb = new LoginBean(System.currentTimeMillis());
				lockHash.put(username, lb);
				// subsequent bad attempts
			} else {
				if (lb
						.getAttemptsWithinTimePeriod(System.currentTimeMillis() - 300000) >= 2) {
					// 300000ms = 5 minutes
					authLog.warn("setting user name inactive... " + username);
					lockHash.remove(username);
					setInactive(dbConnection, username);
				} else {
					authLog
							.debug("adding attempt for user name... "
									+ username);
					lb.addAttempt(System.currentTimeMillis());
				}
			}
		}
	}

	/**
	 * Checks if a user's password has expired.
	 * 
	 * @param dbConnection
	 *            database connection
	 * @param username
	 *            username
	 * @return true if user's password has expired
	 * @throws SQLException
	 */
	protected synchronized boolean isPasswordExpired(Connection dbConnection,
			String username) throws SQLException {
		int passwordLifeInDays = getPasswordLife();

		if (passwordLifeInDays <= 0) {
			return false;
		}

		Timestamp passwordModifiedDate = getPasswordModifiedDate(dbConnection,
				username);

		if (passwordModifiedDate == null) {
			return true;
		}

		Calendar expirationDate = Calendar.getInstance();
		expirationDate.setTime(passwordModifiedDate);
		expirationDate.add(Calendar.DAY_OF_YEAR, passwordLifeInDays);

		return new java.util.Date().after(expirationDate.getTime());
	}

	/**
	 * Gets password life, in days, from database.
	 * 
	 * @return password life, in days, or 0 if no expiration
	 */
	private int getPasswordLife() {
		return PropertyManager.getInstance().getInteger(
				PropertyManager.PASSWORD_LIFE_PROPERTY,
				PropertyManager.DEFAULT_PASSWORD_LIFE);
	}

	private void closeStatement(Statement stmt) {
		if (stmt != null) {
			try {
				stmt.close();
			} catch (SQLException ex) {
				// no-op
			}
		}
	}

	private Timestamp getPasswordModifiedDate(Connection dbConnection,
			String username) {
		String sql = "select PASSWORD_MODIFIED_DATE " + " from " + userTable
				+ " where " + userNameCol + " = ?";
		PreparedStatement pstmt = null;
		Timestamp passwordModifiedDate = null;
		try {
			pstmt = dbConnection.prepareStatement(sql);
			pstmt.setString(1, username);

			ResultSet rs = pstmt.executeQuery();
			passwordModifiedDate = null;
			if (rs.next()) {
				passwordModifiedDate = rs.getTimestamp(1);
			}
		} catch (SQLException e) {
			authLog.error("Unable to get password modified date for "
					+ username, e);
		} finally {
			closeStatement(pstmt);
		}
		return passwordModifiedDate;
	}

	protected boolean isInactive(String username, Connection dbConnection)
			throws SQLException {
		authLog.debug("enter user name... " + username);

		PreparedStatement pstmt = dbConnection.prepareStatement("select status"
				+ " from " + userTable + " where " + userNameCol + " = ?");
		pstmt.setString(1, username);
		ResultSet rs = pstmt.executeQuery();
		if (rs.next()) {
			return rs.getInt(1) == 0;
		} else {
			authLog.warn("isInactive failed to find user.  Shouldn't happen!");
			return true; // default to inactive
		}
	}

	protected synchronized void setInactive(Connection dbConnection,
			String username) throws SQLException {
		// shouldn't come here if not a valid username
		authLog.debug("enter user name... " + username);
		PreparedStatement stmt = dbConnection.prepareStatement("UPDATE "
				+ userTable + " set status=0 " + " WHERE " + userNameCol
				+ " = ?");
		stmt.setString(1, username);
		stmt.executeUpdate();
		authLog.debug("leave");
	}

	/**
	 * Open the specified database connection. Copied from
	 * <code>DataSourceRealm</code> because it had private access there.
	 * 
	 * @return Connection to the database
	 */
	protected Connection open() {

		try {
			
			StandardServer server = (StandardServer) ServerFactory.getServer();
			// Commented as JBoss level DataSource object being used
//			 Context context = server.getGlobalNamingContext();
//			 DataSource dataSource =
//			 (DataSource)context.lookup(dataSourceName);
			Context context = new InitialContext();
			DataSource dataSource = (DataSource) context
					.lookup(dataSourceName);
			return dataSource.getConnection();
		} catch (Exception e) {
			e.printStackTrace();
			// Log the problem for posterity
			// log(sm.getString("dataSourceRealm.exception"), e);
		}
		return null;
	}

	/**
	 * Close the specified database connection. Copied from
	 * <code>DataSourceRealm</code> because it had private access there.
	 * 
	 * @param dbConnection
	 *            The connection to be closed
	 */
	protected void close(Connection dbConnection) {
		// Do nothing if the database connection is already closed
		if (dbConnection == null)
			return;

		// Close this database connection, and log any errors
		try {
			dbConnection.close();
		} catch (SQLException e) {
			// log(sm.getString("dataSourceRealm.close"), e); // Just log it
			// here
		}

	}

}
