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