Package org.dcm4che3.net.audit

Source Code of org.dcm4che3.net.audit.AuditLogger

/* ***** 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 java.io.ByteArrayOutputStream;
import java.io.Closeable;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.FilenameFilter;
import java.io.IOException;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.lang.management.ManagementFactory;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.net.URI;
import java.net.UnknownHostException;
import java.nio.charset.Charset;
import java.security.GeneralSecurityException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Collection;
import java.util.Comparator;
import java.util.GregorianCalendar;
import java.util.List;
import java.util.Locale;
import java.util.Random;
import java.util.TimeZone;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;

import javax.net.ssl.SSLContext;

import org.dcm4che3.audit.ActiveParticipant;
import org.dcm4che3.audit.AuditMessage;
import org.dcm4che3.audit.AuditSourceIdentification;
import org.dcm4che3.audit.AuditSourceTypeCode;
import org.dcm4che3.audit.AuditMessages;
import org.dcm4che3.audit.AuditMessages.RoleIDCode;
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;

/**
* @author Gunter Zeilinger <gunterze@gmail.com>
* @author Michael Backhaus <michael.backhaus@agfa.com>
*/
public class AuditLogger extends DeviceExtension {

    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 = "DICOM+RFC3881";

    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;
   
    private Device arrDevice;
    private Facility facility = Facility.authpriv;
    private Severity successSeverity = Severity.notice;
    private Severity minorFailureSeverity = Severity.warning;
    private Severity seriousFailureSeverity = Severity.err;
    private Severity majorFailureSeverity = Severity.crit;
    private String applicationName;
    private String auditSourceID;
    private String auditEnterpriseSiteID;
    private String[] auditSourceTypeCodes = {};
    private String messageID = MESSAGE_ID;
    private String encoding = "UTF-8";
    private String schemaURI = AuditMessages.SCHEMA_URI;
    private boolean timestampInUTC = false;
    private boolean includeBOM = true;
    private boolean formatXML;
    private Boolean installed;
    private Boolean includeInstanceUID = false;
    private File spoolDirectory;
    private String spoolFileNamePrefix = "audit";
    private String spoolFileNameSuffix= ".log";
    private int retryInterval;

    private final List<AuditSuppressCriteria> suppressAuditMessageFilters =
            new ArrayList<AuditSuppressCriteria>(0);
    private final List<Connection> conns = new ArrayList<Connection>(1);

    private transient MessageBuilder builder;
    private transient ActiveConnection 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 final Device getAuditRecordRepositoryDevice() {
        return arrDevice;
    }

    public String getAuditRecordRepositoryDeviceName() {
        if (arrDevice == null)
            throw new IllegalStateException("AuditRecordRepositoryDevice not initalized");
        return arrDevice.getDeviceName();
    }

    public void setAuditRecordRepositoryDevice(Device arrDevice) {
        SafeClose.close(activeConnection);
        activeConnection = null;
        this.arrDevice = arrDevice;
    }

    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("dicomPrimaryDeviceType")) {
                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 isInstalled() {
        return device != null && device.isInstalled()
                && (installed == null || installed.booleanValue());
    }

    public final Boolean getInstalled() {
        return installed;
    }

    public void setInstalled(Boolean installed) {
        if (installed != null && installed.booleanValue()
                && device != null && !device.isInstalled())
            throw new IllegalStateException("owning device not installed");
        this.installed = installed;
    }

    public Boolean isIncludeInstanceUID() {
        return includeInstanceUID;
    }

    public void setIncludeInstanceUID(Boolean includeInstanceUID) {
        this.includeInstanceUID = includeInstanceUID;
    }

    /**
     * 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());
        conns.add(conn);
    }

    @Override
    public void verifyNotUsed(Connection conn) {
        if (conns.contains(conn))
            throw new IllegalStateException(conn + " used by Audit Logger");
    }

    public boolean removeConnection(Connection conn) {
        return conns.remove(conn);
    }

    public List<Connection> getConnections() {
        return conns;
    }

    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);
        setSpoolDirectory(from.spoolDirectory);
        setSpoolFileNamePrefix(from.spoolFileNamePrefix);
        setSpoolFileNameSuffix(from.spoolFileNameSuffix);
        setRetryInterval(from.retryInterval);
        setInstalled(from.installed);
        setAuditRecordRepositoryDevice(from.arrDevice);
        setAuditSuppressCriteriaList(from.suppressAuditMessageFilters);
        device.reconfigureConnections(conns, from.conns);
        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.
     *
     * 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 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 {
        if (getNumberOfQueuedMessages() > 0) {
            spoolMessage(msg);
        } else {
            try {
                activeConnection().sendMessage(msg);
                lastSentTimeInMillis = System.currentTimeMillis();
                return SendStatus.SENT;
            } catch (IOException e) {
                lastException = e;
                if (retryInterval > 0) {
                    LOG.info("Failed to send audit message:", e);
                    spoolMessage(msg);
                    scheduleRetry();
                } else {
                    throw e;
                }
            }
        }
        return SendStatus.QUEUED;
    }

    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(DatagramPacket msg) throws IOException {
        if (spoolDirectory != null)
            spoolDirectory.mkdirs();

        File f = null;
        try {
            f = File.createTempFile(spoolFileNamePrefix, 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", e);
        } finally {
            if (f != null)
                f.delete();
        }
    }

    public void sendQueuedMessages() {
        File dir = spoolDirectory;
        if (dir == null)
            return;

        try {
            File[] queuedMessages = dir.listFiles(FILENAME_FILTER);
            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.getPath() + ".err");
                        file.renameTo(dest);
                        continue;
                    }
                    activeConnection().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);
            scheduleRetry();
        }
        synchronized (this) {
            notify();
        }
    }

    public Exception getLastException() {
        return lastException;
    }

    public long getLastSentTimeInMillis() {
        return lastSentTimeInMillis;
    }

    public int getNumberOfQueuedMessages() {
        try {
            return spoolDirectory.list(FILENAME_FILTER).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 {
        while (getNumberOfQueuedMessages() > 0)
            wait(timeout);
    }

    public synchronized void closeActiveConnection() {
        ActiveConnection activeConnection = this.activeConnection;
        if (activeConnection != null) {
            try {
                activeConnection.close();
            } catch (IOException e) {
                throw new AssertionError(e);
            }
            this.activeConnection = null;
        }
    }

    private synchronized ActiveConnection activeConnection()
            throws IncompatibleConnectionException {
        ActiveConnection activeConnection = this.activeConnection;
        if (activeConnection != null)
            return activeConnection;

        Device arrDev = this.arrDevice;
        if (arrDevice == null)
            throw new IllegalStateException("No AuditRecordRepositoryDevice initalized");

        AuditRecordRepository arr = arrDev.getDeviceExtension(AuditRecordRepository.class);
        if (arr == null)
            throw new IllegalStateException("AuditRecordRepositoryDevice "
                    + arrDevice.getDeviceName()
                    + " does not provide Audit Record Repository");

        for (Connection remoteConn : arr.getConnections())
            if (remoteConn.isInstalled() && remoteConn.isServer())
                for (Connection conn : conns)
                    if (conn.isInstalled() && conn.isCompatible(remoteConn)) {
                        return (this.activeConnection =
                                conn.getProtocol().isTCP()
                                ? new TCPConnection(conn, remoteConn)
                                : new UDPConnection(conn, remoteConn));
                     }
        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);
                AuditMessages.toXML(msg, builder, formatXML, encoding, schemaURI);
            } catch (IOException e) {
                assert false : 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) {
                assert false : 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();
                }
            }
        }

    }

}
TOP

Related Classes of org.dcm4che3.net.audit.AuditLogger

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.