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.HashMap;
021import java.util.LinkedList;
022import java.util.List;
023import java.util.Map;
024import java.util.Random;
025import java.util.concurrent.ConcurrentHashMap;
026
027import org.jivesoftware.smack.AbstractConnectionClosedListener;
028import org.jivesoftware.smack.ConnectionCreationListener;
029import org.jivesoftware.smack.SmackException;
030import org.jivesoftware.smack.SmackException.NoResponseException;
031import org.jivesoftware.smack.SmackException.NotConnectedException;
032import org.jivesoftware.smack.XMPPConnection;
033import org.jivesoftware.smack.XMPPConnectionRegistry;
034import org.jivesoftware.smack.XMPPException;
035import org.jivesoftware.smack.XMPPException.XMPPErrorException;
036import org.jivesoftware.smack.packet.IQ;
037import org.jivesoftware.smack.packet.XMPPError;
038import org.jivesoftware.smackx.bytestreams.BytestreamListener;
039import org.jivesoftware.smackx.bytestreams.BytestreamManager;
040import org.jivesoftware.smackx.bytestreams.ibb.packet.Open;
041import org.jivesoftware.smackx.filetransfer.FileTransferManager;
042import org.jxmpp.jid.Jid;
043
044/**
045 * The InBandBytestreamManager class handles establishing In-Band Bytestreams as specified in the <a
046 * href="http://xmpp.org/extensions/xep-0047.html">XEP-0047</a>.
047 * <p>
048 * The In-Band Bytestreams (IBB) enables two entities to establish a virtual bytestream over which
049 * they can exchange Base64-encoded chunks of data over XMPP itself. It is the fall-back mechanism
050 * in case the Socks5 bytestream method of transferring data is not available.
051 * <p>
052 * There are two ways to send data over an In-Band Bytestream. It could either use IQ stanzas to
053 * send data packets or message stanzas. If IQ stanzas are used every data stanza(/packet) is acknowledged by
054 * the receiver. This is the recommended way to avoid possible rate-limiting penalties. Message
055 * stanzas are not acknowledged because most XMPP server implementation don't support stanza
056 * flow-control method like <a href="http://xmpp.org/extensions/xep-0079.html">Advanced Message
057 * Processing</a>. To set the stanza that should be used invoke {@link #setStanza(StanzaType)}.
058 * <p>
059 * To establish an In-Band Bytestream invoke the {@link #establishSession(Jid)} method. This will
060 * negotiate an in-band bytestream with the given target JID and return a session.
061 * <p>
062 * If a session ID for the In-Band Bytestream was already negotiated (e.g. while negotiating a file
063 * transfer) invoke {@link #establishSession(Jid, String)}.
064 * <p>
065 * To handle incoming In-Band Bytestream requests add an {@link InBandBytestreamListener} to the
066 * manager. There are two ways to add this listener. If you want to be informed about incoming
067 * In-Band Bytestreams from a specific user add the listener by invoking
068 * {@link #addIncomingBytestreamListener(BytestreamListener, Jid)}. If the listener should
069 * respond to all In-Band Bytestream requests invoke
070 * {@link #addIncomingBytestreamListener(BytestreamListener)}.
071 * <p>
072 * Note that the registered {@link InBandBytestreamListener} will NOT be notified on incoming
073 * In-Band bytestream requests sent in the context of <a
074 * href="http://xmpp.org/extensions/xep-0096.html">XEP-0096</a> file transfer. (See
075 * {@link FileTransferManager})
076 * <p>
077 * If no {@link InBandBytestreamListener}s are registered, all incoming In-Band bytestream requests
078 * will be rejected by returning a &lt;not-acceptable/&gt; error to the initiator.
079 * 
080 * @author Henning Staib
081 */
082public final class InBandBytestreamManager implements BytestreamManager {
083
084    /**
085     * Stanzas that can be used to encapsulate In-Band Bytestream data packets.
086     */
087    public enum StanzaType {
088
089        /**
090         * IQ stanza.
091         */
092        IQ,
093
094        /**
095         * Message stanza.
096         */
097        MESSAGE
098    }
099
100    /*
101     * create a new InBandBytestreamManager and register its shutdown listener on every established
102     * connection
103     */
104    static {
105        XMPPConnectionRegistry.addConnectionCreationListener(new ConnectionCreationListener() {
106            @Override
107            public void connectionCreated(final XMPPConnection connection) {
108                // create the manager for this connection
109                InBandBytestreamManager.getByteStreamManager(connection);
110
111                // register shutdown listener
112                connection.addConnectionListener(new AbstractConnectionClosedListener() {
113
114                    @Override
115                    public void connectionTerminated() {
116                        InBandBytestreamManager.getByteStreamManager(connection).disableService();
117                    }
118
119                    @Override
120                    public void reconnectionSuccessful() {
121                        // re-create the manager for this connection
122                        InBandBytestreamManager.getByteStreamManager(connection);
123                    }
124
125                });
126
127            }
128        });
129    }
130
131    /**
132     * Maximum block size that is allowed for In-Band Bytestreams.
133     */
134    public static final int MAXIMUM_BLOCK_SIZE = 65535;
135
136    /* prefix used to generate session IDs */
137    private static final String SESSION_ID_PREFIX = "jibb_";
138
139    /* random generator to create session IDs */
140    private final static Random randomGenerator = new Random();
141
142    /* stores one InBandBytestreamManager for each XMPP connection */
143    private final static Map<XMPPConnection, InBandBytestreamManager> managers = new HashMap<XMPPConnection, InBandBytestreamManager>();
144
145    /* XMPP connection */
146    private final XMPPConnection connection;
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        this.connection = 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        // sending packet will throw exception on timeout or error reply
438        connection.createStanzaCollectorAndSend(byteStreamRequest).nextResultOrThrow();
439
440        InBandBytestreamSession inBandBytestreamSession = new InBandBytestreamSession(
441                        this.connection, byteStreamRequest, targetJID);
442        this.sessions.put(sessionID, inBandBytestreamSession);
443
444        return inBandBytestreamSession;
445    }
446
447    /**
448     * Responses to the given IQ packet's sender with an XMPP error that an In-Band Bytestream is
449     * not accepted.
450     * 
451     * @param request IQ stanza(/packet) that should be answered with a not-acceptable error
452     * @throws NotConnectedException 
453     * @throws InterruptedException 
454     */
455    protected void replyRejectPacket(IQ request) throws NotConnectedException, InterruptedException {
456        IQ error = IQ.createErrorResponse(request, XMPPError.Condition.not_acceptable);
457        this.connection.sendStanza(error);
458    }
459
460    /**
461     * Responses to the given IQ packet's sender with an XMPP error that an In-Band Bytestream open
462     * request is rejected because its block size is greater than the maximum allowed block size.
463     * 
464     * @param request IQ stanza(/packet) that should be answered with a resource-constraint error
465     * @throws NotConnectedException 
466     * @throws InterruptedException 
467     */
468    protected void replyResourceConstraintPacket(IQ request) throws NotConnectedException, InterruptedException {
469        IQ error = IQ.createErrorResponse(request, XMPPError.Condition.resource_constraint);
470        this.connection.sendStanza(error);
471    }
472
473    /**
474     * Responses to the given IQ packet's sender with an XMPP error that an In-Band Bytestream
475     * session could not be found.
476     * 
477     * @param request IQ stanza(/packet) that should be answered with a item-not-found error
478     * @throws NotConnectedException 
479     * @throws InterruptedException 
480     */
481    protected void replyItemNotFoundPacket(IQ request) throws NotConnectedException, InterruptedException {
482        IQ error = IQ.createErrorResponse(request, XMPPError.Condition.item_not_found);
483        this.connection.sendStanza(error);
484    }
485
486    /**
487     * Returns a new unique session ID.
488     * 
489     * @return a new unique session ID
490     */
491    private static String getNextSessionID() {
492        StringBuilder buffer = new StringBuilder();
493        buffer.append(SESSION_ID_PREFIX);
494        buffer.append(Math.abs(randomGenerator.nextLong()));
495        return buffer.toString();
496    }
497
498    /**
499     * Returns the XMPP connection.
500     * 
501     * @return the XMPP connection
502     */
503    protected XMPPConnection getConnection() {
504        return this.connection;
505    }
506
507    /**
508     * Returns the {@link InBandBytestreamListener} that should be informed if a In-Band Bytestream
509     * request from the given initiator JID is received.
510     * 
511     * @param initiator the initiator's JID
512     * @return the listener
513     */
514    protected BytestreamListener getUserListener(Jid initiator) {
515        return this.userListeners.get(initiator);
516    }
517
518    /**
519     * Returns a list of {@link InBandBytestreamListener} that are informed if there are no
520     * listeners for a specific initiator.
521     * 
522     * @return list of listeners
523     */
524    protected List<BytestreamListener> getAllRequestListeners() {
525        return this.allRequestListeners;
526    }
527
528    /**
529     * Returns the sessions map.
530     * 
531     * @return the sessions map
532     */
533    protected Map<String, InBandBytestreamSession> getSessions() {
534        return sessions;
535    }
536
537    /**
538     * Returns the list of session IDs that should be ignored by the InitialtionListener
539     * 
540     * @return list of session IDs
541     */
542    protected List<String> getIgnoredBytestreamRequests() {
543        return ignoredBytestreamRequests;
544    }
545
546    /**
547     * Disables the InBandBytestreamManager by removing its stanza(/packet) listeners and resetting its
548     * internal status, which includes removing this instance from the managers map.
549     */
550    private void disableService() {
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}