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
35package javax.obex;
36
37import java.io.ByteArrayOutputStream;
38import java.io.IOException;
39import java.io.InputStream;
40import java.io.OutputStream;
41
42import android.util.Log;
43
44/**
45 * This class in an implementation of the OBEX ClientSession.
46 * @hide
47 */
48public 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