Commit 2503556d authored by jwilson's avatar jwilson Committed by Brian Carlstrom
Browse files

Switch to OkHttp as URL's preferred HTTP implementation.

Change-Id: Id724b75dd78b68ed00f5db4989c4070896996ec0
parent e99e4edd
......@@ -61,6 +61,7 @@ ifeq ($(WITH_HOST_DALVIK),true)
core-hostdex \
bouncycastle-hostdex \
apache-xml-hostdex \
okhttp-hostdex \
apache-harmony-tests-hostdex \
$(call intermediates-dir-for,JAVA_LIBRARIES,core-tests,,COMMON)/classes.jar
endif
......@@ -96,8 +96,8 @@ include $(CLEAR_VARS)
LOCAL_SRC_FILES := $(call all-test-java-files-under,dalvik dom json luni support xml)
LOCAL_JAVA_RESOURCE_DIRS := $(test_resource_dirs)
LOCAL_NO_STANDARD_LIBRARIES := true
LOCAL_JAVA_LIBRARIES := bouncycastle core core-junit
LOCAL_STATIC_JAVA_LIBRARIES := sqlite-jdbc mockwebserver nist-pkix-tests
LOCAL_JAVA_LIBRARIES := bouncycastle core core-junit okhttp
LOCAL_STATIC_JAVA_LIBRARIES := sqlite-jdbc mockwebserver nist-pkix-tests okhttp-tests
LOCAL_JAVACFLAGS := $(local_javac_flags)
LOCAL_MODULE := core-tests
LOCAL_ADDITIONAL_DEPENDENCIES := $(LOCAL_PATH)/JavaLibrary.mk
......@@ -151,7 +151,7 @@ ifeq ($(WITH_HOST_DALVIK),true)
LOCAL_SRC_FILES := $(call all-test-java-files-under,dalvik dom json luni support xml)
LOCAL_JAVA_RESOURCE_DIRS := $(test_resource_dirs)
LOCAL_NO_STANDARD_LIBRARIES := true
LOCAL_JAVA_LIBRARIES := bouncycastle-hostdex core-hostdex core-junit-hostdex
LOCAL_JAVA_LIBRARIES := bouncycastle-hostdex core-hostdex core-junit-hostdex okhttp-hostdex
LOCAL_STATIC_JAVA_LIBRARIES := sqlite-jdbc-host mockwebserver-host nist-pkix-tests-host
LOCAL_JAVACFLAGS := $(local_javac_flags)
LOCAL_MODULE_TAGS := optional
......
/*
* 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);
}
......@@ -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;
}
......
/*
* 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;
}
}
......@@ -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();
}
......
/*
* 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);
}
}
/*
* 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");
}
}
}
/*
* 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 + "]";
}
}
/*
* 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();
}
}
}
/*
* 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);
}
}
/*
* 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();
}
}
}
/*
* 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");
}
}
}
/*
* 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;
}
}
}
/*
* 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);
}
}
}
/*
* 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();
}
}
This diff is collapsed.
/*
* 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;
}
}
/*
* 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);
}
}
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment