diff options
Diffstat (limited to 'src/com/android/obex/ClientSession.java')
-rw-r--r-- | src/com/android/obex/ClientSession.java | 637 |
1 files changed, 637 insertions, 0 deletions
diff --git a/src/com/android/obex/ClientSession.java b/src/com/android/obex/ClientSession.java new file mode 100644 index 0000000..c3d876f --- /dev/null +++ b/src/com/android/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 com.android.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; + } +} |