/* * Copyright (C) 2004-2008 Jive Software. All rights reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.jivesoftware.openfire.plugin; import java.io.File; import java.util.regex.PatternSyntaxException; import org.jivesoftware.openfire.MessageRouter; import org.jivesoftware.openfire.XMPPServer; import org.jivesoftware.openfire.container.Plugin; import org.jivesoftware.openfire.container.PluginManager; import org.jivesoftware.openfire.interceptor.InterceptorManager; import org.jivesoftware.openfire.interceptor.PacketInterceptor; import org.jivesoftware.openfire.interceptor.PacketRejectedException; import org.jivesoftware.openfire.session.Session; import org.jivesoftware.openfire.user.User; import org.jivesoftware.openfire.user.UserManager; import org.jivesoftware.util.EmailService; import org.jivesoftware.util.JiveGlobals; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.xmpp.packet.JID; import org.xmpp.packet.Message; import org.xmpp.packet.Packet; import org.xmpp.packet.Presence; /** * Content filter plugin. * * @author Conor Hayes */ public class ContentFilterPlugin implements Plugin, PacketInterceptor { private static final Logger Log = LoggerFactory.getLogger(ContentFilterPlugin.class); /** * The expected value is a boolean, if true the user identified by the value * of the property #VIOLATION_NOTIFICATION_CONTACT_PROPERTY will be notified * every time there is a content match, otherwise no notification will be * sent. Then default value is false. */ public static final String VIOLATION_NOTIFICATION_ENABLED_PROPERTY = "plugin.contentFilter.violation.notification.enabled"; /** * The expected value is a user name. The default value is "admin". */ public static final String VIOLATION_NOTIFICATION_CONTACT_PROPERTY = "plugin.contentFilter.violation.notification.contact"; /** * The expected value is a boolean, if true the user identified by the value * of the property #VIOLATION_NOTIFICATION_CONTACT_PROPERTY, will also * receive a copy of the offending packet. The default value is false. */ public static final String VIOLATION_INCLUDE_ORIGNAL_PACKET_ENABLED_PROPERTY = "plugin.contentFilter.violation.notification.include.original.enabled"; /** * The expected value is a boolean, if true the user identified by the value * of the property #VIOLATION_NOTIFICATION_CONTACT_PROPERTY, will receive * notification by IM. The default value is true. */ public static final String VIOLATION_NOTIFICATION_BY_IM_ENABLED_PROPERTY = "plugin.contentFilter.violation.notification.by.im.enabled"; /** * The expected value is a boolean, if true the user identified by the value * of the property #VIOLATION_NOTIFICATION_CONTACT_PROPERTY, will receive * notification by email. The default value is false. */ public static final String VIOLATION_NOTIFICATION_BY_EMAIL_ENABLED_PROPERTY = "plugin.contentFilter.violation.notification.by.email.enabled"; /** * The expected value is a boolean, if true the sender will be notified when * a message is rejected, otherwise the message will be silently * rejected,i.e. the sender will not know that the message was rejected and * the receiver will not get the message. The default value is false. */ public static final String REJECTION_NOTIFICATION_ENABLED_PROPERTY = "plugin.contentFilter.rejection.notification.enabled"; /** * The expected value is a string, containing the desired message for the * sender notification. */ public static final String REJECTION_MSG_PROPERTY = "plugin.contentFilter.rejection.msg"; /** * The expected value is a boolean, if true the value of #PATTERNS_PROPERTY * will be used for pattern matching. */ public static final String PATTERNS_ENABLED_PROPERTY = "plugin.contentFilter.patterns.enabled"; /** * The expected value is a comma separated string of regular expressions. */ public static final String PATTERNS_PROPERTY = "plugin.contentFilter.patterns"; /** * The expected value is a boolean, if true Presence packets will be * filtered */ public static final String FILTER_STATUS_ENABLED_PROPERTY = "plugin.contentFilter.filter.status.enabled"; /** * The expected value is a boolean, if true the value of #MASK_PROPERTY will * be used to mask matching content. */ public static final String MASK_ENABLED_PROPERTY = "plugin.contentFilter.mask.enabled"; /** * The expected value is a string. If this property is set any matching * content will not be rejected but masked with the given value. Setting a * content mask means that property #SENDER_NOTIFICATION_ENABLED_PROPERTY is * ignored. The default value is "**". */ public static final String MASK_PROPERTY = "plugin.contentFilter.mask"; /** * The expected value is a boolean, if false packets whose contents matches one * of the supplied regular expressions will be rejected, otherwise the packet will * be accepted and may be optionally masked. The default value is false. * @see #MASK_ENABLED_PROPERTY */ public static final String ALLOW_ON_MATCH_PROPERTY = "plugin.contentFilter.allow.on.match"; /** * the hook into the inteceptor chain */ private InterceptorManager interceptorManager; /** * used to send violation notifications */ private MessageRouter messageRouter; /** * delegate that does the real work of this plugin */ private ContentFilter contentFilter; /** * flags if sender should be notified of rejections */ private boolean rejectionNotificationEnabled; /** * the rejection msg to send */ private String rejectionMessage; /** * flags if content matches should result in admin notification */ private boolean violationNotificationEnabled; /** * the admin user to send violation notifications to */ private String violationContact; /** * flags if original packet should be included in the message to the * violation contact. */ private boolean violationIncludeOriginalPacketEnabled; /** * flags if violation contact should be notified by IM. */ private boolean violationNotificationByIMEnabled; /** * flags if violation contact should be notified by email. */ private boolean violationNotificationByEmailEnabled; /** * flag if patterns should be used */ private boolean patternsEnabled; /** * the patterns to use */ private String patterns; /** * flag if Presence packets should be filtered. */ private boolean filterStatusEnabled; /** * flag if mask should be used */ private boolean maskEnabled; /** * the mask to use */ private String mask; /** * flag if matching content should be accepted or rejected. */ private boolean allowOnMatch; /** * violation notification messages will be from this JID */ private JID violationNotificationFrom; public ContentFilterPlugin() { contentFilter = new ContentFilter(); interceptorManager = InterceptorManager.getInstance(); violationNotificationFrom = new JID(XMPPServer.getInstance() .getServerInfo().getXMPPDomain()); messageRouter = XMPPServer.getInstance().getMessageRouter(); } /** * Restores the plugin defaults. */ public void reset() { setViolationNotificationEnabled(false); setViolationContact("admin"); setViolationNotificationByIMEnabled(true); setViolationNotificationByEmailEnabled(false); setViolationIncludeOriginalPacketEnabled(false); setRejectionNotificationEnabled(false); setRejectionMessage("Message rejected. This is an automated server response"); setPatternsEnabled(false); setPatterns("fox,dog"); setFilterStatusEnabled(false); setMaskEnabled(false); setMask("***"); setAllowOnMatch(false); } public boolean isAllowOnMatch() { return allowOnMatch; } public void setAllowOnMatch(boolean allow) { allowOnMatch = allow; JiveGlobals.setProperty(ALLOW_ON_MATCH_PROPERTY, allow ? "true" : "false"); changeContentFilterMask(); } public boolean isMaskEnabled() { return maskEnabled; } public void setMaskEnabled(boolean enabled) { maskEnabled = enabled; JiveGlobals.setProperty(MASK_ENABLED_PROPERTY, enabled ? "true" : "false"); changeContentFilterMask(); } public void setMask(String mas) { mask = mas; JiveGlobals.setProperty(MASK_PROPERTY, mas); changeContentFilterMask(); } private void changeContentFilterMask() { if (allowOnMatch && maskEnabled) { contentFilter.setMask(mask); } else { contentFilter.clearMask(); } } public String getMask() { return mask; } public boolean isPatternsEnabled() { return patternsEnabled; } public void setPatternsEnabled(boolean enabled) { patternsEnabled = enabled; JiveGlobals.setProperty(PATTERNS_ENABLED_PROPERTY, enabled ? "true" : "false"); changeContentFilterPatterns(); } public void setPatterns(String patt) { patterns = patt; JiveGlobals.setProperty(PATTERNS_PROPERTY, patt); changeContentFilterPatterns(); } public boolean isFilterStatusEnabled() { return filterStatusEnabled; } public void setFilterStatusEnabled(boolean enabled) { filterStatusEnabled = enabled; JiveGlobals.setProperty(FILTER_STATUS_ENABLED_PROPERTY, enabled ? "true" : "false"); } private void changeContentFilterPatterns() { if (patternsEnabled) { contentFilter.setPatterns(patterns); } else { contentFilter.clearPatterns(); } } public String getPatterns() { return patterns; } public boolean isRejectionNotificationEnabled() { return rejectionNotificationEnabled; } public void setRejectionNotificationEnabled(boolean enabled) { rejectionNotificationEnabled = enabled; JiveGlobals.setProperty(REJECTION_NOTIFICATION_ENABLED_PROPERTY, enabled ? "true" : "false"); } public String getRejectionMessage() { return rejectionMessage; } public void setRejectionMessage(String message) { this.rejectionMessage = message; JiveGlobals.setProperty(REJECTION_MSG_PROPERTY, message); } public boolean isViolationNotificationEnabled() { return violationNotificationEnabled; } public void setViolationNotificationEnabled(boolean enabled) { violationNotificationEnabled = enabled; JiveGlobals.setProperty(VIOLATION_NOTIFICATION_ENABLED_PROPERTY, enabled ? "true" : "false"); } public void setViolationContact(String contact) { violationContact = contact; JiveGlobals.setProperty(VIOLATION_NOTIFICATION_CONTACT_PROPERTY, contact); } public String getViolationContact() { return violationContact; } public boolean isViolationIncludeOriginalPacketEnabled() { return violationIncludeOriginalPacketEnabled; } public void setViolationIncludeOriginalPacketEnabled(boolean enabled) { violationIncludeOriginalPacketEnabled = enabled; JiveGlobals.setProperty( VIOLATION_INCLUDE_ORIGNAL_PACKET_ENABLED_PROPERTY, enabled ? "true" : "false"); } public boolean isViolationNotificationByIMEnabled() { return violationNotificationByIMEnabled; } public void setViolationNotificationByIMEnabled(boolean enabled) { violationNotificationByIMEnabled = enabled; JiveGlobals.setProperty(VIOLATION_NOTIFICATION_BY_IM_ENABLED_PROPERTY, enabled ? "true" : "false"); } public boolean isViolationNotificationByEmailEnabled() { return violationNotificationByEmailEnabled; } public void setViolationNotificationByEmailEnabled(boolean enabled) { violationNotificationByEmailEnabled = enabled; JiveGlobals.setProperty( VIOLATION_NOTIFICATION_BY_EMAIL_ENABLED_PROPERTY, enabled ? "true" : "false"); } public void initializePlugin(PluginManager pManager, File pluginDirectory) { // configure this plugin initFilter(); // register with interceptor manager interceptorManager.addInterceptor(this); } private void initFilter() { // default to false violationNotificationEnabled = JiveGlobals.getBooleanProperty( VIOLATION_NOTIFICATION_ENABLED_PROPERTY, false); // default to "admin" violationContact = JiveGlobals.getProperty( VIOLATION_NOTIFICATION_CONTACT_PROPERTY, "admin"); // default to true violationNotificationByIMEnabled = JiveGlobals.getBooleanProperty( VIOLATION_NOTIFICATION_BY_IM_ENABLED_PROPERTY, true); // default to false violationNotificationByEmailEnabled = JiveGlobals.getBooleanProperty( VIOLATION_NOTIFICATION_BY_EMAIL_ENABLED_PROPERTY, false); // default to false violationIncludeOriginalPacketEnabled = JiveGlobals.getBooleanProperty( VIOLATION_INCLUDE_ORIGNAL_PACKET_ENABLED_PROPERTY, false); // default to false rejectionNotificationEnabled = JiveGlobals.getBooleanProperty( REJECTION_NOTIFICATION_ENABLED_PROPERTY, false); // default to english rejectionMessage = JiveGlobals.getProperty(REJECTION_MSG_PROPERTY, "Message rejected. This is an automated server response"); // default to false patternsEnabled = JiveGlobals.getBooleanProperty( PATTERNS_ENABLED_PROPERTY, false); // default to "fox,dog" patterns = JiveGlobals.getProperty(PATTERNS_PROPERTY, "fox,dog"); try { changeContentFilterPatterns(); } catch (PatternSyntaxException e) { Log.warn("Resetting to default patterns of ContentFilterPlugin", e); // Existing patterns are invalid so reset to default ones setPatterns("fox,dog"); } // default to false filterStatusEnabled = JiveGlobals.getBooleanProperty( FILTER_STATUS_ENABLED_PROPERTY, false); // default to false maskEnabled = JiveGlobals.getBooleanProperty(MASK_ENABLED_PROPERTY, false); // default to "***" mask = JiveGlobals.getProperty(MASK_PROPERTY, "***"); // default to false allowOnMatch = JiveGlobals.getBooleanProperty( ALLOW_ON_MATCH_PROPERTY, false); //v1.2.2 backwards compatibility if (maskEnabled) { allowOnMatch = true; } changeContentFilterMask(); } /** * @see org.jivesoftware.openfire.container.Plugin#destroyPlugin() */ public void destroyPlugin() { // unregister with interceptor manager interceptorManager.removeInterceptor(this); } public void interceptPacket(Packet packet, Session session, boolean read, boolean processed) throws PacketRejectedException { if (isValidTargetPacket(packet, read, processed)) { Packet original = packet; if (Log.isDebugEnabled()) { Log.debug("Content filter: intercepted packet:" + original.toString()); } // make a copy of the original packet only if required, // as it's an expensive operation if (violationNotificationEnabled && violationIncludeOriginalPacketEnabled && maskEnabled) { original = packet.createCopy(); } // filter the packet boolean contentMatched = contentFilter.filter(packet); if (Log.isDebugEnabled()) { Log.debug("Content filter: content matched? " + contentMatched); } // notify admin of violations if (contentMatched && violationNotificationEnabled) { if (Log.isDebugEnabled()) { Log.debug("Content filter: sending violation notification"); Log.debug("Content filter: include original msg? " + this.violationIncludeOriginalPacketEnabled); } sendViolationNotification(original); } // msg will either be rejected silently, rejected with // some notification to sender, or allowed and optionally masked. // allowing a message without masking can be useful if the admin // simply wants to get notified of matches without interrupting // the conversation in the (spy mode!) if (contentMatched) { if (allowOnMatch) { if (Log.isDebugEnabled()) { Log.debug("Content filter: allowed content:" + packet.toString()); } // no further action required } else { // msg must be rejected if (Log.isDebugEnabled()) { Log.debug("Content filter: rejecting packet"); } PacketRejectedException rejected = new PacketRejectedException( "Packet rejected with disallowed content!"); if (rejectionNotificationEnabled) { // let the sender know about the rejection, this is // only possible/useful if the content is not masked rejected.setRejectionMessage(rejectionMessage); } throw rejected; } } } } private boolean isValidTargetPacket(Packet packet, boolean read, boolean processed) { return patternsEnabled && !processed && read && (packet instanceof Message || (filterStatusEnabled && packet instanceof Presence)); } private void sendViolationNotification(Packet originalPacket) { String subject = "Content filter notification! (" + originalPacket.getFrom().getNode() + ")"; String body; if (originalPacket instanceof Message) { Message originalMsg = (Message) originalPacket; body = "Disallowed content detected in message from:" + originalMsg.getFrom() + " to:" + originalMsg.getTo() + ", message was " + (allowOnMatch ? "allowed" + (contentFilter.isMaskingContent() ? " and masked." : " but not masked.") : "rejected.") + (violationIncludeOriginalPacketEnabled ? "\nOriginal subject:" + (originalMsg.getSubject() != null ? originalMsg .getSubject() : "") + "\nOriginal content:" + (originalMsg.getBody() != null ? originalMsg .getBody() : "") : ""); } else { // presence Presence originalPresence = (Presence) originalPacket; body = "Disallowed status detected in presence from:" + originalPresence.getFrom() + ", status was " + (allowOnMatch ? "allowed" + (contentFilter.isMaskingContent() ? " and masked." : " but not masked.") : "rejected.") + (violationIncludeOriginalPacketEnabled ? "\nOriginal status:" + originalPresence.getStatus() : ""); } if (violationNotificationByIMEnabled) { if (Log.isDebugEnabled()) { Log.debug("Content filter: sending IM notification"); } sendViolationNotificationIM(subject, body); } if (violationNotificationByEmailEnabled) { if (Log.isDebugEnabled()) { Log.debug("Content filter: sending email notification"); } sendViolationNotificationEmail(subject, body); } } private void sendViolationNotificationIM(String subject, String body) { Message message = createServerMessage(subject, body); // TODO consider spining off a separate thread here, // in high volume situations, it will result in // in faster response and notification is not required // to be real time. messageRouter.route(message); } private Message createServerMessage(String subject, String body) { Message message = new Message(); message.setTo(violationContact + "@" + violationNotificationFrom.getDomain()); message.setFrom(violationNotificationFrom); message.setSubject(subject); message.setBody(body); return message; } private void sendViolationNotificationEmail(String subject, String body) { try { User user = UserManager.getInstance().getUser(violationContact); //this is automatically put on a another thread for execution. EmailService.getInstance().sendMessage(user.getName(), user.getEmail(), "Openfire", "no_reply@" + violationNotificationFrom.getDomain(), subject, body, null); } catch (Throwable e) { // catch throwable in case email setup is invalid Log.error("Content Filter: Failed to send email, please review Openfire setup", e); } } }