/* * Copyright (c) 2008-2009, Motorola, Inc. * * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * - Redistributions of source code must retain the above copyright notice, * this list of conditions and the following disclaimer. * * - Redistributions in binary form must reproduce the above copyright notice, * this list of conditions and the following disclaimer in the documentation * and/or other materials provided with the distribution. * * - Neither the name of the Motorola, Inc. nor the names of its contributors * may be used to endorse or promote products derived from this software * without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. */ package javax.obex; import android.security.Md5MessageDigest; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.UnsupportedEncodingException; import java.util.Calendar; import java.util.Date; import java.util.TimeZone; /** * This class defines a set of helper methods for the implementation of Obex. * * @hide */ public final class ObexHelper { /** Prevent object construction of helper class */ private ObexHelper() {} /** * Defines the OBEX CONTINUE response code. *
* The value of OBEX_HTTP_CONTINUE
is 0x90 (144).
*/
public static final int OBEX_HTTP_CONTINUE = 0x90;
/**
* The maximum packet size for OBEX packets that this client can handle.
* At present, this must be changed for each port.
*
* OPTIMIZATION: The max packet size should be the Max incoming MTU minus
* OPTIMIZATION: L2CAP package headers and RFCOMM package headers.
*
* OPTIMIZATION: Retrieve the max incoming MTU from
* OPTIMIZATION: LocalDevice.getProperty().
*/
/** android note
* set as 0xFFFE to match remote MPS
*/
public static final int MAX_PACKET_SIZE_INT = 0xFFFE;
/**
* Updates the HeaderSet with the headers received in the byte array
* provided. Invalid headers are ignored.
*
* The first two bits of an OBEX Header specifies the type of object that * is being sent. The table below specifies the meaning of the high * bits. *
Bits 8 and 7 | Value | Description |
---|---|---|
00 | 0x00 | Null Terminated Unicode text, prefixed * with 2 byte unsigned integer |
01 | 0x40 | Byte Sequence, length prefixed with * 2 byte unsigned integer |
10 | 0x80 | 1 byte quantity |
11 | 0xC0 | 4 byte quantity - transmitted in * network byte order (high byte first |
true
if the header should be set to
* null
once it is added to the array or false
* if it should not be nulled out
*
* @return the header of an OBEX packet
*/
public static byte[] createHeader(HeaderSet head, boolean nullOut) {
Long intHeader = null;
String stringHeader = null;
Calendar dateHeader = null;
Byte byteHeader = null;
StringBuffer buffer = null;
byte[] value = null;
byte[] result = null;
byte[] lengthArray = new byte[2];
int length;
HeaderSet headImpl = null;
ByteArrayOutputStream out = new ByteArrayOutputStream();
if (!(head instanceof HeaderSet)) {
throw new IllegalArgumentException("Header not created by createHeaderSet");
}
headImpl = head;
try {
/*
* Determine if there is a connection ID to send. If there is,
* then it should be the first header in the packet.
*/
if ((headImpl.connectionID != null) && (headImpl.getHeader(HeaderSet.TARGET) == null)) {
out.write((byte)0xCB);
out.write(headImpl.connectionID);
}
// Count Header
intHeader = (Long)headImpl.getHeader(HeaderSet.COUNT);
if (intHeader != null) {
out.write((byte)HeaderSet.COUNT);
value = ObexHelper.convertToByteArray(intHeader.longValue());
out.write(value);
if (nullOut) {
headImpl.setHeader(HeaderSet.COUNT, null);
}
}
// Name Header
stringHeader = (String)headImpl.getHeader(HeaderSet.NAME);
if (stringHeader != null) {
out.write((byte)HeaderSet.NAME);
value = ObexHelper.convertToUnicodeByteArray(stringHeader);
length = value.length + 3;
lengthArray[0] = (byte)(255 & (length >> 8));
lengthArray[1] = (byte)(255 & length);
out.write(lengthArray);
out.write(value);
if (nullOut) {
headImpl.setHeader(HeaderSet.NAME, null);
}
}
// Type Header
stringHeader = (String)headImpl.getHeader(HeaderSet.TYPE);
if (stringHeader != null) {
out.write((byte)HeaderSet.TYPE);
try {
value = stringHeader.getBytes("ISO8859_1");
} catch (UnsupportedEncodingException e) {
throw new RuntimeException("Unsupported Encoding Scheme: " + e.getMessage());
}
length = value.length + 4;
lengthArray[0] = (byte)(255 & (length >> 8));
lengthArray[1] = (byte)(255 & length);
out.write(lengthArray);
out.write(value);
out.write(0x00);
if (nullOut) {
headImpl.setHeader(HeaderSet.TYPE, null);
}
}
// Length Header
intHeader = (Long)headImpl.getHeader(HeaderSet.LENGTH);
if (intHeader != null) {
out.write((byte)HeaderSet.LENGTH);
value = ObexHelper.convertToByteArray(intHeader.longValue());
out.write(value);
if (nullOut) {
headImpl.setHeader(HeaderSet.LENGTH, null);
}
}
// Time ISO Header
dateHeader = (Calendar)headImpl.getHeader(HeaderSet.TIME_ISO_8601);
if (dateHeader != null) {
/*
* The ISO Header should take the form YYYYMMDDTHHMMSSZ. The
* 'Z' will only be included if it is a UTC time.
*/
buffer = new StringBuffer();
int temp = dateHeader.get(Calendar.YEAR);
for (int i = temp; i < 1000; i = i * 10) {
buffer.append("0");
}
buffer.append(temp);
temp = dateHeader.get(Calendar.MONTH);
if (temp < 10) {
buffer.append("0");
}
buffer.append(temp);
temp = dateHeader.get(Calendar.DAY_OF_MONTH);
if (temp < 10) {
buffer.append("0");
}
buffer.append(temp);
buffer.append("T");
temp = dateHeader.get(Calendar.HOUR_OF_DAY);
if (temp < 10) {
buffer.append("0");
}
buffer.append(temp);
temp = dateHeader.get(Calendar.MINUTE);
if (temp < 10) {
buffer.append("0");
}
buffer.append(temp);
temp = dateHeader.get(Calendar.SECOND);
if (temp < 10) {
buffer.append("0");
}
buffer.append(temp);
if (dateHeader.getTimeZone().getID().equals("UTC")) {
buffer.append("Z");
}
try {
value = buffer.toString().getBytes("ISO8859_1");
} catch (UnsupportedEncodingException e) {
throw new RuntimeException("UnsupportedEncodingException: " + e.getMessage());
}
length = value.length + 3;
lengthArray[0] = (byte)(255 & (length >> 8));
lengthArray[1] = (byte)(255 & length);
out.write(HeaderSet.TIME_ISO_8601);
out.write(lengthArray);
out.write(value);
if (nullOut) {
headImpl.setHeader(HeaderSet.TIME_ISO_8601, null);
}
}
// Time 4 Byte Header
dateHeader = (Calendar)headImpl.getHeader(HeaderSet.TIME_4_BYTE);
if (dateHeader != null) {
out.write(HeaderSet.TIME_4_BYTE);
/*
* Need to call getTime() twice. The first call will return
* a java.util.Date object. The second call returns the number
* of milliseconds since January 1, 1970. We need to convert
* it to seconds since the TIME_4_BYTE expects the number of
* seconds since January 1, 1970.
*/
value = ObexHelper.convertToByteArray(dateHeader.getTime().getTime() / 1000L);
out.write(value);
if (nullOut) {
headImpl.setHeader(HeaderSet.TIME_4_BYTE, null);
}
}
// Description Header
stringHeader = (String)headImpl.getHeader(HeaderSet.DESCRIPTION);
if (stringHeader != null) {
out.write((byte)HeaderSet.DESCRIPTION);
value = ObexHelper.convertToUnicodeByteArray(stringHeader);
length = value.length + 3;
lengthArray[0] = (byte)(255 & (length >> 8));
lengthArray[1] = (byte)(255 & length);
out.write(lengthArray);
out.write(value);
if (nullOut) {
headImpl.setHeader(HeaderSet.DESCRIPTION, null);
}
}
// Target Header
value = (byte[])headImpl.getHeader(HeaderSet.TARGET);
if (value != null) {
out.write((byte)HeaderSet.TARGET);
length = value.length + 3;
lengthArray[0] = (byte)(255 & (length >> 8));
lengthArray[1] = (byte)(255 & length);
out.write(lengthArray);
out.write(value);
if (nullOut) {
headImpl.setHeader(HeaderSet.TARGET, null);
}
}
// HTTP Header
value = (byte[])headImpl.getHeader(HeaderSet.HTTP);
if (value != null) {
out.write((byte)HeaderSet.HTTP);
length = value.length + 3;
lengthArray[0] = (byte)(255 & (length >> 8));
lengthArray[1] = (byte)(255 & length);
out.write(lengthArray);
out.write(value);
if (nullOut) {
headImpl.setHeader(HeaderSet.HTTP, null);
}
}
// Who Header
value = (byte[])headImpl.getHeader(HeaderSet.WHO);
if (value != null) {
out.write((byte)HeaderSet.WHO);
length = value.length + 3;
lengthArray[0] = (byte)(255 & (length >> 8));
lengthArray[1] = (byte)(255 & length);
out.write(lengthArray);
out.write(value);
if (nullOut) {
headImpl.setHeader(HeaderSet.WHO, null);
}
}
// Connection ID Header
value = (byte[])headImpl.getHeader(HeaderSet.APPLICATION_PARAMETER);
if (value != null) {
out.write((byte)HeaderSet.APPLICATION_PARAMETER);
length = value.length + 3;
lengthArray[0] = (byte)(255 & (length >> 8));
lengthArray[1] = (byte)(255 & length);
out.write(lengthArray);
out.write(value);
if (nullOut) {
headImpl.setHeader(HeaderSet.APPLICATION_PARAMETER, null);
}
}
// Object Class Header
value = (byte[])headImpl.getHeader(HeaderSet.OBJECT_CLASS);
if (value != null) {
out.write((byte)HeaderSet.OBJECT_CLASS);
length = value.length + 3;
lengthArray[0] = (byte)(255 & (length >> 8));
lengthArray[1] = (byte)(255 & length);
out.write(lengthArray);
out.write(value);
if (nullOut) {
headImpl.setHeader(HeaderSet.OBJECT_CLASS, null);
}
}
// Check User Defined Headers
for (int i = 0; i < 16; i++) {
//Unicode String Header
stringHeader = (String)headImpl.getHeader(i + 0x30);
if (stringHeader != null) {
out.write((byte)i + 0x30);
value = ObexHelper.convertToUnicodeByteArray(stringHeader);
length = value.length + 3;
lengthArray[0] = (byte)(255 & (length >> 8));
lengthArray[1] = (byte)(255 & length);
out.write(lengthArray);
out.write(value);
if (nullOut) {
headImpl.setHeader(i + 0x30, null);
}
}
// Byte Sequence Header
value = (byte[])headImpl.getHeader(i + 0x70);
if (value != null) {
out.write((byte)i + 0x70);
length = value.length + 3;
lengthArray[0] = (byte)(255 & (length >> 8));
lengthArray[1] = (byte)(255 & length);
out.write(lengthArray);
out.write(value);
if (nullOut) {
headImpl.setHeader(i + 0x70, null);
}
}
// Byte Header
byteHeader = (Byte)headImpl.getHeader(i + 0xB0);
if (byteHeader != null) {
out.write((byte)i + 0xB0);
out.write(byteHeader.byteValue());
if (nullOut) {
headImpl.setHeader(i + 0xB0, null);
}
}
// Integer header
intHeader = (Long)headImpl.getHeader(i + 0xF0);
if (intHeader != null) {
out.write((byte)i + 0xF0);
out.write(ObexHelper.convertToByteArray(intHeader.longValue()));
if (nullOut) {
headImpl.setHeader(i + 0xF0, null);
}
}
}
// Add the authentication challenge header
if (headImpl.authChall != null) {
out.write((byte)0x4D);
length = headImpl.authChall.length + 3;
lengthArray[0] = (byte)(255 & (length >> 8));
lengthArray[1] = (byte)(255 & length);
out.write(lengthArray);
out.write(headImpl.authChall);
if (nullOut) {
headImpl.authChall = null;
}
}
// Add the authentication response header
if (headImpl.authResp != null) {
out.write((byte)0x4E);
length = headImpl.authResp.length + 3;
lengthArray[0] = (byte)(255 & (length >> 8));
lengthArray[1] = (byte)(255 & length);
out.write(lengthArray);
out.write(headImpl.authResp);
if (nullOut) {
headImpl.authResp = null;
}
}
} catch (IOException e) {
} finally {
result = out.toByteArray();
try {
out.close();
} catch (Exception ex) {
}
}
return result;
}
/**
* Determines where the maximum divide is between headers. This method is
* used by put and get operations to separate headers to a size that meets
* the max packet size allowed.
*
* @param headerArray the headers to separate
*
* @param start the starting index to search
*
* @param maxSize the maximum size of a packet
*
* @return the index of the end of the header block to send or -1 if the
* header could not be divided because the header is too large
*/
public static int findHeaderEnd(byte[] headerArray, int start, int maxSize) {
int fullLength = 0;
int lastLength = -1;
int index = start;
int length = 0;
while ((fullLength < maxSize) && (index < headerArray.length)) {
int headerID = (headerArray[index] < 0 ? headerArray[index] + 256 : headerArray[index]);
lastLength = fullLength;
switch (headerID & (0xC0)) {
case 0x00:
// Fall through
case 0x40:
index++;
length = (headerArray[index] < 0 ? headerArray[index] + 256
: headerArray[index]);
length = length << 8;
index++;
length += (headerArray[index] < 0 ? headerArray[index] + 256
: headerArray[index]);
length -= 3;
index++;
index += length;
fullLength += length + 3;
break;
case 0x80:
index++;
index++;
fullLength += 2;
break;
case 0xC0:
index += 5;
fullLength += 5;
break;
}
}
/*
* Determine if this is the last header or not
*/
if (lastLength == 0) {
/*
* Since this is the last header, check to see if the size of this
* header is less then maxSize. If it is, return the length of the
* header, otherwise return -1. The length of the header is
* returned since it would be the start of the next header
*/
if (fullLength < maxSize) {
return headerArray.length;
} else {
return -1;
}
} else {
return lastLength + start;
}
}
/**
* Converts the byte array to a long.
*
* @param b the byte array to convert to a long
*
* @return the byte array as a long
*/
public static long convertToLong(byte[] b) {
long result = 0;
long value = 0;
long power = 0;
for (int i = (b.length - 1); i >= 0; i--) {
value = b[i];
if (value < 0) {
value += 256;
}
result = result | (value << power);
power += 8;
}
return result;
}
/**
* Converts the long to a 4 byte array. The long must be non negative.
*
* @param l the long to convert
*
* @return a byte array that is the same as the long
*/
public static byte[] convertToByteArray(long l) {
byte[] b = new byte[4];
b[0] = (byte)(255 & (l >> 24));
b[1] = (byte)(255 & (l >> 16));
b[2] = (byte)(255 & (l >> 8));
b[3] = (byte)(255 & l);
return b;
}
/**
* Converts the String to a UNICODE byte array. It will also add the ending
* null characters to the end of the string.
*
* @param s the string to convert
*
* @return the unicode byte array of the string
*/
public static byte[] convertToUnicodeByteArray(String s) {
if (s == null) {
return null;
}
char c[] = s.toCharArray();
byte[] result = new byte[(c.length * 2) + 2];
for (int i = 0; i < c.length; i++) {
result[(i * 2)] = (byte)(c[i] >> 8);
result[((i * 2) + 1)] = (byte)c[i];
}
// Add the UNICODE null character
result[result.length - 2] = 0;
result[result.length - 1] = 0;
return result;
}
/**
* Retrieves the value from the byte array for the tag value specified. The
* array should be of the form Tag - Length - Value triplet.
*
* @param tag the tag to retrieve from the byte array
*
* @param triplet the byte sequence containing the tag length value form
*
* @return the value of the specified tag
*/
public static byte[] getTagValue(byte tag, byte[] triplet) {
int index = findTag(tag, triplet);
if (index == -1) {
return null;
}
index++;
int length = triplet[index] & 0xFF;
byte[] result = new byte[length];
index++;
System.arraycopy(triplet, index, result, 0, length);
return result;
}
/**
* Finds the index that starts the tag value pair in the byte array provide.
*
* @param tag the tag to look for
*
* @param value the byte array to search
*
* @return the starting index of the tag or -1 if the tag could not be found
*/
public static int findTag(byte tag, byte[] value) {
int length = 0;
if (value == null) {
return -1;
}
int index = 0;
while ((index < value.length) && (value[index] != tag)) {
length = value[index + 1] & 0xFF;
index += length + 2;
}
if (index >= value.length) {
return -1;
}
return index;
}
/**
* Converts the byte array provided to a unicode string.
*
* @param b the byte array to convert to a string
*
* @param includesNull determine if the byte string provided contains the
* UNICODE null character at the end or not; if it does, it will be
* removed
*
* @return a Unicode string
*
* @param IllegalArgumentException if the byte array has an odd length
*/
public static String convertToUnicode(byte[] b, boolean includesNull) {
if (b == null) {
return null;
}
int arrayLength = b.length;
if (!((arrayLength % 2) == 0)) {
throw new IllegalArgumentException("Byte array not of a valid form");
}
arrayLength = (arrayLength >> 1);
if (includesNull) {
arrayLength -= 1;
}
char[] c = new char[arrayLength];
for (int i = 0; i < arrayLength; i++) {
int upper = b[2 * i];
int lower = b[(2 * i) + 1];
if (upper < 0) {
upper += 256;
}
if (lower < 0) {
lower += 256;
}
c[i] = (char)((upper << 8) | lower);
}
return new String(c);
}
/**
* Compute the MD5 hash of the byte array provided.
* Does not accumulate input.
*
* @param in the byte array to hash
* @return the MD5 hash of the byte array
*/
public static byte[] computeMd5Hash(byte[] in) {
Md5MessageDigest md5 = new Md5MessageDigest();
return md5.digest(in);
}
/**
* Computes an authentication challenge header.
*
*
* @param nonce the challenge that will be provided to the peer; the
* challenge must be 16 bytes long
*
* @param realm a short description that describes what password to use
*
* @param access if true
then full access will be granted if
* successful; if false
then read only access will be granted
* if successful
*
* @param userID if true
, a user ID is required in the reply;
* if false
, no user ID is required
*
* @throws IllegalArgumentException if the challenge is not 16 bytes
* long; if the realm can not be encoded in less then 255 bytes
*
* @throws IOException if the encoding scheme ISO 8859-1 is not supported
*/
public static byte[] computeAuthenticationChallenge(byte[] nonce, String realm, boolean access,
boolean userID) throws IOException {
byte[] authChall = null;
if (nonce.length != 16) {
throw new IllegalArgumentException("Nonce must be 16 bytes long");
}
/*
* The authentication challenge is a byte sequence of the following form
* byte 0: 0x00 - the tag for the challenge
* byte 1: 0x10 - the length of the challenge; must be 16
* byte 2-17: the authentication challenge
* byte 18: 0x01 - the options tag; this is optional in the spec, but
* we are going to include it in every message
* byte 19: 0x01 - length of the options; must be 1
* byte 20: the value of the options; bit 0 is set if user ID is
* required; bit 1 is set if access mode is read only
* byte 21: 0x02 - the tag for authentication realm; only included if
* an authentication realm is specified
* byte 22: the length of the authentication realm; only included if
* the authentication realm is specified
* byte 23: the encoding scheme of the authentication realm; we will use
* the ISO 8859-1 encoding scheme since it is part of the KVM
* byte 24 & up: the realm if one is specified.
*/
if (realm == null) {
authChall = new byte[21];
} else {
if (realm.length() >= 255) {
throw new IllegalArgumentException("Realm must be less then 255 bytes");
}
authChall = new byte[24 + realm.length()];
authChall[21] = 0x02;
authChall[22] = (byte)(realm.length() + 1);
authChall[23] = 0x01; // ISO 8859-1 Encoding
System.arraycopy(realm.getBytes("ISO8859_1"), 0, authChall, 24, realm.length());
}
// Include the nonce field in the header
authChall[0] = 0x00;
authChall[1] = 0x10;
System.arraycopy(nonce, 0, authChall, 2, 16);
// Include the options header
authChall[18] = 0x01;
authChall[19] = 0x01;
authChall[20] = 0x00;
if (!access) {
authChall[20] = (byte)(authChall[20] | 0x02);
}
if (userID) {
authChall[20] = (byte)(authChall[20] | 0x01);
}
return authChall;
}
}