From 8d0db1a339e90e2fb85975ccd1c25cda7da4b7f7 Mon Sep 17 00:00:00 2001 From: Alex Wenckus Date: Fri, 3 Feb 2006 18:44:22 +0000 Subject: [PATCH] File Transfer. (SMACK-72) (SMACK-122) git-svn-id: http://svn.igniterealtime.org/svn/repos/smack/trunk@3395 b35dd754-fafc-0310-a699-88a17e54d16e --- build/resources/META-INF/smack-config.xml | 1 + build/resources/META-INF/smack.providers | 31 + documentation/extensions/filetransfer.html | 178 +++ documentation/extensions/intro.html | 7 +- documentation/extensions/toc.html | 1 + .../smack/filter/IQTypeFilter.java | 32 + .../smackx/filetransfer/Base64.java | 1416 +++++++++++++++++ .../smackx/filetransfer/FileTransfer.java | 323 ++++ .../filetransfer/FileTransferListener.java | 20 + .../filetransfer/FileTransferManager.java | 162 ++ .../filetransfer/FileTransferNegotiator.java | 387 +++++ .../filetransfer/FileTransferRequest.java | 122 ++ .../filetransfer/IBBTransferNegotiator.java | 398 +++++ .../filetransfer/IncomingFileTransfer.java | 171 ++ .../filetransfer/OutgoingFileTransfer.java | 348 ++++ .../Socks5TransferNegotiator.java | 725 +++++++++ .../smackx/filetransfer/StreamNegotiator.java | 105 ++ .../smackx/packet/ByteStream.java | 465 ++++++ .../smackx/packet/IBBExtensions.java | 225 +++ .../smackx/packet/StreamInitiation.java | 403 +++++ .../smackx/provider/BytestreamsProvider.java | 105 ++ .../smackx/provider/IBBProviders.java | 69 + .../provider/StreamInitiationProvider.java | 88 + 23 files changed, 5781 insertions(+), 1 deletion(-) create mode 100644 documentation/extensions/filetransfer.html create mode 100644 source/org/jivesoftware/smack/filter/IQTypeFilter.java create mode 100644 source/org/jivesoftware/smackx/filetransfer/Base64.java create mode 100644 source/org/jivesoftware/smackx/filetransfer/FileTransfer.java create mode 100644 source/org/jivesoftware/smackx/filetransfer/FileTransferListener.java create mode 100644 source/org/jivesoftware/smackx/filetransfer/FileTransferManager.java create mode 100644 source/org/jivesoftware/smackx/filetransfer/FileTransferNegotiator.java create mode 100644 source/org/jivesoftware/smackx/filetransfer/FileTransferRequest.java create mode 100644 source/org/jivesoftware/smackx/filetransfer/IBBTransferNegotiator.java create mode 100644 source/org/jivesoftware/smackx/filetransfer/IncomingFileTransfer.java create mode 100644 source/org/jivesoftware/smackx/filetransfer/OutgoingFileTransfer.java create mode 100644 source/org/jivesoftware/smackx/filetransfer/Socks5TransferNegotiator.java create mode 100644 source/org/jivesoftware/smackx/filetransfer/StreamNegotiator.java create mode 100644 source/org/jivesoftware/smackx/packet/ByteStream.java create mode 100644 source/org/jivesoftware/smackx/packet/IBBExtensions.java create mode 100644 source/org/jivesoftware/smackx/packet/StreamInitiation.java create mode 100644 source/org/jivesoftware/smackx/provider/BytestreamsProvider.java create mode 100644 source/org/jivesoftware/smackx/provider/IBBProviders.java create mode 100644 source/org/jivesoftware/smackx/provider/StreamInitiationProvider.java diff --git a/build/resources/META-INF/smack-config.xml b/build/resources/META-INF/smack-config.xml index 6d373de68..eb0ea25ce 100644 --- a/build/resources/META-INF/smack-config.xml +++ b/build/resources/META-INF/smack-config.xml @@ -7,6 +7,7 @@ org.jivesoftware.smackx.ServiceDiscoveryManager org.jivesoftware.smackx.XHTMLManager org.jivesoftware.smackx.muc.MultiUserChat + org.jivesoftware.smackx.filetransfer.FileTransferManager diff --git a/build/resources/META-INF/smack.providers b/build/resources/META-INF/smack.providers index 5ece115ef..8033f7676 100644 --- a/build/resources/META-INF/smack.providers +++ b/build/resources/META-INF/smack.providers @@ -156,4 +156,35 @@ org.jivesoftware.smackx.provider.MultipleAddressesProvider + + + si + http://jabber.org/protocol/si + org.jivesoftware.smackx.provider.StreamInitiationProvider + + + + query + http://jabber.org/protocol/bytestreams + org.jivesoftware.smackx.provider.BytestreamsProvider + + + + open + http://jabber.org/protocol/ibb + org.jivesoftware.smackx.provider.IBBProviders$Open + + + + close + http://jabber.org/protocol/ibb + org.jivesoftware.smackx.provider.IBBProviders$Close + + + + data + http://jabber.org/protocol/ibb + org.jivesoftware.smackx.provider.IBBProviders$Data + + \ No newline at end of file diff --git a/documentation/extensions/filetransfer.html b/documentation/extensions/filetransfer.html new file mode 100644 index 000000000..70082e8f9 --- /dev/null +++ b/documentation/extensions/filetransfer.html @@ -0,0 +1,178 @@ + + + + +File Transfer + + + + + +
File Transfer

+ +The file transfer extension allows the user to transmit and receive files. + +

+JEP related: JEP-95 +JEP-96 +JEP-65 +JEP-47 +
+ +
Send a file to another user

+ +Description

+ +A user may wish to send a file to another user. The other user has the option of acception, +rejecting, or ignoring the users request. Smack provides a simple interface in order +to enable the user to easily send a file.

+ +Usage

+ +In order to send a file you must first construct an instance of the FileTransferManager +class. This class has one constructor with one parameter which is your XMPPConnection. +In order to instantiate the manager you should call new FileTransferManager(connection)

+ +

Once you have your FileTransferManager you will need to create an outgoing +file transfer to send a file. The method to use on the FileTransferManager +is the createOutgoingFileTransfer(userID) method. The userID you provide to this +method is the fully-qualified jabber ID of the user you wish to send the file to. A +fully-qualified jabber ID consists of a node, a domain, and a resource, the user +must be connected to the resource in order to be able to recieve the file transfer.

+ +

Now that you have your OutgoingFileTransfer instance you will want +to send the file. The method to send a file is sendFile(file, description). The file + you provide to this method should be a readable file on the local file system, and the description is a short + description of the file to help the user decide whether or not they would like to recieve the file.

+ +

For information on monitoring the progress of a file transfer see the monitoring progress +section of this document.

+ +

Other means to send a file are also provided as part of the OutgoingFileTransfer. Please +consult the Javadoc for more information.

+ + +Examples

+ +In this example we can see how to send a file:
+

+
+      // Create the file transfer manager
+      FileTransferManager manager = new FileTransferManager(connection);
+		
+      // Create the outgoing file transfer
+      OutgoingFileTransfer transfer = manager.createOutgoingFileTransfer("romeo@montague.net");
+		
+      // Send the file
+      transfer.sendFile(new File("shakespeare_complete_works.txt"), "You won't believe this!");
+
+
+
+ +
+ +
Recieving a file from another user

+ +Description

+ +The user may wish to recieve files from another user. The process of recieving a file is event driven, +new file transfer requests are recieved from other users via a listener registered with the file transfer +manager.

+ +Usage

+ +In order to recieve a file you must first construct an instance of the FileTransferManager +class. This class has one constructor with one parameter which is your XMPPConnection. +In order to instantiate the manager you should call new FileTransferManager(connection)

+ +

Once you have your FileTransferManager you will need to register a listener +with it. The FileTransferListner interface has one method, fileTransferRequest(request). +When a request is recieved through this method, you can either accept or reject the +request. To help you make your decision there are several methods in the FileTransferRequest +class that return information about the transfer request.

+ +

To accept the file transfer, call the accept(), +this method will create an IncomingFileTransfer. After you have the file transfer you may start +to transfer the file by calling the recieveFile(file) method. +The file provided to this method will be where the data from thefile transfer is saved.

+ +

Finally, to reject the file transfer the only method you need to call is reject() +on the IncomingFileTransfer.

+ +

For information on monitoring the progress of a file transfer see the monitoring progress +section of this document.

+ +

Other means to recieve a file are also provided as part of the IncomingFileTransfer. Please +consult the Javadoc for more information.

+ +Examples

+ +In this example we can see how to approve or reject a file transfer request:
+

+
      // Create the file transfer manager
+      final FileTransferManager manager = new FileTransferManager(connection);
+
+      // Create the listener
+      manager.addFileTransferListener(new FileTransferListener() {
+            public void fileTransferRequest(FileTransferRequest request) {
+                  // Check to see if the request should be accepted
+                  if(shouldAccept(request)) {
+                        // Accept it
+                        IncomingFileTransfer transfer = request.accept();
+                        transfer.recieveFile(new File("shakespeare_complete_works.txt"));
+                  } else {
+                        // Reject it
+                        request.reject();
+                  }
+            }
+      });
+
+
+ +
+ +
Monitoring the progress of a file transfer

+ +Description

+ +While a file transfer is in progress you may wish to monitor the progress of a file transfer.

+ +Usage

+ +

Both the IncomingFileTransfer and the OutgoingFileTransfer +extend the FileTransfer class which provides several methods to monitor +how a file transfer is progressing:

+ + +Examples

+ +In this example we can see how to monitor a file transfer:
+

+
      while(!transfer.isDone()) {
+            if(transfer.getStatus().equals(Status.ERROR)) {
+                  System.out.println("ERROR!!! " + transfer.getError());
+            } else {
+                  System.out.println(transfer.getStatus());
+                  System.out.println(transfer.getProgress());
+            }
+            sleep(1000);
+      }
+
+
+ + + diff --git a/documentation/extensions/intro.html b/documentation/extensions/intro.html index 2005966e1..f97de3e0e 100644 --- a/documentation/extensions/intro.html +++ b/documentation/extensions/intro.html @@ -64,7 +64,12 @@ Service Discovery JEP-30 Allows to discover services in XMPP entities. - + + + File Transfer + JEP-96 + Transfer files between two users over XMPP. + \ No newline at end of file diff --git a/documentation/extensions/toc.html b/documentation/extensions/toc.html index b05e0d8f4..6246a3644 100644 --- a/documentation/extensions/toc.html +++ b/documentation/extensions/toc.html @@ -20,6 +20,7 @@ Time Exchange
Group Chat Invitations
Service Discovery
+File Transfer

diff --git a/source/org/jivesoftware/smack/filter/IQTypeFilter.java b/source/org/jivesoftware/smack/filter/IQTypeFilter.java new file mode 100644 index 000000000..21370be3d --- /dev/null +++ b/source/org/jivesoftware/smack/filter/IQTypeFilter.java @@ -0,0 +1,32 @@ +/** + * + */ +package org.jivesoftware.smack.filter; + +import org.jivesoftware.smack.packet.IQ; +import org.jivesoftware.smack.packet.Packet; + +/** + * A filter for IQ packet types. Returns true only if the packet is an IQ packet + * and it matches the type provided in the constructor. + * + * @author Alexander Wenckus + * + */ +public class IQTypeFilter implements PacketFilter { + + private IQ.Type type; + + public IQTypeFilter(IQ.Type type) { + this.type = type; + } + + /* + * (non-Javadoc) + * + * @see org.jivesoftware.smack.filter.PacketFilter#accept(org.jivesoftware.smack.packet.Packet) + */ + public boolean accept(Packet packet) { + return (packet instanceof IQ && ((IQ) packet).getType().equals(type)); + } +} diff --git a/source/org/jivesoftware/smackx/filetransfer/Base64.java b/source/org/jivesoftware/smackx/filetransfer/Base64.java new file mode 100644 index 000000000..d45a4f34a --- /dev/null +++ b/source/org/jivesoftware/smackx/filetransfer/Base64.java @@ -0,0 +1,1416 @@ +package org.jivesoftware.smackx.filetransfer; + +/** + * Encodes and decodes to and from Base64 notation.

Change Log: + *

+ * + *

I am placing this code in the Public Domain. Do with it as you + * will. This software comes with no guarantees or warranties but with plenty of + * well-wishing instead! Please visit http://iharder.net/base64 periodically + * to check for updates or to contribute improvements. + *

+ * + * @author Robert Harder + * @author rob@iharder.net + * @version 2.1 + */ +class Base64 { + + /* ******** P U B L I C F I E L D S ******** */ + + /** + * No options specified. Value is zero. + */ + public final static int NO_OPTIONS = 0; + + /** + * Specify encoding. + */ + public final static int ENCODE = 1; + + /** + * Specify decoding. + */ + public final static int DECODE = 0; + + /** + * Specify that data should be gzip-compressed. + */ + public final static int GZIP = 2; + + /** + * Don't break lines when encoding (violates strict Base64 specification) + */ + public final static int DONT_BREAK_LINES = 8; + + /* ******** P R I V A T E F I E L D S ******** */ + + /** + * Maximum line length (76) of Base64 output. + */ + private final static int MAX_LINE_LENGTH = 76; + + /** + * The equals sign (=) as a byte. + */ + private final static byte EQUALS_SIGN = (byte) '='; + + /** + * The new line character (\n) as a byte. + */ + private final static byte NEW_LINE = (byte) '\n'; + + /** + * Preferred encoding. + */ + private final static String PREFERRED_ENCODING = "UTF-8"; + + /** + * The 64 valid Base64 values. + */ + private final static byte[] ALPHABET; + + private final static byte[] _NATIVE_ALPHABET = /* + * May be something funny + * like EBCDIC + */ + { (byte) 'A', (byte) 'B', (byte) 'C', (byte) 'D', (byte) 'E', (byte) 'F', + (byte) 'G', (byte) 'H', (byte) 'I', (byte) 'J', (byte) 'K', + (byte) 'L', (byte) 'M', (byte) 'N', (byte) 'O', (byte) 'P', + (byte) 'Q', (byte) 'R', (byte) 'S', (byte) 'T', (byte) 'U', + (byte) 'V', (byte) 'W', (byte) 'X', (byte) 'Y', (byte) 'Z', + (byte) 'a', (byte) 'b', (byte) 'c', (byte) 'd', (byte) 'e', + (byte) 'f', (byte) 'g', (byte) 'h', (byte) 'i', (byte) 'j', + (byte) 'k', (byte) 'l', (byte) 'm', (byte) 'n', (byte) 'o', + (byte) 'p', (byte) 'q', (byte) 'r', (byte) 's', (byte) 't', + (byte) 'u', (byte) 'v', (byte) 'w', (byte) 'x', (byte) 'y', + (byte) 'z', (byte) '0', (byte) '1', (byte) '2', (byte) '3', + (byte) '4', (byte) '5', (byte) '6', (byte) '7', (byte) '8', + (byte) '9', (byte) '+', (byte) '/' }; + + /** Determine which ALPHABET to use. */ + static { + byte[] __bytes; + try { + __bytes = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/" + .getBytes(PREFERRED_ENCODING); + } // end try + catch (java.io.UnsupportedEncodingException use) { + __bytes = _NATIVE_ALPHABET; // Fall back to native encoding + } // end catch + ALPHABET = __bytes; + } // end static + + /** + * Translates a Base64 value to either its 6-bit reconstruction value or a + * negative number indicating some other meaning. + */ + private final static byte[] DECODABET = { -9, -9, -9, -9, -9, -9, -9, -9, + -9, // Decimal 0 - 8 + -5, -5, // Whitespace: Tab and Linefeed + -9, -9, // Decimal 11 - 12 + -5, // Whitespace: Carriage Return + -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 14 - + // 26 + -9, -9, -9, -9, -9, // Decimal 27 - 31 + -5, // Whitespace: Space + -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 33 - 42 + 62, // Plus sign at decimal 43 + -9, -9, -9, // Decimal 44 - 46 + 63, // Slash at decimal 47 + 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, // Numbers zero through nine + -9, -9, -9, // Decimal 58 - 60 + -1, // Equals sign at decimal 61 + -9, -9, -9, // Decimal 62 - 64 + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, // Letters 'A' + // through 'N' + 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, // Letters 'O' + // through 'Z' + -9, -9, -9, -9, -9, -9, // Decimal 91 - 96 + 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, // Letters 'a' + // through 'm' + 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, // Letters 'n' + // through 'z' + -9, -9, -9, -9 // Decimal 123 - 126 + /* + * ,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 127 - 139 + * -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 140 - 152 + * -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 153 - 165 + * -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 166 - 178 + * -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 179 - 191 + * -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 192 - 204 + * -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 205 - 217 + * -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 218 - 230 + * -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 231 - 243 + * -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9 // Decimal 244 - 255 + */ + }; + + // I think I end up not using the BAD_ENCODING indicator. + // private final static byte BAD_ENCODING = -9; // Indicates error in + // encoding + private final static byte WHITE_SPACE_ENC = -5; // Indicates white space in + // encoding + + private final static byte EQUALS_SIGN_ENC = -1; // Indicates equals sign in + // encoding + + /** + * Defeats instantiation. + */ + private Base64() { + } + + /* ******** E N C O D I N G M E T H O D S ******** */ + + /** + * Encodes up to the first three bytes of array threeBytes and + * returns a four-byte array in Base64 notation. The actual number of + * significant bytes in your array is given by numSigBytes. The + * array threeBytes needs only be as big as numSigBytes. + * Code can reuse a byte array by passing a four-byte array as b4. + * + * @param b4 + * A reusable byte array to reduce array instantiation + * @param threeBytes + * the array to convert + * @param numSigBytes + * the number of significant bytes in your array + * @return four byte array in Base64 notation. + * @since 1.5.1 + */ + private static byte[] encode3to4(byte[] b4, byte[] threeBytes, + int numSigBytes) { + encode3to4(threeBytes, 0, numSigBytes, b4, 0); + return b4; + } // end encode3to4 + + /** + * Encodes up to three bytes of the array source and writes the + * resulting four Base64 bytes to destination. The source and + * destination arrays can be manipulated anywhere along their length by + * specifying srcOffset and destOffset. This method + * does not check to make sure your arrays are large enough to accomodate + * srcOffset + 3 for the source array or + * destOffset + 4 for the destination array. The + * actual number of significant bytes in your array is given by + * numSigBytes. + * + * @param source + * the array to convert + * @param srcOffset + * the index where conversion begins + * @param numSigBytes + * the number of significant bytes in your array + * @param destination + * the array to hold the conversion + * @param destOffset + * the index where output will be put + * @return the destination array + * @since 1.3 + */ + private static byte[] encode3to4(byte[] source, int srcOffset, + int numSigBytes, byte[] destination, int destOffset) { + // 1 2 3 + // 01234567890123456789012345678901 Bit position + // --------000000001111111122222222 Array position from threeBytes + // --------| || || || | Six bit groups to index ALPHABET + // >>18 >>12 >> 6 >> 0 Right shift necessary + // 0x3f 0x3f 0x3f Additional AND + + // Create buffer with zero-padding if there are only one or two + // significant bytes passed in the array. + // We have to shift left 24 in order to flush out the 1's that appear + // when Java treats a value as negative that is cast from a byte to an + // int. + int inBuff = (numSigBytes > 0 ? ((source[srcOffset] << 24) >>> 8) : 0) + | (numSigBytes > 1 ? ((source[srcOffset + 1] << 24) >>> 16) : 0) + | (numSigBytes > 2 ? ((source[srcOffset + 2] << 24) >>> 24) : 0); + + switch (numSigBytes) { + case 3: + destination[destOffset] = ALPHABET[(inBuff >>> 18)]; + destination[destOffset + 1] = ALPHABET[(inBuff >>> 12) & 0x3f]; + destination[destOffset + 2] = ALPHABET[(inBuff >>> 6) & 0x3f]; + destination[destOffset + 3] = ALPHABET[(inBuff) & 0x3f]; + return destination; + + case 2: + destination[destOffset] = ALPHABET[(inBuff >>> 18)]; + destination[destOffset + 1] = ALPHABET[(inBuff >>> 12) & 0x3f]; + destination[destOffset + 2] = ALPHABET[(inBuff >>> 6) & 0x3f]; + destination[destOffset + 3] = EQUALS_SIGN; + return destination; + + case 1: + destination[destOffset] = ALPHABET[(inBuff >>> 18)]; + destination[destOffset + 1] = ALPHABET[(inBuff >>> 12) & 0x3f]; + destination[destOffset + 2] = EQUALS_SIGN; + destination[destOffset + 3] = EQUALS_SIGN; + return destination; + + default: + return destination; + } // end switch + } // end encode3to4 + + /** + * Serializes an object and returns the Base64-encoded version of that + * serialized object. If the object cannot be serialized or there is another + * error, the method will return null. The object is not + * GZip-compressed before being encoded. + * + * @param serializableObject + * The object to encode + * @return The Base64-encoded object + * @since 1.4 + */ + public static String encodeObject(java.io.Serializable serializableObject) { + return encodeObject(serializableObject, NO_OPTIONS); + } // end encodeObject + + /** + * Serializes an object and returns the Base64-encoded version of that + * serialized object. If the object cannot be serialized or there is another + * error, the method will return null.

Valid options: + * + *

+	 *    GZIP: gzip-compresses object before encoding it.
+	 *    DONT_BREAK_LINES: don't break lines at 76 characters
+	 *      <i>Note: Technically, this makes your encoding non-compliant.</i>
+	 * 
+ * + *

Example: encodeObject( myObj, Base64.GZIP ) or

+ * Example: + * encodeObject( myObj, Base64.GZIP | Base64.DONT_BREAK_LINES ) + * + * @param serializableObject + * The object to encode + * @param options + * Specified options + * @return The Base64-encoded object + * @see Base64#GZIP + * @see Base64#DONT_BREAK_LINES + * @since 2.0 + */ + public static String encodeObject(java.io.Serializable serializableObject, + int options) { + // Streams + java.io.ByteArrayOutputStream baos = null; + java.io.OutputStream b64os = null; + java.io.ObjectOutputStream oos = null; + java.util.zip.GZIPOutputStream gzos = null; + + // Isolate options + int gzip = (options & GZIP); + int dontBreakLines = (options & DONT_BREAK_LINES); + + try { + // ObjectOutputStream -> (GZIP) -> Base64 -> ByteArrayOutputStream + baos = new java.io.ByteArrayOutputStream(); + b64os = new Base64.OutputStream(baos, ENCODE | dontBreakLines); + + // GZip? + if (gzip == GZIP) { + gzos = new java.util.zip.GZIPOutputStream(b64os); + oos = new java.io.ObjectOutputStream(gzos); + } // end if: gzip + else { + oos = new java.io.ObjectOutputStream(b64os); + } + + oos.writeObject(serializableObject); + } // end try + catch (java.io.IOException e) { + e.printStackTrace(); + return null; + } // end catch + finally { + try { + oos.close(); + } catch (Exception e) { + } + try { + gzos.close(); + } catch (Exception e) { + } + try { + b64os.close(); + } catch (Exception e) { + } + try { + baos.close(); + } catch (Exception e) { + } + } // end finally + + // Return value according to relevant encoding. + try { + return new String(baos.toByteArray(), PREFERRED_ENCODING); + } // end try + catch (java.io.UnsupportedEncodingException uue) { + return new String(baos.toByteArray()); + } // end catch + + } // end encode + + /** + * Encodes a byte array into Base64 notation. Does not GZip-compress data. + * + * @param source + * The data to convert + * @since 1.4 + */ + public static String encodeBytes(byte[] source) { + return encodeBytes(source, 0, source.length, NO_OPTIONS); + } // end encodeBytes + + /** + * Encodes a byte array into Base64 notation.

Valid options: + * + *

+	 *    GZIP: gzip-compresses object before encoding it.
+	 *    DONT_BREAK_LINES: don't break lines at 76 characters
+	 *      <i>Note: Technically, this makes your encoding non-compliant.</i>
+	 * 
+ * + *

Example: encodeBytes( myData, Base64.GZIP ) or

+ * Example: + * encodeBytes( myData, Base64.GZIP | Base64.DONT_BREAK_LINES ) + * + * @param source + * The data to convert + * @param options + * Specified options + * @see Base64#GZIP + * @see Base64#DONT_BREAK_LINES + * @since 2.0 + */ + public static String encodeBytes(byte[] source, int options) { + return encodeBytes(source, 0, source.length, options); + } // end encodeBytes + + /** + * Encodes a byte array into Base64 notation. Does not GZip-compress data. + * + * @param source + * The data to convert + * @param off + * Offset in array where conversion should begin + * @param len + * Length of data to convert + * @since 1.4 + */ + public static String encodeBytes(byte[] source, int off, int len) { + return encodeBytes(source, off, len, NO_OPTIONS); + } // end encodeBytes + + /** + * Encodes a byte array into Base64 notation.

Valid options: + * + *

+	 *    GZIP: gzip-compresses object before encoding it.
+	 *    DONT_BREAK_LINES: don't break lines at 76 characters
+	 *      <i>Note: Technically, this makes your encoding non-compliant.</i>
+	 * 
+ * + *

Example: encodeBytes( myData, Base64.GZIP ) or

+ * Example: + * encodeBytes( myData, Base64.GZIP | Base64.DONT_BREAK_LINES ) + * + * @param source + * The data to convert + * @param off + * Offset in array where conversion should begin + * @param len + * Length of data to convert + * @param options + * Specified options + * @see Base64#GZIP + * @see Base64#DONT_BREAK_LINES + * @since 2.0 + */ + public static String encodeBytes(byte[] source, int off, int len, + int options) { + // Isolate options + int dontBreakLines = (options & DONT_BREAK_LINES); + int gzip = (options & GZIP); + + // Compress? + if (gzip == GZIP) { + java.io.ByteArrayOutputStream baos = null; + java.util.zip.GZIPOutputStream gzos = null; + Base64.OutputStream b64os = null; + + try { + // GZip -> Base64 -> ByteArray + baos = new java.io.ByteArrayOutputStream(); + b64os = new Base64.OutputStream(baos, ENCODE | dontBreakLines); + gzos = new java.util.zip.GZIPOutputStream(b64os); + + gzos.write(source, off, len); + gzos.close(); + } // end try + catch (java.io.IOException e) { + e.printStackTrace(); + return null; + } // end catch + finally { + try { + gzos.close(); + } catch (Exception e) { + } + try { + b64os.close(); + } catch (Exception e) { + } + try { + baos.close(); + } catch (Exception e) { + } + } // end finally + + // Return value according to relevant encoding. + try { + return new String(baos.toByteArray(), PREFERRED_ENCODING); + } // end try + catch (java.io.UnsupportedEncodingException uue) { + return new String(baos.toByteArray()); + } // end catch + } // end if: compress + + // Else, don't compress. Better not to use streams at all then. + else { + // Convert option to boolean in way that code likes it. + boolean breakLines = dontBreakLines == 0; + + int len43 = len * 4 / 3; + byte[] outBuff = new byte[(len43) // Main 4:3 + + ((len % 3) > 0 ? 4 : 0) // Account for padding + + (breakLines ? (len43 / MAX_LINE_LENGTH) : 0)]; // New + // lines + int d = 0; + int e = 0; + int len2 = len - 2; + int lineLength = 0; + for (; d < len2; d += 3, e += 4) { + encode3to4(source, d + off, 3, outBuff, e); + + lineLength += 4; + if (breakLines && lineLength == MAX_LINE_LENGTH) { + outBuff[e + 4] = NEW_LINE; + e++; + lineLength = 0; + } // end if: end of line + } // en dfor: each piece of array + + if (d < len) { + encode3to4(source, d + off, len - d, outBuff, e); + e += 4; + } // end if: some padding needed + + // Return value according to relevant encoding. + try { + return new String(outBuff, 0, e, PREFERRED_ENCODING); + } // end try + catch (java.io.UnsupportedEncodingException uue) { + return new String(outBuff, 0, e); + } // end catch + + } // end else: don't compress + + } // end encodeBytes + + /* ******** D E C O D I N G M E T H O D S ******** */ + + /** + * Decodes four bytes from array source and writes the resulting + * bytes (up to three of them) to destination. The source and + * destination arrays can be manipulated anywhere along their length by + * specifying srcOffset and destOffset. This method + * does not check to make sure your arrays are large enough to accomodate + * srcOffset + 4 for the source array or + * destOffset + 3 for the destination array. This + * method returns the actual number of bytes that were converted from the + * Base64 encoding. + * + * @param source + * the array to convert + * @param srcOffset + * the index where conversion begins + * @param destination + * the array to hold the conversion + * @param destOffset + * the index where output will be put + * @return the number of decoded bytes converted + * @since 1.3 + */ + private static int decode4to3(byte[] source, int srcOffset, + byte[] destination, int destOffset) { + // Example: Dk== + if (source[srcOffset + 2] == EQUALS_SIGN) { + // Two ways to do the same thing. Don't know which way I like best. + // int outBuff = ( ( DECODABET[ source[ srcOffset ] ] << 24 ) >>> 6 + // ) + // | ( ( DECODABET[ source[ srcOffset + 1] ] << 24 ) >>> 12 ); + int outBuff = ((DECODABET[source[srcOffset]] & 0xFF) << 18) + | ((DECODABET[source[srcOffset + 1]] & 0xFF) << 12); + + destination[destOffset] = (byte) (outBuff >>> 16); + return 1; + } + + // Example: DkL= + else if (source[srcOffset + 3] == EQUALS_SIGN) { + // Two ways to do the same thing. Don't know which way I like best. + // int outBuff = ( ( DECODABET[ source[ srcOffset ] ] << 24 ) >>> 6 + // ) + // | ( ( DECODABET[ source[ srcOffset + 1 ] ] << 24 ) >>> 12 ) + // | ( ( DECODABET[ source[ srcOffset + 2 ] ] << 24 ) >>> 18 ); + int outBuff = ((DECODABET[source[srcOffset]] & 0xFF) << 18) + | ((DECODABET[source[srcOffset + 1]] & 0xFF) << 12) + | ((DECODABET[source[srcOffset + 2]] & 0xFF) << 6); + + destination[destOffset] = (byte) (outBuff >>> 16); + destination[destOffset + 1] = (byte) (outBuff >>> 8); + return 2; + } + + // Example: DkLE + else { + try { + // Two ways to do the same thing. Don't know which way I like + // best. + // int outBuff = ( ( DECODABET[ source[ srcOffset ] ] << 24 ) + // >>> 6 ) + // | ( ( DECODABET[ source[ srcOffset + 1 ] ] << 24 ) >>> 12 ) + // | ( ( DECODABET[ source[ srcOffset + 2 ] ] << 24 ) >>> 18 ) + // | ( ( DECODABET[ source[ srcOffset + 3 ] ] << 24 ) >>> 24 ); + int outBuff = ((DECODABET[source[srcOffset]] & 0xFF) << 18) + | ((DECODABET[source[srcOffset + 1]] & 0xFF) << 12) + | ((DECODABET[source[srcOffset + 2]] & 0xFF) << 6) + | ((DECODABET[source[srcOffset + 3]] & 0xFF)); + + destination[destOffset] = (byte) (outBuff >> 16); + destination[destOffset + 1] = (byte) (outBuff >> 8); + destination[destOffset + 2] = (byte) (outBuff); + + return 3; + } catch (Exception e) { + System.out.println("" + source[srcOffset] + ": " + + (DECODABET[source[srcOffset]])); + System.out.println("" + source[srcOffset + 1] + ": " + + (DECODABET[source[srcOffset + 1]])); + System.out.println("" + source[srcOffset + 2] + ": " + + (DECODABET[source[srcOffset + 2]])); + System.out.println("" + source[srcOffset + 3] + ": " + + (DECODABET[source[srcOffset + 3]])); + return -1; + } // e nd catch + } + } // end decodeToBytes + + /** + * Very low-level access to decoding ASCII characters in the form of a byte + * array. Does not support automatically gunzipping or any other "fancy" + * features. + * + * @param source + * The Base64 encoded data + * @param off + * The offset of where to begin decoding + * @param len + * The length of characters to decode + * @return decoded data + * @since 1.3 + */ + public static byte[] decode(byte[] source, int off, int len) { + int len34 = len * 3 / 4; + byte[] outBuff = new byte[len34]; // Upper limit on size of output + int outBuffPosn = 0; + + byte[] b4 = new byte[4]; + int b4Posn = 0; + int i = 0; + byte sbiCrop = 0; + byte sbiDecode = 0; + for (i = off; i < off + len; i++) { + sbiCrop = (byte) (source[i] & 0x7f); // Only the low seven bits + sbiDecode = DECODABET[sbiCrop]; + + if (sbiDecode >= WHITE_SPACE_ENC) // White space, Equals sign or + // better + { + if (sbiDecode >= EQUALS_SIGN_ENC) { + b4[b4Posn++] = sbiCrop; + if (b4Posn > 3) { + outBuffPosn += decode4to3(b4, 0, outBuff, outBuffPosn); + b4Posn = 0; + + // If that was the equals sign, break out of 'for' loop + if (sbiCrop == EQUALS_SIGN) { + break; + } + } // end if: quartet built + + } // end if: equals sign or better + + } // end if: white space, equals sign or better + else { + System.err.println("Bad Base64 input character at " + i + ": " + + source[i] + "(decimal)"); + return null; + } // end else: + } // each input character + + byte[] out = new byte[outBuffPosn]; + System.arraycopy(outBuff, 0, out, 0, outBuffPosn); + return out; + } // end decode + + /** + * Decodes data from Base64 notation, automatically detecting + * gzip-compressed data and decompressing it. + * + * @param s + * the string to decode + * @return the decoded data + * @since 1.4 + */ + public static byte[] decode(String s) { + byte[] bytes; + try { + bytes = s.getBytes(PREFERRED_ENCODING); + } // end try + catch (java.io.UnsupportedEncodingException uee) { + bytes = s.getBytes(); + } // end catch + // + + // Decode + bytes = decode(bytes, 0, bytes.length); + + // Check to see if it's gzip-compressed + // GZIP Magic Two-Byte Number: 0x8b1f (35615) + if (bytes != null && bytes.length >= 4) { + + int head = ((int) bytes[0] & 0xff) | ((bytes[1] << 8) & 0xff00); + if (java.util.zip.GZIPInputStream.GZIP_MAGIC == head) { + java.io.ByteArrayInputStream bais = null; + java.util.zip.GZIPInputStream gzis = null; + java.io.ByteArrayOutputStream baos = null; + byte[] buffer = new byte[2048]; + int length = 0; + + try { + baos = new java.io.ByteArrayOutputStream(); + bais = new java.io.ByteArrayInputStream(bytes); + gzis = new java.util.zip.GZIPInputStream(bais); + + while ((length = gzis.read(buffer)) >= 0) { + baos.write(buffer, 0, length); + } // end while: reading input + + // No error? Get new bytes. + bytes = baos.toByteArray(); + + } // end try + catch (java.io.IOException e) { + // Just return originally-decoded bytes + } // end catch + finally { + try { + baos.close(); + } catch (Exception e) { + } + try { + gzis.close(); + } catch (Exception e) { + } + try { + bais.close(); + } catch (Exception e) { + } + } // end finally + + } // end if: gzipped + } // end if: bytes.length >= 2 + + return bytes; + } // end decode + + /** + * Attempts to decode Base64 data and deserialize a Java Object within. + * Returns null if there was an error. + * + * @param encodedObject + * The Base64 data to decode + * @return The decoded and deserialized object + * @since 1.5 + */ + public static Object decodeToObject(String encodedObject) { + // Decode and gunzip if necessary + byte[] objBytes = decode(encodedObject); + + java.io.ByteArrayInputStream bais = null; + java.io.ObjectInputStream ois = null; + Object obj = null; + + try { + bais = new java.io.ByteArrayInputStream(objBytes); + ois = new java.io.ObjectInputStream(bais); + + obj = ois.readObject(); + } // end try + catch (java.io.IOException e) { + e.printStackTrace(); + obj = null; + } // end catch + catch (java.lang.ClassNotFoundException e) { + e.printStackTrace(); + obj = null; + } // end catch + finally { + try { + bais.close(); + } catch (Exception e) { + } + try { + ois.close(); + } catch (Exception e) { + } + } // end finally + + return obj; + } // end decodeObject + + /** + * Convenience method for encoding data to a file. + * + * @param dataToEncode + * byte array of data to encode in base64 form + * @param filename + * Filename for saving encoded data + * @return true if successful, false otherwise + * @since 2.1 + */ + public static boolean encodeToFile(byte[] dataToEncode, String filename) { + boolean success = false; + Base64.OutputStream bos = null; + try { + bos = new Base64.OutputStream( + new java.io.FileOutputStream(filename), Base64.ENCODE); + bos.write(dataToEncode); + success = true; + } // end try + catch (java.io.IOException e) { + + success = false; + } // end catch: IOException + finally { + try { + bos.close(); + } catch (Exception e) { + } + } // end finally + + return success; + } // end encodeToFile + + /** + * Convenience method for decoding data to a file. + * + * @param dataToDecode + * Base64-encoded data as a string + * @param filename + * Filename for saving decoded data + * @return true if successful, false otherwise + * @since 2.1 + */ + public static boolean decodeToFile(String dataToDecode, String filename) { + boolean success = false; + Base64.OutputStream bos = null; + try { + bos = new Base64.OutputStream( + new java.io.FileOutputStream(filename), Base64.DECODE); + bos.write(dataToDecode.getBytes(PREFERRED_ENCODING)); + success = true; + } // end try + catch (java.io.IOException e) { + success = false; + } // end catch: IOException + finally { + try { + bos.close(); + } catch (Exception e) { + } + } // end finally + + return success; + } // end decodeToFile + + /** + * Convenience method for reading a base64-encoded file and decoding it. + * + * @param filename + * Filename for reading encoded data + * @return decoded byte array or null if unsuccessful + * @since 2.1 + */ + public static byte[] decodeFromFile(String filename) { + byte[] decodedData = null; + Base64.InputStream bis = null; + try { + // Set up some useful variables + java.io.File file = new java.io.File(filename); + byte[] buffer = null; + int length = 0; + int numBytes = 0; + + // Check for size of file + if (file.length() > Integer.MAX_VALUE) { + System.err + .println("File is too big for this convenience method (" + + file.length() + " bytes)."); + return null; + } // end if: file too big for int index + buffer = new byte[(int) file.length()]; + + // Open a stream + bis = new Base64.InputStream(new java.io.BufferedInputStream( + new java.io.FileInputStream(file)), Base64.DECODE); + + // Read until done + while ((numBytes = bis.read(buffer, length, 4096)) >= 0) { + length += numBytes; + } + + // Save in a variable to return + decodedData = new byte[length]; + System.arraycopy(buffer, 0, decodedData, 0, length); + + } // end try + catch (java.io.IOException e) { + System.err.println("Error decoding from file " + filename); + } // end catch: IOException + finally { + try { + bis.close(); + } catch (Exception e) { + } + } // end finally + + return decodedData; + } // end decodeFromFile + + /** + * Convenience method for reading a binary file and base64-encoding it. + * + * @param filename + * Filename for reading binary data + * @return base64-encoded string or null if unsuccessful + * @since 2.1 + */ + public static String encodeFromFile(String filename) { + String encodedData = null; + Base64.InputStream bis = null; + try { + // Set up some useful variables + java.io.File file = new java.io.File(filename); + byte[] buffer = new byte[(int) (file.length() * 1.4)]; + int length = 0; + int numBytes = 0; + + // Open a stream + bis = new Base64.InputStream(new java.io.BufferedInputStream( + new java.io.FileInputStream(file)), Base64.ENCODE); + + // Read until done + while ((numBytes = bis.read(buffer, length, 4096)) >= 0) { + length += numBytes; + } + + // Save in a variable to return + encodedData = new String(buffer, 0, length, + Base64.PREFERRED_ENCODING); + + } // end try + catch (java.io.IOException e) { + System.err.println("Error encoding from file " + filename); + } // end catch: IOException + finally { + try { + bis.close(); + } catch (Exception e) { + } + } // end finally + + return encodedData; + } // end encodeFromFile + + /* ******** I N N E R C L A S S I N P U T S T R E A M ******** */ + + /** + * A {@link Base64.InputStream} will read data from another + * java.io.InputStream, given in the constructor, and + * encode/decode to/from Base64 notation on the fly. + * + * @see Base64 + * @since 1.3 + */ + public static class InputStream extends java.io.FilterInputStream { + private boolean encode; // Encoding or decoding + + private int position; // Current position in the buffer + + private byte[] buffer; // Small buffer holding converted data + + private int bufferLength; // Length of buffer (3 or 4) + + private int numSigBytes; // Number of meaningful bytes in the buffer + + private int lineLength; + + private boolean breakLines; // Break lines at less than 80 characters + + /** + * Constructs a {@link Base64.InputStream} in DECODE mode. + * + * @param in + * the java.io.InputStream from which to read + * data. + * @since 1.3 + */ + public InputStream(java.io.InputStream in) { + this(in, DECODE); + } // end constructor + + /** + * Constructs a {@link Base64.InputStream} in either ENCODE or DECODE + * mode.

Valid options: + * + *

+		 *    ENCODE or DECODE: Encode or Decode as data is read.
+		 *    DONT_BREAK_LINES: don't break lines at 76 characters
+		 *      (only meaningful when encoding)
+		 *      <i>Note: Technically, this makes your encoding non-compliant.</i>
+		 * 
+ * + *

Example: + * new Base64.InputStream( in, Base64.DECODE ) + * + * @param in + * the java.io.InputStream from which to read + * data. + * @param options + * Specified options + * @see Base64#ENCODE + * @see Base64#DECODE + * @see Base64#DONT_BREAK_LINES + * @since 2.0 + */ + public InputStream(java.io.InputStream in, int options) { + super(in); + this.breakLines = (options & DONT_BREAK_LINES) != DONT_BREAK_LINES; + this.encode = (options & ENCODE) == ENCODE; + this.bufferLength = encode ? 4 : 3; + this.buffer = new byte[bufferLength]; + this.position = -1; + this.lineLength = 0; + } // end constructor + + /** + * Reads enough of the input stream to convert to/from Base64 and + * returns the next byte. + * + * @return next byte + * @since 1.3 + */ + public int read() throws java.io.IOException { + // Do we need to get data? + if (position < 0) { + if (encode) { + byte[] b3 = new byte[3]; + int numBinaryBytes = 0; + for (int i = 0; i < 3; i++) { + try { + int b = in.read(); + + // If end of stream, b is -1. + if (b >= 0) { + b3[i] = (byte) b; + numBinaryBytes++; + } // end if: not end of stream + + } // end try: read + catch (java.io.IOException e) { + // Only a problem if we got no data at all. + if (i == 0) { + throw e; + } + + } // end catch + } // end for: each needed input byte + + if (numBinaryBytes > 0) { + encode3to4(b3, 0, numBinaryBytes, buffer, 0); + position = 0; + numSigBytes = 4; + } // end if: got data + else { + return -1; + } // end else + } // end if: encoding + + // Else decoding + else { + byte[] b4 = new byte[4]; + int i = 0; + for (i = 0; i < 4; i++) { + // Read four "meaningful" bytes: + int b = 0; + do { + b = in.read(); + } while (b >= 0 + && DECODABET[b & 0x7f] <= WHITE_SPACE_ENC); + + if (b < 0) { + break; // Reads a -1 if end of stream + } + + b4[i] = (byte) b; + } // end for: each needed input byte + + if (i == 4) { + numSigBytes = decode4to3(b4, 0, buffer, 0); + position = 0; + } // end if: got four characters + else if (i == 0) { + return -1; + } // end else if: also padded correctly + else { + // Must have broken out from above. + throw new java.io.IOException( + "Improperly padded Base64 input."); + } // end + + } // end else: decode + } // end else: get data + + // Got data? + if (position >= 0) { + // End of relevant data? + if (/* !encode && */position >= numSigBytes) { + return -1; + } + + if (encode && breakLines && lineLength >= MAX_LINE_LENGTH) { + lineLength = 0; + return '\n'; + } // end if + else { + lineLength++; // This isn't important when decoding + // but throwing an extra "if" seems + // just as wasteful. + + int b = buffer[position++]; + + if (position >= bufferLength) { + position = -1; + } + + return b & 0xFF; // This is how you "cast" a byte that's + // intended to be unsigned. + } // end else + } // end if: position >= 0 + + // Else error + else { + // When JDK1.4 is more accepted, use an assertion here. + throw new java.io.IOException( + "Error in Base64 code reading stream."); + } // end else + } // end read + + /** + * Calls {@link #read()} repeatedly until the end of stream is reached + * or len bytes are read. Returns number of bytes read into + * array or -1 if end of stream is encountered. + * + * @param dest + * array to hold values + * @param off + * offset for array + * @param len + * max number of bytes to read into array + * @return bytes read into array or -1 if end of stream is encountered. + * @since 1.3 + */ + public int read(byte[] dest, int off, int len) + throws java.io.IOException { + int i; + int b; + for (i = 0; i < len; i++) { + b = read(); + + // if( b < 0 && i == 0 ) + // return -1; + + if (b >= 0) { + dest[off + i] = (byte) b; + } else if (i == 0) { + return -1; + } else { + break; // Out of 'for' loop + } + } // end for: each byte read + return i; + } // end read + + } // end inner class InputStream + + /* ******** I N N E R C L A S S O U T P U T S T R E A M ******** */ + + /** + * A {@link Base64.OutputStream} will write data to another + * java.io.OutputStream, given in the constructor, and + * encode/decode to/from Base64 notation on the fly. + * + * @see Base64 + * @since 1.3 + */ + public static class OutputStream extends java.io.FilterOutputStream { + private boolean encode; + + private int position; + + private byte[] buffer; + + private int bufferLength; + + private int lineLength; + + private boolean breakLines; + + private byte[] b4; // Scratch used in a few places + + private boolean suspendEncoding; + + /** + * Constructs a {@link Base64.OutputStream} in ENCODE mode. + * + * @param out + * the java.io.OutputStream to which data will be + * written. + * @since 1.3 + */ + public OutputStream(java.io.OutputStream out) { + this(out, ENCODE); + } // end constructor + + /** + * Constructs a {@link Base64.OutputStream} in either ENCODE or DECODE + * mode.

Valid options: + * + *

+		 *    ENCODE or DECODE: Encode or Decode as data is read.
+		 *    DONT_BREAK_LINES: don't break lines at 76 characters
+		 *      (only meaningful when encoding)
+		 *      <i>Note: Technically, this makes your encoding non-compliant.</i>
+		 * 
+ * + *

Example: + * new Base64.OutputStream( out, Base64.ENCODE ) + * + * @param out + * the java.io.OutputStream to which data will be + * written. + * @param options + * Specified options. + * @see Base64#ENCODE + * @see Base64#DECODE + * @see Base64#DONT_BREAK_LINES + * @since 1.3 + */ + public OutputStream(java.io.OutputStream out, int options) { + super(out); + this.breakLines = (options & DONT_BREAK_LINES) != DONT_BREAK_LINES; + this.encode = (options & ENCODE) == ENCODE; + this.bufferLength = encode ? 3 : 4; + this.buffer = new byte[bufferLength]; + this.position = 0; + this.lineLength = 0; + this.suspendEncoding = false; + this.b4 = new byte[4]; + } // end constructor + + /** + * Writes the byte to the output stream after converting to/from Base64 + * notation. When encoding, bytes are buffered three at a time before + * the output stream actually gets a write() call. When decoding, bytes + * are buffered four at a time. + * + * @param theByte + * the byte to write + * @since 1.3 + */ + public void write(int theByte) throws java.io.IOException { + // Encoding suspended? + if (suspendEncoding) { + super.out.write(theByte); + return; + } // end if: supsended + + // Encode? + if (encode) { + buffer[position++] = (byte) theByte; + if (position >= bufferLength) // Enough to encode. + { + out.write(encode3to4(b4, buffer, bufferLength)); + + lineLength += 4; + if (breakLines && lineLength >= MAX_LINE_LENGTH) { + out.write(NEW_LINE); + lineLength = 0; + } // end if: end of line + + position = 0; + } // end if: enough to output + } // end if: encoding + + // Else, Decoding + else { + // Meaningful Base64 character? + if (DECODABET[theByte & 0x7f] > WHITE_SPACE_ENC) { + buffer[position++] = (byte) theByte; + if (position >= bufferLength) // Enough to output. + { + int len = Base64.decode4to3(buffer, 0, b4, 0); + out.write(b4, 0, len); + // out.write( Base64.decode4to3( buffer ) ); + position = 0; + } // end if: enough to output + } // end if: meaningful base64 character + else if (DECODABET[theByte & 0x7f] != WHITE_SPACE_ENC) { + throw new java.io.IOException( + "Invalid character in Base64 data."); + } // end else: not white space either + } // end else: decoding + } // end write + + /** + * Calls {@link #write(int)} repeatedly until len bytes are + * written. + * + * @param theBytes + * array from which to read bytes + * @param off + * offset for array + * @param len + * max number of bytes to read into array + * @since 1.3 + */ + public void write(byte[] theBytes, int off, int len) + throws java.io.IOException { + // Encoding suspended? + if (suspendEncoding) { + super.out.write(theBytes, off, len); + return; + } // end if: supsended + + for (int i = 0; i < len; i++) { + write(theBytes[off + i]); + } // end for: each byte written + + } // end write + + /** + * Method added by PHIL. [Thanks, PHIL. -Rob] This pads the buffer + * without closing the stream. + */ + public void flushBase64() throws java.io.IOException { + if (position > 0) { + if (encode) { + out.write(encode3to4(b4, buffer, position)); + position = 0; + } // end if: encoding + else { + throw new java.io.IOException( + "Base64 input not properly padded."); + } // end else: decoding + } // end if: buffer partially full + + } // end flush + + /** + * Flushes and closes (I think, in the superclass) the stream. + * + * @since 1.3 + */ + public void close() throws java.io.IOException { + // 1. Ensure that pending characters are written + flushBase64(); + + // 2. Actually close the stream + // Base class both flushes and closes. + super.close(); + + buffer = null; + out = null; + } // end close + + /** + * Suspends encoding of the stream. May be helpful if you need to embed + * a piece of base640-encoded data in a stream. + * + * @since 1.5.1 + */ + public void suspendEncoding() throws java.io.IOException { + flushBase64(); + this.suspendEncoding = true; + } // end suspendEncoding + + /** + * Resumes encoding of the stream. May be helpful if you need to embed a + * piece of base640-encoded data in a stream. + * + * @since 1.5.1 + */ + public void resumeEncoding() { + this.suspendEncoding = false; + } // end resumeEncoding + + } // end inner class OutputStream + +} // end class Base64 + diff --git a/source/org/jivesoftware/smackx/filetransfer/FileTransfer.java b/source/org/jivesoftware/smackx/filetransfer/FileTransfer.java new file mode 100644 index 000000000..3756aa582 --- /dev/null +++ b/source/org/jivesoftware/smackx/filetransfer/FileTransfer.java @@ -0,0 +1,323 @@ +package org.jivesoftware.smackx.filetransfer; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +import org.jivesoftware.smack.XMPPException; + +/** + * Contains the generic file information and progress related to a particular + * file transfer. + * + * @author Alexander Wenckus + * + */ +public abstract class FileTransfer { + + private String fileName; + + private String filePath; + + private long fileSize; + + private String peer; + + private org.jivesoftware.smackx.filetransfer.FileTransfer.Status status; + + protected FileTransferNegotiator negotiator; + + protected String streamID; + + protected long amountWritten = -1; + + private Error error; + + private Exception exception; + + protected FileTransfer(String peer, String streamID, + FileTransferNegotiator negotiator) { + this.peer = peer; + this.streamID = streamID; + this.negotiator = negotiator; + } + + protected void setFileInfo(String fileName, long fileSize) { + this.fileName = fileName; + this.fileSize = fileSize; + } + + protected void setFileInfo(String path, String fileName, long fileSize) { + this.filePath = path; + this.fileName = fileName; + this.fileSize = fileSize; + } + + /** + * Returns the size of the file being transfered. + * + * @return Returns the size of the file being transfered. + */ + public long getFileSize() { + return fileSize; + } + + /** + * Returns the name of the file being transfered. + * + * @return Returns the name of the file being transfered. + */ + public String getFileName() { + return fileName; + } + + /** + * Returns the local path of the file. + * + * @return Returns the local path of the file. + */ + public String getFilePath() { + return filePath; + } + + /** + * Returns the JID of the peer for this file transfer. + * + * @return Returns the JID of the peer for this file transfer. + */ + public String getPeer() { + return peer; + } + + /** + * Returns the progress of the file transfer as a number between 0 and 1. + * + * @return Returns the progress of the file transfer as a number between 0 + * and 1. + */ + public double getProgress() { + return 0; + } + + /** + * Returns true if the transfer has been cancled, if it has stopped because + * of a an error, or the transfer completed succesfully. + * + * @return Returns true if the transfer has been cancled, if it has stopped + * because of a an error, or the transfer completed succesfully. + */ + public boolean isDone() { + return status == Status.CANCLED || status == Status.ERROR + || status == Status.COMPLETE; + } + + /** + * Retuns the current status of the file transfer. + * + * @return Retuns the current status of the file transfer. + */ + public Status getStatus() { + return status; + } + + protected void setError(Error type) { + this.error = type; + } + + /** + * When {@link #getStatus()} returns that there was an {@link Status#ERROR} + * during the transfer, the type of error can be retrieved through this + * method. + * + * @return Returns the type of error that occured if one has occured. + */ + public Error getError() { + return error; + } + + /** + * If an exception occurs asynchronously it will be stored for later + * retrival. If there is an error there maybe an exception set. + * + * @return The exception that occured or null if there was no exception. + * @see #getError() + */ + public Exception getException() { + return exception; + } + + /** + * Cancels the file transfer. + */ + public abstract void cancel(); + + protected void setException(Exception exception) { + this.exception = exception; + } + + protected final void setStatus(Status status) { + this.status = status; + } + + protected void writeToStream(final InputStream in, final OutputStream out) + throws XMPPException { + final byte[] b = new byte[1000]; + int count = 0; + amountWritten = 0; + try { + count = in.read(b); + } catch (IOException e) { + throw new XMPPException("error reading from input stream", e); + } + while (count != -1 && !getStatus().equals(Status.CANCLED)) { + if (getStatus().equals(Status.CANCLED)) { + return; + } + + // write to the output stream + try { + out.write(b, 0, count); + } catch (IOException e) { + throw new XMPPException("error writing to output stream", e); + } + + amountWritten += count; + + // read more bytes from the input stream + try { + count = in.read(b); + } catch (IOException e) { + throw new XMPPException("error reading from input stream", e); + } + } + + // the connection was likely terminated abrubtly if these are not + // equal + if (!getStatus().equals(Status.CANCLED) && getError() == Error.NONE + && amountWritten != fileSize) { + this.error = Error.CONNECTION; + } + } + + /** + * A class to represent the current status of the file transfer. + * + * @author Alexander Wenckus + * + */ + public static class Status { + /** + * An error occured during the transfer. + * + * @see FileTransfer#getError() + */ + public static final Status ERROR = new Status(); + + /** + * The file transfer is being negotiated with the peer. The party + * recieving the file has the option to accept or refuse a file transfer + * request. If they accept, then the process of stream negotiation will + * begin. If they refuse the file will not be transfered. + * + * @see #NEGOTIATING_STREAM + */ + public static final Status NEGOTIATING_TRANSFER = new Status(); + + /** + * The peer has refused the file transfer request halting the file + * transfer negotiation process. + */ + public static final Status REFUSED = new Status(); + + /** + * The stream to transfer the file is being negotiated over the chosen + * stream type. After the stream negotiating process is complete the + * status becomes negotiated. + * + * @see #NEGOTIATED + */ + public static final Status NEGOTIATING_STREAM = new Status(); + + /** + * After the stream negotitation has completed the intermediate state + * between the time when the negotiation is finished and the actual + * transfer begins. + */ + public static final Status NEGOTIATED = new Status(); + + /** + * The transfer is in progress. + * + * @see FileTransfer#getProgress() + */ + public static final Status IN_PROGRESS = new Status(); + + /** + * The transfer has completed successfully. + */ + public static final Status COMPLETE = new Status(); + + /** + * The file transfer was canceled + */ + public static final Status CANCLED = new Status(); + } + + public static class Error { + /** + * No error + */ + public static final Error NONE = new Error("No error"); + + /** + * The peer did not find any of the provided stream mechanisms + * acceptable. + */ + public static final Error NOT_ACCEPTABLE = new Error( + "The peer did not find any of the provided stream mechanisms acceptable."); + + /** + * The provided file to transfer does not exist or could not be read. + */ + public static final Error BAD_FILE = new Error( + "The provided file to transfer does not exist or could not be read."); + + /** + * The remote user did not respond or the connection timed out. + */ + public static final Error NO_RESPONSE = new Error( + "The remote user did not respond or the connection timed out."); + + /** + * An error occured over the socket connected to send the file. + */ + public static final Error CONNECTION = new Error( + "An error occured over the socket connected to send the file."); + + /** + * An error occured while sending or recieving the file + */ + protected static final Error STREAM = new Error( + "An error occured while sending or recieving the file"); + + private final String msg; + + private Error(String msg) { + this.msg = msg; + } + + /** + * Returns a String representation of this error. + * + * @return Returns a String representation of this error. + */ + public String getMessage() { + return msg; + } + + public String toString() { + return msg; + } + } + +} diff --git a/source/org/jivesoftware/smackx/filetransfer/FileTransferListener.java b/source/org/jivesoftware/smackx/filetransfer/FileTransferListener.java new file mode 100644 index 000000000..bd83fc679 --- /dev/null +++ b/source/org/jivesoftware/smackx/filetransfer/FileTransferListener.java @@ -0,0 +1,20 @@ +/* + * Created on Jun 23, 2005 + */ +package org.jivesoftware.smackx.filetransfer; + +/** + * File transfers can cause several events to be raised. These events can be + * monitored through this interface. + * + * @author Alexander Wenckus + */ +public interface FileTransferListener { + /** + * A request to send a file has been recieved from another user. + * + * @param request + * The request from the other user. + */ + public void fileTransferRequest(final FileTransferRequest request); +} diff --git a/source/org/jivesoftware/smackx/filetransfer/FileTransferManager.java b/source/org/jivesoftware/smackx/filetransfer/FileTransferManager.java new file mode 100644 index 000000000..1940a86a1 --- /dev/null +++ b/source/org/jivesoftware/smackx/filetransfer/FileTransferManager.java @@ -0,0 +1,162 @@ +/** + * + */ +package org.jivesoftware.smackx.filetransfer; + +import java.util.ArrayList; +import java.util.List; + +import org.jivesoftware.smack.PacketListener; +import org.jivesoftware.smack.XMPPConnection; +import org.jivesoftware.smack.filter.AndFilter; +import org.jivesoftware.smack.filter.IQTypeFilter; +import org.jivesoftware.smack.filter.PacketTypeFilter; +import org.jivesoftware.smack.packet.IQ; +import org.jivesoftware.smack.packet.Packet; +import org.jivesoftware.smack.packet.XMPPError; +import org.jivesoftware.smack.util.StringUtils; +import org.jivesoftware.smackx.packet.StreamInitiation; + +/** + * The file transfer manager class handles the sending and recieving of files. + * To send a file invoke the {@link #createOutgoingFileTransfer(String)} method. + *

+ * And to recieve a file add a file transfer listener to the manager. The + * listener will notify you when there is a new file transfer request. To create + * the {@link IncomingFileTransfer} object accept the transfer, or, if the + * transfer is not desirable reject it. + * + * @author Alexander Wenckus + * + */ +public class FileTransferManager { + + private final FileTransferNegotiator fileTransferNegotiator; + + private List listeners; + + private XMPPConnection connection; + + /** + * Creates a file transfer manager to initiate and receive file transfers. + * + * @param connection + * The XMPPConnection that the file transfers will use. + */ + public FileTransferManager(XMPPConnection connection) { + this.connection = connection; + this.fileTransferNegotiator = FileTransferNegotiator + .getInstanceFor(connection); + } + + /** + * Add a file transfer listener to listen to incoming file transfer + * requests. + * + * @param li + * The listener + * @see #removeFileTransferListener(FileTransferListener) + * @see FileTransferListener + */ + public void addFileTransferListener(final FileTransferListener li) { + if (listeners == null) { + initListeners(); + } + synchronized (this.listeners) { + listeners.add(li); + } + } + + private void initListeners() { + listeners = new ArrayList(); + + connection.addPacketListener(new PacketListener() { + public void processPacket(Packet packet) { + fireNewRequest((StreamInitiation) packet); + } + }, new AndFilter(new PacketTypeFilter(StreamInitiation.class), + new IQTypeFilter(IQ.Type.SET))); + } + + protected void fireNewRequest(StreamInitiation initiation) { + FileTransferListener[] listeners = null; + synchronized (this.listeners) { + listeners = new FileTransferListener[this.listeners.size()]; + this.listeners.toArray(listeners); + } + FileTransferRequest request = new FileTransferRequest(this, initiation); + for (int i = 0; i < listeners.length; i++) { + listeners[i].fileTransferRequest(request); + } + } + + /** + * Removes a file transfer listener. + * + * @param li + * The file transfer listener to be removed + * @see FileTransferListener + */ + public void removeFileTransferListener(final FileTransferListener li) { + if (listeners == null) { + return; + } + synchronized (this.listeners) { + listeners.remove(li); + } + } + + /** + * Creates an OutgoingFileTransfer to send a file to another user. + * + * @param userID + * The fully qualified jabber ID with resource of the user to + * send the file to. + * @return The send file object on which the negotiated transfer can be run. + */ + public OutgoingFileTransfer createOutgoingFileTransfer(String userID) { + if (userID == null || StringUtils.parseName(userID).length() <= 0 + || StringUtils.parseServer(userID).length() <= 0 + || StringUtils.parseResource(userID).length() <= 0) { + throw new IllegalArgumentException( + "The provided user id was not fully qualified"); + } + + return new OutgoingFileTransfer(connection.getUser(), userID, + fileTransferNegotiator.getNextStreamID(), + fileTransferNegotiator); + } + + /** + * When the file transfer request is acceptable, this method should be + * invoked. It will create an IncomingFileTransfer which allows the + * transmission of the file to procede. + * + * @param request + * The remote request that is being accepted. + * @return The IncomingFileTransfer which manages the download of the file + * from the transfer initiator. + */ + protected IncomingFileTransfer createIncomingFileTransfer( + FileTransferRequest request) { + if (request == null) { + throw new NullPointerException("RecieveRequest cannot be null"); + } + + IncomingFileTransfer transfer = new IncomingFileTransfer(request, + fileTransferNegotiator); + transfer.setFileInfo(request.getFileName(), request.getFileSize()); + + return transfer; + } + + protected void rejectIncomingFileTransfer(FileTransferRequest request) { + StreamInitiation initiation = request.getStreamInitiation(); + + IQ rejection = FileTransferNegotiator.createIQ( + initiation.getPacketID(), initiation.getFrom(), initiation + .getTo(), IQ.Type.ERROR); + rejection.setError(new XMPPError(403)); + connection.sendPacket(rejection); + } +} diff --git a/source/org/jivesoftware/smackx/filetransfer/FileTransferNegotiator.java b/source/org/jivesoftware/smackx/filetransfer/FileTransferNegotiator.java new file mode 100644 index 000000000..5e854be7c --- /dev/null +++ b/source/org/jivesoftware/smackx/filetransfer/FileTransferNegotiator.java @@ -0,0 +1,387 @@ +/* + * Created on Jun 16, 2005 + */ +package org.jivesoftware.smackx.filetransfer; + +import org.jivesoftware.smack.ConnectionListener; +import org.jivesoftware.smack.PacketCollector; +import org.jivesoftware.smack.XMPPConnection; +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.packet.DataForm; +import org.jivesoftware.smackx.packet.StreamInitiation; + +import java.net.URLConnection; +import java.util.*; + +/** + * 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 JEP-0096: File Transfer + */ +public class FileTransferNegotiator { + + // Static + + /** + * The XMPP namespace of the SOCKS5 bytestream + */ + public static final String BYTE_STREAM = "http://jabber.org/protocol/bytestreams"; + + /** + * The XMPP namespace of the In-Band bytestream + */ + public static final String INBAND_BYTE_STREAM = "http://jabber.org/protocol/ibb"; + + private static final String[] NAMESPACE = { + "http://jabber.org/protocol/si/profile/file-transfer", + "http://jabber.org/protocol/si", BYTE_STREAM, INBAND_BYTE_STREAM}; + + private static final String[] PROTOCOLS = {BYTE_STREAM, INBAND_BYTE_STREAM}; + + private static final Map transferObject = new HashMap(); + + 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(); + + /** + * 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 XMPPConnection connection) { + if (!connection.isConnected()) { + return null; + } + + if (transferObject.containsKey(connection)) { + return (FileTransferNegotiator) 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 XMPPConnection connection, + final boolean isEnabled) { + ServiceDiscoveryManager manager = ServiceDiscoveryManager + .getInstanceFor(connection); + for (int i = 0; i < NAMESPACE.length; i++) { + if (isEnabled) { + manager.addFeature(NAMESPACE[i]); + } + else { + manager.removeFeature(NAMESPACE[i]); + } + } + } + + /** + * 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 XMPPConnection connection) { + for (int i = 0; i < NAMESPACE.length; i++) { + if (!ServiceDiscoveryManager.getInstanceFor(connection) + .includesFeature(NAMESPACE[i])) + return false; + } + return true; + } + + /** + * A convience 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. + */ + protected 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 getSupportedProtocols() { + return Collections.unmodifiableList(Arrays.asList(PROTOCOLS)); + } + + // non-static + + private final XMPPConnection connection; + + private final StreamNegotiator byteStreamTransferManager; + + private final StreamNegotiator inbandTransferManager; + + private FileTransferNegotiator(final XMPPConnection connection) { + configureConnection(connection); + + this.connection = connection; + byteStreamTransferManager = new Socks5TransferNegotiator(connection); + inbandTransferManager = new IBBTransferNegotiator(connection); + } + + private void configureConnection(final XMPPConnection connection) { + connection.addConnectionListener(new ConnectionListener() { + public void connectionClosed() { + cleanup(connection); + } + + public void connectionClosedOnError(Exception e) { + cleanup(connection); + } + }); + } + + private void cleanup(final XMPPConnection connection) { + transferObject.remove(connection); + } + + /** + * 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) { + XMPPError error = new XMPPError(400); + IQ iqPacket = createIQ(si.getPacketID(), si.getFrom(), si.getTo(), + IQ.Type.ERROR); + iqPacket.setError(error); + connection.sendPacket(iqPacket); + throw new XMPPException("No stream methods contained in packet.", error); + } + + // select the appropriate protocol + + StreamNegotiator selectedStreamNegotiator; + try { + selectedStreamNegotiator = selectProtocol(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 it = form.getFields(); it.hasNext();) { + field = (FormField) it.next(); + if (field.getVariable().equals(STREAM_DATA_FIELD_NAME)) { + break; + } + field = null; + } + return field; + } + + private StreamNegotiator selectProtocol(final FormField field) + throws XMPPException { + String variable = null; + boolean isByteStream = false; + boolean isIBB = false; + for (Iterator it = field.getOptions(); it.hasNext();) { + variable = ((FormField.Option) it.next()).getValue(); + if (variable.equals(BYTE_STREAM)) { + isByteStream = true; + } + else if (variable.equals(INBAND_BYTE_STREAM)) { + isIBB = true; + } + } + + if (!isByteStream && !isIBB) { + XMPPError error = new XMPPError(400); + throw new XMPPException("No acceptable transfer mechanism", error); + } + + return (isByteStream ? byteStreamTransferManager + : 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(403, "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() { + StringBuffer buffer = new StringBuffer(); + 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. + *

+ * If they accept, the packet will contain the other user's choosen stream + * type to send the file across. The two choices this implementation + * provides to the other user for file transfer are SOCKS5 Bytestreams, + * which is the prefered method of transfer, and In-Band Bytestreams, + * which is the fallback mechanism. + *

+ * 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. + *

+ * 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 getUploadNegotiator((((FormField) response + .getFeatureNegotiationForm().getFields().next()) + .getValues().next()).toString()); + + } + 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 getUploadNegotiator(String selectedProtocol) { + if (selectedProtocol.equals(BYTE_STREAM)) { + return byteStreamTransferManager; + } + else if (selectedProtocol.equals(INBAND_BYTE_STREAM)) { + return inbandTransferManager; + } + else { + return null; + } + } + + private DataForm createDefaultInitiationForm() { + DataForm form = new DataForm(Form.TYPE_FORM); + FormField field = new FormField(STREAM_DATA_FIELD_NAME); + field.setType(FormField.TYPE_LIST_SINGLE); + field.addOption(new FormField.Option(BYTE_STREAM)); + field.addOption(new FormField.Option(INBAND_BYTE_STREAM)); + form.addField(field); + return form; + } +} diff --git a/source/org/jivesoftware/smackx/filetransfer/FileTransferRequest.java b/source/org/jivesoftware/smackx/filetransfer/FileTransferRequest.java new file mode 100644 index 000000000..9774060f3 --- /dev/null +++ b/source/org/jivesoftware/smackx/filetransfer/FileTransferRequest.java @@ -0,0 +1,122 @@ +/** + * + */ +package org.jivesoftware.smackx.filetransfer; + +import org.jivesoftware.smackx.packet.StreamInitiation; + +/** + * A request to send a file recieved from another user. + * + * @author Alexander Wenckus + * + */ +public class FileTransferRequest { + private final StreamInitiation streamInitiation; + + private final FileTransferManager manager; + + /** + * A recieve request is constructed from the Stream Initiation request + * received from the initator. + * + * @param manager + * The manager handling this file transfer + * + * @param si + * The Stream initiaton recieved from the initiator. + */ + public FileTransferRequest(FileTransferManager manager, StreamInitiation si) { + this.streamInitiation = si; + this.manager = manager; + } + + /** + * Returns the name of the file. + * + * @return Returns the name of the file. + */ + public String getFileName() { + return streamInitiation.getFile().getName(); + } + + /** + * Returns the size in bytes of the file. + * + * @return Returns the size in bytes of the file. + */ + public long getFileSize() { + return streamInitiation.getFile().getSize(); + } + + /** + * Returns the description of the file provided by the requestor. + * + * @return Returns the description of the file provided by the requestor. + */ + public String getDescription() { + return streamInitiation.getFile().getDesc(); + } + + /** + * Returns the mime-type of the file. + * + * @return Returns the mime-type of the file. + */ + public String getMimeType() { + return streamInitiation.getMimeType(); + } + + /** + * Returns the fully-qualified jabber ID of the user that requested this + * file transfer. + * + * @return Returns the fully-qualified jabber ID of the user that requested + * this file transfer. + */ + public String getRequestor() { + return streamInitiation.getFrom(); + } + + /** + * Returns the stream ID that uniquely identifies this file transfer. + * + * @return Returns the stream ID that uniquely identifies this file + * transfer. + */ + public String getStreamID() { + return streamInitiation.getSessionID(); + } + + /** + * Returns the stream initiation packet that was sent by the requestor which + * contains the parameters of the file transfer being transfer and also the + * methods available to transfer the file. + * + * @return Returns the stream initiation packet that was sent by the + * requestor which contains the parameters of the file transfer + * being transfer and also the methods available to transfer the + * file. + */ + protected StreamInitiation getStreamInitiation() { + return streamInitiation; + } + + /** + * Accepts this file transfer and creates the incoming file transfer. + * + * @return Returns the IncomingFileTransfer on which the + * file transfer can be carried out. + */ + public IncomingFileTransfer accept() { + return manager.createIncomingFileTransfer(this); + } + + /** + * Rejects the file transfer request. + */ + public void reject() { + manager.rejectIncomingFileTransfer(this); + } + +} diff --git a/source/org/jivesoftware/smackx/filetransfer/IBBTransferNegotiator.java b/source/org/jivesoftware/smackx/filetransfer/IBBTransferNegotiator.java new file mode 100644 index 000000000..45766c9e7 --- /dev/null +++ b/source/org/jivesoftware/smackx/filetransfer/IBBTransferNegotiator.java @@ -0,0 +1,398 @@ +/** + * + */ +package org.jivesoftware.smackx.filetransfer; + +import org.jivesoftware.smack.*; +import org.jivesoftware.smack.filter.*; +import org.jivesoftware.smack.packet.IQ; +import org.jivesoftware.smack.packet.Message; +import org.jivesoftware.smack.packet.Packet; +import org.jivesoftware.smackx.packet.IBBExtensions; +import org.jivesoftware.smackx.packet.IBBExtensions.Open; +import org.jivesoftware.smackx.packet.StreamInitiation; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +/** + * The in-band bytestream file transfer method, or IBB for short, transfers the + * file over the same XML Stream used by XMPP. It is the fall-back mechanism in + * case the SOCKS5 bytestream method of transfering files is not available. + * + * @author Alexander Wenckus + * @see JEP-0047: In-Band + * Bytestreams (IBB) + */ +public class IBBTransferNegotiator extends StreamNegotiator { + + protected static final String NAMESPACE = "http://jabber.org/protocol/ibb"; + + public static final int DEFAULT_BLOCK_SIZE = 4096; + + private XMPPConnection connection; + + /** + * The default constructor for the In-Band Bystream Negotiator. + * + * @param connection The connection which this negotiator works on. + */ + protected IBBTransferNegotiator(XMPPConnection connection) { + this.connection = connection; + } + + /* + * (non-Javadoc) + * + * @see org.jivesoftware.smackx.filetransfer.StreamNegotiator#initiateDownload(org.jivesoftware.smackx.packet.StreamInitiation, + * java.io.File) + */ + public InputStream initiateIncomingStream(StreamInitiation initiation) + throws XMPPException { + StreamInitiation response = super.createInitiationAccept(initiation, + NAMESPACE); + + // establish collector to await response + PacketCollector collector = connection + .createPacketCollector(new AndFilter(new FromContainsFilter( + initiation.getFrom()), new IBBSidFilter(initiation.getSessionID()))); + connection.sendPacket(response); + + IBBExtensions.Open openRequest = (IBBExtensions.Open) collector + .nextResult(SmackConfiguration.getPacketReplyTimeout()); + if (openRequest == null) { + throw new XMPPException("No response from file transfer initiator"); + } + else if (openRequest.getType().equals(IQ.Type.ERROR)) { + throw new XMPPException(openRequest.getError()); + } + collector.cancel(); + + PacketFilter dataFilter = new AndFilter(new PacketExtensionFilter( + IBBExtensions.Data.ELEMENT_NAME, IBBExtensions.NAMESPACE), + new FromMatchesFilter(initiation.getFrom())); + PacketFilter closeFilter = new AndFilter(new PacketTypeFilter( + IBBExtensions.Close.class), new FromMatchesFilter(initiation + .getFrom())); + + InputStream stream = new IBBInputStream(openRequest.getSessionID(), + dataFilter, closeFilter); + + initInBandTransfer(openRequest); + + return stream; + } + + /** + * Creates and sends the response for the open request. + * + * @param openRequest The open request recieved from the peer. + */ + private void initInBandTransfer(final Open openRequest) { + connection.sendPacket(FileTransferNegotiator.createIQ(openRequest + .getPacketID(), openRequest.getFrom(), openRequest.getTo(), + IQ.Type.RESULT)); + } + + public OutputStream initiateOutgoingStream(String streamID, String initiator, + String target) throws XMPPException { + Open openIQ = new Open(streamID, DEFAULT_BLOCK_SIZE); + openIQ.setTo(target); + openIQ.setType(IQ.Type.SET); + + // wait for the result from the peer + PacketCollector collector = connection + .createPacketCollector(new PacketIDFilter(openIQ.getPacketID())); + connection.sendPacket(openIQ); + + IQ openResponse = (IQ) collector.nextResult(); + collector.cancel(); + + if (openResponse == null) { + throw new XMPPException("No response from peer"); + } + + IQ.Type type = openResponse.getType(); + if (!type.equals(IQ.Type.RESULT)) { + if (type.equals(IQ.Type.ERROR)) { + throw new XMPPException("Target returned an error", + openResponse.getError()); + } + else { + throw new XMPPException("Target returned unknown response"); + } + } + + return new IBBOutputStream(target, streamID, DEFAULT_BLOCK_SIZE); + } + + public String getNamespace() { + return NAMESPACE; + } + + private class IBBOutputStream extends OutputStream { + + protected byte[] buffer; + + protected int count = 0; + + protected int seq = 0; + + private final Message template; + + private final int options = Base64.DONT_BREAK_LINES; + + private IQ closePacket; + + private String messageID; + + IBBOutputStream(String userID, String sid, int blockSize) { + if (blockSize <= 0) { + throw new IllegalArgumentException("Buffer size <= 0"); + } + buffer = new byte[blockSize]; + template = new Message(userID); + messageID = template.getPacketID(); + template.addExtension(new IBBExtensions.Data(sid)); + closePacket = createClosePacket(userID, sid); + } + + private IQ createClosePacket(String userID, String sid) { + IQ packet = new IBBExtensions.Close(sid); + packet.setTo(userID); + packet.setType(IQ.Type.SET); + return packet; + } + + public void write(int b) throws IOException { + if (count >= buffer.length) { + flushBuffer(); + } + + buffer[count++] = (byte) b; + } + + public synchronized void write(byte b[], int off, int len) + throws IOException { + if (len >= buffer.length) { + throw new IllegalArgumentException( + "byte size exceeds blocksize"); + } + if (len > buffer.length - count) { + flushBuffer(); + } + System.arraycopy(b, off, buffer, count, len); + count += len; + } + + private void flushBuffer() { + writeToXML(buffer, 0, count); + + count = 0; + } + + private void writeToXML(byte[] buffer, int offset, int len) { + template.setPacketID(messageID + "_" + seq); + IBBExtensions.Data ext = (IBBExtensions.Data) template + .getExtension(IBBExtensions.Data.ELEMENT_NAME, + IBBExtensions.NAMESPACE); + + String enc = Base64.encodeBytes(buffer, offset, count, options); + + ext.setData(enc); + ext.setSeq(seq); + + connection.sendPacket(template); + + seq = (seq + 1 == 65535 ? 0 : seq + 1); + } + + public void close() throws IOException { + connection.sendPacket(closePacket); + } + + public void flush() throws IOException { + flushBuffer(); + } + + public void write(byte[] b) throws IOException { + write(b, 0, b.length); + } + + } + + private class IBBInputStream extends InputStream implements PacketListener { + + private String streamID; + + private PacketCollector dataCollector; + + private byte[] buffer; + + private int bufferPointer; + + private int seq = -1; + + private boolean isDone; + + private boolean isEOF; + + private boolean isClosed; + + private IQ closeConfirmation; + + private IBBInputStream(String streamID, PacketFilter dataFilter, + PacketFilter closeFilter) { + this.streamID = streamID; + this.dataCollector = connection.createPacketCollector(dataFilter); + connection.addPacketListener(this, closeFilter); + this.bufferPointer = -1; + } + + public synchronized int read() throws IOException { + if (isEOF || isClosed) { + return -1; + } + if (bufferPointer == -1 || bufferPointer >= buffer.length) { + loadBufferWait(); + } + + return (int) buffer[bufferPointer++]; + } + + public synchronized int read(byte[] b) throws IOException { + return read(b, 0, b.length); + } + + public synchronized int read(byte[] b, int off, int len) + throws IOException { + if (isEOF || isClosed) { + return -1; + } + if (bufferPointer == -1 || bufferPointer >= buffer.length) { + if (!loadBufferWait()) { + isEOF = true; + return -1; + } + } + + if (len - off > buffer.length - bufferPointer) { + len = buffer.length - bufferPointer; + } + + System.arraycopy(buffer, bufferPointer, b, off, len); + bufferPointer += len; + return len; + } + + private boolean loadBufferWait() throws IOException { + IBBExtensions.Data data; + do { + Message mess = null; + while (mess == null) { + if (isDone) { + mess = (Message) dataCollector.pollResult(); + if (mess == null) { + return false; + } + } + else { + mess = (Message) dataCollector.nextResult(1000); + } + } + data = (IBBExtensions.Data) mess.getExtension( + IBBExtensions.Data.ELEMENT_NAME, + IBBExtensions.NAMESPACE); + } + while (!data.getSessionID().equals(streamID)); + checkSequence((int) data.getSeq()); + buffer = Base64.decode(data.getData()); + bufferPointer = 0; + return true; + } + + private void checkSequence(int seq) throws IOException { + if (this.seq == 65535) { + this.seq = -1; + } + if (seq - 1 != this.seq) { + cancelTransfer(); + throw new IOException("Packets out of sequence"); + } + else { + this.seq = seq; + } + } + + private void cancelTransfer() { + cleanup(); + + sendCancelMessage(); + } + + private void cleanup() { + dataCollector.cancel(); + connection.removePacketListener(this); + } + + private void sendCancelMessage() { + } + + public boolean markSupported() { + return false; + } + + public void processPacket(Packet packet) { + IBBExtensions.Close close = (IBBExtensions.Close) packet; + if (close.getSessionID().equals(streamID)) { + isDone = true; + closeConfirmation = FileTransferNegotiator.createIQ(packet + .getPacketID(), packet.getFrom(), packet.getTo(), + IQ.Type.RESULT); + } + } + + public synchronized void close() throws IOException { + if (isClosed) { + return; + } + cleanup(); + + if (isEOF) { + sendCloseConfirmation(); + } + else { + sendCancelMessage(); + } + isClosed = true; + } + + private void sendCloseConfirmation() { + connection.sendPacket(closeConfirmation); + } + } + + private static class IBBSidFilter implements PacketFilter { + + private String sessionID; + + public IBBSidFilter(String sessionID) { + if (sessionID == null) { + throw new IllegalArgumentException("StreamID cannot be null"); + } + this.sessionID = sessionID; + } + + public boolean accept(Packet packet) { + if (!IBBExtensions.Open.class.isInstance(packet)) { + return false; + } + IBBExtensions.Open open = (IBBExtensions.Open) packet; + String sessionID = open.getSessionID(); + + return (sessionID != null && sessionID.equals(this.sessionID)); + } + } + +} diff --git a/source/org/jivesoftware/smackx/filetransfer/IncomingFileTransfer.java b/source/org/jivesoftware/smackx/filetransfer/IncomingFileTransfer.java new file mode 100644 index 000000000..1204ff104 --- /dev/null +++ b/source/org/jivesoftware/smackx/filetransfer/IncomingFileTransfer.java @@ -0,0 +1,171 @@ +/** + * + */ +package org.jivesoftware.smackx.filetransfer; + +import org.jivesoftware.smack.XMPPException; + +import java.io.*; + +/** + * An incoming file transfer is created when the + * {@link FileTransferManager#createIncomingFileTransfer(FileTransferRequest)} + * method is invoked. It is a file being sent to the local user from another + * user on the jabber network. There are two stages of the file transfer to be + * concerned with and they can be handled in different ways depending upon the + * method that is invoked on this class. + *

+ * The first way that a file is recieved is by calling the + * {@link #recieveFile()} method. This method, negotiates the appropriate stream + * method and then returns the InputStream to read the file + * data from. + *

+ * The second way that a file can be recieved through this class is by invoking + * the {@link #recieveFile(File)} method. This method returns immediatly and + * takes as its parameter a file on the local file system where the file + * recieved from the transfer will be put. + * + * @author Alexander Wenckus + */ +public class IncomingFileTransfer extends FileTransfer { + + private FileTransferRequest recieveRequest; + + private Thread transferThread; + + private InputStream inputStream; + + protected IncomingFileTransfer(FileTransferRequest request, + FileTransferNegotiator transferNegotiator) { + super(request.getRequestor(), request.getStreamID(), transferNegotiator); + this.recieveRequest = request; + } + + /** + * Negotiates the stream method to transfer the file over and then returns + * the negotiated stream. + * + * @return The negotiated InputStream from which to read the data. + * @throws XMPPException If there is an error in the negotiation process an exception + * is thrown. + */ + public InputStream recieveFile() throws XMPPException { + if (inputStream != null) { + throw new IllegalStateException("Transfer already negotiated!"); + } + + try { + inputStream = negotiateStream(); + } + catch (XMPPException e) { + setException(e); + throw e; + } + + return inputStream; + } + + /** + * This method negotitates the stream and then transfer's the file over the + * negotiated stream. The transfered file will be saved at the provided + * location. + *

+ * This method will return immedialtly, file transfer progress can be + * monitored through several methods: + *

+ *

+ * + * @param file The location to save the file. + * @throws XMPPException + * @throws IllegalArgumentException This exception is thrown when the the provided file is + * either null, or cannot be written to. + */ + public void recieveFile(final File file) throws XMPPException { + if (file != null) { + if (!file.exists()) { + try { + file.createNewFile(); + } + catch (IOException e) { + throw new XMPPException( + "Could not create file to write too", e); + } + } + if (!file.canWrite()) { + throw new IllegalArgumentException("Cannot write to provided file"); + } + } + else { + throw new IllegalArgumentException("File cannot be null"); + } + + transferThread = new Thread(new Runnable() { + public void run() { + try { + inputStream = negotiateStream(); + } + catch (XMPPException e) { + handleXMPPException(e); + return; + } + + OutputStream outputStream = null; + try { + outputStream = new FileOutputStream(file); + setStatus(Status.IN_PROGRESS); + writeToStream(inputStream, outputStream); + } + catch (XMPPException e) { + setStatus(FileTransfer.Status.ERROR); + setError(Error.STREAM); + setException(e); + } + catch (FileNotFoundException e) { + setStatus(FileTransfer.Status.ERROR); + setError(Error.BAD_FILE); + setException(e); + } + + if (getStatus().equals(Status.IN_PROGRESS)) + setStatus(Status.COMPLETE); + try { + if (inputStream != null) { + inputStream.close(); + } + if (outputStream != null) { + outputStream.close(); + } + } + catch (IOException e) { + } + } + }, "File Transfer " + streamID); + transferThread.start(); + + } + + private void handleXMPPException(XMPPException e) { + setStatus(FileTransfer.Status.ERROR); + setException(e); + } + + private InputStream negotiateStream() throws XMPPException { + setStatus(Status.NEGOTIATING_TRANSFER); + StreamNegotiator streamNegotiator = negotiator + .selectStreamNegotiator(recieveRequest); + setStatus(Status.NEGOTIATING_STREAM); + InputStream inputStream = streamNegotiator + .initiateIncomingStream(recieveRequest.getStreamInitiation()); + setStatus(Status.NEGOTIATED); + return inputStream; + } + + public void cancel() { + setStatus(Status.CANCLED); + } + +} diff --git a/source/org/jivesoftware/smackx/filetransfer/OutgoingFileTransfer.java b/source/org/jivesoftware/smackx/filetransfer/OutgoingFileTransfer.java new file mode 100644 index 000000000..ee39eabb9 --- /dev/null +++ b/source/org/jivesoftware/smackx/filetransfer/OutgoingFileTransfer.java @@ -0,0 +1,348 @@ +/** + * + */ +package org.jivesoftware.smackx.filetransfer; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +import org.jivesoftware.smack.XMPPException; +import org.jivesoftware.smack.packet.XMPPError; + +/** + * Handles the sending of a file to another user. File transfer's in jabber have + * several steps and there are several methods in this class that handle these + * steps differently. + * + * @author Alexander Wenckus + * + */ +public class OutgoingFileTransfer extends FileTransfer { + + private static int RESPONSE_TIMEOUT = 60 * 1000; + + /** + * Returns the time in milliseconds after which the file transfer + * negotiation process will timeout if the other user has not responded. + * + * @return Returns the time in milliseconds after which the file transfer + * negotiation process will timeout if the remote user has not + * responded. + */ + public static int getResponseTimeout() { + return RESPONSE_TIMEOUT; + } + + /** + * Sets the time in milliseconds after which the file transfer negotiation + * process will timeout if the other user has not responded. + * + * @param responseTimeout + * The timeout time in milliseconds. + */ + public void setResponseTimeout(int responseTimeout) { + RESPONSE_TIMEOUT = responseTimeout; + } + + private OutputStream outputStream; + + private String initiator; + + private Thread transferThread; + + protected OutgoingFileTransfer(String initiator, String target, + String streamID, FileTransferNegotiator transferNegotiator) { + super(target, streamID, transferNegotiator); + this.initiator = initiator; + } + + protected void setOutputStream(OutputStream stream) { + if (outputStream == null) { + this.outputStream = stream; + } + } + + /** + * Returns the output stream connected to the peer to transfer the file. It + * is only available after it has been succesfully negotiated by the + * {@link StreamNegotiator}. + * + * @return Returns the output stream connected to the peer to transfer the + * file. + */ + protected OutputStream getOutputStream() { + if (getStatus().equals(FileTransfer.Status.NEGOTIATED)) { + return outputStream; + } else { + return null; + } + } + + /** + * This method handles the negotiation of the file transfer and the stream, + * it only returns the created stream after the negotiation has been completed. + * + * @param fileName + * The name of the file that will be transmitted. It is + * preferable for this name to have an extension as it will be + * used to determine the type of file it is. + * @param fileSize + * The size in bytes of the file that will be transmitted. + * @param description + * A description of the file that will be transmitted. + * @return The OutputStream that is connected to the peer to transmit the + * file. + * @throws XMPPException + * Thrown if an error occurs during the file transfer + * negotiation process. + */ + public synchronized OutputStream sendFile(String fileName, long fileSize, + String description) throws XMPPException { + if (isDone() || outputStream != null) { + throw new IllegalStateException( + "The negotation process has already" + + " been attempted on this file transfer"); + } + try { + this.outputStream = negotiateStream(fileName, fileSize, description); + } catch (XMPPException e) { + handleXMPPException(e); + throw e; + } + return outputStream; + } + + /** + * This methods handles the transfer and stream negotiation process. It + * returns immediately and its progress can be monitored through the + * {@link NegotiationProgress} callback. When the negotiation process is + * complete the OutputStream can be retrieved from the callback via the + * {@link NegotiationProgress#getOutputStream()} method. + * + * @param fileName + * The name of the file that will be transmitted. It is + * preferable for this name to have an extension as it will be + * used to determine the type of file it is. + * @param fileSize + * The size in bytes of the file that will be transmitted. + * @param description + * A description of the file that will be transmitted. + * @param progress + * A callback to monitor the progress of the file transfer + * negotiation process and to retrieve the OutputStream when it + * is complete. + */ + public synchronized void sendFile(final String fileName, + final long fileSize, final String description, + NegotiationProgress progress) { + checkTransferThread(); + if (isDone() || outputStream != null) { + throw new IllegalStateException( + "The negotation process has already" + + " been attempted for this file transfer"); + } + progress.delegate = this; + transferThread = new Thread(new Runnable() { + public void run() { + try { + OutgoingFileTransfer.this.outputStream = negotiateStream( + fileName, fileSize, description); + } catch (XMPPException e) { + handleXMPPException(e); + } + } + }, "File Transfer Negotiation " + streamID); + transferThread.start(); + } + + private void checkTransferThread() { + if (transferThread != null && transferThread.isAlive() || isDone()) { + throw new IllegalStateException( + "File transfer in progress or has already completed."); + } + } + + /** + * This method handles the stream negotiation process and transmits the file + * to the remote user. It returns immediatly and the progress of the file + * transfer can be monitored through several methods: + * + * + * + * @throws XMPPException + * If there is an error during the negotiation process or the + * sending of the file. + */ + public synchronized void sendFile(final File file, final String description) + throws XMPPException { + checkTransferThread(); + if (file == null || !file.exists() || !file.canRead()) { + throw new IllegalArgumentException("Could not read file"); + } else { + setFileInfo(file.getAbsolutePath(), file.getName(), file.length()); + } + + transferThread = new Thread(new Runnable() { + public void run() { + try { + outputStream = negotiateStream(file.getName(), file + .length(), description); + } catch (XMPPException e) { + handleXMPPException(e); + return; + } + if (outputStream == null) { + return; + } + + if (!getStatus().equals(Status.NEGOTIATED)) { + return; + } + setStatus(Status.IN_PROGRESS); + + InputStream inputStream = null; + try { + inputStream = new FileInputStream(file); + writeToStream(inputStream, outputStream); + } catch (FileNotFoundException e) { + setStatus(FileTransfer.Status.ERROR); + setError(Error.BAD_FILE); + setException(e); + } catch (XMPPException e) { + setStatus(FileTransfer.Status.ERROR); + setException(e); + } finally { + try { + if (inputStream != null) { + inputStream.close(); + } + + outputStream.flush(); + outputStream.close(); + } catch (IOException e) { + } + } + if (getStatus().equals(Status.IN_PROGRESS)) { + setStatus(FileTransfer.Status.COMPLETE); + } + } + + }, "File Transfer " + streamID); + transferThread.start(); + } + + private void handleXMPPException(XMPPException e) { + setStatus(FileTransfer.Status.ERROR); + XMPPError error = e.getXMPPError(); + if (error != null) { + int code = error.getCode(); + if (code == 403) { + setStatus(Status.REFUSED); + return; + } else if (code == 400) { + setStatus(Status.ERROR); + setError(Error.NOT_ACCEPTABLE); + } + } + setException(e); + return; + } + + /** + * Returns the amount of bytes that have been sent for the file transfer. Or + * -1 if the file transfer has not started. + *

+ * Note: This method is only useful when the {@link #sendFile(File, String)} + * method is called, as it is the only method that actualy transmits the + * file. + * + * @return Returns the amount of bytes that have been sent for the file + * transfer. Or -1 if the file transfer has not started. + */ + public long getBytesSent() { + return amountWritten; + } + + private OutputStream negotiateStream(String fileName, long fileSize, + String description) throws XMPPException { + // Negotiate the file transfer profile + + setStatus(Status.NEGOTIATING_TRANSFER); + StreamNegotiator streamNegotiator = negotiator.negotiateOutgoingTransfer( + getPeer(), streamID, fileName, fileSize, description, + RESPONSE_TIMEOUT); + + if (streamNegotiator == null) { + setStatus(Status.ERROR); + setError(Error.NO_RESPONSE); + return null; + } + + if (!getStatus().equals(Status.NEGOTIATING_TRANSFER)) { + return null; + } + + // Negotiate the stream + + setStatus(Status.NEGOTIATING_STREAM); + outputStream = streamNegotiator.initiateOutgoingStream(streamID, + initiator, getPeer()); + if (!getStatus().equals(Status.NEGOTIATING_STREAM)) { + return null; + } + setStatus(Status.NEGOTIATED); + return outputStream; + } + + public void cancel() { + setStatus(Status.CANCLED); + } + + /** + * A callback class to retrive the status of an outgoing transfer + * negotiation process. + * + * @author Alexander Wenckus + * + */ + public static class NegotiationProgress { + + private OutgoingFileTransfer delegate; + + /** + * Returns the current status of the negotiation process. + * + * @return Returns the current status of the negotiation process. + */ + public Status getStatus() { + if (delegate == null) { + throw new IllegalStateException("delegate not yet set"); + } + return delegate.getStatus(); + } + + /** + * Once the negotiation process is completed the output stream can be + * retrieved. + * + * @return Once the negotiation process is completed the output stream + * can be retrieved. + * + */ + public OutputStream getOutputStream() { + if (delegate == null) { + throw new IllegalStateException("delegate not yet set"); + } + return delegate.getOutputStream(); + } + } + +} diff --git a/source/org/jivesoftware/smackx/filetransfer/Socks5TransferNegotiator.java b/source/org/jivesoftware/smackx/filetransfer/Socks5TransferNegotiator.java new file mode 100644 index 000000000..da880527d --- /dev/null +++ b/source/org/jivesoftware/smackx/filetransfer/Socks5TransferNegotiator.java @@ -0,0 +1,725 @@ +package org.jivesoftware.smackx.filetransfer; + +import org.jivesoftware.smack.PacketCollector; +import org.jivesoftware.smack.SmackConfiguration; +import org.jivesoftware.smack.XMPPConnection; +import org.jivesoftware.smack.XMPPException; +import org.jivesoftware.smack.filter.AndFilter; +import org.jivesoftware.smack.filter.FromMatchesFilter; +import org.jivesoftware.smack.filter.PacketFilter; +import org.jivesoftware.smack.filter.PacketIDFilter; +import org.jivesoftware.smack.packet.IQ; +import org.jivesoftware.smack.packet.Packet; +import org.jivesoftware.smack.util.StringUtils; +import org.jivesoftware.smackx.ServiceDiscoveryManager; +import org.jivesoftware.smackx.packet.Bytestream; +import org.jivesoftware.smackx.packet.Bytestream.StreamHost; +import org.jivesoftware.smackx.packet.Bytestream.StreamHostUsed; +import org.jivesoftware.smackx.packet.DiscoverInfo; +import org.jivesoftware.smackx.packet.DiscoverInfo.Identity; +import org.jivesoftware.smackx.packet.DiscoverItems; +import org.jivesoftware.smackx.packet.DiscoverItems.Item; +import org.jivesoftware.smackx.packet.StreamInitiation; + +import java.io.*; +import java.net.*; +import java.util.*; + +/** + * A SOCKS5 bytestream is negotiated partly over the XMPP XML stream and partly + * over a seperate socket. The actual transfer though takes place over a + * seperatly created socket. + *

+ * A SOCKS5 file transfer generally has three parites, the initiator, the + * target, and the stream host. The stream host is a specialized SOCKS5 proxy + * setup on the server, or, the Initiator can act as the Stream Host if the + * proxy is not available. + *

+ * The advantage of having a seperate proxy over directly connecting to + * eachother is if the Initator and the Target are not on the same LAN and are + * operating behind NAT, the proxy allows for a common location for both parties + * to connect to and transfer the file. + *

+ * Smack will attempt to automatically discover any proxies present on your + * server. If any are detected they will be forwarded to any user attempting to + * recieve files from you. + * + * @author Alexander Wenckus + * @see JEP-0065: SOCKS5 + * Bytestreams + */ +public class Socks5TransferNegotiator extends StreamNegotiator { + + protected static final String NAMESPACE = "http://jabber.org/protocol/bytestreams"; + + public static boolean isAllowLocalProxyHost = true; + + private final XMPPConnection connection; + + private List proxies; + + private List streamHosts; + private ProxyProcess proxyProcess; + + public Socks5TransferNegotiator(final XMPPConnection connection) { + this.connection = connection; + } + + /* + * (non-Javadoc) + * + * @see org.jivesoftware.smackx.filetransfer.StreamNegotiator#initiateDownload(org.jivesoftware.smackx.packet.StreamInitiation, + * java.io.File) + */ + public InputStream initiateIncomingStream(StreamInitiation initiation) + throws XMPPException { + StreamInitiation response = super.createInitiationAccept(initiation, + NAMESPACE); + + // establish collector to await response + PacketCollector collector = connection + .createPacketCollector(new AndFilter(new FromMatchesFilter(initiation.getFrom()), + new BytestreamSIDFilter(initiation.getSessionID()))); + connection.sendPacket(response); + + Bytestream streamHostsInfo = (Bytestream) collector + .nextResult(SmackConfiguration.getPacketReplyTimeout()); + if (streamHostsInfo == null) { + throw new XMPPException("No response from file transfer initiator"); + } + else if (streamHostsInfo.getType().equals(IQ.Type.ERROR)) { + throw new XMPPException(streamHostsInfo.getError()); + } + collector.cancel(); + + // select appropriate host + SelectedHostInfo selectedHost = selectHost(streamHostsInfo); + + // send used-host confirmation + Bytestream streamResponse = createUsedHostConfirmation( + selectedHost.selectedHost, streamHostsInfo.getFrom(), + streamHostsInfo.getTo(), streamHostsInfo.getPacketID()); + connection.sendPacket(streamResponse); + + try { + return selectedHost.establishedSocket.getInputStream(); + } + catch (IOException e) { + throw new XMPPException("Error establishing input stream", e); + } + } + + /** + * The used host confirmation is sent to the initiator to indicate to them + * which of the hosts they provided has been selected and successfully + * connected to. + * + * @param selectedHost The selected stream host. + * @param initiator The initiator of the stream. + * @param target The target of the stream. + * @param packetID The of the packet being responded to. + * @return The packet that was created to send to the initiator. + */ + private Bytestream createUsedHostConfirmation(StreamHost selectedHost, + String initiator, String target, String packetID) { + Bytestream streamResponse = new Bytestream(); + streamResponse.setTo(initiator); + streamResponse.setFrom(target); + streamResponse.setType(IQ.Type.RESULT); + streamResponse.setPacketID(packetID); + streamResponse.setUsedHost(selectedHost.getJID()); + return streamResponse; + } + + /** + * @param streamHostsInfo + * @return + * @throws XMPPException + */ + private SelectedHostInfo selectHost(Bytestream streamHostsInfo) + throws XMPPException { + Iterator it = streamHostsInfo.getStreamHosts().iterator(); + StreamHost selectedHost = null; + Socket socket = null; + while (it.hasNext()) { + selectedHost = (StreamHost) it.next(); + + // establish socket + try { + socket = new Socket(selectedHost.getAddress(), selectedHost + .getPort()); + establishSOCKS5ConnectionToProxy(socket, createDigest( + streamHostsInfo.getSessionID(), streamHostsInfo + .getFrom(), streamHostsInfo.getTo())); + break; + } + catch (IOException e) { + e.printStackTrace(); + selectedHost = null; + socket = null; + continue; + } + } + if (selectedHost == null || socket == null) { + throw new XMPPException( + "Could not establish socket with any provided host"); + } + + return new SelectedHostInfo(selectedHost, socket); + } + + /** + * Creates the digest needed for a byte stream. It is the SHA1(sessionID + + * initiator + target). + * + * @param sessionID The sessionID of the stream negotiation + * @param initiator The inititator of the stream negotiation + * @param target The target of the stream negotiation + * @return SHA-1 hash of the three parameters + */ + private String createDigest(final String sessionID, final String initiator, + final String target) { + return StringUtils.hash(sessionID + StringUtils.parseName(initiator) + + "@" + StringUtils.parseServer(initiator) + "/" + + StringUtils.parseResource(initiator) + + StringUtils.parseName(target) + "@" + + StringUtils.parseServer(target) + "/" + + StringUtils.parseResource(target)); + } + + /* + * (non-Javadoc) + * + * @see org.jivesoftware.smackx.filetransfer.StreamNegotiator#initiateUpload(java.lang.String, + * org.jivesoftware.smackx.packet.StreamInitiation, java.io.File) + */ + public OutputStream initiateOutgoingStream(String streamID, String initiator, + String target) throws XMPPException { + Socket socket; + try { + socket = initBytestreamSocket(streamID, initiator, target); + } + catch (Exception e) { + throw new XMPPException("Error establishing transfer socket", e); + } + + if (socket != null) { + try { + return socket.getOutputStream(); + } + catch (IOException e) { + throw new XMPPException("Error establishing output stream", e); + } + } + return null; + } + + private Socket initBytestreamSocket(final String sessionID, + String initiator, String target) throws Exception { + ProxyProcess process; + try { + process = establishListeningSocket(); + } + catch (IOException io) { + process = null; + } + + String localIP; + try { + localIP = discoverLocalIP(); + } + catch (UnknownHostException e1) { + localIP = null; + } + + Bytestream query = createByteStreamInit(initiator, target, sessionID, + localIP, (process != null ? process.getPort() : 0)); + + // if the local host is one of the options we need to wait for the + // remote connection. + Socket conn = waitForUsedHostResponse(sessionID, process, createDigest( + sessionID, initiator, target), query).establishedSocket; + cleanupListeningSocket(); + return conn; + } + + + /** + * Waits for the peer to respond with which host they chose to use. + * + * @param sessionID The session id of the stream. + * @param proxy The server socket which will listen locally for remote + * connections. + * @param digest + * @param query + * @return + * @throws XMPPException + * @throws IOException + */ + private SelectedHostInfo waitForUsedHostResponse(String sessionID, + final ProxyProcess proxy, final String digest, + final Bytestream query) throws XMPPException, IOException { + SelectedHostInfo info = new SelectedHostInfo(); + + PacketCollector collector = connection + .createPacketCollector(new PacketIDFilter(query.getPacketID())); + connection.sendPacket(query); + + Packet packet = collector.nextResult(); + collector.cancel(); + Bytestream response; + if (packet instanceof Bytestream) { + response = (Bytestream) packet; + } + else { + throw new XMPPException("Unexpected response from remote user"); + } + + StreamHostUsed used = response.getUsedHost(); + StreamHost usedHost = query.getStreamHost(used.getJID()); + if (usedHost == null) { + throw new XMPPException("Remote user responded with unknown host"); + } + // The local computer is acting as the proxy + if (used.getJID().equals(query.getFrom())) { + info.establishedSocket = proxy.getSocket(digest); + info.selectedHost = usedHost; + return info; + } + else { + info.establishedSocket = new Socket(usedHost.getAddress(), usedHost + .getPort()); + establishSOCKS5ConnectionToProxy(info.establishedSocket, digest); + + Bytestream activate = createByteStreamActivate(sessionID, response + .getTo(), usedHost.getJID(), response.getFrom()); + + collector = connection.createPacketCollector(new PacketIDFilter( + activate.getPacketID())); + connection.sendPacket(activate); + + IQ serverResponse = (IQ) collector.nextResult(); + collector.cancel(); + if (!serverResponse.getType().equals(IQ.Type.RESULT)) { + info.establishedSocket.close(); + return null; + } + return info; + } + } + + private ProxyProcess establishListeningSocket() throws IOException { + if (proxyProcess == null) { + proxyProcess = new ProxyProcess(new ServerSocket(7777)); + proxyProcess.start(); + } + proxyProcess.addTransfer(); + return proxyProcess; + } + + private void cleanupListeningSocket() { + if (proxyProcess == null) { + return; + } + proxyProcess.removeTransfer(); + } + + private String discoverLocalIP() throws UnknownHostException { + return InetAddress.getLocalHost().getHostAddress(); + } + + /** + * The bytestream init looks like this: + *

+ *

+     * <iq type='set'
+     *     from='initiator@host1/foo'
+     *     to='target@host2/bar'
+     *     id='initiate'>
+     *   <query xmlns='http://jabber.org/protocol/bytestreams'
+     *          sid='mySID'
+     * 	 mode='tcp'>
+     *     <streamhost
+     *         jid='initiator@host1/foo'
+     *         host='192.168.4.1'
+     *        port='5086'/>
+     *     <streamhost
+     *         jid='proxy.host3'
+     *         host='24.24.24.1'
+     *         zeroconf='_jabber.bytestreams'/>
+     *   </query>
+     * </iq>
+     * 
+ * + * @param from initiator@host1/foo - The file transfer initiator. + * @param to target@host2/bar - The file transfer target. + * @param sid 'mySID' - the unique identifier for this file transfer + * @param localIP The IP of the local machine if it is being provided, null otherwise. + * @param port The port of the local mahine if it is being provided, null otherwise. + * @return Returns the created Bytestream packet + */ + private Bytestream createByteStreamInit(final String from, final String to, + final String sid, final String localIP, final int port) { + + Bytestream bs = new Bytestream(); + bs.setTo(to); + bs.setFrom(from); + bs.setSessionID(sid); + bs.setType(IQ.Type.SET); + bs.setMode(Bytestream.Mode.TCP); + if (localIP != null && port > 0) { + bs.addStreamHost(from, localIP, port); + } + if (proxies == null) { + initProxies(); + } + if (streamHosts != null) { + Iterator it = streamHosts.iterator(); + while (it.hasNext()) { + bs.addStreamHost((StreamHost) it.next()); + } + } + + return bs; + } + + private void initProxies() { + proxies = new ArrayList(); + ServiceDiscoveryManager manager = ServiceDiscoveryManager + .getInstanceFor(connection); + + DiscoverItems discoItems; + try { + discoItems = manager.discoverItems(connection.getServiceName()); + + DiscoverItems.Item item; + DiscoverInfo info; + DiscoverInfo.Identity identity; + + Iterator it = discoItems.getItems(); + while (it.hasNext()) { + item = (Item) it.next(); + info = manager.discoverInfo(item.getEntityID()); + Iterator itx = info.getIdentities(); + while (itx.hasNext()) { + identity = (Identity) itx.next(); + if (identity.getCategory().equalsIgnoreCase("proxy") + && identity.getType().equalsIgnoreCase( + "bytestreams")) { + proxies.add(info.getFrom()); + } + } + } + } + catch (XMPPException e) { + return; + } + if (proxies.size() > 0) { + initStreamHosts(); + } + + } + + private void initStreamHosts() { + List streamHosts = new ArrayList(); + Iterator it = proxies.iterator(); + IQ query; + PacketCollector collector; + Bytestream response; + while (it.hasNext()) { + String jid = it.next().toString(); + query = new IQ() { + public String getChildElementXML() { + return ""; + } + }; + query.setType(IQ.Type.GET); + query.setTo(jid); + + collector = connection.createPacketCollector(new PacketIDFilter( + query.getPacketID())); + connection.sendPacket(query); + + response = (Bytestream) collector.nextResult(SmackConfiguration + .getPacketReplyTimeout()); + if (response != null) { + streamHosts.addAll(response.getStreamHosts()); + } + collector.cancel(); + } + this.streamHosts = streamHosts; + } + + /** + * Returns the packet to send notification to the stream host to activate + * the stream. + * + * @param sessionID The session ID of the file transfer to activate. + * @param from + * @param to The JID of the stream host + * @param target The JID of the file transfer target. + * @return Returns the packet to send notification to the stream host to + * activate the stream. + */ + private static Bytestream createByteStreamActivate(final String sessionID, + final String from, final String to, final String target) { + Bytestream activate = new Bytestream(sessionID); + activate.setMode(null); + activate.setToActivate(target); + activate.setFrom(from); + activate.setTo(to); + activate.setType(IQ.Type.SET); + return activate; + } + + /** + * Negotiates the Socks 5 bytestream when the local computer is acting as + * the proxy. + * + * @param connection The socket connection with the peer. + * @return The SHA-1 digest that is used to uniquely identify the file + * transfer. + * @throws XMPPException + * @throws IOException + */ + private String establishSocks5UploadConnection(Socket connection) throws XMPPException, IOException { + OutputStream out = new DataOutputStream(connection.getOutputStream()); + InputStream in = new DataInputStream(connection.getInputStream()); + + // first byte is version should be 5 + int b = in.read(); + if (b != 5) { + throw new XMPPException("Only SOCKS5 supported"); + } + + // second byte number of authentication methods supported + b = in.read(); + int[] auth = new int[b]; + for (int i = 0; i < b; i++) { + auth[i] = in.read(); + } + + int authMethod = -1; + for (int i = 0; i < auth.length; i++) { + authMethod = (auth[i] == 0 ? 0 : -1); // only auth method + // 0, no + // authentication, + // supported + if (authMethod == 0) { + break; + } + } + if (authMethod != 0) { + throw new XMPPException("Authentication method not supported"); + } + byte[] cmd = new byte[2]; + cmd[0] = (byte) 0x05; + cmd[1] = (byte) 0x00; + out.write(cmd); + + String responseDigest = createIncomingSocks5Message(in); + cmd = createOutgoingSocks5Message(0, responseDigest); + + if (!connection.isConnected()) { + throw new XMPPException("Socket closed by remote user"); + } + out.write(cmd); + return responseDigest; + } + + public String getNamespace() { + return NAMESPACE; + } + + private void establishSOCKS5ConnectionToProxy(Socket socket, String digest) + throws IOException { + + byte[] cmd = new byte[3]; + + cmd[0] = (byte) 0x05; + cmd[1] = (byte) 0x01; + cmd[2] = (byte) 0x00; + + OutputStream out = new DataOutputStream(socket.getOutputStream()); + out.write(cmd); + + InputStream in = new DataInputStream(socket.getInputStream()); + byte[] response = new byte[2]; + + in.read(response); + + cmd = createOutgoingSocks5Message(1, digest); + out.write(cmd); + createIncomingSocks5Message(in); + } + + private String createIncomingSocks5Message(InputStream in) + throws IOException { + byte[] cmd = new byte[5]; + in.read(cmd, 0, 5); + + byte[] addr = new byte[cmd[4]]; + in.read(addr, 0, addr.length); + String digest = new String(addr); + in.read(); + in.read(); + + return digest; + } + + private byte[] createOutgoingSocks5Message(int cmd, String digest) { + byte addr[] = digest.getBytes(); + + byte[] data = new byte[7 + addr.length]; + data[0] = (byte) 5; + data[1] = (byte) cmd; + data[2] = (byte) 0; + data[3] = (byte) 0x3; + data[4] = (byte) addr.length; + + System.arraycopy(addr, 0, data, 5, addr.length); + data[data.length - 2] = (byte) 0; + data[data.length - 1] = (byte) 0; + + return data; + } + + private class SelectedHostInfo { + + protected XMPPException exception; + + protected StreamHost selectedHost; + + protected Socket establishedSocket; + + SelectedHostInfo(StreamHost selectedHost, Socket establishedSocket) { + this.selectedHost = selectedHost; + this.establishedSocket = establishedSocket; + } + + public SelectedHostInfo() { + } + } + + private class ProxyProcess implements Runnable { + + private ServerSocket listeningSocket; + + private Map connectionMap = new HashMap(); + + private boolean done = false; + + private Thread thread; + private int transfers; + + public void run() { + try { + listeningSocket.setSoTimeout(10000); + } + catch (SocketException e) { + e.printStackTrace(); + } + while (!done) { + Socket conn = null; + synchronized (ProxyProcess.this) { + while (transfers <= 0) { + transfers = -1; + try { + ProxyProcess.this.wait(); + } + catch (InterruptedException e) { + } + } + } + try { + synchronized (listeningSocket) { + conn = listeningSocket.accept(); + } + if (conn == null) { + continue; + } + String digest = establishSocks5UploadConnection(conn); + synchronized (connectionMap) { + connectionMap.put(digest, conn); + } + } + catch (IOException e) { + } + catch (XMPPException e) { + e.printStackTrace(); + if (conn != null) { + try { + conn.close(); + } + catch (IOException e1) { + } + } + } + } + } + + + public void start() { + thread.start(); + } + + public void stop() { + done = true; + } + + public int getPort() { + return listeningSocket.getLocalPort(); + } + + ProxyProcess(ServerSocket listeningSocket) { + thread = new Thread(this, "File Transfer Connection Listener"); + this.listeningSocket = listeningSocket; + } + + public Socket getSocket(String digest) { + synchronized (connectionMap) { + return (Socket) connectionMap.get(digest); + } + } + + public void addTransfer() { + synchronized (this) { + if (transfers == -1) { + transfers = 1; + thread.notify(); + } + else { + transfers++; + } + } + } + + public void removeTransfer() { + synchronized (this) { + transfers--; + } + } + } + + private static class BytestreamSIDFilter implements PacketFilter { + + private String sessionID; + + public BytestreamSIDFilter(String sessionID) { + if (sessionID == null) { + throw new IllegalArgumentException("StreamID cannot be null"); + } + this.sessionID = sessionID; + } + + public boolean accept(Packet packet) { + if (!Bytestream.class.isInstance(packet)) { + return false; + } + Bytestream bytestream = (Bytestream) packet; + String sessionID = bytestream.getSessionID(); + + return (sessionID != null && sessionID.equals(this.sessionID)); + } + } +} diff --git a/source/org/jivesoftware/smackx/filetransfer/StreamNegotiator.java b/source/org/jivesoftware/smackx/filetransfer/StreamNegotiator.java new file mode 100644 index 000000000..f54180ab4 --- /dev/null +++ b/source/org/jivesoftware/smackx/filetransfer/StreamNegotiator.java @@ -0,0 +1,105 @@ +/** + * + */ +package org.jivesoftware.smackx.filetransfer; + +import java.io.InputStream; +import java.io.OutputStream; + +import org.jivesoftware.smack.XMPPException; +import org.jivesoftware.smack.packet.IQ; +import org.jivesoftware.smackx.Form; +import org.jivesoftware.smackx.FormField; +import org.jivesoftware.smackx.packet.DataForm; +import org.jivesoftware.smackx.packet.StreamInitiation; + +/** + * After the file transfer negotiation process is completed according to + * JEP-0096, the negotation process is passed off to a particular stream + * negotiator. The stream negotiator will then negotiate the chosen stream and + * return the stream to transfer the file. + * + * + * @author Alexander Wenckus + * + */ +public abstract class StreamNegotiator { + + /** + * Creates the initiation acceptance packet to forward to the stream + * initiator. + * + * @param streamInitiationOffer + * The offer from the stream initatior to connect for a stream. + * @param namespace + * The namespace that relates to the accepted means of transfer. + * @return The response to be forwarded to the initator. + */ + public StreamInitiation createInitiationAccept( + StreamInitiation streamInitiationOffer, String namespace) { + StreamInitiation response = new StreamInitiation(); + response.setTo(streamInitiationOffer.getFrom()); + response.setFrom(streamInitiationOffer.getTo()); + response.setType(IQ.Type.RESULT); + response.setPacketID(streamInitiationOffer.getPacketID()); + + DataForm form = new DataForm(Form.TYPE_SUBMIT); + FormField field = new FormField( + FileTransferNegotiator.STREAM_DATA_FIELD_NAME); + field.addValue(namespace); + form.addField(field); + + response.setFeatureNegotiationForm(form); + return response; + } + + /** + * This method handles the file stream download negotiation process. The + * appropriate stream negotiator's initiate incoming stream is called after + * an appropriate file transfer method is selected. The manager will respond + * to the initatior with the selected means of transfer, then it will handle + * any negotation specific to the particular transfer method. This method + * returns the InputStream, ready to transfer the file. + * + * @param initiation + * The initation that triggered this download. + * @return After the negotation process is complete, the InputStream to + * write a file to is returned. + * @throws XMPPException + * If an error occurs during this process an XMPPException is + * thrown. + */ + public abstract InputStream initiateIncomingStream( + StreamInitiation initiation) throws XMPPException; + + /** + * This method handles the file upload stream negotiation process. The + * particular stream negotiator is determined during the file transfer + * negotiation process. This method returns the OutputStream to transmit the + * file to the remote user. + * + * @param streamID + * The streamID that uniquely identifies the file transfer. + * @param initiator + * The fully-qualified JID of the initiator of the file transfer. + * @param target + * The fully-qualified JID of the target or reciever of the file + * transfer. + * @return The negotiated stream ready for data. + * @throws XMPPException + * If an error occurs during the negotiation process an + * exception will be thrown. + */ + public abstract OutputStream initiateOutgoingStream(String streamID, + String initiator, String target) throws XMPPException; + + /** + * Returns the XMPP namespace reserved for this particular type of file + * transfer. + * + * @return Returns the XMPP namespace reserved for this particular type of + * file transfer. + */ + public abstract String getNamespace(); + +} diff --git a/source/org/jivesoftware/smackx/packet/ByteStream.java b/source/org/jivesoftware/smackx/packet/ByteStream.java new file mode 100644 index 000000000..e073bc9d8 --- /dev/null +++ b/source/org/jivesoftware/smackx/packet/ByteStream.java @@ -0,0 +1,465 @@ +/* + * Created on Jun 21, 2005 + */ +package org.jivesoftware.smackx.packet; + +import org.jivesoftware.smack.packet.IQ; +import org.jivesoftware.smack.packet.PacketExtension; + +import java.util.*; + +/** + * A packet representing part of a Socks5 Bytestream negotiation. + * + * @author Alexander Wenckus + */ +public class Bytestream extends IQ { + + private String sessionID; + + private Mode mode = Mode.TCP; + + private final List streamHosts = new ArrayList(); + + private StreamHostUsed usedHost; + + private Activate toActivate; + + /** + * The default constructor + */ + public Bytestream() { + super(); + } + + /** + * A constructor where the session ID can be specified. + * + * @param SID The session ID related to the negotiation. + * @see #setSessionID(String) + */ + public Bytestream(final String SID) { + super(); + setSessionID(SID); + } + + /** + * Set the session ID related to the Byte Stream. The session ID is a unique + * identifier used to differentiate between stream negotations. + * + * @param sessionID + */ + public void setSessionID(final String sessionID) { + this.sessionID = sessionID; + } + + /** + * Returns the session ID related to the Byte Stream negotiation. + * + * @return Returns the session ID related to the Byte Stream negotiation. + * @see #setSessionID(String) + */ + public String getSessionID() { + return sessionID; + } + + /** + * Set the transport mode. This should be put in the initiation of the + * interaction. + * + * @param mode + * @see Mode + */ + public void setMode(final Mode mode) { + this.mode = mode; + } + + /** + * Returns the transport mode. + * + * @return Returns the transport mode. + * @see #setMode(Mode) + */ + public Mode getMode() { + return mode; + } + + /** + * Adds a potential stream host that the remote user can connect to to + * receive the file. + * + * @param JID The jabber ID of the stream host. + * @param address The internet address of the stream host. + * @return The added stream host. + */ + public StreamHost addStreamHost(final String JID, final String address) { + return addStreamHost(JID, address, 0); + } + + /** + * Adds a potential stream host that the remote user can connect to to + * receive the file. + * + * @param JID The jabber ID of the stream host. + * @param address The internet address of the stream host. + * @param port The port on which the remote host is seeking connections. + * @return The added stream host. + */ + public StreamHost addStreamHost(final String JID, final String address, + final int port) { + StreamHost host = new StreamHost(JID, address); + host.setPort(port); + addStreamHost(host); + + return host; + } + + /** + * Adds a potential stream host that the remote user can transfer the file + * through. + * + * @param host The potential stream host. + */ + public void addStreamHost(final StreamHost host) { + streamHosts.add(host); + } + + /** + * Returns the list of stream hosts contained in the packet. + * + * @return Returns the list of stream hosts contained in the packet. + */ + public Collection getStreamHosts() { + return Collections.unmodifiableCollection(streamHosts); + } + + /** + * Returns the stream host related to the given jabber ID, or null if there + * is none. + * + * @param JID The jabber ID of the desired stream host. + * @return Returns the stream host related to the given jabber ID, or null + * if there is none. + */ + public StreamHost getStreamHost(final String JID) { + StreamHost host; + for (Iterator it = streamHosts.iterator(); it.hasNext();) { + host = (StreamHost) it.next(); + if (host.getJID().equals(JID)) { + return host; + } + } + + return null; + } + + /** + * Returns the count of stream hosts contained in this packet. + * + * @return Returns the count of stream hosts contained in this packet. + */ + public int countStreamHosts() { + return streamHosts.size(); + } + + /** + * Upon connecting to the stream host the target of the stream replys to the + * initiator with the jabber id of the Socks5 host that they used. + * + * @param JID The jabber ID of the used host. + */ + public void setUsedHost(final String JID) { + this.usedHost = new StreamHostUsed(JID); + } + + /** + * Returns the Socks5 host connected to by the remote user. + * + * @return Returns the Socks5 host connected to by the remote user. + */ + public StreamHostUsed getUsedHost() { + return usedHost; + } + + /** + * Returns the activate element of the packet sent to the proxy host to + * verify the identity of the initiator and match them to the appropriate + * stream. + * + * @return Returns the activate element of the packet sent to the proxy host + * to verify the identity of the initiator and match them to the + * appropriate stream. + */ + public Activate getToActivate() { + return toActivate; + } + + /** + * Upon the response from the target of the used host the activate packet is + * sent to the Socks5 proxy. The proxy will activate the stream or return an + * error after verifying the identity of the initiator, using the activate + * packet. + * + * @param targetID The jabber ID of the target of the file transfer. + */ + public void setToActivate(final String targetID) { + this.toActivate = new Activate(targetID); + } + + public String getChildElementXML() { + StringBuffer buf = new StringBuffer(); + + buf.append(""); + if (getToActivate() == null) { + for (Iterator it = getStreamHosts().iterator(); it.hasNext();) + buf.append(((StreamHost) it.next()).toXML()); + } + else { + buf.append(getToActivate().toXML()); + } + } + else if (this.getType().equals(IQ.Type.RESULT)) { + buf.append(">"); + if (getUsedHost() != null) + buf.append(getUsedHost().toXML()); + } + else { + return null; + } + buf.append(""); + + return buf.toString(); + } + + /** + * Packet extension that represents a potential Socks5 proxy for the file + * transfer. Stream hosts are forwared to the target of the file transfer + * who then chooses and connects to one. + * + * @author Alexander Wenckus + */ + public static class StreamHost implements PacketExtension { + + public static String NAMESPACE = ""; + + public static String ELEMENTNAME = "streamhost"; + + private final String JID; + + private final String addy; + + private int port = 0; + + /** + * Default constructor. + * + * @param JID The jabber ID of the stream host. + * @param address The internet address of the stream host. + */ + public StreamHost(final String JID, final String address) { + this.JID = JID; + this.addy = address; + } + + /** + * Returns the jabber ID of the stream host. + * + * @return Returns the jabber ID of the stream host. + */ + public String getJID() { + return JID; + } + + /** + * Returns the internet address of the stream host. + * + * @return Returns the internet address of the stream host. + */ + public String getAddress() { + return addy; + } + + /** + * Sets the port of the stream host. + * + * @param port The port on which the potential stream host would accept + * the connection. + */ + public void setPort(final int port) { + this.port = port; + } + + /** + * Returns the port on which the potential stream host would accept the + * connection. + * + * @return Returns the port on which the potential stream host would + * accept the connection. + */ + public int getPort() { + return port; + } + + public String getNamespace() { + return NAMESPACE; + } + + public String getElementName() { + return ELEMENTNAME; + } + + public String toXML() { + StringBuffer buf = new StringBuffer(); + + buf.append("<").append(getElementName()).append(" "); + buf.append("jid=\"").append(getJID()).append("\" "); + buf.append("host=\"").append(getAddress()).append("\" "); + if (getPort() != 0) + buf.append("port=\"").append(getPort()).append("\""); + else + buf.append("zeroconf=\"_jabber.bytestreams\""); + buf.append("/>"); + + return buf.toString(); + } + } + + /** + * After selected a Socks5 stream host and successfully connecting, the + * target of the file transfer returns a byte stream packet with the stream + * host used extension. + * + * @author Alexander Wenckus + */ + public static class StreamHostUsed implements PacketExtension { + + public String NAMESPACE = ""; + + public static String ELEMENTNAME = "streamhost-used"; + + private final String JID; + + /** + * Default constructor. + * + * @param JID The jabber ID of the selected stream host. + */ + public StreamHostUsed(final String JID) { + this.JID = JID; + } + + /** + * Returns the jabber ID of the selected stream host. + * + * @return Returns the jabber ID of the selected stream host. + */ + public String getJID() { + return JID; + } + + public String getNamespace() { + return NAMESPACE; + } + + public String getElementName() { + return ELEMENTNAME; + } + + public String toXML() { + StringBuffer buf = new StringBuffer(); + buf.append("<").append(getElementName()).append(" "); + buf.append("jid=\"").append(getJID()).append("\" "); + buf.append("/>"); + return buf.toString(); + } + } + + /** + * The packet sent by the stream initiator to the stream proxy to activate + * the connection. + * + * @author Alexander Wenckus + */ + public static class Activate implements PacketExtension { + + public String NAMESPACE = ""; + + public static String ELEMENTNAME = "activate"; + + private final String target; + + /** + * Default constructor specifying the target of the stream. + * + * @param target The target of the stream. + */ + public Activate(final String target) { + this.target = target; + } + + /** + * Returns the target of the activation. + * + * @return Returns the target of the activation. + */ + public String getTarget() { + return target; + } + + public String getNamespace() { + return NAMESPACE; + } + + public String getElementName() { + return ELEMENTNAME; + } + + public String toXML() { + StringBuffer buf = new StringBuffer(); + buf.append("<").append(getElementName()).append(">"); + buf.append(getTarget()); + buf.append(""); + return buf.toString(); + } + } + + /** + * The stream can be either a TCP stream or a UDP stream. + * + * @author Alexander Wenckus + */ + public static class Mode { + + /** + * A TCP based stream. + */ + public static Mode TCP = new Mode("tcp"); + + /** + * A UDP based stream. + */ + public static Mode UDP = new Mode("udp"); + + private final String modeString; + + private Mode(final String mode) { + this.modeString = mode; + } + + public String toString() { + return modeString; + } + + public boolean equals(final Object obj) { + if (!(obj instanceof Mode)) + return false; + return modeString.equals(((Mode) obj).modeString); + } + } +} diff --git a/source/org/jivesoftware/smackx/packet/IBBExtensions.java b/source/org/jivesoftware/smackx/packet/IBBExtensions.java new file mode 100644 index 000000000..8b49ace1a --- /dev/null +++ b/source/org/jivesoftware/smackx/packet/IBBExtensions.java @@ -0,0 +1,225 @@ +/* + * Created on Jul 5, 2005 + */ +package org.jivesoftware.smackx.packet; + +import org.jivesoftware.smack.packet.IQ; +import org.jivesoftware.smack.packet.PacketExtension; + +/** + * The different extensions used throughtout the negotiation and transfer + * process. + * + * @author Alexander Wenckus + * + */ +public class IBBExtensions { + + public static final String NAMESPACE = "http://jabber.org/protocol/ibb"; + + private abstract static class IBB extends IQ { + final String sid; + + private IBB(final String sid) { + this.sid = sid; + } + + /** + * Returns the unique stream ID for this file transfer. + * + * @return Returns the unique stream ID for this file transfer. + */ + public String getSessionID() { + return sid; + } + + public String getNamespace() { + return NAMESPACE; + } + } + + /** + * Represents a request to open the file transfer. + * + * @author Alexander Wenckus + * + */ + public static class Open extends IBB { + + public static final String ELEMENT_NAME = "open"; + + private final int blockSize; + + /** + * Constructs an open packet. + * + * @param sid + * The streamID of the file transfer. + * @param blockSize + * The block size of the file transfer. + */ + public Open(final String sid, final int blockSize) { + super(sid); + this.blockSize = blockSize; + } + + /** + * The size blocks in which the data will be sent. + * + * @return The size blocks in which the data will be sent. + */ + public int getBlockSize() { + return blockSize; + } + + public String getElementName() { + return ELEMENT_NAME; + } + + public String getChildElementXML() { + StringBuffer buf = new StringBuffer(); + buf.append("<").append(getElementName()).append(" xmlns=\"").append(getNamespace()).append("\" "); + buf.append("sid=\"").append(getSessionID()).append("\" "); + buf.append("block-size=\"").append(getBlockSize()).append("\""); + buf.append("/>"); + return buf.toString(); + } + } + + /** + * A data packet containing a portion of the file being sent encoded in + * base64. + * + * @author Alexander Wenckus + * + */ + public static class Data implements PacketExtension { + + private long seq; + + private String data; + + public static final String ELEMENT_NAME = "data"; + + final String sid; + + /** + * Returns the unique stream ID identifying this file transfer. + * + * @return Returns the unique stream ID identifying this file transfer. + */ + public String getSessionID() { + return sid; + } + + public String getNamespace() { + return NAMESPACE; + } + + /** + * A constructor. + * + * @param sid + * The stream ID. + */ + public Data(final String sid) { + this.sid = sid; + } + + public Data(final String sid, final long seq, final String data) { + this(sid); + this.seq = seq; + this.data = data; + } + + public String getElementName() { + return ELEMENT_NAME; + } + + /** + * Returns the data contained in this packet. + * + * @return Returns the data contained in this packet. + */ + public String getData() { + return data; + } + + /** + * Sets the data contained in this packet. + * + * @param data + * The data encoded in base65 + */ + public void setData(final String data) { + this.data = data; + } + + /** + * Returns the sequence of this packet in regard to the other data + * packets. + * + * @return Returns the sequence of this packet in regard to the other + * data packets. + */ + public long getSeq() { + return seq; + } + + /** + * Sets the sequence of this packet. + * + * @param seq + * A number between 0 and 65535 + */ + public void setSeq(final long seq) { + this.seq = seq; + } + + public String toXML() { + StringBuffer buf = new StringBuffer(); + buf.append("<").append(getElementName()).append(" xmlns=\"").append(getNamespace()) + .append("\" "); + buf.append("sid=\"").append(getSessionID()).append("\" "); + buf.append("seq=\"").append(getSeq()).append("\""); + buf.append(">"); + buf.append(getData()); + buf.append(""); + return buf.toString(); + } + } + + /** + * Represents the closing of the file transfer. + * + * + * @author Alexander Wenckus + * + */ + public static class Close extends IBB { + public static final String ELEMENT_NAME = "close"; + + /** + * The constructor. + * + * @param sid + * The unique stream ID identifying this file transfer. + */ + public Close(String sid) { + super(sid); + } + + public String getElementName() { + return ELEMENT_NAME; + } + + public String getChildElementXML() { + StringBuffer buf = new StringBuffer(); + buf.append("<").append(getElementName()).append(" xmlns=\"").append(getNamespace()).append("\" "); + buf.append("sid=\"").append(getSessionID()).append("\""); + buf.append("/>"); + return buf.toString(); + } + + } +} diff --git a/source/org/jivesoftware/smackx/packet/StreamInitiation.java b/source/org/jivesoftware/smackx/packet/StreamInitiation.java new file mode 100644 index 000000000..922ed224f --- /dev/null +++ b/source/org/jivesoftware/smackx/packet/StreamInitiation.java @@ -0,0 +1,403 @@ +/* + * Created on Jun 16, 2005 + */ +package org.jivesoftware.smackx.packet; + +import org.jivesoftware.smack.packet.IQ; +import org.jivesoftware.smack.packet.PacketExtension; +import org.jivesoftware.smack.util.StringUtils; + +import java.util.Date; + +/** + * The process by which two entities initiate a stream. + * + * @author Alexander Wenckus + */ +public class StreamInitiation extends IQ { + + private String id; + + private String mimeType; + + private File file; + + private Feature featureNegotiation; + + /** + * The "id" attribute is an opaque identifier. This attribute MUST be + * present on type='set', and MUST be a valid string. This SHOULD NOT be + * sent back on type='result', since the "id" attribute provides the + * only context needed. This value is generated by the Sender, and the same + * value MUST be used throughout a session when talking to the Receiver. + * + * @param id The "id" attribute. + */ + public void setSesssionID(final String id) { + this.id = id; + } + + /** + * Uniquely identifies a stream initiation to the recipient. + * + * @return The "id" attribute. + * @see #setSesssionID(String) + */ + public String getSessionID() { + return id; + } + + /** + * The "mime-type" attribute identifies the MIME-type for the data across + * the stream. This attribute MUST be a valid MIME-type as registered with + * the Internet Assigned Numbers Authority (IANA) [3] (specifically, as + * listed at ). During + * negotiation, this attribute SHOULD be present, and is otherwise not + * required. If not included during negotiation, its value is assumed to be + * "binary/octect-stream". + * + * @param mimeType The valid mime-type. + */ + public void setMimeType(final String mimeType) { + this.mimeType = mimeType; + } + + /** + * Identifies the type of file that is desired to be transfered. + * + * @return The mime-type. + * @see #setMimeType(String) + */ + public String getMimeType() { + return mimeType; + } + + /** + * Sets the file which contains the information pertaining to the file to be + * transfered. + * + * @param file The file identified by the stream initiator to be sent. + */ + public void setFile(final File file) { + this.file = file; + } + + /** + * Returns the file containing the information about the request. + * + * @return Returns the file containing the information about the request. + */ + public File getFile() { + return file; + } + + /** + * Sets the data form which contains the valid methods of stream neotiation + * and transfer. + * + * @param form The dataform containing the methods. + */ + public void setFeatureNegotiationForm(final DataForm form) { + this.featureNegotiation = new Feature(form); + } + + /** + * Returns the data form which contains the valid methods of stream + * neotiation and transfer. + * + * @return Returns the data form which contains the valid methods of stream + * neotiation and transfer. + */ + public DataForm getFeatureNegotiationForm() { + return featureNegotiation.getData(); + } + + /* + * (non-Javadoc) + * + * @see org.jivesoftware.smack.packet.IQ#getChildElementXML() + */ + public String getChildElementXML() { + StringBuffer buf = new StringBuffer(); + if (this.getType().equals(IQ.Type.SET)) { + buf.append(""); + + // Add the file section if there is one. + String fileXML = file.toXML(); + if (fileXML != null) { + buf.append(fileXML); + } + } + else if (this.getType().equals(IQ.Type.RESULT)) { + buf.append(""); + } + else { + throw new IllegalArgumentException("IQ Type not understood"); + } + if (featureNegotiation != null) { + buf.append(featureNegotiation.toXML()); + } + buf.append(""); + return buf.toString(); + } + + /** + *
    + *
  • size: The size, in bytes, of the data to be sent.
  • + *
  • name: The name of the file that the Sender wishes to send.
  • + *
  • date: The last modification time of the file. This is specified + * using the DateTime profile as described in Jabber Date and Time Profiles.
  • + *
  • hash: The MD5 sum of the file contents.
  • + *
+ *

+ *

+ * <desc> is used to provide a sender-generated description of the + * file so the receiver can better understand what is being sent. It MUST + * NOT be sent in the result. + *

+ *

+ * When <range> is sent in the offer, it should have no attributes. + * This signifies that the sender can do ranged transfers. When a Stream + * Initiation result is sent with the element, it uses these + * attributes: + *

+ *

    + *
  • offset: Specifies the position, in bytes, to start transferring the + * file data from. This defaults to zero (0) if not specified.
  • + *
  • length - Specifies the number of bytes to retrieve starting at + * offset. This defaults to the length of the file from offset to the end.
  • + *
+ *

+ *

+ * Both attributes are OPTIONAL on the <range> element. Sending no + * attributes is synonymous with not sending the <range> element. When + * no <range> element is sent in the Stream Initiation result, the + * Sender MUST send the complete file starting at offset 0. More generally, + * data is sent over the stream byte for byte starting at the offset + * position for the length specified. + * + * @author Alexander Wenckus + */ + public static class File implements PacketExtension { + + private final String name; + + private final long size; + + private String hash; + + private Date date; + + private String desc; + + private boolean isRanged; + + /** + * Constructor providing the name of the file and its size. + * + * @param name The name of the file. + * @param size The size of the file in bytes. + */ + public File(final String name, final long size) { + if (name == null) { + throw new NullPointerException("name cannot be null"); + } + + this.name = name; + this.size = size; + } + + /** + * Returns the file's name. + * + * @return Returns the file's name. + */ + public String getName() { + return name; + } + + /** + * Returns the file's size. + * + * @return Returns the file's size. + */ + public long getSize() { + return size; + } + + /** + * Sets the MD5 sum of the file's contents + * + * @param hash The MD5 sum of the file's contents. + */ + public void setHash(final String hash) { + this.hash = hash; + } + + /** + * Returns the MD5 sum of the file's contents + * + * @return Returns the MD5 sum of the file's contents + */ + public String getHash() { + return hash; + } + + /** + * Sets the date that the file was last modified. + * + * @param date The date that the file was last modified. + */ + public void setDate(Date date) { + this.date = date; + } + + /** + * Returns the date that the file was last modified. + * + * @return Returns the date that the file was last modified. + */ + public Date getDate() { + return date; + } + + /** + * Sets the description of the file. + * + * @param desc The description of the file so that the file reciever can + * know what file it is. + */ + public void setDesc(final String desc) { + this.desc = desc; + } + + /** + * Returns the description of the file. + * + * @return Returns the description of the file. + */ + public String getDesc() { + return desc; + } + + /** + * True if a range can be provided and false if it cannot. + * + * @param isRanged True if a range can be provided and false if it cannot. + */ + public void setRanged(final boolean isRanged) { + this.isRanged = isRanged; + } + + /** + * Returns whether or not the initiator can support a range for the file + * tranfer. + * + * @return Returns whether or not the initiator can support a range for + * the file tranfer. + */ + public boolean isRanged() { + return isRanged; + } + + public String getElementName() { + return "file"; + } + + public String getNamespace() { + return "http://jabber.org/protocol/si/profile/file-transfer"; + } + + public String toXML() { + StringBuffer buffer = new StringBuffer(); + + buffer.append("<").append(getElementName()).append(" xmlns=\"") + .append(getNamespace()).append("\" "); + + if (getName() != null) { + buffer.append("name=\"").append(getName()).append("\" "); + } + + if (getSize() > 0) { + buffer.append("size=\"").append(getSize()).append("\" "); + } + + if (getDate() != null) { + buffer.append("date=\"").append(DelayInformation.UTC_FORMAT.format(date)).append("\" "); + } + + if (getHash() != null) { + buffer.append("hash=\"").append(getHash()).append("\" "); + } + + if ((desc != null && desc.length() > 0) || isRanged) { + buffer.append(">"); + if (getDesc() != null && desc.length() > 0) { + buffer.append("").append(StringUtils.escapeForXML(getDesc())).append(""); + } + if (isRanged()) { + buffer.append(""); + } + buffer.append(""); + } + else { + buffer.append("/>"); + } + return buffer.toString(); + } + } + + /** + * The feature negotiation portion of the StreamInitiation packet. + * + * @author Alexander Wenckus + * + */ + public class Feature implements PacketExtension { + + private final DataForm data; + + /** + * The dataform can be provided as part of the constructor. + * + * @param data The dataform. + */ + public Feature(final DataForm data) { + this.data = data; + } + + /** + * Returns the dataform associated with the feature negotiation. + * + * @return Returns the dataform associated with the feature negotiation. + */ + public DataForm getData() { + return data; + } + + public String getNamespace() { + return "http://jabber.org/protocol/feature-neg"; + } + + public String getElementName() { + return "feature"; + } + + public String toXML() { + StringBuffer buf = new StringBuffer(); + buf + .append(""); + buf.append(data.toXML()); + buf.append(""); + return buf.toString(); + } + } +} diff --git a/source/org/jivesoftware/smackx/provider/BytestreamsProvider.java b/source/org/jivesoftware/smackx/provider/BytestreamsProvider.java new file mode 100644 index 000000000..062817a48 --- /dev/null +++ b/source/org/jivesoftware/smackx/provider/BytestreamsProvider.java @@ -0,0 +1,105 @@ +/* + * Copyright [2005] [Mindbridge.com, Inc.] + * + * 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. + * + */ + +/* + * Created on Jun 21, 2005 + */ +package org.jivesoftware.smackx.provider; + +import org.jivesoftware.smack.packet.IQ; +import org.jivesoftware.smack.provider.IQProvider; +import org.jivesoftware.smackx.packet.Bytestream; +import org.xmlpull.v1.XmlPullParser; + +/** + * Parses a bytestream packet. + * + * @author Alexander Wenckus + */ +public class BytestreamsProvider implements IQProvider { + + /* + * (non-Javadoc) + * + * @see org.jivesoftware.smack.provider.IQProvider#parseIQ(org.xmlpull.v1.XmlPullParser) + */ + /* + * (non-Javadoc) + * + * @see org.jivesoftware.smack.provider.IQProvider#parseIQ(org.xmlpull.v1.XmlPullParser) + */ + /* + * (non-Javadoc) + * + * @see org.jivesoftware.smack.provider.IQProvider#parseIQ(org.xmlpull.v1.XmlPullParser) + */ + public IQ parseIQ(XmlPullParser parser) throws Exception { + // StringBuffer buf = new StringBuffer(); + boolean done = false; + + Bytestream toReturn = new Bytestream(); + + String id = parser.getAttributeValue("", "sid"); + String mode = parser.getAttributeValue("", "mode"); + + // streamhost + String JID = null; + String host = null; + String port = null; + + int eventType; + String elementName; + // String namespace; + while (!done) { + eventType = parser.next(); + elementName = parser.getName(); + // namespace = parser.getNamespace(); + if (eventType == XmlPullParser.START_TAG) { + if (elementName.equals(Bytestream.StreamHost.ELEMENTNAME)) { + JID = parser.getAttributeValue("", "jid"); + host = parser.getAttributeValue("", "host"); + port = parser.getAttributeValue("", "port"); + } else if (elementName + .equals(Bytestream.StreamHostUsed.ELEMENTNAME)) { + toReturn.setUsedHost(parser.getAttributeValue("", "jid")); + } else if (elementName.equals(Bytestream.Activate.ELEMENTNAME)) { + toReturn.setToActivate(parser.getAttributeValue("", "jid")); + } + } else if (eventType == XmlPullParser.END_TAG) { + if (elementName.equals("streamhost")) { + if (port == null) { + toReturn.addStreamHost(JID, host); + } else { + toReturn.addStreamHost(JID, host, Integer + .parseInt(port)); + } + JID = null; + host = null; + port = null; + } else if (elementName.equals("query")) { + done = true; + } + } + } + + toReturn.setMode((mode == "udp" ? Bytestream.Mode.UDP + : Bytestream.Mode.TCP)); + toReturn.setSessionID(id); + return toReturn; + } + +} diff --git a/source/org/jivesoftware/smackx/provider/IBBProviders.java b/source/org/jivesoftware/smackx/provider/IBBProviders.java new file mode 100644 index 000000000..e0c3204d1 --- /dev/null +++ b/source/org/jivesoftware/smackx/provider/IBBProviders.java @@ -0,0 +1,69 @@ +/* + * Created on Jul 5, 2005 + */ +package org.jivesoftware.smackx.provider; + +import org.jivesoftware.smack.packet.IQ; +import org.jivesoftware.smack.packet.PacketExtension; +import org.jivesoftware.smack.provider.IQProvider; +import org.jivesoftware.smack.provider.PacketExtensionProvider; +import org.jivesoftware.smackx.packet.IBBExtensions; +import org.xmlpull.v1.XmlPullParser; + +/** + * + * Parses an IBB packet. + * + * @author Alexander Wenckus + */ +public class IBBProviders { + + /** + * Parses an open IBB packet. + * + * @author Alexander Wenckus + * + */ + public static class Open implements IQProvider { + public IQ parseIQ(XmlPullParser parser) throws Exception { + final String sid = parser.getAttributeValue("", "sid"); + final int blockSize = Integer.parseInt(parser.getAttributeValue("", + "block-size")); + + return new IBBExtensions.Open(sid, blockSize); + } + } + + /** + * Parses a data IBB packet. + * + * @author Alexander Wenckus + * + */ + public static class Data implements PacketExtensionProvider { + public PacketExtension parseExtension(XmlPullParser parser) + throws Exception { + final String sid = parser.getAttributeValue("", "sid"); + final long seq = Long + .parseLong(parser.getAttributeValue("", "seq")); + final String data = parser.nextText(); + + return new IBBExtensions.Data(sid, seq, data); + } + } + + /** + * Parses a close IBB packet. + * + * @author Alexander Wenckus + * + */ + public static class Close implements IQProvider { + public IQ parseIQ(XmlPullParser parser) throws Exception { + final String sid = parser.getAttributeValue("", "sid"); + + return new IBBExtensions.Close(sid); + } + } + +} diff --git a/source/org/jivesoftware/smackx/provider/StreamInitiationProvider.java b/source/org/jivesoftware/smackx/provider/StreamInitiationProvider.java new file mode 100644 index 000000000..8d5a55715 --- /dev/null +++ b/source/org/jivesoftware/smackx/provider/StreamInitiationProvider.java @@ -0,0 +1,88 @@ +/* + * Created on Jun 16, 2005 + */ +package org.jivesoftware.smackx.provider; + +import org.jivesoftware.smack.packet.IQ; +import org.jivesoftware.smack.provider.IQProvider; +import org.jivesoftware.smackx.packet.DataForm; +import org.jivesoftware.smackx.packet.DelayInformation; +import org.jivesoftware.smackx.packet.StreamInitiation; +import org.jivesoftware.smackx.packet.StreamInitiation.File; +import org.jivesoftware.smackx.provider.DataFormProvider; +import org.xmlpull.v1.XmlPullParser; + +/** + * The StreamInitiationProvider parses StreamInitiation packets. + * + * @author Alexander Wenckus + * + */ +public class StreamInitiationProvider implements IQProvider { + + public IQ parseIQ(final XmlPullParser parser) throws Exception { + boolean done = false; + + // si + String id = parser.getAttributeValue("", "id"); + String mimeType = parser.getAttributeValue("", "mime-type"); + + StreamInitiation initiation = new StreamInitiation(); + + // file + String name = null; + String size = null; + String hash = null; + String date = null; + String desc = null; + boolean isRanged = false; + + // feature + DataForm form = null; + DataFormProvider dataFormProvider = new DataFormProvider(); + + int eventType; + String elementName; + String namespace; + while (!done) { + eventType = parser.next(); + elementName = parser.getName(); + namespace = parser.getNamespace(); + if (eventType == XmlPullParser.START_TAG) { + if (elementName.equals("file")) { + name = parser.getAttributeValue("", "name"); + size = parser.getAttributeValue("", "size"); + hash = parser.getAttributeValue("", "hash"); + date = parser.getAttributeValue("", "date"); + } else if (elementName.equals("desc")) { + desc = parser.nextText(); + } else if (elementName.equals("range")) { + isRanged = true; + } else if (elementName.equals("x") + && namespace.equals("jabber:x:data")) { + form = (DataForm) dataFormProvider.parseExtension(parser); + } + } else if (eventType == XmlPullParser.END_TAG) { + if (elementName.equals("si")) { + done = true; + } else if (elementName.equals("file")) { + File file = new File(name, Long.parseLong(size)); + file.setHash(hash); + if (date != null) + file.setDate(DelayInformation.UTC_FORMAT.parse(date)); + file.setDesc(desc); + file.setRanged(isRanged); + initiation.setFile(file); + } + } + } + + initiation.setSesssionID(id); + initiation.setMimeType(mimeType); + + initiation.setFeatureNegotiationForm(form); + + return initiation; + } + +}