InBandBytestreamManager.java

  1. /**
  2.  *
  3.  * Copyright the original author or authors
  4.  *
  5.  * Licensed under the Apache License, Version 2.0 (the "License");
  6.  * you may not use this file except in compliance with the License.
  7.  * You may obtain a copy of the License at
  8.  *
  9.  *     http://www.apache.org/licenses/LICENSE-2.0
  10.  *
  11.  * Unless required by applicable law or agreed to in writing, software
  12.  * distributed under the License is distributed on an "AS IS" BASIS,
  13.  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14.  * See the License for the specific language governing permissions and
  15.  * limitations under the License.
  16.  */
  17. package org.jivesoftware.smackx.bytestreams.ibb;

  18. import java.util.Collections;
  19. import java.util.LinkedList;
  20. import java.util.List;
  21. import java.util.Map;
  22. import java.util.WeakHashMap;
  23. import java.util.concurrent.ConcurrentHashMap;

  24. import org.jivesoftware.smack.AbstractConnectionClosedListener;
  25. import org.jivesoftware.smack.ConnectionCreationListener;
  26. import org.jivesoftware.smack.Manager;
  27. import org.jivesoftware.smack.SmackException;
  28. import org.jivesoftware.smack.SmackException.NoResponseException;
  29. import org.jivesoftware.smack.SmackException.NotConnectedException;
  30. import org.jivesoftware.smack.XMPPConnection;
  31. import org.jivesoftware.smack.XMPPConnectionRegistry;
  32. import org.jivesoftware.smack.XMPPException;
  33. import org.jivesoftware.smack.XMPPException.XMPPErrorException;
  34. import org.jivesoftware.smack.packet.IQ;
  35. import org.jivesoftware.smack.packet.StanzaError;
  36. import org.jivesoftware.smack.util.StringUtils;

  37. import org.jivesoftware.smackx.bytestreams.BytestreamListener;
  38. import org.jivesoftware.smackx.bytestreams.BytestreamManager;
  39. import org.jivesoftware.smackx.bytestreams.ibb.packet.Open;
  40. import org.jivesoftware.smackx.filetransfer.FileTransferManager;

  41. import org.jxmpp.jid.Jid;

  42. /**
  43.  * The InBandBytestreamManager class handles establishing In-Band Bytestreams as specified in the <a
  44.  * href="http://xmpp.org/extensions/xep-0047.html">XEP-0047</a>.
  45.  * <p>
  46.  * The In-Band Bytestreams (IBB) enables two entities to establish a virtual bytestream over which
  47.  * they can exchange Base64-encoded chunks of data over XMPP itself. It is the fall-back mechanism
  48.  * in case the Socks5 bytestream method of transferring data is not available.
  49.  * <p>
  50.  * There are two ways to send data over an In-Band Bytestream. It could either use IQ stanzas to
  51.  * send data packets or message stanzas. If IQ stanzas are used every data stanza is acknowledged by
  52.  * the receiver. This is the recommended way to avoid possible rate-limiting penalties. Message
  53.  * stanzas are not acknowledged because most XMPP server implementation don't support stanza
  54.  * flow-control method like <a href="http://xmpp.org/extensions/xep-0079.html">Advanced Message
  55.  * Processing</a>. To set the stanza that should be used invoke {@link #setStanza(StanzaType)}.
  56.  * <p>
  57.  * To establish an In-Band Bytestream invoke the {@link #establishSession(Jid)} method. This will
  58.  * negotiate an in-band bytestream with the given target JID and return a session.
  59.  * <p>
  60.  * If a session ID for the In-Band Bytestream was already negotiated (e.g. while negotiating a file
  61.  * transfer) invoke {@link #establishSession(Jid, String)}.
  62.  * <p>
  63.  * To handle incoming In-Band Bytestream requests add an {@link InBandBytestreamListener} to the
  64.  * manager. There are two ways to add this listener. If you want to be informed about incoming
  65.  * In-Band Bytestreams from a specific user add the listener by invoking
  66.  * {@link #addIncomingBytestreamListener(BytestreamListener, Jid)}. If the listener should
  67.  * respond to all In-Band Bytestream requests invoke
  68.  * {@link #addIncomingBytestreamListener(BytestreamListener)}.
  69.  * <p>
  70.  * Note that the registered {@link InBandBytestreamListener} will NOT be notified on incoming
  71.  * In-Band bytestream requests sent in the context of <a
  72.  * href="http://xmpp.org/extensions/xep-0096.html">XEP-0096</a> file transfer. (See
  73.  * {@link FileTransferManager})
  74.  * <p>
  75.  * If no {@link InBandBytestreamListener}s are registered, all incoming In-Band bytestream requests
  76.  * will be rejected by returning a &lt;not-acceptable/&gt; error to the initiator.
  77.  *
  78.  * @author Henning Staib
  79.  */
  80. public final class InBandBytestreamManager extends Manager implements BytestreamManager {

  81.     /**
  82.      * Stanzas that can be used to encapsulate In-Band Bytestream data packets.
  83.      */
  84.     public enum StanzaType {

  85.         /**
  86.          * IQ stanza.
  87.          */
  88.         IQ,

  89.         /**
  90.          * Message stanza.
  91.          */
  92.         MESSAGE
  93.     }

  94.     /*
  95.      * create a new InBandBytestreamManager and register its shutdown listener on every established
  96.      * connection
  97.      */
  98.     static {
  99.         XMPPConnectionRegistry.addConnectionCreationListener(new ConnectionCreationListener() {
  100.             @Override
  101.             public void connectionCreated(final XMPPConnection connection) {
  102.                 // create the manager for this connection
  103.                 InBandBytestreamManager.getByteStreamManager(connection);
  104.             }
  105.         });
  106.     }

  107.     /**
  108.      * Maximum block size that is allowed for In-Band Bytestreams.
  109.      */
  110.     public static final int MAXIMUM_BLOCK_SIZE = 65535;

  111.     /* prefix used to generate session IDs */
  112.     private static final String SESSION_ID_PREFIX = "jibb_";

  113.     /* stores one InBandBytestreamManager for each XMPP connection */
  114.     private static final Map<XMPPConnection, InBandBytestreamManager> managers = new WeakHashMap<>();

  115.     /*
  116.      * assigns a user to a listener that is informed if an In-Band Bytestream request for this user
  117.      * is received
  118.      */
  119.     private final Map<Jid, BytestreamListener> userListeners = new ConcurrentHashMap<>();

  120.     /*
  121.      * list of listeners that respond to all In-Band Bytestream requests if there are no user
  122.      * specific listeners for that request
  123.      */
  124.     private final List<BytestreamListener> allRequestListeners = Collections.synchronizedList(new LinkedList<BytestreamListener>());

  125.     /* listener that handles all incoming In-Band Bytestream requests */
  126.     private final InitiationListener initiationListener;

  127.     /* listener that handles all incoming In-Band Bytestream IQ data packets */
  128.     private final DataListener dataListener;

  129.     /* listener that handles all incoming In-Band Bytestream close requests */
  130.     private final CloseListener closeListener;

  131.     /* assigns a session ID to the In-Band Bytestream session */
  132.     private final Map<String, InBandBytestreamSession> sessions = new ConcurrentHashMap<String, InBandBytestreamSession>();

  133.     /* block size used for new In-Band Bytestreams */
  134.     private int defaultBlockSize = 4096;

  135.     /* maximum block size allowed for this connection */
  136.     private int maximumBlockSize = MAXIMUM_BLOCK_SIZE;

  137.     /* the stanza used to send data packets */
  138.     private StanzaType stanza = StanzaType.IQ;

  139.     /*
  140.      * list containing session IDs of In-Band Bytestream open packets that should be ignored by the
  141.      * InitiationListener
  142.      */
  143.     private final List<String> ignoredBytestreamRequests = Collections.synchronizedList(new LinkedList<String>());

  144.     /**
  145.      * Returns the InBandBytestreamManager to handle In-Band Bytestreams for a given
  146.      * {@link XMPPConnection}.
  147.      *
  148.      * @param connection the XMPP connection
  149.      * @return the InBandBytestreamManager for the given XMPP connection
  150.      */
  151.     public static synchronized InBandBytestreamManager getByteStreamManager(XMPPConnection connection) {
  152.         if (connection == null)
  153.             return null;
  154.         InBandBytestreamManager manager = managers.get(connection);
  155.         if (manager == null) {
  156.             manager = new InBandBytestreamManager(connection);
  157.             managers.put(connection, manager);
  158.         }
  159.         return manager;
  160.     }

  161.     /**
  162.      * Constructor.
  163.      *
  164.      * @param connection the XMPP connection
  165.      */
  166.     private InBandBytestreamManager(XMPPConnection connection) {
  167.         super(connection);

  168.         connection.addConnectionListener(new AbstractConnectionClosedListener() {
  169.             @Override
  170.             public void connectionTerminated() {
  171.                 // reset internal status
  172.                 InBandBytestreamManager.this.sessions.clear();
  173.                 InBandBytestreamManager.this.ignoredBytestreamRequests.clear();
  174.             }
  175.         });

  176.         // register bytestream open packet listener
  177.         this.initiationListener = new InitiationListener(this);
  178.         connection.registerIQRequestHandler(initiationListener);

  179.         // register bytestream data packet listener
  180.         this.dataListener = new DataListener(this);
  181.         connection.registerIQRequestHandler(dataListener);

  182.         // register bytestream close packet listener
  183.         this.closeListener = new CloseListener(this);
  184.         connection.registerIQRequestHandler(closeListener);
  185.     }

  186.     /**
  187.      * Adds InBandBytestreamListener that is called for every incoming in-band bytestream request
  188.      * unless there is a user specific InBandBytestreamListener registered.
  189.      * <p>
  190.      * If no listeners are registered all In-Band Bytestream request are rejected with a
  191.      * &lt;not-acceptable/&gt; error.
  192.      * <p>
  193.      * Note that the registered {@link InBandBytestreamListener} will NOT be notified on incoming
  194.      * Socks5 bytestream requests sent in the context of <a
  195.      * href="http://xmpp.org/extensions/xep-0096.html">XEP-0096</a> file transfer. (See
  196.      * {@link FileTransferManager})
  197.      *
  198.      * @param listener the listener to register
  199.      */
  200.     @Override
  201.     public void addIncomingBytestreamListener(BytestreamListener listener) {
  202.         this.allRequestListeners.add(listener);
  203.     }

  204.     /**
  205.      * Removes the given listener from the list of listeners for all incoming In-Band Bytestream
  206.      * requests.
  207.      *
  208.      * @param listener the listener to remove
  209.      */
  210.     @Override
  211.     public void removeIncomingBytestreamListener(BytestreamListener listener) {
  212.         this.allRequestListeners.remove(listener);
  213.     }

  214.     /**
  215.      * Adds InBandBytestreamListener that is called for every incoming in-band bytestream request
  216.      * from the given user.
  217.      * <p>
  218.      * Use this method if you are awaiting an incoming Socks5 bytestream request from a specific
  219.      * user.
  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.      * @param initiatorJID the JID of the user that wants to establish an In-Band Bytestream
  231.      */
  232.     @Override
  233.     public void addIncomingBytestreamListener(BytestreamListener listener, Jid initiatorJID) {
  234.         this.userListeners.put(initiatorJID, listener);
  235.     }

  236.     /**
  237.      * Removes the listener for the given user.
  238.      *
  239.      * @param initiatorJID the JID of the user the listener should be removed
  240.      */
  241.     @Override
  242.     public void removeIncomingBytestreamListener(Jid initiatorJID) {
  243.         this.userListeners.remove(initiatorJID);
  244.     }

  245.     /**
  246.      * Use this method to ignore the next incoming In-Band Bytestream request containing the given
  247.      * session ID. No listeners will be notified for this request and and no error will be returned
  248.      * to the initiator.
  249.      * <p>
  250.      * This method should be used if you are awaiting an In-Band Bytestream request as a reply to
  251.      * another stanza (e.g. file transfer).
  252.      *
  253.      * @param sessionID to be ignored
  254.      */
  255.     public void ignoreBytestreamRequestOnce(String sessionID) {
  256.         this.ignoredBytestreamRequests.add(sessionID);
  257.     }

  258.     /**
  259.      * Returns the default block size that is used for all outgoing in-band bytestreams for this
  260.      * connection.
  261.      * <p>
  262.      * The recommended default block size is 4096 bytes. See <a
  263.      * href="http://xmpp.org/extensions/xep-0047.html#usage">XEP-0047</a> Section 5.
  264.      *
  265.      * @return the default block size
  266.      */
  267.     public int getDefaultBlockSize() {
  268.         return defaultBlockSize;
  269.     }

  270.     /**
  271.      * Sets the default block size that is used for all outgoing in-band bytestreams for this
  272.      * connection.
  273.      * <p>
  274.      * The default block size must be between 1 and 65535 bytes. The recommended default block size
  275.      * is 4096 bytes. See <a href="http://xmpp.org/extensions/xep-0047.html#usage">XEP-0047</a>
  276.      * Section 5.
  277.      *
  278.      * @param defaultBlockSize the default block size to set
  279.      */
  280.     public void setDefaultBlockSize(int defaultBlockSize) {
  281.         if (defaultBlockSize <= 0 || defaultBlockSize > MAXIMUM_BLOCK_SIZE) {
  282.             throw new IllegalArgumentException("Default block size must be between 1 and "
  283.                             + MAXIMUM_BLOCK_SIZE);
  284.         }
  285.         this.defaultBlockSize = defaultBlockSize;
  286.     }

  287.     /**
  288.      * Returns the maximum block size that is allowed for In-Band Bytestreams for this connection.
  289.      * <p>
  290.      * Incoming In-Band Bytestream open request will be rejected with an
  291.      * &lt;resource-constraint/&gt; error if the block size is greater then the maximum allowed
  292.      * block size.
  293.      * <p>
  294.      * The default maximum block size is 65535 bytes.
  295.      *
  296.      * @return the maximum block size
  297.      */
  298.     public int getMaximumBlockSize() {
  299.         return maximumBlockSize;
  300.     }

  301.     /**
  302.      * Sets the maximum block size that is allowed for In-Band Bytestreams for this connection.
  303.      * <p>
  304.      * The maximum block size must be between 1 and 65535 bytes.
  305.      * <p>
  306.      * Incoming In-Band Bytestream open request will be rejected with an
  307.      * &lt;resource-constraint/&gt; error if the block size is greater then the maximum allowed
  308.      * block size.
  309.      *
  310.      * @param maximumBlockSize the maximum block size to set
  311.      */
  312.     public void setMaximumBlockSize(int maximumBlockSize) {
  313.         if (maximumBlockSize <= 0 || maximumBlockSize > MAXIMUM_BLOCK_SIZE) {
  314.             throw new IllegalArgumentException("Maximum block size must be between 1 and "
  315.                             + MAXIMUM_BLOCK_SIZE);
  316.         }
  317.         this.maximumBlockSize = maximumBlockSize;
  318.     }

  319.     /**
  320.      * Returns the stanza used to send data packets.
  321.      * <p>
  322.      * Default is {@link StanzaType#IQ}. See <a
  323.      * href="http://xmpp.org/extensions/xep-0047.html#message">XEP-0047</a> Section 4.
  324.      *
  325.      * @return the stanza used to send data packets
  326.      */
  327.     public StanzaType getStanza() {
  328.         return stanza;
  329.     }

  330.     /**
  331.      * Sets the stanza used to send data packets.
  332.      * <p>
  333.      * The use of {@link StanzaType#IQ} is recommended. See <a
  334.      * href="http://xmpp.org/extensions/xep-0047.html#message">XEP-0047</a> Section 4.
  335.      *
  336.      * @param stanza the stanza to set
  337.      */
  338.     public void setStanza(StanzaType stanza) {
  339.         this.stanza = stanza;
  340.     }

  341.     /**
  342.      * Establishes an In-Band Bytestream with the given user and returns the session to send/receive
  343.      * data to/from the user.
  344.      * <p>
  345.      * Use this method to establish In-Band Bytestreams to users accepting all incoming In-Band
  346.      * Bytestream requests since this method doesn't provide a way to tell the user something about
  347.      * the data to be sent.
  348.      * <p>
  349.      * To establish an In-Band Bytestream after negotiation the kind of data to be sent (e.g. file
  350.      * transfer) use {@link #establishSession(Jid, String)}.
  351.      *
  352.      * @param targetJID the JID of the user an In-Band Bytestream should be established
  353.      * @return the session to send/receive data to/from the user
  354.      * @throws XMPPException if the user doesn't support or accept in-band bytestreams, or if the
  355.      *         user prefers smaller block sizes
  356.      * @throws SmackException if there was no response from the server.
  357.      * @throws InterruptedException if the calling thread was interrupted.
  358.      */
  359.     @Override
  360.     public InBandBytestreamSession establishSession(Jid targetJID) throws XMPPException, SmackException, InterruptedException {
  361.         String sessionID = getNextSessionID();
  362.         return establishSession(targetJID, sessionID);
  363.     }

  364.     /**
  365.      * Establishes an In-Band Bytestream with the given user using the given session ID and returns
  366.      * the session to send/receive data to/from the user.
  367.      *
  368.      * @param targetJID the JID of the user an In-Band Bytestream should be established
  369.      * @param sessionID the session ID for the In-Band Bytestream request
  370.      * @return the session to send/receive data to/from the user
  371.      * @throws XMPPErrorException if the user doesn't support or accept in-band bytestreams, or if the
  372.      *         user prefers smaller block sizes
  373.      * @throws NoResponseException if there was no response from the server.
  374.      * @throws NotConnectedException if the XMPP connection is not connected.
  375.      * @throws InterruptedException if the calling thread was interrupted.
  376.      */
  377.     @Override
  378.     public InBandBytestreamSession establishSession(Jid targetJID, String sessionID)
  379.                     throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
  380.         Open byteStreamRequest = new Open(sessionID, this.defaultBlockSize, this.stanza);
  381.         byteStreamRequest.setTo(targetJID);

  382.         final XMPPConnection connection = connection();

  383.         // sending packet will throw exception on timeout or error reply
  384.         connection.sendIqRequestAndWaitForResponse(byteStreamRequest);

  385.         InBandBytestreamSession inBandBytestreamSession = new InBandBytestreamSession(
  386.                         connection, byteStreamRequest, targetJID);
  387.         this.sessions.put(sessionID, inBandBytestreamSession);

  388.         return inBandBytestreamSession;
  389.     }

  390.     /**
  391.      * Responses to the given IQ packet's sender with an XMPP error that an In-Band Bytestream is
  392.      * not accepted.
  393.      *
  394.      * @param request IQ stanza that should be answered with a not-acceptable error
  395.      * @throws NotConnectedException if the XMPP connection is not connected.
  396.      * @throws InterruptedException if the calling thread was interrupted.
  397.      */
  398.     void replyRejectPacket(IQ request) throws NotConnectedException, InterruptedException {
  399.         IQ error = IQ.createErrorResponse(request, StanzaError.Condition.not_acceptable);
  400.         connection().sendStanza(error);
  401.     }

  402.     /**
  403.      * Responses to the given IQ packet's sender with an XMPP error that an In-Band Bytestream
  404.      * session could not be found.
  405.      *
  406.      * @param request IQ stanza that should be answered with a item-not-found error
  407.      * @throws NotConnectedException if the XMPP connection is not connected.
  408.      * @throws InterruptedException if the calling thread was interrupted.
  409.      */
  410.     void replyItemNotFoundPacket(IQ request) throws NotConnectedException, InterruptedException {
  411.         IQ error = IQ.createErrorResponse(request, StanzaError.Condition.item_not_found);
  412.         connection().sendStanza(error);
  413.     }

  414.     /**
  415.      * Returns a new unique session ID.
  416.      *
  417.      * @return a new unique session ID
  418.      */
  419.     private static String getNextSessionID() {
  420.         StringBuilder buffer = new StringBuilder();
  421.         buffer.append(SESSION_ID_PREFIX);
  422.         buffer.append(StringUtils.secureOnlineAttackSafeRandomString());
  423.         return buffer.toString();
  424.     }

  425.     /**
  426.      * Returns the XMPP connection.
  427.      *
  428.      * @return the XMPP connection
  429.      */
  430.     XMPPConnection getConnection() {
  431.         return connection();
  432.     }

  433.     /**
  434.      * Returns the {@link InBandBytestreamListener} that should be informed if a In-Band Bytestream
  435.      * request from the given initiator JID is received.
  436.      *
  437.      * @param initiator the initiator's JID
  438.      * @return the listener
  439.      */
  440.     BytestreamListener getUserListener(Jid initiator) {
  441.         return this.userListeners.get(initiator);
  442.     }

  443.     /**
  444.      * Returns a list of {@link InBandBytestreamListener} that are informed if there are no
  445.      * listeners for a specific initiator.
  446.      *
  447.      * @return list of listeners
  448.      */
  449.     List<BytestreamListener> getAllRequestListeners() {
  450.         return this.allRequestListeners;
  451.     }

  452.     /**
  453.      * Returns the sessions map.
  454.      *
  455.      * @return the sessions map
  456.      */
  457.     Map<String, InBandBytestreamSession> getSessions() {
  458.         return sessions;
  459.     }

  460.     /**
  461.      * Returns the list of session IDs that should be ignored by the InitiationListener
  462.      *
  463.      * @return list of session IDs
  464.      */
  465.     List<String> getIgnoredBytestreamRequests() {
  466.         return ignoredBytestreamRequests;
  467.     }

  468. }