diff options
Diffstat (limited to 'base/common/src/com/netscape/cms/logging')
-rw-r--r-- | base/common/src/com/netscape/cms/logging/LogEntry.java | 134 | ||||
-rw-r--r-- | base/common/src/com/netscape/cms/logging/LogFile.java | 1534 | ||||
-rw-r--r-- | base/common/src/com/netscape/cms/logging/RollingLogFile.java | 658 |
3 files changed, 2326 insertions, 0 deletions
diff --git a/base/common/src/com/netscape/cms/logging/LogEntry.java b/base/common/src/com/netscape/cms/logging/LogEntry.java new file mode 100644 index 000000000..d91bd7406 --- /dev/null +++ b/base/common/src/com/netscape/cms/logging/LogEntry.java @@ -0,0 +1,134 @@ +// --- BEGIN COPYRIGHT BLOCK --- +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; version 2 of the License. +// +// This program 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 General Public License for more details. +// +// You should have received a copy of the GNU General Public License along +// with this program; if not, write to the Free Software Foundation, Inc., +// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +// +// (C) 2007 Red Hat, Inc. +// All rights reserved. +// --- END COPYRIGHT BLOCK --- +package com.netscape.cms.logging; + +import java.text.DateFormat; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.Vector; + +/** + * A log entry of LogFile + * + * @version $Revision$, $Date$ + */ +public class LogEntry { + private String mEntry; + private String mLevel; + private String mSource; + private String mDetail; + private String mDate; + private String mTime; + private Vector<String> mRow; + + private final String DATE_PATTERN = "dd/MMM/yyyy:HH:mm:ss z"; + + /** + * Constructor for a LogEntry. + * + */ + public LogEntry(String entry) throws ParseException { + mEntry = entry; + mRow = parse(); + } + + /** + * parse a log entry + * + * return a vector of the segments of the entry + */ + + public Vector<String> parse() throws ParseException { + int x = mEntry.indexOf("["); + + if (x == -1) + throw new ParseException(mEntry, 0); + String temp = mEntry.substring(x + 1); + + x = temp.indexOf("]"); + if (x == -1) + throw new ParseException(mEntry, 0); + + String dateStr = temp.substring(0, x); + SimpleDateFormat format = new SimpleDateFormat(DATE_PATTERN); + Date date = format.parse(dateStr); + + mDate = DateFormat.getDateInstance().format(date); + mTime = DateFormat.getTimeInstance().format(date); + + temp = temp.substring(x + 2); + x = temp.indexOf("]"); + if (x == -1) + throw new ParseException(mEntry, 0); + mSource = temp.substring(1, x); + + temp = temp.substring(x + 2); + x = temp.indexOf("]"); + if (x == -1) + throw new ParseException(mEntry, 0); + mLevel = temp.substring(1, x); + + mDetail = temp.substring(x + 2); + + Vector<String> row = new Vector<String>(); + + row.addElement(mSource); + row.addElement(mLevel); + row.addElement(mDate); + row.addElement(mTime); + row.addElement(mDetail); + + //System.out.println(mSource +"," + mLevel +","+ mDate+","+mTime+","+mDetail); + return row; + + } + + public String getSource() { + return mSource; + } + + public String getLevel() { + return mLevel; + } + + public String getDetail() { + return mDetail; + } + + public String getDate() { + return mDate; + } + + public String getTime() { + return mTime; + } + + public Vector<String> getRow() { + return mRow; + } + + public String getEntry() { + return mEntry; + } + + public void appendDetail(String msg) { + mDetail = mDetail + "\n" + msg; + mEntry = mEntry + "\n" + msg; + } +} diff --git a/base/common/src/com/netscape/cms/logging/LogFile.java b/base/common/src/com/netscape/cms/logging/LogFile.java new file mode 100644 index 000000000..5144bf16d --- /dev/null +++ b/base/common/src/com/netscape/cms/logging/LogFile.java @@ -0,0 +1,1534 @@ +// --- BEGIN COPYRIGHT BLOCK --- +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; version 2 of the License. +// +// This program 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 General Public License for more details. +// +// You should have received a copy of the GNU General Public License along +// with this program; if not, write to the Free Software Foundation, Inc., +// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +// +// (C) 2007 Red Hat, Inc. +// All rights reserved. +// --- END COPYRIGHT BLOCK --- +package com.netscape.cms.logging; + +import java.io.BufferedReader; +import java.io.BufferedWriter; +import java.io.ByteArrayOutputStream; +import java.io.CharArrayReader; +import java.io.CharArrayWriter; +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileReader; +import java.io.FileWriter; +import java.io.FilterOutputStream; +import java.io.IOException; +import java.io.LineNumberReader; +import java.io.PrintStream; +import java.io.PrintWriter; +import java.io.RandomAccessFile; +import java.io.UnsupportedEncodingException; +import java.security.GeneralSecurityException; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.security.NoSuchProviderException; +import java.security.PrivateKey; +import java.security.Provider; +import java.security.Signature; +import java.security.SignatureException; +import java.security.interfaces.DSAPrivateKey; +import java.security.interfaces.RSAPrivateKey; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.Hashtable; +import java.util.Locale; +import java.util.Properties; +import java.util.StringTokenizer; +import java.util.Vector; + +import javax.servlet.ServletException; + +import org.mozilla.jss.CryptoManager; +import org.mozilla.jss.crypto.CryptoToken; +import org.mozilla.jss.crypto.ObjectNotFoundException; +import org.mozilla.jss.crypto.TokenException; +import org.mozilla.jss.crypto.X509Certificate; +import org.mozilla.jss.util.Base64OutputStream; + +import com.netscape.certsrv.apps.CMS; +import com.netscape.certsrv.base.EBaseException; +import com.netscape.certsrv.base.IConfigStore; +import com.netscape.certsrv.base.IExtendedPluginInfo; +import com.netscape.certsrv.base.ISubsystem; +import com.netscape.certsrv.common.Constants; +import com.netscape.certsrv.common.NameValuePairs; +import com.netscape.certsrv.logging.AuditEvent; +import com.netscape.certsrv.logging.ConsoleError; +import com.netscape.certsrv.logging.ELogException; +import com.netscape.certsrv.logging.ILogEvent; +import com.netscape.certsrv.logging.ILogEventListener; +import com.netscape.certsrv.logging.ILogger; +import com.netscape.certsrv.logging.SignedAuditEvent; +import com.netscape.certsrv.logging.SystemEvent; +import com.netscape.cmsutil.util.Utils; + +/** + * A log event listener which write logs to log files + * + * @version $Revision$, $Date$ + **/ +public class LogFile implements ILogEventListener, IExtendedPluginInfo { + public static final String PROP_TYPE = "type"; + public static final String PROP_REGISTER = "register"; + public static final String PROP_ON = "enable"; + public static final String PROP_TRACE = "trace"; + public static final String PROP_SIGNED_AUDIT_LOG_SIGNING = "logSigning"; + public static final String PROP_SIGNED_AUDIT_CERT_NICKNAME = + "signedAuditCertNickname"; + public static final String PROP_SIGNED_AUDIT_EVENTS = "events"; + public static final String PROP_LEVEL = "level"; + static final String PROP_FILE_NAME = "fileName"; + static final String PROP_LAST_HASH_FILE_NAME = "lastHashFileName"; + static final String PROP_BUFFER_SIZE = "bufferSize"; + static final String PROP_FLUSH_INTERVAL = "flushInterval"; + + private final static String LOGGING_SIGNED_AUDIT_AUDIT_LOG_STARTUP = + "LOGGING_SIGNED_AUDIT_AUDIT_LOG_STARTUP_2"; + private final static String LOGGING_SIGNED_AUDIT_SIGNING = + "LOGGING_SIGNED_AUDIT_SIGNING_3"; + private final static String LOGGING_SIGNED_AUDIT_AUDIT_LOG_SHUTDOWN = + "LOGGING_SIGNED_AUDIT_AUDIT_LOG_SHUTDOWN_2"; + private final static String LOG_SIGNED_AUDIT_EXCEPTION = + "LOG_SIGNED_AUDIT_EXCEPTION_1"; + + protected ILogger mSignedAuditLogger = CMS.getSignedAuditLogger(); + protected IConfigStore mConfig = null; + + /** + * The date string used in the log file name + */ + static final String DATE_PATTERN = "yyyyMMddHHmmss"; + + //It may be interesting to make this flexable someday.... + protected SimpleDateFormat mLogFileDateFormat = new SimpleDateFormat(DATE_PATTERN); + + /** + * The default output stream buffer size in bytes + */ + static final int BUFFER_SIZE = 512; + + /** + * The default output flush interval in seconds + */ + static final int FLUSH_INTERVAL = 5; + + /** + * The log file + */ + protected File mFile = null; + + /** + * The log file name + */ + protected String mFileName = null; + + /** + * The log file output stream + */ + protected BufferedWriter mLogWriter = null; + + /** + * The log date entry format pattern + */ + protected String mDatePattern = "dd/MMM/yyyy:HH:mm:ss z"; + + /** + * The log date entry format + */ + protected SimpleDateFormat mLogDateFormat = new SimpleDateFormat(mDatePattern); + + /** + * The date object used for log entries + */ + protected Date mDate = new Date(); + + /** + * The number of bytes written to the current log file + */ + protected int mBytesWritten = 0; + + /** + * The output buffer size in bytes + */ + protected int mBufferSize = BUFFER_SIZE; + + /** + * The output buffer flush interval + */ + protected int mFlushInterval = FLUSH_INTERVAL; + + /** + * The number of unflushed bytes + */ + protected int mBytesUnflushed = 0; + + /** + * The output buffer flush interval thread + */ + private Thread mFlushThread = null; + + /** + * The selected log event types + */ + protected String mSelectedEventsList = null; + protected Vector<String> mSelectedEvents = null; + + /** + * The eventType that this log is triggered + */ + protected String mType = null; + + /** + * The log is turned on/off + */ + protected boolean mOn = false; + + /** + * Should this log listener self-register or not + */ + protected boolean mRegister = false; + + protected boolean mTrace = false; + + /** + * Log signing is on/off + */ + protected boolean mLogSigning = false; + + /** + * Nickname of certificate to use to sign log. + */ + private String mSAuditCertNickName = ""; + + /** + * The provider used by the KeyGenerator and Mac + */ + static final String CRYPTO_PROVIDER = "Mozilla-JSS"; + + /** + * The log level threshold + * Only logs with level greater or equal than this value will be written + */ + protected long mLevel = 1; + + /** + * Constructor for a LogFile. + * + */ + public LogFile() { + } + + public void init(ISubsystem owner, IConfigStore config) + throws EBaseException { + mConfig = config; + + try { + mOn = config.getBoolean(PROP_ON, true); + } catch (EBaseException e) { + throw new ELogException(CMS.getUserMessage("CMS_BASE_GET_PROPERTY_FAILED", + config.getName() + "." + PROP_ON)); + } + + try { + mLogSigning = config.getBoolean(PROP_SIGNED_AUDIT_LOG_SIGNING, + false); + } catch (EBaseException e) { + throw new ELogException(CMS.getUserMessage("CMS_BASE_GET_PROPERTY_FAILED", + config.getName() + "." + PROP_SIGNED_AUDIT_LOG_SIGNING)); + } + + if (mOn && mLogSigning) { + try { + mSAuditCertNickName = config.getString( + PROP_SIGNED_AUDIT_CERT_NICKNAME); + CMS.debug("LogFile: init(): audit log signing enabled. signedAuditCertNickname=" + mSAuditCertNickName); + } catch (EBaseException e) { + throw new ELogException(CMS.getUserMessage("CMS_BASE_GET_PROPERTY_FAILED", + config.getName() + "." + + PROP_SIGNED_AUDIT_CERT_NICKNAME)); + } + if (mSAuditCertNickName == null || + mSAuditCertNickName.trim().equals("")) { + throw new ELogException(CMS.getUserMessage( + "CMS_BASE_GET_PROPERTY_FAILED", + config.getName() + "." + + PROP_SIGNED_AUDIT_CERT_NICKNAME)); + } + } + + // selective logging + mSelectedEventsList = null; + try { + mSelectedEventsList = config.getString(PROP_SIGNED_AUDIT_EVENTS); + } catch (EBaseException e) { + // when not specified, ALL are selected by default + } + mSelectedEvents = string2Vector(mSelectedEventsList); + + try { + init(config); + } catch (IOException e) { + throw new ELogException(CMS.getUserMessage("CMS_LOG_UNEXPECTED_EXCEPTION", e.toString())); + } + } + + /** + * turns a comma-separated String into a Vector + */ + protected Vector<String> string2Vector(String theString) { + Vector<String> theVector = new Vector<String>(); + if (theString == null) { + return theVector; + } + + StringTokenizer tokens = new StringTokenizer(theString, + ","); + while (tokens.hasMoreTokens()) { + String eventId = tokens.nextToken().trim(); + + theVector.addElement(eventId); + CMS.debug("LogFile: log event type selected: " + eventId); + } + return theVector; + } + + /** + * add the event to the selected events list + * + * @param event to be selected + */ + public void selectEvent(String event) { + if (!mSelectedEvents.contains(event)) + mSelectedEvents.addElement(event); + } + + /** + * remove the event from the selected events list + * + * @param event to be de-selected + */ + public void deselectEvent(String event) { + if (mSelectedEvents.contains(event)) + mSelectedEvents.removeElement(event); + } + + /** + * replace the selected events list + * + * @param events comma-separated event list + */ + public void replaceEvents(String events) { + Vector<String> v = string2Vector(events); + mSelectedEvents.removeAllElements(); + mSelectedEvents = v; + } + + public static String base64Encode(byte[] bytes) throws IOException { + // All this streaming is lame, but Base64OutputStream needs a + // PrintStream + ByteArrayOutputStream output = new ByteArrayOutputStream(); + Base64OutputStream b64 = new Base64OutputStream(new + PrintStream(new + FilterOutputStream(output) + ) + ); + + b64.write(bytes); + b64.flush(); + + // This is internationally safe because Base64 chars are + // contained within 8859_1 + return output.toString("8859_1"); + } + + private static boolean mInSignedAuditLogFailureMode = false; + + private static synchronized void shutdownCMS() { + if (mInSignedAuditLogFailureMode == false) { + + // Set signed audit log failure mode true + // No, this isn't a race condition, because the method is + // synchronized. We just want to avoid an infinite loop. + mInSignedAuditLogFailureMode = true; + + // Block all new incoming requests + if (CMS.areRequestsDisabled() == false) { + // XXX is this a race condition? + CMS.disableRequests(); + } + + // Terminate all requests in process + CMS.terminateRequests(); + + // Call graceful shutdown of the CMS server + // Call force shutdown to get added functionality of + // making sure to kill the web server. + + CMS.forceShutdown(); + } + } + + /** + * Initialize and open the log using the parameters from a config store + * + * @param config The property config store to find values in + */ + public void init(IConfigStore config) throws IOException, + EBaseException { + String fileName = null; + String defaultFileName = null; + String signedAuditDefaultFileName = ""; + + mConfig = config; + + try { + mTrace = config.getBoolean(PROP_TRACE, false); + } catch (EBaseException e) { + throw new ELogException(CMS.getUserMessage("CMS_BASE_GET_PROPERTY_FAILED", + config.getName() + "." + PROP_TRACE)); + } + + try { + mType = config.getString(PROP_TYPE, "system"); + } catch (EBaseException e) { + throw new ELogException(CMS.getUserMessage("CMS_BASE_GET_PROPERTY_FAILED", + config.getName() + "." + PROP_TYPE)); + } + + try { + mRegister = config.getBoolean(PROP_REGISTER, true); + } catch (EBaseException e) { + throw new ELogException(CMS.getUserMessage("CMS_BASE_GET_PROPERTY_FAILED", + config.getName() + "." + PROP_REGISTER)); + } + + if (mOn) { + if (mRegister) { + CMS.getLogger().getLogQueue().addLogEventListener(this); + } + } else { + // shutdown the listener, remove the listener + if (mRegister) { + CMS.getLogger().getLogQueue().removeLogEventListener(this); + shutdown(); + } + } + + try { + mLevel = config.getInteger(PROP_LEVEL, 3); + } catch (EBaseException e) { + e.printStackTrace(); + throw new ELogException(CMS.getUserMessage("CMS_BASE_GET_PROPERTY_FAILED", + config.getName() + "." + PROP_LEVEL)); + } + + try { + // retrieve the subsystem + String subsystem = ""; + + ISubsystem caSubsystem = CMS.getSubsystem("ca"); + if (caSubsystem != null) { + subsystem = "ca"; + } + + ISubsystem raSubsystem = CMS.getSubsystem("ra"); + if (raSubsystem != null) { + subsystem = "ra"; + } + + ISubsystem kraSubsystem = CMS.getSubsystem("kra"); + if (kraSubsystem != null) { + subsystem = "kra"; + } + + ISubsystem ocspSubsystem = CMS.getSubsystem("ocsp"); + if (ocspSubsystem != null) { + subsystem = "ocsp"; + } + + // retrieve the instance name + String instIDPath = CMS.getInstanceDir(); + int index = instIDPath.lastIndexOf("/"); + String instID = instIDPath.substring(index + 1); + + // build the default signedAudit file name + signedAuditDefaultFileName = subsystem + "_" + + instID + "_" + "audit"; + + } catch (Exception e2) { + throw new ELogException( + CMS.getUserMessage("CMS_BASE_GET_PROPERTY_FAILED", + config.getName() + "." + + PROP_FILE_NAME)); + } + + // the default value is determined by the eventType. + if (mType.equals(ILogger.PROP_SIGNED_AUDIT)) { + defaultFileName = "logs/signedAudit/" + signedAuditDefaultFileName; + } else if (mType.equals(ILogger.PROP_SYSTEM)) { + defaultFileName = "logs/system"; + } else if (mType.equals(ILogger.PROP_AUDIT)) { + defaultFileName = "logs/transactions"; + } else { + //wont get here + throw new ELogException(CMS.getUserMessage("CMS_LOG_INVALID_LOG_TYPE", + config.getName())); + } + + try { + fileName = config.getString(PROP_FILE_NAME, defaultFileName); + } catch (EBaseException e) { + throw new ELogException(CMS.getUserMessage("CMS_BASE_GET_PROPERTY_FAILED", + config.getName() + "." + PROP_FILE_NAME)); + } + + if (mOn) { + init(fileName, config.getInteger(PROP_BUFFER_SIZE, BUFFER_SIZE), + config.getInteger(PROP_FLUSH_INTERVAL, FLUSH_INTERVAL)); + } + } + + /** + * Initialize and open the log + * + * @param bufferSize The buffer size for the output stream in bytes + * @param flushInterval The interval in seconds to flush the log + */ + public void init(String fileName, int bufferSize, int flushInterval) throws IOException, ELogException { + + if (fileName == null) + throw new ELogException(CMS.getUserMessage("CMS_LOG_INVALID_FILE_NAME", "null")); + + //If we want to reuse the old log files + //mFileName = fileName + "." + mLogFileDateFormat.format(mDate); + mFileName = fileName; + if (!Utils.isNT()) { + // Always insure that a physical file exists! + Utils.exec("touch " + mFileName); + Utils.exec("chmod 00640 " + mFileName); + } + mFile = new File(mFileName); + mBufferSize = bufferSize; + setFlushInterval(flushInterval); + open(); + } + + private PrivateKey mSigningKey = null; + private Signature mSignature = null; + + private void setupSigning() throws EBaseException { + try { + + Provider[] providers = java.security.Security.getProviders(); + int ps = providers.length; + for (int i = 0; i < ps; i++) { + CMS.debug("LogFile: provider " + i + "= " + providers[i].getName()); + } + + CryptoManager cm = CryptoManager.getInstance(); + + // find CertServer's private key + X509Certificate cert = cm.findCertByNickname(mSAuditCertNickName); + if (cert != null) { + CMS.debug("LogFile: setupSignig(): found cert:" + mSAuditCertNickName); + } else { + CMS.debug("LogFile: setupSignig(): cert not found:" + mSAuditCertNickName); + } + mSigningKey = cm.findPrivKeyByCert(cert); + + String sigAlgorithm; + if (mSigningKey instanceof RSAPrivateKey) { + sigAlgorithm = "SHA-256/RSA"; + } else if (mSigningKey instanceof DSAPrivateKey) { + sigAlgorithm = "SHA-256/DSA"; + } else { + throw new NoSuchAlgorithmException("Unknown private key type"); + } + + CryptoToken savedToken = cm.getThreadToken(); + try { + CryptoToken keyToken = + ((org.mozilla.jss.pkcs11.PK11PrivKey) mSigningKey) + .getOwningToken(); + cm.setThreadToken(keyToken); + mSignature = java.security.Signature.getInstance(sigAlgorithm, + CRYPTO_PROVIDER); + } finally { + cm.setThreadToken(savedToken); + } + + mSignature.initSign(mSigningKey); + + // get the last signature from the currently-opened file + String entry = getLastSignature(mFile); + if (entry != null) { + mSignature.update(entry.getBytes("UTF-8")); + mSignature.update(LINE_SEP_BYTE); + } + + // Always start off with a signature. That way, even if there + // were problems with the log file we inherited, we will + // get a fresh start with this instance. + pushSignature(); + + } catch (CryptoManager.NotInitializedException nie) { + setupSigningFailure("BASE_CRYPTOMANAGER_UNINITIALIZED", nie); + } catch (ObjectNotFoundException onfe) { + setupSigningFailure("LOG_SIGNING_CERT_NOT_FOUND", onfe); + } catch (TokenException te) { + setupSigningFailure("BASE_TOKEN_ERROR_0", te); + } catch (NoSuchAlgorithmException nsae) { + setupSigningFailure("LOG_NO_SUCH_ALGORITHM_0", nsae); + } catch (NoSuchProviderException nspe) { + setupSigningFailure("BASE_PROVIDER_NOT_SUPPORTED", nspe); + } catch (InvalidKeyException ike) { + setupSigningFailure("BASE_INVALID_KEY", ike); + } catch (SignatureException se) { + setupSigningFailure("LOG_SIGNING_OP_FAILED", se); + } catch (UnsupportedEncodingException uee) { + setupSigningFailure("LOG_UNEXPECTED_EXCEPTION", uee); + } catch (IOException ioe) { + setupSigningFailure("LOG_UNEXPECTED_EXCEPTION", ioe); + } catch (Exception e) { + setupSigningFailure("LOG_UNEXPECTED_EXCEPTION", e); + } + } + + private static void setupSigningFailure(String logMessageCode, Exception e) + throws EBaseException { + try { + ConsoleError.send(new SystemEvent( + CMS.getLogMessage(logMessageCode))); + } catch (Exception e2) { + // don't allow an exception while printing to the console + // prevent us from running the rest of this function. + e2.printStackTrace(); + } + e.printStackTrace(); + shutdownCMS(); + throw new EBaseException(e.toString()); + } + + /** + * Startup the instance + * <P> + * + * <ul> + * <li>signed.audit LOGGING_SIGNED_AUDIT_AUDIT_LOG_STARTUP used at audit function startup + * </ul> + * + * @exception EBaseException if an internal error occurred + */ + public void startup() throws EBaseException { + // ensure that any low-level exceptions are reported + // to the signed audit log and stored as failures + CMS.debug("LogFile: entering LogFile.startup()"); + if (mOn && mLogSigning) { + try { + setupSigning(); + audit(CMS.getLogMessage( + LOGGING_SIGNED_AUDIT_AUDIT_LOG_STARTUP, + ILogger.SYSTEM_UID, + ILogger.SUCCESS)); + } catch (EBaseException e) { + audit(CMS.getLogMessage( + LOGGING_SIGNED_AUDIT_AUDIT_LOG_STARTUP, + ILogger.SYSTEM_UID, + ILogger.FAILURE)); + throw e; + } + } + + } + + /** + * Retrieves the eventType this log is triggered. + */ + public String getType() { + return mType; + } + + /** + * Retrieves the log on/off. + */ + public String getOn() { + return String.valueOf(mOn); + } + + /** + * Retrieves the log level threshold. + */ + public long getLevel() { + return mLevel; + } + + /** + * Retrieves the base log file name. + */ + public String getName() { + return mFileName; + } + + private boolean firstOpen = true; + + /** + * Record that the signed audit log has been signed + * <P> + * + * <ul> + * <li>signed.audit LOGGING_SIGNED_AUDIT_SIGNING used when a signature on the audit log is generated (same as + * "flush" time) + * </ul> + * + * @exception IOException for input/output problems + * @exception ELogException when plugin implementation fails + * @exception SignatureException when signing fails + * @exception InvalidKeyException when an invalid key is utilized + */ + private void pushSignature() throws IOException, ELogException, + SignatureException, InvalidKeyException { + byte[] sigBytes = null; + + if (mSignature == null) { + return; + } + + sigBytes = mSignature.sign(); + mSignature.initSign(mSigningKey); + + Object o[] = new Object[1]; + o[0] = null; + + // cook up a signed audit log message to record mac + // so as to avoid infinite recursiveness of calling + // the log() method + String auditMessage = CMS.getLogMessage( + LOGGING_SIGNED_AUDIT_SIGNING, + ILogger.SYSTEM_UID, + ILogger.SUCCESS, + base64Encode(sigBytes)); + + if (mSignedAuditLogger == null) { + return; + } + + ILogEvent ev = mSignedAuditLogger.create( + ILogger.EV_SIGNED_AUDIT, + (Properties) null, + ILogger.S_SIGNED_AUDIT, + ILogger.LL_SECURITY, + auditMessage, + o, + ILogger.L_SINGLELINE); + + String logMesg = logEvt2String(ev); + doLog(logMesg, true); + } + + private static String getLastSignature(File f) throws IOException { + BufferedReader r = new BufferedReader(new FileReader(f)); + String lastSig = null; + String curLine = null; + while ((curLine = r.readLine()) != null) { + if (curLine.indexOf("AUDIT_LOG_SIGNING") != -1) { + lastSig = curLine; + } + } + r.close(); + return lastSig; + } + + /** + * Open the log file. This creates the buffered FileWriter + * + */ + protected synchronized void open() throws IOException { + RandomAccessFile out; + + try { + out = new RandomAccessFile(mFile, "rw"); + out.seek(out.length()); + //XXX int or long? + mBytesWritten = (int) out.length(); + if (!Utils.isNT()) { + try { + Utils.exec("chmod 00640 " + mFile.getCanonicalPath()); + } catch (IOException e) { + CMS.debug("Unable to change file permissions on " + + mFile.toString()); + } + } + mLogWriter = new BufferedWriter( + new FileWriter(out.getFD()), mBufferSize); + + // The first time we open, mSignature will not have been + // initialized yet. That's ok, we will push our first signature + // in setupSigning(). + if (mLogSigning && (mSignature != null)) { + try { + pushSignature(); + } catch (ELogException le) { + ConsoleError.send( + new SystemEvent(CMS.getUserMessage("CMS_LOG_ILLEGALARGUMENT", + mFileName))); + } + } + } catch (IllegalArgumentException iae) { + ConsoleError.send( + new SystemEvent(CMS.getUserMessage("CMS_LOG_ILLEGALARGUMENT", + mFileName))); + } catch (GeneralSecurityException gse) { + // error with signed audit log, shutdown CMS + gse.printStackTrace(); + shutdownCMS(); + } + + mBytesUnflushed = 0; + } + + /** + * Flush the log file. Also update the MAC for hash protected logs + * + */ + public synchronized void flush() { + try { + if (mLogSigning) { + try { + pushSignature(); + } catch (ELogException le) { + ConsoleError.send(new SystemEvent(CMS.getUserMessage("CMS_LOG_FLUSH_LOG_FAILED", mFileName, + le.toString()))); + } + } + + if (mLogWriter != null) { + mLogWriter.flush(); + } + } catch (IOException e) { + ConsoleError.send(new SystemEvent(CMS.getUserMessage("CMS_LOG_FLUSH_LOG_FAILED", mFileName, e.toString()))); + if (mLogSigning) { + //error in writing to signed audit log, shut down CMS + e.printStackTrace(); + shutdownCMS(); + } + } catch (GeneralSecurityException gse) { + // error with signed audit log, shutdown CMS + gse.printStackTrace(); + shutdownCMS(); + } + + mBytesUnflushed = 0; + } + + /** + * Close the log file + * + */ + protected synchronized void close() { + try { + flush(); + if (mLogWriter != null) { + mLogWriter.close(); + } + } catch (IOException e) { + ConsoleError.send(new SystemEvent(CMS.getUserMessage("CMS_LOG_CLOSE_FAILED", mFileName, e.toString()))); + } + mLogWriter = null; + } + + /** + * Shutdown this log file. + * <P> + * + * <ul> + * <li>signed.audit LOGGING_SIGNED_AUDIT_AUDIT_LOG_SHUTDOWN used at audit function shutdown + * </ul> + */ + public synchronized void shutdown() { + String auditMessage = null; + + CMS.debug("LogFile:In log shutdown"); + + setFlushInterval(0); + + // log signed audit shutdown success + auditMessage = CMS.getLogMessage( + LOGGING_SIGNED_AUDIT_AUDIT_LOG_SHUTDOWN, + ILogger.SYSTEM_UID, + ILogger.SUCCESS); + + audit(auditMessage); + + close(); + } + + /** + * Set the flush interval + * <P> + * + * @param flushInterval The amount of time in seconds until the log + * is flush. A value of 0 will disable autoflush. This will also set + * the update period for hash protected logs. + **/ + public synchronized void setFlushInterval(int flushInterval) { + mFlushInterval = flushInterval * 1000; + + if ((mFlushThread == null) && (mFlushInterval > 0)) { + mFlushThread = new FlushThread(); + mFlushThread.setDaemon(true); + mFlushThread.start(); + } + + this.notify(); + } + + /** + * Log flush thread. Sleep for the flush interval and flush the + * log. Changing flush interval to 0 will cause this thread to exit. + */ + final class FlushThread extends Thread { + + /** + * Flush thread constructor including thread name + */ + public FlushThread() { + super(); + super.setName(mFileName + ".flush-" + (Thread.activeCount() + 1)); + } + + public void run() { + while (mFlushInterval > 0) { + // Sleep for the interval and then flush the log + synchronized (LogFile.this) { + try { + LogFile.this.wait(mFlushInterval); + } catch (InterruptedException e) { + // This shouldn't happen very often + ConsoleError.send(new + SystemEvent(CMS.getUserMessage("CMS_LOG_THREAD_INTERRUPT", "flush"))); + } + } + + if (mFlushInterval == 0) { + break; + } + + if (mBytesUnflushed > 0) { + flush(); + } + } + mFlushThread = null; + } + } + + /** + * Synchronized method to write a string to the log file. All I18N + * should take place before this call. + * + * @param entry The log entry string + */ + protected synchronized void log(String entry) throws ELogException { + doLog(entry, false); + } + + // Standard line separator byte. We always sign this line separator, + // regardless of what we actually write to the file, so that signature + // verification is platform-independent. + private static final byte LINE_SEP_BYTE = 0x0a; + + /** + * This method actually does the logging, and is not overridden + * by subclasses, so you can call it and know that it will do exactly + * what you see below. + */ + private synchronized void doLog(String entry, boolean noFlush) + throws ELogException { + if (mLogWriter == null) { + String[] params = { mFileName, entry }; + + throw new ELogException(CMS.getUserMessage("CMS_LOG_LOGFILE_CLOSED", params)); + } else { + try { + mLogWriter.write(entry, 0/*offset*/, entry.length()); + + if (mLogSigning == true) { + if (mSignature != null) { + // include newline for calculating MAC + mSignature.update(entry.getBytes("UTF-8")); + } else { + CMS.debug("LogFile: mSignature is not yet ready... null in log()"); + } + } + if (mTrace) { + CharArrayWriter cw = new CharArrayWriter(200); + PrintWriter pw = new PrintWriter(cw); + Exception e = new Exception(); + e.printStackTrace(pw); + char[] c = cw.toCharArray(); + cw.close(); + pw.close(); + + CharArrayReader cr = new CharArrayReader(c); + LineNumberReader lr = new LineNumberReader(cr); + + String text = null; + String method = null; + String fileAndLine = null; + if (lr.ready()) { + text = lr.readLine(); + do { + text = lr.readLine(); + } while (text.indexOf("logging") != -1); + int p = text.indexOf("("); + fileAndLine = text.substring(p); + + String classandmethod = text.substring(0, p); + int q = classandmethod.lastIndexOf("."); + method = classandmethod.substring(q + 1); + mLogWriter.write(fileAndLine, 0/*offset*/, fileAndLine.length()); + mLogWriter.write(" ", 0/*offset*/, " ".length()); + mLogWriter.write(method, 0/*offset*/, method.length()); + } + } + mLogWriter.newLine(); + + if (mLogSigning == true) { + if (mSignature != null) { + mSignature.update(LINE_SEP_BYTE); + } else { + CMS.debug("LogFile: mSignature is null in log() 2"); + } + } + } catch (IOException e) { + ConsoleError.send(new SystemEvent(CMS.getUserMessage("CMS_LOG_WRITE_FAILED", mFileName, entry, + e.toString()))); + if (mLogSigning) { + // Failed to write to audit log, shut down CMS + e.printStackTrace(); + shutdownCMS(); + } + } catch (IllegalStateException e) { + CMS.debug("LogFile: exception thrown in log(): " + e.toString()); + ConsoleError.send(new SignedAuditEvent(CMS.getLogMessage(LOG_SIGNED_AUDIT_EXCEPTION, e.toString()))); + } catch (GeneralSecurityException gse) { + // DJN: handle error + CMS.debug("LogFile: exception thrown in log(): " + + gse.toString()); + gse.printStackTrace(); + ConsoleError.send(new SignedAuditEvent(CMS.getLogMessage( + LOG_SIGNED_AUDIT_EXCEPTION, gse.toString()))); + } + + // XXX + // Although length will be in Unicode dual-bytes, the PrintWriter + // will only print out 1 byte per character. I suppose this could + // be dependent on the encoding of your log file, but it ain't that + // smart yet. Also, add one for the newline. (hmm, on NT, CR+LF) + int nBytes = entry.length() + 1; + + mBytesWritten += nBytes; + mBytesUnflushed += nBytes; + + if (mBufferSize > 0 && mBytesUnflushed > mBufferSize && !noFlush) { + flush(); + } + } + } + + /** + * Write an event to the log file + * + * @param ev The event to be logged. + */ + public void log(ILogEvent ev) throws ELogException { + if (ev instanceof AuditEvent) { + if (!mType.equals("transaction") || (!mOn) || mLevel > ev.getLevel()) { + return; + } + } else if (ev instanceof SystemEvent) { + if (!mType.equals("system") || (!mOn) || mLevel > ev.getLevel()) { + return; + } + } else if (ev instanceof SignedAuditEvent) { + if (!mType.equals("signedAudit") || (!mOn) || mLevel > ev.getLevel()) { + return; + } + } + + // Is the event type selected? + // If no selection specified in configuration, then all are selected + // If no type specified in propertity file, then treated as selected + if (mSelectedEvents.size() > 0) { + String type = ev.getEventType(); + if (type != null) { + if (!mSelectedEvents.contains(type)) { + CMS.debug("LogFile: event type not selected: " + type); + return; + } + } + } + + String entry = logEvt2String(ev); + + log(entry); + } + + public String logEvt2String(ILogEvent ev) { + String entry = null; + + // Hmm.. multiple threads could hit this and reset the time. + // Do we care? + mDate.setTime(ev.getTimeStamp()); + + // XXX + // This should follow the Common Log Format which still needs + // some work. + if (ev.getMultiline() == ILogger.L_MULTILINE) { + entry = CMS.getPID() + "." + Thread.currentThread().getName() + " - [" + + mLogDateFormat.format(mDate) + "] [" + + Integer.toString(ev.getSource()) + "] [" + Integer.toString(ev.getLevel()) + + "] " + prepareMultiline(ev.toString()); + } else { + entry = CMS.getPID() + "." + Thread.currentThread().getName() + " - [" + + mLogDateFormat.format(mDate) + "] [" + + Integer.toString(ev.getSource()) + "] [" + Integer.toString(ev.getLevel()) + + "] " + ev.toString(); + } + + return entry; + } + + /** + * change multi-line log entry by replace "\n" with "\n " + * + * @param original The original multi-line log entry. + */ + private String prepareMultiline(String original) { + int i, last = 0; + + //NT: \r\n, unix: \n + while ((i = original.indexOf("\n", last)) != -1) { + last = i + 1; + original = original.substring(0, i + 1) + " " + original.substring(i + 1); + } + return original; + } + + /** + * Read all entries whose logLevel>=lowLevel && log source = source + * to at most maxLine entries(from end) + * If the parameter is -1, it's ignored and return all entries + * + * @param maxLine The maximum lines to be returned + * @param lowLevel The lowest log level to be returned + * @param source The particular log source to be returned + * @param fName The log file name to be read. If it's null, read the current + * log file + */ + public Vector<LogEntry> readEntry(int maxLine, int lowLevel, int source, String fName) { + Vector<LogEntry> mEntries = new Vector<LogEntry>(); + String fileName = mFileName; + BufferedReader fBuffer; + int lineNo = 0; // lineNo of the current entry in the log file + int line = 0; // line of readed valid entries + String firstLine = null; // line buffer + String nextLine = null; + String entry = null; + LogEntry logEntry = null; + + /* + this variable is added to accormodate misplaced multiline entries + write out buffered log entry when next entry is parsed successfully + this implementation is assuming parsing is more time consuming than + condition check + */ + LogEntry preLogEntry = null; + + if (fName != null) { + fileName = fName; + } + try { + //XXX think about this + fBuffer = new BufferedReader(new FileReader(fileName)); + do { + try { + nextLine = fBuffer.readLine(); + if (nextLine != null) { + if ((nextLine.length() == 0) || (nextLine.charAt(0) == ' ')) { + // It's a continuous line + entry = null; + if (nextLine.length() > 1) + firstLine = firstLine + "\n" + nextLine.substring(1); + else + firstLine = firstLine + "\n"; + + } else { + // It's a new entry + entry = firstLine; + firstLine = nextLine; + } + // parse the previous entry, the current one is buffered + if (entry != null) { + try { + logEntry = new LogEntry(entry); + // if parse succeed, write out previous entry + if (preLogEntry != null) { + if ((Integer.parseInt(preLogEntry.getLevel()) >= lowLevel) && + ((Integer.parseInt(preLogEntry.getSource()) == source) || + (source == ILogger.S_ALL) + )) { + mEntries.addElement(preLogEntry); + if (maxLine == -1) { + line++; + } else if (line < maxLine) { + line++; + } else { + mEntries.removeElementAt(0); + } + } + } + preLogEntry = logEntry; + } catch (ParseException e) { + if (preLogEntry != null) { + preLogEntry.appendDetail(entry); + } else { + firstLine = firstLine + "\n" + nextLine; + } + entry = null; + logEntry = null; + } + } + } + lineNo++; + + } catch (IOException e) { + CMS.getLogger().log(ILogger.EV_SYSTEM, ILogger.S_OTHER, + ILogger.LL_FAILURE, + CMS.getLogMessage("LOGGING_READ_ERROR", fileName, + Integer.toString(lineNo))); + } + + } while (nextLine != null); + + // need to process the last 2 entries of the file + if (firstLine != null) { + if (logEntry != null) { + preLogEntry = logEntry; + } + entry = firstLine; + try { + logEntry = new LogEntry(entry); + + /* System.out.println( + Integer.toString(Integer.parseInt(logEntry.getLevel())) + +","+Integer.toString(lowLevel)+","+ + Integer.toString(Integer.parseInt(logEntry.getSource())) + +","+Integer.toString(source) ); + */ + if (preLogEntry != null) { + if ((Integer.parseInt(preLogEntry.getLevel()) >= lowLevel) && + ((Integer.parseInt(preLogEntry.getSource()) == source) || + (source == ILogger.S_ALL) + )) { + mEntries.addElement(preLogEntry); + if (maxLine == -1) { + line++; + } else if (line < maxLine) { + line++; + } else { + mEntries.removeElementAt(0); + } + } + } + preLogEntry = logEntry; + } catch (ParseException e) { + preLogEntry.appendDetail(entry); + } + + if (preLogEntry != null) { + if ((Integer.parseInt(preLogEntry.getLevel()) >= lowLevel) + && + ((Integer.parseInt(preLogEntry.getSource()) == source) + || + (source == ILogger.S_ALL) + )) { + // parse the entry, pass to UI + mEntries.addElement(preLogEntry); + if (maxLine == -1) { + line++; + } else if (line < maxLine) { + line++; + } else { + mEntries.removeElementAt(0); + } + } + } + + }// end: last entry + + try { + fBuffer.close(); + } catch (IOException e) { + CMS.getLogger().log(ILogger.EV_SYSTEM, ILogger.S_OTHER, + ILogger.LL_FAILURE, "logging:" + fileName + + " failed to close for reading"); + } + + } catch (FileNotFoundException e) { + CMS.getLogger().log(ILogger.EV_SYSTEM, ILogger.S_OTHER, + ILogger.LL_FAILURE, + CMS.getLogMessage("LOGGING_FILE_NOT_FOUND", + fileName)); + } + return mEntries; + } + + /** + * Retrieves the configuration store of this subsystem. + * <P> + * + * @return configuration store + */ + public IConfigStore getConfigStore() { + return mConfig; + } + + /** + * Retrieve last "maxLine" number of system log with log lever >"level" + * and from source "source". If the parameter is omitted. All entries + * are sent back. + */ + public synchronized NameValuePairs retrieveLogContent(Hashtable<String, String> req) throws ServletException, + IOException, EBaseException { + NameValuePairs params = new NameValuePairs(); + String tmp, fName = null; + int maxLine = -1, level = -1, source = -1; + Vector<LogEntry> entries = null; + + if ((tmp = (String) req.get(Constants.PR_LOG_ENTRY)) != null) { + maxLine = Integer.parseInt(tmp); + } + if ((tmp = (String) req.get(Constants.PR_LOG_LEVEL)) != null) { + level = Integer.parseInt(tmp); + } + if ((tmp = (String) req.get(Constants.PR_LOG_SOURCE)) != null) { + source = Integer.parseInt(tmp); + } + tmp = (String) req.get(Constants.PR_LOG_NAME); + if (!(tmp.equals(Constants.PR_CURRENT_LOG))) { + fName = tmp; + } else { + flush(); + } + + try { + entries = readEntry(maxLine, level, source, fName); + for (int i = 0; i < entries.size(); i++) { + params.put(Integer.toString(i) + + ((LogEntry) entries.elementAt(i)).getEntry(), ""); + } + } catch (Exception e) { + CMS.getLogger().log(ILogger.EV_SYSTEM, ILogger.S_OTHER, + ILogger.LL_WARN, + "System log parse error"); + } + return params; + } + + /** + * Retrieve log file list. + */ + public synchronized NameValuePairs retrieveLogList(Hashtable<String, String> req) throws ServletException, + IOException, EBaseException { + return null; + } + + public String getImplName() { + return "LogFile"; + } + + public String getDescription() { + return "LogFile"; + } + + public Vector<String> getDefaultParams() { + Vector<String> v = new Vector<String>(); + + v.addElement(PROP_TYPE + "="); + v.addElement(PROP_ON + "="); + v.addElement(PROP_LEVEL + "="); + v.addElement(PROP_FILE_NAME + "="); + v.addElement(PROP_BUFFER_SIZE + "="); + v.addElement(PROP_FLUSH_INTERVAL + "="); + + // needs to find a way to determine what type you want. if this + // is not for the signed audit type, then we should not show the + // following parameters. + //if( mType.equals( ILogger.PROP_SIGNED_AUDIT ) ) { + v.addElement(PROP_SIGNED_AUDIT_LOG_SIGNING + "="); + v.addElement(PROP_SIGNED_AUDIT_CERT_NICKNAME + "="); + v.addElement(PROP_SIGNED_AUDIT_EVENTS + "="); + //} + + return v; + } + + public Vector<String> getInstanceParams() { + Vector<String> v = new Vector<String>(); + + try { + + if (mType == null) { + v.addElement(PROP_TYPE + "="); + } else { + v.addElement(PROP_TYPE + "=" + + mConfig.getString(PROP_TYPE)); + } + v.addElement(PROP_ON + "=" + String.valueOf(mOn)); + if (mLevel == 0) + v.addElement(PROP_LEVEL + "=" + ILogger.LL_DEBUG_STRING); + else if (mLevel == 1) + v.addElement(PROP_LEVEL + "=" + ILogger.LL_INFO_STRING); + else if (mLevel == 2) + v.addElement(PROP_LEVEL + "=" + ILogger.LL_WARN_STRING); + else if (mLevel == 3) + v.addElement(PROP_LEVEL + "=" + ILogger.LL_FAILURE_STRING); + else if (mLevel == 4) + v.addElement(PROP_LEVEL + "=" + ILogger.LL_MISCONF_STRING); + else if (mLevel == 5) + v.addElement(PROP_LEVEL + "=" + ILogger.LL_CATASTRPHE_STRING); + else if (mLevel == 6) + v.addElement(PROP_LEVEL + "=" + ILogger.LL_SECURITY_STRING); + + if (mFileName == null) { + v.addElement(PROP_FILE_NAME + "="); + } else { + v.addElement(PROP_FILE_NAME + "=" + + mFileName); + } + v.addElement(PROP_BUFFER_SIZE + "=" + mBufferSize); + v.addElement(PROP_FLUSH_INTERVAL + "=" + mFlushInterval / 1000); + + if ((mType != null) && mType.equals(ILogger.PROP_SIGNED_AUDIT)) { + v.addElement(PROP_SIGNED_AUDIT_LOG_SIGNING + "=" + + String.valueOf(mLogSigning)); + + if (mSAuditCertNickName == null) { + v.addElement(PROP_SIGNED_AUDIT_CERT_NICKNAME + "="); + } else { + v.addElement(PROP_SIGNED_AUDIT_CERT_NICKNAME + "=" + + mSAuditCertNickName); + } + + if (mSelectedEventsList == null) { + v.addElement(PROP_SIGNED_AUDIT_EVENTS + "="); + } else { + v.addElement(PROP_SIGNED_AUDIT_EVENTS + "=" + + mSelectedEventsList); + } + } + } catch (Exception e) { + } + return v; + } + + public String[] getExtendedPluginInfo(Locale locale) { + if (mType.equals(ILogger.PROP_SIGNED_AUDIT)) { + String[] params = { + PROP_TYPE + + ";choice(transaction,signedAudit,system);The log event type this instance is listening to", + PROP_ON + ";boolean;Turn on the listener", + PROP_LEVEL + ";choice(" + ILogger.LL_DEBUG_STRING + "," + + ILogger.LL_INFO_STRING + "," + + ILogger.LL_WARN_STRING + "," + + ILogger.LL_FAILURE_STRING + "," + + ILogger.LL_MISCONF_STRING + "," + + ILogger.LL_CATASTRPHE_STRING + "," + + ILogger.LL_SECURITY_STRING + + ");Only log message with level higher than this filter will be written by this listener", + PROP_FILE_NAME + ";string;The name of the file the log is written to", + PROP_BUFFER_SIZE + ";integer;The size of the buffer to receive log messages in kilobytes(KB)", + PROP_FLUSH_INTERVAL + + ";integer;The maximum time in seconds before the buffer is flushed to the file", + IExtendedPluginInfo.HELP_TOKEN + + ";configuration-logrules-logfile", + IExtendedPluginInfo.HELP_TEXT + + ";Write the log messages to a file", + PROP_SIGNED_AUDIT_LOG_SIGNING + + ";boolean;Enable audit logs to be signed", + PROP_SIGNED_AUDIT_CERT_NICKNAME + + ";string;The nickname of the certificate to be used to sign audit logs", + PROP_SIGNED_AUDIT_EVENTS + + ";string;A comma-separated list of strings used to specify particular signed audit log events", + }; + + return params; + } else { + // mType.equals( ILogger.PROP_AUDIT ) || + // mType.equals( ILogger.PROP_SYSTEM ) + String[] params = { + PROP_TYPE + + ";choice(transaction,signedAudit,system);The log event type this instance is listening to", + PROP_ON + ";boolean;Turn on the listener", + PROP_LEVEL + ";choice(" + ILogger.LL_DEBUG_STRING + "," + + ILogger.LL_INFO_STRING + "," + + ILogger.LL_WARN_STRING + "," + + ILogger.LL_FAILURE_STRING + "," + + ILogger.LL_MISCONF_STRING + "," + + ILogger.LL_CATASTRPHE_STRING + "," + + ILogger.LL_SECURITY_STRING + + ");Only log message with level higher than this filter will be written by this listener", + PROP_FILE_NAME + ";string;The name of the file the log is written to", + PROP_BUFFER_SIZE + ";integer;The size of the buffer to receive log messages in kilobytes(KB)", + PROP_FLUSH_INTERVAL + + ";integer;The maximum time in seconds before the buffer is flushed to the file", + IExtendedPluginInfo.HELP_TOKEN + + ";configuration-logrules-logfile", + IExtendedPluginInfo.HELP_TEXT + + ";Write the log messages to a file" + }; + + return params; + } + } + + /** + * Signed Audit Log + * + * This method is inherited by all classes that extend this "LogFile" + * class, and is called to store messages to the signed audit log. + * <P> + * + * @param msg signed audit log message + */ + protected void audit(String msg) { + // in this case, do NOT strip preceding/trailing whitespace + // from passed-in String parameters + + if (mSignedAuditLogger == null) { + return; + } + + mSignedAuditLogger.log(ILogger.EV_SIGNED_AUDIT, + null, + ILogger.S_SIGNED_AUDIT, + ILogger.LL_SECURITY, + msg); + } +} diff --git a/base/common/src/com/netscape/cms/logging/RollingLogFile.java b/base/common/src/com/netscape/cms/logging/RollingLogFile.java new file mode 100644 index 000000000..93455e9fe --- /dev/null +++ b/base/common/src/com/netscape/cms/logging/RollingLogFile.java @@ -0,0 +1,658 @@ +// --- BEGIN COPYRIGHT BLOCK --- +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; version 2 of the License. +// +// This program 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 General Public License for more details. +// +// You should have received a copy of the GNU General Public License along +// with this program; if not, write to the Free Software Foundation, Inc., +// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +// +// (C) 2007 Red Hat, Inc. +// All rights reserved. +// --- END COPYRIGHT BLOCK --- +package com.netscape.cms.logging; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FilenameFilter; +import java.io.IOException; +import java.io.PrintWriter; +import java.util.Hashtable; +import java.util.Locale; +import java.util.Vector; + +import javax.servlet.ServletException; + +import com.netscape.certsrv.apps.CMS; +import com.netscape.certsrv.base.EBaseException; +import com.netscape.certsrv.base.IConfigStore; +import com.netscape.certsrv.base.IExtendedPluginInfo; +import com.netscape.certsrv.common.NameValuePairs; +import com.netscape.certsrv.logging.ConsoleError; +import com.netscape.certsrv.logging.ELogException; +import com.netscape.certsrv.logging.ILogEvent; +import com.netscape.certsrv.logging.ILogger; +import com.netscape.certsrv.logging.SystemEvent; +import com.netscape.cmsutil.util.Utils; + +/** + * A rotating log file for Certificate log events. This class loosely follows + * the Netscape Common Log API implementing rollover interval, size and file + * naming conventions. It does not yet implement Disk Usage. + * + * @version $Revision$, $Date$ + */ +public class RollingLogFile extends LogFile { + public static final String PROP_MAX_FILE_SIZE = "maxFileSize"; + public static final String PROP_ROLLOVER_INTERVAL = "rolloverInterval"; + public static final String PROP_EXPIRATION_TIME = "expirationTime"; + + /** + * The default max file size in bytes + */ + static final int MAX_FILE_SIZE = 100; + + /** + * The default rollover interval in seconds + */ + static final String ROLLOVER_INTERVAL = "2592000"; + + /** + * The default expiration time in seconds + */ + static final String EXPIRATION_TIME = "2592000"; + + /** + * The maximum file size in bytes + */ + protected int mMaxFileSize = 0; + + /** + * The amount of time in miniseconds between log rotations + */ + protected long mRolloverInterval = 0; + + /** + * The thread responsible for rotating the log + */ + private Thread mRolloverThread = null; + + /** + * The incrementing backup number for the log file names + */ + private int mFileNumber = 1; + + /** + * The amount of time before a backed up log is removed in milliseconds + */ + protected long mExpirationTime = 0; + + /** + * The thread responsible for removing expired log files + */ + private Thread mExpirationThread = null; + + /** + * The object used as a lock for expiration thread synchronization + */ + private Object mExpLock = new Object(); + + private final static String LOGGING_SIGNED_AUDIT_LOG_DELETE = + "LOGGING_SIGNED_AUDIT_LOG_DELETE_3"; + + /** + * Construct a RollingLogFile + */ + public RollingLogFile() { + } + + /** + * Initialize and open a RollingLogFile using the prop config store + * + * @param config The property config store to find values in + */ + public void init(IConfigStore config) throws IOException, + EBaseException { + super.init(config); + + rl_init(config.getInteger(PROP_MAX_FILE_SIZE, MAX_FILE_SIZE), + config.getString(PROP_ROLLOVER_INTERVAL, ROLLOVER_INTERVAL), + config.getString(PROP_EXPIRATION_TIME, EXPIRATION_TIME)); + } + + /** + * Convenience routine to initialized the RollingLogFile specific + * attributes. + */ + protected void rl_init(int maxFileSize, String rolloverInterval, + String expirationTime) { + mMaxFileSize = maxFileSize * 1024; + setRolloverTime(rolloverInterval); + setExpirationTime(expirationTime); + } + + public void startup() throws EBaseException { + super.startup(); + } + + /** + * Shutdown this log file. + */ + public synchronized void shutdown() { + setRolloverTime("0"); + setExpirationTime("0"); + super.shutdown(); + } + + /** + * Set the rollover interval + * + * @param rolloverSeconds The amount of time in seconds until the log + * is rotated. A value of 0 will disable log rollover. + **/ + public synchronized void setRolloverTime(String rolloverSeconds) { + mRolloverInterval = Long.valueOf(rolloverSeconds).longValue() * 1000; + + if ((mRolloverThread == null) && (mRolloverInterval > 0)) { + mRolloverThread = new RolloverThread(); + mRolloverThread.setDaemon(true); + mRolloverThread.start(); + } + + this.notify(); + } + + /** + * Get the rollover interval + * + * @return The interval in seconds in which the log is rotated + **/ + public synchronized int getRolloverTime() { + return (int) (mRolloverInterval / 1000); + } + + /** + * Set the file expiration time + * + * @param expirationSeconds The amount of time in seconds until log files + * are deleted + **/ + public void setExpirationTime(String expirationSeconds) { + + // Need to completely protect changes to mExpiration time + // and make sure they only happen while the thread is sleeping + synchronized (mExpLock) { + mExpirationTime = Long.valueOf(expirationSeconds).longValue() * 1000; + + if (mExpirationThread == null) { + if (mExpirationTime > 0) { + mExpirationThread = new ExpirationThread(); + mExpirationThread.setDaemon(true); + mExpirationThread.start(); + } + } else { + mExpLock.notify(); + } + } + } + + /** + * Get the expiration time + * + * @return The age in seconds in which log files are delete + **/ + public int getExpirationTime() { + return (int) (mExpirationTime / 1000); + } + + /** + * Rotate the log file to a backup file with a incrementing integer + * extension + **/ + public synchronized void rotate() + throws IOException { + + //File backupFile = new File(mFileName + "." + mFileNumber); + File backupFile = new File(mFileName + "." + mLogFileDateFormat.format(mDate)); + + // close, backup, and reopen the log file zeroizing its contents + super.close(); + try { + if (Utils.isNT()) { + // NT is very picky on the path + Utils.exec("copy " + + mFile.getCanonicalPath().replace('/', '\\') + + " " + + backupFile.getCanonicalPath().replace('/', + '\\')); + } else { + // Create a copy of the original file which + // preserves the original file permissions. + Utils.exec("cp -p " + mFile.getCanonicalPath() + " " + + backupFile.getCanonicalPath()); + } + + // Zeroize the original file if and only if + // the backup copy was successful. + if (backupFile.exists()) { + + // Make certain that the backup file has + // the correct permissions. + if (!Utils.isNT()) { + Utils.exec("chmod 00640 " + backupFile.getCanonicalPath()); + } + + try { + // Open and close the original file + // to zeroize its contents. + PrintWriter pw = new PrintWriter(mFile); + pw.close(); + + // Make certain that the original file retains + // the correct permissions. + if (!Utils.isNT()) { + Utils.exec("chmod 00640 " + mFile.getCanonicalPath()); + } + } catch (FileNotFoundException e) { + CMS.debug("Unable to zeroize " + + mFile.toString()); + } + } else { + CMS.debug("Unable to backup " + + mFile.toString() + " to " + + backupFile.toString()); + } + } catch (Exception e) { + CMS.debug("Unable to backup " + + mFile.toString() + " to " + + backupFile.toString()); + } + super.open(); // will reset mBytesWritten + mFileNumber++; + } + + /** + * Remove any log files which have not been modified in the specified + * time + * <P> + * + * NOTE: automatic removal of log files is currently NOT supported! + * <P> + * + * <ul> + * <li>signed.audit LOGGING_SIGNED_AUDIT_LOG_DELETE used AFTER audit log expires (authorization should not allow, + * but in case authorization gets compromised make sure it is written AFTER the log expiration happens) + * </ul> + * + * @param expirationSeconds The number of seconds since the expired files + * have been modified. + * @return the time in milliseconds when the next file expires + **/ + public long expire(long expirationSeconds) throws ELogException { + String auditMessage = null; + + if (expirationSeconds <= 0) + throw new ELogException(CMS.getUserMessage("CMS_LOG_EXPIRATION_TIME_ZERO")); + + long expirationTime = expirationSeconds * 1000; + long currentTime = System.currentTimeMillis(); + long oldestFile = currentTime; + + String dirName = mFile.getParent(); + + if (dirName == null) + dirName = "."; + File dir = new File(dirName); + + // Get just the base name, minus the .date extension + //int len = mFile.getName().length() - LogFile.DATE_PATTERN.length() - 1; + //String baseName = mFile.getName().substring(0, len); + String fileName = mFile.getName(); + String baseName = null, pathName = null; + int index = fileName.lastIndexOf("/"); + + if (index != -1) { // "/" exist in fileName + pathName = fileName.substring(0, index); + baseName = fileName.substring(index + 1); + dirName = dirName.concat("/" + pathName); + } else { // "/" NOT exist in fileName + baseName = fileName; + } + + fileFilter ff = new fileFilter(baseName + "."); + String[] filelist = dir.list(ff); + + if (filelist == null) { // Crap! Something is wrong. + throw new ELogException(CMS.getUserMessage("CMS_LOG_DIRECTORY_LIST_FAILED", + dirName, ff.toString())); + } + + // Walk through the list of files which match this log file name + // and delete the old ones. + for (int i = 0; i < filelist.length; i++) { + if (pathName != null) { + filelist[i] = pathName + "/" + filelist[i]; + } else { + filelist[i] = dirName + "/" + filelist[i]; + } + + String fullname = dirName + File.separatorChar + filelist[i]; + File file = new File(fullname); + long fileTime = file.lastModified(); + + // Java documentation on File says lastModified() should not + // be interpeted. The doc is wrong. See JavaSoft bug #4094538 + if ((currentTime - fileTime) > expirationTime) { + file.delete(); + + if (file.exists()) { + // log failure in deleting an expired signed audit log file + auditMessage = CMS.getLogMessage( + LOGGING_SIGNED_AUDIT_LOG_DELETE, + ILogger.SYSTEM_UID, + ILogger.FAILURE, + fullname); + } else { + // log success in deleting an expired signed audit log file + auditMessage = CMS.getLogMessage( + LOGGING_SIGNED_AUDIT_LOG_DELETE, + ILogger.SYSTEM_UID, + ILogger.SUCCESS, + fullname); + } + + audit(auditMessage); + } else if (fileTime < oldestFile) { + oldestFile = fileTime; + } + } + return oldestFile + expirationTime; + } + + // + // Rollover and Expiration threads + // + // At first glance you may think it's a waste of thread resources to have + // two threads for every log file, but the truth is that these threads are + // sleeping 99% of the time. NxN thread implementations (Solaris, NT, + // IRIX 6.4, Unixware, etc...) will handle these in user space. + // + // You may be able to join these into one thread, and deal with + // multiple wakeup times, but the code would sure look ugly, and the race + // conditions are numerous as is. Furthermore, this is what user space + // threads will do for you anyways. + // + + /** + * Log rotation thread. Sleep for the rollover interval and rotate the + * log. Changing rollover interval to 0 will cause this thread to exit. + */ + final class RolloverThread extends Thread { + + /** + * Rollover thread constructor including thread name + */ + public RolloverThread() { + super(); + super.setName(mFileName + ".rollover-" + (Thread.activeCount() + 1)); + } + + public void run() { + while (mRolloverInterval > 0) { + // Sleep for the interval and then rotate the log + synchronized (RollingLogFile.this) { + try { + RollingLogFile.this.wait(mRolloverInterval); + } catch (InterruptedException e) { + // This shouldn't happen very often + CMS.getLogger().getLogQueue().log(new + SystemEvent(CMS.getUserMessage("CMS_LOG_THREAD_INTERRUPT", "rollover"))); + } + } + + if (mRolloverInterval == 0) { + break; + } + + if (mBytesWritten > 0) { + try { + rotate(); + } catch (IOException e) { + ConsoleError.send(new + SystemEvent(CMS.getUserMessage("CMS_LOG_ROTATE_LOG_FAILED", mFile.getName(), + e.toString()))); + break; + } + } + // else + // Don't rotate empty logs + // flag in log summary file? + } + mRolloverThread = null; + } + } + + /** + * Log expiration thread. Sleep for the expiration interval and + * delete any files which are too old. + * Changing expiration interval to 0 will cause this thread to exit. + */ + final class ExpirationThread extends Thread { + + /** + * ExpirationThread thread constructor including thread name + */ + public ExpirationThread() { + super(); + super.setName(mFileName + ".expiration-" + (Thread.activeCount() + 1)); + } + + public void run() { + synchronized (mExpLock) { + while (mExpirationTime > 0) { + long wakeupTime = 0; + long sleepTime = 0; + + // First, remove any old log files and figure out when the + // next one expires + try { + wakeupTime = expire((long) (mExpirationTime / 1000)); + } catch (SecurityException e) { + ConsoleError.send(new + SystemEvent(CMS.getUserMessage("CMS_LOG_EXPIRE_LOG_FAILED", e.toString()))); + break; + } catch (ELogException e) { + ConsoleError.send(new + SystemEvent(CMS.getUserMessage("CMS_LOG_EXPIRE_LOG_FAILED", e.toString()))); + break; + } + + sleepTime = wakeupTime - System.currentTimeMillis(); + //System.out.println("wakeup " + wakeupTime); + //System.out.println("current "+System.currentTimeMillis()); + //System.out.println("sleep " + sleepTime); + // Sleep for the interval and then check the directory + // Note: mExpirationTime can only change while we're + // sleeping + if (sleepTime > 0) { + try { + mExpLock.wait(sleepTime); + } catch (InterruptedException e) { + // This shouldn't happen very often + ConsoleError.send(new + SystemEvent(CMS.getUserMessage("CMS_LOG_THREAD_INTERRUPT", "expiration"))); + } + } + } + } + mExpirationThread = null; + } + } + + /** + * Write an event to the log file + * + * @param ev The event to be logged. + **/ + public synchronized void log(ILogEvent ev) throws ELogException { + //xxx, Shall we log first without checking if it exceed the maximum? + super.log(ev); // Will increment mBytesWritten + + if ((0 != mMaxFileSize) && (mBytesWritten > mMaxFileSize)) { + flush(); + try { + rotate(); + } catch (IOException e) { + throw new ELogException(CMS.getUserMessage("CMS_LOG_ROTATE_LOG_FAILED", mFile.getName(), e.toString())); + } + } + } + + /** + * Retrieve log file list. + */ + public synchronized NameValuePairs retrieveLogList(Hashtable<String, String> req + ) throws ServletException, + IOException, EBaseException { + NameValuePairs params = new NameValuePairs(); + String[] files = null; + + files = fileList(); + for (int i = 0; i < files.length; i++) { + params.put(files[i], ""); + } + return params; + } + + /** + * Get the log file list in the log directory + * + * @return an array of filenames with related path to cert server root + */ + protected String[] fileList() { + String pathName = null, baseName = null; + + String dirName = mFile.getParent(); + String fileName = mFile.getName(); + int index = fileName.lastIndexOf("/"); + + if (index != -1) { // "/" exist in fileName + pathName = fileName.substring(0, index); + baseName = fileName.substring(index + 1); + if (dirName == null) { + dirName = pathName; + } else { + dirName = dirName.concat("/" + pathName); + } + } else { // "/" NOT exist in fileName + baseName = fileName; + } + + File dir = new File(dirName); + + fileFilter ff = new fileFilter(baseName + "."); + //There are some difference here. both should work + //error,logs,logs/error jdk115 + //logs/system,., logs/system jdk116 + //System.out.println(mFile.getName()+","+dirName+","+mFile.getPath()); //log/system,. + + String[] filelist = dir.list(ff); + + for (int i = 0; i < filelist.length; i++) { + if (pathName != null) { + filelist[i] = pathName + "/" + filelist[i]; + } else { + filelist[i] = dirName + "/" + filelist[i]; + } + } + return filelist; + } + + public String getImplName() { + return "RollingLogFile"; + } + + public String getDescription() { + return "RollingLogFile"; + } + + public Vector<String> getDefaultParams() { + Vector<String> v = super.getDefaultParams(); + + v.addElement(PROP_MAX_FILE_SIZE + "="); + v.addElement(PROP_ROLLOVER_INTERVAL + "="); + //v.addElement(PROP_EXPIRATION_TIME + "="); + return v; + } + + public Vector<String> getInstanceParams() { + Vector<String> v = super.getInstanceParams(); + + try { + v.addElement(PROP_MAX_FILE_SIZE + "=" + mMaxFileSize / 1024); + if (mRolloverInterval / 1000 <= 60 * 60) + v.addElement(PROP_ROLLOVER_INTERVAL + "=" + "Hourly"); + else if (mRolloverInterval / 1000 <= 60 * 60 * 24) + v.addElement(PROP_ROLLOVER_INTERVAL + "=" + "Daily"); + else if (mRolloverInterval / 1000 <= 60 * 60 * 24 * 7) + v.addElement(PROP_ROLLOVER_INTERVAL + "=" + "Weekly"); + else if (mRolloverInterval / 1000 <= 60 * 60 * 24 * 30) + v.addElement(PROP_ROLLOVER_INTERVAL + "=" + "Monthly"); + else if (mRolloverInterval / 1000 <= 60 * 60 * 24 * 366) + v.addElement(PROP_ROLLOVER_INTERVAL + "=" + "Yearly"); + + //v.addElement(PROP_EXPIRATION_TIME + "=" + mExpirationTime / 1000); + } catch (Exception e) { + } + return v; + } + + public String[] getExtendedPluginInfo(Locale locale) { + String[] p = super.getExtendedPluginInfo(locale); + Vector<String> info = new Vector<String>(); + + for (int i = 0; i < p.length; i++) { + if (!p[i].startsWith(IExtendedPluginInfo.HELP_TOKEN) && !p[i].startsWith(IExtendedPluginInfo.HELP_TEXT)) + info.addElement(p[i]); + } + info.addElement(PROP_MAX_FILE_SIZE + + ";integer;If the current log file size if bigger than this parameter in kilobytes(KB), the file will be rotated."); + info.addElement(PROP_ROLLOVER_INTERVAL + + ";choice(Hourly,Daily,Weekly,Monthly,Yearly);The frequency of the log being rotated."); + info.addElement(PROP_EXPIRATION_TIME + + ";integer;The amount of time before a backed up log is removed in seconds"); + info.addElement(IExtendedPluginInfo.HELP_TOKEN + + //";configuration-logrules-rollinglogfile"); + ";configuration-adminbasics"); + info.addElement(IExtendedPluginInfo.HELP_TEXT + + ";Write the log messages to a file which will be rotated automatically."); + String[] params = new String[info.size()]; + + info.copyInto(params); + return params; + + } +} + +/** + * A file filter to select the file with a given prefix + */ +class fileFilter implements FilenameFilter { + String patternToMatch = null; + + public fileFilter(String pattern) { + patternToMatch = pattern; + } + + public boolean accept(File dir, String name) { + if (name.startsWith(patternToMatch)) + return true; + else + return false; + } +} |