1
0
Fork 0
mirror of https://codeberg.org/Mercury-IM/Smack synced 2024-06-28 14:34:51 +02:00
Smack/extensions/src/main/java/org/jivesoftware/smackx/filetransfer/FileTransferNegotiator.java
Lars Noschinski 6c7296a37b Add and use IQReplyFilter (SMACK-533)
In the absence of checks on the from address, it is possible for other
clients to fake an answer to an IQ request.

This commit adds an IQReplyFilter, which drops all packets which are not
a valid reply to an IQ request. In particular, it checks for packet id,
from address and packet type.

Most(?) places waiting for a reply to an IQ request are converted to use
the IQReplyFilter.

For a discussion of the issues, see the thread "Spoofing of iq ids and
misbehaving servers" from 2014-01 on the jdev@jabber.org mailing list
and following discussion in February and March.
2014-03-07 16:13:07 +01:00

480 lines
17 KiB
Java

/**
*
* Copyright 2003-2006 Jive Software.
*
* 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.filetransfer;
import java.net.URLConnection;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.concurrent.ConcurrentHashMap;
import org.jivesoftware.smack.Connection;
import org.jivesoftware.smack.ConnectionListener;
import org.jivesoftware.smack.PacketCollector;
import org.jivesoftware.smack.XMPPException;
import org.jivesoftware.smack.packet.IQ;
import org.jivesoftware.smack.packet.Packet;
import org.jivesoftware.smack.packet.XMPPError;
import org.jivesoftware.smackx.bytestreams.ibb.InBandBytestreamManager;
import org.jivesoftware.smackx.bytestreams.socks5.Socks5BytestreamManager;
import org.jivesoftware.smackx.disco.ServiceDiscoveryManager;
import org.jivesoftware.smackx.si.packet.StreamInitiation;
import org.jivesoftware.smackx.xdata.Form;
import org.jivesoftware.smackx.xdata.FormField;
import org.jivesoftware.smackx.xdata.packet.DataForm;
/**
* Manages the negotiation of file transfers according to JEP-0096. If a file is
* being sent the remote user chooses the type of stream under which the file
* will be sent.
*
* @author Alexander Wenckus
* @see <a href="http://xmpp.org/extensions/xep-0096.html">XEP-0096: SI File Transfer</a>
*/
public class FileTransferNegotiator {
// Static
private static final String[] NAMESPACE = {
"http://jabber.org/protocol/si/profile/file-transfer",
"http://jabber.org/protocol/si"};
private static final Map<Connection, FileTransferNegotiator> transferObject =
new ConcurrentHashMap<Connection, FileTransferNegotiator>();
private static final String STREAM_INIT_PREFIX = "jsi_";
protected static final String STREAM_DATA_FIELD_NAME = "stream-method";
private static final Random randomGenerator = new Random();
/**
* A static variable to use only offer IBB for file transfer. It is generally recommend to only
* set this variable to true for testing purposes as IBB is the backup file transfer method
* and shouldn't be used as the only transfer method in production systems.
*/
public static boolean IBB_ONLY = (System.getProperty("ibb") != null);//true;
/**
* Returns the file transfer negotiator related to a particular connection.
* When this class is requested on a particular connection the file transfer
* service is automatically enabled.
*
* @param connection The connection for which the transfer manager is desired
* @return The IMFileTransferManager
*/
public static FileTransferNegotiator getInstanceFor(
final Connection connection) {
if (connection == null) {
throw new IllegalArgumentException("Connection cannot be null");
}
if (!connection.isConnected()) {
return null;
}
if (transferObject.containsKey(connection)) {
return transferObject.get(connection);
}
else {
FileTransferNegotiator transfer = new FileTransferNegotiator(
connection);
setServiceEnabled(connection, true);
transferObject.put(connection, transfer);
return transfer;
}
}
/**
* Enable the Jabber services related to file transfer on the particular
* connection.
*
* @param connection The connection on which to enable or disable the services.
* @param isEnabled True to enable, false to disable.
*/
public static void setServiceEnabled(final Connection connection,
final boolean isEnabled) {
ServiceDiscoveryManager manager = ServiceDiscoveryManager
.getInstanceFor(connection);
List<String> namespaces = new ArrayList<String>();
namespaces.addAll(Arrays.asList(NAMESPACE));
namespaces.add(InBandBytestreamManager.NAMESPACE);
if (!IBB_ONLY) {
namespaces.add(Socks5BytestreamManager.NAMESPACE);
}
for (String namespace : namespaces) {
if (isEnabled) {
if (!manager.includesFeature(namespace)) {
manager.addFeature(namespace);
}
} else {
manager.removeFeature(namespace);
}
}
}
/**
* Checks to see if all file transfer related services are enabled on the
* connection.
*
* @param connection The connection to check
* @return True if all related services are enabled, false if they are not.
*/
public static boolean isServiceEnabled(final Connection connection) {
ServiceDiscoveryManager manager = ServiceDiscoveryManager
.getInstanceFor(connection);
List<String> namespaces = new ArrayList<String>();
namespaces.addAll(Arrays.asList(NAMESPACE));
namespaces.add(InBandBytestreamManager.NAMESPACE);
if (!IBB_ONLY) {
namespaces.add(Socks5BytestreamManager.NAMESPACE);
}
for (String namespace : namespaces) {
if (!manager.includesFeature(namespace)) {
return false;
}
}
return true;
}
/**
* A convenience method to create an IQ packet.
*
* @param ID The packet ID of the
* @param to To whom the packet is addressed.
* @param from From whom the packet is sent.
* @param type The IQ type of the packet.
* @return The created IQ packet.
*/
public static IQ createIQ(final String ID, final String to,
final String from, final IQ.Type type) {
IQ iqPacket = new IQ() {
public String getChildElementXML() {
return null;
}
};
iqPacket.setPacketID(ID);
iqPacket.setTo(to);
iqPacket.setFrom(from);
iqPacket.setType(type);
return iqPacket;
}
/**
* Returns a collection of the supported transfer protocols.
*
* @return Returns a collection of the supported transfer protocols.
*/
public static Collection<String> getSupportedProtocols() {
List<String> protocols = new ArrayList<String>();
protocols.add(InBandBytestreamManager.NAMESPACE);
if (!IBB_ONLY) {
protocols.add(Socks5BytestreamManager.NAMESPACE);
}
return Collections.unmodifiableList(protocols);
}
// non-static
private final Connection connection;
private final StreamNegotiator byteStreamTransferManager;
private final StreamNegotiator inbandTransferManager;
private FileTransferNegotiator(final Connection connection) {
configureConnection(connection);
this.connection = connection;
byteStreamTransferManager = new Socks5TransferNegotiator(connection);
inbandTransferManager = new IBBTransferNegotiator(connection);
}
private void configureConnection(final Connection connection) {
connection.addConnectionListener(new ConnectionListener() {
public void connectionClosed() {
cleanup(connection);
}
public void connectionClosedOnError(Exception e) {
cleanup(connection);
}
public void reconnectionFailed(Exception e) {
// ignore
}
public void reconnectionSuccessful() {
// ignore
}
public void reconnectingIn(int seconds) {
// ignore
}
});
}
private void cleanup(final Connection connection) {
if (transferObject.remove(connection) != null) {
inbandTransferManager.cleanup();
}
}
/**
* Selects an appropriate stream negotiator after examining the incoming file transfer request.
*
* @param request The related file transfer request.
* @return The file transfer object that handles the transfer
* @throws XMPPException If there are either no stream methods contained in the packet, or
* there is not an appropriate stream method.
*/
public StreamNegotiator selectStreamNegotiator(
FileTransferRequest request) throws XMPPException {
StreamInitiation si = request.getStreamInitiation();
FormField streamMethodField = getStreamMethodField(si
.getFeatureNegotiationForm());
if (streamMethodField == null) {
String errorMessage = "No stream methods contained in packet.";
XMPPError error = new XMPPError(XMPPError.Condition.bad_request, errorMessage);
IQ iqPacket = createIQ(si.getPacketID(), si.getFrom(), si.getTo(),
IQ.Type.ERROR);
iqPacket.setError(error);
connection.sendPacket(iqPacket);
throw new XMPPException(errorMessage, error);
}
// select the appropriate protocol
StreamNegotiator selectedStreamNegotiator;
try {
selectedStreamNegotiator = getNegotiator(streamMethodField);
}
catch (XMPPException e) {
IQ iqPacket = createIQ(si.getPacketID(), si.getFrom(), si.getTo(),
IQ.Type.ERROR);
iqPacket.setError(e.getXMPPError());
connection.sendPacket(iqPacket);
throw e;
}
// return the appropriate negotiator
return selectedStreamNegotiator;
}
private FormField getStreamMethodField(DataForm form) {
FormField field = null;
for (Iterator<FormField> it = form.getFields(); it.hasNext();) {
field = it.next();
if (field.getVariable().equals(STREAM_DATA_FIELD_NAME)) {
break;
}
field = null;
}
return field;
}
private StreamNegotiator getNegotiator(final FormField field)
throws XMPPException {
String variable;
boolean isByteStream = false;
boolean isIBB = false;
for (Iterator<FormField.Option> it = field.getOptions(); it.hasNext();) {
variable = it.next().getValue();
if (variable.equals(Socks5BytestreamManager.NAMESPACE) && !IBB_ONLY) {
isByteStream = true;
}
else if (variable.equals(InBandBytestreamManager.NAMESPACE)) {
isIBB = true;
}
}
if (!isByteStream && !isIBB) {
XMPPError error = new XMPPError(XMPPError.Condition.bad_request,
"No acceptable transfer mechanism");
throw new XMPPException(error.getMessage(), error);
}
//if (isByteStream && isIBB && field.getType().equals(FormField.TYPE_LIST_MULTI)) {
if (isByteStream && isIBB) {
return new FaultTolerantNegotiator(connection,
byteStreamTransferManager,
inbandTransferManager);
}
else if (isByteStream) {
return byteStreamTransferManager;
}
else {
return inbandTransferManager;
}
}
/**
* Reject a stream initiation request from a remote user.
*
* @param si The Stream Initiation request to reject.
*/
public void rejectStream(final StreamInitiation si) {
XMPPError error = new XMPPError(XMPPError.Condition.forbidden, "Offer Declined");
IQ iqPacket = createIQ(si.getPacketID(), si.getFrom(), si.getTo(),
IQ.Type.ERROR);
iqPacket.setError(error);
connection.sendPacket(iqPacket);
}
/**
* Returns a new, unique, stream ID to identify a file transfer.
*
* @return Returns a new, unique, stream ID to identify a file transfer.
*/
public String getNextStreamID() {
StringBuilder buffer = new StringBuilder();
buffer.append(STREAM_INIT_PREFIX);
buffer.append(Math.abs(randomGenerator.nextLong()));
return buffer.toString();
}
/**
* Send a request to another user to send them a file. The other user has
* the option of, accepting, rejecting, or not responding to a received file
* transfer request.
* <p/>
* If they accept, the packet will contain the other user's chosen stream
* type to send the file across. The two choices this implementation
* provides to the other user for file transfer are <a
* href="http://www.jabber.org/jeps/jep-0065.html">SOCKS5 Bytestreams</a>,
* which is the preferred method of transfer, and <a
* href="http://www.jabber.org/jeps/jep-0047.html">In-Band Bytestreams</a>,
* which is the fallback mechanism.
* <p/>
* The other user may choose to decline the file request if they do not
* desire the file, their client does not support JEP-0096, or if there are
* no acceptable means to transfer the file.
* <p/>
* Finally, if the other user does not respond this method will return null
* after the specified timeout.
*
* @param userID The userID of the user to whom the file will be sent.
* @param streamID The unique identifier for this file transfer.
* @param fileName The name of this file. Preferably it should include an
* extension as it is used to determine what type of file it is.
* @param size The size, in bytes, of the file.
* @param desc A description of the file.
* @param responseTimeout The amount of time, in milliseconds, to wait for the remote
* user to respond. If they do not respond in time, this
* @return Returns the stream negotiator selected by the peer.
* @throws XMPPException Thrown if there is an error negotiating the file transfer.
*/
public StreamNegotiator negotiateOutgoingTransfer(final String userID,
final String streamID, final String fileName, final long size,
final String desc, int responseTimeout) throws XMPPException {
StreamInitiation si = new StreamInitiation();
si.setSessionID(streamID);
si.setMimeType(URLConnection.guessContentTypeFromName(fileName));
StreamInitiation.File siFile = new StreamInitiation.File(fileName, size);
siFile.setDesc(desc);
si.setFile(siFile);
si.setFeatureNegotiationForm(createDefaultInitiationForm());
si.setFrom(connection.getUser());
si.setTo(userID);
si.setType(IQ.Type.SET);
PacketCollector collector = connection.createPacketCollectorAndSend(si);
Packet siResponse = collector.nextResult(responseTimeout);
collector.cancel();
if (siResponse instanceof IQ) {
IQ iqResponse = (IQ) siResponse;
if (iqResponse.getType().equals(IQ.Type.RESULT)) {
StreamInitiation response = (StreamInitiation) siResponse;
return getOutgoingNegotiator(getStreamMethodField(response
.getFeatureNegotiationForm()));
}
else if (iqResponse.getType().equals(IQ.Type.ERROR)) {
throw new XMPPException(iqResponse.getError());
}
else {
throw new XMPPException("File transfer response unreadable");
}
}
else {
return null;
}
}
private StreamNegotiator getOutgoingNegotiator(final FormField field)
throws XMPPException {
String variable;
boolean isByteStream = false;
boolean isIBB = false;
for (Iterator<String> it = field.getValues(); it.hasNext();) {
variable = it.next();
if (variable.equals(Socks5BytestreamManager.NAMESPACE) && !IBB_ONLY) {
isByteStream = true;
}
else if (variable.equals(InBandBytestreamManager.NAMESPACE)) {
isIBB = true;
}
}
if (!isByteStream && !isIBB) {
XMPPError error = new XMPPError(XMPPError.Condition.bad_request,
"No acceptable transfer mechanism");
throw new XMPPException(error.getMessage(), error);
}
if (isByteStream && isIBB) {
return new FaultTolerantNegotiator(connection,
byteStreamTransferManager, inbandTransferManager);
}
else if (isByteStream) {
return byteStreamTransferManager;
}
else {
return inbandTransferManager;
}
}
private DataForm createDefaultInitiationForm() {
DataForm form = new DataForm(Form.TYPE_FORM);
FormField field = new FormField(STREAM_DATA_FIELD_NAME);
field.setType(FormField.TYPE_LIST_SINGLE);
if (!IBB_ONLY) {
field.addOption(new FormField.Option(Socks5BytestreamManager.NAMESPACE));
}
field.addOption(new FormField.Option(InBandBytestreamManager.NAMESPACE));
form.addField(field);
return form;
}
}