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 
ClientSession(final ObexTransport trans)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      */
ClientSession(final ObexTransport trans, final boolean supportsSrm)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 
connect(final HeaderSet header)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 
get(HeaderSet header)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      */
setConnectionID(long id)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 
delete(HeaderSet header)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 
disconnect(HeaderSet header)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 
getConnectionID()279     public long getConnectionID() {
280 
281         if (mConnectionId == null) {
282             return -1;
283         }
284         return ObexHelper.convertToLong(mConnectionId);
285     }
286 
put(HeaderSet header)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 
setAuthenticator(Authenticator auth)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 
setPath(HeaderSet header, boolean backup, boolean create)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      */
ensureOpen()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      */
setRequestInactive()429     /*package*/synchronized void setRequestInactive() {
430         mRequestActive = false;
431     }
432 
433     /**
434      * Set request to active.
435      * @throws IOException if already active
436      */
setRequestActive()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      */
sendRequest(int opCode, byte[] head, HeaderSet header, PrivateInputStream privateInput, boolean srmActive)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 
close()607     public void close() throws IOException {
608         mOpen = false;
609         mInput.close();
610         mOutput.close();
611     }
612 
isSrmSupported()613     public boolean isSrmSupported() {
614         return mLocalSrmSupported;
615     }
616 }
617