/*******************************************************************************
* This file is part of OpenNMS(R).
*
* Copyright (C) 2006-2011 The OpenNMS Group, Inc.
* OpenNMS(R) is Copyright (C) 1999-2011 The OpenNMS Group, Inc.
*
* OpenNMS(R) is a registered trademark of The OpenNMS Group, Inc.
*
* OpenNMS(R) 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, either version 3 of the License,
* or (at your option) any later version.
*
* OpenNMS(R) 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 OpenNMS(R). If not, see:
* http://www.gnu.org/licenses/
*
* For more information contact:
* OpenNMS(R) Licensing <license@opennms.org>
* http://www.opennms.org/
* http://www.opennms.com/
*******************************************************************************/
package org.opennms.netmgt.syslogd;
import static org.opennms.core.utils.InetAddressUtils.addr;
import java.io.UnsupportedEncodingException;
import java.lang.reflect.Method;
import java.net.DatagramPacket;
import java.net.InetAddress;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.regex.PatternSyntaxException;
import org.apache.commons.lang.builder.ToStringBuilder;
import org.opennms.core.utils.InetAddressUtils;
import org.opennms.core.utils.LogUtils;
import org.opennms.netmgt.config.SyslogdConfigFactory;
import org.opennms.netmgt.config.syslogd.HideMatch;
import org.opennms.netmgt.config.syslogd.HideMessage;
import org.opennms.netmgt.config.syslogd.HostaddrMatch;
import org.opennms.netmgt.config.syslogd.HostnameMatch;
import org.opennms.netmgt.config.syslogd.ParameterAssignment;
import org.opennms.netmgt.config.syslogd.ProcessMatch;
import org.opennms.netmgt.config.syslogd.UeiList;
import org.opennms.netmgt.config.syslogd.UeiMatch;
import org.opennms.netmgt.model.events.EventBuilder;
import org.opennms.netmgt.xml.event.Event;
/**
* This routine does the majority of Syslogd's work.
* Improvements most likely are to be made.
*
* @author <a href="mailto:joed@opennms.org">Johan Edstrom</a>
* @author <a href="mailto:brozow@opennms.org">Mathew Brozowski</a>
* @author <a href="mailto:dj@opennms.org">DJ Gregor</a>
* @author <a href="mailto:mhuot@opennms.org">Mike Huot</a>
* @author <a href="mailto:jeffg@opennms.org">Jeff Gehlbach</a>
* @author <a href="mailto:weave@oculan.com">Brian Weaver </a>
*/
final class ConvertToEvent {
/** Constant <code>HIDDEN_MESSAGE="The message logged has been removed due"{trunked}</code> */
protected static final String HIDDEN_MESSAGE = "The message logged has been removed due to configuration of Syslogd; it may contain sensitive data.";
/**
* The received XML event, decoded using the US-ASCII encoding.
*/
private final String m_eventXML;
/**
* The Internet address of the sending agent.
*/
private final InetAddress m_sender;
/**
* The port of the agent on the remote system.
*/
private final int m_port;
/**
* The list of event that have been acknowledged.
*/
private final List<Event> m_ackEvents = new ArrayList<Event>();
private Event m_event;
private static Class<? extends SyslogParser> m_parserClass = null;
private static Map<String,Pattern> m_patterns = new ConcurrentHashMap<String,Pattern>();
/**
* Private constructor to prevent the used of <em>new</em> except by the
* <code>make</code> method.
*
* @param eventXml
* @param port
* @param addr
*/
private ConvertToEvent(InetAddress addr, int port, String eventXml) {
m_sender = addr;
m_port = port;
m_eventXML = eventXml;
}
public static void invalidate() {
m_parserClass = null;
m_patterns.clear();
}
/**
* Constructs a new event encapsulation instance based upon the
* information passed to the method. The passed datagram data is decoded
* into a string using the <tt>US-ASCII</tt> character encoding.
*
* @param packet The datagram received from the remote agent.
* @throws java.io.UnsupportedEncodingException
* Thrown if the data buffer cannot be decoded using the
* US-ASCII encoding.
* @throws MessageDiscardedException
*/
static ConvertToEvent make(final DatagramPacket packet, final String matchPattern, final int hostGroup, final int messageGroup, final UeiList ueiList, final HideMessage hideMessage, final String discardUei)
throws UnsupportedEncodingException, MessageDiscardedException {
return make(packet.getAddress(), packet.getPort(), packet.getData(), packet.getLength(), matchPattern, hostGroup, messageGroup, ueiList, hideMessage, discardUei);
}
/**
* Constructs a new event encapsulation instance based upon the
* information passed to the method. The passed byte array is decoded into
* a string using the <tt>US-ASCII</tt> character encoding.
*
* @param addr The remote agent's address.
* @param port The remote agent's port
* @param data The XML data in US-ASCII encoding.
* @param len The length of the XML data in the buffer.
* @throws java.io.UnsupportedEncodingException
* Thrown if the data buffer cannot be decoded using the
* US-ASCII encoding.
* @throws MessageDiscardedException
*/
static ConvertToEvent make(final InetAddress addr, final int port, final byte[] data,
final int len, final String matchPattern, final int hostGroup, final int messageGroup,
final UeiList ueiList, final HideMessage hideMessage, final String discardUei)
throws UnsupportedEncodingException, MessageDiscardedException {
if (m_parserClass == null) {
final String parser = SyslogdConfigFactory.getInstance().getParser();
try {
m_parserClass = Class.forName(parser).asSubclass(SyslogParser.class);
} catch (final Exception ex) {
LogUtils.debugf(ConvertToEvent.class, ex, "Unable to instantiate Syslog parser class specified in config: %s", parser);
m_parserClass = CustomSyslogParser.class;
}
}
String deZeroedData = new String(data, 0, len, "US-ASCII");
if (deZeroedData.endsWith("\0")) {
deZeroedData = deZeroedData.substring(0, deZeroedData.length() - 1);
}
final ConvertToEvent e = new ConvertToEvent(addr, port, deZeroedData);
LogUtils.debugf(ConvertToEvent.class, "Converting to event: %s", e);
final SyslogParser parser;
try {
Method m = m_parserClass.getDeclaredMethod("getParser", String.class);
Object[] args = new Object[] { e.m_eventXML };
parser = (SyslogParser)m.invoke(ConvertToEvent.class, args);
} catch (final Exception ex) {
LogUtils.debugf(ConvertToEvent.class, ex, "Unable to get parser for class '%s'", m_parserClass.getName());
throw new MessageDiscardedException(ex);
}
if (!parser.find()) {
throw new MessageDiscardedException("message does not match");
}
SyslogMessage message;
try {
message = parser.parse();
} catch (final SyslogParserException ex) {
LogUtils.debugf(ConvertToEvent.class, ex, "Unable to parse '%s'", e.m_eventXML);
throw new MessageDiscardedException(ex);
}
LogUtils.debugf(ConvertToEvent.class, "got syslog message %s", message);
if (message == null) {
throw new MessageDiscardedException(String.format("Unable to parse '%s'", e.m_eventXML));
}
// Build a basic event out of the syslog message
final String priorityTxt = message.getSeverity().toString();
final String facilityTxt = message.getFacility().toString();
EventBuilder bldr = new EventBuilder("uei.opennms.org/syslogd/" + facilityTxt + "/" + priorityTxt, "syslogd");
bldr.setCreationTime(message.getDate());
// Set event host
bldr.setHost(InetAddressUtils.getLocalHostName());
final String hostAddress = message.getHostAddress();
if (hostAddress != null && hostAddress.length() > 0) {
// Set nodeId
long nodeId = SyslogdIPMgr.getNodeId(hostAddress);
if (nodeId != -1) {
bldr.setNodeid(nodeId);
}
bldr.setInterface(addr(hostAddress));
}
bldr.setLogDest("logndisplay");
// We will also here find out if, the host needs to
// be replaced, the message matched to a UEI, and
// last if we need to actually hide the message.
// this being potentially helpful in avoiding showing
// operator a password or other data that should be
// confidential.
/*
* We matched on a regexp for host/message pair.
* This can be a forwarded message as in BSD Style
* or syslog-ng.
* We assume that the host is given to us
* as an IP/Hostname and that the resolver
* on the ONMS host actually can resolve the
* node to match against nodeId.
*/
Pattern msgPat = null;
Matcher msgMat = null;
// Time to verify UEI matching.
final String fullText = message.getFullText();
final String matchedText = message.getMatchedMessage();
final List<UeiMatch> ueiMatch = ueiList == null? null : ueiList.getUeiMatchCollection();
if (ueiMatch == null) {
LogUtils.warnf(ConvertToEvent.class, "No ueiList configured.");
} else {
for (final UeiMatch uei : ueiMatch) {
final boolean otherStuffMatches = matchFacility(uei.getFacilityCollection(), facilityTxt) &&
matchSeverity(uei.getSeverityCollection(), priorityTxt) &&
matchProcess(uei.getProcessMatch(), message.getProcessName()) &&
matchHostname(uei.getHostnameMatch(), message.getHostName()) &&
matchHostAddr(uei.getHostaddrMatch(), message.getHostAddress());
if (otherStuffMatches && uei.getMatch().getType().equals("substr")) {
if (matchSubstring(discardUei, bldr, matchedText, uei)) {
break;
}
} else if (otherStuffMatches && (uei.getMatch().getType().startsWith("regex"))) {
if (matchRegex(message, uei, bldr, discardUei)) {
break;
}
}
}
}
// Time to verify if we need to hide the message
boolean doHide = false;
final List<HideMatch> hideMatch = hideMessage == null? null : hideMessage.getHideMatchCollection();
if (hideMatch == null) {
LogUtils.warnf(ConvertToEvent.class, "No hideMessage configured.");
} else {
for (final HideMatch hide : hideMatch) {
if (hide.getMatch().getType().equals("substr")) {
if (fullText.contains(hide.getMatch().getExpression())) {
// We should hide the message based on this match
doHide = true;
}
} else if (hide.getMatch().getType().equals("regex")) {
try {
msgPat = Pattern.compile(hide.getMatch().getExpression(), Pattern.MULTILINE);
msgMat = msgPat.matcher(fullText);
} catch (PatternSyntaxException pse) {
LogUtils.warnf(ConvertToEvent.class, pse, "Failed to compile regex pattern '%s'", hide.getMatch().getExpression());
msgMat = null;
}
if ((msgMat != null) && (msgMat.find())) {
// We should hide the message based on this match
doHide = true;
}
}
if (doHide) {
LogUtils.debugf(ConvertToEvent.class, "Hiding syslog message from Event - May contain sensitive data");
message.setMessage(HIDDEN_MESSAGE);
// We want to stop here, no point in checking further hideMatches
break;
}
}
}
// Using parms provides configurability.
bldr.setLogMessage(message.getMessage());
bldr.addParam("syslogmessage", message.getMessage());
bldr.addParam("severity", "" + priorityTxt);
bldr.addParam("timestamp", message.getSyslogFormattedDate());
if (message.getProcessName() != null) {
bldr.addParam("process", message.getProcessName());
}
bldr.addParam("service", "" + facilityTxt);
if (message.getProcessId() != null) {
bldr.addParam("processid", message.getProcessId().toString());
}
e.m_event = bldr.getEvent();
return e;
}
private static boolean matchFind(final String expression, final String input, final String context) {
final Pattern pat = getPattern(expression);
if (pat == null) {
LogUtils.debugf(ConvertToEvent.class, "Unable to get pattern for expression '%s' in %s context", expression, context);
return false;
}
final Matcher mat = pat.matcher(input);
if (mat != null && mat.find()) return true;
return false;
}
private static boolean matchHostAddr(final HostaddrMatch hostaddrMatch, final String hostAddress) {
if (hostaddrMatch == null) return true;
if (hostAddress == null) return false;
final String expression = hostaddrMatch.getExpression();
if (matchFind(expression, hostAddress, "hostaddr-match")) {
LogUtils.tracef(ConvertToEvent.class, "Successful regex hostaddr-match for input '%s' against expression '%s'", hostAddress, expression);
return true;
}
return false;
}
private static boolean matchHostname(final HostnameMatch hostnameMatch, final String hostName) {
if (hostnameMatch == null) return true;
if (hostName == null) return false;
final String expression = hostnameMatch.getExpression();
if (matchFind(expression, hostName, "hostname-match")) {
LogUtils.tracef(ConvertToEvent.class, "Successful regex hostname-match for input '%s' against expression '%s'", hostName, expression);
return true;
}
return false;
}
private static boolean matchProcess(final ProcessMatch processMatch, final String processName) {
if (processMatch == null) return true;
if (processName == null) return false;
final String expression = processMatch.getExpression();
if (matchFind(expression, processName, "process-match")) {
LogUtils.tracef("Successful regex process-match for input '%s' against expression '%s'", processName, expression);
return true;
}
return false;
}
private static boolean matchSeverity(List<String> severities, String priorityTxt) {
if (severities.size() == 0) return true;
for (String severity : severities) {
if (severity.toLowerCase().equals(priorityTxt.toLowerCase())) return true;
}
return false;
}
private static boolean matchFacility(List<String> facilities, String facilityTxt) {
if (facilities.size() == 0) return true;
for (String facility : facilities) {
if (facility.toLowerCase().equals(facilityTxt.toLowerCase())) return true;
}
return false;
}
private static Pattern getPattern(final String expression) {
final Pattern msgPat = m_patterns.get(expression);
if (msgPat == null) {
try {
final Pattern newPat = Pattern.compile(expression, Pattern.MULTILINE);
m_patterns.put(expression, newPat);
return newPat;
} catch(final PatternSyntaxException pse) {
LogUtils.warnf(ConvertToEvent.class, pse, "Failed to compile regex pattern '%s'", expression);
}
}
return msgPat;
}
private static boolean matchSubstring(final String discardUei, final EventBuilder bldr, String message, final UeiMatch uei) throws MessageDiscardedException {
boolean doIMatch = false;
boolean traceEnabled = LogUtils.isTraceEnabled(ConvertToEvent.class);
if (message.contains(uei.getMatch().getExpression())) {
if (discardUei.equals(uei.getUei())) {
if (traceEnabled) LogUtils.tracef(ConvertToEvent.class, "Specified UEI '%s' is same as discard-uei, discarding this message.", uei.getUei());
throw new MessageDiscardedException();
} else {
//We can pass a new UEI on this
if (traceEnabled) LogUtils.tracef(ConvertToEvent.class, "Changed the UEI of a Syslogd event, based on substring match, to : %s", uei.getUei());
bldr.setUei(uei.getUei());
// I think we want to stop processing here so the first
// ueiMatch wins, right?
doIMatch = true;
}
} else {
if (traceEnabled) LogUtils.tracef(ConvertToEvent.class, "No substring match for text of a Syslogd event to : %s", uei.getMatch().getExpression());
}
return doIMatch;
}
private static boolean matchRegex(final SyslogMessage message, final UeiMatch uei, final EventBuilder bldr, final String discardUei) throws MessageDiscardedException {
boolean traceEnabled = LogUtils.isTraceEnabled(ConvertToEvent.class);
final String expression = uei.getMatch().getExpression();
final Pattern msgPat = getPattern(expression);
final Matcher msgMat;
if (msgPat == null) {
LogUtils.debugf(ConvertToEvent.class, "Unable to create pattern for expression '%s'", expression);
return false;
} else {
final String text;
if (message.getMatchedMessage() != null) {
text = message.getMatchedMessage();
} else {
text = message.getFullText();
}
msgMat = msgPat.matcher(text);
}
if ((msgMat != null) && (msgMat.find())) {
if (discardUei.equals(uei.getUei())) {
LogUtils.debugf(ConvertToEvent.class, "Specified UEI '%s' is same as discard-uei, discarding this message.", uei.getUei());
throw new MessageDiscardedException();
}
// We matched a UEI
bldr.setUei(uei.getUei());
if (msgMat.groupCount() > 0 && uei.getMatch().isDefaultParameterMapping()) {
if (traceEnabled) LogUtils.tracef(ConvertToEvent.class, "Doing default parameter mappings for this regex match.");
for (int groupNum = 1; groupNum <= msgMat.groupCount(); groupNum++) {
if (traceEnabled) LogUtils.tracef(ConvertToEvent.class, "Added parm 'group%d' with value '%s' to Syslogd event based on regex match group", groupNum, msgMat.group(groupNum));
bldr.addParam("group"+groupNum, msgMat.group(groupNum));
}
}
if (msgMat.groupCount() > 0 && uei.getParameterAssignmentCount() > 0) {
if (traceEnabled) LogUtils.tracef(ConvertToEvent.class, "Doing user-specified parameter assignments for this regex match.");
for (ParameterAssignment assignment : uei.getParameterAssignmentCollection()) {
String parmName = assignment.getParameterName();
String parmValue = msgMat.group(assignment.getMatchingGroup());
parmValue = parmValue == null ? "" : parmValue;
bldr.addParam(parmName, parmValue);
if (traceEnabled) LogUtils.tracef(ConvertToEvent.class, "Added parm '%s' with value '%s' to Syslogd event based on user-specified parameter assignment", parmName, parmValue);
}
}
// I think we want to stop processing here so the first
// ueiMatch wins, right?
return true;
}
if (traceEnabled) LogUtils.tracef(ConvertToEvent.class, "Message '%s' did not regex-match pattern '%s'", message.getMessage(), expression);
return false;
}
/**
* Adds the event to the list of events acknowledged in this event XML
* document.
*
* @param e The event to acknowledge.
*/
void ackEvent(final Event e) {
if (!m_ackEvents.contains(e))
m_ackEvents.add(e);
}
/**
* Returns the raw XML data as a string.
*/
String getXmlData() {
return m_eventXML;
}
/**
* Returns the sender's address.
*/
InetAddress getSender() {
return m_sender;
}
/**
* Returns the sender's port
*/
int getPort() {
return m_port;
}
/**
* Get the acknowledged events
*
* @return a {@link java.util.List} object.
*/
public List<Event> getAckedEvents() {
return m_ackEvents;
}
/**
* <p>getEvent</p>
*
* @return a {@link org.opennms.netmgt.xml.event.Event} object.
*/
public Event getEvent() {
return m_event;
}
/**
* {@inheritDoc}
*
* Returns true if the instance matches the object based upon the remote
* agent's address & port. If the passed instance is from the same
* agent then it is considered equal.
*/
public boolean equals(final Object o) {
if (o != null && o instanceof ConvertToEvent) {
final ConvertToEvent e = (ConvertToEvent) o;
return (this == e || (m_port == e.m_port && m_sender.equals(e.m_sender)));
}
return false;
}
/**
* Returns the hash code of the instance. The hash code is computed by
* taking the bitwise XOR of the port and the agent's Internet address
* hash code.
*
* @return The 32-bit has code for the instance.
*/
public int hashCode() {
return (m_port ^ m_sender.hashCode());
}
/**
* <p>toString</p>
*
* @return a {@link java.lang.String} object.
*/
public String toString() {
return new ToStringBuilder(this)
.append("Sender", m_sender)
.append("Port", m_port)
.append("Acknowledged Events", m_ackEvents)
.append("Event", m_event)
.toString();
}
}