All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.jivesoftware.openfire.audit.spi.AuditorImpl Maven / Gradle / Ivy

The newest version!
/*
 * Copyright (C) 2005-2008 Jive Software. All rights reserved.
 *
 * Licensed under the Apache License, Version 2.0 (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.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.jivesoftware.openfire.audit.spi;

import java.io.BufferedWriter;
import java.io.File;
import java.io.FileOutputStream;
import java.io.FilenameFilter;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.List;
import java.util.TimeZone;
import java.util.TimerTask;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;

import org.dom4j.DocumentFactory;
import org.dom4j.Element;
import org.jivesoftware.openfire.audit.AuditManager;
import org.jivesoftware.openfire.audit.Auditor;
import org.jivesoftware.openfire.session.Session;
import org.jivesoftware.util.FastDateFormat;
import org.jivesoftware.util.JiveGlobals;
import org.jivesoftware.util.LocaleUtils;
import org.jivesoftware.util.StringUtils;
import org.jivesoftware.util.TaskEngine;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.xmpp.packet.IQ;
import org.xmpp.packet.Message;
import org.xmpp.packet.Packet;
import org.xmpp.packet.Presence;

public class AuditorImpl implements Auditor {

    private static final Logger Log = LoggerFactory.getLogger(AuditorImpl.class);

    private AuditManager auditManager;
    private File currentAuditFile;
    private Writer writer;
    private org.jivesoftware.util.XMLWriter xmlWriter;
    /**
     * Limit date used to detect when we need to rollover files. This date will be
     * configured as the last second of the day.
     */
    private Date currentDateLimit;
    /**
     * Max size in bytes that all audit log files may have. When the limit is reached
     * oldest audit log files will be removed until total size is under the limit.
     */
    private long maxTotalSize;
    /**
     * Max size in bytes that each audit log file may have. Once the limit has been
     * reached a new audit file will be created.
     */
    private long maxFileSize;
    /**
     * Max number of days to keep audit information. Once the limit has been reached
     * audit files that contain information that exceed the limit will be deleted.
     */
    private int maxDays;
    /**
     * Flag that indicates if packets can still be accepted to be saved to the audit log.
     */
    private boolean closed = false;
    /**
     * Directoty (absolute path) where the audit files will be saved.
     */
    private String logDir;
    /**
     * File (or better say directory) of the folder that contains the audit logs.
     */
    private File baseFolder;

    /**
     * Queue that holds the audited packets that will be later saved to an XML file.
     */
    private BlockingQueue logQueue = new LinkedBlockingQueue<>();

    /**
     * Allow only a limited number of files for each day, max. three digits (000-999)
     */
    private final int maxTotalFilesDay = 1000;
    /**
     * Track the current index number `...-nnn.log´
     */
    private int filesIndex = 0;
    /**
     * Timer to save queued logs to the XML file.
     */
    private SaveQueuedPacketsTask saveQueuedPacketsTask;
    private FastDateFormat dateFormat;
    private static FastDateFormat auditFormat;

    public AuditorImpl(AuditManager manager) {
        auditManager = manager;
        dateFormat = FastDateFormat.getInstance("yyyyMMdd", TimeZone.getTimeZone("UTC"));
        auditFormat = FastDateFormat.getInstance("MMM dd, yyyy hh:mm:ss:SSS a", JiveGlobals.getLocale());
    }

    protected void setMaxValues(int totalSize, int fileSize, int days) {
        maxTotalSize = (long) totalSize * 1024l * 1024l;
        maxFileSize = (long) fileSize * 1024l * 1024l;
        maxDays = days;
    }

    public void setLogTimeout(int logTimeout) {
        // Cancel any existing task because the timeout has changed
        if (saveQueuedPacketsTask != null) {
            saveQueuedPacketsTask.cancel();
        }
        // Create a new task and schedule it with the new timeout
        saveQueuedPacketsTask = new SaveQueuedPacketsTask();
        TaskEngine.getInstance().schedule(saveQueuedPacketsTask, logTimeout, logTimeout);

    }

    public void setLogDir(String logDir) {
        this.logDir = logDir;
        // Create and catch file of the base folder that will contain audit files
        baseFolder = new File(logDir);
        // Create the folder if it does not exist
        if (!baseFolder.exists()) {
            if ( !baseFolder.mkdir() ) {
                Log.error( "Unable to create log directory: {}", baseFolder );
            }
        }
    }

    @Override
    public int getQueuedPacketsNumber() {
        return logQueue.size();
    }

    @Override
    public void audit(Packet packet, Session session) {
        if (auditManager.isEnabled()) {
            if (packet instanceof Message) {
                if (auditManager.isAuditMessage()) {
                    writePacket(packet, session);
                }
            }
            else if (packet instanceof Presence) {
                if (auditManager.isAuditPresence()) {
                    writePacket(packet, session);
                }
            }
            else if (packet instanceof IQ) {
                if (auditManager.isAuditIQ()) {
                    writePacket(packet, session);
                }
            }
        }
    }

    private void writePacket(Packet packet, Session session) {
        if (!closed) {
            // Add to the logging queue this new entry that will be saved later
            logQueue.add(new AuditPacket(packet.createCopy(), session));
        }
    }

    @Override
    public void stop() {
        // Stop queuing packets since we are being stopped
        closed = true;
        // Save all remaining queued packets to the XML file
        saveQueuedPackets();
        close();
    }

    private void close() {
        if (xmlWriter != null) {
            try {
                xmlWriter.flush();
                writer.write("");
                xmlWriter.close();
                writer = null;
                xmlWriter = null;
            }
            catch (Exception e) {
                Log.error(LocaleUtils.getLocalizedString("admin.error"), e);
            }
        }
    }

    private void prepareAuditFile(Date auditDate) throws IOException {
        ensureMaxTotalSize();

        // Rotate file if: we just started, current file size exceeded limit or date has changed
        if (currentAuditFile == null || currentAuditFile.length() > maxFileSize ||
                xmlWriter == null || currentDateLimit == null || auditDate.after(currentDateLimit))
        {
            createAuditFile(auditDate);
        }
    }

    /**
     * Ensures that max total size limit is not exceeded. If total size of audit files
     * exceed the limit then oldest audit files will be removed until total size does
     * not exceed limit.
     */
    private void ensureMaxTotalSize() {
        // Get list of existing audit files
        FilenameFilter filter = new FilenameFilter() {
            @Override
            public boolean accept(File dir, String name) {
                return name.startsWith("jive.audit-") && name.endsWith(".log");
            }
        };
        File[] files = baseFolder.listFiles(filter);
        if (files == null) {
            Log.debug( "Path '{}' does not denote a directory, or an IO exception occured while trying to list its content.", baseFolder );
            return;
        }
        long totalLength = 0;
        for (File file : files) {
            totalLength = totalLength + file.length();
        }
        // Check if total size has been exceeded
        if (totalLength > maxTotalSize) {
            // Sort files by name (chronological order)
            List sortedFiles = new ArrayList<>(Arrays.asList(files));
            Collections.sort(sortedFiles, new Comparator() {
                @Override
                public int compare(File o1, File o2) {
                    return o1.getName().compareTo(o2.getName());
                }
            });
            // Delete as many old files as required to be under the limit
            while (totalLength > maxTotalSize && !sortedFiles.isEmpty()) {
                File fileToDelete = sortedFiles.remove(0);
                totalLength = totalLength - fileToDelete.length();
                if (fileToDelete.equals(currentAuditFile)) {
                    // Close current file
                    close();
                }
                // Delete oldest file
                if ( !fileToDelete.delete() )
                {
                    Log.warn( "Unable to delete file '{}' as part of regular log rotation based on size of files (Openfire failed to clean up after itself)!", fileToDelete );
                }
            }
        }
    }

    /**
     * Deletes old audit files that exceeded the max number of days limit.
     */
    private void ensureMaxDays() {
        if (maxDays == -1) {
            // Do nothing since we don't have any limit
            return;
        }

        // Set limit date after which we need to delete old audit files
        Calendar calendar = Calendar.getInstance();
        calendar.add(Calendar.DATE, maxDays * -1);

        final String oldestFile =
                "jive.audit-" + dateFormat.format(calendar.getTime()) + "-000.log";

        // Get list of audit files to delete
        FilenameFilter filter = new FilenameFilter() {
            @Override
            public boolean accept(File dir, String name) {
                return name.startsWith("jive.audit-") && name.endsWith(".log") &&
                        name.compareTo(oldestFile) < 0;
            }
        };
        File[] files = baseFolder.listFiles(filter);
        // Delete old audit files
        for (File fileToDelete : files) {
            if (fileToDelete.equals(currentAuditFile)) {
                // Close current file
                close();
            }
            if ( !fileToDelete.delete() )
            {
                Log.warn( "Unable to delete file '{}' as part of regular log rotation based on age of file. (Openfire failed to clean up after itself)!", fileToDelete );
            }
        }
    }

    /* if this new logic still causes problems one may want to 
    * use log4j or change the file format from YYYYmmdd-nnn to YYYYmmdd-HHMM */
    /**
    * Sets xmlWriter so this class can use it to write audit logs
* The audit filename currentAuditFile will be `jive.audit-YYYYmmdd-nnn.log´
* `nnn´ will be reset to `000´ when a new log file is created the next day
* `nnn´ will be increased for log files which belong to the same day
* WARNING: If log files of the current day are deleted and the server is restarted then * the value of `nnn´ may be random (it's calculated by `Math.max(files.length, filesIndex);´ * with `filesIndex=0´ and `files.length=nr(existing jive.audit-YYYYmmdd-???.log files)´ - * if there are 10 audit files (033-043) then nnn will be 10 instead of 44).
* If `nnn=999´ then all audit data will be written to this file till the next day.
* @param auditDate * @throws IOException */ private void createAuditFile(Date auditDate) throws IOException { final String filePrefix = "jive.audit-" + dateFormat.format(auditDate) + "-"; if (currentDateLimit == null || auditDate.after(currentDateLimit)) { // Set limit date after which we need to rollover the audit file (based on the date) Calendar calendar = Calendar.getInstance(); calendar.setTime(auditDate); calendar.set(Calendar.HOUR_OF_DAY, 23); calendar.set(Calendar.MINUTE, 59); calendar.set(Calendar.SECOND, 59); calendar.set(Calendar.MILLISECOND, 999); currentDateLimit = calendar.getTime(); filesIndex = 0; } // Get list of existing audit files FilenameFilter filter = new FilenameFilter() { @Override public boolean accept(File dir, String name) { return name.startsWith(filePrefix) && name.endsWith(".log"); } }; File[] files = baseFolder.listFiles(filter); // if some daily files were already deleted then files.length will be smaller than filesIndex // see also WARNING above filesIndex = Math.max(files.length, filesIndex); if (filesIndex >= maxTotalFilesDay) { // don't close this file, continue auditing to it return; } File tmpAuditFile = new File(logDir, filePrefix + StringUtils.zeroPadString(Integer.toString(filesIndex), 3) + ".log"); if ( (filesIndex == maxTotalFilesDay-1) && !tmpAuditFile.exists() ) { Log.warn("Creating last audit file for this date: " + dateFormat.format(auditDate)); } while ( (filesIndex<(maxTotalFilesDay-1)) && (tmpAuditFile.exists()) ) { Log.debug("Audit file '"+ tmpAuditFile.getName() +"' does already exist."); filesIndex++; tmpAuditFile = new File(logDir, filePrefix + StringUtils.zeroPadString(Integer.toString(filesIndex), 3) + ".log"); } currentAuditFile = tmpAuditFile; close(); // always append to an existing file (after restart) writer = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(currentAuditFile, true), StandardCharsets.UTF_8)); writer.write(""); xmlWriter = new org.jivesoftware.util.XMLWriter(writer); } /** * Saves the queued entries to an XML file and checks that very old files are deleted. */ private class SaveQueuedPacketsTask extends TimerTask { @Override public void run() { try { // Ensure that saved audit logs are not too old ensureMaxDays(); // Save queued packets to the audit logs saveQueuedPackets(); } catch (Throwable e) { Log.error(LocaleUtils.getLocalizedString("admin.error"), e); } } } private void saveQueuedPackets() { List packets = new ArrayList<>(logQueue.size()); logQueue.drainTo(packets); for (AuditPacket auditPacket : packets) { try { prepareAuditFile(auditPacket.getCreationDate()); Element element = auditPacket.getElement(); // Protect against null elements. if (element != null) { xmlWriter.write(element); } } catch (IOException e) { Log.error(LocaleUtils.getLocalizedString("admin.error"), e); // Add again the entry to the queue to save it later if (xmlWriter != null) { logQueue.add(auditPacket); } } } try { if (xmlWriter != null) { xmlWriter.flush(); } } catch (IOException ioe) { Log.error(ioe.getMessage(), ioe); } } /** * Wrapper on a Packet with information about the packet's status at the moment * when the message was queued.

* * The idea is to wrap every packet that is needed to be audited and then add the * wrapper to a queue that will be later processed (i.e. saved to the XML file). */ private static class AuditPacket { private static DocumentFactory docFactory = DocumentFactory.getInstance(); private Element element; private Date creationDate; public AuditPacket(Packet packet, Session session) { element = docFactory.createElement("packet", "http://www.jivesoftware.org"); creationDate = new Date(); if (session != null && session.getStreamID() != null) { element.addAttribute("streamID", session.getStreamID().toString()); } switch (session == null ? 0 : session.getStatus()) { case Session.STATUS_AUTHENTICATED: element.addAttribute("status", "auth"); break; case Session.STATUS_CLOSED: element.addAttribute("status", "closed"); break; case Session.STATUS_CONNECTED: element.addAttribute("status", "connected"); // This is a workaround. Since we don't want to have an incorrect FROM attribute // value we need to clean up the FROM attribute. The FROM attribute will contain // an incorrect value since we are setting a fake JID until the user actually // authenticates with the server. packet.setFrom((String) null); break; default: element.addAttribute("status", "unknown"); break; } element.addAttribute("timestamp", auditFormat.format(creationDate)); element.add(packet.getElement()); } /** * Returns the Element associated with this audit packet. * * @return the Element. */ public Element getElement() { return element; } /** * Returns the date when the packet was audited. This is the time when the * packet was queued to be saved. * * @return the date when the packet was audited. */ public Date getCreationDate() { return creationDate; } } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy