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