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 }