From 836d09034338c388c2a314e40a7f74e691ead48e Mon Sep 17 00:00:00 2001 From: Alexey Bakhtin <abakhtin@openjdk.org> Date: Fri, 27 Sep 2024 09:18:37 -0700 Subject: [PATCH] 8328286: Enhance HTTP client Reviewed-by: mbalao Backport-of: cf8dc79f392c8ec3414d8b36803f026852c4e386 --- .../java/net/doc-files/net-properties.html | 9 + .../classes/sun/net/www/MessageHeader.java | 55 ++++ .../www/protocol/http/HttpURLConnection.java | 23 +- src/java.base/share/conf/net.properties | 17 ++ .../jdk/internal/net/http/Exchange.java | 38 ++- .../internal/net/http/Http1HeaderParser.java | 49 ++- .../internal/net/http/Http2ClientImpl.java | 79 +++-- .../internal/net/http/Http2Connection.java | 281 ++++++++++++++---- .../jdk/internal/net/http/HttpClientImpl.java | 4 +- .../internal/net/http/HttpRequestImpl.java | 4 +- .../net/http/ResponseBodyHandlers.java | 12 +- .../classes/jdk/internal/net/http/Stream.java | 114 ++++++- .../net/http/common/HeaderDecoder.java | 6 +- .../jdk/internal/net/http/common/Utils.java | 13 + .../jdk/internal/net/http/hpack/Decoder.java | 80 ++++- .../net/http/hpack/DecodingCallback.java | 14 + .../jdk/internal/net/http/hpack/Encoder.java | 14 +- .../classes/sun/net/httpserver/Request.java | 35 ++- .../sun/net/httpserver/ServerConfig.java | 18 +- .../http2/PushPromiseContinuation.java | 9 +- .../test/lib/common/HttpServerAdapters.java | 12 +- .../test/lib/http2/HpackTestEncoder.java | 174 +++++++++++ .../test/lib/http2/Http2TestExchange.java | 13 + .../test/lib/http2/Http2TestExchangeImpl.java | 16 +- .../lib/http2/Http2TestServerConnection.java | 158 +++++++--- 25 files changed, 1074 insertions(+), 173 deletions(-) create mode 100644 test/jdk/java/net/httpclient/lib/jdk/httpclient/test/lib/http2/HpackTestEncoder.java diff --git a/src/java.base/share/classes/java/net/doc-files/net-properties.html b/src/java.base/share/classes/java/net/doc-files/net-properties.html index b7ed2f9924f..2c10d18a46e 100644 --- a/src/java.base/share/classes/java/net/doc-files/net-properties.html +++ b/src/java.base/share/classes/java/net/doc-files/net-properties.html @@ -240,6 +240,15 @@ of proxies.</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> diff --git a/src/java.base/share/classes/sun/net/www/MessageHeader.java b/src/java.base/share/classes/sun/net/www/MessageHeader.java index 22b16407dd2..4a60ab482c7 100644 --- a/src/java.base/share/classes/sun/net/www/MessageHeader.java +++ b/src/java.base/share/classes/sun/net/www/MessageHeader.java @@ -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.*; @@ -46,11 +48,32 @@ 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); } @@ -477,10 +500,28 @@ 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 { @@ -494,7 +535,15 @@ 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 ':': @@ -528,6 +577,9 @@ class MessageHeader { s = ns; } s[len++] = (char) c; + if (maxHeaderSize > 0 && len > maxRemaining) { + checkMaxHeaderSize(len); + } } firstc = -1; } @@ -549,6 +601,9 @@ 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); } } diff --git a/src/java.base/share/classes/sun/net/www/protocol/http/HttpURLConnection.java b/src/java.base/share/classes/sun/net/www/protocol/http/HttpURLConnection.java index 7dc9f99eb18..288ebfd8504 100644 --- a/src/java.base/share/classes/sun/net/www/protocol/http/HttpURLConnection.java +++ b/src/java.base/share/classes/sun/net/www/protocol/http/HttpURLConnection.java @@ -171,6 +171,8 @@ public class HttpURLConnection extends java.net.HttpURLConnection { */ private static int bufSize4ES = 0; + private static final int maxHeaderSize; + /* * Restrict setting of request headers through the public api * consistent with JavaScript XMLHttpRequest2 with a few @@ -285,6 +287,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"; @@ -759,7 +774,7 @@ public class HttpURLConnection extends java.net.HttpURLConnection { } ps = (PrintStream) http.getOutputStream(); connected=true; - responses = new MessageHeader(); + responses = new MessageHeader(maxHeaderSize); setRequests=false; writeRequests(); } @@ -917,7 +932,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; @@ -2872,7 +2887,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. @@ -3062,7 +3077,7 @@ public class HttpURLConnection extends java.net.HttpURLConnection { } catch (IOException e) { } } responseCode = -1; - responses = new MessageHeader(); + responses = new MessageHeader(maxHeaderSize); connected = false; } diff --git a/src/java.base/share/conf/net.properties b/src/java.base/share/conf/net.properties index 67f294355a1..2aa9a9630be 100644 --- a/src/java.base/share/conf/net.properties +++ b/src/java.base/share/conf/net.properties @@ -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. diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/Exchange.java b/src/java.net.http/share/classes/jdk/internal/net/http/Exchange.java index c2f8390e461..60d2b2b410a 100644 --- a/src/java.net.http/share/classes/jdk/internal/net/http/Exchange.java +++ b/src/java.net.http/share/classes/jdk/internal/net/http/Exchange.java @@ -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 @@ -40,6 +40,7 @@ import java.util.Map; import java.util.Optional; import java.util.concurrent.CompletableFuture; import java.util.concurrent.Executor; +import java.util.concurrent.atomic.AtomicInteger; import java.util.function.Function; import java.net.http.HttpClient; import java.net.http.HttpHeaders; @@ -68,6 +69,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; @@ -92,6 +95,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; @@ -315,7 +320,7 @@ final class Exchange<T> { public void h2Upgrade() { upgrading = true; - request.setH2Upgrade(client.client2()); + request.setH2Upgrade(this); } synchronized IOException getCancelCause() { @@ -416,6 +421,7 @@ 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); CompletableFuture<Response> cf = @@ -492,12 +498,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); @@ -759,6 +773,14 @@ final class Exchange<T> { return multi.version(); } + boolean pushEnabled() { + return pushGroup != null; + } + + String h2cSettingsStrings() { + return client.client2().getSettingsString(pushEnabled()); + } + String dbgString() { return dbgTag; } diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/Http1HeaderParser.java b/src/java.net.http/share/classes/jdk/internal/net/http/Http1HeaderParser.java index 669c173e3f8..8c796193015 100644 --- a/src/java.net.http/share/classes/jdk/internal/net/http/Http1HeaderParser.java +++ b/src/java.net.http/share/classes/jdk/internal/net/http/Http1HeaderParser.java @@ -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; } } diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/Http2ClientImpl.java b/src/java.net.http/share/classes/jdk/internal/net/http/Http2ClientImpl.java index 9f74a70d318..0f2db7738fc 100644 --- a/src/java.net.http/share/classes/jdk/internal/net/http/Http2ClientImpl.java +++ b/src/java.net.http/share/classes/jdk/internal/net/http/Http2ClientImpl.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2015, 2018, 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 @@ -38,7 +38,6 @@ import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.CompletableFuture; -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; @@ -48,6 +47,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 @@ -94,14 +94,19 @@ class Http2ClientImpl { CompletableFuture<Http2Connection> getConnectionFor(HttpRequestImpl req, Exchange<?> exchange) { String key = Http2Connection.keyFor(req); + boolean pushEnabled = exchange.pushEnabled(); synchronized (this) { Http2Connection connection = connections.get(key); if (connection != null) { try { - if (connection.closed || !connection.reserveStream(true)) { + if (connection.closed + || !connection.reserveStream(true, pushEnabled)) { if (debug.on()) - debug.log("removing found closed or closing connection: %s", connection); + debug.log("removing connection from pool since " + + "it couldn't be reserved for use%s: %s", + pushEnabled ? " with server push enabled" : + "", connection); deleteConnection(connection); } else { // fast path if connection already exists @@ -128,7 +133,7 @@ class Http2ClientImpl { synchronized (Http2ClientImpl.this) { if (conn != null) { try { - conn.reserveStream(true); + conn.reserveStream(true, exchange.pushEnabled()); } catch (IOException e) { throw new UncheckedIOException(e); // shouldn't happen } @@ -161,10 +166,21 @@ class Http2ClientImpl { synchronized(this) { 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); @@ -204,8 +220,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(); @@ -215,14 +231,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 @@ -243,7 +252,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( @@ -252,14 +272,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, @@ -272,6 +293,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; } } diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/Http2Connection.java b/src/java.net.http/share/classes/jdk/internal/net/http/Http2Connection.java index 920e5cefa95..f363231e1c8 100644 --- a/src/java.net.http/share/classes/jdk/internal/net/http/Http2Connection.java +++ b/src/java.net.http/share/classes/jdk/internal/net/http/Http2Connection.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2015, 2021, 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 @@ -29,6 +29,7 @@ import java.io.EOFException; import java.io.IOException; import java.io.UncheckedIOException; import java.net.InetSocketAddress; +import java.net.ProtocolException; import java.net.URI; import java.nio.ByteBuffer; import java.nio.charset.StandardCharsets; @@ -44,6 +45,8 @@ import java.util.concurrent.ConcurrentMap; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.Flow; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; import java.util.function.Function; import java.util.function.Supplier; import javax.net.ssl.SSLEngine; @@ -245,6 +248,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; + } + } + } + } + volatile boolean closed; //------------------------------------- @@ -265,7 +307,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; @@ -279,12 +321,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; + 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 @@ -292,19 +346,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); @@ -323,14 +379,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); @@ -363,7 +421,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 @@ -387,7 +446,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); @@ -402,13 +462,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()); @@ -431,15 +493,21 @@ 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 - synchronized boolean reserveStream(boolean clientInitiated) throws IOException { + synchronized boolean reserveStream(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.deleteConnection(this); return false; - } else if (!clientInitiated && (lastReservedServerStreamid + 2) >= MAX_SERVER_STREAM_ID) { + } else if (!clientInitiated && (lastReservedServerStreamid >= MAX_SERVER_STREAM_ID - 2)) { setFinalStream(); client2.deleteConnection(this); return false; @@ -464,6 +532,10 @@ class Http2Connection { return true; } + synchronized boolean shouldClose() { + return finalStream() && streams.isEmpty(); + } + /** * Throws an IOException if h2 was not negotiated */ @@ -591,6 +663,10 @@ class Http2Connection { return this.key; } + public boolean serverPushEnabled() { + return clientSettings.getParameter(SettingsFrame.ENABLE_PUSH) == 1; + } + boolean offerConnection() { return client2.offerConnection(this); } @@ -689,7 +765,7 @@ class Http2Connection { } Throwable getRecordedCause() { - return cause; + return cause.get(); } void shutdown(Throwable t) { @@ -699,6 +775,7 @@ class Http2Connection { if (closed == true) return; closed = true; } + cause.compareAndSet(null, t); if (Log.errors()) { if (!(t instanceof EOFException) || isActive()) { Log.logError(t); @@ -706,9 +783,8 @@ class Http2Connection { Log.logError("Shutting down connection: {0}", t.getMessage()); } } - Throwable initialCause = this.cause; - if (initialCause == null) this.cause = t; client2.deleteConnection(this); + subscriber.stop(cause.get()); for (Stream<?> s : streams.values()) { try { s.connectionClosing(t); @@ -762,17 +838,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; } @@ -800,29 +898,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; } @@ -830,7 +940,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; @@ -843,6 +953,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 (closed) return; if (debug.on()) { @@ -867,38 +987,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 { @@ -1019,9 +1166,15 @@ class Http2Connection { private void protocolError(int errorCode, String msg) throws IOException { + String protocolError = "protocol error" + (msg == null?"":(": " + msg)); + ProtocolException protocolException = + new ProtocolException(protocolError); + 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) @@ -1161,7 +1314,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); } <T> void putStream(Stream<T> stream, int streamid) { @@ -1223,16 +1376,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); + } } } } @@ -1295,6 +1450,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 { @@ -1353,7 +1510,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 = @@ -1374,10 +1531,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(); @@ -1412,6 +1568,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); @@ -1420,19 +1577,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(); } @@ -1444,6 +1603,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(); + } + } } synchronized boolean isActive() { diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/HttpClientImpl.java b/src/java.net.http/share/classes/jdk/internal/net/http/HttpClientImpl.java index 671ae9b7ca4..bb0dd914afb 100644 --- a/src/java.net.http/share/classes/jdk/internal/net/http/HttpClientImpl.java +++ b/src/java.net.http/share/classes/jdk/internal/net/http/HttpClientImpl.java @@ -584,7 +584,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 { diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/HttpRequestImpl.java b/src/java.net.http/share/classes/jdk/internal/net/http/HttpRequestImpl.java index d2b908dffa5..27e65446844 100644 --- a/src/java.net.http/share/classes/jdk/internal/net/http/HttpRequestImpl.java +++ b/src/java.net.http/share/classes/jdk/internal/net/http/HttpRequestImpl.java @@ -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", "h2c"); - systemHeadersBuilder.setHeader("HTTP2-Settings", h2client.getSettingsString()); + systemHeadersBuilder.setHeader("HTTP2-Settings", exchange.h2cSettingsStrings()); } @Override diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/ResponseBodyHandlers.java b/src/java.net.http/share/classes/jdk/internal/net/http/ResponseBodyHandlers.java index 66d89ae1fc5..22e03238d21 100644 --- a/src/java.net.http/share/classes/jdk/internal/net/http/ResponseBodyHandlers.java +++ b/src/java.net.http/share/classes/jdk/internal/net/http/ResponseBodyHandlers.java @@ -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; diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/Stream.java b/src/java.net.http/share/classes/jdk/internal/net/http/Stream.java index e781467db6b..0b9cf64d490 100644 --- a/src/java.net.http/share/classes/jdk/internal/net/http/Stream.java +++ b/src/java.net.http/share/classes/jdk/internal/net/http/Stream.java @@ -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.nio.ByteBuffer; import java.util.ArrayList; @@ -41,6 +42,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.function.BiPredicate; import java.net.http.HttpClient; @@ -48,10 +50,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. * @@ -136,6 +141,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) @@ -460,28 +468,62 @@ 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() 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) + if (responseCode < 100 || responseCode > 199) { this.finalResponseCodeReceived = true; + } else { + protocolErrorMsg = checkInterimResponseCountExceeded(); + } + + if (protocolErrorMsg != null) { + if (debug.on()) { + debug.log(protocolErrorMsg); + } + 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()) { @@ -500,6 +542,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(); } @@ -1057,7 +1108,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() @@ -1242,7 +1293,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); @@ -1311,6 +1362,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. @@ -1320,16 +1372,17 @@ class Stream<T> extends ExchangeImpl<T> { 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() { @@ -1422,7 +1475,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; @@ -1504,7 +1566,9 @@ class Stream<T> extends ExchangeImpl<T> { return connection.dbgString() + "/Stream("+streamid+")"; } - private class HeadersConsumer extends ValidatingHeadersConsumer { + private class HeadersConsumer extends ValidatingHeadersConsumer implements DecodingCallback { + + boolean maxHeaderListSizeReached; @Override public void reset() { @@ -1517,6 +1581,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(); @@ -1539,6 +1606,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(); + } + } } private static final VarHandle STREAM_STATE; diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/common/HeaderDecoder.java b/src/java.net.http/share/classes/jdk/internal/net/http/common/HeaderDecoder.java index 62d03844d2e..d81f52e6630 100644 --- a/src/java.net.http/share/classes/jdk/internal/net/http/common/HeaderDecoder.java +++ b/src/java.net.http/share/classes/jdk/internal/net/http/common/HeaderDecoder.java @@ -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() { diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/common/Utils.java b/src/java.net.http/share/classes/jdk/internal/net/http/common/Utils.java index 0e0c9a3eb9e..323042de3d7 100644 --- a/src/java.net.http/share/classes/jdk/internal/net/http/common/Utils.java +++ b/src/java.net.http/share/classes/jdk/internal/net/http/common/Utils.java @@ -541,6 +541,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()); diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/hpack/Decoder.java b/src/java.net.http/share/classes/jdk/internal/net/http/hpack/Decoder.java index 9d57a734ac3..881be12c67c 100644 --- a/src/java.net.http/share/classes/jdk/internal/net/http/hpack/Decoder.java +++ b/src/java.net.http/share/classes/jdk/internal/net/http/hpack/Decoder.java @@ -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; } } diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/hpack/DecodingCallback.java b/src/java.net.http/share/classes/jdk/internal/net/http/hpack/DecodingCallback.java index 5e9df860feb..228f9bf0206 100644 --- a/src/java.net.http/share/classes/jdk/internal/net/http/hpack/DecodingCallback.java +++ b/src/java.net.http/share/classes/jdk/internal/net/http/hpack/DecodingCallback.java @@ -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)); + } } diff --git a/src/java.net.http/share/classes/jdk/internal/net/http/hpack/Encoder.java b/src/java.net.http/share/classes/jdk/internal/net/http/hpack/Encoder.java index 4188937b1ad..c603e917ca4 100644 --- a/src/java.net.http/share/classes/jdk/internal/net/http/hpack/Encoder.java +++ b/src/java.net.http/share/classes/jdk/internal/net/http/hpack/Encoder.java @@ -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; + } } diff --git a/src/jdk.httpserver/share/classes/sun/net/httpserver/Request.java b/src/jdk.httpserver/share/classes/sun/net/httpserver/Request.java index 03e2a9ee8f5..0e136d80437 100644 --- a/src/jdk.httpserver/share/classes/sun/net/httpserver/Request.java +++ b/src/jdk.httpserver/share/classes/sun/net/httpserver/Request.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2005, 2021, 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 @@ -42,8 +42,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 { @@ -76,6 +78,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) { @@ -88,20 +91,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; @@ -139,13 +149,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*/ @@ -179,6 +198,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; } @@ -206,6 +230,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 } diff --git a/src/jdk.httpserver/share/classes/sun/net/httpserver/ServerConfig.java b/src/jdk.httpserver/share/classes/sun/net/httpserver/ServerConfig.java index 303db604a19..63d6a57258a 100644 --- a/src/jdk.httpserver/share/classes/sun/net/httpserver/ServerConfig.java +++ b/src/jdk.httpserver/share/classes/sun/net/httpserver/ServerConfig.java @@ -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; @@ -104,6 +108,14 @@ class ServerConfig { "sun.net.httpserver.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); @@ -212,6 +224,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 diff --git a/test/jdk/java/net/httpclient/http2/PushPromiseContinuation.java b/test/jdk/java/net/httpclient/http2/PushPromiseContinuation.java index 152b3d194eb..cc6bc1a9515 100644 --- a/test/jdk/java/net/httpclient/http2/PushPromiseContinuation.java +++ b/test/jdk/java/net/httpclient/http2/PushPromiseContinuation.java @@ -49,6 +49,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; @@ -192,7 +193,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(); } @@ -219,7 +221,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); } diff --git a/test/jdk/java/net/httpclient/lib/jdk/httpclient/test/lib/common/HttpServerAdapters.java b/test/jdk/java/net/httpclient/lib/jdk/httpclient/test/lib/common/HttpServerAdapters.java index 623644df5a6..6bbcc4c3741 100644 --- a/test/jdk/java/net/httpclient/lib/jdk/httpclient/test/lib/common/HttpServerAdapters.java +++ b/test/jdk/java/net/httpclient/lib/jdk/httpclient/test/lib/common/HttpServerAdapters.java @@ -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 @@ -240,6 +240,7 @@ public interface HttpServerAdapters { public abstract String getRequestMethod(); public abstract void close(); public abstract InetSocketAddress getRemoteAddress(); + public abstract InetSocketAddress getLocalAddress(); public void serverPush(URI uri, HttpHeaders headers, byte[] body) { ByteArrayInputStream bais = new ByteArrayInputStream(body); serverPush(uri, headers, bais); @@ -302,7 +303,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 @@ -363,6 +367,10 @@ public interface HttpServerAdapters { public InetSocketAddress getRemoteAddress() { return exchange.getRemoteAddress(); } + @Override + public InetSocketAddress getLocalAddress() { + return exchange.getLocalAddress(); + } @Override public URI getRequestURI() { return exchange.getRequestURI(); } diff --git a/test/jdk/java/net/httpclient/lib/jdk/httpclient/test/lib/http2/HpackTestEncoder.java b/test/jdk/java/net/httpclient/lib/jdk/httpclient/test/lib/http2/HpackTestEncoder.java new file mode 100644 index 00000000000..f54a4a766b8 --- /dev/null +++ b/test/jdk/java/net/httpclient/lib/jdk/httpclient/test/lib/http2/HpackTestEncoder.java @@ -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; + } + +} diff --git a/test/jdk/java/net/httpclient/lib/jdk/httpclient/test/lib/http2/Http2TestExchange.java b/test/jdk/java/net/httpclient/lib/jdk/httpclient/test/lib/http2/Http2TestExchange.java index 208a8d54e08..1a8b5d92af5 100644 --- a/test/jdk/java/net/httpclient/lib/jdk/httpclient/test/lib/http2/Http2TestExchange.java +++ b/test/jdk/java/net/httpclient/lib/jdk/httpclient/test/lib/http2/Http2TestExchange.java @@ -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. diff --git a/test/jdk/java/net/httpclient/lib/jdk/httpclient/test/lib/http2/Http2TestExchangeImpl.java b/test/jdk/java/net/httpclient/lib/jdk/httpclient/test/lib/http2/Http2TestExchangeImpl.java index 8f7c154e656..5ac6ab0fd26 100644 --- a/test/jdk/java/net/httpclient/lib/jdk/httpclient/test/lib/http2/Http2TestExchangeImpl.java +++ b/test/jdk/java/net/httpclient/lib/jdk/httpclient/test/lib/http2/Http2TestExchangeImpl.java @@ -26,6 +26,7 @@ package jdk.httpclient.test.lib.http2; 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 javax.net.ssl.SSLSession; import java.io.IOException; @@ -37,6 +38,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 { @@ -130,8 +132,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 { this.responseLength = responseLength; if (responseLength !=0 && rCode != 204 && !isHeadRequest()) { long clen = responseLength > 0 ? responseLength : 0; @@ -142,7 +149,7 @@ public class Http2TestExchangeImpl implements Http2TestExchange { HttpHeaders headers = rspheadersBuilder.build(); Http2TestServerConnection.ResponseHeaders response - = new Http2TestServerConnection.ResponseHeaders(headers); + = new Http2TestServerConnection.ResponseHeaders(headers, insertionPolicy); response.streamid(streamid); response.setFlag(HeaderFrame.END_HEADERS); @@ -156,6 +163,11 @@ public class Http2TestExchangeImpl implements Http2TestExchange { System.err.println("Sent response headers " + rCode); } + @Override + public void sendFrames(List<Http2Frame> frames) throws IOException { + conn.sendFrames(frames); + } + @Override public InetSocketAddress getRemoteAddress() { return (InetSocketAddress) conn.socket.getRemoteSocketAddress(); diff --git a/test/jdk/java/net/httpclient/lib/jdk/httpclient/test/lib/http2/Http2TestServerConnection.java b/test/jdk/java/net/httpclient/lib/jdk/httpclient/test/lib/http2/Http2TestServerConnection.java index 94f11e843ce..102c7a56721 100644 --- a/test/jdk/java/net/httpclient/lib/jdk/httpclient/test/lib/http2/Http2TestServerConnection.java +++ b/test/jdk/java/net/httpclient/lib/jdk/httpclient/test/lib/http2/Http2TestServerConnection.java @@ -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 @@ -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; @@ -78,10 +80,13 @@ import java.util.Random; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.ExecutorService; +import java.util.concurrent.locks.ReentrantLock; +import java.util.function.BiPredicate; import java.util.function.Consumer; import static java.nio.charset.StandardCharsets.ISO_8859_1; import static java.nio.charset.StandardCharsets.UTF_8; +import static jdk.internal.net.http.frame.SettingsFrame.DEFAULT_MAX_FRAME_SIZE; import static jdk.internal.net.http.frame.SettingsFrame.HEADER_TABLE_SIZE; /** @@ -100,7 +105,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; @@ -393,7 +398,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. */ @@ -454,7 +461,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) { @@ -740,6 +747,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(); } @@ -850,26 +865,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); @@ -882,18 +909,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); @@ -920,10 +952,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 @@ -1234,11 +1306,29 @@ public class Http2TestServerConnection { // for the hashmap. static class ResponseHeaders extends Http2Frame { - HttpHeaders headers; + final HttpHeaders headers; + final BiPredicate<CharSequence, CharSequence> insertionPolicy; - ResponseHeaders(HttpHeaders headers) { + 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; } } -- GitLab