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