Home | History | Annotate | Download | only in filetransfer
      1 /**
      2  * $RCSfile$
      3  * $Revision$
      4  * $Date$
      5  *
      6  * Copyright 2003-2006 Jive Software.
      7  *
      8  * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License");
      9  * you may not use this file except in compliance with the License.
     10  * You may obtain a copy of the License at
     11  *
     12  *     http://www.apache.org/licenses/LICENSE-2.0
     13  *
     14  * Unless required by applicable law or agreed to in writing, software
     15  * distributed under the License is distributed on an "AS IS" BASIS,
     16  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     17  * See the License for the specific language governing permissions and
     18  * limitations under the License.
     19  */
     20 package org.jivesoftware.smackx.filetransfer;
     21 
     22 import java.net.URLConnection;
     23 import java.util.ArrayList;
     24 import java.util.Arrays;
     25 import java.util.Collection;
     26 import java.util.Collections;
     27 import java.util.Iterator;
     28 import java.util.List;
     29 import java.util.Map;
     30 import java.util.Random;
     31 import java.util.concurrent.ConcurrentHashMap;
     32 
     33 import org.jivesoftware.smack.Connection;
     34 import org.jivesoftware.smack.ConnectionListener;
     35 import org.jivesoftware.smack.PacketCollector;
     36 import org.jivesoftware.smack.XMPPException;
     37 import org.jivesoftware.smack.filter.PacketIDFilter;
     38 import org.jivesoftware.smack.packet.IQ;
     39 import org.jivesoftware.smack.packet.Packet;
     40 import org.jivesoftware.smack.packet.XMPPError;
     41 import org.jivesoftware.smackx.Form;
     42 import org.jivesoftware.smackx.FormField;
     43 import org.jivesoftware.smackx.ServiceDiscoveryManager;
     44 import org.jivesoftware.smackx.bytestreams.ibb.InBandBytestreamManager;
     45 import org.jivesoftware.smackx.bytestreams.socks5.Socks5BytestreamManager;
     46 import org.jivesoftware.smackx.packet.DataForm;
     47 import org.jivesoftware.smackx.packet.StreamInitiation;
     48 
     49 /**
     50  * Manages the negotiation of file transfers according to JEP-0096. If a file is
     51  * being sent the remote user chooses the type of stream under which the file
     52  * will be sent.
     53  *
     54  * @author Alexander Wenckus
     55  * @see <a href="http://xmpp.org/extensions/xep-0096.html">XEP-0096: SI File Transfer</a>
     56  */
     57 public class FileTransferNegotiator {
     58 
     59     // Static
     60 
     61     private static final String[] NAMESPACE = {
     62             "http://jabber.org/protocol/si/profile/file-transfer",
     63             "http://jabber.org/protocol/si"};
     64 
     65     private static final Map<Connection, FileTransferNegotiator> transferObject =
     66             new ConcurrentHashMap<Connection, FileTransferNegotiator>();
     67 
     68     private static final String STREAM_INIT_PREFIX = "jsi_";
     69 
     70     protected static final String STREAM_DATA_FIELD_NAME = "stream-method";
     71 
     72     private static final Random randomGenerator = new Random();
     73 
     74     /**
     75      * A static variable to use only offer IBB for file transfer. It is generally recommend to only
     76      * set this variable to true for testing purposes as IBB is the backup file transfer method
     77      * and shouldn't be used as the only transfer method in production systems.
     78      */
     79     public static boolean IBB_ONLY = (System.getProperty("ibb") != null);//true;
     80 
     81     /**
     82      * Returns the file transfer negotiator related to a particular connection.
     83      * When this class is requested on a particular connection the file transfer
     84      * service is automatically enabled.
     85      *
     86      * @param connection The connection for which the transfer manager is desired
     87      * @return The IMFileTransferManager
     88      */
     89     public static FileTransferNegotiator getInstanceFor(
     90             final Connection connection) {
     91         if (connection == null) {
     92             throw new IllegalArgumentException("Connection cannot be null");
     93         }
     94         if (!connection.isConnected()) {
     95             return null;
     96         }
     97 
     98         if (transferObject.containsKey(connection)) {
     99             return transferObject.get(connection);
    100         }
    101         else {
    102             FileTransferNegotiator transfer = new FileTransferNegotiator(
    103                     connection);
    104             setServiceEnabled(connection, true);
    105             transferObject.put(connection, transfer);
    106             return transfer;
    107         }
    108     }
    109 
    110     /**
    111      * Enable the Jabber services related to file transfer on the particular
    112      * connection.
    113      *
    114      * @param connection The connection on which to enable or disable the services.
    115      * @param isEnabled  True to enable, false to disable.
    116      */
    117     public static void setServiceEnabled(final Connection connection,
    118             final boolean isEnabled) {
    119         ServiceDiscoveryManager manager = ServiceDiscoveryManager
    120                 .getInstanceFor(connection);
    121 
    122         List<String> namespaces = new ArrayList<String>();
    123         namespaces.addAll(Arrays.asList(NAMESPACE));
    124         namespaces.add(InBandBytestreamManager.NAMESPACE);
    125         if (!IBB_ONLY) {
    126             namespaces.add(Socks5BytestreamManager.NAMESPACE);
    127         }
    128 
    129         for (String namespace : namespaces) {
    130             if (isEnabled) {
    131                 if (!manager.includesFeature(namespace)) {
    132                     manager.addFeature(namespace);
    133                 }
    134             } else {
    135                 manager.removeFeature(namespace);
    136             }
    137         }
    138 
    139     }
    140 
    141     /**
    142      * Checks to see if all file transfer related services are enabled on the
    143      * connection.
    144      *
    145      * @param connection The connection to check
    146      * @return True if all related services are enabled, false if they are not.
    147      */
    148     public static boolean isServiceEnabled(final Connection connection) {
    149         ServiceDiscoveryManager manager = ServiceDiscoveryManager
    150                 .getInstanceFor(connection);
    151 
    152         List<String> namespaces = new ArrayList<String>();
    153         namespaces.addAll(Arrays.asList(NAMESPACE));
    154         namespaces.add(InBandBytestreamManager.NAMESPACE);
    155         if (!IBB_ONLY) {
    156             namespaces.add(Socks5BytestreamManager.NAMESPACE);
    157         }
    158 
    159         for (String namespace : namespaces) {
    160             if (!manager.includesFeature(namespace)) {
    161                 return false;
    162             }
    163         }
    164         return true;
    165     }
    166 
    167     /**
    168      * A convenience method to create an IQ packet.
    169      *
    170      * @param ID   The packet ID of the
    171      * @param to   To whom the packet is addressed.
    172      * @param from From whom the packet is sent.
    173      * @param type The IQ type of the packet.
    174      * @return The created IQ packet.
    175      */
    176     public static IQ createIQ(final String ID, final String to,
    177             final String from, final IQ.Type type) {
    178         IQ iqPacket = new IQ() {
    179             public String getChildElementXML() {
    180                 return null;
    181             }
    182         };
    183         iqPacket.setPacketID(ID);
    184         iqPacket.setTo(to);
    185         iqPacket.setFrom(from);
    186         iqPacket.setType(type);
    187 
    188         return iqPacket;
    189     }
    190 
    191     /**
    192      * Returns a collection of the supported transfer protocols.
    193      *
    194      * @return Returns a collection of the supported transfer protocols.
    195      */
    196     public static Collection<String> getSupportedProtocols() {
    197         List<String> protocols = new ArrayList<String>();
    198         protocols.add(InBandBytestreamManager.NAMESPACE);
    199         if (!IBB_ONLY) {
    200             protocols.add(Socks5BytestreamManager.NAMESPACE);
    201         }
    202         return Collections.unmodifiableList(protocols);
    203     }
    204 
    205     // non-static
    206 
    207     private final Connection connection;
    208 
    209     private final StreamNegotiator byteStreamTransferManager;
    210 
    211     private final StreamNegotiator inbandTransferManager;
    212 
    213     private FileTransferNegotiator(final Connection connection) {
    214         configureConnection(connection);
    215 
    216         this.connection = connection;
    217         byteStreamTransferManager = new Socks5TransferNegotiator(connection);
    218         inbandTransferManager = new IBBTransferNegotiator(connection);
    219     }
    220 
    221     private void configureConnection(final Connection connection) {
    222         connection.addConnectionListener(new ConnectionListener() {
    223             public void connectionClosed() {
    224                 cleanup(connection);
    225             }
    226 
    227             public void connectionClosedOnError(Exception e) {
    228                 cleanup(connection);
    229             }
    230 
    231             public void reconnectionFailed(Exception e) {
    232                 // ignore
    233             }
    234 
    235             public void reconnectionSuccessful() {
    236                 // ignore
    237             }
    238 
    239             public void reconnectingIn(int seconds) {
    240                 // ignore
    241             }
    242         });
    243     }
    244 
    245     private void cleanup(final Connection connection) {
    246         if (transferObject.remove(connection) != null) {
    247             inbandTransferManager.cleanup();
    248         }
    249     }
    250 
    251     /**
    252      * Selects an appropriate stream negotiator after examining the incoming file transfer request.
    253      *
    254      * @param request The related file transfer request.
    255      * @return The file transfer object that handles the transfer
    256      * @throws XMPPException If there are either no stream methods contained in the packet, or
    257      *                       there is not an appropriate stream method.
    258      */
    259     public StreamNegotiator selectStreamNegotiator(
    260             FileTransferRequest request) throws XMPPException {
    261         StreamInitiation si = request.getStreamInitiation();
    262         FormField streamMethodField = getStreamMethodField(si
    263                 .getFeatureNegotiationForm());
    264 
    265         if (streamMethodField == null) {
    266             String errorMessage = "No stream methods contained in packet.";
    267             XMPPError error = new XMPPError(XMPPError.Condition.bad_request, errorMessage);
    268             IQ iqPacket = createIQ(si.getPacketID(), si.getFrom(), si.getTo(),
    269                     IQ.Type.ERROR);
    270             iqPacket.setError(error);
    271             connection.sendPacket(iqPacket);
    272             throw new XMPPException(errorMessage, error);
    273         }
    274 
    275         // select the appropriate protocol
    276 
    277         StreamNegotiator selectedStreamNegotiator;
    278         try {
    279             selectedStreamNegotiator = getNegotiator(streamMethodField);
    280         }
    281         catch (XMPPException e) {
    282             IQ iqPacket = createIQ(si.getPacketID(), si.getFrom(), si.getTo(),
    283                     IQ.Type.ERROR);
    284             iqPacket.setError(e.getXMPPError());
    285             connection.sendPacket(iqPacket);
    286             throw e;
    287         }
    288 
    289         // return the appropriate negotiator
    290 
    291         return selectedStreamNegotiator;
    292     }
    293 
    294     private FormField getStreamMethodField(DataForm form) {
    295         FormField field = null;
    296         for (Iterator<FormField> it = form.getFields(); it.hasNext();) {
    297             field = it.next();
    298             if (field.getVariable().equals(STREAM_DATA_FIELD_NAME)) {
    299                 break;
    300             }
    301             field = null;
    302         }
    303         return field;
    304     }
    305 
    306     private StreamNegotiator getNegotiator(final FormField field)
    307             throws XMPPException {
    308         String variable;
    309         boolean isByteStream = false;
    310         boolean isIBB = false;
    311         for (Iterator<FormField.Option> it = field.getOptions(); it.hasNext();) {
    312             variable = it.next().getValue();
    313             if (variable.equals(Socks5BytestreamManager.NAMESPACE) && !IBB_ONLY) {
    314                 isByteStream = true;
    315             }
    316             else if (variable.equals(InBandBytestreamManager.NAMESPACE)) {
    317                 isIBB = true;
    318             }
    319         }
    320 
    321         if (!isByteStream && !isIBB) {
    322             XMPPError error = new XMPPError(XMPPError.Condition.bad_request,
    323                     "No acceptable transfer mechanism");
    324             throw new XMPPException(error.getMessage(), error);
    325         }
    326 
    327        //if (isByteStream && isIBB && field.getType().equals(FormField.TYPE_LIST_MULTI)) {
    328         if (isByteStream && isIBB) {
    329             return new FaultTolerantNegotiator(connection,
    330                     byteStreamTransferManager,
    331                     inbandTransferManager);
    332         }
    333         else if (isByteStream) {
    334             return byteStreamTransferManager;
    335         }
    336         else {
    337             return inbandTransferManager;
    338         }
    339     }
    340 
    341     /**
    342      * Reject a stream initiation request from a remote user.
    343      *
    344      * @param si The Stream Initiation request to reject.
    345      */
    346     public void rejectStream(final StreamInitiation si) {
    347         XMPPError error = new XMPPError(XMPPError.Condition.forbidden, "Offer Declined");
    348         IQ iqPacket = createIQ(si.getPacketID(), si.getFrom(), si.getTo(),
    349                 IQ.Type.ERROR);
    350         iqPacket.setError(error);
    351         connection.sendPacket(iqPacket);
    352     }
    353 
    354     /**
    355      * Returns a new, unique, stream ID to identify a file transfer.
    356      *
    357      * @return Returns a new, unique, stream ID to identify a file transfer.
    358      */
    359     public String getNextStreamID() {
    360         StringBuilder buffer = new StringBuilder();
    361         buffer.append(STREAM_INIT_PREFIX);
    362         buffer.append(Math.abs(randomGenerator.nextLong()));
    363 
    364         return buffer.toString();
    365     }
    366 
    367     /**
    368      * Send a request to another user to send them a file. The other user has
    369      * the option of, accepting, rejecting, or not responding to a received file
    370      * transfer request.
    371      * <p/>
    372      * If they accept, the packet will contain the other user's chosen stream
    373      * type to send the file across. The two choices this implementation
    374      * provides to the other user for file transfer are <a
    375      * href="http://www.jabber.org/jeps/jep-0065.html">SOCKS5 Bytestreams</a>,
    376      * which is the preferred method of transfer, and <a
    377      * href="http://www.jabber.org/jeps/jep-0047.html">In-Band Bytestreams</a>,
    378      * which is the fallback mechanism.
    379      * <p/>
    380      * The other user may choose to decline the file request if they do not
    381      * desire the file, their client does not support JEP-0096, or if there are
    382      * no acceptable means to transfer the file.
    383      * <p/>
    384      * Finally, if the other user does not respond this method will return null
    385      * after the specified timeout.
    386      *
    387      * @param userID          The userID of the user to whom the file will be sent.
    388      * @param streamID        The unique identifier for this file transfer.
    389      * @param fileName        The name of this file. Preferably it should include an
    390      *                        extension as it is used to determine what type of file it is.
    391      * @param size            The size, in bytes, of the file.
    392      * @param desc            A description of the file.
    393      * @param responseTimeout The amount of time, in milliseconds, to wait for the remote
    394      *                        user to respond. If they do not respond in time, this
    395      * @return Returns the stream negotiator selected by the peer.
    396      * @throws XMPPException Thrown if there is an error negotiating the file transfer.
    397      */
    398     public StreamNegotiator negotiateOutgoingTransfer(final String userID,
    399             final String streamID, final String fileName, final long size,
    400             final String desc, int responseTimeout) throws XMPPException {
    401         StreamInitiation si = new StreamInitiation();
    402         si.setSesssionID(streamID);
    403         si.setMimeType(URLConnection.guessContentTypeFromName(fileName));
    404 
    405         StreamInitiation.File siFile = new StreamInitiation.File(fileName, size);
    406         siFile.setDesc(desc);
    407         si.setFile(siFile);
    408 
    409         si.setFeatureNegotiationForm(createDefaultInitiationForm());
    410 
    411         si.setFrom(connection.getUser());
    412         si.setTo(userID);
    413         si.setType(IQ.Type.SET);
    414 
    415         PacketCollector collector = connection
    416                 .createPacketCollector(new PacketIDFilter(si.getPacketID()));
    417         connection.sendPacket(si);
    418         Packet siResponse = collector.nextResult(responseTimeout);
    419         collector.cancel();
    420 
    421         if (siResponse instanceof IQ) {
    422             IQ iqResponse = (IQ) siResponse;
    423             if (iqResponse.getType().equals(IQ.Type.RESULT)) {
    424                 StreamInitiation response = (StreamInitiation) siResponse;
    425                 return getOutgoingNegotiator(getStreamMethodField(response
    426                         .getFeatureNegotiationForm()));
    427 
    428             }
    429             else if (iqResponse.getType().equals(IQ.Type.ERROR)) {
    430                 throw new XMPPException(iqResponse.getError());
    431             }
    432             else {
    433                 throw new XMPPException("File transfer response unreadable");
    434             }
    435         }
    436         else {
    437             return null;
    438         }
    439     }
    440 
    441     private StreamNegotiator getOutgoingNegotiator(final FormField field)
    442             throws XMPPException {
    443         String variable;
    444         boolean isByteStream = false;
    445         boolean isIBB = false;
    446         for (Iterator<String> it = field.getValues(); it.hasNext();) {
    447             variable = it.next();
    448             if (variable.equals(Socks5BytestreamManager.NAMESPACE) && !IBB_ONLY) {
    449                 isByteStream = true;
    450             }
    451             else if (variable.equals(InBandBytestreamManager.NAMESPACE)) {
    452                 isIBB = true;
    453             }
    454         }
    455 
    456         if (!isByteStream && !isIBB) {
    457             XMPPError error = new XMPPError(XMPPError.Condition.bad_request,
    458                     "No acceptable transfer mechanism");
    459             throw new XMPPException(error.getMessage(), error);
    460         }
    461 
    462         if (isByteStream && isIBB) {
    463             return new FaultTolerantNegotiator(connection,
    464                     byteStreamTransferManager, inbandTransferManager);
    465         }
    466         else if (isByteStream) {
    467             return byteStreamTransferManager;
    468         }
    469         else {
    470             return inbandTransferManager;
    471         }
    472     }
    473 
    474     private DataForm createDefaultInitiationForm() {
    475         DataForm form = new DataForm(Form.TYPE_FORM);
    476         FormField field = new FormField(STREAM_DATA_FIELD_NAME);
    477         field.setType(FormField.TYPE_LIST_SINGLE);
    478         if (!IBB_ONLY) {
    479             field.addOption(new FormField.Option(Socks5BytestreamManager.NAMESPACE));
    480         }
    481         field.addOption(new FormField.Option(InBandBytestreamManager.NAMESPACE));
    482         form.addField(field);
    483         return form;
    484     }
    485 }
    486