1 // Copyright 2015 The Chromium Authors 2 // Use of this source code is governed by a BSD-style license that can be 3 // found in the LICENSE file. 4 5 package org.chromium.net.urlconnection; 6 7 import androidx.annotation.VisibleForTesting; 8 9 import org.chromium.net.UploadDataProvider; 10 import org.chromium.net.UploadDataSink; 11 12 import androidx.annotation.VisibleForTesting; 13 14 import java.io.IOException; 15 import java.net.HttpRetryException; 16 import java.net.ProtocolException; 17 import java.nio.ByteBuffer; 18 19 /** 20 * An implementation of {@link java.io.OutputStream} to send data to a server, 21 * when {@link CronetHttpURLConnection#setFixedLengthStreamingMode} is used. 22 * This implementation does not buffer the entire request body in memory. 23 * It does not support rewind. Note that {@link #write} should only be called 24 * from the thread on which the {@link #mConnection} is created. 25 */ 26 @VisibleForTesting 27 public final class CronetFixedModeOutputStream extends CronetOutputStream { 28 // CronetFixedModeOutputStream buffers up to this value and wait for UploadDataStream 29 // to consume the data. This field is non-final, so it can be changed for tests. 30 // Using 16384 bytes is because the internal read buffer is 14520 for QUIC, 31 // 16384 for SPDY, and 16384 for normal HTTP/1.1 stream. 32 @VisibleForTesting public static int sDefaultBufferLength = 16384; 33 private final CronetHttpURLConnection mConnection; 34 private final MessageLoop mMessageLoop; 35 private final long mContentLength; 36 // Internal buffer for holding bytes from the client until the bytes are 37 // copied to the UploadDataSink in UploadDataProvider.read(). 38 // CronetFixedModeOutputStream allows client to provide up to 39 // sDefaultBufferLength bytes, and wait for UploadDataProvider.read() to be 40 // called after which point mBuffer is cleared so client can fill in again. 41 // While the client is filling the buffer (via {@code write()}), the buffer's 42 // position points to the next byte to be provided by the client, and limit 43 // points to the end of the buffer. The buffer is flipped before it is 44 // passed to the UploadDataProvider for consuming. Once it is flipped, 45 // buffer position points to the next byte to be copied to the 46 // UploadDataSink, and limit points to the end of data available to be 47 // copied to UploadDataSink. When the UploadDataProvider has provided all 48 // remaining bytes from the buffer to UploadDataSink, it clears the buffer 49 // so client can fill it again. 50 private final ByteBuffer mBuffer; 51 private final UploadDataProvider mUploadDataProvider = new UploadDataProviderImpl(); 52 private long mBytesWritten; 53 54 /** 55 * Package protected constructor. 56 * @param connection The CronetHttpURLConnection object. 57 * @param contentLength The content length of the request body. Non-zero for 58 * non-chunked upload. 59 */ CronetFixedModeOutputStream( CronetHttpURLConnection connection, long contentLength, MessageLoop messageLoop)60 CronetFixedModeOutputStream( 61 CronetHttpURLConnection connection, long contentLength, MessageLoop messageLoop) { 62 if (connection == null) { 63 throw new NullPointerException(); 64 } 65 if (contentLength < 0) { 66 throw new IllegalArgumentException( 67 "Content length must be larger than 0 for non-chunked upload."); 68 } 69 mContentLength = contentLength; 70 int bufferSize = (int) Math.min(mContentLength, sDefaultBufferLength); 71 mBuffer = ByteBuffer.allocate(bufferSize); 72 mConnection = connection; 73 mMessageLoop = messageLoop; 74 mBytesWritten = 0; 75 } 76 77 @Override write(int oneByte)78 public void write(int oneByte) throws IOException { 79 checkNotClosed(); 80 checkNotExceedContentLength(1); 81 ensureBufferHasRemaining(); 82 mBuffer.put((byte) oneByte); 83 mBytesWritten++; 84 uploadIfComplete(); 85 } 86 87 @Override write(byte[] buffer, int offset, int count)88 public void write(byte[] buffer, int offset, int count) throws IOException { 89 checkNotClosed(); 90 if (buffer.length - offset < count || offset < 0 || count < 0) { 91 throw new IndexOutOfBoundsException(); 92 } 93 checkNotExceedContentLength(count); 94 int toSend = count; 95 while (toSend > 0) { 96 ensureBufferHasRemaining(); 97 int sent = Math.min(toSend, mBuffer.remaining()); 98 mBuffer.put(buffer, offset + count - toSend, sent); 99 toSend -= sent; 100 } 101 mBytesWritten += count; 102 uploadIfComplete(); 103 } 104 105 /** 106 * If {@code mBuffer} is full, wait until it is consumed and there is 107 * space to write more data to it. 108 */ ensureBufferHasRemaining()109 private void ensureBufferHasRemaining() throws IOException { 110 if (!mBuffer.hasRemaining()) { 111 uploadBufferInternal(); 112 } 113 } 114 115 /** 116 * Waits for the native stack to upload {@code mBuffer}'s contents because 117 * the client has provided all bytes to be uploaded and there is no need to 118 * wait for or expect the client to provide more bytes. 119 */ uploadIfComplete()120 private void uploadIfComplete() throws IOException { 121 if (mBytesWritten == mContentLength) { 122 // Entire post data has been received. Now wait for network stack to 123 // read it. 124 uploadBufferInternal(); 125 } 126 } 127 128 /** 129 * Helper function to upload {@code mBuffer} to the native stack. This 130 * function blocks until {@code mBuffer} is consumed and there is space to 131 * write more data. 132 */ uploadBufferInternal()133 private void uploadBufferInternal() throws IOException { 134 checkNotClosed(); 135 mBuffer.flip(); 136 mMessageLoop.loop(); 137 checkNoException(); 138 } 139 140 /** 141 * Throws {@link java.net.ProtocolException} if adding {@code numBytes} will 142 * exceed content length. 143 */ checkNotExceedContentLength(int numBytes)144 private void checkNotExceedContentLength(int numBytes) throws ProtocolException { 145 if (mBytesWritten + numBytes > mContentLength) { 146 throw new ProtocolException( 147 "expected " 148 + (mContentLength - mBytesWritten) 149 + " bytes but received " 150 + numBytes); 151 } 152 } 153 154 // Below are CronetOutputStream implementations: 155 156 @Override setConnected()157 void setConnected() throws IOException { 158 // Do nothing. 159 } 160 161 @Override checkReceivedEnoughContent()162 void checkReceivedEnoughContent() throws IOException { 163 if (mBytesWritten < mContentLength) { 164 throw new ProtocolException("Content received is less than Content-Length."); 165 } 166 } 167 168 @Override getUploadDataProvider()169 UploadDataProvider getUploadDataProvider() { 170 return mUploadDataProvider; 171 } 172 173 private class UploadDataProviderImpl extends UploadDataProvider { 174 @Override getLength()175 public long getLength() { 176 return mContentLength; 177 } 178 179 @Override read(final UploadDataSink uploadDataSink, final ByteBuffer byteBuffer)180 public void read(final UploadDataSink uploadDataSink, final ByteBuffer byteBuffer) { 181 if (byteBuffer.remaining() >= mBuffer.remaining()) { 182 byteBuffer.put(mBuffer); 183 // Reuse this buffer. 184 mBuffer.clear(); 185 uploadDataSink.onReadSucceeded(false); 186 // Quit message loop so embedder can write more data. 187 mMessageLoop.quit(); 188 } else { 189 int oldLimit = mBuffer.limit(); 190 mBuffer.limit(mBuffer.position() + byteBuffer.remaining()); 191 byteBuffer.put(mBuffer); 192 mBuffer.limit(oldLimit); 193 uploadDataSink.onReadSucceeded(false); 194 } 195 } 196 197 @Override rewind(UploadDataSink uploadDataSink)198 public void rewind(UploadDataSink uploadDataSink) { 199 uploadDataSink.onRewindError( 200 new HttpRetryException("Cannot retry streamed Http body", -1)); 201 } 202 } 203 204 /** 205 * Sets the default buffer length for use in tests. 206 */ setDefaultBufferLengthForTesting(int length)207 public static void setDefaultBufferLengthForTesting(int length) { 208 sDefaultBufferLength = length; 209 } 210 } 211