/** * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.jivesoftware.smackx.bytestreams.ibb; import java.util.Collections; import java.util.HashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Random; import java.util.concurrent.ConcurrentHashMap; import org.jivesoftware.smack.AbstractConnectionListener; import org.jivesoftware.smack.Connection; import org.jivesoftware.smack.ConnectionCreationListener; import org.jivesoftware.smack.XMPPException; import org.jivesoftware.smack.packet.IQ; import org.jivesoftware.smack.packet.XMPPError; import org.jivesoftware.smack.util.SyncPacketSend; import org.jivesoftware.smackx.bytestreams.BytestreamListener; import org.jivesoftware.smackx.bytestreams.BytestreamManager; import org.jivesoftware.smackx.bytestreams.ibb.packet.Open; import org.jivesoftware.smackx.filetransfer.FileTransferManager; /** * The InBandBytestreamManager class handles establishing In-Band Bytestreams as * specified in the <a * href="http://xmpp.org/extensions/xep-0047.html">XEP-0047</a>. * <p> * The In-Band Bytestreams (IBB) enables two entities to establish a virtual * bytestream over which they can exchange Base64-encoded chunks of data over * XMPP itself. It is the fall-back mechanism in case the Socks5 bytestream * method of transferring data is not available. * <p> * There are two ways to send data over an In-Band Bytestream. It could either * use IQ stanzas to send data packets or message stanzas. If IQ stanzas are * used every data packet is acknowledged by the receiver. This is the * recommended way to avoid possible rate-limiting penalties. Message stanzas * are not acknowledged because most XMPP server implementation don't support * stanza flow-control method like <a * href="http://xmpp.org/extensions/xep-0079.html">Advanced Message * Processing</a>. To set the stanza that should be used invoke * {@link #setStanza(StanzaType)}. * <p> * To establish an In-Band Bytestream invoke the * {@link #establishSession(String)} method. This will negotiate an in-band * bytestream with the given target JID and return a session. * <p> * If a session ID for the In-Band Bytestream was already negotiated (e.g. while * negotiating a file transfer) invoke {@link #establishSession(String, String)}. * <p> * To handle incoming In-Band Bytestream requests add an * {@link InBandBytestreamListener} to the manager. There are two ways to add * this listener. If you want to be informed about incoming In-Band Bytestreams * from a specific user add the listener by invoking * {@link #addIncomingBytestreamListener(BytestreamListener, String)}. If the * listener should respond to all In-Band Bytestream requests invoke * {@link #addIncomingBytestreamListener(BytestreamListener)}. * <p> * Note that the registered {@link InBandBytestreamListener} will NOT be * notified on incoming In-Band bytestream requests sent in the context of <a * href="http://xmpp.org/extensions/xep-0096.html">XEP-0096</a> file transfer. * (See {@link FileTransferManager}) * <p> * If no {@link InBandBytestreamListener}s are registered, all incoming In-Band * bytestream requests will be rejected by returning a <not-acceptable/> * error to the initiator. * * @author Henning Staib */ public class InBandBytestreamManager implements BytestreamManager { /** * Stanzas that can be used to encapsulate In-Band Bytestream data packets. */ public enum StanzaType { /** * IQ stanza. */ IQ, /** * Message stanza. */ MESSAGE } /* * create a new InBandBytestreamManager and register its shutdown listener * on every established connection */ static { Connection .addConnectionCreationListener(new ConnectionCreationListener() { @Override public void connectionCreated(Connection connection) { final InBandBytestreamManager manager; manager = InBandBytestreamManager .getByteStreamManager(connection); // register shutdown listener connection .addConnectionListener(new AbstractConnectionListener() { @Override public void connectionClosed() { manager.disableService(); } }); } }); } /** * The XMPP namespace of the In-Band Bytestream */ public static final String NAMESPACE = "http://jabber.org/protocol/ibb"; /** * Maximum block size that is allowed for In-Band Bytestreams */ public static final int MAXIMUM_BLOCK_SIZE = 65535; /* prefix used to generate session IDs */ private static final String SESSION_ID_PREFIX = "jibb_"; /* random generator to create session IDs */ private final static Random randomGenerator = new Random(); /* stores one InBandBytestreamManager for each XMPP connection */ private final static Map<Connection, InBandBytestreamManager> managers = new HashMap<Connection, InBandBytestreamManager>(); /** * Returns the InBandBytestreamManager to handle In-Band Bytestreams for a * given {@link Connection}. * * @param connection * the XMPP connection * @return the InBandBytestreamManager for the given XMPP connection */ public static synchronized InBandBytestreamManager getByteStreamManager( Connection connection) { if (connection == null) { return null; } InBandBytestreamManager manager = managers.get(connection); if (manager == null) { manager = new InBandBytestreamManager(connection); managers.put(connection, manager); } return manager; } /* XMPP connection */ private final Connection connection; /* * assigns a user to a listener that is informed if an In-Band Bytestream * request for this user is received */ private final Map<String, BytestreamListener> userListeners = new ConcurrentHashMap<String, BytestreamListener>(); /* * list of listeners that respond to all In-Band Bytestream requests if * there are no user specific listeners for that request */ private final List<BytestreamListener> allRequestListeners = Collections .synchronizedList(new LinkedList<BytestreamListener>()); /* listener that handles all incoming In-Band Bytestream requests */ private final InitiationListener initiationListener; /* listener that handles all incoming In-Band Bytestream IQ data packets */ private final DataListener dataListener; /* listener that handles all incoming In-Band Bytestream close requests */ private final CloseListener closeListener; /* assigns a session ID to the In-Band Bytestream session */ private final Map<String, InBandBytestreamSession> sessions = new ConcurrentHashMap<String, InBandBytestreamSession>(); /* block size used for new In-Band Bytestreams */ private int defaultBlockSize = 4096; /* maximum block size allowed for this connection */ private int maximumBlockSize = MAXIMUM_BLOCK_SIZE; /* the stanza used to send data packets */ private StanzaType stanza = StanzaType.IQ; /* * list containing session IDs of In-Band Bytestream open packets that * should be ignored by the InitiationListener */ private final List<String> ignoredBytestreamRequests = Collections .synchronizedList(new LinkedList<String>()); /** * Constructor. * * @param connection * the XMPP connection */ private InBandBytestreamManager(Connection connection) { this.connection = connection; // register bytestream open packet listener initiationListener = new InitiationListener(this); this.connection.addPacketListener(initiationListener, initiationListener.getFilter()); // register bytestream data packet listener dataListener = new DataListener(this); this.connection.addPacketListener(dataListener, dataListener.getFilter()); // register bytestream close packet listener closeListener = new CloseListener(this); this.connection.addPacketListener(closeListener, closeListener.getFilter()); } /** * Adds InBandBytestreamListener that is called for every incoming in-band * bytestream request unless there is a user specific * InBandBytestreamListener registered. * <p> * If no listeners are registered all In-Band Bytestream request are * rejected with a <not-acceptable/> error. * <p> * Note that the registered {@link InBandBytestreamListener} will NOT be * notified on incoming Socks5 bytestream requests sent in the context of <a * href="http://xmpp.org/extensions/xep-0096.html">XEP-0096</a> file * transfer. (See {@link FileTransferManager}) * * @param listener * the listener to register */ @Override public void addIncomingBytestreamListener(BytestreamListener listener) { allRequestListeners.add(listener); } /** * Adds InBandBytestreamListener that is called for every incoming in-band * bytestream request from the given user. * <p> * Use this method if you are awaiting an incoming Socks5 bytestream request * from a specific user. * <p> * If no listeners are registered all In-Band Bytestream request are * rejected with a <not-acceptable/> error. * <p> * Note that the registered {@link InBandBytestreamListener} will NOT be * notified on incoming Socks5 bytestream requests sent in the context of <a * href="http://xmpp.org/extensions/xep-0096.html">XEP-0096</a> file * transfer. (See {@link FileTransferManager}) * * @param listener * the listener to register * @param initiatorJID * the JID of the user that wants to establish an In-Band * Bytestream */ @Override public void addIncomingBytestreamListener(BytestreamListener listener, String initiatorJID) { userListeners.put(initiatorJID, listener); } /** * Disables the InBandBytestreamManager by removing its packet listeners and * resetting its internal status. */ private void disableService() { // remove manager from static managers map managers.remove(connection); // remove all listeners registered by this manager connection.removePacketListener(initiationListener); connection.removePacketListener(dataListener); connection.removePacketListener(closeListener); // shutdown threads initiationListener.shutdown(); // reset internal status userListeners.clear(); allRequestListeners.clear(); sessions.clear(); ignoredBytestreamRequests.clear(); } /** * Establishes an In-Band Bytestream with the given user and returns the * session to send/receive data to/from the user. * <p> * Use this method to establish In-Band Bytestreams to users accepting all * incoming In-Band Bytestream requests since this method doesn't provide a * way to tell the user something about the data to be sent. * <p> * To establish an In-Band Bytestream after negotiation the kind of data to * be sent (e.g. file transfer) use * {@link #establishSession(String, String)}. * * @param targetJID * the JID of the user an In-Band Bytestream should be * established * @return the session to send/receive data to/from the user * @throws XMPPException * if the user doesn't support or accept in-band bytestreams, or * if the user prefers smaller block sizes */ @Override public InBandBytestreamSession establishSession(String targetJID) throws XMPPException { final String sessionID = getNextSessionID(); return establishSession(targetJID, sessionID); } /** * Establishes an In-Band Bytestream with the given user using the given * session ID and returns the session to send/receive data to/from the user. * * @param targetJID * the JID of the user an In-Band Bytestream should be * established * @param sessionID * the session ID for the In-Band Bytestream request * @return the session to send/receive data to/from the user * @throws XMPPException * if the user doesn't support or accept in-band bytestreams, or * if the user prefers smaller block sizes */ @Override public InBandBytestreamSession establishSession(String targetJID, String sessionID) throws XMPPException { final Open byteStreamRequest = new Open(sessionID, defaultBlockSize, stanza); byteStreamRequest.setTo(targetJID); // sending packet will throw exception on timeout or error reply SyncPacketSend.getReply(connection, byteStreamRequest); final InBandBytestreamSession inBandBytestreamSession = new InBandBytestreamSession( connection, byteStreamRequest, targetJID); sessions.put(sessionID, inBandBytestreamSession); return inBandBytestreamSession; } /** * Returns a list of {@link InBandBytestreamListener} that are informed if * there are no listeners for a specific initiator. * * @return list of listeners */ protected List<BytestreamListener> getAllRequestListeners() { return allRequestListeners; } /** * Returns the XMPP connection. * * @return the XMPP connection */ protected Connection getConnection() { return connection; } /** * Returns the default block size that is used for all outgoing in-band * bytestreams for this connection. * <p> * The recommended default block size is 4096 bytes. See <a * href="http://xmpp.org/extensions/xep-0047.html#usage">XEP-0047</a> * Section 5. * * @return the default block size */ public int getDefaultBlockSize() { return defaultBlockSize; } /** * Returns the list of session IDs that should be ignored by the * InitialtionListener * * @return list of session IDs */ protected List<String> getIgnoredBytestreamRequests() { return ignoredBytestreamRequests; } /** * Returns the maximum block size that is allowed for In-Band Bytestreams * for this connection. * <p> * Incoming In-Band Bytestream open request will be rejected with an * <resource-constraint/> error if the block size is greater then the * maximum allowed block size. * <p> * The default maximum block size is 65535 bytes. * * @return the maximum block size */ public int getMaximumBlockSize() { return maximumBlockSize; } /** * Returns a new unique session ID. * * @return a new unique session ID */ private String getNextSessionID() { final StringBuilder buffer = new StringBuilder(); buffer.append(SESSION_ID_PREFIX); buffer.append(Math.abs(randomGenerator.nextLong())); return buffer.toString(); } /** * Returns the sessions map. * * @return the sessions map */ protected Map<String, InBandBytestreamSession> getSessions() { return sessions; } /** * Returns the stanza used to send data packets. * <p> * Default is {@link StanzaType#IQ}. See <a * href="http://xmpp.org/extensions/xep-0047.html#message">XEP-0047</a> * Section 4. * * @return the stanza used to send data packets */ public StanzaType getStanza() { return stanza; } /** * Returns the {@link InBandBytestreamListener} that should be informed if a * In-Band Bytestream request from the given initiator JID is received. * * @param initiator * the initiator's JID * @return the listener */ protected BytestreamListener getUserListener(String initiator) { return userListeners.get(initiator); } /** * Use this method to ignore the next incoming In-Band Bytestream request * containing the given session ID. No listeners will be notified for this * request and and no error will be returned to the initiator. * <p> * This method should be used if you are awaiting an In-Band Bytestream * request as a reply to another packet (e.g. file transfer). * * @param sessionID * to be ignored */ public void ignoreBytestreamRequestOnce(String sessionID) { ignoredBytestreamRequests.add(sessionID); } /** * Removes the given listener from the list of listeners for all incoming * In-Band Bytestream requests. * * @param listener * the listener to remove */ @Override public void removeIncomingBytestreamListener(BytestreamListener listener) { allRequestListeners.remove(listener); } /** * Removes the listener for the given user. * * @param initiatorJID * the JID of the user the listener should be removed */ @Override public void removeIncomingBytestreamListener(String initiatorJID) { userListeners.remove(initiatorJID); } /** * Responses to the given IQ packet's sender with an XMPP error that an * In-Band Bytestream session could not be found. * * @param request * IQ packet that should be answered with a item-not-found error */ protected void replyItemNotFoundPacket(IQ request) { final XMPPError xmppError = new XMPPError( XMPPError.Condition.item_not_found); final IQ error = IQ.createErrorResponse(request, xmppError); connection.sendPacket(error); } /** * Responses to the given IQ packet's sender with an XMPP error that an * In-Band Bytestream is not accepted. * * @param request * IQ packet that should be answered with a not-acceptable error */ protected void replyRejectPacket(IQ request) { final XMPPError xmppError = new XMPPError( XMPPError.Condition.no_acceptable); final IQ error = IQ.createErrorResponse(request, xmppError); connection.sendPacket(error); } /** * Responses to the given IQ packet's sender with an XMPP error that an * In-Band Bytestream open request is rejected because its block size is * greater than the maximum allowed block size. * * @param request * IQ packet that should be answered with a resource-constraint * error */ protected void replyResourceConstraintPacket(IQ request) { final XMPPError xmppError = new XMPPError( XMPPError.Condition.resource_constraint); final IQ error = IQ.createErrorResponse(request, xmppError); connection.sendPacket(error); } /** * Sets the default block size that is used for all outgoing in-band * bytestreams for this connection. * <p> * The default block size must be between 1 and 65535 bytes. The recommended * default block size is 4096 bytes. See <a * href="http://xmpp.org/extensions/xep-0047.html#usage">XEP-0047</a> * Section 5. * * @param defaultBlockSize * the default block size to set */ public void setDefaultBlockSize(int defaultBlockSize) { if (defaultBlockSize <= 0 || defaultBlockSize > MAXIMUM_BLOCK_SIZE) { throw new IllegalArgumentException( "Default block size must be between 1 and " + MAXIMUM_BLOCK_SIZE); } this.defaultBlockSize = defaultBlockSize; } /** * Sets the maximum block size that is allowed for In-Band Bytestreams for * this connection. * <p> * The maximum block size must be between 1 and 65535 bytes. * <p> * Incoming In-Band Bytestream open request will be rejected with an * <resource-constraint/> error if the block size is greater then the * maximum allowed block size. * * @param maximumBlockSize * the maximum block size to set */ public void setMaximumBlockSize(int maximumBlockSize) { if (maximumBlockSize <= 0 || maximumBlockSize > MAXIMUM_BLOCK_SIZE) { throw new IllegalArgumentException( "Maximum block size must be between 1 and " + MAXIMUM_BLOCK_SIZE); } this.maximumBlockSize = maximumBlockSize; } /** * Sets the stanza used to send data packets. * <p> * The use of {@link StanzaType#IQ} is recommended. See <a * href="http://xmpp.org/extensions/xep-0047.html#message">XEP-0047</a> * Section 4. * * @param stanza * the stanza to set */ public void setStanza(StanzaType stanza) { this.stanza = stanza; } }