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