/** * 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.smackx.bytestreams.BytestreamListener; import org.jivesoftware.smackx.bytestreams.BytestreamManager; import org.jivesoftware.smackx.bytestreams.ibb.packet.Open; import org.jivesoftware.smackx.filetransfer.FileTransferManager; import org.jivesoftware.smackx.packet.SyncPacketSend; /** * The InBandBytestreamManager class handles establishing In-Band Bytestreams as specified in the XEP-0047. *
* 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. *
* 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 Advanced Message * Processing. To set the stanza that should be used invoke {@link #setStanza(StanzaType)}. *
* 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. *
* If a session ID for the In-Band Bytestream was already negotiated (e.g. while negotiating a file * transfer) invoke {@link #establishSession(String, String)}. *
* 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)}. *
* Note that the registered {@link InBandBytestreamListener} will NOT be notified on incoming * In-Band bytestream requests sent in the context of XEP-0096 file transfer. (See * {@link FileTransferManager}) *
* 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
* If no listeners are registered all In-Band Bytestream request are rejected with a
* <not-acceptable/> error.
*
* Note that the registered {@link InBandBytestreamListener} will NOT be notified on incoming
* Socks5 bytestream requests sent in the context of XEP-0096 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.
*
* Use this method if you are awaiting an incoming Socks5 bytestream request from a specific
* user.
*
* If no listeners are registered all In-Band Bytestream request are rejected with a
* <not-acceptable/> error.
*
* Note that the registered {@link InBandBytestreamListener} will NOT be notified on incoming
* Socks5 bytestream requests sent in the context of XEP-0096 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.
*
* 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.
*
* The recommended default block size is 4096 bytes. See XEP-0047 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.
*
* The default block size must be between 1 and 65535 bytes. The recommended default block size
* is 4096 bytes. See XEP-0047
* 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.
*
* 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.
*
* 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.
*
* The maximum block size must be between 1 and 65535 bytes.
*
* 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.
*
* Default is {@link StanzaType#IQ}. See XEP-0047 Section 4.
*
* @return the stanza used to send data packets
*/
public StanzaType getStanza() {
return stanza;
}
/**
* Sets the stanza used to send data packets.
*
* The use of {@link StanzaType#IQ} is recommended. See XEP-0047 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.
*
* 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.
*
* 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