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