8328286: Enhance HTTP client

Reviewed-by: aefimov, michaelm
This commit is contained in:
Daniel Fuchs 2024-05-07 19:29:49 +00:00 committed by Jaikiran Pai
parent 893e7bc894
commit 03bc6b359f
29 changed files with 1126 additions and 185 deletions

View File

@ -253,6 +253,15 @@ to determine the proxy that should be used for connecting to a given URI.</P>
</OL>
<P>The channel binding tokens generated are of the type "tls-server-end-point" as defined in
RFC 5929.</P>
<LI><P><B>{@systemProperty jdk.http.maxHeaderSize}</B> (default: 393216 or 384kB)<BR>
This is the maximum header field section size that a client is prepared to accept.
This is computed as the sum of the size of the uncompressed header name, plus
the size of the uncompressed header value, plus an overhead of 32 bytes for
each field section line. If a peer sends a field section that exceeds this
size a {@link java.net.ProtocolException ProtocolException} will be raised.
This applies to all versions of the HTTP protocol. A value of zero or a negative
value means no limit. If left unspecified, the default value is 393216 bytes.
</UL>
<P>All these properties are checked only once at startup.</P>
<a id="AddressCache"></a>

View File

@ -30,6 +30,8 @@
package sun.net.www;
import java.io.*;
import java.lang.reflect.Array;
import java.net.ProtocolException;
import java.util.Collections;
import java.util.*;
@ -45,11 +47,32 @@ public final class MessageHeader {
private String[] values;
private int nkeys;
// max number of bytes for headers, <=0 means unlimited;
// this corresponds to the length of the names, plus the length
// of the values, plus an overhead of 32 bytes per name: value
// pair.
// Note: we use the same definition as HTTP/2 SETTINGS_MAX_HEADER_LIST_SIZE
// see RFC 9113, section 6.5.2.
// https://www.rfc-editor.org/rfc/rfc9113.html#SETTINGS_MAX_HEADER_LIST_SIZE
private final int maxHeaderSize;
// Aggregate size of the field lines (name + value + 32) x N
// that have been parsed and accepted so far.
// This is defined as a long to force promotion to long
// and avoid overflows; see checkNewSize;
private long size;
public MessageHeader () {
this(0);
}
public MessageHeader (int maxHeaderSize) {
this.maxHeaderSize = maxHeaderSize;
grow();
}
public MessageHeader (InputStream is) throws java.io.IOException {
maxHeaderSize = 0;
parseHeader(is);
}
@ -476,10 +499,28 @@ public final class MessageHeader {
public void parseHeader(InputStream is) throws java.io.IOException {
synchronized (this) {
nkeys = 0;
size = 0;
}
mergeHeader(is);
}
private void checkMaxHeaderSize(int sz) throws ProtocolException {
if (maxHeaderSize > 0) checkNewSize(size, sz, 0);
}
private long checkNewSize(long size, int name, int value) throws ProtocolException {
// See SETTINGS_MAX_HEADER_LIST_SIZE, RFC 9113, section 6.5.2.
long newSize = size + name + value + 32;
if (maxHeaderSize > 0 && newSize > maxHeaderSize) {
Arrays.fill(keys, 0, nkeys, null);
Arrays.fill(values,0, nkeys, null);
nkeys = 0;
throw new ProtocolException(String.format("Header size too big: %s > %s",
newSize, maxHeaderSize));
}
return newSize;
}
/** Parse and merge a MIME header from an input stream. */
@SuppressWarnings("fallthrough")
public void mergeHeader(InputStream is) throws java.io.IOException {
@ -493,7 +534,15 @@ public final class MessageHeader {
int c;
boolean inKey = firstc > ' ';
s[len++] = (char) firstc;
checkMaxHeaderSize(len);
parseloop:{
// We start parsing for a new name value pair here.
// The max header size includes an overhead of 32 bytes per
// name value pair.
// See SETTINGS_MAX_HEADER_LIST_SIZE, RFC 9113, section 6.5.2.
long maxRemaining = maxHeaderSize > 0
? maxHeaderSize - size - 32
: Long.MAX_VALUE;
while ((c = is.read()) >= 0) {
switch (c) {
case ':':
@ -527,6 +576,9 @@ public final class MessageHeader {
s = ns;
}
s[len++] = (char) c;
if (maxHeaderSize > 0 && len > maxRemaining) {
checkMaxHeaderSize(len);
}
}
firstc = -1;
}
@ -548,6 +600,9 @@ public final class MessageHeader {
v = new String();
else
v = String.copyValueOf(s, keyend, len - keyend);
int klen = k == null ? 0 : k.length();
size = checkNewSize(size, klen, v.length());
add(k, v);
}
}

View File

@ -172,6 +172,8 @@ public class HttpURLConnection extends java.net.HttpURLConnection {
*/
private static final int bufSize4ES;
private static final int maxHeaderSize;
/*
* Restrict setting of request headers through the public api
* consistent with JavaScript XMLHttpRequest2 with a few
@ -288,6 +290,19 @@ public class HttpURLConnection extends java.net.HttpURLConnection {
} else {
restrictedHeaderSet = null;
}
int defMaxHeaderSize = 384 * 1024;
String maxHeaderSizeStr = getNetProperty("jdk.http.maxHeaderSize");
int maxHeaderSizeVal = defMaxHeaderSize;
if (maxHeaderSizeStr != null) {
try {
maxHeaderSizeVal = Integer.parseInt(maxHeaderSizeStr);
} catch (NumberFormatException n) {
maxHeaderSizeVal = defMaxHeaderSize;
}
}
if (maxHeaderSizeVal < 0) maxHeaderSizeVal = 0;
maxHeaderSize = maxHeaderSizeVal;
}
static final String httpVersion = "HTTP/1.1";
@ -754,7 +769,7 @@ public class HttpURLConnection extends java.net.HttpURLConnection {
}
ps = (PrintStream) http.getOutputStream();
connected=true;
responses = new MessageHeader();
responses = new MessageHeader(maxHeaderSize);
setRequests=false;
writeRequests();
}
@ -912,7 +927,7 @@ public class HttpURLConnection extends java.net.HttpURLConnection {
throws IOException {
super(checkURL(u));
requests = new MessageHeader();
responses = new MessageHeader();
responses = new MessageHeader(maxHeaderSize);
userHeaders = new MessageHeader();
this.handler = handler;
instProxy = p;
@ -2810,7 +2825,7 @@ public class HttpURLConnection extends java.net.HttpURLConnection {
}
// clear out old response headers!!!!
responses = new MessageHeader();
responses = new MessageHeader(maxHeaderSize);
if (stat == HTTP_USE_PROXY) {
/* This means we must re-request the resource through the
* proxy denoted in the "Location:" field of the response.
@ -3000,7 +3015,7 @@ public class HttpURLConnection extends java.net.HttpURLConnection {
} catch (IOException e) { }
}
responseCode = -1;
responses = new MessageHeader();
responses = new MessageHeader(maxHeaderSize);
connected = false;
}

View File

@ -130,3 +130,20 @@ jdk.http.auth.tunneling.disabledSchemes=Basic
#jdk.http.ntlm.transparentAuth=trustedHosts
#
jdk.http.ntlm.transparentAuth=disabled
#
# Maximum HTTP field section size that a client is prepared to accept
#
# jdk.http.maxHeaderSize=393216
#
# This is the maximum header field section size that a client is prepared to accept.
# This is computed as the sum of the size of the uncompressed header name, plus
# the size of the uncompressed header value, plus an overhead of 32 bytes for
# each field section line. If a peer sends a field section that exceeds this
# size a {@link java.net.ProtocolException ProtocolException} will be raised.
# This applies to all versions of the HTTP protocol. A value of zero or a negative
# value means no limit. If left unspecified, the default value is 393216 bytes
# or 384kB.
#
# Note: This property is currently used by the JDK Reference implementation. It
# is not guaranteed to be examined and used by other implementations.

View File

@ -41,6 +41,7 @@ import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Function;
import java.net.http.HttpClient;
import java.net.http.HttpHeaders;
@ -69,6 +70,8 @@ import static jdk.internal.net.http.common.Utils.permissionForProxy;
*/
final class Exchange<T> {
static final int MAX_NON_FINAL_RESPONSES =
Utils.getIntegerNetProperty("jdk.httpclient.maxNonFinalResponses", 8);
final Logger debug = Utils.getDebugLogger(this::dbgString, Utils.DEBUG);
final HttpRequestImpl request;
@ -93,6 +96,8 @@ final class Exchange<T> {
// exchange so that it can be aborted/timed out mid setup.
final ConnectionAborter connectionAborter = new ConnectionAborter();
final AtomicInteger nonFinalResponses = new AtomicInteger();
Exchange(HttpRequestImpl request, MultiExchange<T> multi) {
this.request = request;
this.upgrading = false;
@ -359,7 +364,7 @@ final class Exchange<T> {
public void h2Upgrade() {
upgrading = true;
request.setH2Upgrade(client.client2());
request.setH2Upgrade(this);
}
synchronized IOException getCancelCause() {
@ -482,9 +487,9 @@ final class Exchange<T> {
Log.logResponse(r1::toString);
int rcode = r1.statusCode();
if (rcode == 100) {
nonFinalResponses.incrementAndGet();
Log.logTrace("Received 100-Continue: sending body");
if (debug.on())
debug.log("Received 100-Continue for %s", r1);
if (debug.on()) debug.log("Received 100-Continue for %s", r1);
CompletableFuture<Response> cf =
exchImpl.sendBodyAsync()
.thenCompose(exIm -> exIm.getResponseAsync(parentExecutor));
@ -492,9 +497,9 @@ final class Exchange<T> {
cf = wrapForLog(cf);
return cf;
} else {
Log.logTrace("Expectation failed: Received {0}", rcode);
if (debug.on())
debug.log("Expect-Continue failed (%d) for: %s", rcode, r1);
Log.logTrace("Expectation failed: Received {0}",
rcode);
if (debug.on()) debug.log("Expect-Continue failed (%d) for: %s", rcode, r1);
if (upgrading && rcode == 101) {
IOException failed = new IOException(
"Unable to handle 101 while waiting for 100");
@ -559,12 +564,20 @@ final class Exchange<T> {
+ rsp.statusCode());
}
assert exchImpl != null : "Illegal state - current exchange isn't set";
// ignore this Response and wait again for the subsequent response headers
final CompletableFuture<Response> cf = exchImpl.getResponseAsync(parentExecutor);
// we recompose the CF again into the ignore1xxResponse check/function because
// the 1xx response is allowed to be sent multiple times for a request, before
// a final response arrives
return cf.thenCompose(this::ignore1xxResponse);
int count = nonFinalResponses.incrementAndGet();
if (MAX_NON_FINAL_RESPONSES > 0 && (count < 0 || count > MAX_NON_FINAL_RESPONSES)) {
return MinimalFuture.failedFuture(
new ProtocolException(String.format(
"Too many interim responses received: %s > %s",
count, MAX_NON_FINAL_RESPONSES)));
} else {
// ignore this Response and wait again for the subsequent response headers
final CompletableFuture<Response> cf = exchImpl.getResponseAsync(parentExecutor);
// we recompose the CF again into the ignore1xxResponse check/function because
// the 1xx response is allowed to be sent multiple times for a request, before
// a final response arrives
return cf.thenCompose(this::ignore1xxResponse);
}
} else {
// return the already completed future
return MinimalFuture.completedFuture(rsp);
@ -829,6 +842,14 @@ final class Exchange<T> {
return multi.version();
}
boolean pushEnabled() {
return pushGroup != null;
}
String h2cSettingsStrings() {
return client.client2().getSettingsString(pushEnabled());
}
String dbgString() {
return dbgTag;
}

View File

@ -25,6 +25,7 @@
package jdk.internal.net.http;
import java.io.IOException;
import java.net.ProtocolException;
import java.nio.BufferUnderflowException;
import java.nio.ByteBuffer;
@ -53,6 +54,12 @@ class Http1HeaderParser {
private int responseCode;
private HttpHeaders headers;
private Map<String,List<String>> privateMap = new HashMap<>();
private long size;
private static final int K = 1024;
private static final int MAX_HTTP_HEADER_SIZE = Utils.getIntegerNetProperty(
"jdk.http.maxHeaderSize",
Integer.MIN_VALUE, Integer.MAX_VALUE, 384 * K, true);
enum State { INITIAL,
STATUS_LINE,
@ -164,11 +171,16 @@ class Http1HeaderParser {
return (char)(input.get() & 0xFF);
}
private void readResumeStatusLine(ByteBuffer input) {
private void readResumeStatusLine(ByteBuffer input) throws ProtocolException {
final long max = MAX_HTTP_HEADER_SIZE - size - 32 - sb.length();
int count = 0;
char c = 0;
while (input.hasRemaining() && (c = get(input)) != CR) {
if (c == LF) break;
sb.append(c);
if (++count > max) {
checkMaxHeaderSize(sb.length());
}
}
if (c == CR) {
state = State.STATUS_LINE_FOUND_CR;
@ -185,6 +197,7 @@ class Http1HeaderParser {
}
statusLine = sb.toString();
size = size + 32 + statusLine.length();
sb = new StringBuilder();
if (!statusLine.startsWith("HTTP/1.")) {
throw protocolException("Invalid status line: \"%s\"", statusLine);
@ -205,7 +218,23 @@ class Http1HeaderParser {
state = State.STATUS_LINE_END;
}
private void maybeStartHeaders(ByteBuffer input) {
private void checkMaxHeaderSize(int sz) throws ProtocolException {
long s = size + sz + 32;
if (MAX_HTTP_HEADER_SIZE > 0 && s > MAX_HTTP_HEADER_SIZE) {
throw new ProtocolException(String.format("Header size too big: %s > %s",
s, MAX_HTTP_HEADER_SIZE));
}
}
static private long newSize(long size, int name, int value) throws ProtocolException {
long newSize = size + name + value + 32;
if (MAX_HTTP_HEADER_SIZE > 0 && newSize > MAX_HTTP_HEADER_SIZE) {
throw new ProtocolException(String.format("Header size too big: %s > %s",
newSize, MAX_HTTP_HEADER_SIZE));
}
return newSize;
}
private void maybeStartHeaders(ByteBuffer input) throws ProtocolException {
assert state == State.STATUS_LINE_END;
assert sb.length() == 0;
char c = get(input);
@ -215,6 +244,7 @@ class Http1HeaderParser {
state = State.STATUS_LINE_END_LF;
} else {
sb.append(c);
checkMaxHeaderSize(sb.length());
state = State.HEADER;
}
}
@ -232,9 +262,11 @@ class Http1HeaderParser {
}
}
private void readResumeHeader(ByteBuffer input) {
private void readResumeHeader(ByteBuffer input) throws ProtocolException {
assert state == State.HEADER;
assert input.hasRemaining();
final long max = MAX_HTTP_HEADER_SIZE - size - 32 - sb.length();
int count = 0;
while (input.hasRemaining()) {
char c = get(input);
if (c == CR) {
@ -248,6 +280,9 @@ class Http1HeaderParser {
if (c == HT)
c = SP;
sb.append(c);
if (++count > max) {
checkMaxHeaderSize(sb.length());
}
}
}
@ -268,12 +303,12 @@ class Http1HeaderParser {
if (!Utils.isValidValue(value)) {
throw protocolException("Invalid header value \"%s: %s\"", name, value);
}
size = newSize(size, name.length(), value.length());
privateMap.computeIfAbsent(name.toLowerCase(Locale.US),
k -> new ArrayList<>()).add(value);
}
private void resumeOrLF(ByteBuffer input) {
private void resumeOrLF(ByteBuffer input) throws ProtocolException {
assert state == State.HEADER_FOUND_CR || state == State.HEADER_FOUND_LF;
char c = state == State.HEADER_FOUND_LF ? LF : get(input);
if (c == LF) {
@ -283,10 +318,12 @@ class Http1HeaderParser {
state = State.HEADER_FOUND_CR_LF;
} else if (c == SP || c == HT) {
sb.append(SP); // parity with MessageHeaders
checkMaxHeaderSize(sb.length());
state = State.HEADER;
} else {
sb = new StringBuilder();
sb.append(c);
checkMaxHeaderSize(1);
state = State.HEADER;
}
}
@ -312,6 +349,7 @@ class Http1HeaderParser {
} else if (c == SP || c == HT) {
assert sb.length() != 0;
sb.append(SP); // continuation line
checkMaxHeaderSize(sb.length());
state = State.HEADER;
} else {
if (sb.length() > 0) {
@ -322,6 +360,7 @@ class Http1HeaderParser {
addHeaderFromString(headerString);
}
sb.append(c);
checkMaxHeaderSize(sb.length());
state = State.HEADER;
}
}

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2015, 2023, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2015, 2024, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
@ -36,7 +36,6 @@ import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.locks.ReentrantLock;
import jdk.internal.net.http.common.Log;
import jdk.internal.net.http.common.Logger;
import jdk.internal.net.http.common.MinimalFuture;
import jdk.internal.net.http.common.Utils;
@ -46,6 +45,7 @@ import static jdk.internal.net.http.frame.SettingsFrame.ENABLE_PUSH;
import static jdk.internal.net.http.frame.SettingsFrame.HEADER_TABLE_SIZE;
import static jdk.internal.net.http.frame.SettingsFrame.MAX_CONCURRENT_STREAMS;
import static jdk.internal.net.http.frame.SettingsFrame.MAX_FRAME_SIZE;
import static jdk.internal.net.http.frame.SettingsFrame.MAX_HEADER_LIST_SIZE;
/**
* Http2 specific aspects of HttpClientImpl
@ -98,16 +98,20 @@ class Http2ClientImpl {
CompletableFuture<Http2Connection> getConnectionFor(HttpRequestImpl req,
Exchange<?> exchange) {
String key = Http2Connection.keyFor(req);
boolean pushEnabled = exchange.pushEnabled();
connectionPoolLock.lock();
try {
Http2Connection connection = connections.get(key);
if (connection != null) {
try {
if (!connection.tryReserveForPoolCheckout() || !connection.reserveStream(true)) {
if (!connection.tryReserveForPoolCheckout()
|| !connection.reserveStream(true, pushEnabled)) {
if (debug.on())
debug.log("removing connection from pool since it couldn't be" +
" reserved for use: %s", connection);
" reserved for use%s: %s",
pushEnabled ? " with server push enabled" : "",
connection);
removeFromPool(connection);
} else {
// fast path if connection already exists
@ -137,7 +141,7 @@ class Http2ClientImpl {
try {
if (conn != null) {
try {
conn.reserveStream(true);
conn.reserveStream(true, exchange.pushEnabled());
} catch (IOException e) {
throw new UncheckedIOException(e); // shouldn't happen
}
@ -183,10 +187,21 @@ class Http2ClientImpl {
}
Http2Connection c1 = connections.putIfAbsent(key, c);
if (c1 != null) {
c.setFinalStream();
if (debug.on())
debug.log("existing entry in connection pool for %s", key);
return false;
if (c.serverPushEnabled() && !c1.serverPushEnabled()) {
c1.setFinalStream();
connections.remove(key, c1);
connections.put(key, c);
if (debug.on()) {
debug.log("Replacing %s with %s in connection pool", c1, c);
}
if (c1.shouldClose()) c1.close();
return true;
} else {
c.setFinalStream();
if (debug.on())
debug.log("existing entry in connection pool for %s", key);
return false;
}
}
if (debug.on())
debug.log("put in the connection pool: %s", c);
@ -250,8 +265,8 @@ class Http2ClientImpl {
}
/** Returns the client settings as a base64 (url) encoded string */
String getSettingsString() {
SettingsFrame sf = getClientSettings();
String getSettingsString(boolean defaultServerPush) {
SettingsFrame sf = getClientSettings(defaultServerPush);
byte[] settings = sf.toByteArray(); // without the header
Base64.Encoder encoder = Base64.getUrlEncoder()
.withoutPadding();
@ -261,14 +276,7 @@ class Http2ClientImpl {
private static final int K = 1024;
private static int getParameter(String property, int min, int max, int defaultValue) {
int value = Utils.getIntegerNetProperty(property, defaultValue);
// use default value if misconfigured
if (value < min || value > max) {
Log.logError("Property value for {0}={1} not in [{2}..{3}]: " +
"using default={4}", property, value, min, max, defaultValue);
value = defaultValue;
}
return value;
return Utils.getIntegerNetProperty(property, min, max, defaultValue, true);
}
// used for the connection window, to have a connection window size
@ -288,7 +296,18 @@ class Http2ClientImpl {
streamWindow, Integer.MAX_VALUE, defaultValue);
}
SettingsFrame getClientSettings() {
/**
* This method is used to test whether pushes are globally
* disabled on all connections.
* @return true if pushes are globally disabled on all connections
*/
boolean serverPushDisabled() {
return getParameter(
"jdk.httpclient.enablepush",
0, 1, 1) == 0;
}
SettingsFrame getClientSettings(boolean defaultServerPush) {
SettingsFrame frame = new SettingsFrame();
// default defined for HTTP/2 is 4 K, we use 16 K.
frame.setParameter(HEADER_TABLE_SIZE, getParameter(
@ -297,14 +316,15 @@ class Http2ClientImpl {
// O: does not accept push streams. 1: accepts push streams.
frame.setParameter(ENABLE_PUSH, getParameter(
"jdk.httpclient.enablepush",
0, 1, 1));
0, 1, defaultServerPush ? 1 : 0));
// HTTP/2 recommends to set the number of concurrent streams
// no lower than 100. We use 100. 0 means no stream would be
// accepted. That would render the client to be non functional,
// so we won't let 0 be configured for our Http2ClientImpl.
// no lower than 100. We use 100, unless push promises are
// disabled.
int initialServerStreams = frame.getParameter(ENABLE_PUSH) == 0
? 0 : 100;
frame.setParameter(MAX_CONCURRENT_STREAMS, getParameter(
"jdk.httpclient.maxstreams",
1, Integer.MAX_VALUE, 100));
0, Integer.MAX_VALUE, initialServerStreams));
// Maximum size is 2^31-1. Don't allow window size to be less
// than the minimum frame size as this is likely to be a
// configuration error. HTTP/2 specify a default of 64 * K -1,
@ -317,6 +337,14 @@ class Http2ClientImpl {
frame.setParameter(MAX_FRAME_SIZE, getParameter(
"jdk.httpclient.maxframesize",
16 * K, 16 * K * K -1, 16 * K));
// Maximum field section size we're prepared to accept
// This is the uncompressed name + value size + 32 per field line
int maxHeaderSize = getParameter(
"jdk.http.maxHeaderSize",
Integer.MIN_VALUE, Integer.MAX_VALUE, 384 * K);
// If the property is <= 0 the value is unlimited
if (maxHeaderSize <= 0) maxHeaderSize = -1;
frame.setParameter(MAX_HEADER_LIST_SIZE, maxHeaderSize);
return frame;
}

View File

@ -31,6 +31,7 @@ import java.io.UncheckedIOException;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.VarHandle;
import java.net.InetSocketAddress;
import java.net.ProtocolException;
import java.net.http.HttpClient;
import java.net.http.HttpHeaders;
import java.nio.ByteBuffer;
@ -49,6 +50,7 @@ import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.Flow;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.function.Function;
@ -88,10 +90,12 @@ import jdk.internal.net.http.hpack.DecodingCallback;
import jdk.internal.net.http.hpack.Encoder;
import static java.nio.charset.StandardCharsets.UTF_8;
import static jdk.internal.net.http.frame.SettingsFrame.DEFAULT_INITIAL_WINDOW_SIZE;
import static jdk.internal.net.http.frame.SettingsFrame.ENABLE_PUSH;
import static jdk.internal.net.http.frame.SettingsFrame.HEADER_TABLE_SIZE;
import static jdk.internal.net.http.frame.SettingsFrame.INITIAL_WINDOW_SIZE;
import static jdk.internal.net.http.frame.SettingsFrame.MAX_CONCURRENT_STREAMS;
import static jdk.internal.net.http.frame.SettingsFrame.MAX_FRAME_SIZE;
import static jdk.internal.net.http.frame.SettingsFrame.MAX_HEADER_LIST_SIZE;
/**
* An Http2Connection. Encapsulates the socket(channel) and any SSLEngine used
@ -327,6 +331,45 @@ class Http2Connection {
}
}
private final class PushPromiseDecoder extends HeaderDecoder implements DecodingCallback {
final int parentStreamId;
final int pushPromiseStreamId;
final Stream<?> parent;
final AtomicReference<Throwable> errorRef = new AtomicReference<>();
PushPromiseDecoder(int parentStreamId, int pushPromiseStreamId, Stream<?> parent) {
this.parentStreamId = parentStreamId;
this.pushPromiseStreamId = pushPromiseStreamId;
this.parent = parent;
}
@Override
protected void addHeader(String name, String value) {
if (errorRef.get() == null) {
super.addHeader(name, value);
}
}
@Override
public void onMaxHeaderListSizeReached(long size, int maxHeaderListSize) throws ProtocolException {
try {
DecodingCallback.super.onMaxHeaderListSizeReached(size, maxHeaderListSize);
} catch (ProtocolException pe) {
if (parent != null) {
if (errorRef.compareAndSet(null, pe)) {
// cancel the parent stream
resetStream(pushPromiseStreamId, ResetFrame.REFUSED_STREAM);
parent.onProtocolError(pe);
}
} else {
// interrupt decoding and closes the connection
throw pe;
}
}
}
}
private static final int HALF_CLOSED_LOCAL = 1;
private static final int HALF_CLOSED_REMOTE = 2;
@ -355,7 +398,7 @@ class Http2Connection {
private final Decoder hpackIn;
final SettingsFrame clientSettings;
private volatile SettingsFrame serverSettings;
private record PushContinuationState(HeaderDecoder pushContDecoder, PushPromiseFrame pushContFrame) {}
private record PushContinuationState(PushPromiseDecoder pushContDecoder, PushPromiseFrame pushContFrame) {}
private volatile PushContinuationState pushContinuationState;
private final String key; // for HttpClientImpl.connections map
private final FramesDecoder framesDecoder;
@ -370,12 +413,24 @@ class Http2Connection {
private final FramesController framesController = new FramesController();
private final Http2TubeSubscriber subscriber;
final ConnectionWindowUpdateSender windowUpdater;
private volatile Throwable cause;
private final AtomicReference<Throwable> cause = new AtomicReference<>();
private volatile Supplier<ByteBuffer> initial;
private volatile Stream<?> initialStream;
static final int DEFAULT_FRAME_SIZE = 16 * 1024;
private ValidatingHeadersConsumer orphanedConsumer;
private final AtomicInteger orphanedHeaders = new AtomicInteger();
static final int DEFAULT_FRAME_SIZE = 16 * 1024;
static final int MAX_LITERAL_WITH_INDEXING =
Utils.getIntegerNetProperty("jdk.httpclient.maxLiteralWithIndexing",512);
// The maximum number of HEADER frames, CONTINUATION frames, or PUSH_PROMISE frames
// referring to an already closed or non-existent stream that a client will accept to
// process. Receiving frames referring to non-existent or closed streams doesn't necessarily
// constitute an HTTP/2 protocol error, but receiving too many may indicate a problem
// with the connection. If this limit is reached, a {@link java.net.ProtocolException
// ProtocolException} will be raised and the connection will be closed.
static final int MAX_ORPHANED_HEADERS = 1024;
// TODO: need list of control frames from other threads
// that need to be sent
@ -383,19 +438,21 @@ class Http2Connection {
private Http2Connection(HttpConnection connection,
Http2ClientImpl client2,
int nextstreamid,
String key) {
String key,
boolean defaultServerPush) {
this.connection = connection;
this.client2 = client2;
this.subscriber = new Http2TubeSubscriber(client2.client());
this.nextstreamid = nextstreamid;
this.key = key;
this.clientSettings = this.client2.getClientSettings();
this.clientSettings = this.client2.getClientSettings(defaultServerPush);
this.framesDecoder = new FramesDecoder(this::processFrame,
clientSettings.getParameter(SettingsFrame.MAX_FRAME_SIZE));
// serverSettings will be updated by server
this.serverSettings = SettingsFrame.defaultRFCSettings();
this.hpackOut = new Encoder(serverSettings.getParameter(HEADER_TABLE_SIZE));
this.hpackIn = new Decoder(clientSettings.getParameter(HEADER_TABLE_SIZE));
this.hpackIn = new Decoder(clientSettings.getParameter(HEADER_TABLE_SIZE),
clientSettings.getParameter(MAX_HEADER_LIST_SIZE), MAX_LITERAL_WITH_INDEXING);
if (debugHpack.on()) {
debugHpack.log("For the record:" + super.toString());
debugHpack.log("Decoder created: %s", hpackIn);
@ -414,14 +471,16 @@ class Http2Connection {
private Http2Connection(HttpConnection connection,
Http2ClientImpl client2,
Exchange<?> exchange,
Supplier<ByteBuffer> initial)
Supplier<ByteBuffer> initial,
boolean defaultServerPush)
throws IOException, InterruptedException
{
this(connection,
client2,
3, // stream 1 is registered during the upgrade
keyFor(connection));
reserveStream(true);
keyFor(connection),
defaultServerPush);
reserveStream(true, clientSettings.getFlag(ENABLE_PUSH));
Log.logTrace("Connection send window size {0} ", windowController.connectionWindowSize());
Stream<?> initialStream = createStream(exchange);
@ -454,7 +513,8 @@ class Http2Connection {
Exchange<?> exchange,
Supplier<ByteBuffer> initial)
{
return MinimalFuture.supply(() -> new Http2Connection(connection, client2, exchange, initial));
return MinimalFuture.supply(() -> new Http2Connection(connection, client2, exchange, initial,
exchange.pushEnabled()));
}
// Requires TLS handshake. So, is really async
@ -478,7 +538,8 @@ class Http2Connection {
.thenCompose(notused-> {
CompletableFuture<Http2Connection> cf = new MinimalFuture<>();
try {
Http2Connection hc = new Http2Connection(request, h2client, connection);
Http2Connection hc = new Http2Connection(request, h2client,
connection, exchange.pushEnabled());
cf.complete(hc);
} catch (IOException e) {
cf.completeExceptionally(e);
@ -493,13 +554,15 @@ class Http2Connection {
*/
private Http2Connection(HttpRequestImpl request,
Http2ClientImpl h2client,
HttpConnection connection)
HttpConnection connection,
boolean defaultServerPush)
throws IOException
{
this(connection,
h2client,
1,
keyFor(request));
keyFor(request),
defaultServerPush);
Log.logTrace("Connection send window size {0} ", windowController.connectionWindowSize());
@ -522,24 +585,30 @@ class Http2Connection {
// if false returned then a new Http2Connection is required
// if true, the stream may be assigned to this connection
// for server push, if false returned, then the stream should be cancelled
boolean reserveStream(boolean clientInitiated) throws IOException {
boolean reserveStream(boolean clientInitiated, boolean pushEnabled) throws IOException {
stateLock.lock();
try {
return reserveStream0(clientInitiated);
return reserveStream0(clientInitiated, pushEnabled);
} finally {
stateLock.unlock();
}
}
private boolean reserveStream0(boolean clientInitiated) throws IOException {
private boolean reserveStream0(boolean clientInitiated, boolean pushEnabled) throws IOException {
if (finalStream()) {
return false;
}
if (clientInitiated && (lastReservedClientStreamid + 2) >= MAX_CLIENT_STREAM_ID) {
// If requesting to reserve a stream for an exchange for which push is enabled,
// we will reserve the stream in this connection only if this connection is also
// push enabled, unless pushes are globally disabled.
boolean pushCompatible = !clientInitiated || !pushEnabled
|| this.serverPushEnabled()
|| client2.serverPushDisabled();
if (clientInitiated && (lastReservedClientStreamid >= MAX_CLIENT_STREAM_ID -2 || !pushCompatible)) {
setFinalStream();
client2.removeFromPool(this);
return false;
} else if (!clientInitiated && (lastReservedServerStreamid + 2) >= MAX_SERVER_STREAM_ID) {
} else if (!clientInitiated && (lastReservedServerStreamid >= MAX_SERVER_STREAM_ID - 2)) {
setFinalStream();
client2.removeFromPool(this);
return false;
@ -564,6 +633,15 @@ class Http2Connection {
return true;
}
boolean shouldClose() {
stateLock.lock();
try {
return finalStream() && streams.isEmpty();
} finally {
stateLock.unlock();
}
}
/**
* Throws an IOException if h2 was not negotiated
*/
@ -691,6 +769,10 @@ class Http2Connection {
return this.key;
}
public boolean serverPushEnabled() {
return clientSettings.getParameter(SettingsFrame.ENABLE_PUSH) == 1;
}
boolean offerConnection() {
return client2.offerConnection(this);
}
@ -795,7 +877,7 @@ class Http2Connection {
}
Throwable getRecordedCause() {
return cause;
return cause.get();
}
void shutdown(Throwable t) {
@ -804,11 +886,11 @@ class Http2Connection {
stateLock.lock();
try {
if (!markShutdownRequested()) return;
Throwable initialCause = this.cause;
if (initialCause == null && t != null) this.cause = t;
cause.compareAndSet(null, t);
} finally {
stateLock.unlock();
}
if (Log.errors()) {
if (t!= null && (!(t instanceof EOFException) || isActive())) {
Log.logError(t);
@ -819,6 +901,7 @@ class Http2Connection {
}
}
client2.removeFromPool(this);
subscriber.stop(cause.get());
for (Stream<?> s : streams.values()) {
try {
s.connectionClosing(t);
@ -872,17 +955,39 @@ class Http2Connection {
return;
}
if (frame instanceof PushPromiseFrame && !serverPushEnabled()) {
String protocolError = "received a PUSH_PROMISE when SETTINGS_ENABLE_PUSH is 0";
protocolError(ResetFrame.PROTOCOL_ERROR, protocolError);
return;
}
Stream<?> stream = getStream(streamid);
var nextstreamid = this.nextstreamid;
if (stream == null && (streamid & 0x01) == 0x01 && streamid >= nextstreamid) {
String protocolError = String.format(
"received a frame for a non existing streamid(%s) >= nextstreamid(%s)",
streamid, nextstreamid);
protocolError(ResetFrame.PROTOCOL_ERROR, protocolError);
return;
}
if (stream == null && pushContinuationState == null) {
// Should never receive a frame with unknown stream id
if (frame instanceof HeaderFrame) {
if (frame instanceof HeaderFrame hf) {
String protocolError = checkMaxOrphanedHeadersExceeded(hf);
if (protocolError != null) {
protocolError(ResetFrame.PROTOCOL_ERROR, protocolError);
return;
}
// always decode the headers as they may affect
// connection-level HPACK decoding state
DecodingCallback decoder = new ValidatingHeadersConsumer()::onDecoded;
if (orphanedConsumer == null || frame.getClass() != ContinuationFrame.class) {
orphanedConsumer = new ValidatingHeadersConsumer();
}
DecodingCallback decoder = orphanedConsumer::onDecoded;
try {
decodeHeaders((HeaderFrame) frame, decoder);
} catch (UncheckedIOException e) {
decodeHeaders(hf, decoder);
} catch (IOException | UncheckedIOException e) {
protocolError(ResetFrame.PROTOCOL_ERROR, e.getMessage());
return;
}
@ -910,29 +1015,41 @@ class Http2Connection {
// While push frame is not null, the only acceptable frame on this
// stream is a Continuation frame
if (pushContinuationState != null) {
PushContinuationState pcs = pushContinuationState;
if (pcs != null) {
if (frame instanceof ContinuationFrame cf) {
if (stream == null) {
String protocolError = checkMaxOrphanedHeadersExceeded(cf);
if (protocolError != null) {
protocolError(ResetFrame.PROTOCOL_ERROR, protocolError);
return;
}
}
try {
if (streamid == pushContinuationState.pushContFrame.streamid())
handlePushContinuation(stream, cf);
else
protocolError(ErrorFrame.PROTOCOL_ERROR, "Received a Continuation Frame with an " +
"unexpected stream id");
} catch (UncheckedIOException e) {
if (streamid == pcs.pushContFrame.streamid())
handlePushContinuation(pcs, stream, cf);
else {
String protocolError = "Received a CONTINUATION with " +
"unexpected stream id: " + streamid + " != "
+ pcs.pushContFrame.streamid();
protocolError(ErrorFrame.PROTOCOL_ERROR, protocolError);
}
} catch (IOException | UncheckedIOException e) {
debug.log("Error handling Push Promise with Continuation: " + e.getMessage(), e);
protocolError(ErrorFrame.PROTOCOL_ERROR, e.getMessage());
return;
}
} else {
pushContinuationState = null;
protocolError(ErrorFrame.PROTOCOL_ERROR, "Expected a Continuation frame but received " + frame);
String protocolError = "Expected a CONTINUATION frame but received " + frame;
protocolError(ErrorFrame.PROTOCOL_ERROR, protocolError);
return;
}
} else {
if (frame instanceof PushPromiseFrame pp) {
try {
handlePushPromise(stream, pp);
} catch (UncheckedIOException e) {
} catch (IOException | UncheckedIOException e) {
protocolError(ErrorFrame.PROTOCOL_ERROR, e.getMessage());
return;
}
@ -940,7 +1057,7 @@ class Http2Connection {
// decode headers
try {
decodeHeaders(hf, stream.rspHeadersConsumer());
} catch (UncheckedIOException e) {
} catch (IOException | UncheckedIOException e) {
debug.log("Error decoding headers: " + e.getMessage(), e);
protocolError(ErrorFrame.PROTOCOL_ERROR, e.getMessage());
return;
@ -953,6 +1070,16 @@ class Http2Connection {
}
}
private String checkMaxOrphanedHeadersExceeded(HeaderFrame hf) {
if (MAX_ORPHANED_HEADERS > 0 ) {
int orphaned = orphanedHeaders.incrementAndGet();
if (orphaned < 0 || orphaned > MAX_ORPHANED_HEADERS) {
return "Too many orphaned header frames received on connection";
}
}
return null;
}
final void dropDataFrame(DataFrame df) {
if (isMarked(closedState, SHUTDOWN_REQUESTED)) return;
if (debug.on()) {
@ -977,38 +1104,65 @@ class Http2Connection {
private <T> void handlePushPromise(Stream<T> parent, PushPromiseFrame pp)
throws IOException
{
int promisedStreamid = pp.getPromisedStream();
if ((promisedStreamid & 0x01) != 0x00) {
throw new ProtocolException("Received PUSH_PROMISE for stream " + promisedStreamid);
}
int streamId = pp.streamid();
if ((streamId & 0x01) != 0x01) {
throw new ProtocolException("Received PUSH_PROMISE on stream " + streamId);
}
// always decode the headers as they may affect connection-level HPACK
// decoding state
assert pushContinuationState == null;
HeaderDecoder decoder = new HeaderDecoder();
decodeHeaders(pp, decoder::onDecoded);
int promisedStreamid = pp.getPromisedStream();
PushPromiseDecoder decoder = new PushPromiseDecoder(streamId, promisedStreamid, parent);
decodeHeaders(pp, decoder);
if (pp.endHeaders()) {
completePushPromise(promisedStreamid, parent, decoder.headers());
if (decoder.errorRef.get() == null) {
completePushPromise(promisedStreamid, parent, decoder.headers());
}
} else {
pushContinuationState = new PushContinuationState(decoder, pp);
}
}
private <T> void handlePushContinuation(Stream<T> parent, ContinuationFrame cf)
private <T> void handlePushContinuation(PushContinuationState pcs, Stream<T> parent, ContinuationFrame cf)
throws IOException {
var pcs = pushContinuationState;
decodeHeaders(cf, pcs.pushContDecoder::onDecoded);
assert pcs.pushContFrame.streamid() == cf.streamid() : String.format(
"Received CONTINUATION on a different stream %s != %s",
cf.streamid(), pcs.pushContFrame.streamid());
decodeHeaders(cf, pcs.pushContDecoder);
// if all continuations are sent, set pushWithContinuation to null
if (cf.endHeaders()) {
completePushPromise(pcs.pushContFrame.getPromisedStream(), parent,
pcs.pushContDecoder.headers());
if (pcs.pushContDecoder.errorRef.get() == null) {
completePushPromise(pcs.pushContFrame.getPromisedStream(), parent,
pcs.pushContDecoder.headers());
}
pushContinuationState = null;
}
}
private <T> void completePushPromise(int promisedStreamid, Stream<T> parent, HttpHeaders headers)
throws IOException {
if (parent == null) {
resetStream(promisedStreamid, ResetFrame.REFUSED_STREAM);
return;
}
HttpRequestImpl parentReq = parent.request;
if (promisedStreamid < nextPushStream) {
// From RFC 9113 section 5.1.1:
// The identifier of a newly established stream MUST be numerically
// greater than all streams that the initiating endpoint has
// opened or reserved.
protocolError(ResetFrame.PROTOCOL_ERROR, String.format(
"Unexpected stream identifier: %s < %s", promisedStreamid, nextPushStream));
return;
}
if (promisedStreamid != nextPushStream) {
// we don't support skipping stream ids;
resetStream(promisedStreamid, ResetFrame.PROTOCOL_ERROR);
return;
} else if (!reserveStream(false)) {
} else if (!reserveStream(false, true)) {
resetStream(promisedStreamid, ResetFrame.REFUSED_STREAM);
return;
} else {
@ -1177,11 +1331,17 @@ class Http2Connection {
private void protocolError(int errorCode, String msg)
throws IOException
{
String protocolError = "protocol error" + (msg == null?"":(": " + msg));
ProtocolException protocolException =
new ProtocolException(protocolError);
if (markHalfClosedLocal()) {
framesDecoder.close(protocolError);
subscriber.stop(protocolException);
if (debug.on()) debug.log("Sending GOAWAY due to " + protocolException);
GoAwayFrame frame = new GoAwayFrame(0, errorCode);
sendFrame(frame);
}
shutdown(new IOException("protocol error" + (msg == null?"":(": " + msg))));
shutdown(protocolException);
}
private void handleSettings(SettingsFrame frame)
@ -1356,7 +1516,7 @@ class Http2Connection {
<T> Stream.PushedStream<T> createPushStream(Stream<T> parent, Exchange<T> pushEx) {
PushGroup<T> pg = parent.exchange.getPushGroup();
return new Stream.PushedStream<>(pg, this, pushEx);
return new Stream.PushedStream<>(parent, pg, this, pushEx);
}
/**
@ -1466,16 +1626,18 @@ class Http2Connection {
private List<ByteBuffer> encodeHeadersImpl(int bufferSize, HttpHeaders... headers) {
ByteBuffer buffer = getHeaderBuffer(bufferSize);
List<ByteBuffer> buffers = new ArrayList<>();
for(HttpHeaders header : headers) {
for (HttpHeaders header : headers) {
for (Map.Entry<String, List<String>> e : header.map().entrySet()) {
String lKey = e.getKey().toLowerCase(Locale.US);
List<String> values = e.getValue();
for (String value : values) {
hpackOut.header(lKey, value);
while (!hpackOut.encode(buffer)) {
buffer.flip();
buffers.add(buffer);
buffer = getHeaderBuffer(bufferSize);
if (!buffer.hasRemaining()) {
buffer.flip();
buffers.add(buffer);
buffer = getHeaderBuffer(bufferSize);
}
}
}
}
@ -1514,7 +1676,7 @@ class Http2Connection {
Throwable cause = null;
synchronized (this) {
if (isMarked(closedState, SHUTDOWN_REQUESTED)) {
cause = this.cause;
cause = this.cause.get();
if (cause == null) {
cause = new IOException("Connection closed");
}
@ -1553,6 +1715,8 @@ class Http2Connection {
Stream<?> stream = registerNewStream(oh);
// provide protection from inserting unordered frames between Headers and Continuation
if (stream != null) {
// we are creating a new stream: reset orphaned header count
orphanedHeaders.set(0);
publisher.enqueue(encodeHeaders(oh, stream));
}
} else {
@ -1621,7 +1785,7 @@ class Http2Connection {
private volatile Flow.Subscription subscription;
private volatile boolean completed;
private volatile boolean dropped;
private volatile Throwable error;
private final AtomicReference<Throwable> errorRef = new AtomicReference<>();
private final ConcurrentLinkedQueue<ByteBuffer> queue
= new ConcurrentLinkedQueue<>();
private final SequentialScheduler scheduler =
@ -1642,10 +1806,9 @@ class Http2Connection {
asyncReceive(buffer);
}
} catch (Throwable t) {
Throwable x = error;
if (x == null) error = t;
errorRef.compareAndSet(null, t);
} finally {
Throwable x = error;
Throwable x = errorRef.get();
if (x != null) {
if (debug.on()) debug.log("Stopping scheduler", x);
scheduler.stop();
@ -1680,6 +1843,7 @@ class Http2Connection {
@Override
public void onNext(List<ByteBuffer> item) {
if (completed) return;
if (debug.on()) debug.log(() -> "onNext: got " + Utils.remaining(item)
+ " bytes in " + item.size() + " buffers");
queue.addAll(item);
@ -1688,19 +1852,21 @@ class Http2Connection {
@Override
public void onError(Throwable throwable) {
if (completed) return;
if (debug.on()) debug.log(() -> "onError: " + throwable);
error = throwable;
errorRef.compareAndSet(null, throwable);
completed = true;
runOrSchedule();
}
@Override
public void onComplete() {
if (completed) return;
String msg = isActive()
? "EOF reached while reading"
: "Idle connection closed by HTTP/2 peer";
if (debug.on()) debug.log(msg);
error = new EOFException(msg);
errorRef.compareAndSet(null, new EOFException(msg));
completed = true;
runOrSchedule();
}
@ -1712,6 +1878,18 @@ class Http2Connection {
// then we might not need the 'dropped' boolean?
dropped = true;
}
void stop(Throwable error) {
if (errorRef.compareAndSet(null, error)) {
completed = true;
scheduler.stop();
queue.clear();
if (subscription != null) {
subscription.cancel();
}
queue.clear();
}
}
}
boolean isActive() {

View File

@ -964,7 +964,9 @@ final class HttpClientImpl extends HttpClient implements Trackable {
// SSLException
throw new SSLException(msg, throwable);
} else if (throwable instanceof ProtocolException) {
throw new ProtocolException(msg);
ProtocolException pe = new ProtocolException(msg);
pe.initCause(throwable);
throw pe;
} else if (throwable instanceof IOException) {
throw new IOException(msg, throwable);
} else {

View File

@ -287,10 +287,10 @@ public class HttpRequestImpl extends HttpRequest implements WebSocketRequest {
InetSocketAddress authority() { return authority; }
void setH2Upgrade(Http2ClientImpl h2client) {
void setH2Upgrade(Exchange<?> exchange) {
systemHeadersBuilder.setHeader("Connection", "Upgrade, HTTP2-Settings");
systemHeadersBuilder.setHeader("Upgrade", Alpns.H2C);
systemHeadersBuilder.setHeader("HTTP2-Settings", h2client.getSettingsString());
systemHeadersBuilder.setHeader("HTTP2-Settings", exchange.h2cSettingsStrings());
}
@Override

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2018, 2021, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2018, 2024, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
@ -37,6 +37,7 @@ import java.nio.file.Paths;
import java.security.AccessControlContext;
import java.security.AccessController;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentMap;
import java.util.function.Function;
@ -137,16 +138,21 @@ public final class ResponseBodyHandlers {
if (!initiatingURI.getHost().equalsIgnoreCase(pushRequestURI.getHost()))
return;
String initiatingScheme = initiatingURI.getScheme();
String pushRequestScheme = pushRequestURI.getScheme();
if (!initiatingScheme.equalsIgnoreCase(pushRequestScheme)) return;
int initiatingPort = initiatingURI.getPort();
if (initiatingPort == -1 ) {
if ("https".equalsIgnoreCase(initiatingURI.getScheme()))
if ("https".equalsIgnoreCase(initiatingScheme))
initiatingPort = 443;
else
initiatingPort = 80;
}
int pushPort = pushRequestURI.getPort();
if (pushPort == -1 ) {
if ("https".equalsIgnoreCase(pushRequestURI.getScheme()))
if ("https".equalsIgnoreCase(pushRequestScheme))
pushPort = 443;
else
pushPort = 80;

View File

@ -30,6 +30,7 @@ import java.io.IOException;
import java.io.UncheckedIOException;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.VarHandle;
import java.net.ProtocolException;
import java.net.URI;
import java.net.http.HttpResponse.BodyHandler;
import java.net.http.HttpResponse.ResponseInfo;
@ -43,6 +44,7 @@ import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.Executor;
import java.util.concurrent.Flow;
import java.util.concurrent.Flow.Subscription;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
@ -52,10 +54,13 @@ import java.net.http.HttpHeaders;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.net.http.HttpResponse.BodySubscriber;
import jdk.internal.net.http.common.*;
import jdk.internal.net.http.frame.*;
import jdk.internal.net.http.hpack.DecodingCallback;
import static jdk.internal.net.http.Exchange.MAX_NON_FINAL_RESPONSES;
/**
* Http/2 Stream handling.
*
@ -140,6 +145,9 @@ class Stream<T> extends ExchangeImpl<T> {
private volatile boolean closed;
private volatile boolean endStreamSent;
private volatile boolean finalResponseCodeReceived;
private volatile boolean trailerReceived;
private AtomicInteger nonFinalResponseCount = new AtomicInteger();
// Indicates the first reason that was invoked when sending a ResetFrame
// to the server. A streamState of 0 indicates that no reset was sent.
// (see markStream(int code)
@ -520,16 +528,38 @@ class Stream<T> extends ExchangeImpl<T> {
// The Hpack decoder decodes into one of these consumers of name,value pairs
DecodingCallback rspHeadersConsumer() {
return rspHeadersConsumer::onDecoded;
return rspHeadersConsumer;
}
String checkInterimResponseCountExceeded() {
// this is also checked by Exchange - but tracking it here too provides
// a more informative message.
int count = nonFinalResponseCount.incrementAndGet();
if (MAX_NON_FINAL_RESPONSES > 0 && (count < 0 || count > MAX_NON_FINAL_RESPONSES)) {
return String.format(
"Stream %s PROTOCOL_ERROR: too many interim responses received: %s > %s",
streamid, count, MAX_NON_FINAL_RESPONSES);
}
return null;
}
protected void handleResponse(HeaderFrame hf) throws IOException {
HttpHeaders responseHeaders = responseHeadersBuilder.build();
if (!finalResponseCodeReceived) {
responseCode = (int) responseHeaders
.firstValueAsLong(":status")
.orElseThrow(() -> new IOException("no statuscode in response"));
try {
responseCode = (int) responseHeaders
.firstValueAsLong(":status")
.orElseThrow(() -> new ProtocolException(String.format(
"Stream %s PROTOCOL_ERROR: no status code in response",
streamid)));
} catch (ProtocolException cause) {
cancelImpl(cause, ResetFrame.PROTOCOL_ERROR);
rspHeadersConsumer.reset();
return;
}
String protocolErrorMsg = null;
// If informational code, response is partially complete
if (responseCode < 100 || responseCode > 199) {
this.finalResponseCodeReceived = true;
@ -537,23 +567,31 @@ class Stream<T> extends ExchangeImpl<T> {
// see RFC 9113 section 8.1:
// A HEADERS frame with the END_STREAM flag set that carries an
// informational status code is malformed
String msg = ("Stream %s PROTOCOL_ERROR: " +
"HEADERS frame with status %s has END_STREAM flag set")
.formatted(streamid, responseCode);
protocolErrorMsg = String.format(
"Stream %s PROTOCOL_ERROR: " +
"HEADERS frame with status %s has END_STREAM flag set",
streamid, responseCode);
} else {
protocolErrorMsg = checkInterimResponseCountExceeded();
}
if (protocolErrorMsg != null) {
if (debug.on()) {
debug.log(msg);
debug.log(protocolErrorMsg);
}
cancelImpl(new IOException(msg), ResetFrame.PROTOCOL_ERROR);
cancelImpl(new ProtocolException(protocolErrorMsg), ResetFrame.PROTOCOL_ERROR);
rspHeadersConsumer.reset();
return;
}
response = new Response(
request, exchange, responseHeaders, connection(),
responseCode, HttpClient.Version.HTTP_2);
/* TODO: review if needs to be removed
the value is not used, but in case `content-length` doesn't parse as
long, there will be NumberFormatException. If left as is, make sure
code up the stack handles NFE correctly. */
/* TODO: review if needs to be removed
the value is not used, but in case `content-length` doesn't parse as
long, there will be NumberFormatException. If left as is, make sure
code up the stack handles NFE correctly. */
responseHeaders.firstValueAsLong("content-length");
if (Log.headers()) {
@ -572,6 +610,15 @@ class Stream<T> extends ExchangeImpl<T> {
Log.dumpHeaders(sb, " ", responseHeaders);
Log.logHeaders(sb.toString());
}
if (trailerReceived) {
String protocolErrorMsg = String.format(
"Stream %s PROTOCOL_ERROR: trailers already received", streamid);
if (debug.on()) {
debug.log(protocolErrorMsg);
}
cancelImpl(new ProtocolException(protocolErrorMsg), ResetFrame.PROTOCOL_ERROR);
}
trailerReceived = true;
rspHeadersConsumer.reset();
}
@ -1182,7 +1229,7 @@ class Stream<T> extends ExchangeImpl<T> {
/**
* A List of responses relating to this stream. Normally there is only
* one response, but intermediate responses like 100 are allowed
* one response, but interim responses like 100 are allowed
* and must be passed up to higher level before continuing. Deals with races
* such as if responses are returned before the CFs get created by
* getResponseAsync()
@ -1401,7 +1448,7 @@ class Stream<T> extends ExchangeImpl<T> {
cancelImpl(e, ResetFrame.CANCEL);
}
private void cancelImpl(final Throwable e, final int resetFrameErrCode) {
void cancelImpl(final Throwable e, final int resetFrameErrCode) {
errorRef.compareAndSet(null, e);
if (debug.on()) {
if (streamid == 0) debug.log("cancelling stream: %s", (Object)e);
@ -1511,6 +1558,7 @@ class Stream<T> extends ExchangeImpl<T> {
}
static class PushedStream<T> extends Stream<T> {
final Stream<T> parent;
final PushGroup<T> pushGroup;
// push streams need the response CF allocated up front as it is
// given directly to user via the multi handler callback function.
@ -1520,16 +1568,17 @@ class Stream<T> extends ExchangeImpl<T> {
volatile HttpResponse.BodyHandler<T> pushHandler;
private volatile boolean finalPushResponseCodeReceived;
PushedStream(PushGroup<T> pushGroup,
PushedStream(Stream<T> parent,
PushGroup<T> pushGroup,
Http2Connection connection,
Exchange<T> pushReq) {
// ## no request body possible, null window controller
super(connection, pushReq, null);
this.parent = parent;
this.pushGroup = pushGroup;
this.pushReq = pushReq.request();
this.pushCF = new MinimalFuture<>();
this.responseCF = new MinimalFuture<>();
}
CompletableFuture<HttpResponse<T>> responseCF() {
@ -1617,7 +1666,16 @@ class Stream<T> extends ExchangeImpl<T> {
.orElse(-1);
if (responseCode == -1) {
completeResponseExceptionally(new IOException("No status code"));
cancelImpl(new ProtocolException("No status code"), ResetFrame.PROTOCOL_ERROR);
rspHeadersConsumer.reset();
return;
} else if (responseCode >= 100 && responseCode < 200) {
String protocolErrorMsg = checkInterimResponseCountExceeded();
if (protocolErrorMsg != null) {
cancelImpl(new ProtocolException(protocolErrorMsg), ResetFrame.PROTOCOL_ERROR);
rspHeadersConsumer.reset();
return;
}
}
this.finalPushResponseCodeReceived = true;
@ -1727,7 +1785,9 @@ class Stream<T> extends ExchangeImpl<T> {
}
}
private class HeadersConsumer extends ValidatingHeadersConsumer {
private class HeadersConsumer extends ValidatingHeadersConsumer implements DecodingCallback {
boolean maxHeaderListSizeReached;
@Override
public void reset() {
@ -1740,6 +1800,9 @@ class Stream<T> extends ExchangeImpl<T> {
public void onDecoded(CharSequence name, CharSequence value)
throws UncheckedIOException
{
if (maxHeaderListSizeReached) {
return;
}
try {
String n = name.toString();
String v = value.toString();
@ -1762,6 +1825,23 @@ class Stream<T> extends ExchangeImpl<T> {
protected String formatMessage(String message, String header) {
return "malformed response: " + super.formatMessage(message, header);
}
@Override
public void onMaxHeaderListSizeReached(long size, int maxHeaderListSize) throws ProtocolException {
if (maxHeaderListSizeReached) return;
try {
DecodingCallback.super.onMaxHeaderListSizeReached(size, maxHeaderListSize);
} catch (ProtocolException cause) {
maxHeaderListSizeReached = true;
// If this is a push stream: cancel the parent.
if (Stream.this instanceof Stream.PushedStream<?> ps) {
ps.parent.onProtocolError(cause);
}
// cancel the stream, continue processing
onProtocolError(cause);
reset();
}
}
}
final class Http2StreamResponseSubscriber<U> extends HttpBodySubscriberWrapper<U> {

View File

@ -39,7 +39,11 @@ public class HeaderDecoder extends ValidatingHeadersConsumer {
String n = name.toString();
String v = value.toString();
super.onDecoded(n, v);
headersBuilder.addHeader(n, v);
addHeader(n, v);
}
protected void addHeader(String name, String value) {
headersBuilder.addHeader(name, value);
}
public HttpHeaders headers() {

View File

@ -616,6 +616,19 @@ public final class Utils {
Integer.parseInt(System.getProperty(name, String.valueOf(defaultValue))));
}
public static int getIntegerNetProperty(String property, int min, int max, int defaultValue, boolean log) {
int value = Utils.getIntegerNetProperty(property, defaultValue);
// use default value if misconfigured
if (value < min || value > max) {
if (log && Log.errors()) {
Log.logError("Property value for {0}={1} not in [{2}..{3}]: " +
"using default={4}", property, value, min, max, defaultValue);
}
value = defaultValue;
}
return value;
}
public static SSLParameters copySSLParameters(SSLParameters p) {
SSLParameters p1 = new SSLParameters();
p1.setAlgorithmConstraints(p.getAlgorithmConstraints());

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2014, 2018, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2014, 2024, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
@ -27,6 +27,7 @@ package jdk.internal.net.http.hpack;
import jdk.internal.net.http.hpack.HPACK.Logger;
import java.io.IOException;
import java.net.ProtocolException;
import java.nio.ByteBuffer;
import java.util.List;
import java.util.concurrent.atomic.AtomicLong;
@ -107,12 +108,16 @@ public final class Decoder {
private final StringReader stringReader;
private final StringBuilder name;
private final StringBuilder value;
private final int maxHeaderListSize;
private final int maxIndexed;
private int intValue;
private boolean firstValueRead;
private boolean firstValueIndex;
private boolean nameHuffmanEncoded;
private boolean valueHuffmanEncoded;
private int capacity;
private long size;
private int indexed;
/**
* Constructs a {@code Decoder} with the specified initial capacity of the
@ -129,6 +134,31 @@ public final class Decoder {
* if capacity is negative
*/
public Decoder(int capacity) {
this(capacity, 0, 0);
}
/**
* Constructs a {@code Decoder} with the specified initial capacity of the
* header table, a max header list size, and a maximum number of literals
* with indexing per header section.
*
* <p> The value of the capacity has to be agreed between decoder and encoder out-of-band,
* e.g. by a protocol that uses HPACK
* (see <a href="https://tools.ietf.org/html/rfc7541#section-4.2">4.2. Maximum Table Size</a>).
*
* @param capacity
* a non-negative integer
* @param maxHeaderListSize
* a maximum value for the header list size. This is the uncompressed
* names size + uncompressed values size + 32 bytes per field line
* @param maxIndexed
* the maximum number of literal with indexing we're prepared to handle
* for a header field section
*
* @throws IllegalArgumentException
* if capacity is negative
*/
public Decoder(int capacity, int maxHeaderListSize, int maxIndexed) {
id = DECODERS_IDS.incrementAndGet();
logger = HPACK.getLogger().subLogger("Decoder#" + id);
if (logger.isLoggable(NORMAL)) {
@ -145,6 +175,8 @@ public final class Decoder {
toString(), hashCode);
});
}
this.maxHeaderListSize = maxHeaderListSize;
this.maxIndexed = maxIndexed;
setMaxCapacity0(capacity);
table = new SimpleHeaderTable(capacity, logger.subLogger("HeaderTable"));
integerReader = new IntegerReader();
@ -242,22 +274,25 @@ public final class Decoder {
requireNonNull(consumer, "consumer");
if (logger.isLoggable(NORMAL)) {
logger.log(NORMAL, () -> format("reading %s, end of header block? %s",
headerBlock, endOfHeaderBlock));
headerBlock, endOfHeaderBlock));
}
while (headerBlock.hasRemaining()) {
proceed(headerBlock, consumer);
}
if (endOfHeaderBlock && state != State.READY) {
logger.log(NORMAL, () -> format("unexpected end of %s representation",
state));
state));
throw new IOException("Unexpected end of header block");
}
if (endOfHeaderBlock) {
size = indexed = 0;
}
}
private void proceed(ByteBuffer input, DecodingCallback action)
throws IOException {
switch (state) {
case READY -> resumeReady(input);
case READY -> resumeReady(input, action);
case INDEXED -> resumeIndexed(input, action);
case LITERAL -> resumeLiteral(input, action);
case LITERAL_WITH_INDEXING -> resumeLiteralWithIndexing(input, action);
@ -268,7 +303,7 @@ public final class Decoder {
}
}
private void resumeReady(ByteBuffer input) {
private void resumeReady(ByteBuffer input, DecodingCallback action) throws IOException {
int b = input.get(input.position()) & 0xff; // absolute read
State s = states.get(b);
if (logger.isLoggable(EXTRA)) {
@ -289,6 +324,9 @@ public final class Decoder {
}
break;
case LITERAL_WITH_INDEXING:
if (maxIndexed > 0 && ++indexed > maxIndexed) {
action.onMaxLiteralWithIndexingReached(indexed, maxIndexed);
}
state = State.LITERAL_WITH_INDEXING;
firstValueIndex = (b & 0b0011_1111) != 0;
if (firstValueIndex) {
@ -315,6 +353,12 @@ public final class Decoder {
}
}
private void checkMaxHeaderListSize(long sz, DecodingCallback consumer) throws ProtocolException {
if (maxHeaderListSize > 0 && sz > maxHeaderListSize) {
consumer.onMaxHeaderListSizeReached(sz, maxHeaderListSize);
}
}
// 0 1 2 3 4 5 6 7
// +---+---+---+---+---+---+---+---+
// | 1 | Index (7+) |
@ -332,6 +376,8 @@ public final class Decoder {
}
try {
SimpleHeaderTable.HeaderField f = getHeaderFieldAt(intValue);
size = size + 32 + f.name.length() + f.value.length();
checkMaxHeaderListSize(size, action);
action.onIndexed(intValue, f.name, f.value);
} finally {
state = State.READY;
@ -374,7 +420,7 @@ public final class Decoder {
//
private void resumeLiteral(ByteBuffer input, DecodingCallback action)
throws IOException {
if (!completeReading(input)) {
if (!completeReading(input, action)) {
return;
}
try {
@ -385,6 +431,8 @@ public final class Decoder {
intValue, value, valueHuffmanEncoded));
}
SimpleHeaderTable.HeaderField f = getHeaderFieldAt(intValue);
size = size + 32 + f.name.length() + value.length();
checkMaxHeaderListSize(size, action);
action.onLiteral(intValue, f.name, value, valueHuffmanEncoded);
} else {
if (logger.isLoggable(NORMAL)) {
@ -392,6 +440,8 @@ public final class Decoder {
"literal without indexing ('%s', huffman=%b, '%s', huffman=%b)",
name, nameHuffmanEncoded, value, valueHuffmanEncoded));
}
size = size + 32 + name.length() + value.length();
checkMaxHeaderListSize(size, action);
action.onLiteral(name, nameHuffmanEncoded, value, valueHuffmanEncoded);
}
} finally {
@ -425,7 +475,7 @@ public final class Decoder {
private void resumeLiteralWithIndexing(ByteBuffer input,
DecodingCallback action)
throws IOException {
if (!completeReading(input)) {
if (!completeReading(input, action)) {
return;
}
try {
@ -445,6 +495,8 @@ public final class Decoder {
}
SimpleHeaderTable.HeaderField f = getHeaderFieldAt(intValue);
n = f.name;
size = size + 32 + n.length() + v.length();
checkMaxHeaderListSize(size, action);
action.onLiteralWithIndexing(intValue, n, v, valueHuffmanEncoded);
} else {
n = name.toString();
@ -453,6 +505,8 @@ public final class Decoder {
"literal with incremental indexing ('%s', huffman=%b, '%s', huffman=%b)",
n, nameHuffmanEncoded, value, valueHuffmanEncoded));
}
size = size + 32 + n.length() + v.length();
checkMaxHeaderListSize(size, action);
action.onLiteralWithIndexing(n, nameHuffmanEncoded, v, valueHuffmanEncoded);
}
table.put(n, v);
@ -486,7 +540,7 @@ public final class Decoder {
private void resumeLiteralNeverIndexed(ByteBuffer input,
DecodingCallback action)
throws IOException {
if (!completeReading(input)) {
if (!completeReading(input, action)) {
return;
}
try {
@ -497,6 +551,8 @@ public final class Decoder {
intValue, value, valueHuffmanEncoded));
}
SimpleHeaderTable.HeaderField f = getHeaderFieldAt(intValue);
size = size + 32 + f.name.length() + value.length();
checkMaxHeaderListSize(size, action);
action.onLiteralNeverIndexed(intValue, f.name, value, valueHuffmanEncoded);
} else {
if (logger.isLoggable(NORMAL)) {
@ -504,6 +560,8 @@ public final class Decoder {
"literal never indexed ('%s', huffman=%b, '%s', huffman=%b)",
name, nameHuffmanEncoded, value, valueHuffmanEncoded));
}
size = size + 32 + name.length() + value.length();
checkMaxHeaderListSize(size, action);
action.onLiteralNeverIndexed(name, nameHuffmanEncoded, value, valueHuffmanEncoded);
}
} finally {
@ -541,7 +599,7 @@ public final class Decoder {
}
}
private boolean completeReading(ByteBuffer input) throws IOException {
private boolean completeReading(ByteBuffer input, DecodingCallback action) throws IOException {
if (!firstValueRead) {
if (firstValueIndex) {
if (!integerReader.read(input)) {
@ -551,6 +609,8 @@ public final class Decoder {
integerReader.reset();
} else {
if (!stringReader.read(input, name)) {
long sz = size + 32 + name.length();
checkMaxHeaderListSize(sz, action);
return false;
}
nameHuffmanEncoded = stringReader.isHuffmanEncoded();
@ -560,6 +620,8 @@ public final class Decoder {
return false;
} else {
if (!stringReader.read(input, value)) {
long sz = size + 32 + name.length() + value.length();
checkMaxHeaderListSize(sz, action);
return false;
}
}

View File

@ -24,6 +24,7 @@
*/
package jdk.internal.net.http.hpack;
import java.net.ProtocolException;
import java.nio.ByteBuffer;
/**
@ -292,4 +293,17 @@ public interface DecodingCallback {
* new capacity of the header table
*/
default void onSizeUpdate(int capacity) { }
default void onMaxHeaderListSizeReached(long size, int maxHeaderListSize)
throws ProtocolException {
throw new ProtocolException(String
.format("Size exceeds MAX_HEADERS_LIST_SIZE: %s > %s",
size, maxHeaderListSize));
}
default void onMaxLiteralWithIndexingReached(long indexed, int maxIndexed)
throws ProtocolException {
throw new ProtocolException(String.format("Too many literal with indexing: %s > %s",
indexed, maxIndexed));
}
}

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2014, 2018, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2014, 2024, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
@ -258,9 +258,10 @@ public class Encoder {
}
}
}
assert encoding : "encoding is false";
}
private boolean isHuffmanBetterFor(CharSequence value) {
protected final boolean isHuffmanBetterFor(CharSequence value) {
// prefer Huffman encoding only if it is strictly smaller than Latin-1
return huffmanWriter.lengthOf(value) < value.length();
}
@ -340,6 +341,10 @@ public class Encoder {
return 0;
}
protected final int tableIndexOf(CharSequence name, CharSequence value) {
return getHeaderTable().indexOf(name, value);
}
/**
* Encodes the {@linkplain #header(CharSequence, CharSequence) set up}
* header into the given buffer.
@ -380,6 +385,7 @@ public class Encoder {
writer.reset(); // FIXME: WHY?
encoding = false;
}
assert done || encoding : "done: " + done + ", encoding: " + encoding;
return done;
}
@ -542,4 +548,8 @@ public class Encoder {
"Previous encoding operation hasn't finished yet");
}
}
protected final Logger logger() {
return logger;
}
}

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2015, 2022, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2015, 2024, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
@ -115,6 +115,25 @@
* The HTTP/2 client maximum frame size in bytes. The server is not permitted to send a frame
* larger than this.
* </li>
* <li><p><b>{@systemProperty jdk.httpclient.maxLiteralWithIndexing}</b> (default: 512)<br>
* The maximum number of header field lines (header name and value pairs) that a
* client is willing to add to the HPack Decoder dynamic table during the decoding
* of an entire header field section.
* This is purely an implementation limit.
* If a peer sends a field section with encoding that
* exceeds this limit a {@link java.net.ProtocolException ProtocolException} will be raised.
* A value of zero or a negative value means no limit.
* </li>
* <li><p><b>{@systemProperty jdk.httpclient.maxNonFinalResponses}</b> (default: 8)<br>
* The maximum number of interim (non-final) responses that a client is prepared
* to accept on a request-response stream before the final response is received.
* Interim responses are responses with a status in the range [100, 199] inclusive.
* This is purely an implementation limit.
* If a peer sends a number of interim response that exceeds this limit before
* sending the final response, a {@link java.net.ProtocolException ProtocolException}
* will be raised.
* A value of zero or a negative value means no limit.
* </li>
* <li><p><b>{@systemProperty jdk.httpclient.maxstreams}</b> (default: 100)<br>
* The maximum number of HTTP/2 push streams that the client will permit servers to open
* simultaneously.
@ -155,6 +174,15 @@
* conf/net.properties)<br>A comma separated list of HTTP authentication scheme names, that
* are disallowed for use by the HTTP client implementation, for HTTP CONNECT tunneling.
* </li>
* <li><p><b>{@systemProperty jdk.http.maxHeaderSize}</b> (default: 393216 or 384kB)
* <br>The maximum header field section size that the client is prepared to accept.
* This is computed as the sum of the size of the uncompressed header name, plus
* the size of the uncompressed header value, plus an overhead of 32 bytes for
* each field section line. If a peer sends a field section that exceeds this
* size a {@link java.net.ProtocolException ProtocolException} will be raised.
* This applies to all versions of the protocol. A value of zero or a negative
* value means no limit.
* </li>
* </ul>
* @moduleGraph
* @since 11

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2014, 2022, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2014, 2024, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
@ -70,6 +70,16 @@
* while the headers are being read, then the connection is terminated and the request ignored.
* If the value is less than or equal to zero, then the default value is used.
* </li>
* <li><p><b>{@systemProperty sun.net.httpserver.maxReqHeaderSize}</b> (default: 393216 or 384kB)<br>
* The maximum header field section size that the server is prepared to accept.
* This is computed as the sum of the size of the header name, plus
* the size of the header value, plus an overhead of 32 bytes for
* each field section line. The request line counts as a first field section line,
* where the name is empty and the value is the whole line.
* If this limit is exceeded while the headers are being read, then the connection
* is terminated and the request ignored.
* If the value is less than or equal to zero, there is no limit.
* </li>
* <li><p><b>{@systemProperty sun.net.httpserver.maxReqTime}</b> (default: -1)<br>
* The maximum time in milliseconds allowed to receive a request headers and body.
* In practice, the actual time is a function of request size, network speed, and handler

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2005, 2023, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2005, 2024, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
@ -44,8 +44,10 @@ class Request {
private SocketChannel chan;
private InputStream is;
private OutputStream os;
private final int maxReqHeaderSize;
Request (InputStream rawInputStream, OutputStream rawout) throws IOException {
this.maxReqHeaderSize = ServerConfig.getMaxReqHeaderSize();
is = rawInputStream;
os = rawout;
do {
@ -75,6 +77,7 @@ class Request {
public String readLine () throws IOException {
boolean gotCR = false, gotLF = false;
pos = 0; lineBuf = new StringBuffer();
long lsize = 32;
while (!gotLF) {
int c = is.read();
if (c == -1) {
@ -87,20 +90,27 @@ class Request {
gotCR = false;
consume (CR);
consume (c);
lsize = lsize + 2;
}
} else {
if (c == CR) {
gotCR = true;
} else {
consume (c);
lsize = lsize + 1;
}
}
if (maxReqHeaderSize > 0 && lsize > maxReqHeaderSize) {
throw new IOException("Maximum header (" +
"sun.net.httpserver.maxReqHeaderSize) exceeded, " +
ServerConfig.getMaxReqHeaderSize() + ".");
}
}
lineBuf.append (buf, 0, pos);
return new String (lineBuf);
}
private void consume (int c) {
private void consume (int c) throws IOException {
if (pos == BUF_LEN) {
lineBuf.append (buf);
pos = 0;
@ -138,13 +148,22 @@ class Request {
len = 1;
firstc = c;
}
long hsize = startLine.length() + 32L;
while (firstc != LF && firstc != CR && firstc >= 0) {
int keyend = -1;
int c;
boolean inKey = firstc > ' ';
s[len++] = (char) firstc;
hsize = hsize + 1;
parseloop:{
// We start parsing for a new name value pair here.
// The max header size includes an overhead of 32 bytes per
// name value pair.
// See SETTINGS_MAX_HEADER_LIST_SIZE, RFC 9113, section 6.5.2.
long maxRemaining = maxReqHeaderSize > 0
? maxReqHeaderSize - hsize - 32
: Long.MAX_VALUE;
while ((c = is.read()) >= 0) {
switch (c) {
/*fallthrough*/
@ -178,6 +197,11 @@ class Request {
s = ns;
}
s[len++] = (char) c;
if (maxReqHeaderSize > 0 && len > maxRemaining) {
throw new IOException("Maximum header (" +
"sun.net.httpserver.maxReqHeaderSize) exceeded, " +
ServerConfig.getMaxReqHeaderSize() + ".");
}
}
firstc = -1;
}
@ -205,6 +229,13 @@ class Request {
"sun.net.httpserver.maxReqHeaders) exceeded, " +
ServerConfig.getMaxReqHeaders() + ".");
}
hsize = hsize + len + 32;
if (maxReqHeaderSize > 0 && hsize > maxReqHeaderSize) {
throw new IOException("Maximum header (" +
"sun.net.httpserver.maxReqHeaderSize) exceeded, " +
ServerConfig.getMaxReqHeaderSize() + ".");
}
if (k == null) { // Headers disallows null keys, use empty string
k = ""; // instead to represent invalid key
}

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2005, 2022, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2005, 2024, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
@ -49,6 +49,7 @@ class ServerConfig {
// timing out request/response if max request/response time is configured
private static final long DEFAULT_REQ_RSP_TIMER_TASK_SCHEDULE_MILLIS = 1000;
private static final int DEFAULT_MAX_REQ_HEADERS = 200;
private static final int DEFAULT_MAX_REQ_HEADER_SIZE = 380 * 1024;
private static final long DEFAULT_DRAIN_AMOUNT = 64 * 1024;
private static long idleTimerScheduleMillis;
@ -62,6 +63,9 @@ class ServerConfig {
private static int maxIdleConnections;
// The maximum number of request headers allowable
private static int maxReqHeaders;
// a maximum value for the header list size. This is the
// names size + values size + 32 bytes per field line
private static int maxReqHeadersSize;
// max time a request or response is allowed to take
private static long maxReqTime;
private static long maxRspTime;
@ -107,6 +111,14 @@ class ServerConfig {
maxReqHeaders = DEFAULT_MAX_REQ_HEADERS;
}
// a value <= 0 means unlimited
maxReqHeadersSize = Integer.getInteger(
"sun.net.httpserver.maxReqHeaderSize",
DEFAULT_MAX_REQ_HEADER_SIZE);
if (maxReqHeadersSize <= 0) {
maxReqHeadersSize = 0;
}
maxReqTime = Long.getLong("sun.net.httpserver.maxReqTime",
DEFAULT_MAX_REQ_TIME);
@ -215,6 +227,10 @@ class ServerConfig {
return maxReqHeaders;
}
static int getMaxReqHeaderSize() {
return maxReqHeadersSize;
}
/**
* @return Returns the maximum amount of time the server will wait for the request to be read
* completely. This method can return a value of 0 or negative to imply no maximum limit has

View File

@ -59,6 +59,7 @@ import java.io.PrintWriter;
import java.io.Writer;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.ProtocolException;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.URI;
@ -361,7 +362,7 @@ public class ExpectContinueTest implements HttpServerAdapters {
}
if (exceptionally && testThrowable != null) {
err.println("Finished exceptionally Test throwable: " + testThrowable);
assertEquals(IOException.class, testThrowable.getClass());
assertEquals(testThrowable.getClass(), ProtocolException.class);
} else if (exceptionally) {
throw new TestException("Expected case to finish with an IOException but testException is null");
} else if (resp != null) {

View File

@ -136,6 +136,7 @@ public class ShutdownNow implements HttpServerAdapters {
if (message.equals("shutdownNow")) return true;
// exception from cancelling an HTTP/2 stream
if (message.matches("Stream [0-9]+ cancelled")) return true;
if (message.contains("connection closed locally")) return true;
return false;
}

View File

@ -40,6 +40,7 @@ import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.ProtocolException;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpHeaders;
@ -195,7 +196,8 @@ public class PushPromiseContinuation {
client.sendAsync(hreq, HttpResponse.BodyHandlers.ofString(UTF_8), pph);
CompletionException t = expectThrows(CompletionException.class, () -> cf.join());
assertEquals(t.getCause().getClass(), IOException.class, "Expected an IOException but got " + t.getCause());
assertEquals(t.getCause().getClass(), ProtocolException.class,
"Expected a ProtocolException but got " + t.getCause());
System.err.println("Client received the following expected exception: " + t.getCause());
faultyServer.stop();
}
@ -222,7 +224,10 @@ public class PushPromiseContinuation {
static class Http2PushPromiseHeadersExchangeImpl extends Http2TestExchangeImpl {
Http2PushPromiseHeadersExchangeImpl(int streamid, String method, HttpHeaders reqheaders, HttpHeadersBuilder rspheadersBuilder, URI uri, InputStream is, SSLSession sslSession, BodyOutputStream os, Http2TestServerConnection conn, boolean pushAllowed) {
Http2PushPromiseHeadersExchangeImpl(int streamid, String method, HttpHeaders reqheaders,
HttpHeadersBuilder rspheadersBuilder, URI uri, InputStream is,
SSLSession sslSession, BodyOutputStream os,
Http2TestServerConnection conn, boolean pushAllowed) {
super(streamid, method, reqheaders, rspheadersBuilder, uri, is, sslSession, os, conn, pushAllowed);
}

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2018, 2023, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2018, 2024, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
@ -241,6 +241,7 @@ public interface HttpServerAdapters {
public abstract void close();
public abstract InetSocketAddress getRemoteAddress();
public abstract String getConnectionKey();
public abstract InetSocketAddress getLocalAddress();
public void serverPush(URI uri, HttpHeaders headers, byte[] body) {
ByteArrayInputStream bais = new ByteArrayInputStream(body);
serverPush(uri, headers, bais);
@ -303,7 +304,10 @@ public interface HttpServerAdapters {
public InetSocketAddress getRemoteAddress() {
return exchange.getRemoteAddress();
}
@Override
public InetSocketAddress getLocalAddress() {
return exchange.getLocalAddress();
}
@Override
public URI getRequestURI() { return exchange.getRequestURI(); }
@Override
@ -370,6 +374,10 @@ public interface HttpServerAdapters {
public InetSocketAddress getRemoteAddress() {
return exchange.getRemoteAddress();
}
@Override
public InetSocketAddress getLocalAddress() {
return exchange.getLocalAddress();
}
@Override
public String getConnectionKey() {

View File

@ -0,0 +1,174 @@
/*
* Copyright (c) 2015, 2023, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
package jdk.httpclient.test.lib.http2;
import java.util.function.*;
import jdk.internal.net.http.hpack.Encoder;
import static java.lang.String.format;
import static java.util.Objects.requireNonNull;
import static jdk.internal.net.http.hpack.HPACK.Logger.Level.EXTRA;
import static jdk.internal.net.http.hpack.HPACK.Logger.Level.NORMAL;
public class HpackTestEncoder extends Encoder {
public HpackTestEncoder(int maxCapacity) {
super(maxCapacity);
}
/**
* Sets up the given header {@code (name, value)} with possibly sensitive
* value.
*
* <p> If the {@code value} is sensitive (think security, secrecy, etc.)
* this encoder will compress it using a special representation
* (see <a href="https://tools.ietf.org/html/rfc7541#section-6.2.3">6.2.3. Literal Header Field Never Indexed</a>).
*
* <p> Fixates {@code name} and {@code value} for the duration of encoding.
*
* @param name
* the name
* @param value
* the value
* @param sensitive
* whether or not the value is sensitive
*
* @throws NullPointerException
* if any of the arguments are {@code null}
* @throws IllegalStateException
* if the encoder hasn't fully encoded the previous header, or
* hasn't yet started to encode it
* @see #header(CharSequence, CharSequence)
* @see DecodingCallback#onDecoded(CharSequence, CharSequence, boolean)
*/
public void header(CharSequence name,
CharSequence value,
boolean sensitive) throws IllegalStateException {
if (sensitive || getMaxCapacity() == 0) {
super.header(name, value, true);
} else {
header(name, value, false, (n,v) -> false);
}
}
/**
* Sets up the given header {@code (name, value)} with possibly sensitive
* value.
*
* <p> If the {@code value} is sensitive (think security, secrecy, etc.)
* this encoder will compress it using a special representation
* (see <a href="https://tools.ietf.org/html/rfc7541#section-6.2.3">6.2.3. Literal Header Field Never Indexed</a>).
*
* <p> Fixates {@code name} and {@code value} for the duration of encoding.
*
* @param name
* the name
* @param value
* the value
* @param insertionPolicy
* a bipredicate to indicate whether a name value pair
* should be added to the dynamic table
*
* @throws NullPointerException
* if any of the arguments are {@code null}
* @throws IllegalStateException
* if the encoder hasn't fully encoded the previous header, or
* hasn't yet started to encode it
* @see #header(CharSequence, CharSequence)
* @see DecodingCallback#onDecoded(CharSequence, CharSequence, boolean)
*/
public void header(CharSequence name,
CharSequence value,
BiPredicate<CharSequence, CharSequence> insertionPolicy)
throws IllegalStateException {
header(name, value, false, insertionPolicy);
}
/**
* Sets up the given header {@code (name, value)} with possibly sensitive
* value.
*
* <p> If the {@code value} is sensitive (think security, secrecy, etc.)
* this encoder will compress it using a special representation
* (see <a href="https://tools.ietf.org/html/rfc7541#section-6.2.3">
* 6.2.3. Literal Header Field Never Indexed</a>).
*
* <p> Fixates {@code name} and {@code value} for the duration of encoding.
*
* @param name
* the name
* @param value
* the value
* @param sensitive
* whether or not the value is sensitive
* @param insertionPolicy
* a bipredicate to indicate whether a name value pair
* should be added to the dynamic table
*
* @throws NullPointerException
* if any of the arguments are {@code null}
* @throws IllegalStateException
* if the encoder hasn't fully encoded the previous header, or
* hasn't yet started to encode it
* @see #header(CharSequence, CharSequence)
* @see DecodingCallback#onDecoded(CharSequence, CharSequence, boolean)
*/
public void header(CharSequence name,
CharSequence value,
boolean sensitive,
BiPredicate<CharSequence, CharSequence> insertionPolicy)
throws IllegalStateException {
if (sensitive == true || getMaxCapacity() == 0 || !insertionPolicy.test(name, value)) {
super.header(name, value, sensitive);
return;
}
var logger = logger();
// Arguably a good balance between complexity of implementation and
// efficiency of encoding
requireNonNull(name, "name");
requireNonNull(value, "value");
var t = getHeaderTable();
int index = tableIndexOf(name, value);
if (logger.isLoggable(NORMAL)) {
logger.log(NORMAL, () -> format("encoding with indexing ('%s', '%s'): index:%s",
name, value, index));
}
if (index > 0) {
indexed(index);
} else {
boolean huffmanValue = isHuffmanBetterFor(value);
if (index < 0) {
literalWithIndexing(-index, value, huffmanValue);
} else {
boolean huffmanName = isHuffmanBetterFor(name);
literalWithIndexing(name, huffmanName, value, huffmanValue);
}
}
}
protected int calculateCapacity(int maxCapacity) {
return maxCapacity;
}
}

View File

@ -29,9 +29,12 @@ import java.io.OutputStream;
import java.net.URI;
import java.net.InetSocketAddress;
import java.net.http.HttpHeaders;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.function.BiPredicate;
import javax.net.ssl.SSLSession;
import jdk.internal.net.http.common.HttpHeadersBuilder;
import jdk.internal.net.http.frame.Http2Frame;
public interface Http2TestExchange {
@ -53,6 +56,12 @@ public interface Http2TestExchange {
void sendResponseHeaders(int rCode, long responseLength) throws IOException;
default void sendResponseHeaders(int rCode, long responseLength,
BiPredicate<CharSequence, CharSequence> insertionPolicy)
throws IOException {
sendResponseHeaders(rCode, responseLength);
}
InetSocketAddress getRemoteAddress();
int getResponseCode();
@ -65,6 +74,10 @@ public interface Http2TestExchange {
void serverPush(URI uri, HttpHeaders headers, InputStream content);
default void sendFrames(List<Http2Frame> frames) throws IOException {
throw new UnsupportedOperationException("not implemented");
}
/**
* Send a PING on this exchanges connection, and completes the returned CF
* with the number of milliseconds it took to get a valid response.

View File

@ -27,6 +27,7 @@ import jdk.httpclient.test.lib.http2.Http2TestServerConnection.ResponseHeaders;
import jdk.internal.net.http.common.HttpHeadersBuilder;
import jdk.internal.net.http.frame.HeaderFrame;
import jdk.internal.net.http.frame.HeadersFrame;
import jdk.internal.net.http.frame.Http2Frame;
import jdk.internal.net.http.frame.ResetFrame;
import javax.net.ssl.SSLSession;
@ -39,6 +40,7 @@ import java.net.http.HttpHeaders;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.function.BiPredicate;
public class Http2TestExchangeImpl implements Http2TestExchange {
@ -132,8 +134,13 @@ public class Http2TestExchangeImpl implements Http2TestExchange {
return os;
}
@Override
public void sendResponseHeaders(int rCode, long responseLength) throws IOException {
sendResponseHeaders(rCode, responseLength, (n,v) -> false);
}
@Override
public void sendResponseHeaders(int rCode, long responseLength,
BiPredicate<CharSequence, CharSequence> insertionPolicy)
throws IOException {
// Do not set Content-Length for 100, and do not set END_STREAM
if (rCode == 100) responseLength = 0;
@ -147,7 +154,7 @@ public class Http2TestExchangeImpl implements Http2TestExchange {
HttpHeaders headers = rspheadersBuilder.build();
ResponseHeaders response
= new ResponseHeaders(headers);
= new ResponseHeaders(headers, insertionPolicy);
response.streamid(streamid);
response.setFlag(HeaderFrame.END_HEADERS);
@ -172,6 +179,11 @@ public class Http2TestExchangeImpl implements Http2TestExchange {
conn.outputQ.put(response);
}
@Override
public void sendFrames(List<Http2Frame> frames) throws IOException {
conn.sendFrames(frames);
}
@Override
public InetSocketAddress getRemoteAddress() {
return (InetSocketAddress) conn.socket.getRemoteSocketAddress();

View File

@ -24,6 +24,8 @@
package jdk.httpclient.test.lib.http2;
import jdk.internal.net.http.common.HttpHeadersBuilder;
import jdk.internal.net.http.common.Log;
import jdk.internal.net.http.frame.ContinuationFrame;
import jdk.internal.net.http.frame.DataFrame;
import jdk.internal.net.http.frame.ErrorFrame;
import jdk.internal.net.http.frame.FramesDecoder;
@ -80,6 +82,7 @@ import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.locks.ReentrantLock;
import java.util.function.BiPredicate;
import java.util.function.Consumer;
import java.util.function.Predicate;
@ -87,6 +90,7 @@ import java.util.function.Predicate;
import static java.nio.charset.StandardCharsets.ISO_8859_1;
import static java.nio.charset.StandardCharsets.UTF_8;
import static jdk.internal.net.http.frame.ErrorFrame.REFUSED_STREAM;
import static jdk.internal.net.http.frame.SettingsFrame.DEFAULT_MAX_FRAME_SIZE;
import static jdk.internal.net.http.frame.SettingsFrame.HEADER_TABLE_SIZE;
/**
@ -105,7 +109,7 @@ public class Http2TestServerConnection {
final Http2TestExchangeSupplier exchangeSupplier;
final InputStream is;
final OutputStream os;
volatile Encoder hpackOut;
volatile HpackTestEncoder hpackOut;
volatile Decoder hpackIn;
volatile SettingsFrame clientSettings;
final SettingsFrame serverSettings;
@ -421,7 +425,9 @@ public class Http2TestServerConnection {
}
public int getMaxFrameSize() {
return clientSettings.getParameter(SettingsFrame.MAX_FRAME_SIZE);
var max = clientSettings.getParameter(SettingsFrame.MAX_FRAME_SIZE);
if (max <= 0) max = DEFAULT_MAX_FRAME_SIZE;
return max;
}
/** Sends a pre-canned HTTP/1.1 response. */
@ -482,7 +488,7 @@ public class Http2TestServerConnection {
//System.out.println("ServerSettings: " + serverSettings);
//System.out.println("ClientSettings: " + clientSettings);
hpackOut = new Encoder(serverSettings.getParameter(HEADER_TABLE_SIZE));
hpackOut = new HpackTestEncoder(serverSettings.getParameter(HEADER_TABLE_SIZE));
hpackIn = new Decoder(clientSettings.getParameter(HEADER_TABLE_SIZE));
if (!secure) {
@ -812,6 +818,14 @@ public class Http2TestServerConnection {
}
}
public void sendFrames(List<Http2Frame> frames) throws IOException {
synchronized (outputQ) {
for (var frame : frames) {
outputQ.put(frame);
}
}
}
protected HttpHeadersBuilder createNewHeadersBuilder() {
return new HttpHeadersBuilder();
}
@ -938,26 +952,38 @@ public class Http2TestServerConnection {
return (streamid & 0x01) == 0x00;
}
final ReentrantLock headersLock = new ReentrantLock();
/** Encodes an group of headers, without any ordering guarantees. */
public List<ByteBuffer> encodeHeaders(HttpHeaders headers) {
return encodeHeaders(headers, (n,v) -> false);
}
public List<ByteBuffer> encodeHeaders(HttpHeaders headers,
BiPredicate<CharSequence, CharSequence> insertionPolicy) {
List<ByteBuffer> buffers = new LinkedList<>();
ByteBuffer buf = getBuffer();
boolean encoded;
for (Map.Entry<String, List<String>> entry : headers.map().entrySet()) {
List<String> values = entry.getValue();
String key = entry.getKey().toLowerCase();
for (String value : values) {
do {
hpackOut.header(key, value);
encoded = hpackOut.encode(buf);
if (!encoded) {
buf.flip();
buffers.add(buf);
buf = getBuffer();
}
} while (!encoded);
headersLock.lock();
try {
for (Map.Entry<String, List<String>> entry : headers.map().entrySet()) {
List<String> values = entry.getValue();
String key = entry.getKey().toLowerCase();
for (String value : values) {
hpackOut.header(key, value, insertionPolicy);
do {
encoded = hpackOut.encode(buf);
if (!encoded && !buf.hasRemaining()) {
buf.flip();
buffers.add(buf);
buf = getBuffer();
}
} while (!encoded);
}
}
} finally {
headersLock.unlock();
}
buf.flip();
buffers.add(buf);
@ -970,18 +996,23 @@ public class Http2TestServerConnection {
ByteBuffer buf = getBuffer();
boolean encoded;
for (Map.Entry<String, String> entry : headers) {
String value = entry.getValue();
String key = entry.getKey().toLowerCase();
do {
headersLock.lock();
try {
for (Map.Entry<String, String> entry : headers) {
String value = entry.getValue();
String key = entry.getKey().toLowerCase();
hpackOut.header(key, value);
encoded = hpackOut.encode(buf);
if (!encoded) {
buf.flip();
buffers.add(buf);
buf = getBuffer();
}
} while (!encoded);
do {
encoded = hpackOut.encode(buf);
if (!encoded && !buf.hasRemaining()) {
buf.flip();
buffers.add(buf);
buf = getBuffer();
}
} while (!encoded);
}
} finally {
headersLock.unlock();
}
buf.flip();
buffers.add(buf);
@ -1008,10 +1039,50 @@ public class Http2TestServerConnection {
break;
} else throw x;
}
if (frame instanceof ResponseHeaders) {
ResponseHeaders rh = (ResponseHeaders)frame;
HeadersFrame hf = new HeadersFrame(rh.streamid(), rh.getFlags(), encodeHeaders(rh.headers));
writeFrame(hf);
if (frame instanceof ResponseHeaders rh) {
var buffers = encodeHeaders(rh.headers, rh.insertionPolicy);
int maxFrameSize = Math.min(rh.getMaxFrameSize(), getMaxFrameSize() - 64);
int next = 0;
int cont = 0;
do {
// If the total size of headers exceeds the max frame
// size we need to split the headers into one
// HeadersFrame + N x ContinuationFrames
int remaining = maxFrameSize;
var list = new ArrayList<ByteBuffer>(buffers.size());
for (; next < buffers.size(); next++) {
var b = buffers.get(next);
var len = b.remaining();
if (!b.hasRemaining()) continue;
if (len <= remaining) {
remaining -= len;
list.add(b);
} else {
if (next == 0) {
list.add(b.slice(b.position(), remaining));
b.position(b.position() + remaining);
remaining = 0;
}
break;
}
}
int flags = rh.getFlags();
if (next != buffers.size()) {
flags = flags & ~HeadersFrame.END_HEADERS;
}
if (cont > 0) {
flags = flags & ~HeadersFrame.END_STREAM;
}
HeaderFrame hf = cont == 0
? new HeadersFrame(rh.streamid(), flags, list)
: new ContinuationFrame(rh.streamid(), flags, list);
if (Log.headers()) {
// avoid too much chatter: log only if Log.headers() is enabled
System.err.println("TestServer writing " + hf);
}
writeFrame(hf);
cont++;
} while (next < buffers.size());
} else if (frame instanceof OutgoingPushPromise) {
handlePush((OutgoingPushPromise)frame);
} else
@ -1322,11 +1393,29 @@ public class Http2TestServerConnection {
// for the hashmap.
public static class ResponseHeaders extends Http2Frame {
HttpHeaders headers;
final HttpHeaders headers;
final BiPredicate<CharSequence, CharSequence> insertionPolicy;
final int maxFrameSize;
public ResponseHeaders(HttpHeaders headers) {
this(headers, (n,v) -> false);
}
public ResponseHeaders(HttpHeaders headers, BiPredicate<CharSequence, CharSequence> insertionPolicy) {
this(headers, insertionPolicy, Integer.MAX_VALUE);
}
public ResponseHeaders(HttpHeaders headers,
BiPredicate<CharSequence, CharSequence> insertionPolicy,
int maxFrameSize) {
super(0, 0);
this.headers = headers;
this.insertionPolicy = insertionPolicy;
this.maxFrameSize = maxFrameSize;
}
public int getMaxFrameSize() {
return maxFrameSize;
}
}