/**
* 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;
}
}