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 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 final static Random randomGenerator = new Random();
143
144    /* stores one InBandBytestreamManager for each XMPP connection */
145    private final static 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    // TODO: Change argument to Jid in Smack 4.3.
285    @SuppressWarnings("CollectionIncompatibleType")
286    public void removeIncomingBytestreamListener(String initiatorJID) {
287        this.userListeners.remove(initiatorJID);
288    }
289
290    /**
291     * Use this method to ignore the next incoming In-Band Bytestream request containing the given
292     * session ID. No listeners will be notified for this request and and no error will be returned
293     * to the initiator.
294     * <p>
295     * This method should be used if you are awaiting an In-Band Bytestream request as a reply to
296     * another stanza(/packet) (e.g. file transfer).
297     * 
298     * @param sessionID to be ignored
299     */
300    public void ignoreBytestreamRequestOnce(String sessionID) {
301        this.ignoredBytestreamRequests.add(sessionID);
302    }
303
304    /**
305     * Returns the default block size that is used for all outgoing in-band bytestreams for this
306     * connection.
307     * <p>
308     * The recommended default block size is 4096 bytes. See <a
309     * href="http://xmpp.org/extensions/xep-0047.html#usage">XEP-0047</a> Section 5.
310     * 
311     * @return the default block size
312     */
313    public int getDefaultBlockSize() {
314        return defaultBlockSize;
315    }
316
317    /**
318     * Sets the default block size that is used for all outgoing in-band bytestreams for this
319     * connection.
320     * <p>
321     * The default block size must be between 1 and 65535 bytes. The recommended default block size
322     * is 4096 bytes. See <a href="http://xmpp.org/extensions/xep-0047.html#usage">XEP-0047</a>
323     * Section 5.
324     * 
325     * @param defaultBlockSize the default block size to set
326     */
327    public void setDefaultBlockSize(int defaultBlockSize) {
328        if (defaultBlockSize <= 0 || defaultBlockSize > MAXIMUM_BLOCK_SIZE) {
329            throw new IllegalArgumentException("Default block size must be between 1 and "
330                            + MAXIMUM_BLOCK_SIZE);
331        }
332        this.defaultBlockSize = defaultBlockSize;
333    }
334
335    /**
336     * Returns the maximum block size that is allowed for In-Band Bytestreams for this connection.
337     * <p>
338     * Incoming In-Band Bytestream open request will be rejected with an
339     * &lt;resource-constraint/&gt; error if the block size is greater then the maximum allowed
340     * block size.
341     * <p>
342     * The default maximum block size is 65535 bytes.
343     * 
344     * @return the maximum block size
345     */
346    public int getMaximumBlockSize() {
347        return maximumBlockSize;
348    }
349
350    /**
351     * Sets the maximum block size that is allowed for In-Band Bytestreams for this connection.
352     * <p>
353     * The maximum block size must be between 1 and 65535 bytes.
354     * <p>
355     * Incoming In-Band Bytestream open request will be rejected with an
356     * &lt;resource-constraint/&gt; error if the block size is greater then the maximum allowed
357     * block size.
358     * 
359     * @param maximumBlockSize the maximum block size to set
360     */
361    public void setMaximumBlockSize(int maximumBlockSize) {
362        if (maximumBlockSize <= 0 || maximumBlockSize > MAXIMUM_BLOCK_SIZE) {
363            throw new IllegalArgumentException("Maximum block size must be between 1 and "
364                            + MAXIMUM_BLOCK_SIZE);
365        }
366        this.maximumBlockSize = maximumBlockSize;
367    }
368
369    /**
370     * Returns the stanza used to send data packets.
371     * <p>
372     * Default is {@link StanzaType#IQ}. See <a
373     * href="http://xmpp.org/extensions/xep-0047.html#message">XEP-0047</a> Section 4.
374     * 
375     * @return the stanza used to send data packets
376     */
377    public StanzaType getStanza() {
378        return stanza;
379    }
380
381    /**
382     * Sets the stanza used to send data packets.
383     * <p>
384     * The use of {@link StanzaType#IQ} is recommended. See <a
385     * href="http://xmpp.org/extensions/xep-0047.html#message">XEP-0047</a> Section 4.
386     * 
387     * @param stanza the stanza to set
388     */
389    public void setStanza(StanzaType stanza) {
390        this.stanza = stanza;
391    }
392
393    /**
394     * Establishes an In-Band Bytestream with the given user and returns the session to send/receive
395     * data to/from the user.
396     * <p>
397     * Use this method to establish In-Band Bytestreams to users accepting all incoming In-Band
398     * Bytestream requests since this method doesn't provide a way to tell the user something about
399     * the data to be sent.
400     * <p>
401     * To establish an In-Band Bytestream after negotiation the kind of data to be sent (e.g. file
402     * transfer) use {@link #establishSession(Jid, String)}.
403     * 
404     * @param targetJID the JID of the user an In-Band Bytestream should be established
405     * @return the session to send/receive data to/from the user
406     * @throws XMPPException if the user doesn't support or accept in-band bytestreams, or if the
407     *         user prefers smaller block sizes
408     * @throws SmackException if there was no response from the server.
409     * @throws InterruptedException 
410     */
411    @Override
412    public InBandBytestreamSession establishSession(Jid targetJID) throws XMPPException, SmackException, InterruptedException {
413        String sessionID = getNextSessionID();
414        return establishSession(targetJID, sessionID);
415    }
416
417    /**
418     * Establishes an In-Band Bytestream with the given user using the given session ID and returns
419     * the session to send/receive data to/from the user.
420     * 
421     * @param targetJID the JID of the user an In-Band Bytestream should be established
422     * @param sessionID the session ID for the In-Band Bytestream request
423     * @return the session to send/receive data to/from the user
424     * @throws XMPPErrorException if the user doesn't support or accept in-band bytestreams, or if the
425     *         user prefers smaller block sizes
426     * @throws NoResponseException if there was no response from the server.
427     * @throws NotConnectedException 
428     * @throws InterruptedException 
429     */
430    @Override
431    public InBandBytestreamSession establishSession(Jid targetJID, String sessionID)
432                    throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
433        Open byteStreamRequest = new Open(sessionID, this.defaultBlockSize, this.stanza);
434        byteStreamRequest.setTo(targetJID);
435
436        final XMPPConnection connection = connection();
437
438        // sending packet will throw exception on timeout or error reply
439        connection.createStanzaCollectorAndSend(byteStreamRequest).nextResultOrThrow();
440
441        InBandBytestreamSession inBandBytestreamSession = new InBandBytestreamSession(
442                        connection, byteStreamRequest, targetJID);
443        this.sessions.put(sessionID, inBandBytestreamSession);
444
445        return inBandBytestreamSession;
446    }
447
448    /**
449     * Responses to the given IQ packet's sender with an XMPP error that an In-Band Bytestream is
450     * not accepted.
451     * 
452     * @param request IQ stanza(/packet) that should be answered with a not-acceptable error
453     * @throws NotConnectedException 
454     * @throws InterruptedException 
455     */
456    protected void replyRejectPacket(IQ request) throws NotConnectedException, InterruptedException {
457        IQ error = IQ.createErrorResponse(request, XMPPError.Condition.not_acceptable);
458        connection().sendStanza(error);
459    }
460
461    /**
462     * Responses to the given IQ packet's sender with an XMPP error that an In-Band Bytestream open
463     * request is rejected because its block size is greater than the maximum allowed block size.
464     * 
465     * @param request IQ stanza(/packet) that should be answered with a resource-constraint error
466     * @throws NotConnectedException 
467     * @throws InterruptedException 
468     */
469    protected void replyResourceConstraintPacket(IQ request) throws NotConnectedException, InterruptedException {
470        IQ error = IQ.createErrorResponse(request, XMPPError.Condition.resource_constraint);
471        connection().sendStanza(error);
472    }
473
474    /**
475     * Responses to the given IQ packet's sender with an XMPP error that an In-Band Bytestream
476     * session could not be found.
477     * 
478     * @param request IQ stanza(/packet) that should be answered with a item-not-found error
479     * @throws NotConnectedException 
480     * @throws InterruptedException 
481     */
482    protected void replyItemNotFoundPacket(IQ request) throws NotConnectedException, InterruptedException {
483        IQ error = IQ.createErrorResponse(request, XMPPError.Condition.item_not_found);
484        connection().sendStanza(error);
485    }
486
487    /**
488     * Returns a new unique session ID.
489     * 
490     * @return a new unique session ID
491     */
492    private static String getNextSessionID() {
493        StringBuilder buffer = new StringBuilder();
494        buffer.append(SESSION_ID_PREFIX);
495        buffer.append(Math.abs(randomGenerator.nextLong()));
496        return buffer.toString();
497    }
498
499    /**
500     * Returns the XMPP connection.
501     * 
502     * @return the XMPP connection
503     */
504    protected XMPPConnection getConnection() {
505        return connection();
506    }
507
508    /**
509     * Returns the {@link InBandBytestreamListener} that should be informed if a In-Band Bytestream
510     * request from the given initiator JID is received.
511     * 
512     * @param initiator the initiator's JID
513     * @return the listener
514     */
515    protected BytestreamListener getUserListener(Jid initiator) {
516        return this.userListeners.get(initiator);
517    }
518
519    /**
520     * Returns a list of {@link InBandBytestreamListener} that are informed if there are no
521     * listeners for a specific initiator.
522     * 
523     * @return list of listeners
524     */
525    protected List<BytestreamListener> getAllRequestListeners() {
526        return this.allRequestListeners;
527    }
528
529    /**
530     * Returns the sessions map.
531     * 
532     * @return the sessions map
533     */
534    protected Map<String, InBandBytestreamSession> getSessions() {
535        return sessions;
536    }
537
538    /**
539     * Returns the list of session IDs that should be ignored by the InitialtionListener
540     * 
541     * @return list of session IDs
542     */
543    protected List<String> getIgnoredBytestreamRequests() {
544        return ignoredBytestreamRequests;
545    }
546
547    /**
548     * Disables the InBandBytestreamManager by removing its stanza(/packet) listeners and resetting its
549     * internal status, which includes removing this instance from the managers map.
550     */
551    private void disableService() {
552        final XMPPConnection connection = connection();
553
554        // remove manager from static managers map
555        managers.remove(connection);
556
557        // remove all listeners registered by this manager
558        connection.unregisterIQRequestHandler(initiationListener);
559        connection.unregisterIQRequestHandler(dataListener);
560        connection.unregisterIQRequestHandler(closeListener);
561
562        // shutdown threads
563        this.initiationListener.shutdown();
564
565        // reset internal status
566        this.userListeners.clear();
567        this.allRequestListeners.clear();
568        this.sessions.clear();
569        this.ignoredBytestreamRequests.clear();
570
571    }
572
573}