001/**
002 *
003 * Copyright 2003-2006 Jive Software.
004 *
005 * Licensed under the Apache License, Version 2.0 (the "License");
006 * you may not use this file except in compliance with the License.
007 * You may obtain a copy of the License at
008 *
009 *     http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.jivesoftware.smackx.filetransfer;
018
019import java.net.URLConnection;
020import java.util.ArrayList;
021import java.util.Arrays;
022import java.util.Collection;
023import java.util.Collections;
024import java.util.List;
025import java.util.Map;
026import java.util.Random;
027import java.util.WeakHashMap;
028
029import org.jivesoftware.smack.Manager;
030import org.jivesoftware.smack.SmackException.NoResponseException;
031import org.jivesoftware.smack.SmackException.NotConnectedException;
032import org.jivesoftware.smack.XMPPConnection;
033import org.jivesoftware.smack.XMPPException.XMPPErrorException;
034import org.jivesoftware.smack.packet.IQ;
035import org.jivesoftware.smack.packet.Stanza;
036import org.jivesoftware.smack.packet.XMPPError;
037
038import org.jivesoftware.smackx.bytestreams.ibb.packet.DataPacketExtension;
039import org.jivesoftware.smackx.bytestreams.socks5.packet.Bytestream;
040import org.jivesoftware.smackx.disco.ServiceDiscoveryManager;
041import org.jivesoftware.smackx.filetransfer.FileTransferException.NoAcceptableTransferMechanisms;
042import org.jivesoftware.smackx.filetransfer.FileTransferException.NoStreamMethodsOfferedException;
043import org.jivesoftware.smackx.si.packet.StreamInitiation;
044import org.jivesoftware.smackx.xdata.FormField;
045import org.jivesoftware.smackx.xdata.packet.DataForm;
046
047import org.jxmpp.jid.Jid;
048
049/**
050 * Manages the negotiation of file transfers according to XEP-0096. If a file is
051 * being sent the remote user chooses the type of stream under which the file
052 * will be sent.
053 *
054 * @author Alexander Wenckus
055 * @see <a href="http://xmpp.org/extensions/xep-0096.html">XEP-0096: SI File Transfer</a>
056 */
057public final class FileTransferNegotiator extends Manager {
058
059    public static final String SI_NAMESPACE = "http://jabber.org/protocol/si";
060    public static final String SI_PROFILE_FILE_TRANSFER_NAMESPACE = "http://jabber.org/protocol/si/profile/file-transfer";
061    private static final String[] NAMESPACE = { SI_NAMESPACE, SI_PROFILE_FILE_TRANSFER_NAMESPACE };
062
063    private static final Map<XMPPConnection, FileTransferNegotiator> INSTANCES = new WeakHashMap<>();
064
065    private static final String STREAM_INIT_PREFIX = "jsi_";
066
067    protected static final String STREAM_DATA_FIELD_NAME = "stream-method";
068
069    private static final Random randomGenerator = new Random();
070
071    /**
072     * A static variable to use only offer IBB for file transfer. It is generally recommend to only
073     * set this variable to true for testing purposes as IBB is the backup file transfer method
074     * and shouldn't be used as the only transfer method in production systems.
075     */
076    public static boolean IBB_ONLY = (System.getProperty("ibb") != null);//true;
077
078    /**
079     * Returns the file transfer negotiator related to a particular connection.
080     * When this class is requested on a particular connection the file transfer
081     * service is automatically enabled.
082     *
083     * @param connection The connection for which the transfer manager is desired
084     * @return The FileTransferNegotiator
085     */
086    public static synchronized FileTransferNegotiator getInstanceFor(
087            final XMPPConnection connection) {
088        FileTransferNegotiator fileTransferNegotiator = INSTANCES.get(connection);
089        if (fileTransferNegotiator == null) {
090            fileTransferNegotiator = new FileTransferNegotiator(connection);
091            INSTANCES.put(connection, fileTransferNegotiator);
092        }
093        return fileTransferNegotiator;
094    }
095
096    /**
097     * Enable the Jabber services related to file transfer on the particular
098     * connection.
099     *
100     * @param connection The connection on which to enable or disable the services.
101     * @param isEnabled  True to enable, false to disable.
102     */
103    private static void setServiceEnabled(final XMPPConnection connection,
104            final boolean isEnabled) {
105        ServiceDiscoveryManager manager = ServiceDiscoveryManager
106                .getInstanceFor(connection);
107
108        List<String> namespaces = new ArrayList<>();
109        namespaces.addAll(Arrays.asList(NAMESPACE));
110        namespaces.add(DataPacketExtension.NAMESPACE);
111        if (!IBB_ONLY) {
112            namespaces.add(Bytestream.NAMESPACE);
113        }
114
115        for (String namespace : namespaces) {
116            if (isEnabled) {
117                manager.addFeature(namespace);
118            } else {
119                manager.removeFeature(namespace);
120            }
121        }
122    }
123
124    /**
125     * Checks to see if all file transfer related services are enabled on the
126     * connection.
127     *
128     * @param connection The connection to check
129     * @return True if all related services are enabled, false if they are not.
130     */
131    public static boolean isServiceEnabled(final XMPPConnection connection) {
132        ServiceDiscoveryManager manager = ServiceDiscoveryManager
133                .getInstanceFor(connection);
134
135        List<String> namespaces = new ArrayList<>();
136        namespaces.addAll(Arrays.asList(NAMESPACE));
137        namespaces.add(DataPacketExtension.NAMESPACE);
138        if (!IBB_ONLY) {
139            namespaces.add(Bytestream.NAMESPACE);
140        }
141
142        for (String namespace : namespaces) {
143            if (!manager.includesFeature(namespace)) {
144                return false;
145            }
146        }
147        return true;
148    }
149
150    /**
151     * Returns a collection of the supported transfer protocols.
152     *
153     * @return Returns a collection of the supported transfer protocols.
154     */
155    public static Collection<String> getSupportedProtocols() {
156        List<String> protocols = new ArrayList<>();
157        protocols.add(DataPacketExtension.NAMESPACE);
158        if (!IBB_ONLY) {
159            protocols.add(Bytestream.NAMESPACE);
160        }
161        return Collections.unmodifiableList(protocols);
162    }
163
164    // non-static
165
166    private final StreamNegotiator byteStreamTransferManager;
167
168    private final StreamNegotiator inbandTransferManager;
169
170    private FileTransferNegotiator(final XMPPConnection connection) {
171        super(connection);
172        byteStreamTransferManager = new Socks5TransferNegotiator(connection);
173        inbandTransferManager = new IBBTransferNegotiator(connection);
174
175        setServiceEnabled(connection, true);
176    }
177
178    /**
179     * Selects an appropriate stream negotiator after examining the incoming file transfer request.
180     *
181     * @param request The related file transfer request.
182     * @return The file transfer object that handles the transfer
183     * @throws NoStreamMethodsOfferedException If there are either no stream methods contained in the packet, or
184     *                       there is not an appropriate stream method.
185     * @throws NotConnectedException 
186     * @throws NoAcceptableTransferMechanisms 
187     * @throws InterruptedException 
188     */
189    public StreamNegotiator selectStreamNegotiator(
190            FileTransferRequest request) throws NotConnectedException, NoStreamMethodsOfferedException, NoAcceptableTransferMechanisms, InterruptedException {
191        StreamInitiation si = request.getStreamInitiation();
192        FormField streamMethodField = getStreamMethodField(si
193                .getFeatureNegotiationForm());
194
195        if (streamMethodField == null) {
196            String errorMessage = "No stream methods contained in stanza.";
197            XMPPError.Builder error = XMPPError.from(XMPPError.Condition.bad_request, errorMessage);
198            IQ iqPacket = IQ.createErrorResponse(si, error);
199            connection().sendStanza(iqPacket);
200            throw new FileTransferException.NoStreamMethodsOfferedException();
201        }
202
203        // select the appropriate protocol
204        StreamNegotiator selectedStreamNegotiator;
205        try {
206            selectedStreamNegotiator = getNegotiator(streamMethodField);
207        }
208        catch (NoAcceptableTransferMechanisms e) {
209            IQ iqPacket = IQ.createErrorResponse(si, XMPPError.from(XMPPError.Condition.bad_request, "No acceptable transfer mechanism"));
210            connection().sendStanza(iqPacket);
211            throw e;
212        }
213
214        // return the appropriate negotiator
215
216        return selectedStreamNegotiator;
217    }
218
219    private static FormField getStreamMethodField(DataForm form) {
220        return form.getField(STREAM_DATA_FIELD_NAME);
221    }
222
223    private StreamNegotiator getNegotiator(final FormField field)
224            throws NoAcceptableTransferMechanisms {
225        String variable;
226        boolean isByteStream = false;
227        boolean isIBB = false;
228        for (FormField.Option option : field.getOptions()) {
229            variable = option.getValue();
230            if (variable.equals(Bytestream.NAMESPACE) && !IBB_ONLY) {
231                isByteStream = true;
232            }
233            else if (variable.equals(DataPacketExtension.NAMESPACE)) {
234                isIBB = true;
235            }
236        }
237
238        if (!isByteStream && !isIBB) {
239            throw new FileTransferException.NoAcceptableTransferMechanisms();
240        }
241
242        if (isByteStream && isIBB) { 
243            return new FaultTolerantNegotiator(connection(),
244                    byteStreamTransferManager,
245                    inbandTransferManager);
246        }
247        else if (isByteStream) {
248            return byteStreamTransferManager;
249        }
250        else {
251            return inbandTransferManager;
252        }
253    }
254
255    /**
256     * Returns a new, unique, stream ID to identify a file transfer.
257     *
258     * @return Returns a new, unique, stream ID to identify a file transfer.
259     */
260    public static String getNextStreamID() {
261        StringBuilder buffer = new StringBuilder();
262        buffer.append(STREAM_INIT_PREFIX);
263        buffer.append(Math.abs(randomGenerator.nextLong()));
264
265        return buffer.toString();
266    }
267
268    /**
269     * Send a request to another user to send them a file. The other user has
270     * the option of, accepting, rejecting, or not responding to a received file
271     * transfer request.
272     * <p>
273     * If they accept, the stanza(/packet) will contain the other user's chosen stream
274     * type to send the file across. The two choices this implementation
275     * provides to the other user for file transfer are <a
276     * href="http://www.xmpp.org/extensions/jep-0065.html">SOCKS5 Bytestreams</a>,
277     * which is the preferred method of transfer, and <a
278     * href="http://www.xmpp.org/extensions/jep-0047.html">In-Band Bytestreams</a>,
279     * which is the fallback mechanism.
280     * </p>
281     * <p>
282     * The other user may choose to decline the file request if they do not
283     * desire the file, their client does not support XEP-0096, or if there are
284     * no acceptable means to transfer the file.
285     * </p>
286     * Finally, if the other user does not respond this method will return null
287     * after the specified timeout.
288     *
289     * @param userID          The userID of the user to whom the file will be sent.
290     * @param streamID        The unique identifier for this file transfer.
291     * @param fileName        The name of this file. Preferably it should include an
292     *                        extension as it is used to determine what type of file it is.
293     * @param size            The size, in bytes, of the file.
294     * @param desc            A description of the file.
295     * @param responseTimeout The amount of time, in milliseconds, to wait for the remote
296     *                        user to respond. If they do not respond in time, this
297     * @return Returns the stream negotiator selected by the peer.
298     * @throws XMPPErrorException Thrown if there is an error negotiating the file transfer.
299     * @throws NotConnectedException 
300     * @throws NoResponseException 
301     * @throws NoAcceptableTransferMechanisms 
302     * @throws InterruptedException 
303     */
304    public StreamNegotiator negotiateOutgoingTransfer(final Jid userID,
305            final String streamID, final String fileName, final long size,
306            final String desc, int responseTimeout) throws XMPPErrorException, NotConnectedException, NoResponseException, NoAcceptableTransferMechanisms, InterruptedException {
307        StreamInitiation si = new StreamInitiation();
308        si.setSessionID(streamID);
309        si.setMimeType(URLConnection.guessContentTypeFromName(fileName));
310
311        StreamInitiation.File siFile = new StreamInitiation.File(fileName, size);
312        siFile.setDesc(desc);
313        si.setFile(siFile);
314
315        si.setFeatureNegotiationForm(createDefaultInitiationForm());
316
317        si.setFrom(connection().getUser());
318        si.setTo(userID);
319        si.setType(IQ.Type.set);
320
321        Stanza siResponse = connection().createStanzaCollectorAndSend(si).nextResultOrThrow(
322                        responseTimeout);
323
324        if (siResponse instanceof IQ) {
325            IQ iqResponse = (IQ) siResponse;
326            if (iqResponse.getType().equals(IQ.Type.result)) {
327                StreamInitiation response = (StreamInitiation) siResponse;
328                return getOutgoingNegotiator(getStreamMethodField(response
329                        .getFeatureNegotiationForm()));
330
331            }
332            else {
333                throw new XMPPErrorException(iqResponse, iqResponse.getError());
334            }
335        }
336        else {
337            return null;
338        }
339    }
340
341    private StreamNegotiator getOutgoingNegotiator(final FormField field) throws NoAcceptableTransferMechanisms {
342        boolean isByteStream = false;
343        boolean isIBB = false;
344        for (String variable : field.getValues()) {
345            if (variable.equals(Bytestream.NAMESPACE) && !IBB_ONLY) {
346                isByteStream = true;
347            }
348            else if (variable.equals(DataPacketExtension.NAMESPACE)) {
349                isIBB = true;
350            }
351        }
352
353        if (!isByteStream && !isIBB) {
354            throw new FileTransferException.NoAcceptableTransferMechanisms();
355        }
356
357        if (isByteStream && isIBB) {
358            return new FaultTolerantNegotiator(connection(),
359                    byteStreamTransferManager, inbandTransferManager);
360        }
361        else if (isByteStream) {
362            return byteStreamTransferManager;
363        }
364        else {
365            return inbandTransferManager;
366        }
367    }
368
369    private static DataForm createDefaultInitiationForm() {
370        DataForm form = new DataForm(DataForm.Type.form);
371        FormField field = new FormField(STREAM_DATA_FIELD_NAME);
372        field.setType(FormField.Type.list_single);
373        if (!IBB_ONLY) {
374            field.addOption(new FormField.Option(Bytestream.NAMESPACE));
375        }
376        field.addOption(new FormField.Option(DataPacketExtension.NAMESPACE));
377        form.addField(field);
378        return form;
379    }
380}