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.XMPPError;
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(/packet) 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 reconnectionSuccessful() {
124                        // re-create the manager for this connection
125                        InBandBytestreamManager.getByteStreamManager(connection);
126                    }
127
128                });
129
130            }
131        });
132    }
133
134    /**
135     * Maximum block size that is allowed for In-Band Bytestreams.
136     */
137    public static final int MAXIMUM_BLOCK_SIZE = 65535;
138
139    /* prefix used to generate session IDs */
140    private static final String SESSION_ID_PREFIX = "jibb_";
141
142    /* random generator to create session IDs */
143    private final static Random randomGenerator = new Random();
144
145    /* stores one InBandBytestreamManager for each XMPP connection */
146    private final static Map<XMPPConnection, InBandBytestreamManager> managers = new WeakHashMap<>();
147
148    /*
149     * assigns a user to a listener that is informed if an In-Band Bytestream request for this user
150     * is received
151     */
152    private final Map<Jid, BytestreamListener> userListeners = new ConcurrentHashMap<>();
153
154    /*
155     * list of listeners that respond to all In-Band Bytestream requests if there are no user
156     * specific listeners for that request
157     */
158    private final List<BytestreamListener> allRequestListeners = Collections.synchronizedList(new LinkedList<BytestreamListener>());
159
160    /* listener that handles all incoming In-Band Bytestream requests */
161    private final InitiationListener initiationListener;
162
163    /* listener that handles all incoming In-Band Bytestream IQ data packets */
164    private final DataListener dataListener;
165
166    /* listener that handles all incoming In-Band Bytestream close requests */
167    private final CloseListener closeListener;
168
169    /* assigns a session ID to the In-Band Bytestream session */
170    private final Map<String, InBandBytestreamSession> sessions = new ConcurrentHashMap<String, InBandBytestreamSession>();
171
172    /* block size used for new In-Band Bytestreams */
173    private int defaultBlockSize = 4096;
174
175    /* maximum block size allowed for this connection */
176    private int maximumBlockSize = MAXIMUM_BLOCK_SIZE;
177
178    /* the stanza used to send data packets */
179    private StanzaType stanza = StanzaType.IQ;
180
181    /*
182     * list containing session IDs of In-Band Bytestream open packets that should be ignored by the
183     * InitiationListener
184     */
185    private List<String> ignoredBytestreamRequests = Collections.synchronizedList(new LinkedList<String>());
186
187    /**
188     * Returns the InBandBytestreamManager to handle In-Band Bytestreams for a given
189     * {@link XMPPConnection}.
190     * 
191     * @param connection the XMPP connection
192     * @return the InBandBytestreamManager for the given XMPP connection
193     */
194    public static synchronized InBandBytestreamManager getByteStreamManager(XMPPConnection connection) {
195        if (connection == null)
196            return null;
197        InBandBytestreamManager manager = managers.get(connection);
198        if (manager == null) {
199            manager = new InBandBytestreamManager(connection);
200            managers.put(connection, manager);
201        }
202        return manager;
203    }
204
205    /**
206     * Constructor.
207     * 
208     * @param connection the XMPP connection
209     */
210    private InBandBytestreamManager(XMPPConnection connection) {
211        super(connection);
212
213        // register bytestream open packet listener
214        this.initiationListener = new InitiationListener(this);
215        connection.registerIQRequestHandler(initiationListener);
216
217        // register bytestream data packet listener
218        this.dataListener = new DataListener(this);
219        connection.registerIQRequestHandler(dataListener);
220
221        // register bytestream close packet listener
222        this.closeListener = new CloseListener(this);
223        connection.registerIQRequestHandler(closeListener);
224    }
225
226    /**
227     * Adds InBandBytestreamListener that is called for every incoming in-band bytestream request
228     * unless there is a user specific InBandBytestreamListener registered.
229     * <p>
230     * If no listeners are registered all In-Band Bytestream request are rejected with a
231     * &lt;not-acceptable/&gt; error.
232     * <p>
233     * Note that the registered {@link InBandBytestreamListener} will NOT be notified on incoming
234     * Socks5 bytestream requests sent in the context of <a
235     * href="http://xmpp.org/extensions/xep-0096.html">XEP-0096</a> file transfer. (See
236     * {@link FileTransferManager})
237     * 
238     * @param listener the listener to register
239     */
240    @Override
241    public void addIncomingBytestreamListener(BytestreamListener listener) {
242        this.allRequestListeners.add(listener);
243    }
244
245    /**
246     * Removes the given listener from the list of listeners for all incoming In-Band Bytestream
247     * requests.
248     * 
249     * @param listener the listener to remove
250     */
251    @Override
252    public void removeIncomingBytestreamListener(BytestreamListener listener) {
253        this.allRequestListeners.remove(listener);
254    }
255
256    /**
257     * Adds InBandBytestreamListener that is called for every incoming in-band bytestream request
258     * from the given user.
259     * <p>
260     * Use this method if you are awaiting an incoming Socks5 bytestream request from a specific
261     * user.
262     * <p>
263     * If no listeners are registered all In-Band Bytestream request are rejected with a
264     * &lt;not-acceptable/&gt; error.
265     * <p>
266     * Note that the registered {@link InBandBytestreamListener} will NOT be notified on incoming
267     * Socks5 bytestream requests sent in the context of <a
268     * href="http://xmpp.org/extensions/xep-0096.html">XEP-0096</a> file transfer. (See
269     * {@link FileTransferManager})
270     * 
271     * @param listener the listener to register
272     * @param initiatorJID the JID of the user that wants to establish an In-Band Bytestream
273     */
274    @Override
275    public void addIncomingBytestreamListener(BytestreamListener listener, Jid initiatorJID) {
276        this.userListeners.put(initiatorJID, listener);
277    }
278
279    /**
280     * Removes the listener for the given user.
281     * 
282     * @param initiatorJID the JID of the user the listener should be removed
283     */
284    @Override
285    // TODO: Change argument to Jid in Smack 4.3.
286    @SuppressWarnings("CollectionIncompatibleType")
287    public void removeIncomingBytestreamListener(String initiatorJID) {
288        this.userListeners.remove(initiatorJID);
289    }
290
291    /**
292     * Use this method to ignore the next incoming In-Band Bytestream request containing the given
293     * session ID. No listeners will be notified for this request and and no error will be returned
294     * to the initiator.
295     * <p>
296     * This method should be used if you are awaiting an In-Band Bytestream request as a reply to
297     * another stanza(/packet) (e.g. file transfer).
298     * 
299     * @param sessionID to be ignored
300     */
301    public void ignoreBytestreamRequestOnce(String sessionID) {
302        this.ignoredBytestreamRequests.add(sessionID);
303    }
304
305    /**
306     * Returns the default block size that is used for all outgoing in-band bytestreams for this
307     * connection.
308     * <p>
309     * The recommended default block size is 4096 bytes. See <a
310     * href="http://xmpp.org/extensions/xep-0047.html#usage">XEP-0047</a> Section 5.
311     * 
312     * @return the default block size
313     */
314    public int getDefaultBlockSize() {
315        return defaultBlockSize;
316    }
317
318    /**
319     * Sets the default block size that is used for all outgoing in-band bytestreams for this
320     * connection.
321     * <p>
322     * The default block size must be between 1 and 65535 bytes. The recommended default block size
323     * is 4096 bytes. See <a href="http://xmpp.org/extensions/xep-0047.html#usage">XEP-0047</a>
324     * Section 5.
325     * 
326     * @param defaultBlockSize the default block size to set
327     */
328    public void setDefaultBlockSize(int defaultBlockSize) {
329        if (defaultBlockSize <= 0 || defaultBlockSize > MAXIMUM_BLOCK_SIZE) {
330            throw new IllegalArgumentException("Default block size must be between 1 and "
331                            + MAXIMUM_BLOCK_SIZE);
332        }
333        this.defaultBlockSize = defaultBlockSize;
334    }
335
336    /**
337     * Returns the maximum block size that is allowed for In-Band Bytestreams for this connection.
338     * <p>
339     * Incoming In-Band Bytestream open request will be rejected with an
340     * &lt;resource-constraint/&gt; error if the block size is greater then the maximum allowed
341     * block size.
342     * <p>
343     * The default maximum block size is 65535 bytes.
344     * 
345     * @return the maximum block size
346     */
347    public int getMaximumBlockSize() {
348        return maximumBlockSize;
349    }
350
351    /**
352     * Sets the maximum block size that is allowed for In-Band Bytestreams for this connection.
353     * <p>
354     * The maximum block size must be between 1 and 65535 bytes.
355     * <p>
356     * Incoming In-Band Bytestream open request will be rejected with an
357     * &lt;resource-constraint/&gt; error if the block size is greater then the maximum allowed
358     * block size.
359     * 
360     * @param maximumBlockSize the maximum block size to set
361     */
362    public void setMaximumBlockSize(int maximumBlockSize) {
363        if (maximumBlockSize <= 0 || maximumBlockSize > MAXIMUM_BLOCK_SIZE) {
364            throw new IllegalArgumentException("Maximum block size must be between 1 and "
365                            + MAXIMUM_BLOCK_SIZE);
366        }
367        this.maximumBlockSize = maximumBlockSize;
368    }
369
370    /**
371     * Returns the stanza used to send data packets.
372     * <p>
373     * Default is {@link StanzaType#IQ}. See <a
374     * href="http://xmpp.org/extensions/xep-0047.html#message">XEP-0047</a> Section 4.
375     * 
376     * @return the stanza used to send data packets
377     */
378    public StanzaType getStanza() {
379        return stanza;
380    }
381
382    /**
383     * Sets the stanza used to send data packets.
384     * <p>
385     * The use of {@link StanzaType#IQ} is recommended. See <a
386     * href="http://xmpp.org/extensions/xep-0047.html#message">XEP-0047</a> Section 4.
387     * 
388     * @param stanza the stanza to set
389     */
390    public void setStanza(StanzaType stanza) {
391        this.stanza = stanza;
392    }
393
394    /**
395     * Establishes an In-Band Bytestream with the given user and returns the session to send/receive
396     * data to/from the user.
397     * <p>
398     * Use this method to establish In-Band Bytestreams to users accepting all incoming In-Band
399     * Bytestream requests since this method doesn't provide a way to tell the user something about
400     * the data to be sent.
401     * <p>
402     * To establish an In-Band Bytestream after negotiation the kind of data to be sent (e.g. file
403     * transfer) use {@link #establishSession(Jid, String)}.
404     * 
405     * @param targetJID the JID of the user an In-Band Bytestream should be established
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     * @throws SmackException if there was no response from the server.
410     * @throws InterruptedException 
411     */
412    @Override
413    public InBandBytestreamSession establishSession(Jid targetJID) throws XMPPException, SmackException, InterruptedException {
414        String sessionID = getNextSessionID();
415        return establishSession(targetJID, sessionID);
416    }
417
418    /**
419     * Establishes an In-Band Bytestream with the given user using the given session ID and returns
420     * the session to send/receive data to/from the user.
421     * 
422     * @param targetJID the JID of the user an In-Band Bytestream should be established
423     * @param sessionID the session ID for the In-Band Bytestream request
424     * @return the session to send/receive data to/from the user
425     * @throws XMPPErrorException if the user doesn't support or accept in-band bytestreams, or if the
426     *         user prefers smaller block sizes
427     * @throws NoResponseException if there was no response from the server.
428     * @throws NotConnectedException 
429     * @throws InterruptedException 
430     */
431    @Override
432    public InBandBytestreamSession establishSession(Jid targetJID, String sessionID)
433                    throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
434        Open byteStreamRequest = new Open(sessionID, this.defaultBlockSize, this.stanza);
435        byteStreamRequest.setTo(targetJID);
436
437        final XMPPConnection connection = connection();
438
439        // sending packet will throw exception on timeout or error reply
440        connection.createStanzaCollectorAndSend(byteStreamRequest).nextResultOrThrow();
441
442        InBandBytestreamSession inBandBytestreamSession = new InBandBytestreamSession(
443                        connection, byteStreamRequest, targetJID);
444        this.sessions.put(sessionID, inBandBytestreamSession);
445
446        return inBandBytestreamSession;
447    }
448
449    /**
450     * Responses to the given IQ packet's sender with an XMPP error that an In-Band Bytestream is
451     * not accepted.
452     * 
453     * @param request IQ stanza(/packet) that should be answered with a not-acceptable error
454     * @throws NotConnectedException 
455     * @throws InterruptedException 
456     */
457    protected void replyRejectPacket(IQ request) throws NotConnectedException, InterruptedException {
458        IQ error = IQ.createErrorResponse(request, XMPPError.Condition.not_acceptable);
459        connection().sendStanza(error);
460    }
461
462    /**
463     * Responses to the given IQ packet's sender with an XMPP error that an In-Band Bytestream open
464     * request is rejected because its block size is greater than the maximum allowed block size.
465     * 
466     * @param request IQ stanza(/packet) that should be answered with a resource-constraint error
467     * @throws NotConnectedException 
468     * @throws InterruptedException 
469     */
470    protected void replyResourceConstraintPacket(IQ request) throws NotConnectedException, InterruptedException {
471        IQ error = IQ.createErrorResponse(request, XMPPError.Condition.resource_constraint);
472        connection().sendStanza(error);
473    }
474
475    /**
476     * Responses to the given IQ packet's sender with an XMPP error that an In-Band Bytestream
477     * session could not be found.
478     * 
479     * @param request IQ stanza(/packet) that should be answered with a item-not-found error
480     * @throws NotConnectedException 
481     * @throws InterruptedException 
482     */
483    protected void replyItemNotFoundPacket(IQ request) throws NotConnectedException, InterruptedException {
484        IQ error = IQ.createErrorResponse(request, XMPPError.Condition.item_not_found);
485        connection().sendStanza(error);
486    }
487
488    /**
489     * Returns a new unique session ID.
490     * 
491     * @return a new unique session ID
492     */
493    private static String getNextSessionID() {
494        StringBuilder buffer = new StringBuilder();
495        buffer.append(SESSION_ID_PREFIX);
496        buffer.append(Math.abs(randomGenerator.nextLong()));
497        return buffer.toString();
498    }
499
500    /**
501     * Returns the XMPP connection.
502     * 
503     * @return the XMPP connection
504     */
505    protected XMPPConnection getConnection() {
506        return connection();
507    }
508
509    /**
510     * Returns the {@link InBandBytestreamListener} that should be informed if a In-Band Bytestream
511     * request from the given initiator JID is received.
512     * 
513     * @param initiator the initiator's JID
514     * @return the listener
515     */
516    protected BytestreamListener getUserListener(Jid initiator) {
517        return this.userListeners.get(initiator);
518    }
519
520    /**
521     * Returns a list of {@link InBandBytestreamListener} that are informed if there are no
522     * listeners for a specific initiator.
523     * 
524     * @return list of listeners
525     */
526    protected List<BytestreamListener> getAllRequestListeners() {
527        return this.allRequestListeners;
528    }
529
530    /**
531     * Returns the sessions map.
532     * 
533     * @return the sessions map
534     */
535    protected Map<String, InBandBytestreamSession> getSessions() {
536        return sessions;
537    }
538
539    /**
540     * Returns the list of session IDs that should be ignored by the InitialtionListener
541     * 
542     * @return list of session IDs
543     */
544    protected List<String> getIgnoredBytestreamRequests() {
545        return ignoredBytestreamRequests;
546    }
547
548    /**
549     * Disables the InBandBytestreamManager by removing its stanza(/packet) listeners and resetting its
550     * internal status, which includes removing this instance from the managers map.
551     */
552    private void disableService() {
553        final XMPPConnection connection = connection();
554
555        // remove manager from static managers map
556        managers.remove(connection);
557
558        // remove all listeners registered by this manager
559        connection.unregisterIQRequestHandler(initiationListener);
560        connection.unregisterIQRequestHandler(dataListener);
561        connection.unregisterIQRequestHandler(closeListener);
562
563        // shutdown threads
564        this.initiationListener.shutdown();
565
566        // reset internal status
567        this.userListeners.clear();
568        this.allRequestListeners.clear();
569        this.sessions.clear();
570        this.ignoredBytestreamRequests.clear();
571
572    }
573
574}