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.HashMap;
  20. import java.util.LinkedList;
  21. import java.util.List;
  22. import java.util.Map;
  23. import java.util.Random;
  24. import java.util.concurrent.ConcurrentHashMap;

  25. import org.jivesoftware.smack.AbstractConnectionClosedListener;
  26. import org.jivesoftware.smack.ConnectionCreationListener;
  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.XMPPError;
  36. import org.jivesoftware.smackx.bytestreams.BytestreamListener;
  37. import org.jivesoftware.smackx.bytestreams.BytestreamManager;
  38. import org.jivesoftware.smackx.bytestreams.ibb.packet.Open;
  39. import org.jivesoftware.smackx.filetransfer.FileTransferManager;
  40. import org.jxmpp.jid.Jid;

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

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

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

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

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

  102.                 // register shutdown listener
  103.                 connection.addConnectionListener(new AbstractConnectionClosedListener() {

  104.                     @Override
  105.                     public void connectionTerminated() {
  106.                         InBandBytestreamManager.getByteStreamManager(connection).disableService();
  107.                     }

  108.                     @Override
  109.                     public void reconnectionSuccessful() {
  110.                         // re-create the manager for this connection
  111.                         InBandBytestreamManager.getByteStreamManager(connection);
  112.                     }

  113.                 });

  114.             }
  115.         });
  116.     }

  117.     /**
  118.      * Maximum block size that is allowed for In-Band Bytestreams
  119.      */
  120.     public static final int MAXIMUM_BLOCK_SIZE = 65535;

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

  123.     /* random generator to create session IDs */
  124.     private final static Random randomGenerator = new Random();

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

  127.     /* XMPP connection */
  128.     private final XMPPConnection connection;

  129.     /*
  130.      * assigns a user to a listener that is informed if an In-Band Bytestream request for this user
  131.      * is received
  132.      */
  133.     private final Map<Jid, BytestreamListener> userListeners = new ConcurrentHashMap<>();

  134.     /*
  135.      * list of listeners that respond to all In-Band Bytestream requests if there are no user
  136.      * specific listeners for that request
  137.      */
  138.     private final List<BytestreamListener> allRequestListeners = Collections.synchronizedList(new LinkedList<BytestreamListener>());

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

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

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

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

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

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

  151.     /* the stanza used to send data packets */
  152.     private StanzaType stanza = StanzaType.IQ;

  153.     /*
  154.      * list containing session IDs of In-Band Bytestream open packets that should be ignored by the
  155.      * InitiationListener
  156.      */
  157.     private List<String> ignoredBytestreamRequests = Collections.synchronizedList(new LinkedList<String>());

  158.     /**
  159.      * Returns the InBandBytestreamManager to handle In-Band Bytestreams for a given
  160.      * {@link XMPPConnection}.
  161.      *
  162.      * @param connection the XMPP connection
  163.      * @return the InBandBytestreamManager for the given XMPP connection
  164.      */
  165.     public static synchronized InBandBytestreamManager getByteStreamManager(XMPPConnection connection) {
  166.         if (connection == null)
  167.             return null;
  168.         InBandBytestreamManager manager = managers.get(connection);
  169.         if (manager == null) {
  170.             manager = new InBandBytestreamManager(connection);
  171.             managers.put(connection, manager);
  172.         }
  173.         return manager;
  174.     }

  175.     /**
  176.      * Constructor.
  177.      *
  178.      * @param connection the XMPP connection
  179.      */
  180.     private InBandBytestreamManager(XMPPConnection connection) {
  181.         this.connection = connection;

  182.         // register bytestream open packet listener
  183.         this.initiationListener = new InitiationListener(this);
  184.         connection.registerIQRequestHandler(initiationListener);

  185.         // register bytestream data packet listener
  186.         this.dataListener = new DataListener(this);
  187.         connection.registerIQRequestHandler(dataListener);

  188.         // register bytestream close packet listener
  189.         this.closeListener = new CloseListener(this);
  190.         connection.registerIQRequestHandler(closeListener);
  191.     }

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

  209.     /**
  210.      * Removes the given listener from the list of listeners for all incoming In-Band Bytestream
  211.      * requests.
  212.      *
  213.      * @param listener the listener to remove
  214.      */
  215.     public void removeIncomingBytestreamListener(BytestreamListener listener) {
  216.         this.allRequestListeners.remove(listener);
  217.     }

  218.     /**
  219.      * Adds InBandBytestreamListener that is called for every incoming in-band bytestream request
  220.      * from the given user.
  221.      * <p>
  222.      * Use this method if you are awaiting an incoming Socks5 bytestream request from a specific
  223.      * user.
  224.      * <p>
  225.      * If no listeners are registered all In-Band Bytestream request are rejected with a
  226.      * &lt;not-acceptable/&gt; error.
  227.      * <p>
  228.      * Note that the registered {@link InBandBytestreamListener} will NOT be notified on incoming
  229.      * Socks5 bytestream requests sent in the context of <a
  230.      * href="http://xmpp.org/extensions/xep-0096.html">XEP-0096</a> file transfer. (See
  231.      * {@link FileTransferManager})
  232.      *
  233.      * @param listener the listener to register
  234.      * @param initiatorJID the JID of the user that wants to establish an In-Band Bytestream
  235.      */
  236.     public void addIncomingBytestreamListener(BytestreamListener listener, Jid initiatorJID) {
  237.         this.userListeners.put(initiatorJID, listener);
  238.     }

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

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

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

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

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

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

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

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

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

  365.     /**
  366.      * Establishes an In-Band Bytestream with the given user using the given session ID and returns
  367.      * the session to send/receive data to/from the user.
  368.      *
  369.      * @param targetJID the JID of the user an In-Band Bytestream should be established
  370.      * @param sessionID the session ID for the In-Band Bytestream request
  371.      * @return the session to send/receive data to/from the user
  372.      * @throws XMPPErrorException if the user doesn't support or accept in-band bytestreams, or if the
  373.      *         user prefers smaller block sizes
  374.      * @throws NoResponseException if there was no response from the server.
  375.      * @throws NotConnectedException
  376.      * @throws InterruptedException
  377.      */
  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.         // sending packet will throw exception on timeout or error reply
  383.         connection.createPacketCollectorAndSend(byteStreamRequest).nextResultOrThrow();

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

  387.         return inBandBytestreamSession;
  388.     }

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

  402.     /**
  403.      * Responses to the given IQ packet's sender with an XMPP error that an In-Band Bytestream open
  404.      * request is rejected because its block size is greater than the maximum allowed block size.
  405.      *
  406.      * @param request IQ packet that should be answered with a resource-constraint error
  407.      * @throws NotConnectedException
  408.      * @throws InterruptedException
  409.      */
  410.     protected void replyResourceConstraintPacket(IQ request) throws NotConnectedException, InterruptedException {
  411.         XMPPError xmppError = new XMPPError(XMPPError.Condition.resource_constraint);
  412.         IQ error = IQ.createErrorResponse(request, xmppError);
  413.         this.connection.sendStanza(error);
  414.     }

  415.     /**
  416.      * Responses to the given IQ packet's sender with an XMPP error that an In-Band Bytestream
  417.      * session could not be found.
  418.      *
  419.      * @param request IQ packet that should be answered with a item-not-found error
  420.      * @throws NotConnectedException
  421.      * @throws InterruptedException
  422.      */
  423.     protected void replyItemNotFoundPacket(IQ request) throws NotConnectedException, InterruptedException {
  424.         XMPPError xmppError = new XMPPError(XMPPError.Condition.item_not_found);
  425.         IQ error = IQ.createErrorResponse(request, xmppError);
  426.         this.connection.sendStanza(error);
  427.     }

  428.     /**
  429.      * Returns a new unique session ID.
  430.      *
  431.      * @return a new unique session ID
  432.      */
  433.     private String getNextSessionID() {
  434.         StringBuilder buffer = new StringBuilder();
  435.         buffer.append(SESSION_ID_PREFIX);
  436.         buffer.append(Math.abs(randomGenerator.nextLong()));
  437.         return buffer.toString();
  438.     }

  439.     /**
  440.      * Returns the XMPP connection.
  441.      *
  442.      * @return the XMPP connection
  443.      */
  444.     protected XMPPConnection getConnection() {
  445.         return this.connection;
  446.     }

  447.     /**
  448.      * Returns the {@link InBandBytestreamListener} that should be informed if a In-Band Bytestream
  449.      * request from the given initiator JID is received.
  450.      *
  451.      * @param initiator the initiator's JID
  452.      * @return the listener
  453.      */
  454.     protected BytestreamListener getUserListener(Jid initiator) {
  455.         return this.userListeners.get(initiator);
  456.     }

  457.     /**
  458.      * Returns a list of {@link InBandBytestreamListener} that are informed if there are no
  459.      * listeners for a specific initiator.
  460.      *
  461.      * @return list of listeners
  462.      */
  463.     protected List<BytestreamListener> getAllRequestListeners() {
  464.         return this.allRequestListeners;
  465.     }

  466.     /**
  467.      * Returns the sessions map.
  468.      *
  469.      * @return the sessions map
  470.      */
  471.     protected Map<String, InBandBytestreamSession> getSessions() {
  472.         return sessions;
  473.     }

  474.     /**
  475.      * Returns the list of session IDs that should be ignored by the InitialtionListener
  476.      *
  477.      * @return list of session IDs
  478.      */
  479.     protected List<String> getIgnoredBytestreamRequests() {
  480.         return ignoredBytestreamRequests;
  481.     }

  482.     /**
  483.      * Disables the InBandBytestreamManager by removing its packet listeners and resetting its
  484.      * internal status, which includes removing this instance from the managers map.
  485.      */
  486.     private void disableService() {

  487.         // remove manager from static managers map
  488.         managers.remove(connection);

  489.         // remove all listeners registered by this manager
  490.         connection.unregisterIQRequestHandler(initiationListener);
  491.         connection.unregisterIQRequestHandler(dataListener);
  492.         connection.unregisterIQRequestHandler(closeListener);

  493.         // shutdown threads
  494.         this.initiationListener.shutdown();

  495.         // reset internal status
  496.         this.userListeners.clear();
  497.         this.allRequestListeners.clear();
  498.         this.sessions.clear();
  499.         this.ignoredBytestreamRequests.clear();

  500.     }

  501. }