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