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