FileTransferNegotiator.java

  1. /**
  2.  *
  3.  * Copyright 2003-2006 Jive Software.
  4.  *
  5.  * Licensed under the Apache License, Version 2.0 (the "License");
  6.  * you may not use this file except in compliance with the License.
  7.  * You may obtain a copy of the License at
  8.  *
  9.  *     http://www.apache.org/licenses/LICENSE-2.0
  10.  *
  11.  * Unless required by applicable law or agreed to in writing, software
  12.  * distributed under the License is distributed on an "AS IS" BASIS,
  13.  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14.  * See the License for the specific language governing permissions and
  15.  * limitations under the License.
  16.  */
  17. package org.jivesoftware.smackx.filetransfer;

  18. import java.net.URLConnection;
  19. import java.util.ArrayList;
  20. import java.util.Arrays;
  21. import java.util.Collection;
  22. import java.util.Collections;
  23. import java.util.List;
  24. import java.util.Map;
  25. import java.util.Random;
  26. import java.util.WeakHashMap;

  27. import org.jivesoftware.smack.Manager;
  28. import org.jivesoftware.smack.SmackException.NoResponseException;
  29. import org.jivesoftware.smack.SmackException.NotConnectedException;
  30. import org.jivesoftware.smack.XMPPConnection;
  31. import org.jivesoftware.smack.XMPPException.XMPPErrorException;
  32. import org.jivesoftware.smack.packet.IQ;
  33. import org.jivesoftware.smack.packet.Stanza;
  34. import org.jivesoftware.smack.packet.XMPPError;
  35. import org.jivesoftware.smackx.bytestreams.ibb.packet.DataPacketExtension;
  36. import org.jivesoftware.smackx.bytestreams.socks5.packet.Bytestream;
  37. import org.jivesoftware.smackx.disco.ServiceDiscoveryManager;
  38. import org.jivesoftware.smackx.filetransfer.FileTransferException.NoAcceptableTransferMechanisms;
  39. import org.jivesoftware.smackx.filetransfer.FileTransferException.NoStreamMethodsOfferedException;
  40. import org.jivesoftware.smackx.si.packet.StreamInitiation;
  41. import org.jivesoftware.smackx.xdata.FormField;
  42. import org.jivesoftware.smackx.xdata.packet.DataForm;
  43. import org.jxmpp.jid.Jid;

  44. /**
  45.  * Manages the negotiation of file transfers according to XEP-0096. If a file is
  46.  * being sent the remote user chooses the type of stream under which the file
  47.  * will be sent.
  48.  *
  49.  * @author Alexander Wenckus
  50.  * @see <a href="http://xmpp.org/extensions/xep-0096.html">XEP-0096: SI File Transfer</a>
  51.  */
  52. public class FileTransferNegotiator extends Manager {

  53.     public static final String SI_NAMESPACE = "http://jabber.org/protocol/si";
  54.     public static final String SI_PROFILE_FILE_TRANSFER_NAMESPACE = "http://jabber.org/protocol/si/profile/file-transfer";
  55.     private static final String[] NAMESPACE = { SI_NAMESPACE, SI_PROFILE_FILE_TRANSFER_NAMESPACE };

  56.     private static final Map<XMPPConnection, FileTransferNegotiator> INSTANCES = new WeakHashMap<XMPPConnection, FileTransferNegotiator>();

  57.     private static final String STREAM_INIT_PREFIX = "jsi_";

  58.     protected static final String STREAM_DATA_FIELD_NAME = "stream-method";

  59.     private static final Random randomGenerator = new Random();

  60.     /**
  61.      * A static variable to use only offer IBB for file transfer. It is generally recommend to only
  62.      * set this variable to true for testing purposes as IBB is the backup file transfer method
  63.      * and shouldn't be used as the only transfer method in production systems.
  64.      */
  65.     public static boolean IBB_ONLY = (System.getProperty("ibb") != null);//true;

  66.     /**
  67.      * Returns the file transfer negotiator related to a particular connection.
  68.      * When this class is requested on a particular connection the file transfer
  69.      * service is automatically enabled.
  70.      *
  71.      * @param connection The connection for which the transfer manager is desired
  72.      * @return The FileTransferNegotiator
  73.      */
  74.     public static synchronized FileTransferNegotiator getInstanceFor(
  75.             final XMPPConnection connection) {
  76.         FileTransferNegotiator fileTransferNegotiator = INSTANCES.get(connection);
  77.         if (fileTransferNegotiator == null) {
  78.             fileTransferNegotiator = new FileTransferNegotiator(connection);
  79.             INSTANCES.put(connection, fileTransferNegotiator);
  80.         }
  81.         return fileTransferNegotiator;
  82.     }

  83.     /**
  84.      * Enable the Jabber services related to file transfer on the particular
  85.      * connection.
  86.      *
  87.      * @param connection The connection on which to enable or disable the services.
  88.      * @param isEnabled  True to enable, false to disable.
  89.      */
  90.     private static void setServiceEnabled(final XMPPConnection connection,
  91.             final boolean isEnabled) {
  92.         ServiceDiscoveryManager manager = ServiceDiscoveryManager
  93.                 .getInstanceFor(connection);

  94.         List<String> namespaces = new ArrayList<String>();
  95.         namespaces.addAll(Arrays.asList(NAMESPACE));
  96.         namespaces.add(DataPacketExtension.NAMESPACE);
  97.         if (!IBB_ONLY) {
  98.             namespaces.add(Bytestream.NAMESPACE);
  99.         }

  100.         for (String namespace : namespaces) {
  101.             if (isEnabled) {
  102.                 manager.addFeature(namespace);
  103.             } else {
  104.                 manager.removeFeature(namespace);
  105.             }
  106.         }
  107.     }

  108.     /**
  109.      * Checks to see if all file transfer related services are enabled on the
  110.      * connection.
  111.      *
  112.      * @param connection The connection to check
  113.      * @return True if all related services are enabled, false if they are not.
  114.      */
  115.     public static boolean isServiceEnabled(final XMPPConnection connection) {
  116.         ServiceDiscoveryManager manager = ServiceDiscoveryManager
  117.                 .getInstanceFor(connection);

  118.         List<String> namespaces = new ArrayList<String>();
  119.         namespaces.addAll(Arrays.asList(NAMESPACE));
  120.         namespaces.add(DataPacketExtension.NAMESPACE);
  121.         if (!IBB_ONLY) {
  122.             namespaces.add(Bytestream.NAMESPACE);
  123.         }

  124.         for (String namespace : namespaces) {
  125.             if (!manager.includesFeature(namespace)) {
  126.                 return false;
  127.             }
  128.         }
  129.         return true;
  130.     }

  131.     /**
  132.      * Returns a collection of the supported transfer protocols.
  133.      *
  134.      * @return Returns a collection of the supported transfer protocols.
  135.      */
  136.     public static Collection<String> getSupportedProtocols() {
  137.         List<String> protocols = new ArrayList<String>();
  138.         protocols.add(DataPacketExtension.NAMESPACE);
  139.         if (!IBB_ONLY) {
  140.             protocols.add(Bytestream.NAMESPACE);
  141.         }
  142.         return Collections.unmodifiableList(protocols);
  143.     }

  144.     // non-static

  145.     private final StreamNegotiator byteStreamTransferManager;

  146.     private final StreamNegotiator inbandTransferManager;

  147.     private FileTransferNegotiator(final XMPPConnection connection) {
  148.         super(connection);
  149.         byteStreamTransferManager = new Socks5TransferNegotiator(connection);
  150.         inbandTransferManager = new IBBTransferNegotiator(connection);

  151.         setServiceEnabled(connection, true);
  152.     }

  153.     /**
  154.      * Selects an appropriate stream negotiator after examining the incoming file transfer request.
  155.      *
  156.      * @param request The related file transfer request.
  157.      * @return The file transfer object that handles the transfer
  158.      * @throws NoStreamMethodsOfferedException If there are either no stream methods contained in the packet, or
  159.      *                       there is not an appropriate stream method.
  160.      * @throws NotConnectedException
  161.      * @throws NoAcceptableTransferMechanisms
  162.      * @throws InterruptedException
  163.      */
  164.     public StreamNegotiator selectStreamNegotiator(
  165.             FileTransferRequest request) throws NotConnectedException, NoStreamMethodsOfferedException, NoAcceptableTransferMechanisms, InterruptedException {
  166.         StreamInitiation si = request.getStreamInitiation();
  167.         FormField streamMethodField = getStreamMethodField(si
  168.                 .getFeatureNegotiationForm());

  169.         if (streamMethodField == null) {
  170.             String errorMessage = "No stream methods contained in stanza.";
  171.             XMPPError error = XMPPError.from(XMPPError.Condition.bad_request, errorMessage);
  172.             IQ iqPacket = IQ.createErrorResponse(si, error);
  173.             connection().sendStanza(iqPacket);
  174.             throw new FileTransferException.NoStreamMethodsOfferedException();
  175.         }

  176.         // select the appropriate protocol
  177.         StreamNegotiator selectedStreamNegotiator;
  178.         try {
  179.             selectedStreamNegotiator = getNegotiator(streamMethodField);
  180.         }
  181.         catch (NoAcceptableTransferMechanisms e) {
  182.             IQ iqPacket = IQ.createErrorResponse(si, XMPPError.from(XMPPError.Condition.bad_request, "No acceptable transfer mechanism"));
  183.             connection().sendStanza(iqPacket);
  184.             throw e;
  185.         }

  186.         // return the appropriate negotiator

  187.         return selectedStreamNegotiator;
  188.     }

  189.     private FormField getStreamMethodField(DataForm form) {
  190.         for (FormField field : form.getFields()) {
  191.             if (field.getVariable().equals(STREAM_DATA_FIELD_NAME)) {
  192.                 return field;
  193.             }
  194.         }
  195.         return null;
  196.     }

  197.     private StreamNegotiator getNegotiator(final FormField field)
  198.             throws NoAcceptableTransferMechanisms {
  199.         String variable;
  200.         boolean isByteStream = false;
  201.         boolean isIBB = false;
  202.         for (FormField.Option option : field.getOptions()) {
  203.             variable = option.getValue();
  204.             if (variable.equals(Bytestream.NAMESPACE) && !IBB_ONLY) {
  205.                 isByteStream = true;
  206.             }
  207.             else if (variable.equals(DataPacketExtension.NAMESPACE)) {
  208.                 isIBB = true;
  209.             }
  210.         }

  211.         if (!isByteStream && !isIBB) {
  212.             throw new FileTransferException.NoAcceptableTransferMechanisms();
  213.         }

  214.         if (isByteStream && isIBB) {
  215.             return new FaultTolerantNegotiator(connection(),
  216.                     byteStreamTransferManager,
  217.                     inbandTransferManager);
  218.         }
  219.         else if (isByteStream) {
  220.             return byteStreamTransferManager;
  221.         }
  222.         else {
  223.             return inbandTransferManager;
  224.         }
  225.     }

  226.     /**
  227.      * Returns a new, unique, stream ID to identify a file transfer.
  228.      *
  229.      * @return Returns a new, unique, stream ID to identify a file transfer.
  230.      */
  231.     public String getNextStreamID() {
  232.         StringBuilder buffer = new StringBuilder();
  233.         buffer.append(STREAM_INIT_PREFIX);
  234.         buffer.append(Math.abs(randomGenerator.nextLong()));

  235.         return buffer.toString();
  236.     }

  237.     /**
  238.      * Send a request to another user to send them a file. The other user has
  239.      * the option of, accepting, rejecting, or not responding to a received file
  240.      * transfer request.
  241.      * <p/>
  242.      * If they accept, the packet will contain the other user's chosen stream
  243.      * type to send the file across. The two choices this implementation
  244.      * provides to the other user for file transfer are <a
  245.      * href="http://www.xmpp.org/extensions/jep-0065.html">SOCKS5 Bytestreams</a>,
  246.      * which is the preferred method of transfer, and <a
  247.      * href="http://www.xmpp.org/extensions/jep-0047.html">In-Band Bytestreams</a>,
  248.      * which is the fallback mechanism.
  249.      * <p/>
  250.      * The other user may choose to decline the file request if they do not
  251.      * desire the file, their client does not support XEP-0096, or if there are
  252.      * no acceptable means to transfer the file.
  253.      * <p/>
  254.      * Finally, if the other user does not respond this method will return null
  255.      * after the specified timeout.
  256.      *
  257.      * @param userID          The userID of the user to whom the file will be sent.
  258.      * @param streamID        The unique identifier for this file transfer.
  259.      * @param fileName        The name of this file. Preferably it should include an
  260.      *                        extension as it is used to determine what type of file it is.
  261.      * @param size            The size, in bytes, of the file.
  262.      * @param desc            A description of the file.
  263.      * @param responseTimeout The amount of time, in milliseconds, to wait for the remote
  264.      *                        user to respond. If they do not respond in time, this
  265.      * @return Returns the stream negotiator selected by the peer.
  266.      * @throws XMPPErrorException Thrown if there is an error negotiating the file transfer.
  267.      * @throws NotConnectedException
  268.      * @throws NoResponseException
  269.      * @throws NoAcceptableTransferMechanisms
  270.      * @throws InterruptedException
  271.      */
  272.     public StreamNegotiator negotiateOutgoingTransfer(final Jid userID,
  273.             final String streamID, final String fileName, final long size,
  274.             final String desc, int responseTimeout) throws XMPPErrorException, NotConnectedException, NoResponseException, NoAcceptableTransferMechanisms, InterruptedException {
  275.         StreamInitiation si = new StreamInitiation();
  276.         si.setSessionID(streamID);
  277.         si.setMimeType(URLConnection.guessContentTypeFromName(fileName));

  278.         StreamInitiation.File siFile = new StreamInitiation.File(fileName, size);
  279.         siFile.setDesc(desc);
  280.         si.setFile(siFile);

  281.         si.setFeatureNegotiationForm(createDefaultInitiationForm());

  282.         si.setFrom(connection().getUser());
  283.         si.setTo(userID);
  284.         si.setType(IQ.Type.set);

  285.         Stanza siResponse = connection().createPacketCollectorAndSend(si).nextResultOrThrow(
  286.                         responseTimeout);

  287.         if (siResponse instanceof IQ) {
  288.             IQ iqResponse = (IQ) siResponse;
  289.             if (iqResponse.getType().equals(IQ.Type.result)) {
  290.                 StreamInitiation response = (StreamInitiation) siResponse;
  291.                 return getOutgoingNegotiator(getStreamMethodField(response
  292.                         .getFeatureNegotiationForm()));

  293.             }
  294.             else {
  295.                 throw new XMPPErrorException(iqResponse.getError());
  296.             }
  297.         }
  298.         else {
  299.             return null;
  300.         }
  301.     }

  302.     private StreamNegotiator getOutgoingNegotiator(final FormField field) throws NoAcceptableTransferMechanisms {
  303.         boolean isByteStream = false;
  304.         boolean isIBB = false;
  305.         for (String variable : field.getValues()) {
  306.             if (variable.equals(Bytestream.NAMESPACE) && !IBB_ONLY) {
  307.                 isByteStream = true;
  308.             }
  309.             else if (variable.equals(DataPacketExtension.NAMESPACE)) {
  310.                 isIBB = true;
  311.             }
  312.         }

  313.         if (!isByteStream && !isIBB) {
  314.             throw new FileTransferException.NoAcceptableTransferMechanisms();
  315.         }

  316.         if (isByteStream && isIBB) {
  317.             return new FaultTolerantNegotiator(connection(),
  318.                     byteStreamTransferManager, inbandTransferManager);
  319.         }
  320.         else if (isByteStream) {
  321.             return byteStreamTransferManager;
  322.         }
  323.         else {
  324.             return inbandTransferManager;
  325.         }
  326.     }

  327.     private DataForm createDefaultInitiationForm() {
  328.         DataForm form = new DataForm(DataForm.Type.form);
  329.         FormField field = new FormField(STREAM_DATA_FIELD_NAME);
  330.         field.setType(FormField.Type.list_single);
  331.         if (!IBB_ONLY) {
  332.             field.addOption(new FormField.Option(Bytestream.NAMESPACE));
  333.         }
  334.         field.addOption(new FormField.Option(DataPacketExtension.NAMESPACE));
  335.         form.addField(field);
  336.         return form;
  337.     }
  338. }