1 /*
   2  * Copyright (c) 2015, 2016, Oracle and/or its affiliates. All rights reserved.
   3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
   4  *
   5  * This code is free software; you can redistribute it and/or modify it
   6  * under the terms of the GNU General Public License version 2 only, as
   7  * published by the Free Software Foundation.  Oracle designates this
   8  * particular file as subject to the "Classpath" exception as provided
   9  * by Oracle in the LICENSE file that accompanied this code.
  10  *
  11  * This code is distributed in the hope that it will be useful, but WITHOUT
  12  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
  13  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
  14  * version 2 for more details (a copy is included in the LICENSE file that
  15  * accompanied this code).
  16  *
  17  * You should have received a copy of the GNU General Public License version
  18  * 2 along with this work; if not, write to the Free Software Foundation,
  19  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
  20  *
  21  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
  22  * or visit www.oracle.com if you need additional information or have any
  23  * questions.
  24  */
  25 
  26 package java.net.http;
  27 
  28 import java.io.IOException;
  29 import java.nio.ByteBuffer;
  30 
  31 /**
  32  * Implements chunked/fixed transfer encodings of HTTP/1.1 responses.
  33  */
  34 class ResponseContent {
  35 
  36     final HttpResponse.BodyProcessor<?> userProcessor;
  37     final HttpResponse.BodyProcessor<?> pusher;
  38     final HttpConnection connection;
  39     final int contentLength;
  40     ByteBuffer buffer;
  41     ByteBuffer lastBufferUsed;
  42     final ResponseHeaders headers;
  43     final Http1Response.FlowController flowController;
  44 
  45     ResponseContent(HttpConnection connection,
  46                     int contentLength,
  47                     ResponseHeaders h,
  48                     HttpResponse.BodyProcessor<?> userProcessor,
  49                     Http1Response.FlowController flowController) {
  50         this.userProcessor = userProcessor;
  51         this.pusher = (HttpResponse.BodyProcessor)userProcessor;
  52         this.connection = connection;
  53         this.contentLength = contentLength;
  54         this.headers = h;
  55         this.flowController = flowController;
  56     }
  57 
  58     static final int LF = 10;
  59     static final int CR = 13;
  60     static final int SP = 0x20;
  61     static final int BUF_SIZE = 1024;
  62 
  63     boolean chunkedContent, chunkedContentInitialized;
  64 
  65     private boolean contentChunked() throws IOException {
  66         if (chunkedContentInitialized) {
  67             return chunkedContent;
  68         }
  69         if (contentLength == -1) {
  70             String tc = headers.firstValue("Transfer-Encoding")
  71                                .orElse("");
  72             if (!tc.equals("")) {
  73                 if (tc.equalsIgnoreCase("chunked")) {
  74                     chunkedContent = true;
  75                 } else {
  76                     throw new IOException("invalid content");
  77                 }
  78             } else {
  79                 chunkedContent = false;
  80             }
  81         }
  82         chunkedContentInitialized = true;
  83         return chunkedContent;
  84     }
  85 
  86     /**
  87      * Entry point for pusher. b is an initial ByteBuffer that may
  88      * have some data in it. When this method returns, the body
  89      * has been fully processed.
  90      */
  91     void pushBody(ByteBuffer b) throws IOException {
  92         // TODO: check status
  93         if (contentChunked()) {
  94             pushBodyChunked(b);
  95         } else {
  96             pushBodyFixed(b);
  97         }
  98     }
  99 
 100     // reads and returns chunklen. Position of chunkbuf is first byte
 101     // of chunk on return. chunklen includes the CR LF at end of chunk
 102     int readChunkLen() throws IOException {
 103         chunklen = 0;
 104         boolean cr = false;
 105         while (true) {
 106             getHunk();
 107             int c = chunkbuf.get();
 108             if (cr) {
 109                 if (c == LF) {
 110                     return chunklen + 2;
 111                 } else {
 112                     throw new IOException("invalid chunk header");
 113                 }
 114             }
 115             if (c == CR) {
 116                 cr = true;
 117             } else {
 118                 int digit = toDigit(c);
 119                 chunklen = chunklen * 16 + digit;
 120             }
 121         }
 122     }
 123 
 124     int chunklen = -1;      // number of bytes in chunk (fixed)
 125     int bytesremaining;     // number of bytes in chunk left to be read incl CRLF
 126     int bytesread;
 127     ByteBuffer chunkbuf;    // initialise
 128 
 129     // make sure we have at least 1 byte to look at
 130     private void getHunk() throws IOException {
 131         while (chunkbuf == null || !chunkbuf.hasRemaining()) {
 132 
 133             if (chunkbuf != null) {
 134                 connection.returnBuffer(chunkbuf);
 135             }
 136             chunkbuf = connection.read();
 137         }
 138     }
 139 
 140     private void consumeBytes(int n) throws IOException {
 141         getHunk();
 142         while (n > 0) {
 143             int e = Math.min(chunkbuf.remaining(), n);
 144             chunkbuf.position(chunkbuf.position() + e);
 145             n -= e;
 146             if (n > 0)
 147                 getHunk();
 148         }
 149     }
 150 
 151     /**
 152      * Returns a ByteBuffer containing a chunk of data or a "hunk" of data
 153      * (a chunk of a chunk if the chunk size is larger than our ByteBuffers).
 154      */
 155     ByteBuffer readChunkedBuffer() throws IOException {
 156         if (chunklen == -1) {
 157             // new chunk
 158             bytesremaining = readChunkLen();
 159             chunklen = bytesremaining - 2;
 160             if (chunklen == 0) {
 161                 consumeBytes(2);
 162                 return null;
 163             }
 164         }
 165 
 166         getHunk();
 167         bytesread = chunkbuf.remaining();
 168         ByteBuffer returnBuffer;
 169 
 170         /**
 171          * Cases. Always at least one byte is read by getHunk()
 172          *
 173          * 1) one read contains exactly 1 chunk. Strip off CRLF and pass buffer on
 174          * 2) one read contains a hunk. If at end of chunk, consume CRLF.Pass buffer up.
 175          * 3) read contains rest of chunk and more data. Copy buffer.
 176          */
 177         if (bytesread == bytesremaining) {
 178             // common case: 1 read = 1 chunk (or final hunk of chunk)
 179             chunkbuf.limit(chunkbuf.limit() - 2); // remove trailing CRLF
 180             bytesremaining = 0;
 181             returnBuffer = chunkbuf;
 182             chunkbuf = null;
 183             chunklen = -1;
 184         } else if (bytesread < bytesremaining) {
 185             // read a hunk, maybe including CR or LF or both
 186             bytesremaining -= bytesread;
 187             if (bytesremaining <= 2) {
 188                 // remove any trailing CR LF already read, and then read the rest
 189                 chunkbuf.limit(chunkbuf.limit() - (2 - bytesremaining));
 190                 consumeBytes(bytesremaining);
 191                 chunklen = -1;
 192             }
 193             returnBuffer = chunkbuf;
 194             chunkbuf = null;
 195         } else {
 196             // bytesread > bytesremaining
 197             returnBuffer = splitChunkedBuffer(bytesremaining-2);
 198             bytesremaining = 0;
 199             chunklen = -1;
 200             consumeBytes(2);
 201         }
 202         return returnBuffer;
 203     }
 204 
 205     ByteBuffer initialBuffer;
 206     int fixedBytesReturned;
 207 
 208     ByteBuffer getResidue() {
 209         return lastBufferUsed;
 210     }
 211 
 212     private void compactBuffer(ByteBuffer buf) {
 213         buf.compact()
 214            .flip();
 215     }
 216 
 217     /**
 218      * Copies inbuf (numBytes from its position) to new buffer. The returned
 219      * buffer's position is zero and limit is at end (numBytes)
 220      */
 221     private ByteBuffer copyBuffer(ByteBuffer inbuf, int numBytes) {
 222         ByteBuffer b1 = connection.getBuffer();
 223         assert b1.remaining() >= numBytes;
 224         byte[] b = b1.array();
 225         inbuf.get(b, 0, numBytes);
 226         b1.limit(numBytes);
 227         return b1;
 228     }
 229 
 230     /**
 231      * Split numBytes of data out of chunkbuf from the remainder,
 232      * copying whichever part is smaller. chunkbuf points to second part
 233      * of buffer on return. The returned buffer is the data from position
 234      * to position + numBytes. Both buffers positions are reset so same
 235      * data can be re-read.
 236      */
 237     private ByteBuffer splitChunkedBuffer(int numBytes) {
 238         ByteBuffer newbuf = connection.getBuffer();
 239         byte[] b = newbuf.array();
 240         int midpoint = chunkbuf.position() + numBytes;
 241         int remainder = chunkbuf.limit() - midpoint;
 242 
 243         if (numBytes < remainder) {
 244             // copy first part of chunkbuf to new buf
 245             chunkbuf.get(b, 0, numBytes);
 246             newbuf.limit(numBytes);
 247             return newbuf;
 248         } else {
 249             // copy remainder of chunkbuf to newbuf and make newbuf chunkbuf
 250             chunkbuf.mark();
 251             chunkbuf.position(midpoint);
 252             chunkbuf.get(b, 0, remainder);
 253             chunkbuf.reset();
 254             chunkbuf.limit(midpoint);
 255             newbuf.limit(remainder);
 256             newbuf.position(0);
 257             ByteBuffer tmp = chunkbuf;
 258             chunkbuf = newbuf;
 259             return tmp;
 260         }
 261     }
 262 
 263     private void pushBodyChunked(ByteBuffer b) throws IOException {
 264         chunkbuf = b;
 265         while (true) {
 266             ByteBuffer b1 = readChunkedBuffer();
 267             if (b1 != null) {
 268                 if (b1.hasRemaining()) {
 269                     request(1); // wait till we can send
 270                     pusher.onResponseBodyChunk(b1);
 271                     lastBufferUsed = b1;
 272                 }
 273             } else {
 274                 return;
 275             }
 276         }
 277     }
 278 
 279     private int toDigit(int b) throws IOException {
 280         if (b >= 0x30 && b <= 0x39) {
 281             return b - 0x30;
 282         }
 283         if (b >= 0x41 && b <= 0x46) {
 284             return b - 0x41 + 10;
 285         }
 286         if (b >= 0x61 && b <= 0x66) {
 287             return b - 0x61 + 10;
 288         }
 289         throw new IOException("Invalid chunk header byte " + b);
 290     }
 291 
 292     private void request(long value) throws IOException {
 293         try {
 294             flowController.request(value);
 295         } catch (InterruptedException e) {
 296             throw new IOException(e);
 297         }
 298     }
 299 
 300     private void pushBodyFixed(ByteBuffer b) throws IOException {
 301         lastBufferUsed = b;
 302         for (int remaining = contentLength; remaining > 0;) {
 303             int bufsize = b.remaining();
 304             if (bufsize > remaining) {
 305                 // more data available than required, must copy
 306                 lastBufferUsed = b;
 307                 b = copyBuffer(b, remaining);
 308                 remaining = 0;
 309             } else {
 310                 // pass entire buffer up to user
 311                 remaining -= bufsize;
 312                 compactBuffer(b);
 313             }
 314             request(1); // wait till we can send
 315             pusher.onResponseBodyChunk(b);
 316             if (remaining > 0) {
 317                 b = connection.read();
 318                 if (b == null) {
 319                     throw new IOException("Error reading response");
 320                 }
 321                 lastBufferUsed = b;
 322             }
 323         }
 324     }
 325 }