aboutsummaryrefslogtreecommitdiff
path: root/src/org/jivesoftware/smackx/filetransfer/FileTransferNegotiator.java
blob: d1fb7bf3dd6a755e76998921a57afa3c858b71c7 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
/**
 * $RCSfile$
 * $Revision$
 * $Date$
 *
 * Copyright 2003-2006 Jive Software.
 *
 * 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.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.filter.PacketIDFilter;
import org.jivesoftware.smack.packet.IQ;
import org.jivesoftware.smack.packet.Packet;
import org.jivesoftware.smack.packet.XMPPError;
import org.jivesoftware.smackx.Form;
import org.jivesoftware.smackx.FormField;
import org.jivesoftware.smackx.ServiceDiscoveryManager;
import org.jivesoftware.smackx.bytestreams.ibb.InBandBytestreamManager;
import org.jivesoftware.smackx.bytestreams.socks5.Socks5BytestreamManager;
import org.jivesoftware.smackx.packet.DataForm;
import org.jivesoftware.smackx.packet.StreamInitiation;

/**
 * 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.setSesssionID(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
                .createPacketCollector(new PacketIDFilter(si.getPacketID()));
        connection.sendPacket(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;
    }
}