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 */
14package org.jivesoftware.smackx.bytestreams.ibb;
15
16import java.util.Collections;
17import java.util.HashMap;
18import java.util.LinkedList;
19import java.util.List;
20import java.util.Map;
21import java.util.Random;
22import java.util.concurrent.ConcurrentHashMap;
23
24import org.jivesoftware.smack.AbstractConnectionListener;
25import org.jivesoftware.smack.Connection;
26import org.jivesoftware.smack.ConnectionCreationListener;
27import org.jivesoftware.smack.XMPPException;
28import org.jivesoftware.smack.packet.IQ;
29import org.jivesoftware.smack.packet.XMPPError;
30import org.jivesoftware.smack.util.SyncPacketSend;
31import org.jivesoftware.smackx.bytestreams.BytestreamListener;
32import org.jivesoftware.smackx.bytestreams.BytestreamManager;
33import org.jivesoftware.smackx.bytestreams.ibb.packet.Open;
34import 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 */
74public 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 {
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
499    protected List<BytestreamListener> getAllRequestListeners() {
500        return this.allRequestListeners;
501    }
502
503    /**
504     * Returns the sessions map.
505     *
506     * @return the sessions map
507     */
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     */
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     */
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