/* * JBoss, Home of Professional Open Source. * Copyright 2017 Red Hat, Inc., and individual contributors * as indicated by the @author tags. * * 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.wildfly.security.audit; import java.io.File; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.nio.file.StandardCopyOption; import java.text.SimpleDateFormat; import java.util.Calendar; import java.util.Date; import java.util.TimeZone; import static org.wildfly.common.Assert.checkNotNullParam; import static org.wildfly.security._private.ElytronMessages.audit; /** * An audit endpoint which rotates the log at a preset time interval or the size of the log. * * Based on {@link org.jboss.logmanager.handlers.PeriodicSizeRotatingFileHandler}. * * @author <a href="mailto:jkalina@redhat.com">Jan Kalina</a> * @author <a href="mailto:jperkins@redhat.com">James R. Perkins</a> */ public class RotatingFileAuditEndpoint extends FileAuditEndpoint { private final long rotateSize; private final int maxBackupIndex; private final boolean rotateOnBoot; private final SimpleDateFormat format; private final Period period; private final TimeZone timeZone; private String nextSuffix; private long nextRollover = Long.MAX_VALUE; private long currentSize = 0; RotatingFileAuditEndpoint(Builder builder) throws IOException { super(builder); this.rotateSize = builder.rotateSize; this.maxBackupIndex = builder.maxBackupIndex; this.rotateOnBoot = builder.rotateOnBoot; this.format = builder.format; this.period = builder.period; this.timeZone = builder.timeZone; final File file = getFile(); calcNextRollover(file != null && file.lastModified() > 0 ? file.lastModified() : System.currentTimeMillis()); if (rotateOnBoot && maxBackupIndex > 0 && file != null && file.exists() && file.length() > 0L) { rotate(file); } } @Override protected void write(byte[] bytes) throws IOException { super.write(bytes); currentSize += bytes.length; } @Override protected void preWrite(Date date) { final long recordMillis = date.getTime(); if (recordMillis >= nextRollover) { // time based rollover try { final File file = getFile(); closeStreams(); // close the original file (some OSes won't let you move/rename a file that is open) final Path target = Paths.get(file.getAbsolutePath() + nextSuffix); Files.move(file.toPath(), target, StandardCopyOption.REPLACE_EXISTING); setFile(file); currentSize = 0; } catch (IOException e) { audit.unableToRotateLogFile(e); } calcNextRollover(recordMillis); } else if (currentSize > rotateSize && maxBackupIndex > 0) { // file size based rollover try { final File file = getFile(); if (file == null) { // no file is set; a direct output stream or writer was specified return; } rotate(file); currentSize = 0; } catch (IOException e) { audit.unableToRotateLogFile(e); } } } /** * Moves file to file.1, file.1 to file.2 etc. Removes file.{maxBackupIndex} */ private void rotate(final File file) throws IOException { closeStreams(); final Path fileWithSuffix = Paths.get(file.getAbsolutePath() + nextSuffix); Files.deleteIfExists(Paths.get(fileWithSuffix + "." + maxBackupIndex)); for (int i = maxBackupIndex - 1; i >= 1; i--) { final Path src = Paths.get(fileWithSuffix + "." + i); if (Files.exists(src)) { final Path target = Paths.get(fileWithSuffix + "." + (i + 1)); Files.move(src, target, StandardCopyOption.REPLACE_EXISTING); } } Files.move(file.toPath(), Paths.get(fileWithSuffix + ".1"), StandardCopyOption.REPLACE_EXISTING); setFile(file); } /** * For given time and period obtains time when should be new log file started */ private void calcNextRollover(final long fromTime) { if (period == Period.NEVER || format == null) { nextRollover = Long.MAX_VALUE; return; } nextSuffix = format.format(new Date(fromTime)); final Calendar calendar = Calendar.getInstance(timeZone); calendar.setTimeInMillis(fromTime); final Period period = this.period; // clear out less-significant fields switch (period) { default: case YEAR: calendar.set(Calendar.MONTH, 0); case MONTH: calendar.set(Calendar.DAY_OF_MONTH, 0); calendar.clear(Calendar.WEEK_OF_MONTH); case WEEK: if (period == Period.WEEK) { calendar.set(Calendar.DAY_OF_WEEK, calendar.getFirstDayOfWeek()); } else { calendar.clear(Calendar.DAY_OF_WEEK); } calendar.clear(Calendar.DAY_OF_WEEK_IN_MONTH); case DAY: calendar.set(Calendar.HOUR_OF_DAY, 0); case HALF_DAY: if (period == Period.HALF_DAY) { calendar.set(Calendar.HOUR, 0); } else { //We want both HOUR_OF_DAY and (HOUR + AM_PM) to be zeroed out //This should ensure the hour is truly zeroed out calendar.set(Calendar.HOUR, 0); calendar.set(Calendar.AM_PM, 0); } case HOUR: calendar.set(Calendar.MINUTE, 0); case MINUTE: calendar.set(Calendar.SECOND, 0); calendar.set(Calendar.MILLISECOND, 0); } // increment the relevant field switch (period) { case YEAR: calendar.add(Calendar.YEAR, 1); break; case MONTH: calendar.add(Calendar.MONTH, 1); break; case WEEK: calendar.add(Calendar.WEEK_OF_YEAR, 1); break; case DAY: calendar.add(Calendar.DAY_OF_MONTH, 1); break; case HALF_DAY: calendar.add(Calendar.AM_PM, 1); break; case HOUR: calendar.add(Calendar.HOUR_OF_DAY, 1); break; case MINUTE: calendar.add(Calendar.MINUTE, 1); break; } nextRollover = calendar.getTimeInMillis(); } /** * Possible period values. Keep in strictly ascending order of magnitude. */ public enum Period { MINUTE, HOUR, HALF_DAY, DAY, WEEK, MONTH, YEAR, NEVER, } private static <T extends Comparable<? super T>> T min(T a, T b) { return a.compareTo(b) <= 0 ? a : b; } public static Builder builder() { return new Builder(); } public static class Builder extends FileAuditEndpoint.Builder { private long rotateSize = 0xa0000L; // 10 MB by default private int maxBackupIndex = 1; private boolean rotateOnBoot; SimpleDateFormat format; Period period = Period.NEVER; TimeZone timeZone = TimeZone.getDefault(); Builder() { super(); } /** * Set the log file size the file should rotate at. * * @param rotateSize the size the file should rotate at * @return this builder. */ public Builder setRotateSize(long rotateSize) { this.rotateSize = rotateSize; return this; } /** * Set the maximum number of files to backup. * * @param maxBackupIndex the maximum number of files to backup * @return this builder. */ public Builder setMaxBackupIndex(int maxBackupIndex) { this.maxBackupIndex = maxBackupIndex; return this; } /** * Set to a value of {@code true} if the file should be rotated before the a new file is set. The rotation only * happens if the file names are the same and the file has a {@link java.io.File#length() length} greater than 0. * * @param rotateOnBoot {@code true} to rotate on boot, otherwise {@code false} * @return this builder. */ public Builder setRotateOnBoot(boolean rotateOnBoot) { this.rotateOnBoot = rotateOnBoot; return this; } /** * Set the configured time zone for this handler. * * @param timeZone the configured time zone * @return this builder. */ public Builder setTimeZone(TimeZone timeZone) { this.timeZone = checkNotNullParam("timeZone", timeZone); return this; } /** * Set the suffix string. The string is in a format which can be understood by {@link java.text.SimpleDateFormat}. * The period of the rotation is automatically calculated based on the suffix. * * @param suffix the suffix * @throws IllegalArgumentException if the suffix is not valid */ public Builder setSuffix(String suffix) throws IllegalArgumentException { format = new SimpleDateFormat(suffix); format.setTimeZone(timeZone); final int len = suffix.length(); period = Period.NEVER; for (int i = 0; i < len; i ++) { switch (suffix.charAt(i)) { case 'y': period = min(period, Period.YEAR); break; case 'M': period = min(period, Period.MONTH); break; case 'w': case 'W': period = min(period, Period.WEEK); break; case 'D': case 'd': case 'F': case 'E': period = min(period, Period.DAY); break; case 'a': period = min(period, Period.HALF_DAY); break; case 'H': case 'k': case 'K': case 'h': period = min(period, Period.HOUR); break; case 'm': period = min(period, Period.MINUTE); break; case '\'': while (suffix.charAt(++i) != '\''){} break; case 's': case 'S': throw audit.rotatingBySecondUnsupported(suffix); } } return this; } @Override public AuditEndpoint build() throws IOException { return new RotatingFileAuditEndpoint(this); } } }