| /* |
| * 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.squareup.okhttp.internal.http; |
| |
| import com.squareup.okhttp.Connection; |
| import com.squareup.okhttp.internal.AbstractOutputStream; |
| import com.squareup.okhttp.internal.Util; |
| import java.io.ByteArrayOutputStream; |
| import java.io.IOException; |
| import java.io.InputStream; |
| import java.io.OutputStream; |
| import java.net.CacheRequest; |
| import java.net.ProtocolException; |
| import java.net.Socket; |
| |
| import static com.squareup.okhttp.internal.Util.checkOffsetAndCount; |
| |
| public final class HttpTransport implements Transport { |
| /** |
| * The timeout to use while discarding a stream of input data. Since this is |
| * used for connection reuse, this timeout should be significantly less than |
| * the time it takes to establish a new connection. |
| */ |
| private static final int DISCARD_STREAM_TIMEOUT_MILLIS = 100; |
| |
| public static final int DEFAULT_CHUNK_LENGTH = 1024; |
| |
| private final HttpEngine httpEngine; |
| private final InputStream socketIn; |
| private final OutputStream socketOut; |
| |
| /** |
| * This stream buffers the request headers and the request body when their |
| * combined size is less than MAX_REQUEST_BUFFER_LENGTH. By combining them |
| * we can save socket writes, which in turn saves a packet transmission. |
| * This is socketOut if the request size is large or unknown. |
| */ |
| private OutputStream requestOut; |
| |
| public HttpTransport(HttpEngine httpEngine, OutputStream outputStream, InputStream inputStream) { |
| this.httpEngine = httpEngine; |
| this.socketOut = outputStream; |
| this.requestOut = outputStream; |
| this.socketIn = inputStream; |
| } |
| |
| @Override public OutputStream createRequestBody() throws IOException { |
| boolean chunked = httpEngine.requestHeaders.isChunked(); |
| if (!chunked |
| && httpEngine.policy.getChunkLength() > 0 |
| && httpEngine.connection.getHttpMinorVersion() != 0) { |
| httpEngine.requestHeaders.setChunked(); |
| chunked = true; |
| } |
| |
| // Stream a request body of unknown length. |
| if (chunked) { |
| int chunkLength = httpEngine.policy.getChunkLength(); |
| if (chunkLength == -1) { |
| chunkLength = DEFAULT_CHUNK_LENGTH; |
| } |
| writeRequestHeaders(); |
| return new ChunkedOutputStream(requestOut, chunkLength); |
| } |
| |
| // Stream a request body of a known length. |
| int fixedContentLength = httpEngine.policy.getFixedContentLength(); |
| if (fixedContentLength != -1) { |
| httpEngine.requestHeaders.setContentLength(fixedContentLength); |
| writeRequestHeaders(); |
| return new FixedLengthOutputStream(requestOut, fixedContentLength); |
| } |
| |
| // Buffer a request body of a known length. |
| int contentLength = httpEngine.requestHeaders.getContentLength(); |
| if (contentLength != -1) { |
| writeRequestHeaders(); |
| return new RetryableOutputStream(contentLength); |
| } |
| |
| // Buffer a request body of an unknown length. Don't write request |
| // headers until the entire body is ready; otherwise we can't set the |
| // Content-Length header correctly. |
| return new RetryableOutputStream(); |
| } |
| |
| @Override public void flushRequest() throws IOException { |
| requestOut.flush(); |
| requestOut = socketOut; |
| } |
| |
| @Override public void writeRequestBody(RetryableOutputStream requestBody) throws IOException { |
| requestBody.writeToSocket(requestOut); |
| } |
| |
| /** |
| * Prepares the HTTP headers and sends them to the server. |
| * |
| * <p>For streaming requests with a body, headers must be prepared |
| * <strong>before</strong> the output stream has been written to. Otherwise |
| * the body would need to be buffered! |
| * |
| * <p>For non-streaming requests with a body, headers must be prepared |
| * <strong>after</strong> the output stream has been written to and closed. |
| * This ensures that the {@code Content-Length} header field receives the |
| * proper value. |
| */ |
| public void writeRequestHeaders() throws IOException { |
| httpEngine.writingRequestHeaders(); |
| RawHeaders headersToSend = httpEngine.requestHeaders.getHeaders(); |
| byte[] bytes = headersToSend.toBytes(); |
| requestOut.write(bytes); |
| } |
| |
| @Override public ResponseHeaders readResponseHeaders() throws IOException { |
| RawHeaders headers = RawHeaders.fromBytes(socketIn); |
| httpEngine.connection.setHttpMinorVersion(headers.getHttpMinorVersion()); |
| httpEngine.receiveHeaders(headers); |
| return new ResponseHeaders(httpEngine.uri, headers); |
| } |
| |
| public boolean makeReusable(boolean streamCancelled, OutputStream requestBodyOut, |
| InputStream responseBodyIn) { |
| if (streamCancelled) { |
| return false; |
| } |
| |
| // We cannot reuse sockets that have incomplete output. |
| if (requestBodyOut != null && !((AbstractOutputStream) requestBodyOut).isClosed()) { |
| return false; |
| } |
| |
| // If the request specified that the connection shouldn't be reused, don't reuse it. |
| if (httpEngine.requestHeaders.hasConnectionClose()) { |
| return false; |
| } |
| |
| // If the response specified that the connection shouldn't be reused, don't reuse it. |
| if (httpEngine.responseHeaders != null && httpEngine.responseHeaders.hasConnectionClose()) { |
| return false; |
| } |
| |
| if (responseBodyIn instanceof UnknownLengthHttpInputStream) { |
| return false; |
| } |
| |
| if (responseBodyIn != null) { |
| return discardStream(httpEngine, responseBodyIn); |
| } |
| |
| return true; |
| } |
| |
| /** |
| * Discards the response body so that the connection can be reused. This |
| * needs to be done judiciously, since it delays the current request in |
| * order to speed up a potential future request that may never occur. |
| */ |
| private static boolean discardStream(HttpEngine httpEngine, InputStream responseBodyIn) { |
| Connection connection = httpEngine.connection; |
| if (connection == null) return false; |
| Socket socket = connection.getSocket(); |
| if (socket == null) return false; |
| try { |
| int socketTimeout = socket.getSoTimeout(); |
| socket.setSoTimeout(DISCARD_STREAM_TIMEOUT_MILLIS); |
| try { |
| Util.skipAll(responseBodyIn); |
| return true; |
| } finally { |
| socket.setSoTimeout(socketTimeout); |
| } |
| } catch (IOException e) { |
| return false; |
| } |
| } |
| |
| @Override public InputStream getTransferStream(CacheRequest cacheRequest) throws IOException { |
| if (!httpEngine.hasResponseBody()) { |
| return new FixedLengthInputStream(socketIn, cacheRequest, httpEngine, 0); |
| } |
| |
| if (httpEngine.responseHeaders.isChunked()) { |
| return new ChunkedInputStream(socketIn, cacheRequest, this); |
| } |
| |
| if (httpEngine.responseHeaders.getContentLength() != -1) { |
| return new FixedLengthInputStream(socketIn, cacheRequest, httpEngine, |
| httpEngine.responseHeaders.getContentLength()); |
| } |
| |
| // Wrap the input stream from the connection (rather than just returning |
| // "socketIn" directly here), so that we can control its use after the |
| // reference escapes. |
| return new UnknownLengthHttpInputStream(socketIn, cacheRequest, httpEngine); |
| } |
| |
| /** An HTTP body with a fixed length known in advance. */ |
| private static final class FixedLengthOutputStream extends AbstractOutputStream { |
| private final OutputStream socketOut; |
| private int bytesRemaining; |
| |
| private FixedLengthOutputStream(OutputStream socketOut, int bytesRemaining) { |
| this.socketOut = socketOut; |
| this.bytesRemaining = bytesRemaining; |
| } |
| |
| @Override public void write(byte[] buffer, int offset, int count) throws IOException { |
| checkNotClosed(); |
| checkOffsetAndCount(buffer.length, offset, count); |
| if (count > bytesRemaining) { |
| throw new ProtocolException("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 ProtocolException("unexpected end of stream"); |
| } |
| } |
| } |
| |
| /** |
| * An HTTP body with alternating chunk sizes and chunk bodies. Chunks are |
| * buffered until {@code maxChunkLength} bytes are ready, at which point the |
| * chunk is written and the buffer is cleared. |
| */ |
| private static final class ChunkedOutputStream extends AbstractOutputStream { |
| private static final byte[] CRLF = { '\r', '\n' }; |
| private static final byte[] HEX_DIGITS = { |
| '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f' |
| }; |
| private static final byte[] FINAL_CHUNK = new byte[] { '0', '\r', '\n', '\r', '\n' }; |
| |
| /** Scratch space for up to 8 hex digits, and then a constant CRLF. */ |
| private final byte[] hex = { 0, 0, 0, 0, 0, 0, 0, 0, '\r', '\n' }; |
| |
| private final OutputStream socketOut; |
| private final int maxChunkLength; |
| private final ByteArrayOutputStream bufferedChunk; |
| |
| private ChunkedOutputStream(OutputStream socketOut, int maxChunkLength) { |
| this.socketOut = socketOut; |
| this.maxChunkLength = Math.max(1, dataLength(maxChunkLength)); |
| this.bufferedChunk = new ByteArrayOutputStream(maxChunkLength); |
| } |
| |
| /** |
| * Returns the amount of data that can be transmitted in a chunk whose total |
| * length (data+headers) is {@code dataPlusHeaderLength}. This is presumably |
| * useful to match sizes with wire-protocol packets. |
| */ |
| private int dataLength(int dataPlusHeaderLength) { |
| int headerLength = 4; // "\r\n" after the size plus another "\r\n" after the data |
| for (int i = dataPlusHeaderLength - headerLength; i > 0; i >>= 4) { |
| headerLength++; |
| } |
| return dataPlusHeaderLength - headerLength; |
| } |
| |
| @Override public synchronized void write(byte[] buffer, int offset, int count) |
| throws IOException { |
| checkNotClosed(); |
| checkOffsetAndCount(buffer.length, offset, count); |
| |
| while (count > 0) { |
| int numBytesWritten; |
| |
| if (bufferedChunk.size() > 0 || count < maxChunkLength) { |
| // fill the buffered chunk and then maybe write that to the stream |
| numBytesWritten = Math.min(count, maxChunkLength - bufferedChunk.size()); |
| // TODO: skip unnecessary copies from buffer->bufferedChunk? |
| bufferedChunk.write(buffer, offset, numBytesWritten); |
| if (bufferedChunk.size() == maxChunkLength) { |
| writeBufferedChunkToSocket(); |
| } |
| } else { |
| // write a single chunk of size maxChunkLength to the stream |
| numBytesWritten = maxChunkLength; |
| writeHex(numBytesWritten); |
| socketOut.write(buffer, offset, numBytesWritten); |
| socketOut.write(CRLF); |
| } |
| |
| offset += numBytesWritten; |
| count -= numBytesWritten; |
| } |
| } |
| |
| /** |
| * Equivalent to, but cheaper than writing Integer.toHexString().getBytes() |
| * followed by CRLF. |
| */ |
| private void writeHex(int i) throws IOException { |
| int cursor = 8; |
| do { |
| hex[--cursor] = HEX_DIGITS[i & 0xf]; |
| } while ((i >>>= 4) != 0); |
| socketOut.write(hex, cursor, hex.length - cursor); |
| } |
| |
| @Override public synchronized void flush() throws IOException { |
| if (closed) { |
| return; // don't throw; this stream might have been closed on the caller's behalf |
| } |
| writeBufferedChunkToSocket(); |
| socketOut.flush(); |
| } |
| |
| @Override public synchronized void close() throws IOException { |
| if (closed) { |
| return; |
| } |
| closed = true; |
| writeBufferedChunkToSocket(); |
| socketOut.write(FINAL_CHUNK); |
| } |
| |
| private void writeBufferedChunkToSocket() throws IOException { |
| int size = bufferedChunk.size(); |
| if (size <= 0) { |
| return; |
| } |
| |
| writeHex(size); |
| bufferedChunk.writeTo(socketOut); |
| bufferedChunk.reset(); |
| socketOut.write(CRLF); |
| } |
| } |
| |
| /** An HTTP body with a fixed length specified in advance. */ |
| private static class FixedLengthInputStream extends AbstractHttpInputStream { |
| private int bytesRemaining; |
| |
| public FixedLengthInputStream(InputStream is, CacheRequest cacheRequest, HttpEngine httpEngine, |
| int length) throws IOException { |
| super(is, httpEngine, cacheRequest); |
| bytesRemaining = length; |
| if (bytesRemaining == 0) { |
| endOfInput(false); |
| } |
| } |
| |
| @Override public int read(byte[] buffer, int offset, int count) throws IOException { |
| 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 ProtocolException("unexpected end of stream"); |
| } |
| bytesRemaining -= read; |
| cacheWrite(buffer, offset, read); |
| if (bytesRemaining == 0) { |
| endOfInput(false); |
| } |
| 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; |
| } |
| if (bytesRemaining != 0 && !discardStream(httpEngine, this)) { |
| unexpectedEndOfInput(); |
| } |
| closed = true; |
| } |
| } |
| |
| /** An HTTP body with alternating chunk sizes and chunk bodies. */ |
| private static class ChunkedInputStream extends AbstractHttpInputStream { |
| private static final int NO_CHUNK_YET = -1; |
| private final HttpTransport transport; |
| private int bytesRemainingInChunk = NO_CHUNK_YET; |
| private boolean hasMoreChunks = true; |
| |
| ChunkedInputStream(InputStream is, CacheRequest cacheRequest, HttpTransport transport) |
| throws IOException { |
| super(is, transport.httpEngine, cacheRequest); |
| this.transport = transport; |
| } |
| |
| @Override public int read(byte[] buffer, int offset, int count) throws IOException { |
| 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); |
| return read; |
| } |
| |
| private void readChunkSize() throws IOException { |
| // read the suffix of the previous chunk |
| if (bytesRemainingInChunk != NO_CHUNK_YET) { |
| Util.readAsciiLine(in); |
| } |
| String chunkSizeString = Util.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 ProtocolException("Expected a hex chunk size but was " + chunkSizeString); |
| } |
| if (bytesRemainingInChunk == 0) { |
| hasMoreChunks = false; |
| RawHeaders rawResponseHeaders = httpEngine.responseHeaders.getHeaders(); |
| RawHeaders.readHeaders(transport.socketIn, rawResponseHeaders); |
| httpEngine.receiveHeaders(rawResponseHeaders); |
| endOfInput(false); |
| } |
| } |
| |
| @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; |
| } |
| if (hasMoreChunks && !discardStream(httpEngine, this)) { |
| unexpectedEndOfInput(); |
| } |
| closed = true; |
| } |
| } |
| } |