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