001/**
002 *
003 * Copyright the original author or authors
004 *
005 * Licensed under the Apache License, Version 2.0 (the "License");
006 * you may not use this file except in compliance with the License.
007 * You may obtain a copy of the License at
008 *
009 *     http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.jivesoftware.smackx.bytestreams.ibb;
018
019import java.util.Collections;
020import java.util.LinkedList;
021import java.util.List;
022import java.util.Map;
023import java.util.Random;
024import java.util.WeakHashMap;
025import java.util.concurrent.ConcurrentHashMap;
026
027import org.jivesoftware.smack.AbstractConnectionClosedListener;
028import org.jivesoftware.smack.ConnectionCreationListener;
029import org.jivesoftware.smack.Manager;
030import org.jivesoftware.smack.SmackException;
031import org.jivesoftware.smack.SmackException.NoResponseException;
032import org.jivesoftware.smack.SmackException.NotConnectedException;
033import org.jivesoftware.smack.XMPPConnection;
034import org.jivesoftware.smack.XMPPConnectionRegistry;
035import org.jivesoftware.smack.XMPPException;
036import org.jivesoftware.smack.XMPPException.XMPPErrorException;
037import org.jivesoftware.smack.packet.IQ;
038import org.jivesoftware.smack.packet.StanzaError;
039
040import org.jivesoftware.smackx.bytestreams.BytestreamListener;
041import org.jivesoftware.smackx.bytestreams.BytestreamManager;
042import org.jivesoftware.smackx.bytestreams.ibb.packet.Open;
043import org.jivesoftware.smackx.filetransfer.FileTransferManager;
044
045import org.jxmpp.jid.Jid;
046
047/**
048 * The InBandBytestreamManager class handles establishing In-Band Bytestreams as specified in the <a
049 * href="http://xmpp.org/extensions/xep-0047.html">XEP-0047</a>.
050 * <p>
051 * The In-Band Bytestreams (IBB) enables two entities to establish a virtual bytestream over which
052 * they can exchange Base64-encoded chunks of data over XMPP itself. It is the fall-back mechanism
053 * in case the Socks5 bytestream method of transferring data is not available.
054 * <p>
055 * There are two ways to send data over an In-Band Bytestream. It could either use IQ stanzas to
056 * send data packets or message stanzas. If IQ stanzas are used every data stanza is acknowledged by
057 * the receiver. This is the recommended way to avoid possible rate-limiting penalties. Message
058 * stanzas are not acknowledged because most XMPP server implementation don't support stanza
059 * flow-control method like <a href="http://xmpp.org/extensions/xep-0079.html">Advanced Message
060 * Processing</a>. To set the stanza that should be used invoke {@link #setStanza(StanzaType)}.
061 * <p>
062 * To establish an In-Band Bytestream invoke the {@link #establishSession(Jid)} method. This will
063 * negotiate an in-band bytestream with the given target JID and return a session.
064 * <p>
065 * If a session ID for the In-Band Bytestream was already negotiated (e.g. while negotiating a file
066 * transfer) invoke {@link #establishSession(Jid, String)}.
067 * <p>
068 * To handle incoming In-Band Bytestream requests add an {@link InBandBytestreamListener} to the
069 * manager. There are two ways to add this listener. If you want to be informed about incoming
070 * In-Band Bytestreams from a specific user add the listener by invoking
071 * {@link #addIncomingBytestreamListener(BytestreamListener, Jid)}. If the listener should
072 * respond to all In-Band Bytestream requests invoke
073 * {@link #addIncomingBytestreamListener(BytestreamListener)}.
074 * <p>
075 * Note that the registered {@link InBandBytestreamListener} will NOT be notified on incoming
076 * In-Band bytestream requests sent in the context of <a
077 * href="http://xmpp.org/extensions/xep-0096.html">XEP-0096</a> file transfer. (See
078 * {@link FileTransferManager})
079 * <p>
080 * If no {@link InBandBytestreamListener}s are registered, all incoming In-Band bytestream requests
081 * will be rejected by returning a &lt;not-acceptable/&gt; error to the initiator.
082 *
083 * @author Henning Staib
084 */
085public final class InBandBytestreamManager extends Manager implements BytestreamManager {
086
087    /**
088     * Stanzas that can be used to encapsulate In-Band Bytestream data packets.
089     */
090    public enum StanzaType {
091
092        /**
093         * IQ stanza.
094         */
095        IQ,
096
097        /**
098         * Message stanza.
099         */
100        MESSAGE
101    }
102
103    /*
104     * create a new InBandBytestreamManager and register its shutdown listener on every established
105     * connection
106     */
107    static {
108        XMPPConnectionRegistry.addConnectionCreationListener(new ConnectionCreationListener() {
109            @Override
110            public void connectionCreated(final XMPPConnection connection) {
111                // create the manager for this connection
112                InBandBytestreamManager.getByteStreamManager(connection);
113
114                // register shutdown listener
115                connection.addConnectionListener(new AbstractConnectionClosedListener() {
116
117                    @Override
118                    public void connectionTerminated() {
119                        InBandBytestreamManager.getByteStreamManager(connection).disableService();
120                    }
121
122                    @Override
123                    public void connected(XMPPConnection connection) {
124                        InBandBytestreamManager.getByteStreamManager(connection);
125                    }
126
127                });
128
129            }
130        });
131    }
132
133    /**
134     * Maximum block size that is allowed for In-Band Bytestreams.
135     */
136    public static final int MAXIMUM_BLOCK_SIZE = 65535;
137
138    /* prefix used to generate session IDs */
139    private static final String SESSION_ID_PREFIX = "jibb_";
140
141    /* random generator to create session IDs */
142    private static final Random randomGenerator = new Random();
143
144    /* stores one InBandBytestreamManager for each XMPP connection */
145    private static final Map<XMPPConnection, InBandBytestreamManager> managers = new WeakHashMap<>();
146
147    /*
148     * assigns a user to a listener that is informed if an In-Band Bytestream request for this user
149     * is received
150     */
151    private final Map<Jid, BytestreamListener> userListeners = new ConcurrentHashMap<>();
152
153    /*
154     * list of listeners that respond to all In-Band Bytestream requests if there are no user
155     * specific listeners for that request
156     */
157    private final List<BytestreamListener> allRequestListeners = Collections.synchronizedList(new LinkedList<BytestreamListener>());
158
159    /* listener that handles all incoming In-Band Bytestream requests */
160    private final InitiationListener initiationListener;
161
162    /* listener that handles all incoming In-Band Bytestream IQ data packets */
163    private final DataListener dataListener;
164
165    /* listener that handles all incoming In-Band Bytestream close requests */
166    private final CloseListener closeListener;
167
168    /* assigns a session ID to the In-Band Bytestream session */
169    private final Map<String, InBandBytestreamSession> sessions = new ConcurrentHashMap<String, InBandBytestreamSession>();
170
171    /* block size used for new In-Band Bytestreams */
172    private int defaultBlockSize = 4096;
173
174    /* maximum block size allowed for this connection */
175    private int maximumBlockSize = MAXIMUM_BLOCK_SIZE;
176
177    /* the stanza used to send data packets */
178    private StanzaType stanza = StanzaType.IQ;
179
180    /*
181     * list containing session IDs of In-Band Bytestream open packets that should be ignored by the
182     * InitiationListener
183     */
184    private final List<String> ignoredBytestreamRequests = Collections.synchronizedList(new LinkedList<String>());
185
186    /**
187     * Returns the InBandBytestreamManager to handle In-Band Bytestreams for a given
188     * {@link XMPPConnection}.
189     *
190     * @param connection the XMPP connection
191     * @return the InBandBytestreamManager for the given XMPP connection
192     */
193    public static synchronized InBandBytestreamManager getByteStreamManager(XMPPConnection connection) {
194        if (connection == null)
195            return null;
196        InBandBytestreamManager manager = managers.get(connection);
197        if (manager == null) {
198            manager = new InBandBytestreamManager(connection);
199            managers.put(connection, manager);
200        }
201        return manager;
202    }
203
204    /**
205     * Constructor.
206     *
207     * @param connection the XMPP connection
208     */
209    private InBandBytestreamManager(XMPPConnection connection) {
210        super(connection);
211
212        // register bytestream open packet listener
213        this.initiationListener = new InitiationListener(this);
214        connection.registerIQRequestHandler(initiationListener);
215
216        // register bytestream data packet listener
217        this.dataListener = new DataListener(this);
218        connection.registerIQRequestHandler(dataListener);
219
220        // register bytestream close packet listener
221        this.closeListener = new CloseListener(this);
222        connection.registerIQRequestHandler(closeListener);
223    }
224
225    /**
226     * Adds InBandBytestreamListener that is called for every incoming in-band bytestream request
227     * unless there is a user specific InBandBytestreamListener registered.
228     * <p>
229     * If no listeners are registered all In-Band Bytestream request are rejected with a
230     * &lt;not-acceptable/&gt; error.
231     * <p>
232     * Note that the registered {@link InBandBytestreamListener} will NOT be notified on incoming
233     * Socks5 bytestream requests sent in the context of <a
234     * href="http://xmpp.org/extensions/xep-0096.html">XEP-0096</a> file transfer. (See
235     * {@link FileTransferManager})
236     *
237     * @param listener the listener to register
238     */
239    @Override
240    public void addIncomingBytestreamListener(BytestreamListener listener) {
241        this.allRequestListeners.add(listener);
242    }
243
244    /**
245     * Removes the given listener from the list of listeners for all incoming In-Band Bytestream
246     * requests.
247     *
248     * @param listener the listener to remove
249     */
250    @Override
251    public void removeIncomingBytestreamListener(BytestreamListener listener) {
252        this.allRequestListeners.remove(listener);
253    }
254
255    /**
256     * Adds InBandBytestreamListener that is called for every incoming in-band bytestream request
257     * from the given user.
258     * <p>
259     * Use this method if you are awaiting an incoming Socks5 bytestream request from a specific
260     * user.
261     * <p>
262     * If no listeners are registered all In-Band Bytestream request are rejected with a
263     * &lt;not-acceptable/&gt; error.
264     * <p>
265     * Note that the registered {@link InBandBytestreamListener} will NOT be notified on incoming
266     * Socks5 bytestream requests sent in the context of <a
267     * href="http://xmpp.org/extensions/xep-0096.html">XEP-0096</a> file transfer. (See
268     * {@link FileTransferManager})
269     *
270     * @param listener the listener to register
271     * @param initiatorJID the JID of the user that wants to establish an In-Band Bytestream
272     */
273    @Override
274    public void addIncomingBytestreamListener(BytestreamListener listener, Jid initiatorJID) {
275        this.userListeners.put(initiatorJID, listener);
276    }
277
278    /**
279     * Removes the listener for the given user.
280     *
281     * @param initiatorJID the JID of the user the listener should be removed
282     */
283    @Override
284    public void removeIncomingBytestreamListener(Jid initiatorJID) {
285        this.userListeners.remove(initiatorJID);
286    }
287
288    /**
289     * Use this method to ignore the next incoming In-Band Bytestream request containing the given
290     * session ID. No listeners will be notified for this request and and no error will be returned
291     * to the initiator.
292     * <p>
293     * This method should be used if you are awaiting an In-Band Bytestream request as a reply to
294     * another stanza (e.g. file transfer).
295     *
296     * @param sessionID to be ignored
297     */
298    public void ignoreBytestreamRequestOnce(String sessionID) {
299        this.ignoredBytestreamRequests.add(sessionID);
300    }
301
302    /**
303     * Returns the default block size that is used for all outgoing in-band bytestreams for this
304     * connection.
305     * <p>
306     * The recommended default block size is 4096 bytes. See <a
307     * href="http://xmpp.org/extensions/xep-0047.html#usage">XEP-0047</a> Section 5.
308     *
309     * @return the default block size
310     */
311    public int getDefaultBlockSize() {
312        return defaultBlockSize;
313    }
314
315    /**
316     * Sets the default block size that is used for all outgoing in-band bytestreams for this
317     * connection.
318     * <p>
319     * The default block size must be between 1 and 65535 bytes. The recommended default block size
320     * is 4096 bytes. See <a href="http://xmpp.org/extensions/xep-0047.html#usage">XEP-0047</a>
321     * Section 5.
322     *
323     * @param defaultBlockSize the default block size to set
324     */
325    public void setDefaultBlockSize(int defaultBlockSize) {
326        if (defaultBlockSize <= 0 || defaultBlockSize > MAXIMUM_BLOCK_SIZE) {
327            throw new IllegalArgumentException("Default block size must be between 1 and "
328                            + MAXIMUM_BLOCK_SIZE);
329        }
330        this.defaultBlockSize = defaultBlockSize;
331    }
332
333    /**
334     * Returns the maximum block size that is allowed for In-Band Bytestreams for this connection.
335     * <p>
336     * Incoming In-Band Bytestream open request will be rejected with an
337     * &lt;resource-constraint/&gt; error if the block size is greater then the maximum allowed
338     * block size.
339     * <p>
340     * The default maximum block size is 65535 bytes.
341     *
342     * @return the maximum block size
343     */
344    public int getMaximumBlockSize() {
345        return maximumBlockSize;
346    }
347
348    /**
349     * Sets the maximum block size that is allowed for In-Band Bytestreams for this connection.
350     * <p>
351     * The maximum block size must be between 1 and 65535 bytes.
352     * <p>
353     * Incoming In-Band Bytestream open request will be rejected with an
354     * &lt;resource-constraint/&gt; error if the block size is greater then the maximum allowed
355     * block size.
356     *
357     * @param maximumBlockSize the maximum block size to set
358     */
359    public void setMaximumBlockSize(int maximumBlockSize) {
360        if (maximumBlockSize <= 0 || maximumBlockSize > MAXIMUM_BLOCK_SIZE) {
361            throw new IllegalArgumentException("Maximum block size must be between 1 and "
362                            + MAXIMUM_BLOCK_SIZE);
363        }
364        this.maximumBlockSize = maximumBlockSize;
365    }
366
367    /**
368     * Returns the stanza used to send data packets.
369     * <p>
370     * Default is {@link StanzaType#IQ}. See <a
371     * href="http://xmpp.org/extensions/xep-0047.html#message">XEP-0047</a> Section 4.
372     *
373     * @return the stanza used to send data packets
374     */
375    public StanzaType getStanza() {
376        return stanza;
377    }
378
379    /**
380     * Sets the stanza used to send data packets.
381     * <p>
382     * The use of {@link StanzaType#IQ} is recommended. See <a
383     * href="http://xmpp.org/extensions/xep-0047.html#message">XEP-0047</a> Section 4.
384     *
385     * @param stanza the stanza to set
386     */
387    public void setStanza(StanzaType stanza) {
388        this.stanza = stanza;
389    }
390
391    /**
392     * Establishes an In-Band Bytestream with the given user and returns the session to send/receive
393     * data to/from the user.
394     * <p>
395     * Use this method to establish In-Band Bytestreams to users accepting all incoming In-Band
396     * Bytestream requests since this method doesn't provide a way to tell the user something about
397     * the data to be sent.
398     * <p>
399     * To establish an In-Band Bytestream after negotiation the kind of data to be sent (e.g. file
400     * transfer) use {@link #establishSession(Jid, String)}.
401     *
402     * @param targetJID the JID of the user an In-Band Bytestream should be established
403     * @return the session to send/receive data to/from the user
404     * @throws XMPPException if the user doesn't support or accept in-band bytestreams, or if the
405     *         user prefers smaller block sizes
406     * @throws SmackException if there was no response from the server.
407     * @throws InterruptedException
408     */
409    @Override
410    public InBandBytestreamSession establishSession(Jid targetJID) throws XMPPException, SmackException, InterruptedException {
411        String sessionID = getNextSessionID();
412        return establishSession(targetJID, sessionID);
413    }
414
415    /**
416     * Establishes an In-Band Bytestream with the given user using the given session ID and returns
417     * the session to send/receive data to/from the user.
418     *
419     * @param targetJID the JID of the user an In-Band Bytestream should be established
420     * @param sessionID the session ID for the In-Band Bytestream request
421     * @return the session to send/receive data to/from the user
422     * @throws XMPPErrorException if the user doesn't support or accept in-band bytestreams, or if the
423     *         user prefers smaller block sizes
424     * @throws NoResponseException if there was no response from the server.
425     * @throws NotConnectedException
426     * @throws InterruptedException
427     */
428    @Override
429    public InBandBytestreamSession establishSession(Jid targetJID, String sessionID)
430                    throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
431        Open byteStreamRequest = new Open(sessionID, this.defaultBlockSize, this.stanza);
432        byteStreamRequest.setTo(targetJID);
433
434        final XMPPConnection connection = connection();
435
436        // sending packet will throw exception on timeout or error reply
437        connection.createStanzaCollectorAndSend(byteStreamRequest).nextResultOrThrow();
438
439        InBandBytestreamSession inBandBytestreamSession = new InBandBytestreamSession(
440                        connection, byteStreamRequest, targetJID);
441        this.sessions.put(sessionID, inBandBytestreamSession);
442
443        return inBandBytestreamSession;
444    }
445
446    /**
447     * Responses to the given IQ packet's sender with an XMPP error that an In-Band Bytestream is
448     * not accepted.
449     *
450     * @param request IQ stanza that should be answered with a not-acceptable error
451     * @throws NotConnectedException
452     * @throws InterruptedException
453     */
454    protected void replyRejectPacket(IQ request) throws NotConnectedException, InterruptedException {
455        IQ error = IQ.createErrorResponse(request, StanzaError.Condition.not_acceptable);
456        connection().sendStanza(error);
457    }
458
459    /**
460     * Responses to the given IQ packet's sender with an XMPP error that an In-Band Bytestream open
461     * request is rejected because its block size is greater than the maximum allowed block size.
462     *
463     * @param request IQ stanza that should be answered with a resource-constraint error
464     * @throws NotConnectedException
465     * @throws InterruptedException
466     */
467    protected void replyResourceConstraintPacket(IQ request) throws NotConnectedException, InterruptedException {
468        IQ error = IQ.createErrorResponse(request, StanzaError.Condition.resource_constraint);
469        connection().sendStanza(error);
470    }
471
472    /**
473     * Responses to the given IQ packet's sender with an XMPP error that an In-Band Bytestream
474     * session could not be found.
475     *
476     * @param request IQ stanza that should be answered with a item-not-found error
477     * @throws NotConnectedException
478     * @throws InterruptedException
479     */
480    protected void replyItemNotFoundPacket(IQ request) throws NotConnectedException, InterruptedException {
481        IQ error = IQ.createErrorResponse(request, StanzaError.Condition.item_not_found);
482        connection().sendStanza(error);
483    }
484
485    /**
486     * Returns a new unique session ID.
487     *
488     * @return a new unique session ID
489     */
490    private static String getNextSessionID() {
491        StringBuilder buffer = new StringBuilder();
492        buffer.append(SESSION_ID_PREFIX);
493        buffer.append(Math.abs(randomGenerator.nextLong()));
494        return buffer.toString();
495    }
496
497    /**
498     * Returns the XMPP connection.
499     *
500     * @return the XMPP connection
501     */
502    protected XMPPConnection getConnection() {
503        return connection();
504    }
505
506    /**
507     * Returns the {@link InBandBytestreamListener} that should be informed if a In-Band Bytestream
508     * request from the given initiator JID is received.
509     *
510     * @param initiator the initiator's JID
511     * @return the listener
512     */
513    protected BytestreamListener getUserListener(Jid initiator) {
514        return this.userListeners.get(initiator);
515    }
516
517    /**
518     * Returns a list of {@link InBandBytestreamListener} that are informed if there are no
519     * listeners for a specific initiator.
520     *
521     * @return list of listeners
522     */
523    protected List<BytestreamListener> getAllRequestListeners() {
524        return this.allRequestListeners;
525    }
526
527    /**
528     * Returns the sessions map.
529     *
530     * @return the sessions map
531     */
532    protected Map<String, InBandBytestreamSession> getSessions() {
533        return sessions;
534    }
535
536    /**
537     * Returns the list of session IDs that should be ignored by the InitialtionListener
538     *
539     * @return list of session IDs
540     */
541    protected List<String> getIgnoredBytestreamRequests() {
542        return ignoredBytestreamRequests;
543    }
544
545    /**
546     * Disables the InBandBytestreamManager by removing its stanza listeners and resetting its
547     * internal status, which includes removing this instance from the managers map.
548     */
549    private void disableService() {
550        final XMPPConnection connection = connection();
551
552        // remove manager from static managers map
553        managers.remove(connection);
554
555        // remove all listeners registered by this manager
556        connection.unregisterIQRequestHandler(initiationListener);
557        connection.unregisterIQRequestHandler(dataListener);
558        connection.unregisterIQRequestHandler(closeListener);
559
560        // shutdown threads
561        this.initiationListener.shutdown();
562
563        // reset internal status
564        this.userListeners.clear();
565        this.allRequestListeners.clear();
566        this.sessions.clear();
567        this.ignoredBytestreamRequests.clear();
568
569    }
570
571}