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