/*
* Password Management Servlets (PWM)
* http://www.pwm-project.org
*
* Copyright (c) 2006-2009 Novell, Inc.
* Copyright (c) 2009-2017 The PWM Project
*
* 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
package password.pwm.svc.event;
import org.graylog2.syslog4j.SyslogIF;
import org.graylog2.syslog4j.impl.AbstractSyslogConfigIF;
import org.graylog2.syslog4j.impl.AbstractSyslogWriter;
import org.graylog2.syslog4j.impl.backlog.NullSyslogBackLogHandler;
import org.graylog2.syslog4j.impl.net.AbstractNetSyslog;
import org.graylog2.syslog4j.impl.net.tcp.TCPNetSyslog;
import org.graylog2.syslog4j.impl.net.tcp.TCPNetSyslogConfig;
import org.graylog2.syslog4j.impl.net.tcp.ssl.SSLTCPNetSyslog;
import org.graylog2.syslog4j.impl.net.tcp.ssl.SSLTCPNetSyslogConfig;
import org.graylog2.syslog4j.impl.net.tcp.ssl.SSLTCPNetSyslogWriter;
import org.graylog2.syslog4j.impl.net.udp.UDPNetSyslog;
import org.graylog2.syslog4j.impl.net.udp.UDPNetSyslogConfig;
import password.pwm.AppProperty;
import password.pwm.PwmApplication;
import password.pwm.PwmConstants;
import password.pwm.config.Configuration;
import password.pwm.config.PwmSetting;
import password.pwm.error.ErrorInformation;
import password.pwm.error.PwmError;
import password.pwm.error.PwmOperationalException;
import password.pwm.health.HealthRecord;
import password.pwm.health.HealthStatus;
import password.pwm.health.HealthTopic;
import password.pwm.util.java.JsonUtil;
import password.pwm.util.java.TimeDuration;
import password.pwm.util.localdb.WorkQueueProcessor;
import password.pwm.util.secure.X509Utils;
import password.pwm.util.localdb.LocalDB;
import password.pwm.util.localdb.LocalDBException;
import password.pwm.util.localdb.LocalDBStoredQueue;
import password.pwm.util.logging.PwmLogger;
import javax.net.SocketFactory;
import javax.net.ssl.SSLContext;
import javax.net.ssl.X509TrustManager;
import java.io.Serializable;
import java.security.KeyManagementException;
import java.security.NoSuchAlgorithmException;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public class SyslogAuditService {
private static final PwmLogger LOGGER = PwmLogger.forClass(SyslogAuditService.class);
private static final int WARNING_WINDOW_MS = 30 * 60 * 1000;
private static final String SYSLOG_INSTANCE_NAME = "syslog-audit";
private static final int LENGTH_OVERSIZE = 1024;
private SyslogIF syslogInstance = null;
private ErrorInformation lastError = null;
private X509Certificate[] certificates = null;
private WorkQueueProcessor<String> workQueueProcessor;
private final Configuration configuration;
public SyslogAuditService(final PwmApplication pwmApplication)
throws LocalDBException
{
this.configuration = pwmApplication.getConfig();
this.certificates = configuration.readSettingAsCertificate(PwmSetting.AUDIT_SYSLOG_CERTIFICATES);
final String syslogConfigString = configuration.readSettingAsString(PwmSetting.AUDIT_SYSLOG_SERVERS);
final SyslogConfig syslogConfig;
try {
syslogConfig = SyslogConfig.fromConfigString(syslogConfigString);
syslogInstance = makeSyslogInstance(syslogConfig);
LOGGER.trace("queued service running for " + syslogConfig);
} catch (IllegalArgumentException e) {
LOGGER.error("error parsing syslog configuration for '" + syslogConfigString + "', error: " + e.getMessage());
}
final WorkQueueProcessor.Settings settings = new WorkQueueProcessor.Settings();
settings.setMaxEvents(Integer.parseInt(configuration.readAppProperty(AppProperty.QUEUE_SYSLOG_MAX_COUNT)));
settings.setRetryDiscardAge(new TimeDuration(Long.parseLong(configuration.readAppProperty(AppProperty.QUEUE_SYSLOG_MAX_AGE_MS))));
settings.setRetryInterval(new TimeDuration(Long.parseLong(configuration.readAppProperty(AppProperty.QUEUE_SYSLOG_RETRY_TIMEOUT_MS))));
final LocalDBStoredQueue localDBStoredQueue = LocalDBStoredQueue.createLocalDBStoredQueue(pwmApplication, pwmApplication.getLocalDB(), LocalDB.DB.SYSLOG_QUEUE);
workQueueProcessor = new WorkQueueProcessor<>(pwmApplication, localDBStoredQueue, settings, new SyslogItemProcessor(), this.getClass());
}
private class SyslogItemProcessor implements WorkQueueProcessor.ItemProcessor<String> {
@Override
public WorkQueueProcessor.ProcessResult process(final String workItem) {
return processEvent(workItem);
}
@Override
public String convertToDebugString(final String workItem) {
return JsonUtil.serialize(workItem);
}
}
private SyslogIF makeSyslogInstance(final SyslogConfig syslogConfig)
{
final AbstractSyslogConfigIF syslogConfigIF;
final AbstractNetSyslog syslogInstance;
switch (syslogConfig.getProtocol()) {
case sslTcp:
case tls: {
syslogConfigIF = new SSLTCPNetSyslogConfig();
((SSLTCPNetSyslogConfig)syslogConfigIF).setBackLogHandlers(Collections.singletonList(new NullSyslogBackLogHandler()));
syslogInstance = new LocalTrustSSLTCPNetSyslog();
}
break;
case tcp: {
syslogConfigIF = new TCPNetSyslogConfig();
((TCPNetSyslogConfig) syslogConfigIF).setBackLogHandlers(Collections.singletonList(new NullSyslogBackLogHandler()));
syslogInstance = new TCPNetSyslog();
}
break;
case udp: {
syslogConfigIF = new UDPNetSyslogConfig();
syslogInstance = new UDPNetSyslog();
}
break;
default:
throw new IllegalArgumentException("unknown protocol type");
}
final int maxLength = Integer.parseInt(configuration.readAppProperty(AppProperty.AUDIT_SYSLOG_MAX_MESSAGE_LENGTH));
syslogConfigIF.setThreaded(false);
syslogConfigIF.setMaxQueueSize(0);
syslogConfigIF.setMaxMessageLength(maxLength + LENGTH_OVERSIZE);
syslogConfigIF.setThrowExceptionOnWrite(true);
syslogConfigIF.setHost(syslogConfig.getHost());
syslogConfigIF.setPort(syslogConfig.getPort());
syslogInstance.initialize(SYSLOG_INSTANCE_NAME, syslogConfigIF);
return syslogInstance;
}
public void add(final AuditRecord event) throws PwmOperationalException {
try {
final String syslogMsg = convertAuditRecordToSyslogMessage(event, configuration);
workQueueProcessor.submit(syslogMsg);
} catch (PwmOperationalException e) {
LOGGER.warn("unable to add email to queue: " + e.getMessage());
}
}
public List<HealthRecord> healthCheck() {
final List<HealthRecord> healthRecords = new ArrayList<>();
if (lastError != null) {
final ErrorInformation errorInformation = lastError;
if (TimeDuration.fromCurrent(errorInformation.getDate()).isShorterThan(WARNING_WINDOW_MS)) {
healthRecords.add(new HealthRecord(HealthStatus.WARN, HealthTopic.Audit,
errorInformation.toUserStr(PwmConstants.DEFAULT_LOCALE, configuration)));
}
}
return healthRecords;
}
private WorkQueueProcessor.ProcessResult processEvent(final String auditRecord) {
final SyslogIF syslogIF = syslogInstance;
try {
syslogIF.info(auditRecord);
LOGGER.trace("delivered syslog audit event: " + auditRecord);
lastError = null;
return WorkQueueProcessor.ProcessResult.SUCCESS;
} catch (Exception e) {
final String errorMsg = "error while sending syslog message to remote service: " + e.getMessage();
final ErrorInformation errorInformation = new ErrorInformation(PwmError.ERROR_SYSLOG_WRITE_ERROR, errorMsg, new String[]{e.getMessage()});
lastError = errorInformation;
LOGGER.error(errorInformation.toDebugStr());
}
return WorkQueueProcessor.ProcessResult.RETRY;
}
public void close() {
final SyslogIF syslogIF = syslogInstance;
syslogIF.shutdown();
workQueueProcessor.close();
syslogInstance = null;
}
private static String convertAuditRecordToSyslogMessage(
final AuditRecord auditRecord,
final Configuration configuration
)
{
final int maxLength = Integer.parseInt(configuration.readAppProperty(AppProperty.AUDIT_SYSLOG_MAX_MESSAGE_LENGTH));
final StringBuilder message = new StringBuilder();
message.append(PwmConstants.PWM_APP_NAME);
message.append(" ");
final String jsonValue = JsonUtil.serialize(auditRecord);
if (message.length() + jsonValue.length() <= maxLength) {
message.append(jsonValue);
} else {
final AuditRecord inputRecord = JsonUtil.cloneUsingJson(auditRecord, auditRecord.getClass());
inputRecord.message = inputRecord.message == null ? "" : inputRecord.message;
inputRecord.narrative= inputRecord.narrative == null ? "" : inputRecord.narrative;
final String truncateMessage = configuration.readAppProperty(AppProperty.AUDIT_SYSLOG_TRUNCATE_MESSAGE);
final AuditRecord copiedRecord = JsonUtil.cloneUsingJson(auditRecord, auditRecord.getClass());
copiedRecord.message = "";
copiedRecord.narrative = "";
final int shortenedMessageLength = message.length()
+ JsonUtil.serialize(copiedRecord).length()
+ truncateMessage.length();
final int maxMessageAndNarrativeLength = maxLength - (shortenedMessageLength + (truncateMessage.length() * 2));
int maxMessageLength = inputRecord.getMessage().length();
int maxNarrativeLength = inputRecord.getNarrative().length();
{
int top = maxMessageAndNarrativeLength;
while (maxMessageLength + maxNarrativeLength > maxMessageAndNarrativeLength) {
top--;
maxMessageLength = Math.min(maxMessageLength, top);
maxNarrativeLength = Math.min(maxNarrativeLength, top);
}
}
copiedRecord.message = inputRecord.getMessage().length() > maxMessageLength
? inputRecord.message.substring(0, maxMessageLength) + truncateMessage
: inputRecord.message;
copiedRecord.narrative = inputRecord.getNarrative().length() > maxNarrativeLength
? inputRecord.narrative.substring(0, maxNarrativeLength) + truncateMessage
: inputRecord.narrative;
message.append(JsonUtil.serialize(copiedRecord));
}
return message.toString();
}
public static class SyslogConfig implements Serializable {
public enum Protocol { sslTcp, tcp, udp, tls }
private Protocol protocol;
private String host;
private int port;
public SyslogConfig(final Protocol protocol, final String host, final int port) {
this.protocol = protocol;
this.host = host;
this.port = port;
}
public Protocol getProtocol() {
return protocol;
}
public String getHost() {
return host;
}
public int getPort() {
return port;
}
public static SyslogConfig fromConfigString(final String input) throws IllegalArgumentException {
if (input == null) {
throw new IllegalArgumentException("input cannot be null");
}
final String[] parts = input.split(",");
if (parts.length != 3) {
throw new IllegalArgumentException("input must have three comma separated parts.");
}
final Protocol protocol;
try {
protocol = Protocol.valueOf(parts[0]);
} catch (IllegalArgumentException e) {
throw new IllegalArgumentException("unknown protocol '" + parts[0] + "'");
}
final int port;
try {
port = Integer.parseInt(parts[2]);
} catch (NumberFormatException e) {
throw new IllegalArgumentException("invalid port number '" + parts[2] + "'");
}
return new SyslogConfig(protocol,parts[1],port);
}
public String toString() {
return JsonUtil.serialize(this);
}
}
public int queueSize() {
return workQueueProcessor.queueSize();
}
private class LocalTrustSyslogWriterClass extends SSLTCPNetSyslogWriter {
private LocalTrustSyslogWriterClass()
{
super();
}
@Override
protected SocketFactory obtainSocketFactory()
{
if (certificates != null && certificates.length >= 1) {
try {
final SSLContext sc = SSLContext.getInstance("SSL");
sc.init(null, new X509TrustManager[]{new X509Utils.CertMatchingTrustManager(configuration, certificates)},
new java.security.SecureRandom());
return sc.getSocketFactory();
} catch (NoSuchAlgorithmException | KeyManagementException e) {
LOGGER.error("unexpected error loading syslog certificates: " + e.getMessage());
}
}
return super.obtainSocketFactory();
}
}
private class LocalTrustSSLTCPNetSyslog extends SSLTCPNetSyslog {
@Override
public AbstractSyslogWriter createWriter()
{
final LocalTrustSyslogWriterClass newClass = new LocalTrustSyslogWriterClass();
newClass.initialize(this);
return newClass;
}
}
}