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