diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 000000000000..d64569567334 --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/pom.xml b/pom.xml new file mode 100644 index 000000000000..348a3a6cb105 --- /dev/null +++ b/pom.xml @@ -0,0 +1,166 @@ + + + + 4.0.0 + + + org.sonatype.oss + oss-parent + 7 + + com.squareup.okhttp + okhttp + 20120723 + jar + + okhttp + An HTTP+SPDY client for Android and Java applications + https://github.com/square/okhttp + + + UTF-8 + + + 1.6 + 8.1.2.v20120308 + 20120401 + + + 3.8.2 + + + + https://github.com/square/okhttp/ + scm:git:https://github.com/square/okhttp.git + scm:git:git@github.com:square/okhttp.git + + + + GitHub Issues + https://github.com/square/okhttp/issues + + + + + Apache 2.0 + http://www.apache.org/licenses/LICENSE-2.0.txt + + + + + + org.mortbay.jetty.npn + npn-boot + ${npn.version} + + + com.google.mockwebserver + mockwebserver + ${mockwebserver.version} + test + + + junit + junit + ${junit.version} + test + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 2.5 + + ${java.version} + ${java.version} + + + + org.sonatype.plugins + jarjar-maven-plugin + + + package + + jarjar + + + + asm:asm + org.sonatype.sisu.inject:cglib + + + + libcore.** + com.squareup.okhttp.libcore.@1 + + + com.squareup.okhttp.** + + + + + + + + maven-surefire-plugin + + -Xbootclasspath/p:${settings.localRepository}/org/mortbay/jetty/npn/npn-boot/${npn.version}/npn-boot-${npn.version}.jar + + + + org.apache.maven.plugins + maven-source-plugin + + + attach-sources + verify + jar-no-fork + + + + + org.apache.maven.plugins + maven-javadoc-plugin + + + attach-javadocs + jar + + + + + org.apache.maven.plugins + maven-gpg-plugin + + + sign-artifacts + verify + + sign + + + + + + + + diff --git a/src/main/java/com/squareup/okhttp/OkHttpConnection.java b/src/main/java/com/squareup/okhttp/OkHttpConnection.java new file mode 100644 index 000000000000..d98d330ec326 --- /dev/null +++ b/src/main/java/com/squareup/okhttp/OkHttpConnection.java @@ -0,0 +1,808 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.squareup.okhttp; + +import java.io.IOException; +import java.io.InputStream; +import java.net.ProtocolException; +import java.net.Proxy; +import java.net.SocketPermission; +import java.net.URL; +import java.net.URLConnection; +import java.util.Arrays; +import libcore.net.http.HttpEngine; + +/** + * An {@link java.net.URLConnection} for HTTP (RFC 2616) used to send and + * receive data over the web. Data may be of any type and length. This class may + * be used to send and receive streaming data whose length is not known in + * advance. + * + *

Uses of this class follow a pattern: + *

    + *
  1. Obtain a new {@code HttpURLConnection} by calling {@link + * java.net.URL#openConnection() URL.openConnection()} and casting the result to + * {@code HttpURLConnection}. + *
  2. Prepare the request. The primary property of a request is its URI. + * Request headers may also include metadata such as credentials, preferred + * content types, and session cookies. + *
  3. Optionally upload a request body. Instances must be configured with + * {@link #setDoOutput(boolean) setDoOutput(true)} if they include a + * request body. Transmit data by writing to the stream returned by {@link + * #getOutputStream()}. + *
  4. Read the response. Response headers typically include metadata such as + * the response body's content type and length, modified dates and session + * cookies. The response body may be read from the stream returned by {@link + * #getInputStream()}. If the response has no body, that method returns an + * empty stream. + *
  5. Disconnect. Once the response body has been read, the {@code + * HttpURLConnection} should be closed by calling {@link #disconnect()}. + * Disconnecting releases the resources held by a connection so they may + * be closed or reused. + *
+ * + *

For example, to retrieve the webpage at {@code http://www.android.com/}: + *

   {@code
+ *   URL url = new URL("http://www.android.com/");
+ *   HttpURLConnection urlConnection = (HttpURLConnection) url.openConnection();
+ *   try {
+ *     InputStream in = new BufferedInputStream(urlConnection.getInputStream());
+ *     readStream(in);
+ *   } finally {
+ *     urlConnection.disconnect();
+ *   }
+ * }
+ * + *

Secure Communication with HTTPS

+ * Calling {@link java.net.URL#openConnection()} on a URL with the "https" + * scheme will return an {@code HttpsURLConnection}, which allows for + * overriding the default {@link javax.net.ssl.HostnameVerifier + * HostnameVerifier} and {@link javax.net.ssl.SSLSocketFactory + * SSLSocketFactory}. An application-supplied {@code SSLSocketFactory} + * created from an {@link javax.net.ssl.SSLContext SSLContext} can + * provide a custom {@link javax.net.ssl.X509TrustManager + * X509TrustManager} for verifying certificate chains and a custom + * {@link javax.net.ssl.X509KeyManager X509KeyManager} for supplying + * client certificates. See {@link OkHttpsConnection HttpsURLConnection} for + * more details. + * + *

Response Handling

+ * {@code HttpURLConnection} will follow up to five HTTP redirects. It will + * follow redirects from one origin server to another. This implementation + * doesn't follow redirects from HTTPS to HTTP or vice versa. + * + *

If the HTTP response indicates that an error occurred, {@link + * #getInputStream()} will throw an {@link java.io.IOException}. Use {@link + * #getErrorStream()} to read the error response. The headers can be read in + * the normal way using {@link #getHeaderFields()}, + * + *

Posting Content

+ * To upload data to a web server, configure the connection for output using + * {@link #setDoOutput(boolean) setDoOutput(true)}. + * + *

For best performance, you should call either {@link + * #setFixedLengthStreamingMode(int)} when the body length is known in advance, + * or {@link #setChunkedStreamingMode(int)} when it is not. Otherwise {@code + * HttpURLConnection} will be forced to buffer the complete request body in + * memory before it is transmitted, wasting (and possibly exhausting) heap and + * increasing latency. + * + *

For example, to perform an upload:

   {@code
+ *   HttpURLConnection urlConnection = (HttpURLConnection) url.openConnection();
+ *   try {
+ *     urlConnection.setDoOutput(true);
+ *     urlConnection.setChunkedStreamingMode(0);
+ *
+ *     OutputStream out = new BufferedOutputStream(urlConnection.getOutputStream());
+ *     writeStream(out);
+ *
+ *     InputStream in = new BufferedInputStream(urlConnection.getInputStream());
+ *     readStream(in);
+ *   } finally {
+ *     urlConnection.disconnect();
+ *   }
+ * }
+ * + *

Performance

+ * The input and output streams returned by this class are not + * buffered. Most callers should wrap the returned streams with {@link + * java.io.BufferedInputStream BufferedInputStream} or {@link + * java.io.BufferedOutputStream BufferedOutputStream}. Callers that do only bulk + * reads or writes may omit buffering. + * + *

When transferring large amounts of data to or from a server, use streams + * to limit how much data is in memory at once. Unless you need the entire + * body to be in memory at once, process it as a stream (rather than storing + * the complete body as a single byte array or string). + * + *

To reduce latency, this class may reuse the same underlying {@code Socket} + * for multiple request/response pairs. As a result, HTTP connections may be + * held open longer than necessary. Calls to {@link #disconnect()} may return + * the socket to a pool of connected sockets. This behavior can be disabled by + * setting the {@code http.keepAlive} system property to {@code false} before + * issuing any HTTP requests. The {@code http.maxConnections} property may be + * used to control how many idle connections to each server will be held. + * + *

By default, this implementation of {@code HttpURLConnection} requests that + * servers use gzip compression. Since {@link #getContentLength()} returns the + * number of bytes transmitted, you cannot use that method to predict how many + * bytes can be read from {@link #getInputStream()}. Instead, read that stream + * until it is exhausted: when {@link java.io.InputStream#read} returns -1. Gzip + * compression can be disabled by setting the acceptable encodings in the + * request header:

   {@code
+ *   urlConnection.setRequestProperty("Accept-Encoding", "identity");
+ * }
+ * + *

Handling Network Sign-On

+ * Some Wi-Fi networks block Internet access until the user clicks through a + * sign-on page. Such sign-on pages are typically presented by using HTTP + * redirects. You can use {@link #getURL()} to test if your connection has been + * unexpectedly redirected. This check is not valid until after + * the response headers have been received, which you can trigger by calling + * {@link #getHeaderFields()} or {@link #getInputStream()}. For example, to + * check that a response was not redirected to an unexpected host: + *
   {@code
+ *   HttpURLConnection urlConnection = (HttpURLConnection) url.openConnection();
+ *   try {
+ *     InputStream in = new BufferedInputStream(urlConnection.getInputStream());
+ *     if (!url.getHost().equals(urlConnection.getURL().getHost())) {
+ *       // we were redirected! Kick the user out to the browser to sign on?
+ *     }
+ *     ...
+ *   } finally {
+ *     urlConnection.disconnect();
+ *   }
+ * }
+ * + *

HTTP Authentication

+ * {@code HttpURLConnection} supports HTTP basic authentication. Use + * {@link java.net.Authenticator} to set the VM-wide authentication handler: + *
   {@code
+ *   Authenticator.setDefault(new Authenticator() {
+ *     protected PasswordAuthentication getPasswordAuthentication() {
+ *       return new PasswordAuthentication(username, password.toCharArray());
+ *     }
+ *   });
+ * }
+ * Unless paired with HTTPS, this is not a secure mechanism for + * user authentication. In particular, the username, password, request and + * response are all transmitted over the network without encryption. + * + *

Sessions with Cookies

+ * To establish and maintain a potentially long-lived session between client + * and server, {@code HttpURLConnection} includes an extensible cookie manager. + * Enable VM-wide cookie management using {@link java.net.CookieHandler} and {@link + * java.net.CookieManager}:
   {@code
+ *   CookieManager cookieManager = new CookieManager();
+ *   CookieHandler.setDefault(cookieManager);
+ * }
+ * By default, {@code CookieManager} accepts cookies from the origin + * server only. Two other policies are included: {@link + * java.net.CookiePolicy#ACCEPT_ALL} and {@link java.net.CookiePolicy#ACCEPT_NONE}. Implement + * {@link java.net.CookiePolicy} to define a custom policy. + * + *

The default {@code CookieManager} keeps all accepted cookies in memory. It + * will forget these cookies when the VM exits. Implement {@link java.net.CookieStore} to + * define a custom cookie store. + * + *

In addition to the cookies set by HTTP responses, you may set cookies + * programmatically. To be included in HTTP request headers, cookies must have + * the domain and path properties set. + * + *

By default, new instances of {@code HttpCookie} work only with servers + * that support RFC 2965 + * cookies. Many web servers support only the older specification, RFC 2109. For compatibility + * with the most web servers, set the cookie version to 0. + * + *

For example, to receive {@code www.twitter.com} in French:

   {@code
+ *   HttpCookie cookie = new HttpCookie("lang", "fr");
+ *   cookie.setDomain("twitter.com");
+ *   cookie.setPath("/");
+ *   cookie.setVersion(0);
+ *   cookieManager.getCookieStore().add(new URI("http://twitter.com/"), cookie);
+ * }
+ * + *

HTTP Methods

+ *

{@code HttpURLConnection} uses the {@code GET} method by default. It will + * use {@code POST} if {@link #setDoOutput setDoOutput(true)} has been called. + * Other HTTP methods ({@code OPTIONS}, {@code HEAD}, {@code PUT}, {@code + * DELETE} and {@code TRACE}) can be used with {@link #setRequestMethod}. + * + *

Proxies

+ * By default, this class will connect directly to the origin + * server. It can also connect via an {@link java.net.Proxy.Type#HTTP HTTP} or {@link + * java.net.Proxy.Type#SOCKS SOCKS} proxy. To use a proxy, use {@link + * java.net.URL#openConnection(java.net.Proxy) URL.openConnection(Proxy)} when creating the + * connection. + * + *

IPv6 Support

+ *

This class includes transparent support for IPv6. For hosts with both IPv4 + * and IPv6 addresses, it will attempt to connect to each of a host's addresses + * until a connection is established. + * + *

Response Caching

+ * Android 4.0 (Ice Cream Sandwich) includes a response cache. See {@code + * android.net.http.HttpResponseCache} for instructions on enabling HTTP caching + * in your application. + * + *

Avoiding Bugs In Earlier Releases

+ * Prior to Android 2.2 (Froyo), this class had some frustrating bugs. In + * particular, calling {@code close()} on a readable {@code InputStream} could + * poison the + * connection pool. Work around this by disabling connection pooling: + *
   {@code
+ * private void disableConnectionReuseIfNecessary() {
+ *   // Work around pre-Froyo bugs in HTTP connection reuse.
+ *   if (Integer.parseInt(Build.VERSION.SDK) < Build.VERSION_CODES.FROYO) {
+ *     System.setProperty("http.keepAlive", "false");
+ *   }
+ * }}
+ * + *

Each instance of {@code HttpURLConnection} may be used for one + * request/response pair. Instances of this class are not thread safe. + */ +public abstract class OkHttpConnection extends URLConnection { + + /** + * The subset of HTTP methods that the user may select via {@link + * #setRequestMethod(String)}. + */ + private static final String[] PERMITTED_USER_METHODS = { + HttpEngine.OPTIONS, + HttpEngine.GET, + HttpEngine.HEAD, + HttpEngine.POST, + HttpEngine.PUT, + HttpEngine.DELETE, + HttpEngine.TRACE + // Note: we don't allow users to specify "CONNECT" + }; + + /** + * The HTTP request method of this {@code HttpURLConnection}. The default + * value is {@code "GET"}. + */ + protected String method = HttpEngine.GET; + + /** + * The status code of the response obtained from the HTTP request. The + * default value is {@code -1}. + *

+ *

  • 1xx: Informational
  • + *
  • 2xx: Success
  • + *
  • 3xx: Relocation/Redirection
  • + *
  • 4xx: Client Error
  • + *
  • 5xx: Server Error
  • + */ + protected int responseCode = -1; + + /** + * The HTTP response message which corresponds to the response code. + */ + protected String responseMessage; + + /** + * Flag to define whether the protocol will automatically follow redirects + * or not. The default value is {@code true}. + */ + protected boolean instanceFollowRedirects = followRedirects; + + private static boolean followRedirects = true; + + /** + * If the HTTP chunked encoding is enabled this parameter defines the + * chunk-length. Default value is {@code -1} that means the chunked encoding + * mode is disabled. + */ + protected int chunkLength = -1; + + /** + * If using HTTP fixed-length streaming mode this parameter defines the + * fixed length of content. Default value is {@code -1} that means the + * fixed-length streaming mode is disabled. + */ + protected int fixedContentLength = -1; + + // 2XX: generally "OK" + // 3XX: relocation/redirect + // 4XX: client error + // 5XX: server error + /** + * Numeric status code, 202: Accepted + */ + public static final int HTTP_ACCEPTED = 202; + + /** + * Numeric status code, 502: Bad Gateway + */ + public static final int HTTP_BAD_GATEWAY = 502; + + /** + * Numeric status code, 405: Bad Method + */ + public static final int HTTP_BAD_METHOD = 405; + + /** + * Numeric status code, 400: Bad Request + */ + public static final int HTTP_BAD_REQUEST = 400; + + /** + * Numeric status code, 408: Client Timeout + */ + public static final int HTTP_CLIENT_TIMEOUT = 408; + + /** + * Numeric status code, 409: Conflict + */ + public static final int HTTP_CONFLICT = 409; + + /** + * Numeric status code, 201: Created + */ + public static final int HTTP_CREATED = 201; + + /** + * Numeric status code, 413: Entity too large + */ + public static final int HTTP_ENTITY_TOO_LARGE = 413; + + /** + * Numeric status code, 403: Forbidden + */ + public static final int HTTP_FORBIDDEN = 403; + + /** + * Numeric status code, 504: Gateway timeout + */ + public static final int HTTP_GATEWAY_TIMEOUT = 504; + + /** + * Numeric status code, 410: Gone + */ + public static final int HTTP_GONE = 410; + + /** + * Numeric status code, 500: Internal error + */ + public static final int HTTP_INTERNAL_ERROR = 500; + + /** + * Numeric status code, 411: Length required + */ + public static final int HTTP_LENGTH_REQUIRED = 411; + + /** + * Numeric status code, 301 Moved permanently + */ + public static final int HTTP_MOVED_PERM = 301; + + /** + * Numeric status code, 302: Moved temporarily + */ + public static final int HTTP_MOVED_TEMP = 302; + + /** + * Numeric status code, 300: Multiple choices + */ + public static final int HTTP_MULT_CHOICE = 300; + + /** + * Numeric status code, 204: No content + */ + public static final int HTTP_NO_CONTENT = 204; + + /** + * Numeric status code, 406: Not acceptable + */ + public static final int HTTP_NOT_ACCEPTABLE = 406; + + /** + * Numeric status code, 203: Not authoritative + */ + public static final int HTTP_NOT_AUTHORITATIVE = 203; + + /** + * Numeric status code, 404: Not found + */ + public static final int HTTP_NOT_FOUND = 404; + + /** + * Numeric status code, 501: Not implemented + */ + public static final int HTTP_NOT_IMPLEMENTED = 501; + + /** + * Numeric status code, 304: Not modified + */ + public static final int HTTP_NOT_MODIFIED = 304; + + /** + * Numeric status code, 200: OK + */ + public static final int HTTP_OK = 200; + + /** + * Numeric status code, 206: Partial + */ + public static final int HTTP_PARTIAL = 206; + + /** + * Numeric status code, 402: Payment required + */ + public static final int HTTP_PAYMENT_REQUIRED = 402; + + /** + * Numeric status code, 412: Precondition failed + */ + public static final int HTTP_PRECON_FAILED = 412; + + /** + * Numeric status code, 407: Proxy authentication required + */ + public static final int HTTP_PROXY_AUTH = 407; + + /** + * Numeric status code, 414: Request too long + */ + public static final int HTTP_REQ_TOO_LONG = 414; + + /** + * Numeric status code, 205: Reset + */ + public static final int HTTP_RESET = 205; + + /** + * Numeric status code, 303: See other + */ + public static final int HTTP_SEE_OTHER = 303; + + /** + * Numeric status code, 500: Internal error + * + * @deprecated Use {@link #HTTP_INTERNAL_ERROR} + */ + @Deprecated + public static final int HTTP_SERVER_ERROR = 500; + + /** + * Numeric status code, 305: Use proxy. + * + *

    Like Firefox and Chrome, this class doesn't honor this response code. + * Other implementations respond to this status code by retrying the request + * using the HTTP proxy named by the response's Location header field. + */ + public static final int HTTP_USE_PROXY = 305; + + /** + * Numeric status code, 401: Unauthorized + */ + public static final int HTTP_UNAUTHORIZED = 401; + + /** + * Numeric status code, 415: Unsupported type + */ + public static final int HTTP_UNSUPPORTED_TYPE = 415; + + /** + * Numeric status code, 503: Unavailable + */ + public static final int HTTP_UNAVAILABLE = 503; + + /** + * Numeric status code, 505: Version not supported + */ + public static final int HTTP_VERSION = 505; + + public static OkHttpConnection open(URL url) { + return new libcore.net.http.HttpURLConnectionImpl(url, 443); + } + + public static OkHttpConnection open(URL url, Proxy proxy) { + return new libcore.net.http.HttpURLConnectionImpl(url, 443, proxy); + } + + /** + * Constructs a new {@code HttpURLConnection} instance pointing to the + * resource specified by the {@code url}. + * + * @param url + * the URL of this connection. + * @see java.net.URL + * @see java.net.URLConnection + */ + protected OkHttpConnection(URL url) { + super(url); + } + + /** + * Releases this connection so that its resources may be either reused or + * closed. + * + *

    Unlike other Java implementations, this will not necessarily close + * socket connections that can be reused. You can disable all connection + * reuse by setting the {@code http.keepAlive} system property to {@code + * false} before issuing any HTTP requests. + */ + public abstract void disconnect(); + + /** + * Returns an input stream from the server in the case of an error such as + * the requested file has not been found on the remote server. This stream + * can be used to read the data the server will send back. + * + * @return the error input stream returned by the server. + */ + public InputStream getErrorStream() { + return null; + } + + /** + * Returns the value of {@code followRedirects} which indicates if this + * connection follows a different URL redirected by the server. It is + * enabled by default. + * + * @return the value of the flag. + * @see #setFollowRedirects + */ + public static boolean getFollowRedirects() { + return followRedirects; + } + + /** + * Returns the permission object (in this case {@code SocketPermission}) + * with the host and the port number as the target name and {@code + * "resolve, connect"} as the action list. If the port number of this URL + * instance is lower than {@code 0} the port will be set to {@code 80}. + * + * @return the permission object required for this connection. + * @throws java.io.IOException + * if an IO exception occurs during the creation of the + * permission object. + */ + @Override + public java.security.Permission getPermission() throws IOException { + int port = url.getPort(); + if (port < 0) { + port = 80; + } + return new SocketPermission(url.getHost() + ":" + port, + "connect, resolve"); + } + + /** + * Returns the request method which will be used to make the request to the + * remote HTTP server. All possible methods of this HTTP implementation is + * listed in the class definition. + * + * @return the request method string. + * @see #method + * @see #setRequestMethod + */ + public String getRequestMethod() { + return method; + } + + /** + * Returns the response code returned by the remote HTTP server. + * + * @return the response code, -1 if no valid response code. + * @throws java.io.IOException + * if there is an IO error during the retrieval. + * @see #getResponseMessage + */ + public int getResponseCode() throws IOException { + // Call getInputStream() first since getHeaderField() doesn't return + // exceptions + getInputStream(); + String response = getHeaderField(0); + if (response == null) { + return -1; + } + response = response.trim(); + int mark = response.indexOf(" ") + 1; + if (mark == 0) { + return -1; + } + int last = mark + 3; + if (last > response.length()) { + last = response.length(); + } + responseCode = Integer.parseInt(response.substring(mark, last)); + if (last + 1 <= response.length()) { + responseMessage = response.substring(last + 1); + } + return responseCode; + } + + /** + * Returns the response message returned by the remote HTTP server. + * + * @return the response message. {@code null} if no such response exists. + * @throws java.io.IOException + * if there is an error during the retrieval. + * @see #getResponseCode() + */ + public String getResponseMessage() throws IOException { + if (responseMessage != null) { + return responseMessage; + } + getResponseCode(); + return responseMessage; + } + + /** + * Sets the flag of whether this connection will follow redirects returned + * by the remote server. + * + * @param auto + * the value to enable or disable this option. + */ + public static void setFollowRedirects(boolean auto) { + followRedirects = auto; + } + + /** + * Sets the request command which will be sent to the remote HTTP server. + * This method can only be called before the connection is made. + * + * @param method + * the string representing the method to be used. + * @throws java.net.ProtocolException + * if this is called after connected, or the method is not + * supported by this HTTP implementation. + * @see #getRequestMethod() + * @see #method + */ + public void setRequestMethod(String method) throws ProtocolException { + if (connected) { + throw new ProtocolException("Connection already established"); + } + for (String permittedUserMethod : PERMITTED_USER_METHODS) { + if (permittedUserMethod.equals(method)) { + // if there is a supported method that matches the desired + // method, then set the current method and return + this.method = permittedUserMethod; + return; + } + } + // if none matches, then throw ProtocolException + throw new ProtocolException("Unknown method '" + method + "'; must be one of " + + Arrays.toString(PERMITTED_USER_METHODS)); + } + + /** + * Returns whether this connection uses a proxy server or not. + * + * @return {@code true} if this connection passes a proxy server, false + * otherwise. + */ + public abstract boolean usingProxy(); + + /** + * Returns the encoding used to transmit the response body over the network. + * This is null or "identity" if the content was not encoded, or "gzip" if + * the body was gzip compressed. Most callers will be more interested in the + * {@link #getContentType() content type}, which may also include the + * content's character encoding. + */ + @Override public String getContentEncoding() { + return super.getContentEncoding(); // overridden for Javadoc only + } + + /** + * Returns whether this connection follows redirects. + * + * @return {@code true} if this connection follows redirects, false + * otherwise. + */ + public boolean getInstanceFollowRedirects() { + return instanceFollowRedirects; + } + + /** + * Sets whether this connection follows redirects. + * + * @param followRedirects + * {@code true} if this connection will follows redirects, false + * otherwise. + */ + public void setInstanceFollowRedirects(boolean followRedirects) { + instanceFollowRedirects = followRedirects; + } + + /** + * Returns the date value in milliseconds since {@code 01.01.1970, 00:00h} + * corresponding to the header field {@code field}. The {@code defaultValue} + * will be returned if no such field can be found in the response header. + * + * @param field + * the header field name. + * @param defaultValue + * the default value to use if the specified header field wont be + * found. + * @return the header field represented in milliseconds since January 1, + * 1970 GMT. + */ + @Override + public long getHeaderFieldDate(String field, long defaultValue) { + return super.getHeaderFieldDate(field, defaultValue); + } + + /** + * If the length of a HTTP request body is known ahead, sets fixed length to + * enable streaming without buffering. Sets after connection will cause an + * exception. + * + * @see #setChunkedStreamingMode + * @param contentLength + * the fixed length of the HTTP request body. + * @throws IllegalStateException + * if already connected or another mode already set. + * @throws IllegalArgumentException + * if {@code contentLength} is less than zero. + */ + public void setFixedLengthStreamingMode(int contentLength) { + if (super.connected) { + throw new IllegalStateException("Already connected"); + } + if (chunkLength > 0) { + throw new IllegalStateException("Already in chunked mode"); + } + if (contentLength < 0) { + throw new IllegalArgumentException("contentLength < 0"); + } + this.fixedContentLength = contentLength; + } + + /** + * Stream a request body whose length is not known in advance. Old HTTP/1.0 + * only servers may not support this mode. + * + *

    When HTTP chunked encoding is used, the stream is divided into + * chunks, each prefixed with a header containing the chunk's size. Setting + * a large chunk length requires a large internal buffer, potentially + * wasting memory. Setting a small chunk length increases the number of + * bytes that must be transmitted because of the header on every chunk. + * Most caller should use {@code 0} to get the system default. + * + * @see #setFixedLengthStreamingMode + * @param chunkLength the length to use, or {@code 0} for the default chunk + * length. + * @throws IllegalStateException if already connected or another mode + * already set. + */ + public void setChunkedStreamingMode(int chunkLength) { + if (super.connected) { + throw new IllegalStateException("Already connected"); + } + if (fixedContentLength >= 0) { + throw new IllegalStateException("Already in fixed-length mode"); + } + if (chunkLength <= 0) { + this.chunkLength = HttpEngine.DEFAULT_CHUNK_LENGTH; + } else { + this.chunkLength = chunkLength; + } + } +} diff --git a/src/main/java/com/squareup/okhttp/OkHttpsConnection.java b/src/main/java/com/squareup/okhttp/OkHttpsConnection.java new file mode 100644 index 000000000000..7c7540c325eb --- /dev/null +++ b/src/main/java/com/squareup/okhttp/OkHttpsConnection.java @@ -0,0 +1,309 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.squareup.okhttp; + +import java.net.Proxy; +import java.net.URL; +import java.security.Principal; +import java.security.cert.Certificate; +import java.security.cert.X509Certificate; +import javax.net.ssl.HostnameVerifier; +import javax.net.ssl.HttpsURLConnection; +import javax.net.ssl.SSLPeerUnverifiedException; +import javax.net.ssl.SSLSocketFactory; + +/** + * An {@link java.net.HttpURLConnection} for HTTPS (RFC 2818). A + * connected {@code HttpsURLConnection} allows access to the + * negotiated cipher suite, the server certificate chain, and the + * client certificate chain if any. + * + *

    Providing an application specific X509TrustManager

    + * + * If an application wants to trust Certificate Authority (CA) + * certificates that are not part of the system, it should specify its + * own {@code X509TrustManager} via a {@code SSLSocketFactory} set on + * the {@code HttpsURLConnection}. The {@code X509TrustManager} can be + * created based on a {@code KeyStore} using a {@code + * TrustManagerFactory} to supply trusted CA certificates. Note that + * self-signed certificates are effectively their own CA and can be + * trusted by including them in a {@code KeyStore}. + * + *

    For example, to trust a set of certificates specified by a {@code KeyStore}: + *

       {@code
    + *   KeyStore keyStore = ...;
    + *   TrustManagerFactory tmf = TrustManagerFactory.getInstance("X509");
    + *   tmf.init(keyStore);
    + *
    + *   SSLContext context = SSLContext.getInstance("TLS");
    + *   context.init(null, tmf.getTrustManagers(), null);
    + *
    + *   URL url = new URL("https://www.example.com/");
    + *   HttpsURLConnection urlConnection = (HttpsURLConnection) url.openConnection();
    + *   urlConnection.setSSLSocketFactory(context.getSocketFactory());
    + *   InputStream in = urlConnection.getInputStream();
    + * }
    + * + *

    It is possible to implement {@code X509TrustManager} directly + * instead of using one created by a {@code + * TrustManagerFactory}. While this is straightforward in the insecure + * case of allowing all certificate chains to pass verification, + * writing a proper implementation will usually want to take advantage + * of {@link java.security.cert.CertPathValidator + * CertPathValidator}. In general, it might be better to write a + * custom {@code KeyStore} implementation to pass to the {@code + * TrustManagerFactory} than to try and write a custom {@code + * X509TrustManager}. + * + *

    Providing an application specific X509KeyManager

    + * + * A custom {@code X509KeyManager} can be used to supply a client + * certificate and its associated private key to authenticate a + * connection to the server. The {@code X509KeyManager} can be created + * based on a {@code KeyStore} using a {@code KeyManagerFactory}. + * + *

    For example, to supply client certificates from a {@code KeyStore}: + *

       {@code
    + *   KeyStore keyStore = ...;
    + *   KeyManagerFactory kmf = KeyManagerFactory.getInstance("X509");
    + *   kmf.init(keyStore);
    + *
    + *   SSLContext context = SSLContext.getInstance("TLS");
    + *   context.init(kmf.getKeyManagers(), null, null);
    + *
    + *   URL url = new URL("https://www.example.com/");
    + *   HttpsURLConnection urlConnection = (HttpsURLConnection) url.openConnection();
    + *   urlConnection.setSSLSocketFactory(context.getSocketFactory());
    + *   InputStream in = urlConnection.getInputStream();
    + * }
    + * + *

    A {@code X509KeyManager} can also be implemented directly. This + * can allow an application to return a certificate and private key + * from a non-{@code KeyStore} source or to specify its own logic for + * selecting a specific credential to use when many may be present in + * a single {@code KeyStore}. + * + *

    TLS Intolerance Support

    + * + * This class attempts to create secure connections using common TLS + * extensions and SSL deflate compression. Should that fail, the + * connection will be retried with SSLv3 only. + */ +public abstract class OkHttpsConnection extends OkHttpConnection { + + private static HostnameVerifier defaultHostnameVerifier + = HttpsURLConnection.getDefaultHostnameVerifier(); + + private static SSLSocketFactory defaultSSLSocketFactory = (SSLSocketFactory) SSLSocketFactory + .getDefault(); + + public static OkHttpsConnection open(URL url) { + return new libcore.net.http.HttpsURLConnectionImpl(url, 443); + } + + public static OkHttpsConnection open(URL url, Proxy proxy) { + return new libcore.net.http.HttpsURLConnectionImpl(url, 443, proxy); + } + + /** + * Sets the default hostname verifier to be used by new instances. + * + * @param v + * the new default hostname verifier + * @throws IllegalArgumentException + * if the specified verifier is {@code null}. + */ + public static void setDefaultHostnameVerifier(HostnameVerifier v) { + if (v == null) { + throw new IllegalArgumentException("HostnameVerifier is null"); + } + defaultHostnameVerifier = v; + } + + /** + * Returns the default hostname verifier. + * + * @return the default hostname verifier. + */ + public static HostnameVerifier getDefaultHostnameVerifier() { + return defaultHostnameVerifier; + } + + /** + * Sets the default SSL socket factory to be used by new instances. + * + * @param sf + * the new default SSL socket factory. + * @throws IllegalArgumentException + * if the specified socket factory is {@code null}. + */ + public static void setDefaultSSLSocketFactory(SSLSocketFactory sf) { + if (sf == null) { + throw new IllegalArgumentException("SSLSocketFactory is null"); + } + defaultSSLSocketFactory = sf; + } + + /** + * Returns the default SSL socket factory for new instances. + * + * @return the default SSL socket factory for new instances. + */ + public static SSLSocketFactory getDefaultSSLSocketFactory() { + return defaultSSLSocketFactory; + } + + /** + * The host name verifier used by this connection. It is initialized from + * the default hostname verifier + * {@link #setDefaultHostnameVerifier(javax.net.ssl.HostnameVerifier)} or + * {@link #getDefaultHostnameVerifier()}. + */ + protected HostnameVerifier hostnameVerifier; + + private SSLSocketFactory sslSocketFactory; + + /** + * Creates a new {@code HttpsURLConnection} with the specified {@code URL}. + * + * @param url + * the {@code URL} to connect to. + */ + protected OkHttpsConnection(URL url) { + super(url); + hostnameVerifier = defaultHostnameVerifier; + sslSocketFactory = defaultSSLSocketFactory; + } + + /** + * Returns the name of the cipher suite negotiated during the SSL handshake. + * + * @return the name of the cipher suite negotiated during the SSL handshake. + * @throws IllegalStateException + * if no connection has been established yet. + */ + public abstract String getCipherSuite(); + + /** + * Returns the list of local certificates used during the handshake. These + * certificates were sent to the peer. + * + * @return Returns the list of certificates used during the handshake with + * the local identity certificate followed by CAs, or {@code null} + * if no certificates were used during the handshake. + * @throws IllegalStateException + * if no connection has been established yet. + */ + public abstract Certificate[] getLocalCertificates(); + + /** + * Return the list of certificates identifying the peer during the + * handshake. + * + * @return the list of certificates identifying the peer with the peer's + * identity certificate followed by CAs. + * @throws javax.net.ssl.SSLPeerUnverifiedException + * if the identity of the peer has not been verified.. + * @throws IllegalStateException + * if no connection has been established yet. + */ + public abstract Certificate[] getServerCertificates() throws SSLPeerUnverifiedException; + + /** + * Returns the {@code Principal} identifying the peer. + * + * @return the {@code Principal} identifying the peer. + * @throws javax.net.ssl.SSLPeerUnverifiedException + * if the identity of the peer has not been verified. + * @throws IllegalStateException + * if no connection has been established yet. + */ + public Principal getPeerPrincipal() throws SSLPeerUnverifiedException { + Certificate[] certs = getServerCertificates(); + if (certs == null || certs.length == 0 || (!(certs[0] instanceof X509Certificate))) { + throw new SSLPeerUnverifiedException("No server's end-entity certificate"); + } + return ((X509Certificate) certs[0]).getSubjectX500Principal(); + } + + /** + * Returns the {@code Principal} used to identify the local host during the handshake. + * + * @return the {@code Principal} used to identify the local host during the handshake, or + * {@code null} if none was used. + * @throws IllegalStateException + * if no connection has been established yet. + */ + public Principal getLocalPrincipal() { + Certificate[] certs = getLocalCertificates(); + if (certs == null || certs.length == 0 || (!(certs[0] instanceof X509Certificate))) { + return null; + } + return ((X509Certificate) certs[0]).getSubjectX500Principal(); + } + + /** + * Sets the hostname verifier for this instance. + * + * @param v + * the hostname verifier for this instance. + * @throws IllegalArgumentException + * if the specified verifier is {@code null}. + */ + public void setHostnameVerifier(HostnameVerifier v) { + if (v == null) { + throw new IllegalArgumentException("HostnameVerifier is null"); + } + hostnameVerifier = v; + } + + /** + * Returns the hostname verifier used by this instance. + * + * @return the hostname verifier used by this instance. + */ + public HostnameVerifier getHostnameVerifier() { + return hostnameVerifier; + } + + /** + * Sets the SSL socket factory for this instance. + * + * @param sf + * the SSL socket factory to be used by this instance. + * @throws IllegalArgumentException + * if the specified socket factory is {@code null}. + */ + public void setSSLSocketFactory(SSLSocketFactory sf) { + if (sf == null) { + throw new IllegalArgumentException("SSLSocketFactory is null"); + } + sslSocketFactory = sf; + } + + /** + * Returns the SSL socket factory used by this instance. + * + * @return the SSL socket factory used by this instance. + */ + public SSLSocketFactory getSSLSocketFactory() { + return sslSocketFactory; + } + +} diff --git a/src/main/java/libcore/io/AsynchronousCloseMonitor.java b/src/main/java/libcore/io/AsynchronousCloseMonitor.java new file mode 100644 index 000000000000..62eec24af9bc --- /dev/null +++ b/src/main/java/libcore/io/AsynchronousCloseMonitor.java @@ -0,0 +1,26 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package libcore.io; + +import java.io.FileDescriptor; + +public final class AsynchronousCloseMonitor { + private AsynchronousCloseMonitor() { + } + + public static native void signalBlockedThreads(FileDescriptor fd); +} diff --git a/src/main/java/libcore/io/Base64.java b/src/main/java/libcore/io/Base64.java new file mode 100644 index 000000000000..153722192c1e --- /dev/null +++ b/src/main/java/libcore/io/Base64.java @@ -0,0 +1,161 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** +* @author Alexander Y. Kleymenov +*/ + +package libcore.io; + +import libcore.util.Charsets; +import libcore.util.EmptyArray; + +/** + * Base64 encoder/decoder. + * In violation of the RFC, this encoder doesn't wrap lines at 76 columns. + */ +public final class Base64 { + private Base64() { + } + + public static byte[] decode(byte[] in) { + return decode(in, in.length); + } + + public static byte[] decode(byte[] in, int len) { + // approximate output length + int length = len / 4 * 3; + // return an empty array on empty or short input without padding + if (length == 0) { + return EmptyArray.BYTE; + } + // temporary array + byte[] out = new byte[length]; + // number of padding characters ('=') + int pad = 0; + byte chr; + // compute the number of the padding characters + // and adjust the length of the input + for (;;len--) { + chr = in[len-1]; + // skip the neutral characters + if ((chr == '\n') || (chr == '\r') || + (chr == ' ') || (chr == '\t')) { + continue; + } + if (chr == '=') { + pad++; + } else { + break; + } + } + // index in the output array + int outIndex = 0; + // index in the input array + int inIndex = 0; + // holds the value of the input character + int bits = 0; + // holds the value of the input quantum + int quantum = 0; + for (int i=0; i= 'A') && (chr <= 'Z')) { + // char ASCII value + // A 65 0 + // Z 90 25 (ASCII - 65) + bits = chr - 65; + } else if ((chr >= 'a') && (chr <= 'z')) { + // char ASCII value + // a 97 26 + // z 122 51 (ASCII - 71) + bits = chr - 71; + } else if ((chr >= '0') && (chr <= '9')) { + // char ASCII value + // 0 48 52 + // 9 57 61 (ASCII + 4) + bits = chr + 4; + } else if (chr == '+') { + bits = 62; + } else if (chr == '/') { + bits = 63; + } else { + return null; + } + // append the value to the quantum + quantum = (quantum << 6) | (byte) bits; + if (inIndex%4 == 3) { + // 4 characters were read, so make the output: + out[outIndex++] = (byte) (quantum >> 16); + out[outIndex++] = (byte) (quantum >> 8); + out[outIndex++] = (byte) quantum; + } + inIndex++; + } + if (pad > 0) { + // adjust the quantum value according to the padding + quantum = quantum << (6*pad); + // make output + out[outIndex++] = (byte) (quantum >> 16); + if (pad == 1) { + out[outIndex++] = (byte) (quantum >> 8); + } + } + // create the resulting array + byte[] result = new byte[outIndex]; + System.arraycopy(out, 0, result, 0, outIndex); + return result; + } + + private static final byte[] map = new byte[] + {'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', + 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', + 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', + 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '0', '1', '2', '3', + '4', '5', '6', '7', '8', '9', '+', '/'}; + + public static String encode(byte[] in) { + int length = (in.length + 2) * 4 / 3; + byte[] out = new byte[length]; + int index = 0, end = in.length - in.length % 3; + for (int i = 0; i < end; i += 3) { + out[index++] = map[(in[i] & 0xff) >> 2]; + out[index++] = map[((in[i] & 0x03) << 4) | ((in[i+1] & 0xff) >> 4)]; + out[index++] = map[((in[i+1] & 0x0f) << 2) | ((in[i+2] & 0xff) >> 6)]; + out[index++] = map[(in[i+2] & 0x3f)]; + } + switch (in.length % 3) { + case 1: + out[index++] = map[(in[end] & 0xff) >> 2]; + out[index++] = map[(in[end] & 0x03) << 4]; + out[index++] = '='; + out[index++] = '='; + break; + case 2: + out[index++] = map[(in[end] & 0xff) >> 2]; + out[index++] = map[((in[end] & 0x03) << 4) | ((in[end+1] & 0xff) >> 4)]; + out[index++] = map[((in[end+1] & 0x0f) << 2)]; + out[index++] = '='; + break; + } + return new String(out, 0, index, Charsets.US_ASCII); + } +} diff --git a/src/main/java/libcore/io/BufferIterator.java b/src/main/java/libcore/io/BufferIterator.java new file mode 100644 index 000000000000..7f3ad472bcd0 --- /dev/null +++ b/src/main/java/libcore/io/BufferIterator.java @@ -0,0 +1,62 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package libcore.io; + +/** + * Iterates over big- or little-endian bytes. See {@link MemoryMappedFile#bigEndianIterator} and + * {@link MemoryMappedFile#littleEndianIterator}. + * + * @hide don't make this public without adding bounds checking. + */ +public abstract class BufferIterator { + /** + * Seeks to the absolute position {@code offset}, measured in bytes from the start. + */ + public abstract void seek(int offset); + + /** + * Skips forwards or backwards {@code byteCount} bytes from the current position. + */ + public abstract void skip(int byteCount); + + /** + * Copies {@code byteCount} bytes from the current position into {@code dst}, starting at + * {@code dstOffset}, and advances the current position {@code byteCount} bytes. + */ + public abstract void readByteArray(byte[] dst, int dstOffset, int byteCount); + + /** + * Returns the byte at the current position, and advances the current position one byte. + */ + public abstract byte readByte(); + + /** + * Returns the 32-bit int at the current position, and advances the current position four bytes. + */ + public abstract int readInt(); + + /** + * Copies {@code intCount} 32-bit ints from the current position into {@code dst}, starting at + * {@code dstOffset}, and advances the current position {@code 4 * intCount} bytes. + */ + public abstract void readIntArray(int[] dst, int dstOffset, int intCount); + + /** + * Returns the 16-bit short at the current position, and advances the current position two bytes. + */ + public abstract short readShort(); +} diff --git a/src/main/java/libcore/io/DiskLruCache.java b/src/main/java/libcore/io/DiskLruCache.java new file mode 100644 index 000000000000..b6c3638cdeb0 --- /dev/null +++ b/src/main/java/libcore/io/DiskLruCache.java @@ -0,0 +1,834 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package libcore.io; + +import java.io.BufferedInputStream; +import java.io.BufferedWriter; +import java.io.Closeable; +import java.io.EOFException; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.FileWriter; +import java.io.FilterOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.io.Writer; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; +import libcore.util.Charsets; +import libcore.util.Libcore; + +/** + * A cache that uses a bounded amount of space on a filesystem. Each cache + * entry has a string key and a fixed number of values. Values are byte + * sequences, accessible as streams or files. Each value must be between {@code + * 0} and {@code Integer.MAX_VALUE} bytes in length. + * + *

    The cache stores its data in a directory on the filesystem. This + * directory must be exclusive to the cache; the cache may delete or overwrite + * files from its directory. It is an error for multiple processes to use the + * same cache directory at the same time. + * + *

    This cache limits the number of bytes that it will store on the + * filesystem. When the number of stored bytes exceeds the limit, the cache will + * remove entries in the background until the limit is satisfied. The limit is + * not strict: the cache may temporarily exceed it while waiting for files to be + * deleted. The limit does not include filesystem overhead or the cache + * journal so space-sensitive applications should set a conservative limit. + * + *

    Clients call {@link #edit} to create or update the values of an entry. An + * entry may have only one editor at one time; if a value is not available to be + * edited then {@link #edit} will return null. + *

    + * Every {@link #edit} call must be matched by a call to {@link Editor#commit} + * or {@link Editor#abort}. Committing is atomic: a read observes the full set + * of values as they were before or after the commit, but never a mix of values. + * + *

    Clients call {@link #get} to read a snapshot of an entry. The read will + * observe the value at the time that {@link #get} was called. Updates and + * removals after the call do not impact ongoing reads. + * + *

    This class is tolerant of some I/O errors. If files are missing from the + * filesystem, the corresponding entries will be dropped from the cache. If + * an error occurs while writing a cache value, the edit will fail silently. + * Callers should handle other problems by catching {@code IOException} and + * responding appropriately. + */ +public final class DiskLruCache implements Closeable { + static final String JOURNAL_FILE = "journal"; + static final String JOURNAL_FILE_TMP = "journal.tmp"; + static final String MAGIC = "libcore.io.DiskLruCache"; + static final String VERSION_1 = "1"; + static final long ANY_SEQUENCE_NUMBER = -1; + private static final String CLEAN = "CLEAN"; + private static final String DIRTY = "DIRTY"; + private static final String REMOVE = "REMOVE"; + private static final String READ = "READ"; + + /* + * This cache uses a journal file named "journal". A typical journal file + * looks like this: + * libcore.io.DiskLruCache + * 1 + * 100 + * 2 + * + * CLEAN 3400330d1dfc7f3f7f4b8d4d803dfcf6 832 21054 + * DIRTY 335c4c6028171cfddfbaae1a9c313c52 + * CLEAN 335c4c6028171cfddfbaae1a9c313c52 3934 2342 + * REMOVE 335c4c6028171cfddfbaae1a9c313c52 + * DIRTY 1ab96a171faeeee38496d8b330771a7a + * CLEAN 1ab96a171faeeee38496d8b330771a7a 1600 234 + * READ 335c4c6028171cfddfbaae1a9c313c52 + * READ 3400330d1dfc7f3f7f4b8d4d803dfcf6 + * + * The first five lines of the journal form its header. They are the + * constant string "libcore.io.DiskLruCache", the disk cache's version, + * the application's version, the value count, and a blank line. + * + * Each of the subsequent lines in the file is a record of the state of a + * cache entry. Each line contains space-separated values: a state, a key, + * and optional state-specific values. + * o DIRTY lines track that an entry is actively being created or updated. + * Every successful DIRTY action should be followed by a CLEAN or REMOVE + * action. DIRTY lines without a matching CLEAN or REMOVE indicate that + * temporary files may need to be deleted. + * o CLEAN lines track a cache entry that has been successfully published + * and may be read. A publish line is followed by the lengths of each of + * its values. + * o READ lines track accesses for LRU. + * o REMOVE lines track entries that have been deleted. + * + * The journal file is appended to as cache operations occur. The journal may + * occasionally be compacted by dropping redundant lines. A temporary file named + * "journal.tmp" will be used during compaction; that file should be deleted if + * it exists when the cache is opened. + */ + + private final File directory; + private final File journalFile; + private final File journalFileTmp; + private final int appVersion; + private final long maxSize; + private final int valueCount; + private long size = 0; + private Writer journalWriter; + private final LinkedHashMap lruEntries + = new LinkedHashMap(0, 0.75f, true); + private int redundantOpCount; + + /** + * To differentiate between old and current snapshots, each entry is given + * a sequence number each time an edit is committed. A snapshot is stale if + * its sequence number is not equal to its entry's sequence number. + */ + private long nextSequenceNumber = 0; + + /** This cache uses a single background thread to evict entries. */ + private final ExecutorService executorService = new ThreadPoolExecutor(0, 1, + 60L, TimeUnit.SECONDS, new LinkedBlockingQueue()); + private final Callable cleanupCallable = new Callable() { + @Override public Void call() throws Exception { + synchronized (DiskLruCache.this) { + if (journalWriter == null) { + return null; // closed + } + trimToSize(); + if (journalRebuildRequired()) { + rebuildJournal(); + redundantOpCount = 0; + } + } + return null; + } + }; + + private DiskLruCache(File directory, int appVersion, int valueCount, long maxSize) { + this.directory = directory; + this.appVersion = appVersion; + this.journalFile = new File(directory, JOURNAL_FILE); + this.journalFileTmp = new File(directory, JOURNAL_FILE_TMP); + this.valueCount = valueCount; + this.maxSize = maxSize; + } + + /** + * Opens the cache in {@code directory}, creating a cache if none exists + * there. + * + * @param directory a writable directory + * @param appVersion + * @param valueCount the number of values per cache entry. Must be positive. + * @param maxSize the maximum number of bytes this cache should use to store + * @throws IOException if reading or writing the cache directory fails + */ + public static DiskLruCache open(File directory, int appVersion, int valueCount, long maxSize) + throws IOException { + if (maxSize <= 0) { + throw new IllegalArgumentException("maxSize <= 0"); + } + if (valueCount <= 0) { + throw new IllegalArgumentException("valueCount <= 0"); + } + + // prefer to pick up where we left off + DiskLruCache cache = new DiskLruCache(directory, appVersion, valueCount, maxSize); + if (cache.journalFile.exists()) { + try { + cache.readJournal(); + cache.processJournal(); + cache.journalWriter = new BufferedWriter(new FileWriter(cache.journalFile, true)); + return cache; + } catch (IOException journalIsCorrupt) { + Libcore.logW("DiskLruCache " + directory + " is corrupt: " + + journalIsCorrupt.getMessage() + ", removing"); + cache.delete(); + } + } + + // create a new empty cache + directory.mkdirs(); + cache = new DiskLruCache(directory, appVersion, valueCount, maxSize); + cache.rebuildJournal(); + return cache; + } + + private void readJournal() throws IOException { + InputStream in = new BufferedInputStream(new FileInputStream(journalFile)); + try { + String magic = Streams.readAsciiLine(in); + String version = Streams.readAsciiLine(in); + String appVersionString = Streams.readAsciiLine(in); + String valueCountString = Streams.readAsciiLine(in); + String blank = Streams.readAsciiLine(in); + if (!MAGIC.equals(magic) + || !VERSION_1.equals(version) + || !Integer.toString(appVersion).equals(appVersionString) + || !Integer.toString(valueCount).equals(valueCountString) + || !"".equals(blank)) { + throw new IOException("unexpected journal header: [" + + magic + ", " + version + ", " + valueCountString + ", " + blank + "]"); + } + + while (true) { + try { + readJournalLine(Streams.readAsciiLine(in)); + } catch (EOFException endOfJournal) { + break; + } + } + } finally { + IoUtils.closeQuietly(in); + } + } + + private void readJournalLine(String line) throws IOException { + String[] parts = line.split(" "); + if (parts.length < 2) { + throw new IOException("unexpected journal line: " + line); + } + + String key = parts[1]; + if (parts[0].equals(REMOVE) && parts.length == 2) { + lruEntries.remove(key); + return; + } + + Entry entry = lruEntries.get(key); + if (entry == null) { + entry = new Entry(key); + lruEntries.put(key, entry); + } + + if (parts[0].equals(CLEAN) && parts.length == 2 + valueCount) { + entry.readable = true; + entry.currentEditor = null; + entry.setLengths(Arrays.copyOfRange(parts, 2, parts.length)); + } else if (parts[0].equals(DIRTY) && parts.length == 2) { + entry.currentEditor = new Editor(entry); + } else if (parts[0].equals(READ) && parts.length == 2) { + // this work was already done by calling lruEntries.get() + } else { + throw new IOException("unexpected journal line: " + line); + } + } + + /** + * Computes the initial size and collects garbage as a part of opening the + * cache. Dirty entries are assumed to be inconsistent and will be deleted. + */ + private void processJournal() throws IOException { + deleteIfExists(journalFileTmp); + for (Iterator i = lruEntries.values().iterator(); i.hasNext(); ) { + Entry entry = i.next(); + if (entry.currentEditor == null) { + for (int t = 0; t < valueCount; t++) { + size += entry.lengths[t]; + } + } else { + entry.currentEditor = null; + for (int t = 0; t < valueCount; t++) { + deleteIfExists(entry.getCleanFile(t)); + deleteIfExists(entry.getDirtyFile(t)); + } + i.remove(); + } + } + } + + /** + * Creates a new journal that omits redundant information. This replaces the + * current journal if it exists. + */ + private synchronized void rebuildJournal() throws IOException { + if (journalWriter != null) { + journalWriter.close(); + } + + Writer writer = new BufferedWriter(new FileWriter(journalFileTmp)); + writer.write(MAGIC); + writer.write("\n"); + writer.write(VERSION_1); + writer.write("\n"); + writer.write(Integer.toString(appVersion)); + writer.write("\n"); + writer.write(Integer.toString(valueCount)); + writer.write("\n"); + writer.write("\n"); + + for (Entry entry : lruEntries.values()) { + if (entry.currentEditor != null) { + writer.write(DIRTY + ' ' + entry.key + '\n'); + } else { + writer.write(CLEAN + ' ' + entry.key + entry.getLengths() + '\n'); + } + } + + writer.close(); + journalFileTmp.renameTo(journalFile); + journalWriter = new BufferedWriter(new FileWriter(journalFile, true)); + } + + private static void deleteIfExists(File file) throws IOException { + Libcore.deleteIfExists(file); + } + + /** + * Returns a snapshot of the entry named {@code key}, or null if it doesn't + * exist is not currently readable. If a value is returned, it is moved to + * the head of the LRU queue. + */ + public synchronized Snapshot get(String key) throws IOException { + checkNotClosed(); + validateKey(key); + Entry entry = lruEntries.get(key); + if (entry == null) { + return null; + } + + if (!entry.readable) { + return null; + } + + /* + * Open all streams eagerly to guarantee that we see a single published + * snapshot. If we opened streams lazily then the streams could come + * from different edits. + */ + InputStream[] ins = new InputStream[valueCount]; + try { + for (int i = 0; i < valueCount; i++) { + ins[i] = new FileInputStream(entry.getCleanFile(i)); + } + } catch (FileNotFoundException e) { + // a file must have been deleted manually! + return null; + } + + redundantOpCount++; + journalWriter.append(READ + ' ' + key + '\n'); + if (journalRebuildRequired()) { + executorService.submit(cleanupCallable); + } + + return new Snapshot(key, entry.sequenceNumber, ins); + } + + /** + * Returns an editor for the entry named {@code key}, or null if another + * edit is in progress. + */ + public Editor edit(String key) throws IOException { + return edit(key, ANY_SEQUENCE_NUMBER); + } + + private synchronized Editor edit(String key, long expectedSequenceNumber) throws IOException { + checkNotClosed(); + validateKey(key); + Entry entry = lruEntries.get(key); + if (expectedSequenceNumber != ANY_SEQUENCE_NUMBER + && (entry == null || entry.sequenceNumber != expectedSequenceNumber)) { + return null; // snapshot is stale + } + if (entry == null) { + entry = new Entry(key); + lruEntries.put(key, entry); + } else if (entry.currentEditor != null) { + return null; // another edit is in progress + } + + Editor editor = new Editor(entry); + entry.currentEditor = editor; + + // flush the journal before creating files to prevent file leaks + journalWriter.write(DIRTY + ' ' + key + '\n'); + journalWriter.flush(); + return editor; + } + + /** + * Returns the directory where this cache stores its data. + */ + public File getDirectory() { + return directory; + } + + /** + * Returns the maximum number of bytes that this cache should use to store + * its data. + */ + public long maxSize() { + return maxSize; + } + + /** + * Returns the number of bytes currently being used to store the values in + * this cache. This may be greater than the max size if a background + * deletion is pending. + */ + public synchronized long size() { + return size; + } + + private synchronized void completeEdit(Editor editor, boolean success) throws IOException { + Entry entry = editor.entry; + if (entry.currentEditor != editor) { + throw new IllegalStateException(); + } + + // if this edit is creating the entry for the first time, every index must have a value + if (success && !entry.readable) { + for (int i = 0; i < valueCount; i++) { + if (!entry.getDirtyFile(i).exists()) { + editor.abort(); + throw new IllegalStateException("edit didn't create file " + i); + } + } + } + + for (int i = 0; i < valueCount; i++) { + File dirty = entry.getDirtyFile(i); + if (success) { + if (dirty.exists()) { + File clean = entry.getCleanFile(i); + dirty.renameTo(clean); + long oldLength = entry.lengths[i]; + long newLength = clean.length(); + entry.lengths[i] = newLength; + size = size - oldLength + newLength; + } + } else { + deleteIfExists(dirty); + } + } + + redundantOpCount++; + entry.currentEditor = null; + if (entry.readable | success) { + entry.readable = true; + journalWriter.write(CLEAN + ' ' + entry.key + entry.getLengths() + '\n'); + if (success) { + entry.sequenceNumber = nextSequenceNumber++; + } + } else { + lruEntries.remove(entry.key); + journalWriter.write(REMOVE + ' ' + entry.key + '\n'); + } + + if (size > maxSize || journalRebuildRequired()) { + executorService.submit(cleanupCallable); + } + } + + /** + * We only rebuild the journal when it will halve the size of the journal + * and eliminate at least 2000 ops. + */ + private boolean journalRebuildRequired() { + final int REDUNDANT_OP_COMPACT_THRESHOLD = 2000; + return redundantOpCount >= REDUNDANT_OP_COMPACT_THRESHOLD + && redundantOpCount >= lruEntries.size(); + } + + /** + * Drops the entry for {@code key} if it exists and can be removed. Entries + * actively being edited cannot be removed. + * + * @return true if an entry was removed. + */ + public synchronized boolean remove(String key) throws IOException { + checkNotClosed(); + validateKey(key); + Entry entry = lruEntries.get(key); + if (entry == null || entry.currentEditor != null) { + return false; + } + + for (int i = 0; i < valueCount; i++) { + File file = entry.getCleanFile(i); + if (!file.delete()) { + throw new IOException("failed to delete " + file); + } + size -= entry.lengths[i]; + entry.lengths[i] = 0; + } + + redundantOpCount++; + journalWriter.append(REMOVE + ' ' + key + '\n'); + lruEntries.remove(key); + + if (journalRebuildRequired()) { + executorService.submit(cleanupCallable); + } + + return true; + } + + /** + * Returns true if this cache has been closed. + */ + public boolean isClosed() { + return journalWriter == null; + } + + private void checkNotClosed() { + if (journalWriter == null) { + throw new IllegalStateException("cache is closed"); + } + } + + /** + * Force buffered operations to the filesystem. + */ + public synchronized void flush() throws IOException { + checkNotClosed(); + trimToSize(); + journalWriter.flush(); + } + + /** + * Closes this cache. Stored values will remain on the filesystem. + */ + public synchronized void close() throws IOException { + if (journalWriter == null) { + return; // already closed + } + for (Entry entry : new ArrayList(lruEntries.values())) { + if (entry.currentEditor != null) { + entry.currentEditor.abort(); + } + } + trimToSize(); + journalWriter.close(); + journalWriter = null; + } + + private void trimToSize() throws IOException { + while (size > maxSize) { + Map.Entry toEvict = lruEntries.entrySet().iterator().next(); + remove(toEvict.getKey()); + } + } + + /** + * Closes the cache and deletes all of its stored values. This will delete + * all files in the cache directory including files that weren't created by + * the cache. + */ + public void delete() throws IOException { + close(); + IoUtils.deleteContents(directory); + } + + private void validateKey(String key) { + if (key.contains(" ") || key.contains("\n") || key.contains("\r")) { + throw new IllegalArgumentException( + "keys must not contain spaces or newlines: \"" + key + "\""); + } + } + + private static String inputStreamToString(InputStream in) throws IOException { + return Streams.readFully(new InputStreamReader(in, Charsets.UTF_8)); + } + + /** + * A snapshot of the values for an entry. + */ + public final class Snapshot implements Closeable { + private final String key; + private final long sequenceNumber; + private final InputStream[] ins; + + private Snapshot(String key, long sequenceNumber, InputStream[] ins) { + this.key = key; + this.sequenceNumber = sequenceNumber; + this.ins = ins; + } + + /** + * Returns an editor for this snapshot's entry, or null if either the + * entry has changed since this snapshot was created or if another edit + * is in progress. + */ + public Editor edit() throws IOException { + return DiskLruCache.this.edit(key, sequenceNumber); + } + + /** + * Returns the unbuffered stream with the value for {@code index}. + */ + public InputStream getInputStream(int index) { + return ins[index]; + } + + /** + * Returns the string value for {@code index}. + */ + public String getString(int index) throws IOException { + return inputStreamToString(getInputStream(index)); + } + + @Override public void close() { + for (InputStream in : ins) { + IoUtils.closeQuietly(in); + } + } + } + + /** + * Edits the values for an entry. + */ + public final class Editor { + private final Entry entry; + private boolean hasErrors; + + private Editor(Entry entry) { + this.entry = entry; + } + + /** + * Returns an unbuffered input stream to read the last committed value, + * or null if no value has been committed. + */ + public InputStream newInputStream(int index) throws IOException { + synchronized (DiskLruCache.this) { + if (entry.currentEditor != this) { + throw new IllegalStateException(); + } + if (!entry.readable) { + return null; + } + return new FileInputStream(entry.getCleanFile(index)); + } + } + + /** + * Returns the last committed value as a string, or null if no value + * has been committed. + */ + public String getString(int index) throws IOException { + InputStream in = newInputStream(index); + return in != null ? inputStreamToString(in) : null; + } + + /** + * Returns a new unbuffered output stream to write the value at + * {@code index}. If the underlying output stream encounters errors + * when writing to the filesystem, this edit will be aborted when + * {@link #commit} is called. The returned output stream does not throw + * IOExceptions. + */ + public OutputStream newOutputStream(int index) throws IOException { + synchronized (DiskLruCache.this) { + if (entry.currentEditor != this) { + throw new IllegalStateException(); + } + return new FaultHidingOutputStream(new FileOutputStream(entry.getDirtyFile(index))); + } + } + + /** + * Sets the value at {@code index} to {@code value}. + */ + public void set(int index, String value) throws IOException { + Writer writer = null; + try { + writer = new OutputStreamWriter(newOutputStream(index), Charsets.UTF_8); + writer.write(value); + } finally { + IoUtils.closeQuietly(writer); + } + } + + /** + * Commits this edit so it is visible to readers. This releases the + * edit lock so another edit may be started on the same key. + */ + public void commit() throws IOException { + if (hasErrors) { + completeEdit(this, false); + remove(entry.key); // the previous entry is stale + } else { + completeEdit(this, true); + } + } + + /** + * Aborts this edit. This releases the edit lock so another edit may be + * started on the same key. + */ + public void abort() throws IOException { + completeEdit(this, false); + } + + private class FaultHidingOutputStream extends FilterOutputStream { + private FaultHidingOutputStream(OutputStream out) { + super(out); + } + + @Override public void write(int oneByte) { + try { + out.write(oneByte); + } catch (IOException e) { + hasErrors = true; + } + } + + @Override public void write(byte[] buffer, int offset, int length) { + try { + out.write(buffer, offset, length); + } catch (IOException e) { + hasErrors = true; + } + } + + @Override public void close() { + try { + out.close(); + } catch (IOException e) { + hasErrors = true; + } + } + + @Override public void flush() { + try { + out.flush(); + } catch (IOException e) { + hasErrors = true; + } + } + } + } + + private final class Entry { + private final String key; + + /** Lengths of this entry's files. */ + private final long[] lengths; + + /** True if this entry has ever been published */ + private boolean readable; + + /** The ongoing edit or null if this entry is not being edited. */ + private Editor currentEditor; + + /** The sequence number of the most recently committed edit to this entry. */ + private long sequenceNumber; + + private Entry(String key) { + this.key = key; + this.lengths = new long[valueCount]; + } + + public String getLengths() throws IOException { + StringBuilder result = new StringBuilder(); + for (long size : lengths) { + result.append(' ').append(size); + } + return result.toString(); + } + + /** + * Set lengths using decimal numbers like "10123". + */ + private void setLengths(String[] strings) throws IOException { + if (strings.length != valueCount) { + throw invalidLengths(strings); + } + + try { + for (int i = 0; i < strings.length; i++) { + lengths[i] = Long.parseLong(strings[i]); + } + } catch (NumberFormatException e) { + throw invalidLengths(strings); + } + } + + private IOException invalidLengths(String[] strings) throws IOException { + throw new IOException("unexpected journal line: " + Arrays.toString(strings)); + } + + public File getCleanFile(int i) { + return new File(directory, key + "." + i); + } + + public File getDirtyFile(int i) { + return new File(directory, key + "." + i + ".tmp"); + } + } +} diff --git a/src/main/java/libcore/io/IoUtils.java b/src/main/java/libcore/io/IoUtils.java new file mode 100644 index 000000000000..307737d5b5a6 --- /dev/null +++ b/src/main/java/libcore/io/IoUtils.java @@ -0,0 +1,72 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package libcore.io; + +import java.io.Closeable; +import java.io.File; +import java.io.IOException; +import java.net.Socket; + +public final class IoUtils { + private IoUtils() { + } + + /** + * Closes 'closeable', ignoring any checked exceptions. Does nothing if 'closeable' is null. + */ + public static void closeQuietly(Closeable closeable) { + if (closeable != null) { + try { + closeable.close(); + } catch (RuntimeException rethrown) { + throw rethrown; + } catch (Exception ignored) { + } + } + } + + /** + * Closes 'socket', ignoring any exceptions. Does nothing if 'socket' is null. + */ + public static void closeQuietly(Socket socket) { + if (socket != null) { + try { + socket.close(); + } catch (Exception ignored) { + } + } + } + + /** + * Recursively delete everything in {@code dir}. + */ + // TODO: this should specify paths as Strings rather than as Files + public static void deleteContents(File dir) throws IOException { + File[] files = dir.listFiles(); + if (files == null) { + throw new IllegalArgumentException("not a directory: " + dir); + } + for (File file : files) { + if (file.isDirectory()) { + deleteContents(file); + } + if (!file.delete()) { + throw new IOException("failed to delete file: " + file); + } + } + } +} diff --git a/src/main/java/libcore/io/OsConstants.java b/src/main/java/libcore/io/OsConstants.java new file mode 100644 index 000000000000..68a165c8de75 --- /dev/null +++ b/src/main/java/libcore/io/OsConstants.java @@ -0,0 +1,724 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package libcore.io; + +public final class OsConstants { + private OsConstants() { } + + public static boolean S_ISBLK(int mode) { return (mode & S_IFMT) == S_IFBLK; } + public static boolean S_ISCHR(int mode) { return (mode & S_IFMT) == S_IFCHR; } + public static boolean S_ISDIR(int mode) { return (mode & S_IFMT) == S_IFDIR; } + public static boolean S_ISFIFO(int mode) { return (mode & S_IFMT) == S_IFIFO; } + public static boolean S_ISREG(int mode) { return (mode & S_IFMT) == S_IFREG; } + public static boolean S_ISLNK(int mode) { return (mode & S_IFMT) == S_IFLNK; } + public static boolean S_ISSOCK(int mode) { return (mode & S_IFMT) == S_IFSOCK; } + + public static int WEXITSTATUS(int status) { return (status & 0xff00) >> 8; } + public static boolean WCOREDUMP(int status) { return (status & 0x80) != 0; } + public static int WTERMSIG(int status) { return status & 0x7f; } + public static int WSTOPSIG(int status) { return WEXITSTATUS(status); } + public static boolean WIFEXITED(int status) { return (WTERMSIG(status) == 0); } + public static boolean WIFSTOPPED(int status) { return (WTERMSIG(status) == 0x7f); } + public static boolean WIFSIGNALED(int status) { return (WTERMSIG(status + 1) >= 2); } + + public static final int AF_INET = placeholder(); + public static final int AF_INET6 = placeholder(); + public static final int AF_UNIX = placeholder(); + public static final int AF_UNSPEC = placeholder(); + public static final int AI_ADDRCONFIG = placeholder(); + public static final int AI_ALL = placeholder(); + public static final int AI_CANONNAME = placeholder(); + public static final int AI_NUMERICHOST = placeholder(); + public static final int AI_NUMERICSERV = placeholder(); + public static final int AI_PASSIVE = placeholder(); + public static final int AI_V4MAPPED = placeholder(); + public static final int E2BIG = placeholder(); + public static final int EACCES = placeholder(); + public static final int EADDRINUSE = placeholder(); + public static final int EADDRNOTAVAIL = placeholder(); + public static final int EAFNOSUPPORT = placeholder(); + public static final int EAGAIN = placeholder(); + public static final int EAI_AGAIN = placeholder(); + public static final int EAI_BADFLAGS = placeholder(); + public static final int EAI_FAIL = placeholder(); + public static final int EAI_FAMILY = placeholder(); + public static final int EAI_MEMORY = placeholder(); + public static final int EAI_NODATA = placeholder(); + public static final int EAI_NONAME = placeholder(); + public static final int EAI_OVERFLOW = placeholder(); + public static final int EAI_SERVICE = placeholder(); + public static final int EAI_SOCKTYPE = placeholder(); + public static final int EAI_SYSTEM = placeholder(); + public static final int EALREADY = placeholder(); + public static final int EBADF = placeholder(); + public static final int EBADMSG = placeholder(); + public static final int EBUSY = placeholder(); + public static final int ECANCELED = placeholder(); + public static final int ECHILD = placeholder(); + public static final int ECONNABORTED = placeholder(); + public static final int ECONNREFUSED = placeholder(); + public static final int ECONNRESET = placeholder(); + public static final int EDEADLK = placeholder(); + public static final int EDESTADDRREQ = placeholder(); + public static final int EDOM = placeholder(); + public static final int EDQUOT = placeholder(); + public static final int EEXIST = placeholder(); + public static final int EFAULT = placeholder(); + public static final int EFBIG = placeholder(); + public static final int EHOSTUNREACH = placeholder(); + public static final int EIDRM = placeholder(); + public static final int EILSEQ = placeholder(); + public static final int EINPROGRESS = placeholder(); + public static final int EINTR = placeholder(); + public static final int EINVAL = placeholder(); + public static final int EIO = placeholder(); + public static final int EISCONN = placeholder(); + public static final int EISDIR = placeholder(); + public static final int ELOOP = placeholder(); + public static final int EMFILE = placeholder(); + public static final int EMLINK = placeholder(); + public static final int EMSGSIZE = placeholder(); + public static final int EMULTIHOP = placeholder(); + public static final int ENAMETOOLONG = placeholder(); + public static final int ENETDOWN = placeholder(); + public static final int ENETRESET = placeholder(); + public static final int ENETUNREACH = placeholder(); + public static final int ENFILE = placeholder(); + public static final int ENOBUFS = placeholder(); + public static final int ENODATA = placeholder(); + public static final int ENODEV = placeholder(); + public static final int ENOENT = placeholder(); + public static final int ENOEXEC = placeholder(); + public static final int ENOLCK = placeholder(); + public static final int ENOLINK = placeholder(); + public static final int ENOMEM = placeholder(); + public static final int ENOMSG = placeholder(); + public static final int ENOPROTOOPT = placeholder(); + public static final int ENOSPC = placeholder(); + public static final int ENOSR = placeholder(); + public static final int ENOSTR = placeholder(); + public static final int ENOSYS = placeholder(); + public static final int ENOTCONN = placeholder(); + public static final int ENOTDIR = placeholder(); + public static final int ENOTEMPTY = placeholder(); + public static final int ENOTSOCK = placeholder(); + public static final int ENOTSUP = placeholder(); + public static final int ENOTTY = placeholder(); + public static final int ENXIO = placeholder(); + public static final int EOPNOTSUPP = placeholder(); + public static final int EOVERFLOW = placeholder(); + public static final int EPERM = placeholder(); + public static final int EPIPE = placeholder(); + public static final int EPROTO = placeholder(); + public static final int EPROTONOSUPPORT = placeholder(); + public static final int EPROTOTYPE = placeholder(); + public static final int ERANGE = placeholder(); + public static final int EROFS = placeholder(); + public static final int ESPIPE = placeholder(); + public static final int ESRCH = placeholder(); + public static final int ESTALE = placeholder(); + public static final int ETIME = placeholder(); + public static final int ETIMEDOUT = placeholder(); + public static final int ETXTBSY = placeholder(); + public static final int EWOULDBLOCK = placeholder(); + public static final int EXDEV = placeholder(); + public static final int EXIT_FAILURE = placeholder(); + public static final int EXIT_SUCCESS = placeholder(); + public static final int FD_CLOEXEC = placeholder(); + public static final int FIONREAD = placeholder(); + public static final int F_DUPFD = placeholder(); + public static final int F_GETFD = placeholder(); + public static final int F_GETFL = placeholder(); + public static final int F_GETLK = placeholder(); + public static final int F_GETLK64 = placeholder(); + public static final int F_GETOWN = placeholder(); + public static final int F_OK = placeholder(); + public static final int F_RDLCK = placeholder(); + public static final int F_SETFD = placeholder(); + public static final int F_SETFL = placeholder(); + public static final int F_SETLK = placeholder(); + public static final int F_SETLK64 = placeholder(); + public static final int F_SETLKW = placeholder(); + public static final int F_SETLKW64 = placeholder(); + public static final int F_SETOWN = placeholder(); + public static final int F_UNLCK = placeholder(); + public static final int F_WRLCK = placeholder(); + public static final int IFF_ALLMULTI = placeholder(); + public static final int IFF_AUTOMEDIA = placeholder(); + public static final int IFF_BROADCAST = placeholder(); + public static final int IFF_DEBUG = placeholder(); + public static final int IFF_DYNAMIC = placeholder(); + public static final int IFF_LOOPBACK = placeholder(); + public static final int IFF_MASTER = placeholder(); + public static final int IFF_MULTICAST = placeholder(); + public static final int IFF_NOARP = placeholder(); + public static final int IFF_NOTRAILERS = placeholder(); + public static final int IFF_POINTOPOINT = placeholder(); + public static final int IFF_PORTSEL = placeholder(); + public static final int IFF_PROMISC = placeholder(); + public static final int IFF_RUNNING = placeholder(); + public static final int IFF_SLAVE = placeholder(); + public static final int IFF_UP = placeholder(); + public static final int IPPROTO_ICMP = placeholder(); + public static final int IPPROTO_IP = placeholder(); + public static final int IPPROTO_IPV6 = placeholder(); + public static final int IPPROTO_RAW = placeholder(); + public static final int IPPROTO_TCP = placeholder(); + public static final int IPPROTO_UDP = placeholder(); + public static final int IPV6_CHECKSUM = placeholder(); + public static final int IPV6_MULTICAST_HOPS = placeholder(); + public static final int IPV6_MULTICAST_IF = placeholder(); + public static final int IPV6_MULTICAST_LOOP = placeholder(); + public static final int IPV6_RECVDSTOPTS = placeholder(); + public static final int IPV6_RECVHOPLIMIT = placeholder(); + public static final int IPV6_RECVHOPOPTS = placeholder(); + public static final int IPV6_RECVPKTINFO = placeholder(); + public static final int IPV6_RECVRTHDR = placeholder(); + public static final int IPV6_RECVTCLASS = placeholder(); + public static final int IPV6_TCLASS = placeholder(); + public static final int IPV6_UNICAST_HOPS = placeholder(); + public static final int IPV6_V6ONLY = placeholder(); + public static final int IP_MULTICAST_IF = placeholder(); + public static final int IP_MULTICAST_LOOP = placeholder(); + public static final int IP_MULTICAST_TTL = placeholder(); + public static final int IP_TOS = placeholder(); + public static final int IP_TTL = placeholder(); + public static final int MAP_FIXED = placeholder(); + public static final int MAP_PRIVATE = placeholder(); + public static final int MAP_SHARED = placeholder(); + public static final int MCAST_JOIN_GROUP = placeholder(); + public static final int MCAST_LEAVE_GROUP = placeholder(); + public static final int MCL_CURRENT = placeholder(); + public static final int MCL_FUTURE = placeholder(); + public static final int MSG_CTRUNC = placeholder(); + public static final int MSG_DONTROUTE = placeholder(); + public static final int MSG_EOR = placeholder(); + public static final int MSG_OOB = placeholder(); + public static final int MSG_PEEK = placeholder(); + public static final int MSG_TRUNC = placeholder(); + public static final int MSG_WAITALL = placeholder(); + public static final int MS_ASYNC = placeholder(); + public static final int MS_INVALIDATE = placeholder(); + public static final int MS_SYNC = placeholder(); + public static final int NI_DGRAM = placeholder(); + public static final int NI_NAMEREQD = placeholder(); + public static final int NI_NOFQDN = placeholder(); + public static final int NI_NUMERICHOST = placeholder(); + public static final int NI_NUMERICSERV = placeholder(); + public static final int O_ACCMODE = placeholder(); + public static final int O_APPEND = placeholder(); + public static final int O_CREAT = placeholder(); + public static final int O_EXCL = placeholder(); + public static final int O_NOCTTY = placeholder(); + public static final int O_NONBLOCK = placeholder(); + public static final int O_RDONLY = placeholder(); + public static final int O_RDWR = placeholder(); + public static final int O_SYNC = placeholder(); + public static final int O_TRUNC = placeholder(); + public static final int O_WRONLY = placeholder(); + public static final int POLLERR = placeholder(); + public static final int POLLHUP = placeholder(); + public static final int POLLIN = placeholder(); + public static final int POLLNVAL = placeholder(); + public static final int POLLOUT = placeholder(); + public static final int POLLPRI = placeholder(); + public static final int POLLRDBAND = placeholder(); + public static final int POLLRDNORM = placeholder(); + public static final int POLLWRBAND = placeholder(); + public static final int POLLWRNORM = placeholder(); + public static final int PROT_EXEC = placeholder(); + public static final int PROT_NONE = placeholder(); + public static final int PROT_READ = placeholder(); + public static final int PROT_WRITE = placeholder(); + public static final int R_OK = placeholder(); + public static final int SEEK_CUR = placeholder(); + public static final int SEEK_END = placeholder(); + public static final int SEEK_SET = placeholder(); + public static final int SHUT_RD = placeholder(); + public static final int SHUT_RDWR = placeholder(); + public static final int SHUT_WR = placeholder(); + public static final int SIGABRT = placeholder(); + public static final int SIGALRM = placeholder(); + public static final int SIGBUS = placeholder(); + public static final int SIGCHLD = placeholder(); + public static final int SIGCONT = placeholder(); + public static final int SIGFPE = placeholder(); + public static final int SIGHUP = placeholder(); + public static final int SIGILL = placeholder(); + public static final int SIGINT = placeholder(); + public static final int SIGIO = placeholder(); + public static final int SIGKILL = placeholder(); + public static final int SIGPIPE = placeholder(); + public static final int SIGPROF = placeholder(); + public static final int SIGPWR = placeholder(); + public static final int SIGQUIT = placeholder(); + public static final int SIGRTMAX = placeholder(); + public static final int SIGRTMIN = placeholder(); + public static final int SIGSEGV = placeholder(); + public static final int SIGSTKFLT = placeholder(); + public static final int SIGSTOP = placeholder(); + public static final int SIGSYS = placeholder(); + public static final int SIGTERM = placeholder(); + public static final int SIGTRAP = placeholder(); + public static final int SIGTSTP = placeholder(); + public static final int SIGTTIN = placeholder(); + public static final int SIGTTOU = placeholder(); + public static final int SIGURG = placeholder(); + public static final int SIGUSR1 = placeholder(); + public static final int SIGUSR2 = placeholder(); + public static final int SIGVTALRM = placeholder(); + public static final int SIGWINCH = placeholder(); + public static final int SIGXCPU = placeholder(); + public static final int SIGXFSZ = placeholder(); + public static final int SIOCGIFADDR = placeholder(); + public static final int SIOCGIFBRDADDR = placeholder(); + public static final int SIOCGIFDSTADDR = placeholder(); + public static final int SIOCGIFNETMASK = placeholder(); + public static final int SOCK_DGRAM = placeholder(); + public static final int SOCK_RAW = placeholder(); + public static final int SOCK_SEQPACKET = placeholder(); + public static final int SOCK_STREAM = placeholder(); + public static final int SOL_SOCKET = placeholder(); + public static final int SO_BINDTODEVICE = placeholder(); + public static final int SO_BROADCAST = placeholder(); + public static final int SO_DEBUG = placeholder(); + public static final int SO_DONTROUTE = placeholder(); + public static final int SO_ERROR = placeholder(); + public static final int SO_KEEPALIVE = placeholder(); + public static final int SO_LINGER = placeholder(); + public static final int SO_OOBINLINE = placeholder(); + public static final int SO_RCVBUF = placeholder(); + public static final int SO_RCVLOWAT = placeholder(); + public static final int SO_RCVTIMEO = placeholder(); + public static final int SO_REUSEADDR = placeholder(); + public static final int SO_SNDBUF = placeholder(); + public static final int SO_SNDLOWAT = placeholder(); + public static final int SO_SNDTIMEO = placeholder(); + public static final int SO_TYPE = placeholder(); + public static final int STDERR_FILENO = placeholder(); + public static final int STDIN_FILENO = placeholder(); + public static final int STDOUT_FILENO = placeholder(); + public static final int S_IFBLK = placeholder(); + public static final int S_IFCHR = placeholder(); + public static final int S_IFDIR = placeholder(); + public static final int S_IFIFO = placeholder(); + public static final int S_IFLNK = placeholder(); + public static final int S_IFMT = placeholder(); + public static final int S_IFREG = placeholder(); + public static final int S_IFSOCK = placeholder(); + public static final int S_IRGRP = placeholder(); + public static final int S_IROTH = placeholder(); + public static final int S_IRUSR = placeholder(); + public static final int S_IRWXG = placeholder(); + public static final int S_IRWXO = placeholder(); + public static final int S_IRWXU = placeholder(); + public static final int S_ISGID = placeholder(); + public static final int S_ISUID = placeholder(); + public static final int S_ISVTX = placeholder(); + public static final int S_IWGRP = placeholder(); + public static final int S_IWOTH = placeholder(); + public static final int S_IWUSR = placeholder(); + public static final int S_IXGRP = placeholder(); + public static final int S_IXOTH = placeholder(); + public static final int S_IXUSR = placeholder(); + public static final int TCP_NODELAY = placeholder(); + public static final int WCONTINUED = placeholder(); + public static final int WEXITED = placeholder(); + public static final int WNOHANG = placeholder(); + public static final int WNOWAIT = placeholder(); + public static final int WSTOPPED = placeholder(); + public static final int WUNTRACED = placeholder(); + public static final int W_OK = placeholder(); + public static final int X_OK = placeholder(); + public static final int _SC_2_CHAR_TERM = placeholder(); + public static final int _SC_2_C_BIND = placeholder(); + public static final int _SC_2_C_DEV = placeholder(); + public static final int _SC_2_C_VERSION = placeholder(); + public static final int _SC_2_FORT_DEV = placeholder(); + public static final int _SC_2_FORT_RUN = placeholder(); + public static final int _SC_2_LOCALEDEF = placeholder(); + public static final int _SC_2_SW_DEV = placeholder(); + public static final int _SC_2_UPE = placeholder(); + public static final int _SC_2_VERSION = placeholder(); + public static final int _SC_AIO_LISTIO_MAX = placeholder(); + public static final int _SC_AIO_MAX = placeholder(); + public static final int _SC_AIO_PRIO_DELTA_MAX = placeholder(); + public static final int _SC_ARG_MAX = placeholder(); + public static final int _SC_ASYNCHRONOUS_IO = placeholder(); + public static final int _SC_ATEXIT_MAX = placeholder(); + public static final int _SC_AVPHYS_PAGES = placeholder(); + public static final int _SC_BC_BASE_MAX = placeholder(); + public static final int _SC_BC_DIM_MAX = placeholder(); + public static final int _SC_BC_SCALE_MAX = placeholder(); + public static final int _SC_BC_STRING_MAX = placeholder(); + public static final int _SC_CHILD_MAX = placeholder(); + public static final int _SC_CLK_TCK = placeholder(); + public static final int _SC_COLL_WEIGHTS_MAX = placeholder(); + public static final int _SC_DELAYTIMER_MAX = placeholder(); + public static final int _SC_EXPR_NEST_MAX = placeholder(); + public static final int _SC_FSYNC = placeholder(); + public static final int _SC_GETGR_R_SIZE_MAX = placeholder(); + public static final int _SC_GETPW_R_SIZE_MAX = placeholder(); + public static final int _SC_IOV_MAX = placeholder(); + public static final int _SC_JOB_CONTROL = placeholder(); + public static final int _SC_LINE_MAX = placeholder(); + public static final int _SC_LOGIN_NAME_MAX = placeholder(); + public static final int _SC_MAPPED_FILES = placeholder(); + public static final int _SC_MEMLOCK = placeholder(); + public static final int _SC_MEMLOCK_RANGE = placeholder(); + public static final int _SC_MEMORY_PROTECTION = placeholder(); + public static final int _SC_MESSAGE_PASSING = placeholder(); + public static final int _SC_MQ_OPEN_MAX = placeholder(); + public static final int _SC_MQ_PRIO_MAX = placeholder(); + public static final int _SC_NGROUPS_MAX = placeholder(); + public static final int _SC_NPROCESSORS_CONF = placeholder(); + public static final int _SC_NPROCESSORS_ONLN = placeholder(); + public static final int _SC_OPEN_MAX = placeholder(); + public static final int _SC_PAGESIZE = placeholder(); + public static final int _SC_PAGE_SIZE = placeholder(); + public static final int _SC_PASS_MAX = placeholder(); + public static final int _SC_PHYS_PAGES = placeholder(); + public static final int _SC_PRIORITIZED_IO = placeholder(); + public static final int _SC_PRIORITY_SCHEDULING = placeholder(); + public static final int _SC_REALTIME_SIGNALS = placeholder(); + public static final int _SC_RE_DUP_MAX = placeholder(); + public static final int _SC_RTSIG_MAX = placeholder(); + public static final int _SC_SAVED_IDS = placeholder(); + public static final int _SC_SEMAPHORES = placeholder(); + public static final int _SC_SEM_NSEMS_MAX = placeholder(); + public static final int _SC_SEM_VALUE_MAX = placeholder(); + public static final int _SC_SHARED_MEMORY_OBJECTS = placeholder(); + public static final int _SC_SIGQUEUE_MAX = placeholder(); + public static final int _SC_STREAM_MAX = placeholder(); + public static final int _SC_SYNCHRONIZED_IO = placeholder(); + public static final int _SC_THREADS = placeholder(); + public static final int _SC_THREAD_ATTR_STACKADDR = placeholder(); + public static final int _SC_THREAD_ATTR_STACKSIZE = placeholder(); + public static final int _SC_THREAD_DESTRUCTOR_ITERATIONS = placeholder(); + public static final int _SC_THREAD_KEYS_MAX = placeholder(); + public static final int _SC_THREAD_PRIORITY_SCHEDULING = placeholder(); + public static final int _SC_THREAD_PRIO_INHERIT = placeholder(); + public static final int _SC_THREAD_PRIO_PROTECT = placeholder(); + public static final int _SC_THREAD_SAFE_FUNCTIONS = placeholder(); + public static final int _SC_THREAD_STACK_MIN = placeholder(); + public static final int _SC_THREAD_THREADS_MAX = placeholder(); + public static final int _SC_TIMERS = placeholder(); + public static final int _SC_TIMER_MAX = placeholder(); + public static final int _SC_TTY_NAME_MAX = placeholder(); + public static final int _SC_TZNAME_MAX = placeholder(); + public static final int _SC_VERSION = placeholder(); + public static final int _SC_XBS5_ILP32_OFF32 = placeholder(); + public static final int _SC_XBS5_ILP32_OFFBIG = placeholder(); + public static final int _SC_XBS5_LP64_OFF64 = placeholder(); + public static final int _SC_XBS5_LPBIG_OFFBIG = placeholder(); + public static final int _SC_XOPEN_CRYPT = placeholder(); + public static final int _SC_XOPEN_ENH_I18N = placeholder(); + public static final int _SC_XOPEN_LEGACY = placeholder(); + public static final int _SC_XOPEN_REALTIME = placeholder(); + public static final int _SC_XOPEN_REALTIME_THREADS = placeholder(); + public static final int _SC_XOPEN_SHM = placeholder(); + public static final int _SC_XOPEN_UNIX = placeholder(); + public static final int _SC_XOPEN_VERSION = placeholder(); + public static final int _SC_XOPEN_XCU_VERSION = placeholder(); + + public static String gaiName(int error) { + if (error == EAI_AGAIN) { + return "EAI_AGAIN"; + } + if (error == EAI_BADFLAGS) { + return "EAI_BADFLAGS"; + } + if (error == EAI_FAIL) { + return "EAI_FAIL"; + } + if (error == EAI_FAMILY) { + return "EAI_FAMILY"; + } + if (error == EAI_MEMORY) { + return "EAI_MEMORY"; + } + if (error == EAI_NODATA) { + return "EAI_NODATA"; + } + if (error == EAI_NONAME) { + return "EAI_NONAME"; + } + if (error == EAI_OVERFLOW) { + return "EAI_OVERFLOW"; + } + if (error == EAI_SERVICE) { + return "EAI_SERVICE"; + } + if (error == EAI_SOCKTYPE) { + return "EAI_SOCKTYPE"; + } + if (error == EAI_SYSTEM) { + return "EAI_SYSTEM"; + } + return null; + } + + public static String errnoName(int errno) { + if (errno == E2BIG) { + return "E2BIG"; + } + if (errno == EACCES) { + return "EACCES"; + } + if (errno == EADDRINUSE) { + return "EADDRINUSE"; + } + if (errno == EADDRNOTAVAIL) { + return "EADDRNOTAVAIL"; + } + if (errno == EAFNOSUPPORT) { + return "EAFNOSUPPORT"; + } + if (errno == EAGAIN) { + return "EAGAIN"; + } + if (errno == EALREADY) { + return "EALREADY"; + } + if (errno == EBADF) { + return "EBADF"; + } + if (errno == EBADMSG) { + return "EBADMSG"; + } + if (errno == EBUSY) { + return "EBUSY"; + } + if (errno == ECANCELED) { + return "ECANCELED"; + } + if (errno == ECHILD) { + return "ECHILD"; + } + if (errno == ECONNABORTED) { + return "ECONNABORTED"; + } + if (errno == ECONNREFUSED) { + return "ECONNREFUSED"; + } + if (errno == ECONNRESET) { + return "ECONNRESET"; + } + if (errno == EDEADLK) { + return "EDEADLK"; + } + if (errno == EDESTADDRREQ) { + return "EDESTADDRREQ"; + } + if (errno == EDOM) { + return "EDOM"; + } + if (errno == EDQUOT) { + return "EDQUOT"; + } + if (errno == EEXIST) { + return "EEXIST"; + } + if (errno == EFAULT) { + return "EFAULT"; + } + if (errno == EFBIG) { + return "EFBIG"; + } + if (errno == EHOSTUNREACH) { + return "EHOSTUNREACH"; + } + if (errno == EIDRM) { + return "EIDRM"; + } + if (errno == EILSEQ) { + return "EILSEQ"; + } + if (errno == EINPROGRESS) { + return "EINPROGRESS"; + } + if (errno == EINTR) { + return "EINTR"; + } + if (errno == EINVAL) { + return "EINVAL"; + } + if (errno == EIO) { + return "EIO"; + } + if (errno == EISCONN) { + return "EISCONN"; + } + if (errno == EISDIR) { + return "EISDIR"; + } + if (errno == ELOOP) { + return "ELOOP"; + } + if (errno == EMFILE) { + return "EMFILE"; + } + if (errno == EMLINK) { + return "EMLINK"; + } + if (errno == EMSGSIZE) { + return "EMSGSIZE"; + } + if (errno == EMULTIHOP) { + return "EMULTIHOP"; + } + if (errno == ENAMETOOLONG) { + return "ENAMETOOLONG"; + } + if (errno == ENETDOWN) { + return "ENETDOWN"; + } + if (errno == ENETRESET) { + return "ENETRESET"; + } + if (errno == ENETUNREACH) { + return "ENETUNREACH"; + } + if (errno == ENFILE) { + return "ENFILE"; + } + if (errno == ENOBUFS) { + return "ENOBUFS"; + } + if (errno == ENODATA) { + return "ENODATA"; + } + if (errno == ENODEV) { + return "ENODEV"; + } + if (errno == ENOENT) { + return "ENOENT"; + } + if (errno == ENOEXEC) { + return "ENOEXEC"; + } + if (errno == ENOLCK) { + return "ENOLCK"; + } + if (errno == ENOLINK) { + return "ENOLINK"; + } + if (errno == ENOMEM) { + return "ENOMEM"; + } + if (errno == ENOMSG) { + return "ENOMSG"; + } + if (errno == ENOPROTOOPT) { + return "ENOPROTOOPT"; + } + if (errno == ENOSPC) { + return "ENOSPC"; + } + if (errno == ENOSR) { + return "ENOSR"; + } + if (errno == ENOSTR) { + return "ENOSTR"; + } + if (errno == ENOSYS) { + return "ENOSYS"; + } + if (errno == ENOTCONN) { + return "ENOTCONN"; + } + if (errno == ENOTDIR) { + return "ENOTDIR"; + } + if (errno == ENOTEMPTY) { + return "ENOTEMPTY"; + } + if (errno == ENOTSOCK) { + return "ENOTSOCK"; + } + if (errno == ENOTSUP) { + return "ENOTSUP"; + } + if (errno == ENOTTY) { + return "ENOTTY"; + } + if (errno == ENXIO) { + return "ENXIO"; + } + if (errno == EOPNOTSUPP) { + return "EOPNOTSUPP"; + } + if (errno == EOVERFLOW) { + return "EOVERFLOW"; + } + if (errno == EPERM) { + return "EPERM"; + } + if (errno == EPIPE) { + return "EPIPE"; + } + if (errno == EPROTO) { + return "EPROTO"; + } + if (errno == EPROTONOSUPPORT) { + return "EPROTONOSUPPORT"; + } + if (errno == EPROTOTYPE) { + return "EPROTOTYPE"; + } + if (errno == ERANGE) { + return "ERANGE"; + } + if (errno == EROFS) { + return "EROFS"; + } + if (errno == ESPIPE) { + return "ESPIPE"; + } + if (errno == ESRCH) { + return "ESRCH"; + } + if (errno == ESTALE) { + return "ESTALE"; + } + if (errno == ETIME) { + return "ETIME"; + } + if (errno == ETIMEDOUT) { + return "ETIMEDOUT"; + } + if (errno == ETXTBSY) { + return "ETXTBSY"; + } + if (errno == EWOULDBLOCK) { + return "EWOULDBLOCK"; + } + if (errno == EXDEV) { + return "EXDEV"; + } + return null; + } + + private static native void initConstants(); + + // A hack to avoid these constants being inlined by javac... + private static int placeholder() { return 0; } + // ...because we want to initialize them at runtime. + static { + initConstants(); + } +} diff --git a/src/main/java/libcore/io/SizeOf.java b/src/main/java/libcore/io/SizeOf.java new file mode 100644 index 000000000000..728fbfce7a0c --- /dev/null +++ b/src/main/java/libcore/io/SizeOf.java @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package libcore.io; + +public final class SizeOf { + public static final int CHAR = 2; + public static final int DOUBLE = 8; + public static final int FLOAT = 4; + public static final int INT = 4; + public static final int LONG = 8; + public static final int SHORT = 2; + + private SizeOf() { + } +} diff --git a/src/main/java/libcore/io/Streams.java b/src/main/java/libcore/io/Streams.java new file mode 100644 index 000000000000..194b77510371 --- /dev/null +++ b/src/main/java/libcore/io/Streams.java @@ -0,0 +1,216 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package libcore.io; + +import java.io.ByteArrayOutputStream; +import java.io.EOFException; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.Reader; +import java.io.StringWriter; +import java.util.concurrent.atomic.AtomicReference; +import libcore.util.Libcore; + +public final class Streams { + private static AtomicReference skipBuffer = new AtomicReference(); + + private Streams() {} + + /** + * Implements InputStream.read(int) in terms of InputStream.read(byte[], int, int). + * InputStream assumes that you implement InputStream.read(int) and provides default + * implementations of the others, but often the opposite is more efficient. + */ + public static int readSingleByte(InputStream in) throws IOException { + byte[] buffer = new byte[1]; + int result = in.read(buffer, 0, 1); + return (result != -1) ? buffer[0] & 0xff : -1; + } + + /** + * Implements OutputStream.write(int) in terms of OutputStream.write(byte[], int, int). + * OutputStream assumes that you implement OutputStream.write(int) and provides default + * implementations of the others, but often the opposite is more efficient. + */ + public static void writeSingleByte(OutputStream out, int b) throws IOException { + byte[] buffer = new byte[1]; + buffer[0] = (byte) (b & 0xff); + out.write(buffer); + } + + /** + * Fills 'dst' with bytes from 'in', throwing EOFException if insufficient bytes are available. + */ + public static void readFully(InputStream in, byte[] dst) throws IOException { + readFully(in, dst, 0, dst.length); + } + + /** + * Reads exactly 'byteCount' bytes from 'in' (into 'dst' at offset 'offset'), and throws + * EOFException if insufficient bytes are available. + * + * Used to implement {@link java.io.DataInputStream#readFully(byte[], int, int)}. + */ + public static void readFully(InputStream in, byte[] dst, int offset, int byteCount) throws IOException { + if (byteCount == 0) { + return; + } + if (in == null) { + throw new NullPointerException("in == null"); + } + if (dst == null) { + throw new NullPointerException("dst == null"); + } + Libcore.checkOffsetAndCount(dst.length, offset, byteCount); + while (byteCount > 0) { + int bytesRead = in.read(dst, offset, byteCount); + if (bytesRead < 0) { + throw new EOFException(); + } + offset += bytesRead; + byteCount -= bytesRead; + } + } + + /** + * Returns a byte[] containing the remainder of 'in', closing it when done. + */ + public static byte[] readFully(InputStream in) throws IOException { + try { + return readFullyNoClose(in); + } finally { + in.close(); + } + } + + /** + * Returns a byte[] containing the remainder of 'in'. + */ + public static byte[] readFullyNoClose(InputStream in) throws IOException { + ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + byte[] buffer = new byte[1024]; + int count; + while ((count = in.read(buffer)) != -1) { + bytes.write(buffer, 0, count); + } + return bytes.toByteArray(); + } + + /** + * Returns the remainder of 'reader' as a string, closing it when done. + */ + public static String readFully(Reader reader) throws IOException { + try { + StringWriter writer = new StringWriter(); + char[] buffer = new char[1024]; + int count; + while ((count = reader.read(buffer)) != -1) { + writer.write(buffer, 0, count); + } + return writer.toString(); + } finally { + reader.close(); + } + } + + public static void skipAll(InputStream in) throws IOException { + do { + in.skip(Long.MAX_VALUE); + } while (in.read() != -1); + } + + /** + * Call {@code in.read()} repeatedly until either the stream is exhausted or + * {@code byteCount} bytes have been read. + * + *

    This method reuses the skip buffer but is careful to never use it at + * the same time that another stream is using it. Otherwise streams that use + * the caller's buffer for consistency checks like CRC could be clobbered by + * other threads. A thread-local buffer is also insufficient because some + * streams may call other streams in their skip() method, also clobbering the + * buffer. + */ + public static long skipByReading(InputStream in, long byteCount) throws IOException { + // acquire the shared skip buffer. + byte[] buffer = skipBuffer.getAndSet(null); + if (buffer == null) { + buffer = new byte[4096]; + } + + long skipped = 0; + while (skipped < byteCount) { + int toRead = (int) Math.min(byteCount - skipped, buffer.length); + int read = in.read(buffer, 0, toRead); + if (read == -1) { + break; + } + skipped += read; + if (read < toRead) { + break; + } + } + + // release the shared skip buffer. + skipBuffer.set(buffer); + + return skipped; + } + + /** + * Copies all of the bytes from {@code in} to {@code out}. Neither stream is closed. + * Returns the total number of bytes transferred. + */ + public static int copy(InputStream in, OutputStream out) throws IOException { + int total = 0; + byte[] buffer = new byte[8192]; + int c; + while ((c = in.read(buffer)) != -1) { + total += c; + out.write(buffer, 0, c); + } + return total; + } + + /** + * Returns the ASCII characters up to but not including the next "\r\n", or + * "\n". + * + * @throws java.io.EOFException if the stream is exhausted before the next newline + * character. + */ + public static String readAsciiLine(InputStream in) throws IOException { + // TODO: support UTF-8 here instead + + StringBuilder result = new StringBuilder(80); + while (true) { + int c = in.read(); + if (c == -1) { + throw new EOFException(); + } else if (c == '\n') { + break; + } + + result.append((char) c); + } + int length = result.length(); + if (length > 0 && result.charAt(length - 1) == '\r') { + result.setLength(length - 1); + } + return result.toString(); + } +} diff --git a/src/main/java/libcore/net/MimeUtils.java b/src/main/java/libcore/net/MimeUtils.java new file mode 100644 index 000000000000..f8038f0b651b --- /dev/null +++ b/src/main/java/libcore/net/MimeUtils.java @@ -0,0 +1,480 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package libcore.net; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.HashMap; +import java.util.Map; +import java.util.Properties; + +/** + * Utilities for dealing with MIME types. + * Used to implement java.net.URLConnection and android.webkit.MimeTypeMap. + */ +public final class MimeUtils { + private static final Map mimeTypeToExtensionMap = new HashMap(); + + private static final Map extensionToMimeTypeMap = new HashMap(); + + static { + // The following table is based on /etc/mime.types data minus + // chemical/* MIME types and MIME types that don't map to any + // file extensions. We also exclude top-level domain names to + // deal with cases like: + // + // mail.google.com/a/google.com + // + // and "active" MIME types (due to potential security issues). + + add("application/andrew-inset", "ez"); + add("application/dsptype", "tsp"); + add("application/futuresplash", "spl"); + add("application/hta", "hta"); + add("application/mac-binhex40", "hqx"); + add("application/mac-compactpro", "cpt"); + add("application/mathematica", "nb"); + add("application/msaccess", "mdb"); + add("application/oda", "oda"); + add("application/ogg", "ogg"); + add("application/pdf", "pdf"); + add("application/pgp-keys", "key"); + add("application/pgp-signature", "pgp"); + add("application/pics-rules", "prf"); + add("application/rar", "rar"); + add("application/rdf+xml", "rdf"); + add("application/rss+xml", "rss"); + add("application/zip", "zip"); + add("application/vnd.android.package-archive", "apk"); + add("application/vnd.cinderella", "cdy"); + add("application/vnd.ms-pki.stl", "stl"); + add("application/vnd.oasis.opendocument.database", "odb"); + add("application/vnd.oasis.opendocument.formula", "odf"); + add("application/vnd.oasis.opendocument.graphics", "odg"); + add("application/vnd.oasis.opendocument.graphics-template", "otg"); + add("application/vnd.oasis.opendocument.image", "odi"); + add("application/vnd.oasis.opendocument.spreadsheet", "ods"); + add("application/vnd.oasis.opendocument.spreadsheet-template", "ots"); + add("application/vnd.oasis.opendocument.text", "odt"); + add("application/vnd.oasis.opendocument.text-master", "odm"); + add("application/vnd.oasis.opendocument.text-template", "ott"); + add("application/vnd.oasis.opendocument.text-web", "oth"); + add("application/vnd.google-earth.kml+xml", "kml"); + add("application/vnd.google-earth.kmz", "kmz"); + add("application/msword", "doc"); + add("application/msword", "dot"); + add("application/vnd.openxmlformats-officedocument.wordprocessingml.document", "docx"); + add("application/vnd.openxmlformats-officedocument.wordprocessingml.template", "dotx"); + add("application/vnd.ms-excel", "xls"); + add("application/vnd.ms-excel", "xlt"); + add("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", "xlsx"); + add("application/vnd.openxmlformats-officedocument.spreadsheetml.template", "xltx"); + add("application/vnd.ms-powerpoint", "ppt"); + add("application/vnd.ms-powerpoint", "pot"); + add("application/vnd.ms-powerpoint", "pps"); + add("application/vnd.openxmlformats-officedocument.presentationml.presentation", "pptx"); + add("application/vnd.openxmlformats-officedocument.presentationml.template", "potx"); + add("application/vnd.openxmlformats-officedocument.presentationml.slideshow", "ppsx"); + add("application/vnd.rim.cod", "cod"); + add("application/vnd.smaf", "mmf"); + add("application/vnd.stardivision.calc", "sdc"); + add("application/vnd.stardivision.draw", "sda"); + add("application/vnd.stardivision.impress", "sdd"); + add("application/vnd.stardivision.impress", "sdp"); + add("application/vnd.stardivision.math", "smf"); + add("application/vnd.stardivision.writer", "sdw"); + add("application/vnd.stardivision.writer", "vor"); + add("application/vnd.stardivision.writer-global", "sgl"); + add("application/vnd.sun.xml.calc", "sxc"); + add("application/vnd.sun.xml.calc.template", "stc"); + add("application/vnd.sun.xml.draw", "sxd"); + add("application/vnd.sun.xml.draw.template", "std"); + add("application/vnd.sun.xml.impress", "sxi"); + add("application/vnd.sun.xml.impress.template", "sti"); + add("application/vnd.sun.xml.math", "sxm"); + add("application/vnd.sun.xml.writer", "sxw"); + add("application/vnd.sun.xml.writer.global", "sxg"); + add("application/vnd.sun.xml.writer.template", "stw"); + add("application/vnd.visio", "vsd"); + add("application/x-abiword", "abw"); + add("application/x-apple-diskimage", "dmg"); + add("application/x-bcpio", "bcpio"); + add("application/x-bittorrent", "torrent"); + add("application/x-cdf", "cdf"); + add("application/x-cdlink", "vcd"); + add("application/x-chess-pgn", "pgn"); + add("application/x-cpio", "cpio"); + add("application/x-debian-package", "deb"); + add("application/x-debian-package", "udeb"); + add("application/x-director", "dcr"); + add("application/x-director", "dir"); + add("application/x-director", "dxr"); + add("application/x-dms", "dms"); + add("application/x-doom", "wad"); + add("application/x-dvi", "dvi"); + add("application/x-flac", "flac"); + add("application/x-font", "pfa"); + add("application/x-font", "pfb"); + add("application/x-font", "gsf"); + add("application/x-font", "pcf"); + add("application/x-font", "pcf.Z"); + add("application/x-freemind", "mm"); + add("application/x-futuresplash", "spl"); + add("application/x-gnumeric", "gnumeric"); + add("application/x-go-sgf", "sgf"); + add("application/x-graphing-calculator", "gcf"); + add("application/x-gtar", "gtar"); + add("application/x-gtar", "tgz"); + add("application/x-gtar", "taz"); + add("application/x-hdf", "hdf"); + add("application/x-ica", "ica"); + add("application/x-internet-signup", "ins"); + add("application/x-internet-signup", "isp"); + add("application/x-iphone", "iii"); + add("application/x-iso9660-image", "iso"); + add("application/x-jmol", "jmz"); + add("application/x-kchart", "chrt"); + add("application/x-killustrator", "kil"); + add("application/x-koan", "skp"); + add("application/x-koan", "skd"); + add("application/x-koan", "skt"); + add("application/x-koan", "skm"); + add("application/x-kpresenter", "kpr"); + add("application/x-kpresenter", "kpt"); + add("application/x-kspread", "ksp"); + add("application/x-kword", "kwd"); + add("application/x-kword", "kwt"); + add("application/x-latex", "latex"); + add("application/x-lha", "lha"); + add("application/x-lzh", "lzh"); + add("application/x-lzx", "lzx"); + add("application/x-maker", "frm"); + add("application/x-maker", "maker"); + add("application/x-maker", "frame"); + add("application/x-maker", "fb"); + add("application/x-maker", "book"); + add("application/x-maker", "fbdoc"); + add("application/x-mif", "mif"); + add("application/x-ms-wmd", "wmd"); + add("application/x-ms-wmz", "wmz"); + add("application/x-msi", "msi"); + add("application/x-ns-proxy-autoconfig", "pac"); + add("application/x-nwc", "nwc"); + add("application/x-object", "o"); + add("application/x-oz-application", "oza"); + add("application/x-pkcs12", "p12"); + add("application/x-pkcs12", "pfx"); + add("application/x-pkcs7-certreqresp", "p7r"); + add("application/x-pkcs7-crl", "crl"); + add("application/x-quicktimeplayer", "qtl"); + add("application/x-shar", "shar"); + add("application/x-shockwave-flash", "swf"); + add("application/x-stuffit", "sit"); + add("application/x-sv4cpio", "sv4cpio"); + add("application/x-sv4crc", "sv4crc"); + add("application/x-tar", "tar"); + add("application/x-texinfo", "texinfo"); + add("application/x-texinfo", "texi"); + add("application/x-troff", "t"); + add("application/x-troff", "roff"); + add("application/x-troff-man", "man"); + add("application/x-ustar", "ustar"); + add("application/x-wais-source", "src"); + add("application/x-wingz", "wz"); + add("application/x-webarchive", "webarchive"); + add("application/x-webarchive-xml", "webarchivexml"); + add("application/x-x509-ca-cert", "crt"); + add("application/x-x509-user-cert", "crt"); + add("application/x-xcf", "xcf"); + add("application/x-xfig", "fig"); + add("application/xhtml+xml", "xhtml"); + add("audio/3gpp", "3gpp"); + add("audio/amr", "amr"); + add("audio/basic", "snd"); + add("audio/midi", "mid"); + add("audio/midi", "midi"); + add("audio/midi", "kar"); + add("audio/midi", "xmf"); + add("audio/mobile-xmf", "mxmf"); + add("audio/mpeg", "mpga"); + add("audio/mpeg", "mpega"); + add("audio/mpeg", "mp2"); + add("audio/mpeg", "mp3"); + add("audio/mpeg", "m4a"); + add("audio/mpegurl", "m3u"); + add("audio/prs.sid", "sid"); + add("audio/x-aiff", "aif"); + add("audio/x-aiff", "aiff"); + add("audio/x-aiff", "aifc"); + add("audio/x-gsm", "gsm"); + add("audio/x-mpegurl", "m3u"); + add("audio/x-ms-wma", "wma"); + add("audio/x-ms-wax", "wax"); + add("audio/x-pn-realaudio", "ra"); + add("audio/x-pn-realaudio", "rm"); + add("audio/x-pn-realaudio", "ram"); + add("audio/x-realaudio", "ra"); + add("audio/x-scpls", "pls"); + add("audio/x-sd2", "sd2"); + add("audio/x-wav", "wav"); + add("image/bmp", "bmp"); + add("image/gif", "gif"); + add("image/ico", "cur"); + add("image/ico", "ico"); + add("image/ief", "ief"); + add("image/jpeg", "jpeg"); + add("image/jpeg", "jpg"); + add("image/jpeg", "jpe"); + add("image/pcx", "pcx"); + add("image/png", "png"); + add("image/svg+xml", "svg"); + add("image/svg+xml", "svgz"); + add("image/tiff", "tiff"); + add("image/tiff", "tif"); + add("image/vnd.djvu", "djvu"); + add("image/vnd.djvu", "djv"); + add("image/vnd.wap.wbmp", "wbmp"); + add("image/x-cmu-raster", "ras"); + add("image/x-coreldraw", "cdr"); + add("image/x-coreldrawpattern", "pat"); + add("image/x-coreldrawtemplate", "cdt"); + add("image/x-corelphotopaint", "cpt"); + add("image/x-icon", "ico"); + add("image/x-jg", "art"); + add("image/x-jng", "jng"); + add("image/x-ms-bmp", "bmp"); + add("image/x-photoshop", "psd"); + add("image/x-portable-anymap", "pnm"); + add("image/x-portable-bitmap", "pbm"); + add("image/x-portable-graymap", "pgm"); + add("image/x-portable-pixmap", "ppm"); + add("image/x-rgb", "rgb"); + add("image/x-xbitmap", "xbm"); + add("image/x-xpixmap", "xpm"); + add("image/x-xwindowdump", "xwd"); + add("model/iges", "igs"); + add("model/iges", "iges"); + add("model/mesh", "msh"); + add("model/mesh", "mesh"); + add("model/mesh", "silo"); + add("text/calendar", "ics"); + add("text/calendar", "icz"); + add("text/comma-separated-values", "csv"); + add("text/css", "css"); + add("text/html", "htm"); + add("text/html", "html"); + add("text/h323", "323"); + add("text/iuls", "uls"); + add("text/mathml", "mml"); + // add ".txt" first so it will be the default for ExtensionFromMimeType + add("text/plain", "txt"); + add("text/plain", "asc"); + add("text/plain", "text"); + add("text/plain", "diff"); + add("text/plain", "po"); // reserve "pot" for vnd.ms-powerpoint + add("text/richtext", "rtx"); + add("text/rtf", "rtf"); + add("text/texmacs", "ts"); + add("text/text", "phps"); + add("text/tab-separated-values", "tsv"); + add("text/xml", "xml"); + add("text/x-bibtex", "bib"); + add("text/x-boo", "boo"); + add("text/x-c++hdr", "h++"); + add("text/x-c++hdr", "hpp"); + add("text/x-c++hdr", "hxx"); + add("text/x-c++hdr", "hh"); + add("text/x-c++src", "c++"); + add("text/x-c++src", "cpp"); + add("text/x-c++src", "cxx"); + add("text/x-chdr", "h"); + add("text/x-component", "htc"); + add("text/x-csh", "csh"); + add("text/x-csrc", "c"); + add("text/x-dsrc", "d"); + add("text/x-haskell", "hs"); + add("text/x-java", "java"); + add("text/x-literate-haskell", "lhs"); + add("text/x-moc", "moc"); + add("text/x-pascal", "p"); + add("text/x-pascal", "pas"); + add("text/x-pcs-gcd", "gcd"); + add("text/x-setext", "etx"); + add("text/x-tcl", "tcl"); + add("text/x-tex", "tex"); + add("text/x-tex", "ltx"); + add("text/x-tex", "sty"); + add("text/x-tex", "cls"); + add("text/x-vcalendar", "vcs"); + add("text/x-vcard", "vcf"); + add("video/3gpp", "3gpp"); + add("video/3gpp", "3gp"); + add("video/3gpp", "3g2"); + add("video/dl", "dl"); + add("video/dv", "dif"); + add("video/dv", "dv"); + add("video/fli", "fli"); + add("video/m4v", "m4v"); + add("video/mpeg", "mpeg"); + add("video/mpeg", "mpg"); + add("video/mpeg", "mpe"); + add("video/mp4", "mp4"); + add("video/mpeg", "VOB"); + add("video/quicktime", "qt"); + add("video/quicktime", "mov"); + add("video/vnd.mpegurl", "mxu"); + add("video/x-la-asf", "lsf"); + add("video/x-la-asf", "lsx"); + add("video/x-mng", "mng"); + add("video/x-ms-asf", "asf"); + add("video/x-ms-asf", "asx"); + add("video/x-ms-wm", "wm"); + add("video/x-ms-wmv", "wmv"); + add("video/x-ms-wmx", "wmx"); + add("video/x-ms-wvx", "wvx"); + add("video/x-msvideo", "avi"); + add("video/x-sgi-movie", "movie"); + add("x-conference/x-cooltalk", "ice"); + add("x-epoc/x-sisx-app", "sisx"); + applyOverrides(); + } + + private static void add(String mimeType, String extension) { + // + // if we have an existing x --> y mapping, we do not want to + // override it with another mapping x --> ? + // this is mostly because of the way the mime-type map below + // is constructed (if a mime type maps to several extensions + // the first extension is considered the most popular and is + // added first; we do not want to overwrite it later). + // + if (!mimeTypeToExtensionMap.containsKey(mimeType)) { + mimeTypeToExtensionMap.put(mimeType, extension); + } + extensionToMimeTypeMap.put(extension, mimeType); + } + + private static InputStream getContentTypesPropertiesStream() { + // User override? + String userTable = System.getProperty("content.types.user.table"); + if (userTable != null) { + File f = new File(userTable); + if (f.exists()) { + try { + return new FileInputStream(f); + } catch (IOException ignored) { + } + } + } + + // Standard location? + File f = new File(System.getProperty("java.home"), "lib" + File.separator + "content-types.properties"); + if (f.exists()) { + try { + return new FileInputStream(f); + } catch (IOException ignored) { + } + } + + return null; + } + + /** + * This isn't what the RI does. The RI doesn't have hard-coded defaults, so supplying your + * own "content.types.user.table" means you don't get any of the built-ins, and the built-ins + * come from "$JAVA_HOME/lib/content-types.properties". + */ + private static void applyOverrides() { + // Get the appropriate InputStream to read overrides from, if any. + InputStream stream = getContentTypesPropertiesStream(); + if (stream == null) { + return; + } + + try { + try { + // Read the properties file... + Properties overrides = new Properties(); + overrides.load(stream); + // And translate its mapping to ours... + for (Map.Entry entry : overrides.entrySet()) { + String extension = (String) entry.getKey(); + String mimeType = (String) entry.getValue(); + add(mimeType, extension); + } + } finally { + stream.close(); + } + } catch (IOException ignored) { + } + } + + private MimeUtils() { + } + + /** + * Returns true if the given MIME type has an entry in the map. + * @param mimeType A MIME type (i.e. text/plain) + * @return True iff there is a mimeType entry in the map. + */ + public static boolean hasMimeType(String mimeType) { + if (mimeType == null || mimeType.isEmpty()) { + return false; + } + return mimeTypeToExtensionMap.containsKey(mimeType); + } + + /** + * Returns the MIME type for the given extension. + * @param extension A file extension without the leading '.' + * @return The MIME type for the given extension or null iff there is none. + */ + public static String guessMimeTypeFromExtension(String extension) { + if (extension == null || extension.isEmpty()) { + return null; + } + return extensionToMimeTypeMap.get(extension); + } + + /** + * Returns true if the given extension has a registered MIME type. + * @param extension A file extension without the leading '.' + * @return True iff there is an extension entry in the map. + */ + public static boolean hasExtension(String extension) { + if (extension == null || extension.isEmpty()) { + return false; + } + return extensionToMimeTypeMap.containsKey(extension); + } + + /** + * Returns the registered extension for the given MIME type. Note that some + * MIME types map to multiple extensions. This call will return the most + * common extension for the given MIME type. + * @param mimeType A MIME type (i.e. text/plain) + * @return The extension for the given MIME type or null iff there is none. + */ + public static String guessExtensionFromMimeType(String mimeType) { + if (mimeType == null || mimeType.isEmpty()) { + return null; + } + return mimeTypeToExtensionMap.get(mimeType); + } +} diff --git a/src/main/java/libcore/net/http/AbstractHttpInputStream.java b/src/main/java/libcore/net/http/AbstractHttpInputStream.java new file mode 100644 index 000000000000..70f76b7ce719 --- /dev/null +++ b/src/main/java/libcore/net/http/AbstractHttpInputStream.java @@ -0,0 +1,107 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package libcore.net.http; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.CacheRequest; +import libcore.io.Streams; + +/** + * An input stream for the body of an HTTP response. + * + *

    Since a single socket's input stream may be used to read multiple HTTP + * responses from the same server, subclasses shouldn't close the socket stream. + * + *

    A side effect of reading an HTTP response is that the response cache + * is populated. If the stream is closed early, that cache entry will be + * invalidated. + */ +abstract class AbstractHttpInputStream extends InputStream { + protected final InputStream in; + protected final HttpEngine httpEngine; + private final CacheRequest cacheRequest; + private final OutputStream cacheBody; + protected boolean closed; + + AbstractHttpInputStream(InputStream in, HttpEngine httpEngine, + CacheRequest cacheRequest) throws IOException { + this.in = in; + this.httpEngine = httpEngine; + + OutputStream cacheBody = cacheRequest != null ? cacheRequest.getBody() : null; + + // some apps return a null body; for compatibility we treat that like a null cache request + if (cacheBody == null) { + cacheRequest = null; + } + + this.cacheBody = cacheBody; + this.cacheRequest = cacheRequest; + } + + /** + * read() is implemented using read(byte[], int, int) so subclasses only + * need to override the latter. + */ + @Override public final int read() throws IOException { + return Streams.readSingleByte(this); + } + + protected final void checkNotClosed() throws IOException { + if (closed) { + throw new IOException("stream closed"); + } + } + + protected final void cacheWrite(byte[] buffer, int offset, int count) throws IOException { + if (cacheBody != null) { + cacheBody.write(buffer, offset, count); + } + } + + /** + * Closes the cache entry and makes the socket available for reuse. This + * should be invoked when the end of the body has been reached. + */ + protected final void endOfInput(boolean reuseSocket) throws IOException { + if (cacheRequest != null) { + cacheBody.close(); + } + httpEngine.release(reuseSocket); + } + + /** + * Calls abort on the cache entry and disconnects the socket. This + * should be invoked when the connection is closed unexpectedly to + * invalidate the cache entry and to prevent the HTTP connection from + * being reused. HTTP messages are sent in serial so whenever a message + * cannot be read to completion, subsequent messages cannot be read + * either and the connection must be discarded. + * + *

    An earlier implementation skipped the remaining bytes, but this + * requires that the entire transfer be completed. If the intention was + * to cancel the transfer, closing the connection is the only solution. + */ + protected final void unexpectedEndOfInput() { + if (cacheRequest != null) { + cacheRequest.abort(); + } + httpEngine.release(false); + } +} diff --git a/src/main/java/libcore/net/http/AbstractHttpOutputStream.java b/src/main/java/libcore/net/http/AbstractHttpOutputStream.java new file mode 100644 index 000000000000..1e1b47b096e7 --- /dev/null +++ b/src/main/java/libcore/net/http/AbstractHttpOutputStream.java @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package libcore.net.http; + +import java.io.IOException; +import java.io.OutputStream; + +/** + * An output stream for the body of an HTTP request. + * + *

    Since a single socket's output stream may be used to write multiple HTTP + * requests to the same server, subclasses should not close the socket stream. + */ +abstract class AbstractHttpOutputStream extends OutputStream { + protected boolean closed; + + @Override public final void write(int data) throws IOException { + write(new byte[] { (byte) data }); + } + + protected final void checkNotClosed() throws IOException { + if (closed) { + throw new IOException("stream closed"); + } + } +} diff --git a/src/main/java/libcore/net/http/Challenge.java b/src/main/java/libcore/net/http/Challenge.java new file mode 100644 index 000000000000..d373c0a6629e --- /dev/null +++ b/src/main/java/libcore/net/http/Challenge.java @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package libcore.net.http; + +/** + * An RFC 2617 challenge. + */ +final class Challenge { + final String scheme; + final String realm; + + Challenge(String scheme, String realm) { + this.scheme = scheme; + this.realm = realm; + } + + @Override public boolean equals(Object o) { + return o instanceof Challenge + && ((Challenge) o).scheme.equals(scheme) + && ((Challenge) o).realm.equals(realm); + } + + @Override public int hashCode() { + return scheme.hashCode() + 31 * realm.hashCode(); + } +} diff --git a/src/main/java/libcore/net/http/HeaderParser.java b/src/main/java/libcore/net/http/HeaderParser.java new file mode 100644 index 000000000000..26e77b64cc2d --- /dev/null +++ b/src/main/java/libcore/net/http/HeaderParser.java @@ -0,0 +1,163 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package libcore.net.http; + +import java.util.ArrayList; +import java.util.List; + +final class HeaderParser { + + public interface CacheControlHandler { + void handle(String directive, String parameter); + } + + /** + * Parse a comma-separated list of cache control header values. + */ + public static void parseCacheControl(String value, CacheControlHandler handler) { + int pos = 0; + while (pos < value.length()) { + int tokenStart = pos; + pos = skipUntil(value, pos, "=,"); + String directive = value.substring(tokenStart, pos).trim(); + + if (pos == value.length() || value.charAt(pos) == ',') { + pos++; // consume ',' (if necessary) + handler.handle(directive, null); + continue; + } + + pos++; // consume '=' + pos = skipWhitespace(value, pos); + + String parameter; + + // quoted string + if (pos < value.length() && value.charAt(pos) == '\"') { + pos++; // consume '"' open quote + int parameterStart = pos; + pos = skipUntil(value, pos, "\""); + parameter = value.substring(parameterStart, pos); + pos++; // consume '"' close quote (if necessary) + + // unquoted string + } else { + int parameterStart = pos; + pos = skipUntil(value, pos, ","); + parameter = value.substring(parameterStart, pos).trim(); + } + + handler.handle(directive, parameter); + } + } + + /** + * Parse RFC 2617 challenges. This API is only interested in the scheme + * name and realm. + */ + public static List parseChallenges( + RawHeaders responseHeaders, String challengeHeader) { + /* + * auth-scheme = token + * auth-param = token "=" ( token | quoted-string ) + * challenge = auth-scheme 1*SP 1#auth-param + * realm = "realm" "=" realm-value + * realm-value = quoted-string + */ + List result = new ArrayList(); + for (int h = 0; h < responseHeaders.length(); h++) { + if (!challengeHeader.equalsIgnoreCase(responseHeaders.getFieldName(h))) { + continue; + } + String value = responseHeaders.getValue(h); + int pos = 0; + while (pos < value.length()) { + int tokenStart = pos; + pos = skipUntil(value, pos, " "); + + String scheme = value.substring(tokenStart, pos).trim(); + pos = skipWhitespace(value, pos); + + // TODO: This currently only handles schemes with a 'realm' parameter; + // It needs to be fixed to handle any scheme and any parameters + // http://code.google.com/p/android/issues/detail?id=11140 + + if (!value.regionMatches(pos, "realm=\"", 0, "realm=\"".length())) { + break; // unexpected challenge parameter; give up + } + + pos += "realm=\"".length(); + int realmStart = pos; + pos = skipUntil(value, pos, "\""); + String realm = value.substring(realmStart, pos); + pos++; // consume '"' close quote + pos = skipUntil(value, pos, ","); + pos++; // consume ',' comma + pos = skipWhitespace(value, pos); + result.add(new Challenge(scheme, realm)); + } + } + return result; + } + + /** + * Returns the next index in {@code input} at or after {@code pos} that + * contains a character from {@code characters}. Returns the input length if + * none of the requested characters can be found. + */ + private static int skipUntil(String input, int pos, String characters) { + for (; pos < input.length(); pos++) { + if (characters.indexOf(input.charAt(pos)) != -1) { + break; + } + } + return pos; + } + + /** + * Returns the next non-whitespace character in {@code input} that is white + * space. Result is undefined if input contains newline characters. + */ + private static int skipWhitespace(String input, int pos) { + for (; pos < input.length(); pos++) { + char c = input.charAt(pos); + if (c != ' ' && c != '\t') { + break; + } + } + return pos; + } + + /** + * Returns {@code value} as a positive integer, or 0 if it is negative, or + * -1 if it cannot be parsed. + */ + public static int parseSeconds(String value) { + try { + long seconds = Long.parseLong(value); + if (seconds > Integer.MAX_VALUE) { + return Integer.MAX_VALUE; + } else if (seconds < 0) { + return 0; + } else { + return (int) seconds; + } + } catch (NumberFormatException e) { + return -1; + } + } +} diff --git a/src/main/java/libcore/net/http/HttpConnection.java b/src/main/java/libcore/net/http/HttpConnection.java new file mode 100644 index 000000000000..c3fb2a9d52df --- /dev/null +++ b/src/main/java/libcore/net/http/HttpConnection.java @@ -0,0 +1,374 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package libcore.net.http; + +import java.io.BufferedInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.Proxy; +import java.net.ProxySelector; +import java.net.Socket; +import java.net.SocketAddress; +import java.net.SocketException; +import java.net.URI; +import java.net.UnknownHostException; +import java.util.Arrays; +import java.util.List; +import javax.net.ssl.HostnameVerifier; +import javax.net.ssl.SSLSocket; +import javax.net.ssl.SSLSocketFactory; +import libcore.io.IoUtils; +import libcore.net.spdy.SpdyConnection; +import libcore.util.Charsets; +import libcore.util.Libcore; +import libcore.util.Objects; + +/** + * Holds the sockets and streams of an HTTP, HTTPS, or HTTPS+SPDY connection, + * which may be used for multiple HTTP request/response exchanges. Connections + * may be direct to the origin server or via a proxy. Create an instance using + * the {@link Address} inner class. + * + *

    Do not confuse this class with the misnamed {@code HttpURLConnection}, + * which isn't so much a connection as a single request/response pair. + */ +final class HttpConnection { + private static final byte[] NPN_PROTOCOLS = new byte[] { + 6, 's', 'p', 'd', 'y', '/', '2', + 8, 'h', 't', 't', 'p', '/', '1', '.', '1', + }; + private static final byte[] SPDY2 = new byte[] { + 's', 'p', 'd', 'y', '/', '2', + }; + private static final byte[] HTTP_11 = new byte[] { + 'h', 't', 't', 'p', '/', '1', '.', '1', + }; + + private final Address address; + private final Socket socket; + private InputStream inputStream; + private OutputStream outputStream; + private SSLSocket sslSocket; + private InputStream sslInputStream; + private OutputStream sslOutputStream; + private boolean recycled = false; + private SpdyConnection spdyConnection; + + /** + * The version this client will use. Either 0 for HTTP/1.0, or 1 for + * HTTP/1.1. Upon receiving a non-HTTP/1.1 response, this client + * automatically sets its version to HTTP/1.0. + */ + int httpMinorVersion = 1; // Assume HTTP/1.1 + + private HttpConnection(Address config, int connectTimeout) throws IOException { + this.address = config; + + /* + * Try each of the host's addresses for best behavior in mixed IPv4/IPv6 + * environments. See http://b/2876927 + * TODO: add a hidden method so that Socket.tryAllAddresses can does this for us + */ + Socket socketCandidate = null; + InetAddress[] addresses = InetAddress.getAllByName(config.socketHost); + for (int i = 0; i < addresses.length; i++) { + socketCandidate = (config.proxy != null && config.proxy.type() != Proxy.Type.HTTP) + ? new Socket(config.proxy) + : new Socket(); + try { + socketCandidate.connect( + new InetSocketAddress(addresses[i], config.socketPort), connectTimeout); + break; + } catch (IOException e) { + if (i == addresses.length - 1) { + throw e; + } + } + } + + if (socketCandidate == null) { + throw new IOException(); + } + + this.socket = socketCandidate; + + /* + * Buffer the socket stream to permit efficient parsing of HTTP headers + * and chunk sizes. Benchmarks suggest 128 is sufficient. We cannot + * buffer when setting up a tunnel because we may consume bytes intended + * for the SSL socket. + */ + int bufferSize = 128; + inputStream = address.requiresTunnel + ? socket.getInputStream() + : new BufferedInputStream(socket.getInputStream(), bufferSize); + outputStream = socket.getOutputStream(); + } + + public static HttpConnection connect(URI uri, SSLSocketFactory sslSocketFactory, + Proxy proxy, boolean requiresTunnel, int connectTimeout) throws IOException { + /* + * Try an explicitly-specified proxy. + */ + if (proxy != null) { + Address address = (proxy.type() == Proxy.Type.DIRECT) + ? new Address(uri, sslSocketFactory) + : new Address(uri, sslSocketFactory, proxy, requiresTunnel); + return HttpConnectionPool.INSTANCE.get(address, connectTimeout); + } + + /* + * Try connecting to each of the proxies provided by the ProxySelector + * until a connection succeeds. + */ + ProxySelector selector = ProxySelector.getDefault(); + List proxyList = selector.select(uri); + if (proxyList != null) { + for (Proxy selectedProxy : proxyList) { + if (selectedProxy.type() == Proxy.Type.DIRECT) { + // the same as NO_PROXY + // TODO: if the selector recommends a direct connection, attempt that? + continue; + } + try { + Address address = new Address(uri, sslSocketFactory, + selectedProxy, requiresTunnel); + return HttpConnectionPool.INSTANCE.get(address, connectTimeout); + } catch (IOException e) { + // failed to connect, tell it to the selector + selector.connectFailed(uri, selectedProxy.address(), e); + } + } + } + + /* + * Try a direct connection. If this fails, this method will throw. + */ + return HttpConnectionPool.INSTANCE.get(new Address(uri, sslSocketFactory), connectTimeout); + } + + public void closeSocketAndStreams() { + IoUtils.closeQuietly(sslOutputStream); + IoUtils.closeQuietly(sslInputStream); + IoUtils.closeQuietly(sslSocket); + IoUtils.closeQuietly(outputStream); + IoUtils.closeQuietly(inputStream); + IoUtils.closeQuietly(socket); + } + + public void setSoTimeout(int readTimeout) throws SocketException { + socket.setSoTimeout(readTimeout); + } + + Socket getSocket() { + return sslSocket != null ? sslSocket : socket; + } + + public Address getAddress() { + return address; + } + + /** + * Create an {@code SSLSocket} and perform the SSL handshake + * (performing certificate validation. + * + * @param sslSocketFactory Source of new {@code SSLSocket} instances. + * @param tlsTolerant If true, assume server can handle common + */ + public SSLSocket setupSecureSocket(SSLSocketFactory sslSocketFactory, + HostnameVerifier hostnameVerifier, boolean tlsTolerant) throws IOException { + if (spdyConnection != null || sslOutputStream != null || sslInputStream != null) { + throw new IllegalStateException(); + } + + // Create the wrapper over connected socket. + sslSocket = (SSLSocket) sslSocketFactory.createSocket(socket, + address.uriHost, address.uriPort, true /* autoClose */); + Libcore.makeTlsTolerant(sslSocket, address.socketHost, tlsTolerant); + + if (tlsTolerant) { + Libcore.setNpnProtocols(sslSocket, NPN_PROTOCOLS); + } + + // Force handshake. This can throw! + sslSocket.startHandshake(); + + // Verify that the socket's certificates are acceptable for the target host. + if (!hostnameVerifier.verify(address.uriHost, sslSocket.getSession())) { + throw new IOException("Hostname '" + address.uriHost + "' was not verified"); + } + + // SSL success. Prepare to hand out Transport instances. + sslOutputStream = sslSocket.getOutputStream(); + sslInputStream = sslSocket.getInputStream(); + + byte[] selectedProtocol; + if (tlsTolerant + && (selectedProtocol = Libcore.getNpnSelectedProtocol(sslSocket)) != null) { + if (Arrays.equals(selectedProtocol, SPDY2)) { + spdyConnection = new SpdyConnection.Builder( + true, sslInputStream, sslOutputStream).build(); + HttpConnectionPool.INSTANCE.share(this); + } else if (!Arrays.equals(selectedProtocol, HTTP_11)) { + throw new IOException("Unexpected NPN transport " + + new String(selectedProtocol, Charsets.ISO_8859_1)); + } + } + + return sslSocket; + } + + /** + * Return an {@code SSLSocket} if already connected, otherwise null. + */ + public SSLSocket getSecureSocketIfConnected() { + return sslSocket; + } + + /** + * Returns true if this connection has been used to satisfy an earlier + * HTTP request/response pair. + */ + public boolean isRecycled() { + return recycled; + } + + public void setRecycled() { + this.recycled = true; + } + + /** + * Returns true if this connection is eligible to be reused for another + * request/response pair. + */ + protected boolean isEligibleForRecycling() { + return !socket.isClosed() + && !socket.isInputShutdown() + && !socket.isOutputShutdown(); + } + + /** + * Returns the transport appropriate for this connection. + */ + public Transport newTransport(HttpEngine httpEngine) throws IOException { + if (spdyConnection != null) { + return new SpdyTransport(httpEngine, spdyConnection); + } else if (sslSocket != null) { + return new HttpTransport(httpEngine, sslOutputStream, sslInputStream); + } else { + return new HttpTransport(httpEngine, outputStream, inputStream); + } + } + + /** + * Returns true if this is a SPDY connection. Such connections can be used + * in multiple HTTP requests simultaneously. + */ + public boolean isSpdy() { + return spdyConnection != null; + } + + /** + * This address has two parts: the address we connect to directly and the + * origin address of the resource. These are the same unless a proxy is + * being used. It also includes the SSL socket factory so that a socket will + * not be reused if its SSL configuration is different. + */ + public static final class Address { + private final Proxy proxy; + private final boolean requiresTunnel; + private final String uriHost; + private final int uriPort; + private final String socketHost; + private final int socketPort; + private final SSLSocketFactory sslSocketFactory; + + public Address(URI uri, SSLSocketFactory sslSocketFactory) throws UnknownHostException { + this.proxy = null; + this.requiresTunnel = false; + this.uriHost = uri.getHost(); + this.uriPort = Libcore.getEffectivePort(uri); + this.sslSocketFactory = sslSocketFactory; + this.socketHost = uriHost; + this.socketPort = uriPort; + if (uriHost == null) { + throw new UnknownHostException(uri.toString()); + } + } + + /** + * @param requiresTunnel true if the HTTP connection needs to tunnel one + * protocol over another, such as when using HTTPS through an HTTP + * proxy. When doing so, we must avoid buffering bytes intended for + * the higher-level protocol. + */ + public Address(URI uri, SSLSocketFactory sslSocketFactory, + Proxy proxy, boolean requiresTunnel) throws UnknownHostException { + this.proxy = proxy; + this.requiresTunnel = requiresTunnel; + this.uriHost = uri.getHost(); + this.uriPort = Libcore.getEffectivePort(uri); + this.sslSocketFactory = sslSocketFactory; + + SocketAddress proxyAddress = proxy.address(); + if (!(proxyAddress instanceof InetSocketAddress)) { + throw new IllegalArgumentException("Proxy.address() is not an InetSocketAddress: " + + proxyAddress.getClass()); + } + InetSocketAddress proxySocketAddress = (InetSocketAddress) proxyAddress; + this.socketHost = proxySocketAddress.getHostName(); + this.socketPort = proxySocketAddress.getPort(); + if (uriHost == null) { + throw new UnknownHostException(uri.toString()); + } + } + + public Proxy getProxy() { + return proxy; + } + + @Override public boolean equals(Object other) { + if (other instanceof Address) { + Address that = (Address) other; + return Objects.equal(this.proxy, that.proxy) + && this.uriHost.equals(that.uriHost) + && this.uriPort == that.uriPort + && Objects.equal(this.sslSocketFactory, that.sslSocketFactory) + && this.requiresTunnel == that.requiresTunnel; + } + return false; + } + + @Override public int hashCode() { + int result = 17; + result = 31 * result + uriHost.hashCode(); + result = 31 * result + uriPort; + result = 31 * result + (sslSocketFactory != null ? sslSocketFactory.hashCode() : 0); + result = 31 * result + (proxy != null ? proxy.hashCode() : 0); + result = 31 * result + (requiresTunnel ? 1 : 0); + return result; + } + + public HttpConnection connect(int connectTimeout) throws IOException { + return new HttpConnection(this, connectTimeout); + } + } +} diff --git a/src/main/java/libcore/net/http/HttpConnectionPool.java b/src/main/java/libcore/net/http/HttpConnectionPool.java new file mode 100644 index 000000000000..490c98adcb28 --- /dev/null +++ b/src/main/java/libcore/net/http/HttpConnectionPool.java @@ -0,0 +1,152 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package libcore.net.http; + +import java.io.IOException; +import java.net.Socket; +import java.net.SocketException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import libcore.util.Libcore; + +/** + * A pool of HTTP and SPDY connections. This class exposes its tuning parameters + * as system properties: + *

    + * + *

    This class doesn't adjust its configuration as system properties + * are changed. This assumes that the applications that set these parameters do + * so before making HTTP connections, and that this class is initialized lazily. + */ +final class HttpConnectionPool { + public static final HttpConnectionPool INSTANCE = new HttpConnectionPool(); + + private final int maxConnections; + private final HashMap> connectionPool + = new HashMap>(); + + private HttpConnectionPool() { + String keepAlive = System.getProperty("http.keepAlive"); + if (keepAlive != null && !Boolean.parseBoolean(keepAlive)) { + maxConnections = 0; + return; + } + + String maxConnectionsString = System.getProperty("http.maxConnections"); + this.maxConnections = maxConnectionsString != null + ? Integer.parseInt(maxConnectionsString) + : 5; + } + + public HttpConnection get(HttpConnection.Address address, int connectTimeout) + throws IOException { + // First try to reuse an existing HTTP connection. + synchronized (connectionPool) { + List connections = connectionPool.get(address); + while (connections != null) { + HttpConnection connection = connections.get(connections.size() - 1); + if (!connection.isSpdy()) { + connections.remove(connections.size() - 1); + } + if (connections.isEmpty()) { + connectionPool.remove(address); + connections = null; + } + if (connection.isEligibleForRecycling()) { + // Since Socket is recycled, re-tag before using + Socket socket = connection.getSocket(); + Libcore.tagSocket(socket); + return connection; + } + } + } + + /* + * We couldn't find a reusable connection, so we need to create a new + * connection. We're careful not to do so while holding a lock! + */ + return address.connect(connectTimeout); + } + + /** + * Gives the HTTP/HTTPS connection to the pool. It is an error to use {@code + * connection} after calling this method. + */ + public void recycle(HttpConnection connection) { + if (connection.isSpdy()) { + throw new IllegalArgumentException(); + } + + Socket socket = connection.getSocket(); + try { + Libcore.untagSocket(socket); + } catch (SocketException e) { + // When unable to remove tagging, skip recycling and close + Libcore.logW("Unable to untagSocket(): " + e); + connection.closeSocketAndStreams(); + return; + } + + if (maxConnections > 0 && connection.isEligibleForRecycling()) { + HttpConnection.Address address = connection.getAddress(); + synchronized (connectionPool) { + List connections = connectionPool.get(address); + if (connections == null) { + connections = new ArrayList(); + connectionPool.put(address, connections); + } + if (connections.size() < maxConnections) { + connection.setRecycled(); + connections.add(connection); + return; // keep the connection open + } + } + } + + // don't close streams while holding a lock! + connection.closeSocketAndStreams(); + } + + /** + * Shares the SPDY connection with the pool. Callers to this method may + * continue to use {@code connection}. + */ + public void share(HttpConnection connection) { + if (!connection.isSpdy()) { + throw new IllegalArgumentException(); + } + if (maxConnections <= 0 || !connection.isEligibleForRecycling()) { + return; + } + HttpConnection.Address address = connection.getAddress(); + synchronized (connectionPool) { + List connections = connectionPool.get(address); + if (connections == null) { + connections = new ArrayList(1); + connections.add(connection); + connectionPool.put(address, connections); + } + } + } +} diff --git a/src/main/java/libcore/net/http/HttpDate.java b/src/main/java/libcore/net/http/HttpDate.java new file mode 100644 index 000000000000..a41cf8193fdd --- /dev/null +++ b/src/main/java/libcore/net/http/HttpDate.java @@ -0,0 +1,91 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package libcore.net.http; + +import java.text.DateFormat; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.Locale; +import java.util.TimeZone; + +/** + * Best-effort parser for HTTP dates. + */ +public final class HttpDate { + + /** + * Most websites serve cookies in the blessed format. Eagerly create the parser to ensure such + * cookies are on the fast path. + */ + private static final ThreadLocal STANDARD_DATE_FORMAT + = new ThreadLocal() { + @Override protected DateFormat initialValue() { + DateFormat rfc1123 = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss zzz", Locale.US); + rfc1123.setTimeZone(TimeZone.getTimeZone("UTC")); + return rfc1123; + } + }; + + /** + * If we fail to parse a date in a non-standard format, try each of these formats in sequence. + */ + private static final String[] BROWSER_COMPATIBLE_DATE_FORMATS = new String[] { + /* This list comes from {@code org.apache.http.impl.cookie.BrowserCompatSpec}. */ + "EEEE, dd-MMM-yy HH:mm:ss zzz", // RFC 1036 + "EEE MMM d HH:mm:ss yyyy", // ANSI C asctime() + "EEE, dd-MMM-yyyy HH:mm:ss z", + "EEE, dd-MMM-yyyy HH-mm-ss z", + "EEE, dd MMM yy HH:mm:ss z", + "EEE dd-MMM-yyyy HH:mm:ss z", + "EEE dd MMM yyyy HH:mm:ss z", + "EEE dd-MMM-yyyy HH-mm-ss z", + "EEE dd-MMM-yy HH:mm:ss z", + "EEE dd MMM yy HH:mm:ss z", + "EEE,dd-MMM-yy HH:mm:ss z", + "EEE,dd-MMM-yyyy HH:mm:ss z", + "EEE, dd-MM-yyyy HH:mm:ss z", + + /* RI bug 6641315 claims a cookie of this format was once served by www.yahoo.com */ + "EEE MMM d yyyy HH:mm:ss z", + }; + + /** + * Returns the date for {@code value}. Returns null if the value couldn't be + * parsed. + */ + public static Date parse(String value) { + try { + return STANDARD_DATE_FORMAT.get().parse(value); + } catch (ParseException ignore) { + } + for (String formatString : BROWSER_COMPATIBLE_DATE_FORMATS) { + try { + return new SimpleDateFormat(formatString, Locale.US).parse(value); + } catch (ParseException ignore) { + } + } + return null; + } + + /** + * Returns the string for {@code value}. + */ + public static String format(Date value) { + return STANDARD_DATE_FORMAT.get().format(value); + } +} diff --git a/src/main/java/libcore/net/http/HttpEngine.java b/src/main/java/libcore/net/http/HttpEngine.java new file mode 100644 index 000000000000..6fd15a422c12 --- /dev/null +++ b/src/main/java/libcore/net/http/HttpEngine.java @@ -0,0 +1,640 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package libcore.net.http; + +import com.squareup.okhttp.OkHttpConnection; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.CacheRequest; +import java.net.CacheResponse; +import java.net.CookieHandler; +import java.net.Proxy; +import java.net.ResponseCache; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URL; +import java.util.Collections; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.zip.GZIPInputStream; +import javax.net.ssl.SSLSocketFactory; +import libcore.io.IoUtils; +import libcore.util.EmptyArray; +import libcore.util.ExtendedResponseCache; +import libcore.util.Libcore; +import libcore.util.ResponseSource; + +/** + * Handles a single HTTP request/response pair. Each HTTP engine follows this + * lifecycle: + *

      + *
    1. It is created. + *
    2. The HTTP request message is sent with sendRequest(). Once the request + * is sent it is an error to modify the request headers. After + * sendRequest() has been called the request body can be written to if + * it exists. + *
    3. The HTTP response message is read with readResponse(). After the + * response has been read the response headers and body can be read. + * All responses have a response body input stream, though in some + * instances this stream is empty. + *
    + * + *

    The request and response may be served by the HTTP response cache, by the + * network, or by both in the event of a conditional GET. + * + *

    This class may hold a socket connection that needs to be released or + * recycled. By default, this socket connection is held when the last byte of + * the response is consumed. To release the connection when it is no longer + * required, use {@link #automaticallyReleaseConnectionToPool()}. + */ +public class HttpEngine { + private static final CacheResponse BAD_GATEWAY_RESPONSE = new CacheResponse() { + @Override public Map> getHeaders() throws IOException { + Map> result = new HashMap>(); + result.put(null, Collections.singletonList("HTTP/1.1 502 Bad Gateway")); + return result; + } + @Override public InputStream getBody() throws IOException { + return new ByteArrayInputStream(EmptyArray.BYTE); + } + }; + public static final int DEFAULT_CHUNK_LENGTH = 1024; + + public static final String OPTIONS = "OPTIONS"; + public static final String GET = "GET"; + public static final String HEAD = "HEAD"; + public static final String POST = "POST"; + public static final String PUT = "PUT"; + public static final String DELETE = "DELETE"; + public static final String TRACE = "TRACE"; + public static final String CONNECT = "CONNECT"; + + public static final int HTTP_CONTINUE = 100; + + protected final HttpURLConnectionImpl policy; + + protected final String method; + + private ResponseSource responseSource; + + protected HttpConnection connection; + private OutputStream requestBodyOut; + + private Transport transport; + + private InputStream responseBodyIn; + + private final ResponseCache responseCache = ResponseCache.getDefault(); + private CacheResponse cacheResponse; + private CacheRequest cacheRequest; + + /** The time when the request headers were written, or -1 if they haven't been written yet. */ + long sentRequestMillis = -1; + + /** + * True if this client added an "Accept-Encoding: gzip" header field and is + * therefore responsible for also decompressing the transfer stream. + */ + private boolean transparentGzip; + + final URI uri; + + final RequestHeaders requestHeaders; + + /** Null until a response is received from the network or the cache */ + ResponseHeaders responseHeaders; + + /* + * The cache response currently being validated on a conditional get. Null + * if the cached response doesn't exist or doesn't need validation. If the + * conditional get succeeds, these will be used for the response headers and + * body. If it fails, these be closed and set to null. + */ + private ResponseHeaders cachedResponseHeaders; + private InputStream cachedResponseBody; + + /** + * True if the socket connection should be released to the connection pool + * when the response has been fully read. + */ + private boolean automaticallyReleaseConnectionToPool; + + /** True if the socket connection is no longer needed by this engine. */ + private boolean connectionReleased; + + /** + * @param requestHeaders the client's supplied request headers. This class + * creates a private copy that it can mutate. + * @param connection the connection used for an intermediate response + * immediately prior to this request/response pair, such as a same-host + * redirect. This engine assumes ownership of the connection and must + * release it when it is unneeded. + */ + public HttpEngine(HttpURLConnectionImpl policy, String method, RawHeaders requestHeaders, + HttpConnection connection, RetryableOutputStream requestBodyOut) throws IOException { + this.policy = policy; + this.method = method; + this.connection = connection; + this.requestBodyOut = requestBodyOut; + + try { + uri = Libcore.toUriLenient(policy.getURL()); + } catch (URISyntaxException e) { + throw new IOException(e); + } + + this.requestHeaders = new RequestHeaders(uri, new RawHeaders(requestHeaders)); + } + + public URI getUri() { + return uri; + } + + /** + * Figures out what the response source will be, and opens a socket to that + * source if necessary. Prepares the request headers and gets ready to start + * writing the request body if it exists. + */ + public final void sendRequest() throws IOException { + if (responseSource != null) { + return; + } + + prepareRawRequestHeaders(); + initResponseSource(); + if (responseCache instanceof ExtendedResponseCache) { + ((ExtendedResponseCache) responseCache).trackResponse(responseSource); + } + + /* + * The raw response source may require the network, but the request + * headers may forbid network use. In that case, dispose of the network + * response and use a BAD_GATEWAY response instead. + */ + if (requestHeaders.isOnlyIfCached() && responseSource.requiresConnection()) { + if (responseSource == ResponseSource.CONDITIONAL_CACHE) { + IoUtils.closeQuietly(cachedResponseBody); + } + this.responseSource = ResponseSource.CACHE; + this.cacheResponse = BAD_GATEWAY_RESPONSE; + RawHeaders rawResponseHeaders = RawHeaders.fromMultimap(cacheResponse.getHeaders()); + setResponse(new ResponseHeaders(uri, rawResponseHeaders), cacheResponse.getBody()); + } + + if (responseSource.requiresConnection()) { + sendSocketRequest(); + } else if (connection != null) { + HttpConnectionPool.INSTANCE.recycle(connection); + connection = null; + } + } + + /** + * Initialize the source for this response. It may be corrected later if the + * request headers forbids network use. + */ + private void initResponseSource() throws IOException { + responseSource = ResponseSource.NETWORK; + if (!policy.getUseCaches() || responseCache == null) { + return; + } + + CacheResponse candidate = responseCache.get(uri, method, + requestHeaders.getHeaders().toMultimap()); + if (candidate == null) { + return; + } + + Map> responseHeadersMap = candidate.getHeaders(); + cachedResponseBody = candidate.getBody(); + if (!acceptCacheResponseType(candidate) + || responseHeadersMap == null + || cachedResponseBody == null) { + IoUtils.closeQuietly(cachedResponseBody); + return; + } + + RawHeaders rawResponseHeaders = RawHeaders.fromMultimap(responseHeadersMap); + cachedResponseHeaders = new ResponseHeaders(uri, rawResponseHeaders); + long now = System.currentTimeMillis(); + this.responseSource = cachedResponseHeaders.chooseResponseSource(now, requestHeaders); + if (responseSource == ResponseSource.CACHE) { + this.cacheResponse = candidate; + setResponse(cachedResponseHeaders, cachedResponseBody); + } else if (responseSource == ResponseSource.CONDITIONAL_CACHE) { + this.cacheResponse = candidate; + } else if (responseSource == ResponseSource.NETWORK) { + IoUtils.closeQuietly(cachedResponseBody); + } else { + throw new AssertionError(); + } + } + + private void sendSocketRequest() throws IOException { + if (connection == null) { + connect(); + } + + if (transport != null) { + throw new IllegalStateException(); + } + + transport = connection.newTransport(this); + + if (hasRequestBody() && requestBodyOut == null) { + // Create a request body if we don't have one already. We'll already + // have one if we're retrying a failed POST. + requestBodyOut = transport.createRequestBody(); + } + } + + /** + * Connect to the origin server either directly or via a proxy. + */ + protected void connect() throws IOException { + if (connection == null) { + connection = openSocketConnection(); + } + } + + protected final HttpConnection openSocketConnection() throws IOException { + HttpConnection result = HttpConnection.connect(uri, getSslSocketFactory(), + policy.getProxy(), requiresTunnel(), policy.getConnectTimeout()); + Proxy proxy = result.getAddress().getProxy(); + if (proxy != null) { + policy.setProxy(proxy); + // Add the authority to the request line when we're using a proxy. + requestHeaders.getHeaders().setStatusLine(getRequestLine()); + } + result.setSoTimeout(policy.getReadTimeout()); + return result; + } + + /** + * @param body the response body, or null if it doesn't exist or isn't + * available. + */ + private void setResponse(ResponseHeaders headers, InputStream body) throws IOException { + if (this.responseBodyIn != null) { + throw new IllegalStateException(); + } + this.responseHeaders = headers; + if (body != null) { + initContentStream(body); + } + } + + boolean hasRequestBody() { + return method == POST || method == PUT; + } + + /** + * Returns the request body or null if this request doesn't have a body. + */ + public final OutputStream getRequestBody() { + if (responseSource == null) { + throw new IllegalStateException(); + } + return requestBodyOut; + } + + public final boolean hasResponse() { + return responseHeaders != null; + } + + public final RequestHeaders getRequestHeaders() { + return requestHeaders; + } + + public final ResponseHeaders getResponseHeaders() { + if (responseHeaders == null) { + throw new IllegalStateException(); + } + return responseHeaders; + } + + public final int getResponseCode() { + if (responseHeaders == null) { + throw new IllegalStateException(); + } + return responseHeaders.getHeaders().getResponseCode(); + } + + public final InputStream getResponseBody() { + if (responseHeaders == null) { + throw new IllegalStateException(); + } + return responseBodyIn; + } + + public final CacheResponse getCacheResponse() { + return cacheResponse; + } + + public final HttpConnection getConnection() { + return connection; + } + + public final boolean hasRecycledConnection() { + return connection != null && connection.isRecycled(); + } + + /** + * Returns true if {@code cacheResponse} is of the right type. This + * condition is necessary but not sufficient for the cached response to + * be used. + */ + protected boolean acceptCacheResponseType(CacheResponse cacheResponse) { + return true; + } + + private void maybeCache() throws IOException { + // Are we caching at all? + if (!policy.getUseCaches() || responseCache == null) { + return; + } + + // Should we cache this response for this request? + if (!responseHeaders.isCacheable(requestHeaders)) { + return; + } + + // Offer this request to the cache. + cacheRequest = responseCache.put(uri, getHttpConnectionToCache()); + } + + protected OkHttpConnection getHttpConnectionToCache() { + return policy; + } + + /** + * Cause the socket connection to be released to the connection pool when + * it is no longer needed. If it is already unneeded, it will be pooled + * immediately. Otherwise the connection is held so that redirects can be + * handled by the same connection. + */ + public final void automaticallyReleaseConnectionToPool() { + automaticallyReleaseConnectionToPool = true; + if (connection != null && connectionReleased) { + HttpConnectionPool.INSTANCE.recycle(connection); + connection = null; + } + } + + /** + * Releases this engine so that its resources may be either reused or + * closed. Also call {@link #automaticallyReleaseConnectionToPool} unless + * the connection will be used to follow a redirect. + */ + public final void release(boolean reusable) { + // If the response body comes from the cache, close it. + if (responseBodyIn == cachedResponseBody) { + IoUtils.closeQuietly(responseBodyIn); + } + + if (!connectionReleased && connection != null) { + connectionReleased = true; + + if (!reusable || !transport.makeReusable(requestBodyOut, responseBodyIn)) { + connection.closeSocketAndStreams(); + connection = null; + } else if (automaticallyReleaseConnectionToPool) { + HttpConnectionPool.INSTANCE.recycle(connection); + connection = null; + } + } + } + + private void initContentStream(InputStream transferStream) throws IOException { + if (transparentGzip && responseHeaders.isContentEncodingGzip()) { + /* + * If the response was transparently gzipped, remove the gzip header field + * so clients don't double decompress. http://b/3009828 + */ + responseHeaders.stripContentEncoding(); + responseBodyIn = new GZIPInputStream(transferStream); + } else { + responseBodyIn = transferStream; + } + } + + /** + * Returns true if the response must have a (possibly 0-length) body. + * See RFC 2616 section 4.3. + */ + public final boolean hasResponseBody() { + int responseCode = responseHeaders.getHeaders().getResponseCode(); + + // HEAD requests never yield a body regardless of the response headers. + if (method == HEAD) { + return false; + } + + if (method != CONNECT + && (responseCode < HTTP_CONTINUE || responseCode >= 200) + && responseCode != HttpURLConnectionImpl.HTTP_NO_CONTENT + && responseCode != HttpURLConnectionImpl.HTTP_NOT_MODIFIED) { + return true; + } + + /* + * If the Content-Length or Transfer-Encoding headers disagree with the + * response code, the response is malformed. For best compatibility, we + * honor the headers. + */ + if (responseHeaders.getContentLength() != -1 || responseHeaders.isChunked()) { + return true; + } + + return false; + } + + /** + * Populates requestHeaders with defaults and cookies. + * + *

    This client doesn't specify a default {@code Accept} header because it + * doesn't know what content types the application is interested in. + */ + private void prepareRawRequestHeaders() throws IOException { + requestHeaders.getHeaders().setStatusLine(getRequestLine()); + + if (requestHeaders.getUserAgent() == null) { + requestHeaders.setUserAgent(getDefaultUserAgent()); + } + + if (requestHeaders.getHost() == null) { + requestHeaders.setHost(getOriginAddress(policy.getURL())); + } + + // TODO: this shouldn't be set for SPDY (it's ignored) + if ((connection == null || connection.httpMinorVersion != 0) + && requestHeaders.getConnection() == null) { + requestHeaders.setConnection("Keep-Alive"); + } + + if (requestHeaders.getAcceptEncoding() == null) { + transparentGzip = true; + // TODO: this shouldn't be set for SPDY (it isn't necessary) + requestHeaders.setAcceptEncoding("gzip"); + } + + if (hasRequestBody() && requestHeaders.getContentType() == null) { + requestHeaders.setContentType("application/x-www-form-urlencoded"); + } + + long ifModifiedSince = policy.getIfModifiedSince(); + if (ifModifiedSince != 0) { + requestHeaders.setIfModifiedSince(new Date(ifModifiedSince)); + } + + CookieHandler cookieHandler = CookieHandler.getDefault(); + if (cookieHandler != null) { + requestHeaders.addCookies( + cookieHandler.get(uri, requestHeaders.getHeaders().toMultimap())); + } + } + + /** + * Returns the request status line, like "GET / HTTP/1.1". This is exposed + * to the application by {@link HttpURLConnectionImpl#getHeaderFields}, so + * it needs to be set even if the transport is SPDY. + */ + String getRequestLine() { + String protocol = (connection == null || connection.httpMinorVersion != 0) + ? "HTTP/1.1" + : "HTTP/1.0"; + return method + " " + requestString() + " " + protocol; + } + + private String requestString() { + URL url = policy.getURL(); + if (includeAuthorityInRequestLine()) { + return url.toString(); + } else { + String fileOnly = url.getFile(); + if (fileOnly == null) { + fileOnly = "/"; + } else if (!fileOnly.startsWith("/")) { + fileOnly = "/" + fileOnly; + } + return fileOnly; + } + } + + /** + * Returns true if the request line should contain the full URL with host + * and port (like "GET http://android.com/foo HTTP/1.1") or only the path + * (like "GET /foo HTTP/1.1"). + * + *

    This is non-final because for HTTPS it's never necessary to supply the + * full URL, even if a proxy is in use. + */ + protected boolean includeAuthorityInRequestLine() { + return policy.usingProxy(); + } + + /** + * Returns the SSL configuration for connections created by this engine. + * We cannot reuse HTTPS connections if the socket factory has changed. + */ + protected SSLSocketFactory getSslSocketFactory() { + return null; + } + + protected final String getDefaultUserAgent() { + String agent = System.getProperty("http.agent"); + return agent != null ? agent : ("Java" + System.getProperty("java.version")); + } + + protected final String getOriginAddress(URL url) { + int port = url.getPort(); + String result = url.getHost(); + if (port > 0 && port != policy.getDefaultPort()) { + result = result + ":" + port; + } + return result; + } + + protected boolean requiresTunnel() { + return false; + } + + /** + * Flushes the remaining request header and body, parses the HTTP response + * headers and starts reading the HTTP response body if it exists. + */ + public final void readResponse() throws IOException { + if (hasResponse()) { + return; + } + + if (responseSource == null) { + throw new IllegalStateException("readResponse() without sendRequest()"); + } + + if (!responseSource.requiresConnection()) { + return; + } + + if (sentRequestMillis == -1) { + if (requestBodyOut instanceof RetryableOutputStream) { + int contentLength = ((RetryableOutputStream) requestBodyOut).contentLength(); + requestHeaders.setContentLength(contentLength); + } + transport.writeRequestHeaders(); + } + + if (requestBodyOut != null) { + requestBodyOut.close(); + if (requestBodyOut instanceof RetryableOutputStream) { + transport.writeRequestBody((RetryableOutputStream) requestBodyOut); + } + } + + transport.flushRequest(); + + responseHeaders = transport.readResponseHeaders(); + responseHeaders.setLocalTimestamps(sentRequestMillis, System.currentTimeMillis()); + + if (responseSource == ResponseSource.CONDITIONAL_CACHE) { + if (cachedResponseHeaders.validate(responseHeaders)) { + release(true); + ResponseHeaders combinedHeaders = cachedResponseHeaders.combine(responseHeaders); + setResponse(combinedHeaders, cachedResponseBody); + if (responseCache instanceof ExtendedResponseCache) { + ExtendedResponseCache httpResponseCache = (ExtendedResponseCache) responseCache; + httpResponseCache.trackConditionalCacheHit(); + httpResponseCache.update(cacheResponse, getHttpConnectionToCache()); + } + return; + } else { + IoUtils.closeQuietly(cachedResponseBody); + } + } + + if (hasResponseBody()) { + maybeCache(); // reentrant. this calls into user code which may call back into this! + } + + initContentStream(transport.getTransferStream(cacheRequest)); + } +} diff --git a/src/main/java/libcore/net/http/HttpResponseCache.java b/src/main/java/libcore/net/http/HttpResponseCache.java new file mode 100644 index 000000000000..c7982679a569 --- /dev/null +++ b/src/main/java/libcore/net/http/HttpResponseCache.java @@ -0,0 +1,596 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package libcore.net.http; + +import com.squareup.okhttp.OkHttpConnection; +import com.squareup.okhttp.OkHttpsConnection; +import java.io.BufferedInputStream; +import java.io.BufferedWriter; +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.FilterInputStream; +import java.io.FilterOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.io.Writer; +import java.net.CacheRequest; +import java.net.CacheResponse; +import java.net.ResponseCache; +import java.net.SecureCacheResponse; +import java.net.URI; +import java.net.URLConnection; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.security.Principal; +import java.security.cert.Certificate; +import java.security.cert.CertificateEncodingException; +import java.security.cert.CertificateException; +import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import javax.net.ssl.SSLPeerUnverifiedException; +import libcore.io.Base64; +import libcore.io.DiskLruCache; +import libcore.io.IoUtils; +import libcore.io.Streams; +import libcore.util.Charsets; +import libcore.util.ExtendedResponseCache; +import libcore.util.IntegralToString; +import libcore.util.ResponseSource; + +/** + * Cache responses in a directory on the file system. Most clients should use + * {@code android.net.HttpResponseCache}, the stable, documented front end for + * this. + */ +public final class HttpResponseCache extends ResponseCache implements ExtendedResponseCache { + // TODO: add APIs to iterate the cache? + private static final int VERSION = 201105; + private static final int ENTRY_METADATA = 0; + private static final int ENTRY_BODY = 1; + private static final int ENTRY_COUNT = 2; + + private final DiskLruCache cache; + + /* read and write statistics, all guarded by 'this' */ + private int writeSuccessCount; + private int writeAbortCount; + private int networkCount; + private int hitCount; + private int requestCount; + + public HttpResponseCache(File directory, long maxSize) throws IOException { + cache = DiskLruCache.open(directory, VERSION, ENTRY_COUNT, maxSize); + } + + private String uriToKey(URI uri) { + try { + MessageDigest messageDigest = MessageDigest.getInstance("MD5"); + byte[] md5bytes = messageDigest.digest(uri.toString().getBytes(Charsets.UTF_8)); + return IntegralToString.bytesToHexString(md5bytes, false); + } catch (NoSuchAlgorithmException e) { + throw new AssertionError(e); + } + } + + @Override public CacheResponse get(URI uri, String requestMethod, + Map> requestHeaders) { + String key = uriToKey(uri); + DiskLruCache.Snapshot snapshot; + Entry entry; + try { + snapshot = cache.get(key); + if (snapshot == null) { + return null; + } + entry = new Entry(new BufferedInputStream(snapshot.getInputStream(ENTRY_METADATA))); + } catch (IOException e) { + // Give up because the cache cannot be read. + return null; + } + + if (!entry.matches(uri, requestMethod, requestHeaders)) { + snapshot.close(); + return null; + } + + return entry.isHttps() + ? new EntrySecureCacheResponse(entry, snapshot) + : new EntryCacheResponse(entry, snapshot); + } + + @Override public CacheRequest put(URI uri, URLConnection urlConnection) throws IOException { + if (!(urlConnection instanceof OkHttpConnection)) { + return null; + } + + OkHttpConnection httpConnection = (OkHttpConnection) urlConnection; + String requestMethod = httpConnection.getRequestMethod(); + String key = uriToKey(uri); + + if (requestMethod.equals(HttpEngine.POST) + || requestMethod.equals(HttpEngine.PUT) + || requestMethod.equals(HttpEngine.DELETE)) { + try { + cache.remove(key); + } catch (IOException ignored) { + // The cache cannot be written. + } + return null; + } else if (!requestMethod.equals(HttpEngine.GET)) { + /* + * Don't cache non-GET responses. We're technically allowed to cache + * HEAD requests and some POST requests, but the complexity of doing + * so is high and the benefit is low. + */ + return null; + } + + HttpEngine httpEngine = getHttpEngine(httpConnection); + if (httpEngine == null) { + // Don't cache unless the HTTP implementation is ours. + return null; + } + + ResponseHeaders response = httpEngine.getResponseHeaders(); + if (response.hasVaryAll()) { + return null; + } + + RawHeaders varyHeaders = httpEngine.getRequestHeaders().getHeaders().getAll( + response.getVaryFields()); + Entry entry = new Entry(uri, varyHeaders, httpConnection); + DiskLruCache.Editor editor = null; + try { + editor = cache.edit(key); + if (editor == null) { + return null; + } + entry.writeTo(editor); + return new CacheRequestImpl(editor); + } catch (IOException e) { + abortQuietly(editor); + return null; + } + } + + /** + * Handles a conditional request hit by updating the stored cache response + * with the headers from {@code httpConnection}. The cached response body is + * not updated. If the stored response has changed since {@code + * conditionalCacheHit} was returned, this does nothing. + */ + @Override + public void update(CacheResponse conditionalCacheHit, OkHttpConnection httpConnection) { + HttpEngine httpEngine = getHttpEngine(httpConnection); + URI uri = httpEngine.getUri(); + ResponseHeaders response = httpEngine.getResponseHeaders(); + RawHeaders varyHeaders = httpEngine.getRequestHeaders().getHeaders() + .getAll(response.getVaryFields()); + Entry entry = new Entry(uri, varyHeaders, httpConnection); + DiskLruCache.Snapshot snapshot = (conditionalCacheHit instanceof EntryCacheResponse) + ? ((EntryCacheResponse) conditionalCacheHit).snapshot + : ((EntrySecureCacheResponse) conditionalCacheHit).snapshot; + DiskLruCache.Editor editor = null; + try { + editor = snapshot.edit(); // returns null if snapshot is not current + if (editor != null) { + entry.writeTo(editor); + editor.commit(); + } + } catch (IOException e) { + abortQuietly(editor); + } + } + + private void abortQuietly(DiskLruCache.Editor editor) { + // Give up because the cache cannot be written. + try { + if (editor != null) { + editor.abort(); + } + } catch (IOException ignored) { + } + } + + private HttpEngine getHttpEngine(URLConnection httpConnection) { + if (httpConnection instanceof HttpURLConnectionImpl) { + return ((HttpURLConnectionImpl) httpConnection).getHttpEngine(); + } else if (httpConnection instanceof HttpsURLConnectionImpl) { + return ((HttpsURLConnectionImpl) httpConnection).getHttpEngine(); + } else { + return null; + } + } + + public DiskLruCache getCache() { + return cache; + } + + public synchronized int getWriteAbortCount() { + return writeAbortCount; + } + + public synchronized int getWriteSuccessCount() { + return writeSuccessCount; + } + + public synchronized void trackResponse(ResponseSource source) { + requestCount++; + + switch (source) { + case CACHE: + hitCount++; + break; + case CONDITIONAL_CACHE: + case NETWORK: + networkCount++; + break; + } + } + + public synchronized void trackConditionalCacheHit() { + hitCount++; + } + + public synchronized int getNetworkCount() { + return networkCount; + } + + public synchronized int getHitCount() { + return hitCount; + } + + public synchronized int getRequestCount() { + return requestCount; + } + + private final class CacheRequestImpl extends CacheRequest { + private final DiskLruCache.Editor editor; + private OutputStream cacheOut; + private boolean done; + private OutputStream body; + + public CacheRequestImpl(final DiskLruCache.Editor editor) throws IOException { + this.editor = editor; + this.cacheOut = editor.newOutputStream(ENTRY_BODY); + this.body = new FilterOutputStream(cacheOut) { + @Override public void close() throws IOException { + synchronized (HttpResponseCache.this) { + if (done) { + return; + } + done = true; + writeSuccessCount++; + } + super.close(); + editor.commit(); + } + }; + } + + @Override public void abort() { + synchronized (HttpResponseCache.this) { + if (done) { + return; + } + done = true; + writeAbortCount++; + } + IoUtils.closeQuietly(cacheOut); + try { + editor.abort(); + } catch (IOException ignored) { + } + } + + @Override public OutputStream getBody() throws IOException { + return body; + } + } + + private static final class Entry { + private final String uri; + private final RawHeaders varyHeaders; + private final String requestMethod; + private final RawHeaders responseHeaders; + private final String cipherSuite; + private final Certificate[] peerCertificates; + private final Certificate[] localCertificates; + + /* + * Reads an entry from an input stream. A typical entry looks like this: + * http://google.com/foo + * GET + * 2 + * Accept-Language: fr-CA + * Accept-Charset: UTF-8 + * HTTP/1.1 200 OK + * 3 + * Content-Type: image/png + * Content-Length: 100 + * Cache-Control: max-age=600 + * + * A typical HTTPS file looks like this: + * https://google.com/foo + * GET + * 2 + * Accept-Language: fr-CA + * Accept-Charset: UTF-8 + * HTTP/1.1 200 OK + * 3 + * Content-Type: image/png + * Content-Length: 100 + * Cache-Control: max-age=600 + * + * AES_256_WITH_MD5 + * 2 + * base64-encoded peerCertificate[0] + * base64-encoded peerCertificate[1] + * -1 + * + * The file is newline separated. The first two lines are the URL and + * the request method. Next is the number of HTTP Vary request header + * lines, followed by those lines. + * + * Next is the response status line, followed by the number of HTTP + * response header lines, followed by those lines. + * + * HTTPS responses also contain SSL session information. This begins + * with a blank line, and then a line containing the cipher suite. Next + * is the length of the peer certificate chain. These certificates are + * base64-encoded and appear each on their own line. The next line + * contains the length of the local certificate chain. These + * certificates are also base64-encoded and appear each on their own + * line. A length of -1 is used to encode a null array. + */ + public Entry(InputStream in) throws IOException { + try { + uri = Streams.readAsciiLine(in); + requestMethod = Streams.readAsciiLine(in); + varyHeaders = new RawHeaders(); + int varyRequestHeaderLineCount = readInt(in); + for (int i = 0; i < varyRequestHeaderLineCount; i++) { + varyHeaders.addLine(Streams.readAsciiLine(in)); + } + + responseHeaders = new RawHeaders(); + responseHeaders.setStatusLine(Streams.readAsciiLine(in)); + int responseHeaderLineCount = readInt(in); + for (int i = 0; i < responseHeaderLineCount; i++) { + responseHeaders.addLine(Streams.readAsciiLine(in)); + } + + if (isHttps()) { + String blank = Streams.readAsciiLine(in); + if (!blank.isEmpty()) { + throw new IOException("expected \"\" but was \"" + blank + "\""); + } + cipherSuite = Streams.readAsciiLine(in); + peerCertificates = readCertArray(in); + localCertificates = readCertArray(in); + } else { + cipherSuite = null; + peerCertificates = null; + localCertificates = null; + } + } finally { + in.close(); + } + } + + public Entry(URI uri, RawHeaders varyHeaders, OkHttpConnection httpConnection) { + this.uri = uri.toString(); + this.varyHeaders = varyHeaders; + this.requestMethod = httpConnection.getRequestMethod(); + this.responseHeaders = RawHeaders.fromMultimap(httpConnection.getHeaderFields()); + + if (isHttps()) { + OkHttpsConnection httpsConnection + = (OkHttpsConnection) httpConnection; + cipherSuite = httpsConnection.getCipherSuite(); + Certificate[] peerCertificatesNonFinal = null; + try { + peerCertificatesNonFinal = httpsConnection.getServerCertificates(); + } catch (SSLPeerUnverifiedException ignored) { + } + peerCertificates = peerCertificatesNonFinal; + localCertificates = httpsConnection.getLocalCertificates(); + } else { + cipherSuite = null; + peerCertificates = null; + localCertificates = null; + } + } + + public void writeTo(DiskLruCache.Editor editor) throws IOException { + OutputStream out = editor.newOutputStream(0); + Writer writer = new BufferedWriter(new OutputStreamWriter(out, Charsets.UTF_8)); + + writer.write(uri + '\n'); + writer.write(requestMethod + '\n'); + writer.write(Integer.toString(varyHeaders.length()) + '\n'); + for (int i = 0; i < varyHeaders.length(); i++) { + writer.write(varyHeaders.getFieldName(i) + ": " + + varyHeaders.getValue(i) + '\n'); + } + + writer.write(responseHeaders.getStatusLine() + '\n'); + writer.write(Integer.toString(responseHeaders.length()) + '\n'); + for (int i = 0; i < responseHeaders.length(); i++) { + writer.write(responseHeaders.getFieldName(i) + ": " + + responseHeaders.getValue(i) + '\n'); + } + + if (isHttps()) { + writer.write('\n'); + writer.write(cipherSuite + '\n'); + writeCertArray(writer, peerCertificates); + writeCertArray(writer, localCertificates); + } + writer.close(); + } + + private boolean isHttps() { + return uri.startsWith("https://"); + } + + private int readInt(InputStream in) throws IOException { + String intString = Streams.readAsciiLine(in); + try { + return Integer.parseInt(intString); + } catch (NumberFormatException e) { + throw new IOException("expected an int but was \"" + intString + "\""); + } + } + + private Certificate[] readCertArray(InputStream in) throws IOException { + int length = readInt(in); + if (length == -1) { + return null; + } + try { + CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509"); + Certificate[] result = new Certificate[length]; + for (int i = 0; i < result.length; i++) { + String line = Streams.readAsciiLine(in); + byte[] bytes = Base64.decode(line.getBytes(Charsets.US_ASCII)); + result[i] = certificateFactory.generateCertificate( + new ByteArrayInputStream(bytes)); + } + return result; + } catch (CertificateException e) { + throw new IOException(e); + } + } + + private void writeCertArray(Writer writer, Certificate[] certificates) throws IOException { + if (certificates == null) { + writer.write("-1\n"); + return; + } + try { + writer.write(Integer.toString(certificates.length) + '\n'); + for (Certificate certificate : certificates) { + byte[] bytes = certificate.getEncoded(); + String line = Base64.encode(bytes); + writer.write(line + '\n'); + } + } catch (CertificateEncodingException e) { + throw new IOException(e); + } + } + + public boolean matches(URI uri, String requestMethod, + Map> requestHeaders) { + return this.uri.equals(uri.toString()) + && this.requestMethod.equals(requestMethod) + && new ResponseHeaders(uri, responseHeaders) + .varyMatches(varyHeaders.toMultimap(), requestHeaders); + } + } + + /** + * Returns an input stream that reads the body of a snapshot, closing the + * snapshot when the stream is closed. + */ + private static InputStream newBodyInputStream(final DiskLruCache.Snapshot snapshot) { + return new FilterInputStream(snapshot.getInputStream(ENTRY_BODY)) { + @Override public void close() throws IOException { + snapshot.close(); + super.close(); + } + }; + } + + static class EntryCacheResponse extends CacheResponse { + private final Entry entry; + private final DiskLruCache.Snapshot snapshot; + private final InputStream in; + + public EntryCacheResponse(Entry entry, DiskLruCache.Snapshot snapshot) { + this.entry = entry; + this.snapshot = snapshot; + this.in = newBodyInputStream(snapshot); + } + + @Override public Map> getHeaders() { + return entry.responseHeaders.toMultimap(); + } + + @Override public InputStream getBody() { + return in; + } + } + + static class EntrySecureCacheResponse extends SecureCacheResponse { + private final Entry entry; + private final DiskLruCache.Snapshot snapshot; + private final InputStream in; + + public EntrySecureCacheResponse(Entry entry, DiskLruCache.Snapshot snapshot) { + this.entry = entry; + this.snapshot = snapshot; + this.in = newBodyInputStream(snapshot); + } + + @Override public Map> getHeaders() { + return entry.responseHeaders.toMultimap(); + } + + @Override public InputStream getBody() { + return in; + } + + @Override public String getCipherSuite() { + return entry.cipherSuite; + } + + @Override public List getServerCertificateChain() + throws SSLPeerUnverifiedException { + if (entry.peerCertificates == null || entry.peerCertificates.length == 0) { + throw new SSLPeerUnverifiedException(null); + } + return Arrays.asList(entry.peerCertificates.clone()); + } + + @Override public Principal getPeerPrincipal() throws SSLPeerUnverifiedException { + if (entry.peerCertificates == null || entry.peerCertificates.length == 0) { + throw new SSLPeerUnverifiedException(null); + } + return ((X509Certificate) entry.peerCertificates[0]).getSubjectX500Principal(); + } + + @Override public List getLocalCertificateChain() { + if (entry.localCertificates == null || entry.localCertificates.length == 0) { + return null; + } + return Arrays.asList(entry.localCertificates.clone()); + } + + @Override public Principal getLocalPrincipal() { + if (entry.localCertificates == null || entry.localCertificates.length == 0) { + return null; + } + return ((X509Certificate) entry.localCertificates[0]).getSubjectX500Principal(); + } + } +} diff --git a/src/main/java/libcore/net/http/HttpTransport.java b/src/main/java/libcore/net/http/HttpTransport.java new file mode 100644 index 000000000000..e47f32bdce90 --- /dev/null +++ b/src/main/java/libcore/net/http/HttpTransport.java @@ -0,0 +1,596 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package libcore.net.http; + +import java.io.BufferedOutputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.CacheRequest; +import java.net.CookieHandler; +import java.net.URL; +import libcore.io.Streams; +import libcore.util.Charsets; +import libcore.util.Libcore; + +final class HttpTransport implements Transport { + /** + * The maximum number of bytes to buffer when sending headers and a request + * body. When the headers and body can be sent in a single write, the + * request completes sooner. In one WiFi benchmark, using a large enough + * buffer sped up some uploads by half. + */ + private static final int MAX_REQUEST_BUFFER_LENGTH = 32768; + + private final HttpEngine httpEngine; + private final InputStream socketIn; + private final OutputStream socketOut; + + /** + * This stream buffers the request headers and the request body when their + * combined size is less than MAX_REQUEST_BUFFER_LENGTH. By combining them + * we can save socket writes, which in turn saves a packet transmission. + * This is socketOut if the request size is large or unknown. + */ + private OutputStream requestOut; + + public HttpTransport(HttpEngine httpEngine, + OutputStream outputStream, InputStream inputStream) { + this.httpEngine = httpEngine; + this.socketOut = outputStream; + this.requestOut = outputStream; + this.socketIn = inputStream; + } + + @Override public OutputStream createRequestBody() throws IOException { + boolean chunked = httpEngine.requestHeaders.isChunked(); + if (!chunked + && httpEngine.policy.getChunkLength() > 0 + && httpEngine.connection.httpMinorVersion != 0) { + httpEngine.requestHeaders.setChunked(); + chunked = true; + } + + // Stream a request body of unknown length. + if (chunked) { + int chunkLength = httpEngine.policy.getChunkLength(); + if (chunkLength == -1) { + chunkLength = HttpEngine.DEFAULT_CHUNK_LENGTH; + } + writeRequestHeaders(); + return new ChunkedOutputStream(requestOut, chunkLength); + } + + // Stream a request body of a known length. + int fixedContentLength = httpEngine.policy.getFixedContentLength(); + if (fixedContentLength != -1) { + httpEngine.requestHeaders.setContentLength(fixedContentLength); + writeRequestHeaders(); + return new FixedLengthOutputStream(requestOut, fixedContentLength); + } + + // Buffer a request body of a known length. + int contentLength = httpEngine.requestHeaders.getContentLength(); + if (contentLength != -1) { + writeRequestHeaders(); + return new RetryableOutputStream(contentLength); + } + + // Buffer a request body of an unknown length. Don't write request + // headers until the entire body is ready; otherwise we can't set the + // Content-Length header correctly. + return new RetryableOutputStream(); + } + + @Override public void flushRequest() throws IOException { + requestOut.flush(); + requestOut = socketOut; + } + + @Override public void writeRequestBody(RetryableOutputStream requestBody) throws IOException { + requestBody.writeToSocket(requestOut); + } + + /** + * Prepares the HTTP headers and sends them to the server. + * + *

    For streaming requests with a body, headers must be prepared + * before the output stream has been written to. Otherwise + * the body would need to be buffered! + * + *

    For non-streaming requests with a body, headers must be prepared + * after the output stream has been written to and closed. + * This ensures that the {@code Content-Length} header field receives the + * proper value. + */ + public void writeRequestHeaders() throws IOException { + if (httpEngine.sentRequestMillis != -1) { + throw new IllegalStateException(); + } + httpEngine.sentRequestMillis = System.currentTimeMillis(); + + int contentLength = httpEngine.requestHeaders.getContentLength(); + RawHeaders headersToSend = getNetworkRequestHeaders(); + byte[] bytes = headersToSend.toHeaderString().getBytes(Charsets.ISO_8859_1); + + if (contentLength != -1 && bytes.length + contentLength <= MAX_REQUEST_BUFFER_LENGTH) { + requestOut = new BufferedOutputStream(socketOut, bytes.length + contentLength); + } + + requestOut.write(bytes); + } + + private RawHeaders getNetworkRequestHeaders() { + return httpEngine.method == HttpEngine.CONNECT + ? getTunnelNetworkRequestHeaders() + : httpEngine.requestHeaders.getHeaders(); + } + + /** + * If we're establishing an HTTPS tunnel with CONNECT (RFC 2817 5.2), send + * only the minimum set of headers. This avoids sending potentially + * sensitive data like HTTP cookies to the proxy unencrypted. + */ + private RawHeaders getTunnelNetworkRequestHeaders() { + RequestHeaders privateHeaders = httpEngine.requestHeaders; + URL url = httpEngine.policy.getURL(); + + RawHeaders result = new RawHeaders(); + result.setStatusLine("CONNECT " + url.getHost() + ":" + Libcore.getEffectivePort(url) + + " HTTP/1.1"); + + // Always set Host and User-Agent. + String host = privateHeaders.getHost(); + if (host == null) { + host = httpEngine.getOriginAddress(url); + } + result.set("Host", host); + + String userAgent = privateHeaders.getUserAgent(); + if (userAgent == null) { + userAgent = httpEngine.getDefaultUserAgent(); + } + result.set("User-Agent", userAgent); + + // Copy over the Proxy-Authorization header if it exists. + String proxyAuthorization = privateHeaders.getProxyAuthorization(); + if (proxyAuthorization != null) { + result.set("Proxy-Authorization", proxyAuthorization); + } + + // Always set the Proxy-Connection to Keep-Alive for the benefit of + // HTTP/1.0 proxies like Squid. + result.set("Proxy-Connection", "Keep-Alive"); + return result; + } + + @Override public ResponseHeaders readResponseHeaders() throws IOException { + RawHeaders headers; + do { + headers = new RawHeaders(); + headers.setStatusLine(Streams.readAsciiLine(socketIn)); + httpEngine.connection.httpMinorVersion = headers.getHttpMinorVersion(); + readHeaders(headers); + } while (headers.getResponseCode() == HttpEngine.HTTP_CONTINUE); + return new ResponseHeaders(httpEngine.uri, headers); + } + + /** + * Reads headers or trailers and updates the cookie store. + */ + private void readHeaders(RawHeaders headers) throws IOException { + // parse the result headers until the first blank line + String line; + while (!(line = Streams.readAsciiLine(socketIn)).isEmpty()) { + headers.addLine(line); + } + + CookieHandler cookieHandler = CookieHandler.getDefault(); + if (cookieHandler != null) { + cookieHandler.put(httpEngine.uri, headers.toMultimap()); + } + } + + public boolean makeReusable(OutputStream requestBodyOut, InputStream responseBodyIn) { + // We cannot reuse sockets that have incomplete output. + if (requestBodyOut != null && !((AbstractHttpOutputStream) requestBodyOut).closed) { + return false; + } + + // If the headers specify that the connection shouldn't be reused, don't reuse it. + if (httpEngine.requestHeaders.hasConnectionClose() + || (httpEngine.responseHeaders != null + && httpEngine.responseHeaders.hasConnectionClose())) { + return false; + } + + if (responseBodyIn instanceof UnknownLengthHttpInputStream) { + return false; + } + + if (responseBodyIn != null) { + // Discard the response body before the connection can be reused. + try { + Streams.skipAll(responseBodyIn); + } catch (IOException e) { + return false; + } + } + + return true; + } + + @Override public InputStream getTransferStream(CacheRequest cacheRequest) throws IOException { + if (!httpEngine.hasResponseBody()) { + return new FixedLengthInputStream(socketIn, cacheRequest, httpEngine, 0); + } + + if (httpEngine.responseHeaders.isChunked()) { + return new ChunkedInputStream(socketIn, cacheRequest, this); + } + + if (httpEngine.responseHeaders.getContentLength() != -1) { + return new FixedLengthInputStream(socketIn, cacheRequest, httpEngine, + httpEngine.responseHeaders.getContentLength()); + } + + /* + * Wrap the input stream from the HttpConnection (rather than + * just returning "socketIn" directly here), so that we can control + * its use after the reference escapes. + */ + return new UnknownLengthHttpInputStream(socketIn, cacheRequest, httpEngine); + } + + /** + * An HTTP body with a fixed length known in advance. + */ + private static class FixedLengthOutputStream extends AbstractHttpOutputStream { + private final OutputStream socketOut; + private int bytesRemaining; + + private FixedLengthOutputStream(OutputStream socketOut, int bytesRemaining) { + this.socketOut = socketOut; + this.bytesRemaining = bytesRemaining; + } + + @Override public void write(byte[] buffer, int offset, int count) throws IOException { + checkNotClosed(); + Libcore.checkOffsetAndCount(buffer.length, offset, count); + if (count > bytesRemaining) { + throw new IOException("expected " + bytesRemaining + + " bytes but received " + count); + } + socketOut.write(buffer, offset, count); + bytesRemaining -= count; + } + + @Override public void flush() throws IOException { + if (closed) { + return; // don't throw; this stream might have been closed on the caller's behalf + } + socketOut.flush(); + } + + @Override public void close() throws IOException { + if (closed) { + return; + } + closed = true; + if (bytesRemaining > 0) { + throw new IOException("unexpected end of stream"); + } + } + } + + /** + * An HTTP body with alternating chunk sizes and chunk bodies. Chunks are + * buffered until {@code maxChunkLength} bytes are ready, at which point the + * chunk is written and the buffer is cleared. + */ + private static class ChunkedOutputStream extends AbstractHttpOutputStream { + private static final byte[] CRLF = { '\r', '\n' }; + private static final byte[] HEX_DIGITS = { + '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f' + }; + private static final byte[] FINAL_CHUNK = new byte[] { '0', '\r', '\n', '\r', '\n' }; + + /** Scratch space for up to 8 hex digits, and then a constant CRLF */ + private final byte[] hex = { 0, 0, 0, 0, 0, 0, 0, 0, '\r', '\n' }; + + private final OutputStream socketOut; + private final int maxChunkLength; + private final ByteArrayOutputStream bufferedChunk; + + private ChunkedOutputStream(OutputStream socketOut, int maxChunkLength) { + this.socketOut = socketOut; + this.maxChunkLength = Math.max(1, dataLength(maxChunkLength)); + this.bufferedChunk = new ByteArrayOutputStream(maxChunkLength); + } + + /** + * Returns the amount of data that can be transmitted in a chunk whose total + * length (data+headers) is {@code dataPlusHeaderLength}. This is presumably + * useful to match sizes with wire-protocol packets. + */ + private int dataLength(int dataPlusHeaderLength) { + int headerLength = 4; // "\r\n" after the size plus another "\r\n" after the data + for (int i = dataPlusHeaderLength - headerLength; i > 0; i >>= 4) { + headerLength++; + } + return dataPlusHeaderLength - headerLength; + } + + @Override public synchronized void write(byte[] buffer, int offset, int count) + throws IOException { + checkNotClosed(); + Libcore.checkOffsetAndCount(buffer.length, offset, count); + + while (count > 0) { + int numBytesWritten; + + if (bufferedChunk.size() > 0 || count < maxChunkLength) { + // fill the buffered chunk and then maybe write that to the stream + numBytesWritten = Math.min(count, maxChunkLength - bufferedChunk.size()); + // TODO: skip unnecessary copies from buffer->bufferedChunk? + bufferedChunk.write(buffer, offset, numBytesWritten); + if (bufferedChunk.size() == maxChunkLength) { + writeBufferedChunkToSocket(); + } + + } else { + // write a single chunk of size maxChunkLength to the stream + numBytesWritten = maxChunkLength; + writeHex(numBytesWritten); + socketOut.write(buffer, offset, numBytesWritten); + socketOut.write(CRLF); + } + + offset += numBytesWritten; + count -= numBytesWritten; + } + } + + /** + * Equivalent to, but cheaper than writing Integer.toHexString().getBytes() + * followed by CRLF. + */ + private void writeHex(int i) throws IOException { + int cursor = 8; + do { + hex[--cursor] = HEX_DIGITS[i & 0xf]; + } while ((i >>>= 4) != 0); + socketOut.write(hex, cursor, hex.length - cursor); + } + + @Override public synchronized void flush() throws IOException { + if (closed) { + return; // don't throw; this stream might have been closed on the caller's behalf + } + writeBufferedChunkToSocket(); + socketOut.flush(); + } + + @Override public synchronized void close() throws IOException { + if (closed) { + return; + } + closed = true; + writeBufferedChunkToSocket(); + socketOut.write(FINAL_CHUNK); + } + + private void writeBufferedChunkToSocket() throws IOException { + int size = bufferedChunk.size(); + if (size <= 0) { + return; + } + + writeHex(size); + bufferedChunk.writeTo(socketOut); + bufferedChunk.reset(); + socketOut.write(CRLF); + } + } + + /** + * An HTTP body with a fixed length specified in advance. + */ + private static class FixedLengthInputStream extends AbstractHttpInputStream { + private int bytesRemaining; + + public FixedLengthInputStream(InputStream is, CacheRequest cacheRequest, + HttpEngine httpEngine, int length) throws IOException { + super(is, httpEngine, cacheRequest); + bytesRemaining = length; + if (bytesRemaining == 0) { + endOfInput(true); + } + } + + @Override public int read(byte[] buffer, int offset, int count) throws IOException { + Libcore.checkOffsetAndCount(buffer.length, offset, count); + checkNotClosed(); + if (bytesRemaining == 0) { + return -1; + } + int read = in.read(buffer, offset, Math.min(count, bytesRemaining)); + if (read == -1) { + unexpectedEndOfInput(); // the server didn't supply the promised content length + throw new IOException("unexpected end of stream"); + } + bytesRemaining -= read; + cacheWrite(buffer, offset, read); + if (bytesRemaining == 0) { + endOfInput(true); + } + return read; + } + + @Override public int available() throws IOException { + checkNotClosed(); + return bytesRemaining == 0 ? 0 : Math.min(in.available(), bytesRemaining); + } + + @Override public void close() throws IOException { + if (closed) { + return; + } + closed = true; + if (bytesRemaining != 0) { + unexpectedEndOfInput(); + } + } + } + + /** + * An HTTP body with alternating chunk sizes and chunk bodies. + */ + private static class ChunkedInputStream extends AbstractHttpInputStream { + private static final int MIN_LAST_CHUNK_LENGTH = "\r\n0\r\n\r\n".length(); + private static final int NO_CHUNK_YET = -1; + private final HttpTransport transport; + private int bytesRemainingInChunk = NO_CHUNK_YET; + private boolean hasMoreChunks = true; + + ChunkedInputStream(InputStream is, CacheRequest cacheRequest, + HttpTransport transport) throws IOException { + super(is, transport.httpEngine, cacheRequest); + this.transport = transport; + } + + @Override public int read(byte[] buffer, int offset, int count) throws IOException { + Libcore.checkOffsetAndCount(buffer.length, offset, count); + checkNotClosed(); + + if (!hasMoreChunks) { + return -1; + } + if (bytesRemainingInChunk == 0 || bytesRemainingInChunk == NO_CHUNK_YET) { + readChunkSize(); + if (!hasMoreChunks) { + return -1; + } + } + int read = in.read(buffer, offset, Math.min(count, bytesRemainingInChunk)); + if (read == -1) { + unexpectedEndOfInput(); // the server didn't supply the promised chunk length + throw new IOException("unexpected end of stream"); + } + bytesRemainingInChunk -= read; + cacheWrite(buffer, offset, read); + + /* + * If we're at the end of a chunk and the next chunk size is readable, + * read it! Reading the last chunk causes the underlying connection to + * be recycled and we want to do that as early as possible. Otherwise + * self-delimiting streams like gzip will never be recycled. + * http://code.google.com/p/android/issues/detail?id=7059 + */ + if (bytesRemainingInChunk == 0 && in.available() >= MIN_LAST_CHUNK_LENGTH) { + readChunkSize(); + } + + return read; + } + + private void readChunkSize() throws IOException { + // read the suffix of the previous chunk + if (bytesRemainingInChunk != NO_CHUNK_YET) { + Streams.readAsciiLine(in); + } + String chunkSizeString = Streams.readAsciiLine(in); + int index = chunkSizeString.indexOf(";"); + if (index != -1) { + chunkSizeString = chunkSizeString.substring(0, index); + } + try { + bytesRemainingInChunk = Integer.parseInt(chunkSizeString.trim(), 16); + } catch (NumberFormatException e) { + throw new IOException("Expected a hex chunk size, but was " + chunkSizeString); + } + if (bytesRemainingInChunk == 0) { + hasMoreChunks = false; + transport.readHeaders(httpEngine.responseHeaders.getHeaders()); + endOfInput(true); + } + } + + @Override public int available() throws IOException { + checkNotClosed(); + if (!hasMoreChunks || bytesRemainingInChunk == NO_CHUNK_YET) { + return 0; + } + return Math.min(in.available(), bytesRemainingInChunk); + } + + @Override public void close() throws IOException { + if (closed) { + return; + } + + closed = true; + if (hasMoreChunks) { + unexpectedEndOfInput(); + } + } + } + + /** + * An HTTP payload terminated by the end of the socket stream. + */ + private static class UnknownLengthHttpInputStream extends AbstractHttpInputStream { + private boolean inputExhausted; + + private UnknownLengthHttpInputStream(InputStream is, CacheRequest cacheRequest, + HttpEngine httpEngine) throws IOException { + super(is, httpEngine, cacheRequest); + } + + @Override public int read(byte[] buffer, int offset, int count) throws IOException { + Libcore.checkOffsetAndCount(buffer.length, offset, count); + checkNotClosed(); + if (in == null || inputExhausted) { + return -1; + } + int read = in.read(buffer, offset, count); + if (read == -1) { + inputExhausted = true; + endOfInput(false); + return -1; + } + cacheWrite(buffer, offset, read); + return read; + } + + @Override public int available() throws IOException { + checkNotClosed(); + return in == null ? 0 : in.available(); + } + + @Override public void close() throws IOException { + if (closed) { + return; + } + closed = true; + if (!inputExhausted) { + unexpectedEndOfInput(); + } + } + } +} diff --git a/src/main/java/libcore/net/http/HttpURLConnectionImpl.java b/src/main/java/libcore/net/http/HttpURLConnectionImpl.java new file mode 100644 index 000000000000..dda9b95eb620 --- /dev/null +++ b/src/main/java/libcore/net/http/HttpURLConnectionImpl.java @@ -0,0 +1,515 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package libcore.net.http; + +import com.squareup.okhttp.OkHttpConnection; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.Authenticator; +import java.net.HttpRetryException; +import java.net.HttpURLConnection; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.PasswordAuthentication; +import java.net.ProtocolException; +import java.net.Proxy; +import java.net.SocketPermission; +import java.net.URL; +import libcore.util.Charsets; +import java.security.Permission; +import java.util.List; +import java.util.Map; +import libcore.io.Base64; +import libcore.util.Libcore; + +/** + * This implementation uses HttpEngine to send requests and receive responses. + * This class may use multiple HttpEngines to follow redirects, authentication + * retries, etc. to retrieve the final response body. + * + *

    What does 'connected' mean?

    + * This class inherits a {@code connected} field from the superclass. That field + * is not used to indicate not whether this URLConnection is + * currently connected. Instead, it indicates whether a connection has ever been + * attempted. Once a connection has been attempted, certain properties (request + * header fields, request method, etc.) are immutable. Test the {@code + * connection} field on this class for null/non-null to determine of an instance + * is currently connected to a server. + */ +public class HttpURLConnectionImpl extends OkHttpConnection { + /** + * HTTP 1.1 doesn't specify how many redirects to follow, but HTTP/1.0 + * recommended 5. http://www.w3.org/Protocols/HTTP/1.0/spec.html#Code3xx + */ + private static final int MAX_REDIRECTS = 5; + + private final int defaultPort; + + private Proxy proxy; + + private final RawHeaders rawRequestHeaders = new RawHeaders(); + + private int redirectionCount; + + protected IOException httpEngineFailure; + protected HttpEngine httpEngine; + + public HttpURLConnectionImpl(URL url, int port) { + super(url); + defaultPort = port; + } + + public HttpURLConnectionImpl(URL url, int port, Proxy proxy) { + this(url, port); + this.proxy = proxy; + } + + @Override public final void connect() throws IOException { + initHttpEngine(); + try { + httpEngine.sendRequest(); + } catch (IOException e) { + httpEngineFailure = e; + throw e; + } + } + + @Override public final void disconnect() { + // Calling disconnect() before a connection exists should have no effect. + if (httpEngine != null) { + httpEngine.release(false); + } + } + + /** + * Returns an input stream from the server in the case of error such as the + * requested file (txt, htm, html) is not found on the remote server. + */ + @Override public final InputStream getErrorStream() { + try { + HttpEngine response = getResponse(); + if (response.hasResponseBody() + && response.getResponseCode() >= HTTP_BAD_REQUEST) { + return response.getResponseBody(); + } + return null; + } catch (IOException e) { + return null; + } + } + + /** + * Returns the value of the field at {@code position}. Returns null if there + * are fewer than {@code position} headers. + */ + @Override public final String getHeaderField(int position) { + try { + return getResponse().getResponseHeaders().getHeaders().getValue(position); + } catch (IOException e) { + return null; + } + } + + /** + * Returns the value of the field corresponding to the {@code fieldName}, or + * null if there is no such field. If the field has multiple values, the + * last value is returned. + */ + @Override public final String getHeaderField(String fieldName) { + try { + RawHeaders rawHeaders = getResponse().getResponseHeaders().getHeaders(); + return fieldName == null + ? rawHeaders.getStatusLine() + : rawHeaders.get(fieldName); + } catch (IOException e) { + return null; + } + } + + @Override public final String getHeaderFieldKey(int position) { + try { + return getResponse().getResponseHeaders().getHeaders().getFieldName(position); + } catch (IOException e) { + return null; + } + } + + @Override public final Map> getHeaderFields() { + try { + return getResponse().getResponseHeaders().getHeaders().toMultimap(); + } catch (IOException e) { + return null; + } + } + + @Override public final Map> getRequestProperties() { + if (connected) { + throw new IllegalStateException( + "Cannot access request header fields after connection is set"); + } + return rawRequestHeaders.toMultimap(); + } + + @Override public final InputStream getInputStream() throws IOException { + if (!doInput) { + throw new ProtocolException("This protocol does not support input"); + } + + HttpEngine response = getResponse(); + + /* + * if the requested file does not exist, throw an exception formerly the + * Error page from the server was returned if the requested file was + * text/html this has changed to return FileNotFoundException for all + * file types + */ + if (getResponseCode() >= HTTP_BAD_REQUEST) { + throw new FileNotFoundException(url.toString()); + } + + InputStream result = response.getResponseBody(); + if (result == null) { + throw new IOException("No response body exists; responseCode=" + getResponseCode()); + } + return result; + } + + @Override public final OutputStream getOutputStream() throws IOException { + connect(); + + OutputStream result = httpEngine.getRequestBody(); + if (result == null) { + throw new ProtocolException("method does not support a request body: " + method); + } else if (httpEngine.hasResponse()) { + throw new ProtocolException("cannot write request body after response has been read"); + } + + return result; + } + + @Override public final Permission getPermission() throws IOException { + String connectToAddress = getConnectToHost() + ":" + getConnectToPort(); + return new SocketPermission(connectToAddress, "connect, resolve"); + } + + private String getConnectToHost() { + return usingProxy() + ? ((InetSocketAddress) proxy.address()).getHostName() + : getURL().getHost(); + } + + private int getConnectToPort() { + int hostPort = usingProxy() + ? ((InetSocketAddress) proxy.address()).getPort() + : getURL().getPort(); + return hostPort < 0 ? getDefaultPort() : hostPort; + } + + @Override public final String getRequestProperty(String field) { + if (field == null) { + return null; + } + return rawRequestHeaders.get(field); + } + + private void initHttpEngine() throws IOException { + if (httpEngineFailure != null) { + throw httpEngineFailure; + } else if (httpEngine != null) { + return; + } + + connected = true; + try { + if (doOutput) { + if (method == HttpEngine.GET) { + // they are requesting a stream to write to. This implies a POST method + method = HttpEngine.POST; + } else if (method != HttpEngine.POST && method != HttpEngine.PUT) { + // If the request method is neither POST nor PUT, then you're not writing + throw new ProtocolException(method + " does not support writing"); + } + } + httpEngine = newHttpEngine(method, rawRequestHeaders, null, null); + } catch (IOException e) { + httpEngineFailure = e; + throw e; + } + } + + /** + * Create a new HTTP engine. This hook method is non-final so it can be + * overridden by HttpsURLConnectionImpl. + */ + protected HttpEngine newHttpEngine(String method, RawHeaders requestHeaders, + HttpConnection connection, RetryableOutputStream requestBody) throws IOException { + return new HttpEngine(this, method, requestHeaders, connection, requestBody); + } + + /** + * Aggressively tries to get the final HTTP response, potentially making + * many HTTP requests in the process in order to cope with redirects and + * authentication. + */ + private HttpEngine getResponse() throws IOException { + initHttpEngine(); + + if (httpEngine.hasResponse()) { + return httpEngine; + } + + while (true) { + try { + httpEngine.sendRequest(); + httpEngine.readResponse(); + } catch (IOException e) { + /* + * If the connection was recycled, its staleness may have caused + * the failure. Silently retry with a different connection. + */ + OutputStream requestBody = httpEngine.getRequestBody(); + if (httpEngine.hasRecycledConnection() + && (requestBody == null || requestBody instanceof RetryableOutputStream)) { + httpEngine.release(false); + httpEngine = newHttpEngine(method, rawRequestHeaders, null, + (RetryableOutputStream) requestBody); + continue; + } + httpEngineFailure = e; + throw e; + } + + Retry retry = processResponseHeaders(); + if (retry == Retry.NONE) { + httpEngine.automaticallyReleaseConnectionToPool(); + return httpEngine; + } + + /* + * The first request was insufficient. Prepare for another... + */ + String retryMethod = method; + OutputStream requestBody = httpEngine.getRequestBody(); + + /* + * Although RFC 2616 10.3.2 specifies that a HTTP_MOVED_PERM + * redirect should keep the same method, Chrome, Firefox and the + * RI all issue GETs when following any redirect. + */ + int responseCode = getResponseCode(); + if (responseCode == HTTP_MULT_CHOICE || responseCode == HTTP_MOVED_PERM + || responseCode == HTTP_MOVED_TEMP || responseCode == HTTP_SEE_OTHER) { + retryMethod = HttpEngine.GET; + requestBody = null; + } + + if (requestBody != null && !(requestBody instanceof RetryableOutputStream)) { + throw new HttpRetryException("Cannot retry streamed HTTP body", + httpEngine.getResponseCode()); + } + + if (retry == Retry.DIFFERENT_CONNECTION) { + httpEngine.automaticallyReleaseConnectionToPool(); + } + + httpEngine.release(true); + + httpEngine = newHttpEngine(retryMethod, rawRequestHeaders, + httpEngine.getConnection(), (RetryableOutputStream) requestBody); + } + } + + HttpEngine getHttpEngine() { + return httpEngine; + } + + enum Retry { + NONE, + SAME_CONNECTION, + DIFFERENT_CONNECTION + } + + /** + * Returns the retry action to take for the current response headers. The + * headers, proxy and target URL or this connection may be adjusted to + * prepare for a follow up request. + */ + private Retry processResponseHeaders() throws IOException { + switch (getResponseCode()) { + case HTTP_PROXY_AUTH: + if (!usingProxy()) { + throw new IOException( + "Received HTTP_PROXY_AUTH (407) code while not using proxy"); + } + // fall-through + case HTTP_UNAUTHORIZED: + boolean credentialsFound = processAuthHeader(getResponseCode(), + httpEngine.getResponseHeaders(), rawRequestHeaders); + return credentialsFound ? Retry.SAME_CONNECTION : Retry.NONE; + + case HTTP_MULT_CHOICE: + case HTTP_MOVED_PERM: + case HTTP_MOVED_TEMP: + case HTTP_SEE_OTHER: + if (!getInstanceFollowRedirects()) { + return Retry.NONE; + } + if (++redirectionCount > MAX_REDIRECTS) { + throw new ProtocolException("Too many redirects"); + } + String location = getHeaderField("Location"); + if (location == null) { + return Retry.NONE; + } + URL previousUrl = url; + url = new URL(previousUrl, location); + if (!previousUrl.getProtocol().equals(url.getProtocol())) { + return Retry.NONE; // the scheme changed; don't retry. + } + if (previousUrl.getHost().equals(url.getHost()) + && Libcore.getEffectivePort(previousUrl) == Libcore.getEffectivePort(url)) { + return Retry.SAME_CONNECTION; + } else { + return Retry.DIFFERENT_CONNECTION; + } + + default: + return Retry.NONE; + } + } + + /** + * React to a failed authorization response by looking up new credentials. + * + * @return true if credentials have been added to successorRequestHeaders + * and another request should be attempted. + */ + final boolean processAuthHeader(int responseCode, ResponseHeaders response, + RawHeaders successorRequestHeaders) throws IOException { + if (responseCode != HTTP_PROXY_AUTH && responseCode != HTTP_UNAUTHORIZED) { + throw new IllegalArgumentException(); + } + + // keep asking for username/password until authorized + String challengeHeader = responseCode == HTTP_PROXY_AUTH + ? "Proxy-Authenticate" + : "WWW-Authenticate"; + String credentials = getAuthorizationCredentials(response.getHeaders(), challengeHeader); + if (credentials == null) { + return false; // could not find credentials, end request cycle + } + + // add authorization credentials, bypassing the already-connected check + String fieldName = responseCode == HTTP_PROXY_AUTH + ? "Proxy-Authorization" + : "Authorization"; + successorRequestHeaders.set(fieldName, credentials); + return true; + } + + /** + * Returns the authorization credentials on the base of provided challenge. + */ + private String getAuthorizationCredentials(RawHeaders responseHeaders, String challengeHeader) + throws IOException { + List challenges = HeaderParser.parseChallenges(responseHeaders, challengeHeader); + if (challenges.isEmpty()) { + throw new IOException("No authentication challenges found"); + } + + for (Challenge challenge : challenges) { + // use the global authenticator to get the password + PasswordAuthentication auth = Authenticator.requestPasswordAuthentication( + getConnectToInetAddress(), getConnectToPort(), url.getProtocol(), + challenge.realm, challenge.scheme); + if (auth == null) { + continue; + } + + // base64 encode the username and password + String usernameAndPassword = auth.getUserName() + ":" + new String(auth.getPassword()); + byte[] bytes = usernameAndPassword.getBytes(Charsets.ISO_8859_1); + String encoded = Base64.encode(bytes); + return challenge.scheme + " " + encoded; + } + + return null; + } + + private InetAddress getConnectToInetAddress() throws IOException { + return usingProxy() + ? ((InetSocketAddress) proxy.address()).getAddress() + : InetAddress.getByName(getURL().getHost()); + } + + final int getDefaultPort() { + return defaultPort; + } + + /** @see HttpURLConnection#setFixedLengthStreamingMode(int) */ + final int getFixedContentLength() { + return fixedContentLength; + } + + /** @see HttpURLConnection#setChunkedStreamingMode(int) */ + final int getChunkLength() { + return chunkLength; + } + + final Proxy getProxy() { + return proxy; + } + + final void setProxy(Proxy proxy) { + this.proxy = proxy; + } + + @Override public final boolean usingProxy() { + return (proxy != null && proxy.type() != Proxy.Type.DIRECT); + } + + @Override public String getResponseMessage() throws IOException { + return getResponse().getResponseHeaders().getHeaders().getResponseMessage(); + } + + @Override public final int getResponseCode() throws IOException { + return getResponse().getResponseCode(); + } + + @Override public final void setRequestProperty(String field, String newValue) { + if (connected) { + throw new IllegalStateException("Cannot set request property after connection is made"); + } + if (field == null) { + throw new NullPointerException("field == null"); + } + rawRequestHeaders.set(field, newValue); + } + + @Override public final void addRequestProperty(String field, String value) { + if (connected) { + throw new IllegalStateException("Cannot add request property after connection is made"); + } + if (field == null) { + throw new NullPointerException("field == null"); + } + rawRequestHeaders.add(field, value); + } +} diff --git a/src/main/java/libcore/net/http/HttpsHandler.java b/src/main/java/libcore/net/http/HttpsHandler.java new file mode 100644 index 000000000000..ed9ba7243749 --- /dev/null +++ b/src/main/java/libcore/net/http/HttpsHandler.java @@ -0,0 +1,42 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package libcore.net.http; + +import java.io.IOException; +import java.net.Proxy; +import java.net.URL; +import java.net.URLConnection; +import java.net.URLStreamHandler; + +public final class HttpsHandler extends URLStreamHandler { + + @Override protected URLConnection openConnection(URL url) throws IOException { + return new HttpsURLConnectionImpl(url, getDefaultPort()); + } + + @Override protected URLConnection openConnection(URL url, Proxy proxy) throws IOException { + if (url == null || proxy == null) { + throw new IllegalArgumentException("url == null || proxy == null"); + } + return new HttpsURLConnectionImpl(url, getDefaultPort(), proxy); + } + + @Override protected int getDefaultPort() { + return 443; + } +} diff --git a/src/main/java/libcore/net/http/HttpsURLConnectionImpl.java b/src/main/java/libcore/net/http/HttpsURLConnectionImpl.java new file mode 100644 index 000000000000..49d8d6f3b350 --- /dev/null +++ b/src/main/java/libcore/net/http/HttpsURLConnectionImpl.java @@ -0,0 +1,535 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package libcore.net.http; + +import com.squareup.okhttp.OkHttpConnection; +import com.squareup.okhttp.OkHttpsConnection; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.CacheResponse; +import java.net.ProtocolException; +import java.net.Proxy; +import java.net.SecureCacheResponse; +import java.net.URL; +import java.security.Permission; +import java.security.Principal; +import java.security.cert.Certificate; +import java.security.cert.CertificateException; +import java.util.List; +import java.util.Map; +import javax.net.ssl.SSLHandshakeException; +import javax.net.ssl.SSLPeerUnverifiedException; +import javax.net.ssl.SSLSocket; +import javax.net.ssl.SSLSocketFactory; + +public final class HttpsURLConnectionImpl extends OkHttpsConnection { + + /** HttpUrlConnectionDelegate allows reuse of HttpURLConnectionImpl */ + private final HttpUrlConnectionDelegate delegate; + + public HttpsURLConnectionImpl(URL url, int port) { + super(url); + delegate = new HttpUrlConnectionDelegate(url, port); + } + + public HttpsURLConnectionImpl(URL url, int port, Proxy proxy) { + super(url); + delegate = new HttpUrlConnectionDelegate(url, port, proxy); + } + + private void checkConnected() { + if (delegate.getSSLSocket() == null) { + throw new IllegalStateException("Connection has not yet been established"); + } + } + + HttpEngine getHttpEngine() { + return delegate.getHttpEngine(); + } + + @Override + public String getCipherSuite() { + SecureCacheResponse cacheResponse = delegate.getCacheResponse(); + if (cacheResponse != null) { + return cacheResponse.getCipherSuite(); + } + checkConnected(); + return delegate.getSSLSocket().getSession().getCipherSuite(); + } + + @Override + public Certificate[] getLocalCertificates() { + SecureCacheResponse cacheResponse = delegate.getCacheResponse(); + if (cacheResponse != null) { + List result = cacheResponse.getLocalCertificateChain(); + return result != null ? result.toArray(new Certificate[result.size()]) : null; + } + checkConnected(); + return delegate.getSSLSocket().getSession().getLocalCertificates(); + } + + @Override + public Certificate[] getServerCertificates() throws SSLPeerUnverifiedException { + SecureCacheResponse cacheResponse = delegate.getCacheResponse(); + if (cacheResponse != null) { + List result = cacheResponse.getServerCertificateChain(); + return result != null ? result.toArray(new Certificate[result.size()]) : null; + } + checkConnected(); + return delegate.getSSLSocket().getSession().getPeerCertificates(); + } + + @Override + public Principal getPeerPrincipal() throws SSLPeerUnverifiedException { + SecureCacheResponse cacheResponse = delegate.getCacheResponse(); + if (cacheResponse != null) { + return cacheResponse.getPeerPrincipal(); + } + checkConnected(); + return delegate.getSSLSocket().getSession().getPeerPrincipal(); + } + + @Override + public Principal getLocalPrincipal() { + SecureCacheResponse cacheResponse = delegate.getCacheResponse(); + if (cacheResponse != null) { + return cacheResponse.getLocalPrincipal(); + } + checkConnected(); + return delegate.getSSLSocket().getSession().getLocalPrincipal(); + } + + @Override + public void disconnect() { + delegate.disconnect(); + } + + @Override + public InputStream getErrorStream() { + return delegate.getErrorStream(); + } + + @Override + public String getRequestMethod() { + return delegate.getRequestMethod(); + } + + @Override + public int getResponseCode() throws IOException { + return delegate.getResponseCode(); + } + + @Override + public String getResponseMessage() throws IOException { + return delegate.getResponseMessage(); + } + + @Override + public void setRequestMethod(String method) throws ProtocolException { + delegate.setRequestMethod(method); + } + + @Override + public boolean usingProxy() { + return delegate.usingProxy(); + } + + @Override + public boolean getInstanceFollowRedirects() { + return delegate.getInstanceFollowRedirects(); + } + + @Override + public void setInstanceFollowRedirects(boolean followRedirects) { + delegate.setInstanceFollowRedirects(followRedirects); + } + + @Override + public void connect() throws IOException { + connected = true; + delegate.connect(); + } + + @Override + public boolean getAllowUserInteraction() { + return delegate.getAllowUserInteraction(); + } + + @Override + public Object getContent() throws IOException { + return delegate.getContent(); + } + + @SuppressWarnings("unchecked") // Spec does not generify + @Override + public Object getContent(Class[] types) throws IOException { + return delegate.getContent(types); + } + + @Override + public String getContentEncoding() { + return delegate.getContentEncoding(); + } + + @Override + public int getContentLength() { + return delegate.getContentLength(); + } + + @Override + public String getContentType() { + return delegate.getContentType(); + } + + @Override + public long getDate() { + return delegate.getDate(); + } + + @Override + public boolean getDefaultUseCaches() { + return delegate.getDefaultUseCaches(); + } + + @Override + public boolean getDoInput() { + return delegate.getDoInput(); + } + + @Override + public boolean getDoOutput() { + return delegate.getDoOutput(); + } + + @Override + public long getExpiration() { + return delegate.getExpiration(); + } + + @Override + public String getHeaderField(int pos) { + return delegate.getHeaderField(pos); + } + + @Override + public Map> getHeaderFields() { + return delegate.getHeaderFields(); + } + + @Override + public Map> getRequestProperties() { + return delegate.getRequestProperties(); + } + + @Override + public void addRequestProperty(String field, String newValue) { + delegate.addRequestProperty(field, newValue); + } + + @Override + public String getHeaderField(String key) { + return delegate.getHeaderField(key); + } + + @Override + public long getHeaderFieldDate(String field, long defaultValue) { + return delegate.getHeaderFieldDate(field, defaultValue); + } + + @Override + public int getHeaderFieldInt(String field, int defaultValue) { + return delegate.getHeaderFieldInt(field, defaultValue); + } + + @Override + public String getHeaderFieldKey(int posn) { + return delegate.getHeaderFieldKey(posn); + } + + @Override + public long getIfModifiedSince() { + return delegate.getIfModifiedSince(); + } + + @Override + public InputStream getInputStream() throws IOException { + return delegate.getInputStream(); + } + + @Override + public long getLastModified() { + return delegate.getLastModified(); + } + + @Override + public OutputStream getOutputStream() throws IOException { + return delegate.getOutputStream(); + } + + @Override + public Permission getPermission() throws IOException { + return delegate.getPermission(); + } + + @Override + public String getRequestProperty(String field) { + return delegate.getRequestProperty(field); + } + + @Override + public URL getURL() { + return delegate.getURL(); + } + + @Override + public boolean getUseCaches() { + return delegate.getUseCaches(); + } + + @Override + public void setAllowUserInteraction(boolean newValue) { + delegate.setAllowUserInteraction(newValue); + } + + @Override + public void setDefaultUseCaches(boolean newValue) { + delegate.setDefaultUseCaches(newValue); + } + + @Override + public void setDoInput(boolean newValue) { + delegate.setDoInput(newValue); + } + + @Override + public void setDoOutput(boolean newValue) { + delegate.setDoOutput(newValue); + } + + @Override + public void setIfModifiedSince(long newValue) { + delegate.setIfModifiedSince(newValue); + } + + @Override + public void setRequestProperty(String field, String newValue) { + delegate.setRequestProperty(field, newValue); + } + + @Override + public void setUseCaches(boolean newValue) { + delegate.setUseCaches(newValue); + } + + @Override + public void setConnectTimeout(int timeoutMillis) { + delegate.setConnectTimeout(timeoutMillis); + } + + @Override + public int getConnectTimeout() { + return delegate.getConnectTimeout(); + } + + @Override + public void setReadTimeout(int timeoutMillis) { + delegate.setReadTimeout(timeoutMillis); + } + + @Override + public int getReadTimeout() { + return delegate.getReadTimeout(); + } + + @Override + public String toString() { + return delegate.toString(); + } + + @Override + public void setFixedLengthStreamingMode(int contentLength) { + delegate.setFixedLengthStreamingMode(contentLength); + } + + @Override + public void setChunkedStreamingMode(int chunkLength) { + delegate.setChunkedStreamingMode(chunkLength); + } + + private final class HttpUrlConnectionDelegate extends HttpURLConnectionImpl { + private HttpUrlConnectionDelegate(URL url, int port) { + super(url, port); + } + + private HttpUrlConnectionDelegate(URL url, int port, Proxy proxy) { + super(url, port, proxy); + } + + @Override protected HttpEngine newHttpEngine(String method, RawHeaders requestHeaders, + HttpConnection connection, RetryableOutputStream requestBody) throws IOException { + return new HttpsEngine(this, method, requestHeaders, connection, requestBody, + HttpsURLConnectionImpl.this); + } + + public SecureCacheResponse getCacheResponse() { + HttpsEngine engine = (HttpsEngine) httpEngine; + return engine != null ? (SecureCacheResponse) engine.getCacheResponse() : null; + } + + public SSLSocket getSSLSocket() { + HttpsEngine engine = (HttpsEngine) httpEngine; + return engine != null ? engine.sslSocket : null; + } + } + + private static class HttpsEngine extends HttpEngine { + + /** + * Local stash of HttpsEngine.connection.sslSocket for answering + * queries such as getCipherSuite even after + * httpsEngine.Connection has been recycled. It's presence is also + * used to tell if the HttpsURLConnection is considered connected, + * as opposed to the connected field of URLConnection or the a + * non-null connect in HttpURLConnectionImpl + */ + private SSLSocket sslSocket; + + private final HttpsURLConnectionImpl enclosing; + + /** + * @param policy the HttpURLConnectionImpl with connection configuration + * @param enclosing the HttpsURLConnection with HTTPS features + */ + private HttpsEngine(HttpURLConnectionImpl policy, String method, RawHeaders requestHeaders, + HttpConnection connection, RetryableOutputStream requestBody, + HttpsURLConnectionImpl enclosing) throws IOException { + super(policy, method, requestHeaders, connection, requestBody); + this.sslSocket = connection != null ? connection.getSecureSocketIfConnected() : null; + this.enclosing = enclosing; + } + + @Override protected void connect() throws IOException { + // First try an SSL connection with compression and various TLS + // extensions enabled, if it fails (and its not unheard of that it + // will) fallback to a barebones connection. + try { + makeSslConnection(true); + } catch (IOException e) { + // If the problem was a CertificateException from the X509TrustManager, + // do not retry, we didn't have an abrupt server initiated exception. + if (e instanceof SSLHandshakeException + && e.getCause() instanceof CertificateException) { + throw e; + } + release(false); + makeSslConnection(false); + } + } + + /** + * Attempt to make an HTTPS connection. + * + * @param tlsTolerant If true, assume server can handle common + * TLS extensions and SSL deflate compression. If false, use + * an SSL3 only fallback mode without compression. + */ + private void makeSslConnection(boolean tlsTolerant) throws IOException { + // make an SSL Tunnel on the first message pair of each SSL + proxy connection + if (connection == null) { + connection = openSocketConnection(); + if (connection.getAddress().getProxy() != null) { + makeTunnel(policy, connection, getRequestHeaders()); + } + } + + // if super.makeConnection returned a connection from the + // pool, sslSocket needs to be initialized here. If it is + // a new connection, it will be initialized by + // getSecureSocket below. + sslSocket = connection.getSecureSocketIfConnected(); + + // we already have an SSL connection, + if (sslSocket != null) { + return; + } + + sslSocket = connection.setupSecureSocket( + enclosing.getSSLSocketFactory(), enclosing.getHostnameVerifier(), tlsTolerant); + } + + /** + * To make an HTTPS connection over an HTTP proxy, send an unencrypted + * CONNECT request to create the proxy connection. This may need to be + * retried if the proxy requires authorization. + */ + private void makeTunnel(HttpURLConnectionImpl policy, HttpConnection connection, + RequestHeaders requestHeaders) throws IOException { + RawHeaders rawRequestHeaders = requestHeaders.getHeaders(); + while (true) { + HttpEngine connect = new ProxyConnectEngine(policy, rawRequestHeaders, connection); + connect.sendRequest(); + connect.readResponse(); + + int responseCode = connect.getResponseCode(); + switch (connect.getResponseCode()) { + case HTTP_OK: + return; + case HTTP_PROXY_AUTH: + rawRequestHeaders = new RawHeaders(rawRequestHeaders); + boolean credentialsFound = policy.processAuthHeader(HTTP_PROXY_AUTH, + connect.getResponseHeaders(), rawRequestHeaders); + if (credentialsFound) { + continue; + } else { + throw new IOException("Failed to authenticate with proxy"); + } + default: + throw new IOException("Unexpected response code for CONNECT: " + responseCode); + } + } + } + + @Override protected boolean acceptCacheResponseType(CacheResponse cacheResponse) { + return cacheResponse instanceof SecureCacheResponse; + } + + @Override protected boolean includeAuthorityInRequestLine() { + // Even if there is a proxy, it isn't involved. Always request just the file. + return false; + } + + @Override protected SSLSocketFactory getSslSocketFactory() { + return enclosing.getSSLSocketFactory(); + } + + @Override protected OkHttpConnection getHttpConnectionToCache() { + return enclosing; + } + } + + private static class ProxyConnectEngine extends HttpEngine { + public ProxyConnectEngine(HttpURLConnectionImpl policy, RawHeaders requestHeaders, + HttpConnection connection) throws IOException { + super(policy, HttpEngine.CONNECT, requestHeaders, connection, null); + } + + @Override protected boolean requiresTunnel() { + return true; + } + } +} diff --git a/src/main/java/libcore/net/http/RawHeaders.java b/src/main/java/libcore/net/http/RawHeaders.java new file mode 100644 index 000000000000..4f9ec9e06dbd --- /dev/null +++ b/src/main/java/libcore/net/http/RawHeaders.java @@ -0,0 +1,389 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package libcore.net.http; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashSet; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; +import java.util.TreeMap; +import libcore.util.Libcore; + +/** + * The HTTP status and unparsed header fields of a single HTTP message. Values + * are represented as uninterpreted strings; use {@link RequestHeaders} and + * {@link ResponseHeaders} for interpreted headers. This class maintains the + * order of the header fields within the HTTP message. + * + *

    This class tracks fields line-by-line. A field with multiple comma- + * separated values on the same line will be treated as a field with a single + * value by this class. It is the caller's responsibility to detect and split + * on commas if their field permits multiple values. This simplifies use of + * single-valued fields whose values routinely contain commas, such as cookies + * or dates. + * + *

    This class trims whitespace from values. It never returns values with + * leading or trailing whitespace. + */ +public final class RawHeaders { + private static final Comparator FIELD_NAME_COMPARATOR = new Comparator() { + // @FindBugsSuppressWarnings("ES_COMPARING_PARAMETER_STRING_WITH_EQ") + @Override public int compare(String a, String b) { + if (a == b) { + return 0; + } else if (a == null) { + return -1; + } else if (b == null) { + return 1; + } else { + return String.CASE_INSENSITIVE_ORDER.compare(a, b); + } + } + }; + + private final List namesAndValues = new ArrayList(20); + private String statusLine; + private int httpMinorVersion = 1; + private int responseCode = -1; + private String responseMessage; + + public RawHeaders() {} + + public RawHeaders(RawHeaders copyFrom) { + namesAndValues.addAll(copyFrom.namesAndValues); + statusLine = copyFrom.statusLine; + httpMinorVersion = copyFrom.httpMinorVersion; + responseCode = copyFrom.responseCode; + responseMessage = copyFrom.responseMessage; + } + + /** + * Sets the response status line (like "HTTP/1.0 200 OK") or request line + * (like "GET / HTTP/1.1"). + */ + public void setStatusLine(String statusLine) { + statusLine = statusLine.trim(); + this.statusLine = statusLine; + + if (statusLine == null || !statusLine.startsWith("HTTP/")) { + return; + } + statusLine = statusLine.trim(); + int mark = statusLine.indexOf(" ") + 1; + if (mark == 0) { + return; + } + if (statusLine.charAt(mark - 2) != '1') { + this.httpMinorVersion = 0; + } + int last = mark + 3; + if (last > statusLine.length()) { + last = statusLine.length(); + } + this.responseCode = Integer.parseInt(statusLine.substring(mark, last)); + if (last + 1 <= statusLine.length()) { + this.responseMessage = statusLine.substring(last + 1); + } + } + + public void computeResponseStatusLineFromSpdyHeaders() throws IOException { + String status = null; + String version = null; + for (int i = 0; i < namesAndValues.size(); i += 2) { + String name = namesAndValues.get(i); + if (name.equals("status")) { + status = namesAndValues.get(i + 1); + } else if (name.equals("version")) { + version = namesAndValues.get(i + 1); + } + } + if (status == null || version == null) { + throw new IOException("Expected 'status' and 'version' headers not present"); + } + setStatusLine(version + " " + status); + } + + /** + * @param method like "GET", "POST", "HEAD", etc. + * @param scheme like "https" + * @param url like "/foo/bar.html" + * @param version like "HTTP/1.1" + */ + public void addSpdyRequestHeaders(String method, String scheme, String url, String version) { + // TODO: populate the statusLine for the client's benefit? + add("method", method); + add("scheme", scheme); + add("url", url); + add("version", version); + } + + public String getStatusLine() { + return statusLine; + } + + /** + * Returns the status line's HTTP minor version. This returns 0 for HTTP/1.0 + * and 1 for HTTP/1.1. This returns 1 if the HTTP version is unknown. + */ + public int getHttpMinorVersion() { + return httpMinorVersion != -1 ? httpMinorVersion : 1; + } + + /** + * Returns the HTTP status code or -1 if it is unknown. + */ + public int getResponseCode() { + return responseCode; + } + + /** + * Returns the HTTP status message or null if it is unknown. + */ + public String getResponseMessage() { + return responseMessage; + } + + /** + * Add an HTTP header line containing a field name, a literal colon, and a + * value. + */ + public void addLine(String line) { + int index = line.indexOf(":"); + if (index == -1) { + add("", line); + } else { + add(line.substring(0, index), line.substring(index + 1)); + } + } + + /** + * Add a field with the specified value. + */ + public void add(String fieldName, String value) { + if (fieldName == null) { + throw new IllegalArgumentException("fieldName == null"); + } + if (value == null) { + /* + * Given null values, the RI sends a malformed field line like + * "Accept\r\n". For platform compatibility and HTTP compliance, we + * print a warning and ignore null values. + */ + Libcore.logW("Ignoring HTTP header field '" + fieldName + "' because its value is null"); + return; + } + namesAndValues.add(fieldName); + namesAndValues.add(value.trim()); + } + + public void removeAll(String fieldName) { + for (int i = 0; i < namesAndValues.size(); i += 2) { + if (fieldName.equalsIgnoreCase(namesAndValues.get(i))) { + namesAndValues.remove(i); // field name + namesAndValues.remove(i); // value + } + } + } + + public void addAll(String fieldName, List headerFields) { + for (String value : headerFields) { + add(fieldName, value); + } + } + + /** + * Set a field with the specified value. If the field is not found, it is + * added. If the field is found, the existing values are replaced. + */ + public void set(String fieldName, String value) { + removeAll(fieldName); + add(fieldName, value); + } + + /** + * Returns the number of field values. + */ + public int length() { + return namesAndValues.size() / 2; + } + + /** + * Returns the field at {@code position} or null if that is out of range. + */ + public String getFieldName(int index) { + int fieldNameIndex = index * 2; + if (fieldNameIndex < 0 || fieldNameIndex >= namesAndValues.size()) { + return null; + } + return namesAndValues.get(fieldNameIndex); + } + + /** + * Returns the value at {@code index} or null if that is out of range. + */ + public String getValue(int index) { + int valueIndex = index * 2 + 1; + if (valueIndex < 0 || valueIndex >= namesAndValues.size()) { + return null; + } + return namesAndValues.get(valueIndex); + } + + /** + * Returns the last value corresponding to the specified field, or null. + */ + public String get(String fieldName) { + for (int i = namesAndValues.size() - 2; i >= 0; i -= 2) { + if (fieldName.equalsIgnoreCase(namesAndValues.get(i))) { + return namesAndValues.get(i + 1); + } + } + return null; + } + + /** + * @param fieldNames a case-insensitive set of HTTP header field names. + */ + public RawHeaders getAll(Set fieldNames) { + RawHeaders result = new RawHeaders(); + for (int i = 0; i < namesAndValues.size(); i += 2) { + String fieldName = namesAndValues.get(i); + if (fieldNames.contains(fieldName)) { + result.add(fieldName, namesAndValues.get(i + 1)); + } + } + return result; + } + + public String toHeaderString() { + StringBuilder result = new StringBuilder(256); + result.append(statusLine).append("\r\n"); + for (int i = 0; i < namesAndValues.size(); i += 2) { + result.append(namesAndValues.get(i)).append(": ") + .append(namesAndValues.get(i + 1)).append("\r\n"); + } + result.append("\r\n"); + return result.toString(); + } + + /** + * Returns an immutable map containing each field to its list of values. The + * status line is mapped to null. + */ + public Map> toMultimap() { + Map> result = new TreeMap>(FIELD_NAME_COMPARATOR); + for (int i = 0; i < namesAndValues.size(); i += 2) { + String fieldName = namesAndValues.get(i); + String value = namesAndValues.get(i + 1); + + List allValues = new ArrayList(); + List otherValues = result.get(fieldName); + if (otherValues != null) { + allValues.addAll(otherValues); + } + allValues.add(value); + result.put(fieldName, Collections.unmodifiableList(allValues)); + } + if (statusLine != null) { + result.put(null, Collections.unmodifiableList(Collections.singletonList(statusLine))); + } + return Collections.unmodifiableMap(result); + } + + /** + * Creates a new instance from the given map of fields to values. If + * present, the null field's last element will be used to set the status + * line. + */ + public static RawHeaders fromMultimap(Map> map) { + RawHeaders result = new RawHeaders(); + for (Entry> entry : map.entrySet()) { + String fieldName = entry.getKey(); + List values = entry.getValue(); + if (fieldName != null) { + result.addAll(fieldName, values); + } else if (!values.isEmpty()) { + result.setStatusLine(values.get(values.size() - 1)); + } + } + return result; + } + + /** + * Returns a list of alternating names and values. Names are all lower case. + * No names are repeated. If any name has multiple values, they are + * concatenated using "\0" as a delimiter. + */ + public List toNameValueBlock() { + Set names = new HashSet(); + List result = new ArrayList(); + for (int i = 0; i < namesAndValues.size(); i += 2) { + String name = namesAndValues.get(i).toLowerCase(Locale.US); + String value = namesAndValues.get(i + 1); + + // TODO: promote this check to where names and values are created + if (name.isEmpty() || value.isEmpty() + || name.indexOf('\0') != -1 || value.indexOf('\0') != -1) { + throw new IllegalArgumentException("Unexpected header: " + name + ": " + value); + } + + // If we haven't seen this name before, add the pair to the end of the list... + if (names.add(name)) { + result.add(name); + result.add(value); + continue; + } + + // ...otherwise concatenate the existing values and this value. + for (int j = 0; j < result.size(); j += 2) { + if (name.equals(result.get(j))) { + result.set(j + 1, result.get(j + 1) + "\0" + value); + break; + } + } + } + return result; + } + + public static RawHeaders fromNameValueBlock(List nameValueBlock) { + if (nameValueBlock.size() % 2 != 0) { + throw new IllegalArgumentException("Unexpected name value block: " + nameValueBlock); + } + RawHeaders result = new RawHeaders(); + for (int i = 0; i < nameValueBlock.size(); i += 2) { + String name = nameValueBlock.get(i); + String values = nameValueBlock.get(i + 1); + for (int start = 0; start < values.length(); ) { + int end = values.indexOf(start, '\0'); + if (end == -1) { + end = values.length(); + } + result.namesAndValues.add(name); + result.namesAndValues.add(values.substring(start, end)); + start = end + 1; + } + } + return result; + } +} diff --git a/src/main/java/libcore/net/http/RequestHeaders.java b/src/main/java/libcore/net/http/RequestHeaders.java new file mode 100644 index 000000000000..3b536ce58933 --- /dev/null +++ b/src/main/java/libcore/net/http/RequestHeaders.java @@ -0,0 +1,292 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package libcore.net.http; + +import java.net.URI; +import java.util.Date; +import java.util.List; +import java.util.Map; + +/** + * Parsed HTTP request headers. + */ +public final class RequestHeaders { + private final URI uri; + private final RawHeaders headers; + + /** Don't use a cache to satisfy this request. */ + private boolean noCache; + private int maxAgeSeconds = -1; + private int maxStaleSeconds = -1; + private int minFreshSeconds = -1; + + /** + * This field's name "only-if-cached" is misleading. It actually means "do + * not use the network". It is set by a client who only wants to make a + * request if it can be fully satisfied by the cache. Cached responses that + * would require validation (ie. conditional gets) are not permitted if this + * header is set. + */ + private boolean onlyIfCached; + + /** + * True if the request contains an authorization field. Although this isn't + * necessarily a shared cache, it follows the spec's strict requirements for + * shared caches. + */ + private boolean hasAuthorization; + + private int contentLength = -1; + private String transferEncoding; + private String userAgent; + private String host; + private String connection; + private String acceptEncoding; + private String contentType; + private String ifModifiedSince; + private String ifNoneMatch; + private String proxyAuthorization; + + public RequestHeaders(URI uri, RawHeaders headers) { + this.uri = uri; + this.headers = headers; + + HeaderParser.CacheControlHandler handler = new HeaderParser.CacheControlHandler() { + @Override public void handle(String directive, String parameter) { + if (directive.equalsIgnoreCase("no-cache")) { + noCache = true; + } else if (directive.equalsIgnoreCase("max-age")) { + maxAgeSeconds = HeaderParser.parseSeconds(parameter); + } else if (directive.equalsIgnoreCase("max-stale")) { + maxStaleSeconds = HeaderParser.parseSeconds(parameter); + } else if (directive.equalsIgnoreCase("min-fresh")) { + minFreshSeconds = HeaderParser.parseSeconds(parameter); + } else if (directive.equalsIgnoreCase("only-if-cached")) { + onlyIfCached = true; + } + } + }; + + for (int i = 0; i < headers.length(); i++) { + String fieldName = headers.getFieldName(i); + String value = headers.getValue(i); + if ("Cache-Control".equalsIgnoreCase(fieldName)) { + HeaderParser.parseCacheControl(value, handler); + } else if ("Pragma".equalsIgnoreCase(fieldName)) { + if (value.equalsIgnoreCase("no-cache")) { + noCache = true; + } + } else if ("If-None-Match".equalsIgnoreCase(fieldName)) { + ifNoneMatch = value; + } else if ("If-Modified-Since".equalsIgnoreCase(fieldName)) { + ifModifiedSince = value; + } else if ("Authorization".equalsIgnoreCase(fieldName)) { + hasAuthorization = true; + } else if ("Content-Length".equalsIgnoreCase(fieldName)) { + try { + contentLength = Integer.parseInt(value); + } catch (NumberFormatException ignored) { + } + } else if ("Transfer-Encoding".equalsIgnoreCase(fieldName)) { + transferEncoding = value; + } else if ("User-Agent".equalsIgnoreCase(fieldName)) { + userAgent = value; + } else if ("Host".equalsIgnoreCase(fieldName)) { + host = value; + } else if ("Connection".equalsIgnoreCase(fieldName)) { + connection = value; + } else if ("Accept-Encoding".equalsIgnoreCase(fieldName)) { + acceptEncoding = value; + } else if ("Content-Type".equalsIgnoreCase(fieldName)) { + contentType = value; + } else if ("Proxy-Authorization".equalsIgnoreCase(fieldName)) { + proxyAuthorization = value; + } + } + } + + public boolean isChunked() { + return "chunked".equalsIgnoreCase(transferEncoding); + } + + public boolean hasConnectionClose() { + return "close".equalsIgnoreCase(connection); + } + + public URI getUri() { + return uri; + } + + public RawHeaders getHeaders() { + return headers; + } + + public boolean isNoCache() { + return noCache; + } + + public int getMaxAgeSeconds() { + return maxAgeSeconds; + } + + public int getMaxStaleSeconds() { + return maxStaleSeconds; + } + + public int getMinFreshSeconds() { + return minFreshSeconds; + } + + public boolean isOnlyIfCached() { + return onlyIfCached; + } + + public boolean hasAuthorization() { + return hasAuthorization; + } + + public int getContentLength() { + return contentLength; + } + + public String getTransferEncoding() { + return transferEncoding; + } + + public String getUserAgent() { + return userAgent; + } + + public String getHost() { + return host; + } + + public String getConnection() { + return connection; + } + + public String getAcceptEncoding() { + return acceptEncoding; + } + + public String getContentType() { + return contentType; + } + + public String getIfModifiedSince() { + return ifModifiedSince; + } + + public String getIfNoneMatch() { + return ifNoneMatch; + } + + public String getProxyAuthorization() { + return proxyAuthorization; + } + + public void setChunked() { + if (this.transferEncoding != null) { + headers.removeAll("Transfer-Encoding"); + } + headers.add("Transfer-Encoding", "chunked"); + this.transferEncoding = "chunked"; + } + + public void setContentLength(int contentLength) { + if (this.contentLength != -1) { + headers.removeAll("Content-Length"); + } + headers.add("Content-Length", Integer.toString(contentLength)); + this.contentLength = contentLength; + } + + public void setUserAgent(String userAgent) { + if (this.userAgent != null) { + headers.removeAll("User-Agent"); + } + headers.add("User-Agent", userAgent); + this.userAgent = userAgent; + } + + public void setHost(String host) { + if (this.host != null) { + headers.removeAll("Host"); + } + headers.add("Host", host); + this.host = host; + } + + public void setConnection(String connection) { + if (this.connection != null) { + headers.removeAll("Connection"); + } + headers.add("Connection", connection); + this.connection = connection; + } + + public void setAcceptEncoding(String acceptEncoding) { + if (this.acceptEncoding != null) { + headers.removeAll("Accept-Encoding"); + } + headers.add("Accept-Encoding", acceptEncoding); + this.acceptEncoding = acceptEncoding; + } + + public void setContentType(String contentType) { + if (this.contentType != null) { + headers.removeAll("Content-Type"); + } + headers.add("Content-Type", contentType); + this.contentType = contentType; + } + + public void setIfModifiedSince(Date date) { + if (ifModifiedSince != null) { + headers.removeAll("If-Modified-Since"); + } + String formattedDate = HttpDate.format(date); + headers.add("If-Modified-Since", formattedDate); + ifModifiedSince = formattedDate; + } + + public void setIfNoneMatch(String ifNoneMatch) { + if (this.ifNoneMatch != null) { + headers.removeAll("If-None-Match"); + } + headers.add("If-None-Match", ifNoneMatch); + this.ifNoneMatch = ifNoneMatch; + } + + /** + * Returns true if the request contains conditions that save the server from + * sending a response that the client has locally. When the caller adds + * conditions, this cache won't participate in the request. + */ + public boolean hasConditions() { + return ifModifiedSince != null || ifNoneMatch != null; + } + + public void addCookies(Map> allCookieHeaders) { + for (Map.Entry> entry : allCookieHeaders.entrySet()) { + String key = entry.getKey(); + if ("Cookie".equalsIgnoreCase(key) || "Cookie2".equalsIgnoreCase(key)) { + headers.addAll(key, entry.getValue()); + } + } + } +} diff --git a/src/main/java/libcore/net/http/ResponseHeaders.java b/src/main/java/libcore/net/http/ResponseHeaders.java new file mode 100644 index 000000000000..d2311f350d9e --- /dev/null +++ b/src/main/java/libcore/net/http/ResponseHeaders.java @@ -0,0 +1,503 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package libcore.net.http; + +import java.net.HttpURLConnection; +import libcore.util.ResponseSource; +import java.net.URI; +import java.util.Collections; +import java.util.Date; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.TreeSet; +import java.util.concurrent.TimeUnit; +import libcore.util.Objects; + +/** + * Parsed HTTP response headers. + */ +public final class ResponseHeaders { + + /** HTTP header name for the local time when the request was sent. */ + private static final String SENT_MILLIS = "X-Android-Sent-Millis"; + + /** HTTP header name for the local time when the response was received. */ + private static final String RECEIVED_MILLIS = "X-Android-Received-Millis"; + + private final URI uri; + private final RawHeaders headers; + + /** The server's time when this response was served, if known. */ + private Date servedDate; + + /** The last modified date of the response, if known. */ + private Date lastModified; + + /** + * The expiration date of the response, if known. If both this field and the + * max age are set, the max age is preferred. + */ + private Date expires; + + /** + * Extension header set by HttpURLConnectionImpl specifying the timestamp + * when the HTTP request was first initiated. + */ + private long sentRequestMillis; + + /** + * Extension header set by HttpURLConnectionImpl specifying the timestamp + * when the HTTP response was first received. + */ + private long receivedResponseMillis; + + /** + * In the response, this field's name "no-cache" is misleading. It doesn't + * prevent us from caching the response; it only means we have to validate + * the response with the origin server before returning it. We can do this + * with a conditional get. + */ + private boolean noCache; + + /** If true, this response should not be cached. */ + private boolean noStore; + + /** + * The duration past the response's served date that it can be served + * without validation. + */ + private int maxAgeSeconds = -1; + + /** + * The "s-maxage" directive is the max age for shared caches. Not to be + * confused with "max-age" for non-shared caches, As in Firefox and Chrome, + * this directive is not honored by this cache. + */ + private int sMaxAgeSeconds = -1; + + /** + * This request header field's name "only-if-cached" is misleading. It + * actually means "do not use the network". It is set by a client who only + * wants to make a request if it can be fully satisfied by the cache. + * Cached responses that would require validation (ie. conditional gets) are + * not permitted if this header is set. + */ + private boolean isPublic; + private boolean mustRevalidate; + private String etag; + private int ageSeconds = -1; + + /** Case-insensitive set of field names. */ + private Set varyFields = Collections.emptySet(); + + private String contentEncoding; + private String transferEncoding; + private int contentLength = -1; + private String connection; + + public ResponseHeaders(URI uri, RawHeaders headers) { + this.uri = uri; + this.headers = headers; + + HeaderParser.CacheControlHandler handler = new HeaderParser.CacheControlHandler() { + @Override public void handle(String directive, String parameter) { + if (directive.equalsIgnoreCase("no-cache")) { + noCache = true; + } else if (directive.equalsIgnoreCase("no-store")) { + noStore = true; + } else if (directive.equalsIgnoreCase("max-age")) { + maxAgeSeconds = HeaderParser.parseSeconds(parameter); + } else if (directive.equalsIgnoreCase("s-maxage")) { + sMaxAgeSeconds = HeaderParser.parseSeconds(parameter); + } else if (directive.equalsIgnoreCase("public")) { + isPublic = true; + } else if (directive.equalsIgnoreCase("must-revalidate")) { + mustRevalidate = true; + } + } + }; + + for (int i = 0; i < headers.length(); i++) { + String fieldName = headers.getFieldName(i); + String value = headers.getValue(i); + if ("Cache-Control".equalsIgnoreCase(fieldName)) { + HeaderParser.parseCacheControl(value, handler); + } else if ("Date".equalsIgnoreCase(fieldName)) { + servedDate = HttpDate.parse(value); + } else if ("Expires".equalsIgnoreCase(fieldName)) { + expires = HttpDate.parse(value); + } else if ("Last-Modified".equalsIgnoreCase(fieldName)) { + lastModified = HttpDate.parse(value); + } else if ("ETag".equalsIgnoreCase(fieldName)) { + etag = value; + } else if ("Pragma".equalsIgnoreCase(fieldName)) { + if (value.equalsIgnoreCase("no-cache")) { + noCache = true; + } + } else if ("Age".equalsIgnoreCase(fieldName)) { + ageSeconds = HeaderParser.parseSeconds(value); + } else if ("Vary".equalsIgnoreCase(fieldName)) { + // Replace the immutable empty set with something we can mutate. + if (varyFields.isEmpty()) { + varyFields = new TreeSet(String.CASE_INSENSITIVE_ORDER); + } + for (String varyField : value.split(",")) { + varyFields.add(varyField.trim()); + } + } else if ("Content-Encoding".equalsIgnoreCase(fieldName)) { + contentEncoding = value; + } else if ("Transfer-Encoding".equalsIgnoreCase(fieldName)) { + transferEncoding = value; + } else if ("Content-Length".equalsIgnoreCase(fieldName)) { + try { + contentLength = Integer.parseInt(value); + } catch (NumberFormatException ignored) { + } + } else if ("Connection".equalsIgnoreCase(fieldName)) { + connection = value; + } else if (SENT_MILLIS.equalsIgnoreCase(fieldName)) { + sentRequestMillis = Long.parseLong(value); + } else if (RECEIVED_MILLIS.equalsIgnoreCase(fieldName)) { + receivedResponseMillis = Long.parseLong(value); + } + } + } + + public boolean isContentEncodingGzip() { + return "gzip".equalsIgnoreCase(contentEncoding); + } + + public void stripContentEncoding() { + contentEncoding = null; + headers.removeAll("Content-Encoding"); + } + + public boolean isChunked() { + return "chunked".equalsIgnoreCase(transferEncoding); + } + + public boolean hasConnectionClose() { + return "close".equalsIgnoreCase(connection); + } + + public URI getUri() { + return uri; + } + + public RawHeaders getHeaders() { + return headers; + } + + public Date getServedDate() { + return servedDate; + } + + public Date getLastModified() { + return lastModified; + } + + public Date getExpires() { + return expires; + } + + public boolean isNoCache() { + return noCache; + } + + public boolean isNoStore() { + return noStore; + } + + public int getMaxAgeSeconds() { + return maxAgeSeconds; + } + + public int getSMaxAgeSeconds() { + return sMaxAgeSeconds; + } + + public boolean isPublic() { + return isPublic; + } + + public boolean isMustRevalidate() { + return mustRevalidate; + } + + public String getEtag() { + return etag; + } + + public Set getVaryFields() { + return varyFields; + } + + public String getContentEncoding() { + return contentEncoding; + } + + public int getContentLength() { + return contentLength; + } + + public String getConnection() { + return connection; + } + + public void setLocalTimestamps(long sentRequestMillis, long receivedResponseMillis) { + this.sentRequestMillis = sentRequestMillis; + headers.add(SENT_MILLIS, Long.toString(sentRequestMillis)); + this.receivedResponseMillis = receivedResponseMillis; + headers.add(RECEIVED_MILLIS, Long.toString(receivedResponseMillis)); + } + + /** + * Returns the current age of the response, in milliseconds. The calculation + * is specified by RFC 2616, 13.2.3 Age Calculations. + */ + private long computeAge(long nowMillis) { + long apparentReceivedAge = servedDate != null + ? Math.max(0, receivedResponseMillis - servedDate.getTime()) + : 0; + long receivedAge = ageSeconds != -1 + ? Math.max(apparentReceivedAge, TimeUnit.SECONDS.toMillis(ageSeconds)) + : apparentReceivedAge; + long responseDuration = receivedResponseMillis - sentRequestMillis; + long residentDuration = nowMillis - receivedResponseMillis; + return receivedAge + responseDuration + residentDuration; + } + + /** + * Returns the number of milliseconds that the response was fresh for, + * starting from the served date. + */ + private long computeFreshnessLifetime() { + if (maxAgeSeconds != -1) { + return TimeUnit.SECONDS.toMillis(maxAgeSeconds); + } else if (expires != null) { + long servedMillis = servedDate != null ? servedDate.getTime() : receivedResponseMillis; + long delta = expires.getTime() - servedMillis; + return delta > 0 ? delta : 0; + } else if (lastModified != null && uri.getRawQuery() == null) { + /* + * As recommended by the HTTP RFC and implemented in Firefox, the + * max age of a document should be defaulted to 10% of the + * document's age at the time it was served. Default expiration + * dates aren't used for URIs containing a query. + */ + long servedMillis = servedDate != null ? servedDate.getTime() : sentRequestMillis; + long delta = servedMillis - lastModified.getTime(); + return delta > 0 ? (delta / 10) : 0; + } + return 0; + } + + /** + * Returns true if computeFreshnessLifetime used a heuristic. If we used a + * heuristic to serve a cached response older than 24 hours, we are required + * to attach a warning. + */ + private boolean isFreshnessLifetimeHeuristic() { + return maxAgeSeconds == -1 && expires == null; + } + + /** + * Returns true if this response can be stored to later serve another + * request. + */ + public boolean isCacheable(RequestHeaders request) { + /* + * Always go to network for uncacheable response codes (RFC 2616, 13.4), + * This implementation doesn't support caching partial content. + */ + int responseCode = headers.getResponseCode(); + if (responseCode != HttpURLConnection.HTTP_OK + && responseCode != HttpURLConnection.HTTP_NOT_AUTHORITATIVE + && responseCode != HttpURLConnection.HTTP_MULT_CHOICE + && responseCode != HttpURLConnection.HTTP_MOVED_PERM + && responseCode != HttpURLConnection.HTTP_GONE) { + return false; + } + + /* + * Responses to authorized requests aren't cacheable unless they include + * a 'public', 'must-revalidate' or 's-maxage' directive. + */ + if (request.hasAuthorization() + && !isPublic + && !mustRevalidate + && sMaxAgeSeconds == -1) { + return false; + } + + if (noStore) { + return false; + } + + return true; + } + + /** + * Returns true if a Vary header contains an asterisk. Such responses cannot + * be cached. + */ + public boolean hasVaryAll() { + return varyFields.contains("*"); + } + + /** + * Returns true if none of the Vary headers on this response have changed + * between {@code cachedRequest} and {@code newRequest}. + */ + public boolean varyMatches(Map> cachedRequest, + Map> newRequest) { + for (String field : varyFields) { + if (!Objects.equal(cachedRequest.get(field), newRequest.get(field))) { + return false; + } + } + return true; + } + + /** + * Returns the source to satisfy {@code request} given this cached response. + */ + public ResponseSource chooseResponseSource(long nowMillis, RequestHeaders request) { + /* + * If this response shouldn't have been stored, it should never be used + * as a response source. This check should be redundant as long as the + * persistence store is well-behaved and the rules are constant. + */ + if (!isCacheable(request)) { + return ResponseSource.NETWORK; + } + + if (request.isNoCache() || request.hasConditions()) { + return ResponseSource.NETWORK; + } + + long ageMillis = computeAge(nowMillis); + long freshMillis = computeFreshnessLifetime(); + + if (request.getMaxAgeSeconds() != -1) { + freshMillis = Math.min(freshMillis, + TimeUnit.SECONDS.toMillis(request.getMaxAgeSeconds())); + } + + long minFreshMillis = 0; + if (request.getMinFreshSeconds() != -1) { + minFreshMillis = TimeUnit.SECONDS.toMillis(request.getMinFreshSeconds()); + } + + long maxStaleMillis = 0; + if (!mustRevalidate && request.getMaxStaleSeconds() != -1) { + maxStaleMillis = TimeUnit.SECONDS.toMillis(request.getMaxStaleSeconds()); + } + + if (!noCache && ageMillis + minFreshMillis < freshMillis + maxStaleMillis) { + if (ageMillis + minFreshMillis >= freshMillis) { + headers.add("Warning", "110 HttpURLConnection \"Response is stale\""); + } + if (ageMillis > TimeUnit.HOURS.toMillis(24) && isFreshnessLifetimeHeuristic()) { + headers.add("Warning", "113 HttpURLConnection \"Heuristic expiration\""); + } + return ResponseSource.CACHE; + } + + if (lastModified != null) { + request.setIfModifiedSince(lastModified); + } else if (servedDate != null) { + request.setIfModifiedSince(servedDate); + } + + if (etag != null) { + request.setIfNoneMatch(etag); + } + + return request.hasConditions() + ? ResponseSource.CONDITIONAL_CACHE + : ResponseSource.NETWORK; + } + + /** + * Returns true if this cached response should be used; false if the + * network response should be used. + */ + public boolean validate(ResponseHeaders networkResponse) { + if (networkResponse.headers.getResponseCode() == HttpURLConnection.HTTP_NOT_MODIFIED) { + return true; + } + + /* + * The HTTP spec says that if the network's response is older than our + * cached response, we may return the cache's response. Like Chrome (but + * unlike Firefox), this client prefers to return the newer response. + */ + if (lastModified != null + && networkResponse.lastModified != null + && networkResponse.lastModified.getTime() < lastModified.getTime()) { + return true; + } + + return false; + } + + /** + * Combines this cached header with a network header as defined by RFC 2616, + * 13.5.3. + */ + public ResponseHeaders combine(ResponseHeaders network) { + RawHeaders result = new RawHeaders(); + result.setStatusLine(headers.getStatusLine()); + + for (int i = 0; i < headers.length(); i++) { + String fieldName = headers.getFieldName(i); + String value = headers.getValue(i); + if (fieldName.equals("Warning") && value.startsWith("1")) { + continue; // drop 100-level freshness warnings + } + if (!isEndToEnd(fieldName) || network.headers.get(fieldName) == null) { + result.add(fieldName, value); + } + } + + for (int i = 0; i < network.headers.length(); i++) { + String fieldName = network.headers.getFieldName(i); + if (isEndToEnd(fieldName)) { + result.add(fieldName, network.headers.getValue(i)); + } + } + + return new ResponseHeaders(uri, result); + } + + /** + * Returns true if {@code fieldName} is an end-to-end HTTP header, as + * defined by RFC 2616, 13.5.1. + */ + private static boolean isEndToEnd(String fieldName) { + return !fieldName.equalsIgnoreCase("Connection") + && !fieldName.equalsIgnoreCase("Keep-Alive") + && !fieldName.equalsIgnoreCase("Proxy-Authenticate") + && !fieldName.equalsIgnoreCase("Proxy-Authorization") + && !fieldName.equalsIgnoreCase("TE") + && !fieldName.equalsIgnoreCase("Trailers") + && !fieldName.equalsIgnoreCase("Transfer-Encoding") + && !fieldName.equalsIgnoreCase("Upgrade"); + } +} diff --git a/src/main/java/libcore/net/http/RetryableOutputStream.java b/src/main/java/libcore/net/http/RetryableOutputStream.java new file mode 100644 index 000000000000..c8110be75eaf --- /dev/null +++ b/src/main/java/libcore/net/http/RetryableOutputStream.java @@ -0,0 +1,72 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package libcore.net.http; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import libcore.util.Libcore; + +/** + * An HTTP request body that's completely buffered in memory. This allows + * the post body to be transparently re-sent if the HTTP request must be + * sent multiple times. + */ +final class RetryableOutputStream extends AbstractHttpOutputStream { + private final int limit; + private final ByteArrayOutputStream content; + + public RetryableOutputStream(int limit) { + this.limit = limit; + this.content = new ByteArrayOutputStream(limit); + } + + public RetryableOutputStream() { + this.limit = -1; + this.content = new ByteArrayOutputStream(); + } + + @Override public synchronized void close() { + if (closed) { + return; + } + closed = true; + if (content.size() < limit) { + throw new IllegalStateException("content-length promised " + + limit + " bytes, but received " + content.size()); + } + } + + @Override public synchronized void write(byte[] buffer, int offset, int count) + throws IOException { + checkNotClosed(); + Libcore.checkOffsetAndCount(buffer.length, offset, count); + if (limit != -1 && content.size() > limit - count) { + throw new IOException("exceeded content-length limit of " + limit + " bytes"); + } + content.write(buffer, offset, count); + } + + public synchronized int contentLength() { + close(); + return content.size(); + } + + public void writeToSocket(OutputStream socketOut) throws IOException { + content.writeTo(socketOut); + } +} diff --git a/src/main/java/libcore/net/http/SpdyTransport.java b/src/main/java/libcore/net/http/SpdyTransport.java new file mode 100644 index 000000000000..547658cbf50b --- /dev/null +++ b/src/main/java/libcore/net/http/SpdyTransport.java @@ -0,0 +1,91 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package libcore.net.http; + +import libcore.net.spdy.SpdyConnection; +import libcore.net.spdy.SpdyStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InterruptedIOException; +import java.io.OutputStream; +import java.net.CacheRequest; +import java.util.List; + +final class SpdyTransport implements Transport { + private final HttpEngine httpEngine; + private final SpdyConnection spdyConnection; + private SpdyStream stream; + + // TODO: set sentMillis + // TODO: set cookie stuff + + SpdyTransport(HttpEngine httpEngine, SpdyConnection spdyConnection) { + this.httpEngine = httpEngine; + this.spdyConnection = spdyConnection; + } + + @Override public OutputStream createRequestBody() throws IOException { + // TODO: if we aren't streaming up to the server, we should buffer the whole request + writeRequestHeaders(); + return stream.getOutputStream(); + } + + @Override public void writeRequestHeaders() throws IOException { + if (stream != null) { + return; + } + RawHeaders requestHeaders = httpEngine.requestHeaders.getHeaders(); + String version = httpEngine.connection.httpMinorVersion == 1 ? "HTTP/1.1" : "HTTP/1.0"; + requestHeaders.addSpdyRequestHeaders(httpEngine.method, httpEngine.uri.getScheme(), + httpEngine.uri.getPath(), version); + boolean hasRequestBody = httpEngine.hasRequestBody(); + boolean hasResponseBody = true; + stream = spdyConnection.newStream(requestHeaders.toNameValueBlock(), + hasRequestBody, hasResponseBody); + } + + @Override public void writeRequestBody(RetryableOutputStream requestBody) throws IOException { + throw new UnsupportedOperationException(); + } + + @Override public void flushRequest() throws IOException { + stream.getOutputStream().close(); + } + + @Override public ResponseHeaders readResponseHeaders() throws IOException { + // TODO: fix the SPDY implementation so this throws a (buffered) IOException + try { + List nameValueBlock = stream.getResponseHeaders(); + RawHeaders rawHeaders = RawHeaders.fromNameValueBlock(nameValueBlock); + rawHeaders.computeResponseStatusLineFromSpdyHeaders(); + return new ResponseHeaders(httpEngine.uri, rawHeaders); + } catch (InterruptedException e) { + InterruptedIOException rethrow = new InterruptedIOException(); + rethrow.initCause(e); + throw rethrow; + } + } + + @Override public InputStream getTransferStream(CacheRequest cacheRequest) throws IOException { + // TODO: handle HTTP responses that don't have a response body + return stream.getInputStream(); + } + + @Override public boolean makeReusable(OutputStream requestBodyOut, InputStream responseBodyIn) { + return true; + } +} diff --git a/src/main/java/libcore/net/http/Transport.java b/src/main/java/libcore/net/http/Transport.java new file mode 100644 index 000000000000..3d4c8ddbb2f2 --- /dev/null +++ b/src/main/java/libcore/net/http/Transport.java @@ -0,0 +1,71 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package libcore.net.http; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.CacheRequest; + +interface Transport { + /** + * Returns an output stream where the request body can be written. The + * returned stream will of one of two types: + *

      + *
    • Direct. Bytes are written to the socket and + * forgotten. This is most efficient, particularly for large request + * bodies. The returned stream may be buffered; the caller must call + * {@link #flushRequest} before reading the response.
    • + *
    • Buffered. Bytes are written to an in memory + * buffer, and must be explicitly flushed with a call to {@link + * #writeRequestBody}. This allows HTTP authorization (401, 407) + * responses to be retransmitted transparently.
    • + *
    + */ + // TODO: don't bother retransmitting the request body? It's quite a corner + // case and there's uncertainty whether Firefox or Chrome do this + OutputStream createRequestBody() throws IOException; + + /** + * This should update the HTTP engine's sentRequestMillis field. + */ + void writeRequestHeaders() throws IOException; + + /** + * Sends the request body returned by {@link #createRequestBody} to the + * remote peer. + */ + void writeRequestBody(RetryableOutputStream requestBody) throws IOException; + + /** + * Flush the request body to the underlying socket. + */ + void flushRequest() throws IOException; + + /** + * Read response headers and update the cookie manager. + */ + ResponseHeaders readResponseHeaders() throws IOException; + + // TODO: make this the content stream? + InputStream getTransferStream(CacheRequest cacheRequest) throws IOException; + + /** + * Returns true if the underlying connection can be recycled. + */ + boolean makeReusable(OutputStream requestBodyOut, InputStream responseBodyIn); +} diff --git a/src/main/java/libcore/net/spdy/IncomingStreamHandler.java b/src/main/java/libcore/net/spdy/IncomingStreamHandler.java new file mode 100644 index 000000000000..69cc8e110e4a --- /dev/null +++ b/src/main/java/libcore/net/spdy/IncomingStreamHandler.java @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package libcore.net.spdy; + +import java.io.IOException; + +/** + * Listener to be notified when a connected peer creates a new stream. + */ +public interface IncomingStreamHandler { + IncomingStreamHandler REFUSE_INCOMING_STREAMS = new IncomingStreamHandler() { + @Override public void receive(SpdyStream stream) throws IOException { + stream.close(SpdyStream.RST_REFUSED_STREAM); + } + }; + + /** + * Handle a new stream from this connection's peer. Implementations should + * respond by either {@link SpdyStream#reply(java.util.List) replying to the + * stream} or {@link SpdyStream#close(int) closing it}. This response does + * not need to be synchronous. + */ + void receive(SpdyStream stream) throws IOException; +} diff --git a/src/main/java/libcore/net/spdy/SpdyConnection.java b/src/main/java/libcore/net/spdy/SpdyConnection.java new file mode 100644 index 000000000000..daef8f5f616e --- /dev/null +++ b/src/main/java/libcore/net/spdy/SpdyConnection.java @@ -0,0 +1,282 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package libcore.net.spdy; + +import java.io.Closeable; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.Socket; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.Executor; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +/** + * A socket connection to a remote peer. A connection hosts streams which can + * send and receive data. + */ +public final class SpdyConnection implements Closeable { + + /* + * Socket writes are guarded by this. Socket reads are unguarded but are + * only made by the reader thread. + */ + + static final int FLAG_FIN = 0x01; + static final int FLAG_UNIDIRECTIONAL = 0x02; + + static final int TYPE_EOF = -1; + static final int TYPE_DATA = 0x00; + static final int TYPE_SYN_STREAM = 0x01; + static final int TYPE_SYN_REPLY = 0x02; + static final int TYPE_RST_STREAM = 0x03; + static final int TYPE_SETTINGS = 0x04; + static final int TYPE_NOOP = 0x05; + static final int TYPE_PING = 0x06; + static final int TYPE_GOAWAY = 0x07; + static final int TYPE_HEADERS = 0x08; + static final int VERSION = 2; + + /** Guarded by this */ + private int nextStreamId; + private final SpdyReader spdyReader; + private final SpdyWriter spdyWriter; + private final Executor executor; + + /** + * User code to run in response to an incoming stream. This must not be run + * on the read thread, otherwise a deadlock is possible. + */ + private final IncomingStreamHandler handler; + + private final Map streams = Collections.synchronizedMap( + new HashMap()); + + private SpdyConnection(Builder builder) { + nextStreamId = builder.client ? 1 : 2; + spdyReader = new SpdyReader(builder.in); + spdyWriter = new SpdyWriter(builder.out); + handler = builder.handler; + + String name = isClient() ? "ClientReader" : "ServerReader"; + executor = builder.executor != null + ? builder.executor + : Executors.newCachedThreadPool(Threads.newThreadFactory(name)); + executor.execute(new Reader()); + } + + /** + * Returns true if this peer initiated the connection. + */ + public boolean isClient() { + return nextStreamId % 2 == 1; + } + + private SpdyStream getStream(int id) { + SpdyStream stream = streams.get(id); + if (stream == null) { + throw new UnsupportedOperationException("TODO " + id + "; " + streams); // TODO: rst stream + } + return stream; + } + + void removeStream(int streamId) { + streams.remove(streamId); + } + + /** + * Returns a new locally-initiated stream. + * + * @param out true to create an output stream that we can use to send data + * to the remote peer. Corresponds to {@code FLAG_FIN}. + * @param in true to create an input stream that the remote peer can use to + * send data to us. Corresponds to {@code FLAG_UNIDIRECTIONAL}. + */ + public synchronized SpdyStream newStream(List requestHeaders, boolean out, boolean in) + throws IOException { + int streamId = nextStreamId; // TODO + nextStreamId += 2; + int flags = (out ? 0 : FLAG_FIN) | (in ? 0 : FLAG_UNIDIRECTIONAL); + int associatedStreamId = 0; // TODO + int priority = 0; // TODO + + SpdyStream result = new SpdyStream(streamId, this, requestHeaders, flags); + streams.put(streamId, result); + + spdyWriter.flags = flags; + spdyWriter.streamId = streamId; + spdyWriter.associatedStreamId = associatedStreamId; + spdyWriter.priority = priority; + spdyWriter.nameValueBlock = requestHeaders; + spdyWriter.synStream(); + + return result; + } + + synchronized void writeSynReply(int streamId, List alternating) throws IOException { + int flags = 0; // TODO + spdyWriter.flags = flags; + spdyWriter.streamId = streamId; + spdyWriter.nameValueBlock = alternating; + spdyWriter.synReply(); + } + + /** Writes a complete data frame. */ + synchronized void writeFrame(byte[] bytes, int offset, int length) throws IOException { + spdyWriter.out.write(bytes, offset, length); + } + + void writeSynResetLater(final int streamId, final int statusCode) { + executor.execute(new Runnable() { + @Override public void run() { + try { + writeSynReset(streamId, statusCode); + } catch (IOException ignored) { + } + } + }); + } + + synchronized void writeSynReset(int streamId, int statusCode) throws IOException { + int flags = 0; // TODO + spdyWriter.flags = flags; + spdyWriter.streamId = streamId; + spdyWriter.statusCode = statusCode; + spdyWriter.synReset(); + } + + public synchronized void flush() throws IOException { + spdyWriter.out.flush(); + } + + @Override public synchronized void close() throws IOException { + // TODO: graceful close; send RST frames + // TODO: close all streams to release waiting readers + if (executor instanceof ExecutorService) { + ((ExecutorService) executor).shutdown(); + } + } + + public static class Builder { + private InputStream in; + private OutputStream out; + private IncomingStreamHandler handler = IncomingStreamHandler.REFUSE_INCOMING_STREAMS; + private Executor executor; + public boolean client; + + /** + * @param client true if this peer initiated the connection; false if + * this peer accepted the connection. + */ + public Builder(boolean client, Socket socket) throws IOException { + this(client, socket.getInputStream(), socket.getOutputStream()); + } + + /** + * @param client true if this peer initiated the connection; false if this + * peer accepted the connection. + */ + public Builder(boolean client, InputStream in, OutputStream out) { + this.client = client; + this.in = in; + this.out = out; + } + + public Builder executor(Executor executor) { + this.executor = executor; + return this; + } + + public Builder handler(IncomingStreamHandler handler) { + this.handler = handler; + return this; + } + + public SpdyConnection build() { + return new SpdyConnection(this); + } + } + + private class Reader implements Runnable { + @Override public void run() { + try { + while (readFrame()) { + } + close(); + } catch (Throwable e) { + e.printStackTrace(); // TODO + } + } + + private boolean readFrame() throws IOException { + switch (spdyReader.nextFrame()) { + case TYPE_EOF: + return false; + + case TYPE_DATA: + getStream(spdyReader.streamId) + .receiveData(spdyReader.in, spdyReader.flags, spdyReader.length); + return true; + + case TYPE_SYN_STREAM: + final SpdyStream stream = new SpdyStream(spdyReader.streamId, SpdyConnection.this, + spdyReader.nameValueBlock, spdyReader.flags); + SpdyStream previous = streams.put(spdyReader.streamId, stream); + if (previous != null) { + previous.close(SpdyStream.RST_PROTOCOL_ERROR); + } + executor.execute(new Runnable() { + @Override public void run() { + try { + handler.receive(stream); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + }); + return true; + + case TYPE_SYN_REPLY: + // TODO: honor flags + getStream(spdyReader.streamId).receiveReply(spdyReader.nameValueBlock); + return true; + + case TYPE_RST_STREAM: + getStream(spdyReader.streamId).receiveRstStream(spdyReader.statusCode); + return true; + + case SpdyConnection.TYPE_SETTINGS: + // TODO: implement + System.out.println("Unimplemented TYPE_SETTINGS frame discarded"); + return true; + + case SpdyConnection.TYPE_NOOP: + case SpdyConnection.TYPE_PING: + case SpdyConnection.TYPE_GOAWAY: + case SpdyConnection.TYPE_HEADERS: + throw new UnsupportedOperationException(); + } + + // TODO: throw IOException here? + return false; + } + } +} diff --git a/src/main/java/libcore/net/spdy/SpdyReader.java b/src/main/java/libcore/net/spdy/SpdyReader.java new file mode 100644 index 000000000000..9540b230ab5d --- /dev/null +++ b/src/main/java/libcore/net/spdy/SpdyReader.java @@ -0,0 +1,211 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package libcore.net.spdy; + +import java.io.DataInputStream; +import java.io.EOFException; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.Charset; +import java.util.ArrayList; +import java.util.List; +import java.util.logging.Logger; +import java.util.zip.DataFormatException; +import java.util.zip.Inflater; +import java.util.zip.InflaterInputStream; +import libcore.io.Streams; + +/** + * Read version 2 SPDY frames. + */ +final class SpdyReader { + public static final Charset UTF_8 = Charset.forName("UTF-8"); + public static final byte[] DICTIONARY = ("" + + "optionsgetheadpostputdeletetraceacceptaccept-charsetaccept-encodingaccept-" + + "languageauthorizationexpectfromhostif-modified-sinceif-matchif-none-matchi" + + "f-rangeif-unmodifiedsincemax-forwardsproxy-authorizationrangerefererteuser" + + "-agent10010120020120220320420520630030130230330430530630740040140240340440" + + "5406407408409410411412413414415416417500501502503504505accept-rangesageeta" + + "glocationproxy-authenticatepublicretry-afterservervarywarningwww-authentic" + + "ateallowcontent-basecontent-encodingcache-controlconnectiondatetrailertran" + + "sfer-encodingupgradeviawarningcontent-languagecontent-lengthcontent-locati" + + "oncontent-md5content-rangecontent-typeetagexpireslast-modifiedset-cookieMo" + + "ndayTuesdayWednesdayThursdayFridaySaturdaySundayJanFebMarAprMayJunJulAugSe" + + "pOctNovDecchunkedtext/htmlimage/pngimage/jpgimage/gifapplication/xmlapplic" + + "ation/xhtmltext/plainpublicmax-agecharset=iso-8859-1utf-8gzipdeflateHTTP/1" + + ".1statusversionurl\0").getBytes(UTF_8); + + public final DataInputStream in; + public int flags; + public int length; + public int streamId; + public int associatedStreamId; + public int version; + public int type; + public int priority; + public int statusCode; + + public List nameValueBlock; + private final DataInputStream nameValueBlockIn; + private int compressedLimit; + + SpdyReader(InputStream in) { + this.in = new DataInputStream(in); + this.nameValueBlockIn = newNameValueBlockStream(); + } + + /** + * Advance to the next frame in the source data. If the frame is of + * TYPE_DATA, it's the caller's responsibility to read length bytes from + * the input stream before the next call to nextFrame(). + */ + public int nextFrame() throws IOException { + int w1; + try { + w1 = in.readInt(); + } catch (EOFException e) { + return SpdyConnection.TYPE_EOF; + } + int w2 = in.readInt(); + + boolean control = (w1 & 0x80000000) != 0; + flags = (w2 & 0xff000000) >>> 24; + length = (w2 & 0xffffff); + + if (control) { + version = (w1 & 0x7fff0000) >>> 16; + type = (w1 & 0xffff); + + switch (type) { + case SpdyConnection.TYPE_SYN_STREAM: + readSynStream(); + return SpdyConnection.TYPE_SYN_STREAM; + + case SpdyConnection.TYPE_SYN_REPLY: + readSynReply(); + return SpdyConnection.TYPE_SYN_REPLY; + + case SpdyConnection.TYPE_RST_STREAM: + readSynReset(); + return SpdyConnection.TYPE_RST_STREAM; + + default: + readControlFrame(); + return type; + } + } else { + streamId = w1 & 0x7fffffff; + return SpdyConnection.TYPE_DATA; + } + } + + private void readSynStream() throws IOException { + int w1 = in.readInt(); + int w2 = in.readInt(); + int s3 = in.readShort(); + streamId = w1 & 0x7fffffff; + associatedStreamId = w2 & 0x7fffffff; + priority = s3 & 0xc000 >> 14; + // int unused = s3 & 0x3fff; + nameValueBlock = readNameValueBlock(length - 10); + } + + private void readSynReply() throws IOException { + int w1 = in.readInt(); + in.readShort(); // unused + streamId = w1 & 0x7fffffff; + nameValueBlock = readNameValueBlock(length - 6); + } + + private void readSynReset() throws IOException { + streamId = in.readInt() & 0x7fffffff; + statusCode = in.readInt(); + } + + private void readControlFrame() throws IOException { + Streams.skipByReading(in, length); + } + + private DataInputStream newNameValueBlockStream() { + // Limit the inflater input stream to only those bytes in the Name/Value block. + final InputStream throttleStream = new InputStream() { + @Override public int read() throws IOException { + return Streams.readSingleByte(this); + } + + @Override public int read(byte[] buffer, int offset, int byteCount) throws IOException { + byteCount = Math.min(byteCount, compressedLimit); + int consumed = in.read(buffer, offset, byteCount); + compressedLimit -= consumed; + return consumed; + } + + @Override public void close() throws IOException { + in.close(); + } + }; + + // Subclass inflater to install a dictionary when it's needed. + Inflater inflater = new Inflater() { + @Override + public int inflate(byte[] buffer, int offset, int count) throws DataFormatException { + int result = super.inflate(buffer, offset, count); + if (result == 0 && needsDictionary()) { + setDictionary(DICTIONARY); + result = super.inflate(buffer, offset, count); + } + return result; + } + }; + + return new DataInputStream(new InflaterInputStream(throttleStream, inflater)); + } + + private List readNameValueBlock(int length) throws IOException { + this.compressedLimit += length; + try { + List entries = new ArrayList(); + + int numberOfPairs = nameValueBlockIn.readShort(); + for (int i = 0; i < numberOfPairs; i++) { + String name = readString(); + String values = readString(); + if (name.isEmpty() || values.isEmpty()) { + throw new IOException(); // TODO: PROTOCOL ERROR + } + entries.add(name); + entries.add(values); + } + + if (compressedLimit != 0) { + Logger.getLogger(getClass().getName()) + .warning("compressedLimit > 0" + compressedLimit); + } + + return entries; + } catch (DataFormatException e) { + throw new IOException(e); + } + } + + private String readString() throws DataFormatException, IOException { + int length = nameValueBlockIn.readShort(); + byte[] bytes = new byte[length]; + Streams.readFully(nameValueBlockIn, bytes); + return new String(bytes, 0, length, UTF_8); + } +} diff --git a/src/main/java/libcore/net/spdy/SpdyServer.java b/src/main/java/libcore/net/spdy/SpdyServer.java new file mode 100644 index 000000000000..190c549a8485 --- /dev/null +++ b/src/main/java/libcore/net/spdy/SpdyServer.java @@ -0,0 +1,114 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package libcore.net.spdy; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.ServerSocket; +import java.net.Socket; +import java.util.Arrays; +import java.util.List; + +/** + * A basic SPDY server that serves the contents of a local directory. This + * server will service a single SPDY connection. + */ +public final class SpdyServer implements IncomingStreamHandler { + private final File baseDirectory; + + public SpdyServer(File baseDirectory) { + this.baseDirectory = baseDirectory; + } + + private void run() throws IOException { + ServerSocket serverSocket = new ServerSocket(8888); + serverSocket.setReuseAddress(true); + + Socket socket = serverSocket.accept(); + new SpdyConnection.Builder(false, socket) + .handler(this) + .build(); + } + + @Override public void receive(final SpdyStream stream) throws IOException { + List requestHeaders = stream.getRequestHeaders(); + String path = null; + for (int i = 0; i < requestHeaders.size(); i += 2) { + String s = requestHeaders.get(i); + if (s.equals("url")) { + path = requestHeaders.get(i + 1); + break; + } + } + + if (path == null) { + // TODO: send bad request error + throw new AssertionError(); + } + + File file = new File(baseDirectory + path); + + if (file.exists() && !file.isDirectory()) { + serveFile(stream, file); + } else { + send404(stream, path); + } + } + + private void send404(SpdyStream stream, String path) throws IOException { + List responseHeaders = Arrays.asList( + "status", "404", + "version", "HTTP/1.1", + "content-type", "text/plain" + ); + OutputStream out = stream.reply(responseHeaders); + String text = "Not found: " + path; + out.write(text.getBytes()); + out.close(); + } + + private void serveFile(SpdyStream stream, File file) throws IOException { + InputStream in = new FileInputStream(file); + byte[] buffer = new byte[8192]; + OutputStream out = stream.reply(Arrays.asList( + "status", "200", + "version", "HTTP/1.1", + "content-type", contentType(file) + )); + int count; + while ((count = in.read(buffer)) != -1) { + out.write(buffer, 0, count); + } + out.close(); + } + + private String contentType(File file) { + return file.getName().endsWith(".html") ? "text/html" : "text/plain"; + } + + public static void main(String... args) throws IOException { + if (args.length != 1 || args[0].startsWith("-")) { + System.out.println("Usage: SpdyServer "); + return; + } + + new SpdyServer(new File(args[0])).run(); + } +} diff --git a/src/main/java/libcore/net/spdy/SpdyStream.java b/src/main/java/libcore/net/spdy/SpdyStream.java new file mode 100644 index 000000000000..bb1192cc8021 --- /dev/null +++ b/src/main/java/libcore/net/spdy/SpdyStream.java @@ -0,0 +1,410 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package libcore.net.spdy; + +import java.io.IOException; +import java.io.InputStream; +import java.io.InterruptedIOException; +import java.io.OutputStream; +import static java.nio.ByteOrder.BIG_ENDIAN; +import java.util.List; +import libcore.io.Streams; +import libcore.util.Libcore; + +/** + * A logical bidirectional stream. + */ +public final class SpdyStream { + + /* + * Internal state is guarded by this. No long-running or potentially + * blocking operations are performed while the lock is held. + */ + + private static final int DATA_FRAME_HEADER_LENGTH = 8; + + public static final int RST_PROTOCOL_ERROR = 1; + public static final int RST_INVALID_STREAM = 2; + public static final int RST_REFUSED_STREAM = 3; + public static final int RST_UNSUPPORTED_VERSION = 4; + public static final int RST_CANCEL = 5; + public static final int RST_INTERNAL_ERROR = 6; + public static final int RST_FLOW_CONTROL_ERROR = 7; + + private final int id; + private final SpdyConnection connection; + + /** Headers sent by the stream initiator. Immutable and non null. */ + private final List requestHeaders; + + /** Headers sent in the stream reply. Null if reply is either not sent or not sent yet. */ + private List responseHeaders; + + private final SpdyDataInputStream in = new SpdyDataInputStream(); + private final SpdyDataOutputStream out = new SpdyDataOutputStream(); + + /** + * The reason why this stream was abnormally closed. If there are multiple + * reasons to abnormally close this stream (such as both peers closing it + * near-simultaneously) then this is the first reason known to this peer. + */ + private int rstStatusCode = -1; + + /** + * True if either side has shut down the input stream. We will receive no + * more bytes beyond those already in the buffer. Guarded by this. + */ + private boolean inFinished; + + /** + * True if either side has shut down the output stream. We will write no + * more bytes to the output stream. Guarded by this. + */ + private boolean outFinished; + + SpdyStream(int id, SpdyConnection connection, List requestHeaders, int flags) { + this.id = id; + this.connection = connection; + this.requestHeaders = requestHeaders; + + if (isLocallyInitiated()) { + // I am the sender + inFinished = (flags & SpdyConnection.FLAG_UNIDIRECTIONAL) != 0; + outFinished = (flags & SpdyConnection.FLAG_FIN) != 0; + } else { + // I am the receiver + inFinished = (flags & SpdyConnection.FLAG_FIN) != 0; + outFinished = (flags & SpdyConnection.FLAG_UNIDIRECTIONAL) != 0; + } + } + + /** + * Returns true if this stream was created by this peer. + */ + public boolean isLocallyInitiated() { + boolean streamIsClient = (id % 2 == 1); + return connection.isClient() == streamIsClient; + } + + public SpdyConnection getConnection() { + return connection; + } + + public List getRequestHeaders() { + return requestHeaders; + } + + public synchronized List getResponseHeaders() throws InterruptedException { + while (responseHeaders == null && rstStatusCode == -1) { + wait(); + } + return responseHeaders; + } + + /** + * Returns the reason why this stream was closed, or -1 if it closed + * normally or has not yet been closed. + */ + public synchronized int getRstStatusCode() { // TODO: rename this? + return rstStatusCode; + } + + public InputStream getInputStream() { + return in; + } + + public OutputStream getOutputStream() { + if (!isLocallyInitiated()) { + throw new IllegalStateException("use reply for a remotely initiated stream"); + } + return out; + } + + /** + * Sends a reply. + */ + // TODO: support reply with FIN + public synchronized OutputStream reply(List responseHeaders) throws IOException { + if (responseHeaders == null) { + throw new NullPointerException("responseHeaders == null"); + } + if (isLocallyInitiated()) { + throw new IllegalStateException("cannot reply to a locally initiated stream"); + } + synchronized (this) { + if (this.responseHeaders != null) { + throw new IllegalStateException("reply already sent"); + } + this.responseHeaders = responseHeaders; + } + connection.writeSynReply(id, responseHeaders); + return out; + } + + /** + * Abnormally terminate this stream. + */ + public synchronized void close(int rstStatusCode) { + // TODO: no-op if inFinished == true and outFinished == true ? + if (this.rstStatusCode != -1) { + this.rstStatusCode = rstStatusCode; + inFinished = true; + outFinished = true; + connection.removeStream(id); + notifyAll(); + connection.writeSynResetLater(id, rstStatusCode); + } + } + + synchronized void receiveReply(List strings) throws IOException { + if (!isLocallyInitiated() || responseHeaders != null) { + throw new IOException(); // TODO: send RST + } + responseHeaders = strings; + notifyAll(); + } + + synchronized void receiveData(InputStream in, int flags, int length) throws IOException { + this.in.receive(in, length); + if ((flags & SpdyConnection.FLAG_FIN) != 0) { + inFinished = true; + notifyAll(); + } + } + + synchronized void receiveRstStream(int statusCode) { + if (rstStatusCode != -1) { + rstStatusCode = statusCode; + inFinished = true; + outFinished = true; + notifyAll(); + } + } + + /** + * An input stream that reads the incoming data frames of a stream. Although + * this class uses synchronization to safely receive incoming data frames, + * it is not intended for use by multiple readers. + */ + private final class SpdyDataInputStream extends InputStream { + /* + * Store incoming data bytes in a circular buffer. When the buffer is + * empty, pos == -1. Otherwise pos is the first byte to read and limit + * is the first byte to write. + * + * { - - - X X X X - - - } + * ^ ^ + * pos limit + * + * { X X X - - - - X X X } + * ^ ^ + * limit pos + */ + + private final byte[] buffer = new byte[64 * 1024]; // 64KiB specified by TODO + /** the next byte to be read, or -1 if the buffer is empty. Never buffer.length */ + private int pos = -1; + /** the last byte to be read. Never buffer.length */ + private int limit; + /** True if the caller has closed this stream. */ + private boolean closed; + + @Override public int available() throws IOException { + synchronized (SpdyStream.this) { + checkNotClosed(); + if (pos == -1) { + return 0; + } else if (limit > pos) { + return limit - pos; + } else { + return limit + (buffer.length - pos); + } + } + } + + @Override public int read() throws IOException { + return Streams.readSingleByte(this); + } + + @Override public int read(byte[] b, int offset, int count) throws IOException { + synchronized (SpdyStream.this) { + checkNotClosed(); + Libcore.checkOffsetAndCount(b.length, offset, count); + + while (pos == -1 && !inFinished) { + try { + SpdyStream.this.wait(); + } catch (InterruptedException e) { + throw new InterruptedIOException(); + } + } + + if (pos == -1) { + return -1; + } + + int copied = 0; + + // drain from [pos..buffer.length) + if (limit <= pos) { + int bytesToCopy = Math.min(count, buffer.length - pos); + System.arraycopy(buffer, pos, b, offset, bytesToCopy); + pos += bytesToCopy; + copied += bytesToCopy; + if (pos == buffer.length) { + pos = 0; + } + } + + // drain from [pos..limit) + if (copied < count) { + int bytesToCopy = Math.min(limit - pos, count - copied); + System.arraycopy(buffer, pos, b, offset + copied, bytesToCopy); + pos += bytesToCopy; + copied += bytesToCopy; + } + + // TODO: notify peer of flow-control + + if (pos == limit) { + pos = -1; + limit = 0; + } + + return copied; + } + } + + void receive(InputStream in, int byteCount) throws IOException { + if (inFinished) { + return; // ignore this; probably a benign race + } + if (byteCount == 0) { + return; + } + + if (byteCount > buffer.length - available()) { + throw new IOException(); // TODO: RST the stream + } + + // fill [limit..buffer.length) + if (pos < limit) { + int firstCopyCount = Math.min(byteCount, buffer.length - limit); + Streams.readFully(in, buffer, limit, firstCopyCount); + limit += firstCopyCount; + byteCount -= firstCopyCount; + if (limit == buffer.length) { + limit = 0; + } + } + + // fill [limit..pos) + if (byteCount > 0) { + Streams.readFully(in, buffer, limit, byteCount); + limit += byteCount; + } + + if (pos == -1) { + pos = 0; + SpdyStream.this.notifyAll(); + } + } + + @Override public void close() throws IOException { + closed = true; + // TODO: send RST to peer if !inFinished + } + + private void checkNotClosed() throws IOException { + if (closed) { + throw new IOException("stream closed"); + } + } + } + + /** + * An output stream that writes outgoing data frames of a stream. This class + * is not thread safe. + */ + private final class SpdyDataOutputStream extends OutputStream { + private final byte[] buffer = new byte[8192]; + private int pos = DATA_FRAME_HEADER_LENGTH; + + /** True if the caller has closed this stream. */ + private boolean closed; + + @Override public void write(int b) throws IOException { + Streams.writeSingleByte(this, b); + } + + @Override public void write(byte[] bytes, int offset, int count) throws IOException { + Libcore.checkOffsetAndCount(bytes.length, offset, count); + checkNotClosed(); + + while (count > 0) { + if (pos == buffer.length) { + writeFrame(false); + } + int bytesToCopy = Math.min(count, buffer.length - pos); + System.arraycopy(bytes, offset, buffer, pos, bytesToCopy); + pos += bytesToCopy; + offset += bytesToCopy; + count -= bytesToCopy; + } + } + + @Override public void flush() throws IOException { + checkNotClosed(); + if (pos > DATA_FRAME_HEADER_LENGTH) { + writeFrame(false); + connection.flush(); + } + } + + @Override public void close() throws IOException { + if (!closed) { + closed = true; + writeFrame(true); + connection.flush(); + } + } + + private void writeFrame(boolean last) throws IOException { + int flags = 0; + if (last) { + flags |= SpdyConnection.FLAG_FIN; + } + int length = pos - DATA_FRAME_HEADER_LENGTH; + Libcore.pokeInt(buffer, 0, id & 0x7fffffff, BIG_ENDIAN); + Libcore.pokeInt(buffer, 4, (flags & 0xff) << 24 | length & 0xffffff, BIG_ENDIAN); + connection.writeFrame(buffer, 0, pos); + pos = DATA_FRAME_HEADER_LENGTH; + } + + private void checkNotClosed() throws IOException { + synchronized (SpdyStream.this) { + if (closed) { + throw new IOException("stream closed"); + } + if (outFinished) { + throw new IOException("output stream finished " + + "(RST status code=" + rstStatusCode + ")"); + } + } + } + } +} diff --git a/src/main/java/libcore/net/spdy/SpdyWriter.java b/src/main/java/libcore/net/spdy/SpdyWriter.java new file mode 100644 index 000000000000..cfd8a047d2e7 --- /dev/null +++ b/src/main/java/libcore/net/spdy/SpdyWriter.java @@ -0,0 +1,108 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package libcore.net.spdy; + +import java.io.ByteArrayOutputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.util.List; +import java.util.zip.Deflater; +import java.util.zip.DeflaterOutputStream; + +/** + * Write version 2 SPDY frames. + */ +final class SpdyWriter { + final DataOutputStream out; + public int flags; + public int streamId; + public int associatedStreamId; + public int priority; + public int statusCode; + + public List nameValueBlock; + private final ByteArrayOutputStream nameValueBlockBuffer; + private final DataOutputStream nameValueBlockOut; + + SpdyWriter(OutputStream out) { + this.out = new DataOutputStream(out); + + Deflater deflater = new Deflater(); + deflater.setDictionary(SpdyReader.DICTIONARY); + nameValueBlockBuffer = new ByteArrayOutputStream(); + nameValueBlockOut = new DataOutputStream( + new DeflaterOutputStream(nameValueBlockBuffer, deflater, true)); + } + + public void synStream() throws IOException { + writeNameValueBlockToBuffer(); + int length = 10 + nameValueBlockBuffer.size(); + int type = SpdyConnection.TYPE_SYN_STREAM; + + int unused = 0; + out.writeInt(0x80000000 | (SpdyConnection.VERSION & 0x7fff) << 16 | type & 0xffff); + out.writeInt((flags & 0xff) << 24 | length & 0xffffff); + out.writeInt(streamId & 0x7fffffff); + out.writeInt(associatedStreamId & 0x7fffffff); + out.writeShort((priority & 0x3) << 30 | (unused & 0x3FFF) << 16); + nameValueBlockBuffer.writeTo(out); + out.flush(); + } + + public void synReply() throws IOException { + writeNameValueBlockToBuffer(); + int type = SpdyConnection.TYPE_SYN_REPLY; + int length = nameValueBlockBuffer.size() + 6; + int unused = 0; + + out.writeInt(0x80000000 | (SpdyConnection.VERSION & 0x7fff) << 16 | type & 0xffff); + out.writeInt((flags & 0xff) << 24 | length & 0xffffff); + out.writeInt(streamId & 0x7fffffff); + out.writeShort(unused); + nameValueBlockBuffer.writeTo(out); + out.flush(); + } + + public void synReset() throws IOException { + int type = SpdyConnection.TYPE_RST_STREAM; + int length = 8; + out.writeInt(0x80000000 | (SpdyConnection.VERSION & 0x7fff) << 16 | type & 0xffff); + out.writeInt((flags & 0xff) << 24 | length & 0xffffff); + out.writeInt(streamId & 0x7fffffff); + out.writeInt(statusCode); + } + + public void data(byte[] data) throws IOException { + int length = data.length; + out.writeInt(streamId & 0x7fffffff); + out.writeInt((flags & 0xff) << 24 | length & 0xffffff); + out.write(data); + out.flush(); + } + + private void writeNameValueBlockToBuffer() throws IOException { + nameValueBlockBuffer.reset(); + int numberOfPairs = nameValueBlock.size() / 2; + nameValueBlockOut.writeShort(numberOfPairs); + for (String s : nameValueBlock) { + nameValueBlockOut.writeShort(s.length()); + nameValueBlockOut.write(s.getBytes(SpdyReader.UTF_8)); + } + nameValueBlockOut.flush(); + } +} diff --git a/src/main/java/libcore/net/spdy/Threads.java b/src/main/java/libcore/net/spdy/Threads.java new file mode 100644 index 000000000000..cb29a519f294 --- /dev/null +++ b/src/main/java/libcore/net/spdy/Threads.java @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package libcore.net.spdy; + +import java.util.concurrent.ThreadFactory; + +final class Threads { + public static ThreadFactory newThreadFactory(final String name) { + return new ThreadFactory() { + @Override public Thread newThread(Runnable r) { + return new Thread(r, name); + } + }; + } +} diff --git a/src/main/java/libcore/util/BasicLruCache.java b/src/main/java/libcore/util/BasicLruCache.java new file mode 100644 index 000000000000..14124381b34a --- /dev/null +++ b/src/main/java/libcore/util/BasicLruCache.java @@ -0,0 +1,121 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package libcore.util; + +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * A minimal least-recently-used cache for libcore. Prefer {@code + * android.util.LruCache} where that is available. + */ +public class BasicLruCache { + private final LinkedHashMap map; + private final int maxSize; + + public BasicLruCache(int maxSize) { + if (maxSize <= 0) { + throw new IllegalArgumentException("maxSize <= 0"); + } + this.maxSize = maxSize; + this.map = new LinkedHashMap(0, 0.75f, true); + } + + /** + * Returns the value for {@code key} if it exists in the cache or can be + * created by {@code #create}. If a value was returned, it is moved to the + * head of the queue. This returns null if a value is not cached and cannot + * be created. + */ + public synchronized final V get(K key) { + if (key == null) { + throw new NullPointerException(); + } + + V result = map.get(key); + if (result != null) { + return result; + } + + result = create(key); + + if (result != null) { + map.put(key, result); + trimToSize(maxSize); + } + return result; + } + + /** + * Caches {@code value} for {@code key}. The value is moved to the head of + * the queue. + * + * @return the previous value mapped by {@code key}. Although that entry is + * no longer cached, it has not been passed to {@link #entryEvicted}. + */ + public synchronized final V put(K key, V value) { + if (key == null || value == null) { + throw new NullPointerException(); + } + + V previous = map.put(key, value); + trimToSize(maxSize); + return previous; + } + + private void trimToSize(int maxSize) { + while (map.size() > maxSize) { + Map.Entry toEvict = map.entrySet().iterator().next(); + + K key = toEvict.getKey(); + V value = toEvict.getValue(); + map.remove(key); + + entryEvicted(key, value); + } + } + + /** + * Called for entries that have reached the tail of the least recently used + * queue and are be removed. The default implementation does nothing. + */ + protected void entryEvicted(K key, V value) {} + + /** + * Called after a cache miss to compute a value for the corresponding key. + * Returns the computed value or null if no value can be computed. The + * default implementation returns null. + */ + protected V create(K key) { + return null; + } + + /** + * Returns a copy of the current contents of the cache, ordered from least + * recently accessed to most recently accessed. + */ + public synchronized final Map snapshot() { + return new LinkedHashMap(map); + } + + /** + * Clear the cache, calling {@link #entryEvicted} on each removed entry. + */ + public synchronized final void evictAll() { + trimToSize(0); + } +} diff --git a/src/main/java/libcore/util/Charsets.java b/src/main/java/libcore/util/Charsets.java new file mode 100644 index 000000000000..c8b2c348bc5a --- /dev/null +++ b/src/main/java/libcore/util/Charsets.java @@ -0,0 +1,48 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package libcore.util; + +import java.nio.charset.Charset; + +/** + * Provides convenient access to the most important built-in charsets. Saves a hash lookup and + * unnecessary handling of UnsupportedEncodingException at call sites, compared to using the + * charset's name. + * + * Also various special-case charset conversions (for performance). + * + * @hide internal use only + */ +public class Charsets { + /** + * A cheap and type-safe constant for the ISO-8859-1 Charset. + */ + public static final Charset ISO_8859_1 = Charset.forName("ISO-8859-1"); + + /** + * A cheap and type-safe constant for the US-ASCII Charset. + */ + public static final Charset US_ASCII = Charset.forName("US-ASCII"); + + /** + * A cheap and type-safe constant for the UTF-8 Charset. + */ + public static final Charset UTF_8 = Charset.forName("UTF-8"); + + private Charsets() { + } +} diff --git a/src/main/java/libcore/util/CollectionUtils.java b/src/main/java/libcore/util/CollectionUtils.java new file mode 100644 index 000000000000..45c2ae6abe33 --- /dev/null +++ b/src/main/java/libcore/util/CollectionUtils.java @@ -0,0 +1,98 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package libcore.util; + +import java.lang.ref.Reference; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.Iterator; +import java.util.List; + +public final class CollectionUtils { + private CollectionUtils() {} + + /** + * Returns an iterator over the values referenced by the elements of {@code + * iterable}. + * + * @param trim true to remove reference objects from the iterable after + * their referenced values have been cleared. + */ + public static Iterable dereferenceIterable( + final Iterable> iterable, final boolean trim) { + return new Iterable() { + public Iterator iterator() { + return new Iterator() { + private final Iterator> delegate = iterable.iterator(); + private boolean removeIsOkay; + private T next; + + private void computeNext() { + removeIsOkay = false; + while (next == null && delegate.hasNext()) { + next = delegate.next().get(); + if (trim && next == null) { + delegate.remove(); + } + } + } + + @Override public boolean hasNext() { + computeNext(); + return next != null; + } + + @Override public T next() { + if (!hasNext()) { + throw new IllegalStateException(); + } + T result = next; + removeIsOkay = true; + next = null; + return result; + } + + public void remove() { + if (!removeIsOkay) { + throw new IllegalStateException(); + } + delegate.remove(); + } + }; + } + }; + } + + /** + * Sorts and removes duplicate elements from {@code list}. This method does + * not use {@link Object#equals}: only the comparator defines equality. + */ + public static void removeDuplicates(List list, Comparator comparator) { + Collections.sort(list, comparator); + int j = 1; + for (int i = 1; i < list.size(); i++) { + if (comparator.compare(list.get(j - 1), list.get(i)) != 0) { + T object = list.get(i); + list.set(j++, object); + } + } + if (j < list.size()) { + list.subList(j, list.size()).clear(); + } + } +} diff --git a/src/main/java/libcore/util/DefaultFileNameMap.java b/src/main/java/libcore/util/DefaultFileNameMap.java new file mode 100644 index 000000000000..e817a72fbccf --- /dev/null +++ b/src/main/java/libcore/util/DefaultFileNameMap.java @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package libcore.util; + +import java.net.FileNameMap; +import java.util.Locale; +import libcore.net.MimeUtils; + +/** + * Implements {@link java.net.FileNameMap} in terms of {@link libcore.net.MimeUtils}. + */ +class DefaultFileNameMap implements FileNameMap { + public String getContentTypeFor(String filename) { + if (filename.endsWith("/")) { + // a directory, return html + return MimeUtils.guessMimeTypeFromExtension("html"); + } + int lastCharInExtension = filename.lastIndexOf('#'); + if (lastCharInExtension < 0) { + lastCharInExtension = filename.length(); + } + int firstCharInExtension = filename.lastIndexOf('.') + 1; + String ext = ""; + if (firstCharInExtension > filename.lastIndexOf('/')) { + ext = filename.substring(firstCharInExtension, lastCharInExtension); + } + return MimeUtils.guessMimeTypeFromExtension(ext.toLowerCase(Locale.US)); + } +} diff --git a/src/main/java/libcore/util/EmptyArray.java b/src/main/java/libcore/util/EmptyArray.java new file mode 100644 index 000000000000..6c9987829d8a --- /dev/null +++ b/src/main/java/libcore/util/EmptyArray.java @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package libcore.util; + +public final class EmptyArray { + private EmptyArray() {} + + public static final boolean[] BOOLEAN = new boolean[0]; + public static final byte[] BYTE = new byte[0]; + public static final char[] CHAR = new char[0]; + public static final double[] DOUBLE = new double[0]; + public static final int[] INT = new int[0]; + + public static final Class[] CLASS = new Class[0]; + public static final Object[] OBJECT = new Object[0]; + public static final String[] STRING = new String[0]; + public static final Throwable[] THROWABLE = new Throwable[0]; + public static final StackTraceElement[] STACK_TRACE_ELEMENT = new StackTraceElement[0]; +} diff --git a/src/main/java/libcore/util/ExtendedResponseCache.java b/src/main/java/libcore/util/ExtendedResponseCache.java new file mode 100644 index 000000000000..b3f91916ed18 --- /dev/null +++ b/src/main/java/libcore/util/ExtendedResponseCache.java @@ -0,0 +1,54 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package libcore.util; + +import com.squareup.okhttp.OkHttpConnection; +import java.net.CacheResponse; + +/** + * A response cache that supports statistics tracking and updating stored + * responses. Implementations of {@link java.net.ResponseCache} should implement this + * interface to receive additional support from the HTTP engine. + * + * @hide + */ +public interface ExtendedResponseCache { + + /* + * This hidden interface is defined in a non-hidden package (java.net) so + * its @hide tag will be parsed by Doclava. This hides this interface from + * implementing classes' documentation. + */ + + /** + * Track an HTTP response being satisfied by {@code source}. + * @hide + */ + void trackResponse(ResponseSource source); + + /** + * Track an conditional GET that was satisfied by this cache. + * @hide + */ + void trackConditionalCacheHit(); + + /** + * Updates stored HTTP headers using a hit on a conditional GET. + * @hide + */ + void update(CacheResponse conditionalCacheHit, OkHttpConnection httpConnection); +} diff --git a/src/main/java/libcore/util/IntegralToString.java b/src/main/java/libcore/util/IntegralToString.java new file mode 100644 index 000000000000..1b66e510ccda --- /dev/null +++ b/src/main/java/libcore/util/IntegralToString.java @@ -0,0 +1,68 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package libcore.util; + +/** + * Converts integral types to strings. This class is public but hidden so that it can also be + * used by java.util.Formatter to speed up %d. This class is in java.lang so that it can take + * advantage of the package-private String constructor. + * + * The most important methods are appendInt/appendLong and intToString(int)/longToString(int). + * The former are used in the implementation of StringBuilder, StringBuffer, and Formatter, while + * the latter are used by Integer.toString and Long.toString. + * + * The append methods take AbstractStringBuilder rather than Appendable because the latter requires + * CharSequences, while we only have raw char[]s. Since much of the savings come from not creating + * any garbage, we can't afford temporary CharSequence instances. + * + * One day the performance advantage of the binary/hex/octal specializations will be small enough + * that we can lose the duplication, but until then this class offers the full set. + * + * @hide + */ +public final class IntegralToString { + /** + * The digits for every supported radix. + */ + private static final char[] DIGITS = { + '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', + 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', + 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', + 'u', 'v', 'w', 'x', 'y', 'z' + }; + + private static final char[] UPPER_CASE_DIGITS = { + '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', + 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', + 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', + 'U', 'V', 'W', 'X', 'Y', 'Z' + }; + + private IntegralToString() { + } + + public static String bytesToHexString(byte[] bytes, boolean upperCase) { + char[] digits = upperCase ? UPPER_CASE_DIGITS : DIGITS; + char[] buf = new char[bytes.length * 2]; + int c = 0; + for (byte b : bytes) { + buf[c++] = digits[(b >> 4) & 0xf]; + buf[c++] = digits[b & 0xf]; + } + return new String(buf); + } +} diff --git a/src/main/java/libcore/util/Libcore.java b/src/main/java/libcore/util/Libcore.java new file mode 100644 index 000000000000..4ccda829420e --- /dev/null +++ b/src/main/java/libcore/util/Libcore.java @@ -0,0 +1,194 @@ +package libcore.util; + +import java.io.File; +import java.io.IOException; +import java.lang.reflect.InvocationTargetException; +import java.net.Socket; +import java.net.SocketException; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URL; +import java.nio.ByteOrder; +import java.util.ArrayList; +import java.util.List; +import javax.net.ssl.SSLSocket; +import org.eclipse.jetty.npn.NextProtoNego; + +/** + * APIs for interacting with Android's core library. This mostly emulates the + * Android core library for interoperability with other runtimes. + */ +public class Libcore { + + public static void makeTlsTolerant(SSLSocket socket, String socketHost, boolean tlsTolerant) { + if (!tlsTolerant) { + socket.setEnabledProtocols(new String [] { "SSLv3" }); + return; + } + + try { + Class openSslSocketClass = Class.forName( + "org.apache.harmony.xnet.provider.jsse.OpenSSLSocketImpl"); + if (openSslSocketClass.isInstance(socket)) { + openSslSocketClass.getMethod("setEnabledCompressionMethods", String[].class) + .invoke(socket, new Object[] { new String[]{"ZLIB"}}); + openSslSocketClass.getMethod("setUseSessionTickets", boolean.class) + .invoke(socket, true); + openSslSocketClass.getMethod("setHostname", String.class) + .invoke(socket, socketHost); + } + } catch (ClassNotFoundException ignored) { + // TODO: support the RI's socket classes + } catch (InvocationTargetException e) { + throw new RuntimeException(e); + } catch (NoSuchMethodException e) { + throw new RuntimeException(e); + } catch (IllegalAccessException e) { + throw new AssertionError(e); + } + } + + public static byte[] getNpnSelectedProtocol(SSLSocket socket) { + // First try Android's APIs. + try { + Class openSslSocketClass = Class.forName( + "org.apache.harmony.xnet.provider.jsse.OpenSSLSocketImpl"); + return (byte[]) openSslSocketClass.getMethod("getNpnSelectedProtocol").invoke(socket); + } catch (ClassNotFoundException ignored) { + // this isn't Android; fall through to try OpenJDK with Jetty + } catch (IllegalAccessException e) { + throw new AssertionError(); + } catch (InvocationTargetException e) { + throw new AssertionError(); + } catch (NoSuchMethodException e) { + throw new AssertionError(); + } + + // Next try OpenJDK. + JettyNpnProvider provider = (JettyNpnProvider) NextProtoNego.get(socket); + if (!provider.unsupported && provider.selected == null) { + throw new IllegalStateException("No callback received. Is NPN configured properly?"); + } + return provider.unsupported + ? null + : provider.selected.getBytes(Charsets.US_ASCII); + } + + public static void setNpnProtocols(SSLSocket socket, byte[] npnProtocols) { + // First try Android's APIs. + try { + Class openSslSocketClass = Class.forName( + "org.apache.harmony.xnet.provider.jsse.OpenSSLSocketImpl"); + openSslSocketClass.getMethod("setNpnProtocols", byte[].class) + .invoke(socket, npnProtocols); + } catch (ClassNotFoundException ignored) { + // this isn't Android; fall through to try OpenJDK with Jetty + } catch (IllegalAccessException e) { + throw new AssertionError(); + } catch (InvocationTargetException e) { + throw new AssertionError(); + } catch (NoSuchMethodException e) { + throw new AssertionError(); + } + + // Next try OpenJDK. + List strings = new ArrayList(); + for (int i = 0; i < npnProtocols.length; ) { + int length = npnProtocols[i++]; + strings.add(new String(npnProtocols, i, length, Charsets.US_ASCII)); + i += length; + } + JettyNpnProvider provider = new JettyNpnProvider(); + provider.protocols = strings; + NextProtoNego.put(socket, provider); + } + + private static class JettyNpnProvider + implements NextProtoNego.ClientProvider, NextProtoNego.ServerProvider { + List protocols; + boolean unsupported; + String selected; + + @Override public boolean supports() { + return true; + } + @Override public List protocols() { + return protocols; + } + @Override public void unsupported() { + this.unsupported = true; + } + @Override public void protocolSelected(String selected) { + this.selected = selected; + } + @Override public String selectProtocol(List strings) { + // TODO: use OpenSSL's algorithm which uses 2 lists + System.out.println("CLIENT PROTOCOLS: " + protocols + " SERVER PROTOCOLS: " + strings); + String selected = protocols.get(0); + protocolSelected(selected); + return selected; + } + } + + public static void deleteIfExists(File file) throws IOException { + // okhttp-changed: was Libcore.os.remove() in a try/catch block + file.delete(); + } + + public static void logW(String warning) { + // okhttp-changed: was System.logw() + System.out.println(warning); + } + + public static int getEffectivePort(URI uri) { + return getEffectivePort(uri.getScheme(), uri.getPort()); + } + + public static int getEffectivePort(URL url) { + return getEffectivePort(url.getProtocol(), url.getPort()); + } + + private static int getEffectivePort(String scheme, int specifiedPort) { + if (specifiedPort != -1) { + return specifiedPort; + } + + if ("http".equalsIgnoreCase(scheme)) { + return 80; + } else if ("https".equalsIgnoreCase(scheme)) { + return 443; + } else { + return -1; + } + } + + public static void checkOffsetAndCount(int arrayLength, int offset, int count) { + if ((offset | count) < 0 || offset > arrayLength || arrayLength - offset < count) { + throw new ArrayIndexOutOfBoundsException(); + } + } + + public static void tagSocket(Socket socket) { + } + + public static void untagSocket(Socket socket) throws SocketException { + } + + public static URI toUriLenient(URL url) throws URISyntaxException { + return url.toURI(); // this isn't as good as the built-in toUriLenient + } + + public static void pokeInt(byte[] dst, int offset, int value, ByteOrder order) { + if (order == ByteOrder.BIG_ENDIAN) { + dst[offset++] = (byte) ((value >> 24) & 0xff); + dst[offset++] = (byte) ((value >> 16) & 0xff); + dst[offset++] = (byte) ((value >> 8) & 0xff); + dst[offset ] = (byte) ((value >> 0) & 0xff); + } else { + dst[offset++] = (byte) ((value >> 0) & 0xff); + dst[offset++] = (byte) ((value >> 8) & 0xff); + dst[offset++] = (byte) ((value >> 16) & 0xff); + dst[offset ] = (byte) ((value >> 24) & 0xff); + } + } +} diff --git a/src/main/java/libcore/util/MutableBoolean.java b/src/main/java/libcore/util/MutableBoolean.java new file mode 100644 index 000000000000..359a8f90eaee --- /dev/null +++ b/src/main/java/libcore/util/MutableBoolean.java @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package libcore.util; + +public final class MutableBoolean { + public boolean value; + + public MutableBoolean(boolean value) { + this.value = value; + } +} diff --git a/src/main/java/libcore/util/MutableByte.java b/src/main/java/libcore/util/MutableByte.java new file mode 100644 index 000000000000..13f780b3d7dc --- /dev/null +++ b/src/main/java/libcore/util/MutableByte.java @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package libcore.util; + +public final class MutableByte { + public byte value; + + public MutableByte(byte value) { + this.value = value; + } +} diff --git a/src/main/java/libcore/util/MutableChar.java b/src/main/java/libcore/util/MutableChar.java new file mode 100644 index 000000000000..1cafc3cd4fb9 --- /dev/null +++ b/src/main/java/libcore/util/MutableChar.java @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package libcore.util; + +public final class MutableChar { + public char value; + + public MutableChar(char value) { + this.value = value; + } +} diff --git a/src/main/java/libcore/util/MutableDouble.java b/src/main/java/libcore/util/MutableDouble.java new file mode 100644 index 000000000000..4473ae61c8fa --- /dev/null +++ b/src/main/java/libcore/util/MutableDouble.java @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package libcore.util; + +public final class MutableDouble { + public double value; + + public MutableDouble(double value) { + this.value = value; + } +} diff --git a/src/main/java/libcore/util/MutableFloat.java b/src/main/java/libcore/util/MutableFloat.java new file mode 100644 index 000000000000..f81fba534103 --- /dev/null +++ b/src/main/java/libcore/util/MutableFloat.java @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package libcore.util; + +public final class MutableFloat { + public float value; + + public MutableFloat(float value) { + this.value = value; + } +} diff --git a/src/main/java/libcore/util/MutableInt.java b/src/main/java/libcore/util/MutableInt.java new file mode 100644 index 000000000000..c8feb3aeea5a --- /dev/null +++ b/src/main/java/libcore/util/MutableInt.java @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package libcore.util; + +public final class MutableInt { + public int value; + + public MutableInt(int value) { + this.value = value; + } +} diff --git a/src/main/java/libcore/util/MutableLong.java b/src/main/java/libcore/util/MutableLong.java new file mode 100644 index 000000000000..ad9b78e95698 --- /dev/null +++ b/src/main/java/libcore/util/MutableLong.java @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package libcore.util; + +public final class MutableLong { + public long value; + + public MutableLong(long value) { + this.value = value; + } +} diff --git a/src/main/java/libcore/util/MutableShort.java b/src/main/java/libcore/util/MutableShort.java new file mode 100644 index 000000000000..78b4c33db401 --- /dev/null +++ b/src/main/java/libcore/util/MutableShort.java @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package libcore.util; + +public final class MutableShort { + public short value; + + public MutableShort(short value) { + this.value = value; + } +} diff --git a/src/main/java/libcore/util/Objects.java b/src/main/java/libcore/util/Objects.java new file mode 100644 index 000000000000..781731677cb8 --- /dev/null +++ b/src/main/java/libcore/util/Objects.java @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package libcore.util; + +public final class Objects { + private Objects() {} + + /** + * Returns true if two possibly-null objects are equal. + */ + public static boolean equal(Object a, Object b) { + return a == b || (a != null && a.equals(b)); + } + + public static int hashCode(Object o) { + return (o == null) ? 0 : o.hashCode(); + } +} diff --git a/src/main/java/libcore/util/ResponseSource.java b/src/main/java/libcore/util/ResponseSource.java new file mode 100644 index 000000000000..8e7bfae5c686 --- /dev/null +++ b/src/main/java/libcore/util/ResponseSource.java @@ -0,0 +1,45 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package libcore.util; + +/** + * Where the HTTP client should look for a response. + * + * @hide + */ +public enum ResponseSource { + + /** + * Return the response from the cache immediately. + */ + CACHE, + + /** + * Make a conditional request to the host, returning the cache response if + * the cache is valid and the network response otherwise. + */ + CONDITIONAL_CACHE, + + /** + * Return the response from the network. + */ + NETWORK; + + public boolean requiresConnection() { + return this == CONDITIONAL_CACHE || this == NETWORK; + } +} diff --git a/src/main/java/libcore/util/SneakyThrow.java b/src/main/java/libcore/util/SneakyThrow.java new file mode 100644 index 000000000000..1911788ccd3d --- /dev/null +++ b/src/main/java/libcore/util/SneakyThrow.java @@ -0,0 +1,70 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package libcore.util; + +/** + * Exploits a weakness in the runtime to throw an arbitrary throwable without + * the traditional declaration. This is a dangerous API that should be + * used with great caution. Typically this is useful when rethrowing + * throwables that are of a known range of types. + * + *

    The following code must enumerate several types to rethrow: + *

    + * public void close() throws IOException {
    + *     Throwable thrown = null;
    + *     ...
    + *
    + *     if (thrown != null) {
    + *         if (thrown instanceof IOException) {
    + *             throw (IOException) thrown;
    + *         } else if (thrown instanceof RuntimeException) {
    + *             throw (RuntimeException) thrown;
    + *         } else if (thrown instanceof Error) {
    + *             throw (Error) thrown;
    + *         } else {
    + *             throw new AssertionError();
    + *         }
    + *     }
    + * }
    + * With SneakyThrow, rethrowing is easier: + *
    + * public void close() throws IOException {
    + *     Throwable thrown = null;
    + *     ...
    + *
    + *     if (thrown != null) {
    + *         SneakyThrow.sneakyThrow(thrown);
    + *     }
    + * }
    + */ +public final class SneakyThrow { + private SneakyThrow() {} + + public static void sneakyThrow(Throwable t) { + SneakyThrow.sneakyThrow2(t); + } + + /** + * Exploits unsafety to throw an exception that the compiler wouldn't permit + * but that the runtime doesn't check. See Java Puzzlers #43. + */ + @SuppressWarnings("unchecked") + private static void sneakyThrow2(Throwable t) throws T { + throw (T) t; + } +} diff --git a/src/test/java/libcore/net/http/ExternalSpdyTest.java b/src/test/java/libcore/net/http/ExternalSpdyTest.java new file mode 100644 index 000000000000..d317945f0205 --- /dev/null +++ b/src/test/java/libcore/net/http/ExternalSpdyTest.java @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package libcore.net.http; + +import com.squareup.okhttp.OkHttpsConnection; +import java.io.BufferedReader; +import java.io.InputStreamReader; +import java.net.URL; +import javax.net.ssl.HostnameVerifier; +import javax.net.ssl.SSLSession; +import junit.framework.TestCase; + +public final class ExternalSpdyTest extends TestCase { + + public void testSpdy() throws Exception { + URL url = new URL("https://www.google.ca/"); + OkHttpsConnection connection = OkHttpsConnection.open(url); + + connection.setHostnameVerifier(new HostnameVerifier() { + @Override public boolean verify(String s, SSLSession sslSession) { + System.out.println("VERIFYING " + s); + return true; + } + }); + + int responseCode = connection.getResponseCode(); + System.out.println(responseCode); + + BufferedReader reader = new BufferedReader(new InputStreamReader(connection.getInputStream(), "UTF-8")); + String line; + while ((line = reader.readLine()) != null) { + System.out.println(line); + } + } +} diff --git a/src/test/java/libcore/net/http/NewURLConnectionTest.java b/src/test/java/libcore/net/http/NewURLConnectionTest.java new file mode 100644 index 000000000000..8c6121e15af2 --- /dev/null +++ b/src/test/java/libcore/net/http/NewURLConnectionTest.java @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package libcore.net.http; + +import junit.framework.TestCase; + +public final class NewURLConnectionTest extends TestCase { + + public void testUrlConnection() { + } + + // TODO: write a test that shows pooled connections detect HTTP/1.0 (vs. HTTP/1.1) + + // TODO: write a test that shows POST bodies are retained on AUTH problems (or prove it unnecessary) + + // TODO: cookies + trailers. Do cookie headers get processed too many times? + + // TODO: crash on header names or values containing the '\0' character + + // TODO: crash on empty names and empty values + + // TODO: deflate compression + + // TODO: read the outgoing status line and incoming status line? + +} diff --git a/src/test/java/libcore/net/http/URLConnectionTest.java b/src/test/java/libcore/net/http/URLConnectionTest.java new file mode 100644 index 000000000000..f5faf245786a --- /dev/null +++ b/src/test/java/libcore/net/http/URLConnectionTest.java @@ -0,0 +1,2057 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package libcore.net.http; + +import com.google.mockwebserver.MockResponse; +import com.google.mockwebserver.MockWebServer; +import com.google.mockwebserver.RecordedRequest; +import com.google.mockwebserver.SocketPolicy; +import com.squareup.okhttp.OkHttpConnection; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.Authenticator; +import java.net.CacheRequest; +import java.net.CacheResponse; +import java.net.ConnectException; +import java.net.HttpRetryException; +import java.net.PasswordAuthentication; +import java.net.ProtocolException; +import java.net.Proxy; +import java.net.ResponseCache; +import java.net.SocketTimeoutException; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URL; +import java.net.URLConnection; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicReference; +import java.util.zip.GZIPInputStream; +import java.util.zip.GZIPOutputStream; +import javax.net.ssl.HostnameVerifier; +import javax.net.ssl.SSLSession; +import javax.net.ssl.X509TrustManager; +import junit.framework.TestCase; +import static com.google.mockwebserver.SocketPolicy.DISCONNECT_AT_END; +import static com.google.mockwebserver.SocketPolicy.DISCONNECT_AT_START; +import static com.google.mockwebserver.SocketPolicy.SHUTDOWN_INPUT_AT_END; +import static com.google.mockwebserver.SocketPolicy.SHUTDOWN_OUTPUT_AT_END; + +/** + * Android's URLConnectionTest. + */ +public final class URLConnectionTest extends TestCase { + + private static final Authenticator SIMPLE_AUTHENTICATOR = new Authenticator() { + protected PasswordAuthentication getPasswordAuthentication() { + return new PasswordAuthentication("username", "password".toCharArray()); + } + }; + + /** base64("username:password") */ + private static final String BASE_64_CREDENTIALS = "dXNlcm5hbWU6cGFzc3dvcmQ="; + + private MockWebServer server = new MockWebServer(); + private String hostName; + + @Override protected void setUp() throws Exception { + super.setUp(); + hostName = server.getHostName(); + } + + @Override protected void tearDown() throws Exception { + ResponseCache.setDefault(null); + Authenticator.setDefault(null); + System.clearProperty("proxyHost"); + System.clearProperty("proxyPort"); + System.clearProperty("http.proxyHost"); + System.clearProperty("http.proxyPort"); + System.clearProperty("https.proxyHost"); + System.clearProperty("https.proxyPort"); + server.shutdown(); + super.tearDown(); + } + + private static OkHttpConnection openConnection(URL url) { + return OkHttpConnection.open(url); + } + + public void testRequestHeaders() throws IOException, InterruptedException { + server.enqueue(new MockResponse()); + server.play(); + + OkHttpConnection urlConnection = openConnection(server.getUrl("/")); + urlConnection.addRequestProperty("D", "e"); + urlConnection.addRequestProperty("D", "f"); + assertEquals("f", urlConnection.getRequestProperty("D")); + assertEquals("f", urlConnection.getRequestProperty("d")); + Map> requestHeaders = urlConnection.getRequestProperties(); + assertEquals(newSet("e", "f"), new HashSet(requestHeaders.get("D"))); + assertEquals(newSet("e", "f"), new HashSet(requestHeaders.get("d"))); + try { + requestHeaders.put("G", Arrays.asList("h")); + fail("Modified an unmodifiable view."); + } catch (UnsupportedOperationException expected) { + } + try { + requestHeaders.get("D").add("i"); + fail("Modified an unmodifiable view."); + } catch (UnsupportedOperationException expected) { + } + try { + urlConnection.setRequestProperty(null, "j"); + fail(); + } catch (NullPointerException expected) { + } + try { + urlConnection.addRequestProperty(null, "k"); + fail(); + } catch (NullPointerException expected) { + } + urlConnection.setRequestProperty("NullValue", null); // should fail silently! + assertNull(urlConnection.getRequestProperty("NullValue")); + urlConnection.addRequestProperty("AnotherNullValue", null); // should fail silently! + assertNull(urlConnection.getRequestProperty("AnotherNullValue")); + + urlConnection.getResponseCode(); + RecordedRequest request = server.takeRequest(); + assertContains(request.getHeaders(), "D: e"); + assertContains(request.getHeaders(), "D: f"); + assertContainsNoneMatching(request.getHeaders(), "NullValue.*"); + assertContainsNoneMatching(request.getHeaders(), "AnotherNullValue.*"); + assertContainsNoneMatching(request.getHeaders(), "G:.*"); + assertContainsNoneMatching(request.getHeaders(), "null:.*"); + + try { + urlConnection.addRequestProperty("N", "o"); + fail("Set header after connect"); + } catch (IllegalStateException expected) { + } + try { + urlConnection.setRequestProperty("P", "q"); + fail("Set header after connect"); + } catch (IllegalStateException expected) { + } + try { + urlConnection.getRequestProperties(); + fail(); + } catch (IllegalStateException expected) { + } + } + + public void testGetRequestPropertyReturnsLastValue() throws Exception { + server.play(); + OkHttpConnection urlConnection = openConnection(server.getUrl("/")); + urlConnection.addRequestProperty("A", "value1"); + urlConnection.addRequestProperty("A", "value2"); + assertEquals("value2", urlConnection.getRequestProperty("A")); + } + + public void testResponseHeaders() throws IOException, InterruptedException { + server.enqueue(new MockResponse() + .setStatus("HTTP/1.0 200 Fantastic") + .addHeader("A: c") + .addHeader("B: d") + .addHeader("A: e") + .setChunkedBody("ABCDE\nFGHIJ\nKLMNO\nPQR", 8)); + server.play(); + + OkHttpConnection urlConnection = openConnection(server.getUrl("/")); + assertEquals(200, urlConnection.getResponseCode()); + assertEquals("Fantastic", urlConnection.getResponseMessage()); + assertEquals("HTTP/1.0 200 Fantastic", urlConnection.getHeaderField(null)); + Map> responseHeaders = urlConnection.getHeaderFields(); + assertEquals(Arrays.asList("HTTP/1.0 200 Fantastic"), responseHeaders.get(null)); + assertEquals(newSet("c", "e"), new HashSet(responseHeaders.get("A"))); + assertEquals(newSet("c", "e"), new HashSet(responseHeaders.get("a"))); + try { + responseHeaders.put("N", Arrays.asList("o")); + fail("Modified an unmodifiable view."); + } catch (UnsupportedOperationException expected) { + } + try { + responseHeaders.get("A").add("f"); + fail("Modified an unmodifiable view."); + } catch (UnsupportedOperationException expected) { + } + assertEquals("A", urlConnection.getHeaderFieldKey(0)); + assertEquals("c", urlConnection.getHeaderField(0)); + assertEquals("B", urlConnection.getHeaderFieldKey(1)); + assertEquals("d", urlConnection.getHeaderField(1)); + assertEquals("A", urlConnection.getHeaderFieldKey(2)); + assertEquals("e", urlConnection.getHeaderField(2)); + } + + public void testGetErrorStreamOnSuccessfulRequest() throws Exception { + server.enqueue(new MockResponse().setBody("A")); + server.play(); + OkHttpConnection connection = openConnection(server.getUrl("/")); + assertNull(connection.getErrorStream()); + } + + public void testGetErrorStreamOnUnsuccessfulRequest() throws Exception { + server.enqueue(new MockResponse().setResponseCode(404).setBody("A")); + server.play(); + OkHttpConnection connection = openConnection(server.getUrl("/")); + assertEquals("A", readAscii(connection.getErrorStream(), Integer.MAX_VALUE)); + } + + // Check that if we don't read to the end of a response, the next request on the + // recycled connection doesn't get the unread tail of the first request's response. + // http://code.google.com/p/android/issues/detail?id=2939 + public void test_2939() throws Exception { + MockResponse response = new MockResponse().setChunkedBody("ABCDE\nFGHIJ\nKLMNO\nPQR", 8); + + server.enqueue(response); + server.enqueue(response); + server.play(); + + assertContent("ABCDE", openConnection(server.getUrl("/")), 5); + assertContent("ABCDE", openConnection(server.getUrl("/")), 5); + } + + // Check that we recognize a few basic mime types by extension. + // http://code.google.com/p/android/issues/detail?id=10100 + public void test_10100() throws Exception { + assertEquals("image/jpeg", URLConnection.guessContentTypeFromName("someFile.jpg")); + assertEquals("application/pdf", URLConnection.guessContentTypeFromName("stuff.pdf")); + } + + public void testConnectionsArePooled() throws Exception { + MockResponse response = new MockResponse().setBody("ABCDEFGHIJKLMNOPQR"); + + server.enqueue(response); + server.enqueue(response); + server.enqueue(response); + server.play(); + + assertContent("ABCDEFGHIJKLMNOPQR", openConnection(server.getUrl("/foo"))); + assertEquals(0, server.takeRequest().getSequenceNumber()); + assertContent("ABCDEFGHIJKLMNOPQR", openConnection(server.getUrl("/bar?baz=quux"))); + assertEquals(1, server.takeRequest().getSequenceNumber()); + assertContent("ABCDEFGHIJKLMNOPQR", openConnection(server.getUrl("/z"))); + assertEquals(2, server.takeRequest().getSequenceNumber()); + } + + public void testChunkedConnectionsArePooled() throws Exception { + MockResponse response = new MockResponse().setChunkedBody("ABCDEFGHIJKLMNOPQR", 5); + + server.enqueue(response); + server.enqueue(response); + server.enqueue(response); + server.play(); + + assertContent("ABCDEFGHIJKLMNOPQR", openConnection(server.getUrl("/foo"))); + assertEquals(0, server.takeRequest().getSequenceNumber()); + assertContent("ABCDEFGHIJKLMNOPQR", openConnection(server.getUrl("/bar?baz=quux"))); + assertEquals(1, server.takeRequest().getSequenceNumber()); + assertContent("ABCDEFGHIJKLMNOPQR", openConnection(server.getUrl("/z"))); + assertEquals(2, server.takeRequest().getSequenceNumber()); + } + + public void testServerClosesSocket() throws Exception { + testServerClosesOutput(DISCONNECT_AT_END); + } + + public void testServerShutdownInput() throws Exception { + testServerClosesOutput(SHUTDOWN_INPUT_AT_END); + } + + public void SUPPRESSED_testServerShutdownOutput() throws Exception { + testServerClosesOutput(SHUTDOWN_OUTPUT_AT_END); + } + + private void testServerClosesOutput(SocketPolicy socketPolicy) throws Exception { + server.enqueue(new MockResponse() + .setBody("This connection won't pool properly") + .setSocketPolicy(socketPolicy)); + server.enqueue(new MockResponse() + .setBody("This comes after a busted connection")); + server.play(); + + assertContent("This connection won't pool properly", openConnection(server.getUrl("/a"))); + assertEquals(0, server.takeRequest().getSequenceNumber()); + assertContent("This comes after a busted connection", openConnection(server.getUrl("/b"))); + // sequence number 0 means the HTTP socket connection was not reused + assertEquals(0, server.takeRequest().getSequenceNumber()); + } + + enum WriteKind { BYTE_BY_BYTE, SMALL_BUFFERS, LARGE_BUFFERS } + + public void test_chunkedUpload_byteByByte() throws Exception { + doUpload(TransferKind.CHUNKED, WriteKind.BYTE_BY_BYTE); + } + + public void test_chunkedUpload_smallBuffers() throws Exception { + doUpload(TransferKind.CHUNKED, WriteKind.SMALL_BUFFERS); + } + + public void test_chunkedUpload_largeBuffers() throws Exception { + doUpload(TransferKind.CHUNKED, WriteKind.LARGE_BUFFERS); + } + + public void SUPPRESSED_test_fixedLengthUpload_byteByByte() throws Exception { + doUpload(TransferKind.FIXED_LENGTH, WriteKind.BYTE_BY_BYTE); + } + + public void test_fixedLengthUpload_smallBuffers() throws Exception { + doUpload(TransferKind.FIXED_LENGTH, WriteKind.SMALL_BUFFERS); + } + + public void test_fixedLengthUpload_largeBuffers() throws Exception { + doUpload(TransferKind.FIXED_LENGTH, WriteKind.LARGE_BUFFERS); + } + + private void doUpload(TransferKind uploadKind, WriteKind writeKind) throws Exception { + int n = 512*1024; + server.setBodyLimit(0); + server.enqueue(new MockResponse()); + server.play(); + + OkHttpConnection conn = openConnection(server.getUrl("/")); + conn.setDoOutput(true); + conn.setRequestMethod("POST"); + if (uploadKind == TransferKind.CHUNKED) { + conn.setChunkedStreamingMode(-1); + } else { + conn.setFixedLengthStreamingMode(n); + } + OutputStream out = conn.getOutputStream(); + if (writeKind == WriteKind.BYTE_BY_BYTE) { + for (int i = 0; i < n; ++i) { + out.write('x'); + } + } else { + byte[] buf = new byte[writeKind == WriteKind.SMALL_BUFFERS ? 256 : 64*1024]; + Arrays.fill(buf, (byte) 'x'); + for (int i = 0; i < n; i += buf.length) { + out.write(buf, 0, Math.min(buf.length, n - i)); + } + } + out.close(); + assertEquals(200, conn.getResponseCode()); + RecordedRequest request = server.takeRequest(); + assertEquals(n, request.getBodySize()); + if (uploadKind == TransferKind.CHUNKED) { + assertTrue(request.getChunkSizes().size() > 0); + } else { + assertTrue(request.getChunkSizes().isEmpty()); + } + } + + public void testGetResponseCodeNoResponseBody() throws Exception { + server.enqueue(new MockResponse() + .addHeader("abc: def")); + server.play(); + + URL url = server.getUrl("/"); + OkHttpConnection conn = openConnection(url); + conn.setDoInput(false); + assertEquals("def", conn.getHeaderField("abc")); + assertEquals(200, conn.getResponseCode()); + try { + conn.getInputStream(); + fail(); + } catch (ProtocolException expected) { + } + } + +// public void testConnectViaHttps() throws IOException, InterruptedException { +// TestSSLContext testSSLContext = TestSSLContext.create(); +// +// server.useHttps(testSSLContext.serverContext.getSocketFactory(), false); +// server.enqueue(new MockResponse().setBody("this response comes via HTTPS")); +// server.play(); +// +// HttpsURLConnection connection = (HttpsURLConnection) server.getUrl("/foo").openConnection(); +// connection.setSSLSocketFactory(testSSLContext.clientContext.getSocketFactory()); +// +// assertContent("this response comes via HTTPS", connection); +// +// RecordedRequest request = server.takeRequest(); +// assertEquals("GET /foo HTTP/1.1", request.getRequestLine()); +// } +// +// public void testConnectViaHttpsReusingConnections() throws IOException, InterruptedException { +// TestSSLContext testSSLContext = TestSSLContext.create(); +// +// server.useHttps(testSSLContext.serverContext.getSocketFactory(), false); +// server.enqueue(new MockResponse().setBody("this response comes via HTTPS")); +// server.enqueue(new MockResponse().setBody("another response via HTTPS")); +// server.play(); +// +// HttpsURLConnection connection = (HttpsURLConnection) server.getUrl("/").openConnection(); +// connection.setSSLSocketFactory(testSSLContext.clientContext.getSocketFactory()); +// assertContent("this response comes via HTTPS", connection); +// +// connection = (HttpsURLConnection) server.getUrl("/").openConnection(); +// connection.setSSLSocketFactory(testSSLContext.clientContext.getSocketFactory()); +// assertContent("another response via HTTPS", connection); +// +// assertEquals(0, server.takeRequest().getSequenceNumber()); +// assertEquals(1, server.takeRequest().getSequenceNumber()); +// } +// +// public void testConnectViaHttpsReusingConnectionsDifferentFactories() +// throws IOException, InterruptedException { +// TestSSLContext testSSLContext = TestSSLContext.create(); +// +// server.useHttps(testSSLContext.serverContext.getSocketFactory(), false); +// server.enqueue(new MockResponse().setBody("this response comes via HTTPS")); +// server.enqueue(new MockResponse().setBody("another response via HTTPS")); +// server.play(); +// +// // install a custom SSL socket factory so the server can be authorized +// HttpsURLConnection connection = (HttpsURLConnection) server.getUrl("/").openConnection(); +// connection.setSSLSocketFactory(testSSLContext.clientContext.getSocketFactory()); +// assertContent("this response comes via HTTPS", connection); +// +// connection = (HttpsURLConnection) server.getUrl("/").openConnection(); +// try { +// readAscii(connection.getInputStream(), Integer.MAX_VALUE); +// fail("without an SSL socket factory, the connection should fail"); +// } catch (SSLException expected) { +// } +// } +// +// public void testConnectViaHttpsWithSSLFallback() throws IOException, InterruptedException { +// TestSSLContext testSSLContext = TestSSLContext.create(); +// +// server.useHttps(testSSLContext.serverContext.getSocketFactory(), false); +// server.enqueue(new MockResponse().setSocketPolicy(DISCONNECT_AT_START)); +// server.enqueue(new MockResponse().setBody("this response comes via SSL")); +// server.play(); +// +// HttpsURLConnection connection = (HttpsURLConnection) server.getUrl("/foo").openConnection(); +// connection.setSSLSocketFactory(testSSLContext.clientContext.getSocketFactory()); +// +// assertContent("this response comes via SSL", connection); +// +// RecordedRequest request = server.takeRequest(); +// assertEquals("GET /foo HTTP/1.1", request.getRequestLine()); +// } +// +// /** +// * Verify that we don't retry connections on certificate verification errors. +// * +// * http://code.google.com/p/android/issues/detail?id=13178 +// */ +// public void testConnectViaHttpsToUntrustedServer() throws IOException, InterruptedException { +// TestSSLContext testSSLContext = TestSSLContext.create(TestKeyStore.getClientCA2(), +// TestKeyStore.getServer()); +// +// server.useHttps(testSSLContext.serverContext.getSocketFactory(), false); +// server.enqueue(new MockResponse()); // unused +// server.play(); +// +// HttpsURLConnection connection = (HttpsURLConnection) server.getUrl("/foo").openConnection(); +// connection.setSSLSocketFactory(testSSLContext.clientContext.getSocketFactory()); +// try { +// connection.getInputStream(); +// fail(); +// } catch (SSLHandshakeException expected) { +// assertTrue(expected.getCause() instanceof CertificateException); +// } +// assertEquals(0, server.getRequestCount()); +// } + + public void testConnectViaProxyUsingProxyArg() throws Exception { + testConnectViaProxy(ProxyConfig.CREATE_ARG); + } + + public void testConnectViaProxyUsingProxySystemProperty() throws Exception { + testConnectViaProxy(ProxyConfig.PROXY_SYSTEM_PROPERTY); + } + + public void testConnectViaProxyUsingHttpProxySystemProperty() throws Exception { + testConnectViaProxy(ProxyConfig.HTTP_PROXY_SYSTEM_PROPERTY); + } + + private void testConnectViaProxy(ProxyConfig proxyConfig) throws Exception { + MockResponse mockResponse = new MockResponse().setBody("this response comes via a proxy"); + server.enqueue(mockResponse); + server.play(); + + URL url = new URL("http://android.com/foo"); + OkHttpConnection connection = proxyConfig.connect(server, url); + assertContent("this response comes via a proxy", connection); + + RecordedRequest request = server.takeRequest(); + assertEquals("GET http://android.com/foo HTTP/1.1", request.getRequestLine()); + assertContains(request.getHeaders(), "Host: android.com"); + } + + public void testContentDisagreesWithContentLengthHeader() throws IOException { + server.enqueue(new MockResponse() + .setBody("abc\r\nYOU SHOULD NOT SEE THIS") + .clearHeaders() + .addHeader("Content-Length: 3")); + server.play(); + + assertContent("abc", openConnection(server.getUrl("/"))); + } + + public void testContentDisagreesWithChunkedHeader() throws IOException { + MockResponse mockResponse = new MockResponse(); + mockResponse.setChunkedBody("abc", 3); + ByteArrayOutputStream bytesOut = new ByteArrayOutputStream(); + bytesOut.write(mockResponse.getBody()); + bytesOut.write("\r\nYOU SHOULD NOT SEE THIS".getBytes()); + mockResponse.setBody(bytesOut.toByteArray()); + mockResponse.clearHeaders(); + mockResponse.addHeader("Transfer-encoding: chunked"); + + server.enqueue(mockResponse); + server.play(); + + assertContent("abc", openConnection(server.getUrl("/"))); + } + +// public void testConnectViaHttpProxyToHttpsUsingProxyArgWithNoProxy() throws Exception { +// testConnectViaDirectProxyToHttps(ProxyConfig.NO_PROXY); +// } +// +// public void testConnectViaHttpProxyToHttpsUsingHttpProxySystemProperty() throws Exception { +// // https should not use http proxy +// testConnectViaDirectProxyToHttps(ProxyConfig.HTTP_PROXY_SYSTEM_PROPERTY); +// } +// +// private void testConnectViaDirectProxyToHttps(ProxyConfig proxyConfig) throws Exception { +// TestSSLContext testSSLContext = TestSSLContext.create(); +// +// server.useHttps(testSSLContext.serverContext.getSocketFactory(), false); +// server.enqueue(new MockResponse().setBody("this response comes via HTTPS")); +// server.play(); +// +// URL url = server.getUrl("/foo"); +// HttpsURLConnection connection = (HttpsURLConnection) proxyConfig.connect(server, url); +// connection.setSSLSocketFactory(testSSLContext.clientContext.getSocketFactory()); +// +// assertContent("this response comes via HTTPS", connection); +// +// RecordedRequest request = server.takeRequest(); +// assertEquals("GET /foo HTTP/1.1", request.getRequestLine()); +// } +// +// public void testConnectViaHttpProxyToHttpsUsingProxyArg() throws Exception { +// testConnectViaHttpProxyToHttps(ProxyConfig.CREATE_ARG); +// } +// +// /** +// * We weren't honoring all of the appropriate proxy system properties when +// * connecting via HTTPS. http://b/3097518 +// */ +// public void testConnectViaHttpProxyToHttpsUsingProxySystemProperty() throws Exception { +// testConnectViaHttpProxyToHttps(ProxyConfig.PROXY_SYSTEM_PROPERTY); +// } +// +// public void testConnectViaHttpProxyToHttpsUsingHttpsProxySystemProperty() throws Exception { +// testConnectViaHttpProxyToHttps(ProxyConfig.HTTPS_PROXY_SYSTEM_PROPERTY); +// } +// +// /** +// * We were verifying the wrong hostname when connecting to an HTTPS site +// * through a proxy. http://b/3097277 +// */ +// private void testConnectViaHttpProxyToHttps(ProxyConfig proxyConfig) throws Exception { +// TestSSLContext testSSLContext = TestSSLContext.create(); +// RecordingHostnameVerifier hostnameVerifier = new RecordingHostnameVerifier(); +// +// server.useHttps(testSSLContext.serverContext.getSocketFactory(), true); +// server.enqueue(new MockResponse() +// .setSocketPolicy(SocketPolicy.UPGRADE_TO_SSL_AT_END) +// .clearHeaders()); +// server.enqueue(new MockResponse().setBody("this response comes via a secure proxy")); +// server.play(); +// +// URL url = new URL("https://android.com/foo"); +// HttpsURLConnection connection = (HttpsURLConnection) proxyConfig.connect(server, url); +// connection.setSSLSocketFactory(testSSLContext.clientContext.getSocketFactory()); +// connection.setHostnameVerifier(hostnameVerifier); +// +// assertContent("this response comes via a secure proxy", connection); +// +// RecordedRequest connect = server.takeRequest(); +// assertEquals("Connect line failure on proxy", +// "CONNECT android.com:443 HTTP/1.1", connect.getRequestLine()); +// assertContains(connect.getHeaders(), "Host: android.com"); +// +// RecordedRequest get = server.takeRequest(); +// assertEquals("GET /foo HTTP/1.1", get.getRequestLine()); +// assertContains(get.getHeaders(), "Host: android.com"); +// assertEquals(Arrays.asList("verify android.com"), hostnameVerifier.calls); +// } +// +// /** +// * Test which headers are sent unencrypted to the HTTP proxy. +// */ +// public void testProxyConnectIncludesProxyHeadersOnly() +// throws IOException, InterruptedException { +// RecordingHostnameVerifier hostnameVerifier = new RecordingHostnameVerifier(); +// TestSSLContext testSSLContext = TestSSLContext.create(); +// +// server.useHttps(testSSLContext.serverContext.getSocketFactory(), true); +// server.enqueue(new MockResponse() +// .setSocketPolicy(SocketPolicy.UPGRADE_TO_SSL_AT_END) +// .clearHeaders()); +// server.enqueue(new MockResponse().setBody("encrypted response from the origin server")); +// server.play(); +// +// URL url = new URL("https://android.com/foo"); +// HttpsURLConnection connection = (HttpsURLConnection) url.openConnection( +// server.toProxyAddress()); +// connection.addRequestProperty("Private", "Secret"); +// connection.addRequestProperty("Proxy-Authorization", "bar"); +// connection.addRequestProperty("User-Agent", "baz"); +// connection.setSSLSocketFactory(testSSLContext.clientContext.getSocketFactory()); +// connection.setHostnameVerifier(hostnameVerifier); +// assertContent("encrypted response from the origin server", connection); +// +// RecordedRequest connect = server.takeRequest(); +// assertContainsNoneMatching(connect.getHeaders(), "Private.*"); +// assertContains(connect.getHeaders(), "Proxy-Authorization: bar"); +// assertContains(connect.getHeaders(), "User-Agent: baz"); +// assertContains(connect.getHeaders(), "Host: android.com"); +// assertContains(connect.getHeaders(), "Proxy-Connection: Keep-Alive"); +// +// RecordedRequest get = server.takeRequest(); +// assertContains(get.getHeaders(), "Private: Secret"); +// assertEquals(Arrays.asList("verify android.com"), hostnameVerifier.calls); +// } +// +// public void testProxyAuthenticateOnConnect() throws Exception { +// Authenticator.setDefault(SIMPLE_AUTHENTICATOR); +// TestSSLContext testSSLContext = TestSSLContext.create(); +// server.useHttps(testSSLContext.serverContext.getSocketFactory(), true); +// server.enqueue(new MockResponse() +// .setResponseCode(407) +// .addHeader("Proxy-Authenticate: Basic realm=\"localhost\"")); +// server.enqueue(new MockResponse() +// .setSocketPolicy(SocketPolicy.UPGRADE_TO_SSL_AT_END) +// .clearHeaders()); +// server.enqueue(new MockResponse().setBody("A")); +// server.play(); +// +// URL url = new URL("https://android.com/foo"); +// HttpsURLConnection connection = (HttpsURLConnection) url.openConnection( +// server.toProxyAddress()); +// connection.setSSLSocketFactory(testSSLContext.clientContext.getSocketFactory()); +// connection.setHostnameVerifier(new RecordingHostnameVerifier()); +// assertContent("A", connection); +// +// RecordedRequest connect1 = server.takeRequest(); +// assertEquals("CONNECT android.com:443 HTTP/1.1", connect1.getRequestLine()); +// assertContainsNoneMatching(connect1.getHeaders(), "Proxy\\-Authorization.*"); +// +// RecordedRequest connect2 = server.takeRequest(); +// assertEquals("CONNECT android.com:443 HTTP/1.1", connect2.getRequestLine()); +// assertContains(connect2.getHeaders(), "Proxy-Authorization: Basic " + BASE_64_CREDENTIALS); +// +// RecordedRequest get = server.takeRequest(); +// assertEquals("GET /foo HTTP/1.1", get.getRequestLine()); +// assertContainsNoneMatching(get.getHeaders(), "Proxy\\-Authorization.*"); +// } + + public void testDisconnectedConnection() throws IOException { + server.enqueue(new MockResponse().setBody("ABCDEFGHIJKLMNOPQR")); + server.play(); + + OkHttpConnection connection = openConnection(server.getUrl("/")); + InputStream in = connection.getInputStream(); + assertEquals('A', (char) in.read()); + connection.disconnect(); + try { + in.read(); + fail("Expected a connection closed exception"); + } catch (IOException expected) { + } + } + + public void testDisconnectBeforeConnect() throws IOException { + server.enqueue(new MockResponse().setBody("A")); + server.play(); + + OkHttpConnection connection = openConnection(server.getUrl("/")); + connection.disconnect(); + + assertContent("A", connection); + assertEquals(200, connection.getResponseCode()); + } + + public void testDefaultRequestProperty() throws Exception { + URLConnection.setDefaultRequestProperty("X-testSetDefaultRequestProperty", "A"); + assertNull(URLConnection.getDefaultRequestProperty("X-setDefaultRequestProperty")); + } + + /** + * Reads {@code count} characters from the stream. If the stream is + * exhausted before {@code count} characters can be read, the remaining + * characters are returned and the stream is closed. + */ + private String readAscii(InputStream in, int count) throws IOException { + StringBuilder result = new StringBuilder(); + for (int i = 0; i < count; i++) { + int value = in.read(); + if (value == -1) { + in.close(); + break; + } + result.append((char) value); + } + return result.toString(); + } + + public void testMarkAndResetWithContentLengthHeader() throws IOException { + testMarkAndReset(TransferKind.FIXED_LENGTH); + } + + public void testMarkAndResetWithChunkedEncoding() throws IOException { + testMarkAndReset(TransferKind.CHUNKED); + } + + public void testMarkAndResetWithNoLengthHeaders() throws IOException { + testMarkAndReset(TransferKind.END_OF_STREAM); + } + + private void testMarkAndReset(TransferKind transferKind) throws IOException { + MockResponse response = new MockResponse(); + transferKind.setBody(response, "ABCDEFGHIJKLMNOPQRSTUVWXYZ", 1024); + server.enqueue(response); + server.enqueue(response); + server.play(); + + InputStream in = openConnection(server.getUrl("/")).getInputStream(); + assertFalse("This implementation claims to support mark().", in.markSupported()); + in.mark(5); + assertEquals("ABCDE", readAscii(in, 5)); + try { + in.reset(); + fail(); + } catch (IOException expected) { + } + assertEquals("FGHIJKLMNOPQRSTUVWXYZ", readAscii(in, Integer.MAX_VALUE)); + assertContent("ABCDEFGHIJKLMNOPQRSTUVWXYZ", openConnection(server.getUrl("/"))); + } + + /** + * We've had a bug where we forget the HTTP response when we see response + * code 401. This causes a new HTTP request to be issued for every call into + * the URLConnection. + */ + public void SUPPRESSED_testUnauthorizedResponseHandling() throws IOException { + MockResponse response = new MockResponse() + .addHeader("WWW-Authenticate: challenge") + .setResponseCode(401) // UNAUTHORIZED + .setBody("Unauthorized"); + server.enqueue(response); + server.enqueue(response); + server.enqueue(response); + server.play(); + + URL url = server.getUrl("/"); + OkHttpConnection conn = openConnection(url); + + assertEquals(401, conn.getResponseCode()); + assertEquals(401, conn.getResponseCode()); + assertEquals(401, conn.getResponseCode()); + assertEquals(1, server.getRequestCount()); + } + + public void testNonHexChunkSize() throws IOException { + server.enqueue(new MockResponse() + .setBody("5\r\nABCDE\r\nG\r\nFGHIJKLMNOPQRSTU\r\n0\r\n\r\n") + .clearHeaders() + .addHeader("Transfer-encoding: chunked")); + server.play(); + + URLConnection connection = openConnection(server.getUrl("/")); + try { + readAscii(connection.getInputStream(), Integer.MAX_VALUE); + fail(); + } catch (IOException e) { + } + } + + public void testMissingChunkBody() throws IOException { + server.enqueue(new MockResponse() + .setBody("5") + .clearHeaders() + .addHeader("Transfer-encoding: chunked") + .setSocketPolicy(DISCONNECT_AT_END)); + server.play(); + + URLConnection connection = openConnection(server.getUrl("/")); + try { + readAscii(connection.getInputStream(), Integer.MAX_VALUE); + fail(); + } catch (IOException e) { + } + } + + /** + * This test checks whether connections are gzipped by default. This + * behavior in not required by the API, so a failure of this test does not + * imply a bug in the implementation. + */ + public void testGzipEncodingEnabledByDefault() throws IOException, InterruptedException { + server.enqueue(new MockResponse() + .setBody(gzip("ABCABCABC".getBytes("UTF-8"))) + .addHeader("Content-Encoding: gzip")); + server.play(); + + URLConnection connection = openConnection(server.getUrl("/")); + assertEquals("ABCABCABC", readAscii(connection.getInputStream(), Integer.MAX_VALUE)); + assertNull(connection.getContentEncoding()); + + RecordedRequest request = server.takeRequest(); + assertContains(request.getHeaders(), "Accept-Encoding: gzip"); + } + + public void testClientConfiguredGzipContentEncoding() throws Exception { + server.enqueue(new MockResponse() + .setBody(gzip("ABCDEFGHIJKLMNOPQRSTUVWXYZ".getBytes("UTF-8"))) + .addHeader("Content-Encoding: gzip")); + server.play(); + + URLConnection connection = openConnection(server.getUrl("/")); + connection.addRequestProperty("Accept-Encoding", "gzip"); + InputStream gunzippedIn = new GZIPInputStream(connection.getInputStream()); + assertEquals("ABCDEFGHIJKLMNOPQRSTUVWXYZ", readAscii(gunzippedIn, Integer.MAX_VALUE)); + + RecordedRequest request = server.takeRequest(); + assertContains(request.getHeaders(), "Accept-Encoding: gzip"); + } + + public void testGzipAndConnectionReuseWithFixedLength() throws Exception { + testClientConfiguredGzipContentEncodingAndConnectionReuse(TransferKind.FIXED_LENGTH); + } + + public void testGzipAndConnectionReuseWithChunkedEncoding() throws Exception { + testClientConfiguredGzipContentEncodingAndConnectionReuse(TransferKind.CHUNKED); + } + + public void testClientConfiguredCustomContentEncoding() throws Exception { + server.enqueue(new MockResponse() + .setBody("ABCDE") + .addHeader("Content-Encoding: custom")); + server.play(); + + URLConnection connection = openConnection(server.getUrl("/")); + connection.addRequestProperty("Accept-Encoding", "custom"); + assertEquals("ABCDE", readAscii(connection.getInputStream(), Integer.MAX_VALUE)); + + RecordedRequest request = server.takeRequest(); + assertContains(request.getHeaders(), "Accept-Encoding: custom"); + } + + /** + * Test a bug where gzip input streams weren't exhausting the input stream, + * which corrupted the request that followed. + * http://code.google.com/p/android/issues/detail?id=7059 + */ + private void testClientConfiguredGzipContentEncodingAndConnectionReuse( + TransferKind transferKind) throws Exception { + MockResponse responseOne = new MockResponse(); + responseOne.addHeader("Content-Encoding: gzip"); + transferKind.setBody(responseOne, gzip("one (gzipped)".getBytes("UTF-8")), 5); + server.enqueue(responseOne); + MockResponse responseTwo = new MockResponse(); + transferKind.setBody(responseTwo, "two (identity)", 5); + server.enqueue(responseTwo); + server.play(); + + URLConnection connection = openConnection(server.getUrl("/")); + connection.addRequestProperty("Accept-Encoding", "gzip"); + InputStream gunzippedIn = new GZIPInputStream(connection.getInputStream()); + assertEquals("one (gzipped)", readAscii(gunzippedIn, Integer.MAX_VALUE)); + assertEquals(0, server.takeRequest().getSequenceNumber()); + + connection = openConnection(server.getUrl("/")); + assertEquals("two (identity)", readAscii(connection.getInputStream(), Integer.MAX_VALUE)); + assertEquals(1, server.takeRequest().getSequenceNumber()); + } + + /** + * Obnoxiously test that the chunk sizes transmitted exactly equal the + * requested data+chunk header size. Although setChunkedStreamingMode() + * isn't specific about whether the size applies to the data or the + * complete chunk, the RI interprets it as a complete chunk. + */ + public void testSetChunkedStreamingMode() throws IOException, InterruptedException { + server.enqueue(new MockResponse()); + server.play(); + + OkHttpConnection urlConnection = openConnection(server.getUrl("/")); + urlConnection.setChunkedStreamingMode(8); + urlConnection.setDoOutput(true); + OutputStream outputStream = urlConnection.getOutputStream(); + outputStream.write("ABCDEFGHIJKLMNOPQ".getBytes("US-ASCII")); + assertEquals(200, urlConnection.getResponseCode()); + + RecordedRequest request = server.takeRequest(); + assertEquals("ABCDEFGHIJKLMNOPQ", new String(request.getBody(), "US-ASCII")); + assertEquals(Arrays.asList(3, 3, 3, 3, 3, 2), request.getChunkSizes()); + } + + public void testAuthenticateWithFixedLengthStreaming() throws Exception { + testAuthenticateWithStreamingPost(StreamingMode.FIXED_LENGTH); + } + + public void testAuthenticateWithChunkedStreaming() throws Exception { + testAuthenticateWithStreamingPost(StreamingMode.CHUNKED); + } + + private void testAuthenticateWithStreamingPost(StreamingMode streamingMode) throws Exception { + MockResponse pleaseAuthenticate = new MockResponse() + .setResponseCode(401) + .addHeader("WWW-Authenticate: Basic realm=\"protected area\"") + .setBody("Please authenticate."); + server.enqueue(pleaseAuthenticate); + server.play(); + + Authenticator.setDefault(SIMPLE_AUTHENTICATOR); + OkHttpConnection connection = openConnection(server.getUrl("/")); + connection.setDoOutput(true); + byte[] requestBody = { 'A', 'B', 'C', 'D' }; + if (streamingMode == StreamingMode.FIXED_LENGTH) { + connection.setFixedLengthStreamingMode(requestBody.length); + } else if (streamingMode == StreamingMode.CHUNKED) { + connection.setChunkedStreamingMode(0); + } + OutputStream outputStream = connection.getOutputStream(); + outputStream.write(requestBody); + outputStream.close(); + try { + connection.getInputStream(); + fail(); + } catch (HttpRetryException expected) { + } + + // no authorization header for the request... + RecordedRequest request = server.takeRequest(); + assertContainsNoneMatching(request.getHeaders(), "Authorization: Basic .*"); + assertEquals(Arrays.toString(requestBody), Arrays.toString(request.getBody())); + } + + public void testSetValidRequestMethod() throws Exception { + server.play(); + assertValidRequestMethod("GET"); + assertValidRequestMethod("DELETE"); + assertValidRequestMethod("HEAD"); + assertValidRequestMethod("OPTIONS"); + assertValidRequestMethod("POST"); + assertValidRequestMethod("PUT"); + assertValidRequestMethod("TRACE"); + } + + private void assertValidRequestMethod(String requestMethod) throws Exception { + OkHttpConnection connection = openConnection(server.getUrl("/")); + connection.setRequestMethod(requestMethod); + assertEquals(requestMethod, connection.getRequestMethod()); + } + + public void testSetInvalidRequestMethodLowercase() throws Exception { + server.play(); + assertInvalidRequestMethod("get"); + } + + public void testSetInvalidRequestMethodConnect() throws Exception { + server.play(); + assertInvalidRequestMethod("CONNECT"); + } + + private void assertInvalidRequestMethod(String requestMethod) throws Exception { + OkHttpConnection connection = openConnection(server.getUrl("/")); + try { + connection.setRequestMethod(requestMethod); + fail(); + } catch (ProtocolException expected) { + } + } + + public void testCannotSetNegativeFixedLengthStreamingMode() throws Exception { + server.play(); + OkHttpConnection connection = openConnection(server.getUrl("/")); + try { + connection.setFixedLengthStreamingMode(-2); + fail(); + } catch (IllegalArgumentException expected) { + } + } + + public void testCanSetNegativeChunkedStreamingMode() throws Exception { + server.play(); + OkHttpConnection connection = openConnection(server.getUrl("/")); + connection.setChunkedStreamingMode(-2); + } + + public void testCannotSetFixedLengthStreamingModeAfterConnect() throws Exception { + server.enqueue(new MockResponse().setBody("A")); + server.play(); + OkHttpConnection connection = openConnection(server.getUrl("/")); + assertEquals("A", readAscii(connection.getInputStream(), Integer.MAX_VALUE)); + try { + connection.setFixedLengthStreamingMode(1); + fail(); + } catch (IllegalStateException expected) { + } + } + + public void testCannotSetChunkedStreamingModeAfterConnect() throws Exception { + server.enqueue(new MockResponse().setBody("A")); + server.play(); + OkHttpConnection connection = openConnection(server.getUrl("/")); + assertEquals("A", readAscii(connection.getInputStream(), Integer.MAX_VALUE)); + try { + connection.setChunkedStreamingMode(1); + fail(); + } catch (IllegalStateException expected) { + } + } + + public void testCannotSetFixedLengthStreamingModeAfterChunkedStreamingMode() throws Exception { + server.play(); + OkHttpConnection connection = openConnection(server.getUrl("/")); + connection.setChunkedStreamingMode(1); + try { + connection.setFixedLengthStreamingMode(1); + fail(); + } catch (IllegalStateException expected) { + } + } + + public void testCannotSetChunkedStreamingModeAfterFixedLengthStreamingMode() throws Exception { + server.play(); + OkHttpConnection connection = openConnection(server.getUrl("/")); + connection.setFixedLengthStreamingMode(1); + try { + connection.setChunkedStreamingMode(1); + fail(); + } catch (IllegalStateException expected) { + } + } + +// public void testSecureFixedLengthStreaming() throws Exception { +// testSecureStreamingPost(StreamingMode.FIXED_LENGTH); +// } +// +// public void testSecureChunkedStreaming() throws Exception { +// testSecureStreamingPost(StreamingMode.CHUNKED); +// } + + /** + * Users have reported problems using HTTPS with streaming request bodies. + * http://code.google.com/p/android/issues/detail?id=12860 + */ +// private void testSecureStreamingPost(StreamingMode streamingMode) throws Exception { +// TestSSLContext testSSLContext = TestSSLContext.create(); +// server.useHttps(testSSLContext.serverContext.getSocketFactory(), false); +// server.enqueue(new MockResponse().setBody("Success!")); +// server.play(); +// +// HttpsURLConnection connection = (HttpsURLConnection) server.getUrl("/").openConnection(); +// connection.setSSLSocketFactory(testSSLContext.clientContext.getSocketFactory()); +// connection.setDoOutput(true); +// byte[] requestBody = { 'A', 'B', 'C', 'D' }; +// if (streamingMode == StreamingMode.FIXED_LENGTH) { +// connection.setFixedLengthStreamingMode(requestBody.length); +// } else if (streamingMode == StreamingMode.CHUNKED) { +// connection.setChunkedStreamingMode(0); +// } +// OutputStream outputStream = connection.getOutputStream(); +// outputStream.write(requestBody); +// outputStream.close(); +// assertEquals("Success!", readAscii(connection.getInputStream(), Integer.MAX_VALUE)); +// +// RecordedRequest request = server.takeRequest(); +// assertEquals("POST / HTTP/1.1", request.getRequestLine()); +// if (streamingMode == StreamingMode.FIXED_LENGTH) { +// assertEquals(Collections.emptyList(), request.getChunkSizes()); +// } else if (streamingMode == StreamingMode.CHUNKED) { +// assertEquals(Arrays.asList(4), request.getChunkSizes()); +// } +// assertEquals(Arrays.toString(requestBody), Arrays.toString(request.getBody())); +// } + + enum StreamingMode { + FIXED_LENGTH, CHUNKED + } + + public void testAuthenticateWithPost() throws Exception { + MockResponse pleaseAuthenticate = new MockResponse() + .setResponseCode(401) + .addHeader("WWW-Authenticate: Basic realm=\"protected area\"") + .setBody("Please authenticate."); + // fail auth three times... + server.enqueue(pleaseAuthenticate); + server.enqueue(pleaseAuthenticate); + server.enqueue(pleaseAuthenticate); + // ...then succeed the fourth time + server.enqueue(new MockResponse().setBody("Successful auth!")); + server.play(); + + Authenticator.setDefault(SIMPLE_AUTHENTICATOR); + OkHttpConnection connection = openConnection(server.getUrl("/")); + connection.setDoOutput(true); + byte[] requestBody = { 'A', 'B', 'C', 'D' }; + OutputStream outputStream = connection.getOutputStream(); + outputStream.write(requestBody); + outputStream.close(); + assertEquals("Successful auth!", readAscii(connection.getInputStream(), Integer.MAX_VALUE)); + + // no authorization header for the first request... + RecordedRequest request = server.takeRequest(); + assertContainsNoneMatching(request.getHeaders(), "Authorization: Basic .*"); + + // ...but the three requests that follow include an authorization header + for (int i = 0; i < 3; i++) { + request = server.takeRequest(); + assertEquals("POST / HTTP/1.1", request.getRequestLine()); + assertContains(request.getHeaders(), "Authorization: Basic " + BASE_64_CREDENTIALS); + assertEquals(Arrays.toString(requestBody), Arrays.toString(request.getBody())); + } + } + + public void testAuthenticateWithGet() throws Exception { + MockResponse pleaseAuthenticate = new MockResponse() + .setResponseCode(401) + .addHeader("WWW-Authenticate: Basic realm=\"protected area\"") + .setBody("Please authenticate."); + // fail auth three times... + server.enqueue(pleaseAuthenticate); + server.enqueue(pleaseAuthenticate); + server.enqueue(pleaseAuthenticate); + // ...then succeed the fourth time + server.enqueue(new MockResponse().setBody("Successful auth!")); + server.play(); + + Authenticator.setDefault(SIMPLE_AUTHENTICATOR); + OkHttpConnection connection = openConnection(server.getUrl("/")); + assertEquals("Successful auth!", readAscii(connection.getInputStream(), Integer.MAX_VALUE)); + + // no authorization header for the first request... + RecordedRequest request = server.takeRequest(); + assertContainsNoneMatching(request.getHeaders(), "Authorization: Basic .*"); + + // ...but the three requests that follow requests include an authorization header + for (int i = 0; i < 3; i++) { + request = server.takeRequest(); + assertEquals("GET / HTTP/1.1", request.getRequestLine()); + assertContains(request.getHeaders(), "Authorization: Basic " + BASE_64_CREDENTIALS); + } + } + + public void testRedirectedWithChunkedEncoding() throws Exception { + testRedirected(TransferKind.CHUNKED, true); + } + + public void testRedirectedWithContentLengthHeader() throws Exception { + testRedirected(TransferKind.FIXED_LENGTH, true); + } + + public void testRedirectedWithNoLengthHeaders() throws Exception { + testRedirected(TransferKind.END_OF_STREAM, false); + } + + private void testRedirected(TransferKind transferKind, boolean reuse) throws Exception { + MockResponse response = new MockResponse() + .setResponseCode(OkHttpConnection.HTTP_MOVED_TEMP) + .addHeader("Location: /foo"); + transferKind.setBody(response, "This page has moved!", 10); + server.enqueue(response); + server.enqueue(new MockResponse().setBody("This is the new location!")); + server.play(); + + URLConnection connection = openConnection(server.getUrl("/")); + assertEquals("This is the new location!", + readAscii(connection.getInputStream(), Integer.MAX_VALUE)); + + RecordedRequest first = server.takeRequest(); + assertEquals("GET / HTTP/1.1", first.getRequestLine()); + RecordedRequest retry = server.takeRequest(); + assertEquals("GET /foo HTTP/1.1", retry.getRequestLine()); + if (reuse) { + assertEquals("Expected connection reuse", 1, retry.getSequenceNumber()); + } + } + +// public void testRedirectedOnHttps() throws IOException, InterruptedException { +// TestSSLContext testSSLContext = TestSSLContext.create(); +// server.useHttps(testSSLContext.serverContext.getSocketFactory(), false); +// server.enqueue(new MockResponse() +// .setResponseCode(HttpURLConnection.HTTP_MOVED_TEMP) +// .addHeader("Location: /foo") +// .setBody("This page has moved!")); +// server.enqueue(new MockResponse().setBody("This is the new location!")); +// server.play(); +// +// HttpsURLConnection connection = (HttpsURLConnection) server.getUrl("/").openConnection(); +// connection.setSSLSocketFactory(testSSLContext.clientContext.getSocketFactory()); +// assertEquals("This is the new location!", +// readAscii(connection.getInputStream(), Integer.MAX_VALUE)); +// +// RecordedRequest first = server.takeRequest(); +// assertEquals("GET / HTTP/1.1", first.getRequestLine()); +// RecordedRequest retry = server.takeRequest(); +// assertEquals("GET /foo HTTP/1.1", retry.getRequestLine()); +// assertEquals("Expected connection reuse", 1, retry.getSequenceNumber()); +// } +// +// public void testNotRedirectedFromHttpsToHttp() throws IOException, InterruptedException { +// TestSSLContext testSSLContext = TestSSLContext.create(); +// server.useHttps(testSSLContext.serverContext.getSocketFactory(), false); +// server.enqueue(new MockResponse() +// .setResponseCode(HttpURLConnection.HTTP_MOVED_TEMP) +// .addHeader("Location: http://anyhost/foo") +// .setBody("This page has moved!")); +// server.play(); +// +// HttpsURLConnection connection = (HttpsURLConnection) server.getUrl("/").openConnection(); +// connection.setSSLSocketFactory(testSSLContext.clientContext.getSocketFactory()); +// assertEquals("This page has moved!", +// readAscii(connection.getInputStream(), Integer.MAX_VALUE)); +// } + + public void testNotRedirectedFromHttpToHttps() throws IOException, InterruptedException { + server.enqueue(new MockResponse() + .setResponseCode(OkHttpConnection.HTTP_MOVED_TEMP) + .addHeader("Location: https://anyhost/foo") + .setBody("This page has moved!")); + server.play(); + + OkHttpConnection connection = openConnection(server.getUrl("/")); + assertEquals("This page has moved!", + readAscii(connection.getInputStream(), Integer.MAX_VALUE)); + } + + public void SUPPRESSED_testRedirectToAnotherOriginServer() throws Exception { + MockWebServer server2 = new MockWebServer(); + server2.enqueue(new MockResponse().setBody("This is the 2nd server!")); + server2.play(); + + server.enqueue(new MockResponse() + .setResponseCode(OkHttpConnection.HTTP_MOVED_TEMP) + .addHeader("Location: " + server2.getUrl("/").toString()) + .setBody("This page has moved!")); + server.enqueue(new MockResponse().setBody("This is the first server again!")); + server.play(); + + URLConnection connection = openConnection(server.getUrl("/")); + assertEquals("This is the 2nd server!", + readAscii(connection.getInputStream(), Integer.MAX_VALUE)); + assertEquals(server2.getUrl("/"), connection.getURL()); + + // make sure the first server was careful to recycle the connection + assertEquals("This is the first server again!", + readAscii(server.getUrl("/").openStream(), Integer.MAX_VALUE)); + + RecordedRequest first = server.takeRequest(); + assertContains(first.getHeaders(), "Host: " + hostName + ":" + server.getPort()); + RecordedRequest second = server2.takeRequest(); + assertContains(second.getHeaders(), "Host: " + hostName + ":" + server2.getPort()); + RecordedRequest third = server.takeRequest(); + assertEquals("Expected connection reuse", 1, third.getSequenceNumber()); + + server2.shutdown(); + } + + public void testResponse300MultipleChoiceWithPost() throws Exception { + // Chrome doesn't follow the redirect, but Firefox and the RI both do + testResponseRedirectedWithPost(OkHttpConnection.HTTP_MULT_CHOICE); + } + + public void testResponse301MovedPermanentlyWithPost() throws Exception { + testResponseRedirectedWithPost(OkHttpConnection.HTTP_MOVED_PERM); + } + + public void testResponse302MovedTemporarilyWithPost() throws Exception { + testResponseRedirectedWithPost(OkHttpConnection.HTTP_MOVED_TEMP); + } + + public void testResponse303SeeOtherWithPost() throws Exception { + testResponseRedirectedWithPost(OkHttpConnection.HTTP_SEE_OTHER); + } + + private void testResponseRedirectedWithPost(int redirectCode) throws Exception { + server.enqueue(new MockResponse() + .setResponseCode(redirectCode) + .addHeader("Location: /page2") + .setBody("This page has moved!")); + server.enqueue(new MockResponse().setBody("Page 2")); + server.play(); + + OkHttpConnection connection = openConnection(server.getUrl("/page1")); + connection.setDoOutput(true); + byte[] requestBody = { 'A', 'B', 'C', 'D' }; + OutputStream outputStream = connection.getOutputStream(); + outputStream.write(requestBody); + outputStream.close(); + assertEquals("Page 2", readAscii(connection.getInputStream(), Integer.MAX_VALUE)); + assertTrue(connection.getDoOutput()); + + RecordedRequest page1 = server.takeRequest(); + assertEquals("POST /page1 HTTP/1.1", page1.getRequestLine()); + assertEquals(Arrays.toString(requestBody), Arrays.toString(page1.getBody())); + + RecordedRequest page2 = server.takeRequest(); + assertEquals("GET /page2 HTTP/1.1", page2.getRequestLine()); + } + + public void testResponse305UseProxy() throws Exception { + server.play(); + server.enqueue(new MockResponse() + .setResponseCode(OkHttpConnection.HTTP_USE_PROXY) + .addHeader("Location: " + server.getUrl("/")) + .setBody("This page has moved!")); + server.enqueue(new MockResponse().setBody("Proxy Response")); + + OkHttpConnection connection = openConnection(server.getUrl("/foo")); + // Fails on the RI, which gets "Proxy Response" + assertEquals("This page has moved!", + readAscii(connection.getInputStream(), Integer.MAX_VALUE)); + + RecordedRequest page1 = server.takeRequest(); + assertEquals("GET /foo HTTP/1.1", page1.getRequestLine()); + assertEquals(1, server.getRequestCount()); + } + +// public void testHttpsWithCustomTrustManager() throws Exception { +// RecordingHostnameVerifier hostnameVerifier = new RecordingHostnameVerifier(); +// RecordingTrustManager trustManager = new RecordingTrustManager(); +// SSLContext sc = SSLContext.getInstance("TLS"); +// sc.init(null, new TrustManager[] { trustManager }, new java.security.SecureRandom()); +// +// HostnameVerifier defaultHostnameVerifier = HttpsURLConnection.getDefaultHostnameVerifier(); +// HttpsURLConnection.setDefaultHostnameVerifier(hostnameVerifier); +// SSLSocketFactory defaultSSLSocketFactory = HttpsURLConnection.getDefaultSSLSocketFactory(); +// HttpsURLConnection.setDefaultSSLSocketFactory(sc.getSocketFactory()); +// try { +// TestSSLContext testSSLContext = TestSSLContext.create(); +// server.useHttps(testSSLContext.serverContext.getSocketFactory(), false); +// server.enqueue(new MockResponse().setBody("ABC")); +// server.enqueue(new MockResponse().setBody("DEF")); +// server.enqueue(new MockResponse().setBody("GHI")); +// server.play(); +// +// URL url = server.getUrl("/"); +// assertEquals("ABC", readAscii(url.openStream(), Integer.MAX_VALUE)); +// assertEquals("DEF", readAscii(url.openStream(), Integer.MAX_VALUE)); +// assertEquals("GHI", readAscii(url.openStream(), Integer.MAX_VALUE)); +// +// assertEquals(Arrays.asList("verify " + hostName), hostnameVerifier.calls); +// assertEquals(Arrays.asList("checkServerTrusted [" +// + "CN=" + hostName + " 1, " +// + "CN=Test Intermediate Certificate Authority 1, " +// + "CN=Test Root Certificate Authority 1" +// + "] RSA"), +// trustManager.calls); +// } finally { +// HttpsURLConnection.setDefaultHostnameVerifier(defaultHostnameVerifier); +// HttpsURLConnection.setDefaultSSLSocketFactory(defaultSSLSocketFactory); +// } +// } +// +// public void testConnectTimeouts() throws IOException { +// StuckServer ss = new StuckServer(); +// int serverPort = ss.getLocalPort(); +// URLConnection urlConnection = new URL("http://localhost:" + serverPort).openConnection(); +// int timeout = 1000; +// urlConnection.setConnectTimeout(timeout); +// long start = System.currentTimeMillis(); +// try { +// urlConnection.getInputStream(); +// fail(); +// } catch (SocketTimeoutException expected) { +// long actual = System.currentTimeMillis() - start; +// assertTrue(Math.abs(timeout - actual) < 500); +// } finally { +// ss.close(); +// } +// } + + public void testReadTimeouts() throws IOException { + /* + * This relies on the fact that MockWebServer doesn't close the + * connection after a response has been sent. This causes the client to + * try to read more bytes than are sent, which results in a timeout. + */ + MockResponse timeout = new MockResponse() + .setBody("ABC") + .clearHeaders() + .addHeader("Content-Length: 4"); + server.enqueue(timeout); + server.enqueue(new MockResponse().setBody("unused")); // to keep the server alive + server.play(); + + URLConnection urlConnection = openConnection(server.getUrl("/")); + urlConnection.setReadTimeout(1000); + InputStream in = urlConnection.getInputStream(); + assertEquals('A', in.read()); + assertEquals('B', in.read()); + assertEquals('C', in.read()); + try { + in.read(); // if Content-Length was accurate, this would return -1 immediately + fail(); + } catch (SocketTimeoutException expected) { + } + } + + public void testSetChunkedEncodingAsRequestProperty() throws IOException, InterruptedException { + server.enqueue(new MockResponse()); + server.play(); + + OkHttpConnection urlConnection = openConnection(server.getUrl("/")); + urlConnection.setRequestProperty("Transfer-encoding", "chunked"); + urlConnection.setDoOutput(true); + urlConnection.getOutputStream().write("ABC".getBytes("UTF-8")); + assertEquals(200, urlConnection.getResponseCode()); + + RecordedRequest request = server.takeRequest(); + assertEquals("ABC", new String(request.getBody(), "UTF-8")); + } + + public void testConnectionCloseInRequest() throws IOException, InterruptedException { + server.enqueue(new MockResponse()); // server doesn't honor the connection: close header! + server.enqueue(new MockResponse()); + server.play(); + + OkHttpConnection a = openConnection(server.getUrl("/")); + a.setRequestProperty("Connection", "close"); + assertEquals(200, a.getResponseCode()); + + OkHttpConnection b = openConnection(server.getUrl("/")); + assertEquals(200, b.getResponseCode()); + + assertEquals(0, server.takeRequest().getSequenceNumber()); + assertEquals("When connection: close is used, each request should get its own connection", + 0, server.takeRequest().getSequenceNumber()); + } + + public void testConnectionCloseInResponse() throws IOException, InterruptedException { + server.enqueue(new MockResponse().addHeader("Connection: close")); + server.enqueue(new MockResponse()); + server.play(); + + OkHttpConnection a = openConnection(server.getUrl("/")); + assertEquals(200, a.getResponseCode()); + + OkHttpConnection b = openConnection(server.getUrl("/")); + assertEquals(200, b.getResponseCode()); + + assertEquals(0, server.takeRequest().getSequenceNumber()); + assertEquals("When connection: close is used, each request should get its own connection", + 0, server.takeRequest().getSequenceNumber()); + } + + public void testConnectionCloseWithRedirect() throws IOException, InterruptedException { + MockResponse response = new MockResponse() + .setResponseCode(OkHttpConnection.HTTP_MOVED_TEMP) + .addHeader("Location: /foo") + .addHeader("Connection: close"); + server.enqueue(response); + server.enqueue(new MockResponse().setBody("This is the new location!")); + server.play(); + + URLConnection connection = openConnection(server.getUrl("/")); + assertEquals("This is the new location!", + readAscii(connection.getInputStream(), Integer.MAX_VALUE)); + + assertEquals(0, server.takeRequest().getSequenceNumber()); + assertEquals("When connection: close is used, each request should get its own connection", + 0, server.takeRequest().getSequenceNumber()); + } + + public void testResponseCodeDisagreesWithHeaders() throws IOException, InterruptedException { + server.enqueue(new MockResponse() + .setResponseCode(OkHttpConnection.HTTP_NO_CONTENT) + .setBody("This body is not allowed!")); + server.play(); + + URLConnection connection = openConnection(server.getUrl("/")); + assertEquals("This body is not allowed!", + readAscii(connection.getInputStream(), Integer.MAX_VALUE)); + } + + public void testSingleByteReadIsSigned() throws IOException { + server.enqueue(new MockResponse().setBody(new byte[] { -2, -1 })); + server.play(); + + URLConnection connection = openConnection(server.getUrl("/")); + InputStream in = connection.getInputStream(); + assertEquals(254, in.read()); + assertEquals(255, in.read()); + assertEquals(-1, in.read()); + } + + public void testFlushAfterStreamTransmittedWithChunkedEncoding() throws IOException { + testFlushAfterStreamTransmitted(TransferKind.CHUNKED); + } + + public void testFlushAfterStreamTransmittedWithFixedLength() throws IOException { + testFlushAfterStreamTransmitted(TransferKind.FIXED_LENGTH); + } + + public void testFlushAfterStreamTransmittedWithNoLengthHeaders() throws IOException { + testFlushAfterStreamTransmitted(TransferKind.END_OF_STREAM); + } + + /** + * We explicitly permit apps to close the upload stream even after it has + * been transmitted. We also permit flush so that buffered streams can + * do a no-op flush when they are closed. http://b/3038470 + */ + private void testFlushAfterStreamTransmitted(TransferKind transferKind) throws IOException { + server.enqueue(new MockResponse().setBody("abc")); + server.play(); + + OkHttpConnection connection = openConnection(server.getUrl("/")); + connection.setDoOutput(true); + byte[] upload = "def".getBytes("UTF-8"); + + if (transferKind == TransferKind.CHUNKED) { + connection.setChunkedStreamingMode(0); + } else if (transferKind == TransferKind.FIXED_LENGTH) { + connection.setFixedLengthStreamingMode(upload.length); + } + + OutputStream out = connection.getOutputStream(); + out.write(upload); + assertEquals("abc", readAscii(connection.getInputStream(), Integer.MAX_VALUE)); + + out.flush(); // dubious but permitted + try { + out.write("ghi".getBytes("UTF-8")); + fail(); + } catch (IOException expected) { + } + } + + public void testGetHeadersThrows() throws IOException { + server.enqueue(new MockResponse().setSocketPolicy(DISCONNECT_AT_START)); + server.play(); + + OkHttpConnection connection = openConnection(server.getUrl("/")); + try { + connection.getInputStream(); + fail(); + } catch (IOException expected) { + } + + try { + connection.getInputStream(); + fail(); + } catch (IOException expected) { + } + } + + public void SUPPRESSED_testGetKeepAlive() throws Exception { + MockWebServer server = new MockWebServer(); + server.enqueue(new MockResponse().setBody("ABC")); + server.play(); + + // The request should work once and then fail + URLConnection connection = openConnection(server.getUrl("")); + InputStream input = connection.getInputStream(); + assertEquals("ABC", readAscii(input, Integer.MAX_VALUE)); + input.close(); + try { + openConnection(server.getUrl("")).getInputStream(); + fail(); + } catch (ConnectException expected) { + } + } + + /** + * This test goes through the exhaustive set of interesting ASCII characters + * because most of those characters are interesting in some way according to + * RFC 2396 and RFC 2732. http://b/1158780 + */ + public void SUPPRESSED_testLenientUrlToUri() throws Exception { + // alphanum + testUrlToUriMapping("abzABZ09", "abzABZ09", "abzABZ09", "abzABZ09", "abzABZ09"); + + // control characters + testUrlToUriMapping("\u0001", "%01", "%01", "%01", "%01"); + testUrlToUriMapping("\u001f", "%1F", "%1F", "%1F", "%1F"); + + // ascii characters + testUrlToUriMapping("%20", "%20", "%20", "%20", "%20"); + testUrlToUriMapping("%20", "%20", "%20", "%20", "%20"); + testUrlToUriMapping(" ", "%20", "%20", "%20", "%20"); + testUrlToUriMapping("!", "!", "!", "!", "!"); + testUrlToUriMapping("\"", "%22", "%22", "%22", "%22"); + testUrlToUriMapping("#", null, null, null, "%23"); + testUrlToUriMapping("$", "$", "$", "$", "$"); + testUrlToUriMapping("&", "&", "&", "&", "&"); + testUrlToUriMapping("'", "'", "'", "'", "'"); + testUrlToUriMapping("(", "(", "(", "(", "("); + testUrlToUriMapping(")", ")", ")", ")", ")"); + testUrlToUriMapping("*", "*", "*", "*", "*"); + testUrlToUriMapping("+", "+", "+", "+", "+"); + testUrlToUriMapping(",", ",", ",", ",", ","); + testUrlToUriMapping("-", "-", "-", "-", "-"); + testUrlToUriMapping(".", ".", ".", ".", "."); + testUrlToUriMapping("/", null, "/", "/", "/"); + testUrlToUriMapping(":", null, ":", ":", ":"); + testUrlToUriMapping(";", ";", ";", ";", ";"); + testUrlToUriMapping("<", "%3C", "%3C", "%3C", "%3C"); + testUrlToUriMapping("=", "=", "=", "=", "="); + testUrlToUriMapping(">", "%3E", "%3E", "%3E", "%3E"); + testUrlToUriMapping("?", null, null, "?", "?"); + testUrlToUriMapping("@", "@", "@", "@", "@"); + testUrlToUriMapping("[", null, "%5B", null, "%5B"); + testUrlToUriMapping("\\", "%5C", "%5C", "%5C", "%5C"); + testUrlToUriMapping("]", null, "%5D", null, "%5D"); + testUrlToUriMapping("^", "%5E", "%5E", "%5E", "%5E"); + testUrlToUriMapping("_", "_", "_", "_", "_"); + testUrlToUriMapping("`", "%60", "%60", "%60", "%60"); + testUrlToUriMapping("{", "%7B", "%7B", "%7B", "%7B"); + testUrlToUriMapping("|", "%7C", "%7C", "%7C", "%7C"); + testUrlToUriMapping("}", "%7D", "%7D", "%7D", "%7D"); + testUrlToUriMapping("~", "~", "~", "~", "~"); + testUrlToUriMapping("~", "~", "~", "~", "~"); + testUrlToUriMapping("\u007f", "%7F", "%7F", "%7F", "%7F"); + + // beyond ascii + testUrlToUriMapping("\u0080", "%C2%80", "%C2%80", "%C2%80", "%C2%80"); + testUrlToUriMapping("\u20ac", "\u20ac", "\u20ac", "\u20ac", "\u20ac"); + testUrlToUriMapping("\ud842\udf9f", + "\ud842\udf9f", "\ud842\udf9f", "\ud842\udf9f", "\ud842\udf9f"); + } + + public void SUPPRESSED_testLenientUrlToUriNul() throws Exception { + testUrlToUriMapping("\u0000", "%00", "%00", "%00", "%00"); // RI fails this + } + + private void testUrlToUriMapping(String string, String asAuthority, String asFile, + String asQuery, String asFragment) throws Exception { + if (asAuthority != null) { + assertEquals("http://host" + asAuthority + ".tld/", + backdoorUrlToUri(new URL("http://host" + string + ".tld/")).toString()); + } + if (asFile != null) { + assertEquals("http://host.tld/file" + asFile + "/", + backdoorUrlToUri(new URL("http://host.tld/file" + string + "/")).toString()); + } + if (asQuery != null) { + assertEquals("http://host.tld/file?q" + asQuery + "=x", + backdoorUrlToUri(new URL("http://host.tld/file?q" + string + "=x")).toString()); + } + assertEquals("http://host.tld/file#" + asFragment + "-x", + backdoorUrlToUri(new URL("http://host.tld/file#" + asFragment + "-x")).toString()); + } + + /** + * Exercises HttpURLConnection to convert URL to a URI. Unlike URL#toURI, + * HttpURLConnection recovers from URLs with unescaped but unsupported URI + * characters like '{' and '|' by escaping these characters. + */ + private URI backdoorUrlToUri(URL url) throws Exception { + final AtomicReference uriReference = new AtomicReference(); + + ResponseCache.setDefault(new ResponseCache() { + @Override public CacheRequest put(URI uri, URLConnection connection) throws IOException { + return null; + } + @Override public CacheResponse get(URI uri, String requestMethod, + Map> requestHeaders) throws IOException { + uriReference.set(uri); + throw new UnsupportedOperationException(); + } + }); + + try { + OkHttpConnection connection = openConnection(url); + connection.getResponseCode(); + } catch (Exception expected) { + if (expected.getCause() instanceof URISyntaxException) { + expected.printStackTrace(); + } + } + + return uriReference.get(); + } + + /** + * Don't explode if the cache returns a null body. http://b/3373699 + */ + public void testResponseCacheReturnsNullOutputStream() throws Exception { + final AtomicBoolean aborted = new AtomicBoolean(); + ResponseCache.setDefault(new ResponseCache() { + @Override public CacheResponse get(URI uri, String requestMethod, + Map> requestHeaders) throws IOException { + return null; + } + @Override public CacheRequest put(URI uri, URLConnection connection) throws IOException { + return new CacheRequest() { + @Override public void abort() { + aborted.set(true); + } + @Override public OutputStream getBody() throws IOException { + return null; + } + }; + } + }); + + server.enqueue(new MockResponse().setBody("abcdef")); + server.play(); + + OkHttpConnection connection = openConnection(server.getUrl("/")); + InputStream in = connection.getInputStream(); + assertEquals("abc", readAscii(in, 3)); + in.close(); + assertFalse(aborted.get()); // The best behavior is ambiguous, but RI 6 doesn't abort here + } + + + /** + * http://code.google.com/p/android/issues/detail?id=14562 + */ + public void testReadAfterLastByte() throws Exception { + server.enqueue(new MockResponse() + .setBody("ABC") + .clearHeaders() + .addHeader("Connection: close") + .setSocketPolicy(SocketPolicy.DISCONNECT_AT_END)); + server.play(); + + OkHttpConnection connection = openConnection(server.getUrl("/")); + InputStream in = connection.getInputStream(); + assertEquals("ABC", readAscii(in, 3)); + assertEquals(-1, in.read()); + assertEquals(-1, in.read()); // throws IOException in Gingerbread + } + + public void testGetContent() throws Exception { + server.enqueue(new MockResponse() + .addHeader("Content-Type: text/plain") + .setBody("A")); + server.play(); + OkHttpConnection connection = openConnection(server.getUrl("/")); + InputStream in = (InputStream) connection.getContent(); + assertEquals("A", readAscii(in, Integer.MAX_VALUE)); + } + + public void testGetContentOfType() throws Exception { + server.enqueue(new MockResponse() + .addHeader("Content-Type: text/plain") + .setBody("A")); + server.play(); + OkHttpConnection connection = openConnection(server.getUrl("/")); + try { + connection.getContent(null); + fail(); + } catch (NullPointerException expected) { + } + try { + connection.getContent(new Class[] { null }); + fail(); + } catch (NullPointerException expected) { + } + assertNull(connection.getContent(new Class[] { getClass() })); + connection.disconnect(); + } + + public void testGetOutputStreamOnGetFails() throws Exception { + server.enqueue(new MockResponse()); + server.play(); + OkHttpConnection connection = openConnection(server.getUrl("/")); + try { + connection.getOutputStream(); + fail(); + } catch (ProtocolException expected) { + } + } + + public void testGetOutputAfterGetInputStreamFails() throws Exception { + server.enqueue(new MockResponse()); + server.play(); + OkHttpConnection connection = openConnection(server.getUrl("/")); + connection.setDoOutput(true); + try { + connection.getInputStream(); + connection.getOutputStream(); + fail(); + } catch (ProtocolException expected) { + } + } + + public void testSetDoOutputOrDoInputAfterConnectFails() throws Exception { + server.enqueue(new MockResponse()); + server.play(); + OkHttpConnection connection = openConnection(server.getUrl("/")); + connection.connect(); + try { + connection.setDoOutput(true); + fail(); + } catch (IllegalStateException expected) { + } + try { + connection.setDoInput(true); + fail(); + } catch (IllegalStateException expected) { + } + connection.disconnect(); + } + + public void testClientSendsContentLength() throws Exception { + server.enqueue(new MockResponse().setBody("A")); + server.play(); + OkHttpConnection connection = openConnection(server.getUrl("/")); + connection.setDoOutput(true); + OutputStream out = connection.getOutputStream(); + out.write(new byte[] { 'A', 'B', 'C' }); + out.close(); + assertEquals("A", readAscii(connection.getInputStream(), Integer.MAX_VALUE)); + RecordedRequest request = server.takeRequest(); + assertContains(request.getHeaders(), "Content-Length: 3"); + } + + public void testGetContentLengthConnects() throws Exception { + server.enqueue(new MockResponse().setBody("ABC")); + server.play(); + OkHttpConnection connection = openConnection(server.getUrl("/")); + assertEquals(3, connection.getContentLength()); + connection.disconnect(); + } + + public void testGetContentTypeConnects() throws Exception { + server.enqueue(new MockResponse() + .addHeader("Content-Type: text/plain") + .setBody("ABC")); + server.play(); + OkHttpConnection connection = openConnection(server.getUrl("/")); + assertEquals("text/plain", connection.getContentType()); + connection.disconnect(); + } + + public void testGetContentEncodingConnects() throws Exception { + server.enqueue(new MockResponse() + .addHeader("Content-Encoding: identity") + .setBody("ABC")); + server.play(); + OkHttpConnection connection = openConnection(server.getUrl("/")); + assertEquals("identity", connection.getContentEncoding()); + connection.disconnect(); + } + + // http://b/4361656 + public void testUrlContainsQueryButNoPath() throws Exception { + server.enqueue(new MockResponse().setBody("A")); + server.play(); + URL url = new URL("http", server.getHostName(), server.getPort(), "?query"); + assertEquals("A", readAscii(openConnection(url).getInputStream(), Integer.MAX_VALUE)); + RecordedRequest request = server.takeRequest(); + assertEquals("GET /?query HTTP/1.1", request.getRequestLine()); + } + + // http://code.google.com/p/android/issues/detail?id=20442 + public void testInputStreamAvailableWithChunkedEncoding() throws Exception { + testInputStreamAvailable(TransferKind.CHUNKED); + } + + public void testInputStreamAvailableWithContentLengthHeader() throws Exception { + testInputStreamAvailable(TransferKind.FIXED_LENGTH); + } + + public void testInputStreamAvailableWithNoLengthHeaders() throws Exception { + testInputStreamAvailable(TransferKind.END_OF_STREAM); + } + + private void testInputStreamAvailable(TransferKind transferKind) throws IOException { + String body = "ABCDEFGH"; + MockResponse response = new MockResponse(); + transferKind.setBody(response, body, 4); + server.enqueue(response); + server.play(); + URLConnection connection = openConnection(server.getUrl("/")); + InputStream in = connection.getInputStream(); + for (int i = 0; i < body.length(); i++) { + assertTrue(in.available() >= 0); + assertEquals(body.charAt(i), in.read()); + } + assertEquals(0, in.available()); + assertEquals(-1, in.read()); + } + + /** + * Returns a gzipped copy of {@code bytes}. + */ + public byte[] gzip(byte[] bytes) throws IOException { + ByteArrayOutputStream bytesOut = new ByteArrayOutputStream(); + OutputStream gzippedOut = new GZIPOutputStream(bytesOut); + gzippedOut.write(bytes); + gzippedOut.close(); + return bytesOut.toByteArray(); + } + + /** + * Reads at most {@code limit} characters from {@code in} and asserts that + * content equals {@code expected}. + */ + private void assertContent(String expected, URLConnection connection, int limit) + throws IOException { + connection.connect(); + assertEquals(expected, readAscii(connection.getInputStream(), limit)); + ((OkHttpConnection) connection).disconnect(); + } + + private void assertContent(String expected, URLConnection connection) throws IOException { + assertContent(expected, connection, Integer.MAX_VALUE); + } + + private void assertContains(List headers, String header) { + assertTrue(headers.toString(), headers.contains(header)); + } + + private void assertContainsNoneMatching(List headers, String pattern) { + for (String header : headers) { + if (header.matches(pattern)) { + fail("Header " + header + " matches " + pattern); + } + } + } + + private Set newSet(String... elements) { + return new HashSet(Arrays.asList(elements)); + } + + enum TransferKind { + CHUNKED() { + @Override void setBody(MockResponse response, byte[] content, int chunkSize) + throws IOException { + response.setChunkedBody(content, chunkSize); + } + }, + FIXED_LENGTH() { + @Override void setBody(MockResponse response, byte[] content, int chunkSize) { + response.setBody(content); + } + }, + END_OF_STREAM() { + @Override void setBody(MockResponse response, byte[] content, int chunkSize) { + response.setBody(content); + response.setSocketPolicy(DISCONNECT_AT_END); + for (Iterator h = response.getHeaders().iterator(); h.hasNext(); ) { + if (h.next().startsWith("Content-Length:")) { + h.remove(); + break; + } + } + } + }; + + abstract void setBody(MockResponse response, byte[] content, int chunkSize) + throws IOException; + + void setBody(MockResponse response, String content, int chunkSize) throws IOException { + setBody(response, content.getBytes("UTF-8"), chunkSize); + } + } + + enum ProxyConfig { + NO_PROXY() { + @Override public OkHttpConnection connect(MockWebServer server, URL url) + throws IOException { + return OkHttpConnection.open(url, Proxy.NO_PROXY); + } + }, + + CREATE_ARG() { + @Override public OkHttpConnection connect(MockWebServer server, URL url) + throws IOException { + return OkHttpConnection.open(url, server.toProxyAddress()); + } + }, + + PROXY_SYSTEM_PROPERTY() { + @Override public OkHttpConnection connect(MockWebServer server, URL url) + throws IOException { + System.setProperty("proxyHost", "localhost"); + System.setProperty("proxyPort", Integer.toString(server.getPort())); + return OkHttpConnection.open(url); + } + }, + + HTTP_PROXY_SYSTEM_PROPERTY() { + @Override public OkHttpConnection connect(MockWebServer server, URL url) + throws IOException { + System.setProperty("http.proxyHost", "localhost"); + System.setProperty("http.proxyPort", Integer.toString(server.getPort())); + return openConnection(url); + } + }, + + HTTPS_PROXY_SYSTEM_PROPERTY() { + @Override public OkHttpConnection connect(MockWebServer server, URL url) + throws IOException { + System.setProperty("https.proxyHost", "localhost"); + System.setProperty("https.proxyPort", Integer.toString(server.getPort())); + return openConnection(url); + } + }; + + public abstract OkHttpConnection connect(MockWebServer server, URL url) throws IOException; + } + + private static class RecordingTrustManager implements X509TrustManager { + private final List calls = new ArrayList(); + + public X509Certificate[] getAcceptedIssuers() { + calls.add("getAcceptedIssuers"); + return new X509Certificate[] {}; + } + + public void checkClientTrusted(X509Certificate[] chain, String authType) + throws CertificateException { + calls.add("checkClientTrusted " + certificatesToString(chain) + " " + authType); + } + + public void checkServerTrusted(X509Certificate[] chain, String authType) + throws CertificateException { + calls.add("checkServerTrusted " + certificatesToString(chain) + " " + authType); + } + + private String certificatesToString(X509Certificate[] certificates) { + List result = new ArrayList(); + for (X509Certificate certificate : certificates) { + result.add(certificate.getSubjectDN() + " " + certificate.getSerialNumber()); + } + return result.toString(); + } + } + + private static class RecordingHostnameVerifier implements HostnameVerifier { + private final List calls = new ArrayList(); + + public boolean verify(String hostname, SSLSession session) { + calls.add("verify " + hostname); + return true; + } + } +} diff --git a/src/test/java/libcore/net/spdy/MockSpdyPeer.java b/src/test/java/libcore/net/spdy/MockSpdyPeer.java new file mode 100644 index 000000000000..0ea3d867b1e5 --- /dev/null +++ b/src/test/java/libcore/net/spdy/MockSpdyPeer.java @@ -0,0 +1,134 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package libcore.net.spdy; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.ServerSocket; +import java.net.Socket; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.Executor; +import java.util.concurrent.Executors; +import java.util.concurrent.LinkedBlockingQueue; +import libcore.io.Streams; + +/** + * Replays prerecorded outgoing frames and records incoming frames. + */ +public final class MockSpdyPeer { + private int frameCount = 0; + private final List outFrames = new ArrayList(); + private final BlockingQueue inFrames = new LinkedBlockingQueue(); + private int port; + private final Executor executor = Executors.newCachedThreadPool( + Threads.newThreadFactory("MockSpdyPeer")); + + public void acceptFrame() { + frameCount++; + } + + public SpdyWriter sendFrame() { + OutFrame frame = new OutFrame(frameCount++); + outFrames.add(frame); + return new SpdyWriter(frame.out); + } + + public int getPort() { + return port; + } + + public InFrame takeFrame() throws InterruptedException { + return inFrames.take(); + } + + public void play() throws IOException { + final ServerSocket serverSocket = new ServerSocket(0); + serverSocket.setReuseAddress(true); + this.port = serverSocket.getLocalPort(); + executor.execute(new Runnable() { + @Override public void run() { + try { + readAndWriteFrames(serverSocket); + } catch (IOException e) { + e.printStackTrace(); // TODO + } + } + }); + } + + private void readAndWriteFrames(ServerSocket serverSocket) throws IOException { + Socket socket = serverSocket.accept(); + OutputStream out = socket.getOutputStream(); + InputStream in = socket.getInputStream(); + + Iterator outFramesIterator = outFrames.iterator(); + OutFrame nextOutFrame = null; + + for (int i = 0; i < frameCount; i++) { + if (nextOutFrame == null && outFramesIterator.hasNext()) { + nextOutFrame = outFramesIterator.next(); + } + + if (nextOutFrame != null && nextOutFrame.sequence == i) { + // write a frame + nextOutFrame.out.writeTo(out); + nextOutFrame = null; + + } else { + // read a frame + SpdyReader reader = new SpdyReader(in); + byte[] data = null; + int type = reader.nextFrame(); + if (type == SpdyConnection.TYPE_DATA) { + data = new byte[reader.length]; + Streams.readFully(in, data); + } + inFrames.add(new InFrame(i, reader, data)); + } + } + } + + public Socket openSocket() throws IOException { + return new Socket("localhost", port); + } + + private static class OutFrame { + private final int sequence; + private final ByteArrayOutputStream out = new ByteArrayOutputStream(); + + private OutFrame(int sequence) { + this.sequence = sequence; + } + } + + public static class InFrame { + public final int sequence; + public final SpdyReader reader; + public final byte[] data; + + public InFrame(int sequence, SpdyReader reader, byte[] data) { + this.sequence = sequence; + this.reader = reader; + this.data = data; + } + } +} \ No newline at end of file diff --git a/src/test/java/libcore/net/spdy/SpdyConnectionTest.java b/src/test/java/libcore/net/spdy/SpdyConnectionTest.java new file mode 100644 index 000000000000..206a561e7fad --- /dev/null +++ b/src/test/java/libcore/net/spdy/SpdyConnectionTest.java @@ -0,0 +1,96 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package libcore.net.spdy; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.util.Arrays; +import java.util.List; +import junit.framework.TestCase; + +public final class SpdyConnectionTest extends TestCase { + private final MockSpdyPeer peer = new MockSpdyPeer(); + + public void testClientCreatesStreamAndServerReplies() throws Exception { + // write the mocking script + peer.acceptFrame(); + SpdyWriter reply = peer.sendFrame(); + reply.streamId = 1; + reply.nameValueBlock = Arrays.asList("a", "android"); + reply.synReply(); + SpdyWriter replyData = peer.sendFrame(); + replyData.flags = SpdyConnection.FLAG_FIN; + replyData.streamId = 1; + replyData.data("robot".getBytes()); + peer.acceptFrame(); + peer.play(); + + // play it back + SpdyConnection connection = new SpdyConnection.Builder(true, peer.openSocket()).build(); + SpdyStream stream = connection.newStream(Arrays.asList("b", "banana"), true, true); + List responseHeaders = stream.getResponseHeaders(); + assertEquals(Arrays.asList("a", "android"), responseHeaders); + BufferedReader reader = new BufferedReader(new InputStreamReader(stream.getInputStream())); + assertEquals("robot", reader.readLine()); + assertEquals(null, reader.readLine()); + OutputStream out = stream.getOutputStream(); + out.write("c3po".getBytes()); + out.close(); + + // verify the peer received what was expected + MockSpdyPeer.InFrame synStream = peer.takeFrame(); + assertEquals(0, synStream.reader.flags); + assertEquals(1, synStream.reader.streamId); + assertEquals(0, synStream.reader.associatedStreamId); + assertEquals(Arrays.asList("b", "banana"), synStream.reader.nameValueBlock); + MockSpdyPeer.InFrame requestData = peer.takeFrame(); + assertTrue(Arrays.equals("c3po".getBytes(), requestData.data)); + } + + public void testServerCreatesStreamAndClientReplies() throws Exception { + // write the mocking script + SpdyWriter newStream = peer.sendFrame(); + newStream.flags = 0; + newStream.streamId = 2; + newStream.associatedStreamId = 0; + newStream.nameValueBlock = Arrays.asList("a", "android"); + newStream.synStream(); + peer.acceptFrame(); + peer.play(); + + // play it back + IncomingStreamHandler handler = new IncomingStreamHandler() { + @Override public void receive(SpdyStream stream) throws IOException { + assertEquals(Arrays.asList("a", "android"), stream.getRequestHeaders()); + assertEquals(-1, stream.getRstStatusCode()); + stream.reply(Arrays.asList("b", "banana")); + } + }; + new SpdyConnection.Builder(true, peer.openSocket()) + .handler(handler) + .build(); + + // verify the peer received what was expected + MockSpdyPeer.InFrame synStream = peer.takeFrame(); + assertEquals(0, synStream.reader.flags); + assertEquals(2, synStream.reader.streamId); + assertEquals(0, synStream.reader.associatedStreamId); + assertEquals(Arrays.asList("b", "banana"), synStream.reader.nameValueBlock); + } +}