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