mirror of
https://github.com/square/okhttp.git
synced 2026-01-24 04:02:07 +03:00
Merge pull request #1 from square/jwilson/initialimport
Initial import.
This commit is contained in:
202
LICENSE.txt
Normal file
202
LICENSE.txt
Normal file
@@ -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.
|
||||
166
pom.xml
Normal file
166
pom.xml
Normal file
@@ -0,0 +1,166 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!--
|
||||
Copyright (C) 2012 Square, Inc.
|
||||
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.
|
||||
-->
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<parent>
|
||||
<groupId>org.sonatype.oss</groupId>
|
||||
<artifactId>oss-parent</artifactId>
|
||||
<version>7</version>
|
||||
</parent>
|
||||
<groupId>com.squareup.okhttp</groupId>
|
||||
<artifactId>okhttp</artifactId>
|
||||
<version>20120723</version>
|
||||
<packaging>jar</packaging>
|
||||
|
||||
<name>okhttp</name>
|
||||
<description>An HTTP+SPDY client for Android and Java applications</description>
|
||||
<url>https://github.com/square/okhttp</url>
|
||||
|
||||
<properties>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
|
||||
<!-- Compilation -->
|
||||
<java.version>1.6</java.version>
|
||||
<npn.version>8.1.2.v20120308</npn.version>
|
||||
<mockwebserver.version>20120401</mockwebserver.version>
|
||||
|
||||
<!-- Test Dependencies -->
|
||||
<junit.version>3.8.2</junit.version>
|
||||
</properties>
|
||||
|
||||
<scm>
|
||||
<url>https://github.com/square/okhttp/</url>
|
||||
<connection>scm:git:https://github.com/square/okhttp.git</connection>
|
||||
<developerConnection>scm:git:git@github.com:square/okhttp.git</developerConnection>
|
||||
</scm>
|
||||
|
||||
<issueManagement>
|
||||
<system>GitHub Issues</system>
|
||||
<url>https://github.com/square/okhttp/issues</url>
|
||||
</issueManagement>
|
||||
|
||||
<licenses>
|
||||
<license>
|
||||
<name>Apache 2.0</name>
|
||||
<url>http://www.apache.org/licenses/LICENSE-2.0.txt</url>
|
||||
</license>
|
||||
</licenses>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.mortbay.jetty.npn</groupId>
|
||||
<artifactId>npn-boot</artifactId>
|
||||
<version>${npn.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.google.mockwebserver</groupId>
|
||||
<artifactId>mockwebserver</artifactId>
|
||||
<version>${mockwebserver.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>junit</groupId>
|
||||
<artifactId>junit</artifactId>
|
||||
<version>${junit.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-compiler-plugin</artifactId>
|
||||
<version>2.5</version>
|
||||
<configuration>
|
||||
<source>${java.version}</source>
|
||||
<target>${java.version}</target>
|
||||
</configuration>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.sonatype.plugins</groupId>
|
||||
<artifactId>jarjar-maven-plugin</artifactId>
|
||||
<executions>
|
||||
<execution>
|
||||
<phase>package</phase>
|
||||
<goals>
|
||||
<goal>jarjar</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<includes>
|
||||
<include>asm:asm</include>
|
||||
<include>org.sonatype.sisu.inject:cglib</include>
|
||||
</includes>
|
||||
<rules>
|
||||
<rule>
|
||||
<pattern>libcore.**</pattern>
|
||||
<result>com.squareup.okhttp.libcore.@1</result>
|
||||
</rule>
|
||||
<keep>
|
||||
<pattern>com.squareup.okhttp.**</pattern>
|
||||
</keep>
|
||||
</rules>
|
||||
</configuration>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<artifactId>maven-surefire-plugin</artifactId>
|
||||
<configuration>
|
||||
<argLine>-Xbootclasspath/p:${settings.localRepository}/org/mortbay/jetty/npn/npn-boot/${npn.version}/npn-boot-${npn.version}.jar</argLine>
|
||||
</configuration>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-source-plugin</artifactId>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>attach-sources</id>
|
||||
<phase>verify</phase>
|
||||
<goals><goal>jar-no-fork</goal></goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-javadoc-plugin</artifactId>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>attach-javadocs</id>
|
||||
<goals><goal>jar</goal></goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-gpg-plugin</artifactId>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>sign-artifacts</id>
|
||||
<phase>verify</phase>
|
||||
<goals>
|
||||
<goal>sign</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</project>
|
||||
|
||||
808
src/main/java/com/squareup/okhttp/OkHttpConnection.java
Normal file
808
src/main/java/com/squareup/okhttp/OkHttpConnection.java
Normal file
@@ -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 (<a
|
||||
* href="http://tools.ietf.org/html/rfc2616">RFC 2616</a>) 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.
|
||||
*
|
||||
* <p>Uses of this class follow a pattern:
|
||||
* <ol>
|
||||
* <li>Obtain a new {@code HttpURLConnection} by calling {@link
|
||||
* java.net.URL#openConnection() URL.openConnection()} and casting the result to
|
||||
* {@code HttpURLConnection}.
|
||||
* <li>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.
|
||||
* <li>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()}.
|
||||
* <li>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.
|
||||
* <li>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.
|
||||
* </ol>
|
||||
*
|
||||
* <p>For example, to retrieve the webpage at {@code http://www.android.com/}:
|
||||
* <pre> {@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();
|
||||
* }
|
||||
* }</pre>
|
||||
*
|
||||
* <h3>Secure Communication with HTTPS</h3>
|
||||
* 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.
|
||||
*
|
||||
* <h3>Response Handling</h3>
|
||||
* {@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.
|
||||
*
|
||||
* <p>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()},
|
||||
*
|
||||
* <h3>Posting Content</h3>
|
||||
* To upload data to a web server, configure the connection for output using
|
||||
* {@link #setDoOutput(boolean) setDoOutput(true)}.
|
||||
*
|
||||
* <p>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.
|
||||
*
|
||||
* <p>For example, to perform an upload: <pre> {@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();
|
||||
* }
|
||||
* }</pre>
|
||||
*
|
||||
* <h3>Performance</h3>
|
||||
* The input and output streams returned by this class are <strong>not
|
||||
* buffered</strong>. 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.
|
||||
*
|
||||
* <p>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).
|
||||
*
|
||||
* <p>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.
|
||||
*
|
||||
* <p>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: <pre> {@code
|
||||
* urlConnection.setRequestProperty("Accept-Encoding", "identity");
|
||||
* }</pre>
|
||||
*
|
||||
* <h3>Handling Network Sign-On</h3>
|
||||
* 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 <strong>after</strong>
|
||||
* 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:
|
||||
* <pre> {@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();
|
||||
* }
|
||||
* }</pre>
|
||||
*
|
||||
* <h3>HTTP Authentication</h3>
|
||||
* {@code HttpURLConnection} supports <a
|
||||
* href="http://www.ietf.org/rfc/rfc2617">HTTP basic authentication</a>. Use
|
||||
* {@link java.net.Authenticator} to set the VM-wide authentication handler:
|
||||
* <pre> {@code
|
||||
* Authenticator.setDefault(new Authenticator() {
|
||||
* protected PasswordAuthentication getPasswordAuthentication() {
|
||||
* return new PasswordAuthentication(username, password.toCharArray());
|
||||
* }
|
||||
* });
|
||||
* }</pre>
|
||||
* Unless paired with HTTPS, this is <strong>not</strong> a secure mechanism for
|
||||
* user authentication. In particular, the username, password, request and
|
||||
* response are all transmitted over the network without encryption.
|
||||
*
|
||||
* <h3>Sessions with Cookies</h3>
|
||||
* 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}: <pre> {@code
|
||||
* CookieManager cookieManager = new CookieManager();
|
||||
* CookieHandler.setDefault(cookieManager);
|
||||
* }</pre>
|
||||
* By default, {@code CookieManager} accepts cookies from the <a
|
||||
* href="http://www.w3.org/Protocols/rfc2616/rfc2616-sec1.html">origin
|
||||
* server</a> 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.
|
||||
*
|
||||
* <p>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.
|
||||
*
|
||||
* <p>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.
|
||||
*
|
||||
* <p>By default, new instances of {@code HttpCookie} work only with servers
|
||||
* that support <a href="http://www.ietf.org/rfc/rfc2965.txt">RFC 2965</a>
|
||||
* cookies. Many web servers support only the older specification, <a
|
||||
* href="http://www.ietf.org/rfc/rfc2109.txt">RFC 2109</a>. For compatibility
|
||||
* with the most web servers, set the cookie version to 0.
|
||||
*
|
||||
* <p>For example, to receive {@code www.twitter.com} in French: <pre> {@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);
|
||||
* }</pre>
|
||||
*
|
||||
* <h3>HTTP Methods</h3>
|
||||
* <p>{@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}.
|
||||
*
|
||||
* <h3>Proxies</h3>
|
||||
* By default, this class will connect directly to the <a
|
||||
* href="http://www.w3.org/Protocols/rfc2616/rfc2616-sec1.html">origin
|
||||
* server</a>. 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.
|
||||
*
|
||||
* <h3>IPv6 Support</h3>
|
||||
* <p>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.
|
||||
*
|
||||
* <h3>Response Caching</h3>
|
||||
* 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.
|
||||
*
|
||||
* <h3>Avoiding Bugs In Earlier Releases</h3>
|
||||
* Prior to Android 2.2 (Froyo), this class had some frustrating bugs. In
|
||||
* particular, calling {@code close()} on a readable {@code InputStream} could
|
||||
* <a href="http://code.google.com/p/android/issues/detail?id=2939">poison the
|
||||
* connection pool</a>. Work around this by disabling connection pooling:
|
||||
* <pre> {@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");
|
||||
* }
|
||||
* }}</pre>
|
||||
*
|
||||
* <p>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}.
|
||||
* <p>
|
||||
* <li>1xx: Informational</li>
|
||||
* <li>2xx: Success</li>
|
||||
* <li>3xx: Relocation/Redirection</li>
|
||||
* <li>4xx: Client Error</li>
|
||||
* <li>5xx: Server Error</li>
|
||||
*/
|
||||
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.
|
||||
*
|
||||
* <p>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.
|
||||
*
|
||||
* <p>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.
|
||||
*
|
||||
* <p>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;
|
||||
}
|
||||
}
|
||||
}
|
||||
309
src/main/java/com/squareup/okhttp/OkHttpsConnection.java
Normal file
309
src/main/java/com/squareup/okhttp/OkHttpsConnection.java
Normal file
@@ -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 (<a
|
||||
* href="http://tools.ietf.org/html/rfc2818">RFC 2818</a>). A
|
||||
* connected {@code HttpsURLConnection} allows access to the
|
||||
* negotiated cipher suite, the server certificate chain, and the
|
||||
* client certificate chain if any.
|
||||
*
|
||||
* <h3>Providing an application specific X509TrustManager</h3>
|
||||
*
|
||||
* 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}.
|
||||
*
|
||||
* <p>For example, to trust a set of certificates specified by a {@code KeyStore}:
|
||||
* <pre> {@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();
|
||||
* }</pre>
|
||||
*
|
||||
* <p>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}.
|
||||
*
|
||||
* <h3>Providing an application specific X509KeyManager</h3>
|
||||
*
|
||||
* 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}.
|
||||
*
|
||||
* <p>For example, to supply client certificates from a {@code KeyStore}:
|
||||
* <pre> {@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();
|
||||
* }</pre>
|
||||
*
|
||||
* <p>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}.
|
||||
*
|
||||
* <h3>TLS Intolerance Support</h3>
|
||||
*
|
||||
* 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;
|
||||
}
|
||||
|
||||
}
|
||||
26
src/main/java/libcore/io/AsynchronousCloseMonitor.java
Normal file
26
src/main/java/libcore/io/AsynchronousCloseMonitor.java
Normal file
@@ -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);
|
||||
}
|
||||
161
src/main/java/libcore/io/Base64.java
Normal file
161
src/main/java/libcore/io/Base64.java
Normal file
@@ -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;
|
||||
|
||||
/**
|
||||
* <a href="http://www.ietf.org/rfc/rfc2045.txt">Base64</a> 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<len; i++) {
|
||||
chr = in[i];
|
||||
// skip the neutral characters
|
||||
if ((chr == '\n') || (chr == '\r') ||
|
||||
(chr == ' ') || (chr == '\t')) {
|
||||
continue;
|
||||
}
|
||||
if ((chr >= '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);
|
||||
}
|
||||
}
|
||||
62
src/main/java/libcore/io/BufferIterator.java
Normal file
62
src/main/java/libcore/io/BufferIterator.java
Normal file
@@ -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();
|
||||
}
|
||||
834
src/main/java/libcore/io/DiskLruCache.java
Normal file
834
src/main/java/libcore/io/DiskLruCache.java
Normal file
@@ -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.
|
||||
*
|
||||
* <p>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.
|
||||
*
|
||||
* <p>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.
|
||||
*
|
||||
* <p>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.
|
||||
* <ul>
|
||||
* <li>When an entry is being <strong>created</strong> it is necessary to
|
||||
* supply a full set of values; the empty value should be used as a
|
||||
* placeholder if necessary.
|
||||
* <li>When an entry is being <strong>edited</strong>, it is not necessary
|
||||
* to supply data for every value; values default to their previous
|
||||
* value.
|
||||
* </ul>
|
||||
* 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.
|
||||
*
|
||||
* <p>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.
|
||||
*
|
||||
* <p>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<String, Entry> lruEntries
|
||||
= new LinkedHashMap<String, Entry>(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<Runnable>());
|
||||
private final Callable<Void> cleanupCallable = new Callable<Void>() {
|
||||
@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<Entry> 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<Entry>(lruEntries.values())) {
|
||||
if (entry.currentEditor != null) {
|
||||
entry.currentEditor.abort();
|
||||
}
|
||||
}
|
||||
trimToSize();
|
||||
journalWriter.close();
|
||||
journalWriter = null;
|
||||
}
|
||||
|
||||
private void trimToSize() throws IOException {
|
||||
while (size > maxSize) {
|
||||
Map.Entry<String, 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");
|
||||
}
|
||||
}
|
||||
}
|
||||
72
src/main/java/libcore/io/IoUtils.java
Normal file
72
src/main/java/libcore/io/IoUtils.java
Normal file
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
724
src/main/java/libcore/io/OsConstants.java
Normal file
724
src/main/java/libcore/io/OsConstants.java
Normal file
@@ -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();
|
||||
}
|
||||
}
|
||||
29
src/main/java/libcore/io/SizeOf.java
Normal file
29
src/main/java/libcore/io/SizeOf.java
Normal file
@@ -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() {
|
||||
}
|
||||
}
|
||||
216
src/main/java/libcore/io/Streams.java
Normal file
216
src/main/java/libcore/io/Streams.java
Normal file
@@ -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<byte[]> skipBuffer = new AtomicReference<byte[]>();
|
||||
|
||||
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.
|
||||
*
|
||||
* <p>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();
|
||||
}
|
||||
}
|
||||
480
src/main/java/libcore/net/MimeUtils.java
Normal file
480
src/main/java/libcore/net/MimeUtils.java
Normal file
@@ -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<String, String> mimeTypeToExtensionMap = new HashMap<String, String>();
|
||||
|
||||
private static final Map<String, String> extensionToMimeTypeMap = new HashMap<String, String>();
|
||||
|
||||
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<Object, Object> 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);
|
||||
}
|
||||
}
|
||||
107
src/main/java/libcore/net/http/AbstractHttpInputStream.java
Normal file
107
src/main/java/libcore/net/http/AbstractHttpInputStream.java
Normal file
@@ -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.
|
||||
*
|
||||
* <p>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.
|
||||
*
|
||||
* <p>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.
|
||||
*
|
||||
* <p>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);
|
||||
}
|
||||
}
|
||||
40
src/main/java/libcore/net/http/AbstractHttpOutputStream.java
Normal file
40
src/main/java/libcore/net/http/AbstractHttpOutputStream.java
Normal file
@@ -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.
|
||||
*
|
||||
* <p>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");
|
||||
}
|
||||
}
|
||||
}
|
||||
40
src/main/java/libcore/net/http/Challenge.java
Normal file
40
src/main/java/libcore/net/http/Challenge.java
Normal file
@@ -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();
|
||||
}
|
||||
}
|
||||
163
src/main/java/libcore/net/http/HeaderParser.java
Normal file
163
src/main/java/libcore/net/http/HeaderParser.java
Normal file
@@ -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<Challenge> 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<Challenge> result = new ArrayList<Challenge>();
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
374
src/main/java/libcore/net/http/HttpConnection.java
Normal file
374
src/main/java/libcore/net/http/HttpConnection.java
Normal file
@@ -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.
|
||||
*
|
||||
* <p>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<Proxy> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
152
src/main/java/libcore/net/http/HttpConnectionPool.java
Normal file
152
src/main/java/libcore/net/http/HttpConnectionPool.java
Normal file
@@ -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:
|
||||
* <ul>
|
||||
* <li>{@code http.keepAlive} true if HTTP and SPDY connections should be
|
||||
* pooled at all. Default is true.
|
||||
* <li>{@code http.maxConnections} maximum number of connections to each host.
|
||||
* Default is 5.
|
||||
* </ul>
|
||||
*
|
||||
* <p>This class <i>doesn't</i> 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<HttpConnection.Address, List<HttpConnection>> connectionPool
|
||||
= new HashMap<HttpConnection.Address, List<HttpConnection>>();
|
||||
|
||||
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<HttpConnection> 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<HttpConnection> connections = connectionPool.get(address);
|
||||
if (connections == null) {
|
||||
connections = new ArrayList<HttpConnection>();
|
||||
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<HttpConnection> connections = connectionPool.get(address);
|
||||
if (connections == null) {
|
||||
connections = new ArrayList<HttpConnection>(1);
|
||||
connections.add(connection);
|
||||
connectionPool.put(address, connections);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
91
src/main/java/libcore/net/http/HttpDate.java
Normal file
91
src/main/java/libcore/net/http/HttpDate.java
Normal file
@@ -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<DateFormat> STANDARD_DATE_FORMAT
|
||||
= new ThreadLocal<DateFormat>() {
|
||||
@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);
|
||||
}
|
||||
}
|
||||
640
src/main/java/libcore/net/http/HttpEngine.java
Normal file
640
src/main/java/libcore/net/http/HttpEngine.java
Normal file
@@ -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:
|
||||
* <ol>
|
||||
* <li>It is created.
|
||||
* <li>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.
|
||||
* <li>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.
|
||||
* </ol>
|
||||
*
|
||||
* <p>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.
|
||||
*
|
||||
* <p>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<String, List<String>> getHeaders() throws IOException {
|
||||
Map<String, List<String>> result = new HashMap<String, List<String>>();
|
||||
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<String, List<String>> 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.
|
||||
*
|
||||
* <p>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").
|
||||
*
|
||||
* <p>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));
|
||||
}
|
||||
}
|
||||
596
src/main/java/libcore/net/http/HttpResponseCache.java
Normal file
596
src/main/java/libcore/net/http/HttpResponseCache.java
Normal file
@@ -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<String, List<String>> 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<String, List<String>> 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<String, List<String>> 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<String, List<String>> getHeaders() {
|
||||
return entry.responseHeaders.toMultimap();
|
||||
}
|
||||
|
||||
@Override public InputStream getBody() {
|
||||
return in;
|
||||
}
|
||||
|
||||
@Override public String getCipherSuite() {
|
||||
return entry.cipherSuite;
|
||||
}
|
||||
|
||||
@Override public List<Certificate> 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<Certificate> 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
596
src/main/java/libcore/net/http/HttpTransport.java
Normal file
596
src/main/java/libcore/net/http/HttpTransport.java
Normal file
@@ -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.
|
||||
*
|
||||
* <p>For streaming requests with a body, headers must be prepared
|
||||
* <strong>before</strong> the output stream has been written to. Otherwise
|
||||
* the body would need to be buffered!
|
||||
*
|
||||
* <p>For non-streaming requests with a body, headers must be prepared
|
||||
* <strong>after</strong> 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
515
src/main/java/libcore/net/http/HttpURLConnectionImpl.java
Normal file
515
src/main/java/libcore/net/http/HttpURLConnectionImpl.java
Normal file
@@ -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.
|
||||
*
|
||||
* <h3>What does 'connected' mean?</h3>
|
||||
* This class inherits a {@code connected} field from the superclass. That field
|
||||
* is <strong>not</strong> 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<String, List<String>> getHeaderFields() {
|
||||
try {
|
||||
return getResponse().getResponseHeaders().getHeaders().toMultimap();
|
||||
} catch (IOException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override public final Map<String, List<String>> 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<Challenge> 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);
|
||||
}
|
||||
}
|
||||
42
src/main/java/libcore/net/http/HttpsHandler.java
Normal file
42
src/main/java/libcore/net/http/HttpsHandler.java
Normal file
@@ -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;
|
||||
}
|
||||
}
|
||||
535
src/main/java/libcore/net/http/HttpsURLConnectionImpl.java
Normal file
535
src/main/java/libcore/net/http/HttpsURLConnectionImpl.java
Normal file
@@ -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<Certificate> 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<Certificate> 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<String, List<String>> getHeaderFields() {
|
||||
return delegate.getHeaderFields();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, List<String>> 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
389
src/main/java/libcore/net/http/RawHeaders.java
Normal file
389
src/main/java/libcore/net/http/RawHeaders.java
Normal file
@@ -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.
|
||||
*
|
||||
* <p>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.
|
||||
*
|
||||
* <p>This class trims whitespace from values. It never returns values with
|
||||
* leading or trailing whitespace.
|
||||
*/
|
||||
public final class RawHeaders {
|
||||
private static final Comparator<String> FIELD_NAME_COMPARATOR = new Comparator<String>() {
|
||||
// @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<String> namesAndValues = new ArrayList<String>(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<String> 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<String> 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<String, List<String>> toMultimap() {
|
||||
Map<String, List<String>> result = new TreeMap<String, List<String>>(FIELD_NAME_COMPARATOR);
|
||||
for (int i = 0; i < namesAndValues.size(); i += 2) {
|
||||
String fieldName = namesAndValues.get(i);
|
||||
String value = namesAndValues.get(i + 1);
|
||||
|
||||
List<String> allValues = new ArrayList<String>();
|
||||
List<String> 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<String, List<String>> map) {
|
||||
RawHeaders result = new RawHeaders();
|
||||
for (Entry<String, List<String>> entry : map.entrySet()) {
|
||||
String fieldName = entry.getKey();
|
||||
List<String> 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<String> toNameValueBlock() {
|
||||
Set<String> names = new HashSet<String>();
|
||||
List<String> result = new ArrayList<String>();
|
||||
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<String> 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;
|
||||
}
|
||||
}
|
||||
292
src/main/java/libcore/net/http/RequestHeaders.java
Normal file
292
src/main/java/libcore/net/http/RequestHeaders.java
Normal file
@@ -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<String, List<String>> allCookieHeaders) {
|
||||
for (Map.Entry<String, List<String>> entry : allCookieHeaders.entrySet()) {
|
||||
String key = entry.getKey();
|
||||
if ("Cookie".equalsIgnoreCase(key) || "Cookie2".equalsIgnoreCase(key)) {
|
||||
headers.addAll(key, entry.getValue());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
503
src/main/java/libcore/net/http/ResponseHeaders.java
Normal file
503
src/main/java/libcore/net/http/ResponseHeaders.java
Normal file
@@ -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<String> 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>(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<String> 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<String, List<String>> cachedRequest,
|
||||
Map<String, List<String>> 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");
|
||||
}
|
||||
}
|
||||
72
src/main/java/libcore/net/http/RetryableOutputStream.java
Normal file
72
src/main/java/libcore/net/http/RetryableOutputStream.java
Normal file
@@ -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);
|
||||
}
|
||||
}
|
||||
91
src/main/java/libcore/net/http/SpdyTransport.java
Normal file
91
src/main/java/libcore/net/http/SpdyTransport.java
Normal file
@@ -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<String> 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;
|
||||
}
|
||||
}
|
||||
71
src/main/java/libcore/net/http/Transport.java
Normal file
71
src/main/java/libcore/net/http/Transport.java
Normal file
@@ -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:
|
||||
* <ul>
|
||||
* <li><strong>Direct.</strong> 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.</li>
|
||||
* <li><strong>Buffered.</strong> 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.</li>
|
||||
* </ul>
|
||||
*/
|
||||
// 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);
|
||||
}
|
||||
38
src/main/java/libcore/net/spdy/IncomingStreamHandler.java
Normal file
38
src/main/java/libcore/net/spdy/IncomingStreamHandler.java
Normal file
@@ -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;
|
||||
}
|
||||
282
src/main/java/libcore/net/spdy/SpdyConnection.java
Normal file
282
src/main/java/libcore/net/spdy/SpdyConnection.java
Normal file
@@ -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<Integer, SpdyStream> streams = Collections.synchronizedMap(
|
||||
new HashMap<Integer, SpdyStream>());
|
||||
|
||||
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<String> 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<String> 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
211
src/main/java/libcore/net/spdy/SpdyReader.java
Normal file
211
src/main/java/libcore/net/spdy/SpdyReader.java
Normal file
@@ -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<String> 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<String> readNameValueBlock(int length) throws IOException {
|
||||
this.compressedLimit += length;
|
||||
try {
|
||||
List<String> entries = new ArrayList<String>();
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
114
src/main/java/libcore/net/spdy/SpdyServer.java
Normal file
114
src/main/java/libcore/net/spdy/SpdyServer.java
Normal file
@@ -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<String> 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<String> 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 <base directory>");
|
||||
return;
|
||||
}
|
||||
|
||||
new SpdyServer(new File(args[0])).run();
|
||||
}
|
||||
}
|
||||
410
src/main/java/libcore/net/spdy/SpdyStream.java
Normal file
410
src/main/java/libcore/net/spdy/SpdyStream.java
Normal file
@@ -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<String> requestHeaders;
|
||||
|
||||
/** Headers sent in the stream reply. Null if reply is either not sent or not sent yet. */
|
||||
private List<String> 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<String> 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<String> getRequestHeaders() {
|
||||
return requestHeaders;
|
||||
}
|
||||
|
||||
public synchronized List<String> 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<String> 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<String> 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 + ")");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
108
src/main/java/libcore/net/spdy/SpdyWriter.java
Normal file
108
src/main/java/libcore/net/spdy/SpdyWriter.java
Normal file
@@ -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<String> 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();
|
||||
}
|
||||
}
|
||||
29
src/main/java/libcore/net/spdy/Threads.java
Normal file
29
src/main/java/libcore/net/spdy/Threads.java
Normal file
@@ -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);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
121
src/main/java/libcore/util/BasicLruCache.java
Normal file
121
src/main/java/libcore/util/BasicLruCache.java
Normal file
@@ -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<K, V> {
|
||||
private final LinkedHashMap<K, V> map;
|
||||
private final int maxSize;
|
||||
|
||||
public BasicLruCache(int maxSize) {
|
||||
if (maxSize <= 0) {
|
||||
throw new IllegalArgumentException("maxSize <= 0");
|
||||
}
|
||||
this.maxSize = maxSize;
|
||||
this.map = new LinkedHashMap<K, V>(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<K, V> 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<K, V> snapshot() {
|
||||
return new LinkedHashMap<K, V>(map);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear the cache, calling {@link #entryEvicted} on each removed entry.
|
||||
*/
|
||||
public synchronized final void evictAll() {
|
||||
trimToSize(0);
|
||||
}
|
||||
}
|
||||
48
src/main/java/libcore/util/Charsets.java
Normal file
48
src/main/java/libcore/util/Charsets.java
Normal file
@@ -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() {
|
||||
}
|
||||
}
|
||||
98
src/main/java/libcore/util/CollectionUtils.java
Normal file
98
src/main/java/libcore/util/CollectionUtils.java
Normal file
@@ -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 <T> Iterable<T> dereferenceIterable(
|
||||
final Iterable<? extends Reference<T>> iterable, final boolean trim) {
|
||||
return new Iterable<T>() {
|
||||
public Iterator<T> iterator() {
|
||||
return new Iterator<T>() {
|
||||
private final Iterator<? extends Reference<T>> 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 <T> void removeDuplicates(List<T> list, Comparator<? super T> 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
43
src/main/java/libcore/util/DefaultFileNameMap.java
Normal file
43
src/main/java/libcore/util/DefaultFileNameMap.java
Normal file
@@ -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));
|
||||
}
|
||||
}
|
||||
33
src/main/java/libcore/util/EmptyArray.java
Normal file
33
src/main/java/libcore/util/EmptyArray.java
Normal file
@@ -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];
|
||||
}
|
||||
54
src/main/java/libcore/util/ExtendedResponseCache.java
Normal file
54
src/main/java/libcore/util/ExtendedResponseCache.java
Normal file
@@ -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);
|
||||
}
|
||||
68
src/main/java/libcore/util/IntegralToString.java
Normal file
68
src/main/java/libcore/util/IntegralToString.java
Normal file
@@ -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);
|
||||
}
|
||||
}
|
||||
194
src/main/java/libcore/util/Libcore.java
Normal file
194
src/main/java/libcore/util/Libcore.java
Normal file
@@ -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<String> strings = new ArrayList<String>();
|
||||
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<String> protocols;
|
||||
boolean unsupported;
|
||||
String selected;
|
||||
|
||||
@Override public boolean supports() {
|
||||
return true;
|
||||
}
|
||||
@Override public List<String> protocols() {
|
||||
return protocols;
|
||||
}
|
||||
@Override public void unsupported() {
|
||||
this.unsupported = true;
|
||||
}
|
||||
@Override public void protocolSelected(String selected) {
|
||||
this.selected = selected;
|
||||
}
|
||||
@Override public String selectProtocol(List<String> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
25
src/main/java/libcore/util/MutableBoolean.java
Normal file
25
src/main/java/libcore/util/MutableBoolean.java
Normal file
@@ -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;
|
||||
}
|
||||
}
|
||||
25
src/main/java/libcore/util/MutableByte.java
Normal file
25
src/main/java/libcore/util/MutableByte.java
Normal file
@@ -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;
|
||||
}
|
||||
}
|
||||
25
src/main/java/libcore/util/MutableChar.java
Normal file
25
src/main/java/libcore/util/MutableChar.java
Normal file
@@ -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;
|
||||
}
|
||||
}
|
||||
25
src/main/java/libcore/util/MutableDouble.java
Normal file
25
src/main/java/libcore/util/MutableDouble.java
Normal file
@@ -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;
|
||||
}
|
||||
}
|
||||
25
src/main/java/libcore/util/MutableFloat.java
Normal file
25
src/main/java/libcore/util/MutableFloat.java
Normal file
@@ -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;
|
||||
}
|
||||
}
|
||||
25
src/main/java/libcore/util/MutableInt.java
Normal file
25
src/main/java/libcore/util/MutableInt.java
Normal file
@@ -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;
|
||||
}
|
||||
}
|
||||
25
src/main/java/libcore/util/MutableLong.java
Normal file
25
src/main/java/libcore/util/MutableLong.java
Normal file
@@ -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;
|
||||
}
|
||||
}
|
||||
25
src/main/java/libcore/util/MutableShort.java
Normal file
25
src/main/java/libcore/util/MutableShort.java
Normal file
@@ -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;
|
||||
}
|
||||
}
|
||||
32
src/main/java/libcore/util/Objects.java
Normal file
32
src/main/java/libcore/util/Objects.java
Normal file
@@ -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();
|
||||
}
|
||||
}
|
||||
45
src/main/java/libcore/util/ResponseSource.java
Normal file
45
src/main/java/libcore/util/ResponseSource.java
Normal file
@@ -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;
|
||||
}
|
||||
}
|
||||
70
src/main/java/libcore/util/SneakyThrow.java
Normal file
70
src/main/java/libcore/util/SneakyThrow.java
Normal file
@@ -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. <strong>This is a dangerous API that should be
|
||||
* used with great caution.</strong> Typically this is useful when rethrowing
|
||||
* throwables that are of a known range of types.
|
||||
*
|
||||
* <p>The following code must enumerate several types to rethrow:
|
||||
* <pre>
|
||||
* 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();
|
||||
* }
|
||||
* }
|
||||
* }</pre>
|
||||
* With SneakyThrow, rethrowing is easier:
|
||||
* <pre>
|
||||
* public void close() throws IOException {
|
||||
* Throwable thrown = null;
|
||||
* ...
|
||||
*
|
||||
* if (thrown != null) {
|
||||
* SneakyThrow.sneakyThrow(thrown);
|
||||
* }
|
||||
* }</pre>
|
||||
*/
|
||||
public final class SneakyThrow {
|
||||
private SneakyThrow() {}
|
||||
|
||||
public static void sneakyThrow(Throwable t) {
|
||||
SneakyThrow.<Error>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 <T extends Throwable> void sneakyThrow2(Throwable t) throws T {
|
||||
throw (T) t;
|
||||
}
|
||||
}
|
||||
49
src/test/java/libcore/net/http/ExternalSpdyTest.java
Normal file
49
src/test/java/libcore/net/http/ExternalSpdyTest.java
Normal file
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
40
src/test/java/libcore/net/http/NewURLConnectionTest.java
Normal file
40
src/test/java/libcore/net/http/NewURLConnectionTest.java
Normal file
@@ -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?
|
||||
|
||||
}
|
||||
2057
src/test/java/libcore/net/http/URLConnectionTest.java
Normal file
2057
src/test/java/libcore/net/http/URLConnectionTest.java
Normal file
File diff suppressed because it is too large
Load Diff
134
src/test/java/libcore/net/spdy/MockSpdyPeer.java
Normal file
134
src/test/java/libcore/net/spdy/MockSpdyPeer.java
Normal file
@@ -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<OutFrame> outFrames = new ArrayList<OutFrame>();
|
||||
private final BlockingQueue<InFrame> inFrames = new LinkedBlockingQueue<InFrame>();
|
||||
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<OutFrame> 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
96
src/test/java/libcore/net/spdy/SpdyConnectionTest.java
Normal file
96
src/test/java/libcore/net/spdy/SpdyConnectionTest.java
Normal file
@@ -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<String> 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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user