package org.jasig.cas.web.support;

import java.math.BigInteger;
import java.util.HashMap;
import java.util.Map;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;


public class ThrottledSubmissionByIpAddrUsernameHandlerInterceptorAdapter extends HandlerInterceptorAdapter implements InitializingBean {

    private final Log log = LogFactory.getLog(getClass());

    //*********************************************************************
    // Constants
    
	/**
	* The number of failed login attempts to allow before locking out
	* the source IP address.  (Note that failed login attempts "expire"
	* regularly.)
	* 
	* The max threshold will allow an idled IP access after 15 minutes
	*/
	private static final int FAILURE_THRESHOLD = 7;  // limit plus one
	
    /**
     * The interval to wait before expiring recorded failure attempts.
     * Currently set at 60 seconds - runs once each minute
     */
    private static final int FAILURE_TIMEOUT = 60;
	  
    /**
     * The interval to wait before allowing an idle IP back.
     * Currently set at 15 minutes
     */
    private static final int BLOCK_TIMEOUT = 15;
    
    //*********************************************************************
    //Properties
    
    /** Max threshold calculated as addition of blockTimeout and failureThreshhold. */
    private int FAILURE_THRESHOLD_MAX = FAILURE_THRESHOLD + BLOCK_TIMEOUT;
    
    /** The threshold before we stop someone from authenticating. */
    private int failureThreshhold = FAILURE_THRESHOLD;
    
    /** The failure timeout before we clean up one failure attempt. */
    int failureTimeout = FAILURE_TIMEOUT;
    
    /** The time to block user before allowing to start authenticating again. */
    private int blockTimeout = BLOCK_TIMEOUT;
    

    public void setFailureThreshhold(final int failureThreshhold) {
        this.failureThreshhold = failureThreshhold;
    }
    
    public void setFailureTimeout(final int failureTimeout) {
        this.failureTimeout = failureTimeout;
    }
    
    public void setBlockTimeout(final int blockTimeout) {
        this.blockTimeout = blockTimeout;
        this.FAILURE_THRESHOLD_MAX = failureThreshhold + blockTimeout;
    }
    
    //*********************************************************************
	    

    /** Map of offenders to the number of their offenses. */
    private HashMap<String,Integer> restrictedUsersMap = new HashMap<String,Integer>();

    protected final static class ExpirationThread extends Thread {

        /** Reference to the map of restricted users. */
        private Map<String, Integer> restrictedUsersMap;

        /** The timeout failure. */
        private int failureTimeout;

        public ExpirationThread(final Map<String, Integer> restrictedUsersMap, final int failureTimeout) {
            this.restrictedUsersMap = restrictedUsersMap;
            this.failureTimeout = failureTimeout;
        }

        public void run() {
            while (true) {
                try {
                    Thread.sleep(this.failureTimeout * 1000);
                    cleanUpFailures();
                } catch (final InterruptedException e) {
                    // ignore
                }
            }
        }

        private synchronized void cleanUpFailures() {
        	Map<String, Integer> map = this.restrictedUsersMap;
            for (final String key : map.keySet()) {
	            final Integer integer = map.get(key);
	            final int newValue = integer.intValue() - 1;
	
	            if (newValue == 0) {
	                map.remove(key);
	            } else {
	                map.put(key, Integer.valueOf(newValue));
	            }
	            System.out.println("In ThrottledSubmissionByIpAddrUsernameHandlerInterceptorAdapter, cleanUpFailures() key : "+ key +" newValue: " + newValue);
            }//end of for
        }//end of cleanUpFailures()
        
    }//end of ExpirationThread
    
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
    	log.info("In ThrottledSubmissionByIpAddrUsernameHandlerInterceptorAdapter, in preHandle() ");

        final String remoteAddr = request.getRemoteAddr();
        final String username = request.getParameter("username");
      	String faliureId = remoteAddr + ":" + username;
    	int failures = 0;
    	if(restrictedUsersMap.get(faliureId) != null){
    		failures = ((Integer)restrictedUsersMap.get(faliureId)).intValue();
    	}
    	log.info("In ThrottledSubmissionByIpAddrUsernameHandlerInterceptorAdapter, in preHandle() faliureId = " + faliureId + " faliures= " + failures);
    	if ( failures >= failureThreshhold ){
    		response.sendRedirect(request.getContextPath() + "/locked");
    		return false;
    	}
    	return true;
    }

    public synchronized void postHandle(final HttpServletRequest request,
        final HttpServletResponse response, final Object handler,
        final ModelAndView modelAndView) throws Exception {

        final String remoteAddr = request.getRemoteAddr();
        final String username = request.getParameter("username");
      	String faliureId = remoteAddr + ":" + username;
        //log.info("In ThrottledSubmissionByIpAddrUsernameHandlerInterceptorAdapter, modelAndView.getViewName(): " + modelAndView.getViewName());
        //log.info("In ThrottledSubmissionByIpAddrUsernameHandlerInterceptorAdapter, request.getMethod() : " + request.getMethod());

    	int failures = 0;
    	
    	if(restrictedUsersMap.get(faliureId) != null){
    		failures = ((Integer)restrictedUsersMap.get(faliureId)).intValue();
    	}

    	log.info("In ThrottledSubmissionByIpAddrUsernameHandlerInterceptorAdapter, in postHandle() faliureId = " + faliureId + " faliures= " + failures);
    	
    	log.info("In ThrottledSubmissionByIpAddrUsernameHandlerInterceptorAdapter, restrictedUsersMap size: " + restrictedUsersMap.size());
    	for (final String key : restrictedUsersMap.keySet()) {
    		log.info("Key : " + key+ " value: " + restrictedUsersMap.get(key));
  		}
    	String requestMethod = request.getMethod();
    	log.info("In ThrottledSubmissionByIpAddrUsernameHandlerInterceptorAdapter, requestMethod: " + requestMethod);
    	String viewName = modelAndView.getViewName();
    	log.info("In ThrottledSubmissionByIpAddrUsernameHandlerInterceptorAdapter, viewName: " + viewName);
        
    	if (requestMethod.equals("GET") || modelAndView.getViewName() == null || !viewName.equals("casLoginView")) {
        	log.info("In ThrottledSubmissionByIpAddrUsernameHandlerInterceptorAdapter, inside if. returning to view: " + modelAndView.getViewName());
          	return;
        }
       
        try {
        	failures = failures + 1;
            
            if ( failures == failureThreshhold ) {            	
             	failures = FAILURE_THRESHOLD_MAX;  // force 15 minute delay
                log.warn("In ThrottledSubmissionByIpAddressHandlerInterceptorAdapter, Setting Lockout for failureID=[" + faliureId + "]");                
            } else if ( failures > FAILURE_THRESHOLD_MAX ) {  //  does count exceed 15 minute limit
            	failures = FAILURE_THRESHOLD_MAX;      //  keep it at 15 minutes (or so) for lockout
            }
            
            restrictedUsersMap.put(faliureId, new Integer(failures));
            
            if (failures > failureThreshhold) {
            	log.warn("In ThrottledSubmissionByIpAddressHandlerInterceptorAdapter, Possible hacking attack from " + remoteAddr 
            			+ " with username = " + username +". More than " + this.failureThreshhold + " failed login attempts");
            			//+ " within " + this.failureTimeout + " seconds.");
            	modelAndView.setViewName("casFailureAuthenticationThreshhold");
            }
        } catch (NumberFormatException e) {
            log.warn("Skipping ip-address blocking. Possible reason: IPv6 Address not supported: " + e.getMessage());
        } catch (Exception ex) {
            log.error(ex.getMessage());
        }
    }

    public void afterPropertiesSet() throws Exception {
        final Thread thread = new ExpirationThread(this.restrictedUsersMap, this.failureTimeout);
        thread.setDaemon(true);
        thread.start();
    }
}
