AuditorImpl.java 16 KB
Newer Older
1 2 3 4 5
/**
 * $RCSfile$
 * $Revision: 3186 $
 * $Date: 2005-12-11 00:07:52 -0300 (Sun, 11 Dec 2005) $
 *
6
 * Copyright (C) 2005-2008 Jive Software. All rights reserved.
7 8
 *
 * This software is published under the terms of the GNU Public License (GPL),
9 10
 * a copy of which is included in this distribution, or a commercial license
 * agreement with Jive.
11 12
 */

13
package org.jivesoftware.openfire.audit.spi;
14 15 16

import org.dom4j.DocumentFactory;
import org.dom4j.Element;
17 18 19
import org.jivesoftware.openfire.audit.AuditManager;
import org.jivesoftware.openfire.audit.Auditor;
import org.jivesoftware.openfire.session.Session;
Gaston Dombiak's avatar
Gaston Dombiak committed
20
import org.jivesoftware.util.*;
21 22 23 24 25 26
import org.xmpp.packet.IQ;
import org.xmpp.packet.Message;
import org.xmpp.packet.Packet;
import org.xmpp.packet.Presence;

import java.io.*;
27 28
import java.util.*;
import java.util.concurrent.BlockingQueue;
29
import java.util.concurrent.LinkedBlockingQueue;
30 31 32 33 34 35 36

public class AuditorImpl implements Auditor {

    private AuditManager auditManager;
    private File currentAuditFile;
    private Writer writer;
    private org.jivesoftware.util.XMLWriter xmlWriter;
37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59
    /**
     * 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 int 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 int 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.
     */
60 61 62 63 64
    private boolean closed = false;
    /**
     * Directoty (absolute path) where the audit files will be saved.
     */
    private String logDir;
65 66 67 68
    /**
     * File (or better say directory) of the folder that contains the audit logs.
     */
    private File baseFolder;
69 70 71 72

    /**
     * Queue that holds the audited packets that will be later saved to an XML file.
     */
73
    private BlockingQueue<AuditPacket> logQueue = new LinkedBlockingQueue<AuditPacket>();
74 75 76 77 78 79

    /**
     * Timer to save queued logs to the XML file.
     */
    private Timer timer = new Timer("Auditor");
    private SaveQueuedPacketsTask saveQueuedPacketsTask;
80
    private FastDateFormat dateFormat;
81
    private static FastDateFormat auditFormat;
82 83 84

    public AuditorImpl(AuditManager manager) {
        auditManager = manager;
85
        dateFormat = FastDateFormat.getInstance("yyyyMMdd", TimeZone.getTimeZone("UTC"));
86
        auditFormat = FastDateFormat.getInstance("MMM dd, yyyy hh:mm:ss:SSS a", JiveGlobals.getLocale());
87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117
    }

    protected void setMaxValues(int totalSize, int fileSize, int days) {
        maxTotalSize = totalSize * 1024*1024;
        maxFileSize = fileSize * 1024*1024;
        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();
        timer.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()) {
            baseFolder.mkdir();
        }
    }

    public int getQueuedPacketsNumber() {
        return logQueue.size();
118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139
    }

    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);
                }
            }
        }
    }

140 141 142 143 144 145 146
    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));
        }
    }

147
    public void stop() {
148 149
        // Stop queuing packets since we are being stopped
        closed = true;
150 151 152 153 154 155 156 157 158 159 160 161 162 163
        // Stop the scheduled task for saving queued packets to the XML file
        timer.cancel();
        // Save all remaining queued packets to the XML file
        saveQueuedPackets();
        close();
    }

    private void close() {
        if (xmlWriter != null) {
            try {
                xmlWriter.flush();
                writer.write("</jive>");
                xmlWriter.close();
                writer = null;
164
                xmlWriter = null;
165 166 167 168 169 170 171
            }
            catch (Exception e) {
                Log.error(LocaleUtils.getLocalizedString("admin.error"), e);
            }
        }
    }

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

175 176 177 178 179
        // 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);
180 181 182
        }
    }

183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203
    /**
     * 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() {
            public boolean accept(File dir, String name) {
                return name.startsWith("jive.audit-") && name.endsWith(".log");
            }
        };
        File[] files = baseFolder.listFiles(filter);
        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<File> sortedFiles = new ArrayList<File>(Arrays.asList(files));
204 205 206
            Collections.sort(sortedFiles, new Comparator<File>() {
                public int compare(File o1, File o2) {
                    return o1.getName().compareTo(o2.getName());
207 208 209 210 211 212 213 214 215 216 217 218 219 220
                }
            });
            // 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
                fileToDelete.delete();
            }
        }
221 222
    }

223 224 225 226 227 228 229
    /**
     * 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;
230 231
        }

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

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

239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254
        // Get list of audit files to delete
        FilenameFilter filter = new FilenameFilter() {
            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();
            }
            fileToDelete.delete();
        }
255 256
    }

257
    private void createAuditFile(Date auditDate) throws IOException {
258
        close();
259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274
        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, 23);
            calendar.set(Calendar.MINUTE, 59);
            calendar.set(Calendar.SECOND, 59);
            calendar.set(Calendar.MILLISECOND, 999);
            currentDateLimit = calendar.getTime();
        }

        final String filePrefix = "jive.audit-" + dateFormat.format(auditDate) + "-";
        // Get list of existing audit files
        FilenameFilter filter = new FilenameFilter() {
            public boolean accept(File dir, String name) {
                return name.startsWith(filePrefix) && name.endsWith(".log");
275
            }
276 277 278 279 280
        };
        File[] files = baseFolder.listFiles(filter);
        if (files.length == 0) {
            // This is the first audit file for the day
            currentAuditFile = new File(logDir, filePrefix + "000.log");
281
        }
282 283 284 285 286 287 288 289 290 291 292 293 294 295 296
        else {
            // Search the last index used for the day
            File lastFile = files[files.length - 1];
            StringTokenizer tokenizer = new StringTokenizer(lastFile.getName(), "-.");
            // Skip "jive"
            tokenizer.nextToken();
            // Skip "audit"
            tokenizer.nextToken();
            // Skip "date"
            tokenizer.nextToken();
            int index = Integer.parseInt(tokenizer.nextToken()) + 1;
            if (index > 999) {
                Log.warn("Failed to created audit file. Max limit of 999 files has been reached " +
                        "for the date: " + dateFormat.format(auditDate));
                return;
297
            }
298 299 300 301 302 303 304 305 306 307 308
            currentAuditFile = new File(logDir,
                    filePrefix + StringUtils.zeroPadString(Integer.toString(index), 3) + ".log");
        }


        // Find the next available log file name
        /*for (int i = 0; i < 1000; i++) {
            currentAuditFile = new File(logDir,
                    filePrefix + StringUtils.zeroPadString(Integer.toString(i), 3) + ".log");
            if (!currentAuditFile.exists()) {
                break;
309 310 311
            }
        }

312 313 314 315 316 317
        if (currentAuditFile == null) {
            Log.warn("Audit log not create since there are more than 999 files for the date: " +
                    dateFormat.format(auditDate));
            return;
        }*/

318 319 320 321 322 323
        writer = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(currentAuditFile), "UTF-8"));
        writer.write("<jive xmlns=\"http://www.jivesoftware.org\">");
        xmlWriter = new org.jivesoftware.util.XMLWriter(writer);
    }

    /**
324
     * Saves the queued entries to an XML file and checks that very old files are deleted.
325 326 327 328
     */
    private class SaveQueuedPacketsTask extends TimerTask {
        public void run() {
            try {
329 330 331
                // Ensure that saved audit logs are not too old
                ensureMaxDays();
                // Save queued packets to the audit logs
332 333 334 335 336 337 338 339 340
                saveQueuedPackets();
            }
            catch (Throwable e) {
                Log.error(LocaleUtils.getLocalizedString("admin.error"), e);
            }
        }
    }

    private void saveQueuedPackets() {
341 342 343 344
        List<AuditPacket> packets = new ArrayList<AuditPacket>(logQueue.size());
        logQueue.drainTo(packets);
        for (AuditPacket auditPacket : packets) {
            try {
345
                prepareAuditFile(auditPacket.getCreationDate());
346 347 348 349
                Element element = auditPacket.getElement();
                // Protect against null elements.
                if (element != null) {
                    xmlWriter.write(element);
350
                }
351 352 353 354 355
            }
            catch (IOException e) {
                Log.error(LocaleUtils.getLocalizedString("admin.error"), e);
                // Add again the entry to the queue to save it later
                if (xmlWriter != null) {
356 357 358 359 360 361 362 363 364 365
                    logQueue.add(auditPacket);
                }
            }
        }
        try {
            if (xmlWriter != null) {
                xmlWriter.flush();
            }
        }
        catch (IOException ioe) {
366
            Log.error(ioe);
367 368 369 370 371 372 373 374 375 376 377 378 379 380 381
        }
    }

    /**
     * Wrapper on a Packet with information about the packet's status at the moment
     * when the message was queued.<p>
     *
     * 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;
382
        private Date creationDate;
383 384 385

        public AuditPacket(Packet packet, Session session) {
            element = docFactory.createElement("packet", "http://www.jivesoftware.org");
386
            creationDate = new Date();
387
            if (session != null && session.getStreamID() != null) {
388 389
                element.addAttribute("streamID", session.getStreamID().toString());
            }
390
            switch (session == null ? 0 : session.getStatus()) {
391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408
                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;
            }
409
            element.addAttribute("timestamp", auditFormat.format(creationDate));
410 411 412 413 414 415 416 417 418 419 420
            element.add(packet.getElement());
        }

        /**
         * Returns the Element associated with this audit packet.
         *
         * @return the Element.
         */
        public Element getElement() {
            return element;
        }
421 422 423 424 425 426 427 428 429 430

        /**
         * 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;
        }
431 432
    }
}