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}