ClientSession.java revision 41557e18fa3fc5791a5a1c7f0051ae3385608bc4
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 implements the <code>Operation</code> interface.  It will read
42 * and write data via puts and gets.
43 *
44 * @hide
45 */
46public final class ClientSession implements ObexSession {
47    private Authenticator mAuthenticator;
48
49    private boolean mOpen;
50
51    // Determines if an OBEX layer connection has been established
52    private boolean mObexConnected;
53
54    private byte[] mConnectionId = null;
55
56    private byte[] mChallengeDigest = null;
57
58    /*
59     * The max Packet size must be at least 256 according to the OBEX
60     * specification.
61     */
62    private int maxPacketSize = 256;
63
64    private boolean mRequestActive;
65
66    private final InputStream mInput;
67
68    private final OutputStream mOutput;
69
70    public ClientSession(ObexTransport trans) throws IOException {
71        mInput = trans.openInputStream();
72        mOutput = trans.openOutputStream();
73        mOpen = true;
74        mRequestActive = false;
75    }
76
77    public HeaderSet connect(HeaderSet header) throws IOException {
78        ensureOpen();
79        if (mObexConnected) {
80            throw new IOException("Already connected to server");
81        }
82        setRequestActive();
83
84        int totalLength = 4;
85        byte[] head = null;
86
87        // Determine the header byte array
88        if (header != null) {
89            if (header.nonce != null) {
90                mChallengeDigest = new byte[16];
91                System.arraycopy(header.nonce, 0, mChallengeDigest, 0, 16);
92            }
93            head = ObexHelper.createHeader(header, false);
94            totalLength += head.length;
95        }
96        /*
97        * Write the OBEX CONNECT packet to the server.
98        * Byte 0: 0x80
99        * Byte 1&2: Connect Packet Length
100        * Byte 3: OBEX Version Number (Presently, 0x10)
101        * Byte 4: Flags (For TCP 0x00)
102        * Byte 5&6: Max OBEX Packet Length (Defined in MAX_PACKET_SIZE)
103        * Byte 7 to n: headers
104        */
105        byte[] requestPacket = new byte[totalLength];
106        // We just need to start at  byte 3 since the sendRequest() method will
107        // handle the length and 0x80.
108        requestPacket[0] = (byte)0x10;
109        requestPacket[1] = (byte)0x00;
110        requestPacket[2] = (byte)(ObexHelper.MAX_PACKET_SIZE_INT >> 8);
111        requestPacket[3] = (byte)(ObexHelper.MAX_PACKET_SIZE_INT & 0xFF);
112        if (head != null) {
113            System.arraycopy(head, 0, requestPacket, 4, head.length);
114        }
115
116        // check with local max packet size
117        if ((requestPacket.length + 3) > ObexHelper.MAX_PACKET_SIZE_INT) {
118            throw new IOException("Packet size exceeds max packet size");
119        }
120
121        HeaderSet returnHeaderSet = new HeaderSet();
122        sendRequest(0x80, requestPacket, returnHeaderSet, null);
123
124        /*
125        * Read the response from the OBEX server.
126        * Byte 0: Response Code (If successful then OBEX_HTTP_OK)
127        * Byte 1&2: Packet Length
128        * Byte 3: OBEX Version Number
129        * Byte 4: Flags3
130        * Byte 5&6: Max OBEX packet Length
131        * Byte 7 to n: Optional HeaderSet
132        */
133        if (returnHeaderSet.responseCode == ResponseCodes.OBEX_HTTP_OK) {
134            mObexConnected = true;
135        }
136        setRequestInactive();
137
138        return returnHeaderSet;
139    }
140
141    public Operation get(HeaderSet header) throws IOException {
142
143        if (!mObexConnected) {
144            throw new IOException("Not connected to the server");
145        }
146        setRequestActive();
147
148        ensureOpen();
149
150        if (header == null) {
151            header = new HeaderSet();
152        } else {
153            if (header.nonce != null) {
154                mChallengeDigest = new byte[16];
155                System.arraycopy(header.nonce, 0, mChallengeDigest, 0, 16);
156            }
157        }
158        // Add the connection ID if one exists
159        if (mConnectionId != null) {
160            header.connectionID = new byte[4];
161            System.arraycopy(mConnectionId, 0, header.connectionID, 0, 4);
162        }
163
164        return new ClientOperation(mInput, maxPacketSize, this, header, true);
165    }
166
167    /**
168     *  0xCB Connection Id an identifier used for OBEX connection multiplexing
169     */
170    public void setConnectionID(long id) {
171        if ((id < 0) || (id > 0xFFFFFFFFL)) {
172            throw new IllegalArgumentException("Connection ID is not in a valid range");
173        }
174        mConnectionId = ObexHelper.convertToByteArray(id);
175    }
176
177    public HeaderSet delete(HeaderSet header) throws IOException {
178
179        Operation op = put(header);
180        op.getResponseCode();
181        HeaderSet returnValue = op.getReceivedHeaders();
182        op.close();
183
184        return returnValue;
185    }
186
187    public HeaderSet disconnect(HeaderSet header) throws IOException {
188        if (!mObexConnected) {
189            throw new IOException("Not connected to the server");
190        }
191        setRequestActive();
192
193        ensureOpen();
194        // Determine the header byte array
195        byte[] head = null;
196        if (header != null) {
197            if (header.nonce != null) {
198                mChallengeDigest = new byte[16];
199                System.arraycopy(header.nonce, 0, mChallengeDigest, 0, 16);
200            }
201            // Add the connection ID if one exists
202            if (mConnectionId != null) {
203                header.connectionID = new byte[4];
204                System.arraycopy(mConnectionId, 0, header.connectionID, 0, 4);
205            }
206            head = ObexHelper.createHeader(header, false);
207
208            if ((head.length + 3) > maxPacketSize) {
209                throw new IOException("Packet size exceeds max packet size");
210            }
211        } else {
212            // Add the connection ID if one exists
213            if (mConnectionId != null) {
214                head = new byte[5];
215                head[0] = (byte)0xCB;
216                System.arraycopy(mConnectionId, 0, head, 1, 4);
217            }
218        }
219
220        HeaderSet returnHeaderSet = new HeaderSet();
221        sendRequest(0x81, head, returnHeaderSet, null);
222
223        /*
224         * An OBEX DISCONNECT reply from the server:
225         * Byte 1: Response code
226         * Bytes 2 & 3: packet size
227         * Bytes 4 & up: headers
228         */
229
230        /* response code , and header are ignored
231         * */
232
233        synchronized (this) {
234            mObexConnected = false;
235            setRequestInactive();
236        }
237
238        return returnHeaderSet;
239    }
240
241    public long getConnectionID() {
242
243        if (mConnectionId == null) {
244            return -1;
245        }
246        return ObexHelper.convertToLong(mConnectionId);
247    }
248
249    public Operation put(HeaderSet header) throws IOException {
250        if (!mObexConnected) {
251            throw new IOException("Not connected to the server");
252        }
253        setRequestActive();
254
255        ensureOpen();
256
257        if (header == null) {
258            header = new HeaderSet();
259        } else {
260            // when auth is initated by client ,save the digest
261            if (header.nonce != null) {
262                mChallengeDigest = new byte[16];
263                System.arraycopy(header.nonce, 0, mChallengeDigest, 0, 16);
264            }
265        }
266
267        // Add the connection ID if one exists
268        if (mConnectionId != null) {
269
270            header.connectionID = new byte[4];
271            System.arraycopy(mConnectionId, 0, header.connectionID, 0, 4);
272        }
273
274        return new ClientOperation(mInput, maxPacketSize, this, header, false);
275    }
276
277    public void setAuthenticator(Authenticator auth) {
278        if (auth == null) {
279            throw new NullPointerException("Authenticator may not be null");
280        }
281        mAuthenticator = auth;
282    }
283
284    public HeaderSet setPath(HeaderSet header, boolean backup, boolean create)
285            throws IOException {
286        if (!mObexConnected) {
287            throw new IOException("Not connected to the server");
288        }
289        setRequestActive();
290        ensureOpen();
291
292        int totalLength = 2;
293        byte[] head = null;
294
295        if (header == null) {
296            header = new HeaderSet();
297        } else {
298            if (header.nonce != null) {
299                mChallengeDigest = new byte[16];
300                System.arraycopy(header.nonce, 0, mChallengeDigest, 0, 16);
301            }
302        }
303
304        // when auth is initiated by client ,save the digest
305        if (header.nonce != null) {
306            mChallengeDigest = new byte[16];
307            System.arraycopy(header.nonce, 0, mChallengeDigest, 0, 16);
308        }
309
310        // Add the connection ID if one exists
311        if (mConnectionId != null) {
312            header.connectionID = new byte[4];
313            System.arraycopy(mConnectionId, 0, header.connectionID, 0, 4);
314        }
315
316        head = ObexHelper.createHeader(header, false);
317        totalLength += head.length;
318
319        if (totalLength > maxPacketSize) {
320            throw new IOException("Packet size exceeds max packet size");
321        }
322
323        int flags = 0;
324        /*
325         * The backup flag bit is bit 0 so if we add 1, this will set that bit
326         */
327        if (backup) {
328            flags++;
329        }
330        /*
331         * The create bit is bit 1 so if we or with 2 the bit will be set.
332         */
333        if (!create) {
334            flags |= 2;
335        }
336
337        /*
338         * An OBEX SETPATH packet to the server:
339         * Byte 1: 0x85
340         * Byte 2 & 3: packet size
341         * Byte 4: flags
342         * Byte 5: constants
343         * Byte 6 & up: headers
344         */
345        byte[] packet = new byte[totalLength];
346        packet[0] = (byte)flags;
347        packet[1] = (byte)0x00;
348        if (header != null) {
349            System.arraycopy(head, 0, packet, 2, head.length);
350        }
351
352        HeaderSet returnHeaderSet = new HeaderSet();
353        sendRequest(0x85, packet, returnHeaderSet, null);
354
355        /*
356         * An OBEX SETPATH reply from the server:
357         * Byte 1: Response code
358         * Bytes 2 & 3: packet size
359         * Bytes 4 & up: headers
360         */
361
362        setRequestInactive();
363
364        return returnHeaderSet;
365    }
366
367    /**
368     * Verifies that the connection is open.
369     *
370     * @throws IOException if the connection is closed
371     */
372    public synchronized void ensureOpen() throws IOException {
373        if (!mOpen) {
374            throw new IOException("Connection closed");
375        }
376    }
377
378    /**
379     * Set request inactive.
380     * Allows Put and get operation objects to tell this object when they are
381     * done.
382     */
383    /*package*/ synchronized void setRequestInactive() {
384        mRequestActive = false;
385    }
386
387    /**
388     * Set request to active.
389     * @throws IOException if already active
390     */
391    private synchronized void setRequestActive() throws IOException {
392        if (mRequestActive) {
393            throw new IOException("OBEX request is already being performed");
394        }
395        mRequestActive = true;
396    }
397
398    /**
399     * Sends a standard request to the client.  It will then wait for the reply
400     * and update the header set object provided.  If any authentication
401     * headers (i.e. authentication challenge or authentication response) are
402     * received, they will be processed.
403     *
404     * @param code the type of request to send to the client
405     *
406     * @param head the headers to send to the server
407     *
408     * @param challenge the nonce that was sent in the authentication
409     * challenge header located in <code>head</code>; <code>null</code>
410     * if no authentication header is included in <code>head</code>
411     *
412     * @param header the header object to update with the response
413     *
414     * @param input the input stream used by the Operation object; null if this
415     * is called on a CONNECT, SETPATH or DISCONNECT
416     *
417     * return <code>true</code> if the operation completed successfully;
418     * <code>false</code> if an authentication response failed to pass
419     *
420     * @throws IOException if an IO error occurs
421     */
422    public boolean sendRequest(int code, byte[] head, HeaderSet header,
423            PrivateInputStream privateInput) throws IOException {
424        //check header length with local max size
425        if (head != null) {
426            if ((head.length + 3) > ObexHelper.MAX_PACKET_SIZE_INT) {
427                throw new IOException("header too large ");
428            }
429        }
430        //byte[] nonce;
431        int bytesReceived;
432        ByteArrayOutputStream out = new ByteArrayOutputStream();
433        out.write((byte)code);
434
435        // Determine if there are any headers to send
436        if (head == null) {
437            out.write(0x00);
438            out.write(0x03);
439        } else {
440            out.write((byte)((head.length + 3) >> 8));
441            out.write((byte)(head.length + 3));
442            out.write(head);
443        }
444
445        // Write the request to the output stream and flush the stream
446        mOutput.write(out.toByteArray());
447        mOutput.flush();
448
449        header.responseCode = mInput.read();
450
451        int length = ((mInput.read() << 8) | (mInput.read()));
452
453        if (length > ObexHelper.MAX_PACKET_SIZE_INT) {
454            throw new IOException("Packet received exceeds packet size limit");
455        }
456        if (length > 3) {
457            byte[] data = null;
458            if (code == 0x80) {
459                int version = mInput.read();
460                int flags = mInput.read();
461                maxPacketSize = (mInput.read() << 8) + mInput.read();
462
463                //check with local max size
464                if (maxPacketSize > ObexHelper.MAX_PACKET_SIZE_INT) {
465                    maxPacketSize = ObexHelper.MAX_PACKET_SIZE_INT;
466                }
467
468                if (length > 7) {
469                    data = new byte[length - 7];
470
471                    bytesReceived = mInput.read(data);
472                    while (bytesReceived != (length - 7)) {
473                        bytesReceived += mInput.read(data, bytesReceived, data.length
474                                - bytesReceived);
475                    }
476                } else {
477                    return true;
478                }
479            } else {
480                data = new byte[length - 3];
481                bytesReceived = mInput.read(data);
482
483                while (bytesReceived != (length - 3)) {
484                    bytesReceived += mInput.read(data, bytesReceived, data.length - bytesReceived);
485                }
486                if (code == 0xFF) {
487                    return true;
488                }
489            }
490
491            byte[] body = ObexHelper.updateHeaderSet(header, data);
492            if ((privateInput != null) && (body != null)) {
493                privateInput.writeBytes(body, 1);
494            }
495
496            if (header.connectionID != null) {
497                mConnectionId = new byte[4];
498                System.arraycopy(header.connectionID, 0, mConnectionId, 0, 4);
499            }
500
501            if (header.authResp != null) {
502                if (!handleAuthResp(header.authResp)) {
503                    setRequestInactive();
504                    throw new IOException("Authentication Failed");
505                }
506            }
507
508            if ((header.responseCode == ResponseCodes.OBEX_HTTP_UNAUTHORIZED)
509                    && (header.authChall != null)) {
510
511                if (handleAuthChall(header)) {
512                    out.write((byte)0x4E);
513                    out.write((byte)((header.authResp.length + 3) >> 8));
514                    out.write((byte)(header.authResp.length + 3));
515                    out.write(header.authResp);
516                    header.authChall = null;
517                    header.authResp = null;
518
519                    byte[] sendHeaders = new byte[out.size() - 3];
520                    System.arraycopy(out.toByteArray(), 3, sendHeaders, 0, sendHeaders.length);
521
522                    return sendRequest(code, sendHeaders, header, privateInput);
523                }
524            }
525        }
526
527        return true;
528    }
529
530    /**
531     * Called when the client received an authentication challenge header.  This
532     * will cause the authenticator to handle the authentication challenge.
533     *
534     * @param header the header with the authentication challenge
535     *
536     * @return <code>true</code> if the last request should be resent;
537     * <code>false</code> if the last request should not be resent
538     */
539    protected boolean handleAuthChall(HeaderSet header) {
540
541        if (mAuthenticator == null) {
542            return false;
543        }
544
545        /*
546         * An authentication challenge is made up of one required and two
547         * optional tag length value triplets.  The tag 0x00 is required to be
548         * in the authentication challenge and it represents the challenge
549         * digest that was received.  The tag 0x01 is the options tag.  This
550         * tag tracks if user ID is required and if full access will be
551         * granted.  The tag 0x02 is the realm, which provides a description of
552         * which user name and password to use.
553         */
554        byte[] challenge = ObexHelper.getTagValue((byte)0x00, header.authChall);
555        byte[] option = ObexHelper.getTagValue((byte)0x01, header.authChall);
556        byte[] description = ObexHelper.getTagValue((byte)0x02, header.authChall);
557
558        String realm = "";
559        if (description != null) {
560            byte[] realmString = new byte[description.length - 1];
561            System.arraycopy(description, 1, realmString, 0, realmString.length);
562
563            switch (description[0] & 0xFF) {
564
565                case 0x00:
566                    // ASCII encoding
567                    // Fall through
568                case 0x01:
569                    // ISO-8859-1 encoding
570                    try {
571                        realm = new String(realmString, "ISO8859_1");
572                    } catch (Exception e) {
573                        throw new RuntimeException("Unsupported Encoding Scheme");
574                    }
575                    break;
576
577                case 0xFF:
578                    // UNICODE Encoding
579                    realm = ObexHelper.convertToUnicode(realmString, false);
580                    break;
581
582                case 0x02:
583                    // ISO-8859-2 encoding
584                    // Fall through
585                case 0x03:
586                    // ISO-8859-3 encoding
587                    // Fall through
588                case 0x04:
589                    // ISO-8859-4 encoding
590                    // Fall through
591                case 0x05:
592                    // ISO-8859-5 encoding
593                    // Fall through
594                case 0x06:
595                    // ISO-8859-6 encoding
596                    // Fall through
597                case 0x07:
598                    // ISO-8859-7 encoding
599                    // Fall through
600                case 0x08:
601                    // ISO-8859-8 encoding
602                    // Fall through
603                case 0x09:
604                    // ISO-8859-9 encoding
605                    // Fall through
606                default:
607                    throw new RuntimeException("Unsupported Encoding Scheme");
608            }
609        }
610
611        boolean isUserIDRequired = false;
612        boolean isFullAccess = true;
613        if (option != null) {
614            if ((option[0] & 0x01) != 0) {
615                isUserIDRequired = true;
616            }
617
618            if ((option[0] & 0x02) != 0) {
619                isFullAccess = false;
620            }
621        }
622
623        PasswordAuthentication result = null;
624        header.authChall = null;
625
626        try {
627            result = mAuthenticator.onAuthenticationChallenge(realm, isUserIDRequired, isFullAccess);
628        } catch (Exception e) {
629            return false;
630        }
631
632        /*
633         * If no password was provided then do not resend the request
634         */
635        if (result == null) {
636            return false;
637        }
638
639        byte[] password = result.getPassword();
640        if (password == null) {
641            return false;
642        }
643
644        byte[] userName = result.getUserName();
645
646        /*
647         * Create the authentication response header.  It includes 1 required
648         * and 2 option tag length value triples.  The required triple has a
649         * tag of 0x00 and is the response digest.  The first optional tag is
650         * 0x01 and represents the user ID.  If no user ID is provided, then
651         * no user ID will be sent.  The second optional tag is 0x02 and is the
652         * challenge that was received.  This will always be sent
653         */
654        if (userName != null) {
655            header.authResp = new byte[38 + userName.length];
656            header.authResp[36] = (byte)0x01;
657            header.authResp[37] = (byte)userName.length;
658            System.arraycopy(userName, 0, header.authResp, 38, userName.length);
659        } else {
660            header.authResp = new byte[36];
661        }
662
663        // Create the secret String
664        byte[] digest = new byte[challenge.length + password.length];
665        System.arraycopy(challenge, 0, digest, 0, challenge.length);
666        System.arraycopy(password, 0, digest, challenge.length, password.length);
667
668        // Add the Response Digest
669        header.authResp[0] = (byte)0x00;
670        header.authResp[1] = (byte)0x10;
671
672        byte[] responseDigest = ObexHelper.computeMd5Hash(digest);
673        System.arraycopy(responseDigest, 0, header.authResp, 2, 16);
674
675        // Add the challenge
676        header.authResp[18] = (byte)0x02;
677        header.authResp[19] = (byte)0x10;
678        System.arraycopy(challenge, 0, header.authResp, 20, 16);
679
680        return true;
681    }
682
683    /**
684     * Called when the client received an authentication response header.  This
685     * will cause the authenticator to handle the authentication response.
686     *
687     * @param authResp the authentication response
688     *
689     * @return <code>true</code> if the response passed; <code>false</code> if
690     * the response failed
691     */
692    protected boolean handleAuthResp(byte[] authResp) {
693        if (mAuthenticator == null) {
694            return false;
695        }
696
697        byte[] correctPassword = mAuthenticator.onAuthenticationResponse(ObexHelper.getTagValue(
698                (byte)0x01, authResp));
699        if (correctPassword == null) {
700            return false;
701        }
702
703        byte[] temp = new byte[correctPassword.length + 16];
704        System.arraycopy(mChallengeDigest, 0, temp, 0, 16);
705        System.arraycopy(correctPassword, 0, temp, 16, correctPassword.length);
706
707        byte[] correctResponse = ObexHelper.computeMd5Hash(temp);
708        byte[] actualResponse = ObexHelper.getTagValue((byte)0x00, authResp);
709        for (int i = 0; i < 16; i++) {
710            if (correctResponse[i] != actualResponse[i]) {
711                return false;
712            }
713        }
714
715        return true;
716    }
717
718    public void close() throws IOException {
719        mOpen = false;
720        mInput.close();
721        mOutput.close();
722    }
723}
724