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 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 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 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 144 public String getUrl() { 145 return mUrl; 146 } 147 148 @Override 149 public void setOffset(long offset) { 150 mOffset = offset; 151 } 152 153 @Override 154 public void setContentLengthLimit(long limit, boolean cancelEarly) { 155 mContentLengthLimit = limit; 156 mCancelIfContentLengthOverLimit = cancelEarly; 157 } 158 159 @Override 160 public void setUploadData(String contentType, byte[] data) { 161 validateNotStarted(); 162 mPostContentType = contentType; 163 mPostData = data; 164 mPostDataChannel = null; 165 } 166 167 @Override 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 183 public void setHttpMethod(String method) { 184 validateNotStarted(); 185 mMethod = method; 186 } 187 188 @Override 189 public void start() { 190 getExecutor().execute(new Runnable() { 191 @Override 192 public void run() { 193 startOnExecutorThread(); 194 } 195 }); 196 } 197 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 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 338 private void readResponseAsync() { 339 getExecutor().execute(new Runnable() { 340 @Override 341 public void run() { 342 readResponse(); 343 } 344 }); 345 } 346 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 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 403 public void cancel() { 404 synchronized (mLock) { 405 if (mCanceled) { 406 return; 407 } 408 409 mCanceled = true; 410 } 411 } 412 413 @Override 414 public boolean isCanceled() { 415 synchronized (mLock) { 416 return mCanceled; 417 } 418 } 419 420 @Override 421 public String getNegotiatedProtocol() { 422 return ""; 423 } 424 425 @Override 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 441 public IOException getException() { 442 if (mException == null && mContentLengthOverLimit) { 443 mException = new ResponseTooLargeException(); 444 } 445 return mException; 446 } 447 448 private void onContentLengthOverLimit() { 449 mContentLengthOverLimit = true; 450 cancel(); 451 } 452 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 461 public ByteBuffer getByteBuffer() { 462 return ((ChunkedWritableByteChannel) mSink).getByteBuffer(); 463 } 464 465 @Override 466 public byte[] getResponseAsBytes() { 467 return ((ChunkedWritableByteChannel) mSink).getBytes(); 468 } 469 470 @Override 471 public long getContentLength() { 472 return mContentLength; 473 } 474 475 @Override 476 public String getContentType() { 477 return mContentType; 478 } 479 480 @Override 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 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 503 private void validateNotStarted() { 504 if (mStarted) { 505 throw new IllegalStateException("Request already started"); 506 } 507 } 508 } 509