Home | History | Annotate | Download | only in obex
      1 /*
      2  * Copyright (c) 2015 The Android Open Source Project
      3  * Copyright (C) 2015 Samsung LSI
      4  * Copyright (c) 2008-2009, Motorola, Inc.
      5  *
      6  * All rights reserved.
      7  *
      8  * Redistribution and use in source and binary forms, with or without
      9  * modification, are permitted provided that the following conditions are met:
     10  *
     11  * - Redistributions of source code must retain the above copyright notice,
     12  * this list of conditions and the following disclaimer.
     13  *
     14  * - Redistributions in binary form must reproduce the above copyright notice,
     15  * this list of conditions and the following disclaimer in the documentation
     16  * and/or other materials provided with the distribution.
     17  *
     18  * - Neither the name of the Motorola, Inc. nor the names of its contributors
     19  * may be used to endorse or promote products derived from this software
     20  * without specific prior written permission.
     21  *
     22  * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
     23  * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
     24  * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
     25  * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
     26  * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
     27  * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
     28  * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
     29  * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
     30  * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
     31  * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
     32  * POSSIBILITY OF SUCH DAMAGE.
     33  */
     34 
     35 package javax.obex;
     36 
     37 import java.io.ByteArrayOutputStream;
     38 import java.io.IOException;
     39 import java.io.InputStream;
     40 import java.io.OutputStream;
     41 
     42 import android.util.Log;
     43 
     44 /**
     45  * This class in an implementation of the OBEX ClientSession.
     46  * @hide
     47  */
     48 public final class ClientSession extends ObexSession {
     49 
     50     private static final String TAG = "ClientSession";
     51 
     52     private boolean mOpen;
     53 
     54     // Determines if an OBEX layer connection has been established
     55     private boolean mObexConnected;
     56 
     57     private byte[] mConnectionId = null;
     58 
     59     /*
     60      * The max Packet size must be at least 255 according to the OBEX
     61      * specification.
     62      */
     63     private int mMaxTxPacketSize = ObexHelper.LOWER_LIMIT_MAX_PACKET_SIZE;
     64 
     65     private boolean mRequestActive;
     66 
     67     private final InputStream mInput;
     68 
     69     private final OutputStream mOutput;
     70 
     71     private final boolean mLocalSrmSupported;
     72 
     73     private final ObexTransport mTransport;
     74 
     75     public ClientSession(final ObexTransport trans) throws IOException {
     76         mInput = trans.openInputStream();
     77         mOutput = trans.openOutputStream();
     78         mOpen = true;
     79         mRequestActive = false;
     80         mLocalSrmSupported = trans.isSrmSupported();
     81         mTransport = trans;
     82     }
     83 
     84     /**
     85      * Create a ClientSession
     86      * @param trans The transport to use for OBEX transactions
     87      * @param supportsSrm True if Single Response Mode should be used e.g. if the
     88      *        supplied transport is a TCP or l2cap channel.
     89      * @throws IOException if it occurs while opening the transport streams.
     90      */
     91     public ClientSession(final ObexTransport trans, final boolean supportsSrm) throws IOException {
     92         mInput = trans.openInputStream();
     93         mOutput = trans.openOutputStream();
     94         mOpen = true;
     95         mRequestActive = false;
     96         mLocalSrmSupported = supportsSrm;
     97         mTransport = trans;
     98     }
     99 
    100     public HeaderSet connect(final HeaderSet header) throws IOException {
    101         ensureOpen();
    102         if (mObexConnected) {
    103             throw new IOException("Already connected to server");
    104         }
    105         setRequestActive();
    106 
    107         int totalLength = 4;
    108         byte[] head = null;
    109 
    110         // Determine the header byte array
    111         if (header != null) {
    112             if (header.nonce != null) {
    113                 mChallengeDigest = new byte[16];
    114                 System.arraycopy(header.nonce, 0, mChallengeDigest, 0, 16);
    115             }
    116             head = ObexHelper.createHeader(header, false);
    117             totalLength += head.length;
    118         }
    119         /*
    120         * Write the OBEX CONNECT packet to the server.
    121         * Byte 0: 0x80
    122         * Byte 1&2: Connect Packet Length
    123         * Byte 3: OBEX Version Number (Presently, 0x10)
    124         * Byte 4: Flags (For TCP 0x00)
    125         * Byte 5&6: Max OBEX Packet Length (Defined in MAX_PACKET_SIZE)
    126         * Byte 7 to n: headers
    127         */
    128         byte[] requestPacket = new byte[totalLength];
    129         int maxRxPacketSize = ObexHelper.getMaxRxPacketSize(mTransport);
    130         // We just need to start at  byte 3 since the sendRequest() method will
    131         // handle the length and 0x80.
    132         requestPacket[0] = (byte)0x10;
    133         requestPacket[1] = (byte)0x00;
    134         requestPacket[2] = (byte)(maxRxPacketSize >> 8);
    135         requestPacket[3] = (byte)(maxRxPacketSize & 0xFF);
    136         if (head != null) {
    137             System.arraycopy(head, 0, requestPacket, 4, head.length);
    138         }
    139 
    140         // Since we are not yet connected, the peer max packet size is unknown,
    141         // hence we are only guaranteed the server will use the first 7 bytes.
    142         if ((requestPacket.length + 3) > ObexHelper.MAX_PACKET_SIZE_INT) {
    143             throw new IOException("Packet size exceeds max packet size for connect");
    144         }
    145 
    146         HeaderSet returnHeaderSet = new HeaderSet();
    147         sendRequest(ObexHelper.OBEX_OPCODE_CONNECT, requestPacket, returnHeaderSet, null, false);
    148 
    149         /*
    150         * Read the response from the OBEX server.
    151         * Byte 0: Response Code (If successful then OBEX_HTTP_OK)
    152         * Byte 1&2: Packet Length
    153         * Byte 3: OBEX Version Number
    154         * Byte 4: Flags3
    155         * Byte 5&6: Max OBEX packet Length
    156         * Byte 7 to n: Optional HeaderSet
    157         */
    158         if (returnHeaderSet.responseCode == ResponseCodes.OBEX_HTTP_OK) {
    159             mObexConnected = true;
    160         }
    161         setRequestInactive();
    162 
    163         return returnHeaderSet;
    164     }
    165 
    166     public Operation get(HeaderSet header) throws IOException {
    167 
    168         if (!mObexConnected) {
    169             throw new IOException("Not connected to the server");
    170         }
    171         setRequestActive();
    172 
    173         ensureOpen();
    174 
    175         HeaderSet head;
    176         if (header == null) {
    177             head = new HeaderSet();
    178         } else {
    179             head = header;
    180             if (head.nonce != null) {
    181                 mChallengeDigest = new byte[16];
    182                 System.arraycopy(head.nonce, 0, mChallengeDigest, 0, 16);
    183             }
    184         }
    185         // Add the connection ID if one exists
    186         if (mConnectionId != null) {
    187             head.mConnectionID = new byte[4];
    188             System.arraycopy(mConnectionId, 0, head.mConnectionID, 0, 4);
    189         }
    190 
    191         if(mLocalSrmSupported) {
    192             head.setHeader(HeaderSet.SINGLE_RESPONSE_MODE, ObexHelper.OBEX_SRM_ENABLE);
    193             /* TODO: Consider creating an interface to get the wait state.
    194              * On an android system, I cannot see when this is to be used.
    195              * except perhaps if we are to wait for user accept on a push message.
    196             if(getLocalWaitState()) {
    197                 head.setHeader(HeaderSet.SINGLE_RESPONSE_MODE_PARAMETER, ObexHelper.OBEX_SRMP_WAIT);
    198             }
    199             */
    200         }
    201 
    202         return new ClientOperation(mMaxTxPacketSize, this, head, true);
    203     }
    204 
    205     /**
    206      * 0xCB Connection Id an identifier used for OBEX connection multiplexing
    207      */
    208     public void setConnectionID(long id) {
    209         if ((id < 0) || (id > 0xFFFFFFFFL)) {
    210             throw new IllegalArgumentException("Connection ID is not in a valid range");
    211         }
    212         mConnectionId = ObexHelper.convertToByteArray(id);
    213     }
    214 
    215     public HeaderSet delete(HeaderSet header) throws IOException {
    216 
    217         Operation op = put(header);
    218         op.getResponseCode();
    219         HeaderSet returnValue = op.getReceivedHeader();
    220         op.close();
    221 
    222         return returnValue;
    223     }
    224 
    225     public HeaderSet disconnect(HeaderSet header) throws IOException {
    226         if (!mObexConnected) {
    227             throw new IOException("Not connected to the server");
    228         }
    229         setRequestActive();
    230 
    231         ensureOpen();
    232         // Determine the header byte array
    233         byte[] head = null;
    234         if (header != null) {
    235             if (header.nonce != null) {
    236                 mChallengeDigest = new byte[16];
    237                 System.arraycopy(header.nonce, 0, mChallengeDigest, 0, 16);
    238             }
    239             // Add the connection ID if one exists
    240             if (mConnectionId != null) {
    241                 header.mConnectionID = new byte[4];
    242                 System.arraycopy(mConnectionId, 0, header.mConnectionID, 0, 4);
    243             }
    244             head = ObexHelper.createHeader(header, false);
    245 
    246             if ((head.length + 3) > mMaxTxPacketSize) {
    247                 throw new IOException("Packet size exceeds max packet size");
    248             }
    249         } else {
    250             // Add the connection ID if one exists
    251             if (mConnectionId != null) {
    252                 head = new byte[5];
    253                 head[0] = (byte)HeaderSet.CONNECTION_ID;
    254                 System.arraycopy(mConnectionId, 0, head, 1, 4);
    255             }
    256         }
    257 
    258         HeaderSet returnHeaderSet = new HeaderSet();
    259         sendRequest(ObexHelper.OBEX_OPCODE_DISCONNECT, head, returnHeaderSet, null, false);
    260 
    261         /*
    262          * An OBEX DISCONNECT reply from the server:
    263          * Byte 1: Response code
    264          * Bytes 2 & 3: packet size
    265          * Bytes 4 & up: headers
    266          */
    267 
    268         /* response code , and header are ignored
    269          * */
    270 
    271         synchronized (this) {
    272             mObexConnected = false;
    273             setRequestInactive();
    274         }
    275 
    276         return returnHeaderSet;
    277     }
    278 
    279     public long getConnectionID() {
    280 
    281         if (mConnectionId == null) {
    282             return -1;
    283         }
    284         return ObexHelper.convertToLong(mConnectionId);
    285     }
    286 
    287     public Operation put(HeaderSet header) throws IOException {
    288         if (!mObexConnected) {
    289             throw new IOException("Not connected to the server");
    290         }
    291         setRequestActive();
    292 
    293         ensureOpen();
    294         HeaderSet head;
    295         if (header == null) {
    296             head = new HeaderSet();
    297         } else {
    298             head = header;
    299             // when auth is initiated by client ,save the digest
    300             if (head.nonce != null) {
    301                 mChallengeDigest = new byte[16];
    302                 System.arraycopy(head.nonce, 0, mChallengeDigest, 0, 16);
    303             }
    304         }
    305 
    306         // Add the connection ID if one exists
    307         if (mConnectionId != null) {
    308 
    309             head.mConnectionID = new byte[4];
    310             System.arraycopy(mConnectionId, 0, head.mConnectionID, 0, 4);
    311         }
    312 
    313         if(mLocalSrmSupported) {
    314             head.setHeader(HeaderSet.SINGLE_RESPONSE_MODE, ObexHelper.OBEX_SRM_ENABLE);
    315             /* TODO: Consider creating an interface to get the wait state.
    316              * On an android system, I cannot see when this is to be used.
    317             if(getLocalWaitState()) {
    318                 head.setHeader(HeaderSet.SINGLE_RESPONSE_MODE_PARAMETER, ObexHelper.OBEX_SRMP_WAIT);
    319             }
    320              */
    321         }
    322         return new ClientOperation(mMaxTxPacketSize, this, head, false);
    323     }
    324 
    325     public void setAuthenticator(Authenticator auth) throws IOException {
    326         if (auth == null) {
    327             throw new IOException("Authenticator may not be null");
    328         }
    329         mAuthenticator = auth;
    330     }
    331 
    332     public HeaderSet setPath(HeaderSet header, boolean backup, boolean create) throws IOException {
    333         if (!mObexConnected) {
    334             throw new IOException("Not connected to the server");
    335         }
    336         setRequestActive();
    337         ensureOpen();
    338 
    339         int totalLength = 2;
    340         byte[] head = null;
    341         HeaderSet headset;
    342         if (header == null) {
    343             headset = new HeaderSet();
    344         } else {
    345             headset = header;
    346             if (headset.nonce != null) {
    347                 mChallengeDigest = new byte[16];
    348                 System.arraycopy(headset.nonce, 0, mChallengeDigest, 0, 16);
    349             }
    350         }
    351 
    352         // when auth is initiated by client ,save the digest
    353         if (headset.nonce != null) {
    354             mChallengeDigest = new byte[16];
    355             System.arraycopy(headset.nonce, 0, mChallengeDigest, 0, 16);
    356         }
    357 
    358         // Add the connection ID if one exists
    359         if (mConnectionId != null) {
    360             headset.mConnectionID = new byte[4];
    361             System.arraycopy(mConnectionId, 0, headset.mConnectionID, 0, 4);
    362         }
    363 
    364         head = ObexHelper.createHeader(headset, false);
    365         totalLength += head.length;
    366 
    367         if (totalLength > mMaxTxPacketSize) {
    368             throw new IOException("Packet size exceeds max packet size");
    369         }
    370 
    371         int flags = 0;
    372         /*
    373          * The backup flag bit is bit 0 so if we add 1, this will set that bit
    374          */
    375         if (backup) {
    376             flags++;
    377         }
    378         /*
    379          * The create bit is bit 1 so if we or with 2 the bit will be set.
    380          */
    381         if (!create) {
    382             flags |= 2;
    383         }
    384 
    385         /*
    386          * An OBEX SETPATH packet to the server:
    387          * Byte 1: 0x85
    388          * Byte 2 & 3: packet size
    389          * Byte 4: flags
    390          * Byte 5: constants
    391          * Byte 6 & up: headers
    392          */
    393         byte[] packet = new byte[totalLength];
    394         packet[0] = (byte)flags;
    395         packet[1] = (byte)0x00;
    396         if (headset != null) {
    397             System.arraycopy(head, 0, packet, 2, head.length);
    398         }
    399 
    400         HeaderSet returnHeaderSet = new HeaderSet();
    401         sendRequest(ObexHelper.OBEX_OPCODE_SETPATH, packet, returnHeaderSet, null, false);
    402 
    403         /*
    404          * An OBEX SETPATH reply from the server:
    405          * Byte 1: Response code
    406          * Bytes 2 & 3: packet size
    407          * Bytes 4 & up: headers
    408          */
    409 
    410         setRequestInactive();
    411 
    412         return returnHeaderSet;
    413     }
    414 
    415     /**
    416      * Verifies that the connection is open.
    417      * @throws IOException if the connection is closed
    418      */
    419     public synchronized void ensureOpen() throws IOException {
    420         if (!mOpen) {
    421             throw new IOException("Connection closed");
    422         }
    423     }
    424 
    425     /**
    426      * Set request inactive. Allows Put and get operation objects to tell this
    427      * object when they are done.
    428      */
    429     /*package*/synchronized void setRequestInactive() {
    430         mRequestActive = false;
    431     }
    432 
    433     /**
    434      * Set request to active.
    435      * @throws IOException if already active
    436      */
    437     private synchronized void setRequestActive() throws IOException {
    438         if (mRequestActive) {
    439             throw new IOException("OBEX request is already being performed");
    440         }
    441         mRequestActive = true;
    442     }
    443 
    444     /**
    445      * Sends a standard request to the client. It will then wait for the reply
    446      * and update the header set object provided. If any authentication headers
    447      * (i.e. authentication challenge or authentication response) are received,
    448      * they will be processed.
    449      * @param opCode the type of request to send to the client
    450      * @param head the headers to send to the client
    451      * @param header the header object to update with the response
    452      * @param privateInput the input stream used by the Operation object; null
    453      *        if this is called on a CONNECT, SETPATH or DISCONNECT
    454      * @return
    455      *        <code>true</code> if the operation completed successfully;
    456      *        <code>false</code> if an authentication response failed to pass
    457      * @throws IOException if an IO error occurs
    458      */
    459     public boolean sendRequest(int opCode, byte[] head, HeaderSet header,
    460             PrivateInputStream privateInput, boolean srmActive) throws IOException {
    461         //check header length with local max size
    462         if (head != null) {
    463             if ((head.length + 3) > ObexHelper.MAX_PACKET_SIZE_INT) {
    464                 // TODO: This is an implementation limit - not a specification requirement.
    465                 throw new IOException("header too large ");
    466             }
    467         }
    468 
    469         boolean skipSend = false;
    470         boolean skipReceive = false;
    471         if (srmActive == true) {
    472             if (opCode == ObexHelper.OBEX_OPCODE_PUT) {
    473                 // we are in the middle of a SRM PUT operation, don't expect a continue.
    474                 skipReceive = true;
    475             } else if (opCode == ObexHelper.OBEX_OPCODE_GET) {
    476                 // We are still sending the get request, send, but don't expect continue
    477                 // until the request is transfered (the final bit is set)
    478                 skipReceive = true;
    479             } else if (opCode == ObexHelper.OBEX_OPCODE_GET_FINAL) {
    480                 // All done sending the request, expect data from the server, without
    481                 // sending continue.
    482                 skipSend = true;
    483             }
    484 
    485         }
    486 
    487         int bytesReceived;
    488         ByteArrayOutputStream out = new ByteArrayOutputStream();
    489         out.write((byte)opCode);
    490 
    491         // Determine if there are any headers to send
    492         if (head == null) {
    493             out.write(0x00);
    494             out.write(0x03);
    495         } else {
    496             out.write((byte)((head.length + 3) >> 8));
    497             out.write((byte)(head.length + 3));
    498             out.write(head);
    499         }
    500 
    501         if (!skipSend) {
    502             // Write the request to the output stream and flush the stream
    503             mOutput.write(out.toByteArray());
    504             // TODO: is this really needed? if this flush is implemented
    505             //       correctly, we will get a gap between each obex packet.
    506             //       which is kind of the idea behind SRM to avoid.
    507             //  Consider offloading to another thread (async action)
    508             mOutput.flush();
    509         }
    510 
    511         if (!skipReceive) {
    512             header.responseCode = mInput.read();
    513 
    514             int length = ((mInput.read() << 8) | (mInput.read()));
    515 
    516             if (length > ObexHelper.getMaxRxPacketSize(mTransport)) {
    517                 throw new IOException("Packet received exceeds packet size limit");
    518             }
    519             if (length > ObexHelper.BASE_PACKET_LENGTH) {
    520                 byte[] data = null;
    521                 if (opCode == ObexHelper.OBEX_OPCODE_CONNECT) {
    522                     @SuppressWarnings("unused")
    523                     int version = mInput.read();
    524                     @SuppressWarnings("unused")
    525                     int flags = mInput.read();
    526                     mMaxTxPacketSize = (mInput.read() << 8) + mInput.read();
    527 
    528                     //check with local max size
    529                     if (mMaxTxPacketSize > ObexHelper.MAX_CLIENT_PACKET_SIZE) {
    530                         mMaxTxPacketSize = ObexHelper.MAX_CLIENT_PACKET_SIZE;
    531                     }
    532 
    533                     // check with transport maximum size
    534                     if(mMaxTxPacketSize > ObexHelper.getMaxTxPacketSize(mTransport)) {
    535                         // To increase this size, increase the buffer size in L2CAP layer
    536                         // in Bluedroid.
    537                         Log.w(TAG, "An OBEX packet size of " + mMaxTxPacketSize + "was"
    538                                 + " requested. Transport only allows: "
    539                                 + ObexHelper.getMaxTxPacketSize(mTransport)
    540                                 + " Lowering limit to this value.");
    541                         mMaxTxPacketSize = ObexHelper.getMaxTxPacketSize(mTransport);
    542                     }
    543 
    544                     if (length > 7) {
    545                         data = new byte[length - 7];
    546 
    547                         bytesReceived = mInput.read(data);
    548                         while (bytesReceived != (length - 7)) {
    549                             bytesReceived += mInput.read(data, bytesReceived, data.length
    550                                     - bytesReceived);
    551                         }
    552                     } else {
    553                         return true;
    554                     }
    555                 } else {
    556                     data = new byte[length - 3];
    557                     bytesReceived = mInput.read(data);
    558 
    559                     while (bytesReceived != (length - 3)) {
    560                         bytesReceived += mInput.read(data, bytesReceived, data.length - bytesReceived);
    561                     }
    562                     if (opCode == ObexHelper.OBEX_OPCODE_ABORT) {
    563                         return true;
    564                     }
    565                 }
    566 
    567                 byte[] body = ObexHelper.updateHeaderSet(header, data);
    568                 if ((privateInput != null) && (body != null)) {
    569                     privateInput.writeBytes(body, 1);
    570                 }
    571 
    572                 if (header.mConnectionID != null) {
    573                     mConnectionId = new byte[4];
    574                     System.arraycopy(header.mConnectionID, 0, mConnectionId, 0, 4);
    575                 }
    576 
    577                 if (header.mAuthResp != null) {
    578                     if (!handleAuthResp(header.mAuthResp)) {
    579                         setRequestInactive();
    580                         throw new IOException("Authentication Failed");
    581                     }
    582                 }
    583 
    584                 if ((header.responseCode == ResponseCodes.OBEX_HTTP_UNAUTHORIZED)
    585                         && (header.mAuthChall != null)) {
    586 
    587                     if (handleAuthChall(header)) {
    588                         out.write((byte)HeaderSet.AUTH_RESPONSE);
    589                         out.write((byte)((header.mAuthResp.length + 3) >> 8));
    590                         out.write((byte)(header.mAuthResp.length + 3));
    591                         out.write(header.mAuthResp);
    592                         header.mAuthChall = null;
    593                         header.mAuthResp = null;
    594 
    595                         byte[] sendHeaders = new byte[out.size() - 3];
    596                         System.arraycopy(out.toByteArray(), 3, sendHeaders, 0, sendHeaders.length);
    597 
    598                         return sendRequest(opCode, sendHeaders, header, privateInput, false);
    599                     }
    600                 }
    601             }
    602         }
    603 
    604         return true;
    605     }
    606 
    607     public void close() throws IOException {
    608         mOpen = false;
    609         mInput.close();
    610         mOutput.close();
    611     }
    612 
    613     public boolean isSrmSupported() {
    614         return mLocalSrmSupported;
    615     }
    616 }
    617