Skip to content

Commit 03bc6b3

Browse files
dfuchjaikiran
authored andcommittedOct 15, 2024
8328286: Enhance HTTP client
Reviewed-by: aefimov, michaelm
1 parent 893e7bc commit 03bc6b3

29 files changed

+1125
-184
lines changed
 

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

+9
Original file line numberDiff line numberDiff line change
@@ -253,6 +253,15 @@ <H2>Misc HTTP URL stream protocol handler properties</H2>
253253
</OL>
254254
<P>The channel binding tokens generated are of the type "tls-server-end-point" as defined in
255255
RFC 5929.</P>
256+
257+
<LI><P><B>{@systemProperty jdk.http.maxHeaderSize}</B> (default: 393216 or 384kB)<BR>
258+
This is the maximum header field section size that a client is prepared to accept.
259+
This is computed as the sum of the size of the uncompressed header name, plus
260+
the size of the uncompressed header value, plus an overhead of 32 bytes for
261+
each field section line. If a peer sends a field section that exceeds this
262+
size a {@link java.net.ProtocolException ProtocolException} will be raised.
263+
This applies to all versions of the HTTP protocol. A value of zero or a negative
264+
value means no limit. If left unspecified, the default value is 393216 bytes.
256265
</UL>
257266
<P>All these properties are checked only once at startup.</P>
258267
<a id="AddressCache"></a>

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

+55
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

@@ -45,11 +47,32 @@ public final class MessageHeader {
4547
private String[] values;
4648
private int nkeys;
4749

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

5274
public MessageHeader (InputStream is) throws java.io.IOException {
75+
maxHeaderSize = 0;
5376
parseHeader(is);
5477
}
5578

@@ -476,10 +499,28 @@ public static String canonicalID(String id) {
476499
public void parseHeader(InputStream is) throws java.io.IOException {
477500
synchronized (this) {
478501
nkeys = 0;
502+
size = 0;
479503
}
480504
mergeHeader(is);
481505
}
482506

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

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

+19-4
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,8 @@ public class HttpURLConnection extends java.net.HttpURLConnection {
172172
*/
173173
private static final int bufSize4ES;
174174

175+
private static final int maxHeaderSize;
176+
175177
/*
176178
* Restrict setting of request headers through the public api
177179
* consistent with JavaScript XMLHttpRequest2 with a few
@@ -288,6 +290,19 @@ private static Set<String> schemesListToSet(String list) {
288290
} else {
289291
restrictedHeaderSet = null;
290292
}
293+
294+
int defMaxHeaderSize = 384 * 1024;
295+
String maxHeaderSizeStr = getNetProperty("jdk.http.maxHeaderSize");
296+
int maxHeaderSizeVal = defMaxHeaderSize;
297+
if (maxHeaderSizeStr != null) {
298+
try {
299+
maxHeaderSizeVal = Integer.parseInt(maxHeaderSizeStr);
300+
} catch (NumberFormatException n) {
301+
maxHeaderSizeVal = defMaxHeaderSize;
302+
}
303+
}
304+
if (maxHeaderSizeVal < 0) maxHeaderSizeVal = 0;
305+
maxHeaderSize = maxHeaderSizeVal;
291306
}
292307

293308
static final String httpVersion = "HTTP/1.1";
@@ -754,7 +769,7 @@ private void writeRequests() throws IOException {
754769
}
755770
ps = (PrintStream) http.getOutputStream();
756771
connected=true;
757-
responses = new MessageHeader();
772+
responses = new MessageHeader(maxHeaderSize);
758773
setRequests=false;
759774
writeRequests();
760775
}
@@ -912,7 +927,7 @@ protected HttpURLConnection(URL u, Proxy p, Handler handler)
912927
throws IOException {
913928
super(checkURL(u));
914929
requests = new MessageHeader();
915-
responses = new MessageHeader();
930+
responses = new MessageHeader(maxHeaderSize);
916931
userHeaders = new MessageHeader();
917932
this.handler = handler;
918933
instProxy = p;
@@ -2810,7 +2825,7 @@ private boolean followRedirect0(String loc, int stat, URL locUrl)
28102825
}
28112826

28122827
// clear out old response headers!!!!
2813-
responses = new MessageHeader();
2828+
responses = new MessageHeader(maxHeaderSize);
28142829
if (stat == HTTP_USE_PROXY) {
28152830
/* This means we must re-request the resource through the
28162831
* proxy denoted in the "Location:" field of the response.
@@ -3000,7 +3015,7 @@ private void reset() throws IOException {
30003015
} catch (IOException e) { }
30013016
}
30023017
responseCode = -1;
3003-
responses = new MessageHeader();
3018+
responses = new MessageHeader(maxHeaderSize);
30043019
connected = false;
30053020
}
30063021

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

+17
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

+33-12
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
import java.util.concurrent.Executor;
4242
import java.util.concurrent.TimeUnit;
4343
import java.util.concurrent.TimeoutException;
44+
import java.util.concurrent.atomic.AtomicInteger;
4445
import java.util.function.Function;
4546
import java.net.http.HttpClient;
4647
import java.net.http.HttpHeaders;
@@ -69,6 +70,8 @@
6970
*/
7071
final class Exchange<T> {
7172

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

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

99+
final AtomicInteger nonFinalResponses = new AtomicInteger();
100+
96101
Exchange(HttpRequestImpl request, MultiExchange<T> multi) {
97102
this.request = request;
98103
this.upgrading = false;
@@ -359,7 +364,7 @@ <T> CompletableFuture<T> checkCancelled(CompletableFuture<T> cf, HttpConnection
359364

360365
public void h2Upgrade() {
361366
upgrading = true;
362-
request.setH2Upgrade(client.client2());
367+
request.setH2Upgrade(this);
363368
}
364369

365370
synchronized IOException getCancelCause() {
@@ -482,19 +487,19 @@ private CompletableFuture<Response> expectContinue(ExchangeImpl<T> ex) {
482487
Log.logResponse(r1::toString);
483488
int rcode = r1.statusCode();
484489
if (rcode == 100) {
490+
nonFinalResponses.incrementAndGet();
485491
Log.logTrace("Received 100-Continue: sending body");
486-
if (debug.on())
487-
debug.log("Received 100-Continue for %s", r1);
492+
if (debug.on()) debug.log("Received 100-Continue for %s", r1);
488493
CompletableFuture<Response> cf =
489494
exchImpl.sendBodyAsync()
490495
.thenCompose(exIm -> exIm.getResponseAsync(parentExecutor));
491496
cf = wrapForUpgrade(cf);
492497
cf = wrapForLog(cf);
493498
return cf;
494499
} else {
495-
Log.logTrace("Expectation failed: Received {0}", rcode);
496-
if (debug.on())
497-
debug.log("Expect-Continue failed (%d) for: %s", rcode, r1);
500+
Log.logTrace("Expectation failed: Received {0}",
501+
rcode);
502+
if (debug.on()) debug.log("Expect-Continue failed (%d) for: %s", rcode, r1);
498503
if (upgrading && rcode == 101) {
499504
IOException failed = new IOException(
500505
"Unable to handle 101 while waiting for 100");
@@ -559,12 +564,20 @@ private CompletableFuture<Response> ignore1xxResponse(final Response rsp) {
559564
+ rsp.statusCode());
560565
}
561566
assert exchImpl != null : "Illegal state - current exchange isn't set";
562-
// ignore this Response and wait again for the subsequent response headers
563-
final CompletableFuture<Response> cf = exchImpl.getResponseAsync(parentExecutor);
564-
// we recompose the CF again into the ignore1xxResponse check/function because
565-
// the 1xx response is allowed to be sent multiple times for a request, before
566-
// a final response arrives
567-
return cf.thenCompose(this::ignore1xxResponse);
567+
int count = nonFinalResponses.incrementAndGet();
568+
if (MAX_NON_FINAL_RESPONSES > 0 && (count < 0 || count > MAX_NON_FINAL_RESPONSES)) {
569+
return MinimalFuture.failedFuture(
570+
new ProtocolException(String.format(
571+
"Too many interim responses received: %s > %s",
572+
count, MAX_NON_FINAL_RESPONSES)));
573+
} else {
574+
// ignore this Response and wait again for the subsequent response headers
575+
final CompletableFuture<Response> cf = exchImpl.getResponseAsync(parentExecutor);
576+
// we recompose the CF again into the ignore1xxResponse check/function because
577+
// the 1xx response is allowed to be sent multiple times for a request, before
578+
// a final response arrives
579+
return cf.thenCompose(this::ignore1xxResponse);
580+
}
568581
} else {
569582
// return the already completed future
570583
return MinimalFuture.completedFuture(rsp);
@@ -829,6 +842,14 @@ HttpClient.Version version() {
829842
return multi.version();
830843
}
831844

845+
boolean pushEnabled() {
846+
return pushGroup != null;
847+
}
848+
849+
String h2cSettingsStrings() {
850+
return client.client2().getSettingsString(pushEnabled());
851+
}
852+
832853
String dbgString() {
833854
return dbgTag;
834855
}

0 commit comments

Comments
 (0)
Please sign in to comment.