From ea63463c2ff8a67f63e541036f0d5998b5bb1f3e Mon Sep 17 00:00:00 2001 From: Jesse Wilson Date: Mon, 23 Jul 2012 10:02:28 -0400 Subject: [PATCH] Initial import. This is a fork of okhttp from http://code.google.com/p/okhttp. I'm moving it to Github since that's where my code reviewers are. I've renamed the core package from com.google.okhttp to com.squareup.okhttp because Square is where I expect ongoing development of this project to take place. All code in this project is subject to be contributed upstream to AOSP. In particular, all code in the libcore package is derived from AOSP and intended to be contributed back to AOSP on an ongoing basis. --- LICENSE.txt | 202 ++ pom.xml | 166 ++ .../com/squareup/okhttp/OkHttpConnection.java | 808 +++++++ .../squareup/okhttp/OkHttpsConnection.java | 309 +++ .../libcore/io/AsynchronousCloseMonitor.java | 26 + src/main/java/libcore/io/Base64.java | 161 ++ src/main/java/libcore/io/BufferIterator.java | 62 + src/main/java/libcore/io/DiskLruCache.java | 834 +++++++ src/main/java/libcore/io/IoUtils.java | 72 + src/main/java/libcore/io/OsConstants.java | 724 ++++++ src/main/java/libcore/io/SizeOf.java | 29 + src/main/java/libcore/io/Streams.java | 216 ++ src/main/java/libcore/net/MimeUtils.java | 480 ++++ .../net/http/AbstractHttpInputStream.java | 107 + .../net/http/AbstractHttpOutputStream.java | 40 + src/main/java/libcore/net/http/Challenge.java | 40 + .../java/libcore/net/http/HeaderParser.java | 163 ++ .../java/libcore/net/http/HttpConnection.java | 374 +++ .../libcore/net/http/HttpConnectionPool.java | 152 ++ src/main/java/libcore/net/http/HttpDate.java | 91 + .../java/libcore/net/http/HttpEngine.java | 640 +++++ .../libcore/net/http/HttpResponseCache.java | 596 +++++ .../java/libcore/net/http/HttpTransport.java | 596 +++++ .../net/http/HttpURLConnectionImpl.java | 515 +++++ .../java/libcore/net/http/HttpsHandler.java | 42 + .../net/http/HttpsURLConnectionImpl.java | 535 +++++ .../java/libcore/net/http/RawHeaders.java | 389 ++++ .../java/libcore/net/http/RequestHeaders.java | 292 +++ .../libcore/net/http/ResponseHeaders.java | 503 ++++ .../net/http/RetryableOutputStream.java | 72 + .../java/libcore/net/http/SpdyTransport.java | 91 + src/main/java/libcore/net/http/Transport.java | 71 + .../net/spdy/IncomingStreamHandler.java | 38 + .../java/libcore/net/spdy/SpdyConnection.java | 282 +++ .../java/libcore/net/spdy/SpdyReader.java | 211 ++ .../java/libcore/net/spdy/SpdyServer.java | 114 + .../java/libcore/net/spdy/SpdyStream.java | 410 ++++ .../java/libcore/net/spdy/SpdyWriter.java | 108 + src/main/java/libcore/net/spdy/Threads.java | 29 + src/main/java/libcore/util/BasicLruCache.java | 121 + src/main/java/libcore/util/Charsets.java | 48 + .../java/libcore/util/CollectionUtils.java | 98 + .../java/libcore/util/DefaultFileNameMap.java | 43 + src/main/java/libcore/util/EmptyArray.java | 33 + .../libcore/util/ExtendedResponseCache.java | 54 + .../java/libcore/util/IntegralToString.java | 68 + src/main/java/libcore/util/Libcore.java | 194 ++ .../java/libcore/util/MutableBoolean.java | 25 + src/main/java/libcore/util/MutableByte.java | 25 + src/main/java/libcore/util/MutableChar.java | 25 + src/main/java/libcore/util/MutableDouble.java | 25 + src/main/java/libcore/util/MutableFloat.java | 25 + src/main/java/libcore/util/MutableInt.java | 25 + src/main/java/libcore/util/MutableLong.java | 25 + src/main/java/libcore/util/MutableShort.java | 25 + src/main/java/libcore/util/Objects.java | 32 + .../java/libcore/util/ResponseSource.java | 45 + src/main/java/libcore/util/SneakyThrow.java | 70 + .../libcore/net/http/ExternalSpdyTest.java | 49 + .../net/http/NewURLConnectionTest.java | 40 + .../libcore/net/http/URLConnectionTest.java | 2057 +++++++++++++++++ .../java/libcore/net/spdy/MockSpdyPeer.java | 134 ++ .../libcore/net/spdy/SpdyConnectionTest.java | 96 + 63 files changed, 13972 insertions(+) create mode 100644 LICENSE.txt create mode 100644 pom.xml create mode 100644 src/main/java/com/squareup/okhttp/OkHttpConnection.java create mode 100644 src/main/java/com/squareup/okhttp/OkHttpsConnection.java create mode 100644 src/main/java/libcore/io/AsynchronousCloseMonitor.java create mode 100644 src/main/java/libcore/io/Base64.java create mode 100644 src/main/java/libcore/io/BufferIterator.java create mode 100644 src/main/java/libcore/io/DiskLruCache.java create mode 100644 src/main/java/libcore/io/IoUtils.java create mode 100644 src/main/java/libcore/io/OsConstants.java create mode 100644 src/main/java/libcore/io/SizeOf.java create mode 100644 src/main/java/libcore/io/Streams.java create mode 100644 src/main/java/libcore/net/MimeUtils.java create mode 100644 src/main/java/libcore/net/http/AbstractHttpInputStream.java create mode 100644 src/main/java/libcore/net/http/AbstractHttpOutputStream.java create mode 100644 src/main/java/libcore/net/http/Challenge.java create mode 100644 src/main/java/libcore/net/http/HeaderParser.java create mode 100644 src/main/java/libcore/net/http/HttpConnection.java create mode 100644 src/main/java/libcore/net/http/HttpConnectionPool.java create mode 100644 src/main/java/libcore/net/http/HttpDate.java create mode 100644 src/main/java/libcore/net/http/HttpEngine.java create mode 100644 src/main/java/libcore/net/http/HttpResponseCache.java create mode 100644 src/main/java/libcore/net/http/HttpTransport.java create mode 100644 src/main/java/libcore/net/http/HttpURLConnectionImpl.java create mode 100644 src/main/java/libcore/net/http/HttpsHandler.java create mode 100644 src/main/java/libcore/net/http/HttpsURLConnectionImpl.java create mode 100644 src/main/java/libcore/net/http/RawHeaders.java create mode 100644 src/main/java/libcore/net/http/RequestHeaders.java create mode 100644 src/main/java/libcore/net/http/ResponseHeaders.java create mode 100644 src/main/java/libcore/net/http/RetryableOutputStream.java create mode 100644 src/main/java/libcore/net/http/SpdyTransport.java create mode 100644 src/main/java/libcore/net/http/Transport.java create mode 100644 src/main/java/libcore/net/spdy/IncomingStreamHandler.java create mode 100644 src/main/java/libcore/net/spdy/SpdyConnection.java create mode 100644 src/main/java/libcore/net/spdy/SpdyReader.java create mode 100644 src/main/java/libcore/net/spdy/SpdyServer.java create mode 100644 src/main/java/libcore/net/spdy/SpdyStream.java create mode 100644 src/main/java/libcore/net/spdy/SpdyWriter.java create mode 100644 src/main/java/libcore/net/spdy/Threads.java create mode 100644 src/main/java/libcore/util/BasicLruCache.java create mode 100644 src/main/java/libcore/util/Charsets.java create mode 100644 src/main/java/libcore/util/CollectionUtils.java create mode 100644 src/main/java/libcore/util/DefaultFileNameMap.java create mode 100644 src/main/java/libcore/util/EmptyArray.java create mode 100644 src/main/java/libcore/util/ExtendedResponseCache.java create mode 100644 src/main/java/libcore/util/IntegralToString.java create mode 100644 src/main/java/libcore/util/Libcore.java create mode 100644 src/main/java/libcore/util/MutableBoolean.java create mode 100644 src/main/java/libcore/util/MutableByte.java create mode 100644 src/main/java/libcore/util/MutableChar.java create mode 100644 src/main/java/libcore/util/MutableDouble.java create mode 100644 src/main/java/libcore/util/MutableFloat.java create mode 100644 src/main/java/libcore/util/MutableInt.java create mode 100644 src/main/java/libcore/util/MutableLong.java create mode 100644 src/main/java/libcore/util/MutableShort.java create mode 100644 src/main/java/libcore/util/Objects.java create mode 100644 src/main/java/libcore/util/ResponseSource.java create mode 100644 src/main/java/libcore/util/SneakyThrow.java create mode 100644 src/test/java/libcore/net/http/ExternalSpdyTest.java create mode 100644 src/test/java/libcore/net/http/NewURLConnectionTest.java create mode 100644 src/test/java/libcore/net/http/URLConnectionTest.java create mode 100644 src/test/java/libcore/net/spdy/MockSpdyPeer.java create mode 100644 src/test/java/libcore/net/spdy/SpdyConnectionTest.java diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 000000000..d64569567 --- /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 000000000..348a3a6cb --- /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 000000000..d98d330ec --- /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 000000000..7c7540c32 --- /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 000000000..62eec24af --- /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 000000000..153722192 --- /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 000000000..7f3ad472b --- /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 000000000..b6c3638cd --- /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 000000000..307737d5b --- /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 000000000..68a165c8d --- /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 000000000..728fbfce7 --- /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 000000000..194b77510 --- /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 000000000..f8038f0b6 --- /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 000000000..70f76b7ce --- /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 000000000..1e1b47b09 --- /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 000000000..d373c0a66 --- /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 000000000..26e77b64c --- /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 000000000..c3fb2a9d5 --- /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 000000000..490c98adc --- /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 000000000..a41cf8193 --- /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 000000000..6fd15a422 --- /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 000000000..c7982679a --- /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 000000000..e47f32bdc --- /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 000000000..dda9b95eb --- /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 000000000..ed9ba7243 --- /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 000000000..49d8d6f3b --- /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 000000000..4f9ec9e06 --- /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 000000000..3b536ce58 --- /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 000000000..d2311f350 --- /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 000000000..c8110be75 --- /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 000000000..547658cbf --- /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 000000000..3d4c8ddbb --- /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 000000000..69cc8e110 --- /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 000000000..daef8f5f6 --- /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 000000000..9540b230a --- /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 000000000..190c549a8 --- /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 000000000..bb1192cc8 --- /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 000000000..cfd8a047d --- /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 000000000..cb29a519f --- /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 000000000..14124381b --- /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 000000000..c8b2c348b --- /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 000000000..45c2ae6ab --- /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 000000000..e817a72fb --- /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 000000000..6c9987829 --- /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 000000000..b3f91916e --- /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 000000000..1b66e510c --- /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 000000000..4ccda8294 --- /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 000000000..359a8f90e --- /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 000000000..13f780b3d --- /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 000000000..1cafc3cd4 --- /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 000000000..4473ae61c --- /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 000000000..f81fba534 --- /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 000000000..c8feb3aee --- /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 000000000..ad9b78e95 --- /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 000000000..78b4c33db --- /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 000000000..781731677 --- /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 000000000..8e7bfae5c --- /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 000000000..1911788cc --- /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 000000000..d317945f0 --- /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 000000000..8c6121e15 --- /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 000000000..f5faf2457 --- /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 000000000..0ea3d867b --- /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 000000000..206a561e7 --- /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); + } +}