/* * 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 static org.wildfly.common.Assert.checkNotNullParam; import static org.wildfly.security._private.ElytronMessages.audit; import java.io.BufferedOutputStream; import java.io.Closeable; import java.io.File; import java.io.FileDescriptor; import java.io.FileOutputStream; import java.io.IOException; import java.io.OutputStream; import java.nio.charset.StandardCharsets; import java.nio.file.Path; import java.text.DateFormat; import java.text.SimpleDateFormat; import java.util.Date; import java.util.function.Supplier; /** * An audit endpoint to record all audit events to a local file. * * @author <a href="mailto:darran.lofthouse@jboss.com">Darran Lofthouse</a> */ public class FileAuditEndpoint implements AuditEndpoint { private static final byte[] LINE_TERMINATOR = System.lineSeparator().getBytes(StandardCharsets.UTF_8); private volatile boolean accepting = true; private final Supplier<DateFormat> dateFormatSupplier; private final boolean syncOnAccept; private File file; private FileDescriptor fileDescriptor; private OutputStream outputStream; FileAuditEndpoint(Builder builder) throws IOException { this.dateFormatSupplier = builder.dateFormatSupplier; this.syncOnAccept = builder.syncOnAccept; setFile(builder.location.toFile()); } protected void setFile(final File file) throws IOException { boolean ok = false; final FileOutputStream fos = new FileOutputStream(file, true); try { final OutputStream bos = new BufferedOutputStream(fos); try { this.fileDescriptor = fos.getFD(); this.outputStream = bos; this.file = file; ok = true; } finally { if (! ok) { safeClose(bos); } } } finally { if (! ok) { safeClose(fos); } } } protected File getFile() { return file; } private void safeClose(Closeable c) { try { if (c != null) c.close(); } catch (Exception e) { audit.trace(e); } } /** * Writes <code>bytes.length</code> bytes from the specified byte array * to the underlying output stream managed by this class. * The general contract for <code>write(bytes)</code> is that it must be * invoked from a synchronization block surrounding one log message processing. * * @param bytes the data. * @throws IOException if an I/O error occurs. */ protected void write(byte[] bytes) throws IOException { outputStream.write(bytes); } /** * The general contract for <code>preWrite(date)</code> is that any method override * must ensure thread safety invoking this method from a synchronization block * surrounding one log message processing. * * @param date */ protected void preWrite(Date date) { // NO-OP by default } @Override public void accept(EventPriority t, String u) throws IOException { if (!accepting) return; Date date = new Date(); synchronized(this) { if (!accepting) return; // We may have been waiting to get in here. preWrite(date); boolean started = false; try { write(dateFormatSupplier.get().format(date).getBytes(StandardCharsets.UTF_8)); started = true; write(new byte[]{','}); write(t.toString().getBytes(StandardCharsets.UTF_8)); write(new byte[]{','}); write(u.getBytes(StandardCharsets.UTF_8)); write(LINE_TERMINATOR); } catch (IOException e) { throw started ? audit.partialSecurityEventWritten(e) : e; } if (syncOnAccept) { outputStream.flush(); fileDescriptor.sync(); } } } @Override public void close() throws IOException { accepting = false; synchronized (this) { closeStreams(); } } /** * Close opened file streams. * Must be called synchronized block together with reopening using {@code setFile()}. */ protected void closeStreams() throws IOException { outputStream.flush(); fileDescriptor.sync(); outputStream.close(); } public static Builder builder() { return new Builder(); } public static class Builder { private Supplier<DateFormat> dateFormatSupplier = SimpleDateFormat::new; private Path location = new File("audit.log").toPath(); private boolean syncOnAccept = true; Builder() { } /** * Set the {@link Supplier<DateFormat>} to obtain the formatter for dates. * * @param dateFormatSupplier the {@link Supplier<DateFormat>} to obtain the formatter for dates. * @return this builder. */ public Builder setDateFormatSupplier(Supplier<DateFormat> dateFormatSupplier) { this.dateFormatSupplier = checkNotNullParam("dateFormatSupplier", dateFormatSupplier); return this; } /** * Set the location to write the audit events to. * * @param location the location to write the audit events to. * @return this builder. */ public Builder setLocation(Path location) { this.location = checkNotNullParam("location", location); return this; } /** * Sets if the output should be flushed and system buffers forces to synchronize on each event accepted. * * @param syncOnAccept should the output be flushed and system buffers forces to synchronize on each event accepted. * @return this builder. */ public Builder setSyncOnAccept(boolean syncOnAccept) { this.syncOnAccept = syncOnAccept; return this; } public AuditEndpoint build() throws IOException { return new FileAuditEndpoint(this); } } }