1 /* 2 * Copyright (C) 2006 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package android.net.http; 18 19 import java.io.EOFException; 20 import java.io.InputStream; 21 import java.io.IOException; 22 import java.util.Iterator; 23 import java.util.Map; 24 import java.util.Map.Entry; 25 import java.util.zip.GZIPInputStream; 26 27 import org.apache.http.entity.InputStreamEntity; 28 import org.apache.http.Header; 29 import org.apache.http.HttpEntity; 30 import org.apache.http.HttpEntityEnclosingRequest; 31 import org.apache.http.HttpException; 32 import org.apache.http.HttpHost; 33 import org.apache.http.HttpRequest; 34 import org.apache.http.HttpStatus; 35 import org.apache.http.ParseException; 36 import org.apache.http.ProtocolVersion; 37 38 import org.apache.http.StatusLine; 39 import org.apache.http.message.BasicHttpRequest; 40 import org.apache.http.message.BasicHttpEntityEnclosingRequest; 41 import org.apache.http.protocol.RequestContent; 42 43 /** 44 * Represents an HTTP request for a given host. 45 * 46 * {@hide} 47 */ 48 49 class Request { 50 51 /** The eventhandler to call as the request progresses */ 52 EventHandler mEventHandler; 53 54 private Connection mConnection; 55 56 /** The Apache http request */ 57 BasicHttpRequest mHttpRequest; 58 59 /** The path component of this request */ 60 String mPath; 61 62 /** Host serving this request */ 63 HttpHost mHost; 64 65 /** Set if I'm using a proxy server */ 66 HttpHost mProxyHost; 67 68 /** True if request has been cancelled */ 69 volatile boolean mCancelled = false; 70 71 int mFailCount = 0; 72 73 // This will be used to set the Range field if we retry a connection. This 74 // is http/1.1 feature. 75 private int mReceivedBytes = 0; 76 77 private InputStream mBodyProvider; 78 private int mBodyLength; 79 80 private final static String HOST_HEADER = "Host"; 81 private final static String ACCEPT_ENCODING_HEADER = "Accept-Encoding"; 82 private final static String CONTENT_LENGTH_HEADER = "content-length"; 83 84 /* Used to synchronize waitUntilComplete() requests */ 85 private final Object mClientResource = new Object(); 86 87 /** True if loading should be paused **/ 88 private boolean mLoadingPaused = false; 89 90 /** 91 * Processor used to set content-length and transfer-encoding 92 * headers. 93 */ 94 private static RequestContent requestContentProcessor = 95 new RequestContent(); 96 97 /** 98 * Instantiates a new Request. 99 * @param method GET/POST/PUT 100 * @param host The server that will handle this request 101 * @param path path part of URI 102 * @param bodyProvider InputStream providing HTTP body, null if none 103 * @param bodyLength length of body, must be 0 if bodyProvider is null 104 * @param eventHandler request will make progress callbacks on 105 * this interface 106 * @param headers reqeust headers 107 */ Request(String method, HttpHost host, HttpHost proxyHost, String path, InputStream bodyProvider, int bodyLength, EventHandler eventHandler, Map<String, String> headers)108 Request(String method, HttpHost host, HttpHost proxyHost, String path, 109 InputStream bodyProvider, int bodyLength, 110 EventHandler eventHandler, 111 Map<String, String> headers) { 112 mEventHandler = eventHandler; 113 mHost = host; 114 mProxyHost = proxyHost; 115 mPath = path; 116 mBodyProvider = bodyProvider; 117 mBodyLength = bodyLength; 118 119 if (bodyProvider == null && !"POST".equalsIgnoreCase(method)) { 120 mHttpRequest = new BasicHttpRequest(method, getUri()); 121 } else { 122 mHttpRequest = new BasicHttpEntityEnclosingRequest( 123 method, getUri()); 124 // it is ok to have null entity for BasicHttpEntityEnclosingRequest. 125 // By using BasicHttpEntityEnclosingRequest, it will set up the 126 // correct content-length, content-type and content-encoding. 127 if (bodyProvider != null) { 128 setBodyProvider(bodyProvider, bodyLength); 129 } 130 } 131 addHeader(HOST_HEADER, getHostPort()); 132 133 /* FIXME: if webcore will make the root document a 134 high-priority request, we can ask for gzip encoding only on 135 high priority reqs (saving the trouble for images, etc) */ 136 addHeader(ACCEPT_ENCODING_HEADER, "gzip"); 137 addHeaders(headers); 138 } 139 140 /** 141 * @param pause True if the load should be paused. 142 */ setLoadingPaused(boolean pause)143 synchronized void setLoadingPaused(boolean pause) { 144 mLoadingPaused = pause; 145 146 // Wake up the paused thread if we're unpausing the load. 147 if (!mLoadingPaused) { 148 notify(); 149 } 150 } 151 152 /** 153 * @param connection Request served by this connection 154 */ setConnection(Connection connection)155 void setConnection(Connection connection) { 156 mConnection = connection; 157 } 158 getEventHandler()159 /* package */ EventHandler getEventHandler() { 160 return mEventHandler; 161 } 162 163 /** 164 * Add header represented by given pair to request. Header will 165 * be formatted in request as "name: value\r\n". 166 * @param name of header 167 * @param value of header 168 */ addHeader(String name, String value)169 void addHeader(String name, String value) { 170 if (name == null) { 171 String damage = "Null http header name"; 172 HttpLog.e(damage); 173 throw new NullPointerException(damage); 174 } 175 if (value == null || value.length() == 0) { 176 String damage = "Null or empty value for header \"" + name + "\""; 177 HttpLog.e(damage); 178 throw new RuntimeException(damage); 179 } 180 mHttpRequest.addHeader(name, value); 181 } 182 183 /** 184 * Add all headers in given map to this request. This is a helper 185 * method: it calls addHeader for each pair in the map. 186 */ addHeaders(Map<String, String> headers)187 void addHeaders(Map<String, String> headers) { 188 if (headers == null) { 189 return; 190 } 191 192 Entry<String, String> entry; 193 Iterator<Entry<String, String>> i = headers.entrySet().iterator(); 194 while (i.hasNext()) { 195 entry = i.next(); 196 addHeader(entry.getKey(), entry.getValue()); 197 } 198 } 199 200 /** 201 * Send the request line and headers 202 */ sendRequest(AndroidHttpClientConnection httpClientConnection)203 void sendRequest(AndroidHttpClientConnection httpClientConnection) 204 throws HttpException, IOException { 205 206 if (mCancelled) return; // don't send cancelled requests 207 208 if (HttpLog.LOGV) { 209 HttpLog.v("Request.sendRequest() " + mHost.getSchemeName() + "://" + getHostPort()); 210 // HttpLog.v(mHttpRequest.getRequestLine().toString()); 211 if (false) { 212 Iterator i = mHttpRequest.headerIterator(); 213 while (i.hasNext()) { 214 Header header = (Header)i.next(); 215 HttpLog.v(header.getName() + ": " + header.getValue()); 216 } 217 } 218 } 219 220 requestContentProcessor.process(mHttpRequest, 221 mConnection.getHttpContext()); 222 httpClientConnection.sendRequestHeader(mHttpRequest); 223 if (mHttpRequest instanceof HttpEntityEnclosingRequest) { 224 httpClientConnection.sendRequestEntity( 225 (HttpEntityEnclosingRequest) mHttpRequest); 226 } 227 228 if (HttpLog.LOGV) { 229 HttpLog.v("Request.requestSent() " + mHost.getSchemeName() + "://" + getHostPort() + mPath); 230 } 231 } 232 233 234 /** 235 * Receive a single http response. 236 * 237 * @param httpClientConnection the request to receive the response for. 238 */ readResponse(AndroidHttpClientConnection httpClientConnection)239 void readResponse(AndroidHttpClientConnection httpClientConnection) 240 throws IOException, ParseException { 241 242 if (mCancelled) return; // don't send cancelled requests 243 244 StatusLine statusLine = null; 245 boolean hasBody = false; 246 httpClientConnection.flush(); 247 int statusCode = 0; 248 249 Headers header = new Headers(); 250 do { 251 statusLine = httpClientConnection.parseResponseHeader(header); 252 statusCode = statusLine.getStatusCode(); 253 } while (statusCode < HttpStatus.SC_OK); 254 if (HttpLog.LOGV) HttpLog.v( 255 "Request.readResponseStatus() " + 256 statusLine.toString().length() + " " + statusLine); 257 258 ProtocolVersion v = statusLine.getProtocolVersion(); 259 mEventHandler.status(v.getMajor(), v.getMinor(), 260 statusCode, statusLine.getReasonPhrase()); 261 mEventHandler.headers(header); 262 HttpEntity entity = null; 263 hasBody = canResponseHaveBody(mHttpRequest, statusCode); 264 265 if (hasBody) 266 entity = httpClientConnection.receiveResponseEntity(header); 267 268 // restrict the range request to the servers claiming that they are 269 // accepting ranges in bytes 270 boolean supportPartialContent = "bytes".equalsIgnoreCase(header 271 .getAcceptRanges()); 272 273 if (entity != null) { 274 InputStream is = entity.getContent(); 275 276 // process gzip content encoding 277 Header contentEncoding = entity.getContentEncoding(); 278 InputStream nis = null; 279 byte[] buf = null; 280 int count = 0; 281 try { 282 if (contentEncoding != null && 283 contentEncoding.getValue().equals("gzip")) { 284 nis = new GZIPInputStream(is); 285 } else { 286 nis = is; 287 } 288 289 /* accumulate enough data to make it worth pushing it 290 * up the stack */ 291 buf = mConnection.getBuf(); 292 int len = 0; 293 int lowWater = buf.length / 2; 294 while (len != -1) { 295 synchronized(this) { 296 while (mLoadingPaused) { 297 // Put this (network loading) thread to sleep if WebCore 298 // has asked us to. This can happen with plugins for 299 // example, if we are streaming data but the plugin has 300 // filled its internal buffers. 301 try { 302 wait(); 303 } catch (InterruptedException e) { 304 HttpLog.e("Interrupted exception whilst " 305 + "network thread paused at WebCore's request." 306 + " " + e.getMessage()); 307 } 308 } 309 } 310 311 len = nis.read(buf, count, buf.length - count); 312 313 if (len != -1) { 314 count += len; 315 if (supportPartialContent) mReceivedBytes += len; 316 } 317 if (len == -1 || count >= lowWater) { 318 if (HttpLog.LOGV) HttpLog.v("Request.readResponse() " + count); 319 mEventHandler.data(buf, count); 320 count = 0; 321 } 322 } 323 } catch (EOFException e) { 324 /* InflaterInputStream throws an EOFException when the 325 server truncates gzipped content. Handle this case 326 as we do truncated non-gzipped content: no error */ 327 if (count > 0) { 328 // if there is uncommited content, we should commit them 329 mEventHandler.data(buf, count); 330 } 331 if (HttpLog.LOGV) HttpLog.v( "readResponse() handling " + e); 332 } catch(IOException e) { 333 // don't throw if we have a non-OK status code 334 if (statusCode == HttpStatus.SC_OK 335 || statusCode == HttpStatus.SC_PARTIAL_CONTENT) { 336 if (supportPartialContent && count > 0) { 337 // if there is uncommited content, we should commit them 338 // as we will continue the request 339 mEventHandler.data(buf, count); 340 } 341 throw e; 342 } 343 } finally { 344 if (nis != null) { 345 nis.close(); 346 } 347 } 348 } 349 mConnection.setCanPersist(entity, statusLine.getProtocolVersion(), 350 header.getConnectionType()); 351 mEventHandler.endData(); 352 complete(); 353 354 if (HttpLog.LOGV) HttpLog.v("Request.readResponse(): done " + 355 mHost.getSchemeName() + "://" + getHostPort() + mPath); 356 } 357 358 /** 359 * Data will not be sent to or received from server after cancel() 360 * call. Does not close connection--use close() below for that. 361 * 362 * Called by RequestHandle from non-network thread 363 */ cancel()364 synchronized void cancel() { 365 if (HttpLog.LOGV) { 366 HttpLog.v("Request.cancel(): " + getUri()); 367 } 368 369 // Ensure that the network thread is not blocked by a hanging request from WebCore to 370 // pause the load. 371 mLoadingPaused = false; 372 notify(); 373 374 mCancelled = true; 375 if (mConnection != null) { 376 mConnection.cancel(); 377 } 378 } 379 getHostPort()380 String getHostPort() { 381 String myScheme = mHost.getSchemeName(); 382 int myPort = mHost.getPort(); 383 384 // Only send port when we must... many servers can't deal with it 385 if (myPort != 80 && myScheme.equals("http") || 386 myPort != 443 && myScheme.equals("https")) { 387 return mHost.toHostString(); 388 } else { 389 return mHost.getHostName(); 390 } 391 } 392 getUri()393 String getUri() { 394 if (mProxyHost == null || 395 mHost.getSchemeName().equals("https")) { 396 return mPath; 397 } 398 return mHost.getSchemeName() + "://" + getHostPort() + mPath; 399 } 400 401 /** 402 * for debugging 403 */ toString()404 public String toString() { 405 return mPath; 406 } 407 408 409 /** 410 * If this request has been sent once and failed, it must be reset 411 * before it can be sent again. 412 */ reset()413 void reset() { 414 /* clear content-length header */ 415 mHttpRequest.removeHeaders(CONTENT_LENGTH_HEADER); 416 417 if (mBodyProvider != null) { 418 try { 419 mBodyProvider.reset(); 420 } catch (IOException ex) { 421 if (HttpLog.LOGV) HttpLog.v( 422 "failed to reset body provider " + 423 getUri()); 424 } 425 setBodyProvider(mBodyProvider, mBodyLength); 426 } 427 428 if (mReceivedBytes > 0) { 429 // reset the fail count as we continue the request 430 mFailCount = 0; 431 // set the "Range" header to indicate that the retry will continue 432 // instead of restarting the request 433 HttpLog.v("*** Request.reset() to range:" + mReceivedBytes); 434 mHttpRequest.setHeader("Range", "bytes=" + mReceivedBytes + "-"); 435 } 436 } 437 438 /** 439 * Pause thread request completes. Used for synchronous requests, 440 * and testing 441 */ waitUntilComplete()442 void waitUntilComplete() { 443 synchronized (mClientResource) { 444 try { 445 if (HttpLog.LOGV) HttpLog.v("Request.waitUntilComplete()"); 446 mClientResource.wait(); 447 if (HttpLog.LOGV) HttpLog.v("Request.waitUntilComplete() done waiting"); 448 } catch (InterruptedException e) { 449 } 450 } 451 } 452 complete()453 void complete() { 454 synchronized (mClientResource) { 455 mClientResource.notifyAll(); 456 } 457 } 458 459 /** 460 * Decide whether a response comes with an entity. 461 * The implementation in this class is based on RFC 2616. 462 * Unknown methods and response codes are supposed to 463 * indicate responses with an entity. 464 * <br/> 465 * Derived executors can override this method to handle 466 * methods and response codes not specified in RFC 2616. 467 * 468 * @param request the request, to obtain the executed method 469 * @param response the response, to obtain the status code 470 */ 471 canResponseHaveBody(final HttpRequest request, final int status)472 private static boolean canResponseHaveBody(final HttpRequest request, 473 final int status) { 474 475 if ("HEAD".equalsIgnoreCase(request.getRequestLine().getMethod())) { 476 return false; 477 } 478 return status >= HttpStatus.SC_OK 479 && status != HttpStatus.SC_NO_CONTENT 480 && status != HttpStatus.SC_NOT_MODIFIED; 481 } 482 483 /** 484 * Supply an InputStream that provides the body of a request. It's 485 * not great that the caller must also provide the length of the data 486 * returned by that InputStream, but the client needs to know up 487 * front, and I'm not sure how to get this out of the InputStream 488 * itself without a costly readthrough. I'm not sure skip() would 489 * do what we want. If you know a better way, please let me know. 490 */ setBodyProvider(InputStream bodyProvider, int bodyLength)491 private void setBodyProvider(InputStream bodyProvider, int bodyLength) { 492 if (!bodyProvider.markSupported()) { 493 throw new IllegalArgumentException( 494 "bodyProvider must support mark()"); 495 } 496 // Mark beginning of stream 497 bodyProvider.mark(Integer.MAX_VALUE); 498 499 ((BasicHttpEntityEnclosingRequest)mHttpRequest).setEntity( 500 new InputStreamEntity(bodyProvider, bodyLength)); 501 } 502 503 504 /** 505 * Handles SSL error(s) on the way down from the user (the user 506 * has already provided their feedback). 507 */ handleSslErrorResponse(boolean proceed)508 public void handleSslErrorResponse(boolean proceed) { 509 HttpsConnection connection = (HttpsConnection)(mConnection); 510 if (connection != null) { 511 connection.restartConnection(proceed); 512 } 513 } 514 515 /** 516 * Helper: calls error() on eventhandler with appropriate message 517 * This should not be called before the mConnection is set. 518 */ error(int errorId, int resourceId)519 void error(int errorId, int resourceId) { 520 mEventHandler.error( 521 errorId, 522 mConnection.mContext.getText( 523 resourceId).toString()); 524 } 525 526 } 527