diff options
author | Rahul Sabnis <rahulsabnis@google.com> | 2022-02-24 00:53:16 +0000 |
---|---|---|
committer | Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com> | 2022-02-24 00:53:16 +0000 |
commit | 087f18d9922c2a5138d50766cda39bef2d5e4f38 (patch) | |
tree | 4c267b351ffc00bd6f94a85a89c2bdd83e15f7c0 | |
parent | e9f833ab7e4dea8d0f3f0364aa93b082049a4055 (diff) | |
parent | bff9287ebf3001ba7390df1999a846237db19019 (diff) | |
download | obex-087f18d9922c2a5138d50766cda39bef2d5e4f38.tar.gz |
Add obex library source code, build file, and OWNERS file am: 9af11bf90f am: bff9287ebf
Original change: https://android-review.googlesource.com/c/platform/external/obex/+/1995473
Change-Id: Ib2ad1eaa73ccf0a3bdf9e31c3f9828f6b4ddf7d7
-rw-r--r-- | Android.bp | 41 | ||||
-rw-r--r-- | OWNERS | 6 | ||||
-rw-r--r-- | javax/obex/ApplicationParameter.java | 187 | ||||
-rw-r--r-- | javax/obex/Authenticator.java | 114 | ||||
-rw-r--r-- | javax/obex/BaseStream.java | 75 | ||||
-rw-r--r-- | javax/obex/ClientOperation.java | 819 | ||||
-rw-r--r-- | javax/obex/ClientSession.java | 637 | ||||
-rw-r--r-- | javax/obex/HeaderSet.java | 728 | ||||
-rw-r--r-- | javax/obex/ObexHelper.java | 1100 | ||||
-rw-r--r-- | javax/obex/ObexPacket.java | 71 | ||||
-rw-r--r-- | javax/obex/ObexSession.java | 223 | ||||
-rw-r--r-- | javax/obex/ObexTransport.java | 112 | ||||
-rw-r--r-- | javax/obex/Operation.java | 167 | ||||
-rw-r--r-- | javax/obex/PasswordAuthentication.java | 82 | ||||
-rw-r--r-- | javax/obex/PrivateInputStream.java | 180 | ||||
-rw-r--r-- | javax/obex/PrivateOutputStream.java | 171 | ||||
-rw-r--r-- | javax/obex/ResponseCodes.java | 377 | ||||
-rw-r--r-- | javax/obex/ServerOperation.java | 868 | ||||
-rw-r--r-- | javax/obex/ServerRequestHandler.java | 290 | ||||
-rw-r--r-- | javax/obex/ServerSession.java | 750 | ||||
-rw-r--r-- | javax/obex/SessionNotifier.java | 127 |
21 files changed, 7125 insertions, 0 deletions
diff --git a/Android.bp b/Android.bp new file mode 100644 index 0000000..d0e5041 --- /dev/null +++ b/Android.bp @@ -0,0 +1,41 @@ +// OBEX library +package { + default_applicable_licenses: ["obex_license"], +} + +// Added automatically by a large-scale-change that took the approach of +// 'apply every license found to every target'. While this makes sure we respect +// every license restriction, it may not be entirely correct. +// +// e.g. GPL in an MIT project might only apply to the contrib/ directory. +// +// Please consider splitting the single license below into multiple licenses, +// taking care not to lose any license_kind information, and overriding the +// default license using the 'licenses: [...]' property on targets as needed. +// +// For unused files, consider creating a 'fileGroup' with "//visibility:private" +// to attach the license to, and including a comment whether the files may be +// used in the current project. +// See: http://go/android-license-faq +license { + name: "obex_license", + visibility: [":__subpackages__"], + license_kinds: [ + "SPDX-license-identifier-Apache-2.0", + "SPDX-license-identifier-BSD", + ], + license_text: [ + "LICENSE", + ], +} + +java_library { + name: "obex", + + srcs: ["**/*.java"], + apex_available: [ + "com.android.bluetooth", + ], + sdk_version: "module_current", + min_sdk_version: "Tiramisu", +} @@ -0,0 +1,6 @@ +# Owners for OBEX library + +rahulsabnis@google.com +sattiraju@google.com +siyuanh@google.com +zachoverflow@google.com diff --git a/javax/obex/ApplicationParameter.java b/javax/obex/ApplicationParameter.java new file mode 100644 index 0000000..c2f14c6 --- /dev/null +++ b/javax/obex/ApplicationParameter.java @@ -0,0 +1,187 @@ +/* + * 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; + +/** + * Represents an Application Parameter header for OBEX as defined by the IrDA specification. + */ +public final class ApplicationParameter { + + private byte[] mArray; + + private int mLength; + + private int mMaxLength = 1000; + + /** + * Possible values for the tag field in the Application Parameter header. + */ + public static class TRIPLET_TAGID { + public static final byte ORDER_TAGID = 0x01; + + public static final byte SEARCH_VALUE_TAGID = 0x02; + + public static final byte SEARCH_ATTRIBUTE_TAGID = 0x03; + + // if equals to "0", PSE only reply number of contacts + public static final byte MAXLISTCOUNT_TAGID = 0x04; + + public static final byte LISTSTARTOFFSET_TAGID = 0x05; + + public static final byte PROPERTY_SELECTOR_TAGID = 0x06; + + public static final byte FORMAT_TAGID = 0x07; + + // only used if max list count = 0 + public static final byte PHONEBOOKSIZE_TAGID = 0x08; + + // only used in "mch" in response + public static final byte NEWMISSEDCALLS_TAGID = 0x09; + + public static final byte SUPPORTEDFEATURE_TAGID = 0x10; + + public static final byte PRIMARYVERSIONCOUNTER_TAGID = 0x0A; + + public static final byte SECONDARYVERSIONCOUNTER_TAGID = 0x0B; + + public static final byte VCARDSELECTOR_TAGID = 0x0C; + + public static final byte DATABASEIDENTIFIER_TAGID = 0x0D; + + public static final byte VCARDSELECTOROPERATOR_TAGID = 0x0E; + + public static final byte RESET_NEW_MISSED_CALLS_TAGID = 0x0F; + } + + /** + * Possible values for the value field in the Application Parameter header. + */ + public static class TRIPLET_VALUE { + public static class ORDER { + public static final byte ORDER_BY_INDEX = 0x00; + + public static final byte ORDER_BY_ALPHANUMERIC = 0x01; + + public static final byte ORDER_BY_PHONETIC = 0x02; + } + + public static class SEARCHATTRIBUTE { + public static final byte SEARCH_BY_NAME = 0x00; + + public static final byte SEARCH_BY_NUMBER = 0x01; + + public static final byte SEARCH_BY_SOUND = 0x02; + } + + public static class FORMAT { + public static final byte VCARD_VERSION_21 = 0x00; + + public static final byte VCARD_VERSION_30 = 0x01; + } + } + + /** + * Possible values for the length field in the Application Parameter header. + */ + public static class TRIPLET_LENGTH { + public static final byte ORDER_LENGTH = 1; + + public static final byte SEARCH_ATTRIBUTE_LENGTH = 1; + + public static final byte MAXLISTCOUNT_LENGTH = 2; + + public static final byte LISTSTARTOFFSET_LENGTH = 2; + + public static final byte PROPERTY_SELECTOR_LENGTH = 8; + + public static final byte FORMAT_LENGTH = 1; + + public static final byte PHONEBOOKSIZE_LENGTH = 2; + + public static final byte NEWMISSEDCALLS_LENGTH = 1; + + public static final byte SUPPORTEDFEATURE_LENGTH = 4; + + public static final byte PRIMARYVERSIONCOUNTER_LENGTH = 16; + + public static final byte SECONDARYVERSIONCOUNTER_LENGTH = 16; + + public static final byte VCARDSELECTOR_LENGTH = 8; + + public static final byte DATABASEIDENTIFIER_LENGTH = 16; + + public static final byte VCARDSELECTOROPERATOR_LENGTH = 1; + + public static final byte RESETNEWMISSEDCALLS_LENGTH = 1; + } + + /** + * Constructs an ApplicationParameter header + */ + public ApplicationParameter() { + mArray = new byte[mMaxLength]; + mLength = 0; + } + + /** + * Adds a triplet of tag, length, and value to this application parameter header as per the + * IrDA specifications. + * + * @param tag one of {@link TRIPLET_TAGID} + * @param len one of {@link TRIPLET_LENGTH} + * @param value is the value required for the supplied tag + */ + public void addTriplet(byte tag, byte len, byte[] value) { + if ((mLength + len + 2) > mMaxLength) { + byte[] array_tmp = new byte[mLength + 4 * len]; + System.arraycopy(mArray, 0, array_tmp, 0, mLength); + mArray = array_tmp; + mMaxLength = mLength + 4 * len; + } + mArray[mLength++] = tag; + mArray[mLength++] = len; + System.arraycopy(value, 0, mArray, mLength, len); + mLength += len; + } + + /** + * Gets the application parameter header as a byte array. + * + * @return a byte array representing the application parameter header + */ + public byte[] getHeader() { + byte[] para = new byte[mLength]; + System.arraycopy(mArray, 0, para, 0, mLength); + return para; + } +} diff --git a/javax/obex/Authenticator.java b/javax/obex/Authenticator.java new file mode 100644 index 0000000..12ef47f --- /dev/null +++ b/javax/obex/Authenticator.java @@ -0,0 +1,114 @@ +/* + * 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; + +/** + * This interface provides a way to respond to authentication challenge and + * authentication response headers. When a client or server receives an + * authentication challenge or authentication response header, the + * <code>onAuthenticationChallenge()</code> or + * <code>onAuthenticationResponse()</code> will be called, respectively, by the + * implementation. + * <P> + * For more information on how the authentication procedure works in OBEX, + * please review the IrOBEX specification at <A + * HREF="http://www.irda.org">http://www.irda.org</A>. + * <P> + * <STRONG>Authentication Challenges</STRONG> + * <P> + * When a client or server receives an authentication challenge header, the + * <code>onAuthenticationChallenge()</code> method will be invoked by the OBEX + * API implementation. The application will then return the user name (if + * needed) and password via a <code>PasswordAuthentication</code> object. The + * password in this object is not sent in the authentication response. Instead, + * the 16-byte challenge received in the authentication challenge is combined + * with the password returned from the <code>onAuthenticationChallenge()</code> + * method and passed through the MD5 hash algorithm. The resulting value is sent + * in the authentication response along with the user name if it was provided. + * <P> + * <STRONG>Authentication Responses</STRONG> + * <P> + * When a client or server receives an authentication response header, the + * <code>onAuthenticationResponse()</code> method is invoked by the API + * implementation with the user name received in the authentication response + * header. (The user name will be <code>null</code> if no user name was provided + * in the authentication response header.) The application must determine the + * correct password. This value should be returned from the + * <code>onAuthenticationResponse()</code> method. If the authentication request + * should fail without the implementation checking the password, + * <code>null</code> should be returned by the application. (This is needed for + * reasons like not recognizing the user name, etc.) If the returned value is + * not <code>null</code>, the OBEX API implementation will combine the password + * returned from the <code>onAuthenticationResponse()</code> method and + * challenge sent via the authentication challenge, apply the MD5 hash + * algorithm, and compare the result to the response hash received in the + * authentication response header. If the values are not equal, an + * <code>IOException</code> will be thrown if the client requested + * authentication. If the server requested authentication, the + * <code>onAuthenticationFailure()</code> method will be called on the + * <code>ServerRequestHandler</code> that failed authentication. The connection + * is <B>not</B> closed if authentication failed. + */ +public interface Authenticator { + + /** + * Called when a client or a server receives an authentication challenge + * header. It should respond to the challenge with a + * <code>PasswordAuthentication</code> that contains the correct user name + * and password for the challenge. + * @param description the description of which user name and password should + * be used; if no description is provided in the authentication + * challenge or the description is encoded in an encoding scheme that + * is not supported, an empty string will be provided + * @param isUserIdRequired <code>true</code> if the user ID is required; + * <code>false</code> if the user ID is not required + * @param isFullAccess <code>true</code> if full access to the server will + * be granted; <code>false</code> if read only access will be granted + * @return a <code>PasswordAuthentication</code> object containing the user + * name and password used for authentication + */ + PasswordAuthentication onAuthenticationChallenge(String description, boolean isUserIdRequired, + boolean isFullAccess); + + /** + * Called when a client or server receives an authentication response + * header. This method will provide the user name and expect the correct + * password to be returned. + * @param userName the user name provided in the authentication response; may + * be <code>null</code> + * @return the correct password for the user name provided; if + * <code>null</code> is returned then the authentication request + * failed + */ + byte[] onAuthenticationResponse(byte[] userName); +} diff --git a/javax/obex/BaseStream.java b/javax/obex/BaseStream.java new file mode 100644 index 0000000..440e060 --- /dev/null +++ b/javax/obex/BaseStream.java @@ -0,0 +1,75 @@ +/* + * 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 java.io.IOException; + +/** + * This interface defines the methods needed by a parent that uses the + * PrivateInputStream and PrivateOutputStream objects defined in this package. + */ +public interface BaseStream { + + /** + * Verifies that this object is still open. + * @throws IOException if the object is closed + */ + void ensureOpen() throws IOException; + + /** + * Verifies that additional information may be sent. In other words, the + * operation is not done. + * @throws IOException if the operation is completed + */ + void ensureNotDone() throws IOException; + + /** + * Continues the operation since there is no data to read. + * @param sendEmpty <code>true</code> if the operation should send an empty + * packet or not send anything if there is no data to send + * @param inStream <code>true</code> if the stream is input stream or is + * output stream + * @return <code>true</code> if the operation was completed; + * <code>false</code> if no operation took place + * @throws IOException if an IO error occurs + */ + boolean continueOperation(boolean sendEmpty, boolean inStream) throws IOException; + + /** + * Called when the output or input stream is closed. + * @param inStream <code>true</code> if the input stream is closed; + * <code>false</code> if the output stream is closed + * @throws IOException if an IO error occurs + */ + void streamClosed(boolean inStream) throws IOException; +} diff --git a/javax/obex/ClientOperation.java b/javax/obex/ClientOperation.java new file mode 100644 index 0000000..d5387d1 --- /dev/null +++ b/javax/obex/ClientOperation.java @@ -0,0 +1,819 @@ +/* + * Copyright (c) 2015 The Android Open Source Project + * Copyright (C) 2015 Samsung LSI + * 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 java.io.ByteArrayOutputStream; +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +/** + * This class implements the {@link Operation} interface. It will read and write data via puts and + * gets. + */ +public final class ClientOperation implements Operation, BaseStream { + + private static final String TAG = "ClientOperation"; + + private static final boolean V = ObexHelper.VDBG; + + private ClientSession mParent; + + private boolean mInputOpen; + + private PrivateInputStream mPrivateInput; + + private boolean mPrivateInputOpen; + + private PrivateOutputStream mPrivateOutput; + + private boolean mPrivateOutputOpen; + + private String mExceptionMessage; + + private int mMaxPacketSize; + + private boolean mOperationDone; + + private boolean mGetOperation; + + private boolean mGetFinalFlag; + + private HeaderSet mRequestHeader; + + private HeaderSet mReplyHeader; + + private boolean mEndOfBodySent; + + private boolean mSendBodyHeader = true; + // A latch - when triggered, there is not way back ;-) + private boolean mSrmActive = false; + + // Assume SRM disabled - until support is confirmed + // by the server + private boolean mSrmEnabled = false; + // keep waiting until final-bit is received in request + // to handle the case where the SRM enable header is in + // a different OBEX packet than the SRMP header. + private boolean mSrmWaitingForRemote = true; + + + /** + * Creates new OperationImpl to read and write data to a server. + * + * @param maxSize the maximum packet size + * @param p the parent to this object + * @param type <code>true</code> if this is a get request; + * <code>false</code>. if this is a put request + * @param header the header to set in the initial request + * @throws IOException if an IO error occurred + */ + ClientOperation(int maxSize, ClientSession p, HeaderSet header, boolean type) + throws IOException { + + mParent = p; + mEndOfBodySent = false; + mInputOpen = true; + mOperationDone = false; + mMaxPacketSize = maxSize; + mGetOperation = type; + mGetFinalFlag = false; + + mPrivateInputOpen = false; + mPrivateOutputOpen = false; + mPrivateInput = null; + mPrivateOutput = null; + + mReplyHeader = new HeaderSet(); + + mRequestHeader = new HeaderSet(); + + int[] headerList = header.getHeaderList(); + + if (headerList != null) { + + for (int i = 0; i < headerList.length; i++) { + mRequestHeader.setHeader(headerList[i], header.getHeader(headerList[i])); + } + } + + if ((header).mAuthChall != null) { + mRequestHeader.mAuthChall = new byte[(header).mAuthChall.length]; + System.arraycopy((header).mAuthChall, 0, mRequestHeader.mAuthChall, 0, + (header).mAuthChall.length); + } + + if ((header).mAuthResp != null) { + mRequestHeader.mAuthResp = new byte[(header).mAuthResp.length]; + System.arraycopy((header).mAuthResp, 0, mRequestHeader.mAuthResp, 0, + (header).mAuthResp.length); + + } + + if ((header).mConnectionID != null) { + mRequestHeader.mConnectionID = new byte[4]; + System.arraycopy((header).mConnectionID, 0, mRequestHeader.mConnectionID, 0, + 4); + + } + } + + /** + * Allows setting the flag which will force GET to always be sent as single packet request with + * the final flag set. This is to improve compatibility with some profiles, i.e. PBAP which + * require requests to be sent this way. + * + * @param flag true if the final flag should be set in the packet, false if it should not + */ + public void setGetFinalFlag(boolean flag) { + mGetFinalFlag = flag; + } + + /** + * Sends an ABORT message to the server. By calling this method, the corresponding input and + * output streams will be closed along with this object. + * + * @throws IOException if the transaction has already ended or if an OBEX server called this + * method + */ + public synchronized void abort() throws IOException { + ensureOpen(); + //no compatible with sun-ri + if ((mOperationDone) && (mReplyHeader.responseCode != ResponseCodes.OBEX_HTTP_CONTINUE)) { + throw new IOException("Operation has already ended"); + } + + mExceptionMessage = "Operation aborted"; + if ((!mOperationDone) && (mReplyHeader.responseCode == ResponseCodes.OBEX_HTTP_CONTINUE)) { + mOperationDone = true; + /* + * Since we are not sending any headers or returning any headers then + * we just need to write and read the same bytes + */ + mParent.sendRequest(ObexHelper.OBEX_OPCODE_ABORT, null, mReplyHeader, null, false); + + if (mReplyHeader.responseCode != ResponseCodes.OBEX_HTTP_OK) { + throw new IOException("Invalid response code from server"); + } + + mExceptionMessage = null; + } + + close(); + } + + /** + * Retrieves the response code retrieved from the server. Response codes are + * defined in the <code>ResponseCodes</code> interface. + * @return the response code retrieved from the server + * @throws IOException if an error occurred in the transport layer during + * the transaction; if this method is called on a + * <code>HeaderSet</code> object created by calling + * <code>createHeaderSet</code> in a <code>ClientSession</code> + * object + */ + public synchronized int getResponseCode() throws IOException { + if ((mReplyHeader.responseCode == -1) + || (mReplyHeader.responseCode == ResponseCodes.OBEX_HTTP_CONTINUE)) { + validateConnection(); + } + + return mReplyHeader.responseCode; + } + + /** + * Open and return an input stream for a connection. + * @return an input stream + * @throws IOException if an I/O error occurs + */ + public InputStream openInputStream() throws IOException { + + ensureOpen(); + + if (mPrivateInputOpen) + throw new IOException("no more input streams available"); + if (mGetOperation) { + // send the GET request here + validateConnection(); + } else { + if (mPrivateInput == null) { + mPrivateInput = new PrivateInputStream(this); + } + } + + mPrivateInputOpen = true; + + return mPrivateInput; + } + + /** + * Open and return a data input stream for a connection. + * @return an input stream + * @throws IOException if an I/O error occurs + * + * @hide + */ + public DataInputStream openDataInputStream() throws IOException { + return new DataInputStream(openInputStream()); + } + + /** + * Open and return an output stream for a connection. + * @return an output stream + * @throws IOException if an I/O error occurs + */ + public OutputStream openOutputStream() throws IOException { + + ensureOpen(); + ensureNotDone(); + + if (mPrivateOutputOpen) + throw new IOException("no more output streams available"); + + if (mPrivateOutput == null) { + // there are 3 bytes operation headers and 3 bytes body headers // + mPrivateOutput = new PrivateOutputStream(this, getMaxPacketSize()); + } + + mPrivateOutputOpen = true; + + return mPrivateOutput; + } + + public int getMaxPacketSize() { + return mMaxPacketSize - 6 - getHeaderLength(); + } + + /** @hide */ + public int getHeaderLength() { + // OPP may need it + byte[] headerArray = ObexHelper.createHeader(mRequestHeader, false); + return headerArray.length; + } + + /** + * Open and return a data output stream for a connection. + * @return an output stream + * @throws IOException if an I/O error occurs + */ + public DataOutputStream openDataOutputStream() throws IOException { + return new DataOutputStream(openOutputStream()); + } + + /** + * Closes the connection and ends the transaction + * @throws IOException if the operation has already ended or is closed + */ + public void close() throws IOException { + mInputOpen = false; + mPrivateInputOpen = false; + mPrivateOutputOpen = false; + mParent.setRequestInactive(); + } + + /** + * Returns the headers that have been received during the operation. + * Modifying the object returned has no effect on the headers that are sent + * or retrieved. + * @return the headers received during this <code>Operation</code> + * @throws IOException if this <code>Operation</code> has been closed + */ + public HeaderSet getReceivedHeader() throws IOException { + ensureOpen(); + + return mReplyHeader; + } + + /** + * Specifies the headers that should be sent in the next OBEX message that + * is sent. + * @param headers the headers to send in the next message + * @throws IOException if this <code>Operation</code> has been closed or the + * transaction has ended and no further messages will be exchanged + * @throws IllegalArgumentException if <code>headers</code> was not created + * by a call to <code>ServerRequestHandler.createHeaderSet()</code> + * @throws NullPointerException if <code>headers</code> is <code>null</code> + * + * @hide + */ + public void sendHeaders(HeaderSet headers) throws IOException { + ensureOpen(); + if (mOperationDone) { + throw new IOException("Operation has already exchanged all data"); + } + + if (headers == null) { + throw new IOException("Headers may not be null"); + } + + int[] headerList = headers.getHeaderList(); + if (headerList != null) { + for (int i = 0; i < headerList.length; i++) { + mRequestHeader.setHeader(headerList[i], headers.getHeader(headerList[i])); + } + } + } + + /** + * Verifies that additional information may be sent. In other words, the + * operation is not done. + * @throws IOException if the operation is completed + * + * @hide + */ + public void ensureNotDone() throws IOException { + if (mOperationDone) { + throw new IOException("Operation has completed"); + } + } + + /** + * Verifies that the connection is open and no exceptions should be thrown. + * @throws IOException if an exception needs to be thrown + * + * @hide + */ + public void ensureOpen() throws IOException { + mParent.ensureOpen(); + + if (mExceptionMessage != null) { + throw new IOException(mExceptionMessage); + } + if (!mInputOpen) { + throw new IOException("Operation has already ended"); + } + } + + /** + * Verifies that the connection is open and the proper data has been read. + * @throws IOException if an IO error occurs + */ + private void validateConnection() throws IOException { + ensureOpen(); + + // Make sure that a response has been received from remote + // before continuing + if (mPrivateInput == null || mReplyHeader.responseCode == -1) { + startProcessing(); + } + } + + /** + * Sends a request to the client of the specified type. + * This function will enable SRM and set SRM active if the server + * response allows this. + * @param opCode the request code to send to the client + * @return <code>true</code> if there is more data to send; + * <code>false</code> if there is no more data to send + * @throws IOException if an IO error occurs + */ + private boolean sendRequest(int opCode) throws IOException { + boolean returnValue = false; + ByteArrayOutputStream out = new ByteArrayOutputStream(); + int bodyLength = -1; + byte[] headerArray = ObexHelper.createHeader(mRequestHeader, true); + if (mPrivateOutput != null) { + bodyLength = mPrivateOutput.size(); + } + + /* + * Determine if there is space to add a body request. At present + * this method checks to see if there is room for at least a 17 + * byte body header. This number needs to be at least 6 so that + * there is room for the header ID and length and the reply ID and + * length, but it is a waste of resources if we can't send much of + * the body. + */ + final int MINIMUM_BODY_LENGTH = 3; + if ((ObexHelper.BASE_PACKET_LENGTH + headerArray.length + MINIMUM_BODY_LENGTH) + > mMaxPacketSize) { + int end = 0; + int start = 0; + // split & send the headerArray in multiple packets. + + while (end != headerArray.length) { + //split the headerArray + + end = ObexHelper.findHeaderEnd(headerArray, start, mMaxPacketSize + - ObexHelper.BASE_PACKET_LENGTH); + // can not split + if (end == -1) { + mOperationDone = true; + abort(); + mExceptionMessage = "Header larger then can be sent in a packet"; + mInputOpen = false; + + if (mPrivateInput != null) { + mPrivateInput.close(); + } + + if (mPrivateOutput != null) { + mPrivateOutput.close(); + } + throw new IOException("OBEX Packet exceeds max packet size"); + } + + byte[] sendHeader = new byte[end - start]; + System.arraycopy(headerArray, start, sendHeader, 0, sendHeader.length); + if (!mParent.sendRequest(opCode, sendHeader, mReplyHeader, mPrivateInput, false)) { + return false; + } + + if (mReplyHeader.responseCode != ResponseCodes.OBEX_HTTP_CONTINUE) { + return false; + } + + start = end; + } + + // Enable SRM if it should be enabled + checkForSrm(); + + if (bodyLength > 0) { + return true; + } else { + return false; + } + } else { + /* All headers will fit into a single package */ + if(mSendBodyHeader == false) { + /* As we are not to send any body data, set the FINAL_BIT */ + opCode |= ObexHelper.OBEX_OPCODE_FINAL_BIT_MASK; + } + out.write(headerArray); + } + + if (bodyLength > 0) { + /* + * Determine if we can send the whole body or just part of + * the body. Remember that there is the 3 bytes for the + * response message and 3 bytes for the header ID and length + */ + if (bodyLength > (mMaxPacketSize - headerArray.length - 6)) { + returnValue = true; + + bodyLength = mMaxPacketSize - headerArray.length - 6; + } + + byte[] body = mPrivateOutput.readBytes(bodyLength); + + /* + * Since this is a put request if the final bit is set or + * the output stream is closed we need to send the 0x49 + * (End of Body) otherwise, we need to send 0x48 (Body) + */ + if ((mPrivateOutput.isClosed()) && (!returnValue) && (!mEndOfBodySent) + && ((opCode & ObexHelper.OBEX_OPCODE_FINAL_BIT_MASK) != 0)) { + out.write(HeaderSet.END_OF_BODY); + mEndOfBodySent = true; + } else { + out.write(HeaderSet.BODY); + } + + bodyLength += 3; + out.write((byte)(bodyLength >> 8)); + out.write((byte)bodyLength); + + if (body != null) { + out.write(body); + } + } + + if (mPrivateOutputOpen && bodyLength <= 0 && !mEndOfBodySent) { + // only 0x82 or 0x83 can send 0x49 + if ((opCode & ObexHelper.OBEX_OPCODE_FINAL_BIT_MASK) == 0) { + out.write(HeaderSet.BODY); + } else { + out.write(HeaderSet.END_OF_BODY); + mEndOfBodySent = true; + } + + bodyLength = 3; + out.write((byte)(bodyLength >> 8)); + out.write((byte)bodyLength); + } + + if (out.size() == 0) { + if (!mParent.sendRequest(opCode, null, mReplyHeader, mPrivateInput, mSrmActive)) { + return false; + } + // Enable SRM if it should be enabled + checkForSrm(); + return returnValue; + } + if ((out.size() > 0) + && (!mParent.sendRequest(opCode, out.toByteArray(), + mReplyHeader, mPrivateInput, mSrmActive))) { + return false; + } + // Enable SRM if it should be enabled + checkForSrm(); + + // send all of the output data in 0x48, + // send 0x49 with empty body + if ((mPrivateOutput != null) && (mPrivateOutput.size() > 0)) + returnValue = true; + + return returnValue; + } + + private void checkForSrm() throws IOException { + Byte srmMode = (Byte)mReplyHeader.getHeader(HeaderSet.SINGLE_RESPONSE_MODE); + if(mParent.isSrmSupported() == true && srmMode != null + && srmMode == ObexHelper.OBEX_SRM_ENABLE) { + mSrmEnabled = true; + } + /** + * Call this only when a complete obex packet have been received. + * (This is not optimal, but the current design is not really suited to + * the way SRM is specified.) + * The BT usage of SRM is not really safe - it assumes that the SRMP will fit + * into every OBEX packet, hence if another header occupies the entire packet, + * the scheme will not work - unlikely though. + */ + if(mSrmEnabled) { + mSrmWaitingForRemote = false; + Byte srmp = (Byte)mReplyHeader.getHeader(HeaderSet.SINGLE_RESPONSE_MODE_PARAMETER); + if(srmp != null && srmp == ObexHelper.OBEX_SRMP_WAIT) { + mSrmWaitingForRemote = true; + // Clear the wait header, as the absence of the header in the next packet + // indicates don't wait anymore. + mReplyHeader.setHeader(HeaderSet.SINGLE_RESPONSE_MODE_PARAMETER, null); + } + } + if((mSrmWaitingForRemote == false) && (mSrmEnabled == true)) { + mSrmActive = true; + } + } + + /** + * This method starts the processing thread results. It will send the + * initial request. If the response takes more than one packet, a thread + * will be started to handle additional requests + * @throws IOException if an IO error occurs + */ + private synchronized void startProcessing() throws IOException { + + if (mPrivateInput == null) { + mPrivateInput = new PrivateInputStream(this); + } + boolean more = true; + + if (mGetOperation) { + if (!mOperationDone) { + if (!mGetFinalFlag) { + mReplyHeader.responseCode = ResponseCodes.OBEX_HTTP_CONTINUE; + while ((more) && (mReplyHeader.responseCode == + ResponseCodes.OBEX_HTTP_CONTINUE)) { + more = sendRequest(ObexHelper.OBEX_OPCODE_GET); + } + // For GET we need to loop until all headers have been sent, + // And then we wait for the first continue package with the + // reply. + if (mReplyHeader.responseCode == ResponseCodes.OBEX_HTTP_CONTINUE) { + mParent.sendRequest(ObexHelper.OBEX_OPCODE_GET_FINAL, + null, mReplyHeader, mPrivateInput, mSrmActive); + } + if (mReplyHeader.responseCode != ResponseCodes.OBEX_HTTP_CONTINUE) { + mOperationDone = true; + } else { + checkForSrm(); + } + } else { + more = sendRequest(ObexHelper.OBEX_OPCODE_GET_FINAL); + + if (more) { + throw new IOException("FINAL_GET forced, data didn't fit into one packet"); + } + + mOperationDone = true; + } + } + } else { + // PUT operation + if (!mOperationDone) { + mReplyHeader.responseCode = ResponseCodes.OBEX_HTTP_CONTINUE; + while ((more) && (mReplyHeader.responseCode == ResponseCodes.OBEX_HTTP_CONTINUE)) { + more = sendRequest(ObexHelper.OBEX_OPCODE_PUT); + } + } + + if (mReplyHeader.responseCode == ResponseCodes.OBEX_HTTP_CONTINUE) { + mParent.sendRequest(ObexHelper.OBEX_OPCODE_PUT_FINAL, + null, mReplyHeader, mPrivateInput, mSrmActive); + } + + if (mReplyHeader.responseCode != ResponseCodes.OBEX_HTTP_CONTINUE) { + mOperationDone = true; + } + } + } + + /** + * Continues the operation since there is no data to read. + * @param sendEmpty <code>true</code> if the operation should send an empty + * packet or not send anything if there is no data to send + * @param inStream <code>true</code> if the stream is input stream or is + * output stream + * @throws IOException if an IO error occurs + */ + public synchronized boolean continueOperation(boolean sendEmpty, boolean inStream) + throws IOException { + + // One path to the first put operation - the other one does not need to + // handle SRM, as all will fit into one packet. + + if (mGetOperation) { + if ((inStream) && (!mOperationDone)) { + // to deal with inputstream in get operation + mParent.sendRequest(ObexHelper.OBEX_OPCODE_GET_FINAL, + null, mReplyHeader, mPrivateInput, mSrmActive); + /* + * Determine if that was not the last packet in the operation + */ + if (mReplyHeader.responseCode != ResponseCodes.OBEX_HTTP_CONTINUE) { + mOperationDone = true; + } else { + checkForSrm(); + } + + return true; + + } else if ((!inStream) && (!mOperationDone)) { + // to deal with outputstream in get operation + + if (mPrivateInput == null) { + mPrivateInput = new PrivateInputStream(this); + } + + if (!mGetFinalFlag) { + sendRequest(ObexHelper.OBEX_OPCODE_GET); + } else { + sendRequest(ObexHelper.OBEX_OPCODE_GET_FINAL); + } + if (mReplyHeader.responseCode != ResponseCodes.OBEX_HTTP_CONTINUE) { + mOperationDone = true; + } + return true; + + } else if (mOperationDone) { + return false; + } + + } else { + // PUT operation + if ((!inStream) && (!mOperationDone)) { + // to deal with outputstream in put operation + if (mReplyHeader.responseCode == -1) { + mReplyHeader.responseCode = ResponseCodes.OBEX_HTTP_CONTINUE; + } + sendRequest(ObexHelper.OBEX_OPCODE_PUT); + return true; + } else if ((inStream) && (!mOperationDone)) { + // How to deal with inputstream in put operation ? + return false; + + } else if (mOperationDone) { + return false; + } + + } + return false; + } + + /** + * Called when the output or input stream is closed. + * @param inStream <code>true</code> if the input stream is closed; + * <code>false</code> if the output stream is closed + * @throws IOException if an IO error occurs + * + * @hide + */ + public void streamClosed(boolean inStream) throws IOException { + if (!mGetOperation) { + if ((!inStream) && (!mOperationDone)) { + // to deal with outputstream in put operation + + boolean more = true; + + if ((mPrivateOutput != null) && (mPrivateOutput.size() <= 0)) { + byte[] headerArray = ObexHelper.createHeader(mRequestHeader, false); + if (headerArray.length <= 0) + more = false; + } + // If have not sent any data so send all now + if (mReplyHeader.responseCode == -1) { + mReplyHeader.responseCode = ResponseCodes.OBEX_HTTP_CONTINUE; + } + + while ((more) && (mReplyHeader.responseCode == ResponseCodes.OBEX_HTTP_CONTINUE)) { + more = sendRequest(ObexHelper.OBEX_OPCODE_PUT); + } + + /* + * According to the IrOBEX specification, after the final put, you + * only have a single reply to send. so we don't need the while + * loop. + */ + while (mReplyHeader.responseCode == ResponseCodes.OBEX_HTTP_CONTINUE) { + + sendRequest(ObexHelper.OBEX_OPCODE_PUT_FINAL); + } + mOperationDone = true; + } else if ((inStream) && (mOperationDone)) { + // how to deal with input stream in put stream ? + mOperationDone = true; + } + } else { + if ((inStream) && (!mOperationDone)) { + + // to deal with inputstream in get operation + // Have not sent any data so send it all now + + if (mReplyHeader.responseCode == -1) { + mReplyHeader.responseCode = ResponseCodes.OBEX_HTTP_CONTINUE; + } + + while (mReplyHeader.responseCode == ResponseCodes.OBEX_HTTP_CONTINUE + && !mOperationDone) { + if (!sendRequest(ObexHelper.OBEX_OPCODE_GET_FINAL)) { + break; + } + } + while (mReplyHeader.responseCode == ResponseCodes.OBEX_HTTP_CONTINUE + && !mOperationDone) { + mParent.sendRequest(ObexHelper.OBEX_OPCODE_GET_FINAL, null, + mReplyHeader, mPrivateInput, false); + // Regardless of the SRM state, wait for the response. + } + mOperationDone = true; + } else if ((!inStream) && (!mOperationDone)) { + // to deal with outputstream in get operation + // part of the data may have been sent in continueOperation. + + boolean more = true; + + if ((mPrivateOutput != null) && (mPrivateOutput.size() <= 0)) { + byte[] headerArray = ObexHelper.createHeader(mRequestHeader, false); + if (headerArray.length <= 0) + more = false; + } + + if (mPrivateInput == null) { + mPrivateInput = new PrivateInputStream(this); + } + if ((mPrivateOutput != null) && (mPrivateOutput.size() <= 0)) + more = false; + + mReplyHeader.responseCode = ResponseCodes.OBEX_HTTP_CONTINUE; + while ((more) && (mReplyHeader.responseCode == ResponseCodes.OBEX_HTTP_CONTINUE)) { + more = sendRequest(ObexHelper.OBEX_OPCODE_GET); + } + sendRequest(ObexHelper.OBEX_OPCODE_GET_FINAL); + // parent.sendRequest(0x83, null, replyHeaders, privateInput); + if (mReplyHeader.responseCode != ResponseCodes.OBEX_HTTP_CONTINUE) { + mOperationDone = true; + } + } + } + } + + /** @hide */ + public void noBodyHeader(){ + mSendBodyHeader = false; + } +} diff --git a/javax/obex/ClientSession.java b/javax/obex/ClientSession.java new file mode 100644 index 0000000..fcdc2c7 --- /dev/null +++ b/javax/obex/ClientSession.java @@ -0,0 +1,637 @@ +/* + * Copyright (c) 2015 The Android Open Source Project + * Copyright (C) 2015 Samsung LSI + * 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.util.Log; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +/** + * This class in an implementation of the OBEX ClientSession. + */ +public final class ClientSession extends ObexSession { + + private static final String TAG = "ClientSession"; + + private boolean mOpen; + + // Determines if an OBEX layer connection has been established + private boolean mObexConnected; + + private byte[] mConnectionId = null; + + /* + * The max Packet size must be at least 255 according to the OBEX + * specification. + */ + private int mMaxTxPacketSize = ObexHelper.LOWER_LIMIT_MAX_PACKET_SIZE; + + private boolean mRequestActive; + + private final InputStream mInput; + + private final OutputStream mOutput; + + private final boolean mLocalSrmSupported; + + private final ObexTransport mTransport; + + /** + * Creates a ClientSession. + * + * @param transport the transport to use for OBEX transactions + * @throws IOException if it occurs while opening the transport streams + */ + public ClientSession(final ObexTransport transport) throws IOException { + mInput = transport.openInputStream(); + mOutput = transport.openOutputStream(); + mOpen = true; + mRequestActive = false; + mLocalSrmSupported = transport.isSrmSupported(); + mTransport = transport; + } + + /** + * Create a ClientSession. + * + * @param transport the transport to use for OBEX transactions + * @param supportsSrm true if Single Response Mode should be used e.g. if the + * supplied transport is a TCP or l2cap channel + * @throws IOException if it occurs while opening the transport streams + * + * @hide + */ + public ClientSession(final ObexTransport transport, final boolean supportsSrm) + throws IOException { + mInput = transport.openInputStream(); + mOutput = transport.openOutputStream(); + mOpen = true; + mRequestActive = false; + mLocalSrmSupported = supportsSrm; + mTransport = transport; + } + + public HeaderSet connect(final HeaderSet header) throws IOException { + ensureOpen(); + if (mObexConnected) { + throw new IOException("Already connected to server"); + } + setRequestActive(); + + int totalLength = 4; + byte[] head = null; + + // Determine the header byte array + if (header != null) { + if (header.nonce != null) { + mChallengeDigest = new byte[16]; + System.arraycopy(header.nonce, 0, mChallengeDigest, 0, 16); + } + head = ObexHelper.createHeader(header, false); + totalLength += head.length; + } + /* + * Write the OBEX CONNECT packet to the server. + * Byte 0: 0x80 + * Byte 1&2: Connect Packet Length + * Byte 3: OBEX Version Number (Presently, 0x10) + * Byte 4: Flags (For TCP 0x00) + * Byte 5&6: Max OBEX Packet Length (Defined in MAX_PACKET_SIZE) + * Byte 7 to n: headers + */ + byte[] requestPacket = new byte[totalLength]; + int maxRxPacketSize = ObexHelper.getMaxRxPacketSize(mTransport); + // We just need to start at byte 3 since the sendRequest() method will + // handle the length and 0x80. + requestPacket[0] = (byte)0x10; + requestPacket[1] = (byte)0x00; + requestPacket[2] = (byte)(maxRxPacketSize >> 8); + requestPacket[3] = (byte)(maxRxPacketSize & 0xFF); + if (head != null) { + System.arraycopy(head, 0, requestPacket, 4, head.length); + } + + // Since we are not yet connected, the peer max packet size is unknown, + // hence we are only guaranteed the server will use the first 7 bytes. + if ((requestPacket.length + 3) > ObexHelper.MAX_PACKET_SIZE_INT) { + throw new IOException("Packet size exceeds max packet size for connect"); + } + + HeaderSet returnHeaderSet = new HeaderSet(); + sendRequest(ObexHelper.OBEX_OPCODE_CONNECT, requestPacket, returnHeaderSet, null, false); + + /* + * Read the response from the OBEX server. + * Byte 0: Response Code (If successful then OBEX_HTTP_OK) + * Byte 1&2: Packet Length + * Byte 3: OBEX Version Number + * Byte 4: Flags3 + * Byte 5&6: Max OBEX packet Length + * Byte 7 to n: Optional HeaderSet + */ + if (returnHeaderSet.responseCode == ResponseCodes.OBEX_HTTP_OK) { + mObexConnected = true; + } + setRequestInactive(); + + return returnHeaderSet; + } + + public Operation get(HeaderSet header) throws IOException { + + if (!mObexConnected) { + throw new IOException("Not connected to the server"); + } + setRequestActive(); + + ensureOpen(); + + HeaderSet head; + if (header == null) { + head = new HeaderSet(); + } else { + head = header; + if (head.nonce != null) { + mChallengeDigest = new byte[16]; + System.arraycopy(head.nonce, 0, mChallengeDigest, 0, 16); + } + } + // Add the connection ID if one exists + if (mConnectionId != null) { + head.mConnectionID = new byte[4]; + System.arraycopy(mConnectionId, 0, head.mConnectionID, 0, 4); + } + + if(mLocalSrmSupported) { + head.setHeader(HeaderSet.SINGLE_RESPONSE_MODE, ObexHelper.OBEX_SRM_ENABLE); + /* TODO: Consider creating an interface to get the wait state. + * On an android system, I cannot see when this is to be used. + * except perhaps if we are to wait for user accept on a push message. + if(getLocalWaitState()) { + head.setHeader(HeaderSet.SINGLE_RESPONSE_MODE_PARAMETER, ObexHelper.OBEX_SRMP_WAIT); + } + */ + } + + return new ClientOperation(mMaxTxPacketSize, this, head, true); + } + + /** + * 0xCB Connection Id an identifier used for OBEX connection multiplexing + * + * @hide + */ + public void setConnectionID(long id) { + if ((id < 0) || (id > 0xFFFFFFFFL)) { + throw new IllegalArgumentException("Connection ID is not in a valid range"); + } + mConnectionId = ObexHelper.convertToByteArray(id); + } + + /** @hide */ + public HeaderSet delete(HeaderSet header) throws IOException { + + Operation op = put(header); + op.getResponseCode(); + HeaderSet returnValue = op.getReceivedHeader(); + op.close(); + + return returnValue; + } + + public HeaderSet disconnect(HeaderSet header) throws IOException { + if (!mObexConnected) { + throw new IOException("Not connected to the server"); + } + setRequestActive(); + + ensureOpen(); + // Determine the header byte array + byte[] head = null; + if (header != null) { + if (header.nonce != null) { + mChallengeDigest = new byte[16]; + System.arraycopy(header.nonce, 0, mChallengeDigest, 0, 16); + } + // Add the connection ID if one exists + if (mConnectionId != null) { + header.mConnectionID = new byte[4]; + System.arraycopy(mConnectionId, 0, header.mConnectionID, 0, 4); + } + head = ObexHelper.createHeader(header, false); + + if ((head.length + 3) > mMaxTxPacketSize) { + throw new IOException("Packet size exceeds max packet size"); + } + } else { + // Add the connection ID if one exists + if (mConnectionId != null) { + head = new byte[5]; + head[0] = (byte)HeaderSet.CONNECTION_ID; + System.arraycopy(mConnectionId, 0, head, 1, 4); + } + } + + HeaderSet returnHeaderSet = new HeaderSet(); + sendRequest(ObexHelper.OBEX_OPCODE_DISCONNECT, head, returnHeaderSet, null, false); + + /* + * An OBEX DISCONNECT reply from the server: + * Byte 1: Response code + * Bytes 2 & 3: packet size + * Bytes 4 & up: headers + */ + + /* + * response code , and header are ignored + */ + + synchronized (this) { + mObexConnected = false; + setRequestInactive(); + } + + return returnHeaderSet; + } + + /** @hide */ + public long getConnectionID() { + + if (mConnectionId == null) { + return -1; + } + return ObexHelper.convertToLong(mConnectionId); + } + + public Operation put(HeaderSet header) throws IOException { + if (!mObexConnected) { + throw new IOException("Not connected to the server"); + } + setRequestActive(); + + ensureOpen(); + HeaderSet head; + if (header == null) { + head = new HeaderSet(); + } else { + head = header; + // when auth is initiated by client ,save the digest + if (head.nonce != null) { + mChallengeDigest = new byte[16]; + System.arraycopy(head.nonce, 0, mChallengeDigest, 0, 16); + } + } + + // Add the connection ID if one exists + if (mConnectionId != null) { + + head.mConnectionID = new byte[4]; + System.arraycopy(mConnectionId, 0, head.mConnectionID, 0, 4); + } + + if(mLocalSrmSupported) { + head.setHeader(HeaderSet.SINGLE_RESPONSE_MODE, ObexHelper.OBEX_SRM_ENABLE); + /* TODO: Consider creating an interface to get the wait state. + * On an android system, I cannot see when this is to be used. + if(getLocalWaitState()) { + head.setHeader(HeaderSet.SINGLE_RESPONSE_MODE_PARAMETER, ObexHelper.OBEX_SRMP_WAIT); + } + */ + } + return new ClientOperation(mMaxTxPacketSize, this, head, false); + } + + /** @hide */ + public void setAuthenticator(Authenticator auth) throws IOException { + if (auth == null) { + throw new IOException("Authenticator may not be null"); + } + mAuthenticator = auth; + } + + public HeaderSet setPath(HeaderSet header, boolean backup, boolean create) throws IOException { + if (!mObexConnected) { + throw new IOException("Not connected to the server"); + } + setRequestActive(); + ensureOpen(); + + int totalLength = 2; + byte[] head = null; + HeaderSet headset; + if (header == null) { + headset = new HeaderSet(); + } else { + headset = header; + if (headset.nonce != null) { + mChallengeDigest = new byte[16]; + System.arraycopy(headset.nonce, 0, mChallengeDigest, 0, 16); + } + } + + // when auth is initiated by client ,save the digest + if (headset.nonce != null) { + mChallengeDigest = new byte[16]; + System.arraycopy(headset.nonce, 0, mChallengeDigest, 0, 16); + } + + // Add the connection ID if one exists + if (mConnectionId != null) { + headset.mConnectionID = new byte[4]; + System.arraycopy(mConnectionId, 0, headset.mConnectionID, 0, 4); + } + + head = ObexHelper.createHeader(headset, false); + totalLength += head.length; + + if (totalLength > mMaxTxPacketSize) { + throw new IOException("Packet size exceeds max packet size"); + } + + int flags = 0; + /* + * The backup flag bit is bit 0 so if we add 1, this will set that bit + */ + if (backup) { + flags++; + } + /* + * The create bit is bit 1 so if we or with 2 the bit will be set. + */ + if (!create) { + flags |= 2; + } + + /* + * An OBEX SETPATH packet to the server: + * Byte 1: 0x85 + * Byte 2 & 3: packet size + * Byte 4: flags + * Byte 5: constants + * Byte 6 & up: headers + */ + byte[] packet = new byte[totalLength]; + packet[0] = (byte)flags; + packet[1] = (byte)0x00; + if (headset != null) { + System.arraycopy(head, 0, packet, 2, head.length); + } + + HeaderSet returnHeaderSet = new HeaderSet(); + sendRequest(ObexHelper.OBEX_OPCODE_SETPATH, packet, returnHeaderSet, null, false); + + /* + * An OBEX SETPATH reply from the server: + * Byte 1: Response code + * Bytes 2 & 3: packet size + * Bytes 4 & up: headers + */ + + setRequestInactive(); + + return returnHeaderSet; + } + + /** + * Verifies that the connection is open. + * @throws IOException if the connection is closed + * + * @hide + */ + public synchronized void ensureOpen() throws IOException { + if (!mOpen) { + throw new IOException("Connection closed"); + } + } + + /** + * Set request inactive. Allows Put and get operation objects to tell this + * object when they are done. + */ + /*package*/synchronized void setRequestInactive() { + mRequestActive = false; + } + + /** + * Set request to active. + * @throws IOException if already active + */ + private synchronized void setRequestActive() throws IOException { + if (mRequestActive) { + throw new IOException("OBEX request is already being performed"); + } + mRequestActive = true; + } + + /** + * Sends a standard request to the client. It will then wait for the reply + * and update the header set object provided. If any authentication headers + * (i.e. authentication challenge or authentication response) are received, + * they will be processed. + * @param opCode the type of request to send to the client + * @param head the headers to send to the client + * @param header the header object to update with the response + * @param privateInput the input stream used by the Operation object; null + * if this is called on a CONNECT, SETPATH or DISCONNECT + * @return + * <code>true</code> if the operation completed successfully; + * <code>false</code> if an authentication response failed to pass + * @throws IOException if an IO error occurs + * + * @hide + */ + public boolean sendRequest(int opCode, byte[] head, HeaderSet header, + PrivateInputStream privateInput, boolean srmActive) throws IOException { + //check header length with local max size + if (head != null) { + if ((head.length + 3) > ObexHelper.MAX_PACKET_SIZE_INT) { + // TODO: This is an implementation limit - not a specification requirement. + throw new IOException("header too large "); + } + } + + boolean skipSend = false; + boolean skipReceive = false; + if (srmActive == true) { + if (opCode == ObexHelper.OBEX_OPCODE_PUT) { + // we are in the middle of a SRM PUT operation, don't expect a continue. + skipReceive = true; + } else if (opCode == ObexHelper.OBEX_OPCODE_GET) { + // We are still sending the get request, send, but don't expect continue + // until the request is transferred (the final bit is set) + skipReceive = true; + } else if (opCode == ObexHelper.OBEX_OPCODE_GET_FINAL) { + // All done sending the request, expect data from the server, without + // sending continue. + skipSend = true; + } + + } + + int bytesReceived; + ByteArrayOutputStream out = new ByteArrayOutputStream(); + out.write((byte)opCode); + + // Determine if there are any headers to send + if (head == null) { + out.write(0x00); + out.write(0x03); + } else { + out.write((byte)((head.length + 3) >> 8)); + out.write((byte)(head.length + 3)); + out.write(head); + } + + if (!skipSend) { + // Write the request to the output stream and flush the stream + mOutput.write(out.toByteArray()); + // TODO: is this really needed? if this flush is implemented + // correctly, we will get a gap between each obex packet. + // which is kind of the idea behind SRM to avoid. + // Consider offloading to another thread (async action) + mOutput.flush(); + } + + if (!skipReceive) { + header.responseCode = mInput.read(); + + int length = ((mInput.read() << 8) | (mInput.read())); + + if (length > ObexHelper.getMaxRxPacketSize(mTransport)) { + throw new IOException("Packet received exceeds packet size limit"); + } + if (length > ObexHelper.BASE_PACKET_LENGTH) { + byte[] data = null; + if (opCode == ObexHelper.OBEX_OPCODE_CONNECT) { + @SuppressWarnings("unused") + int version = mInput.read(); + @SuppressWarnings("unused") + int flags = mInput.read(); + mMaxTxPacketSize = (mInput.read() << 8) + mInput.read(); + + //check with local max size + if (mMaxTxPacketSize > ObexHelper.MAX_CLIENT_PACKET_SIZE) { + mMaxTxPacketSize = ObexHelper.MAX_CLIENT_PACKET_SIZE; + } + + // check with transport maximum size + if(mMaxTxPacketSize > ObexHelper.getMaxTxPacketSize(mTransport)) { + // To increase this size, increase the buffer size in L2CAP layer + // in Bluedroid. + Log.w(TAG, "An OBEX packet size of " + mMaxTxPacketSize + "was" + + " requested. Transport only allows: " + + ObexHelper.getMaxTxPacketSize(mTransport) + + " Lowering limit to this value."); + mMaxTxPacketSize = ObexHelper.getMaxTxPacketSize(mTransport); + } + + if (length > 7) { + data = new byte[length - 7]; + + bytesReceived = mInput.read(data); + while (bytesReceived != (length - 7)) { + bytesReceived += mInput.read(data, bytesReceived, data.length + - bytesReceived); + } + } else { + return true; + } + } else { + data = new byte[length - 3]; + bytesReceived = mInput.read(data); + + while (bytesReceived != (length - 3)) { + bytesReceived += mInput.read(data, bytesReceived, + data.length - bytesReceived); + } + if (opCode == ObexHelper.OBEX_OPCODE_ABORT) { + return true; + } + } + + byte[] body = ObexHelper.updateHeaderSet(header, data); + if ((privateInput != null) && (body != null)) { + privateInput.writeBytes(body, 1); + } + + if (header.mConnectionID != null) { + mConnectionId = new byte[4]; + System.arraycopy(header.mConnectionID, 0, mConnectionId, 0, 4); + } + + if (header.mAuthResp != null) { + if (!handleAuthResp(header.mAuthResp)) { + setRequestInactive(); + throw new IOException("Authentication Failed"); + } + } + + if ((header.responseCode == ResponseCodes.OBEX_HTTP_UNAUTHORIZED) + && (header.mAuthChall != null)) { + + if (handleAuthChall(header)) { + out.write((byte)HeaderSet.AUTH_RESPONSE); + out.write((byte)((header.mAuthResp.length + 3) >> 8)); + out.write((byte)(header.mAuthResp.length + 3)); + out.write(header.mAuthResp); + header.mAuthChall = null; + header.mAuthResp = null; + + byte[] sendHeaders = new byte[out.size() - 3]; + System.arraycopy(out.toByteArray(), 3, sendHeaders, 0, sendHeaders.length); + + return sendRequest(opCode, sendHeaders, header, privateInput, false); + } + } + } + } + + return true; + } + + public void close() throws IOException { + mOpen = false; + mInput.close(); + mOutput.close(); + } + + /** @hide */ + public boolean isSrmSupported() { + return mLocalSrmSupported; + } +} diff --git a/javax/obex/HeaderSet.java b/javax/obex/HeaderSet.java new file mode 100644 index 0000000..8ce8d5f --- /dev/null +++ b/javax/obex/HeaderSet.java @@ -0,0 +1,728 @@ +/* + * Copyright (c) 2014 The Android Open Source Project + * 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 java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.security.SecureRandom; +import java.util.Calendar; + +/** + * This class implements the javax.obex.HeaderSet interface for OBEX over + * RFCOMM or OBEX over l2cap. + */ +public final class HeaderSet { + + /** + * Represents the OBEX Count header. This allows the connection statement to + * tell the server how many objects it plans to send or retrieve. + * <P> + * The value of <code>COUNT</code> is 0xC0 (192). + */ + public static final int COUNT = 0xC0; + + /** + * Represents the OBEX Name header. This specifies the name of the object. + * <P> + * The value of <code>NAME</code> is 0x01 (1). + */ + public static final int NAME = 0x01; + + /** + * Represents the OBEX Type header. This allows a request to specify the + * type of the object (e.g. text, html, binary, etc.). + * <P> + * The value of <code>TYPE</code> is 0x42 (66). + */ + public static final int TYPE = 0x42; + + /** + * Represents the OBEX Length header. This is the length of the object in + * bytes. + * <P> + * The value of <code>LENGTH</code> is 0xC3 (195). + */ + public static final int LENGTH = 0xC3; + + /** + * Represents the OBEX Time header using the ISO 8601 standards. This is the + * preferred time header. + * <P> + * The value of <code>TIME_ISO_8601</code> is 0x44 (68). + */ + public static final int TIME_ISO_8601 = 0x44; + + /** + * Represents the OBEX Time header using the 4 byte representation. This is + * only included for backwards compatibility. It represents the number of + * seconds since January 1, 1970. + * <P> + * The value of <code>TIME_4_BYTE</code> is 0xC4 (196). + */ + public static final int TIME_4_BYTE = 0xC4; + + /** + * Represents the OBEX Description header. This is a text description of the + * object. + * <P> + * The value of <code>DESCRIPTION</code> is 0x05 (5). + */ + public static final int DESCRIPTION = 0x05; + + /** + * Represents the OBEX Target header. This is the name of the service an + * operation is targeted to. + * <P> + * The value of <code>TARGET</code> is 0x46 (70). + */ + public static final int TARGET = 0x46; + + /** + * Represents the OBEX HTTP header. This allows an HTTP 1.X header to be + * included in a request or reply. + * <P> + * The value of <code>HTTP</code> is 0x47 (71). + */ + public static final int HTTP = 0x47; + + /** + * Represents the OBEX BODY header. + * <P> + * The value of <code>BODY</code> is 0x48 (72). + * + * @hide + */ + public static final int BODY = 0x48; + + /** + * Represents the OBEX End of BODY header. + * <P> + * The value of <code>BODY</code> is 0x49 (73). + * + * @hide + */ + public static final int END_OF_BODY = 0x49; + + /** + * Represents the OBEX Who header. Identifies the OBEX application to + * determine if the two peers are talking to each other. + * <P> + * The value of <code>WHO</code> is 0x4A (74). + */ + public static final int WHO = 0x4A; + + /** + * Represents the OBEX Connection ID header. Identifies used for OBEX + * connection multiplexing. + * <P> + * The value of <code>CONNECTION_ID</code> is 0xCB (203). + * + * @hide + */ + public static final int CONNECTION_ID = 0xCB; + + /** + * Represents the OBEX Application Parameter header. This header specifies + * additional application request and response information. + * <P> + * The value of <code>APPLICATION_PARAMETER</code> is 0x4C (76). + */ + public static final int APPLICATION_PARAMETER = 0x4C; + + /** + * Represents the OBEX authentication digest-challenge. + * <P> + * The value of <code>AUTH_CHALLENGE</code> is 0x4D (77). + * + * @hide + */ + public static final int AUTH_CHALLENGE = 0x4D; + + /** + * Represents the OBEX authentication digest-response. + * <P> + * The value of <code>AUTH_RESPONSE</code> is 0x4E (78). + * + * @hide + */ + public static final int AUTH_RESPONSE = 0x4E; + + /** + * Represents the OBEX Object Class header. This header specifies the OBEX + * object class of the object. + * <P> + * The value of <code>OBJECT_CLASS</code> is 0x4F (79). + */ + public static final int OBJECT_CLASS = 0x4F; + + /** + * Represents the OBEX Single Response Mode (SRM). This header is used + * for Single response mode, introduced in OBEX 1.5. + * <P> + * The value of <code>SINGLE_RESPONSE_MODE</code> is 0x97 (151). + * + * @hide + */ + public static final int SINGLE_RESPONSE_MODE = 0x97; + + /** + * Represents the OBEX Single Response Mode Parameters. This header is used + * for Single response mode, introduced in OBEX 1.5. + * <P> + * The value of <code>SINGLE_RESPONSE_MODE_PARAMETER</code> is 0x98 (152). + * + * @hide + */ + public static final int SINGLE_RESPONSE_MODE_PARAMETER = 0x98; + + private Long mCount; // 4 byte unsigned integer + + private String mName; // null terminated Unicode text string + + private boolean mEmptyName; + + private String mType; // null terminated ASCII text string + + private Long mLength; // 4 byte unsigend integer + + private Calendar mIsoTime; // String of the form YYYYMMDDTHHMMSSZ + + private Calendar mByteTime; // 4 byte unsigned integer + + private String mDescription; // null terminated Unicode text String + + private byte[] mTarget; // byte sequence + + private byte[] mHttpHeader; // byte sequence + + private byte[] mWho; // length prefixed byte sequence + + private byte[] mAppParam; // byte sequence of the form tag length value + + private byte[] mObjectClass; // byte sequence + + private String[] mUnicodeUserDefined; // null terminated unicode string + + private byte[][] mSequenceUserDefined; // byte sequence user defined + + private Byte[] mByteUserDefined; // 1 byte + + private Long[] mIntegerUserDefined; // 4 byte unsigned integer + + private SecureRandom mRandom = null; + + private Byte mSingleResponseMode; // byte to indicate enable/disable/support for SRM + + private Byte mSrmParam; // byte representing the SRM parameters - only "wait" + // is supported by Bluetooth + + /*package*/ byte[] nonce; + + public byte[] mAuthChall; // The authentication challenge header + + public byte[] mAuthResp; // The authentication response header + + public byte[] mConnectionID; // THe connection ID + + public int responseCode; + + /** + * Creates new <code>HeaderSet</code> object. + */ + public HeaderSet() { + mUnicodeUserDefined = new String[16]; + mSequenceUserDefined = new byte[16][]; + mByteUserDefined = new Byte[16]; + mIntegerUserDefined = new Long[16]; + responseCode = -1; + } + + /** + * Sets flag for special "value" of NAME header which should be empty. This + * is not the same as NAME header with empty string in which case it will + * have length of 5 bytes. It should be 3 bytes with only header id and + * length field. + */ + public void setEmptyNameHeader() { + mName = null; + mEmptyName = true; + } + + /** + * Gets flag for special "value" of NAME header which should be empty. See + * above. + * + * @hide + */ + public boolean getEmptyNameHeader() { + return mEmptyName; + } + + /** + * Sets the value of the header identifier to the value provided. The type + * of object must correspond to the Java type defined in the description of + * this interface. If <code>null</code> is passed as the + * <code>headerValue</code> then the header will be removed from the set of + * headers to include in the next request. + * @param headerID the identifier to include in the message + * @param headerValue the value of the header identifier + * @throws IllegalArgumentException if the header identifier provided is not + * one defined in this interface or a user-defined header; if the + * type of <code>headerValue</code> is not the correct Java type as + * defined in the description of this interface\ + */ + public void setHeader(int headerID, Object headerValue) { + long temp = -1; + + switch (headerID) { + case COUNT: + if (!(headerValue instanceof Long)) { + if (headerValue == null) { + mCount = null; + break; + } + throw new IllegalArgumentException("Count must be a Long"); + } + temp = ((Long)headerValue).longValue(); + if ((temp < 0L) || (temp > 0xFFFFFFFFL)) { + throw new IllegalArgumentException("Count must be between 0 and 0xFFFFFFFF"); + } + mCount = (Long)headerValue; + break; + case NAME: + if ((headerValue != null) && (!(headerValue instanceof String))) { + throw new IllegalArgumentException("Name must be a String"); + } + mEmptyName = false; + mName = (String)headerValue; + break; + case TYPE: + if ((headerValue != null) && (!(headerValue instanceof String))) { + throw new IllegalArgumentException("Type must be a String"); + } + mType = (String)headerValue; + break; + case LENGTH: + if (!(headerValue instanceof Long)) { + if (headerValue == null) { + mLength = null; + break; + } + throw new IllegalArgumentException("Length must be a Long"); + } + temp = ((Long)headerValue).longValue(); + if ((temp < 0L) || (temp > 0xFFFFFFFFL)) { + throw new IllegalArgumentException("Length must be between 0 and 0xFFFFFFFF"); + } + mLength = (Long)headerValue; + break; + case TIME_ISO_8601: + if ((headerValue != null) && (!(headerValue instanceof Calendar))) { + throw new IllegalArgumentException("Time ISO 8601 must be a Calendar"); + } + mIsoTime = (Calendar)headerValue; + break; + case TIME_4_BYTE: + if ((headerValue != null) && (!(headerValue instanceof Calendar))) { + throw new IllegalArgumentException("Time 4 Byte must be a Calendar"); + } + mByteTime = (Calendar)headerValue; + break; + case DESCRIPTION: + if ((headerValue != null) && (!(headerValue instanceof String))) { + throw new IllegalArgumentException("Description must be a String"); + } + mDescription = (String)headerValue; + break; + case TARGET: + if (headerValue == null) { + mTarget = null; + } else { + if (!(headerValue instanceof byte[])) { + throw new IllegalArgumentException("Target must be a byte array"); + } else { + mTarget = new byte[((byte[])headerValue).length]; + System.arraycopy(headerValue, 0, mTarget, 0, mTarget.length); + } + } + break; + case HTTP: + if (headerValue == null) { + mHttpHeader = null; + } else { + if (!(headerValue instanceof byte[])) { + throw new IllegalArgumentException("HTTP must be a byte array"); + } else { + mHttpHeader = new byte[((byte[])headerValue).length]; + System.arraycopy(headerValue, 0, mHttpHeader, 0, mHttpHeader.length); + } + } + break; + case WHO: + if (headerValue == null) { + mWho = null; + } else { + if (!(headerValue instanceof byte[])) { + throw new IllegalArgumentException("WHO must be a byte array"); + } else { + mWho = new byte[((byte[])headerValue).length]; + System.arraycopy(headerValue, 0, mWho, 0, mWho.length); + } + } + break; + case OBJECT_CLASS: + if (headerValue == null) { + mObjectClass = null; + } else { + if (!(headerValue instanceof byte[])) { + throw new IllegalArgumentException("Object Class must be a byte array"); + } else { + mObjectClass = new byte[((byte[])headerValue).length]; + System.arraycopy(headerValue, 0, mObjectClass, 0, mObjectClass.length); + } + } + break; + case APPLICATION_PARAMETER: + if (headerValue == null) { + mAppParam = null; + } else { + if (!(headerValue instanceof byte[])) { + throw new IllegalArgumentException( + "Application Parameter must be a byte array"); + } else { + mAppParam = new byte[((byte[])headerValue).length]; + System.arraycopy(headerValue, 0, mAppParam, 0, mAppParam.length); + } + } + break; + case SINGLE_RESPONSE_MODE: + if (headerValue == null) { + mSingleResponseMode = null; + } else { + if (!(headerValue instanceof Byte)) { + throw new IllegalArgumentException( + "Single Response Mode must be a Byte"); + } else { + mSingleResponseMode = (Byte)headerValue; + } + } + break; + case SINGLE_RESPONSE_MODE_PARAMETER: + if (headerValue == null) { + mSrmParam = null; + } else { + if (!(headerValue instanceof Byte)) { + throw new IllegalArgumentException( + "Single Response Mode Parameter must be a Byte"); + } else { + mSrmParam = (Byte)headerValue; + } + } + break; + default: + // Verify that it was not a Unicode String user Defined + if ((headerID >= 0x30) && (headerID <= 0x3F)) { + if ((headerValue != null) && (!(headerValue instanceof String))) { + throw new IllegalArgumentException( + "Unicode String User Defined must be a String"); + } + mUnicodeUserDefined[headerID - 0x30] = (String)headerValue; + + break; + } + // Verify that it was not a byte sequence user defined value + if ((headerID >= 0x70) && (headerID <= 0x7F)) { + + if (headerValue == null) { + mSequenceUserDefined[headerID - 0x70] = null; + } else { + if (!(headerValue instanceof byte[])) { + throw new IllegalArgumentException( + "Byte Sequence User Defined must be a byte array"); + } else { + mSequenceUserDefined[headerID - 0x70] + = new byte[((byte[])headerValue).length]; + System.arraycopy(headerValue, 0, mSequenceUserDefined[headerID - 0x70], + 0, mSequenceUserDefined[headerID - 0x70].length); + } + } + break; + } + // Verify that it was not a Byte user Defined + if ((headerID >= 0xB0) && (headerID <= 0xBF)) { + if ((headerValue != null) && (!(headerValue instanceof Byte))) { + throw new IllegalArgumentException("ByteUser Defined must be a Byte"); + } + mByteUserDefined[headerID - 0xB0] = (Byte)headerValue; + + break; + } + // Verify that is was not the 4 byte unsigned integer user + // defined header + if ((headerID >= 0xF0) && (headerID <= 0xFF)) { + if (!(headerValue instanceof Long)) { + if (headerValue == null) { + mIntegerUserDefined[headerID - 0xF0] = null; + break; + } + throw new IllegalArgumentException("Integer User Defined must be a Long"); + } + temp = ((Long)headerValue).longValue(); + if ((temp < 0L) || (temp > 0xFFFFFFFFL)) { + throw new IllegalArgumentException( + "Integer User Defined must be between 0 and 0xFFFFFFFF"); + } + mIntegerUserDefined[headerID - 0xF0] = (Long)headerValue; + break; + } + throw new IllegalArgumentException("Invalid Header Identifier"); + } + } + + /** + * Retrieves the value of the header identifier provided. The type of the + * Object returned is defined in the description of this interface. + * @param headerID the header identifier whose value is to be returned + * @return the value of the header provided or <code>null</code> if the + * header identifier specified is not part of this + * <code>HeaderSet</code> object + * @throws IllegalArgumentException if the <code>headerID</code> is not one + * defined in this interface or any of the user-defined headers + * @throws IOException if an error occurred in the transport layer during + * the operation or if the connection has been closed + */ + public Object getHeader(int headerID) throws IOException { + + switch (headerID) { + case COUNT: + return mCount; + case NAME: + return mName; + case TYPE: + return mType; + case LENGTH: + return mLength; + case TIME_ISO_8601: + return mIsoTime; + case TIME_4_BYTE: + return mByteTime; + case DESCRIPTION: + return mDescription; + case TARGET: + return mTarget; + case HTTP: + return mHttpHeader; + case WHO: + return mWho; + case CONNECTION_ID: + return mConnectionID; + case OBJECT_CLASS: + return mObjectClass; + case APPLICATION_PARAMETER: + return mAppParam; + case SINGLE_RESPONSE_MODE: + return mSingleResponseMode; + case SINGLE_RESPONSE_MODE_PARAMETER: + return mSrmParam; + default: + // Verify that it was not a Unicode String user Defined + if ((headerID >= 0x30) && (headerID <= 0x3F)) { + return mUnicodeUserDefined[headerID - 0x30]; + } + // Verify that it was not a byte sequence user defined header + if ((headerID >= 0x70) && (headerID <= 0x7F)) { + return mSequenceUserDefined[headerID - 0x70]; + } + // Verify that it was not a byte user defined header + if ((headerID >= 0xB0) && (headerID <= 0xBF)) { + return mByteUserDefined[headerID - 0xB0]; + } + // Verify that it was not a integer user defined header + if ((headerID >= 0xF0) && (headerID <= 0xFF)) { + return mIntegerUserDefined[headerID - 0xF0]; + } + throw new IllegalArgumentException("Invalid Header Identifier"); + } + } + + /** + * Retrieves the list of headers that may be retrieved via the + * <code>getHeader</code> method that will not return <code>null</code>. In + * other words, this method returns all the headers that are available in + * this object. + * @see #getHeader + * @return the array of headers that are set in this object or + * <code>null</code> if no headers are available + * @throws IOException if an error occurred in the transport layer during + * the operation or the connection has been closed + * + * @hide + */ + public int[] getHeaderList() throws IOException { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + + if (mCount != null) { + out.write(COUNT); + } + if (mName != null) { + out.write(NAME); + } + if (mType != null) { + out.write(TYPE); + } + if (mLength != null) { + out.write(LENGTH); + } + if (mIsoTime != null) { + out.write(TIME_ISO_8601); + } + if (mByteTime != null) { + out.write(TIME_4_BYTE); + } + if (mDescription != null) { + out.write(DESCRIPTION); + } + if (mTarget != null) { + out.write(TARGET); + } + if (mHttpHeader != null) { + out.write(HTTP); + } + if (mWho != null) { + out.write(WHO); + } + if (mAppParam != null) { + out.write(APPLICATION_PARAMETER); + } + if (mObjectClass != null) { + out.write(OBJECT_CLASS); + } + if(mSingleResponseMode != null) { + out.write(SINGLE_RESPONSE_MODE); + } + if(mSrmParam != null) { + out.write(SINGLE_RESPONSE_MODE_PARAMETER); + } + + for (int i = 0x30; i < 0x40; i++) { + if (mUnicodeUserDefined[i - 0x30] != null) { + out.write(i); + } + } + + for (int i = 0x70; i < 0x80; i++) { + if (mSequenceUserDefined[i - 0x70] != null) { + out.write(i); + } + } + + for (int i = 0xB0; i < 0xC0; i++) { + if (mByteUserDefined[i - 0xB0] != null) { + out.write(i); + } + } + + for (int i = 0xF0; i < 0x100; i++) { + if (mIntegerUserDefined[i - 0xF0] != null) { + out.write(i); + } + } + + byte[] headers = out.toByteArray(); + out.close(); + + if ((headers == null) || (headers.length == 0)) { + return null; + } + + int[] result = new int[headers.length]; + for (int i = 0; i < headers.length; i++) { + // Convert the byte to a positive integer. That is, an integer + // between 0 and 256. + result[i] = headers[i] & 0xFF; + } + + return result; + } + + /** + * Sets the authentication challenge header. The <code>realm</code> will be + * encoded based upon the default encoding scheme used by the implementation + * to encode strings. Therefore, the encoding scheme used to encode the + * <code>realm</code> is application dependent. + * @param realm a short description that describes what password to use; if + * <code>null</code> no realm will be sent in the authentication + * challenge header + * @param userID if <code>true</code>, a user ID is required in the reply; + * if <code>false</code>, no user ID is required + * @param access if <code>true</code> then full access will be granted if + * successful; if <code>false</code> then read-only access will be + * granted if successful + * @throws IOException + * + * @hide + */ + public void createAuthenticationChallenge(String realm, boolean userID, boolean access) + throws IOException { + + nonce = new byte[16]; + if(mRandom == null) { + mRandom = new SecureRandom(); + } + for (int i = 0; i < 16; i++) { + nonce[i] = (byte)mRandom.nextInt(); + } + + mAuthChall = ObexHelper.computeAuthenticationChallenge(nonce, realm, access, userID); + } + + /** + * Returns the response code received from the server. Response codes are + * defined in the <code>ResponseCodes</code> class. + * @see ResponseCodes + * @return the response code retrieved from the server + * @throws IOException if an error occurred in the transport layer during + * the transaction; if this method is called on a + * <code>HeaderSet</code> object created by calling + * <code>createHeaderSet()</code> in a <code>ClientSession</code> + * object; if this object was created by an OBEX server + */ + public int getResponseCode() throws IOException { + if (responseCode == -1) { + throw new IOException("May not be called on a server"); + } else { + return responseCode; + } + } +} diff --git a/javax/obex/ObexHelper.java b/javax/obex/ObexHelper.java new file mode 100644 index 0000000..30dc8f8 --- /dev/null +++ b/javax/obex/ObexHelper.java @@ -0,0 +1,1100 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * Copyright (C) 2015 Samsung LSI + * 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.util.Log; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +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. + */ +public final class ObexHelper { + + private static final String TAG = "ObexHelper"; + public static final boolean VDBG = false; + /** + * Defines the basic packet length used by OBEX. Every OBEX packet has the + * same basic format:<BR> + * Byte 0: Request or Response Code Byte 1&2: Length of the packet. + */ + public static final int BASE_PACKET_LENGTH = 3; + + /** Prevent object construction of helper class */ + private ObexHelper() { + } + + /** + * The maximum packet size for OBEX packets that this client can handle. At + * present, this must be changed for each port. TODO: The max packet size + * should be the Max incoming MTU minus TODO: L2CAP package headers and + * RFCOMM package headers. TODO: Retrieve the max incoming MTU from TODO: + * LocalDevice.getProperty(). + * NOTE: This value must be larger than or equal to the L2CAP SDU + */ + /* + * android note set as 0xFFFE to match remote MPS + */ + public static final int MAX_PACKET_SIZE_INT = 0xFFFE; + + // The minimum allowed max packet size is 255 according to the OBEX specification + public static final int LOWER_LIMIT_MAX_PACKET_SIZE = 255; + + // The length of OBEX Byte Sequency Header Id according to the OBEX specification + public static final int OBEX_BYTE_SEQ_HEADER_LEN = 0x03; + + /** + * Temporary workaround to be able to push files to Windows 7. + * TODO: Should be removed as soon as Microsoft updates their driver. + */ + public static final int MAX_CLIENT_PACKET_SIZE = 0xFC00; + + public static final int OBEX_OPCODE_FINAL_BIT_MASK = 0x80; + + public static final int OBEX_OPCODE_CONNECT = 0x80; + + public static final int OBEX_OPCODE_DISCONNECT = 0x81; + + public static final int OBEX_OPCODE_PUT = 0x02; + + public static final int OBEX_OPCODE_PUT_FINAL = 0x82; + + public static final int OBEX_OPCODE_GET = 0x03; + + public static final int OBEX_OPCODE_GET_FINAL = 0x83; + + public static final int OBEX_OPCODE_RESERVED = 0x04; + + public static final int OBEX_OPCODE_RESERVED_FINAL = 0x84; + + public static final int OBEX_OPCODE_SETPATH = 0x85; + + public static final int OBEX_OPCODE_ABORT = 0xFF; + + public static final int OBEX_AUTH_REALM_CHARSET_ASCII = 0x00; + + public static final int OBEX_AUTH_REALM_CHARSET_ISO_8859_1 = 0x01; + + public static final int OBEX_AUTH_REALM_CHARSET_ISO_8859_2 = 0x02; + + public static final int OBEX_AUTH_REALM_CHARSET_ISO_8859_3 = 0x03; + + public static final int OBEX_AUTH_REALM_CHARSET_ISO_8859_4 = 0x04; + + public static final int OBEX_AUTH_REALM_CHARSET_ISO_8859_5 = 0x05; + + public static final int OBEX_AUTH_REALM_CHARSET_ISO_8859_6 = 0x06; + + public static final int OBEX_AUTH_REALM_CHARSET_ISO_8859_7 = 0x07; + + public static final int OBEX_AUTH_REALM_CHARSET_ISO_8859_8 = 0x08; + + public static final int OBEX_AUTH_REALM_CHARSET_ISO_8859_9 = 0x09; + + public static final int OBEX_AUTH_REALM_CHARSET_UNICODE = 0xFF; + + public static final byte OBEX_SRM_ENABLE = 0x01; // For BT we only need enable/disable + public static final byte OBEX_SRM_DISABLE = 0x00; + public static final byte OBEX_SRM_SUPPORT = 0x02; // Unused for now + + public static final byte OBEX_SRMP_WAIT = 0x01; // Only SRMP value used by BT + + /** + * Updates the HeaderSet with the headers received in the byte array + * provided. Invalid headers are ignored. + * <P> + * 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. + * <TABLE> + * <TR> + * <TH>Bits 8 and 7</TH> + * <TH>Value</TH> + * <TH>Description</TH> + * </TR> + * <TR> + * <TD>00</TD> + * <TD>0x00</TD> + * <TD>Null Terminated Unicode text, prefixed with 2 byte unsigned integer</TD> + * </TR> + * <TR> + * <TD>01</TD> + * <TD>0x40</TD> + * <TD>Byte Sequence, length prefixed with 2 byte unsigned integer</TD> + * </TR> + * <TR> + * <TD>10</TD> + * <TD>0x80</TD> + * <TD>1 byte quantity</TD> + * </TR> + * <TR> + * <TD>11</TD> + * <TD>0xC0</TD> + * <TD>4 byte quantity - transmitted in network byte order (high byte first</TD> + * </TR> + * </TABLE> + * This method uses the information in this table to determine the type of + * Java object to create and passes that object with the full header to + * setHeader() to update the HeaderSet object. Invalid headers will cause an + * exception to be thrown. When it is thrown, it is ignored. + * @param header the HeaderSet to update + * @param headerArray the byte array containing headers + * @return the result of the last start body or end body header provided; + * the first byte in the result will specify if a body or end of + * body is received + * @throws IOException if an invalid header was found + */ + public static byte[] updateHeaderSet(HeaderSet header, byte[] headerArray) throws IOException { + int index = 0; + int length = 0; + int headerID; + byte[] value = null; + byte[] body = null; + HeaderSet headerImpl = header; + try { + while (index < headerArray.length) { + headerID = 0xFF & headerArray[index]; + switch (headerID & (0xC0)) { + + /* + * 0x00 is a unicode null terminate string with the first + * two bytes after the header identifier being the length + */ + case 0x00: + // Fall through + /* + * 0x40 is a byte sequence with the first + * two bytes after the header identifier being the length + */ + case 0x40: + boolean trimTail = true; + index++; + length = ((0xFF & headerArray[index]) << 8) + + (0xFF & headerArray[index + 1]); + index += 2; + if (length <= OBEX_BYTE_SEQ_HEADER_LEN) { + Log.e(TAG, "Remote sent an OBEX packet with " + + "incorrect header length = " + length); + break; + } + length -= OBEX_BYTE_SEQ_HEADER_LEN; + value = new byte[length]; + System.arraycopy(headerArray, index, value, 0, length); + if (length == 0 || (length > 0 && (value[length - 1] != 0))) { + trimTail = false; + } + switch (headerID) { + case HeaderSet.TYPE: + try { + // Remove trailing null + if (trimTail == false) { + headerImpl.setHeader(headerID, new String(value, 0, + value.length, "ISO8859_1")); + } else { + headerImpl.setHeader(headerID, new String(value, 0, + value.length - 1, "ISO8859_1")); + } + } catch (UnsupportedEncodingException e) { + throw e; + } + break; + + case HeaderSet.AUTH_CHALLENGE: + headerImpl.mAuthChall = new byte[length]; + System.arraycopy(headerArray, index, headerImpl.mAuthChall, 0, + length); + break; + + case HeaderSet.AUTH_RESPONSE: + headerImpl.mAuthResp = new byte[length]; + System.arraycopy(headerArray, index, headerImpl.mAuthResp, 0, + length); + break; + + case HeaderSet.BODY: + /* Fall Through */ + case HeaderSet.END_OF_BODY: + body = new byte[length + 1]; + body[0] = (byte)headerID; + System.arraycopy(headerArray, index, body, 1, length); + break; + + case HeaderSet.TIME_ISO_8601: + try { + String dateString = new String(value, "ISO8859_1"); + Calendar temp = Calendar.getInstance(); + if ((dateString.length() == 16) + && (dateString.charAt(15) == 'Z')) { + temp.setTimeZone(TimeZone.getTimeZone("UTC")); + } + temp.set(Calendar.YEAR, Integer.parseInt(dateString.substring( + 0, 4))); + temp.set(Calendar.MONTH, Integer.parseInt(dateString.substring( + 4, 6))); + temp.set(Calendar.DAY_OF_MONTH, Integer.parseInt(dateString + .substring(6, 8))); + temp.set(Calendar.HOUR_OF_DAY, Integer.parseInt(dateString + .substring(9, 11))); + temp.set(Calendar.MINUTE, Integer.parseInt(dateString + .substring(11, 13))); + temp.set(Calendar.SECOND, Integer.parseInt(dateString + .substring(13, 15))); + headerImpl.setHeader(HeaderSet.TIME_ISO_8601, temp); + } catch (UnsupportedEncodingException e) { + throw e; + } + break; + + default: + if ((headerID & 0xC0) == 0x00) { + headerImpl.setHeader(headerID, ObexHelper.convertToUnicode( + value, true)); + } else { + headerImpl.setHeader(headerID, value); + } + } + + index += length; + break; + + /* + * 0x80 is a byte header. The only valid byte headers are + * the 16 user defined byte headers. + */ + case 0x80: + index++; + try { + headerImpl.setHeader(headerID, Byte.valueOf(headerArray[index])); + } catch (Exception e) { + // Not a valid header so ignore + } + index++; + break; + + /* + * 0xC0 is a 4 byte unsigned integer header and with the + * exception of TIME_4_BYTE will be converted to a Long + * and added. + */ + case 0xC0: + index++; + value = new byte[4]; + System.arraycopy(headerArray, index, value, 0, 4); + try { + if (headerID != HeaderSet.TIME_4_BYTE) { + // Determine if it is a connection ID. These + // need to be handled differently + if (headerID == HeaderSet.CONNECTION_ID) { + headerImpl.mConnectionID = new byte[4]; + System.arraycopy(value, 0, headerImpl.mConnectionID, 0, 4); + } else { + headerImpl.setHeader(headerID, Long + .valueOf(convertToLong(value))); + } + } else { + Calendar temp = Calendar.getInstance(); + temp.setTime(new Date(convertToLong(value) * 1000L)); + headerImpl.setHeader(HeaderSet.TIME_4_BYTE, temp); + } + } catch (Exception e) { + // Not a valid header so ignore + throw new IOException("Header was not formatted properly", e); + } + index += 4; + break; + } + + } + } catch (IOException e) { + throw new IOException("Header was not formatted properly", e); + } + + return body; + } + + /** + * Creates the header part of OBEX packet based on the header provided. + * TODO: Could use getHeaderList() to get the array of headers to include + * and then use the high two bits to determine the type of the object + * and construct the byte array from that. This will make the size smaller. + * @param head the header used to construct the byte array + * @param nullOut <code>true</code> if the header should be set to + * <code>null</code> once it is added to the array or + * <code>false</code> 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(); + 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.mConnectionID != null) + && (headImpl.getHeader(HeaderSet.TARGET) == null)) { + + out.write((byte)HeaderSet.CONNECTION_ID); + out.write(headImpl.mConnectionID); + } + + // 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)(0xFF & (length >> 8)); + lengthArray[1] = (byte)(0xFF & length); + out.write(lengthArray); + out.write(value); + if (nullOut) { + headImpl.setHeader(HeaderSet.NAME, null); + } + } else if (headImpl.getEmptyNameHeader()) { + out.write((byte) HeaderSet.NAME); + lengthArray[0] = (byte) 0x00; + lengthArray[1] = (byte) 0x03; + out.write(lengthArray); + } + + // 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 e; + } + + 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 e; + } + + 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.mAuthChall != null) { + out.write((byte)HeaderSet.AUTH_CHALLENGE); + length = headImpl.mAuthChall.length + 3; + lengthArray[0] = (byte)(255 & (length >> 8)); + lengthArray[1] = (byte)(255 & length); + out.write(lengthArray); + out.write(headImpl.mAuthChall); + if (nullOut) { + headImpl.mAuthChall = null; + } + } + + // Add the authentication response header + if (headImpl.mAuthResp != null) { + out.write((byte)HeaderSet.AUTH_RESPONSE); + length = headImpl.mAuthResp.length + 3; + lengthArray[0] = (byte)(255 & (length >> 8)); + lengthArray[1] = (byte)(255 & length); + out.write(lengthArray); + out.write(headImpl.mAuthResp); + if (nullOut) { + headImpl.mAuthResp = null; + } + } + + // TODO: + // If the SRM and SRMP header is in use, they must be send in the same OBEX packet + // But the current structure of the obex code cannot handle this, and therefore + // it makes sense to put them in the tail of the headers, since we then reduce the + // chance of enabling SRM to soon. The down side is that SRM cannot be used while + // transferring non-body headers + + // Add the SRM header + byteHeader = (Byte)headImpl.getHeader(HeaderSet.SINGLE_RESPONSE_MODE); + if (byteHeader != null) { + out.write((byte)HeaderSet.SINGLE_RESPONSE_MODE); + out.write(byteHeader.byteValue()); + if (nullOut) { + headImpl.setHeader(HeaderSet.SINGLE_RESPONSE_MODE, null); + } + } + + // Add the SRM parameter header + byteHeader = (Byte)headImpl.getHeader(HeaderSet.SINGLE_RESPONSE_MODE_PARAMETER); + if (byteHeader != null) { + out.write((byte)HeaderSet.SINGLE_RESPONSE_MODE_PARAMETER); + out.write(byteHeader.byteValue()); + if (nullOut) { + headImpl.setHeader(HeaderSet.SINGLE_RESPONSE_MODE_PARAMETER, 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; + + // TODO: Ensure SRM and SRMP headers are not split into two OBEX packets + + 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 than 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 + * @throws IllegalArgumentException if the byte array has an odd length + */ + public static String convertToUnicode(byte[] b, boolean includesNull) { + if (b == null || b.length == 0) { + 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; + } + // If upper and lower both equal 0, it should be the end of string. + // Ignore left bytes from array to avoid potential issues + if (upper == 0 && lower == 0) { + return new String(c, 0, i); + } + + 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) { + try { + MessageDigest md5 = MessageDigest.getInstance("MD5"); + return md5.digest(in); + } catch (NoSuchAlgorithmException e) { + throw new RuntimeException(e); + } + } + + /** + * 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 <code>true</code> then full access will be granted if + * successful; if <code>false</code> then read only access will be + * granted if successful + * @param userID if <code>true</code>, a user ID is required in the reply; + * if <code>false</code>, no user ID is required + * @throws IllegalArgumentException if the challenge is not 16 bytes long; + * if the realm can not be encoded in less than 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 than 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; + } + + /** + * Return the maximum allowed OBEX packet to transmit. + * OBEX packets transmitted must be smaller than this value. + * @param transport Reference to the ObexTransport in use. + * @return the maximum allowed OBEX packet to transmit + */ + public static int getMaxTxPacketSize(ObexTransport transport) { + int size = transport.getMaxTransmitPacketSize(); + return validateMaxPacketSize(size); + } + + /** + * Return the maximum allowed OBEX packet to receive - used in OBEX connect. + * @param transport + * @return the maximum allowed OBEX packet to receive + */ + public static int getMaxRxPacketSize(ObexTransport transport) { + int size = transport.getMaxReceivePacketSize(); + return validateMaxPacketSize(size); + } + + private static int validateMaxPacketSize(int size) { + if (VDBG && (size > MAX_PACKET_SIZE_INT)) { + Log.w(TAG, "The packet size supported for the connection (" + size + ") is larger" + + " than the configured OBEX packet size: " + MAX_PACKET_SIZE_INT); + } + if (size != -1 && size < MAX_PACKET_SIZE_INT) { + if (size < LOWER_LIMIT_MAX_PACKET_SIZE) { + throw new IllegalArgumentException(size + " is less that the lower limit: " + + LOWER_LIMIT_MAX_PACKET_SIZE); + } + return size; + } + return MAX_PACKET_SIZE_INT; + } +} diff --git a/javax/obex/ObexPacket.java b/javax/obex/ObexPacket.java new file mode 100644 index 0000000..bb6c96e --- /dev/null +++ b/javax/obex/ObexPacket.java @@ -0,0 +1,71 @@ +/* + * Copyright (c) 2015 The Android Open Source Project + * Copyright (c) 2015 Samsung LSI + * + * 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. + */ + +package javax.obex; + +import java.io.IOException; +import java.io.InputStream; + +public class ObexPacket { + public int mHeaderId; + public int mLength; + public byte[] mPayload = null; + + private ObexPacket(int headerId, int length) { + mHeaderId = headerId; + mLength = length; + } + + /** + * Create a complete OBEX packet by reading data from an InputStream. + * @param is the input stream to read from. + * @return the OBEX packet read. + * @throws IOException if an IO exception occurs during read. + */ + public static ObexPacket read(InputStream is) throws IOException { + int headerId = is.read(); + return read(headerId, is); + } + + /** + * Read the remainder of an OBEX packet, with a specified headerId. + * @param headerId the headerId already read from the stream. + * @param is the stream to read from, assuming 1 byte have already been read. + * @return the OBEX packet read. + * @throws IOException + */ + public static ObexPacket read(int headerId, InputStream is) throws IOException { + // Read the 2 byte length field from the stream + int length = is.read(); + length = (length << 8) + is.read(); + + ObexPacket newPacket = new ObexPacket(headerId, length); + + int bytesReceived; + byte[] temp = null; + if (length > 3) { + // First three bytes already read, compensating for this + temp = new byte[length - 3]; + bytesReceived = is.read(temp); + while (bytesReceived != temp.length) { + bytesReceived += is.read(temp, bytesReceived, temp.length - bytesReceived); + } + } + newPacket.mPayload = temp; + return newPacket; + } +} diff --git a/javax/obex/ObexSession.java b/javax/obex/ObexSession.java new file mode 100644 index 0000000..373e573 --- /dev/null +++ b/javax/obex/ObexSession.java @@ -0,0 +1,223 @@ +/* + * 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.util.Log; + +import java.io.IOException; + +/** + * The <code>ObexSession</code> interface characterizes the term + * "OBEX Connection" as defined in the IrDA Object Exchange Protocol v1.2, which + * could be the server-side view of an OBEX connection, or the client-side view + * of the same connection, which is established by server's accepting of a + * client issued "CONNECT". + * <P> + * This interface serves as the common super class for + * <CODE>ClientSession</CODE> and <CODE>ServerSession</CODE>. + */ +public class ObexSession { + + private static final String TAG = "ObexSession"; + private static final boolean V = ObexHelper.VDBG; + + protected Authenticator mAuthenticator; + + protected byte[] mChallengeDigest; + + /** + * Called when the server received an authentication challenge header. This + * will cause the authenticator to handle the authentication challenge. + * @param header the header with the authentication challenge + * @return <code>true</code> if the last request should be resent; + * <code>false</code> if the last request should not be resent + * @throws IOException + */ + public boolean handleAuthChall(HeaderSet header) throws IOException { + if (mAuthenticator == null) { + return false; + } + + /* + * An authentication challenge is made up of one required and two + * optional tag length value triplets. The tag 0x00 is required to be in + * the authentication challenge and it represents the challenge digest + * that was received. The tag 0x01 is the options tag. This tag tracks + * if user ID is required and if full access will be granted. The tag + * 0x02 is the realm, which provides a description of which user name + * and password to use. + */ + byte[] challenge = ObexHelper.getTagValue((byte)0x00, header.mAuthChall); + byte[] option = ObexHelper.getTagValue((byte)0x01, header.mAuthChall); + byte[] description = ObexHelper.getTagValue((byte)0x02, header.mAuthChall); + + String realm = null; + if (description != null) { + byte[] realmString = new byte[description.length - 1]; + System.arraycopy(description, 1, realmString, 0, realmString.length); + + switch (description[0] & 0xFF) { + + case ObexHelper.OBEX_AUTH_REALM_CHARSET_ASCII: + // ASCII encoding + // Fall through + case ObexHelper.OBEX_AUTH_REALM_CHARSET_ISO_8859_1: + // ISO-8859-1 encoding + try { + realm = new String(realmString, "ISO8859_1"); + } catch (Exception e) { + throw new IOException("Unsupported Encoding Scheme"); + } + break; + + case ObexHelper.OBEX_AUTH_REALM_CHARSET_UNICODE: + // UNICODE Encoding + realm = ObexHelper.convertToUnicode(realmString, false); + break; + + default: + throw new IOException("Unsupported Encoding Scheme"); + } + } + + boolean isUserIDRequired = false; + boolean isFullAccess = true; + if (option != null) { + if ((option[0] & 0x01) != 0) { + isUserIDRequired = true; + } + + if ((option[0] & 0x02) != 0) { + isFullAccess = false; + } + } + + PasswordAuthentication result = null; + header.mAuthChall = null; + + try { + result = mAuthenticator + .onAuthenticationChallenge(realm, isUserIDRequired, isFullAccess); + } catch (Exception e) { + if (V) Log.d(TAG, "Exception occurred - returning false", e); + return false; + } + + /* + * If no password is provided then we not resent the request + */ + if (result == null) { + return false; + } + + byte[] password = result.getPassword(); + if (password == null) { + return false; + } + + byte[] userName = result.getUserName(); + + /* + * Create the authentication response header. It includes 1 required and + * 2 option tag length value triples. The required triple has a tag of + * 0x00 and is the response digest. The first optional tag is 0x01 and + * represents the user ID. If no user ID is provided, then no user ID + * will be sent. The second optional tag is 0x02 and is the challenge + * that was received. This will always be sent + */ + if (userName != null) { + header.mAuthResp = new byte[38 + userName.length]; + header.mAuthResp[36] = (byte)0x01; + header.mAuthResp[37] = (byte)userName.length; + System.arraycopy(userName, 0, header.mAuthResp, 38, userName.length); + } else { + header.mAuthResp = new byte[36]; + } + + // Create the secret String + byte[] digest = new byte[challenge.length + password.length + 1]; + System.arraycopy(challenge, 0, digest, 0, challenge.length); + // Insert colon between challenge and password + digest[challenge.length] = (byte)0x3A; + System.arraycopy(password, 0, digest, challenge.length + 1, password.length); + + // Add the Response Digest + header.mAuthResp[0] = (byte)0x00; + header.mAuthResp[1] = (byte)0x10; + + System.arraycopy(ObexHelper.computeMd5Hash(digest), 0, header.mAuthResp, 2, 16); + + // Add the challenge + header.mAuthResp[18] = (byte)0x02; + header.mAuthResp[19] = (byte)0x10; + System.arraycopy(challenge, 0, header.mAuthResp, 20, 16); + + return true; + } + + /** + * Called when the server received an authentication response header. This + * will cause the authenticator to handle the authentication response. + * @param authResp the authentication response + * @return <code>true</code> if the response passed; <code>false</code> if + * the response failed + */ + public boolean handleAuthResp(byte[] authResp) { + if (mAuthenticator == null) { + return false; + } + // get the correct password from the application + byte[] correctPassword = mAuthenticator.onAuthenticationResponse(ObexHelper.getTagValue( + (byte)0x01, authResp)); + if (correctPassword == null) { + return false; + } + + byte[] temp = new byte[correctPassword.length + 16]; + + System.arraycopy(mChallengeDigest, 0, temp, 0, 16); + System.arraycopy(correctPassword, 0, temp, 16, correctPassword.length); + + byte[] correctResponse = ObexHelper.computeMd5Hash(temp); + byte[] actualResponse = ObexHelper.getTagValue((byte)0x00, authResp); + + // compare the MD5 hash array . + for (int i = 0; i < 16; i++) { + if (correctResponse[i] != actualResponse[i]) { + return false; + } + } + + return true; + } +} diff --git a/javax/obex/ObexTransport.java b/javax/obex/ObexTransport.java new file mode 100644 index 0000000..cb16c17 --- /dev/null +++ b/javax/obex/ObexTransport.java @@ -0,0 +1,112 @@ +/* + * 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 java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +/** + * The <code>ObexTransport</code> interface defines the underlying transport + * connection which carries the OBEX protocol( such as TCP, RFCOMM device file + * exposed by Bluetooth or USB in kernel, RFCOMM socket emulated in Android + * platform, Irda). This interface provides an abstract layer to be used by the + * <code>ObexConnection</code>. Each kind of medium shall have its own + * implementation to wrap and follow the same interface. + * <P> + * See section 1.2.2 of IrDA Object Exchange Protocol specification. + * <P> + * Different kind of medium may have different construction - for example, the + * RFCOMM device file medium may be constructed from a file descriptor or simply + * a string while the TCP medium usually from a socket. + */ +public interface ObexTransport { + + void create() throws IOException; + + void listen() throws IOException; + + void close() throws IOException; + + void connect() throws IOException; + + void disconnect() throws IOException; + + InputStream openInputStream() throws IOException; + + OutputStream openOutputStream() throws IOException; + + DataInputStream openDataInputStream() throws IOException; + + DataOutputStream openDataOutputStream() throws IOException; + + /** + * Must return the maximum allowed OBEX packet that can be sent over + * the transport. For L2CAP this will be the Max SDU reported by the + * peer device. + * The returned value will be used to set the outgoing OBEX packet + * size. Therefore this value shall not change. + * For RFCOMM or other transport types where the OBEX packets size + * is unrelated to the transport packet size, return -1; + * Exception can be made (like PBAP transport) with a smaller value + * to avoid bad effect on other profiles using the RFCOMM; + * @return the maximum allowed OBEX packet that can be send over + * the transport. Or -1 in case of don't care. + */ + int getMaxTransmitPacketSize(); + + /** + * Must return the maximum allowed OBEX packet that can be received over + * the transport. For L2CAP this will be the Max SDU configured for the + * L2CAP channel. + * The returned value will be used to validate the incoming packet size + * values. + * For RFCOMM or other transport types where the OBEX packets size + * is unrelated to the transport packet size, return -1; + * @return the maximum allowed OBEX packet that can be send over + * the transport. Or -1 in case of don't care. + */ + int getMaxReceivePacketSize(); + + /** + * Shall return true if the transport in use supports SRM. + * @return + * <code>true</code> if SRM operation is supported, and is to be enabled. + * <code>false</code> if SRM operations are not supported, or should not be used. + */ + boolean isSrmSupported(); + + +} diff --git a/javax/obex/Operation.java b/javax/obex/Operation.java new file mode 100644 index 0000000..1f6c20a --- /dev/null +++ b/javax/obex/Operation.java @@ -0,0 +1,167 @@ +/* + * 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 java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +/** + * The <code>Operation</code> interface provides ways to manipulate a single + * OBEX PUT or GET operation. The implementation of this interface sends OBEX + * packets as they are built. If during the operation the peer in the operation + * ends the operation, an <code>IOException</code> is thrown on the next read + * from the input stream, write to the output stream, or call to + * <code>sendHeaders()</code>. + * <P> + * <STRONG>How Headers are Handled</STRONG> + * <P> + * As headers are received, they may be retrieved through the + * <code>getReceivedHeaders()</code> method. If new headers are set during the + * operation, the new headers will be sent during the next packet exchange. + * <P> + * <STRONG>PUT example</STRONG> + * <P> + * <PRE> + * void putObjectViaOBEX(ClientSession conn, HeaderSet head, byte[] obj) throws IOException { + * // Include the length header + * head.setHeader(head.LENGTH, new Long(obj.length)); + * // Initiate the PUT request + * Operation op = conn.put(head); + * // Open the output stream to put the object to it + * DataOutputStream out = op.openDataOutputStream(); + * // Send the object to the server + * out.write(obj); + * // End the transaction + * out.close(); + * op.close(); + * } + * </PRE> + * <P> + * <STRONG>GET example</STRONG> + * <P> + * <PRE> + * byte[] getObjectViaOBEX(ClientSession conn, HeaderSet head) throws IOException { + * // Send the initial GET request to the server + * Operation op = conn.get(head); + * // Retrieve the length of the object being sent back + * int length = op.getLength(); + * // Create space for the object + * byte[] obj = new byte[length]; + * // Get the object from the input stream + * DataInputStream in = trans.openDataInputStream(); + * in.read(obj); + * // End the transaction + * in.close(); + * op.close(); + * return obj; + * } + * </PRE> + * + * <H3>Client PUT Operation Flow</H3> For PUT operations, a call to + * <code>close()</code> the <code>OutputStream</code> returned from + * <code>openOutputStream()</code> or <code>openDataOutputStream()</code> will + * signal that the request is done. (In OBEX terms, the End-Of-Body header + * should be sent and the final bit in the request will be set.) At this point, + * the reply from the server may begin to be processed. A call to + * <code>getResponseCode()</code> will do an implicit close on the + * <code>OutputStream</code> and therefore signal that the request is done. + * <H3>Client GET Operation Flow</H3> For GET operation, a call to + * <code>openInputStream()</code> or <code>openDataInputStream()</code> will + * signal that the request is done. (In OBEX terms, the final bit in the request + * will be set.) A call to <code>getResponseCode()</code> will cause an implicit + * close on the <code>InputStream</code>. No further data may be read at this + * point. + */ +public interface Operation { + + /** + * Sends an ABORT message to the server. By calling this method, the + * corresponding input and output streams will be closed along with this + * object. No headers are sent in the abort request. This will end the + * operation since <code>close()</code> will be called by this method. + * @throws IOException if the transaction has already ended or if an OBEX + * server calls this method + */ + void abort() throws IOException; + + /** + * Returns the headers that have been received during the operation. + * Modifying the object returned has no effect on the headers that are sent + * or retrieved. + * @return the headers received during this <code>Operation</code> + * @throws IOException if this <code>Operation</code> has been closed + */ + HeaderSet getReceivedHeader() throws IOException; + + /** + * Specifies the headers that should be sent in the next OBEX message that + * is sent. + * @param headers the headers to send in the next message + * @throws IOException if this <code>Operation</code> has been closed or the + * transaction has ended and no further messages will be exchanged + * @throws IllegalArgumentException if <code>headers</code> was not created + * by a call to <code>ServerRequestHandler.createHeaderSet()</code> + * or <code>ClientSession.createHeaderSet()</code> + * @throws NullPointerException if <code>headers</code> if <code>null</code> + */ + void sendHeaders(HeaderSet headers) throws IOException; + + /** + * Returns the response code received from the server. Response codes are + * defined in the <code>ResponseCodes</code> class. + * @see ResponseCodes + * @return the response code retrieved from the server + * @throws IOException if an error occurred in the transport layer during + * the transaction; if this object was created by an OBEX server + */ + int getResponseCode() throws IOException; + + int getHeaderLength(); + + InputStream openInputStream() throws IOException; + + DataInputStream openDataInputStream() throws IOException; + + OutputStream openOutputStream() throws IOException; + + DataOutputStream openDataOutputStream() throws IOException; + + void close() throws IOException; + + int getMaxPacketSize(); + + public void noBodyHeader(); +} diff --git a/javax/obex/PasswordAuthentication.java b/javax/obex/PasswordAuthentication.java new file mode 100644 index 0000000..cb2303e --- /dev/null +++ b/javax/obex/PasswordAuthentication.java @@ -0,0 +1,82 @@ +/* + * 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; + +/** + * This class holds user name and password combinations. + */ +public final class PasswordAuthentication { + + private byte[] mUserName; + + private final byte[] mPassword; + + /** + * Creates a new <code>PasswordAuthentication</code> with the user name and + * password provided. + * @param userName the user name to include; this may be <code>null</code> + * @param password the password to include in the response + * @throws NullPointerException if <code>password</code> is + * <code>null</code> + */ + public PasswordAuthentication(final byte[] userName, final byte[] password) { + if (userName != null) { + mUserName = new byte[userName.length]; + System.arraycopy(userName, 0, mUserName, 0, userName.length); + } + + mPassword = new byte[password.length]; + System.arraycopy(password, 0, mPassword, 0, password.length); + } + + /** + * Retrieves the user name that was specified in the constructor. The user + * name may be <code>null</code>. + * @return the user name + * + * @hide + */ + public byte[] getUserName() { + return mUserName; + } + + /** + * Retrieves the password. + * @return the password + * + * @hide + */ + public byte[] getPassword() { + return mPassword; + } +} diff --git a/javax/obex/PrivateInputStream.java b/javax/obex/PrivateInputStream.java new file mode 100644 index 0000000..18219f9 --- /dev/null +++ b/javax/obex/PrivateInputStream.java @@ -0,0 +1,180 @@ +/* + * 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 java.io.IOException; +import java.io.InputStream; + +/** + * This object provides an input stream to the Operation objects used in this + * package. + */ +public final class PrivateInputStream extends InputStream { + + private BaseStream mParent; + + private byte[] mData; + + private int mIndex; + + private boolean mOpen; + + /** + * Creates an input stream for the <code>Operation</code> to read from + * @param p the connection this input stream is for + */ + public PrivateInputStream(BaseStream p) { + mParent = p; + mData = new byte[0]; + mIndex = 0; + mOpen = true; + } + + /** + * Returns the number of bytes that can be read (or skipped over) from this + * input stream without blocking by the next caller of a method for this + * input stream. The next caller might be the same thread or or another + * thread. + * @return the number of bytes that can be read from this input stream + * without blocking + * @throws IOException if an I/O error occurs + */ + @Override + public synchronized int available() throws IOException { + ensureOpen(); + return mData.length - mIndex; + } + + /** + * Reads the next byte of data from the input stream. The value byte is + * returned as an int in the range 0 to 255. If no byte is available because + * the end of the stream has been reached, the value -1 is returned. This + * method blocks until input data is available, the end of the stream is + * detected, or an exception is thrown. + * @return the byte read from the input stream or -1 if it reaches the end of + * stream + * @throws IOException if an I/O error occurs + */ + @Override + public synchronized int read() throws IOException { + ensureOpen(); + while (mData.length == mIndex) { + if (!mParent.continueOperation(true, true)) { + return -1; + } + } + return (mData[mIndex++] & 0xFF); + } + + @Override + public int read(byte[] b) throws IOException { + return read(b, 0, b.length); + } + + @Override + public synchronized int read(byte[] b, int offset, int length) throws IOException { + + if (b == null) { + throw new IOException("buffer is null"); + } + if ((offset | length) < 0 || length > b.length - offset) { + throw new ArrayIndexOutOfBoundsException("index outof bound"); + } + ensureOpen(); + + int currentDataLength = mData.length - mIndex; + int remainReadLength = length; + int offset1 = offset; + int result = 0; + + while (currentDataLength <= remainReadLength) { + System.arraycopy(mData, mIndex, b, offset1, currentDataLength); + mIndex += currentDataLength; + offset1 += currentDataLength; + result += currentDataLength; + remainReadLength -= currentDataLength; + + if (!mParent.continueOperation(true, true)) { + return result == 0 ? -1 : result; + } + currentDataLength = mData.length - mIndex; + } + if (remainReadLength > 0) { + System.arraycopy(mData, mIndex, b, offset1, remainReadLength); + mIndex += remainReadLength; + result += remainReadLength; + } + return result; + } + + /** + * Allows the <code>OperationImpl</code> thread to add body data to the + * input stream. + * @param body the data to add to the stream + * @param start the start of the body to array to copy + */ + public synchronized void writeBytes(byte[] body, int start) { + + int length = (body.length - start) + (mData.length - mIndex); + byte[] temp = new byte[length]; + + System.arraycopy(mData, mIndex, temp, 0, mData.length - mIndex); + System.arraycopy(body, start, temp, mData.length - mIndex, body.length - start); + + mData = temp; + mIndex = 0; + notifyAll(); + } + + /** + * Verifies that this stream is open + * @throws IOException if the stream is not open + */ + private void ensureOpen() throws IOException { + mParent.ensureOpen(); + if (!mOpen) { + throw new IOException("Input stream is closed"); + } + } + + /** + * Closes the input stream. If the input stream is already closed, do + * nothing. + * @throws IOException this will never happen + */ + @Override + public void close() throws IOException { + mOpen = false; + mParent.streamClosed(true); + } +} diff --git a/javax/obex/PrivateOutputStream.java b/javax/obex/PrivateOutputStream.java new file mode 100644 index 0000000..e8bfd1c --- /dev/null +++ b/javax/obex/PrivateOutputStream.java @@ -0,0 +1,171 @@ +/* + * 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 java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.OutputStream; + +/** + * This object provides an output stream to the Operation objects used in this + * package. + */ +public final class PrivateOutputStream extends OutputStream { + + private BaseStream mParent; + + private ByteArrayOutputStream mArray; + + private boolean mOpen; + + private int mMaxPacketSize; + + /** + * Creates an empty <code>PrivateOutputStream</code> to write to. + * @param p the connection that this stream runs over + */ + public PrivateOutputStream(BaseStream p, int maxSize) { + mParent = p; + mArray = new ByteArrayOutputStream(); + mMaxPacketSize = maxSize; + mOpen = true; + } + + /** + * Determines how many bytes have been written to the output stream. + * @return the number of bytes written to the output stream + */ + public int size() { + return mArray.size(); + } + + /** + * Writes the specified byte to this output stream. The general contract for + * write is that one byte is written to the output stream. The byte to be + * written is the eight low-order bits of the argument b. The 24 high-order + * bits of b are ignored. + * @param b the byte to write + * @throws IOException if an I/O error occurs + */ + @Override + public synchronized void write(int b) throws IOException { + ensureOpen(); + mParent.ensureNotDone(); + mArray.write(b); + if (mArray.size() == mMaxPacketSize) { + mParent.continueOperation(true, false); + } + } + + @Override + public void write(byte[] buffer) throws IOException { + write(buffer, 0, buffer.length); + } + + @Override + public synchronized void write(byte[] buffer, int offset, int count) throws IOException { + int offset1 = offset; + int remainLength = count; + + if (buffer == null) { + throw new IOException("buffer is null"); + } + if ((offset | count) < 0 || count > buffer.length - offset) { + throw new IndexOutOfBoundsException("index outof bound"); + } + + ensureOpen(); + mParent.ensureNotDone(); + while ((mArray.size() + remainLength) >= mMaxPacketSize) { + int bufferLeft = mMaxPacketSize - mArray.size(); + mArray.write(buffer, offset1, bufferLeft); + offset1 += bufferLeft; + remainLength -= bufferLeft; + mParent.continueOperation(true, false); + } + if (remainLength > 0) { + mArray.write(buffer, offset1, remainLength); + } + } + + /** + * Reads the bytes that have been written to this stream. + * @param size the size of the array to return + * @return the byte array that is written + */ + public synchronized byte[] readBytes(int size) { + if (mArray.size() > 0) { + byte[] temp = mArray.toByteArray(); + mArray.reset(); + byte[] result = new byte[size]; + System.arraycopy(temp, 0, result, 0, size); + if (temp.length != size) { + mArray.write(temp, size, temp.length - size); + } + return result; + } else { + return null; + } + } + + /** + * Verifies that this stream is open + * @throws IOException if the stream is not open + */ + private void ensureOpen() throws IOException { + mParent.ensureOpen(); + if (!mOpen) { + throw new IOException("Output stream is closed"); + } + } + + /** + * Closes the output stream. If the input stream is already closed, do + * nothing. + * @throws IOException this will never happen + */ + @Override + public void close() throws IOException { + mOpen = false; + mParent.streamClosed(false); + } + + /** + * Determines if the connection is closed + * @return <code>true</code> if the connection is closed; <code>false</code> + * if the connection is open + */ + public boolean isClosed() { + return !mOpen; + } +} diff --git a/javax/obex/ResponseCodes.java b/javax/obex/ResponseCodes.java new file mode 100644 index 0000000..a46490d --- /dev/null +++ b/javax/obex/ResponseCodes.java @@ -0,0 +1,377 @@ +/* + * 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; + +/** + * The <code>ResponseCodes</code> class contains the list of valid response + * codes a server may send to a client. + * <P> + * <STRONG>IMPORTANT NOTE</STRONG> + * <P> + * The values in this interface represent the values defined in the IrOBEX + * specification, which is different with the HTTP specification. + * <P> + * <code>OBEX_DATABASE_FULL</code> and <code>OBEX_DATABASE_LOCKED</code> require + * further description since they are not defined in HTTP. The server will send + * an <code>OBEX_DATABASE_FULL</code> message when the client requests that + * something be placed into a database but the database is full (cannot take + * more data). <code>OBEX_DATABASE_LOCKED</code> will be returned when the + * client wishes to access a database, database table, or database record that + * has been locked. + */ +public final class ResponseCodes { + + /** + * Defines the OBEX CONTINUE response code. + * <P> + * The value of <code>OBEX_HTTP_CONTINUE</code> is 0x90 (144). + */ + public static final int OBEX_HTTP_CONTINUE = 0x90; + + /** + * Defines the OBEX SUCCESS response code. + * <P> + * The value of <code>OBEX_HTTP_OK</code> is 0xA0 (160). + */ + public static final int OBEX_HTTP_OK = 0xA0; + + /** + * Defines the OBEX CREATED response code. + * <P> + * The value of <code>OBEX_HTTP_CREATED</code> is 0xA1 (161). + * + * @hide + */ + public static final int OBEX_HTTP_CREATED = 0xA1; + + /** + * Defines the OBEX ACCEPTED response code. + * <P> + * The value of <code>OBEX_HTTP_ACCEPTED</code> is 0xA2 (162). + * + * @hide + */ + public static final int OBEX_HTTP_ACCEPTED = 0xA2; + + /** + * Defines the OBEX NON-AUTHORITATIVE INFORMATION response code. + * <P> + * The value of <code>OBEX_HTTP_NOT_AUTHORITATIVE</code> is 0xA3 (163). + * + * @hide + */ + public static final int OBEX_HTTP_NOT_AUTHORITATIVE = 0xA3; + + /** + * Defines the OBEX NO CONTENT response code. + * <P> + * The value of <code>OBEX_HTTP_NO_CONTENT</code> is 0xA4 (164). + * + * @hide + */ + public static final int OBEX_HTTP_NO_CONTENT = 0xA4; + + /** + * Defines the OBEX RESET CONTENT response code. + * <P> + * The value of <code>OBEX_HTTP_RESET</code> is 0xA5 (165). + * + * @hide + */ + public static final int OBEX_HTTP_RESET = 0xA5; + + /** + * Defines the OBEX PARTIAL CONTENT response code. + * <P> + * The value of <code>OBEX_HTTP_PARTIAL</code> is 0xA6 (166). + * + * @hide + */ + public static final int OBEX_HTTP_PARTIAL = 0xA6; + + /** + * Defines the OBEX MULTIPLE_CHOICES response code. + * <P> + * The value of <code>OBEX_HTTP_MULT_CHOICE</code> is 0xB0 (176). + * + * @hide + */ + public static final int OBEX_HTTP_MULT_CHOICE = 0xB0; + + /** + * Defines the OBEX MOVED PERMANENTLY response code. + * <P> + * The value of <code>OBEX_HTTP_MOVED_PERM</code> is 0xB1 (177). + * + * @hide + */ + public static final int OBEX_HTTP_MOVED_PERM = 0xB1; + + /** + * Defines the OBEX MOVED TEMPORARILY response code. + * <P> + * The value of <code>OBEX_HTTP_MOVED_TEMP</code> is 0xB2 (178). + * + * @hide + */ + public static final int OBEX_HTTP_MOVED_TEMP = 0xB2; + + /** + * Defines the OBEX SEE OTHER response code. + * <P> + * The value of <code>OBEX_HTTP_SEE_OTHER</code> is 0xB3 (179). + * + * @hide + */ + public static final int OBEX_HTTP_SEE_OTHER = 0xB3; + + /** + * Defines the OBEX NOT MODIFIED response code. + * <P> + * The value of <code>OBEX_HTTP_NOT_MODIFIED</code> is 0xB4 (180). + * + * @hide + */ + public static final int OBEX_HTTP_NOT_MODIFIED = 0xB4; + + /** + * Defines the OBEX USE PROXY response code. + * <P> + * The value of <code>OBEX_HTTP_USE_PROXY</code> is 0xB5 (181). + * + * @hide + */ + public static final int OBEX_HTTP_USE_PROXY = 0xB5; + + /** + * Defines the OBEX BAD REQUEST response code. + * <P> + * The value of <code>OBEX_HTTP_BAD_REQUEST</code> is 0xC0 (192). + */ + public static final int OBEX_HTTP_BAD_REQUEST = 0xC0; + + /** + * Defines the OBEX UNAUTHORIZED response code. + * <P> + * The value of <code>OBEX_HTTP_UNAUTHORIZED</code> is 0xC1 (193). + * + * @hide + */ + public static final int OBEX_HTTP_UNAUTHORIZED = 0xC1; + + /** + * Defines the OBEX PAYMENT REQUIRED response code. + * <P> + * The value of <code>OBEX_HTTP_PAYMENT_REQUIRED</code> is 0xC2 (194). + * + * @hide + */ + public static final int OBEX_HTTP_PAYMENT_REQUIRED = 0xC2; + + /** + * Defines the OBEX FORBIDDEN response code. + * <P> + * The value of <code>OBEX_HTTP_FORBIDDEN</code> is 0xC3 (195). + */ + public static final int OBEX_HTTP_FORBIDDEN = 0xC3; + + /** + * Defines the OBEX NOT FOUND response code. + * <P> + * The value of <code>OBEX_HTTP_NOT_FOUND</code> is 0xC4 (196). + */ + public static final int OBEX_HTTP_NOT_FOUND = 0xC4; + + /** + * Defines the OBEX METHOD NOT ALLOWED response code. + * <P> + * The value of <code>OBEX_HTTP_BAD_METHOD</code> is 0xC5 (197). + * + * @hide + */ + public static final int OBEX_HTTP_BAD_METHOD = 0xC5; + + /** + * Defines the OBEX NOT ACCEPTABLE response code. + * <P> + * The value of <code>OBEX_HTTP_NOT_ACCEPTABLE</code> is 0xC6 (198). + */ + public static final int OBEX_HTTP_NOT_ACCEPTABLE = 0xC6; + + /** + * Defines the OBEX PROXY AUTHENTICATION REQUIRED response code. + * <P> + * The value of <code>OBEX_HTTP_PROXY_AUTH</code> is 0xC7 (199). + * + * @hide + */ + public static final int OBEX_HTTP_PROXY_AUTH = 0xC7; + + /** + * Defines the OBEX REQUEST TIME OUT response code. + * <P> + * The value of <code>OBEX_HTTP_TIMEOUT</code> is 0xC8 (200). + * + * @hide + */ + public static final int OBEX_HTTP_TIMEOUT = 0xC8; + + /** + * Defines the OBEX METHOD CONFLICT response code. + * <P> + * The value of <code>OBEX_HTTP_CONFLICT</code> is 0xC9 (201). + * + * @hide + */ + public static final int OBEX_HTTP_CONFLICT = 0xC9; + + /** + * Defines the OBEX METHOD GONE response code. + * <P> + * The value of <code>OBEX_HTTP_GONE</code> is 0xCA (202). + * + * @hide + */ + public static final int OBEX_HTTP_GONE = 0xCA; + + /** + * Defines the OBEX METHOD LENGTH REQUIRED response code. + * <P> + * The value of <code>OBEX_HTTP_LENGTH_REQUIRED</code> is 0xCB (203). + */ + public static final int OBEX_HTTP_LENGTH_REQUIRED = 0xCB; + + /** + * Defines the OBEX PRECONDITION FAILED response code. + * <P> + * The value of <code>OBEX_HTTP_PRECON_FAILED</code> is 0xCC (204). + */ + public static final int OBEX_HTTP_PRECON_FAILED = 0xCC; + + /** + * Defines the OBEX REQUESTED ENTITY TOO LARGE response code. + * <P> + * The value of <code>OBEX_HTTP_ENTITY_TOO_LARGE</code> is 0xCD (205). + * + * @hide + */ + public static final int OBEX_HTTP_ENTITY_TOO_LARGE = 0xCD; + + /** + * Defines the OBEX REQUESTED URL TOO LARGE response code. + * <P> + * The value of <code>OBEX_HTTP_REQ_TOO_LARGE</code> is 0xCE (206). + * + * @hide + */ + public static final int OBEX_HTTP_REQ_TOO_LARGE = 0xCE; + + /** + * Defines the OBEX UNSUPPORTED MEDIA TYPE response code. + * <P> + * The value of <code>OBEX_HTTP_UNSUPPORTED_TYPE</code> is 0xCF (207). + */ + public static final int OBEX_HTTP_UNSUPPORTED_TYPE = 0xCF; + + /** + * Defines the OBEX INTERNAL SERVER ERROR response code. + * <P> + * The value of <code>OBEX_HTTP_INTERNAL_ERROR</code> is 0xD0 (208). + */ + public static final int OBEX_HTTP_INTERNAL_ERROR = 0xD0; + + /** + * Defines the OBEX NOT IMPLEMENTED response code. + * <P> + * The value of <code>OBEX_HTTP_NOT_IMPLEMENTED</code> is 0xD1 (209). + */ + public static final int OBEX_HTTP_NOT_IMPLEMENTED = 0xD1; + + /** + * Defines the OBEX BAD GATEWAY response code. + * <P> + * The value of <code>OBEX_HTTP_BAD_GATEWAY</code> is 0xD2 (210). + * + * @hide + */ + public static final int OBEX_HTTP_BAD_GATEWAY = 0xD2; + + /** + * Defines the OBEX SERVICE UNAVAILABLE response code. + * <P> + * The value of <code>OBEX_HTTP_UNAVAILABLE</code> is 0xD3 (211). + */ + public static final int OBEX_HTTP_UNAVAILABLE = 0xD3; + + /** + * Defines the OBEX GATEWAY TIMEOUT response code. + * <P> + * The value of <code>OBEX_HTTP_GATEWAY_TIMEOUT</code> is 0xD4 (212). + * + * @hide + */ + public static final int OBEX_HTTP_GATEWAY_TIMEOUT = 0xD4; + + /** + * Defines the OBEX HTTP VERSION NOT SUPPORTED response code. + * <P> + * The value of <code>OBEX_HTTP_VERSION</code> is 0xD5 (213). + * + * @hide + */ + public static final int OBEX_HTTP_VERSION = 0xD5; + + /** + * Defines the OBEX DATABASE FULL response code. + * <P> + * The value of <code>OBEX_DATABASE_FULL</code> is 0xE0 (224). + * + * @hide + */ + public static final int OBEX_DATABASE_FULL = 0xE0; + + /** + * Defines the OBEX DATABASE LOCKED response code. + * <P> + * The value of <code>OBEX_DATABASE_LOCKED</code> is 0xE1 (225). + * + * @hide + */ + public static final int OBEX_DATABASE_LOCKED = 0xE1; + + /** + * Constructor does nothing. + */ + private ResponseCodes() { + } +} diff --git a/javax/obex/ServerOperation.java b/javax/obex/ServerOperation.java new file mode 100644 index 0000000..4e77b6c --- /dev/null +++ b/javax/obex/ServerOperation.java @@ -0,0 +1,868 @@ +/* Copyright (c) 2015 The Android Open Source Project + * Copyright (C) 2015 Samsung LSI + * 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.util.Log; + +import java.io.ByteArrayOutputStream; +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +/** + * This class implements the Operation interface for server side connections. + * <P> + * <STRONG>Request Codes</STRONG> There are four different request codes that + * are in this class. 0x02 is a PUT request that signals that the request is not + * complete and requires an additional OBEX packet. 0x82 is a PUT request that + * says that request is complete. In this case, the server can begin sending the + * response. The 0x03 is a GET request that signals that the request is not + * finished. When the server receives a 0x83, the client is signaling the server + * that it is done with its request. TODO: Extend the ClientOperation and reuse + * the methods defined TODO: in that class. + */ +public final class ServerOperation implements Operation, BaseStream { + + private static final String TAG = "ServerOperation"; + + private static final boolean V = ObexHelper.VDBG; // Verbose debugging + + private boolean mAborted; + + /** @hide */ + public HeaderSet requestHeader; + + /** @hide */ + public HeaderSet replyHeader; + + /** @hide */ + public boolean finalBitSet; + + private InputStream mInput; + + private ServerSession mParent; + + private int mMaxPacketLength; + + private int mResponseSize; + + private boolean mClosed; + + private boolean mGetOperation; + + private PrivateInputStream mPrivateInput; + + private PrivateOutputStream mPrivateOutput; + + private ObexTransport mTransport; + + private boolean mPrivateOutputOpen; + + private String mExceptionString; + + private ServerRequestHandler mListener; + + private boolean mRequestFinished; + + private boolean mHasBody; + + private boolean mSendBodyHeader = true; + // Assume SRM disabled - needs to be explicit + // enabled by client + private boolean mSrmEnabled = false; + // A latch - when triggered, there is not way back ;-) + private boolean mSrmActive = false; + // Set to true when a SRM enable response have been send + private boolean mSrmResponseSent = false; + // keep waiting until final-bit is received in request + // to handle the case where the SRM enable header is in + // a different OBEX packet than the SRMP header. + private boolean mSrmWaitingForRemote = true; + // Why should we wait? - currently not exposed to apps. + private boolean mSrmLocalWait = false; + + /** + * Creates new ServerOperation + * @param p the parent that created this object + * @param in the input stream to read from + * @param request the initial request that was received from the client + * @param maxSize the max packet size that the client will accept + * @param listen the listener that is responding to the request + * @throws IOException if an IO error occurs + * + * @hide + */ + public ServerOperation(ServerSession p, InputStream in, int request, int maxSize, + ServerRequestHandler listen) throws IOException { + + mAborted = false; + mParent = p; + mInput = in; + mMaxPacketLength = maxSize; + mClosed = false; + requestHeader = new HeaderSet(); + replyHeader = new HeaderSet(); + mPrivateInput = new PrivateInputStream(this); + mResponseSize = 3; + mListener = listen; + mRequestFinished = false; + mPrivateOutputOpen = false; + mHasBody = false; + ObexPacket packet; + mTransport = p.getTransport(); + + /* + * Determine if this is a PUT request + */ + if ((request == ObexHelper.OBEX_OPCODE_PUT) || + (request == ObexHelper.OBEX_OPCODE_PUT_FINAL)) { + /* + * It is a PUT request. + */ + mGetOperation = false; + + /* + * Determine if the final bit is set + */ + if ((request & ObexHelper.OBEX_OPCODE_FINAL_BIT_MASK) == 0) { + finalBitSet = false; + } else { + finalBitSet = true; + mRequestFinished = true; + } + } else if ((request == ObexHelper.OBEX_OPCODE_GET) || + (request == ObexHelper.OBEX_OPCODE_GET_FINAL)) { + /* + * It is a GET request. + */ + mGetOperation = true; + + // For Get request, final bit set is decided by server side logic + finalBitSet = false; + + if (request == ObexHelper.OBEX_OPCODE_GET_FINAL) { + mRequestFinished = true; + } + } else { + throw new IOException("ServerOperation can not handle such request"); + } + + packet = ObexPacket.read(request, mInput); + + /* + * Determine if the packet length is larger than this device can receive + */ + if (packet.mLength > ObexHelper.getMaxRxPacketSize(mTransport)) { + mParent.sendResponse(ResponseCodes.OBEX_HTTP_REQ_TOO_LARGE, null); + throw new IOException("Packet received was too large. Length: " + + packet.mLength + " maxLength: " + ObexHelper.getMaxRxPacketSize(mTransport)); + } + + /* + * Determine if any headers were sent in the initial request + */ + if (packet.mLength > 3) { + if(!handleObexPacket(packet)) { + return; + } + /* Don't Pre-Send continue when Remote requested for SRM + * Let the Application confirm. + */ + if (V) Log.v(TAG, "Get App confirmation if SRM ENABLED case: " + mSrmEnabled + + " not hasBody case: " + mHasBody); + if (!mHasBody && !mSrmEnabled) { + while ((!mGetOperation) && (!finalBitSet)) { + sendReply(ResponseCodes.OBEX_HTTP_CONTINUE); + if (mPrivateInput.available() > 0) { + break; + } + } + } + } + /* Don't Pre-Send continue when Remote requested for SRM + * Let the Application confirm. + */ + if (V) Log.v(TAG, "Get App confirmation if SRM ENABLED case: " + mSrmEnabled + + " not finalPacket: " + finalBitSet + " not GETOp Case: " + mGetOperation); + while ((!mSrmEnabled) && (!mGetOperation) && (!finalBitSet) + && (mPrivateInput.available() == 0)) { + sendReply(ResponseCodes.OBEX_HTTP_CONTINUE); + if (mPrivateInput.available() > 0) { + break; + } + } + + // wait for get request finished !!!! + while (mGetOperation && !mRequestFinished) { + sendReply(ResponseCodes.OBEX_HTTP_CONTINUE); + } + } + + /** + * Parse headers and update member variables + * @param packet the received obex packet + * @return false for failing authentication - and a OBEX_HTTP_UNAUTHORIZED + * response have been send. Else true. + * @throws IOException + */ + private boolean handleObexPacket(ObexPacket packet) throws IOException { + byte[] body = updateRequestHeaders(packet); + + if (body != null) { + mHasBody = true; + } + if (mListener.getConnectionId() != -1 && requestHeader.mConnectionID != null) { + mListener.setConnectionId(ObexHelper + .convertToLong(requestHeader.mConnectionID)); + } else { + mListener.setConnectionId(1); + } + + if (requestHeader.mAuthResp != null) { + if (!mParent.handleAuthResp(requestHeader.mAuthResp)) { + mExceptionString = "Authentication Failed"; + mParent.sendResponse(ResponseCodes.OBEX_HTTP_UNAUTHORIZED, null); + mClosed = true; + requestHeader.mAuthResp = null; + return false; + } + requestHeader.mAuthResp = null; + } + + if (requestHeader.mAuthChall != null) { + mParent.handleAuthChall(requestHeader); + // send the auhtResp to the client + replyHeader.mAuthResp = new byte[requestHeader.mAuthResp.length]; + System.arraycopy(requestHeader.mAuthResp, 0, replyHeader.mAuthResp, 0, + replyHeader.mAuthResp.length); + requestHeader.mAuthResp = null; + requestHeader.mAuthChall = null; + } + + if (body != null) { + mPrivateInput.writeBytes(body, 1); + } + return true; + } + + /** + * Update the request header set, and sniff on SRM headers to update local state. + * @param data the OBEX packet data + * @return any bytes in a body/end-of-body header returned by {@link ObexHelper.updateHeaderSet} + * @throws IOException + */ + private byte[] updateRequestHeaders(ObexPacket packet) throws IOException { + byte[] body = null; + if (packet.mPayload != null) { + body = ObexHelper.updateHeaderSet(requestHeader, packet.mPayload); + } + Byte srmMode = (Byte)requestHeader.getHeader(HeaderSet.SINGLE_RESPONSE_MODE); + if(mTransport.isSrmSupported() && srmMode != null + && srmMode == ObexHelper.OBEX_SRM_ENABLE) { + mSrmEnabled = true; + if(V) Log.d(TAG,"SRM is now ENABLED (but not active) for this operation"); + } + checkForSrmWait(packet.mHeaderId); + if((!mSrmWaitingForRemote) && (mSrmEnabled)) { + if(V) Log.d(TAG,"SRM is now ACTIVE for this operation"); + mSrmActive = true; + } + return body; + } + + /** + * Call this only when a complete request have been received. + * (This is not optimal, but the current design is not really suited to + * the way SRM is specified.) + */ + private void checkForSrmWait(int headerId){ + if (mSrmEnabled && (headerId == ObexHelper.OBEX_OPCODE_GET + || headerId == ObexHelper.OBEX_OPCODE_GET_FINAL + || headerId == ObexHelper.OBEX_OPCODE_PUT)) { + try { + mSrmWaitingForRemote = false; + Byte srmp = (Byte)requestHeader.getHeader(HeaderSet.SINGLE_RESPONSE_MODE_PARAMETER); + if(srmp != null && srmp == ObexHelper.OBEX_SRMP_WAIT) { + mSrmWaitingForRemote = true; + // Clear the wait header, as the absents of the header when the final bit is set + // indicates don't wait. + requestHeader.setHeader(HeaderSet.SINGLE_RESPONSE_MODE_PARAMETER, null); + } + } catch (IOException e) {if(V){Log.w(TAG,"Exception while extracting header",e);}} + } + } + + /** @hide */ + public boolean isValidBody() { + return mHasBody; + } + + /** + * Determines if the operation should continue or should wait. If it should + * continue, this method will continue the operation. + * @param sendEmpty if <code>true</code> then this will continue the + * operation even if no headers will be sent; if <code>false</code> + * then this method will only continue the operation if there are + * headers to send + * @param inStream if<code>true</code> the stream is input stream, otherwise + * output stream + * @return <code>true</code> if the operation was completed; + * <code>false</code> if no operation took place + * + * @hide + */ + public synchronized boolean continueOperation(boolean sendEmpty, boolean inStream) + throws IOException { + if (!mGetOperation) { + if (!finalBitSet) { + if (sendEmpty) { + sendReply(ResponseCodes.OBEX_HTTP_CONTINUE); + return true; + } else { + if ((mResponseSize > 3) || (mPrivateOutput.size() > 0)) { + sendReply(ResponseCodes.OBEX_HTTP_CONTINUE); + return true; + } else { + return false; + } + } + } else { + return false; + } + } else { + sendReply(ResponseCodes.OBEX_HTTP_CONTINUE); + return true; + } + } + + /** + * Sends a reply to the client. If the reply is a OBEX_HTTP_CONTINUE, it + * will wait for a response from the client before ending unless SRM is active. + * @param type the response code to send back to the client + * @return <code>true</code> if the final bit was not set on the reply; + * <code>false</code> if no reply was received because the operation + * ended, an abort was received, the final bit was set in the + * reply or SRM is active. + * @throws IOException if an IO error occurs + * + * @hide + */ + public synchronized boolean sendReply(int type) throws IOException { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + boolean skipSend = false; + boolean skipReceive = false; + boolean srmRespSendPending = false; + + long id = mListener.getConnectionId(); + if (id == -1) { + replyHeader.mConnectionID = null; + } else { + replyHeader.mConnectionID = ObexHelper.convertToByteArray(id); + } + + if(mSrmEnabled && !mSrmResponseSent) { + // As we are not ensured that the SRM enable is in the first OBEX packet + // We must check for each reply. + if(V)Log.v(TAG, "mSrmEnabled==true, sending SRM enable response."); + replyHeader.setHeader(HeaderSet.SINGLE_RESPONSE_MODE, (byte)ObexHelper.OBEX_SRM_ENABLE); + srmRespSendPending = true; + } + + if(mSrmEnabled && !mGetOperation && mSrmLocalWait) { + replyHeader.setHeader(HeaderSet.SINGLE_RESPONSE_MODE, (byte)ObexHelper.OBEX_SRMP_WAIT); + } + + byte[] headerArray = ObexHelper.createHeader(replyHeader, true); // This clears the headers + int bodyLength = -1; + int originalBodyLength = -1; + + if (mPrivateOutput != null) { + bodyLength = mPrivateOutput.size(); + originalBodyLength = bodyLength; + } + + if ((ObexHelper.BASE_PACKET_LENGTH + headerArray.length) > mMaxPacketLength) { + + int end = 0; + int start = 0; + + while (end != headerArray.length) { + end = ObexHelper.findHeaderEnd(headerArray, start, mMaxPacketLength + - ObexHelper.BASE_PACKET_LENGTH); + if (end == -1) { + + mClosed = true; + + if (mPrivateInput != null) { + mPrivateInput.close(); + } + + if (mPrivateOutput != null) { + mPrivateOutput.close(); + } + mParent.sendResponse(ResponseCodes.OBEX_HTTP_INTERNAL_ERROR, null); + throw new IOException("OBEX Packet exceeds max packet size"); + } + byte[] sendHeader = new byte[end - start]; + System.arraycopy(headerArray, start, sendHeader, 0, sendHeader.length); + + mParent.sendResponse(type, sendHeader); + start = end; + } + + if (bodyLength > 0) { + return true; + } else { + return false; + } + + } else { + out.write(headerArray); + } + + // For Get operation: if response code is OBEX_HTTP_OK, then this is the + // last packet; so set finalBitSet to true. + if (mGetOperation && type == ResponseCodes.OBEX_HTTP_OK) { + finalBitSet = true; + } + + if(mSrmActive) { + if(!mGetOperation && type == ResponseCodes.OBEX_HTTP_CONTINUE && + mSrmResponseSent == true) { + // we are in the middle of a SRM PUT operation, don't send a continue. + skipSend = true; + } else if(mGetOperation && mRequestFinished == false && mSrmResponseSent == true) { + // We are still receiving the get request, receive, but don't send continue. + skipSend = true; + } else if(mGetOperation && mRequestFinished == true) { + // All done receiving the GET request, send data to the client, without + // expecting a continue. + skipReceive = true; + } + if(V)Log.v(TAG, "type==" + type + " skipSend==" + skipSend + + " skipReceive==" + skipReceive); + } + if(srmRespSendPending) { + if(V)Log.v(TAG, + "SRM Enabled (srmRespSendPending == true)- sending SRM Enable response"); + mSrmResponseSent = true; + } + + if ((finalBitSet) || (headerArray.length < (mMaxPacketLength - 20))) { + if (bodyLength > 0) { + /* + * Determine if I can send the whole body or just part of + * the body. Remember that there is the 3 bytes for the + * response message and 3 bytes for the header ID and length + */ + if (bodyLength > (mMaxPacketLength - headerArray.length - 6)) { + bodyLength = mMaxPacketLength - headerArray.length - 6; + } + + byte[] body = mPrivateOutput.readBytes(bodyLength); + + /* + * Since this is a put request if the final bit is set or + * the output stream is closed we need to send the 0x49 + * (End of Body) otherwise, we need to send 0x48 (Body) + */ + if ((finalBitSet) || (mPrivateOutput.isClosed())) { + if(mSendBodyHeader == true) { + out.write(0x49); + bodyLength += 3; + out.write((byte)(bodyLength >> 8)); + out.write((byte)bodyLength); + out.write(body); + } + } else { + if(mSendBodyHeader == true) { + out.write(0x48); + bodyLength += 3; + out.write((byte)(bodyLength >> 8)); + out.write((byte)bodyLength); + out.write(body); + } + } + + } + } + + if ((finalBitSet) && (type == ResponseCodes.OBEX_HTTP_OK) && (originalBodyLength <= 0)) { + if(mSendBodyHeader) { + out.write(0x49); + originalBodyLength = 3; + out.write((byte)(originalBodyLength >> 8)); + out.write((byte)originalBodyLength); + } + } + + if(skipSend == false) { + mResponseSize = 3; + mParent.sendResponse(type, out.toByteArray()); + } + + if (type == ResponseCodes.OBEX_HTTP_CONTINUE) { + + if(mGetOperation && skipReceive) { + // Here we need to check for and handle abort (throw an exception). + // Any other signal received should be discarded silently (only on server side) + checkSrmRemoteAbort(); + } else { + // Receive and handle data (only send reply if !skipSend) + // Read a complete OBEX Packet + ObexPacket packet = ObexPacket.read(mInput); + + int headerId = packet.mHeaderId; + if ((headerId != ObexHelper.OBEX_OPCODE_PUT) + && (headerId != ObexHelper.OBEX_OPCODE_PUT_FINAL) + && (headerId != ObexHelper.OBEX_OPCODE_GET) + && (headerId != ObexHelper.OBEX_OPCODE_GET_FINAL)) { + + /* + * Determine if an ABORT was sent as the reply + */ + if (headerId == ObexHelper.OBEX_OPCODE_ABORT) { + handleRemoteAbort(); + } else { + // TODO:shall we send this if it occurs during SRM? Errata on the subject + mParent.sendResponse(ResponseCodes.OBEX_HTTP_BAD_REQUEST, null); + mClosed = true; + mExceptionString = "Bad Request Received"; + throw new IOException("Bad Request Received"); + } + } else { + + if ((headerId == ObexHelper.OBEX_OPCODE_PUT_FINAL)) { + finalBitSet = true; + } else if (headerId == ObexHelper.OBEX_OPCODE_GET_FINAL) { + mRequestFinished = true; + } + + /* + * Determine if the packet length is larger than the negotiated packet size + */ + if (packet.mLength > ObexHelper.getMaxRxPacketSize(mTransport)) { + mParent.sendResponse(ResponseCodes.OBEX_HTTP_REQ_TOO_LARGE, null); + throw new IOException("Packet received was too large"); + } + + /* + * Determine if any headers were sent in the initial request + */ + if (packet.mLength > 3 || (mSrmEnabled && packet.mLength == 3)) { + if(handleObexPacket(packet) == false) { + return false; + } + } + } + + } + return true; + } else { + return false; + } + } + + /** + * This method will look for an abort from the peer during a SRM transfer. + * The function will not block if no data has been received from the remote device. + * If data have been received, the function will block while reading the incoming + * OBEX package. + * An Abort request will be handled, and cause an IOException("Abort Received"). + * Other messages will be discarded silently as per GOEP specification. + * @throws IOException if an abort request have been received. + * TODO: I think this is an error in the specification. If we discard other messages, + * the peer device will most likely stall, as it will not receive the expected + * response for the message... + * I'm not sure how to understand "Receipt of invalid or unexpected SRM or SRMP + * header values shall be ignored by the receiving device." + * If any signal is received during an active SRM transfer it is unexpected regardless + * whether or not it contains SRM/SRMP headers... + */ + private void checkSrmRemoteAbort() throws IOException { + if(mInput.available() > 0) { + ObexPacket packet = ObexPacket.read(mInput); + /* + * Determine if an ABORT was sent as the reply + */ + if (packet.mHeaderId == ObexHelper.OBEX_OPCODE_ABORT) { + handleRemoteAbort(); + } else { + // TODO: should we throw an exception here anyway? - don't see how to + // ignore SRM/SRMP headers without ignoring the complete signal + // (in this particular case). + Log.w(TAG, "Received unexpected request from client - discarding...\n" + + " headerId: " + packet.mHeaderId + " length: " + packet.mLength); + } + } + } + + private void handleRemoteAbort() throws IOException { + /* TODO: To increase the speed of the abort operation in SRM, we need + * to be able to flush the L2CAP queue for the PSM in use. + * This could be implemented by introducing a control + * message to be send over the socket, that in the abort case + * could carry a flush command. */ + mParent.sendResponse(ResponseCodes.OBEX_HTTP_OK, null); + mClosed = true; + mAborted = true; + mExceptionString = "Abort Received"; + throw new IOException("Abort Received"); + } + + /** + * Sends an ABORT message to the server. By calling this method, the + * corresponding input and output streams will be closed along with this + * object. + * @throws IOException if the transaction has already ended or if an OBEX + * server called this method + * + * @hide + */ + public void abort() throws IOException { + throw new IOException("Called from a server"); + } + + /** + * Returns the headers that have been received during the operation. + * Modifying the object returned has no effect on the headers that are sent + * or retrieved. + * @return the headers received during this <code>Operation</code> + * @throws IOException if this <code>Operation</code> has been closed + * + * @hide + */ + public HeaderSet getReceivedHeader() throws IOException { + ensureOpen(); + return requestHeader; + } + + /** + * Specifies the headers that should be sent in the next OBEX message that + * is sent. + * @param headers the headers to send in the next message + * @throws IOException if this <code>Operation</code> has been closed or the + * transaction has ended and no further messages will be exchanged + * @throws IllegalArgumentException if <code>headers</code> was not created + * by a call to <code>ServerRequestHandler.createHeaderSet()</code> + * + * @hide + */ + public void sendHeaders(HeaderSet headers) throws IOException { + ensureOpen(); + + if (headers == null) { + throw new IOException("Headers may not be null"); + } + + int[] headerList = headers.getHeaderList(); + if (headerList != null) { + for (int i = 0; i < headerList.length; i++) { + replyHeader.setHeader(headerList[i], headers.getHeader(headerList[i])); + } + + } + } + + /** + * Retrieves the response code retrieved from the server. Response codes are + * defined in the <code>ResponseCodes</code> interface. + * @return the response code retrieved from the server + * @throws IOException if an error occurred in the transport layer during + * the transaction; if this method is called on a + * <code>HeaderSet</code> object created by calling + * <code>createHeaderSet</code> in a <code>ClientSession</code> + * object; if this is called from a server + * + * @hide + */ + public int getResponseCode() throws IOException { + throw new IOException("Called from a server"); + } + + /** @hide */ + public int getMaxPacketSize() { + return mMaxPacketLength - 6 - getHeaderLength(); + } + + /** @hide */ + public int getHeaderLength() { + long id = mListener.getConnectionId(); + if (id == -1) { + replyHeader.mConnectionID = null; + } else { + replyHeader.mConnectionID = ObexHelper.convertToByteArray(id); + } + + byte[] headerArray = ObexHelper.createHeader(replyHeader, false); + + return headerArray.length; + } + + /** + * Open and return an input stream for a connection. + * @return an input stream + * @throws IOException if an I/O error occurs + * + * @hide + */ + public InputStream openInputStream() throws IOException { + ensureOpen(); + return mPrivateInput; + } + + /** + * Open and return a data input stream for a connection. + * @return an input stream + * @throws IOException if an I/O error occurs + * + * @hide + */ + public DataInputStream openDataInputStream() throws IOException { + return new DataInputStream(openInputStream()); + } + + /** + * Open and return an output stream for a connection. + * @return an output stream + * @throws IOException if an I/O error occurs + * + * @hide + */ + public OutputStream openOutputStream() throws IOException { + ensureOpen(); + + if (mPrivateOutputOpen) { + throw new IOException("no more input streams available, stream already opened"); + } + + if (!mRequestFinished) { + throw new IOException("no output streams available ,request not finished"); + } + + if (mPrivateOutput == null) { + mPrivateOutput = new PrivateOutputStream(this, getMaxPacketSize()); + } + mPrivateOutputOpen = true; + return mPrivateOutput; + } + + /** + * Open and return a data output stream for a connection. + * @return an output stream + * @throws IOException if an I/O error occurs + * + * @hide + */ + public DataOutputStream openDataOutputStream() throws IOException { + return new DataOutputStream(openOutputStream()); + } + + /** + * Closes the connection and ends the transaction + * @throws IOException if the operation has already ended or is closed + * + * @hide + */ + public void close() throws IOException { + ensureOpen(); + mClosed = true; + } + + /** + * Verifies that the connection is open and no exceptions should be thrown. + * @throws IOException if an exception needs to be thrown + * + * @hide + */ + public void ensureOpen() throws IOException { + if (mExceptionString != null) { + throw new IOException(mExceptionString); + } + if (mClosed) { + throw new IOException("Operation has already ended"); + } + } + + /** + * Verifies that additional information may be sent. In other words, the + * operation is not done. + * <P> + * Included to implement the BaseStream interface only. It does not do + * anything on the server side since the operation of the Operation object + * is not done until after the handler returns from its method. + * @throws IOException if the operation is completed + * + * @hide + */ + public void ensureNotDone() throws IOException { + } + + /** + * Called when the output or input stream is closed. It does not do anything + * on the server side since the operation of the Operation object is not + * done until after the handler returns from its method. + * @param inStream <code>true</code> if the input stream is closed; + * <code>false</code> if the output stream is closed + * @throws IOException if an IO error occurs + * + * @hide + */ + public void streamClosed(boolean inStream) throws IOException { + + } + + /** @hide */ + public void noBodyHeader(){ + mSendBodyHeader = false; + } + + /** + * Returns whether the operation is aborted. + */ + public boolean isAborted() { + return mAborted; + } + + /** + * Set whether the operation is aborted. + * + * @param aborted {@code true} if the operation is aborted, {@code false} otherwise + */ + public void setAborted(boolean aborted) { + this.mAborted = aborted; + } +} diff --git a/javax/obex/ServerRequestHandler.java b/javax/obex/ServerRequestHandler.java new file mode 100644 index 0000000..f33f234 --- /dev/null +++ b/javax/obex/ServerRequestHandler.java @@ -0,0 +1,290 @@ +/* + * 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; + +/** + * The <code>ServerRequestHandler</code> class defines an event listener that + * will respond to OBEX requests made to the server. + * <P> + * The <code>onConnect()</code>, <code>onSetPath()</code>, + * <code>onDelete()</code>, <code>onGet()</code>, and <code>onPut()</code> + * methods may return any response code defined in the + * <code>ResponseCodes</code> class except for <code>OBEX_HTTP_CONTINUE</code>. + * If <code>OBEX_HTTP_CONTINUE</code> or a value not defined in the + * <code>ResponseCodes</code> class is returned, the server implementation will + * send an <code>OBEX_HTTP_INTERNAL_ERROR</code> response to the client. + * <P> + * <STRONG>Connection ID and Target Headers</STRONG> + * <P> + * According to the IrOBEX specification, a packet may not contain a Connection + * ID and Target header. Since the Connection ID header is managed by the + * implementation, it will not send a Connection ID header, if a Connection ID + * was specified, in a packet that has a Target header. In other words, if an + * application adds a Target header to a <code>HeaderSet</code> object used in + * an OBEX operation and a Connection ID was specified, no Connection ID will be + * sent in the packet containing the Target header. + * <P> + * <STRONG>CREATE-EMPTY Requests</STRONG> + * <P> + * A CREATE-EMPTY request allows clients to create empty objects on the server. + * When a CREATE-EMPTY request is received, the <code>onPut()</code> method will + * be called by the implementation. To differentiate between a normal PUT + * request and a CREATE-EMPTY request, an application must open the + * <code>InputStream</code> from the <code>Operation</code> object passed to the + * <code>onPut()</code> method. For a PUT request, the application will be able + * to read Body data from this <code>InputStream</code>. For a CREATE-EMPTY + * request, there will be no Body data to read. Therefore, a call to + * <code>InputStream.read()</code> will return -1. + */ +public class ServerRequestHandler { + + private long mConnectionId; + + /** + * Creates a <code>ServerRequestHandler</code>. + */ + protected ServerRequestHandler() { + /* + * A connection ID of -1 implies there is no connection ID + */ + mConnectionId = -1; + } + + /** + * Sets the connection ID header to include in the reply packets. + * @param connectionId the connection ID to use; -1 if no connection ID + * should be sent + * @throws IllegalArgumentException if <code>id</code> is not in the range + * -1 to 2<sup>32</sup>-1 + * + * @hide + */ + public void setConnectionId(final long connectionId) { + if ((connectionId < -1) || (connectionId > 0xFFFFFFFFL)) { + throw new IllegalArgumentException("Illegal Connection ID"); + } + mConnectionId = connectionId; + } + + /** + * Retrieves the connection ID that is being used in the present connection. + * This method will return -1 if no connection ID is being used. + * @return the connection id being used or -1 if no connection ID is being + * used + * + * @hide + */ + public long getConnectionId() { + return mConnectionId; + } + + /** + * Called when a CONNECT request is received. + * <P> + * If this method is not implemented by the class that extends this class, + * <code>onConnect()</code> will always return an <code>OBEX_HTTP_OK</code> + * response code. + * <P> + * The headers received in the request can be retrieved from the + * <code>request</code> argument. The headers that should be sent in the + * reply must be specified in the <code>reply</code> argument. + * @param request contains the headers sent by the client; + * <code>request</code> will never be <code>null</code> + * @param reply the headers that should be sent in the reply; + * <code>reply</code> will never be <code>null</code> + * @return a response code defined in <code>ResponseCodes</code> that will + * be returned to the client; if an invalid response code is + * provided, the <code>OBEX_HTTP_INTERNAL_ERROR</code> response code + * will be used + */ + public int onConnect(HeaderSet request, HeaderSet reply) { + return ResponseCodes.OBEX_HTTP_OK; + } + + /** + * Called when a DISCONNECT request is received. + * <P> + * The headers received in the request can be retrieved from the + * <code>request</code> argument. The headers that should be sent in the + * reply must be specified in the <code>reply</code> argument. + * @param request contains the headers sent by the client; + * <code>request</code> will never be <code>null</code> + * @param reply the headers that should be sent in the reply; + * <code>reply</code> will never be <code>null</code> + */ + public void onDisconnect(HeaderSet request, HeaderSet reply) { + } + + /** + * Called when a SETPATH request is received. + * <P> + * If this method is not implemented by the class that extends this class, + * <code>onSetPath()</code> will always return an + * <code>OBEX_HTTP_NOT_IMPLEMENTED</code> response code. + * <P> + * The headers received in the request can be retrieved from the + * <code>request</code> argument. The headers that should be sent in the + * reply must be specified in the <code>reply</code> argument. + * @param request contains the headers sent by the client; + * <code>request</code> will never be <code>null</code> + * @param reply the headers that should be sent in the reply; + * <code>reply</code> will never be <code>null</code> + * @param backup <code>true</code> if the client requests that the server + * back up one directory before changing to the path described by + * <code>name</code>; <code>false</code> to apply the request to the + * present path + * @param create <code>true</code> if the path should be created if it does + * not already exist; <code>false</code> if the path should not be + * created if it does not exist and an error code should be returned + * @return a response code defined in <code>ResponseCodes</code> that will + * be returned to the client; if an invalid response code is + * provided, the <code>OBEX_HTTP_INTERNAL_ERROR</code> response code + * will be used + */ + public int onSetPath(HeaderSet request, HeaderSet reply, boolean backup, boolean create) { + + return ResponseCodes.OBEX_HTTP_NOT_IMPLEMENTED; + } + + /** + * Called when a DELETE request is received. + * <P> + * If this method is not implemented by the class that extends this class, + * <code>onDelete()</code> will always return an + * <code>OBEX_HTTP_NOT_IMPLEMENTED</code> response code. + * <P> + * The headers received in the request can be retrieved from the + * <code>request</code> argument. The headers that should be sent in the + * reply must be specified in the <code>reply</code> argument. + * @param request contains the headers sent by the client; + * <code>request</code> will never be <code>null</code> + * @param reply the headers that should be sent in the reply; + * <code>reply</code> will never be <code>null</code> + * @return a response code defined in <code>ResponseCodes</code> that will + * be returned to the client; if an invalid response code is + * provided, the <code>OBEX_HTTP_INTERNAL_ERROR</code> response code + * will be used + */ + public int onDelete(HeaderSet request, HeaderSet reply) { + return ResponseCodes.OBEX_HTTP_NOT_IMPLEMENTED; + } + + /** + * Called when a ABORT request is received. + */ + public int onAbort(HeaderSet request, HeaderSet reply) { + return ResponseCodes.OBEX_HTTP_NOT_IMPLEMENTED; + } + + /** + * Called when a PUT request is received. + * <P> + * If this method is not implemented by the class that extends this class, + * <code>onPut()</code> will always return an + * <code>OBEX_HTTP_NOT_IMPLEMENTED</code> response code. + * <P> + * If an ABORT request is received during the processing of a PUT request, + * <code>op</code> will be closed by the implementation. + * @param operation contains the headers sent by the client and allows new + * headers to be sent in the reply; <code>op</code> will never be + * <code>null</code> + * @return a response code defined in <code>ResponseCodes</code> that will + * be returned to the client; if an invalid response code is + * provided, the <code>OBEX_HTTP_INTERNAL_ERROR</code> response code + * will be used + */ + public int onPut(Operation operation) { + return ResponseCodes.OBEX_HTTP_NOT_IMPLEMENTED; + } + + /** + * Called when a GET request is received. + * <P> + * If this method is not implemented by the class that extends this class, + * <code>onGet()</code> will always return an + * <code>OBEX_HTTP_NOT_IMPLEMENTED</code> response code. + * <P> + * If an ABORT request is received during the processing of a GET request, + * <code>op</code> will be closed by the implementation. + * @param operation contains the headers sent by the client and allows new + * headers to be sent in the reply; <code>op</code> will never be + * <code>null</code> + * @return a response code defined in <code>ResponseCodes</code> that will + * be returned to the client; if an invalid response code is + * provided, the <code>OBEX_HTTP_INTERNAL_ERROR</code> response code + * will be used + */ + public int onGet(Operation operation) { + return ResponseCodes.OBEX_HTTP_NOT_IMPLEMENTED; + } + + /** + * Called when this object attempts to authenticate a client and the + * authentication request fails because the response digest in the + * authentication response header was wrong. + * <P> + * If this method is not implemented by the class that extends this class, + * this method will do nothing. + * @param userName the user name returned in the authentication response; + * <code>null</code> if no user name was provided in the response + */ + public void onAuthenticationFailure(byte[] userName) { + } + + /** + * Called by ServerSession to update the status of current transaction + * <P> + * If this method is not implemented by the class that extends this class, + * this method will do nothing. + */ + public void updateStatus(String message) { + } + + /** + * Called when session is closed. + * <P> + * If this method is not implemented by the class that extends this class, + * this method will do nothing. + */ + public void onClose() { + } + + /** + * Override to add Single Response Mode support - e.g. if the supplied + * transport is l2cap. + * @return True if SRM is supported, else False + */ + public boolean isSrmSupported() { + return false; + } +} diff --git a/javax/obex/ServerSession.java b/javax/obex/ServerSession.java new file mode 100644 index 0000000..68cd766 --- /dev/null +++ b/javax/obex/ServerSession.java @@ -0,0 +1,750 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * Copyright (c) 2015 Samsung LSI + * 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.util.Log; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +/** + * This class in an implementation of the OBEX ServerSession. + */ +public final class ServerSession extends ObexSession implements Runnable { + + private static final String TAG = "Obex ServerSession"; + private static final boolean V = ObexHelper.VDBG; + + private ObexTransport mTransport; + + private InputStream mInput; + + private OutputStream mOutput; + + private ServerRequestHandler mListener; + + private Thread mProcessThread; + + private int mMaxPacketLength; + + private boolean mClosed; + + /** + * Creates new ServerSession. + * + * @param transport the connection to the client + * @param handler the event listener that will process requests + * @param auth the authenticator to use with this connection + * @throws IOException if an error occurred while opening the input and + * output streams + */ + public ServerSession(ObexTransport transport, ServerRequestHandler handler, Authenticator auth) + throws IOException { + mAuthenticator = auth; + mTransport = transport; + mInput = mTransport.openInputStream(); + mOutput = mTransport.openOutputStream(); + mListener = handler; + mMaxPacketLength = 256; + + mClosed = false; + mProcessThread = new Thread(this); + mProcessThread.start(); + } + + /** + * Processes requests made to the server and forwards them to the + * appropriate event listener. + * + * @hide + */ + public void run() { + try { + + boolean done = false; + while (!done && !mClosed) { + if(V) Log.v(TAG, "Waiting for incoming request..."); + int requestType = mInput.read(); + if(V) Log.v(TAG, "Read request: " + requestType); + switch (requestType) { + case ObexHelper.OBEX_OPCODE_CONNECT: + handleConnectRequest(); + break; + + case ObexHelper.OBEX_OPCODE_DISCONNECT: + handleDisconnectRequest(); + break; + + case ObexHelper.OBEX_OPCODE_GET: + case ObexHelper.OBEX_OPCODE_GET_FINAL: + handleGetRequest(requestType); + break; + + case ObexHelper.OBEX_OPCODE_PUT: + case ObexHelper.OBEX_OPCODE_PUT_FINAL: + handlePutRequest(requestType); + break; + + case ObexHelper.OBEX_OPCODE_SETPATH: + handleSetPathRequest(); + break; + case ObexHelper.OBEX_OPCODE_ABORT: + handleAbortRequest(); + break; + + case -1: + done = true; + break; + + default: + + /* + * Received a request type that is not recognized so I am + * just going to read the packet and send a not implemented + * to the client + */ + int length = mInput.read(); + length = (length << 8) + mInput.read(); + for (int i = 3; i < length; i++) { + mInput.read(); + } + sendResponse(ResponseCodes.OBEX_HTTP_NOT_IMPLEMENTED, null); + } + } + + } catch (NullPointerException e) { + Log.d(TAG, "Exception occurred - ignoring", e); + } catch (Exception e) { + Log.d(TAG, "Exception occurred - ignoring", e); + } + close(); + } + + /** + * Handles a ABORT request from a client. This method will read the rest of + * the request from the client. Assuming the request is valid, it will + * create a <code>HeaderSet</code> object to pass to the + * <code>ServerRequestHandler</code> object. After the handler processes the + * request, this method will create a reply message to send to the server. + * + * @throws IOException if an error occurred at the transport layer + */ + private void handleAbortRequest() throws IOException { + int code = ResponseCodes.OBEX_HTTP_OK; + HeaderSet request = new HeaderSet(); + HeaderSet reply = new HeaderSet(); + + int length = mInput.read(); + length = (length << 8) + mInput.read(); + if (length > ObexHelper.getMaxRxPacketSize(mTransport)) { + code = ResponseCodes.OBEX_HTTP_REQ_TOO_LARGE; + } else { + for (int i = 3; i < length; i++) { + mInput.read(); + } + code = mListener.onAbort(request, reply); + Log.v(TAG, "onAbort request handler return value- " + code); + code = validateResponseCode(code); + } + sendResponse(code, null); + } + + /** + * Handles a PUT request from a client. This method will provide a + * <code>ServerOperation</code> object to the request handler. The + * <code>ServerOperation</code> object will handle the rest of the request. + * It will also send replies and receive requests until the final reply + * should be sent. When the final reply should be sent, this method will get + * the response code to use and send the reply. The + * <code>ServerOperation</code> object will always reply with a + * OBEX_HTTP_CONTINUE reply. It will only reply if further information is + * needed. + * @param type the type of request received; either 0x02 or 0x82 + * @throws IOException if an error occurred at the transport layer + */ + private void handlePutRequest(int type) throws IOException { + ServerOperation op = new ServerOperation(this, mInput, type, mMaxPacketLength, mListener); + try { + int response = -1; + + if ((op.finalBitSet) && !op.isValidBody()) { + response = validateResponseCode(mListener + .onDelete(op.requestHeader, op.replyHeader)); + } else { + response = validateResponseCode(mListener.onPut(op)); + } + if (response != ResponseCodes.OBEX_HTTP_OK && !op.isAborted()) { + op.sendReply(response); + } else if (!op.isAborted()) { + // wait for the final bit + while (!op.finalBitSet) { + op.sendReply(ResponseCodes.OBEX_HTTP_CONTINUE); + } + op.sendReply(response); + } + } catch (Exception e) { + /*To fix bugs in aborted cases, + *(client abort file transfer prior to the last packet which has the end of body header, + *internal error should not be sent because server has already replied with + *OK response in "sendReply") + */ + if(V) Log.d(TAG,"Exception occurred - sending OBEX_HTTP_INTERNAL_ERROR reply",e); + if (!op.isAborted()) { + sendResponse(ResponseCodes.OBEX_HTTP_INTERNAL_ERROR, null); + } + } + } + + /** + * Handles a GET request from a client. This method will provide a + * <code>ServerOperation</code> object to the request handler. The + * <code>ServerOperation</code> object will handle the rest of the request. + * It will also send replies and receive requests until the final reply + * should be sent. When the final reply should be sent, this method will get + * the response code to use and send the reply. The + * <code>ServerOperation</code> object will always reply with a + * OBEX_HTTP_CONTINUE reply. It will only reply if further information is + * needed. + * @param type the type of request received; either 0x03 or 0x83 + * @throws IOException if an error occurred at the transport layer + */ + private void handleGetRequest(int type) throws IOException { + ServerOperation op = new ServerOperation(this, mInput, type, mMaxPacketLength, mListener); + try { + int response = validateResponseCode(mListener.onGet(op)); + + if (!op.isAborted()) { + op.sendReply(response); + } + } catch (Exception e) { + if(V) Log.d(TAG,"Exception occurred - sending OBEX_HTTP_INTERNAL_ERROR reply",e); + sendResponse(ResponseCodes.OBEX_HTTP_INTERNAL_ERROR, null); + } + } + + /** + * Send standard response. + * @param code the response code to send + * @param header the headers to include in the response + * @throws IOException if an IO error occurs + * + * @hide + */ + public void sendResponse(int code, byte[] header) throws IOException { + int totalLength = 3; + byte[] data = null; + OutputStream op = mOutput; + if (op == null) { + return; + } + + if (header != null) { + totalLength += header.length; + data = new byte[totalLength]; + data[0] = (byte)code; + data[1] = (byte)(totalLength >> 8); + data[2] = (byte)totalLength; + System.arraycopy(header, 0, data, 3, header.length); + } else { + data = new byte[totalLength]; + data[0] = (byte)code; + data[1] = (byte)0x00; + data[2] = (byte)totalLength; + } + op.write(data); + op.flush(); // TODO: Do we need to flush? + } + + /** + * Handles a SETPATH request from a client. This method will read the rest + * of the request from the client. Assuming the request is valid, it will + * create a <code>HeaderSet</code> object to pass to the + * <code>ServerRequestHandler</code> object. After the handler processes the + * request, this method will create a reply message to send to the server + * with the response code provided. + * @throws IOException if an error occurred at the transport layer + */ + private void handleSetPathRequest() throws IOException { + int length; + int flags; + @SuppressWarnings("unused") + int constants; + int totalLength = 3; + byte[] head = null; + int code = -1; + int bytesReceived; + HeaderSet request = new HeaderSet(); + HeaderSet reply = new HeaderSet(); + + length = mInput.read(); + length = (length << 8) + mInput.read(); + flags = mInput.read(); + constants = mInput.read(); + + if (length > ObexHelper.getMaxRxPacketSize(mTransport)) { + code = ResponseCodes.OBEX_HTTP_REQ_TOO_LARGE; + totalLength = 3; + } else { + if (length > 5) { + byte[] headers = new byte[length - 5]; + bytesReceived = mInput.read(headers); + + while (bytesReceived != headers.length) { + bytesReceived += mInput.read(headers, bytesReceived, headers.length + - bytesReceived); + } + + ObexHelper.updateHeaderSet(request, headers); + + if (mListener.getConnectionId() != -1 && request.mConnectionID != null) { + mListener.setConnectionId(ObexHelper.convertToLong(request.mConnectionID)); + } else { + mListener.setConnectionId(1); + } + // the Auth chan is initiated by the server, client sent back the authResp . + if (request.mAuthResp != null) { + if (!handleAuthResp(request.mAuthResp)) { + code = ResponseCodes.OBEX_HTTP_UNAUTHORIZED; + mListener.onAuthenticationFailure(ObexHelper.getTagValue((byte)0x01, + request.mAuthResp)); + } + request.mAuthResp = null; + } + } + + if (code != ResponseCodes.OBEX_HTTP_UNAUTHORIZED) { + // the Auth challenge is initiated by the client + // the server will send back the authResp to the client + if (request.mAuthChall != null) { + handleAuthChall(request); + reply.mAuthResp = new byte[request.mAuthResp.length]; + System.arraycopy(request.mAuthResp, 0, reply.mAuthResp, 0, + reply.mAuthResp.length); + request.mAuthChall = null; + request.mAuthResp = null; + } + boolean backup = false; + boolean create = true; + if (!((flags & 1) == 0)) { + backup = true; + } + if (!((flags & 2) == 0)) { + create = false; + } + + try { + code = mListener.onSetPath(request, reply, backup, create); + } catch (Exception e) { + if(V) Log.d(TAG,"Exception occurred - sending OBEX_HTTP_INTERNAL_ERROR reply", + e); + sendResponse(ResponseCodes.OBEX_HTTP_INTERNAL_ERROR, null); + return; + } + + code = validateResponseCode(code); + + if (reply.nonce != null) { + mChallengeDigest = new byte[16]; + System.arraycopy(reply.nonce, 0, mChallengeDigest, 0, 16); + } else { + mChallengeDigest = null; + } + + long id = mListener.getConnectionId(); + if (id == -1) { + reply.mConnectionID = null; + } else { + reply.mConnectionID = ObexHelper.convertToByteArray(id); + } + + head = ObexHelper.createHeader(reply, false); + totalLength += head.length; + + if (totalLength > mMaxPacketLength) { + totalLength = 3; + head = null; + code = ResponseCodes.OBEX_HTTP_INTERNAL_ERROR; + } + } + } + + // Compute Length of OBEX SETPATH packet + byte[] replyData = new byte[totalLength]; + replyData[0] = (byte)code; + replyData[1] = (byte)(totalLength >> 8); + replyData[2] = (byte)totalLength; + if (head != null) { + System.arraycopy(head, 0, replyData, 3, head.length); + } + /* + * Write the OBEX SETPATH packet to the server. Byte 0: response code + * Byte 1&2: Connect Packet Length Byte 3 to n: headers + */ + mOutput.write(replyData); + mOutput.flush(); + } + + /** + * Handles a disconnect request from a client. This method will read the + * rest of the request from the client. Assuming the request is valid, it + * will create a <code>HeaderSet</code> object to pass to the + * <code>ServerRequestHandler</code> object. After the handler processes the + * request, this method will create a reply message to send to the server. + * @throws IOException if an error occurred at the transport layer + */ + private void handleDisconnectRequest() throws IOException { + int length; + int code = ResponseCodes.OBEX_HTTP_OK; + int totalLength = 3; + byte[] head = null; + int bytesReceived; + HeaderSet request = new HeaderSet(); + HeaderSet reply = new HeaderSet(); + + length = mInput.read(); + length = (length << 8) + mInput.read(); + + if (length > ObexHelper.getMaxRxPacketSize(mTransport)) { + code = ResponseCodes.OBEX_HTTP_REQ_TOO_LARGE; + totalLength = 3; + } else { + if (length > 3) { + byte[] headers = new byte[length - 3]; + bytesReceived = mInput.read(headers); + + while (bytesReceived != headers.length) { + bytesReceived += mInput.read(headers, bytesReceived, headers.length + - bytesReceived); + } + + ObexHelper.updateHeaderSet(request, headers); + } + + if (mListener.getConnectionId() != -1 && request.mConnectionID != null) { + mListener.setConnectionId(ObexHelper.convertToLong(request.mConnectionID)); + } else { + mListener.setConnectionId(1); + } + + if (request.mAuthResp != null) { + if (!handleAuthResp(request.mAuthResp)) { + code = ResponseCodes.OBEX_HTTP_UNAUTHORIZED; + mListener.onAuthenticationFailure(ObexHelper.getTagValue((byte)0x01, + request.mAuthResp)); + } + request.mAuthResp = null; + } + + if (code != ResponseCodes.OBEX_HTTP_UNAUTHORIZED) { + + if (request.mAuthChall != null) { + handleAuthChall(request); + request.mAuthChall = null; + } + + try { + mListener.onDisconnect(request, reply); + } catch (Exception e) { + if(V) Log.d(TAG,"Exception occurred - sending OBEX_HTTP_INTERNAL_ERROR reply", + e); + sendResponse(ResponseCodes.OBEX_HTTP_INTERNAL_ERROR, null); + return; + } + + long id = mListener.getConnectionId(); + if (id == -1) { + reply.mConnectionID = null; + } else { + reply.mConnectionID = ObexHelper.convertToByteArray(id); + } + + head = ObexHelper.createHeader(reply, false); + totalLength += head.length; + + if (totalLength > mMaxPacketLength) { + totalLength = 3; + head = null; + code = ResponseCodes.OBEX_HTTP_INTERNAL_ERROR; + } + } + } + + // Compute Length of OBEX CONNECT packet + byte[] replyData; + if (head != null) { + replyData = new byte[3 + head.length]; + } else { + replyData = new byte[3]; + } + replyData[0] = (byte)code; + replyData[1] = (byte)(totalLength >> 8); + replyData[2] = (byte)totalLength; + if (head != null) { + System.arraycopy(head, 0, replyData, 3, head.length); + } + /* + * Write the OBEX DISCONNECT packet to the server. Byte 0: response code + * Byte 1&2: Connect Packet Length Byte 3 to n: headers + */ + mOutput.write(replyData); + mOutput.flush(); + } + + /** + * Handles a connect request from a client. This method will read the rest + * of the request from the client. Assuming the request is valid, it will + * create a <code>HeaderSet</code> object to pass to the + * <code>ServerRequestHandler</code> object. After the handler processes the + * request, this method will create a reply message to send to the server + * with the response code provided. + * @throws IOException if an error occurred at the transport layer + */ + private void handleConnectRequest() throws IOException { + int packetLength; + @SuppressWarnings("unused") + int version; + @SuppressWarnings("unused") + int flags; + int totalLength = 7; + byte[] head = null; + int code = -1; + HeaderSet request = new HeaderSet(); + HeaderSet reply = new HeaderSet(); + int bytesReceived; + + if(V) Log.v(TAG,"handleConnectRequest()"); + + /* + * Read in the length of the OBEX packet, OBEX version, flags, and max + * packet length + */ + packetLength = mInput.read(); + packetLength = (packetLength << 8) + mInput.read(); + if(V) Log.v(TAG,"handleConnectRequest() - packetLength: " + packetLength); + + version = mInput.read(); + flags = mInput.read(); + mMaxPacketLength = mInput.read(); + mMaxPacketLength = (mMaxPacketLength << 8) + mInput.read(); + + if(V) Log.v(TAG,"handleConnectRequest() - version: " + version + + " MaxLength: " + mMaxPacketLength + " flags: " + flags); + + // should we check it? + if (mMaxPacketLength > ObexHelper.MAX_PACKET_SIZE_INT) { + mMaxPacketLength = ObexHelper.MAX_PACKET_SIZE_INT; + } + + if(mMaxPacketLength > ObexHelper.getMaxTxPacketSize(mTransport)) { + Log.w(TAG, "Requested MaxObexPacketSize " + mMaxPacketLength + + " is larger than the max size supported by the transport: " + + ObexHelper.getMaxTxPacketSize(mTransport) + + " Reducing to this size."); + mMaxPacketLength = ObexHelper.getMaxTxPacketSize(mTransport); + } + + if (packetLength > ObexHelper.getMaxRxPacketSize(mTransport)) { + code = ResponseCodes.OBEX_HTTP_REQ_TOO_LARGE; + totalLength = 7; + } else { + if (packetLength > 7) { + byte[] headers = new byte[packetLength - 7]; + bytesReceived = mInput.read(headers); + + while (bytesReceived != headers.length) { + bytesReceived += mInput.read(headers, bytesReceived, headers.length + - bytesReceived); + } + + ObexHelper.updateHeaderSet(request, headers); + } + + if (mListener.getConnectionId() != -1 && request.mConnectionID != null) { + mListener.setConnectionId(ObexHelper.convertToLong(request.mConnectionID)); + } else { + mListener.setConnectionId(1); + } + + if (request.mAuthResp != null) { + if (!handleAuthResp(request.mAuthResp)) { + code = ResponseCodes.OBEX_HTTP_UNAUTHORIZED; + mListener.onAuthenticationFailure(ObexHelper.getTagValue((byte)0x01, + request.mAuthResp)); + } + request.mAuthResp = null; + } + + if (code != ResponseCodes.OBEX_HTTP_UNAUTHORIZED) { + if (request.mAuthChall != null) { + handleAuthChall(request); + reply.mAuthResp = new byte[request.mAuthResp.length]; + System.arraycopy(request.mAuthResp, 0, reply.mAuthResp, 0, + reply.mAuthResp.length); + request.mAuthChall = null; + request.mAuthResp = null; + } + + try { + code = mListener.onConnect(request, reply); + code = validateResponseCode(code); + + if (reply.nonce != null) { + mChallengeDigest = new byte[16]; + System.arraycopy(reply.nonce, 0, mChallengeDigest, 0, 16); + } else { + mChallengeDigest = null; + } + long id = mListener.getConnectionId(); + if (id == -1) { + reply.mConnectionID = null; + } else { + reply.mConnectionID = ObexHelper.convertToByteArray(id); + } + + head = ObexHelper.createHeader(reply, false); + totalLength += head.length; + + if (totalLength > mMaxPacketLength) { + totalLength = 7; + head = null; + code = ResponseCodes.OBEX_HTTP_INTERNAL_ERROR; + } + } catch (Exception e) { + if(V) Log.d(TAG,"Exception occurred - sending OBEX_HTTP_INTERNAL_ERROR reply", + e); + totalLength = 7; + head = null; + code = ResponseCodes.OBEX_HTTP_INTERNAL_ERROR; + } + + } + } + + // Compute Length of OBEX CONNECT packet + byte[] length = ObexHelper.convertToByteArray(totalLength); + + /* + * Write the OBEX CONNECT packet to the server. Byte 0: response code + * Byte 1&2: Connect Packet Length Byte 3: OBEX Version Number + * (Presently, 0x10) Byte 4: Flags (For TCP 0x00) Byte 5&6: Max OBEX + * Packet Length (Defined in MAX_PACKET_SIZE) Byte 7 to n: headers + */ + byte[] sendData = new byte[totalLength]; + int maxRxLength = ObexHelper.getMaxRxPacketSize(mTransport); + if (maxRxLength > mMaxPacketLength) { + if(V) Log.v(TAG,"Set maxRxLength to min of maxRxServrLen:" + maxRxLength + + " and MaxNegotiated from Client: " + mMaxPacketLength); + maxRxLength = mMaxPacketLength; + } + sendData[0] = (byte)code; + sendData[1] = length[2]; + sendData[2] = length[3]; + sendData[3] = (byte)0x10; + sendData[4] = (byte)0x00; + sendData[5] = (byte)(maxRxLength >> 8); + sendData[6] = (byte)(maxRxLength & 0xFF); + + if (head != null) { + System.arraycopy(head, 0, sendData, 7, head.length); + } + + mOutput.write(sendData); + mOutput.flush(); + } + + /** + * Closes the server session - in detail close I/O streams and the + * underlying transport layer. Internal flag is also set so that later + * attempt to read/write will throw an exception. + */ + public synchronized void close() { + if (mListener != null) { + mListener.onClose(); + } + try { + /* Set state to closed before interrupting the thread by closing the streams */ + mClosed = true; + if(mInput != null) + mInput.close(); + if(mOutput != null) + mOutput.close(); + if(mTransport != null) + mTransport.close(); + } catch (Exception e) { + if(V) Log.d(TAG,"Exception occurred during close() - ignore",e); + } + mTransport = null; + mInput = null; + mOutput = null; + mListener = null; + } + + /** + * Verifies that the response code is valid. If it is not valid, it will + * return the <code>OBEX_HTTP_INTERNAL_ERROR</code> response code. + * @param code the response code to check + * @return the valid response code or <code>OBEX_HTTP_INTERNAL_ERROR</code> + * if <code>code</code> is not valid + */ + private int validateResponseCode(int code) { + + if ((code >= ResponseCodes.OBEX_HTTP_OK) && (code <= ResponseCodes.OBEX_HTTP_PARTIAL)) { + return code; + } + if ((code >= ResponseCodes.OBEX_HTTP_MULT_CHOICE) + && (code <= ResponseCodes.OBEX_HTTP_USE_PROXY)) { + return code; + } + if ((code >= ResponseCodes.OBEX_HTTP_BAD_REQUEST) + && (code <= ResponseCodes.OBEX_HTTP_UNSUPPORTED_TYPE)) { + return code; + } + if ((code >= ResponseCodes.OBEX_HTTP_INTERNAL_ERROR) + && (code <= ResponseCodes.OBEX_HTTP_VERSION)) { + return code; + } + if ((code >= ResponseCodes.OBEX_DATABASE_FULL) + && (code <= ResponseCodes.OBEX_DATABASE_LOCKED)) { + return code; + } + return ResponseCodes.OBEX_HTTP_INTERNAL_ERROR; + } + + /** @hide */ + public ObexTransport getTransport() { + return mTransport; + } +} diff --git a/javax/obex/SessionNotifier.java b/javax/obex/SessionNotifier.java new file mode 100644 index 0000000..d2760f7 --- /dev/null +++ b/javax/obex/SessionNotifier.java @@ -0,0 +1,127 @@ +/* + * 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 java.io.IOException; + +/** + * The <code>SessionNotifier</code> interface defines a connection notifier for + * server-side OBEX connections. When a <code>SessionNotifier</code> is created + * and calls <code>acceptAndOpen()</code>, it will begin listening for clients + * to create a connection at the transport layer. When the transport layer + * connection is received, the <code>acceptAndOpen()</code> method will return a + * <code>javax.microedition.io.Connection</code> that is the connection to the + * client. The <code>acceptAndOpen()</code> method also takes a + * <code>ServerRequestHandler</code> argument that will process the requests + * from the client that connects to the server. + */ +public interface SessionNotifier { + + /** + * Waits for a transport layer connection to be established and specifies + * the handler to handle the requests from the client. No authenticator is + * associated with this connection, therefore, it is implementation + * dependent as to how an authentication challenge and authentication + * response header will be received and processed. + * <P> + * <H4>Additional Note for OBEX over Bluetooth</H4> If this method is called + * on a <code>SessionNotifier</code> object that does not have a + * <code>ServiceRecord</code> in the SDDB, the <code>ServiceRecord</code> + * for this object will be added to the SDDB. This method requests the BCC + * to put the local device in connectable mode so that it will respond to + * connection attempts by clients. + * <P> + * The following checks are done to verify that the service record provided + * is valid. If any of these checks fail, then a + * <code>ServiceRegistrationException</code> is thrown. + * <UL> + * <LI>ServiceClassIDList and ProtocolDescriptorList, the mandatory service + * attributes for a <code>btgoep</code> service record, must be present in + * the <code>ServiceRecord</code> associated with this notifier. + * <LI>L2CAP, RFCOMM and OBEX must all be in the ProtocolDescriptorList + * <LI>The <code>ServiceRecord</code> associated with this notifier must not + * have changed the RFCOMM server channel number + * </UL> + * <P> + * This method will not ensure that <code>ServiceRecord</code> associated + * with this notifier is a completely valid service record. It is the + * responsibility of the application to ensure that the service record + * follows all of the applicable syntactic and semantic rules for service + * record correctness. + * @param handler the request handler that will respond to OBEX requests + * @return the connection to the client + * @throws IOException if an error occurs in the transport layer + * @throws NullPointerException if <code>handler</code> is <code>null</code> + */ + ObexSession acceptAndOpen(ServerRequestHandler handler) throws IOException; + + /** + * Waits for a transport layer connection to be established and specifies + * the handler to handle the requests from the client and the + * <code>Authenticator</code> to use to respond to authentication challenge + * and authentication response headers. + * <P> + * <H4>Additional Note for OBEX over Bluetooth</H4> If this method is called + * on a <code>SessionNotifier</code> object that does not have a + * <code>ServiceRecord</code> in the SDDB, the <code>ServiceRecord</code> + * for this object will be added to the SDDB. This method requests the BCC + * to put the local device in connectable mode so that it will respond to + * connection attempts by clients. + * <P> + * The following checks are done to verify that the service record provided + * is valid. If any of these checks fail, then a + * <code>ServiceRegistrationException</code> is thrown. + * <UL> + * <LI>ServiceClassIDList and ProtocolDescriptorList, the mandatory service + * attributes for a <code>btgoep</code> service record, must be present in + * the <code>ServiceRecord</code> associated with this notifier. + * <LI>L2CAP, RFCOMM and OBEX must all be in the ProtocolDescriptorList + * <LI>The <code>ServiceRecord</code> associated with this notifier must not + * have changed the RFCOMM server channel number + * </UL> + * <P> + * This method will not ensure that <code>ServiceRecord</code> associated + * with this notifier is a completely valid service record. It is the + * responsibility of the application to ensure that the service record + * follows all of the applicable syntactic and semantic rules for service + * record correctness. + * @param handler the request handler that will respond to OBEX requests + * @param auth the <code>Authenticator</code> to use with this connection; + * if <code>null</code> then no <code>Authenticator</code> will be + * used + * @return the connection to the client + * @throws IOException if an error occurs in the transport layer + * @throws NullPointerException if <code>handler</code> is <code>null</code> + */ + ObexSession acceptAndOpen(ServerRequestHandler handler, Authenticator auth) throws IOException; +} |