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