• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /**
2  * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License");
3  * you may not use this file except in compliance with the License.
4  * You may obtain a copy of the License at
5  *
6  *     http://www.apache.org/licenses/LICENSE-2.0
7  *
8  * Unless required by applicable law or agreed to in writing, software
9  * distributed under the License is distributed on an "AS IS" BASIS,
10  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11  * See the License for the specific language governing permissions and
12  * limitations under the License.
13  */
14 package org.jivesoftware.smackx.bytestreams.ibb;
15 
16 import java.util.Collections;
17 import java.util.HashMap;
18 import java.util.LinkedList;
19 import java.util.List;
20 import java.util.Map;
21 import java.util.Random;
22 import java.util.concurrent.ConcurrentHashMap;
23 
24 import org.jivesoftware.smack.AbstractConnectionListener;
25 import org.jivesoftware.smack.Connection;
26 import org.jivesoftware.smack.ConnectionCreationListener;
27 import org.jivesoftware.smack.XMPPException;
28 import org.jivesoftware.smack.packet.IQ;
29 import org.jivesoftware.smack.packet.XMPPError;
30 import org.jivesoftware.smack.util.SyncPacketSend;
31 import org.jivesoftware.smackx.bytestreams.BytestreamListener;
32 import org.jivesoftware.smackx.bytestreams.BytestreamManager;
33 import org.jivesoftware.smackx.bytestreams.ibb.packet.Open;
34 import org.jivesoftware.smackx.filetransfer.FileTransferManager;
35 
36 /**
37  * The InBandBytestreamManager class handles establishing In-Band Bytestreams as specified in the <a
38  * href="http://xmpp.org/extensions/xep-0047.html">XEP-0047</a>.
39  * <p>
40  * The In-Band Bytestreams (IBB) enables two entities to establish a virtual bytestream over which
41  * they can exchange Base64-encoded chunks of data over XMPP itself. It is the fall-back mechanism
42  * in case the Socks5 bytestream method of transferring data is not available.
43  * <p>
44  * There are two ways to send data over an In-Band Bytestream. It could either use IQ stanzas to
45  * send data packets or message stanzas. If IQ stanzas are used every data packet is acknowledged by
46  * the receiver. This is the recommended way to avoid possible rate-limiting penalties. Message
47  * stanzas are not acknowledged because most XMPP server implementation don't support stanza
48  * flow-control method like <a href="http://xmpp.org/extensions/xep-0079.html">Advanced Message
49  * Processing</a>. To set the stanza that should be used invoke {@link #setStanza(StanzaType)}.
50  * <p>
51  * To establish an In-Band Bytestream invoke the {@link #establishSession(String)} method. This will
52  * negotiate an in-band bytestream with the given target JID and return a session.
53  * <p>
54  * If a session ID for the In-Band Bytestream was already negotiated (e.g. while negotiating a file
55  * transfer) invoke {@link #establishSession(String, String)}.
56  * <p>
57  * To handle incoming In-Band Bytestream requests add an {@link InBandBytestreamListener} to the
58  * manager. There are two ways to add this listener. If you want to be informed about incoming
59  * In-Band Bytestreams from a specific user add the listener by invoking
60  * {@link #addIncomingBytestreamListener(BytestreamListener, String)}. If the listener should
61  * respond to all In-Band Bytestream requests invoke
62  * {@link #addIncomingBytestreamListener(BytestreamListener)}.
63  * <p>
64  * Note that the registered {@link InBandBytestreamListener} will NOT be notified on incoming
65  * In-Band bytestream requests sent in the context of <a
66  * href="http://xmpp.org/extensions/xep-0096.html">XEP-0096</a> file transfer. (See
67  * {@link FileTransferManager})
68  * <p>
69  * If no {@link InBandBytestreamListener}s are registered, all incoming In-Band bytestream requests
70  * will be rejected by returning a &lt;not-acceptable/&gt; error to the initiator.
71  *
72  * @author Henning Staib
73  */
74 public class InBandBytestreamManager implements BytestreamManager {
75 
76     /**
77      * Stanzas that can be used to encapsulate In-Band Bytestream data packets.
78      */
79     public enum StanzaType {
80 
81         /**
82          * IQ stanza.
83          */
84         IQ,
85 
86         /**
87          * Message stanza.
88          */
89         MESSAGE
90     }
91 
92     /*
93      * create a new InBandBytestreamManager and register its shutdown listener on every established
94      * connection
95      */
96     static {
Connection.addConnectionCreationListener(new ConnectionCreationListener() { public void connectionCreated(Connection connection) { final InBandBytestreamManager manager; manager = InBandBytestreamManager.getByteStreamManager(connection); connection.addConnectionListener(new AbstractConnectionListener() { public void connectionClosed() { manager.disableService(); } }); } })97         Connection.addConnectionCreationListener(new ConnectionCreationListener() {
98             public void connectionCreated(Connection connection) {
99                 final InBandBytestreamManager manager;
100                 manager = InBandBytestreamManager.getByteStreamManager(connection);
101 
102                 // register shutdown listener
103                 connection.addConnectionListener(new AbstractConnectionListener() {
104 
105                     public void connectionClosed() {
106                         manager.disableService();
107                     }
108 
109                 });
110 
111             }
112         });
113     }
114 
115     /**
116      * The XMPP namespace of the In-Band Bytestream
117      */
118     public static final String NAMESPACE = "http://jabber.org/protocol/ibb";
119 
120     /**
121      * Maximum block size that is allowed for In-Band Bytestreams
122      */
123     public static final int MAXIMUM_BLOCK_SIZE = 65535;
124 
125     /* prefix used to generate session IDs */
126     private static final String SESSION_ID_PREFIX = "jibb_";
127 
128     /* random generator to create session IDs */
129     private final static Random randomGenerator = new Random();
130 
131     /* stores one InBandBytestreamManager for each XMPP connection */
132     private final static Map<Connection, InBandBytestreamManager> managers = new HashMap<Connection, InBandBytestreamManager>();
133 
134     /* XMPP connection */
135     private final Connection connection;
136 
137     /*
138      * assigns a user to a listener that is informed if an In-Band Bytestream request for this user
139      * is received
140      */
141     private final Map<String, BytestreamListener> userListeners = new ConcurrentHashMap<String, BytestreamListener>();
142 
143     /*
144      * list of listeners that respond to all In-Band Bytestream requests if there are no user
145      * specific listeners for that request
146      */
147     private final List<BytestreamListener> allRequestListeners = Collections.synchronizedList(new LinkedList<BytestreamListener>());
148 
149     /* listener that handles all incoming In-Band Bytestream requests */
150     private final InitiationListener initiationListener;
151 
152     /* listener that handles all incoming In-Band Bytestream IQ data packets */
153     private final DataListener dataListener;
154 
155     /* listener that handles all incoming In-Band Bytestream close requests */
156     private final CloseListener closeListener;
157 
158     /* assigns a session ID to the In-Band Bytestream session */
159     private final Map<String, InBandBytestreamSession> sessions = new ConcurrentHashMap<String, InBandBytestreamSession>();
160 
161     /* block size used for new In-Band Bytestreams */
162     private int defaultBlockSize = 4096;
163 
164     /* maximum block size allowed for this connection */
165     private int maximumBlockSize = MAXIMUM_BLOCK_SIZE;
166 
167     /* the stanza used to send data packets */
168     private StanzaType stanza = StanzaType.IQ;
169 
170     /*
171      * list containing session IDs of In-Band Bytestream open packets that should be ignored by the
172      * InitiationListener
173      */
174     private List<String> ignoredBytestreamRequests = Collections.synchronizedList(new LinkedList<String>());
175 
176     /**
177      * Returns the InBandBytestreamManager to handle In-Band Bytestreams for a given
178      * {@link Connection}.
179      *
180      * @param connection the XMPP connection
181      * @return the InBandBytestreamManager for the given XMPP connection
182      */
getByteStreamManager(Connection connection)183     public static synchronized InBandBytestreamManager getByteStreamManager(Connection connection) {
184         if (connection == null)
185             return null;
186         InBandBytestreamManager manager = managers.get(connection);
187         if (manager == null) {
188             manager = new InBandBytestreamManager(connection);
189             managers.put(connection, manager);
190         }
191         return manager;
192     }
193 
194     /**
195      * Constructor.
196      *
197      * @param connection the XMPP connection
198      */
InBandBytestreamManager(Connection connection)199     private InBandBytestreamManager(Connection connection) {
200         this.connection = connection;
201 
202         // register bytestream open packet listener
203         this.initiationListener = new InitiationListener(this);
204         this.connection.addPacketListener(this.initiationListener,
205                         this.initiationListener.getFilter());
206 
207         // register bytestream data packet listener
208         this.dataListener = new DataListener(this);
209         this.connection.addPacketListener(this.dataListener, this.dataListener.getFilter());
210 
211         // register bytestream close packet listener
212         this.closeListener = new CloseListener(this);
213         this.connection.addPacketListener(this.closeListener, this.closeListener.getFilter());
214 
215     }
216 
217     /**
218      * Adds InBandBytestreamListener that is called for every incoming in-band bytestream request
219      * unless there is a user specific InBandBytestreamListener registered.
220      * <p>
221      * If no listeners are registered all In-Band Bytestream request are rejected with a
222      * &lt;not-acceptable/&gt; error.
223      * <p>
224      * Note that the registered {@link InBandBytestreamListener} will NOT be notified on incoming
225      * Socks5 bytestream requests sent in the context of <a
226      * href="http://xmpp.org/extensions/xep-0096.html">XEP-0096</a> file transfer. (See
227      * {@link FileTransferManager})
228      *
229      * @param listener the listener to register
230      */
addIncomingBytestreamListener(BytestreamListener listener)231     public void addIncomingBytestreamListener(BytestreamListener listener) {
232         this.allRequestListeners.add(listener);
233     }
234 
235     /**
236      * Removes the given listener from the list of listeners for all incoming In-Band Bytestream
237      * requests.
238      *
239      * @param listener the listener to remove
240      */
removeIncomingBytestreamListener(BytestreamListener listener)241     public void removeIncomingBytestreamListener(BytestreamListener listener) {
242         this.allRequestListeners.remove(listener);
243     }
244 
245     /**
246      * Adds InBandBytestreamListener that is called for every incoming in-band bytestream request
247      * from the given user.
248      * <p>
249      * Use this method if you are awaiting an incoming Socks5 bytestream request from a specific
250      * user.
251      * <p>
252      * If no listeners are registered all In-Band Bytestream request are rejected with a
253      * &lt;not-acceptable/&gt; error.
254      * <p>
255      * Note that the registered {@link InBandBytestreamListener} will NOT be notified on incoming
256      * Socks5 bytestream requests sent in the context of <a
257      * href="http://xmpp.org/extensions/xep-0096.html">XEP-0096</a> file transfer. (See
258      * {@link FileTransferManager})
259      *
260      * @param listener the listener to register
261      * @param initiatorJID the JID of the user that wants to establish an In-Band Bytestream
262      */
addIncomingBytestreamListener(BytestreamListener listener, String initiatorJID)263     public void addIncomingBytestreamListener(BytestreamListener listener, String initiatorJID) {
264         this.userListeners.put(initiatorJID, listener);
265     }
266 
267     /**
268      * Removes the listener for the given user.
269      *
270      * @param initiatorJID the JID of the user the listener should be removed
271      */
removeIncomingBytestreamListener(String initiatorJID)272     public void removeIncomingBytestreamListener(String initiatorJID) {
273         this.userListeners.remove(initiatorJID);
274     }
275 
276     /**
277      * Use this method to ignore the next incoming In-Band Bytestream request containing the given
278      * session ID. No listeners will be notified for this request and and no error will be returned
279      * to the initiator.
280      * <p>
281      * This method should be used if you are awaiting an In-Band Bytestream request as a reply to
282      * another packet (e.g. file transfer).
283      *
284      * @param sessionID to be ignored
285      */
ignoreBytestreamRequestOnce(String sessionID)286     public void ignoreBytestreamRequestOnce(String sessionID) {
287         this.ignoredBytestreamRequests.add(sessionID);
288     }
289 
290     /**
291      * Returns the default block size that is used for all outgoing in-band bytestreams for this
292      * connection.
293      * <p>
294      * The recommended default block size is 4096 bytes. See <a
295      * href="http://xmpp.org/extensions/xep-0047.html#usage">XEP-0047</a> Section 5.
296      *
297      * @return the default block size
298      */
getDefaultBlockSize()299     public int getDefaultBlockSize() {
300         return defaultBlockSize;
301     }
302 
303     /**
304      * Sets the default block size that is used for all outgoing in-band bytestreams for this
305      * connection.
306      * <p>
307      * The default block size must be between 1 and 65535 bytes. The recommended default block size
308      * is 4096 bytes. See <a href="http://xmpp.org/extensions/xep-0047.html#usage">XEP-0047</a>
309      * Section 5.
310      *
311      * @param defaultBlockSize the default block size to set
312      */
setDefaultBlockSize(int defaultBlockSize)313     public void setDefaultBlockSize(int defaultBlockSize) {
314         if (defaultBlockSize <= 0 || defaultBlockSize > MAXIMUM_BLOCK_SIZE) {
315             throw new IllegalArgumentException("Default block size must be between 1 and "
316                             + MAXIMUM_BLOCK_SIZE);
317         }
318         this.defaultBlockSize = defaultBlockSize;
319     }
320 
321     /**
322      * Returns the maximum block size that is allowed for In-Band Bytestreams for this connection.
323      * <p>
324      * Incoming In-Band Bytestream open request will be rejected with an
325      * &lt;resource-constraint/&gt; error if the block size is greater then the maximum allowed
326      * block size.
327      * <p>
328      * The default maximum block size is 65535 bytes.
329      *
330      * @return the maximum block size
331      */
getMaximumBlockSize()332     public int getMaximumBlockSize() {
333         return maximumBlockSize;
334     }
335 
336     /**
337      * Sets the maximum block size that is allowed for In-Band Bytestreams for this connection.
338      * <p>
339      * The maximum block size must be between 1 and 65535 bytes.
340      * <p>
341      * Incoming In-Band Bytestream open request will be rejected with an
342      * &lt;resource-constraint/&gt; error if the block size is greater then the maximum allowed
343      * block size.
344      *
345      * @param maximumBlockSize the maximum block size to set
346      */
setMaximumBlockSize(int maximumBlockSize)347     public void setMaximumBlockSize(int maximumBlockSize) {
348         if (maximumBlockSize <= 0 || maximumBlockSize > MAXIMUM_BLOCK_SIZE) {
349             throw new IllegalArgumentException("Maximum block size must be between 1 and "
350                             + MAXIMUM_BLOCK_SIZE);
351         }
352         this.maximumBlockSize = maximumBlockSize;
353     }
354 
355     /**
356      * Returns the stanza used to send data packets.
357      * <p>
358      * Default is {@link StanzaType#IQ}. See <a
359      * href="http://xmpp.org/extensions/xep-0047.html#message">XEP-0047</a> Section 4.
360      *
361      * @return the stanza used to send data packets
362      */
getStanza()363     public StanzaType getStanza() {
364         return stanza;
365     }
366 
367     /**
368      * Sets the stanza used to send data packets.
369      * <p>
370      * The use of {@link StanzaType#IQ} is recommended. See <a
371      * href="http://xmpp.org/extensions/xep-0047.html#message">XEP-0047</a> Section 4.
372      *
373      * @param stanza the stanza to set
374      */
setStanza(StanzaType stanza)375     public void setStanza(StanzaType stanza) {
376         this.stanza = stanza;
377     }
378 
379     /**
380      * Establishes an In-Band Bytestream with the given user and returns the session to send/receive
381      * data to/from the user.
382      * <p>
383      * Use this method to establish In-Band Bytestreams to users accepting all incoming In-Band
384      * Bytestream requests since this method doesn't provide a way to tell the user something about
385      * the data to be sent.
386      * <p>
387      * To establish an In-Band Bytestream after negotiation the kind of data to be sent (e.g. file
388      * transfer) use {@link #establishSession(String, String)}.
389      *
390      * @param targetJID the JID of the user an In-Band Bytestream should be established
391      * @return the session to send/receive data to/from the user
392      * @throws XMPPException if the user doesn't support or accept in-band bytestreams, or if the
393      *         user prefers smaller block sizes
394      */
establishSession(String targetJID)395     public InBandBytestreamSession establishSession(String targetJID) throws XMPPException {
396         String sessionID = getNextSessionID();
397         return establishSession(targetJID, sessionID);
398     }
399 
400     /**
401      * Establishes an In-Band Bytestream with the given user using the given session ID and returns
402      * the session to send/receive data to/from the user.
403      *
404      * @param targetJID the JID of the user an In-Band Bytestream should be established
405      * @param sessionID the session ID for the In-Band Bytestream request
406      * @return the session to send/receive data to/from the user
407      * @throws XMPPException if the user doesn't support or accept in-band bytestreams, or if the
408      *         user prefers smaller block sizes
409      */
establishSession(String targetJID, String sessionID)410     public InBandBytestreamSession establishSession(String targetJID, String sessionID)
411                     throws XMPPException {
412         Open byteStreamRequest = new Open(sessionID, this.defaultBlockSize, this.stanza);
413         byteStreamRequest.setTo(targetJID);
414 
415         // sending packet will throw exception on timeout or error reply
416         SyncPacketSend.getReply(this.connection, byteStreamRequest);
417 
418         InBandBytestreamSession inBandBytestreamSession = new InBandBytestreamSession(
419                         this.connection, byteStreamRequest, targetJID);
420         this.sessions.put(sessionID, inBandBytestreamSession);
421 
422         return inBandBytestreamSession;
423     }
424 
425     /**
426      * Responses to the given IQ packet's sender with an XMPP error that an In-Band Bytestream is
427      * not accepted.
428      *
429      * @param request IQ packet that should be answered with a not-acceptable error
430      */
replyRejectPacket(IQ request)431     protected void replyRejectPacket(IQ request) {
432         XMPPError xmppError = new XMPPError(XMPPError.Condition.no_acceptable);
433         IQ error = IQ.createErrorResponse(request, xmppError);
434         this.connection.sendPacket(error);
435     }
436 
437     /**
438      * Responses to the given IQ packet's sender with an XMPP error that an In-Band Bytestream open
439      * request is rejected because its block size is greater than the maximum allowed block size.
440      *
441      * @param request IQ packet that should be answered with a resource-constraint error
442      */
replyResourceConstraintPacket(IQ request)443     protected void replyResourceConstraintPacket(IQ request) {
444         XMPPError xmppError = new XMPPError(XMPPError.Condition.resource_constraint);
445         IQ error = IQ.createErrorResponse(request, xmppError);
446         this.connection.sendPacket(error);
447     }
448 
449     /**
450      * Responses to the given IQ packet's sender with an XMPP error that an In-Band Bytestream
451      * session could not be found.
452      *
453      * @param request IQ packet that should be answered with a item-not-found error
454      */
replyItemNotFoundPacket(IQ request)455     protected void replyItemNotFoundPacket(IQ request) {
456         XMPPError xmppError = new XMPPError(XMPPError.Condition.item_not_found);
457         IQ error = IQ.createErrorResponse(request, xmppError);
458         this.connection.sendPacket(error);
459     }
460 
461     /**
462      * Returns a new unique session ID.
463      *
464      * @return a new unique session ID
465      */
getNextSessionID()466     private String getNextSessionID() {
467         StringBuilder buffer = new StringBuilder();
468         buffer.append(SESSION_ID_PREFIX);
469         buffer.append(Math.abs(randomGenerator.nextLong()));
470         return buffer.toString();
471     }
472 
473     /**
474      * Returns the XMPP connection.
475      *
476      * @return the XMPP connection
477      */
getConnection()478     protected Connection getConnection() {
479         return this.connection;
480     }
481 
482     /**
483      * Returns the {@link InBandBytestreamListener} that should be informed if a In-Band Bytestream
484      * request from the given initiator JID is received.
485      *
486      * @param initiator the initiator's JID
487      * @return the listener
488      */
getUserListener(String initiator)489     protected BytestreamListener getUserListener(String initiator) {
490         return this.userListeners.get(initiator);
491     }
492 
493     /**
494      * Returns a list of {@link InBandBytestreamListener} that are informed if there are no
495      * listeners for a specific initiator.
496      *
497      * @return list of listeners
498      */
getAllRequestListeners()499     protected List<BytestreamListener> getAllRequestListeners() {
500         return this.allRequestListeners;
501     }
502 
503     /**
504      * Returns the sessions map.
505      *
506      * @return the sessions map
507      */
getSessions()508     protected Map<String, InBandBytestreamSession> getSessions() {
509         return sessions;
510     }
511 
512     /**
513      * Returns the list of session IDs that should be ignored by the InitialtionListener
514      *
515      * @return list of session IDs
516      */
getIgnoredBytestreamRequests()517     protected List<String> getIgnoredBytestreamRequests() {
518         return ignoredBytestreamRequests;
519     }
520 
521     /**
522      * Disables the InBandBytestreamManager by removing its packet listeners and resetting its
523      * internal status.
524      */
disableService()525     private void disableService() {
526 
527         // remove manager from static managers map
528         managers.remove(connection);
529 
530         // remove all listeners registered by this manager
531         this.connection.removePacketListener(this.initiationListener);
532         this.connection.removePacketListener(this.dataListener);
533         this.connection.removePacketListener(this.closeListener);
534 
535         // shutdown threads
536         this.initiationListener.shutdown();
537 
538         // reset internal status
539         this.userListeners.clear();
540         this.allRequestListeners.clear();
541         this.sessions.clear();
542         this.ignoredBytestreamRequests.clear();
543 
544     }
545 
546 }
547