1 /*
2  * Copyright (c) 2015, 2018, 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.internal.net.http;
27 
28 import java.io.IOException;
29 import java.net.URI;
30 import java.nio.ByteBuffer;
31 import java.util.ArrayList;
32 import java.util.List;
33 import java.util.Map;
34 import java.net.InetSocketAddress;
35 import java.util.Objects;
36 import java.util.concurrent.Flow;
37 import java.util.function.BiPredicate;
38 import java.net.http.HttpHeaders;
39 import java.net.http.HttpRequest;
40 import jdk.internal.net.http.Http1Exchange.Http1BodySubscriber;
41 import jdk.internal.net.http.common.HttpHeadersBuilder;
42 import jdk.internal.net.http.common.Log;
43 import jdk.internal.net.http.common.Logger;
44 import jdk.internal.net.http.common.Utils;
45 
46 import static java.lang.String.format;
47 import static java.nio.charset.StandardCharsets.US_ASCII;
48 
49 /**
50  *  An HTTP/1.1 request.
51  */
52 class Http1Request {
53 
54     private static final String COOKIE_HEADER = "Cookie";
55     private static final BiPredicate<String,String> NOCOOKIES =
56             (k,v) -> !COOKIE_HEADER.equalsIgnoreCase(k);
57 
58     private final HttpRequestImpl request;
59     private final Http1Exchange<?> http1Exchange;
60     private final HttpConnection connection;
61     private final HttpRequest.BodyPublisher requestPublisher;
62     private final HttpHeaders userHeaders;
63     private final HttpHeadersBuilder systemHeadersBuilder;
64     private volatile boolean streaming;
65     private volatile long contentLength;
66 
Http1Request(HttpRequestImpl request, Http1Exchange<?> http1Exchange)67     Http1Request(HttpRequestImpl request,
68                  Http1Exchange<?> http1Exchange)
69         throws IOException
70     {
71         this.request = request;
72         this.http1Exchange = http1Exchange;
73         this.connection = http1Exchange.connection();
74         this.requestPublisher = request.requestPublisher;  // may be null
75         this.userHeaders = request.getUserHeaders();
76         this.systemHeadersBuilder = request.getSystemHeadersBuilder();
77     }
78 
logHeaders(String completeHeaders)79     private void logHeaders(String completeHeaders) {
80         if (Log.headers()) {
81             //StringBuilder sb = new StringBuilder(256);
82             //sb.append("REQUEST HEADERS:\n");
83             //Log.dumpHeaders(sb, "    ", systemHeaders);
84             //Log.dumpHeaders(sb, "    ", userHeaders);
85             //Log.logHeaders(sb.toString());
86 
87             String s = completeHeaders.replaceAll("\r\n", "\n");
88             if (s.endsWith("\n\n")) s = s.substring(0, s.length() - 2);
89             Log.logHeaders("REQUEST HEADERS:\n{0}\n", s);
90         }
91     }
92 
93 
collectHeaders0(StringBuilder sb)94     private void collectHeaders0(StringBuilder sb) {
95         BiPredicate<String,String> filter =
96                 connection.headerFilter(request);
97 
98         // Filter out 'Cookie:' headers, we will collect them at the end.
99         BiPredicate<String,String> nocookies = NOCOOKIES.and(filter);
100 
101         HttpHeaders systemHeaders = systemHeadersBuilder.build();
102 
103         // If we're sending this request through a tunnel,
104         // then don't send any preemptive proxy-* headers that
105         // the authentication filter may have saved in its
106         // cache.
107         collectHeaders1(sb, systemHeaders, nocookies);
108 
109         // If we're sending this request through a tunnel,
110         // don't send any user-supplied proxy-* headers
111         // to the target server.
112         collectHeaders1(sb, userHeaders, nocookies);
113 
114         // Gather all 'Cookie:' headers and concatenate their
115         // values in a single line.
116         collectCookies(sb, systemHeaders, userHeaders);
117 
118         // terminate headers
119         sb.append('\r').append('\n');
120     }
121 
122     // Concatenate any 'Cookie:' header in a single line, as mandated
123     // by RFC 6265, section 5.4:
124     //
125     // <<When the user agent generates an HTTP request, the user agent MUST
126     //   NOT attach more than one Cookie header field.>>
127     //
128     // This constraint is relaxed for the HTTP/2 protocol, which
129     // explicitly allows sending multiple Cookie header fields.
130     // RFC 7540 section 8.1.2.5:
131     //
132     // <<To allow for better compression efficiency, the Cookie header
133     //   field MAY be split into separate header fields, each with one or
134     //   more cookie-pairs.>>
135     //
136     // This method will therefore concatenate multiple Cookie header field
137     // values into a single field, in a similar way than was implemented in
138     // the legacy HttpURLConnection.
139     //
140     // Note that at this point this method performs no further validation
141     // on the actual field-values, except to check that they do not contain
142     // any illegal character for header field values.
143     //
collectCookies(StringBuilder sb, HttpHeaders system, HttpHeaders user)144     private void collectCookies(StringBuilder sb,
145                                 HttpHeaders system,
146                                 HttpHeaders user) {
147         List<String> systemList = system.allValues(COOKIE_HEADER);
148         List<String> userList = user.allValues(COOKIE_HEADER);
149         boolean found = false;
150         if (systemList != null) {
151             for (String cookie : systemList) {
152                 if (!found) {
153                     found = true;
154                     sb.append(COOKIE_HEADER).append(':').append(' ');
155                 } else {
156                     sb.append(';').append(' ');
157                 }
158                 sb.append(cookie);
159             }
160         }
161         if (userList != null) {
162             for (String cookie : userList) {
163                 if (!found) {
164                     found = true;
165                     sb.append(COOKIE_HEADER).append(':').append(' ');
166                 } else {
167                     sb.append(';').append(' ');
168                 }
169                 sb.append(cookie);
170             }
171         }
172         if (found) sb.append('\r').append('\n');
173     }
174 
collectHeaders1(StringBuilder sb, HttpHeaders headers, BiPredicate<String,String> filter)175     private void collectHeaders1(StringBuilder sb,
176                                  HttpHeaders headers,
177                                  BiPredicate<String,String> filter) {
178         for (Map.Entry<String,List<String>> entry : headers.map().entrySet()) {
179             String key = entry.getKey();
180             List<String> values = entry.getValue();
181             for (String value : values) {
182                 if (!filter.test(key, value))
183                     continue;
184                 sb.append(key).append(':').append(' ')
185                         .append(value)
186                         .append('\r').append('\n');
187             }
188         }
189     }
190 
getPathAndQuery(URI uri)191     private String getPathAndQuery(URI uri) {
192         String path = uri.getRawPath();
193         String query = uri.getRawQuery();
194         if (path == null || path.equals("")) {
195             path = "/";
196         }
197         if (query == null) {
198             query = "";
199         }
200         if (query.equals("")) {
201             return Utils.encode(path);
202         } else {
203             return Utils.encode(path + "?" + query);
204         }
205     }
206 
authorityString(InetSocketAddress addr)207     private String authorityString(InetSocketAddress addr) {
208         return addr.getHostString() + ":" + addr.getPort();
209     }
210 
hostString()211     private String hostString() {
212         URI uri = request.uri();
213         int port = uri.getPort();
214         String host = uri.getHost();
215 
216         boolean defaultPort;
217         if (port == -1) {
218             defaultPort = true;
219         } else if (request.secure()) {
220             defaultPort = port == 443;
221         } else {
222             defaultPort = port == 80;
223         }
224 
225         if (defaultPort) {
226             return host;
227         } else {
228             return host + ":" + Integer.toString(port);
229         }
230     }
231 
requestURI()232     private String requestURI() {
233         URI uri = request.uri();
234         String method = request.method();
235 
236         if ((request.proxy() == null && !method.equals("CONNECT"))
237                 || request.isWebSocket()) {
238             return getPathAndQuery(uri);
239         }
240         if (request.secure()) {
241             if (request.method().equals("CONNECT")) {
242                 // use authority for connect itself
243                 return authorityString(request.authority());
244             } else {
245                 // requests over tunnel do not require full URL
246                 return getPathAndQuery(uri);
247             }
248         }
249         if (request.method().equals("CONNECT")) {
250             // use authority for connect itself
251             return authorityString(request.authority());
252         }
253 
254         return uri == null? authorityString(request.authority()) : uri.toString();
255     }
256 
257     private boolean finished;
258 
finished()259     synchronized boolean finished() {
260         return  finished;
261     }
262 
setFinished()263     synchronized void setFinished() {
264         finished = true;
265     }
266 
headers()267     List<ByteBuffer> headers() {
268         if (Log.requests() && request != null) {
269             Log.logRequest(request.toString());
270         }
271         String uriString = requestURI();
272         StringBuilder sb = new StringBuilder(64);
273         sb.append(request.method())
274           .append(' ')
275           .append(uriString)
276           .append(" HTTP/1.1\r\n");
277 
278         URI uri = request.uri();
279         if (uri != null) {
280             systemHeadersBuilder.setHeader("Host", hostString());
281         }
282         if (requestPublisher == null) {
283             // Not a user request, or maybe a method, e.g. GET, with no body.
284             contentLength = 0;
285         } else {
286             contentLength = requestPublisher.contentLength();
287         }
288 
289         if (contentLength == 0) {
290             systemHeadersBuilder.setHeader("Content-Length", "0");
291         } else if (contentLength > 0) {
292             systemHeadersBuilder.setHeader("Content-Length", Long.toString(contentLength));
293             streaming = false;
294         } else {
295             streaming = true;
296             systemHeadersBuilder.setHeader("Transfer-encoding", "chunked");
297         }
298         collectHeaders0(sb);
299         String hs = sb.toString();
300         logHeaders(hs);
301         ByteBuffer b = ByteBuffer.wrap(hs.getBytes(US_ASCII));
302         return List.of(b);
303     }
304 
continueRequest()305     Http1BodySubscriber continueRequest()  {
306         Http1BodySubscriber subscriber;
307         if (streaming) {
308             subscriber = new StreamSubscriber();
309             requestPublisher.subscribe(subscriber);
310         } else {
311             if (contentLength == 0)
312                 return null;
313 
314             subscriber = new FixedContentSubscriber();
315             requestPublisher.subscribe(subscriber);
316         }
317         return subscriber;
318     }
319 
320     final class StreamSubscriber extends Http1BodySubscriber {
321 
StreamSubscriber()322         StreamSubscriber() { super(debug); }
323 
324         @Override
onSubscribe(Flow.Subscription subscription)325         public void onSubscribe(Flow.Subscription subscription) {
326             if (isSubscribed()) {
327                 Throwable t = new IllegalStateException("already subscribed");
328                 http1Exchange.appendToOutgoing(t);
329             } else {
330                 setSubscription(subscription);
331             }
332         }
333 
334         @Override
onNext(ByteBuffer item)335         public void onNext(ByteBuffer item) {
336             Objects.requireNonNull(item);
337             if (complete) {
338                 Throwable t = new IllegalStateException("subscription already completed");
339                 http1Exchange.appendToOutgoing(t);
340             } else {
341                 int chunklen = item.remaining();
342                 ArrayList<ByteBuffer> l = new ArrayList<>(3);
343                 l.add(getHeader(chunklen));
344                 l.add(item);
345                 l.add(ByteBuffer.wrap(CRLF));
346                 http1Exchange.appendToOutgoing(l);
347             }
348         }
349 
350         @Override
currentStateMessage()351         public String currentStateMessage() {
352             return "streaming request body " + (complete ? "complete" : "incomplete");
353         }
354 
355         @Override
onError(Throwable throwable)356         public void onError(Throwable throwable) {
357             if (complete)
358                 return;
359 
360             cancelSubscription();
361             http1Exchange.appendToOutgoing(throwable);
362         }
363 
364         @Override
onComplete()365         public void onComplete() {
366             if (complete) {
367                 Throwable t = new IllegalStateException("subscription already completed");
368                 http1Exchange.appendToOutgoing(t);
369             } else {
370                 ArrayList<ByteBuffer> l = new ArrayList<>(2);
371                 l.add(ByteBuffer.wrap(EMPTY_CHUNK_BYTES));
372                 l.add(ByteBuffer.wrap(CRLF));
373                 complete = true;
374                 //setFinished();
375                 http1Exchange.appendToOutgoing(l);
376                 http1Exchange.appendToOutgoing(COMPLETED);
377                 setFinished();  // TODO: before or after,? does it matter?
378 
379             }
380         }
381     }
382 
383     final class FixedContentSubscriber extends Http1BodySubscriber {
384 
385         private volatile long contentWritten;
FixedContentSubscriber()386         FixedContentSubscriber() { super(debug); }
387 
388         @Override
onSubscribe(Flow.Subscription subscription)389         public void onSubscribe(Flow.Subscription subscription) {
390             if (isSubscribed()) {
391                 Throwable t = new IllegalStateException("already subscribed");
392                 http1Exchange.appendToOutgoing(t);
393             } else {
394                 setSubscription(subscription);
395             }
396         }
397 
398         @Override
onNext(ByteBuffer item)399         public void onNext(ByteBuffer item) {
400             if (debug.on()) debug.log("onNext");
401             Objects.requireNonNull(item);
402             if (complete) {
403                 Throwable t = new IllegalStateException("subscription already completed");
404                 http1Exchange.appendToOutgoing(t);
405             } else {
406                 long writing = item.remaining();
407                 long written = (contentWritten += writing);
408 
409                 if (written > contentLength) {
410                     cancelSubscription();
411                     String msg = connection.getConnectionFlow()
412                                   + " [" + Thread.currentThread().getName() +"] "
413                                   + "Too many bytes in request body. Expected: "
414                                   + contentLength + ", got: " + written;
415                     http1Exchange.appendToOutgoing(new IOException(msg));
416                 } else {
417                     http1Exchange.appendToOutgoing(List.of(item));
418                 }
419             }
420         }
421 
422         @Override
currentStateMessage()423         public String currentStateMessage() {
424             return format("fixed content-length: %d, bytes sent: %d",
425                            contentLength, contentWritten);
426         }
427 
428         @Override
onError(Throwable throwable)429         public void onError(Throwable throwable) {
430             if (debug.on()) debug.log("onError");
431             if (complete)  // TODO: error?
432                 return;
433 
434             cancelSubscription();
435             http1Exchange.appendToOutgoing(throwable);
436         }
437 
438         @Override
onComplete()439         public void onComplete() {
440             if (debug.on()) debug.log("onComplete");
441             if (complete) {
442                 Throwable t = new IllegalStateException("subscription already completed");
443                 http1Exchange.appendToOutgoing(t);
444             } else {
445                 complete = true;
446                 long written = contentWritten;
447                 if (contentLength > written) {
448                     cancelSubscription();
449                     Throwable t = new IOException(connection.getConnectionFlow()
450                                          + " [" + Thread.currentThread().getName() +"] "
451                                          + "Too few bytes returned by the publisher ("
452                                                   + written + "/"
453                                                   + contentLength + ")");
454                     http1Exchange.appendToOutgoing(t);
455                 } else {
456                     http1Exchange.appendToOutgoing(COMPLETED);
457                 }
458             }
459         }
460     }
461 
462     private static final byte[] CRLF = {'\r', '\n'};
463     private static final byte[] EMPTY_CHUNK_BYTES = {'0', '\r', '\n'};
464 
465     /** Returns a header for a particular chunk size */
getHeader(int size)466     private static ByteBuffer getHeader(int size) {
467         String hexStr = Integer.toHexString(size);
468         byte[] hexBytes = hexStr.getBytes(US_ASCII);
469         byte[] header = new byte[hexStr.length()+2];
470         System.arraycopy(hexBytes, 0, header, 0, hexBytes.length);
471         header[hexBytes.length] = CRLF[0];
472         header[hexBytes.length+1] = CRLF[1];
473         return ByteBuffer.wrap(header);
474     }
475 
476     final Logger debug = Utils.getDebugLogger(this::toString, Utils.DEBUG);
477 
478 }
479