/** * 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() { public void connectionCreated(Connection connection) { final InBandBytestreamManager manager; manager = InBandBytestreamManager.getByteStreamManager(connection); // register shutdown listener connection.addConnectionListener(new AbstractConnectionListener() { 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>(); /* 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 List<String> ignoredBytestreamRequests = Collections.synchronizedList(new LinkedList<String>()); /** * 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; } /** * Constructor. * * @param connection the XMPP connection */ private InBandBytestreamManager(Connection connection) { this.connection = connection; // register bytestream open packet listener this.initiationListener = new InitiationListener(this); this.connection.addPacketListener(this.initiationListener, this.initiationListener.getFilter()); // register bytestream data packet listener this.dataListener = new DataListener(this); this.connection.addPacketListener(this.dataListener, this.dataListener.getFilter()); // register bytestream close packet listener this.closeListener = new CloseListener(this); this.connection.addPacketListener(this.closeListener, this.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 */ public void addIncomingBytestreamListener(BytestreamListener listener) { this.allRequestListeners.add(listener); } /** * Removes the given listener from the list of listeners for all incoming In-Band Bytestream * requests. * * @param listener the listener to remove */ public void removeIncomingBytestreamListener(BytestreamListener listener) { this.allRequestListeners.remove(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 */ public void addIncomingBytestreamListener(BytestreamListener listener, String initiatorJID) { this.userListeners.put(initiatorJID, listener); } /** * Removes the listener for the given user. * * @param initiatorJID the JID of the user the listener should be removed */ public void removeIncomingBytestreamListener(String initiatorJID) { this.userListeners.remove(initiatorJID); } /** * 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) { this.ignoredBytestreamRequests.add(sessionID); } /** * 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; } /** * 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; } /** * 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; } /** * 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; } /** * 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; } /** * 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; } /** * 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 */ public InBandBytestreamSession establishSession(String targetJID) throws XMPPException { 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 */ public InBandBytestreamSession establishSession(String targetJID, String sessionID) throws XMPPException { Open byteStreamRequest = new Open(sessionID, this.defaultBlockSize, this.stanza); byteStreamRequest.setTo(targetJID); // sending packet will throw exception on timeout or error reply SyncPacketSend.getReply(this.connection, byteStreamRequest); InBandBytestreamSession inBandBytestreamSession = new InBandBytestreamSession( this.connection, byteStreamRequest, targetJID); this.sessions.put(sessionID, inBandBytestreamSession); return inBandBytestreamSession; } /** * 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) { XMPPError xmppError = new XMPPError(XMPPError.Condition.no_acceptable); IQ error = IQ.createErrorResponse(request, xmppError); this.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) { XMPPError xmppError = new XMPPError(XMPPError.Condition.resource_constraint); IQ error = IQ.createErrorResponse(request, xmppError); this.connection.sendPacket(error); } /** * 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) { XMPPError xmppError = new XMPPError(XMPPError.Condition.item_not_found); IQ error = IQ.createErrorResponse(request, xmppError); this.connection.sendPacket(error); } /** * Returns a new unique session ID. * * @return a new unique session ID */ private String getNextSessionID() { StringBuilder buffer = new StringBuilder(); buffer.append(SESSION_ID_PREFIX); buffer.append(Math.abs(randomGenerator.nextLong())); return buffer.toString(); } /** * Returns the XMPP connection. * * @return the XMPP connection */ protected Connection getConnection() { return this.connection; } /** * 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 this.userListeners.get(initiator); } /** * 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 this.allRequestListeners; } /** * Returns the sessions map. * * @return the sessions map */ protected Map<String, InBandBytestreamSession> getSessions() { return sessions; } /** * Returns the list of session IDs that should be ignored by the InitialtionListener * * @return list of session IDs */ protected List<String> getIgnoredBytestreamRequests() { return ignoredBytestreamRequests; } /** * 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 this.connection.removePacketListener(this.initiationListener); this.connection.removePacketListener(this.dataListener); this.connection.removePacketListener(this.closeListener); // shutdown threads this.initiationListener.shutdown(); // reset internal status this.userListeners.clear(); this.allRequestListeners.clear(); this.sessions.clear(); this.ignoredBytestreamRequests.clear(); } }