• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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      */
getInstanceFor( final Connection connection)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      */
setServiceEnabled(final Connection connection, final boolean isEnabled)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      */
isServiceEnabled(final Connection connection)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      */
createIQ(final String ID, final String to, final String from, final IQ.Type type)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      */
getSupportedProtocols()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 
FileTransferNegotiator(final Connection connection)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 
configureConnection(final Connection connection)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 
cleanup(final Connection connection)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      */
selectStreamNegotiator( FileTransferRequest request)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 
getStreamMethodField(DataForm form)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 
getNegotiator(final FormField field)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      */
rejectStream(final StreamInitiation si)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      */
getNextStreamID()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      */
negotiateOutgoingTransfer(final String userID, final String streamID, final String fileName, final long size, final String desc, int responseTimeout)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 
getOutgoingNegotiator(final FormField field)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 
createDefaultInitiationForm()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