Home | History | Annotate | Download | only in net
      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