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
26package jdk.incubator.http;
27
28import sun.net.www.MessageHeader;
29
30import java.io.IOException;
31import java.io.InputStream;
32import java.net.ProtocolException;
33import java.nio.ByteBuffer;
34import java.util.ArrayList;
35import java.util.HashMap;
36import java.util.List;
37import java.util.Locale;
38import java.util.Map;
39import java.util.Optional;
40import java.util.OptionalLong;
41
42import static java.lang.String.format;
43import static jdk.incubator.http.internal.common.Utils.isValidName;
44import static jdk.incubator.http.internal.common.Utils.isValidValue;
45import 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 */
51final 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                        if (v1 == null) {
158                            ArrayList<String> newV = new ArrayList<>();
159                            newV.addAll(v2);
160                            return newV;
161                        } else {
162                            v1.addAll(v2);
163                            return v1;
164                        }
165                    });
166        }
167        return cookedHeaders;
168    }
169
170    int getContentLength() throws IOException {
171        return (int) firstValueAsLong("Content-Length").orElse(-1);
172    }
173
174    @Override
175    public Optional<String> firstValue(String name) {
176        return delegate.firstValue(name);
177    }
178
179    @Override
180    public OptionalLong firstValueAsLong(String name) {
181        return delegate.firstValueAsLong(name);
182    }
183
184    @Override
185    public List<String> allValues(String name) {
186        return delegate.allValues(name);
187    }
188
189    @Override
190    public Map<String, List<String>> map() {
191        return delegate.map();
192    }
193}
194