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