/******************************************************************************
 * JBoss, a division of Red Hat                                               *
 * Copyright 2010, Red Hat Middleware, LLC, and individual                    *
 * contributors as indicated by the @authors tag. See the                     *
 * copyright.txt in the distribution for a full listing of                    *
 * individual contributors.                                                   *
 *                                                                            *
 * This 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 (at your option) any later version.                        *
 *                                                                            *
 * This software 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 software; if not, write to the Free                *
 * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA         *
 * 02110-1301 USA, or see the FSF site: http://www.fsf.org.                   *
 ******************************************************************************/
package org.exoplatform.web.security.errorlogin;

import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;

import org.exoplatform.container.xml.InitParams;
import org.exoplatform.services.mail.MailService;
import org.exoplatform.services.log.Log;
import org.exoplatform.services.log.ExoLogger;

/**
 * Service can be used to track invalid login attempts of users and do some actions when some number of successive login
 * attempts is detected.
 *
 * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
 * @version $Revision$
 */
public class InvalidLoginAttemptsService {
  private static final Log                                log        = ExoLogger.getLogger(InvalidLoginAttemptsService.class);

    /**
     * If false, then feature of sending mails to administrator about bad login attempts is disabled.
     */
    private final Boolean sendMailEnabled;

    /**
     * Number of successive invalid login attempts of user, after the mail will be send.
     */
    private final Integer numberOfFailedAttempts;

    /**
     * Policy for indication that invalid login attempts are coming from same source. Valid values are SESSION,
     * SESSION_AND_USER, SERVER. SESSION is default and it means that login attempts are coming from same HTTP session.
     * SESSION_AND_USER is indicating login attempts of same user and from same HTTP session. SERVER means login attempts from
     * same remote server.
     */
    private InvalidLoginPolicy invalidLoginPolicy;

    /**
     * This will be used as 'from' header in admin mail
     */
    private final String mailFrom;

    /**
     * This should be admin e-mail address, where e-mail about invalid login attempts will be send.
     */
    private final String mailTo;

    /**
     * Subject of email about invalid login attempts.
     */
    private final String mailSubject;

    /**
     * Content of mail message, which will be send to administrator. Real content will be based on value of parameter
     * "invalidLoginPolicy". Tokens like ${username}, ${sessionId}, ${hostname} will be replaced with real values from attacker.
     */
    private final String mailMessage;

    /**
     * MailService injected by exo kernel.
     */
    private final MailService mailService;

    /**
     * Helper map to track login attempts from different users.
     */
    private final ConcurrentMap<InvalidAttemptKey, Integer> attemptMap = new ConcurrentHashMap<InvalidAttemptKey, Integer>();

    public InvalidLoginAttemptsService(InitParams params, MailService mailService) {
        this.mailService = mailService;
        this.sendMailEnabled = Boolean.valueOf(params.getValueParam("sendingMailsEnabled").getValue());
        this.numberOfFailedAttempts = Integer.parseInt(params.getValueParam("numberOfFailedAttempts").getValue());
        this.invalidLoginPolicy = InvalidLoginPolicy.valueOf(params.getValueParam("invalidLoginPolicy").getValue());
        this.mailFrom = params.getValueParam("mailFrom").getValue();
        this.mailTo = params.getValueParam("mailTo").getValue();
        this.mailSubject = params.getValueParam("mailSubject").getValue();
        this.mailMessage = params.getPropertiesParam("mailMessage").getProperty(invalidLoginPolicy.toString());
    }

    /**
     * This should be called each time when invalid login attempt is detected (typically from HttpFilter)
     *
     * @param sessionId
     * @param username
     * @param hostname
     */
    public void badLoginAttempt(String sessionId, String username, String hostname) {
        if (log.isDebugEnabled()) {
            log.debug(new StringBuilder("Detected invalid login attempt. Session id=").append(sessionId).append(", username=")
                    .append(username).append(", hostname=").append(hostname));
        }
        if (numberOfFailedAttempts == 1) {
            sendMail(sessionId, username, hostname);
            return;
        }

        InvalidAttemptKey key = InvalidAttemptKey.createKey(invalidLoginPolicy, sessionId, username, hostname);

        // TODO: better synchronization
        int currentCount = 1;
        if (attemptMap.containsKey(key)) {
            currentCount = attemptMap.get(key) + 1;
            if (currentCount == numberOfFailedAttempts) {
                sendMail(sessionId, username, hostname);
                attemptMap.remove(key);
            } else {
                attemptMap.put(key, currentCount);
            }
        } else {
            attemptMap.put(key, currentCount);
        }
    }

    /**
     * This should be called each time successful login attempt is detected. We need to use it to clean previous bad attempts
     * when we have good attempt.
     *
     * @param sessionId
     * @param username
     * @param hostname
     */
    public void successfulLoginAttempt(String sessionId, String username, String hostname) {
        if (log.isDebugEnabled()) {
            log.debug(new StringBuilder("Detected successful login attempt. Session id=").append(sessionId)
                    .append(", username=").append(username).append(", hostname=").append(hostname));
        }
        InvalidAttemptKey key = InvalidAttemptKey.createKey(invalidLoginPolicy, sessionId, username, hostname);
        attemptMap.remove(key);
    }

    /**
     * Allows to set custom invalidLoginPolicy
     *
     * @param invalidLoginPolicy
     */
    public void setInvalidLoginPolicy(InvalidLoginPolicy invalidLoginPolicy) {
        this.invalidLoginPolicy = invalidLoginPolicy;
    }

    private void sendMail(String sessionId, String username, String hostname) {
        // return if sending mails disabled in configuration.
        if (!sendMailEnabled) {
            if (log.isDebugEnabled()) {
                log.debug("Sending of mails disabled. Mail won't be send about invalid login attempts.");
            }
            return;
        }

        // replace tokens from configuration with real values.
        String result = mailMessage.replaceAll("\\$\\{sessionId\\}", sessionId);
        result = result.replaceAll("\\$\\{username\\}", username);
        result = result.replaceAll("\\$\\{hostname\\}", hostname);
        result = result.replaceAll("\\$\\{number\\}", String.valueOf(numberOfFailedAttempts));

        try {
            if (log.isDebugEnabled()) {
                log.debug("Sending mail about the invalid login attempts. Mail message is: " + result);
            }
            mailService.sendMessage(mailFrom, mailTo, mailSubject, result);
        } catch (Exception e) {
            // log exception but not throw it when sending of mail happen
            log.error("Error when sending mail to admin after detected invalid number of login attempts.", e);
        }
    }

}
