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 
  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 
  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 
  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     //
 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 
 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 
 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 
 217     private String authorityString(InetSocketAddress addr) {
 218         return addr.getHostString() + ":" + addr.getPort();
 219     }
 220 
 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 
 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 
 269     synchronized boolean finished() {
 270         return  finished;
 271     }
 272 
 273     synchronized void setFinished() {
 274         finished = true;
 275     }
 276 
 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 
 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 
 332         StreamSubscriber() { super(debug); }
 333 
 334         @Override
 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
 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
 361         public String currentStateMessage() {
 362             return "streaming request body " + (complete ? "complete" : "incomplete");
 363         }
 364 
 365         @Override
 366         public void onError(Throwable throwable) {
 367             if (complete)
 368                 return;
 369 
 370             cancelSubscription();
 371             http1Exchange.appendToOutgoing(throwable);
 372         }
 373 
 374         @Override
 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;
 396         FixedContentSubscriber() { super(debug); }
 397 
 398         @Override
 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
 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
 433         public String currentStateMessage() {
 434             return format("fixed content-length: %d, bytes sent: %d",
 435                            contentLength, contentWritten);
 436         }
 437 
 438         @Override
 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
 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 */
 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 }