diff options
Diffstat (limited to 'luni')
46 files changed, 203 insertions, 7504 deletions
diff --git a/luni/src/main/java/java/net/ExtendedResponseCache.java b/luni/src/main/java/java/net/ExtendedResponseCache.java deleted file mode 100644 index a70e734..0000000 --- a/luni/src/main/java/java/net/ExtendedResponseCache.java +++ /dev/null @@ -1,51 +0,0 @@ -/* - * 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 java.net; - -/** - * A response cache that supports statistics tracking and updating stored - * responses. Implementations of {@link 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, HttpURLConnection httpConnection); -} diff --git a/luni/src/main/java/java/net/HttpURLConnection.java b/luni/src/main/java/java/net/HttpURLConnection.java index 2023887..573dde6 100644 --- a/luni/src/main/java/java/net/HttpURLConnection.java +++ b/luni/src/main/java/java/net/HttpURLConnection.java @@ -20,7 +20,6 @@ package java.net; import java.io.IOException; import java.io.InputStream; import java.util.Arrays; -import libcore.net.http.HttpEngine; /** * An {@link URLConnection} for HTTP (<a @@ -257,19 +256,20 @@ import libcore.net.http.HttpEngine; * request/response pair. Instances of this class are not thread safe. */ public abstract class HttpURLConnection extends URLConnection { + private static final int DEFAULT_CHUNK_LENGTH = 1024; /** * 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 + "OPTIONS", + "GET", + "HEAD", + "POST", + "PUT", + "DELETE", + "TRACE" // Note: we don't allow users to specify "CONNECT" }; @@ -277,7 +277,7 @@ public abstract class HttpURLConnection extends URLConnection { * The HTTP request method of this {@code HttpURLConnection}. The default * value is {@code "GET"}. */ - protected String method = HttpEngine.GET; + protected String method = "GET"; /** * The status code of the response obtained from the HTTP request. The @@ -787,7 +787,7 @@ public abstract class HttpURLConnection extends URLConnection { throw new IllegalStateException("Already in fixed-length mode"); } if (chunkLength <= 0) { - this.chunkLength = HttpEngine.DEFAULT_CHUNK_LENGTH; + this.chunkLength = DEFAULT_CHUNK_LENGTH; } else { this.chunkLength = chunkLength; } diff --git a/luni/src/main/java/java/net/ResponseSource.java b/luni/src/main/java/java/net/ResponseSource.java deleted file mode 100644 index fb974e9..0000000 --- a/luni/src/main/java/java/net/ResponseSource.java +++ /dev/null @@ -1,45 +0,0 @@ -/* - * 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 java.net; - -/** - * Where the HTTP client should look for a response. - * - * @hide - */ -public enum ResponseSource { - - /** - * Return the response from the cache immediately. - */ - CACHE, - - /** - * Make a conditional request to the host, returning the cache response if - * the cache is valid and the network response otherwise. - */ - CONDITIONAL_CACHE, - - /** - * Return the response from the network. - */ - NETWORK; - - public boolean requiresConnection() { - return this == CONDITIONAL_CACHE || this == NETWORK; - } -} diff --git a/luni/src/main/java/java/net/URL.java b/luni/src/main/java/java/net/URL.java index 9d44498..dd487bc 100644 --- a/luni/src/main/java/java/net/URL.java +++ b/luni/src/main/java/java/net/URL.java @@ -24,8 +24,6 @@ import java.io.ObjectOutputStream; import java.io.Serializable; import java.util.Hashtable; import java.util.jar.JarFile; -import libcore.net.http.HttpHandler; -import libcore.net.http.HttpsHandler; import libcore.net.url.FileHandler; import libcore.net.url.FtpHandler; import libcore.net.url.JarHandler; @@ -427,9 +425,19 @@ public final class URL implements Serializable { } else if (protocol.equals("ftp")) { streamHandler = new FtpHandler(); } else if (protocol.equals("http")) { - streamHandler = new HttpHandler(); + try { + String name = "com.android.okhttp.HttpHandler"; + streamHandler = (URLStreamHandler) Class.forName(name).newInstance(); + } catch (Exception e) { + throw new AssertionError(e); + } } else if (protocol.equals("https")) { - streamHandler = new HttpsHandler(); + try { + String name = "com.android.okhttp.HttpsHandler"; + streamHandler = (URLStreamHandler) Class.forName(name).newInstance(); + } catch (Exception e) { + throw new AssertionError(e); + } } else if (protocol.equals("jar")) { streamHandler = new JarHandler(); } diff --git a/luni/src/main/java/java/nio/DirectByteBuffer.java b/luni/src/main/java/java/nio/DirectByteBuffer.java index 43db9e0..1d12f2e 100644 --- a/luni/src/main/java/java/nio/DirectByteBuffer.java +++ b/luni/src/main/java/java/nio/DirectByteBuffer.java @@ -44,7 +44,7 @@ class DirectByteBuffer extends MappedByteBuffer { } // Used by the JNI NewDirectByteBuffer function. - DirectByteBuffer(int address, int capacity) { + DirectByteBuffer(long address, int capacity) { this(MemoryBlock.wrapFromJni(address, capacity), capacity, 0, false, null); } diff --git a/luni/src/main/java/java/util/Locale.java b/luni/src/main/java/java/util/Locale.java index d513bd7..2b89c76 100644 --- a/luni/src/main/java/java/util/Locale.java +++ b/luni/src/main/java/java/util/Locale.java @@ -495,29 +495,33 @@ public final class Locale implements Cloneable, Serializable { } /** - * Returns the three letter ISO country code which corresponds to the country + * Returns the three-letter ISO 3166 country code which corresponds to the country * code for this {@code Locale}. + * @throws MissingResourceException if there's no 3-letter country code for this locale. */ public String getISO3Country() { - if (countryCode.length() == 0) { - return countryCode; + String code = ICU.getISO3CountryNative(toString()); + if (!countryCode.isEmpty() && code.isEmpty()) { + throw new MissingResourceException("No 3-letter country code for locale: " + this, "FormatData_" + this, "ShortCountry"); } - return ICU.getISO3CountryNative(toString()); + return code; } /** - * Returns the three letter ISO language code which corresponds to the language + * Returns the three-letter ISO 639-2/T language code which corresponds to the language * code for this {@code Locale}. + * @throws MissingResourceException if there's no 3-letter language code for this locale. */ public String getISO3Language() { - if (languageCode.length() == 0) { - return languageCode; + String code = ICU.getISO3LanguageNative(toString()); + if (!languageCode.isEmpty() && code.isEmpty()) { + throw new MissingResourceException("No 3-letter language code for locale: " + this, "FormatData_" + this, "ShortLanguage"); } - return ICU.getISO3LanguageNative(toString()); + return code; } /** - * Returns an array of strings containing all the two-letter ISO country codes that can be + * Returns an array of strings containing all the two-letter ISO 3166 country codes that can be * used as the country code when constructing a {@code Locale}. */ public static String[] getISOCountries() { @@ -525,7 +529,7 @@ public final class Locale implements Cloneable, Serializable { } /** - * Returns an array of strings containing all the two-letter ISO language codes that can be + * Returns an array of strings containing all the two-letter ISO 639-1 language codes that can be * used as the language code when constructing a {@code Locale}. */ public static String[] getISOLanguages() { diff --git a/luni/src/main/java/java/util/zip/ZipEntry.java b/luni/src/main/java/java/util/zip/ZipEntry.java index 563f649..c8c733d 100644 --- a/luni/src/main/java/java/util/zip/ZipEntry.java +++ b/luni/src/main/java/java/util/zip/ZipEntry.java @@ -357,7 +357,13 @@ public class ZipEntry implements ZipConstants, Cloneable { throw new ZipException("Central Directory Entry not found"); } - it.seek(10); + it.seek(8); + int gpbf = it.readShort(); + + if ((gpbf & ~ZipFile.GPBF_SUPPORTED_MASK) != 0) { + throw new ZipException("Invalid General Purpose Bit Flag: " + gpbf); + } + compressionMethod = it.readShort(); time = it.readShort(); modDate = it.readShort(); diff --git a/luni/src/main/java/java/util/zip/ZipFile.java b/luni/src/main/java/java/util/zip/ZipFile.java index 4dacb82..93410b7 100644 --- a/luni/src/main/java/java/util/zip/ZipFile.java +++ b/luni/src/main/java/java/util/zip/ZipFile.java @@ -68,6 +68,12 @@ public class ZipFile implements ZipConstants { static final int GPBF_UTF8_FLAG = 1 << 11; /** + * Supported General Purpose Bit Flags Mask. + * Bit mask of supported GPBF bits. + */ + static final int GPBF_SUPPORTED_MASK = GPBF_DATA_DESCRIPTOR_FLAG | GPBF_UTF8_FLAG; + + /** * Open zip file for reading. */ public static final int OPEN_READ = 1; @@ -242,11 +248,19 @@ public class ZipFile implements ZipConstants { RandomAccessFile localRaf = raf; synchronized (localRaf) { // We don't know the entry data's start position. All we have is the - // position of the entry's local header. At position 28 we find the - // length of the extra data. In some cases this length differs from - // the one coming in the central header. - RAFStream rafStream = new RAFStream(localRaf, entry.localHeaderRelOffset + 28); + // position of the entry's local header. At position 6 we find the + // General Purpose Bit Flag. + // http://www.pkware.com/documents/casestudies/APPNOTE.TXT + RAFStream rafStream= new RAFStream(localRaf, entry.localHeaderRelOffset + 6); DataInputStream is = new DataInputStream(rafStream); + int gpbf = Short.reverseBytes(is.readShort()); + if ((gpbf & ~ZipFile.GPBF_SUPPORTED_MASK) != 0) { + throw new ZipException("Invalid General Purpose Bit Flag: " + gpbf); + } + + // At position 28 we find the length of the extra data. In some cases + // this length differs from the one coming in the central header. + is.skipBytes(20); int localExtraLenOrWhatever = Short.reverseBytes(is.readShort()); is.close(); diff --git a/luni/src/main/java/java/util/zip/ZipInputStream.java b/luni/src/main/java/java/util/zip/ZipInputStream.java index 97aa350..0135e1e 100644 --- a/luni/src/main/java/java/util/zip/ZipInputStream.java +++ b/luni/src/main/java/java/util/zip/ZipInputStream.java @@ -237,6 +237,10 @@ public class ZipInputStream extends InflaterInputStream implements ZipConstants throw new ZipException("Cannot read local header version " + version); } int flags = peekShort(LOCFLG - LOCVER); + if ((flags & ~ZipFile.GPBF_SUPPORTED_MASK) != 0) { + throw new ZipException("Invalid General Purpose Bit Flag: " + flags); + } + hasDD = ((flags & ZipFile.GPBF_DATA_DESCRIPTOR_FLAG) != 0); int ceLastModifiedTime = peekShort(LOCTIM - LOCVER); int ceLastModifiedDate = peekShort(LOCTIM - LOCVER + 2); diff --git a/luni/src/main/java/libcore/net/http/AbstractHttpInputStream.java b/luni/src/main/java/libcore/net/http/AbstractHttpInputStream.java deleted file mode 100644 index 70f76b7..0000000 --- a/luni/src/main/java/libcore/net/http/AbstractHttpInputStream.java +++ /dev/null @@ -1,107 +0,0 @@ -/* - * 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); - } -} diff --git a/luni/src/main/java/libcore/net/http/AbstractHttpOutputStream.java b/luni/src/main/java/libcore/net/http/AbstractHttpOutputStream.java deleted file mode 100644 index 1e1b47b..0000000 --- a/luni/src/main/java/libcore/net/http/AbstractHttpOutputStream.java +++ /dev/null @@ -1,40 +0,0 @@ -/* - * 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"); - } - } -} diff --git a/luni/src/main/java/libcore/net/http/Challenge.java b/luni/src/main/java/libcore/net/http/Challenge.java deleted file mode 100644 index 0326c17..0000000 --- a/luni/src/main/java/libcore/net/http/Challenge.java +++ /dev/null @@ -1,46 +0,0 @@ -/* - * 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. - * - * @hide - */ -public final class Challenge { - final String scheme; - final String realm; - - public 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(); - } - - @Override public String toString() { - return "Challenge[" + scheme + " " + realm + "]"; - } -} diff --git a/luni/src/main/java/libcore/net/http/ChunkedInputStream.java b/luni/src/main/java/libcore/net/http/ChunkedInputStream.java deleted file mode 100644 index 91cff7a..0000000 --- a/luni/src/main/java/libcore/net/http/ChunkedInputStream.java +++ /dev/null @@ -1,114 +0,0 @@ -/* - * 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.net.CacheRequest; -import java.util.Arrays; -import libcore.io.Streams; - -/** - * An HTTP body with alternating chunk sizes and chunk bodies. - */ -final 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 int bytesRemainingInChunk = NO_CHUNK_YET; - private boolean hasMoreChunks = true; - - ChunkedInputStream(InputStream is, CacheRequest cacheRequest, - HttpEngine httpEngine) throws IOException { - super(is, httpEngine, cacheRequest); - } - - @Override public int read(byte[] buffer, int offset, int count) throws IOException { - Arrays.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; - httpEngine.readTrailers(); - 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(); - } - } -} diff --git a/luni/src/main/java/libcore/net/http/ChunkedOutputStream.java b/luni/src/main/java/libcore/net/http/ChunkedOutputStream.java deleted file mode 100644 index db65f7b..0000000 --- a/luni/src/main/java/libcore/net/http/ChunkedOutputStream.java +++ /dev/null @@ -1,133 +0,0 @@ -/* - * 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 java.util.Arrays; - -/** - * 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. - */ -final 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; - - - public 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(); - Arrays.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); - } -} diff --git a/luni/src/main/java/libcore/net/http/FixedLengthInputStream.java b/luni/src/main/java/libcore/net/http/FixedLengthInputStream.java deleted file mode 100644 index 1091af7..0000000 --- a/luni/src/main/java/libcore/net/http/FixedLengthInputStream.java +++ /dev/null @@ -1,72 +0,0 @@ -/* - * 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.net.CacheRequest; -import java.util.Arrays; - -/** - * An HTTP body with a fixed length specified in advance. - */ -final 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 { - Arrays.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(); - } - } -} diff --git a/luni/src/main/java/libcore/net/http/FixedLengthOutputStream.java b/luni/src/main/java/libcore/net/http/FixedLengthOutputStream.java deleted file mode 100644 index 461eed9..0000000 --- a/luni/src/main/java/libcore/net/http/FixedLengthOutputStream.java +++ /dev/null @@ -1,61 +0,0 @@ -/* - * 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; -import java.util.Arrays; - -/** - * An HTTP body with a fixed length known in advance. - */ -final class FixedLengthOutputStream extends AbstractHttpOutputStream { - private final OutputStream socketOut; - private int bytesRemaining; - - public FixedLengthOutputStream(OutputStream socketOut, int bytesRemaining) { - this.socketOut = socketOut; - this.bytesRemaining = bytesRemaining; - } - - @Override public void write(byte[] buffer, int offset, int count) throws IOException { - checkNotClosed(); - Arrays.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"); - } - } -} diff --git a/luni/src/main/java/libcore/net/http/HeaderParser.java b/luni/src/main/java/libcore/net/http/HeaderParser.java deleted file mode 100644 index 8d5770e..0000000 --- a/luni/src/main/java/libcore/net/http/HeaderParser.java +++ /dev/null @@ -1,166 +0,0 @@ -/* - * 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; - -/** - * @hide - */ -public 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; - } - } -} diff --git a/luni/src/main/java/libcore/net/http/HttpConnection.java b/luni/src/main/java/libcore/net/http/HttpConnection.java deleted file mode 100644 index 4a6e65d..0000000 --- a/luni/src/main/java/libcore/net/http/HttpConnection.java +++ /dev/null @@ -1,343 +0,0 @@ -/* - * 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.List; -import javax.net.ssl.HostnameVerifier; -import javax.net.ssl.SSLSocket; -import javax.net.ssl.SSLSocketFactory; -import libcore.io.IoUtils; -import libcore.util.Objects; -import org.apache.harmony.xnet.provider.jsse.OpenSSLSocketImpl; - -/** - * Holds the sockets and streams of an HTTP or HTTPS 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 final Address address; - private final Socket socket; - private InputStream inputStream; - private OutputStream outputStream; - private SSLSocket unverifiedSocket; - private SSLSocket sslSocket; - private InputStream sslInputStream; - private OutputStream sslOutputStream; - private boolean recycled = false; - - 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; - } - } - } - - this.socket = socketCandidate; - } - - 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); - } - - public OutputStream getOutputStream() throws IOException { - if (sslSocket != null) { - if (sslOutputStream == null) { - sslOutputStream = sslSocket.getOutputStream(); - } - return sslOutputStream; - } else if(outputStream == null) { - outputStream = socket.getOutputStream(); - } - return outputStream; - } - - public InputStream getInputStream() throws IOException { - if (sslSocket != null) { - if (sslInputStream == null) { - sslInputStream = sslSocket.getInputStream(); - } - return sslInputStream; - } else if (inputStream == null) { - /* - * 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); - } - return inputStream; - } - - protected 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 - * TLS extensions and SSL deflate compression. If false, use - * an SSL3 only fallback mode without compression. - */ - public void setupSecureSocket(SSLSocketFactory sslSocketFactory, boolean tlsTolerant) - throws IOException { - // create the wrapper over connected socket - unverifiedSocket = (SSLSocket) sslSocketFactory.createSocket(socket, - address.uriHost, address.uriPort, true /* autoClose */); - // tlsTolerant mimics Chrome's behavior - if (tlsTolerant && unverifiedSocket instanceof OpenSSLSocketImpl) { - OpenSSLSocketImpl openSslSocket = (OpenSSLSocketImpl) unverifiedSocket; - openSslSocket.setUseSessionTickets(true); - openSslSocket.setHostname(address.uriHost); - // use SSLSocketFactory default enabled protocols - } else { - unverifiedSocket.setEnabledProtocols(new String [] { "SSLv3" }); - } - // force handshake, which can throw - unverifiedSocket.startHandshake(); - } - - /** - * Return an {@code SSLSocket} that is not only connected but has - * also passed hostname verification. - * - * @param hostnameVerifier Used to verify the hostname we - * connected to is an acceptable match for the peer certificate - * chain of the SSLSession. - */ - public SSLSocket verifySecureSocketHostname(HostnameVerifier hostnameVerifier) - throws IOException { - if (!hostnameVerifier.verify(address.uriHost, unverifiedSocket.getSession())) { - throw new IOException("Hostname '" + address.uriHost + "' was not verified"); - } - sslSocket = unverifiedSocket; - 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(); - } - - /** - * 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 = uri.getEffectivePort(); - 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 = uri.getEffectivePort(); - this.sslSocketFactory = sslSocketFactory; - - SocketAddress proxyAddress = proxy.address(); - if (!(proxyAddress instanceof InetSocketAddress)) { - throw new IllegalArgumentException("Proxy.address() is not an InetSocketAddress: " - + proxyAddress.getClass()); - } - InetSocketAddress proxySocketAddress = (InetSocketAddress) proxyAddress; - this.socketHost = proxySocketAddress.getHostName(); - this.socketPort = proxySocketAddress.getPort(); - if (uriHost == null) { - throw new UnknownHostException(uri.toString()); - } - } - - public Proxy getProxy() { - return proxy; - } - - @Override public boolean equals(Object other) { - if (other instanceof Address) { - Address that = (Address) other; - return Objects.equal(this.proxy, that.proxy) - && this.uriHost.equals(that.uriHost) - && this.uriPort == that.uriPort - && Objects.equal(this.sslSocketFactory, that.sslSocketFactory) - && this.requiresTunnel == that.requiresTunnel; - } - return false; - } - - @Override public int hashCode() { - int result = 17; - result = 31 * result + uriHost.hashCode(); - result = 31 * result + uriPort; - result = 31 * result + (sslSocketFactory != null ? sslSocketFactory.hashCode() : 0); - result = 31 * result + (proxy != null ? proxy.hashCode() : 0); - result = 31 * result + (requiresTunnel ? 1 : 0); - return result; - } - - public HttpConnection connect(int connectTimeout) throws IOException { - return new HttpConnection(this, connectTimeout); - } - } -} diff --git a/luni/src/main/java/libcore/net/http/HttpConnectionPool.java b/luni/src/main/java/libcore/net/http/HttpConnectionPool.java deleted file mode 100644 index 1f5f4d9..0000000 --- a/luni/src/main/java/libcore/net/http/HttpConnectionPool.java +++ /dev/null @@ -1,120 +0,0 @@ -/* - * 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 dalvik.system.SocketTagger; -import java.io.IOException; -import java.net.Socket; -import java.net.SocketException; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; - -/** - * A pool of HTTP connections. This class exposes its tuning parameters as - * system properties: - * <ul> - * <li>{@code http.keepAlive} true if HTTP connections should be pooled at - * all. Default is true. - * <li>{@code http.maxConnections} maximum number of connections to each URI. - * 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.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(); - SocketTagger.get().tag(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); - } - - public void recycle(HttpConnection connection) { - Socket socket = connection.getSocket(); - try { - SocketTagger.get().untag(socket); - } catch (SocketException e) { - // When unable to remove tagging, skip recycling and close - System.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(); - } -} diff --git a/luni/src/main/java/libcore/net/http/HttpEngine.java b/luni/src/main/java/libcore/net/http/HttpEngine.java deleted file mode 100644 index 8d81c38..0000000 --- a/luni/src/main/java/libcore/net/http/HttpEngine.java +++ /dev/null @@ -1,852 +0,0 @@ -/* - * 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.BufferedOutputStream; -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.ExtendedResponseCache; -import java.net.HttpURLConnection; -import java.net.Proxy; -import java.net.ResponseCache; -import java.net.ResponseSource; -import java.net.URI; -import java.net.URISyntaxException; -import java.net.URL; -import java.nio.charset.Charsets; -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.io.Streams; -import libcore.util.EmptyArray; - -/** - * 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 GATEWAY_TIMEOUT_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 504 Gateway Timeout")); - return result; - } - @Override public InputStream getBody() throws IOException { - return new ByteArrayInputStream(EmptyArray.BYTE); - } - }; - - /** - * 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; - - 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; - - /** - * 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 - */ - public static final int MAX_REDIRECTS = 5; - - protected final HttpURLConnectionImpl policy; - - protected final String method; - - private ResponseSource responseSource; - - protected HttpConnection connection; - private InputStream socketIn; - private 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; - private AbstractHttpOutputStream requestBodyOut; - - 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. */ - private 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; - - boolean sendChunked; - - /** - * 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. - */ - // TODO: is HTTP minor version tracked across HttpEngines? - private int httpMinorVersion = 1; // Assume HTTP/1.1 - - private final URI uri; - - private final RequestHeaders requestHeaders; - - /** Null until a response is received from the network or the cache */ - private 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 = policy.getURL().toURILenient(); - } 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 GATEWAY_TIMEOUT response instead, as specified - * by http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.9.4. - */ - if (requestHeaders.isOnlyIfCached() && responseSource.requiresConnection()) { - if (responseSource == ResponseSource.CONDITIONAL_CACHE) { - IoUtils.closeQuietly(cachedResponseBody); - } - this.responseSource = ResponseSource.CACHE; - this.cacheResponse = GATEWAY_TIMEOUT_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 (socketOut != null || requestOut != null || socketIn != null) { - throw new IllegalStateException(); - } - - socketOut = connection.getOutputStream(); - requestOut = socketOut; - socketIn = connection.getInputStream(); - - if (hasRequestBody()) { - initRequestBodyOut(); - } - } - - /** - * 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); - } - result.setSoTimeout(policy.getReadTimeout()); - return result; - } - - protected void initRequestBodyOut() throws IOException { - int chunkLength = policy.getChunkLength(); - if (chunkLength > 0 || requestHeaders.isChunked()) { - sendChunked = true; - if (chunkLength == -1) { - chunkLength = DEFAULT_CHUNK_LENGTH; - } - } - - if (socketOut == null) { - throw new IllegalStateException("No socket to write to; was a POST cached?"); - } - - if (httpMinorVersion == 0) { - sendChunked = false; - } - - int fixedContentLength = policy.getFixedContentLength(); - if (requestBodyOut != null) { - // request body was already initialized by the predecessor HTTP engine - } else if (fixedContentLength != -1) { - writeRequestHeaders(fixedContentLength); - requestBodyOut = new FixedLengthOutputStream(requestOut, fixedContentLength); - } else if (sendChunked) { - writeRequestHeaders(-1); - requestBodyOut = new ChunkedOutputStream(requestOut, chunkLength); - } else if (requestHeaders.getContentLength() != -1) { - writeRequestHeaders(requestHeaders.getContentLength()); - requestBodyOut = new RetryableOutputStream(requestHeaders.getContentLength()); - } else { - requestBodyOut = new RetryableOutputStream(); - } - } - - /** - * @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; - this.httpMinorVersion = responseHeaders.getHeaders().getHttpMinorVersion(); - if (body != null) { - initContentStream(body); - } - } - - private 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 { - // Never cache responses to proxy CONNECT requests. - if (method == CONNECT) { - return; - } - - // 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 HttpURLConnection 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. - */ - public final void automaticallyReleaseConnectionToPool() { - automaticallyReleaseConnectionToPool = true; - if (connection != null && connectionReleased) { - HttpConnectionPool.INSTANCE.recycle(connection); - connection = null; - } - } - - public final void markConnectionAsRecycled() { - if (connection != null) { - connection.setRecycled(); - } - } - - /** - * Releases this engine so that its resources may be either reused or - * closed. - */ - 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; - - // We cannot reuse sockets that have incomplete output. - if (requestBodyOut != null && !requestBodyOut.closed) { - reusable = false; - } - - // If the request specified that the connection shouldn't be reused, - // don't reuse it. This advice doesn't apply to CONNECT requests because - // the "Connection: close" header goes the origin server, not the proxy. - if (requestHeaders.hasConnectionClose() && method != CONNECT) { - reusable = false; - } - - // If the response specified that the connection shouldn't be reused, don't reuse it. - if (responseHeaders != null && responseHeaders.hasConnectionClose()) { - reusable = false; - } - - if (responseBodyIn instanceof UnknownLengthHttpInputStream) { - reusable = false; - } - - if (reusable && responseBodyIn != null) { - // We must discard the response body before the connection can be reused. - try { - Streams.skipAll(responseBodyIn); - } catch (IOException e) { - reusable = false; - } - } - - if (!reusable) { - 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 - * - * Also remove the Content-Length in this case because it contains the length - * of the gzipped response. This isn't terribly useful and is dangerous because - * clients can query the content length, but not the content encoding. - */ - responseHeaders.stripContentEncoding(); - responseHeaders.stripContentLength(); - responseBodyIn = new GZIPInputStream(transferStream); - } else { - responseBodyIn = transferStream; - } - } - - private InputStream getTransferStream() throws IOException { - if (!hasResponseBody()) { - return new FixedLengthInputStream(socketIn, cacheRequest, this, 0); - } - - if (responseHeaders.isChunked()) { - return new ChunkedInputStream(socketIn, cacheRequest, this); - } - - if (responseHeaders.getContentLength() != -1) { - return new FixedLengthInputStream(socketIn, cacheRequest, this, - 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, this); - } - - private void readResponseHeaders() throws IOException { - RawHeaders headers; - do { - headers = new RawHeaders(); - headers.setStatusLine(Streams.readAsciiLine(socketIn)); - readHeaders(headers); - } while (headers.getResponseCode() == HTTP_CONTINUE); - setResponse(new ResponseHeaders(uri, headers), null); - } - - /** - * 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; - } - - /** - * Trailers are headers included after the last chunk of a response encoded - * with chunked encoding. - */ - final void readTrailers() throws IOException { - readHeaders(responseHeaders.getHeaders()); - } - - 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(uri, headers.toMultimap()); - } - } - - /** - * 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. - * - * @param contentLength the number of bytes in the request body, or -1 if - * the request body length is unknown. - */ - private void writeRequestHeaders(int contentLength) throws IOException { - if (sentRequestMillis != -1) { - throw new IllegalStateException(); - } - - 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); - } - - sentRequestMillis = System.currentTimeMillis(); - requestOut.write(bytes); - } - - /** - * Returns the headers to send on a network request. - * - * <p>This adds the content length and content-type headers, which are - * neither needed nor known when querying the response cache. - * - * <p>It updates the status line, which may need to be fully qualified if - * the connection is using a proxy. - */ - protected RawHeaders getNetworkRequestHeaders() throws IOException { - requestHeaders.getHeaders().setStatusLine(getRequestLine()); - - int fixedContentLength = policy.getFixedContentLength(); - if (fixedContentLength != -1) { - requestHeaders.setContentLength(fixedContentLength); - } else if (sendChunked) { - requestHeaders.setChunked(); - } else if (requestBodyOut instanceof RetryableOutputStream) { - int contentLength = ((RetryableOutputStream) requestBodyOut).contentLength(); - requestHeaders.setContentLength(contentLength); - } - - return requestHeaders.getHeaders(); - } - - /** - * 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())); - } - - if (httpMinorVersion > 0 && requestHeaders.getConnection() == null) { - requestHeaders.setConnection("Keep-Alive"); - } - - if (requestHeaders.getAcceptEncoding() == null) { - transparentGzip = true; - 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())); - } - } - - private String getRequestLine() { - String protocol = (httpMinorVersion == 0) ? "HTTP/1.0" : "HTTP/1.1"; - 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) { - int contentLength = requestBodyOut instanceof RetryableOutputStream - ? ((RetryableOutputStream) requestBodyOut).contentLength() - : -1; - writeRequestHeaders(contentLength); - } - - if (requestBodyOut != null) { - requestBodyOut.close(); - if (requestBodyOut instanceof RetryableOutputStream) { - ((RetryableOutputStream) requestBodyOut).writeToSocket(requestOut); - } - } - - requestOut.flush(); - requestOut = socketOut; - - 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(getTransferStream()); - } -} diff --git a/luni/src/main/java/libcore/net/http/HttpHandler.java b/luni/src/main/java/libcore/net/http/HttpHandler.java deleted file mode 100644 index e168f42..0000000 --- a/luni/src/main/java/libcore/net/http/HttpHandler.java +++ /dev/null @@ -1,42 +0,0 @@ -/* - * 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 HttpHandler extends URLStreamHandler { - - @Override protected URLConnection openConnection(URL u) throws IOException { - return new HttpURLConnectionImpl(u, 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 HttpURLConnectionImpl(url, getDefaultPort(), proxy); - } - - @Override protected int getDefaultPort() { - return 80; - } -} diff --git a/luni/src/main/java/libcore/net/http/HttpResponseCache.java b/luni/src/main/java/libcore/net/http/HttpResponseCache.java deleted file mode 100644 index 1a9dfd1..0000000 --- a/luni/src/main/java/libcore/net/http/HttpResponseCache.java +++ /dev/null @@ -1,591 +0,0 @@ -/* - * 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.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.ExtendedResponseCache; -import java.net.HttpURLConnection; -import java.net.ResponseCache; -import java.net.ResponseSource; -import java.net.SecureCacheResponse; -import java.net.URI; -import java.net.URLConnection; -import java.nio.charset.Charsets; -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.HttpsURLConnection; -import javax.net.ssl.SSLPeerUnverifiedException; -import libcore.io.Base64; -import libcore.io.DiskLruCache; -import libcore.io.IoUtils; -import libcore.io.StrictLineReader; - -/** - * 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(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 HttpURLConnection)) { - return null; - } - - HttpURLConnection httpConnection = (HttpURLConnection) 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. - */ - public void update(CacheResponse conditionalCacheHit, HttpURLConnection 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(HttpURLConnection 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 write(byte[] buffer, int offset, int length) throws IOException { - // Since we don't override "write(int oneByte)", we can write directly to "out" - // and avoid the inefficient implementation from the FilterOutputStream. - out.write(buffer, offset, length); - } - }; - } - - @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 { - StrictLineReader reader = new StrictLineReader(in, Charsets.US_ASCII); - uri = reader.readLine(); - requestMethod = reader.readLine(); - varyHeaders = new RawHeaders(); - int varyRequestHeaderLineCount = reader.readInt(); - for (int i = 0; i < varyRequestHeaderLineCount; i++) { - varyHeaders.addLine(reader.readLine()); - } - - responseHeaders = new RawHeaders(); - responseHeaders.setStatusLine(reader.readLine()); - int responseHeaderLineCount = reader.readInt(); - for (int i = 0; i < responseHeaderLineCount; i++) { - responseHeaders.addLine(reader.readLine()); - } - - if (isHttps()) { - String blank = reader.readLine(); - if (!blank.isEmpty()) { - throw new IOException("expected \"\" but was \"" + blank + "\""); - } - cipherSuite = reader.readLine(); - peerCertificates = readCertArray(reader); - localCertificates = readCertArray(reader); - } else { - cipherSuite = null; - peerCertificates = null; - localCertificates = null; - } - } finally { - in.close(); - } - } - - public Entry(URI uri, RawHeaders varyHeaders, HttpURLConnection httpConnection) { - this.uri = uri.toString(); - this.varyHeaders = varyHeaders; - this.requestMethod = httpConnection.getRequestMethod(); - this.responseHeaders = RawHeaders.fromMultimap(httpConnection.getHeaderFields()); - - if (isHttps()) { - HttpsURLConnection httpsConnection = (HttpsURLConnection) 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(ENTRY_METADATA); - 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 Certificate[] readCertArray(StrictLineReader reader) throws IOException { - int length = reader.readInt(); - 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 = reader.readLine(); - 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(); - } - } -} diff --git a/luni/src/main/java/libcore/net/http/HttpURLConnectionImpl.java b/luni/src/main/java/libcore/net/http/HttpURLConnectionImpl.java deleted file mode 100644 index 3e6503f..0000000 --- a/luni/src/main/java/libcore/net/http/HttpURLConnectionImpl.java +++ /dev/null @@ -1,519 +0,0 @@ -/* - * 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.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 java.nio.charset.Charsets; -import java.security.Permission; -import java.util.List; -import java.util.Map; -import libcore.io.Base64; -import libcore.io.IoUtils; - -/** - * 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. - */ -class HttpURLConnectionImpl extends HttpURLConnection { - - private final int defaultPort; - - private Proxy proxy; - - private final RawHeaders rawRequestHeaders = new RawHeaders(); - - private int redirectionCount; - - protected IOException httpEngineFailure; - protected HttpEngine httpEngine; - - protected HttpURLConnectionImpl(URL url, int port) { - super(url); - defaultPort = port; - } - - protected 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) { - // We close the response body here instead of in - // HttpEngine.release because that is called when input - // has been completely read from the underlying socket. - // However the response body can be a GZIPInputStream that - // still has unread data. - if (httpEngine.hasResponse()) { - IoUtils.closeQuietly(httpEngine.getResponseBody()); - } - 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(); - } else { - httpEngine.markConnectionAsRecycled(); - } - - 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 > HttpEngine.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()) - && previousUrl.getEffectivePort() == url.getEffectivePort()) { - 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("Bad response code: " + responseCode); - } - - // 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); - } -} diff --git a/luni/src/main/java/libcore/net/http/HttpsHandler.java b/luni/src/main/java/libcore/net/http/HttpsHandler.java deleted file mode 100644 index ed9ba72..0000000 --- a/luni/src/main/java/libcore/net/http/HttpsHandler.java +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package libcore.net.http; - -import java.io.IOException; -import java.net.Proxy; -import java.net.URL; -import java.net.URLConnection; -import java.net.URLStreamHandler; - -public final class HttpsHandler extends URLStreamHandler { - - @Override protected URLConnection openConnection(URL url) throws IOException { - return new HttpsURLConnectionImpl(url, getDefaultPort()); - } - - @Override protected URLConnection openConnection(URL url, Proxy proxy) throws IOException { - if (url == null || proxy == null) { - throw new IllegalArgumentException("url == null || proxy == null"); - } - return new HttpsURLConnectionImpl(url, getDefaultPort(), proxy); - } - - @Override protected int getDefaultPort() { - return 443; - } -} diff --git a/luni/src/main/java/libcore/net/http/HttpsURLConnectionImpl.java b/luni/src/main/java/libcore/net/http/HttpsURLConnectionImpl.java deleted file mode 100644 index 9e3e4ef..0000000 --- a/luni/src/main/java/libcore/net/http/HttpsURLConnectionImpl.java +++ /dev/null @@ -1,580 +0,0 @@ -/* - * 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.io.InputStream; -import java.io.OutputStream; -import java.net.CacheResponse; -import java.net.HttpURLConnection; -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.HttpsURLConnection; -import javax.net.ssl.SSLHandshakeException; -import javax.net.ssl.SSLPeerUnverifiedException; -import javax.net.ssl.SSLSocket; -import javax.net.ssl.SSLSocketFactory; - -final class HttpsURLConnectionImpl extends HttpsURLConnection { - - /** HttpUrlConnectionDelegate allows reuse of HttpURLConnectionImpl */ - private final HttpUrlConnectionDelegate delegate; - - protected HttpsURLConnectionImpl(URL url, int port) { - super(url); - delegate = new HttpUrlConnectionDelegate(url, port); - } - - protected 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 more - // barebones connections - boolean connectionReused; - try { - connectionReused = 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); - connectionReused = makeSslConnection(false); - } - - if (!connectionReused) { - sslSocket = connection.verifySecureSocketHostname(enclosing.getHostnameVerifier()); - } - } - - /** - * Attempt to make an https connection. Returns true if a - * connection was reused, false otherwise. - * - * @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 boolean 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 true; - } - - connection.setupSecureSocket(enclosing.getSSLSocketFactory(), tlsTolerant); - return false; - } - - /** - * 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 HttpURLConnection 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); - } - - /** - * 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. - */ - @Override protected RawHeaders getNetworkRequestHeaders() throws IOException { - RequestHeaders privateHeaders = getRequestHeaders(); - URL url = policy.getURL(); - - RawHeaders result = new RawHeaders(); - result.setStatusLine("CONNECT " + url.getHost() + ":" + url.getEffectivePort() - + " HTTP/1.1"); - - // Always set Host and User-Agent. - String host = privateHeaders.getHost(); - if (host == null) { - host = getOriginAddress(url); - } - result.set("Host", host); - - String userAgent = privateHeaders.getUserAgent(); - if (userAgent == null) { - userAgent = 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 protected boolean requiresTunnel() { - return true; - } - } -} diff --git a/luni/src/main/java/libcore/net/http/RawHeaders.java b/luni/src/main/java/libcore/net/http/RawHeaders.java deleted file mode 100644 index 21b48f1..0000000 --- a/luni/src/main/java/libcore/net/http/RawHeaders.java +++ /dev/null @@ -1,297 +0,0 @@ -/* - * 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.util.ArrayList; -import java.util.Collections; -import java.util.Comparator; -import java.util.List; -import java.util.Map; -import java.util.Map.Entry; -import java.util.Set; -import java.util.TreeMap; - -/** - * 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 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. - */ - System.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; - } -} diff --git a/luni/src/main/java/libcore/net/http/RequestHeaders.java b/luni/src/main/java/libcore/net/http/RequestHeaders.java deleted file mode 100644 index 3b536ce..0000000 --- a/luni/src/main/java/libcore/net/http/RequestHeaders.java +++ /dev/null @@ -1,292 +0,0 @@ -/* - * 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()); - } - } - } -} diff --git a/luni/src/main/java/libcore/net/http/ResponseHeaders.java b/luni/src/main/java/libcore/net/http/ResponseHeaders.java deleted file mode 100644 index c0b4200..0000000 --- a/luni/src/main/java/libcore/net/http/ResponseHeaders.java +++ /dev/null @@ -1,508 +0,0 @@ -/* - * 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 java.net.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 void stripContentLength() { - contentLength = -1; - headers.removeAll("Content-Length"); - } - - 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"); - } -} diff --git a/luni/src/main/java/libcore/net/http/RetryableOutputStream.java b/luni/src/main/java/libcore/net/http/RetryableOutputStream.java deleted file mode 100644 index 67bb5b8..0000000 --- a/luni/src/main/java/libcore/net/http/RetryableOutputStream.java +++ /dev/null @@ -1,72 +0,0 @@ -/* - * 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 java.util.Arrays; - -/** - * 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() throws IOException { - if (closed) { - return; - } - closed = true; - if (content.size() < limit) { - throw new IOException("content-length promised " - + limit + " bytes, but received " + content.size()); - } - } - - @Override public synchronized void write(byte[] buffer, int offset, int count) - throws IOException { - checkNotClosed(); - Arrays.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() throws IOException { - close(); - return content.size(); - } - - public void writeToSocket(OutputStream socketOut) throws IOException { - content.writeTo(socketOut); - } -} diff --git a/luni/src/main/java/libcore/net/http/UnknownLengthHttpInputStream.java b/luni/src/main/java/libcore/net/http/UnknownLengthHttpInputStream.java deleted file mode 100644 index a8224a3..0000000 --- a/luni/src/main/java/libcore/net/http/UnknownLengthHttpInputStream.java +++ /dev/null @@ -1,65 +0,0 @@ -/* - * 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.net.CacheRequest; -import java.util.Arrays; - -/** - * An HTTP payload terminated by the end of the socket stream. - */ -final class UnknownLengthHttpInputStream extends AbstractHttpInputStream { - private boolean inputExhausted; - - 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 { - Arrays.checkOffsetAndCount(buffer.length, offset, count); - checkNotClosed(); - if (in == null || inputExhausted) { - return -1; - } - int read = in.read(buffer, offset, count); - if (read == -1) { - inputExhausted = true; - endOfInput(false); - return -1; - } - cacheWrite(buffer, offset, read); - return read; - } - - @Override public int available() throws IOException { - checkNotClosed(); - return in == null ? 0 : in.available(); - } - - @Override public void close() throws IOException { - if (closed) { - return; - } - closed = true; - if (!inputExhausted) { - unexpectedEndOfInput(); - } - } -} diff --git a/luni/src/main/java/org/apache/harmony/xnet/provider/jsse/NativeCrypto.java b/luni/src/main/java/org/apache/harmony/xnet/provider/jsse/NativeCrypto.java index b24ad0e..9991af4 100644 --- a/luni/src/main/java/org/apache/harmony/xnet/provider/jsse/NativeCrypto.java +++ b/luni/src/main/java/org/apache/harmony/xnet/provider/jsse/NativeCrypto.java @@ -206,6 +206,8 @@ public final class NativeCrypto { public static native byte[] EC_GROUP_get_order(long groupCtx); + public static native int EC_GROUP_get_degree(long groupCtx); + public static native byte[] EC_GROUP_get_cofactor(long groupCtx); public static native long EC_POINT_new(long groupRef); @@ -417,8 +419,6 @@ public final class NativeCrypto { public static native String[] get_X509_ex_xkusage(long x509ctx); - public static native int X509_check_ca(long x509ctx); - public static native int get_X509_ex_pathlen(long x509ctx); public static native long X509_get_notBefore(long x509ctx); @@ -439,6 +439,8 @@ public final class NativeCrypto { // --- X509 EXFLAG --------------------------------------------------------- + public static final int EXFLAG_CA = 0x10; + public static final int EXFLAG_CRITICAL = 0x200; // --- PKCS7 --------------------------------------------------------------- diff --git a/luni/src/main/java/org/apache/harmony/xnet/provider/jsse/OpenSSLDSAKeyFactory.java b/luni/src/main/java/org/apache/harmony/xnet/provider/jsse/OpenSSLDSAKeyFactory.java index 0dad242..efa4747 100644 --- a/luni/src/main/java/org/apache/harmony/xnet/provider/jsse/OpenSSLDSAKeyFactory.java +++ b/luni/src/main/java/org/apache/harmony/xnet/provider/jsse/OpenSSLDSAKeyFactory.java @@ -173,7 +173,7 @@ public class OpenSSLDSAKeyFactory extends KeyFactorySpi { } catch (InvalidKeySpecException e) { throw new InvalidKeyException(e); } - } else if ("PKCS#8".equals(key.getFormat())) { + } else if ((key instanceof PrivateKey) && ("PKCS#8".equals(key.getFormat()))) { byte[] encoded = key.getEncoded(); if (encoded == null) { throw new InvalidKeyException("Key does not support encoding"); @@ -183,7 +183,7 @@ public class OpenSSLDSAKeyFactory extends KeyFactorySpi { } catch (InvalidKeySpecException e) { throw new InvalidKeyException(e); } - } else if ("X.509".equals(key.getFormat())) { + } else if ((key instanceof PublicKey) && ("X.509".equals(key.getFormat()))) { byte[] encoded = key.getEncoded(); if (encoded == null) { throw new InvalidKeyException("Key does not support encoding"); diff --git a/luni/src/main/java/org/apache/harmony/xnet/provider/jsse/OpenSSLECDHKeyAgreement.java b/luni/src/main/java/org/apache/harmony/xnet/provider/jsse/OpenSSLECDHKeyAgreement.java index 096e300..5146156 100644 --- a/luni/src/main/java/org/apache/harmony/xnet/provider/jsse/OpenSSLECDHKeyAgreement.java +++ b/luni/src/main/java/org/apache/harmony/xnet/provider/jsse/OpenSSLECDHKeyAgreement.java @@ -19,9 +19,10 @@ package org.apache.harmony.xnet.provider.jsse; import java.security.InvalidAlgorithmParameterException; import java.security.InvalidKeyException; import java.security.Key; +import java.security.KeyFactory; +import java.security.PrivateKey; +import java.security.PublicKey; import java.security.SecureRandom; -import java.security.interfaces.ECPrivateKey; -import java.security.interfaces.ECPublicKey; import java.security.spec.AlgorithmParameterSpec; import javax.crypto.KeyAgreementSpi; @@ -54,36 +55,14 @@ public final class OpenSSLECDHKeyAgreement extends KeyAgreementSpi { if (!lastPhase) { throw new IllegalStateException("ECDH only has one phase"); } + if (key == null) { throw new InvalidKeyException("key == null"); } - if (!(key instanceof ECPublicKey)) { - throw new InvalidKeyException("This phase requires an ECPublicKey. Actual key type: " - + key.getClass()); - } - ECPublicKey publicKey = (ECPublicKey) key; - - OpenSSLKey openSslPublicKey; - if (publicKey instanceof OpenSSLECPublicKey) { - // OpenSSL-backed key - openSslPublicKey = ((OpenSSLECPublicKey) publicKey).getOpenSSLKey(); - } else { - // Not an OpenSSL-backed key -- create an OpenSSL-backed key from its X.509 encoding - if (!"X.509".equals(publicKey.getFormat())) { - throw new InvalidKeyException("Non-OpenSSL public key (" + publicKey.getClass() - + ") offers unsupported encoding format: " + publicKey.getFormat()); - } - byte[] encoded = publicKey.getEncoded(); - if (encoded == null) { - throw new InvalidKeyException("Non-OpenSSL public key (" + publicKey.getClass() - + ") does not provide encoded form"); - } - try { - openSslPublicKey = new OpenSSLKey(NativeCrypto.d2i_PUBKEY(encoded)); - } catch (Exception e) { - throw new InvalidKeyException("Failed to decode X.509 encoded public key", e); - } + if (!(key instanceof PublicKey)) { + throw new InvalidKeyException("Not a public key: " + key.getClass()); } + OpenSSLKey openSslPublicKey = translateKeyToEcOpenSSLKey(key); byte[] buffer = new byte[mExpectedResultLength]; int actualResultLength = NativeCrypto.ECDH_compute_key( @@ -142,35 +121,15 @@ public final class OpenSSLECDHKeyAgreement extends KeyAgreementSpi { if (key == null) { throw new InvalidKeyException("key == null"); } - if (!(key instanceof ECPrivateKey)) { - throw new InvalidKeyException("Not an EC private key: " + key.getClass()); - } - ECPrivateKey privateKey = (ECPrivateKey) key; - mExpectedResultLength = - (privateKey.getParams().getCurve().getField().getFieldSize() + 7) / 8; - - OpenSSLKey openSslPrivateKey; - if (privateKey instanceof OpenSSLECPrivateKey) { - // OpenSSL-backed key - openSslPrivateKey = ((OpenSSLECPrivateKey) privateKey).getOpenSSLKey(); - } else { - // Not an OpenSSL-backed key -- create an OpenSSL-backed key from its PKCS#8 encoding - if (!"PKCS#8".equals(privateKey.getFormat())) { - throw new InvalidKeyException("Non-OpenSSL private key (" + privateKey.getClass() - + ") offers unsupported encoding format: " + privateKey.getFormat()); - } - byte[] encoded = privateKey.getEncoded(); - if (encoded == null) { - throw new InvalidKeyException("Non-OpenSSL private key (" + privateKey.getClass() - + ") does not provide encoded form"); - } - try { - openSslPrivateKey = new OpenSSLKey(NativeCrypto.d2i_PKCS8_PRIV_KEY_INFO(encoded)); - } catch (Exception e) { - throw new InvalidKeyException("Failed to decode PKCS#8 encoded private key", e); - } + if (!(key instanceof PrivateKey)) { + throw new InvalidKeyException("Not a private key: " + key.getClass()); } - mOpenSslPrivateKey = openSslPrivateKey; + + OpenSSLKey openSslKey = translateKeyToEcOpenSSLKey(key); + int fieldSizeBits = NativeCrypto.EC_GROUP_get_degree(NativeCrypto.EC_KEY_get0_group( + openSslKey.getPkeyContext())); + mExpectedResultLength = (fieldSizeBits + 7) / 8; + mOpenSslPrivateKey = openSslKey; } @Override @@ -188,4 +147,13 @@ public final class OpenSSLECDHKeyAgreement extends KeyAgreementSpi { throw new IllegalStateException("Key agreement not completed"); } } + + private static OpenSSLKey translateKeyToEcOpenSSLKey(Key key) throws InvalidKeyException { + try { + return ((OpenSSLKeyHolder) KeyFactory.getInstance( + "EC", OpenSSLProvider.PROVIDER_NAME).translateKey(key)).getOpenSSLKey(); + } catch (Exception e) { + throw new InvalidKeyException("Failed to translate key to OpenSSL EC key", e); + } + } } diff --git a/luni/src/main/java/org/apache/harmony/xnet/provider/jsse/OpenSSLECKeyFactory.java b/luni/src/main/java/org/apache/harmony/xnet/provider/jsse/OpenSSLECKeyFactory.java index c8e4f4e..e4673a5 100644 --- a/luni/src/main/java/org/apache/harmony/xnet/provider/jsse/OpenSSLECKeyFactory.java +++ b/luni/src/main/java/org/apache/harmony/xnet/provider/jsse/OpenSSLECKeyFactory.java @@ -159,7 +159,7 @@ public class OpenSSLECKeyFactory extends KeyFactorySpi { } catch (InvalidKeySpecException e) { throw new InvalidKeyException(e); } - } else if ("PKCS#8".equals(key.getFormat())) { + } else if ((key instanceof PrivateKey) && ("PKCS#8".equals(key.getFormat()))) { byte[] encoded = key.getEncoded(); if (encoded == null) { throw new InvalidKeyException("Key does not support encoding"); @@ -169,7 +169,7 @@ public class OpenSSLECKeyFactory extends KeyFactorySpi { } catch (InvalidKeySpecException e) { throw new InvalidKeyException(e); } - } else if ("X.509".equals(key.getFormat())) { + } else if ((key instanceof PublicKey) && ("X.509".equals(key.getFormat()))) { byte[] encoded = key.getEncoded(); if (encoded == null) { throw new InvalidKeyException("Key does not support encoding"); diff --git a/luni/src/main/java/org/apache/harmony/xnet/provider/jsse/OpenSSLRSAKeyFactory.java b/luni/src/main/java/org/apache/harmony/xnet/provider/jsse/OpenSSLRSAKeyFactory.java index 8e720a5..9c61abd 100644 --- a/luni/src/main/java/org/apache/harmony/xnet/provider/jsse/OpenSSLRSAKeyFactory.java +++ b/luni/src/main/java/org/apache/harmony/xnet/provider/jsse/OpenSSLRSAKeyFactory.java @@ -204,7 +204,7 @@ public class OpenSSLRSAKeyFactory extends KeyFactorySpi { } catch (InvalidKeySpecException e) { throw new InvalidKeyException(e); } - } else if ("PKCS#8".equals(key.getFormat())) { + } else if ((key instanceof PrivateKey) && ("PKCS#8".equals(key.getFormat()))) { byte[] encoded = key.getEncoded(); if (encoded == null) { throw new InvalidKeyException("Key does not support encoding"); @@ -214,7 +214,7 @@ public class OpenSSLRSAKeyFactory extends KeyFactorySpi { } catch (InvalidKeySpecException e) { throw new InvalidKeyException(e); } - } else if ("X.509".equals(key.getFormat())) { + } else if ((key instanceof PublicKey) && ("X.509".equals(key.getFormat()))) { byte[] encoded = key.getEncoded(); if (encoded == null) { throw new InvalidKeyException("Key does not support encoding"); diff --git a/luni/src/main/java/org/apache/harmony/xnet/provider/jsse/OpenSSLX509CertPath.java b/luni/src/main/java/org/apache/harmony/xnet/provider/jsse/OpenSSLX509CertPath.java index 4639dfd..a96c361 100644 --- a/luni/src/main/java/org/apache/harmony/xnet/provider/jsse/OpenSSLX509CertPath.java +++ b/luni/src/main/java/org/apache/harmony/xnet/provider/jsse/OpenSSLX509CertPath.java @@ -137,10 +137,21 @@ public class OpenSSLX509CertPath extends CertPath { private static CertPath fromPkiPathEncoding(InputStream inStream) throws CertificateException { OpenSSLBIOInputStream bis = new OpenSSLBIOInputStream(inStream); + final boolean markable = inStream.markSupported(); + if (markable) { + inStream.mark(PUSHBACK_SIZE); + } + final long[] certRefs; try { certRefs = NativeCrypto.ASN1_seq_unpack_X509_bio(bis.getBioContext()); } catch (Exception e) { + if (markable) { + try { + inStream.reset(); + } catch (IOException ignored) { + } + } throw new CertificateException(e); } finally { NativeCrypto.BIO_free(bis.getBioContext()); @@ -218,6 +229,10 @@ public class OpenSSLX509CertPath extends CertPath { public static CertPath fromEncoding(InputStream inStream, String encoding) throws CertificateException { + if (inStream == null) { + throw new CertificateException("inStream == null"); + } + Encoding enc = Encoding.findByApiName(encoding); if (enc == null) { throw new CertificateException("Invalid encoding: " + encoding); diff --git a/luni/src/main/java/org/apache/harmony/xnet/provider/jsse/OpenSSLX509Certificate.java b/luni/src/main/java/org/apache/harmony/xnet/provider/jsse/OpenSSLX509Certificate.java index af960d5..1f48001 100644 --- a/luni/src/main/java/org/apache/harmony/xnet/provider/jsse/OpenSSLX509Certificate.java +++ b/luni/src/main/java/org/apache/harmony/xnet/provider/jsse/OpenSSLX509Certificate.java @@ -309,7 +309,7 @@ public class OpenSSLX509Certificate extends X509Certificate { @Override public int getBasicConstraints() { - if (NativeCrypto.X509_check_ca(mContext) != 1) { + if ((NativeCrypto.get_X509_ex_flags(mContext) & NativeCrypto.EXFLAG_CA) == 0) { return -1; } diff --git a/luni/src/main/java/org/apache/harmony/xnet/provider/jsse/OpenSSLX509CertificateFactory.java b/luni/src/main/java/org/apache/harmony/xnet/provider/jsse/OpenSSLX509CertificateFactory.java index 09e2507..ca79785 100644 --- a/luni/src/main/java/org/apache/harmony/xnet/provider/jsse/OpenSSLX509CertificateFactory.java +++ b/luni/src/main/java/org/apache/harmony/xnet/provider/jsse/OpenSSLX509CertificateFactory.java @@ -116,8 +116,11 @@ public class OpenSSLX509CertificateFactory extends CertificateFactorySpi { public Collection<? extends T> generateItems(InputStream inStream) throws ParsingException { + if (inStream == null) { + throw new ParsingException("inStream == null"); + } try { - if (inStream == null || inStream.available() == 0) { + if (inStream.available() == 0) { return Collections.emptyList(); } } catch (IOException e) { @@ -291,6 +294,10 @@ public class OpenSSLX509CertificateFactory extends CertificateFactorySpi { @Override public Collection<? extends CRL> engineGenerateCRLs(InputStream inStream) throws CRLException { + if (inStream == null) { + return Collections.emptyList(); + } + try { return crlParser.generateItems(inStream); } catch (ParsingException e) { diff --git a/luni/src/main/native/org_apache_harmony_xnet_provider_jsse_NativeCrypto.cpp b/luni/src/main/native/org_apache_harmony_xnet_provider_jsse_NativeCrypto.cpp index ee6a57e..65ac31f 100644 --- a/luni/src/main/native/org_apache_harmony_xnet_provider_jsse_NativeCrypto.cpp +++ b/luni/src/main/native/org_apache_harmony_xnet_provider_jsse_NativeCrypto.cpp @@ -394,6 +394,9 @@ static void throwForEvpError(JNIEnv* env, int reason, const char *message) { case EVP_R_WRONG_PUBLIC_KEY_TYPE: throwSignatureException(env, message); break; + case EVP_R_UNSUPPORTED_ALGORITHM: + throwNoSuchAlgorithmException(env, message); + break; default: jniThrowRuntimeException(env, message); break; @@ -2360,6 +2363,22 @@ static jbyteArray NativeCrypto_EC_GROUP_get_order(JNIEnv* env, jclass, jlong gro return orderArray; } +static jint NativeCrypto_EC_GROUP_get_degree(JNIEnv* env, jclass, jlong groupRef) +{ + const EC_GROUP* group = reinterpret_cast<const EC_GROUP*>(groupRef); + JNI_TRACE("EC_GROUP_get_degree(%p)", group); + + jint degree = EC_GROUP_get_degree(group); + if (degree == 0) { + JNI_TRACE("EC_GROUP_get_degree(%p) => unsupported", group); + jniThrowRuntimeException(env, "not supported"); + return 0; + } + + JNI_TRACE("EC_GROUP_get_degree(%p) => %d", group, degree); + return degree; +} + static jbyteArray NativeCrypto_EC_GROUP_get_cofactor(JNIEnv* env, jclass, jlong groupRef) { const EC_GROUP* group = reinterpret_cast<const EC_GROUP*>(groupRef); @@ -4672,6 +4691,13 @@ static jbyteArray NativeCrypto_i2d_X509(JNIEnv* env, jclass, jlong x509Ref) { return ASN1ToByteArray<X509, i2d_X509>(env, x509); } +static jbyteArray NativeCrypto_i2d_X509_PUBKEY(JNIEnv* env, jclass, jlong x509Ref) { + X509* x509 = reinterpret_cast<X509*>(static_cast<uintptr_t>(x509Ref)); + JNI_TRACE("i2d_X509_PUBKEY(%p)", x509); + return ASN1ToByteArray<X509_PUBKEY, i2d_X509_PUBKEY>(env, X509_get_X509_PUBKEY(x509)); +} + + template<typename T, T* (*PEM_read_func)(BIO*, T**, pem_password_cb*, void*)> static jlong PEM_ASN1Object_to_jlong(JNIEnv* env, jlong bioRef) { BIO* bio = reinterpret_cast<BIO*>(static_cast<uintptr_t>(bioRef)); @@ -4686,6 +4712,10 @@ static jlong PEM_ASN1Object_to_jlong(JNIEnv* env, jlong bioRef) { T* x = PEM_read_func(bio, NULL, NULL, NULL); if (x == NULL) { throwExceptionIfNecessary(env, "PEM_ASN1Object_to_jlong"); + // Sometimes the PEM functions fail without pushing an error + if (!env->ExceptionCheck()) { + jniThrowRuntimeException(env, "Failure parsing PEM"); + } JNI_TRACE("PEM_ASN1Object_to_jlong(%p) => threw exception", bio); return 0; } @@ -5154,21 +5184,6 @@ static jobjectArray NativeCrypto_get_X509_ex_xkusage(JNIEnv* env, jclass, jlong return exKeyUsage.release(); } -static jint NativeCrypto_X509_check_ca(JNIEnv* env, jclass, jlong x509Ref) { - X509* x509 = reinterpret_cast<X509*>(static_cast<uintptr_t>(x509Ref)); - JNI_TRACE("X509_check_ca(%p)", x509); - - if (x509 == NULL) { - jniThrowNullPointerException(env, "x509 == null"); - JNI_TRACE("X509_check_ca(%p) => x509 == null", x509); - return 0; - } - - int ret = X509_check_ca(x509); - JNI_TRACE("X509_check_ca(%p) => %d", x509, ret); - return ret; -} - static jint NativeCrypto_get_X509_ex_pathlen(JNIEnv* env, jclass, jlong x509Ref) { X509* x509 = reinterpret_cast<X509*>(static_cast<uintptr_t>(x509Ref)); JNI_TRACE("get_X509_ex_pathlen(%p)", x509); @@ -7821,6 +7836,7 @@ static JNINativeMethod sNativeCryptoMethods[] = { NATIVE_METHOD(NativeCrypto, EC_GROUP_get_curve_name, "(J)Ljava/lang/String;"), NATIVE_METHOD(NativeCrypto, EC_GROUP_get_curve, "(J)[[B"), NATIVE_METHOD(NativeCrypto, EC_GROUP_get_order, "(J)[B"), + NATIVE_METHOD(NativeCrypto, EC_GROUP_get_degree, "(J)I"), NATIVE_METHOD(NativeCrypto, EC_GROUP_get_cofactor, "(J)[B"), NATIVE_METHOD(NativeCrypto, EC_GROUP_clear_free, "(J)V"), NATIVE_METHOD(NativeCrypto, EC_GROUP_cmp, "(JJ)Z"), @@ -7882,6 +7898,7 @@ static JNINativeMethod sNativeCryptoMethods[] = { NATIVE_METHOD(NativeCrypto, d2i_X509_bio, "(J)J"), NATIVE_METHOD(NativeCrypto, d2i_X509, "([B)J"), NATIVE_METHOD(NativeCrypto, i2d_X509, "(J)[B"), + NATIVE_METHOD(NativeCrypto, i2d_X509_PUBKEY, "(J)[B"), NATIVE_METHOD(NativeCrypto, PEM_read_bio_X509, "(J)J"), NATIVE_METHOD(NativeCrypto, PEM_read_bio_PKCS7, "(JI)[J"), NATIVE_METHOD(NativeCrypto, d2i_PKCS7_bio, "(JI)[J"), @@ -7902,7 +7919,6 @@ static JNINativeMethod sNativeCryptoMethods[] = { NATIVE_METHOD(NativeCrypto, get_X509_subjectUID, "(J)[Z"), NATIVE_METHOD(NativeCrypto, get_X509_ex_kusage, "(J)[Z"), NATIVE_METHOD(NativeCrypto, get_X509_ex_xkusage, "(J)[Ljava/lang/String;"), - NATIVE_METHOD(NativeCrypto, X509_check_ca, "(J)I"), NATIVE_METHOD(NativeCrypto, get_X509_ex_pathlen, "(J)I"), NATIVE_METHOD(NativeCrypto, X509_get_ext_oid, "(JLjava/lang/String;)[B"), NATIVE_METHOD(NativeCrypto, X509_CRL_get_ext_oid, "(JLjava/lang/String;)[B"), diff --git a/luni/src/test/java/libcore/java/net/URLConnectionTest.java b/luni/src/test/java/libcore/java/net/URLConnectionTest.java index 1f2ebf9..ac38eb8 100644 --- a/luni/src/test/java/libcore/java/net/URLConnectionTest.java +++ b/luni/src/test/java/libcore/java/net/URLConnectionTest.java @@ -16,14 +16,11 @@ package libcore.java.net; +import com.android.okhttp.internal.http.HttpResponseCache; import com.google.mockwebserver.MockResponse; import com.google.mockwebserver.MockWebServer; import com.google.mockwebserver.RecordedRequest; import com.google.mockwebserver.SocketPolicy; -import static com.google.mockwebserver.SocketPolicy.DISCONNECT_AT_END; -import static com.google.mockwebserver.SocketPolicy.DISCONNECT_AT_START; -import static com.google.mockwebserver.SocketPolicy.SHUTDOWN_INPUT_AT_END; -import static com.google.mockwebserver.SocketPolicy.SHUTDOWN_OUTPUT_AT_END; import dalvik.system.CloseGuard; import java.io.ByteArrayOutputStream; import java.io.Closeable; @@ -49,21 +46,15 @@ import java.net.URLConnection; import java.net.UnknownHostException; import java.security.cert.CertificateException; import java.security.cert.X509Certificate; -import java.text.DateFormat; -import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; -import java.util.Date; import java.util.HashSet; import java.util.Iterator; import java.util.List; -import java.util.Locale; import java.util.Map; import java.util.Set; -import java.util.TimeZone; import java.util.UUID; -import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicReference; import java.util.zip.GZIPInputStream; @@ -81,9 +72,13 @@ import junit.framework.TestCase; import libcore.java.lang.ref.FinalizationTester; import libcore.java.security.TestKeyStore; import libcore.javax.net.ssl.TestSSLContext; -import libcore.net.http.HttpResponseCache; import tests.net.StuckServer; +import static com.google.mockwebserver.SocketPolicy.DISCONNECT_AT_END; +import static com.google.mockwebserver.SocketPolicy.DISCONNECT_AT_START; +import static com.google.mockwebserver.SocketPolicy.SHUTDOWN_INPUT_AT_END; +import static com.google.mockwebserver.SocketPolicy.SHUTDOWN_OUTPUT_AT_END; + public final class URLConnectionTest extends TestCase { private MockWebServer server = new MockWebServer(); private HttpResponseCache cache; diff --git a/luni/src/test/java/libcore/java/nio/BufferTest.java b/luni/src/test/java/libcore/java/nio/BufferTest.java index a4e5eaf..ae90d49 100644 --- a/luni/src/test/java/libcore/java/nio/BufferTest.java +++ b/luni/src/test/java/libcore/java/nio/BufferTest.java @@ -715,7 +715,7 @@ public class BufferTest extends TestCase { public void testHasArrayOnJniDirectByteBuffer() throws Exception { // Simulate a call to JNI's NewDirectByteBuffer. Class<?> c = Class.forName("java.nio.DirectByteBuffer"); - Constructor<?> ctor = c.getDeclaredConstructor(int.class, int.class); + Constructor<?> ctor = c.getDeclaredConstructor(long.class, int.class); ctor.setAccessible(true); ByteBuffer bb = (ByteBuffer) ctor.newInstance(0, 0); diff --git a/luni/src/test/java/libcore/java/util/LocaleTest.java b/luni/src/test/java/libcore/java/util/LocaleTest.java index b0522a3..5de67da 100644 --- a/luni/src/test/java/libcore/java/util/LocaleTest.java +++ b/luni/src/test/java/libcore/java/util/LocaleTest.java @@ -25,6 +25,7 @@ import java.text.NumberFormat; import java.util.Arrays; import java.util.Calendar; import java.util.Locale; +import java.util.MissingResourceException; public class LocaleTest extends junit.framework.TestCase { // http://b/2611311; if there's no display language/country/variant, use the raw codes. @@ -125,4 +126,42 @@ public class LocaleTest extends junit.framework.TestCase { } assertEquals(1, count); } -} + + public void test_getISO3Country() { + // Empty country code. + assertEquals("", new Locale("en", "").getISO3Country()); + + // Invalid country code. + try { + assertEquals("", new Locale("en", "XX").getISO3Country()); + fail(); + } catch (MissingResourceException expected) { + assertEquals("FormatData_en_XX", expected.getClassName()); + assertEquals("ShortCountry", expected.getKey()); + } + + // Valid country code. + assertEquals("CAN", new Locale("", "CA").getISO3Country()); + assertEquals("CAN", new Locale("en", "CA").getISO3Country()); + assertEquals("CAN", new Locale("xx", "CA").getISO3Country()); + } + + public void test_getISO3Language() { + // Empty language code. + assertEquals("", new Locale("", "US").getISO3Language()); + + // Invalid language code. + try { + assertEquals("", new Locale("xx", "US").getISO3Language()); + fail(); + } catch (MissingResourceException expected) { + assertEquals("FormatData_xx_US", expected.getClassName()); + assertEquals("ShortLanguage", expected.getKey()); + } + + // Valid language code. + assertEquals("eng", new Locale("en", "").getISO3Language()); + assertEquals("eng", new Locale("en", "CA").getISO3Language()); + assertEquals("eng", new Locale("en", "XX").getISO3Language()); + } + } diff --git a/luni/src/test/java/libcore/net/http/HttpResponseCacheTest.java b/luni/src/test/java/libcore/net/http/HttpResponseCacheTest.java deleted file mode 100644 index 133924e..0000000 --- a/luni/src/test/java/libcore/net/http/HttpResponseCacheTest.java +++ /dev/null @@ -1,1886 +0,0 @@ -/* - * 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 com.google.mockwebserver.MockResponse; -import com.google.mockwebserver.MockWebServer; -import com.google.mockwebserver.RecordedRequest; -import static com.google.mockwebserver.SocketPolicy.DISCONNECT_AT_END; -import java.io.BufferedReader; -import java.io.ByteArrayOutputStream; -import java.io.File; -import java.io.FileNotFoundException; -import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.io.OutputStream; -import java.lang.reflect.InvocationHandler; -import java.net.CacheRequest; -import java.net.CacheResponse; -import java.net.CookieHandler; -import java.net.CookieManager; -import java.net.HttpCookie; -import java.net.HttpURLConnection; -import java.net.ResponseCache; -import java.net.SecureCacheResponse; -import java.net.URI; -import java.net.URISyntaxException; -import java.net.URL; -import java.net.URLConnection; -import java.security.Principal; -import java.security.cert.Certificate; -import java.text.DateFormat; -import java.text.SimpleDateFormat; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.Date; -import java.util.Deque; -import java.util.Iterator; -import java.util.List; -import java.util.Locale; -import java.util.Map; -import java.util.TimeZone; -import java.util.UUID; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicInteger; -import java.util.concurrent.atomic.AtomicReference; -import java.util.zip.GZIPOutputStream; -import javax.net.ssl.HttpsURLConnection; -import junit.framework.TestCase; -import libcore.javax.net.ssl.TestSSLContext; -import tests.io.MockOs; - -public final class HttpResponseCacheTest extends TestCase { - private MockWebServer server = new MockWebServer(); - private HttpResponseCache cache; - private final MockOs mockOs = new MockOs(); - private final CookieManager cookieManager = new CookieManager(); - - @Override protected void setUp() throws Exception { - super.setUp(); - - String tmp = System.getProperty("java.io.tmpdir"); - File cacheDir = new File(tmp, "HttpCache-" + UUID.randomUUID()); - cache = new HttpResponseCache(cacheDir, Integer.MAX_VALUE); - ResponseCache.setDefault(cache); - mockOs.install(); - CookieHandler.setDefault(cookieManager); - } - - @Override protected void tearDown() throws Exception { - mockOs.uninstall(); - server.shutdown(); - ResponseCache.setDefault(null); - cache.getCache().delete(); - CookieHandler.setDefault(null); - super.tearDown(); - } - - /** - * Test that response caching is consistent with the RI and the spec. - * http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.4 - */ - public void testResponseCachingByResponseCode() throws Exception { - // Test each documented HTTP/1.1 code, plus the first unused value in each range. - // http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html - - // We can't test 100 because it's not really a response. - // assertCached(false, 100); - assertCached(false, 101); - assertCached(false, 102); - assertCached(true, 200); - assertCached(false, 201); - assertCached(false, 202); - assertCached(true, 203); - assertCached(false, 204); - assertCached(false, 205); - assertCached(false, 206); // we don't cache partial responses - assertCached(false, 207); - assertCached(true, 300); - assertCached(true, 301); - for (int i = 302; i <= 308; ++i) { - assertCached(false, i); - } - for (int i = 400; i <= 406; ++i) { - assertCached(false, i); - } - // (See test_responseCaching_407.) - assertCached(false, 408); - assertCached(false, 409); - // (See test_responseCaching_410.) - for (int i = 411; i <= 418; ++i) { - assertCached(false, i); - } - for (int i = 500; i <= 506; ++i) { - assertCached(false, i); - } - } - - /** - * Response code 407 should only come from proxy servers. Android's client - * throws if it is sent by an origin server. - */ - public void testOriginServerSends407() throws Exception { - server.enqueue(new MockResponse().setResponseCode(407)); - server.play(); - - URL url = server.getUrl("/"); - HttpURLConnection conn = (HttpURLConnection) url.openConnection(); - try { - conn.getResponseCode(); - fail(); - } catch (IOException expected) { - } - } - - public void test_responseCaching_410() throws Exception { - // the HTTP spec permits caching 410s, but the RI doesn't. - assertCached(true, 410); - } - - private void assertCached(boolean shouldPut, int responseCode) throws Exception { - server = new MockWebServer(); - MockResponse response = new MockResponse() - .addHeader("Last-Modified: " + formatDate(-1, TimeUnit.HOURS)) - .addHeader("Expires: " + formatDate(1, TimeUnit.HOURS)) - .setResponseCode(responseCode) - .setBody("ABCDE") - .addHeader("WWW-Authenticate: challenge"); - if (responseCode == HttpURLConnection.HTTP_PROXY_AUTH) { - response.addHeader("Proxy-Authenticate: Basic realm=\"protected area\""); - } else if (responseCode == HttpURLConnection.HTTP_UNAUTHORIZED) { - response.addHeader("WWW-Authenticate: Basic realm=\"protected area\""); - } - server.enqueue(response); - server.play(); - - URL url = server.getUrl("/"); - HttpURLConnection conn = (HttpURLConnection) url.openConnection(); - assertEquals(responseCode, conn.getResponseCode()); - - // exhaust the content stream - readAscii(conn); - - CacheResponse cached = cache.get(url.toURI(), "GET", - Collections.<String, List<String>>emptyMap()); - if (shouldPut) { - assertNotNull(Integer.toString(responseCode), cached); - cached.getBody().close(); - } else { - assertNull(Integer.toString(responseCode), cached); - } - server.shutdown(); // tearDown() isn't sufficient; this test starts multiple servers - } - - /** - * Test that we can interrogate the response when the cache is being - * populated. http://code.google.com/p/android/issues/detail?id=7787 - */ - public void testResponseCacheCallbackApis() throws Exception { - final String body = "ABCDE"; - final AtomicInteger cacheCount = new AtomicInteger(); - - server.enqueue(new MockResponse() - .setStatus("HTTP/1.1 200 Fantastic") - .addHeader("fgh: ijk") - .setBody(body)); - server.play(); - - ResponseCache.setDefault(new ResponseCache() { - @Override public CacheResponse get(URI uri, String requestMethod, - Map<String, List<String>> requestHeaders) throws IOException { - return null; - } - @Override public CacheRequest put(URI uri, URLConnection conn) throws IOException { - HttpURLConnection httpConnection = (HttpURLConnection) conn; - try { - httpConnection.getRequestProperties(); - fail(); - } catch (IllegalStateException expected) { - } - try { - httpConnection.addRequestProperty("K", "V"); - fail(); - } catch (IllegalStateException expected) { - } - assertEquals("HTTP/1.1 200 Fantastic", httpConnection.getHeaderField(null)); - assertEquals(Arrays.asList("HTTP/1.1 200 Fantastic"), - httpConnection.getHeaderFields().get(null)); - assertEquals(200, httpConnection.getResponseCode()); - assertEquals("Fantastic", httpConnection.getResponseMessage()); - assertEquals(body.length(), httpConnection.getContentLength()); - assertEquals("ijk", httpConnection.getHeaderField("fgh")); - try { - httpConnection.getInputStream(); // the RI doesn't forbid this, but it should - fail(); - } catch (IOException expected) { - } - cacheCount.incrementAndGet(); - return null; - } - }); - - URL url = server.getUrl("/"); - URLConnection connection = url.openConnection(); - assertEquals(body, readAscii(connection)); - assertEquals(1, cacheCount.get()); - } - - - public void testResponseCachingAndInputStreamSkipWithFixedLength() throws IOException { - testResponseCaching(TransferKind.FIXED_LENGTH); - } - - public void testResponseCachingAndInputStreamSkipWithChunkedEncoding() throws IOException { - testResponseCaching(TransferKind.CHUNKED); - } - - public void testResponseCachingAndInputStreamSkipWithNoLengthHeaders() throws IOException { - testResponseCaching(TransferKind.END_OF_STREAM); - } - - /** - * HttpURLConnection.getInputStream().skip(long) causes ResponseCache corruption - * http://code.google.com/p/android/issues/detail?id=8175 - */ - private void testResponseCaching(TransferKind transferKind) throws IOException { - MockResponse response = new MockResponse() - .addHeader("Last-Modified: " + formatDate(-1, TimeUnit.HOURS)) - .addHeader("Expires: " + formatDate(1, TimeUnit.HOURS)) - .setStatus("HTTP/1.1 200 Fantastic"); - transferKind.setBody(response, "I love puppies but hate spiders", 1); - server.enqueue(response); - server.play(); - - // Make sure that calling skip() doesn't omit bytes from the cache. - HttpURLConnection urlConnection = (HttpURLConnection) server.getUrl("/").openConnection(); - InputStream in = urlConnection.getInputStream(); - assertEquals("I love ", readAscii(urlConnection, "I love ".length())); - reliableSkip(in, "puppies but hate ".length()); - assertEquals("spiders", readAscii(urlConnection, "spiders".length())); - assertEquals(-1, in.read()); - in.close(); - assertEquals(1, cache.getWriteSuccessCount()); - assertEquals(0, cache.getWriteAbortCount()); - - urlConnection = (HttpURLConnection) server.getUrl("/").openConnection(); // cached! - in = urlConnection.getInputStream(); - assertEquals("I love puppies but hate spiders", - readAscii(urlConnection, "I love puppies but hate spiders".length())); - assertEquals(200, urlConnection.getResponseCode()); - assertEquals("Fantastic", urlConnection.getResponseMessage()); - - assertEquals(-1, in.read()); - in.close(); - assertEquals(1, cache.getWriteSuccessCount()); - assertEquals(0, cache.getWriteAbortCount()); - assertEquals(2, cache.getRequestCount()); - assertEquals(1, cache.getHitCount()); - } - - public void testSecureResponseCaching() throws IOException { - TestSSLContext testSSLContext = TestSSLContext.create(); - server.useHttps(testSSLContext.serverContext.getSocketFactory(), false); - server.enqueue(new MockResponse() - .addHeader("Last-Modified: " + formatDate(-1, TimeUnit.HOURS)) - .addHeader("Expires: " + formatDate(1, TimeUnit.HOURS)) - .setBody("ABC")); - server.play(); - - HttpsURLConnection connection = (HttpsURLConnection) server.getUrl("/").openConnection(); - connection.setSSLSocketFactory(testSSLContext.clientContext.getSocketFactory()); - assertEquals("ABC", readAscii(connection)); - - // OpenJDK 6 fails on this line, complaining that the connection isn't open yet - String suite = connection.getCipherSuite(); - List<Certificate> localCerts = toListOrNull(connection.getLocalCertificates()); - List<Certificate> serverCerts = toListOrNull(connection.getServerCertificates()); - Principal peerPrincipal = connection.getPeerPrincipal(); - Principal localPrincipal = connection.getLocalPrincipal(); - - connection = (HttpsURLConnection) server.getUrl("/").openConnection(); // cached! - connection.setSSLSocketFactory(testSSLContext.clientContext.getSocketFactory()); - assertEquals("ABC", readAscii(connection)); - - assertEquals(2, cache.getRequestCount()); - assertEquals(1, cache.getNetworkCount()); - assertEquals(1, cache.getHitCount()); - - assertEquals(suite, connection.getCipherSuite()); - assertEquals(localCerts, toListOrNull(connection.getLocalCertificates())); - assertEquals(serverCerts, toListOrNull(connection.getServerCertificates())); - assertEquals(peerPrincipal, connection.getPeerPrincipal()); - assertEquals(localPrincipal, connection.getLocalPrincipal()); - } - - public void testCacheReturnsInsecureResponseForSecureRequest() throws IOException { - TestSSLContext testSSLContext = TestSSLContext.create(); - server.useHttps(testSSLContext.serverContext.getSocketFactory(), false); - server.enqueue(new MockResponse().setBody("ABC")); - server.enqueue(new MockResponse().setBody("DEF")); - server.play(); - - ResponseCache.setDefault(new InsecureResponseCache()); - - HttpsURLConnection connection = (HttpsURLConnection) server.getUrl("/").openConnection(); - connection.setSSLSocketFactory(testSSLContext.clientContext.getSocketFactory()); - assertEquals("ABC", readAscii(connection)); - - connection = (HttpsURLConnection) server.getUrl("/").openConnection(); // not cached! - connection.setSSLSocketFactory(testSSLContext.clientContext.getSocketFactory()); - assertEquals("DEF", readAscii(connection)); - } - - public void testResponseCachingAndRedirects() throws Exception { - server.enqueue(new MockResponse() - .addHeader("Last-Modified: " + formatDate(-1, TimeUnit.HOURS)) - .addHeader("Expires: " + formatDate(1, TimeUnit.HOURS)) - .setResponseCode(HttpURLConnection.HTTP_MOVED_PERM) - .addHeader("Location: /foo")); - server.enqueue(new MockResponse() - .addHeader("Last-Modified: " + formatDate(-1, TimeUnit.HOURS)) - .addHeader("Expires: " + formatDate(1, TimeUnit.HOURS)) - .setBody("ABC")); - server.enqueue(new MockResponse().setBody("DEF")); - server.play(); - - URLConnection connection = server.getUrl("/").openConnection(); - assertEquals("ABC", readAscii(connection)); - - connection = server.getUrl("/").openConnection(); // cached! - assertEquals("ABC", readAscii(connection)); - - assertEquals(4, cache.getRequestCount()); // 2 requests + 2 redirects - assertEquals(2, cache.getNetworkCount()); - assertEquals(2, cache.getHitCount()); - } - - public void testRedirectToCachedResult() throws Exception { - server.enqueue(new MockResponse() - .addHeader("Cache-Control: max-age=60") - .setBody("ABC")); - server.enqueue(new MockResponse() - .setResponseCode(HttpURLConnection.HTTP_MOVED_PERM) - .addHeader("Location: /foo")); - server.enqueue(new MockResponse().setBody("DEF")); - server.play(); - - assertEquals("ABC", readAscii(server.getUrl("/foo").openConnection())); - RecordedRequest request1 = server.takeRequest(); - assertEquals("GET /foo HTTP/1.1", request1.getRequestLine()); - assertEquals(0, request1.getSequenceNumber()); - - assertEquals("ABC", readAscii(server.getUrl("/bar").openConnection())); - RecordedRequest request2 = server.takeRequest(); - assertEquals("GET /bar HTTP/1.1", request2.getRequestLine()); - assertEquals(1, request2.getSequenceNumber()); - - // an unrelated request should reuse the pooled connection - assertEquals("DEF", readAscii(server.getUrl("/baz").openConnection())); - RecordedRequest request3 = server.takeRequest(); - assertEquals("GET /baz HTTP/1.1", request3.getRequestLine()); - assertEquals(2, request3.getSequenceNumber()); - } - - public void testSecureResponseCachingAndRedirects() throws IOException { - TestSSLContext testSSLContext = TestSSLContext.create(); - server.useHttps(testSSLContext.serverContext.getSocketFactory(), false); - server.enqueue(new MockResponse() - .addHeader("Last-Modified: " + formatDate(-1, TimeUnit.HOURS)) - .addHeader("Expires: " + formatDate(1, TimeUnit.HOURS)) - .setResponseCode(HttpURLConnection.HTTP_MOVED_PERM) - .addHeader("Location: /foo")); - server.enqueue(new MockResponse() - .addHeader("Last-Modified: " + formatDate(-1, TimeUnit.HOURS)) - .addHeader("Expires: " + formatDate(1, TimeUnit.HOURS)) - .setBody("ABC")); - server.enqueue(new MockResponse().setBody("DEF")); - server.play(); - - HttpsURLConnection connection = (HttpsURLConnection) server.getUrl("/").openConnection(); - connection.setSSLSocketFactory(testSSLContext.clientContext.getSocketFactory()); - assertEquals("ABC", readAscii(connection)); - - connection = (HttpsURLConnection) server.getUrl("/").openConnection(); // cached! - connection.setSSLSocketFactory(testSSLContext.clientContext.getSocketFactory()); - assertEquals("ABC", readAscii(connection)); - - assertEquals(4, cache.getRequestCount()); // 2 direct + 2 redirect = 4 - assertEquals(2, cache.getHitCount()); - } - - public void testResponseCacheRequestHeaders() throws IOException, URISyntaxException { - server.enqueue(new MockResponse().setBody("ABC")); - server.play(); - - final AtomicReference<Map<String, List<String>>> requestHeadersRef - = new AtomicReference<Map<String, List<String>>>(); - ResponseCache.setDefault(new ResponseCache() { - @Override public CacheResponse get(URI uri, String requestMethod, - Map<String, List<String>> requestHeaders) throws IOException { - requestHeadersRef.set(requestHeaders); - return null; - } - @Override public CacheRequest put(URI uri, URLConnection conn) throws IOException { - return null; - } - }); - - URL url = server.getUrl("/"); - URLConnection urlConnection = url.openConnection(); - urlConnection.addRequestProperty("A", "android"); - readAscii(urlConnection); - assertEquals(Arrays.asList("android"), requestHeadersRef.get().get("A")); - } - - - public void testServerDisconnectsPrematurelyWithContentLengthHeader() throws IOException { - testServerPrematureDisconnect(TransferKind.FIXED_LENGTH); - } - - public void testServerDisconnectsPrematurelyWithChunkedEncoding() throws IOException { - testServerPrematureDisconnect(TransferKind.CHUNKED); - } - - public void testServerDisconnectsPrematurelyWithNoLengthHeaders() throws IOException { - /* - * Intentionally empty. This case doesn't make sense because there's no - * such thing as a premature disconnect when the disconnect itself - * indicates the end of the data stream. - */ - } - - private void testServerPrematureDisconnect(TransferKind transferKind) throws IOException { - MockResponse response = new MockResponse(); - transferKind.setBody(response, "ABCDE\nFGHIJKLMNOPQRSTUVWXYZ", 16); - server.enqueue(truncateViolently(response, 16)); - server.enqueue(new MockResponse().setBody("Request #2")); - server.play(); - - BufferedReader reader = new BufferedReader(new InputStreamReader( - server.getUrl("/").openConnection().getInputStream())); - assertEquals("ABCDE", reader.readLine()); - try { - reader.readLine(); - fail("This implementation silently ignored a truncated HTTP body."); - } catch (IOException expected) { - } finally { - reader.close(); - } - - assertEquals(1, cache.getWriteAbortCount()); - assertEquals(0, cache.getWriteSuccessCount()); - URLConnection connection = server.getUrl("/").openConnection(); - assertEquals("Request #2", readAscii(connection)); - assertEquals(1, cache.getWriteAbortCount()); - assertEquals(1, cache.getWriteSuccessCount()); - } - - public void testClientPrematureDisconnectWithContentLengthHeader() throws IOException { - testClientPrematureDisconnect(TransferKind.FIXED_LENGTH); - } - - public void testClientPrematureDisconnectWithChunkedEncoding() throws IOException { - testClientPrematureDisconnect(TransferKind.CHUNKED); - } - - public void testClientPrematureDisconnectWithNoLengthHeaders() throws IOException { - testClientPrematureDisconnect(TransferKind.END_OF_STREAM); - } - - private void testClientPrematureDisconnect(TransferKind transferKind) throws IOException { - MockResponse response = new MockResponse(); - transferKind.setBody(response, "ABCDE\nFGHIJKLMNOPQRSTUVWXYZ", 1024); - server.enqueue(response); - server.enqueue(new MockResponse().setBody("Request #2")); - server.play(); - - URLConnection connection = server.getUrl("/").openConnection(); - InputStream in = connection.getInputStream(); - assertEquals("ABCDE", readAscii(connection, 5)); - in.close(); - try { - in.read(); - fail("Expected an IOException because the stream is closed."); - } catch (IOException expected) { - } - - assertEquals(1, cache.getWriteAbortCount()); - assertEquals(0, cache.getWriteSuccessCount()); - connection = server.getUrl("/").openConnection(); - assertEquals("Request #2", readAscii(connection)); - assertEquals(1, cache.getWriteAbortCount()); - assertEquals(1, cache.getWriteSuccessCount()); - } - - public void testDefaultExpirationDateFullyCachedForLessThan24Hours() throws Exception { - // last modified: 105 seconds ago - // served: 5 seconds ago - // default lifetime: (105 - 5) / 10 = 10 seconds - // expires: 10 seconds from served date = 5 seconds from now - server.enqueue(new MockResponse() - .addHeader("Last-Modified: " + formatDate(-105, TimeUnit.SECONDS)) - .addHeader("Date: " + formatDate(-5, TimeUnit.SECONDS)) - .setBody("A")); - server.play(); - - URL url = server.getUrl("/"); - assertEquals("A", readAscii(url.openConnection())); - URLConnection connection = url.openConnection(); - assertEquals("A", readAscii(connection)); - assertNull(connection.getHeaderField("Warning")); - } - - public void testDefaultExpirationDateConditionallyCached() throws Exception { - // last modified: 115 seconds ago - // served: 15 seconds ago - // default lifetime: (115 - 15) / 10 = 10 seconds - // expires: 10 seconds from served date = 5 seconds ago - String lastModifiedDate = formatDate(-115, TimeUnit.SECONDS); - RecordedRequest conditionalRequest = assertConditionallyCached(new MockResponse() - .addHeader("Last-Modified: " + lastModifiedDate) - .addHeader("Date: " + formatDate(-15, TimeUnit.SECONDS))); - List<String> headers = conditionalRequest.getHeaders(); - assertTrue(headers.contains("If-Modified-Since: " + lastModifiedDate)); - } - - public void testDefaultExpirationDateFullyCachedForMoreThan24Hours() throws Exception { - // last modified: 105 days ago - // served: 5 days ago - // default lifetime: (105 - 5) / 10 = 10 days - // expires: 10 days from served date = 5 days from now - server.enqueue(new MockResponse() - .addHeader("Last-Modified: " + formatDate(-105, TimeUnit.DAYS)) - .addHeader("Date: " + formatDate(-5, TimeUnit.DAYS)) - .setBody("A")); - server.play(); - - assertEquals("A", readAscii(server.getUrl("/").openConnection())); - URLConnection connection = server.getUrl("/").openConnection(); - assertEquals("A", readAscii(connection)); - assertEquals("113 HttpURLConnection \"Heuristic expiration\"", - connection.getHeaderField("Warning")); - } - - public void testNoDefaultExpirationForUrlsWithQueryString() throws Exception { - server.enqueue(new MockResponse() - .addHeader("Last-Modified: " + formatDate(-105, TimeUnit.SECONDS)) - .addHeader("Date: " + formatDate(-5, TimeUnit.SECONDS)) - .setBody("A")); - server.enqueue(new MockResponse().setBody("B")); - server.play(); - - URL url = server.getUrl("/?foo=bar"); - assertEquals("A", readAscii(url.openConnection())); - assertEquals("B", readAscii(url.openConnection())); - } - - public void testExpirationDateInThePastWithLastModifiedHeader() throws Exception { - String lastModifiedDate = formatDate(-2, TimeUnit.HOURS); - RecordedRequest conditionalRequest = assertConditionallyCached(new MockResponse() - .addHeader("Last-Modified: " + lastModifiedDate) - .addHeader("Expires: " + formatDate(-1, TimeUnit.HOURS))); - List<String> headers = conditionalRequest.getHeaders(); - assertTrue(headers.contains("If-Modified-Since: " + lastModifiedDate)); - } - - public void testExpirationDateInThePastWithNoLastModifiedHeader() throws Exception { - assertNotCached(new MockResponse() - .addHeader("Expires: " + formatDate(-1, TimeUnit.HOURS))); - } - - public void testExpirationDateInTheFuture() throws Exception { - assertFullyCached(new MockResponse() - .addHeader("Expires: " + formatDate(1, TimeUnit.HOURS))); - } - - public void testMaxAgePreferredWithMaxAgeAndExpires() throws Exception { - assertFullyCached(new MockResponse() - .addHeader("Date: " + formatDate(0, TimeUnit.HOURS)) - .addHeader("Expires: " + formatDate(-1, TimeUnit.HOURS)) - .addHeader("Cache-Control: max-age=60")); - } - - public void testMaxAgeInThePastWithDateAndLastModifiedHeaders() throws Exception { - String lastModifiedDate = formatDate(-2, TimeUnit.HOURS); - RecordedRequest conditionalRequest = assertConditionallyCached(new MockResponse() - .addHeader("Date: " + formatDate(-120, TimeUnit.SECONDS)) - .addHeader("Last-Modified: " + lastModifiedDate) - .addHeader("Cache-Control: max-age=60")); - List<String> headers = conditionalRequest.getHeaders(); - assertTrue(headers.contains("If-Modified-Since: " + lastModifiedDate)); - } - - public void testMaxAgeInThePastWithDateHeaderButNoLastModifiedHeader() throws Exception { - /* - * Chrome interprets max-age relative to the local clock. Both our cache - * and Firefox both use the earlier of the local and server's clock. - */ - assertNotCached(new MockResponse() - .addHeader("Date: " + formatDate(-120, TimeUnit.SECONDS)) - .addHeader("Cache-Control: max-age=60")); - } - - public void testMaxAgeInTheFutureWithDateHeader() throws Exception { - assertFullyCached(new MockResponse() - .addHeader("Date: " + formatDate(0, TimeUnit.HOURS)) - .addHeader("Cache-Control: max-age=60")); - } - - public void testMaxAgeInTheFutureWithNoDateHeader() throws Exception { - assertFullyCached(new MockResponse() - .addHeader("Cache-Control: max-age=60")); - } - - public void testMaxAgeWithLastModifiedButNoServedDate() throws Exception { - assertFullyCached(new MockResponse() - .addHeader("Last-Modified: " + formatDate(-120, TimeUnit.SECONDS)) - .addHeader("Cache-Control: max-age=60")); - } - - public void testMaxAgeInTheFutureWithDateAndLastModifiedHeaders() throws Exception { - assertFullyCached(new MockResponse() - .addHeader("Last-Modified: " + formatDate(-120, TimeUnit.SECONDS)) - .addHeader("Date: " + formatDate(0, TimeUnit.SECONDS)) - .addHeader("Cache-Control: max-age=60")); - } - - public void testMaxAgePreferredOverLowerSharedMaxAge() throws Exception { - assertFullyCached(new MockResponse() - .addHeader("Date: " + formatDate(-2, TimeUnit.MINUTES)) - .addHeader("Cache-Control: s-maxage=60") - .addHeader("Cache-Control: max-age=180")); - } - - public void testMaxAgePreferredOverHigherMaxAge() throws Exception { - assertNotCached(new MockResponse() - .addHeader("Date: " + formatDate(-2, TimeUnit.MINUTES)) - .addHeader("Cache-Control: s-maxage=180") - .addHeader("Cache-Control: max-age=60")); - } - - public void testRequestMethodOptionsIsNotCached() throws Exception { - testRequestMethod("OPTIONS", false); - } - - public void testRequestMethodGetIsCached() throws Exception { - testRequestMethod("GET", true); - } - - public void testRequestMethodHeadIsNotCached() throws Exception { - // We could support this but choose not to for implementation simplicity - testRequestMethod("HEAD", false); - } - - public void testRequestMethodPostIsNotCached() throws Exception { - // We could support this but choose not to for implementation simplicity - testRequestMethod("POST", false); - } - - public void testRequestMethodPutIsNotCached() throws Exception { - testRequestMethod("PUT", false); - } - - public void testRequestMethodDeleteIsNotCached() throws Exception { - testRequestMethod("DELETE", false); - } - - public void testRequestMethodTraceIsNotCached() throws Exception { - testRequestMethod("TRACE", false); - } - - private void testRequestMethod(String requestMethod, boolean expectCached) throws Exception { - /* - * 1. seed the cache (potentially) - * 2. expect a cache hit or miss - */ - server.enqueue(new MockResponse() - .addHeader("Expires: " + formatDate(1, TimeUnit.HOURS)) - .addHeader("X-Response-ID: 1")); - server.enqueue(new MockResponse() - .addHeader("X-Response-ID: 2")); - server.play(); - - URL url = server.getUrl("/"); - - HttpURLConnection request1 = (HttpURLConnection) url.openConnection(); - request1.setRequestMethod(requestMethod); - addRequestBodyIfNecessary(requestMethod, request1); - assertEquals("1", request1.getHeaderField("X-Response-ID")); - - URLConnection request2 = url.openConnection(); - if (expectCached) { - assertEquals("1", request1.getHeaderField("X-Response-ID")); - } else { - assertEquals("2", request2.getHeaderField("X-Response-ID")); - } - } - - public void testPostInvalidatesCache() throws Exception { - testMethodInvalidates("POST"); - } - - public void testPutInvalidatesCache() throws Exception { - testMethodInvalidates("PUT"); - } - - public void testDeleteMethodInvalidatesCache() throws Exception { - testMethodInvalidates("DELETE"); - } - - private void testMethodInvalidates(String requestMethod) throws Exception { - /* - * 1. seed the cache - * 2. invalidate it - * 3. expect a cache miss - */ - server.enqueue(new MockResponse().setBody("A") - .addHeader("Expires: " + formatDate(1, TimeUnit.HOURS))); - server.enqueue(new MockResponse().setBody("B")); - server.enqueue(new MockResponse().setBody("C")); - server.play(); - - URL url = server.getUrl("/"); - - assertEquals("A", readAscii(url.openConnection())); - - HttpURLConnection invalidate = (HttpURLConnection) url.openConnection(); - invalidate.setRequestMethod(requestMethod); - addRequestBodyIfNecessary(requestMethod, invalidate); - assertEquals("B", readAscii(invalidate)); - - assertEquals("C", readAscii(url.openConnection())); - } - - public void testEtag() throws Exception { - RecordedRequest conditionalRequest = assertConditionallyCached(new MockResponse() - .addHeader("ETag: v1")); - assertTrue(conditionalRequest.getHeaders().contains("If-None-Match: v1")); - } - - public void testEtagAndExpirationDateInThePast() throws Exception { - String lastModifiedDate = formatDate(-2, TimeUnit.HOURS); - RecordedRequest conditionalRequest = assertConditionallyCached(new MockResponse() - .addHeader("ETag: v1") - .addHeader("Last-Modified: " + lastModifiedDate) - .addHeader("Expires: " + formatDate(-1, TimeUnit.HOURS))); - List<String> headers = conditionalRequest.getHeaders(); - assertTrue(headers.contains("If-None-Match: v1")); - assertTrue(headers.contains("If-Modified-Since: " + lastModifiedDate)); - } - - public void testEtagAndExpirationDateInTheFuture() throws Exception { - assertFullyCached(new MockResponse() - .addHeader("ETag: v1") - .addHeader("Last-Modified: " + formatDate(-2, TimeUnit.HOURS)) - .addHeader("Expires: " + formatDate(1, TimeUnit.HOURS))); - } - - public void testCacheControlNoCache() throws Exception { - assertNotCached(new MockResponse().addHeader("Cache-Control: no-cache")); - } - - public void testCacheControlNoCacheAndExpirationDateInTheFuture() throws Exception { - String lastModifiedDate = formatDate(-2, TimeUnit.HOURS); - RecordedRequest conditionalRequest = assertConditionallyCached(new MockResponse() - .addHeader("Last-Modified: " + lastModifiedDate) - .addHeader("Expires: " + formatDate(1, TimeUnit.HOURS)) - .addHeader("Cache-Control: no-cache")); - List<String> headers = conditionalRequest.getHeaders(); - assertTrue(headers.contains("If-Modified-Since: " + lastModifiedDate)); - } - - public void testPragmaNoCache() throws Exception { - assertNotCached(new MockResponse().addHeader("Pragma: no-cache")); - } - - public void testPragmaNoCacheAndExpirationDateInTheFuture() throws Exception { - String lastModifiedDate = formatDate(-2, TimeUnit.HOURS); - RecordedRequest conditionalRequest = assertConditionallyCached(new MockResponse() - .addHeader("Last-Modified: " + lastModifiedDate) - .addHeader("Expires: " + formatDate(1, TimeUnit.HOURS)) - .addHeader("Pragma: no-cache")); - List<String> headers = conditionalRequest.getHeaders(); - assertTrue(headers.contains("If-Modified-Since: " + lastModifiedDate)); - } - - public void testCacheControlNoStore() throws Exception { - assertNotCached(new MockResponse().addHeader("Cache-Control: no-store")); - } - - public void testCacheControlNoStoreAndExpirationDateInTheFuture() throws Exception { - assertNotCached(new MockResponse() - .addHeader("Last-Modified: " + formatDate(-2, TimeUnit.HOURS)) - .addHeader("Expires: " + formatDate(1, TimeUnit.HOURS)) - .addHeader("Cache-Control: no-store")); - } - - public void testPartialRangeResponsesDoNotCorruptCache() throws Exception { - /* - * 1. request a range - * 2. request a full document, expecting a cache miss - */ - server.enqueue(new MockResponse().setBody("AA") - .setResponseCode(HttpURLConnection.HTTP_PARTIAL) - .addHeader("Expires: " + formatDate(1, TimeUnit.HOURS)) - .addHeader("Content-Range: bytes 1000-1001/2000")); - server.enqueue(new MockResponse().setBody("BB")); - server.play(); - - URL url = server.getUrl("/"); - - URLConnection range = url.openConnection(); - range.addRequestProperty("Range", "bytes=1000-1001"); - assertEquals("AA", readAscii(range)); - - assertEquals("BB", readAscii(url.openConnection())); - } - - public void testServerReturnsDocumentOlderThanCache() throws Exception { - server.enqueue(new MockResponse().setBody("A") - .addHeader("Last-Modified: " + formatDate(-2, TimeUnit.HOURS)) - .addHeader("Expires: " + formatDate(-1, TimeUnit.HOURS))); - server.enqueue(new MockResponse().setBody("B") - .addHeader("Last-Modified: " + formatDate(-4, TimeUnit.HOURS))); - server.play(); - - URL url = server.getUrl("/"); - - assertEquals("A", readAscii(url.openConnection())); - assertEquals("A", readAscii(url.openConnection())); - } - - public void testNonIdentityEncodingAndConditionalCache() throws Exception { - assertNonIdentityEncodingCached(new MockResponse() - .addHeader("Last-Modified: " + formatDate(-2, TimeUnit.HOURS)) - .addHeader("Expires: " + formatDate(-1, TimeUnit.HOURS))); - } - - public void testNonIdentityEncodingAndFullCache() throws Exception { - assertNonIdentityEncodingCached(new MockResponse() - .addHeader("Last-Modified: " + formatDate(-2, TimeUnit.HOURS)) - .addHeader("Expires: " + formatDate(1, TimeUnit.HOURS))); - } - - private void assertNonIdentityEncodingCached(MockResponse response) throws Exception { - server.enqueue(response - .setBody(gzip("ABCABCABC".getBytes("UTF-8"))) - .addHeader("Content-Encoding: gzip")); - server.enqueue(new MockResponse().setResponseCode(HttpURLConnection.HTTP_NOT_MODIFIED)); - - server.play(); - assertEquals("ABCABCABC", readAscii(server.getUrl("/").openConnection())); - assertEquals("ABCABCABC", readAscii(server.getUrl("/").openConnection())); - } - - public void testExpiresDateBeforeModifiedDate() throws Exception { - assertConditionallyCached(new MockResponse() - .addHeader("Last-Modified: " + formatDate(-1, TimeUnit.HOURS)) - .addHeader("Expires: " + formatDate(-2, TimeUnit.HOURS))); - } - - public void testRequestMaxAge() throws IOException { - server.enqueue(new MockResponse().setBody("A") - .addHeader("Last-Modified: " + formatDate(-2, TimeUnit.HOURS)) - .addHeader("Date: " + formatDate(-1, TimeUnit.MINUTES)) - .addHeader("Expires: " + formatDate(1, TimeUnit.HOURS))); - server.enqueue(new MockResponse().setBody("B")); - - server.play(); - assertEquals("A", readAscii(server.getUrl("/").openConnection())); - - URLConnection connection = server.getUrl("/").openConnection(); - connection.addRequestProperty("Cache-Control", "max-age=30"); - assertEquals("B", readAscii(connection)); - } - - public void testRequestMinFresh() throws IOException { - server.enqueue(new MockResponse().setBody("A") - .addHeader("Cache-Control: max-age=60") - .addHeader("Date: " + formatDate(0, TimeUnit.MINUTES))); - server.enqueue(new MockResponse().setBody("B")); - - server.play(); - assertEquals("A", readAscii(server.getUrl("/").openConnection())); - - URLConnection connection = server.getUrl("/").openConnection(); - connection.addRequestProperty("Cache-Control", "min-fresh=120"); - assertEquals("B", readAscii(connection)); - } - - public void testRequestMaxStale() throws IOException { - server.enqueue(new MockResponse().setBody("A") - .addHeader("Cache-Control: max-age=120") - .addHeader("Date: " + formatDate(-4, TimeUnit.MINUTES))); - server.enqueue(new MockResponse().setBody("B")); - - server.play(); - assertEquals("A", readAscii(server.getUrl("/").openConnection())); - - URLConnection connection = server.getUrl("/").openConnection(); - connection.addRequestProperty("Cache-Control", "max-stale=180"); - assertEquals("A", readAscii(connection)); - assertEquals("110 HttpURLConnection \"Response is stale\"", - connection.getHeaderField("Warning")); - } - - public void testRequestMaxStaleNotHonoredWithMustRevalidate() throws IOException { - server.enqueue(new MockResponse().setBody("A") - .addHeader("Cache-Control: max-age=120, must-revalidate") - .addHeader("Date: " + formatDate(-4, TimeUnit.MINUTES))); - server.enqueue(new MockResponse().setBody("B")); - - server.play(); - assertEquals("A", readAscii(server.getUrl("/").openConnection())); - - URLConnection connection = server.getUrl("/").openConnection(); - connection.addRequestProperty("Cache-Control", "max-stale=180"); - assertEquals("B", readAscii(connection)); - } - - public void testRequestOnlyIfCachedWithNoResponseCached() throws IOException { - // (no responses enqueued) - server.play(); - - HttpURLConnection connection = (HttpURLConnection) server.getUrl("/").openConnection(); - connection.addRequestProperty("Cache-Control", "only-if-cached"); - assertGatewayTimeout(connection); - } - - public void testRequestOnlyIfCachedWithFullResponseCached() throws IOException { - server.enqueue(new MockResponse().setBody("A") - .addHeader("Cache-Control: max-age=30") - .addHeader("Date: " + formatDate(0, TimeUnit.MINUTES))); - server.play(); - - assertEquals("A", readAscii(server.getUrl("/").openConnection())); - URLConnection connection = server.getUrl("/").openConnection(); - connection.addRequestProperty("Cache-Control", "only-if-cached"); - assertEquals("A", readAscii(server.getUrl("/").openConnection())); - } - - public void testRequestOnlyIfCachedWithConditionalResponseCached() throws IOException { - server.enqueue(new MockResponse().setBody("A") - .addHeader("Cache-Control: max-age=30") - .addHeader("Date: " + formatDate(-1, TimeUnit.MINUTES))); - server.play(); - - assertEquals("A", readAscii(server.getUrl("/").openConnection())); - HttpURLConnection connection = (HttpURLConnection) server.getUrl("/").openConnection(); - connection.addRequestProperty("Cache-Control", "only-if-cached"); - assertGatewayTimeout(connection); - } - - public void testRequestOnlyIfCachedWithUnhelpfulResponseCached() throws IOException { - server.enqueue(new MockResponse().setBody("A")); - server.play(); - - assertEquals("A", readAscii(server.getUrl("/").openConnection())); - HttpURLConnection connection = (HttpURLConnection) server.getUrl("/").openConnection(); - connection.addRequestProperty("Cache-Control", "only-if-cached"); - assertGatewayTimeout(connection); - } - - public void testRequestCacheControlNoCache() throws Exception { - server.enqueue(new MockResponse() - .addHeader("Last-Modified: " + formatDate(-120, TimeUnit.SECONDS)) - .addHeader("Date: " + formatDate(0, TimeUnit.SECONDS)) - .addHeader("Cache-Control: max-age=60") - .setBody("A")); - server.enqueue(new MockResponse().setBody("B")); - server.play(); - - URL url = server.getUrl("/"); - assertEquals("A", readAscii(url.openConnection())); - URLConnection connection = url.openConnection(); - connection.setRequestProperty("Cache-Control", "no-cache"); - assertEquals("B", readAscii(connection)); - } - - public void testRequestPragmaNoCache() throws Exception { - server.enqueue(new MockResponse() - .addHeader("Last-Modified: " + formatDate(-120, TimeUnit.SECONDS)) - .addHeader("Date: " + formatDate(0, TimeUnit.SECONDS)) - .addHeader("Cache-Control: max-age=60") - .setBody("A")); - server.enqueue(new MockResponse().setBody("B")); - server.play(); - - URL url = server.getUrl("/"); - assertEquals("A", readAscii(url.openConnection())); - URLConnection connection = url.openConnection(); - connection.setRequestProperty("Pragma", "no-cache"); - assertEquals("B", readAscii(connection)); - } - - public void testClientSuppliedIfModifiedSinceWithCachedResult() throws Exception { - MockResponse response = new MockResponse() - .addHeader("ETag: v3") - .addHeader("Cache-Control: max-age=0"); - String ifModifiedSinceDate = formatDate(-24, TimeUnit.HOURS); - RecordedRequest request = assertClientSuppliedCondition( - response, "If-Modified-Since", ifModifiedSinceDate); - List<String> headers = request.getHeaders(); - assertTrue(headers.contains("If-Modified-Since: " + ifModifiedSinceDate)); - assertFalse(headers.contains("If-None-Match: v3")); - } - - public void testClientSuppliedIfNoneMatchSinceWithCachedResult() throws Exception { - String lastModifiedDate = formatDate(-3, TimeUnit.MINUTES); - MockResponse response = new MockResponse() - .addHeader("Last-Modified: " + lastModifiedDate) - .addHeader("Date: " + formatDate(-2, TimeUnit.MINUTES)) - .addHeader("Cache-Control: max-age=0"); - RecordedRequest request = assertClientSuppliedCondition( - response, "If-None-Match", "v1"); - List<String> headers = request.getHeaders(); - assertTrue(headers.contains("If-None-Match: v1")); - assertFalse(headers.contains("If-Modified-Since: " + lastModifiedDate)); - } - - private RecordedRequest assertClientSuppliedCondition(MockResponse seed, String conditionName, - String conditionValue) throws Exception { - server.enqueue(seed.setBody("A")); - server.enqueue(new MockResponse().setResponseCode(HttpURLConnection.HTTP_NOT_MODIFIED)); - server.play(); - - URL url = server.getUrl("/"); - assertEquals("A", readAscii(url.openConnection())); - - HttpURLConnection connection = (HttpURLConnection) url.openConnection(); - connection.addRequestProperty(conditionName, conditionValue); - assertEquals(HttpURLConnection.HTTP_NOT_MODIFIED, connection.getResponseCode()); - assertEquals("", readAscii(connection)); - - server.takeRequest(); // seed - return server.takeRequest(); - } - - public void testSetIfModifiedSince() throws Exception { - Date since = new Date(); - server.enqueue(new MockResponse().setBody("A")); - server.play(); - - URL url = server.getUrl("/"); - URLConnection connection = url.openConnection(); - connection.setIfModifiedSince(since.getTime()); - assertEquals("A", readAscii(connection)); - RecordedRequest request = server.takeRequest(); - assertTrue(request.getHeaders().contains("If-Modified-Since: " + formatDate(since))); - } - - public void testClientSuppliedConditionWithoutCachedResult() throws Exception { - server.enqueue(new MockResponse() - .setResponseCode(HttpURLConnection.HTTP_NOT_MODIFIED)); - server.play(); - - HttpURLConnection connection = (HttpURLConnection) server.getUrl("/").openConnection(); - String clientIfModifiedSince = formatDate(-24, TimeUnit.HOURS); - connection.addRequestProperty("If-Modified-Since", clientIfModifiedSince); - assertEquals(HttpURLConnection.HTTP_NOT_MODIFIED, connection.getResponseCode()); - assertEquals("", readAscii(connection)); - } - - public void testAuthorizationRequestHeaderPreventsCaching() throws Exception { - server.enqueue(new MockResponse() - .addHeader("Last-Modified: " + formatDate(-2, TimeUnit.MINUTES)) - .addHeader("Cache-Control: max-age=60") - .setBody("A")); - server.enqueue(new MockResponse().setBody("B")); - server.play(); - - URL url = server.getUrl("/"); - URLConnection connection = url.openConnection(); - connection.addRequestProperty("Authorization", "password"); - assertEquals("A", readAscii(connection)); - assertEquals("B", readAscii(url.openConnection())); - } - - public void testAuthorizationResponseCachedWithSMaxAge() throws Exception { - assertAuthorizationRequestFullyCached(new MockResponse() - .addHeader("Cache-Control: s-maxage=60")); - } - - public void testAuthorizationResponseCachedWithPublic() throws Exception { - assertAuthorizationRequestFullyCached(new MockResponse() - .addHeader("Cache-Control: public")); - } - - public void testAuthorizationResponseCachedWithMustRevalidate() throws Exception { - assertAuthorizationRequestFullyCached(new MockResponse() - .addHeader("Cache-Control: must-revalidate")); - } - - public void assertAuthorizationRequestFullyCached(MockResponse response) throws Exception { - server.enqueue(response - .addHeader("Cache-Control: max-age=60") - .setBody("A")); - server.enqueue(new MockResponse().setBody("B")); - server.play(); - - URL url = server.getUrl("/"); - URLConnection connection = url.openConnection(); - connection.addRequestProperty("Authorization", "password"); - assertEquals("A", readAscii(connection)); - assertEquals("A", readAscii(url.openConnection())); - } - - public void testContentLocationDoesNotPopulateCache() throws Exception { - server.enqueue(new MockResponse() - .addHeader("Cache-Control: max-age=60") - .addHeader("Content-Location: /bar") - .setBody("A")); - server.enqueue(new MockResponse().setBody("B")); - server.play(); - - assertEquals("A", readAscii(server.getUrl("/foo").openConnection())); - assertEquals("B", readAscii(server.getUrl("/bar").openConnection())); - } - - public void testUseCachesFalseDoesNotWriteToCache() throws Exception { - server.enqueue(new MockResponse() - .addHeader("Cache-Control: max-age=60") - .setBody("A").setBody("A")); - server.enqueue(new MockResponse().setBody("B")); - server.play(); - - URLConnection connection = server.getUrl("/").openConnection(); - connection.setUseCaches(false); - assertEquals("A", readAscii(connection)); - assertEquals("B", readAscii(server.getUrl("/").openConnection())); - } - - public void testUseCachesFalseDoesNotReadFromCache() throws Exception { - server.enqueue(new MockResponse() - .addHeader("Cache-Control: max-age=60") - .setBody("A").setBody("A")); - server.enqueue(new MockResponse().setBody("B")); - server.play(); - - assertEquals("A", readAscii(server.getUrl("/").openConnection())); - URLConnection connection = server.getUrl("/").openConnection(); - connection.setUseCaches(false); - assertEquals("B", readAscii(connection)); - } - - public void testDefaultUseCachesSetsInitialValueOnly() throws Exception { - URL url = new URL("http://localhost/"); - URLConnection c1 = url.openConnection(); - URLConnection c2 = url.openConnection(); - assertTrue(c1.getDefaultUseCaches()); - c1.setDefaultUseCaches(false); - try { - assertTrue(c1.getUseCaches()); - assertTrue(c2.getUseCaches()); - URLConnection c3 = url.openConnection(); - assertFalse(c3.getUseCaches()); - } finally { - c1.setDefaultUseCaches(true); - } - } - - public void testConnectionIsReturnedToPoolAfterConditionalSuccess() throws Exception { - server.enqueue(new MockResponse() - .addHeader("Last-Modified: " + formatDate(-1, TimeUnit.HOURS)) - .addHeader("Cache-Control: max-age=0") - .setBody("A")); - server.enqueue(new MockResponse().setResponseCode(HttpURLConnection.HTTP_NOT_MODIFIED)); - server.enqueue(new MockResponse().setBody("B")); - server.play(); - - assertEquals("A", readAscii(server.getUrl("/a").openConnection())); - assertEquals("A", readAscii(server.getUrl("/a").openConnection())); - assertEquals("B", readAscii(server.getUrl("/b").openConnection())); - - assertEquals(0, server.takeRequest().getSequenceNumber()); - assertEquals(1, server.takeRequest().getSequenceNumber()); - assertEquals(2, server.takeRequest().getSequenceNumber()); - } - - public void testStatisticsConditionalCacheMiss() throws Exception { - server.enqueue(new MockResponse() - .addHeader("Last-Modified: " + formatDate(-1, TimeUnit.HOURS)) - .addHeader("Cache-Control: max-age=0") - .setBody("A")); - server.enqueue(new MockResponse().setBody("B")); - server.enqueue(new MockResponse().setBody("C")); - server.play(); - - assertEquals("A", readAscii(server.getUrl("/").openConnection())); - assertEquals(1, cache.getRequestCount()); - assertEquals(1, cache.getNetworkCount()); - assertEquals(0, cache.getHitCount()); - assertEquals("B", readAscii(server.getUrl("/").openConnection())); - assertEquals("C", readAscii(server.getUrl("/").openConnection())); - assertEquals(3, cache.getRequestCount()); - assertEquals(3, cache.getNetworkCount()); - assertEquals(0, cache.getHitCount()); - } - - public void testStatisticsConditionalCacheHit() throws Exception { - server.enqueue(new MockResponse() - .addHeader("Last-Modified: " + formatDate(-1, TimeUnit.HOURS)) - .addHeader("Cache-Control: max-age=0") - .setBody("A")); - server.enqueue(new MockResponse().setResponseCode(HttpURLConnection.HTTP_NOT_MODIFIED)); - server.enqueue(new MockResponse().setResponseCode(HttpURLConnection.HTTP_NOT_MODIFIED)); - server.play(); - - assertEquals("A", readAscii(server.getUrl("/").openConnection())); - assertEquals(1, cache.getRequestCount()); - assertEquals(1, cache.getNetworkCount()); - assertEquals(0, cache.getHitCount()); - assertEquals("A", readAscii(server.getUrl("/").openConnection())); - assertEquals("A", readAscii(server.getUrl("/").openConnection())); - assertEquals(3, cache.getRequestCount()); - assertEquals(3, cache.getNetworkCount()); - assertEquals(2, cache.getHitCount()); - } - - public void testStatisticsFullCacheHit() throws Exception { - server.enqueue(new MockResponse() - .addHeader("Cache-Control: max-age=60") - .setBody("A")); - server.play(); - - assertEquals("A", readAscii(server.getUrl("/").openConnection())); - assertEquals(1, cache.getRequestCount()); - assertEquals(1, cache.getNetworkCount()); - assertEquals(0, cache.getHitCount()); - assertEquals("A", readAscii(server.getUrl("/").openConnection())); - assertEquals("A", readAscii(server.getUrl("/").openConnection())); - assertEquals(3, cache.getRequestCount()); - assertEquals(1, cache.getNetworkCount()); - assertEquals(2, cache.getHitCount()); - } - - public void testVaryMatchesChangedRequestHeaderField() throws Exception { - server.enqueue(new MockResponse() - .addHeader("Cache-Control: max-age=60") - .addHeader("Vary: Accept-Language") - .setBody("A")); - server.enqueue(new MockResponse().setBody("B")); - server.play(); - - URL url = server.getUrl("/"); - HttpURLConnection frConnection = (HttpURLConnection) url.openConnection(); - frConnection.addRequestProperty("Accept-Language", "fr-CA"); - assertEquals("A", readAscii(frConnection)); - - HttpURLConnection enConnection = (HttpURLConnection) url.openConnection(); - enConnection.addRequestProperty("Accept-Language", "en-US"); - assertEquals("B", readAscii(enConnection)); - } - - public void testVaryMatchesUnchangedRequestHeaderField() throws Exception { - server.enqueue(new MockResponse() - .addHeader("Cache-Control: max-age=60") - .addHeader("Vary: Accept-Language") - .setBody("A")); - server.enqueue(new MockResponse().setBody("B")); - server.play(); - - URL url = server.getUrl("/"); - URLConnection connection1 = url.openConnection(); - connection1.addRequestProperty("Accept-Language", "fr-CA"); - assertEquals("A", readAscii(connection1)); - URLConnection connection2 = url.openConnection(); - connection2.addRequestProperty("Accept-Language", "fr-CA"); - assertEquals("A", readAscii(connection2)); - } - - public void testVaryMatchesAbsentRequestHeaderField() throws Exception { - server.enqueue(new MockResponse() - .addHeader("Cache-Control: max-age=60") - .addHeader("Vary: Foo") - .setBody("A")); - server.enqueue(new MockResponse().setBody("B")); - server.play(); - - assertEquals("A", readAscii(server.getUrl("/").openConnection())); - assertEquals("A", readAscii(server.getUrl("/").openConnection())); - } - - public void testVaryMatchesAddedRequestHeaderField() throws Exception { - server.enqueue(new MockResponse() - .addHeader("Cache-Control: max-age=60") - .addHeader("Vary: Foo") - .setBody("A")); - server.enqueue(new MockResponse().setBody("B")); - server.play(); - - assertEquals("A", readAscii(server.getUrl("/").openConnection())); - URLConnection fooConnection = server.getUrl("/").openConnection(); - fooConnection.addRequestProperty("Foo", "bar"); - assertEquals("B", readAscii(fooConnection)); - } - - public void testVaryMatchesRemovedRequestHeaderField() throws Exception { - server.enqueue(new MockResponse() - .addHeader("Cache-Control: max-age=60") - .addHeader("Vary: Foo") - .setBody("A")); - server.enqueue(new MockResponse().setBody("B")); - server.play(); - - URLConnection fooConnection = server.getUrl("/").openConnection(); - fooConnection.addRequestProperty("Foo", "bar"); - assertEquals("A", readAscii(fooConnection)); - assertEquals("B", readAscii(server.getUrl("/").openConnection())); - } - - public void testVaryFieldsAreCaseInsensitive() throws Exception { - server.enqueue(new MockResponse() - .addHeader("Cache-Control: max-age=60") - .addHeader("Vary: ACCEPT-LANGUAGE") - .setBody("A")); - server.enqueue(new MockResponse().setBody("B")); - server.play(); - - URL url = server.getUrl("/"); - URLConnection connection1 = url.openConnection(); - connection1.addRequestProperty("Accept-Language", "fr-CA"); - assertEquals("A", readAscii(connection1)); - URLConnection connection2 = url.openConnection(); - connection2.addRequestProperty("accept-language", "fr-CA"); - assertEquals("A", readAscii(connection2)); - } - - public void testVaryMultipleFieldsWithMatch() throws Exception { - server.enqueue(new MockResponse() - .addHeader("Cache-Control: max-age=60") - .addHeader("Vary: Accept-Language, Accept-Charset") - .addHeader("Vary: Accept-Encoding") - .setBody("A")); - server.enqueue(new MockResponse().setBody("B")); - server.play(); - - URL url = server.getUrl("/"); - URLConnection connection1 = url.openConnection(); - connection1.addRequestProperty("Accept-Language", "fr-CA"); - connection1.addRequestProperty("Accept-Charset", "UTF-8"); - connection1.addRequestProperty("Accept-Encoding", "identity"); - assertEquals("A", readAscii(connection1)); - URLConnection connection2 = url.openConnection(); - connection2.addRequestProperty("Accept-Language", "fr-CA"); - connection2.addRequestProperty("Accept-Charset", "UTF-8"); - connection2.addRequestProperty("Accept-Encoding", "identity"); - assertEquals("A", readAscii(connection2)); - } - - public void testVaryMultipleFieldsWithNoMatch() throws Exception { - server.enqueue(new MockResponse() - .addHeader("Cache-Control: max-age=60") - .addHeader("Vary: Accept-Language, Accept-Charset") - .addHeader("Vary: Accept-Encoding") - .setBody("A")); - server.enqueue(new MockResponse().setBody("B")); - server.play(); - - URL url = server.getUrl("/"); - URLConnection frConnection = url.openConnection(); - frConnection.addRequestProperty("Accept-Language", "fr-CA"); - frConnection.addRequestProperty("Accept-Charset", "UTF-8"); - frConnection.addRequestProperty("Accept-Encoding", "identity"); - assertEquals("A", readAscii(frConnection)); - URLConnection enConnection = url.openConnection(); - enConnection.addRequestProperty("Accept-Language", "en-CA"); - enConnection.addRequestProperty("Accept-Charset", "UTF-8"); - enConnection.addRequestProperty("Accept-Encoding", "identity"); - assertEquals("B", readAscii(enConnection)); - } - - public void testVaryMultipleFieldValuesWithMatch() throws Exception { - server.enqueue(new MockResponse() - .addHeader("Cache-Control: max-age=60") - .addHeader("Vary: Accept-Language") - .setBody("A")); - server.enqueue(new MockResponse().setBody("B")); - server.play(); - - URL url = server.getUrl("/"); - URLConnection connection1 = url.openConnection(); - connection1.addRequestProperty("Accept-Language", "fr-CA, fr-FR"); - connection1.addRequestProperty("Accept-Language", "en-US"); - assertEquals("A", readAscii(connection1)); - - URLConnection connection2 = url.openConnection(); - connection2.addRequestProperty("Accept-Language", "fr-CA, fr-FR"); - connection2.addRequestProperty("Accept-Language", "en-US"); - assertEquals("A", readAscii(connection2)); - } - - public void testVaryMultipleFieldValuesWithNoMatch() throws Exception { - server.enqueue(new MockResponse() - .addHeader("Cache-Control: max-age=60") - .addHeader("Vary: Accept-Language") - .setBody("A")); - server.enqueue(new MockResponse().setBody("B")); - server.play(); - - URL url = server.getUrl("/"); - URLConnection connection1 = url.openConnection(); - connection1.addRequestProperty("Accept-Language", "fr-CA, fr-FR"); - connection1.addRequestProperty("Accept-Language", "en-US"); - assertEquals("A", readAscii(connection1)); - - URLConnection connection2 = url.openConnection(); - connection2.addRequestProperty("Accept-Language", "fr-CA"); - connection2.addRequestProperty("Accept-Language", "en-US"); - assertEquals("B", readAscii(connection2)); - } - - public void testVaryAsterisk() throws Exception { - server.enqueue(new MockResponse() - .addHeader("Cache-Control: max-age=60") - .addHeader("Vary: *") - .setBody("A")); - server.enqueue(new MockResponse().setBody("B")); - server.play(); - - assertEquals("A", readAscii(server.getUrl("/").openConnection())); - assertEquals("B", readAscii(server.getUrl("/").openConnection())); - } - - public void testVaryAndHttps() throws Exception { - TestSSLContext testSSLContext = TestSSLContext.create(); - server.useHttps(testSSLContext.serverContext.getSocketFactory(), false); - server.enqueue(new MockResponse() - .addHeader("Cache-Control: max-age=60") - .addHeader("Vary: Accept-Language") - .setBody("A")); - server.enqueue(new MockResponse().setBody("B")); - server.play(); - - URL url = server.getUrl("/"); - HttpsURLConnection connection1 = (HttpsURLConnection) url.openConnection(); - connection1.setSSLSocketFactory(testSSLContext.clientContext.getSocketFactory()); - connection1.addRequestProperty("Accept-Language", "en-US"); - assertEquals("A", readAscii(connection1)); - - HttpsURLConnection connection2 = (HttpsURLConnection) url.openConnection(); - connection2.setSSLSocketFactory(testSSLContext.clientContext.getSocketFactory()); - connection2.addRequestProperty("Accept-Language", "en-US"); - assertEquals("A", readAscii(connection2)); - } - - public void testDiskWriteFailureCacheDegradation() throws Exception { - Deque<InvocationHandler> writeHandlers = mockOs.getHandlers("write"); - int i = 0; - boolean hasMoreScenarios = true; - while (hasMoreScenarios) { - mockOs.enqueueNormal("write", i++); - mockOs.enqueueFault("write"); - exercisePossiblyFaultyCache(false); - hasMoreScenarios = writeHandlers.isEmpty(); - writeHandlers.clear(); - } - System.out.println("Exercising the cache performs " + (i - 1) + " writes."); - } - - public void testDiskReadFailureCacheDegradation() throws Exception { - Deque<InvocationHandler> readHandlers = mockOs.getHandlers("read"); - int i = 0; - boolean hasMoreScenarios = true; - while (hasMoreScenarios) { - mockOs.enqueueNormal("read", i++); - mockOs.enqueueFault("read"); - exercisePossiblyFaultyCache(true); - hasMoreScenarios = readHandlers.isEmpty(); - readHandlers.clear(); - } - System.out.println("Exercising the cache performs " + (i - 1) + " reads."); - } - - public void testCachePlusCookies() throws Exception { - server.enqueue(new MockResponse() - .addHeader("Set-Cookie: a=FIRST; domain=" + server.getCookieDomain() + ";") - .addHeader("Last-Modified: " + formatDate(-1, TimeUnit.HOURS)) - .addHeader("Cache-Control: max-age=0") - .setBody("A")); - server.enqueue(new MockResponse() - .addHeader("Set-Cookie: a=SECOND; domain=" + server.getCookieDomain() + ";") - .setResponseCode(HttpURLConnection.HTTP_NOT_MODIFIED)); - server.play(); - - URL url = server.getUrl("/"); - assertEquals("A", readAscii(url.openConnection())); - assertCookies(url, "a=FIRST"); - assertEquals("A", readAscii(url.openConnection())); - assertCookies(url, "a=SECOND"); - } - - public void testGetHeadersReturnsNetworkEndToEndHeaders() throws Exception { - server.enqueue(new MockResponse() - .addHeader("Allow: GET, HEAD") - .addHeader("Last-Modified: " + formatDate(-1, TimeUnit.HOURS)) - .addHeader("Cache-Control: max-age=0") - .setBody("A")); - server.enqueue(new MockResponse() - .addHeader("Allow: GET, HEAD, PUT") - .setResponseCode(HttpURLConnection.HTTP_NOT_MODIFIED)); - server.play(); - - URLConnection connection1 = server.getUrl("/").openConnection(); - assertEquals("A", readAscii(connection1)); - assertEquals("GET, HEAD", connection1.getHeaderField("Allow")); - - URLConnection connection2 = server.getUrl("/").openConnection(); - assertEquals("A", readAscii(connection2)); - assertEquals("GET, HEAD, PUT", connection2.getHeaderField("Allow")); - } - - public void testGetHeadersReturnsCachedHopByHopHeaders() throws Exception { - server.enqueue(new MockResponse() - .addHeader("Transfer-Encoding: identity") - .addHeader("Last-Modified: " + formatDate(-1, TimeUnit.HOURS)) - .addHeader("Cache-Control: max-age=0") - .setBody("A")); - server.enqueue(new MockResponse() - .addHeader("Transfer-Encoding: none") - .setResponseCode(HttpURLConnection.HTTP_NOT_MODIFIED)); - server.play(); - - URLConnection connection1 = server.getUrl("/").openConnection(); - assertEquals("A", readAscii(connection1)); - assertEquals("identity", connection1.getHeaderField("Transfer-Encoding")); - - URLConnection connection2 = server.getUrl("/").openConnection(); - assertEquals("A", readAscii(connection2)); - assertEquals("identity", connection2.getHeaderField("Transfer-Encoding")); - } - - public void testGetHeadersDeletesCached100LevelWarnings() throws Exception { - server.enqueue(new MockResponse() - .addHeader("Warning: 199 test danger") - .addHeader("Last-Modified: " + formatDate(-1, TimeUnit.HOURS)) - .addHeader("Cache-Control: max-age=0") - .setBody("A")); - server.enqueue(new MockResponse() - .setResponseCode(HttpURLConnection.HTTP_NOT_MODIFIED)); - server.play(); - - URLConnection connection1 = server.getUrl("/").openConnection(); - assertEquals("A", readAscii(connection1)); - assertEquals("199 test danger", connection1.getHeaderField("Warning")); - - URLConnection connection2 = server.getUrl("/").openConnection(); - assertEquals("A", readAscii(connection2)); - assertEquals(null, connection2.getHeaderField("Warning")); - } - - public void testGetHeadersRetainsCached200LevelWarnings() throws Exception { - server.enqueue(new MockResponse() - .addHeader("Warning: 299 test danger") - .addHeader("Last-Modified: " + formatDate(-1, TimeUnit.HOURS)) - .addHeader("Cache-Control: max-age=0") - .setBody("A")); - server.enqueue(new MockResponse() - .setResponseCode(HttpURLConnection.HTTP_NOT_MODIFIED)); - server.play(); - - URLConnection connection1 = server.getUrl("/").openConnection(); - assertEquals("A", readAscii(connection1)); - assertEquals("299 test danger", connection1.getHeaderField("Warning")); - - URLConnection connection2 = server.getUrl("/").openConnection(); - assertEquals("A", readAscii(connection2)); - assertEquals("299 test danger", connection2.getHeaderField("Warning")); - } - - public void assertCookies(URL url, String... expectedCookies) throws Exception { - List<String> actualCookies = new ArrayList<String>(); - for (HttpCookie cookie : cookieManager.getCookieStore().get(url.toURI())) { - actualCookies.add(cookie.toString()); - } - assertEquals(Arrays.asList(expectedCookies), actualCookies); - } - - public void testCachePlusRange() throws Exception { - assertNotCached(new MockResponse() - .setResponseCode(HttpURLConnection.HTTP_PARTIAL) - .addHeader("Date: " + formatDate(0, TimeUnit.HOURS)) - .addHeader("Content-Range: bytes 100-100/200") - .addHeader("Cache-Control: max-age=60")); - } - - public void testConditionalHitUpdatesCache() throws Exception { - server.enqueue(new MockResponse() - .addHeader("Last-Modified: " + formatDate(0, TimeUnit.SECONDS)) - .addHeader("Cache-Control: max-age=0") - .setBody("A")); - server.enqueue(new MockResponse() - .addHeader("Cache-Control: max-age=30") - .addHeader("Allow: GET, HEAD") - .setResponseCode(HttpURLConnection.HTTP_NOT_MODIFIED)); - server.enqueue(new MockResponse().setBody("B")); - server.play(); - - // cache miss; seed the cache - HttpURLConnection connection1 = (HttpURLConnection) server.getUrl("/a").openConnection(); - assertEquals("A", readAscii(connection1)); - assertEquals(null, connection1.getHeaderField("Allow")); - - // conditional cache hit; update the cache - HttpURLConnection connection2 = (HttpURLConnection) server.getUrl("/a").openConnection(); - assertEquals(HttpURLConnection.HTTP_OK, connection2.getResponseCode()); - assertEquals("A", readAscii(connection2)); - assertEquals("GET, HEAD", connection2.getHeaderField("Allow")); - - // full cache hit - HttpURLConnection connection3 = (HttpURLConnection) server.getUrl("/a").openConnection(); - assertEquals("A", readAscii(connection3)); - assertEquals("GET, HEAD", connection3.getHeaderField("Allow")); - - assertEquals(2, server.getRequestCount()); - } - - /** - * @param delta the offset from the current date to use. Negative - * values yield dates in the past; positive values yield dates in the - * future. - */ - private String formatDate(long delta, TimeUnit timeUnit) { - return formatDate(new Date(System.currentTimeMillis() + timeUnit.toMillis(delta))); - } - - private String formatDate(Date date) { - DateFormat rfc1123 = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss zzz", Locale.US); - rfc1123.setTimeZone(TimeZone.getTimeZone("UTC")); - return rfc1123.format(date); - } - - private void addRequestBodyIfNecessary(String requestMethod, HttpURLConnection invalidate) - throws IOException { - if (requestMethod.equals("POST") || requestMethod.equals("PUT")) { - invalidate.setDoOutput(true); - OutputStream requestBody = invalidate.getOutputStream(); - requestBody.write('x'); - requestBody.close(); - } - } - - private void assertNotCached(MockResponse response) throws Exception { - server.enqueue(response.setBody("A")); - server.enqueue(new MockResponse().setBody("B")); - server.play(); - - URL url = server.getUrl("/"); - assertEquals("A", readAscii(url.openConnection())); - assertEquals("B", readAscii(url.openConnection())); - } - - private void exercisePossiblyFaultyCache(boolean permitReadBodyFailures) throws Exception { - server.shutdown(); - server = new MockWebServer(); - server.enqueue(new MockResponse() - .addHeader("Cache-Control: max-age=60") - .setBody("A")); - server.enqueue(new MockResponse().setBody("B")); - server.play(); - - URL url = server.getUrl("/" + UUID.randomUUID()); - assertEquals("A", readAscii(url.openConnection())); - - URLConnection connection = url.openConnection(); - InputStream in = connection.getInputStream(); - try { - int bodyChar = in.read(); - assertTrue(bodyChar == 'A' || bodyChar == 'B'); - assertEquals(-1, in.read()); - } catch (IOException e) { - if (!permitReadBodyFailures) { - throw e; - } - } - } - - /** - * @return the request with the conditional get headers. - */ - private RecordedRequest assertConditionallyCached(MockResponse response) throws Exception { - // scenario 1: condition succeeds - server.enqueue(response.setBody("A").setStatus("HTTP/1.1 200 A-OK")); - server.enqueue(new MockResponse().setResponseCode(HttpURLConnection.HTTP_NOT_MODIFIED)); - - // scenario 2: condition fails - server.enqueue(response.setBody("B").setStatus("HTTP/1.1 200 B-OK")); - server.enqueue(new MockResponse().setStatus("HTTP/1.1 200 C-OK").setBody("C")); - - server.play(); - - URL valid = server.getUrl("/valid"); - HttpURLConnection connection1 = (HttpURLConnection) valid.openConnection(); - assertEquals("A", readAscii(connection1)); - assertEquals(HttpURLConnection.HTTP_OK, connection1.getResponseCode()); - assertEquals("A-OK", connection1.getResponseMessage()); - HttpURLConnection connection2 = (HttpURLConnection) valid.openConnection(); - assertEquals("A", readAscii(connection2)); - assertEquals(HttpURLConnection.HTTP_OK, connection2.getResponseCode()); - assertEquals("A-OK", connection2.getResponseMessage()); - - URL invalid = server.getUrl("/invalid"); - HttpURLConnection connection3 = (HttpURLConnection) invalid.openConnection(); - assertEquals("B", readAscii(connection3)); - assertEquals(HttpURLConnection.HTTP_OK, connection3.getResponseCode()); - assertEquals("B-OK", connection3.getResponseMessage()); - HttpURLConnection connection4 = (HttpURLConnection) invalid.openConnection(); - assertEquals("C", readAscii(connection4)); - assertEquals(HttpURLConnection.HTTP_OK, connection4.getResponseCode()); - assertEquals("C-OK", connection4.getResponseMessage()); - - server.takeRequest(); // regular get - return server.takeRequest(); // conditional get - } - - private void assertFullyCached(MockResponse response) throws Exception { - server.enqueue(response.setBody("A")); - server.enqueue(response.setBody("B")); - server.play(); - - URL url = server.getUrl("/"); - assertEquals("A", readAscii(url.openConnection())); - assertEquals("A", readAscii(url.openConnection())); - } - - /** - * Shortens the body of {@code response} but not the corresponding headers. - * Only useful to test how clients respond to the premature conclusion of - * the HTTP body. - */ - private MockResponse truncateViolently(MockResponse response, int numBytesToKeep) { - response.setSocketPolicy(DISCONNECT_AT_END); - List<String> headers = new ArrayList<String>(response.getHeaders()); - response.setBody(Arrays.copyOfRange(response.getBody(), 0, numBytesToKeep)); - response.getHeaders().clear(); - response.getHeaders().addAll(headers); - return response; - } - - /** - * Reads {@code count} characters from the stream. If the stream is - * exhausted before {@code count} characters can be read, the remaining - * characters are returned and the stream is closed. - */ - private String readAscii(URLConnection connection, int count) throws IOException { - HttpURLConnection httpConnection = (HttpURLConnection) connection; - InputStream in = httpConnection.getResponseCode() < HttpURLConnection.HTTP_BAD_REQUEST - ? connection.getInputStream() - : httpConnection.getErrorStream(); - StringBuilder result = new StringBuilder(); - for (int i = 0; i < count; i++) { - int value = in.read(); - if (value == -1) { - in.close(); - break; - } - result.append((char) value); - } - return result.toString(); - } - - private String readAscii(URLConnection connection) throws IOException { - return readAscii(connection, Integer.MAX_VALUE); - } - - private void reliableSkip(InputStream in, int length) throws IOException { - while (length > 0) { - length -= in.skip(length); - } - } - - private void assertGatewayTimeout(HttpURLConnection connection) throws IOException { - try { - connection.getInputStream(); - fail(); - } catch (FileNotFoundException expected) { - } - assertEquals(504, connection.getResponseCode()); - assertEquals(-1, connection.getErrorStream().read()); - } - - enum TransferKind { - CHUNKED() { - @Override void setBody(MockResponse response, byte[] content, int chunkSize) - throws IOException { - response.setChunkedBody(content, chunkSize); - } - }, - FIXED_LENGTH() { - @Override void setBody(MockResponse response, byte[] content, int chunkSize) { - response.setBody(content); - } - }, - END_OF_STREAM() { - @Override void setBody(MockResponse response, byte[] content, int chunkSize) { - response.setBody(content); - response.setSocketPolicy(DISCONNECT_AT_END); - for (Iterator<String> h = response.getHeaders().iterator(); h.hasNext(); ) { - if (h.next().startsWith("Content-Length:")) { - h.remove(); - break; - } - } - } - }; - - abstract void setBody(MockResponse response, byte[] content, int chunkSize) - throws IOException; - - void setBody(MockResponse response, String content, int chunkSize) throws IOException { - setBody(response, content.getBytes("UTF-8"), chunkSize); - } - } - - private <T> List<T> toListOrNull(T[] arrayOrNull) { - return arrayOrNull != null ? Arrays.asList(arrayOrNull) : null; - } - - /** - * Returns a gzipped copy of {@code bytes}. - */ - public byte[] gzip(byte[] bytes) throws IOException { - ByteArrayOutputStream bytesOut = new ByteArrayOutputStream(); - OutputStream gzippedOut = new GZIPOutputStream(bytesOut); - gzippedOut.write(bytes); - gzippedOut.close(); - return bytesOut.toByteArray(); - } - - private class InsecureResponseCache extends ResponseCache { - @Override public CacheRequest put(URI uri, URLConnection connection) throws IOException { - return cache.put(uri, connection); - } - - @Override public CacheResponse get(URI uri, String requestMethod, - Map<String, List<String>> requestHeaders) throws IOException { - final CacheResponse response = cache.get(uri, requestMethod, requestHeaders); - if (response instanceof SecureCacheResponse) { - return new CacheResponse() { - @Override public InputStream getBody() throws IOException { - return response.getBody(); - } - @Override public Map<String, List<String>> getHeaders() throws IOException { - return response.getHeaders(); - } - }; - } - return response; - } - } -} diff --git a/luni/src/test/java/libcore/net/http/ParsedHeadersTest.java b/luni/src/test/java/libcore/net/http/ParsedHeadersTest.java deleted file mode 100644 index d7ba441..0000000 --- a/luni/src/test/java/libcore/net/http/ParsedHeadersTest.java +++ /dev/null @@ -1,244 +0,0 @@ -/* -* 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.Arrays; -import java.util.Collections; -import java.util.Date; -import junit.framework.TestCase; - -public final class ParsedHeadersTest extends TestCase { - - private URI uri; - - @Override protected void setUp() throws Exception { - super.setUp(); - uri = new URI("http", "localhost", "/"); - } - - public void testUpperCaseHttpHeaders() { - RawHeaders headers = new RawHeaders(); - headers.add("CACHE-CONTROL", "no-store"); - headers.add("DATE", "Thu, 01 Jan 1970 00:00:01 UTC"); - headers.add("EXPIRES", "Thu, 01 Jan 1970 00:00:02 UTC"); - headers.add("LAST-MODIFIED", "Thu, 01 Jan 1970 00:00:03 UTC"); - headers.add("ETAG", "v1"); - headers.add("PRAGMA", "no-cache"); - ResponseHeaders parsedHeaders = new ResponseHeaders(uri, headers); - assertTrue(parsedHeaders.isNoStore()); - assertEquals(new Date(1000), parsedHeaders.getServedDate()); - assertEquals(new Date(2000), parsedHeaders.getExpires()); - assertEquals(new Date(3000), parsedHeaders.getLastModified()); - assertEquals("v1", parsedHeaders.getEtag()); - assertTrue(parsedHeaders.isNoCache()); - } - - public void testCommaSeparatedCacheControlHeaders() { - RawHeaders headers = new RawHeaders(); - headers.add("Cache-Control", "no-store, max-age=60, public"); - ResponseHeaders parsedHeaders = new ResponseHeaders(uri, headers); - assertTrue(parsedHeaders.isNoStore()); - assertEquals(60, parsedHeaders.getMaxAgeSeconds()); - assertTrue(parsedHeaders.isPublic()); - } - - public void testQuotedFieldName() { - RawHeaders headers = new RawHeaders(); - headers.add("Cache-Control", "private=\"Set-Cookie\", no-store"); - ResponseHeaders parsedHeaders = new ResponseHeaders(uri, headers); - assertTrue(parsedHeaders.isNoStore()); - } - - public void testUnquotedValue() { - RawHeaders headers = new RawHeaders(); - headers.add("Cache-Control", "private=Set-Cookie, no-store"); - ResponseHeaders parsedHeaders = new ResponseHeaders(uri, headers); - assertTrue(parsedHeaders.isNoStore()); - } - - public void testQuotedValue() { - RawHeaders headers = new RawHeaders(); - headers.add("Cache-Control", "private=\" a, no-cache, c \", no-store"); - ResponseHeaders parsedHeaders = new ResponseHeaders(uri, headers); - assertTrue(parsedHeaders.isNoStore()); - assertFalse(parsedHeaders.isNoCache()); - } - - public void testDanglingQuote() { - RawHeaders headers = new RawHeaders(); - headers.add("Cache-Control", "private=\"a, no-cache, c"); - ResponseHeaders parsedHeaders = new ResponseHeaders(uri, headers); - assertFalse(parsedHeaders.isNoCache()); - } - - public void testTrailingComma() { - RawHeaders headers = new RawHeaders(); - headers.add("Cache-Control", "public,"); - ResponseHeaders parsedHeaders = new ResponseHeaders(uri, headers); - assertTrue(parsedHeaders.isPublic()); - } - - public void testTrailingEquals() { - RawHeaders headers = new RawHeaders(); - headers.add("Cache-Control", "private="); - new ResponseHeaders(uri, headers); - } - - public void testSpaceBeforeEquals() { - RawHeaders headers = new RawHeaders(); - headers.add("Cache-Control", "max-age =60"); - RequestHeaders parsedHeaders = new RequestHeaders(uri, headers); - assertEquals(60, parsedHeaders.getMaxAgeSeconds()); - } - - public void testSpaceAfterEqualsWithQuotes() { - RawHeaders headers = new RawHeaders(); - headers.add("Cache-Control", "max-age= \"60\""); - RequestHeaders parsedHeaders = new RequestHeaders(uri, headers); - assertEquals(60, parsedHeaders.getMaxAgeSeconds()); - } - - public void testSpaceAfterEqualsWithoutQuotes() { - RawHeaders headers = new RawHeaders(); - headers.add("Cache-Control", "max-age= 60"); - RequestHeaders parsedHeaders = new RequestHeaders(uri, headers); - assertEquals(60, parsedHeaders.getMaxAgeSeconds()); - } - - public void testCacheControlRequestDirectivesAreCaseInsensitive() { - RawHeaders headers = new RawHeaders(); - headers.add("Cache-Control", "NO-CACHE"); - headers.add("Cache-Control", "MAX-AGE=60"); - headers.add("Cache-Control", "MAX-STALE=70"); - headers.add("Cache-Control", "MIN-FRESH=80"); - headers.add("Cache-Control", "ONLY-IF-CACHED"); - RequestHeaders parsedHeaders = new RequestHeaders(uri, headers); - assertTrue(parsedHeaders.isNoCache()); - assertEquals(60, parsedHeaders.getMaxAgeSeconds()); - assertEquals(70, parsedHeaders.getMaxStaleSeconds()); - assertEquals(80, parsedHeaders.getMinFreshSeconds()); - assertTrue(parsedHeaders.isOnlyIfCached()); - } - - public void testCacheControlResponseDirectivesAreCaseInsensitive() { - RawHeaders headers = new RawHeaders(); - headers.add("Cache-Control", "NO-CACHE"); - headers.add("Cache-Control", "NO-STORE"); - headers.add("Cache-Control", "MAX-AGE=60"); - headers.add("Cache-Control", "S-MAXAGE=70"); - headers.add("Cache-Control", "PUBLIC"); - headers.add("Cache-Control", "MUST-REVALIDATE"); - ResponseHeaders parsedHeaders = new ResponseHeaders(uri, headers); - assertTrue(parsedHeaders.isNoCache()); - assertTrue(parsedHeaders.isNoStore()); - assertEquals(60, parsedHeaders.getMaxAgeSeconds()); - assertEquals(70, parsedHeaders.getSMaxAgeSeconds()); - assertTrue(parsedHeaders.isPublic()); - assertTrue(parsedHeaders.isMustRevalidate()); - } - - public void testPragmaDirectivesAreCaseInsensitive() { - RawHeaders headers = new RawHeaders(); - headers.add("Pragma", "NO-CACHE"); - RequestHeaders parsedHeaders = new RequestHeaders(uri, headers); - assertTrue(parsedHeaders.isNoCache()); - } - - public void testMissingInteger() { - RawHeaders headers = new RawHeaders(); - headers.add("Cache-Control", "max-age"); - RequestHeaders parsedHeaders = new RequestHeaders(uri, headers); - assertEquals(-1, parsedHeaders.getMaxAgeSeconds()); - } - - public void testInvalidInteger() { - RawHeaders headers = new RawHeaders(); - headers.add("Cache-Control", "MAX-AGE=pi"); - RequestHeaders requestHeaders = new RequestHeaders(uri, headers); - assertEquals(-1, requestHeaders.getMaxAgeSeconds()); - } - - public void testVeryLargeInteger() { - RawHeaders headers = new RawHeaders(); - headers.add("Cache-Control", "MAX-AGE=" + (Integer.MAX_VALUE + 1L)); - RequestHeaders parsedHeaders = new RequestHeaders(uri, headers); - assertEquals(Integer.MAX_VALUE, parsedHeaders.getMaxAgeSeconds()); - } - - public void testNegativeInteger() { - RawHeaders headers = new RawHeaders(); - headers.add("Cache-Control", "MAX-AGE=-2"); - RequestHeaders parsedHeaders = new RequestHeaders(uri, headers); - assertEquals(0, parsedHeaders.getMaxAgeSeconds()); - } - - public void testParseChallengesWithCommaInRealm() { - RawHeaders headers = new RawHeaders(); - headers.add("WWW-Authenticate", "s1 realm=\"ab,cd\", s2 realm=\"ef,gh\""); - assertEquals(Arrays.asList(new Challenge("s1", "ab,cd"), new Challenge("s2", "ef,gh")), - HeaderParser.parseChallenges(headers, "WWW-Authenticate")); - } - - public void testParseChallengesWithMultipleHeaders() { - RawHeaders headers = new RawHeaders(); - headers.add("WWW-Authenticate", "s1 realm=\"abc\""); - headers.add("WWW-Authenticate", "s2 realm=\"def\""); - assertEquals(Arrays.asList(new Challenge("s1", "abc"), new Challenge("s2", "def")), - HeaderParser.parseChallenges(headers, "WWW-Authenticate")); - } - - public void testParseChallengesWithExtraWhitespace() { - RawHeaders headers = new RawHeaders(); - headers.add("WWW-Authenticate", " s1 realm=\"a\" , s2 realm=\"b\", "); - assertEquals(Arrays.asList(new Challenge("s1", "a"), new Challenge("s2", "b")), - HeaderParser.parseChallenges(headers, "WWW-Authenticate")); - } - - public void testParseChallengesWithMissingRealm() { - RawHeaders headers = new RawHeaders(); - headers.add("WWW-Authenticate", "Basic"); - assertEquals(Collections.<Challenge>emptyList(), - HeaderParser.parseChallenges(headers, "WWW-Authenticate")); - } - - public void testParseChallengesWithEmptyRealm() { - RawHeaders headers = new RawHeaders(); - headers.add("WWW-Authenticate", "Basic realm=\"\""); - assertEquals(Arrays.asList(new Challenge("Basic", "")), - HeaderParser.parseChallenges(headers, "WWW-Authenticate")); - } - - public void testParseChallengesWithMissingScheme() { - RawHeaders headers = new RawHeaders(); - headers.add("WWW-Authenticate", "realm=\"a\""); - assertEquals(Collections.<Challenge>emptyList(), - HeaderParser.parseChallenges(headers, "WWW-Authenticate")); - } - - // http://code.google.com/p/android/issues/detail?id=11140 - public void testParseChallengesWithManyParameters() { - RawHeaders headers = new RawHeaders(); - headers.add("WWW-Authenticate", "Digest realm=\"abc\"," - + " qop=\"auth,auth-int\"," - + " nonce=\"dcd98b7102dd2f0e8b11d0f600bfb0c093\"," - + " opaque=\"5ccc069c403ebaf9f0171e9517f40e41\"," - + " Basic realm=\"def\""); - assertEquals(Arrays.asList(new Challenge("Digest", "abc"), new Challenge("Basic", "def")), - HeaderParser.parseChallenges(headers, "WWW-Authenticate")); - } -} diff --git a/luni/src/test/java/libcore/net/http/RawHeadersTest.java b/luni/src/test/java/libcore/net/http/RawHeadersTest.java deleted file mode 100644 index d99e851..0000000 --- a/luni/src/test/java/libcore/net/http/RawHeadersTest.java +++ /dev/null @@ -1,54 +0,0 @@ -/* - * 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.util.Arrays; -import junit.framework.TestCase; - -public class RawHeadersTest extends TestCase { - // http://code.google.com/p/android/issues/detail?id=6684 - public void test_caseInsensitiveButCasePreserving() { - RawHeaders h = new RawHeaders(); - h.add("Content-Type", "text/plain"); - // Case-insensitive: - assertEquals("text/plain", h.get("Content-Type")); - assertEquals("text/plain", h.get("Content-type")); - assertEquals("text/plain", h.get("content-type")); - assertEquals("text/plain", h.get("CONTENT-TYPE")); - // ...but case-preserving: - assertEquals("Content-Type", h.toMultimap().keySet().toArray()[0]); - - // We differ from the RI in that the Map we return is also case-insensitive. - // Our behavior seems more consistent. (And code that works on the RI will work on Android.) - assertEquals(Arrays.asList("text/plain"), h.toMultimap().get("Content-Type")); - assertEquals(Arrays.asList("text/plain"), h.toMultimap().get("Content-type")); // RI fails this. - } - - // The copy constructor used to be broken for headers with multiple values. - // http://code.google.com/p/android/issues/detail?id=6722 - public void test_copyConstructor() { - RawHeaders h1 = new RawHeaders(); - h1.add("key", "value1"); - h1.add("key", "value2"); - RawHeaders h2 = RawHeaders.fromMultimap(h1.toMultimap()); - assertEquals(2, h2.length()); - assertEquals("key", h2.getFieldName(0)); - assertEquals("value1", h2.getValue(0)); - assertEquals("key", h2.getFieldName(1)); - assertEquals("value2", h2.getValue(1)); - } -} diff --git a/luni/src/test/java/tests/api/java/util/LocaleTest.java b/luni/src/test/java/tests/api/java/util/LocaleTest.java index e5255e4..071dbc8 100644 --- a/luni/src/test/java/tests/api/java/util/LocaleTest.java +++ b/luni/src/test/java/tests/api/java/util/LocaleTest.java @@ -17,7 +17,6 @@ package tests.api.java.util; -import dalvik.annotation.AndroidOnly; import tests.support.Support_Locale; import java.security.Permission; @@ -172,7 +171,6 @@ public class LocaleTest extends junit.framework.TestCase { /** * java.util.Locale#getDisplayCountry() */ - @AndroidOnly("ICU has different display name for countries") public void test_getDisplayCountry() { // Test for method java.lang.String java.util.Locale.getDisplayCountry() assertTrue("Returned incorrect country: " @@ -248,35 +246,7 @@ public class LocaleTest extends junit.framework.TestCase { .getDisplayVariant(l).equals("WIN32")); } - /** - * java.util.Locale#getISO3Country() - */ - public void test_getISO3Country() { - // Test for method java.lang.String java.util.Locale.getISO3Country() - assertTrue("Returned incorrect ISO3 country: " - + testLocale.getISO3Country(), testLocale.getISO3Country() - .equals("CAN")); - - Locale l = new Locale("", "CD"); - assertEquals("COD", l.getISO3Country()); - - Locale x = new Locale("xx", "C"); - try { - x.getISO3Country(); - } catch (MissingResourceException e) { - //expected - } - } - - /** - * java.util.Locale#getISO3Language() - */ public void test_getISO3Language() { - // Test for method java.lang.String java.util.Locale.getISO3Language() - assertTrue("Returned incorrect ISO3 language: " - + testLocale.getISO3Language(), testLocale.getISO3Language() - .equals("eng")); - Locale l = new Locale("ae"); assertEquals("ave", l.getISO3Language()); @@ -287,13 +257,6 @@ public class LocaleTest extends junit.framework.TestCase { // Regression for Harmony-1129 l = new Locale("ak", ""); assertEquals("aka", l.getISO3Language()); - - Locale x = new Locale("xx", "C"); - try { - x.getISO3Language(); - } catch (MissingResourceException e) { - //expected - } } /** |