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