1 /*
   2  * Copyright (c) 2015, 2017, 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 jdk.incubator.http;
  27 
  28 import sun.net.www.MessageHeader;
  29 
  30 import java.io.IOException;
  31 import java.io.InputStream;
  32 import java.net.ProtocolException;
  33 import java.nio.ByteBuffer;
  34 import java.util.ArrayList;
  35 import java.util.HashMap;
  36 import java.util.List;
  37 import java.util.Locale;
  38 import java.util.Map;
  39 import java.util.Optional;
  40 import java.util.OptionalLong;
  41 
  42 import static java.lang.String.format;
  43 import static jdk.incubator.http.internal.common.Utils.isValidName;
  44 import static jdk.incubator.http.internal.common.Utils.isValidValue;
  45 import static java.util.Objects.requireNonNull;
  46 
  47 /*
  48  * Reads entire header block off channel, in blocking mode.
  49  * This class is not thread-safe.
  50  */
  51 final class ResponseHeaders implements HttpHeaders {
  52 
  53     private static final char CR = '\r';
  54     private static final char LF = '\n';
  55 
  56     private final ImmutableHeaders delegate;
  57 
  58     /*
  59      * This constructor takes a connection from which the header block is read
  60      * and a buffer which may contain an initial portion of this header block.
  61      *
  62      * After the headers have been parsed (this constructor has returned) the
  63      * leftovers (i.e. data, if any, beyond the header block) are accessible
  64      * from this same buffer from its position to its limit.
  65      */
  66     ResponseHeaders(HttpConnection connection, ByteBuffer buffer) throws IOException {
  67         requireNonNull(connection);
  68         requireNonNull(buffer);
  69         InputStreamWrapper input = new InputStreamWrapper(connection, buffer);
  70         delegate = ImmutableHeaders.of(parse(input));
  71     }
  72 
  73     static final class InputStreamWrapper extends InputStream {
  74         final HttpConnection connection;
  75         ByteBuffer buffer;
  76         int lastRead = -1; // last byte read from the buffer
  77         int consumed = 0; // number of bytes consumed.
  78         InputStreamWrapper(HttpConnection connection, ByteBuffer buffer) {
  79             super();
  80             this.connection = connection;
  81             this.buffer = buffer;
  82         }
  83         @Override
  84         public int read() throws IOException {
  85             if (!buffer.hasRemaining()) {
  86                 buffer = connection.read();
  87                 if (buffer == null) {
  88                     return lastRead = -1;
  89                 }
  90             }
  91             // don't let consumed become positive again if it overflowed
  92             // we just want to make sure that consumed == 1 really means
  93             // that only one byte was consumed.
  94             if (consumed >= 0) consumed++;
  95             return lastRead = buffer.get();
  96         }
  97     }
  98 
  99     private static void display(Map<String, List<String>> map) {
 100         map.forEach((k,v) -> {
 101             System.out.print (k + ": ");
 102             for (String val : v) {
 103                 System.out.print(val + ", ");
 104             }
 105             System.out.println("");
 106         });
 107     }
 108 
 109     private Map<String, List<String>> parse(InputStreamWrapper input)
 110          throws IOException
 111     {
 112         // The bulk of work is done by this time-proven class
 113         MessageHeader h = new MessageHeader();
 114         h.parseHeader(input);
 115 
 116         // When there are no headers (and therefore no body), the status line
 117         // will be followed by an empty CRLF line.
 118         // In that case MessageHeader.parseHeader() will consume the first
 119         // CR character and stop there. In this case we must consume the
 120         // remaining LF.
 121         if (input.consumed == 1 && CR == (char) input.lastRead) {
 122             // MessageHeader will not consume LF if the first character it
 123             // finds is CR. This only happens if there are no headers, and
 124             // only one byte will be consumed from the buffer. In this case
 125             // the next byte MUST be LF
 126             if (input.read() != LF) {
 127                 throw new IOException("Unexpected byte sequence when no headers: "
 128                      + ((int)CR) + " " + input.lastRead
 129                      + "(" + ((int)CR) + " " + ((int)LF) + " expected)");
 130             }
 131         }
 132 
 133         Map<String, List<String>> rawHeaders = h.getHeaders();
 134 
 135         // Now some additional post-processing to adapt the results received
 136         // from MessageHeader to what is needed here
 137         Map<String, List<String>> cookedHeaders = new HashMap<>();
 138         for (Map.Entry<String, List<String>> e : rawHeaders.entrySet()) {
 139             String key = e.getKey();
 140             if (key == null) {
 141                 throw new ProtocolException("Bad header-field");
 142             }
 143             if (!isValidName(key)) {
 144                 throw new ProtocolException(format(
 145                         "Bad header-name: '%s'", key));
 146             }
 147             List<String> newValues = e.getValue();
 148             for (String v : newValues) {
 149                 if (!isValidValue(v)) {
 150                     throw new ProtocolException(format(
 151                             "Bad header-value for header-name: '%s'", key));
 152                 }
 153             }
 154             String k = key.toLowerCase(Locale.US);
 155             cookedHeaders.merge(k, newValues,
 156                     (v1, v2) -> {
 157                         ArrayList<String> newV = new ArrayList<>();
 158                         if (v1 != null) {
 159                             newV.addAll(v1);
 160                         }
 161                         newV.addAll(v2);
 162                         return newV;
 163                     });
 164         }
 165         return cookedHeaders;
 166     }
 167 
 168     int getContentLength() throws IOException {
 169         return (int) firstValueAsLong("Content-Length").orElse(-1);
 170     }
 171 
 172     @Override
 173     public Optional<String> firstValue(String name) {
 174         return delegate.firstValue(name);
 175     }
 176 
 177     @Override
 178     public OptionalLong firstValueAsLong(String name) {
 179         return delegate.firstValueAsLong(name);
 180     }
 181 
 182     @Override
 183     public List<String> allValues(String name) {
 184         return delegate.allValues(name);
 185     }
 186 
 187     @Override
 188     public Map<String, List<String>> map() {
 189         return delegate.map();
 190     }
 191 }