/*
* JBoss, Home of Professional Open Source.
* Copyright 2012, Red Hat, Inc., and individual contributors
* as indicated by the @author tags. See the copyright.txt file in the
* distribution for a full listing of individual contributors.
*
* This is free software; you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as
* published by the Free Software Foundation; either version 2.1 of
* the License, or (at your option) any later version.
*
* This software is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this software; if not, write to the Free
* Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
* 02110-1301 USA, or see the FSF site: http://www.fsf.org.
*/
package org.jboss.as.controller.audit;
import java.io.FileInputStream;
import java.io.IOException;
import java.net.InetAddress;
import java.net.PortUnreachableException;
import java.security.KeyStore;
import java.util.logging.ErrorManager;
import javax.net.SocketFactory;
import javax.net.ssl.KeyManager;
import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManager;
import javax.net.ssl.TrustManagerFactory;
import org.jboss.as.controller.interfaces.InetAddressUtil;
import org.jboss.as.controller.logging.ControllerLogger;
import org.jboss.as.controller.services.path.PathManagerService;
import org.jboss.logmanager.ExtLogRecord;
import org.jboss.logmanager.Level;
import org.jboss.logmanager.handlers.SyslogHandler;
import org.jboss.logmanager.handlers.SyslogHandler.Protocol;
import org.jboss.logmanager.handlers.SyslogHandler.SyslogType;
import org.jboss.logmanager.handlers.TcpOutputStream;
import org.xnio.IoUtils;
/**
*
* @author <a href="kabir.khan@jboss.com">Kabir Khan</a>
*/
public class SyslogAuditLogHandler extends AuditLogHandler {
private final PathManagerService pathManager;
private volatile SyslogHandler handler;
private volatile String appName;
private volatile String hostName;
private volatile SyslogType syslogType = SyslogType.RFC5424;
private volatile boolean truncate;
private volatile int maxLength;
private volatile InetAddress syslogServerAddress;
private volatile int port = 514;
private volatile Transport transport = Transport.UDP;
private volatile MessageTransfer messageTransfer = MessageTransfer.NON_TRANSPARENT_FRAMING;
private volatile Facility facility;
private volatile String tlsTrustStorePath;
private volatile String tlsTrustStoreRelativeTo;
private volatile String tlsTrustStorePassword;
private volatile String tlsClientCertStorePath;
private volatile String tlsClientCertStoreRelativeTo;
private volatile String tlsClientCertStorePassword;
private volatile String tlsClientCertStoreKeyPassword;
private volatile TransportErrorManager errorManager;
private volatile int reconnectTimeout = -1;
private volatile long lastErrorTime = -1;
// This should be guarded by the config lock in the ManagedAuditLoggerImpl
private boolean connected;
private static final String APPNAME_FILTER = "[\\P{Print}\\ ]";
private static final String APPNAME_REPLACE_VALUE = "_";
public SyslogAuditLogHandler(String name, String formatterName, int maxFailureCount, PathManagerService pathManager) {
super(name, formatterName, maxFailureCount);
this.pathManager = pathManager;
connected = false;
}
public void setHostName(String hostName) {
assert hostName != null;
this.hostName = hostName;
}
public void setAppName(String appName) {
assert appName != null;
// filter any non-printing characters, or ascii spaces to -
this.appName = appName.replaceAll(APPNAME_FILTER, APPNAME_REPLACE_VALUE);
//This gets updated immediately
if (handler != null) {
handler.setAppName(appName);
}
}
public void setFacility(Facility facility) {
assert facility != null;
this.facility = facility;
//This gets updated immediately
if (handler != null) {
handler.setFacility(facility.convert());
}
}
public void setSyslogType(SyslogType syslogType) {
assert syslogType != null;
this.syslogType = syslogType;
}
public void setTruncate(boolean truncate) {
this.truncate = truncate;
}
public void setMaxLength(int maxLength) {
this.maxLength = maxLength;
}
public void setMessageTransfer(MessageTransfer messageTransfer) {
assert messageTransfer != null;
this.messageTransfer = messageTransfer;
}
public void setSyslogServerAddress(InetAddress syslogServerAddress) {
assert syslogServerAddress != null;
this.syslogServerAddress = syslogServerAddress;
}
public void setPort(int port) {
this.port = port;
}
public void setTransport(Transport transport) {
assert transport != null;
this.transport = transport;
}
public void setTlsTrustStorePath(String tlsTrustStorePath) {
this.tlsTrustStorePath = tlsTrustStorePath;
}
public void setTlsTrustStoreRelativeTo(String tlsTrustStoreRelativeTo) {
this.tlsTrustStoreRelativeTo = tlsTrustStoreRelativeTo;
}
public void setTlsTruststorePassword(String tlsTrustStorePassword) {
this.tlsTrustStorePassword = tlsTrustStorePassword;
}
public void setTlsClientCertStorePath(String tlsClientCertStorePath) {
this.tlsClientCertStorePath = tlsClientCertStorePath;
}
public void setTlsClientCertStoreRelativeTo(String tlsClientCertStoreRelativeTo) {
this.tlsClientCertStoreRelativeTo = tlsClientCertStoreRelativeTo;
}
public void setTlsClientCertStorePassword(String tlsClientCertStorePassword) {
this.tlsClientCertStorePassword = tlsClientCertStorePassword;
}
public void setTlsClientCertStoreKeyPassword(String tlsClientCertStoreKeyPassword) {
this.tlsClientCertStoreKeyPassword = tlsClientCertStoreKeyPassword;
}
public void setReconnectTimeout(int reconnectTimeout) {
this.reconnectTimeout = reconnectTimeout;
}
@Override
boolean isActive() {
if (hasTooManyFailures()) {
if (reconnectTimeout >= 0) {
long end = lastErrorTime + reconnectTimeout * 1000;
return System.currentTimeMillis() > end;
}
return false;
}
return true;
}
@Override
void initialize() {
// Only attempt initialization if this if the first invocation, the server has been stopped or a previous
// initialization invocation has failed
if (connected) {
return;
}
SyslogHandler handler = this.handler;
// If the handler is not null, we should clean it up before initializing a new handler
if (handler != null) {
ControllerLogger.MGMT_OP_LOGGER.debug("Stopping a previously initialized syslog handler.");
stop();
}
try {
final Protocol protocol;
switch (transport) {
case UDP:
protocol = Protocol.UDP;
break;
case TCP:
protocol = Protocol.TCP;
break;
case TLS:
protocol = Protocol.SSL_TCP;
break;
default:
//i18n not needed, user code will not end up here
throw new IllegalStateException("Unknown protocol");
}
handler = new SyslogHandler(syslogServerAddress, port, facility.convert(), syslogType, protocol, hostName == null ? InetAddressUtil.getLocalHostName() : hostName);
handler.setAppName(appName);
handler.setTruncate(truncate);
if (maxLength != 0) {
handler.setMaxLength(maxLength);
}
//Common for all protocols
handler.setSyslogType(syslogType);
final TransportErrorManager errorManager = new TransportErrorManager();
handler.setErrorManager(errorManager);
if (transport != Transport.UDP){
if (messageTransfer == MessageTransfer.NON_TRANSPARENT_FRAMING) {
handler.setUseCountingFraming(false);
handler.setMessageDelimiter("\n");
handler.setUseMessageDelimiter(true);
} else {
handler.setUseCountingFraming(true);
handler.setMessageDelimiter(null);
handler.setUseMessageDelimiter(false);
}
if (transport == Transport.TLS){
final SSLContext context = SSLContext.getInstance("TLS");
KeyManager[] keyManagers = null;
if (tlsClientCertStorePath != null){
final KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
final FileInputStream in = new FileInputStream(pathManager.resolveRelativePathEntry(tlsClientCertStorePath, tlsClientCertStoreRelativeTo));
try {
final KeyStore ks = KeyStore.getInstance("JKS");
ks.load(in, tlsClientCertStorePassword.toCharArray());
kmf.init(ks, tlsClientCertStoreKeyPassword != null ? tlsClientCertStoreKeyPassword.toCharArray() : tlsClientCertStorePassword.toCharArray());
keyManagers = kmf.getKeyManagers();
} finally {
IoUtils.safeClose(in);
}
}
TrustManager[] trustManagers = null;
if (tlsTrustStorePath != null){
final TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
final FileInputStream in = new FileInputStream(pathManager.resolveRelativePathEntry(tlsTrustStorePath, tlsTrustStoreRelativeTo));
try {
final KeyStore ks = KeyStore.getInstance("JKS");
ks.load(in, tlsTrustStorePassword.toCharArray());
tmf.init(ks);
trustManagers = tmf.getTrustManagers();
} finally {
IoUtils.safeClose(in);
}
}
context.init(keyManagers, trustManagers, null);
handler.setOutputStream(new SSLContextOutputStream(context, syslogServerAddress, port));
} else {
handler.setOutputStream(new AuditLogTcpOutputStream(syslogServerAddress, port));
handler.setProtocol(transport == Transport.TCP ? Protocol.TCP : Protocol.SSL_TCP);
}
}
this.handler = handler;
this.errorManager = errorManager;
connected = true;
} catch (Exception e) {
// Failed to initialize the handler, clean up the resources
connected = false;
if (handler != null) {
try {
handler.close();
} catch (Exception ignore){}
}
throw new RuntimeException(e);
}
}
@Override
void stop() {
connected = false;
SyslogHandler handler = this.handler;
this.handler = null;
if (handler != null) {
handler.close();
}
}
private boolean isReconnect() {
return hasTooManyFailures() && isActive();
}
FailureCountHandler getFailureCountHandler() {
return isReconnect() ? new ReconnectFailureCountHandler() : super.getFailureCountHandler();
}
@Override
void writeLogItem(String formattedItem) throws IOException {
boolean reconnect = isReconnect();
if (!reconnect) {
handler.publish(new ExtLogRecord(Level.WARN, formattedItem, SyslogAuditLogHandler.class.getName()));
errorManager.getAndThrowError();
} else {
ControllerLogger.MGMT_OP_LOGGER.attemptingReconnectToSyslog(name, reconnectTimeout);
try {
// Reinitialise the delegating syslog handler if required, if we're already connected we don't need to
// establish a new connection
if (!connected) {
stop();
initialize();
}
handler.publish(new ExtLogRecord(Level.WARN, formattedItem, SyslogAuditLogHandler.class.getName()));
errorManager.getAndThrowError();
lastErrorTime = -1;
} catch (Exception e) {
// A failure has occurred and initialization should be reattempted
connected = false;
lastErrorTime = System.currentTimeMillis();
errorManager.throwAsIoOrRuntimeException(e);
}
}
}
@Override
boolean isDifferent(AuditLogHandler other){
if (other instanceof SyslogAuditLogHandler == false){
return true;
}
SyslogAuditLogHandler otherHandler = (SyslogAuditLogHandler)other;
if (!name.equals(otherHandler.name)){
return true;
}
if (!getFormatterName().equals(otherHandler.getFormatterName())) {
return true;
}
if (!hostName.equals(otherHandler.hostName)){
return true;
}
if (!syslogType.equals(otherHandler.syslogType)){
return true;
}
if (!truncate == otherHandler.truncate) {
return true;
}
if (maxLength != otherHandler.maxLength) {
return true;
}
if (!syslogServerAddress.equals(otherHandler.syslogServerAddress)){
return true;
}
if (port != otherHandler.port){
return true;
}
if (!transport.equals(otherHandler.transport)){
return true;
}
//These may or not be null depending on the transport
if (!compare(messageTransfer, otherHandler.messageTransfer)){
return true;
}
if (!compare(tlsTrustStorePath, otherHandler.tlsTrustStorePath)){
return true;
}
if (!compare(tlsTrustStoreRelativeTo, otherHandler.tlsTrustStoreRelativeTo)){
return true;
}
if (!compare(tlsTrustStorePassword, otherHandler.tlsTrustStorePassword)){
return true;
}
if (!compare(tlsClientCertStorePath, otherHandler.tlsClientCertStorePath)){
return true;
}
if (!compare(tlsClientCertStoreRelativeTo, otherHandler.tlsClientCertStoreRelativeTo)){
return true;
}
if (!compare(tlsClientCertStorePassword, otherHandler.tlsClientCertStorePassword)){
return true;
}
if (!compare(tlsClientCertStoreKeyPassword, otherHandler.tlsClientCertStoreKeyPassword)){
return true;
}
return false;
}
private boolean compare(Object one, Object two){
if (one == null && two == null){
return true;
}
if (one == null && two != null){
return false;
}
if (one != null && two == null){
return false;
}
return one.equals(two);
}
public enum Transport {
UDP,
TCP,
TLS
}
public enum MessageTransfer {
OCTET_COUNTING,
NON_TRANSPARENT_FRAMING;
}
/**
* Facility as defined by RFC-5424 (<a href="http://tools.ietf.org/html/rfc5424">http://tools.ietf.org/html/rfc5424</a>)
* and RFC-3164 (<a href="http://tools.ietf.org/html/rfc3164">http://tools.ietf.org/html/rfc3164</a>).
*/
public static enum Facility {
KERNEL(SyslogHandler.Facility.KERNEL),
USER_LEVEL(SyslogHandler.Facility.USER_LEVEL),
MAIL_SYSTEM(SyslogHandler.Facility.MAIL_SYSTEM),
SYSTEM_DAEMONS(SyslogHandler.Facility.SYSTEM_DAEMONS),
SECURITY(SyslogHandler.Facility.SECURITY),
SYSLOGD(SyslogHandler.Facility.SYSLOGD),
LINE_PRINTER(SyslogHandler.Facility.LINE_PRINTER),
NETWORK_NEWS(SyslogHandler.Facility.NETWORK_NEWS),
UUCP(SyslogHandler.Facility.UUCP),
CLOCK_DAEMON(SyslogHandler.Facility.CLOCK_DAEMON),
SECURITY2(SyslogHandler.Facility.SECURITY2),
FTP_DAEMON(SyslogHandler.Facility.FTP_DAEMON),
NTP(SyslogHandler.Facility.NTP),
LOG_AUDIT(SyslogHandler.Facility.LOG_AUDIT),
LOG_ALERT(SyslogHandler.Facility.LOG_ALERT),
CLOCK_DAEMON2(SyslogHandler.Facility.CLOCK_DAEMON2),
LOCAL_USE_0(SyslogHandler.Facility.LOCAL_USE_0),
LOCAL_USE_1(SyslogHandler.Facility.LOCAL_USE_1),
LOCAL_USE_2(SyslogHandler.Facility.LOCAL_USE_2),
LOCAL_USE_3(SyslogHandler.Facility.LOCAL_USE_3),
LOCAL_USE_4(SyslogHandler.Facility.LOCAL_USE_4),
LOCAL_USE_5(SyslogHandler.Facility.LOCAL_USE_5),
LOCAL_USE_6(SyslogHandler.Facility.LOCAL_USE_6),
LOCAL_USE_7(SyslogHandler.Facility.LOCAL_USE_7);
private final SyslogHandler.Facility realFacility;
private Facility(SyslogHandler.Facility realFacility) {
this.realFacility = realFacility;
}
public SyslogHandler.Facility convert(){
return realFacility;
}
}
// By default the TcpOutputStream attempts to reconnect on it's own, use our own to avoid the automatic reconnect
// See LOGMGR-113 for details on a better way to do this in the future
@SuppressWarnings("deprecation")
private static class AuditLogTcpOutputStream extends TcpOutputStream {
protected AuditLogTcpOutputStream(InetAddress host, int port) throws IOException {
super(SocketFactory.getDefault().createSocket(host, port));
}
}
@SuppressWarnings("deprecation")
private static class SSLContextOutputStream extends TcpOutputStream {
protected SSLContextOutputStream(SSLContext sslContext, InetAddress host, int port) throws IOException {
// Continue to use the deprecated constructor until LOGMGR-113 is resolved
super(sslContext.getSocketFactory().createSocket(host, port));
}
}
private class TransportErrorManager extends ErrorManager {
private volatile Exception error;
public TransportErrorManager() {
}
@Override
public synchronized void error(String msg, Exception ex, int code) {
error = ex;
lastErrorTime = System.currentTimeMillis();
}
void getAndThrowError() throws IOException {
Exception error = this.error;
this.error = null;
if (error != null) {
// This method is only invoked by writeLog() which is guarded by the config lock in ManagedAuditLoggerImpl
// If we've received an error we're going to assume we're not connected to force a reconnect
connected = false;
throwAsIoOrRuntimeException(error);
}
}
void throwAsIoOrRuntimeException(Throwable t) throws IOException {
if (t instanceof PortUnreachableException && transport == Transport.UDP) {
//This is an exception that may or may not happen, see the javadoc for DatagramSocket.send().
//We don't want something this unreliable polluting the failure count.
//With UDP syslogging set up against a non-existent syslog server:
//On OS X this exception never happens.
//On Linux this seems to happens every other send, so we end up with a loop
// odd send works; failure count = 0
// even send fails; failure count = 1
// odd send works; failure count reset to 0
//
//Also, we don't want the full stack trace for this which would get printed by StandardFailureCountHandler.StandardFailureCountHandler.failure(),
//which also handles the failure count, so we swallow the exception and print a warning.
ControllerLogger.MGMT_OP_LOGGER.udpSyslogServerUnavailable(getName(), t.getLocalizedMessage());
return;
}
if (t instanceof IOException) {
throw (IOException)t;
}
if (t instanceof RuntimeException) {
throw (RuntimeException)t;
}
throw new RuntimeException(t);
}
}
}