Skip to content

Commit 4dc3701

Browse files
Alexey BakhtinRealCLanger
authored andcommitted
8328286: Enhance HTTP client
Reviewed-by: mbalao Backport-of: cf8dc79f392c8ec3414d8b36803f026852c4e386
1 parent 124191f commit 4dc3701

File tree

25 files changed

+1074
-173
lines changed

25 files changed

+1074
-173
lines changed

src/java.base/share/classes/java/net/doc-files/net-properties.html

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -240,6 +240,15 @@ <H2>Misc HTTP URL stream protocol handler properties</H2>
240240
</OL>
241241
<P>The channel binding tokens generated are of the type "tls-server-end-point" as defined in
242242
RFC 5929.</P>
243+
244+
<LI><P><B>{@systemProperty jdk.http.maxHeaderSize}</B> (default: 393216 or 384kB)<BR>
245+
This is the maximum header field section size that a client is prepared to accept.
246+
This is computed as the sum of the size of the uncompressed header name, plus
247+
the size of the uncompressed header value, plus an overhead of 32 bytes for
248+
each field section line. If a peer sends a field section that exceeds this
249+
size a {@link java.net.ProtocolException ProtocolException} will be raised.
250+
This applies to all versions of the HTTP protocol. A value of zero or a negative
251+
value means no limit. If left unspecified, the default value is 393216 bytes.
243252
</UL>
244253
<P>All these properties are checked only once at startup.</P>
245254
<a id="AddressCache"></a>

src/java.base/share/classes/sun/net/www/MessageHeader.java

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@
3030
package sun.net.www;
3131

3232
import java.io.*;
33+
import java.lang.reflect.Array;
34+
import java.net.ProtocolException;
3335
import java.util.Collections;
3436
import java.util.*;
3537

@@ -46,11 +48,32 @@ class MessageHeader {
4648
private String values[];
4749
private int nkeys;
4850

51+
// max number of bytes for headers, <=0 means unlimited;
52+
// this corresponds to the length of the names, plus the length
53+
// of the values, plus an overhead of 32 bytes per name: value
54+
// pair.
55+
// Note: we use the same definition as HTTP/2 SETTINGS_MAX_HEADER_LIST_SIZE
56+
// see RFC 9113, section 6.5.2.
57+
// https://www.rfc-editor.org/rfc/rfc9113.html#SETTINGS_MAX_HEADER_LIST_SIZE
58+
private final int maxHeaderSize;
59+
60+
// Aggregate size of the field lines (name + value + 32) x N
61+
// that have been parsed and accepted so far.
62+
// This is defined as a long to force promotion to long
63+
// and avoid overflows; see checkNewSize;
64+
private long size;
65+
4966
public MessageHeader () {
67+
this(0);
68+
}
69+
70+
public MessageHeader (int maxHeaderSize) {
71+
this.maxHeaderSize = maxHeaderSize;
5072
grow();
5173
}
5274

5375
public MessageHeader (InputStream is) throws java.io.IOException {
76+
maxHeaderSize = 0;
5477
parseHeader(is);
5578
}
5679

@@ -477,10 +500,28 @@ public static String canonicalID(String id) {
477500
public void parseHeader(InputStream is) throws java.io.IOException {
478501
synchronized (this) {
479502
nkeys = 0;
503+
size = 0;
480504
}
481505
mergeHeader(is);
482506
}
483507

508+
private void checkMaxHeaderSize(int sz) throws ProtocolException {
509+
if (maxHeaderSize > 0) checkNewSize(size, sz, 0);
510+
}
511+
512+
private long checkNewSize(long size, int name, int value) throws ProtocolException {
513+
// See SETTINGS_MAX_HEADER_LIST_SIZE, RFC 9113, section 6.5.2.
514+
long newSize = size + name + value + 32;
515+
if (maxHeaderSize > 0 && newSize > maxHeaderSize) {
516+
Arrays.fill(keys, 0, nkeys, null);
517+
Arrays.fill(values,0, nkeys, null);
518+
nkeys = 0;
519+
throw new ProtocolException(String.format("Header size too big: %s > %s",
520+
newSize, maxHeaderSize));
521+
}
522+
return newSize;
523+
}
524+
484525
/** Parse and merge a MIME header from an input stream. */
485526
@SuppressWarnings("fallthrough")
486527
public void mergeHeader(InputStream is) throws java.io.IOException {
@@ -494,7 +535,15 @@ public void mergeHeader(InputStream is) throws java.io.IOException {
494535
int c;
495536
boolean inKey = firstc > ' ';
496537
s[len++] = (char) firstc;
538+
checkMaxHeaderSize(len);
497539
parseloop:{
540+
// We start parsing for a new name value pair here.
541+
// The max header size includes an overhead of 32 bytes per
542+
// name value pair.
543+
// See SETTINGS_MAX_HEADER_LIST_SIZE, RFC 9113, section 6.5.2.
544+
long maxRemaining = maxHeaderSize > 0
545+
? maxHeaderSize - size - 32
546+
: Long.MAX_VALUE;
498547
while ((c = is.read()) >= 0) {
499548
switch (c) {
500549
case ':':
@@ -528,6 +577,9 @@ public void mergeHeader(InputStream is) throws java.io.IOException {
528577
s = ns;
529578
}
530579
s[len++] = (char) c;
580+
if (maxHeaderSize > 0 && len > maxRemaining) {
581+
checkMaxHeaderSize(len);
582+
}
531583
}
532584
firstc = -1;
533585
}
@@ -549,6 +601,9 @@ public void mergeHeader(InputStream is) throws java.io.IOException {
549601
v = new String();
550602
else
551603
v = String.copyValueOf(s, keyend, len - keyend);
604+
int klen = k == null ? 0 : k.length();
605+
606+
size = checkNewSize(size, klen, v.length());
552607
add(k, v);
553608
}
554609
}

src/java.base/share/classes/sun/net/www/protocol/http/HttpURLConnection.java

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,8 @@ public class HttpURLConnection extends java.net.HttpURLConnection {
171171
*/
172172
private static int bufSize4ES = 0;
173173

174+
private static final int maxHeaderSize;
175+
174176
/*
175177
* Restrict setting of request headers through the public api
176178
* consistent with JavaScript XMLHttpRequest2 with a few
@@ -285,6 +287,19 @@ private static Set<String> schemesListToSet(String list) {
285287
} else {
286288
restrictedHeaderSet = null;
287289
}
290+
291+
int defMaxHeaderSize = 384 * 1024;
292+
String maxHeaderSizeStr = getNetProperty("jdk.http.maxHeaderSize");
293+
int maxHeaderSizeVal = defMaxHeaderSize;
294+
if (maxHeaderSizeStr != null) {
295+
try {
296+
maxHeaderSizeVal = Integer.parseInt(maxHeaderSizeStr);
297+
} catch (NumberFormatException n) {
298+
maxHeaderSizeVal = defMaxHeaderSize;
299+
}
300+
}
301+
if (maxHeaderSizeVal < 0) maxHeaderSizeVal = 0;
302+
maxHeaderSize = maxHeaderSizeVal;
288303
}
289304

290305
static final String httpVersion = "HTTP/1.1";
@@ -759,7 +774,7 @@ private void writeRequests() throws IOException {
759774
}
760775
ps = (PrintStream) http.getOutputStream();
761776
connected=true;
762-
responses = new MessageHeader();
777+
responses = new MessageHeader(maxHeaderSize);
763778
setRequests=false;
764779
writeRequests();
765780
}
@@ -917,7 +932,7 @@ protected HttpURLConnection(URL u, Proxy p, Handler handler)
917932
throws IOException {
918933
super(checkURL(u));
919934
requests = new MessageHeader();
920-
responses = new MessageHeader();
935+
responses = new MessageHeader(maxHeaderSize);
921936
userHeaders = new MessageHeader();
922937
this.handler = handler;
923938
instProxy = p;
@@ -2872,7 +2887,7 @@ private boolean followRedirect0(String loc, int stat, URL locUrl)
28722887
}
28732888

28742889
// clear out old response headers!!!!
2875-
responses = new MessageHeader();
2890+
responses = new MessageHeader(maxHeaderSize);
28762891
if (stat == HTTP_USE_PROXY) {
28772892
/* This means we must re-request the resource through the
28782893
* proxy denoted in the "Location:" field of the response.
@@ -3062,7 +3077,7 @@ private void reset() throws IOException {
30623077
} catch (IOException e) { }
30633078
}
30643079
responseCode = -1;
3065-
responses = new MessageHeader();
3080+
responses = new MessageHeader(maxHeaderSize);
30663081
connected = false;
30673082
}
30683083

src/java.base/share/conf/net.properties

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,3 +130,20 @@ jdk.http.auth.tunneling.disabledSchemes=Basic
130130
#jdk.http.ntlm.transparentAuth=trustedHosts
131131
#
132132
jdk.http.ntlm.transparentAuth=disabled
133+
134+
#
135+
# Maximum HTTP field section size that a client is prepared to accept
136+
#
137+
# jdk.http.maxHeaderSize=393216
138+
#
139+
# This is the maximum header field section size that a client is prepared to accept.
140+
# This is computed as the sum of the size of the uncompressed header name, plus
141+
# the size of the uncompressed header value, plus an overhead of 32 bytes for
142+
# each field section line. If a peer sends a field section that exceeds this
143+
# size a {@link java.net.ProtocolException ProtocolException} will be raised.
144+
# This applies to all versions of the HTTP protocol. A value of zero or a negative
145+
# value means no limit. If left unspecified, the default value is 393216 bytes
146+
# or 384kB.
147+
#
148+
# Note: This property is currently used by the JDK Reference implementation. It
149+
# is not guaranteed to be examined and used by other implementations.

src/java.net.http/share/classes/jdk/internal/net/http/Exchange.java

Lines changed: 30 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright (c) 2015, 2023, Oracle and/or its affiliates. All rights reserved.
2+
* Copyright (c) 2015, 2024, Oracle and/or its affiliates. All rights reserved.
33
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
44
*
55
* This code is free software; you can redistribute it and/or modify it
@@ -40,6 +40,7 @@
4040
import java.util.Optional;
4141
import java.util.concurrent.CompletableFuture;
4242
import java.util.concurrent.Executor;
43+
import java.util.concurrent.atomic.AtomicInteger;
4344
import java.util.function.Function;
4445
import java.net.http.HttpClient;
4546
import java.net.http.HttpHeaders;
@@ -68,6 +69,8 @@
6869
*/
6970
final class Exchange<T> {
7071

72+
static final int MAX_NON_FINAL_RESPONSES =
73+
Utils.getIntegerNetProperty("jdk.httpclient.maxNonFinalResponses", 8);
7174
final Logger debug = Utils.getDebugLogger(this::dbgString, Utils.DEBUG);
7275

7376
final HttpRequestImpl request;
@@ -92,6 +95,8 @@ final class Exchange<T> {
9295
// exchange so that it can be aborted/timed out mid setup.
9396
final ConnectionAborter connectionAborter = new ConnectionAborter();
9497

98+
final AtomicInteger nonFinalResponses = new AtomicInteger();
99+
95100
Exchange(HttpRequestImpl request, MultiExchange<T> multi) {
96101
this.request = request;
97102
this.upgrading = false;
@@ -315,7 +320,7 @@ <T> CompletableFuture<T> checkCancelled(CompletableFuture<T> cf, HttpConnection
315320

316321
public void h2Upgrade() {
317322
upgrading = true;
318-
request.setH2Upgrade(client.client2());
323+
request.setH2Upgrade(this);
319324
}
320325

321326
synchronized IOException getCancelCause() {
@@ -416,6 +421,7 @@ private CompletableFuture<Response> expectContinue(ExchangeImpl<T> ex) {
416421
Log.logResponse(r1::toString);
417422
int rcode = r1.statusCode();
418423
if (rcode == 100) {
424+
nonFinalResponses.incrementAndGet();
419425
Log.logTrace("Received 100-Continue: sending body");
420426
if (debug.on()) debug.log("Received 100-Continue for %s", r1);
421427
CompletableFuture<Response> cf =
@@ -492,12 +498,20 @@ private CompletableFuture<Response> ignore1xxResponse(final Response rsp) {
492498
+ rsp.statusCode());
493499
}
494500
assert exchImpl != null : "Illegal state - current exchange isn't set";
495-
// ignore this Response and wait again for the subsequent response headers
496-
final CompletableFuture<Response> cf = exchImpl.getResponseAsync(parentExecutor);
497-
// we recompose the CF again into the ignore1xxResponse check/function because
498-
// the 1xx response is allowed to be sent multiple times for a request, before
499-
// a final response arrives
500-
return cf.thenCompose(this::ignore1xxResponse);
501+
int count = nonFinalResponses.incrementAndGet();
502+
if (MAX_NON_FINAL_RESPONSES > 0 && (count < 0 || count > MAX_NON_FINAL_RESPONSES)) {
503+
return MinimalFuture.failedFuture(
504+
new ProtocolException(String.format(
505+
"Too many interim responses received: %s > %s",
506+
count, MAX_NON_FINAL_RESPONSES)));
507+
} else {
508+
// ignore this Response and wait again for the subsequent response headers
509+
final CompletableFuture<Response> cf = exchImpl.getResponseAsync(parentExecutor);
510+
// we recompose the CF again into the ignore1xxResponse check/function because
511+
// the 1xx response is allowed to be sent multiple times for a request, before
512+
// a final response arrives
513+
return cf.thenCompose(this::ignore1xxResponse);
514+
}
501515
} else {
502516
// return the already completed future
503517
return MinimalFuture.completedFuture(rsp);
@@ -759,6 +773,14 @@ HttpClient.Version version() {
759773
return multi.version();
760774
}
761775

776+
boolean pushEnabled() {
777+
return pushGroup != null;
778+
}
779+
780+
String h2cSettingsStrings() {
781+
return client.client2().getSettingsString(pushEnabled());
782+
}
783+
762784
String dbgString() {
763785
return dbgTag;
764786
}

0 commit comments

Comments
 (0)