blob: a925746fda50bb2a5e669429e5668e7886114d69 [file] [log] [blame]
/* GENERATED SOURCE. DO NOT MODIFY. */
/*
* 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 com.android.okhttp.internal.http;
import com.android.okhttp.Headers;
import com.android.okhttp.Request;
import com.android.okhttp.Response;
import com.android.okhttp.ResponseBody;
import com.android.okhttp.internal.Internal;
import com.android.okhttp.internal.Util;
import com.android.okhttp.internal.io.RealConnection;
import java.io.EOFException;
import java.io.IOException;
import java.net.ProtocolException;
import com.android.okhttp.okio.Buffer;
import com.android.okhttp.okio.BufferedSink;
import com.android.okhttp.okio.BufferedSource;
import com.android.okhttp.okio.ForwardingTimeout;
import com.android.okhttp.okio.Okio;
import com.android.okhttp.okio.Sink;
import com.android.okhttp.okio.Source;
import com.android.okhttp.okio.Timeout;
import static com.android.okhttp.internal.Util.checkOffsetAndCount;
import static com.android.okhttp.internal.http.StatusLine.HTTP_CONTINUE;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
/**
* A socket connection that can be used to send HTTP/1.1 messages. This class
* strictly enforces the following lifecycle:
* <ol>
* <li>{@link #writeRequest Send request headers}.
* <li>Open a sink to write the request body. Either {@link
* #newFixedLengthSink fixed-length} or {@link #newChunkedSink chunked}.
* <li>Write to and then close that sink.
* <li>{@link #readResponse Read response headers}.
* <li>Open a source to read the response body. Either {@link
* #newFixedLengthSource fixed-length}, {@link #newChunkedSource chunked}
* or {@link #newUnknownLengthSource unknown length}.
* <li>Read from and close that source.
* </ol>
* <p>Exchanges that do not have a request body may skip creating and closing
* the request body. Exchanges that do not have a response body can call {@link
* #newFixedLengthSource(long) newFixedLengthSource(0)} and may skip reading and
* closing that source.
* @hide This class is not part of the Android public SDK API
*/
public final class Http1xStream implements HttpStream {
private static final int STATE_IDLE = 0; // Idle connections are ready to write request headers.
private static final int STATE_OPEN_REQUEST_BODY = 1;
private static final int STATE_WRITING_REQUEST_BODY = 2;
private static final int STATE_READ_RESPONSE_HEADERS = 3;
private static final int STATE_OPEN_RESPONSE_BODY = 4;
private static final int STATE_READING_RESPONSE_BODY = 5;
private static final int STATE_CLOSED = 6;
/** The stream allocation that owns this stream. May be null for HTTPS proxy tunnels. */
private final StreamAllocation streamAllocation;
private final BufferedSource source;
private final BufferedSink sink;
private HttpEngine httpEngine;
private int state = STATE_IDLE;
public Http1xStream(StreamAllocation streamAllocation, BufferedSource source, BufferedSink sink) {
this.streamAllocation = streamAllocation;
this.source = source;
this.sink = sink;
}
@Override public void setHttpEngine(HttpEngine httpEngine) {
this.httpEngine = httpEngine;
}
@Override public Sink createRequestBody(Request request, long contentLength) throws IOException {
if ("chunked".equalsIgnoreCase(request.header("Transfer-Encoding"))) {
// Stream a request body of unknown length.
return newChunkedSink();
}
if (contentLength != -1) {
// Stream a request body of a known length.
return newFixedLengthSink(contentLength);
}
throw new IllegalStateException(
"Cannot stream a request body without chunked encoding or a known content length!");
}
@Override public void cancel() {
RealConnection connection = streamAllocation.connection();
if (connection != null) connection.cancel();
}
/**
* 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.
*/
@Override public void writeRequestHeaders(Request request) throws IOException {
httpEngine.writingRequestHeaders();
String requestLine = RequestLine.get(
request, httpEngine.getConnection().getRoute().getProxy().type());
writeRequest(request.headers(), requestLine);
}
@Override public Response.Builder readResponseHeaders() throws IOException {
return readResponse();
}
@Override public ResponseBody openResponseBody(Response response) throws IOException {
Source source = getTransferStream(response);
return new RealResponseBody(response.headers(), Okio.buffer(source));
}
private Source getTransferStream(Response response) throws IOException {
if (!HttpEngine.hasBody(response)) {
return newFixedLengthSource(0);
}
if ("chunked".equalsIgnoreCase(response.header("Transfer-Encoding"))) {
return newChunkedSource(httpEngine);
}
long contentLength = OkHeaders.contentLength(response);
if (contentLength != -1) {
return newFixedLengthSource(contentLength);
}
// Wrap the input stream from the connection (rather than just returning
// "socketIn" directly here), so that we can control its use after the
// reference escapes.
return newUnknownLengthSource();
}
/** Returns true if this connection is closed. */
public boolean isClosed() {
return state == STATE_CLOSED;
}
@Override public void finishRequest() throws IOException {
sink.flush();
}
/** Returns bytes of a request header for sending on an HTTP transport. */
public void writeRequest(Headers headers, String requestLine) throws IOException {
if (state != STATE_IDLE) throw new IllegalStateException("state: " + state);
sink.writeUtf8(requestLine).writeUtf8("\r\n");
for (int i = 0, size = headers.size(); i < size; i ++) {
sink.writeUtf8(headers.name(i))
.writeUtf8(": ")
.writeUtf8(headers.value(i))
.writeUtf8("\r\n");
}
sink.writeUtf8("\r\n");
state = STATE_OPEN_REQUEST_BODY;
}
/** Parses bytes of a response header from an HTTP transport. */
public Response.Builder readResponse() throws IOException {
if (state != STATE_OPEN_REQUEST_BODY && state != STATE_READ_RESPONSE_HEADERS) {
throw new IllegalStateException("state: " + state);
}
try {
while (true) {
StatusLine statusLine = StatusLine.parse(source.readUtf8LineStrict());
Response.Builder responseBuilder = new Response.Builder()
.protocol(statusLine.protocol)
.code(statusLine.code)
.message(statusLine.message)
.headers(readHeaders());
if (statusLine.code != HTTP_CONTINUE) {
state = STATE_OPEN_RESPONSE_BODY;
return responseBuilder;
}
}
} catch (EOFException e) {
// Provide more context if the server ends the stream before sending a response.
IOException exception = new IOException("unexpected end of stream on " + streamAllocation);
exception.initCause(e);
throw exception;
}
}
/** Reads headers or trailers. */
public Headers readHeaders() throws IOException {
Headers.Builder headers = new Headers.Builder();
// parse the result headers until the first blank line
for (String line; (line = source.readUtf8LineStrict()).length() != 0; ) {
Internal.instance.addLenient(headers, line);
}
return headers.build();
}
public Sink newChunkedSink() {
if (state != STATE_OPEN_REQUEST_BODY) throw new IllegalStateException("state: " + state);
state = STATE_WRITING_REQUEST_BODY;
return new ChunkedSink();
}
public Sink newFixedLengthSink(long contentLength) {
if (state != STATE_OPEN_REQUEST_BODY) throw new IllegalStateException("state: " + state);
state = STATE_WRITING_REQUEST_BODY;
return new FixedLengthSink(contentLength);
}
@Override public void writeRequestBody(RetryableSink requestBody) throws IOException {
if (state != STATE_OPEN_REQUEST_BODY) throw new IllegalStateException("state: " + state);
state = STATE_READ_RESPONSE_HEADERS;
requestBody.writeToSocket(sink);
}
public Source newFixedLengthSource(long length) throws IOException {
if (state != STATE_OPEN_RESPONSE_BODY) throw new IllegalStateException("state: " + state);
state = STATE_READING_RESPONSE_BODY;
return new FixedLengthSource(length);
}
public Source newChunkedSource(HttpEngine httpEngine) throws IOException {
if (state != STATE_OPEN_RESPONSE_BODY) throw new IllegalStateException("state: " + state);
state = STATE_READING_RESPONSE_BODY;
return new ChunkedSource(httpEngine);
}
public Source newUnknownLengthSource() throws IOException {
if (state != STATE_OPEN_RESPONSE_BODY) throw new IllegalStateException("state: " + state);
if (streamAllocation == null) throw new IllegalStateException("streamAllocation == null");
state = STATE_READING_RESPONSE_BODY;
streamAllocation.noNewStreams();
return new UnknownLengthSource();
}
/**
* Sets the delegate of {@code timeout} to {@link Timeout#NONE} and resets its underlying timeout
* to the default configuration. Use this to avoid unexpected sharing of timeouts between pooled
* connections.
*/
private void detachTimeout(ForwardingTimeout timeout) {
Timeout oldDelegate = timeout.delegate();
timeout.setDelegate(Timeout.NONE);
oldDelegate.clearDeadline();
oldDelegate.clearTimeout();
}
/** An HTTP body with a fixed length known in advance. */
private final class FixedLengthSink implements Sink {
private final ForwardingTimeout timeout = new ForwardingTimeout(sink.timeout());
private boolean closed;
private long bytesRemaining;
private FixedLengthSink(long bytesRemaining) {
this.bytesRemaining = bytesRemaining;
}
@Override public Timeout timeout() {
return timeout;
}
@Override public void write(Buffer source, long byteCount) throws IOException {
if (closed) throw new IllegalStateException("closed");
checkOffsetAndCount(source.size(), 0, byteCount);
if (byteCount > bytesRemaining) {
throw new ProtocolException("expected " + bytesRemaining
+ " bytes but received " + byteCount);
}
sink.write(source, byteCount);
bytesRemaining -= byteCount;
}
@Override public void flush() throws IOException {
if (closed) return; // Don't throw; this stream might have been closed on the caller's behalf.
sink.flush();
}
@Override public void close() throws IOException {
if (closed) return;
closed = true;
if (bytesRemaining > 0) throw new ProtocolException("unexpected end of stream");
detachTimeout(timeout);
state = STATE_READ_RESPONSE_HEADERS;
}
}
/**
* An HTTP body with alternating chunk sizes and chunk bodies. It is the
* caller's responsibility to buffer chunks; typically by using a buffered
* sink with this sink.
*/
private final class ChunkedSink implements Sink {
private final ForwardingTimeout timeout = new ForwardingTimeout(sink.timeout());
private boolean closed;
@Override public Timeout timeout() {
return timeout;
}
@Override public void write(Buffer source, long byteCount) throws IOException {
if (closed) throw new IllegalStateException("closed");
if (byteCount == 0) return;
sink.writeHexadecimalUnsignedLong(byteCount);
sink.writeUtf8("\r\n");
sink.write(source, byteCount);
sink.writeUtf8("\r\n");
}
@Override public synchronized void flush() throws IOException {
if (closed) return; // Don't throw; this stream might have been closed on the caller's behalf.
sink.flush();
}
@Override public synchronized void close() throws IOException {
if (closed) return;
closed = true;
sink.writeUtf8("0\r\n\r\n");
detachTimeout(timeout);
state = STATE_READ_RESPONSE_HEADERS;
}
}
private abstract class AbstractSource implements Source {
protected final ForwardingTimeout timeout = new ForwardingTimeout(source.timeout());
protected boolean closed;
@Override public Timeout timeout() {
return timeout;
}
/**
* 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() throws IOException {
if (state != STATE_READING_RESPONSE_BODY) throw new IllegalStateException("state: " + state);
detachTimeout(timeout);
state = STATE_CLOSED;
if (streamAllocation != null) {
streamAllocation.streamFinished(Http1xStream.this);
}
}
protected final void unexpectedEndOfInput() {
if (state == STATE_CLOSED) return;
state = STATE_CLOSED;
if (streamAllocation != null) {
streamAllocation.noNewStreams();
streamAllocation.streamFinished(Http1xStream.this);
}
}
}
/** An HTTP body with a fixed length specified in advance. */
private class FixedLengthSource extends AbstractSource {
private long bytesRemaining;
public FixedLengthSource(long length) throws IOException {
bytesRemaining = length;
if (bytesRemaining == 0) {
endOfInput();
}
}
@Override public long read(Buffer sink, long byteCount) throws IOException {
if (byteCount < 0) throw new IllegalArgumentException("byteCount < 0: " + byteCount);
if (closed) throw new IllegalStateException("closed");
if (bytesRemaining == 0) return -1;
long read = source.read(sink, Math.min(bytesRemaining, byteCount));
if (read == -1) {
unexpectedEndOfInput(); // The server didn't supply the promised content length.
throw new ProtocolException("unexpected end of stream");
}
bytesRemaining -= read;
if (bytesRemaining == 0) {
endOfInput();
}
return read;
}
@Override public void close() throws IOException {
if (closed) return;
if (bytesRemaining != 0
&& !Util.discard(this, DISCARD_STREAM_TIMEOUT_MILLIS, MILLISECONDS)) {
unexpectedEndOfInput();
}
closed = true;
}
}
/** An HTTP body with alternating chunk sizes and chunk bodies. */
private class ChunkedSource extends AbstractSource {
private static final long NO_CHUNK_YET = -1L;
private long bytesRemainingInChunk = NO_CHUNK_YET;
private boolean hasMoreChunks = true;
private final HttpEngine httpEngine;
ChunkedSource(HttpEngine httpEngine) throws IOException {
this.httpEngine = httpEngine;
}
@Override public long read(Buffer sink, long byteCount) throws IOException {
if (byteCount < 0) throw new IllegalArgumentException("byteCount < 0: " + byteCount);
if (closed) throw new IllegalStateException("closed");
if (!hasMoreChunks) return -1;
if (bytesRemainingInChunk == 0 || bytesRemainingInChunk == NO_CHUNK_YET) {
readChunkSize();
if (!hasMoreChunks) return -1;
}
long read = source.read(sink, Math.min(byteCount, bytesRemainingInChunk));
if (read == -1) {
unexpectedEndOfInput(); // The server didn't supply the promised chunk length.
throw new ProtocolException("unexpected end of stream");
}
bytesRemainingInChunk -= read;
return read;
}
private void readChunkSize() throws IOException {
// Read the suffix of the previous chunk.
if (bytesRemainingInChunk != NO_CHUNK_YET) {
source.readUtf8LineStrict();
}
try {
bytesRemainingInChunk = source.readHexadecimalUnsignedLong();
String extensions = source.readUtf8LineStrict().trim();
if (bytesRemainingInChunk < 0 || (!extensions.isEmpty() && !extensions.startsWith(";"))) {
throw new ProtocolException("expected chunk size and optional extensions but was \""
+ bytesRemainingInChunk + extensions + "\"");
}
} catch (NumberFormatException e) {
throw new ProtocolException(e.getMessage());
}
if (bytesRemainingInChunk == 0L) {
hasMoreChunks = false;
httpEngine.receiveHeaders(readHeaders());
endOfInput();
}
}
@Override public void close() throws IOException {
if (closed) return;
if (hasMoreChunks && !Util.discard(this, DISCARD_STREAM_TIMEOUT_MILLIS, MILLISECONDS)) {
unexpectedEndOfInput();
}
closed = true;
}
}
/** An HTTP message body terminated by the end of the underlying stream. */
private class UnknownLengthSource extends AbstractSource {
private boolean inputExhausted;
@Override public long read(Buffer sink, long byteCount)
throws IOException {
if (byteCount < 0) throw new IllegalArgumentException("byteCount < 0: " + byteCount);
if (closed) throw new IllegalStateException("closed");
if (inputExhausted) return -1;
long read = source.read(sink, byteCount);
if (read == -1) {
inputExhausted = true;
endOfInput();
return -1;
}
return read;
}
@Override public void close() throws IOException {
if (closed) return;
if (!inputExhausted) {
unexpectedEndOfInput();
}
closed = true;
}
}
}