/* ***** BEGIN LICENSE BLOCK ***** * Version: MPL 1.1/GPL 2.0/LGPL 2.1 * * The contents of this file are subject to the Mozilla Public License Version * 1.1 (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * http://www.mozilla.org/MPL/ * * Software distributed under the License is distributed on an "AS IS" basis, * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License * for the specific language governing rights and limitations under the * License. * * The Original Code is part of dcm4che, an implementation of DICOM(TM) in * Java(TM), hosted at https://github.com/gunterze/dcm4che3. * * The Initial Developer of the Original Code is * Agfa Healthcare. * Portions created by the Initial Developer are Copyright (C) 2012 * the Initial Developer. All Rights Reserved. * * Contributor(s): * See @authors listed below * * Alternatively, the contents of this file may be used under the terms of * either the GNU General Public License Version 2 or later (the "GPL"), or * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), * in which case the provisions of the GPL or the LGPL are applicable instead * of those above. If you wish to allow use of your version of this file only * under the terms of either the GPL or the LGPL, and not to allow others to * use your version of this file under the terms of the MPL, indicate your * decision by deleting the provisions above and replace them with the notice * and other provisions required by the GPL or the LGPL. If you do not delete * the provisions above, a recipient may use your version of this file under * the terms of any one of the MPL, the GPL or the LGPL. * * ***** END LICENSE BLOCK ***** */ package org.dcm4che3.net.audit; import com.lmax.disruptor.*; import com.lmax.disruptor.dsl.Disruptor; import com.lmax.disruptor.dsl.ProducerType; import org.dcm4che3.audit.*; import org.dcm4che3.audit.AuditMessages.RoleIDCode; import org.dcm4che3.audit.AuditMessage; import org.dcm4che3.conf.core.api.ConfigurableClass; import org.dcm4che3.conf.core.api.ConfigurableProperty; import org.dcm4che3.conf.core.api.LDAP; import org.dcm4che3.net.Connection; import org.dcm4che3.net.Device; import org.dcm4che3.net.DeviceExtension; import org.dcm4che3.net.IncompatibleConnectionException; import org.dcm4che3.util.SafeClose; import org.dcm4che3.util.StreamUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.net.ssl.SSLContext; import java.io.*; import java.lang.management.ManagementFactory; import java.net.*; import java.nio.charset.Charset; import java.security.GeneralSecurityException; import java.util.*; import java.util.concurrent.Executor; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; /** * @author Gunter Zeilinger <gunterze@gmail.com> * @author Michael Backhaus <michael.backhaus@agfa.com> */ @LDAP(objectClasses = "dcmAuditLogger") @ConfigurableClass public class AuditLogger extends DeviceExtension { private static final String DICOM_PRIMARY_DEVICE_TYPE = "dicomPrimaryDeviceType"; private static final String DEVICE_NAME_IN_FILENAME_SEPARATOR = "-._"; private static Disruptor<AuditMessageEvent> disruptor; public enum SendStatus { SENT, QUEUED, SUPPRESSED } private static final long serialVersionUID = 1595714214186063103L; private static final int MSG_PROMPT_LEN = 8192; private static Logger LOG = LoggerFactory.getLogger(AuditLogger.class); public enum Facility { kern, // (0) -- kernel messages user, // (1) -- user-level messages mail, // (2) -- mail system messages daemon, // (3) -- system daemons' messages auth, // (4) -- authorization messages syslog, // (5) -- messages generated internally by syslogd lpr, // (6) -- line printer subsystem messages news, // (7) -- network news subsystem messages uucp, // (8) -- UUCP subsystem messages cron, // (9) -- clock daemon messages authpriv, // (10)-- security/authorization messages ftp, // (11)-- ftp daemon messages ntp, // (12)-- NTP subsystem messages audit, // (13)-- audit messages console, // (14)-- console messages cron2, // (15)-- clock daemon messages local0, // (16) local1, // (17) local2, // (18) local3, // (19) local4, // (20) local5, // (21) local6, // (22) local7, // (23) } public enum Severity { emerg, // (0) -- emergency; system is unusable alert, // (1) -- action must be taken immediately crit, // (2) -- critical condition err, // (3) -- error condition warning, // (4) -- warning condition notice, // (5) -- normal but significant condition info, // (6) -- informational message debug // (7) -- debug-level messages } public static final String MESSAGE_ID = "IHE+RFC-3881"; private static final int[] DIGITS_0X = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', }; private static final int[] DIGITS_X0 = { '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '1', '1', '1', '1', '1', '1', '1', '1', '1', '1', '2', '2', '2', '2', '2', '2', '2', '2', '2', '2', '3', '3', '3', '3', '3', '3', '3', '3', '3', '3', '4', '4', '4', '4', '4', '4', '4', '4', '4', '4', '5', '5', '5', '5', '5', '5', '5', '5', '5', '5', '6', '6', '6', '6', '6', '6', '6', '6', '6', '6', '7', '7', '7', '7', '7', '7', '7', '7', '7', '7', '8', '8', '8', '8', '8', '8', '8', '8', '8', '8', '9', '9', '9', '9', '9', '9', '9', '9', '9', '9', }; private static final byte[] BOM = {(byte) 0xEF, (byte) 0xBB, (byte) 0xBF}; private static final char SYSLOG_VERSION = '1'; private static final InetAddress localHost = localHost(); private static final String processID = processID(); private static final Comparator<File> FILE_COMPARATOR = new Comparator<File>() { @Override public int compare(File o1, File o2) { long diff = o1.lastModified() - o2.lastModified(); return diff < 0 ? -1 : diff > 0 ? 1 : 0; } }; private static volatile AuditLogger defaultLogger; @ConfigurableProperty(name = "dcmAuditRecordRepositoryDeviceReference", label = "ARR Device", description = "Devices that correspond to Audit Record Repositories to which audit messages are sent", tags = ConfigurableProperty.Tag.PRIMARY, collectionOfReferences = true) private List<Device> auditRecordRepositoryDevices = new ArrayList<Device>(); @ConfigurableProperty( name = "dcmAuditFacility", enumRepresentation = ConfigurableProperty.EnumRepresentation.ORDINAL, defaultValue = "10") private Facility facility = Facility.authpriv; @ConfigurableProperty( name = "dcmAuditSuccessSeverity", enumRepresentation = ConfigurableProperty.EnumRepresentation.ORDINAL, defaultValue = "5" ) private Severity successSeverity = Severity.notice; @ConfigurableProperty( name = "dcmAuditMinorFailureSeverity", enumRepresentation = ConfigurableProperty.EnumRepresentation.ORDINAL, defaultValue = "4" ) private Severity minorFailureSeverity = Severity.warning; @ConfigurableProperty( name = "dcmAuditSeriousFailureSeverity", enumRepresentation = ConfigurableProperty.EnumRepresentation.ORDINAL, defaultValue = "3" ) private Severity seriousFailureSeverity = Severity.err; @ConfigurableProperty( name = "dcmAuditMajorFailureSeverity", enumRepresentation = ConfigurableProperty.EnumRepresentation.ORDINAL, defaultValue = "2" ) private Severity majorFailureSeverity = Severity.crit; @ConfigurableProperty(name = "dcmAuditApplicationName") private String applicationName; @ConfigurableProperty(name = "dcmAuditSourceID") private String auditSourceID; @ConfigurableProperty(name = "dcmAuditEnterpriseSiteID") private String auditEnterpriseSiteID; @ConfigurableProperty(name = "dcmAuditSourceTypeCode") private String[] auditSourceTypeCodes = {}; @ConfigurableProperty(name = "dcmAuditMessageID", defaultValue = MESSAGE_ID) private String messageID = MESSAGE_ID; @ConfigurableProperty(name = "dcmAuditMessageEncoding", defaultValue = "UTF-8") private String encoding = "UTF-8"; @ConfigurableProperty(name = "dcmAuditMessageSchemaURI", defaultValue =AuditMessages.SCHEMA_URI ) private String schemaURI = AuditMessages.SCHEMA_URI; @ConfigurableProperty(name = "dcmAuditTimestampInUTC", defaultValue = "false") private boolean timestampInUTC = false; @ConfigurableProperty(name = "dcmAuditMessageBOM", defaultValue = "true") private boolean includeBOM = true; @ConfigurableProperty(name = "dcmAuditMessageFormatXML", defaultValue = "false") private boolean formatXML; @ConfigurableProperty(name = "dcmAuditMessageSupplement95Schema", defaultValue = "false") private boolean supplement95; @ConfigurableProperty(name = "dicomInstalled") private Boolean auditLoggerInstalled; @ConfigurableProperty(name = "dcmAuditIncludeInstanceUID") private Boolean doIncludeInstanceUID = false; @ConfigurableProperty(name = "dcmAuditLoggerSpoolDirectoryURI") private String spoolDirectoryURI; private File spoolDirectory; private String spoolFileNamePrefix = "audit"; private String spoolFileNameSuffix = ".log"; @ConfigurableProperty(name = "dcmAuditLoggerRetryInterval", defaultValue = "0") private int retryInterval; @LDAP( noContainerNode = true, distinguishingField = "cn" ) @ConfigurableProperty(name = "dcmAuditSuppressCriteria") private final List<AuditSuppressCriteria> suppressAuditMessageFilters = new ArrayList<AuditSuppressCriteria>(0); @ConfigurableProperty(name = "dicomNetworkConnectionReference", label = "Connections", description = "Connections that can be used to send audit messages", tags = ConfigurableProperty.Tag.PRIMARY, collectionOfReferences = true) private List<Connection> connections = new ArrayList<Connection>(1); private transient MessageBuilder builder; private transient Map<String,ActiveConnection> activeConnection = new HashMap<String, ActiveConnection>(); private transient ScheduledFuture<?> retryTimer; private transient Exception lastException; private transient long lastSentTimeInMillis; private transient final FilenameFilter FILENAME_FILTER = new FilenameFilter() { @Override public boolean accept(File dir, String name) { return name.startsWith(spoolFileNamePrefix) && name.endsWith(spoolFileNameSuffix); } }; public List<AuditSuppressCriteria> getSuppressAuditMessageFilters() { return suppressAuditMessageFilters; } public void setSuppressAuditMessageFilters(List<AuditSuppressCriteria> suppressAuditMessageFilters) { this.suppressAuditMessageFilters.clear(); for (AuditSuppressCriteria filter : suppressAuditMessageFilters) this.suppressAuditMessageFilters.add(filter); } public final List<Device> getAuditRecordRepositoryDevices() { return auditRecordRepositoryDevices; } public List<String> getAuditRecordRepositoryDeviceNames() { if (auditRecordRepositoryDevices == null) throw new IllegalStateException("AuditRecordRepositoryDevice not initalized"); List<String> names = new ArrayList<String>(auditRecordRepositoryDevices.size()); for (Device d : auditRecordRepositoryDevices) { names.add(d.getDeviceName()); } return names; } public void setAuditRecordRepositoryDevices(List<Device> arrDevices) { for (ActiveConnection c : activeConnection.values()) SafeClose.close(c); activeConnection.clear(); if(arrDevices==null){ this.auditRecordRepositoryDevices.clear(); }else{ this.auditRecordRepositoryDevices = arrDevices; } } public void addAuditRecordRepositoryDevice(Device device) { auditRecordRepositoryDevices.add(device); } public boolean removeAuditRecordRepositoryDevice(Device device) { return auditRecordRepositoryDevices.remove(device); } public final Facility getFacility() { return facility; } public final void setFacility(Facility facility) { if (facility == null) throw new NullPointerException(); this.facility = facility; } public final Severity getSuccessSeverity() { return successSeverity; } public final void setSuccessSeverity(Severity severity) { if (severity == null) throw new NullPointerException(); this.successSeverity = severity; } public final Severity getMinorFailureSeverity() { return minorFailureSeverity; } public final void setMinorFailureSeverity(Severity severity) { if (severity == null) throw new NullPointerException(); this.minorFailureSeverity = severity; } public final Severity getSeriousFailureSeverity() { return seriousFailureSeverity; } public final void setSeriousFailureSeverity(Severity severity) { if (severity == null) throw new NullPointerException(); this.seriousFailureSeverity = severity; } public final Severity getMajorFailureSeverity() { return majorFailureSeverity; } public final void setMajorFailureSeverity(Severity severity) { if (severity == null) throw new NullPointerException(); this.majorFailureSeverity = severity; } public final String getApplicationName() { return applicationName; } private String applicationName() { return applicationName != null ? applicationName : auditSourceID(); } public final void setApplicationName(String applicationName) { this.applicationName = applicationName; } public final String getAuditSourceID() { return auditSourceID; } public final void setAuditSourceID(String auditSourceID) { this.auditSourceID = auditSourceID; } private String auditSourceID() { return auditSourceID != null ? auditSourceID : getDevice().getDeviceName(); } public final String getAuditEnterpriseSiteID() { return auditEnterpriseSiteID; } public final void setAuditEnterpriseSiteID(String auditEnterpriseSiteID) { this.auditEnterpriseSiteID = auditEnterpriseSiteID; } public String[] getAuditSourceTypeCodes() { return auditSourceTypeCodes; } public void setAuditSourceTypeCodes(String... auditSourceTypeCode) { this.auditSourceTypeCodes = auditSourceTypeCode; } public ActiveParticipant createActiveParticipant( boolean requestor, RoleIDCode... roleIDs) { Collection<String> aets = device.getApplicationAETitles(); return createActiveParticipant(requestor, processID(), AuditMessages.alternativeUserIDForAETitle( aets.toArray(new String[aets.size()])), applicationName(), localHost().getHostName(), roleIDs); } public ActiveParticipant createActiveParticipant( boolean requestor, String userID, String alternativeUserID, String userName, String hostName, RoleIDCode... roleIDs) { ActiveParticipant ap = new ActiveParticipant(); ap.setUserID(userID); ap.setAlternativeUserID(alternativeUserID); ap.setUserName(userName); ap.setUserIsRequestor(requestor); ap.setNetworkAccessPointID(hostName); ap.setNetworkAccessPointTypeCode(AuditMessages.isIP(hostName) ? AuditMessages.NetworkAccessPointTypeCode.IPAddress : AuditMessages.NetworkAccessPointTypeCode.MachineName); for (RoleIDCode roleID : roleIDs) ap.getRoleIDCode().add(roleID); return ap; } public AuditSourceIdentification createAuditSourceIdentification() { AuditSourceIdentification asi = new AuditSourceIdentification(); asi.setAuditSourceID(auditSourceID()); if (auditEnterpriseSiteID != null) { if (auditEnterpriseSiteID.equals("dicomInstitutionName")) { String[] institutionNames = getDevice().getInstitutionNames(); if (institutionNames.length > 0) asi.setAuditEnterpriseSiteID(institutionNames[0]); } else asi.setAuditEnterpriseSiteID(auditEnterpriseSiteID); } for (String code : auditSourceTypeCodes) { if (code.equals(DICOM_PRIMARY_DEVICE_TYPE)) { for (String type : device.getPrimaryDeviceTypes()) { AuditSourceTypeCode astc = new AuditSourceTypeCode(); astc.setCode(type); astc.setCodeSystemName("DCM"); asi.getAuditSourceTypeCode().add(astc); } } else { AuditSourceTypeCode astc = new AuditSourceTypeCode(); astc.setCode(code); asi.getAuditSourceTypeCode().add(astc); } } return asi; } public final String getMessageID() { return messageID; } public final void setMessageID(String messageID) { this.messageID = messageID; } public final String getEncoding() { return encoding; } public final void setEncoding(String encoding) { if (!Charset.isSupported(encoding)) throw new IllegalArgumentException( "Charset not supported: " + encoding); this.encoding = encoding; } public final String getSchemaURI() { return schemaURI; } public final void setSchemaURI(String schemaURI) { this.schemaURI = schemaURI; } public final boolean isTimestampInUTC() { return timestampInUTC; } public final void setTimestampInUTC(boolean timestampInUTC) { this.timestampInUTC = timestampInUTC; } public final boolean isIncludeBOM() { return includeBOM; } public final void setIncludeBOM(boolean includeBOM) { this.includeBOM = includeBOM; } public final boolean isFormatXML() { return formatXML; } public final void setFormatXML(boolean formatXML) { this.formatXML = formatXML; } public boolean isSupplement95() { return supplement95; } public void setSupplement95(boolean sup95) { this.supplement95 = sup95; } public boolean isInstalled() { return device != null && device.isInstalled() && (auditLoggerInstalled == null || auditLoggerInstalled.booleanValue()); } public final Boolean getAuditLoggerInstalled() { return auditLoggerInstalled; } public void setAuditLoggerInstalled(Boolean installed) { if (installed != null && installed.booleanValue() && device != null && !device.isInstalled()) throw new IllegalStateException("owning device not installed"); this.auditLoggerInstalled = installed; } public Boolean isIncludeInstanceUID() { return doIncludeInstanceUID; } public Boolean getDoIncludeInstanceUID() { return doIncludeInstanceUID; } public void setDoIncludeInstanceUID(Boolean doIncludeInstanceUID) { this.doIncludeInstanceUID = doIncludeInstanceUID; } /** * Get spool directory into which messages failed to sent to the record * repository are stored for later re-send. * * @return The directory in which the messages failed to sent are stored, * or {@code null} if the default temporary-file directory is to * be used */ public File getSpoolDirectory() { return spoolDirectory; } /** * Set spool directory into which messages failed sent to the record * repository are stored for later re-send. * * @param directory The directory in which the messages failed to sent are * stored, or {@code null} if the default temporary-file * directory is to be used */ public void setSpoolDirectory(File directory) { this.spoolDirectory = directory; } public String getSpoolDirectoryURI() { return spoolDirectory != null ? spoolDirectory.toURI().toString() : null; } public void setSpoolDirectoryURI(String uri) { this.spoolDirectory = uri != null ? new File(URI.create(uri)) : null; } public String getSpoolNameFilePrefix() { return spoolFileNamePrefix; } public void setSpoolFileNamePrefix(String prefix) { if (prefix.length() < 3) throw new IllegalArgumentException("Spool file name prefix too short"); this.spoolFileNamePrefix = prefix; } public String getSpoolFileNameSuffix() { return spoolFileNameSuffix; } public void setSpoolFileNameSuffix(String suffix) { if (suffix.isEmpty()) throw new IllegalArgumentException("Spool file name suffix cannot be empty"); this.spoolFileNameSuffix = suffix; } /** * Get interval in seconds to retry to sent messages which could not be * sent to the record repository or {@code 0} if messages failed to sent * are not spooled for later re-send. * * @return interval retry interval in seconds or {@code 0} * @see #write(Calendar, AuditMessage) */ public int getRetryInterval() { return retryInterval; } /** * Set interval in seconds to retry to sent messages which could not be * sent to the record repository or {@code 0} if messages failed to sent * are not spooled for later re-send. * * @param interval retry interval in seconds or {@code 0} * @see #write(Calendar, AuditMessage) */ public void setRetryInterval(int interval) { this.retryInterval = interval; } public void addConnection(Connection conn) { if (!conn.getProtocol().isSyslog()) throw new IllegalArgumentException( "Audit Logger does not support protocol " + conn.getProtocol()); if (device != null && device != conn.getDevice()) throw new IllegalStateException(conn + " not contained by " + device.getDeviceName()); connections.add(conn); } @Override public void verifyNotUsed(Connection conn) { if (connections.contains(conn)) throw new IllegalStateException(conn + " used by Audit Logger"); } public boolean removeConnection(Connection conn) { return connections.remove(conn); } public void setConnections(List<Connection> connections) { this.connections.clear(); for (Connection connection : connections) addConnection(connection); } public List<Connection> getConnections() { return connections; } public List<AuditSuppressCriteria> getAuditSuppressCriteriaList() { return suppressAuditMessageFilters; } public AuditSuppressCriteria findAuditSuppressCriteriaByCommonName(String cn) { for (AuditSuppressCriteria criteria : suppressAuditMessageFilters) { if (criteria.getCommonName().equals(cn)) return criteria; } return null; } public void setAuditSuppressCriteriaList(List<AuditSuppressCriteria> filters) { this.suppressAuditMessageFilters.clear(); this.suppressAuditMessageFilters.addAll(filters); } public void addAuditSuppressCriteria(AuditSuppressCriteria criteria) { this.suppressAuditMessageFilters.add(criteria); } public void clearAllAuditSuppressCriteria() { this.suppressAuditMessageFilters.clear(); } /** * Test if the Event Identification and the Active ActiveParticipant of an * Audit Message matches one of the {@code AuditSuppressCriteria} * * @param msg Audit Message to test * @return {@code true} the specified audit message will be suppressed; * otherwise {@code false} */ public boolean isAuditMessageSuppressed(AuditMessage msg) { for (AuditSuppressCriteria criteria : suppressAuditMessageFilters) { if (criteria.match(msg)) return true; } return false; } @Override public void reconfigure(DeviceExtension from) { reconfigure((AuditLogger) from); } private void reconfigure(AuditLogger from) { setFacility(from.facility); setSuccessSeverity(from.successSeverity); setMinorFailureSeverity(from.minorFailureSeverity); setSeriousFailureSeverity(from.seriousFailureSeverity); setMajorFailureSeverity(from.majorFailureSeverity); setApplicationName(from.applicationName); setAuditSourceID(from.auditSourceID); setAuditEnterpriseSiteID(from.auditEnterpriseSiteID); setAuditSourceTypeCodes(from.auditSourceTypeCodes); setMessageID(from.messageID); setEncoding(from.encoding); setSchemaURI(from.schemaURI); setTimestampInUTC(from.timestampInUTC); setIncludeBOM(from.includeBOM); setFormatXML(from.formatXML); setSupplement95(from.isSupplement95()); setSpoolDirectory(from.spoolDirectory); setSpoolFileNamePrefix(from.spoolFileNamePrefix); setSpoolFileNameSuffix(from.spoolFileNameSuffix); setRetryInterval(from.retryInterval); setAuditLoggerInstalled(from.auditLoggerInstalled); setAuditRecordRepositoryDevices(from.auditRecordRepositoryDevices); setAuditSuppressCriteriaList(from.suppressAuditMessageFilters); device.reconfigureConnections(connections, from.connections); closeActiveConnection(); } public Calendar timeStamp() { return timestampInUTC ? new GregorianCalendar(TimeZone.getTimeZone("UTC"), Locale.ENGLISH) : new GregorianCalendar(Locale.ENGLISH); } /** * Send Audit Message by Syslog Protocol to Audit Record Repository, if the * message does not match any configured {@code AuditSuppressCriteria}. If * an I/O error occurs sending the message to the {@code AuditRecordRepository} * and if a {@code RetryInterval) is configured, the message will be spooled * into the configured {@code SpoolDirectory} for later re-send and the * method returns {@code false}. If no {@code RetryInterval} is configured, * the method throws an {@code IOException) if an I/O error occurs sending * the message. * <p/> * Attention: sending via UDP without getting an I/O error does not ensure * that the Audit Record Repository actually received the message! * * @param timeStamp included in Syslog Header * @param msg Audit Message * @return {@code SendStatus.SUPPRESSED} if the message was suppressed; * {@code SendStatus.SENT} if the message was successfully emitted; * {@code SendStatus.QUEUED} if the message was spooled for later re-send * @throws IllegalStateException if there is no {@code AuditRecordRepository} associated with * this {@code AuditLogger} * @throws IncompatibleConnectionException if no {@code Connection) of this {@code AuditLogger} is compatible * with any {@code Connection) of the associated {@code AuditRecordRepository} * @throws GeneralSecurityException if the {@link SSLContext} could not get intialized from configured * private key and public certificates * @throws IOException if an I/O error occurs sending the message to the {@code AuditRecordRepository} * or on spooling the message to the file system */ public SendStatus write(Calendar timeStamp, AuditMessage msg) throws IncompatibleConnectionException, GeneralSecurityException, IOException { if (isAuditMessageSuppressed(msg)) return SendStatus.SUPPRESSED; return sendMessage(builder().createMessage(timeStamp, msg)); } public void writeAsync(Calendar timeStamp, AuditMessage msg) throws IncompatibleConnectionException, GeneralSecurityException, IOException, InsufficientCapacityException { if (isAuditMessageSuppressed(msg)) return; RingBuffer<AuditMessageEvent> ringBuffer = getDisruptor(this).getRingBuffer(); long sequence = ringBuffer.next(); // Grab the next sequence try { AuditMessageEvent msgenrtry = ringBuffer.get(sequence); // Get the entry in the Disruptor // for the sequence msgenrtry.setLogger(this); // Fill with data msgenrtry.setMessage(msg); } finally { ringBuffer.publish(sequence); } } public SendStatus write(Calendar timeStamp, Severity severity, byte[] data, int off, int len) throws IncompatibleConnectionException, GeneralSecurityException, IOException { return sendMessage( builder().createMessage(timeStamp, severity, data, off, len)); } private MessageBuilder builder() { if (builder == null) builder = new MessageBuilder(); return builder; } private SendStatus sendMessage(DatagramPacket msg) throws IncompatibleConnectionException, GeneralSecurityException, IOException { String deviceName; SendStatus status = SendStatus.SENT; for (Device arrDev : auditRecordRepositoryDevices) { deviceName = arrDev.getDeviceName(); if (getNumberOfQueuedMessages(deviceName) > 0) { spoolMessage(deviceName, msg); } else { try { activeConnection(arrDev).sendMessage(msg); lastSentTimeInMillis = System.currentTimeMillis(); } catch (IOException e) { lastException = e; if (retryInterval > 0) { LOG.info("Failed to send audit message:", e); spoolMessage(deviceName, msg); scheduleRetry(); status = SendStatus.QUEUED; } else { throw e; } } } } return status; } private synchronized void scheduleRetry() { if (retryTimer != null || retryInterval <= 0) { return; } LOG.debug("Scheduled retry in {} s", retryInterval); retryTimer = getDevice().schedule( new Runnable() { @Override public void run() { synchronized (AuditLogger.this) { retryTimer = null; } sendQueuedMessages(); } }, retryInterval, TimeUnit.SECONDS); } private void spoolMessage(String deviceName, DatagramPacket msg) throws IOException { if (spoolDirectory != null) spoolDirectory.mkdirs(); File f = null; try { f = File.createTempFile(spoolFileNamePrefix+DEVICE_NAME_IN_FILENAME_SEPARATOR+deviceName+DEVICE_NAME_IN_FILENAME_SEPARATOR, spoolFileNameSuffix, spoolDirectory); if (spoolDirectory == null) spoolDirectory = f.getParentFile(); LOG.info("Spool audit message to {}", f); FileOutputStream out = new FileOutputStream(f); try { out.write(msg.getData(), msg.getOffset(), msg.getLength()); } finally { SafeClose.close(out); } f = null; } catch (IOException e) { throw new IOException("Failed to spool audit message for device "+deviceName, e); } finally { if (f != null) f.delete(); } } public void sendQueuedMessages() { File dir = spoolDirectory; if (dir == null) return; boolean failed = false; for (final Device arrDev : this.auditRecordRepositoryDevices) { try { FilenameFilter fnFilter = new FilenameFilter() { String prefix = spoolFileNamePrefix+DEVICE_NAME_IN_FILENAME_SEPARATOR+arrDev.getDeviceName()+DEVICE_NAME_IN_FILENAME_SEPARATOR; @Override public boolean accept(File dir, String name) { return name.startsWith(prefix) && name.endsWith(spoolFileNameSuffix); } }; File[] queuedMessages = dir.listFiles(fnFilter); byte[] b = null; while (queuedMessages != null && queuedMessages.length > 0) { Arrays.sort(queuedMessages, FILE_COMPARATOR); for (File file : queuedMessages) { LOG.debug("Read audit message from {}", file); int len = (int) file.length(); if (b == null || b.length < len) b = new byte[len]; try { FileInputStream in = new FileInputStream(file); try { StreamUtils.readFully(in, b, 0, len); } finally { SafeClose.close(in); } } catch (IOException e) { LOG.warn("Failed to read audit message from {}", file, e); File dest = new File(file.getParent(), file.getName() + ".err"); file.renameTo(dest); continue; } activeConnection(arrDev).sendMessage(new DatagramPacket(b, 0, len)); lastSentTimeInMillis = System.currentTimeMillis(); if (file.delete()) LOG.debug("Delete spool file {}", file); else LOG.warn("Failed to delete spool file {}", file); } queuedMessages = dir.listFiles(FILENAME_FILTER); } } catch (Exception e) { lastException = e; LOG.info("Failed to send audit message:", e); failed = true; } } if (failed) scheduleRetry(); synchronized (this) { notify(); } } public Exception getLastException() { return lastException; } public long getLastSentTimeInMillis() { return lastSentTimeInMillis; } public int getNumberOfQueuedMessages() { int tot = 0; for (Device d : this.auditRecordRepositoryDevices) tot += getNumberOfQueuedMessages(d.getDeviceName()); return tot; } public int getNumberOfQueuedMessages(final String deviceName) { try { FilenameFilter fnFilter = new FilenameFilter() { String prefix = spoolFileNamePrefix+DEVICE_NAME_IN_FILENAME_SEPARATOR+deviceName+DEVICE_NAME_IN_FILENAME_SEPARATOR; @Override public boolean accept(File dir, String name) { return name.startsWith(prefix) && name.endsWith(spoolFileNameSuffix); } }; return spoolDirectory.list(fnFilter).length; } catch (NullPointerException e) { return 0; } } public File[] getQueuedMessages() { try { return spoolDirectory.listFiles(FILENAME_FILTER); } catch (NullPointerException e) { return null; } } public synchronized void waitForNoQueuedMessages(long timeout) throws InterruptedException { int count; for (Device arrDev : this.auditRecordRepositoryDevices) { while ( (count = getNumberOfQueuedMessages(arrDev.getDeviceName())) > 0) { LOG.debug("Wait for {} queued Audit Messages for AuditRepository {}!", count, arrDev.getDeviceName()); wait(timeout); } } } public synchronized void closeActiveConnection() { for (Map.Entry<String, ActiveConnection> entry : activeConnection.entrySet()) { try { entry.getValue().close(); } catch (IOException e) { LOG.error("Failed to close active connection to {}", entry.getKey(), e); throw new AssertionError(e); } this.activeConnection.clear(); } } private synchronized ActiveConnection activeConnection(Device arrDev) throws IncompatibleConnectionException { ActiveConnection activeConnection = this.activeConnection.get(arrDev.getDeviceName()); if (activeConnection != null) return activeConnection; AuditRecordRepository arr = arrDev.getDeviceExtension(AuditRecordRepository.class); if (arr == null) throw new IllegalStateException("AuditRecordRepositoryDevice " + arrDev.getDeviceName() + " does not provide Audit Record Repository"); for (Connection remoteConn : arr.getConnections()) if (remoteConn.isInstalled() && remoteConn.isServer()) for (Connection conn : connections) if (conn.isInstalled() && conn.isCompatible(remoteConn)) { activeConnection = conn.getProtocol().isTCP() ? new TCPConnection(conn, remoteConn) : new UDPConnection(conn, remoteConn); this.activeConnection.put(arrDev.getDeviceName(), activeConnection); return activeConnection; } throw new IncompatibleConnectionException( "No compatible connection to " + arr + " available on " + this); } public static String processID() { String s = ManagementFactory.getRuntimeMXBean().getName(); int atPos = s.indexOf('@'); return atPos > 0 ? s.substring(0, atPos) : Integer.toString(new Random().nextInt() & 0x7fffffff); } public static InetAddress localHost() { try { return InetAddress.getLocalHost(); } catch (UnknownHostException e) { return null; } } private Severity severityOf(AuditMessage msg) { String eventOutcomeIndicator = msg.getEventIdentification() .getEventOutcomeIndicator(); if (eventOutcomeIndicator.length() == 1) switch (eventOutcomeIndicator.charAt(0)) { case '0': return successSeverity; case '4': return minorFailureSeverity; case '8': return seriousFailureSeverity; } else if (eventOutcomeIndicator.equals("12")) return majorFailureSeverity; throw new IllegalArgumentException( "Illegal eventOutcomeIndicator: " + eventOutcomeIndicator); } private int prival(Severity severity) { return (facility.ordinal() << 3) | severity.ordinal(); } public static AuditLogger getDefaultLogger() { return defaultLogger; } public static void setDefaultLogger(AuditLogger defaultLogger) { AuditLogger.defaultLogger = defaultLogger; } private class MessageBuilder extends ByteArrayOutputStream { DatagramPacket createMessage(Calendar timeStamp, AuditMessage msg) { try { reset(); writeHeader(severityOf(msg), timeStamp); if (!supplement95) { AuditMessages.toXML(msg, builder, formatXML, encoding, schemaURI); } else { AuditMessages.toSupplement95XML(msg, builder, formatXML, encoding, schemaURI); } } catch (IOException e) { throw new RuntimeException(e); } return new DatagramPacket(buf, 0, count); } DatagramPacket createMessage(Calendar timeStamp, Severity severity, byte[] data, int off, int len) { try { reset(); writeHeader(severity, timeStamp); write(data, off, len); } catch (IOException e) { throw new RuntimeException(e); } return new DatagramPacket(buf, 0, count); } void writeHeader(Severity severity, Calendar timeStamp) throws IOException { write('<'); writeInt(prival(severity)); write('>'); write(SYSLOG_VERSION); write(' '); write(timeStamp); write(' '); if (localHost != null) write(localHost.getCanonicalHostName().getBytes(encoding)); else write('-'); write(' '); write(applicationName().getBytes(encoding)); write(' '); write(processID.getBytes(encoding)); write(' '); if (messageID != null) write(messageID.getBytes(encoding)); else write('-'); write(' '); write('-'); write(' '); if (includeBOM && encoding.equals("UTF-8")) write(BOM); } void writeInt(int i) { if (i >= 100) writeNNN(i); else if (i >= 10) writeNN(i); else writeN(i); } void write(Calendar timeStamp) { writeNNNN(timeStamp.get(Calendar.YEAR)); write('-'); writeNN(timeStamp.get(Calendar.MONTH) + 1); write('-'); writeNN(timeStamp.get(Calendar.DAY_OF_MONTH)); write('T'); writeNN(timeStamp.get(Calendar.HOUR_OF_DAY)); write(':'); writeNN(timeStamp.get(Calendar.MINUTE)); write(':'); writeNN(timeStamp.get(Calendar.SECOND)); write('.'); writeNNN(timeStamp.get(Calendar.MILLISECOND)); int tzOffset = timeStamp.get(Calendar.ZONE_OFFSET) + timeStamp.get(Calendar.DST_OFFSET); if (tzOffset == 0) write('Z'); else { tzOffset /= 60000; if (tzOffset > 0) write('+'); else { write('-'); tzOffset = -tzOffset; } writeNN(tzOffset / 60); write(':'); writeNN(tzOffset % 60); } } void writeNNNN(int i) { writeNN(i / 100); writeNN(i % 100); } void writeNNN(int i) { writeN(i / 100); writeNN(i % 100); } void writeNN(int i) { write(DIGITS_X0[i]); write(DIGITS_0X[i]); } void writeN(int i) { write(DIGITS_0X[i]); } } private static String toString(DatagramPacket packet) { try { int len = packet.getLength(); boolean truncate = len > MSG_PROMPT_LEN; String s = new String(packet.getData(), 0, truncate ? MSG_PROMPT_LEN : len, "UTF-8"); if (truncate) s += "..."; return s; } catch (UnsupportedEncodingException e) { throw new RuntimeException(e); } } private abstract class ActiveConnection implements Closeable { final Connection conn; final Connection remoteConn; ActiveConnection(Connection conn, Connection remoteConn) { this.conn = conn; this.remoteConn = remoteConn; } abstract void sendMessage(DatagramPacket msg) throws IOException, IncompatibleConnectionException, GeneralSecurityException; } private class UDPConnection extends ActiveConnection { DatagramSocket ds; UDPConnection(Connection conn, Connection remoteConn) { super(conn, remoteConn); } @Override void sendMessage(DatagramPacket msg) throws IOException { if (ds == null) ds = conn.createDatagramSocket(); InetSocketAddress endPoint = remoteConn.getEndPoint(); LOG.info("Send audit message to {}", endPoint); if (LOG.isDebugEnabled()) LOG.debug(AuditLogger.toString(msg)); msg.setSocketAddress(endPoint); ds.send(msg); } @Override public void close() { if (ds != null) { ds.close(); ds = null; } } } private class TCPConnection extends ActiveConnection { Socket sock; OutputStream out; ScheduledFuture<?> idleTimer; TCPConnection(Connection conn, Connection remoteConn) { super(conn, remoteConn); } void connect() throws IOException, IncompatibleConnectionException, GeneralSecurityException { if (sock == null) { sock = conn.connect(remoteConn); out = sock.getOutputStream(); } } @Override synchronized void sendMessage(DatagramPacket packet) throws IOException, IncompatibleConnectionException, GeneralSecurityException { stopIdleTimer(); connect(); try { trySendMessage(packet); } catch (IOException e) { LOG.info("Failed to send audit message to {} - reconnect", sock, e); close(); connect(); trySendMessage(packet); } startIdleTimer(); } void trySendMessage(DatagramPacket packet) throws IOException { LOG.info("Send audit message to {}", sock); if (LOG.isDebugEnabled()) LOG.debug(AuditLogger.toString(packet)); out.write(Integer.toString(packet.getLength()).getBytes(encoding)); out.write(' '); out.write(packet.getData(), packet.getOffset(), packet.getLength()); out.flush(); } private void startIdleTimer() { int idleTimeout = conn.getIdleTimeout(); if (idleTimeout > 0) { LOG.debug("Start Idle timeout of {} ms for {}", idleTimeout, sock); try { idleTimer = conn.getDevice().schedule( new Runnable() { @Override public void run() { onIdleTimerExpired(); } }, idleTimeout, TimeUnit.MILLISECONDS); } catch (Exception e) { LOG.warn("Failed to start Idle timeout", e); } } } private void stopIdleTimer() { if (idleTimer != null) { LOG.debug("Stop Idle timer for {}", sock); idleTimer.cancel(false); idleTimer = null; } } @Override public synchronized void close() { stopIdleTimer(); closeSocket(); } private void closeSocket() { if (sock != null) conn.close(sock); sock = null; out = null; } private void onIdleTimerExpired() { ScheduledFuture<?> expiredIdleTimer = idleTimer; synchronized (this) { if (expiredIdleTimer != idleTimer) { LOG.debug("Detect restart of Idle timer for {}", sock); } else { LOG.info("Idle timeout for {} expired", sock); idleTimer = null; closeSocket(); } } } } public static Disruptor<AuditMessageEvent> getDisruptor(AuditLogger logger) { if (disruptor == null) disruptor = initializeDisruptor(logger); return disruptor; } private static Disruptor<AuditMessageEvent> initializeDisruptor(AuditLogger logger) { // Executor that will be used to construct new threads for consumers Executor executor = Executors.newCachedThreadPool(); // The factory for the event AuditMessageEventFactory factory = new AuditMessageEventFactory(); // Specify the size of the ring buffer, must be power of 2. int bufferSize = 8; Disruptor<AuditMessageEvent> disruptorInstance = new Disruptor<AuditMessageEvent>(factory, bufferSize, executor); // Connect the handler disruptorInstance.handleEventsWith(new AuditMessageEventHandler()); // Start the Disruptor, starts all threads running disruptorInstance.start(); return disruptorInstance; } }