• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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.content.Context;
8 import android.text.TextUtils;
9 
10 import org.apache.http.HttpStatus;
11 
12 import java.io.FileNotFoundException;
13 import java.io.IOException;
14 import java.io.InputStream;
15 import java.io.OutputStream;
16 import java.net.HttpURLConnection;
17 import java.net.ProtocolException;
18 import java.net.URL;
19 import java.nio.ByteBuffer;
20 import java.nio.channels.ReadableByteChannel;
21 import java.nio.channels.WritableByteChannel;
22 import java.util.List;
23 import java.util.Map;
24 import java.util.Map.Entry;
25 import java.util.concurrent.ExecutorService;
26 import java.util.concurrent.Executors;
27 import java.util.concurrent.ThreadFactory;
28 import java.util.concurrent.atomic.AtomicInteger;
29 import java.util.zip.GZIPInputStream;
30 
31 /**
32  * Network request using the HttpUrlConnection implementation.
33  */
34 class HttpUrlConnectionUrlRequest implements HttpUrlRequest {
35 
36     private static final int MAX_CHUNK_SIZE = 8192;
37 
38     private static final int CONNECT_TIMEOUT = 3000;
39 
40     private static final int READ_TIMEOUT = 90000;
41 
42     private final Context mContext;
43 
44     private final String mUrl;
45 
46     private final Map<String, String> mHeaders;
47 
48     private final WritableByteChannel mSink;
49 
50     private final HttpUrlRequestListener mListener;
51 
52     private IOException mException;
53 
54     private HttpURLConnection mConnection;
55 
56     private long mOffset;
57 
58     private int mContentLength;
59 
60     private int mUploadContentLength;
61 
62     private long mContentLengthLimit;
63 
64     private boolean mCancelIfContentLengthOverLimit;
65 
66     private boolean mContentLengthOverLimit;
67 
68     private boolean mSkippingToOffset;
69 
70     private long mSize;
71 
72     private String mPostContentType;
73 
74     private byte[] mPostData;
75 
76     private ReadableByteChannel mPostDataChannel;
77 
78     private String mContentType;
79 
80     private int mHttpStatusCode;
81 
82     private boolean mStarted;
83 
84     private boolean mCanceled;
85 
86     private String mMethod;
87 
88     private InputStream mResponseStream;
89 
90     private final Object mLock;
91 
92     private static ExecutorService sExecutorService;
93 
94     private static final Object sExecutorServiceLock = new Object();
95 
HttpUrlConnectionUrlRequest(Context context, String url, int requestPriority, Map<String, String> headers, HttpUrlRequestListener listener)96     HttpUrlConnectionUrlRequest(Context context, String url,
97             int requestPriority, Map<String, String> headers,
98             HttpUrlRequestListener listener) {
99         this(context, url, requestPriority, headers,
100                 new ChunkedWritableByteChannel(), listener);
101     }
102 
HttpUrlConnectionUrlRequest(Context context, String url, int requestPriority, Map<String, String> headers, WritableByteChannel sink, HttpUrlRequestListener listener)103     HttpUrlConnectionUrlRequest(Context context, String url,
104             int requestPriority, Map<String, String> headers,
105             WritableByteChannel sink, HttpUrlRequestListener listener) {
106         if (context == null) {
107             throw new NullPointerException("Context is required");
108         }
109         if (url == null) {
110             throw new NullPointerException("URL is required");
111         }
112         mContext = context;
113         mUrl = url;
114         mHeaders = headers;
115         mSink = sink;
116         mListener = listener;
117         mLock = new Object();
118     }
119 
getExecutor()120     private static ExecutorService getExecutor() {
121         synchronized (sExecutorServiceLock) {
122             if (sExecutorService == null) {
123                 ThreadFactory threadFactory = new ThreadFactory() {
124                     private final AtomicInteger mCount = new AtomicInteger(1);
125 
126                         @Override
127                     public Thread newThread(Runnable r) {
128                         Thread thread = new Thread(r,
129                                 "HttpUrlConnection #" +
130                                 mCount.getAndIncrement());
131                         // Note that this thread is not doing actual networking.
132                         // It's only a controller.
133                         thread.setPriority(Thread.NORM_PRIORITY);
134                         return thread;
135                     }
136                 };
137                 sExecutorService = Executors.newCachedThreadPool(threadFactory);
138             }
139             return sExecutorService;
140         }
141     }
142 
143     @Override
getUrl()144     public String getUrl() {
145         return mUrl;
146     }
147 
148     @Override
setOffset(long offset)149     public void setOffset(long offset) {
150         mOffset = offset;
151     }
152 
153     @Override
setContentLengthLimit(long limit, boolean cancelEarly)154     public void setContentLengthLimit(long limit, boolean cancelEarly) {
155         mContentLengthLimit = limit;
156         mCancelIfContentLengthOverLimit = cancelEarly;
157     }
158 
159     @Override
setUploadData(String contentType, byte[] data)160     public void setUploadData(String contentType, byte[] data) {
161         validateNotStarted();
162         mPostContentType = contentType;
163         mPostData = data;
164         mPostDataChannel = null;
165     }
166 
167     @Override
setUploadChannel(String contentType, ReadableByteChannel channel, long contentLength)168     public void setUploadChannel(String contentType,
169             ReadableByteChannel channel, long contentLength) {
170         validateNotStarted();
171         if (contentLength > Integer.MAX_VALUE) {
172             throw new IllegalArgumentException(
173                 "Upload contentLength is too big.");
174         }
175         mUploadContentLength = (int) contentLength;
176         mPostContentType = contentType;
177         mPostDataChannel = channel;
178         mPostData = null;
179     }
180 
181 
182     @Override
setHttpMethod(String method)183     public void setHttpMethod(String method) {
184         validateNotStarted();
185         mMethod = method;
186     }
187 
188     @Override
start()189     public void start() {
190         getExecutor().execute(new Runnable() {
191             @Override
192             public void run() {
193                 startOnExecutorThread();
194             }
195         });
196     }
197 
startOnExecutorThread()198     private void startOnExecutorThread() {
199         boolean readingResponse = false;
200         try {
201             synchronized (mLock) {
202                 if (mCanceled) {
203                     return;
204                 }
205             }
206 
207             URL url = new URL(mUrl);
208             mConnection = (HttpURLConnection) url.openConnection();
209             // If configured, use the provided http verb.
210             if (mMethod != null) {
211                 try {
212                     mConnection.setRequestMethod(mMethod);
213                 } catch (ProtocolException e) {
214                     // Since request hasn't started earlier, it
215                     // must be an illegal HTTP verb.
216                     throw new IllegalArgumentException(e);
217                 }
218             }
219             mConnection.setConnectTimeout(CONNECT_TIMEOUT);
220             mConnection.setReadTimeout(READ_TIMEOUT);
221             mConnection.setInstanceFollowRedirects(true);
222             if (mHeaders != null) {
223                 for (Entry<String, String> header : mHeaders.entrySet()) {
224                     mConnection.setRequestProperty(header.getKey(),
225                             header.getValue());
226                 }
227             }
228 
229             if (mOffset != 0) {
230                 mConnection.setRequestProperty("Range",
231                         "bytes=" + mOffset + "-");
232             }
233 
234             if (mConnection.getRequestProperty("User-Agent") == null) {
235                 mConnection.setRequestProperty("User-Agent",
236                         UserAgent.from(mContext));
237             }
238 
239             if (mPostData != null || mPostDataChannel != null) {
240                 uploadData();
241             }
242 
243             InputStream stream = null;
244             try {
245                 // We need to open the stream before asking for the response
246                 // code.
247                 stream = mConnection.getInputStream();
248             } catch (FileNotFoundException ex) {
249                 // Ignore - the response has no body.
250             }
251 
252             mHttpStatusCode = mConnection.getResponseCode();
253             mContentType = mConnection.getContentType();
254             mContentLength = mConnection.getContentLength();
255             if (mContentLengthLimit > 0 && mContentLength > mContentLengthLimit
256                     && mCancelIfContentLengthOverLimit) {
257                 onContentLengthOverLimit();
258                 return;
259             }
260 
261             mListener.onResponseStarted(this);
262 
263             mResponseStream = isError(mHttpStatusCode) ? mConnection
264                     .getErrorStream()
265                     : stream;
266 
267             if (mResponseStream != null
268                     && "gzip".equals(mConnection.getContentEncoding())) {
269                 mResponseStream = new GZIPInputStream(mResponseStream);
270                 mContentLength = -1;
271             }
272 
273             if (mOffset != 0) {
274                 // The server may ignore the request for a byte range.
275                 if (mHttpStatusCode == HttpStatus.SC_OK) {
276                     if (mContentLength != -1) {
277                         mContentLength -= mOffset;
278                     }
279                     mSkippingToOffset = true;
280                 } else {
281                     mSize = mOffset;
282                 }
283             }
284 
285             if (mResponseStream != null) {
286                 readingResponse = true;
287                 readResponseAsync();
288             }
289         } catch (IOException e) {
290             mException = e;
291         } finally {
292             if (mPostDataChannel != null) {
293                 try {
294                     mPostDataChannel.close();
295                 } catch (IOException e) {
296                     // Ignore
297                 }
298             }
299 
300             // Don't call onRequestComplete yet if we are reading the response
301             // on a separate thread
302             if (!readingResponse) {
303                 mListener.onRequestComplete(this);
304             }
305         }
306     }
307 
uploadData()308     private void uploadData() throws IOException {
309         mConnection.setDoOutput(true);
310         if (!TextUtils.isEmpty(mPostContentType)) {
311             mConnection.setRequestProperty("Content-Type", mPostContentType);
312         }
313 
314         OutputStream uploadStream = null;
315         try {
316             if (mPostData != null) {
317                 mConnection.setFixedLengthStreamingMode(mPostData.length);
318                 uploadStream = mConnection.getOutputStream();
319                 uploadStream.write(mPostData);
320             } else {
321                 mConnection.setFixedLengthStreamingMode(mUploadContentLength);
322                 uploadStream = mConnection.getOutputStream();
323                 byte[] bytes = new byte[MAX_CHUNK_SIZE];
324                 ByteBuffer byteBuffer = ByteBuffer.wrap(bytes);
325                 while (mPostDataChannel.read(byteBuffer) > 0) {
326                     byteBuffer.flip();
327                     uploadStream.write(bytes, 0, byteBuffer.limit());
328                     byteBuffer.clear();
329                 }
330             }
331         } finally {
332             if (uploadStream != null) {
333                 uploadStream.close();
334             }
335         }
336     }
337 
readResponseAsync()338     private void readResponseAsync() {
339         getExecutor().execute(new Runnable() {
340             @Override
341             public void run() {
342                 readResponse();
343             }
344         });
345     }
346 
readResponse()347     private void readResponse() {
348         try {
349             if (mResponseStream != null) {
350                 readResponseStream();
351             }
352         } catch (IOException e) {
353             mException = e;
354         } finally {
355             try {
356                 mConnection.disconnect();
357             } catch (ArrayIndexOutOfBoundsException t) {
358                 // Ignore it.
359             }
360 
361             try {
362                 mSink.close();
363             } catch (IOException e) {
364                 if (mException == null) {
365                     mException = e;
366                 }
367             }
368         }
369         mListener.onRequestComplete(this);
370     }
371 
readResponseStream()372     private void readResponseStream() throws IOException {
373         byte[] buffer = new byte[MAX_CHUNK_SIZE];
374         int size;
375         while (!isCanceled() && (size = mResponseStream.read(buffer)) != -1) {
376             int start = 0;
377             int count = size;
378             mSize += size;
379             if (mSkippingToOffset) {
380                 if (mSize <= mOffset) {
381                     continue;
382                 } else {
383                     mSkippingToOffset = false;
384                     start = (int) (mOffset - (mSize - size));
385                     count -= start;
386                 }
387             }
388 
389             if (mContentLengthLimit != 0 && mSize > mContentLengthLimit) {
390                 count -= (int) (mSize - mContentLengthLimit);
391                 if (count > 0) {
392                     mSink.write(ByteBuffer.wrap(buffer, start, count));
393                 }
394                 onContentLengthOverLimit();
395                 return;
396             }
397 
398             mSink.write(ByteBuffer.wrap(buffer, start, count));
399         }
400     }
401 
402     @Override
cancel()403     public void cancel() {
404         synchronized (mLock) {
405             if (mCanceled) {
406                 return;
407             }
408 
409             mCanceled = true;
410         }
411     }
412 
413     @Override
isCanceled()414     public boolean isCanceled() {
415         synchronized (mLock) {
416             return mCanceled;
417         }
418     }
419 
420     @Override
getNegotiatedProtocol()421     public String getNegotiatedProtocol() {
422         return "";
423     }
424 
425     @Override
getHttpStatusCode()426     public int getHttpStatusCode() {
427         int httpStatusCode = mHttpStatusCode;
428 
429         // If we have been able to successfully resume a previously interrupted
430         // download,
431         // the status code will be 206, not 200. Since the rest of the
432         // application is
433         // expecting 200 to indicate success, we need to fake it.
434         if (httpStatusCode == HttpStatus.SC_PARTIAL_CONTENT) {
435             httpStatusCode = HttpStatus.SC_OK;
436         }
437         return httpStatusCode;
438     }
439 
440     @Override
getException()441     public IOException getException() {
442         if (mException == null && mContentLengthOverLimit) {
443             mException = new ResponseTooLargeException();
444         }
445         return mException;
446     }
447 
onContentLengthOverLimit()448     private void onContentLengthOverLimit() {
449         mContentLengthOverLimit = true;
450         cancel();
451     }
452 
isError(int statusCode)453     private static boolean isError(int statusCode) {
454         return (statusCode / 100) != 2;
455     }
456 
457     /**
458      * Returns the response as a ByteBuffer.
459      */
460     @Override
getByteBuffer()461     public ByteBuffer getByteBuffer() {
462         return ((ChunkedWritableByteChannel) mSink).getByteBuffer();
463     }
464 
465     @Override
getResponseAsBytes()466     public byte[] getResponseAsBytes() {
467         return ((ChunkedWritableByteChannel) mSink).getBytes();
468     }
469 
470     @Override
getContentLength()471     public long getContentLength() {
472         return mContentLength;
473     }
474 
475     @Override
getContentType()476     public String getContentType() {
477         return mContentType;
478     }
479 
480     @Override
getHeader(String name)481     public String getHeader(String name) {
482         if (mConnection == null) {
483             throw new IllegalStateException("Response headers not available");
484         }
485         Map<String, List<String>> headerFields = mConnection.getHeaderFields();
486         if (headerFields != null) {
487             List<String> headerValues = headerFields.get(name);
488             if (headerValues != null) {
489                 return TextUtils.join(", ", headerValues);
490             }
491         }
492         return null;
493     }
494 
495     @Override
getAllHeaders()496     public Map<String, List<String>> getAllHeaders() {
497         if (mConnection == null) {
498             throw new IllegalStateException("Response headers not available");
499         }
500         return mConnection.getHeaderFields();
501     }
502 
validateNotStarted()503     private void validateNotStarted() {
504         if (mStarted) {
505             throw new IllegalStateException("Request already started");
506         }
507     }
508 }
509