/*
* Copyright (C) 2009-2017 Slava Semushin <slava.semushin@gmail.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
package ru.mystamps.web.service;
import java.util.Date;
import javax.servlet.http.HttpServletRequest;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.Validate;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.scheduling.annotation.Async;
import org.springframework.transaction.annotation.Transactional;
import lombok.RequiredArgsConstructor;
import ru.mystamps.web.Db;
import ru.mystamps.web.dao.SuspiciousActivityDao;
import ru.mystamps.web.dao.dto.AddSuspiciousActivityDbDto;
import ru.mystamps.web.support.spring.security.SecurityContextUtils;
@RequiredArgsConstructor
public class SiteServiceImpl implements SiteService {
// see initiate-suspicious_activities_types-table changeset
// in src/main/resources/liquibase/initial-state.xml
public static final String PAGE_NOT_FOUND = "PageNotFound";
public static final String AUTHENTICATION_FAILED = "AuthenticationFailed";
// see add-types-for-csrf-tokens-to-suspicious_activities_types-table changeset
// in src/main/resources/liquibase/version/0.4/2016-02-19--csrf_events.xml
public static final String MISSING_CSRF_TOKEN = "MissingCsrfToken";
public static final String INVALID_CSRF_TOKEN = "InvalidCsrfToken";
private static final Logger LOG = LoggerFactory.getLogger(SiteServiceImpl.class);
private final SuspiciousActivityDao suspiciousActivities;
@Override
@SuppressWarnings("PMD.UseObjectForClearerAPI")
@Async
@Transactional
public void logAboutAbsentPage(
String page,
String method,
Integer userId,
String ip,
String referer,
String agent) {
logEvent(PAGE_NOT_FOUND, page, method, userId, ip, referer, agent, new Date());
}
@Override
@SuppressWarnings("PMD.UseObjectForClearerAPI")
@Transactional
public void logAboutFailedAuthentication(
String page,
String method,
Integer userId,
String ip,
String referer,
String agent,
Date date) {
logEvent(AUTHENTICATION_FAILED, page, method, userId, ip, referer, agent, date);
}
/**
* @author Sergey Chechenev
*/
@Override
@Transactional
public void logAboutMissingCsrfToken(HttpServletRequest request) {
logEvent(
MISSING_CSRF_TOKEN,
request.getRequestURI(),
request.getMethod(),
SecurityContextUtils.getUserId(),
request.getRemoteAddr(),
request.getHeader("referer"),
request.getHeader("user-agent"),
new Date()
);
}
/**
* @author Sergey Chechenev
*/
@Override
@Transactional
public void logAboutInvalidCsrfToken(HttpServletRequest request) {
logEvent(
INVALID_CSRF_TOKEN,
request.getRequestURI(),
request.getMethod(),
SecurityContextUtils.getUserId(),
request.getRemoteAddr(),
request.getHeader("referer"),
request.getHeader("user-agent"),
new Date()
);
}
// protected for using in unit tests
@SuppressWarnings({"PMD.UseObjectForClearerAPI", "checkstyle:parameternumber"})
protected void logEvent(
String type,
String page,
String method,
Integer userId,
String ip,
String referer,
String agent,
Date date) {
Validate.isTrue(type != null, "Type of suspicious activity must be non null");
Validate.isTrue(page != null, "Page must be non null");
AddSuspiciousActivityDbDto activity = new AddSuspiciousActivityDbDto();
activity.setType(type);
activity.setOccurredAt(date == null ? new Date() : date);
activity.setPage(abbreviatePage(page));
activity.setMethod(abbreviateMethod(method));
activity.setUserId(userId);
activity.setIp(StringUtils.defaultString(ip));
activity.setRefererPage(StringUtils.stripToNull(abbreviateRefererPage(referer)));
activity.setUserAgent(StringUtils.stripToNull(abbreviateUserAgent(agent)));
suspiciousActivities.add(activity);
}
/**
* Abbreviate name of HTTP method.
* @param method name of HTTP method
* @return name of the method as-is or its abbreviation with three points at the end
* @author Aleksandr Zorin
*/
private static String abbreviateMethod(String method) {
return abbreviateIfLengthGreaterThan(method, Db.SuspiciousActivity.METHOD_LENGTH, "method");
}
private static String abbreviatePage(String page) {
return abbreviateIfLengthGreaterThan(page, Db.SuspiciousActivity.PAGE_URL_LENGTH, "page");
}
private static String abbreviateRefererPage(String referer) {
return abbreviateIfLengthGreaterThan(
referer,
Db.SuspiciousActivity.REFERER_PAGE_LENGTH,
"referer_page"
);
}
private static String abbreviateUserAgent(String agent) {
return abbreviateIfLengthGreaterThan(
agent,
Db.SuspiciousActivity.USER_AGENT_LENGTH,
"user_agent"
);
}
// CheckStyle: ignore LineLength for next 1 lines
private static String abbreviateIfLengthGreaterThan(String text, int maxLength, String fieldName) {
if (text == null || text.length() <= maxLength) {
return text;
}
// TODO(security): fix possible log injection
LOG.warn(
"Length of value for '{}' field ({}) exceeds max field size ({}): '{}'",
fieldName,
text.length(),
maxLength,
text
);
return StringUtils.abbreviate(text, maxLength);
}
}