1 /*
2  * Copyright (C) 2005-2008 Jive Software. All rights reserved.
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *     http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package org.jivesoftware.openfire;
18 
19 import java.util.Collections;
20 import java.util.Iterator;
21 import java.util.List;
22 import java.util.concurrent.CopyOnWriteArrayList;
23 
24 import org.jivesoftware.openfire.container.BasicModule;
25 import org.jivesoftware.openfire.disco.ServerFeaturesProvider;
26 import org.jivesoftware.openfire.privacy.PrivacyList;
27 import org.jivesoftware.openfire.privacy.PrivacyListManager;
28 import org.jivesoftware.openfire.user.UserManager;
29 import org.jivesoftware.util.JiveGlobals;
30 import org.slf4j.Logger;
31 import org.slf4j.LoggerFactory;
32 import org.xmpp.packet.JID;
33 import org.xmpp.packet.Message;
34 import org.xmpp.packet.PacketError;
35 
36 /**
37  * Controls what is done with offline messages.
38  *
39  * @author Iain Shigeoka
40  */
41 public class OfflineMessageStrategy extends BasicModule implements ServerFeaturesProvider {
42 
43     private static final Logger Log = LoggerFactory.getLogger(OfflineMessageStrategy.class);
44 
45     private static int quota = 100*1024; // Default to 100 K.
46     private static Type type = Type.store_and_bounce;
47 
48     private static List<OfflineMessageListener> listeners = new CopyOnWriteArrayList<>();
49 
50     private OfflineMessageStore messageStore;
51     private JID serverAddress;
52     private PacketRouter router;
53 
OfflineMessageStrategy()54     public OfflineMessageStrategy() {
55         super("Offline Message Strategy");
56     }
57 
getQuota()58     public int getQuota() {
59         return quota;
60     }
61 
setQuota(int quota)62     public void setQuota(int quota) {
63         OfflineMessageStrategy.quota = quota;
64         JiveGlobals.setProperty("xmpp.offline.quota", Integer.toString(quota));
65     }
66 
getType()67     public OfflineMessageStrategy.Type getType() {
68         return type;
69     }
70 
setType(OfflineMessageStrategy.Type type)71     public void setType(OfflineMessageStrategy.Type type) {
72         if (type == null) {
73             throw new IllegalArgumentException();
74         }
75         OfflineMessageStrategy.type = type;
76         JiveGlobals.setProperty("xmpp.offline.type", type.toString());
77     }
78 
storeOffline(Message message)79     public void storeOffline(Message message) {
80         if (message != null) {
81             // Do nothing if the message was sent to the server itself, an anonymous user or a non-existent user
82             // Also ignore message carbons
83             JID recipientJID = message.getTo();
84             if (recipientJID == null || serverAddress.equals(recipientJID) ||
85                     recipientJID.getNode() == null ||
86                     message.getExtension("received", "urn:xmpp:carbons:2") != null ||
87                     !UserManager.getInstance().isRegisteredUser(recipientJID, false)) {
88                 return;
89             }
90 
91             // Do not store messages if communication is blocked
92             PrivacyList list =
93                     PrivacyListManager.getInstance().getDefaultPrivacyList(recipientJID.getNode());
94             if (list != null && list.shouldBlockPacket(message)) {
95                 if (message.getType() == Message.Type.error){
96                     Log.debug("Avoid generating an error in response to a stanza that itself is an error (to avoid the chance of entering an endless back-and-forth of exchanging errors). Suppress sending an {} error in response to: {}", PacketError.Condition.service_unavailable, message);
97                     return;
98                 }
99                 Message result = message.createCopy();
100                 result.setTo(message.getFrom());
101                 result.setFrom(message.getTo());
102                 result.setError(PacketError.Condition.service_unavailable);
103 
104                 XMPPServer.getInstance().getPacketRouter().route(result);
105                 return;
106             }
107 
108             // 8.5.2.  localpart@domainpart
109             // 8.5.2.2.  No Available or Connected Resources
110             if (recipientJID.getResource() == null) {
111                 if (message.getType() == Message.Type.headline || message.getType() == Message.Type.error) {
112                     // For a message stanza of type "headline" or "error", the server MUST silently ignore the message.
113                     return;
114                 }
115                 // // For a message stanza of type "groupchat", the server MUST return an error to the sender, which SHOULD be <service-unavailable/>.
116                 else if (message.getType() == Message.Type.groupchat) {
117                     bounce(message);
118                     return;
119                 }
120             } else {
121                 // 8.5.3.  localpart@domainpart/resourcepart
122                 // 8.5.3.2.1.  Message
123 
124                 // For a message stanza of type "normal", "groupchat", or "headline", the server MUST either (a) silently ignore the stanza
125                 // or (b) return an error stanza to the sender, which SHOULD be <service-unavailable/>.
126                 if (message.getType() == Message.Type.normal || message.getType() == Message.Type.groupchat || message.getType() == Message.Type.headline) {
127                     // Depending on the OfflineMessageStragey, we may silently ignore or bounce
128                     if (type == Type.bounce) {
129                         bounce(message);
130                     }
131                     // Either bounce or silently ignore, never store such messages
132                     return;
133                 }
134                 // For a message stanza of type "error", the server MUST silently ignore the stanza.
135                 else if (message.getType() == Message.Type.error) {
136                     return;
137                 }
138             }
139 
140             switch (type) {
141             case bounce:
142                 bounce(message);
143                 break;
144             case store:
145                 store(message);
146                 break;
147             case store_and_bounce:
148                 if (underQuota(message)) {
149                     store(message);
150                 }
151                 else {
152                     Log.debug( "Unable to store, as user is over storage quota. Bouncing message instead: " + message.toXML() );
153                     bounce(message);
154                 }
155                 break;
156             case store_and_drop:
157                 if (underQuota(message)) {
158                     store(message);
159                 } else {
160                     Log.debug( "Unable to store, as user is over storage quota. Silently dropping message: " + message.toXML() );
161                 }
162                 break;
163             case drop:
164                 // Drop essentially means silently ignore/do nothing
165                 break;
166             }
167         }
168     }
169 
170     /**
171      * Registers a listener to receive events.
172      *
173      * @param listener the listener.
174      */
addListener(OfflineMessageListener listener)175     public static void addListener(OfflineMessageListener listener) {
176         if (listener == null) {
177             throw new NullPointerException();
178         }
179         listeners.add(listener);
180     }
181 
182     /**
183      * Unregisters a listener to receive events.
184      *
185      * @param listener the listener.
186      */
removeListener(OfflineMessageListener listener)187     public static void removeListener(OfflineMessageListener listener) {
188         listeners.remove(listener);
189     }
190 
underQuota(Message message)191     private boolean underQuota(Message message) {
192         return quota > messageStore.getSize(message.getTo().getNode()) + message.toXML().length();
193     }
194 
store(Message message)195     private void store(Message message) {
196         final OfflineMessage offlineMessage = messageStore.addMessage(message);
197         // Inform listeners that an offline message was stored
198         if (offlineMessage != null && !listeners.isEmpty()) {
199             for (OfflineMessageListener listener : listeners) {
200                 try {
201                     listener.messageStored(offlineMessage);
202                 } catch (Exception e) {
203                     Log.warn("An exception occurred while dispatching a 'messageStored' event!", e);
204                 }
205             }
206         }
207     }
208 
bounce(Message message)209     private void bounce(Message message) {
210         // Do nothing if the sender was the server itself
211         if (message.getFrom() == null || message.getFrom().equals( serverAddress )) {
212             return;
213         }
214 
215         try {
216             if (message.getType() == Message.Type.error){
217                 Log.debug("Avoid generating an error in response to a stanza that itself is an error (to avoid the chance of entering an endless back-and-forth of exchanging errors). Suppress sending an {} error in response to: {}", PacketError.Condition.service_unavailable, message);
218             } else {
219                 // Generate a rejection response to the sender
220                 Message errorResponse = message.createCopy();
221                 // return an error stanza to the sender, which SHOULD be <service-unavailable/>
222                 errorResponse.setError(PacketError.Condition.service_unavailable);
223                 errorResponse.setFrom(message.getTo());
224                 errorResponse.setTo(message.getFrom());
225                 // Send the response
226                 router.route(errorResponse);
227                 // Inform listeners that an offline message was bounced
228             }
229             if (!listeners.isEmpty()) {
230                 for (OfflineMessageListener listener : listeners) {
231                     try {
232                         listener.messageBounced(message);
233                     } catch (Exception e) {
234                         Log.warn("An exception occurred while dispatching a 'messageBounced' event!", e);
235                     }
236                 }
237             }
238         }
239         catch (Exception e) {
240             Log.error(e.getMessage(), e);
241         }
242     }
243 
244     @Override
initialize(XMPPServer server)245     public void initialize(XMPPServer server) {
246         super.initialize(server);
247         messageStore = server.getOfflineMessageStore();
248         router = server.getPacketRouter();
249         serverAddress = new JID(server.getServerInfo().getXMPPDomain());
250 
251         JiveGlobals.migrateProperty("xmpp.offline.quota");
252         JiveGlobals.migrateProperty("xmpp.offline.type");
253 
254         String quota = JiveGlobals.getProperty("xmpp.offline.quota");
255         if (quota != null && quota.length() > 0) {
256             OfflineMessageStrategy.quota = Integer.parseInt(quota);
257         }
258         String type = JiveGlobals.getProperty("xmpp.offline.type");
259         if (type != null && type.length() > 0) {
260             OfflineMessageStrategy.type = Type.valueOf(type);
261         }
262     }
263 
264     @Override
getFeatures()265     public Iterator<String> getFeatures() {
266         switch (type) {
267             case store:
268             case store_and_bounce:
269             case store_and_drop:
270                 // http://xmpp.org/extensions/xep-0160.html#disco
271                 return Collections.singleton("msgoffline").iterator();
272         }
273         return Collections.emptyIterator();
274     }
275 
276     /**
277      * Strategy types.
278      */
279     public enum Type {
280 
281         /**
282          * All messages are bounced to the sender.
283          */
284         bounce,
285 
286         /**
287          * All messages are silently dropped.
288          */
289         drop,
290 
291         /**
292          * All messages are stored.
293          */
294         store,
295 
296         /**
297          * Messages are stored up to the storage limit, and then bounced.
298          */
299         store_and_bounce,
300 
301         /**
302          * Messages are stored up to the storage limit, and then silently dropped.
303          */
304         store_and_drop
305     }
306 }
307