Home | History | Annotate | Download | only in ibb
      1 /**
      2  * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License");
      3  * you may not use this file except in compliance with the License.
      4  * You may obtain a copy of the License at
      5  *
      6  *     http://www.apache.org/licenses/LICENSE-2.0
      7  *
      8  * Unless required by applicable law or agreed to in writing, software
      9  * distributed under the License is distributed on an "AS IS" BASIS,
     10  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     11  * See the License for the specific language governing permissions and
     12  * limitations under the License.
     13  */
     14 package org.jivesoftware.smackx.bytestreams.ibb;
     15 
     16 import java.util.Collections;
     17 import java.util.HashMap;
     18 import java.util.LinkedList;
     19 import java.util.List;
     20 import java.util.Map;
     21 import java.util.Random;
     22 import java.util.concurrent.ConcurrentHashMap;
     23 
     24 import org.jivesoftware.smack.AbstractConnectionListener;
     25 import org.jivesoftware.smack.Connection;
     26 import org.jivesoftware.smack.ConnectionCreationListener;
     27 import org.jivesoftware.smack.XMPPException;
     28 import org.jivesoftware.smack.packet.IQ;
     29 import org.jivesoftware.smack.packet.XMPPError;
     30 import org.jivesoftware.smack.util.SyncPacketSend;
     31 import org.jivesoftware.smackx.bytestreams.BytestreamListener;
     32 import org.jivesoftware.smackx.bytestreams.BytestreamManager;
     33 import org.jivesoftware.smackx.bytestreams.ibb.packet.Open;
     34 import org.jivesoftware.smackx.filetransfer.FileTransferManager;
     35 
     36 /**
     37  * The InBandBytestreamManager class handles establishing In-Band Bytestreams as specified in the <a
     38  * href="http://xmpp.org/extensions/xep-0047.html">XEP-0047</a>.
     39  * <p>
     40  * The In-Band Bytestreams (IBB) enables two entities to establish a virtual bytestream over which
     41  * they can exchange Base64-encoded chunks of data over XMPP itself. It is the fall-back mechanism
     42  * in case the Socks5 bytestream method of transferring data is not available.
     43  * <p>
     44  * There are two ways to send data over an In-Band Bytestream. It could either use IQ stanzas to
     45  * send data packets or message stanzas. If IQ stanzas are used every data packet is acknowledged by
     46  * the receiver. This is the recommended way to avoid possible rate-limiting penalties. Message
     47  * stanzas are not acknowledged because most XMPP server implementation don't support stanza
     48  * flow-control method like <a href="http://xmpp.org/extensions/xep-0079.html">Advanced Message
     49  * Processing</a>. To set the stanza that should be used invoke {@link #setStanza(StanzaType)}.
     50  * <p>
     51  * To establish an In-Band Bytestream invoke the {@link #establishSession(String)} method. This will
     52  * negotiate an in-band bytestream with the given target JID and return a session.
     53  * <p>
     54  * If a session ID for the In-Band Bytestream was already negotiated (e.g. while negotiating a file
     55  * transfer) invoke {@link #establishSession(String, String)}.
     56  * <p>
     57  * To handle incoming In-Band Bytestream requests add an {@link InBandBytestreamListener} to the
     58  * manager. There are two ways to add this listener. If you want to be informed about incoming
     59  * In-Band Bytestreams from a specific user add the listener by invoking
     60  * {@link #addIncomingBytestreamListener(BytestreamListener, String)}. If the listener should
     61  * respond to all In-Band Bytestream requests invoke
     62  * {@link #addIncomingBytestreamListener(BytestreamListener)}.
     63  * <p>
     64  * Note that the registered {@link InBandBytestreamListener} will NOT be notified on incoming
     65  * In-Band bytestream requests sent in the context of <a
     66  * href="http://xmpp.org/extensions/xep-0096.html">XEP-0096</a> file transfer. (See
     67  * {@link FileTransferManager})
     68  * <p>
     69  * If no {@link InBandBytestreamListener}s are registered, all incoming In-Band bytestream requests
     70  * will be rejected by returning a &lt;not-acceptable/&gt; error to the initiator.
     71  *
     72  * @author Henning Staib
     73  */
     74 public class InBandBytestreamManager implements BytestreamManager {
     75 
     76     /**
     77      * Stanzas that can be used to encapsulate In-Band Bytestream data packets.
     78      */
     79     public enum StanzaType {
     80 
     81         /**
     82          * IQ stanza.
     83          */
     84         IQ,
     85 
     86         /**
     87          * Message stanza.
     88          */
     89         MESSAGE
     90     }
     91 
     92     /*
     93      * create a new InBandBytestreamManager and register its shutdown listener on every established
     94      * connection
     95      */
     96     static {
     97         Connection.addConnectionCreationListener(new ConnectionCreationListener() {
     98             public void connectionCreated(Connection connection) {
     99                 final InBandBytestreamManager manager;
    100                 manager = InBandBytestreamManager.getByteStreamManager(connection);
    101 
    102                 // register shutdown listener
    103                 connection.addConnectionListener(new AbstractConnectionListener() {
    104 
    105                     public void connectionClosed() {
    106                         manager.disableService();
    107                     }
    108 
    109                 });
    110 
    111             }
    112         });
    113     }
    114 
    115     /**
    116      * The XMPP namespace of the In-Band Bytestream
    117      */
    118     public static final String NAMESPACE = "http://jabber.org/protocol/ibb";
    119 
    120     /**
    121      * Maximum block size that is allowed for In-Band Bytestreams
    122      */
    123     public static final int MAXIMUM_BLOCK_SIZE = 65535;
    124 
    125     /* prefix used to generate session IDs */
    126     private static final String SESSION_ID_PREFIX = "jibb_";
    127 
    128     /* random generator to create session IDs */
    129     private final static Random randomGenerator = new Random();
    130 
    131     /* stores one InBandBytestreamManager for each XMPP connection */
    132     private final static Map<Connection, InBandBytestreamManager> managers = new HashMap<Connection, InBandBytestreamManager>();
    133 
    134     /* XMPP connection */
    135     private final Connection connection;
    136 
    137     /*
    138      * assigns a user to a listener that is informed if an In-Band Bytestream request for this user
    139      * is received
    140      */
    141     private final Map<String, BytestreamListener> userListeners = new ConcurrentHashMap<String, BytestreamListener>();
    142 
    143     /*
    144      * list of listeners that respond to all In-Band Bytestream requests if there are no user
    145      * specific listeners for that request
    146      */
    147     private final List<BytestreamListener> allRequestListeners = Collections.synchronizedList(new LinkedList<BytestreamListener>());
    148 
    149     /* listener that handles all incoming In-Band Bytestream requests */
    150     private final InitiationListener initiationListener;
    151 
    152     /* listener that handles all incoming In-Band Bytestream IQ data packets */
    153     private final DataListener dataListener;
    154 
    155     /* listener that handles all incoming In-Band Bytestream close requests */
    156     private final CloseListener closeListener;
    157 
    158     /* assigns a session ID to the In-Band Bytestream session */
    159     private final Map<String, InBandBytestreamSession> sessions = new ConcurrentHashMap<String, InBandBytestreamSession>();
    160 
    161     /* block size used for new In-Band Bytestreams */
    162     private int defaultBlockSize = 4096;
    163 
    164     /* maximum block size allowed for this connection */
    165     private int maximumBlockSize = MAXIMUM_BLOCK_SIZE;
    166 
    167     /* the stanza used to send data packets */
    168     private StanzaType stanza = StanzaType.IQ;
    169 
    170     /*
    171      * list containing session IDs of In-Band Bytestream open packets that should be ignored by the
    172      * InitiationListener
    173      */
    174     private List<String> ignoredBytestreamRequests = Collections.synchronizedList(new LinkedList<String>());
    175 
    176     /**
    177      * Returns the InBandBytestreamManager to handle In-Band Bytestreams for a given
    178      * {@link Connection}.
    179      *
    180      * @param connection the XMPP connection
    181      * @return the InBandBytestreamManager for the given XMPP connection
    182      */
    183     public static synchronized InBandBytestreamManager getByteStreamManager(Connection connection) {
    184         if (connection == null)
    185             return null;
    186         InBandBytestreamManager manager = managers.get(connection);
    187         if (manager == null) {
    188             manager = new InBandBytestreamManager(connection);
    189             managers.put(connection, manager);
    190         }
    191         return manager;
    192     }
    193 
    194     /**
    195      * Constructor.
    196      *
    197      * @param connection the XMPP connection
    198      */
    199     private InBandBytestreamManager(Connection connection) {
    200         this.connection = connection;
    201 
    202         // register bytestream open packet listener
    203         this.initiationListener = new InitiationListener(this);
    204         this.connection.addPacketListener(this.initiationListener,
    205                         this.initiationListener.getFilter());
    206 
    207         // register bytestream data packet listener
    208         this.dataListener = new DataListener(this);
    209         this.connection.addPacketListener(this.dataListener, this.dataListener.getFilter());
    210 
    211         // register bytestream close packet listener
    212         this.closeListener = new CloseListener(this);
    213         this.connection.addPacketListener(this.closeListener, this.closeListener.getFilter());
    214 
    215     }
    216 
    217     /**
    218      * Adds InBandBytestreamListener that is called for every incoming in-band bytestream request
    219      * unless there is a user specific InBandBytestreamListener registered.
    220      * <p>
    221      * If no listeners are registered all In-Band Bytestream request are rejected with a
    222      * &lt;not-acceptable/&gt; error.
    223      * <p>
    224      * Note that the registered {@link InBandBytestreamListener} will NOT be notified on incoming
    225      * Socks5 bytestream requests sent in the context of <a
    226      * href="http://xmpp.org/extensions/xep-0096.html">XEP-0096</a> file transfer. (See
    227      * {@link FileTransferManager})
    228      *
    229      * @param listener the listener to register
    230      */
    231     public void addIncomingBytestreamListener(BytestreamListener listener) {
    232         this.allRequestListeners.add(listener);
    233     }
    234 
    235     /**
    236      * Removes the given listener from the list of listeners for all incoming In-Band Bytestream
    237      * requests.
    238      *
    239      * @param listener the listener to remove
    240      */
    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     public void addIncomingBytestreamListener(BytestreamListener listener, String initiatorJID) {
    264         this.userListeners.put(initiatorJID, listener);
    265     }
    266 
    267     /**
    268      * Removes the listener for the given user.
    269      *
    270      * @param initiatorJID the JID of the user the listener should be removed
    271      */
    272     public void removeIncomingBytestreamListener(String initiatorJID) {
    273         this.userListeners.remove(initiatorJID);
    274     }
    275 
    276     /**
    277      * Use this method to ignore the next incoming In-Band Bytestream request containing the given
    278      * session ID. No listeners will be notified for this request and and no error will be returned
    279      * to the initiator.
    280      * <p>
    281      * This method should be used if you are awaiting an In-Band Bytestream request as a reply to
    282      * another packet (e.g. file transfer).
    283      *
    284      * @param sessionID to be ignored
    285      */
    286     public void ignoreBytestreamRequestOnce(String sessionID) {
    287         this.ignoredBytestreamRequests.add(sessionID);
    288     }
    289 
    290     /**
    291      * Returns the default block size that is used for all outgoing in-band bytestreams for this
    292      * connection.
    293      * <p>
    294      * The recommended default block size is 4096 bytes. See <a
    295      * href="http://xmpp.org/extensions/xep-0047.html#usage">XEP-0047</a> Section 5.
    296      *
    297      * @return the default block size
    298      */
    299     public int getDefaultBlockSize() {
    300         return defaultBlockSize;
    301     }
    302 
    303     /**
    304      * Sets the default block size that is used for all outgoing in-band bytestreams for this
    305      * connection.
    306      * <p>
    307      * The default block size must be between 1 and 65535 bytes. The recommended default block size
    308      * is 4096 bytes. See <a href="http://xmpp.org/extensions/xep-0047.html#usage">XEP-0047</a>
    309      * Section 5.
    310      *
    311      * @param defaultBlockSize the default block size to set
    312      */
    313     public void setDefaultBlockSize(int defaultBlockSize) {
    314         if (defaultBlockSize <= 0 || defaultBlockSize > MAXIMUM_BLOCK_SIZE) {
    315             throw new IllegalArgumentException("Default block size must be between 1 and "
    316                             + MAXIMUM_BLOCK_SIZE);
    317         }
    318         this.defaultBlockSize = defaultBlockSize;
    319     }
    320 
    321     /**
    322      * Returns the maximum block size that is allowed for In-Band Bytestreams for this connection.
    323      * <p>
    324      * Incoming In-Band Bytestream open request will be rejected with an
    325      * &lt;resource-constraint/&gt; error if the block size is greater then the maximum allowed
    326      * block size.
    327      * <p>
    328      * The default maximum block size is 65535 bytes.
    329      *
    330      * @return the maximum block size
    331      */
    332     public int getMaximumBlockSize() {
    333         return maximumBlockSize;
    334     }
    335 
    336     /**
    337      * Sets the maximum block size that is allowed for In-Band Bytestreams for this connection.
    338      * <p>
    339      * The maximum block size must be between 1 and 65535 bytes.
    340      * <p>
    341      * Incoming In-Band Bytestream open request will be rejected with an
    342      * &lt;resource-constraint/&gt; error if the block size is greater then the maximum allowed
    343      * block size.
    344      *
    345      * @param maximumBlockSize the maximum block size to set
    346      */
    347     public void setMaximumBlockSize(int maximumBlockSize) {
    348         if (maximumBlockSize <= 0 || maximumBlockSize > MAXIMUM_BLOCK_SIZE) {
    349             throw new IllegalArgumentException("Maximum block size must be between 1 and "
    350                             + MAXIMUM_BLOCK_SIZE);
    351         }
    352         this.maximumBlockSize = maximumBlockSize;
    353     }
    354 
    355     /**
    356      * Returns the stanza used to send data packets.
    357      * <p>
    358      * Default is {@link StanzaType#IQ}. See <a
    359      * href="http://xmpp.org/extensions/xep-0047.html#message">XEP-0047</a> Section 4.
    360      *
    361      * @return the stanza used to send data packets
    362      */
    363     public StanzaType getStanza() {
    364         return stanza;
    365     }
    366 
    367     /**
    368      * Sets the stanza used to send data packets.
    369      * <p>
    370      * The use of {@link StanzaType#IQ} is recommended. See <a
    371      * href="http://xmpp.org/extensions/xep-0047.html#message">XEP-0047</a> Section 4.
    372      *
    373      * @param stanza the stanza to set
    374      */
    375     public void setStanza(StanzaType stanza) {
    376         this.stanza = stanza;
    377     }
    378 
    379     /**
    380      * Establishes an In-Band Bytestream with the given user and returns the session to send/receive
    381      * data to/from the user.
    382      * <p>
    383      * Use this method to establish In-Band Bytestreams to users accepting all incoming In-Band
    384      * Bytestream requests since this method doesn't provide a way to tell the user something about
    385      * the data to be sent.
    386      * <p>
    387      * To establish an In-Band Bytestream after negotiation the kind of data to be sent (e.g. file
    388      * transfer) use {@link #establishSession(String, String)}.
    389      *
    390      * @param targetJID the JID of the user an In-Band Bytestream should be established
    391      * @return the session to send/receive data to/from the user
    392      * @throws XMPPException if the user doesn't support or accept in-band bytestreams, or if the
    393      *         user prefers smaller block sizes
    394      */
    395     public InBandBytestreamSession establishSession(String targetJID) throws XMPPException {
    396         String sessionID = getNextSessionID();
    397         return establishSession(targetJID, sessionID);
    398     }
    399 
    400     /**
    401      * Establishes an In-Band Bytestream with the given user using the given session ID and returns
    402      * the session to send/receive data to/from the user.
    403      *
    404      * @param targetJID the JID of the user an In-Band Bytestream should be established
    405      * @param sessionID the session ID for the In-Band Bytestream request
    406      * @return the session to send/receive data to/from the user
    407      * @throws XMPPException if the user doesn't support or accept in-band bytestreams, or if the
    408      *         user prefers smaller block sizes
    409      */
    410     public InBandBytestreamSession establishSession(String targetJID, String sessionID)
    411                     throws XMPPException {
    412         Open byteStreamRequest = new Open(sessionID, this.defaultBlockSize, this.stanza);
    413         byteStreamRequest.setTo(targetJID);
    414 
    415         // sending packet will throw exception on timeout or error reply
    416         SyncPacketSend.getReply(this.connection, byteStreamRequest);
    417 
    418         InBandBytestreamSession inBandBytestreamSession = new InBandBytestreamSession(
    419                         this.connection, byteStreamRequest, targetJID);
    420         this.sessions.put(sessionID, inBandBytestreamSession);
    421 
    422         return inBandBytestreamSession;
    423     }
    424 
    425     /**
    426      * Responses to the given IQ packet's sender with an XMPP error that an In-Band Bytestream is
    427      * not accepted.
    428      *
    429      * @param request IQ packet that should be answered with a not-acceptable error
    430      */
    431     protected void replyRejectPacket(IQ request) {
    432         XMPPError xmppError = new XMPPError(XMPPError.Condition.no_acceptable);
    433         IQ error = IQ.createErrorResponse(request, xmppError);
    434         this.connection.sendPacket(error);
    435     }
    436 
    437     /**
    438      * Responses to the given IQ packet's sender with an XMPP error that an In-Band Bytestream open
    439      * request is rejected because its block size is greater than the maximum allowed block size.
    440      *
    441      * @param request IQ packet that should be answered with a resource-constraint error
    442      */
    443     protected void replyResourceConstraintPacket(IQ request) {
    444         XMPPError xmppError = new XMPPError(XMPPError.Condition.resource_constraint);
    445         IQ error = IQ.createErrorResponse(request, xmppError);
    446         this.connection.sendPacket(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 packet that should be answered with a item-not-found error
    454      */
    455     protected void replyItemNotFoundPacket(IQ request) {
    456         XMPPError xmppError = new XMPPError(XMPPError.Condition.item_not_found);
    457         IQ error = IQ.createErrorResponse(request, xmppError);
    458         this.connection.sendPacket(error);
    459     }
    460 
    461     /**
    462      * Returns a new unique session ID.
    463      *
    464      * @return a new unique session ID
    465      */
    466     private String getNextSessionID() {
    467         StringBuilder buffer = new StringBuilder();
    468         buffer.append(SESSION_ID_PREFIX);
    469         buffer.append(Math.abs(randomGenerator.nextLong()));
    470         return buffer.toString();
    471     }
    472 
    473     /**
    474      * Returns the XMPP connection.
    475      *
    476      * @return the XMPP connection
    477      */
    478     protected Connection getConnection() {
    479         return this.connection;
    480     }
    481 
    482     /**
    483      * Returns the {@link InBandBytestreamListener} that should be informed if a In-Band Bytestream
    484      * request from the given initiator JID is received.
    485      *
    486      * @param initiator the initiator's JID
    487      * @return the listener
    488      */
    489     protected BytestreamListener getUserListener(String initiator) {
    490         return this.userListeners.get(initiator);
    491     }
    492 
    493     /**
    494      * Returns a list of {@link InBandBytestreamListener} that are informed if there are no
    495      * listeners for a specific initiator.
    496      *
    497      * @return list of listeners
    498      */
    499     protected List<BytestreamListener> getAllRequestListeners() {
    500         return this.allRequestListeners;
    501     }
    502 
    503     /**
    504      * Returns the sessions map.
    505      *
    506      * @return the sessions map
    507      */
    508     protected Map<String, InBandBytestreamSession> getSessions() {
    509         return sessions;
    510     }
    511 
    512     /**
    513      * Returns the list of session IDs that should be ignored by the InitialtionListener
    514      *
    515      * @return list of session IDs
    516      */
    517     protected List<String> getIgnoredBytestreamRequests() {
    518         return ignoredBytestreamRequests;
    519     }
    520 
    521     /**
    522      * Disables the InBandBytestreamManager by removing its packet listeners and resetting its
    523      * internal status.
    524      */
    525     private void disableService() {
    526 
    527         // remove manager from static managers map
    528         managers.remove(connection);
    529 
    530         // remove all listeners registered by this manager
    531         this.connection.removePacketListener(this.initiationListener);
    532         this.connection.removePacketListener(this.dataListener);
    533         this.connection.removePacketListener(this.closeListener);
    534 
    535         // shutdown threads
    536         this.initiationListener.shutdown();
    537 
    538         // reset internal status
    539         this.userListeners.clear();
    540         this.allRequestListeners.clear();
    541         this.sessions.clear();
    542         this.ignoredBytestreamRequests.clear();
    543 
    544     }
    545 
    546 }
    547