1 // Copyright 2014 The Chromium Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style license that can be 3 // found in the LICENSE file. 4 5 package org.chromium.net; 6 7 import android.util.Log; 8 9 import org.apache.http.conn.ConnectTimeoutException; 10 import org.chromium.base.CalledByNative; 11 import org.chromium.base.JNINamespace; 12 13 import java.io.IOException; 14 import java.net.MalformedURLException; 15 import java.net.URL; 16 import java.net.UnknownHostException; 17 import java.nio.ByteBuffer; 18 import java.nio.channels.ReadableByteChannel; 19 import java.nio.channels.WritableByteChannel; 20 import java.util.ArrayList; 21 import java.util.HashMap; 22 import java.util.List; 23 import java.util.Map; 24 import java.util.Map.Entry; 25 26 /** 27 * Network request using the native http stack implementation. 28 */ 29 @JNINamespace("cronet") 30 public class ChromiumUrlRequest implements HttpUrlRequest { 31 /** 32 * Native adapter object, owned by UrlRequest. 33 */ 34 private long mUrlRequestAdapter; 35 private final ChromiumUrlRequestContext mRequestContext; 36 private final String mUrl; 37 private final int mPriority; 38 private final Map<String, String> mHeaders; 39 private final WritableByteChannel mSink; 40 private Map<String, String> mAdditionalHeaders; 41 private String mUploadContentType; 42 private String mMethod; 43 private byte[] mUploadData; 44 private ReadableByteChannel mUploadChannel; 45 private boolean mChunkedUpload; 46 private IOException mSinkException; 47 private volatile boolean mStarted; 48 private volatile boolean mCanceled; 49 private volatile boolean mRecycled; 50 private volatile boolean mFinished; 51 private boolean mHeadersAvailable; 52 private String mContentType; 53 private long mUploadContentLength; 54 private final HttpUrlRequestListener mListener; 55 private boolean mBufferFullResponse; 56 private long mOffset; 57 private long mContentLength; 58 private long mContentLengthLimit; 59 private boolean mCancelIfContentLengthOverLimit; 60 private boolean mContentLengthOverLimit; 61 private boolean mSkippingToOffset; 62 private long mSize; 63 private final Object mLock = new Object(); 64 65 public ChromiumUrlRequest(ChromiumUrlRequestContext requestContext, 66 String url, int priority, Map<String, String> headers, 67 HttpUrlRequestListener listener) { 68 this(requestContext, url, priority, headers, 69 new ChunkedWritableByteChannel(), listener); 70 mBufferFullResponse = true; 71 } 72 73 /** 74 * Constructor. 75 * 76 * @param requestContext The context. 77 * @param url The URL. 78 * @param priority Request priority, e.g. {@link #REQUEST_PRIORITY_MEDIUM}. 79 * @param headers HTTP headers. 80 * @param sink The output channel into which downloaded content will be 81 * written. 82 */ 83 public ChromiumUrlRequest(ChromiumUrlRequestContext requestContext, 84 String url, int priority, Map<String, String> headers, 85 WritableByteChannel sink, HttpUrlRequestListener listener) { 86 if (requestContext == null) { 87 throw new NullPointerException("Context is required"); 88 } 89 if (url == null) { 90 throw new NullPointerException("URL is required"); 91 } 92 mRequestContext = requestContext; 93 mUrl = url; 94 mPriority = convertRequestPriority(priority); 95 mHeaders = headers; 96 mSink = sink; 97 mUrlRequestAdapter = nativeCreateRequestAdapter( 98 mRequestContext.getChromiumUrlRequestContextAdapter(), 99 mUrl, 100 mPriority); 101 mListener = listener; 102 } 103 104 @Override 105 public void setOffset(long offset) { 106 mOffset = offset; 107 if (offset != 0) { 108 addHeader("Range", "bytes=" + offset + "-"); 109 } 110 } 111 112 /** 113 * The compressed content length as reported by the server. May be -1 if 114 * the server did not provide a length. Some servers may also report the 115 * wrong number. Since this is the compressed content length, and only 116 * uncompressed content is returned by the consumer, the consumer should 117 * not rely on this value. 118 */ 119 @Override 120 public long getContentLength() { 121 return mContentLength; 122 } 123 124 @Override 125 public void setContentLengthLimit(long limit, boolean cancelEarly) { 126 mContentLengthLimit = limit; 127 mCancelIfContentLengthOverLimit = cancelEarly; 128 } 129 130 @Override 131 public int getHttpStatusCode() { 132 int httpStatusCode = nativeGetHttpStatusCode(mUrlRequestAdapter); 133 134 // TODO(mef): Investigate the following: 135 // If we have been able to successfully resume a previously interrupted 136 // download, the status code will be 206, not 200. Since the rest of the 137 // application is expecting 200 to indicate success, we need to fake it. 138 if (httpStatusCode == 206) { 139 httpStatusCode = 200; 140 } 141 return httpStatusCode; 142 } 143 144 /** 145 * Returns an exception if any, or null if the request was completed 146 * successfully. 147 */ 148 @Override 149 public IOException getException() { 150 if (mSinkException != null) { 151 return mSinkException; 152 } 153 154 validateNotRecycled(); 155 156 int errorCode = nativeGetErrorCode(mUrlRequestAdapter); 157 switch (errorCode) { 158 case ChromiumUrlRequestError.SUCCESS: 159 if (mContentLengthOverLimit) { 160 return new ResponseTooLargeException(); 161 } 162 return null; 163 case ChromiumUrlRequestError.UNKNOWN: 164 return new IOException( 165 nativeGetErrorString(mUrlRequestAdapter)); 166 case ChromiumUrlRequestError.MALFORMED_URL: 167 return new MalformedURLException("Malformed URL: " + mUrl); 168 case ChromiumUrlRequestError.CONNECTION_TIMED_OUT: 169 return new ConnectTimeoutException("Connection timed out"); 170 case ChromiumUrlRequestError.UNKNOWN_HOST: 171 String host; 172 try { 173 host = new URL(mUrl).getHost(); 174 } catch (MalformedURLException e) { 175 host = mUrl; 176 } 177 return new UnknownHostException("Unknown host: " + host); 178 default: 179 throw new IllegalStateException( 180 "Unrecognized error code: " + errorCode); 181 } 182 } 183 184 @Override 185 public ByteBuffer getByteBuffer() { 186 return ((ChunkedWritableByteChannel) getSink()).getByteBuffer(); 187 } 188 189 @Override 190 public byte[] getResponseAsBytes() { 191 return ((ChunkedWritableByteChannel) getSink()).getBytes(); 192 } 193 194 /** 195 * Adds a request header. Must be done before request has started. 196 */ 197 public void addHeader(String header, String value) { 198 synchronized (mLock) { 199 validateNotStarted(); 200 if (mAdditionalHeaders == null) { 201 mAdditionalHeaders = new HashMap<String, String>(); 202 } 203 mAdditionalHeaders.put(header, value); 204 } 205 } 206 207 /** 208 * Sets data to upload as part of a POST or PUT request. 209 * 210 * @param contentType MIME type of the upload content or null if this is not 211 * an upload. 212 * @param data The content that needs to be uploaded. 213 */ 214 @Override 215 public void setUploadData(String contentType, byte[] data) { 216 synchronized (mLock) { 217 validateNotStarted(); 218 validateContentType(contentType); 219 mUploadContentType = contentType; 220 mUploadData = data; 221 mUploadChannel = null; 222 mChunkedUpload = false; 223 } 224 } 225 226 /** 227 * Sets a readable byte channel to upload as part of a POST or PUT request. 228 * 229 * @param contentType MIME type of the upload content or null if this is not 230 * an upload request. 231 * @param channel The channel to read to read upload data from if this is an 232 * upload request. 233 * @param contentLength The length of data to upload. 234 */ 235 @Override 236 public void setUploadChannel(String contentType, 237 ReadableByteChannel channel, long contentLength) { 238 synchronized (mLock) { 239 validateNotStarted(); 240 validateContentType(contentType); 241 mUploadContentType = contentType; 242 mUploadChannel = channel; 243 mUploadContentLength = contentLength; 244 mUploadData = null; 245 mChunkedUpload = false; 246 } 247 } 248 249 /** 250 * Sets this request up for chunked uploading. To upload data call 251 * {@link #appendChunk(ByteBuffer, boolean)} after {@link #start()}. 252 * 253 * @param contentType MIME type of the post content or null if this is not a 254 * POST request. 255 */ 256 public void setChunkedUpload(String contentType) { 257 synchronized (mLock) { 258 validateNotStarted(); 259 validateContentType(contentType); 260 mUploadContentType = contentType; 261 mChunkedUpload = true; 262 mUploadData = null; 263 mUploadChannel = null; 264 } 265 } 266 267 /** 268 * Uploads a new chunk. Must have called {@link #setChunkedUpload(String)} 269 * and {@link #start()}. 270 * 271 * @param chunk The data, which will not be modified. It must not be empty 272 * and its current position must be zero. 273 * @param isLastChunk Whether this chunk is the last one. 274 */ 275 public void appendChunk(ByteBuffer chunk, boolean isLastChunk) 276 throws IOException { 277 if (!chunk.hasRemaining()) { 278 throw new IllegalArgumentException( 279 "Attempted to write empty buffer."); 280 } 281 if (chunk.position() != 0) { 282 throw new IllegalArgumentException("The position must be zero."); 283 } 284 synchronized (mLock) { 285 if (!mStarted) { 286 throw new IllegalStateException("Request not yet started."); 287 } 288 if (!mChunkedUpload) { 289 throw new IllegalStateException( 290 "Request not set for chunked uploadind."); 291 } 292 if (mUrlRequestAdapter == 0) { 293 throw new IOException("Native peer destroyed."); 294 } 295 nativeAppendChunk(mUrlRequestAdapter, chunk, chunk.limit(), 296 isLastChunk); 297 } 298 } 299 300 @Override 301 public void setHttpMethod(String method) { 302 validateNotStarted(); 303 mMethod = method; 304 } 305 306 public WritableByteChannel getSink() { 307 return mSink; 308 } 309 310 @Override 311 public void start() { 312 synchronized (mLock) { 313 if (mCanceled) { 314 return; 315 } 316 317 validateNotStarted(); 318 validateNotRecycled(); 319 320 mStarted = true; 321 322 String method = mMethod; 323 if (method == null && 324 ((mUploadData != null && mUploadData.length > 0) || 325 mUploadChannel != null || mChunkedUpload)) { 326 // Default to POST if there is data to upload but no method was 327 // specified. 328 method = "POST"; 329 } 330 331 if (method != null) { 332 nativeSetMethod(mUrlRequestAdapter, method); 333 } 334 335 if (mHeaders != null && !mHeaders.isEmpty()) { 336 for (Entry<String, String> entry : mHeaders.entrySet()) { 337 nativeAddHeader(mUrlRequestAdapter, entry.getKey(), 338 entry.getValue()); 339 } 340 } 341 342 if (mAdditionalHeaders != null) { 343 for (Entry<String, String> entry : 344 mAdditionalHeaders.entrySet()) { 345 nativeAddHeader(mUrlRequestAdapter, entry.getKey(), 346 entry.getValue()); 347 } 348 } 349 350 if (mUploadData != null && mUploadData.length > 0) { 351 nativeSetUploadData(mUrlRequestAdapter, mUploadContentType, 352 mUploadData); 353 } else if (mUploadChannel != null) { 354 nativeSetUploadChannel(mUrlRequestAdapter, mUploadContentType, 355 mUploadContentLength); 356 } else if (mChunkedUpload) { 357 nativeEnableChunkedUpload(mUrlRequestAdapter, 358 mUploadContentType); 359 } 360 361 nativeStart(mUrlRequestAdapter); 362 } 363 } 364 365 @Override 366 public void cancel() { 367 synchronized (mLock) { 368 if (mCanceled) { 369 return; 370 } 371 372 mCanceled = true; 373 374 if (!mRecycled) { 375 nativeCancel(mUrlRequestAdapter); 376 } 377 } 378 } 379 380 @Override 381 public boolean isCanceled() { 382 synchronized (mLock) { 383 return mCanceled; 384 } 385 } 386 387 public boolean isRecycled() { 388 synchronized (mLock) { 389 return mRecycled; 390 } 391 } 392 393 @Override 394 public String getNegotiatedProtocol() { 395 validateNotRecycled(); 396 validateHeadersAvailable(); 397 return nativeGetNegotiatedProtocol(mUrlRequestAdapter); 398 } 399 400 @Override 401 public String getContentType() { 402 return mContentType; 403 } 404 405 @Override 406 public String getHeader(String name) { 407 validateNotRecycled(); 408 validateHeadersAvailable(); 409 return nativeGetHeader(mUrlRequestAdapter, name); 410 } 411 412 // All response headers. 413 @Override 414 public Map<String, List<String>> getAllHeaders() { 415 validateNotRecycled(); 416 validateHeadersAvailable(); 417 ResponseHeadersMap result = new ResponseHeadersMap(); 418 nativeGetAllHeaders(mUrlRequestAdapter, result); 419 return result; 420 } 421 422 @Override 423 public String getUrl() { 424 return mUrl; 425 } 426 427 private static int convertRequestPriority(int priority) { 428 switch (priority) { 429 case HttpUrlRequest.REQUEST_PRIORITY_IDLE: 430 return ChromiumUrlRequestPriority.IDLE; 431 case HttpUrlRequest.REQUEST_PRIORITY_LOWEST: 432 return ChromiumUrlRequestPriority.LOWEST; 433 case HttpUrlRequest.REQUEST_PRIORITY_LOW: 434 return ChromiumUrlRequestPriority.LOW; 435 case HttpUrlRequest.REQUEST_PRIORITY_MEDIUM: 436 return ChromiumUrlRequestPriority.MEDIUM; 437 case HttpUrlRequest.REQUEST_PRIORITY_HIGHEST: 438 return ChromiumUrlRequestPriority.HIGHEST; 439 default: 440 return ChromiumUrlRequestPriority.MEDIUM; 441 } 442 } 443 444 private void onContentLengthOverLimit() { 445 mContentLengthOverLimit = true; 446 cancel(); 447 } 448 449 /** 450 * A callback invoked when the response has been fully consumed. 451 */ 452 private void onRequestComplete() { 453 mListener.onRequestComplete(this); 454 } 455 456 private void validateNotRecycled() { 457 if (mRecycled) { 458 throw new IllegalStateException("Accessing recycled request"); 459 } 460 } 461 462 private void validateNotStarted() { 463 if (mStarted) { 464 throw new IllegalStateException("Request already started"); 465 } 466 } 467 468 private void validateHeadersAvailable() { 469 if (!mHeadersAvailable) { 470 throw new IllegalStateException("Response headers not available"); 471 } 472 } 473 474 private void validateContentType(String contentType) { 475 if (contentType == null) { 476 throw new NullPointerException("contentType is required"); 477 } 478 } 479 480 // Private methods called by native library. 481 482 /** 483 * If @CalledByNative method throws an exception, request gets cancelled 484 * and exception could be retrieved from request using getException(). 485 */ 486 private void onCalledByNativeException(Exception e) { 487 mSinkException = new IOException( 488 "CalledByNative method has thrown an exception", e); 489 Log.e(ChromiumUrlRequestContext.LOG_TAG, 490 "Exception in CalledByNative method", e); 491 try { 492 cancel(); 493 } catch (Exception cancel_exception) { 494 Log.e(ChromiumUrlRequestContext.LOG_TAG, 495 "Exception trying to cancel request", cancel_exception); 496 } 497 } 498 499 /** 500 * A callback invoked when the first chunk of the response has arrived. 501 */ 502 @CalledByNative 503 private void onResponseStarted() { 504 try { 505 mContentType = nativeGetContentType(mUrlRequestAdapter); 506 mContentLength = nativeGetContentLength(mUrlRequestAdapter); 507 mHeadersAvailable = true; 508 509 if (mContentLengthLimit > 0 && 510 mContentLength > mContentLengthLimit && 511 mCancelIfContentLengthOverLimit) { 512 onContentLengthOverLimit(); 513 return; 514 } 515 516 if (mBufferFullResponse && mContentLength != -1 && 517 !mContentLengthOverLimit) { 518 ((ChunkedWritableByteChannel) getSink()).setCapacity( 519 (int) mContentLength); 520 } 521 522 if (mOffset != 0) { 523 // The server may ignore the request for a byte range, in which 524 // case status code will be 200, instead of 206. Note that we 525 // cannot call getHttpStatusCode as it rewrites 206 into 200. 526 if (nativeGetHttpStatusCode(mUrlRequestAdapter) == 200) { 527 // TODO(mef): Revisit this logic. 528 if (mContentLength != -1) { 529 mContentLength -= mOffset; 530 } 531 mSkippingToOffset = true; 532 } else { 533 mSize = mOffset; 534 } 535 } 536 mListener.onResponseStarted(this); 537 } catch (Exception e) { 538 onCalledByNativeException(e); 539 } 540 } 541 542 /** 543 * Consumes a portion of the response. 544 * 545 * @param byteBuffer The ByteBuffer to append. Must be a direct buffer, and 546 * no references to it may be retained after the method ends, as 547 * it wraps code managed on the native heap. 548 */ 549 @CalledByNative 550 private void onBytesRead(ByteBuffer buffer) { 551 try { 552 if (mContentLengthOverLimit) { 553 return; 554 } 555 556 int size = buffer.remaining(); 557 mSize += size; 558 if (mSkippingToOffset) { 559 if (mSize <= mOffset) { 560 return; 561 } else { 562 mSkippingToOffset = false; 563 buffer.position((int) (mOffset - (mSize - size))); 564 } 565 } 566 567 boolean contentLengthOverLimit = 568 (mContentLengthLimit != 0 && mSize > mContentLengthLimit); 569 if (contentLengthOverLimit) { 570 buffer.limit(size - (int) (mSize - mContentLengthLimit)); 571 } 572 573 while (buffer.hasRemaining()) { 574 mSink.write(buffer); 575 } 576 if (contentLengthOverLimit) { 577 onContentLengthOverLimit(); 578 } 579 } catch (Exception e) { 580 onCalledByNativeException(e); 581 } 582 } 583 584 /** 585 * Notifies the listener, releases native data structures. 586 */ 587 @SuppressWarnings("unused") 588 @CalledByNative 589 private void finish() { 590 try { 591 synchronized (mLock) { 592 mFinished = true; 593 594 if (mRecycled) { 595 return; 596 } 597 try { 598 mSink.close(); 599 } catch (IOException e) { 600 // Ignore 601 } 602 try { 603 if (mUploadChannel != null && mUploadChannel.isOpen()) { 604 mUploadChannel.close(); 605 } 606 } catch (IOException e) { 607 // Ignore 608 } 609 onRequestComplete(); 610 nativeDestroyRequestAdapter(mUrlRequestAdapter); 611 mUrlRequestAdapter = 0; 612 mRecycled = true; 613 } 614 } catch (Exception e) { 615 mSinkException = new IOException("Exception in finish", e); 616 } 617 } 618 619 /** 620 * Appends header |name| with value |value| to |headersMap|. 621 */ 622 @SuppressWarnings("unused") 623 @CalledByNative 624 private void onAppendResponseHeader(ResponseHeadersMap headersMap, 625 String name, String value) { 626 try { 627 if (!headersMap.containsKey(name)) { 628 headersMap.put(name, new ArrayList<String>()); 629 } 630 headersMap.get(name).add(value); 631 } catch (Exception e) { 632 onCalledByNativeException(e); 633 } 634 } 635 636 /** 637 * Reads a sequence of bytes from upload channel into the given buffer. 638 * @param dest The buffer into which bytes are to be transferred. 639 * @return Returns number of bytes read (could be 0) or -1 and closes 640 * the channel if error occured. 641 */ 642 @SuppressWarnings("unused") 643 @CalledByNative 644 private int readFromUploadChannel(ByteBuffer dest) { 645 try { 646 if (mUploadChannel == null || !mUploadChannel.isOpen()) 647 return -1; 648 int result = mUploadChannel.read(dest); 649 if (result < 0) { 650 mUploadChannel.close(); 651 return 0; 652 } 653 return result; 654 } catch (Exception e) { 655 onCalledByNativeException(e); 656 } 657 return -1; 658 } 659 660 // Native methods are implemented in chromium_url_request.cc. 661 662 private native long nativeCreateRequestAdapter( 663 long urlRequestContextAdapter, String url, int priority); 664 665 private native void nativeAddHeader(long urlRequestAdapter, String name, 666 String value); 667 668 private native void nativeSetMethod(long urlRequestAdapter, String method); 669 670 private native void nativeSetUploadData(long urlRequestAdapter, 671 String contentType, byte[] content); 672 673 private native void nativeSetUploadChannel(long urlRequestAdapter, 674 String contentType, long contentLength); 675 676 private native void nativeEnableChunkedUpload(long urlRequestAdapter, 677 String contentType); 678 679 private native void nativeAppendChunk(long urlRequestAdapter, 680 ByteBuffer chunk, int chunkSize, boolean isLastChunk); 681 682 private native void nativeStart(long urlRequestAdapter); 683 684 private native void nativeCancel(long urlRequestAdapter); 685 686 private native void nativeDestroyRequestAdapter(long urlRequestAdapter); 687 688 private native int nativeGetErrorCode(long urlRequestAdapter); 689 690 private native int nativeGetHttpStatusCode(long urlRequestAdapter); 691 692 private native String nativeGetErrorString(long urlRequestAdapter); 693 694 private native String nativeGetContentType(long urlRequestAdapter); 695 696 private native long nativeGetContentLength(long urlRequestAdapter); 697 698 private native String nativeGetHeader(long urlRequestAdapter, String name); 699 700 private native void nativeGetAllHeaders(long urlRequestAdapter, 701 ResponseHeadersMap headers); 702 703 private native String nativeGetNegotiatedProtocol(long urlRequestAdapter); 704 705 // Explicit class to work around JNI-generator generics confusion. 706 private class ResponseHeadersMap extends HashMap<String, List<String>> { 707 } 708 } 709